From 827971d1b92c053adc5dc79180984abe2784fda3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 18:33:47 +0000 Subject: [PATCH] feat(pm): report the closing-keyword contradiction on the squash-commit 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 --- scripts/pm/check-half-states.mjs | 441 ++++++++++++++++++++++++++++++- 1 file changed, 436 insertions(+), 5 deletions(-) diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index 1c30e4e39f..f027b605cb 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -407,6 +407,33 @@ * residue. Recent residue is a live duty; the deep tail is a backfill * question. Report-only like the rest — the remedy is a label write a * seat performs, never one this script performs. + * H23 a SQUASH COMMIT MESSAGE on the default branch carrying `Part of #N` + * and a closing keyword bound to that same `#N` — H7's contradiction on + * the SECOND surface GitHub closes cards from, and the only item here + * that reads commits (#10942). Every closing-keyword reader this repo + * owns takes a PR body: H7 and H21 above, and the blocking gate + * `scripts/check-partof-closing-keyword.mjs`. GitHub's 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 read. ⭐ The message is COMPOSED AT MERGE TIME from the + * branch's own commit messages, not from the PR body — so the + * contradiction can exist on `main` while every body was clean, and a + * body-side guard is not merely looking in the wrong place, it is + * looking at a text that never contained it (measured on PR #9478: body + * clean under H7 and under the gate, squash message carrying both). + * Measured 2026-08-22 over all 1,546 first-parent messages in the pinned + * window 2026-08-11T00:00Z…08-22T18:00Z: 270 bindings across 234 + * messages, 6 carrying the contradiction, all 6 multi-commit branches + * whose squash concatenated a `Part of` trailer and a closing trailer. + * ⛔ The extractors run at `markdown: false` here and MUST: a commit + * message is not markdown, so backticks do not neutralise a keyword, and + * the finding sentence therefore prescribes REWORDING and never H7's + * backtick remedy — an author who has internalised the body remedy is + * exactly who will misapply it here. H21's negation window is + * deliberately NOT ported: it flags 0 of the 270 on this surface, because commit messages carry no + * `## Out of scope` register. Report-only and measure-first by ruling + * (2026-08-22); a blocking posture for this surface is a later card on + * its own baseline. * * ## The close mechanism, measured (#8293) * @@ -1120,15 +1147,42 @@ export function stripMarkdownCode(body, { inline = true } = {}) { return out.join('\n'); } +/** + * ## The `{ markdown: false }` half — the SURFACE the text came from (H23) + * + * Both extractors below take one option, and it selects a *surface* rather than + * a strictness. `markdown: true` (the default, and every pre-existing caller) + * means the text is a PR or issue BODY, where GitHub renders markdown before its + * reference parser runs and a keyword inside a code span or fence therefore does + * NOT fire — measured, twice, in `stripMarkdownCode`'s docblock. + * + * `markdown: false` means the text is a COMMIT MESSAGE. GitHub's closing-keyword + * parser reads commit messages on the default branch too, and a commit message + * is not markdown: there is no renderer in front of it, so backticks and fences + * are ordinary characters and a keyword sitting inside them binds exactly like + * one in plain prose. Stripping there would delete real bindings and hand the + * author a remedy that does not work — H23's whole point, argued at length in + * its section. + * + * ⛔ The asymmetry is not a tuning knob and must not be "unified" later: the two + * surfaces genuinely differ in GitHub's own behavior, so a single reading is + * wrong for one of them whichever way it is set. One extractor read two ways, + * the same shape `stripMarkdownCode`'s own `{ inline }` option takes, so the two + * surfaces can never drift onto two different grammars. The default is + * unchanged, so every existing caller keeps byte-identical output. + */ + /** The `#N` a body declares itself only PART of. */ -export function partOfTargets(body) { - return new Set([...stripMarkdownCode(body).matchAll(partOfRe())].map((m) => m[1])); +export function partOfTargets(body, { markdown = true } = {}) { + const text = markdown ? stripMarkdownCode(body) : String(body ?? ''); + return new Set([...text.matchAll(partOfRe())].map((m) => m[1])); } /** `#N` -> the closing keyword bound to it (first occurrence wins, for the message). */ -export function closingKeywordTargets(body) { +export function closingKeywordTargets(body, { markdown = true } = {}) { const found = new Map(); - for (const m of stripMarkdownCode(body).matchAll(closingKeywordRe())) { + const text = markdown ? stripMarkdownCode(body) : String(body ?? ''); + for (const m of text.matchAll(closingKeywordRe())) { if (!found.has(m[2])) found.set(m[2], m[1]); } return found; @@ -3336,6 +3390,173 @@ export function h22ClosedCardPmResidue(issue) { ); } +// --------------------------------------------------------------------------- +// H23 — the SECOND SURFACE: a squash commit message on the default branch that +// carries `Part of #N` and a closing keyword bound to that same `#N` (#10942). +// +// ## The gap +// +// Every closing-keyword reader this repo owns is handed a PULL REQUEST BODY. +// H7 and H21 above take a `pr` and read `pr.body`; +// `scripts/check-partof-closing-keyword.mjs` — the blocking gate — is handed +// `PR_BODY` by `.github/workflows/partof-closing-keyword-guard.yml`. GitHub's +// closing-keyword parser acts on TWO surfaces: the PR body, and the COMMIT +// MESSAGES of commits that land on the default branch. This repo squash-merges, +// so every merged PR contributes exactly one commit message to `main`, and that +// message was never read by anything here. +// +// ## The mechanism, and why a body-side guard could not 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 specimens below are multi-commit +// branches where one commit's trailer said `Fixes #N` and another's said +// `Part of #N`; 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 #9320 and no `Part of` anywhere, so +// the body is clean under H7 and under the blocking gate, and correctly so — +// 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 (2026-08-22, this change's own stage-1 pass) +// +// Corpus: all 1,546 first-parent commit messages on `main` in the window +// 2026-08-11T00:00:00Z … 2026-08-22T18:00:00Z (first and last message +// 2026-08-11T01:21:16Z … 2026-08-22T17:59:02Z), read with the extractors above at +// `markdown: false`. `main` is LINEAR — 1,975 commits reachable, 1,975 on the +// first-parent walk, 0 merge commits — so the commit list and the squash-message +// list are the same list, and the REST reader below needs no first-parent filter. +// +// ⚠️ The window bounds are spelled as full ISO instants on purpose. `git log +// --since=2026-08-11` is an APPROXIDATE: git fills the unspecified time-of-day +// from *now*, so a bare date silently slides the corpus forward as the clock +// moves — two runs of this measurement twelve minutes apart returned 1,443 and +// 1,441 messages for what read as one window. Anyone re-deriving these numbers +// must pin both instants or they are measuring a different corpus. +// +// 270 closing-keyword bindings across 234 messages +// 6 messages carry `Part of #N` AND a keyword bound to that same `#N` +// sha card keyword (columns kept apart on purpose — see +// 0c24898c0 10377 Fixes the remedy note below; this file +// d7283250d 10219 Fixes must not itself put a keyword next +// af2a989be 9320 Fixes to a live card number) +// 3db37957c 8355 Fixes +// 7e06f51ee 8060 Fixes +// 30536e37c 7828 Fixes +// 1,545 of 1,546 subjects end with the squash marker `(#PR)` +// 0 of those 1,545 trailing markers are bound as a card by the extractor +// +// That last number is the PR-correlation guarantee, and it is measured rather +// than argued: the separator in `closingKeywordRe` is horizontal whitespace and +// an optional colon, so the `(` in `… (#11085)` stands between any preceding +// keyword and the number and no subject's own PR marker can ever be read as a +// card binding. `commitSubjectPrNumber` reads it as what it is instead. +// +// ⚠️ Six rows are evidence that the shape REACHES `main` unguarded, not six +// adjudicated wrong closes. In every one the lead commit's `Fixes #N` looks +// deliberate, so the finding sentence reports the contradiction and explicitly +// declines to adjudicate it — the reader checks the card. Deciding whether this +// class ever earns a BLOCKING posture is a later card on its own baseline +// (grading ruling, 2026-08-22): this row exists to measure the surface first, +// exactly the posture #10392 was required to take for the body surface. +// +// ## The remedy text differs from H7's and H21's, and that is the point +// +// H7 and H21 both end with "or put the keyword in backticks", which is CORRECT +// for a body and FALSE here: a commit message is not markdown, nothing renders +// it, and backticks are ordinary characters to the parser. An author who has +// internalised the body remedy is precisely the author who will reach for it on +// this surface, so the sentence says out loud that it does not work here and +// gives the only remedy that does — reword, so no closing keyword sits next to +// the number. The self-test pins the difference in BOTH directions (H7's +// sentence carries the backtick clause; this one must never carry it), because +// the realistic regression is someone copying H7's tail across. +// +// ## Why H21's negation window is NOT ported to this surface +// +// Measured, on the same 1,546-message corpus: H21's window and marker set flag +// 0 of the 270 bindings. Commit messages here do not carry the deliberate- +// non-closure register at all — no `## Out of scope`, no "filed, not fixed +// here" — because that register belongs to a PR body's prose sections. Porting +// it would add a second predicate over this surface with zero measured yield +// and its own false-positive risk, so it stays out until something measures a +// reason for it. (`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 at ~72 columns — so the port is available to +// a later card at no design cost.) +// +// ## And why stripping is not merely "harmless to skip" +// +// On this corpus the two readings agree exactly — 270 bindings either way, and +// the same 6 findings — so the asymmetry buys no finding today, and saying +// otherwise would be a claim the measurement does not support. It is not clean +// for lack of opportunity, which is the failure mode a 0-difference number +// invites: 1,064 of the 1,546 messages DO carry markdown-looking code (a squash +// body routinely quotes the PR body whole), 487 of those carry a `#N` inside the +// code region and 361 carry a closing-keyword word inside it. But not one +// carries a keyword and a number ADJACENT inside code — which is why the two +// readings agree. The population is everywhere; no author has +// yet landed the two adjacent inside a fence. The first who does — most likely +// the author following the body-surface remedy — is the case the surface-correct +// reading catches and a stripped reading would silently drop. +// --------------------------------------------------------------------------- + +/** + * The squash marker a commit SUBJECT ends with — `(#PR)` — or null. + * + * Read off the first line only, and anchored to its end: that is where the + * merge writes it, and a `(#123)` in the message BODY is quoted prose from + * somewhere else, not this commit's delivery. Measured at 1,545 of 1,546 + * subjects in the corpus above, which is why the correlation is worth having + * for free rather than through a per-commit `/pulls` request. + */ +export function commitSubjectPrNumber(message) { + const subject = String(message ?? '').split('\n', 1)[0]; + const m = /\(#(\d+)\)\s*$/.exec(subject.trim()); + return m ? m[1] : null; +} + +/** + * H23 — null when the commit message is clean, else the finding sentence. + * + * Pure over the REST commit row (`{ sha, html_url, commit: { message } }`), like + * every predicate here, so the self-test drives it with the real specimens. + * + * The extractors are H7's, at `markdown: false` — same functions, different + * surface. Bound PER CARD NUMBER exactly as H7 binds: a message that is + * `Part of #A` and separately closes #B is the normal correct shape and stays + * clean. + */ +export function h23CommitMessageContradiction(commit) { + const message = commit?.commit?.message ?? ''; + const declared = partOfTargets(message, { markdown: false }); + if (declared.size === 0) return null; + const closing = closingKeywordTargets(message, { markdown: false }); + const clashes = [...declared].filter((n) => closing.has(n)); + if (clashes.length === 0) return null; + + const sha = String(commit?.sha ?? '').slice(0, 9) || '(unknown sha)'; + const pr = commitSubjectPrNumber(message); + const via = pr ? ` (landed by PR #${pr})` : ''; + const cards = clashes.map((n) => `#${n}`).join(', '); + const pairs = clashes.map((n) => `\`Part of #${n}\` and \`${closing.get(n)}\` bound to #${n}`).join('; '); + + return ( + `squash commit \`${sha}\`${via} carries BOTH instructions in one message — ${pairs}. ` + + `GitHub's closing-keyword parser reads commit messages on the default branch exactly as it ` + + `reads a PR body, and ignores the surrounding prose, so this message already told GitHub to ` + + `close ${cards} when it landed. No body-side guard could have seen it: the squash message is ` + + `composed at merge time from the branch's own commit messages, so the contradiction need never ` + + `have existed in any body (measured on PR #9478, whose body is clean under H7 and under the ` + + `blocking gate). Re-read ${cards} and judge its state deliberately — this row reports the ` + + `contradiction and does NOT adjudicate whether the close was intended. ` + + `⚠️ For the next message: a commit message is NOT markdown — nothing renders it, so backticks ` + + `and fences are ordinary characters here and quoting the keyword does not neutralise it. That ` + + `is the PR-BODY remedy (H7's and H21's sentences end with it, correctly, for bodies) and it is ` + + `false on this surface. The only fix here is to REWORD, so that no closing keyword sits next to ` + + `a card number.` + ); +} + // --------------------------------------------------------------------------- // Report rendering — pure over (findings, counts), so `--self-test` pins both // media offline. The live sweep below picks a renderer and prints it; nothing @@ -3389,7 +3610,8 @@ export function isLoudFinding(message) { * holdProbed?: number, holdCandidates?: number, fallbackProbed?: number, * fallbackCandidates?: number, restartProbed?: number, * restartCandidates?: number, blockerResolved?: number, - * blockerTargets?: number }} counts + * blockerTargets?: number, commits?: number, commitBindings?: number, + * commitBindingMessages?: number }} counts * @param {number} findingCount */ export function summaryLine(counts, findingCount) { @@ -3425,6 +3647,16 @@ export function summaryLine(counts, findingCount) { // all must not read the same as a board where every dispatch is live (#4690). const refRead = counts.dispatchRefRead ?? 0; const refTargets = counts.dispatchRefTargets ?? 0; + // H23's coverage numbers. Not a `read X of Y` pair — nothing here can fail per + // row — but the same duty in the measure-first register the row was + // commissioned in (#10942): the row's yield is ~6 in 1,546, so a quiet H23 is + // the normal reading and the ONLY thing separating "this surface was read and + // is clean" from "no commit message was read at all" is these counts. The + // binding totals ride along because they are what a later blocking-promotion + // decision needs and they cost nothing to carry. + const commits = counts.commits ?? 0; + const commitBindings = counts.commitBindings ?? 0; + const commitBindingMessages = counts.commitBindingMessages ?? 0; return ( `check-half-states: swept ${counts.issues} open pm-/p0-labeled issue(s), ${counts.unscoped} open ` + `issue(s) in the unscoped pass (H13–H15, H18), ${counts.prs} open PR(s) ` + @@ -3432,6 +3664,9 @@ export function summaryLine(counts, findingCount) { `and ${counts.merged} recently-merged PR(s) in ${counts.repo} — ${findingCount} half-state(s) found. ` + `H22 read ${counts.closed ?? 0} recently-closed issue(s) for \`pm:*\` state residue (bounded window; ` + `older closed carriers are outside it by design). ` + + `H23 read ${commits} squash commit message(s) from the default branch's recent window, carrying ` + + `${commitBindings} closing-keyword binding(s) across ${commitBindingMessages} message(s) ` + + `(bounded window; a message that landed before it is invisible by design). ` + `Hold comments read on ${held} of ${holdCandidates} H17 candidate(s). ` + `\`Blocked-by:\` comment fallback read on ${fbProbed} of ${fbCandidates} candidate(s)` + `${fbProbed < fbCandidates ? " — H14's stale direction is SUSPENDED for this sweep (the index is known incomplete)" : ''}. ` + @@ -4228,6 +4463,13 @@ async function sweep(options = {}) { // got a definite open/closed answer. blockerTargets: 0, blockerResolved: 0, + // H23's coverage numbers (#10942) — how many commit messages this pass read + // and how much closing-keyword traffic they carry. Initialised to 0 here + // rather than left absent so a sweep that throws before the commit pass + // still renders numbers instead of the string `undefined`. + commits: 0, + commitBindings: 0, + commitBindingMessages: 0, }; // H17's gathering rides out of the sweep the same way, because it has the // same per-row failure mode as H16's detail pass and therefore owes the @@ -4258,6 +4500,9 @@ async function sweep(options = {}) { restartProbed: stats.restartProbed, blockerTargets: stats.blockerTargets, blockerResolved: stats.blockerResolved, + commits: stats.commits, + commitBindings: stats.commitBindings, + commitBindingMessages: stats.commitBindingMessages, }; // The oracle is read ONCE per sweep, after gathering: it is a local // `git ls-files`, not a request, and every candidate token is checked @@ -4334,6 +4579,56 @@ async function listRecentlyClosedIssues() { return out; } +/** + * H23's bounded commit window (#10942) — the most recent commits on the + * repository's DEFAULT BRANCH, capped at three pages, the same "bounded window, + * stated boundary" discipline as the two windows above. + * + * ## Why REST and not `git log`, in a file that already shells out to git + * + * `readTrackedFiles` proves a git channel exists here, and a `git log` read + * would cost no API quota at all — so the choice needs a reason. It is the + * runner: `.github/workflows/half-state-patrol.yml` checks out with + * `actions/checkout@v7` and no `fetch-depth`, whose default is **1**. `git log` + * there would read exactly ONE commit message and report a clean surface, which + * is #4690 in its purest form — an unread input rendering as a clean one, four + * times a day, forever. This is not a hypothetical about someone else's + * container: the checkout this change was authored in arrived shallow at 375 + * commits, and the 11-day corpus in H23's section only became readable after an + * explicit `git fetch --deepen`. A channel that is dark in the one place the + * sweep actually runs is not a cheaper channel. + * + * ## The page cap, in the units that decide it + * + * Measured over the corpus above: 1,546 commits in 11.7 days ≈ 132/day. Three + * pages ≈ 300 commits ≈ 2.3 days, against a patrol that fires every 6 hours — + * roughly a 9× overlap, so a message has to survive nine consecutive sweeps to + * age out unseen. (The `~18 merges/day` figure in `listRecentlyMergedPullRequests` + * predates that acceleration; nothing here depends on it, but a future reader + * re-deriving a window from it should re-measure first.) + * + * No `sha=` parameter: the endpoint defaults to the repository's own default + * branch, which keeps this reader repo-agnostic exactly like every other listing + * here — `PM_SWEEP_REPO` can name a repo whose default branch is not `main`. + * Ordering is the endpoint's own reverse-chronological walk of that branch and + * needs no first-parent filter here: `main` is linear (measured — 1,975 commits + * reachable, 1,975 on the first-parent walk, 0 merge commits), so the commit list + * and the squash-message list are the same list. A repo that DOES carry merge + * commits would simply feed this row a few branch-side messages, which are a + * surface GitHub's parser reads too — wider, never wrong. + */ +const COMMIT_WINDOW_PAGES = 3; + +async function listRecentDefaultBranchCommits() { + const out = []; + for (let page = 1; page <= COMMIT_WINDOW_PAGES; page++) { + const batch = await rest(`/repos/${OWNER_REPO}/commits?per_page=100&page=${page}`); + out.push(...batch); + if (batch.length < 100) break; + } + return out; +} + /** * The unscoped listing H13 needs: the domain-without-pm-state shape is * DEFINED by the absence of every label the listings below key on, so no @@ -4582,6 +4877,37 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen if (residue) findings.push([issue, 'H22', residue]); } + // H23 — the commit-message surface (#10942). The counting is not incidental: + // this row's measured yield is ~6 in 1,546, so a silent H23 is the normal + // reading, and the summary line's coverage numbers are the only thing that + // separates "read and clean" from "no message was read". They are gathered in + // the same walk as the verdicts so the two can never disagree. + // + // The finding row is keyed to the PR the squash marker names, because that is + // the artifact a reader searches for and it keeps this row shaped like the + // other PR-scoped rows (H7, H12, H16, H21). The LINK is the commit, which is + // the evidence. When a subject carries no marker (1 of 1,546 measured) the + // first contradicted card number stands in, so a row is never dropped for + // want of a number to sort by. + for (const commit of await listRecentDefaultBranchCommits()) { + const message = commit?.commit?.message ?? ''; + stats.commits = (stats.commits ?? 0) + 1; + const bindings = closingKeywordTargets(message, { markdown: false }); + if (bindings.size > 0) { + stats.commitBindings = (stats.commitBindings ?? 0) + bindings.size; + stats.commitBindingMessages = (stats.commitBindingMessages ?? 0) + 1; + } + const contradiction = h23CommitMessageContradiction(commit); + if (!contradiction) continue; + const pr = commitSubjectPrNumber(message); + const fallback = [...partOfTargets(message, { markdown: false })].find((n) => bindings.has(n)); + findings.push([ + { number: Number(pr ?? fallback ?? 0), html_url: commit?.html_url ?? '' }, + 'H23', + contradiction, + ]); + } + // H13 — the one item whose population no label page can list (note at // `listAllOpenIssues`). Kept out of `seen` so H1–H12 keep their exact // inputs and the summary line stays honest about what each pass covered; @@ -5506,6 +5832,111 @@ function selfTest() { t('summary: the H22 clause states what the closed pass read', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, closed: 200 }, 0).includes('H22 read 200 recently-closed issue(s)'), true); t('summary: an absent closed count degrades to 0, never to undefined', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0 }, 0).includes('H22 read 0 recently-closed'), true); + // -- H23: the COMMIT-MESSAGE surface (#10942) ------------------------------- + // + // Every case here drives the REST commit shape the sweep passes in, and the + // asymmetry cases are run against H7 in the same breath: the claim is not + // "this predicate fires", it is "these two surfaces answer DIFFERENTLY on one + // text", and only the paired assertion can say that. + const commitRow = (message, sha = 'abc123def0') => ({ + sha, + html_url: `https://github.com/o/r/commit/${sha}`, + commit: { message }, + }); + /** + * The shape the squash actually produced in all six measured specimens: a + * lead commit whose trailer CLOSES the card, and a later commit on the same + * branch whose trailer says it is only `Part of` it — concatenated by the + * merge into one message. Reconstructed rather than quoted: the self-test is + * offline and pure, so it pins the SHAPE the corpus measured, not the bytes. + */ + const squashOf = (card, pr, keyword = 'Fixes') => + commitRow( + `fix(scope): the lead half (#${pr})\n\n` + + `* fix(scope): the lead half\n\nProse about the fix.\n\n${keyword} #${card}\n\n` + + `* test(scope): the second half\n\nMore prose.\n\nPart of #${card}\n\n---------\n\n` + + `Co-authored-by: Claude \n`, + `sha${pr}xx`, + ); + + // The six specimens the card measured, by (sha, card, PR). Columns kept apart + // deliberately, here and in the section docblock: this file must not put a + // closing keyword next to a live card number in any text a merge could read. + for (const [sha, card, pr] of [ + ['0c24898c0', '10377', '10389'], + ['d7283250d', '10219', '10291'], + ['af2a989be', '9320', '9478'], + ['3db37957c', '8355', '8419'], + ['7e06f51ee', '8060', '8167'], + ['30536e37c', '7828', '8128'], + ]) { + t(`H23: the measured specimen shape (${sha}, card ${card}) fires`, typeof h23CommitMessageContradiction(squashOf(card, pr)), 'string'); + } + + // ⛔ THE ASYMMETRY — the card's point 2, pinned in both directions on ONE text. + // A commit message is not markdown, so a quoted keyword binds here; the same + // bytes in a PR body do not, and H7 must keep saying so. + const backticked = 'Part of #77\n\nThe blocking gate wanted `Fixes #77` here.\n'; + t('H23: a backticked keyword in a COMMIT MESSAGE is a binding', typeof h23CommitMessageContradiction(commitRow(backticked)), 'string'); + t('H23: …while the same bytes in a PR BODY are not (H7 stays clean)', h7PartOfWithClosingKeyword({ body: backticked }), null); + const fencedCommit = 'Part of #77\n\n```\nFixes #77\n```\n'; + t('H23: a FENCED keyword in a commit message is a binding too', typeof h23CommitMessageContradiction(commitRow(fencedCommit)), 'string'); + t('H23: …and the same bytes in a PR body are still not (H7 stays clean)', h7PartOfWithClosingKeyword({ body: fencedCommit }), null); + // The extractors themselves, at the two surfaces — the option is the whole + // mechanism, so it is pinned directly and not only through the verdicts. + t('extractor: a quoted keyword is invisible on the BODY surface (default)', closingKeywordTargets('a `Fixes #1` b').size, 0); + t('extractor: …and visible on the COMMIT surface', closingKeywordTargets('a `Fixes #1` b', { markdown: false }).size, 1); + t('extractor: `Part of` in a fence is invisible on the body surface', partOfTargets('```\nPart of #1\n```').size, 0); + t('extractor: …and visible on the commit surface', partOfTargets('```\nPart of #1\n```', { markdown: false }).size, 1); + t('extractor: the default is byte-identical to the pre-option reading', closingKeywordTargets('Fixes #1').get('1'), 'Fixes'); + + // The REMEDY TEXT. The realistic regression is someone copying H7's tail + // across, so H7's own sentence is asserted to CARRY the clause this one must + // never carry — a one-sided assertion would pass against a sentence that lost + // both. + const fired23 = h23CommitMessageContradiction(squashOf('9320', '9478')); + const fired7 = h7PartOfWithClosingKeyword({ body: 'Part of #77\n\nFixes #77' }); + t('H23: the sentence prescribes REWORDING', fired23.includes('REWORD'), true); + t('H23: …and never the body-surface backtick remedy', fired23.includes('put the keyword in backticks'), false); + t('H23: …nor any "in backticks" advice at all', fired23.includes('in backticks'), false); + t('H23: …and says out loud that this surface is not markdown', fired23.includes('NOT markdown'), true); + t('H7: …while H7 KEEPS that remedy, which is correct for a body', fired7.includes('put the keyword in backticks'), true); + t('H23: the sentence names the commit sha', fired23.includes('sha9478xx'), true); + t('H23: …the bound card', fired23.includes('#9320'), true); + t('H23: …and the PR the squash marker names', fired23.includes('PR #9478'), true); + t('H23: …and it declines to adjudicate the close', fired23.includes('does NOT adjudicate'), true); + + // Clean directions. H7's per-number binding carries over unchanged: a message + // that is part of one card and closes another is the normal correct shape. + t('H23: `Part of #A` + a keyword bound to #B -> clean', h23CommitMessageContradiction(commitRow('Part of #77\n\nFixes #88')), null); + t('H23: a plain closing trailer with no `Part of` -> clean', h23CommitMessageContradiction(commitRow('fix(x): a fix (#99)\n\nFixes #77')), null); + t('H23: `Part of` alone -> clean', h23CommitMessageContradiction(commitRow('Part of #77')), null); + t('H23: gerunds are not closing keywords here either', h23CommitMessageContradiction(commitRow('Part of #77\n\nStill fixing #77.')), null); + t('H23: empty / missing message', h23CommitMessageContradiction(commitRow(undefined)), null); + t('H23: a missing commit object', h23CommitMessageContradiction(undefined), null); + // H21's negation window is deliberately NOT ported to this surface: it flags 0 + // of the 270 measured bindings, so this row is the `Part of` contradiction and + // nothing else. A future port is a card with its own numbers. + t('H23: a negated bare close is NOT this row (H21 not ported)', h23CommitMessageContradiction(commitRow('This does not fix #77.')), null); + + // The PR correlation, and the measured property that makes it safe: the `(` + // of the squash marker stands between any preceding keyword and the number, + // so no subject's own marker can be read as a card binding (0 of 1,545). + t('commitSubjectPrNumber: reads the squash marker', commitSubjectPrNumber('fix(x): a subject (#11085)\n\nbody'), '11085'); + t('commitSubjectPrNumber: absent marker -> null', commitSubjectPrNumber('fix(x): a subject\n\nbody'), null); + t('commitSubjectPrNumber: a marker in the BODY is not the subject\'s', commitSubjectPrNumber('fix(x): a subject\n\nquoted from another commit (#123)'), null); + t('commitSubjectPrNumber: empty message', commitSubjectPrNumber(''), null); + t('the squash marker is never bound as a card (the paren stands between)', closingKeywordTargets('fix(x): a subject that fixed (#11085)', { markdown: false }).size, 0); + t('…including the substring case the corpus is full of', closingKeywordTargets('fix(rest): optional KernelResolver.resolveEnvironment (#11085)', { markdown: false }).size, 0); + + // The summary line's H23 clause — with a yield of ~6 in 1,546 a quiet row is + // the NORMAL reading, so the coverage numbers are the only thing separating a + // read surface from an unread one (#4690). + t('summary: the H23 clause states what the commit pass read', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, commits: 300, commitBindings: 51, commitBindingMessages: 44 }, 0).includes('H23 read 300 squash commit message(s)'), true); + t('summary: …and the binding totals a promotion decision would need', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, commits: 300, commitBindings: 51, commitBindingMessages: 44 }, 0).includes('51 closing-keyword binding(s) across 44 message(s)'), true); + t('summary: …and states the window boundary', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, commits: 300 }, 0).includes('invisible by design'), true); + t('summary: absent H23 counts degrade to 0, never to undefined', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0 }, 0).includes('H23 read 0 squash commit message(s)'), true); + // -- H9: `pm:on-hold` without a machine-fireable `Restart-when:` ------------ const hold = (body) => issue(['pm:on-hold'], [], body); t('H9: hold with no Restart-when line -> finding', typeof h9OnHoldNoRestartWhen(hold('parked until the train ships')), 'string');