Skip to content

Commit cf33adb

Browse files
hotlongclaude
andauthored
fix(lint): the partof gate stops asking for a rewrite it also forbids (#16749)
`check-partof-closing-keyword`'s RULE 2 guidance told the author to push reworded commits while forbidding amend, rebase and force-push in the same breath. On an already-pushed branch those are the same act: the gate reads the PR's commit list, so a commit on top joins that list and leaves the offending message in it, and rewording a pushed commit is the rewrite AGENTS.md bans. The red was therefore permanent for that branch however the author responded, and a seat that read the text concluded an amend was wanted and instructed one — the dev's refusal is what stopped it, not the wording. The same text also had the merger "take the squash message from that body". Measured on 0a61db1, the squash of PR 16646: the landed message is the branch commit's verbatim and carries its Refs trailer, while the body's closing keyword appears nowhere in it. The squash message is assembled from the COMMITS. That does not weaken RULE 2 — it is RULE 2's premise. - The guidance now splits on timing. An UNPUSHED branch gets the reachable remedy (reword now, push once, green). An already-pushed branch is told plainly that no author action clears the red, with the three measured facts that let a reader stop working on it: the check run is advisory at the branch-protection layer, the squash takes the commits, and the card closes from the PR body's keyword regardless. - What a landed trailer costs is stated by spelling rather than flattened: Part-of and Refs land as a reference and move no card, a closing keyword lands on the surface the parser reads. The landing decision itself is handed to whoever lands the PR — this gate reports and does not order a merge. - Six self-test pins hold the repaired shape, and the RULE 2 battery floor rises 13 to 19 so they are floored rather than merely present. ⛔ No verdict logic, exit code or required-context change: on PR 16646's shape the gate still exits 1 naming that commit, and still exits 0 with the trailer line removed, byte-identically. Claude-Session: https://claude.ai/code/session_01P58euzUXCVJNwmhuPC9DXY Co-authored-by: Claude <noreply@anthropic.com>
1 parent a8f3faa commit cf33adb

1 file changed

Lines changed: 110 additions & 12 deletions

File tree

scripts/check-partof-closing-keyword.mjs

Lines changed: 110 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,48 @@
109109
* their own, so a branch pushed to the default branch by any other route closes
110110
* the cards its intermediate commits name.
111111
*
112-
* The remedy is the contract this repository already carries, cited rather than
113-
* restated — the card relation is declared ONCE, in the PR body, and the merger
114-
* takes the squash message from that body. ⛔ Nothing in this gate's output asks
115-
* anyone to rewrite history: amend, rebase and force-push are forbidden here,
116-
* and a red on an already-pushed branch is repaired the way this repo's merges
117-
* already repair it, in the body and at the merge.
112+
* The contract is the one this repository already carries, cited rather than
113+
* restated — the card relation is declared ONCE, in the PR body, and no commit
114+
* carries a card trailer.
115+
*
116+
* ## RULE 2 — what the output may ask for, and when it may ask for nothing
117+
*
118+
* ⛔ Nothing in this gate's output asks anyone to rewrite history: amend, rebase
119+
* and force-push are forbidden here. That prohibition is absolute, which makes
120+
* the TIMING of a RULE 2 finding the whole story:
121+
*
122+
* - BEFORE the branch is pushed the remedy is ordinary work — reword the
123+
* commit messages, state the relation only in the body, push once. Nothing
124+
* published is rewritten and the check goes green.
125+
* - ONCE THE BRANCH IS PUSHED no author action clears the red. This gate reads
126+
* the PR's COMMIT LIST, so a new commit on top JOINS that list and leaves
127+
* the offending message in it; the only thing that would remove it is the
128+
* rewrite forbidden above. The red is PERMANENT for that branch, and saying
129+
* so plainly is the point of this section. An earlier revision instead told
130+
* the author to push reworded commits — which on a pushed branch IS the
131+
* forbidden rewrite — and a seat that read it instructed one; the dev's
132+
* refusal is what stopped it, not the wording.
133+
*
134+
* ⚠️ That earlier revision also had the merger "take the squash message from
135+
* that body". Measured, that is false: on 0a61db1f5, the squash of PR #16646,
136+
* the landed message is the branch commit's verbatim and carries its
137+
* `Refs #16624`, while the body's `Fixes #16624` appears nowhere in it. The
138+
* squash message is assembled from the COMMITS. This does not weaken RULE 2 —
139+
* it is RULE 2's premise: a trailer left on a pushed commit really does reach
140+
* the default branch's permanent history.
141+
*
142+
* What that costs depends on the spelling, and the output says so rather than
143+
* flattening it: Part-of and Refs land as a reference and move no card, while a
144+
* CLOSING keyword lands on the surface GitHub's parser reads. The asymmetry is
145+
* why the rule refuses all three at PR time rather than only the contradictory
146+
* ones.
147+
*
148+
* ⛔ Whether a pull request LANDS carrying this red is not this gate's call and
149+
* its output must not make it. The check run is advisory at the branch-
150+
* protection layer — absent from the required-context registry, and its workflow
151+
* subscribes to no `merge_group` event because a queue build carries no body for
152+
* it to judge — so that decision belongs to whoever lands the PR, under the
153+
* rules that bind them. This gate reports.
118154
*
119155
* ## Where the commit messages come from, and why the endpoint and not a walk
120156
*
@@ -314,7 +350,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
314350
'Context reading: presence, not truthiness.': 4,
315351
'The wiring itself. A gate whose workflow step is deleted or whose': 6,
316352
'The predicate source this gate reuses must still be there to reuse.': 1,
317-
'RULE 2 — every card-relation spelling in a commit message is a finding,': 13,
353+
'RULE 2 — every card-relation spelling in a commit message is a finding,': 19,
318354
'The regression fixture: the squash that assembled a contradiction no': 4,
319355
'RULE 2 delegates to the sweep extractors at the commit-message reading.': 3,
320356
'The commit list input. An absent, broken or empty list can never read': 8,
@@ -505,10 +541,11 @@ export function commitTrailerFindings(commits) {
505541
`message that lands on the default branch — a text no one writes and no one reviews, which ` +
506542
`carries every trailer its inputs carried and can contradict itself where its parts did not. ` +
507543
`The trailer is also live on its own. ${RELATION_CONTRACT} ` +
508-
`Remedy: move this relation into the PR body, where it is stated once, and take the squash ` +
509-
`message from that body at merge. ` +
544+
`Remedy, while this branch is still UNPUSHED: reword this commit message so the relation is ` +
545+
`stated only in the PR body. ` +
510546
`⛔ Do NOT amend, rebase or force-push to remove it — rewriting pushed history is forbidden ` +
511-
`here, and it is not what fixes this.`,
547+
`here; on an already-pushed branch nothing removes it, and the summary below is what to do ` +
548+
`about that.`,
512549
);
513550
}
514551
return findings;
@@ -658,8 +695,32 @@ export function judge(ctx) {
658695
);
659696
for (const finding of commitFindings) lines.push(` ${finding}`, '');
660697
lines.push(
661-
' Pushing the reworded commits re-runs this check. ⛔ The repair is NOT a history rewrite:',
662-
' state the relation once in the PR body and take the squash message from that body at merge.',
698+
' ⛔ The repair is NOT a history rewrite. Amend, rebase and force-push are forbidden in this',
699+
' repository and this gate never asks for one. What it does ask for turns on one thing only:',
700+
'',
701+
' BRANCH NOT PUSHED YET — reword the commit messages now and push once. Nothing published is',
702+
' rewritten, the relation goes in the PR body where the contract puts it, and this check is green.',
703+
'',
704+
' BRANCH ALREADY PUSHED — no author action clears this red, and that is expected rather than a',
705+
" problem to solve. This gate reads the PR's COMMIT LIST, so a new commit on top JOINS that list",
706+
' and leaves the message above in it; the only thing that would remove it is the rewrite forbidden',
707+
' above. Three measured facts, so this red can be READ rather than acted on:',
708+
'',
709+
' 1. This check run is advisory at the branch-protection layer: it is absent from the',
710+
' required-context registry, and its workflow subscribes to no `merge_group` event because a',
711+
' queue build carries no PR body for it to judge.',
712+
' 2. The squash message is assembled from the COMMIT messages, not from the PR body. Measured on',
713+
" 0a61db1f5, the squash of PR #16646: the landed message is the branch commit's, verbatim,",
714+
" carrying its `Refs #16624`; the body's `Fixes #16624` is nowhere in it.",
715+
' 3. The card relation is safe either way. #16624 closed on that same merge although no commit',
716+
" message named a closing keyword for it — the PR BODY's keyword is what acts. Declaring the",
717+
' relation once in the body is the whole contract, and it already works.',
718+
'',
719+
' So the residue of landing this red is the trailer above sitting in the permanent history, and',
720+
' what that costs depends on its spelling: `Part of` and `Refs` land as a reference and move no',
721+
" card, while a CLOSING keyword lands on the surface GitHub's parser reads. ⛔ Weigh that under the",
722+
' landing rules that bind you — this gate found a real contradiction between your commits and your',
723+
' body, and it does not decide whether the pull request merges.',
663724
);
664725
}
665726
return { exit: EXIT_CONTRADICTION, lines: [...lines, ...(unread.length ? ['', ...unread] : [])] };
@@ -881,6 +942,43 @@ function selfTest() {
881942
named.includes(RELATION_CONTRACT),
882943
true,
883944
);
945+
// The guidance text itself, pinned. This gate once told the author to push
946+
// reworded commits while also forbidding a rewrite — unsatisfiable on a
947+
// pushed branch, and a seat that read it instructed an amend and force-push.
948+
// These six hold the repaired shape: the forbidden action stays forbidden,
949+
// the reachable remedy is scoped to an UNPUSHED branch, the pushed case is
950+
// named as permanent, the measured squash fact replaces the false one, and
951+
// the gate still refuses to decide the merge.
952+
t(
953+
'the guidance still forbids amend, rebase and force-push',
954+
/ Do NOT amend, rebase or force-push/.test(named),
955+
true,
956+
);
957+
t(
958+
'the reachable remedy is scoped to a branch that is NOT pushed yet',
959+
named.includes('UNPUSHED') && named.includes('BRANCH NOT PUSHED YET'),
960+
true,
961+
);
962+
t(
963+
'the already-pushed case is named, and named as clearing for nobody',
964+
named.includes('BRANCH ALREADY PUSHED') && named.includes('no author action clears this red'),
965+
true,
966+
);
967+
t(
968+
'the guidance no longer claims the squash message comes from the PR body',
969+
/squash message from that body/.test(named),
970+
false,
971+
);
972+
t(
973+
'the guidance states the MEASURED squash fact instead',
974+
named.includes('assembled from the COMMIT messages, not from the PR body') && named.includes('0a61db1f5'),
975+
true,
976+
);
977+
t(
978+
'the guidance leaves the landing decision to the lander rather than ordering a merge',
979+
named.includes('does not decide whether the pull request merges'),
980+
true,
981+
);
884982
t(
885983
'an ordinary commit message with no card relation is clean',
886984
commitVerdict('fix(cli): stop counting the walk instead of the tree\n\nBody prose about the change.\n').exit,

0 commit comments

Comments
 (0)