From 6fc4612e1b6467dd2cd42735539d22040d09f92d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 05:36:46 +0000 Subject: [PATCH] =?UTF-8?q?feat(pm):=20give=20check-half-states=20a=20stan?= =?UTF-8?q?ding=20caller=20=E2=80=94=20scheduled=20patrol=20workflow=20+?= =?UTF-8?q?=20markdown=20anchor=20report?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sweeper carried thirteen predicates and no calendar: its documented consumer was "a PM seat's patrol round", and the live sweep cannot run inside a PM session container at all (#7412 class 1). So it watched nothing. - `.github/workflows/half-state-patrol.yml`: scheduled 4x/day at :37 (offset from the hourly triage Routine so the patrol never reads the board mid-heal), runs the live sweep on a runner where the transport prerequisite is met, and rewrites ONE pinned anchor issue in place. Never a comment per run; edit history is the archive. - `--format=markdown` / `--provenance=` on the sweeper: the anchor body is rendered by the script, where --self-test pins it. Loud H13 P0-SUSPECT rows sort above the fold and out of truncation's reach; the plain terminal output is byte-identical to before. Report-only stays report-only: no H-predicate becomes a gate, no label is ever written, findings never fail the run. The job fails only when the sweep could not RUN or its report could not be delivered — a stale anchor reads exactly like a clean board, which is the failure this card exists to end. Part of #9844 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AeA3nU1B5Q2pgxqxgUrexd --- .github/workflows/half-state-patrol.yml | 254 ++++++++++++++++++ scripts/pm/check-half-states.mjs | 335 +++++++++++++++++++++++- 2 files changed, 579 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/half-state-patrol.yml diff --git a/.github/workflows/half-state-patrol.yml b/.github/workflows/half-state-patrol.yml new file mode 100644 index 0000000000..97017627e1 --- /dev/null +++ b/.github/workflows/half-state-patrol.yml @@ -0,0 +1,254 @@ +name: Half-State Patrol + +# The standing caller for `scripts/pm/check-half-states.mjs` (#9844). +# +# ## Why a workflow, and not "a seat should run it" +# +# The sweeper carries thirteen predicates over the dispatch protocol's +# label/assignee/PR invariants, and for most of its life its documented consumer +# was "a PM seat's patrol round" — which is to say, nobody's calendar. A shift +# covering two lanes declared a queue empty from memory while eight malformed +# claims (H2) and an unenumerated backlog sat on the board. Not one predicate had +# fired. A healing mechanism with no scheduled caller heals only in the +# counterfactual, and an alarm added to a script nobody runs is still silence. +# +# "Some seat should run it" also kept not happening for a MEASURED reason, not a +# discipline one: the live sweep cannot run inside a PM session container at all +# (#7412 class 1 — api.github.com refuses that egress in both directions, with and +# without a token). The fix therefore had to move the caller somewhere the +# transport prerequisite is actually met. A GitHub Actions runner with the +# workflow's own `GITHUB_TOKEN` is that place — #7412 class 2, the triage Routine +# container, is the same shape and measured reachable with 15,000 core quota. +# +# ## What lands where +# +# One pinned ANCHOR ISSUE, rewritten in place every run (`ANCHOR_ISSUE` below). +# Never a comment per run: the board is one board, a per-run comment stream would +# be a second tracker that nobody prunes, and GitHub's edit history is already the +# archive this needs. The body is owned end-to-end by the generator, so no run can +# leave half of it stale. +# +# The `Swept` timestamp in that body is the patrol's heartbeat and is deliberately +# refreshed even when the findings are unchanged: a timestamp that stops advancing +# is how a reader learns the standing caller died. That is the whole defect class +# this workflow exists to close, so the run must not "optimize away" the no-op +# edit that proves it is alive. +# +# ## Report-only, and the one thing that is NOT report-only +# +# Findings never fail anything. A completed sweep exits 0 whether it found 0 or 40 +# half-states, this job never writes a label, never closes a card, never fixes a +# state, and no H-predicate is a blocking gate — the script's own header argues +# that at length (a half-state is a fact about a live shared board, not about +# whichever PR happens to run CI next). +# +# The job DOES fail when the sweep could not run, or when its report could not be +# delivered. That is not a gate on the board; it is the patrol reporting its own +# death. A workflow that quietly does nothing because a credential lapsed is the +# exact shape this repo keeps having to fix (#4449, #9575), and it is doubly +# unacceptable here: silent non-delivery would leave a stale anchor body that +# reads exactly like a clean board — the #4690 failure ("could not read the input" +# must never look like "input is clean") with a timestamp on it. Failing costs +# nobody a PR: this workflow gates no branch and blocks no queue. + +on: + schedule: + # Four times a day, six hours apart, at :37 past the hour. + # + # The minute is offset ON PURPOSE. The triage Routine that heals these same + # states fires hourly near the top of the hour, and a patrol landing at the + # same minute would keep reading the board mid-heal — reporting half-states + # the healer is in the middle of pairing, i.e. manufacturing findings that + # clear themselves. :37 puts this sweep in the quiet part of the healer's + # cycle in both directions. Four runs/day rather than hourly: H13's own + # threshold is 2h and the incident it comes from sat ~26h, so six-hourly + # detection is two orders of magnitude better than the status quo (never) + # while staying cheap on the core quota this sweep shares with the loop's + # hot path. + - cron: '37 1,7,13,19 * * *' + workflow_dispatch: {} + # Changes to the patrol itself get exercised before they merge — the same + # posture as engine-split-metric.yml. On a pull_request run the sweep still + # executes (that is the point: the transport, the flags and the rendering are + # proven on a real runner), but the anchor write is skipped and the rendered + # body goes to the run's step summary instead. A PR must never rewrite the + # board's pinned view. + pull_request: + paths: + - 'scripts/pm/check-half-states.mjs' + - '.github/workflows/half-state-patrol.yml' + +# Least privilege: this job reads the repo and writes exactly one issue BODY. +# `issues: write` is the narrowest scope GitHub offers for that edit; the job +# never uses it for labels, comments, assignees or state, and the sweeper it +# calls is read-only against the API by construction. +permissions: + contents: read + issues: write + +# One patrol at a time. A scheduled run overlapping a manual dispatch would have +# two runs racing to rewrite the same body, and the loser's findings would vanish +# with no trace but an edit-history entry. +concurrency: + group: half-state-patrol + cancel-in-progress: false + +env: + # The pinned anchor issue whose body this workflow owns. + # + # TO ROTATE: open a new `tracking`-labeled issue, put its number here, and note + # the handover in the OLD issue's body before closing it (its edit history is + # the archive and does not travel). Nothing else reads this number, so the + # rotation is this one line. + # + # The anchor deliberately carries `tracking` and NO `domain:*` label: `tracking` + # is in the sweeper's own H13_EXEMPT_LABELS, so the anchor can never appear as a + # finding in the sweep it hosts. + ANCHOR_ISSUE: '9857' + +jobs: + patrol: + name: Live half-state sweep + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: '22' + + # No `pnpm install`: the sweeper imports nothing but `node:process` and + # global `fetch`. Installing the workspace here would buy nothing and would + # give a scheduled patrol a lockfile it could fail on. + - name: Run the live sweep + id: sweep + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PROVENANCE: >- + run [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + · commit `${{ github.sha }}` · trigger `${{ github.event_name }}` + run: | + set +e + node scripts/pm/check-half-states.mjs \ + --format=markdown \ + --provenance="$PROVENANCE" \ + > "$RUNNER_TEMP/report.md" 2> "$RUNNER_TEMP/report.err" + code=$? + set -e + # Captured with NO pipe in between. `cmd | tail` would report the + # PIPE's status — `tail` essentially never fails, so a green and a red + # sweep both read as 0, and the script's own header calls this trap out + # by name (its exit codes are 0 / 2 / 3 and the split is the point). + echo "exit_code=$code" >> "$GITHUB_OUTPUT" + echo "check-half-states exited $code" + cat "$RUNNER_TEMP/report.err" >&2 || true + + - name: Update the pinned anchor issue + # A pull_request run proves the sweep; it must not touch the board. + if: github.event_name != 'pull_request' + uses: actions/github-script@v9 + env: + SWEEP_EXIT: ${{ steps.sweep.outputs.exit_code }} + with: + # Delivery is retried, never assumed (#9575): this single PATCH is the + # entire product of the run, and a transient answer from the issues + # endpoint would otherwise discard a completed sweep. + retries: 3 + script: | + const fs = require('fs'); + const path = require('path'); + const exitCode = Number(process.env.SWEEP_EXIT); + const anchor = Number(process.env.ANCHOR_ISSUE); + const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; + const read = (name) => { + try { return fs.readFileSync(path.join(process.env.RUNNER_TEMP, name), 'utf8'); } + catch { return ''; } + }; + + // The composition split, deliberately: a COMPLETED sweep renders its + // own body (in the script, where --self-test pins every property of + // it). Only the did-not-run body is composed here, because saying + // "my callee failed" is the caller's job and the script's classified + // output is already the authored explanation — this wraps it, it + // does not re-word it. + let body; + if (exitCode === 0) { + body = read('report.md'); + if (!body.trim()) { + throw new Error('the sweep exited 0 but produced an empty report — refusing to blank the anchor'); + } + } else { + const classified = (read('report.err') || read('report.md') || '(no output captured)').trim(); + const kind = exitCode === 3 + ? 'PREREQUISITE NOT MET — the runner could not reach the board' + : 'SWEEP FAILED — an unclassified failure'; + body = [ + 'os-half-state-sweep — machine-findable marker for this generated view.', + '', + `# ⛔ THE SWEEP DID NOT RUN (exit ${exitCode})`, + '', + `_Attempted ${new Date().toISOString()} · [run log](${runUrl}) · ${kind}._`, + '', + 'Nothing below is a finding. **No issue was judged**, so this body says nothing about whether', + 'the board carries half-states — it is not a clean board and it is not a dirty one, it is no', + 'reading at all. A sweep that could not run must never read as a clean board.', + '', + 'The standing patrol is DOWN until this is fixed; the previous run\'s findings are in this', + 'issue\'s edit history. The sweeper\'s own classified output:', + '', + '```', + classified, + '```', + ].join('\n'); + } + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: anchor, + body, + }); + core.info(`anchor #${anchor} updated (${body.length} chars, sweep exit ${exitCode})`); + + - name: Publish the rendered body to the run summary + # Always: on a PR this IS the delivery, and on a scheduled run it makes + # the run log self-contained when someone opens it after an alert. + if: always() + run: | + { + echo "### Half-state patrol — sweep exit ${{ steps.sweep.outputs.exit_code }}" + echo + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "_Anchor write skipped: a pull_request run proves the sweep without touching the board._" + echo + fi + echo '
Rendered anchor body' + echo + cat "$RUNNER_TEMP/report.md" 2>/dev/null || echo '(no report produced)' + echo + echo '
' + echo + echo '
stderr' + echo + echo '```' + cat "$RUNNER_TEMP/report.err" 2>/dev/null || true + echo '```' + echo + echo '
' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Fail the run if the sweep could not run + # LAST, on purpose: the anchor is updated with the did-not-run report + # BEFORE the job goes red. Land the truth, then raise the alarm — a run + # that failed early would leave the previous body in place with its old + # timestamp, which is precisely the stale-reads-as-clean shape above. + # + # Findings are NOT a failure condition and never appear here: exit 0 with + # 40 half-states is a successful patrol. + if: steps.sweep.outputs.exit_code != '0' + run: | + echo "::error::check-half-states exited ${{ steps.sweep.outputs.exit_code }} — the standing patrol did not read the board. See the anchor issue and this run's stderr." + exit 1 diff --git a/scripts/pm/check-half-states.mjs b/scripts/pm/check-half-states.mjs index 94cfed4c50..8c948f8bf3 100644 --- a/scripts/pm/check-half-states.mjs +++ b/scripts/pm/check-half-states.mjs @@ -8,6 +8,31 @@ * node scripts/pm/check-half-states.mjs # sweep the live repo * node scripts/pm/check-half-states.mjs --probe # can live mode run HERE? (no sweep) * node scripts/pm/check-half-states.mjs --self-test # verify the predicates offline + * node scripts/pm/check-half-states.mjs --format=markdown [--provenance='…'] + * # the same sweep, rendered for an issue body + * + * ## The standing caller (#9844) + * + * For most of this file's life its consumer was "a PM seat's patrol round" — + * which is to say, nobody's calendar. A shift covering two lanes declared a + * queue empty from memory while eight malformed claims (H2) and an unenumerated + * backlog sat on the board; not one predicate here had fired, because nothing + * standing ever called them. An alarm added to a script nobody runs is still + * silence, and the transport note below explains why "some seat should run it" + * kept not happening: the live sweep cannot run inside a PM session container + * at all. + * + * So the caller is now `.github/workflows/half-state-patrol.yml` — a scheduled + * workflow, on a runner where the transport prerequisite is met, landing the + * result by rewriting ONE pinned anchor issue in place (edit history is the + * archive; never a comment per run). `--format=markdown` exists for exactly + * that consumer, and `--provenance` lets the caller stamp its own run identity + * into a body this script otherwise renders repo-agnostically. + * + * What did NOT change, and must not: this stays report-only. The workflow never + * fails a build over findings and never writes a label. The one thing it DOES + * treat as a failure is its own non-delivery — a patrol that cannot land its + * report is the disease, not a finding. * * ## Why report-only, and why the exit code is ALWAYS 0 on a completed sweep * @@ -740,6 +765,16 @@ export const H13_EXEMPT_LABELS = ['tracking', 'status:parked', 'qa-run']; */ export const DOMAIN_HALF_STATE_STALE_HOURS = 2; +/** + * The prefix H13 stamps on a self-declared-P0 row. Exported because a SECOND + * reader now depends on it: the markdown renderer sorts loud rows to the top + * of the anchor body (see `renderMarkdown`). A shared constant, not a string + * literal in two files — the loudness and the thing that reads the loudness + * must never be able to drift apart, which is the whole failure family this + * script belongs to. + */ +export const P0_SUSPECT_MARKER = '🚨 P0-SUSPECT:'; + /** * Whether the card's own title/body self-declares P0 / data-integrity — the * incident card carried its emergency-triage trigger in its body while the @@ -778,12 +813,204 @@ export function h13DomainWithoutPmState(issue, nowMs = Date.now()) { `inventory: pair the domain label with its pm-state in one write, oldest first.`; if (!h13SelfDeclaredP0(issue)) return base; return ( - `🚨 P0-SUSPECT: the card's own title/body self-declares P0/data-integrity, and for that ` + + `${P0_SUSPECT_MARKER} the card's own title/body self-declares P0/data-integrity, and for that ` + `class the emergency-triage channel (immediate triage subagent) is the mandated move, ` + `never the hourly Routine. ${base}` ); } +// --------------------------------------------------------------------------- +// Report rendering — pure over (findings, counts), so `--self-test` pins both +// media offline. The live sweep below picks a renderer and prints it; nothing +// about WHAT is swept or WHICH predicates fire depends on the format. +// +// Two media exist because this script gained a second consumer. The first is a +// terminal: a patrol round reads the plain lines and scrolls. The second is a +// pinned anchor ISSUE BODY, rewritten in place by the scheduled workflow that +// gave this sweeper a standing caller (`.github/workflows/half-state-patrol.yml`) +// — a surface with a fold, a hard size cap, and readers who will not scroll. +// That difference, and only that, is why the two renderers order rows +// differently; see `renderMarkdown`. +// --------------------------------------------------------------------------- + +/** Accepted `--format` values. An unrecognized one is a usage error (exit 2). */ +export const OUTPUT_FORMATS = ['plain', 'markdown']; + +/** + * GitHub's hard cap on an issue body, and the budget the markdown renderer + * keeps under it. A body that exceeds the cap is REJECTED by the API — the + * whole run's report would vanish over one long row — so the renderer trims + * and SAYS it trimmed. Silent truncation is the #4690 shape (an unreadable + * result must not read as a clean one), so the omission notice is part of the + * rendered body, never a log line the anchor's reader never sees. + */ +export const ISSUE_BODY_LIMIT = 65536; +export const MARKDOWN_BODY_BUDGET = 60000; + +/** Is this finding one of H13's louder self-declared-P0 rows? */ +export function isLoudFinding(message) { + return String(message ?? '').startsWith(P0_SUSPECT_MARKER); +} + +/** + * 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 + * clean" and "nothing was swept", and it carries the report-only contract so + * a reader who sees only this line cannot mistake it for a gate verdict. + * + * @param {{ repo: string, issues: number, unscoped: number, prs: number, merged: number }} counts + * @param {number} findingCount + */ +export function summaryLine(counts, findingCount) { + return ( + `check-half-states: swept ${counts.issues} open pm-/p0-labeled issue(s), ${counts.unscoped} open ` + + `issue(s) in H13's unscoped pass, ${counts.prs} open PR(s) ` + + `and ${counts.merged} recently-merged PR(s) in ${counts.repo} — ${findingCount} half-state(s) found. ` + + `Report-only: findings are patrol input, not a gate verdict.` + ); +} + +/** + * The terminal report — byte-identical to what this script printed before the + * format switch existed. Findings arrive already sorted by issue number and + * that order is kept: a terminal has no fold, so there is nothing for a + * priority sort to buy here, and changing it would churn every seat's habit. + */ +export function renderPlain(findings, counts) { + const lines = findings.map( + ([issue, code, msg]) => ` ${code} #${issue.number} ${msg}\n ${issue.html_url}`, + ); + lines.push(summaryLine(counts, findings.length)); + return lines.join('\n'); +} + +/** + * Provenance is a one-line string the CALLER supplies (`--provenance=…`): the + * script knows it swept, it does not know it was a GitHub Actions run #123 at + * commit abc1234, and teaching it would couple a repo-agnostic sweeper to one + * caller. Collapsed to a single line and length-capped here rather than + * trusted: it is interpolated into a markdown italic line, and a newline in it + * would silently break the header apart. + */ +export function normalizeProvenance(text) { + return String(text ?? '') + .replace(/\s+/g, ' ') + .trim() + .slice(0, 300); +} + +/** + * The anchor-body report. + * + * Row order differs from `renderPlain` on purpose, and the reason is the + * medium: this body is READ AT A FOLD and TRIMMED AT A CAP. A P0-SUSPECT row + * sitting at position 38 of 40 — or trimmed off the end entirely — is exactly + * the silence this sweeper's standing caller exists to end, so loud rows sort + * first and are therefore the last things truncation could ever reach. Within + * each band the issue-number order is preserved, so the list is still stable + * run to run and diffable in the anchor's edit history. + * + * The header is deliberately restated every run rather than left as a + * hand-written preamble the workflow must not clobber: the body is owned by + * this generator, end to end, so there is no half of it that a run can leave + * stale. First line is a bare literal marker with no angle brackets — the + * board's markers are grepped as literal text, never as comment syntax, + * because GitHub's body sanitizer eats short `<…>` fragments on write. + */ +export function renderMarkdown(findings, counts, options = {}) { + const provenance = normalizeProvenance(options.provenance); + const sweptAt = options.sweptAt instanceof Date ? options.sweptAt : new Date(); + const rows = [...findings].sort( + (a, b) => Number(isLoudFinding(b[2])) - Number(isLoudFinding(a[2])) || a[0].number - b[0].number, + ); + const loudCount = rows.filter(([, , msg]) => isLoudFinding(msg)).length; + + const head = [ + 'os-half-state-sweep — machine-findable marker for this generated view.', + '', + '**Generated view — not a second tracker.** Authority lives on each card and PR (one-board rule);' + + ' this body is rewritten IN PLACE by the scheduled patrol workflow' + + ' (`.github/workflows/half-state-patrol.yml`) on every run, and the edit history is the archive.' + + ' **Report-only**: every row is patrol input, never a gate verdict, and this sweep never fixes a' + + ' state. Each predicate and the protocol clause it enforces are documented in' + + ' `scripts/pm/check-half-states.mjs`.', + '', + `_Swept ${sweptAt.toISOString()}${provenance ? ` · ${provenance}` : ''}_`, + '', + 'The timestamp above is the patrol\'s own heartbeat: a `Swept` line that stops advancing means the' + + ' standing caller died, which is the failure this anchor was created to make visible. Read it' + + ' before you read the rows.', + '', + ]; + + if (loudCount > 0) { + head.push( + `🚨 **${loudCount} P0-SUSPECT row(s) in this sweep** — for that class the mandated move is the` + + ' emergency-triage channel (an immediate triage subagent), never waiting for the next hourly' + + ' Routine fire. They are sorted to the top of the list below.', + '', + ); + } + + head.push(`**${summaryLine(counts, rows.length)}**`, ''); + + if (rows.length === 0) { + head.push( + '✅ No half-states found in this sweep. This line means the board was READ and is clean — a sweep' + + ' that could not RUN replaces this whole body with a prerequisite/failure report instead, so a' + + ' green anchor is never the sound of a broken sweeper.', + ); + return head.join('\n'); + } + + head.push('### Findings', '', ''); + const body = head.join('\n'); + const rendered = []; + let used = body.length; + for (let i = 0; i < rows.length; i++) { + const [issue, code, msg] = rows[i]; + const line = `- **${code}** [#${issue.number}](${issue.html_url}) — ${msg}`; + // Reserve room for the omission notice itself, so the trim can always + // announce itself even when it fires on the very last row. + const notice = `\n- _… ${rows.length - i} further row(s) omitted to fit GitHub's issue-body limit; the full list is in the workflow run log._`; + if (used + line.length + 1 + notice.length > MARKDOWN_BODY_BUDGET) { + rendered.push(notice.slice(1)); + break; + } + rendered.push(line); + used += line.length + 1; + } + return `${body}${rendered.join('\n')}`; +} + +/** + * Output options off argv. Pure, so `--self-test` pins the usage errors too: + * a mistyped `--format` must be a LOUD non-zero exit, never a silent fallback + * to plain text that would leave the anchor updated with an unreadable body. + * + * @param {string[]} argv + * @returns {{ format: string, provenance: string, error?: string }} + */ +export function parseOutputOptions(argv) { + const out = { format: 'plain', provenance: '' }; + for (const arg of argv ?? []) { + const fmt = /^--format=([\s\S]*)$/.exec(arg); + if (fmt) { + if (!OUTPUT_FORMATS.includes(fmt[1])) { + return { + ...out, + error: `unknown --format=${fmt[1]} — expected one of: ${OUTPUT_FORMATS.join(', ')}`, + }; + } + out.format = fmt[1]; + continue; + } + const prov = /^--provenance=([\s\S]*)$/.exec(arg); + if (prov) out.provenance = normalizeProvenance(prov[1]); + } + return out; +} + // --------------------------------------------------------------------------- // Transport prerequisite — the classifier (pure) and the probe that feeds it. // @@ -1136,7 +1363,7 @@ async function listIssues(label) { return out; } -async function sweep() { +async function sweep(options = {}) { // Answered once, before any listing — so an unusable transport costs ONE // classified verdict instead of a raw HTTP status from whichever label page // happened to go first (`pm:dispatched`, in the failure #7412 recorded). @@ -1156,14 +1383,17 @@ async function sweep() { } findings.sort((a, b) => a[0].number - b[0].number); - for (const [issue, code, msg] of findings) { - console.log(` ${code} #${issue.number} ${msg}\n ${issue.html_url}`); - } + const counts = { + repo: OWNER_REPO, + issues: seen.size, + unscoped: seenUnscoped.size, + prs: seenPrs.size, + merged: seenMerged.size, + }; console.log( - `check-half-states: swept ${seen.size} open pm-/p0-labeled issue(s), ${seenUnscoped.size} open ` + - `issue(s) in H13's unscoped pass, ${seenPrs.size} open PR(s) ` + - `and ${seenMerged.size} recently-merged PR(s) in ${OWNER_REPO} — ${findings.length} half-state(s) found. ` + - `Report-only: findings are patrol input, not a gate verdict.`, + options.format === 'markdown' + ? renderMarkdown(findings, counts, { provenance: options.provenance }) + : renderPlain(findings, counts), ); } @@ -1648,6 +1878,82 @@ function selfTest() { 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); + // -- report rendering, both media (#9844) --------------------------------- + // The standing caller writes the markdown into a pinned issue body, so the + // properties pinned here are the ones a broken body would cost: the plain + // output must not have moved, the loud rows must outrank truncation, the + // trim must announce itself, and a mistyped --format must be loud. + const finding = (number, code, msg) => [{ number, html_url: `https://example.test/${number}` }, code, msg]; + const counts = { repo: 'o/r', issues: 3, unscoped: 4, prs: 5, merged: 6 }; + const quietRow = finding(200, 'H2', 'assignee set but no claim comment on the thread'); + const loudRow = finding(900, 'H13', `${P0_SUSPECT_MARKER} the card self-declares P0. base sentence.`); + + // The plain renderer is the pre-#9844 output, unchanged: two lines per + // finding (code/number/message, then the URL indented), summary last. + t( + 'plain: a finding renders as the pre-existing two-line shape', + renderPlain([quietRow], counts).split('\n').slice(0, 2).join('|'), + ' H2 #200 assignee set but no claim comment on the thread| https://example.test/200', + ); + t('plain: the summary sentence ends the report', renderPlain([quietRow], counts).endsWith('not a gate verdict.'), true); + // renderPlain does NOT reorder: the live sweep hands it findings already + // sorted by issue number, and a terminal has no fold for a priority sort to + // buy anything at. Pinned in the direction that would actually regress — + // someone "helpfully" giving the plain path the markdown sort — by feeding + // it loud-first input and requiring the loud row to stay where it was put. + t('plain: preserves the caller\'s order, applying no priority sort', renderPlain([loudRow, quietRow], counts).indexOf('#900') < renderPlain([loudRow, quietRow], counts).indexOf('#200'), true); + t('plain: …and the markdown renderer on the same input DOES sort loud first', renderMarkdown([quietRow, loudRow], counts).indexOf('#900') < renderMarkdown([quietRow, loudRow], counts).indexOf('#200'), true); + t('summaryLine: names what was READ, not only what was found', summaryLine(counts, 0).includes('swept 3 open pm-/p0-labeled issue(s)'), 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. + t('loudness: H13\'s P0 line is recognised by the renderer', isLoudFinding(h13DomainWithoutPmState(domainCard(['domain:engine-core'], hoursAgo(26), p0Body), NOW)), true); + t('loudness: H13\'s base line is not', isLoudFinding(h13DomainWithoutPmState(domainCard(['domain:engine-core'], hoursAgo(26)), NOW)), false); + + // Fold discipline: loud first, issue-number order within each band. + const mixed = renderMarkdown([quietRow, loudRow, finding(100, 'H1', '`pm:dispatched` with no assignee')], counts); + t('markdown: loud rows sort above quiet ones', mixed.indexOf('#900') < mixed.indexOf('#100'), true); + t('markdown: quiet rows keep issue-number order', mixed.indexOf('#100') < mixed.indexOf('#200'), true); + t('markdown: the alarm line counts the loud rows', mixed.includes('**1 P0-SUSPECT row(s) in this sweep**'), true); + t('markdown: no alarm line when nothing is loud', renderMarkdown([quietRow], counts).includes('P0-SUSPECT row(s) in this sweep'), false); + t('markdown: rows are links, not bare numbers', mixed.includes('[#200](https://example.test/200)'), true); + t('markdown: the literal marker leads the body (no angle brackets to sanitize)', mixed.startsWith('os-half-state-sweep'), true); + t('markdown: the body carries no HTML-comment marker the sanitizer could eat', mixed.includes('