…ed reader
`renderers/form/input.tsx` reads
`cn("grid w-full items-center gap-1.5", schema.wrapperClass)`. The TypeScript
face declared the key all along; the zod mirror never did, so the value rode
through `.passthrough()` and `{ type: 'input', wrapperClass: 42 }` validated
GREEN — while the identical document on any of the other eight
`schema.wrapperClass` readers was refused at the key.
Measured BY BLOCK, not by keyword: a bare grep for `wrapperClass` over
`form.zod.ts` hits three neighbouring declarations (`TextareaSchema`,
`SelectSchema`, `CheckboxSchema`) and reads like the key is already there.
Parsing the file into its `export const` blocks and asking per block finds
`InputSchema` declaring no such member.
- `zod/form.zod.ts` — declare it in the form `CheckboxSchema` uses, in the
position the TS face puts it (after `onChange`), citing the read by its text
rather than by a line address.
- `zod/form.zod.ts` — `InputShorthandSchema` stops RE-declaring the key. It
existed only because `InputSchema` lacked it; the arm inherits it through the
`.omit()` now, like every other key.
- `__tests__/zod-mirror-parity.test.ts` — the ledger row was that entry's only
key, so the entry emptied: 14 / 86 to 13 / 85, LOCAL 11 / 69 to 10 / 68,
SPEC-DERIVED unmoved. Every figure re-derived from the file's own AST.
- `__tests__/wrapper-class-declared-7722.test.ts` — the self-expiring exemption
expired and is DELETED, not weakened.
- `__tests__/input-wrapper-class-mirrored-8072.test.ts` — the accept-to-refuse
pin, with a string control, a sibling control and an undeclared-key control.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EpHzwH4wTy5sd7ibkD2yq
Fixes #8072
renderers/form/input.tsxreadscn("grid w-full items-center gap-1.5", schema.wrapperClass). The TypeScript face has declared the key all along (form.ts, docblock "Input wrapper CSS class"); the zod mirror never did, so the value rode through.passthrough()and{ type: 'input', wrapperClass: 42 }validated green — while the identical document on any of the other eightschema.wrapperClassreaders was refused at the key.The premise, re-derived BY BLOCK rather than by keyword
A bare
grep wrapperClassoverform.zod.tshits three sites that read like "it is already there". They belong to the neighbours. Parsing the file into itsexport constblocks and asking per block:wrapperClassdeclaredInputSchemaTextareaSchemaSelectSchemaCheckboxSchemaThe renderer read is live and unchanged;
input.tsxand the TS face are untouched by this branch.The four census figures, derived off the file and shown
The card's step 2 quoted
14 entries / 87 keys — 2 / 3 spec-derived, 12 / 84 local. All four were stale — objectui#7762 had moved every one of them, and the file's own sentences say so. Re-derived with the same instrument the census uses (ledgerEntryMembers, the file's TypeScript AST, union arms walked) plusSPEC_DERIVED_PAIRSmembership, before and after:UnmirroredDeclaredtotalThe working, in one line each:
UnmirroredDeclared['form.zod.ts#InputSchema']carried exactly one arm,wrapperClass. Removing the key therefore emptied the entry, so entry count and key total move together — unlike objectui#7762, which took one key off a fifteen-key entry and moved only the key total. That is the trap in the obvious arithmetic, and it lands on the safe side here.KnownDrift, not this ledger:'form.zod.ts#InputSchema': 'onChange', a runtime slot the mirror refuses by name.KnownDriftis untouched, and its own figures did not move.form.zod.ts#InputSchemais not inSPEC_DERIVED_PAIRS, so the whole move lands in the LOCAL half and the SPEC-DERIVED figures stay put.RuntimeOnlyDeclaredholds three entries, none of them this pair, and it is a subset ofUnmirroredDeclared— so the union shrinks with the ledger and the subset leg stays true.Seven prose sites move, not four. The card and the dispatch between them name the top-of-file bullet, the two split bullets and the totals line; the census also reads the cross-ledger sentence (two figures) and both ledger docstrings' key totals, which live in different paragraphs. All seven are corrected, each with the cumulative history sentence its section keeps. Every one is pinned: the four census blocks are green.
The pin: accepted to refused, with three controls
New file
packages/types/src/__tests__/input-wrapper-class-mirrored-8072.test.ts. Run against this branch's base before the mirror moved, it failed on exactly the legs that should fail and passed the controls — 11 passed, 4 failed:{ type: 'input', wrapperClass: 42 }accepted on the mirror and throughsafeValidateSchema;wrapperClassabsent from a 33-key shape.wrapperClassoninput.checkboxsibling was already refused at the key — this branch touches no mirror butInputSchema, so that leg reads the same on either side of it.wrapperClassoninputparses green on both faces and the value survives the parse.After the change all legs pass.
One direction the dispatch did not name: the derived STRICT face moves the OPPOSITE way
StrictAnyComponentSchema(objectui#8345) is derived from these mirrors and closes unknown keys, so before this change it refused an authored stringwrapperClassoninput— the exact spelling the published TypeScript declaration invites an author to write. It parses now. Measured, not reasoned: that leg failed on the base tree and passes here. The two faces move apart — the tolerant one narrows, the strict one stops contradicting the TS declaration — and both are pinned.This does not move clause 2: the key is already on the published TypeScript face, so the strict face is being pulled back to a declaration that already shipped, and the published TS surface does not move by one byte.
Reverse verification
Committed first, then the declaration was deleted from
form.zod.tsand the pins re-run. The mutation was proved on disk before the run — anchor text 1 occurrence to 0, blob hash2a664196bto022429bdf— and the restore proved by hash equality plus an emptygit diff HEAD, not by an exit code. The test reaches the mirror through a relative source import, so no build sits in the path.Six legs turned red, and which six is the useful part:
declares wrapperClass on the zod face ... unless ledgered— proof the exemption was really deleted rather than left covering the case;InputShorthandSchema declares it and refuses the wrong-typed value— proof the removed local re-declaration is genuinely supplied byInputSchemathrough the.omit();The three controls stayed green under the mutant. 77 passed alongside the 6 failures; restored, all green again.
objectui#8072plus a closing paren) also matched a second, untouched comment, so the post-mutation count read 1 instead of 0 and the script refused the reading and restored. The anchor was re-pointed at text unique to the removed declaration. The mutation had in fact landed; the guard was measuring the wrong thing, which is the failure it exists to catch.Step 3 was deleted, not weakened
LEDGERED_UNMIRROREDinwrapper-class-declared-7722.test.tsis empty now, and its docblock records that the exemption expired as designed. The sweep judges all nine readers alike. The exemption's two conditions — mirror still lacks the key, ledger row still on disk — are both false, which is what turned it red the moment step 1 landed.Also in this branch
InputShorthandSchemastops re-declaringwrapperClasslocally. That declaration existed only becauseInputSchemalacked it, and its comment said so in as many words; with the row shrunk, the arm inherits the key through the.omit()like every other key. Its membership and refusal are pinned, and the ablation above shows the pin can fail. The comment it replaces also carried a cross-file line address, which the house rule bans..changeset/7722-wrapper-class-five-more.md— prose only, frontmatter untouched. Its closing sentence said this row "stays there" and the exemption is carried; both go false at release time with this change pending beside it.check:changeset-claimsreports that read, names correcting the body as the remedy, andcheck:changeset-overwriterecords the write as its own case 2.Verification
Commit
67ac055d6.pnpm --filter @object-ui/types type-check— green (all three tsconfigs:--noEmit, examples, test).pnpm exec vitest run packages/types/— 205 files, 4786 tests, all passing. Run through the shared serialisation lock;VERDICT command-exit 0.pnpm --filter @object-ui/types lint— 0 errors (290 pre-existingno-explicit-anywarnings, none in a file this branch touches). 276 files linted, read from the--format jsonoutput; all four touched files score 0 errors and 0 warnings. The narrowing to one package is sound because this repo's ESLint is not type-aware —tseslint.configs.recommended, noparserOptions.projectand noprojectService, zero type-checked rules — so no file this branch does not touch can change verdict.check:control-bytes,check:new-line-citations(0 new, and one existing cross-file address removed),check:test-path-roots,check:pending-changeset-literals,check:handler-key-reads,check-changeset-presence.Acceptance notes
Noted, not filed:
.changeset/pin-ledger-docstring-key-totals.mdstates in the present tense that two instruments "measure 87". That figure was already stale on this branch's base — objectui#7762 took the ledger to 86 — so it is not falsified by this change and is left alone. Whoever next touches that changeset is the carrier.wrapperClassgrows from five to six now thatinputmirrors it. Not touched here, as the triage note asked.LEDGERED_UNMIRROREDis now an empty map, so the exemption test iterates nothing and passes vacuously. It is a ratchet sitting at zero rather than a defect; the docblock says so and says a new row expires the same way.UnmirroredDeclaredare that ledger's worklist and were not swept.Generated by Claude Code