fix(types): key the grid and report zod mirrors by their own declaration vocabulary (objectui#8516, objectui#8556) - #8573
Conversation
…, not by string `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<Record<…>>`, 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…ror-partial-record-narrowing
…sified 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE
In-seat contract review — VERDICT: PASS (head
|
…igures 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Landing check ② is UNSATISFIABLE here — recorded, ⛔ not passed silently, ⛔ not bought with a false declarationDispatching seat. This PR is green on ① and ③ and not enqueued, because ② cannot be satisfied without writing something untrue.
C2 is fixed. Both cards were missing the declaration line in the carrier the gate actually reads. #8556's packed pointer comment carried no C5 cannot be cleared, and that is a property of the tell, not of this PR. It fires on the CONSTRUCTOR and has no notion of direction:
The contract review measured the actual direction: 357 ⇒ The tell says "wider", the measurement says "narrower", and the checker offers exactly one route to exit 0: re-declare Disposition. PR stays a draft, ⛔ not enqueued by this seat. Everything a merger needs is green and measured; the one red is a checker that cannot express "narrower". Whoever enqueues this should do so knowing ② is red for that reason and ⛔ not because the declaration is wrong. Generated by Claude Code |
Enqueued with ② red — the decision, and whose it isDispatching seat, Not a governed surface, verified rather than assumed: Landing checks:
On ②, and I am recording this as my call rather than leaving it implicit. The C5 tell fires on the constructor and has no notion of direction: both flagged lines are replacements — What ② exists to protect has been checked by a stronger instrument: the contract review measured the actual direction — 357
Generated by Claude Code |
Brings 27 commits of `main` onto this branch and resolves the one conflicting
path, `packages/types/src/__tests__/zod-mirror-parity.test.ts`. Both sides had
rewritten the same header bullet for `WiderThanDeclared`.
Kept both intents:
* main's objectui#8338 retirement of `feedback.zod.ts#ToastSchema::action`
(its ledger entry, arm row and header history), objectui#8248's LIVE /
HISTORICAL ruling and objectui#8458's movement pin;
* this branch's OPEN-RECORD operator clause, its synthetic recognition pins
and the caricature negative.
What the merge falsified, measured rather than assumed. PR #8573
(`d4733f27e`, objectui#8516 / objectui#8556) narrowed BOTH mirrors this card
ledgered — `GridSchema.columns` and `ReportComponentSchema.exportConfigs` — to
`z.partialRecord`, so the operator reports neither on the merged tree. The two
ledger entries and their arm rows are removed (with the entries in place,
`tsc -p tsconfig.test.json` reddens at `assertionWiderMatchesLedger` and
`assertionWiderLedgerRecordsEveryKey`), and the runtime describe block that
asserted `safeParse({ columns: { xxl: 6 } })` returns green is removed because
it no longer does — that reachability is now pinned, inverted, by main's own
`mirror-partial-record-narrowing-8516.test.ts`.
Every header figure is re-derived from the tree by the file's own pins, not
stepped by hand: `WiderThanDeclared` reads 22 / 35 / 45 — 6 / 29 / 0 / 10,
which is main's reading unchanged, because the clause moves no live key today.
Three prose figures that the merge left standing without an instrument are
anchored to the revision they were read at, and one bare count no pin reached
is removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
The `WiderArmClass` docblock listed OPEN-RECORD among the sub-classes "the docblocks above name", which was true while this branch carried two ledger rows for it. PR #8573 narrowed both mirrors, the rows are gone, and no docblock above names one — so the sentence was a sub-class name with nothing behind it, which is the exact rot this file exists to catch. Reverts the list to what `main` has and records why OPEN-RECORD is absent, so the next reader does not "restore" it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Fixes #8516
Fixes #8556
Partial[Record[BreakpointName, number]], not the real spelling. AGENTS.md records that GitHub silently deletes tag-shaped fragments on save, backticks and fenced blocks included, and this PR is entirely about the shape of a type.What changed
Two mirrors in
@object-ui/types/zodrestated a CLOSED key set as an OPEN one, so the validator that judges authored JSON accepted a spelling its own published declaration refuses. One defect class, one repair, landed together.GridSchema.columns(zod/layout.zod.ts)z.record(z.string(), z.number())z.partialRecordover the six breakpointsnumberORPartial[Record[BreakpointName, number]](objectui#8505)ReportComponentSchema.exportConfigs(zod/reports.zod.ts)z.record(z.string(), ReportExportConfigSchema)z.partialRecord(ReportExportFormatSchema, …)Partial[Record[ReportExportFormat, ReportExportConfig]](objectui#6121)⛔ Not
z.record(z.enum([…]), …). Measured here on zod 4.4.3, not quoted: that spelling makes zod require every member, so{ md: 2 }fails with fiveinvalid_typeissues, one per absent key. It would trade each divergence for its exact opposite. The measurement is now an executable pin rather than folklore, and the firing control below reproduces it.The three shape questions, answered by measurement
Q1 — refuse or strip, and which diagnostic does an author actually see? Both candidates have the right accept set; they differ only in where the bad key surfaces. Measured through the real printer path (
columnsis an undiscriminated union, so zod emits one top-levelinvalid_unionwhose message is a bare "Invalid input";@object-ui/cli'sexplainUnionIssueexpands the arms and rebases their paths):os-ui validateprintsz.objectof six optionals +.strict()Unrecognized key: "xxl"·Path: columns·Code: unrecognized_keysz.partialRecord(chosen)Invalid key in record·Path: columns → xxl·Code: invalid_keyBoth name
xxl;.strict()puts it in the message,partialRecordputs it in the path. Chosenz.partialRecordon three readings the diagnostic does not decide: its inferred input type is exactly the declaration (pinned invariantly, both directions, as_columnsFace); it reuses the already-exportedReportExportFormatSchemaverbatim on the second key, where.strict()would hand-restate five format names; and it avoids writing a seventh hand-copy of the six-member breakpoint object, which the triage comment on #8516 explicitly warned against. Contract review added a fourth reading this PR had not claimed:z.partialRecordclones its key schema, so reusing the sharedReportExportFormatSchemahere leaves that const's own_zod.valuesintact anddefaultExportFormat: 'xml'on the same node is still refused. Both spellings REFUSE rather than strip —BaseSchemais.passthrough(), butcolumnsis a declared member.Q2 — breaking for stored metadata? Measured across every tracked file in this repo and the
objectstacksibling checkout, with test fixtures and changeset prose separated out rather than folded into the authored total:columnscontent/docs,examples/schema-catalog,skills/objectui)apps/site)objectstacksibling checkoutexportConfigs: one authored site —content/docs/core/report-schema.mdx:372, keyedpdf/excel/csv, all in vocabulary. None inobjectstack.⇒ This narrowing refuses zero documents that exist today, and the ones it would refuse are already rendering wrong. Graded
minorwith the break spelled out, per objectui#8485's precedent; this repo refusesmajor.exportConfigswas reported as having zero authored inhabitants. The scan that produced that zero could not have found a true positive: its pattern required the value's braces to contain no nested braces, and every realexportConfigscontains per-format objects — an unfalsifiable zero of exactly the shape AGENTS.md warns about. Re-run brace-balanced, it is one, and in vocabulary, so the conclusion is unchanged. (b) The authoredcolumnstotal was given as 28, which was both an arithmetic slip and a category error: it subtracted the out-of-vocabulary sites from the grand total and then called the remainder "authored", silently counting renderer and type test fixtures as authored documents. The defined authored total is 10.main), so it is most likely a third partition of the same population — plausibly non-test files plus some fixtures, or themaintree rather than this branch. The number above is stated with its method (git ls-filesover both trees, brace-balanced extraction, atype: 'grid'requirement, categorised by path) so it can be re-run and disagreed with precisely. All three readings agree on the load-bearing fact, which is the one the grade rests on: zero authored documents carry an out-of-vocabulary key.Q3 — does the sibling
responsive-gridmirror move? No, and it is left alone.@object-ui/layout'sBreakpointColumnMap(packages/layout/src/ResponsiveGrid.tsx) already declares exactly the sixxs…2xloptional numbers by hand and already agrees. It is also not a mirror in the relevant sense:packages/layout/src/contains no.zod.tsfile at all andResponsiveGrid.tsximports no zod — it is a React props type, not an authoring face. Nothing to move.Hypotheses — one falsified, one unexercised, one confirmed
H1 — "the ledger already SEES both keys" is FALSE. PR #8553 has not landed: it is still
open,merged: false, againstmain. So the repairedWiderThanDeclaredoperator is not onmain, the pre-repair operator still reads an index-signature record and a partial record over a key union as mutually assignable, and neither key has, or ever had, a ledger entry. Both cards' framing and the dispatch's serial constraint were written expecting the opposite.The falsification is not just a paperwork note — it is visible in the firing control: removing this repair reddens 7 runtime pins and 5 compile-time ones but leaves
zod-mirror-parity.test.tsgreen, which is precisely the blindness objectui#8553 was opened to repair.H2 — the ledger counter conflict never arose. The dispatch cleared a serial constraint on the premise that both cards edit
zod-mirror-parity.test.tsand would collide with PR #8501's patch round. Neither card required editing that file, because neither key had an entry to move. Re-derived with the file's OWN AST instrument (ledgerEntryKeys, the same walk the file runs on itself) atorigin/main e411c3e58and at this branch's head — identical in both directions:MIRRORSpairsKnownDriftRuntimeOnlyDeclaredUnmirroredDeclaredWiderThanDeclaredNo hand-reconciliation was performed and none was needed. The file's own header pins ("the ledger entry counts the header states are derived, not prose", and the key-total pin beside it) are green.
H3 — confirmed and collected.
grid-columns-breakpoint-narrowing-8505.test.ts's last block assertedsuccess: truefor thexxlnode as a handoff pin. It is flipped, not deleted. It also gained a positive control: with the flip, both readings in that block are nowfalse, so on their own they would be equally green against a mirror that refuses everything — the caricature that file's own header warns about. The new row asserts{ md: 2 }is still accepted, which is also where thez.enumovershoot lands.What this PR does NOT close
These mirrors judge the document they are handed, and only that document. A
gridnested under another node'schildrenis reached throughSchemaNodeSchema(base.zod.ts), a lazy union over the passthroughBaseSchemaCorethat never re-enters the per-type arms — so the nested case is still accepted:safeValidateSchema{ type: 'grid', columns: { xxl: 6 } }{ type: 'container', children: [ { type: 'grid', columns: { xxl: 6 } } ] }Both rows were run, not reasoned about. This is pre-existing and untouched here — this PR neither widens nor narrows the nested path — and it is stated so that nobody reads this PR as having closed it.
These two PRs touch the same ledger from opposite ends, and the collision is real. Stated as measured, ⛔ with no claim about which should land first — that is the dispatching seat's call, and it is recorded on #8553 as well.
maintoday, neither key has aWiderThanDeclaredentry. PR test(types): teach the WiderThanDeclared operator to see an open-record mirror #8553 is still open, so the pre-repair operator reads an index-signature record and a partial record over a key union as mutually assignable and reports both pairs as clean. Re-derived with the parity file's own AST instrument atorigin/mainand at this branch's head:WiderThanDeclaredis 22 entries / 35 keys on both sides.mainas it stands, and it is what that PR exists to make visible.assertionDriftMatchesLedgerreddens on them. It reddens in the merge queue, on the rebuilt candidate — so it ejects that PR from the queue rather than breakingmain. The failure is loud and contained; it is not a silent landing.describeblock that runs the published validator on documents the published types refuse, and this PR flips both of its readings:ca7a9cc74)GridSchema.safeParse({ type: 'grid', columns: { xxl: 6 } }).successistruefalseReportComponentSchemaaccepts anxmlexport format⭐ #8553 anticipated this in that block's own comment — it says the repair should redden there, where the ledger entry is, "instead of passing silently". So the collision is by design on its side, not a surprise; what it needs is a decision about order.
WiderThanDeclaredrows (layout.zod.ts#GridSchema/columnsat test(types): teach the WiderThanDeclared operator to see an open-record mirror #8553's line 2474,reports.zod.ts#ReportComponentSchema/exportConfigsat 2532) and the two runtime assertions above — as part of its own merge, not as a follow-up.Firing controls
Run from the committed tree, each mutation proven on disk by counting the target and injected text in both directions, each restore proven by
git diff HEADempty andgit hash-objectbyte-identity against the HEAD blobs.tsc -p tsconfig.test.jsonz.record(z.enum([…]), …)spelling_exportConfigsIsPartialwas first written as anEqoverkeyofof the two maps.keyoferases optionality, sokeyof Record[F, V]andkeyof Partial[Record[F, V]]are the same type, and the constant stayed green under the very overshoot its row is named for — it did not appear among B's errors, while its grid counterpart_columnsIsPartial, which compares the whole map type, did. It now compares the map to its ownPartial, a fixed point only when the map is already partial, and it is line 133 in B's error list — shown red rather than asserted to work. A pin that cannot fail is worse than no pin, because it is counted as coverage; the file records the banned spelling beside the fixed one so it is not reintroduced.Ablation B's load-bearing compile error is the fourth:
That is
assertionDriftMatchesLedger— the ledger reconciliation objectui#8556 ruled this must be pinned against rather than an accept set alone. The overshoot hazard is therefore verified on this tree, not inherited from objectui#8517's report. Its 22 runtime failures are exactly the per-member accepting rows: six breakpoints, five formats, the empty maps, and the flipped block's new positive control.Bytes — the stated blocker, re-measured
#8516 said a zod narrowing here was unaffordable: the console
frameworkchunk measured 70,999 gzip bytes against a 71,000 ceiling. PR #8550 landed the maintainer's raise — the ceiling is100_000against a re-derived72_245baseline, ~27.7 KB of headroom.PER_CHUNK_GZIP_CEILINGSandPER_CHUNK_BASELINEare untouched here.The actual cost of this diff, measured on the two emitted modules (comment-stripped, gzip -9): +24 gzipped bytes (+63 raw), or 0.09% of the available headroom. Under the old ceiling it would indeed have been 24 bytes over — the blocker was real, and it is now gone. The authoritative gauge is CI's
Bundle Analysisover the whole chunk.Verification
@object-ui/typestestspnpm exec vitest run packages/types/@object-ui/typestype-checkpnpm --filter @object-ui/types type-check(all THREE programs;tsc --noEmit -p tsconfig.jsonalone is a known false green for this ledger)tsc -p tsconfig.test.json --listFileszod-mirror-parity.test.tspresentvitest run packages/cli/ packages/components/ packages/plugin-report/ examples/schema-catalog/turbo run type-checkforcli,components,plugin-reportcheck-changeset-presence.mjs,check-changeset-no-major.mjscheck:control-bytescheck:spec-symbolscheck:dist-completenesscheck:self-import,check:esm-specifierseslint --no-inline-config --format jsonover the 4 changed filesEvery exit code was captured by redirect before any pipe.
Merged, not rebased.
origin/mainwas merged in at1cca4415e; the two incoming commits touchpackages/coreandpackages/app-shelland neither touchespackages/types—git diffoverzod-mirror-parity.test.tsacross the merge is empty. Re-run afterwards on the merged head: all threetype-checkprograms exit 0,packages/typesis 148 files / 2837 tests green, andpackages/cli+packages/plugin-reportare 31 files / 429 tests green. The ledger counts were re-derived after the merge and did not move —MIRRORS158 pairs,KnownDrift41/63,RuntimeOnlyDeclared7/24,UnmirroredDeclared14/86,WiderThanDeclared22/35, identical to the pre-merge reading. Nothing was reconciled by hand.Declared narrowing on lint. The repository-wide
eslint . --no-inline-configis left to CI. The narrowing to 4 files is a measurement, not an omission: the population is read from eslint's own config, the count of 4 from its--format jsonoutput, and the invariance claim is thateslint.config.jsenables no type-aware linting — it extendstseslint.configs.recommended, notrecommendedTypeChecked, and itslanguageOptionsdeclares noparserOptions.projectorprojectService. With no type information in scope, this diff cannot move the verdict on any file it did not touch.check:readme-exportsis NOT MEASURED, not red. It failed with 508 "type entry./dist/index.d.tsis not on disk — runpnpm buildfirst" againstapp-shell,auth,collaborationand other packages this diff does not touch; only@object-ui/typeswas built in this worktree. Its prerequisite is a full-repo build, which CI performs.check:eager-closureis declared to CI. It readsapps/console/dist/eager-closure.json, which requires a console build; the byte arithmetic above stands in for it locally.验收备注
Out-of-scope observations from this run. ⛔ None is filed as a card and none is repaired here.
The PR test(types): teach the WiderThanDeclared operator to see an open-record mirror #8553 sequencing hazard has its own section above, because whoever merges this needs it without hunting for it.
✅ Now repaired in this PR, on a ruling.
.changeset/8505-grid-columns-breakpoint-narrowing.mdended with three statements this PR falsified: that the zod mirror is deliberately not narrowed, the retired 70,999-against-71,000 byte reasoning, and a description of the handoff assertion as asserting acceptance. It was first reported here rather than edited unilaterally; the dispatching seat ruled it in scope, on the grounds that the changeset is unreleased and a PR that falsifies pending text owns correcting it. All three are corrected in a second commit, each naming fix(types): key the grid and report zod mirrors by their own declaration vocabulary (objectui#8516, objectui#8556) #8573 and quoting the new state rather than re-arguing it. The frontmatter and the file's other four paragraphs are byte-identical.check:readme-exportscannot run in a worktree that has not built every package (508 "type entry not on disk" reports against packages this diff does not touch). That is its documented prerequisite, not a defect; noted only so the NOT-MEASURED row in the table above is not read as a red gate.Carrier: this PR's reviewer, plus whoever sequences #8553.
Scope
Six files, all inside the declared surface (the sixth is objectui#8505's changeset, corrected on the ruling recorded above). Contract review returned PASS on the contract path limb. No new export and no new error code, so
Clause-②staysno: both repairs are pull-backs to already-published declarations and both accept sets get strictly narrower. Net +314 lines against the PM's ≤ 350 budget.@object-ui/layout,PER_CHUNK_GZIP_CEILINGSandPER_CHUNK_BASELINEwere not touched.⛔ Left as a draft deliberately:
*.zod.tsis the contract path limb, so the pre-enqueue in-seat contract review is the dispatching seat's step, not this one's.needs:contract-reviewis not pre-hung.Session:
session_01CZY49skxUBYyJcdnTcYPrE🤖 Generated with Claude Code
https://claude.ai/code/session_01CZY49skxUBYyJcdnTcYPrE
Generated by Claude Code