Skip to content

feat(pm): report a closing keyword bound to a card the sentence says it is NOT closing (H21) - #10939

Merged
huangyiirene merged 1 commit into
mainfrom
claude/issue-10392-closing-keyword-negation-guard
Aug 21, 2026
Merged

feat(pm): report a closing keyword bound to a card the sentence says it is NOT closing (H21)#10939
huangyiirene merged 1 commit into
mainfrom
claude/issue-10392-closing-keyword-negation-guard

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

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 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.

Premise verified on origin/main

Confirmed before writing anything, on base 9d101d284:

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:23Z2026-08-21T19:11:30Z, 2,564,259 body characters, zero empty bodies), read through stripMarkdownCode exactly as H7 reads. The specimen PR is inside this window, so the measurement has a known true positive to detect rather than only an absence to report.

window scopeflagged / 301 keyword matchesfalse positives
sentence (shipped)1 — PR #10241, the specimen0
paragraph1 — same single hit0
whole body before the match1413
anywhere in body301300

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 here and #10851's filed, 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-112026-08-21) — a surface GitHub's closing-keyword parser also reads — carry 228 keyword+#N matches across 197 commits and produce 0 flags. That arm contributes no true positive either: commit messages here do not carry the ## Out of scope register 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 not and filed are the two that fire on the specimen.

Corpus provenance, declared.gh is not installed in this container and api.github.com is refused to curl and to node (the script's own transport note documents this container class). Corpus A came through the mcp__github__* tooling, whose oversized results are written to disk verbatim, so the bodies were measured as bytes rather than retyped. Corpus B is git log on origin/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.mjs imports h7PartOfWithClosingKeyword and fails PRs on it. Extending H7 in 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. H21 is 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.mjs in 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.yml already 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

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 (not, n't/cannot, never, no longer, filed/filing, out of scope, rather than, instead of, without, remains open). Disjoint from H7 by construction: a number already declared Part of is H7's row and is skipped here, so the two never double-report one number.

The bare noun file/files is 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 unionnode scripts/pm/dispatch-gates.mjs (no path arguments; change set re-derived from the merge base) on a clean worktree at 7c3378fe4, the final commit. It printed 10 entries, which collapse to 7 distinct checks (three appear twice, once as a pnpm alias and once as the node invocation; check-half-states.mjs appears 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-parityOK (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).H21 reuses the existing closingKeywordRe() rather than adding a fourth copy of the grammar, so this stays at 5 registered files
  • check: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-parityOK: all 82 declared cross-package glob(s) (71 unique) are covered by \core` or `crosspkg` …`
  • check:cross-package-test-inputsOK: 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.mjs under half-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-bytes was 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 touching scripts/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 H21 was added as a new section rather than by restructuring H7, 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

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

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

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

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

    ✗ Build failed in 5.71s
    

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

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

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

历史信号:

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

分诊清单:

  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 376b3dcAug 21, 2026
33 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-10392-closing-keyword-negation-guard branch August 21, 2026 20:09
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

Development

Successfully merging this pull request may close these issues.

A "filed, not fixed" sentence silently closed a real card — partof-closing-keyword-guard's trigger is narrower than its own rationale

2 participants

@huangyiirene@claude