Skip to content

fix(pm): check-widening-tells reports a key re-typed INTO a universal acceptor - #18948

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-18629-widening-tells-reverse-direction
Sep 18, 2026
Merged

os-steve merged 1 commit into
mainfrom
claude/issue-18629-widening-tells-reverse-direction

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #18629

Clause-②: no

check-widening-tells now reports a key re-typed into a universal acceptor. #16943's replacement budget is stated in its own words as "replacement-vs-net-addition, not spelling", and that arithmetic is deliberately directionless: it confirms that the same key was rewritten and never asks whether the rewrite accepts more or less. That blindness made a real widening silent.

Premise, re-derived on the current tree

The dispatch warned that the merged PR #18920 moved this file by +165 lines, so every line reference on the card is stale. The defect was re-located and re-measured on the branch base 54145cce, with the card's own two controls lit on the same harness:

probe before after
INVERSE — filter: z.union([A, B]) re-typed to filter: z.unknown().optional() exit 0, declines exit 4, T1
CONTROL — a brand-new key (must fire) exit 4, T1 exit 4, T1
CONTROL — z.enum widened in place (must fire) exit 4, T1 exit 4, T1

Both controls fire under both readings, so that declines is a reading and not a mis-scoped probe. The premise holds exactly as filed.

The repair

#18234 supplied the one direction fact a line can carry, on the REMOVED side: a removed value that accepted everything proves the replacement is a subset by construction. This is that same fact read on the ADDED side, and the two are one predicate seen from either end. widensKeyIntoUniversalAcceptor asks for two facts the change block carries:

  1. the added line declares the key as a universal acceptor, by declaresUniversalAcceptorKey — the same reading [finding] check-widening-tells C5: narrowing a Zod key OUT of z.unknown() fires T1 when the new value opens z.union( and declines when it does not — #17618's three-fact spend cannot express "the removed value accepted everything" #18234 certifies a removed one with, so the two ends of the budget cannot drift apart about what "accepts everything" means; and
  2. a removed line names the SAME key and carries no universal-acceptor call at all.

Absence of evidence still leaves the budget paying, exactly as absence of evidence leaves a tell firing everywhere else in this file. This is not a change to the budget for every key, which is what the card and #18234's own section both predicted the repair would take.

Fact 2 is a MENTION rather than "not certified by fact 1", and that is the precision. 33 of this tree's 132 universal-acceptor key lines do not terminate on their own line, so negating fact 1 would fire on a pure reformat of an already-universal key — a false positive on a diff that changes no accept set at all. This gate is PM infrastructure and a false positive here blocks other seats' cards, so the reading declines there instead and the cost is stated in the header with its overturn condition.

The widening still SPENDS its unit and is reported on top of it. Refusing the spend would hand the unit to the next added line in the block, so a genuine new key riding along with the widening would go silent — this file's surplus rule inverted, one report bought at the price of another. Pinned: a block spelling - filter: z.union([A, B]), + filter: z.unknown(), + other: z.string(), reports BOTH rows, each with its own file:line.

The battery case that had to red

#18234's battery asserted this silence on the stated ground that the case "is what reds the day that lands". It did: the first self-test run after the one-line wiring failed exactly one case of 481, that one. It is replaced by the case asserting the inverse now fires while #18234's own direction still declines — so a repair of the inverse cannot be bought by breaking the narrowing side.

The new battery is 27 cases: 15 firing, 6 declining controls (neutral re-spelling, reformat, mention-inside-narrower, wrapped added chain, rename onto a new key, and #18234's live pair still clean), 6 on the reader itself. Self-test total 481 → 508.

Reverse verification

Ablation from the committed state, the on-disk write proven by scripts/ablation-replace.mjs (anchor 1 → 0, blob 31cb9de0f2952a87037c1241), then restored to a byte-identical tree (hash back to 31cb9de0f295, git diff HEAD empty):

No build step sits in this path (the gate runs from source as a node script), so no dist preflight applies.

Blast radius — how many existing diffs newly fire

Zero, on both boards this gate judges, with the harness control lit.

board window newly reported rows
objectstack-ai/objectstack 1219 commits, d83d079b 2026-09-06T18:43Z … e295d68a 0
objectstack-ai/objectui 1050 commits, … 3e4f632 2026-09-18T05:06Z 0
CONTROL — the card's own patch through the same sweep code path 1

Counting rows whose why carries the new wording is exact here rather than approximate: the only behavioural change is the one wired line, so a newly-reported row is precisely a row where the predicate answered true and the budget would otherwise have paid. The objectui sweep resolved no file-local declaring forms (its blobs are not in this object store), which cannot move this count — the new reading is line-local and never consults them.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derives 29 families from the one changed path. All 29 were run, each exit code captured before any pipe; all 29 exit 0. Reconciled with --ran carrying command :: exit N, so its 0 NOT-MEASURED is a derived zero rather than the runner's claim.

Eight of the 29 are --self-test-only invocations that judge a script's own battery against fixtures and so cannot judge this diff's content either way: check-closing-keyword-parity, check-declaration-mirrors, check-scripts-symbol-anchors, check-self-test-wired, check-self-test-workflow-commands, check-whole-set-label-write, bare-root-worklist, and check:pm-widening-tells. The last is the exception worth naming: it is check-widening-tells.mjs --self-test, and since this diff is that file, its battery is the direct judge of the change rather than a bystander.

Beyond the derived set:

  • pnpm lint (eslint . --no-inline-config, the whole repo, not a narrowing) — exit 0 at e295d68a, 68s.
  • the three other modules in this subtree, one of which imports this one (check-clause2-carriers, 941 cases; check-half-states, 4963; check-prior-rulings, 99) — all exit 0.
  • control-byte self-scan over the changed file — no hits.

This diff's own clause-② reading is NOT MEASURED, honestly: scripts/pm/** is covered by no declared surface, so the gate's exit 0 on it is evidence about no surface at all.

Changeset

skip-changeset, measured rather than assumed. The only changed file lives in the private root package (@objectstack/spec-monorepo, private: true, no files[]), and no package's files[] names scripts/. Grepping the new symbol across all 126 existing published paths of every non-private package returns 0 hits, with the positive control defineStack returning 45 over the same path set. Nothing published moves.

Acceptance notes


Generated by Claude Code

… acceptor

#16943's replacement budget confirms that a key was rewritten and never asks
whether the rewrite accepts more or less. That direction blindness made a real
widening silent: `filter: z.union([A, B])` replaced by `filter: z.unknown()`
earned a T1 unit on the removed line and spent it on the added one, so the gate
declined while both of the filing card's controls (a brand-new key, an enum
widened in place) fired on the same harness.

#18234 supplied the one direction fact a line can carry, on the REMOVED side.
This is that same fact read on the ADDED side: the added line declares the key
as a universal acceptor by `declaresUniversalAcceptorKey` — the same reading,
so the two ends of the budget cannot drift apart — and a removed line names the
SAME key carrying no acceptor call at all. Absence of evidence still leaves the
budget paying, as everywhere else in this file.

The widening SPENDS its unit and is reported on top of it. Refusing the spend
would hand the unit to the next added line, silencing a genuine new key riding
along with the widening — the surplus rule inverted.

The `#18234` battery asserted this silence on the stated ground that the case
"is what reds the day that lands"; it did, and it is replaced by the case that
asserts the inverse now fires while #18234's own direction still declines.

Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-authored-by: Claude <noreply@anthropic.com>
@os-steve os-steve added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 18, 2026 — with Claude
@os-steve
os-steve marked this pull request as ready for review September 18, 2026 07:51
@os-steve
os-steve added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit d8fac9a Sep 18, 2026
37 checks passed
@os-steve
os-steve deleted the claude/issue-18629-widening-tells-reverse-direction branch September 18, 2026 08:11
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