Repository navigation
fix(pm): let check-widening-tells' T1 express that the REMOVED value accepted everything - #18626
Merged
Merged
Conversation
`check-widening-tells`'s T1 says "the accept set gains a spelling an author may now write", and it fired on a diff that takes a spelling away: a key narrowed out of `z.unknown()` — zod's universal acceptor — into `z.union([…])`. Every value the union admits was already admitted, and `'stage=won'` and `42` are refused now, so the accept set SHRANK. The finding is the control set, not the reasoning: the SAME removal declined when the replacement was spelled `z.array(z.any())` and fired when the identical narrowing was spelled `z.union([`. One semantic change, two verdicts, decided by nothing but the added value's first line. #16943's replacement budget was EARNED and then refused at the SPEND, because #17618 lets a closed-set-valued key spend only on three facts, the second of which — both member lists readable — a universal acceptor can never carry: it has no member list, which is exactly the case where no comparison is needed, since the removed set is the universe and every replacement is a subset of it by construction. So a second way to supply #17618's own requirement is added — positive, block-local evidence that the set gained nothing, carried by the REMOVED value's semantics instead of by a list comparison. Nothing about the added value's spelling is read. The class is measured, not assumed: on zod 4.4.3 `z.unknown()`, `z.any()` and both with `.optional()` accept all eight probe values, while `z.unknown().refine(…)` refuses seven — so a narrowing chain step disqualifies the line, and `z.custom()` (0 code occurrences here) is not declared. Sensitivity is unchanged and bracketed by controls that still fire: a removed acceptor on a different key, a removed value that was never universal, a `.refine()`d one, the dark control with nothing removed, an inline enum widened in place, and a genuine new key riding along on an exhausted budget. Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
…-universal-acceptor
Collaborator
Author
席内复核(派发席
|
| case | origin/main 的门禁 |
本 PR 的门禁 |
|---|---|---|
live(z.unknown() → z.union([) |
FIRES T1@1809 | declines |
control A(同一次删除 → z.array(z.any())) |
declines | declines |
| control B(全新键,什么都没删) | FIRES T1@1810 | FIRES T1@1810 |
control C(z.enum 加成员) |
FIRES T1@1809 | FIRES T1@1809 |
control D(z.enum 减成员) |
declines | declines |
⭐ 夹具有效性对照(卡面自己披露过一次因漏 { repo } 而作废的探针,所以这一步不能省):control B 在两份门禁下都响 ⇒ 上面那个 declines 是读数,不是「我把作用域调错了所以什么都不响」。
⇒ 误报消失,该响的仍然响。落在「不再误报」那一侧,⛔ 没有滑向「少报」。
其余机械读数(2026-09-17T09:54Z)
--self-test:origin/main309 cases,PR 头 342 cases,两边 exit 0(各自树内真跑)。- 改动 1 个路径
scripts/pm/check-widening-tells.mjs(+338/−5),⛔ 未触packages/spec/src/**。 - PR 正文行首
Clause-②: no与Fixes #18234;--pair 18626退出 0(「both carriers agree … no widening tell」)。 - 已发布面:
scripts/pm/**不被任何包的files[]发运 ⇒skip-changeset成立。
我认可施工席的两个判断,并记下依据
- 识别键落在「被移除的值」上,⛔ 不在新值的拼法上 —— 这正是缺陷的自变量。
z.any()是否算全接受子,施工席用 zod 4.4.3 的safeParse对 8 个值实测(全收),⛔ 不是推断;而z.unknown().refine(...)8 收 1 ⇒ 链上有收窄步就不算。 - 新逻辑只读在既有的 spend 析取里 ⇒ 它只能让一个已经挣到的预算单位被花掉,⛔ 永远不能压掉一条没付过账的行。
放行路径
绿判据 2026-09-17T09:54Z 读为 NOT GREEN(6 条 in_progress)⇒ 等;绿后翻 ready → 重读到 total_count 真的移动 → 武装。
Generated by Claude Code
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 free
to 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 #18234
Clause-②: no
scripts/pm/**is shipped by no package (measured below), and a precision fix to a gatemoves no published contract face. Re-judged from the actual diff: one file,
scripts/pm/check-widening-tells.mjs.What moved
T1 says of itself "the accept set gains a spelling an author may now write", and it fired on
a diff that takes a spelling away: a key narrowed OUT of
z.unknown()— zod's universalacceptor — into
z.union([...]).'stage=won'and42were admitted before and are refusednow, so the accept set SHRANK.
The finding is the control set, not that reasoning: the SAME removal DECLINED when the
replacement was spelled
z.array(z.any())and FIRED when the identical narrowing was spelledz.union([. One semantic change, two opposite verdicts, decided by nothing but whether theadded value's first line opens a closed-set constructor.
#16943's replacement budget was EARNED (the removed
filter:line is T1-shaped and buys oneT1 unit) and then refused at the SPEND, because #17618 lets a closed-set-valued key spend only
on three facts, the second of which — both member lists readable on their own line — a
universal acceptor can never carry. It has no member list, and that is precisely the case
where no comparison is needed: the removed set is the UNIVERSE, so every replacement is a
subset of it by construction.
So this adds a SECOND way to supply #17618's own requirement — positive, block-local evidence
that the set gained nothing, carried by the REMOVED value's semantics instead of by a list
comparison. Nothing about the added value's spelling is read; that variable is the defect.
This is the "no longer false-positives" side, proven, not the "reports less" side
The triage ruling draws the line: "stop it false-positiving" and "make it report less" may
differ by one condition in code and sit on opposite sides of the charter. Everything that
should still fire, still fires:
- filter: z.unknown()...then+ filter: z.union([(members on later lines)+ filter: z.array(z.any()).optional()...z.enum(['a','b'])thenz.enum(['a','b','c'])z.enum(['a','b','c'])thenz.enum(['a','b'])- legacyFilter: z.unknown()then+ filter: z.union([- filter: z.string()then+ filter: z.union([- filter: z.unknown().refine(...)then+ filter: z.union([z.any()(new)- filter: z.any()then+ filter: z.union([The recognition keys on the REMOVED value being a universal acceptor, never on the spelling of
the added one: a
z.discriminatedUnionand az.literalreplacement read the same as thez.unionone, and thez.array(z.any())spelling is unchanged.Evidence
1. The live pair, on the bytes objectui#9540 actually pushed
Fetched by ref, never
FETCH_HEAD:git fetch --depth 3 origin refs/pull/9540/head:pr9540,head
3e60765fb4a80c3ee5c098b8133a26533de39842, diff3e60765^..3e60765(4 files).21b7c12b4(this branch's base):exit 4,T1 packages/types/src/zod/objectql.zod.ts:1811 — + filter: z.union([— byte-identical to the C5 row on the card.exit 0, "4 changed file(s) — 1 judged against a declared surface (no widening tell), 3 NOT MEASURED".PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9540from THIS seat exits 3 (PREREQUISITE NOT MET —
GET /repos/objectstack-ai/objectui/pullsisHTTP 403 for this credential, with and without the token). That is a transport refusal, not a
reading, so the line above is measured on the tell half — the half this PR moves — against the
PR's real bytes.
2. The self-test battery: 309 cases before, 342 after
pnpm check:pm-widening-tells— exit 0. Every pre-existing case is unchanged (309 passed onthe matcher change alone, before any case was added). The new battery is 33 cases, pinned at 33
in
SELF_TEST_BATTERIES, and the firing half is written first: the dark control, a differentkey, a value that was never universal, a
.refine()d one, an inline enum widened in place, agenuine new key on an exhausted budget, and a set whose list opens on a later line.
3. Ablations, with on-disk evidence
All three through
scripts/ablation-replace.mjs(anchor must hit, before/after counts,git hash-objectchange, proven restore, EXIT/INT/TERM trap). Blob092294bc786bat HEAD forevery leg, and every leg restored to it with
git diff HEADempty.e344d529aa737dbb792745294. History simulation — 1,048 commits, zero rows move
Every commit in this checkout touching
packages/spec(1,048), diffed at--unified=3andjudged by BOTH module versions in one process:
Harness control (the reading is not "the two modules are one module"): the same harness reports
the live pair as
T1@1811under the base gate and[]under this branch, and reports control Bfiring under both. The landed population of the repaired class in this repo is therefore ZERO —
the live carrier is in the sibling repo — which is why the decline is proven by the live pair,
the battery and the ablations rather than by this simulation.
5. Counts, each with a firing control
safeParseover42,'stage=won',null,undefined,{},[],trueand a function:z.unknown(),z.any()and both with.optional()accept all eight. Firing control on the same probe:z.unknown().refine(v => typeof v === 'string')accepts ONE of the eight, andz.union([z.array(z.any()), z.record(z.string(), z.any())])accepts two — the narrowing thiscard is about.
packages/spec/src+packages/runtime/srcopen
z.unknown()(127) orz.any()(5); the whole tail vocabulary is.optional()(89),.describe()(84),.meta()(1). Nothing else appears, so the allowlist is those three andevery other step leaves the tell firing.
balanced
)that continues on the next line, 4 end inside an open(. The reading isline-local, so the 33 keep telling (the loud direction).
z.custom()is deliberately NOT declared — 0 code occurrences in those two trees (3 texthits, all prose in a docblock or test comment). Firing control: the same scan finds all 132
acceptor lines. A row for a shape no tree carries is dead data, and
z.custom(fn)narrows.skip-changeset, measured
70 manifests in this repo declare a
files[]; none of them namesscripts/pm, and the changedfile lives outside every package directory. Firing control on the same reader:
packages/lintreads back["dist","README.md","CHANGELOG.md"]. Nothing published moves.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 29commands for this diff (identical before and after merging
origin/main). All 29 were run withthe exit code captured before any pipe, and reconciled with
--ran; results are in the reportcomment on #18234.
Acceptance notes
universal acceptor (
z.union([...])thenz.unknown()) is a real widening and raises no tell.That silence is #16943's replacement budget and it PREDATES this PR: measured identical at
21b7c12b4, with the new-key and widened-enum controls firing on the same harness. It is notrepaired here because reporting it changes the budget for EVERY key rather than for this class,
which is a different card's scope. The battery asserts it as a quiet direction, so the case
reds the day it is repaired.
is not certified and its replacement still fires (33 of 132 lines on this tree take that
spelling). The overturn condition is written into the header: the first landed pair whose
removed acceptor wraps closes it by reading the block's removed run forward instead of one line.
Generated by Claude Code