Skip to content

Commit e0a4efa

Browse files
committed
fix(pm): bind H31's carrier pair through the closing keyword
H31 compares the two carriers of the `needs:contract-review` gate, and it took `prDeliversCard` -- the wide body-first/branch-fallback delivery relation -- as its population. That relation counts a `Part of #N` line, which declares MEMBERSHIP in what #N tracks rather than a delivery of it, so an epic tracker whose sub-PRs are gated correctly drew a row saying the gate was missing from the card half of a dual carrier. The row's remedy text is action-shaped: acting on it hangs the gate on a card nothing will ever close, where the stroke that clears gates never arrives. The comparison now runs over PRs bound by a closing keyword -- the binding that makes the landing reach the card, which is what the clearing stroke rides. The shared relation is untouched (its docblock forbids narrowing it), so H8, H31, H35 and H53 still agree about which PR delivers which card. Weaker bindings are DECLINED, never silently dropped: a weak-bound delivering PR whose carrier differs from the card's still produces a row that names both carriers and the binding it read, states that it declined, and prescribes no write. Agreement on a weak binding stays silent, as before. Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4a524b8 commit e0a4efa

1 file changed

Lines changed: 85 additions & 1 deletion

File tree

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

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25927,7 +25927,8 @@ async function selfTest() {
2592725927
t('H31: a PR row whose labels could not be read is not judged as bare', h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [{ ...bare, labels: undefined }]), null);
2592825928
t('H31: …and one readable bare PR alongside it still fires', typeof h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [{ ...bare, labels: undefined }, gatePr(11845, [])]), 'string');
2592925929
// The delivery relation is H8's, shared rather than re-derived.
25930-
t('H31: the branch-name fallback delivers a body-silent PR', typeof h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [{ ...bare, body: '' }]), 'string');
25930+
t('H31: the branch-name fallback still produces a row for a body-silent PR', typeof h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [{ ...bare, body: '' }]), 'string');
25931+
t('H31: …but it is the DECLINED one — a body that declared nothing does not close the card (#18229)', says(h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [{ ...bare, body: '' }]), 'DECLINES to judge'), true);
2593125932
t('H31: …and H8 reads the same PR as delivering the same card', prDeliversCard({ ...bare, body: '' }, '11427'), true);
2593225933
t('H31: a CLOSED card is out of scope', h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL], { state: 'closed' }), [bare]), null);
2593325934
t('H31: a missing issue does not crash', h31ContractReviewCarrierSplit(undefined, [bare]), null);
@@ -25949,6 +25950,89 @@ async function selfTest() {
2594925950
// all — the shape this row deliberately does NOT report.
2595025951
t('H31 live: #10025 (gated, no PR carrier yet) -> clean', h31ContractReviewCarrierSplit({ ...gateCard(['domain:services', 'pm:blocked', CONTRACT_REVIEW_LABEL]), number: 10025 }, [live11844]), null);
2595125952

25953+
// -- H31: which binding makes a CARRIER PAIR (#18229) -----------------------
25954+
// The defect, byte-shaped from anchor #9857's 2026-09-14T19:45Z sweep: PR
25955+
// #18212 carries `Fixes #18202` (its own card, gated) AND a `Part of #14122`
25956+
// line naming the epic tracker, and H31 read the second as a delivery — so a
25957+
// correctly gated sub-PR manufactured an action-shaped row against a card no
25958+
// PR will ever close.
25959+
const pr18212 = (labels = ['documentation', 'size/l', 'tests', 'tooling', CONTRACT_REVIEW_LABEL]) => ({
25960+
number: 18212,
25961+
merged_at: null,
25962+
draft: true,
25963+
body:
25964+
'Fixes #18202\n\nPart of #14122 — the epic tracking the one-artifact/N-packages family. ' +
25965+
'That tracker stays open.',
25966+
head: { ref: 'claude/issue-18202-crossref-dependency-aware' },
25967+
labels: labels.map((name) => ({ name })),
25968+
});
25969+
const card18229 = (number, labels) => ({
25970+
number,
25971+
state: 'open',
25972+
labels: labels.map((name) => ({ name })),
25973+
assignees: [],
25974+
body: '',
25975+
title: '',
25976+
});
25977+
const tracker14122 = card18229(14122, ['priority:p2', TRACKING_ANCHOR_LABEL, 'domain:spec']);
25978+
const delivered18202 = card18229(18202, ['bug', 'priority:p1', 'pm:dispatched', 'domain:spec']);
25979+
25980+
// The binding predicate itself, both directions on ONE body.
25981+
t('#18229: a closing keyword closes the card it names', bindingClosesCard(pr18212(), '18202'), true);
25982+
t('#18229: a `Part of` line on the SAME body does not', bindingClosesCard(pr18212(), '14122'), false);
25983+
t('#18229: nor an inline `Part of`', bindingClosesCard({ number: 1, body: 'see part of #7918 above' }, '7918'), false);
25984+
t('#18229: nor the branch-name fallback', bindingClosesCard({ number: 1, body: '', head: { ref: 'claude/issue-9834-x' } }, '9834'), false);
25985+
t('#18229: a keyword outranks an inline `Part of` for the SAME card, exactly as GitHub does', bindingClosesCard({ number: 1, body: 'Fixes #7918\n\nsee part of #7918 above' }, '7918'), true);
25986+
t('#18229: a PR bound to no card at all closes nothing', bindingClosesCard({ number: 1, body: 'no declaration' }, '9999'), false);
25987+
t('#18229: a missing PR does not crash', bindingClosesCard(undefined, '1'), false);
25988+
// ⛔ The SHARED relation is untouched — the narrowing is H31's own filter.
25989+
t('#18229: ⛔ `prDeliversCard` is NOT narrowed — the tracker is still a delivery to H8/H35/H53', prDeliversCard(pr18212(), '14122'), true);
25990+
t('#18229: …and the evidence kind H8 prints is unchanged', deliveryEvidence(pr18212(), '14122'), 'part-of');
25991+
25992+
// The PIN: the tracker's row no longer prescribes a write.
25993+
const row14122 = h31ContractReviewCarrierSplit(tracker14122, [pr18212()]);
25994+
t('#18229 pin: the `Part of`-only tracker still produces a row — ⛔ not a silent skip', typeof row14122, 'string');
25995+
t('#18229 pin: …and the row says it DECLINED rather than reporting clean', says(row14122, 'DECLINES to judge'), true);
25996+
t('#18229 pin: …naming the binding it read', says(row14122, '#18212 (draft, via a `Part of` declaration)'), true);
25997+
t('#18229 pin: …and why a tracker can never clear a gate', says(row14122, 'ruling-anchor state that stays OPEN by design'), true);
25998+
t('#18229 pin: …the row prescribes NOTHING', says(row14122, 'prescribes NOTHING'), true);
25999+
t('#18229 pin: …it is ⛔ NOT the action-shaped sentence any more', says(row14122, 'more dangerous half'), false);
26000+
t('#18229 pin: …nor does it ask anyone to hang the gate on the card', says(row14122, 'do not hang the gate on the'), true);
26001+
t('#18229 pin: a decline is not a LOUD row', isLoudFinding(row14122), false);
26002+
t('#18229 pin: …and ⛔ not an UNJUDGED-ranked one either — it must never sort ahead of a real split', isUnjudgedFinding(row14122), false);
26003+
t('#18229 pin: the decline still carries the dual-carrier contract', says(row14122, 'READ-BACK'), true);
26004+
26005+
// The CONTROL: the card that PR actually closes is judged exactly as before.
26006+
const row18202 = h31ContractReviewCarrierSplit(delivered18202, [pr18212()]);
26007+
t('#18229 control: a closing-keyword binding whose card lacks the gate — the row STANDS', typeof row18202, 'string');
26008+
t('#18229 control: …and it is the unchanged action-shaped sentence', says(row18202, 'more dangerous half'), true);
26009+
t('#18229 control: …which is ⛔ not a decline', says(row18202, 'DECLINES to judge'), false);
26010+
t('#18229 control: …and the gated card half agrees -> clean', h31ContractReviewCarrierSplit(card(18202, ['bug', CONTRACT_REVIEW_LABEL]), [pr18212()]), null);
26011+
26012+
// Agreement on a weak binding stays SILENT — a standing row per tracker per
26013+
// sweep is the disease this fix cures, not the cure.
26014+
t('#18229: weak binding, both carriers gated -> clean', h31ContractReviewCarrierSplit(card(14122, [TRACKING_ANCHOR_LABEL, CONTRACT_REVIEW_LABEL]), [pr18212()]), null);
26015+
t('#18229: weak binding, neither carrier gated -> clean', h31ContractReviewCarrierSplit(tracker14122, [pr18212(['documentation', 'size/l'])]), null);
26016+
26017+
// The decline is about the BINDING; the anchor clause is the extra the label
26018+
// buys. A weak-bound ordinary card still reports, without that clause.
26019+
const weakOrdinary = h31ContractReviewCarrierSplit(card18229(14122, ['priority:p2']), [pr18212()]);
26020+
t('#18229: a weak-bound card with NO `tracking` still reports the split', says(weakOrdinary, 'DECLINES to judge'), true);
26021+
t('#18229: …without the ruling-anchor clause, which the label is what buys', says(weakOrdinary, 'ruling-anchor state'), false);
26022+
26023+
// Precedence: an adjudicable split outranks a decline, and names only the PR
26024+
// that can actually close the card.
26025+
const mixedCard18229 = card18229(11427, ['pm:dispatched']);
26026+
const mixed18229 = h31ContractReviewCarrierSplit(mixedCard18229, [gatePr(11844, [CONTRACT_REVIEW_LABEL]), { ...pr18212(), body: 'Part of #11427' }]);
26027+
t('#18229 precedence: a real carrier split wins over a decline', says(mixed18229, 'more dangerous half'), true);
26028+
t('#18229 precedence: …and names the closing-bound PR', says(mixed18229, '#11844'), true);
26029+
t('#18229 precedence: …⛔ never the `Part of`-bound one', says(mixed18229, '#18212'), false);
26030+
26031+
// One spelling of the ruling-anchor state, two readers (H13's exemption list
26032+
// and H31's clause) — a second literal is the drift this constant prevents.
26033+
t('#18229: the ruling-anchor label has ONE spelling', TRACKING_ANCHOR_LABEL, 'tracking');
26034+
t("#18229: …and H13's exemption list reads that same constant", H13_EXEMPT_LABELS.includes(TRACKING_ANCHOR_LABEL), true);
26035+
2595226036
// -- The window arithmetic (#11118) ----------------------------------------
2595326037
// The derivation is executable so that a cap and the sentence justifying it
2595426038
// cannot drift apart again: H8's docblock quoted `~18 merges/day` while the

0 commit comments

Comments
 (0)