From 0034eb6ccbaae2f9fd35d0b21be4a21b3024fd13 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 12:45:42 +0000 Subject: [PATCH] fix(pm): re-sync the ported half-state sweeper and pin it against upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ported `scripts/pm/check-half-states.mjs` had drifted 4,637 lines behind objectstack with nothing able to detect it. Re-synced verbatim to a named upstream commit, preserving the declared divergences, and added a parity gate so the next drift is red rather than silent. Measured before, on 2026-08-28: ported copy 9,340 lines / 1,116 self-test cases objectstack upstream 12,948 lines / 1,574 self-test cases diff between them 4,637 lines The re-sync is mechanical: upstream's blob plus 14 declared divergences, each an exact text pair, applied forward. `scripts/upstream-port-pin.json` carries the upstream commit, the SHA-256 of its blob and those pairs; `scripts/check-upstream-port-parity.mjs` reverses them out of the ported files and requires the reconstruction to hash to the pinned digest. It fetches nothing — the digest is the input. `scripts/invoked-as.mjs` needed no re-sync: its predicate is already byte-identical to upstream's. Its nine port adaptations (prose and one fixture) are declared in the same pin, so it is covered too. Three divergences are new and were DISCOVERED by the re-sync rather than authored by it: upstream's H32 self-test rows compare a seat title's `@ ` suffix against the LIVE resolved sweep repo, so their specimens invert here and a verbatim copy fails its own suite. That class is upstream's to retire. Co-Authored-By: Claude --- .github/workflows/half-state-patrol.yml | 48 +- .github/workflows/lint.yml | 38 + package.json | 1 + scripts/__tests__/check-half-states.test.ts | 74 +- .../upstream-port-parity-wiring.test.ts | 115 + scripts/check-upstream-port-parity.mjs | 601 +++ scripts/pm/check-half-states.mjs | 4503 +++++++++++++++-- scripts/upstream-port-pin.json | 160 + 8 files changed, 5152 insertions(+), 388 deletions(-) create mode 100644 scripts/__tests__/upstream-port-parity-wiring.test.ts create mode 100644 scripts/check-upstream-port-parity.mjs create mode 100644 scripts/upstream-port-pin.json diff --git a/.github/workflows/half-state-patrol.yml b/.github/workflows/half-state-patrol.yml index acc7895b11..f44e3d2d8c 100644 --- a/.github/workflows/half-state-patrol.yml +++ b/.github/workflows/half-state-patrol.yml @@ -8,8 +8,31 @@ name: Half-State Patrol # prose below is upstream's and its issue numbers (#9844, #4449, #9575, #4690, # #7412, #11217 …) are OBJECTSTACK numbers — do not read them as objectui cards. # -# Every divergence from upstream is listed here, once, so a future re-sync knows -# exactly what it must not clobber: +# ## Where the divergence list lives (objectui#6642) +# +# ⚠️ It is no longer this comment. This header used to carry the whole list and +# to end with "nothing else diverges; the predicates are untouched" — and that +# sentence rotted, silently, for months. Measured 2026-08-28: the ported copy +# stood at 9,340 lines against upstream's 12,948 (a 4,637-line `diff`), running +# 1,116 self-test cases where upstream ran 1,574. The three deliberate +# adaptations below were accurate the whole time; what the list could not say +# was that ~3,600 lines of upstream fixes had never arrived, because a +# hand-maintained enumeration can only describe what someone remembered to +# write down. +# +# The enumeration is therefore MACHINE-CHECKED now, in +# `scripts/upstream-port-pin.json`: every declared divergence as an exact text +# pair, with the upstream commit and the SHA-256 of the blob it was taken from. +# `scripts/check-upstream-port-parity.mjs` reverses those pairs out of the +# ported files and requires the reconstruction to hash to the pinned digest, so +# a drift beyond the declared set — here or upstream — is RED in `lint.yml` +# rather than invisible. Bumping the pin (`--resync`) is the deliberate re-sync +# act. ⛔ Do not maintain a second copy of the divergence list in this comment; +# that is what just failed. +# +# What the pin CANNOT see, and what therefore still belongs here, is the wiring: +# the pin judges file contents, and the two items below are decisions taken in +# THIS file about how the sweeper is called. # # 1. `PM_SWEEP_CLOSED_FLOOR` on the sweep step — H22's closed-card reader is # ON here, but judges only cards closed on/after the cutover date. It read @@ -46,10 +69,25 @@ name: Half-State Patrol # and it treats recent closed residue as a live duty. # 2. `scripts/invoked-as.mjs` is in the `paths:` filter below — the sweeper # imports it, and it was ported alongside. -# 3. `scripts/pm/check-half-states.mjs` carries `DEFAULT_SWEEP_REPO = -# 'objectstack-ai/objectui'` so a bare terminal run here sweeps THIS board. # -# Nothing else diverges; the predicates are untouched. +# The IN-SCRIPT divergences are the pin's business, not this comment's, but the +# classes are worth knowing before you open it — three of them, and the set is +# meant to shrink: +# +# a. `DEFAULT_SWEEP_REPO = 'objectstack-ai/objectui'`, so a bare terminal run +# here sweeps THIS board, plus the two self-test rows that pinned the +# constant against objectstack's literal. +# b. `PM_SWEEP_CLOSED_WINDOW_PAGES` / `resolveClosedWindowPages` — an +# objectui-only escape hatch that can switch H22's closed reader fully OFF, +# authored during the port and never upstreamed. UNSET here since +# 2026-08-28 (the floor in (1) is what holds the historical residue out), +# so it is dormant, but it is live code and its "the surface is UNREAD, not +# clean" summary branch is the #4690 property the port turned on. +# c. Three H32 self-test rows. `seatLane` compares a seat title's `@ ` +# suffix against the LIVE resolved sweep repo, so which name is FOREIGN is +# install-dependent and upstream's specimens invert here. ⚠️ This class is +# upstream's to remove: derive the specimen from the resolved repo there +# and all three entries disappear. # # ## Why a workflow, and not "a seat should run it" # diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f99fe63144..6b73feea24 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -201,6 +201,44 @@ jobs: node scripts/check-entry-guard.mjs --self-test node scripts/check-entry-guard.mjs + # ── The ported objectstack tooling is a PINNED copy (objectui#6642) ─── + # `scripts/pm/check-half-states.mjs` came from objectstack (objectui#5791) + # under a workflow header calling it a verbatim copy and enumerating the + # three things a re-sync must not clobber. Nothing checked either half. + # Measured 2026-08-28, before this step existed: the ported copy stood at + # 9,340 lines against upstream's 12,948 — a 4,637-line `diff` — and its + # own `--self-test` ran 1,116 cases where upstream's ran 1,574. So 458 + # predicate cases had landed upstream and never arrived here, while the + # patrol went on rendering a confident report with the corresponding rows + # simply missing. + # + # The direction of harm is this repository's least visible one: a drifted + # copy does not fail, it REPORTS. It became load-bearing once already — + # objectui#6641 had to hand-port H22's closure floor into this copy, + # because wiring the new environment variable in the workflow alone would + # have set a variable this copy did not read. + # + # The gate reverses the DECLARED divergences out of each ported file and + # requires the reconstruction to hash to the pinned upstream digest, so + # drift beyond the declared set is byte-detectable in both directions — + # an edit here, or upstream moving. ⛔ It fetches nothing: a gate that + # reached api.github.com would be red on a network hiccup and green on a + # cached 200, and this repo's whole reason for owning a patrol is that a + # check which cannot read its input must never read as clean (#4690). + # + # Runs before install, next to the two gates above and for the same + # reason: node builtins and one local module only, so an install failure + # cannot take it down with it. `--self-test` runs FIRST — it drives the + # real comparer over fixtures (parity holds, drift outside a region, + # drift inside one, an ambiguous anchor, the pin-bump procedure, and + # every malformed-pin shape), which is what stops a comparer that + # recognises nothing from reading as a clean tree. + - name: Verify the ported objectstack tooling still matches its pin + if: steps.relevant.outputs.should_run == 'true' + run: | + node scripts/check-upstream-port-parity.mjs --self-test + node scripts/check-upstream-port-parity.mjs + - name: Turbo Cache if: steps.relevant.outputs.should_run == 'true' uses: actions/cache@v6 diff --git a/package.json b/package.json index 988c493105..90157792f4 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "check:eager-closure": "node scripts/check-eager-closure-budget.mjs", "check:docs-route-closure": "node scripts/check-docs-route-eager-closure.mjs", "check:entry-guard": "node scripts/check-entry-guard.mjs", + "check:upstream-port-parity": "node scripts/check-upstream-port-parity.mjs", "check:pre-install-import-graph": "node scripts/check-pre-install-import-graph.mjs", "check:vi-mock-specifiers": "node scripts/check-vi-mock-specifiers.mjs", "check:shell-escape-residue": "node scripts/check-shell-escape-residue.mjs", diff --git a/scripts/__tests__/check-half-states.test.ts b/scripts/__tests__/check-half-states.test.ts index a4968401b2..ddd301c92d 100644 --- a/scripts/__tests__/check-half-states.test.ts +++ b/scripts/__tests__/check-half-states.test.ts @@ -13,6 +13,7 @@ import { resolveClosedWindowPages, resolveClosureFloor, resolveSweepRepo, + seatLane, summaryLine, } from '../pm/check-half-states.mjs'; @@ -21,18 +22,35 @@ import { * * ## What this file is for, and what it deliberately is not * - * The sweeper carries its own ~1,077-case `--self-test`, and that suite is the - * authority on the twenty-odd predicates. Re-asserting predicates here would - * fork the pin: two copies drifting apart, one of them not the one upstream - * maintains. So the first test below simply RUNS that suite in CI — the point - * being that a port whose self-test nobody executes is the #4690 shape again - * (a check that reads as enforcement while nothing invokes it). + * The sweeper carries its own `--self-test`, and that suite is the authority on + * the twenty-odd predicates. Re-asserting predicates here would fork the pin: + * two copies drifting apart, one of them not the one upstream maintains. So the + * first test below simply RUNS that suite in CI — the point being that a port + * whose self-test nobody executes is the #4690 shape again (a check that reads + * as enforcement while nothing invokes it). + * + * ⚠️ REPLACED PIN (objectui#6642): that sentence used to say "~1,077-case", and + * the figure was 1,116 by then and is 1,574 after the re-sync. The count is + * deliberately gone rather than refreshed — a hand-copied enumeration drifts by + * construction and a stale one reads exactly as authoritative as a fresh one, + * which is the lesson `lint-workflow.test.ts` records at length for this repo. + * The assertion below never read the number and still does not. * * Everything after it pins the ADAPTATIONS instead — the handful of places this * install diverges from upstream. Those are exactly the lines a future verbatim * re-sync from objectstack would clobber silently, and each one is load-bearing: * dropping any of them does not break the patrol loudly, it makes the patrol * report something false quietly. + * + * ## What this file cannot see, and what now can (objectui#6642) + * + * By construction it looks only at THIS copy. It cannot tell whether upstream + * has moved, which is how the port drifted 4,637 lines behind while every test + * here stayed green. `scripts/check-upstream-port-parity.mjs` is the half that + * looks the other way: it pins the ported files against a named upstream commit + * modulo the same adaptations, byte-for-byte. The two are complements — that + * gate proves the copy still IS the copy; this file proves the adaptations + * survived being one. */ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); @@ -82,6 +100,50 @@ describe('check-half-states — sweeps THIS board (objectui#5791 adaptation)', ( }); }); +describe('check-half-states — H32 lane foreignness inverts here (objectui#6642)', () => { + /** + * A divergence the re-sync DISCOVERED rather than one it authored, and the + * only one that made a verbatim copy impossible outright: upstream's H32 rows + * do not merely read oddly here, they FAIL. Three of them, on the first run + * of upstream's suite against this install. + * + * `seatLane` decides whether a seat post's lane is readable from this sweep by + * comparing the title's `@ ` suffix against the RESOLVED sweep repo — + * a live value, not a constant. Upstream's self-test rows hard-code + * `@ objectui` as the foreign specimen and `@ objectstack` as the own-board + * one, which is correct there and exactly backwards here. + * + * The property being asserted is identical in both installs; only the + * specimens swap. ⚠️ Which is why this block is the one adaptation that + * should NOT be defended: the day upstream derives its specimen from the + * resolved repo instead of writing the name, three pin entries disappear and + * these rows become redundant with upstream's own. + */ + const seat = (title: string) => ({ title }); + + it('reads a SIBLING board\'s lane as foreign — and the sibling here is objectstack', () => { + expect(seatLane(seat('[PM seat] domain:devx @ objectstack — 🟢 os-x')).foreign).toBe(true); + }); + + it('…and this board\'s own lane as readable, keeping the bare label', () => { + const own = seatLane(seat('[PM seat] domain:devx @ objectui — 🟢 os-x')); + expect(own.foreign).toBe(false); + expect(own.lane).toBe('domain:devx'); + }); + + it('the specimens follow the RESOLVED sweep repo, which is what makes this a divergence', () => { + // Pinning the coupling itself rather than a value: this line is the reason + // upstream's rows cannot be carried verbatim, and its removal upstream is + // the event that retires this whole block plus three pin entries. + const src = fs.readFileSync(sweeperPath, 'utf8'); + expect(src).toContain("SWEEP_REPO.repo.split('/')[1]"); + // Both places this install resolves that value agree, so the rows above + // hold on a runner (GITHUB_REPOSITORY) and in a bare terminal (the default). + expect(resolveSweepRepo({}).repo).toBe('objectstack-ai/objectui'); + expect(resolveSweepRepo({ GITHUB_REPOSITORY: 'objectstack-ai/objectui' }).repo).toBe('objectstack-ai/objectui'); + }); +}); + describe('check-half-states — H22 runs here behind a DATED CLOSURE FLOOR (objectui#5985)', () => { /** * ⚠️ REPLACED PIN, not a respelled one. Until 2026-08-28 this block pinned the diff --git a/scripts/__tests__/upstream-port-parity-wiring.test.ts b/scripts/__tests__/upstream-port-parity-wiring.test.ts new file mode 100644 index 0000000000..5589c44ae0 --- /dev/null +++ b/scripts/__tests__/upstream-port-parity-wiring.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, it } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parse as parseYaml } from 'yaml'; + +const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..'); +const GATE = 'scripts/check-upstream-port-parity.mjs'; +const PIN = 'scripts/upstream-port-pin.json'; + +/** + * objectui#6642: `scripts/pm/check-half-states.mjs` was copied here from + * objectstack (objectui#5791) and then drifted for months with nothing able to + * see it. Measured the day this gate landed: 9,340 lines here against 12,948 + * upstream — a 4,637-line `diff` — and 1,116 self-test cases here against + * upstream's 1,574. The patrol kept rendering a confident report with the + * missing predicates' rows simply absent. + * + * The gate closes that. This file pins the gate to its WIRING, in the direction + * that goes wrong quietly: a parity check nobody runs is indistinguishable from + * a parity check that passes — which is exactly the state the ported sweeper + * was already in, one level down. + * + * Deliberately NOT asserted here: any digest, any line count, or the number of + * declared divergences. Those live in the pin, they move every time someone + * re-syncs, and a copy of them here would be a second thing to keep honest — + * the lesson `lint-workflow.test.ts` records at length for this same workflow. + * What is asserted is that the mechanism is reachable, runs, and is not + * vacuous. + */ +describe('check-upstream-port-parity is wired, not merely present', () => { + const workflow = parseYaml(fs.readFileSync(path.join(ROOT, '.github/workflows/lint.yml'), 'utf8')); + const steps: Array> = workflow.jobs.lint.steps; + const gateSteps = steps.filter((s) => typeof s.run === 'string' && (s.run as string).includes(GATE)); + + it('the gate script and its pin both exist', () => { + expect(fs.existsSync(path.join(ROOT, GATE))).toBe(true); + expect(fs.existsSync(path.join(ROOT, PIN))).toBe(true); + }); + + it('package.json aliases it, and the alias points at the script that exists', () => { + const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); + const alias = pkg.scripts['check:upstream-port-parity']; + expect(alias).toBeTruthy(); + expect(alias).toContain(GATE); + }); + + it('lint.yml runs it — exactly one step, both legs', () => { + expect(gateSteps).toHaveLength(1); + const run = gateSteps[0].run as string; + expect(run).toContain(`node ${GATE} --self-test`); + expect(run.split('\n').some((l) => l.trim() === `node ${GATE}`)).toBe(true); + }); + + it('that step is not disabled — it runs whenever the job runs its other steps', () => { + // The same guard the rest of the job uses, read off a sibling rather than + // hard-coded: objectui#3523's shape may be renamed, and a test pinning the + // literal would fail on a rename while a step commented out with + // `if: false` would not. + const condition = gateSteps[0].if; + const others = steps + .filter((s) => s !== gateSteps[0] && typeof s.uses !== 'undefined') + .map((s) => s.if); + expect(others).toContain(condition); + }); + + it('it runs BEFORE pnpm install, so an install failure cannot take it with it', () => { + const gateIndex = steps.indexOf(gateSteps[0]); + const installIndex = steps.findIndex( + (s) => typeof s.run === 'string' && (s.run as string).includes('pnpm install'), + ); + expect(installIndex).toBeGreaterThan(-1); + expect(gateIndex).toBeLessThan(installIndex); + }); + + it('the pin really pins the ported sweeper — the file the card is about', () => { + // The one content assertion, and it is about COVERAGE rather than about + // values: a pin that stopped naming `check-half-states.mjs` would leave the + // gate green while the drift it was written for resumed. + const pin = JSON.parse(fs.readFileSync(path.join(ROOT, PIN), 'utf8')); + const pinned = pin.files.map((f: { ported: string }) => f.ported); + expect(pinned).toContain('scripts/pm/check-half-states.mjs'); + // …and its helper, which the patrol workflow's own `paths:` filter already + // treats as part of the same unit. + expect(pinned).toContain('scripts/invoked-as.mjs'); + }); + + it('the pinned files are the ones the patrol workflow watches', () => { + // Both directions of the same claim: a file added to the patrol's paths + // filter but not to the pin drifts unwatched, and a file in the pin that + // the patrol no longer uses is a stale obligation. + const patrol = parseYaml( + fs.readFileSync(path.join(ROOT, '.github/workflows/half-state-patrol.yml'), 'utf8'), + ); + const watched: string[] = patrol.on.pull_request.paths; + const pin = JSON.parse(fs.readFileSync(path.join(ROOT, PIN), 'utf8')); + const pinned: string[] = pin.files.map((f: { ported: string }) => f.ported); + for (const p of pinned) expect(watched).toContain(p); + }); + + it('its self-test passes — the half that makes a green comparison mean something', () => { + const out = execFileSync('node', [GATE, '--self-test'], { cwd: ROOT, encoding: 'utf8' }); + expect(out).toMatch(/check-upstream-port-parity self-test: \d+ cases pass/); + }); + + it('and the tree itself is at parity right now', () => { + // Not a duplicate of the CI step: this is the assertion that the pin + // shipped in this commit describes the files shipped in this commit. A pin + // updated without its file, or the reverse, fails here at review time + // rather than on someone else's branch. + const out = execFileSync('node', [GATE], { cwd: ROOT, encoding: 'utf8' }); + expect(out).toMatch(/ported file\(s\) match/); + }); +}); diff --git a/scripts/check-upstream-port-parity.mjs b/scripts/check-upstream-port-parity.mjs new file mode 100644 index 0000000000..53837ad000 --- /dev/null +++ b/scripts/check-upstream-port-parity.mjs @@ -0,0 +1,601 @@ +#!/usr/bin/env node + +/** + * check-upstream-port-parity -- the ported objectstack tooling in this tree is + * a PINNED copy, and drift from the pin is RED. + * + * node scripts/check-upstream-port-parity.mjs # verify the pin + * node scripts/check-upstream-port-parity.mjs --list # what is pinned, and how far it diverges + * node scripts/check-upstream-port-parity.mjs --self-test # verify the checker itself + * node scripts/check-upstream-port-parity.mjs --resync --ref + * # the deliberate re-sync act + * + * ## What this gate is for + * + * `scripts/pm/check-half-states.mjs` was copied here from objectstack + * (objectui#5791) under a workflow header that calls it a verbatim copy and + * enumerates the handful of things a re-sync "must not clobber". Both halves of + * that promise decayed with nothing watching. Measured on 2026-08-28, before + * this gate landed: + * + * objectui's ported copy 9,340 lines + * objectstack upstream 12,948 lines + * `diff` between them 4,637 lines + * the sweeper's own --self-test, here 1,116 cases + * the same suite, on upstream's copy 1,574 cases + * + * So 458 predicate cases and ~3,600 lines of fixes had landed upstream and + * never arrived here, and the copy went on rendering a confident patrol report + * with the corresponding rows simply missing. Nothing could see it: the port's + * own test file pins the ADAPTATIONS (correctly -- that is its job) and by + * construction cannot look at upstream at all. + * + * The direction of harm is the one this tree treats as worst. A drifted copy + * does not fail; it reports. It became load-bearing once: objectui#6641 had to + * hand-port H22's closure floor into this copy, because wiring the new + * environment variable in the workflow alone would have set a variable this + * copy did not read -- an unfloored closed reader at ~87% residue density, + * arriving as a full-looking anchor body. + * + * ## The shape: reverse the declared divergences, then compare BYTES + * + * The pin (`scripts/upstream-port-pin.json`) carries, per ported file, the + * upstream ref it was taken from, the SHA-256 of that upstream blob, and the + * DECLARED DIVERGENCES as exact text pairs. Verification runs backwards: + * + * ported file --(reverse each declared divergence)--> reconstruction + * SHA-256(reconstruction) === the pinned upstream digest ? green : red + * + * Byte equality is the assertion, so there is no "close enough" reading and no + * heuristic to tune. Three failure directions, all named: + * + * 1. an edit OUTSIDE every declared region -- the reversal succeeds and the + * digest differs; + * 2. an edit INSIDE a declared region -- the reversal finds its `ported` + * snippet zero times and says which divergence; + * 3. upstream moved -- same as (1) from this side, and the fix is a re-sync + * rather than a revert, which is why the message names the procedure. + * + * ⛔ What it deliberately does NOT do is fetch anything. A gate that reached + * api.github.com would be red on a network hiccup and green on a cached 200, + * and this repository's whole reason for owning a patrol is that a check which + * cannot read its input must never read as clean (#4690). The pinned digest IS + * the input; the `ref` beside it is provenance for a human, and the gate says + * so rather than implying it verified it. + * + * ## Why a digest and not a checked-in copy of upstream + * + * The obvious spelling -- commit upstream's blob, apply a patch, diff -- costs + * 784 KB of duplicated source that every future reader has to be told to + * ignore, and it puts a second copy of the predicates in the tree, which is the + * same disease one level up. The digest is 64 characters and answers exactly + * the same question. The cost is that a red gate cannot show you upstream's + * side of the diff; the message names the two commands that do. + * + * ## Bumping the pin IS the re-sync + * + * git -C fetch origin main + * git -C show origin/main:scripts/pm/check-half-states.mjs > /tmp/up.mjs + * node scripts/check-upstream-port-parity.mjs --resync /tmp/up.mjs --ref + * + * `--resync` applies the declared divergences FORWARD onto the new upstream + * text, writes the ported file, and rewrites the pin's ref and digest. It is + * the only supported way to move the pin, because the alternative -- editing a + * digest by hand until the gate goes green -- is indistinguishable from + * baselining the drift it exists to catch. Afterwards, run the ported file's + * own suites: a divergence whose anchor upstream deleted fails LOUDLY here + * (zero occurrences), but a divergence that still applies and no longer makes + * sense is only visible to those tests. + * + * ## The divergences are a checklist, not a licence + * + * Every entry carries a `why`. An adaptation nobody can justify in one sentence + * is drift that was written down rather than drift that was decided, and the + * self-test refuses a pin whose entries lack one. The set is meant to SHRINK: + * three of the entries here exist only because upstream's own self-test rows + * are coupled to the resolved sweep repo, and would disappear the day upstream + * derives those specimens instead of hard-coding them. + * + * Exit: 0 = parity holds, 1 = drift, 2 = the pin itself is unusable. + */ + +import { createHash } from 'node:crypto'; +import { readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +export const PIN_PATH = 'scripts/upstream-port-pin.json'; + +const HEX40 = /^[0-9a-f]{40}$/; +const HEX64 = /^[0-9a-f]{64}$/; + +/** SHA-256 of a UTF-8 text, hex. The one comparison this gate makes. */ +export function digest(text) { + return createHash('sha256').update(Buffer.from(text, 'utf8')).digest('hex'); +} + +/** + * Decode bytes as STRICT UTF-8. + * + * `Buffer#toString('utf8')` replaces malformed sequences with U+FFFD, which + * would turn "this file is not valid UTF-8" into "the digest does not match" -- + * a true statement pointing at the wrong cause, and one whose obvious repair is + * to bump the pin. Strict decoding makes it its own error. + */ +export function decodeUtf8(bytes, label) { + try { + return new TextDecoder('utf-8', { fatal: true }).decode(bytes); + } catch { + throw new Error(`${label}: not valid UTF-8 — refusing to guess at its bytes`); + } +} + +/** + * Every structural rule the pin has to satisfy before any comparison is worth + * making. Returns the problems; empty means usable. + * + * This is separate from verification on purpose. A malformed pin is BAD USAGE + * (exit 2), not a finding about the tree: reporting it as drift would send the + * next reader to diff a file that is fine. + */ +export function validatePin(pin) { + const problems = []; + const bad = (m) => problems.push(m); + if (!pin || typeof pin !== 'object' || Array.isArray(pin)) { + return ['the pin is not a JSON object']; + } + const up = pin.upstream; + if (!up || typeof up !== 'object') bad('`upstream` is missing'); + else { + if (typeof up.repo !== 'string' || !/^[\w.-]+\/[\w.-]+$/.test(up.repo)) { + bad('`upstream.repo` is not an `owner/name` repository'); + } + // A ref that is not a full commit sha cannot identify one tree. A branch + // name would make the pin read as precise while naming a moving target. + if (typeof up.ref !== 'string' || !HEX40.test(up.ref)) { + bad('`upstream.ref` is not a 40-character commit sha'); + } + } + if (!Array.isArray(pin.files) || pin.files.length === 0) { + bad('`files` is missing or empty — a pin that pins nothing is green by construction'); + return problems; + } + const seen = new Set(); + for (const [i, f] of pin.files.entries()) { + const at = `files[${i}]`; + if (!f || typeof f !== 'object') { bad(`${at} is not an object`); continue; } + for (const key of ['ported', 'upstreamPath']) { + if (typeof f[key] !== 'string' || !f[key]) bad(`${at}.${key} is missing`); + } + if (typeof f.ported === 'string') { + if (path.isAbsolute(f.ported) || f.ported.split(/[\\/]/).includes('..')) { + bad(`${at}.ported must be a repo-relative path without \`..\``); + } + if (seen.has(f.ported)) bad(`${at}.ported is pinned twice (${f.ported})`); + seen.add(f.ported); + } + if (typeof f.upstreamSha256 !== 'string' || !HEX64.test(f.upstreamSha256)) { + bad(`${at}.upstreamSha256 is not a 64-character SHA-256 digest`); + } + if (!Array.isArray(f.divergences)) { bad(`${at}.divergences is not an array`); continue; } + const ids = new Set(); + for (const [j, d] of f.divergences.entries()) { + const dat = `${at}.divergences[${j}]`; + if (!d || typeof d !== 'object') { bad(`${dat} is not an object`); continue; } + if (typeof d.id !== 'string' || !d.id.trim()) bad(`${dat}.id is missing`); + else if (ids.has(d.id)) bad(`${dat}.id is a duplicate (${d.id})`); + else ids.add(d.id); + // An undocumented adaptation is drift someone wrote down. The reason is + // the whole difference between a divergence list and a diff. + if (typeof d.why !== 'string' || d.why.trim().length < 10) { + bad(`${dat}.why is missing — every declared divergence states why it exists`); + } + if (typeof d.upstream !== 'string' || !d.upstream) bad(`${dat}.upstream is empty`); + if (typeof d.ported !== 'string' || !d.ported) bad(`${dat}.ported is empty`); + if (typeof d.upstream === 'string' && d.upstream === d.ported) { + bad(`${dat} declares a divergence between two identical texts`); + } + } + } + return problems; +} + +/** + * Rewrite `text` by replacing each divergence's `from` side with its `to` side, + * in order, requiring EXACTLY ONE occurrence at each step. + * + * The occurrence count is the load-bearing part. A snippet matching twice would + * let the replacement land on whichever came first, which is a coin-flip + * dressed as a check; a snippet matching zero times means the region it names + * has been edited, which is the second of the three drift directions and needs + * to be said in those words rather than surfacing as a digest mismatch. + * + * @returns {{ text: string, problems: string[] }} + */ +export function rewrite(text, divergences, direction) { + const forward = direction !== 'reverse'; + const ordered = forward ? divergences : [...divergences].reverse(); + const problems = []; + let out = text; + for (const d of ordered) { + const from = forward ? d.upstream : d.ported; + const to = forward ? d.ported : d.upstream; + const n = countOccurrences(out, from); + if (n !== 1) { + problems.push( + `divergence \`${d.id}\`: expected its ${forward ? 'upstream' : 'ported'} text exactly once, found ${n}` + + (n === 0 + ? ' — the region it declares has been edited, or upstream moved it' + : ' — the anchor is ambiguous and must be widened until it is unique'), + ); + continue; + } + out = out.replace(from, to); + } + return { text: out, problems }; +} + +/** Non-overlapping occurrences of a literal substring. */ +export function countOccurrences(haystack, needle) { + let n = 0; + let i = haystack.indexOf(needle); + while (i !== -1) { + n++; + i = haystack.indexOf(needle, i + needle.length); + } + return n; +} + +/** + * The verdict for one pinned file, computed from text alone so the self-test + * drives the real logic over fixtures rather than over the tree. + * + * @returns {{ ok: boolean, reasons: string[], actual: string }} + */ +export function verifyFile(entry, portedText) { + const { text, problems } = rewrite(portedText, entry.divergences ?? [], 'reverse'); + const actual = digest(text); + if (problems.length) return { ok: false, reasons: problems, actual }; + if (actual !== entry.upstreamSha256) { + return { + ok: false, + actual, + reasons: [ + `reconstruction does not match the pinned upstream blob\n` + + ` pinned : ${entry.upstreamSha256}\n` + + ` actual : ${actual}\n` + + ` Every declared divergence still applied cleanly, so the difference is OUTSIDE all of ` + + `them — either this copy was edited without declaring it, or upstream moved.`, + ], + }; + } + return { ok: true, reasons: [], actual }; +} + +function countOccurrencesSafe(text, needle) { + return needle ? countOccurrences(text, needle) : 0; +} + +function readPin(root = ROOT) { + const raw = readFileSync(path.join(root, PIN_PATH)); + return JSON.parse(decodeUtf8(raw, PIN_PATH)); +} + +function readPorted(root, rel) { + return decodeUtf8(readFileSync(path.join(root, rel)), rel); +} + +function resyncCommand(pin, entry) { + return ( + ` git -C fetch origin main\n` + + ` git -C show origin/main:${entry.upstreamPath} > /tmp/upstream.mjs\n` + + ` node scripts/check-upstream-port-parity.mjs --resync /tmp/upstream.mjs --ref \n` + + ` Upstream is ${pin.upstream.repo}; the pin currently names ${pin.upstream.ref}.` + ); +} + +function main(root = ROOT) { + let pin; + try { + pin = readPin(root); + } catch (err) { + console.error(`check-upstream-port-parity: cannot read ${PIN_PATH} — ${err.message}`); + return 2; + } + const structural = validatePin(pin); + if (structural.length) { + console.error(`check-upstream-port-parity: ${PIN_PATH} is not usable:`); + for (const p of structural) console.error(` - ${p}`); + console.error( + ' A pin that cannot be read is not a clean tree. Refusing to report parity from it.', + ); + return 2; + } + + let failed = 0; + for (const entry of pin.files) { + let portedText; + try { + portedText = readPorted(root, entry.ported); + } catch (err) { + console.error(`✗ ${entry.ported}: cannot be read — ${err.message}`); + failed++; + continue; + } + const verdict = verifyFile(entry, portedText); + if (verdict.ok) { + console.log( + `✓ ${entry.ported}: byte-identical to ${pin.upstream.repo}@${pin.upstream.ref.slice(0, 9)}:` + + `${entry.upstreamPath} modulo ${entry.divergences.length} declared divergence(s).`, + ); + continue; + } + failed++; + console.error(`✗ ${entry.ported}: DRIFTED from the pinned upstream copy.`); + for (const r of verdict.reasons) console.error(` ${r}`); + console.error(' To re-sync (bumping the pin is the deliberate act):'); + console.error(resyncCommand(pin, entry)); + } + + if (failed) { + console.error( + `✗ check-upstream-port-parity: ${failed} of ${pin.files.length} ported file(s) drifted. ` + + '⛔ Do not edit the pinned digest by hand to clear this — that baselines the drift the gate exists to catch.', + ); + return 1; + } + console.log( + `✓ check-upstream-port-parity: ${pin.files.length} ported file(s) match ` + + `${pin.upstream.repo}@${pin.upstream.ref.slice(0, 9)} modulo their declared divergences. ` + + '(The digest is verified; the ref beside it is provenance and is NOT fetched.)', + ); + return 0; +} + +function list(root = ROOT) { + const pin = readPin(root); + console.log(`upstream: ${pin.upstream.repo}@${pin.upstream.ref}`); + for (const entry of pin.files) { + const portedText = readPorted(root, entry.ported); + console.log(`\n${entry.ported} <- ${entry.upstreamPath}`); + console.log(` pinned upstream digest: ${entry.upstreamSha256}`); + console.log(` declared divergences : ${entry.divergences.length}`); + for (const d of entry.divergences) { + const lines = d.ported.split('\n').length; + console.log( + ` - ${d.id} (${lines} line(s), ${countOccurrencesSafe(portedText, d.ported)} match(es) in the ported copy)`, + ); + console.log(` ${d.why}`); + } + } + return 0; +} + +function resync(argv, root = ROOT) { + const fileAt = argv.indexOf('--resync'); + const refAt = argv.indexOf('--ref'); + const upstreamFile = argv[fileAt + 1]; + const ref = argv[refAt + 1]; + if (!upstreamFile || upstreamFile.startsWith('--')) { + console.error('check-upstream-port-parity: --resync needs a path to the new upstream file'); + return 2; + } + if (!ref || !HEX40.test(ref)) { + console.error( + 'check-upstream-port-parity: --ref must be the full 40-character commit sha the file was taken from. ' + + 'A branch name names a moving target and would make the pin read as precise when it is not.', + ); + return 2; + } + const pin = readPin(root); + const structural = validatePin(pin); + if (structural.length) { + console.error('check-upstream-port-parity: refusing to re-sync from an unusable pin:'); + for (const p of structural) console.error(` - ${p}`); + return 2; + } + const upstreamText = decodeUtf8(readFileSync(upstreamFile), upstreamFile); + // WHICH pinned file this is. Named explicitly, or inferred only when there is + // exactly one candidate. ⛔ Never guessed from the argument's basename: a + // wrong guess would forward-apply one file's divergences onto another file's + // text, and the result would be written to disk before anything could notice. + const named = argv.includes('--path') ? argv[argv.indexOf('--path') + 1] : null; + const candidates = named ? pin.files.filter((f) => f.upstreamPath === named) : pin.files; + if (candidates.length !== 1) { + console.error( + `check-upstream-port-parity: ${candidates.length === 0 ? 'no pinned file matches' : 'more than one file is pinned'}. ` + + `Pass --path , one of: ${pin.files.map((f) => f.upstreamPath).join(', ')}`, + ); + return 2; + } + const entry = candidates[0]; + const { text, problems } = rewrite(upstreamText, entry.divergences, 'forward'); + if (problems.length) { + console.error(`check-upstream-port-parity: the declared divergences do not apply to the new upstream text:`); + for (const p of problems) console.error(` - ${p}`); + console.error( + ' ⛔ This is the re-sync doing its job, not a bug in it: upstream changed a region this port adapts. ' + + 'Re-decide the divergence by hand, update the pin entry, and run this again.', + ); + return 1; + } + writeFileSync(path.join(root, entry.ported), text, 'utf8'); + entry.upstreamSha256 = digest(upstreamText); + pin.upstream.ref = ref; + writeFileSync(path.join(root, PIN_PATH), `${JSON.stringify(pin, null, 2)}\n`, 'utf8'); + console.log( + `✓ re-synced ${entry.ported} from ${pin.upstream.repo}@${ref.slice(0, 9)}:${entry.upstreamPath} ` + + `(${entry.divergences.length} divergence(s) re-applied) and bumped the pin.`, + ); + console.log(' ⚠️ Now run the ported file\'s own suites: a divergence that still APPLIES but no longer'); + console.log(' makes sense is invisible here and visible only there.'); + return 0; +} + +// ── the self-test ──────────────────────────────────────────────────────────── +// +// Fixture-driven: everything below runs the real `rewrite`, `verifyFile` and +// `validatePin` over hand-built texts, so a green scan of the tree means the +// recogniser works rather than that it recognised nothing. The last block is +// the exception and is deliberate -- it drives the SHIPPED pin, because a pin +// this gate cannot parse is the one failure a fixture can never show. +function selfTest() { + const cases = []; + const t = (name, ok, detail) => cases.push({ name, ok, detail }); + + const UP = [ + 'const A = 1;', + 'export const REPO = "objectstack-ai/objectstack";', + 'function f() {', + ' return 1;', + '}', + 'const Z = 9;', + '', + ].join('\n'); + const DIVS = [ + { + id: 'repo-constant', + why: 'this install names its own board', + upstream: 'export const REPO = "objectstack-ai/objectstack";\n', + ported: 'export const REPO = "objectstack-ai/objectui";\n', + }, + { + id: 'extra-guard', + why: 'an escape hatch that exists only here', + upstream: 'function f() {\n', + ported: 'function f() {\n if (OFF) return 0;\n', + }, + ]; + const PORTED = rewrite(UP, DIVS, 'forward').text; + const ENTRY = { + ported: 'scripts/x.mjs', + upstreamPath: 'scripts/x.mjs', + upstreamSha256: digest(UP), + divergences: DIVS, + }; + + // ── row 1: parity holds ──────────────────────────────────────────────────── + t('forward application produces a text that differs from upstream', PORTED !== UP); + t('parity holds: the declared divergences reverse to the pinned digest', verifyFile(ENTRY, PORTED).ok); + t('…and the round trip is byte-exact, not merely same-digest', rewrite(PORTED, DIVS, 'reverse').text === UP); + + // ── row 2: drift BEYOND the patch reds ───────────────────────────────────── + const driftedOutside = PORTED.replace('const Z = 9;', 'const Z = 10;'); + const outside = verifyFile(ENTRY, driftedOutside); + t('drift outside every declared region is RED', !outside.ok); + t('…and is reported as a digest mismatch, not as a broken divergence', outside.reasons.join(' ').includes('OUTSIDE all of')); + t('…and the message names the pinned digest so the reader can diff', outside.reasons.join(' ').includes(ENTRY.upstreamSha256)); + // The direction that matters most: an edit that LOOKS like an adaptation but + // was never declared. Upstream's own text, deleted here, is exactly the + // ~3,600-line shape this gate was written for, one line at a time. + const deleted = PORTED.replace(' return 1;\n', ''); + t('a DELETED upstream line is RED (the drift shape this gate exists for)', !verifyFile(ENTRY, deleted).ok); + // A comment-only edit is still drift: the ported copy is a copy. + t('a prose-only edit outside a declared region is RED too', !verifyFile(ENTRY, `// note\n${PORTED}`).ok); + + // ── row 3: drift INSIDE a declared region names the divergence ───────────── + const driftedInside = PORTED.replace(' if (OFF) return 0;', ' if (OFF) return [];'); + const inside = verifyFile(ENTRY, driftedInside); + t('an edit inside a declared region is RED', !inside.ok); + t('…and names the divergence rather than the digest', inside.reasons.join(' ').includes('`extra-guard`')); + t('…and says the region was edited or upstream moved it', inside.reasons.join(' ').includes('has been edited')); + + // An AMBIGUOUS anchor must be refused, never applied to the first match: a + // replacement that picks one of two identical sites is a coin flip wearing a + // check's clothing. + const ambiguous = [{ id: 'dup', why: 'x'.repeat(20), upstream: 'const A = 1;\n', ported: 'const A = 2;\n' }]; + const twice = `${UP}const A = 1;\n`; + const amb = rewrite(twice, ambiguous, 'forward'); + t('an anchor matching twice is refused, not applied to the first', amb.problems.length === 1 && amb.text === twice); + t('…and the message says to widen the anchor', amb.problems.join(' ').includes('widened')); + + // ── row 4: the pin-bump procedure ────────────────────────────────────────── + // Upstream grows a line. The OLD pin must red; forward-applying the same + // divergences onto the new upstream and re-digesting must go green — that is + // the whole of `--resync`, driven here without touching the filesystem. + const UP2 = UP.replace('const Z = 9;', 'const Z = 9;\nconst NEW = 1;'); + const bumped = rewrite(UP2, DIVS, 'forward'); + t('a moved upstream still applies the divergences cleanly', bumped.problems.length === 0); + t('…and the OLD pin reds against the re-synced file (the bump is required)', !verifyFile(ENTRY, bumped.text).ok); + const ENTRY2 = { ...ENTRY, upstreamSha256: digest(UP2) }; + t('…and the BUMPED pin goes green on it', verifyFile(ENTRY2, bumped.text).ok); + t('…while the bumped pin still reds on the pre-bump copy', !verifyFile(ENTRY2, PORTED).ok); + // The other half of the bump: a divergence whose anchor upstream DELETED must + // fail loudly at re-sync time rather than being silently dropped. + const UP3 = UP.replace('function f() {\n', ''); + const lost = rewrite(UP3, DIVS, 'forward'); + t('a divergence whose upstream anchor vanished fails the re-sync loudly', lost.problems.length === 1); + t('…naming the divergence that no longer applies', lost.problems.join(' ').includes('`extra-guard`')); + + // ── row 5: a malformed pin is REFUSED, never read as clean ───────────────── + const good = { upstream: { repo: 'o/r', ref: 'a'.repeat(40) }, files: [ENTRY] }; + t('the fixture pin is well-formed', validatePin(good).length === 0); + const broken = [ + ['a non-object pin', 'nope'], + ['no files at all', { ...good, files: [] }], + ['a branch name where a commit sha belongs', { ...good, upstream: { repo: 'o/r', ref: 'main' }, }], + ['a short ref', { ...good, upstream: { repo: 'o/r', ref: 'abc1234' } }], + ['a repo that is not owner/name', { ...good, upstream: { repo: 'objectstack', ref: 'a'.repeat(40) } }], + ['a digest that is not SHA-256', { ...good, files: [{ ...ENTRY, upstreamSha256: 'deadbeef' }] }], + ['an absolute ported path', { ...good, files: [{ ...ENTRY, ported: '/etc/passwd' }] }], + ['a ported path escaping the repo', { ...good, files: [{ ...ENTRY, ported: '../x.mjs' }] }], + ['the same file pinned twice', { ...good, files: [ENTRY, ENTRY] }], + ['a divergence with no id', { ...good, files: [{ ...ENTRY, divergences: [{ ...DIVS[0], id: '' }] }] }], + ['duplicate divergence ids', { ...good, files: [{ ...ENTRY, divergences: [DIVS[0], DIVS[0]] }] }], + ['a divergence with no stated reason', { ...good, files: [{ ...ENTRY, divergences: [{ ...DIVS[0], why: '' }] }] }], + ['an empty divergence side', { ...good, files: [{ ...ENTRY, divergences: [{ ...DIVS[0], ported: '' }] }] }], + ['a divergence between two identical texts', { ...good, files: [{ ...ENTRY, divergences: [{ ...DIVS[0], ported: DIVS[0].upstream }] }] }], + ]; + for (const [name, pin] of broken) { + t(`malformed pin refused: ${name}`, validatePin(pin).length > 0); + } + + // ── the shipped pin, and the tree it pins ───────────────────────────────── + // Fixtures cannot show that the REAL pin parses, and a pin that does not + // parse is the one state in which this gate has nothing to say. + let shipped = null; + try { + shipped = readPin(); + } catch (err) { + t('the shipped pin parses', false, err.message); + } + if (shipped) { + const problems = validatePin(shipped); + t('the shipped pin is well-formed', problems.length === 0, problems.join('; ')); + t('…and pins at least one file', Array.isArray(shipped.files) && shipped.files.length >= 1); + t( + '…and every declared divergence states a reason', + shipped.files.every((f) => (f.divergences ?? []).every((d) => typeof d.why === 'string' && d.why.trim().length >= 10)), + ); + } + + const failed = cases.filter((c) => !c.ok); + for (const c of failed) console.error(` ✗ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`); + if (failed.length) { + console.error(`✗ check-upstream-port-parity self-test: ${failed.length} of ${cases.length} case(s) failed.`); + return 1; + } + console.log( + `✓ check-upstream-port-parity self-test: ${cases.length} cases pass — parity holds on an undrifted copy, ` + + 'drift outside the declared regions reds as a digest mismatch, drift inside one names its divergence, ' + + 'an ambiguous anchor is refused rather than applied, the pin-bump procedure round-trips (and a vanished ' + + 'anchor fails it loudly), and every malformed-pin shape is refused instead of read as clean.', + ); + return 0; +} + +if (isEntrypoint(import.meta.url)) { + const argv = process.argv; + process.exit( + argv.includes('--self-test') + ? selfTest() + : argv.includes('--resync') + ? resync(argv) + : argv.includes('--list') + ? list() + : main(), + ); +} diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index 394b8ebfe6..0f70eb029c 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -560,6 +560,32 @@ * pushed one commit and then died (its branch moved after the claim), for * the same reason — that is precisely the card the protocol protects. * + * ## H28 — the STALE BODY LINE that shadowed the live blocker + * + * H28 an open `pm:blocked` card whose BODY names a `Blocked-by:` target that + * has CLOSED while a COMMENT names one that is still OPEN. The body is + * the canonical home for the line, so this is the re-park written half: + * a seat found the body's upstream closed, carded the real prerequisite, + * wrote the NEW blocker into a comment — and left the spent one in the + * body. Measured: one card sat in exactly this shape while its real + * blocker was open and `pm:dispatched`, and it was RELEASED to `pm:queue` + * on the strength of the stale line. + * ⚠️ The mechanism half is the gate that used to sit in front of the + * liveness read. `needsBlockedByComments` skips a card whose body already + * carries a line — correct for H4, which only asks whether the line + * EXISTS — but H19/H26 borrowed that gathering and so resolved ONLY the + * stale body target, found it closed, and published "the block has + * outlived its blocker": a FALSE unlock candidate, on three consecutive + * sweeps. The liveness read is therefore UNGATED now + * (`needsBlockerLivenessComments`), and H4's cheap question stays cheap. + * Ungating alone would only turn the false candidate into a PARTIAL one, + * so this row is its pair: it names the stale body line as the thing to + * fix and asks for the live blocker to be MIGRATED to the body, enforcing + * the canonical-home doctrine at the moment the wrong shape is written + * rather than trusting a seat to remember it mid-re-park. + * FREE: the same resolutions H19 and H26 already hold, asked a third + * question — which CHANNEL each target arrived in. + * * ## The close mechanism, measured (#8293) * * A half-delivered card (#8131) was closed `completed` two seconds after its @@ -910,8 +936,28 @@ export function h1DispatchedNoAssignee(issue) { * prose containing "claim" is not a claim comment. No `g` flag — a shared * regex carrying `lastIndex` between callers is a state bug waiting for its * second reader. + * + * ## The separator is ONE character, and that is a maintainer ruling (2026-08-11) + * + * `.claude/skills/pm-dispatch/SKILL.md` records it verbatim: 「首行以字面 + * `Claim:` 开头是机器判据(维护者 2026-08-11 裁定;巡查谓词只认这一个拼写且保持 + * 严格,修法是全舰队向文档拼写收敛,⛔ 不放宽谓词)」. So a dash-written or + * fullwidth-written claim is a MALFORMED claim, not an unrecognised dialect, + * and the repair direction is the WRITE side. ⛔ Do not widen this class to + * accept another separator — that is the one change the ruling closes. What + * the patrol gained instead is visibility: `h34ClaimShapedNonCanonicalSeparator` + * reports the near miss on its own row without redefining what a claim IS. + * + * ⚠️ The class was `[::]` until #12090 — U+003A written TWICE, not "ASCII or + * fullwidth" as three separate readers (this file's own H33 note, the filing + * card's body, and a grading comment) each assumed from its shape. U+FF1A has + * never matched here, measured by codepoint and by a live probe over 172 + * dispatched cards (0 fullwidth claims). Collapsed to a single `:` so the code + * stops implying an affordance it never honored; behaviour is byte-identical, + * and the fullwidth spelling now surfaces on the H34 row like every other + * non-canonical separator. */ -export const CLAIM_COMMENT_MARKER = /^\s*>?\s*Claim(?:ed)?\s*[::]/mi; +export const CLAIM_COMMENT_MARKER = /^\s*>?\s*Claim(?:ed)?\s*:/mi; export function h2AssigneeNoClaimComment(issue, commentBodies) { const labels = labelNames(issue); @@ -2924,16 +2970,21 @@ export function blockerTargetKey(ref, ownerRepo = OWNER_REPO) { * open listing by construction) — a permanent no-op that costs a row of noise * in every explanation of what H19 read. * - * ## The comment channel's stated boundary + * ## The comment channel is UNGATED here (#11747) * - * `commentBodies` is whatever the sweep's gated fallback read, and that gate - * (`needsBlockedByComments`) skips a card whose BODY already carries a line. - * So a card with a body line AND a second, different blocker parked in a - * comment has its comment-borne target invisible to H19 — a bound inherited - * from the gate, not a decision taken here. `undefined` (unconsulted) and - * `null` (consulted, unreadable) both contribute nothing; the `null` case is - * a card H4 is already firing on with a sentence that says the thread could - * not be read, which is the louder and more accurate place for it. + * `commentBodies` is what `needsBlockerLivenessComments` gathered, and that + * gate is the label alone — it does NOT skip a card whose body already carries + * a line, which is where `needsBlockedByComments` (H4's gate, correctly) stops. + * The bound this note used to record was a real defect: a card with a body line + * AND a second, different blocker parked in a comment had its comment-borne + * target invisible here, so a RE-PARK — body line spent, new blocker in a + * comment — resolved only the closed target and published a false unlock + * candidate. Both channels now, unconditionally, for every `pm:blocked` card. + * + * `undefined` (unconsulted) and `null` (consulted, unreadable) still contribute + * nothing; the `null` case is a card H4 is already firing on with a sentence + * that says the thread could not be read, which is the louder and more accurate + * place for it. * * @param {object} issue * @param {string[]|null|undefined} commentBodies @@ -2974,6 +3025,36 @@ export function needsBlockerLiveness(issue) { return labelNames(issue ?? {}).includes('pm:blocked'); } +/** + * Which cards buy a comment fetch FOR THE LIVENESS READ — deliberately NOT + * `needsBlockedByComments`, and the difference is the defect this gate exists + * to end (#11747). + * + * That gate skips any card whose BODY already carries a `Blocked-by:` line, + * which is exactly right for the question IT serves: H4 asks whether the author + * left the machine anything at all, and a body line answers that without the + * network. H19 and H26 ask a different question — is what the line names still + * RUNNING — and for that question a body line is not an answer, it is one + * channel's worth of targets. Borrowing H4's gate made the liveness read resolve + * ONLY the body target on precisely the cards where the body is most likely to + * be spent: a RE-PARK. A seat that finds the body's upstream closed, cards the + * real prerequisite and writes the new blocker into a comment leaves a card + * whose body names a closed issue and whose comment names an open one — and the + * gated read saw only the closed one, published "the block has outlived its + * blocker", and a card was released into an open blocker on the strength of it. + * + * So the liveness read is ungated: every `pm:blocked` card contributes both + * channels, always. The cost is the gate's own complement — one comment fetch + * per blocked card that HAS a body line (15 of 33 in the 2026-08-24 census), + * bounded by an inventory the sweep already pages and paid once per sweep off + * the SHARED comment cache, so a card H2/H4/H17 already fetched costs nothing. + * ⛔ H4's gate is deliberately left alone: making the cheap question expensive + * would buy nothing — a body line really does discharge the duty H4 audits. + */ +export function needsBlockerLivenessComments(issue) { + return needsBlockerLiveness(issue); +} + /** * How many targets a row names before it counts the rest — the same render * budget `BLOCKING_DEPENDENT_LIST_CAP` keeps, for the same reason (the @@ -2983,14 +3064,31 @@ export function needsBlockerLiveness(issue) { */ export const H19_TARGET_LIST_CAP = 5; -/** `#N` for a local target, `owner/repo#N` for a cross-repo one, + its note. */ +/** + * `#N` for a local target, `owner/repo#N` for a cross-repo one, + its note. + * + * An unresolved target carries the disambiguating repo reading when one was + * taken (#11218), so the CAUSE is legible per target rather than only in the + * aggregate sentence — a reader walking rows must be able to tell "we cannot + * see that repo" from "that number is not there" without leaving the row. + * `repoReadable === undefined` (no probe taken, e.g. a LOCAL target) renders + * exactly as it always did. + */ function namedTargets(rows) { const shown = rows.slice(0, H19_TARGET_LIST_CAP); const named = shown .map((r) => { const ref = `\`${r.local ? `#${r.number}` : r.key}\``; if (r.state === 'closed') return `${ref}${r.closedAt ? ` (closed ${r.closedAt})` : ' (closed)'}`; - if (r.state === 'unresolved') return `${ref}${r.detail ? ` (${r.detail})` : ''}`; + if (r.state === 'unresolved') { + const why = + r.repoReadable === false + ? `${r.detail ? `${r.detail}; ` : ''}\`${r.repo}\` is NOT readable to this sweep's credential` + : r.repoReadable === true + ? `${r.detail ? `${r.detail}; ` : ''}\`${r.repo}\` IS readable, so that number is not there` + : r.detail ?? ''; + return `${ref}${why ? ` (${why})` : ''}`; + } return ref; }) .join(', '); @@ -3010,13 +3108,47 @@ function namedTargets(rows) { * target FIRES a row — a quieter one, which says the liveness is unjudged * rather than asserting anything about the block. * - * What the row deliberately does NOT do is name a CAUSE for an unresolved - * target. A 404 on `owner/repo#N` is equally "that repo is not reachable to + * ## The cause is MEASURED now, not guessed — and still never inferred (#11218) + * + * This row used to refuse to name a CAUSE for an unresolved target, on solid + * grounds: a 404 on `owner/repo#N` is equally "that repo is not reachable to * this credential" and "that issue number does not exist in a perfectly - * reachable repo", and this file's standing posture is to refuse to name what - * it cannot distinguish (the transport classifier's narrowness, and H16's - * refusal to vouch for an `unknown` mergeability). The observation — the ref - * and the HTTP status — is reported; the diagnosis is the reader's. + * reachable repo", and this file's standing posture is to refuse to name what it + * cannot DISTINGUISH (the transport classifier's narrowness, H16's refusal to + * vouch for an `unknown` mergeability). + * + * The two ARE distinguishable, by one extra reading, and refusing to make it was + * leaving a real fact on the floor. `GET /repos//` answers whether + * this credential can see the repo AT ALL, independently of any issue number in + * it. Repo readable + issue 404 ⇒ the number does not exist there. Repo 404 ⇒ a + * CREDENTIAL SCOPE gap, and the target is unjudgeable for a reason that has + * nothing to do with the card. That is the same two-stage shape + * `classifyRepoRead` already uses on the swept repo — a second reading turning + * an ambiguous refusal into a named one — and it costs ONE request per distinct + * SIBLING repo per sweep (2 on this board), cached, never per target. + * + * ⛔ What did NOT change is the posture: the row still reports only what it + * OBSERVED. The probe is a measurement, not an inference, and where it is + * unavailable the wording falls back to the old undiagnosed sentence rather than + * guessing. + * + * ## Why this is the whole of the cross-repo half that CAN land here + * + * The unresolvable class is cross-repo by construction — the contract-first + * split manufactures it (a parent plus one sub-issue per repo, the downstream + * carrying `Blocked-by:`). Giving the resolver a genuine cross-repo READ needs a + * credential the patrol does not hold and by standing ruling will not be given: + * the workflow header states it (「⛔ Each install uses its OWN + * `secrets.GITHUB_TOKEN` and reads its own repo. No cross-repo credential, no + * matrix over repos, no PAT」 — refused at grading, per-repo install chosen + * instead), and Actions' own token is repo-scoped by construction, so this is not + * a knob this file could turn even if it wanted to. ⛔ Widening it is a + * routing/security decision and not this script's to take. + * + * The accepted consequence, named in that same ruling, is that a cross-repo + * target stays UNJUDGED in each install. This row's job is therefore to make + * that UNJUDGED honest and LOUD rather than to pretend it away — which is what + * the probe above and the anti-truncation handling in `renderMarkdown` do. * * ## A PARTIAL discharge is reported as partial, not as an unblock * @@ -3079,14 +3211,26 @@ export function h19BlockOutlivedBlocker(issue, resolutions) { ); } + const scoped = unresolved.filter((r) => r.repoReadable === false); + const scopeNote = + scoped.length === 0 + ? ' A cross-repo target resolves only when its repo answers this sweep\'s credential.' + : ` ⚠️ ${scoped.length} of them are unjudgeable for a reason that has NOTHING to do with this ` + + 'card: the repo itself does not answer this sweep\'s credential (measured directly, by a ' + + 'separate `GET /repos//` — not inferred from the issue 404). That is the ' + + 'cross-repo class the contract-first split manufactures, and it is a standing, ACCEPTED ' + + 'limit rather than a defect to chase: each patrol install reads its own repo with its own ' + + 'repo-scoped token by ruling, so no re-run and no re-read of this card will ever resolve ' + + 'these. ⛔ Do not "fix" it on the card — judge the target BY HAND, or take a credential ' + + 'change to routing/security, whose call it is.'; return ( `\`pm:blocked\` and ${unresolved.length} of ${rows.length} \`Blocked-by:\` target(s) could NOT be ` + `resolved this sweep (${namedTargets(unresolved)}) — so whether this block has outlived its blocker ` + 'is UNJUDGED, not confirmed. Unread is not still-open (#4690): a target dropped in silence reads as ' + 'a healthy block forever, which is the exact failure this item exists to end, so it is named here ' + - 'instead. A cross-repo target resolves when its repo answers this sweep\'s credential; the status is ' + - 'reported and the cause is not guessed at (a 404 is equally an unreachable repo and a number that ' + - 'does not exist).' + + 'instead. ⚠️ UNJUDGED is not a quiet row and must not be skimmed as one: this card\'s block is ' + + 'exactly as unverified as if nothing had been read at all.' + + scopeNote + (open.length > 0 ? ` The card's other ${open.length} target(s) did resolve, and are still open.` : '') + @@ -3214,11 +3358,18 @@ export const H20_BRANCH_LIST_CAP = 5; * tolerated — 「Branch: `claude/issue-10312-…`」 is the natural markdown for a * line meant to be grepped, and the same decorated-directive lesson H4 paid * for (#10102) applies verbatim here. + * + * ⚠️ The separator class here carried the SAME duplicated-U+003A typo the claim + * marker did (`[::]`, two ASCII colons, never the fullwidth U+FF1A its shape + * implied) and is collapsed with it in #12090 — byte-identical behaviour, one + * fewer place where the code reads as if it honoured a spelling it does not. + * See `CLAIM_COMMENT_MARKER`'s note for the codepoint reading and the live + * probe behind it. */ export function claimedBranches(body) { const out = []; const text = String(body ?? ''); - for (const line of text.matchAll(/^\s*>?\s*Branch(?:es)?\s*[::]\s*(.*)$/gim)) { + for (const line of text.matchAll(/^\s*>?\s*Branch(?:es)?\s*:\s*(.*)$/gim)) { for (const hit of String(line[1] ?? '').matchAll(CLAIM_BRANCH_SHAPE)) { if (!out.includes(hit[0])) out.push(hit[0]); } @@ -3661,7 +3812,7 @@ export const PM_RESIDUE_LABELS = [ * * `floor` is the optional dated closure floor (see `resolveClosureFloor`): a * `Date` before which a closed card is out of scope, or null for "judge every - * card in the window", which is the default and upstream's own behaviour. + * card in the window", which is the default and this repo's own behaviour. * * ⚠️ A card whose `closed_at` cannot be read is judged, NOT skipped. The floor * is a scope decision that needs a date to make; without one the card's @@ -4022,6 +4173,337 @@ export function h25AwaitingMaintainerExclusivity(issue) { ); } +// --------------------------------------------------------------------------- +// H29 — the pm state labels are ONE-OF, GENERALLY (#11179). +// +// H3 and H25 are both this invariant, each pinned to the carrier that was +// measured drifting: H3 to the one pair (`pm:queue` + `pm:dispatched`), H25 to +// the one label (`pm:awaiting-maintainer`, written while its population was +// still zero). Between them the vocabulary has six members and fifteen pairs, +// and eleven of those pairs had no reader at all — including the two this card +// was filed on: +// +// • `pm:queue` + `needs-user-decision` — the state model defines `pm:queue` +// as 「无可问之事」, so the pair is a card that is simultaneously ready to +// dispatch and waiting on a ruling. The measured seat behaviour was exactly +// that: the analysis was posted, the decision label went on, and the queue +// label was never taken off — 「判断做了(有分析产出),状态写入没做」. +// • `pm:queue` + `pm:blocked` — the unlock/park transitions are two +// INDEPENDENT label writes with no exclusivity invariant between them, so a +// half-finished park leaves both. The measured specimen sat dual-hung for +// three days. +// +// LIVE at the time of writing (2026-08-24 board read): #11534 carries +// `needs-user-decision` + `pm:blocked` — a third pair, in a third direction, +// which is the point: pinning pairs one at a time is how the family kept +// producing a new unreported shape. This row asks the invariant itself. +// +// ## It reports the pairs no other row owns, and only those +// +// A breach must be reported ONCE. H3 owns `pm:queue` + `pm:dispatched` (with +// its own measured specimen and its own sentence) and H25 owns every pair +// containing `pm:awaiting-maintainer` (with a per-label clause naming the +// specific lie). So this row skips exactly those and reports the remainder — +// and on a card carrying THREE states it still reports the pairs the others do +// not, rather than going silent because one of them fired. Both exclusions are +// pinned in the self-test, in both directions: the excluded pair is silent +// HERE and the owning row does fire on it. +// +// ## Free, and report-only +// +// Two label reads on a card the sweep already holds — no request. ⛔ Never a +// label written from this script: which state is TRUE is a judgement about the +// card (is it waiting on a ruling, or on a blocker, or on nothing?), and the +// same half-written transition that produced the pair would be reproduced by a +// sweeper guessing at it. The row names both claims and asks for ONE write. +// --------------------------------------------------------------------------- + +/** + * The ONE-OF vocabulary, in ONE place: the awaiting state plus the five it + * excludes. Derived from H25's list rather than re-typed, so the two rows can + * never disagree about what a "pm state" is — the same single-constant + * discipline `AWAITING_MAINTAINER_LABEL` itself was introduced with, and the + * failure family this whole file belongs to. + * + * ⚠️ This is the THIRD `pm:*` label set in this file, and the three are + * deliberately different questions with deliberately different answers. Do not + * unify them on the strength of the similar names — the self-test pins all + * three pairwise: + * + * `PM_STATE_LABELS` (H13) "does any label make this card VISIBLE to a + * named reader?" — so it carries `finding`, `pm:epic` + * and `pm:seat`, none of which is a position on the + * work state machine. + * `PM_RESIDUE_LABELS` (H22) "does this label CLAIM work is in flight?" — + * so it carries `pm:blocking` (a derived priority + * cache, not a state) and drops `needs-user-decision` + * (a fine state to close in). + * `PM_EXCLUSIVE_STATE_LABELS` (H25/H29) "is this a position the card can be + * IN, such that two of them contradict?" — identity + * stickers (`pm:seat`, `pm:epic`) legally coexist + * with any state and are out; `pm:blocking` and + * `pm:retriage` are annotations ON a state and are + * out; `finding` is a card KIND rather than a + * position and is out. `needs-user-decision` is IN, + * because a card awaiting a ruling is somewhere, and + * somewhere else is a contradiction. + */ +export const PM_EXCLUSIVE_STATE_LABELS = [ + AWAITING_MAINTAINER_LABEL, + ...AWAITING_MAINTAINER_EXCLUSIVE_LABELS, +]; + +/** + * What each state claims ON ITS OWN — one clause, so a row names the two + * contradicting claims rather than complaining that two labels are present. + * + * Deliberately NOT merged with H25's `AWAITING_MAINTAINER_CONFLICT_REASON`: + * that map says what a pairing WITH THE AWAITING STATE specifically lies about + * (it reads as the second half of one sentence), while this one says what the + * label asserts by itself, which is what a general pair needs on both sides. + * The self-test pins that every `PM_EXCLUSIVE_STATE_LABELS` member has an entry, so the + * vocabulary cannot be half-extended the way four string literals would be. + */ +export const PM_STATE_CLAIM = { + 'pm:queue': 'dispatchable NOW, with nothing left to ask', + 'pm:dispatched': 'an agent is working it under a live claim', + 'pm:blocked': 'it cannot start until a `Blocked-by:` target closes', + 'pm:on-hold': 'it is parked behind a machine-fireable `Restart-when:`', + [AWAITING_MAINTAINER_LABEL]: 'its remaining work is a manual maintainer action', + 'needs-user-decision': 'a maintainer RULING is owed before anything can move', +}; + +/** A pair as an order-independent key, so the exclusions cannot depend on label order. */ +const pmStatePairKey = (a, b) => [a, b].sort().join('|'); + +/** + * The pairs another row already reports, by key. `pm:queue` + `pm:dispatched` + * is H3's; every pair containing the awaiting label is H25's (handled by the + * label test below rather than enumerated, so a future member added to + * `AWAITING_MAINTAINER_EXCLUSIVE_LABELS` is covered without a second edit). + */ +const H3_PAIR_KEY = pmStatePairKey('pm:queue', 'pm:dispatched'); + +/** H29 — null when at most one state claim stands, else the finding sentence. */ +export function h29PmStateExclusivity(issue) { + if (issue?.state === 'closed') return null; + const labels = labelNames(issue ?? {}); + const present = PM_EXCLUSIVE_STATE_LABELS.filter((l) => labels.includes(l)); + if (present.length < 2) return null; + const pairs = []; + for (let i = 0; i < present.length; i++) { + for (let j = i + 1; j < present.length; j++) { + const a = present[i]; + const b = present[j]; + if (a === AWAITING_MAINTAINER_LABEL || b === AWAITING_MAINTAINER_LABEL) continue; // H25's + if (pmStatePairKey(a, b) === H3_PAIR_KEY) continue; // H3's + pairs.push([a, b]); + } + } + if (pairs.length === 0) return null; + const named = pairs + .map(([a, b]) => `\`${a}\` (${PM_STATE_CLAIM[a]}) + \`${b}\` (${PM_STATE_CLAIM[b]})`) + .join('; '); + return ( + `two pm STATE labels on one card — ${named} — and the state labels are ONE-OF: each is a ` + + 'claim about where the card IS, so two of them leave the queue view, the lane view, the ' + + 'unlock scan and the decision inbox to pick which one they believe, and every one of them ' + + 'picks differently. The measured origin is never a disagreement about the card: it is a ' + + 'TRANSITION written as an ADD instead of a REPLACE — the judgement was made and posted, and ' + + 'the half of the write that costs nothing but bookkeeping (dropping the state being left) ' + + 'was skipped. `pm:queue` in particular is defined as 「无可问之事」, so pairing it with any ' + + 'other state contradicts its own definition rather than merely competing with it. Remedy: ' + + 'decide which ONE state is true and drop the rest in a single write — and write every ' + + 'transition as replace-not-add so the pair cannot recur. Report-only: ⛔ never a label ' + + 'written from this script, because which state is true is a judgement about the card and a ' + + 'sweeper guessing at it would reproduce the very half-write that made the pair.' + ); +} + +// --------------------------------------------------------------------------- +// H30 — a `pm:queue` card rotting unclaimed (#11179). +// +// `pm:queue` is the one ACTIVE state on the board: it asserts the card is +// dispatchable now, with nothing left to ask. Every other aged row here +// (H10/H11/H12/H13/H18) watches a state where waiting is legal and asks whether +// the wait has gone too long. This one watches the state where waiting is not a +// state at all, and asks why nothing happened. +// +// The measured incident: three cards left in `pm:queue` while the seat that +// owned them had already produced the analysis that should have moved them +// (「判断做了(有分析产出),状态写入没做(纯开销的那半)」). Nothing on the +// board said so, because a queued card looks exactly like a queued card no +// matter how long it has been one — the queue view's ordinary contents and a +// forgotten card are the same rows. +// +// ## The horizon, and why it is NOT H11's 7 days +// +// The aging SHAPE is H11's — `updated_at`, report-only, threshold named in the +// row, an unreadable stamp flagging rather than reading as fresh. The NUMBER is +// not, and reusing it would have been the mistake: 7 days is calibrated for a +// PARKED state, where a legitimate short park has cleared by then. Measured +// against all 40 open `pm:queue` cards on 2026-08-24: +// +// >1d 17 · >2d 10 · >3d 8 · >4d 4 · >5d 3 · >7d 0 +// +// At 7 days the row cannot fire on today's board at all — a check that cannot +// fail is the shape this file exists to catch, not to add. At 1 day it reports +// 43% of the queue, which is queue DEPTH rather than rot. 3 days is the +// smallest horizon that clears the ordinary depth while still exceeding the +// measured dual-hang this card was filed on (3 days), and it names 8 of 40 — +// a minority a human can actually walk. +// +// ## What the row asks for, and what it refuses to judge +// +// It does NOT say the card is wrong, and it does not rank it. It forces ONE +// explicit transition — dispatch it, convert it to `needs-user-decision`, +// withdraw it, or rewrite it — because the failure this closes is a decision +// that was made and never written down. Report-only, and pointedly: a sweeper +// that re-labelled here would be choosing the transition, which is the whole +// judgement. ⛔ Never a label written from this script. +// --------------------------------------------------------------------------- + +/** + * H30 threshold — 3 days, derived above from the live distribution rather than + * inherited from H11's parked horizon. Days rather than hours because the queue + * is legitimately deep: the unit has to be one a reader would call "sat there". + */ +export const QUEUE_ROT_STALE_DAYS = 3; + +/** H30 — null when clean, else the finding sentence. */ +export function h30QueueRotting(issue, nowMs = Date.now()) { + if (issue?.state === 'closed') return null; + if (!labelNames(issue ?? {}).includes('pm:queue')) return null; + const updated = Date.parse(issue?.updated_at ?? ''); + const ageDays = Number.isFinite(updated) ? (nowMs - updated) / 86_400_000 : null; + if (ageDays !== null && ageDays <= QUEUE_ROT_STALE_DAYS) return null; + const reading = + ageDays === null + ? 'an unreadable `updated_at` (which must not read as fresh)' + : `~${Math.round(ageDays)}d with no activity of any kind (threshold ${QUEUE_ROT_STALE_DAYS}d)`; + return ( + `\`pm:queue\` with ${reading} — the queue is the one state that asserts the card is ` + + 'dispatchable NOW with nothing left to ask, so a card sitting in it is not inventory the ' + + 'way a parked card is: it is a card the lane keeps passing over. The measured shape is a ' + + 'judgement that WAS made and never written (「判断做了(有分析产出),状态写入没做」) — the ' + + 'analysis lands in a comment and the state stays where it was, which is indistinguishable ' + + 'from an ordinary queued card at every glance. This row does not judge the card and does ' + + 'not rank it: it asks for ONE explicit transition — dispatch it, convert it to ' + + '`needs-user-decision` if it turns out to carry an unanswered question (the queue means ' + + '「无可问之事」), park it with a machine-fireable exit, withdraw it, or rewrite a premise ' + + 'that no longer holds. Report-only: ⛔ never a label written from this script — choosing ' + + 'which of those transitions applies is the whole judgement.' + ); +} + +// --------------------------------------------------------------------------- +// H31 — the contract-review gate carried on ONE of its two carriers (#11179). +// +// `needs:contract-review` is a DUAL-carrier gate: the ruling +// (maintainer 2026-08-22, 「简化一点是否可以直接挂 PR 侧」「两边都挂好」) puts it +// on the card AND on the PR, hung in one stroke and — the half that failed — +// cleared in one stroke, each carrier written through the label discipline's +// read-modify-write + read-back (SKILL.md 2026-08-18: 「承载闸门语义的标签……挂与 +// 清两向同此四步,闸门被剥不是红灯是放行」). +// +// Two writes, one postcondition, and nothing ever checked the pair. The +// measured miss: a PASS verdict was posted, the PR carrier was cleared, and the +// card carrier was not — so the card stayed gated behind a review that had +// already passed, and the only evidence that anything was wrong was the label +// itself, on a card nobody was looking at. +// +// The other direction is the dangerous one and the same row catches it: a gate +// stripped from the card while the PR still carries it reads, to the enqueue +// path, as a card that was never gated. 「闸门被剥不是红灯是放行」 — a stripped +// gate is a GREEN light, and 「被剥」 and 「从未挂过」 are indistinguishable in +// the evidence. A row comparing the two carriers is the only reader that can +// tell them apart. +// +// LIVE at the time of writing (2026-08-24): card #11427 carries the gate while +// its delivering open PR #11844 does not. +// +// ## The silence that is NOT a bug: a card with no delivering open PR +// +// `references/contract-review.md` makes card-side-FIRST legal and expected: +// 「PR 一存在即挂,报告先于 PR 到达则先挂卡侧、ACCEPT 时补齐 PR 侧」. So a gated +// card with no PR yet is a correct intermediate state, not a premature hang, +// and this row stays silent on it — deliberately declining the "gate label on a +// card with no PR carrier is premature" shape, which would report the protocol's +// own prescribed sequence as a defect. The comparison begins when a delivering +// PR exists, which is exactly when the pair becomes checkable. +// +// ## Free, and bounded +// +// The open-PR listing is already in hand (H7/H12/H21 list it, H8 already passes +// it around for the same delivery question), and the delivery relation is +// `prDeliversCard` — the same body-first/branch-fallback relation H8 reads, so +// this row can never disagree with H8 about which PR delivers which card. No +// request, no new parser. MERGED PRs are deliberately out of scope: the gate +// governs enqueue and landing while the PR is open, and a merged carrier is a +// closed-out stroke rather than a live half-write. +// +// Report-only, and emphatically: this is a GATE. ⛔ Never a label written from +// this script — a sweeper that hung or cleared a review gate would be issuing +// the review verdict, and the one thing the whole clause-② chain forbids is +// 自查放行. +// --------------------------------------------------------------------------- + +/** The clause-② gate label — one constant, both carriers. */ +export const CONTRACT_REVIEW_LABEL = 'needs:contract-review'; + +/** + * H31 — null when the two carriers agree (or the comparison is not yet + * possible), else the finding sentence. + * + * A PR row whose `labels` is not an array is one this sweep could not read, and + * it is EXCLUDED from the comparison rather than counted as unlabelled: reading + * an unreadable carrier as a bare one would manufacture a finding out of a read + * failure, which is the #4690 direction this file keeps in the one place it + * actually matters — the direction that invents evidence. + * + * @param {object} issue — an OPEN issue. + * @param {object[]} openPrs — the open-PR listing the sweep already holds. + */ +export function h31ContractReviewCarrierSplit(issue, openPrs) { + if (issue?.state === 'closed') return null; + const n = String(issue?.number ?? ''); + if (!n || n === '0') return null; + const delivering = (openPrs ?? []).filter( + (pr) => pr && !pr.merged_at && Array.isArray(pr.labels) && prDeliversCard(pr, n), + ); + if (delivering.length === 0) return null; // card-side-first is legal — see the header note. + const cardGated = labelNames(issue ?? {}).includes(CONTRACT_REVIEW_LABEL); + const gatedPrs = delivering.filter((pr) => labelNames(pr).includes(CONTRACT_REVIEW_LABEL)); + const barePrs = delivering.filter((pr) => !labelNames(pr).includes(CONTRACT_REVIEW_LABEL)); + const list = (prs) => prs.map((p) => `#${p.number}${p.draft ? ' (draft)' : ''}`).join(', '); + const contract = + 'The gate is a DUAL carrier — 「两边都挂好」, hung in one stroke and cleared in one ' + + 'stroke, each carrier written read-modify-write with a READ-BACK ' + + '(「闸门被剥不是红灯是放行」: a stripped gate is a GREEN light, and 「被剥」 and 「从未挂过」 are ' + + 'indistinguishable in the evidence, so the read-back is the only way either is ever ' + + 'noticed). Report-only: ⛔ never a label written from this script — hanging or clearing a ' + + 'review gate from a sweeper would be issuing the verdict, which is 自查放行.'; + if (cardGated && barePrs.length > 0) { + return ( + `\`${CONTRACT_REVIEW_LABEL}\` on the CARD while its delivering open PR ${list(barePrs)} ` + + 'does NOT carry it — the two carriers of one gate disagree, so the pair was written half ' + + 'way: either the hang never reached the PR side (「PR 一存在即挂」, and the PR exists), or ' + + 'a PASS cleared the PR side and stopped there, leaving the card gated behind a review that ' + + `has already passed. ${contract}` + ); + } + if (!cardGated && gatedPrs.length > 0) { + return ( + `\`${CONTRACT_REVIEW_LABEL}\` on the delivering open PR ${list(gatedPrs)} while the CARD ` + + 'does NOT carry it — the more dangerous half of the same split: to the enqueue path an ' + + 'ungated card is a card that was never gated, so the review chain this PR is still waiting ' + + 'on is invisible to the queue, and the card can be enqueued straight past a gate that is ' + + `demonstrably still live one carrier over. ${contract}` + ); + } + return null; +} + // --------------------------------------------------------------------------- // H26 — a block whose target can never CLOSE, and the stale chain (#11219). // @@ -4267,117 +4749,1817 @@ export function claimDelivery(n, openPrs, mergedPrs) { * comparison is not a "no" (#4690), and collapsing it would let one unparseable * date manufacture a finding about a card nobody measured. */ -export function branchMovedSinceClaim(refState, claim) { - const head = Date.parse(refState?.headCommittedAt ?? ''); - const posted = Date.parse(claim?.createdAt ?? ''); - if (!Number.isFinite(head) || !Number.isFinite(posted)) return null; - return head > posted; +export function branchMovedSinceClaim(refState, claim) { + const head = Date.parse(refState?.headCommittedAt ?? ''); + const posted = Date.parse(claim?.createdAt ?? ''); + if (!Number.isFinite(head) || !Number.isFinite(posted)) return null; + return head > posted; +} + +/** + * H27 — null when clean, else the finding sentence. + * + * ## The conjunction, and why each term is load-bearing + * + * `pm:dispatched` the card still claims to be in flight + * claim older than 24h the protocol's own stale line + * a claimed branch EXISTS (else it is H20's row, not this one) + * NO branch moved since the claim — nothing was pushed for this dispatch + * no PR delivers the card neither open nor within the merged window + * + * ⛔ Dropping the branch-activity term would give exactly the PR-keyed row H20 + * refuses to be, and it is refused there for a measured reason: a dev inside a + * long build legitimately has a ref and no PR for over an hour. That objection + * is answered here by BOTH remaining terms and not by the threshold alone — a + * dev 24 hours in with commits landing is excluded by branch activity, and a + * dev with a PR open is excluded by delivery. What is left is a branch that has + * not moved since it was claimed, with nothing to show for a day. + * + * ## What it under-reports, stated rather than discovered + * + * A dev that pushed one commit and THEN died is not reported: its branch moved + * after the claim, so the activity term clears it. That is the measured shape + * of one of the three incident cards, and widening the term to "no activity in + * the last 24h" would catch it — at the cost of colliding with the protocol's + * 「有带提交活分支的认领永不回收」, which is a rule about exactly that card. + * Under-reporting on a card the protocol protects is the same call H17's + * extractor and H20's branch-shape matcher make: a row a reader cannot act on + * is worse than no row. + * + * @param {object} issue — an OPEN issue. + * @param {{ branches: string[], createdAt: string|null }|null} claim + * @param {{ branch: string, state: 'exists'|'absent'|'unreadable', + * headCommittedAt?: string|null }[]} refStates + * @param {{ open: number, merged: number }} delivery — `claimDelivery`. + */ +export function h27DeadClaimNoProgress(issue, claim, refStates, delivery, nowMs = Date.now()) { + if (!labelNames(issue ?? {}).includes('pm:dispatched')) return null; + if (!claim || (claim.branches ?? []).length === 0) return null; + const age = claimAgeHours(claim, nowMs); + if (age !== null && age <= DEAD_CLAIM_STALE_HOURS) return null; + + const rows = refStates ?? []; + if (rows.length === 0) return null; + const present = rows.filter((r) => r.state === 'exists'); + // No ref at all is H20's row; an unreadable probe is H20's quieter one. This + // row speaks only about branches it KNOWS are there. + if (present.length === 0) return null; + + // A delivery in either channel ends the question: an open PR is live work (or + // work already handed over), and a merged one is H8's row about a paired + // write, never this row's about a dead agent. + const { open = 0, merged = 0 } = delivery ?? {}; + if (open > 0 || merged > 0) return null; + + const moved = present.map((r) => branchMovedSinceClaim(r, claim)); + if (moved.some((m) => m === true)) return null; + + const named = namedBranches(present.map((r) => ({ branch: r.branch, state: r.state }))); + const recovery = + ' Report-only, and pointedly NOT a reclaim: the protocol reclaims a claim whose branch does ' + + 'NOT exist and states 「有带提交活分支的认领永不回收」, so a row about a branch that DOES ' + + 'exist can never be authority to drop an assignee. The remedy is the post-kill recovery ' + + 'inspection (`references/dispatch-runbook.md`): probe the claimant, then read all THREE ' + + 'states — on the remote / on the container disk only / gone — and hand anything found to a ' + + 'replacement flagged UNVERIFIED. ⛔ Never a label written from this script.'; + + if (moved.some((m) => m === null)) { + return ( + `\`pm:dispatched\` with a complete claim naming ${named}, and whether that branch has MOVED ` + + 'since the claim could not be determined this sweep (an unreadable claim or head-commit ' + + 'timestamp) — so this dispatch is UNJUDGED, not confirmed healthy. Unread is not "no ' + + 'activity" and it is not "activity" either (#4690); a liveness comparison dropped in ' + + 'silence reads as a working dev forever, which is the exact failure this item exists to ' + + 'end. Read the branch and the claim by hand.' + + recovery + ); + } + + const reading = + age === null + ? 'an unreadable claim timestamp (which must not read as fresh)' + : `~${Math.round(age)}h after the claim was posted (threshold ${DEAD_CLAIM_STALE_HOURS}h, the ` + + "protocol's own stale-claim line)"; + + return ( + `\`pm:dispatched\` with a PERFECT claim — assignee set, a first-line \`Claim:\` comment, and ` + + `${named} present on the remote — that has NOT MOVED SINCE IT WAS CLAIMED, with no PR ` + + `delivering the card, ${reading}. This is what a dev agent that DIED leaves behind, and the ` + + 'measured cause arrives in batches rather than singly: one shared-account capacity limit ' + + 'killed three concurrently-dispatched agents at once. ⭐ The card is indistinguishable from ' + + 'healthy in-flight work from the card itself — every field is correct, which is why no ' + + 'predicate here fired on it: H1 wants a missing assignee, H2 a missing claim comment, H8 a ' + + 'merged PR, and H20 no remote ref at all. H20 misses it BY CONSTRUCTION, not by accident: ' + + 'the dev-agent definition makes pushing the empty branch the first action of the task, so a ' + + 'protocol-compliant agent that dies still leaves a ref. Left unreported it does worse than ' + + "sit there — the next PM's round-open mutual-exclusion read treats a dead `Claim:` as a live " + + 'claim by another session and stays off the card, so one dead agent blocks the lane. ⛔ Rule ' + + 'out one reading first: a delivery that merged BEFORE this sweep\'s merged window ' + + `(${MERGED_WINDOW_PAGES} pages) is invisible here, so a card whose PR landed days ago and ` + + 'whose branch was never deleted can reach this row — check the card for a merged delivery ' + + 'before treating it as a death.' + + recovery + ); +} + +// --------------------------------------------------------------------------- +// H28 — the STALE BODY LINE that shadowed the live blocker (#11747). +// +// The body is the canonical home for `Blocked-by:`; a comment is a legal second +// channel, deliberately, because rewriting a body through the MCP escaping +// hazard is the riskier write. Those two facts are consistent right up to the +// moment a card is RE-PARKED, and then they collide: the seat that finds the +// body's upstream closed cards the real prerequisite, writes the NEW blocker +// into a comment — the cheap, safe write — and leaves the SPENT one in the +// body. The card is now stating two blockers, one of them a fact about the +// past, and the machinery cannot tell which is current from the line alone. +// +// ## Why ungating the liveness read is necessary and NOT sufficient +// +// Before `needsBlockerLivenessComments`, the liveness read borrowed H4's gate +// and so read only the body on exactly these cards: it resolved the closed +// target, found nothing else, and published "every target it names is closed: +// nothing this card declared a wait on is still running" — a false unlock +// candidate, three sweeps running, acted on once. Ungating fixes the falsehood: +// the same card now resolves both targets and H19 reports 1 of 2 closed, a +// PARTIAL discharge that says the card may still be legitimately blocked. +// +// But PARTIAL is where H19's duty ends. It reports the block, not the WRITE +// that produced the ambiguity, and it says nothing about which channel is +// carrying the live blocker — so the stale body line survives, and the next +// re-park writes the same shape again. This row is the pair: it names the body +// line as spent, names the live blocker sitting in a comment, and asks for the +// migration. That is what makes the canonical-home doctrine enforced rather +// than merely written down — the failure this whole card measured is a doctrine +// that existed in prose and was not complied with, and prose is what failed. +// +// ## The exact shape, and what it deliberately does NOT fire on +// +// Fires only on the CONJUNCTION: a body-named target that is CLOSED **and** a +// comment-named target, absent from the body, that is OPEN. Each half alone is +// a different, healthy-or-already-reported state: +// +// - body closed, no live comment target -> H19's ordinary expired block. The +// line is spent and so is the wait; there is nothing to migrate. +// - body open + comment open -> two live blockers stated in two channels. +// Untidy, not wrong, and no row: both are current, and demanding a body +// rewrite for tidiness would push seats at the very write the comment +// channel exists to avoid. +// - a target named in BOTH channels -> not a migration candidate at all; the +// body already carries it. Only a comment-ONLY live target can be missing +// from the canonical home. +// - an UNRESOLVED target on either side is silent here. H19 already fires the +// unjudged sentence on that card (#4690), and a migration instruction built +// on a target this sweep could not read would be a guess. +// +// ## Quota +// +// Free. H19 and H26 already hold these resolutions; this row asks the same rows +// a third question — which CHANNEL each target arrived in — which the sweep can +// answer from bodies it has already read. +// +// Report-only. The remedy is a body rewrite by the owning seat; ⛔ never a +// label or a body written from this script. +// --------------------------------------------------------------------------- + +/** + * The canonical keys a card names in ONE channel — the split H28 needs and the + * only thing it adds to what H19 already computed. + * + * Deliberately built from the same `blockedByTargets` + `blockerTargetKey` pair + * the union uses, rather than a second parser: a channel split that recognised + * a different set of spellings than the union would report migrations for + * targets the union never resolved. + * + * @returns {Set} canonical `owner/repo#N` keys. + */ +export function blockerChannelKeys(text, issue, ownerRepo = OWNER_REPO) { + const keys = new Set(); + for (const ref of blockedByTargets(text)) { + const target = blockerTargetKey(ref, ownerRepo); + if (!Number.isFinite(target.number)) continue; + if (target.local && target.number === issue?.number) continue; + keys.add(target.key); + } + return keys; +} + +/** + * H28 — null when the body's line is not shadowing a live comment-borne + * blocker, else the finding sentence. + * + * @param {object} issue — an OPEN issue. + * @param {{ key: string, number: number, local: boolean, + * state: 'open'|'closed'|'unresolved', closedAt?: string|null }[]} resolutions + * @param {string[]|null|undefined} commentBodies — as gathered for the liveness + * read. `undefined`/`null` contribute no comment channel, so no row. + */ +export function h28StaleBodyBlockerLine(issue, resolutions, commentBodies, ownerRepo = OWNER_REPO) { + if (!needsBlockerLiveness(issue)) return null; + const rows = resolutions ?? []; + if (rows.length === 0) return null; + + const bodyKeys = blockerChannelKeys(issue?.body, issue, ownerRepo); + const commentKeys = new Set(); + for (const body of commentBodies ?? []) { + for (const key of blockerChannelKeys(body, issue, ownerRepo)) commentKeys.add(key); + } + if (bodyKeys.size === 0 || commentKeys.size === 0) return null; + + const spent = rows.filter((r) => r.state === 'closed' && bodyKeys.has(r.key)); + const live = rows.filter( + (r) => r.state === 'open' && commentKeys.has(r.key) && !bodyKeys.has(r.key), + ); + if (spent.length === 0 || live.length === 0) return null; + + return ( + `\`pm:blocked\` whose BODY names ${spent.length} CLOSED \`Blocked-by:\` target(s) ` + + `(${namedTargets(spent)}) while a COMMENT names ${live.length} that ${live.length === 1 ? 'is' : 'are'} ` + + `still OPEN (${namedTargets(live)}) and appear(s) nowhere in the body — so the body line is ` + + 'STALE: it states a wait that is over, and the wait that is actually running is parked in the ' + + 'channel the body is supposed to be the canonical home for. This is the written half of a ' + + 'RE-PARK: a seat found the body\'s upstream closed, carded the real prerequisite, and wrote ' + + 'the new blocker into a comment (the cheaper, safer write) without spending the body line. ' + + '⚠️ Read what that costs before the migration: until the liveness read was ungated this card ' + + 'resolved ONLY the closed body target and was published as a card whose every blocker had ' + + 'closed — a FALSE unlock candidate, and one such card was released to `pm:queue` while its ' + + 'real blocker was open and dispatched. The row now fires alongside H19\'s PARTIAL discharge ' + + 'rather than instead of it: H19 says the block is half-expired, this says WHICH half is ' + + 'documentation. Remedy: rewrite the body line to name the live blocker (the comment stays as ' + + 'history), so the next reader — human or sweep — finds the current wait in the canonical ' + + 'home. ⛔ Report-only: never a body or a label written from this script.' + ); +} + +// --------------------------------------------------------------------------- +// H32 — a HELD seat sitting idle over a non-empty lane queue (#11706). +// +// Every other row here watches a CARD or a PR. This one watches the SEAT, and +// it is the first: the patrol could see a card that nobody moved and could not +// see a lane whose seat had stopped moving it. The filing seat reported its own +// defect — it ended round after round with 「要我继续派吗?」 while in-flight was +// zero and 40+ dispatchable cards sat in its queue — against a skill clause that +// already says ⛔ 不等人闸 in as many words. So ⛔ this row adds no prose rule: +// the rule is not missing, the DETECTABILITY was (「规则不缺,措辞也不含糊」). +// +// ## The shape, and why it is the same two-signals-unpaired register as the rest +// +// The board asserts two things that cannot both be healthy: a lane that has +// dispatchable work and nothing in flight, and a seat that declares itself HELD. +// Either one alone is ordinary — an empty lane is a finished lane, and a held +// seat with work in flight is a working seat. Together they say the lane's +// throughput is zero while someone is on the clock for it, which is invisible +// from any single card: every queued card looks exactly like an ordinary queued +// card (that is H30's whole point), and the seat post looks exactly like a seat +// post. +// +// ## The threshold, derived the H30 way — from the measured distribution +// +// Grading declined to let this number be guessed and named the precedent +// (`QUEUE_ROT_STALE_DAYS`, derived from the live queue-age distribution). So it +// is measured, from the quantity the card itself names — the gap between one +// claim and the next on a lane that is working. +// +// Census, 2026-08-25, every `Claim:` comment on the six active objectstack +// lanes' open `pm:dispatched` cards plus the recently-closed window (n = 169 +// inter-claim gaps, per-lane then pooled): +// +// p50 7 min · p75 41 min · p90 324 min · p95 731 min · max 6073 min +// +// The distribution is BIMODAL and reading it as one hump is the trap: inside a +// dispatch wave a seat claims a batch minutes apart (hence p50 = 7), and between +// waves it goes quiet for hours. The upper mode is the one this threshold has to +// clear. Inspected individually, all 12 gaps above 480 min span a shift boundary +// or a night — `domain:devx` 2026-08-19T11:48 → 08-23T17:01, `domain:engine` +// 08-24T15:22 → 08-25T01:47 across a 收班/开轮 pair — i.e. NOT an active seat +// pausing, which is the population this row must never report. Nothing measured +// between waves WITHIN a held shift reached 480. +// +// 480 minutes (8h) therefore sits above the whole measured active-seat tail and +// below the cross-shift band that dominates everything past it. The patrol fires +// four times a day, so an idle seat surfaces on the second sweep after the +// threshold passes — a detection latency well inside the shift it is wasting. +// +// ⚠️ The threshold is the LAST gate, not the main one. Three structural gates in +// front of it do the real narrowing, and they are why this number can be this +// tight without manufacturing accusations. A false 怠工 row costs a working seat +// an argument; a late one costs six hours. The asymmetry is deliberate. +// +// ## Legitimate waits are excluded STRUCTURALLY, never by the threshold +// +// Grading was explicit: a seat whose latest marker names a live blocker (等 CI / +// 等裁决 / 等人工步骤, or an awaiting-class state) is exempt REGARDLESS of +// elapsed time. That is the difference between a seat that is idle and a seat +// that is waiting, and no amount of clock can tell them apart — only the seat's +// own declaration can. So the exemption is a read of the latest marker, and it +// is unbounded: a seat blocked for three days is not reported by this row. +// --------------------------------------------------------------------------- + +/** + * H32's idleness horizon — 8 hours, derived in the header above from the + * measured inter-claim distribution rather than chosen. Minutes rather than + * days (H30's unit) because the quantity it bounds is a within-shift interval: + * a seat's round is hours long, and a horizon in days could not fire inside the + * shift it is about. + */ +export const SEAT_IDLE_STALE_MINUTES = 480; + +/** + * Markers that put a seat in a DECLARED WAIT — the structural exemption, read + * off the seat's latest marker and unbounded by time. + * + * A closed set of measured terms, in `H17_TRIGGER_ANCHOR_TERMS`'s register and + * for its reason: an invented spelling would exempt nothing that exists, and a + * loose one would exempt everything. Both spellings of each wait are carried + * because seat posts are written in both languages and neither is canonical. + * + * ⚠️ Under-matching here FIRES the row on a seat that really is waiting, which + * is the expensive direction — so this list is the one part of H32 that should + * grow the moment a seat is seen declaring a wait in a spelling it lacks. + */ +export const SEAT_WAIT_MARKERS = [ + '等 ci', + '等ci', + '等裁决', + '等待裁决', + '等人工', + '等维护者', + '等人合', + '决策箱', + 'awaiting', + 'awaiting-maintainer', + 'blocked', + 'blocked-by', + 'needs-user-decision', + 'waiting on ci', + 'waiting for ci', + 'waiting on a ruling', + 'waiting on the maintainer', +]; + +/** + * Does this seat marker declare a live wait? Case-folded substring over the + * marker body — deliberately LOOSER than the anchored first-line reads + * elsewhere in this file, because the asymmetry runs the other way here: a + * missed exemption is a false accusation against a working seat, while an + * over-eager one merely keeps this row quiet on a seat a human can still see. + */ +export function seatDeclaresWait(markerBody) { + const text = String(markerBody ?? '').toLowerCase(); + return SEAT_WAIT_MARKERS.some((term) => text.includes(term)); +} + +/** + * The lane a seat post speaks for, parsed from its title, plus whether that + * lane is READABLE FROM THIS BOARD. + * + * Seat titles are `[PM seat] `, and the lane half is one of + * three measured shapes (2026-08-25 census, all 12 open seat posts): + * + * `domain:engine` — a lane on THIS board + * `domain:devx @ objectui` — a lane on a SIBLING board + * `repo:cloud` / `skills` / `triage (objectstack-wide)` + * + * The `foreign` flag is the load-bearing half and it exists for the same reason + * H19 refuses to guess at a cross-repo 404: this sweep reads ONE repo. A seat + * whose lane lives in a sibling repo has an inventory this patrol cannot see at + * all, so its queue reads as EMPTY here — and an empty queue makes this row + * silent, which is the harmless direction, but only by accident. Naming the + * class keeps the accident from turning into a finding the day the counting + * changes. `repo:*`-scoped and lane-less seats (`triage`) are foreign for the + * same reason: there is no `domain:*` label to count a lane inventory against. + * + * @returns {{ lane: string|null, foreign: boolean }} + */ +export function seatLane(issue) { + const m = /^\[PM seat\]\s*(.*?)\s*—\s*(.*)$/u.exec(issue?.title ?? ''); + if (!m) return { lane: null, foreign: true }; + const raw = m[1].trim(); + // An `@ ` suffix names the board the lane lives on. Present ⇒ the lane + // is only READABLE there, whatever its `domain:*` spelling says here. + const at = /^(.*?)\s*@\s*(\S+)\s*$/u.exec(raw); + const lane = (at ? at[1] : raw).trim(); + const elsewhere = at ? at[2] !== SWEEP_REPO.repo.split('/')[1] : false; + if (!/^domain:[a-z0-9][a-z0-9._-]*$/i.test(lane)) return { lane: null, foreign: true }; + return { lane, foreign: elsewhere }; +} + +/** + * Is this seat post declaring a HELD seat — a 🟢 with a real holder? + * + * Reuses `h5SeatStickerDesync`'s reading of the status word rather than + * re-deriving it, so the two items can never disagree about what 🟢 means. A + * ⏳ vacant / 🔴 收班 vacant / ⏸️ paused seat is deliberately OUT of scope: an + * unheld seat over a non-empty queue is a ROUTING gap (nobody is on the clock), + * not the 怠工 this row is about, and reporting it here would put an accusation + * on a seat that has correctly said it is not working. The queue cards + * themselves are H30's population and are reported there, on their own terms. + * + * `Routine` seats are held by a scheduled caller with no claim cadence of their + * own, so they are excluded on the same grounds `h5SeatStickerDesync` excludes + * them from the assignee comparison. + */ +export function seatIsHeld(issue) { + const m = /^\[PM seat\]\s*(.*?)\s*—\s*(.*)$/u.exec(issue?.title ?? ''); + if (!m) return false; + const status = m[2].trim(); + if (!status.startsWith('🟢')) return false; + const holder = status.replace('🟢', '').trim().split(/\s+/u)[0] ?? ''; + return holder.length > 0 && holder !== 'Routine'; +} + +/** + * The seat's latest utterance — the marker whose age is this row's clock and + * whose text carries the wait exemption. + * + * Recency is `created_at` with a THREAD-ORDER fallback, exactly as + * `governingClaim` resolves it and for the same reason: an unparseable stamp + * must not silently promote an older comment to "latest". An unreadable stamp + * yields a `null` age, which the predicate treats as "must not read as fresh" + * — H10/H13/H18/H20's standing call on an unreadable timestamp (#4690). + * + * @param {{ body?: string, created_at?: string }[]} commentRows + * @returns {{ body: string, createdAt: string|null } | null} + */ +export function latestSeatMarker(commentRows) { + const rows = Array.isArray(commentRows) ? commentRows : []; + let best = null; + rows.forEach((row, index) => { + const parsed = Date.parse(row?.created_at ?? ''); + const stamp = Number.isFinite(parsed) ? parsed : null; + const candidate = { body: String(row?.body ?? ''), createdAt: row?.created_at ?? null, stamp, index }; + if (best === null) { + best = candidate; + return; + } + const newer = stamp === null || best.stamp === null ? index > best.index : stamp >= best.stamp; + if (newer) best = candidate; + }); + return best === null ? null : { body: best.body, createdAt: best.createdAt }; +} + +/** How old the seat's latest marker is, in minutes — `null` when unreadable (#4690). */ +export function seatMarkerAgeMinutes(marker, nowMs = Date.now()) { + const posted = Date.parse(marker?.createdAt ?? ''); + return Number.isFinite(posted) ? (nowMs - posted) / 60_000 : null; +} + +/** + * Which seat posts buy a comment fetch — exported for the reason every + * gathering policy here is: a policy that decides what gets READ AT ALL is + * where a silent hole would live. + * + * A HELD seat on a lane THIS board can count, and nothing else. The `foreign` + * and unheld cases are decided from the title alone, so the fetch is bought + * only for seats this row could actually speak about — 6 of the 12 open seat + * posts at the 2026-08-25 census, and the only comment fetches the seat + * population has ever bought (the H2 branch explicitly skips `pm:seat`). + */ +export function h32NeedsSeatComments(issue) { + if (!labelNames(issue ?? {}).includes('pm:seat')) return false; + if (!seatIsHeld(issue)) return false; + return !seatLane(issue).foreign; +} + +/** + * H32 — null when clean, else the finding sentence. + * + * @param {object} issue — the seat post. + * @param {{ body: string, createdAt: string|null }|null|undefined} marker — + * the latest seat-post comment. `undefined` unconsulted, `null` unreadable. + * @param {{ unclaimed: number, inFlight: number }} lane — the lane inventory, + * counted off listings this sweep already holds. + */ +export function h32SeatIdleOverQueue(issue, marker, lane, nowMs = Date.now()) { + if (!labelNames(issue ?? {}).includes('pm:seat')) return null; + if (!seatIsHeld(issue)) return null; + const { lane: laneName, foreign } = seatLane(issue); + if (foreign || !laneName) return null; + + const unclaimed = Number(lane?.unclaimed ?? 0); + const inFlight = Number(lane?.inFlight ?? 0); + // The board half. Both halves are required: an empty queue is a finished + // lane, and work in flight is a working seat. + if (!(unclaimed > 0 && inFlight === 0)) return null; + + // An unconsulted or unreadable thread declines to judge rather than firing. + // The asymmetry against H4 (which fires on an unreadable channel) is + // deliberate and runs on the same rule H4 states: H4's remedy is "add a + // line", cheap and idempotent, so an unreadable channel can safely fire it. + // This row's output is an accusation that a named holder is not working, and + // an unread thread is exactly where a declared wait would have been. Firing + // blind here would manufacture the false positive the wait exemption exists + // to prevent. + if (marker === undefined || marker === null) return null; + if (seatDeclaresWait(marker.body)) return null; + + const age = seatMarkerAgeMinutes(marker, nowMs); + if (age !== null && age <= SEAT_IDLE_STALE_MINUTES) return null; + const clock = + age === null + ? 'an unreadable marker timestamp (which must not read as fresh)' + : `~${Math.round(age)} min since the seat's latest marker (threshold ${SEAT_IDLE_STALE_MINUTES} min)`; + + return ( + `\`pm:seat\` HELD, and its lane \`${laneName}\` has ${unclaimed} unclaimed \`pm:queue\` card(s) with ` + + `NOTHING in flight — ${clock}. The board is asserting two things that cannot both be healthy: a lane ` + + 'with dispatchable work and zero throughput, and a named holder on the clock for it. Neither half is ' + + 'visible from any card — a queued card looks exactly like an ordinary queued card however long it has ' + + 'been one (H30), and a seat post looks exactly like a seat post — which is why the measured incident ' + + 'was reported by the seat ITSELF and by no gauge: it ended round after round with 「要我继续派吗?」 ' + + 'while in-flight was zero and 40+ dispatchable cards sat in the queue, against a clause that already ' + + 'says ⛔ 不等人闸 in as many words. ⛔ So this row is NOT a new rule and must not be read as one — the ' + + 'rule is not missing, the detectability was (「规则不缺,措辞也不含糊」). ⚠️ A DECLARED wait is exempt ' + + 'here regardless of elapsed time: a latest marker naming 等 CI / 等裁决 / 等人工步骤 or an ' + + 'awaiting-class state silences this row, so a seat that IS waiting states it and is not reported. ' + + 'That is also the remedy when this fires and the wait is real — say what it is waiting on, in the ' + + 'marker. Otherwise the move is the one the clause already names: dispatch the next wave, or hand the ' + + 'seat over and mark it vacant so the lane reads as unheld rather than held-and-still. Report-only: ' + + '⛔ never a label, a title or a marker written from this script — which of those two moves applies is ' + + 'the whole judgement.' + ); +} + +// --------------------------------------------------------------------------- +// H33 — an in-flight claim written BEFORE the ruling that now stands (#11724). +// +// The filing seat skipped the mandatory decision re-read under speed-up and +// wrote a dispatch order that INVERTED a standing triage ruling — the ruling +// scoped the work to option 1 executed as a sweep and said in as many words +// that options 2/3 were 「file, don't fold in」; the order forbade the sweep and +// pointed the dev at 2/3. The dev followed the LATER ruling (correct) and +// reported the conflict, seeing one of the three contradictions. +// +// ⛔ Like H32 this adds no rule. The step exists, and the same session had +// already been saved by it twice that day — two cards whose bodies said 「A/B +// 未决」 while triage had long since ruled direction A in a COMMENT. Grading +// declined the other half of the card's remedy menu (a mandatory +// `Prior rulings read:` claim field) on the card's own reasoning: a field 99% +// filled with `none` is ritual, and a ritual field is as untrustworthy as none. +// What it promoted is this — structurally decidable, no threshold, no ritual, +// and silent on the overwhelming majority of cards, which carry no ruling at all. +// +// ## What the row actually asserts, precisely +// +// NOT "the dispatcher failed to read" — that is unobservable. What is observable +// is an ORDERING: the claim that put this card in flight is older than a ruling +// now standing on its thread. The dispatch order therefore cannot carry that +// ruling's constraints, whether it was written blind to a ruling already there +// or overtaken by one posted after. Both readings have the same consequence and +// the same remedy, which is why the row does not try to tell them apart: a dev +// is working from an order that predates the current ruling, and somebody has to +// re-read before the work lands rather than after. +// +// Same two-signals-unpaired register as the rest: a standing ruling and an +// in-flight claim, which should be paired and are not. +// +// ## Measured yield — a LOW-yield row, and silence is its normal reading +// +// Run over the live board on 2026-08-25 it reports NOTHING: of 54 open +// `pm:dispatched` cards, 27 carry no claim this file's marker can read (see +// `latestClaimComment`'s blind-spot note), 4 carry no ruling at all, and on the +// remainder every ruling PRECEDES its claim — which is the healthy ordering and +// exactly what the row wants to see. +// +// That a check reports nothing today is worth distinguishing from a check that +// CANNOT report anything — H30's header makes the point, and it is the reason +// this number was measured rather than assumed. Over the recently-closed window +// the shape is real and rare: 4 of the 107 closed cards carrying a readable +// claim have a ruling posted after it (#11781, #11673, #11106, #10166; the gaps +// run 8 min to ~17 h). So the expected steady state is a quiet row that fires a +// few times a week, in H23's register (~6 in 1,546) rather than H30's. +// --------------------------------------------------------------------------- + +/** + * What counts as a TRIAGE RULING comment — a closed set of measured opening + * shapes, in `H17_TRIGGER_ANCHOR_TERMS`'s register and for its reason. + * + * Census of every comment on 40 open `pm:dispatched` cards (2026-08-25), by + * first non-empty line with markdown decoration stripped: + * + * `Triage: lands in …` · `Triage routing: domain:skills + finding` · + * `Triage (first-touch grading): …` · `Triage (Routine seat, hourly round): …` · + * `Triage: → decision inbox …` · `Concentrated triage batch: …` · + * `Concentrated triage batch (final tail): …` · `Concentrated triage round: …` · + * `Skills-lane self-triage (run-to-empty fire): …` · `Grading (skills seat, …)` · + * `Maintainer ruling — option 1: …` + * + * Anchored at the START of the first line, never a substring of the body, and + * the reason is measured too: the same census carries 「Serial-constraint + * addendum … the triage comment above」 and 「⚠️ Section 3's REST-fallback claim + * needs qualifying」 — prose that MENTIONS a ruling or a claim without being + * one. A contains-match would read both as rulings and manufacture a row on + * every card that discusses its own triage. Under-reporting on an unrecognised + * spelling is H17's and H20's standing call, for H20's stated reason: a + * fabricated row sends a reader to check something that was never there. + */ +export const TRIAGE_RULING_ANCHORS = [ + /^triage\b/iu, + /^concentrated\s+triage\b/iu, + /^[\w:@.-]+[\s-]lane\s+self-triage\b/iu, + /^grading\b/iu, + /^maintainer\s+ruling\b/iu, +]; + +/** + * Is this comment body a triage ruling? Read from its FIRST non-empty line, + * with markdown decoration stripped — seats bold and blockquote these openings + * (「**Triage: …**」, 「> Triage routing: …」) and the decorated-directive lesson + * H4 paid for (#10102) applies verbatim. + */ +export function isTriageRulingComment(body) { + const first = String(body ?? '') + .split('\n') + .map((l) => l.trim()) + .find((l) => l.length > 0); + if (!first) return false; + const bare = first.replace(/^[>\s]*/u, '').replace(/[*_`#]/gu, '').trim(); + return TRIAGE_RULING_ANCHORS.some((re) => re.test(bare)); +} + +/** + * The newest `Claim:` comment on a thread, WITH its timestamp — and + * deliberately not `governingClaim`, which is the same read narrowed to claims + * that name a protocol-shaped BRANCH. + * + * That narrowing is right for H20/H27, whose whole question is about the branch + * a claim names. It would be wrong here and would silently empty this row's + * population: the measured claim census carries 「Claim: PM loop round R6」 — a + * well-formed claim naming no branch of its own — and the shape is still live. + * Re-measured 2026-08-27 over 161 pm-tracked cards and every comment on them: + * of 19 canonical claim comments, 3 name no protocol-shaped branch, and on 3 + * cards this row reads a claim `governingClaim` cannot (「Claim: PM loop round 1 + * (QA wave #9296)」 is one). What this row needs from a claim is only WHEN it + * was written. + * + * ⛔ A second specimen used to stand beside the first — 「Claim pointer: folded + * into the 11678 family dispatch」, offered as another well-formed branchless + * claim. It is not one, and it is not in this row's population at all: + * `CLAIM_COMMENT_MARKER` wants the colon directly after the word, and here the + * word is followed by ` pointer`, so the marker has never matched it. Under the + * same 2026-08-11 ruling that governs the dash spellings it is a MALFORMED + * claim — and unlike those it is invisible to H34 as well, which reports only a + * punctuation separator (that gap was measured and deliberately left open; see + * H34's header). The paragraph's conclusion is unaffected: it rests on the + * first specimen, which is real, and on the live count above. + * + * ## ⚠️ The inherited blind spot, and what it is NOT (#12090) + * + * `CLAIM_COMMENT_MARKER` requires the canonical colon, and some live claims are + * written with an em dash instead (「Claim — , session …」), which the + * marker does not match. Those cards are invisible to this row. + * + * ⛔ They are NOT well-formed claims in a dialect this file fails to read. The + * maintainer's 2026-08-11 ruling makes the literal `Claim:` the single machine + * criterion and closes the widening explicitly (SKILL.md step 4, quoted in full + * at `CLAIM_COMMENT_MARKER`), so a dash-written claim is MALFORMED and the + * repair is the write side. This paragraph used to call them 「real claims, + * correctly formed」 — a straight contradiction of the protocol this file + * enforces, and the exact shape of drift the strictness exists to prevent. + * + * Inherited on purpose rather than patched here. Defining a second, WIDER + * notion of "a claim" for H33 alone would leave the file disagreeing with + * itself about what a claim IS — H2 would go on calling those cards claimless + * while H33 read their claims off the same threads — and a file that + * contradicts itself about its own vocabulary is a worse defect than the gap. + * The consequence here is UNDER-reporting, which is the direction this file + * takes on every unrecognised spelling (H17's extractor, H20's branch shape), + * never a fabricated row. + * + * Where those claims DO surface is H34, which reports the near miss as its own + * observation and points the seat at the canonical spelling — visibility + * without redefining the vocabulary. + * + * @param {{ body?: string, created_at?: string }[]} commentRows + * @returns {{ createdAt: string|null } | null} + */ +export function latestClaimComment(commentRows) { + const rows = Array.isArray(commentRows) ? commentRows : []; + let best = null; + rows.forEach((row, index) => { + if (!CLAIM_COMMENT_MARKER.test(String(row?.body ?? ''))) return; + const parsed = Date.parse(row?.created_at ?? ''); + const stamp = Number.isFinite(parsed) ? parsed : null; + const candidate = { createdAt: row?.created_at ?? null, stamp, index }; + if (best === null) { + best = candidate; + return; + } + const newer = stamp === null || best.stamp === null ? index > best.index : stamp >= best.stamp; + if (newer) best = candidate; + }); + return best === null ? null : { createdAt: best.createdAt }; +} + +/** + * H33 — null when clean, else the finding sentence. + * + * Both timestamps must be READABLE for the row to fire, and that is the one + * place this item declines where its neighbours insist. Elsewhere an unreadable + * stamp must not read as fresh, because there the stamp is an AGE and the + * conservative reading is "old". Here the stamp is one side of an ORDER + * comparison, and an unreadable one does not make the comparison conservative — + * it makes it undefined. A row asserting that a claim predates a ruling, built + * on a stamp nobody could read, would be a fabricated ordering, which is worse + * than a missing row (#4690 cuts the other way when the unread datum is not the + * finding but its evidence). + * + * @param {object} issue — an OPEN issue. + * @param {{ body?: string, created_at?: string }[]|null|undefined} commentRows + */ +export function h33ClaimPredatesRuling(issue, commentRows) { + if (!labelNames(issue ?? {}).includes('pm:dispatched')) return null; + if (!Array.isArray(commentRows)) return null; + const claim = latestClaimComment(commentRows); + const claimStamp = Date.parse(claim?.createdAt ?? ''); + if (!Number.isFinite(claimStamp)) return null; + + const rulings = commentRows + .filter((row) => isTriageRulingComment(row?.body)) + .map((row) => ({ at: row?.created_at ?? null, stamp: Date.parse(row?.created_at ?? '') })) + .filter((r) => Number.isFinite(r.stamp) && r.stamp > claimStamp) + .sort((a, b) => b.stamp - a.stamp); + if (rulings.length === 0) return null; + + const newest = rulings[0]; + return ( + `\`pm:dispatched\` whose latest \`Claim:\` comment (${claim.createdAt}) PREDATES ` + + `${rulings.length} triage-ruling comment(s) on this same card, the newest posted ${newest.at} — so ` + + 'the order this card is in flight under was written before the ruling that now stands on its thread, ' + + 'and cannot be carrying that ruling\'s constraints. ⚠️ The row asserts an ORDERING, not a state of ' + + 'mind: whether the dispatcher wrote blind to a ruling already there or was overtaken by one posted ' + + 'after is not observable and does not change the remedy. The measured cost of the first reading: a ' + + 'dispatch order INVERTED a standing ruling — the ruling scoped the work to option 1 executed as a ' + + 'sweep and said 「file, don\'t fold in」 about options 2/3, and the order forbade the sweep and ' + + 'pointed the dev at 2/3. The dev followed the later ruling and reported the conflict, seeing one of ' + + 'three contradictions. Remedy: re-read the ruling against the dispatch order NOW, while the work is ' + + 'still in flight and the correction is a message rather than a rollback — and if they disagree, say ' + + 'so on the card so the dev is not left arbitrating between two orders. ⛔ This is not a new rule: the ' + + 're-read step exists, and the same session it failed on had already been saved by it twice that day. ' + + 'Report-only: ⛔ never a label written from this script.' + ); +} + +// --------------------------------------------------------------------------- +// H34 — a claim-shaped comment with a NON-CANONICAL SEPARATOR (#12090). +// +// The card that filed this measured 24 of 54 open `pm:dispatched` cards +// carrying 「Claim — , session …」 with an EM DASH, invisible to +// `CLAIM_COMMENT_MARKER`, and proposed widening the class. That proposal is +// CLOSED, and this row is what was taken instead. +// +// ## Why the predicate is not widened — the ruling, verbatim +// +// `.claude/skills/pm-dispatch/SKILL.md` (step 4 of the claim protocol) records +// 「首行以字面 `Claim:` 开头是机器判据(维护者 2026-08-11 裁定;巡查谓词只认这 +// 一个拼写且保持严格,修法是全舰队向文档拼写收敛,⛔ 不放宽谓词)」. Under that +// ruling a dash-written claim is a MALFORMED claim, H2's row on it is a CORRECT +// report, and the repair is the write side converging on the documented +// spelling. Widening the reader would make the enforced protocol drift away +// from the written one permanently — the declared-≠-enforced shape this repo +// treats as a defect everywhere else — and would do it by editing a maintainer +// ruling's own text, which is not a patrol script's call. +// +// ## …and why the gap is nevertheless real, which is what this row closes +// +// The cost the card found is not only H2's loudness. `governingClaim` gates on +// the same marker before reading `Branch:`, so a card claimed with a dash buys +// no ref probe and can never produce an H20 「no remote ref」 or H27 「dead +// claim」 row: it sits OUTSIDE dispatch-liveness entirely, silently, for as long +// as it is in flight. H33 inherits the same blindness. So the malformed claim +// costs one noisy row and three quiet ones, and the quiet ones are the +// expensive direction. +// +// This row makes the malformed spelling VISIBLE without redefining what a claim +// is. `Claim:` stays the single machine criterion — H2/H20/H27/H33 are +// untouched, byte for byte — and the near miss is reported as its own, +// separately-named observation whose remedy points at the canonical spelling. +// The file therefore never disagrees with itself about its own vocabulary, +// which is the objection that kept the gap open. +// +// ## Measured, fresh, at dispatch time (2026-08-25 ~15:00Z) +// +// Over the LIVE open `pm:dispatched` population (45 cards): 40 colon-form, +// 1 dash-only, 0 hyphen, 0 fullwidth, 4 with no claim in any spelling. The +// filing card's 24-of-54 has already decayed to 1-of-45 in a day — the +// maintainer's prescribed convergence is working, which is precisely why the +// widening was not worth a ruling. Over the 128 `pm:dispatched` cards closed +// since 2026-08-24 the shape is recurring rather than historical: 13 dash-only +// claims, clustered in identifiable seat batches. So this row's expected steady +// state is quiet — H23's register, not H30's — and its value is that a seat +// writing the wrong spelling learns about it while the card is still in flight, +// which is how a fleet converges. +// +// ## Deliberately NOT the whole near-miss space +// +// Two suppressions, both under-reporting on purpose (H17's and H20's standing +// direction — a fabricated row sends a reader to check something that was never +// there): +// +// • A thread that ALSO carries a canonical claim is silent here. That card is +// machine-visible, so the row would have no remedy to offer. The residual +// it accepts: a RE-claim written with a dash over an older colon claim +// leaves `governingClaim` reading the stale one. Narrower than the shape +// this row is for, and naming it would cost a row on every card that ever +// wrote a dash claim once. +// • A claim-shaped line whose remainder carries none of the protocol's own +// content (a session reference, the word "seat", or a protocol-shaped +// branch anywhere in the comment) is silent. 「Claim - see above」 in prose +// is not evidence that a claim was attempted. +// +// ## The WORD position — measured 2026-08-27, and NOT widened +// +// A filed finding asked whether the separator class should cover a WORD where +// the punctuation goes, on the strength of 「Claim pointer: folded into the +// 11678 family dispatch」 — a string `latestClaimComment`'s header cited as a +// claim the marker reads, which it never was. Measured before answering, +// because the header above says this row's strictness was paid for once and +// a widening is exactly what it was paid to prevent. +// +// The census: 161 pm-tracked cards — 13 open `pm:dispatched`, 76 open +// `pm:queue`, 72 `pm:dispatched` closed since 2026-08-26 — and all 343 comments +// on them, classified by what follows a line-opening claim word. 19 canonical +// `Claim:`; 2 with a declared punctuation separator (both EM DASH, both on +// threads with no canonical claim — this row's live population, and it fires); +// and ZERO with a distinct word in the separator position. The 「Claim pointer:」 +// specimen occurs nowhere on the board: its only live instance is the finding +// card quoting this file quoting it. +// +// So the widening buys nothing measurable, and it would spend the conservative +// half's whole margin to buy it. `Claim` followed by a word is ordinary English +// — 「Claiming this card」, 「Claim comments are …」 — where `Claim` followed by +// an em dash is not; `looksLikeClaimContent` would carry that load for a +// population of zero. RECORDED, NOT WIDENED. +// +// The same census found three OTHER shapes invisible to both markers, recorded +// so a later reader can reopen the question on numbers instead of re-measuring +// — and each is already harmless for a DIFFERENT reason, which is the actual +// finding: +// +// • 2 「Claim (dev): …」 openings. Both threads ALSO carry a canonical claim, +// so the first suppression above already covers them: they are a SECOND +// claim comment on a machine-visible card, never a substitute for one. +// • 2 inflected openings (「Claiming this card. session …」) and 1 decorated +// with backticks (「`Claim:` devx@objectstack seat …」). All three sit on +// UNASSIGNED cards, which this row and H2 both decline to judge — the +// residual already declared above. +// +// Three different shapes, three different reasons, none of them this one. Each +// is its own question with its own noise floor if it ever acquires a population. +// --------------------------------------------------------------------------- + +/** + * The separators a claim-shaped line is measured to carry INSTEAD of the + * canonical colon, each with the name the row prints. Codepoints are spelled + * out rather than pasted: these characters are visually near-identical to each + * other and to the colon, and a row that says 「EM DASH (U+2014)」 tells a seat + * what to search for in a way a rendered glyph cannot. + * + * U+FF1A is here, and it is the reason it can be: the claim marker's class + * looked like it accepted it for the whole of this file's life and never did + * (see `CLAIM_COMMENT_MARKER`). In a protocol whose own SKILL.md is written in + * Chinese, 「Claim:」 is a plausible spelling; it was invisible to every reader + * and is now merely non-canonical, which is a state a seat can act on. + */ +export const NON_CANONICAL_CLAIM_SEPARATORS = [ + ['—', 'EM DASH (U+2014)'], + ['–', 'EN DASH (U+2013)'], + ['-', 'HYPHEN-MINUS (U+002D)'], + [':', 'FULLWIDTH COLON (U+FF1A)'], +]; + +/** + * A claim-shaped opening whose separator is not the canonical colon. + * + * Mirrors `CLAIM_COMMENT_MARKER`'s strictness deliberately — same optional + * blockquote, same must-BEGIN-with-the-word anchor, same absence of `g` — so + * that the two markers partition claim-shaped openings rather than overlapping. + * The prose control the strictness was paid for (#7488) is still excluded here + * for the same reason it is there: 「the next seat should claim: only after the + * ruling lands」 does not BEGIN with the word. + * + * ⚠️ `[ \t]*`, NOT `\s*`, on both sides of the word. `\s` matches a NEWLINE, so + * `Claim\n- something` would put a markdown BULLET's hyphen in the separator + * position and read an ordinary list as a malformed claim — and a hyphen is the + * commonest line-opening character in this repo's comment bodies, where the + * colon marker never had to care (a line starting with `:` is not a thing + * anyone writes). + * + * Stated precisely, because it is easy to over-claim: `nearMissClaimSeparators` + * already runs this regex per SPLIT LINE, so the reader below is safe either + * way and the character class is not what saves it. What the class protects is + * this constant AS AN EXPORT — it is `m`-flagged and reusable, and a future + * caller doing `CLAIM_NEAR_MISS_MARKER.test(body)` over a whole comment gets + * the right answer only because of it. Both properties are pinned separately in + * the self-test, at the regex and at the reader, so neither can go green on the + * other's behalf. + * + * Capture groups: 1 = the separator, 2 = the rest of the line. + */ +export const CLAIM_NEAR_MISS_MARKER = new RegExp( + `^[ \\t]*>?[ \\t]*Claim(?:ed)?[ \\t]*([${NON_CANONICAL_CLAIM_SEPARATORS.map(([ch]) => `\\u${ch.codePointAt(0).toString(16).padStart(4, '0')}`).join('')}])[ \\t]*(.*)$`, + 'mi' +); + +/** + * Does this line's remainder carry the claim protocol's own content? + * + * The conservative half of the detection. The protocol's claim comment is + * required to name a session ID and a branch, and the measured dash claims do: + * 「Claim — skills seat `session_01RM…`. Folded dispatch …」 and 「Claim — + * domain:cli lane execution seat, session 019siH5jDmk5hrayvfyojUqR, round + * R36」. Requiring one of those tokens is what separates an attempted claim + * from a line that merely opens with the word. + * + * A protocol-shaped branch is accepted from ANYWHERE in the comment rather than + * from the opening line, because the template puts `Branch:` on its own line — + * the same reason `claimedBranches` reads a directive line rather than the + * first one. + */ +export function looksLikeClaimContent(remainder, body) { + if (/\bsessions?\b|\bseat\b/iu.test(String(remainder ?? ''))) return true; + return claimedBranches(body).length > 0; +} + +/** + * Which non-canonical separators this comment body opens a claim-shaped line + * with — de-duplicated, in the order declared, empty when none does. + * + * @param {string} body + * @returns {string[]} the printable separator NAMES + */ +export function nearMissClaimSeparators(body) { + const text = String(body ?? ''); + const found = []; + for (const line of text.split('\n')) { + const hit = CLAIM_NEAR_MISS_MARKER.exec(line); + if (!hit) continue; + if (!looksLikeClaimContent(hit[2], text)) continue; + const name = NON_CANONICAL_CLAIM_SEPARATORS.find(([ch]) => ch === hit[1])?.[1]; + if (name && !found.includes(name)) found.push(name); + } + return found; +} + +/** + * H34 — null when clean, else the finding sentence. + * + * Gated on the SAME population H2 judges (pm-tracked, assigned) and read off + * the SAME comment bodies that branch already fetched, so the row costs no + * request at all. It fires only where H2 fires — no canonical claim on the + * thread — which makes every H34 row a companion that EXPLAINS its card's H2 + * row rather than a second accusation about the same fact. + * + * The residual that gate accepts, stated rather than dropped: an UNASSIGNED + * `pm:dispatched` card buys no comment fetch here and so is never judged. That + * card is H1's finding in its own right, and its dispatch is already the thing + * being questioned. + * + * @param {object} issue — an OPEN issue. + * @param {(string|null|undefined)[]} commentBodies + */ +export function h34ClaimShapedNonCanonicalSeparator(issue, commentBodies) { + const labels = labelNames(issue ?? {}); + const pmTracked = labels.some((l) => l === 'pm:queue' || l === 'pm:dispatched'); + if (!pmTracked || (issue?.assignees ?? []).length === 0) return null; + if (!Array.isArray(commentBodies)) return null; + // A card with a readable claim is machine-visible; the row has no remedy for it. + if (commentBodies.some((b) => CLAIM_COMMENT_MARKER.test(String(b ?? '')))) return null; + + const separators = []; + let lines = 0; + for (const body of commentBodies) { + const names = nearMissClaimSeparators(body); + if (names.length === 0) continue; + lines += 1; + for (const name of names) if (!separators.includes(name)) separators.push(name); + } + if (separators.length === 0) return null; + + return ( + `a claim-shaped comment whose separator is NOT the canonical colon — ${lines} comment(s), ` + + `${separators.join(' + ')} where the protocol writes \`Claim:\` — and NO comment on this thread ` + + 'matches the marker. So this card reads as CLAIMLESS to H2 (its row on this card is correct, not a ' + + 'false positive) and sits outside dispatch-liveness entirely: `governingClaim` gates on the same ' + + 'marker before reading `Branch:`, so H20 can never report a missing remote ref for it and H27 can ' + + 'never report a dead claim — the two rows that exist to catch an abandoned dispatch. Remedy, and it ' + + 'is the WRITE side: the claiming seat re-posts (or edits) the claim so its FIRST line begins with ' + + 'the literal `Claim:`, per SKILL.md step 4 — 「首行以字面 `Claim:` 开头是机器判据(维护者 ' + + '2026-08-11 裁定;巡查谓词只认这一个拼写且保持严格,修法是全舰队向文档拼写收敛,⛔ 不放宽谓词)」. ' + + '⛔ The patrol predicate is NOT widened to accept the separator this card used; that is the one ' + + 'repair the ruling closes. Report-only: ⛔ never a label written from this script.' + ); +} + +// --------------------------------------------------------------------------- +// H35 — a gate label REMOVED with no matching review-chain evidence (#11881). +// +// H31 above compares the gate's two carriers as they stand NOW and says, in its +// own header, exactly what it cannot do: 「闸门被剥不是红灯是放行」 — a stripped +// gate is a GREEN light, and 「被剥」 and 「从未挂过」 are indistinguishable in +// the evidence. A label that was removed is ABSENT, and absence has two causes. +// Every reader in this file until now has been a reader of STATE, so none of +// them can separate the two. This row reads the EVENT that produced the state. +// +// The filing card's measurement is what makes the question concrete: on PR +// #11470 the erasing actor was a SEAT (`claude[bot]`, 33 and 92 minutes after +// the Auto Label job), not a workflow — so the whole-set-PUT gate that shipped +// for that incident sweeps `.github/workflows/**`, `.github/actions/**` and +// `scripts/**` and cannot reach the actor at all. Seats write through the API at +// runtime. The compensating control the card asks for is DETECTION, and the +// triage ruling (2026-08-25 14:58Z) scoped this card to exactly that: a +// report-only patrol, adding detection and weakening no gate. ⛔ Escalation and +// enforcement are a LATER card and deliberately absent here. +// +// ## The transport, and why this row costs no per-card fetch +// +// The obvious reading — fetch each card's timeline — is the trade this sweep +// declines everywhere it arises (H15 declines it by name for the age of a +// label; H16's header forbids "fixing" its proxy with one). This row does not +// need it. `GET /repos/{repo}/issues/events` is a REPO-WIDE, newest-first +// stream of the same `labeled`/`unlabeled` rows, and it carries the full issue +// payload — number, state, CURRENT labels, body — on every row. So the whole +// population is one paginated window of the shape this file already keeps three +// of (H8's merged-PR window, H23's commit window, H22's closed-issue window), +// and the per-card cost is zero. PRs arrive through it too: a pull request IS +// an issue to this endpoint, which is what lets one window see both carriers of +// a dual-carrier gate. +// +// MEASURED 2026-08-26T01:26:58Z, 160 pages: 16,000 events spanning +// 2026-08-22T15:35:07Z … 2026-08-26T01:26:58Z = 3.41 days ⇒ ~4,691 events/day. +// +// ## What counts as "matching review-chain evidence" — and why it is STRUCTURAL +// +// `references/contract-review.md` names the evidence for a legitimate clear: +// 「PASS 评论 + 标签缺失 + PR head 自复审后未动 = 已复审清标,不是被剥」. Read +// literally that makes the discriminator a PASS COMMENT, and a predicate built +// on it does not survive measurement. The verdicts are free prose and their +// wording varies card to card — `**Contract review: PASS**`, +// `**Contract review — PASS**`, `## Post-merge contract-review verdict: **PASS**` +// were all live in one 18-hour window — so over the 35 card-side removals in +// that window a strict marker matched 5 and a loose one matched 10. Widening +// the regex until the rest match is the tolerant-consumer antipattern this repo +// forbids by name, and its end state is worse than noise: an "any PASS token" +// reading matched 26 of 35, including threads whose PASS was about something +// else entirely — a check that can barely fail, which is the shape this file +// exists to CATCH rather than to add. +// +// The protocol leaves a second, MACHINE-READABLE definition of the same event, +// and this row uses that one: 「PR 与卡双载体同笔挂」…「PASS 双载体同笔清标」 — +// the gate is hung in one stroke and cleared in one stroke, ACROSS BOTH +// CARRIERS. A legitimate clear therefore leaves TWO removals, one per carrier, +// seconds apart, by the same actor. A strip leaves ONE. That is a structural +// invariant taken from the protocol's own words, not a parse of prose, and it +// is the reason this row can decline the comment fetch as well as the timeline +// fetch. +// +// ## The stroke window, derived from the measured gap distribution +// +// Same corpus, 206 gate removals: for each, the gap to the nearest removal on +// the OPPOSITE carrier kind by the SAME actor. +// +// ≤1s 50 | ≤2s 65 | ≤3s 5 | ≤5s 21 | ≤10s 16 | ≤30s 11 | ≤60s 4 +// (60,90]s 0 ← the distribution is EMPTY here +// ≤120s 4 | ≤300s 1 | then 1000s, 3344s, … hours +// +// The same-stroke cluster ends at 101s and the next observation is 275s — a +// 2.7x jump across an empty region. `H35_SAME_STROKE_SECONDS = 120` sits inside +// that gap, so the threshold is a reading rather than a preference: no value +// between 102 and 274 classifies the corpus differently. The tail past 120s is +// hours wide, which is a different stroke by any reading. The batching is why +// the cluster has width at all — the review Routine runs hourly and 「每小时一轮 +// 即天然攒批」, and a measured batch cleared 4 PRs + 4 cards in 14 seconds. +// +// ## Three outcomes, never two (#4690) +// +// A lone removal is NOT automatically a strip, and the corpus says so loudly. +// 34 of 36 lone removals were gates that had been hung on the CARD ONLY — the +// PR carrier never carried the label at all (verified per-PR: PR #12401 and PR +// #12287, the two most recent, have zero gate events in their entire history). +// For a single-carrier gate there is no second carrier to clear, so a lone +// clear is exactly what a CORRECT clear looks like, and flagging it would +// report the majority shape of the board as a violation. +// +// So the removal is judged against its own HANG, which the same window already +// carries: a gate hung in a dual stroke and cleared in a lone one is the pair +// written half way — that is the finding. A gate hung lone and cleared lone is +// internally consistent and gets the file's UNJUDGED treatment instead of a +// verdict: there is no structural evidence in EITHER direction, which is not +// the same as evidence of correctness, and 「read, and it carries nothing」 vs +// 「could not be read」 is the pair this whole file refuses to conflate. A +// removal whose hang predates the window is `undated` and is counted, never +// guessed at. +// +// ⚠️ The unjudgeable class is the honest residue of this card, and it is the +// LARGER half: measured 29 over 3.41 days against 0 half-writes. Where the +// dual-carrier discipline is actually followed it holds — 0 half-write clears +// in 206 removals — and the exposure has moved to single-carrier gates, which +// no carrier comparison (H31's or this one's) can ever adjudicate. Closing THAT +// half needs a producer-side change (a canonical machine-readable verdict, or +// the PR-side hang that 「PR 一存在即挂」 already requires), which is a decision +// this row records rather than takes. +// +// ## What is reported, and the deliberate asymmetry in the two classes +// +// `half-write` is reported for any carrier, open or closed: a gate cleared half +// way on a PR that then merged is the bypass that already happened, and the +// population is ~0/day so it cannot flood the report. `unjudgeable` is reported +// only while the carrier is still OPEN and the label still ABSENT — that is the +// subset a reader can still act on, and it is the difference between 0.22 rows +// per run and 8.5. Measured live subset: 3 cards over 3.41 days. +// +// A removal whose label is back is SILENT in both classes. That is the +// read-back working — the card's own §2 names the 13-minute re-application on +// #11470 as「consistent with an accidental loss caught by read-back」— and a row +// for it would report the control functioning as a defect. +// +// Report-only, and emphatically: like H31 this row's subject is a GATE. ⛔ Never +// a label written from this script — a sweeper that re-hung a review gate would +// be issuing the review verdict, which is 自查放行. +// --------------------------------------------------------------------------- + +/** + * The gate-semantic label family this row patrols. + * + * MEASURED on the live repo 2026-08-26 (`GET /labels`, 57 labels): the family + * has exactly ONE member. It is a LIST rather than the bare constant because + * the ruling names a family and the next gate label must join it here rather + * than fork a row — but the list is not speculative padding, and + * `needs-user-decision` deliberately stays out of it: it marks a card awaiting + * a maintainer, not a review chain with a dual-carrier hang/clear protocol, so + * the same-stroke invariant below is meaningless for it. + */ +export const GATE_SEMANTIC_LABELS = [CONTRACT_REVIEW_LABEL]; + +/** Is this label one the row patrols? */ +export function isGateSemanticLabel(name) { + return GATE_SEMANTIC_LABELS.includes(String(name ?? '')); +} + +/** + * How far apart two carrier writes can be and still be 「同笔」 — 120s, read + * out of the empty region between the measured 101s and 275s (header above). + */ +export const H35_SAME_STROKE_SECONDS = 120; + +/** + * The issue-event production rate, MEASURED — the divisor the window below + * uses, in the same executable shape H8's window uses `MEASURED_MERGES_PER_DAY`. + * + * read 2026-08-26T01:26:58Z, `GET /repos/{repo}/issues/events`, 160 pages + * window 2026-08-22T15:35:07Z … 2026-08-26T01:26:58Z (3.41 days) + * rows 16,000 events, of which 415 carried a gate-semantic label + * rate 16,000 / 3.41 = ~4,691 events/day + */ +export const MEASURED_ISSUE_EVENTS_PER_DAY = 4691; + +/** + * The detection horizon — 12h, i.e. TWO patrol cycles at the 6-hourly cadence. + * + * One cycle would put every removal within one run of aging out, so a single + * failed or skipped run loses the finding permanently (this is a horizon, not a + * retry budget — H8's window states the same thing). Two cycles means every + * removal is seen by at least two consecutive runs. Past the horizon the + * finding is not delayed, it is gone: nothing else in this file reads events. + */ +export const H35_EVENT_WINDOW_HOURS = 12; + +/** + * The quota backstop, in pages of 100. + * + * At the measured rate the horizon needs `eventWindowPages()` = 24 pages; the + * cap is 30, which absorbs a day ~25% busier than the corpus before truncating. + * A run that HITS the cap has a short window, and the summary line says so — + * a truncated window must never read as a clean one (#4690). + */ +export const H35_EVENT_PAGE_CAP = 30; + +/** + * Pages of 100 needed to cover `hours` at the measured event rate. The + * arithmetic is executable rather than prose for the reason `windowCoverageDays` + * exists: a rate that moves must move the derivation with it, where a test can + * see it. + */ +export function eventWindowPages( + hours = H35_EVENT_WINDOW_HOURS, + ratePerDay = MEASURED_ISSUE_EVENTS_PER_DAY, + perPage = 100, +) { + if (!Number.isFinite(hours) || !Number.isFinite(ratePerDay) || ratePerDay <= 0) return null; + if (!Number.isFinite(perPage) || perPage <= 0) return null; + return Math.ceil(((hours / 24) * ratePerDay) / perPage); +} + +/** Every `labeled`/`unlabeled` event in a window that carries a gate-semantic label. */ +export function gateLabelEvents(events) { + return (events ?? []).filter( + (e) => + e && + (e.event === 'labeled' || e.event === 'unlabeled') && + isGateSemanticLabel(e.label?.name), + ); +} + +/** Is this event row on a PULL REQUEST carrier rather than a card? */ +function eventOnPullRequest(event) { + return Boolean(event?.issue?.pull_request); +} + +/** Does the carrier this event names still carry the label the event moved? */ +function carrierStillLabelled(event) { + const name = String(event?.label?.name ?? ''); + return (event?.issue?.labels ?? []).some((l) => l?.name === name); +} + +/** + * Is `event` half of a 「同笔」 dual-carrier stroke? True when the SIBLING + * carrier saw the same verb, on the same label, by the same actor, within the + * stroke window. + * + * `siblingNumbers` resolves the other carrier and is INJECTED rather than + * derived here: the sweep answers it with `prDeliversCard` over the PR windows + * it already holds, which is the same delivery relation H8 and H31 read — so + * the three rows can never disagree about which PR delivers which card. It + * returns `null` when the relation is unresolvable (no delivering PR in the + * windows, a body that declares nothing), and an unresolvable sibling means NOT + * PAIRED — which routes the removal to a judged-against-its-hang path below, + * never straight to a finding. + */ +export function pairedAcrossCarriers(event, gateEvents, options = {}) { + const { sameStrokeSeconds = H35_SAME_STROKE_SECONDS, siblingNumbers = () => null } = options; + const at = Date.parse(event?.created_at ?? ''); + if (!Number.isFinite(at)) return false; + const siblings = siblingNumbers(event); + if (!Array.isArray(siblings) || siblings.length === 0) return false; + const wanted = new Set(siblings.map((n) => Number(n))); + const actor = String(event?.actor?.login ?? ''); + const label = String(event?.label?.name ?? ''); + const onPr = eventOnPullRequest(event); + return (gateEvents ?? []).some((o) => { + if (!o || o === event) return false; + if (o.event !== event.event) return false; + if (String(o.label?.name ?? '') !== label) return false; + if (String(o.actor?.login ?? '') !== actor) return false; + if (eventOnPullRequest(o) === onPr) return false; + if (!wanted.has(Number(o.issue?.number))) return false; + const t = Date.parse(o.created_at ?? ''); + return Number.isFinite(t) && Math.abs(t - at) <= sameStrokeSeconds * 1000; + }); +} + +/** The most recent hang of the same label on the same carrier BEFORE `removal`. */ +export function precedingHang(removal, gateEvents) { + const at = Date.parse(removal?.created_at ?? ''); + if (!Number.isFinite(at)) return null; + const label = String(removal?.label?.name ?? ''); + const number = Number(removal?.issue?.number); + const hangs = (gateEvents ?? []) + .filter( + (e) => + e && + e.event === 'labeled' && + String(e.label?.name ?? '') === label && + Number(e.issue?.number) === number && + Number.isFinite(Date.parse(e.created_at ?? '')) && + Date.parse(e.created_at) < at, + ) + .sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at)); + return hangs[0] ?? null; +} + +/** + * H35's classifier — the three-valued half, asserted directly by the self-test. + * + * @returns one of: + * `'not-applicable'` the row is not an `unlabeled` of a gate-semantic label + * `'rehung'` the carrier carries the label again (read-back worked) + * `'paired'` cleared in a 「同笔」 dual-carrier stroke — the evidence + * `'half-write'` hung in a dual stroke, cleared in a lone one — FINDING + * `'unjudgeable'` hung lone and cleared lone (single-carrier gate) + * `'undated'` no hang inside the window; declines to judge + */ +export function h35RemovalVerdict(removal, gateEvents, options = {}) { + if (!removal || removal.event !== 'unlabeled') return 'not-applicable'; + if (!isGateSemanticLabel(removal.label?.name)) return 'not-applicable'; + if (carrierStillLabelled(removal)) return 'rehung'; + if (pairedAcrossCarriers(removal, gateEvents, options)) return 'paired'; + const hang = precedingHang(removal, gateEvents); + if (!hang) return 'undated'; + return pairedAcrossCarriers(hang, gateEvents, options) ? 'half-write' : 'unjudgeable'; +} + +/** Shared tail — the posture, stated on every row this block emits. */ +const H35_CONTRACT = + 'Report-only: ⛔ never a label written from this script — re-hanging a review gate from a sweeper ' + + 'would be issuing the verdict, which is 自查放行. Detection only; escalation and enforcement are ' + + 'a later card by the 2026-08-25 ruling.'; + +/** + * H35 — null when the removal needs no row, else the finding sentence. + * + * Two classes reach a row, and they say different things on purpose. The + * `open`/`absent` narrowing applies to `unjudgeable` ONLY and the header states + * why: that class is common and actionable only while the carrier is live, + * while `half-write` is ~0/day and names damage that may already have landed. + * + * @param {object} removal — an `unlabeled` event row from the repo-wide window. + * @param {object[]} gateEvents — every gate-semantic label event in that window. + */ +export function h35GateRemovalWithoutEvidence(removal, gateEvents, options = {}) { + const verdict = h35RemovalVerdict(removal, gateEvents, options); + const label = String(removal?.label?.name ?? ''); + const actor = String(removal?.actor?.login ?? 'an unreadable actor'); + const at = String(removal?.created_at ?? 'an unreadable time'); + const carrier = eventOnPullRequest(removal) ? 'PULL REQUEST' : 'CARD'; + + if (verdict === 'half-write') { + return ( + `\`${label}\` was REMOVED from this ${carrier} by \`${actor}\` at ${at} in a LONE stroke, ` + + 'while the hang it clears was written across BOTH carriers — so the pair was cleared half way. ' + + '「PASS 双载体同笔清标」 makes a legitimate clear two removals seconds apart, one per carrier; ' + + 'this one has no sibling within ' + + `${H35_SAME_STROKE_SECONDS}s. Either the clear never reached the second carrier, or the label ` + + 'was stripped — and 「闸门被剥不是红灯是放行」, so the failure direction is TOWARD release: an ' + + 'ungated carrier reads to the enqueue path as one that was never gated. Remedy is a READ, not a ' + + 'write: check the card thread for a current review verdict before re-hanging — 「PASS 评论 + 标签' + + `缺失 + PR head 自复审后未动 = 已复审清标,不是被剥」. ${H35_CONTRACT}` + ); + } + + if (verdict === 'unjudgeable') { + if (removal?.issue?.state !== 'open') return null; + return ( + `\`${label}\` was removed from this open ${carrier} by \`${actor}\` at ${at} and the gate is ` + + 'still absent — UNJUDGED, not clean. The hang it clears was ALSO a lone stroke: this gate only ' + + 'ever had ONE carrier, so 「双载体同笔清标」 leaves no structural evidence in either direction ' + + 'and no carrier comparison — H31\'s or this row\'s — can say whether it was cleared or stripped. ' + + 'The only remaining evidence is a review verdict written as free prose, which has no canonical ' + + 'machine-readable form (measured: a strict marker matched 5 of 35 removals, a loose one 10), so ' + + 'this row declines to parse it rather than widen into a check that cannot fail. Two producer-side ' + + 'repairs would each make this judgeable: hang the PR carrier as 「PR 一存在即挂」 already ' + + `requires, or give the verdict a canonical marker. ${H35_CONTRACT}` + ); + } + + return null; +} + +// --------------------------------------------------------------------------- +// H36 — two open PRs holding the same changed file, one side already ACCEPTED +// or armed (#12286). +// +// ## The incident, and what exactly went unheld +// +// Two open PRs edited one runtime source file across lanes for ~4h. The +// later-opened one merged first; the earlier — reviewed, ACCEPTED, 32/32 +// checks green — was left in a silent merge conflict. Nothing mechanical +// objected, and the single-claim gate was RIGHT not to: its header forbids +// per-incident growth of `SINGLE_CLAIM_PATHS`, and its declared scope held. +// What is unheld is LEGIBILITY: the same-file fence lives in a claim comment +// in one lane's thread, so a second seat that never asks the cross-lane +// question gets no signal from anywhere. The platform volunteers nothing +// either: `mergeable_state` stayed `unknown` throughout (computed lazily), so +// the seat's first reading was wrong and it armed auto-merge on a dirty head. +// +// ## What this row is — patrol INPUT, never a verdict +// +// A pair sharing a changed file is often perfectly fine (the incident's own +// two sides were additive and semantically safe). The row does not judge the +// pair; it makes the hold visible on the anchor every seat already reads, so +// the cross-lane walk the claim shape requires has a mechanical backstop for +// the case where a seat skipped or misjudged it — which is exactly the +// two-possibility fork the incident could not distinguish after the fact. +// Report-only like everything here: the remedy is a PROBE — fetch the PR ref, +// `git merge-tree --write-tree` against today's main answers "would this land +// clean" for free — never a label and never a gate. +// +// ## Why "ACCEPTED/armed" gates the row +// +// A pair of unarmed drafts is ordinary concurrent work-in-progress; the walk +// at their own dispatch time is the instrument for that. The board state +// worth a standing row is the incident's: a side that is DONE — ready +// (= reviewed by construction here: dev PRs flip ready only at review +// ACCEPT) or armed — can be silently passed by the other side landing first, +// after which every proxy signal reads healthy while it rots. `auto_merge` +// is read in the finding-INCREASING direction only, H16's argument exactly: +// arming resolves no conflict, so an armed side is more at risk, not handled. +// +// ## The noise floor is CLOSED, and argued from the single-claim gate's data +// +// That gate's header measured shared-changed-path collisions over ~650 PRs: +// the top repo-wide pairs are the lock file (33), a plugin manifest (21) and +// the root manifest (15) — ordinary concurrent work in a repo taking ~18 +// merges a day. Pairing on those would put a row on the anchor every sweep +// and bury the one that matters. So exactly two spellings are excluded, both +// shared BY CONSTRUCTION, and the closed set is pinned by the self-test +// against per-incident growth (the discipline `SINGLE_CLAIM_PATHS` applies +// to itself): the pnpm lockfile (touched by any PR moving dependencies, and +// merged mechanically) and `.changeset/` (every PR ADDS a uniquely-named +// file there — a collision on one is not a source-file hold). +// `changeset-release/*` PRs are excluded as CANDIDATES for the matching +// reason: the Version Packages PR consumes every changeset file on the +// board, so it would pair with essentially every open PR, and it is +// regenerated from `main` on every push — it holds nothing. +// +// ## Bounded, and bounded honestly +// +// The files fetch is ONE page per candidate, candidates being the open PRs +// this sweep already listed — bounded by the open population (~19 at the +// reading this landed on). A PR with more changed files than the page has +// the tail unread: that can only MISS a pair, never invent one, and both the +// summary line's `read X of Y` and a per-row truncation sentence say so. +// Positive evidence only — this row never asserts "no overlap". +// --------------------------------------------------------------------------- + +/** The closed noise floor — exact spellings, then prefixes. See the banner. */ +export const H36_SHARED_PATH_NOISE = Object.freeze(['pnpm-lock.yaml']); +export const H36_SHARED_PREFIX_NOISE = Object.freeze(['.changeset/']); +export const H36_FILES_PAGE_SIZE = 100; +export const H36_SAMPLE_PATHS = 3; + +export function h36NoisePath(path) { + const p = String(path ?? ''); + if (H36_SHARED_PATH_NOISE.includes(p)) return true; + return H36_SHARED_PREFIX_NOISE.some((prefix) => p.startsWith(prefix)); +} + +/** ACCEPTED (ready = reviewed by construction) or armed — the at-risk side. */ +export function h36AcceptedOrArmed(pr) { + if (!pr) return false; + return pr.draft === false || pr.auto_merge != null; +} + +/** + * Whether this open PR is worth a files page — the gathering policy, H16's + * idiom: answerable from the LIST row alone, and never NARROWER than the + * predicate's population. A pair needs one accepted/armed side, but the + * OTHER side can be any open PR (a draft included — the incident's second + * side was one when the window opened), so every open PR outside the + * changeset-release exclusion is a candidate. + */ +export function h36NeedsFiles(pr) { + if (!pr || pr.merged_at) return false; + return !String(pr.head?.ref ?? '').startsWith('changeset-release/'); +} + +/** The all-failed transport judgement — H16's, verbatim in shape. */ +export function h36DetailPassUnreadable(candidates, probed) { + return (candidates ?? 0) > 0 && (probed ?? 0) === 0; +} + +/** + * The pair rows. `filesByPr` maps PR number → `{ paths, truncated }`; a PR + * absent from the map was unread (a failed page) and simply cannot pair — + * the coverage pair reports that shortfall, and a missed pair is the only + * possible consequence. One row per PAIR, keyed to the accepted/armed side + * (both accepted: the earlier-created one), because that is the side that + * silently rots when the other lands first; the other side is named in the + * sentence. + */ +export function h36SharedFileHolds(openPrs, filesByPr) { + const rows = []; + const prs = (openPrs ?? []).filter((pr) => h36NeedsFiles(pr)); + prs.sort((a, b) => String(a.created_at ?? '').localeCompare(String(b.created_at ?? ''))); + for (let i = 0; i < prs.length; i++) { + for (let j = i + 1; j < prs.length; j++) { + const a = prs[i]; + const b = prs[j]; + if (!h36AcceptedOrArmed(a) && !h36AcceptedOrArmed(b)) continue; + const fa = filesByPr?.get?.(a.number); + const fb = filesByPr?.get?.(b.number); + if (!fa || !fb) continue; + const inB = new Set((fb.paths ?? []).filter((p) => !h36NoisePath(p))); + const shared = (fa.paths ?? []).filter((p) => !h36NoisePath(p) && inB.has(p)); + if (shared.length === 0) continue; + const key = h36AcceptedOrArmed(a) ? a : b; + const other = key === a ? b : a; + const state = + key.auto_merge != null + ? key.draft === false + ? 'ready AND armed' + : 'armed' + : 'ready (reviewed by construction)'; + const sample = shared + .slice(0, H36_SAMPLE_PATHS) + .map((p) => `\`${p}\``) + .join(', '); + const more = shared.length > H36_SAMPLE_PATHS ? `, +${shared.length - H36_SAMPLE_PATHS} more` : ''; + const truncated = + fa.truncated || fb.truncated + ? ' One side\'s file list was TRUNCATED at the page size, which can only have hidden MORE overlap.' + : ''; + rows.push([ + key, + `open and ${state}, sharing ${shared.length} changed file(s) with open PR #${other.number} ` + + `(${sample}${more}) — a cross-lane same-file hold made visible: the fence otherwise lives ` + + 'only in one lane\'s claim comment, and `mergeable_state` volunteers nothing (`unknown` is ' + + 'not a reading). Patrol input, NOT a verdict — both sides may be additive and disjoint. ' + + 'Whichever lands second re-probes before (re-)arming: fetch the PR ref and run ' + + '`git merge-tree --write-tree` against current `main` — the zero-quota reading GitHub ' + + `never volunteers.${truncated}`, + ]); + } + } + return rows; +} + +// --------------------------------------------------------------------------- +// H37 — a FAMILY DISPATCH whose per-member label writes did not all land +// (#12629). +// +// ## The incident, and the exact reason nothing said so +// +// A family dispatch folds N already-triaged cards into ONE dev run: one shared +// branch named after the CHAIN HEAD, one worktree, one PR — and one +// `pm:dispatched` write PER MEMBER. Those writes are N separate calls with +// nothing tying them together, so a partial run is possible and produced no +// signal at all. Measured on the filing seat's own error: in a 3-card fold it +// wrote 2 of the 3, leaving one member carrying a member-pointer claim comment +// naming the chain head while its label still read `pm:queue`. +// +// That combination is a lie in the DISPATCHABLE direction — the queue label +// says "free to claim" about a card a fold already holds — and every per-card +// predicate in this file reads it as clean, CORRECTLY: +// +// - H1/H2/H3/H24/H29 key on `assignee` x label x claim-comment pairings ON +// ONE CARD. That card had no assignee and a consistent-looking `pm:queue`. +// - The fold's own record lives in the CHAIN HEAD's claim comment, on +// another card. Nothing joined the two. +// - The seat's in-flight ledger is its seat post, which lists the fold by +// its HEAD. +// +// The mirror direction is measured too, on this card's own grading round: +// #12200 was left `pm:dispatched` WITH its assignee after the fold's recovery +// had declared release — the same non-atomic write failing the other way, and +// equally invisible until a seat happened to look. Both directions are rows +// here, because a predicate that reported only the first would have said +// nothing about the second instance on the very card that filed it. +// +// ## The join, and why the BRANCH is the whole of it +// +// The fold convention (SKILL.md, step 4 -> 家族派发的折叠认领约定) is: +// +// 共享分支按链首卡命名,每张成员卡各留认领评论并点名该分支 +// +// — the shared branch is named for the chain head, and EVERY member card +// leaves its own claim comment naming that branch (a member with no branch of +// its own is the design, not a half-state). So the fold's roster is already +// written on the board in a machine-readable field: the `Branch:` directive +// this file has read since H20, through `claimedBranches` / `governingClaim`. +// +// A card whose governing claim names `claude/issue-H-...` with H NOT its own +// number IS the member pointer, and a branch carrying at least one such +// claimant IS a live fold. That single reading covers BOTH halves the filing +// card asks to join — 「a member pointer on this card」 and 「named as a member +// in a live `Claim:` on another open card」 — because the shared branch is the +// one string both of them spell, and each member's claim names it. +// +// ⛔ No prose scraping, deliberately. A claim comment names sibling cards, +// serial-constraint predecessors and blocker targets as bare `#N` in its own +// text — the specimen on this row's own filing card names the chain that HELD +// this script — so scanning claim prose for card numbers would manufacture +// members out of the one field the protocol fills in correctly. H20's header +// makes the same call about branch spellings, for the same reason: a fabricated +// row sends a reader to check something that was never there. +// +// ## What the row compares, in BOTH directions +// +// Per fold, the HEAD's own state says whether the fold is in flight, and every +// other claimant is read against it: +// +// head `pm:dispatched` -> IN FLIGHT. A claimant without +// `pm:dispatched` is a missed write — the +// filing seat's own error. +// head CLOSED, or open without -> RELEASED. A claimant still carrying +// `pm:dispatched` `pm:dispatched` is residue — #12200's +// mirror. +// head open + undispatched, -> the HEAD's own half of the write is the one +// some claimant dispatched that missed. The row lands on the head, and +// it is visible from the members' claims and +// from nothing else on the board. +// head in neither population -> UNRESOLVABLE, and the row DECLINES rather +// than picking a side (H19's call). +// +// A fold in which NOTHING is dispatched — head included — is deliberately +// quiet. It is indistinguishable from an abandoned or not-yet-launched fold, +// and those cards are already H30's population on their own terms. +// Under-report over fabricate is this file's standing direction. +// +// ## The noise floor is CLOSED, and it is the ordinary case +// +// Exactly ONE exclusion, and it covers essentially the whole board: a claim +// naming its OWN card's branch is not fold evidence. Every solo dispatch here +// writes exactly that, so without the exclusion this row would fire on every +// dispatched card in existence. A branch is a fold branch only when some +// claimant is not its head. ⛔ Nothing else is suppressed by name — this row +// carries no per-incident exclusion list to grow, which is the discipline H36's +// closed noise floor states and `SINGLE_CLAIM_PATHS` applies to itself. +// +// ## Bounded, and bounded honestly +// +// Round 1 is FREE. It reads the claim threads this sweep ALREADY holds: every +// open `pm:dispatched` card (the dispatch-liveness loop) and every assigned +// pm-tracked card (H2's fetch). So fold DISCOVERY converges on the open +// `pm:dispatched` population, exactly as the filing card bounds it — a fold +// with a dispatched member announces itself there at no cost. +// +// Round 2 buys ONE comment page per open `pm:queue` card, and ONLY when round 1 +// actually saw a live fold. A board with no fold in flight therefore costs +// NOTHING, and a board with one costs at most the queue page count (40 open +// `pm:queue` cards at the 2026-08-24 census H30 measured, against a 15,000/h +// core quota and four sweeps a day). It is bought for `pm:queue` because that +// is precisely where the dispatchable-direction lie parks — a card the queue +// offers while a fold already holds it — and it is the one population whose +// threads no other item here reads. +// +// Positive evidence only. A member page that fails drops that card out of the +// roster — a MISS, never an invention — and the coverage pair reports it, with +// an all-failed pass named in the summary as the transport it is (#4690). The +// pass does NOT rethrow, unlike H16's and H36's: those rows ARE their detail +// pass, while two of this row's three directions are free and already gathered, +// so discarding the sweep would cost more readings than it protects. ⛔ This +// row never asserts that a fold's writes all landed. +// +// Report-only like everything here: the remedy is a seat re-reading the fold's +// members and writing the missing half. ⛔ Never a label written from this +// script. +// --------------------------------------------------------------------------- + +/** How many fold members one row names before it counts the rest — H19/H20's budget, same grounds. */ +export const H37_MEMBER_LIST_CAP = 5; + +/** + * The live folds on this board, keyed by the SHARED BRANCH. + * + * @param {{ number: number, branches: string[] }[]} claims — one entry per card + * whose governing claim named at least one protocol-shaped branch. + * @returns {Map} branch -> + * the chain head the branch is named for, and every card claiming it. + * + * The single exclusion IS the noise floor (see the banner): a branch whose only + * claimant is its own head is ordinary solo work and never a fold. Branch keys + * come back sorted so a roster is deterministic for a reader and for the + * self-test; claimants are sorted for the same reason. + */ +export function h37FoldBranches(claims) { + const byBranch = new Map(); + for (const claim of claims ?? []) { + const number = Number(claim?.number); + if (!Number.isFinite(number)) continue; + for (const branch of claim?.branches ?? []) { + // ⚠️ The null check is separate from the finiteness one and must stay so: + // `branchNameTarget` returns NULL for a branch it cannot read, and + // `Number(null)` is 0 — a perfectly finite head number for a card that + // does not exist. Collapsing the two mints a phantom fold on every + // unreadable branch spelling, which then gates the round-2 member read on + // nothing (caught by this row's own pin). + const target = branchNameTarget(branch); + if (target === null) continue; + const head = Number(target); + if (!Number.isFinite(head)) continue; + if (!byBranch.has(branch)) byBranch.set(branch, { head, claimants: [] }); + const entry = byBranch.get(branch); + if (!entry.claimants.includes(number)) entry.claimants.push(number); + } + } + const folds = new Map(); + for (const branch of [...byBranch.keys()].sort()) { + const entry = byBranch.get(branch); + if (!entry.claimants.some((n) => n !== entry.head)) continue; + folds.set(branch, { + head: entry.head, + claimants: [...entry.claimants].sort((a, b) => a - b), + }); + } + return folds; +} + +/** + * Whether the fold headed by `head` is in flight, released, or unreadable — + * four-valued, because "open but not dispatched" and "closed" are the same + * RELEASE for a member's verdict while only the first can carry a row of its + * own. + * + * `unknown` is the #4690 state and is kept distinct from both: a head this + * sweep never listed cannot be reported as released, which would accuse every + * dispatched member of residue on the strength of a card nobody read. + */ +export function h37HeadState(head, openByNumber, closedByNumber) { + const n = Number(head); + const open = openByNumber?.get?.(n); + if (open) return labelNames(open).includes('pm:dispatched') ? 'in-flight' : 'open-undispatched'; + return closedByNumber?.get?.(n) ? 'closed' : 'unknown'; } /** - * H27 — null when clean, else the finding sentence. - * - * ## The conjunction, and why each term is load-bearing - * - * `pm:dispatched` the card still claims to be in flight - * claim older than 24h the protocol's own stale line - * a claimed branch EXISTS (else it is H20's row, not this one) - * NO branch moved since the claim — nothing was pushed for this dispatch - * no PR delivers the card neither open nor within the merged window - * - * ⛔ Dropping the branch-activity term would give exactly the PR-keyed row H20 - * refuses to be, and it is refused there for a measured reason: a dev inside a - * long build legitimately has a ref and no PR for over an hour. That objection - * is answered here by BOTH remaining terms and not by the threshold alone — a - * dev 24 hours in with commits landing is excluded by branch activity, and a - * dev with a PR open is excluded by delivery. What is left is a branch that has - * not moved since it was claimed, with nothing to show for a day. - * - * ## What it under-reports, stated rather than discovered - * - * A dev that pushed one commit and THEN died is not reported: its branch moved - * after the claim, so the activity term clears it. That is the measured shape - * of one of the three incident cards, and widening the term to "no activity in - * the last 24h" would catch it — at the cost of colliding with the protocol's - * 「有带提交活分支的认领永不回收」, which is a rule about exactly that card. - * Under-reporting on a card the protocol protects is the same call H17's - * extractor and H20's branch-shape matcher make: a row a reader cannot act on - * is worse than no row. + * The member classifier — asserted directly by the self-test, H35's idiom, so + * the three-way fold is pinned independently of any sentence it produces. * - * @param {object} issue — an OPEN issue. - * @param {{ branches: string[], createdAt: string|null }|null} claim - * @param {{ branch: string, state: 'exists'|'absent'|'unreadable', - * headCommittedAt?: string|null }[]} refStates - * @param {{ open: number, merged: number }} delivery — `claimDelivery`. + * @param {'in-flight'|'open-undispatched'|'closed'|'unknown'} headState + * @param {boolean} memberDispatched + * @returns {'undispatched-member'|'dispatched-residue'|'clean'|'unresolvable-head'} */ -export function h27DeadClaimNoProgress(issue, claim, refStates, delivery, nowMs = Date.now()) { - if (!labelNames(issue ?? {}).includes('pm:dispatched')) return null; - if (!claim || (claim.branches ?? []).length === 0) return null; - const age = claimAgeHours(claim, nowMs); - if (age !== null && age <= DEAD_CLAIM_STALE_HOURS) return null; - - const rows = refStates ?? []; - if (rows.length === 0) return null; - const present = rows.filter((r) => r.state === 'exists'); - // No ref at all is H20's row; an unreadable probe is H20's quieter one. This - // row speaks only about branches it KNOWS are there. - if (present.length === 0) return null; +export function h37MemberVerdict(headState, memberDispatched) { + if (headState === 'unknown') return 'unresolvable-head'; + if (headState === 'in-flight') return memberDispatched ? 'clean' : 'undispatched-member'; + return memberDispatched ? 'dispatched-residue' : 'clean'; +} - // A delivery in either channel ends the question: an open PR is live work (or - // work already handed over), and a merged one is H8's row about a paired - // write, never this row's about a dead agent. - const { open = 0, merged = 0 } = delivery ?? {}; - if (open > 0 || merged > 0) return null; +/** Which cards buy a member comment page — the gathering policy, exported like every other. */ +export function h37NeedsMemberRead(issue, foldsSeen) { + if (!((foldsSeen ?? 0) > 0)) return false; + const labels = labelNames(issue ?? {}); + if (labels.includes('pm:dispatched')) return false; + return labels.includes('pm:queue'); +} - const moved = present.map((r) => branchMovedSinceClaim(r, claim)); - if (moved.some((m) => m === true)) return null; +/** + * Whether the member pass failed as a TRANSPORT rather than leaving a bounded + * gap — H16's judgement, shared in shape and deliberately not re-derived. Zero + * candidates is a clean reading (no fold was live, or the queue is empty), never + * a fault. + */ +export function h37MemberPassUnreadable(candidates, probed) { + return (candidates ?? 0) > 0 && (probed ?? 0) === 0; +} - const named = namedBranches(present.map((r) => ({ branch: r.branch, state: r.state }))); - const recovery = - ' Report-only, and pointedly NOT a reclaim: the protocol reclaims a claim whose branch does ' + - 'NOT exist and states 「有带提交活分支的认领永不回收」, so a row about a branch that DOES ' + - 'exist can never be authority to drop an assignee. The remedy is the post-kill recovery ' + - 'inspection (`references/dispatch-runbook.md`): probe the claimant, then read all THREE ' + - 'states — on the remote / on the container disk only / gone — and hand anything found to a ' + - 'replacement flagged UNVERIFIED. ⛔ Never a label written from this script.'; +/** `#N` for each fold member, capped at the render budget, + its note. */ +function namedMembers(numbers) { + const shown = numbers.slice(0, H37_MEMBER_LIST_CAP); + const named = shown.map((n) => `#${n}`).join(', '); + return `${named}${numbers.length > shown.length ? ` +${numbers.length - shown.length} more` : ''}`; +} - if (moved.some((m) => m === null)) { - return ( - `\`pm:dispatched\` with a complete claim naming ${named}, and whether that branch has MOVED ` + - 'since the claim could not be determined this sweep (an unreadable claim or head-commit ' + - 'timestamp) — so this dispatch is UNJUDGED, not confirmed healthy. Unread is not "no ' + - 'activity" and it is not "activity" either (#4690); a liveness comparison dropped in ' + - 'silence reads as a working dev forever, which is the exact failure this item exists to ' + - 'end. Read the branch and the claim by hand.' + - recovery - ); +/** + * The rows. One per CARD whose own `pm:dispatched` disagrees with the fold it + * claims, keyed to that card because that is where the missing write belongs. + * + * A claimant this sweep did not list as open is skipped rather than judged: the + * roster is assembled from claim TEXT, and a number that resolves to nothing + * open here is not evidence of anything (H19's treatment of an absent + * resolution, and the same reason `h37HeadState` keeps `unknown` separate). + * + * @param {Map} folds + * @param {Map} openByNumber + * @param {Map} closedByNumber + */ +export function h37FamilyMemberDrift(folds, openByNumber, closedByNumber) { + const rows = []; + for (const [branch, entry] of folds ?? new Map()) { + const head = entry?.head; + const headState = h37HeadState(head, openByNumber, closedByNumber); + if (headState === 'unknown') continue; + const members = (entry?.claimants ?? []).filter((n) => n !== head); + const dispatchedMembers = []; + const pending = []; + for (const n of members) { + const card = openByNumber?.get?.(n); + if (!card) continue; + const dispatched = labelNames(card).includes('pm:dispatched'); + if (dispatched) dispatchedMembers.push(n); + pending.push([card, h37MemberVerdict(headState, dispatched)]); + } + for (const [card, verdict] of pending) { + if (verdict === 'undispatched-member') { + rows.push([ + card, + `carries a family-dispatch member pointer at \`${branch}\` — the shared branch of the fold ` + + `headed by #${head}, which IS \`pm:dispatched\` — while this card carries none. The fold's ` + + 'per-member label writes are separate calls with nothing tying them together, so a partial ' + + 'run leaves the queue offering a card the fold already holds, and every per-card predicate ' + + 'here reads it as clean. Patrol input, NOT a verdict — a claim may name another card\'s ' + + 'branch for a reason that is not a fold (a cross-lane hand-off, a mistyped branch). Remedy: ' + + `re-read the fold's members off \`${branch}\` and write the missing half (assign + state in ` + + 'ONE write), or correct the branch this claim names. Claimants seen on that branch: ' + + `${namedMembers(entry?.claimants ?? [])}.`, + ]); + } else if (verdict === 'dispatched-residue') { + rows.push([ + card, + `still carries \`pm:dispatched\` while the fold it claims — \`${branch}\`, headed by #${head} ` + + `— has ${headState === 'closed' ? 'CLOSED' : 'released the state (open, no `pm:dispatched`)'}. ` + + 'That is the same non-atomic family write failing in the MIRROR direction, and it reads to ' + + 'every per-card predicate as an ordinary in-flight card. Patrol input, NOT a verdict — this ' + + 'member may still have work of its own outstanding. Remedy: clear the residue in the write ' + + 'that cleared the head (drop `pm:dispatched` and the assignee together — H24\'s ' + + '「同笔摘 assignee」), or re-dispatch it on its own card and its own branch if work remains. ' + + `Claimants seen on that branch: ${namedMembers(entry?.claimants ?? [])}.`, + ]); + } + } + if (headState === 'open-undispatched' && dispatchedMembers.length > 0) { + rows.push([ + openByNumber.get(Number(head)), + `is the chain head of \`${branch}\` and carries NO \`pm:dispatched\`, while ` + + `${dispatchedMembers.length} card(s) claiming that branch do (${namedMembers(dispatchedMembers)}) ` + + '— the head\'s own half of the fold\'s label write is the one that did not land, and it is ' + + 'visible ONLY from those members\' claims: nothing on this card says so. Patrol input, NOT a ' + + 'verdict — the members may equally be pointing at the wrong branch. Remedy: confirm which ' + + 'reading holds, then write the missing half here or correct the claims that name it.', + ]); + } } - - const reading = - age === null - ? 'an unreadable claim timestamp (which must not read as fresh)' - : `~${Math.round(age)}h after the claim was posted (threshold ${DEAD_CLAIM_STALE_HOURS}h, the ` + - "protocol's own stale-claim line)"; - - return ( - `\`pm:dispatched\` with a PERFECT claim — assignee set, a first-line \`Claim:\` comment, and ` + - `${named} present on the remote — that has NOT MOVED SINCE IT WAS CLAIMED, with no PR ` + - `delivering the card, ${reading}. This is what a dev agent that DIED leaves behind, and the ` + - 'measured cause arrives in batches rather than singly: one shared-account capacity limit ' + - 'killed three concurrently-dispatched agents at once. ⭐ The card is indistinguishable from ' + - 'healthy in-flight work from the card itself — every field is correct, which is why no ' + - 'predicate here fired on it: H1 wants a missing assignee, H2 a missing claim comment, H8 a ' + - 'merged PR, and H20 no remote ref at all. H20 misses it BY CONSTRUCTION, not by accident: ' + - 'the dev-agent definition makes pushing the empty branch the first action of the task, so a ' + - 'protocol-compliant agent that dies still leaves a ref. Left unreported it does worse than ' + - "sit there — the next PM's round-open mutual-exclusion read treats a dead `Claim:` as a live " + - 'claim by another session and stays off the card, so one dead agent blocks the lane. ⛔ Rule ' + - 'out one reading first: a delivery that merged BEFORE this sweep\'s merged window ' + - `(${MERGED_WINDOW_PAGES} pages) is invisible here, so a card whose PR landed days ago and ` + - 'whose branch was never deleted can reach this row — check the card for a merged delivery ' + - 'before treating it as a death.' + - recovery - ); + return rows; } // --------------------------------------------------------------------------- @@ -4413,6 +6595,101 @@ export function isLoudFinding(message) { return String(message ?? '').startsWith(P0_SUSPECT_MARKER); } +/** + * The marker an UNJUDGED row carries — a row reporting that an input could not + * be read, as opposed to one reporting a state that was read (#11218). + * + * ## Why this needs to exist at all: the promise was measurably false + * + * The summary sentence has been saying 「each unresolved target is named on its + * own card's row, never dropped」 — and on the 2026-08-25T02:08Z sweep it was + * not true. That run resolved 25 of 28 targets and rendered FIVE H19 rows, all + * of them same-repo and closed-led; not one carried an unresolved target, + * because the markdown body ran out of budget and 199 rows were trimmed. The + * rows the header promised were never dropped had been dropped, by the trim, + * while the header went on promising it. + * + * That is precisely #4690 wearing this item's own uniform: "could not read the + * input" rendered as clean, inside the mechanism built to stop exactly that. A + * reader could not have caught it either — the trimmed rows announce themselves + * only as a COUNT, so 「199 further row(s) omitted」 and 「every unresolved target + * is named」 sat in one body, contradicting each other, with nothing to say which + * was true. + * + * The fix is ordering, not budget: unjudged rows sort ahead of judged ones, so + * the trim can only ever fall on rows that DID make a determination. Judged rows + * are recoverable from the run log and say so; an unjudged one is the report's + * only trace of a gap in what was read. This is the same reservation + * `renderTriggerIndex` gets and for the same reason — a section whose absence is + * indistinguishable from good news must not be what the trim eats. + */ +export const UNJUDGED_MARKER = '⚠️ UNJUDGED:'; + +/** + * Is this row an unjudged one — an input that could not be read, rather than a + * state that was? + * + * Two sources, deliberately: the explicit marker, and H19's unresolved-branch + * sentence, which is load-bearing prose that predates the marker and is pinned + * by the self-test. Matching the sentence rather than only the marker keeps the + * protection working for the population it was measured on rather than only for + * rows written after it. + */ +export function isUnjudgedFinding(message) { + const text = String(message ?? ''); + return text.startsWith(UNJUDGED_MARKER) || text.includes('is UNJUDGED, not confirmed'); +} + +/** + * Every counter the summary sentence reads OFF `stats` — the forwarding + * contract between the sweep and the renderers, in one place. + * + * ## Why this is a list and not eleven hand-written lines + * + * It used to be eleven hand-written lines, and two of them were missing. + * `dispatchRefTargets`/`dispatchRefRead` were computed by the H20/H27 pass and + * never copied into `counts`, so the dispatch-liveness clause rendered + * `read on 0 of 0` on every live sweep from the day it was added — the + * 2026-08-25T02:08Z run said it had read no branch at all while publishing two + * H20 findings, which only a non-empty ref cache can produce. + * + * The failure is invisible by construction: `counts.x ?? 0` renders a missing + * key and a genuine zero identically, and a genuine zero is a legitimate + * reading ("nothing to probe this sweep"). So the coverage pair — the mechanism + * whose entire job is to prove a pass examined something — was the one thing in + * the report that could go quiet without any evidence that it had. #4690, + * wearing the uniform of the check built to prevent it. + * + * Enumerating the keys makes the assembly mechanical: `sweep()` copies THIS + * list, so adding a counter to `stats` and forgetting to forward it is no + * longer possible, and the self-test can assert the contract directly rather + * than re-deriving it from a rendered sentence. + */ +export const SWEEP_COUNT_KEYS = [ + 'conflictCandidates', + 'conflictProbed', + 'sharedFileCandidates', + 'sharedFileProbed', + 'liveFolds', + 'memberReadCandidates', + 'memberReadProbed', + 'fallbackCandidates', + 'fallbackProbed', + 'restartCandidates', + 'restartProbed', + 'blockerTargets', + 'blockerResolved', + 'dispatchRefTargets', + 'dispatchRefRead', + 'crossRepoProbed', + 'crossRepoUnreadable', + 'seatCandidates', + 'seatMarkersRead', + 'commits', + 'commitBindings', + 'commitBindingMessages', +]; + /** * The summary sentence both media end on — the one line that says what was * READ, not just what was found. It is the difference between "the board is @@ -4433,11 +6710,13 @@ export function isLoudFinding(message) { * holdProbed?: number, holdCandidates?: number, fallbackProbed?: number, * fallbackCandidates?: number, restartProbed?: number, * restartCandidates?: number, blockerResolved?: number, - * blockerTargets?: number, commits?: number, commitBindings?: number, - * commitBindingMessages?: number, - * closedWindowDisabled?: boolean, closedFloor?: string }} counts + * blockerTargets?: number, crossRepoProbed?: number, + * crossRepoUnreadable?: number, seatMarkersRead?: number, + * seatCandidates?: number, commits?: number, commitBindings?: number, + * commitBindingMessages?: number, closedWindowDisabled?: boolean, closedFloor?: string }} counts * @param {number} findingCount */ + export function summaryLine(counts, findingCount) { const probed = counts.conflictProbed ?? 0; const candidates = counts.conflictCandidates ?? 0; @@ -4465,6 +6744,33 @@ export function summaryLine(counts, findingCount) { // still live (#4690). const btResolved = counts.blockerResolved ?? 0; const btTargets = counts.blockerTargets ?? 0; + // H19's cross-repo reachability readings (#11218) — how many DISTINCT sibling + // repos this sweep probed directly, and how many refused. Reported because it + // is what turns an unresolved cross-repo target from a guess into a + // measurement, and because a reader needs to know the limit is structural + // (a ruling) rather than a transient failure worth re-running. + 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 + // 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 + // measured residue of this row (29 over the 3.41-day derivation corpus, against + // 0 half-writes), and burying it would let a quiet H35 section read as "the + // gate is watched" when most removals are structurally unwatchable. + const gateRemovals = counts.gateRemovals ?? 0; + const gateEventPages = counts.eventPages ?? 0; + const gateUnjudgeable = counts.gate_unjudgeable ?? 0; + const gateUndated = counts.gate_undated ?? 0; + const gateWindowTruncated = Boolean(counts.eventWindowTruncated); + // H32's coverage pair — held, own-board seats and how many had their marker + // thread read. A shortfall is not silent (an unread thread makes H32 DECLINE + // to judge that seat, which is the quiet direction), so this is the only + // place a reader could see that a seat went unexamined. + const seatsRead = counts.seatMarkersRead ?? 0; + const seatCandidates = counts.seatCandidates ?? 0; // The fifth pair, H20's, and the same shape as H19's for the same reason: an // unreadable ref fires its own card's quieter row, so this is a total rather // than the only place the gap shows. Still owed — a pass that read no ref at @@ -4505,12 +6811,59 @@ export function summaryLine(counts, findingCount) { `${rwProbed < rwCandidates ? " — each unread thread fires its own card's H9 row, never dropped" : ''}. ` + `Blocker liveness (H19): targets resolved on ${btResolved} of ${btTargets} distinct \`Blocked-by:\` ` + `target(s) named by open \`pm:blocked\` card(s)` + - `${btResolved < btTargets ? ' — each unresolved target is named on its own card\'s row, never dropped' : ''}. ` + + `${ + btResolved < btTargets + ? ` — the ${btTargets - btResolved} unresolved target(s) are named on their own cards' rows, and ` + + 'those rows sort ABOVE the size trim so they cannot be what a truncated body drops (#11218: ' + + 'this clause used to be an unconditional promise, and on the 2026-08-25T02:08Z sweep it was ' + + 'false — 199 rows were trimmed and not one rendered row carried an unresolved target)' + : '' + }` + + `${ + crossRepoProbed > 0 + ? ` Cross-repo reachability was measured directly on ${crossRepoProbed} sibling repo(s), of which ` + + `${crossRepoUnreadable} do(es) not answer this credential — those targets are unjudgeable by ` + + 'ruling (each install reads its own repo with its own repo-scoped token) and ⛔ no re-run ' + + 'resolves them.' + : '' + } ` + `Dispatch liveness (H20 + H27): remote branch read on ${refRead} of ${refTargets} distinct claimed ` + `branch(es) named by open \`pm:dispatched\` card(s) past the ${DISPATCHED_NO_REF_STALE_MINUTES}-minute ` + `threshold — one read serving both rows, so H27's ${DEAD_CLAIM_STALE_HOURS}h population is a subset ` + 'of this one and costs no request of its own' + `${refRead < refTargets ? ' — each unread branch is named on its own card\'s row, never dropped' : ''}. ` + + `Seat liveness (H32): marker thread read on ${seatsRead} of ${seatCandidates} HELD seat post(s) whose ` + + 'lane is countable on THIS board — a seat held for a sibling repo\'s lane is out of scope here (its ' + + 'inventory is unreadable from this sweep, so an empty-looking queue would mean nothing), and an ' + + 'unread thread makes H32 decline to judge that seat rather than accuse it. ' + + `Gate-removal patrol (H35): ${gateRemovals} removal(s) of a gate-semantic label read from ` + + `${gateEventPages} page(s) of the repo-wide issue-event stream over the last ` + + `${H35_EVENT_WINDOW_HOURS}h — no per-card timeline fetch. ${gateUnjudgeable} of them are ` + + 'UNJUDGEABLE (a gate that only ever had ONE carrier leaves 「双载体同笔清标」 no evidence in ' + + 'either direction, so neither H31 nor H35 can say cleared-or-stripped)' + + `${gateUndated > 0 ? `, and ${gateUndated} more had no hang inside the window` : ''}` + + `${ + gateWindowTruncated + ? ` ⚠️ The event window was TRUNCATED at the ${H35_EVENT_PAGE_CAP}-page cap before reaching ` + + `the ${H35_EVENT_WINDOW_HOURS}h horizon — this run saw LESS than its stated window, so a ` + + 'quiet H35 section here is a short read, not a clean board.' + : '.' + } ` + + `Shared-file holds (H36): changed-file page read on ${counts.sharedFileProbed ?? 0} of ` + + `${counts.sharedFileCandidates ?? 0} open PR(s) — a pair needs both sides read, so a shortfall ` + + 'can only MISS a hold, never invent one. ' + + `Family folds (H37): ${counts.liveFolds ?? 0} live shared branch(es) claimed by more than their ` + + `own chain head, and a member comment page read on ${counts.memberReadProbed ?? 0} of ` + + `${counts.memberReadCandidates ?? 0} open \`pm:queue\` card(s) — that second read is bought ONLY ` + + 'when a fold is live, so 0 of 0 is a board with no fold in flight rather than a pass that ' + + 'skipped one, and an unread member can only MISS a drifted write, never invent one.' + + `${ + h37MemberPassUnreadable(counts.memberReadCandidates, counts.memberReadProbed) + ? ' ⚠️ NO member page was readable on this run, so H37 saw a live fold and judged its queue ' + + 'side on NOTHING — a quiet H37 section here is the transport, not a fold whose writes all ' + + 'landed.' + : '' + } ` + `Report-only: findings are patrol input, not a gate verdict.` ); } @@ -4648,10 +7001,18 @@ export function normalizeProvenance(text) { export function renderMarkdown(findings, counts, options = {}) { const provenance = normalizeProvenance(options.provenance); const sweptAt = options.sweptAt instanceof Date ? options.sweptAt : new Date(); + // Three ranks, and the middle one is load-bearing (#11218): P0-suspect rows, + // then UNJUDGED rows, then everything else by card number. The trim below + // eats the TAIL, so this ordering is what makes the summary's "never dropped" + // clause true — see `UNJUDGED_MARKER` for the sweep on which it was false. const rows = [...findings].sort( - (a, b) => Number(isLoudFinding(b[2])) - Number(isLoudFinding(a[2])) || a[0].number - b[0].number, + (a, b) => + Number(isLoudFinding(b[2])) - Number(isLoudFinding(a[2])) || + Number(isUnjudgedFinding(b[2])) - Number(isUnjudgedFinding(a[2])) || + a[0].number - b[0].number, ); const loudCount = rows.filter(([, , msg]) => isLoudFinding(msg)).length; + const unjudgedCount = rows.filter(([, , msg]) => isUnjudgedFinding(msg)).length; const head = [ 'os-half-state-sweep — machine-findable marker for this generated view.', @@ -4680,6 +7041,16 @@ export function renderMarkdown(findings, counts, options = {}) { ); } + if (unjudgedCount > 0) { + head.push( + `⚠️ **${unjudgedCount} UNJUDGED row(s) in this sweep** — an input this patrol could NOT read,` + + ' not a state it read and found clean. They are sorted above the ordinary rows so the' + + " body's size trim can never be what removes them (#4690, #11218), and they are the rows to" + + ' judge BY HAND: nothing in a later sweep will resolve them on its own.', + '', + ); + } + head.push(`**${summaryLine(counts, rows.length)}**`, ''); // The H17 index is built BEFORE the findings are laid out and appended @@ -5286,6 +7657,11 @@ async function sweep(options = {}) { const stats = { conflictCandidates: 0, conflictProbed: 0, + // H36's coverage pair (#12286) — open PRs whose changed-file page this + // sweep owes, and how many pages actually answered. Same per-row failure + // mode as H16's detail pass, so it owes the same `read X of Y`. + sharedFileCandidates: 0, + sharedFileProbed: 0, fallbackCandidates: 0, fallbackProbed: 0, // H9's coverage pair — `pm:on-hold` cards whose verdict the comment @@ -5296,6 +7672,13 @@ async function sweep(options = {}) { // got a definite open/closed answer. blockerTargets: 0, blockerResolved: 0, + // H19's cross-repo reachability pair (#11218) and H32's seat pair (#11706) + // — initialised here for the reason the commit counts are: a sweep that + // throws before those passes still renders numbers rather than `undefined`. + crossRepoProbed: 0, + crossRepoUnreadable: 0, + seatCandidates: 0, + seatMarkersRead: 0, // H23's coverage numbers (#10942) — how many commit messages this pass read // and how much closing-keyword traffic they carry. Initialised to 0 here // rather than left absent so a sweep that throws before the commit pass @@ -5327,19 +7710,17 @@ async function sweep(options = {}) { // rather than "read 0" — a disabled reader and an empty result are the same // number and opposite facts (#4690). closedWindowDisabled: CLOSED_WINDOW.pages === 0, - conflictCandidates: stats.conflictCandidates, - conflictProbed: stats.conflictProbed, + // The hold pair is the one counter that does NOT live on `stats` — it is + // accumulated on the H17 gathering object — so it is forwarded by hand and + // everything else comes off the enumerated contract below. holdCandidates: hold.candidates, holdProbed: hold.probed, - fallbackCandidates: stats.fallbackCandidates, - fallbackProbed: stats.fallbackProbed, - restartCandidates: stats.restartCandidates, - restartProbed: stats.restartProbed, - blockerTargets: stats.blockerTargets, - blockerResolved: stats.blockerResolved, - commits: stats.commits, - commitBindings: stats.commitBindings, - commitBindingMessages: stats.commitBindingMessages, + // ⚠️ Copied from `SWEEP_COUNT_KEYS` rather than listed here, because two of + // these keys were missing from the hand-written list and rendered the + // dispatch-liveness clause as `read on 0 of 0` on every live sweep. See the + // constant for the incident; the point of the loop is that forgetting a key + // is no longer a thing this assembly can do. + ...Object.fromEntries(SWEEP_COUNT_KEYS.map((key) => [key, stats[key]])), }; // The oracle is read ONCE per sweep, after gathering: it is a local // `git ls-files`, not a request, and every candidate token is checked @@ -5586,20 +7967,20 @@ const CLOSED_WINDOW = resolveClosedWindowPages(process.env); * H22's DATED CLOSURE FLOOR — the cutover date at and after which a closed * card's `pm:*` residue is judged (objectui#5985). * - * ## The dilemma this dissolves, and why THIS install is the one that needed it + * ## The dilemma this dissolves * * The window above is bounded by UPDATE recency, which is the wrong axis for - * the question this repo kept running into: "was this card closed under the - * convention, or before it existed?" Measured here 2026-08-24, while porting - * this file: 815 closed cards carry `pm:dispatched`, and ~347 of the 400 - * issues in the window carry some `pm:*` residue (~87%, against the 26% - * upstream measured on its own board). At that density H22 reports the - * CONVENTION rather than a defect — ~347 rows that exhaust the anchor body - * budget and trim every other predicate's findings out of the report. So the - * reader shipped OFF (`PM_SWEEP_CLOSED_WINDOW_PAGES: '0'`, divergence 1), and - * objectui#5985 recorded the choice as a two-way one: either stripping is the - * rule (and ~815 cards need a BACKFILL before H22 can be honest) or it is not - * (and H22 is simply not a predicate this repo wants). + * the one question a sibling install kept running into: "was this card closed + * under the convention, or before it existed?" Measured in objectui + * 2026-08-24, while porting this file: 815 closed cards there carry + * `pm:dispatched`, and ~347 of the 400 issues in the window above carry some + * `pm:*` residue (~87%, against the 26% this repo measured on its own board). + * At that density H22 reports the CONVENTION rather than a defect — ~347 rows + * that exhaust the anchor body budget and trim every other predicate's + * findings out of the report. That install therefore shipped with the closed + * reader switched off, and its card recorded the choice as a two-way one: + * either stripping is the rule (and ~815 cards need a BACKFILL before H22 can + * be honest) or it is not (and H22 is simply not a predicate that repo wants). * * The floor is the third option both readings omit. `pm:*` on a card closed * before the convention was written is inert history: nothing queries it as a @@ -5612,23 +7993,24 @@ const CLOSED_WINDOW = resolveClosedWindowPages(process.env); * cards: 815 mutating writes to make a report quieter is machinery serving the * instrument, and no code path here writes a label at all. * - * ## Ported, not invented here + * ## Default: unset, which is exactly today's behaviour * - * This is upstream's code (objectstack `scripts/pm/check-half-states.mjs`), - * carried across with the docblock re-pointed at this board's measurement. ⚠️ - * It is NOT a fourth hand divergence: a re-sync that replaces this file with - * upstream's must keep the floor, because upstream has it. What IS divergent - * is the WIRING in `.github/workflows/half-state-patrol.yml` — see divergence - * 1 there, which now sets a floor instead of switching the reader off. + * An install that wants every card in the window judged sets nothing, and this + * resolver returns a null floor that the predicate ignores. That keeps this + * repo's own patrol byte-identical across this change — it measured 26% and + * treats recent closed residue as a live duty — and makes the floor a + * per-install adaptation rather than a policy shipped to everyone. * * ## Malformed is REFUSED, never defaulted * * A typo'd floor that silently became "no floor" would restore the 87% flood - * here, four times a day, and the flood reads as a working patrol — the same - * trap `resolveSweepRepo` and `resolveClosedWindowPages` refuse by name. Only + * on the one install that set it, four times a day, and the flood reads as a + * working patrol — the same trap `resolveSweepRepo` refuses by name. So an + * unparseable value is `valid: false` and the entrypoint exits 2 on it. Only * the `YYYY-MM-DD` spelling is accepted: a bare `Date` parse would take * "yesterday-ish" strings and timezone-bearing ones whose midnight is not the - * one the workflow author meant, and the value is written by hand exactly once. + * one the workflow author meant, and the value is written by hand in a + * workflow file exactly once. */ export function resolveClosureFloor(env = {}) { const raw = String(env.PM_SWEEP_CLOSED_FLOOR ?? '').trim(); @@ -5727,6 +8109,45 @@ async function listRecentDefaultBranchCommits() { return out; } +/** + * 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). + * + * 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 + * predates it — on a quiet stretch that is two pages, and the cap only binds + * when the board is busier than the corpus it was derived from. Both bounds are + * reported: `eventPages` counts what was read and `eventWindowTruncated` says + * the horizon was NOT reached, because a short window that reads as a clean one + * is the #4690 direction this file refuses everywhere. + * + * PRs ride this endpoint too (a pull request is an issue to it), which is what + * lets one window see BOTH carriers of a dual-carrier gate. + */ +async function listRecentIssueEvents(stats = {}, nowMs = Date.now()) { + const horizon = nowMs - H35_EVENT_WINDOW_HOURS * 3_600_000; + const out = []; + let reachedHorizon = false; + let page = 1; + for (; page <= H35_EVENT_PAGE_CAP; page++) { + const batch = await rest(`/repos/${OWNER_REPO}/issues/events?per_page=100&page=${page}`); + out.push(...batch); + const oldest = Date.parse(batch[batch.length - 1]?.created_at ?? ''); + if (batch.length < 100 || (Number.isFinite(oldest) && oldest <= horizon)) { + reachedHorizon = true; + break; + } + } + stats.eventPages = Math.min(page, H35_EVENT_PAGE_CAP); + stats.eventRows = out.length; + stats.eventWindowTruncated = !reachedHorizon; + return out.filter((e) => { + const at = Date.parse(e?.created_at ?? ''); + return Number.isFinite(at) && at > horizon; + }); +} + /** * The unscoped listing H13 needs: the domain-without-pm-state shape is * DEFINED by the absence of every label the listings below key on, so no @@ -5826,6 +8247,13 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen const restartFor = (issue) => restart.unreadable.has(issue.number) ? null : restart.comments.get(issue.number); + // H32's gathered seat markers (#11706), judged after the label pages finish: + // the predicate needs the LANE INVENTORY, and that is a count over the + // `pm:queue`/`pm:dispatched` listings which are only complete once this loop + // has consumed them. Gathering and judging are therefore split across the + // loop boundary, the same way H8's merged-PR window is. + const seatMarkers = new Map(); + for (const issue of seen.values()) { const labels = labelNames(issue); if (h1DispatchedNoAssignee(issue)) { @@ -5842,6 +8270,15 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen if (queuedAndTaken) findings.push([issue, 'H24', queuedAndTaken]); const doubleState = h25AwaitingMaintainerExclusivity(issue); if (doubleState) findings.push([issue, 'H25', doubleState]); + // H29 + H30 — the same free reads, one label-set and one timestamp. Their + // populations are covered here BY CONSTRUCTION: every H29 pair contains at + // least one label this loop lists (the ONE-OF vocabulary is the label pages + // plus `needs-user-decision`, which can only pair WITH one of them), and + // H30's population is the `pm:queue` page itself. + const twoStates = h29PmStateExclusivity(issue); + if (twoStates) findings.push([issue, 'H29', twoStates]); + const rotting = h30QueueRotting(issue); + if (rotting) findings.push([issue, 'H30', rotting]); // H4 — judged across BOTH channels. The fetch is gated by // `needsBlockedByComments`, so it costs a request only for the body-clean // cards whose verdict it can actually change (~2/3 of the blocked @@ -5850,6 +8287,17 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen if (needsBlockedByComments(issue)) await gatherBlockedByComments(issue); const unblockedByNothing = h4BlockedNoBlockedBy(issue, fallbackFor(issue)); if (unblockedByNothing) findings.push([issue, 'H4', unblockedByNothing]); + // …and the LIVENESS read's own gathering, UNGATED (#11747). H19/H26/H28 ask + // whether what the line names is still RUNNING, and for that a body line is + // one channel's targets rather than an answer — so a card whose body line is + // spent and whose live blocker sits in a comment must contribute both. It + // rides the same cache as the H4 fetch above (a card gathered there is a + // no-op here), so the union of the two gates still costs at most one request + // per card; the delta is the gate's complement — the blocked cards that DO + // carry a body line. Gathered here rather than in the H19 loop below on + // purpose: the total-shortfall rethrow reads these stats, so every fetch this + // sweep makes must be counted before that check runs. + if (needsBlockerLivenessComments(issue)) await gatherBlockedByComments(issue); // H9 — judged across BOTH channels since #10403, on the same gated-fetch // trade as H4: a hold whose body already carries a fireable line is // answered without the network; a body-clean one buys (at most) the one @@ -5868,6 +8316,25 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen const kb = (Buffer.byteLength(issue.body ?? '', 'utf8') / 1024).toFixed(1); findings.push([issue, 'H6', `seat body is ${kb} KB (soft bound ~10 KB) — compact to the six-section current-state template (#7583; edit history is the archive)`]); } + // H32 (#11706) — gathered here, judged after the lane inventory exists. + // The seat population has never bought a comment fetch (the H2 branch + // below skips `pm:seat` explicitly), so this is a NEW cost and it is + // gated hard: HELD seats on a lane this board can count, 6 of 12 at the + // 2026-08-25 census. An unreadable thread leaves the seat unjudged — the + // predicate declines rather than accusing — and the summary pair says so. + if (h32NeedsSeatComments(issue)) { + stats.seatCandidates = (stats.seatCandidates ?? 0) + 1; + try { + const marker = latestSeatMarker(await commentRowsFor(issue)); + stats.seatMarkersRead = (stats.seatMarkersRead ?? 0) + 1; + seatMarkers.set(issue.number, { issue, marker }); + } catch { + // Left out of `seatMarkers` entirely: the predicate's `undefined` + // and `null` both decline, and the coverage pair is what states the + // gap. No rethrow — a seat this sweep could not read is one seat + // unexamined, not a report worth discarding. + } + } } else if ((issue.assignees ?? []).length > 0 && labels.some((l) => l === 'pm:queue' || l === 'pm:dispatched')) { // H2 needs the comment thread — fetched only for candidates (exactly the // pm-tracked set h2 judges; the on-hold/p0 listings above must not buy @@ -5879,6 +8346,13 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen if (h2AssigneeNoClaimComment(issue, comments)) { findings.push([issue, 'H2', 'assignee set but no claim comment on the thread']); } + // H34 (#12090) — the same bodies, asked WHY H2 is firing. Free by + // construction: it reads the page already in hand, on exactly H2's + // population, and fires only on cards H2 is already reporting. Judged + // here rather than in the dispatched loop below so a `pm:queue` claim + // written with a dash is covered too, and so the two rows land together. + const nearMiss = h34ClaimShapedNonCanonicalSeparator(issue, comments); + if (nearMiss) findings.push([issue, 'H34', nearMiss]); } // H17 — the trigger-file index. Gathering only: the card's own body plus @@ -5912,6 +8386,38 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen throw lastHoldError; } + // H32 (#11706) — the lane inventory, then the verdicts. Counted off the label + // pages this sweep already holds, so the whole row costs nothing beyond the + // gated seat-marker fetches above. + // + // "Unclaimed" is `pm:queue` with NO assignee, which is the queue view's own + // dispatchable population — an assigned `pm:queue` card is H24's + // contradiction and is deliberately NOT counted as work this seat could take + // (the claim protocol reads it as taken). Counting it would let one + // half-state manufacture another. + const laneInventory = new Map(); + const laneBucket = (lane) => { + if (!laneInventory.has(lane)) laneInventory.set(lane, { unclaimed: 0, inFlight: 0 }); + return laneInventory.get(lane); + }; + for (const issue of seen.values()) { + const labels = labelNames(issue); + const lanes = labels.filter((l) => /^domain:/u.test(l)); + if (lanes.length === 0) continue; + const unclaimed = labels.includes('pm:queue') && (issue.assignees ?? []).length === 0; + const inFlight = labels.includes('pm:dispatched'); + for (const lane of lanes) { + const bucket = laneBucket(lane); + if (unclaimed) bucket.unclaimed += 1; + if (inFlight) bucket.inFlight += 1; + } + } + for (const { issue, marker } of seatMarkers.values()) { + const { lane } = seatLane(issue); + const idle = h32SeatIdleOverQueue(issue, marker, laneInventory.get(lane) ?? { unclaimed: 0, inFlight: 0 }); + if (idle) findings.push([issue, 'H32', idle]); + } + // H7 + H12 + H21 — the PR side. Listed straight from `/pulls` rather than // filtered out of the label pages above: PRs carry no `pm:*` label, so the // issue sweep cannot see them (it discards them explicitly). Drafts are @@ -5965,6 +8471,36 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen throw lastDetailError; } + // H36 (#12286) — the shared-file pass, H16's transport posture over one + // changed-file page per open PR (the population this sweep already listed; + // gathering policy at `h36NeedsFiles`). A failed page drops that PR out of + // the pairing — a MISS, never an invention — and the summary's `read X of Y` + // says so; all-failed is the transport, not a clean board (#4690). + let lastFilesError = null; + const filesByPr = new Map(); + for (const pr of seenPrs.values()) { + if (!h36NeedsFiles(pr)) continue; + stats.sharedFileCandidates = (stats.sharedFileCandidates ?? 0) + 1; + let page; + try { + page = await rest(`/repos/${OWNER_REPO}/pulls/${pr.number}/files?per_page=${H36_FILES_PAGE_SIZE}`); + } catch (err) { + lastFilesError = err; + continue; + } + stats.sharedFileProbed = (stats.sharedFileProbed ?? 0) + 1; + filesByPr.set(pr.number, { + paths: (Array.isArray(page) ? page : []).map((f) => String(f?.filename ?? '')), + truncated: Array.isArray(page) && page.length >= H36_FILES_PAGE_SIZE, + }); + } + if (h36DetailPassUnreadable(stats.sharedFileCandidates, stats.sharedFileProbed)) { + throw lastFilesError; + } + for (const [pr, hold36] of h36SharedFileHolds([...seenPrs.values()], filesByPr)) { + findings.push([pr, 'H36', hold36]); + } + // H8 — one bounded merged-PR listing (window note at the helper), matched // against the already-collected open `pm:dispatched` cards; no per-card fetch. // @@ -6037,6 +8573,48 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen // all, so only the unscoped listing is guaranteed to see every carrier. const retriageAged = h18RetriageAged(issue); if (retriageAged) findings.push([issue, 'H18', retriageAged]); + // H31 — the contract-review gate's two carriers, compared. This listing + // rather than the label loop for the same reason H18 is here: + // `needs:contract-review` is not one of the labels that loop pages, so a + // gated card carrying no `pm:*` state at all is first visible HERE. The + // open-PR side is `openWindow`, already assembled above for H8 — no + // request, and the same `prDeliversCard` relation, so the two rows can + // never disagree about which PR delivers which card. + const gateSplit = h31ContractReviewCarrierSplit(issue, openWindow); + if (gateSplit) findings.push([issue, 'H31', gateSplit]); + } + + // H35 (#11881) — the EVENT behind the state H31 compares. One repo-wide + // window, no per-card fetch; the sibling resolver below is `prDeliversCard` + // over the two PR windows this sweep already holds, so H8, H31 and H35 read + // ONE delivery relation and can never disagree about which PR delivers which + // card. An unresolvable sibling returns null and the removal is judged + // against its own hang instead — never straight to a finding. + const prWindow = [...mergedWindow, ...openWindow]; + const siblingNumbers = (event) => { + const number = Number(event?.issue?.number); + if (!Number.isFinite(number)) return null; + if (event?.issue?.pull_request) { + const pr = prWindow.find((p) => Number(p?.number) === number); + if (!pr) return null; + const cards = [...seenUnscoped.keys(), ...seen.keys()].filter((n) => + prDeliversCard(pr, String(n)), + ); + return cards.length > 0 ? cards : null; + } + const prs = prWindow.filter((p) => prDeliversCard(p, String(number))).map((p) => p.number); + return prs.length > 0 ? prs : null; + }; + const eventWindow = await listRecentIssueEvents(stats); + const gateEvents = gateLabelEvents(eventWindow); + stats.gateLabelEvents = gateEvents.length; + const removals = gateEvents.filter((e) => e.event === 'unlabeled'); + stats.gateRemovals = removals.length; + for (const removal of removals) { + const verdict = h35RemovalVerdict(removal, gateEvents, { siblingNumbers }); + stats[`gate_${verdict.replace(/-/g, '_')}`] = (stats[`gate_${verdict.replace(/-/g, '_')}`] ?? 0) + 1; + const row = h35GateRemovalWithoutEvidence(removal, gateEvents, { siblingNumbers }); + if (row) findings.push([removal.issue, 'H35', row]); } // H14 + H15 — the same unscoped listing, read a second way. It is the right @@ -6142,7 +8720,16 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen for (const issue of seen.values()) { if (!labelNames(issue).includes('pm:dispatched')) continue; - const claim = governingClaim(await commentRowsFor(issue)); + const commentRows = await commentRowsFor(issue); + // H33 (#11724) — the same thread this loop already holds, asked a + // different question: not "is the claimed branch alive" but "was the order + // this card is in flight under written before the ruling that now stands". + // Judged for EVERY `pm:dispatched` card, ahead of H20's age gate, because + // the ordering it reads has nothing to do with how old the claim is: a + // claim posted five minutes ago can already be behind a ruling posted four. + const blindClaim = h33ClaimPredatesRuling(issue, commentRows); + if (blindClaim) findings.push([issue, 'H33', blindClaim]); + const claim = governingClaim(commentRows); if (!h20NeedsRefProbe(issue, claim)) continue; const states = []; for (const branch of claim.branches) states.push({ branch, ...(await resolveBranchRef(branch)) }); @@ -6169,6 +8756,54 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen stats.dispatchRefTargets = refCache.size; stats.dispatchRefRead = [...refCache.values()].filter((r) => r.state !== 'unreadable').length; + // H37 (#12629) — the family-dispatch join, in two rounds (banner at the + // predicate). Placed HERE, after the dispatch-liveness loop, because round 1 + // is exactly that loop's leftovers: `commentCache` now holds a thread for + // every open `pm:dispatched` card and every assigned pm-tracked card, so the + // fold roster costs no request at all. + const claimsOf = (rows, number) => { + const claim = governingClaim(rows); + return claim && (claim.branches ?? []).length > 0 ? { number, branches: claim.branches } : null; + }; + const h37Claims = []; + for (const [number, rows] of commentCache) { + const entry = claimsOf(rows, number); + if (entry) h37Claims.push(entry); + } + // Round 2 — gated on round 1 having actually SEEN a fold (`h37NeedsMemberRead`), + // so a board with none costs nothing. `commentRowsFor` memoises, so a candidate + // already read above is counted as read and costs no second request; the pair + // therefore describes the POPULATION rather than only the new fetches. + const foldsSeen = h37FoldBranches(h37Claims).size; + for (const issue of seen.values()) { + if (!h37NeedsMemberRead(issue, foldsSeen)) continue; + stats.memberReadCandidates = (stats.memberReadCandidates ?? 0) + 1; + let rows; + try { + rows = await commentRowsFor(issue); + } catch { + // One member unread is one card out of the roster, never a report worth + // discarding — H32's posture, and the banner says why this pass does not + // rethrow the way H16's and H36's do. The coverage pair states the gap and + // the summary names an all-failed pass as the transport it is. + continue; + } + stats.memberReadProbed = (stats.memberReadProbed ?? 0) + 1; + const entry = claimsOf(rows, issue.number); + if (entry && !h37Claims.some((c) => c.number === entry.number)) h37Claims.push(entry); + } + // The roster is rebuilt over the UNION: a member found in round 2 can complete + // a fold round 1 saw only one side of, and judging the round-1 roster would + // discard exactly the card the second read was bought for. + const h37OpenByNumber = new Map(); + for (const [number, issue] of seenUnscoped) h37OpenByNumber.set(number, issue); + for (const [number, issue] of seen) h37OpenByNumber.set(number, issue); + const h37Folds = h37FoldBranches(h37Claims); + stats.liveFolds = h37Folds.size; + for (const [card, drift] of h37FamilyMemberDrift(h37Folds, h37OpenByNumber, seenClosed)) { + findings.push([card, 'H37', drift]); + } + // H19 — blocker liveness. Last, because it is the only pass that reads a // card this sweep did not list: every other item answers from a listing // already in hand, while "is the target still open" is a fact about an @@ -6199,6 +8834,31 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen for (const [number, issue] of seenUnscoped) openLocalIssues.set(number, issue); for (const [number, issue] of seen) openLocalIssues.set(number, issue); const blockerCache = new Map(); + + // The disambiguating second reading (#11218). A target 404 is ambiguous — + // unreachable repo, or a number that is not there — and `GET /repos//` + // separates them, independently of any issue number. One request per distinct + // SIBLING repo per sweep (2 on this board), taken LAZILY: it is bought only + // when a cross-repo target actually failed, so a sweep whose cross-repo + // targets all resolve pays nothing at all. + // + // `true` readable · `false` not readable · `null` the probe itself failed for + // some other reason, which must not be reported as either (#4690) — the row + // falls back to the undiagnosed wording rather than picking a side. + const repoReadCache = new Map(); + const probeRepoReadable = async (repo) => { + if (repoReadCache.has(repo)) return repoReadCache.get(repo); + let verdict; + try { + await rest(`/repos/${repo}`); + verdict = true; + } catch (err) { + verdict = err?.status === 404 || err?.status === 403 || err?.status === 401 ? false : null; + } + repoReadCache.set(repo, verdict); + return verdict; + }; + const resolveBlockerTarget = async (target) => { const cached = blockerCache.get(target.key); if (cached) return cached; @@ -6231,6 +8891,12 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen state: 'unresolved', closedAt: null, detail: err?.status ? `HTTP ${err.status}` : 'unreadable', + // LOCAL targets get no probe: the swept repo's readability is already + // settled by the pre-sweep transport prerequisite, and a local 404 is + // unambiguous (the number is not on this board). `undefined` — not + // `null` — so the renderer can tell "not asked" from "asked, and the + // asking failed". + repoReadable: target.local ? undefined : await probeRepoReadable(target.repo), }; } } @@ -6253,12 +8919,24 @@ async function sweepInto(findings, seen, seenPrs, seenMerged, seenUnscoped, seen // different reads. const indefinite = h26BlockOnIndefiniteTarget(issue, resolutions); if (indefinite) findings.push([issue, 'H26', indefinite]); + // H28 — the same resolutions, asked a THIRD question: which CHANNEL each + // target arrived in. H19 reports that the block is half-expired; this + // reports that the expired half is the one sitting in the canonical home, + // which is the write to fix. Both fire on one card, deliberately. + const staleBody = h28StaleBodyBlockerLine(issue, resolutions, fallbackFor(issue)); + if (staleBody) findings.push([issue, 'H28', staleBody]); } // Distinct targets, which is the unit the cache and the request count are // in — and the word is in the summary sentence so the number cannot be read // as a per-card edge count. stats.blockerTargets = blockerCache.size; stats.blockerResolved = [...blockerCache.values()].filter((r) => r.state !== 'unresolved').length; + // The cross-repo reachability readings (#11218) — distinct SIBLING repos + // probed, and how many refused this credential. `false` only: a `null` probe + // failed for some other reason and is not evidence of a scope gap, so it is + // counted as probed and not as unreadable. + stats.crossRepoProbed = repoReadCache.size; + stats.crossRepoUnreadable = [...repoReadCache.values()].filter((v) => v === false).length; } // --------------------------------------------------------------------------- @@ -6275,6 +8953,55 @@ function selfTest() { title, }); + // -- Row-text wrappers: every message assertion goes through one ----------- + // + // Each `hNrow(...)` is `String(hN…(...) ?? '')`. The predicates here are + // three-valued BY DESIGN — `null` when the card is clean, a string when the + // row fires — so a bare `predicate(...).includes(needle)` throws + // `TypeError: Cannot read properties of null (reading 'includes')` the + // moment a change makes that predicate go clean. That throw happens while + // evaluating `t()`'s ARGUMENTS, before `t()` runs, so no harness-level + // catch can convert it into a case: the suite ABORTS at the first such + // line, every later case never runs, and the output names a TypeError + // instead of a row. Which line you land on depends on ordering, so the + // information you lose is arbitrary. + // + // The cost is paid exactly during ABLATION — mutate a predicate, read which + // cases go red — which is the discipline this file's own headers lean on to + // prove a new row is failable. Through a wrapper, a nulled row instead + // reports `(got false, want true)` under its own case name and the suite + // runs to completion. ⚠️ The predicates themselves are UNCHANGED and are + // still asserted three-valued directly: `typeof pred(...) === 'string'` and + // `pred(...) === null` sites deliberately do NOT go through a wrapper — + // wrapping those would make every one of them trivially true. + const h4row = (...args) => String(h4BlockedNoBlockedBy(...args) ?? ''); + const h7row = (...args) => String(h7PartOfWithClosingKeyword(...args) ?? ''); + const h8row = (...args) => String(h8MergedPrStillDispatched(...args) ?? ''); + const h9row = (...args) => String(h9OnHoldNoRestartWhen(...args) ?? ''); + const h10row = (...args) => String(h10StaleUnclaimedP0(...args) ?? ''); + const h11row = (...args) => String(h11ImportantParked(...args) ?? ''); + const h12row = (...args) => String(h12OrphanLanding(...args) ?? ''); + const h13row = (...args) => String(h13DomainWithoutPmState(...args) ?? ''); + const h14row = (...args) => String(h14BlockingCacheIncoherent(...args) ?? ''); + const h16row = (...args) => String(h16StuckMergeConflict(...args) ?? ''); + const h18row = (...args) => String(h18RetriageAged(...args) ?? ''); + const h19row = (...args) => String(h19BlockOutlivedBlocker(...args) ?? ''); + const h22row = (...args) => String(h22ClosedCardPmResidue(...args) ?? ''); + const h24row = (...args) => String(h24QueuedWithAssignee(...args) ?? ''); + const h25row = (...args) => String(h25AwaitingMaintainerExclusivity(...args) ?? ''); + const h26row = (...args) => String(h26BlockOnIndefiniteTarget(...args) ?? ''); + const h32row = (...args) => String(h32SeatIdleOverQueue(...args) ?? ''); + const h33row = (...args) => String(h33ClaimPredatesRuling(...args) ?? ''); + // H34's wrapper (the pattern this generalizes) stays beside its own block, + // as do H8's `halvesRow` and H27's `dead27Row` — all three wrap a helper + // that is itself declared locally, next to the fixtures it closes over. + // H29/H30/H31 use a different reader, `says()`, declared at those blocks: + // it returns a DESCRIBING string (`NO MESSAGE (null)`) rather than `''`, so + // it also distinguishes "row fired without the needle" from "row went + // silent" on a case whose expectation is `false`. Both shapes run to + // completion; `says()` is the more informative and the more invasive, and + // unifying on one of them is a diff of its own, not this one. + t('H1: dispatched + no assignee -> finding', h1DispatchedNoAssignee(issue(['pm:dispatched'])), true); t('H1: dispatched + assignee -> clean', h1DispatchedNoAssignee(issue(['pm:dispatched'], ['os-help'])), false); t('H2: assignee + no claim comment -> finding', h2AssigneeNoClaimComment(issue(['pm:dispatched'], ['os-help']), ['looks good', 'triage: routed']), true); @@ -6296,23 +9023,23 @@ function selfTest() { t('H4: blocked without body line -> finding', typeof h4BlockedNoBlockedBy(issue(['pm:blocked'], [], 'waiting on upstream')), 'string'); t('H4: blocked with Blocked-by line -> clean', h4BlockedNoBlockedBy(issue(['pm:blocked'], [], 'Blocked-by: #123')), null); t('H4: unblocked card is out of scope', h4BlockedNoBlockedBy(issue([], [], '')), null); - t('H4: …and an unconsulted comment channel is not claimed as empty', h4BlockedNoBlockedBy(issue(['pm:blocked'], [], 'waiting on upstream')).includes('NEITHER channel'), false); - t('H4: the body-only sentence still names the unlock sweep as the stake', h4BlockedNoBlockedBy(issue(['pm:blocked'], [], 'waiting on upstream')).includes('unlock sweep greps'), true); + t('H4: …and an unconsulted comment channel is not claimed as empty', h4row(issue(['pm:blocked'], [], 'waiting on upstream')).includes('NEITHER channel'), false); + t('H4: the body-only sentence still names the unlock sweep as the stake', h4row(issue(['pm:blocked'], [], 'waiting on upstream')).includes('unlock sweep greps'), true); // H4 — the COMMENT channel (#8941 / #10061). Four shapes, positive and // negative, plus the unreadable one that is neither. t('H4: body clean but a comment carries the line -> clean', h4BlockedNoBlockedBy(issue(['pm:blocked'], [], 'waiting on upstream'), ['triage note', 'Blocked-by: #9465']), null); t('H4: body line AND a comment line (the union shape) -> clean', h4BlockedNoBlockedBy(issue(['pm:blocked'], [], 'Blocked-by: #123'), ['Blocked-by: #9465']), null); t('H4: neither channel -> finding', typeof h4BlockedNoBlockedBy(issue(['pm:blocked'], [], 'waiting on upstream'), ['triage note', 'graded p2']), 'string'); - t('H4: …and the sentence names BOTH channels', h4BlockedNoBlockedBy(issue(['pm:blocked'], [], 'waiting'), ['nothing here']).includes('NEITHER channel'), true); - t('H4: …and says a comment discharges the duty too', h4BlockedNoBlockedBy(issue(['pm:blocked'], [], 'waiting'), ['nothing here']).includes('Either channel discharges'), true); - t('H4: an empty comment thread is a real reading, not an unconsulted one', h4BlockedNoBlockedBy(issue(['pm:blocked'], [], 'waiting'), []).includes('NEITHER channel'), true); + t('H4: …and the sentence names BOTH channels', h4row(issue(['pm:blocked'], [], 'waiting'), ['nothing here']).includes('NEITHER channel'), true); + t('H4: …and says a comment discharges the duty too', h4row(issue(['pm:blocked'], [], 'waiting'), ['nothing here']).includes('Either channel discharges'), true); + t('H4: an empty comment thread is a real reading, not an unconsulted one', h4row(issue(['pm:blocked'], [], 'waiting'), []).includes('NEITHER channel'), true); // Unreadable is neither of the two: the row FIRES (a transport failure must // not shrink the patrol below its pre-fallback reach) and says why. t('H4: an UNREADABLE comment thread still fires', typeof h4BlockedNoBlockedBy(issue(['pm:blocked'], [], 'waiting'), null), 'string'); - t('H4: …but never claims the second channel is empty', h4BlockedNoBlockedBy(issue(['pm:blocked'], [], 'waiting'), null).includes('NEITHER channel'), false); - t('H4: …and says the thread could not be read', h4BlockedNoBlockedBy(issue(['pm:blocked'], [], 'waiting'), null).includes('could'), true); - t('H4: …citing the unreadable-is-not-absent rule', h4BlockedNoBlockedBy(issue(['pm:blocked'], [], 'waiting'), null).includes('#4690'), true); + t('H4: …but never claims the second channel is empty', h4row(issue(['pm:blocked'], [], 'waiting'), null).includes('NEITHER channel'), false); + t('H4: …and says the thread could not be read', h4row(issue(['pm:blocked'], [], 'waiting'), null).includes('could'), true); + t('H4: …citing the unreadable-is-not-absent rule', h4row(issue(['pm:blocked'], [], 'waiting'), null).includes('#4690'), true); // A comment line clears H4 whatever the ref says: H4's question is "did the // author leave the machine anything", which a cross-repo blocker answers. t('H4: a cross-repo comment line still discharges the duty', h4BlockedNoBlockedBy(issue(['pm:blocked'], [], 'waiting'), ['Blocked-by: objectstack-ai/objectui#4356']), null); @@ -6502,11 +9229,11 @@ function selfTest() { 'the PM should close #8131 deliberately once #8136 lands.', ); t('H7: the #8277 specimen is a finding', typeof h7PartOfWithClosingKeyword(pr8277), 'string'); - t('H7: …and it names the card it will close', h7PartOfWithClosingKeyword(pr8277).includes('Part of #8131'), true); + t('H7: …and it names the card it will close', h7row(pr8277).includes('Part of #8131'), true); // The measurement that refutes the sidebar hypothesis: the SAME body names // #8136 one clause later with no keyword, and #8136 took no closing link. // The predicate must reproduce that asymmetry, not blanket-flag both numbers. - t('H7: …and does NOT implicate #8136 from the same sentence', h7PartOfWithClosingKeyword(pr8277).includes('#8136'), false); + t('H7: …and does NOT implicate #8136 from the same sentence', h7row(pr8277).includes('#8136'), false); // Specimen 2 — PR #8261 (`Part of #8103`), the same round's other partial // delivery, which stayed open. No keyword anywhere near its number. @@ -6617,11 +9344,12 @@ function selfTest() { 'measurement above shows is lost on the predicate path.', ); const fired21 = h21NegatedClosingKeyword(pr10241); + const fired21Row = String(fired21 ?? ''); t('H21: the #10241 specimen FIRES', typeof fired21, 'string'); - t('H21: …and names the card it will close', fired21.includes('`fixed #10240`'), true); - t('H21: …and quotes the offending sentence back', fired21.includes('Filed, not fixed: #10240'), true); - t('H21: …and says the parser ignores the negation', fired21.includes('negations included'), true); - t('H21: …and offers the safe rewordings', fired21.includes('#10240 is not addressed here'), true); + t('H21: …and names the card it will close', fired21Row.includes('`fixed #10240`'), true); + t('H21: …and quotes the offending sentence back', fired21Row.includes('Filed, not fixed: #10240'), true); + t('H21: …and says the parser ignores the negation', fired21Row.includes('negations included'), true); + t('H21: …and offers the safe rewordings', fired21Row.includes('#10240 is not addressed here'), true); // H7 is silent on this body — the gap that made the row necessary. If this // ever inverts, H21 is redundant rather than merely quiet. t('H21: …and H7 is silent on it (the gap this row exists for)', h7PartOfWithClosingKeyword(pr10241), null); @@ -6722,12 +9450,12 @@ function selfTest() { ); t( 'H8: …and the finding names the delivering PR', - h8MergedPrStillDispatched(dispatched(4321), [mergedPr(4400, 'Part of #4321')]).includes('#4400'), + h8row(dispatched(4321), [mergedPr(4400, 'Part of #4321')]).includes('#4400'), true, ); t( 'H8: …and prescribes the paired write, not just the fact', - h8MergedPrStillDispatched(dispatched(4321), [mergedPr(4400, 'Part of #4321')]).includes('pm:dispatched'), + h8row(dispatched(4321), [mergedPr(4400, 'Part of #4321')]).includes('pm:dispatched'), true, ); // The closing-keyword arm: an OPEN dispatched card named by a merged PR's @@ -6771,7 +9499,7 @@ function selfTest() { ); t( 'H8: two merged deliverers -> both named', - h8MergedPrStillDispatched(dispatched(4321), [mergedPr(4400, 'Part of #4321'), mergedPr(4500, 'Fixes #4321')]).includes('#4500'), + h8row(dispatched(4321), [mergedPr(4400, 'Part of #4321'), mergedPr(4500, 'Fixes #4321')]).includes('#4500'), true, ); t('H8: empty merged window -> clean', h8MergedPrStillDispatched(dispatched(4321), []), null); @@ -6875,38 +9603,43 @@ function selfTest() { const openHalf = (number, body, draft = false) => ({ number, body, draft, merged_at: null }); const halves = (openPrs) => h8MergedPrStillDispatched(dispatched(9834), [mergedPr(10004, 'Part of #9834')], openPrs); + // `halves` is itself three-valued, so it needs the same row-text wrapper as + // the predicate it closes over — and it must stay a SEPARATE binding rather + // than `halves` being stringified in place, because the `typeof halves(...)` + // cases below assert exactly the nullability a `String()` would erase. + const halvesRow = (...args) => String(halves(...args) ?? ''); t('H8 open: a half-delivered card still reports', typeof halves([openHalf(10226, 'Part of #9834', true)]), 'string'); // The whole point of the downgrade: the destructive prescription must not // fire on a card whose remaining half is open. t( 'H8 open: …and does NOT prescribe dropping the label', - halves([openHalf(10226, 'Part of #9834', true)]).includes('Drop `pm:dispatched`'), + halvesRow([openHalf(10226, 'Part of #9834', true)]).includes('Drop `pm:dispatched`'), false, ); t( 'H8 open: …and says the label is CORRECT here', - halves([openHalf(10226, 'Part of #9834', true)]).includes('must NOT be dropped'), + halvesRow([openHalf(10226, 'Part of #9834', true)]).includes('must NOT be dropped'), true, ); - t('H8 open: …and names the open half', halves([openHalf(10226, 'Part of #9834', true)]).includes('#10226'), true); - t('H8 open: …and the merged half too', halves([openHalf(10226, 'Part of #9834', true)]).includes('#10004'), true); - t('H8 open: …and counts them, N of M', halves([openHalf(10226, 'Part of #9834', true)]).includes('1 of 2'), true); + t('H8 open: …and names the open half', halvesRow([openHalf(10226, 'Part of #9834', true)]).includes('#10226'), true); + t('H8 open: …and the merged half too', halvesRow([openHalf(10226, 'Part of #9834', true)]).includes('#10004'), true); + t('H8 open: …and counts them, N of M', halvesRow([openHalf(10226, 'Part of #9834', true)]).includes('1 of 2'), true); // A draft open half is the specimen's own shape — never filtered out. - t('H8 open: …and marks the open half as a draft', halves([openHalf(10226, 'Part of #9834', true)]).includes('(draft)'), true); + t('H8 open: …and marks the open half as a draft', halvesRow([openHalf(10226, 'Part of #9834', true)]).includes('(draft)'), true); t('H8 open: a NON-draft open half counts identically', typeof halves([openHalf(10226, 'Part of #9834', false)]), 'string'); // …and the row it replaces is unchanged whenever every deliverer HAS merged — // the genuine #8683 case, which must keep its prescription. t( 'H8 open: no open deliverer -> the destructive prescription still fires', - halves([]).includes('Drop `pm:dispatched`'), + halvesRow([]).includes('Drop `pm:dispatched`'), true, ); - t('H8 open: a missing open list is the pre-#10468 reading', halves(undefined).includes('Drop `pm:dispatched`'), true); + t('H8 open: a missing open list is the pre-#10468 reading', halvesRow(undefined).includes('Drop `pm:dispatched`'), true); t( 'H8 open: an open PR delivering a DIFFERENT card does not downgrade the row', - halves([openHalf(10226, 'Part of #9999')]).includes('Drop `pm:dispatched`'), + halvesRow([openHalf(10226, 'Part of #9999')]).includes('Drop `pm:dispatched`'), true, ); // No merged deliverer at all is still clean — the open side never MANUFACTURES @@ -6920,19 +9653,19 @@ function selfTest() { // included — a `Refs #N` open half is as live as a `Part of #N` one. t( 'H8 open: the branch-name fallback applies to the open side too', - halves([{ number: 10226, body: 'Refs #9834', draft: false, merged_at: null, head: { ref: 'claude/issue-9834-error-counter' } }]).includes('must NOT be dropped'), + halvesRow([{ number: 10226, body: 'Refs #9834', draft: false, merged_at: null, head: { ref: 'claude/issue-9834-error-counter' } }]).includes('must NOT be dropped'), true, ); // …and its re-scope guard travels with it. t( 'H8 open: a re-scoped open branch does not soften the row', - halves([{ number: 10226, body: 'Part of #9999', draft: false, merged_at: null, head: { ref: 'claude/issue-9834-x' } }]).includes('Drop `pm:dispatched`'), + halvesRow([{ number: 10226, body: 'Part of #9999', draft: false, merged_at: null, head: { ref: 'claude/issue-9834-x' } }]).includes('Drop `pm:dispatched`'), true, ); // A merged row appearing in the open list is not an outstanding half. t( 'H8 open: a merged row in the open list is not an open half', - halves([{ number: 10226, body: 'Part of #9834', merged_at: '2026-08-20T00:00:00Z' }]).includes('Drop `pm:dispatched`'), + halvesRow([{ number: 10226, body: 'Part of #9834', merged_at: '2026-08-20T00:00:00Z' }]).includes('Drop `pm:dispatched`'), true, ); @@ -6945,21 +9678,21 @@ function selfTest() { }); t('H22: closed + pm:dispatched -> finding', typeof h22ClosedCardPmResidue(closedCard(['pm:dispatched'])), 'string'); - t('H22: …and names the residue label', h22ClosedCardPmResidue(closedCard(['pm:dispatched'])).includes('`pm:dispatched`'), true); - t('H22: …and names the close reason', h22ClosedCardPmResidue(closedCard(['pm:dispatched'])).includes('closed `completed`'), true); + t('H22: …and names the residue label', h22row(closedCard(['pm:dispatched'])).includes('`pm:dispatched`'), true); + t('H22: …and names the close reason', h22row(closedCard(['pm:dispatched'])).includes('closed `completed`'), true); t( 'H22: …and prescribes only the label strip, no other write', - h22ClosedCardPmResidue(closedCard(['pm:dispatched'])).includes('already closed'), + h22row(closedCard(['pm:dispatched'])).includes('already closed'), true, ); t('H22: a not_planned close is residue too', typeof h22ClosedCardPmResidue(closedCard(['pm:queue'], 'not_planned')), 'string'); t('H22: a missing state_reason still renders a sentence', typeof h22ClosedCardPmResidue({ ...closedCard(['pm:queue']), state_reason: null }), 'string'); t( 'H22: …and never prints the string undefined', - h22ClosedCardPmResidue({ ...closedCard(['pm:queue']), state_reason: null }).includes('undefined'), + h22row({ ...closedCard(['pm:queue']), state_reason: null }).includes('undefined'), false, ); - t('H22: several residue labels are all named', h22ClosedCardPmResidue(closedCard(['pm:blocked', 'pm:blocking'])).includes('`pm:blocking`'), true); + t('H22: several residue labels are all named', h22row(closedCard(['pm:blocked', 'pm:blocking'])).includes('`pm:blocking`'), true); // The gate that keeps this from restating H3: an OPEN card is never this // row's, whatever it carries — every other item here already reads it. @@ -6975,8 +9708,8 @@ function selfTest() { t('H22: `pm:epic` likewise', h22ClosedCardPmResidue(closedCard(['pm:epic'])), null); t('H22: `pm:retriage` is deliberately out of the measured set', h22ClosedCardPmResidue(closedCard(['pm:retriage'])), null); // …but a seat card ALSO carrying a state label is still residue. - t('H22: `pm:seat` + a state label is residue for the state label', h22ClosedCardPmResidue(closedCard(['pm:seat', 'pm:dispatched'])).includes('`pm:dispatched`'), true); - t('H22: …and does not name the identity sticker', h22ClosedCardPmResidue(closedCard(['pm:seat', 'pm:dispatched'])).includes('`pm:seat`'), false); + t('H22: `pm:seat` + a state label is residue for the state label', h22row(closedCard(['pm:seat', 'pm:dispatched'])).includes('`pm:dispatched`'), true); + t('H22: …and does not name the identity sticker', h22row(closedCard(['pm:seat', 'pm:dispatched'])).includes('`pm:seat`'), false); // The census's five plus the state ruled in on 2026-08-23, each pinned — the // set is the item's scope, so a silent edit to it should break a test rather @@ -6998,9 +9731,9 @@ function selfTest() { // -- H22's DATED CLOSURE FLOOR (objectui#5985) ------------------------------ // - // The floor is what lets THIS install re-enable the row without the backfill - // its own card thought was the only alternative: judge cards closed on/after - // the cutover date, leave the ~815 historical carriers unjudged, write no + // The floor is what lets a sibling install re-enable this row without the + // backfill its own card thought was the only alternative: judge cards closed + // on/after a cutover date, leave the historical carriers unjudged, write no // labels at all. The cases below pin the three properties that decision rests // on — the floor is HONOURED, its absence changes nothing, and a malformed // value is refused rather than silently becoming "no floor". @@ -7012,25 +9745,26 @@ function selfTest() { t('H22 floor: a card closed BEFORE the floor is out of scope', h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-08-01T09:00:00Z'), FLOOR), null); t('H22 floor: …however much residue it carries', h22ClosedCardPmResidue(closedOn(['pm:dispatched', 'pm:queue', 'pm:blocked'], '2026-01-01T00:00:00Z'), FLOOR), null); t('H22 floor: a card closed AFTER the floor is judged', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-08-29T09:00:00Z'), FLOOR), 'string'); - t('H22 floor: …and the row still names the residue label', h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-08-29T09:00:00Z'), FLOOR).includes('`pm:dispatched`'), true); + t('H22 floor: …and the row still names the residue label', h22row(closedOn(['pm:dispatched'], '2026-08-29T09:00:00Z'), FLOOR).includes('`pm:dispatched`'), true); // The boundary is inclusive: the cutover date is the first day the convention // applies, so a card closed within it is the convention's own population. t('H22 floor: a card closed ON the floor date is judged', typeof h22ClosedCardPmResidue(closedOn(['pm:queue'], '2026-08-28T00:00:00Z'), FLOOR), 'string'); t('H22 floor: …and later the same day too', typeof h22ClosedCardPmResidue(closedOn(['pm:queue'], '2026-08-28T23:59:59Z'), FLOOR), 'string'); t('H22 floor: one second before the floor is out', h22ClosedCardPmResidue(closedOn(['pm:queue'], '2026-08-27T23:59:59Z'), FLOOR), null); - // The floor narrows scope; it never invents findings. - t('H22 floor: a clean card after the floor is still clean', h22ClosedCardPmResidue(closedOn(['domain:ui'], '2026-08-29T09:00:00Z'), FLOOR), null); + // The floor narrows scope; it never invents findings. A clean recent card is + // still clean, and an OPEN card is still not this row's. + t('H22 floor: a clean card after the floor is still clean', h22ClosedCardPmResidue(closedOn(['domain:cli'], '2026-08-29T09:00:00Z'), FLOOR), null); t('H22 floor: the closed gate still outranks the floor', h22ClosedCardPmResidue({ ...issue(['pm:dispatched']), state: 'open', closed_at: null }, FLOOR), null); // Fail-OPEN on an unreadable closure date: the floor cannot be applied, so // the card stays visible rather than being dropped on unread data (#4690). t('H22 floor: a card with no closed_at is judged, not dropped', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], null), FLOOR), 'string'); t('H22 floor: …and an unparseable one likewise', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], 'not-a-date'), FLOOR), 'string'); - // Floor ABSENT — upstream's default, and the property that makes the ported - // code a no-op for an install that does not set the variable. + // Floor ABSENT — the default, and the property that makes this change a + // no-op for the install that wants every card in the window judged. t('H22 floor: absent floor judges an old closed card exactly as before', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-01-01T00:00:00Z')), 'string'); t('H22 floor: …an explicit null is the same as omitting it', typeof h22ClosedCardPmResidue(closedOn(['pm:dispatched'], '2026-01-01T00:00:00Z'), null), 'string'); - t('H22 floor: …and a clean old card is still clean', h22ClosedCardPmResidue(closedOn(['domain:ui'], '2026-01-01T00:00:00Z'), null), null); + t('H22 floor: …and a clean old card is still clean', h22ClosedCardPmResidue(closedOn(['domain:cli'], '2026-01-01T00:00:00Z'), null), null); // resolveClosureFloor — the env reading, including the loud refusal. t('closure floor: unset means no floor', resolveClosureFloor({}).floor, null); @@ -7042,7 +9776,7 @@ function selfTest() { t('closure floor: …and names its source', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: '2026-08-28' }).source, 'PM_SWEEP_CLOSED_FLOOR'); t('closure floor: surrounding whitespace is trimmed, not rejected', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: ' 2026-08-28 ' }).valid, true); // Malformed is REFUSED. Each of these would otherwise become "no floor" and - // restore the ~347-row flood this install shut off. + // restore the flood on the one install that set the variable. for (const bad of ['28-08-2026', '2026/08/28', 'yesterday', '2026-08-28T00:00:00Z', '2026-8-28', 'O', '0']) { t(`closure floor: \`${bad}\` is refused, not defaulted`, resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: bad }).valid, false); } @@ -7063,6 +9797,7 @@ function selfTest() { // The summary line's H22 clause — a pass that read nothing must not read the // same as a board with no residue (#4690), so the count is always stated. t('summary: the H22 clause states what the closed pass read', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, closed: 200 }, 0).includes('H22 read 200 recently-closed issue(s)'), true); + t('summary: an absent closed count degrades to 0, never to undefined', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0 }, 0).includes('H22 read 0 recently-closed'), true); // …and when a floor is in force the line SAYS so: "read 200" with a floor // silently applied would overstate what was judged, which is the same // unread-reads-as-clean defect the count itself exists to prevent. @@ -7072,7 +9807,6 @@ function selfTest() { // The DISABLED branch still wins over a floor: a 0-page window read nothing, // so the line must keep saying UNREAD rather than describing a floored pass. t('summary: a disabled reader with a floor set still reads UNREAD', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, closedWindowDisabled: true, closedFloor: '2026-08-28' }, 0).includes('UNREAD'), true); - t('summary: an absent closed count degrades to 0, never to undefined', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0 }, 0).includes('H22 read 0 recently-closed'), true); // -- H23: the COMMIT-MESSAGE surface (#10942) ------------------------------- // @@ -7136,8 +9870,8 @@ function selfTest() { // across, so H7's own sentence is asserted to CARRY the clause this one must // never carry — a one-sided assertion would pass against a sentence that lost // both. - const fired23 = h23CommitMessageContradiction(squashOf('9320', '9478')); - const fired7 = h7PartOfWithClosingKeyword({ body: 'Part of #77\n\nFixes #77' }); + const fired23 = String(h23CommitMessageContradiction(squashOf('9320', '9478')) ?? ''); + const fired7 = String(h7PartOfWithClosingKeyword({ body: 'Part of #77\n\nFixes #77' }) ?? ''); t('H23: the sentence prescribes REWORDING', fired23.includes('REWORD'), true); t('H23: …and never the body-surface backtick remedy', fired23.includes('put the keyword in backticks'), false); t('H23: …nor any "in backticks" advice at all', fired23.includes('in backticks'), false); @@ -7182,14 +9916,14 @@ function selfTest() { // -- H9: `pm:on-hold` without a machine-fireable `Restart-when:` ------------ const hold = (body) => issue(['pm:on-hold'], [], body); t('H9: hold with no Restart-when line -> finding', typeof h9OnHoldNoRestartWhen(hold('parked until the train ships')), 'string'); - t('H9: …and the finding prescribes the close default', h9OnHoldNoRestartWhen(hold('parked')).includes('not planned'), true); + t('H9: …and the finding prescribes the close default', h9row(hold('parked')).includes('not planned'), true); t('H9: closed-upstream form -> clean', h9OnHoldNoRestartWhen(hold('Restart-when: closed acme/widgets#123')), null); t('H9: executable-predicate form -> clean', h9OnHoldNoRestartWhen(hold('Restart-when: npm view create-objectstack dist-tags reports >= 17.0.0')), null); t('H9: mid-body line -> clean', h9OnHoldNoRestartWhen(hold('Context first.\nRestart-when: closed acme/widgets#123\nMore prose.')), null); // `manual` is a hold trying to opt out of having an exit — it counts as // missing, or the one-word spelling defeats the invariant. t('H9: manual form -> finding', typeof h9OnHoldNoRestartWhen(hold('Restart-when: manual — first EE customer asking')), 'string'); - t('H9: …and the finding names the manual shape', h9OnHoldNoRestartWhen(hold('Restart-when: manual — reason')).includes('manual'), true); + t('H9: …and the finding names the manual shape', h9row(hold('Restart-when: manual — reason')).includes('manual'), true); t('H9: Manual case-insensitive as a VALUE -> finding', typeof h9OnHoldNoRestartWhen(hold('Restart-when: Manual — reason')), 'string'); t('H9: manual line + fireable line -> clean', h9OnHoldNoRestartWhen(hold('Restart-when: manual — x\nRestart-when: closed acme/widgets#9')), null); // The KEY is byte-stable like `Blocked-by:` — a lowercase key is a line the @@ -7222,10 +9956,10 @@ function selfTest() { // hold must still fire, or backticks would become the opt-out the bare // spelling is denied. t('H9: a decorated `manual` hold still fires', typeof h9OnHoldNoRestartWhen(hold('`Restart-when: manual — first EE customer asking`')), 'string'); - t('H9: …and still names the manual shape', h9OnHoldNoRestartWhen(hold('**Restart-when: manual — reason**')).includes('manual'), true); + t('H9: …and still names the manual shape', h9row(hold('**Restart-when: manual — reason**')).includes('manual'), true); // -- H9's remedy text: verify/unwrap first, close last (#10102) ------------ - const h9NoLine = h9OnHoldNoRestartWhen(hold('parked until the train ships')); + const h9NoLine = String(h9OnHoldNoRestartWhen(hold('parked until the train ships')) ?? ''); t('H9: the no-line row names the decorated/unparsed possibility', h9NoLine.includes('cannot parse'), true); t('H9: …and tells the seat to read the body before acting', h9NoLine.includes('READ THE BODY BEFORE ACTING'), true); t('H9: …and demotes closing to the last resort', h9NoLine.includes('Closing is the LAST resort'), true); @@ -7233,15 +9967,15 @@ function selfTest() { // The `manual` row is NOT a parse failure — a line was read — so it must not // carry the "maybe it is there" hedge, or the one row that really does mean // "this hold has no exit" starts reading as uncertain. - t('H9: the manual row carries no unparsed hedge', h9OnHoldNoRestartWhen(hold('Restart-when: manual — reason')).includes('cannot parse'), false); - t('H9: …but does still demote closing', h9OnHoldNoRestartWhen(hold('Restart-when: manual — reason')).includes('Closing is the LAST resort'), true); + t('H9: the manual row carries no unparsed hedge', h9row(hold('Restart-when: manual — reason')).includes('cannot parse'), false); + t('H9: …but does still demote closing', h9row(hold('Restart-when: manual — reason')).includes('Closing is the LAST resort'), true); // The channel contract, stated in the row itself — two channels since // #10403, symmetric with H4/H14. An undocumented difference between two // adjacent rules is how the last two half-states on that lane were made, // and an undocumented SAMENESS would repeat it in mirror image. t('H9: the row states the two-channel contract', h9NoLine.includes('body OR a comment'), true); t('H9: …and names the predicates it now matches', h9NoLine.includes('H4/H14'), true); - t('H9: …and the manual row states it too', h9OnHoldNoRestartWhen(hold('Restart-when: manual — x')).includes('body OR a comment'), true); + t('H9: …and the manual row states it too', h9row(hold('Restart-when: manual — x')).includes('body OR a comment'), true); // -- H9's COMMENT channel (#10403) ----------------------------------------- // The incident fixture: a machine-fireable exit parked in a comment — the @@ -7261,21 +9995,21 @@ function selfTest() { t('H9: a mid-sentence mention in a comment is not a line', typeof h9OnHoldNoRestartWhen(hold('parked'), ['someone should add a `Restart-when: closed acme/w#1` line']), 'string'); // Both channels read and empty: the sentence says EITHER, so the reader // knows both were judged — and an unconsulted channel is never claimed. - t('H9: neither channel -> the sentence names EITHER channel', h9OnHoldNoRestartWhen(hold('parked'), ['no directive here']).includes('EITHER channel'), true); - t('H9: an empty comment thread is a real reading', h9OnHoldNoRestartWhen(hold('parked'), []).includes('EITHER channel'), true); - t('H9: an unconsulted comment channel is not claimed as read', h9OnHoldNoRestartWhen(hold('parked')).includes('EITHER channel'), false); - t('H9: …and the both-channels hedge tells the seat to read the thread too', h9OnHoldNoRestartWhen(hold('parked'), []).includes('BODY AND THE THREAD'), true); + t('H9: neither channel -> the sentence names EITHER channel', h9row(hold('parked'), ['no directive here']).includes('EITHER channel'), true); + t('H9: an empty comment thread is a real reading', h9row(hold('parked'), []).includes('EITHER channel'), true); + t('H9: an unconsulted comment channel is not claimed as read', h9row(hold('parked')).includes('EITHER channel'), false); + t('H9: …and the both-channels hedge tells the seat to read the thread too', h9row(hold('parked'), []).includes('BODY AND THE THREAD'), true); // Unreadable is neither read nor absent (#4690): the row fires on the cheap // side, says the thread could not be read, and never claims EITHER. t('H9: an UNREADABLE comment thread still fires', typeof h9OnHoldNoRestartWhen(hold('parked'), null), 'string'); - t('H9: …but never claims the second channel is empty', h9OnHoldNoRestartWhen(hold('parked'), null).includes('EITHER channel'), false); - t('H9: …and says the thread could not be read', h9OnHoldNoRestartWhen(hold('parked'), null).includes('could NOT be read'), true); - t('H9: …citing the unreadable-is-not-absent rule', h9OnHoldNoRestartWhen(hold('parked'), null).includes('#4690'), true); + t('H9: …but never claims the second channel is empty', h9row(hold('parked'), null).includes('EITHER channel'), false); + t('H9: …and says the thread could not be read', h9row(hold('parked'), null).includes('could NOT be read'), true); + t('H9: …citing the unreadable-is-not-absent rule', h9row(hold('parked'), null).includes('#4690'), true); t('H9: a fireable BODY line clears even an unreadable thread', h9OnHoldNoRestartWhen(hold('Restart-when: closed acme/widgets#123'), null), null); // Manual across channels: a manual body line plus a fireable comment line is // the mixed shape a seat actually writes when upgrading a hold in place. t('H9: manual body line + fireable comment line -> clean', h9OnHoldNoRestartWhen(hold('Restart-when: manual — x'), ['Restart-when: closed acme/widgets#9']), null); - t('H9: manual lines in BOTH channels still name the manual shape', h9OnHoldNoRestartWhen(hold('Restart-when: manual — x'), ['Restart-when: manual — y']).includes('manual'), true); + t('H9: manual lines in BOTH channels still name the manual shape', h9row(hold('Restart-when: manual — x'), ['Restart-when: manual — y']).includes('manual'), true); // The gathering policy — what gets READ AT ALL (mirrors the H4 gate pins). t('gate: a body-clean pm:on-hold card is an H9 candidate', needsRestartWhenComments(hold('no line here')), true); @@ -7292,7 +10026,7 @@ function selfTest() { updated_at: updatedAt, }); t('H10: unassigned p0 past the threshold -> finding', typeof h10StaleUnclaimedP0(p0([], hoursAgo(36), ['pm:queue']), NOW), 'string'); - t('H10: …and the finding names the threshold', h10StaleUnclaimedP0(p0([], hoursAgo(36)), NOW).includes(`${P0_UNCLAIMED_STALE_HOURS}h`), true); + t('H10: …and the finding names the threshold', h10row(p0([], hoursAgo(36)), NOW).includes(`${P0_UNCLAIMED_STALE_HOURS}h`), true); t('H10: fresh unassigned p0 -> clean', h10StaleUnclaimedP0(p0([], hoursAgo(1)), NOW), null); t('H10: exactly at the threshold -> clean (strictly beyond fires)', h10StaleUnclaimedP0(p0([], hoursAgo(P0_UNCLAIMED_STALE_HOURS)), NOW), null); t('H10: assigned p0 is out of scope however old', h10StaleUnclaimedP0(p0(['os-help'], hoursAgo(200)), NOW), null); @@ -7316,8 +10050,8 @@ function selfTest() { t('H11: bug label + blocked -> finding', typeof h11ImportantParked(parkedCard(['bug', 'pm:blocked']), NOW), 'string'); t('H11: security label + on-hold -> finding', typeof h11ImportantParked(parkedCard(['security', 'pm:on-hold']), NOW), 'string'); t('H11: priority:p1 + blocked -> finding', typeof h11ImportantParked(parkedCard(['priority:p1', 'pm:blocked']), NOW), 'string'); - t('H11: …and the finding names the parked state', h11ImportantParked(parkedCard(['bug', 'pm:blocked']), NOW).includes('pm:blocked'), true); - t('H11: …and the threshold', h11ImportantParked(parkedCard(['bug', 'pm:blocked']), NOW).includes(`${IMPORTANT_PARKED_STALE_DAYS}d`), true); + t('H11: …and the finding names the parked state', h11row(parkedCard(['bug', 'pm:blocked']), NOW).includes('pm:blocked'), true); + t('H11: …and the threshold', h11row(parkedCard(['bug', 'pm:blocked']), NOW).includes(`${IMPORTANT_PARKED_STALE_DAYS}d`), true); t('H11: fresh park is clean', h11ImportantParked(parkedCard(['bug', 'pm:on-hold'], { created: daysAgo(2) }), NOW), null); t('H11: exactly at the threshold is clean (strictly beyond fires)', h11ImportantParked(parkedCard(['bug', 'pm:on-hold'], { created: daysAgo(IMPORTANT_PARKED_STALE_DAYS) }), NOW), null); t('H11: important but not parked is out of scope', h11ImportantParked(parkedCard(['bug', 'pm:queue']), NOW), null); @@ -7338,8 +10072,8 @@ function selfTest() { merged_at: null, }); t('H12: ready + unarmed + stale -> finding', typeof h12OrphanLanding(openPr(), NOW), 'string'); - t('H12: …and the finding names the threshold', h12OrphanLanding(openPr(), NOW).includes(`${ORPHAN_LANDING_STALE_HOURS}h`), true); - t('H12: …and prescribes the landing-window re-read, not just the fact', h12OrphanLanding(openPr(), NOW).includes('landing window'), true); + t('H12: …and the finding names the threshold', h12row(openPr(), NOW).includes(`${ORPHAN_LANDING_STALE_HOURS}h`), true); + t('H12: …and prescribes the landing-window re-read, not just the fact', h12row(openPr(), NOW).includes('landing window'), true); t('H12: draft is out of scope however old (parked deliberately)', h12OrphanLanding(openPr({ draft: true, updated: hoursAgo(200) }), NOW), null); t('H12: armed auto-merge -> clean (queue machinery holds it)', h12OrphanLanding(openPr({ auto_merge: { merge_method: 'squash' } }), NOW), null); t('H12: fresh ready PR -> clean', h12OrphanLanding(openPr({ updated: hoursAgo(1) }), NOW), null); @@ -7360,8 +10094,8 @@ function selfTest() { ...extra, }); t('H13: aged domain card with no pm-state -> finding', typeof h13DomainWithoutPmState(domainCard(['domain:engine-core', 'bug', 'regression'], hoursAgo(26)), NOW), 'string'); - t('H13: …and the finding names the threshold', h13DomainWithoutPmState(domainCard(['domain:engine-core'], hoursAgo(26)), NOW).includes(`${DOMAIN_HALF_STATE_STALE_HOURS}h`), true); - t('H13: …and blames the healing loop, not inventory', h13DomainWithoutPmState(domainCard(['domain:engine-core'], hoursAgo(26)), NOW).includes('healing loop'), true); + t('H13: …and the finding names the threshold', h13row(domainCard(['domain:engine-core'], hoursAgo(26)), NOW).includes(`${DOMAIN_HALF_STATE_STALE_HOURS}h`), true); + t('H13: …and blames the healing loop, not inventory', h13row(domainCard(['domain:engine-core'], hoursAgo(26)), NOW).includes('healing loop'), true); t('H13: pm:queue pairs the domain label -> clean', h13DomainWithoutPmState(domainCard(['domain:engine-core', 'pm:queue'], hoursAgo(26)), NOW), null); t('H13: needs-user-decision is a state (the inbox reads it) -> clean', h13DomainWithoutPmState(domainCard(['domain:spec', 'needs-user-decision'], hoursAgo(200)), NOW), null); t('H13: finding is a state (the grading round reads it) -> clean', h13DomainWithoutPmState(domainCard(['domain:cli', 'finding'], hoursAgo(200)), NOW), null); @@ -7379,23 +10113,23 @@ function selfTest() { t('H13: absent updated_at -> finding, not fresh', typeof h13DomainWithoutPmState(domainCard(['domain:engine-core'], undefined), NOW), 'string'); // The louder line — the measured card carried its trigger in its own body. const p0Body = { body: 'P0 checklist-item failure (data-integrity DELETE regression) — priority label is triage’s to set' }; - t('H13: body self-declaring P0 -> louder line', h13DomainWithoutPmState(domainCard(['domain:engine-core'], hoursAgo(26), p0Body), NOW).includes('P0-SUSPECT'), true); - t('H13: …which prescribes the emergency-triage channel', h13DomainWithoutPmState(domainCard(['domain:engine-core'], hoursAgo(26), p0Body), NOW).includes('emergency-triage'), true); + t('H13: body self-declaring P0 -> louder line', h13row(domainCard(['domain:engine-core'], hoursAgo(26), p0Body), NOW).includes('P0-SUSPECT'), true); + t('H13: …which prescribes the emergency-triage channel', h13row(domainCard(['domain:engine-core'], hoursAgo(26), p0Body), NOW).includes('emergency-triage'), true); t('H13: data-integrity phrasing alone fires the louder line', h13SelfDeclaredP0({ title: '', body: 'a data integrity regression in DELETE' }), true); t('H13: the title is scanned too', h13SelfDeclaredP0({ title: 'p0 suspect: rows vanish', body: '' }), true); // Strip reuse (H7 reading 4): quoting the token in backticks is not a // self-declaration, and `P0` inside a word is not the token. t('H13: P0 only inside backticks is not a self-declaration', h13SelfDeclaredP0({ title: '', body: 'the card quotes `P0` in passing' }), false); t('H13: P0 inside a word does not fire', h13SelfDeclaredP0({ title: '', body: 'the HTTP0 protocol note' }), false); - t('H13: a quiet body stays on the base line', h13DomainWithoutPmState(domainCard(['domain:engine-core'], hoursAgo(26), { body: 'ordinary defect' }), NOW).includes('P0-SUSPECT'), false); + t('H13: a quiet body stays on the base line', h13row(domainCard(['domain:engine-core'], hoursAgo(26), { body: 'ordinary defect' }), NOW).includes('P0-SUSPECT'), false); // -- H18: `pm:retriage` aged past one triage cycle (2026-08-19/20 ruling) -- // Reuses `domainCard` — a generic (labels, updated_at, extra) issue builder, // not a domain-specific one despite the name. t('H18: retriage past the threshold, coexisting pm:queue -> finding', typeof h18RetriageAged(domainCard(['pm:retriage', 'pm:queue'], hoursAgo(3)), NOW), 'string'); - t('H18: …and the finding names the threshold', h18RetriageAged(domainCard(['pm:retriage', 'pm:queue'], hoursAgo(3)), NOW).includes(`${RETRIAGE_STALE_HOURS}h`), true); - t('H18: …and names the coexisting standing label', h18RetriageAged(domainCard(['pm:retriage', 'pm:queue'], hoursAgo(3)), NOW).includes('`pm:queue`'), true); - t('H18: multiple coexisting labels are all named', h18RetriageAged(domainCard(['pm:retriage', 'pm:blocked', 'pm:blocking'], hoursAgo(3)), NOW).includes('`pm:blocked`') && h18RetriageAged(domainCard(['pm:retriage', 'pm:blocked', 'pm:blocking'], hoursAgo(3)), NOW).includes('`pm:blocking`'), true); + t('H18: …and the finding names the threshold', h18row(domainCard(['pm:retriage', 'pm:queue'], hoursAgo(3)), NOW).includes(`${RETRIAGE_STALE_HOURS}h`), true); + t('H18: …and names the coexisting standing label', h18row(domainCard(['pm:retriage', 'pm:queue'], hoursAgo(3)), NOW).includes('`pm:queue`'), true); + t('H18: multiple coexisting labels are all named', h18row(domainCard(['pm:retriage', 'pm:blocked', 'pm:blocking'], hoursAgo(3)), NOW).includes('`pm:blocked`') && h18row(domainCard(['pm:retriage', 'pm:blocked', 'pm:blocking'], hoursAgo(3)), NOW).includes('`pm:blocking`'), true); // Under-threshold: fresh objection is normal intake latency, not a finding. t('H18: retriage under the threshold -> clean', h18RetriageAged(domainCard(['pm:retriage', 'pm:queue'], hoursAgo(1)), NOW), null); t('H18: exactly at the threshold -> clean (strictly beyond fires)', h18RetriageAged(domainCard(['pm:retriage', 'pm:queue'], hoursAgo(RETRIAGE_STALE_HOURS)), NOW), null); @@ -7403,10 +10137,10 @@ function selfTest() { t('H18: no pm:retriage label -> out of scope however old', h18RetriageAged(domainCard(['pm:queue'], hoursAgo(200)), NOW), null); // The disputed-target variant: `pm:retriage` alone, no coexisting `pm:*`. t('H18: retriage ALONE (no coexisting pm:* label) -> finding', typeof h18RetriageAged(domainCard(['pm:retriage'], hoursAgo(3)), NOW), 'string'); - t('H18: …and names the disputed-target note', h18RetriageAged(domainCard(['pm:retriage'], hoursAgo(3)), NOW).includes('异议对象不明'), true); - t('H18: …and does not claim a coexisting label it does not have', h18RetriageAged(domainCard(['pm:retriage'], hoursAgo(3)), NOW).includes('alongside its standing'), false); + t('H18: …and names the disputed-target note', h18row(domainCard(['pm:retriage'], hoursAgo(3)), NOW).includes('异议对象不明'), true); + t('H18: …and does not claim a coexisting label it does not have', h18row(domainCard(['pm:retriage'], hoursAgo(3)), NOW).includes('alongside its standing'), false); // A non-`pm:*` label (e.g. `domain:*`) never counts as the coexisting label. - t('H18: a domain: label is not counted as a coexisting pm:* label', h18RetriageAged(domainCard(['pm:retriage', 'domain:skills'], hoursAgo(3)), NOW).includes('异议对象不明'), true); + t('H18: a domain: label is not counted as a coexisting pm:* label', h18row(domainCard(['pm:retriage', 'domain:skills'], hoursAgo(3)), NOW).includes('异议对象不明'), true); // #4690 in miniature, same as H10–H13: unreadable must not read as fresh. t('H18: unreadable updated_at -> finding, not fresh', typeof h18RetriageAged(domainCard(['pm:retriage'], 'not-a-date'), NOW), 'string'); t('H18: absent updated_at -> finding, not fresh', typeof h18RetriageAged(domainCard(['pm:retriage'], undefined), NOW), 'string'); @@ -7500,19 +10234,19 @@ function selfTest() { // Direction A — the label carried with nothing targeting it. t('H14-A: pm:blocking with nothing targeting it -> finding', typeof h14BlockingCacheIncoherent(carded(7276, ['pm:queue', 'pm:blocking']), idx([])), 'string'); - t('H14-A: …and it names the stale-cache reading', h14BlockingCacheIncoherent(carded(7276, ['pm:blocking']), idx([])).includes('stale derived cache'), true); - t('H14-A: …and prescribes the derivation pass, never a label from here', h14BlockingCacheIncoherent(carded(7276, ['pm:blocking']), idx([])).includes('derivation pass'), true); - t('H14-A: …and says why stale is worse than absent', h14BlockingCacheIncoherent(carded(7276, ['pm:blocking']), idx([])).includes('with authority'), true); + t('H14-A: …and it names the stale-cache reading', h14row(carded(7276, ['pm:blocking']), idx([])).includes('stale derived cache'), true); + t('H14-A: …and prescribes the derivation pass, never a label from here', h14row(carded(7276, ['pm:blocking']), idx([])).includes('derivation pass'), true); + t('H14-A: …and says why stale is worse than absent', h14row(carded(7276, ['pm:blocking']), idx([])).includes('with authority'), true); // The repo-boundary wording (#10139): STALE reads as "no dependent in this // repo", never as exhaustive over the population, and the remedy is // conditional on a cross-repo check rather than an outright drop. - t('H14-A: …names the repo boundary', h14BlockingCacheIncoherent(carded(7276, ['pm:blocking']), idx([])).includes('no dependent found in this repo'), true); - t('H14-A: …and says cross-repo dependents are not swept', h14BlockingCacheIncoherent(carded(7276, ['pm:blocking']), idx([])).includes('cross-repo dependents are not swept'), true); - t('H14-A: …and the remedy is conditional on verifying cross-repo dependents', h14BlockingCacheIncoherent(carded(7276, ['pm:blocking']), idx([])).includes('verify cross-repo dependents before'), true); + t('H14-A: …names the repo boundary', h14row(carded(7276, ['pm:blocking']), idx([])).includes('no dependent found in this repo'), true); + t('H14-A: …and says cross-repo dependents are not swept', h14row(carded(7276, ['pm:blocking']), idx([])).includes('cross-repo dependents are not swept'), true); + t('H14-A: …and the remedy is conditional on verifying cross-repo dependents', h14row(carded(7276, ['pm:blocking']), idx([])).includes('verify cross-repo dependents before'), true); // The negative: the old exhaustive phrasing ("the full two-channel index", // instructing an unconditional drop) must be gone — it is what would have // told a reader to sever the live #7917 / objectui#4356 edge. - t('H14-A: …and the old exhaustive phrasing is GONE', h14BlockingCacheIncoherent(carded(7276, ['pm:blocking']), idx([])).includes('full two-channel index'), false); + t('H14-A: …and the old exhaustive phrasing is GONE', h14row(carded(7276, ['pm:blocking']), idx([])).includes('full two-channel index'), false); // The negative for direction A: the label is EARNED, so nothing to report. t( 'H14-A: pm:blocking with a real dependent -> clean', @@ -7523,8 +10257,8 @@ function selfTest() { // Direction B — targeted, but the cache never landed. const missingIdx = idx([carded(9650, ['pm:queue'], 'Blocked-by: #9832')]); t('H14-B: targeted without pm:blocking -> finding', typeof h14BlockingCacheIncoherent(carded(9832, ['bug', 'pm:dispatched', 'domain:cli']), missingIdx), 'string'); - t('H14-B: …and it names the waiting card', h14BlockingCacheIncoherent(carded(9832, ['pm:dispatched']), missingIdx).includes('#9650'), true); - t('H14-B: …and calls it an invisible unblocker', h14BlockingCacheIncoherent(carded(9832, ['pm:dispatched']), missingIdx).includes('selection order cannot see'), true); + t('H14-B: …and it names the waiting card', h14row(carded(9832, ['pm:dispatched']), missingIdx).includes('#9650'), true); + t('H14-B: …and calls it an invisible unblocker', h14row(carded(9832, ['pm:dispatched']), missingIdx).includes('selection order cannot see'), true); // The negative for direction B: no label and nobody waiting is the ordinary // shape of ~230 of this board's ~234 open cards. It must be silent, or the // row means nothing. @@ -7538,8 +10272,8 @@ function selfTest() { ); // Fan-out cap: named, then counted. const manyDeps = idx(Array.from({ length: 7 }, (_, i) => carded(100 + i, [], 'Blocked-by: #5'))); - t('H14-B: a large fan-out names the cap and counts the rest', h14BlockingCacheIncoherent(carded(5, ['pm:queue']), manyDeps).includes(`+${7 - BLOCKING_DEPENDENT_LIST_CAP} more`), true); - t('H14-B: …and reports the true total, not the capped one', h14BlockingCacheIncoherent(carded(5, ['pm:queue']), manyDeps).includes('targeted by 7 open card(s)'), true); + t('H14-B: a large fan-out names the cap and counts the rest', h14row(carded(5, ['pm:queue']), manyDeps).includes(`+${7 - BLOCKING_DEPENDENT_LIST_CAP} more`), true); + t('H14-B: …and reports the true total, not the capped one', h14row(carded(5, ['pm:queue']), manyDeps).includes('targeted by 7 open card(s)'), true); t('H14: a missing index does not crash and reads as untargeted', h14BlockingCacheIncoherent(carded(5, ['pm:blocking']), undefined) !== null, true); // Reverse verification against the LIVE board, 2026-08-19 (234 open cards). @@ -7559,7 +10293,7 @@ function selfTest() { ]; const liveIdx = idx(liveBodies); t('H14 reverse-verify: #7276 (the board\'s only pm:blocking card) -> stale finding', typeof h14BlockingCacheIncoherent(carded(7276, ['pm:queue', 'domain:devx', 'pm:blocking']), liveIdx), 'string'); - t('H14 reverse-verify: #9832 (targeted by #9650, unlabeled) -> missing finding naming #9650', h14BlockingCacheIncoherent(carded(9832, ['bug', 'pm:dispatched', 'domain:cli']), liveIdx).includes('#9650'), true); + t('H14 reverse-verify: #9832 (targeted by #9650, unlabeled) -> missing finding naming #9650', h14row(carded(9832, ['bug', 'pm:dispatched', 'domain:cli']), liveIdx).includes('#9650'), true); t('H14 reverse-verify: #9919 (targeted by #9249, unlabeled) -> missing finding', typeof h14BlockingCacheIncoherent(carded(9919, ['pm:queue', 'repo:cloud']), liveIdx), 'string'); // …and the four measured NON-findings from the same reading, which is what // makes the six above readable as signal rather than as a predicate that @@ -7599,8 +10333,8 @@ function selfTest() { const epic9465 = carded(9465, ['domain:devx', 'pm:epic', 'pm:blocking']); const decision9968 = carded(9968, ['pm:decision', 'pm:blocking']); // The defect, pinned: this is what the body-only index reported. - t('H14 false-stale: #9465 reads STALE against a body-only index', h14BlockingCacheIncoherent(epic9465, bodyOnlyIdx)?.includes('stale derived cache'), true); - t('H14 false-stale: #9968 reads STALE against a body-only index', h14BlockingCacheIncoherent(decision9968, bodyOnlyIdx)?.includes('stale derived cache'), true); + t('H14 false-stale: #9465 reads STALE against a body-only index', h14row(epic9465, bodyOnlyIdx).includes('stale derived cache'), true); + t('H14 false-stale: #9968 reads STALE against a body-only index', h14row(decision9968, bodyOnlyIdx).includes('stale derived cache'), true); // The fix: the same two cards against the two-channel index. t('H14 false-stale: #9465 is CLEAN once comment edges are read', h14BlockingCacheIncoherent(epic9465, unionIdx), null); t('H14 false-stale: #9968 is CLEAN once comment edges are read', h14BlockingCacheIncoherent(decision9968, unionIdx), null); @@ -7608,9 +10342,9 @@ function selfTest() { t('H14 false-stale: …and #9969 and #9652 both point at #9968', unionIdx.get(9968).join(','), '9969,9652'); // Direction B rides the same union: a comment-only edge is enough to call a // card an invisible unblocker. - t('H14-B: a comment-only edge produces a missing-cache row', h14BlockingCacheIncoherent(carded(9465, ['domain:devx']), unionIdx)?.includes('#9709'), true); - t('H14-B: …and the sentence names the channel pair', h14BlockingCacheIncoherent(carded(9465, ['domain:devx']), unionIdx).includes('body or comment'), true); - t('H14-A: …the stale sentence names both channels too', h14BlockingCacheIncoherent(epic9465, bodyOnlyIdx).includes('body OR comment'), true); + t('H14-B: a comment-only edge produces a missing-cache row', h14row(carded(9465, ['domain:devx']), unionIdx).includes('#9709'), true); + t('H14-B: …and the sentence names the channel pair', h14row(carded(9465, ['domain:devx']), unionIdx).includes('body or comment'), true); + t('H14-A: …the stale sentence names both channels too', h14row(epic9465, bodyOnlyIdx).includes('body OR comment'), true); // -- H14 under an INCOMPLETE index (a gated comment fetch failed) ---------- // @@ -7620,7 +10354,7 @@ function selfTest() { t('H14-A: stale is SUSPENDED when the index is known incomplete', h14BlockingCacheIncoherent(epic9465, bodyOnlyIdx, { indexComplete: false }), null); t('H14-A: …and still fires when the index is complete', typeof h14BlockingCacheIncoherent(epic9465, bodyOnlyIdx, { indexComplete: true }), 'string'); t('H14-A: …and completeness defaults to true for body-only callers', typeof h14BlockingCacheIncoherent(epic9465, bodyOnlyIdx), 'string'); - t('H14-B: missing SURVIVES an incomplete index', h14BlockingCacheIncoherent(carded(9465, ['domain:devx']), unionIdx, { indexComplete: false })?.includes('#9709'), true); + t('H14-B: missing SURVIVES an incomplete index', h14row(carded(9465, ['domain:devx']), unionIdx, { indexComplete: false }).includes('#9709'), true); t('H14-B: …and an earned label stays clean either way', h14BlockingCacheIncoherent(epic9465, unionIdx, { indexComplete: false }), null); // The summary line carries the third `read X of Y` pair, and says out loud @@ -7745,17 +10479,18 @@ function selfTest() { // POSITIVE — a closed target fires. const expired10112 = h19BlockOutlivedBlocker(blockedCard(10112), [target(10126, 'closed', { closedAt: '2026-08-20T09:03:37Z' })]); + const expired10112Row = String(expired10112 ?? ''); t('H19: a CLOSED target fires', typeof expired10112, 'string'); - t('H19: …and names the target', expired10112.includes('`#10126`'), true); - t('H19: …with the close timestamp, so the latency is readable off the row', expired10112.includes('closed 2026-08-20T09:03:37Z'), true); - t('H19: …and says the block outlived its blocker', expired10112.includes('outlived its blocker'), true); - t('H19: …and says nothing else here asks this question', expired10112.includes('H4 asks whether the line EXISTS'), true); - t('H19: …and hands the release to the unlock sweep\'s double-checks', expired10112.includes('放行双查'), true); - t('H19: …naming double-check ① (most recent conversion comment)', expired10112.includes('MOST RECENT conversion comment'), true); - t('H19: …and double-check ② (a newer merged PR refuses release)', expired10112.includes('MERGED PR newer than that conversion comment'), true); - t('H19: …and forbids a label written from this script', expired10112.includes('never a label written from this script'), true); - t('H19: a fully discharged block says every target is closed', expired10112.includes('Every target it names is closed'), true); - t('H19: …and does not claim a partial discharge', expired10112.includes('PARTIAL'), false); + t('H19: …and names the target', expired10112Row.includes('`#10126`'), true); + t('H19: …with the close timestamp, so the latency is readable off the row', expired10112Row.includes('closed 2026-08-20T09:03:37Z'), true); + t('H19: …and says the block outlived its blocker', expired10112Row.includes('outlived its blocker'), true); + t('H19: …and says nothing else here asks this question', expired10112Row.includes('H4 asks whether the line EXISTS'), true); + t('H19: …and hands the release to the unlock sweep\'s double-checks', expired10112Row.includes('放行双查'), true); + t('H19: …naming double-check ① (most recent conversion comment)', expired10112Row.includes('MOST RECENT conversion comment'), true); + t('H19: …and double-check ② (a newer merged PR refuses release)', expired10112Row.includes('MERGED PR newer than that conversion comment'), true); + t('H19: …and forbids a label written from this script', expired10112Row.includes('never a label written from this script'), true); + t('H19: a fully discharged block says every target is closed', expired10112Row.includes('Every target it names is closed'), true); + t('H19: …and does not claim a partial discharge', expired10112Row.includes('PARTIAL'), false); // NEGATIVE — an open target is clean, and silence here is a real reading. t('H19: an OPEN target -> clean', h19BlockOutlivedBlocker(blockedCard(1), [target(2, 'open')]), null); @@ -7766,34 +10501,82 @@ function selfTest() { // PARTIAL — one of two closed. Fires, and says it is partial. const partial = h19BlockOutlivedBlocker(blockedCard(1), [target(2, 'closed', { closedAt: '2026-08-20T07:58:08Z' }), target(3, 'open')]); + const partialRow = String(partial ?? ''); t('H19: one closed of two still fires', typeof partial, 'string'); - t('H19: …and reports the count as 1 of 2', partial.includes('1 of 2 `Blocked-by:` target(s)'), true); - t('H19: …names it a PARTIAL discharge', partial.includes('PARTIAL'), true); - t('H19: …names the target that is still open', partial.includes('`#3`'), true); - t('H19: …and does not decide the card is unblocked', partial.includes('it does not decide it'), true); - t('H19: two closed of two reads as 2 of 2', h19BlockOutlivedBlocker(blockedCard(1), [target(2, 'closed'), target(3, 'closed')]).includes('2 of 2'), true); + t('H19: …and reports the count as 1 of 2', partialRow.includes('1 of 2 `Blocked-by:` target(s)'), true); + t('H19: …names it a PARTIAL discharge', partialRow.includes('PARTIAL'), true); + t('H19: …names the target that is still open', partialRow.includes('`#3`'), true); + t('H19: …and does not decide the card is unblocked', partialRow.includes('it does not decide it'), true); + t('H19: two closed of two reads as 2 of 2', h19row(blockedCard(1), [target(2, 'closed'), target(3, 'closed')]).includes('2 of 2'), true); // UNRESOLVED — never reads as clean, and never reads as closed either. const unresolvedOnly = h19BlockOutlivedBlocker(blockedCard(1), [foreign('objectstack-ai/cloud', 88, 'unresolved', { detail: 'HTTP 404' })]); + const unresolvedOnlyRow = String(unresolvedOnly ?? ''); t('H19: an UNRESOLVED target fires rather than reading clean', typeof unresolvedOnly, 'string'); - t('H19: …saying the liveness is UNJUDGED', unresolvedOnly.includes('UNJUDGED, not confirmed'), true); - t('H19: …and never claims the block is expired', unresolvedOnly.includes('outlived its blocker. Nothing else here'), false); - t('H19: …citing the unreadable-is-not-absent rule', unresolvedOnly.includes('#4690'), true); - t('H19: …naming the cross-repo target in full owner/repo#N form', unresolvedOnly.includes('`objectstack-ai/cloud#88`'), true); - t('H19: …with the observed status', unresolvedOnly.includes('HTTP 404'), true); - t('H19: …and refuses to guess WHY it did not resolve', unresolvedOnly.includes('the cause is not guessed at'), true); - t('H19: …and still routes the release through the unlock sweep', unresolvedOnly.includes('放行双查'), true); + t('H19: …saying the liveness is UNJUDGED', unresolvedOnlyRow.includes('UNJUDGED, not confirmed'), true); + t('H19: …and never claims the block is expired', unresolvedOnlyRow.includes('outlived its blocker. Nothing else here'), false); + t('H19: …citing the unreadable-is-not-absent rule', unresolvedOnlyRow.includes('#4690'), true); + t('H19: …naming the cross-repo target in full owner/repo#N form', unresolvedOnlyRow.includes('`objectstack-ai/cloud#88`'), true); + t('H19: …with the observed status', unresolvedOnlyRow.includes('HTTP 404'), true); + t('H19: …and still routes the release through the unlock sweep', unresolvedOnlyRow.includes('放行双查'), true); + // UNJUDGED must not read at judged-row weight (#11218 half 2). The premise + // re-verification asked for exactly this check against the live report. + t('H19: …and says UNJUDGED is not a quiet row', unresolvedOnlyRow.includes('must not be skimmed'), true); + t('H19: …and equates it with having read nothing at all', unresolvedOnlyRow.includes('exactly as unverified as if nothing had been read'), true); + // With NO repo probe taken, the wording stays undiagnosed — the pre-#11218 + // posture, preserved rather than silently upgraded. + t('H19: an unprobed cross-repo target claims no cause', unresolvedOnlyRow.includes('resolves only when its repo answers'), true); + t('H19: …and asserts nothing about the repo either way', unresolvedOnlyRow.includes('is NOT readable') || unresolvedOnlyRow.includes('IS readable'), false); + + // -- The MEASURED cause (#11218 half 1, the half that can land) ------------ + // + // A cross-repo 404 is ambiguous; `GET /repos//` disambiguates + // it. The row reports the measurement, never an inference from the issue 404. + const scopeGap = String(h19BlockOutlivedBlocker(blockedCard(10938), [ + foreign('objectstack-ai/cloud', 944, 'unresolved', { detail: 'HTTP 404', repoReadable: false }), + ]) ?? ''); + t('H19 cause: an unreadable REPO is named per target', scopeGap.includes("`objectstack-ai/cloud` is NOT readable to this sweep's credential"), true); + t('H19 cause: …and the observed status is still carried', scopeGap.includes('HTTP 404'), true); + t('H19 cause: …and it is declared measured, not inferred', scopeGap.includes('measured directly'), true); + t('H19 cause: …and named a standing ACCEPTED limit, not a defect to chase', scopeGap.includes('standing, ACCEPTED'), true); + t('H19 cause: …so no re-run is prescribed', scopeGap.includes('no re-run and no re-read of this card will ever resolve'), true); + t('H19 cause: …and the credential call is routed to routing/security', scopeGap.includes('routing/security'), true); + t('H19 cause: …and ⛔ the card is not the place to fix it', scopeGap.includes('Do not "fix" it on the card'), true); + // The OTHER leg of the same probe: repo readable, so the number is not there. + const missingNumber = String(h19BlockOutlivedBlocker(blockedCard(1), [ + foreign('objectstack-ai/objectui', 999999, 'unresolved', { detail: 'HTTP 404', repoReadable: true }), + ]) ?? ''); + t('H19 cause: a READABLE repo means the number is not there', missingNumber.includes('IS readable, so that number is not there'), true); + t('H19 cause: …and that is NOT reported as a scope gap', missingNumber.includes('is NOT readable'), false); + t('H19 cause: …nor as an accepted cross-repo limit', missingNumber.includes('standing, ACCEPTED'), false); + // A failed probe (`null`) picks NEITHER side — #4690 at the probe's own level. + const probeFailed = String(h19BlockOutlivedBlocker(blockedCard(1), [ + foreign('objectstack-ai/cloud', 88, 'unresolved', { detail: 'HTTP 500', repoReadable: null }), + ]) ?? ''); + t('H19 cause: an unreadable PROBE names no cause at all', probeFailed.includes('IS readable') || probeFailed.includes('is NOT readable'), false); + t('H19 cause: …but the target and status are still named', probeFailed.includes('`objectstack-ai/cloud#88`') && probeFailed.includes('HTTP 500'), true); + // Mixed: only the scope-gapped ones are counted in the loud clause. + const mixedCause = String(h19BlockOutlivedBlocker(blockedCard(1), [ + foreign('objectstack-ai/cloud', 944, 'unresolved', { detail: 'HTTP 404', repoReadable: false }), + foreign('objectstack-ai/objectui', 4356, 'unresolved', { detail: 'HTTP 404', repoReadable: true }), + ]) ?? ''); + t('H19 cause: the scope-gap count is the unreadable-repo ones only', mixedCause.includes('1 of them are unjudgeable'), true); + // A LOCAL target is never probed, and renders exactly as it always did. + const localUnresolved = String(h19BlockOutlivedBlocker(blockedCard(1), [target(77, 'unresolved', { detail: 'HTTP 404' })]) ?? ''); + t('H19 cause: a LOCAL unresolved target claims no repo reading', localUnresolved.includes('readable'), false); + t('H19 cause: …and is still named with its status', localUnresolved.includes('`#77` (HTTP 404)'), true); // An unresolved target alongside an open one still fires, and says which. const mixedUnresolved = h19BlockOutlivedBlocker(blockedCard(1), [target(2, 'open'), foreign('objectstack-ai/objectui', 4356, 'unresolved', { detail: 'HTTP 403' })]); + const mixedUnresolvedRow = String(mixedUnresolved ?? ''); t('H19: unresolved + open still fires', typeof mixedUnresolved, 'string'); - t('H19: …and reports the resolved remainder as open', mixedUnresolved.includes("The card's other 1 target(s) did resolve, and are still open."), true); + t('H19: …and reports the resolved remainder as open', mixedUnresolvedRow.includes("The card's other 1 target(s) did resolve, and are still open."), true); // Closed AND unresolved: the closed row leads, the gap is appended. - const closedAndUnresolved = h19BlockOutlivedBlocker(blockedCard(1), [target(2, 'closed'), foreign('objectstack-ai/cloud', 88, 'unresolved', { detail: 'HTTP 404' })]); + const closedAndUnresolved = String(h19BlockOutlivedBlocker(blockedCard(1), [target(2, 'closed'), foreign('objectstack-ai/cloud', 88, 'unresolved', { detail: 'HTTP 404' })]) ?? ''); t('H19: a closed target leads even when another is unresolved', closedAndUnresolved.includes('outlived its blocker'), true); t('H19: …and the unresolved one is still declared unjudged', closedAndUnresolved.includes('unjudged, not open'), true); // The render budget: many targets are capped and the row says it counted. - const manyClosed = h19BlockOutlivedBlocker(blockedCard(1), [2, 3, 4, 5, 6, 7, 8].map((n) => target(n, 'closed'))); + const manyClosed = String(h19BlockOutlivedBlocker(blockedCard(1), [2, 3, 4, 5, 6, 7, 8].map((n) => target(n, 'closed'))) ?? ''); t('H19: the target list is capped at the render budget', manyClosed.includes(`+${7 - H19_TARGET_LIST_CAP} more`), true); t('H19: …and the count is the full one, not the shown one', manyClosed.includes('7 of 7'), true); @@ -7813,7 +10596,7 @@ function selfTest() { t('H19 measured ①: …a body-only read would have found nothing', keysOf(blockedCard(10112, 'body carries no line')), ''); t( 'H19 measured ①: …and the card fires once its target is resolved closed', - h19BlockOutlivedBlocker(blockedCard(10112, 'body carries no line'), [target(10126, 'closed', { closedAt: '2026-08-20T09:03:37Z' })]).includes('`#10126` (closed 2026-08-20T09:03:37Z)'), + h19row(blockedCard(10112, 'body carries no line'), [target(10126, 'closed', { closedAt: '2026-08-20T09:03:37Z' })]).includes('`#10126` (closed 2026-08-20T09:03:37Z)'), true, ); @@ -7827,7 +10610,7 @@ function selfTest() { t('H19 measured ②: the decorated body line yields the target', keysOf(blockedCard(10063, liveDecoratedBody)), 'objectstack-ai/objectstack#9612'); t( 'H19 measured ②: …and the card fires once its target is resolved closed', - h19BlockOutlivedBlocker(blockedCard(10063, liveDecoratedBody), [target(9612, 'closed', { closedAt: '2026-08-20T07:58:08Z' })]).includes('closed 2026-08-20T07:58:08Z'), + h19row(blockedCard(10063, liveDecoratedBody), [target(9612, 'closed', { closedAt: '2026-08-20T07:58:08Z' })]).includes('closed 2026-08-20T07:58:08Z'), true, ); // The prose around the line names #9612 four more times; only the DIRECTIVE @@ -7845,8 +10628,13 @@ function selfTest() { t('summary: the H19 coverage pair is reported', summaryLine(btCounts(11, 12), 1).includes('targets resolved on 11 of 12 distinct `Blocked-by:` target(s)'), true); t('summary: …and says the unit is DISTINCT targets, not per-card edges', summaryLine(btCounts(11, 12), 1).includes('distinct'), true); t('summary: …scoped to the population H19 judges', summaryLine(btCounts(11, 12), 1).includes('named by open `pm:blocked` card(s)'), true); - t('summary: an H19 shortfall points at the rows that carry it', summaryLine(btCounts(11, 12), 1).includes('each unresolved target is named on its own card\'s row, never dropped'), true); - t('summary: a complete H19 pass adds no shortfall clause', summaryLine(btCounts(12, 12), 1).includes('never dropped'), false); + // The shortfall clause still points at the rows that carry the gap — but it + // no longer PROMISES they survive, it says what makes them survive (the + // unjudged sort band). The bare "never dropped" wording was measurably false + // on 2026-08-25T02:08Z; see `UNJUDGED_MARKER`. + t('summary: an H19 shortfall points at the rows that carry it', summaryLine(btCounts(11, 12), 1).includes("unresolved target(s) are named on their own cards' rows"), true); + t('summary: …and names the mechanism instead of promising the outcome', summaryLine(btCounts(11, 12), 1).includes('sort ABOVE the size trim'), true); + t('summary: a complete H19 pass adds no shortfall clause', summaryLine(btCounts(12, 12), 1).includes('unresolved target(s) are named'), false); t('summary: absent H19 counts degrade to 0, never to undefined', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0 }, 0).includes('resolved on 0 of 0 distinct'), true); t('summary: …and the H19 clause never prints the string undefined', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0 }, 0).includes('undefined'), false); t('summary: the report-only contract still ends the sentence', summaryLine(btCounts(12, 12), 1).endsWith('not a gate verdict.'), true); @@ -7920,18 +10708,19 @@ function selfTest() { const refState = (branch, state, detail = null) => ({ branch, state, detail }); const absentRef = [refState('claude/issue-8878-dispatch-latency', 'absent')]; const fired20 = h20DispatchedNoBranchRef(dispatchedCard(), gov(claim8878), absentRef, NOW_20); + const fired20Row = String(fired20 ?? ''); t('H20: the measured #8878 shape FIRES', typeof fired20, 'string'); - t('H20: …and names the branch that has no ref', fired20.includes('`claude/issue-8878-dispatch-latency`'), true); - t('H20: …and says NO SUCH REMOTE REF EXISTS', fired20.includes('NO SUCH REMOTE REF EXISTS'), true); - t('H20: …with the measured age and the threshold', fired20.includes(`~74 min after the claim was posted (threshold ${DISPATCHED_NO_REF_STALE_MINUTES} min)`), true); - t('H20: …and states the two-acts mechanism', fired20.includes('Claiming and dispatching are two acts with a gap between them'), true); - t('H20: …and that it is invisible from the card itself', fired20.includes('invisible from the card'), true); - t('H20: …and warns the symptom is identical to a DEAD agent', fired20.includes('IDENTICAL to a dev agent that died'), true); - t('H20: …naming the opposite remedies rather than diagnosing one', fired20.includes('a dead agent needs a probe, an undispatched claim needs a dispatch'), true); - t('H20: …and carries the ⛔ keying rule verbatim', fired20.includes('keys on NO REF AT ALL, never on "no PR yet"'), true); - t('H20: …with the reason a PR key would be wrong', fired20.includes('legitimately has a ref and no PR for over an hour'), true); - t('H20: …and routes an already-merged delivery to H8 instead', fired20.includes("the missing paired write is H8's"), true); - t('H20: …and forbids a label written from this script', fired20.includes('never a label written from this script'), true); + t('H20: …and names the branch that has no ref', fired20Row.includes('`claude/issue-8878-dispatch-latency`'), true); + t('H20: …and says NO SUCH REMOTE REF EXISTS', fired20Row.includes('NO SUCH REMOTE REF EXISTS'), true); + t('H20: …with the measured age and the threshold', fired20Row.includes(`~74 min after the claim was posted (threshold ${DISPATCHED_NO_REF_STALE_MINUTES} min)`), true); + t('H20: …and states the two-acts mechanism', fired20Row.includes('Claiming and dispatching are two acts with a gap between them'), true); + t('H20: …and that it is invisible from the card itself', fired20Row.includes('invisible from the card'), true); + t('H20: …and warns the symptom is identical to a DEAD agent', fired20Row.includes('IDENTICAL to a dev agent that died'), true); + t('H20: …naming the opposite remedies rather than diagnosing one', fired20Row.includes('a dead agent needs a probe, an undispatched claim needs a dispatch'), true); + t('H20: …and carries the ⛔ keying rule verbatim', fired20Row.includes('keys on NO REF AT ALL, never on "no PR yet"'), true); + t('H20: …with the reason a PR key would be wrong', fired20Row.includes('legitimately has a ref and no PR for over an hour'), true); + t('H20: …and routes an already-merged delivery to H8 instead', fired20Row.includes("the missing paired write is H8's"), true); + t('H20: …and forbids a label written from this script', fired20Row.includes('never a label written from this script'), true); t('H20: not a loud finding', isLoudFinding(fired20), false); // ★ The regression pin the filing card asked for by name: a dev inside a long @@ -7972,24 +10761,25 @@ function selfTest() { [refState('claude/issue-8878-dispatch-latency', 'unreadable', 'HTTP 500')], NOW_20, ); + const unread20Row = String(unread20 ?? ''); t('H20 unreadable: does NOT read as healthy', unread20 === null, false); t('H20 unreadable: fires its own row', typeof unread20, 'string'); - t('H20 unreadable: …which says the dispatch is UNJUDGED', unread20.includes('UNJUDGED, not confirmed'), true); - t('H20 unreadable: …and reports the observed status', unread20.includes('HTTP 500'), true); - t('H20 unreadable: …and does NOT assert the finding it did not measure', unread20.includes('NO SUCH REMOTE REF EXISTS'), false); - t('H20 unreadable: …citing the unread-is-not-absent rule', unread20.includes('#4690'), true); - t('H20 unreadable: …and refuses to guess WHY', unread20.includes('the cause is not guessed at'), true); + t('H20 unreadable: …which says the dispatch is UNJUDGED', unread20Row.includes('UNJUDGED, not confirmed'), true); + t('H20 unreadable: …and reports the observed status', unread20Row.includes('HTTP 500'), true); + t('H20 unreadable: …and does NOT assert the finding it did not measure', unread20Row.includes('NO SUCH REMOTE REF EXISTS'), false); + t('H20 unreadable: …citing the unread-is-not-absent rule', unread20Row.includes('#4690'), true); + t('H20 unreadable: …and refuses to guess WHY', unread20Row.includes('the cause is not guessed at'), true); t('H20 unreadable: not a loud finding either', isLoudFinding(unread20), false); // Mixed readings. "No ref at all" is a claim about EVERY branch the card // names, so one unread probe is enough to withhold it — and one existing ref // is enough to call the card healthy. - const mixedUnread20 = h20DispatchedNoBranchRef( + const mixedUnread20 = String(h20DispatchedNoBranchRef( dispatchedCard(), gov([claimRow(minsAgo20(74), 'Claim: seat.\nBranch: `claude/issue-1-a`\nBranch: `claude/issue-1-b`')]), [refState('claude/issue-1-a', 'absent'), refState('claude/issue-1-b', 'unreadable', 'HTTP 502')], NOW_20, - ); + ) ?? ''); t('H20 mixed: absent + unreadable takes the quieter row', mixedUnread20.includes('UNJUDGED, not confirmed'), true); t('H20 mixed: …and still names the branch that resolved absent', mixedUnread20.includes('`claude/issue-1-a`'), true); t('H20 mixed: …explaining why one unread probe withholds the finding', mixedUnread20.includes('one unread probe is enough to withhold it'), true); @@ -8011,15 +10801,16 @@ function selfTest() { t('H20: absent ref states -> no row', h20DispatchedNoBranchRef(dispatchedCard(), gov(claim8878), undefined, NOW_20), null); t('H20: a missing issue does not crash', h20DispatchedNoBranchRef(undefined, gov(claim8878), absentRef, NOW_20), null); const unstamped20 = h20DispatchedNoBranchRef(dispatchedCard(), gov([claimRow('not-a-date', claimBody8878)]), absentRef, NOW_20); + const unstamped20Row = String(unstamped20 ?? ''); t('H20: an unreadable claim timestamp fires rather than reading fresh', typeof unstamped20, 'string'); - t('H20: …and says so in place of an age', unstamped20.includes('an unreadable claim timestamp (which must not read as fresh)'), true); + t('H20: …and says so in place of an age', unstamped20Row.includes('an unreadable claim timestamp (which must not read as fresh)'), true); const many20 = Array.from({ length: 7 }, (_, i) => refState(`claude/issue-1-b${i}`, 'absent')); - const capped20 = h20DispatchedNoBranchRef( + const capped20 = String(h20DispatchedNoBranchRef( dispatchedCard(), gov([claimRow(minsAgo20(74), `Claim: seat.\n${many20.map((r) => `Branch: \`${r.branch}\``).join('\n')}`)]), many20, NOW_20, - ); + ) ?? ''); t('H20: the branch list is capped at the render budget', capped20.includes(`+${7 - H20_BRANCH_LIST_CAP} more`), true); // The summary line's fifth `read X of Y` pair — H19's shape, and owed for the @@ -8040,6 +10831,7 @@ function selfTest() { t('summary: the pair is declared as serving BOTH rows', summaryLine(refCounts(5, 5), 1).includes('Dispatch liveness (H20 + H27)'), true); t('summary: …naming H27\'s threshold', summaryLine(refCounts(5, 5), 1).includes(`H27's ${DEAD_CLAIM_STALE_HOURS}h population is a subset`), true); t('summary: …and that it costs no request of its own', summaryLine(refCounts(5, 5), 1).includes('costs no request of its own'), true); + t('summary: the report-only contract still ends the sentence after H20', summaryLine(refCounts(5, 5), 1).endsWith('not a gate verdict.'), true); // -- H27: the claim is PERFECT and the claimant is dead (#11248) ------------ @@ -8076,27 +10868,31 @@ function selfTest() { 'delivery' in over ? over.delivery : noDelivery, NOW_27, ); + // Same reason as H8's `halvesRow`: `dead27` is three-valued, and the + // `typeof dead27()` / `dead27(…) === null` cases below assert precisely the + // nullability that stringifying `dead27` itself would erase. + const dead27Row = (...args) => String(dead27(...args) ?? ''); // ★ The finding itself, and the facts the sentence must carry. t('H27: a frozen branch + no PR past 24h -> finding', typeof dead27(), 'string'); - t('H27: …and names the branch', dead27().includes(`\`${BR_27}\``), true); - t('H27: …and says the branch has not moved since the claim', dead27().includes('NOT MOVED SINCE IT WAS CLAIMED'), true); - t('H27: …and reports the age against the protocol threshold', dead27().includes(`threshold ${DEAD_CLAIM_STALE_HOURS}h`), true); - t('H27: …calling that threshold the protocol\'s own line, not a heuristic', dead27().includes("protocol's own stale-claim line"), true); - t('H27: …and states the measured ~26h age', dead27().includes('~26h after the claim was posted'), true); - t('H27: …and names the lane-block consequence, not just the silence', dead27().includes('mutual-exclusion read'), true); - t('H27: …and explains WHY H20 cannot see it', dead27().includes('pushing the empty branch the first action'), true); - t('H27: …and rules out the pre-window merged delivery first', dead27().includes(`${MERGED_WINDOW_PAGES} pages`), true); + t('H27: …and names the branch', dead27Row().includes(`\`${BR_27}\``), true); + t('H27: …and says the branch has not moved since the claim', dead27Row().includes('NOT MOVED SINCE IT WAS CLAIMED'), true); + t('H27: …and reports the age against the protocol threshold', dead27Row().includes(`threshold ${DEAD_CLAIM_STALE_HOURS}h`), true); + t('H27: …calling that threshold the protocol\'s own line, not a heuristic', dead27Row().includes("protocol's own stale-claim line"), true); + t('H27: …and states the measured ~26h age', dead27Row().includes('~26h after the claim was posted'), true); + t('H27: …and names the lane-block consequence, not just the silence', dead27Row().includes('mutual-exclusion read'), true); + t('H27: …and explains WHY H20 cannot see it', dead27Row().includes('pushing the empty branch the first action'), true); + t('H27: …and rules out the pre-window merged delivery first', dead27Row().includes(`${MERGED_WINDOW_PAGES} pages`), true); t('H27: not a loud finding', isLoudFinding(dead27()), false); // ★ Report-only, and specifically NOT a reclaim — the protocol protects a // claim whose branch carries commits, so this row must never read as // authority to drop an assignee. - t('H27: the remedy is the recovery inspection', dead27().includes('post-kill recovery'), true); - t('H27: …naming all three recovery states', dead27().includes('on the remote / on the container disk only / gone'), true); - t('H27: …and the UNVERIFIED hand-off', dead27().includes('flagged UNVERIFIED'), true); - t('H27: …quoting the protocol rule that forbids reclaiming this card', dead27().includes('有带提交活分支的认领永不回收'), true); - t('H27: …and never a label written from this script', dead27().includes('Never a label written from this script'), true); + t('H27: the remedy is the recovery inspection', dead27Row().includes('post-kill recovery'), true); + t('H27: …naming all three recovery states', dead27Row().includes('on the remote / on the container disk only / gone'), true); + t('H27: …and the UNVERIFIED hand-off', dead27Row().includes('flagged UNVERIFIED'), true); + t('H27: …quoting the protocol rule that forbids reclaiming this card', dead27Row().includes('有带提交活分支的认领永不回收'), true); + t('H27: …and never a label written from this script', dead27Row().includes('Never a label written from this script'), true); // ★ Disjoint from H20 BY CONSTRUCTION, in both directions, on one fixture. const absent27 = [{ branch: BR_27, state: 'absent', detail: null, headCommittedAt: null }]; @@ -8114,10 +10910,10 @@ function selfTest() { t('H27: activity is measured against the CLAIM, not the threshold', dead27({ refs: frozen('2026-08-23T05:47:00Z') }), null); // Three-valued, never two (#4690): an unreadable comparison is not a "no". t('H27: an unreadable head timestamp does NOT read as healthy', dead27({ refs: frozen(null) }) === null, false); - t('H27: …and fires the quieter UNJUDGED row instead', dead27({ refs: frozen(null) }).includes('UNJUDGED, not confirmed healthy'), true); - t('H27: …which does not assert the finding it did not measure', dead27({ refs: frozen(null) }).includes('NOT MOVED SINCE IT WAS CLAIMED'), false); - t('H27: …citing the unread-is-not-absent rule', dead27({ refs: frozen(null) }).includes('#4690'), true); - t('H27: an absent head field reads as unknown, not as an old date', dead27({ refs: [{ branch: BR_27, state: 'exists' }] }).includes('UNJUDGED'), true); + t('H27: …and fires the quieter UNJUDGED row instead', dead27Row({ refs: frozen(null) }).includes('UNJUDGED, not confirmed healthy'), true); + t('H27: …which does not assert the finding it did not measure', dead27Row({ refs: frozen(null) }).includes('NOT MOVED SINCE IT WAS CLAIMED'), false); + t('H27: …citing the unread-is-not-absent rule', dead27Row({ refs: frozen(null) }).includes('#4690'), true); + t('H27: an absent head field reads as unknown, not as an old date', dead27Row({ refs: [{ branch: BR_27, state: 'exists' }] }).includes('UNJUDGED'), true); t('H27: branchMovedSinceClaim is three-valued', [branchMovedSinceClaim(frozen()[0], claim27()), branchMovedSinceClaim(frozen('2026-08-23T09:00:00Z')[0], claim27()), branchMovedSinceClaim(frozen(null)[0], claim27())].join(','), 'false,true,'); // ★ The delivery term, through H8's own relation so the two cannot drift. @@ -8156,10 +10952,11 @@ function selfTest() { t('H27: a young claim -> no row even with a frozen branch', dead27({ claim: claim27('2026-08-24T04:00:00Z') }), null); t('H27: exactly AT the threshold is not past it', dead27({ claim: claim27(new Date(NOW_27 - DEAD_CLAIM_STALE_HOURS * 3_600_000).toISOString()) }), null); const unstamped27 = dead27({ claim: claim27('not-a-date') }); + const unstamped27Row = String(unstamped27 ?? ''); t('H27: an unreadable claim timestamp does not read as fresh', unstamped27 === null, false); - t('H27: …and yields the UNJUDGED row (the comparison is impossible)', unstamped27.includes('UNJUDGED'), true); + t('H27: …and yields the UNJUDGED row (the comparison is impossible)', unstamped27Row.includes('UNJUDGED'), true); const many27 = Array.from({ length: 7 }, (_, i) => ({ branch: `claude/issue-1-b${i}`, state: 'exists', headCommittedAt: '2026-08-22T09:00:00Z' })); - t('H27: the branch list is capped at the render budget', dead27({ refs: many27 }).includes(`+${7 - H20_BRANCH_LIST_CAP} more`), true); + t('H27: the branch list is capped at the render budget', dead27Row({ refs: many27 }).includes(`+${7 - H20_BRANCH_LIST_CAP} more`), true); // -- H16: open non-draft PR stuck in a merge conflict (2026-08-19 incident) -- // The single-PR payload shape, since `mergeable_state` is absent from the @@ -8175,12 +10972,12 @@ function selfTest() { } = {}) => ({ draft, mergeable_state, auto_merge, head, body, updated_at: updated, merged_at }); t('H16: dirty beyond the threshold -> finding', typeof h16StuckMergeConflict(conflictPr(), NOW), 'string'); - t('H16: …and the finding names the threshold', h16StuckMergeConflict(conflictPr(), NOW).includes(`${MERGE_CONFLICT_STALE_HOURS}h`), true); - t('H16: …and names the platform state it read', h16StuckMergeConflict(conflictPr(), NOW).includes('mergeable_state: dirty'), true); - t('H16: …and prescribes the merge-and-resolve remedy', h16StuckMergeConflict(conflictPr(), NOW).includes('merges `main` into the branch'), true); + t('H16: …and the finding names the threshold', h16row(conflictPr(), NOW).includes(`${MERGE_CONFLICT_STALE_HOURS}h`), true); + t('H16: …and names the platform state it read', h16row(conflictPr(), NOW).includes('mergeable_state: dirty'), true); + t('H16: …and prescribes the merge-and-resolve remedy', h16row(conflictPr(), NOW).includes('merges `main` into the branch'), true); // The proxy must be DECLARED in the row, not silently substituted: a reader // shown "~4h" has to know it is silence on the PR, not the conflict's age. - t('H16: …and declares the age is the PR\'s updated_at, not the conflict\'s', h16StuckMergeConflict(conflictPr(), NOW).includes("Age is the PR's `updated_at`, not the conflict's"), true); + t('H16: …and declares the age is the PR\'s updated_at, not the conflict\'s', h16row(conflictPr(), NOW).includes("Age is the PR's `updated_at`, not the conflict's"), true); t('H16: dirty within the threshold -> clean (a fresh push is mid-resolution)', h16StuckMergeConflict(conflictPr({ updated: hoursAgo(1) }), NOW), null); t('H16: exactly at the threshold -> clean (strictly beyond fires)', h16StuckMergeConflict(conflictPr({ updated: hoursAgo(MERGE_CONFLICT_STALE_HOURS) }), NOW), null); t('H16: draft is out of scope however old (parked deliberately)', h16StuckMergeConflict(conflictPr({ draft: true, updated: hoursAgo(200) }), NOW), null); @@ -8209,16 +11006,16 @@ function selfTest() { // `auto_merge` as finding-reducing and is right to; here the arming is what // made every proxy signal read healthy while the PR went nowhere. t('H16: armed auto-merge does NOT suppress the row', typeof h16StuckMergeConflict(conflictPr({ auto_merge: { merge_method: 'squash' } }), NOW), 'string'); - t('H16: …and the row says auto-merge does not resolve conflicts', h16StuckMergeConflict(conflictPr({ auto_merge: { merge_method: 'squash' } }), NOW).includes('does NOT resolve conflicts'), true); + t('H16: …and the row says auto-merge does not resolve conflicts', h16row(conflictPr({ auto_merge: { merge_method: 'squash' } }), NOW).includes('does NOT resolve conflicts'), true); // The contrast that makes the divergence deliberate rather than an oversight: // one PR row, two predicates, opposite readings of the same armed field. t('H16: …while H12 stays clean on that same armed PR (the divergence is by design)', h12OrphanLanding(conflictPr({ auto_merge: { merge_method: 'squash' }, updated: hoursAgo(50) }), NOW), null); // The held-card clause — the row names the delivery, not only the branch. - t('H16: a `Fixes #N` body names the card it is holding', h16StuckMergeConflict(conflictPr({ body: 'Fixes #9763\n\nsome prose' }), NOW).includes('holding card #9763'), true); - t('H16: `Part of #N` counts as held too (H8\'s reading of delivery)', h16StuckMergeConflict(conflictPr({ body: 'Part of #9652' }), NOW).includes('holding card #9652'), true); - t('H16: two cards are pluralised and listed in order', h16StuckMergeConflict(conflictPr({ body: 'Fixes #9961\nFixes #9936' }), NOW).includes('holding cards #9936, #9961'), true); - t('H16: a body with no card carries no holding clause', h16StuckMergeConflict(conflictPr({ body: 'no card here' }), NOW).includes('holding'), false); + t('H16: a `Fixes #N` body names the card it is holding', h16row(conflictPr({ body: 'Fixes #9763\n\nsome prose' }), NOW).includes('holding card #9763'), true); + t('H16: `Part of #N` counts as held too (H8\'s reading of delivery)', h16row(conflictPr({ body: 'Part of #9652' }), NOW).includes('holding card #9652'), true); + t('H16: two cards are pluralised and listed in order', h16row(conflictPr({ body: 'Fixes #9961\nFixes #9936' }), NOW).includes('holding cards #9936, #9961'), true); + t('H16: a body with no card carries no holding clause', h16row(conflictPr({ body: 'no card here' }), NOW).includes('holding'), false); // #8293 reading 4 carries over: a body QUOTING the spelling names nothing. t('H16: a backticked `Fixes #N` is not a held card', h16HeldCards('the dispatch asked for `Fixes #8284`').length, 0); t('H16: h16HeldCards de-duplicates and sorts', h16HeldCards('Fixes #30\nPart of #12\nFixes #30').join(','), '12,30'); @@ -8292,9 +11089,9 @@ function selfTest() { head: { ref: 'claude/issue-9763-literal-collector-spellings' }, }); t('H16 incident: the #9826 shape is a finding', typeof h16StuckMergeConflict(pr9826, NOW), 'string'); - t('H16 incident: …fires despite auto-merge being armed', h16StuckMergeConflict(pr9826, NOW).includes('MERGE CONFLICT'), true); - t('H16 incident: …and names the card it was holding', h16StuckMergeConflict(pr9826, NOW).includes('holding card #9763'), true); - t('H16 incident: …at its measured ~4h age', h16StuckMergeConflict(pr9826, NOW).includes('untouched for ~4h'), true); + t('H16 incident: …fires despite auto-merge being armed', h16row(pr9826, NOW).includes('MERGE CONFLICT'), true); + t('H16 incident: …and names the card it was holding', h16row(pr9826, NOW).includes('holding card #9763'), true); + t('H16 incident: …at its measured ~4h age', h16row(pr9826, NOW).includes('untouched for ~4h'), true); t('H16 incident: …and the sweep would have spent a request on it', h16NeedsDetail(pr9826, NOW), true); // The counterfactual that makes the fixture mean something: at the moment // the conflict appeared, the same PR was silent — the threshold is what @@ -8568,6 +11365,43 @@ function selfTest() { t('summaryLine: absent H16 counts degrade to 0, never to undefined', summaryLine({ repo: 'o/r', issues: 1, unscoped: 1, prs: 1, merged: 1 }, 0).includes('read on 0 of 0'), true); t('summaryLine: …and never prints the string undefined', summaryLine({ repo: 'o/r', issues: 1, unscoped: 1, prs: 1, merged: 1 }, 0).includes('undefined'), false); + // -- The H19 coverage clause, and the promise it used to make falsely ------- + const btCounts2 = (blockerResolved, blockerTargets, extra = {}) => ({ ...counts, blockerResolved, blockerTargets, ...extra }); + t('summary: a complete H19 pass adds no shortfall clause', summaryLine(btCounts2(12, 12), 1).includes('unresolved target(s) are named'), false); + t('summary: an H19 shortfall counts the unresolved targets', summaryLine(btCounts2(25, 28), 1).includes('the 3 unresolved target(s) are named on their own cards\' rows'), true); + t('summary: …and promises the TRIM cannot drop them, which is the fixable half', summaryLine(btCounts2(25, 28), 1).includes('sort ABOVE the size trim'), true); + t('summary: …and cites the sweep on which the old promise was false', summaryLine(btCounts2(25, 28), 1).includes('2026-08-25T02:08Z'), true); + t('summary: …and no longer makes the bare "never dropped" claim for H19', summaryLine(btCounts2(25, 28), 1).includes('each unresolved target is named on its own card\'s row, never dropped'), false); + + // -- The cross-repo reachability pair (#11218) ------------------------------ + const xCounts = (crossRepoProbed, crossRepoUnreadable) => btCounts2(25, 28, { crossRepoProbed, crossRepoUnreadable }); + t('summary: cross-repo probes are reported', summaryLine(xCounts(2, 1), 1).includes('measured directly on 2 sibling repo(s)'), true); + t('summary: …naming how many refused', summaryLine(xCounts(2, 1), 1).includes('1 do(es) not answer this credential'), true); + t('summary: …and that a re-run will not help', summaryLine(xCounts(2, 1), 1).includes('no re-run'), true); + t('summary: no probes taken -> no cross-repo clause at all', summaryLine(xCounts(0, 0), 1).includes('sibling repo(s)'), false); + t('summary: absent cross-repo counts degrade to 0, never to undefined', summaryLine(counts, 0).includes('undefined'), false); + + // -- H32's seat coverage pair (#11706) ------------------------------------- + const seatCounts = (seatMarkersRead, seatCandidates) => ({ ...counts, seatMarkersRead, seatCandidates }); + t('summary: the H32 seat pair is reported', summaryLine(seatCounts(6, 6), 1).includes('marker thread read on 6 of 6 HELD seat post(s)'), true); + t('summary: …scoped to lanes countable on this board', summaryLine(seatCounts(6, 6), 1).includes('countable on THIS board'), true); + t('summary: …and says a sibling-lane seat is out of scope', summaryLine(seatCounts(6, 6), 1).includes("a seat held for a sibling repo's lane is out of scope"), true); + t('summary: …and that an unread thread declines rather than accuses', summaryLine(seatCounts(2, 6), 1).includes('decline to judge that seat rather than accuse it'), true); + t('summary: absent H32 counts degrade to 0, never to undefined', summaryLine(counts, 0).includes('marker thread read on 0 of 0'), true); + + // -- The dispatch-liveness pair was COMPUTED and never forwarded ----------- + // + // A wiring regression, not a wording one: `sweep()` assembled `counts` + // without `dispatchRefTargets`/`dispatchRefRead`, so this clause rendered + // `0 of 0` on every live sweep — including 2026-08-25T02:08Z, which said + // `read on 0 of 0` while publishing two H20 findings that a non-empty ref + // cache is the only way to produce. Pinned as the CONTRACT the assembly owes, + // so a future edit that drops the keys again fails here rather than in the + // anchor body six hours later. + t('summary: the H20/H27 pair is a real reading, not a constant 0 of 0', summaryLine(refCounts(4, 5), 1).includes('read on 0 of 0 distinct claimed'), false); + t('summary: SWEEP_COUNT_KEYS names every count the summary consumes', SWEEP_COUNT_KEYS.includes('dispatchRefTargets') && SWEEP_COUNT_KEYS.includes('dispatchRefRead'), true); + t('summary: …including the pairs added since', SWEEP_COUNT_KEYS.includes('crossRepoProbed') && SWEEP_COUNT_KEYS.includes('seatCandidates'), true); + // The loudness contract between H13 and the renderer — one constant, two // readers. If the prefix ever drifts, this pair fails rather than the alarm // going quietly unsorted. @@ -8636,6 +11470,37 @@ function selfTest() { t('markdown: the trim announces itself in the body', trimmed.includes('further row(s) omitted'), true); t('markdown: truncation can never reach a loud row', trimmed.includes('#900'), true); + // -- The UNJUDGED band and the trim (#11218) ------------------------------- + // + // The regression this pins is MEASURED, not hypothetical. On the + // 2026-08-25T02:08Z sweep the summary said "each unresolved target is named + // on its own card's row, never dropped" while 199 rows were trimmed and NOT + // ONE rendered row carried an unresolved target. The header promised exactly + // what the trim had just eaten. + const unjudgedRow = finding( + 9999, + 'H19', + h19BlockOutlivedBlocker(blockedCard(9999), [foreign('objectstack-ai/cloud', 944, 'unresolved', { detail: 'HTTP 404', repoReadable: false })]), + ); + t('markdown: an UNJUDGED row is recognised as such', isUnjudgedFinding(unjudgedRow[2]), true); + t('markdown: …a judged H19 row is NOT', isUnjudgedFinding(h19BlockOutlivedBlocker(blockedCard(1), [target(2, 'closed')])), false); + t('markdown: …and an ordinary row is NOT', isUnjudgedFinding('`pm:dispatched` with no assignee'), false); + t('markdown: the explicit marker is recognised too', isUnjudgedFinding(`${UNJUDGED_MARKER} something could not be read`), true); + const withUnjudged = renderMarkdown([...many, unjudgedRow], counts); + t('markdown: the trim can never reach an UNJUDGED row', withUnjudged.includes('#9999'), true); + t('markdown: …even though that row sorts LAST by card number', many.every(([i]) => i.number < 9999), true); + t('markdown: …and the trim still fired', withUnjudged.includes('further row(s) omitted'), true); + t('markdown: …and the body is still under budget', withUnjudged.length <= MARKDOWN_BODY_BUDGET, true); + t('markdown: an UNJUDGED row is banner-announced', withUnjudged.includes('UNJUDGED row(s) in this sweep'), true); + t('markdown: …and the banner says a later sweep will not fix it', withUnjudged.includes('nothing in a later sweep will resolve them'), true); + t('markdown: no banner when nothing is unjudged', renderMarkdown([quietRow], counts).includes('UNJUDGED row(s) in this sweep'), false); + // A loud P0 row still outranks an unjudged one: the emergency channel is the + // more urgent of the two, and both survive the trim regardless. + t('markdown: a loud row still sorts above an UNJUDGED one', renderMarkdown([unjudgedRow, loudRow], counts).indexOf('#900') < renderMarkdown([unjudgedRow, loudRow], counts).indexOf('#9999'), true); + t('markdown: an UNJUDGED row sorts above an ordinary one', renderMarkdown([quietRow, unjudgedRow], counts).indexOf('#9999') < renderMarkdown([quietRow, unjudgedRow], counts).indexOf('#200'), true); + // The plain renderer keeps the caller's order, as it always has. + t('plain: applies no unjudged sort either', renderPlain([quietRow, unjudgedRow], counts).indexOf('#200') < renderPlain([quietRow, unjudgedRow], counts).indexOf('#9999'), true); + // Provenance is caller-supplied text interpolated into one italic line: a // newline in it would break the header apart, so it is flattened, not trusted. t('provenance: newlines are collapsed to one line', normalizeProvenance('run 7\nsha abc'), 'run 7 sha abc'); @@ -9014,15 +11879,15 @@ function selfTest() { t('H24: assigned but not queued is out of scope (H1/H2 own it)', h24QueuedWithAssignee(queued(['pm:dispatched'], ['os-elon'])), null); t('H24: neither -> clean', h24QueuedWithAssignee(queued(['domain:skills'], [])), null); t('H24: a missing issue does not crash', h24QueuedWithAssignee(undefined), null); - t('H24: …and the row names the login so residue and ownership are separable', h24QueuedWithAssignee(queued(['pm:queue'], ['yinlianghui'])).includes('`yinlianghui`'), true); - t('H24: every assignee is named, not just the first', h24QueuedWithAssignee(queued(['pm:queue'], ['os-elon', 'qq9340100'])).includes('`qq9340100`'), true); + t('H24: …and the row names the login so residue and ownership are separable', h24row(queued(['pm:queue'], ['yinlianghui'])).includes('`yinlianghui`'), true); + t('H24: every assignee is named, not just the first', h24row(queued(['pm:queue'], ['os-elon', 'qq9340100'])).includes('`qq9340100`'), true); t('H24: assignees given as plain logins are read too', typeof h24QueuedWithAssignee({ ...queued(['pm:queue']), assignees: ['os-elon'] }), 'string'); // The ruling's ORDER, pinned: the rule fires on a human assignment too, and // the sentence carries the asymmetric remedy rather than an exemption. t('H24: a human assignment still fires (exemption is a later explicit marker)', typeof h24QueuedWithAssignee(queued(['pm:queue'], ['yinlianghui'])), 'string'); - t('H24: …and the row refuses the human-clearing write', h24QueuedWithAssignee(queued(['pm:queue'], ['yinlianghui'])).includes('never be cleared by an agent'), true); - t('H24: …and names the paired write it is owed', h24QueuedWithAssignee(queued(['pm:queue'], ['os-elon'])).includes('同笔摘 assignee'), true); - t('H24: …and names both contradicting readers', h24QueuedWithAssignee(queued(['pm:queue'], ['os-elon'])).includes('dispatchable NOW'), true); + t('H24: …and the row refuses the human-clearing write', h24row(queued(['pm:queue'], ['yinlianghui'])).includes('never be cleared by an agent'), true); + t('H24: …and names the paired write it is owed', h24row(queued(['pm:queue'], ['os-elon'])).includes('同笔摘 assignee'), true); + t('H24: …and names both contradicting readers', h24row(queued(['pm:queue'], ['os-elon'])).includes('dispatchable NOW'), true); // The closed gate, in mirror image to H22's open gate: one card, one row. t('H24: a CLOSED queued+assigned card is H22 residue, not this row', h24QueuedWithAssignee(queued(['pm:queue'], ['os-elon'], { state: 'closed' })), null); t('H24: …and H22 does fire on that same card', typeof h22ClosedCardPmResidue(queued(['pm:queue'], ['os-elon'], { state: 'closed', state_reason: 'completed' })), 'string'); @@ -9041,25 +11906,25 @@ function selfTest() { // half-delivered branch must NOT (there the label and the claim are CORRECT). const pairedMerged = [{ number: 900, merged_at: '2026-08-22T10:00:00Z', body: 'Fixes #10638', head: { ref: 'x' } }]; const pairedOpenHalf = [{ number: 901, merged_at: null, draft: true, body: 'Part of #10638', head: { ref: 'y' } }]; - t('H8: the full-delivery remedy names 同笔摘 assignee', h8MergedPrStillDispatched(queued(['pm:dispatched'], ['os-elon']), pairedMerged, []).includes('同笔摘 assignee'), true); - t('H8: …and points at H24 as the state it prevents', h8MergedPrStillDispatched(queued(['pm:dispatched'], ['os-elon']), pairedMerged, []).includes('H24'), true); - t('H8: …and keeps the human-assignment refusal', h8MergedPrStillDispatched(queued(['pm:dispatched'], ['os-elon']), pairedMerged, []).includes('never cleared by an agent'), true); - t('H8: the HALF-delivered branch prescribes no assignee drop', h8MergedPrStillDispatched(queued(['pm:dispatched'], ['os-elon']), pairedMerged, pairedOpenHalf).includes('同笔摘 assignee'), false); - t('H8: …and still says the label is correct there', h8MergedPrStillDispatched(queued(['pm:dispatched'], ['os-elon']), pairedMerged, pairedOpenHalf).includes('must NOT be dropped'), true); - t('H19: the release text names 同笔摘 assignee', h19BlockOutlivedBlocker(queued(['pm:blocked']), [{ key: 'objectstack-ai/objectstack#2', number: 2, local: true, state: 'closed' }]).includes('同笔摘 assignee'), true); - t('H19: …on the unresolved branch too (one release contract, one sentence)', h19BlockOutlivedBlocker(queued(['pm:blocked']), [{ key: 'objectstack-ai/cloud#2', number: 2, local: false, state: 'unresolved', detail: 'HTTP 404' }]).includes('同笔摘 assignee'), true); + t('H8: the full-delivery remedy names 同笔摘 assignee', h8row(queued(['pm:dispatched'], ['os-elon']), pairedMerged, []).includes('同笔摘 assignee'), true); + t('H8: …and points at H24 as the state it prevents', h8row(queued(['pm:dispatched'], ['os-elon']), pairedMerged, []).includes('H24'), true); + t('H8: …and keeps the human-assignment refusal', h8row(queued(['pm:dispatched'], ['os-elon']), pairedMerged, []).includes('never cleared by an agent'), true); + t('H8: the HALF-delivered branch prescribes no assignee drop', h8row(queued(['pm:dispatched'], ['os-elon']), pairedMerged, pairedOpenHalf).includes('同笔摘 assignee'), false); + t('H8: …and still says the label is correct there', h8row(queued(['pm:dispatched'], ['os-elon']), pairedMerged, pairedOpenHalf).includes('must NOT be dropped'), true); + t('H19: the release text names 同笔摘 assignee', h19row(queued(['pm:blocked']), [{ key: 'objectstack-ai/objectstack#2', number: 2, local: true, state: 'closed' }]).includes('同笔摘 assignee'), true); + t('H19: …on the unresolved branch too (one release contract, one sentence)', h19row(queued(['pm:blocked']), [{ key: 'objectstack-ai/cloud#2', number: 2, local: false, state: 'unresolved', detail: 'HTTP 404' }]).includes('同笔摘 assignee'), true); // -- H25 + the `pm:awaiting-maintainer` vocabulary (#11196 fix 5) ----------- t('the ruled spelling is pm:-prefixed', AWAITING_MAINTAINER_LABEL, 'pm:awaiting-maintainer'); t('H25: awaiting + pm:queue -> finding', typeof h25AwaitingMaintainerExclusivity(queued([AWAITING_MAINTAINER_LABEL, 'pm:queue'])), 'string'); - t('H25: …and the row names the coexisting label', h25AwaitingMaintainerExclusivity(queued([AWAITING_MAINTAINER_LABEL, 'pm:queue'])).includes('`pm:queue`'), true); - t('H25: …and the specific lie, not a tidiness complaint', h25AwaitingMaintainerExclusivity(queued([AWAITING_MAINTAINER_LABEL, 'pm:on-hold'])).includes('Restart-when'), true); + t('H25: …and the row names the coexisting label', h25row(queued([AWAITING_MAINTAINER_LABEL, 'pm:queue'])).includes('`pm:queue`'), true); + t('H25: …and the specific lie, not a tidiness complaint', h25row(queued([AWAITING_MAINTAINER_LABEL, 'pm:on-hold'])).includes('Restart-when'), true); t('H25: awaiting ALONE -> clean', h25AwaitingMaintainerExclusivity(queued([AWAITING_MAINTAINER_LABEL])), null); t('H25: awaiting + a non-state label -> clean', h25AwaitingMaintainerExclusivity(queued([AWAITING_MAINTAINER_LABEL, 'domain:skills', 'priority:p0', 'pm:blocking'])), null); t('H25: no awaiting label -> out of scope however many states', h25AwaitingMaintainerExclusivity(queued(['pm:queue', 'pm:dispatched'])), null); t('H25: a CLOSED card is H22 residue, not a live exclusivity breach', h25AwaitingMaintainerExclusivity(queued([AWAITING_MAINTAINER_LABEL, 'pm:queue'], [], { state: 'closed' })), null); t('H25: a missing issue does not crash', h25AwaitingMaintainerExclusivity(undefined), null); - t('H25: several conflicts are ALL named', h25AwaitingMaintainerExclusivity(queued([AWAITING_MAINTAINER_LABEL, 'pm:queue', 'needs-user-decision'])).includes('`needs-user-decision`'), true); + t('H25: several conflicts are ALL named', h25row(queued([AWAITING_MAINTAINER_LABEL, 'pm:queue', 'needs-user-decision'])).includes('`needs-user-decision`'), true); for (const conflicting of AWAITING_MAINTAINER_EXCLUSIVE_LABELS) { t(`H25: awaiting + \`${conflicting}\` -> finding`, typeof h25AwaitingMaintainerExclusivity(queued([AWAITING_MAINTAINER_LABEL, conflicting])), 'string'); } @@ -9067,16 +11932,197 @@ function selfTest() { // the vocabulary cannot be half-added (the defect class this family is about). t('vocabulary: H13 treats awaiting as a real state -> clean', h13DomainWithoutPmState(domainCard(['domain:skills', AWAITING_MAINTAINER_LABEL], hoursAgo(200)), NOW), null); t('vocabulary: …while the same card without it is still H13', typeof h13DomainWithoutPmState(domainCard(['domain:skills'], hoursAgo(200)), NOW), 'string'); - t('vocabulary: H22 counts awaiting as residue on a closed card', h22ClosedCardPmResidue(closedCard([AWAITING_MAINTAINER_LABEL])).includes(`\`${AWAITING_MAINTAINER_LABEL}\``), true); + t('vocabulary: H22 counts awaiting as residue on a closed card', h22row(closedCard([AWAITING_MAINTAINER_LABEL])).includes(`\`${AWAITING_MAINTAINER_LABEL}\``), true); t('vocabulary: …and an open card carrying it is not H22 residue', h22ClosedCardPmResidue(queued([AWAITING_MAINTAINER_LABEL])), null); t('vocabulary: H11 sees awaiting as a PARKED state', typeof h11ImportantParked(parkedCard(['bug', AWAITING_MAINTAINER_LABEL]), NOW), 'string'); - t('vocabulary: …and names it as the parked state', h11ImportantParked(parkedCard(['bug', AWAITING_MAINTAINER_LABEL]), NOW).includes(`\`${AWAITING_MAINTAINER_LABEL}\``), true); - t('vocabulary: …with the exit this state actually has (no Restart-when re-check)', h11ImportantParked(parkedCard(['bug', AWAITING_MAINTAINER_LABEL]), NOW).includes('Restart-when'), false); - t('vocabulary: …and it says the state has no machine exit', h11ImportantParked(parkedCard(['bug', AWAITING_MAINTAINER_LABEL]), NOW).includes('NO machine exit'), true); - t('vocabulary: H11 keeps the mechanical remedy for a BLOCKED card', h11ImportantParked(parkedCard(['bug', 'pm:blocked']), NOW).includes('Restart-when'), true); + t('vocabulary: …and names it as the parked state', h11row(parkedCard(['bug', AWAITING_MAINTAINER_LABEL]), NOW).includes(`\`${AWAITING_MAINTAINER_LABEL}\``), true); + t('vocabulary: …with the exit this state actually has (no Restart-when re-check)', h11row(parkedCard(['bug', AWAITING_MAINTAINER_LABEL]), NOW).includes('Restart-when'), false); + t('vocabulary: …and it says the state has no machine exit', h11row(parkedCard(['bug', AWAITING_MAINTAINER_LABEL]), NOW).includes('NO machine exit'), true); + t('vocabulary: H11 keeps the mechanical remedy for a BLOCKED card', h11row(parkedCard(['bug', 'pm:blocked']), NOW).includes('Restart-when'), true); t('vocabulary: a fresh awaiting park is still clean', h11ImportantParked(parkedCard(['bug', AWAITING_MAINTAINER_LABEL], { created: daysAgo(2) }), NOW), null); t('vocabulary: an UNimportant awaiting card is not inventory', h11ImportantParked(parkedCard([AWAITING_MAINTAINER_LABEL]), NOW), null); + // -- H29 / H30 / H31: the half-state rule families of #11179 --------------- + // + // ⚠️ ASSERTION SHAPE. Every message assertion below goes through a helper + // that checks `typeof` FIRST and returns a describing STRING when the + // predicate has gone null. The neighbouring blocks assert + // `predicate(...).includes(...)` directly, which turns a regression that + // makes a row go silent into a `TypeError` crash at the first assertion + // instead of a named failing case — recorded as an observation on the + // sibling PR that landed H28, and honoured here for the new blocks rather + // than by restructuring the existing ones (that restructuring is a diff of + // its own, and this card must merge cleanly onto that PR). + const says = (msg, needle) => + typeof msg === 'string' ? msg.includes(needle) : `NO MESSAGE (${msg === null ? 'null' : typeof msg})`; + + // -- H29: the pm state labels are ONE-OF, generally ------------------------- + const h29 = (labels, extra = {}) => h29PmStateExclusivity(queued(labels, [], extra)); + // The vocabulary itself, pinned in both halves so it cannot be half-extended. + t('H29: the ONE-OF vocabulary is the awaiting state plus everything it excludes', PM_EXCLUSIVE_STATE_LABELS.length, 6); + t('H29: …and needs-user-decision is one of the states, not a side label', PM_EXCLUSIVE_STATE_LABELS.includes('needs-user-decision'), true); + t('H29: every state in the vocabulary has a claim clause', PM_EXCLUSIVE_STATE_LABELS.every((l) => typeof PM_STATE_CLAIM[l] === 'string' && PM_STATE_CLAIM[l].length > 0), true); + t('H29: …and the claim map declares nothing the vocabulary does not carry', Object.keys(PM_STATE_CLAIM).every((l) => PM_EXCLUSIVE_STATE_LABELS.includes(l)), true); + // The three `pm:*` sets are three different questions. Pinned pairwise so a + // later reader cannot unify them on the strength of the similar names — + // exactly the guard H22 already keeps against H13's set. + t('H29: the exclusivity set is NOT H13\'s visibility set', PM_EXCLUSIVE_STATE_LABELS.join(',') === PM_STATE_LABELS.join(','), false); + t('H29: …H13\'s carries `finding`, which is a card KIND, not a position', PM_STATE_LABELS.includes('finding') && !PM_EXCLUSIVE_STATE_LABELS.includes('finding'), true); + t('H29: …and the identity stickers legally coexist with a state', PM_EXCLUSIVE_STATE_LABELS.includes('pm:seat') || PM_EXCLUSIVE_STATE_LABELS.includes('pm:epic'), false); + t('H29: the exclusivity set is NOT H22\'s residue set', PM_EXCLUSIVE_STATE_LABELS.join(',') === PM_RESIDUE_LABELS.join(','), false); + t('H29: …pm:blocking is an annotation ON a state, so it is not exclusive', PM_RESIDUE_LABELS.includes('pm:blocking') && !PM_EXCLUSIVE_STATE_LABELS.includes('pm:blocking'), true); + t('H29: …while needs-user-decision is a position, so it IS exclusive', PM_EXCLUSIVE_STATE_LABELS.includes('needs-user-decision') && !PM_RESIDUE_LABELS.includes('needs-user-decision'), true); + // The two pairs this card was filed on. + t('H29: pm:queue + needs-user-decision -> finding', typeof h29(['pm:queue', 'needs-user-decision']), 'string'); + t('H29: …and it names BOTH claims, not just the labels', says(h29(['pm:queue', 'needs-user-decision']), 'a maintainer RULING is owed'), true); + t('H29: …and quotes the queue definition the pair contradicts', says(h29(['pm:queue', 'needs-user-decision']), '无可问之事'), true); + t('H29: pm:queue + pm:blocked -> finding', typeof h29(['pm:queue', 'pm:blocked']), 'string'); + t('H29: …and names the transition defect, not a tidiness complaint', says(h29(['pm:queue', 'pm:blocked']), 'ADD instead of a REPLACE'), true); + t('H29: …and prescribes ONE write', says(h29(['pm:queue', 'pm:blocked']), 'in a single write'), true); + t('H29: report-only — never a label from this script', says(h29(['pm:queue', 'pm:blocked']), 'never a label written from this script'), true); + t('H29: not a loud finding', isLoudFinding(h29(['pm:queue', 'pm:blocked'])), false); + // The live-board specimen, 2026-08-24: a THIRD pair, in a third direction. + t('H29 live: #11534 (needs-user-decision + pm:blocked) -> finding', typeof h29(['documentation', 'needs-user-decision', 'domain:devx', 'pm:blocked']), 'string'); + t('H29 live: …and names both live states', says(h29(['documentation', 'needs-user-decision', 'domain:devx', 'pm:blocked']), '`pm:blocked`') === true && says(h29(['documentation', 'needs-user-decision', 'domain:devx', 'pm:blocked']), '`needs-user-decision`') === true, true); + // Clean shapes. + t('H29: one state alone -> clean', h29(['pm:queue']), null); + t('H29: no state at all -> clean', h29(['domain:skills', 'bug']), null); + t('H29: non-state pm:* labels are not states', h29(['pm:queue', 'pm:blocking', 'pm:retriage', 'priority:p0', 'domain:spec']), null); + t('H29: a CLOSED card is H22 residue, not a live exclusivity breach', h29(['pm:queue', 'pm:blocked'], { state: 'closed' }), null); + t('H29: a missing issue does not crash', h29PmStateExclusivity(undefined), null); + t('H29: an absent state field is judged, not exempted', typeof h29(['pm:queue', 'pm:blocked'], { state: undefined }), 'string'); + // The two exclusions, each pinned in BOTH directions: silent here, and the + // owning row does fire on the same card. One breach, one row. + t('H29: pm:queue + pm:dispatched is H3\'s pair, not this row', h29(['pm:queue', 'pm:dispatched']), null); + t('H29: …and H3 does fire on it', h3QueueAndDispatched(queued(['pm:queue', 'pm:dispatched'])), true); + t('H29: any pair containing the awaiting state is H25\'s', h29([AWAITING_MAINTAINER_LABEL, 'pm:blocked']), null); + t('H29: …and H25 does fire on it', typeof h25AwaitingMaintainerExclusivity(queued([AWAITING_MAINTAINER_LABEL, 'pm:blocked'])), 'string'); + for (const other of AWAITING_MAINTAINER_EXCLUSIVE_LABELS) { + t(`H29: awaiting + \`${other}\` stays H25's row`, h29([AWAITING_MAINTAINER_LABEL, other]), null); + } + // …and an excluded pair does not silence the pairs no one else owns. + const three29 = h29(['pm:queue', 'pm:dispatched', 'pm:blocked']); + t('H29: three states -> the two pairs H3 does not own are still reported', typeof three29, 'string'); + t('H29: …naming queue + blocked', says(three29, '`pm:queue` (dispatchable NOW, with nothing left to ask) + `pm:blocked`'), true); + t('H29: …and dispatched + blocked', says(three29, '`pm:dispatched` (an agent is working it under a live claim) + `pm:blocked`'), true); + const awaitingPlusTwo = h29([AWAITING_MAINTAINER_LABEL, 'pm:queue', 'pm:blocked']); + t('H29: awaiting alongside two others still reports the pair H25 cannot', typeof awaitingPlusTwo, 'string'); + t('H29: …and does not re-report the awaiting pairs', says(awaitingPlusTwo, AWAITING_MAINTAINER_LABEL), false); + + // -- H30: a `pm:queue` card rotting unclaimed -------------------------------- + const queueCard = (labels, updatedAt, extra = {}) => ({ + number: 10534, + state: 'open', + labels: labels.map((name) => ({ name })), + assignees: [], + body: '', + title: '', + updated_at: updatedAt, + ...extra, + }); + const h30 = (labels, updatedAt, extra = {}) => h30QueueRotting(queueCard(labels, updatedAt, extra), NOW); + t('H30: queued and idle past the horizon -> finding', typeof h30(['pm:queue'], daysAgo(5)), 'string'); + t('H30: …and the row states the threshold it used', says(h30(['pm:queue'], daysAgo(5)), `threshold ${QUEUE_ROT_STALE_DAYS}d`), true); + t('H30: …and the measured age', says(h30(['pm:queue'], daysAgo(5)), '~5d'), true); + t('H30: …and asks for ONE explicit transition rather than a grade', says(h30(['pm:queue'], daysAgo(5)), 'ONE explicit transition'), true); + t('H30: …naming the decision route the queue definition implies', says(h30(['pm:queue'], daysAgo(5)), '无可问之事'), true); + t('H30: …and quotes the measured failure shape', says(h30(['pm:queue'], daysAgo(5)), '判断做了'), true); + t('H30: report-only — never a label from this script', says(h30(['pm:queue'], daysAgo(5)), 'never a label written from this script'), true); + t('H30: not a loud finding', isLoudFinding(h30(['pm:queue'], daysAgo(5))), false); + // The horizon's edges. + t('H30: under the horizon -> clean', h30(['pm:queue'], daysAgo(2)), null); + t('H30: exactly at the horizon -> clean (strictly beyond fires)', h30(['pm:queue'], daysAgo(QUEUE_ROT_STALE_DAYS)), null); + t('H30: just past it -> finding', typeof h30(['pm:queue'], daysAgo(QUEUE_ROT_STALE_DAYS + 0.5)), 'string'); + // #4690 direction: an unreadable stamp flags, never reads as fresh. + t('H30: unreadable updated_at -> finding, not fresh', typeof h30(['pm:queue'], 'not-a-date'), 'string'); + t('H30: absent updated_at -> finding, not fresh', typeof h30(['pm:queue'], undefined), 'string'); + t('H30: …and the row says the stamp is what it could not read', says(h30(['pm:queue'], undefined), 'unreadable `updated_at`'), true); + // Out of scope. + t('H30: not queued -> out of scope however old', h30(['pm:blocked'], daysAgo(200)), null); + t('H30: a CLOSED queued card is H22 residue', h30(['pm:queue'], daysAgo(200), { state: 'closed' }), null); + t('H30: a missing issue does not crash', h30QueueRotting(undefined, NOW), null); + // The live distribution the horizon was cut from (2026-08-24 board read): + // the three oldest fire, the ordinary queue depth stays quiet. + const NOW_11179 = Date.parse('2026-08-24T22:40:00Z'); + const live30 = (n, updatedAt) => h30QueueRotting({ ...queueCard(['pm:queue'], updatedAt), number: n }, NOW_11179); + t('H30 live: #9997, idle since 08-19 -> finding', typeof live30(9997, '2026-08-19T15:24:06Z'), 'string'); + t('H30 live: #7251, idle since 08-19 -> finding', typeof live30(7251, '2026-08-19T20:35:45Z'), 'string'); + t('H30 live: #10735, idle ~3.4d -> finding', typeof live30(10735, '2026-08-21T13:10:51Z'), 'string'); + t('H30 live: #11150, idle ~1.9d -> clean (queue depth is not rot)', live30(11150, '2026-08-23T00:52:13Z'), null); + t('H30 live: #11852, worked today -> clean', live30(11852, '2026-08-24T22:19:56Z'), null); + // Adjacency: the aged rows next door decline this shape, which is why it + // needed a row rather than a widening. + t('H30 adjacency: H24 is silent (the card has no assignee)', h24QueuedWithAssignee(queueCard(['pm:queue'], daysAgo(5))), null); + t('H30 adjacency: H18 is silent (no pm:retriage)', h18RetriageAged(queueCard(['pm:queue'], daysAgo(5)), NOW), null); + t('H30 adjacency: H11 is silent (pm:queue is not a PARKED state)', h11ImportantParked({ ...queueCard(['bug', 'pm:queue'], daysAgo(5)), created_at: daysAgo(30) }, NOW), null); + + // -- H31: the contract-review gate's two carriers --------------------------- + const gateCard = (labels, extra = {}) => ({ + number: 11427, + state: 'open', + labels: labels.map((name) => ({ name })), + assignees: [], + body: '', + title: '', + ...extra, + }); + const gatePr = (number, labels, extra = {}) => ({ + number, + merged_at: null, + draft: true, + body: 'Fixes #11427', + head: { ref: `claude/issue-11427-x` }, + labels: labels.map((name) => ({ name })), + ...extra, + }); + const bare = gatePr(11844, ['documentation', 'size/l', 'tests']); + const gated = gatePr(11844, ['documentation', 'size/l', CONTRACT_REVIEW_LABEL]); + t('H31: gated card + a bare delivering PR -> finding', typeof h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL, 'pm:dispatched']), [bare]), 'string'); + t('H31: …and it names the PR that is missing the carrier', says(h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [bare]), '#11844 (draft)'), true); + t('H31: …and names both failure routes (hang never reached / PASS stopped half way)', says(h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [bare]), 'already passed'), true); + t('H31: bare card + a gated delivering PR -> finding', typeof h31ContractReviewCarrierSplit(gateCard(['pm:dispatched']), [gated]), 'string'); + t('H31: …and calls that the more dangerous half', says(h31ContractReviewCarrierSplit(gateCard([]), [gated]), 'more dangerous half'), true); + t('H31: …because a stripped gate reads as a green light', says(h31ContractReviewCarrierSplit(gateCard([]), [gated]), '闸门被剥不是红灯是放行'), true); + t('H31: both directions carry the read-back contract', says(h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [bare]), 'READ-BACK'), true); + t('H31: report-only — never a label from this script', says(h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [bare]), 'never a label written from this script'), true); + t('H31: …naming the rule that forbids it', says(h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [bare]), '自查放行'), true); + t('H31: not a loud finding', isLoudFinding(h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [bare])), false); + // Agreement, both ways, is clean. + t('H31: both carriers gated -> clean', h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [gated]), null); + t('H31: neither carrier gated -> clean', h31ContractReviewCarrierSplit(gateCard(['pm:dispatched']), [bare]), null); + t('H31: one gated + one bare delivering PR still fires and names the bare one', says(h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [gated, gatePr(11845, [])]), '#11845'), true); + // The protocol's own intermediate state, NOT a finding: card-side first. + t('H31: gated card with NO delivering open PR -> clean (card-side-first is legal)', h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL, 'pm:blocked']), []), null); + t('H31: …and a PR that delivers some OTHER card does not start the comparison', h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [{ ...bare, body: 'Fixes #9999', head: { ref: 'claude/issue-9999-x' } }]), null); + // A merged carrier is a closed-out stroke, not a live half-write. + t('H31: a MERGED delivering PR is out of scope', h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [{ ...bare, merged_at: '2026-08-24T01:00:00Z' }]), null); + // #4690, in the direction that matters here: an unreadable carrier must not + // be read as a bare one, or a read failure manufactures a gate finding. + 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); + t('H31: …and one readable bare PR alongside it still fires', typeof h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [{ ...bare, labels: undefined }, gatePr(11845, [])]), 'string'); + // The delivery relation is H8's, shared rather than re-derived. + t('H31: the branch-name fallback delivers a body-silent PR', typeof h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), [{ ...bare, body: '' }]), 'string'); + t('H31: …and H8 reads the same PR as delivering the same card', prDeliversCard({ ...bare, body: '' }, '11427'), true); + t('H31: a CLOSED card is out of scope', h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL], { state: 'closed' }), [bare]), null); + t('H31: a missing issue does not crash', h31ContractReviewCarrierSplit(undefined, [bare]), null); + t('H31: an absent PR listing does not crash', h31ContractReviewCarrierSplit(gateCard([CONTRACT_REVIEW_LABEL]), undefined), null); + // The live specimen, 2026-08-24, byte-shaped: card #11427 gated, its + // delivering draft PR #11844 carrying every label EXCEPT the gate. + const live11427 = gateCard(['bug', 'pm:dispatched', 'domain:services', CONTRACT_REVIEW_LABEL]); + const live11844 = { + number: 11844, + merged_at: null, + draft: true, + body: 'Fixes #11427\n\nhydrate a tombstoned sys_file that still has a live holder', + head: { ref: 'claude/issue-11427-file-hydration-tombstone' }, + labels: ['documentation', 'size/l', 'dependencies', 'tests', 'tooling'].map((name) => ({ name })), + }; + t('H31 live: #11427 gated while its delivering PR #11844 is not -> finding', typeof h31ContractReviewCarrierSplit(live11427, [live11844]), 'string'); + t('H31 live: …and the row names the PR', says(h31ContractReviewCarrierSplit(live11427, [live11844]), '#11844 (draft)'), true); + // …and #10025, the other live carrier: gated, `pm:blocked`, no open PR at + // all — the shape this row deliberately does NOT report. + t('H31 live: #10025 (gated, no PR carrier yet) -> clean', h31ContractReviewCarrierSplit({ ...gateCard(['domain:services', 'pm:blocked', CONTRACT_REVIEW_LABEL]), number: 10025 }, [live11844]), null); + // -- The window arithmetic (#11118) ---------------------------------------- // The derivation is executable so that a cap and the sentence justifying it // cannot drift apart again: H8's docblock quoted `~18 merges/day` while the @@ -9126,10 +12172,10 @@ function selfTest() { ...extra, }); t('H26: target parked in pm:on-hold -> finding', typeof h26BlockOnIndefiniteTarget(waiting(), [tgt(987, ['pm:on-hold'])]), 'string'); - t('H26: …and the row says the block has no releasing mechanism', h26BlockOnIndefiniteTarget(waiting(), [tgt(987, ['pm:on-hold'])]).includes('NO MECHANISM THAT WILL EVER RELEASE IT'), true); - t('H26: …and names the target and its state', h26BlockOnIndefiniteTarget(waiting(), [tgt(987, ['pm:on-hold'])]).includes('`#987` (`pm:on-hold`)'), true); + t('H26: …and the row says the block has no releasing mechanism', h26row(waiting(), [tgt(987, ['pm:on-hold'])]).includes('NO MECHANISM THAT WILL EVER RELEASE IT'), true); + t('H26: …and names the target and its state', h26row(waiting(), [tgt(987, ['pm:on-hold'])]).includes('`#987` (`pm:on-hold`)'), true); t('H26: target parked in needs-user-decision -> finding', typeof h26BlockOnIndefiniteTarget(waiting(75), [tgt(68, ['needs-user-decision'])]), 'string'); - t('H26: a target carrying BOTH indefinite states names both', h26BlockOnIndefiniteTarget(waiting(), [tgt(987, ['pm:on-hold', 'needs-user-decision'])]).includes('`pm:on-hold` + `needs-user-decision`'), true); + t('H26: a target carrying BOTH indefinite states names both', h26row(waiting(), [tgt(987, ['pm:on-hold', 'needs-user-decision'])]).includes('`pm:on-hold` + `needs-user-decision`'), true); // The clean directions — an ordinary open target is not this row's business. t('H26: an ordinary open target -> clean', h26BlockOnIndefiniteTarget(waiting(), [tgt(987, ['pm:queue', 'domain:devx'])]), null); t('H26: an unlabelled open target -> clean', h26BlockOnIndefiniteTarget(waiting(), [tgt(987, [])]), null); @@ -9143,32 +12189,734 @@ function selfTest() { // An unresolved target is silent HERE and loud in H19 — one gap, one row. t('H26: an unresolved target is silent (H19 owns the unjudged sentence)', h26BlockOnIndefiniteTarget(waiting(), [{ ...tgt(987, null), state: 'unresolved', detail: 'HTTP 404' }]), null); t('H26: …and a labels-less open row cannot be judged either', h26BlockOnIndefiniteTarget(waiting(), [{ ...tgt(987, undefined) }]), null); - t('H26: …while H19 states that gap', h19BlockOutlivedBlocker(waiting(), [{ ...tgt(987, null), state: 'unresolved', detail: 'HTTP 404' }]).includes('UNJUDGED'), true); + t('H26: …while H19 states that gap', h19row(waiting(), [{ ...tgt(987, null), state: 'unresolved', detail: 'HTTP 404' }]).includes('UNJUDGED'), true); // The chain leg. t('H26: a target that is itself pm:blocked -> the transitive row', typeof h26BlockOnIndefiniteTarget(waiting(1395), [tgt(10101, ['pm:blocked'])]), 'string'); - t('H26: …and it says to look one level further', h26BlockOnIndefiniteTarget(waiting(1395), [tgt(10101, ['pm:blocked'])]).includes('TRANSITIVE'), true); - t('H26: …and does not claim the block can never release', h26BlockOnIndefiniteTarget(waiting(1395), [tgt(10101, ['pm:blocked'])]).includes('NO MECHANISM'), false); + t('H26: …and it says to look one level further', h26row(waiting(1395), [tgt(10101, ['pm:blocked'])]).includes('TRANSITIVE'), true); + t('H26: …and does not claim the block can never release', h26row(waiting(1395), [tgt(10101, ['pm:blocked'])]).includes('NO MECHANISM'), false); // Both legs at once, on two different targets, in one row. - const bothLegs = h26BlockOnIndefiniteTarget(waiting(), [tgt(987, ['pm:on-hold']), tgt(10101, ['pm:blocked'])]); + const bothLegs = String(h26BlockOnIndefiniteTarget(waiting(), [tgt(987, ['pm:on-hold']), tgt(10101, ['pm:blocked'])]) ?? ''); t('H26: both legs report together', bothLegs.includes('NO MECHANISM THAT WILL EVER RELEASE IT') && bothLegs.includes('TRANSITIVE'), true); // A target that is BOTH parked and blocked is named ONCE, under the reading // that ends the wait forever rather than the one that merely lengthens it. - const bothOnOne = h26BlockOnIndefiniteTarget(waiting(), [tgt(987, ['pm:on-hold', 'pm:blocked'])]); + const bothOnOne = String(h26BlockOnIndefiniteTarget(waiting(), [tgt(987, ['pm:on-hold', 'pm:blocked'])]) ?? ''); t('H26: a parked AND blocked target is named once, as indefinite', bothOnOne.includes('NO MECHANISM THAT WILL EVER RELEASE IT'), true); t('H26: …and not a second time as a chain', bothOnOne.includes('TRANSITIVE'), false); // A partially indefinite block still reports: one live blocker does not make // the indefinite one fireable. t('H26: one indefinite target among open ones still fires', typeof h26BlockOnIndefiniteTarget(waiting(), [tgt(900, ['pm:queue']), tgt(987, ['pm:on-hold'])]), 'string'); // The render cap, shared with H19 so one card cannot flood the anchor body. - const manyIndefinite = h26BlockOnIndefiniteTarget(waiting(), [1, 2, 3, 4, 5, 6, 7].map((n) => tgt(n, ['pm:on-hold']))); + const manyIndefinite = String(h26BlockOnIndefiniteTarget(waiting(), [1, 2, 3, 4, 5, 6, 7].map((n) => tgt(n, ['pm:on-hold']))) ?? ''); t('H26: the target list is capped like H19\'s', manyIndefinite.includes(`+${7 - H19_TARGET_LIST_CAP} more`), true); t('H26: …and still counts the full set', manyIndefinite.includes('on 7 target(s)'), true); // Cross-repo targets are addressed by full key, as in H19's rows. - t('H26: a cross-repo target is named owner/repo#N', h26BlockOnIndefiniteTarget(waiting(), [{ ...tgt(68, ['needs-user-decision']), local: false, key: 'objectstack-ai/objectos#68' }]).includes('`objectstack-ai/objectos#68`'), true); + t('H26: a cross-repo target is named owner/repo#N', h26row(waiting(), [{ ...tgt(68, ['needs-user-decision']), local: false, key: 'objectstack-ai/objectos#68' }]).includes('`objectstack-ai/objectos#68`'), true); // Both rows can fire on ONE card — different halves of one wait. const expiredAndIndefinite = [{ ...tgt(900, ['pm:queue']), state: 'closed' }, tgt(987, ['pm:on-hold'])]; t('H26 + H19: a partially expired, partially indefinite block fires both', Boolean(h19BlockOutlivedBlocker(waiting(), expiredAndIndefinite)) && Boolean(h26BlockOnIndefiniteTarget(waiting(), expiredAndIndefinite)), true); + // -- The UNGATED liveness read + H28: the stale body line (#11747) ---------- + // + // The fixture is the measured card's RECORDED BYTE SHAPE, not a sketch: body + // `Blocked-by: #9255` (closed 2026-08-19) plus the re-park's comment, which + // states the new blocker in the backticked-key spelling the census recorded + // (`Blocked-by:` #11501, open and dispatched). Both legs of the reverse + // verification run over these same bytes. + const REPARK_BODY = 'Blocked-by: #9255\n\nSome further prose about the card.'; + const REPARK_COMMENT = + 'PM re-park 2026-08-24: #9255 discharged, but the real prerequisite is carded.\n' + + '`Blocked-by:` #11501'; + const reparked = { ...issue(['pm:blocked'], [], REPARK_BODY), number: 9592, state: 'open' }; + const reparkKeys = (comments) => + blockerTargetsFor(reparked, comments, 'objectstack-ai/objectstack').map((t2) => t2.key).join(' '); + + // The GATE — the whole defect in two lines. H4's gate skips this card because + // its body carries a line; the liveness gate must NOT, or the live blocker is + // invisible to the only item that asks whether the wait is still running. + t('H28 gate: H4\'s gate skips a blocked card that HAS a body line', needsBlockedByComments(reparked), false); + t('H28 gate: …while the liveness gate reads it anyway', needsBlockerLivenessComments(reparked), true); + t('H28 gate: the liveness gate is the label alone, body-clean or not', needsBlockerLivenessComments(blockedCard(1, 'no line here')), true); + t('H28 gate: a card without the label buys no liveness fetch', needsBlockerLivenessComments(blockedCard(1, 'Blocked-by: #2', ['pm:queue'])), false); + t('H28 gate: a pm:blocking card is out of scope here too', needsBlockerLivenessComments(blockedCard(1, '', ['pm:blocking'])), false); + t('H28 gate: a missing issue does not crash', needsBlockerLivenessComments(undefined), false); + + // REVERSE VERIFICATION, both legs, over the recorded bytes. + // OLD behaviour = what the gate produced: the comment channel never reached + // the liveness read, so the card resolved ONE target and it was closed. + const asSwept = reparkKeys(undefined); + t('H28 repro (OLD, gated): only the stale body target is resolved', asSwept, 'objectstack-ai/objectstack#9255'); + const falseCandidate = String(h19BlockOutlivedBlocker(reparked, [target(9255, 'closed', { closedAt: '2026-08-19T11:28:26Z' })]) ?? ''); + t('H28 repro (OLD, gated): H19 publishes 1 of 1 CLOSED', falseCandidate.includes('1 of 1 `Blocked-by:` target(s)'), true); + t('H28 repro (OLD, gated): …as a FULL discharge — the false unlock candidate', falseCandidate.includes('Every target it names is closed'), true); + t('H28 repro (OLD, gated): …and never says PARTIAL', falseCandidate.includes('PARTIAL'), false); + // NEW behaviour = ungated: both channels, so the live blocker is resolved too. + const ungated = [target(9255, 'closed', { closedAt: '2026-08-19T11:28:26Z' }), target(11501, 'open')]; + t('H28 repro (NEW, ungated): both channels are unioned', reparkKeys([REPARK_COMMENT]), 'objectstack-ai/objectstack#9255 objectstack-ai/objectstack#11501'); + const partialNow = String(h19BlockOutlivedBlocker(reparked, ungated) ?? ''); + t('H28 repro (NEW, ungated): H19 reads 1 of 2', partialNow.includes('1 of 2 `Blocked-by:` target(s)'), true); + t('H28 repro (NEW, ungated): …and calls it a PARTIAL discharge', partialNow.includes('PARTIAL'), true); + t('H28 repro (NEW, ungated): …naming the live blocker as still open', partialNow.includes('`#11501`'), true); + t('H28 repro (NEW, ungated): …and no longer claims every target closed', partialNow.includes('Every target it names is closed'), false); + + // The PAIRED row — what ungating alone does not say. + const stale9592 = h28StaleBodyBlockerLine(reparked, ungated, [REPARK_COMMENT], 'objectstack-ai/objectstack'); + const stale9592Row = String(stale9592 ?? ''); + t('H28: the re-park shape fires', typeof stale9592, 'string'); + t('H28: …naming the spent BODY target', stale9592Row.includes('`#9255` (closed 2026-08-19T11:28:26Z)'), true); + t('H28: …and the live COMMENT target', stale9592Row.includes('`#11501`'), true); + t('H28: …calling the body line STALE', stale9592Row.includes('the body line is ') && stale9592Row.includes('STALE'), true); + t('H28: …and asking for the migration to the canonical home', stale9592Row.includes('rewrite the body line to name the live blocker'), true); + t('H28: …naming the re-park as the write that produced it', stale9592Row.includes('RE-PARK'), true); + t('H28: …and recording the false unlock candidate the gate used to publish', stale9592Row.includes('FALSE unlock candidate'), true); + t('H28: report-only, never a body written from this script', stale9592Row.includes('never a body or a label written from this script'), true); + // H19 and H28 fire TOGETHER on this card — different halves of one wait. + t('H28 + H19: both rows fire on the re-parked card', Boolean(partialNow) && Boolean(stale9592), true); + + // NEGATIVES — each half of the conjunction alone is a different state. + t('H28: a closed body target with NO live comment target is H19\'s row alone', h28StaleBodyBlockerLine(reparked, [target(9255, 'closed')], ['no line in this comment'], 'objectstack-ai/objectstack'), null); + t('H28: …and H19 does fire on it', typeof h19BlockOutlivedBlocker(reparked, [target(9255, 'closed')]), 'string'); + t('H28: an OPEN body target beside an open comment target -> no row', h28StaleBodyBlockerLine(reparked, [target(9255, 'open'), target(11501, 'open')], [REPARK_COMMENT], 'objectstack-ai/objectstack'), null); + // A target named in BOTH channels is already in the canonical home: there is + // nothing to migrate, so the closed-body half alone must not fire. + const bothChannels = { ...issue(['pm:blocked'], [], 'Blocked-by: #9255'), number: 9592 }; + t('H28: a target stated in both channels is not a migration candidate', h28StaleBodyBlockerLine(bothChannels, [target(9255, 'closed'), target(11501, 'open')], ['Blocked-by: #9255'], 'objectstack-ai/objectstack'), null); + // Unresolved targets are H19's unjudged sentence, never a migration order. + t('H28: an UNRESOLVED comment target is silent (a migration built on a guess)', h28StaleBodyBlockerLine(reparked, [target(9255, 'closed'), { ...target(11501, 'unresolved'), detail: 'HTTP 404' }], [REPARK_COMMENT], 'objectstack-ai/objectstack'), null); + t('H28: an UNRESOLVED body target is silent too', h28StaleBodyBlockerLine(reparked, [{ ...target(9255, 'unresolved'), detail: 'HTTP 404' }, target(11501, 'open')], [REPARK_COMMENT], 'objectstack-ai/objectstack'), null); + // The channel inputs the sweep can hand it, and the label gate. + t('H28: an unconsulted comment thread -> no row', h28StaleBodyBlockerLine(reparked, ungated, undefined, 'objectstack-ai/objectstack'), null); + t('H28: an unreadable comment thread -> no row (H4 owns that sentence)', h28StaleBodyBlockerLine(reparked, ungated, null, 'objectstack-ai/objectstack'), null); + t('H28: a body with no line at all -> no row (the comment IS the only home)', h28StaleBodyBlockerLine({ ...reparked, body: 'no line here' }, [target(11501, 'open')], [REPARK_COMMENT], 'objectstack-ai/objectstack'), null); + t('H28: no resolutions -> no row', h28StaleBodyBlockerLine(reparked, [], [REPARK_COMMENT], 'objectstack-ai/objectstack'), null); + t('H28: absent resolutions -> no row', h28StaleBodyBlockerLine(reparked, undefined, [REPARK_COMMENT], 'objectstack-ai/objectstack'), null); + t('H28: the label gate outranks the shape', h28StaleBodyBlockerLine({ ...reparked, labels: [{ name: 'pm:queue' }] }, ungated, [REPARK_COMMENT], 'objectstack-ai/objectstack'), null); + // The channel split reads the SAME spellings the union does — a split that + // recognised fewer would report migrations for targets nothing resolved. + t('H28 split: the plain body spelling', [...blockerChannelKeys('Blocked-by: #9255', reparked, 'objectstack-ai/objectstack')].join(' '), 'objectstack-ai/objectstack#9255'); + t('H28 split: the backticked-key comment spelling', [...blockerChannelKeys('`Blocked-by:` #11501', reparked, 'objectstack-ai/objectstack')].join(' '), 'objectstack-ai/objectstack#11501'); + t('H28 split: a cross-repo ref keeps its full key', [...blockerChannelKeys('Blocked-by: objectui#4356', reparked, 'objectstack-ai/objectstack')].join(' '), 'objectstack-ai/objectui#4356'); + t('H28 split: a self-reference is dropped, as in the union', [...blockerChannelKeys('Blocked-by: #9592', reparked, 'objectstack-ai/objectstack')].join(' '), ''); + t('H28 split: a mid-sentence prose mention is NOT a directive', [...blockerChannelKeys('the stated **Blocked-by: #9255** is discharged', reparked, 'objectstack-ai/objectstack')].join(' '), ''); + t('H28 split: no text at all is an empty set', blockerChannelKeys(undefined, reparked, 'objectstack-ai/objectstack').size, 0); + // Multiple stale/live targets are counted and capped like every other row. + const manyStale = String(h28StaleBodyBlockerLine( + { ...issue(['pm:blocked'], [], 'Blocked-by: #1\nBlocked-by: #2'), number: 9592 }, + [target(1, 'closed'), target(2, 'closed'), target(11501, 'open')], + [REPARK_COMMENT], + 'objectstack-ai/objectstack', + ) ?? ''); + t('H28: two stale body targets are counted', manyStale.includes('names 2 CLOSED'), true); + t('H28: …and the live one is still named', manyStale.includes('`#11501`'), true); + + // -- H32 — a HELD seat idle over a non-empty lane queue (#11706) ------------ + // + // Driven with the REAL seat-title shapes from the 2026-08-25 census (all 12 + // open seat posts), because every gate in this row is a read of that title + // and invented spellings would prove nothing about the population it runs on. + const NOW32 = Date.parse('2026-08-25T08:00:00Z'); + const minsAgo = (m) => new Date(NOW32 - m * 60_000).toISOString(); + const seat = (title, comments = 0) => ({ + ...issue(['pm:seat'], [], '', title), + number: 6017, + comments, + }); + const HELD = '[PM seat] domain:spec — 🟢 session_01NDGG54XF5gbTLdQzCtnaVV · R6 dispatch wave (batch:5)'; + const marker = (body, m) => ({ body, createdAt: minsAgo(m) }); + const busy = { unclaimed: 15, inFlight: 7 }; + const idleLane = { unclaimed: 15, inFlight: 0 }; + + // The lane parse, across the three measured title shapes. + t('H32 lane: a plain domain lane is own-board', seatLane(seat(HELD)).lane, 'domain:spec'); + t('H32 lane: …and not foreign', seatLane(seat(HELD)).foreign, false); + // ADAPTED FOR THIS REPO. `seatLane` compares the `@ ` suffix against + // the LIVE `SWEEP_REPO`, so which name is FOREIGN is install-dependent and + // upstream's two rows invert here. The property is identical — a sibling + // board's lane is unreadable from this sweep, this board's own lane is not. + t('H32 lane: an `@ sibling` suffix is FOREIGN', seatLane(seat('[PM seat] domain:devx @ objectstack — 🟢 os-x')).foreign, true); + t('H32 lane: an `@ own-repo` suffix is NOT foreign', seatLane(seat('[PM seat] domain:devx @ objectui — 🟢 os-x')).foreign, false); + t('H32 lane: …and keeps the bare lane label', seatLane(seat('[PM seat] domain:devx @ objectui — 🟢 os-x')).lane, 'domain:devx'); + t('H32 lane: a repo-scoped seat has no countable lane', seatLane(seat('[PM seat] repo:cloud — 🟢 os-x')).lane, null); + t('H32 lane: …and is foreign', seatLane(seat('[PM seat] repo:cloud — 🟢 os-x')).foreign, true); + t('H32 lane: a lane-less seat (skills) is foreign', seatLane(seat('[PM seat] skills — 🟢 os-zhuang (session_x)')).foreign, true); + t('H32 lane: the triage seat is foreign', seatLane(seat('[PM seat] triage (objectstack-wide) — 🟢 Routine')).foreign, true); + t('H32 lane: an unparseable title is foreign, never guessed', seatLane(seat('not a seat title')).foreign, true); + + // The held/vacant gate — an unheld seat is a ROUTING gap, never 怠工. + t('H32 held: 🟢 with a holder', seatIsHeld(seat(HELD)), true); + t('H32 held: ⏳ vacant is NOT held', seatIsHeld(seat('[PM seat] domain:devx @ objectui — ⏳ vacant')), false); + t('H32 held: 🔴 收班 vacant is NOT held', seatIsHeld(seat('[PM seat] domain:spec — 🔴 收班 vacant · 上一班 os-warren')), false); + t('H32 held: ⏸️ paused is NOT held', seatIsHeld(seat('[PM seat] domain:spec — ⏸️ paused')), false); + t('H32 held: a Routine seat is excluded (no claim cadence of its own)', seatIsHeld(seat('[PM seat] triage (objectstack-wide) — 🟢 Routine')), false); + + // The board halves — both required. + t('H32: held + idle lane + stale marker -> finding', typeof h32SeatIdleOverQueue(seat(HELD), marker('Round-start marker — R6.', 600), idleLane, NOW32), 'string'); + t('H32: …and it names the lane', h32row(seat(HELD), marker('Round-start marker — R6.', 600), idleLane, NOW32).includes('`domain:spec`'), true); + t('H32: …and the unclaimed count', h32row(seat(HELD), marker('Round-start marker — R6.', 600), idleLane, NOW32).includes('15 unclaimed'), true); + t('H32: work IN FLIGHT is a working seat -> clean', h32SeatIdleOverQueue(seat(HELD), marker('Round-start marker — R6.', 600), busy, NOW32), null); + t('H32: an EMPTY queue is a finished lane -> clean', h32SeatIdleOverQueue(seat(HELD), marker('Round-start marker — R6.', 600), { unclaimed: 0, inFlight: 0 }, NOW32), null); + t('H32: a vacant seat is out of scope however deep the queue', h32SeatIdleOverQueue(seat('[PM seat] domain:spec — ⏳ vacant'), marker('收班', 6000), idleLane, NOW32), null); + t('H32: a FOREIGN lane is out of scope (its inventory is unreadable here)', h32SeatIdleOverQueue(seat('[PM seat] domain:devx @ objectstack — 🟢 os-x'), marker('Round-start marker', 600), idleLane, NOW32), null); + t('H32: a non-seat card is out of scope', h32SeatIdleOverQueue({ ...issue(['pm:queue']), title: HELD }, marker('x', 600), idleLane, NOW32), null); + + // The threshold, at both edges of SEAT_IDLE_STALE_MINUTES. + t('H32: a marker inside the horizon -> clean', h32SeatIdleOverQueue(seat(HELD), marker('Round-start marker — R6.', SEAT_IDLE_STALE_MINUTES - 1), idleLane, NOW32), null); + t('H32: exactly at the horizon -> clean (strictly past it fires)', h32SeatIdleOverQueue(seat(HELD), marker('Round-start marker — R6.', SEAT_IDLE_STALE_MINUTES), idleLane, NOW32), null); + t('H32: one minute past -> finding', typeof h32SeatIdleOverQueue(seat(HELD), marker('Round-start marker — R6.', SEAT_IDLE_STALE_MINUTES + 1), idleLane, NOW32), 'string'); + // The measured cross-shift interval that must NOT fire: domain:engine's + // 收班 23:10 -> 开轮 01:47 pair, the shape the 480-min horizon was set above. + t('H32: a 收班/开轮 turnaround inside the horizon is clean', h32SeatIdleOverQueue(seat(HELD), marker('**开轮 / ROUND-OPEN marker** — domain:engine seat', 157), idleLane, NOW32), null); + + // The wait exemption — structural, unbounded by time (grading's ruling). + for (const [name, body] of [ + ['等 CI', '开轮 R7 — 在飞 0,等 CI 收敛后再派'], + ['等裁决', 'Round brief — 队列非空但全部等裁决'], + ['等人工步骤', '收班简报 — 剩余卡等人工步骤(手动 release)'], + ['awaiting', 'Round-open marker — every queued card is awaiting-maintainer'], + ['a named Blocked-by', 'Round brief — the whole queue is Blocked-by: #123'], + ['决策箱', '收班 — 队列 12,其中 12 在决策箱'], + ]) { + t(`H32 exempt: a marker naming ${name} silences the row`, h32SeatIdleOverQueue(seat(HELD), marker(body, 6000), idleLane, NOW32), null); + } + t('H32 exempt: …and the exemption is UNBOUNDED by time', h32SeatIdleOverQueue(seat(HELD), marker('等裁决', 60_000), idleLane, NOW32), null); + t('H32: a marker with no wait declared is NOT exempt', typeof h32SeatIdleOverQueue(seat(HELD), marker('Round brief — 10 PR landed.', 6000), idleLane, NOW32), 'string'); + t('H32 exempt: seatDeclaresWait is case-folded', seatDeclaresWait('Everything is AWAITING the maintainer'), true); + t('H32 exempt: …and plain prose declares nothing', seatDeclaresWait('Round 2 result — os-warren'), false); + + // The unread/unconsulted thread DECLINES — the one asymmetry against H4. + t('H32: an unconsulted thread declines to judge', h32SeatIdleOverQueue(seat(HELD), undefined, idleLane, NOW32), null); + t('H32: an UNREADABLE thread declines too (never a blind accusation)', h32SeatIdleOverQueue(seat(HELD), null, idleLane, NOW32), null); + t('H32: a seat with NO marker at all declines', h32SeatIdleOverQueue(seat(HELD), latestSeatMarker([]), idleLane, NOW32), null); + // An unreadable STAMP still fires — that is the #4690 direction, and it + // differs from an unreadable THREAD because the wait exemption was still read. + t('H32: an unreadable marker stamp must not read as fresh', typeof h32SeatIdleOverQueue(seat(HELD), { body: 'Round-start marker', createdAt: 'not-a-date' }, idleLane, NOW32), 'string'); + t('H32: …and the row says so rather than printing a number', h32row(seat(HELD), { body: 'Round-start marker', createdAt: 'not-a-date' }, idleLane, NOW32).includes('unreadable marker timestamp'), true); + + // latestSeatMarker — recency, with governingClaim's thread-order fallback. + t('H32 marker: the NEWEST comment wins', latestSeatMarker([{ body: 'old', created_at: minsAgo(600) }, { body: 'new', created_at: minsAgo(10) }]).body, 'new'); + t('H32 marker: …regardless of thread order', latestSeatMarker([{ body: 'new', created_at: minsAgo(10) }, { body: 'old', created_at: minsAgo(600) }]).body, 'new'); + t('H32 marker: an unparseable stamp falls back to thread order', latestSeatMarker([{ body: 'first', created_at: 'nope' }, { body: 'last', created_at: 'nope' }]).body, 'last'); + t('H32 marker: an empty thread is null', latestSeatMarker([]), null); + t('H32 marker: a non-array is null', latestSeatMarker(undefined), null); + t('H32 age: an unreadable stamp is null, not 0', seatMarkerAgeMinutes({ createdAt: 'nope' }, NOW32), null); + + // The gathering gate buys a fetch only for seats the row can speak about. + t('H32 gate: a held own-board seat is a candidate', h32NeedsSeatComments(seat(HELD)), true); + t('H32 gate: a foreign-lane seat buys no fetch', h32NeedsSeatComments(seat('[PM seat] domain:devx @ objectstack — 🟢 os-x')), false); + t('H32 gate: a vacant seat buys no fetch', h32NeedsSeatComments(seat('[PM seat] domain:spec — ⏳ vacant')), false); + t('H32 gate: a non-seat card buys no fetch', h32NeedsSeatComments(issue(['pm:queue'])), false); + + // Adjacency: H32 must not restate what H30/H24 already say about the cards. + t('H32 adjacency: H30 speaks about the CARD, H32 about the SEAT', h30QueueRotting(seat(HELD), NOW32), null); + + // -- H33 — a claim written before the ruling that now stands (#11724) ------- + const dispatched33 = (n = 4964) => ({ ...issue(['pm:dispatched'], ['os-zhuang']), number: n }); + const row33 = (createdAt, body) => ({ created_at: createdAt, body }); + const CLAIM_AT = '2026-08-20T10:00:00Z'; + const RULING_AT = '2026-08-21T09:00:00Z'; + const claimRow33 = row33(CLAIM_AT, 'Claim: skills seat `session_x`.\nBranch: `claude/issue-4964-x`'); + const rulingRow33 = row33(RULING_AT, 'Triage: lands in packages/rest — dispatch scope = option 1 as a sweep.'); + + t('H33: a claim predating a ruling -> finding', typeof h33ClaimPredatesRuling(dispatched33(), [claimRow33, rulingRow33]), 'string'); + t('H33: …and it names both stamps', h33row(dispatched33(), [claimRow33, rulingRow33]).includes(RULING_AT), true); + t('H33: a ruling BEFORE the claim -> clean (the order could carry it)', h33ClaimPredatesRuling(dispatched33(), [row33('2026-08-19T09:00:00Z', 'Triage: routed'), claimRow33]), null); + t('H33: no ruling on the thread at all -> clean (the ordinary card)', h33ClaimPredatesRuling(dispatched33(), [claimRow33, row33(RULING_AT, 'ACCEPT — PR #12066 reviewed')]), null); + t('H33: no claim at all -> clean', h33ClaimPredatesRuling(dispatched33(), [rulingRow33]), null); + t('H33: the label gate outranks the shape', h33ClaimPredatesRuling({ ...issue(['pm:queue']) }, [claimRow33, rulingRow33]), null); + t('H33: an unconsulted thread -> clean', h33ClaimPredatesRuling(dispatched33(), undefined), null); + t('H33: an unreadable thread -> clean', h33ClaimPredatesRuling(dispatched33(), null), null); + // The LATEST claim is what counts — a re-claim after the ruling clears it. + t('H33: a re-claim AFTER the ruling clears the row', h33ClaimPredatesRuling(dispatched33(), [claimRow33, rulingRow33, row33('2026-08-22T08:00:00Z', 'Claim: PM loop round R7')]), null); + // Both stamps must be readable — an ordering built on an unreadable stamp + // would be fabricated, which is worse than a missing row. + t('H33: an unreadable CLAIM stamp declines', h33ClaimPredatesRuling(dispatched33(), [row33('nope', 'Claim: PM loop round R6'), rulingRow33]), null); + t('H33: an unreadable RULING stamp declines', h33ClaimPredatesRuling(dispatched33(), [claimRow33, row33('nope', 'Triage: routed')]), null); + // Multiple rulings are counted, and the NEWEST is the one named. + const twoRulings = String(h33ClaimPredatesRuling(dispatched33(), [claimRow33, rulingRow33, row33('2026-08-22T09:00:00Z', 'Maintainer ruling — option 1 stands')]) ?? ''); + t('H33: two later rulings are counted', twoRulings.includes('2 triage-ruling comment(s)'), true); + t('H33: …and the NEWEST is the one quoted', twoRulings.includes('2026-08-22T09:00:00Z'), true); + + // The ruling anchors, driven with the measured openings (2026-08-25 census). + for (const [name, line] of [ + ['Triage:', 'Triage: lands in packages/rest/src/rest-server.ts; domain:cli.'], + ['Triage routing:', 'Triage routing: domain:skills + finding — pm-dispatch process defect'], + ['Triage (first-touch grading):', 'Triage (first-touch grading): graduated finding → pm:blocked'], + ['Triage (Routine seat…)', 'Triage (Routine seat, hourly round): routed → finding + domain:skills'], + ['Concentrated triage batch:', 'Concentrated triage batch: finding → pm:queue + domain:engine, Task, M'], + ['Concentrated triage round:', 'Concentrated triage round: finding → pm:queue + domain:engine stands'], + ['Skills-lane self-triage', 'Skills-lane self-triage (run-to-empty fire): finding → pm:queue'], + ['Grading (…)', 'Grading (skills seat, session `session_x`, 2026-08-23 concentrated round): promoted'], + ['Maintainer ruling —', 'Maintainer ruling — option 1: every failure payload carries the advisory lists'], + ]) { + t(`H33 anchor: ${name} is a ruling`, isTriageRulingComment(line), true); + } + // Decoration is expected — seats bold and blockquote these openings. + t('H33 anchor: a BOLDED ruling opening still reads', isTriageRulingComment('**Triage: routed → pm:queue**'), true); + t('H33 anchor: a BLOCKQUOTED one too', isTriageRulingComment('> Triage routing: domain:skills'), true); + t('H33 anchor: a heading-marked one too', isTriageRulingComment('## Grading (skills seat)'), true); + t('H33 anchor: leading blank lines are skipped', isTriageRulingComment('\n\n Triage: routed'), true); + // …and the strictness the tolerance must NOT cost. Both specimens are real + // comments from the same census that MENTION a ruling without being one. + t('H33 anchor: prose mentioning the triage comment is NOT a ruling', isTriageRulingComment('Serial-constraint addendum to the R6 claim above: the triage comment says'), false); + t('H33 anchor: a correction referencing a ruling is NOT one', isTriageRulingComment("⚠️ Section 3's REST-fallback claim needs qualifying before it lands"), false); + t('H33 anchor: a dev report is NOT a ruling', isTriageRulingComment('os-dev-report'), false); + t('H33 anchor: an ACCEPT is NOT a ruling', isTriageRulingComment('ACCEPT — PR #12066 · reviewed against GitHub'), false); + t('H33 anchor: a claim is NOT a ruling', isTriageRulingComment('Claim: PM loop round R6'), false); + t('H33 anchor: empty text is not a ruling', isTriageRulingComment(''), false); + + // latestClaimComment — deliberately NOT branch-gated, unlike governingClaim. + const branchless = row33('2026-08-21T10:00:00Z', 'Claim: PM loop round R6'); + t('H33 claim: a BRANCHLESS claim still counts here', latestClaimComment([branchless]).createdAt, '2026-08-21T10:00:00Z'); + t('H33 claim: …while governingClaim correctly ignores it', governingClaim([branchless]), null); + t('H33 claim: the newest claim wins', latestClaimComment([claimRow33, branchless]).createdAt, '2026-08-21T10:00:00Z'); + t('H33 claim: a thread with no claim is null', latestClaimComment([rulingRow33]), null); + t('H33 claim: a non-array is null', latestClaimComment(null), null); + // ⚠️ A KNOWN, MEASURED blind spot, pinned here so it is visible in the suite + // rather than discovered again from a silent row. `CLAIM_COMMENT_MARKER` + // requires the canonical colon, and some live `pm:dispatched` cards carry a + // claim written with an EM DASH (「Claim — skills seat `session_…`」), which + // the marker cannot see. ⛔ Those are MALFORMED claims, not a dialect: the + // 2026-08-11 maintainer ruling makes `Claim:` the single machine criterion + // and closes the widening (⛔ 不放宽谓词). H33 therefore inherits the + // blindness rather than defining a second, wider notion of "claim" than + // H2/H20/H27 use — a file that disagrees with itself about what a claim IS + // would be the worse defect. The consequence is UNDER-reporting, this file's + // standing direction for an unrecognised spelling (H17/H20), never a + // fabricated row. Where the malformed claim becomes VISIBLE is H34 (#12090), + // whose cases sit below. + t('H33 claim: an EM-DASH claim is invisible to the shared marker (malformed, by ruling)', latestClaimComment([row33(CLAIM_AT, 'Claim — skills seat `session_x`.')]), null); + t('H33: …so a card claimed that way under-reports rather than fabricating', h33ClaimPredatesRuling(dispatched33(), [row33(CLAIM_AT, 'Claim — skills seat `session_x`.'), rulingRow33]), null); + + // -- H34 — a claim-shaped comment with a non-canonical separator (#12090) --- + // The acceptance pair for this row is the FOUR-WAY split below: the marker's + // verdict and H34's verdict are asserted on the same specimen every time, so + // no case can go green by quietly redefining what a claim is. + const tracked34 = (labels = ['pm:dispatched']) => issue(labels, ['os-zhuang']); + // ⚠️ `?? ''` rather than a bare `.includes` on the predicate's return. A row + // that goes NULL under a mutation must report a NAMED failing case, not throw + // and abort the suite before the remaining 200 cases run — measured while + // reverse-verifying this row: widening the marker turned a red suite into a + // TypeError whose message named neither the row nor the mutation. + const h34row = (...args) => String(h34ClaimShapedNonCanonicalSeparator(...args) ?? ''); + const DASH_CLAIM = 'Claim — skills seat `session_01RM`. Folded dispatch, patrol-predicates pack.'; + const COLON_CLAIM = 'Claim: skills seat `session_01RM`.\nBranch: `claude/issue-12090-x`'; + const PROSE_CONTROL = 'the next seat should claim: only after the ruling lands'; + const FULLWIDTH_CLAIM = 'Claim:skills seat `session_01RM`, round R36.'; + + // 1. dash claim -> the near-miss row fires AND the marker stays false. + t('H34: an EM-DASH claim fires the near-miss row', typeof h34ClaimShapedNonCanonicalSeparator(tracked34(), [DASH_CLAIM]), 'string'); + t('H34: …and CLAIM_COMMENT_MARKER is still false on it (⛔ 不放宽谓词)', CLAIM_COMMENT_MARKER.test(DASH_CLAIM), false); + t('H34: …and the row NAMES the separator by codepoint', h34row(tracked34(), [DASH_CLAIM]).includes('EM DASH (U+2014)'), true); + t('H34: …and points the remedy at the canonical spelling', h34row(tracked34(), [DASH_CLAIM]).includes('begins with the literal `Claim:`'), true); + t('H34: …and says the predicate is NOT widened', h34row(tracked34(), [DASH_CLAIM]).includes('NOT widened'), true); + // …and H2 still reports the card, correctly, as claimless. + t('H34: H2 still fires on the same card, and that row is CORRECT', h2AssigneeNoClaimComment(tracked34(), [DASH_CLAIM]), true); + + // 2. colon claim -> the marker is true and the near-miss row is silent. + t('H34: a COLON claim leaves the near-miss row silent', h34ClaimShapedNonCanonicalSeparator(tracked34(), [COLON_CLAIM]), null); + t('H34: …because the marker reads it', CLAIM_COMMENT_MARKER.test(COLON_CLAIM), true); + t('H34: …and H2 is clean', h2AssigneeNoClaimComment(tracked34(), [COLON_CLAIM]), false); + + // 3. the #7488 prose control -> NEITHER reader sees a claim. + t('H34: the prose control fires neither reader (H34)', h34ClaimShapedNonCanonicalSeparator(tracked34(), [PROSE_CONTROL]), null); + t('H34: …nor the marker', CLAIM_COMMENT_MARKER.test(PROSE_CONTROL), false); + + // 4. fullwidth colon -> a near miss, which is what the collapsed `[::]` class + // makes honest: U+FF1A never matched the marker and now says so out loud. + t('H34: a FULLWIDTH-COLON claim fires the near-miss row', typeof h34ClaimShapedNonCanonicalSeparator(tracked34(), [FULLWIDTH_CLAIM]), 'string'); + t('H34: …named as such', h34row(tracked34(), [FULLWIDTH_CLAIM]).includes('FULLWIDTH COLON (U+FF1A)'), true); + t('H34: …and the marker has never matched it (behaviour unchanged by #12090)', CLAIM_COMMENT_MARKER.test(FULLWIDTH_CLAIM), false); + + // The remaining separators, each pinned by name. + t('H34: an EN DASH is a near miss', nearMissClaimSeparators('Claim – skills seat, session 019x').join(','), 'EN DASH (U+2013)'); + t('H34: a HYPHEN-MINUS is a near miss', nearMissClaimSeparators('Claim - skills seat, session 019x').join(','), 'HYPHEN-MINUS (U+002D)'); + t('H34: `Claimed —` is one too', nearMissClaimSeparators('Claimed — by the skills seat, session 019x').join(','), 'EM DASH (U+2014)'); + t('H34: a BLOCKQUOTED near miss reads (the template is a blockquote)', nearMissClaimSeparators('> Claim — skills seat, session 019x').join(','), 'EM DASH (U+2014)'); + + // ⚠️ The `[ \t]*` decision, pinned TWICE and deliberately: once on the + // exported regex (where the character class is what answers) and once on the + // reader (where the per-line split answers). A single case would let the + // regex be relaxed to `\s*` with the suite still green — measured: the + // reader-level case alone survives that mutation untouched. + t('H34: the exported marker does not span lines (the class, not the split)', CLAIM_NEAR_MISS_MARKER.test('Claim\n- skills seat, session 019x'), false); + t('H34: a markdown bullet on the NEXT line is not a separator', nearMissClaimSeparators('Claim\n- skills seat, session 019x').length, 0); + t('H34: …the same shape with a colon claim is unaffected', CLAIM_COMMENT_MARKER.test('Claim\n- skills seat'), false); + + // The conservative content half: a claim-shaped opening with none of the + // protocol's own content is NOT evidence that a claim was attempted. + t('H34: a contentless claim-shaped line is silent', nearMissClaimSeparators('Claim - see above').length, 0); + t('H34: …but a `Branch:` line elsewhere in the comment is content', nearMissClaimSeparators('Claim - see above\nBranch: `claude/issue-12090-x`').join(','), 'HYPHEN-MINUS (U+002D)'); + + // Suppression: a thread that ALSO carries a readable claim is machine-visible, + // so the row has no remedy to offer and stays quiet. + t('H34: a thread carrying BOTH spellings is silent', h34ClaimShapedNonCanonicalSeparator(tracked34(), [DASH_CLAIM, COLON_CLAIM]), null); + // Gates, mirroring H2's exactly — same population, same declines. + t('H34: `pm:queue` is in scope too', typeof h34ClaimShapedNonCanonicalSeparator(tracked34(['pm:queue']), [DASH_CLAIM]), 'string'); + t('H34: an untracked card is out of scope', h34ClaimShapedNonCanonicalSeparator(issue(['domain:skills'], ['os-zhuang']), [DASH_CLAIM]), null); + t('H34: an UNASSIGNED card is out of scope (H1 owns that card)', h34ClaimShapedNonCanonicalSeparator(issue(['pm:dispatched']), [DASH_CLAIM]), null); + t('H34: an unconsulted thread declines', h34ClaimShapedNonCanonicalSeparator(tracked34(), undefined), null); + t('H34: an unreadable thread declines', h34ClaimShapedNonCanonicalSeparator(tracked34(), null), null); + t('H34: an empty thread is clean', h34ClaimShapedNonCanonicalSeparator(tracked34(), []), null); + t('H34: a null body among the comments does not throw', h34ClaimShapedNonCanonicalSeparator(tracked34(), [null, DASH_CLAIM]) !== null, true); + // Two malformed comments, two separators, counted and de-duplicated. + const twoNearMiss = h34row(tracked34(), [DASH_CLAIM, 'Claim – other seat, session 019y']); + t('H34: multiple malformed comments are counted', twoNearMiss.includes('2 comment(s)'), true); + t('H34: …and both separators named', twoNearMiss.includes('EM DASH (U+2014) + EN DASH (U+2013)'), true); + t('H34: a separator is named once, not per comment', h34row(tracked34(), [DASH_CLAIM, DASH_CLAIM]).includes('EM DASH (U+2014) + EM DASH'), false); + // No `g` flag on the shared marker — the state bug the colon marker's header + // names, asserted here because this regex is exported and reused per line. + t('H34: the near-miss marker carries no `g` flag', CLAIM_NEAR_MISS_MARKER.global, false); + t('H34: …so repeated reads of one line agree', nearMissClaimSeparators(DASH_CLAIM).join() === nearMissClaimSeparators(DASH_CLAIM).join(), true); + + // -- H35 — a gate label removed with no matching review-chain evidence ----- + // -- (#11881). Fixtures are event rows in the repo-wide stream's shape. + const gateEvent = (over, { n, pr = false, actor = 'os-seat', at, labels = [] }) => ({ + event: over, + label: { name: CONTRACT_REVIEW_LABEL }, + actor: { login: actor }, + created_at: at, + issue: { + number: n, + state: 'open', + labels: labels.map((name) => ({ name })), + pull_request: pr ? { url: 'x' } : undefined, + html_url: `https://example.invalid/${n}`, + }, + }); + // The delivery relation, stubbed: card 900 <-> PR 901. + const sib900 = (e) => (e.issue.pull_request ? [900] : [901]); + const o900 = { siblingNumbers: sib900 }; + const h35row = (...args) => String(h35GateRemovalWithoutEvidence(...args) ?? ''); + + // The healthy shape: hung across both carriers, cleared across both. + const dualHang = [ + gateEvent('labeled', { n: 900, at: '2026-08-26T10:00:00Z' }), + gateEvent('labeled', { n: 901, pr: true, at: '2026-08-26T10:00:02Z' }), + ]; + const dualClear = [ + gateEvent('unlabeled', { n: 900, at: '2026-08-26T12:00:00Z' }), + gateEvent('unlabeled', { n: 901, pr: true, at: '2026-08-26T12:00:03Z' }), + ]; + const healthy = [...dualHang, ...dualClear]; + t('H35: a dual-carrier clear is the review-chain evidence -> silent', h35RemovalVerdict(dualClear[0], healthy, o900), 'paired'); + t('H35: …and emits no row', h35GateRemovalWithoutEvidence(dualClear[0], healthy, o900), null); + t('H35: …on the PR carrier too', h35RemovalVerdict(dualClear[1], healthy, o900), 'paired'); + + // ⭐ THE FINDING: hung in a dual stroke, cleared on ONE carrier only. + const halfWrite = [...dualHang, dualClear[0]]; + t('H35: hung dual + cleared lone -> half-write', h35RemovalVerdict(dualClear[0], halfWrite, o900), 'half-write'); + t('H35: …and the row fires', typeof h35GateRemovalWithoutEvidence(dualClear[0], halfWrite, o900), 'string'); + t('H35: …naming the carrier it was removed from', h35row(dualClear[0], halfWrite, o900).includes('REMOVED from this CARD'), true); + t('H35: …and the actor', h35row(dualClear[0], halfWrite, o900).includes('`os-seat`'), true); + t('H35: …and the failure direction is toward release', h35row(dualClear[0], halfWrite, o900).includes('闸门被剥不是红灯是放行'), true); + t('H35: …and the remedy is a READ, not a re-hang', h35row(dualClear[0], halfWrite, o900).includes('Remedy is a READ, not a write'), true); + t('H35: …and it states the report-only posture', h35row(dualClear[0], halfWrite, o900).includes('never a label written from this script'), true); + // The half-write class is NOT narrowed to open carriers — a gate cleared half + // way on a PR that then merged is the bypass that already happened. + const closedCarrier = { ...dualClear[0], issue: { ...dualClear[0].issue, state: 'closed' } }; + t('H35: a half-write on a CLOSED carrier still reports', typeof h35GateRemovalWithoutEvidence(closedCarrier, [...dualHang, closedCarrier], o900), 'string'); + + // The single-carrier gate — the measured majority shape, and NOT a finding. + const loneHang = gateEvent('labeled', { n: 900, at: '2026-08-26T10:00:00Z' }); + const loneClear = gateEvent('unlabeled', { n: 900, at: '2026-08-26T12:00:00Z' }); + const singleCarrier = [loneHang, loneClear]; + t('H35: hung lone + cleared lone -> unjudgeable, never a violation', h35RemovalVerdict(loneClear, singleCarrier, o900), 'unjudgeable'); + t('H35: …and the row says UNJUDGED rather than clean', h35row(loneClear, singleCarrier, o900).includes('UNJUDGED, not clean'), true); + t('H35: …and names both producer-side repairs', h35row(loneClear, singleCarrier, o900).includes('PR 一存在即挂'), true); + t('H35: …and refuses to parse the prose verdict', h35row(loneClear, singleCarrier, o900).includes('declines to parse it'), true); + // …but only while the carrier is live: the narrowing that keeps this class at + // ~0.22 rows/run instead of ~8.5 (header's measured figures). + const closedLone = { ...loneClear, issue: { ...loneClear.issue, state: 'closed' } }; + t('H35: an unjudgeable clear on a CLOSED carrier emits no row', h35GateRemovalWithoutEvidence(closedLone, [loneHang, closedLone], o900), null); + t('H35: …though it still classifies as unjudgeable', h35RemovalVerdict(closedLone, [loneHang, closedLone], o900), 'unjudgeable'); + + // Re-hung: the read-back worked. Reporting it would call the control a defect. + const rehung = gateEvent('unlabeled', { n: 900, at: '2026-08-26T12:00:00Z', labels: [CONTRACT_REVIEW_LABEL] }); + t('H35: a removal whose label is BACK -> rehung, silent', h35RemovalVerdict(rehung, [...dualHang, rehung], o900), 'rehung'); + t('H35: …and emits no row', h35GateRemovalWithoutEvidence(rehung, [...dualHang, rehung], o900), null); + + // Three input states, never two (#4690): no hang in the window = decline. + t('H35: a removal with no hang in the window -> undated, not a finding', h35RemovalVerdict(dualClear[0], [dualClear[0]], o900), 'undated'); + t('H35: …and emits no row', h35GateRemovalWithoutEvidence(dualClear[0], [dualClear[0]], o900), null); + // An unresolvable sibling must not manufacture a finding: it degrades to the + // hang comparison, which for a lone hang is `unjudgeable`. + t('H35: an unresolvable sibling degrades, never accuses', h35RemovalVerdict(loneClear, singleCarrier, { siblingNumbers: () => null }), 'unjudgeable'); + + // Scope: only `unlabeled`, only gate-semantic labels. + t('H35: a `labeled` row is not applicable', h35RemovalVerdict(dualHang[0], healthy, o900), 'not-applicable'); + const otherLabel = { ...loneClear, label: { name: 'size/l' } }; + t('H35: a non-gate label is not applicable', h35RemovalVerdict(otherLabel, [otherLabel], o900), 'not-applicable'); + t('H35: `needs-user-decision` is deliberately NOT in the family', isGateSemanticLabel('needs-user-decision'), false); + t('H35: the gate label IS', isGateSemanticLabel(CONTRACT_REVIEW_LABEL), true); + t('H35: the family and H31 share ONE constant', GATE_SEMANTIC_LABELS.includes(CONTRACT_REVIEW_LABEL), true); + + // ⚠️ THE VACUITY GUARD. H35's half-write class measured ZERO over the 3.41-day + // derivation corpus — a true reading of a board where the dual-carrier + // discipline holds, and indistinguishable from a predicate that CANNOT fire. + // These two cases are the difference, and they must be read as a pair: the + // classifier reaches `half-write` on a constructed input, and a mutation that + // makes the row go permanently silent turns them red HERE rather than passing + // as a quiet board. ⛔ Do not delete either one to make an ablation quieter. + t('H35 vacuity guard: the half-write class is REACHABLE', h35RemovalVerdict(dualClear[0], halfWrite, o900) === 'half-write', true); + t('H35 vacuity guard: …and produces a non-empty row', h35row(dualClear[0], halfWrite, o900).length > 0, true); + // The stroke window is a threshold read out of an EMPTY region of the measured + // distribution (101s .. 275s), so these two pin both of its sides. + const slowPair = [ + ...dualHang, + dualClear[0], + gateEvent('unlabeled', { n: 901, pr: true, at: '2026-08-26T12:01:30Z' }), + ]; + t('H35: a 90s dual clear is still ONE stroke (inside the 120s window)', h35RemovalVerdict(dualClear[0], slowPair, o900), 'paired'); + const hoursApart = [ + ...dualHang, + dualClear[0], + gateEvent('unlabeled', { n: 901, pr: true, at: '2026-08-26T15:00:00Z' }), + ]; + t('H35: a clear hours later is NOT the same stroke', h35RemovalVerdict(dualClear[0], hoursApart, o900), 'half-write'); + // 「同笔」 is one actor's stroke — a different login is not the same write. + const otherActor = [ + ...dualHang, + dualClear[0], + gateEvent('unlabeled', { n: 901, pr: true, actor: 'os-other', at: '2026-08-26T12:00:03Z' }), + ]; + t('H35: a different actor is not 同笔', h35RemovalVerdict(dualClear[0], otherActor, o900), 'half-write'); + + // The window derivation, executable rather than prose (H8's `windowCoverageDays` shape). + t('H35: the horizon is TWO patrol cycles', H35_EVENT_WINDOW_HOURS / PATROL_CADENCE_HOURS, 2); + t('H35: 12h at the measured rate needs 24 pages', eventWindowPages(), 24); + t('H35: …and the cap leaves headroom above that', H35_EVENT_PAGE_CAP > eventWindowPages(), true); + t('H35: a zero rate cannot divide, and says so', eventWindowPages(12, 0), null); + t('H35: the stroke window sits inside the measured empty region', H35_SAME_STROKE_SECONDS > 101 && H35_SAME_STROKE_SECONDS < 275, true); + // The window filter is a TIME horizon; the page cap is only its backstop. + t('H35: gateLabelEvents keeps both verbs', gateLabelEvents(healthy).length, 4); + t('H35: …and drops non-gate labels', gateLabelEvents([...healthy, otherLabel]).length, 4); + t('H35: …and drops non-label events', gateLabelEvents([...healthy, { event: 'closed' }]).length, 4); + + // The summary line carries the residue, so a quiet section cannot read as + // "the gate is watched" when most removals are structurally unwatchable. + const gateCounts = { gateRemovals: 7, eventPages: 24, gate_unjudgeable: 5, gate_undated: 1 }; + t('H35 summary: the removal count is reported', summaryLine(gateCounts, 1).includes('7 removal(s) of a gate-semantic label'), true); + t('H35 summary: …with the pages read', summaryLine(gateCounts, 1).includes('24 page(s) of the repo-wide issue-event stream'), true); + t('H35 summary: …and states it made no per-card fetch', summaryLine(gateCounts, 1).includes('no per-card timeline fetch'), true); + t('H35 summary: …and carries the unjudgeable residue', summaryLine(gateCounts, 1).includes('5 of them are UNJUDGEABLE'), true); + t('H35 summary: …and the undated count', summaryLine(gateCounts, 1).includes('1 more had no hang inside the window'), true); + t('H35 summary: a truncated window is announced, never silent', summaryLine({ ...gateCounts, eventWindowTruncated: true }, 0).includes('TRUNCATED'), true); + t('H35 summary: …and says a quiet section is a SHORT READ', summaryLine({ ...gateCounts, eventWindowTruncated: true }, 0).includes('short read, not a clean board'), true); + t('H35 summary: an untruncated window makes no such claim', summaryLine(gateCounts, 1).includes('TRUNCATED'), false); + // Absent counts degrade to 0, never to `undefined` — H32's pair does the same. + t('H35 summary: absent counts degrade to 0', summaryLine({}, 0).includes('0 removal(s) of a gate-semantic label'), true); + + // -- H36 — cross-lane same-file holds (report-only patrol input, #12286) --- + const h36pr = (number, opts = {}) => ({ + number, + created_at: opts.created_at ?? '2026-08-25T10:00:00Z', + draft: opts.draft ?? true, + auto_merge: opts.auto_merge ?? null, + merged_at: opts.merged_at ?? null, + head: { ref: opts.ref ?? `claude/issue-${number}-x` }, + html_url: `https://example.test/${number}`, + }); + const h36files = (entries, truncated = []) => + new Map( + Object.entries(entries).map(([n, paths]) => [ + Number(n), + { paths, truncated: truncated.includes(Number(n)) }, + ]), + ); + const h36rows = (...args) => h36SharedFileHolds(...args); + // The wrapper discipline the row-text wrappers at the top explain: a nulled + // first row must report as red CASES, never abort the suite in argument + // evaluation. The count assertions stay on `h36rows` directly. + const h36row1 = (...args) => String(h36SharedFileHolds(...args)[0]?.[1] ?? ''); + const h36key1 = (...args) => h36SharedFileHolds(...args)[0]?.[0]?.number ?? null; + const readyEarly = h36pr(100, { draft: false }); + const draftLate = h36pr(200, { created_at: '2026-08-25T11:00:00Z' }); + const sharedAuto = h36files({ + 100: ['packages/runtime/src/domains/automation.ts'], + 200: ['packages/runtime/src/domains/automation.ts', 'docs/x.md'], + }); + // ⭐ THE FINDING — the incident's own shape: an accepted side and any other + // open PR holding one source file. + t('H36: ready + draft sharing a source file -> one row', h36rows([readyEarly, draftLate], sharedAuto).length, 1); + t('H36: …keyed to the accepted side', h36key1([readyEarly, draftLate], sharedAuto), 100); + t('H36: …naming the other side', h36row1([readyEarly, draftLate], sharedAuto).includes('#200'), true); + t('H36: …and the shared path', h36row1([readyEarly, draftLate], sharedAuto).includes('automation.ts'), true); + t('H36: …and states the non-verdict posture', h36row1([readyEarly, draftLate], sharedAuto).includes('NOT a verdict'), true); + t('H36: …and the remedy is the local probe', h36row1([readyEarly, draftLate], sharedAuto).includes('merge-tree'), true); + // The gate on the row: some side must be DONE. + t('H36: two unarmed drafts -> silent (the dispatch-time walk owns that case)', h36rows([h36pr(100), draftLate], sharedAuto).length, 0); + t('H36: an ARMED draft is at risk, finding-increasing like H16', h36rows([h36pr(100, { auto_merge: { merge_method: 'squash' } }), draftLate], sharedAuto).length, 1); + t('H36: both accepted -> keyed to the earlier-created side', h36key1([h36pr(100, { draft: false }), h36pr(200, { draft: false, created_at: '2026-08-25T11:00:00Z' })], sharedAuto), 100); + // The noise floor: closed, two spellings, pinned against per-incident growth. + t('H36: lockfile-only overlap is the noise floor -> silent', h36rows([readyEarly, draftLate], h36files({ 100: ['pnpm-lock.yaml'], 200: ['pnpm-lock.yaml'] })).length, 0); + t('H36: .changeset/ overlap is the noise floor -> silent', h36rows([readyEarly, draftLate], h36files({ 100: ['.changeset/a.md'], 200: ['.changeset/a.md'] })).length, 0); + t('H36: the noise floor is CLOSED at two spellings — growing it needs its own card', H36_SHARED_PATH_NOISE.length + H36_SHARED_PREFIX_NOISE.length, 2); + t('H36: a source path is NOT noise', h36NoisePath('packages/runtime/src/domains/automation.ts'), false); + // Candidate policy: never narrower than the predicate's population. + t('H36: changeset-release PRs are not candidates', h36NeedsFiles(h36pr(300, { ref: 'changeset-release/main' })), false); + t('H36: a merged row is not a candidate', h36NeedsFiles(h36pr(300, { merged_at: '2026-08-25T12:00:00Z' })), false); + t('H36: a draft IS a candidate (the pair\'s other side can be one)', h36NeedsFiles(draftLate), true); + // An unread side cannot pair — a miss, never an invention. + t('H36: an unread side cannot pair', h36rows([readyEarly, draftLate], h36files({ 100: ['x.ts'] })).length, 0); + // Truncation: pairs on what was seen, and says the list was short. + const truncPair = h36files( + { 100: ['packages/runtime/src/domains/automation.ts'], 200: ['packages/runtime/src/domains/automation.ts'] }, + [200], + ); + t('H36: a truncated list still pairs on what was seen', h36rows([readyEarly, draftLate], truncPair).length, 1); + t('H36: …and the row says so, in the only-hides-more direction', h36row1([readyEarly, draftLate], truncPair).includes('TRUNCATED'), true); + t('H36: an untruncated pair makes no such claim', h36row1([readyEarly, draftLate], sharedAuto).includes('TRUNCATED'), false); + // The transport judgement — H16's shape, asserted on this pass's own pair. + t('H36: an all-failed files pass is the transport, not a clean board', h36DetailPassUnreadable(3, 0), true); + t('H36: zero candidates is a clean reading', h36DetailPassUnreadable(0, 0), false); + t('H36: a partial read is a bounded gap, not a failure', h36DetailPassUnreadable(3, 1), false); + // The coverage pair reaches the summary line, and degrades to 0. + t('H36 summary: the pair is reported', summaryLine({ sharedFileProbed: 17, sharedFileCandidates: 19 }, 0).includes('changed-file page read on 17 of 19 open PR(s)'), true); + t('H36 summary: …and states the miss-only direction', summaryLine({}, 0).includes('MISS a hold, never invent one'), true); + t('H36: both count keys ride the enumerated contract', SWEEP_COUNT_KEYS.includes('sharedFileCandidates') && SWEEP_COUNT_KEYS.includes('sharedFileProbed'), true); + // The markdown medium renders an H36 row like every other finding row. + t('markdown: an H36 row links the PR it names', renderMarkdown([[{ number: 100, html_url: 'https://example.test/100' }, 'H36', 'shares a file']], { repo: 'r', issues: 0, unscoped: 0, prs: 2, merged: 0 }).includes('- **H36** [#100](https://example.test/100)'), true); + + // -- H37 — family-dispatch member drift (report-only patrol input, #12629) -- + // + // The fixtures are the two MEASURED instances the filing card carries: a + // 3-card fold whose third member kept `pm:queue` while the fold ran, and the + // mirror — a member left `pm:dispatched` WITH its assignee after the head had + // released. Fold #11678 is the shape the live claim census records + // (「folded into the 11678 family dispatch」). + const FOLD_37 = 'claude/issue-11678-family-fold'; + const h37card = (number, labels, assignees = ['os-litant']) => ({ + number, + html_url: `https://example.test/${number}`, + labels: labels.map((name) => ({ name })), + assignees: assignees.map((login) => ({ login })), + }); + const h37claim = (number, ...branches) => ({ number, branches }); + const h37map = (...cards) => new Map(cards.map((c) => [c.number, c])); + const h37rows = (...args) => h37FamilyMemberDrift(...args); + // The wrapper discipline: a nulled row must report as a red CASE, never abort + // the suite while evaluating `t()`'s arguments. + const h37row1 = (...args) => String(h37FamilyMemberDrift(...args)[0]?.[1] ?? ''); + const h37key1 = (...args) => h37FamilyMemberDrift(...args)[0]?.[0]?.number ?? null; + const foldClaims37 = [h37claim(11678, FOLD_37), h37claim(11679, FOLD_37), h37claim(11680, FOLD_37)]; + const folds37 = h37FoldBranches(foldClaims37); + const headLive37 = h37card(11678, ['domain:skills', 'pm:dispatched']); + + // The roster: what makes a branch a FOLD, and the one exclusion that is the + // whole noise floor. + t('H37 roster: two cards claiming one branch is a fold', folds37.size, 1); + t('H37 roster: …keyed to the shared branch', [...folds37.keys()][0], FOLD_37); + t('H37 roster: …whose head is the card the branch is named for', folds37.get(FOLD_37).head, 11678); + t('H37 roster: …and every claimant is on it, sorted', folds37.get(FOLD_37).claimants.join(','), '11678,11679,11680'); + // ⛔ THE NOISE FLOOR — and it is the ordinary case: every solo dispatch on + // this board claims its own branch, so without this the row fires on all of them. + t('H37 roster: a claim naming its OWN branch is not a fold', h37FoldBranches([h37claim(11678, FOLD_37)]).size, 0); + t('H37 roster: …not even several of them', h37FoldBranches([h37claim(11678, FOLD_37), h37claim(9000, 'claude/issue-9000-solo')]).size, 0); + t('H37 roster: a lone FOREIGN claimant is already a fold (the head may be silent)', h37FoldBranches([h37claim(9999, 'claude/issue-8888-x')]).size, 1); + t('H37 roster: a branch with no readable card number is not a roster key', h37FoldBranches([h37claim(1, 'main'), h37claim(2, 'main')]).size, 0); + t('H37 roster: a duplicate claimant is counted once', h37FoldBranches([h37claim(11679, FOLD_37, FOLD_37)]).get(FOLD_37).claimants.length, 1); + t('H37 roster: no claims at all -> no folds', h37FoldBranches([]).size, 0); + t('H37 roster: a missing claim list does not crash', h37FoldBranches(undefined).size, 0); + + // The head state — four-valued, because `unknown` must never read as released. + t('H37 head: dispatched and open -> in flight', h37HeadState(11678, h37map(headLive37), new Map()), 'in-flight'); + t('H37 head: open without the label -> released, and separably so', h37HeadState(11678, h37map(h37card(11678, ['pm:queue'])), new Map()), 'open-undispatched'); + t('H37 head: closed -> released', h37HeadState(11678, new Map(), h37map(h37card(11678, []))), 'closed'); + t('H37 head: listed nowhere -> unknown, never released (#4690)', h37HeadState(11678, new Map(), new Map()), 'unknown'); + + // The classifier, asserted directly — H35's idiom, so the fold is pinned + // independently of any sentence it produces. + t('H37 verdict: fold in flight + member undispatched -> the missed write', h37MemberVerdict('in-flight', false), 'undispatched-member'); + t('H37 verdict: fold in flight + member dispatched -> clean', h37MemberVerdict('in-flight', true), 'clean'); + t('H37 verdict: head CLOSED + member dispatched -> residue', h37MemberVerdict('closed', true), 'dispatched-residue'); + t('H37 verdict: head open-undispatched + member dispatched -> residue', h37MemberVerdict('open-undispatched', true), 'dispatched-residue'); + t('H37 verdict: nothing dispatched anywhere -> clean (an abandoned fold is H30\'s)', h37MemberVerdict('open-undispatched', false), 'clean'); + t('H37 verdict: an unresolvable head declines rather than accusing', h37MemberVerdict('unknown', true), 'unresolvable-head'); + + // ⭐ THE FINDING, direction 1 — the filing seat's own error: the fold ran and + // one member kept `pm:queue`, with no assignee, looking clean to every + // per-card predicate here. + const stranded37 = h37card(11680, ['domain:skills', 'pm:queue'], []); + const liveFold37 = [folds37, h37map(headLive37, h37card(11679, ['pm:dispatched']), stranded37), new Map()]; + t('H37: a fold in flight with one member on `pm:queue` -> one row', h37rows(...liveFold37).length, 1); + t('H37: …keyed to the stranded member, not the head', h37key1(...liveFold37), 11680); + t('H37: …naming the shared branch', h37row1(...liveFold37).includes(FOLD_37), true); + t('H37: …and the chain head', h37row1(...liveFold37).includes('#11678'), true); + t('H37: …stating the non-verdict posture', h37row1(...liveFold37).includes('NOT a verdict'), true); + t('H37: …and naming the alternative reading a reader might find', h37row1(...liveFold37).includes('cross-lane hand-off'), true); + t('H37: …with the remedy as ONE write', h37row1(...liveFold37).includes('ONE write'), true); + + // ⭐ THE FINDING, direction 2 — the MIRROR measured on this card's own round + // (#12200): the head released, the member kept `pm:dispatched` + assignee. + const residue37 = [ + h37FoldBranches([h37claim(11679, FOLD_37)]), + h37map(h37card(11679, ['pm:dispatched'])), + h37map(h37card(11678, [])), + ]; + t('H37 mirror: head closed + member still dispatched -> one row', h37rows(...residue37).length, 1); + t('H37 mirror: …keyed to the member carrying the residue', h37key1(...residue37), 11679); + t('H37 mirror: …and it says the head CLOSED', h37row1(...residue37).includes('CLOSED'), true); + t('H37 mirror: …naming it as the same write failing the other way', h37row1(...residue37).includes('MIRROR direction'), true); + const released37 = [ + h37FoldBranches([h37claim(11679, FOLD_37)]), + h37map(h37card(11679, ['pm:dispatched']), h37card(11678, ['pm:queue'])), + new Map(), + ]; + t('H37 mirror: an open head that dropped the label reads as released too', h37rows(...released37).length, 2); + + // ⭐ THE FINDING, direction 3 — the HEAD's own half missed, visible from the + // members' claims and from nothing on the head's card. + const headMissed37 = h37rows(...released37).find(([card]) => card.number === 11678); + t('H37 head half: the undispatched head gets its own row', headMissed37 === undefined, false); + t('H37 head half: …and it says the members are the only witnesses', String(headMissed37?.[1] ?? '').includes('visible ONLY from'), true); + t('H37 head half: …counting the dispatched claimants', String(headMissed37?.[1] ?? '').includes('#11679'), true); + + // Silence, in both directions — the halves that must NOT fire. + t('H37 silent: a fold whose members are all dispatched', h37rows(folds37, h37map(headLive37, h37card(11679, ['pm:dispatched']), h37card(11680, ['pm:dispatched'])), new Map()).length, 0); + t('H37 silent: a fold with nothing dispatched at all (abandoned, or not yet launched)', h37rows(folds37, h37map(h37card(11678, ['pm:queue']), h37card(11679, ['pm:queue']), h37card(11680, ['pm:queue'])), new Map()).length, 0); + t('H37 silent: an unresolvable head judges nothing', h37rows(folds37, h37map(h37card(11679, ['pm:queue']), h37card(11680, ['pm:queue'])), new Map()).length, 0); + t('H37 silent: a claimant this sweep never listed is skipped, not judged', h37rows(folds37, h37map(headLive37), new Map()).length, 0); + t('H37 silent: no folds -> no rows', h37rows(new Map(), h37map(headLive37), new Map()).length, 0); + t('H37 silent: a missing roster does not crash', h37rows(undefined, h37map(headLive37), new Map()).length, 0); + + // The gathering policy: never wider than the fold population it is bought for. + t('H37 gate: a `pm:queue` card is read once a fold is live', h37NeedsMemberRead(h37card(9, ['pm:queue']), 1), true); + t('H37 gate: …and costs NOTHING on a board with no fold', h37NeedsMemberRead(h37card(9, ['pm:queue']), 0), false); + t('H37 gate: a dispatched card buys nothing (its thread is already read)', h37NeedsMemberRead(h37card(9, ['pm:dispatched']), 1), false); + t('H37 gate: a card in neither state is out of scope', h37NeedsMemberRead(h37card(9, ['pm:blocked']), 1), false); + t('H37 gate: a missing issue does not crash', h37NeedsMemberRead(undefined, 1), false); + t('H37 gate: an absent fold count is not a fold', h37NeedsMemberRead(h37card(9, ['pm:queue']), undefined), false); + + // The transport judgement — H16's shape, on this pass's own pair. + t('H37: an all-failed member pass is the transport, not a fold whose writes landed', h37MemberPassUnreadable(40, 0), true); + t('H37: zero candidates is a clean reading (no fold was live)', h37MemberPassUnreadable(0, 0), false); + t('H37: a partial read is a bounded gap, not a failure', h37MemberPassUnreadable(40, 12), false); + + // The render budget, H19/H20's cap and note. + const wide37 = h37FoldBranches([1, 2, 3, 4, 5, 6, 7].map((n) => h37claim(11670 + n, FOLD_37))); + t('H37: a wide fold names the cap and counts the rest', h37row1(wide37, h37map(h37card(11678, ['pm:dispatched']), h37card(11671, ['pm:queue'])), new Map()).includes(`+${7 - H37_MEMBER_LIST_CAP} more`), true); + + // The coverage pair reaches the summary line, and degrades to 0. + t('H37 summary: the pair is reported', summaryLine({ liveFolds: 2, memberReadProbed: 38, memberReadCandidates: 40 }, 0).includes('member comment page read on 38 of 40'), true); + t('H37 summary: …and the fold count with it', summaryLine({ liveFolds: 2 }, 0).includes('2 live shared branch(es)'), true); + t('H37 summary: …stating the miss-only direction', summaryLine({}, 0).includes('never invent one'), true); + t('H37 summary: an all-failed pass is named as the transport', summaryLine({ liveFolds: 1, memberReadCandidates: 40, memberReadProbed: 0 }, 0).includes('NO member page was readable'), true); + t('H37 summary: …and 0 of 0 makes no such claim', summaryLine({}, 0).includes('NO member page was readable'), false); + t('H37: all three count keys ride the enumerated contract', ['liveFolds', 'memberReadCandidates', 'memberReadProbed'].every((k) => SWEEP_COUNT_KEYS.includes(k)), true); + // The markdown medium renders an H37 row like every other finding row. + t('markdown: an H37 row links the card it names', renderMarkdown([[{ number: 11680, html_url: 'https://example.test/11680' }, 'H37', 'member pointer without the label']], { repo: 'r', issues: 1, unscoped: 0, prs: 0, merged: 0 }).includes('- **H37** [#11680](https://example.test/11680)'), 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. + t('claim marker: the canonical colon still matches', CLAIM_COMMENT_MARKER.test('Claim: PM loop round R6'), true); + t('claim marker: `Claimed:` still matches', CLAIM_COMMENT_MARKER.test('Claimed: PM loop round R6'), true); + t('claim marker: a blockquoted claim still matches', CLAIM_COMMENT_MARKER.test('> Claim: PM loop round R6'), true); + t('claim marker: whitespace before the colon is still tolerated', CLAIM_COMMENT_MARKER.test('Claim : PM loop round R6'), true); + t('claim marker: the FULLWIDTH colon does not match, and never did', CLAIM_COMMENT_MARKER.test('Claim:PM loop round R6'), false); + t('branch line: the canonical colon still reads', claimedBranches('Branch: `claude/issue-12090-x`').join(','), 'claude/issue-12090-x'); + t('branch line: `Branches:` still reads', claimedBranches('Branches: `claude/issue-12090-x`').join(','), 'claude/issue-12090-x'); + t('branch line: a FULLWIDTH colon does not read, and never did', claimedBranches('Branch:`claude/issue-12090-x`').length, 0); + // -- resolveSweepRepo: the parameterisation that makes a verbatim sibling // -- install correct rather than a green report about the wrong board (#11217) t('sweep repo: PM_SWEEP_REPO wins when set', resolveSweepRepo({ PM_SWEEP_REPO: 'objectstack-ai/cloud', GITHUB_REPOSITORY: 'objectstack-ai/objectui' }).repo, 'objectstack-ai/cloud'); @@ -9193,6 +12941,7 @@ function selfTest() { // the wrong board" the parameterisation exists to prevent, arriving through // the one input nobody sets. t('sweep repo: this install defaults to its OWN board', DEFAULT_SWEEP_REPO, 'objectstack-ai/objectui'); + t('sweep repo: …and that reading is valid', resolveSweepRepo({ GITHUB_REPOSITORY: 'objectstack-ai/objectstack' }).valid, true); // Empty/whitespace is UNSET, not a value: Actions expressions expand to '' // for an unset variable, and treating '' as a repo would sweep nothing while // reporting a completed run. @@ -9285,14 +13034,14 @@ if (isMain) { process.exit(2); } // A malformed closure floor is the same class and gets the same answer. It - // must not degrade to "no floor": this is the install whose closed surface is - // ~87% residue, so a silent default would flood the anchor four times a day - // and the flood renders as a working patrol. + // must not degrade to "no floor": the install that sets one is the install + // whose closed surface is ~87% residue, so a silent default would flood the + // anchor body four times a day and the flood renders as a working patrol. if (!process.argv.includes('--self-test') && !CLOSED_FLOOR.valid) { console.error( `check-half-states: ${CLOSED_FLOOR.source}=${JSON.stringify(CLOSED_FLOOR.raw)} is not a ` + - '`YYYY-MM-DD` date. Refusing to fall back to an unfloored closed pass — on this board, ' + - 'no floor is a report about the convention rather than about defects.', + '`YYYY-MM-DD` date. Refusing to fall back to an unfloored closed pass — on the install ' + + 'that needs a floor, no floor is a report about the convention rather than about defects.', ); process.exit(2); } diff --git a/scripts/upstream-port-pin.json b/scripts/upstream-port-pin.json new file mode 100644 index 0000000000..c964fd8496 --- /dev/null +++ b/scripts/upstream-port-pin.json @@ -0,0 +1,160 @@ +{ + "upstream": { + "repo": "objectstack-ai/objectstack", + "ref": "2b4178aa53ca62089f43e2cfae0b7838cf340dd1" + }, + "files": [ + { + "ported": "scripts/pm/check-half-states.mjs", + "upstreamPath": "scripts/pm/check-half-states.mjs", + "upstreamSha256": "c06c84517630e180eb9e4bc1f0e13d069e5dfc71b6889baac0ebcd37256ea1bc", + "divergences": [ + { + "id": "default-sweep-repo", + "why": "objectui's copy sweeps THIS board when nothing says otherwise; upstream's constant names objectstack.", + "upstream": "export const DEFAULT_SWEEP_REPO = 'objectstack-ai/objectstack';\n", + "ported": "export const DEFAULT_SWEEP_REPO = 'objectstack-ai/objectui';\n" + }, + { + "id": "summary-counts-typedef", + "why": "The summaryLine counts contract carries the objectui-only `closedWindowDisabled` flag.", + "upstream": " * commitBindingMessages?: number, closedFloor?: string }} counts\n", + "ported": " * commitBindingMessages?: number, closedWindowDisabled?: boolean, closedFloor?: string }} counts\n" + }, + { + "id": "summary-unread-branch", + "why": "A DISABLED closed reader must render as UNREAD, never as `read 0` (#4690). Upstream has no disabled state.", + "upstream": " `H22 read ${counts.closed ?? 0} recently-closed issue(s) for \\`pm:*\\` state residue (bounded window; ` +\n `older closed carriers are outside it by design` +\n `${counts.closedFloor ? `, and only cards closed on/after ${counts.closedFloor} are judged — ` +\n 'earlier closures predate the strip-on-close convention and are NOT a reading about them' : ''}). ` +\n", + "ported": " (counts.closedWindowDisabled\n ? 'H22 (closed-card `pm:*` state residue) is DISABLED in this install and NO closed issue was ' +\n 'read — this sweep therefore says NOTHING about closed-card residue: that surface is UNREAD, ' +\n 'not clean (see `resolveClosedWindowPages` and `PM_SWEEP_CLOSED_WINDOW_PAGES` in ' +\n '`.github/workflows/half-state-patrol.yml`). '\n : `H22 read ${counts.closed ?? 0} recently-closed issue(s) for \\`pm:*\\` state residue (bounded window; ` +\n `older closed carriers are outside it by design` +\n `${counts.closedFloor ? `, and only cards closed on/after ${counts.closedFloor} are judged — ` +\n 'earlier closures predate the strip-on-close convention and are NOT a reading about them' : ''}). `) +\n" + }, + { + "id": "closed-window-counts", + "why": "The sweep forwards whether the closed reader was switched off, so summaryLine can say UNREAD.", + "upstream": " closed: seenClosed.size,\n", + "ported": " closed: seenClosed.size,\n // ADAPTED (objectui#5791): the summary line must be able to say \"UNREAD\"\n // rather than \"read 0\" — a disabled reader and an empty result are the same\n // number and opposite facts (#4690).\n closedWindowDisabled: CLOSED_WINDOW.pages === 0,\n" + }, + { + "id": "closed-window-resolver", + "why": "PM_SWEEP_CLOSED_WINDOW_PAGES: objectui-only escape hatch that can switch the closed reader OFF. Authored during the port, never upstreamed. Unset keeps upstream's 4, so the default is byte-equivalent.", + "upstream": "export const CLOSED_ISSUE_WINDOW_PAGES = 4;\n", + "ported": "export const CLOSED_ISSUE_WINDOW_PAGES = 4;\n\n/**\n * ⚠️ ADAPTED FOR THIS REPO (objectui#5791) — the one predicate whose upstream\n * default is WRONG here, and the measurement that says so.\n *\n * Every other collector in this file is `state=open` by construction, so the\n * port carries them unchanged. H22 is the single closed-issue reader, and it is\n * the one row whose yield depends on a CONVENTION rather than on a defect —\n * which is why the same code means different things in the two repos.\n *\n * ## The measurement (objectui, 2026-08-24, re-measured for this port)\n *\n * closed cards carrying `pm:dispatched`, repo-wide 815\n * closed issues in this window (4 pages = 400 rows) 400\n * …spanning updated 2026-08-18T03:36:15Z … 08-24T09:48Z 6.2 days\n * of those 400, carrying a `PM_RESIDUE_LABELS` member:\n * `pm:dispatched` 259\n * `pm:queue` 86\n * `pm:blocked` 1\n * `pm:on-hold` 1\n * ≈ 347 rows\n *\n * Upstream measured 129 of 500 (26%) and called that recent residue a live\n * duty. Here it is ~87% of the window. That inversion is not this repo being\n * behind on a chore — it is that stripping `pm:*` on close was never this\n * lane's practice (objectui#5791's own thread records two seats reading the\n * same corpus and landing on opposite conventions). A row that fires on ~87%\n * of everything it reads is not a finding, it is the convention restated 347\n * times, and it would consume the entire `MARKDOWN_BODY_BUDGET` and trim every\n * OTHER predicate's rows out of the anchor body — the patrol dead on arrival,\n * which is precisely what this card exists to prevent.\n *\n * So the window is PARAMETERISED rather than the predicate edited: `h22ClosedCardPmResidue`\n * is untouched and still correct, and this install simply does not open the\n * closed reader. `PM_SWEEP_CLOSED_WINDOW_PAGES=0` disables it; unset keeps\n * upstream's 4, so the file's DEFAULT behaviour is byte-identical to objectstack\n * and a future verbatim re-sync of the predicate cannot silently re-enable\n * anything — the choice lives in the workflow, where it is one visible line.\n *\n * ⛔ Disabling is NOT the same as reading clean, and `summaryLine` says so in\n * the rendered body rather than reporting `read 0` (#4690: an input that was\n * never read must never render as an input that was read and found clean).\n * Re-enabling is a BACKFILL decision — strip the historical residue first, then\n * drop the variable — and never a quiet default flip.\n *\n * @param {Record} [env]\n * @returns {{ pages: number, source: string, valid: boolean, raw: string }}\n */\nexport function resolveClosedWindowPages(env = {}) {\n const raw = String(env.PM_SWEEP_CLOSED_WINDOW_PAGES ?? '').trim();\n // Unset and whitespace are UNSET, not a value — the same reading\n // `resolveSweepRepo` gives an unexpanded Actions expression.\n if (raw === '') return { pages: CLOSED_ISSUE_WINDOW_PAGES, source: 'default', valid: true, raw };\n // A malformed value is REFUSED at the CLI, never silently replaced by the\n // default: a typo'd `PM_SWEEP_CLOSED_WINDOW_PAGES=O` that quietly restored\n // the 4-page default would re-open the 347 rows this install disabled, and\n // the anchor would read as though someone had chosen that.\n if (!/^\\d+$/.test(raw)) return { pages: CLOSED_ISSUE_WINDOW_PAGES, source: 'PM_SWEEP_CLOSED_WINDOW_PAGES', valid: false, raw };\n return { pages: Number(raw), source: 'PM_SWEEP_CLOSED_WINDOW_PAGES', valid: true, raw };\n}\n\nconst CLOSED_WINDOW = resolveClosedWindowPages(process.env);\n" + }, + { + "id": "closed-window-fetch-gate", + "why": "0 pages spends no request at all; upstream always walks CLOSED_ISSUE_WINDOW_PAGES.", + "upstream": "async function listRecentlyClosedIssues() {\n const out = [];\n for (let page = 1; page <= CLOSED_ISSUE_WINDOW_PAGES; page++) {\n", + "ported": "async function listRecentlyClosedIssues() {\n // 0 pages = the closed reader is off in this install. Returning early (rather\n // than letting the loop not execute) keeps the intent legible and makes it\n // explicit that NO request is spent.\n if (CLOSED_WINDOW.pages === 0) return [];\n const out = [];\n for (let page = 1; page <= CLOSED_WINDOW.pages; page++) {\n" + }, + { + "id": "floor-window-independence", + "why": "Self-test rows pinning the two knobs as independent — the window is not an alias of the floor.", + "upstream": " t('closure floor: …and is reported as itself for the error message', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: 'yesterday' }).raw, 'yesterday');\n", + "ported": " t('closure floor: …and is reported as itself for the error message', resolveClosureFloor({ PM_SWEEP_CLOSED_FLOOR: 'yesterday' }).raw, 'yesterday');\n // The floor and the page window are INDEPENDENT knobs: a 0-page window still\n // reads nothing whatever the floor says, and that is the disabled summary,\n // not a floored one. Pinned because the workflow now sets the floor INSTEAD\n // of the 0, and a future reader must not read one as an alias of the other.\n t('closure floor: the floor does not switch the reader on', resolveClosedWindowPages({ PM_SWEEP_CLOSED_WINDOW_PAGES: '0', PM_SWEEP_CLOSED_FLOOR: '2026-08-28' }).pages, 0);\n t('closure floor: …and the window does not set a floor', resolveClosureFloor({ PM_SWEEP_CLOSED_WINDOW_PAGES: '4' }).floor, null);\n" + }, + { + "id": "summary-disabled-beats-floor", + "why": "Self-test row: a 0-page window still reads UNREAD even with a floor set.", + "upstream": " t('summary: an unfloored pass adds no floor clause', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, closed: 200 }, 0).includes('are judged'), false);\n", + "ported": " t('summary: an unfloored pass adds no floor clause', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, closed: 200 }, 0).includes('are judged'), false);\n // The DISABLED branch still wins over a floor: a 0-page window read nothing,\n // so the line must keep saying UNREAD rather than describing a floored pass.\n t('summary: a disabled reader with a floor set still reads UNREAD', summaryLine({ repo: 'r', issues: 1, unscoped: 1, prs: 0, merged: 0, closedWindowDisabled: true, closedFloor: '2026-08-28' }, 0).includes('UNREAD'), true);\n" + }, + { + "id": "h32-lane-own-board", + "why": "`seatLane` reads the LIVE resolved sweep repo, so upstream's two rows invert in this install: here `@ objectui` is the own board and `@ objectstack` is the sibling. The property asserted is unchanged.", + "upstream": " t('H32 lane: an `@ sibling` suffix is FOREIGN', seatLane(seat('[PM seat] domain:devx @ objectui — 🟢 os-x')).foreign, true);\n t('H32 lane: an `@ own-repo` suffix is NOT foreign', seatLane(seat('[PM seat] domain:devx @ objectstack — 🟢 os-x')).foreign, false);\n t('H32 lane: …and keeps the bare lane label', seatLane(seat('[PM seat] domain:devx @ objectstack — 🟢 os-x')).lane, 'domain:devx');\n", + "ported": " // ADAPTED FOR THIS REPO. `seatLane` compares the `@ ` suffix against\n // the LIVE `SWEEP_REPO`, so which name is FOREIGN is install-dependent and\n // upstream's two rows invert here. The property is identical — a sibling\n // board's lane is unreadable from this sweep, this board's own lane is not.\n t('H32 lane: an `@ sibling` suffix is FOREIGN', seatLane(seat('[PM seat] domain:devx @ objectstack — 🟢 os-x')).foreign, true);\n t('H32 lane: an `@ own-repo` suffix is NOT foreign', seatLane(seat('[PM seat] domain:devx @ objectui — 🟢 os-x')).foreign, false);\n t('H32 lane: …and keeps the bare lane label', seatLane(seat('[PM seat] domain:devx @ objectui — 🟢 os-x')).lane, 'domain:devx');\n" + }, + { + "id": "h32-foreign-out-of-scope", + "why": "Same inversion, in the H32 predicate row: the FOREIGN specimen must name a board this sweep cannot read.", + "upstream": " t('H32: a FOREIGN lane is out of scope (its inventory is unreadable here)', h32SeatIdleOverQueue(seat('[PM seat] domain:devx @ objectui — 🟢 os-x'), marker('Round-start marker', 600), idleLane, NOW32), null);\n", + "ported": " t('H32: a FOREIGN lane is out of scope (its inventory is unreadable here)', h32SeatIdleOverQueue(seat('[PM seat] domain:devx @ objectstack — 🟢 os-x'), marker('Round-start marker', 600), idleLane, NOW32), null);\n" + }, + { + "id": "h32-foreign-no-fetch", + "why": "Same inversion, in the H32 comment-fetch gate row.", + "upstream": " t('H32 gate: a foreign-lane seat buys no fetch', h32NeedsSeatComments(seat('[PM seat] domain:devx @ objectui — 🟢 os-x')), false);\n", + "ported": " t('H32 gate: a foreign-lane seat buys no fetch', h32NeedsSeatComments(seat('[PM seat] domain:devx @ objectstack — 🟢 os-x')), false);\n" + }, + { + "id": "sweep-repo-self-test", + "why": "Upstream pins the objectstack leg against DEFAULT_SWEEP_REPO because there the two strings are equal. Here the default is objectui, so the leg is pinned to its literal and the default gets its own row.", + "upstream": " // The objectstack leg, pinned in the exact shape its runner provides: same\n // string as the hardcoded default this replaced, so every request path is\n // byte-identical and the behaviour is unchanged by construction.\n t('sweep repo: the objectstack runner resolves to the pre-change constant', resolveSweepRepo({ PM_SWEEP_REPO: 'objectstack-ai/objectstack', GITHUB_REPOSITORY: 'objectstack-ai/objectstack' }).repo, DEFAULT_SWEEP_REPO);\n t('sweep repo: …and with only GITHUB_REPOSITORY set, identically', resolveSweepRepo({ GITHUB_REPOSITORY: 'objectstack-ai/objectstack' }).repo, DEFAULT_SWEEP_REPO);\n", + "ported": " // ADAPTED FOR THIS REPO (objectui#5791). Upstream these two cases pinned\n // objectstack to `DEFAULT_SWEEP_REPO` because there the two strings were the\n // same. In this install the default is objectui, so the objectstack leg is\n // pinned to its LITERAL instead — the property being asserted is unchanged\n // (an explicit repo is honoured exactly as given) and it no longer rides on\n // a constant whose value differs per install.\n t('sweep repo: an explicit objectstack target is honoured verbatim', resolveSweepRepo({ PM_SWEEP_REPO: 'objectstack-ai/objectstack', GITHUB_REPOSITORY: 'objectstack-ai/objectstack' }).repo, 'objectstack-ai/objectstack');\n t('sweep repo: …and with only GITHUB_REPOSITORY set, identically', resolveSweepRepo({ GITHUB_REPOSITORY: 'objectstack-ai/objectstack' }).repo, 'objectstack-ai/objectstack');\n t('sweep repo: …and that reading is valid', resolveSweepRepo({ GITHUB_REPOSITORY: 'objectstack-ai/objectstack' }).valid, true);\n // The adaptation itself, pinned: a bare terminal in THIS repo must sweep THIS\n // board. Upstream's constant would have made `node scripts/pm/check-half-states.mjs`\n // here render a fully green report about objectstack — the exact \"report about\n // the wrong board\" the parameterisation exists to prevent, arriving through\n // the one input nobody sets.\n t('sweep repo: this install defaults to its OWN board', DEFAULT_SWEEP_REPO, 'objectstack-ai/objectui');\n" + }, + { + "id": "closed-window-self-test", + "why": "Self-test rows for the objectui-only page-window resolver and its UNREAD rendering.", + "upstream": " t('sweep repo: the rendered summary names the swept repo', summaryLine({ repo: 'objectstack-ai/objectui', issues: 3, unscoped: 4, prs: 1, merged: 2 }, 0).includes('objectstack-ai/objectui'), true);\n", + "ported": " t('sweep repo: the rendered summary names the swept repo', summaryLine({ repo: 'objectstack-ai/objectui', issues: 3, unscoped: 4, prs: 1, merged: 2 }, 0).includes('objectstack-ai/objectui'), true);\n\n // -- resolveClosedWindowPages: the objectui adaptation (objectui#5791) ------\n // The port's ONE behavioural divergence, pinned in both directions so neither\n // a re-sync from objectstack nor a stray environment variable can move it\n // without turning a case red.\n t('closed window: unset keeps upstream\\'s default', resolveClosedWindowPages({}).pages, CLOSED_ISSUE_WINDOW_PAGES);\n t('closed window: …and that default is still 4, byte-identical to objectstack', CLOSED_ISSUE_WINDOW_PAGES, 4);\n t('closed window: …reported as the default source', resolveClosedWindowPages({}).source, 'default');\n t('closed window: whitespace is unset too', resolveClosedWindowPages({ PM_SWEEP_CLOSED_WINDOW_PAGES: ' ' }).pages, CLOSED_ISSUE_WINDOW_PAGES);\n t('closed window: 0 disables the closed reader', resolveClosedWindowPages({ PM_SWEEP_CLOSED_WINDOW_PAGES: '0' }).pages, 0);\n t('closed window: …and that is a VALID reading, not an error', resolveClosedWindowPages({ PM_SWEEP_CLOSED_WINDOW_PAGES: '0' }).valid, true);\n t('closed window: an explicit page count is honoured', resolveClosedWindowPages({ PM_SWEEP_CLOSED_WINDOW_PAGES: '2' }).pages, 2);\n t('closed window: surrounding whitespace is trimmed, not rejected', resolveClosedWindowPages({ PM_SWEEP_CLOSED_WINDOW_PAGES: ' 3 ' }).pages, 3);\n // Malformed values are refused, never silently defaulted (the CLI exits 2).\n t('closed window: a non-numeric value is invalid', resolveClosedWindowPages({ PM_SWEEP_CLOSED_WINDOW_PAGES: 'O' }).valid, false);\n t('closed window: …and is reported as itself for the error message', resolveClosedWindowPages({ PM_SWEEP_CLOSED_WINDOW_PAGES: 'O' }).raw, 'O');\n t('closed window: a negative value is invalid', resolveClosedWindowPages({ PM_SWEEP_CLOSED_WINDOW_PAGES: '-1' }).valid, false);\n t('closed window: a decimal is invalid', resolveClosedWindowPages({ PM_SWEEP_CLOSED_WINDOW_PAGES: '1.5' }).valid, false);\n\n // ⛔ The property this whole adaptation turns on (#4690): a disabled reader\n // must render as UNREAD, never as a clean reading of the closed surface.\n const disabledSummary = summaryLine({ repo: 'objectstack-ai/objectui', issues: 3, unscoped: 4, prs: 1, merged: 2, closed: 0, closedWindowDisabled: true }, 0);\n t('closed window: a disabled reader says so in the rendered summary', disabledSummary.includes('is DISABLED in this install'), true);\n t('closed window: …and says the surface is UNREAD, not clean', disabledSummary.includes('UNREAD'), true);\n t('closed window: …and never renders the \"read 0\" phrasing that reads as clean', disabledSummary.includes('H22 read 0'), false);\n t('closed window: …and names the variable that re-enables it', disabledSummary.includes('PM_SWEEP_CLOSED_WINDOW_PAGES'), true);\n // The other direction: an ENABLED reader that found nothing still reports a\n // count, because that genuinely IS a clean reading of the surface.\n const enabledSummary = summaryLine({ repo: 'objectstack-ai/objectui', issues: 3, unscoped: 4, prs: 1, merged: 2, closed: 0 }, 0);\n t('closed window: an enabled reader reports its count', enabledSummary.includes('H22 read 0 recently-closed issue(s)'), true);\n t('closed window: …and does not claim to be disabled', enabledSummary.includes('is DISABLED in this install'), false);\n" + }, + { + "id": "closed-window-cli-refusal", + "why": "A malformed page count is refused (exit 2), never silently defaulted back to 4.", + "upstream": " // A malformed closure floor is the same class and gets the same answer. It\n // must not degrade to \"no floor\": the install that sets one is the install\n", + "ported": " // ADAPTED (objectui#5791): a malformed window value is bad usage (exit 2) for\n // the same reason a malformed sweep target is — silently falling back to the\n // 4-page default would re-open a closed reader this install deliberately shut,\n // and the anchor would carry ~347 convention rows as though someone chose that.\n if (!process.argv.includes('--self-test') && !CLOSED_WINDOW.valid) {\n console.error(\n `check-half-states: PM_SWEEP_CLOSED_WINDOW_PAGES=${JSON.stringify(CLOSED_WINDOW.raw)} is not a ` +\n 'non-negative integer. Refusing to fall back to the default page count — silently re-opening ' +\n 'the closed-card reader would fill the anchor with residue nobody asked to see.',\n );\n process.exit(2);\n }\n // A malformed closure floor is the same class and gets the same answer. It\n // must not degrade to \"no floor\": the install that sets one is the install\n" + } + ] + }, + { + "ported": "scripts/invoked-as.mjs", + "upstreamPath": "scripts/invoked-as.mjs", + "upstreamSha256": "90f72bf45a2fd158b19d5774269eb96a6b8b61540251ce68c29ddce7c93bfaa7", + "divergences": [ + { + "id": "invoked-as-1", + "why": "Port adaptation: this tree's prose, gate and fixture (objectui#5984/#6092).", + "upstream": " * imported me\" before it decides whether to do anything. Each one used to\n * answer that with its own hand-typed comparison of `process.argv[1]` against\n * `import.meta.url`, and the copies had drifted into ELEVEN distinct spellings\n * across 33 files -- measured, not estimated. Nine of the eleven were wrong,\n * and wrong in a direction nothing in CI can see.\n", + "ported": " * imported me\" before it decides whether to do anything. Left to a hand-typed\n * comparison of `process.argv[1]` against `import.meta.url`, those answers\n * drift: in the objectstack tree this module was ported FROM (#5984) they had\n * reached ELEVEN distinct spellings across 33 files -- measured, not estimated\n * -- and nine of the eleven were wrong, in a direction nothing in CI can see.\n *\n * THIS repository has since been swept, and a gate here enforces the rule. Read\n * \"## What enforces this, and what it took to get here\" below for the numbers\n * and the commit they were taken on. Two paths this header names are\n * objectstack's and do not exist here; each is labelled where it appears.\n" + }, + { + "id": "invoked-as-2", + "why": "Port adaptation: this tree's prose, gate and fixture (objectui#5984/#6092).", + "upstream": " * an inert child is a GREEN gate. Measured on the tree that motivated this\n * module:\n", + "ported": " * an inert child is a GREEN gate. Measured HERE, on a real blocking gate, with\n * one guide file made deliberately wrong so that the direct run is RED:\n" + }, + { + "id": "invoked-as-3", + "why": "Port adaptation: this tree's prose, gate and fixture (objectui#5984/#6092).", + "upstream": " * scripts/pm/check-governed-merges.mjs --test AGENTS.md\n * direct : exit=3, \"GOVERNED -- no seat arms auto-merge\"\n * symlink : exit=0, no output\n", + "ported": " * node scripts/check-skills-paths.mjs\n * direct : exit=1, 696 bytes naming the dead path and how to fix it\n * symlink : exit=0, no output at all\n" + }, + { + "id": "invoked-as-4", + "why": "Port adaptation: this tree's prose, gate and fixture (objectui#5984/#6092).", + "upstream": " * and `EXIT_TEST_NOT_GOVERNED` is 0. So through a symlink the register's\n * \"this PR is GOVERNED, a human merge is the review record\" answer and its\n * \"NOT governed, ordinary queue landing applies\" answer are the SAME EXIT CODE.\n * A seat reading the status rather than the printed verdict gets a clearance\n * to arm auto-merge from a tool that never ran, on the one surface where human\n * merge IS the review record (Prime Directive #14).\n", + "ported": " * Same tree, same defect, same gate -- reached through a symlink it reports the\n * clean answer, and a wrapper holding `result.status` cannot tell that apart\n * from a pass. `check-skills-paths.mjs` CARRIED the no-realpath spelling when\n * that was measured, as did 27 of its neighbours; every one of them routes\n * through `isEntrypoint` now -- see below for what closed them, and what keeps\n * them shut.\n *\n * The same measurement in objectstack, where this module came from, lands on a\n * gate whose exit codes make it worse still: `scripts/pm/check-governed-merges.mjs`\n * prints \"GOVERNED -- no seat arms auto-merge\" with exit 3 directly and exits 0\n * with no output through a symlink -- and its `EXIT_TEST_NOT_GOVERNED` is also\n * 0, so the two opposite verdicts collapse to one status and a seat reading the\n * status gets a clearance to arm auto-merge from a tool that never ran. That\n * file and that directive number are objectstack's; neither exists here.\n" + }, + { + "id": "invoked-as-5", + "why": "Port adaptation: this tree's prose, gate and fixture (objectui#5984/#6092).", + "upstream": " * `scripts/check-entry-guard.mjs` enforces this: a `process.argv[1]` in an\n * entry-guard position anywhere in `scripts/**` that is not this module is a\n * failure. That gate is what stops a TWELFTH spelling, which is the whole\n * reason this file exists rather than a one-time sweep.\n", + "ported": " * ## What enforces this, and what it took to get here\n *\n * `scripts/check-entry-guard.mjs` enforces the rule: a `process.argv[1]` in an\n * entry-guard position anywhere in `scripts/**` outside this module is a\n * failure. It is wired in `package.json` as `check:entry-guard` and run by\n * `.github/workflows/lint.yml` BEFORE `pnpm install`, so an install failure\n * cannot take the gate down with it. `scripts/__tests__/entry-guard-wiring.test.ts`\n * pins that wiring rather than the numbers, because a gate nobody runs is\n * indistinguishable from a gate that passes.\n *\n * Measured on `2dc4aa709`. Naming the commit is the point: these are a\n * SNAPSHOT, and an older snapshot left standing in the present tense is exactly\n * what this section had to be rewritten to fix.\n *\n * pnpm check:entry-guard -> 47 scripts/ files scanned, no\n * guard outside the baseline;\n * 0 still hand-type one\n * (SHRINK-ONLY); 42 export\n * bindings, 42 inert on import\n * git grep -l 'isEntrypoint' -- scripts -> 40 files\n * git grep -l 'process.argv\\[1\\]' -- scripts -> 3 files\n *\n * Those three are this module -- the one place allowed to read `argv[1]` -- and\n * `check-entry-guard.mjs` and `js-comment-mask.mjs`, which carry the broken\n * spellings as FIXTURE data rather than as guards; the gate's comment/string\n * masking is what tells those apart from the real thing.\n *\n * The history, because the figures above only mean something against it. This\n * module arrived from objectstack (#5984) with prose describing OBJECTSTACK'S\n * tree: it named a gate that did not exist here and a sweep that had not\n * happened here. Measured then, on `7c96c9420`: `check-entry-guard` appeared in\n * exactly ONE place, the paragraph claiming it; `isEntrypoint` had a single\n * adopter (`scripts/pm/check-half-states.mjs`); and 29 hand-typed guards stood\n * in NINE textually distinct spellings, 28 of them with no realpath leg --\n * including the exact percent-encoding spelling this header warns about above,\n * then still in `check-node-esm-load.mjs`. objectui#6092 closed both halves:\n * the gate landed (#6133), the 29 call sites were converted (#6145), and the\n * second rule's baseline reached zero (#6156).\n *\n * The gate, not the sweep, is what holds. A one-time sweep starts rotting the\n * day it merges, because nothing stops a THIRTIETH spelling from being typed\n * next week; `KNOWN_HAND_TYPED_GUARDS` is empty and SHRINK-ONLY, so typing one\n * is now RED rather than a convention politely ignored.\n *\n * When these numbers move, re-take them and re-name the commit -- do not edit\n * the figures in place under the old one. A refreshed count under a stale\n * commit is a measurement nobody can reproduce, and that is the failure this\n * whole section records.\n" + }, + { + "id": "invoked-as-6", + "why": "Port adaptation: this tree's prose, gate and fixture (objectui#5984/#6092).", + "upstream": " * `packages/cli/src/utils/invocation.ts` exports `isProcessEntry`, the same\n * predicate for the same reason (its header cites this defect). It is NOT\n * imported here and this is not imported there: `scripts/` runs as plain .mjs\n", + "ported": " * objectstack's `packages/cli/src/utils/invocation.ts` exports `isProcessEntry`,\n * the same predicate for the same reason (its header cites this defect). Note\n * the repo: objectui's `packages/cli` has no such util, and `isProcessEntry`\n * appears nowhere in this tree, so the pairing below is a CROSS-REPO obligation\n * rather than a local one.\n *\n * Why the copies are not collapsed into one: `scripts/` runs as plain .mjs\n" + }, + { + "id": "invoked-as-7", + "why": "Port adaptation: this tree's prose, gate and fixture (objectui#5984/#6092).", + "upstream": " * A third copy lives outside this repo: objectui's `scripts/invoked-as.mjs`\n * carries the same predicate under the same name (ported from here, #5984), so\n * the pairing is a CROSS-REPO obligation as well as a local one.\n *\n * The duplication is therefore structural, but DIVERGENCE is not allowed --\n * two predicates answering this question differently is precisely the defect\n * being closed. All three copies carry the same two legs: realpath for\n * symlinks, and directory resolution for `node `. Change one, change the\n * others.\n", + "ported": " * The duplication is therefore structural, but DIVERGENCE is not allowed -- two\n * predicates answering this question differently is precisely the defect being\n * closed. All three copies carry the same two legs: realpath for symlinks, and\n * directory resolution for `node `. Change one, change the others.\n" + }, + { + "id": "invoked-as-8", + "why": "Port adaptation: this tree's prose, gate and fixture (objectui#5984/#6092).", + "upstream": "import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';\n", + "ported": "import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';\n" + }, + { + "id": "invoked-as-9", + "why": "Port adaptation: this tree's prose, gate and fixture (objectui#5984/#6092).", + "upstream": " t('an unrelated existing file is not this module', !invokedAs(resolve(SELF, '..', 'js-comment-mask.mjs'), SELF));\n", + "ported": " // A neighbour that must really be there. The ported spelling of this case\n // named `js-comment-mask.mjs`, which exists in objectstack and NOT here -- so\n // it silently became a second copy of the case below it, and both passed. The\n // existence assertion is what stops that from happening again the next time\n // the named file moves.\n const neighbour = resolve(SELF, '..', 'check-control-bytes.mjs');\n t('the neighbour fixture still exists (or the next case tests nothing)', existsSync(neighbour), neighbour);\n t('an unrelated existing file is not this module', !invokedAs(neighbour, SELF));\n" + } + ] + } + ] +}