Uh oh!
There was an error while loading. Please reload this page.
feat(pm): report a closing keyword bound to a card the sentence says it is NOT closing (H21) - #10939
Merged
huangyiirene merged 1 commit intoAug 21, 2026
Conversation
…it is NOT closing (H21) H7's header states the rule generally — GitHub's parser matches the keyword plus the number and ignores the surrounding prose, negations included — but its predicate is bound to a `Part of #N` declaration and fires only when the same `#N` carries both. A body that declares `Part of` for nothing is silent by construction, however plainly it says the card stays open. That gap cost a card. PR #10241 carried no `Part of` anywhere, and a sentence in its `## Out of scope` section bound a past-tense closing keyword directly to card #10240; that card closed `completed` two seconds after the merge and read as finished until a human reopened it a day later. (This commit message states the specimen that way on purpose: a commit message is a closing-keyword surface too, and it is NOT markdown, so backticks would not have protected it. Quoting the sentence verbatim here would have closed the same card a second time — the first draft of this message did exactly that, caught by running this predicate over it.) H21 flags a closing keyword bound to a `#N` the body never declared itself part of, when the SENTENCE containing it carries a negation or filing marker. Disjoint from H7 by construction: a number already declared `Part of` is H7's row and is skipped here. Stage-1 corpus measurement, which the commissioning ruling made the precondition to any blocking posture — the 300 most recently merged PR bodies (2026-08-19T15:13:23Z..2026-08-21T19:11:30Z, 2,564,259 body chars): sentence window (shipped) 1 / 301 keyword matches — the specimen. 0 FP. paragraph window 1 / 301 whole body before match 14 / 301 — 13 false positives anywhere in body 301 / 301 — the naive shape The 13 whole-body false positives are one PR (#10714) legitimately closing fourteen cards, so the sentence bound is load-bearing rather than stylistic. The result is not clean for lack of opportunity: the same corpus carries 116 sentences in the deliberate-non-closure register ("## Out of scope", "filed, not repaired here", "#N is not addressed here") and only one author bound a closing keyword to the number. Second corpus, same answer: 1,418 squash commit messages on main (2026-08-11..21), 228 keyword matches, 0 flags. Report-only, and deliberately NOT wired into the blocking gate: scripts/check-partof-closing-keyword.mjs imports h7PartOfWithClosingKeyword and fails PRs on it, so H21 is a separate function that gate does not import — widening the class must not silently widen a check that fails builds. The scheduled patrol already calls this sweep, so no workflow edit is needed. Part of #10392
This was referenced Aug 21, 2026
huangyiirene
marked this pull request as ready for review
August 21, 2026 19:57
Contributor
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32521194622 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Uh oh!
There was an error while loading. Please reload this page.
huangyiirene
deleted the
claude/issue-10392-closing-keyword-negation-guard
branch
August 21, 2026 20:09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#10392
H7's header states the rule in fully general terms — GitHub's parser matches the keyword plus the number and ignores the surrounding prose entirely, negations and modals included. Its predicate is narrower than its own sentence: it is bound to aPart of #Ndeclaration and fires only when the same#Ncarries both. A body that declaresPart offor nothing is silent by construction, however plainly it says the card stays open.Premise verified on
origin/mainConfirmed before writing anything, on base
9d101d284:h7PartOfWithClosingKeywordreturnsnullwhenpartOfTargets(body).size === 0— the gap is exactly as the card describes, and the existing self-test already pins it (H7: plain \Fixes #N` with no Part of is out of scope`).2026-08-20T15:10:06Z, its body carries noPart ofanywhere, and the card it named closedcompletedat15:10:08Z— two seconds later. That card is nowstate: open, state_reason: reopened(triage reopened it), and itsclosed_by_pull_requestsstill names fix(service-storage): tombstone the prior file when an attachment re-points file_id #10241, so the closing link survives the reopen.Stage 1 — the corpus measurement, which the ruling made the precondition
Corpus A — the 300 most recently merged PR bodies into
main(2026-08-19T15:13:23Z…2026-08-21T19:11:30Z, 2,564,259 body characters, zero empty bodies), read throughstripMarkdownCodeexactly asH7reads. The specimen PR is inside this window, so the measurement has a known true positive to detect rather than only an absence to report.277 of the 300 bodies carry a closing keyword bound to a number. That is the quantitative reason the naive widening is unavailable: a rule over keyword presence would report every correct PR in the corpus.
The sentence bound is load-bearing, not stylistic. All 13 whole-body false positives are a single PR (#10714) that legitimately closes fourteen cards with a wall of closing lines and merely contains a negation word earlier in a long body. A body-scoped negation check would red-flag the most correct multi-close PR in the corpus thirteen times.
The clean result is not an artifact of the predicate being inert. The same corpus carries 116 sentences in exactly the deliberate-non-closure register this rule reads (
## Out of scope, "filed, not repaired here", "#N is not addressed here", "#N remains open"). The register is everywhere; only one author bound a closing keyword to the number. Two near-misses are the specimen's wording almost exactly and are correctly clean — #10876's## Out of scope — filed, not repaired hereand #10851'sfiled, not fixed here— neither binds a keyword to a number. Both are self-test fixtures.Corpus B — a second, different surface, same answer. All 1,418 squash commit messages on
main(2026-08-11…2026-08-21) — a surface GitHub's closing-keyword parser also reads — carry 228 keyword+#Nmatches across 197 commits and produce 0 flags. That arm contributes no true positive either: commit messages here do not carry the## Out of scoperegister at all, which is worth stating rather than letting a second zero read as extra confirmation.Per-marker, each of the eleven markers was also measured alone against both corpora; every one is independently clean, and
notandfiledare the two that fire on the specimen.Corpus provenance, declared.
ghis not installed in this container andapi.github.comis refused tocurland to node (the script's own transport note documents this container class). Corpus A came through themcp__github__*tooling, whose oversized results are written to disk verbatim, so the bodies were measured as bytes rather than retyped. Corpus B isgit logonorigin/main. Both corpora and both harnesses are reproducible; neither was sampled or filtered.Stage 2 — report-only, as commissioned
The rate is zero rather than "nonzero-but-low", so the ruling's wording would permit a blocking posture. This PR does not take one, for a reason the measurement itself surfaced:
scripts/check-partof-closing-keyword.mjsimportsh7PartOfWithClosingKeywordand fails PRs on it. ExtendingH7in place would therefore have silently promoted a brand-new rule to blocking on every PR in the repo — the opposite of the staged order, and through a file outside this card's declared surface.H21is deliberately a separate function that gate does not import, so widening the class cannot widen a check that fails builds.A promotion to blocking is left as its own decision with these numbers in hand. It needs
scripts/check-partof-closing-keyword.mjsin surface, and my recommendation is to wait for a wider window than 2.2 days before making a check that can hard-red every PR.No workflow edit is part of this.
.github/workflows/half-state-patrol.ymlalready runs the whole sweep on a schedule, so a new row is picked up with no wiring..github/workflows/partof-closing-keyword-guard.yml— the one file this card conditionally allowed — is untouched, because touching it would mean widening the blocking gate.The rule
H21flags a closing keyword bound to a#Nthe body never declared itself part of, when the sentence containing it carries a negation or filing marker (not,n't/cannot,never,no longer,filed/filing,out of scope,rather than,instead of,without,remains open). Disjoint fromH7by construction: a number already declaredPart ofisH7's row and is skipped here, so the two never double-report one number.The bare noun
file/filesis deliberately not a marker although it too measured clean — it carries no negation sense ("this file fixes a bug" is a normal close) and is among the most common nouns in this repo's prose, so it is the marker most likely to become a false positive on a corpus this one did not sample.The window treats sentence punctuation, a blank line and a markdown structural line start as boundaries, but not a plain newline: PR bodies are soft-wrapped while this repo's commit messages are hard-wrapped at ~72 columns, so a newline boundary would have blinded the window on Corpus B specifically.
This PR reproduced the defect on itself, and the guard caught it
The first draft of the commit message quoted the specimen sentence verbatim. A commit message is a closing-keyword surface too, and it is not markdown, so backticks would not have protected it — merging would have closed the reopened card a second time. Running this predicate over the draft message caught it, and the sentence was reworded rather than decorated. The commit message says so, and it is the sharpest available evidence that the advice "keep closing keywords away from other card numbers" is not something a careful author reliably gets right.
Verification
node scripts/pm/check-half-states.mjs --self-test→✓ check-half-states self-test: 774 cases pass.(36 new cases; the positive fixture is the specimen sentence byte-for-byte, the negatives are real sentences from the measured corpus.)The shipped predicate was re-run over both corpora to confirm the numbers above describe the code that shipped and not the exploratory harness — 300 bodies / 277 with a keyword / 301 matches / 1 finding, and 1,418 commit messages / 228 matches / 0 findings.
Gate union —
node scripts/pm/dispatch-gates.mjs(no path arguments; change set re-derived from the merge base) on a clean worktree at7c3378fe4, the final commit. It printed 10 entries, which collapse to 7 distinct checks (three appear twice, once as apnpmalias and once as thenodeinvocation;check-half-states.mjsappears as both its self-test and its live patrol form). Exit codes captured before any pipe; each quoted from the gate's own verdict line. All green:check:pm-half-states—✓ check-half-states self-test: 774 cases pass.check:partof-closing-keyword—✓ check-partof-closing-keyword self-test: 28 cases pass.(the blocking gate, unchanged and still green)check:closing-keyword-parity—OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across 6317 tracked file(s), all registered).—H21reuses the existingclosingKeywordRe()rather than adding a fourth copy of the grammar, so this stays at 5 registered filescheck:entry-guard—✓ check:entry-guard: 131 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 89 export bindings, 79 of them inert on import (10 known-unsafe, ⛔ SHRINK-ONLY).check:parse-guard—✓ check:parse-guard: 130 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.check:ci-filter-parity—OK: all 82 declared cross-package glob(s) (71 unique) are covered by \core` or `crosspkg` …`check:cross-package-test-inputs—OK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.The one entry with no local verdict is
node scripts/pm/check-half-states.mjsunderhalf-state-patrol.yml— the live sweep, which cannot run in this container (GitHub is MCP-only here, the exact container class the script's own transport note documents). Its offline form is the self-test above.Outside the derived union, the edited file was scanned directly for raw control bytes with the exit code captured before the pipe (
grep_exit=1— no matches).check:nul-byteswas not in the derived union and is not claimed as one of the families above; the scan was run anyway because this change is entirely prose about control-adjacent parsing.No changeset:
scripts/pm/**is internal PM tooling and publishes nothing. Precedent is unanimous — the last 12 commits touchingscripts/pm/carry zero changesets — so this follows the repo's convention rather than inventing one.Out of scope
Two pre-existing findings name this same file (#10688, #10468). Neither is touched here, and
H21was added as a new section rather than by restructuringH7, so it should not collide with their future fixes.One new observation was filed rather than folded in — see the report comment on the card.
Generated by Claude Code