From b93320823c551e1c0454c38cbf451bda53619d1f Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 8 Sep 2026 09:01:10 +0000 Subject: [PATCH] fix(check-partof-closing-keyword): say what DISCHARGES a pushed-branch red, beside the rewrite ban The already-pushed paragraph said that no author action clears the red and forbade the rewrite, but never what does clear it, so a reader was left knowing only what not to do. It now states the discharge as a CONDITION on the repository setting: a merge whose squash message is the PR BODY is what keeps the trailer out of the default branch; at `COMMIT_MESSAGES` that takes the lander replacing the assembled commit list with the PR body by hand at the merge button (a queue merge edits nothing, so there the residue lands); at `PR_BODY` every squash does it, queue included. Worded on the setting's value rather than today's value, so it stays true after the ruled setting change is applied. The header carries the same paragraph as the authority on detail. Detection is untouched: RULE 2 and its four counterfactuals are byte-identical, as are the finding sentence, the repair paragraph and the six existing guidance pins. Three new pins hold the discharge sentence, the setting and both of its values, and that the sentence sits BESIDE the prohibition rather than in place of it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01P58euzUXCVJNwmhuPC9DXY --- scripts/check-partof-closing-keyword.mjs | 43 ++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/scripts/check-partof-closing-keyword.mjs b/scripts/check-partof-closing-keyword.mjs index 76e6ea816d..fb822185aa 100644 --- a/scripts/check-partof-closing-keyword.mjs +++ b/scripts/check-partof-closing-keyword.mjs @@ -139,6 +139,17 @@ * it is RULE 2's premise: a trailer left on a pushed commit really does reach * the default branch's permanent history. * + * What DISCHARGES a pushed-branch red is therefore the merge, and only a merge + * whose squash message is the PR BODY — then the trailer never reaches the + * default branch. Whether that happens by itself is the repository setting + * `squash_merge_commit_message`: at `COMMIT_MESSAGES` (the value behind the + * measurement above) it takes the lander replacing the assembled commit list + * with the PR body by hand at the merge button, which a queue merge never does; + * at `PR_BODY` every squash does it, queue included. The output states that + * CONDITION rather than the value the setting holds today, so it stays true + * whichever way the repository is configured when it is read — and discharging + * the red never rewrites history. + * * What that costs depends on the spelling, and the output says so rather than * flattening it: Part-of and Refs land as a reference and move no card, while a * CLOSING keyword lands on the surface GitHub's parser reads. The asymmetry is @@ -769,6 +780,13 @@ export function judge(ctx) { " card, while a CLOSING keyword lands on the surface GitHub's parser reads. ⛔ Weigh that under the", ' landing rules that bind you — this gate found a real contradiction between your commits and your', ' body, and it does not decide whether the pull request merges.', + '', + ' What DISCHARGES it is the merge, and only a merge whose squash message is the PR BODY: then the', + ' trailer above never reaches the default branch. Whether that happens by itself is the repository', + ' setting `squash_merge_commit_message`. At `COMMIT_MESSAGES` (the state fact 2 measured) it takes', + ' the lander replacing the assembled commit list with the PR body BY HAND at the merge button — a', + ' queue merge edits nothing, so there the residue lands. At `PR_BODY` every squash does it, queue', + ' included. Until that merge the red stays on this branch; ⛔ discharging it never rewrites history.', ); } return { exit: EXIT_CONTRADICTION, lines: [...lines, ...(unread.length ? ['', ...unread] : [])] }; @@ -1043,6 +1061,31 @@ function selfTest() { named.includes('does not decide whether the pull request merges'), true, ); + // The discharge sentence. Saying that nothing clears the red on the branch is + // half of the pushed case; the other half is WHAT discharges it — the merge, + // and only a merge whose squash message is the PR body — and the setting that + // decides whether that is by hand or automatic. It is stated as a CONDITION on + // the setting's value, never as today's value, so these three hold whichever + // way the repository is configured when they run; and the sentence must sit + // beside the prohibition, never in place of it. + t( + 'the already-pushed case says what DISCHARGES the red: a merge whose squash message is the PR body', + named.includes('What DISCHARGES it is the merge') && named.includes('squash message is the PR BODY'), + true, + ); + t( + 'and names the setting that decides by-hand versus automatic, with both of its values', + named.includes('`squash_merge_commit_message`') && + named.includes('`COMMIT_MESSAGES`') && + named.includes('`PR_BODY`') && + named.includes('BY HAND'), + true, + ); + t( + 'and the discharge sentence sits BESIDE the prohibition, never in place of it', + named.includes('discharging it never rewrites history') && /⛔ Do NOT amend, rebase or force-push/.test(named), + true, + ); t( 'an ordinary commit message with no card relation is clean', commitVerdict('fix(cli): stop counting the walk instead of the tree\n\nBody prose about the change.\n').exit,