Uh oh!
There was an error while loading. Please reload this page.
test(types): ledger the declared-but-unmirrored half of the zod mirror comparison - #6149
Merged
Merged
Conversation
…rrored keys The forward comparison mapped over the INTERSECTION of the mirror's shape keys and `keyof` the declaration, so a key declared on the TS side and absent from the mirror did not compare unequal — it left the comparison entirely. The reverse direction was separately erased by `BaseSchema`'s `[key: string]: any`, which collapses `keyof` to bare `string`. `WithoutIndexSignature` recovers a declaration's own literal members through a homomorphic mapped type, and `ForwardDrift` unions the narrowness check with the declared-but-unmirrored set. Part of #6058 — instrument only; the measurement it produces is the card's deliverable and the ledger question it raises is a ruling, not an edit.
…r comparison Round 2 of objectui#6058, implementing the PM's Q1 option A ruling. The union comparison landed on the branch at cef2559 as one type; the ruling splits where it is RECORDED, not what it compares. `DriftOf` goes back to `NarrowerThanDeclared` alone, so `KnownDrift` keeps its meaning ("the mirror declares the key and REFUSES its declared type"), its 12 entries and its citable 17 -> 13 history from objectui#5927 / PR objectui#6032, untouched. The half that was invisible until this card — a declared key the mirror has never heard of, which did not compare unequal but LEFT the comparison — lands as `UnmirroredOf` against its own shrink-only ledger, `UnmirroredDeclared`, seeded at the measured 16 entries / 121 keys. A seed of 121 is a FLOOR, not a waiver: the guard saw ZERO of these before, so nothing previously caught is let through, and visible-and-ratcheted facts go from 0 to 121. Same shape as objectui#6133's KNOWN_HAND_TYPED_GUARDS. Both reconciliations now route through one exported `ReconcileAgainstLedger` — semantics-preserving for the existing assertion — so the recognition pins under it cover both. Those pins drive it with synthetic pairs in every direction a ledger can be wrong (growth on a clean pair, growth on a ledgered pair, a stale key, a stale entry), because a run over today's tree can only ever show today's tree is green. Q4's routing is recorded rather than exempted: the 3 spec-derived entries (14 keys) are marked for objectui#2231; the other 13 (107 keys) are local mirror omissions. Fixes objectui#6141 in passing: the header's "163 pairs" (twice) and "13 entries" were stale, and they are the numbers other cards quote as the size of the drift problem. Corrected to the measured 158 / 12, with each number's provenance stated. Part of #6058
Both measured on this branch rather than assumed, because the difference is what cost objectui#6058 a pass. Through a NAMED alias TS prints the alias and elaborates with exactly ONE member and no ellipsis, so 23 offending pairs read as one; written INLINE it resolves the union and prints a real `... N more ...` marker. That is why the new assertion is spelled inline, and it is worth writing down so the next reader does not re-derive a wrong count from a truthful-looking error. Also corrects the exit-code framing on `assertionUnmirroredMatchesLedger`. The round-1 reading — exit 2 before AND after the ablation — was a property of the UN-SEEDED tree, where the comparison was already red on 23 pairs. Seeding restored the exit code as a usable signal; the note now says which state it describes and that the per-pair set is the reading either way. The runtime half's `Tests 5 passed (5)` does not move in either state, which is the half a reviewer is most likely to misread. Part of #6058
Contributor
✅ 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
|
This was referenced Aug 24, 2026
yinlianghui-tw
marked this pull request as ready for review
August 24, 2026 21:34
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#6141
Part of #6058
Round 2 of #6058, implementing the PM's ruling on all three open questions. Round 1
stopped rather than grow a ledger or soften a comparison; this round lands the shape the
ruling chose. Union gate run at
ccde25fab.1. The runtime half does NOT move, and that is correct.
zod-mirror-parity'sdescribeblock readsTests 5 passed (5)before the fix, after the fix, and under theablation. It is a population census — the registry is closed,
SPEC_DERIVED_PAIRSre-derives, every exclusion carries a reason — and it never compared keys at all. The
whole comparison this PR changes is compile-time. A reviewer who reads "the guard now
reddens" as a change in that 5-passed line will conclude the fix does nothing.
2. The file's exit code was not the discriminating signal on the un-seeded tree.
Round 1 measured
PARITY_COMPILE_EXIT=2both before and after the ablation, becausethe union comparison was already red on the 23 pre-existing pairs. What changed was the
per-pair drift set. Seeding the ledger is what restores the exit code as a usable
signal — and only while the tree is green at rest. Both readings are now written into the
file so the next person does not re-derive the wrong one.
What lands
Q1 was ruled option A: a second, separately ledgered class.
DriftOfgoes back toNarrowerThanDeclaredalone, soKnownDriftandNarrowerThanDeclaredare untouched — 12 entries, 17 keys, shrink-only, and #5927 /#6032's citable 17 → 13 shrink stays comparable with everything after it. The half that
was invisible until this card lands as
UnmirroredOfagainst its own shrink-only ledger,UnmirroredDeclared, seeded at the measured 16 entries / 121 keys.saw zero of them:
NarrowerThanDeclaredmaps over the INTERSECTION of the mirror'skey set and the declaration's —
MirroredKeys of M, intersected withkeyof D— so adeclared key the mirror never mentions did not compare unequal, it left the comparison.
Nothing previously caught is being let through, because there was no such thing.
Visible-and-ratcheted facts go from 0 to 121, and any new declared-but-unmirrored key
on any of the 158 pairs reddens immediately. Same shape and same discipline as #6133's
KNOWN_HAND_TYPED_GUARDS.Q4 was ruled option A — route, do not exempt. The 3 spec-derived entries (14 keys:
DashboardComponentSchema+title,DashboardWidgetSchema+pagination/searchable,ObjectViewSchemaat 11) are marked in the ledger for #2231; the other 13 entries(107 keys) are local mirror omissions. The instrument itself is unchanged for them —
exempting spec-derived pairs would re-blind exactly the pairs #5927 leaned on hardest.
Q2 was ruled option A — the reading-versus-rejecting distinction is documented inline
on
WithoutIndexSignature. #5155's ceiling on what a mirror can REJECT stands untouched;what this lifts is what the GUARD can READ.
⛔ None of the 121 keys are mirrored here, per the order. They are handed back for a
follow-up lane, already sorted.
Fixes#6141 — the stale prose, in the same file and the same verification surface
The header said "163 pairs" twice and "13 entries" once. Measured: 158 pairs
(
Object.keys(MIRRORS).length, whichassertionRegistryHalvesAgreealready pins equal tokeyof Declared) and 12 entries. The file's own "146 pairs with no entry" was thenumber that was right — 158 − 12 = 146. Each count now states where it came from, because
these are the numbers other cards quote as the size of the drift problem: #5927's framing
and #6058's own dispatch both inherited "163".
Verification
Every exit code captured before any pipe; each gate quotes its own verdict line.
vitest from the repo root, never package-scoped (#3378).
The two-instruments ablation, carried forward with the seeded ledger
Same mutation as the card: revert only the zod mirror to its pre-#5903 state, leaving
the ten TS declarations on
ObjectGanttSchemaintact. Applied by an anchored script thatexits non-zero on an anchor miss, under
trap … EXIT INT TERMrestoring withgit checkout HEAD --(never the bare form, which reads the mutation back and exits 0).Mutation confirmed on disk, not by the editor's exit code — each of the ten keys
counted 1 → 0 in the mirror, the
objectui#5903banner 0, and the deliberately-untouchedcontrol on the other side still present (
skipWeekendsinobjectql.ts: 2,mobileReadOnly: 1).[]tsc -p tsconfig.test.jsonEXIT=0, 0 linesEXIT=2, names the pairTests 5 passed (5),EXIT=0Tests 5 passed (5),EXIT=0gantt-declared-keys.test.ts9 passed,EXIT=02 failed | 7 passed,EXIT=1The after-set is
criticalPath, holidays, markers, mobileReadOnly, navigation, persistLayout, readOnly, showBaselines, skipWeekends, viewName— exactly the ten #5903declared, no more and no less. Restore leg verified:
git status --shortempty.The new ratchet is SHOWN to fail, in both directions
⭐ A baseline that has never been shown to fail is indistinguishable from no baseline, and
a run over today's tree can only ever show that today's tree is green. Recognition is
pinned two ways.
Durably, in the file — both reconciliations route through one exported
ReconcileAgainstLedger, driven by synthetic pairs: agreement → silent; a clean pair →silent; growth on a clean pair → red; growth on a ledgered pair (which is also a
fact deleted from the seed) → red; a stale key → red; a stale whole entry → red.
They are mutually non-vacuous: a degenerate
neverimplementation fails the pins thatexpect the pair key, and a degenerate pass-through fails the pins that expect
never.End-to-end, on the real tree — three further ablations, each anchor-checked and
trap-restored:
wrapperClassinto theInputSchemamirror. Measured set goes['wrapperClass']→[];EXIT=2namingform.zod.ts#InputSchema.InputSchemaentry and one key (viewTabBar) fromObjectViewSchema's 11, mirrors untouched.EXIT=2naming bothform.zod.ts#InputSchemaandobjectql.zod.ts#ObjectViewSchema— a fact removed fromthe seed is not silently re-acceptable, whether it is a whole entry or one key inside
one.
... 4 more ..., and the last.The recorded trap, sharpened
The round-1 note said the assertion "names only its FIRST offending member". Measuring it
properly gives a more useful rule: the two spellings print differently. Through a
named alias (
assertionDriftMatchesLedgerviaLedgerMismatch) TS prints the aliasname and elaborates with exactly one member and no ellipsis, so 23 offenders read as
one, and
--noErrorTruncationdoes not expand it. Written inline (as the newassertion is) TS resolves the union and prints a real
... N more ...marker. That is whythe new assertion is spelled inline, and it is written into the file.
The authoritative read is still the compiler API — a Program over
tsconfig.test.json,one binding per pair, walking each type's union members via
isStringLiteral().value,bypassing type printing entirely. Non-vacuity control: it read all 158 pairs and
returned an EMPTY set for 142, and its output matched the seeded ledger exactly.
Gates at
ccde25fabtsc -p packages/types/tsconfig.test.json0pnpm --filter @object-ui/types type-check0tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json(script name confirmed, not a zero-match)packages/types0Test Files 58 passed (58)/Tests 632 passed (632)check:changeset-presence0check:changeset-fixed0check:changeset-no-major0majorbump."check:control-bytes0check:lint-coverage0check:type-check-coverage0Control-byte scan beyond the gate on the changed files:
grep -naPexit 1, no match.eslint narrowing, declared with all three pieces of evidence — targeted run on the one
changed file,
EXIT=0, 0 errors / 0 warnings. (1) The population is read from eslint's ownconfig resolution, not from a guess about which files count; (2) the file count
1is readfrom the
--format jsonarray length; (3) invariance for untouched files —eslint.config.jsdeclares no
projectServiceand noparserOptions.project(grep count 0), so type-awarelinting is off and nothing in this diff can move the verdict on a file it did not touch.
Three present, so this is a measurement. The repo-wide
pnpm lintrun remains CI's.The changeset has empty frontmatter: nothing outside
__tests__/is touched, so thisreleases nothing.
Generated by Claude Code