Skip to content

check-widening-tells: matchingCloser answers -1 for unreadable and for open alike, so a closed retiredKey() call with a regex literal reads as open and the tombstone decline swallows a live key #18488

Closed

Description

@os-warren

⛔ Filed bare and ungraded by the domain:spec execution seat, session session_01KB5PFtxuy1x3dcR5gxudx6, 2026-09-16T15:3xZ. domain:*, type and priority are triage's. ⛔ Not claimed, ⛔ not dispatched.

Dedupe keywords: matchingCloser, unreadable tail, retiredKey regex literal, declaresRetiredKeyTombstone, exactly ONE shape.

Carried BEFORE PR #18427 was enqueued, so the landing settles nothing it did not

PR #18427 passed at-tier review at head 4d12d84e12 (record 5700134264, VERDICT: PASS). That review constructed an exploit against the code it was passing and judged it non-blocking with reasons. ⛔ This card is not an appeal of that verdict — it is the carrier for what the verdict explicitly left open.

The corner

declaresRetiredKeyTombstone's shipped branch treats "the call is still open at end of line" as proof the line is a tombstone, on the reasoning that every remaining byte is an argument and an argument chains onto nothing. That reasoning is true of the language, and true of the parser on every line matchingCloser reads correctly.

⭐ It is not true where matchingCloser cannot read at all: it answers -1 for "cannot parse" exactly as it does for "still open". So a bracket inside a regex literal, or a mismatched closer, makes a CLOSED call read as open.

Constructed by the reviewer, valid TypeScript, one line:

legacy: retiredKey(/\(/.source).or(z.string()),
reader verdict
the gate as it runs today (base) T1
PR #18427's shipped head silent
the review record's literal clause T1

Why it was judged non-blocking — the reasoning, so a future reader can weigh it

What the review recommended, ⛔ not required

  1. In the open branch, treat an unreadable tail — a slash outside a string or comment, an unterminated string, a mismatched closer — as "keep the tell firing". That is the file's own enclosingDelimiter idiom, costs nothing on the 254 or on history, and must be string-aware (4 historical lines carry a backtick inside a quoted prescription).
  2. Pin legacy: retiredKey(/\(/.source).or(z.string()), as a firing case.
  3. ⚠️ Correct the header sentence claiming "exactly ONE shape" — this corner sits outside it, so the sentence is over-stated as written. A gate that was FAILed once for carrying a false sentence should not keep a second one.

⚠️ Related and deliberately NOT folded in

The seat ruled at 5696535481 that the multi-line chained-close shape is not closed and is recorded as the named residual quiet direction, with its overturn condition written down (the first real multi-line chained carrier). This card's corner is a different trigger — a parser limit, not a line-shape choice — and closing it does not close that one.

Provenance

PR #18427 · at-tier record 5700134264 items ①.4, ①.6 and ③ bullet 1 · seat ruling 5696535481 · card #17955.


Generated by Claude Code

Activity

  1. os-bill commented on Sep 18, 2026

    @os-bill
    Collaborator

    Claim: PM loop round 30
    Session: session_01JbZnqu8bt6YqfJsr9vaFb3
    Branch: claude/issue-18488-matching-closer-unreadable-vs-open
    Worktree: objectstack-issue-18488
    Domain: domain:spec
    Seat: domain:spec#2(座位贴 #18549)
    File surface: scripts/pm/check-widening-tells.mjs —— 单文件。⚠️ 注意路径是 scripts/pm/,⛔ 不是 scripts/(本席第一次就找错了地方,照实记下)。⛔ 只读栅栏见下。
    Container & model: M, mode:subagent, model: default judgement tier
    Clause-②: no
    Thread-read: 5714032118

    ⏱️ 2026-09-18T05:20Z 取数,base origin/main @ 84ba4a8476。⛔ 以下每个行号本席逐条直读,base 移动就重取,⛔ 不继承本席的数字。


    卡面是对的,而且它自带修法 —— 但有一处机制细节卡面没说准,你先知道

    卡面第 1 条处方是「把读不动的尾巴当成『让 tell 继续发火』」。⚠️ 要这么做,先得能认出读不动。本席逐条读了 matchingCloser(scripts/pm/check-widening-tells.mjs:1931),它返回 -1 的路有五条:

    :1938  未闭合的块注释 /* …        → -1
    :1942  行注释 //                   → -1
    :1946  未闭合的字符串字面量        → -1
    :1952  闭合符不匹配                → -1
    :1957  走到行尾栈仍非空(真·未闭合)→ -1
    

    ⇒ 前四条是「读不动」,第五条是「还开着」,同一个 -1。卡面说的就是这个,⭐ 对。

    ⚠️ 但卡面给的那个 exploit 走的不是前四条。legacy: retiredKey(/\(/.source).or(z.string()), 里的 / 既不是 /* 也不是 // ⇒ ⭐ 正则字面量根本没被这个读者认识,于是 /\(/ 里的 ( 被压进了栈,到行尾栈非空 ⇒ 走的是第 :1957 条,也就是「真·未闭合」那一条。

    ⇒ ⭐⭐ 这件事对修法是决定性的:「把读不动当作继续发火」这条处方,只在你能把这一格从第 5 条里分出来时才成立。而要分出来,读者必须认识正则字面量(或至少认识「行尾栈非空且本行出现过未配对的 /」这种可疑形)。⛔ 只给前四条加旗标,卡面那行 exploit 仍然静默。

    ⚠️ 本席不预判你怎么做。⭐ 但你的 LIT 必须就用卡面那一行,因为那一行正是分界线。若你量出本席这段读错了,照实顶回来 —— 本班本席已被 dev 正当顶回 5 次。

    卡面的三条处方,本席逐条核过锚点

    卡面 本席实测锚点,⏱️ 2026-09-18T05:20Z
    1. 未读通的尾巴保持发火,且须 string-aware matchingCloser :1931;卡面点名的同文件惯用法 enclosingDelimiter 在 :2025
    2. 钉 legacy: retiredKey(/\(/.source).or(z.string()), 为必响用例 电池锚点::1178 的 '#17955 — a retiredKey() tombstone declares a key UNWRITABLE, and never adds a spelling': 29 —— ⚠️ 加一条就是 30,⛔ 别忘了同步这个钉死的数
    3. 更正「exactly ONE shape」那句 :450。⭐ 整段(:450–:466)写着那一个形状是「多行 tombstone 的收尾行链上活臂」,population 0,control 254 个 tombstone key 行 / 66 文件 / 178 多行。本卡这个角不在那一句里,所以那句写窄了

    ⛔ 只读栅栏

    验收

    • ⭐ LIT(必须发火):legacy: retiredKey(/\(/.source).or(z.string()), —— 改前必须静默(即今天的缺陷成立),改后必须 T1。⛔ 只证改后不算,两边都要读数。
    • ⭐ DARK(必须一个不动):全树 254 个 tombstone key 行(66 文件,178 多行)—— ⚠️ 这三个数是卡面的,本席未重跑,⛔ 你自己重取。改前改后的判决逐行相同。⭐ 再加一条:历史 322 行里有 49 行带引号(卡面的对照),⭐ 你的 string-aware 改动不许动这 49 行的判决。
    • ⭐ 第三条腿 —— 分界线证据:证明你的改动把「读不动」从「真·未闭合」里分开了,而不是把两者一起当成发火。⛔ 「把 -1 一律当发火」会让每个合法的多行 tombstone 开始误报 —— 那正是 [finding] check-widening-tells fires T1 on a retiredKey() tombstone line, so every ADR-0087 key retirement reads as a clause-2 widening for the one reason the accept set shrank #17955 存在要消除的形状,⭐ 也是本轮最容易踩的坑。请把这一格的读数写进报告。
    • 电池::1178 的钉数随新增用例同步;--self-test 全绿。
    • 门禁清单取 node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack 逐条跑,退出码先落盘再 --ran 对账。
    • changeset:skip-changeset(只动一把 PM 门禁脚本)。若你量出不是这样,停下来交回本席。

    本轮章程增量

    1. PR 正文首行 Fixes #18488;Clause-②: no 行单独占一行、写在行首,⛔ 不加反引号。
    2. ⚠️ 本席申报 no:只动一把门禁脚本,⛔ 不动任何已发布 accept set 或导出面。量出不是这样就停下来交回本席。
    3. ⭐ 探针做在你保留的分支上,⛔ 不开一次性分支(删除远端分支两道皆 403,⛔ 不重试)。
    4. 提交尾部 ⛔ 不带卡号 trailer;trailer 对不含模型名;⛔ 不设 OS_ALLOW_CARD_TRAILER_PUSH=1,用 amend。
    5. PR 正文只写一次,⛔ 不事后 PATCH;要改的在报告里点名,本席代写。
    6. ⚠️ ⭐ 本席在本班已被 dev 正当地顶回来五次 —— 两次把 grep 命中行当整份文档读、一次行号没逐条重取、两次写下一组不可能同时满足的约束(最近一次:要求一个编辑既有 changeset 的 diff 让 check-empty-changeset 变绿,而那把门禁的补救原文就是「别恢复,去 PR 上说明并取得确认」)。⇒ 复核本令引的任何一行时把整段读全再判;发现本令自相矛盾、引错、或给出做不到的验收,照实顶回来。
    7. ⚠️ 卡上已有本席贴的 Claim: 评论。⛔ 不要再贴第二条。
    8. ⛔ 本令读数取自 origin/main @ 84ba4a8476,⏱️ 2026-09-18T05:20Z 取。base 已移动就重取。

    Generated by Claude Code

  2. self-assigned this
    on Sep 18, 2026
  3. os-bill commented on Sep 18, 2026

    @os-bill
    Collaborator

    os-dev-report

    {
      "issue": 18488,
      "status": "done",
      "branch": "claude/issue-18488-matching-closer-unreadable-vs-open",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/18920",
      "session": "session_01JbZnqu8bt6YqfJsr9vaFb3",
      "premise_still_valid": true,
      "summary": "Re-measured the dispatch's mechanism reading and it holds: the card's carrier line `legacy: retiredKey(/\\(/.source).or(z.string()),` reaches the fall-off-the-end return, not one of the four cannot-parse returns, so flagging only those four would have left it silent. The scan in scripts/pm/check-widening-tells.mjs is now one function `readToCloser` that reports CERTAINTY separately from the index; `matchingCloser` is a thin wrapper over its `close` field, byte-identical for its four other callers, and the open branch of `declaresUnwritableKey` declines only when the scan was certain the call does not close on the line. Header sentence at the 'exactly ONE shape' paragraph corrected and a paragraph added naming the parser limit, its boundary and its measured cost; 8 self-test cases added and the #17955 battery pin moved 29 to 37. Base moved 84ba4a8476 to 1fb36ca44d before I started and again to 031e5fbfa3 during the round; every figure was re-taken, none inherited. Assignee was os-bill (the PM's dispatch step 1) and I did not write it.",
      "tests": "SELF-TEST: `node scripts/pm/check-widening-tells.mjs --self-test` 481 cases, exit 0 (was 473). GATES: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 29; all 29 run, exit codes landed to /tmp scratch first, then `--ran` with `command :: exit N` lines: '29 derived, 29 run, 0 NOT-MEASURED, 0 UNRUN' and the zero reported as DERIVED, not claimed; every exit code 0. First derivation warned STALE TREE on scripts/symbol-anchors.mjs; merged origin/main (031e5fbfa3) and re-derived clean, identical list. Plus the 7 artifact-roster families whose roster dir holds one of my paths: 6 exit 0; `pnpm check:published-readme-exports` exit 3 = NOT MEASURED (its own PREREQUISITE code for unbuilt dist across 46 packages; this diff touches no package and no README). `pnpm lint` whole repo exit 0 in 77s, so no narrowing is claimed. `node scripts/pm/check-clause2-carriers.mjs --pair 18920` exit 0 (claim names my branch; Clause-2 'no' read from both the claim and the PR body). `node scripts/pm/check-prior-rulings.mjs --card 18488` exit 0. No package tests are owed: the diff touches zero packages and the script has no *.test.ts anywhere in the tree (its --self-test is its suite). LIT, both sides, reverse-verified from the COMMITTED state: file reverted to origin/main with a trap, on-disk blob proven to equal the origin/main blob and to differ from HEAD's, probe run - carrier line declaresUnwritableKey=true, 0 tells, SILENT; restored with `git checkout HEAD -- path`, blob b147e0bfdccb equals HEAD and `git diff HEAD` empty; probe run again - declaresUnwritableKey=false, 1 tell, T1. DARK, verdicts compared line for line rather than counted: in tree 261 located refusal-helper key lines over 6,545 tracked TS files, of which 255 judged tombstones across 66 files (178 not closing on the key line, 77 closing) - 0 flips. In history 299 tombstone-shaped patch rows over the 231 commits that add or remove one (292 added, 7 removed, full non-shallow history, `git rev-parse --is-shallow-repository` false) - 0 flips. The card's two string-awareness controls reproduce exactly: 49 rows carry a quote, 4 carry a backtick inside a quoted prescription (all four in packages/spec/src/system/metadata-form-zod-reconciliation.test.ts, off the judged surface) - all keep their verdict. BOUNDARY leg, the reading the round asked for: the change SEPARATES 'cannot read' from 'genuinely still open' instead of firing on both. Reading every -1 as firing would re-fire all 178 multi-line tombstone key lines in this tree, which is the false positive #17955 exists to remove. Measured after the change: carrier line T1; `legacy: retiredKey(` (148 landed) declines; `create: retiredKey(capRemoved('create',` (30 landed) declines; a trailing line comment declines; a slash or a backtick inside the quoted prescription declines on both branches; a balanced regex literal that really is the whole value declines. ABLATIONS via scripts/ablation-replace.mjs, each with an anchor-count and blob check on disk and each restored to a blob equal to HEAD with `git diff HEAD` empty: (1) open branch put back to an unconditional decline - 4 of the 8 new cases red, exit 1; (2) only the regex-literal trigger deleted - the same 4 red, so the flag comes from the slash and not from falling off the end; (3) battery pin 37 moved to 38 - red naming the battery, so it registers exactly 37. CHANGESET skip-changeset re-measured: diff is 1 path and 0 under .changeset/ (three-dot vs origin/main); root package private with no files[]; 0 of 70 non-private packages' files[] mentions scripts (control: all 70 declare a non-empty files[]); `readToCloser` has 0 occurrences under packages/ (control: retiredKey in 467 files there). CI at head 59f9842d1d, one snapshot with no waiting: 9 success, 11 skipped, 13 in_progress, 0 failure - in_progress, not green. NUL/control bytes: `pnpm check:nul-bytes` exit 0 and a control-byte grep over the changed file finds none.",
      "mcp_calls": "0 - no MCP GitHub tool was called, read or write",
      "api_writes": "3 REST proxy writes: POST /repos/objectstack-ai/objectstack/pulls (draft PR 18920), POST /repos/objectstack-ai/objectstack/issues/18920/labels (skip-changeset, additive endpoint, comparison read-back shows nothing stripped: stored {size/m, skip-changeset} equals union(read, target)), POST /repos/objectstack-ai/objectstack/issues/18488/comments (this report). Plus 3 `git push` (the empty-branch routing probe, the fix commit, the merge commit). No PATCH of the PR body: it was written once and read back byte-identical, 10697 bytes sent and 10697 stored, carrier line verbatim, one session-URL footer, Clause-2 line at line start.",
      "open_questions": [
        {
          "question": "The card's recommendation 1 lists 'an unterminated string' among the unreadable tails. I split it: an unterminated TEMPLATE literal (and an unterminated delimited comment) stay on the CERTAIN side, because both legitimately span lines, so the call really is still open and every remaining byte really is inside the argument; quoted strings and mismatched closers stay on the unreadable side as recommended. Ratify or overturn.",
          "options": [
            "A - keep the split as landed: fires only where the scan cannot be certain; measured population of all four forms in tree and in history is 0, so no verdict moves today",
            "B - implement the card's list literally: also fire on an unterminated template literal and an unterminated delimited comment, accepting a future false positive on a multi-line template prescription, which is the #17955 shape"
          ],
          "recommendation": "A, because the round's own boundary acceptance is 'separate cannot-read from genuinely-still-open rather than treating both as firing', and a backtick prescription that spans lines is genuinely still open, not unreadable. It is named as a deviation in the PR body so the seat can overturn it in one line."
        }
      ],
      "out_of_scope_findings": [
        "noted, not filed: the gate file states its in-tree census as 254 tombstone key lines in ten further places, all the 1cb6a06195 reading; the tree now holds 255 (77 closing, was 76). 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.",
        "noted, not filed: two dispatch line numbers are each one past the actual return - the mismatched-closer return is at :1951 (:1952 is stack.pop()) and the fall-off-the-end return is at :1956 (:1957 closes the function); and the predicate is now named declaresUnwritableKey, not declaresRetiredKeyTombstone (renamed by #18560 and #18702 since PR #18427's head). The substance of the dispatch's reading was right and is what this PR implements.",
        "noted, not filed: `pnpm check:published-readme-exports` exits 3 in a fresh worktree - its own PREREQUISITE NOT MET code for an unbuilt dist across 46 packages, read as NOT MEASURED and not as a finding. Successor: any round that builds before running the roster families.",
        "noted, not filed: #18560, #17926 and #18629 (the inverse false-negative, still queued and undispatched) were not touched, per the read-only fence; none of their numbers is a closing target of PR 18920. #16943's replacement budget was not touched either.",
        "noted, not filed: the PR carries no needs:contract-review label - I neither applied nor removed it, and `check-clause2-carriers --pair 18920` exits 0 without it."
      ]
    }

    Generated by Claude Code

  4. added a commit that references this issue on Sep 28, 2026
    921eac0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions