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