feat(types): declare the five handler keys the list-view renderer reads (objectui#7804) - #9565
Conversation
…eads (objectui#7804)
The zod arm `type: 'list-view'` selects now declares `onAddRecord`,
`onBulkAction`, `onDensityChange`, `onNavigate` and `onPageSizeChange` as
objectui#6124 RUNTIME SLOTS — a named refusal on the JSON face, a callable
twin on the TypeScript face — and the five matching rows leave
`KNOWN_UNDECLARED_READS`.
Disposition measured per key, not per group. `'retired'` was refused for all
five: every one is read AND invoked. The channel is not the same face for all
of them, which is what this slice turned on:
- `onNavigate` / `onDensityChange` are read off the NODE (`schema.onX`) and
already declared on `ListViewRuntimeProps`.
- `onAddRecord` / `onBulkAction` / `onPageSizeChange` are read off the PROPS
bag (`props.onX`) and already declared as React props on `ListViewProps`
in `@object-ui/plugin-list`.
⇒ nothing is added to `ListViewRuntimeProps`; no published interface widens.
⚠️ This arm FEEDS its own TypeScript face — `ListViewSchema` is `z.input` of
the mirror intersected with `ListViewRuntimeProps` — and a refusal arm's
`z.input` is `never | undefined`, which ANDs a runtime declaration down to
`undefined`. Measured before the fix: `ListViewSchema['onNavigate']` resolved
to `undefined`, down from its function type, and nothing went red, because
`undefined` is assignable to every optional callback parameter the reads hand
it. `ListViewAuthored` gives the runtime half precedence so `'runtime-slot'`'s
promise stays true, and it is a key-remapped mapped type rather than `Omit`
because `Omit` over the passthrough index signature keeps only that signature
(measured: `objectName` read `unknown`). Both readings are pinned in
`list-view-handler-slots-7804.test.ts`, where they fail at `tsc`.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSd9P5u2Mf4p8g4n4SD4Fx
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
`list-view-spec-parity.test.ts` asks every objectui-only member of the `list-view` arm to be a conscious local-vs-upstream decision. The five `handlerKeyRefusal` arms this slice declared are neither branch it offers: promoting one into `@objectstack/spec` would ask the protocol to declare a key JSON cannot express, and calling it a "genuine objectui-only extension" would say the arm accepts something — it accepts nothing. So they get their own named category rather than a row in `SANCTIONED_LOCAL`, and membership is CHECKED: a new assertion refuses a member the arm does not declare, or that accepts either an authored action object or a function, with an authorable sanctioned-local key (`viewType`) as the lit control in the same pass. The set therefore cannot be used to park a real authorable field outside the drift guard. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSd9P5u2Mf4p8g4n4SD4Fx
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Corrects this branch's own first reading. I had concluded that nothing needed adding to `ListViewRuntimeProps` because `onAddRecord` / `onBulkAction` / `onPageSizeChange` are read off the PROPS bag and `ListViewProps` (`@object-ui/plugin-list`) already declares all three as React props. ⛔ That measured only ONE of the two supply paths into that bag. `SchemaRenderer` spreads every non-metadata top-level key of the node into the props it creates the component with, so a host that builds the NODE in TypeScript reaches the same read — and that path was typed only by `BaseSchema`'s passthrough index signature. Declaring the refusal arms without declaring the slots would have narrowed a live programmatic channel from `unknown` to `undefined`, which is wider than this card asks for and is not what `'runtime-slot'` means. It is the same repair objectui#9344's slice made on `ObjectGallerySchema` for the same spread, and it means a published interface WIDENS here as well as narrowing — the changeset says so. `ObjectView.relayRungCensus-7559.test.ts` follows from that: the three entered its derived population with this declaration, and its own failure text says the answer is owed by the change that added them. Three `host-runtime` absences, the existing kind whose validator requires exactly `ListViewRuntimeProps` membership — no new absence kind, no new assertion.⚠️ FILE SURFACE: that census file is outside this claim's declared surface (`packages/types/src/`, `scripts/check-handler-key-read-sites.mjs`, one changeset). It is reported rather than hidden; see the pull request body. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSd9P5u2Mf4p8g4n4SD4Fx
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: In-seat clause-② review by the dispatching ① Derived judgmentsThe card-side declaration was
⇒ ⭐ The judgment that matters most is one the dev found and I had assumed away. My dispatch said the zod arm and the TypeScript face were two independent questions. They are not: this arm feeds its own TS face ( ② Semver level
③ Boundary flags
Implemented-by:
CI on this exact head: 36 checks — 33 success, 3 skipped, 0 non-green, 0 pending; VERDICT: PASS
Generated by Claude Code |
Provenance for the carrier clear — objectui#7804 / PR objectui#9565Both
⭐ Sequence corrected from the sibling PR objectui#9567 earlier this round, where I stripped first and the guard then refused my record at exit 4. ⇒ Generated by Claude Code |
Part of #7804 — slice 3 of the handler-key burn-down: the
ListViewSchemafamily.⛔ No closing keyword: this card's ledger is not empty. Measured on this branch,
with the command:
KNOWN_UNDECLARED_READSread 20 rows at this branch's base (88561fdc4) and reads15 here.
list-view::drained to 0; the lit control in the same pass is the fivelist::rows, which are still there — so the zero is a reading, not an empty query.What this slice does
The zod arm
type: 'list-view'selects now declaresonAddRecord,onBulkAction,onDensityChange,onNavigateandonPageSizeChangeas objectui#6124 RUNTIME SLOTS.Disposition measured per key, never forced to match the group.
'retired'wasrefused for all five — every one is read AND invoked. The channel is not the same face
for all of them, and that split is what this slice turned on:
onNavigateschema.onNavigateintouseNavigationOverlayListViewRuntimeProps(already declared)onDensityChangeschema.onDensityChangeintouseDensityModeListViewRuntimeProps(already declared)app-shell'sObjectView, on itsconst fullSchema: ListViewSchemanodeonAddRecordprops.onAddRecord?.()on the toolbar add buttonListViewProps+ newly declared onListViewRuntimePropsStudioDesignSurface, as a React proponBulkActionprops.onBulkAction?.(action, rows)onPageSizeChangeprops.onPageSizeChange(newSize)on the pager⇒ three members ARE added to
ListViewRuntimeProps, so a published interface widens.That corrects this branch's own first reading — see the Clause-② verdict below.
Every earlier slice of this card drained a plain
export interface X extends BaseSchema,whose TypeScript face is hand-written and therefore untouched by what the mirror declares.
ListViewSchemais not that shape — it isz.inputof the mirror (ListViewInferred)intersected with
ListViewRuntimeProps. AhandlerKeyRefusalarm'sz.inputisnever | undefined, and an intersection ANDs per key, so:Measured, before any fix was written (
tsc -p packages/types/tsconfig.test.json,probing the resolved type by assigning it to
0):ListViewSchema['onNavigate']((recordId: string | number, action?: string) => void) | undefinedundefinedListViewSchema['onDensityChange']((mode: 'compact' | 'comfortable' | 'spacious') => void) | undefinedundefinedListViewSchema['onAddRecord']unknownundefined⛔ And nothing went red —
undefinedis assignable to every optional callbackparameter the reads hand it. That is the silent failure mode: a
'runtime-slot'disposition promises "the TypeScript twin stays callable", and a bare intersection makes
it a lie while every gate stays green.
The intersection now gives the runtime half precedence (
ListViewAuthored). It is akey-remapped mapped type and ⛔ not
Omit, measured the same way:ListViewInferredcarries the passthrough index signature, so
Omitoverkeyof ListViewRuntimePropskeepsonly that signature —
objectNamereadunknowninstead ofstringunder theOmitcandidate. That is the same trap
ListViewPropsdocuments forPropsWithoutRef.Both readings are pinned where they fail, at
tsc, inlist-view-handler-slots-7804.test.ts.Verdicts on the dispatch's five stated assumptions
list-view::— HALF FALSIFIED, and the half that moved matters.The five
list-view::rows are real and are exactly this slice. But the ledger holdsten rows for these same five read sites:
plugin-listregistersListViewRendererunder
'list'as well, and the census keys a registration by its raw type string, sothe alias's reads are judged a second time against
ListSchema— the bullet-listdisplay primitive. Detail and the reason it is ⛔ not drained here: see Acceptance
notes.
zod arm feeds
ListViewInferred, which feeds the declared type. Reported above; theprecedence type is the repair.
ListViewSchemamirror by reference. Its only references are the export inindex.zod.ts, theObjectQLComponentSchemadiscriminated union, andListViewInferred. No sibling slot picks up the refusals the wayObjectViewSchema'snested
form/tableslots did in slice 2. ⇒ no blast radius to report on that axis.names this slice adds to
objectql.tsare checked against what that file alreadydeclares:
onNavigatealready appears there twice (onObjectGridSchemaandObjectViewSchema), so this is a third — see Acceptance notes for the reading.Clause-②: yes— CONFIRMED, and my first reading of it was wrong. I initiallymeasured that the three props-half keys need nothing added, because
ListViewPropsin
@object-ui/plugin-listalready declares all three by name. ⛔ That measured onlyONE of the two supply paths into the props bag.
SchemaRendererspreads everynon-metadata top-level key of the node into the props it creates the component with,
so a host that builds the NODE in TypeScript reaches the same read — and that path was
typed only by
BaseSchema's passthrough index signature. Declaring the refusal armswithout declaring the slots would have narrowed a live programmatic channel from
unknowntoundefined. So the three are declared onListViewRuntimeProps, which isthe same repair objectui#9344's slice made on
ObjectGallerySchemafor the samespread. ⇒ the slice is genuinely mixed-direction and
yesis right.⛔ FILE-SURFACE BREACH, declared — one file, and the PM's call
This claim's declared surface is
packages/types/src/(incl.__tests__/),scripts/check-handler-key-read-sites.mjs, and one changeset. One file outside it isedited:
packages/app-shell/src/views/ObjectView.relayRungCensus-7559.test.ts,three lines plus a comment.
Why, measured: that census derives its population from the
list-viewmirror's shapeand from
ListViewRuntimeProps' declared members, so the three slots entered it withthis change. It reddens —
onAddRecord,onBulkAction,onPageSizeChangereported asneither relayed nor declared — and its own failure text reads "If the member is new on
ListViewSchema, (1) or (2) is owed BY THE CHANGE THAT ADDED IT." The repair is threehost-runtimeabsence entries: an EXISTING absence kind, whose existing validatorrequires exactly
ListViewRuntimePropsmembership, so no new kind and no new assertionwere introduced.
⭐ Measured before touching it: neither sibling claim in this batch holds that file —
objectui#7234's branch touches
ObjectView.objectBoundActions-7234.test.tsxandmetadata-admin/**, objectui#9295 touches noapp-shellfile at all.⇒ The PM ratifies or reverts. To revert just that file:
git checkout 88561fdc4 -- packages/app-shell/src/views/ObjectView.relayRungCensus-7559.test.ts— the rest of the branch stands on its own, and that test goes red again.
Gates
Captured to a file before reading the exit code, every one of them.
All at the final head
cd4eb52aa, working tree clean.pnpm check:handler-key-readspnpm check:changeset-claimspnpm check:control-bytespnpm check:new-line-citationspnpm changeset:check·node scripts/check-changeset-presence.mjspnpm --filter @object-ui/types type-checkpnpm --filter @object-ui/app-shell type-checkplugin-list·plugin-view·core·plugin-grid·react·components·plugin-detailtype-checkpnpm exec vitest run --maxWorkers=2over the affected suitespnpm exec eslint --no-inline-configover the 6 changed lintable filesnode scripts/check-governed-queue-guard.mjs --test …⭐ The gate can still fail. Deleting the
onNavigatearm (mutation proven on disk,1 → 0 occurrences) turns
check:handler-key-readsRED, naming'list-view'.onNavigateas undeclared; restored, and the restore proven by blob hash equal to HEAD's and an empty
git diff HEADfor the path. CollapsingListViewAuthoredback to a bare intersectionturns
tscRED with exactly the two type pins, "does not satisfy the expected typenever".The full table, with the ablation that proves the gate can still fail, is in the
os-dev-reportcomment on #7804.Acceptance notes
noted, not filed — the
list::ListSchematwin of these five rows.plugin-listregisters
ListViewRendererunder'list'with{ namespace: 'view', skipFallback: true },and
skipFallbackis precisely what stops that alias claiming the barelistkey — whichbelongs to the bullet/numbered-list display primitive in
@object-ui/components. Thecensus keys a registration by raw type string, so the alias's five reads are scored against
ListSchema(items/ordered/dividers/dense). Declaring the five there wouldpublish a density handler on a bullet list, so ⛔ they are not drained here; the ledger now
records the measurement at the rows themselves. The same shape produces
form::FormSchemaand
grid::GridSchema.onNavigate—plugin-formandplugin-gridregisterview:formand
view:gridthe same way. Successor: the next slice of this card reads those rows.Dedupe words:
skipFallback,KNOWN_UNDECLARED_READS, namespaced alias, bare type key,check-handler-key-read-sites.noted, not filed — a third
onNavigatedeclaration inobjectql.ts. Slice 2 recordedthat objectui#9357's file-scoped arity reader mis-anchors when one file carries two
declarations of a name with different contracts. This slice adds a third
onNavigatetothat file. It is a
handlerKeyRefusalarm, not a signature-bearing declaration, and thesignature it pairs with (
ListViewRuntimeProps.onNavigate) is unchanged and lives inobjectql.tswhere it always did. CI is the instrument for that reader, and this PR's runis what answers it.
noted, not filed — objectui#9547 is untouched.
ObjectGridSchema.onNavigate's docblockwas not read, edited or relied on here.
Generated by Claude Code
Generated by Claude Code