From 0db351aaa788841de2897cd65fff241cc949291d Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 15 Sep 2026 18:24:24 -0700 Subject: [PATCH] Add canonical verify runner and verification record Verification commands were transcribed in several places and the same full suite was re-run by every role. Add verify.sh/verify.mjs as the single per-component verification sequence emitting a machine-readable record (git_sha, per-command exit/duration/summary). The architect records it for each touched component; the specifier trusts a matching-SHA pass and only re-runs the merged feature's acceptance test. By specifier. --- swarmforge/roles/architect.prompt | 3 + swarmforge/roles/specifier.prompt | 1 + swarmforge/scripts/verify.mjs | 164 ++++++++++++++++++++++++++++++ swarmforge/scripts/verify.sh | 6 ++ 4 files changed, 174 insertions(+) create mode 100755 swarmforge/scripts/verify.mjs create mode 100755 swarmforge/scripts/verify.sh diff --git a/swarmforge/roles/architect.prompt b/swarmforge/roles/architect.prompt index d5716f4..e4859a3 100644 --- a/swarmforge/roles/architect.prompt +++ b/swarmforge/roles/architect.prompt @@ -51,6 +51,9 @@ You are the architect. ## Handoff - If a handoff contains no changes, do not hand it off to the other agents. - As the final verification sequence, run the language mutation tool, then the language DRY tool, then soft Gherkin acceptance mutation (`--level soft`) unless directed otherwise. Fix any issues each tool finds before running the next one. +- **Verification record**: before writing the review report, run the canonical verification runner for every component the task touched: + `swarmforge/scripts/verify.sh --record docs/reviews/-verification.json --task ` + This emits a machine-readable record (per-command exit, duration, summary) with the component `git_sha`. Commit it (force-add under the gitignored `docs/`) in the same commit as the review changes so the specifier can trust it without re-running the full suite. - **Written review report**: at the end of each task or batch, write a concise summary to `docs/reviews/-summary.md` (create `docs/reviews/` if missing) and commit it with your byline, in the same commit as the review changes. The summary is the durable written record diff --git a/swarmforge/roles/specifier.prompt b/swarmforge/roles/specifier.prompt index 287fdba..0174242 100644 --- a/swarmforge/roles/specifier.prompt +++ b/swarmforge/roles/specifier.prompt @@ -24,6 +24,7 @@ You are the specifier. ## Verification - Do not run Gherkin acceptance mutation. - Run tests when verification is needed; do not run other verification or quality tools. +- After merging the architect branch, check `docs/reviews/-verification.json`. It must exist and its `git_sha` must match the merged commit. When it matches and reports `pass`, do not re-run the full suite; run only the acceptance test for the merged feature as an independent check. If the record is missing, stale, or reports a failure, run `swarmforge/scripts/verify.sh` for the affected component(s). ## Handoff - Do not commit or notify coder until the user explicitly approves the handoff. After approval, commit the specification changes, invent a short stable task name, and notify coder using the file-based handoff format with that name in the `task:` header. diff --git a/swarmforge/scripts/verify.mjs b/swarmforge/scripts/verify.mjs new file mode 100755 index 0000000..10bad29 --- /dev/null +++ b/swarmforge/scripts/verify.mjs @@ -0,0 +1,164 @@ +#!/usr/bin/env node +/* + Canonical per-component verification runner for psf-memo. + + Runs the standard verification sequence for one component (unit, property, + acceptance, lint, and build for the client) and emits a machine-readable + JSON record. This is the single source of truth for "what does verification + mean for this component", so roles do not have to transcribe commands. + + Usage: + node swarmforge/scripts/verify.mjs + node swarmforge/scripts/verify.mjs --record --task + + Exit codes: + 0 every command passed + 1 at least one command failed + 2 usage error +*/ + +import { execFileSync } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const repoRoot = path.resolve(__dirname, '..', '..') + +const COMPONENTS = { + client: { + dir: 'psf-memo-client', + commands: [ + ['unit', ['test']], + ['property', ['run', 'test:property']], + ['acceptance', ['run', 'test:acceptance']], + ['lint', ['run', 'lint']], + ['build', ['run', 'build']] + ] + }, + db: { + dir: 'psf-memo-db', + commands: [ + ['unit', ['test']], + ['property', ['run', 'property']], + ['acceptance', ['run', 'acceptance']], + ['lint', ['run', 'lint']] + ] + }, + indexer: { + dir: 'psf-memo-indexer', + commands: [ + ['unit', ['test']], + ['property', ['run', 'property']], + ['acceptance', ['run', 'acceptance']], + ['lint', ['run', 'lint']] + ] + } +} + +function usage () { + console.error('usage: verify.mjs [--record ] [--task ]') +} + +function git (args) { + try { + return execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8' }).trim() + } catch (err) { + return '' + } +} + +function summarize (name, out, ok) { + const clean = out.replace(/\x1b\[[0-9;]*m/g, '') + if (!ok) { + const lines = clean.trim().split('\n') + const tail = lines.slice(-8).join(' | ').replace(/\s+/g, ' ').slice(0, 400) + return `FAILED: ${tail}` + } + let m = clean.match(/(\d+) passing/) + if (m) return `${m[1]} passing` + m = clean.match(/# pass (\d+)/) + if (m) { + const fail = clean.match(/# fail (\d+)/) + return `${m[1]} pass / ${fail ? fail[1] : 0} fail` + } + m = clean.match(/ACCEPTANCE: all (\d+) generated test file\(s\) passed/) + if (m) return `all ${m[1]} acceptance suites passed` + return 'ok' +} + +function runCommand (dir, name, args) { + const started = Date.now() + let out = '' + let exit = 0 + try { + out = execFileSync('npm', args, { + cwd: dir, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + env: process.env + }) + } catch (err) { + exit = typeof err.status === 'number' ? err.status : 1 + out = `${err.stdout || ''}\n${err.stderr || ''}` + } + const ok = exit === 0 + process.stderr.write(` [${ok ? 'ok' : 'FAIL'}] ${name} (${Date.now() - started}ms)\n`) + return { + name, + command: `npm ${args.join(' ')}`, + exit, + duration_ms: Date.now() - started, + summary: summarize(name, out, ok) + } +} + +function main () { + const argv = process.argv.slice(2) + const name = argv[0] + if (!name || !COMPONENTS[name]) { + usage() + process.exit(2) + } + let recordPath = '' + let task = '' + for (let i = 1; i < argv.length; i++) { + if (argv[i] === '--record') recordPath = argv[++i] || '' + else if (argv[i] === '--task') task = argv[++i] || '' + else { + usage() + process.exit(2) + } + } + + const spec = COMPONENTS[name] + const componentDir = path.join(repoRoot, spec.dir) + process.stderr.write(`== verify ${spec.dir} ==\n`) + + const commands = spec.commands.map(([cmdName, args]) => runCommand(componentDir, cmdName, args)) + const failed = commands.filter((c) => c.exit !== 0) + const record = { + schema_version: 1, + task: task || null, + component: spec.dir, + git_sha: git(['rev-parse', 'HEAD']), + branch: git(['rev-parse', '--abbrev-ref', 'HEAD']), + timestamp: new Date().toISOString(), + commands, + result: failed.length === 0 ? 'pass' : 'fail' + } + + const json = `${JSON.stringify(record, null, 2)}\n` + if (recordPath) { + fs.mkdirSync(path.dirname(recordPath), { recursive: true }) + fs.writeFileSync(recordPath, json) + process.stderr.write(`verification record: ${recordPath}\n`) + } else { + process.stdout.write(json) + } + + process.stderr.write(`result: ${record.result} (${commands.length - failed.length}/${commands.length} commands passed)\n`) + process.exit(failed.length === 0 ? 0 : 1) +} + +main() diff --git a/swarmforge/scripts/verify.sh b/swarmforge/scripts/verify.sh new file mode 100755 index 0000000..7e7059c --- /dev/null +++ b/swarmforge/scripts/verify.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Thin wrapper for the canonical per-component verification runner. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +exec node "$SCRIPT_DIR/verify.mjs" "$@"