From 98e7aa8d62af1e0a68f9f7255301396b28fd6a56 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 12:50:55 +0000 Subject: [PATCH 1/3] fix(types): key two zod mirrors by their own declaration's vocabulary, not by string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `GridSchema.columns` (objectui#8516) and `ReportComponentSchema.exportConfigs` (objectui#8556) are one defect class: each mirror restated a CLOSED key set as `z.record(z.string(), …)`, so `os-ui validate` / `check` passed documents the published TypeScript face refuses, and the renderers then ignored the out-of-vocabulary key silently. Both narrow to `z.partialRecord`. NOT `z.record(z.enum([…]), …)`: measured on zod 4.4.3, that spelling requires every member, so it would refuse the partial map both declarations invite — trading each divergence for its opposite. The measurement is pinned executably, at compile time (the inferred map is `Partial>`, not `Record<…>`) and at run time (one accepting row per member). objectui#8505's handoff assertion is flipped, not deleted, and gains a positive control so the pair cannot both read green against a mirror that refuses everything. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE --- ...16-8556-mirror-partial-record-narrowing.md | 52 ++++ ...-columns-breakpoint-narrowing-8505.test.ts | 49 ++-- ...rror-partial-record-narrowing-8516.test.ts | 222 ++++++++++++++++++ packages/types/src/zod/layout.zod.ts | 17 +- packages/types/src/zod/reports.zod.ts | 12 +- 5 files changed, 331 insertions(+), 21 deletions(-) create mode 100644 .changeset/8516-8556-mirror-partial-record-narrowing.md create mode 100644 packages/types/src/__tests__/mirror-partial-record-narrowing-8516.test.ts diff --git a/.changeset/8516-8556-mirror-partial-record-narrowing.md b/.changeset/8516-8556-mirror-partial-record-narrowing.md new file mode 100644 index 0000000000..537d5b0b8d --- /dev/null +++ b/.changeset/8516-8556-mirror-partial-record-narrowing.md @@ -0,0 +1,52 @@ +--- +'@object-ui/types': minor +--- + +**Breaking for authored metadata, at validation time:** two zod mirrors in +`@object-ui/types/zod` now state their own declaration's key vocabulary instead +of `string` (objectui#8516, objectui#8556). + +| key | mirror was | mirror is | +| :-- | :--------- | :-------- | +| `GridSchema.columns` (`zod/layout.zod.ts`) | `z.record(z.string(), z.number())` | a PARTIAL record over the six breakpoints | +| `ReportComponentSchema.exportConfigs` (`zod/reports.zod.ts`) | `z.record(z.string(), ReportExportConfigSchema)` | a PARTIAL record over `ReportExportFormat` | + +Both declarations already stated the closed set — `columns` since objectui#8505, +`exportConfigs` since objectui#6121 — so each mirror was accepting a spelling its +own published type refuses. This is a pull-back to the declaration, not a new +constraint: `os-ui validate` / `check` in `@object-ui/cli` are the real consumers +of these mirrors, and they were passing documents `tsc` rejects. + +**What now fails that used to pass.** A `grid` whose responsive map is keyed +outside `xs` `sm` `md` `lg` `xl` `2xl` — `{ xxl: 6 }` is the one to expect, +because it is the Bootstrap/Ant spelling an author reaches for first — and a +`report` whose `exportConfigs` is keyed outside `pdf` `excel` `csv` `json` +`html`. Both were ALREADY broken at runtime: the grid renderer reads exactly the +six keys and the export engine exactly the five formats, so a document the +mirror used to accept rendered at the default column count, or exported nothing, +with no error and no warning. The narrowing refuses documents that were already +being silently ignored; it refuses nothing that works today. + +**Migration:** `xxl` is `2xl`; `XL` and `2XL` are `xl` and `2xl`. The refusal +names the offending key — `Path: columns → xxl`, `Code: invalid_key` — through +`@object-ui/cli`'s union-arm expansion. + +**Measured accept set in the corpus, before grading this.** Across every tracked +file in this repository and in the `objectstack` sibling checkout: 33 grid nodes +carry an object-valued `columns`, and every out-of-vocabulary key among them is +a deliberate negative fixture inside objectui#8505's own test file. The authored +corpus — `examples/schema-catalog`, `content/docs`, `skills/objectui`, +`apps/site`, the renderer fixtures — is 28 sites, all six-breakpoint-clean. +`exportConfigs` has zero authored inhabitants in either repository. So this +narrowing refuses **zero** documents that exist today, which is why it is graded +`minor` with the break spelled out rather than escalated. + +**The spelling is `z.partialRecord`, and that is load-bearing.** ⛔ Not +`z.record(z.enum([…]), …)`: measured on zod 4.4.3, the plain record over an enum +key REQUIRES every member, so `{ md: 2 }` stops parsing — it would trade this +divergence for its exact opposite, and on `exportConfigs` it would re-impose the +total-`Record` authoring face objectui#6121's maintainer ruling removed. That +measurement is pinned executably in +`__tests__/mirror-partial-record-narrowing-8516.test.ts`, at compile time (the +inferred map is `Partial>`, not `Record<…>`) and at run time (one +accepting row per member), so it cannot rot into folklore. 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..0d89d28380 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 @@ -252,28 +252,28 @@ describe('objectui#8505 — the narrowing broke no producer, and this is why', ( }); }); -/* ── (e) the other authoring face, REPORTED and left open by this card ────── */ +/* ── (e) the other authoring face — the handoff, now COLLECTED ────────────── */ -describe('objectui#8505 — the zod mirror still admits an open record, deliberately', () => { - it('the JSON face still accepts `{ xxl: 6 }` — the gap this card did not close', () => { - // NOT the desired end state. `GridSchema` in `zod/layout.zod.ts` is - // `z.record(z.string(), z.number())`, so `os-ui validate` / `check` - // (`@object-ui/cli`, the real consumer of these mirrors) still passes a - // grid document whose map is keyed by a spelling no renderer reads. +describe('objectui#8505 — the zod mirror now refuses what the declaration refuses', () => { + it('the JSON face refuses `{ xxl: 6 }` — the gap objectui#8516 closed', () => { + // THE HANDOFF PIN, FLIPPED (objectui#8516, the objectui#7070 convention). + // It asserted `success: true` for the whole of objectui#8505's life, with + // the reason stated rather than assumed: that card narrowed the TypeScript + // face only, and this block held the mirror's remaining reading visible so + // it could not rot into a silent assumption that both faces closed + // together. objectui#8516 narrowed the mirror, so the block is collected + // here rather than deleted — the flip IS the record that the handoff was + // honoured, and a deletion would leave nothing that fails if the mirror is + // ever widened back. // - // Left open on a measurement, not a preference: closing it ships runtime - // bytes into the console's `framework` chunk (`packages/(core|react|types)`), - // which measured 70,999 gzip bytes against its 71,000 ceiling on this - // branch's base — one byte of headroom. A zod narrowing here would turn - // `Bundle Analysis` red and make its own fix a byte-hunt through two other - // packages, which is a different card. The TypeScript narrowing this card - // ships costs zero runtime bytes. - // - // This assertion is the HANDOFF (the objectui#7070 convention): when that - // card lands, this block flips to a refusal rather than quietly agreeing - // with whatever the mirror ends up doing. + // ⚠️ The blocker this block used to cite is GONE, and the obsolete number + // is left named on purpose: it said a narrowing was unaffordable because + // the console's `framework` chunk measured 70,999 gzip bytes against a + // 71,000 ceiling. PR #8550 landed the maintainer's raise — that ceiling is + // 100,000 against a re-derived 72,245 baseline — so the narrowing cost no + // byte-hunt at all. const r = GridZodMirror.safeParse({ type: 'grid', columns: { xxl: 6 } }); - expect(r.success).toBe(true); + expect(r.success).toBe(false); }); it('CONTROL — the mirror is live: it refuses a non-numeric column count', () => { @@ -282,4 +282,15 @@ describe('objectui#8505 — the zod mirror still admits an open record, delibera const r = GridZodMirror.safeParse({ type: 'grid', columns: { xs: 'two' } }); expect(r.success).toBe(false); }); + + it('CONTROL — and it still ACCEPTS a partial breakpoint map', () => { + // ⚠️ Added with the flip, and load-bearing because of it. Both readings + // above are now `false`, so on their own they are equally green against a + // mirror that refuses EVERYTHING — the caricature the header's table warns + // about, one authoring face down. This is also the overshoot guard: the + // `z.record(z.enum([…]), …)` spelling objectui#8516 refused reddens exactly + // here, because zod 4 makes it require all six members. + const r = GridZodMirror.safeParse({ type: 'grid', columns: { md: 2 } }); + expect(r.success).toBe(true); + }); }); diff --git a/packages/types/src/__tests__/mirror-partial-record-narrowing-8516.test.ts b/packages/types/src/__tests__/mirror-partial-record-narrowing-8516.test.ts new file mode 100644 index 0000000000..e1ce6f464d --- /dev/null +++ b/packages/types/src/__tests__/mirror-partial-record-narrowing-8516.test.ts @@ -0,0 +1,222 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#8516 + objectui#8556 — two mirrors keyed by their own declaration's + * vocabulary instead of by `string`. + * + * ## The class, and why it is one card's worth of repair twice + * + * A mirror restates a TS declaration by hand. Both keys below restated a CLOSED + * key set as an OPEN one, so the validator that judges authored JSON accepted a + * spelling the published types refuse: + * + * | key | mirror was | declaration | + * | :-- | :--------- | :---------- | + * | `GridSchema.columns` | `z.record(z.string(), z.number())` | `number` OR a partial breakpoint map | + * | `ReportComponentSchema.exportConfigs` | `z.record(z.string(), ReportExportConfigSchema)` | a partial per-format map | + * + * `@object-ui/cli`'s `validate` / `check` are the real consumers of these + * mirrors, so on the primary authoring surface of a server-driven-UI product a + * grid keyed `{ xxl: 6 }` passed validation and then rendered at its default + * column count with no error and no warning — objectui#7097's defect surviving + * on the face objectui#8505 did not touch. + * + * ## ⛔ The spelling that looks right and is not + * + * `z.record(z.enum([…]), …)` — MEASURED on zod 4.4.3, the version this package + * depends on, and re-measured here as an executable pin rather than quoted: + * + * ``` + * z.record(z.enum(SIX), z.number()).safeParse({ md: 2 }) + * -> success: false, five `invalid_type` issues, one per ABSENT member + * ``` + * + * zod 4's plain `z.record` over an enum key REQUIRES every member. So that + * spelling refuses the partial map both declarations explicitly invite — it + * clears the `WiderThanDeclared` reading and produces the opposite divergence + * on the same pair. `z.partialRecord` is the spelling that does not. + * + * The pins that catch the overshoot are `_columnsFace` / `_columnsIsPartial` + * and `_exportConfigsIsPartial` below (compile time) and the accepting rows in + * each `describe` (run time), plus the ledger reconciliation in the sibling + * `zod-mirror-parity.test.ts`, which is the file-level instrument objectui#8556 + * ruled this must be pinned against rather than against an accept set alone. + * + * ## Which program checks the type-level half + * + * `packages/types`' `type-check` runs THREE programs and this file is in the + * third, `tsconfig.test.json` — `tsc --noEmit` builds `tsconfig.json`, which + * excludes `__tests__/` by directory, so it reads none of the `Eq` constants + * below and is a FALSE GREEN for them. Confirmed with `--listFiles`, not + * assumed (the same trap `grid-columns-breakpoint-narrowing-8505.test.ts` + * records, and objectui#8342 before it). + */ + +import { describe, it, expect } from 'vitest'; +import { z } from 'zod'; +import type { GridSchema } from '../layout'; +import type { BreakpointName } from '../mobile'; +import type { ReportExportFormat, ReportComponentSchema } from '../reports'; +import { GridSchema as GridZodMirror } from '../zod/layout.zod'; +import { ReportComponentSchema as ReportZodMirror } from '../zod/reports.zod'; + +/** Mutual assignability, the standard invariant `Eq` — not `extends`. */ +type Eq = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) + ? true + : false; + +const SIX = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'] as const; + +/* ── (a) the two mirror faces, pinned against their declarations ──────────── */ + +type MirrorColumns = z.input; +type MirrorColumnMap = NonNullable>; +type MirrorExportConfigs = NonNullable>; + +describe('objectui#8516 — the grid mirror states the declaration, at compile time', () => { + it('the mirror`s INPUT face equals the declared member, both directions', () => { + // The reconciliation `zod-mirror-parity.test.ts` performs over every + // registered pair, restated locally for the one key this card repaired. + // Invariant, so it fails on a widening back to `Record` AND + // on a narrowing past the declaration. + const _columnsFace: Eq = true; + expect(_columnsFace).toBe(true); + }); + + it('the map arm is keyed by the breakpoint vocabulary, not by string', () => { + // The six names are spelled by hand in `layout.zod.ts` because `mobile.ts` + // has no zod mirror to derive them from. THIS is the derivation: drop a + // breakpoint from either face, or add a seventh, and it stops compiling. + const _columnKeys: Eq = true; + expect(_columnKeys).toBe(true); + }); + + it('the map arm is PARTIAL, not total — the zod-4 overshoot, at compile time', () => { + // `z.record(z.enum(SIX), z.number())` infers the TOTAL `Record`, so this + // constant is the compile-time half of the overshoot guard: it reddens on + // the forbidden spelling without anyone having to run a parse. + const _columnsIsPartial: Eq>> = true; + expect(_columnsIsPartial).toBe(true); + }); +}); + +describe('objectui#8556 — the report mirror states the declaration, at compile time', () => { + it('the map is keyed by ReportExportFormat, not by string', () => { + const _exportKeys: Eq = true; + expect(_exportKeys).toBe(true); + }); + + it('the map is PARTIAL, not total — objectui#6121`s ruling, held on the mirror', () => { + // objectui#6121's maintainer ruling removed the TOTAL `Record` from the + // TypeScript face because it made configuring ONE format an error. The + // forbidden `z.record(z.enum([…]), …)` spelling would re-impose exactly + // that on the mirror, one authoring face over. + const _exportConfigsIsPartial: Eq< + keyof MirrorExportConfigs, + keyof Partial> + > = true; + expect(_exportConfigsIsPartial).toBe(true); + }); + + it('the declaration this mirror restates is itself partial', () => { + const _declared: Eq< + NonNullable, + Partial[ReportExportFormat]>> + > = true; + expect(_declared).toBe(true); + }); +}); + +/* ── (b) the accept set, at run time ──────────────────────────────────────── */ + +describe('objectui#8516 — GridSchema.columns accept set', () => { + it('accepts the bare number arm', () => { + expect(GridZodMirror.safeParse({ type: 'grid', columns: 3 }).success).toBe(true); + }); + + it.each(SIX)('accepts a one-key map for `%s`', (bp) => { + // ⭐ The overshoot guard at run time, one row per member: under + // `z.record(z.enum(SIX), …)` every one of these six rows fails, because + // each names ONE key and zod 4 then demands all six. + expect(GridZodMirror.safeParse({ type: 'grid', columns: { [bp]: 2 } }).success).toBe(true); + }); + + it('accepts the full six-key map, and the empty map', () => { + const full = Object.fromEntries(SIX.map((k, i) => [k, i + 1])); + expect(GridZodMirror.safeParse({ type: 'grid', columns: full }).success).toBe(true); + expect(GridZodMirror.safeParse({ type: 'grid', columns: {} }).success).toBe(true); + }); + + it.each(['xxl', 'XL', '2XL', 'mobile'])('refuses the out-of-vocabulary key `%s`', (bad) => { + // `xxl` is not an arbitrary bad key: it is the Bootstrap/Ant spelling, the + // one an author reaches for first, and the renderer silently ignores it. + expect(GridZodMirror.safeParse({ type: 'grid', columns: { [bad]: 6 } }).success).toBe(false); + }); + + it('names the offending key in the issue an author is shown', () => { + // Q1, measured rather than assumed. `columns` is a UNION, so zod reports one + // top-level `invalid_union` whose message is the bare "Invalid input"; the + // CLI's `explainUnionIssue` expands an undiscriminated union's arms and + // rebases their paths, which is where the key surfaces. `z.partialRecord` + // puts it in the PATH (`columns -> xxl`); the `.strict()` object spelling + // would put it in the MESSAGE instead. Both name it; this pins which. + const r = GridZodMirror.safeParse({ type: 'grid', columns: { xxl: 6 } }); + expect(r.success).toBe(false); + const arms = (r.error!.issues[0] as { errors?: { path: PropertyKey[]; code?: string }[][] }).errors ?? []; + const flat = arms.flat(); + expect(flat.some((i) => i.code === 'invalid_key' && i.path.includes('xxl'))).toBe(true); + }); +}); + +describe('objectui#8556 — ReportComponentSchema.exportConfigs accept set', () => { + const base = { type: 'report' as const }; + + it.each(['pdf', 'excel', 'csv', 'json', 'html'])('accepts a one-format map for `%s`', (fmt) => { + // The same per-member overshoot guard as the grid rows above. + const doc = { ...base, exportConfigs: { [fmt]: { format: fmt } } }; + expect(ReportZodMirror.safeParse(doc).success).toBe(true); + }); + + it('accepts the empty map', () => { + expect(ReportZodMirror.safeParse({ ...base, exportConfigs: {} }).success).toBe(true); + }); + + it('refuses a key outside ReportExportFormat', () => { + // The exact document objectui#8556 measured as parsing green. + const doc = { ...base, exportConfigs: { xml: { format: 'pdf' } } }; + expect(ReportZodMirror.safeParse(doc).success).toBe(false); + }); + + it('CONTROL — the mirror still judges the VALUE, not only the key', () => { + // Without this, every refusal above would read the same against a mirror + // that refuses everything. + const doc = { ...base, exportConfigs: { pdf: { format: 'xml' } } }; + expect(ReportZodMirror.safeParse(doc).success).toBe(false); + }); +}); + +/* ── (c) the zod-4 semantics the repair turns on, pinned executably ───────── */ + +describe('objectui#8516 — why `z.record(z.enum([…]), …)` is the wrong spelling', () => { + it('the plain record over an enum key REQUIRES every member', () => { + // The measurement both cards carry, executable so it cannot rot into + // folklore: if a future zod makes this spelling partial, this row fails and + // the prose above is re-read rather than trusted. + const forbidden = z.record(z.enum(SIX), z.number()); + const r = forbidden.safeParse({ md: 2 }); + expect(r.success).toBe(false); + expect(r.error!.issues.filter((i) => i.code === 'invalid_type')).toHaveLength(5); + }); + + it('…while `z.partialRecord` over the same key accepts it', () => { + const chosen = z.partialRecord(z.enum(SIX), z.number()); + expect(chosen.safeParse({ md: 2 }).success).toBe(true); + expect(chosen.safeParse({ xxl: 6 }).success).toBe(false); + }); +}); diff --git a/packages/types/src/zod/layout.zod.ts b/packages/types/src/zod/layout.zod.ts index 90ba934fbd..80248353d4 100644 --- a/packages/types/src/zod/layout.zod.ts +++ b/packages/types/src/zod/layout.zod.ts @@ -220,9 +220,24 @@ export const StackSchema = BaseSchema.extend({ */ export const GridSchema = BaseSchema.extend({ type: z.literal('grid'), + /** + * Keyed by the BREAKPOINT VOCABULARY, not by `string` (objectui#8516). + * + * `z.partialRecord`, ⛔ never `z.record(z.enum([…]), …)`: measured on zod + * 4.4.3, the plain `z.record` over an enum key REQUIRES every member, so + * `{ md: 2 }` — which the declaration explicitly invites, and which + * `grid-breakpoint-columns-7097.test.tsx` pins the renderer as reading — + * stops parsing. That spelling trades this divergence for its opposite. + * + * The six names are the whole of `BreakpointName` (`../mobile.ts`). They are + * spelled here rather than derived because `mobile.ts` has no zod mirror to + * derive from; the equality is held by a type-level pin in + * `__tests__/mirror-partial-record-narrowing-8516.test.ts`, so adding or + * dropping a breakpoint on either face fails to compile. + */ columns: z.union([ z.number(), - z.record(z.string(), z.number()), + z.partialRecord(z.enum(['xs', 'sm', 'md', 'lg', 'xl', '2xl']), z.number()), ]).optional().describe('Number of columns (responsive)'), gap: z.number().optional().describe('Gap between items (Tailwind scale 0-8)'), children: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional(), diff --git a/packages/types/src/zod/reports.zod.ts b/packages/types/src/zod/reports.zod.ts index a254530612..14e7828dd6 100644 --- a/packages/types/src/zod/reports.zod.ts +++ b/packages/types/src/zod/reports.zod.ts @@ -160,7 +160,17 @@ export const ReportComponentSchema = BaseSchema.extend({ sections: z.array(ReportSectionSchema).optional().describe('Report sections'), schedule: ReportScheduleSchema.optional().describe('Schedule configuration'), defaultExportFormat: ReportExportFormatSchema.optional().describe('Default export format'), - exportConfigs: z.record(z.string(), ReportExportConfigSchema).optional().describe('Export configurations'), + /** + * Keyed by `ReportExportFormatSchema`, not by `string` (objectui#8556): the + * declaration has been `Partial>` since + * objectui#6121 and this mirror still admitted any string key. + * + * `z.partialRecord`, ⛔ never `z.record(ReportExportFormatSchema, …)`: + * measured on zod 4.4.3, the plain `z.record` over an enum key REQUIRES every + * member, which is exactly the total-`Record` authoring face objectui#6121's + * maintainer ruling removed from the TypeScript side. + */ + exportConfigs: z.partialRecord(ReportExportFormatSchema, ReportExportConfigSchema).optional().describe('Export configurations'), showExportButtons: z.boolean().optional().describe('Show export buttons'), showPrintButton: z.boolean().optional().describe('Show print button'), showScheduleButton: z.boolean().optional().describe('Show schedule button'), From 15250f2b6516ec4ed2af6f8af7de7231a29ab3ac Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 13:09:42 +0000 Subject: [PATCH 2/3] docs(changeset): correct objectui#8505's changeset, which this PR falsified Three statements in `.changeset/8505-grid-columns-breakpoint-narrowing.md` became false because of this PR, and a PR that falsifies pending release text owns correcting it. The changeset is unreleased, so this keeps pending text true rather than rewriting a record. Corrected, each naming #8573 as the cause: the zod mirror is no longer left open; the 70,999-against-71,000 byte reasoning that deferred the narrowing is retired by PR #8550's raise to 100,000 against a 72,245 baseline; and the handoff assertion no longer asserts acceptance. Each correction quotes the new state rather than re-arguing the measurement. Nothing else in the file is touched -- the frontmatter and the other four paragraphs are byte-identical. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE --- .../8505-grid-columns-breakpoint-narrowing.md | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/.changeset/8505-grid-columns-breakpoint-narrowing.md b/.changeset/8505-grid-columns-breakpoint-narrowing.md index 09e11a373c..e12009e161 100644 --- a/.changeset/8505-grid-columns-breakpoint-narrowing.md +++ b/.changeset/8505-grid-columns-breakpoint-narrowing.md @@ -31,13 +31,17 @@ from a computed `Record` still type-checks, and only fresh objec LITERALS meet excess-property checking. The narrowing is therefore a check on the authoring spelling, which is where the defect was authored. -**The zod mirror is deliberately NOT narrowed here.** `zod/layout.zod.ts` still -validates `columns` as `z.record(z.string(), z.number())`, so the JSON authoring -face — `os-ui validate` / `check` in `@object-ui/cli`, the real consumer of these -mirrors — still admits `{ xxl: 6 }`. That is reported for its own card, on a -measurement: closing it ships runtime bytes into the console's `framework` chunk -(`packages/(core|react|types)`), which measured 70,999 gzip bytes against its -71,000 ceiling on this branch's base — one byte of headroom. The current reading is -held visible by a handoff assertion in the new test, with a lit control next to it, -so it flips to a refusal when that card lands rather than rotting into an -assumption that both faces closed together. +**The zod mirror WAS deliberately not narrowed here — corrected in #8573.** This +paragraph said `zod/layout.zod.ts` still validates `columns` as +`z.record(z.string(), z.number())`, so the JSON authoring face — `os-ui validate` +/ `check` in `@object-ui/cli`, the real consumer of these mirrors — still admits +`{ xxl: 6 }`. #8573 narrowed that mirror to a partial record over the six +breakpoints (objectui#8516), so the JSON face now refuses `{ xxl: 6 }` exactly as +the declaration above does. Its two supporting statements went with it, and #8573 +quotes each in its current state rather than re-arguing it: the byte measurement +that deferred the narrowing — 70,999 gzip bytes against a 71,000 `framework` +ceiling, one byte of headroom — is retired, because PR #8550 landed the +maintainer's raise to `PER_CHUNK_GZIP_CEILINGS.framework` `100_000` against a +`PER_CHUNK_BASELINE.framework` of `72_245`; and the handoff assertion described +here as holding the current reading visible is flipped to a refusal in #8573, so +it no longer asserts acceptance. From c77dfeb6d0acd8112ad37ce01e1e457d36c714e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 13:28:19 +0000 Subject: [PATCH 3/3] test(types): replace an inert overshoot pin, and correct two corpus figures Contract review found `_exportConfigsIsPartial` inert against the overshoot its own row is named for: it compared `keyof` of the two maps, and `keyof` erases optionality, so a total and a partial record over the same key set are the same type there. Measured: under the forbidden-spelling ablation it did not appear among the compile errors while its grid counterpart, which compares the whole map type, did. It now compares the map to its own `Partial` -- a fixed point only when the map is already partial -- and the header sentence says why `keyof` is banned here, so the spelling is not reintroduced. Two figures in the changeset were also wrong. `exportConfigs` was reported as having zero authored inhabitants; it has one, `content/docs/core/report-schema.mdx`, keyed pdf/excel/csv and so in vocabulary. The scan that produced the zero could not have found a true positive: its pattern forbade nested braces, and every real `exportConfigs` value contains per-format objects. The `columns` count is restated with test fixtures separated from authored sites rather than folded in. Neither correction moves the conclusion -- zero documents that exist today are refused. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE --- ...16-8556-mirror-partial-record-narrowing.md | 30 +++++++++++++------ ...rror-partial-record-narrowing-8516.test.ts | 24 ++++++++++----- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/.changeset/8516-8556-mirror-partial-record-narrowing.md b/.changeset/8516-8556-mirror-partial-record-narrowing.md index 537d5b0b8d..f574ca52d0 100644 --- a/.changeset/8516-8556-mirror-partial-record-narrowing.md +++ b/.changeset/8516-8556-mirror-partial-record-narrowing.md @@ -31,15 +31,27 @@ being silently ignored; it refuses nothing that works today. names the offending key — `Path: columns → xxl`, `Code: invalid_key` — through `@object-ui/cli`'s union-arm expansion. -**Measured accept set in the corpus, before grading this.** Across every tracked -file in this repository and in the `objectstack` sibling checkout: 33 grid nodes -carry an object-valued `columns`, and every out-of-vocabulary key among them is -a deliberate negative fixture inside objectui#8505's own test file. The authored -corpus — `examples/schema-catalog`, `content/docs`, `skills/objectui`, -`apps/site`, the renderer fixtures — is 28 sites, all six-breakpoint-clean. -`exportConfigs` has zero authored inhabitants in either repository. So this -narrowing refuses **zero** documents that exist today, which is why it is graded -`minor` with the break spelled out rather than escalated. +**Measured accept set in the corpus, before grading this.** Counted over every +tracked file in this repository and in the `objectstack` sibling checkout, with +test fixtures and changeset prose separated out rather than folded in: + +- grid nodes carrying an object-valued `columns` — **10 authored sites** (nine in + `content/docs`, `examples/schema-catalog` and `skills/objectui`, one in + `apps/site`), **all keyed inside the six breakpoints**. Every out-of-vocabulary + key in either tree sits in a test fixture written to document the defect, or in + objectui#8505's changeset quoting it. +- `exportConfigs` — **one authored site**, `content/docs/core/report-schema.mdx`, + keyed `pdf` / `excel` / `csv`, all **in vocabulary**. None in `objectstack`. + +So this narrowing refuses **zero** documents that exist today, which is why it is +graded `minor` with the break spelled out rather than escalated. + +**What it does NOT close.** These mirrors judge the document they are handed. A +`grid` nested under another node's `children` still reaches +`SchemaNodeSchema`, a lazy union over the passthrough base that does not re-enter +the per-type arms, so `{ type: 'container', children: [{ type: 'grid', columns: +{ xxl: 6 } }] }` still validates green. That is pre-existing and untouched here — +named so this change is not read as closing the nested case. **The spelling is `z.partialRecord`, and that is load-bearing.** ⛔ Not `z.record(z.enum([…]), …)`: measured on zod 4.4.3, the plain record over an enum diff --git a/packages/types/src/__tests__/mirror-partial-record-narrowing-8516.test.ts b/packages/types/src/__tests__/mirror-partial-record-narrowing-8516.test.ts index e1ce6f464d..3c703cae3f 100644 --- a/packages/types/src/__tests__/mirror-partial-record-narrowing-8516.test.ts +++ b/packages/types/src/__tests__/mirror-partial-record-narrowing-8516.test.ts @@ -42,9 +42,10 @@ * clears the `WiderThanDeclared` reading and produces the opposite divergence * on the same pair. `z.partialRecord` is the spelling that does not. * - * The pins that catch the overshoot are `_columnsFace` / `_columnsIsPartial` - * and `_exportConfigsIsPartial` below (compile time) and the accepting rows in - * each `describe` (run time), plus the ledger reconciliation in the sibling + * The pins that catch the overshoot are `_columnsFace` / `_columnsIsPartial` and + * `_exportConfigsIsPartial` below (compile time — each compares a WHOLE map type, + * ⛔ never a `keyof`, which erases the optionality that IS the overshoot) and the + * accepting rows in each `describe` (run time), plus the ledger reconciliation in the sibling * `zod-mirror-parity.test.ts`, which is the file-level instrument objectui#8556 * ruled this must be pinned against rather than against an accept set alone. * @@ -117,10 +118,19 @@ describe('objectui#8556 — the report mirror states the declaration, at compile // TypeScript face because it made configuring ONE format an error. The // forbidden `z.record(z.enum([…]), …)` spelling would re-impose exactly // that on the mirror, one authoring face over. - const _exportConfigsIsPartial: Eq< - keyof MirrorExportConfigs, - keyof Partial> - > = true; + // + // ⚠️ This pin was FIRST WRITTEN AS `Eq[keyof MirrorExportConfigs, keyof + // Partial[Record[ReportExportFormat, unknown]]]` and that spelling was INERT + // against the very overshoot this row is named for: `keyof` erases + // optionality, so `keyof Record[F, V]` and `keyof Partial[Record[F, V]]` are + // the same type and the constant stayed `true` under the forbidden spelling. + // Measured, not reasoned: under ablation B it did not appear among the + // compile errors, while its grid counterpart `_columnsIsPartial` — which + // compares the WHOLE map type — did. A pin that cannot fail is worse than no + // pin, because it is counted as coverage. ⛔ Do not reintroduce a `keyof` + // comparison here; compare the map to its own `Partial`, which is a fixed + // point only when the map is already partial. + const _exportConfigsIsPartial: Eq> = true; expect(_exportConfigsIsPartial).toBe(true); });