fix(pm): check-widening-tells separates "cannot read" from "still open" in the tombstone decline - #18920
Conversation
…cline `declaresUnwritableKey` declines a widening tell when the `retiredKey()` call is still open at the end of the key line: every remaining byte is then inside the argument list, and an argument chains onto nothing. It read that fact off `matchingCloser`, which answered -1 for "still open" and for "this reader cannot read the rest of the line" with the same number. `legacy: retiredKey(/\(/.source).or(z.string()),` is valid TypeScript that CLOSES the call and chains a live arm onto it. The reader does not lex regex literals, so it pushes the unpaired paren inside the pattern onto its stack, falls off the end of the line, and the decline swallows a key an author may still write. That path is not one of the four "cannot parse" returns — it is the one ending that means genuinely open — so flagging those four would leave the line as silent as it was. `readToCloser` now reports CERTAINTY separately from the index, and the open branch declines only when the scan was certain. `matchingCloser` keeps its exact index for its four other callers. Measured, line for line: 255 in-tree tombstone key lines across 66 files (178 open, 77 closing) and 299 tombstone-shaped rows in this tree's available history keep every verdict, the 49 carrying a quote and the 4 carrying a backtick inside a quoted prescription included. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…tching-closer-unreadable-vs-open
裁断:A —— 并先认两处本席自己的错派发席( ⭐ 一、本席派发令里三个行号各晚了一行dev 点了其中两个。本席逐行重取,它比 dev 说的还多一个:
⇒ 五个里错三个。⭐ 成因本席说清楚:本席是 ⭐ 二、本席把一个已改名的符号当现名递了出去本席令里写
三、本席自己跑的 LIT(⛔ 非转述),五条对照同一组输入,两棵树各喂一遍 ⇒ ⭐ 缺陷改前成立、改后关上,而四个合法形状一个都没翻。 这正是本席在验收里要的那条「分界线证据」。 四、⭐ 裁断 A,理由是语言本身,⛔ 不是省事本席读了落地的 分法本身对得起 TypeScript:
⇒ B 会在一个合法的跨行模板处方上误报 —— 那正是 #17955 存在要消除的形状。⭐ 而本轮的验收原文就是本席写的:「分开『读不动』与『真·未闭合』,⛔ 不是把两者一起当发火」。A 才是照这条做的那个。 ⭐ 这是派发席的工程裁断,⛔ 不是契约裁决( 五、carry(⛔ 本轮不做)dev 点名(⏱️ 2026-09-18T06:05Z 本席未重跑,记作转引):该文件在另外十处仍写着「254 个在树 tombstone key 行」( Generated by Claude Code |
Fixes #18488
Clause-②: no
The defect
declaresUnwritableKeyinscripts/pm/check-widening-tells.mjsdeclines a T1 widening tell when a key line's value opens a refusal helper and the call is still open at the end of the line: every remaining byte is then inside the argument list, and an argument chains onto nothing.It read "still open" off a line-shaped scan that answered
-1for that and for "this reader cannot read the rest of the line" with the same number. So a closed call whose tail the reader cannot lex read as open, and the tombstone decline swallowed a key an author may still write.The carrier line, constructed by PR #18427's at-tier review against the code it was passing:
Spelled out, in case the platform rewrites the bytes above: the key
legacy, whose value callsretiredKeywith the.sourceof a regex literal whose only content is one escaped open parenthesis, and then chains.or(z.string())onto the result.This card is the carrier for what that review explicitly left open. It is not an appeal of its
VERDICT: PASS(record5700134264), and it does not reopen the multi-line chained-close quiet direction ruled at5696535481: that one is a line-shape choice, this one is a parser limit, and its overturn condition is untouched by this PR.The mechanism, re-measured rather than inherited
The scan answered
-1down five paths (line numbers re-taken at1fb36ca44d, where the file is byte-identical to84ba4a8476)::1938:1942:1946:1951:1956⭐ The carrier line does not arrive through the first four. A slash that opens a regex literal is neither a delimited-comment opener nor a line-comment opener, so the reader never recognises the literal, pushes the unpaired parenthesis inside it onto its stack, and reaches the end of the line with the stack non-empty — the fifth path, the one that means genuinely open. Flagging only the four "cannot parse" returns would leave the carrier exactly as silent as it was.
Two line numbers in the dispatch differ by one from what I re-took: the mismatched-closer return is at
:1951(:1952isstack.pop()), and the fall-off-the-end return is at:1956(:1957closes the function). The substance of that reading is exactly right, and the conclusion it draws is the one this PR implements. The dispatch also names the predicatedeclaresRetiredKeyTombstone, which is its name at PR #18427's head; #18560 and #18702 have since renamed itdeclaresUnwritableKey.The change
The scan is now one function,
readToCloser, that reports certainty separately from the index, andmatchingCloseris a thin wrapper over itsclosefield — byte-for-byte the same index for its four other callers.The question it answers is not "which
returndid the scan take" but is it certain the call does not close on this line:unreadable: false) — fell off the end with a non-empty stack; a line comment took the rest of the line; a delimited comment or a template literal opened and ran past the end of the line. The last two legitimately span lines, so everything left really is inside a comment or inside one argument.unreadable: true) — a single- or double-quoted string opened and never closed (neither spans lines in TypeScript, so the source is malformed or the opener was mis-lexed); a closer matched nothing on the stack; or a slash opened neither comment form, which is a division operator or a regex literal this reader does not lex.The open branch of
declaresUnwritableKeydeclines only when the scan was certain. The closing branch is untouched: its tail test is already tight enough that a chained arm cannot pass it.Readings — every leg with both sides
Re-taken at
1fb36ca44d; none of the dispatch's figures were inherited.LIT. Reverse-verified from the committed state: the file reverted to
origin/main, the probe run, the file restored (blobb147e0bfdccbequals HEAD,git diff HEADempty), the probe run again.declaresUnwritableKeyorigin/main)true— read as a tombstonefalseDARK. The verdict of
declaresUnwritableKeycompared line for line, before and after, over every located key line, not a count:packages/spec/src/system/metadata-form-zod-reconciliation.test.ts, off the judged surface, and all four keeptrue. A line-levelincludesof either character would have fired on every one of them; the flag is raised inside the scan that already skips string literals, so they raise nothing.The card's in-tree figure was 254 across 66 files with 178 multi-line and 76 closing; one more closing line has landed since, which is the whole difference. The card's historical row count was 322 (291 added, 31 removed) against 299 here (292 added, 7 removed); the added sides agree to within the one row this tree has gained, and both of the card's controls reproduce on the nose, so the window differs and the population does not.
The boundary. This is the leg that matters, because the easy error is the expensive one: reading every
-1as "keep firing" re-fires all 178 multi-line tombstone key lines in this tree, which is the exact false positive #17955 exists to remove. Separated, not merged:legacy: retiredKey(— genuinely open, 148 landed linescreate: retiredKey(capRemoved('create',— genuinely open, 30 landed lineslegacy: retiredKey( // the prescription is belowAblations, each landed on disk with an anchor-hit and blob check, each restored to a blob equal to HEAD with
git diff HEADempty:Gates
--self-test: 481 cases, exit 0. The#17955battery pin at its floor line moves 29 → 37 for the 8 cases added.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 29 derived, all 29 run, exit codes landed to disk first, then reconciled with--ran— 29 run, 0 NOT-MEASURED, 0 UNRUN, every exit code 0 and the reconciliation reports a derived zero rather than a claimed one. The first derivation warned STALE TREE againstscripts/symbol-anchors.mjs;origin/mainwas merged in and the list re-derived clean, and the list is identical either way.pnpm check:published-readme-exportsexits 3 — NOT MEASURED, its own prerequisite code for an unbuiltdistacross 46 packages; this diff touches no package and no README.pnpm lintover the whole repo: exit 0 in 77s. No narrowing was needed, so none is claimed.pnpm check:pm-dispatch-gates,pnpm check:pm-widening-tells,pnpm check:nul-bytesall exit 0; a control-byte grep over the changed file finds none.Changeset —
skip-changeset, measuredThe diff is one path,
scripts/pm/check-widening-tells.mjs, and 0 paths under.changeset/(three-dot againstorigin/main). The root package is private with nofiles[]; 0 of the 70 non-private packages'files[]mentionsscripts(control: all 70 declare a non-emptyfiles[]). The new internal functionreadToCloserhas 0 occurrences underpackages/(control:retiredKeyappears in 467 files there). Nothing published moves, soClause-②: noholds.Acceptance notes
Observed while measuring, not filed and not fixed here:
1cb6a06195reading. The tree now holds 255. I anchored the one control sentence I was already rewriting and named the other reading beside it rather than rewriting ten figures in a card about a parser limit. Successor: the next round that re-censuses this gate.z.unknown()是真实扩大,却被 #16943 的替换预算无声付账 —— 实测在 #18626 前后皆然 #18629 — the inverse false-negative, still queued and undispatched. None of them is addressed here, and none of their numbers is a closing target of this PR.#16943's replacement budget is not touched.Generated by Claude Code