2nd-component-shape proof: client_card (catalog genericity)
Story-1795 shipped the FIRST typed catalog components —
matter_table and matter_card — but both bind the
SAME DTO (MatterListItem). That left an open question: is the
render substrate genuinely shape-generic, or is it secretly
matter-coupled? This change answers it by registering a
NON-matter shape, client_card, bound to a
DIFFERENT generated DTO (ClientListItem) and dispatched by the
SAME ComponentResultView seam. App-side validation only — it
proves the catalog/dispatch generalizes, not a new live brain/broker path
(RFD-AGT-004 §7.6).
What changed
ClientCard mirrors MatterCard exactly
(labelled region, one card per row, <dl> label/value
pairs) but binds ClientListItem from src/gen —
no hand-written types. Rows come ONLY from the validated frame (no
fetch, no row invention).
client_card is declared on clientsCard with a
first-class dtoBinding {dto: "ClientListItem", fields};
pnpm gen:catalog flattens it into
COMPONENT_CATALOG. check:catalog asserts every
bound field ⊆ the real ClientListItem field-set.
The dispatch map's value type became the structural
RenderCatalogProps (rows: Record<string,
unknown>[] + fields). Each component is authored
against its own bound DTO and erased to that shape in the registry —
that erasure IS the genericity proof. The old
MatterListItem cast in ComponentResultView is
gone; validated rows pass straight through.
resolveRenderComponent("client_card") reads the
fields straight off the generated catalog and passes them
in; the card renders one label/value pair per field. Change the
catalog field-set and the card follows.
An unknown component_id still arrives as
fallbackText and renders as text — never blank. The #1794
text fallback is unchanged; the mounted test re-asserts it alongside
the new client_card case.
matter_table/matter_card were left untouched;
ClientCard reuses the existing card pattern, tokens, and
the shared formatCell. The only shared addition is one
structural props interface + one client field-label key helper — no
generic "EntityCard" mini-framework.
Where it lives
src/components/agents/render-catalog/client-card.tsx— NEW.ClientCardbound toClientListItem; labelled region, one card per row.src/components/agents/render-catalog/registry.ts— registry value type → structuralRenderCatalogProps;client_cardregistered.src/components/agents/render-catalog/fields.ts— NEWRenderCatalogProps+clientFieldLabelKey.src/components/agents/render-catalog/client-card.stories.tsx+render-catalog.fixtures.ts— NEW Storybook coverage (CLIENT_ROWS+ single-row).src/components/agents/agent-chat-panel.tsx—ComponentResultViewpasses validated rows directly (dropped the matter-typed cast + unused import).src/lib/surface-registry/page-card/cards/clients.card.ts— declares theclient_cardcomponent +dtoBinding.src/lib/surface-registry/page-card/catalog.ts— registeredClientListIteminDTO_FIELD_SETS+clientListItemSchemainGENERATED_PROP_SCHEMAS.src/lib/surface-registry/page-card/generated-component-catalog.ts— regenerated (pnpm gen:catalog) with the client_card entry.messages/{en,hi}.json—clientCardRegionLabel+clientField.*(both locales).
Verify
pnpm lint # clean (1 pre-existing avatar.tsx warning, unrelated) pnpm test # 895 passed pnpm build # green (tsc + route + catalog codegen) pnpm check:catalog # OK — 22 L1 surfaces, catalog fresh git diff --check # clean # NOTE: do NOT run pnpm codegen — import contracts from src/gen.
- client_card-from-frame
agent-chat-panel.render.test.tsxdrives a realclient_cardrender.dataframe →reduceFrame→mergeLiveRender→ the LIVE panel; asserts a labelledrole="group"region with the client field labels + bound row data on screen — proving the catalog generalizes beyond matter. - unknown → text an unknown
component_idstill paints the visible text/JSON fallback (regression guard intact). - check:catalog the bound
ClientListItemfields are validated ⊆ the real generated DTO; a typo'd field or propSchemaRef fails the gate.
Reversible calls (stated for review): the client view
fields are name, clientNumberFormatted, clientType, status,
createdAt (the display subset of ClientListItem);
labels are a new Agents.clientField.* key set; cell values
render as their bound DTO string (enum / ISO date as-is) — no per-field
formatter framework. Each card's aria-label is the client
name (matter_card uses title || matterRef).
Non-goal (RFD-AGT-004): NO brain selection or broker
binding for clients (a future live-path ticket), NO new framework. Reuses
the matter_card pattern — a genericity proof, kept minimal.
Files changed
12 file(s), +226 / -19 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +8 | -0 |
| messages/hi.json | Config | +8 | -0 |
| src/components/agents/agent-chat-panel.render.test.tsx | Component | +36 | -0 |
| src/components/agents/agent-chat-panel.tsx | Component | +3 | -8 |
| src/components/agents/render-catalog/client-card.stories.tsx | Component | +21 | -0 |
| src/components/agents/render-catalog/client-card.tsx | Component | +42 | -0 |
| src/components/agents/render-catalog/fields.ts | Component | +17 | -0 |
| src/components/agents/render-catalog/registry.ts | Component | +13 | -10 |
| src/components/agents/render-catalog/render-catalog.fixtures.ts | Component | +40 | -1 |
| src/lib/surface-registry/page-card/cards/clients.card.ts | Library | +14 | -0 |
| src/lib/surface-registry/page-card/catalog.ts | Library | +5 | -0 |
| src/lib/surface-registry/page-card/generated-component-catalog.ts | Library | +19 | -0 |