Skip to content

feat(pm): report the closing-keyword contradiction on the squash-commit surface (H23) - #11117

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-10942-commit-message-surface
Aug 22, 2026
Merged

feat(pm): report the closing-keyword contradiction on the squash-commit surface (H23)#11117
os-zhuang merged 1 commit into
mainfrom
claude/issue-10942-commit-message-surface

Conversation

@claude

@claudeclaudeBot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Fixes#10942

Every closing-keyword reader in this repo is handed a PR body: h7PartOfWithClosingKeyword and h21NegatedClosingKeyword in scripts/pm/check-half-states.mjs, and the blocking gate scripts/check-partof-closing-keyword.mjs. GitHub's closing-keyword parser also acts on commit messages that land on the default branch, and this repo squash-merges — so every merged PR writes exactly one such message that nothing reads.

H23 is a report-only patrol row over that surface: a squash commit message carrying Part of #Nand a closing keyword bound to that same #N — the H7 contradiction shape, on a surface H7 cannot see.

⚠️Note on how card numbers are written below. This body is itself on a surface GitHub closes cards from, so no closing keyword is placed next to a card number anywhere in it; the specimen table keeps sha, card and keyword in separate columns on purpose. That is the same discipline H23's own finding sentence prescribes, and applying it here is the cheapest available demonstration.

The mechanism — why no body-side guard could have caught it

The squash message is composed at merge time from the branch's own commit messages, not from the PR body. All six measured specimens are multi-commit branches where one commit's trailer closed the card and another's said it was only part of it; the squash concatenated them and the contradiction was manufactured by the assembly.

Measured on the clearest one, PR #9478: its body carries a closing keyword bound to card 9320 and no Part of anywhere, so the body is clean under H7 and under the blocking gate — correctly, because the contradictory text existed in no body at all. This is why the row cannot be "H7 with a wider input": there is a text on main that no body ever held.

Measured — stage 1, this change's own pass

Corpus: all 1,546 first-parent commit messages on main in the pinned window 2026-08-11T00:00:00Z … 2026-08-22T18:00:00Z (first/last message 2026-08-11T01:21:16Z … 2026-08-22T17:59:02Z), read with the repo's own extractors.

readingresult
closing-keyword bindings270 across 234 messages
messages carrying the contradiction6
subjects ending with the squash marker (#PR)1,545 of 1,546
those markers read as a card binding0 of 1,545
main linearity1,975 reachable = 1,975 first-parent, 0 merge commits
H21's negation window ported to this surface0 of 270

The six, columns deliberately apart:

shacardkeywordPR
0c24898c010377past-tense fix form#10389
d7283250d10219past-tense fix form#10291
af2a989be9320past-tense fix form#9478
3db37957c8355past-tense fix form#8419
7e06f51ee8060past-tense fix form#8167
30536e37c7828past-tense fix form#8128

These reproduce the six the card body recorded, exactly. They are evidence that the shape reaches main unguarded, not six adjudicated wrong closes — in every one the lead commit's trailer looks deliberate — so the finding sentence reports the contradiction and explicitly declines to adjudicate it.

⚠️Measurement-harness note worth carrying forward:git log --since=2026-08-11 is an approxidate — git fills the unspecified time-of-day from now, so a bare date slides the corpus as the clock moves. Two runs twelve minutes apart returned 1,443 and 1,441 messages for what read as one window. Every number above is from ISO-pinned bounds, and the docblock says so.

The asymmetry (the card's point 2), and what it costs today

A commit message is not markdown: nothing renders it, so backticks and fences are ordinary characters and a quoted keyword binds exactly like one in prose. The extractors therefore gained one option — partOfTargets(text, { markdown: false }) / closingKeywordTargets(text, { markdown: false }) — the same shape stripMarkdownCode's own { inline } option takes. The default is unchanged, so every pre-existing caller keeps byte-identical output (pinned).

Honest accounting of what that buys today: on this corpus the two readings agree exactly — 270 bindings and 6 findings either way. It is not clean for lack of opportunity, which is the failure mode a zero-difference number invites:

  • 1,064 of 1,546 messages carry markdown-looking code (a squash body routinely quotes the PR body whole)
  • 487 of those carry a #N inside the code region
  • 361 carry a closing-keyword word inside the code region
  • 0 carry the two adjacent inside code — which is exactly why the readings agree

The population is everywhere; no author has yet landed the two adjacent inside a fence. The first who does is most likely the author following the body-surface remedy, which is the case the surface-correct reading catches and a stripped reading would silently drop.

The remedy text differs, and the self-test pins the difference both ways

H7 and H21 both end with "or put the keyword in backticks", which is correct for a body and false here. H23's sentence says so out loud and prescribes the only remedy that works on this surface — reword, so no closing keyword sits next to a card number. Because the realistic regression is someone copying H7's tail across, the self-test asserts H7's sentence carries that clause while H23's never does; a one-sided assertion would pass against a sentence that lost both.

Decisions taken, and why

  • H21's negation window is not ported. It flags 0 of the 270 bindings on this surface: commit messages carry no ## Out of scope register, because that register belongs to a PR body's prose sections. A port is a later card with its own numbers. (sentenceStartOffset was already written with this surface in mind — its docblock declines to treat a plain newline as a boundary precisely because commit messages hard-wrap — so the port stays available at no design cost.)
  • REST, not git log, for the live window. The patrol runner (.github/workflows/half-state-patrol.yml) uses actions/checkout@v7 with no fetch-depth, whose default is 1git log there would read one message and report a clean surface four times a day, forever. Not hypothetical about someone else's container: the checkout this change was authored in arrived shallow at 375 commits, and the corpus above only became readable after an explicit git fetch --deepen.
  • Three pages ≈ 300 commits ≈ 2.3 days, against a patrol firing every 6 hours — a ~9× overlap. No sha= parameter, so the endpoint uses the repository's own default branch and the reader stays repo-agnostic like every other listing in the file.
  • Report-only, and no workflow change. The existing patrol workflow already calls the sweep. A blocking posture for this surface is a later card on its own baseline, per the grading ruling.

Tests

node scripts/pm/check-half-states.mjs --self-test879 cases pass (was 838; +41), at 827971d1b.

The shipped predicate was then driven over the real corpus in the REST row shape the sweep passes in: it fires on exactly the six specimens, correlates each to the right PR, and reports 270 binding(s) across 234 message(s) — the coverage numbers the summary line now carries.

Reverse-verification — two legs, mutation proved on disk by marker counts, restore under an EXIT INT TERM trap, both directions predicted before running:

legmutationpredictedobserved
Adrop the surface asymmetry (read the commit surface as markdown)the two asymmetry cases turn red; the six specimen cases stay greenexactly that — 2 red of 879
Bcopy H7's body-surface remedy tail into H23's sentencethe three remedy pins turn redexactly that — 3 red of 879

Leg A's prediction is the interesting one and it held: the six specimens do not distinguish the two readings (the corpus says so — 6 either way), so the asymmetry cases are the only thing pinning the surface distinction. That is why they exist as separate cases rather than being folded into the specimens.

Restore proof: 0 ablation markers on disk, git status --porcelain empty on the file, self-test back to 879.

Gates

Re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs and no hand-fed paths (its own no-path mode, off the merge base), at 827971d1b — 10 families, all green, each quoted from the gate's own verdict line:

gateexitverdict line
check:pm-half-states0✓ check-half-states self-test: 879 cases pass.
check:partof-closing-keyword0✓ check-partof-closing-keyword self-test: 28 cases pass.
check-closing-keyword-parity0check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across 6438 tracked file(s), all registered).
check:entry-guard0✓ check:entry-guard: 136 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 94 export bindings, 84 of them inert on import
check:parse-guard0✓ check:parse-guard: 135 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
check:cross-package-test-inputs0OK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
check-ci-filter-parity0OK: all 86 declared cross-package glob(s) (74 unique) are covered by core or crosspkg…
check-cross-package-test-inputs0(same script as the check: family above)
check-partof-closing-keyword0(same script as the check: family above)
check-half-states (patrol entry)0(same script as the check: family above)

Exit codes were captured before any pipe (cmd > file 2>&1; EXIT=$?), never off a tail.

check-closing-keyword-parity is the one worth naming: it is the gate that would have caught a grammar drift between the three closing-keyword parsers, and this change deliberately adds no new grammar — one extractor read two ways, so the parser count and the registered-file sweep are unchanged.

Lint — a declared narrowing, not a skipped run. Repo-wide pnpm lint is CI's run; this pass linted the one changed file and proves the narrowing excluded nothing:

  1. Population from ESLint's own config, not a guess: new ESLint({}).isPathIgnored('scripts/pm/check-half-states.mjs')false, so the file is in the linted population.
  2. Count from --format json: 1 file linted, 0 errors, 0 warnings, exit 0.
  3. Invariance for untouched files: calculateConfigForFile(...) reports parserOptions.project === null, and eslint.config.mjs states in its own header that this repo "never enables type-aware linting … for ANY file". With no cross-file type program, a one-file diff cannot move any untouched file's verdict.

No changeset: scripts/pm/** only — nothing published. skip-changeset applied additively at PR creation.

Control-byte scan clean (grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' → no match, exit 1).


Generated by Claude Code

…it surface (H23)
Every closing-keyword reader in this repo is handed a PR body: H7 and H21 in
check-half-states.mjs, and the blocking gate check-partof-closing-keyword.mjs.
GitHub's parser also acts on commit messages landing on the default branch, and
this repo squash-merges, so each merged PR writes one such message that nothing
read.
H23 is a REPORT-ONLY patrol row over that surface. It reuses the existing
extractors at a new `markdown: false` surface option, because a commit message
is not markdown: quoting a keyword does not neutralise it there, so the finding
sentence prescribes rewording and never the body-surface remedy. The row states
its own coverage numbers in the summary line, and the bounded window is read
over REST because the patrol runner checks out shallow.
Measured over 1,546 first-parent messages on main (pinned window
2026-08-11T00:00Z to 2026-08-22T18:00Z): 270 keyword bindings across 234
messages, 6 carrying the contradiction, all 6 assembled by squash from a branch
that mixed the two trailer spellings.
Card: #10942
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 22, 2026
@os-zhuangClaude

Copy link
Copy Markdown
Contributor

PM 验收 — ACCEPT

复核:席位 session_01MsbKEG4LtERSLaDrbehM3e 全 diff 审毕(单文件 +436/−5)。定级边界逐项吻合:report-only(无 blocking、无 workflow 变更)、复用既有提取器(markdown: false 选项选而非严格度,默认路径字节不变且有 pin)、面不对称性在同一文本上双向钉死(提交面反引号是绑定/body 面不是,H23 与 H7 成对断言)、补救文案面正确且双向 pin(H7 保留反引号忠告、H23 永不携带——防"抄尾"回归)、量先行(窗口 ISO 钉死、270 绑定/6 矛盾、approxidate 陷阱记入 docblock)。设计判断俱佳:REST 而非 git log(巡逻 runner fetch-depth=1 会永远读一条报净——#4690 纯型)、H21 负向窗不移植(0/270 实测)、squash 标记与卡绑定的分隔性既测又 pin。自测 838→879,两腿消融先预测后观测(leg A 恰好证明六标本不区分两种读法、不对称 case 是唯一钉),恢复三重证。

发现卡#11118(H8 窗口论据 ~18/天 vs 实测 ~132/天)、#11119(squash 拼接生成器——作者侧惯例半)均合规立卡,进下轮集中定级。

落地类:scripts/pm 单文件,非治理面——本席 ready + auto-merge 布防,队列落地后剥 #10942 状态标。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 22, 2026 18:43
@os-zhuang
os-zhuang enabled auto-merge August 22, 2026 18:44
@os-zhuang
os-zhuang added this pull request to the merge queueAug 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32591905896 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 4.73s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 90 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 422f997Aug 22, 2026
33 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10942-commit-message-surface branch August 22, 2026 18:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-zhuang@claude