…d line stops reading as an addition
The two tells the card reproduced fire on added LINES and could not see that a
line replaced an equivalent one: PR #16941 (T2, a form `description:` prose
rewrite on a file with no closed set in it) and PR #16968 (T1, a Zod key whose
`.describe()` text grew, keys 32 -> 32). Neither moved an accept set, and the
C5 row neither raised could be cleared except by declaring `Clause-②: yes` on a
change that does not widen.
`tellsInFile` now reads a REPLACEMENT BUDGET per change block, per tell kind:
each removed line carrying a member or key of kind K buys one added line of
kind K the right not to be reported, spent in patch order, so a block that adds
more than it removed still reports the surplus with its own file:line. The unit
is the change block, never the hunk, so an unrelated removal three context
lines away cannot pay for a real addition. Openers and prose fragments pay for
nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
Fixes #16943
Clause-②: no — this diff changes one PM gate script's classification of a diff. It adds no key, no arm, no export and no registration on any contract, published or registry surface; the only files it touches are
scripts/pm/check-widening-tells.mjsand its in-file self-test, which publish nothing from any released package.The defect, and the root cause the ruling adopted
check-clause2-carriers --pair Nraises a C5 row from the widening tells computed in the siblingcheck-widening-tells.mjs. Those tells read added lines, one at a time, and had no way to see that a line replaced an equivalent one. Two live pairs reproduced that independently — different tells, different file kinds:description:prose value rewrittenz.enum/ union /as const) do not occur inpermission.form.tsat all; sections 4 -> 4, fields 7 -> 7; positive controldefineForm= 2 fires.describe()text grewdescribe(= 1 firesNeither row could be cleared honestly:
c5WideningTell(pair, repo)reads only the pair and the repo, so the remedy sentence's "explain in the claim" moves no exit code and the only input that flips the number is a falseClause-②: yes. This PR fixes the predicate. ⛔ It changes no card's and no PR's clause-② declaration, so it does not trip the re-grade trigger triage wrote down.What was built — a net member/key delta, not a wider pattern
⛔ Not option 1 (tighten T2). Tightening T2 cannot stop T1: two tells reproduced one root cause on different code shapes. ⛔ And not a wider tolerance for string literals — a tell that stopped firing on real closed-set additions would be the more expensive failure.
tellsInFilenow carries a replacement budget, one per change block, per tell kind:FILE_SCHEMA_KEYself-test fixture is exactly that shape and must keep firing; pairing across it would buy silence with the wrong coin.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 established that, andrewritesExistingOpeneris the reading that judges an opener), and a removed line finding(pm):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 declines on the old side, since deleted prose is not a member either.New exported helpers:
memberTellKind(one classifier answering for an added line and a removed one, so the two sides of the comparison cannot drift) andchangeBlocks.Regression on BOTH recorded live pairs, from the real
/pulls/N/filesbytesorigin/maina9550bf)--declaration no --fileson PR #16941packages/spec/src/security/permission.form.ts:30 (T2)--declaration no --fileson PR #16968packages/spec/src/ui/dashboard.zod.ts:825 (T1)Both file listings were fetched from the API and fed to the gate unmodified; the
beforecolumn was taken fromorigin/main's copy of the script running on the same bytes.Positive controls — a REAL widening still fires
Real landed commits, read as real
git diffbytes, so "green" cannot mean the tell stopped working:613bfbd3dbonerror-code-ledger.zod.ts625b0c32b4onplatform-tool-names.ts233222e2edonanalytics.zod.tsdateRange:key whose schema was replacedThe one row that drops in the third control is the T1 on
analytics.zod.ts:440: the keydateRangewas already writable and the block replaced its value. The two genuine closed-set declarations and the ledger registration still fire, and the verdict is stillrefused.The self-test also carries the surplus rule directly: a block removing one member and adding two reports exactly one, at the line no removal paid for.
Ablation
Mutated the budget check away (
if (false ...)in front of the suppression), proved the mutation reached disk, and read the result:Restore leg:
git checkout HEAD -- ...(never a baregit checkout --), then blobb85a2e8404ad0cf4fb48f676959d0d6fdf1c1091equals HEAD's,ABLATED_16943occurrences back to 0,git diff HEADempty andgit status --porcelainempty. The mutation script carried atrap ... EXIT INT TERMon an absolute path throughout.The quiet direction, measured rather than left to be discovered
A one-for-one member rename inside an existing set now declines: nothing in a hunk distinguishes a renamed member from a reworded string, and the ruling this implements is replacement-vs-net-addition, not spelling. Measured over the 82 commits in this tree's history that touch these surfaces — of 715 change blocks that add a member or key line:
.describe()rewrites. Not one is a member rename.What still catches a rename that slips past:
check:api-surfaceon any exported name it moves,check:authorable-surfaceon any authorable key, and the ADR-0087 registries — instruments a rename must move and a rewording cannot. This paragraph is in the file's header too, so the next reader meets it there.The landing surface, corrected
The dispatch named
scripts/pm/check-clause2-carriers.mjsand its self-test. The C5 row is raised there, but the T1/T2 classification it renders lives entirely inscripts/pm/check-widening-tells.mjs, which the carriers file imports — there is one implementation, not two. So the fix lands in the sibling;check-clause2-carriers.mjsis unmodified, and its own self-test still passes (its C5 fixtures are addition-only and unaffected). ⛔ The region PR #17201 landed 20 minutes before this branch was cut (hasInlineClause2Key, the near-missreasonfield,c2DeclarationUnreadable'smissingbranch) is in the other file and is untouched.Verification
node scripts/pm/check-widening-tells.mjs --self-test— 173 cases pass, up from 150 onorigin/main; the new battery#16943 — the net member/key delta: a replaced line is not a net additiondeclares a floor of 23 and the roster floor moves 13 -> 14 for the one battery added.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon the branch tip and reconciled with--ran; counts are in the PR thread's report comment.eslint scripts/pm/check-widening-tells.mjs --no-inline-config --format jsonreports 1 file linted, 0 errors, 0 warnings. The narrowing excludes nothing, because this repo runs oneeslint.config.mjsthat never enables type-aware linting — noparserOptions.projectanywhere in it (grep count 0) and no typed@typescript-eslintrules, a property the config's own header records and measures — so an edit to this file cannot move the verdict on any file it does not contain.scripts/pm/**is repo tooling. The root manifest isprivate: true, and no published package'sfiles[]names a repo-rootscriptspath, so this diff publishes nothing from any released package.验收备注
Noted, not filed:
REFUSAL_SENTENCEstill offers "explain in the claim" as a door that moves no exit code (the file's header already records that at length). 承接者: whoever meets a third instance — triage's own re-grade trigger already routes that to p1.SELF_TEST_BATTERY_FLOORsits one below the roster's actual size, so exactly one battery can be deleted without redding the floor it pins. Left as found and moved 13 -> 14 to preserve that relationship rather than tighten an invariant this card did not measure. 承接者: the next PR that adds or audits a battery in this file.Generated by Claude Code