Skip to content

[finding] The tombstone judge converged, but the Zod-internals READERS around it are still hand-copied in ~10 test files — four of them verbatim #5872

Description

@os-zhuang

⚠️ MEASURED STATE — 2026-08-31. Read this before the original filing below.

The census in the original body is STALE and has now caused one wasted full dispatch. Do not work from it.

Class (1) — ✅ DONE, landed

PR #6047 MERGED 2026-08-24T15:00:26Z (merged: true, head 3b010122f; squash subject test(test-support): confine the spec enum-vocabulary reader to one module (#5872 class 1)). The four verbatim enum-options copies are consolidated onto packages/test-support/src/spec-enum-options.ts (shapeEnumOptions), and all three consuming packages already declare the devDependencies edge.

The 2026-08-25 dead-claim reclaim on this card was a FALSE NEGATIVE. It recorded "no merged commit referencing this card across 711 commits of main" 17 hours after the merge it failed to see, and a 2026-08-31 re-dispatch acted on it. Root cause, measured: main was re-rooted on 2026-08-30 — HEAD history is 28 commits ending at a parentless root (335523906) — so git log over main can no longer see any pre-re-root merge, and PR #6047's head is an ancestor of nothing.

⚠️ On this repo, a "N commits of main" scan is not evidence of absence. Use blob identity against the fetched PR ref (git rev-parse <ref>:<path> per changed file), or simply read the PR's merged flag. Confirmation for #6047: of its 13 changed files, 11 byte-identical to e3d117ae1, 0 absent; the 2 that differ moved only under later unrelated work (#6355's timeScale-alias retirement inside timeline-scale-spec-parity.test.ts's resolveTimelineScale block, and pnpm-lock.yaml).

Classes (2)–(4) — what is actually left

  • Class (2) — array-element unwrapping, three DIFFERENT spellings for one question. ⛔ Not mechanical: a shared reader must pick a behaviour where three copies disagree, which is a genuine verdict-flip risk. Deserves its own round, measured before conversion.
  • Class (3) — the wrapper-key literal list. ⛔ BLOCKED, needs a maintainer ruling — see the decision card linked below. It has grown out of tests into .mjs CI gate scripts, and @object-ui/test-support is private: true with an exports map pointing at TypeScript source, so a plain node scripts/check-*.mjs cannot import it. The class-(1) pattern is unavailable across that language boundary. ⛔ Do not "just do the TypeScript sites" — that ships the exact split this card family exists to prevent, with a language boundary making it permanent.
  • Class (4) — single-site unwraps; has grown since filing, one site now sits in packages/types where live sibling work lands.

⚠️ Classes (3) and (4) have both roughly doubled since the original filing. Re-derive the census before writing anything; a premise_still_valid: false with no PR is a good run on this card.

⭐ Hazard any future round inherits — measured, not theorised

On an empty vocabulary, the "spec accepts a name we do not implement" half of three of the four parity gates stays GREEN — it filters an empty list. Only a non-vacuity assertion catches it. spec-enum-options.ts's docblock makes that assertion an explicit duty of every consumer; any new shared reader must do the same.

⭐ Instrument note for whoever measures next

Counts are satisfiable by a swap. Fingerprint (file, sorted test names) with sha256 instead: on the 2026-08-31 re-measurement timeline-scale's count held at 8 while two test names were swapped by #6355 — identical counts, moved fingerprint. That is the failure counts cannot see.

Related

Uncatalogued sibling family filed separately: 15 files cast a schema to { options?: readonly string[] } and read .options straight off a top-level enum node — no shape resolution, no wrapper walk, but the same quiet-permissive hazard when the cast yields undefined.


Original filing (2026-08-23) — ⚠️ census superseded by the block above

Found while implementing #4947 (PR #5871), which retired the last two local copies of
the ADR-0087 D2 tombstone criterion onto @object-ui/test-support. Filed
unassigned and not fixed on that branch — #4947's scope is the tombstone verdict
only, and this is a different judgement one level out.

No false green or false red today. Every copy below appears correct against the
installed pin. This is the #4434 class caught before the drift, same as #4947 was —
recorded because the copies are already numerous enough that they cannot move together.

What the shared module actually confines

packages/test-support/src/spec-tombstones.ts says, of resolvePropsShape:

Reaching into internals is confined to this module so that a gate never has to.

That is true for shape resolution (shape / def.shape / _def.shape / the
lazySchema() thunk). It is not true for anything else a spec-parity test reads off a
Zod node, and those readers are still written out by hand in each file.

The copies, by spelling

(1) Enum-options reading — four VERBATIM copies, four packages.Consolidated by PR #6047; see the measured-state block above. Identical text
apart from the key name:

file line
packages/components/src/__tests__/data-table-selection-mode.test.tsx 38-39
packages/plugin-list/src/__tests__/add-record-position-spec-parity.test.tsx 49-50
packages/plugin-list/src/__tests__/user-filter-arity-spec-parity.test.tsx 50-51
packages/plugin-timeline/src/__tests__/timeline-scale-spec-parity.test.ts 23-24

all spelling .shape?. as { def?: { innerType?: { options?: readonly string[] } } }
then ?.def?.innerType?.options. Note this one reads only def.innerType — not
_def, and not the un-wrapped case — so a schema whose member is not .optional(), or
a Zod build exposing _def, silently yields undefined. Whether each call site fails
loudly on undefined or quietly derives an empty option set is exactly the per-file
question that having four copies makes expensive to answer.

(2) Array-element unwrapping — three different spellings for one question:

  • packages/plugin-detail/src/__tests__/recordDetailsInputs.spec-parity.test.ts:83
    arr?.element ?? arr?.def?.element ?? arr?._def?.element ?? arr?._def?.type
  • packages/app-shell/src/views/metadata-admin/previews/__tests__/block-config.test.ts:82-91
    — a bounded 6-iteration loop alternating def.innerType / def.element, typed
    Record
  • packages/app-shell/src/views/metadata-admin/clientValidation.optOuts.test.ts:351-360
    — a third spelling again, through _zod.def.element and a def.getter() call

(3) A wrapper-key walk — three copies of one literal list:
['in', 'out', 'innerType', 'schema', 'left', 'right'] appears in
previews/flow-canvas-seeds.spec-parse.test.tsx:124,
inspectors/flow-node-config.spec-reconciliation.test.ts:108, and
packages/core/src/actions/__tests__/actionKeys.pin.test.ts:76.
⚠️ Superseded — this class has since grown out of tests into .mjs CI gate scripts. See the measured-state block.

(4) recordRelatedListInputs.spec-parity.test.ts:61-65 carries yet another
_def.innerType ?? _def.type unwrap.

Why it is worth recording rather than fixing opportunistically

The failure mode is the one spec-tombstones.ts already argues at length: a Zod
internals rework moves one spelling and not the others, and the copies that stop reading
go quietly permissive — a derived option set becomes empty, an element shape becomes
undefined, and the assertion built on it passes over nothing. Copy (1) is the sharpest
case because the four are textually identical right now, so no reviewer diffing one of
them would notice the other three did not move.

It is also worth noting what #4947 did not create: none of this is new. #4947 removed
two copies of the tombstone verdict and left every reader above exactly as it found them.

Suggested disposition (a judgement call, not a prescription)

The obvious shape is to grow @object-ui/test-support's spec module by the readers the
tests actually want — an enumOptions(schema, key) and an arrayElementSchema(schema)
alongside resolvePropsShape — and convert the call sites, per the README's "New gates
import this module" norm. But that is ~10 files across 7 packages for a class with no
live defect, so it may well be right to convert only copy family (1) (four verbatim
duplicates, one small helper, cheapest and highest-drift-risk) and leave (2)-(4) as
recorded. Worth a triage decision on scope before anyone starts.

Refs: #4947 / PR #5871 (the tombstone-verdict convergence), #4434 (the prior
copied-judge drift that motivated @object-ui/test-support), #3809 (the shared judge's
landing).


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

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