fix(pm): the widening refusal names the two doors that move its exit code, and pins the re-declared key - #18539
Conversation
…code, and pins the re-declared key `REFUSAL_SENTENCE` offered "re-declare `yes` or explain in the claim why this addition does not widen". The second branch has no reader: `c5WideningTell()` compares the declaration against the diff's tells and stops, so an author who followed the instruction got the identical exit 4 and no way to learn the remedy was never implemented. That left one working door, `Clause-②: no` -> `yes`, which on a FALSE tell is the widening-that-did-not-happen this family exists against. The sentence now names the two real doors -- re-declare when the diff widens, repair the MATCHER when the tell is false -- and says outright that an explanation moves no exit code. Giving the explanation a reader was refused rather than overlooked: an author-written sentence that clears the author's own gate is self-clearance, and it needs the claim-line syntax #16448 forbids. The other half of the filing did not reproduce. Re-run against all three PR diffs it measured, the matcher reports no tell -- and neither does it at the card's own filing commit (758ac40), because #16943's replacement budget had landed three days earlier. No matcher change was made for it; what it did leave was a missing case, so the `{ error: ... }` re-declaration is now pinned with its dark control (same added lines, nothing removed -> still fires) and a surplus control (a real new key beside it -> still fires). No exit code moves. The self-test grew from 298 to 309 cases -- 11 added in both directions, none removed. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #17848
Clause-②: no
REFUSAL_SENTENCEinscripts/pm/check-widening-tells.mjsnow names the two doors thatactually move its exit code, and
--self-testpins the re-declared-key shape in bothdirections. One file changed. ⛔ No exit code moves.
First act: the card's own specimens, re-measured — half one did not reproduce
The card's table was taken 2026-09-12T11:5xZ. Re-run today against all three PR diffs it
names (fetched as the PR's own diff and fed to
--declaration no --diff), this matcherreports no widening tell on any of them:
system/cache.zod.ts:197ui/view.zod.ts:1615filter:doorsthe same as a zero from a decline:
packages/spec/src/ui/view.zod.ts(thesame file as docs(spec):
navigation.viewstops promising a view selection nothing performs #17796's specimen) firesT1 … :101and exits 4. The surface coversthese files and the run is alive.
line deleted and the three added lines byte-identical, fires
T1 packages/spec/src/ui/component.zod.ts:2504and exits 4. The silence is boughtby the replacement, never by the shape.
⭐ And it did not fire at the card's own filing commit either. Running
check-widening-tells.mjsas it stood at758ac409(origin/main, 2026-09-12T11:38Z —seventeen minutes before the card was written) against the same five inputs reproduces the
same five readings: three specimens exit 0, both controls exit 4. The repair had landed
three days earlier, in #16943's per-change-block replacement budget: a key re-declared in
place removes a T1 line and adds one, and the removal pays.
⇒ the card's "nine tells" was carried over from the threads that accumulated on closed card
#17618; it was not a reading of the gate on the day it was filed. Two of the three PRs have
since merged (#17846 on 2026-09-12, #17638 on 2026-09-13) and #17796 was closed unmerged, so
the "three PRs parked" cost is also spent.
Why no matcher change was made for half one
⛔ The shapes the card floated — pairing across a HUNK, or diffing the file's key SET instead
of the block's added lines — are the silence
changeBlocks's own docblock refuses: a hunkcarries three context lines each side and routinely holds an unrelated removal at one end and
a real addition at the other, so pairing across it pays for a new key with a removal that has
nothing to do with it. Buying that would trade a loud failure for a quiet one to repair a
defect that is not there. A fix that silences T1 generally is worse than the bug, and
this PR does not make one.
What half one did leave is a gap in the instrument, not in the reader: the
{ error: … }re-declaration had no case of its own, and it is arithmetically distinct fromthe
.describe()pair #16943 pinned — the block removes ONE line and adds THREE, of whichexactly one is a key. A budget counting LINES instead of KINDS comes up short right there.
It is pinned now, with both controls above and a third:
filterLogic:) added in the SAME block as there-declaration still fires at its own file:line. One removal pays for one key; a real
widening riding along with a re-declaration is still caught.
Half two — the remedy with no reader — is what this PR repairs
The sentence offered two doors and only one was real:
c5WideningTell()compares the declaration against the diff's tells and stops there; nothingin either file reads an explanation. So an author who followed the instruction got the
identical exit 4 with no way to learn that the remedy was never implemented — and the only
door that DID move the number was
Clause-②: no→yes, which on a false tell is the onething the standing rule forbids outright: 「⛔ 永不把
no翻成yes去过门」.whose only working door is a lie teaches the lie.
without ever changing the string a refused author actually reads. So the file knew and the
author could not: a declared-but-unenforced remedy, which this repo removes rather than
documents.
Three shapes were weighed:
re-declare yesas the only door,which on a FALSE tell is exactly the forbidden lie — the message would then instruct it.
author's own gate is 自查放行, and it needs the new claim-line syntax pm gates: a "widening tell" check — a diff that ADDS a key / arm / branch to a schema or registration while its claim says
Clause-②: nois refused at enqueue (mechanical control for the directional Clause-② ruling on #16349) #16448 forbids.check-widening-tells' T2BARE_STRING_ELEMENTfires on the FIRST fragment of a multi-line string ARGUMENT — one false C5 blocked a landing whose diff only narrows #16822 already ruled where ademonstrated false positive gets repaired — HERE, in the matcher, with a
--self-testcase pinning the shape — and the sentence now says so, names the file to open, allows
filing it as its own card when it is out of the PR's scope, and states outright that an
explanation moves no exit code so nobody spends a round rediscovering it.
Both doors the sentence now names are doors this file can open. That is the pin.
Exit-code contract — ⛔ UNCHANGED
EXIT_OK0 ·EXIT_USAGE1 ·EXIT_INCOMPLETE2 ·EXIT_REFUSED4, all unmoved, and noverdict state changes. Every tell fires exactly where it fired: the only behavioural surface
touched is the TEXT a refusal renders. The seat reading
$?reads the same table it readyesterday.
Verification
--self-test: 309 cases pass, exit 0 — measured against 298 onorigin/main's owncopy of this file, so 11 cases were added and none removed. Sibling
check-clause2-carriers.mjs --self-test: 715 cases pass, exit 0 — it rendersREFUSAL_SENTENCEunparaphrased and that pin still holds through the constant.All 31 commands derived by
dispatch-gates.mjs --commands --repo objectstack-ai/objectstackfor this file surface were run; every one exited 0, each exit code captured by
redirect-then-
$?, never through a pipe.Non-vacuity — three ablation legs, each proving the mutation reached disk by a
grep -c(LINES) count on the anchored text before and after, each restored under atrap … EXIT INT TERM, and each restore proven bygit hash-objectequalling the HEAD blob9ecddb0c…withgit diff HEADempty:⛔ The first attempt at the
t1-silentleg counted an anchor the mutation does not move(before=1, after=1). It was reported as a void reading and re-run against the injected text
rather than quietly retried until something landed.
skip-changeset, measured not asserted.npm pack --dry-run --json --ignore-scriptsinpackages/specpacks 271 entries (the lit control) and 0 matchingscripts/pmorcheck-widening-tells. The root package isprivate: true, and 0 of the 70 publishableworkspace packages contain the changed path. Nothing published moves.
Acceptance notes
reading of the gate on that date; its table's
git grep -oFcounts (which prove the keysare not new) were fresh, the tell count was not. Successor: this PR's body and the report.
SELF_TEST_BATTERY_FLOORis 16 against a roster of 22 declaredbatteries, so five could be deleted without reddening. That is what a floor IS (AGENTS.md
prescribes a minimum, not an equality), and adding batteries must not red — an observation
about slack, not a defect. Successor: none.
needs:contract-review;scripts/pm/**isnot in
GOVERNED_SURFACES(check-governed-merges.mjs:docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md), so this is an ordinary landing path.🤖 Generated with Claude Code
https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Generated by Claude Code
Landing note (seat, 2026-09-17)
Contract review at
CONTRACT_REVIEW_TIERon headf8ca2fdddf: PASS — record is comment5706908992on this PR.⭐ The review did not reason about this gate, it executed it: it
git archived a runnable subset at the head, atorigin/mainand at758ac409(main seventeen minutes before the card was filed), ran--self-testat each (309 / 298 / 269, all exit 0), re-fetched the three specimen PR heads and fed their real diffs through all three file versions, and ran three ablations with the blob hash re-verified after each restore. That is what makes its central finding trustworthy.Central finding, which confirms this PR rather than undermining it: the card's half one — T1 firing on a re-declared key — was never real on any reachable version of the file, with a lit control alive at every one. The card's 「nine tells」 figure reproduces nowhere. Half two (a remedy with no reader) was real and is what this PR fixes. ⇒ closing #17848 on this PR is supportable.
The direction that mattered — does this weaken the gate? No. Every predicate (
patchLines,changeBlocks,memberTellKind,tellsInFileincl. the #16943 budget,wideningRefusal,exitForRefusal) is byte-for-byte unchanged; the exported symbol list is identical at 46; the exit register stays 0/1/2/4; and the t1-silent ablation reds both the new dark and surplus controls. The only output change is the text ofREFUSAL_SENTENCE, which no parser outside the file reads (0 hits, control 9).Pre-landing checks: ① review PASS on record ✅ · ②
check-clause2-carriers --pair 18539exit 0; ⛔ no carriers were hung on this pair (Clause-②: no, no declared surface) so there is nothing to strip ✅ · ③ re-taken at landing time, latest-run-per-check-name ✅. Governed-surface predicate: 0 of 1 path hits the register ⇒ ordinary queue landing.Generated by Claude Code