Skip to content

finding(types): DashboardComponentSchema.widgets is a two-arm union in zod and a one-arm array in TypeScript — the ruled-legal metric-card component node parses green and tsc refuses it, and the parity ledger files the key as SCHEMA-NODE #7952

Description

@claude

Observation-class finding, measured while implementing objectui#7035 (PR #7951). Filed unassigned, not claiming. Grading and domain:* are the triage seat's.

The claim

DashboardComponentSchema.widgets is sorted into zod-mirror-parity.test.ts's SCHEMA-NODE bucket, the bucket objectui#7759 excludes from its disposition lane as "the ANNOTATION, not an accept-set gap". Underneath that classification the pair carries a CONCRETE divergence by objectui#7759's own definition: an author can write the spelling, safeParse returns green, and tsc refuses it.

The divergence is not a key. It is a whole union arm.

face declaration
Zod mirror packages/types/src/zod/complex.zod.ts:887widgets: z.array(z.union([DashboardWidgetSlotComponentSchema, DashboardWidgetSchema])). The first arm (:749) is BaseSchema.extend({ type: z.enum(DASHBOARD_COMPONENT_WIDGET_TYPES) }) — a passthrough component node.
TypeScript packages/types/src/complex.ts:1733widgets: DashboardWidgetSchema[]. One arm. No component-node form.

The first arm exists by the 2026-08-14 maintainer ruling (objectstack#8593), quoted verbatim at complex.ts:1539 and again at complex.zod.ts:735: an SDUI dashboard COMPONENT node validates against objectui's own component schema, and metric-card joins objectui's own CLOSED component enum. classifyWidgetType returns passthrough for it and DashboardRenderer hands { ...widget } to SchemaRenderer. So the arm is deliberate, live, and documented — on the Zod side only.

Measured on a2e10cf96, both faces

Runtime — ACCEPT. DashboardComponentSchema.safeParse on the shape packages/plugin-dashboard/README.md teaches at :48, :178 and :275:

{ type: 'dashboard', widgets: [ { type: 'metric-card', title: 'T', value: '1',
    icon: 'users', trend: 'up', trendValue: '+1%', description: 'd' } ] }
=> ACCEPT, every authored key preserved in the parsed output

The same document rendered through the shipped DashboardRenderer paints title, value, trend, trendValue and description. Both halves of the ruling hold.

TypeScript — REFUSED. The same literal, annotated and compiled --strict against the built dist/:

error TS2561: Object literal may only specify known properties, but 'value' does not
              exist in type 'DashboardWidgetSchema'. Did you mean to write 'values'?

Five occurrences across the README's three legal blocks. There is no annotation an author can write for a shape the platform accepts and the maintainer ruled legal.

Why the ledger reads it as SCHEMA-NODE

WiderThanDeclared's entry (zod-mirror-parity.test.ts:1787) is a MIXED note over four keys, and its clause for this one is three words:

widgets is SCHEMA-NODE.

That is true of the SECOND arm — DashboardWidgetSchema.component is a schema-node slot, and the recursion-breaking unknown on schema-node mirrors is exactly the instrument artifact objectui#7759 describes. It is not true of the FIRST arm, which is a concrete BaseSchema.extend({ type: z.enum(...) }) with no schema-node recursion in it at all. One key, two arms, and the artifact in one of them is absorbing the real divergence in the other.

⇒ The interesting half of this card is not the dashboard. It is that the SCHEMA-NODE bucket is a per-KEY verdict over a per-ARM fact, so a concrete gap can hide behind a schema-node sibling. objectui#7759 sorted 27 keys out of its lane on that verdict; whether any of the other 26 hides one the same way is the question this card opens and does not answer.

What NOT to do

⛔ Do not widen DashboardWidgetSchema with value / icon / trend / trendValue. Both declarations say so in terms, for the same reason (complex.ts:1553, complex.zod.ts:653): those are MetricCard's registry inputs, and a member of DASHBOARD_COMPONENT_WIDGET_TYPES is validated as a component node against passthrough BaseSchema, which is what keeps them. The shape the Zod side already uses — a second declared arm on the widget slot — is the shape the TypeScript side is missing.

Both spellings are a published-contract change and need a ruling, which is why this is a card and not a patch.

Refs

  • objectui#7035 / PR docs(plugin-dashboard): README's chart example taught a rejected type: 'card' widget #7951 — the docs card this was measured under. It fixes the half that IS a docs defect (two type: 'card' widgets, refused by both faces) and leaves the three metric-card blocks byte-untouched, because on the runtime contract they are correct.
  • objectui#7759 — the WiderThanDeclared disposition lane, and the classification this card questions.
  • objectui#4600 — closed the widget type enum.
  • objectstack#8593 — the 2026-08-14 ruling that admits the component node.

Generated by Claude Code

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

    bugSomething isn't workingdomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanefindingneeds-user-decisionpriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions