⚠️ 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
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. Filedunassigned 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.tssays, ofresolvePropsShape:That is true for shape resolution (
shape/def.shape/_def.shape/ thelazySchema()thunk). It is not true for anything else a spec-parity test reads off aZod 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:
packages/components/src/__tests__/data-table-selection-mode.test.tsxpackages/plugin-list/src/__tests__/add-record-position-spec-parity.test.tsxpackages/plugin-list/src/__tests__/user-filter-arity-spec-parity.test.tsxpackages/plugin-timeline/src/__tests__/timeline-scale-spec-parity.test.tsall spelling
.shape?. as { def?: { innerType?: { options?: readonly string[] } } }then
?.def?.innerType?.options. Note this one reads onlydef.innerType— not_def, and not the un-wrapped case — so a schema whose member is not.optional(), ora Zod build exposing
_def, silently yieldsundefined. Whether each call site failsloudly on
undefinedor quietly derives an empty option set is exactly the per-filequestion 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?.typepackages/app-shell/src/views/metadata-admin/previews/__tests__/block-config.test.ts:82-91— a bounded 6-iteration loop alternating
def.innerType/def.element, typedRecordpackages/app-shell/src/views/metadata-admin/clientValidation.optOuts.test.ts:351-360— a third spelling again, through
_zod.def.elementand adef.getter()call(3) A wrapper-key walk — three copies of one literal list:
⚠️ Superseded — this class has since grown out of tests into
['in', 'out', 'innerType', 'schema', 'left', 'right']appears inpreviews/flow-canvas-seeds.spec-parse.test.tsx:124,inspectors/flow-node-config.spec-reconciliation.test.ts:108, andpackages/core/src/actions/__tests__/actionKeys.pin.test.ts:76..mjsCI gate scripts. See the measured-state block.(4)
recordRelatedListInputs.spec-parity.test.ts:61-65carries yet another_def.innerType ?? _def.typeunwrap.Why it is worth recording rather than fixing opportunistically
The failure mode is the one
spec-tombstones.tsalready argues at length: a Zodinternals 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 sharpestcase 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 thetests actually want — an
enumOptions(schema, key)and anarrayElementSchema(schema)alongside
resolvePropsShape— and convert the call sites, per the README's "New gatesimport 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'slanding).
Generated by Claude Code