You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(pm): the widening refusal names the two doors that move its exit code, and pins the re-declared key (#18539)
Fixes#17848
Clause-②: no
`REFUSAL_SENTENCE` in `scripts/pm/check-widening-tells.mjs` now names
the two doors that
actually move its exit code, and `--self-test` pins the re-declared-key
shape in both
directions. 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 matcher
reports **no widening tell on any of them**:
| specimen | judged files | tells | exit |
|:--|--:|--:|--:|
| PR #17638 — `system/cache.zod.ts:197` | 3 of 7 (4 NOT MEASURED) | 0 |
0 |
| PR #17796 — `ui/view.zod.ts:1615` | 1 of 3 (2 NOT MEASURED) | 0 | 0 |
| PR #17846 — seven `filter:` doors | 3 of 5 (2 NOT MEASURED) | 0 | 0 |
⚠️ Every zero above is bracketed by controls, because a zero from a dead
invocation reads
the same as a zero from a decline:
- **Lit control** — a genuinely new key added to
`packages/spec/src/ui/view.zod.ts` (the
same file as #17796's specimen) fires `T1 … :101` and exits **4**. The
surface covers
these files and the run is alive.
- **Dark control** — the card's sharpest specimen reduced to its hunk,
with the removed
line deleted and the three added lines byte-identical, fires
`T1 packages/spec/src/ui/component.zod.ts:2504` and exits **4**. The
silence is bought
by the replacement, never by the shape.
⭐ And it did not fire at the card's own filing commit either. Running
`check-widening-tells.mjs` **as it stood at `758ac409`** (`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 hunk
carries 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 from
the `.describe()` pair #16943 pinned — the block removes ONE line and
adds THREE, of which
exactly 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:
- ⭐ **surplus control** — a genuinely new key (`filterLogic:`) added in
the SAME block as the
re-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:
> re-declare `yes` or explain in the claim why this addition does not
widen
`c5WideningTell()` compares the declaration against the diff's tells and
stops there; nothing
in 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 one
thing the standing rule forbids outright: 「⛔ 永不把 `no` 翻成 `yes` 去过门」. ⚠️
A gate
whose only working door is a lie teaches the lie.
⚠️ This file's header had already recorded that twice — at #16822 and
again at #16943 —
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:**
1. **Delete the second branch.** ⛔ Refused. It leaves `re-declare yes`
as the only door,
which on a FALSE tell is exactly the forbidden lie — the message would
then instruct it.
2. **Give the explanation a reader.** ⛔ Refused. An author-written
sentence that clears the
author's own gate is 自查放行, and it needs the new claim-line syntax #16448
forbids.
3. **Name the door that was always the right one.** ⭐ Chosen. #16822
already ruled where a
demonstrated false positive gets repaired — HERE, in the matcher, with a
`--self-test`
case 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_OK` 0 · `EXIT_USAGE` 1 · `EXIT_INCOMPLETE` 2 · `EXIT_REFUSED` 4,
all unmoved, and no
verdict 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 read
yesterday.
## Verification
`--self-test`: **309 cases pass**, exit 0 — measured against **298** on
`origin/main`'s own
copy of this file, so 11 cases were added and none removed. Sibling
`check-clause2-carriers.mjs --self-test`: **715 cases pass**, exit 0 —
it renders
`REFUSAL_SENTENCE` unparaphrased and that pin still holds through the
constant.
All **31** commands derived by `dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`
for 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 a
`trap … EXIT INT TERM`, and each restore proven by `git hash-object`
equalling the HEAD blob
`9ecddb0c…` with `git diff HEAD` empty:
| leg | mutation | on-disk proof | self-test | which cases red |
|:--|:--|:--|--:|:--|
| budget | the replacement budget never pays | 1 → 0 | exit 1, 26 fail |
the specimen case, the surplus control, and #16943's own live pairs |
| t1-silent | T1 declines unconditionally | 0 → 1 | exit 1, 43 fail | ⭐
the dark control AND the surplus control — the "silencing T1 generally"
direction |
| old-sentence | the pre-#17848 wording restored | 0 → 1 | exit 1, 4
fail | all four sentence pins |
⛔ The first attempt at the `t1-silent` leg 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-scripts` in
`packages/spec` packs **271** entries (the lit control) and **0**
matching `scripts/pm` or
`check-widening-tells`. The root package is `private: true`, and 0 of
the 70 publishable
workspace packages contain the changed path. Nothing published moves.
## Acceptance notes
- **noted, not filed** — the card's own "nine tells, 2026-09-12T11:5xZ"
figure was not a
reading of the gate on that date; its table's `git grep -oF` counts
(which prove the keys
are not new) were fresh, the tell count was not. Successor: this PR's
body and the report.
- **noted, not filed** — `SELF_TEST_BATTERY_FLOOR` is 16 against a
roster of 22 declared
batteries, 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.
- ⛔ Nothing here attaches, removes or waits on `needs:contract-review`;
`scripts/pm/**` is
not in `GOVERNED_SURFACES` (`check-governed-merges.mjs`: `docs/adr/**`,
`.claude/**`,
`skills/**`, `AGENTS.md`, `CLAUDE.md`), so this is an ordinary landing
path.
- ⛔ The three PRs the card names were not touched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
---
_Generated by [Claude
Code](https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6)_
---
## Landing note (seat, 2026-09-17)
Contract review at `CONTRACT_REVIEW_TIER` on head `f8ca2fdddf`: **PASS**
— record is comment `5706908992` on this PR.
⭐ The review did not reason about this gate, it **executed** it: it `git
archive`d a runnable subset at the head, at `origin/main` and at
`758ac409` (main seventeen minutes before the card was filed), ran
`--self-test` at 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`, `tellsInFile`
incl. 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 of
`REFUSAL_SENTENCE`, which no parser outside the file reads (0 hits,
control 9).
⚠️ **Two self-narration discrepancies in this body, appended rather than
rewritten** (this repo squashes, so the body becomes the permanent
commit message): the battery roster is **21** at head, not the 22 the
body states (20 at main); and the budget-ablation failure count measured
**24**, not 26 — the direction is confirmed, the exact figure is not as
stated.
⚠️ **One pin label overclaims, recorded not fixed:** 「the row it reports
is the new key, never the re-declared one」 holds for the fixture's
ordering only. With the new key written *before* the re-declaration in
the same block the gate still refuses (1 tell) but reports the
re-declared line. That is #16943's pre-existing patch-order budget and
it is the loud direction, so nothing is weakened — the word 「never」 is
simply too strong. ⛔ Not fixed in-branch: that moves the head and voids
an otherwise complete review record, for a word in a test label.
**Pre-landing checks:** ① review PASS on record ✅ · ②
`check-clause2-carriers --pair 18539` exit 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](https://claude.ai/code)_
Co-authored-by: Claude <noreply@anthropic.com>
@@ -2823,6 +2897,71 @@ export function selfTest() {
2823
2897
// that reds when it does.
2824
2898
t('⚠️ QUIET — a multi-line tombstone whose CLOSING line chains a live arm is not reported; population 0, and the header carries the overturn condition',tells({filename: TOMBSTONE_FILE,status: 'modified',patch: '@@ -30,0 +30,3 @@\n+ legacy: retiredKey(\n+ LEGACY_PRESCRIPTION,\n+ ).or(z.string()),'}).length===0);
t('⭐ the card’s specimen — key, optionality and element schema byte-identical, an `error` param added — is not a new key',tells(REDECLARED_WITH_ERROR_PARAM).length===0);
2957
+
t('…and the pair reads CLEAN end to end, which is the exit code the card reported as unreachable',wideningRefusal({declaration: 'no',files: [REDECLARED_WITH_ERROR_PARAM]}).state==='clean');
2958
+
t('⛔ DARK CONTROL — the same three added lines with NO removal still fire: the silence is bought by the replacement, never by the shape',tells(ERROR_PARAM_UNPAID).length===1&&tells(ERROR_PARAM_UNPAID)[0]?.tell==='T1');
2959
+
t('…at the line that declares the key, not at the param that chooses a refusal message',at(ERROR_PARAM_UNPAID)[0]==='packages/spec/src/ui/component.zod.ts:2504');
2960
+
t('⛔ an `error:` param is not a key on a shape — its value is not schema-shaped, so it neither fires nor SPENDS the budget',memberTellKind(" error: ruleArrayFilterError({ surface: 'object_grid' }),",{onContractSource: true})===null);
2961
+
t('⭐ SURPLUS CONTROL — a genuinely new key added in the SAME block still fires: one removal pays for one key',tells(ERROR_PARAM_PLUS_NEW_KEY).length===1&&tells(ERROR_PARAM_PLUS_NEW_KEY)[0]?.tell==='T1');
2962
+
t('…and the row it reports is the new key, never the re-declared one',at(ERROR_PARAM_PLUS_NEW_KEY)[0]==='packages/spec/src/ui/component.zod.ts:2508');
2963
+
t('⛔ and the re-declaration does not license the block: a THIRD key with no removal behind it is reported too',tells({ ...REDECLARED_WITH_ERROR_PARAM,patch: `${REDECLARED_WITH_ERROR_PARAM.patch}\n+ extra: z.string(),`}).length===1);
2964
+
2826
2965
// -- T3 --------------------------------------------------------------------
2827
2966
battery('T3 — a new row in a published entry point');
2828
2967
t('a new export row is a tell',tells(FILE_API_SURFACE)[0]?.tell==='T3');
@@ -2882,7 +3021,10 @@ export function selfTest() {
2882
3021
2883
3022
// -- the sentence and the prohibitions ------------------------------------
2884
3023
battery('the refusal sentence, and the two prohibitions it must keep');
2885
-
t('the sentence names both ways out',says(REFUSAL_SENTENCE,'re-declare `yes`')&&says(REFUSAL_SENTENCE,'explain in the claim'));
3024
+
t('the sentence names both ways out',says(REFUSAL_SENTENCE,'re-declare `yes`')&&says(REFUSAL_SENTENCE,'repair it here in the matcher'));
3025
+
t('⛔ #17848 — and BOTH are doors this file can open: the one with no reader is gone',!says(REFUSAL_SENTENCE,'explain in the claim'));
3026
+
t('…and its uselessness is stated outright, so no author spends a round rediscovering it',says(REFUSAL_SENTENCE,'moves no exit code'));
3027
+
t('…while the matcher door names the file to open and the case that must come with it',says(REFUSAL_SENTENCE,'scripts/pm/check-widening-tells.mjs')&&says(REFUSAL_SENTENCE,'`--self-test` case pinning the shape'));
2886
3028
t('…and quotes the declaration in the spelling the reader uses',says(REFUSAL_SENTENCE,'`Clause-②: no`'));
2887
3029
t('⛔ no label name appears anywhere in this file\'s outputs — a checker that hung one would be issuing the verdict',!says(REFUSAL_SENTENCE,'needs:')&&refusalLines(refusedAll).every((l)=>!l.includes('needs:')));
2888
3030
t('⛔ no new claim-line syntax is invented: the two values are the sibling\'s two',wideningRefusal({declaration: 'maybe',files: positives}).state==='not-applicable');
@@ -3093,6 +3235,7 @@ export function selfTest() {
3093
3235
'each bracketed by the control that still fires, '+
3094
3236
'#17300\'s retirement-ledger licence with the firing controls that bracket it on every side, '+
3095
3237
'#17955\'s tombstone decline — read before the budget so a rename is still paid for, and requiring the value to BE the call — with the un-retiring control, the two chained-arm controls that fire, and the multi-line chained close pinned as the residual quiet direction, '+
3238
+
"#17848's re-declared key with a zod `error` param — declined by #16943's budget, bracketed by the dark control that fires when nothing paid and the surplus control that fires on a real new key beside it, "+
3096
3239
"#16448's four positive controls each with its file:line, its negative controls — "+
3097
3240
'the same diffs with `yes`, and a removal-only diff with `no` — the local path composed end '+
3098
3241
'to end so a binary change to a tell surface cannot read as clean, #17112\'s split count with '+
0 commit comments