Surface-card components + dto_bindings + gen:catalog validation (S-D3)
Story-1785 generated the Page Card authority half
(which permissions exist) and explicitly left the
component half — what a surface renders, bound to a
real wire DTO — as the S-D3 follow-on. This change ships that half as
schema + codegen-validation only (no runtime render): the surface card
now declares components with a first-class
dtoBinding, gen:catalog emits the flattened
component catalog, and check:catalog fails CI when a
component binds a field the backend does not ship, or names a capability
that is not one of the surface's operations.
What changed
ListingSurfaceCard gains an optional
components[]. Each is
{ componentId (snake), propSchemaRef, dtoBinding, capabilities }.
The dtoBinding is first-class —
{ dto, fields }, not a bare ref — so the binding is
checkable. Absent on forums/clients (no render half yet); present on
matters.
A component's capabilities reference the surface's
operations by key only. The
needs permission is resolved from the matching
operation at generation time — a component can never claim a
permission its operation does not (§7.6 S-D8).
The validator's DTO field-sets come straight from the generated Zod
schema's .shape (matterListItemSchema) —
the generated contract is the field-set. No second mirror,
no AST parsing.
gen:catalog now emits a second committed file,
generated-component-catalog.ts — every component
flattened with its DTO binding and capabilities resolved to
{key, needs} — kept fresh by the same
--check gate as the authority artifact.
Where it lives
src/lib/surface-registry/page-card/listing-surface.schema.ts— newDtoBinding+ListingComponent; optionalcomponentson the card. Existing cards round-trip unchanged (optional, not defaulted).src/lib/surface-registry/page-card/cards/matters.card.ts— declaresmatter_table/matter_card, both bound toMatterListItem, each exposing thematters.createcapability.src/lib/surface-registry/page-card/catalog.ts—DTO_FIELD_SETS(from the gen Zod.shape);findCatalogViolationsadds checks (6) field ⊆ bound DTO and (7) capability ∈ operations. Injectable field-set arg for tests.scripts/page-card/gen-catalog.ts—deriveComponentCatalog(pure, resolves needs from operations) +renderComponentCatalog;mainnow syncs both artifacts.src/lib/surface-registry/page-card/generated-component-catalog.ts— GENERATED, committed. Do not hand-edit.
Verify
pnpm gen:catalog # emits generated-component-catalog.ts (committed) pnpm check:catalog # freshness (both artifacts) + field/capability subset pnpm lint pnpm typecheck pnpm test pnpm build
- Emits the matter card's two components land in
generated-component-catalog.tswithmatters:writeresolved onto each capability. - Field gate
check:catalogfails when a component binds a field absent from its DTO (negative test:ghostField). - Capability gate it fails when a capability is not an operation on the card (negative test:
matters.delete). - Freshness a drifted
generated-component-catalog.tsfails the LI-6 gate, mirroring the authority artifact. - No regression forums/clients cards (no components) still validate; all 873 tests green.
Coverage added in catalog.test.ts (the two new negative
gates) and generated-authority.test.ts (component-catalog
freshness).
Non-goal (RFD-AGT-004): schema + codegen-validation
only. No reduceFrame, ThreadItemView, the
.tsx components, or any wire/render path — those are the
render-substrate + 003-components tickets.
Files changed
8 file(s), +354 / -16 lines
| Path | Role | + | - |
|---|---|---|---|
| .github/workflows/ci.yml | Workflow | +1 | -1 |
| scripts/page-card/gen-catalog.ts | Script | +106 | -15 |
| src/lib/surface-registry/page-card/cards/matters.card.ts | Library | +23 | -0 |
| src/lib/surface-registry/page-card/catalog.test.ts | Library | +62 | -0 |
| src/lib/surface-registry/page-card/catalog.ts | Library | +63 | -0 |
| src/lib/surface-registry/page-card/generated-authority.test.ts | Library | +7 | -0 |
| src/lib/surface-registry/page-card/generated-component-catalog.ts | Library | +51 | -0 |
| src/lib/surface-registry/page-card/listing-surface.schema.ts | Library | +41 | -0 |