Filed by the dev seat remediating the contract review of PR #7743 (card objectui#7664). The remediation closed the instance; this card is the class.
What happened, measured
The 'kanban' arm of packages/types/src/zod/complex.zod.ts lost onCardClick outright — no refusal arm, no ?: never. BaseSchema is .passthrough(), so a key that leaves the arm is not refused: it stops being judged and the value is KEPT. Measured on the built dist at that head:
| document |
before |
after the deletion |
{ type: 'kanban', columns: [], onCardClick: { action: 'toast' } } |
REFUSED |
ACCEPTED, with {"action":"toast"} surviving into the parsed output |
the same document at onCardMove / onQuickAdd / onColumnAdd / draggable |
REFUSED |
REFUSED |
Every gate stayed green. The ledger in packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts is two hand-written arrays of [file, schema, key, mirror] tuples plus a length assertion. The PR re-keyed the arm by SUBSTITUTION — it replaced the onCardClick tuple with an onQuickAdd one — so RUNTIME_SLOT stayed at 44 and ALL_SITES at 66, and the file went green describing a key set that no longer matched the renderer.
Why no ratchet could have caught it
Every assertion in that file iterates the ledger. Its population is a literal, so:
- a key removed from a mirror AND from the ledger is invisible — nothing derives the population from anywhere else;
- the length assertion is satisfied by any substitution;
- the type-level
KeepsFunction / RetiredIsNever blocks are likewise written per key, so a deleted pair simply stops being asserted.
This is the exact hazard the file's own docblock warns about for the mirror ("The counter-probe below pins that hazard so nobody 'simplifies' a refusal arm into a deletion") — but that counter-probe pins the passthrough BEHAVIOUR on a fixture. Nothing pins the ledger's MEMBERSHIP to the renderers.
What the remediation added, and what it does not cover
PR #7743 now carries packages/plugin-kanban/src/__tests__/kanban-handler-slots-7664.test.tsx, which extracts the schema.on* reads out of KanbanRenderer's body in index.tsx and requires each to be a declared member of the 'kanban' arm carrying the RUNTIME SLOT guidance. It is red on exactly this deletion, and its population is derived, not listed.
That covers 3 of the ledger's 67 sites (onCardMove, onCardClick, onQuickAdd). The remaining 64 — across complex.zod.ts, data-display.zod.ts, form.zod.ts, overlay.zod.ts, navigation.zod.ts, layout.zod.ts, disclosure.zod.ts, feedback.zod.ts, objectql.zod.ts — are still ledgered by hand only, and a deletion at any of them reproduces this failure with every gate green.
Options, not decided here
- Generalise the derivation. One census that walks every registered renderer for
schema.onX / props.onX reads and requires each to be a declared arm member. Strongest, and the direct generalisation of what the kanban file does; the cost is a cross-package reader (@object-ui/types may not import plugins, so it would live in scripts/ or in each plugin).
- Pin the ledger against the mirrors instead of the renderers. Assert that the set of keys whose mirror is a
handlerKeyRefusal / retirementTombstone equals the ledger, derived from the zod sources by AST. Cheaper and package-local; it catches "key left the ledger" and "key left the mirror" only when the two disagree — a deletion from BOTH still passes, so it is strictly weaker than option 1 for this defect.
- Ratchet the count only. A floor on the total, so a substitution that shrinks the population reddens. Cheapest, and it would NOT have caught this one: the substitution held the count constant.
⚠️ Option 3 is listed because it is the tempting one, and it is the one that fails on the actual instance.
Facts a fix would need
- the ledger is
RUNTIME_SLOT (45 after the remediation) + RETIRED (22) = 67 sites, keyed file#Schema.key;
handlerKeyRefusal() and retirementTombstone() in packages/types/src/zod/tombstone.zod.ts are the two shapes a ledgered key's mirror takes, and both write a recognisable description;
- the read-site half is spread across
@object-ui/plugin-* and packages/components, which @object-ui/types cannot import (pnpm check:phantom-deps rejects it, and it would close a cycle).
Refs: objectui#6124 (the ruling that minted the ledger) · objectui#7664 · PR #7743 (where the instance was found and closed) · objectui#7340 (the docs half of the same ruling).
Filed from Claude Code session https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3.
Filed by the dev seat remediating the contract review of PR #7743 (card objectui#7664). The remediation closed the instance; this card is the class.
What happened, measured
The
'kanban'arm ofpackages/types/src/zod/complex.zod.tslostonCardClickoutright — no refusal arm, no?: never.BaseSchemais.passthrough(), so a key that leaves the arm is not refused: it stops being judged and the value is KEPT. Measured on the built dist at that head:{ type: 'kanban', columns: [], onCardClick: { action: 'toast' } }{"action":"toast"}surviving into the parsed outputonCardMove/onQuickAdd/onColumnAdd/draggableEvery gate stayed green. The ledger in
packages/types/src/__tests__/handler-keys-json-refusal-6124.test.tsis two hand-written arrays of[file, schema, key, mirror]tuples plus a length assertion. The PR re-keyed the arm by SUBSTITUTION — it replaced theonCardClicktuple with anonQuickAddone — soRUNTIME_SLOTstayed at 44 andALL_SITESat 66, and the file went green describing a key set that no longer matched the renderer.Why no ratchet could have caught it
Every assertion in that file iterates the ledger. Its population is a literal, so:
KeepsFunction/RetiredIsNeverblocks are likewise written per key, so a deleted pair simply stops being asserted.This is the exact hazard the file's own docblock warns about for the mirror ("The counter-probe below pins that hazard so nobody 'simplifies' a refusal arm into a deletion") — but that counter-probe pins the passthrough BEHAVIOUR on a fixture. Nothing pins the ledger's MEMBERSHIP to the renderers.
What the remediation added, and what it does not cover
PR #7743 now carries
packages/plugin-kanban/src/__tests__/kanban-handler-slots-7664.test.tsx, which extracts theschema.on*reads out ofKanbanRenderer's body inindex.tsxand requires each to be a declared member of the'kanban'arm carrying the RUNTIME SLOT guidance. It is red on exactly this deletion, and its population is derived, not listed.That covers 3 of the ledger's 67 sites (
onCardMove,onCardClick,onQuickAdd). The remaining 64 — acrosscomplex.zod.ts,data-display.zod.ts,form.zod.ts,overlay.zod.ts,navigation.zod.ts,layout.zod.ts,disclosure.zod.ts,feedback.zod.ts,objectql.zod.ts— are still ledgered by hand only, and a deletion at any of them reproduces this failure with every gate green.Options, not decided here
schema.onX/props.onXreads and requires each to be a declared arm member. Strongest, and the direct generalisation of what the kanban file does; the cost is a cross-package reader (@object-ui/typesmay not import plugins, so it would live inscripts/or in each plugin).handlerKeyRefusal/retirementTombstoneequals the ledger, derived from the zod sources by AST. Cheaper and package-local; it catches "key left the ledger" and "key left the mirror" only when the two disagree — a deletion from BOTH still passes, so it is strictly weaker than option 1 for this defect.Facts a fix would need
RUNTIME_SLOT(45 after the remediation) +RETIRED(22) = 67 sites, keyedfile#Schema.key;handlerKeyRefusal()andretirementTombstone()inpackages/types/src/zod/tombstone.zod.tsare the two shapes a ledgered key's mirror takes, and both write a recognisabledescription;@object-ui/plugin-*andpackages/components, which@object-ui/typescannot import (pnpm check:phantom-depsrejects it, and it would close a cycle).Refs: objectui#6124 (the ruling that minted the ledger) · objectui#7664 · PR #7743 (where the instance was found and closed) · objectui#7340 (the docs half of the same ruling).
Filed from Claude Code session
https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3.