fix: close the gaps the review of the trial-5 fixes found

- park-attempt-files: parks in the git dir (outside the work tree, so Sub-step C.2 never registers a parked file again), reads a path with a space, names a new file under a header it cannot read, parks a symlink, never overwrites a parked file, and exits 1 while a file the attempt made is still in the tree; the upward diff is saved in one form whatever the git config.

- brief-validator: BRIEF_SC_NOT_RUNNABLE only when a later backticked span is a command; a criterion that only names a flag or a path stays prose (Handover 1 says so now).

- review ids: counted over the final list (runContract), as fill-finding-ids counts review.md; trimmed_count matches rows by id; engine-findings and fidelity-diff use computeFindingIds; the --workflow dedup keeps one source's different findings apart, the decision-23 put-back stays one copy per triplet.

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

View file

@ -9,81 +9,101 @@
//
// Before the fresh build the executor parks them: every file the attempt
// created (a `new file mode` entry in step-{N}-upward.diff) that is still
// untracked on disk moves into the report dir under its own path. Moved,
// never deleted. A file tracked since then, already gone, or outside the
// repository is kept where it is and named.
// untracked on disk moves to <git dir>/voyage/parked/step-<N>-attempt-<k>/
// under its own path. The git dir, like the test lock's, is outside the work
// tree, so a parked file never comes back as a new file of the next build.
// Moved, never deleted, and never over a file already parked there.
//
// Usage:
// node lib/util/park-attempt-files.mjs --diff <step-N-upward.diff> --to <dir> [--cwd <repo>]
// Prints {"parked": [paths], "kept": [{"path", "reason"}]} on stdout.
// Exit: 0 = done (also when nothing was parked), 2 = could not run (bad usage,
// unreadable diff, not a repository). 2 never means the tree is clean.
// node lib/util/park-attempt-files.mjs --diff <step-N-upward.diff> --step <N> --attempt <k> [--cwd <repo>]
// Prints {"parked": [paths], "kept": [{"path", "reason", "blocking"}], "to": dir}.
// Exit: 0 = no file the attempt made is left in the tree; 1 = one still is
// (a header it could not read, a move that failed, a target already taken):
// the run stops; 2 = could not run (bad usage, unreadable diff, not a
// repository). Neither 1 nor 2 is ever "the tree is clean".
import { readFileSync, existsSync, mkdirSync, renameSync, lstatSync } from 'node:fs';
import { readFileSync, mkdirSync, renameSync, lstatSync } from 'node:fs';
import { join, dirname, resolve, relative, isAbsolute } from 'node:path';
import { spawnSync } from 'node:child_process';
import { isMain } from './is-main.mjs';
/** The paths a unified git diff creates (`new file mode` after its `diff --git` line). */
/**
* What a unified git diff creates: `paths` read from a `diff --git a/X b/X`
* header followed by `new file mode`, and `unread`, the raw headers of new
* files whose header is not that form (another prefix, a quoted path).
*/
export function newFilesInDiff(diffText) {
const paths = [];
let current = null;
const unread = [];
let header = null;
for (const line of String(diffText).split('\n')) {
const head = line.match(/^diff --git a\/(\S+) b\/(\S+)$/);
if (head) {
current = head[2];
continue;
}
if (line.startsWith('diff --git ')) {
current = null; // a quoted or spaced path: not read, so never moved
header = line;
continue;
}
if (current !== null && /^new file mode \d+$/.test(line)) {
paths.push(current);
current = null;
if (header !== null && /^new file mode \d+$/.test(line)) {
// Both names are the same for a new file, so a space in it is read too.
const m = header.match(/^diff --git a\/(.+) b\/\1$/);
if (m) paths.push(m[1]);
else unread.push(header);
header = null;
}
}
return paths;
return { paths, unread };
}
function tracked(cwd, path) {
return spawnSync('git', ['ls-files', '--error-unmatch', '--', path], { cwd, encoding: 'utf8' }).status === 0;
function lexists(abs) {
try {
lstatSync(abs);
return true;
} catch {
return false;
}
}
/**
* @param {{ diffText: string, to: string, cwd?: string }} args
* @returns {{ parked: string[], kept: Array<{path: string, reason: string}> }}
* @param {{ diffText: string, step: string|number, attempt: string|number, cwd?: string }} args
* @returns {{ parked: string[], kept: Array<{path: string, reason: string, blocking: boolean}>, to: string }}
*/
export function parkAttemptFiles({ diffText, to, cwd = process.cwd() }) {
export function parkAttemptFiles({ diffText, step, attempt, cwd = process.cwd() }) {
const root = resolve(cwd);
const dest = resolve(root, to);
const gitDir = spawnSync('git', ['rev-parse', '--absolute-git-dir'], { cwd: root, encoding: 'utf8' }).stdout.trim();
const to = join(gitDir, 'voyage', 'parked', `step-${step}-attempt-${attempt}`);
const trackedPaths = new Set(
spawnSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf8' }).stdout.split('\0').filter(Boolean),
);
const { paths, unread } = newFilesInDiff(diffText);
const parked = [];
const kept = [];
for (const path of newFilesInDiff(diffText)) {
const kept = unread.map((h) => ({ path: h, reason: 'diff header not read: move this file out by hand', blocking: true }));
for (const path of paths) {
const abs = resolve(root, path);
const rel = relative(root, abs);
if (isAbsolute(path) || rel === '' || rel.startsWith('..') || isAbsolute(rel) || rel.split(/[\\/]/)[0] === '.git') {
kept.push({ path, reason: 'outside the repository: refused' });
kept.push({ path, reason: 'outside the repository: refused', blocking: false });
continue;
}
if (!existsSync(abs) || !lstatSync(abs).isFile()) {
kept.push({ path, reason: 'not on disk as a file: nothing to park' });
if (!lexists(abs)) {
kept.push({ path, reason: 'not on disk: nothing to park', blocking: false });
continue;
}
if (tracked(root, rel)) {
kept.push({ path, reason: 'tracked since the attempt: not the executor\'s to move' });
if (trackedPaths.has(rel.split('\\').join('/'))) {
kept.push({ path, reason: 'tracked since the attempt: not the executor\'s to move', blocking: false });
continue;
}
const target = join(to, rel);
if (lexists(target)) {
kept.push({ path, reason: `already parked at ${target}: never overwritten, move it by hand`, blocking: true });
continue;
}
const target = join(dest, rel);
try {
mkdirSync(dirname(target), { recursive: true });
renameSync(abs, target);
parked.push(rel);
} catch (e) {
kept.push({ path, reason: `could not move: ${e.message}` });
kept.push({ path, reason: `could not move: ${e.message}`, blocking: true });
}
}
return { parked, kept };
return { parked, kept, to };
}
if (isMain(import.meta.url)) {
@ -93,10 +113,11 @@ if (isMain(import.meta.url)) {
return i === -1 ? undefined : argv[i + 1];
};
const diff = opt('--diff');
const to = opt('--to');
const step = opt('--step');
const attempt = opt('--attempt');
const cwd = opt('--cwd') ?? process.cwd();
if (!diff || !to) {
process.stderr.write('Usage: park-attempt-files.mjs --diff <step-N-upward.diff> --to <dir> [--cwd <repo>]\n');
if (!diff || !/^[0-9]+$/.test(step ?? '') || !/^[0-9]+$/.test(attempt ?? '')) {
process.stderr.write('Usage: park-attempt-files.mjs --diff <step-N-upward.diff> --step <N> --attempt <k> [--cwd <repo>]\n');
process.exit(2);
}
let diffText;
@ -110,5 +131,7 @@ if (isMain(import.meta.url)) {
process.stderr.write(`park-attempt-files: ${cwd} is not a git repository\n`);
process.exit(2);
}
process.stdout.write(JSON.stringify(parkAttemptFiles({ diffText, to, cwd }), null, 2) + '\n');
const result = parkAttemptFiles({ diffText, step, attempt, cwd });
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
process.exit(result.kept.some((k) => k.blocking) ? 1 : 0);
}