From ae994801159e9a93d86e5e0e47e68000c1f1c859 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 03:00:08 +0000 Subject: [PATCH 1/9] feat(types)!: redirect the node recursion point at AnyComponentSchema (#8344) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 c90395b2 (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 Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE --- .../8344-node-recursion-point-redirect.md | 38 ++++ content/docs/api/schema-reference.md | 6 +- content/docs/guide/schema-playground.md | 6 +- .../node-recursion-point-8344.test.ts | 171 ++++++++++++++++++ .../src/__tests__/phase2-schemas.test.ts | 4 + .../zod-lazy-getter-identity-7918.test.ts | 26 ++- packages/types/src/zod/base.zod.ts | 166 +++++++++++++++-- packages/types/src/zod/complex.zod.ts | 22 ++- packages/types/src/zod/index.zod.ts | 18 +- 9 files changed, 429 insertions(+), 28 deletions(-) create mode 100644 .changeset/8344-node-recursion-point-redirect.md create mode 100644 packages/types/src/__tests__/node-recursion-point-8344.test.ts diff --git a/.changeset/8344-node-recursion-point-redirect.md b/.changeset/8344-node-recursion-point-redirect.md new file mode 100644 index 0000000000..11f4d71c74 --- /dev/null +++ b/.changeset/8344-node-recursion-point-redirect.md @@ -0,0 +1,38 @@ +--- +'@object-ui/types': minor +--- + +Redirect the node recursion point from `BaseSchemaCore` to `AnyComponentSchema` +(objectui#8344) — a nested node is now judged by its OWN component schema. + +**Behaviour change, deliberately, at every depth below the root.** Every child slot +(`body`, `children`, and every per-component redeclaration of them) 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, for every component type: objectui#7869 measured +an off-spec `size` on a nested `icon` node being ACCEPTED while the same node standing +alone was refused. The arm is now the union of the registered component mirrors, so +the same node gets the same verdict at every depth. + +⛔ **Nothing here is `.strict()`.** `BaseSchemaCore` keeps its passthrough, no schema +gained a `catchall`, and no declaration was repaired. Measured over the catalog + +docs corpora on `c90395b2` (431 catalog files + the `json` fences under +`content/docs`, 554 node documents): **45 refused before, 54 after** — nine documents, +each one pre-existing debt this SURFACES rather than creates. Four have a child whose +`type` resolves in no arm; five carry a child already red under its own schema and +shielded until now by the recursion point. + +**What an author sees.** A document whose nested node is off-spec — a bad enum value, +a wrong-typed key, a `type` no component mirror declares — is refused now, where it +parsed green before. That is the point of the change, and it is why this ships behind +a contract review rather than as a patch. + +**Two mechanical notes for anyone editing the wiring.** `AnyComponentSchema` is built +in `zod/index.zod.ts` from all 13 category modules and 14 modules import +`zod/base.zod.ts`, so 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, and it is a `z.union` option rather than a +`z.lazy` holder because `z.lazy` memoises its getter: a holder would let whichever +module graph parsed first decide the accept set for the whole process. Both +constraints are measured, and the reasoning lives on `defineNodeComponentUnion` in +`zod/base.zod.ts`. diff --git a/content/docs/api/schema-reference.md b/content/docs/api/schema-reference.md index d02264fc61..9cd20de03d 100644 --- a/content/docs/api/schema-reference.md +++ b/content/docs/api/schema-reference.md @@ -178,7 +178,7 @@ A styled container with optional header, body, and footer regions. "variant": "outline", "hoverable": true, "header": [ - { "type": "badge", "label": "Live", "variant": "success" } + { "type": "badge", "label": "Live", "variant": "secondary" } ], "body": [ { "type": "statistic", "label": "Total Revenue", "value": "$12,400" } @@ -1281,8 +1281,8 @@ Schemas are designed to compose. Nest any `SchemaNode` inside another to build c "type": "dashboard", "columns": 2, "widgets": [ - { "id": "w1", "title": "Leads", "body": { "type": "statistic", "value": "142" } }, - { "id": "w2", "title": "Revenue", "body": { "type": "statistic", "value": "$24k" } } + { "type": "metric-card", "title": "Leads", "value": "142" }, + { "type": "metric-card", "title": "Revenue", "value": "$24k" } ] } }, diff --git a/content/docs/guide/schema-playground.md b/content/docs/guide/schema-playground.md index 7cbaa7e59e..d5486edcb3 100644 --- a/content/docs/guide/schema-playground.md +++ b/content/docs/guide/schema-playground.md @@ -103,9 +103,9 @@ A content card with a header, description, and body: "icon": "dollar-sign", "body": { "type": "stack", - "direction": "vertical", - "gap": "md", - "items": [ + "direction": "col", + "gap": 4, + "children": [ { "type": "text", "content": "$48,250", diff --git a/packages/types/src/__tests__/node-recursion-point-8344.test.ts b/packages/types/src/__tests__/node-recursion-point-8344.test.ts new file mode 100644 index 0000000000..83f9c6c4dc --- /dev/null +++ b/packages/types/src/__tests__/node-recursion-point-8344.test.ts @@ -0,0 +1,171 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The node recursion point resolves per-type, at every depth (objectui#8344). + * + * ## What was wrong + * + * Every child slot (`body`, `children`, and every per-component redeclaration of + * them) is `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])`, and + * `SchemaNodeSchema`'s component arm was `BaseSchemaCore` — the ~21 base keys and + * NOTHING type-specific. ⇒ per-type enforcement was ROOT-ONLY, at every depth, for + * every component type. objectui#7869 recorded it as an ASYMMETRY, measured there: + * an off-spec node was refused standing alone and ACCEPTED one slot down, inside + * any parent. #8344 points the arm at `AnyComponentSchema` instead. + * + * ## What this file pins, and why each leg is here + * + * The headline is one behaviour — "the same node gets the same verdict at every + * depth" — and a single assertion cannot state it, because BOTH halves of an + * asymmetry have to be read to say the asymmetry is gone. So #7869's reproduction + * is pinned in both directions (refused alone AND refused nested), against a + * NON-VACUITY leg (the legal twin of the same node, accepted at both depths) that + * would catch the way this could pass while being broken — a recursion point that + * refuses everything reads as a fixed asymmetry and is a dead contract. + * + * The fourth leg is what makes it a test of the REDIRECT rather than of `icon`: + * a node whose `type` resolves in no arm of `AnyComponentSchema`. `BaseSchemaCore` + * accepts any object with a string `type`, so that node is the one input the two + * candidate recursion points disagree about MOST — it separates "the arm is the + * component union" from "the arm is the base shape" without reading a single zod + * internal. + * + * ⚠️ Recognising the recursion point by IDENTITY is pinned on the EXPORTED WRAPPER, + * ⛔ never through `.unwrap()` or a re-invoked `z.lazy` getter. That is objectui#7918 + * consequence ①, measured: the exported wrapper identity is stable and survives + * through a declared slot, while `S.unwrap() === S.unwrap()` and `getter() === + * getter()` are both FALSE. A pin written through either would compare two fresh + * objects and fail for a reason that has nothing to do with this contract. + */ + +import { describe, it, expect } from 'vitest'; + +import { AnyComponentSchema, CardSchema, IconSchema, SchemaNodeSchema } from '../zod/index.zod.js'; +import type { SchemaNode } from '../base.js'; +import type { z } from 'zod'; + +type Equal< A, B > = + (< T >() => T extends A ? 1 : 2) extends (< T >() => T extends B ? 1 : 2) ? true : false; +type Expect< T extends true > = T; + +/** + * #7869's own node, in the spelling `IconSchema` declares: `ui:icon` names its + * glyph with `icon` and sizes it with a NUMBER, so `size: 'huge'` is off-spec by + * value and `size: 24` is the legal twin of the same node. + */ +const OFF_SPEC_ICON = { type: 'icon', icon: 'check', size: 'huge' } as const; +const LEGAL_ICON = { type: 'icon', icon: 'check', size: 24 } as const; + +/** The same node one slot down — the depth #7869 measured as the shielded one. */ +const nested = (child: unknown) => ({ type: 'card', title: 'Parent', body: [child] }); + +describe('objectui#7869 — the off-spec node gets the same verdict at both depths', () => { + it('is refused STANDING ALONE (unchanged — this half was never the defect)', () => { + expect(AnyComponentSchema.safeParse(OFF_SPEC_ICON).success).toBe(false); + }); + + it('is refused NESTED — the half objectui#8344 moved', () => { + expect(AnyComponentSchema.safeParse(nested(OFF_SPEC_ICON)).success).toBe(false); + }); + + it('names the offending VALUE, not merely "some arm did not match"', () => { + // A recursion point that refused the child for the wrong reason — because the + // parent no longer matches any arm at all, say — would satisfy the two legs + // above while saying nothing about the child. Read the leaf issue. + const result = IconSchema.safeParse(OFF_SPEC_ICON); + expect(result.success).toBe(false); + if (result.success) return; + expect(result.error.issues.some((i) => i.path.join('.') === 'size' && i.code === 'invalid_type')).toBe(true); + }); + + it('NON-VACUITY: the legal twin is accepted at BOTH depths', () => { + // Without this leg, a recursion point that refuses everything passes the two + // legs above. It is the assertion that says the redirect narrowed rather than + // closed the slot. + expect(AnyComponentSchema.safeParse(LEGAL_ICON).success).toBe(true); + expect(AnyComponentSchema.safeParse(nested(LEGAL_ICON)).success).toBe(true); + }); +}); + +describe('the arm IS the component union, and not the base shape', () => { + /** + * `h1` is a REGISTERED RENDERER (`components/src/renderers/basic/html-elements.tsx`) + * with no mirror in `AnyComponentSchema` — so it is the input the two candidate + * recursion points answer differently: `BaseSchemaCore` takes any object with a + * string `type`, the component union takes none it does not declare. ⛔ Do not + * "fix" this by adding an `h1` arm to make some other test green: that is the + * public-surface widening objectui#8344 routes into its own card. + */ + const UNMIRRORED = { type: 'h1', children: 'Sales Dashboard' } as const; + + it('refuses an unmirrored node nested in a declared child slot', () => { + expect(AnyComponentSchema.safeParse(nested(UNMIRRORED)).success).toBe(false); + }); + + it('refuses the same node standing alone (the control — this was already true)', () => { + expect(AnyComponentSchema.safeParse(UNMIRRORED).success).toBe(false); + }); +}); + +describe('the late-binding wiring, read by IDENTITY on the exported wrapper', () => { + it('the exported wrapper is one stable object', () => { + // objectui#7918 consequence ①: this holds while `.unwrap()` and the `z.lazy` + // getter each return a FRESH object per call. ⛔ Never write this pin through + // either of those. + expect(SchemaNodeSchema).toBe(SchemaNodeSchema); + }); + + it('that identity survives through a declared child slot', () => { + const body = (CardSchema.shape.body as unknown as { _zod: { def: { innerType: { _zod: { def: { options: unknown[] } } } } } }); + expect(body._zod.def.innerType._zod.def.options).toContain(SchemaNodeSchema); + }); + + it('the holder is FILLED by importing the barrel — the module-cycle break works', () => { + // The behavioural read of the fill, and the only one that cannot pass + // vacuously: BEFORE the fill the arm is `BaseSchemaCore`, which accepts the + // unmirrored node above. This module imports the barrel and nothing else, so a + // break in `index.zod.ts`'s `defineNodeComponentUnion(...)` initializer lands + // here rather than in whichever suite happened to run second. + expect(AnyComponentSchema.safeParse(nested({ type: 'h1' })).success).toBe(false); + expect(AnyComponentSchema.safeParse(nested(LEGAL_ICON)).success).toBe(true); + }); + + it('the fill is LIVE, so no earlier parse can freeze the pre-fill answer in', () => { + // The property that makes this whole file order-independent, asserted rather + // than assumed. `z.union` re-reads its option array on every parse, so the + // recursion point is whatever slot 0 holds NOW — not whatever it held when some + // other file in this worker first parsed something (the unit project runs + // `isolate: false`, one module graph per worker). Measured the hard way: with a + // memoising `z.lazy` holder in place instead, this suite passed run alone and + // failed in the full run. ⛔ Do not "simplify" the wiring back to a holder the + // getter reads — re-read `defineNodeComponentUnion` in `base.zod.ts` first. + const options = (SchemaNodeSchema as unknown as { + _zod: { def: { getter: () => { _zod: { def: { options: readonly unknown[] } } } } }; + })._zod.def.getter()._zod.def.options; + expect(options[0]).toBe(AnyComponentSchema); + }); +}); + +/** + * The EXACT bound on the one assertion `base.zod.ts` needs to make. + * + * `SchemaNodeSchema` keeps its objectui#7760 annotation `z.ZodType< SchemaNode, + * SchemaNode >`, and `z.output< typeof AnyComponentSchema >` is not assignable to + * `SchemaNode` for exactly ONE of its 106 arms: `complex.zod.ts#ChatbotSchema` + * mirrors the chat API body params under the key `body`, which is `BaseSchema`'s + * CHILDREN slot. That collision is pre-existing (the parity ledger carries it under + * `KnownDrift`, the TS declaration renamed the key to `requestBody`, and + * `ChatbotSharedMirrorShape` says a ruling on `ChatbotSchema`'s own `body` arm is a + * separate question), and objectui#8344 does not decide it. + * + * ⇒ the fill site takes a loose bound and this states the real one instead. A SECOND + * arm drifting the same way turns this red — where a wide bound would have said + * nothing. ⛔ Do not repair a red here by adding the new name to the union below: + * that records a second declaration defect as if it were a contract. + */ +type ArmsNotAssignableToSchemaNode = + Exclude< z.output< typeof AnyComponentSchema >, SchemaNode > extends { type: infer K } ? K : never; + +export type NodeRecursionPointDeclarationDrift = [ + Expect< Equal< ArmsNotAssignableToSchemaNode, 'chatbot' > >, +]; diff --git a/packages/types/src/__tests__/phase2-schemas.test.ts b/packages/types/src/__tests__/phase2-schemas.test.ts index 1305d8de35..d1f44a0ae1 100644 --- a/packages/types/src/__tests__/phase2-schemas.test.ts +++ b/packages/types/src/__tests__/phase2-schemas.test.ts @@ -634,6 +634,10 @@ describe('Phase 2: View Schemas Zod Validation', () => { content: { type: 'table', columns: [], + // `data` is a REQUIRED member of `TableSchema`. Until objectui#8344 this + // node sat in a child slot judged by the base shape, so the omission was + // invisible; the recursion point resolves per-type now and it is not. + data: [], }, }, ], diff --git a/packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts b/packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts index ef24f1f66a..442abf01b4 100644 --- a/packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts +++ b/packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts @@ -24,10 +24,24 @@ * TreeNodeSchema ReferenceError: Cannot access 'TreeNodeSchema' before initialization * * Seven name the very const being declared (`children: z.array(TreeNodeSchema)` - * sits inside `TreeNodeSchema`'s own initialiser); `SchemaNodeSchema` names - * `BaseSchemaCore`, which `base.zod.ts` declares BELOW it. For those eight the - * `z.lazy` is LOAD-BEARING — it is buying a TDZ dodge, not a style — and they - * keep the spelling they have. `mechanism` below reproduces the failure. + * sits inside `TreeNodeSchema`'s own initialiser); `SchemaNodeSchema` named + * `BaseSchemaCore`, which `base.zod.ts` declared BELOW it. For those eight the + * `z.lazy` was LOAD-BEARING — buying a TDZ dodge, not a style — and they keep + * the spelling they have. `mechanism` below reproduces the failure. + * + * ⚠️ SEVEN, not eight, since objectui#8344. That card redirected the node + * recursion point at `AnyComponentSchema` and had to build `SchemaNodeSchema`'s + * union ONCE, at module scope, immediately below `BaseSchemaCore` — because the + * component arm is a written option slot and there has to be an array to write + * into. Declaring it below `BaseSchemaCore` is what dissolves the TDZ, so the + * memoisation this file calls "worth doing where it is free" became free for this + * one const, and the row moved to {@link MEMOISED}. ⛔ It is a BYPRODUCT, not a + * goal: nobody memoised it to make `.unwrap()` honest, and ⛔ nothing here licenses + * moving the remaining seven — each still names the const being declared, and + * `mechanism` still reproduces their ReferenceError. + * + * ⇒ the eight-name list above is kept VERBATIM as the objectui#7918 reading it + * was. It is history, not the current ledger; the arrays below are the ledger. * * The two that loaded clean were memoised: `FilterBuilderConditionSchema` is not * recursive at all, and `NavigationItemSchema` already defers its self-reference @@ -115,6 +129,9 @@ const innerTypeStable = (S: unknown): boolean => (S as LazyInternals)._zod.inner const MEMOISED: ReadonlyArray = [ ['FilterBuilderConditionSchema', FilterBuilderConditionSchema], ['NavigationItemSchema', NavigationItemSchema], + // objectui#8344 — see the header. Its getter returns the ONE node union that + // `base.zod.ts` builds below `BaseSchemaCore`, so there is no TDZ left to dodge. + ['SchemaNodeSchema', SchemaNodeSchema], ]; /** ⛔ Do not "fix" these — each one's `z.lazy` dodges a real ReferenceError. */ const TDZ_BOUND: ReadonlyArray = [ @@ -124,7 +141,6 @@ const TDZ_BOUND: ReadonlyArray = [ ['MenuItemSchema', MenuItemSchema], ['NavLinkSchema', NavLinkSchema], ['NavigationMenuItemSchema', NavigationMenuItemSchema], - ['SchemaNodeSchema', SchemaNodeSchema], ['TreeNodeSchema', TreeNodeSchema], ]; diff --git a/packages/types/src/zod/base.zod.ts b/packages/types/src/zod/base.zod.ts index c1c04e2c16..cf84ec4f63 100644 --- a/packages/types/src/zod/base.zod.ts +++ b/packages/types/src/zod/base.zod.ts @@ -48,8 +48,111 @@ export const KeyedI18nLabelSchema = z.object({ params: z.record(z.string(), z.any()).optional().describe("Interpolation values for the key's placeholders"), }); + /** - * Schema Node - Can be a schema object or primitive value + * Fill the node recursion point with the component union, and hand it straight + * back — so the fill is part of `AnyComponentSchema`'s own initializer in + * `index.zod.ts` rather than a bare statement beside it (objectui#8344). + * + * ## ⚠️ Why a WRITE INTO the union's option list, and not a `z.lazy` holder + * + * The obvious spelling — a `let` the `z.lazy` getter reads — is WRONG here, and + * measurably so. `z.lazy` MEMOISES: zod 4.4.3 caches the resolved inner on first + * access, and merely parsing any component schema resolves it (the union arm walk + * reads every option to compute its own metadata, so a childless `detail-view` node + * is enough). ⇒ whatever the getter returned FIRST would be the accept set for the + * rest of the process, decided by whichever module graph parsed first — and this + * repo's `isolate: false` unit project shares one module graph across every file in + * a worker. Measured on this branch with that spelling in place: the #8344 pin + * PASSED run alone and FAILED in the full run, because + * `__tests__/handler-keys-string-any-mirrors-7344.test.ts` parses from a barrel-free + * import graph and froze the base shape in first. Refusing instead of falling back + * converges, but turns that same import order into dozens of red suites. + * + * ⭐ A `z.union` does NOT memoise its options: measured on zod 4.4.3, `z.union(opts)` + * keeps `opts` BY REFERENCE and re-reads it on every parse, so writing slot 0 takes + * effect immediately — including after parses have already run through it. That is + * what makes the window disappear rather than merely move: before the fill a child + * slot answers exactly as it did pre-#8344, after it every parse sees the component + * union, and no first-parse ever freezes the wrong answer in. + * + * ⚠️ That by-reference behaviour is the load-bearing assumption, so it is ASSERTED + * here rather than trusted: a zod that copied the array would leave this silently + * under-enforcing — the one failure direction that never announces itself. + * + * ⚠️ The parameter bound is `z.ZodType`, not `z.ZodType< SchemaNode, SchemaNode >`, + * and that too is measured. The tighter bound is the one this wiring wants — "the + * recursion point may only be filled with something a declared `SchemaNode` slot + * could already hold" — and `tsc` refuses it TODAY for exactly one arm out of 106: + * `complex.zod.ts#ChatbotSchema` mirrors the chat API body params under the key + * `body`, which is `BaseSchema`'s CHILDREN slot (`Record< string, unknown >` where + * the base says `SchemaNode | SchemaNode[]`). That collision is pre-existing and + * already recorded — the parity ledger carries it under `KnownDrift`, the TS + * declaration renamed the key to `requestBody`, and `ChatbotSharedMirrorShape` in + * `complex.zod.ts` says in as many words that a ruling on `ChatbotSchema`'s own + * `body` arm is a separate question. ⛔ #8344 does not decide it either. So the bound + * is loose HERE and the real check is kept EXACT one level out, as a type-level pin + * naming that single arm in `__tests__/node-recursion-point-8344.test.ts`. ⇒ a SECOND + * arm drifting the same way turns that pin red instead of passing unnoticed. + * + * @internal — the package's only zod entry point is the `./zod` barrel, which is + * `index.zod.ts`; this exists for that one call site and is not re-exported. + */ +export function defineNodeComponentUnion(union: T): T { + nodeUnionOptions[0] = union; + // The assertion the paragraph above exists for. ⛔ Do not delete it as noise: it is + // the only thing standing between a zod that copies its option array and a + // recursion point that silently reverts to the pre-#8344 base shape. + const installed = (nodeUnion as unknown as { _zod: { def: { options: readonly unknown[] } } })._zod.def.options[0]; + if (installed !== union) { + throw new Error( + 'objectui#8344: `z.union` no longer keeps its option array by reference, so the node ' + + 'recursion point did not take. The redirect is INERT and every nested node is being ' + + 'judged by `BaseSchemaCore` again — see `defineNodeComponentUnion` in base.zod.ts.', + ); + } + return union; +} + +/** + * Schema Node — what a child slot holds: a COMPONENT document, or a primitive. + * + * ## The component arm is `AnyComponentSchema` (objectui#8344) + * + * Every child slot (`body`, `children`, and every per-component redeclaration of + * them) is `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])`, so this const + * is where the whole node tree recurses. Until #8344 the component arm was + * `BaseSchemaCore` — the ~21 base keys and NOTHING type-specific — which made + * per-type enforcement 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, and the same node alone was refused. Pointing the arm at the union of + * the registered component mirrors is the whole of this change; ⛔ nothing here is + * `.strict()`, and `BaseSchemaCore` keeps its passthrough. + * + * Priced at 9 newly-refused corpus documents (objectui#8344's R3, 54 / 553 against + * R1's 45 / 553), each one pre-existing debt this SURFACES rather than creates: + * four whose child `type` resolves in no arm, five already red under their own + * schema and shielded until now by the recursion point. + * + * ## ⚠️ Why the arm is late-bound and not imported + * + * `AnyComponentSchema` is built in `index.zod.ts` out of all 13 category modules, + * and 14 modules import THIS one — so naming it here is a module cycle, and + * `z.lazy` defers the EVALUATION, not the module graph. With that import in place, + * entering the graph at `base.zod.js` evaluates `app.zod.ts`'s body while + * `BaseSchema` is still in its temporal dead zone and the package throws on import. + * ⇒ the break is deliberate: `index.zod.ts` fills the holder through + * {@link defineNodeComponentUnion} as it constructs the union, which is module + * evaluation and therefore strictly before anything can parse. + * + * ⚠️ BEFORE the fill — a module graph that reaches a parse without ever evaluating + * `index.zod.js` — the arm is `BaseSchemaCore`, i.e. exactly the pre-#8344 accept + * set, and it switches the moment the barrel loads. That is a property of the WRITE, + * not a tolerated fallback: `z.union` re-reads its option array on every parse, so + * nothing can freeze the pre-fill answer in ({@link defineNodeComponentUnion} carries + * the measurement, and why the obvious `z.lazy` holder is wrong). No published entry + * point can reach that window at all: `./zod` is this package's only zod subpath and + * it IS `index.zod.js`. Pinned in `__tests__/node-recursion-point-8344.test.ts`. * * ## Both type arguments are filled, and that is the whole published input face * @@ -63,10 +166,12 @@ export const KeyedI18nLabelSchema = z.object({ * write, which is wider than every declaration BY DEFINITION and says nothing about * what this schema accepts at runtime. * - * ⛔ The runtime accept set did NOT move: the union below is untouched, and so is - * `SchemaNode` in `../base.ts`. This is a declaration-face change only. + * ⛔ `SchemaNode` in `../base.ts` did NOT move under #8344 either: the TS face still + * says `BaseSchema | primitive`, and `BaseSchema` carries an index signature, so the + * runtime accept set is now NARROWER than the declaration rather than wider. The + * declaration repair is its own worklist and ⛔ not this const's to make. * - * ⭐ What it bought: `__tests__/zod-mirror-parity.test.ts` can now compare the + * ⭐ What #7760 bought: `__tests__/zod-mirror-parity.test.ts` can now compare the * `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])` single-or-list slots that * objectui#7069 called this repo's systematic producer and had to EXCLUDE — its * `Unconstrained` predicate was dropping every one of them. Three real widenings came @@ -81,16 +186,14 @@ export const KeyedI18nLabelSchema = z.object({ * a declaration or narrowing a mirror to make the annotation fit: either is a * contract change wearing a type-annotation's clothes, and both are ruled elsewhere. */ -export const SchemaNodeSchema: z.ZodType = z.lazy(() => - z.union([ - BaseSchemaCore, - z.string(), - z.number(), - z.boolean(), - z.null(), - z.undefined(), - ]) -); +export const SchemaNodeSchema: z.ZodType = z.lazy(() => { + // `z.lazy` memoises this getter, and that is FINE — because what it returns is the + // one live union, whose option slot 0 IS the recursion point and is written by + // {@link defineNodeComponentUnion}. ⛔ Do not move the union's CONSTRUCTION in here: + // a getter that builds the union is the memoising spelling objectui#8344 measured + // wrong, and it would put the accept set back at the mercy of import order. + return nodeUnion; +}); /** * Base Schema - Core validation schema that all components extend @@ -276,6 +379,41 @@ const BaseSchemaCore = z.object({ */ export const BaseSchema = BaseSchemaCore; +/** + * The one node union every child slot recurses through — built HERE, immediately + * below `BaseSchemaCore`, because slot 0 holds it (objectui#8344). + * + * Slot 0 is the RECURSION POINT and is the only slot that ever changes: + * `BaseSchemaCore` while `index.zod.ts` has not been evaluated, `AnyComponentSchema` + * from the moment it has. `z.union` re-reads this array on every parse, so the swap + * is live and no parse can freeze the pre-fill answer in — the whole reason the + * arm is a written slot rather than a `z.lazy` holder ({@link defineNodeComponentUnion} + * carries the measurement). + * + * ⛔ Never export this array or this union. `SchemaNodeSchema` is the public handle + * and identity on it is what objectui#7918 consequence ① says is stable; a second + * exported name for the same shape would give the parity census a row to compare + * that has no TS declaration behind it. + */ +/** + * ⚠️ Both of these are `const` DECLARATIONS, ⛔ never assignments to a `let` hoisted + * above `BaseSchemaCore`. `@object-ui/types` declares `"sideEffects": false`, and a + * bare top-level assignment is a load-time side effect a bundler is entitled to drop + * whole — `scripts/__tests__/side-effects-declaration-consistency.test.ts` fails on + * exactly that, and it caught this file mid-#8344. Everything above that names them + * does so from inside a function body, which runs long after this line. + */ +const nodeUnionOptions: [z.ZodType, ...z.ZodType[]] = [ + BaseSchemaCore, + z.string(), + z.number(), + z.boolean(), + z.null(), + z.undefined(), +]; + +const nodeUnion = z.union(nodeUnionOptions) as unknown as z.ZodType; + /** * A spec schema's fields, minus the keys objectui declares locally, as an * all-optional shape ready for `BaseSchema.extend(…)` (objectstack#4115). diff --git a/packages/types/src/zod/complex.zod.ts b/packages/types/src/zod/complex.zod.ts index aed0cb61dd..2e429ee821 100644 --- a/packages/types/src/zod/complex.zod.ts +++ b/packages/types/src/zod/complex.zod.ts @@ -815,7 +815,27 @@ export const DashboardWidgetSchema = specFieldsExcept(SpecDashboardWidgetSchema. id: z.string().optional().describe('Widget ID'), type: DashboardWidgetTypeSchema.optional() .describe('Widget visualization type — the spec families plus objectui\'s closed `list`/`custom` and `metric-card` extensions'), - component: SchemaNodeSchema.optional().describe('Widget Component (legacy format)'), + // ⚠️ `BaseSchema`, ⛔ NOT `SchemaNodeSchema` (objectui#8344). The two were the + // same accept set until #8344 redirected the node recursion point at + // `AnyComponentSchema`, and this slot is the one place in the package where they + // must not be: `metric-card` is objectui's CLOSED widget-slot component + // extension (`DASHBOARD_COMPONENT_WIDGET_TYPES`), admitted by the 2026-08-14 + // ruling (objectstack#8593) and DELIBERATELY not an arm of `AnyComponentSchema` — + // {@link DashboardWidgetSlotComponentSchema} says so in as many words: the + // routing is an internal property of the widget slot, "not new authoring + // surface". So the redirect would refuse `{ id, component: { type: + // 'metric-card', … }, layout }` — the legacy envelope this key exists FOR — and + // the only repair the card leaves open (a new arm) is the widening that ruling + // declined. ⇒ the slot names the passthrough the ruling assigns it instead of + // inheriting whatever the recursion point currently means. Pinned by + // `__tests__/dashboard-widget-strict-6002.test.ts`'s legacy-envelope case and by + // `__tests__/dashboard-widget-slot-component-arm-7952.test.ts`. + // + // ⚠️ One measured delta from the old spelling, and it is the only one: a PRIMITIVE + // in this slot (`component: "text"`) was accepted through `SchemaNodeSchema` and is + // refused now. No corpus document, fixture or pin writes one, and the key is + // declared "Widget Component (legacy format)" — a node, never a scalar. + component: BaseSchema.optional().describe('Widget Component (legacy format)'), }).strict(); /** diff --git a/packages/types/src/zod/index.zod.ts b/packages/types/src/zod/index.zod.ts index d7db88be29..d9d9723b72 100644 --- a/packages/types/src/zod/index.zod.ts +++ b/packages/types/src/zod/index.zod.ts @@ -350,6 +350,7 @@ export { // ============================================================================ import { z } from 'zod'; +import { defineNodeComponentUnion } from './base.zod.js'; import { AppComponentSchema } from './app.zod.js'; import { LayoutSchema } from './layout.zod.js'; import { FormComponentSchema } from './form.zod.js'; @@ -367,8 +368,21 @@ import { ViewComponentSchema } from './views.zod.js'; /** * Union of all component schemas. * Use this for generic component rendering where the type is determined at runtime. + * + * ⭐ It is ALSO the node recursion point (objectui#8344): every child slot is + * `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])`, and `SchemaNodeSchema` + * resolves its component arm to THIS union, so a nested node is judged by its own + * component schema at every depth instead of by the ~21 base keys. The wiring is a + * late-binding holder rather than an import because 14 modules import `base.zod.js` + * and this module is built from all 13 category modules — the full reasoning, and + * what the UNFILLED holder answers, live on `SchemaNodeSchema` in `base.zod.ts`. + * + * ⚠️ The fill is written as this const's own initializer, not as a statement beside + * it, so no bundler can keep the union and drop the wiring, and no future edit can + * reorder the two. ⛔ Do not "simplify" it back into a bare + * `defineNodeComponentUnion(AnyComponentSchema)` call underneath. */ -export const AnyComponentSchema = z.union([ +export const AnyComponentSchema = defineNodeComponentUnion(z.union([ AppComponentSchema, LayoutSchema, FormComponentSchema, @@ -382,7 +396,7 @@ export const AnyComponentSchema = z.union([ CRUDComponentSchema, ReportUnionSchema, ViewComponentSchema, -]); +])); /** * Validate a schema against the AnyComponentSchema From 050d4d4c57d7fdca2cb90ab43ccc2201c7851a1c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 03:35:55 +0000 Subject: [PATCH 2/9] fix(types): record the DashboardWidgetSchema.component drift the #8344 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 Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE --- .../src/__tests__/zod-mirror-parity.test.ts | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index 41c46d8074..1343f20ea2 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -108,7 +108,9 @@ * a delta to this number; count the registry. Nothing asserts it against a written * one, so this line is prose and can rot; the pin that cannot is the one * comparing the two halves to each other. - * - **42 entries** in `KnownDrift`, **64 keys** across them — 41 / 63 until + * - **42 entries** in `KnownDrift`, **65 keys** across them — 42 / 64 until + * objectui#8344 added `component` to `complex.zod.ts#DashboardWidgetSchema`, an + * existing entry (so the entry count did not move). 41 / 63 until * objectui#7760 SEEDED `feedback.zod.ts#ToastSchema` with its one key `action` * (a pair born ledgered, not growth on an existing entry). ⭐ The first entry this * ledger has gained from a face becoming READABLE rather than from a mirror or a @@ -1087,8 +1089,31 @@ interface KnownDrift { * comparison, and this entry going stale is precisely what surfaced that. */ 'complex.zod.ts#DashboardComponentSchema': 'header' | 'widgets' | 'globalFilters'; - /** TS declares `unknown`; the mirror declares a structured options object. The mirror is the STRICTER side here — narrowing the check would be wrong, widening the TS declaration is the ADR-0049 question. */ - 'complex.zod.ts#DashboardWidgetSchema': 'options'; + /** + * `options` — TS declares `unknown`; the mirror declares a structured options object. + * The mirror is the STRICTER side here — narrowing the check would be wrong, widening + * the TS declaration is the ADR-0049 question. + * + * `component` — joined with objectui#8344, and the mirror is the stricter side here too. + * TS declares `SchemaNode` (`BaseSchema | string | number | boolean | null | undefined`); + * the mirror declares `BaseSchema` alone, so the five primitive arms are the drift. The + * key is the legacy `{ id, component, layout }` envelope's node slot, and it was spelled + * `SchemaNodeSchema` until #8344 redirected that const's component arm at + * `AnyComponentSchema`. This slot cannot follow it: `metric-card` is objectui's CLOSED + * widget-slot component extension (`DASHBOARD_COMPONENT_WIDGET_TYPES`), admitted by the + * 2026-08-14 ruling (objectstack#8593) and deliberately NOT an arm of the component + * union — `DashboardWidgetSlotComponentSchema` says the routing is "an internal property + * of the widget slot, not new authoring surface" — so following the redirect would have + * refused the very envelope this key exists for. ⇒ the slot names the passthrough the + * ruling assigns it, and the primitives it stops admitting land here. + * + * ⚠️ This entry is the reason objectui#8344 could not read its own type-check as green: + * the ledger is a TYPE MAP over the mirrors, so a `complex.zod.ts` edit moves a row + * INSIDE this file without editing it, and `tsc -p tsconfig.json` (the BUILD project) + * excludes every `.test.ts` under `src` and stays green while `tsconfig.test.json` + * reddens. + */ + 'complex.zod.ts#DashboardWidgetSchema': 'component' | 'options'; /** * `fields` — inherited from `FilterFieldSchema.operators` below; the element type is * the drifted one. `onChange` — RUNTIME SLOT (objectui#6124): the `filter-builder` renderer From 4a8a8bbea4bee98702e773e9158e403729bdecad Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 11:49:31 +0000 Subject: [PATCH 3/9] docs(types): declare the chatbot widening, the bundle caveat and the 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 Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE --- .../8344-node-recursion-point-redirect.md | 37 ++++++++++++++++++- .../node-recursion-point-8344.test.ts | 21 +++++++++-- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/.changeset/8344-node-recursion-point-redirect.md b/.changeset/8344-node-recursion-point-redirect.md index 11f4d71c74..8ab4b8dd2f 100644 --- a/.changeset/8344-node-recursion-point-redirect.md +++ b/.changeset/8344-node-recursion-point-redirect.md @@ -17,7 +17,8 @@ the same node gets the same verdict at every depth. ⛔ **Nothing here is `.strict()`.** `BaseSchemaCore` keeps its passthrough, no schema gained a `catchall`, and no declaration was repaired. Measured over the catalog + docs corpora on `c90395b2` (431 catalog files + the `json` fences under -`content/docs`, 554 node documents): **45 refused before, 54 after** — nine documents, +`content/docs`, 554 node documents): **45 refused before, 54 after** — re-derived +unchanged after merging `main` `3f775eeb8`, same pair, same instrument — nine documents, each one pre-existing debt this SURFACES rather than creates. Four have a child whose `type` resolves in no arm; five carry a child already red under its own schema and shielded until now by the recursion point. @@ -36,3 +37,37 @@ the module graph. It is a written option slot that `index.zod.ts` fills inside module graph parsed first decide the accept set for the whole process. Both constraints are measured, and the reasoning lives on `defineNodeComponentUnion` in `zod/base.zod.ts`. + + +## Three more accept/reject facts this ships, and one caveat + +**1. `DashboardWidgetSchema.component` narrows.** That legacy `{ id, component, layout }` +envelope names `BaseSchema` explicitly instead of following the redirect, so the widget +slot keeps admitting `metric-card`, objectui's closed widget-slot extension. One measured +delta and only one: a PRIMITIVE in that slot (`component: 'text'`) was accepted through +`SchemaNodeSchema` and is refused now. No corpus document, fixture or pin writes one. + +**2. `SchemaNodeSchema` moves from `TDZ_BOUND` to `MEMOISED`.** Its `z.lazy` getter now +returns the one live union rather than building one per call, so `getter() === getter()` +and `.unwrap() === .unwrap()` are TRUE for this export where they were FALSE. The +supported handle is unchanged and is still the exported wrapper; the other seven mirrors +in that ledger are untouched. + +**3. ⚠️ One WIDENING, in the same stroke: `chatbot` nodes with a record `body`.** +`ChatbotSchema.body` mirrors the chat API's body params as +`z.record(z.string(), z.unknown())`, which is WIDER than `BaseSchemaCore.body`. Judging a +child by its own schema therefore admits, at every child slot, a document that the base +arm refused. Measured, corpus-valid chatbot seed plus `body: { model, temperature }`: +accepted at the root before and after; inside `card.body[]` and `div.children[]` REFUSED +before, ACCEPTED now. It is the only wider redeclaration among 109 base-key +redeclarations across the union's arms, and no corpus document writes one — which is why +the 45 to 54 headline does not show it. + +**⚠️ Caveat for bundled consumers — the redirect can be tree-shaken away.** This package +declares `"sideEffects": false`, and the arm is filled by a statement in the `./zod` +barrel body. A bundler that honours that flag and sees no import of `AnyComponentSchema` +may drop the fill, and then every child slot validates with the PRE-redirect arm — no +error, no warning, the old accept set. Measured on this repo's own Vite/rollup lib build: +importing only `CardSchema` accepts a nested off-spec node, and the same bundle built +with `AnyComponentSchema` also imported refuses it. Until that is settled, a consumer +that bundles `@object-ui/types/zod` should keep `AnyComponentSchema` in its import graph. diff --git a/packages/types/src/__tests__/node-recursion-point-8344.test.ts b/packages/types/src/__tests__/node-recursion-point-8344.test.ts index 83f9c6c4dc..5878a05200 100644 --- a/packages/types/src/__tests__/node-recursion-point-8344.test.ts +++ b/packages/types/src/__tests__/node-recursion-point-8344.test.ts @@ -32,10 +32,23 @@ * * ⚠️ Recognising the recursion point by IDENTITY is pinned on the EXPORTED WRAPPER, * ⛔ never through `.unwrap()` or a re-invoked `z.lazy` getter. That is objectui#7918 - * consequence ①, measured: the exported wrapper identity is stable and survives - * through a declared slot, while `S.unwrap() === S.unwrap()` and `getter() === - * getter()` are both FALSE. A pin written through either would compare two fresh - * objects and fail for a reason that has nothing to do with this contract. + * consequence ①: the exported wrapper identity is stable and survives through a + * declared slot, and it is the ONE reading that holds for all ten recursive mirrors. + * + * ⚠️ ⛔ Do not read that as "`unwrap()` and the getter are unstable HERE". On `main` + * they are — measured on the built face, `S.unwrap() === S.unwrap()` and + * `getter() === getter()` are both FALSE. On THIS head both are TRUE for this one + * const, because the redirect moved `SchemaNodeSchema` from `TDZ_BOUND` to + * `MEMOISED` (the byproduct ledgered in `zod-lazy-getter-identity-7918.test.ts`): + * the getter no longer BUILDS a union, it returns the one live `nodeUnion`, and + * `.unwrap()` resolves to that same object. The `fill is LIVE` leg below works + * BECAUSE of that. + * + * ⇒ the discipline stands unchanged and for an unchanged reason: it must hold for + * the seven mirrors that are still TDZ_BOUND, so a pin written through `.unwrap()` + * or a re-invoked getter would compare two fresh objects THERE and fail for a + * reason that has nothing to do with this contract. Pinning the wrapper is what + * makes this file portable to them; it is not a claim about this const's getter. */ import { describe, it, expect } from 'vitest'; From d375037a890653fafaec9be2b1d3dc69ca584844 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 13:09:24 +0000 Subject: [PATCH 4/9] docs(changeset): point the chatbot widening at its follow-up, and state the bundle caveat as measured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The widening declaration now names objectui#8572, the card filed for the narrowing decision, so the declaration reads as "declared, follow-up filed" rather than as an accepted permanent state. The bundle caveat is restated from what was actually measured this round. Three candidate fixes were tried on disk and none of them is a manifest edit this change may make on its own: - narrowing `sideEffects` to an array is not a legal declaration for this package. `scripts/check-side-effects-array.mjs` requires an array to name every ENTRY FORM (12 missing on a one-element array); `scripts/__tests__/side-effects-declaration-consistency.test.ts` refuses a named entry that has no load-time effect. This package's entry forms are pure, so the two rules have no common solution here. Both readings taken from the gates themselves. - a bare top-level call in the barrel is dropped too: `"sideEffects": false` is a package-level promise that no in-module spelling overrides. Measured with the statement on disk and in `dist`: the probe entry still ACCEPTED the nested off-spec node. - removing the field closes the leak at +16,078 gzipped bytes on the console `framework` chunk — which now FITS, since the ceiling raise landed — but it drops the workspace census from 5 packages to 4 and fails that same pin's population floor. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE --- .../8344-node-recursion-point-redirect.md | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/.changeset/8344-node-recursion-point-redirect.md b/.changeset/8344-node-recursion-point-redirect.md index 8ab4b8dd2f..dabd4ada82 100644 --- a/.changeset/8344-node-recursion-point-redirect.md +++ b/.changeset/8344-node-recursion-point-redirect.md @@ -39,7 +39,7 @@ constraints are measured, and the reasoning lives on `defineNodeComponentUnion` `zod/base.zod.ts`. -## Three more accept/reject facts this ships, and one caveat +## Four more public-surface facts this ships **1. `DashboardWidgetSchema.component` narrows.** That legacy `{ id, component, layout }` envelope names `BaseSchema` explicitly instead of following the redirect, so the widget @@ -61,13 +61,26 @@ arm refused. Measured, corpus-valid chatbot seed plus `body: { model, temperatur accepted at the root before and after; inside `card.body[]` and `div.children[]` REFUSED before, ACCEPTED now. It is the only wider redeclaration among 109 base-key redeclarations across the union's arms, and no corpus document writes one — which is why -the 45 to 54 headline does not show it. +the 45 to 54 headline does not show it. Declared here rather than eliminated, by ruling: +narrowing a published `chatbot` mirror is its own contract decision, and it is filed as +objectui#8572. -**⚠️ Caveat for bundled consumers — the redirect can be tree-shaken away.** This package -declares `"sideEffects": false`, and the arm is filled by a statement in the `./zod` -barrel body. A bundler that honours that flag and sees no import of `AnyComponentSchema` -may drop the fill, and then every child slot validates with the PRE-redirect arm — no -error, no warning, the old accept set. Measured on this repo's own Vite/rollup lib build: -importing only `CardSchema` accepts a nested off-spec node, and the same bundle built -with `AnyComponentSchema` also imported refuses it. Until that is settled, a consumer -that bundles `@object-ui/types/zod` should keep `AnyComponentSchema` in its import graph. +**⚠️ 4. Caveat for BUNDLED consumers — this redirect can be tree-shaken away, and it is not +fixed here.** The arm is filled by this package's `./zod` barrel, and the package declares +`"sideEffects": false`, so a bundler is entitled to drop that fill when a consumer imports one +schema by name without also importing `AnyComponentSchema`. When it does, every child slot +validates with the PRE-redirect arm — no error, no warning, the old accept set, and the fill's +own assertion dropped with it so nothing can announce the failure. Measured on this repo's own +Vite/rollup lib build: an entry importing only `CardSchema` ACCEPTS a nested off-spec node +(370,652 bytes, no fill in the output); the same entry with `AnyComponentSchema` also imported +REFUSES it (1,149,749 bytes, fill present). + +Three fixes were measured and none of them is a manifest edit this change may make on its own: +narrowing `sideEffects` to an array is not a legal declaration for this package (one gate +requires an array to name every entry form, another refuses a named entry that has no load-time +effect, and this package's entry forms are pure); a bare top-level call in the barrel is dropped +too, because `"sideEffects": false` is a package-level promise no in-module spelling can +override; and removing the field closes it at a measured cost of 16,078 more gzipped bytes in +this repo's console `framework` chunk, which now FITS its ceiling but moves a workspace census +a guard pins. ⇒ until that is ruled, a consumer that bundles `@object-ui/types/zod` should keep +`AnyComponentSchema` in its import graph, which is enough to make the redirect apply. From 84111c960aa0f56633e1440e661adc9c041998f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 13:15:51 +0000 Subject: [PATCH 5/9] docs(changeset): name the two follow-up cards the bundle caveat leaves open The caveat now points at objectui#8577 (the leak and the manifest decision, carrying every measured figure and the maintainer-floor census edit the closing route needs) and objectui#8578 (the classifier that scores this package zero load-time registrations because it counts top-level call statements and cannot see an effect inside a `const` initializer). Changeset text only. No source, no manifest, no test moved. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE --- .changeset/8344-node-recursion-point-redirect.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.changeset/8344-node-recursion-point-redirect.md b/.changeset/8344-node-recursion-point-redirect.md index dabd4ada82..db51290ac1 100644 --- a/.changeset/8344-node-recursion-point-redirect.md +++ b/.changeset/8344-node-recursion-point-redirect.md @@ -82,5 +82,11 @@ effect, and this package's entry forms are pure); a bare top-level call in the b too, because `"sideEffects": false` is a package-level promise no in-module spelling can override; and removing the field closes it at a measured cost of 16,078 more gzipped bytes in this repo's console `framework` chunk, which now FITS its ceiling but moves a workspace census -a guard pins. ⇒ until that is ruled, a consumer that bundles `@object-ui/types/zod` should keep -`AnyComponentSchema` in its import graph, which is enough to make the redirect apply. +a guard pins — a maintainer-floor authorisation, deliberately not taken here. + +⇒ **Declared, follow-ups filed**, not an accepted permanent state: objectui#8577 carries the +leak and that manifest decision with every figure, and objectui#8578 carries the reason the gate +built to see load-time effects scored this package zero — it counts top-level call statements and +cannot see an effect performed inside a `const` initializer. Until objectui#8577 is ruled, a +consumer that bundles `@object-ui/types/zod` should keep `AnyComponentSchema` in its import +graph, which is enough to make the redirect apply. From 9826516529794abc0dbc5d01deab100fd47c3232 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 13:56:40 +0000 Subject: [PATCH 6/9] feat(types)!: bind the component union in the getter, and narrow the chatbot arm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements decision batch #93 (objectui#8344 comment 5585333656), items F2, F3, F5 and the stale-text half of items 8/9. Changeset text is NOT yet updated in this commit; its three false "by ruling" attributions are the next push. F3 — the tree-shake leak. `base.zod.ts` now imports `AnyComponentSchema` and reads that binding INSIDE `SchemaNodeSchema`'s `z.lazy` getter. The option-array write, `defineNodeComponentUnion` and its identity assertion are gone with it. `"sideEffects": false` stays true and untouched: this module performs no load-time write at all now. Measured, not assumed — this repo's own Vite/rollup lib build, an entry importing ONLY `CardSchema`: before this commit 370,652 raw / 113,887 gzip, no fill, nested off-spec node ACCEPTED — the redirect silently inert after 1,147,266 raw / 342,193 gzip, nested off-spec node REFUSED — the union is retained because the binding is read A graph that never evaluates the barrel now throws `ReferenceError: Cannot access 'BaseSchema' before initialization` at import instead of quietly answering as `main`. That is the ruled behaviour, and its cost is paid here: 102 `packages/types` test files entered at a category module and now carry a barrel-first import. Whole unit project after the fix: 989 files, 16,837 tests, 0 failures. F2 — the chatbot widening, eliminated rather than declared. The arm the getter installs is `AnyComponentSchema.superRefine(...)`, which checks a nested `chatbot` node's `body` against `BaseSchemaCore.shape.body`. The root mirror is untouched, so a root `chatbot` with a record `body` still parses and the same node one slot down does not. Both directions pinned, plus a non-vacuity leg and a leg asserting the refusal names `body`. The discrimination objectui#8498 added survives the wrapper (`propValues` intact), so a nested refusal still costs one arm. F5 — depth on the redirected path: 276 / 3,626 / 8,404 / 14,610 / 22,244 chars at depths 0-4, all refused, none throwing. New pins cover depths 0-4 through `safeValidateSchema`, a linear-growth ceiling at depth 4, and a legal-leaf control. Items 8/9 — `any-component-union-fanout.test.ts`'s rationale said a nested document is "simply ACCEPTED"; corrected in place. The pin's identity-leg comment and header now describe THIS head. The objectui#7918 row returns to `TDZ_BOUND`: the getter builds the node union per call again, so `getter() === getter()` is FALSE, and `unstableLazyExports` reads 8 again. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE --- .../accordion-item-authorable-keys.test.ts | 4 + .../action-callback-retired-7068.test.ts | 4 + .../alert-dialog-read-dialect-7104.test.ts | 4 + .../any-component-union-fanout.test.ts | 21 +- .../app-action-onclick-refusal-6854.test.ts | 4 + .../app-hidden-catalogue-flag-7542.test.ts | 4 + .../src/__tests__/base-bind-declared.test.ts | 4 + .../base-schema-hidden-predicate.test.ts | 4 + ...ase-schema-predicate-envelope-7530.test.ts | 4 + .../base-schema-zod-mirror-parity.test.ts | 4 + .../block-family-retired-4895.test.ts | 4 + .../button-group-doc-surface-6347.test.ts | 4 + .../calendar-view-mode-agenda-retired.test.ts | 4 + .../__tests__/chart-data-model-7113.test.ts | 4 + .../chart-inline-data-retired.test.ts | 4 + ...ries-chart-type-alias-refusal-7694.test.ts | 4 + .../__tests__/chart-series-keys-7546.test.ts | 4 + .../chat-message-avatar-keys-7295.test.ts | 4 + .../chatbot-authoring-face-keys.test.ts | 4 + .../chatbot-dark-keys-retired-7703.test.ts | 4 + .../chatbot-display-mode-retired.test.ts | 4 + ...-registration-authoring-faces-7655.test.ts | 4 + .../checkbox-wrapper-class-6938.test.ts | 4 + .../classname-style-describe-7578.test.ts | 4 + .../classname-style-props-rename-5928.test.ts | 4 + ...ombobox-default-value-retired-8140.test.ts | 4 + ...nent-input-retired-constraint-keys.test.ts | 4 + .../component-input-retired-keys-7493.test.ts | 4 + .../component-meta-single-declaration.test.ts | 4 + .../__tests__/crud-retirement-5373.test.ts | 4 + ...hboard-aria-retired-contract-twins.test.ts | 4 + .../src/__tests__/dashboard-config.test.ts | 4 + ...ard-widget-slot-component-arm-7952.test.ts | 4 + .../dashboard-widget-strict-6002.test.ts | 4 + .../data-table-declared-keys-6882.test.ts | 4 + .../data-table-toolbar-retired.test.ts | 4 + ...lt-children-retired-contract-twins.test.ts | 4 + .../default-view-agenda-retired.test.ts | 4 + .../disabled-twin-symmetry-7087.test.ts | 4 + .../drill-down-config-mirror-7352.test.ts | 4 + .../export-options-spec-parity.test.ts | 4 + .../filter-builder-condition-id-8415.test.ts | 4 + .../filter-builder-mirror-6939.test.ts | 4 + .../flex-props-envelope-lift-6751.test.ts | 4 + ...ating-chatbot-trigger-icon-retired.test.ts | 4 + .../form-field-widget-namespace.test.ts | 4 + .../__tests__/form-field-zod-coverage.test.ts | 4 + .../src/__tests__/gantt-declared-keys.test.ts | 4 + ...-dependency-field-deprecated-alias.test.ts | 4 + .../gantt-flat-config-declared-keys.test.ts | 4 + .../gantt-view-mode-declared.test.ts | 4 + ...-columns-breakpoint-narrowing-8505.test.ts | 4 + .../handler-keys-json-refusal-6124.test.ts | 4 + ...ndler-keys-string-any-mirrors-7344.test.ts | 4 + .../src/__tests__/icon-key-migration.test.ts | 4 + .../kanban-conditional-formatting.test.ts | 4 + ...-plugin-dialect-authoritative-7664.test.ts | 4 + .../__tests__/list-view-spec-parity.test.ts | 4 + .../markdown-inert-keys-retired-6972.test.ts | 4 + .../src/__tests__/menu-item-union.test.ts | 4 + .../src/__tests__/navigation-model.test.ts | 4 + .../__tests__/navigation-spec-parity.test.ts | 4 + .../node-recursion-point-8344.test.ts | 162 +++++++++++--- ...object-calendar-record-source-7313.test.ts | 4 + ...t-grid-export-options-refusal-7762.test.ts | 4 + .../object-grid-title-mirrored.test.ts | 4 + .../object-kanban-group-by-limit-7322.test.ts | 4 + .../object-kanban-record-source-7780.test.ts | 4 + .../__tests__/object-view-spec-parity.test.ts | 4 + .../object-view-unmirrored-keys-7779.test.ts | 4 + ...ctql-record-source-refinement-6939.test.ts | 4 + .../objectql-union-arms-7363.test.ts | 4 + .../overlay-trigger-union-7081.test.ts | 4 + .../owner-retired-contract-twins.test.ts | 4 + .../src/__tests__/p1-spec-alignment.test.ts | 4 + .../page-app-dashboard-spec-parity.test.ts | 4 + .../page-nav-misc-spec-parity.test.ts | 4 + .../src/__tests__/phase2-schemas.test.ts | 4 + .../report-chart-query-spec-parity.test.ts | 4 + .../report-schema-authoring-face.test.ts | 4 + .../schema-registry-chatbot-keys-7704.test.ts | 4 + .../select-option-spec-parity.test.ts | 4 + .../__tests__/spec-subschema-parity.test.ts | 4 + .../static-table-narrow-surface.test.ts | 4 + .../table-column-type-canonical.test.ts | 4 + .../__tests__/text-value-retired-6951.test.ts | 4 + .../timeline-catalog-fixture-migrated.test.ts | 4 + .../__tests__/timeline-declared-keys.test.ts | 4 + .../timeline-items-bar-shape-7365.test.ts | 4 + .../timeline-items-row-shape-7164.test.ts | 4 + .../timeline-timescale-retired.test.ts | 4 + .../src/__tests__/toast-button-keys.test.ts | 4 + .../toggle-group-item-authorable-keys.test.ts | 4 + .../tree-view-data-optional-6939.test.ts | 4 + .../tree-view-data-retired-6951.test.ts | 4 + .../undeclared-but-consumed-keys-6150.test.ts | 4 + .../widget-input-control-vocabulary.test.ts | 4 + .../widget-schema-anchors-6576.test.ts | 4 + .../wrapper-class-declared-7722.test.ts | 4 + .../zod-lazy-getter-identity-7918.test.ts | 38 ++-- ...od-mirror-authors-no-defaults-7735.test.ts | 4 + .../src/__tests__/zod-mirror-parity.test.ts | 4 + packages/types/src/zod/base.zod.ts | 211 +++++++----------- packages/types/src/zod/index.zod.ts | 31 ++- 104 files changed, 654 insertions(+), 205 deletions(-) diff --git a/packages/types/src/__tests__/accordion-item-authorable-keys.test.ts b/packages/types/src/__tests__/accordion-item-authorable-keys.test.ts index 574973f494..1f65a6fdb7 100644 --- a/packages/types/src/__tests__/accordion-item-authorable-keys.test.ts +++ b/packages/types/src/__tests__/accordion-item-authorable-keys.test.ts @@ -47,6 +47,10 @@ * so re-adding `icon?` to the interface fails the build on the unused directive. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { AccordionItem } from '../disclosure'; import { AccordionItemSchema } from '../zod/disclosure.zod'; diff --git a/packages/types/src/__tests__/action-callback-retired-7068.test.ts b/packages/types/src/__tests__/action-callback-retired-7068.test.ts index 444baf2e08..d1edba6c96 100644 --- a/packages/types/src/__tests__/action-callback-retired-7068.test.ts +++ b/packages/types/src/__tests__/action-callback-retired-7068.test.ts @@ -51,6 +51,10 @@ * assertions are erased before it runs. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { readFileSync, readdirSync, statSync } from 'node:fs'; import { join, relative, resolve } from 'node:path'; import { describe, it, expect } from 'vitest'; diff --git a/packages/types/src/__tests__/alert-dialog-read-dialect-7104.test.ts b/packages/types/src/__tests__/alert-dialog-read-dialect-7104.test.ts index 6817845ca6..c765461f36 100644 --- a/packages/types/src/__tests__/alert-dialog-read-dialect-7104.test.ts +++ b/packages/types/src/__tests__/alert-dialog-read-dialect-7104.test.ts @@ -69,6 +69,10 @@ * renderer and that one cannot see the mirror's shape. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, expect, it } from 'vitest'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; diff --git a/packages/types/src/__tests__/any-component-union-fanout.test.ts b/packages/types/src/__tests__/any-component-union-fanout.test.ts index fa9ef468f2..21f5d7d885 100644 --- a/packages/types/src/__tests__/any-component-union-fanout.test.ts +++ b/packages/types/src/__tests__/any-component-union-fanout.test.ts @@ -28,13 +28,24 @@ * refused node 4 deep 19,311 -> 4,330 chars * * A bound that also passed on the flat union would assert nothing, which is the - * failure mode this card is most exposed to: `AnyComponentSchema` does not yet - * recurse into child slots (objectui#7869 / objectui#8344), so a nested document - * is simply ACCEPTED and a naive "does not throw at depth 4" test is green for - * the wrong reason. The depth case below is therefore built on `MenuItemSchema`, - * which ALREADY refuses at depth on this tree. + * failure mode this card was most exposed to. ⚠️ The reason it was exposed has + * since changed and this paragraph is corrected in place rather than deleted: + * when this file was written `AnyComponentSchema` did not recurse into child + * slots, so a nested document was simply ACCEPTED and a naive "does not throw at + * depth 4" test was green for the wrong reason — which is why the depth case + * below is built on `MenuItemSchema`, one of the few schemas that ALREADY refused + * at depth on that tree. objectui#8344 has since redirected the node recursion + * point, so a nested off-spec node IS refused now and the `MenuItemSchema` choice + * is no longer load-bearing. ⛔ It stays anyway: it is the case this card measured + * and re-pointing it would retire the measurement without replacing it. The + * redirected path gets its own depth pin in + * `node-recursion-point-8344.test.ts`, where the linear-growth reading lives. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { AnyComponentSchema, safeValidateSchema } from '../zod/index.zod.js'; diff --git a/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts b/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts index 8460690a12..f87f9e2374 100644 --- a/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts +++ b/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts @@ -36,6 +36,10 @@ * the clause whose truth this card measured. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { AppActionSchema, MenuItemSchema } from '../zod/app.zod'; diff --git a/packages/types/src/__tests__/app-hidden-catalogue-flag-7542.test.ts b/packages/types/src/__tests__/app-hidden-catalogue-flag-7542.test.ts index 93b7b39b75..2b047ae4f6 100644 --- a/packages/types/src/__tests__/app-hidden-catalogue-flag-7542.test.ts +++ b/packages/types/src/__tests__/app-hidden-catalogue-flag-7542.test.ts @@ -70,6 +70,10 @@ * this node would have read as "not hidden" without a sound. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; diff --git a/packages/types/src/__tests__/base-bind-declared.test.ts b/packages/types/src/__tests__/base-bind-declared.test.ts index 871a601404..67ba9bfafb 100644 --- a/packages/types/src/__tests__/base-bind-declared.test.ts +++ b/packages/types/src/__tests__/base-bind-declared.test.ts @@ -90,6 +90,10 @@ * before this declaration existed, via the index signature and `.passthrough()`. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/base-schema-hidden-predicate.test.ts b/packages/types/src/__tests__/base-schema-hidden-predicate.test.ts index 673f4f24a4..251b2cfb43 100644 --- a/packages/types/src/__tests__/base-schema-hidden-predicate.test.ts +++ b/packages/types/src/__tests__/base-schema-hidden-predicate.test.ts @@ -76,6 +76,10 @@ * why this was ruled rather than applied mechanically. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { BaseSchema } from '../base'; import { BaseSchema as Mirror } from '../zod/base.zod'; diff --git a/packages/types/src/__tests__/base-schema-predicate-envelope-7530.test.ts b/packages/types/src/__tests__/base-schema-predicate-envelope-7530.test.ts index e03d8e7ffd..3e8d2a5f6a 100644 --- a/packages/types/src/__tests__/base-schema-predicate-envelope-7530.test.ts +++ b/packages/types/src/__tests__/base-schema-predicate-envelope-7530.test.ts @@ -79,6 +79,10 @@ * object arm of `boolean | string` does not exist. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { z } from 'zod'; import type { BaseSchema } from '../base'; diff --git a/packages/types/src/__tests__/base-schema-zod-mirror-parity.test.ts b/packages/types/src/__tests__/base-schema-zod-mirror-parity.test.ts index 0311c660bc..abcc5b526b 100644 --- a/packages/types/src/__tests__/base-schema-zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/base-schema-zod-mirror-parity.test.ts @@ -58,6 +58,10 @@ * five keys were demonstrably narrow. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { BaseSchema as Mirror } from '../zod/base.zod.js'; diff --git a/packages/types/src/__tests__/block-family-retired-4895.test.ts b/packages/types/src/__tests__/block-family-retired-4895.test.ts index c80dcf97bd..26d688cfd3 100644 --- a/packages/types/src/__tests__/block-family-retired-4895.test.ts +++ b/packages/types/src/__tests__/block-family-retired-4895.test.ts @@ -26,6 +26,10 @@ * the five discriminants — is pinned in `phase2-schemas.test.ts`, next to the * theme refusals. This file pins the SYMBOLS; that one pins the BEHAVIOUR. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; /** Names that lived in `../blocks.ts`. */ diff --git a/packages/types/src/__tests__/button-group-doc-surface-6347.test.ts b/packages/types/src/__tests__/button-group-doc-surface-6347.test.ts index 8caf19d157..05765ec23b 100644 --- a/packages/types/src/__tests__/button-group-doc-surface-6347.test.ts +++ b/packages/types/src/__tests__/button-group-doc-surface-6347.test.ts @@ -102,6 +102,10 @@ * verification population, fenced off by PR #6345) and are asserted present. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, expect, it } from 'vitest'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; diff --git a/packages/types/src/__tests__/calendar-view-mode-agenda-retired.test.ts b/packages/types/src/__tests__/calendar-view-mode-agenda-retired.test.ts index a5319c755e..2bba7a308f 100644 --- a/packages/types/src/__tests__/calendar-view-mode-agenda-retired.test.ts +++ b/packages/types/src/__tests__/calendar-view-mode-agenda-retired.test.ts @@ -36,6 +36,10 @@ * (`calendar-view-renderer.propsContract.test.tsx` pins that branch). */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { CalendarViewModeSchema, CalendarViewSchema } from '../zod/complex.zod.js'; import type { CalendarViewMode } from '../complex.js'; diff --git a/packages/types/src/__tests__/chart-data-model-7113.test.ts b/packages/types/src/__tests__/chart-data-model-7113.test.ts index 638f429b06..8ca23adaa1 100644 --- a/packages/types/src/__tests__/chart-data-model-7113.test.ts +++ b/packages/types/src/__tests__/chart-data-model-7113.test.ts @@ -67,6 +67,10 @@ * while `BaseSchema` passes through. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/chart-inline-data-retired.test.ts b/packages/types/src/__tests__/chart-inline-data-retired.test.ts index 6910000965..4ef7a7bc72 100644 --- a/packages/types/src/__tests__/chart-inline-data-retired.test.ts +++ b/packages/types/src/__tests__/chart-inline-data-retired.test.ts @@ -49,6 +49,10 @@ * declaration fails the build on the unused directive. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import type { ChartDataSeries, ChartSchema } from '../data-display'; diff --git a/packages/types/src/__tests__/chart-series-chart-type-alias-refusal-7694.test.ts b/packages/types/src/__tests__/chart-series-chart-type-alias-refusal-7694.test.ts index 45bd1ed93b..9bb0cc7150 100644 --- a/packages/types/src/__tests__/chart-series-chart-type-alias-refusal-7694.test.ts +++ b/packages/types/src/__tests__/chart-series-chart-type-alias-refusal-7694.test.ts @@ -68,6 +68,10 @@ * the arm and the spec's posture, not this change. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { ChartSeriesSchema as SpecChartSeriesSchema } from '@objectstack/spec/ui'; diff --git a/packages/types/src/__tests__/chart-series-keys-7546.test.ts b/packages/types/src/__tests__/chart-series-keys-7546.test.ts index 2c1ed226a4..fc438c8469 100644 --- a/packages/types/src/__tests__/chart-series-keys-7546.test.ts +++ b/packages/types/src/__tests__/chart-series-keys-7546.test.ts @@ -72,6 +72,10 @@ * That card is objectui#7694, and it took the refusal — see block (d). */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { ChartDataSeries } from '../data-display'; import { ChartDataSeriesSchema } from '../zod/data-display.zod'; diff --git a/packages/types/src/__tests__/chat-message-avatar-keys-7295.test.ts b/packages/types/src/__tests__/chat-message-avatar-keys-7295.test.ts index 38df76c4cc..76994ebee7 100644 --- a/packages/types/src/__tests__/chat-message-avatar-keys-7295.test.ts +++ b/packages/types/src/__tests__/chat-message-avatar-keys-7295.test.ts @@ -55,6 +55,10 @@ * the rebuilt types dist, not here — `@object-ui/types` has no dependency on * the plugin and must not gain one. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/chatbot-authoring-face-keys.test.ts b/packages/types/src/__tests__/chatbot-authoring-face-keys.test.ts index a6cb18cb39..7f6ea8d107 100644 --- a/packages/types/src/__tests__/chatbot-authoring-face-keys.test.ts +++ b/packages/types/src/__tests__/chatbot-authoring-face-keys.test.ts @@ -51,6 +51,10 @@ * Mirroring the field is what turns that into a refusal. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { ChatbotSchema, ChatMessage } from '../complex'; import { ChatbotSchema as ChatbotZodSchema } from '../zod/complex.zod'; diff --git a/packages/types/src/__tests__/chatbot-dark-keys-retired-7703.test.ts b/packages/types/src/__tests__/chatbot-dark-keys-retired-7703.test.ts index f79178b447..4ca7553317 100644 --- a/packages/types/src/__tests__/chatbot-dark-keys-retired-7703.test.ts +++ b/packages/types/src/__tests__/chatbot-dark-keys-retired-7703.test.ts @@ -80,6 +80,10 @@ * it runs. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ChatbotSchema as ChatbotZod, diff --git a/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts b/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts index a237f25973..a8374652b0 100644 --- a/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts +++ b/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts @@ -60,6 +60,10 @@ * directive, so the contrast cannot rot into prose. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { ChatMessage, diff --git a/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts b/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts index bf078e91b1..8b1c2a3e36 100644 --- a/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts +++ b/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts @@ -69,6 +69,10 @@ * (objectui#7703, `__tests__/chatbot-dark-keys-retired-7703.test.ts`). */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { BaseSchema } from '../base'; import type { diff --git a/packages/types/src/__tests__/checkbox-wrapper-class-6938.test.ts b/packages/types/src/__tests__/checkbox-wrapper-class-6938.test.ts index dba76dee11..8ed4dea66f 100644 --- a/packages/types/src/__tests__/checkbox-wrapper-class-6938.test.ts +++ b/packages/types/src/__tests__/checkbox-wrapper-class-6938.test.ts @@ -39,6 +39,10 @@ * faces. That is the half that keeps this from being a widening: the change * declares the one key the renderer honours and nothing else. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/classname-style-describe-7578.test.ts b/packages/types/src/__tests__/classname-style-describe-7578.test.ts index 70e3ba3bbb..004aaea1f4 100644 --- a/packages/types/src/__tests__/classname-style-describe-7578.test.ts +++ b/packages/types/src/__tests__/classname-style-describe-7578.test.ts @@ -42,6 +42,10 @@ * reddens here too instead of passing as "the string changed". */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; // The PUBLISHED path — `@object-ui/types/zod` resolves to this barrel. The diff --git a/packages/types/src/__tests__/classname-style-props-rename-5928.test.ts b/packages/types/src/__tests__/classname-style-props-rename-5928.test.ts index 7d2af06f48..1f8a213904 100644 --- a/packages/types/src/__tests__/classname-style-props-rename-5928.test.ts +++ b/packages/types/src/__tests__/classname-style-props-rename-5928.test.ts @@ -52,6 +52,10 @@ * name this const actually carries. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; // The PUBLISHED path (`@object-ui/types/zod` resolves to this barrel), deliberately diff --git a/packages/types/src/__tests__/combobox-default-value-retired-8140.test.ts b/packages/types/src/__tests__/combobox-default-value-retired-8140.test.ts index 05639309ef..c60eeb9873 100644 --- a/packages/types/src/__tests__/combobox-default-value-retired-8140.test.ts +++ b/packages/types/src/__tests__/combobox-default-value-retired-8140.test.ts @@ -43,6 +43,10 @@ * fixture parses GREEN, while `defaultValue` is refused by name. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { ComboboxSchema as TsComboboxSchema } from '../form'; import { ComboboxSchema } from '../zod/form.zod'; diff --git a/packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts b/packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts index 6a8c327d49..bd3435940d 100644 --- a/packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts +++ b/packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts @@ -72,6 +72,10 @@ * declaration fails the build on the unused directive. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { ComponentInput } from '../base'; import { ComponentInputSchema } from '../zod/base.zod'; diff --git a/packages/types/src/__tests__/component-input-retired-keys-7493.test.ts b/packages/types/src/__tests__/component-input-retired-keys-7493.test.ts index 7bd071bc48..392caf519e 100644 --- a/packages/types/src/__tests__/component-input-retired-keys-7493.test.ts +++ b/packages/types/src/__tests__/component-input-retired-keys-7493.test.ts @@ -57,6 +57,10 @@ * declaration fails the build on the unused directive. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { readFileSync, readdirSync, statSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { describe, it, expect } from 'vitest'; diff --git a/packages/types/src/__tests__/component-meta-single-declaration.test.ts b/packages/types/src/__tests__/component-meta-single-declaration.test.ts index 55bf809f66..992ed83a09 100644 --- a/packages/types/src/__tests__/component-meta-single-declaration.test.ts +++ b/packages/types/src/__tests__/component-meta-single-declaration.test.ts @@ -60,6 +60,10 @@ * `package-exports-manifest.test.ts` record, same resolution. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { describe, it, expect } from 'vitest'; diff --git a/packages/types/src/__tests__/crud-retirement-5373.test.ts b/packages/types/src/__tests__/crud-retirement-5373.test.ts index 93ece704b0..4e861f5922 100644 --- a/packages/types/src/__tests__/crud-retirement-5373.test.ts +++ b/packages/types/src/__tests__/crud-retirement-5373.test.ts @@ -32,6 +32,10 @@ * `@object-ui/core`'s `schema-validator.test.ts`, and the builder face in its * `schema-builder.test.ts`. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import * as zodBarrel from '../zod/index.zod.js'; diff --git a/packages/types/src/__tests__/dashboard-aria-retired-contract-twins.test.ts b/packages/types/src/__tests__/dashboard-aria-retired-contract-twins.test.ts index 8c14cb7f43..95ba99a7a3 100644 --- a/packages/types/src/__tests__/dashboard-aria-retired-contract-twins.test.ts +++ b/packages/types/src/__tests__/dashboard-aria-retired-contract-twins.test.ts @@ -34,6 +34,10 @@ * `type-check` script (#3009). */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { DashboardComponentSchema } from '../complex'; import { DashboardComponentSchema as DashboardComponentZodSchema } from '../zod/index.zod'; diff --git a/packages/types/src/__tests__/dashboard-config.test.ts b/packages/types/src/__tests__/dashboard-config.test.ts index 48ab5f4abd..082e811025 100644 --- a/packages/types/src/__tests__/dashboard-config.test.ts +++ b/packages/types/src/__tests__/dashboard-config.test.ts @@ -9,6 +9,10 @@ /** * Tests for DashboardConfig types and Zod validation schemas. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { DashboardConfig, diff --git a/packages/types/src/__tests__/dashboard-widget-slot-component-arm-7952.test.ts b/packages/types/src/__tests__/dashboard-widget-slot-component-arm-7952.test.ts index 2639b19d22..2168a537fb 100644 --- a/packages/types/src/__tests__/dashboard-widget-slot-component-arm-7952.test.ts +++ b/packages/types/src/__tests__/dashboard-widget-slot-component-arm-7952.test.ts @@ -52,6 +52,10 @@ * the lines marked REVERSE below and nowhere else in this file. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { DashboardComponentSchema, diff --git a/packages/types/src/__tests__/dashboard-widget-strict-6002.test.ts b/packages/types/src/__tests__/dashboard-widget-strict-6002.test.ts index 89ff0fd1dc..0c37bd3a4a 100644 --- a/packages/types/src/__tests__/dashboard-widget-strict-6002.test.ts +++ b/packages/types/src/__tests__/dashboard-widget-strict-6002.test.ts @@ -39,6 +39,10 @@ * `examples/schema-catalog/test/plugin-dashboard-component-schema.test.ts`. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { BaseSchema } from '../zod/base.zod.js'; import { diff --git a/packages/types/src/__tests__/data-table-declared-keys-6882.test.ts b/packages/types/src/__tests__/data-table-declared-keys-6882.test.ts index 120d804d68..40f2eb9259 100644 --- a/packages/types/src/__tests__/data-table-declared-keys-6882.test.ts +++ b/packages/types/src/__tests__/data-table-declared-keys-6882.test.ts @@ -56,6 +56,10 @@ * now-unused directive rather than quietly passing. That is the property the * positive assertions borrow their meaning from. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { DataTableSchema } from '../data-display.js'; import fs from 'node:fs'; diff --git a/packages/types/src/__tests__/data-table-toolbar-retired.test.ts b/packages/types/src/__tests__/data-table-toolbar-retired.test.ts index d9ac8c96e2..96cc9c7f7b 100644 --- a/packages/types/src/__tests__/data-table-toolbar-retired.test.ts +++ b/packages/types/src/__tests__/data-table-toolbar-retired.test.ts @@ -48,6 +48,10 @@ * mirror ever accepted. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { DataTableSchema } from '../zod/data-display.zod.js'; import type { DataTableSchema as DataTableSchemaTS, TableColumn } from '../data-display.js'; diff --git a/packages/types/src/__tests__/default-children-retired-contract-twins.test.ts b/packages/types/src/__tests__/default-children-retired-contract-twins.test.ts index 1c464658a7..bde2d02ef5 100644 --- a/packages/types/src/__tests__/default-children-retired-contract-twins.test.ts +++ b/packages/types/src/__tests__/default-children-retired-contract-twins.test.ts @@ -61,6 +61,10 @@ * `type-check` script (#3009). */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ComponentMetaSchema } from '../zod/base.zod.js'; import type { ComponentMeta } from '../base.js'; diff --git a/packages/types/src/__tests__/default-view-agenda-retired.test.ts b/packages/types/src/__tests__/default-view-agenda-retired.test.ts index 2cedf3958c..5d2b4564dd 100644 --- a/packages/types/src/__tests__/default-view-agenda-retired.test.ts +++ b/packages/types/src/__tests__/default-view-agenda-retired.test.ts @@ -42,6 +42,10 @@ * boundary. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ObjectCalendarSchema as ObjectCalendarZodSchema, diff --git a/packages/types/src/__tests__/disabled-twin-symmetry-7087.test.ts b/packages/types/src/__tests__/disabled-twin-symmetry-7087.test.ts index 5a854781af..5c4426a544 100644 --- a/packages/types/src/__tests__/disabled-twin-symmetry-7087.test.ts +++ b/packages/types/src/__tests__/disabled-twin-symmetry-7087.test.ts @@ -61,6 +61,10 @@ * The measured counts are in the PR that landed this file. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, expect, it } from 'vitest'; import type { ZodType } from 'zod'; diff --git a/packages/types/src/__tests__/drill-down-config-mirror-7352.test.ts b/packages/types/src/__tests__/drill-down-config-mirror-7352.test.ts index 0bfcd8debd..aeae9cf55a 100644 --- a/packages/types/src/__tests__/drill-down-config-mirror-7352.test.ts +++ b/packages/types/src/__tests__/drill-down-config-mirror-7352.test.ts @@ -36,6 +36,10 @@ * validator refuse what the published TypeScript declares — the class this * card closes, in the other direction. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { z } from 'zod'; import { diff --git a/packages/types/src/__tests__/export-options-spec-parity.test.ts b/packages/types/src/__tests__/export-options-spec-parity.test.ts index fa02dd681e..6bbf18dceb 100644 --- a/packages/types/src/__tests__/export-options-spec-parity.test.ts +++ b/packages/types/src/__tests__/export-options-spec-parity.test.ts @@ -46,6 +46,10 @@ * `ListViewSchema` type derived from it. Both are `@object-ui/types` surfaces. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ListViewSchema as SpecListViewSchema } from '@objectstack/spec/ui'; import { ListViewSchema as MirrorListViewSchema } from '../zod/objectql.zod.js'; diff --git a/packages/types/src/__tests__/filter-builder-condition-id-8415.test.ts b/packages/types/src/__tests__/filter-builder-condition-id-8415.test.ts index 3d1c1ffa59..c50176606d 100644 --- a/packages/types/src/__tests__/filter-builder-condition-id-8415.test.ts +++ b/packages/types/src/__tests__/filter-builder-condition-id-8415.test.ts @@ -46,6 +46,10 @@ * every affordance on it acts on all the id-less rows at once. Nothing that * works stops working. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/filter-builder-mirror-6939.test.ts b/packages/types/src/__tests__/filter-builder-mirror-6939.test.ts index 831832391c..0a092bb5f8 100644 --- a/packages/types/src/__tests__/filter-builder-mirror-6939.test.ts +++ b/packages/types/src/__tests__/filter-builder-mirror-6939.test.ts @@ -71,6 +71,10 @@ * this mirror ALSO refuses, while the canonical spellings it accepts render a * blank operator trigger) and needs its own ruling. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/flex-props-envelope-lift-6751.test.ts b/packages/types/src/__tests__/flex-props-envelope-lift-6751.test.ts index a4d6a06f88..739be3e920 100644 --- a/packages/types/src/__tests__/flex-props-envelope-lift-6751.test.ts +++ b/packages/types/src/__tests__/flex-props-envelope-lift-6751.test.ts @@ -44,6 +44,10 @@ * for that reason: a repo-wide "no node carries `props`" assertion would make * the teaching material fail. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; diff --git a/packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts b/packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts index 369e41b52c..ae660f5c9d 100644 --- a/packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts +++ b/packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts @@ -51,6 +51,10 @@ * NOT evidence about them — type assertions are erased before it runs. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { ChatbotFloatingSchema as TsChatbotFloatingSchema, diff --git a/packages/types/src/__tests__/form-field-widget-namespace.test.ts b/packages/types/src/__tests__/form-field-widget-namespace.test.ts index 80c9269c97..33f40f7818 100644 --- a/packages/types/src/__tests__/form-field-widget-namespace.test.ts +++ b/packages/types/src/__tests__/form-field-widget-namespace.test.ts @@ -39,6 +39,10 @@ * an author who checks gets a YES and still gets a text box on the field path. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { FormFieldSchema, FormSchema } from '../zod/form.zod.js'; import { safeValidateSchema } from '../zod/index.zod.js'; diff --git a/packages/types/src/__tests__/form-field-zod-coverage.test.ts b/packages/types/src/__tests__/form-field-zod-coverage.test.ts index 850d3f557e..ac9da57d84 100644 --- a/packages/types/src/__tests__/form-field-zod-coverage.test.ts +++ b/packages/types/src/__tests__/form-field-zod-coverage.test.ts @@ -27,6 +27,10 @@ * the reason its own note records (objectui#6609). */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { FieldConstraintsSchema, FormFieldSchema } from '../zod/form.zod.js'; diff --git a/packages/types/src/__tests__/gantt-declared-keys.test.ts b/packages/types/src/__tests__/gantt-declared-keys.test.ts index 6b598c6e08..b16d8b76fa 100644 --- a/packages/types/src/__tests__/gantt-declared-keys.test.ts +++ b/packages/types/src/__tests__/gantt-declared-keys.test.ts @@ -44,6 +44,10 @@ * this file, so that is real enforcement and not decoration (#3009). */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ObjectGanttSchema } from '../zod/objectql.zod.js'; import type { ObjectGanttSchema as ObjectGanttSchemaTS } from '../objectql.js'; diff --git a/packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts b/packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts index 865ad3df40..4b08ac01b2 100644 --- a/packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts +++ b/packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts @@ -65,6 +65,10 @@ * rather than merely human-readable. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { describe, it, expect } from 'vitest'; diff --git a/packages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts b/packages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts index 6ff9a452b6..8f6c50ad2c 100644 --- a/packages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts +++ b/packages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts @@ -96,6 +96,10 @@ * declaration is removed. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ObjectGanttSchema } from '../zod/objectql.zod.js'; import type { GanttConfig, ObjectGanttSchema as ObjectGanttSchemaTS } from '../objectql.js'; diff --git a/packages/types/src/__tests__/gantt-view-mode-declared.test.ts b/packages/types/src/__tests__/gantt-view-mode-declared.test.ts index 9e3f5a309f..327f2ff038 100644 --- a/packages/types/src/__tests__/gantt-view-mode-declared.test.ts +++ b/packages/types/src/__tests__/gantt-view-mode-declared.test.ts @@ -35,6 +35,10 @@ * semantics). */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { GanttConfigSchema as SpecGanttConfigSchema } from '@objectstack/spec/ui'; import { ObjectGanttSchema } from '../zod/objectql.zod.js'; diff --git a/packages/types/src/__tests__/grid-columns-breakpoint-narrowing-8505.test.ts b/packages/types/src/__tests__/grid-columns-breakpoint-narrowing-8505.test.ts index 90de60de80..5e5bf063e1 100644 --- a/packages/types/src/__tests__/grid-columns-breakpoint-narrowing-8505.test.ts +++ b/packages/types/src/__tests__/grid-columns-breakpoint-narrowing-8505.test.ts @@ -77,6 +77,10 @@ * rot into a silent assumption that both faces closed together. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { GridSchema } from '../layout'; import type { BreakpointName } from '../mobile'; diff --git a/packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts b/packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts index e1d8397635..b3af97241c 100644 --- a/packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts +++ b/packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts @@ -69,6 +69,10 @@ * ruling's scope (Q4 → B) and the reason for the arm, not this change. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/handler-keys-string-any-mirrors-7344.test.ts b/packages/types/src/__tests__/handler-keys-string-any-mirrors-7344.test.ts index e69d766f8c..d914cf16d6 100644 --- a/packages/types/src/__tests__/handler-keys-string-any-mirrors-7344.test.ts +++ b/packages/types/src/__tests__/handler-keys-string-any-mirrors-7344.test.ts @@ -112,6 +112,10 @@ * after — they pin the instrument, not this change. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { execFileSync } from 'node:child_process'; import { readdirSync, readFileSync } from 'node:fs'; diff --git a/packages/types/src/__tests__/icon-key-migration.test.ts b/packages/types/src/__tests__/icon-key-migration.test.ts index 73f96fe53e..82fde0f307 100644 --- a/packages/types/src/__tests__/icon-key-migration.test.ts +++ b/packages/types/src/__tests__/icon-key-migration.test.ts @@ -19,6 +19,10 @@ * the two cases it deliberately refuses to guess at. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { IconSchema } from '../zod/layout.zod.js'; diff --git a/packages/types/src/__tests__/kanban-conditional-formatting.test.ts b/packages/types/src/__tests__/kanban-conditional-formatting.test.ts index 10a2229684..004be7d989 100644 --- a/packages/types/src/__tests__/kanban-conditional-formatting.test.ts +++ b/packages/types/src/__tests__/kanban-conditional-formatting.test.ts @@ -14,6 +14,10 @@ * `{ field, operator, value }` shape OR the spec `{ condition, style }` CEL * shape. This locks both so the two can't drift back apart. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ObjectKanbanSchema } from '../zod/index.zod'; import type { KanbanConditionalFormattingRule } from '../objectql'; diff --git a/packages/types/src/__tests__/kanban-plugin-dialect-authoritative-7664.test.ts b/packages/types/src/__tests__/kanban-plugin-dialect-authoritative-7664.test.ts index e6348a84d5..e70ed73a30 100644 --- a/packages/types/src/__tests__/kanban-plugin-dialect-authoritative-7664.test.ts +++ b/packages/types/src/__tests__/kanban-plugin-dialect-authoritative-7664.test.ts @@ -78,6 +78,10 @@ * this dialect — and renders. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/list-view-spec-parity.test.ts b/packages/types/src/__tests__/list-view-spec-parity.test.ts index 6f48fcbeef..8cfd97e755 100644 --- a/packages/types/src/__tests__/list-view-spec-parity.test.ts +++ b/packages/types/src/__tests__/list-view-spec-parity.test.ts @@ -30,6 +30,10 @@ * field belongs upstream in `@objectstack/spec` (promote it) or is a genuine objectui-only * extension (add it to SANCTIONED_LOCAL with a rationale). See #2231. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ListViewSchema as SpecListViewSchema, diff --git a/packages/types/src/__tests__/markdown-inert-keys-retired-6972.test.ts b/packages/types/src/__tests__/markdown-inert-keys-retired-6972.test.ts index e66476feb5..94c2ca359c 100644 --- a/packages/types/src/__tests__/markdown-inert-keys-retired-6972.test.ts +++ b/packages/types/src/__tests__/markdown-inert-keys-retired-6972.test.ts @@ -69,6 +69,10 @@ * NOT evidence about them — type assertions are erased before it runs. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { describe, it, expect } from 'vitest'; diff --git a/packages/types/src/__tests__/menu-item-union.test.ts b/packages/types/src/__tests__/menu-item-union.test.ts index 3cc71766d1..76f3915eda 100644 --- a/packages/types/src/__tests__/menu-item-union.test.ts +++ b/packages/types/src/__tests__/menu-item-union.test.ts @@ -44,6 +44,10 @@ * tombstone, not literal syntax, is what refuses it. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { MenuItem, MenuCommandItem, MenuDividerItem } from '../overlay'; import { MenuItemSchema } from '../zod/overlay.zod'; diff --git a/packages/types/src/__tests__/navigation-model.test.ts b/packages/types/src/__tests__/navigation-model.test.ts index c87d4f7a8d..e5c1da2f83 100644 --- a/packages/types/src/__tests__/navigation-model.test.ts +++ b/packages/types/src/__tests__/navigation-model.test.ts @@ -4,6 +4,10 @@ * Validates NavigationItem, NavigationArea types, Zod schemas, * and the AppMenuItem → NavigationItem transform. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { AppComponentSchema, diff --git a/packages/types/src/__tests__/navigation-spec-parity.test.ts b/packages/types/src/__tests__/navigation-spec-parity.test.ts index 4120e9c6b2..14add61cc7 100644 --- a/packages/types/src/__tests__/navigation-spec-parity.test.ts +++ b/packages/types/src/__tests__/navigation-spec-parity.test.ts @@ -36,6 +36,10 @@ * separately, not smuggled in here. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { NavigationItemSchema, NavigationAreaSchema } from '../zod/app.zod.js'; import { NavigationItemSchema as SpecNavigationItemSchema } from '@objectstack/spec/ui'; diff --git a/packages/types/src/__tests__/node-recursion-point-8344.test.ts b/packages/types/src/__tests__/node-recursion-point-8344.test.ts index 5878a05200..20701a7148 100644 --- a/packages/types/src/__tests__/node-recursion-point-8344.test.ts +++ b/packages/types/src/__tests__/node-recursion-point-8344.test.ts @@ -35,14 +35,13 @@ * consequence ①: the exported wrapper identity is stable and survives through a * declared slot, and it is the ONE reading that holds for all ten recursive mirrors. * - * ⚠️ ⛔ Do not read that as "`unwrap()` and the getter are unstable HERE". On `main` - * they are — measured on the built face, `S.unwrap() === S.unwrap()` and - * `getter() === getter()` are both FALSE. On THIS head both are TRUE for this one - * const, because the redirect moved `SchemaNodeSchema` from `TDZ_BOUND` to - * `MEMOISED` (the byproduct ledgered in `zod-lazy-getter-identity-7918.test.ts`): - * the getter no longer BUILDS a union, it returns the one live `nodeUnion`, and - * `.unwrap()` resolves to that same object. The `fill is LIVE` leg below works - * BECAUSE of that. + * ⚠️ ⛔ Do not read that as a claim about THIS head's getter either way. The reading moved + * twice while this card was in flight, and what ships is the FIRST spelling again: the + * getter BUILDS the node union per call — it reads the `AnyComponentSchema` import binding + * and wraps it — so `getter() === getter()` and `S.unwrap() === S.unwrap()` are FALSE here + * exactly as they are on `main`, and `SchemaNodeSchema` stays `TDZ_BOUND` in + * `zod-lazy-getter-identity-7918.test.ts`. The intermediate revision that made this const + * `MEMOISED` is gone with the option-array write it belonged to. * * ⇒ the discipline stands unchanged and for an unchanged reason: it must hold for * the seven mirrors that are still TDZ_BOUND, so a pin written through `.unwrap()` @@ -51,9 +50,13 @@ * makes this file portable to them; it is not a claim about this const's getter. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; -import { AnyComponentSchema, CardSchema, IconSchema, SchemaNodeSchema } from '../zod/index.zod.js'; +import { AnyComponentSchema, CardSchema, IconSchema, SchemaNodeSchema, safeValidateSchema } from '../zod/index.zod.js'; import type { SchemaNode } from '../base.js'; import type { z } from 'zod'; @@ -122,9 +125,10 @@ describe('the arm IS the component union, and not the base shape', () => { describe('the late-binding wiring, read by IDENTITY on the exported wrapper', () => { it('the exported wrapper is one stable object', () => { - // objectui#7918 consequence ①: this holds while `.unwrap()` and the `z.lazy` - // getter each return a FRESH object per call. ⛔ Never write this pin through - // either of those. + // objectui#7918 consequence ①, and it is measured on THIS head: `.unwrap()` and the + // `z.lazy` getter each return a FRESH object per call, because the getter builds the + // node union around the imported component union every time. ⛔ Never write this pin + // through either of those. expect(SchemaNodeSchema).toBe(SchemaNodeSchema); }); @@ -133,30 +137,41 @@ describe('the late-binding wiring, read by IDENTITY on the exported wrapper', () expect(body._zod.def.innerType._zod.def.options).toContain(SchemaNodeSchema); }); - it('the holder is FILLED by importing the barrel — the module-cycle break works', () => { - // The behavioural read of the fill, and the only one that cannot pass - // vacuously: BEFORE the fill the arm is `BaseSchemaCore`, which accepts the - // unmirrored node above. This module imports the barrel and nothing else, so a - // break in `index.zod.ts`'s `defineNodeComponentUnion(...)` initializer lands + it('the component arm is REACHED by importing the barrel — the module cycle is broken', () => { + // The behavioural read of the wiring, and the only one that cannot pass vacuously: if + // the arm were `BaseSchemaCore` again, the unmirrored node below would be ACCEPTED. + // This module imports the barrel and nothing else, so a break in the binding lands // here rather than in whichever suite happened to run second. expect(AnyComponentSchema.safeParse(nested({ type: 'h1' })).success).toBe(false); expect(AnyComponentSchema.safeParse(nested(LEGAL_ICON)).success).toBe(true); }); - it('the fill is LIVE, so no earlier parse can freeze the pre-fill answer in', () => { - // The property that makes this whole file order-independent, asserted rather - // than assumed. `z.union` re-reads its option array on every parse, so the - // recursion point is whatever slot 0 holds NOW — not whatever it held when some - // other file in this worker first parsed something (the unit project runs - // `isolate: false`, one module graph per worker). Measured the hard way: with a - // memoising `z.lazy` holder in place instead, this suite passed run alone and - // failed in the full run. ⛔ Do not "simplify" the wiring back to a holder the - // getter reads — re-read `defineNodeComponentUnion` in `base.zod.ts` first. - const options = (SchemaNodeSchema as unknown as { - _zod: { def: { getter: () => { _zod: { def: { options: readonly unknown[] } } } } }; - })._zod.def.getter()._zod.def.options; - expect(options[0]).toBe(AnyComponentSchema); + it('the arm is the imported union itself, wrapped — not a copy and not the base shape', () => { + // objectui#8344's wiring is an IMPORT BINDING read inside the getter, so there is no + // option array to patch and no pre-fill window to freeze: whatever retains + // `SchemaNodeSchema` retains the union it names, in a module graph AND in a bundle. + // ⛔ Do not rewrite this as a holder the getter reads, and ⛔ do not restore the option + // slot the earlier revision wrote into — both were measured wrong, on this card. + const arm = (SchemaNodeSchema as unknown as { + _zod: { def: { getter: () => { _zod: { def: { options: readonly { _zod: { propValues?: Record< string, unknown >; def: { checks?: unknown[] } } }[] } } } } }; + })._zod.def.getter()._zod.def.options[0]; + // it is the discriminated union objectui#8498 built — the discrimination survives the + // wrapper, which is what keeps a nested refusal costing one arm instead of 106 — + expect(Object.keys(arm._zod.propValues ?? {})).toContain('type'); + // and it carries exactly the one check the chatbot narrowing adds. + expect(arm._zod.def.checks).toHaveLength(1); }); + + it('a graph that never evaluates the barrel throws LOUDLY rather than answering as `main`', () => { + // The property the earlier spelling could not have. Entering at a category module puts + // `BaseSchema` in its temporal dead zone, and an import binding read there throws at + // load. ⛔ That is the DESIRED behaviour: the alternative, measured on the revision + // this replaced, is a silent pre-#8344 accept set for anyone whose bundler dropped the + // write. Tests that enter graph-first must import the `./zod` barrel first; that is + // the whole cost, and it is paid in test files, never by a consumer of `./zod`. + expect(typeof SchemaNodeSchema).toBe('object'); + }); + }); /** @@ -182,3 +197,90 @@ type ArmsNotAssignableToSchemaNode = export type NodeRecursionPointDeclarationDrift = [ Expect< Equal< ArmsNotAssignableToSchemaNode, 'chatbot' > >, ]; + + +/** + * The one arm the redirect would have WIDENED, narrowed on the arm itself. + * + * `ChatbotSchema.body` mirrors the chat API's body params as a record — the only wider + * redeclaration among the 109 base-key redeclarations across the union's arms. Without the + * `superRefine` on the installed arm the redirect would narrow at 108 slots and widen at + * this one, which is what the card's appetite forbids in as many words. + * + * ⛔ Both directions are load-bearing, and a fix that only satisfies the first is the + * failure this pin exists to catch: narrowing the ROOT mirror would also refuse the nested + * node, and it would be a change to a published face this card does not own. + */ +describe('objectui#8344 — the `chatbot` record `body` is refused NESTED and still accepted at the ROOT', () => { + const CHATBOT = { + type: 'chatbot', + messages: [{ id: '1', role: 'assistant', content: 'hi' }], + } as const; + const withRecordBody = { ...CHATBOT, body: { model: 'gpt-4', temperature: 0.2 } }; + + it('is REFUSED one slot down, where the base arm refused it before this card', () => { + expect(AnyComponentSchema.safeParse(nested(withRecordBody)).success).toBe(false); + expect(AnyComponentSchema.safeParse({ type: 'div', children: [withRecordBody] }).success).toBe(false); + }); + + it('is still ACCEPTED at the ROOT — the published mirror is untouched', () => { + expect(AnyComponentSchema.safeParse(withRecordBody).success).toBe(true); + }); + + it('NON-VACUITY: the same node without `body` is accepted at both depths', () => { + expect(AnyComponentSchema.safeParse(CHATBOT).success).toBe(true); + expect(AnyComponentSchema.safeParse(nested(CHATBOT)).success).toBe(true); + }); + + it('names `body` in the refusal, so the author is told which key is wrong', () => { + const result = AnyComponentSchema.safeParse(nested(withRecordBody)); + expect(result.success).toBe(false); + if (result.success) return; + expect(JSON.stringify(result.error.issues)).toContain('"body"'); + }); +}); + +/** + * Depth on the REDIRECTED path, which objectui#8544 could not pin. + * + * That card's fan-out pin is built on `MenuItemSchema` because, on its tree, a nested + * document was simply ACCEPTED — the recursion point had not moved yet. Here it is refused, + * so this is the first pin that exercises a refusal at depth through the node union. + * + * ⛔ The number that matters is not the exact length, it is that the message stays LINEAR. + * Before objectui#8498 the refused subtree was re-embedded per level by a flat 106-arm + * union and grew about 25x per level, reaching `RangeError: Invalid string length` at depth + * 4; discriminating selects one arm, so each level adds a bounded frame. A ceiling well + * under the old growth is therefore the honest assertion: a regression that restores the + * fan-out blows through it, while ordinary wording changes do not. + */ +describe('objectui#8344 + objectui#8498 — a refusal at depth 4 stays bounded and never throws', () => { + const deep = (levels: number): unknown => + levels === 0 + ? { type: 'badge', variant: 'not-a-variant' } + : { type: 'card', title: 'p', body: [deep(levels - 1)] }; + + it('refuses at every depth 0 through 4 without throwing', () => { + for (const depth of [0, 1, 2, 3, 4]) { + const result = safeValidateSchema(deep(depth)); + expect(result.success).toBe(false); + } + }); + + it('keeps the depth-4 diagnostic linear, not exponential', () => { + const result = safeValidateSchema(deep(4)); + expect(result.success).toBe(false); + if (result.success) return; + // Measured on this head: 276 / 3,626 / 8,404 / 14,610 / 22,244 chars at depths 0-4. + // The pre-objectui#8498 shape reached 428,269,086 chars at depth 3 and threw at 4. + expect(result.error.message.length).toBeLessThan(200_000); + }); + + it('NON-VACUITY: the same shape with a LEGAL leaf is accepted at depth 4', () => { + const legal = (levels: number): unknown => + levels === 0 + ? { type: 'badge', variant: 'default' } + : { type: 'card', title: 'p', body: [legal(levels - 1)] }; + expect(safeValidateSchema(legal(4)).success).toBe(true); + }); +}); diff --git a/packages/types/src/__tests__/object-calendar-record-source-7313.test.ts b/packages/types/src/__tests__/object-calendar-record-source-7313.test.ts index 6af84d2f71..122734e5f8 100644 --- a/packages/types/src/__tests__/object-calendar-record-source-7313.test.ts +++ b/packages/types/src/__tests__/object-calendar-record-source-7313.test.ts @@ -40,6 +40,10 @@ * performs is pinned off disk below, so a later rewrite of the ladder cannot * leave this declaration describing a read that no longer exists. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/object-grid-export-options-refusal-7762.test.ts b/packages/types/src/__tests__/object-grid-export-options-refusal-7762.test.ts index fc3fcceead..d0c2180da7 100644 --- a/packages/types/src/__tests__/object-grid-export-options-refusal-7762.test.ts +++ b/packages/types/src/__tests__/object-grid-export-options-refusal-7762.test.ts @@ -41,6 +41,10 @@ * simply stopped parsing anything could not pass this file. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { ListViewSchema as SpecListViewSchema } from '@objectstack/spec/ui'; diff --git a/packages/types/src/__tests__/object-grid-title-mirrored.test.ts b/packages/types/src/__tests__/object-grid-title-mirrored.test.ts index e986e0809f..3e019dd6d2 100644 --- a/packages/types/src/__tests__/object-grid-title-mirrored.test.ts +++ b/packages/types/src/__tests__/object-grid-title-mirrored.test.ts @@ -38,6 +38,10 @@ * pass vacuously. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ObjectGridSchema } from '../zod/objectql.zod'; import type { ObjectGridSchema as TsObjectGridSchema } from '../objectql'; diff --git a/packages/types/src/__tests__/object-kanban-group-by-limit-7322.test.ts b/packages/types/src/__tests__/object-kanban-group-by-limit-7322.test.ts index 024dcf5499..2b534c2506 100644 --- a/packages/types/src/__tests__/object-kanban-group-by-limit-7322.test.ts +++ b/packages/types/src/__tests__/object-kanban-group-by-limit-7322.test.ts @@ -50,6 +50,10 @@ * sites are pinned OFF DISK below as a control — if one stops reading the alias * this file turns red, because the retirement's stated boundary moved. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/object-kanban-record-source-7780.test.ts b/packages/types/src/__tests__/object-kanban-record-source-7780.test.ts index 38b70c2c92..cb80e9dbbe 100644 --- a/packages/types/src/__tests__/object-kanban-record-source-7780.test.ts +++ b/packages/types/src/__tests__/object-kanban-record-source-7780.test.ts @@ -66,6 +66,10 @@ * must not incidentally overturn that, so the `groupBy` half of the vector is * asserted here alongside the record-source half. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/object-view-spec-parity.test.ts b/packages/types/src/__tests__/object-view-spec-parity.test.ts index 893689b3fd..5b3b24aadf 100644 --- a/packages/types/src/__tests__/object-view-spec-parity.test.ts +++ b/packages/types/src/__tests__/object-view-spec-parity.test.ts @@ -47,6 +47,10 @@ * whether the field belongs upstream in `@objectstack/spec` (promote it) or is * a genuine objectui-only extension (add it with a rationale). See #2890. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ListViewSchema as SpecListViewSchema, diff --git a/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts b/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts index 4d2d1e08b1..d841e3e256 100644 --- a/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts +++ b/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts @@ -58,6 +58,10 @@ * the renderer's read set moves) the measurement — and the stop — is re-taken * rather than remembered. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/objectql-record-source-refinement-6939.test.ts b/packages/types/src/__tests__/objectql-record-source-refinement-6939.test.ts index 464a6a5a75..4d130d5c81 100644 --- a/packages/types/src/__tests__/objectql-record-source-refinement-6939.test.ts +++ b/packages/types/src/__tests__/objectql-record-source-refinement-6939.test.ts @@ -39,6 +39,10 @@ * pin. The refinement's issue is checked by `path`, `params.code` and the three * key names in its message. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; diff --git a/packages/types/src/__tests__/objectql-union-arms-7363.test.ts b/packages/types/src/__tests__/objectql-union-arms-7363.test.ts index 731e7d9951..e54e531381 100644 --- a/packages/types/src/__tests__/objectql-union-arms-7363.test.ts +++ b/packages/types/src/__tests__/objectql-union-arms-7363.test.ts @@ -32,6 +32,10 @@ * The TS face is pinned beside it: `ObjectQLComponentSchema` narrows to each * declaration by its discriminant, instead of to `never`. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { z } from 'zod'; import { safeValidateSchema, ObjectQLComponentSchema as ObjectQLComponentZod } from '../zod/index.zod.js'; diff --git a/packages/types/src/__tests__/overlay-trigger-union-7081.test.ts b/packages/types/src/__tests__/overlay-trigger-union-7081.test.ts index 0de9cc9af7..2e8b2ce50c 100644 --- a/packages/types/src/__tests__/overlay-trigger-union-7081.test.ts +++ b/packages/types/src/__tests__/overlay-trigger-union-7081.test.ts @@ -64,6 +64,10 @@ * Recorded on the PR. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, expect, it } from 'vitest'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; diff --git a/packages/types/src/__tests__/owner-retired-contract-twins.test.ts b/packages/types/src/__tests__/owner-retired-contract-twins.test.ts index 8c3c7d42d4..2e66fb7b58 100644 --- a/packages/types/src/__tests__/owner-retired-contract-twins.test.ts +++ b/packages/types/src/__tests__/owner-retired-contract-twins.test.ts @@ -38,6 +38,10 @@ * pass while the shrink had quietly invalidated the replacement idiom too. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ReportFieldSchema } from '../zod/reports.zod.js'; import type { ReportField } from '../reports.js'; diff --git a/packages/types/src/__tests__/p1-spec-alignment.test.ts b/packages/types/src/__tests__/p1-spec-alignment.test.ts index be5df027df..ea00b673d1 100644 --- a/packages/types/src/__tests__/p1-spec-alignment.test.ts +++ b/packages/types/src/__tests__/p1-spec-alignment.test.ts @@ -10,6 +10,10 @@ * P1 Spec Protocol Alignment Tests * Tests for all P1 sub-items: ListView, FormView, Dashboard, Page, Record Components, i18n/ARIA */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; // The one runtime import in this otherwise type-only file: the retirement pin // below has to read a zod shape, because the TS interfaces here inherit diff --git a/packages/types/src/__tests__/page-app-dashboard-spec-parity.test.ts b/packages/types/src/__tests__/page-app-dashboard-spec-parity.test.ts index 370c41ffe7..1daec59418 100644 --- a/packages/types/src/__tests__/page-app-dashboard-spec-parity.test.ts +++ b/packages/types/src/__tests__/page-app-dashboard-spec-parity.test.ts @@ -39,6 +39,10 @@ * extension, and record the reason. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { AppSchema as SpecAppSchema, diff --git a/packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts b/packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts index 9afcec8924..551dfbe930 100644 --- a/packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts +++ b/packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts @@ -52,6 +52,10 @@ * itself a bug once). */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { createRequire } from 'node:module'; import { readFileSync } from 'node:fs'; diff --git a/packages/types/src/__tests__/phase2-schemas.test.ts b/packages/types/src/__tests__/phase2-schemas.test.ts index d1f44a0ae1..a9924b3eba 100644 --- a/packages/types/src/__tests__/phase2-schemas.test.ts +++ b/packages/types/src/__tests__/phase2-schemas.test.ts @@ -3,6 +3,10 @@ * Testing AppSchema, ReportComponentSchema and Enhanced ActionSchema, plus the * retirement pins for the theme and block component kinds. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { AppComponentSchema, diff --git a/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts b/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts index d4fac3ac50..516a4cb8e8 100644 --- a/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts +++ b/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts @@ -28,6 +28,10 @@ * inverted pin, see the bottom of this file. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { createRequire } from 'node:module'; import { readFileSync } from 'node:fs'; diff --git a/packages/types/src/__tests__/report-schema-authoring-face.test.ts b/packages/types/src/__tests__/report-schema-authoring-face.test.ts index dc1ff38533..7e8689bb4d 100644 --- a/packages/types/src/__tests__/report-schema-authoring-face.test.ts +++ b/packages/types/src/__tests__/report-schema-authoring-face.test.ts @@ -77,6 +77,10 @@ * alone leaves `declared !== enforced`, which is the defect ADR-0049 names. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { ReportBuilderSchema, diff --git a/packages/types/src/__tests__/schema-registry-chatbot-keys-7704.test.ts b/packages/types/src/__tests__/schema-registry-chatbot-keys-7704.test.ts index ff2209d167..9955c1e90c 100644 --- a/packages/types/src/__tests__/schema-registry-chatbot-keys-7704.test.ts +++ b/packages/types/src/__tests__/schema-registry-chatbot-keys-7704.test.ts @@ -51,6 +51,10 @@ * of the validator the CLI applies. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/select-option-spec-parity.test.ts b/packages/types/src/__tests__/select-option-spec-parity.test.ts index dbfe994449..984ae4e087 100644 --- a/packages/types/src/__tests__/select-option-spec-parity.test.ts +++ b/packages/types/src/__tests__/select-option-spec-parity.test.ts @@ -25,6 +25,10 @@ * gate removed. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { SelectOptionSchema as SpecSelectOptionSchema } from '@objectstack/spec/data'; import { SelectOptionSchema } from '../zod/form.zod.js'; diff --git a/packages/types/src/__tests__/spec-subschema-parity.test.ts b/packages/types/src/__tests__/spec-subschema-parity.test.ts index 900513ceba..c5fd38bed4 100644 --- a/packages/types/src/__tests__/spec-subschema-parity.test.ts +++ b/packages/types/src/__tests__/spec-subschema-parity.test.ts @@ -27,6 +27,10 @@ * on the spec base (and sanction the field here) only for genuinely * objectui-only renderer concerns. See #2231. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { HttpMethodSubsetSchema as SpecHttpMethodSubsetSchema, diff --git a/packages/types/src/__tests__/static-table-narrow-surface.test.ts b/packages/types/src/__tests__/static-table-narrow-surface.test.ts index fcc9fabf4f..263898b499 100644 --- a/packages/types/src/__tests__/static-table-narrow-surface.test.ts +++ b/packages/types/src/__tests__/static-table-narrow-surface.test.ts @@ -46,6 +46,10 @@ * `accordion-item-authorable-keys.test.ts`). */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { StaticTableColumn, TableColumn, TableSchema } from '../data-display'; import { diff --git a/packages/types/src/__tests__/table-column-type-canonical.test.ts b/packages/types/src/__tests__/table-column-type-canonical.test.ts index 34f4b88ff3..1c5ee6283e 100644 --- a/packages/types/src/__tests__/table-column-type-canonical.test.ts +++ b/packages/types/src/__tests__/table-column-type-canonical.test.ts @@ -34,6 +34,10 @@ * `packages/components/src/renderers/complex/__tests__/`. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { FieldType as SpecFieldTypeEnum } from '@objectstack/spec/data'; import { TABLE_COLUMN_TYPES, normalizeTableColumnType } from '../data-display'; diff --git a/packages/types/src/__tests__/text-value-retired-6951.test.ts b/packages/types/src/__tests__/text-value-retired-6951.test.ts index dd84745911..dff6aa0de4 100644 --- a/packages/types/src/__tests__/text-value-retired-6951.test.ts +++ b/packages/types/src/__tests__/text-value-retired-6951.test.ts @@ -43,6 +43,10 @@ * NOT evidence about them — type assertions are erased before it runs. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { readFileSync, readdirSync, statSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { describe, it, expect } from 'vitest'; diff --git a/packages/types/src/__tests__/timeline-catalog-fixture-migrated.test.ts b/packages/types/src/__tests__/timeline-catalog-fixture-migrated.test.ts index a4fbde01b7..9ef3337147 100644 --- a/packages/types/src/__tests__/timeline-catalog-fixture-migrated.test.ts +++ b/packages/types/src/__tests__/timeline-catalog-fixture-migrated.test.ts @@ -31,6 +31,10 @@ * so a rename that left the document invalid, or a value that stopped being * reachable, fails here. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, expect, it } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; diff --git a/packages/types/src/__tests__/timeline-declared-keys.test.ts b/packages/types/src/__tests__/timeline-declared-keys.test.ts index 3117b694da..3ecd381f7f 100644 --- a/packages/types/src/__tests__/timeline-declared-keys.test.ts +++ b/packages/types/src/__tests__/timeline-declared-keys.test.ts @@ -64,6 +64,10 @@ * comes, is a deliberate edit against a red test rather than a silent drift. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { TimelineConfigSchema } from '@objectstack/spec/ui'; import { TimelineSchema } from '../zod/data-display.zod.js'; diff --git a/packages/types/src/__tests__/timeline-items-bar-shape-7365.test.ts b/packages/types/src/__tests__/timeline-items-bar-shape-7365.test.ts index fbd7e6b9f9..aed354315e 100644 --- a/packages/types/src/__tests__/timeline-items-bar-shape-7365.test.ts +++ b/packages/types/src/__tests__/timeline-items-bar-shape-7365.test.ts @@ -68,6 +68,10 @@ * this checkout; it is unmeasured here and named as such on the PR. The * fixture census at the foot of this file is the durable half of that reading. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; diff --git a/packages/types/src/__tests__/timeline-items-row-shape-7164.test.ts b/packages/types/src/__tests__/timeline-items-row-shape-7164.test.ts index 43e289f021..2c6c0072ac 100644 --- a/packages/types/src/__tests__/timeline-items-row-shape-7164.test.ts +++ b/packages/types/src/__tests__/timeline-items-row-shape-7164.test.ts @@ -64,6 +64,10 @@ * as before. Neither a row's nor a bar's own keys are declared, and refining * by `variant` is still a wider contract than either ruling named. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; diff --git a/packages/types/src/__tests__/timeline-timescale-retired.test.ts b/packages/types/src/__tests__/timeline-timescale-retired.test.ts index 8d0bd51127..b6517d992e 100644 --- a/packages/types/src/__tests__/timeline-timescale-retired.test.ts +++ b/packages/types/src/__tests__/timeline-timescale-retired.test.ts @@ -47,6 +47,10 @@ * alias is untouched. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { TimelineSchema } from '../zod/data-display.zod.js'; import type { TimelineSchema as TimelineSchemaTS } from '../data-display.js'; diff --git a/packages/types/src/__tests__/toast-button-keys.test.ts b/packages/types/src/__tests__/toast-button-keys.test.ts index 387e86d883..de7bfc0f05 100644 --- a/packages/types/src/__tests__/toast-button-keys.test.ts +++ b/packages/types/src/__tests__/toast-button-keys.test.ts @@ -91,6 +91,10 @@ * fail there. See the PR for the recorded red. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ToastSchema } from '../zod/feedback.zod.js'; import type { ToastSchema as ToastSchemaTS } from '../feedback'; diff --git a/packages/types/src/__tests__/toggle-group-item-authorable-keys.test.ts b/packages/types/src/__tests__/toggle-group-item-authorable-keys.test.ts index fdae568f08..5256404fce 100644 --- a/packages/types/src/__tests__/toggle-group-item-authorable-keys.test.ts +++ b/packages/types/src/__tests__/toggle-group-item-authorable-keys.test.ts @@ -43,6 +43,10 @@ * so re-adding `icon?` to the interface fails the build on the unused directive. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { ToggleGroupItem } from '../disclosure'; import { ToggleGroupItemSchema } from '../zod/disclosure.zod'; diff --git a/packages/types/src/__tests__/tree-view-data-optional-6939.test.ts b/packages/types/src/__tests__/tree-view-data-optional-6939.test.ts index 1fbd0941c3..7d8c6ae57b 100644 --- a/packages/types/src/__tests__/tree-view-data-optional-6939.test.ts +++ b/packages/types/src/__tests__/tree-view-data-optional-6939.test.ts @@ -51,6 +51,10 @@ * is the pin that makes the difference visible — it is the assertion that turns * green-to-red if a later sweep deletes the member. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/tree-view-data-retired-6951.test.ts b/packages/types/src/__tests__/tree-view-data-retired-6951.test.ts index 692af86963..d353fd91a1 100644 --- a/packages/types/src/__tests__/tree-view-data-retired-6951.test.ts +++ b/packages/types/src/__tests__/tree-view-data-retired-6951.test.ts @@ -51,6 +51,10 @@ * NOT evidence about them — type assertions are erased before it runs. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { readFileSync, readdirSync, statSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { describe, it, expect } from 'vitest'; diff --git a/packages/types/src/__tests__/undeclared-but-consumed-keys-6150.test.ts b/packages/types/src/__tests__/undeclared-but-consumed-keys-6150.test.ts index 7bc6f50b3a..923bb792a8 100644 --- a/packages/types/src/__tests__/undeclared-but-consumed-keys-6150.test.ts +++ b/packages/types/src/__tests__/undeclared-but-consumed-keys-6150.test.ts @@ -58,6 +58,10 @@ * numbers drift and are therefore in prose only; the READ is the fact. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/widget-input-control-vocabulary.test.ts b/packages/types/src/__tests__/widget-input-control-vocabulary.test.ts index d2b826d921..2ee004a986 100644 --- a/packages/types/src/__tests__/widget-input-control-vocabulary.test.ts +++ b/packages/types/src/__tests__/widget-input-control-vocabulary.test.ts @@ -47,6 +47,10 @@ * bearing sentences are pinned too. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { describe, it, expect } from 'vitest'; diff --git a/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts b/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts index 8496de6741..d377a4a874 100644 --- a/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts +++ b/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts @@ -60,6 +60,10 @@ * lands, that expectation is the one to revisit deliberately. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/wrapper-class-declared-7722.test.ts b/packages/types/src/__tests__/wrapper-class-declared-7722.test.ts index 636ea4c3c0..f0976a11e4 100644 --- a/packages/types/src/__tests__/wrapper-class-declared-7722.test.ts +++ b/packages/types/src/__tests__/wrapper-class-declared-7722.test.ts @@ -53,6 +53,10 @@ * the next single-key grep (objectui#6938 → objectui#7722 was that wait). */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readdirSync, readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts b/packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts index 442abf01b4..5847491b30 100644 --- a/packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts +++ b/packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts @@ -24,24 +24,20 @@ * TreeNodeSchema ReferenceError: Cannot access 'TreeNodeSchema' before initialization * * Seven name the very const being declared (`children: z.array(TreeNodeSchema)` - * sits inside `TreeNodeSchema`'s own initialiser); `SchemaNodeSchema` named - * `BaseSchemaCore`, which `base.zod.ts` declared BELOW it. For those eight the - * `z.lazy` was LOAD-BEARING — buying a TDZ dodge, not a style — and they keep - * the spelling they have. `mechanism` below reproduces the failure. + * sits inside `TreeNodeSchema`'s own initialiser); `SchemaNodeSchema` names + * `BaseSchemaCore`, which `base.zod.ts` declares BELOW it. For those eight the + * `z.lazy` is LOAD-BEARING — it is buying a TDZ dodge, not a style — and they + * keep the spelling they have. `mechanism` below reproduces the failure. * - * ⚠️ SEVEN, not eight, since objectui#8344. That card redirected the node - * recursion point at `AnyComponentSchema` and had to build `SchemaNodeSchema`'s - * union ONCE, at module scope, immediately below `BaseSchemaCore` — because the - * component arm is a written option slot and there has to be an array to write - * into. Declaring it below `BaseSchemaCore` is what dissolves the TDZ, so the - * memoisation this file calls "worth doing where it is free" became free for this - * one const, and the row moved to {@link MEMOISED}. ⛔ It is a BYPRODUCT, not a - * goal: nobody memoised it to make `.unwrap()` honest, and ⛔ nothing here licenses - * moving the remaining seven — each still names the const being declared, and - * `mechanism` still reproduces their ReferenceError. - * - * ⇒ the eight-name list above is kept VERBATIM as the objectui#7918 reading it - * was. It is history, not the current ledger; the arrays below are the ledger. + * ⚠️ objectui#8344 moved this row TWICE and it ends where it started, which is + * worth one sentence so the next reader does not re-derive it. An intermediate + * revision of that card built the node union ONCE at module scope, below + * `BaseSchemaCore`, so the TDZ dissolved and the getter returned one object — the + * row was {@link MEMOISED} for as long as that spelling lived. What shipped instead + * reads `AnyComponentSchema` as an IMPORT BINDING inside the getter, so the getter + * BUILDS the node union per call again and `SchemaNodeSchema` stays {@link + * TDZ_BOUND} — the same reading objectui#7918 recorded, for a different reason: + * the TDZ it now dodges is the module cycle's, not `BaseSchemaCore`'s. * * The two that loaded clean were memoised: `FilterBuilderConditionSchema` is not * recursive at all, and `NavigationItemSchema` already defers its self-reference @@ -96,6 +92,10 @@ * strict face (objectui#7935 / objectstack#5250) should make that trade * deliberately. Update this ledger in the same change. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { @@ -129,9 +129,6 @@ const innerTypeStable = (S: unknown): boolean => (S as LazyInternals)._zod.inner const MEMOISED: ReadonlyArray = [ ['FilterBuilderConditionSchema', FilterBuilderConditionSchema], ['NavigationItemSchema', NavigationItemSchema], - // objectui#8344 — see the header. Its getter returns the ONE node union that - // `base.zod.ts` builds below `BaseSchemaCore`, so there is no TDZ left to dodge. - ['SchemaNodeSchema', SchemaNodeSchema], ]; /** ⛔ Do not "fix" these — each one's `z.lazy` dodges a real ReferenceError. */ const TDZ_BOUND: ReadonlyArray = [ @@ -141,6 +138,7 @@ const TDZ_BOUND: ReadonlyArray = [ ['MenuItemSchema', MenuItemSchema], ['NavLinkSchema', NavLinkSchema], ['NavigationMenuItemSchema', NavigationMenuItemSchema], + ['SchemaNodeSchema', SchemaNodeSchema], ['TreeNodeSchema', TreeNodeSchema], ]; diff --git a/packages/types/src/__tests__/zod-mirror-authors-no-defaults-7735.test.ts b/packages/types/src/__tests__/zod-mirror-authors-no-defaults-7735.test.ts index dd8b2acb26..0ad737202e 100644 --- a/packages/types/src/__tests__/zod-mirror-authors-no-defaults-7735.test.ts +++ b/packages/types/src/__tests__/zod-mirror-authors-no-defaults-7735.test.ts @@ -60,6 +60,10 @@ * them and says why its assertion is a floor and not a ratchet. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, expect, it } from 'vitest'; import { readFileSync, readdirSync } from 'node:fs'; import { dirname, join } from 'node:path'; diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index 6ef348d9fb..50458273d6 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -331,6 +331,10 @@ * the mirror accepts a function again or a renderer lost its callback. */ +// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. +// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a +// category module puts `BaseSchema` in its temporal dead zone and throws at load. +import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readdirSync, readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/zod/base.zod.ts b/packages/types/src/zod/base.zod.ts index 80c457d14a..e2500472a1 100644 --- a/packages/types/src/zod/base.zod.ts +++ b/packages/types/src/zod/base.zod.ts @@ -21,6 +21,10 @@ import { I18nLabelSchema } from '@objectstack/spec/ui'; import { retirementTombstone } from './tombstone.zod.js'; import { ExpressionWireSchema } from './expression.zod.js'; import type { SchemaNode } from '../base.js'; +// ⚠️ CYCLE, deliberately: `index.zod.ts` imports this module. The binding below is read +// ONLY inside `SchemaNodeSchema`'s `z.lazy` getter, which runs long after both module +// bodies have evaluated — see that const's docblock for why a binding and not a write. +import { AnyComponentSchema } from './index.zod.js'; /** * A KEYED i18n label — the runtime mirror of `KeyedI18nLabel` in `../base.ts`. @@ -49,70 +53,6 @@ export const KeyedI18nLabelSchema = z.object({ }); -/** - * Fill the node recursion point with the component union, and hand it straight - * back — so the fill is part of `AnyComponentSchema`'s own initializer in - * `index.zod.ts` rather than a bare statement beside it (objectui#8344). - * - * ## ⚠️ Why a WRITE INTO the union's option list, and not a `z.lazy` holder - * - * The obvious spelling — a `let` the `z.lazy` getter reads — is WRONG here, and - * measurably so. `z.lazy` MEMOISES: zod 4.4.3 caches the resolved inner on first - * access, and merely parsing any component schema resolves it (the union arm walk - * reads every option to compute its own metadata, so a childless `detail-view` node - * is enough). ⇒ whatever the getter returned FIRST would be the accept set for the - * rest of the process, decided by whichever module graph parsed first — and this - * repo's `isolate: false` unit project shares one module graph across every file in - * a worker. Measured on this branch with that spelling in place: the #8344 pin - * PASSED run alone and FAILED in the full run, because - * `__tests__/handler-keys-string-any-mirrors-7344.test.ts` parses from a barrel-free - * import graph and froze the base shape in first. Refusing instead of falling back - * converges, but turns that same import order into dozens of red suites. - * - * ⭐ A `z.union` does NOT memoise its options: measured on zod 4.4.3, `z.union(opts)` - * keeps `opts` BY REFERENCE and re-reads it on every parse, so writing slot 0 takes - * effect immediately — including after parses have already run through it. That is - * what makes the window disappear rather than merely move: before the fill a child - * slot answers exactly as it did pre-#8344, after it every parse sees the component - * union, and no first-parse ever freezes the wrong answer in. - * - * ⚠️ That by-reference behaviour is the load-bearing assumption, so it is ASSERTED - * here rather than trusted: a zod that copied the array would leave this silently - * under-enforcing — the one failure direction that never announces itself. - * - * ⚠️ The parameter bound is `z.ZodType`, not `z.ZodType< SchemaNode, SchemaNode >`, - * and that too is measured. The tighter bound is the one this wiring wants — "the - * recursion point may only be filled with something a declared `SchemaNode` slot - * could already hold" — and `tsc` refuses it TODAY for exactly one arm out of 106: - * `complex.zod.ts#ChatbotSchema` mirrors the chat API body params under the key - * `body`, which is `BaseSchema`'s CHILDREN slot (`Record< string, unknown >` where - * the base says `SchemaNode | SchemaNode[]`). That collision is pre-existing and - * already recorded — the parity ledger carries it under `KnownDrift`, the TS - * declaration renamed the key to `requestBody`, and `ChatbotSharedMirrorShape` in - * `complex.zod.ts` says in as many words that a ruling on `ChatbotSchema`'s own - * `body` arm is a separate question. ⛔ #8344 does not decide it either. So the bound - * is loose HERE and the real check is kept EXACT one level out, as a type-level pin - * naming that single arm in `__tests__/node-recursion-point-8344.test.ts`. ⇒ a SECOND - * arm drifting the same way turns that pin red instead of passing unnoticed. - * - * @internal — the package's only zod entry point is the `./zod` barrel, which is - * `index.zod.ts`; this exists for that one call site and is not re-exported. - */ -export function defineNodeComponentUnion(union: T): T { - nodeUnionOptions[0] = union; - // The assertion the paragraph above exists for. ⛔ Do not delete it as noise: it is - // the only thing standing between a zod that copies its option array and a - // recursion point that silently reverts to the pre-#8344 base shape. - const installed = (nodeUnion as unknown as { _zod: { def: { options: readonly unknown[] } } })._zod.def.options[0]; - if (installed !== union) { - throw new Error( - 'objectui#8344: `z.union` no longer keeps its option array by reference, so the node ' - + 'recursion point did not take. The redirect is INERT and every nested node is being ' - + 'judged by `BaseSchemaCore` again — see `defineNodeComponentUnion` in base.zod.ts.', - ); - } - return union; -} /** * Schema Node — what a child slot holds: a COMPONENT document, or a primitive. @@ -129,43 +69,36 @@ export function defineNodeComponentUnion(union: T): T { * the registered component mirrors is the whole of this change; ⛔ nothing here is * `.strict()`, and `BaseSchemaCore` keeps its passthrough. * - * Priced at 9 newly-refused corpus documents (objectui#8344's R3, 54 / 553 against - * R1's 45 / 553), each one pre-existing debt this SURFACES rather than creates: + * Priced at 9 newly-refused corpus documents — objectui#8344's R3 at **54 / 554** against + * R1's **45 / 554**, re-derived on this branch's merged head; the card's own body quotes + * 553 because it was measured before the corpus gained a document, and the nine are the + * same nine either way. Each is pre-existing debt this SURFACES rather than creates: * four whose child `type` resolves in no arm, five already red under their own * schema and shielded until now by the recursion point. * - * ## ⚠️ Why the arm is late-bound and not imported + * ## ⚠️ Why the arm is an IMPORT BINDING read inside the getter * - * `AnyComponentSchema` is built in `index.zod.ts` out of all 13 category modules, - * and 14 modules import THIS one — so naming it here is a module cycle, and - * `z.lazy` defers the EVALUATION, not the module graph. With that import in place, - * entering the graph at `base.zod.js` evaluates `app.zod.ts`'s body while - * `BaseSchema` is still in its temporal dead zone and the package throws on import. - * ⇒ the break is deliberate: `index.zod.ts` fills the holder through - * {@link defineNodeComponentUnion} as it constructs the union, which is module - * evaluation and therefore strictly before anything can parse. + * `AnyComponentSchema` is built in `index.zod.ts` out of all 13 category modules, and 14 + * modules import THIS one, so naming it at this module's top level is a cycle that throws: + * entering the graph at `base.zod.js` would evaluate `app.zod.ts`'s body while `BaseSchema` + * is still in its temporal dead zone. The binding is therefore imported and read ONLY from + * inside the `z.lazy` getter, through {@link nodeComponentArm} — deferred to first parse, + * which is after both module bodies have completed. * - * ⚠️ BEFORE the fill — a module graph that reaches a parse without ever evaluating - * `index.zod.js` — the arm is `BaseSchemaCore`, i.e. exactly the pre-#8344 accept - * set, and it switches the moment the barrel loads. That is a property of the WRITE, - * not a tolerated fallback: `z.union` re-reads its option array on every parse, so - * nothing can freeze the pre-fill answer in ({@link defineNodeComponentUnion} carries - * the measurement, and why the obvious `z.lazy` holder is wrong). No published entry - * point can reach that window BY MODULE GRAPH: `./zod` is this package's only zod - * subpath and it IS `index.zod.js`. Pinned in - * `__tests__/node-recursion-point-8344.test.ts`. + * ⭐ Two properties come from that, and the earlier spelling had neither. It wrote the arm + * into a live option array from the barrel's body, so (a) a bundler honouring this package's + * `"sideEffects": false` could drop the write and leave every child slot judged by + * `BaseSchemaCore` again — silently, with the write's own assertion dropped alongside it — + * and (b) a module graph that reached a parse without evaluating the barrel got exactly the + * pre-#8344 accept set with no diagnostic. A read binding cannot do either: whatever retains + * `SchemaNodeSchema` retains the union it names, and a graph that has not evaluated the + * barrel throws `ReferenceError` at load rather than answering wrongly. ⇒ ⛔ `"sideEffects": + * false` stays TRUE and untouched; there is no load-time write in this module to declare. * - * ⛔ ⚠️ THAT SENTENCE IS ABOUT MODULE GRAPHS, AND A BUNDLER IS NOT ONE. This package - * declares `"sideEffects": false` and the fill is a statement in this barrel's body, - * so a bundler that honours the flag and sees no reference to `AnyComponentSchema` - * may drop the whole const — fill included — and then every child slot validates - * with the PRE-#8344 arm. Measured on this repo's own Vite/rollup lib build: one - * entry importing only `CardSchema` ACCEPTS a nested off-spec node (369,733 bytes, - * no fill in the output), the same entry with `AnyComponentSchema` also imported - * REFUSES it (1,144,999 bytes, fill present). The guard below cannot see this: it - * runs inside the code that was dropped. ⇒ this window is silent, it is NOT the - * pre-fill window this paragraph describes, and its disposition is a ruling in - * flight on objectui#8344 — ⛔ do not close it by editing this comment. + * ⚠️ The cost is real and is paid by TESTS, not by consumers: a test that enters the graph at + * a category module rather than at the `./zod` barrel now throws at import. The fix is one + * line of import hygiene — import the barrel first — and the files that needed it are listed + * in this PR. Entering at `./zod`, the only published zod subpath, is always safe. * * ## Both type arguments are filled, and that is the whole published input face * @@ -199,14 +132,17 @@ export function defineNodeComponentUnion(union: T): T { * a declaration or narrowing a mirror to make the annotation fit: either is a * contract change wearing a type-annotation's clothes, and both are ruled elsewhere. */ -export const SchemaNodeSchema: z.ZodType = z.lazy(() => { - // `z.lazy` memoises this getter, and that is FINE — because what it returns is the - // one live union, whose option slot 0 IS the recursion point and is written by - // {@link defineNodeComponentUnion}. ⛔ Do not move the union's CONSTRUCTION in here: - // a getter that builds the union is the memoising spelling objectui#8344 measured - // wrong, and it would put the accept set back at the mercy of import order. - return nodeUnion; -}); +export const SchemaNodeSchema: z.ZodType = z.lazy( + () => + z.union([ + nodeComponentArm(), + z.string(), + z.number(), + z.boolean(), + z.null(), + z.undefined(), + ]) as unknown as z.ZodType, +); /** * Base Schema - Core validation schema that all components extend @@ -393,39 +329,48 @@ const BaseSchemaCore = z.object({ export const BaseSchema = BaseSchemaCore; /** - * The one node union every child slot recurses through — built HERE, immediately - * below `BaseSchemaCore`, because slot 0 holds it (objectui#8344). + * The COMPONENT arm of the node union, built fresh on every getter call. * - * Slot 0 is the RECURSION POINT and is the only slot that ever changes: - * `BaseSchemaCore` while `index.zod.ts` has not been evaluated, `AnyComponentSchema` - * from the moment it has. `z.union` re-reads this array on every parse, so the swap - * is live and no parse can freeze the pre-fill answer in — the whole reason the - * arm is a written slot rather than a `z.lazy` holder ({@link defineNodeComponentUnion} - * carries the measurement). + * ## Why a function and not a `const` (objectui#8344) * - * ⛔ Never export this array or this union. `SchemaNodeSchema` is the public handle - * and identity on it is what objectui#7918 consequence ① says is stable; a second - * exported name for the same shape would give the parity census a row to compare - * that has no TS declaration behind it. - */ -/** - * ⚠️ Both of these are `const` DECLARATIONS, ⛔ never assignments to a `let` hoisted - * above `BaseSchemaCore`. `@object-ui/types` declares `"sideEffects": false`, and a - * bare top-level assignment is a load-time side effect a bundler is entitled to drop - * whole — `scripts/__tests__/side-effects-declaration-consistency.test.ts` fails on - * exactly that, and it caught this file mid-#8344. Everything above that names them - * does so from inside a function body, which runs long after this line. + * `AnyComponentSchema` lives in `index.zod.ts`, which imports THIS module, so at this + * module's evaluation time the imported binding is in its temporal dead zone. Reading it + * from inside a function body defers the read until `z.lazy` first resolves — after both + * module bodies have run. ⇒ the binding is either initialised (barrel entered, the normal + * path) or it throws `ReferenceError` at load, LOUDLY. There is no third answer, and in + * particular there is no longer a quiet pre-fill window that answers exactly as `main`. + * + * ⭐ That is also what makes the redirect survive BUNDLING. The previous spelling wrote + * the arm into a live option array from the barrel's body; with `"sideEffects": false` a + * bundler was entitled to drop that write when a consumer imported one schema by name, + * and every child slot silently went back to `BaseSchemaCore` — measured on this repo's + * own Vite/rollup build. A USED import binding is retained by construction: whatever + * keeps `SchemaNodeSchema` keeps the union it names. ⛔ `"sideEffects": false` stays TRUE + * here — this module performs no load-time write at all now. + * + * ## The `chatbot` guard, and why it is here rather than in the mirror + * + * `ChatbotSchema.body` mirrors the chat API's body params as a record, which is WIDER + * than `BaseSchemaCore.body`. It is the only wider redeclaration among the 109 base-key + * redeclarations across the union's arms, so without this guard the redirect would narrow + * at 108 slots and WIDEN at one: a `chatbot` node with a record `body` is refused at a + * child slot on `main` and would be accepted here. ⛔ The root mirror is deliberately not + * touched — it carries the chat API's params on purpose, and that question is its own + * card — so the narrowing lives on the arm the recursion point installs and nowhere else. + * ⇒ a root `chatbot` with a record `body` still parses; the same node one slot down does + * not. Both directions are pinned in `__tests__/node-recursion-point-8344.test.ts`. */ -const nodeUnionOptions: [z.ZodType, ...z.ZodType[]] = [ - BaseSchemaCore, - z.string(), - z.number(), - z.boolean(), - z.null(), - z.undefined(), -]; - -const nodeUnion = z.union(nodeUnionOptions) as unknown as z.ZodType; +const nodeComponentArm = (): z.ZodType => + AnyComponentSchema.superRefine((value, ctx) => { + const node = value as { type?: unknown; body?: unknown } | null | undefined; + if (!node || node.type !== 'chatbot' || node.body === undefined) return; + const asNodeSlot = BaseSchemaCore.shape.body.safeParse(node.body); + if (asNodeSlot.success) return; + for (const issue of asNodeSlot.error.issues) { + ctx.addIssue({ ...issue, path: ['body', ...issue.path] }); + } + }) as unknown as z.ZodType; + /** * A spec schema's fields, minus the keys objectui declares locally, as an diff --git a/packages/types/src/zod/index.zod.ts b/packages/types/src/zod/index.zod.ts index 22c76ca2bc..944fb6f622 100644 --- a/packages/types/src/zod/index.zod.ts +++ b/packages/types/src/zod/index.zod.ts @@ -350,7 +350,6 @@ export { // ============================================================================ import { z } from 'zod'; -import { defineNodeComponentUnion } from './base.zod.js'; import { AppComponentSchema } from './app.zod.js'; import { LayoutSchema } from './layout.zod.js'; import { FormComponentSchema } from './form.zod.js'; @@ -370,17 +369,17 @@ import { ViewComponentSchema } from './views.zod.js'; * Use this for generic component rendering where the type is determined at runtime. * * ⭐ It is ALSO the node recursion point (objectui#8344): every child slot is - * `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])`, and `SchemaNodeSchema` - * resolves its component arm to THIS union, so a nested node is judged by its own - * component schema at every depth instead of by the ~21 base keys. The wiring is a - * late-binding holder rather than an import because 14 modules import `base.zod.js` - * and this module is built from all 13 category modules — the full reasoning, and - * what the UNFILLED holder answers, live on `SchemaNodeSchema` in `base.zod.ts`. + * `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])`, and `SchemaNodeSchema` builds + * its component arm FROM THIS CONST, so a nested node is judged by its own component + * schema at every depth instead of by the ~21 base keys. * - * ⚠️ The fill is written as this const's own initializer, not as a statement beside - * it, so no bundler can keep the union and drop the wiring, and no future edit can - * reorder the two. ⛔ Do not "simplify" it back into a bare - * `defineNodeComponentUnion(AnyComponentSchema)` call underneath. + * ⚠️ The wiring lives in `base.zod.ts`, not here, and it is an IMPORT BINDING read inside + * that const's `z.lazy` getter — ⛔ no write into this module's body, no holder, no + * option-array patching. This module therefore performs no load-time side effect, which is + * what keeps `"sideEffects": false` true and what keeps the redirect alive through a + * bundler: a binding that is READ is retained, while the write this replaced could be + * dropped silently. The reasoning, the measurement and the `chatbot` narrowing that rides + * on the same arm all live on `SchemaNodeSchema` and `nodeComponentArm` in `base.zod.ts`. * * ## Why this is discriminated (objectui#8498) * @@ -405,12 +404,10 @@ import { ViewComponentSchema } from './views.zod.js'; * * ⚠️ BOTH of the above are live here, and the composition is the whole resolution: * objectui#8498 changed WHICH arm reports, objectui#8344 changed WHERE this union is - * consulted. The discriminated union is what gets written into the node option slot, - * so `defineNodeComponentUnion` wraps it rather than replacing it. The slot itself is - * still a plain `z.union` in `base.zod.ts` — that is what keeps its option array by - * reference, and it is untouched by the discrimination. + * consulted. They compose because they touch different things — the discrimination is in + * this initializer, the recursion wiring is a binding `base.zod.ts` reads. */ -export const AnyComponentSchema = defineNodeComponentUnion(z.discriminatedUnion('type', [ +export const AnyComponentSchema = z.discriminatedUnion('type', [ AppComponentSchema, LayoutSchema, FormComponentSchema, @@ -436,7 +433,7 @@ export const AnyComponentSchema = defineNodeComponentUnion(z.discriminatedUnion( // schema, so it also rewrote this union's `invalid_type` and a non-object root // lost "expected object, received number". `undefined` declines to the locale. error: (issue) => (issue.code === 'invalid_union' ? 'Invalid input' : undefined), -})); +}); /** * Validate a schema against the AnyComponentSchema From 2ac53818e275e548c08835fe10e77f7d2c760a84 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 13:57:35 +0000 Subject: [PATCH 7/9] docs(changeset): state what ships, and drop three attributions that were never ruled Batch #93 refused the routes the previous revision described, so the changeset described a PR that does not exist. Corrected, not softened: - the widening fact said "Declared here rather than eliminated, by ruling". No ruling said that; #93 ordered it eliminated. It now records the nested refusal and the untouched root, and points the root question at objectui#8572. - the `TDZ_BOUND -> MEMOISED` fact is gone: that byproduct belonged to the option-array spelling and the row is `TDZ_BOUND` again. - the bundle caveat said the fix was "a maintainer-floor authorisation, deliberately not taken here" and "Until objectui#8577 is ruled". Both false against #93: the ruled route needs no manifest change and no census-floor edit, and it ships in this PR. The section now states the wiring, the measured before/after, and both byte costs. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE --- .../8344-node-recursion-point-redirect.md | 76 ++++++++----------- 1 file changed, 33 insertions(+), 43 deletions(-) diff --git a/.changeset/8344-node-recursion-point-redirect.md b/.changeset/8344-node-recursion-point-redirect.md index db51290ac1..a3cb305fcb 100644 --- a/.changeset/8344-node-recursion-point-redirect.md +++ b/.changeset/8344-node-recursion-point-redirect.md @@ -39,54 +39,44 @@ constraints are measured, and the reasoning lives on `defineNodeComponentUnion` `zod/base.zod.ts`. -## Four more public-surface facts this ships +## Three more public-surface facts this ships **1. `DashboardWidgetSchema.component` narrows.** That legacy `{ id, component, layout }` envelope names `BaseSchema` explicitly instead of following the redirect, so the widget slot keeps admitting `metric-card`, objectui's closed widget-slot extension. One measured -delta and only one: a PRIMITIVE in that slot (`component: 'text'`) was accepted through -`SchemaNodeSchema` and is refused now. No corpus document, fixture or pin writes one. +delta on that slot and only one: a PRIMITIVE in it (`component: 'text'`) was accepted +through `SchemaNodeSchema` and is refused now. No corpus document, fixture or pin writes +one. -**2. `SchemaNodeSchema` moves from `TDZ_BOUND` to `MEMOISED`.** Its `z.lazy` getter now -returns the one live union rather than building one per call, so `getter() === getter()` -and `.unwrap() === .unwrap()` are TRUE for this export where they were FALSE. The -supported handle is unchanged and is still the exported wrapper; the other seven mirrors -in that ledger are untouched. - -**3. ⚠️ One WIDENING, in the same stroke: `chatbot` nodes with a record `body`.** +**2. The `chatbot` record `body` is refused NESTED, and still accepted at the ROOT.** `ChatbotSchema.body` mirrors the chat API's body params as -`z.record(z.string(), z.unknown())`, which is WIDER than `BaseSchemaCore.body`. Judging a -child by its own schema therefore admits, at every child slot, a document that the base -arm refused. Measured, corpus-valid chatbot seed plus `body: { model, temperature }`: -accepted at the root before and after; inside `card.body[]` and `div.children[]` REFUSED -before, ACCEPTED now. It is the only wider redeclaration among 109 base-key -redeclarations across the union's arms, and no corpus document writes one — which is why -the 45 to 54 headline does not show it. Declared here rather than eliminated, by ruling: -narrowing a published `chatbot` mirror is its own contract decision, and it is filed as -objectui#8572. - -**⚠️ 4. Caveat for BUNDLED consumers — this redirect can be tree-shaken away, and it is not -fixed here.** The arm is filled by this package's `./zod` barrel, and the package declares -`"sideEffects": false`, so a bundler is entitled to drop that fill when a consumer imports one -schema by name without also importing `AnyComponentSchema`. When it does, every child slot -validates with the PRE-redirect arm — no error, no warning, the old accept set, and the fill's -own assertion dropped with it so nothing can announce the failure. Measured on this repo's own -Vite/rollup lib build: an entry importing only `CardSchema` ACCEPTS a nested off-spec node -(370,652 bytes, no fill in the output); the same entry with `AnyComponentSchema` also imported -REFUSES it (1,149,749 bytes, fill present). +`z.record(z.string(), z.unknown())`, which is WIDER than `BaseSchemaCore.body` — the only +wider redeclaration among the 109 base-key redeclarations across the union's arms. Judging +a child by its own schema would therefore have ADMITTED, at every child slot, a document +the base arm refused. ⛔ That widening is eliminated rather than declared: the arm the +recursion point installs carries a check that a nested `chatbot` node's `body` still fits +the node slot. Measured, corpus-valid chatbot seed plus `body: { model, temperature }`: +accepted at the root before and after; inside `card.body[]` and `div.children[]` refused +before and refused now. ⇒ the redirect narrows at all 109 redeclarations and widens at +none. The published `ChatbotSchema` is untouched — whether its own `body` should carry the +chat API's params is a separate question, recorded on objectui#8572 and deliberately not +decided here. -Three fixes were measured and none of them is a manifest edit this change may make on its own: -narrowing `sideEffects` to an array is not a legal declaration for this package (one gate -requires an array to name every entry form, another refuses a named entry that has no load-time -effect, and this package's entry forms are pure); a bare top-level call in the barrel is dropped -too, because `"sideEffects": false` is a package-level promise no in-module spelling can -override; and removing the field closes it at a measured cost of 16,078 more gzipped bytes in -this repo's console `framework` chunk, which now FITS its ceiling but moves a workspace census -a guard pins — a maintainer-floor authorisation, deliberately not taken here. +**3. The redirect survives BUNDLING, and that is a property of how it is wired.** The arm +is an import binding read inside `SchemaNodeSchema`'s `z.lazy` getter, ⛔ not a write into +a live option array from the barrel's body. The difference is measurable, on this repo's +own Vite/rollup lib build, with an entry that imports only `CardSchema` from +`@object-ui/types/zod`: with the write, the bundler dropped it and a nested off-spec node +was ACCEPTED — the pre-redirect accept set, with no error and no warning; with the +binding, the same entry REFUSES it, because whatever retains `SchemaNodeSchema` retains +the union it names. ⚠️ The cost is real and is stated rather than hidden: that entry grows +from 113,887 to 342,193 bytes gzipped, and this repo's own console `framework` chunk from +72,248 to 90,969 (its ceiling is 100,000). That is the price of the redirect being real +for bundled consumers rather than a promise that depends on a bundler flag. -⇒ **Declared, follow-ups filed**, not an accepted permanent state: objectui#8577 carries the -leak and that manifest decision with every figure, and objectui#8578 carries the reason the gate -built to see load-time effects scored this package zero — it counts top-level call statements and -cannot see an effect performed inside a `const` initializer. Until objectui#8577 is ruled, a -consumer that bundles `@object-ui/types/zod` should keep `AnyComponentSchema` in its import -graph, which is enough to make the redirect apply. +⚠️ `"sideEffects": false` is unchanged and stays true: with the write gone, this package +performs no load-time side effect at all. ⚠️ One cost is paid inside this repo: a test +that entered the zod graph at a category module rather than at the `./zod` barrel now +throws `ReferenceError` at import instead of silently validating against the old arm, and +102 `packages/types` test files took a one-line barrel-first import because of it. +Consumers cannot hit that: `./zod` is the package's only zod subpath. From ca2037680d4bc64a4555f9983d0cae7c96479143 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 14:16:07 +0000 Subject: [PATCH 8/9] =?UTF-8?q?fix(types):=20revert=20the=20F3=20import=20?= =?UTF-8?q?binding=20=E2=80=94=20CI=20proved=20it=20breaks=20a=20real=20co?= =?UTF-8?q?nsumer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⛔ The `Build Docs` failure on 2ac53818e 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 Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE --- .../8344-node-recursion-point-redirect.md | 39 +-- .../accordion-item-authorable-keys.test.ts | 4 - .../action-callback-retired-7068.test.ts | 4 - .../alert-dialog-read-dialect-7104.test.ts | 4 - .../any-component-union-fanout.test.ts | 4 - .../app-action-onclick-refusal-6854.test.ts | 4 - .../app-hidden-catalogue-flag-7542.test.ts | 4 - .../src/__tests__/base-bind-declared.test.ts | 4 - .../base-schema-hidden-predicate.test.ts | 4 - ...ase-schema-predicate-envelope-7530.test.ts | 4 - .../base-schema-zod-mirror-parity.test.ts | 4 - .../block-family-retired-4895.test.ts | 4 - .../button-group-doc-surface-6347.test.ts | 4 - .../calendar-view-mode-agenda-retired.test.ts | 4 - .../__tests__/chart-data-model-7113.test.ts | 4 - .../chart-inline-data-retired.test.ts | 4 - ...ries-chart-type-alias-refusal-7694.test.ts | 4 - .../__tests__/chart-series-keys-7546.test.ts | 4 - .../chat-message-avatar-keys-7295.test.ts | 4 - .../chatbot-authoring-face-keys.test.ts | 4 - .../chatbot-dark-keys-retired-7703.test.ts | 4 - .../chatbot-display-mode-retired.test.ts | 4 - ...-registration-authoring-faces-7655.test.ts | 4 - .../checkbox-wrapper-class-6938.test.ts | 4 - .../classname-style-describe-7578.test.ts | 4 - .../classname-style-props-rename-5928.test.ts | 4 - ...ombobox-default-value-retired-8140.test.ts | 4 - ...nent-input-retired-constraint-keys.test.ts | 4 - .../component-input-retired-keys-7493.test.ts | 4 - .../component-meta-single-declaration.test.ts | 4 - .../__tests__/crud-retirement-5373.test.ts | 4 - ...hboard-aria-retired-contract-twins.test.ts | 4 - .../src/__tests__/dashboard-config.test.ts | 4 - ...ard-widget-slot-component-arm-7952.test.ts | 4 - .../dashboard-widget-strict-6002.test.ts | 4 - .../data-table-declared-keys-6882.test.ts | 4 - .../data-table-toolbar-retired.test.ts | 4 - ...lt-children-retired-contract-twins.test.ts | 4 - .../default-view-agenda-retired.test.ts | 4 - .../disabled-twin-symmetry-7087.test.ts | 4 - .../drill-down-config-mirror-7352.test.ts | 4 - .../export-options-spec-parity.test.ts | 4 - .../filter-builder-condition-id-8415.test.ts | 4 - .../filter-builder-mirror-6939.test.ts | 4 - .../flex-props-envelope-lift-6751.test.ts | 4 - ...ating-chatbot-trigger-icon-retired.test.ts | 4 - .../form-field-widget-namespace.test.ts | 4 - .../__tests__/form-field-zod-coverage.test.ts | 4 - .../src/__tests__/gantt-declared-keys.test.ts | 4 - ...-dependency-field-deprecated-alias.test.ts | 4 - .../gantt-flat-config-declared-keys.test.ts | 4 - .../gantt-view-mode-declared.test.ts | 4 - ...-columns-breakpoint-narrowing-8505.test.ts | 4 - .../handler-keys-json-refusal-6124.test.ts | 4 - ...ndler-keys-string-any-mirrors-7344.test.ts | 4 - .../src/__tests__/icon-key-migration.test.ts | 4 - .../kanban-conditional-formatting.test.ts | 4 - ...-plugin-dialect-authoritative-7664.test.ts | 4 - .../__tests__/list-view-spec-parity.test.ts | 4 - .../markdown-inert-keys-retired-6972.test.ts | 4 - .../src/__tests__/menu-item-union.test.ts | 4 - .../src/__tests__/navigation-model.test.ts | 4 - .../__tests__/navigation-spec-parity.test.ts | 4 - .../node-recursion-point-8344.test.ts | 67 +++-- ...object-calendar-record-source-7313.test.ts | 4 - ...t-grid-export-options-refusal-7762.test.ts | 4 - .../object-grid-title-mirrored.test.ts | 4 - .../object-kanban-group-by-limit-7322.test.ts | 4 - .../object-kanban-record-source-7780.test.ts | 4 - .../__tests__/object-view-spec-parity.test.ts | 4 - .../object-view-unmirrored-keys-7779.test.ts | 4 - ...ctql-record-source-refinement-6939.test.ts | 4 - .../objectql-union-arms-7363.test.ts | 4 - .../overlay-trigger-union-7081.test.ts | 4 - .../owner-retired-contract-twins.test.ts | 4 - .../src/__tests__/p1-spec-alignment.test.ts | 4 - .../page-app-dashboard-spec-parity.test.ts | 4 - .../page-nav-misc-spec-parity.test.ts | 4 - .../src/__tests__/phase2-schemas.test.ts | 4 - .../report-chart-query-spec-parity.test.ts | 4 - .../report-schema-authoring-face.test.ts | 4 - .../schema-registry-chatbot-keys-7704.test.ts | 4 - .../select-option-spec-parity.test.ts | 4 - .../__tests__/spec-subschema-parity.test.ts | 4 - .../static-table-narrow-surface.test.ts | 4 - .../table-column-type-canonical.test.ts | 4 - .../__tests__/text-value-retired-6951.test.ts | 4 - .../timeline-catalog-fixture-migrated.test.ts | 4 - .../__tests__/timeline-declared-keys.test.ts | 4 - .../timeline-items-bar-shape-7365.test.ts | 4 - .../timeline-items-row-shape-7164.test.ts | 4 - .../timeline-timescale-retired.test.ts | 4 - .../src/__tests__/toast-button-keys.test.ts | 4 - .../toggle-group-item-authorable-keys.test.ts | 4 - .../tree-view-data-optional-6939.test.ts | 4 - .../tree-view-data-retired-6951.test.ts | 4 - .../undeclared-but-consumed-keys-6150.test.ts | 4 - .../widget-input-control-vocabulary.test.ts | 4 - .../widget-schema-anchors-6576.test.ts | 4 - .../wrapper-class-declared-7722.test.ts | 4 - .../zod-lazy-getter-identity-7918.test.ts | 38 +-- ...od-mirror-authors-no-defaults-7735.test.ts | 4 - .../src/__tests__/zod-mirror-parity.test.ts | 4 - packages/types/src/zod/base.zod.ts | 231 ++++++++++++------ packages/types/src/zod/index.zod.ts | 31 +-- 105 files changed, 240 insertions(+), 566 deletions(-) diff --git a/.changeset/8344-node-recursion-point-redirect.md b/.changeset/8344-node-recursion-point-redirect.md index a3cb305fcb..a5e70c76b3 100644 --- a/.changeset/8344-node-recursion-point-redirect.md +++ b/.changeset/8344-node-recursion-point-redirect.md @@ -62,21 +62,26 @@ none. The published `ChatbotSchema` is untouched — whether its own `body` shou chat API's params is a separate question, recorded on objectui#8572 and deliberately not decided here. -**3. The redirect survives BUNDLING, and that is a property of how it is wired.** The arm -is an import binding read inside `SchemaNodeSchema`'s `z.lazy` getter, ⛔ not a write into -a live option array from the barrel's body. The difference is measurable, on this repo's -own Vite/rollup lib build, with an entry that imports only `CardSchema` from -`@object-ui/types/zod`: with the write, the bundler dropped it and a nested off-spec node -was ACCEPTED — the pre-redirect accept set, with no error and no warning; with the -binding, the same entry REFUSES it, because whatever retains `SchemaNodeSchema` retains -the union it names. ⚠️ The cost is real and is stated rather than hidden: that entry grows -from 113,887 to 342,193 bytes gzipped, and this repo's own console `framework` chunk from -72,248 to 90,969 (its ceiling is 100,000). That is the price of the redirect being real -for bundled consumers rather than a promise that depends on a bundler flag. +**3. ⚠️ KNOWN GAP, stated rather than papered over: the redirect can still be tree-shaken away +for a bundled consumer.** This package declares `"sideEffects": false` and the arm is filled by +a statement in the `./zod` barrel's body, so a bundler that honours the flag and sees no +reference to `AnyComponentSchema` may drop the fill — and then every child slot validates with +the PRE-redirect arm, with no error and no warning. Measured on this repo's own Vite/rollup lib +build: an entry importing only `CardSchema` ACCEPTS a nested off-spec node (370,652 bytes, no +fill in the output); the same entry with `AnyComponentSchema` also imported REFUSES it +(1,149,749 bytes, fill present). -⚠️ `"sideEffects": false` is unchanged and stays true: with the write gone, this package -performs no load-time side effect at all. ⚠️ One cost is paid inside this repo: a test -that entered the zod graph at a category module rather than at the `./zod` barrel now -throws `ReferenceError` at import instead of silently validating against the old arm, and -102 `packages/types` test files took a one-line barrel-first import because of it. -Consumers cannot hit that: `./zod` is the package's only zod subpath. +⛔ It is NOT closed here, and the reason is measured rather than argued. The route that closes +it by binding the union inside `SchemaNodeSchema`'s `z.lazy` getter was implemented and pushed, +and CI refused it: `Build Docs` failed with `ReferenceError: Cannot access 'BaseSchema' before +initialization` out of `packages/types/dist/zod/app.zod.js`, because that import makes +`base.zod.ts` depend on the barrel and a bundler is free to evaluate the resulting cycle +category-module-first. Reproduced locally in one line — importing `dist/zod/app.zod.js` throws +with the binding in place and loads clean without it. The other three candidates were measured +too: a narrowed `sideEffects` array is not a legal declaration for this package (one gate +requires every entry form to be named, another refuses a named entry with no load-time effect, +and this package's entry forms are pure), a bare top-level call is dropped by the same flag, +and dropping the flag costs 16,078 gzipped bytes on the console `framework` chunk and moves a +workspace census a guard pins. ⇒ until a route survives CI, a consumer that bundles +`@object-ui/types/zod` should keep `AnyComponentSchema` in its import graph, which is enough to +make the redirect apply. diff --git a/packages/types/src/__tests__/accordion-item-authorable-keys.test.ts b/packages/types/src/__tests__/accordion-item-authorable-keys.test.ts index 1f65a6fdb7..574973f494 100644 --- a/packages/types/src/__tests__/accordion-item-authorable-keys.test.ts +++ b/packages/types/src/__tests__/accordion-item-authorable-keys.test.ts @@ -47,10 +47,6 @@ * so re-adding `icon?` to the interface fails the build on the unused directive. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { AccordionItem } from '../disclosure'; import { AccordionItemSchema } from '../zod/disclosure.zod'; diff --git a/packages/types/src/__tests__/action-callback-retired-7068.test.ts b/packages/types/src/__tests__/action-callback-retired-7068.test.ts index d1edba6c96..444baf2e08 100644 --- a/packages/types/src/__tests__/action-callback-retired-7068.test.ts +++ b/packages/types/src/__tests__/action-callback-retired-7068.test.ts @@ -51,10 +51,6 @@ * assertions are erased before it runs. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { readFileSync, readdirSync, statSync } from 'node:fs'; import { join, relative, resolve } from 'node:path'; import { describe, it, expect } from 'vitest'; diff --git a/packages/types/src/__tests__/alert-dialog-read-dialect-7104.test.ts b/packages/types/src/__tests__/alert-dialog-read-dialect-7104.test.ts index c765461f36..6817845ca6 100644 --- a/packages/types/src/__tests__/alert-dialog-read-dialect-7104.test.ts +++ b/packages/types/src/__tests__/alert-dialog-read-dialect-7104.test.ts @@ -69,10 +69,6 @@ * renderer and that one cannot see the mirror's shape. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, expect, it } from 'vitest'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; diff --git a/packages/types/src/__tests__/any-component-union-fanout.test.ts b/packages/types/src/__tests__/any-component-union-fanout.test.ts index 21f5d7d885..f5a2120138 100644 --- a/packages/types/src/__tests__/any-component-union-fanout.test.ts +++ b/packages/types/src/__tests__/any-component-union-fanout.test.ts @@ -42,10 +42,6 @@ * `node-recursion-point-8344.test.ts`, where the linear-growth reading lives. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { AnyComponentSchema, safeValidateSchema } from '../zod/index.zod.js'; diff --git a/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts b/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts index f87f9e2374..8460690a12 100644 --- a/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts +++ b/packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts @@ -36,10 +36,6 @@ * the clause whose truth this card measured. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { AppActionSchema, MenuItemSchema } from '../zod/app.zod'; diff --git a/packages/types/src/__tests__/app-hidden-catalogue-flag-7542.test.ts b/packages/types/src/__tests__/app-hidden-catalogue-flag-7542.test.ts index 2b047ae4f6..93b7b39b75 100644 --- a/packages/types/src/__tests__/app-hidden-catalogue-flag-7542.test.ts +++ b/packages/types/src/__tests__/app-hidden-catalogue-flag-7542.test.ts @@ -70,10 +70,6 @@ * this node would have read as "not hidden" without a sound. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; diff --git a/packages/types/src/__tests__/base-bind-declared.test.ts b/packages/types/src/__tests__/base-bind-declared.test.ts index 67ba9bfafb..871a601404 100644 --- a/packages/types/src/__tests__/base-bind-declared.test.ts +++ b/packages/types/src/__tests__/base-bind-declared.test.ts @@ -90,10 +90,6 @@ * before this declaration existed, via the index signature and `.passthrough()`. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/base-schema-hidden-predicate.test.ts b/packages/types/src/__tests__/base-schema-hidden-predicate.test.ts index 251b2cfb43..673f4f24a4 100644 --- a/packages/types/src/__tests__/base-schema-hidden-predicate.test.ts +++ b/packages/types/src/__tests__/base-schema-hidden-predicate.test.ts @@ -76,10 +76,6 @@ * why this was ruled rather than applied mechanically. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { BaseSchema } from '../base'; import { BaseSchema as Mirror } from '../zod/base.zod'; diff --git a/packages/types/src/__tests__/base-schema-predicate-envelope-7530.test.ts b/packages/types/src/__tests__/base-schema-predicate-envelope-7530.test.ts index 3e8d2a5f6a..e03d8e7ffd 100644 --- a/packages/types/src/__tests__/base-schema-predicate-envelope-7530.test.ts +++ b/packages/types/src/__tests__/base-schema-predicate-envelope-7530.test.ts @@ -79,10 +79,6 @@ * object arm of `boolean | string` does not exist. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { z } from 'zod'; import type { BaseSchema } from '../base'; diff --git a/packages/types/src/__tests__/base-schema-zod-mirror-parity.test.ts b/packages/types/src/__tests__/base-schema-zod-mirror-parity.test.ts index abcc5b526b..0311c660bc 100644 --- a/packages/types/src/__tests__/base-schema-zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/base-schema-zod-mirror-parity.test.ts @@ -58,10 +58,6 @@ * five keys were demonstrably narrow. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { BaseSchema as Mirror } from '../zod/base.zod.js'; diff --git a/packages/types/src/__tests__/block-family-retired-4895.test.ts b/packages/types/src/__tests__/block-family-retired-4895.test.ts index 26d688cfd3..c80dcf97bd 100644 --- a/packages/types/src/__tests__/block-family-retired-4895.test.ts +++ b/packages/types/src/__tests__/block-family-retired-4895.test.ts @@ -26,10 +26,6 @@ * the five discriminants — is pinned in `phase2-schemas.test.ts`, next to the * theme refusals. This file pins the SYMBOLS; that one pins the BEHAVIOUR. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; /** Names that lived in `../blocks.ts`. */ diff --git a/packages/types/src/__tests__/button-group-doc-surface-6347.test.ts b/packages/types/src/__tests__/button-group-doc-surface-6347.test.ts index 05765ec23b..8caf19d157 100644 --- a/packages/types/src/__tests__/button-group-doc-surface-6347.test.ts +++ b/packages/types/src/__tests__/button-group-doc-surface-6347.test.ts @@ -102,10 +102,6 @@ * verification population, fenced off by PR #6345) and are asserted present. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, expect, it } from 'vitest'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; diff --git a/packages/types/src/__tests__/calendar-view-mode-agenda-retired.test.ts b/packages/types/src/__tests__/calendar-view-mode-agenda-retired.test.ts index 2bba7a308f..a5319c755e 100644 --- a/packages/types/src/__tests__/calendar-view-mode-agenda-retired.test.ts +++ b/packages/types/src/__tests__/calendar-view-mode-agenda-retired.test.ts @@ -36,10 +36,6 @@ * (`calendar-view-renderer.propsContract.test.tsx` pins that branch). */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { CalendarViewModeSchema, CalendarViewSchema } from '../zod/complex.zod.js'; import type { CalendarViewMode } from '../complex.js'; diff --git a/packages/types/src/__tests__/chart-data-model-7113.test.ts b/packages/types/src/__tests__/chart-data-model-7113.test.ts index 8ca23adaa1..638f429b06 100644 --- a/packages/types/src/__tests__/chart-data-model-7113.test.ts +++ b/packages/types/src/__tests__/chart-data-model-7113.test.ts @@ -67,10 +67,6 @@ * while `BaseSchema` passes through. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/chart-inline-data-retired.test.ts b/packages/types/src/__tests__/chart-inline-data-retired.test.ts index 4ef7a7bc72..6910000965 100644 --- a/packages/types/src/__tests__/chart-inline-data-retired.test.ts +++ b/packages/types/src/__tests__/chart-inline-data-retired.test.ts @@ -49,10 +49,6 @@ * declaration fails the build on the unused directive. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import type { ChartDataSeries, ChartSchema } from '../data-display'; diff --git a/packages/types/src/__tests__/chart-series-chart-type-alias-refusal-7694.test.ts b/packages/types/src/__tests__/chart-series-chart-type-alias-refusal-7694.test.ts index 9bb0cc7150..45bd1ed93b 100644 --- a/packages/types/src/__tests__/chart-series-chart-type-alias-refusal-7694.test.ts +++ b/packages/types/src/__tests__/chart-series-chart-type-alias-refusal-7694.test.ts @@ -68,10 +68,6 @@ * the arm and the spec's posture, not this change. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { ChartSeriesSchema as SpecChartSeriesSchema } from '@objectstack/spec/ui'; diff --git a/packages/types/src/__tests__/chart-series-keys-7546.test.ts b/packages/types/src/__tests__/chart-series-keys-7546.test.ts index fc438c8469..2c1ed226a4 100644 --- a/packages/types/src/__tests__/chart-series-keys-7546.test.ts +++ b/packages/types/src/__tests__/chart-series-keys-7546.test.ts @@ -72,10 +72,6 @@ * That card is objectui#7694, and it took the refusal — see block (d). */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { ChartDataSeries } from '../data-display'; import { ChartDataSeriesSchema } from '../zod/data-display.zod'; diff --git a/packages/types/src/__tests__/chat-message-avatar-keys-7295.test.ts b/packages/types/src/__tests__/chat-message-avatar-keys-7295.test.ts index 76994ebee7..38df76c4cc 100644 --- a/packages/types/src/__tests__/chat-message-avatar-keys-7295.test.ts +++ b/packages/types/src/__tests__/chat-message-avatar-keys-7295.test.ts @@ -55,10 +55,6 @@ * the rebuilt types dist, not here — `@object-ui/types` has no dependency on * the plugin and must not gain one. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/chatbot-authoring-face-keys.test.ts b/packages/types/src/__tests__/chatbot-authoring-face-keys.test.ts index 7f6ea8d107..a6cb18cb39 100644 --- a/packages/types/src/__tests__/chatbot-authoring-face-keys.test.ts +++ b/packages/types/src/__tests__/chatbot-authoring-face-keys.test.ts @@ -51,10 +51,6 @@ * Mirroring the field is what turns that into a refusal. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { ChatbotSchema, ChatMessage } from '../complex'; import { ChatbotSchema as ChatbotZodSchema } from '../zod/complex.zod'; diff --git a/packages/types/src/__tests__/chatbot-dark-keys-retired-7703.test.ts b/packages/types/src/__tests__/chatbot-dark-keys-retired-7703.test.ts index 4ca7553317..f79178b447 100644 --- a/packages/types/src/__tests__/chatbot-dark-keys-retired-7703.test.ts +++ b/packages/types/src/__tests__/chatbot-dark-keys-retired-7703.test.ts @@ -80,10 +80,6 @@ * it runs. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ChatbotSchema as ChatbotZod, diff --git a/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts b/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts index a8374652b0..a237f25973 100644 --- a/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts +++ b/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts @@ -60,10 +60,6 @@ * directive, so the contrast cannot rot into prose. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { ChatMessage, diff --git a/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts b/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts index 8b1c2a3e36..bf078e91b1 100644 --- a/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts +++ b/packages/types/src/__tests__/chatbot-registration-authoring-faces-7655.test.ts @@ -69,10 +69,6 @@ * (objectui#7703, `__tests__/chatbot-dark-keys-retired-7703.test.ts`). */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { BaseSchema } from '../base'; import type { diff --git a/packages/types/src/__tests__/checkbox-wrapper-class-6938.test.ts b/packages/types/src/__tests__/checkbox-wrapper-class-6938.test.ts index 8ed4dea66f..dba76dee11 100644 --- a/packages/types/src/__tests__/checkbox-wrapper-class-6938.test.ts +++ b/packages/types/src/__tests__/checkbox-wrapper-class-6938.test.ts @@ -39,10 +39,6 @@ * faces. That is the half that keeps this from being a widening: the change * declares the one key the renderer honours and nothing else. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/classname-style-describe-7578.test.ts b/packages/types/src/__tests__/classname-style-describe-7578.test.ts index 004aaea1f4..70e3ba3bbb 100644 --- a/packages/types/src/__tests__/classname-style-describe-7578.test.ts +++ b/packages/types/src/__tests__/classname-style-describe-7578.test.ts @@ -42,10 +42,6 @@ * reddens here too instead of passing as "the string changed". */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; // The PUBLISHED path — `@object-ui/types/zod` resolves to this barrel. The diff --git a/packages/types/src/__tests__/classname-style-props-rename-5928.test.ts b/packages/types/src/__tests__/classname-style-props-rename-5928.test.ts index 1f8a213904..7d2af06f48 100644 --- a/packages/types/src/__tests__/classname-style-props-rename-5928.test.ts +++ b/packages/types/src/__tests__/classname-style-props-rename-5928.test.ts @@ -52,10 +52,6 @@ * name this const actually carries. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; // The PUBLISHED path (`@object-ui/types/zod` resolves to this barrel), deliberately diff --git a/packages/types/src/__tests__/combobox-default-value-retired-8140.test.ts b/packages/types/src/__tests__/combobox-default-value-retired-8140.test.ts index c60eeb9873..05639309ef 100644 --- a/packages/types/src/__tests__/combobox-default-value-retired-8140.test.ts +++ b/packages/types/src/__tests__/combobox-default-value-retired-8140.test.ts @@ -43,10 +43,6 @@ * fixture parses GREEN, while `defaultValue` is refused by name. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { ComboboxSchema as TsComboboxSchema } from '../form'; import { ComboboxSchema } from '../zod/form.zod'; diff --git a/packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts b/packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts index bd3435940d..6a8c327d49 100644 --- a/packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts +++ b/packages/types/src/__tests__/component-input-retired-constraint-keys.test.ts @@ -72,10 +72,6 @@ * declaration fails the build on the unused directive. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { ComponentInput } from '../base'; import { ComponentInputSchema } from '../zod/base.zod'; diff --git a/packages/types/src/__tests__/component-input-retired-keys-7493.test.ts b/packages/types/src/__tests__/component-input-retired-keys-7493.test.ts index 392caf519e..7bd071bc48 100644 --- a/packages/types/src/__tests__/component-input-retired-keys-7493.test.ts +++ b/packages/types/src/__tests__/component-input-retired-keys-7493.test.ts @@ -57,10 +57,6 @@ * declaration fails the build on the unused directive. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { readFileSync, readdirSync, statSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { describe, it, expect } from 'vitest'; diff --git a/packages/types/src/__tests__/component-meta-single-declaration.test.ts b/packages/types/src/__tests__/component-meta-single-declaration.test.ts index 992ed83a09..55bf809f66 100644 --- a/packages/types/src/__tests__/component-meta-single-declaration.test.ts +++ b/packages/types/src/__tests__/component-meta-single-declaration.test.ts @@ -60,10 +60,6 @@ * `package-exports-manifest.test.ts` record, same resolution. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { describe, it, expect } from 'vitest'; diff --git a/packages/types/src/__tests__/crud-retirement-5373.test.ts b/packages/types/src/__tests__/crud-retirement-5373.test.ts index 4e861f5922..93ece704b0 100644 --- a/packages/types/src/__tests__/crud-retirement-5373.test.ts +++ b/packages/types/src/__tests__/crud-retirement-5373.test.ts @@ -32,10 +32,6 @@ * `@object-ui/core`'s `schema-validator.test.ts`, and the builder face in its * `schema-builder.test.ts`. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import * as zodBarrel from '../zod/index.zod.js'; diff --git a/packages/types/src/__tests__/dashboard-aria-retired-contract-twins.test.ts b/packages/types/src/__tests__/dashboard-aria-retired-contract-twins.test.ts index 95ba99a7a3..8c14cb7f43 100644 --- a/packages/types/src/__tests__/dashboard-aria-retired-contract-twins.test.ts +++ b/packages/types/src/__tests__/dashboard-aria-retired-contract-twins.test.ts @@ -34,10 +34,6 @@ * `type-check` script (#3009). */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { DashboardComponentSchema } from '../complex'; import { DashboardComponentSchema as DashboardComponentZodSchema } from '../zod/index.zod'; diff --git a/packages/types/src/__tests__/dashboard-config.test.ts b/packages/types/src/__tests__/dashboard-config.test.ts index 082e811025..48ab5f4abd 100644 --- a/packages/types/src/__tests__/dashboard-config.test.ts +++ b/packages/types/src/__tests__/dashboard-config.test.ts @@ -9,10 +9,6 @@ /** * Tests for DashboardConfig types and Zod validation schemas. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { DashboardConfig, diff --git a/packages/types/src/__tests__/dashboard-widget-slot-component-arm-7952.test.ts b/packages/types/src/__tests__/dashboard-widget-slot-component-arm-7952.test.ts index 2168a537fb..2639b19d22 100644 --- a/packages/types/src/__tests__/dashboard-widget-slot-component-arm-7952.test.ts +++ b/packages/types/src/__tests__/dashboard-widget-slot-component-arm-7952.test.ts @@ -52,10 +52,6 @@ * the lines marked REVERSE below and nowhere else in this file. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { DashboardComponentSchema, diff --git a/packages/types/src/__tests__/dashboard-widget-strict-6002.test.ts b/packages/types/src/__tests__/dashboard-widget-strict-6002.test.ts index 0c37bd3a4a..89ff0fd1dc 100644 --- a/packages/types/src/__tests__/dashboard-widget-strict-6002.test.ts +++ b/packages/types/src/__tests__/dashboard-widget-strict-6002.test.ts @@ -39,10 +39,6 @@ * `examples/schema-catalog/test/plugin-dashboard-component-schema.test.ts`. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { BaseSchema } from '../zod/base.zod.js'; import { diff --git a/packages/types/src/__tests__/data-table-declared-keys-6882.test.ts b/packages/types/src/__tests__/data-table-declared-keys-6882.test.ts index 40f2eb9259..120d804d68 100644 --- a/packages/types/src/__tests__/data-table-declared-keys-6882.test.ts +++ b/packages/types/src/__tests__/data-table-declared-keys-6882.test.ts @@ -56,10 +56,6 @@ * now-unused directive rather than quietly passing. That is the property the * positive assertions borrow their meaning from. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { DataTableSchema } from '../data-display.js'; import fs from 'node:fs'; diff --git a/packages/types/src/__tests__/data-table-toolbar-retired.test.ts b/packages/types/src/__tests__/data-table-toolbar-retired.test.ts index 96cc9c7f7b..d9ac8c96e2 100644 --- a/packages/types/src/__tests__/data-table-toolbar-retired.test.ts +++ b/packages/types/src/__tests__/data-table-toolbar-retired.test.ts @@ -48,10 +48,6 @@ * mirror ever accepted. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { DataTableSchema } from '../zod/data-display.zod.js'; import type { DataTableSchema as DataTableSchemaTS, TableColumn } from '../data-display.js'; diff --git a/packages/types/src/__tests__/default-children-retired-contract-twins.test.ts b/packages/types/src/__tests__/default-children-retired-contract-twins.test.ts index bde2d02ef5..1c464658a7 100644 --- a/packages/types/src/__tests__/default-children-retired-contract-twins.test.ts +++ b/packages/types/src/__tests__/default-children-retired-contract-twins.test.ts @@ -61,10 +61,6 @@ * `type-check` script (#3009). */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ComponentMetaSchema } from '../zod/base.zod.js'; import type { ComponentMeta } from '../base.js'; diff --git a/packages/types/src/__tests__/default-view-agenda-retired.test.ts b/packages/types/src/__tests__/default-view-agenda-retired.test.ts index 5d2b4564dd..2cedf3958c 100644 --- a/packages/types/src/__tests__/default-view-agenda-retired.test.ts +++ b/packages/types/src/__tests__/default-view-agenda-retired.test.ts @@ -42,10 +42,6 @@ * boundary. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ObjectCalendarSchema as ObjectCalendarZodSchema, diff --git a/packages/types/src/__tests__/disabled-twin-symmetry-7087.test.ts b/packages/types/src/__tests__/disabled-twin-symmetry-7087.test.ts index 5c4426a544..5a854781af 100644 --- a/packages/types/src/__tests__/disabled-twin-symmetry-7087.test.ts +++ b/packages/types/src/__tests__/disabled-twin-symmetry-7087.test.ts @@ -61,10 +61,6 @@ * The measured counts are in the PR that landed this file. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, expect, it } from 'vitest'; import type { ZodType } from 'zod'; diff --git a/packages/types/src/__tests__/drill-down-config-mirror-7352.test.ts b/packages/types/src/__tests__/drill-down-config-mirror-7352.test.ts index aeae9cf55a..0bfcd8debd 100644 --- a/packages/types/src/__tests__/drill-down-config-mirror-7352.test.ts +++ b/packages/types/src/__tests__/drill-down-config-mirror-7352.test.ts @@ -36,10 +36,6 @@ * validator refuse what the published TypeScript declares — the class this * card closes, in the other direction. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { z } from 'zod'; import { diff --git a/packages/types/src/__tests__/export-options-spec-parity.test.ts b/packages/types/src/__tests__/export-options-spec-parity.test.ts index 6bbf18dceb..fa02dd681e 100644 --- a/packages/types/src/__tests__/export-options-spec-parity.test.ts +++ b/packages/types/src/__tests__/export-options-spec-parity.test.ts @@ -46,10 +46,6 @@ * `ListViewSchema` type derived from it. Both are `@object-ui/types` surfaces. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ListViewSchema as SpecListViewSchema } from '@objectstack/spec/ui'; import { ListViewSchema as MirrorListViewSchema } from '../zod/objectql.zod.js'; diff --git a/packages/types/src/__tests__/filter-builder-condition-id-8415.test.ts b/packages/types/src/__tests__/filter-builder-condition-id-8415.test.ts index c50176606d..3d1c1ffa59 100644 --- a/packages/types/src/__tests__/filter-builder-condition-id-8415.test.ts +++ b/packages/types/src/__tests__/filter-builder-condition-id-8415.test.ts @@ -46,10 +46,6 @@ * every affordance on it acts on all the id-less rows at once. Nothing that * works stops working. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/filter-builder-mirror-6939.test.ts b/packages/types/src/__tests__/filter-builder-mirror-6939.test.ts index 0a092bb5f8..831832391c 100644 --- a/packages/types/src/__tests__/filter-builder-mirror-6939.test.ts +++ b/packages/types/src/__tests__/filter-builder-mirror-6939.test.ts @@ -71,10 +71,6 @@ * this mirror ALSO refuses, while the canonical spellings it accepts render a * blank operator trigger) and needs its own ruling. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/flex-props-envelope-lift-6751.test.ts b/packages/types/src/__tests__/flex-props-envelope-lift-6751.test.ts index 739be3e920..a4d6a06f88 100644 --- a/packages/types/src/__tests__/flex-props-envelope-lift-6751.test.ts +++ b/packages/types/src/__tests__/flex-props-envelope-lift-6751.test.ts @@ -44,10 +44,6 @@ * for that reason: a repo-wide "no node carries `props`" assertion would make * the teaching material fail. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; diff --git a/packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts b/packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts index ae660f5c9d..369e41b52c 100644 --- a/packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts +++ b/packages/types/src/__tests__/floating-chatbot-trigger-icon-retired.test.ts @@ -51,10 +51,6 @@ * NOT evidence about them — type assertions are erased before it runs. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { ChatbotFloatingSchema as TsChatbotFloatingSchema, diff --git a/packages/types/src/__tests__/form-field-widget-namespace.test.ts b/packages/types/src/__tests__/form-field-widget-namespace.test.ts index 33f40f7818..80c9269c97 100644 --- a/packages/types/src/__tests__/form-field-widget-namespace.test.ts +++ b/packages/types/src/__tests__/form-field-widget-namespace.test.ts @@ -39,10 +39,6 @@ * an author who checks gets a YES and still gets a text box on the field path. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { FormFieldSchema, FormSchema } from '../zod/form.zod.js'; import { safeValidateSchema } from '../zod/index.zod.js'; diff --git a/packages/types/src/__tests__/form-field-zod-coverage.test.ts b/packages/types/src/__tests__/form-field-zod-coverage.test.ts index ac9da57d84..850d3f557e 100644 --- a/packages/types/src/__tests__/form-field-zod-coverage.test.ts +++ b/packages/types/src/__tests__/form-field-zod-coverage.test.ts @@ -27,10 +27,6 @@ * the reason its own note records (objectui#6609). */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { FieldConstraintsSchema, FormFieldSchema } from '../zod/form.zod.js'; diff --git a/packages/types/src/__tests__/gantt-declared-keys.test.ts b/packages/types/src/__tests__/gantt-declared-keys.test.ts index b16d8b76fa..6b598c6e08 100644 --- a/packages/types/src/__tests__/gantt-declared-keys.test.ts +++ b/packages/types/src/__tests__/gantt-declared-keys.test.ts @@ -44,10 +44,6 @@ * this file, so that is real enforcement and not decoration (#3009). */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ObjectGanttSchema } from '../zod/objectql.zod.js'; import type { ObjectGanttSchema as ObjectGanttSchemaTS } from '../objectql.js'; diff --git a/packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts b/packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts index 4b08ac01b2..865ad3df40 100644 --- a/packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts +++ b/packages/types/src/__tests__/gantt-dependency-field-deprecated-alias.test.ts @@ -65,10 +65,6 @@ * rather than merely human-readable. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { describe, it, expect } from 'vitest'; diff --git a/packages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts b/packages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts index 8f6c50ad2c..6ff9a452b6 100644 --- a/packages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts +++ b/packages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts @@ -96,10 +96,6 @@ * declaration is removed. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ObjectGanttSchema } from '../zod/objectql.zod.js'; import type { GanttConfig, ObjectGanttSchema as ObjectGanttSchemaTS } from '../objectql.js'; diff --git a/packages/types/src/__tests__/gantt-view-mode-declared.test.ts b/packages/types/src/__tests__/gantt-view-mode-declared.test.ts index 327f2ff038..9e3f5a309f 100644 --- a/packages/types/src/__tests__/gantt-view-mode-declared.test.ts +++ b/packages/types/src/__tests__/gantt-view-mode-declared.test.ts @@ -35,10 +35,6 @@ * semantics). */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { GanttConfigSchema as SpecGanttConfigSchema } from '@objectstack/spec/ui'; import { ObjectGanttSchema } from '../zod/objectql.zod.js'; diff --git a/packages/types/src/__tests__/grid-columns-breakpoint-narrowing-8505.test.ts b/packages/types/src/__tests__/grid-columns-breakpoint-narrowing-8505.test.ts index 5e5bf063e1..90de60de80 100644 --- a/packages/types/src/__tests__/grid-columns-breakpoint-narrowing-8505.test.ts +++ b/packages/types/src/__tests__/grid-columns-breakpoint-narrowing-8505.test.ts @@ -77,10 +77,6 @@ * rot into a silent assumption that both faces closed together. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { GridSchema } from '../layout'; import type { BreakpointName } from '../mobile'; diff --git a/packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts b/packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts index b3af97241c..e1d8397635 100644 --- a/packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts +++ b/packages/types/src/__tests__/handler-keys-json-refusal-6124.test.ts @@ -69,10 +69,6 @@ * ruling's scope (Q4 → B) and the reason for the arm, not this change. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/handler-keys-string-any-mirrors-7344.test.ts b/packages/types/src/__tests__/handler-keys-string-any-mirrors-7344.test.ts index d914cf16d6..e69d766f8c 100644 --- a/packages/types/src/__tests__/handler-keys-string-any-mirrors-7344.test.ts +++ b/packages/types/src/__tests__/handler-keys-string-any-mirrors-7344.test.ts @@ -112,10 +112,6 @@ * after — they pin the instrument, not this change. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { execFileSync } from 'node:child_process'; import { readdirSync, readFileSync } from 'node:fs'; diff --git a/packages/types/src/__tests__/icon-key-migration.test.ts b/packages/types/src/__tests__/icon-key-migration.test.ts index 82fde0f307..73f96fe53e 100644 --- a/packages/types/src/__tests__/icon-key-migration.test.ts +++ b/packages/types/src/__tests__/icon-key-migration.test.ts @@ -19,10 +19,6 @@ * the two cases it deliberately refuses to guess at. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { IconSchema } from '../zod/layout.zod.js'; diff --git a/packages/types/src/__tests__/kanban-conditional-formatting.test.ts b/packages/types/src/__tests__/kanban-conditional-formatting.test.ts index 004be7d989..10a2229684 100644 --- a/packages/types/src/__tests__/kanban-conditional-formatting.test.ts +++ b/packages/types/src/__tests__/kanban-conditional-formatting.test.ts @@ -14,10 +14,6 @@ * `{ field, operator, value }` shape OR the spec `{ condition, style }` CEL * shape. This locks both so the two can't drift back apart. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ObjectKanbanSchema } from '../zod/index.zod'; import type { KanbanConditionalFormattingRule } from '../objectql'; diff --git a/packages/types/src/__tests__/kanban-plugin-dialect-authoritative-7664.test.ts b/packages/types/src/__tests__/kanban-plugin-dialect-authoritative-7664.test.ts index e70ed73a30..e6348a84d5 100644 --- a/packages/types/src/__tests__/kanban-plugin-dialect-authoritative-7664.test.ts +++ b/packages/types/src/__tests__/kanban-plugin-dialect-authoritative-7664.test.ts @@ -78,10 +78,6 @@ * this dialect — and renders. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/list-view-spec-parity.test.ts b/packages/types/src/__tests__/list-view-spec-parity.test.ts index 8cfd97e755..6f48fcbeef 100644 --- a/packages/types/src/__tests__/list-view-spec-parity.test.ts +++ b/packages/types/src/__tests__/list-view-spec-parity.test.ts @@ -30,10 +30,6 @@ * field belongs upstream in `@objectstack/spec` (promote it) or is a genuine objectui-only * extension (add it to SANCTIONED_LOCAL with a rationale). See #2231. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ListViewSchema as SpecListViewSchema, diff --git a/packages/types/src/__tests__/markdown-inert-keys-retired-6972.test.ts b/packages/types/src/__tests__/markdown-inert-keys-retired-6972.test.ts index 94c2ca359c..e66476feb5 100644 --- a/packages/types/src/__tests__/markdown-inert-keys-retired-6972.test.ts +++ b/packages/types/src/__tests__/markdown-inert-keys-retired-6972.test.ts @@ -69,10 +69,6 @@ * NOT evidence about them — type assertions are erased before it runs. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { describe, it, expect } from 'vitest'; diff --git a/packages/types/src/__tests__/menu-item-union.test.ts b/packages/types/src/__tests__/menu-item-union.test.ts index 76f3915eda..3cc71766d1 100644 --- a/packages/types/src/__tests__/menu-item-union.test.ts +++ b/packages/types/src/__tests__/menu-item-union.test.ts @@ -44,10 +44,6 @@ * tombstone, not literal syntax, is what refuses it. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { MenuItem, MenuCommandItem, MenuDividerItem } from '../overlay'; import { MenuItemSchema } from '../zod/overlay.zod'; diff --git a/packages/types/src/__tests__/navigation-model.test.ts b/packages/types/src/__tests__/navigation-model.test.ts index e5c1da2f83..c87d4f7a8d 100644 --- a/packages/types/src/__tests__/navigation-model.test.ts +++ b/packages/types/src/__tests__/navigation-model.test.ts @@ -4,10 +4,6 @@ * Validates NavigationItem, NavigationArea types, Zod schemas, * and the AppMenuItem → NavigationItem transform. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { AppComponentSchema, diff --git a/packages/types/src/__tests__/navigation-spec-parity.test.ts b/packages/types/src/__tests__/navigation-spec-parity.test.ts index 14add61cc7..4120e9c6b2 100644 --- a/packages/types/src/__tests__/navigation-spec-parity.test.ts +++ b/packages/types/src/__tests__/navigation-spec-parity.test.ts @@ -36,10 +36,6 @@ * separately, not smuggled in here. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { NavigationItemSchema, NavigationAreaSchema } from '../zod/app.zod.js'; import { NavigationItemSchema as SpecNavigationItemSchema } from '@objectstack/spec/ui'; diff --git a/packages/types/src/__tests__/node-recursion-point-8344.test.ts b/packages/types/src/__tests__/node-recursion-point-8344.test.ts index 20701a7148..38f185f5da 100644 --- a/packages/types/src/__tests__/node-recursion-point-8344.test.ts +++ b/packages/types/src/__tests__/node-recursion-point-8344.test.ts @@ -35,13 +35,12 @@ * consequence ①: the exported wrapper identity is stable and survives through a * declared slot, and it is the ONE reading that holds for all ten recursive mirrors. * - * ⚠️ ⛔ Do not read that as a claim about THIS head's getter either way. The reading moved - * twice while this card was in flight, and what ships is the FIRST spelling again: the - * getter BUILDS the node union per call — it reads the `AnyComponentSchema` import binding - * and wraps it — so `getter() === getter()` and `S.unwrap() === S.unwrap()` are FALSE here - * exactly as they are on `main`, and `SchemaNodeSchema` stays `TDZ_BOUND` in - * `zod-lazy-getter-identity-7918.test.ts`. The intermediate revision that made this const - * `MEMOISED` is gone with the option-array write it belonged to. + * ⚠️ ⛔ Do not read that as "`unwrap()` and the getter are unstable HERE". On `main` they are + * — measured on the built face, `S.unwrap() === S.unwrap()` and `getter() === getter()` are + * both FALSE. On THIS head both are TRUE for this one const, because the redirect builds the + * node union once below `BaseSchemaCore` and the getter returns it: the row moves to + * `MEMOISED` in `zod-lazy-getter-identity-7918.test.ts`, as a byproduct rather than a goal. + * The `fill is LIVE` leg below works BECAUSE of that. * * ⇒ the discipline stands unchanged and for an unchanged reason: it must hold for * the seven mirrors that are still TDZ_BOUND, so a pin written through `.unwrap()` @@ -50,10 +49,6 @@ * makes this file portable to them; it is not a claim about this const's getter. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { AnyComponentSchema, CardSchema, IconSchema, SchemaNodeSchema, safeValidateSchema } from '../zod/index.zod.js'; @@ -125,10 +120,11 @@ describe('the arm IS the component union, and not the base shape', () => { describe('the late-binding wiring, read by IDENTITY on the exported wrapper', () => { it('the exported wrapper is one stable object', () => { - // objectui#7918 consequence ①, and it is measured on THIS head: `.unwrap()` and the - // `z.lazy` getter each return a FRESH object per call, because the getter builds the - // node union around the imported component union every time. ⛔ Never write this pin - // through either of those. + // objectui#7918 consequence ①: the EXPORTED wrapper is the stable handle, and it is the + // one reading that holds for all ten recursive mirrors. ⛔ Never write this pin through + // `.unwrap()` or a re-invoked getter — on the seven mirrors that are still `TDZ_BOUND` + // those return a fresh object per call, and a pin written through them would compare two + // fresh objects and fail for a reason that has nothing to do with this contract. expect(SchemaNodeSchema).toBe(SchemaNodeSchema); }); @@ -137,41 +133,34 @@ describe('the late-binding wiring, read by IDENTITY on the exported wrapper', () expect(body._zod.def.innerType._zod.def.options).toContain(SchemaNodeSchema); }); - it('the component arm is REACHED by importing the barrel — the module cycle is broken', () => { - // The behavioural read of the wiring, and the only one that cannot pass vacuously: if - // the arm were `BaseSchemaCore` again, the unmirrored node below would be ACCEPTED. - // This module imports the barrel and nothing else, so a break in the binding lands - // here rather than in whichever suite happened to run second. + it('the holder is FILLED by importing the barrel — the module-cycle break works', () => { + // The behavioural read of the fill, and the only one that cannot pass vacuously: BEFORE + // the fill the arm is `BaseSchemaCore`, which accepts the unmirrored node below. This + // module imports the barrel and nothing else, so a break in `index.zod.ts`'s + // `defineNodeComponentUnion(...)` initializer lands here rather than in whichever suite + // happened to run second. expect(AnyComponentSchema.safeParse(nested({ type: 'h1' })).success).toBe(false); expect(AnyComponentSchema.safeParse(nested(LEGAL_ICON)).success).toBe(true); }); - it('the arm is the imported union itself, wrapped — not a copy and not the base shape', () => { - // objectui#8344's wiring is an IMPORT BINDING read inside the getter, so there is no - // option array to patch and no pre-fill window to freeze: whatever retains - // `SchemaNodeSchema` retains the union it names, in a module graph AND in a bundle. - // ⛔ Do not rewrite this as a holder the getter reads, and ⛔ do not restore the option - // slot the earlier revision wrote into — both were measured wrong, on this card. + it('the fill is LIVE, and slot 0 holds the WRAPPED union, not the bare one', () => { + // `z.union` re-reads its option array on every parse, so the recursion point is whatever + // slot 0 holds NOW — not whatever it held when some other file in this worker first + // parsed something (the unit project runs `isolate: false`, one module graph per worker). + // ⛔ Do not assert `toBe(AnyComponentSchema)` here: what is installed is deliberately the + // `superRefine` WRAPPER that keeps the `chatbot` arm from widening the node slot, and a + // pin on the bare union would go green the moment that narrowing was dropped. const arm = (SchemaNodeSchema as unknown as { _zod: { def: { getter: () => { _zod: { def: { options: readonly { _zod: { propValues?: Record< string, unknown >; def: { checks?: unknown[] } } }[] } } } } }; })._zod.def.getter()._zod.def.options[0]; - // it is the discriminated union objectui#8498 built — the discrimination survives the - // wrapper, which is what keeps a nested refusal costing one arm instead of 106 — + expect(arm).not.toBe(AnyComponentSchema); + // it is still the discriminated union objectui#8498 built — the discrimination survives + // the wrapper, which is what keeps a nested refusal costing one arm instead of 106 — expect(Object.keys(arm._zod.propValues ?? {})).toContain('type'); - // and it carries exactly the one check the chatbot narrowing adds. + // and it carries exactly the one check that narrowing adds. expect(arm._zod.def.checks).toHaveLength(1); }); - it('a graph that never evaluates the barrel throws LOUDLY rather than answering as `main`', () => { - // The property the earlier spelling could not have. Entering at a category module puts - // `BaseSchema` in its temporal dead zone, and an import binding read there throws at - // load. ⛔ That is the DESIRED behaviour: the alternative, measured on the revision - // this replaced, is a silent pre-#8344 accept set for anyone whose bundler dropped the - // write. Tests that enter graph-first must import the `./zod` barrel first; that is - // the whole cost, and it is paid in test files, never by a consumer of `./zod`. - expect(typeof SchemaNodeSchema).toBe('object'); - }); - }); /** diff --git a/packages/types/src/__tests__/object-calendar-record-source-7313.test.ts b/packages/types/src/__tests__/object-calendar-record-source-7313.test.ts index 122734e5f8..6af84d2f71 100644 --- a/packages/types/src/__tests__/object-calendar-record-source-7313.test.ts +++ b/packages/types/src/__tests__/object-calendar-record-source-7313.test.ts @@ -40,10 +40,6 @@ * performs is pinned off disk below, so a later rewrite of the ladder cannot * leave this declaration describing a read that no longer exists. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/object-grid-export-options-refusal-7762.test.ts b/packages/types/src/__tests__/object-grid-export-options-refusal-7762.test.ts index d0c2180da7..fc3fcceead 100644 --- a/packages/types/src/__tests__/object-grid-export-options-refusal-7762.test.ts +++ b/packages/types/src/__tests__/object-grid-export-options-refusal-7762.test.ts @@ -41,10 +41,6 @@ * simply stopped parsing anything could not pass this file. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { ListViewSchema as SpecListViewSchema } from '@objectstack/spec/ui'; diff --git a/packages/types/src/__tests__/object-grid-title-mirrored.test.ts b/packages/types/src/__tests__/object-grid-title-mirrored.test.ts index 3e019dd6d2..e986e0809f 100644 --- a/packages/types/src/__tests__/object-grid-title-mirrored.test.ts +++ b/packages/types/src/__tests__/object-grid-title-mirrored.test.ts @@ -38,10 +38,6 @@ * pass vacuously. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ObjectGridSchema } from '../zod/objectql.zod'; import type { ObjectGridSchema as TsObjectGridSchema } from '../objectql'; diff --git a/packages/types/src/__tests__/object-kanban-group-by-limit-7322.test.ts b/packages/types/src/__tests__/object-kanban-group-by-limit-7322.test.ts index 2b534c2506..024dcf5499 100644 --- a/packages/types/src/__tests__/object-kanban-group-by-limit-7322.test.ts +++ b/packages/types/src/__tests__/object-kanban-group-by-limit-7322.test.ts @@ -50,10 +50,6 @@ * sites are pinned OFF DISK below as a control — if one stops reading the alias * this file turns red, because the retirement's stated boundary moved. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/object-kanban-record-source-7780.test.ts b/packages/types/src/__tests__/object-kanban-record-source-7780.test.ts index cb80e9dbbe..38b70c2c92 100644 --- a/packages/types/src/__tests__/object-kanban-record-source-7780.test.ts +++ b/packages/types/src/__tests__/object-kanban-record-source-7780.test.ts @@ -66,10 +66,6 @@ * must not incidentally overturn that, so the `groupBy` half of the vector is * asserted here alongside the record-source half. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/object-view-spec-parity.test.ts b/packages/types/src/__tests__/object-view-spec-parity.test.ts index 5b3b24aadf..893689b3fd 100644 --- a/packages/types/src/__tests__/object-view-spec-parity.test.ts +++ b/packages/types/src/__tests__/object-view-spec-parity.test.ts @@ -47,10 +47,6 @@ * whether the field belongs upstream in `@objectstack/spec` (promote it) or is * a genuine objectui-only extension (add it with a rationale). See #2890. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ListViewSchema as SpecListViewSchema, diff --git a/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts b/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts index d841e3e256..4d2d1e08b1 100644 --- a/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts +++ b/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts @@ -58,10 +58,6 @@ * the renderer's read set moves) the measurement — and the stop — is re-taken * rather than remembered. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/objectql-record-source-refinement-6939.test.ts b/packages/types/src/__tests__/objectql-record-source-refinement-6939.test.ts index 4d130d5c81..464a6a5a75 100644 --- a/packages/types/src/__tests__/objectql-record-source-refinement-6939.test.ts +++ b/packages/types/src/__tests__/objectql-record-source-refinement-6939.test.ts @@ -39,10 +39,6 @@ * pin. The refinement's issue is checked by `path`, `params.code` and the three * key names in its message. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; diff --git a/packages/types/src/__tests__/objectql-union-arms-7363.test.ts b/packages/types/src/__tests__/objectql-union-arms-7363.test.ts index e54e531381..731e7d9951 100644 --- a/packages/types/src/__tests__/objectql-union-arms-7363.test.ts +++ b/packages/types/src/__tests__/objectql-union-arms-7363.test.ts @@ -32,10 +32,6 @@ * The TS face is pinned beside it: `ObjectQLComponentSchema` narrows to each * declaration by its discriminant, instead of to `never`. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { z } from 'zod'; import { safeValidateSchema, ObjectQLComponentSchema as ObjectQLComponentZod } from '../zod/index.zod.js'; diff --git a/packages/types/src/__tests__/overlay-trigger-union-7081.test.ts b/packages/types/src/__tests__/overlay-trigger-union-7081.test.ts index 2e8b2ce50c..0de9cc9af7 100644 --- a/packages/types/src/__tests__/overlay-trigger-union-7081.test.ts +++ b/packages/types/src/__tests__/overlay-trigger-union-7081.test.ts @@ -64,10 +64,6 @@ * Recorded on the PR. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, expect, it } from 'vitest'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; diff --git a/packages/types/src/__tests__/owner-retired-contract-twins.test.ts b/packages/types/src/__tests__/owner-retired-contract-twins.test.ts index 2e66fb7b58..8c3c7d42d4 100644 --- a/packages/types/src/__tests__/owner-retired-contract-twins.test.ts +++ b/packages/types/src/__tests__/owner-retired-contract-twins.test.ts @@ -38,10 +38,6 @@ * pass while the shrink had quietly invalidated the replacement idiom too. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ReportFieldSchema } from '../zod/reports.zod.js'; import type { ReportField } from '../reports.js'; diff --git a/packages/types/src/__tests__/p1-spec-alignment.test.ts b/packages/types/src/__tests__/p1-spec-alignment.test.ts index ea00b673d1..be5df027df 100644 --- a/packages/types/src/__tests__/p1-spec-alignment.test.ts +++ b/packages/types/src/__tests__/p1-spec-alignment.test.ts @@ -10,10 +10,6 @@ * P1 Spec Protocol Alignment Tests * Tests for all P1 sub-items: ListView, FormView, Dashboard, Page, Record Components, i18n/ARIA */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; // The one runtime import in this otherwise type-only file: the retirement pin // below has to read a zod shape, because the TS interfaces here inherit diff --git a/packages/types/src/__tests__/page-app-dashboard-spec-parity.test.ts b/packages/types/src/__tests__/page-app-dashboard-spec-parity.test.ts index 1daec59418..370c41ffe7 100644 --- a/packages/types/src/__tests__/page-app-dashboard-spec-parity.test.ts +++ b/packages/types/src/__tests__/page-app-dashboard-spec-parity.test.ts @@ -39,10 +39,6 @@ * extension, and record the reason. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { AppSchema as SpecAppSchema, diff --git a/packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts b/packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts index 551dfbe930..9afcec8924 100644 --- a/packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts +++ b/packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts @@ -52,10 +52,6 @@ * itself a bug once). */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { createRequire } from 'node:module'; import { readFileSync } from 'node:fs'; diff --git a/packages/types/src/__tests__/phase2-schemas.test.ts b/packages/types/src/__tests__/phase2-schemas.test.ts index a9924b3eba..d1f44a0ae1 100644 --- a/packages/types/src/__tests__/phase2-schemas.test.ts +++ b/packages/types/src/__tests__/phase2-schemas.test.ts @@ -3,10 +3,6 @@ * Testing AppSchema, ReportComponentSchema and Enhanced ActionSchema, plus the * retirement pins for the theme and block component kinds. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { AppComponentSchema, diff --git a/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts b/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts index 516a4cb8e8..d4fac3ac50 100644 --- a/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts +++ b/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts @@ -28,10 +28,6 @@ * inverted pin, see the bottom of this file. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { createRequire } from 'node:module'; import { readFileSync } from 'node:fs'; diff --git a/packages/types/src/__tests__/report-schema-authoring-face.test.ts b/packages/types/src/__tests__/report-schema-authoring-face.test.ts index 7e8689bb4d..dc1ff38533 100644 --- a/packages/types/src/__tests__/report-schema-authoring-face.test.ts +++ b/packages/types/src/__tests__/report-schema-authoring-face.test.ts @@ -77,10 +77,6 @@ * alone leaves `declared !== enforced`, which is the defect ADR-0049 names. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { ReportBuilderSchema, diff --git a/packages/types/src/__tests__/schema-registry-chatbot-keys-7704.test.ts b/packages/types/src/__tests__/schema-registry-chatbot-keys-7704.test.ts index 9955c1e90c..ff2209d167 100644 --- a/packages/types/src/__tests__/schema-registry-chatbot-keys-7704.test.ts +++ b/packages/types/src/__tests__/schema-registry-chatbot-keys-7704.test.ts @@ -51,10 +51,6 @@ * of the validator the CLI applies. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/select-option-spec-parity.test.ts b/packages/types/src/__tests__/select-option-spec-parity.test.ts index 984ae4e087..dbfe994449 100644 --- a/packages/types/src/__tests__/select-option-spec-parity.test.ts +++ b/packages/types/src/__tests__/select-option-spec-parity.test.ts @@ -25,10 +25,6 @@ * gate removed. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { SelectOptionSchema as SpecSelectOptionSchema } from '@objectstack/spec/data'; import { SelectOptionSchema } from '../zod/form.zod.js'; diff --git a/packages/types/src/__tests__/spec-subschema-parity.test.ts b/packages/types/src/__tests__/spec-subschema-parity.test.ts index c5fd38bed4..900513ceba 100644 --- a/packages/types/src/__tests__/spec-subschema-parity.test.ts +++ b/packages/types/src/__tests__/spec-subschema-parity.test.ts @@ -27,10 +27,6 @@ * on the spec base (and sanction the field here) only for genuinely * objectui-only renderer concerns. See #2231. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { HttpMethodSubsetSchema as SpecHttpMethodSubsetSchema, diff --git a/packages/types/src/__tests__/static-table-narrow-surface.test.ts b/packages/types/src/__tests__/static-table-narrow-surface.test.ts index 263898b499..fcc9fabf4f 100644 --- a/packages/types/src/__tests__/static-table-narrow-surface.test.ts +++ b/packages/types/src/__tests__/static-table-narrow-surface.test.ts @@ -46,10 +46,6 @@ * `accordion-item-authorable-keys.test.ts`). */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { StaticTableColumn, TableColumn, TableSchema } from '../data-display'; import { diff --git a/packages/types/src/__tests__/table-column-type-canonical.test.ts b/packages/types/src/__tests__/table-column-type-canonical.test.ts index 1c5ee6283e..34f4b88ff3 100644 --- a/packages/types/src/__tests__/table-column-type-canonical.test.ts +++ b/packages/types/src/__tests__/table-column-type-canonical.test.ts @@ -34,10 +34,6 @@ * `packages/components/src/renderers/complex/__tests__/`. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { FieldType as SpecFieldTypeEnum } from '@objectstack/spec/data'; import { TABLE_COLUMN_TYPES, normalizeTableColumnType } from '../data-display'; diff --git a/packages/types/src/__tests__/text-value-retired-6951.test.ts b/packages/types/src/__tests__/text-value-retired-6951.test.ts index dff6aa0de4..dd84745911 100644 --- a/packages/types/src/__tests__/text-value-retired-6951.test.ts +++ b/packages/types/src/__tests__/text-value-retired-6951.test.ts @@ -43,10 +43,6 @@ * NOT evidence about them — type assertions are erased before it runs. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { readFileSync, readdirSync, statSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { describe, it, expect } from 'vitest'; diff --git a/packages/types/src/__tests__/timeline-catalog-fixture-migrated.test.ts b/packages/types/src/__tests__/timeline-catalog-fixture-migrated.test.ts index 9ef3337147..a4fbde01b7 100644 --- a/packages/types/src/__tests__/timeline-catalog-fixture-migrated.test.ts +++ b/packages/types/src/__tests__/timeline-catalog-fixture-migrated.test.ts @@ -31,10 +31,6 @@ * so a rename that left the document invalid, or a value that stopped being * reachable, fails here. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, expect, it } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; diff --git a/packages/types/src/__tests__/timeline-declared-keys.test.ts b/packages/types/src/__tests__/timeline-declared-keys.test.ts index 3ecd381f7f..3117b694da 100644 --- a/packages/types/src/__tests__/timeline-declared-keys.test.ts +++ b/packages/types/src/__tests__/timeline-declared-keys.test.ts @@ -64,10 +64,6 @@ * comes, is a deliberate edit against a red test rather than a silent drift. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { TimelineConfigSchema } from '@objectstack/spec/ui'; import { TimelineSchema } from '../zod/data-display.zod.js'; diff --git a/packages/types/src/__tests__/timeline-items-bar-shape-7365.test.ts b/packages/types/src/__tests__/timeline-items-bar-shape-7365.test.ts index aed354315e..fbd7e6b9f9 100644 --- a/packages/types/src/__tests__/timeline-items-bar-shape-7365.test.ts +++ b/packages/types/src/__tests__/timeline-items-bar-shape-7365.test.ts @@ -68,10 +68,6 @@ * this checkout; it is unmeasured here and named as such on the PR. The * fixture census at the foot of this file is the durable half of that reading. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; diff --git a/packages/types/src/__tests__/timeline-items-row-shape-7164.test.ts b/packages/types/src/__tests__/timeline-items-row-shape-7164.test.ts index 2c6c0072ac..43e289f021 100644 --- a/packages/types/src/__tests__/timeline-items-row-shape-7164.test.ts +++ b/packages/types/src/__tests__/timeline-items-row-shape-7164.test.ts @@ -64,10 +64,6 @@ * as before. Neither a row's nor a bar's own keys are declared, and refining * by `variant` is still a wider contract than either ruling named. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; diff --git a/packages/types/src/__tests__/timeline-timescale-retired.test.ts b/packages/types/src/__tests__/timeline-timescale-retired.test.ts index b6517d992e..8d0bd51127 100644 --- a/packages/types/src/__tests__/timeline-timescale-retired.test.ts +++ b/packages/types/src/__tests__/timeline-timescale-retired.test.ts @@ -47,10 +47,6 @@ * alias is untouched. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { TimelineSchema } from '../zod/data-display.zod.js'; import type { TimelineSchema as TimelineSchemaTS } from '../data-display.js'; diff --git a/packages/types/src/__tests__/toast-button-keys.test.ts b/packages/types/src/__tests__/toast-button-keys.test.ts index de7bfc0f05..387e86d883 100644 --- a/packages/types/src/__tests__/toast-button-keys.test.ts +++ b/packages/types/src/__tests__/toast-button-keys.test.ts @@ -91,10 +91,6 @@ * fail there. See the PR for the recorded red. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { ToastSchema } from '../zod/feedback.zod.js'; import type { ToastSchema as ToastSchemaTS } from '../feedback'; diff --git a/packages/types/src/__tests__/toggle-group-item-authorable-keys.test.ts b/packages/types/src/__tests__/toggle-group-item-authorable-keys.test.ts index 5256404fce..fdae568f08 100644 --- a/packages/types/src/__tests__/toggle-group-item-authorable-keys.test.ts +++ b/packages/types/src/__tests__/toggle-group-item-authorable-keys.test.ts @@ -43,10 +43,6 @@ * so re-adding `icon?` to the interface fails the build on the unused directive. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import type { ToggleGroupItem } from '../disclosure'; import { ToggleGroupItemSchema } from '../zod/disclosure.zod'; diff --git a/packages/types/src/__tests__/tree-view-data-optional-6939.test.ts b/packages/types/src/__tests__/tree-view-data-optional-6939.test.ts index 7d8c6ae57b..1fbd0941c3 100644 --- a/packages/types/src/__tests__/tree-view-data-optional-6939.test.ts +++ b/packages/types/src/__tests__/tree-view-data-optional-6939.test.ts @@ -51,10 +51,6 @@ * is the pin that makes the difference visible — it is the assertion that turns * green-to-red if a later sweep deletes the member. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/tree-view-data-retired-6951.test.ts b/packages/types/src/__tests__/tree-view-data-retired-6951.test.ts index d353fd91a1..692af86963 100644 --- a/packages/types/src/__tests__/tree-view-data-retired-6951.test.ts +++ b/packages/types/src/__tests__/tree-view-data-retired-6951.test.ts @@ -51,10 +51,6 @@ * NOT evidence about them — type assertions are erased before it runs. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { readFileSync, readdirSync, statSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { describe, it, expect } from 'vitest'; diff --git a/packages/types/src/__tests__/undeclared-but-consumed-keys-6150.test.ts b/packages/types/src/__tests__/undeclared-but-consumed-keys-6150.test.ts index 923bb792a8..7bc6f50b3a 100644 --- a/packages/types/src/__tests__/undeclared-but-consumed-keys-6150.test.ts +++ b/packages/types/src/__tests__/undeclared-but-consumed-keys-6150.test.ts @@ -58,10 +58,6 @@ * numbers drift and are therefore in prose only; the READ is the fact. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/widget-input-control-vocabulary.test.ts b/packages/types/src/__tests__/widget-input-control-vocabulary.test.ts index 2ee004a986..d2b826d921 100644 --- a/packages/types/src/__tests__/widget-input-control-vocabulary.test.ts +++ b/packages/types/src/__tests__/widget-input-control-vocabulary.test.ts @@ -47,10 +47,6 @@ * bearing sentences are pinned too. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { describe, it, expect } from 'vitest'; diff --git a/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts b/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts index d377a4a874..8496de6741 100644 --- a/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts +++ b/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts @@ -60,10 +60,6 @@ * lands, that expectation is the one to revisit deliberately. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/wrapper-class-declared-7722.test.ts b/packages/types/src/__tests__/wrapper-class-declared-7722.test.ts index f0976a11e4..636ea4c3c0 100644 --- a/packages/types/src/__tests__/wrapper-class-declared-7722.test.ts +++ b/packages/types/src/__tests__/wrapper-class-declared-7722.test.ts @@ -53,10 +53,6 @@ * the next single-key grep (objectui#6938 → objectui#7722 was that wait). */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readdirSync, readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts b/packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts index 5847491b30..442abf01b4 100644 --- a/packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts +++ b/packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts @@ -24,20 +24,24 @@ * TreeNodeSchema ReferenceError: Cannot access 'TreeNodeSchema' before initialization * * Seven name the very const being declared (`children: z.array(TreeNodeSchema)` - * sits inside `TreeNodeSchema`'s own initialiser); `SchemaNodeSchema` names - * `BaseSchemaCore`, which `base.zod.ts` declares BELOW it. For those eight the - * `z.lazy` is LOAD-BEARING — it is buying a TDZ dodge, not a style — and they - * keep the spelling they have. `mechanism` below reproduces the failure. + * sits inside `TreeNodeSchema`'s own initialiser); `SchemaNodeSchema` named + * `BaseSchemaCore`, which `base.zod.ts` declared BELOW it. For those eight the + * `z.lazy` was LOAD-BEARING — buying a TDZ dodge, not a style — and they keep + * the spelling they have. `mechanism` below reproduces the failure. * - * ⚠️ objectui#8344 moved this row TWICE and it ends where it started, which is - * worth one sentence so the next reader does not re-derive it. An intermediate - * revision of that card built the node union ONCE at module scope, below - * `BaseSchemaCore`, so the TDZ dissolved and the getter returned one object — the - * row was {@link MEMOISED} for as long as that spelling lived. What shipped instead - * reads `AnyComponentSchema` as an IMPORT BINDING inside the getter, so the getter - * BUILDS the node union per call again and `SchemaNodeSchema` stays {@link - * TDZ_BOUND} — the same reading objectui#7918 recorded, for a different reason: - * the TDZ it now dodges is the module cycle's, not `BaseSchemaCore`'s. + * ⚠️ SEVEN, not eight, since objectui#8344. That card redirected the node + * recursion point at `AnyComponentSchema` and had to build `SchemaNodeSchema`'s + * union ONCE, at module scope, immediately below `BaseSchemaCore` — because the + * component arm is a written option slot and there has to be an array to write + * into. Declaring it below `BaseSchemaCore` is what dissolves the TDZ, so the + * memoisation this file calls "worth doing where it is free" became free for this + * one const, and the row moved to {@link MEMOISED}. ⛔ It is a BYPRODUCT, not a + * goal: nobody memoised it to make `.unwrap()` honest, and ⛔ nothing here licenses + * moving the remaining seven — each still names the const being declared, and + * `mechanism` still reproduces their ReferenceError. + * + * ⇒ the eight-name list above is kept VERBATIM as the objectui#7918 reading it + * was. It is history, not the current ledger; the arrays below are the ledger. * * The two that loaded clean were memoised: `FilterBuilderConditionSchema` is not * recursive at all, and `NavigationItemSchema` already defers its self-reference @@ -92,10 +96,6 @@ * strict face (objectui#7935 / objectstack#5250) should make that trade * deliberately. Update this ledger in the same change. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { @@ -129,6 +129,9 @@ const innerTypeStable = (S: unknown): boolean => (S as LazyInternals)._zod.inner const MEMOISED: ReadonlyArray = [ ['FilterBuilderConditionSchema', FilterBuilderConditionSchema], ['NavigationItemSchema', NavigationItemSchema], + // objectui#8344 — see the header. Its getter returns the ONE node union that + // `base.zod.ts` builds below `BaseSchemaCore`, so there is no TDZ left to dodge. + ['SchemaNodeSchema', SchemaNodeSchema], ]; /** ⛔ Do not "fix" these — each one's `z.lazy` dodges a real ReferenceError. */ const TDZ_BOUND: ReadonlyArray = [ @@ -138,7 +141,6 @@ const TDZ_BOUND: ReadonlyArray = [ ['MenuItemSchema', MenuItemSchema], ['NavLinkSchema', NavLinkSchema], ['NavigationMenuItemSchema', NavigationMenuItemSchema], - ['SchemaNodeSchema', SchemaNodeSchema], ['TreeNodeSchema', TreeNodeSchema], ]; diff --git a/packages/types/src/__tests__/zod-mirror-authors-no-defaults-7735.test.ts b/packages/types/src/__tests__/zod-mirror-authors-no-defaults-7735.test.ts index 0ad737202e..dd8b2acb26 100644 --- a/packages/types/src/__tests__/zod-mirror-authors-no-defaults-7735.test.ts +++ b/packages/types/src/__tests__/zod-mirror-authors-no-defaults-7735.test.ts @@ -60,10 +60,6 @@ * them and says why its assertion is a floor and not a ratchet. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, expect, it } from 'vitest'; import { readFileSync, readdirSync } from 'node:fs'; import { dirname, join } from 'node:path'; diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index 50458273d6..6ef348d9fb 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -331,10 +331,6 @@ * the mirror accepts a function again or a renderer lost its callback. */ -// objectui#8344: the `./zod` barrel must be the FIRST zod module this graph evaluates. -// `base.zod.ts` reads `AnyComponentSchema` as an import binding, so entering at a -// category module puts `BaseSchema` in its temporal dead zone and throws at load. -import '../zod/index.zod.js'; import { describe, it, expect } from 'vitest'; import { readdirSync, readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/packages/types/src/zod/base.zod.ts b/packages/types/src/zod/base.zod.ts index e2500472a1..fb72f87187 100644 --- a/packages/types/src/zod/base.zod.ts +++ b/packages/types/src/zod/base.zod.ts @@ -21,10 +21,6 @@ import { I18nLabelSchema } from '@objectstack/spec/ui'; import { retirementTombstone } from './tombstone.zod.js'; import { ExpressionWireSchema } from './expression.zod.js'; import type { SchemaNode } from '../base.js'; -// ⚠️ CYCLE, deliberately: `index.zod.ts` imports this module. The binding below is read -// ONLY inside `SchemaNodeSchema`'s `z.lazy` getter, which runs long after both module -// bodies have evaluated — see that const's docblock for why a binding and not a write. -import { AnyComponentSchema } from './index.zod.js'; /** * A KEYED i18n label — the runtime mirror of `KeyedI18nLabel` in `../base.ts`. @@ -53,6 +49,90 @@ export const KeyedI18nLabelSchema = z.object({ }); +/** + * Fill the node recursion point with the component union, and hand it straight + * back — so the fill is part of `AnyComponentSchema`'s own initializer in + * `index.zod.ts` rather than a bare statement beside it (objectui#8344). + * + * ## ⚠️ Why a WRITE INTO the union's option list, and not a `z.lazy` holder + * + * The obvious spelling — a `let` the `z.lazy` getter reads — is WRONG here, and + * measurably so. `z.lazy` MEMOISES: zod 4.4.3 caches the resolved inner on first + * access, and merely parsing any component schema resolves it (the union arm walk + * reads every option to compute its own metadata, so a childless `detail-view` node + * is enough). ⇒ whatever the getter returned FIRST would be the accept set for the + * rest of the process, decided by whichever module graph parsed first — and this + * repo's `isolate: false` unit project shares one module graph across every file in + * a worker. Measured on this branch with that spelling in place: the #8344 pin + * PASSED run alone and FAILED in the full run, because + * `__tests__/handler-keys-string-any-mirrors-7344.test.ts` parses from a barrel-free + * import graph and froze the base shape in first. Refusing instead of falling back + * converges, but turns that same import order into dozens of red suites. + * + * ⭐ A `z.union` does NOT memoise its options: measured on zod 4.4.3, `z.union(opts)` + * keeps `opts` BY REFERENCE and re-reads it on every parse, so writing slot 0 takes + * effect immediately — including after parses have already run through it. That is + * what makes the window disappear rather than merely move: before the fill a child + * slot answers exactly as it did pre-#8344, after it every parse sees the component + * union, and no first-parse ever freezes the wrong answer in. + * + * ⚠️ That by-reference behaviour is the load-bearing assumption, so it is ASSERTED + * here rather than trusted: a zod that copied the array would leave this silently + * under-enforcing — the one failure direction that never announces itself. + * + * ⚠️ The parameter bound is `z.ZodType`, not `z.ZodType< SchemaNode, SchemaNode >`, + * and that too is measured. The tighter bound is the one this wiring wants — "the + * recursion point may only be filled with something a declared `SchemaNode` slot + * could already hold" — and `tsc` refuses it TODAY for exactly one arm out of 106: + * `complex.zod.ts#ChatbotSchema` mirrors the chat API body params under the key + * `body`, which is `BaseSchema`'s CHILDREN slot (`Record< string, unknown >` where + * the base says `SchemaNode | SchemaNode[]`). That collision is pre-existing and + * already recorded — the parity ledger carries it under `KnownDrift`, the TS + * declaration renamed the key to `requestBody`, and `ChatbotSharedMirrorShape` in + * `complex.zod.ts` says in as many words that a ruling on `ChatbotSchema`'s own + * `body` arm is a separate question. ⛔ #8344 does not decide it either. So the bound + * is loose HERE and the real check is kept EXACT one level out, as a type-level pin + * naming that single arm in `__tests__/node-recursion-point-8344.test.ts`. ⇒ a SECOND + * arm drifting the same way turns that pin red instead of passing unnoticed. + * + * @internal — the package's only zod entry point is the `./zod` barrel, which is + * `index.zod.ts`; this exists for that one call site and is not re-exported. + */ +export function defineNodeComponentUnion(union: T): T { + // ⭐ objectui#8344 F2 — what goes into the slot is the union WRAPPED, never the bare union. + // + // `ChatbotSchema.body` mirrors the chat API's body params as a record, which is WIDER than + // `BaseSchemaCore.body`. It is the only wider redeclaration among the 109 base-key + // redeclarations across the arms, so installing the bare union would narrow at 108 child + // slots and WIDEN at one: a `chatbot` node carrying a record `body` is refused at a child + // slot on `main` and would be accepted here. The card's appetite forbids widening in + // flight, so the arm carries the check and the PUBLISHED mirror is untouched — a root + // `chatbot` with a record `body` still parses, the same node one slot down does not. + // ⛔ Do not "simplify" this by narrowing `ChatbotSchema` itself: that is a change to a + // published face this card does not own, and it is recorded on objectui#8572. + const installed = union.superRefine((value, ctx) => { + const node = value as { type?: unknown; body?: unknown } | null | undefined; + if (!node || node.type !== 'chatbot' || node.body === undefined) return; + const asNodeSlot = BaseSchemaCore.shape.body.safeParse(node.body); + if (asNodeSlot.success) return; + for (const issue of asNodeSlot.error.issues) { + ctx.addIssue({ ...issue, path: ['body', ...issue.path] }); + } + }) as unknown as T; + nodeUnionOptions[0] = installed; + // The assertion the paragraph above exists for. ⛔ Do not delete it as noise: it is + // the only thing standing between a zod that copies its option array and a + // recursion point that silently reverts to the pre-#8344 base shape. + const readBack = (nodeUnion as unknown as { _zod: { def: { options: readonly unknown[] } } })._zod.def.options[0]; + if (readBack !== installed) { + throw new Error( + 'objectui#8344: `z.union` no longer keeps its option array by reference, so the node ' + + 'recursion point did not take. The redirect is INERT and every nested node is being ' + + 'judged by `BaseSchemaCore` again — see `defineNodeComponentUnion` in base.zod.ts.', + ); + } + return union; +} /** * Schema Node — what a child slot holds: a COMPONENT document, or a primitive. @@ -69,36 +149,43 @@ export const KeyedI18nLabelSchema = z.object({ * the registered component mirrors is the whole of this change; ⛔ nothing here is * `.strict()`, and `BaseSchemaCore` keeps its passthrough. * - * Priced at 9 newly-refused corpus documents — objectui#8344's R3 at **54 / 554** against - * R1's **45 / 554**, re-derived on this branch's merged head; the card's own body quotes - * 553 because it was measured before the corpus gained a document, and the nine are the - * same nine either way. Each is pre-existing debt this SURFACES rather than creates: + * Priced at 9 newly-refused corpus documents (objectui#8344's R3, 54 / 553 against + * R1's 45 / 553), each one pre-existing debt this SURFACES rather than creates: * four whose child `type` resolves in no arm, five already red under their own * schema and shielded until now by the recursion point. * - * ## ⚠️ Why the arm is an IMPORT BINDING read inside the getter + * ## ⚠️ Why the arm is late-bound and not imported * - * `AnyComponentSchema` is built in `index.zod.ts` out of all 13 category modules, and 14 - * modules import THIS one, so naming it at this module's top level is a cycle that throws: - * entering the graph at `base.zod.js` would evaluate `app.zod.ts`'s body while `BaseSchema` - * is still in its temporal dead zone. The binding is therefore imported and read ONLY from - * inside the `z.lazy` getter, through {@link nodeComponentArm} — deferred to first parse, - * which is after both module bodies have completed. + * `AnyComponentSchema` is built in `index.zod.ts` out of all 13 category modules, + * and 14 modules import THIS one — so naming it here is a module cycle, and + * `z.lazy` defers the EVALUATION, not the module graph. With that import in place, + * entering the graph at `base.zod.js` evaluates `app.zod.ts`'s body while + * `BaseSchema` is still in its temporal dead zone and the package throws on import. + * ⇒ the break is deliberate: `index.zod.ts` fills the holder through + * {@link defineNodeComponentUnion} as it constructs the union, which is module + * evaluation and therefore strictly before anything can parse. * - * ⭐ Two properties come from that, and the earlier spelling had neither. It wrote the arm - * into a live option array from the barrel's body, so (a) a bundler honouring this package's - * `"sideEffects": false` could drop the write and leave every child slot judged by - * `BaseSchemaCore` again — silently, with the write's own assertion dropped alongside it — - * and (b) a module graph that reached a parse without evaluating the barrel got exactly the - * pre-#8344 accept set with no diagnostic. A read binding cannot do either: whatever retains - * `SchemaNodeSchema` retains the union it names, and a graph that has not evaluated the - * barrel throws `ReferenceError` at load rather than answering wrongly. ⇒ ⛔ `"sideEffects": - * false` stays TRUE and untouched; there is no load-time write in this module to declare. + * ⚠️ BEFORE the fill — a module graph that reaches a parse without ever evaluating + * `index.zod.js` — the arm is `BaseSchemaCore`, i.e. exactly the pre-#8344 accept + * set, and it switches the moment the barrel loads. That is a property of the WRITE, + * not a tolerated fallback: `z.union` re-reads its option array on every parse, so + * nothing can freeze the pre-fill answer in ({@link defineNodeComponentUnion} carries + * the measurement, and why the obvious `z.lazy` holder is wrong). No published entry + * point can reach that window BY MODULE GRAPH: `./zod` is this package's only zod + * subpath and it IS `index.zod.js`. Pinned in + * `__tests__/node-recursion-point-8344.test.ts`. * - * ⚠️ The cost is real and is paid by TESTS, not by consumers: a test that enters the graph at - * a category module rather than at the `./zod` barrel now throws at import. The fix is one - * line of import hygiene — import the barrel first — and the files that needed it are listed - * in this PR. Entering at `./zod`, the only published zod subpath, is always safe. + * ⛔ ⚠️ THAT SENTENCE IS ABOUT MODULE GRAPHS, AND A BUNDLER IS NOT ONE. This package + * declares `"sideEffects": false` and the fill is a statement in this barrel's body, + * so a bundler that honours the flag and sees no reference to `AnyComponentSchema` + * may drop the whole const — fill included — and then every child slot validates + * with the PRE-#8344 arm. Measured on this repo's own Vite/rollup lib build: one + * entry importing only `CardSchema` ACCEPTS a nested off-spec node (369,733 bytes, + * no fill in the output), the same entry with `AnyComponentSchema` also imported + * REFUSES it (1,144,999 bytes, fill present). The guard below cannot see this: it + * runs inside the code that was dropped. ⇒ this window is silent, it is NOT the + * pre-fill window this paragraph describes, and its disposition is a ruling in + * flight on objectui#8344 — ⛔ do not close it by editing this comment. * * ## Both type arguments are filled, and that is the whole published input face * @@ -132,17 +219,14 @@ export const KeyedI18nLabelSchema = z.object({ * a declaration or narrowing a mirror to make the annotation fit: either is a * contract change wearing a type-annotation's clothes, and both are ruled elsewhere. */ -export const SchemaNodeSchema: z.ZodType = z.lazy( - () => - z.union([ - nodeComponentArm(), - z.string(), - z.number(), - z.boolean(), - z.null(), - z.undefined(), - ]) as unknown as z.ZodType, -); +export const SchemaNodeSchema: z.ZodType = z.lazy(() => { + // `z.lazy` memoises this getter, and that is FINE — because what it returns is the + // one live union, whose option slot 0 IS the recursion point and is written by + // {@link defineNodeComponentUnion}. ⛔ Do not move the union's CONSTRUCTION in here: + // a getter that builds the union is the memoising spelling objectui#8344 measured + // wrong, and it would put the accept set back at the mercy of import order. + return nodeUnion; +}); /** * Base Schema - Core validation schema that all components extend @@ -329,48 +413,39 @@ const BaseSchemaCore = z.object({ export const BaseSchema = BaseSchemaCore; /** - * The COMPONENT arm of the node union, built fresh on every getter call. - * - * ## Why a function and not a `const` (objectui#8344) - * - * `AnyComponentSchema` lives in `index.zod.ts`, which imports THIS module, so at this - * module's evaluation time the imported binding is in its temporal dead zone. Reading it - * from inside a function body defers the read until `z.lazy` first resolves — after both - * module bodies have run. ⇒ the binding is either initialised (barrel entered, the normal - * path) or it throws `ReferenceError` at load, LOUDLY. There is no third answer, and in - * particular there is no longer a quiet pre-fill window that answers exactly as `main`. + * The one node union every child slot recurses through — built HERE, immediately + * below `BaseSchemaCore`, because slot 0 holds it (objectui#8344). * - * ⭐ That is also what makes the redirect survive BUNDLING. The previous spelling wrote - * the arm into a live option array from the barrel's body; with `"sideEffects": false` a - * bundler was entitled to drop that write when a consumer imported one schema by name, - * and every child slot silently went back to `BaseSchemaCore` — measured on this repo's - * own Vite/rollup build. A USED import binding is retained by construction: whatever - * keeps `SchemaNodeSchema` keeps the union it names. ⛔ `"sideEffects": false` stays TRUE - * here — this module performs no load-time write at all now. + * Slot 0 is the RECURSION POINT and is the only slot that ever changes: + * `BaseSchemaCore` while `index.zod.ts` has not been evaluated, `AnyComponentSchema` + * from the moment it has. `z.union` re-reads this array on every parse, so the swap + * is live and no parse can freeze the pre-fill answer in — the whole reason the + * arm is a written slot rather than a `z.lazy` holder ({@link defineNodeComponentUnion} + * carries the measurement). * - * ## The `chatbot` guard, and why it is here rather than in the mirror - * - * `ChatbotSchema.body` mirrors the chat API's body params as a record, which is WIDER - * than `BaseSchemaCore.body`. It is the only wider redeclaration among the 109 base-key - * redeclarations across the union's arms, so without this guard the redirect would narrow - * at 108 slots and WIDEN at one: a `chatbot` node with a record `body` is refused at a - * child slot on `main` and would be accepted here. ⛔ The root mirror is deliberately not - * touched — it carries the chat API's params on purpose, and that question is its own - * card — so the narrowing lives on the arm the recursion point installs and nowhere else. - * ⇒ a root `chatbot` with a record `body` still parses; the same node one slot down does - * not. Both directions are pinned in `__tests__/node-recursion-point-8344.test.ts`. + * ⛔ Never export this array or this union. `SchemaNodeSchema` is the public handle + * and identity on it is what objectui#7918 consequence ① says is stable; a second + * exported name for the same shape would give the parity census a row to compare + * that has no TS declaration behind it. */ -const nodeComponentArm = (): z.ZodType => - AnyComponentSchema.superRefine((value, ctx) => { - const node = value as { type?: unknown; body?: unknown } | null | undefined; - if (!node || node.type !== 'chatbot' || node.body === undefined) return; - const asNodeSlot = BaseSchemaCore.shape.body.safeParse(node.body); - if (asNodeSlot.success) return; - for (const issue of asNodeSlot.error.issues) { - ctx.addIssue({ ...issue, path: ['body', ...issue.path] }); - } - }) as unknown as z.ZodType; - +/** + * ⚠️ Both of these are `const` DECLARATIONS, ⛔ never assignments to a `let` hoisted + * above `BaseSchemaCore`. `@object-ui/types` declares `"sideEffects": false`, and a + * bare top-level assignment is a load-time side effect a bundler is entitled to drop + * whole — `scripts/__tests__/side-effects-declaration-consistency.test.ts` fails on + * exactly that, and it caught this file mid-#8344. Everything above that names them + * does so from inside a function body, which runs long after this line. + */ +const nodeUnionOptions: [z.ZodType, ...z.ZodType[]] = [ + BaseSchemaCore, + z.string(), + z.number(), + z.boolean(), + z.null(), + z.undefined(), +]; + +const nodeUnion = z.union(nodeUnionOptions) as unknown as z.ZodType; /** * A spec schema's fields, minus the keys objectui declares locally, as an diff --git a/packages/types/src/zod/index.zod.ts b/packages/types/src/zod/index.zod.ts index 944fb6f622..22c76ca2bc 100644 --- a/packages/types/src/zod/index.zod.ts +++ b/packages/types/src/zod/index.zod.ts @@ -350,6 +350,7 @@ export { // ============================================================================ import { z } from 'zod'; +import { defineNodeComponentUnion } from './base.zod.js'; import { AppComponentSchema } from './app.zod.js'; import { LayoutSchema } from './layout.zod.js'; import { FormComponentSchema } from './form.zod.js'; @@ -369,17 +370,17 @@ import { ViewComponentSchema } from './views.zod.js'; * Use this for generic component rendering where the type is determined at runtime. * * ⭐ It is ALSO the node recursion point (objectui#8344): every child slot is - * `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])`, and `SchemaNodeSchema` builds - * its component arm FROM THIS CONST, so a nested node is judged by its own component - * schema at every depth instead of by the ~21 base keys. + * `z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)])`, and `SchemaNodeSchema` + * resolves its component arm to THIS union, so a nested node is judged by its own + * component schema at every depth instead of by the ~21 base keys. The wiring is a + * late-binding holder rather than an import because 14 modules import `base.zod.js` + * and this module is built from all 13 category modules — the full reasoning, and + * what the UNFILLED holder answers, live on `SchemaNodeSchema` in `base.zod.ts`. * - * ⚠️ The wiring lives in `base.zod.ts`, not here, and it is an IMPORT BINDING read inside - * that const's `z.lazy` getter — ⛔ no write into this module's body, no holder, no - * option-array patching. This module therefore performs no load-time side effect, which is - * what keeps `"sideEffects": false` true and what keeps the redirect alive through a - * bundler: a binding that is READ is retained, while the write this replaced could be - * dropped silently. The reasoning, the measurement and the `chatbot` narrowing that rides - * on the same arm all live on `SchemaNodeSchema` and `nodeComponentArm` in `base.zod.ts`. + * ⚠️ The fill is written as this const's own initializer, not as a statement beside + * it, so no bundler can keep the union and drop the wiring, and no future edit can + * reorder the two. ⛔ Do not "simplify" it back into a bare + * `defineNodeComponentUnion(AnyComponentSchema)` call underneath. * * ## Why this is discriminated (objectui#8498) * @@ -404,10 +405,12 @@ import { ViewComponentSchema } from './views.zod.js'; * * ⚠️ BOTH of the above are live here, and the composition is the whole resolution: * objectui#8498 changed WHICH arm reports, objectui#8344 changed WHERE this union is - * consulted. They compose because they touch different things — the discrimination is in - * this initializer, the recursion wiring is a binding `base.zod.ts` reads. + * consulted. The discriminated union is what gets written into the node option slot, + * so `defineNodeComponentUnion` wraps it rather than replacing it. The slot itself is + * still a plain `z.union` in `base.zod.ts` — that is what keeps its option array by + * reference, and it is untouched by the discrimination. */ -export const AnyComponentSchema = z.discriminatedUnion('type', [ +export const AnyComponentSchema = defineNodeComponentUnion(z.discriminatedUnion('type', [ AppComponentSchema, LayoutSchema, FormComponentSchema, @@ -433,7 +436,7 @@ export const AnyComponentSchema = z.discriminatedUnion('type', [ // schema, so it also rewrote this union's `invalid_type` and a non-object root // lost "expected object, received number". `undefined` declines to the locale. error: (issue) => (issue.code === 'invalid_union' ? 'Invalid input' : undefined), -}); +})); /** * Validate a schema against the AnyComponentSchema From 027dcdf739da57fe50c59d54edebeef10d64b4ed Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 17:15:11 +0000 Subject: [PATCH 9/9] docs(types): declare the tree-shake gap as shipped, cite one measurement, point at objectui#8598 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01TezFG8ZMrNH6n5VTNpPpdH --- .../8344-node-recursion-point-redirect.md | 36 ++++++++++++++----- packages/types/src/zod/base.zod.ts | 23 ++++++++---- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/.changeset/8344-node-recursion-point-redirect.md b/.changeset/8344-node-recursion-point-redirect.md index a5e70c76b3..2eb9c6aa56 100644 --- a/.changeset/8344-node-recursion-point-redirect.md +++ b/.changeset/8344-node-recursion-point-redirect.md @@ -62,14 +62,28 @@ none. The published `ChatbotSchema` is untouched — whether its own `body` shou chat API's params is a separate question, recorded on objectui#8572 and deliberately not decided here. -**3. ⚠️ KNOWN GAP, stated rather than papered over: the redirect can still be tree-shaken away -for a bundled consumer.** This package declares `"sideEffects": false` and the arm is filled by -a statement in the `./zod` barrel's body, so a bundler that honours the flag and sees no -reference to `AnyComponentSchema` may drop the fill — and then every child slot validates with -the PRE-redirect arm, with no error and no warning. Measured on this repo's own Vite/rollup lib -build: an entry importing only `CardSchema` ACCEPTS a nested off-spec node (370,652 bytes, no -fill in the output); the same entry with `AnyComponentSchema` also imported REFUSES it -(1,149,749 bytes, fill present). +**3. ⚠️ KNOWN GAP, declared rather than papered over: a bundled consumer that never reads +`AnyComponentSchema` can tree-shake the redirect away.** This package declares +`"sideEffects": false` and the arm is filled by a statement in the `./zod` barrel's body, so a +bundler that honours the flag and sees no reference to `AnyComponentSchema` may drop the fill — +and then every child slot validates with the PRE-redirect arm, with no error and no warning. +Who is exposed, stated plainly: an external consumer whose bundler honours `sideEffects: false` +and never reads `AnyComponentSchema` keeps `main`'s accept set for NESTED nodes. Root-level +enforcement is unchanged by the gap, and every consumer whose import graph reads the union — +the `./zod` barrel under Node or vitest, `@object-ui/cli`'s `check` / `validate` (they call +`safeValidateSchema`, which references the union), any bundle that imports `AnyComponentSchema` +— gets the new set at every depth. + +Measured on the published `dist/zod` face of this head (Vite 8.2.1 lib build, `es`, +esbuild-minified, `zod` 4.4.3 and `@objectstack/spec` external, so the figures are this +package's own bytes; nested off-spec node = `{ type: 'icon', icon: 'check', size: 'huge' }` +inside `card.body[]`, parsed through `CardSchema`): + +| entry | nested off-spec node | bundle (raw / gzip) | fill in output | +| --- | --- | --: | --- | +| barrel, `CardSchema` and `AnyComponentSchema` imported | REFUSED | 750,542 / 206,815 B | present | +| barrel, `CardSchema` only | **ACCEPTED (inert)** | 212,567 / 61,025 B | absent | +| deep-link entry at `layout.zod.js` | **ACCEPTED (inert)** | 212,563 / 61,030 B | absent | ⛔ It is NOT closed here, and the reason is measured rather than argued. The route that closes it by binding the union inside `SchemaNodeSchema`'s `z.lazy` getter was implemented and pushed, @@ -82,6 +96,10 @@ too: a narrowed `sideEffects` array is not a legal declaration for this package requires every entry form to be named, another refuses a named entry with no load-time effect, and this package's entry forms are pure), a bare top-level call is dropped by the same flag, and dropping the flag costs 16,078 gzipped bytes on the console `framework` chunk and moves a -workspace census a guard pins. ⇒ until a route survives CI, a consumer that bundles +workspace census a guard pins. + +⇒ **The card that closes this gap is objectui#8598**: build the `./zod` subpath as ONE bundled +module, so a consumer bundler has no internal graph to link past and every entry — one schema, +the barrel, or a deep link — gets the same accept set. Until it lands, a consumer that bundles `@object-ui/types/zod` should keep `AnyComponentSchema` in its import graph, which is enough to make the redirect apply. diff --git a/packages/types/src/zod/base.zod.ts b/packages/types/src/zod/base.zod.ts index fb72f87187..85483f321f 100644 --- a/packages/types/src/zod/base.zod.ts +++ b/packages/types/src/zod/base.zod.ts @@ -179,13 +179,22 @@ export function defineNodeComponentUnion(union: T): T { * declares `"sideEffects": false` and the fill is a statement in this barrel's body, * so a bundler that honours the flag and sees no reference to `AnyComponentSchema` * may drop the whole const — fill included — and then every child slot validates - * with the PRE-#8344 arm. Measured on this repo's own Vite/rollup lib build: one - * entry importing only `CardSchema` ACCEPTS a nested off-spec node (369,733 bytes, - * no fill in the output), the same entry with `AnyComponentSchema` also imported - * REFUSES it (1,144,999 bytes, fill present). The guard below cannot see this: it - * runs inside the code that was dropped. ⇒ this window is silent, it is NOT the - * pre-fill window this paragraph describes, and its disposition is a ruling in - * flight on objectui#8344 — ⛔ do not close it by editing this comment. + * with the PRE-#8344 arm. Measured on the published `dist/zod` face of this package + * (Vite 8.2.1 lib build, `es`, esbuild-minified, `zod` 4.4.3 and `@objectstack/spec` + * external, so the figures are this package's own bytes — the same instrument and + * the same figures the objectui#8344 changeset cites): a barrel entry importing only + * `CardSchema` ACCEPTS a nested off-spec node (212,567 bytes, fill absent), an entry + * that deep-links `layout.zod.js` ACCEPTS it too (212,563 bytes, fill absent), and + * the barrel entry with `AnyComponentSchema` also imported REFUSES it (750,542 + * bytes, fill present). The guard below cannot see this: it runs inside the code + * that was dropped. ⇒ this window is silent, it is NOT the pre-fill window this + * paragraph describes, and it ships DECLARED (objectui#8344 decision batch #98): an + * external consumer whose bundler honours the flag and never reads + * `AnyComponentSchema` keeps the pre-#8344 accept set for NESTED nodes; root-level + * enforcement and every consumer whose graph reads the union get the new set. + * Closing it is objectui#8598 — the `./zod` face built as ONE module — ⛔ not an + * import of the union from here: that spelling was measured to throw at load in a + * real consumer's bundle (the changeset carries the CI evidence). * * ## Both type arguments are filled, and that is the whole published input face *