feat(types)!: redirect the node recursion point at AnyComponentSchema (#8344) - #8501
Conversation
…#8344) Every child slot is `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])`, and `SchemaNodeSchema`'s component arm was `BaseSchemaCore` — the ~21 base keys and nothing type-specific — so per-type enforcement was ROOT-ONLY at every depth, for every component type. That is objectui#7869, measured there: an off-spec `size` on a nested `icon` node was accepted while the same node alone was refused. The arm is now `AnyComponentSchema`. ⛔ Nothing here is `.strict()`; no declaration is repaired. Measured over the catalog + docs corpora on c90395b (554 node documents): 45 refused before, 54 after — the nine documents the card enumerates, each pre-existing debt this surfaces rather than creates. Two mechanical constraints, both measured rather than assumed: - `AnyComponentSchema` is built in `index.zod.ts` from all 13 category modules while 14 modules import `base.zod.ts`, so the arm cannot be an import: `z.lazy` defers evaluation, not the module graph, and the import deadlocks on `BaseSchema`'s TDZ when the graph is entered at `base.zod.js`. - It is a written `z.union` OPTION SLOT and not a `z.lazy` holder, because `z.lazy` memoises: a holder lets whichever module graph parses first decide the accept set for the whole process. `z.union` re-reads its options every parse, so the fill is live and no first parse can freeze the pre-fill answer in. `complex.zod.ts#DashboardWidgetSchema.component` names `BaseSchema` explicitly now. It is the one slot where the redirect would reverse a standing ruling: `metric-card` is objectui's CLOSED widget-slot component extension (objectstack#8593), admitted there and deliberately not an arm of `AnyComponentSchema`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
⛔ Blocked — two named conditions, both cleared by waiting. ⛔ Not enqueued, ⛔ nothing widened.
1.
|
…e-recursion-point-redirect
…carve-out creates The parity ledger is a TYPE MAP over the mirrors, so the `complex.zod.ts` change in ae99480 moved a drift row INSIDE zod-mirror-parity.test.ts without editing it: `DashboardWidgetSchema.component` names `BaseSchema` where TS declares `SchemaNode`, so the five primitive arms are newly narrower-than-declared. Re-derived from the tree with the compiler API, not copied: `DriftOf` for the pair resolves to 'component' | 'options'. The header figure moves with it, 64 -> 65 keys across an unchanged 42 entries, derived by an AST count of the interface. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE
Contract review (
|
❌ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. Which half objected:
📦 Bundle Size Report
Size Limits
|
⛔ Third blocker, and it is NOT this PR's to fix: the
|
| # | blocker | cleared by |
|---|---|---|
| 1 | #8498 — the RangeError cliff at depth 4 |
that card being fixed (seat ruling A, 5578545366) |
| 2 | the drift-ledger row | ✅ unblocked — #8485 merged and released the file; the dev has pushed a fix, Type Check still converging at this reading |
| 3 | the framework ceiling |
a maintainer decision on the re-baseline |
5578910339; ⛔ this seat does not produce domain:*.
Draft · needs:contract-review on both carriers · ⛔ not enqueued · no auto-merge · no self-approval. Kept watched until it is done.
Generated by Claude Code
…anch Maintenance round: bring the parked draft up to date with main. Merge only, no rebase and no force-push, so the pushed history stays valid. No textual conflicts: the twelve incoming commits and this branch touch disjoint files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE
…memoisation Three accept/reject facts and one caveat the changeset owed a reader: - `DashboardWidgetSchema.component` narrows (a primitive in that slot was accepted and is refused now); - `SchemaNodeSchema` moves TDZ_BOUND -> MEMOISED; - `ChatbotSchema.body` is a record and therefore WIDER than the base arm, so a nested chatbot node carrying one is refused before and accepted now. Measured with a corpus-valid seed at two child slots, against both the narrowing and a legal-node control; - the `sideEffects: false` bundle caveat: a bundler that drops the barrel body leaves the redirect inert, measured on this repo's own Vite/rollup lib build. The pin's header claimed `getter() === getter()` is FALSE for the exported wrapper. Measured on the built face, that holds on `main` and NOT at this head: the redirect made this const MEMOISED, and the `fill is LIVE` leg works because of it. The header now states both readings and why the wrapper is still the handle the pin uses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE
…ated union `main` landed the `AnyComponentSchema` discrimination (objectui#8498, PR #8544) on the same line this branch wraps, so the two changes conflicted textually. Resolved by COMPOSITION, not by choosing: the arm list is now `defineNodeComponentUnion(z.discriminatedUnion('type', [...]))`, keeping the discriminated fan-out AND the node option-slot fill. Both docblock halves kept. The mechanism is unaffected by construction and it was re-measured, not assumed: the option slot lives in `base.zod.ts`'s own plain `z.union(nodeUnionOptions)`, which `main` does not touch, and `defineNodeComponentUnion` writes whatever schema it is handed into slot 0. Build, all three type-check projects and the 148 `packages/types` test files (2805 tests, including the incoming `any-component-union-fanout` pins and this branch's `fill is LIVE` leg) are green on the composed tree. Also records, in `base.zod.ts`, the measured limit of its own claim: the "no published entry point can reach the pre-fill window" sentence is about module graphs, and a bundler honouring `"sideEffects": false` can drop the fill outright. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE
…tSchema retirement One conflict, in the `KnownDrift` header prose: objectui#8338 retired `feedback.zod.ts#ToastSchema` (entry and key both gone) while this branch adds `component` to an existing entry. Both narratives are kept, in the order the history happened, and the leading figures were RE-COUNTED on the merged tree by the AST walk rather than stepped from either side: KnownDrift: 41 entries / 64 keys Green on the merged tree: build, all three type-check projects (the `tsconfig.test.json` one is what judges this file), and 148 `packages/types` test files / 2817 tests. The drift row this branch owns is unchanged and re-derived with the compiler API: `component` and `options`, against the non-vacuity control that reads three keys and `never`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract review (
|
| # | first-review item | status @ 053914f05 |
evidence |
|---|---|---|---|
| 1 | Ruling authority | discharged — by the director, not the dev | #8344 comment 5579648940 (os-zhuang, Ruling recorded, batch #84, 05:15Z): the #7869-C condition is discharged on R3's count, the redirect is authorised, landing order #8498 first. |
| 2 | Drift ledger / Type Check |
discharged at the merge-base, re-opened by main |
Row 'complex.zod.ts#DashboardWidgetSchema': 'component' | 'options' (zod-mirror-parity.test.ts:1116), header 42 entries / 65 keys (:111) — correct against merge-base 0c4694437. ⛔ main 53231688 (#8540, 11:45Z) retired the ToastSchema entry → 41 / 63; the merged figure is 41 / 64 and the two headers conflict (F1). No CI Type Check exists for this head at all (F4) — the PR's exit 0 is a local reading. 6d0e8713c carries 41 entries / 64 keys and the same row; its Type Check is in progress at posting time. |
| 3 | Behind main / composition with #8544 |
composition confirmed; still open on mergeability | git merge-base --is-ancestor 67749c724 refs/review/8501 → yes. index.zod.ts:413: export const AnyComponentSchema = defineNodeComponentUnion(z.discriminatedUnion('type', [ — as claimed. base.zod.ts defineNodeComponentUnion writes nodeUnionOptions[0] and throws on installed !== union (by-reference assert kept). But the head is 5 commits behind main and mergeable_state: dirty (F1). |
| 4 | #8498 | discharged (dependency); residual open | #8498 closed completed by PR #8544 (merged 11:40:59Z), in ancestry. Residual: the handoff (5582303977) asked for depths 0–4 re-measured on the post-#8498 head; the PR reports only parseThrew 1 → 0 on examples/dashboard.ts, no depth table, and no pin exercises the redirected path at depth ≥ 4 (F5). |
| 5 | Changeset | discharged for what was owed; the new fact 3 is the problem | .changeset/8344-node-recursion-point-redirect.md: '@object-ui/types': minor; numbered facts 1 DashboardWidgetSchema.component narrows, 2 TDZ_BOUND → MEMOISED, 3 the chatbot widening, plus the tree-shake caveat. Fact 3 declares an accept-set WIDENING on a published validator — see F2. |
| 6 | framework ceiling |
superseded | #8550 (PR, os-sales) merged to main at 11:49:53Z (fa9e76cc): PER_CHUNK_GZIP_CEILINGS.framework 71,000 → 100,000. Not in 053914f05's ancestry; in 6d0e8713c's. This PR touches no ceiling (scripts/check-eager-closure-budget.mjs not in its diff) — correct. No Bundle Analysis ran on 053914f05 (F4); the −9 B reading (72,239 vs main 72,248) is the PR's, unverified here. |
| 7 | Upstream port parity | n/a, unchanged | No pinned port among the 10 changed files. |
| 8 | Tests | discharged (header); one residual sentence | node-recursion-point-8344.test.ts:38-45 now says the getter/unwrap identity is FALSE on main and TRUE on this head — the contradiction is fixed. Residual: the comment inside the first identity leg (:125-127, "this holds while .unwrap() and the z.lazy getter each return a FRESH object per call") still describes main. No .skip/.only/.todo added (grep of the test diff). phase2-schemas.test.ts:637 data: [] fixture repair kept. |
| 9 | File surface vs claim | still open (record) | Dev report 5585143344 lists files_changed (10 files) but carries no breach line naming complex.zod.ts, phase2-schemas.test.ts, zod-lazy-getter-identity-7918.test.ts as edits outside the claim's declared surface (5577799408). Mechanical. |
| 10 | Governed / CI / mergeability | governed ✓; CI none; mergeability ✗ | Governed paths untouched. No workflow run exists for 053914f05 — the 60 most recent runs on the branch are on 6d0e8713c (12:33Z), 050d4d4c (03:53Z), ae994801 (03:07Z). mergeable_state: dirty confirmed by git merge-tree --write-tree origin/main refs/review/8501 → CONFLICT (content): packages/types/src/__tests__/zod-mirror-parity.test.ts. |
New findings
F1 — blocking (dev; already answered at 6d0e8713c, pending CI). The head conflicts with origin/main in exactly one file, packages/types/src/__tests__/zod-mirror-parity.test.ts:111-123: main (#8540) reads **41 entries** … **63 keys** — 42 / 64 until objectui#8338 RETIRED …, the branch reads **42 entries** … **65 keys** — 42 / 64 until objectui#8344 added component …. The merged truth is 41 / 64 (42−1 entries, 65−1 keys). 6d0e8713c resolves it that way (:111 reads 41 entries / 64 keys, row unchanged at :1129), git merge-tree against main is clean, and main's tip is in its ancestry. That head needs its own tier re-review once CI concludes; this comment does not clear it.
F2 — blocking; the decision is the director's, the change (if ordered) is the dev's. The chatbot widening is real and is not covered by ruling 5579648940, which authorised the redirect "on that count" — a count with 0 newly-accepted documents. Source: complex.zod.ts:582 body: z.record(z.string(), z.unknown()).optional() vs base.zod.ts:288 body: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional(); ChatbotSchema is an arm of ComplexSchema (complex.zod.ts:1078-1083) and therefore of slot 0. At every child slot a chatbot node with a record body was refused on main and is accepted here. The card's appetite is explicit — "⛔ do not widen it in flight", a widening "splits into its own card" — and 「短期不考虑渐进」 was applied in the ruling; "declared, not eliminated" is therefore not admissible without a new Ruling recorded. The dev's reason for not eliminating it ("would narrow the published ChatbotSchema.body mirror") is false: the root ChatbotSchema need not change; only what the recursion slot admits for that one arm must stay at main's answer. Smallest narrowing-only change: in defineNodeComponentUnion, install into nodeUnionOptions[0] not union but union.superRefine((v, ctx) => { if (v?.type === 'chatbot' && v.body !== undefined) { const r = BaseSchemaCore.shape.body.safeParse(v.body); if (!r.success) for (const i of r.error.issues) ctx.addIssue({ ...i, path: ['body', ...i.path] }); } }), assert identity against the installed wrapper, and still return union (the export and the root accept set are untouched). Cost: ~15 lines in base.zod.ts, one pin leg (nested chatbot + record body refused; nested chatbot without body accepted), the fill is LIVE pin's options[0] toBe AnyComponentSchema re-targeted at the wrapper, tens of bytes, a type comparison per nested parse; no published schema changes shape and no ruling is needed. (Intersection with BaseSchemaCore was considered and rejected: it doubles base-key parsing and rewrites every nested error shape.) Two exits, either closes this item: the director records acceptance of the widening, or the dev removes it.
F3 — blocking (director/maintainer choose the route; dev implements). Shipping the redirect with a known silent-inert path under tree-shaking is a contract defect: the changeset promises "a nested node is now judged by its OWN component schema", and for any consumer that bundles @object-ui/types/zod importing named schemas without AnyComponentSchema, that promise is false with no diagnostic. From source: package.json:6 "sideEffects": false; the only fill is the initializer of export const AnyComponentSchema in index.zod.ts:413; a CardSchema-only importer retains the re-export chain (complex.zod.js → base.zod.js) and never the barrel's own body, so slot 0 stays BaseSchemaCore and the fill-time assert cannot run. The pre-fill "answers exactly as main" design is what makes the failure silent. On the five candidates:
- Candidate 2 (narrow
sideEffectsto["./dist/zod/index.zod.js"]) is not implementable as spelled. The repo's own gate contradicts it for a multi-entry package:scripts/__tests__/side-effects-declaration-consistency.test.ts:1274requires an array to name every resolvable entry form (main/module/allexportstargets —@object-ui/typeshas ~50 subpaths — and the workspace-aliassrc/**forms,:547-578), while:1329requires every source entry named to have a statically detected load-time effect, and the detector (impureNode,:709-723) walks onlyExpressionStatements and executed statements — a call inside aconstinitializer is invisible to it. Sosrc/zod/index.zod.ts(andsrc/index.ts, …) would be reported as phantom claims. Its "0 console bytes" claim is plausible on its own terms —@object-ui/types/zodis imported only bypackages/plugin-map/src/ObjectMap.tsx:25(lazy-registered,apps/console/src/register-plugins.ts:43) and the CLI, and the "dropsideEffectsentirely" probe adding only +16,078 B (far below the union's +229,208 B) is consistent with the barrel being unreachable from the console graph — but the true cost is a gate redesign under the 2026-08-29sideEffectsruling, which the table does not state. - Candidate 1 (drop the flag) now fits under the raised ceiling (72,239 + 16,078 ≈ 88.3 KB < 100 KB, headroom ≈ 0.13× the regression) but makes every module of the package unshakeable for every consumer — the "omit the field" wrongness the gate's own doc measured. Not recommended.
- Candidates 4/5 do not close the leak (the PR's own table agrees).
- Recommended: candidate 3, realised as an import binding, not a fill.
base.zod.tsimportsAnyComponentSchemafrom./index.zod.jsand references it inside thez.lazygetter only.sideEffects: falsestays true (no load-time write anywhere), a bundler retains the union because the binding is used, the identity assert andnodeUnionOptionsmachinery go away, and the memoisation hazard the PR measured disappears with it: an import binding is either initialised at first parse (module evaluation completes before any parse) or throwsReferenceErrorat load — never a wrong answer. Entering at the./zodbarrel is TDZ-safe (basecompletes before any category body runs); entering at a category module is not, which is a test-only cost the PR already measured ("turns that same import order into dozens of red suites") and must now be priced: each barrel-free test graph (e.g.handler-keys-string-any-mirrors-7344.test.ts) imports the barrel first. Retention cost for zod-face consumers is the same +229 KB gzip as every leak-closing candidate — it is the price of correctness, not of the spelling; console 0 by the reasoning above. The7918row then returns toTDZ_BOUND(one fewer byproduct).
F4 — record. No CI ever ran on 053914f05 (nor on edc50edf3): the branch's 66 workflow runs split 23 / 22 / 21 across 6d0e8713c, 050d4d4c, ae994801 — every "exit 0" quoted for this head is the dev's local reading. On 6d0e8713c at posting time: Bundle Analysis ✅ (against the raised ceiling), Lint ✅, Governed Surface Guard ✅, Live E2E ✅, Build & E2E ✅, Build Docs ✅, Changeset Fixed Group Check ✅, 15 lightweight workflows ✅; in run 34226745986 the Type Check job (Run type-check step) and Test (shard 1-4/4) are still in progress. Nothing red at this reading; the two checks that judge this PR's own ledger row and pins have not concluded.
F5 — record (dev). #8544's depth pin is built on MenuItemSchema precisely because, on that tree, "AnyComponentSchema does not yet recurse into child slots (objectui#7869 / objectui#8344), so a nested document is simply ACCEPTED" (any-component-union-fanout.test.ts:31-35). On this head that sentence is false and the path this PR opens has no depth-≥4 pin. Reasoned, not measured: per level the refused subtree is now embedded once (2-arm slot union → 6-arm node union → one discriminated arm), so the message grows linearly, not ×25 — the dev's parseThrew 1 → 0 agrees. Owed: a depth-4 card.body pin through safeValidateSchema plus the stale rationale corrected in the same stroke.
F6 — observation. node-recursion-point-8344.test.ts:125-127 residual sentence (item 8); no breach line in the report (item 9); .changeset fact 1 still says "One measured delta and only one" — correct as scoped to that slot, but the same phrase was the one the first review caught on the PR as a whole, so a reader may misread it.
Who discharges what
- Director (rulings): F2 (accept the widening or order its removal); F3 route (the
sideEffectsquestion sits under the 2026-08-29 maintainer ruling); dispatch of the tier re-review at6d0e8713c. - Dev: F1 (done at
6d0e8713c, CI pending), F2 implementation if ordered, F3 implementation with the barrel-first test hygiene priced, F5 pin + stale comment, items 8/9 residuals.
Maintainer-only merge: yes. Clause-② yes on a published packages/types face; the needs:contract-review carrier is hung on both card and PR; the leak fix touches a published sideEffects contract governed by a maintainer ruling. Draft, not enqueued, no auto-merge — keep it so until F1–F3 are discharged and the moved head is re-reviewed at tier.
Generated by Claude Code
Contract review (
|
| item | ruled | head 84111c960 |
status |
|---|---|---|---|
Q1 nested-chatbot widening |
eliminate here: superRefine on the slot-0 wrapper checking chatbot.body against BaseSchemaCore.shape.body; pin both directions; A (declare) and C (own card) refused by name |
base.zod.ts:101-114 installs the union unwrapped, no superRefine; grep -n superRefine over src/zod/base.zod.ts and index.zod.ts at the ref: 0 hits; card #8572 filed instead; changeset :64 says "Declared here rather than eliminated, by ruling" |
not implemented (option C, the refused one) |
| Q2 tree-shake leak | close here: bind AnyComponentSchema as an import inside SchemaNodeSchema's z.lazy getter; sideEffects: false untouched; D, A, B refused by name |
base.zod.ts:202-209 getter returns the pre-built nodeUnion, no import of ./index.zod.js in base.zod.ts; package.json:6 "sideEffects": false unchanged (correct); cards #8577/#8578 filed; changeset :85-90 "maintainer-floor authorisation, deliberately not taken here … Until objectui#8577 is ruled" |
not implemented (option D, the refused one); the ruled route was never measured by the seat — see finding 2 |
| F1 ledger conflict | resolved at 6d0e8713c, CI Type Check green |
header 41 entries / 64 keys (zod-mirror-parity.test.ts:111-115), row :1129 'component' | 'options'; interface diff vs main is exactly that one key; git merge-tree clean; Type Check success on this head |
implemented |
| F4/F5 depth 0–4 table + depth-≥4 pin on the redirected path | owed | no depth pin in the diff (node-recursion-point-8344.test.ts has none; any-component-union-fanout.test.ts is byte-identical to main, still pinned on MenuItemSchema); no table in PR body or reports |
not implemented — measured here instead, finding 4 |
F5 stale fanout rationale :31-35 |
owed | any-component-union-fanout.test.ts:31-35 still says "AnyComponentSchema does not yet recurse into child slots … a nested document is simply ACCEPTED" |
not implemented |
item 8 stale sentence at the pin :125-127 |
owed | now node-recursion-point-8344.test.ts:124-126: "this holds while .unwrap() and the z.lazy getter each return a FRESH object per call" — for a const this head memoised (:38-45 of the same file says so) |
not implemented |
| item 9 file-surface breach line in the report | owed | reports 5585679509 and 5585763505: no line names complex.zod.ts, phase2-schemas.test.ts, zod-lazy-getter-identity-7918.test.ts as edits outside the claim's declared surface (5577799408) |
not implemented |
New findings
1 — blocking (dev). Q1 not implemented, and the reason given is falsified by measurement. The seat's reason (PR body, #8572, changeset :64): eliminating the widening "means narrowing a published chatbot mirror". The ruled fix does not touch ChatbotSchema (complex.zod.ts:523, body at :582); it refuses a record-shaped body only for a chatbot node sitting in a child slot. Measured, three built faces, corpus seed basic-chatbot.json + body: { model: 'gpt-4', temperature: 0.2 }:
| document | main |
head 84111c960 |
head + ruled superRefine |
|---|---|---|---|
ROOT chatbot + record body (AnyComponentSchema) |
ACCEPTED | ACCEPTED | ACCEPTED (root accept set untouched) |
same node in card.body[] |
REFUSED (path=body) |
ACCEPTED | REFUSED (path=body.0.body) |
nested chatbot, no body / nested chatbot with a text node body |
ACCEPTED / ACCEPTED | ACCEPTED / ACCEPTED | ACCEPTED / ACCEPTED (non-vacuity) |
nested off-spec icon / nested h1 (controls) |
ACCEPTED / ACCEPTED | REFUSED / REFUSED | REFUSED / REFUSED |
The nested record-body case is refused on main today, so the ruled fix is not a narrowing against main at all: it restores main's answer at every child slot while the redirect narrows the other 108 base-key redeclarations — exactly the ruling's stated result. The fix used here is a superRefine on the component arm inside the getter (9 lines); the reviewer's slot-0 wrapper spelling is equivalent. #8572's "why this is a card and not a line in that PR" section rests on the falsified premise.
2 — blocking (dev). Q2 not implemented; the ruled route was never tried, and it closes the leak with sideEffects: false untouched. The seat measured routes A (array sideEffects) and C (a bare top-level defineNodeComponentUnion(AnyComponentSchema); statement) and reported both dead — correct, and irrelevant: the ruling refused A and never proposed C. The ruled route is an import binding read inside the getter, retained by reference, which no sideEffects flag licenses a bundler to drop. Measured with the seat's own setup semantics (@object-ui/types resolved from node_modules, "sideEffects": false read by @rollup/plugin-node-resolve, consumer entry export { CardSchema } from '@object-ui/types/zod'):
| face | CardSchema-only bundle |
AnyComponentSchema binding in output |
nested off-spec icon |
nested chatbot + record body |
|---|---|---|---|---|
main |
29,865 B | no | ACCEPTED | REFUSED |
head 84111c960 |
29,933 B — fill dropped, assert dropped | no | ACCEPTED — the leak, reproduced | ACCEPTED |
| head + ruled binding (+ finding 1) | 226,476 B | yes (const AnyComponentSchema = … retained) |
REFUSED | REFUSED |
Both-import control on the same head: 226,658 B, REFUSED — the ruled card-only bundle is within 182 B of it, i.e. the union is retained by reference exactly as ruled, with package.json:6 unchanged. On the module-cycle objection (base.zod.ts:139-146, changeset "the arm cannot be an import"): index.zod.ts' first import is ./app.zod.js (:47), app.zod.js imports base.zod.js, whose body never reads AnyComponentSchema at evaluation time (only the getter does), so entering at the barrel is TDZ-safe — measured: barrel entry on the ruled face parses green, options[0] is the union. Entering barrel-free (layout.zod.js first, barrel never loaded) throws ReferenceError: Cannot access 'BaseSchema' before initialization at import — loud — where the head's barrel-free graph silently answers as main (nested off-spec icon ACCEPTED, no diagnostic). That is the ruling's "pre-fill window becomes loud" and the barrel-first test hygiene it priced. The memoisation objection (base.zod.ts:57-73) applied to a let holder with a silent BaseSchemaCore fallback that a barrel-free graph could freeze in; an import binding has no fallback value — the getter either reads the initialised union or throws — so the objection does not transfer. Consequences: #8577 is filed on a false premise (no manifest change, no falsePackages >= 5 edit is needed; the census floor is untouched because sideEffects: false stays); #8578 (classifier cannot see a const-initializer call) remains a valid independent finding and does not bear on Q2 — under the ruled route the barrel has no load-time effect at all, so the classifier's zero becomes true.
3 — blocking (dev). The changeset and the dev reports attribute the refused dispositions to the ruling. .changeset/8344-node-recursion-point-redirect.md:64 "Declared here rather than eliminated, by ruling", :85 "a maintainer-floor authorisation, deliberately not taken here", :87-90 "Until objectui#8577 is ruled"; dev report 5585679509 ruling_Q1_chatbot_widening.verdict: "implemented as ruled (option C)" and ruling_Q2_sideEffects executing A then C; 5585763505 "Ruling C executed" with no ruling on the card after 12:45Z authorising C. Batch #93 refused A, C and D by name. A published-face changeset that cites a ruling for the opposite of what it ruled must be rewritten to what actually ships once findings 1–2 land (facts 3 and 4 become "eliminated"/"closed", or are deleted).
4 — record (dev). F4/F5: depth 0–5 measured on the head's own face, for the pin the PR still owes. Refused off-spec icon nested in card.body d levels deep, AnyComponentSchema.safeParse, zod 4.4.3: d=0 REFUSED 164 chars; d=1 3,454; d=2 8,172; d=3 14,318; d=4 21,892; d=5 30,894 — all REFUSED, all under 1.2 ms, growth linear (~+6–9k chars/level), no RangeError (identical figures on the ruled face). Control on main: d=0 REFUSED 164, d≥1 ACCEPTED (objectui#7869). A depth-4 pin through safeValidateSchema on the redirected path plus the rewritten any-component-union-fanout.test.ts:31-35 rationale are still owed.
5 — record (dev). Items 8 and 9 unchanged — node-recursion-point-8344.test.ts:124-126 residual sentence; no breach line in 5585679509 / 5585763505 naming the three files outside the claim's surface.
6 — observation. base.zod.ts docblock will need rewriting with finding 2 and carries stale figures now: :132-133 "54 / 553 … 45 / 553" vs the changeset's 554; :163-164 "369,733 … 1,144,999 bytes" vs the changeset's 370,652 / 1,149,749; :153-155 "No published entry point can reach that window" and :168 "do not close it by editing this comment" become moot under the import binding, as does the whole defineNodeComponentUnion / nodeUnionOptions / nodeUnion machinery (:101-114, :419-428) and its fill is LIVE pin, and the 7918 ledger row returns to TDZ_BOUND (the getter builds the union once; memoisation of a stable binding is harmless).
Maintainer-only merge: yes — Clause-② published packages/types face, feat!, batch #93 says "maintainer-merged when green"; and not mergeable now: the head carries neither ruled change, the changeset misattributes the omissions to the ruling, and needs:contract-review must stay hung on both carriers until findings 1–3 are discharged and the patched head is re-reviewed at tier. Draft, not enqueued, no auto-merge — keep it so.
Generated by Claude Code
… consumer ⛔ The `Build Docs` failure on 2ac5381 is THIS BRANCH'S CODE, not the changeset text: `ReferenceError: Cannot access 'BaseSchema' before initialization` out of `packages/types/dist/zod/app.zod.js`, prerendering `/docs/guide/schema-catalog` through `plugin-view`. Reproduced locally in one line — with the binding in place, importing `dist/zod/app.zod.js` throws; without it, it loads clean. Mechanism, measured rather than reasoned: making `base.zod.ts` import the barrel gives the cycle an edge that is only safe when the barrel is evaluated FIRST. Node honours that if the entry is `./zod`; a bundler is free to order the cycle category-module-first, and the site's build does. ⇒ the ruled F3 spelling is sound for a module graph and unsound for a bundle, which is the opposite of the property it was chosen for. My own docblock claim that "consumers cannot hit that" was falsified by CI, and the 102-file barrel-first hygiene could not have helped: the failing consumer already imports the barrel. This commit therefore restores the wiring CI was last green on (`d375037a8`) and KEEPS batch #93's F2 in the spelling the reviewer originally wrote it in: the slot-0 write installs `union.superRefine(...)`, so a nested `chatbot` node with a record `body` is refused while the published root mirror is untouched. The identity assertion now checks the WRAPPER it installed, and the pin says why a `toBe(AnyComponentSchema)` assertion would go green if the narrowing were dropped. The 102 barrel-first imports are reverted with the binding that needed them; the objectui#7918 row is `MEMOISED` again, matching this wiring. ⇒ F2 and F5 ship; F3 does NOT, and the changeset now states the gap, the CI evidence and all four measured candidates instead of claiming a fix. Green on this tree: build 0, type-check 0 (all three projects), packages/types 148 files / 2,824 tests / 0 failed, the four changeset gates 0, and the category-first import that CI choked on now loads clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE
Contract review (
|
| item | ruled | head 2ac53818e |
status |
|---|---|---|---|
Q1 nested-chatbot widening |
superRefine on the installed arm checking body against BaseSchemaCore.shape.body; root untouched; pin both directions |
base.zod.ts:363-372: nodeComponentArm = () => AnyComponentSchema.superRefine(...), keyed on node.type !== 'chatbot' || node.body === undefined → return, checks BaseSchemaCore.shape.body.safeParse(node.body) (the real shape, not a copy), re-adds issues under ['body', ...path]. complex.zod.ts diff vs main is the DashboardWidgetSchema.component carve-out only — ChatbotSchema untouched. Four legs node-recursion-point-8344.test.ts:221-240 (nested in card.body[] and div.children[] refused, root accepted, nested-without-body accepted at both depths, refusal names "body"); removing the refine turns :222-223 red. Measured on the head face with the corpus seed: root ACCEPTED, card.body[] REFUSED path=body.0.body, no-body ACCEPTED, text-node body ACCEPTED. #8544's discrimination survives the wrapper: arm._zod.propValues has type, discriminator: 'type', checks: 1 (:160-162 pins it); a nested refusal is 1 issue, 3,454 chars, not 106 arms |
implemented |
| Q2 tree-shake leak | bind AnyComponentSchema as an import read inside the z.lazy getter; sideEffects: false untouched |
base.zod.ts:27 import { AnyComponentSchema } from './index.zod.js'; read only inside nodeComponentArm() (:363-364), called from the getter (:135-146); no top-level read; git grep defineNodeComponentUnion|nodeUnionOptions refs/review/8501 -- packages → 0 hits in source (one stale mention in the changeset, finding 2). Retention reproduced: CardSchema-only rollup bundle 226,482 B (both-imports 226,502 B, 20 B apart), union + superRefine present, nested off-spec icon REFUSED, nested h1 REFUSED |
implemented as spelled — ⛔ not consumer-safe, finding 1 |
| F1 ledger conflict | resolved, Type Check green |
Type Check success on this head; KnownDrift AST count 41 entries / 64 keys (main: 41 / 63); row zod-mirror-parity.test.ts:1133 'component' | 'options'; F2/F3 do not touch that slot (DashboardWidgetSchema.component is BaseSchema, not the recursion point), so the row is unaffected |
implemented |
| F4/F5 depth table + depth-≥4 pin | owed | node-recursion-point-8344.test.ts:257-285: deep(n) = bad badge leaf under n card.body levels, all depths 0–4 refused via safeValidateSchema (:263-268), message.length < 200_000 at depth 4 (:276), legal-leaf control accepted at depth 4 (:279-284). Measured on the head face: 276 / 3,626 / 8,404 / 14,610 / 22,244 chars (d=5: 31,306), all ≤1.3 ms, 1 issue each — identical to the PR's table. The ceiling is meaningful: pre-#8498 depth 3 read 428,269,086 chars and depth 4 threw RangeError, both fail it; the head sits 9× under it |
implemented |
| items 8/9 | stale fanout rationale; pin :125-127; breach line |
any-component-union-fanout.test.ts:30-42 corrected in place, MenuItemSchema case kept as the measurement; node-recursion-point-8344.test.ts:127-131 "FRESH object per call … because the getter builds the node union … every time" — now true (measured getter() === getter() false); breach list in report 5586356577 files_outside_the_declared_claim_surface names complex.zod.ts, zod-mirror-parity, 7918, phase2-schemas, fanout, the 102 |
implemented |
| changeset attributions | three false "by ruling" lines gone; state what ships | "Declared here … by ruling", "maintainer-floor", "Until objectui#8577" — all gone; facts 2 and 3 (:52-75) state the nested narrowing, the untouched root, the binding, both byte costs; minor bump is correct per check-changeset-no-major.mjs / AGENTS.md §版本号策略 (breaks ship as minor with the break in the body — :8 does) |
partially — :31-39 still describes the retired mechanism, finding 2 |
| #7918 row | back to TDZ_BOUND |
zod-lazy-getter-identity-7918.test.ts:141 in TDZ_BOUND, :32-40 records the double move; measured getter() === getter() false on the head face, so unstableLazyExports reading 8 is consistent ( |
implemented |
| cards | #8577 superseded, #8578 open, #8572 sentence only | #8577 closed not_planned 13:58:25Z with 5586311956 citing the ruling; #8578 open with 5586312517; #8572 open with 5586312717 quoting the Q1 sentence verbatim and "decides nothing here" |
implemented |
New findings
1 — blocking (dev + director). The import-binding route, as spelled, throws in any consumer whose bundler links past the barrel — and this repo's own docs site is one. CI Build Docs (job 102092886796) fails prerendering /docs/guide/schema-catalog:
ReferenceError: Cannot access 'cI' before initialization
at module evaluation (../../packages/types/dist/zod/app.zod.js:296:35)
at <unknown> (app/components/registerCatalogBlocks.ts:184:1)
at module evaluation (../../packages/plugin-view/dist/index.js:2781:1)
> 296 | export const AppComponentSchema = BaseSchema.extend(SpecAppFields.shape).extend({
Mechanism, from the ESM spec rather than from the bundler: the cycle base.zod.js → index.zod.js → {app,layout,…}.zod.js → base.zod.js evaluates correctly only when index.zod.js is the first module of the cycle to start — then app.zod.js pulls base.zod.js to completion before its own body runs. Entering at any other module puts BaseSchema in its TDZ when the first category body runs. Reproduced on the head's own transpiled face in Node: entering at base, complex, layout, app or objectql → ReferenceError: Cannot access 'BaseSchema' before initialization, every time; entering at index.zod.js → 206 exports, all pins green. The docs site is Next.js 16 / Turbopack with @object-ui/types in transpilePackages (apps/site/next.config.mjs:30-36); plugin-view/src imports ObjectMapConfigSchema from @object-ui/types/zod — the documented subpath — and a bundler honouring "sideEffects": false is entitled to link that import straight to objectql.zod.js, skipping the side-effect-free barrel, which makes the consumer barrel-free without writing a single deep import. Rollup (the console's Vite build, and the probes on both sides of this PR) does not skip barrels, which is why Build & E2E is green, why the PR's retention table is right, and why the claim "Consumers cannot reach that: ./zod is the package's only subpath" (PR body; base.zod.ts:96-99, :335-341; index.zod.ts docblock; pin :165-173 "paid in test files, never by a consumer of ./zod"; changeset :99-101) was measured true on the one bundler family that cannot falsify it. Build Docs was green on 84111c960 (30 success / 0 failed, 5586414639); the only packages/types/src change since is F2/F3. ⇒ the ruled Q2 route removes the silent-leak failure mode and replaces it with a load-time crash for webpack/Turbopack/Next.js consumers of a published face. That is worse than the leak for those consumers and is not a merge-when-green state. The 102 barrel-first test imports are the same fragility, paid where it can be paid. ⛔ No spelling is proposed here — it is a director question, since it moves the ruled route: the structural fact is that any recursion point that statically imports the union (through the barrel or any other module) is entry-order-fragile, and the two escapes are (a) a build-time single-file ./zod face so the cycle's order is fixed by the package's own bundler rather than the consumer's, or (b) a route that does not close the cycle at module scope — both outside what #93 ruled.
2 — blocking (dev). The changeset contradicts itself on the mechanism it ships. .changeset/8344-node-recursion-point-redirect.md:31-39 ("Two mechanical notes"): "the arm cannot be an import — z.lazy defers evaluation, not the module graph. It is a written option slot that index.zod.ts fills inside AnyComponentSchema's own initializer … the reasoning lives on defineNodeComponentUnion in zod/base.zod.ts." Fact 3 at :63-75 says the opposite ("an import binding read inside SchemaNodeSchema's z.lazy getter, ⛔ not a write into a live option array"), and defineNodeComponentUnion exists nowhere at the ref except that line. A release-notes input (CLAUDE.md, content/docs/releases/ rule) naming a symbol that does not ship is a factual error; :99-101 ("Consumers cannot hit that") is falsified by finding 1. Delete :31-39 or rewrite it to the binding; rewrite :99-101 to whatever finding 1 resolves to.
3 — record (dev). The 102-file hygiene is one import each, nothing weakened. git diff --numstat 84111c960 refs/review/8501: 99 packages/types test files at exactly +4 / −0 (three comment lines + import '../zod/index.zod.js';), 102 such import lines in total (the other three are inside fanout, node-recursion-point, 7918, which carry real edits); scripted check: in all 102 the added import precedes the file's first ../zod/* or ../*.js import (0 mis-ordered); no expect line touched. The two large test additions (emptyCollection-8526.test.tsx +507, empty-description-props-…-8571.test.tsx +313) are main's #8526/#8571, merged in 05efe7d2. Spot-checked five (accordion-item-authorable-keys, chatbot-authoring-face-keys, dashboard-widget-strict-6002, menu-item-union, phase2-schemas): each entered at a category module (../zod/disclosure.zod, ../zod/complex.zod, ../zod/base.zod.js, ../zod/overlay.zod), i.e. each would throw at import without the line — the same crash as finding 1, which is the point.
4 — record. Console framework and the sensitivity gate. Bundle Analysis log on this head: framework 88.8 KB / 97.7 KB ceiling (headroom 8.8 KB = 0.10x the 89.0 KB regression) — 88.8 KiB = 90,969 B, 97.7 KiB = PER_CHUNK_GZIP_CEILINGS.framework = 100_000; the changeset figure matches. evaluateHeadroomSensitivity (check-eager-closure-budget.mjs:1156-1290) errors only when headroomBytes >= REGRESSION_THIS_GATE_MUST_CATCH_BYTES (:390, 89 KiB — a blind gauge), so 8.8 KB does not flag; freshness pass; BUDGET_CLOSURE_HEADROOM_STATUS: pass. Aggregate closure 3493.9 / 3512.7 KB (headroom 18.8 KB). ⛔ Neither ceiling touched by this PR.
5 — observation. Figures re-derived and matching. Depth 0–4 exactly as claimed; KnownDrift 41/64; git merge-tree clean; rollup card-only vs both-imports 20 B apart (the PR's Vite lib figures differ by 39 B — same conclusion, different build); the main-vs-head comparison in 5586414639 finding 2 (29,933 B fill-dropped, ACCEPTED) is the state this head replaces. The zod-mirror-parity.test.ts:111-118 header narrative reads the two moves out of chronological order (41/63 → #8344 → 41/64, then "42/64 until #8338") but the figures are consistent with both main and the ref.
6 — observation. CI incomplete when this was written. Shards 1, 2 and 4 in progress; the verdict does not depend on them (finding 1 is a completed failure), but the merge-when-green condition cannot be evaluated until they finish. Test shard 3 green.
Maintainer-only merge: yes — Clause-② published packages/types face, feat!, batch #93 says "maintainer-merged when green"; and not mergeable now: Build Docs is red on this head with a consumer-facing load-time crash that the ruled route introduces (finding 1), so the route itself needs a director decision before a patch, and the changeset (finding 2) must follow that decision. Draft, not enqueued, no auto-merge — keep it so; needs:contract-review stays down on both carriers until the re-spelled head is re-reviewed at tier.
Generated by Claude Code
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract review (
|
| item | ruled | head ca2037680 |
status |
|---|---|---|---|
Q1 nested-chatbot widening |
superRefine on the installed arm, root untouched, both directions pinned (#93) |
base.zod.ts:113-120 — the refine now lives inside defineNodeComponentUnion, applied to the discriminated union before it is written into nodeUnionOptions[0] (:122); keyed node.type !== 'chatbot' || node.body === undefined → return, checks BaseSchemaCore.shape.body, re-paths under ['body', …]. complex.zod.ts diff is the DashboardWidgetSchema.component carve-out only; ChatbotSchema untouched. Four legs node-recursion-point-8344.test.ts:203-230. Measured on the head face with the corpus seed: root ACCEPTED, card.body[] REFUSED path=body.0.body, div.children[] REFUSED, no-body ACCEPTED, text-node body ACCEPTED. propValues intact (arm._zod.propValues has type, discriminator: 'type', checks: 1) |
implemented |
| Q2 / F3 tree-shake leak | #93: getter import binding → amended by #96: single-module ./zod, binding kept, 102 imports kept |
base.zod.ts has no import … './index.zod.js' (0 hits in source or emitted JS); recursion point is nodeUnion = z.union(nodeUnionOptions) (:439-448) with slot 0 written by defineNodeComponentUnion from index.zod.ts:413; getter returns the one union (:222-228). No single-module build (build = tsc && node ../../scripts/check-dist-completeness.mjs, unchanged). Leak measured open — table below |
⛔ not implemented (reverted; declared in changeset :65-87) |
| F1 ledger | resolved, Type Check green |
KnownDrift re-counted on the ref: 41 entries / 64 keys, header zod-mirror-parity.test.ts:111 says 41/64; row :1129 'component' | 'options'; Type Check success |
implemented |
| F4/F5 depth pins | 0–4 table + depth-≥4 pin through safeValidateSchema |
node-recursion-point-8344.test.ts:246-275; measured on the head face: 276 / 3,626 / 8,404 / 14,610 / 22,244 chars (d=5: 31,306), 1 issue each, ≤1 ms, legal leaf at d=4 ACCEPTED |
implemented |
| items 8/9 | fanout rationale; pin sentence; breach line | any-component-union-fanout.test.ts:30-42 corrected in place; pin :38-43 now says getter() === getter() is TRUE on this head — measured true (the revert re-memoised: getter returns the module-scope union); :123-128 "fresh object per call" is scoped to the seven TDZ_BOUND mirrors — correct; breach list in report 5586681596 |
implemented |
| changeset | one consistent mechanism, no "consumers cannot hit that", byte costs for what ships | :31-39 describes the option-slot write — matches what ships; fact 3 :65-87 states the leak, the CI ReferenceError, and the four measured candidates; "consumers cannot hit that" gone (0 hits); names #8572 only; minor per §版本号策略 with the break stated at :8 |
accurate for this head — finding 3 for the residue |
| #7918 row | matches the wiring | zod-lazy-getter-identity-7918.test.ts:129-134 SchemaNodeSchema in MEMOISED; measured getter() === getter() true on the head face; unstableLazyExports reading 7 is consistent |
implemented |
| cards | #8577 superseded, #8578 open, #8572 sentence only | #8577 closed not_planned; #8578 open; #8572 open |
as ruled |
| CI | green | run 34237190685 on ca2037680: 30 success / 0 failure / 3 skipped — Build Docs 102098045449 success, Test (shard 1/4)…(4/4) all success, Type Check, Build & E2E, Bundle Analysis, Lint, the four changeset gates, Governed Surface Queue Guard success; skipped = two coverage stubs + dependabot. mergeable_state: clean |
green |
The leak on this head, three bundler classes
Entry probes on the head's transpiled face; zod and @objectstack/spec external (so sizes are the package's own bytes — the changeset's 370,652 / 1,149,749 inline zod and are a different basis, same conclusion). "Nested off-spec icon" = { type: 'icon', icon: 'check', size: 'huge' } inside card.body[], parsed through CardSchema.
| class | entry | load result | nested off-spec icon |
nested h1 (unmirrored) |
bundle raw / gzip | fill in output |
|---|---|---|---|---|---|---|
(i) rollup/Vite, barrel with AnyComponentSchema in the import graph (the console-shaped path when the union is referenced; the CLI's path) |
export { CardSchema, AnyComponentSchema } from './zod/index.zod.js' |
loads | REFUSED | REFUSED | 227,056 / 55,769 | present (defineNodeComponentUnion ×3, superRefine ×9, chatbot check ×1) |
(ii) rollup/Vite, CardSchema-only from the barrel |
export { CardSchema } from './zod/index.zod.js' |
loads | ACCEPTED — redirect inert | ACCEPTED | 29,933 / 8,151 | absent |
(iii) sideEffects:false-honouring deep-linking bundler (Turbopack/webpack model: linked past the barrel to the category module) |
export { CardSchema } from './zod/layout.zod.js' |
loads, no throw | ACCEPTED — redirect inert | ACCEPTED | 29,933 / 8,157 | absent |
| Node direct entry (module graph, not a bundle) | import './zod/app.zod.js' (also layout, base, complex, objectql) |
evaluates, 7 / 24 / 12 / 29 / 23 exports, no ReferenceError |
ACCEPTED until the barrel is evaluated; the same CardSchema object flips to REFUSED the moment index.zod.js loads (the write is live) |
— | — | — |
Node entry at ./zod (the published subpath) |
import './zod/index.zod.js' |
206 exports | REFUSED (1 issue, 3,454 chars) | REFUSED | — | — |
The nested-chatbot-with-record-body row is REFUSED in all three bundles and on the face — in (ii)/(iii) by the pre-#8344 base arm, exactly as on main, so Q1 neither widens nor depends on the fill. Legal twins ACCEPTED everywhere (non-vacuity).
What merging this head as-is would ship
- The redirect is real in a module graph entered at
./zod(Node, vitest, the CLI'scheck/validate—packages/cli/src/commands/{check,validate}.tsimportsafeValidateSchema, which references the union, so any bundle of the CLI retains the fill) and in any bundle that referencesAnyComponentSchema. - The redirect is silently inert for a bundled consumer that imports individual schemas from
@object-ui/types/zodwithoutAnyComponentSchema(class ii), and for anysideEffects:false-honouring bundler that links past the barrel (class iii). No error, no warning, the pre-spec(types): redirect the node recursion point from BaseSchemaCore to AnyComponentSchema — measured at 9 newly-refused documents, and it drops 118 phantom strict refusals #8344 accept set. The changeset says so (:65-87) and tells such consumers to importAnyComponentSchema. - This repo's own console is a class-(ii) consumer: the only zod-face import in its eager closure is
plugin-map/src/ObjectMap.tsx:25(ObjectMapConfigSchema);Bundle Analysison this head readsframework70.6 KB / 97.7 KB ceiling (vs 88.8 KB with the binding in on2ac53818e), i.e. the fill is dropped there. No console code parses a node tree through the zod face (SchemaRendereruses@object-ui/core'svalidateSchema), so nothing observable changes in the console today. - The docs site (Next/Turbopack, class iii) builds green and loads the category modules directly; its bundle does not carry the fill.
- No published-face crash anywhere: every entry order evaluates.
- Q1, the
DashboardWidgetSchema.componentcarve-out, the three doc/fixture repairs, the 45→54 corpus narrowing, and the#7918MEMOISEDrow ship as described. - Nothing the batch Update documentation: consolidate repository URLs and fix broken links #96 route needs is on this head; landing this first means Update documentation: consolidate repository URLs and fix broken links #96 lands as a second
packages/typesPR that re-adds the binding, the 102 imports and the single-module build.
Findings
1 — blocking (director/maintainer). The head does not carry the standing Q2 ruling. base.zod.ts:439-448 (nodeUnionOptions, nodeUnion), index.zod.ts:413 (defineNodeComponentUnion(z.discriminatedUnion(…))), packages/types/package.json build unchanged — the option-slot write is what ships, the leak is open (classes ii and iii above), and the changeset :65-87 declares it. #93 refused D ("with the caveat declared"); #96 refused C and ruled the single-module face with the binding retained and the 102 imports kept; handoff 5586721630 item 3 says this landing state is not acceptable. The head is internally consistent and CI-green, but the verdict cannot be PASS against #96. Whether to merge it anyway is the maintainer's call (the rulings are reversible by the maintainer); the facts for that call are the section above.
2 — record. The revert is clean. git diff 2ac53818e refs/review/8501: 105 files, +240 / −566. Removed: base.zod.ts:24-27 import of ./index.zod.js, nodeComponentArm, the getter that built per call, the pin's "throws LOUDLY" leg, the TDZ_BOUND placement, and 102 × 4 lines of barrel-first hygiene (102 files on 2ac53818e, 0 on the head). Restored: defineNodeComponentUnion with the Q1 refine inside it, nodeUnionOptions/nodeUnion, the MEMOISED row, the "fill is LIVE" pin leg with expect(arm).not.toBe(AnyComponentSchema) (:146-162). No dead import, no comment in source claims the binding (0 hits for binding/nodeComponentArm in base.zod.ts outside unrelated strings; index.zod.ts:376-378 says "late-binding holder", which is the slot write). Only the changeset's fact 3 was rewritten rather than reverted, correctly.
3 — record (dev). Changeset residue, small. (a) The byte pair in base.zod.ts:183-185 (369,733 / 1,144,999) and the changeset :70-72 (370,652 / 1,149,749) are the same measurement on two heads; pick one. (b) base.zod.ts:187-188 "its disposition is a ruling in flight on objectui#8344 — do not close it by editing this comment" now has its ruling (#96); update when that lands. (c) The changeset names #8572 only; the leak paragraph points at no card. #8577 is closed, so the only open card near the leak is #8578 (the classifier half) — if the maintainer merges as-is, the leak needs a card and the changeset a pointer; if #96 lands first, nothing to do. (d) No literal "BREAKING" token — :8 "Behaviour change, deliberately, at every depth below the root" is the break, minor is correct per check-changeset-no-major.mjs; the title carries feat(types)!. Consistent with the repo's other break changesets, so not a defect.
4 — record (dev). The PR body contradicts the head in two places. "How F3 is wired, in one paragraph" and 维护者速读 ②/③ still describe the binding as shipped (framework 72,248 → 90,969, 102 files importing the barrel first, "consumers cannot reach that"), while the round-2 table says F3 was reverted. Not a release-notes input, but it is what the maintainer reads to decide.
5 — record. The 102 barrel-first imports are not needed on this head. With the slot write, a graph entered at a category module does not throw (Node entry at app/layout/base/complex/objectql all evaluate); it answers as main until any file in the worker evaluates the barrel, after which every parse sees the fill because z.union re-reads its array (measured: the same CardSchema object flips ACCEPTED → REFUSED when index.zod.js loads). Under isolate: false the barrel is evaluated by the first file that imports it, and the #8344 pin imports it itself. The four test shards are green with the imports removed. They become load-bearing again the moment the binding returns (#96 says keep them), so the #96 head pays the +408 lines again.
6 — observation. Batch #96 cost, estimated from the tree, not built. (i) Build today: tsc per-file (packages/types/package.json build: tsc && node ../../scripts/check-dist-completeness.mjs; tsconfig.json composite: true, rootDir: ./src, outDir: ./dist); no bundler config in the package; vite ^8.2.1 is a root devDependency and @object-ui/fields / @object-ui/console already build as tsc && vite build …, so a lib-mode vite.config.ts (entry src/zod/index.zod.ts, external zod and @objectstack/spec, output dist/zod/index.zod.js, format es) is the precedented shape. (ii) Gates: check:dist-completeness derives its expected set from tsconfig.json's fileNames via the TypeScript API and keys buildsWithTsc on the first && segment being tsc — if the build stays tsc && vite build && node …check-dist-completeness.mjs and the bundle overwrites dist/zod/index.zod.js in place, every tsc emit is still present and no gate literal moves; the per-file dist/zod/*.zod.js siblings remain in the tarball (dead once the barrel no longer imports them; unreachable through exports), and deleting them would red this gate. check:published-dist reads npm pack for tooling artifacts — a vite.config.ts outside files is not published — unaffected. check:readme-exports reads exports['.'].types — unaffected. check:node-esm-load imports exports['.'] only (esmEntryOf, check-node-esm-load.mjs:760-765) — unaffected; the single file loads under Node regardless (barrel entry measured clean). check:side-effects-array is scoped to packages declaring an ARRAY (readArrayPackages) — types declares false, out of scope; the #3943 consistency pin reads source, and the binding is a read, not an effect. (iii) Manifest: a vite devDependency in packages/types/package.json is likely needed for check:phantom-deps/config resolution — a manifest field, though not one of the eight publish-contract fields; #96's stop-and-report clause applies. (iv) What a dist bundle does NOT change: apps/console/vite.config.ts:464 and vitest.config.mts:501 alias @object-ui/types/zod to src/zod/index.zod.ts, so the console keeps paying the binding's framework cost (88.8 KB measured on 2ac53818e, headroom 8.8 KB) and vitest keeps needing the 102 imports; the docs site (dist, Turbopack) is the consumer the bundle fixes. Estimate: one config file, one script line, probably one devDependency; the tsc siblings question is the only place a gate could bite.
7 — observation. Figures re-derived and matching. Depth 0–4 exactly as claimed; KnownDrift 41/64; chatbot six-row table identical to the PR's; getter() === getter() true; framework 70.6 KB on this head vs the PR body's 72,248 (same within the log's 0.1 KB rounding); CI fully green.
Maintainer-only merge: yes — Clause-② published packages/types face, feat!, #93/#96 "maintainer-merged when green". The head is green and mergeable (clean), but it is not the head the standing ruling describes: merging it ships the redirect with the leak declared, which the director refused twice and which only the maintainer can overrule. Draft, not enqueued, no auto-merge — keep it so either way.
Generated by Claude Code
…ent, point at objectui#8598 Implements decision batch #98 on objectui#8344 (comment 5587037055), Finding 3 of the contract review of `ca2037680` (PR #8501 comment 5587000173). The byte pair `base.zod.ts` quoted (369,733 / 1,144,999) and the pair the changeset quoted (370,652 / 1,149,749) were the same measurement taken on two heads. Both texts now cite ONE measurement, taken on this head with a named instrument: a Vite 8.2.1 lib build of the published `dist/zod` face, `es`, esbuild-minified, `zod` 4.4.3 and `@objectstack/spec` external, each entry built alone and read in a fresh Node process — barrel, CardSchema + AnyComponentSchema 750,542 / 206,815 REFUSED fill present barrel, CardSchema only 212,567 / 61,025 ACCEPTED fill absent deep-link entry at layout.zod.js 212,563 / 61,030 ACCEPTED fill absent Minified so that editing this very docblock cannot move the figure it carries. The stale "its disposition is a ruling in flight" sentence is gone: the ruling is in. The gap ships DECLARED, the declaration names who is exposed (an external consumer whose bundler honours `sideEffects: false` and never reads `AnyComponentSchema` keeps the pre-redirect accept set for NESTED nodes; root-level enforcement and every union-reading graph get the new set), and the changeset's leak paragraph points at objectui#8598 — the `./zod` face built as one module — as the card that closes it. No source outside comments moved. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TezFG8ZMrNH6n5VTNpPpdH
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract review (
|
| # | finding | state | evidence on 027dcdf7 |
|---|---|---|---|
| 3 | byte-pair mismatch base.zod.ts vs changeset; stale "ruling in flight"; no leak-card pointer |
closed | base.zod.ts:186-188 cites 212,567 / 212,563 / 750,542; changeset :84-86 cites the same three raw figures plus gzip 206,815 / 61,025 / 61,030 — one measurement, both texts. Instrument named in both (Vite 8.2.1 lib build, es, esbuild-minified, zod 4.4.3 + @objectstack/spec external, dist/zod face; probe node { type: 'icon', icon: 'check', size: 'huge' } in card.body[] via CardSchema; entry spellings in the body's three-class table) — complete enough to reproduce. git grep "ruling in flight" over packages/types + .changeset: 0 hits. Changeset :101 names objectui#8598 as the closer; :66-75 states who is exposed (external sideEffects:false-honouring bundler that never reads AnyComponentSchema, NESTED nodes only; root, Node/vitest, CLI and union-reading bundles get the new set). Old pairs 369,733 / 1,144,999 and 370,652 / 1,149,749: gone from both. |
| 4 | body / 速读 described the getter binding as shipped | closed | Round-3 section "How the recursion point is wired on this head — the option-slot write" describes slot 0 of z.union(nodeUnionOptions) written from AnyComponentSchema's initializer, and says ⛔ nothing imports the union into base.zod.ts. Three-class table (i)/(ii)/(iii) present. Batch #98 cited by id 5587037055 (3 hits). consumers cannot reach that: 0 hits (the one "consumers cannot hit that" at body L334 is quoted as the claim CI falsified — correct). 速读 「你要做的」①: director 席 undrafts + squash auto-merge after tier re-review PASS and CI green incl. Build Docs, ⛔ not 由维护者合并. Part of #8344 L1 kept; the single Fixes token is the backticked word in "Part of, not Fixes" with no issue number — no closing keyword anywhere (`(closes |
| 5 | 102 barrel-first test imports to revert | closed — nothing to revert, as the report says | The exact comment line added at 98265165 (the \./zod` barrel must be the FIRST zod module…) and the bare import '../zod/index.zod.js';: **102 / 102 hits at 9826516, 0 / 0 on the head**; F3marker inpackages/types/src: 0. The revert landed at ca20376` — my round-2 finding was already stale when written. |
| 6 | console is a class-(ii) consumer | closed | Body L463: "This repo's console is a class-(ii) consumer: …plugin-map/src/ObjectMap.tsx (ObjectMapConfigSchema, one schema)…". |
Contract state (unchanged since ca2037680)
defineNodeComponentUnion (base.zod.ts:101) wraps the handed union in the Q1 superRefine — nested chatbot with body !== undefined re-parsed through BaseSchemaCore.shape.body (:113-116) — writes nodeUnionOptions[0] (:122) and asserts identity by reading _zod.def.options[0] back (:126-131); nodeUnion = z.union(nodeUnionOptions) (:457); the z.lazy getter returns nodeUnion (:231); index.zod.ts:413 AnyComponentSchema = defineNodeComponentUnion(z.discriminatedUnion('type', [...])); complex.zod.ts:838 DashboardWidgetSchema.component: BaseSchema.optional() carve-out. All as reviewed.
Changeset
'@object-ui/types': minor ✓. The break is declared in prose — "Behaviour change, deliberately, at every depth below the root." + "What an author sees … refused now, where it parsed green before" — which is what AGENTS.md:240 asks for (minor + breaking semantics written out; no literal banner required; check-changeset-no-major green). Leak declared with exposure named, objectui#8598 pointer present. ADR-0087: the two sibling changesets that cite it are property-retirement tombstones; not applicable to this change, so its absence is correct.
Governed paths touched: NO — full diff vs merge-base (11 files) hits none of AGENTS.md, CLAUDE.md, .claude/**, docs/adr/**, content/docs/releases/**, skills/**, .github/**, scripts/**. packages/types/package.json untouched; "sideEffects": false stays (line 6).
Clause-②: yes (feat!) — title and root commit ae994801 are feat(types)!:; needs:contract-review present on PR and card #8344. Commit messages: no Fixes / Refs / Part of / Closes trailers in any of the 15 commits (Part of lives in the body only).
CI on 027dcdf7 (read once, ~17:23Z, run 34256323390 et al., 33 check runs)
- in progress:
Build Docs,Type Check,Lint,Test (shard 1/4),Test (shard 2/4),Test (shard 3/4),Test (shard 4/4) - success: Build & E2E, Bundle Analysis, Changeset Declaration / Bump Policy / Overwrite Report / Fixed Group Check, Governed Surface Queue Guard, Control Byte Scan, Doc Snippet Type Check, Doc Component Type Check, Doc Fence Language Check, README Export Check, Pre-Install Import Graph Check, Inert vi.mock Specifier Check, Internal Docs Link Check, Docs Route Eager Closure Check, Skill Eval Token / Example / Guide Path Checks, Action Ref Convention, Shell Escape Residue Scan, Live E2E (informational), label
- skipped: Test (coverage), Test (coverage shard), dependabot
- 0 failures observed.
mergeable_state:unknownon the first read,behindon a second read a minute later —origin/mainis now 2 commits past the merge-base (b1a14f28,968dc1ee); their file sets are disjoint from this PR's 11 files (they touchfield-types.ts,index.ts, one new test).
New findings
- Low (cosmetic, non-blocking) — 速读 ②, body L508, gives one figure for both ACCEPTED entries ("只 import
CardSchema、或深链layout.zod.js的入口放行,212,567 字节"); the deep-link entry is 212,563. Expectation: cite both (212,567 / 212,563) or say ≈212.5 K. Can ride the undraft edit; not worth a commit. - Info (no action here) — objectui#8598's table carries the round-2 review's own probe (227,056 / 29,933 / 29,933 B), the PR's texts carry the dev's instrument (750,542 / 212,567 / 212,563 B). Both are attributed to their source and agree on the three verdicts; a reader following the pointer sees two tables. Expectation: Ship
@object-ui/types/zodas ONE bundled module so the node recursion-point fill survivessideEffects:falsedeep-linking bundlers #8598's H-check re-measures on its own head anyway; optionally a one-line note on the card when it is dispatched.
Nothing else. Verify items 1–10 of the re-review brief all check out; the 速读's Clause-② framing ("已声明、未在每种打包器下强制") matches what the code and changeset actually say.
Maintainer-only merge: no — batch #98 has the director seat undraft and arm squash auto-merge once CI is green. Before doing so the director seat must re-read Build Docs, Type Check, Lint and Test (shard 1/4 … 4/4), all still in progress at this reading, and note mergeable_state: behind (an update-branch may be needed if protection requires up-to-date; the two new main commits do not overlap this PR's files).
Generated by Claude Code
Part of #8344
⛔ Draft, not enqueued, no auto-merge — until the tier re-review of THIS head passes with CI green (including
Build Docs); then the director seat undrafts and arms squash auto-merge (non-governed; objectui#8344 decision batch #98, comment 5587037055).Part of, notFixes: the card stays open when this lands.⭐ The blocker this PR was parked behind is GONE. The ruling was "wait for objectui#8498"; #8498 landed as PR #8544 (
67749c724) and this branch has merged it and composed with it — see the round-2 section for the one-line conflict and how it was resolved without either side losing behaviour. The second condition, theframeworkchunk ceiling, is now measurablymain's own overshoot rather than this branch's.⭐ What ships, in one line (round 3, head
027dcdf7): the recursion point is redirected by an option-slot write that is live by reference; thechatbotwidening is ELIMINATED at child slots (root untouched); the tree-shake gap forsideEffects:false-honouring bundlers that never readAnyComponentSchemaships DECLARED, measured across three bundler classes, and objectui#8598 is the card that closes it. Ruled in batch #98 — the round-3 section at the bottom is the authoritative description of this head; rounds 1–2 are the record of how it got here.The sections between here and the round-3 section are the previous revisions' record; where a figure or a claim there was superseded, it now says so in place.
AnyComponentSchemais a FLATz.unionover 106 arms, andZodError's constructor eagerlyJSON.stringifys the whole issue tree. Once a REFUSED node can appear at a child slot — which is exactly what this PR makes possible — that tree nests once per level and grows about 25x per level.safeParseresult.messagecharsmain)RangeError: Invalid string lengthsafeValidateSchemais documented "Safely validate a schema without throwing errors", and@object-ui/cli'scheck/validatecall it on user documents. Controls: green documents are unaffected (the same shape 6 deep, icon legal, 10 ms — the whole cost is on the refusal path); onmaindepths 1-5 are simply ACCEPTED, which is objectui#7869 itself. Filed as #8498.What changed
SchemaNodeSchema's component arm wasBaseSchemaCore— the ~21 base keys and nothing type-specific — so per-type enforcement was ROOT-ONLY at every depth, for every component type. The arm is nowAnyComponentSchema.⛔ Nothing is
.strict().BaseSchemaCorekeeps its passthrough, no schema gained acatchall, no component arm was added.The module-cycle break, and why the card's suggested spelling is wrong
AnyComponentSchemais built inindex.zod.tsfrom all 13 category modules while 14 modules importbase.zod.ts, so the arm cannot be an import —z.lazydefers evaluation, not the module graph.⭐ The card suggests "a late-binding holder that
index.zod.tsfills". That spelling is measurably wrong and this PR does not use it.z.lazyMEMOISES, and merely parsing any component schema resolves it, so whichever module graph parses first would decide the accept set for the whole process — and theunitproject runsisolate: false, one module graph per worker. Measured with the holder in place: the new pin passed run alone and FAILED in the full run. Making the unfilled holder THROW converges but turns that same import order into dozens of red suites.⇒ the arm is a written
z.unionOPTION SLOT. Measured on zod 4.4.3,z.union(opts)keepsoptsby reference and re-reads it every parse, so the fill is live, the pre-fill window answers exactly asmaindoes, and no first parse can freeze the wrong answer in. That by-reference behaviour is asserted at fill time, because a zod that copied the array would leave the redirect silently inert.One slot names
BaseSchemaexplicitly — ruled A, confirmed as writtencomplex.zod.ts#DashboardWidgetSchema.component(the legacy{ id, component, layout }envelope). Following the redirect there would refusemetric-card, objectui's CLOSED widget-slot component extension admitted by the 2026-08-14 ruling (objectstack#8593) and deliberately not an arm of the component union — so the card's fall-off-the-back route has no landing site. Two pins caught it.SchemaNodeSchemaand is refused now; no corpus document, fixture or pin writes one. ⛔ This sentence used to read "one measured delta and only one" — that was wrong about the PR as a whole and is corrected: the contract review found a second one, thechatbotwidening, measured and declared in the round-2 section below.packages/types/src/__tests__/zod-mirror-parity.test.tsis edited now (it was held by #7762 until PR #8485 merged; it is released).The previous revision reported it "green and untouched". The blobs were identical — but the ledger is a TYPE MAP over the mirrors, so the
complex.zod.tscarve-out moved a drift row inside that file without editing it. CI'sType Checkwas red on one diagnostic:Why the earlier local reading said exit 0 — the cache hypothesis is FALSIFIED. The package's
type-checkis three tsc projects:tsc --noEmit(the BUILD project, which excludessrc/**tests), thentsconfig.examples.json, thentsconfig.test.json(the only one that compiles the ledger). The full script was run ONCE, before the carve-out existed; after it, onlytsc --noEmit -p tsconfig.jsonwas re-run — and that project is green even now. So the exit 0 was a STALE reading attributed to the final tree, not a turbo replay. Proven on a fresh worktree, freshpnpm install, notsconfig.tsbuildinfoon disk, andpnpm --filterdoes not route through turbo at all:pnpm --filter @object-ui/types type-check(all three)tsc --noEmit -p tsconfig.json(the project re-run last time)tsc -p tsconfig.test.json(the project that reads the ledger)⇒ a second false-green path for this file, alongside the known one (vitest cannot see the type-level reconciliation at all): re-running only the build project after a mirror change. Both are now written onto the ledger entry.
The drift row, re-derived from the tree
⛔ No figure copied. Resolved with the compiler API over
tsconfig.test.json, walking union members byisStringLiteral().value:Non-vacuity control, same run, same instrument:
DashboardComponentSchemareads'globalFilters' | 'header' | 'widgets'for the first andneverfor the second — so the probe discriminates rather than echoing.componentis drift because TS declaresSchemaNode(BaseSchema | string | number | boolean | null | undefined) while the mirror now declaresBaseSchemaalone: the five primitive arms. The entry moves'options'→'component' | 'options'. The header figure moves with it, 64 → 65 keys across an unchanged 42 entries, derived by an AST count of the interface, not stepped.Firing control, both directions
tsc -p tsconfig.test.json'options'(UNDER-record)'layout'(OVER-record)Both mutations proven on disk before any result was read (two-key spelling 1 → 0, one-key spelling 1), restored under a
trap … EXIT INT TERMwithgit checkout HEAD -- PATH, and the restore proven byte-identical:git diff HEADempty, blob1343f20eon both sides. ⛔viteston that file is a false green for the type map and was not used as the instrument.The measurement — re-derived at the branch point
R1 and R3 are a matched pair measured at ONE commit,
c90395b2, zod 4.4.3, face read frompackages/types/dist/zod/index.zod.js:BaseSchemaCoreAnyComponentSchemaR3 − R1 = 9, and the newly-refused set is EXACTLY the nine documents the card enumerates, by file and line, with nothing dropping out. Controls, same run: the script's own three-corpus readings 46 / 556 and 296 / 556, against the card's stated 46 / 555 and 296 / 555 at
5505aec1⇒ the corpus grew by one document and the refusal counts did not move.phase2-schemas.test.ts, which was never in the 554.)Per-document disposition of the nine
content/docs/api/schema-reference.md#L173badge.variant: "success"is in noBadgeSchemavocabularycontent/docs/api/schema-reference.md#L1267bodyis undeclared on the.strict()widget schema; rewritten to the ruled-inmetric-cardshorthandcontent/docs/guide/schema-playground.md#L98stack.direction: "vertical",gap: "md", anditems(not aStackSchemaslot;childrenis)examples/schema-catalog/.../with-toast-trigger.jsonbutton.onClickis objectui#6182's only specimen andcomponent-fixture-declared-keys.test.ts's positive controlexamples/schema-catalog/.../search-interface.jsonfilter-builder-mirror-6939.test.tspins it as still-refusing and says the operator vocabulary needs its own rulingexamples/schema-catalog/.../form-integration.jsonui:calendar→ #8499examples/schema-catalog/.../complete-layout.jsonheader/nav/main/footer/article/section→ #8499content/docs/utilities/runner.mdx#L323h1, registered safe-HTML set → #8499content/docs/utilities/runner.mdx#L269my-componentis the reader's own plugin, already exempted; no arm can ever existPlus one 10th document the corpus could not reach, found by the suite: the nested
tablenode inphase2-schemas.test.tsomits the REQUIREDdata— repaired.The other ledger row that moved
zod-lazy-getter-identity-7918.test.ts—SchemaNodeSchemamovesTDZ_BOUND→MEMOISED. Building the union once belowBaseSchemaCoredissolves its TDZ, so the memoisation that file calls "worth doing where it is free" became free for this one const. Recorded as a BYPRODUCT, with #7918's eight-name list kept verbatim as history and an explicit ⛔ against moving the remaining seven. Cross-checked: the measurement script'sunstableLazyExportswent 8 → 7.The⚠️ and it is NOT only type-level — see the round-2 section: the same mirror widens the RUNTIME accept set at every child slot):
chatbotdeclaration collision is NAMED, not silenced (z.output< typeof AnyComponentSchema >is not assignable toSchemaNodefor exactly one arm of 106, becauseChatbotSchemamirrors the chat API body params under the keybody. Pre-existing and already ledgered asKnownDrift. So the fill site takes a loose bound and the real check is kept EXACT one level out, as a type-level pin naming that single arm.Verification, re-run on the merged tree with the turbo cache BYPASSED (⚠️ SUPERSEDED by the round-2 section below — kept as the record of the first merge)
Merged⚠️ The merge first refused with
origin/mainf08d1a86in (⛔ no rebase, no force-push).fatal: refusing to merge unrelated histories— a shallow-checkout artifact, not a divergence: the clone was grafted at depth 1, and the ancestry control leg (a commit known to be inmain's history) also returned exit 1. Aftergit fetch --deepen=200the control leg returned 0,c90395b2was confirmed an ancestor, and the merge-base resolved to exactly the branch point.Everything below ran through
scripts/pm/os-verify-lock.sh(slotissue-8344-objectui), verdicts read from its own VERDICT lines, exit codes captured by redirect before any pipe.pnpm exec turbo run build --filter='./packages/*' --force --concurrency=2— VERDICT command-exit 0, 39/39 tasks, no cache reads.pnpm --filter @object-ui/types type-check— VERDICT command-exit 0 (all three projects).pnpm exec vitest run --project unit(whole project) — 976 of 977 files green. The one red,scripts/__tests__/network-escape-ledger.test.ts, reproduces identically on a clean detachedorigin/mainworktree and is green run alone → pre-existing, filed finding(tooling):network-escape-ledger.test.tsfails on cleanmainin a full--project unitrun — another file leaves avi.fn()onglobalThis.fetch#8500.check:readme-exports(0 unbuilt, 58 keys compared),check:node-esm-load(no foreign-provenance entries this time),check:doc-snippets(632/632 blocks),check:doc-examples(124 blocks). Still exit 0:check:control-bytes,check:doc-types,check:doc-fences,check:self-import,check:phantom-deps,check:unused-deps,check:entry-guard,check:unreferenced-sources,check:sdui-registration-pins,check-type-check-coverage, the three changeset gates.PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 8501→ exit 0, both carriers agree.check-lint-rule-coverage.test.tswent red onapps/console/plugin.js, a git-ignored file (apps/console/.gitignore:16) generated by the forced build. Removing it turns the test green (1 failed → 27 passed) and it is untracked, so it is not in the diff. Already filed upstream as #8369.⛔ Console Performance Budget is RED, and I am not touching it (⚠️ figure SUPERSEDED below — it is now
main's red, not this branch's)Built the console and ran
scripts/check-eager-closure-budget.mjslocally:Exact, not rounded: the
frameworkchunk gzips to 71,007 bytes againstPER_CHUNK_GZIP_CEILINGS.framework = 71_000— over by 7 bytes. This PR's measured contribution is +213 B minified+gzip (base.zod.js+190,index.zod.js+25,complex.zod.js−2), almost all of it the wiring's error string and the option array; the dist figure of +8.6 KB is docblocks and does not ship. Every other ceiling has headroom (aggregate 38.8 KB,ui-components5.6 KB).⛔
PER_CHUNK_GZIP_CEILINGSandPER_CHUNK_BASELINEare untouched — that is a maintainer-floor action and the decision is already with the maintainer. ⛔ The deliverable was not trimmed to chase 7 bytes.Round 2 — batch #93: F2 and F5 ship, F3 was implemented and REVERTED by CI; head
ca2037680⛔ Still a draft, still not enqueued, no auto-merge,
PER_CHUNK_GZIP_CEILINGSandPER_CHUNK_BASELINEuntouched. Every figure below was re-derived on this tree; ⛔ nothingis copied forward from the sections above.
⭐ Batch #93, item by item — what this head does and does not do
Ruling: #8344 comment 5585333656 (director seat, decision batch #93), which followed the
claude-fable-5-1re-review (#8501 comment 5585318764). ⛔ Two earlier answers from the PM seatsent this branch down routes that ruling had already refused; that is recorded plainly in the
report on the card rather than smoothed over here. Every row below is the state of THIS head.
chatbotwidening with asuperRefineon the installed arm, pin both directionsbase.zod.ts#nodeComponentArm; four pin legs innode-recursion-point-8344.test.ts— nested refused, root accepted, nested-without-bodyaccepted, and the refusal namesbodyz.lazygetterBuild Docsfailed withReferenceError: Cannot access 'BaseSchema' before initializationout ofdist/zod/app.zod.js. Makingbase.zod.tsimport the barrel gives the cycle an edge that is only safe when the barrel evaluates FIRST, and a bundler may order the cycle category-module-first — the site's build doesca2037680; the changeset states the gap, the CI evidence and all four measured candidatespackages/typestest files needed it while F3 was in;safeValidateSchemaincluding a legal-leaf control:125-127sentence; the:124-126"FRESH object per call" sentenceany-component-union-fanout.test.ts's "simply ACCEPTED" paragraph corrected in place; both pin comments now describe this head, where the getter builds per callMEMOISEDfact and the "maintainer-floor, deliberately not taken" caveat are gone; the section now states the nested narrowing, the untouched root, and the wiring with both byte costsMEMOISED, matching the wiring that shipsTDZ_BOUNDonly while the F3 binding was in; with the slot-0 write restored the getter returns the one union againneeds:contract-reviewre-hung on both carriers with the round-3 pushca2037680claude-fable-5-1, isolated seat): Q1 / F1 / F4 / F5 / items 8-9 implemented; Q2 not implemented under #96 → batch #98 amended #96 and moved the single-module route to objectui#8598. Findings 3–6 are round 3's owed listca2037680Build Docssuccess,mergeable_state: clean(read after the fact from the check-runs API)How the recursion point is wired on this head —⚠️ replaces the round-2 paragraph, which described the reverted getter binding
The option-slot write.
SchemaNodeSchema's component arm is slot 0 of a plainz.union(nodeUnionOptions)inbase.zod.ts;index.zod.tswrites that slot from insideAnyComponentSchema's ownconstinitializer throughdefineNodeComponentUnion(...), and whatit writes is the discriminated union WRAPPED in the F2
superRefine. The write is live byreference (zod 4.4.3's
z.unionre-reads its option array on every parse; asserted at filltime), the getter returns that one union, and ⛔ nothing imports the union into
base.zod.ts.The tree-shake gap this leaves for a
sideEffects:false-honouring bundler that never readsAnyComponentSchemaships DECLARED, with the three-class measurement and the closing card(objectui#8598) in the round-3 section.
The merges
edc50edf3origin/main3f775eeb8(12 commits)053914f05origin/main0c4694437, which contains #8544 / objectui#8498 (67749c724, merged 11:40:59Z)6d0e8713corigin/mainf5cfbbd81, which contains #8540 / objectui#8338 (theToastSchemaretirement)KnownDriftheader prose⭐ The named interaction risk fired, and it resolved by COMPOSITION rather than by a
choice. #8544 rewrote the very line this branch wraps:
Neither side lost behaviour: #8544 changed WHICH arm reports a refusal, #8344 changed WHERE
this union is consulted. Both docblock halves are kept, with a paragraph naming the
composition.
Why the option-slot mechanism is untouched — checked, not assumed. The slot lives in
base.zod.ts's ownz.union(nodeUnionOptions), and #8544's diff does not containbase.zod.tsat all (read from the PR's own diff, file list:crud.zod.ts,index.zod.ts,objectql.zod.ts, the CLI'sunion-arm-diagnostics.ts, two CLI tests, onetypes test, one changeset).
defineNodeComponentUnionwrites whatever schema it is handedinto slot 0 and asserts identity there, so a discriminated arm is the same write.
Then measured on the composed tree, ⛔ not inferred:
pnpm --filter @object-ui/types buildpnpm --filter @object-ui/types type-check(ALL THREE projects)tsc --noEmit -p tsconfig.json/tsc -p tsconfig.test.json, run separatelyvitest run packages/types/ --project unitany-component-union-fanoutpins and this branch'sthe fill is LIVEleg in the same runvitest run --project unit(whole project)053914f05, ⛔ not re-run after the third mergeRangeError: Invalid string lengthonpackages/types/examples/dashboard.ts. On thecomposed head that count is 1 → 0. That is objectui#8498's fix, observed from this
branch's own corpus run.
The corpus, re-derived — R1 and R3 as a matched pair on ONE corpus
Both legs read the built face out of
packages/types/dist/zod/index.zod.js; the pair differsonly in WHICH tree's face is loaded, so the corpus is identical for R1 and R3 (
main0c4694437), zod 4.4.3.mainas shippedBaseSchemaCoreAnyComponentSchemaAnyComponentSchemaR3 − R1 = 9, and the newly-refused set is byte-for-byte the nine documents the card
enumerates — same files, same line numbers,
no longer refused: 0. Controls from the sameruns: the script's own three-corpus readings 46 / 556 and 296 / 556, unchanged from
the readings this PR quoted at
c90395b2;unstableLazyExports8 onmain, 7 here.The ledger row, re-derived on the composed tree
⛔ No figure stepped or copied. AST count over the interface, plus the compiler API over
tsconfig.test.json:42 entries / 65 keysuntil the third merge;main's #8540 then RETIREDfeedback.zod.ts#ToastSchema,entry and its single key together, taking it to
41 / 64. This branch's own contribution iswhat it always was — one key on an existing entry, no entry added. The conflict that
brought it here was in the header PROSE only, both narratives are kept in the order the
history happened, and the leading figures were RE-COUNTED by the AST walk rather than stepped
from either side. The row itself, and its non-vacuity control, are unchanged across all three
merges, and
tsc -p tsconfig.test.jsonis green on the final tree.F2 as implemented — the widening is gone, and both directions are pinned
ChatbotSchema.bodymirrors the chat API's body params asz.record(z.string(), z.unknown()),wider than
BaseSchemaCore.body, and it is the only wider redeclaration among the 109 base-keyredeclarations across the union's arms. The arm the recursion point installs now carries a
superRefinethat checks a nestedchatbotnode'sbodyagainstBaseSchemaCore.shape.body.⛔ The published
ChatbotSchemais untouched.mainbodyat the ROOTcard.body[]div.children[]bodyicontextnode⇒ the redirect narrows at all 109 redeclarations and widens at none. The discrimination
objectui#8498 added survives the wrapper — the arm still carries
propValues, so a nestedrefusal costs one arm rather than 106, and the depth table below is the proof of that in bytes.
The root question — whether that mirror should carry the chat API's params under
bodyat all— is recorded on objectui#8572 and ⛔ not decided here.
F3 — implemented, measured, and reverted by CI. ⛔ The leak is still open.
What the ruled route bought, measured before it was reverted. Same probe, two entries
differing by one import:
CardSchemaAnyComponentSchema⭐ The reviewer's prototype proved the loud
ReferenceErroron barrel-free entry but explicitlydid not test bundler retention; that table is the missing half, and the route does work.
Why it is not here anyway. CI's
Build Docsfailed on2ac53818e:Making
base.zod.tsimport the barrel gives the module cycle an edge that is only safe when thebarrel is evaluated FIRST. Node honours that when the entry is
./zod; a bundler is free toorder the cycle category-module-first, and the site's build does. ⇒ the spelling is sound for
a module graph and unsound for a bundle — the opposite of the property it was chosen for. ⛔ My
own docblock claim that "consumers cannot hit that" was falsified by CI, and the 102-file
barrel-first hygiene could not have helped: the failing consumer already imports the barrel.
Reproduced locally in one line, both directions: with the binding,
import 'dist/zod/app.zod.js'throws; without it, it loads clean. The docs build passes locally on theshipped tree (
turbo run build --filter=@object-ui/site— 30/30 tasks, compiled successfully).⇒ F2 and F5 ship; F3 does not. All four candidates are now measured and none is available⚠️ The changeset states
without a ruling: the import binding breaks a real consumer, a narrowed
sideEffectsarray isillegal for this package, a bare top-level call is dropped by the same flag, and dropping the
flag costs 16,078 gzipped on
frameworkplus a guard's census floor.the gap rather than claiming a fix.
The eager-closure figure, and why it moved twice
2ac53818e(F3 in)plugin-detail248.46 → 250.72KB and
fields243.24 → 243.54 KB aremain's own changes (#8584 and #8586 landed while thisbranch was in flight; this PR touches neither package). ⇒ two CI runs on different bases are not
apples-to-apples, and the branch is 3 commits behind
mainas written.F5 as implemented — depth on the redirected path
Measured on this head,
safeValidateSchema, a badbadgeleaf under N levels ofcard.body:.messagecharsCompare the pre-objectui#8498 table at the top of this body: 14,624 → 741,330 → 18,956,924 →
428,269,086 →
RangeError. Growth is now linear, ⛔ not ×25 per level, and depth 4 no longerthrows. Pinned with a ceiling well under the old growth plus a legal-leaf control, because a
pin on the exact length would only measure wording.
Console performance budget
frameworkreads 72,248 bytes gzipped on the shipped wiring, against the 100,000 ceilingobjectui#8550 raised —
check:eager-closureexits 0. While F3 was in it read 90,969(+18,721), which is what closing the leak costs this repo's own console; the refused
sideEffects-drop route would have cost 16,078 on the same chunk.the record even though neither ships, because the next attempt at the leak pays one of them.
⛔
PER_CHUNK_GZIP_CEILINGSandPER_CHUNK_BASELINEuntouched by this PR.3,493.9 KB against a 3,512.7 KB budget — 18.8 KB of headroom, about 0.5%. A change of that
size fails the gate. ⛔ Not a proposal to raise anything; just the state.
Gates and tests on this head
pnpm --filter @object-ui/types build— 0 ·type-check(all three projects) — 0vitest run packages/types/— 148 files, 2,825 tests, 0 failedvitest run --project unit(whole project) — 989 files, 16,837 passed, 2 skipped, 0 failedcheck:eager-closure— 0 (see the budget table) · the four changeset gates — 0 ·check:control-bytes— 0main7102b20d9: R1 45 / 554, R3 54 / 554, thesame nine documents and none dropping out, post-repair 51 / 554, three-corpus controls
46 / 556 and 296 / 556
KnownDrift41 entries / 64 keys, row'component' | 'options',non-vacuity control unchanged
ca2037680was read afterwards: green,Build Docsincluded (see the round-2 table).Round 3 — batch #98: what ships, declared; head
027dcdf7Ruling: objectui#8344 comment 5587037055 (director seat, decision batch #98), amending batch #96
(5586607584) after the
claude-fable-5-1review ofca2037680(#8501 comment 5587000173).Provenance: maintainer 「8501 你负责跟进到合并」. This round is a takeover on the same branch
(#8344 comment 5588910851): rounds 1–2 were authored by the os-sales seat in
session_01CZY49skxUBYyJcdnTcYPrE, round 3 by the director seat's dev insession_01TezFG8ZMrNH6n5VTNpPpdH. Round 3 mergedorigin/main9f5de79dd(merge commitf1898a8e, no conflicts) and then changed comment lines inbase.zod.tsand the changesetonly — ⛔ no source outside comments moved, the module graph is untouched.
How the recursion point is wired on this head — the option-slot write
SchemaNodeSchema's component arm is slot 0 of a plainz.union(nodeUnionOptions)inbase.zod.ts, andindex.zod.tswrites that slot from insideAnyComponentSchema's ownconstinitializer throughdefineNodeComponentUnion(z.discriminatedUnion('type', [...])).What is written is the discriminated union WRAPPED in the
superRefinethat keeps a nestedchatbotnode'sbodyat the node-slot shape (F2). The write is live by reference — zod 4.4.3'sz.unionre-reads its option array on every parse, asserted at fill time — so before the barrelevaluates a child slot answers exactly as
main, and from the moment it does every parse seesthe union. The
z.lazygetter returns that one union (memoised,getter() === getter()TRUE,the objectui#7918 row is
MEMOISED). ⛔ Nothing on this head imports the union intobase.zod.ts: that getter-binding route was implemented at98265165, measured, refused by CI(
Build Docs, TDZReferenceErrorunder Next/Turbopack) and reverted atca2037680; thechangeset carries the evidence.
The tree-shake gap ships DECLARED — who is exposed
This package declares
"sideEffects": falseand the fill is a statement in the barrel's body,so a bundler that honours the flag and sees no reference to
AnyComponentSchemadrops it, andevery child slot then validates with the pre-#8344 arm — silently. Measured on this head's
published
dist/zodface (Vite 8.2.1 lib build,es, esbuild-minified,zod4.4.3 and@objectstack/specexternal so the figures are this package's own bytes, ONE entry per build,each read in a fresh Node process; nested off-spec node
{ type: 'icon', icon: 'check', size: 'huge' }incard.body[]throughCardSchema). Same three classes review 5587000173 measured,same verdicts; the figures below are the ones
base.zod.tsand the changeset now cite:AnyComponentSchemain the import graph — the CLI's path (check/validatecallsafeValidateSchema, which references the union)export { CardSchema, AnyComponentSchema } from './zod/index.zod.js'CardSchemaonlyexport { CardSchema } from './zod/index.zod.js'sideEffects:false-honouring deep-linking bundler (Turbopack / webpack model)export { CardSchema } from './zod/layout.zod.js'Legal twin ACCEPTED in all three (non-vacuity). ⇒ an external consumer whose bundler honours
sideEffects: falseand never readsAnyComponentSchemakeepsmain's accept set for NESTEDnodes; root-level enforcement and every barrel-reading consumer (Node, vitest, the CLI, any
bundle that imports the union) get the new set at every depth. No published entry order
throws. The changeset's fact 3 says exactly this and names who closes it.
The closer is objectui#8598 — the
./zodface built as ONE module, so a consumer bundler hasno internal graph to link past. Batch #98 moved that route out of this PR: it is a build-pipeline
change (a second build stage in
packages/types, most likely avitedevDependency — a manifestfield, hence its own stop-and-report), not a schema change, and it does not reach this repo's
console or vitest, which alias
@object-ui/types/zodtosrc. #8598 ispm:blockedon this PRbecause its H-check (a
CardSchema-only entry REFUSES) is false onmainby construction untilthis lands.
This repo's console is a class-(ii) consumer: the only zod-face import in its eager closure
is
packages/plugin-map/src/ObjectMap.tsx(ObjectMapConfigSchema, one schema),Bundle Analysisreadframeworkat ~70.6 KB gzipped onca2037680against the 97.7 KB (100,000 B)ceiling, and no console code parses a node tree through the zod face (
SchemaRenderervalidatesthrough
@object-ui/core) — no observable effect today.Batch #98 owed list, item by item
base.zod.tsvs changeset; the stale "ruling in flight" sentence; the leak paragraph points at #859898265165(102 files, +4 lines each), reverted atca2037680(102 files, −4 each); on this head the import line and its comment match 0 files. Verified the way the ruling asked:pnpm exec vitest run packages/types/ --project unit→ 150 files, 2,866 tests, 0 failed with zero barrel-first imports, so no file needs the barrel first on this headneeds:contract-reviewon PR and card; nothing else movessideEffects: false,PER_CHUNK_GZIP_CEILINGS,PER_CHUNK_BASELINE,packages/types/package.json, the objectui#7918 row andzod-lazy-getter-identity-7918.test.ts, the rootchatbotquestion (objectui#8572): all untouchedGates on this head (
027dcdf7)pnpm --filter @object-ui/types build— 0 ·pnpm --filter @object-ui/types type-check— 0,all three
tscprojects echoed (--noEmit,tsconfig.examples.json,tsconfig.test.json)pnpm exec vitest run packages/types/ --project unit— 150 files, 2,866 tests, 0 failedcheck:control-bytes— 0 · the four changeset gates (presence,no-major,overwrite,fixed) — 0 eachdistafter the docblock edit — figuresbyte-identical (minified output cannot see comment edits; that is why the instrument is minified)
ca2037680: 33 check-runs, 0 failures,Build Docssuccess. This round changes no modulegraph, so
Build Docsis expected to stay green on027dcdf7— ⛔ read it there before undraftingcheck-clause2-carriers.mjs --pair 8501read exit 4 BEFORE the re-hang (labelsoff since 14:27Z while the head moved — the expected 重挂-owed state); the post-re-hang reading
is in the round-3 report on spec(types): redirect the node recursion point from BaseSchemaCore to AnyComponentSchema — measured at 9 newly-refused documents, and it drops 118 phantom strict refusals #8344
维护者速读(草稿)
改了什么 — 子节点的校验入口从「只认 21 个基础键」改成「认它自己的组件 schema」:同一个节点在
任何深度得到同一个判定。机制是
index.zod.ts在构造组件联合体时,把它(外面包一层对嵌套chatbot节点
body的槽位收窄)写进base.zod.ts里那个z.union的 0 号槽位;写入按引用即时生效。⛔ 没有从
base.zod.tsimport 联合体——那条路 CI 证明会让真实使用者的打包产物在加载时抛ReferenceError,已回退,本 head 上不存在。为什么改 — strict 编写面这条线的结构前提:objectui#7869 量到嵌套的 off-spec 节点被放行、同一
节点单独放就被拒。本 PR 关掉这条不对称;
chatbot那一处顺带收窄而不是放宽,根节点不动。风险与代价(含回滚) — ① 语料库上 45 → 54 份文档被拒(9 份,每份都是本来就有的债;3 份已在本 PR⚠️ 已声明、未在每种打包器下强制的缺口:使用者的打包器若遵守
修好,其余各有卡)。②
sideEffects:false且整个 import 图里没有读到AnyComponentSchema,嵌套节点仍按旧规则放行(实测:只 import
CardSchema、或深链layout.zod.js的入口放行,212,567 字节;同时 import 联合体的入口拒绝,750,542 字节)。根节点校验、Node / vitest / CLI、以及所有读到联合体的打包产物都得到新规则。
关掉缺口是 objectui#8598(把
./zod打成单模块),按批次 #98 裁决从本 PR 拆出,并已在 changeset 里点名。③ 本仓 console 只 import 一个 schema(
plugin-map/ObjectMap.tsx),属于第 ② 类,今天没有可观察影响;
framework块约 70.6 KB,上限 97.7 KB。④chatbot根节点的body该不该带聊天 API 参数,留在 objectui#8572。回滚:revert 本 PR 即回到
BaseSchemaCore作递归点,无数据迁移。席位意见 — 待定。
你要做的 — ① 本 PR 不受管(diff 不命中 AGENTS.md / .claude / docs/adr / skills / releases);
按批次 #98 与「8501 你负责跟进到合并」:tier 复审 PASS 且 CI 全绿(含
Build Docs)后,由 director席取消 draft 并挂 squash auto-merge,⛔ 不是「由维护者合并」。② 若你认为「已声明、未在每种打包器下
强制」不能以 Clause-② 面出货,批次 #98 写明可逆:恢复 #96,objectui#8598 变成本 PR 的 blocker 而不是
后续卡。③ objectui#8572 本 PR 不替它做决定。
🤖 Generated with Claude Code
https://claude.ai/code/session_01TezFG8ZMrNH6n5VTNpPpdH
Generated by Claude Code