Skip to content

docs(handoff): point §0 at the P1 merge batch #946

docs(handoff): point §0 at the P1 merge batch

docs(handoff): point §0 at the P1 merge batch #946

Workflow file for this run

name: CI
on:
push:
branches: [master]
pull_request:
branches: [master]
# Manual mutation sweep (Actions -> CI -> Run workflow). Deliberately NOT on the PR path:
# each mutation re-runs its spec's whole test suite, so all 85 specs is hours, and a gate
# nobody can afford to run is worse than no gate — that is the warning in the specs job's
# own comment. Until 2026-09-13 nothing in this repo ever executed `npm run mutate`.
workflow_dispatch:
inputs:
specs:
description: 'Spec files to run (space separated). Blank = every test/mutations/*.json'
required: false
default: ''
shard:
description: 'Zero-based shard i/N (0/1 = all selected specs)'
required: false
default: '0/1'
jobs:
mutations:
name: Mutation specs (manual)
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 350
steps:
# fetch-depth 0: the mutation harness materializes its workspace with
# `git clone --local` and reads history objects, so a shallow checkout fails it.
# Measured 2026-09-13: with the default depth 1 the sweep died on the FIRST spec
# with exit 2 and produced no verdict lines at all.
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Node 24
uses: actions/setup-node@v5
with:
node-version: '24'
- name: Install dependencies
run: npm ci
# PR/push jobs intentionally do not execute mutation specs: this is a cost boundary.
# Optional parallel form (replace the input shard with matrix.shard in Sweep):
# strategy:
# fail-fast: false
# matrix:
# shard: ['0/4', '1/4', '2/4', '3/4']
# Put strategy at the mutations job level, not inside steps. Each job has its own
# checkout, manifest and artifact; never run two shards in one owner worktree.
- name: Sweep
env:
REQUESTED_SPECS: ${{ inputs.specs }}
REQUESTED_SHARD: ${{ inputs.shard }}
run: |
args=(--shard "${REQUESTED_SHARD:-0/1}" --report-dir "$RUNNER_TEMP/mutation-report")
if [ -n "$REQUESTED_SPECS" ]; then args+=(--specs "$REQUESTED_SPECS"); fi
node scripts/mutation-sweep.mjs "${args[@]}"
- name: Assert verdict coverage
if: always()
run: node scripts/mutation-sweep.mjs --verify "$RUNNER_TEMP/mutation-report"
- name: Publish mutation evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: mutation-report-${{ github.run_id }}-${{ strategy.job-index || 0 }}
path: ${{ runner.temp }}/mutation-report/
if-no-files-found: error
syntax:
name: Node syntax check
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- name: Setup Node 24
uses: actions/setup-node@v5
with:
node-version: '24'
- name: Check JS syntax for all src files
run: |
find src -name '*.js' -type f -print0 | xargs -0 -n1 node --check
- name: Validate package.json
run: node -e "JSON.parse(require('fs').readFileSync('package.json'))"
- name: Validate JSON configs
run: |
for f in $(find . -maxdepth 2 -name '*.json' -not -path './node_modules/*' -not -path './.git/*'); do
echo "Checking $f"
node -e "JSON.parse(require('fs').readFileSync('$f'))"
done
# Mutation-spec consistency. Three baseline/anchor drifts reached master in one week
# (#241's anchors, retry-rescue 82/87 -> 88, reasoning-continuity 289 -> 300) and the
# test suite saw none of them: it does not run specs. The anchor/shape checks read files
# only and cost ~0.3s, so they are mandatory rather than a local habit to remember.
#
# Baseline VALUE drift is measured here too, by EXECUTING the covered specs and reading
# the runner's events (scripts/spec-baseline-check.mjs) — never by scraping display text
# and never by rewriting a pin. A changed diff measures only the affected specs; a
# synchronised checkout measures all of them, which costs ~100s for the whole set (82
# specs when that was measured; 85 today). The check used to run only in the local gate,
# so a wrong pin reached CI green.
specs:
name: Mutation spec consistency
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v5
with:
# Change-scoped selection diffs against origin/master, so the history has to be
# here; a shallow clone would silently widen the selection to every spec.
fetch-depth: 0
- name: Setup Node 24
uses: actions/setup-node@v5
with:
node-version: '24'
- name: Anchors unique, specs well-formed
run: node scripts/spec-static-check.mjs
- name: Measured baselines match, from executed evidence
run: node scripts/spec-baseline-check.mjs
- name: Secret scan (includes test/ — half of a typical diff lives there)
run: node scripts/secret-scan.mjs
# The `specs` job scans the WORKING TREE. A credential that was committed and then
# deleted is invisible there and still present in every clone, so this job scans every
# blob reachable from any ref plus the unreachable objects an amend / rebase /
# force-push leaves behind (the objects a rewrite was meant to hide). fetch-depth: 0 is
# the whole point — a shallow clone would scan the tip and report it clean.
#
# A finding that is not in scripts/history-secret-scan.baseline fails this job. That
# file is a reviewed list, one line per accepted historical finding with its reason;
# it is not a mute button (the scan warns about stale entries, so a fixed leak leaves a
# line someone has to delete). The scan is ~15s on a 1.9k-commit history and needs no
# network, no binary download and no token — which is why it is the gate instead of
# gitleaks; see HistorySecretScan.md for the gitleaks cross-check.
history-secrets:
name: History secret scan
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Node 24
uses: actions/setup-node@v5
with:
node-version: '24'
- name: Scan every blob in every ref
run: node scripts/history-secret-scan.mjs
wire-identity:
name: Default-path request bytes
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
with:
# The comparison base is a tag, so the tags have to be here.
fetch-depth: 0
- name: Setup Node 24
uses: actions/setup-node@v5
with:
node-version: '24'
- name: Select the release baseline (strict ancestor, recorded)
# `git describe --tags --abbrev=0` answers "nearest reachable tag", and at a
# tagged HEAD that is HEAD's own tag: a rerun of a release commit compared
# the tree with itself. scripts/wire-base.mjs picks the newest ANNOTATED
# release tag that is a strict ancestor of the tested commit — no
# lightweight fallback — so a rerun at a tag repeats the same comparison
# against the PREVIOUS release, the recorded SHAs never change, and a
# repository without such a tag fails closed (exit 3).
run: |
node scripts/wire-base.mjs
BASE_TAG=$(node scripts/wire-base.mjs --field tag)
BASE_SHA=$(node scripts/wire-base.mjs --field sha)
HEAD_SHA=$(git rev-parse HEAD)
if [ "$BASE_SHA" = "$HEAD_SHA" ]; then
echo "::error::refusing to compare $HEAD_SHA against itself"
exit 1
fi
git worktree add --detach "$RUNNER_TEMP/wire-base" "$BASE_SHA"
echo "comparing $HEAD_SHA against $BASE_SHA ($BASE_TAG)"
echo "WIRE_BASE_TREE=$RUNNER_TEMP/wire-base" >> "$GITHUB_ENV"
echo "WIRE_BASE_SHA=$BASE_SHA" >> "$GITHUB_ENV"
echo "WIRE_BASE_TAG=$BASE_TAG" >> "$GITHUB_ENV"
echo "WIRE_BASE_REQUIRED=1" >> "$GITHUB_ENV"
- name: Same bytes as the last release, or this fails
run: node --import ./test/setup-env.mjs --test test/wire-byte-identity.test.js
test:
name: Unit tests shard ${{ matrix.shard }}
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3]
steps:
- uses: actions/checkout@v5
with:
# test/docs-consistency-guard.test.js resolves the handoff's tag and commit
# claims with git. The default depth-1 clone carries neither the history nor
# the tags, so shards 1 and 3 failed on "ambiguous argument '<sha>^{commit}'"
# and "no release tags reachable from HEAD" while the same suite passed
# locally. The guard fails closed by design; give it the objects, not an
# exemption. release.yml checks out full history for the same reason.
fetch-depth: 0
- name: Setup Node 24
uses: actions/setup-node@v5
with:
node-version: '24'
cache: 'npm'
# Zero npm dependencies: package.json declares none, and the JPEG/PNG decoders
# are vendored under src/vendor/. This step is a no-op today — kept so that a
# future dependency cannot silently break the shards that import it.
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:shard -- ${{ matrix.shard }} 4