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.
This commit is contained in:
Chris Troutner
2026-09-15 18:24:24 -07:00
parent 5bbe276c58
commit 0db351aaa7
4 changed files with 174 additions and 0 deletions
+3
View File
@@ -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 <client|db|indexer> --record docs/reviews/<task>-verification.json --task <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/<task>-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
+1
View File
@@ -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/<task>-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.
+164
View File
@@ -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 <client|db|indexer>
node swarmforge/scripts/verify.mjs <component> --record <file> --task <name>
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 <client|db|indexer> [--record <file>] [--task <name>]')
}
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()
+6
View File
@@ -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" "$@"