fix: round 8 of the release review of 6.3.0, for 6.3.1

Round 8 (0f400d3..29b4ac9, BLOCK: 1 BLOCKER, 12 MAJOR, 1 MINOR). Each fix
began with a test that failed on 29b4ac9 (13 tests, all red there); suite
2221 (2219/0/2).

- BLOCKER: a backticked check whose trailing comment holds a quote, $,
  backtick or backslash is refused, not dropped. The comment was never
  screened, while the executor hands the Verify span as written to red
  proof, the test lock and the done line inside --cmd "...", where a $(...)
  runs and a quote ends the argument. trekplan's text and the reviewer's
  NOT RUN table say so.
- A span that is only a comment is not-a-command, not a placeholder. The
  dropComment text says the allowlist still refuses a word with a # in it.
- The criteria runner, comparison-gate and end-state-gate use isMain: they
  did nothing and exited 0 under a symlinked path. The test now refuses any
  process.argv[1] comparison outside lib/util/is-main.mjs.
- Files: in a field with backticked paths, a plain word counts only before
  the entry's description (a dash, or a colon after the path); a name with
  no slash only first in its entry or after and/or/+/&. "mocks
  fs.readFileSync", "runs on Node.js 22.x" or "no change to package.json"
  no longer make a tests-only step look like it builds something.
  Lowercase makefile, rakefile and justfile are build files. Measured on the
  520 Files: fields in 48 plans of the marketplace and the test repo:
  3 fields read differently (two notes no longer read as files, one
  duplicate name dropped), tests-only changed in 0.
- The test lock compares --cmd without its trailing comment, and the test
  lock and the done line read a stop with stoppedBy, the one reading.
  red-proof drops its second reading, spawnError.
- release-check reads a relative --review in --repo or, when only that one
  exists, from the current directory as 6.3.0 did; two different files are
  exit 2, never guessed. The runbook says so.
- One trailing-json reader: a closing fence may be longer than three
  backticks, in the block reader and the verdict gate alike, and the review
  validator and the synthesis digest use it, so three backticks inside a
  JSON string no longer hide a BLOCKER from the remediation-step check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-11 10:43:56 +02:00
commit a10018f9d3
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
18 changed files with 310 additions and 56 deletions

View file

@ -50,6 +50,7 @@ import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { sliceSteps, parseStepFields } from '../parsers/plan-schema.mjs';
import { commandEnv } from '../util/command-env.mjs';
import { isMain } from '../util/is-main.mjs';
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
@ -149,16 +150,26 @@ export function looksLikeCommand(span) {
return true;
}
const COMMENT_NOT_PLAIN = 'a trailing comment with a quote, $, backtick or backslash (the span reaches a shell inside double quotes)';
// The first backtick-delimited span on the line is the command by convention —
// both templates put it first and a second span holds the expected output.
// A plan step's `Verify:` and `End-to-end:` fields follow the same convention.
export function firstCommand(text) {
const m = text.match(/`([^`]+)`/);
if (!m) return { command: null, reason: 'no-command' };
const raw = dropComment(m[1]);
const span = m[1].trim();
const raw = dropComment(span);
// Template placeholders (`{exact command}`) are not commands.
if (raw === '' || /^\{.*\}$/.test(raw)) return { command: null, reason: 'placeholder' };
if (!looksLikeCommand(raw)) return { command: null, reason: 'not-a-command' };
if (span === '' || /^\{.*\}$/.test(raw)) return { command: null, reason: 'placeholder' };
// A span that is only a comment names nothing to run (round 8).
if (raw === '' || !looksLikeCommand(raw)) return { command: null, reason: 'not-a-command' };
// The comment is never screened, but the executor hands the span as written
// to red proof, the test lock and the done line inside double quotes
// (`--cmd "{Verify command}"`): there a `"` ends the argument and a `$`, a
// backtick or a backslash still means something (release review of 6.3.0,
// round 8). A comment with one of them is refused, never dropped.
if (/["$`\\]/.test(span.slice(raw.length))) return { command: null, reason: COMMENT_NOT_PLAIN };
return { command: raw, reason: '' };
}
@ -170,7 +181,9 @@ export function firstCommand(text) {
* span (`Verify:`, `Regression:`, `End-to-end:`, a `## Verification` bullet)
* and a command passed as written (red proof, the done line, the test lock),
* so the words screened are the words run (round 7). Only a `#` that opens a
* word on the same line is a comment (`tests/a#b.test.mjs` is a path).
* word on the same line is a comment: `tests/a#b.test.mjs` is kept whole here,
* and the allowlist then refuses the word, since a `#` is not a plain word
* (round 8).
*/
export function dropComment(command) {
return String(command ?? '').replace(/[ \t]+#[^\n]*$/, '').trim();
@ -887,6 +900,6 @@ export function main(argv) {
return rep.summary.ok ? 0 : 1;
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
if (isMain(import.meta.url)) {
process.exitCode = main(process.argv.slice(2));
}

View file

@ -23,7 +23,7 @@
import { appendFileSync, existsSync, readFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { DEFAULT_TIMEOUT_MS, allowedCommand, runCommand, screenCommand } from './criteria-runner.mjs';
import { DEFAULT_TIMEOUT_MS, allowedCommand, runCommand, screenCommand, stoppedBy } from './criteria-runner.mjs';
import { repoRoot } from './red-proof.mjs';
import { isMain } from '../util/is-main.mjs';
@ -58,9 +58,9 @@ export function writeDoneLine({ cwd, step, cmd, report, timeoutMs = DEFAULT_TIME
if (head.status !== 0) return { status: 'not-run', reason: `no git HEAD in ${dir}` };
// No shell: the words the screen checked are the words that run.
const r = runCommand(cmd, { cwd: dir, timeoutMs });
if (r.error || r.status === null) {
return { status: 'not-run', reason: `the command did not run to an exit code (${r.error?.message ?? r.signal ?? `exit ${r.status}`})`, exit: r.status };
}
// A stop is read once, by stoppedBy (round 8).
const stopped = stoppedBy(r, { timeoutMs });
if (stopped) return { status: 'not-run', reason: `the command ${stopped.why}`, exit: r.status };
if (r.status !== 0) return { status: 'failed', reason: `the test command exited ${r.status}`, exit: r.status };
const line = `DONE step ${step}: \`${cmd}\` exit 0 at ${head.stdout.trim()} ${new Date().toISOString()}`;
appendFileSync(report, `${line}\n`);

View file

@ -98,7 +98,7 @@ function run(cmd, cwd, timeoutMs) {
const r = runCommand(cmd, { cwd, timeoutMs });
const output = `${r.stdout ?? ''}${r.stderr ?? ''}`;
const stopped = stoppedBy(r, { timeoutMs });
return { exit: r.status, signal: r.signal ?? null, tap: tap(output), tail: output.slice(-TAIL), spawnError: r.error ? r.error.message : null, stopped: stopped?.why ?? null, loadError: LOAD_ERROR.test(output) };
return { exit: r.status, signal: r.signal ?? null, tap: tap(output), tail: output.slice(-TAIL), stopped: stopped?.why ?? null, loadError: LOAD_ERROR.test(output) };
}
/** What the red at the start point was: the test file failing to load, or a test failing. */