Skip to content

finding(types): the ADR-0049 z.never() retirement tombstones refuse by name but their .describe() remediation text never reaches the author — runtime message is zod's generic "expected never" #6105

Description

@yinlianghui

Found while implementing #5852 (out of scope there; recorded, not touched).

Measured on origin/main at 0c282d979, against built dist/

This package's ADR-0049 retirement convention writes a tombstone as
z.never().optional().describe('RETIRED (objectui#NNNN) — <why, and what to use instead>').
packages/types/src/zod/data-display.zod.ts carries nine of them on
StaticTableColumnSchema (minWidth, align, fixed, type, sortable,
filterable, resizable, editable, cell), and its doc block states the
intent:

The z.never().optional() members are ADR-0049 retirement tombstones (the
convention crud.zod.ts confirm set): an authored value is REFUSED at
parse time with the key named in the error path, instead of being silently
stripped the way an undeclared key would be. Loud refusal is the ruled
outcome
— these keys were accepted-and-inert for as long as the static
table shared the rich column type.

The refusal half is real and works. The remediation text does not reach the
author at runtime
. Measured:

StaticTableColumnSchema.safeParse({ header:'H', accessorKey:'a', align:'right' })
  success: false
  path:    ["align"]
  message: "Invalid input: expected never, received string"

The carefully written .describe() string — "RETIRED (objectui#5474) — never
read by the static table; use data-table, or a cellClassName like text-right"

appears nowhere in the issue. .describe() is schema metadata (it surfaces
in generated JSON-Schema / docs), not an error message, so an author who trips
the tombstone is told only that the key expected never. That names which key
is wrong via the path, but says nothing about why it was retired or what to
use instead
, which is the part the convention's doc block was written to
deliver.

Why this is worth recording

The whole point of choosing loud refusal over silent stripping is that the author
learns something. Half the payload currently gets dropped: an AI or human author
who writes align: 'right' on a static table column sees expected never, received string and has no path to cellClassName: 'text-right' without reading
the source.

Candidate remedy (not applied here)

zod 4 (this repo pins zod@4.4.3) accepts a custom error on z.never(), and it
does reach the issue. Verified in this repo's own node_modules:

z.never({ error: 'RETIRED (objectui#5852) — delete the key.' })
  ->  path: ["k"],  message: "RETIRED (objectui#5852) — delete the key."

So the fix shape is to carry the text in both places —
z.never({ error: '…' }).optional().describe('…') — keeping .describe() for
generated docs while making the runtime refusal explanatory. #5852 lands one
instance of that spelling (DashboardConfigSchema.aria), which is what surfaced
the gap in the older ones.

Scope, honestly stated: this is a message-quality finding, not a
correctness one. Every one of these tombstones already refuses the right keys at
the right paths; no validation outcome is wrong. Sizing the sweep (nine keys in
data-display.zod.ts plus the crud.zod.ts confirm set that established the
convention, and any others) is triage's call, not measured here.


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

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions