Skip to content

fix(pm): let check-widening-tells' T1 express that the REMOVED value accepted everything - #18626

Merged
os-try-charles merged 2 commits into
mainfrom
claude/issue-18234-t1-universal-acceptor
Sep 17, 2026
Merged

os-try-charles merged 2 commits into
mainfrom
claude/issue-18234-t1-universal-acceptor

Conversation

@os-try-charles

Copy link
Copy Markdown
Collaborator

Fixes #18234

Clause-②: no

scripts/pm/** is shipped by no package (measured below), and a precision fix to a gate
moves 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 universal
acceptor — into z.union([...]). 'stage=won' and 42 were admitted before and are refused
now, 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 spelled
z.union([. One semantic change, two opposite verdicts, decided by nothing but whether the
added value's first line opens a closed-set constructor.

#16943's replacement budget was EARNED (the removed filter: line is T1-shaped and buys one
T1 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:

case patch before after
live (objectui#9540) - filter: z.unknown()... then + filter: z.union([ (members on later lines) FIRES T1 declines
control A the SAME removal then + filter: z.array(z.any()).optional()... declines declines
control B a genuinely new key, nothing removed FIRES FIRES
control C z.enum(['a','b']) then z.enum(['a','b','c']) FIRES FIRES
control D z.enum(['a','b','c']) then z.enum(['a','b']) declines declines
dark control (new) the live pair's ADDED lines with NOTHING removed FIRES FIRES
different key (new) - legacyFilter: z.unknown() then + filter: z.union([ FIRES FIRES
never universal (new) - filter: z.string() then + filter: z.union([ FIRES FIRES
narrowing step (new) - filter: z.unknown().refine(...) then + filter: z.union([ FIRES FIRES
surplus (new) the live pair PLUS one genuinely new key in the same block FIRES FIRES (the new key)
z.any() (new) - filter: z.any() then + filter: z.union([ FIRES declines

The recognition keys on the REMOVED value being a universal acceptor, never on the spelling of
the added one: a z.discriminatedUnion and a z.literal replacement read the same as the
z.union one, and the z.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, diff 3e60765^..3e60765 (4 files).

  • gate at 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.
  • gate at this branch: 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 9540
from THIS seat exits 3 (PREREQUISITE NOT MET — GET /repos/objectstack-ai/objectui/pulls is
HTTP 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 on
the 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 different
key, a value that was never universal, a .refine()d one, an inline enum widened in place, a
genuine 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-object change, proven restore, EXIT/INT/TERM trap). Blob 092294bc786b at HEAD for
every leg, and every leg restored to it with git diff HEAD empty.

leg mutation blob after self-test
A delete the new spend disjunct e344d529aa73 9 of 342 FAIL
B add `refine pipe` to the inert chain vocabulary 7dbb79274529
C drop the key-identity requirement mutated, restore proven 2 of 342 FAIL

4. History simulation — 1,048 commits, zero rows move

Every commit in this checkout touching packages/spec (1,048), diffed at --unified=3 and
judged by BOTH module versions in one process:

  • rows raised by the base gate: 20,360; by this branch: 20,360
  • rows that now decline: 0. Rows that BEGIN firing: 0. Commits whose verdict moved: 0.

Harness control (the reading is not "the two modules are one module"): the same harness reports
the live pair as T1@1811 under the base gate and [] under this branch, and reports control B
firing 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

  • the universal-acceptor class, measured not assumed — zod 4.4.3, safeParse over 42,
    'stage=won', null, undefined, {}, [], true and 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, and
    z.union([z.array(z.any()), z.record(z.string(), z.any())]) accepts two — the narrowing this
    card is about.
  • the chain vocabulary — 132 key lines on packages/spec/src + packages/runtime/src
    open z.unknown() (127) or z.any() (5); the whole tail vocabulary is .optional() (89),
    .describe() (84), .meta() (1). Nothing else appears, so the allowlist is those three and
    every other step leaves the tell firing.
  • line termination — 99 of those 132 terminate the value on their own line, 29 end with a
    balanced ) that continues on the next line, 4 end inside an open (. The reading is
    line-local, so the 33 keep telling (the loud direction).
  • z.custom() is deliberately NOT declared — 0 code occurrences in those two trees (3 text
    hits, 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 names scripts/pm, and the changed
file lives outside every package directory. Firing control on the same reader:
packages/lint reads back ["dist","README.md","CHANGELOG.md"]. Nothing published moves.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 29
commands for this diff (identical before and after merging origin/main). All 29 were run with
the exit code captured before any pipe, and reconciled with --ran; results are in the report
comment on #18234.

Acceptance notes

⚠️ Noted, not filed here — the INVERSE direction is unreported. A key re-typed INTO a
universal acceptor (z.union([...]) then z.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 not
repaired 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.

⚠️ Residual, in the loud direction. A removed acceptor whose chain WRAPS onto a second line
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

`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>
@os-try-charles os-try-charles added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 17, 2026 — with Claude

Copy link
Copy Markdown
Collaborator Author

席内复核(派发席 domain:devx):条款② PASS;硬停那条线我自己驱动谓词验过了

读数时点 2026-09-17T09:54Z;PR 头 f1ca4aa3f(具名 ref pr18626),对照 origin/main @ 684146bb4。

分诊那条硬停:「不再误报」而不是「少报」—— 本席实测

我把两份门禁(origin/main 的与本 PR 的)在同一个进程里并排载入,用卡面那套对照组各跑一遍(2026-09-17T09:54Z):

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/main 309 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 成立。

我认可施工席的两个判断,并记下依据

  1. 识别键落在「被移除的值」上,⛔ 不在新值的拼法上 —— 这正是缺陷的自变量。z.any() 是否算全接受子,施工席用 zod 4.4.3 的 safeParse 对 8 个值实测(全收),⛔ 不是推断;而 z.unknown().refine(...) 8 收 1 ⇒ 链上有收窄步就不算。
  2. 新逻辑只读在既有的 spend 析取里 ⇒ 它只能让一个已经挣到的预算单位被花掉,⛔ 永远不能压掉一条没付过账的行。

放行路径

绿判据 2026-09-17T09:54Z 读为 NOT GREEN(6 条 in_progress)⇒ 等;绿后翻 ready → 重读到 total_count 真的移动 → 武装。


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants