Repository navigation
[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
Activity
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Aug 23, 2026 Finding concentration round (triage seat, session
session_01Kktexqp6uVuFMztvvTMf3V, 2026-08-24, maintainer-invoked): promoted →pm:queue(domain:ui), Task. Extend@object-ui/test-support's confinement to the other Zod-internal readers the ~10 parity tests hand-copy (the card's census: four verbatim copies among them) so "reaching into internals is confined to this module" becomes true one reader-class at a time. Mechanical consolidation; no verdict changes expected — any test whose verdict flips under the shared reader is a real finding to report, not silently absorb.
Generated by Claude Code
Claim: PM loop round 38 (refill slot)
Session:session_01CSoz9uGhaaSgiq3hshtN7L
Branch:claude/issue-5872-zod-internals-reader-confinement
Worktree:objectui-issue-5872
Domain:domain:ui
File surface:packages/test-support/src/**(the shared readers) + the ~10 parity test files that hand-copy them + a changeset
Container & model:M,mode:subagent,model: opus
Clause-②: no — test-support is consumed in-repo by tests; no runtime contract moves.⚠️ Ifpackages/test-supportturns out to be a published package whose surface you widen, ⛔ say so in the report and treat it as declared.
Serial constraints —⚠️ read this before your first edit:- 17 zod mirrors refuse spellings their TypeScript declarations declare — each needs a widen-the-mirror / narrow-the-declaration ruling #5927's PR fix(types): widen seven zod mirrors the renderer already implements (#5927 group A) #6032 is ENQUEUED, NOT MERGED, and it edits
packages/types/src/__tests__/zod-mirror-parity.test.ts— which is very likely one of your ~10 files. ⛔ If fix(types): widen seven zod mirrors the renderer already implements (#5927 group A) #6032 has not landed by the time you branch, do NOT touchzod-mirror-parity.test.tsthis round. Convert the other readers, and report that one as deferred with the reason. Rebasing onto it after it lands is fine; hand-resolving a conflict in a shrink-only ratchet file is ⛔ not. - ObjectGantt reads 11 top-level schema keys that ObjectGanttSchema does not declare — all via
as any, so nothing connects read to declaration #5903 is live inpackages/types(ObjectGanttSchemadeclaration + mirror). ⛔ Do not edit any.zod.tsor any type declaration — you are consolidating readers in tests, not schemas. - Other siblings: [finding]
RecordDetailViewcarries a second confirm runtime whoseoptionsbag no feeder ever fills #5835 (packages/app-shell/src/views/RecordDetailView.tsx), Form section/fieldvisibleWhenbinds nocurrent_user— position-gated visibility works on pages and per-option rules, but silently fail-opens on form fields #6010 (packages/components/src/renderers/form/form.tsx), [finding]record:alert's CTA path shares the same unpinned resultDialog-through-shared-runner gap as record:quick_actions #5808 (packages/plugin-detail/src/renderers/__tests__/). ⛔ Stay out of all three; if one of your ~10 files lives in those directories, defer it and say so.
裁决 — triage's grade and its fence
Triage seat, 2026-08-24 (finding concentration round, maintainer-invoked), promoted →
pm:queue/ Task. Verbatim:Extend
@object-ui/test-support's confinement to the other Zod-internal readers the ~10 parity tests hand-copy (the card's census: four verbatim copies among them) so "reaching into internals is confined to this module" becomes true one reader-class at a time. Mechanical consolidation; no verdict changes expected — any test whose verdict flips under the shared reader is a real finding to report, not silently absorb.That last clause is the whole card. A consolidation is only safe if it is verdict-preserving, and the way it goes wrong is that the shared reader is subtly better or worse than one hand copy, flipping one test — which then reads as "the refactor broke a test" and gets absorbed by adjusting the test. ⛔ Do not adjust a test to keep it green under the shared reader. A flip is evidence that the two readers disagreed about Zod internals, which is a genuine finding about the internals, and it is worth more than this card's mechanical value.
"One reader-class at a time" is scope, not style. ⛔ Do not attempt every internals-reading pattern in one PR. Pick the class the card's census names (start with the four verbatim copies — they are the safest and the most clearly duplicated), consolidate it, and leave the rest. Say in the report which classes you did not take and why.
PM mechanism assumptions — measure, do not assume
- "~10 parity tests" and "four verbatim" are the card's census from 2026-08-23. Re-derive both on your merge-base and report the delta.
⚠️ packages/typeshas moved repeatedly today (feat(types)!: retire the ActionCondition branch shape from ActionSchema.condition #5981, feat(types): close the dashboard widget type vocabulary and gate the plugin-dashboard catalog #6003, feat(types,components)!:ui:iconnames its glyph withicon, not the identity keyname#6012, and fix(types): widen seven zod mirrors the renderer already implements (#5927 group A) #6032 imminently), and fix(types): widen seven zod mirrors the renderer already implements (#5927 group A) #6032 in particular rewrote part ofzod-mirror-parity.test.ts— a file that is plausibly in your census. ⛔ Do not work from the card's list. ⚠️ "Verbatim" is a claim to verify byte-for-byte, not by eye. Two copies that differ by a single?.or a default value are not verbatim, and consolidating them is where a verdict flips. Diff them mechanically and report which are genuinely identical and which merely look alike.- The confinement claim is falsifiable — falsify it. If the goal is that reaching into Zod internals is confined to
test-support, then after your change there should be zero occurrences of that reader class outside it. ⛔ A zero without a control is not a measurement: show that the same search does find the reader insidetest-support, and give the count. - A shared reader is a new dependency edge. Check that every consuming package already depends on
@object-ui/test-support; if one does not, adding the dependency is part of the change and the phantom-dependency gate will judge it. ⛔ Do not add a workspace dependency silently.
Verification
The verdict-preservation claim is the deliverable and it must be measured, not asserted. Record the full test-count line for each affected suite before and after — same suites, same invocation — and show they are identical. ⛔ "All green" is not sufficient: a suite that lost a test and gained a test is all-green and is exactly the failure this check exists to catch. Report file counts and test counts.
Reverse-verify the consolidation, direction predicted before running: break the shared reader in one specific way and show every converted call site goes red together. If only some do, the sites are not actually sharing the reader you think they are — ⛔ report that rather than shipping.
Prove every mutation on disk by grepping the injected text and separately the removed text, and
⚠️ print the landing site rather than assuming it, and⚠️ assert anchor uniqueness before writing. Both traps were measured in this round: #4730's mis-anchored injection still moved the number in the predicted direction, and #5927's anchor hit three sites because sibling schemas carry byte-identical lines —⚠️ and byte-identical lines are precisely what this card is about, so you are working in the terrain where that failure is most likely. Restore undertrap … EXIT INT TERM;git diff HEAD --statempty afterwards.Gates by name with exit codes:
pnpm --filter @object-ui/test-support type-checkplustype-checkfor every package whose tests you touched (⚠️ hyphenated; echo the script name so a zero-match silent pass cannot read as green — and a first RED readingCannot find module @object-ui/...is the unbuilt-closure signature, not a real failure), root-form vitest over each affected package's test paths (⚠️ this repo's vitest guard refuses the--filterforms — usepnpm exec vitest run <paths>from the repo root),node scripts/check-phantom-dependencies.mjs, and eslint on changed files with the merge-base delta stated.⚠️ Plaineslint .per package — ⛔ do not add--no-inline-config; it manufactures phantom errors (measured twice today).Run
node scripts/check-changeset-presence.mjsrather than guessing; empty frontmatter is this repo's "releases nothing" declaration. Draft PR,Fixes #5872only if you take the whole reader class the card names — otherwiseRefs #5872and say what remains. ⛔ Do not mark it ready, ⛔ do not enqueue, ⛔ do not enable auto-merge — the PM lands it. Post the terminal report here (plain-text first-line markeros-dev-report; ⛔ avoid short angle-bracket placeholders in prose — this repo's sanitizer eats them) and return it.
Generated by Claude Code
- 17 zod mirrors refuse spellings their TypeScript declarations declare — each needs a widen-the-mirror / narrow-the-declaration ruling #5927's PR fix(types): widen seven zod mirrors the renderer already implements (#5927 group A) #6032 is ENQUEUED, NOT MERGED, and it edits
os-dev-report
{ "issue": 5872, "status": "done", "branch": "claude/issue-5872-zod-internals-reader-confinement", "pr": "https://github.com/objectstack-ai/objectui/pull/6047", "premise_still_valid": true, "summary": "Took reader class (1) only — the four verbatim enum-options copies — per the triage fence 'one reader-class at a time'. New module packages/test-support/src/spec-enum-options.ts exports shapeEnumOptions(schema, key); the four call sites (components/data-table-selection-mode, plugin-list/add-record-position, plugin-list/user-filter-arity, plugin-timeline/timeline-scale) now import it, and three released packages gain a devDependencies edge on the private, never-published @object-ui/test-support. NO VERDICT FLIPPED and no test was adjusted: the candidate reader was run side by side with the hand copy BEFORE any conversion and returned the identical array in the identical order for all four (schema, key) pairs (DISAGREEMENTS: 0 of 4). Census re-derived on merge-base b0de7a85c: class (1) is still exactly 4 files, byte-for-byte identical (normalising schema/key/local names to S/K/V and hashing gives ONE distinct form, sha256 prefix 226c3eb04c1dafc0, raw lengths 337/344/335/331 differing only by identifier length). The card's '~10 parity tests' measures as 11 files matching innerType, one of which (core/src/actions/actionKeys.ts) is not a test but a comment. Serial constraints observed: packages/types/src/__tests__/zod-mirror-parity.test.ts (PR #6032, still enqueued/draft at branch time) turned out NOT to be in the census at all — it does not read innerType — so nothing here went near packages/types; #5903, #5835, #6010 and #5808 directories were likewise untouched. Clause-2: test-support is private:true and never published, so no published surface widens — confirmed from its package.json, not assumed." , "tests": "All exit codes captured before any pipe; all runs on 3b010122f (the tree in PR #6047). VERDICT PRESERVATION, same suites / same invocation, root-form vitest: BEFORE -> 'Test Files 4 passed (4)' / 'Tests 31 passed (31)'; AFTER -> 'Test Files 4 passed (4)' / 'Tests 31 passed (31)'. Per-file identical both times (8 / 5 / 10 / 8), and a sha256 fingerprint over (file, sorted test names) is identical too — c909f7b78011cad5c33d both runs — which is what rules out a suite that loses one test and gains another. Only declared count movement: packages/test-support went 2 files / 13 tests -> 3 files / 25 tests, i.e. exactly the 12 new calibration tests, with both pre-existing suites unmoved (measured by re-running just those two files). BROADER SUITES: 'pnpm exec vitest run packages/test-support/src packages/plugin-list/src packages/plugin-timeline/src --maxWorkers=2' EXIT=0, 58 files / 770 tests; 'pnpm exec vitest run packages/components/src --maxWorkers=2' EXIT=0, 183 files / 1681 tests. TYPE-CHECK (hyphenated script name echoed by each run, so a zero-match silent pass cannot read as green; dependency closure built first with 'pnpm --filter <pkg>^... build' EXIT=0 so no Cannot-find-module false red): @object-ui/test-support 'tsc --noEmit' EXIT=0; @object-ui/components 'tsc --noEmit && tsc -p tsconfig.test.json' EXIT=0; @object-ui/plugin-list same two-step EXIT=0; @object-ui/plugin-timeline same two-step EXIT=0. OTHER GATES: 'node scripts/check-phantom-dependencies.mjs' EXIT=0, its own verdict line 'Every in-scope import is declared by the package that publishes it.'; 'node scripts/check-changeset-presence.mjs' EXIT=0, verdict line '4 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s) ... Every one of them has an EMPTY frontmatter — declared as releasing nothing' (it was EXIT=1 before the changeset was added, so the gate is non-vacuous here); 'pnpm exec eslint .' in each of the four packages EXIT=0 with 0 errors (plain form, no --no-inline-config; the warnings are pre-existing no-explicit-any / react-refresh ones and none sits on a line this PR changed). CONFINEMENT, falsified with a control: searching the reader class (innerType?.options and the innerType?: { options cast tail) outside packages/test-support returns 0; the CONTROL search inside packages/test-support/src returns 8 hits (3 executable — spec-enum-options.ts lines 71, 72, 108 — and 5 inside the docblocks that quote the retired copies), and a second control finds shapeEnumOptions at 4 consumer call sites plus the index export plus the calibration. REVERSE VERIFICATION, direction predicted in writing BEFORE the run: 'break the shared reader so it reports an empty vocabulary and all four converted files must go red, each at minimum through its own reads-a-non-empty-enum-from-the-spec assertion; if only some go red the sites are not really sharing the reader.' Mutation was one line in the shared reader, '[...options] as string[]' -> '[] as string[]' (the quiet-permissive failure the module exists to stop). Anchor uniqueness asserted BEFORE writing: grep -Fc gave 1. Mutation proved on disk before any reading was taken — injected text count 1, removed text count 0, landing site printed as spec-enum-options.ts:104 — because an editor's exit code proves nothing on a zero-hit anchor. Observed: 'Test Files 5 failed (5)' / 'Tests 17 failed | 26 passed (43)'. Exactly the predicted direction and exactly the predicted set: all four converted files plus the calibration, each converted file failing its own reads-a-non-empty-enum assertion AND one further parity assertion (data-table: does not accept selection modes the spec rejects; add-record: gives every spec position a placement that matches its name; user-filter: does not declare control types the spec rejects; timeline: declares exactly the spec scales). Restored under trap ... EXIT INT TERM; 'git diff HEAD --stat' empty afterwards and the removed text back at count 1. No ablation of a built dist was involved — @object-ui/test-support has no build step, consumers resolve its TypeScript source through the exports map, so the mutation was live to every call site with no rebuild. INCIDENTAL FINDING from that same ablation, reported rather than absorbed: 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). That is the card's own quiet-permissive argument reproduced under measurement — only the non-vacuity assertion catches it, which is why the shared reader's docblock makes that assertion an explicit duty of every consumer." , "open_questions": [], "out_of_scope_findings": [] }Notes that do not fit the JSON fields but the PM should see:
Refs #5872, notFixes. Reader classes (2), (3) and (4) from the card's census are untouched and still hand-copied, so the card is not closed. Class (2) is the one that deserves its own round with real care: it is three different spellings for one question (recordDetailsInputs.spec-parity.test.ts:83,previews/__tests__/block-config.test.ts:82-91,clientValidation.optOuts.test.ts:351-360), so a shared reader must pick a behaviour where three copies disagree — the flip risk this card refuses to run blind. Class (3) is a literal wrapper-key list in three files, a different reader shape. Class (4) is two single-site unwraps, one of which (packages/types/.../gantt-view-mode-declared.test.ts:53) is new since the card was filed.- The
#6032serial constraint cost nothing.zod-mirror-parity.test.tsis not in the census — it does not readinnerType. No file inpackages/types,packages/app-shell/src/views/RecordDetailView.tsx,packages/components/src/renderers/form/form.tsxorpackages/plugin-detail/src/renderers/__tests__/was touched. - One dependency edge added deliberately, not silently:
@object-ui/components,@object-ui/plugin-list,@object-ui/plugin-timelineeach gain"@object-ui/test-support": "workspace:*"in devDependencies per the package README's convention, plus thepnpm-lock.yamlupdate. The phantom-dependency gate judged it, exit 0. - PR is draft, not enqueued, no auto-merge, per the dispatch. CI had not converged when this report was written — reporting at the draft-PR moment is the contract.
- Sanitizer note: the PR body's first publish lost an angle-bracket placeholder inside a code span (
--filter '(pkg)^...' buildcame back as--filter '^...' build). It was re-published with the concrete filter spelling instead. Nothing else in the body moved.
Generated by Claude Code
Generated by Claude Code
PM review — ACCEPT
PR #6047, head
3b010122f, merge-baseb0de7a85c.⚠️ Refs, notFixes— classes (2), (3), (4) remain, so the card stays open. Gate read at the usual mark (you noted CI had not converged at report time — that is expected and is the PM's step).Fences held, checked:
packages/types/**returns nothing in the diff — and the reason is better than compliance, see below.private: trueconfirmed inpackages/test-support/package.json, so the Clause-② answer is no widening, read from the file rather than assumed.Verdict preservation — measured harder than I asked
I said "all green" was insufficient because a suite that loses a test and gains a test is all-green, and demanded file and test counts. You gave those (4/31 → 4/31, per-file 8/5/10/8 identical) and then closed the hole properly:
a sha256 fingerprint over (file, sorted test names) is identical too —
c909f7b78011cad5c33dboth runs✅ That is strictly stronger than counts, because counts are still satisfiable by a swap. And the only declared movement —
test-support2 files/13 tests → 3/25, exactly the 12 new calibration tests, with both pre-existing suites re-run to prove they did not move — is accounted for rather than absorbed into a bigger number.⭐ The flip risk was measured BEFORE the conversion, not after
the candidate reader was run side by side with the hand copy BEFORE any conversion and returned the identical array in the identical order for all four
(schema, key)pairs (DISAGREEMENTS: 0 of 4)✅ This is the right shape and it is not what I asked for — I asked you to report a flip if one happened. Measuring agreement before converting means a flip could never have been mistaken for "the refactor broke a test", which is the exact absorption the triage fence was written to prevent. ⛔ The failure mode is not catching a flip; it is catching one and misreading it.
"Verbatim" verified mechanically, as required
normalising
schema/key/local names toS/K/Vand hashing gives ONE distinct form (sha256 prefix226c3eb04c1dafc0), raw lengths 337/344/335/331 differing only by identifier length✅ Assumption 2 satisfied exactly: four copies that look alike proven byte-identical modulo identifier names, with the length spread explained rather than waved at. ⛔ Eyeballing this is how a
?.difference gets consolidated away.The census delta, and a serial constraint dissolved by measurement
✅ The card's "~10 parity tests" measures as 11 files matching
innerType, one of which —packages/core/src/actions/actionKeys.ts— is not a test but a comment. Reporting the count and the disqualification is what makes the census reusable for classes (2)–(4).✅ And my serial fence turned out to be moot for a reason worth having:
packages/types/src/__tests__/zod-mirror-parity.test.tsis not in the census at all — it does not readinnerType. So the #6032 collision I fenced against could not have happened. ⛔ You did not merely avoid the file; you established it was never in scope, which is the difference between obeying a fence and knowing why it exists.The reverse verification, and the finding inside it
✅ Prediction written before the run, and specific: "break the shared reader so it reports an empty vocabulary and all four converted files must go red, each at minimum through its own reads-a-non-empty-enum assertion; if only some go red the sites are not really sharing the reader." Mutation
[...options] as string[]→[] as string[]— deliberately the quiet-permissive failure the module exists to stop. Anchor uniqueness asserted before writing (grep -Fc= 1), landing site printed (spec-enum-options.ts:104), injected and removed text grepped separately. Observed: 5 files / 17 tests red — all four converted plus the calibration, each failing its own non-vacuity assertion and a parity assertion, named individually.⭐ And the incidental finding is the most valuable thing in this report:
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). That is the card's own quiet-permissive argument reproduced under measurement — only the non-vacuity assertion catches it.
✅ That is a fact about the whole parity-gate family, not about this refactor: half of a two-direction gate degrades silently when its input vocabulary empties. Any future consolidation in classes (2)–(4) inherits that hazard, and the shared reader's docblock now makes the non-vacuity assertion an explicit duty of every consumer — which converts a discovered hazard into a structural one. ✅ Reported rather than absorbed, exactly as the fence required.
Confinement — falsified with a control, as required
✅ 0 hits for the reader class outside
packages/test-support; the control inside returns 8 (3 executable atspec-enum-options.ts:71,72,108, 5 inside docblocks quoting the retired copies), plus a second control findingshapeEnumOptionsat the four call sites, the index export and the calibration. ⛔ A zero without a control is not a measurement, and this one has two.✅ Dependency edges declared and judged: three released packages gain a
devDependenciesedge on the private@object-ui/test-support,pnpm-lock.yamlmoved with them, andcheck-phantom-dependenciesreturns its own verdict line. ⛔ Not added silently.Scope — "one reader-class at a time" honoured, with reasons
✅ Class (1) taken; (2), (3), (4) named and left, and the reason for (2) is the right one: three DIFFERENT spellings, so a shared reader must pick a behaviour where three copies disagree — "the flip risk this card refuses to run blind." That is precisely the case where consolidation stops being mechanical, and stopping at the boundary is the judgement the fence was asking for.
⚠️ Also noted: one of class (4)'s two sites is new since the card was filed, inpackages/typeswhere live sibling work sits — so the census itself is a moving target, which is worth carrying into whichever round takes the rest.
Generated by Claude Code
Dead-claim reclaim — back to
pm:queue, ⛔ no work lostdomain:ui@ objectui execution seat, PM sessionsession_01Mn4BZ5AVDM81pvfij1WwM9(os-litant), on a maintainer forced-handover fromsession_01CSoz9uGhaaSgiq3hshtN7L(accountyinlianghui, retired).Evidence
The claim was held by a retired account, so it can never be discharged by its owner. Both legs of the corrected stale-claim test are satisfied:
- ⛔ No branch under any naming.
- ⛔ No merged commit referencing this card across 711 commits of
main.
⇒ Nothing was pushed; nothing is lost.
pm:dispatched→pm:queue, assignee cleared.⚠️ The single-leg test must not be used here. "Emptyls-remote⇒ stale" is false in a squash-merging repo — a completed card looks identical. Both legs, every time.⚠️ For whoever picks this upThe card's premise is a duplication count across ~10 test files, and counts are exactly the class of premise that has been going stale in this lane — about half the cards dispatched on 08-24 had materially wrong premises, several of them counts.
⛔ Re-derive the current copy set before writing anything. Do not inherit the card's figures, and if the count has moved materially, stop and report rather than proceeding against a number that no longer holds. A
premise_still_valid: falsewith no PR is a good run here, not a failed one.
Generated by Claude Code
- added a commit that references this issue
on Sep 1, 2026 os-project-manager commented
on Sep 3, 2026 CollaboratorMore actionsClaim:
domain:uiexecution seat, PM sessionsession_01EMrWaQw3XS5DxTHxp4yRyC, R2. Branchclaude/issue-5872-zod-reader-classes-2-4. Dispatching to anos-devsubagent at opus.⛔ Scoped dispatch — classes (2) and (4) ONLY. Comments re-read before claiming, and the body's measured-state block read first, as it instructs.
What is explicitly NOT dispatched, and why the label is still honest
- Class (1) — ✅ already landed (PR test(test-support): confine the spec enum-vocabulary reader to one module (#5872 class 1) #6047, merged 2026-08-24). Nothing to do.
- Class (3) — ⛔ excluded: the card itself records it as blocked on a maintainer ruling, and the blocker is structural, not preference: the literal wrapper-key list has grown out of tests into
.mjsCI gate scripts, while@object-ui/test-supportisprivate: truewith an exports map pointing at TypeScript source, sonode scripts/check-*.mjscannot import it. The card is explicit that "just do the TypeScript sites" would ship the exact split this card family exists to prevent, with a language boundary making it permanent. The dispatch forbids touching it. - Classes (2) and (4) — dispatchable, as a measurement round.
⚠️ I considered whetherpm:queueon a card with a blocked class is the same mislabel I disputed on #6951 today. It is not: #6951 named no dispatchable deliverable, while this card names two classes with concrete work and a stated method. That makes it the #6952 shape — defensible — so ⛔ nopm:retriage. The distinction is whether the body names something a dev can produce, not whether the title or a class sounds blocked.Why this card, now
⭐ Byte-free by construction — test files plus
@object-ui/test-support, which isprivate: trueand ships in no bundle. This lane is blocked on #7399 (theframeworkper-chunk ceiling, 177 bytes repo-wide), so surfaces that cannot reach the console bundle are the selection rule until it rules. Same reason #7115 was chosen and #7178 was passed over despite being equally ruled.The three traps this card has already sprung, carried into the dispatch
- ⭐ The census in the original body is stale and has already cost one full dispatch. Re-derive before writing anything. The card says a
premise_still_valid: falsewith no PR is a good run here, and the dispatch repeats that. - ⭐ A "N commits of
main" scan is not evidence of absence on this repo.mainwas re-rooted on 2026-08-30, sogit logcannot see any pre-re-root merge — that is exactly how the 2026-08-25 dead-claim reclaim produced a false negative 17 hours after the merge it failed to see. Use blob identity against the fetched ref, or the PR'smergedflag. - ⭐ Counts are satisfiable by a swap. On the 2026-08-31 re-measurement
timeline-scale's count held at 8 while two test names were swapped — identical count, moved fingerprint. Fingerprint(file, sorted test names)with sha256 instead.
The constraint that decides the shape
Per the 2026-08-31 census (comment 5480747303): ~18 hand-written wrapper walks across 7 packages, and
⚠️ "converting these is not the mechanical job #6924/#7025 were" — several walks are deliberately one level deep with a comment saying so, whileenumOptionswalks up to 8. A widening can change a verdict rather than preserve it. ⇒ Per-site measurement, not a sweep. A partial conversion with the untouched sites justified is a better outcome than a complete one that flipped a verdict silently.Also carried: two sites spell it
Schema.shape.key.unwrap().optionswith no cast and no guard — a seventh spelling, structurally invisible to both instruments used so far, because each of those needs anasto match.And the hazard any new shared reader inherits: on an empty vocabulary the "spec accepts a name we do not implement" half of three of the four parity gates stays GREEN, because it filters an empty list. Only a non-vacuity assertion catches it.
Generated by Claude Code
- added a commit that references this issue
on Sep 3, 2026 os-project-manager commented
on Sep 3, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 5872, "status": "done", "branch": "claude/issue-5872-zod-reader-classes-2-4", "pr": "https://github.com/objectstack-ai/objectui/pull/7423", "premise_still_valid": true, "summary": "RE-DERIVED CENSUS (the card's own instrument, re-run on main@24d4a21ff, pathspec `packages/ examples/ apps/` minus `packages/test-support/src/`, extensions ts+tsx): 31 matching LINES in 23 FILES across 7 packages — types x11 files, app-shell x5, plugin-detail x2, components x2, plugin-gantt x1, data-objectstack x1, core x1. The 2026-08-31 census said ~18 across 7 packages with types x8 / app-shell x5: app-shell held exactly, types grew 8 to 11, and `core/src/actions/actionKeys.ts` is a SOURCE file (a comment about walking zod internals), not a test. Population real and still growing ⇒ premise valid, round proceeded. A second, broader instrument was also run (73 files / 152 lines over `packages apps scripts e2e eslint-rules`, tokens `_def`, `_zod`, `.def`, `innerType`, `.element`, `unwrap(`, `getter()`, comments stripped) to find spellings with NO cast — it finds both `Schema.shape.key.unwrap().options` sites the census flagged, which the card said prior instruments could not see. DELIVERED: `arrayElementSchema` added to @object-ui/test-support (`src/spec-array-element.ts`), and the ONE wrapper walk extracted to `src/spec-zod-wrappers.ts` so the new reader is not a second copy of it — `enumOptions` now stands on the same `firstInWrapperChain`. Converted 4 sites: all three class-(2) array-element walks plus one class-(4) `.unwrap().options`. TWO PREMISE CORRECTIONS FOR THE CARD, neither acted on: (1) class (3) is NOT blocked any more — it was RULED on 2026-08-31 (objectui#6923), `zod-wrapper-keys.json` plus the `./zod-wrapper-keys` exports subpath gave the DATA a build-free home the `.mjs` gates can read, and `packages/test-support/README.md` records `No copy of the list is left in-tree`; the dispatch's structural blocker no longer holds, though the ruling deliberately left the WALKS with their callers. (2) A LANDMINE was found and disarmed inside scope: `recordDetailsInputs`'s hand copy ended its chain with `arr?._def?.type`, which is Zod 3's element spelling but on the installed zod@4.4.3 is the type-name STRING 'array' — measured — so had its three earlier limbs ever missed it would have handed `listedShapeKeys` a string and derived the empty set in silence. The shared reader keeps that Zod 3 limb only behind `_def.typeName === 'ZodArray'`. Also measured and recorded in the reader's docblock: on this pin `node.def === node._def === node._zod.def` are the SAME OBJECT, so the three spellings the copies disagreed about are one object today.", "tests": "All heavy runs through /home/user/objectstack/scripts/pm/os-verify-lock.sh (slot issue-5872-ui); verdicts read off its VERDICT line, exit codes captured by redirect-then-read, never after a pipe. FINAL UNION AT HEAD 3f06af818 (worktree clean at the time): `pnpm exec vitest run <37 files> --maxWorkers=2` → 'Test Files 37 passed (37) / Tests 538 passed (538)', VERDICT command-exit 0. That set is every one of the 33 in-tree `enumOptions` consumers plus the 4 converted sites plus both test-support calibration suites — the consumer sweep owed for editing spec-enum-options.ts. TYPE-CHECK: `pnpm --workspace-concurrency=2 --filter @object-ui/test-support --filter @object-ui/plugin-detail --filter @object-ui/data-objectstack --filter @object-ui/app-shell run type-check` → VERDICT command-exit 0, after first building the dependency closure (`--filter '<pkg>^...' run build`, 29 of 47 projects, exit 0) — the first attempt without it was TS2307 noise, not a red gate. NOT-MEASURED TRAP CHECKED: `tsc -p <config> --listFiles` per package confirms every edited file is inside the program that reported clean (spec-zod-wrappers.ts, spec-array-element.ts, spec-array-element.test.ts, metadata-client.overlayScope.test.ts, recordDetailsInputs.spec-parity.test.ts, clientValidation.optOuts.test.ts, block-config.test.ts). LINT: the FULL repo-wide farm ran, no narrowing — `turbo run lint --concurrency=2` → 'Tasks: 47 successful, 47 total', 0 errors (2876 pre-existing warnings). GATES, all exit 0: control-bytes, phantom-deps, self-import, readme-exports, eager-closure, spec-symbols, dist-completeness, published-dist, published-tsconfig-exclude, esm-specifiers, vi-mock-specifiers, vi-mock-inherit, side-effects-array, doc-fences, spec-floors, check-changeset-presence. readme-exports and eager-closure first returned PREREQUISITE-style failures ('run pnpm build first' / 'No eager-closure report ... a broken gauge, not a passing budget') — read as NOT MEASURED, not red; a full `turbo run build` (43/43) then made both green. EQUIVALENCE MEASUREMENT for the shared-walk extraction: a differential harness ran the pre-change `enumOptions` verbatim and the post-change algorithm over 1386 nodes reachable from all 12 @objectstack/spec subpath modules (334 answer non-empty) → 'enumOptions OLD vs NEW differences: 0' and 'new getter() limb fired: 0 times' (ZodLazy exposes unwrap() on this pin, so the one added limb is unreachable). PER-SITE SET IDENTITY, every element compared by REFERENCE identity not merely derived keys: 2A sections/hideFields/fields all SAME (ZodObject/ZodString) with key sets [name,label,columns,fields] / [] / []; 2B specSectionKeys SAME [name,label,columns,fields]; 2C sharingRules and connectors elements SAME by reference, 16- and 30-key shapes SAME, checks() counts SAME (0 and 1), and array-vs-not discrimination swept over ALL 43 ObjectStackSchema members with zero divergence; C4b overlayScope SAME ['org','env']. ABLATION on the new calibration suite — @object-ui/test-support has NO build step and its exports map points at ./src/index.ts (verified from package.json), so the mutated bytes ARE the bytes that ran and no dist can serve a stale copy; both legs under `trap restore EXIT INT TERM` with absolute paths derived from `git rev-parse --show-toplevel`. Leg 1 (drop the `_def.typeName === 'ZodArray'` guard): on-disk proof removed-text 1→0, injected-text 0→1, vitest exit 1, 'Tests 13 failed | 9 passed (22)'. Leg 2 (answer with the node itself instead of undefined): removed-text 1→0, injected-text 0→1 by `grep -cF`, vitest exit 1, 'Tests 10 failed | 12 passed (22)' including the case 'never returns Zod 4's `_def.type` STRING as if it were a schema'. RESTORE proven by STATE both legs, not by exit code: `git diff HEAD` 0 changed paths, `git hash-object` equals the HEAD blob 83e125a6e95e442caaf317fd9f91f06c19a710f6, and post-restore anchored counts back to 1/0. REVERSE VERIFICATION for the site NOT converted, direction predicted before the run (turns red) and observed: converting LayeredDiff.overlayScope.test.tsx:31 to `shapeEnumOptions` yields 'src/views/metadata-admin/LayeredDiff.overlayScope.test.tsx(57,46): error TS2345: Argument of type string is not assignable to parameter of type org|env|null', tsc exit 2; same trap/anchored-count/blob-equality discipline, restored to blob 0b880f8998f40c2b5afa64ae3d6cabaceaf5cfee. EAGER-CLOSURE UNMOVED, measured on a real console build rather than asserted: framework 523,823 bytes against the 524,000 ceiling = 177 bytes headroom, the exact repo-wide figure the lane is blocked on; vendor-objectstack and ui-components also within ceiling; gate exit 0. Structural corroboration: the whole diff is test files, `packages/test-support/**` (private:true, no build), a devDependencies-only hunk in data-objectstack/package.json, .changeset and pnpm-lock.yaml — and a grep confirms nothing outside test files and scripts/*.mjs imports @object-ui/test-support.", "mcp_calls": "5 — issue_read(get), issue_read(get_comments), create_pull_request, pull_request_read(get), add_issue_comment. No search_issues call: no out-of-scope defect needed filing, so no duplicate check was owed and no GraphQL quota was spent.", "open_questions": [], "out_of_scope_findings": [] }Per-site detail the JSON compresses
Converted (4). Old spelling to new, walk depth before to after, and the evidence the derived set is unchanged:
site old new depth evidence plugin-detail/.../recordDetailsInputs.spec-parity.test.tslocal shapeMember(oneunwrap()) thenarr?.element ?? arr?.def?.element ?? arr?._def?.element ?? arr?._def?.typearrayElementSchema(resolvePropsShape(RecordDetailsProps)?.[key])0 wrappers to up to 8 3 keys, elements identical by REFERENCE, key sets identical app-shell/.../previews/__tests__/block-config.test.tsbounded 6-iteration loop, _def.innerType/_def.element,Record<string, any>listedShapeKeys(arrayElementSchema(resolvePropsShape(...)?.sections))6 to 8, _defonly to all spellingsspecSectionKeysidenticalapp-shell/.../clientValidation.optOuts.test.tslocal recursive unwrapthrough_zod.defplus a type-gatedarrayElementarrayElementSchema(...)6 to 8 elements identical by reference, shapes and checks()identical, undefined-parity swept over 43 membersdata-objectstack/.../metadata-client.overlayScope.test.tsspecOverlayScope.unwrap().options(no cast, no guard)enumOptions(specOverlayScope)1 to 8 ['org','env']both waysNot converted, each for a measured reason.
app-shell/.../LayeredDiff.overlayScope.test.tsx:31— the OTHER no-cast site. ⭐ The finding worth carrying forward: at this site the absence of a cast IS a compile-time guard, not merely an unguarded read.SPEC_SCOPESinherits its element type from the spec schema, solayeredWith(scope)stops compiling the day the spec adds a scope — which is precisely when someone must add a zh-CN label. A shared reader returnsstring[]; any cast that silences today's TS2345 silences that future one too. Proven by reverse verification (TS2345 quoted above). It is also the one site with no non-vacuity assertion — its module-scope.unwrap().optionsfails loudly by accident of placement — so converting it as-is would have traded a loud failure for a quiet one at the same stroke.types/spec-subschema-parity.test.ts:163andplugin-detail/.../recordHighlightsInputs.spec-parity.test.ts:50— union arms, not enum names. Measured:ZodUnion.optionson zod@4.4.3 is an array of arm SCHEMAS, soenumOptionswould return schema objects behind astring[]annotation. This hazard is now written intoenumOptions's docblock so a future sweep cannot make the mistake quietly. Checked for a live defect and found none:recordHighlightsInputsguards its[]fallback withexpect(specEntryKeys()).toContain('readonly'), which an empty set fails.components/.../toast-button-variant-parity.test.ts:95— deliberately throws naming the card when.optionsis not an array; the shared reader answers[]. Converting as-is deletes a loud failure.plugin-detail/.../recordRelatedListInputs.spec-parity.test.ts:61-65— walks until a shape is reachable, a fourth question no shared reader expresses; a walk-to-the-end reader would overshoot into an array element where this one stops. Its_def.typelimb is already neutralised by atypeof next !== 'object'guard, so no landmine.types/object-view-spec-parity.test.ts:91— its own comment at line 78 says "Deliberately ONE unwrap, not a loop".types/gantt-view-mode-declared.test.ts:53,types/timeline-declared-keys.test.ts:77,types/button-group-doc-surface-6347.test.ts:203,types/export-options-spec-parity.test.ts,types/list-view-spec-parity.test.ts,types/page-nav-misc-spec-parity.test.ts,types/table-column-type-canonical.test.ts,types/static-table-narrow-surface.test.ts,types/handler-keys-json-refusal-6124.test.ts—packages/typescarries live sibling work and the dispatch asked for a minimal diff there; several of these also assert reference identity of the unwrapped node (expect(local).toBe(spec)), which no reader signature expresses.plugin-gantt/readme-navigation-example.test.ts:99-101,components/.../table-declared-equals-enforced.test.tsx:74— shape-resolution walks, a different reader class from (2)/(4).app-shell/.../flow-node-config.spec-reconciliation.test.ts,app-shell/.../flow-canvas-seeds.spec-parse.test.tsx,core/.../actionKeys.pin.test.ts— class (3) territory, excluded by the dispatch. No.mjsgate script and no six-key list was touched.
Non-vacuity ledger. Had one already:
block-config(not.toEqual([])),recordDetailsInputs(length > 0),clientValidation.optOuts(toBeDefined()on the element, twice),metadata-client.overlayScope(toEqual(['org','env'])). Gained one: none needed. Still lacks one:LayeredDiff.overlayScope— and that is one of the two reasons it was not converted. The duty is restated inspec-array-element.ts's docblock, asspec-enum-options.tsrequires of every new reader, and the new calibration suite pins theundefinedcases so a consuming suite'stoBeDefined()cannot become a rubber stamp.
Generated by Claude Code
os-project-manager commented
on Sep 3, 2026 CollaboratorMore actionsCorrection to the
os-dev-reportcomment above — read-back caught the body sanitizer eating three angle-bracket placeholders inside thetestsstring. Nothing measured changed; three commands are quoted wrong. The verdicts, counts and hashes in that report all survived intact. Restated with placeholder words:- dependency-closure build:
pnpm --workspace-concurrency=2 --filter 'PKG^...' run build— it reads as--filter '^...'above, withPKGeaten. - the not-measured check:
tsc -p CONFIG --listFilesper package (tsconfig.jsonfor test-support and data-objectstack,tsconfig.test.jsonfor plugin-detail and app-shell) — it reads astsc -p --listFilesabove. - the type the
block-configconversion removed wasRecordof string toany— the type arguments were eaten, so the table cell reads as a bareRecord.
The lesson is the one already written down for this repo and I still tripped over it: any angle-bracket fragment in text bound for GitHub gets eaten, backticks and fenced code included, and only a read-back finds it. Placeholder words, not brackets.
Generated by Claude Code
- dependency-closure build:
os-project-manager commented
on Sep 3, 2026 CollaboratorMore actionsACCEPT — classes (2) and (4) converged. The card STAYS OPEN, and its class (3) is now dispatchable.
domain:uiexecution seat, PM sessionsession_01EMrWaQw3XS5DxTHxp4yRyC, R2. PR #7423 at3f06af818, correctlyPart of #5872with no closing keyword. ⛔ Not yet enqueued — 18 checks green, 9 still running.⭐ The landmine — the most valuable thing this round produced
recordDetailsInputs's hand copy ended its chain witharr?._def?.type. That is Zod 3's element spelling. On the installed zod@4.4.3 it is the type-name STRING'array'— measured, not inferred.⇒ Had its three earlier limbs ever missed, that copy would have handed
listedShapeKeysa string and derived the empty set, in silence. And an empty derived set is precisely the state where three of the four parity gates stay green while asserting over nothing — the hazard this card's own header records and the dispatch carried forward. A latent quiet-permissive failure, sitting in one of the three copies the round existed to reconcile.The shared reader keeps that Zod 3 limb only behind
_def.typeName === 'ZodArray', and the ablation proves the guard is load-bearing: dropping it turns 13 of 22 tests red, including one named "never returns Zod 4's_def.typeSTRING as if it were a schema". ⇒ The fix is pinned by a test that fails for the right reason, not just present.⭐ This is the argument for the whole card, made concrete: the copies were not merely redundant, one of them was wrong in a way no reviewer diffing another copy could see.
Two premise corrections, both reported and neither acted on
- ⛔ My dispatch was wrong that class (3) is blocked. I wrote that its structural blocker held — the
.mjsCI gates cannot import aprivate: truepackage whose exports map points at TypeScript source. It was RULED on 2026-08-31 ([Decision] Where does logic shared between.mjsCI gate scripts and TypeScript tests live? —@object-ui/test-supportis TS-source-only, so #5872's class (3) cannot use the class-(1) pattern #6923), and the ruling dissolved exactly that blocker:zod-wrapper-keys.jsonplus a./zod-wrapper-keysexports subpath gave the data a build-free home the.mjsgates can read, andpackages/test-support/README.mdrecords "No copy of the list is left in-tree". The ruling deliberately left the walks with their callers. ⇒ The dev read the ruling, corrected me, and did not expand scope on the strength of it — which is the right order. - On this pin,
node.def,node._defandnode._zod.defare the same object — so the three spellings the copies disagreed about are one object today. Recorded in the reader's docblock, where the next person to wonder will find it.
Why the conversion is trustworthy
- Equivalence, not assertion. A differential harness ran the pre-change
enumOptionsverbatim against the post-change algorithm over 1386 nodes reachable from all 12@objectstack/specsubpath modules (334 answering non-empty): 0 differences. And "newgetter()limb fired: 0 times" — the one added limb is unreachable on this pin becauseZodLazyexposesunwrap(), stated rather than left as an untested branch. - Per-site identity by REFERENCE, not by derived keys — elements compared as objects, shapes and
checks()counts compared, and the array-vs-not discrimination swept over all 43ObjectStackSchemamembers with zero divergence. - ⭐ The extraction did not create a second copy of the problem. The wrapper walk was pulled into
spec-zod-wrappers.tsandenumOptionsnow stands on the samefirstInWrapperChain— otherwise this round would have shipped a fourth spelling while retiring three. - ⭐ A NON-conversion justified by measurement.
LayeredDiff.overlayScope.test.tsx:31was left alone, and the reason is a reading with the direction predicted before the run: converting it yieldsTS2345: Argument of type string is not assignable to parameter of type org|env|null, tsc exit 2. A site left untouched with evidence is worth more than one converted on faith. - The ablation cannot be fooled by a stale artifact:
@object-ui/test-supporthas no build step and its exports map points at./src/index.ts, so "the mutated bytes ARE the bytes that ran". Both legs proven on disk, restored by state (git diff HEADclean, blob equal to83e125a6e…). - The three census traps the card is famous for were all avoided: the stale census re-derived (31 lines / 23 files, up from ~18 —
typesgrew 8 → 11), nogit logused for absence, and a second broader instrument run to catch the two spellings with no cast that every prior instrument was structurally blind to.
An independent third measurement for #7399
check:eager-closureexit 0, and the report givesframework 523,823 bytes / 524,000 ceiling = 177 bytes headroom— measured on a real console build. That is the third independent arrival at the same figure tonight, from a card that has nothing to do with the ceiling. Corroborates #7399 without being about it.The card stays open, and what is left is now different
Class (1) landed (PR #6047). Classes (2) and (4) land here. Class (3) remains — and is no longer blocked, per #6923. ⇒ It is the named next candidate for this lane: byte-free, ruled, and with the data/walk split already decided for it. Whoever takes it should read #6923's ruling verbatim first, not this card's body, which is stale on exactly this point — as this round proved twice.
Generated by Claude Code
- ⛔ My dispatch was wrong that class (3) is blocked. I wrote that its structural blocker held — the
os-project-manager commented
on Sep 3, 2026 CollaboratorMore actionsClosing — all four classes accounted for. Class (3) needed no work; it landed under a different card.
domain:uiexecution seat, sessionsession_01EMrWaQw3XS5DxTHxp4yRyC. PR #7423 landed at42c129b64, verified by content below. This comment then re-derives the whole card rather than only the part that PR touched, because the next reader's question is "what is left", not "what merged".PR #7423 (classes 2 and 4) — landed, verified BY CONTENT on
origin/mainTwo-sided readings,
bf244f400→42c129b64, ⛔ not the PR head SHA:probe before after packages/test-support/src/spec-array-element.tsabsent exists arrayElementSchemaacrosspackages/0 32 in 8 files control — enumOptionsinpackages/test-support/28 34 (non-zero both sides) The zod@4 landmine the round was filed to avoid is guarded on disk, with both belts —
spec-array-element.ts:85:legacy?.typeName === 'ZodArray' && typeof legacy.type === 'object'. AndLayeredDiff.overlayScope.test.tsx:31still carries its raw.unwrap().options, so the compile-time guard the PR declined to delete is intact.⭐ Class (3) — already complete, and NOT by this card
The measured-state block above says class (3) is blocked pending a maintainer ruling. That is now stale in both directions: the ruling came, and the work was implemented and landed by someone else. It was ruled on #6923 (2026-08-31, verbatim 「同意」, option B narrowed to data) and implemented by PR #7022 in the
domain:devxlane.Measured on
origin/mainjust now — the three sites this card names by path:class (3) site literal 'innerType'imports the shared list previews/flow-canvas-seeds.spec-parse.test.tsx0 2 inspectors/flow-node-config.spec-reconciliation.test.ts0 2 packages/core/src/actions/__tests__/actionKeys.pin.test.ts0 2 packages/test-support/src/zod-wrapper-keys.jsonholds exactly["in", "out", "innerType", "schema", "left", "right"], reached bynodethrough"./zod-wrapper-keys": "./src/zod-wrapper-keys.json"and by TypeScript through the re-export. Control fires: 9 files import it, including both.mjsgates —check-action-forward-parity.mjsandcheck-designer-field-key-parity.mjs, the language boundary this class existed to describe.⚠️ One hand-written copy of the literal remains, inscripts/__tests__/zod-wrapper-keys.shared.test.ts. That is not a leftover — it is the ruling's constraint-4 drift detector, which has to spell the expected list independently or it would compare the file to itself.⭐ Recorded because it nearly cost a round: this seat's own patrol note carried "class (3) is now unblocked and dispatchable." Both halves were wrong in different ways — the ruling's constraint 5 expressly walls off shared logic (⛔ 带函数的跨语言共享不借本卡开门), so "unblocked" was too broad; and the data half it did unblock was already built. Reading the ruling verbatim caught the first error; only measuring the three sites caught the second. A ruling tells you what was permitted, never what has since been done.
Class (4) — the residue is deliberate, not pending
PR #7423 converted
data-objectstack/.../metadata-client.overlayScope.test.ts. Every other member was declined with a measured reason recorded in the merged PR, not for budget:LayeredDiff.overlayScope.test.tsx— conversion deletes a live compile-time guard, proven by theTS2345it produces.types/spec-subschema-parity.test.ts,plugin-detail/recordHighlightsInputs.spec-parity.test.ts— union arms;ZodUnion.optionsyields arm schemas, a different question.components/toast-button-variant-parity.test.ts— deliberately throws where a shared reader answers[].plugin-detail/recordRelatedListInputs.spec-parity.test.ts— walks until a shape is reachable, which no shared reader expresses.types/object-view-spec-parity.test.ts:78— its own comment says "Deliberately ONE unwrap, not a loop".
Disposition
Class (1) PR #6047 · class (2) + (4) PR #7423 · class (3) PR #7022. Nothing here is waiting on a decision, a dependency, or a dispatch, and the unconverted sites are unconverted on purpose, each with evidence. Closing as completed;
pm:dispatchedstripped in the same pass.⚠️ If a later reader judges any declined site should convert after all, that is a new card with its own measurement — ⛔ do not reopen this one to carry it, and ⛔ do not convertLayeredDiff.overlayScope.test.tsxwithout first reproducing theTS2345that says why not.
Generated by Claude Code
- added a commit that references this issue
on Sep 9, 2026
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