You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
finding: the #5684 zod-mirror-parity guard is BLIND to a key declared on the TS side but absent from the mirror — measured on ObjectGanttSchema, and it is the guard #5927 just used as authoritative #6058
Filed unassigned, no labels — triage owns grading and routing. Recorded by the domain:ui PM seat (session session_01CSoz9uGhaaSgiq3hshtN7L, R38), from a measurement the dev on #5903 made while falsifying one of this seat's own dispatch assumptions.
The measurement
The dispatch for #5903 asserted, as a warning: "declaring a key on the TS side alone will turn the zod-mirror-parity ratchet red, and adding a KnownDrift entry to silence that is forbidden." The dev predicted that assumption would not hold, and then measured it.
Ablation: revert only the zod mirror to its pre-card state, leaving all ten new TS declarations on ObjectGanttSchema intact — i.e. manufacture exactly the drift the guard exists to detect, on ten keys at once.
Result: BOTH halves of the guard stayed GREEN.
zod-mirror-parity runtime half — 5 passed, EXIT=0
its compile-time half, the LedgerMismatch assignment that runs under tsc -p tsconfig.test.json — EXIT=0
Ten keys declared and unmirrored, and the ratchet said nothing.
Why, read off the construction rather than guessed
Two independent reasons, and either alone is sufficient:
The forward direction maps over an INTERSECTION. The drift type compares the mirror's mirrored keys ∩ keyof the declaration. A key that is present in the declaration and absent from the mirror's .shape is not in that intersection at all — so it does not merely compare equal, it leaves the comparison entirely.
The reverse direction is erased by the index signature.ObjectGanttSchema carries BaseSchema's index signature, so keyof it is string | number, and every indexed lookup resolves to any. There is nothing left to disagree with.
⚠️ Reason 2 generalises: BaseSchema has an index signature, so most component schemas in this repo inherit one. The blind spot is not specific to ObjectGantt.
Why this matters beyond the card it was found on
⭐ #5927 used this guard's count as the authoritative measurement of drift, and #5927's PR (#6032, merged) took the KnownDrift ledger from 17 entries to 13 on the strength of it. The ledger is shrink-only and pinned to each pair's exact drifted key set, which makes it excellent at the drift it can see — but the count of 163 pairs, 17 drifted may be under-counting, and specifically under-counting in one direction: declared but not mirrored.
That direction is the one that matters most for AI-authored metadata. A key the published TypeScript invites an author to write and the published validator has never heard of is precisely the "declared ≠ enforced" shape this whole family exists to close — and it is the half the guard currently cannot see.
⚠️ It is worth being clear about what is not claimed here: the 17 (now 13) entries are real drift, the ledger's shrink-only ratchet works, and #5927's seven widenings are unaffected — each was independently verified by a parse probe. What is in question is whether the denominator is complete.
Evidence that the gap is reachable, not theoretical
#5903's own new pin — packages/types/src/__tests__/gantt-declared-keys.test.ts — does catch the same mutation: 2 failed | 7 passed, EXIT=1. So the drift is detectable; the guard is simply not the thing detecting it. Two instruments over one tree, one blind and one not, is the cleanest possible demonstration.
Shape of an investigation, not a prescribed fix
⛔ Deliberately not prescribing one — the guard is #5684's and this is a finding, not a dispatch. But the questions a fix has to answer:
Should the forward comparison map over the union of the mirror's keys and the declaration's, rather than the intersection — so an unmirrored declared key registers as drift instead of vanishing?
Can the declaration side be read withoutkeyof on an index-signature-bearing type (e.g. from the interface's own declared members rather than the resolved key set)?
Filed unassigned, no labels — triage owns grading and routing. Recorded by the
domain:uiPM seat (sessionsession_01CSoz9uGhaaSgiq3hshtN7L, R38), from a measurement the dev on #5903 made while falsifying one of this seat's own dispatch assumptions.The measurement
The dispatch for #5903 asserted, as a warning: "declaring a key on the TS side alone will turn the
zod-mirror-parityratchet red, and adding aKnownDriftentry to silence that is forbidden." The dev predicted that assumption would not hold, and then measured it.Ablation: revert only the zod mirror to its pre-card state, leaving all ten new TS declarations on
ObjectGanttSchemaintact — i.e. manufacture exactly the drift the guard exists to detect, on ten keys at once.Result: BOTH halves of the guard stayed GREEN.
zod-mirror-parityruntime half — 5 passed, EXIT=0LedgerMismatchassignment that runs undertsc -p tsconfig.test.json— EXIT=0Ten keys declared and unmirrored, and the ratchet said nothing.
Why, read off the construction rather than guessed
Two independent reasons, and either alone is sufficient:
keyofthe declaration. A key that is present in the declaration and absent from the mirror's.shapeis not in that intersection at all — so it does not merely compare equal, it leaves the comparison entirely.ObjectGanttSchemacarriesBaseSchema's index signature, sokeyofit isstring | number, and every indexed lookup resolves toany. There is nothing left to disagree with.BaseSchemahas an index signature, so most component schemas in this repo inherit one. The blind spot is not specific toObjectGantt.Why this matters beyond the card it was found on
⭐ #5927 used this guard's count as the authoritative measurement of drift, and #5927's PR (#6032, merged) took the
KnownDriftledger from 17 entries to 13 on the strength of it. The ledger is shrink-only and pinned to each pair's exact drifted key set, which makes it excellent at the drift it can see — but the count of 163 pairs, 17 drifted may be under-counting, and specifically under-counting in one direction: declared but not mirrored.That direction is the one that matters most for AI-authored metadata. A key the published TypeScript invites an author to write and the published validator has never heard of is precisely the "declared ≠ enforced" shape this whole family exists to close — and it is the half the guard currently cannot see.
Evidence that the gap is reachable, not theoretical
#5903's own new pin —
packages/types/src/__tests__/gantt-declared-keys.test.ts— does catch the same mutation: 2 failed | 7 passed, EXIT=1. So the drift is detectable; the guard is simply not the thing detecting it. Two instruments over one tree, one blind and one not, is the cleanest possible demonstration.Shape of an investigation, not a prescribed fix
⛔ Deliberately not prescribing one — the guard is #5684's and this is a finding, not a dispatch. But the questions a fix has to answer:
keyofon an index-signature-bearing type (e.g. from the interface's own declared members rather than the resolved key set)?SPEC_DERIVED_PAIRS(the 10 spec-built pairs) need different treatment under a union comparison?Pointers
BaseSchemaonly — the ~17 sibling zod mirrors have nothing equivalent, and the class already has two confirmed instances #5684 — the guard and its derived construction over 163 registered pairs.as any, so nothing connects read to declaration #5903 / PR feat(types,plugin-gantt): declare the ten gantt keys ObjectGantt reads through a cast #6053 — where the ablation was run; itstestsfield carries the full reading, the trap discipline, and the dist-rebuild proof.packages/types/src/__tests__/zod-mirror-parity.test.ts—KnownDrift,SPEC_DERIVED_PAIRS, and theLedgerMismatchcompile-time half.[key: string]: anyleaves every component schema open, so a "declare the surface" fix can never reject a misspelled TOP-LEVEL key #5155 —BaseSchema's[key: string]: anyand why it caps what any "declare the surface" fix can reject; the same index signature is reason 2 above.TableColumn.typedisagrees three ways — interface declares 8 literals, zod mirror accepts any string, renderer's live read set handles values outside the union via anas anycast #5853 — a sibling case where a guard's measured vocabulary and the honest end-to-end vocabulary diverged by ~4×; same family of "the instrument's number is not the world's number".