test(types): teach the WiderThanDeclared operator to see an open-record mirror - #8553
test(types): teach the WiderThanDeclared operator to see an open-record mirror#8553os-justin wants to merge 5 commits into
Conversation
…rd mirror `zod-mirror-parity.test.ts` decided the WIDER direction with one assignability test, and that test cannot separate `Record[string, V]` from `Partial[Record[UNION, V]]` in EITHER direction — the two are mutually assignable, because TypeScript grants an implicit index signature to mapped and anonymous object types. So a mirror spelled `z.record(z.string(), V)` read as clean against a declaration over a finite key union while genuinely accepting keys the declaration refuses, and both parity directions agreed it was clean (objectui#8517). The repair adds an OPEN-RECORD clause on the branch the assignability test already called clean, so it is additive by construction: a key reported before cannot stop being reported. It reports only the unambiguous case — the mirror has a union arm that is a string-index record with no literal members of its own (the `z.record(z.string(), V)` shape and nothing else), and the declaration has a finite-keyed arm and no open one. Two halves of the predicate were MEASURED rather than reasoned about, each after the pin written for it failed. A zod LOOSE object also answers `true` to `string extends keyof T`, so requiring no literal members is what keeps the whole passthrough population out; and `keyof number` resolves to that primitive's method names, so `FiniteKeyedArms` has to exclude non-objects or a permissive `number | object` declaration reads as narrow. Census delta, measured with the file's own ledger reconciliation: 2 pairs and 2 keys ENTER, none leave. `layout.zod.ts#GridSchema::columns` (objectui#8516's instance) and `reports.zod.ts#ReportComponentSchema::exportConfigs`, which was not known to exist. Both are demonstrated at runtime as accept-set gaps — `safeParse` green, `tsc` refusing the same document — so neither can be read as an artifact of the new clause. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
The caricature docblock said the naive `string extends keyof T` clause "moved 40 pairs of the registry rather than 2", under a sentence reading "Measured, not predicted". The 40 was never measured — it was written from expectation before the ablation ran, which is the exact defect this file exists to catch. Measured: the naive clause moves 3 pairs where the shipped one moves 2. It does not flood the ledger, because it sits on the branch the assignability test already called clean and `Unconstrained` gates ahead of both. Its one extra report is `objectql.zod.ts#ObjectViewSchema::form`, a passthrough-object face — so the argument against it is that its single false report is the wrong KIND of finding, not that there would be many. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
❌ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. Which half objected:
📦 Bundle Size Report
Size Limits
|
Sequencing hazard with #8573 — whichever of us lands second reddens
|
| lands first | lands second | what breaks |
|---|---|---|
| this PR | #8573 | the two entries this PR adds now describe wideness that no longer exists |
| #8573 | this PR | this PR adds entries for wideness it can no longer measure |
Either order, the second PR's assertionDriftMatchesLedger reconciliation goes red. ⭐ The merge queue rebuilds on current main, so the second one ejects rather than lands — no broken main, but a wasted queue cycle and a confusing failure for whoever is on the other side of it.
⛔ I am not asking you to change anything now, and I am not claiming either PR is wrong. Both are correct in isolation. The point is only that the second one through needs its two rows dropped or adjusted, and neither author will otherwise expect a red there.
Measurement provenance, so you can judge rather than take my word: the reading that neither key has an entry on main is #8573's, from an ablation run on its own committed tree — removing its repair reddens 7 runtime pins and 5 compile-time ones while leaving zod-mirror-parity.test.ts green. That is the blindness this PR exists to fix, observed directly.
#8573 is a draft pending its contract review, so this PR is currently ahead. If you land first, ping me and I will carry the ledger adjustment in #8573 rather than making you wait.
Generated by Claude Code
Heads-up: #8573 merged first — this PR will red on rebuild, and here is exactly what to change
#8573 merged at 14:40Z, so the wideness it removes is gone from
⇒ Two things in this PR now describe a
⭐ Your operator repair is unaffected and still correct — this is only about the two rows it was measuring and the fixtures that pinned them. The blindness this PR fixes was real: #8573's own ablation showed
⛔ I have not touched this PR, its labels, or its branch, and ⛔ I am not claiming your card. Ping me if you would rather I carry anything. Generated by Claude Code |
PM diagnosis — CI red for a reason that is not this PR's, plus a merge conflictFailing check: Bundle Analysis, job 102031750627. 1. The Bundle Analysis failure is a stale base, not this diffThis PR is a types test-only change; it cannot move a bundle. Measured:
⇒ The check is being judged against the pre-ruling ceiling. Bringing 2.
|
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
Second sync during the same run: #8601, #8607 and #8608 landed while this branch was being re-verified. None of them touches `packages/types/src/__tests__/zod-mirror-parity.test.ts`, so this merge is clean; #8601 does move a DECLARATION the parity ledger reads (`packages/types/src/record-components.ts`), which is why the branch is re-measured on top of it rather than left behind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
✅ 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
|
PM: this PR is armed but blocked by a failure that is NOT its own — the fix is being ported
Why it is not this PR'sThis PR is ⭐ It is the un-split sibling of a flake already fixed today. ⇒ Same defect, same family, one file over. What I am doing about it⛔ Not re-running, and ⛔ not touching this PR's diff. A dev is dispatched to port #8533's wait-splitting to
If the tree fix lands and shard 3/4 is still red on this head, the failure becomes this PR's and gets root-caused rather than ported. Generated by Claude Code |
Fixes #8517
Notation. Generic type arguments are written with SQUARE brackets throughout —
Partial[Record[K, V]], not the real spelling — because GitHub deletes tag-shaped fragments on save, backticks and fences included. The card does the same, for the same reason.mainin — andmaintook both of this card's live instancesThis branch was 27 commits behind. The merge brought in PR #8573 (
d4733f27e, objectui#8516 + objectui#8556), which narrowed both mirrors this card had ledgered:3f775eeb8maintodaylayout.zod.ts#GridSchemacolumnsz.record(z.string(), z.number())— reported WIDERz.partialRecordover the six breakpoints — cleanreports.zod.ts#ReportComponentSchemaexportConfigsz.record(z.string(), ReportExportConfigSchema)— reported WIDERz.partialRecordoverReportExportFormat— cleanMeasured, not inferred. With the two ledger rows left standing,
tsc -p tsconfig.test.jsonreddens twice:With both rows removed it is EXIT=0 — which is also the statement that no new open-record instance exists anywhere on the merged tree.
So on its merged head this PR is:
safeParsegreen on{ xxl: 6 }and on{ xml: … }) are now false, and that reachability is pinned inverted by main's ownpackages/types/src/__tests__/mirror-partial-record-narrowing-8516.test.ts;⭐ The clause is not left without a job. objectui#8556 ruled the narrowing had to be pinned against "the file-level instrument ... rather than against an accept set alone", and this operator is what makes a re-widening of either mirror fail inside
zod-mirror-parity.test.ts. Before it, the parity file was blind to exactly that — which is the defect objectui#8517 was filed on.⛔ Open question, not decided here
The card's live sample count is now zero. Landing this buys a regression guard on a class that was real twice in one census; the cost is an operator clause the live ledger never exercises. Closing objectui#8517 as overtaken by objectui#8516 is a defensible alternative. I did not pick — the merge is resolved and the branch is green either way.
What was blind, and what fixes it
WiderThanDeclaredKeysdecided by one assignability test. That test cannot separateRecord[string, V]fromPartial[Record[UNION, V]]in either direction — the two are mutually assignable, because TypeScript grants an implicit index signature to mapped and anonymous object types. Both facts are pinned (assertionOpenRecordFitsThePartialRecord,assertionPartialRecordFitsTheOpenRecord), so nobody can "simplify" the clause back to an assignability test.The clause runs only on the branch the assignability test already called clean. That placement is the non-regression guarantee, and it is structural rather than asserted: a key reported before this change cannot stop being reported.
It reports only the unambiguous case:
z.record(z.string(), V)shape and nothing else; andUnconstrained.Two halves of the predicate were measured, not reasoned about
Each was added after the pin written for it failed. Neither was predicted. Both readings are at this card's base
3f775eeb8and are recorded in the file as historical..passthrough()produces, and what everyBaseSchemaheir is — also answerstruetostring extends keyof T;keyof z.input[typeof BaseSchema]is barestring. Requiring no literal members is what keeps the whole passthrough population out.assertionLooseObjectArmIsNotAnOpenRecordwas observed reddening under the naive clause.keyof numberresolves to that primitive's method names, so a barenumberarm read as a finite-keyed object and carried a permissivenumber | objectdeclaration into the report.assertionPermissiveDeclarationIsNotWidenedfailed on the first candidate clause;FiniteKeyedArmsnow excludes non-objects and arrays.The bound is pinned
The clause reads the slot's top-level union arms and no deeper — the same shallowness
Unconstraineddocuments, and for the same measured reason (a recursive predicate drove whole instantiations toanyand went silently green on the pairs with the most structure).assertionNestedOpenRecordIsBelowTheBoundwas observed reddening under a one-level-deeper clause.Figures corrected during the merge
Every count was re-derived from the tree, on the principle the file states about itself — nothing was carried across the conflict.
3f775eeb8where they stand: theGridSchema.columnsreading in the operator docblock, and the naive-clause blast radius ("3 registry pairs where this one moved 2").WiderArmClass's sub-class list no longer names OPEN-RECORD, because no docblock above names a row carrying it. Its absence is recorded inline so the next reader does not restore it.Evidence
Instrument is
type-checkand specifically its third program. Established before it was trusted, with a lit control:tsc --noEmit --listFiles(the emitting program) contains 0 hits forzod-mirror-parityand 1 forsrc/zod/layout.zod.ts. Ablating this change under vitest is green in every leg.All at head
98cd2272a, after the second merge ofmain(0544ae958):pnpm --filter @object-ui/types type-check— EXIT=0, all three programs.pnpm exec vitest run packages/types/— 149 files, 2849 tests, EXIT=0.pnpm exec eslint packages/types/src/__tests__/zod-mirror-parity.test.ts— EXIT=0 (plain form; the repo-wide union is left to CI and is not claimed here).check:control-bytesEXIT=0 (6801 tracked text files); targetedgrep -naPself-scan of the changed file — no match.check:published-tsconfig-excludeEXIT=0;check-changeset-presence.mjsandcheck-changeset-no-major.mjsboth EXIT=0.Ablations, re-run on the final head. Each mutation proved on disk by
git hash-objectbefore and after, each restore proved by the hash returning to theHEADblob andgit diff HEADbeing empty, each leg wrapped in anEXIT INT TERMtrap with absolute paths:tsc -p tsconfig.test.jsonassertionOpenRecordWideningIsReported(925),assertionOpenRecordClauseIsAdditive(1040)Type '"form.zod.ts#ButtonSchema"' is not assignable to type 'never'andType '"object"' is not assignable to type 'never'Scope
Changeset is empty-frontmatter: the only source file is under
src/__tests__/, whichpackages/types/tsconfig.jsonexcludes from the emitting build — proved above with--listFiles, not asserted.skip-changesetwas not used; it is a phantom label in this repo.Two merge commits, no rebase and no force-push, so every existing checkout of this branch stays valid.
Bundle Analysis. Red before the merge for a reason outside this diff: the head predated
fa9e76ccd("raise theframeworkper-chunk ceiling to 100,000 by maintainer ruling", #8550), which is now an ancestor (git merge-base --is-ancestorEXIT=0). No ceiling was touched andscripts/check-eager-closure-budget.mjsis unmodified. This is a types test-only change and moves no bytes.Generated by Claude Code