From 35cf894b89deee05ee73afe4d3a9ead292f17f64 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 11 Sep 2026 00:25:02 +0000 Subject: [PATCH] feat(pm): H59 reports a merged PR whose closing linkage mis-states a card it names GitHub's PR->issue closing linkage is unreliable in both directions, so a merged PR silently mis-states the state of every issue it mentions. H59 reads H8's existing merged window a second way and files a report-only row for each direction: FALSE CLOSE the PR mentions #N, binds no closing keyword to it, and #N closed with the merge. The expensive half: work disappears from the board while reading as done, and no other row here can catch it because a closed card is swept by nothing. FALSE OPEN the PR binds a closing keyword to #N and #N is still open. The cheap half: it surfaces as queue noise. Measured on the live board before writing the predicate: the `closed` event carries no `commit_id` at all on this repo's landing path (six cards plus one page of the repo-wide event stream, zero stamps), so the commit leg is kept for the commit-message surface and the temporal leg is what carries here. The band is measured rather than chosen - 321 keyword-bound pairs sit at p50 1s / p95 2s / p99 4s - and the ruling's one-minute reading is narrowed to 5s because a minute admits a seat closing a card 53s after a merge. A sibling PR that declared the closure inside the same band explains it and the row stays quiet; without that guard two of three live candidates are wrong. Report-only: exit codes unchanged, no label written, nothing reopened. The remedy names the owning seat's act. Direction (b) costs no request; direction (a) buys one bounded timeline page for the handful of cards that survive every free narrowing, capped per sweep with the shortfall printed, and no retry on any status. Claude-Session: https://claude.ai/code/session_01YKEjmbYNvYWJvWGSWx26zK Co-authored-by: Claude --- scripts/pm/check-half-states.mjs | 706 ++++++++++++++++++++++++++++++- 1 file changed, 694 insertions(+), 12 deletions(-) diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index 02666a24b1..fa880f26b5 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -3106,8 +3106,10 @@ export function h14BlockingCacheIncoherent(issue, index, options = {}) { * `created_at`, the same quantity the selection order's own within-rank * tie-break reads (同级按卡龄). The label's age would be the sharper number * and is not available: it lives in a per-card timeline fetch this sweep - * deliberately never makes (the H2 comment fetch is the one exception, and it - * is confined to candidates). Reporting card age and NAMING it as card age + * deliberately never makes for THIS population (the H2 comment fetch is one + * exception and is confined to candidates; H59 buys a timeline page, and is + * confined to the handful of cards a merged PR's body names without a keyword). + * Reporting card age and NAMING it as card age * beats reporting a number whose meaning the reader has to guess. * * An unreadable `created_at` sorts as maximally old rather than being skipped @@ -3149,7 +3151,8 @@ export function h15OldestUnclaimedBlocking(issues, nowMs = Date.now()) { '`priority:p0`, so an unclaimed one aging while fresher cards are picked is selection-order ' + 'drift — visible here by name instead of only in a seat\'s memory. Age is the CARD\'s ' + '(`created_at`, the same quantity the order\'s within-rank tie-break reads), not the label\'s: ' + - 'that would need a per-card timeline fetch this sweep never makes. Visibility row — no ' + + 'that would need a per-card timeline fetch this sweep never makes for this population. ' + + 'Visibility row — no ' + 'threshold, it reports unconditionally, and like everything here it is patrol input, not a verdict.', }; } @@ -10939,9 +10942,11 @@ export function h54DecisionTitledQueueCard(issue) { // ON/AFTER it is judged and can be a row. // // ⚠️ The split reads `updated_at`, not the `labeled` event. This sweep makes no -// per-card timeline fetch (H35's header carries the reasoning; the repo-wide -// event stream it reads instead keeps only gate-semantic labels), so the entry -// instant is in no payload this loop already holds. `updated_at` is the +// per-card timeline fetch for THIS population (H35's header carries the +// reasoning; the repo-wide event stream it reads instead keeps only +// gate-semantic labels, and H59's one timeline page is bought for a handful of +// merged-PR mentions, never for a label page), so the entry instant is in no +// payload this loop already holds. `updated_at` is the // cheapest HONEST proxy and its error runs one way: `updated_at` is never // earlier than the labelling, so a card last touched before the pin certainly // entered before it — the census can never hide a post-pin entry — while a @@ -11088,7 +11093,7 @@ export function h55AwaitingWithoutMaintainerAction(issue, commentBodies) { "machine exit: the state still has none by construction (the ager's note stands) — this row judges the ENTRY " + 'only and never fires on the act being done. Report-only: it writes no label and relabels nothing. ' + `Population: entries judged by \`updated_at\` on/after ${MAINTAINER_ACTION_LINE_SINCE} — the sweep makes ` + - 'no per-card timeline fetch, so a legacy entry touched after that instant lands here too, and costs one ' + + 'no per-card timeline fetch for this population, so a legacy entry touched after that instant lands here too, and costs one ' + 'idempotent act, never a missed post-pin entry; earlier entries are COUNTED in the summary census and filed ' + 'as no row, pending the separate reclassification worklist.' ); @@ -12260,6 +12265,356 @@ export function h58QueuedRulingRow(issue, commentBodies) { ); } +// --------------------------------------------------------------------------- +// H59 — a MERGED PR whose closing linkage mis-states the state of a card its +// body names (#17466). Two directions, both report-only, and the FIRST is the +// one that matters. +// +// ## The defect, in the filing card's measurement +// +// GitHub's PR→issue closing linkage is unreliable in BOTH directions, and it +// was measured twice in one hour on a sibling board: one merged PR CLOSED a +// live follow-up card while its body bound no closing keyword to that number +// anywhere (the grep was run with live controls, so the absence is a reading +// and not a failed search), and a second merged PR LEFT its declared target +// open. So no seat can reason about closure from a PR's text, in either +// direction — which is exactly what makes this mechanical rather than a +// discipline item. +// +// FALSE CLOSE the expensive half. Work disappears from the board while +// reading as done, and it is the one failure no sweep catches, +// because a CLOSED card is not swept. Every other row in this +// file reads open cards. +// FALSE OPEN the cheap half. It shows up as queue noise and somebody +// eventually re-reads the card. +// +// ⛔ This row claims NOTHING about which repo the defect was measured in: the +// filing evidence is a sibling's, the mechanism is GitHub's, and the live +// sweep reports whatever THIS board has. A silent row is a reading about this +// board, never a claim about the mechanism. +// +// ## The transport, and the narrowing that makes it affordable +// +// The population starts as H8's merged window — the same bounded listing, not +// a second one — and the mention set is `referencedNumbers`, this file's one +// reference grammar, so a `#N` and an `owner/repo#N` are read exactly as H40 +// reads them. Measured over the live board (8-day window, 2026-09-11): 1,185 +// merged PRs, 5,830 `#N` mentions, 1,106 of them bound by a closing keyword. +// A per-card read for each of the 3,282 unbound mention pairs is not a budget +// any sweep has, so the row narrows BEFORE it buys anything, and every +// narrowing is counted rather than absorbed: +// +// direction (b) costs ZERO requests. A declared target that is still OPEN +// is answered by the open listing this sweep already holds; +// membership is positive evidence, so a TRUNCATED listing can +// only lose a row, never invent one. +// direction (a) needs `closed_at`, which H22's closed-card window already +// carries. A mentioned-but-unbound card that is neither in +// the open listing nor in that window is UNJUDGED and counted +// (`linkageOutOfWindow`) — ⛔ never clean. That window is +// `CLOSED_ISSUE_WINDOW_DAYS` and H8's is `MERGED_WINDOW_DAYS`, +// so this row's real reach is the SHORTER of the two, and the +// clause says so rather than letting a reader assume eight. +// +// Only what survives both — a mentioned, unbound card whose closure sits +// inside the attribution band below — costs ONE page of that card's timeline, +// capped per sweep at `H59_CARD_READ_CEILING` with the shortfall printed. On +// the live board that survivor set was ONE card. ⛔ No retry loop on any +// status (#17374): a failed read is UNJUDGED and counted. +// +// ⚠️ This is the FIRST per-card timeline fetch in this file, and the rows that +// decline one by name (H11/H15's label age, H18/H13's `updated_at` proxy, +// H35's repo-wide event stream, H55's entry instant) still decline it — their +// notes are narrowed to say so rather than deleted. The fetch is bought here +// for a population that is at most a handful of cards a sweep, never for the +// board. +// +// ## ⭐ The measured shape of a close-by-merge — and the assumption it KILLED +// +// The obvious evidence is the `closed` event naming the merge commit. Measured +// on this board, it is not available at all: +// +// card #17417 closed 2026-09-10T23:44:35Z, `commit_id` null, actor +// `os-litant` — PR #17558 (`Fixes #17417`) merged 23:44:33Z +// cards #16274 · #17081 · #17533 · #15141 · #17058 — same shape, five for +// five: a keyword-bound close, `commit_id` null every time +// the repo-wide event stream, one page, 2026-09-10T23:32Z→2026-09-11T00:10Z +// — 7 `closed` events, 0 carrying a `commit_id` +// +// So the commit leg is written and kept (it is exact wherever GitHub does +// stamp it — a keyword in a PUSHED COMMIT message is H23's surface and does), +// but it fires on nothing here, and a row resting on it alone would be a +// predicate that can never speak. The second leg is what carries this board, +// and the filing ruling names it: the close sits inside the merge's own +// moment, with no other actor to explain it. +// +// ## The band is MEASURED, and the ruling's «merge minute» was too wide +// +// Over the same window, every (merged PR, card) pair where the body BINDS a +// closing keyword and the card closed inside H22's window — 332 pairs, 321 of +// them within ±5 minutes of the merge: +// +// 0–2s 313 3–5s 7 31–60s 1 (p50 1s, p95 2s, p99 4s) +// +// GitHub's own act lands in seconds. A full minute admits a different actor +// entirely, and the live board has the specimen: card #16638 closed 53s after +// PR #16950 merged, by `os-project-manager`, with no window PR binding a +// keyword to it — a SEAT closing a card because the delivery landed, which is +// the protocol working, not a false close. A 60-second band reports it; the +// measured band does not. ⇒ `H59_MERGE_CLOSE_BAND_MS` is 5s, which is the +// operational reading of the ruling's own 「with no other actor」, and the one +// 31–60s keyword pair is named here rather than rounded away. +// +// ## The sibling guard — without it, two of three candidates are wrong +// +// PRs land in batches: #17558 and #17559 merged in the SAME second. So a card +// closed inside the band may have been closed by a SIBLING PR that declared it +// properly, and attributing that to the PR that merely mentioned it is a false +// row about a correct landing. Measured at a 60s band, three candidates: +// #16937 (explained by PR #17190's `Fixes`), #16625 (explained by PR #16698's +// `Fixes`), #16638 (the seat close above). At the measured band and with this +// guard the live board produces ZERO rows — and the clause prints the +// population so that silence is legible as a reading. +// +// ## What this row is NOT +// +// ⛔ Not a gate: report-only, exit codes untouched, no label written, nothing +// reopened. The remedy names the OWNING SEAT's act — reopen the card, restore +// its state labels — and this file performs neither. ⛔ Not a verdict either: +// the band leg is circumstantial by construction, so the row says CANDIDATE, +// prints the actor it read, and names the alternative reading. +// --------------------------------------------------------------------------- + +/** + * How far after a merge GitHub's own keyword closure lands. + * + * Measured, not chosen: 321 keyword-bound (merged PR, closed card) pairs over + * the 8-day window on 2026-09-11 sit at p50 1s / p95 2s / p99 4s, 313 of them + * inside two seconds. The banner carries the histogram and the one 31–60s + * outlier. Widening this is not a tuning knob — it is the difference between + * reporting GitHub's act and reporting a seat's, and the measured specimen for + * the wide reading is named up there. + */ +export const H59_MERGE_CLOSE_BAND_MS = 5_000; + +/** + * The per-sweep cap on timeline pages this row buys, and the page it buys. + * + * The page is ONE, deliberately. The timeline is oldest-first, so a card whose + * history exceeds a page has its `closed` event beyond it — that card is + * UNJUDGED and counted, ⛔ never clean. Measured on six live cards the page + * held 16–40 events, so the shape a normal card takes fits; the cap on CARDS + * is what bounds the run, and it is generous against the measured survivor set + * of one. + */ +export const H59_TIMELINE_PAGE_SIZE = 100; +export const H59_CARD_READ_CEILING = 40; + +/** + * The `#N` a merged PR's body points at, as numbers into the swept repo. + * + * `referencedNumbers` rather than a new scanner: one reference grammar in this + * file, shared with H40, which means fenced blocks are stripped and inline + * spans are KEPT. That asymmetry is right here for H40's reason rather than + * H7's — a mention is a POINTER A READER FOLLOWS, not an instruction to + * GitHub — and it is load-bearing for direction (a): an author who backticks + * `` `Fixes #N` `` has correctly made the keyword inert, so the number is a + * mention with NO keyword bound to it, and a card that closes anyway is + * precisely this row's subject. + */ +export function h59MentionedCards(body, repo) { + return referencedNumbers(body, repo); +} + +/** + * The `#N` a merged PR's body BINDS with a closing keyword, as numbers. + * + * `closingKeywordTargets` — the file's one closing-keyword grammar, ⛔ never a + * second spelling. Its reach is what makes direction (b) honest, and the + * negation trap is the reason: measured, `Merging this must not close #N` + * BINDS `#N` (keyword `close`), because GitHub's parser reads the keyword and + * not the surrounding prose. So a number this returns is DECLARED whatever the + * sentence around it says, and H7 is the row that tells an author so. + */ +export function h59DeclaredCloses(body) { + const out = new Map(); + for (const [digits, keyword] of closingKeywordTargets(body)) out.set(Number(digits), keyword); + return out; +} + +/** + * The NEWEST `closed` event on one page of a card's timeline, projected to the + * three fields this row reads. + * + * `complete` is the page's own honesty: a FULL page may hide a later `closed`, + * so the caller treats it as UNJUDGED rather than reading the absence of an + * event as the absence of a closure (#4690, at page granularity). + * + * @param {object[]|null|undefined} rows — one timeline page, oldest-first. + * @param {number} [pageSize] + */ +export function h59ClosedEvent(rows, pageSize = H59_TIMELINE_PAGE_SIZE) { + if (!Array.isArray(rows)) return { event: null, complete: false, read: false }; + const complete = rows.length < pageSize; + let event = null; + for (const row of rows) { + if (row?.event !== 'closed') continue; + const at = Date.parse(row?.created_at ?? ''); + event = { + actor: String(row?.actor?.login ?? '') || null, + commitId: String(row?.commit_id ?? '') || null, + atMs: Number.isFinite(at) ? at : null, + }; + } + return { event, complete, read: true }; +} + +/** + * Is this PR's merge the readable explanation for that closure? + * + * Two legs, in a deliberate precedence rather than a disjunction of equals: + * + * `commit` the `closed` event NAMES this PR's merge commit. Exact, and the + * only leg that is evidence rather than coincidence. Measured to + * fire on nothing on this board (banner) and kept because it is + * what a commit-message closure produces, which H23 already reads. + * `band` the closure sits inside `H59_MERGE_CLOSE_BAND_MS` after the + * merge. Circumstantial, measured, and never widened to a minute. + * + * A closure BEFORE the merge is not attributable in either leg, and the test is + * one-sided for that reason: GitHub closes after merging, never before. + * + * @returns {{ attributed: boolean, leg: 'commit'|'band'|null, lagMs: number|null }} + */ +export function h59CloseAttribution(pr, closedAtMs, closedEvent, bandMs = H59_MERGE_CLOSE_BAND_MS) { + const mergedAt = Date.parse(pr?.merged_at ?? ''); + if (!Number.isFinite(mergedAt)) return { attributed: false, leg: null, lagMs: null }; + const sha = String(pr?.merge_commit_sha ?? '').toLowerCase(); + const stamped = String(closedEvent?.commitId ?? '').toLowerCase(); + const at = Number.isFinite(closedAtMs) ? closedAtMs : (closedEvent?.atMs ?? NaN); + const lagMs = Number.isFinite(at) ? at - mergedAt : null; + if (sha.length >= 7 && stamped.length >= 7 && (sha.startsWith(stamped) || stamped.startsWith(sha))) { + return { attributed: true, leg: 'commit', lagMs }; + } + if (lagMs === null || lagMs < 0 || lagMs > bandMs) return { attributed: false, leg: null, lagMs }; + return { attributed: true, leg: 'band', lagMs }; +} + +/** + * Which merged PRs in the window DECLARED that closure — the sibling guard. + * + * A card closed inside one PR's band may have been closed by a neighbour that + * asked for it properly, and PRs land in batches (two merged in the same second + * on this board). Attributing a correct landing to the PR that merely mentioned + * the card is a false row about work that went right, so any declaring sibling + * inside the band EXPLAINS the closure and this row goes quiet. + * + * @returns {number[]} the declaring PR numbers, ascending. + */ +export function h59ExplainingPullRequests(cardNumber, closedAtMs, mergedWindow, bandMs = H59_MERGE_CLOSE_BAND_MS) { + const at = Number.isFinite(closedAtMs) ? closedAtMs : NaN; + if (!Number.isFinite(at)) return []; + const out = []; + for (const pr of Array.isArray(mergedWindow) ? mergedWindow : []) { + if (!h59DeclaredCloses(pr?.body ?? '').has(Number(cardNumber))) continue; + const mergedAt = Date.parse(pr?.merged_at ?? ''); + if (!Number.isFinite(mergedAt)) continue; + if (Math.abs(at - mergedAt) > bandMs) continue; + out.push(Number(pr.number)); + } + return out.sort((a, b) => a - b); +} + +/** + * Direction (a) — FALSE CLOSE. The expensive half. + * + * ⛔ The remedy is the OWNING SEAT's act and is named as one: reopen the card + * and restore the state its labels claimed. This file reopens nothing, writes + * no label, and says so in the sentence rather than only in its banner. + */ +export function h59FalseCloseRow({ pr, card, attribution, closedEvent, closedAtMs }) { + const lag = attribution?.lagMs === null || attribution?.lagMs === undefined + ? 'an unreadable interval' + : `${Math.round(attribution.lagMs / 1000)}s`; + const evidence = + attribution?.leg === 'commit' + ? `the \`closed\` event NAMES this PR's merge commit \`${String(closedEvent?.commitId ?? '').slice(0, 10)}\`` + : `the \`closed\` event carries NO \`commit_id\` (the measured shape on this board) and landed ${lag} ` + + `after the merge, inside the measured ${H59_MERGE_CLOSE_BAND_MS / 1000}s band`; + const actor = closedEvent?.actor ? `\`${closedEvent.actor}\`` : 'an unreadable actor'; + return ( + `FALSE CLOSE (candidate): merged PR #${pr?.number} MENTIONS #${card?.number} and binds NO closing ` + + `keyword to that number anywhere in its body, yet the card closed with the merge — ${evidence}, ` + + `actor ${actor}, and no other PR in the merged window declared that closure. ` + + 'This is the expensive direction: work disappears from the board while reading as done, and it is ' + + 'the one failure no other row here can catch, because a CLOSED card is swept by nothing. ' + + 'GitHub\'s closing linkage is unreliable in both directions and ⛔ cannot be reasoned about from a ' + + 'PR\'s text — the absence of a keyword is not the absence of a closure. ' + + 'Remedy, and it belongs to the SEAT THAT OWNS the card: re-read the card against what actually ' + + 'landed, REOPEN it if its work is not done, and restore the state labels the close dropped. ' + + '⛔ This row reopens nothing, writes no label and relabels nothing — it reads, and hands the act ' + + 'to the seat that can judge it. ' + + '⚠️ CANDIDATE, never a verdict: the band leg is circumstantial by construction, so a seat closing ' + + 'this card by hand in the same seconds reads the same way here. The actor above is printed for ' + + 'exactly that judgement, and a closure outside this row\'s two windows is invisible to it.' + ); +} + +/** + * Direction (b) — FALSE OPEN. The cheap half, and it costs no request at all. + * + * The card's presence in the open listing IS the reading, so a truncated + * listing can only lose a row and never invent one. + */ +export function h59FalseOpenRow({ pr, card, keyword }) { + return ( + `FALSE OPEN: merged PR #${pr?.number} binds \`${keyword} #${card?.number}\` in its body, and the card ` + + 'is STILL OPEN after that merge — GitHub did not perform the closure the PR declared. ' + + 'This is the cheaper direction of the same unreliable linkage: it surfaces as queue noise and ' + + 'somebody eventually re-reads the card, where the opposite direction disappears silently. ' + + '⚠️ The keyword is read by the same grammar GitHub uses, which BINDS a number regardless of the ' + + 'prose around it — 「Merging this must not close #N」 declares a closure — so a bound number is ' + + 'DECLARED here whatever the sentence meant. That is what makes this direction honest rather than ' + + 'a guess about intent, and H7 is the row that warns an author before the merge. ' + + 'Remedy, and it belongs to the SEAT THAT OWNS the card: close it if the work landed, or drop the ' + + 'keyword to `Part of` / `Refs` if it did not. ⛔ This row closes nothing and writes no label. ' + + '⚠️ A card whose closure this sweep could not place is UNJUDGED and counted, never clean.' + ); +} + +/** + * H59's population reading, as the summary line prints it. + * + * UNCONDITIONAL, and every number is a bound on what the row could have said. + * The two directions cost very different things and the clause keeps them + * apart: direction (b) is answered by listings already in hand, direction (a) + * buys at most `H59_CARD_READ_CEILING` timeline pages and declares every way a + * mention left it unjudged. + */ +export function h59LinkageClause(counts = {}) { + const judged = counts.linkageJudged ?? 0; + const declared = counts.linkageDeclared ?? 0; + const outOfWindow = counts.linkageOutOfWindow ?? 0; + const read = counts.linkageCardsRead ?? 0; + const unread = counts.linkageUnread ?? 0; + const short = counts.linkageShort ?? 0; + return ( + `${declared} declared closure(s) and ${judged} keyword-LESS mention(s) from the bodies in H8's ` + + 'merged window were judged against the open listing this sweep already holds; a declared target ' + + 'still on that listing is a FALSE OPEN row and costs no request. ' + + `${outOfWindow} further mention(s) named a card that is neither open nor inside H22's ` + + `${CLOSED_ISSUE_WINDOW_DAYS}-day closed window, so their closure could not be PLACED and they are ` + + 'UNJUDGED rather than clean — this row\'s real reach is the SHORTER of the two windows, ⛔ never ' + + `H8's ${MERGED_WINDOW_DAYS} days. Of the mentions that could be placed, only a closure inside the ` + + `measured ${H59_MERGE_CLOSE_BAND_MS / 1000}s band buys evidence: ${read} card(s) had ONE timeline ` + + `page read (cap ${H59_CARD_READ_CEILING} card(s) per sweep, ⛔ no retry on any status), ` + + `${unread} could not be read and ${short} were past that cap — all UNJUDGED, never clean. ` + + 'A declaring SIBLING PR inside the same band EXPLAINS a closure and this row stays quiet, because ' + + 'batched landings put two merges in one second. ⚠️ The `closed` event carries no `commit_id` on ' + + 'this board (measured), so the commit leg is exact where it fires and fires on nothing here; the ' + + 'band leg is circumstantial and every row says so. Rows are a LOWER BOUND.' + ); +} // --------------------------------------------------------------------------- // Report rendering — pure over (findings, counts), so `--self-test` pins both // media offline. The live sweep below picks a renderer and prints it; nothing @@ -12564,6 +12919,21 @@ export const SWEEP_COUNT_KEYS = [ 'rulingMarkerJudged', 'rulingMarkerThreadRead', 'rulingMarkerUnread', + // H59's coverage set (#17466). The row spans TWO windows and the pair that + // matters most is `linkageJudged`/`linkageOutOfWindow`: a mention whose card + // is neither open nor inside H22's closed window cannot have its closure + // PLACED at all, and a pass that printed only what it could place would read + // as a sweep of the whole merged window — the #4690 inversion in this row's + // own uniform. `linkageDeclared` is direction (b)'s judged population, which + // costs nothing; `linkageCardsRead` is what the one purchase actually bought, + // and `linkageUnread`/`linkageShort` are the two ways a card inside the + // attribution band still leaves this row UNJUDGED rather than clean. + 'linkageDeclared', + 'linkageJudged', + 'linkageOutOfWindow', + 'linkageCardsRead', + 'linkageUnread', + 'linkageShort', 'refBeyond', ]; @@ -12639,8 +13009,9 @@ export function summaryLine(counts, findingCount) { const crossRepoProbed = counts.crossRepoProbed ?? 0; const crossRepoUnreadable = counts.crossRepoUnreadable ?? 0; // H35's event window (#11881). Reported as a pair for the same reason every - // pair above is: this is the file's ONLY reader of event history, so if the - // window came up short there is no second reader to notice. `truncated` means + // pair above is: this is the file's only REPO-WIDE reader of event history + // (H59's one timeline page is per-card and answers a different question), so + // if the window came up short there is no second reader to notice. `truncated` means // the page cap bound before the horizon was reached — the run saw less than // its stated 12h and must not read as a board with no gate removals in it. // The `unjudgeable` count is carried into the summary deliberately: it is the @@ -13029,6 +13400,12 @@ export function summaryLine(counts, findingCount) { 'whose thread no other row bought has an UNREAD second channel rather than a clean one, and the ' + 'rows are a LOWER BOUND. It matches ATX HEADINGS outside fenced code only: a marker quoted in ' + 'prose, in a table, in a code span or in a blockquote is invisible here by design. ' + + // H59's coverage reading (#17466). UNCONDITIONAL like every other window's, + // and it is the only place a reader can see that this row spans TWO windows + // and reaches only as far as the shorter one. It also carries the one + // purchase in this file that is a per-card timeline page, its cap, and the + // measured fact that the commit leg fires on nothing here. + `Merged-PR closing linkage (H59): ${h59LinkageClause(counts)} ` + `Report-only: findings are patrol input, not a gate verdict.` ); } @@ -13090,6 +13467,7 @@ export const SUMMARY_CLAUSE_ANCHORS = [ ['h53Carrier', 'Carriers without increment (H53): '], ['h57Scheduled', 'Scheduled non-blocking workflows (H57): '], ['h58RulingMarkers', 'Queued ruling markers (H58): '], + ['h59Linkage', 'Merged-PR closing linkage (H59): '], ['reportOnly', 'Report-only: '], ]; @@ -13587,6 +13965,35 @@ export const HALF_STATE_FAMILY_BAND = Object.freeze({ // band, the row this one is the body-and-thread half of. H58: 'state', + // H59 is a `state` (#17466), and the three refusals are each taken on the + // refused band's OWN criterion rather than on this subject's vocabulary. + // + // ⛔ NOT `gate`, and this is the close one, because the FALSE CLOSE direction + // really is an absence reading as a green light: a card vanishes off the board + // and its silence reads as delivered. That band's criterion has two halves and + // this row satisfies only the second — 「the row's SUBJECT is a GATE that may + // have been stripped or split」 — and this row's subject is a CARD's state, not + // a required check. H31/H35 are about a gate that decides whether something may + // land; nothing here decides that. Reading `gate` off the second half alone + // would make the band mean 「anything protective」, which is H57's refusal taken + // for H57's reason: the band stays about the required set. + // ⛔ NOT `stall`: that criterion is forward motion STOPPED with nothing else to + // move it, and the FALSE CLOSE direction is the opposite failure — the card was + // moved, wrongly and all the way to done. Nothing is waiting on a scan; what is + // wrong is the state the board now shows. FALSE OPEN is not a stall either: the + // card is open, visible, and every queue read still reaches it. + // ⛔ NOT `inventory`: the row alarms about ONE card and one PR, not a + // population. The population reading — the two windows, the mentions that could + // not be placed, the cards read — is a summary clause and takes no band at all + // (H39's shape). + // + // What is left is `state`'s criterion exactly: the board contradicts the record + // of what merged, on ONE card, and the repair is on the board — a seat reopens + // it and restores its labels, or closes it. And it is H8's band, the row this + // one is the closed-card half of: H8 reads a merged PR against a card that is + // still open, this reads the same merged window against a card that is not. + H59: 'state', + H5: 'inventory', H6: 'inventory', H11: 'inventory', @@ -16293,9 +16700,12 @@ async function listRecentDefaultBranchCommits(stats = {}, nowMs = Date.now()) { } /** - * H35's repo-wide issue-event window — the ONLY reader of event history in this - * file, and deliberately not a per-card timeline fetch (H35's header carries - * the reasoning; H15 and H16 decline the per-card shape by name). + * H35's repo-wide issue-event window — the only REPO-WIDE reader of event + * history in this file, and deliberately not a per-card timeline fetch (H35's + * header carries the reasoning; H15 and H16 decline the per-card shape by name). + * ⚠️ H59 does buy a per-card timeline page since #17466, for a bounded set of + * cards a merged PR's body names; it reads a `closed` event and never a label + * one, so nothing here may be answered from it. * * TIME-bounded with a PAGE cap behind it, rather than pages alone. The stream * is strictly newest-first, so the horizon is reached by reading until a row @@ -17992,6 +18402,105 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen if (bare) findings.push([issue, 'H53', bare]); } + // H59 (#17466) — the merged window read a second way: not "did the card's + // paired write land" (H8) but "did GitHub's closing linkage tell the truth + // about this card at all", in both directions. + // + // Placed HERE and not beside H8 because it needs three collections H8 does + // not: the unscoped OPEN listing (the free answer to direction (b)), the + // CLOSED window (the free `closed_at` that decides whether direction (a) is + // worth a request), and the open-PR listing — and all three exist only after + // the passes above. Like H53 it BUYS a fetch, so the foot is where its + // purchase is provably the smallest: everything free has already been read. + const openCardNumbers = new Set([...seenUnscoped.keys(), ...seen.keys()]); + const knownPrNumbers = new Set([...seenPrs.keys(), ...seenMerged.keys()]); + // Distinct cards that survived every free narrowing, each with the PR whose + // merge is the candidate explanation. A card reached by two PRs keeps the + // FIRST, so the cap eats a stable set rather than a set that moves with + // iteration order. + const linkageReads = new Map(); + for (const pr of mergedWindow) { + const body = pr?.body ?? ''; + const declared = h59DeclaredCloses(body); + for (const [number, keyword] of declared) { + if (number === Number(pr?.number)) continue; + stats.linkageDeclared = (stats.linkageDeclared ?? 0) + 1; + // Direction (b) — FALSE OPEN. Positive membership in the open listing IS + // the reading, so a truncated listing loses a row and never invents one. + const open = seenUnscoped.get(number) ?? seen.get(number); + if (open) findings.push([open, 'H59', h59FalseOpenRow({ pr, card: open, keyword })]); + } + for (const number of h59MentionedCards(body, OWNER_REPO)) { + if (number === Number(pr?.number)) continue; + if (declared.has(number)) continue; + if (knownPrNumbers.has(number)) continue; + if (openCardNumbers.has(number)) { + // Open and unbound is the healthy shape this row is silent about, and + // it is judged rather than skipped — the clause counts it. + stats.linkageJudged = (stats.linkageJudged ?? 0) + 1; + continue; + } + const closedCard = seenClosed.get(number); + if (!closedCard) { + // Neither open nor inside H22's window: the closure cannot be PLACED, + // so it is UNJUDGED and counted — ⛔ never clean. + stats.linkageOutOfWindow = (stats.linkageOutOfWindow ?? 0) + 1; + continue; + } + stats.linkageJudged = (stats.linkageJudged ?? 0) + 1; + const closedAtMs = Date.parse(closedCard?.closed_at ?? ''); + if (!Number.isFinite(closedAtMs)) { + stats.linkageUnread = (stats.linkageUnread ?? 0) + 1; + continue; + } + // The free half of the attribution: a closure outside the measured band + // is not this merge's, and buys nothing. + if (!h59CloseAttribution(pr, closedAtMs, null).attributed) continue; + // A sibling PR that DECLARED the closure explains it — batched landings + // put two merges in one second, and a correct landing is not a row. + if (h59ExplainingPullRequests(number, closedAtMs, mergedWindow).length > 0) continue; + if (!linkageReads.has(number)) linkageReads.set(number, { pr, card: closedCard, closedAtMs }); + } + } + for (const [number, candidate] of linkageReads) { + if ((stats.linkageCardsRead ?? 0) >= H59_CARD_READ_CEILING) { + stats.linkageShort = (stats.linkageShort ?? 0) + 1; + continue; + } + let page; + try { + page = await rest( + `/repos/${OWNER_REPO}/issues/${number}/timeline?per_page=${H59_TIMELINE_PAGE_SIZE}&page=1`, + ); + } catch { + // ⛔ No retry loop on any status (#17374). A failed read leaves the card + // UNJUDGED and the coverage numbers are what say so. + stats.linkageUnread = (stats.linkageUnread ?? 0) + 1; + continue; + } + stats.linkageCardsRead = (stats.linkageCardsRead ?? 0) + 1; + const { event, complete } = h59ClosedEvent(page); + // A FULL page may hide a later `closed`, and an absent event on a card the + // closed listing says is closed is a page that did not reach it. + if (!complete || !event) { + stats.linkageUnread = (stats.linkageUnread ?? 0) + 1; + continue; + } + const attribution = h59CloseAttribution(candidate.pr, candidate.closedAtMs, event); + if (!attribution.attributed) continue; + findings.push([ + candidate.card, + 'H59', + h59FalseCloseRow({ + pr: candidate.pr, + card: candidate.card, + attribution, + closedEvent: event, + closedAtMs: candidate.closedAtMs, + }), + ]); + } + // H45's second half — the `pm:epic` index, read as its own population. // // LAST in the sweep, and that placement is the mechanism rather than a @@ -25840,6 +26349,179 @@ Mutual exclusion: \`get_comments\` page 747 → \`[]\`, page 746 = my own R+117 t('H58 band: a gate row still outranks it, so H31/H35 survive the trim longer', familyRank('H31') < familyRank('H58'), true); t('H58 band: …and it outranks an inventory row', familyRank('H58') < familyRank('H14'), true); + // -- H59 — the merged PR whose closing linkage mis-states a card (#17466) --- + // ⛔ The self-test never touches GitHub. The two fixtures carry the two + // MEASURED shapes from the filing card's sibling-board evidence — a merge + // that closed a card it never asked to close, and a merge that left its + // declared target open — spelled here with this board's own measured + // `closed`-event shape (no `commit_id`, a 1–2s lag). + const NOW59 = Date.parse('2026-09-10T23:44:35Z'); + const MERGED59 = '2026-09-10T23:44:33Z'; + // Direction (a): the PR mentions the card and binds NOTHING to it. + const pr59FalseClose = { + number: 1810, + merged_at: MERGED59, + merge_commit_sha: 'd8a833d3d3c0ffee0123456789abcdef01234567', + body: 'Part of #1807\n\nThe follow-up this leaves behind is tracked on #1808.\n', + }; + // Direction (b): the PR binds a keyword and the card is still open. + const pr59FalseOpen = { + number: 1811, + merged_at: MERGED59, + merge_commit_sha: 'e66da5c919c0ffee0123456789abcdef01234567', + body: 'Fixes #1792\n\nThe whole card lands here.\n', + }; + const card59Closed = { + number: 1808, + state: 'closed', + closed_at: '2026-09-10T23:44:35Z', + html_url: 'https://example.invalid/1808', + }; + const card59Open = { number: 1792, state: 'open', html_url: 'https://example.invalid/1792' }; + const closedEvent59 = (over = {}) => ({ + event: 'closed', + actor: { login: 'os-sales' }, + commit_id: null, + created_at: '2026-09-10T23:44:35Z', + ...over, + }); + const page59 = (...rows) => [{ event: 'labeled', created_at: '2026-09-08T03:34:57Z' }, ...rows]; + const REPO59 = 'objectstack-ai/objectstack'; + + // The two extractors — one reference grammar, one closing-keyword grammar. + t('H59 mentions: a bare `#N` in prose is a mention', h59MentionedCards('tracked on #1808.', REPO59).has(1808), true); + t('H59 mentions: …and so is a qualified one naming THIS repo', h59MentionedCards(`see ${REPO59}#1808`, REPO59).has(1808), true); + t('H59 mentions: ⛔ a SIBLING repo qualifier is not a mention here', h59MentionedCards('see objectstack-ai/objectui#1808', REPO59).has(1808), false); + t('H59 mentions: ⛔ a fenced number is not a mention', h59MentionedCards('```\nFixes #1808\n```\n', REPO59).has(1808), false); + t('H59 mentions: a BACKTICKED one still is — a pointer a reader follows', h59MentionedCards('the inert `Fixes #1808` above', REPO59).has(1808), true); + t('H59 declares: `Fixes #N` binds, and the keyword is carried', h59DeclaredCloses('Fixes #1792').get(1792), 'Fixes'); + t('H59 declares: ⛔ `Part of #N` binds nothing', h59DeclaredCloses('Part of #1807').size, 0); + t('H59 declares: ⛔ nor does `Refs #N`', h59DeclaredCloses('Refs #1807').size, 0); + t('H59 declares: ⛔ nor a BACKTICKED keyword — GitHub does not fire in a span', h59DeclaredCloses('the inert `Fixes #1808`').size, 0); + // ⭐ The negation trap, measured: the grammar binds a number regardless of the + // prose around it, which is what makes direction (b) honest rather than a + // guess about intent. + t('H59 declares: ⭐ a NEGATED keyword still BINDS — the trap this card is about', h59DeclaredCloses('Merging this must not close #1792').get(1792), 'close'); + t('H59 declares: the one grammar is shared, never re-spelled', h59DeclaredCloses('Fixes #1792').get(1792), closingKeywordTargets('Fixes #1792').get('1792')); + + // The `closed` event reader, and the page honesty that bounds it. + t('H59 event: the newest `closed` on the page is the one read', h59ClosedEvent(page59(closedEvent59({ created_at: '2026-01-01T00:00:00Z' }), closedEvent59())).event.atMs, NOW59); + t('H59 event: the actor is carried, because the row prints it', h59ClosedEvent(page59(closedEvent59())).event.actor, 'os-sales'); + t('H59 event: a null `commit_id` reads as absent, never as a string', h59ClosedEvent(page59(closedEvent59())).event.commitId, null); + t('H59 event: …and a stamped one is carried', h59ClosedEvent(page59(closedEvent59({ commit_id: 'd8a833d3d3c0ffee0123456789abcdef01234567' }))).event.commitId, 'd8a833d3d3c0ffee0123456789abcdef01234567'); + t('H59 event: a SHORT page is complete — the whole timeline was read', h59ClosedEvent(page59(closedEvent59())).complete, true); + t('H59 event: ⛔ a FULL page is NOT complete, so an absent close is unread', h59ClosedEvent(Array.from({ length: H59_TIMELINE_PAGE_SIZE }, () => ({ event: 'labeled' }))).complete, false); + t('H59 event: an unreadable page is not a crash, and never a reading', h59ClosedEvent(null).read, false); + t('H59 event: a page with no close returns none', h59ClosedEvent(page59()).event, null); + + // Attribution — two legs, in precedence, one-sided in time. + const attr59 = (closedAt, event) => h59CloseAttribution(pr59FalseClose, Date.parse(closedAt), event); + t('H59 attribution: the COMMIT leg is exact when GitHub stamps it', attr59('2026-09-12T00:00:00Z', { commitId: 'd8a833d3d3c0ffee0123456789abcdef01234567' }).leg, 'commit'); + t('H59 attribution: …and an abbreviated stamp still matches its own sha', attr59('2026-09-12T00:00:00Z', { commitId: 'd8a833d3' }).leg, 'commit'); + t('H59 attribution: ⛔ a DIFFERENT commit does not match', attr59('2026-09-12T00:00:00Z', { commitId: 'ffffffffff' }).attributed, false); + t('H59 attribution: ⛔ a stamp shorter than 7 hex is no evidence', attr59('2026-09-12T00:00:00Z', { commitId: 'd8a833' }).attributed, false); + t('H59 attribution: the BAND leg carries this board — no `commit_id` at all', attr59('2026-09-10T23:44:35Z', { commitId: null }).leg, 'band'); + t('H59 attribution: …and reports the measured lag', attr59('2026-09-10T23:44:35Z', { commitId: null }).lagMs, 2000); + t('H59 attribution: a close ON the merge instant is inside the band', attr59(MERGED59, null).attributed, true); + t('H59 attribution: a close at the band edge is still inside', attr59('2026-09-10T23:44:38Z', null).attributed, true); + t('H59 attribution: ⛔ one second past it is NOT', attr59('2026-09-10T23:44:39Z', null).attributed, false); + // ⭐ The measured specimen that killed the ruling's «merge minute»: a seat + // closing a card 53s after the merge is the protocol working, not a defect. + t('H59 attribution: ⛔ the 53s SEAT close is refused — a minute admits another actor', attr59('2026-09-10T23:45:26Z', null).attributed, false); + t('H59 attribution: ⛔ a close BEFORE the merge is not this merge\'s', attr59('2026-09-10T23:44:30Z', null).attributed, false); + t('H59 attribution: an unreadable `merged_at` attributes nothing', h59CloseAttribution({ merged_at: 'not a date' }, NOW59, null).attributed, false); + t('H59 attribution: the band is the MEASURED 5s, not a minute', H59_MERGE_CLOSE_BAND_MS, 5_000); + + // The sibling guard — batched landings put two merges in one second. + const window59 = [pr59FalseClose, { number: 1812, merged_at: MERGED59, body: 'Fixes #1808' }]; + t('H59 sibling: a declaring PR inside the band EXPLAINS the closure', h59ExplainingPullRequests(1808, NOW59, window59).join(','), '1812'); + t('H59 sibling: ⛔ a MENTIONING PR explains nothing', h59ExplainingPullRequests(1808, NOW59, [pr59FalseClose]).length, 0); + t('H59 sibling: a declaring PR OUTSIDE the band explains nothing either', h59ExplainingPullRequests(1808, NOW59, [{ number: 1812, merged_at: '2026-09-10T23:40:00Z', body: 'Fixes #1808' }]).length, 0); + t('H59 sibling: an unreadable closure instant is not explained away', h59ExplainingPullRequests(1808, NaN, window59).length, 0); + t('H59 sibling: an empty window is not a crash', h59ExplainingPullRequests(1808, NOW59, undefined).length, 0); + + // ⭐ Direction (a) — FALSE CLOSE, the expensive half, on the filing shape. + const row59a = h59FalseCloseRow({ + pr: pr59FalseClose, + card: card59Closed, + attribution: attr59('2026-09-10T23:44:35Z', { commitId: null }), + closedEvent: h59ClosedEvent(page59(closedEvent59())).event, + closedAtMs: NOW59, + }); + t('H59 (a): the fixture PR binds NO keyword to the card it closed', h59DeclaredCloses(pr59FalseClose.body).has(1808), false); + t('H59 (a): …while still MENTIONING it', h59MentionedCards(pr59FalseClose.body, REPO59).has(1808), true); + t('H59 (a): the row names both sides', row59a.includes('merged PR #1810 MENTIONS #1808'), true); + t('H59 (a): it says the body bound NO keyword', row59a.includes('binds NO closing keyword'), true); + t('H59 (a): it prints the measured lag rather than asserting a mechanism', row59a.includes('landed 2s after the merge'), true); + t('H59 (a): it says the event carried no `commit_id` — this board\'s shape', row59a.includes('NO `commit_id`'), true); + t('H59 (a): it prints the ACTOR, because that is the reader\'s discriminator', row59a.includes('actor `os-sales`'), true); + t('H59 (a): it names why this is the expensive direction', row59a.includes('a CLOSED card is swept by nothing'), true); + t('H59 (a): the remedy is the OWNING SEAT\'s act, named as one', row59a.includes('belongs to the SEAT THAT OWNS the card'), true); + t('H59 (a): …and it is REOPEN plus the labels the close dropped', row59a.includes('REOPEN it') && row59a.includes('restore the state labels'), true); + t('H59 (a): ⛔ this row reopens nothing itself', row59a.includes('reopens nothing, writes no label and relabels nothing'), true); + t('H59 (a): ⚠️ it is a CANDIDATE and says so', row59a.includes('CANDIDATE, never a verdict'), true); + t('H59 (a): …naming the alternative reading a seat must rule out', row59a.includes('closing this card by hand in the same seconds'), true); + t('H59 (a): a COMMIT-leg row says the event named the merge commit instead', h59FalseCloseRow({ pr: pr59FalseClose, card: card59Closed, attribution: attr59('2026-09-12T00:00:00Z', { commitId: 'd8a833d3d3c0ffee0123456789abcdef01234567' }), closedEvent: { commitId: 'd8a833d3d3c0ffee0123456789abcdef01234567', actor: 'os-sales' } }).includes('NAMES this PR\'s merge commit `d8a833d3d3`'), true); + t('H59 (a): an unreadable actor is said to be unreadable, never omitted', h59FalseCloseRow({ pr: pr59FalseClose, card: card59Closed, attribution: attr59('2026-09-10T23:44:35Z', null), closedEvent: { actor: null, commitId: null } }).includes('an unreadable actor'), true); + t('H59 (a): ⛔ no angle-bracket placeholder reaches the row body', /[<>]/.test(row59a), false); + t('H59 (a): not a loud finding', isLoudFinding(row59a), false); + + // Direction (b) — FALSE OPEN, the cheap half. + const row59b = h59FalseOpenRow({ pr: pr59FalseOpen, card: card59Open, keyword: h59DeclaredCloses(pr59FalseOpen.body).get(1792) }); + t('H59 (b): the fixture PR really binds the keyword', h59DeclaredCloses(pr59FalseOpen.body).get(1792), 'Fixes'); + t('H59 (b): the row quotes the binding as written', row59b.includes('binds `Fixes #1792`'), true); + t('H59 (b): it says the card is still open after the merge', row59b.includes('STILL OPEN after that merge'), true); + t('H59 (b): it states this is the CHEAPER direction', row59b.includes('cheaper direction'), true); + t('H59 (b): it warns that the grammar ignores the prose around the keyword', row59b.includes('BINDS a number regardless of the'), true); + t('H59 (b): …quoting the negation trap itself', row59b.includes('Merging this must not close #N'), true); + t('H59 (b): the remedy is the OWNING SEAT\'s act', row59b.includes('belongs to the SEAT THAT OWNS the card'), true); + t('H59 (b): ⛔ this row closes nothing', row59b.includes('closes nothing and writes no label'), true); + t('H59 (b): ⛔ no angle-bracket placeholder reaches the row body', /[<>]/.test(row59b), false); + t('H59 (b): not a loud finding', isLoudFinding(row59b), false); + + // The two directions are DISTINCT sentences — neither restates the other. + t('H59 directions: the two rows do not share an opening', row59a.slice(0, 20) === row59b.slice(0, 20), false); + + // The clean merged PR, both ways — the controls that keep the row falsifiable. + t('H59 clean: a keyword-bound card that is CLOSED is not a false open', h59DeclaredCloses('Fixes #1808').has(1792), false); + t('H59 clean: a mentioned card closed OUTSIDE the band is not attributed', attr59('2026-09-11T09:00:00Z', { commitId: null }).attributed, false); + t('H59 clean: …and a declaring sibling inside the band silences the row', h59ExplainingPullRequests(1808, NOW59, window59).length > 0, true); + + // Census and forwarding. + t('H59: every count key rides the enumerated forwarding contract', ['linkageDeclared', 'linkageJudged', 'linkageOutOfWindow', 'linkageCardsRead', 'linkageUnread', 'linkageShort'].every((k) => SWEEP_COUNT_KEYS.includes(k)), true); + const SUM59 = saidBy('h59Linkage', summaryLine({ linkageDeclared: 1106, linkageJudged: 479, linkageOutOfWindow: 2803, linkageCardsRead: 1, linkageUnread: 0, linkageShort: 0 }, 0)); + t('H59 census: the declared population is reported', SUM59.includes('1106 declared closure(s)'), true); + t('H59 census: …the keyword-less mentions it could place', SUM59.includes('479 keyword-LESS mention(s)'), true); + t('H59 census: …and the ones it could NOT place, as UNJUDGED', SUM59.includes('2803 further mention(s)') && SUM59.includes('UNJUDGED rather than clean'), true); + t('H59 census: it names the SHORTER of the two windows as its reach', SUM59.includes('SHORTER of the two windows'), true); + t('H59 census: …and refuses to be read as H8\'s window', SUM59.includes(`⛔ never H8's ${MERGED_WINDOW_DAYS} days`), true); + t('H59 census: the purchase is stated with its cap', SUM59.includes(`1 card(s) had ONE timeline page read (cap ${H59_CARD_READ_CEILING} card(s) per sweep`), true); + t('H59 census: ⛔ no retry loop, said in the clause', SUM59.includes('no retry on any status'), true); + t('H59 census: the two unjudged ways are both reported', SUM59.includes('0 could not be read and 0 were past that cap'), true); + t('H59 census: the sibling guard is disclosed', SUM59.includes('declaring SIBLING PR inside the same band EXPLAINS'), true); + t('H59 census: …and so is the measured absence of `commit_id`', SUM59.includes('carries no `commit_id` on'), true); + t('H59 census: rows are declared a LOWER BOUND', SUM59.includes('Rows are a LOWER BOUND'), true); + t('H59 census: the clause renders on a zero run — silence is never absence', saidBy('h59Linkage', summaryLine({}, 0)).includes('0 declared closure(s) and 0 keyword-LESS mention(s)'), true); + t('H59 census: the band in the clause comes from the constant', saidBy('h59Linkage', summaryLine({}, 0)).includes(`measured ${H59_MERGE_CLOSE_BAND_MS / 1000}s band`), true); + + // Band. + t('H59 band: registered as a STATE row — the repair is on the board', familyBand('H59'), 'state'); + t('H59 band: ⛔ NOT `gate` — the subject is a card\'s state, not a required check', familyBand('H59') === 'gate', false); + t('H59 band: ⛔ NOT `stall` — the card was MOVED, wrongly, not halted', familyBand('H59') === 'stall', false); + t('H59 band: ⛔ NOT `inventory` — the row alarms per card; the census is a clause', familyBand('H59') === 'inventory', false); + t('H59 band: …and it is H8\'s band, the row this one is the closed-card half of', familyBand('H59'), familyBand('H8')); + t('H59 band: the sweep really pushes it, so the registry sees it', familyRegistryCoverage().emitted.includes('H59'), true); + t('H59 band: no code is left unregistered by this change', familyRegistryCoverage().missing.length, 0); + t('H59 band: …and no band names a family the sweep never emits', familyRegistryCoverage().extra.length, 0); + t('H59 band: the registry still fits inside the ledger ROW CAP', Object.keys(HALF_STATE_FAMILY_BAND).length <= FAMILY_LEDGER_ROW_CAP, true); + t('H59 band: a gate row still outranks it, so H31/H35 survive the trim longer', familyRank('H31') < familyRank('H59'), true); + t('H59 band: …and it outranks an inventory row', familyRank('H59') < familyRank('H14'), true); + + // Adjacency — ⛔ this row restates no neighbour's verdict on its own specimen. + t('H59 adjacency: H7 is silent — the false-close body carries no `Part of`+keyword clash', h7PartOfWithClosingKeyword(pr59FalseClose), null); + t('H59 adjacency: …and silent on the false-OPEN body too — no `Part of` at all', h7PartOfWithClosingKeyword(pr59FalseOpen), null); + t('H59 adjacency: the one grammar is shared with H7/H8/H23 rather than re-spelled', closingKeywordTargets(pr59FalseOpen.body).has('1792'), true); + // -- The `[::]` collapse (#12090): behaviour-preserving, asserted as such --- // The class held U+003A TWICE, never the fullwidth U+FF1A its shape implied. // These cases pin that the collapse changed nothing a reader could observe.