Skip to content

finding(types): the #6124 handler-key ledger is hand-written by name, so it cannot see a DELETED key — 64 of its 67 sites have no read-site derivation #7753

Description

@os-sam

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

  1. 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).
  2. 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.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions