The review of v6.2.0..f669b87 (BLOCK: 1 BLOCKER, 12 MAJOR, 1 MINOR), each fixed behind a test that failed first, or, for the two missing tests, a test shown to fail on a stub of the branch it covers. - Review steps appended by /trekexecute Phase 7.2 are held to the step rules on any plan version: plan-validator --steps-from <N> (on a plan below 1.8 a template review step validated and was executed). - The guard refuses where no one can be asked also when its rules will not load; the wave merge path names that refusal instead of MERGE_REFUSED. Tests for the classifier-error refusal and post-approval-log's own check. - release-check: the version files may change only in their version after the review (JSON compared without it), README diff lines are read inside hunks only, an unreadable diff is exit 2, and npm test runs without NODE_TEST_CONTEXT. - What counts as a step of only tests: a note in parentheses is not a path, and a data file under specs/ is not a test. End-to-end: rides on the last step that builds behaviour (validator hint, trekplan, template). - A removed line is cited as <rev>:<path>:<line> (findings-schema hint, trekreview re-ask). - skill-lint counts a trailing newline as no line; regression-check takes commandEnv from lib/util/command-env.mjs; CLAUDE.md names four mode-read exemptions. - /security-review, older than v6.2.0: the criteria runner refuses git --output, which writes a file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
108 lines
5 KiB
JavaScript
108 lines
5 KiB
JavaScript
#!/usr/bin/env node
|
|
// lib/verification/regression-check.mjs
|
|
// K10 at execution time: run ONE step's `Regression:` command through the
|
|
// criteria runner's own screens, before the step's checkpoint.
|
|
//
|
|
// node lib/verification/regression-check.mjs --plan <plan.md> --step <N> [--cwd <dir>] [--json]
|
|
// 0 = the step's `Regression:` passed, or the step has no `Regression:`
|
|
// field (`status: "absent"` — nothing new happens, K10)
|
|
// 1 = it failed, the executor denylist blocked it, or it is not runnable
|
|
// (no command, or a command outside the test-runner allowlist): the
|
|
// step is not done, and that is a finding for the fix rounds
|
|
// 2 = could not run: bad usage, a plan that cannot be read, no step N
|
|
// JSON on stdout: {step, status, command, exit_code, output}
|
|
// status: passed | failed | blocked | unrunnable | absent | error
|
|
//
|
|
// The command is the field's first backticked span — the same reading as the
|
|
// runner's `## Verification` bullets — and it goes through the same two
|
|
// screens: the allowlist of test runners, then the plugin's PreToolUse
|
|
// denylist. The plan validator rejects a `Regression:` that cannot run once K10
|
|
// is in it (wave 2, order 9); this is the run-time check, so a plan validated
|
|
// before that is never read as green.
|
|
//
|
|
// The runner reports `exitCode`; this CLI writes `exit_code`, the key
|
|
// progress.json uses (C15). Nothing else is translated.
|
|
//
|
|
// The command runs the way the runner runs it (`/bin/sh -c`), but without
|
|
// NODE_TEST_CONTEXT in its environment (lib/util/command-env.mjs): measured
|
|
// 2026-10-08, a red `node --test` regression read as passed when this CLI was
|
|
// itself started under `node --test`, because the nested run reported to the
|
|
// parent and exited 0.
|
|
|
|
import { readFileSync, realpathSync } from 'node:fs';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { sliceSteps, parseStepFields } from '../parsers/plan-schema.mjs';
|
|
import { firstCommand, runCriteria } from './criteria-runner.mjs';
|
|
import { commandEnv } from '../util/command-env.mjs';
|
|
|
|
function exec(command, { cwd, timeoutMs }) {
|
|
const r = spawnSync('/bin/sh', ['-c', command], { cwd, encoding: 'utf8', timeout: timeoutMs, env: commandEnv() });
|
|
return { status: r.status, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
|
|
}
|
|
|
|
export const REGRESSION_FIELD = 'Regression';
|
|
const EXIT = Object.freeze({ passed: 0, absent: 0, failed: 1, blocked: 1, unrunnable: 1, error: 2 });
|
|
|
|
/**
|
|
* @param {{ planText: string, step: number, cwd?: string, run?: Function }} o
|
|
* @returns {{ step: number, status: string, command: string|null, exit_code: number|null, output: string, exit: 0|1|2 }}
|
|
*/
|
|
export function checkRegression({ planText, step, cwd = process.cwd(), run = (c, o) => runCriteria(c, { ...o, exec }) }) {
|
|
const base = { step, command: null, exit_code: null, output: '' };
|
|
const found = sliceSteps(planText).find((s) => Number(s.n) === step);
|
|
if (!found) return { ...base, status: 'error', output: `no "### Step ${step}:" in the plan`, exit: EXIT.error };
|
|
const field = parseStepFields(found.body).get(REGRESSION_FIELD);
|
|
if (!field) return { ...base, status: 'absent', output: 'no Regression: field — nothing to run', exit: EXIT.absent };
|
|
const { command, reason } = firstCommand(field.text);
|
|
const [r] = run([{ label: `R${step}`, text: field.text, command, reason }], { cwd });
|
|
return {
|
|
step,
|
|
status: r.status,
|
|
command: r.command ?? null,
|
|
exit_code: r.exitCode ?? null,
|
|
output: r.output ?? '',
|
|
exit: EXIT[r.status] ?? EXIT.error,
|
|
};
|
|
}
|
|
|
|
export function main(argv) {
|
|
let plan; let step; let cwd = process.cwd(); let asJson = false;
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
if (a === '--plan' && argv[i + 1]) plan = argv[++i];
|
|
else if (a === '--step' && argv[i + 1]) step = Number(argv[++i]);
|
|
else if (a === '--cwd' && argv[i + 1]) cwd = resolve(argv[++i]);
|
|
else if (a === '--json') asJson = true;
|
|
else {
|
|
process.stderr.write(`regression-check: unknown argument ${a}\n`);
|
|
return 2;
|
|
}
|
|
}
|
|
const say = (r) => {
|
|
const { exit, ...rest } = r;
|
|
process.stdout.write(asJson ? `${JSON.stringify(rest)}\n`
|
|
: `regression step ${rest.step}: ${rest.status}${rest.command ? ` — \`${rest.command}\`` : ''}${rest.exit_code === null ? '' : ` (exit ${rest.exit_code})`}\n`);
|
|
return exit;
|
|
};
|
|
if (!plan || !Number.isInteger(step) || step < 1) {
|
|
process.stderr.write('Usage: regression-check.mjs --plan <plan.md> --step <N> [--cwd <dir>] [--json]\n');
|
|
return 2;
|
|
}
|
|
let planText;
|
|
try { planText = readFileSync(plan, 'utf8'); } catch (e) {
|
|
return say({ step, status: 'error', command: null, exit_code: null, output: `cannot read ${plan}: ${e.message}`, exit: 2 });
|
|
}
|
|
return say(checkRegression({ planText, step, cwd }));
|
|
}
|
|
|
|
function isMain() {
|
|
try {
|
|
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (isMain()) process.exitCode = main(process.argv.slice(2));
|