fix(types): declare WalkableDef.rest as nullable, matching what zod 4 mints (objectui#9491) - #9683
Conversation
… 4 mints (objectui#9491) `packages/types/src/zod/node-derivation.ts` declared `rest?: z.ZodType` — `z.ZodType | undefined` — while zod 4 spells "this tuple has no rest element" as an OWN `rest` key holding `null`. The declaration is what licensed objectui#9088: the `tuple` arm normalised the absent case to `undefined` because the type said that was the absent case, `unchanged` compares by `===`, and so every rest-less tuple was rebuilt. The declaration, not the behaviour. Nothing here changes what either walker does with the value, and no exported schema's accept set moves. One read needed adjusting, contrary to the card's expectation: the local `unchanged` helper in `zod/imported-defaults.ts` types its comparison pairs `z.ZodType | undefined`, and objectui#9088's repair hands it `def.rest` raw. Its parameter now admits `null`; the comparison is still `===`. New pin `walkable-def-null-mint-9491.test.ts` re-derives both halves against the installed zod — `rest` is minted `null`, and no other member the walkers read ever is — plus a compile-time pin so narrowing the member back, or widening a sibling to match it, fails to compile. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
|
changeset-claim-re-read
|
✅ 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
|
At-tier contract review record — PR #9683 / card objectui#9491 — PASSAdopted VERBATIM by the Tier, read from the reviewer's OWN transcript (⛔ not Isolation: the reviewer was given the card, existing rulings and the PR only — ⛔ never the dispatch order, ⛔ never this seat's conclusions, ⛔ never the implementer's report as an input. VERDICT: PASS Subject: How I measured. Nothing in What I measured
What I checked and what I found1. Does the diff do what the card asks — no less, no more? Yes. The card's candidate repair is 2. How big is the surface that actually moved? Internal. Measured three independent ways (rows 1–4): the interface is named in no barrel, is reachable from no 3. Wrong thing made right, or made permanent? Made right. 4. Comparison / refusal behaviour preserved? Yes. 5. Are the pins real? Yes, with a division of labour the file itself states. The compile-time pins are the pin on the repair: Legs A and B each fail 6. Changeset. Correct package, correct level ( Blocking itemsNone. Non-blocking observations (not gating)
⭐ — Record posted by PM Generated by Claude Code |
Fixes #9491
Declare
WalkableDef.restasz.ZodType | null. All readings below were taken on this branch at3713e80, off base72f55c9ec1.Angle-bracket-shaped tokens are spelled out in words in this body (AGENTS.md, "GitHub 会改写你写进 issue/PR 正文的字节"): the generic helper the new pin uses is written
AdmitsNull of WalkableDef's rest, never in the bracket spelling, because the sanitizer deletes those even inside backticks.What this is
packages/types/src/zod/node-derivation.tsdeclaresWalkableDef, the def member set both zod walkers in this package read. It declaredrest?: z.ZodType— i.e.z.ZodType | undefined— while zod 4 spells "this tuple has no rest element" as an OWNrestkey holdingnull.Scope is the declaration, not behaviour. objectui#9088 already repaired the one arm the wrong type misled, by copying
def.restinstead of normalising it. Nothing here re-touches that arm's logic, and no walker's output changes. This corrects the type that licensed the defect, so the next arm written against it is told the truth.The four things the dispatch asked me to measure
1. Premise, re-verified on my own base
origin/main72f55c9ec1— the same head the claim recorded.WalkableDefstill declaredrest?: z.ZodType. Premise live.2. The card's "declaration-only" expectation — FALSIFIED, and this is the one substantive finding
The card says both readers guard with a truthiness test, so the widening should be declaration-only, and calls that "a claim to measure, not to assert". Measured: it is not declaration-only.
The truthiness guards are indeed fine — neither
def.rest ? walk(def.rest) : def.restnorstrict-authoring-face.ts'sdef.rest ? ... : {}moves. What the card did not account for is that objectui#9088's own repair introduced a raw, unguarded flow ofdef.restinto a typed helper: the localunchangedinzod/imported-defaults.tsdeclares its comparison pairsz.ZodType | undefined, and comparing like with like is the whole of that repair. Widening the member alone gives:⇒ one read adjusted:
unchanged's parameter now admitsnullon both sides. The comparison is still===—nullmatches onlynull,undefinedonlyundefined. ⛔ Deliberately NOT a cast at the call site and ⛔ NOT a relaxation to double-equals: either would hide the same inaccuracy this card exists to remove, and the arm would be back to normalising a value to satisfy a signature.3.
def.out, re-measured rather than relayedThe card reports it clean from objectui#9088 and the dispatch told me to treat that as unverified. Re-derived against the installed zod, both directions:
outcarries an own key on all three pipe spellings and holds a schema node in each, nevernull. Clean — confirmed, not relayed. Also re-ran the twelve-member sweep triage asked for: across a matrix of every node kind the walkers meet,restis the only walkable member ever holdingnull, and the only nodes that mint it are the rest-less and empty tuples. The source half agrees: over zod 4.4.3's shipped v4 tree, the onlynullbound to a walkable member name isconst rest = hasRest ? _paramsOrRest : null, at three logical sites doubled across module formats. Both halves are now in the pin file rather than in prose, so a zod bump re-derives them.4. Is
WalkableDefpublished? — shipped, but reachable by no consumerLoad-bearing for the contract review, so both legs are recorded:
@object-ui/types(control)@object-ui/types/zod(control)@object-ui/types/zod/node-derivation.jsERR_PACKAGE_PATH_NOT_EXPORTED@object-ui/types/dist/zod/node-derivation.jsERR_PACKAGE_PATH_NOT_EXPORTEDThe file IS emitted into the published tarball (
dist/zod/node-derivation.d.ts, andfilesshipsdist), and the widened member is visible in those bytes. But theexportsmap has no subpath for it and no wildcard, andWalkableDefappears in no entry point's emitted declarations — the only dist declaration file naming it is its own. ⇒ the widening is internal: its only consumers are the two walkers in this package. DeclaredClause-②: yesstands as ruled; this is the fact a reviewer needs to size it.The new pin, and why it is shaped this way
packages/types/src/__tests__/walkable-def-null-mint-9491.test.ts. Per the dispatch's suggestion, it probes the installed zod rather than asserting a TS type in isolation, because what has to stay true is an agreement between a declaration in this repo and a value minted inside a dependency — and the two move independently. It holds three things:restis mintednull(with the rest-bearing control firing the other way); no other member the walkers read ever is; and the declaration agrees with both, pinned throughtscbecause this package'stype-checkcompiles its tests viatsconfig.test.json.The uniqueness half is not decoration. Without it, the honest repair for
restreads as a licence to spell the same union on any member that looks similar — which would declare an absent case zod never produces, and every read guarding against it would be dead code no test could reach.Reverse verification — four legs, each restored and proven restored
Run from the committed tree at
3713e80. Every leg mutates, proves the mutation reached disk by counting the injected and the removed text, runs, restores withgit checkout HEAD -- path, and proves the restore by comparinggit hash-objectagainst the HEAD blob (an empty hash read as failure, never as "nothing to compare"). Final state:git diff HEADempty.z.ZodTypetype-checkexit 2 —walkable-def-null-mint-9491.test.ts(175,11): error TS2322: Type 'true' is not assignable to type 'false'outto match ittype-checkexit 2 —error TS2322: Type 'false' is not assignable to type 'true'elementin the null-minted setvitestexit 1 —AssertionError: expected [ 'rest' ] to deeply equal [ 'element', 'rest' ], 1 failed / 5 passedunchangedwideningunchangedcallLeg D is the measurement behind finding 2, taken from the committed tree rather than from the working state it was first noticed in. Leg A reddens the pin and not
imported-defaults.ts— the narrower member is still assignable to the widened helper parameter, which is what says the helper widening is a genuine second edit and not a restatement of the first.No permanent mutation test was left behind; the subject resolves through relative source imports, so no
diststaleness can make any of these legs read green for the wrong reason.Gates
Exit codes captured to disk before any pipe, never read through a pipeline.
pnpm --filter @object-ui/types type-checkpnpm --filter @object-ui/types lintpnpm --filter @object-ui/types build(chainscheck-dist-completeness)pnpm exec vitest runover the new pin + the 9088 / 9102 / 8317 neighboursnode scripts/check-changeset-presence.mjspnpm changeset:checkpnpm check:changeset-claimspnpm check:pending-changeset-literalspnpm check:control-bytespnpm check:new-line-citationspnpm check:spec-symbolspnpm check:component-surface-paritynode scripts/check-governed-queue-guard.mjs --test(4 changed paths)Declared narrowing.
pnpm lintisturbo run lint, i.e. every package's own eslint run; I ran@object-ui/types's only. Population and count read from eslint itself (--format json): 270 files in this package, 0 errors. Invariance for the untouched packages: this repo's eslint is not type-aware —eslint.config.jsdeclares noprojectService, noparserOptions.projectand noprojectkey — so no rule anywhere reads the declaration I moved, and a type edit cannot move a verdict on a file it did not touch. Independently, finding 4 shows no other package can even nameWalkableDef. The repo-wide farm is CI's run.⛔ Not run, deliberately: no
pnpm dedupe(check-lockfile-dedupeis non-deterministic here, objectui#9562); the lockfile is untouched. Vitest was never invoked with the name-filter flag — this repo's describe names end in a parenthesised card reference, which that flag reads as a regex and silently matches nothing while exiting 0 (objectui#9660). Every run above names files by path and reports a non-zero pass count.Acceptance notes
Things tripped over, none of them filable under the three classes, none acted on:
packages/types/tsconfig.jsonexcludes tests from the build whiletsconfig.test.jsoncompiles them; the new pin depends on that second project existing to be a gate at all. Noted because a future consolidation of the two would quietly demote a compile-time pin into a comment. Not a defect today — the arrangement is deliberate and documented intsconfig.test.json's own header. Successor: whoever next touches that pair.imported-defaults-rest-less-tuple-9088.test.tsdeclares a LOCALZodDefmirror with the honestrestunion plus anoutunion this branch measured unnecessary, and its comment explains the mirror exists because the shared type was outside that card's file surface. The shared type is now honest, so the mirror could be retired in favour of importingWalkableDef. ⛔ Not done here: it is a refactor of another card's pin file with no defect behind it. Successor: whoever next edits that pin.patch. The published bytes that move are declaration bytes in a file noexportssubpath reaches, so nothing a consumer can import changes shape.维护者速读(草稿)
改了什么。 一个类型声明说谎,现在说实话。
@object-ui/types里两个 zod 遍历器共用一张"我读哪些字段"的类型表,其中rest一项声明为"要么是个 schema,要么没有";而 zod 4 实际填进去的是null。这次把声明改成允许null,并顺带修正了一处因此被卡住的内部比较函数的参数类型。运行时行为一处未动,任何已发布 schema 的接受范围也一处未动。为什么改。 因为这张错声明已经"发过一次许可证":objectui#9088 那个缺陷不是作者写错了,是作者照着类型写、类型是错的、编译器还替他背书 —— 结果每一个无 rest 的 tuple 都被无谓重建。一个把错误合法化的类型比一个错误的实现贵,因为它让下一个人每一步都做对而仍然出错。这次是把发证的那一行收回来。
风险与代价(含回滚)。 风险很低:这个类型没有任何一条
exports通路能被包外引用到(本 PR 用"控制组 + 实验组"两条腿实测过),所以它的消费者只有本包内的两个遍历器,两个都已编译通过。代价是多了一个 pin 测试文件,它会在 zod 升级改变这个行为时主动变红 —— 这正是它存在的意义。回滚就是 revert 本 PR 的单个 commit,不留残留(反向验证用的临时改动已逐字节还原,校验过哈希)。发布影响:一个 patch changeset,跟着固定版本组走。席位意见。
你要做的。 如果同意"声明应当如实反映依赖实际铸出的值"这个方向,这张卡就没有需要你裁的分歧点 —— 唯一值得你看一眼的是本 PR 第 2 条发现:卡面预期"纯声明改动"被实测证伪,多改了一处内部比较函数的参数类型,理由与取舍写在上面。
Generated by Claude Code