Skip to content

Commit ecf91cb

Browse files
claude[bot]claude
andauthored
fix(pm): bind H31's carrier pair through the closing keyword (#18243)
Fixes #18229 ## What changed `scripts/pm/check-half-states.mjs` only. 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, so acting on it hangs the gate on a card nothing will ever close — where the stroke that clears gates never arrives. Three edits: 1. **`bindingClosesCard(pr, n)`** — a new exported predicate, one read of `deliveryEvidence`'s existing grading, never a second keyword parser. It answers "does this PR CLOSE #n", which is the question the gate's clearing stroke rides. `part-of`, `part-of-inline` and `branch-name` all answer false. 2. **H31's own population filter** — the carrier comparison runs over the PRs `bindingClosesCard` accepts. Those rows are byte-identical to before. 3. **A DECLINED row for every other binding** — a weak-bound delivering PR whose carrier differs from the card's still produces a row, one that names both carriers and the binding it read, states that it declined to judge, and prescribes no write. A weak-bound PR whose carrier agrees produces nothing, exactly as before. `TRACKING_ANCHOR_LABEL` is extracted so the ruling-anchor state has one spelling across H13's exemption list and H31's new clause. ## Why this seam, and how the shared invariant survives The shared relation is NOT narrowed. `prDeliversCard`'s docblock forbids it (「⛔ Do not narrow it here to serve H8: that would make the live half invisible to the rows that exist to see it」), and H8's open side, H35's sibling resolver, `claimDelivery`, H53 and the `check-clause2-carriers` pairing all still read it wide. The narrowing is H31's own filter over the population that relation hands it, so the rows still agree about which PR delivers which card and differ only about which binding makes a CARRIER PAIR — a question only H31 asks. Both docblocks that state the invariant were updated to say so rather than left to imply the old thing. Silently dropping the weak bindings would have been #4690 in this row's own uniform: a split that was never judged renders identically to a board whose two carriers agree, and this is the one row that can tell 「被剥」 from 「从未挂过」. Hence the declined row, and hence its scope: it speaks only where the carriers actually differ, because a standing row per tracker per sweep is the disease, not the cure. The declined row is deliberately NOT marked `UNJUDGED_MARKER` — that marker buys trim priority ahead of judged rows, and a decline must never sort ahead of a real carrier split in the same `gate` band. ## Measured on the live board Anchor #9857's sweep at 2026-09-15T01:57Z, commit `b3b43b6`, run 34919049964, carries exactly one H31 row: #14122 (`tracking`), naming open PR #18212 (draft, via a `Part of` declaration). Replayed offline against the 9 open PRs and the 9 open cards those PRs name (the exact H31 candidate set — a card no open PR names can never reach the comparison): | card | `tracking` | delivering evidence | row before | row after | |---|---|---|---|---| | #18122 | no | #18238 closing-keyword | none | none | | #17502 | no | #18231 closing-keyword | none | none | | #17598 | no | #18230 closing-keyword | none | none | | #15410 | no | #18227 closing-keyword | none | none | | #18202 | no | #18212 closing-keyword | none | none | | #14122 | **yes** | #18212 **part-of** | action-shaped finding | **DECLINED row** | | #17396 | no | #18198 closing-keyword | none | none | | #17356 | no | #18131 closing-keyword | none | none | | #17097 | no | #18096 closing-keyword | none | none | One row changes, and it is the expected one. Every other delivering binding on the live board is a closing keyword, so nothing else moves. ## Tests `pnpm check:pm-half-states` (= `node scripts/pm/check-half-states.mjs --self-test`): **4042 cases pass** before, **4075 cases pass** after, exit 0 — 33 new cases. The pin and the control both land, per the card's own remedies: - pin: the `Part of`-only tracker still produces a row (⛔ not a silent skip), the row says `DECLINES to judge`, names the binding it read, explains why a ruling anchor can never clear a gate, and prescribes NOTHING; it is asserted to be neither the old action-shaped sentence nor a LOUD row nor an UNJUDGED-ranked one. - control: a closing-keyword binding whose card lacks the gate — the row STANDS, with the unchanged action-shaped sentence. - plus: agreement on a weak binding stays clean both ways; a weak-bound card without `tracking` still reports the split but without the ruling-anchor clause; an adjudicable split outranks a decline and names only the closing-bound PR. **Ablation** (one-off, restored; run from the committed fix). Mutating `bindingClosesCard` back to the pre-fix wide population (`deliveryEvidence(pr, n) !== null`) turns the self-test RED: ``` == anchor counts BEFORE == old-text occurrences: 1 / new-text occurrences: 0 mutation written == anchor counts AFTER == old-text occurrences: 0 / new-text occurrences: 1 HEAD blob=dca4e1dbff5d6a46d10c5ac53a527b888e5f0811 MUT blob=906ac10ab7837ef5dc746f1bb3ccda8740e88c75 VERDICT ablation-exit=1 ✗ check-half-states self-test: 11 of 4075 case(s) failed. == restore leg == REST blob=dca4e1dbff5d6a46d10c5ac53a527b888e5f0811 old-text occurrences after restore: 1 / new-text occurrences after restore: 0 git diff HEAD bytes: 0 ``` The mutation is proved on disk by the blob hash moving off the HEAD blob and by the two anchor occurrence counts flipping; the restore leg is proved by the blob hash returning to the HEAD blob and by an empty `git diff HEAD`, not by an exit code. There is no build step and no `dist/` for a repo-root `.mjs`, so the rebuild half of the ablation preflight does not apply here. **Gates** — derived with no paths, at head `e0a4efa`: ``` node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -> 41 command(s) node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RECORD_FILE -> Run reconciliation — 41 derived, 41 run, 0 NOT-MEASURED, 0 UNRUN. EXIT CODES — all 41 accounted famil(ies) carry one, so the NOT-MEASURED count above is DERIVED from them. ``` All 41 exited 0, each captured by redirect before any pipe. The gate script's own wiring is inside that set: `pnpm check:pm-half-states` is the package.json step lint.yml runs, and it is the script's `--self-test`. **Lint** — a declared narrowing, not the farm. `eslint scripts/pm/check-half-states.mjs --no-inline-config --format json` at head `e0a4efa`: **1 file linted, 0 errors, 0 warnings**, exit 0. The population is read from eslint's own config, not guessed — an `ESLint` instance over `git ls-files` reports `tracked=8674 in-scope=6755 ignored=1919 no-rules=0`. Invariance: this repo runs one `eslint.config.mjs` which enables type-aware linting for no file at all (every `parserOptions` in it is exactly `{ ecmaVersion: 'latest', sourceType: 'module' }`; zero `project`/`projectService` hits; the config says so itself at its `QUERY_OPTIONS_TEST_GLOBS` note), and that config is not in this diff — so no rule's verdict on a file this PR did not touch can move because of it. The repo-wide `pnpm lint` run is CI's. `skip-changeset`: `scripts/pm/**` is PM tooling and ships in no package's `files[]`, so nothing published moves. ## Acceptance notes - `scripts/pm/check-half-states.mjs` is non-governed PM tooling, so this is in-seat review. `.github/workflows/half-state-patrol.yml` and anchor #9857 are untouched: the workflow runs the script from `main`, so the fix reaches the anchor at the first scheduled sweep after landing. - #18214 is the sibling shape one file over — the clause-② dual carrier that `scripts/pm/check-clause2-carriers.mjs --pair` demands on an epic tracker reached through a `Part of` line. It is out of scope here and remains open. `bindingClosesCard` is exported for it: that file already imports `deliveryEvidence` and `prDeliversCard` from this one, so the fix there is an import rather than a second copy of the predicate. - noted, not filed: the sibling install of this script in `objectui` (`scripts/pm/check-half-states.mjs`) has already drifted from this copy (different md5 at `b3b43b6`), while this file's header describes the pair as copied VERBATIM. No gate can hold two repos equal, so the wording is an aspiration rather than a claim — carrier: the next dev dispatched on a cross-repo patrol card. --- _Generated by [Claude Code](https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 75237a9 commit ecf91cb

1 file changed

Lines changed: 214 additions & 6 deletions

File tree

scripts/pm/check-half-states.mjs

Lines changed: 214 additions & 6 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
// ---------------------------------------------------------------------------
@@ -25803,7 +25927,8 @@ async function selfTest() {
2580325927
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);
2580425928
t('H31: …and one readable bare PR alongside it still fires', typeof h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [{ ...bare, labels: undefined }, gatePr(11845, [])]), 'string');
2580525929
// The delivery relation is H8's, shared rather than re-derived.
25806-
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);
2580725932
t('H31: …and H8 reads the same PR as delivering the same card', prDeliversCard({ ...bare, body: '' }, '11427'), true);
2580825933
t('H31: a CLOSED card is out of scope', h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL], { state: 'closed' }), [bare]), null);
2580925934
t('H31: a missing issue does not crash', h31ContractReviewCarrierSplit(undefined, [bare]), null);
@@ -25825,6 +25950,89 @@ async function selfTest() {
2582525950
// all — the shape this row deliberately does NOT report.
2582625951
t('H31 live: #10025 (gated, no PR carrier yet) -> clean', h31ContractReviewCarrierSplit({ ...gateCard(['domain:services', 'pm:blocked', CONTRACT_REVIEW_LABEL]), number: 10025 }, [live11844]), null);
2582725952

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+
2582826036
// -- The window arithmetic (#11118) ----------------------------------------
2582926037
// The derivation is executable so that a cap and the sentence justifying it
2583026038
// cannot drift apart again: H8's docblock quoted `~18 merges/day` while the

0 commit comments

Comments
 (0)