Skip to content

Commit 4a524b8

Browse files
committed
wip(pm): H31 binds a carrier pair through the closing keyword
Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr Co-authored-by: Claude <noreply@anthropic.com>
1 parent b3b43b6 commit 4a524b8

1 file changed

Lines changed: 129 additions & 5 deletions

File tree

‎scripts/pm/check-half-states.mjs‎

Lines changed: 129 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2155,6 +2155,13 @@ export function branchNameTarget(ref) {
21552155
* not narrow it here to serve H8: that would make the live half invisible to
21562156
* the rows that exist to see it.
21572157
*
2158+
* ⭐ And ⛔ not to serve H31 either (#18229). That row needs a stronger question
2159+
* — "does this PR CLOSE the card", because the gate's clearing stroke rides the
2160+
* landing — and it asks it of `bindingClosesCard` as a filter over the
2161+
* population THIS relation hands it. The narrowing is the READER's and lives
2162+
* beside the row that needs it; the relation stays wide, so H8, H31, H35 and
2163+
* H53 still cannot disagree about which PR delivers which card.
2164+
*
21582165
* ## The boolean is DERIVED from `deliveryEvidence`, and stays byte-identical
21592166
*
21602167
* The verdict is now one `!== null` over `deliveryEvidence` below rather than a
@@ -3014,12 +3021,24 @@ export const PM_STATE_LABELS = [
30143021
'finding',
30153022
];
30163023

3024+
/**
3025+
* The ruling-anchor state: a card that stays OPEN by design, so nothing that
3026+
* fires on a CLOSURE ever fires on it. H9's header carries the measurement
3027+
* (`Restart-when: closed …#5499` parked a card forever because #5499 is one of
3028+
* these), and H31 reads it for the same structural reason — a gate hung on a
3029+
* card nothing closes is never cleared by the stroke that clears gates.
3030+
*
3031+
* One spelling, two readers: a second literal here is exactly the drift that
3032+
* lets one reader move and the other keep answering the old way.
3033+
*/
3034+
export const TRACKING_ANCHOR_LABEL = 'tracking';
3035+
30173036
/**
30183037
* Labels whose NORMAL shape is domain-without-pm-state, excluded by the
30193038
* sweep's own protocol text (SKILL.md, Backlog sweep): flagging them would
30203039
* report the protocol's design as a defect.
30213040
*/
3022-
export const H13_EXEMPT_LABELS = ['tracking', 'status:parked', 'qa-run'];
3041+
export const H13_EXEMPT_LABELS = [TRACKING_ANCHOR_LABEL, 'status:parked', 'qa-run'];
30233042

30243043
/**
30253044
* H13 threshold — "one sweep cycle": the triage Routine fires HOURLY and its
@@ -6045,6 +6064,44 @@ export function h30QueueRotting(issue, nowMs = Date.now()) {
60456064
// governs enqueue and landing while the PR is open, and a merged carrier is a
60466065
// closed-out stroke rather than a live half-write.
60476066
//
6067+
// ## Which binding makes a CARRIER PAIR (#18229)
6068+
//
6069+
// The comparison needs two carriers of ONE gate, and the gate is cleared by the
6070+
// stroke that lands the increment. So the binding that puts a card into it is
6071+
// the one that makes that landing reach the card: a CLOSING KEYWORD. A
6072+
// `Part of #N` line is the opposite declaration — it says the PR is a MEMBER of
6073+
// what #N tracks — and an epic tracker is by construction a card no PR closes.
6074+
//
6075+
// Measured, anchor #9857's 2026-09-14T19:45Z sweep: card #14122 (`tracking`, the
6076+
// one-artifact/N-packages epic) drew a row saying the gate was missing from the
6077+
// card half of a dual carrier, while the two PRs the row named — #18212 and
6078+
// #18213 — each carried `Fixes` for their OWN card (#18202, #18204), each of
6079+
// which carried the gate, and each said in the same line the predicate read that
6080+
// #14122 is a tracker that stays open. Both real dual carriers were intact. The
6081+
// row's own remedy text is action-shaped, so acting on it means hanging
6082+
// `needs:contract-review` on a card nothing closes: the clearing stroke never
6083+
// arrives and the gate sits there as a permanent false blocker. This false
6084+
// positive costs a WRITE nothing later removes, and every correctly gated
6085+
// sub-PR re-manufactures it on every sweep, forever.
6086+
//
6087+
// ⛔ The shared relation is NOT narrowed — `prDeliversCard`'s docblock forbids
6088+
// that, and H8's open side, H35's sibling resolver, `claimDelivery` and the
6089+
// pairing `check-clause2-carriers` derives all still read it wide. The narrowing
6090+
// is H31's OWN, one filter over the population THIS row judges, so the rows
6091+
// still agree about which PR delivers which card and differ only about which
6092+
// binding makes a carrier pair — which is a question only this row asks.
6093+
//
6094+
// ## …and every other binding is DECLINED, never silently dropped
6095+
//
6096+
// A silent drop would be #4690 in this row's own uniform: a split that was never
6097+
// judged would render exactly like a board whose two carriers agree, and this is
6098+
// the one row that can tell 「被剥」 from 「从未挂过」. So a weak-bound delivering PR
6099+
// whose carrier DISAGREES with the card's still produces a row — one that names
6100+
// both carriers and the binding it read, says the comparison is not answerable
6101+
// from that binding, and prescribes NOTHING. A weak-bound PR whose carrier
6102+
// AGREES produces nothing, exactly as before: there is no split to report, and a
6103+
// standing row per tracker per sweep is the disease above, not its cure.
6104+
//
60486105
// Report-only, and emphatically: this is a GATE. ⛔ Never a label written from
60496106
// this script — a sweeper that hung or cleared a review gate would be issuing
60506107
// the review verdict, and the one thing the whole clause-② chain forbids is
@@ -6054,9 +6111,43 @@ export function h30QueueRotting(issue, nowMs = Date.now()) {
60546111
/** The clause-② gate label — one constant, both carriers. */
60556112
export const CONTRACT_REVIEW_LABEL = 'needs:contract-review';
60566113

6114+
/**
6115+
* Does this PR's binding to card `n` make GitHub CLOSE the card on merge — the
6116+
* binding H31's carrier comparison needs (#18229)?
6117+
*
6118+
* One read of `deliveryEvidence`'s grading, ⛔ never a second keyword parser:
6119+
* that function grades the closing keyword FIRST and never lets position
6120+
* downgrade it, so a body carrying both `Fixes #N` and `Part of #N` answers
6121+
* `true` here — which is exactly what GitHub does on merge. The three weaker
6122+
* kinds answer `false`: `part-of` and `part-of-inline` declare MEMBERSHIP, and
6123+
* `branch-name` is a body that declared nothing at all, so neither says the
6124+
* landing reaches this card.
6125+
*
6126+
* ⚠️ It answers "does this PR CLOSE #n", never "does this PR deliver #n" —
6127+
* `prDeliversCard` owns the second question and stays wide for the readers that
6128+
* need a half in flight to be visible. ⛔ Do not substitute one for the other.
6129+
*
6130+
* Exported because the sibling shape is live one file over: the clause-② dual
6131+
* carrier that `check-clause2-carriers --pair` demands on an epic tracker
6132+
* reached through a `Part of` line (#18214) is the same question, and that fix
6133+
* wants this predicate rather than a second copy of it.
6134+
*/
6135+
export function bindingClosesCard(pr, n) {
6136+
return deliveryEvidence(pr, n) === 'closing-keyword';
6137+
}
6138+
60576139
/**
60586140
* H31 — null when the two carriers agree (or the comparison is not yet
6059-
* possible), else the finding sentence.
6141+
* possible), else the finding sentence, else — for a delivering PR bound to
6142+
* this card by something other than a closing keyword — the DECLINED sentence.
6143+
*
6144+
* Three outcomes, never two (#18229). A closing-keyword binding is adjudicated
6145+
* exactly as before, byte for byte. A weaker binding is never adjudicated and
6146+
* never silently dropped: it produces a row only when the two carriers actually
6147+
* differ, and that row states that it declined and prescribes no write. The
6148+
* declined row is deliberately NOT marked `UNJUDGED_MARKER`: that marker buys
6149+
* trim priority ahead of judged rows, and a decline must never sort ahead of a
6150+
* real carrier split in the same `gate` band.
60606151
*
60616152
* A PR row whose `labels` is not an array is one this sweep could not read, and
60626153
* it is EXCLUDED from the comparison rather than counted as unlabelled: reading
@@ -6076,8 +6167,15 @@ export function h31ContractReviewCarrierSplit(issue, openPrs) {
60766167
);
60776168
if (delivering.length === 0) return null; // card-side-first is legal — see the header note.
60786169
const cardGated = labelNames(issue ?? {}).includes(CONTRACT_REVIEW_LABEL);
6079-
const gatedPrs = delivering.filter((pr) => labelNames(pr).includes(CONTRACT_REVIEW_LABEL));
6080-
const barePrs = delivering.filter((pr) => !labelNames(pr).includes(CONTRACT_REVIEW_LABEL));
6170+
// The population split the header argues for (#18229): only a closing keyword
6171+
// makes a landing that reaches THIS card, so only those PRs are a carrier pair
6172+
// with it. The rest are reported below, never judged, and never dropped.
6173+
const judged = delivering.filter((pr) => bindingClosesCard(pr, n));
6174+
const declined = delivering.filter((pr) => !bindingClosesCard(pr, n));
6175+
const gatedIn = (prs) => prs.filter((pr) => labelNames(pr).includes(CONTRACT_REVIEW_LABEL));
6176+
const bareIn = (prs) => prs.filter((pr) => !labelNames(pr).includes(CONTRACT_REVIEW_LABEL));
6177+
const gatedPrs = gatedIn(judged);
6178+
const barePrs = bareIn(judged);
60816179
// With its evidence (#16706) — this row names a PR as DELIVERING the card,
60826180
// and a reader clearing a gate off it needs to know whether that rests on a
60836181
// closing keyword or on a `Part of` that sat mid-line.
@@ -6107,7 +6205,33 @@ export function h31ContractReviewCarrierSplit(issue, openPrs) {
61076205
`demonstrably still live one carrier over. ${contract}`
61086206
);
61096207
}
6110-
return null;
6208+
// Nothing adjudicable, or the adjudicable pair agrees. What is left is the
6209+
// weak-bound half: a PR this sweep believes delivers the card on evidence
6210+
// that does not close it. A DIFFERENCE there is still reportable — silence
6211+
// would render it identically to agreement, which is the one confusion this
6212+
// row exists to end — but it is not adjudicable, so the row says exactly that
6213+
// and asks for no write. Agreement here stays silent, as it was before.
6214+
const unjudgeable = cardGated ? bareIn(declined) : gatedIn(declined);
6215+
if (unjudgeable.length === 0) return null;
6216+
const anchorClause = labelNames(issue ?? {}).includes(TRACKING_ANCHOR_LABEL)
6217+
? ` The card carries \`${TRACKING_ANCHOR_LABEL}\` — a ruling-anchor state that stays OPEN by design, the ` +
6218+
'same property that keeps a `Restart-when: closed …#N` exit from ever firing on one — so no PR will ' +
6219+
'close it, a gate hung here would never be reached by the stroke that clears gates, and it would sit ' +
6220+
'as a permanent blocker on a card whose sub-PRs are gated correctly one level down. '
6221+
: ' ';
6222+
const direction = cardGated
6223+
? `the CARD carries \`${CONTRACT_REVIEW_LABEL}\` while the open PR ${list(unjudgeable)} does NOT`
6224+
: `\`${CONTRACT_REVIEW_LABEL}\` is on the open PR ${list(unjudgeable)} while the CARD does NOT carry it`;
6225+
return (
6226+
`${direction} — and this row DECLINES to judge that pair, which is NOT the same as reporting it ` +
6227+
'clean. The binding between the two is the one printed beside the PR number, and it is not a closing ' +
6228+
'keyword: a `Part of` line declares MEMBERSHIP in what this card tracks and a branch-name fallback ' +
6229+
'declares nothing at all, so neither says this PR\'s landing closes this card — and the gate\'s clearing ' +
6230+
`stroke rides that landing.${anchorClause}⛔ The row prescribes NOTHING: do not hang the gate on the ` +
6231+
'card and do not clear it off the PR on the strength of it. What is owed first is a reading of the PR ' +
6232+
'body — decide which card that PR actually delivers; only if a real half-write is behind it does anyone ' +
6233+
`write a carrier, and then it is the review's owner writing both in one stroke. ${contract}`
6234+
);
61116235
}
61126236

61136237
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)