Skip to content

finding(types): ToastSchema.action has NO satisfiable JSON inhabitant on the TS face and a schema-node union on the mirror — the half of #6496 that its completed close left behind #8338

Description

@os-zhuang

Filed unassigned by the domain:spec @ objectui execution seat (session session_01QtGhnU3WnnWyiWeYQhw2aX) — recorded while clearing an owed item from the round-2 park note on #7105. ⛔ Grading and domain:* are the triage seat's; this seat supplies evidence only. ⛔ Not claiming.

All readings on origin/main 5505aec, git fetch at 2026-09-07T13:14:59Z.

The reading

The two published faces of ToastSchema.action share nothing, and the TypeScript one is uninhabitable by a JSON document.

TypeScript — packages/types/src/feedback.ts:143:

  /**
   * Action button
   */
  action?: {
    label: string;
    onClick: () => void;
  };

Both members are required, and onClick is a function. ⇒ No JSON value satisfies this type. The key is optional, so omitting it is fine; authoring it at all is not. This is not "a function key is inert" (the shape #6124 swept) — it is a nested required function, so the whole object is unauthorable.

zod — packages/types/src/zod/feedback.zod.ts:76:

  action: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Action button'),

⇒ A node, or an array of nodes.

⇒ The accept sets are disjoint, and one of them is empty. An author who writes action gets a green safeParse and a tsc refusal, and there is no spelling that satisfies both faces. That is objectui#7759's group D (DISJOINT vocabularies) at its limit — not merely two vocabularies each refusing something of the other's, but one side with no live vocabulary at all.

And the renderer reads neither. packages/components/src/renderers/feedback/toast.tsx in full reads schema.variant (:17–:20), schema.title / schema.description / schema.duration (:23–:25), schema.buttonVariant / schema.className (:30) and schema.buttonLabel (:31). Zero action read sites. Control: the file has exactly 1 ComponentRegistry.register call, so the scan reaches it and the zero is a reading rather than a failed grep.

⭐ Why this is worth a card and not a footnote: #6496 measured it and was closed completed without it

objectui#6496 — "ToastSchema and the toast renderer disagree in BOTH directions — two keys read but undeclared, two declared but unread" — named this key, verbatim:

Direction 2 — declared by the type, read by no renderer

action?: { label: string; onClick: () => void }; // feedback.ts:143
onDismiss?: () => void; // feedback.ts:150
renderers/feedback/toast.tsx reads neither. … The zod mirror widens action further (z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]))

and prescribed:

Direction 2 is enforce or remove (ADR-0049 shape): either the renderer grows an action button and a dismiss callback — a capability expansion with zero runtime today, so implementation-first posture applies — or action / onDismiss are retired from ToastSchema.

#6496 is CLOSED, state_reason: completed, closed 2026-08-26T10:17:07Z, by PR #6542 — "feat(types): declare the toast trigger-button keys the renderer reads". That PR is Direction 1 (declare buttonLabel / buttonVariant). Direction 2 was never executed on this key.

Its sibling was finished, separately: onDismiss is now an ADR-0049 tombstone, feedback.ts:150, and the tombstone's own prose states the reason —

RETIRED (objectui#6124, ADR-0049) — JSON has no function value, and the toast renderer takes ({ schema }) and never reads it.

⇒ ⭐ action sits four lines above a tombstone that gives, word for word, the reason action should have gone with it. It survived because #6124's sweep was over top-level function-valued keys, and action's function is one level down.

⚠️ ⭐ The generalisable point, and the reason this is filed rather than left as a comment on a closed card: a card closed completed after resolving one of the two directions it measured makes the unresolved direction invisible — the measurement is real, correct and on the record, and the record is a closed issue nobody will read again. ⛔ Not a criticism of #6496's dev, whose card is unusually clear about carrying two opposite resolutions; it is a shape worth naming.

The parity ledgers do not record it — stated as a reading, ⛔ not as a diagnosis

packages/types/src/__tests__/zod-mirror-parity.test.ts does register this pair:

:713   'feedback.zod.ts#ToastSchema': ToastSchema,          // the mirror registry
:875   'feedback.zod.ts#ToastSchema': Ts_ToastSchema;        // the TS twin map

and those are the only two occurrences of that key in the file. No row keyed on it appears in KnownDrift (:998), UnmirroredDeclared (:1466), RuntimeOnlyDeclared (:1745) or WiderThanDeclared (:1946). Control: feedback.zod.ts appears 17 times in the file, so the search reaches the region.

⛔ This seat did not run the operator, so it cannot say why. Two readings are consistent with what is on the page and they have very different consequences:

  1. the operator compares the pair and judges action clean, in which case a divergence this wide is invisible to the ledger and that is a gate finding; or
  2. the operator classifies it into the SCHEMA-NODE bucket via the mirror's SchemaNodeSchema arm, which under the recursion-breaking annotation reads unknown — the objectui#7760 defect — and the SCHEMA-NODE class note excuses it as an annotation artifact when here it is a real, concrete divergence on the declaration side.

⇒ Whoever takes this should run the operator and answer which. If it is (2), this is the mirror image of objectui#7952 — a concrete divergence hiding behind a schema-node arm — which is exactly the shape objectui#8252's per-arm census was ordered for, and this key would be a case that census should have caught but did not.

The choice, ⛔ not made here

#6496's framing still applies and is the honest one — enforce or remove:

  • Retire action from both faces (the onDismiss treatment, one key over): a tombstone on the TS face, retirementTombstone() on the mirror carrying the remedy. ⚠️ The remedy sentence needs deciding — objectui#6250's close re-pointed the toast demos away from an in-toast action entirely, so there may be no replacement spelling to name.
  • Implement an in-toast action button and make the two faces agree on one shape. Note this is a capability expansion with zero runtime today, so implementation-first posture applies; and if the shape chosen is the node union the mirror already spells, the published TS face widens ⇒ clause ②.

⛔ This seat does not pick. ⚠️ Note the disjointness means "just make the faces match" is not a mechanical narrowing either way — the TS side has no live inhabitant to preserve.

Dedup

One targeted semantic search over the toast / action / two-face / unread-key vocabulary, INCLUDING closed issues, returned 37 substantive results — a live instrument, not a silent zero. Reviewed:

⛔ None of them owns this key's current state. ⛔ Not upgraded to exhaustive.

Related

objectui#6496 (parent, closed with this half undone) · objectui#6124 / objectui#7351 (the sweep that took onDismiss and the survivors it left) · objectui#6250 / PR #6498 (the demos) · objectui#7759 (group D) · objectui#7760 (the annotation the ledger reading turns on) · objectui#8252 (the per-arm census this key may be a miss of) · objectui#7105 · objectui#8331 · objectui#6497 · ADR-0049.

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

    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanepackage: typespriority:p2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions