From 2497d75dfa686d6bb0e5bf21784e7ce94a7200b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 22:53:35 +0000 Subject: [PATCH] feat(tooling): give check-release-section-coverage a nightly standing caller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate's advisory half runs on every PR inside lint.yml; its `--strict` half had no caller at all, so the only thing that ever executed was one green job's log line among 106 steps. Measured before building: the 17.2.0 gap has been live for 40 hours, 262 PRs landed on main in that window each emitting the two advisory annotations, and `content/docs/releases/` took zero commits. #10268's acceptance criterion is "visible within a day of a minor publishing", already exceeded with the advisory in place. Verified against a real PR head sha through the check-runs API, the annotation is registered at `path=.github` (no `file=` parameter), rides a green required check among 31 non-failing check runs, and is addressed to whoever opened the next PR — who neither created the debt nor owns the remedy. Volume is not the problem: those were the only two `##[warning]` lines in a 4004-line job log. Audience is. So this adds the reader, and leaves the advisory exactly as it is. The patrol runs the gate TWICE and the order is load-bearing: the gate returns 1 for both "findings under --strict" and "broken instrument", so advisory-first establishes instrument health and only then is `--strict`'s exit 1 unambiguous. That gives `--strict` a real standing caller as a machine-readable predicate while the job itself stays green — findings land in a pinned anchor issue, never in a chronically red scheduled job. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx --- .github/workflows/release-coverage-patrol.yml | 286 ++++++++++++++++++ scripts/render-release-coverage-anchor.mjs | 252 +++++++++++++++ 2 files changed, 538 insertions(+) create mode 100644 .github/workflows/release-coverage-patrol.yml create mode 100644 scripts/render-release-coverage-anchor.mjs diff --git a/.github/workflows/release-coverage-patrol.yml b/.github/workflows/release-coverage-patrol.yml new file mode 100644 index 0000000000..3da99d7be5 --- /dev/null +++ b/.github/workflows/release-coverage-patrol.yml @@ -0,0 +1,286 @@ +name: Release-Coverage Patrol + +# The standing caller for `scripts/check-release-section-coverage.mjs` (#10803). +# +# ## Why a workflow, when the gate ALREADY runs on every PR +# +# Unlike `half-state-patrol.yml` and `required-set-patrol.yml`, this check does +# not need a runner for TRANSPORT reasons. It is fully offline — it reads +# `packages/spec/CHANGELOG.md` and `content/docs/releases/**` straight out of the +# tree, opens no socket, and runs inside `lint.yml` on every pull request today. +# Measured, not assumed: a network-shaped scan of the script (`fetch(`, `http`, +# `api.github.com`, `execSync`, `child_process`, `octokit`, `GITHUB_TOKEN`) +# matches ZERO lines, against 4 matches each in those two patrols' workflows. +# +# So the reason for a standing caller here is AUDIENCE, not reachability, and it +# was measured on a live gap rather than argued (#10803): +# +# 17.2.0's CHANGELOG heading landed 2026-08-23T06:29:35Z. Over the next 40 +# hours, 262 pull requests landed on `main`. Every one of their `lint.yml` +# runs emitted the two advisory `::warning::` annotations. Commits touching +# `content/docs/releases/` in that window: ZERO. Open PRs authoring the +# section: ZERO. #10268's acceptance criterion is "visible within a day of a +# minor publishing" — already exceeded, with the advisory in place and firing. +# +# Three structural reasons that reading is not bad luck, all verified against a +# real PR head sha through the check-runs API: +# +# 1. The annotation is registered at `path=.github`, line 19/20 — the WORKFLOW +# file, because the gate emits `::warning::` with no `file=` parameter. It +# therefore cannot appear on `content/docs/releases/v17.mdx` in the +# Files-changed tab, which is the only place a reviewer reads by default. +# 2. It rides a GREEN required check. That PR head carried 31 check runs, 0 of +# them failing, which GitHub collapses to "All checks have passed". It is +# step 74 of 106 in that job. +# 3. It is addressed to whoever opened the next PR. That author did not create +# the debt and does not own the remedy. A finding every PR author sees and +# no PR author owns is the shape that stays unfixed for 25 days. +# +# Volume is NOT the problem and this file does not pretend otherwise: those two +# annotations were the only `##[warning]` lines in a 4004-line job log, and 2 of +# only 5 annotations across all 31 checks. The advisory is quiet and correct. It +# is simply pointed at the wrong reader, and no amount of shouting on a PR fixes +# that. This workflow adds the reader; it does not replace the advisory. +# +# ## Why this calls `--strict`, and why it still cannot go red +# +# `--strict` promotes findings to exit 1 "for a caller that OWNS the remedy". It +# had no standing caller at all, which is the defect #10803 records. It has one +# here — as a PREDICATE, never as a verdict. +# +# The two runs below are not redundant, and the order is load-bearing. The gate +# deliberately returns 1 for BOTH "findings, under --strict" and "the instrument +# is broken", so `--strict` alone is ambiguous and could not be branched on: +# +# run 1, ADVISORY — exit 0 means the instrument READ the corpus (findings or +# not); non-zero means it could not, and that is fatal in +# every mode by the gate's own design. This run also +# produces the authored, human-readable finding text. +# run 2, --STRICT — with the instrument already proven healthy by run 1, +# exit 1 now means exactly one thing: findings exist. +# +# That is what lets the patrol branch on a machine-readable boolean instead of +# grepping prose, while the JOB stays green whenever findings exist. A `--strict` +# run stays red for as long as the prose is unwritten — 25 days, in the 16.1.0 +# case — and a chronically red scheduled job is how people learn to skim red, +# which `prerelease-pin-watch.yml`'s header warns about directly. Findings land +# in the anchor issue; they never colour this job. +# +# ## What lands where +# +# One pinned ANCHOR ISSUE, rewritten in place every run (`ANCHOR_ISSUE` below) — +# `half-state-patrol.yml`'s shape and its argument, which applies verbatim here: +# never a comment per run, because a per-run comment stream is a second tracker +# nobody prunes and GitHub's edit history is already the archive this needs. +# +# It fits this finding particularly well. The population is tiny — 5 in-scope GA +# minors exist in total (16.0.0, 16.1.0, 17.0.0, 17.1.0, 17.2.0) — so the anchor +# is quiet almost always, and the remedy is owner-shaped: one person writes one +# curated section. An issue can be assigned. An annotation cannot. +# +# The `Swept` timestamp is the patrol's heartbeat and is refreshed even when the +# findings are unchanged: a timestamp that stops advancing is how a reader learns +# the standing caller died. The run must not "optimize away" that no-op edit. +# +# ## Report-only, and the one thing that is NOT report-only +# +# Findings never fail anything. This job never writes release prose — it cannot, +# and the gate's header argues why at length: a curated section is a judgement +# about what is user-facing, read out of 69 package changelogs, and this gate +# GENERATES NOTHING. +# +# 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 release pages; it is the patrol reporting +# its own death. Silent non-delivery would leave a stale anchor body that reads +# exactly like a clean corpus — "could not read the input" must never look like +# "input is clean" (#4690), with a heartbeat on it. Failing costs nobody a PR: +# this workflow gates no branch and blocks no queue. + +on: + # Nightly. #10268's acceptance criterion is "visible within a day of a minor + # publishing", so the cadence is a day, not a week. Offset from + # half-state-patrol.yml's `37 1,7,13,19` so two patrols never contend for the + # same runner minute. + schedule: + - cron: '19 4 * * *' + workflow_dispatch: {} + # Changes to the patrol itself get exercised before they merge — the same + # posture half-state-patrol.yml keeps. On a pull_request run the sweep still + # executes (that is the point: the flags, the exit-code split 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/check-release-section-coverage.mjs' + - '.github/workflows/release-coverage-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 gate it calls +# is read-only against the filesystem 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: release-coverage-patrol + cancel-in-progress: false + +env: + # The pinned anchor issue whose body this workflow owns. + # + # Resolution: the repository variable `RELEASE_COVERAGE_ANCHOR_ISSUE` if set, + # else this repo's own pinned number, else EMPTY — and empty makes the job + # refuse to write rather than guess. The literal is guarded by the repository + # name on purpose: an unguarded fallback would let a verbatim copy in a sibling + # repo rewrite ITS #11879 — some unrelated card — with this repo's findings, + # silently, every night. A number is only meaningful in the repo it was minted + # in. + # + # TO ROTATE (here): open a new `tracking`-labeled issue, put its number below, + # and note the handover in the OLD issue's body before closing it (its edit + # history is the archive and does not travel). + # TO ADOPT (a sibling repo): change NOTHING here — set the repository variable. + # + # ⚠️ Folded scalar, and every continuation line sits at the SAME indent on + # purpose: a more-indented line in a `>-` block keeps its newline literally, + # which would hand the expression parser a multi-line string instead of one + # expression. + ANCHOR_ISSUE: >- + ${{ vars.RELEASE_COVERAGE_ANCHOR_ISSUE + || (github.repository == 'objectstack-ai/objectstack' && '11879') + || '' }} + +jobs: + patrol: + name: Release section-coverage sweep + runs-on: ubuntu-latest + timeout-minutes: 10 + 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 gate imports `node:fs` and one local module. + # Installing the workspace here would buy nothing and would give a + # scheduled patrol a lockfile it could fail on. + # + # No `--self-test` either. lint.yml runs it on every pull request, so this + # job would be re-proving a property already proven on the commit it just + # checked out — and two node invocations in one `run:` block is precisely + # the shape `check:step-collector` exists to refuse, because the first + # one's failure is masked by the second. + - name: Sweep the corpus + id: sweep + run: | + # ── run 1: ADVISORY. Exit code answers "did the instrument READ the + # corpus", which is the only thing that may colour this job. + set +e + node scripts/check-release-section-coverage.mjs \ + > "$RUNNER_TEMP/report.txt" 2> "$RUNNER_TEMP/report.err" + advisory_code=$? + set -e + # Captured with NO pipe in between. `cmd | tail` would report the + # PIPE's status — `tail` essentially never fails, so a healthy and a + # broken instrument would both read as 0. + echo "advisory_code=$advisory_code" >> "$GITHUB_OUTPUT" + echo "advisory run exited $advisory_code" + + # ── run 2: --STRICT, used ONLY as a findings predicate. Meaningful + # solely because run 1 has already established the instrument is + # healthy; the gate returns 1 for a broken instrument too. + strict_code=0 + if [ "$advisory_code" -eq 0 ]; then + set +e + node scripts/check-release-section-coverage.mjs --strict \ + > "$RUNNER_TEMP/strict.txt" 2> "$RUNNER_TEMP/strict.err" + strict_code=$? + set -e + echo "strict run exited $strict_code" + else + echo "instrument is broken — skipping the --strict predicate" + fi + echo "strict_code=$strict_code" >> "$GITHUB_OUTPUT" + + cat "$RUNNER_TEMP/report.err" >&2 || true + + - name: Resolve the anchor issue + # An install with no anchor configured has nowhere to land its report, + # and the ONLY safe behaviour is to say so loudly. The two alternatives + # are both the failure this file exists to prevent: guessing a number + # would rewrite an unrelated card in this repo, and skipping the write + # quietly would leave a patrol that runs, finds, and tells nobody — + # indistinguishable from a clean corpus. + # + # Placed AFTER the sweep so the run summary still carries the findings, + # and skipped on a pull_request run, which never writes an anchor. + if: github.event_name != 'pull_request' + run: | + if [ -z "${ANCHOR_ISSUE//[[:space:]]/}" ]; then + echo "::error::No anchor issue configured for ${{ github.repository }}. The sweep RAN (see the run summary) but has nowhere to land. Open a \`tracking\`-labeled anchor issue in this repo and set the repository variable RELEASE_COVERAGE_ANCHOR_ISSUE to its number (Settings -> Secrets and variables -> Actions -> Variables)." + exit 1 + fi + case "$ANCHOR_ISSUE" in + *[!0-9]*|'') echo "::error::RELEASE_COVERAGE_ANCHOR_ISSUE is '$ANCHOR_ISSUE', which is not an issue number."; exit 1 ;; + esac + echo "anchor: #$ANCHOR_ISSUE in ${{ github.repository }}" + + - name: Render the report + id: render + env: + ADVISORY_CODE: ${{ steps.sweep.outputs.advisory_code }} + STRICT_CODE: ${{ steps.sweep.outputs.strict_code }} + PROVENANCE: >- + run [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + · commit `${{ github.sha }}` · trigger `${{ github.event_name }}` + run: node scripts/render-release-coverage-anchor.mjs > "$RUNNER_TEMP/body.md" + + # The rendered body is the product of the run, so it goes to the step + # summary on EVERY trigger — including the pull_request runs that must not + # touch the anchor. A reader of this workflow's own PR sees exactly what + # would have landed. + - name: Publish the report to the run summary + run: cat "$RUNNER_TEMP/body.md" >> "$GITHUB_STEP_SUMMARY" + + - 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 + with: + # Delivery is retried, never assumed: 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 anchor = Number(process.env.ANCHOR_ISSUE); + const body = fs.readFileSync(path.join(process.env.RUNNER_TEMP, 'body.md'), 'utf8'); + if (!body.trim()) { + throw new Error('the renderer produced an empty body — refusing to blank the anchor'); + } + 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)`); + + # LAST, so a completed sweep always lands its report before the patrol + # raises the alarm about itself — "land the truth, then raise the alarm". + # This is the ONLY step that may redden the job, and findings can never + # reach it: it reads the instrument's verdict, not the corpus's. + - name: Fail if the sweep could not run + if: steps.sweep.outputs.advisory_code != '0' + run: | + echo "::error::check-release-section-coverage could not read the corpus (exit ${{ steps.sweep.outputs.advisory_code }}). The patrol is DOWN — findings, if any, were NOT judged this run. A sweep that could not run must never read as a clean corpus." + exit 1 diff --git a/scripts/render-release-coverage-anchor.mjs b/scripts/render-release-coverage-anchor.mjs new file mode 100644 index 0000000000..f715cbcccd --- /dev/null +++ b/scripts/render-release-coverage-anchor.mjs @@ -0,0 +1,252 @@ +#!/usr/bin/env node +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// render-release-coverage-anchor — composes the anchor-issue body for +// `.github/workflows/release-coverage-patrol.yml` (#10803). +// +// node scripts/render-release-coverage-anchor.mjs # reads the env +// node scripts/render-release-coverage-anchor.mjs --self-test +// +// ## Why this is a script and not eight lines of inline `github-script` +// +// It is the entire product of the patrol run: the one artifact a reader sees. +// Inline JS inside a YAML `script:` block is reachable by no test, so every +// property below — that a broken instrument can never render as a clean corpus, +// that the heartbeat is always present, that the gate's authored prose is +// wrapped rather than re-worded — would be pinned by nothing at all. This repo's +// answer to "a mechanism nobody runs" is the whole reason #10803 exists; a +// renderer nobody can test is the same defect one layer down. +// +// ## The composition split +// +// The gate `check-release-section-coverage.mjs` OWNS its finding prose — it is +// authored, it cites its own measurements, and its `--self-test` pins it. This +// renderer WRAPS that text verbatim inside a fenced block and never re-words it. +// Saying "my callee could not run" is the caller's job and is composed here; +// saying what is wrong with the release pages is the callee's, and is not. +// +// ## The one asymmetry that matters +// +// A sweep that could not run must never render as a clean corpus (#4690). The +// instrument verdict is therefore read FIRST and short-circuits every other +// branch, and the did-not-run body says in its own words that it contains no +// reading at all — not a clean one and not a dirty one. +import { readFileSync } from 'node:fs'; +import { isEntrypoint } from './invoked-as.mjs'; + +const MARKER = 'os-release-coverage-sweep'; + +const PREAMBLE = [ + `${MARKER} — machine-findable marker for this generated view.`, + '', + '**Generated view — not a second tracker.** This body is rewritten IN PLACE by the scheduled ' + + 'patrol workflow (`.github/workflows/release-coverage-patrol.yml`) on every run, and the edit ' + + 'history is the archive. **Report-only**: every finding is patrol input, never a gate verdict. ' + + 'This sweep never writes release prose — it cannot, and the gate\'s own header explains why (a ' + + 'curated section is a judgement about what is user-facing, read out of the per-package ' + + 'changelogs).', +].join('\n'); + +const HEARTBEAT_NOTE = + 'The `Swept` line above is this patrol\'s heartbeat: a timestamp that stops advancing means the ' + + 'standing caller died, which is the failure this anchor exists to make visible. Read it before ' + + 'you read the findings.'; + +/** + * The advisory run's exit code is the INSTRUMENT verdict; the `--strict` run's + * is the findings predicate, and is only meaningful once the instrument is + * known healthy. Encoding that ordering in one place keeps the workflow's two + * runs from being read as interchangeable. + * + * @param {{ advisoryCode: number, strictCode: number }} codes + * @returns {'did-not-run' | 'findings' | 'clean'} + */ +export function verdict({ advisoryCode, strictCode }) { + if (advisoryCode !== 0) return 'did-not-run'; + return strictCode === 0 ? 'clean' : 'findings'; +} + +/** + * @param {object} input + * @param {number} input.advisoryCode + * @param {number} input.strictCode + * @param {string} input.report stdout of the advisory run (the authored prose) + * @param {string} input.errText stderr of the advisory run + * @param {string} input.provenance + * @param {string} input.sweptAt ISO timestamp + * @returns {string} + */ +export function renderBody({ advisoryCode, strictCode, report, errText, provenance, sweptAt }) { + const state = verdict({ advisoryCode, strictCode }); + const stamp = `_Swept ${sweptAt} · ${provenance}_`; + + if (state === 'did-not-run') { + const classified = (errText || report || '(no output captured)').trim(); + return [ + PREAMBLE, + '', + `# ⛔ THE SWEEP DID NOT RUN (exit ${advisoryCode})`, + '', + stamp, + '', + 'Nothing below is a finding. **No release page was judged**, so this body says nothing about ' + + 'whether a published minor is missing its section — it is not a clean corpus 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 corpus.', + '', + 'The standing patrol is DOWN until this is fixed; the previous run\'s findings are in this ' + + 'issue\'s edit history. The gate\'s own classified output:', + '', + '```', + classified, + '```', + '', + HEARTBEAT_NOTE, + ].join('\n'); + } + + if (state === 'clean') { + return [ + PREAMBLE, + '', + '# ✅ Every published minor has its section', + '', + stamp, + '', + 'No findings. Every in-scope GA minor has a heading on its major\'s release page, and every ' + + 'index entry names the newest release of its major.', + '', + '```', + report.trim(), + '```', + '', + HEARTBEAT_NOTE, + ].join('\n'); + } + + return [ + PREAMBLE, + '', + '# ⚠️ A published minor is missing its release-page section', + '', + stamp, + '', + 'The gate\'s findings, verbatim — its prose is authored and cites its own measurements, so this ' + + 'view wraps it rather than re-wording it:', + '', + '```', + report.trim(), + '```', + '', + '**Remedy.** Fold the missing minor into its major\'s release page under the running section ' + + 'both current pages use, and name the newest release in `content/docs/releases/index.mdx`. ' + + 'This is a curated write, not a generated one: `docs/releases-maintenance.md` section 3 is ' + + 'the process, and the section itself is a judgement about what is user-facing, read out of ' + + 'the per-package changelogs.', + '', + 'This is advisory on every pull request by design and stays that way: the two historical gaps ' + + 'ran 5h44m (17.1.0) and 25 days (16.1.0), and hard-failing them would have red 2748 PRs for a ' + + 'debt none of them created. This anchor is the durable, correctly-addressed half — the PR ' + + 'annotation is anchored to `.github` on a green check and is read by whoever opened the next ' + + 'PR, who neither created the debt nor owns the remedy.', + '', + HEARTBEAT_NOTE, + ].join('\n'); +} + +// ── Self-test ──────────────────────────────────────────────────────────────── + +let failures = 0; +let assertions = 0; +function expect(what, ok) { + assertions += 1; + if (ok) { console.log(` ok ${what}`); return; } + failures += 1; + console.error(` FAIL ${what}`); +} + +function selfTest() { + const base = { + report: 'check-release-section-coverage: 2 finding(s)', + errText: '', + provenance: 'run [1](http://x/1)', + sweptAt: '2026-08-24T00:00:00.000Z', + }; + + expect( + 'verdict — a non-zero ADVISORY code is did-not-run whatever --strict said, because a broken ' + + 'instrument makes the predicate meaningless', + verdict({ advisoryCode: 1, strictCode: 0 }) === 'did-not-run' + && verdict({ advisoryCode: 1, strictCode: 1 }) === 'did-not-run', + ); + expect( + 'verdict — instrument healthy + strict 0 is clean; instrument healthy + strict non-zero is ' + + 'findings', + verdict({ advisoryCode: 0, strictCode: 0 }) === 'clean' + && verdict({ advisoryCode: 0, strictCode: 1 }) === 'findings', + ); + + const down = renderBody({ ...base, advisoryCode: 1, strictCode: 0, errText: 'BROKEN INSTRUMENT' }); + expect( + 'did-not-run — never renders as a clean corpus (#4690): it carries the DID NOT RUN heading and ' + + 'none of the clean body\'s claim', + down.includes('THE SWEEP DID NOT RUN') && !down.includes('Every published minor has its section'), + ); + expect( + 'did-not-run — wraps the gate\'s classified stderr rather than re-wording it', + down.includes('BROKEN INSTRUMENT'), + ); + + const clean = renderBody({ ...base, advisoryCode: 0, strictCode: 0, report: 'OK — 5 published minor(s)' }); + expect( + 'clean — states the no-findings verdict and carries the gate\'s own OK line', + clean.includes('Every published minor has its section') && clean.includes('OK — 5 published minor(s)'), + ); + + const found = renderBody({ ...base, advisoryCode: 0, strictCode: 1 }); + expect( + 'findings — names the remedy as a curated write and points at the maintenance process', + found.includes('docs/releases-maintenance.md') && found.includes('curated write'), + ); + expect( + 'findings — wraps the gate\'s authored prose verbatim', + found.includes('check-release-section-coverage: 2 finding(s)'), + ); + + for (const [name, body] of [['did-not-run', down], ['clean', clean], ['findings', found]]) { + expect( + `${name} — carries the machine-findable marker and the heartbeat, so no branch can lose ` + + 'either', + body.startsWith(MARKER) && body.includes('Swept 2026-08-24T00:00:00.000Z') && body.includes(HEARTBEAT_NOTE), + ); + } + + // Counted, never a literal: a hard-coded total silently stops matching the + // moment a case is added, and a self-test that misreports its own size is the + // first thing a reader stops trusting. + console.log(failures === 0 + ? `\nOK render-release-coverage-anchor --self-test: ${assertions} assertions pass` + : `\nFAILED ${failures} of ${assertions} assertion(s)`); + return failures === 0 ? 0 : 1; +} + +function main(argv) { + if (argv.includes('--self-test')) return selfTest(); + + const tmp = process.env.RUNNER_TEMP || '.'; + const read = (name) => { + try { return readFileSync(`${tmp}/${name}`, 'utf8'); } catch { return ''; } + }; + process.stdout.write(renderBody({ + advisoryCode: Number(process.env.ADVISORY_CODE ?? '1'), + strictCode: Number(process.env.STRICT_CODE ?? '0'), + report: read('report.txt'), + errText: read('report.err'), + provenance: process.env.PROVENANCE || '(no provenance)', + sweptAt: new Date().toISOString(), + })); + process.stdout.write('\n'); + return 0; +} + +if (isEntrypoint(import.meta.url)) process.exit(main(process.argv.slice(2)));