Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
d2f797b
feat(constructive-blocks): add copy-in UI blocks skill
yyyyaaa May 30, 2026
11748de
fix(constructive-sdk): correct stale secureTableProvision, SDK import…
yyyyaaa May 30, 2026
6559571
fix(constructive-blocks): make check-sdk.mjs work on standard Next.js…
yyyyaaa May 30, 2026
71de93b
fix(skills): replace modules:['all'] with explicit module lists in pr…
yyyyaaa May 30, 2026
77c0aa1
feat(constructive-blocks): flow-selection guide + flows drift guard
yyyyaaa Jun 1, 2026
fd49876
fix(constructive-blocks): tuple-form modules + drift-guard parity
yyyyaaa Jun 1, 2026
2f8e4a5
fix(constructive-blocks): singular-normalise check-sdk models + decla…
yyyyaaa Jun 1, 2026
50bce57
Merge origin/main (33→20 skill consolidation + fixes) into feat/blocks
yyyyaaa Jun 1, 2026
51ad139
chore(blocks): regenerate flow catalog (5 defect fixes from 11-flow f…
yyyyaaa Jun 2, 2026
37209e7
chore(blocks): regenerate flow references with @constructive/ install…
yyyyaaa Jun 4, 2026
c4150e7
chore(blocks): regenerate flow refs with block-backend contract notes
yyyyaaa Jun 4, 2026
3aaef1b
docs(skills): Blocks<->domain-UI scope boundary (blocks=auth/account/…
yyyyaaa Jun 5, 2026
cfb147a
fix(check-sdk): only fail on backend symbols the block actually impor…
yyyyaaa Jun 5, 2026
d87b9f3
chore(skills): P2 hygiene — check-sdk contract advisories (WARN-only)…
yyyyaaa Jun 5, 2026
02919c7
check-sdk: accept a workspace root for --project and derive the app p…
yyyyaaa Jun 6, 2026
207fd9a
docs(constructive-blocks): make the build surface ambient — replace "…
yyyyaaa Jun 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .agents/skills/constructive-auth/references/auth-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const authDb = createAuthClient({ endpoint: 'http://auth.localhost:3000/graphql'

await authDb.mutation.signUp(
{ input: { email, password } },
{ select: { ok: true, errors: true } }
{ select: { result: { select: { id: true } } } }
).execute();
```

Expand Down Expand Up @@ -110,13 +110,17 @@ localStorage.setItem('device_token', r.outDeviceToken);
### Sign up (first device auto-approved)

```typescript
// `deviceToken` is a SignUpInput field; everything you read back is selected off
// `result` (a SignUpRecord) — there is no top-level field on SignUpPayload.
// `outDeviceToken` is only present when `devices_module` is installed (see §intro).
const result = await authDb.mutation.signUp(
{ input: { email, password, deviceToken: '<new-opaque-token>' } },
{ select: { outDeviceToken: true, accessToken: true } }
{ select: { result: { select: { accessToken: true, outDeviceToken: true } } } }
).execute();

// First device is auto-approved even when require_device_approval is on
localStorage.setItem('device_token', result.signUp.outDeviceToken);
// First device is auto-approved even when require_device_approval is on —
// persist the returned device token for future logins.
localStorage.setItem('device_token', result.signUp.result.outDeviceToken);
```

See [`constructive-platform/references/device-settings.md`](../../constructive-platform/references/device-settings.md) for the full composition matrix of device settings.
Expand Down
Binary file added .agents/skills/constructive-blocks.zip
Binary file not shown.
287 changes: 287 additions & 0 deletions .agents/skills/constructive-blocks/SKILL.md

Large diffs are not rendered by default.

114 changes: 114 additions & 0 deletions .agents/skills/constructive-blocks/references/binding-doctrine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Binding Doctrine

Condensation of the canonical **SDK Binding Contract** for in-skill use. Where any older blocks doc disagrees about data fetching, hooks, clients, providers, or endpoints, this wins. It supersedes the `@constructive-io/data` hybrid, the `<ConstructiveProvider>` model, and any pinned-SDK frame.

## 0. The doctrine

A block binds to the **per-application generated SDK** — the namespaced TypeScript client the *host app* produces with `@constructive-io/graphql-codegen` from *its own* PostGraphile endpoints — **not** to any pinned, hand-written, or pre-published SDK package. It imports generated **React Query hooks** from a convention path (`@/generated/<namespace>`) the host has aliased to its generated output. The block ships no network code of its own.

## 1. Why per-app, not pinned

A Constructive app's GraphQL surface is **dynamic** — a function of which pgpm modules are deployed, the app's `api_schemas` config, and `database_settings` flags. Two apps almost never expose the same operations, types, or field sets. A block pinned to one frozen `.d.ts` is correct for exactly one app and silently wrong for every other (the prior build's failure mode: guessed op names, wrong arg wrappers, wrong payload shapes). Codegen against the host's *live* endpoints encodes the exact operation kind, input shape, payload wrapper, and field names — a block written against the generated signatures is correct by construction.

## 2. Namespaces

Codegen emits one SDK per registered API (a row in `services_public.apis`; its `api_schemas` list the PostgreSQL schemas it exposes; each is reachable at its own subdomain). The four standard namespaces:

| Namespace | Subdomain | Schema set (current) |
|---|---|---|
| `auth` | `auth.` | `constructive_auth_public` + `users_public` + `user_identifiers_public` + `logging_public` |
| `admin` | `admin.` | `memberships_public` + `permissions_public` + `limits_public` + `invites_public` + `status_public` |
| `objects` | `objects.` | `object_store_public` + `object_tree_public` |
| `public` | `api.` | nearly all of the above combined |

**Routing blocks to a namespace:**

- Auth flows (sign-in, password, email/MFA, account, identity) → `auth`.
- Membership / invite / role / permission / limit / status → `admin`. (Invite *acceptance* mutations `submitAppInviteCode` / `submitOrgInviteCode` live in `invites_public`, reachable via `admin` or `public`.)
- File/object blocks → `objects`.
- A block needing ops from more than one schema set targets `public`, **or** imports from two namespaces. Prefer a single namespace per block; document any cross-namespace block in `requires.json` with multiple entries. The list is not closed — an app may register custom APIs.

## 3. Import convention (locked v1)

```tsx
'use client';
import { useSignInMutation } from '@/generated/auth';
import { useOrganizationMembersQuery } from '@/generated/admin';
```

A block **never** imports from a versioned SDK package name, never hardcodes a path like `src/graphql/auth-sdk/api`, and never writes its own `fetch`, GraphQL document, or client bootstrap.

> **Why a convention path, not an injected client?** Generated hooks are hard-bound to a module-level singleton (`getClient()`) — there is no `client` parameter on any hook. The only way a block and the host share one configured client is to import the *same generated module*. The `@/generated/<ns>` alias makes "the same module" a stable, app-agnostic name a block compiles against.

## 4. The override seam (portability)

The default path is the generated hook. Every block also accepts `onSubmit` (mutations) / `adapter` (queries) that **fully replaces** the network call, so the block stays usable on a non-Constructive backend. The block still owns form state, validation, error mapping, and notifications regardless. This is the one soft point in the binding; everything else here is the canonical path.

## 5. Generated hook anatomy

**Naming** (confirmed against real codegen output):

- Custom operations → `use<PascalOp>Mutation` (e.g. `useSignInMutation`, `useRequireStepUpMutation`). The previous plan assumed `useSignIn`; the real name is `useSignInMutation`.
- Table reads → `use<Plural>Query` / `use<Singular>Query` (e.g. `useUsersQuery`, `useUserQuery`).
- Table writes → `useCreate<Name>Mutation` / `useUpdate<Name>Mutation` / `useDelete<Name>Mutation`.

**React Query.** Every hook calls `useMutation`/`useQuery` and needs a `QueryClient` in the tree (the runtime supplies it). Each takes a `selection` field-picker plus standard React Query options:

```tsx
const signIn = useSignInMutation({
selection: { fields: { result: { select: { userId: true, mfaRequired: true } } } },
onSuccess: (data) => { /* data.signIn... */ },
});
await signIn.mutateAsync({ email, password, rememberMe });
```

**Per-namespace singleton.** Each SDK ships its own `configure(config)` / `getClient()` backed by a module-level instance. `configure()` must run **once per namespace** (auth and admin are separate singletons). There is **no** `client` prop on any hook. `OrmClientConfig = { endpoint?, headers?, fetch?, adapter?, realtime? }` — there is **no token-storage property**; auth is attached via `headers`/`fetch`/`adapter` (the runtime uses a `getToken`-driven adapter).

**Model accessor exists iff a `*Connection` type exists.** Codegen infers a table model accessor (`.findMany()` + the `use<Plural>Query` hook) only when the SDL has a `*Connection` object type for that table. Tables exposed only as private-schema views get no accessor and no list hook — only their explicit mutations.

**Op-shape branching** (how a block calls a hook):

- scalar / Connection return → flat-arg, no `select`, raw return.
- object payload return → `{ input }` + `{ select }`, read `.result`.
- table CRUD → `{ where, data }` with a `*Patch` data type (gated on a valid PK).

Always verify the real signature in the generated `.d.ts` / hook file — never guess.

## 6. The runtime block: `blocks-runtime`

One shipped registry item encapsulating host wiring so no human hand-writes provider boilerplate. It is a `registryDependency` of every data block and mounts, once at app root:

1. **One** `<QueryClientProvider>` (one shared `QueryClient` for all namespaces — the "two QueryClients" fear was an *unmounted-provider* artifact, not a real defect).
2. **Per-namespace `configure()`** for each namespace present, reading `NEXT_PUBLIC_<NS>_GRAPHQL_ENDPOINT` and attaching auth via a host `getToken` → `Authorization: Bearer <token>` adapter.

```tsx
<BlocksRuntime namespaces={['auth', 'admin']} getToken={() => tokenManager.getAccessToken()}>
{children}
</BlocksRuntime>
```

A block **never** mounts a provider or calls `configure()`. Tests mount the runtime (or mock the generated hook module) — never react-query directly.

## 7. Generating the SDK (`cnc codegen`)

```bash
cnc codegen --endpoint https://auth.<app-host>/graphql --react-query --orm -o src/generated/auth
cnc codegen --api-names auth,admin,public,objects --react-query --orm -o src/generated
cnc codegen --schema-file ./schemas/auth.graphql --react-query --orm -o src/generated/auth
```

`--react-query` **and** `--orm` are both required. `--dry-run` previews without writing (used by the staleness check). Sources are mutually exclusive: `--endpoint` | `--schema-file` | `--schema-dir` | `--api-names`/`--schemas` | `--config`. Output is never hand-edited (`@generated … DO NOT EDIT`); regeneration is the only correct change.

## 11. Compliance checklist

A reviewer checking a block MUST confirm:

1. **Generated-hook import** — data blocks import from `@/generated/<ns>`, never a package name or hardcoded generated path.
2. **No network code** — no `fetch`, no GraphQL document strings, no `configure()`/`getClient()`, no `QueryClientProvider` in any block file.
3. **Generated hook names** — calls use real generated names (`use<Op>Mutation`, `use<Plural>Query`) and pass a `selection`.
4. **Override seam** — `onSubmit`/`adapter` present and fully replaces the default hook.
5. **`requires.json`** — every data block ships a co-located manifest; presentational blocks ship none.
6. **Runtime dependency** — data blocks list `blocks-runtime` in `registryDependencies`; none mount a provider.
7. **Docs prerequisite** — the registry `docs` field summarizes SDK/proc prerequisites for humans.
8. **Gap honesty** — blocks for known gaps carry the out-of-scope / backend-pending banner; their `requires.json` names the absent op.
9. **No pinned-SDK references** — `grep` for `@constructive-io/data`, `@constructive-io/react`, `useConstructiveClient`, `<ConstructiveProvider>`, `tokenStorage` finds nothing in block source.
Loading