refactor(simplify): one plan read in red proof, one list marker, one put-back rule [skip-docs]

/simplify over ed6699d..HEAD (closing review of the remediation):
- red-proof reads and parses the step once (planStep) for the test list and
  tests_only; normTest goes, plan-schema exports normPlanPath, also used by
  plan-scope-sweep.
- plan-schema builds LIST_ITEM and the 1.9 File: line from one LIST_MARKER;
  the below-1.9 File: regex ends in a lookahead instead of slicing back one.
- the workflow script hoists tripletOf and puts back through dedupByTriplet.
- coordinator-contract keeps the rank in its JSDoc only; engine-findings
  drops a redundant String().
- tests: reuse knownIssues, trekplan19Block, step1Fields, one red-proof cli
  helper with childEnv, one payload-pair builder; drop a test the decision-20
  definition test covers.

No behaviour change. Suite 2462 (2460/0/2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-13 19:47:08 +02:00
commit bcf3adbba7
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
11 changed files with 90 additions and 127 deletions

View file

@ -38,7 +38,7 @@
import { readFileSync, existsSync, realpathSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { resolve, relative, dirname, isAbsolute, sep } from 'node:path';
import { sliceSteps, parseStepFields, entryPaths, testFirstFiles, extractPlanVersion, usesFilesGrammar } from '../parsers/plan-schema.mjs';
import { sliceSteps, parseStepFields, entryPaths, testFirstFiles, extractPlanVersion, usesFilesGrammar, normPlanPath as norm } from '../parsers/plan-schema.mjs';
import { parseManifest } from '../parsers/manifest-yaml.mjs';
import { isMain } from '../util/is-main.mjs';
@ -60,8 +60,6 @@ function pathsIn(text) {
return out;
}
const norm = p => p.replace(/\\/g, '/').replace(/^\.\//, '');
/** Every path the plan names: Files, Test first File, manifest expected_paths. */
export function plannedPaths(planText) {
const out = new Set();

View file

@ -69,7 +69,7 @@ import { tmpdir } from 'node:os';
import { dirname, isAbsolute, join, resolve } from 'node:path';
import { DEFAULT_TIMEOUT_MS, allowedCommand, runCommand, screenCommand, stoppedBy } from './criteria-runner.mjs';
import { commandEnv } from '../util/command-env.mjs';
import { extractPlanVersion, parseStepFields, sliceSteps, stepTestsOnlyFromFields, testFirstFiles } from '../parsers/plan-schema.mjs';
import { extractPlanVersion, normPlanPath, parseStepFields, sliceSteps, stepTestsOnlyFromFields, testFirstFiles } from '../parsers/plan-schema.mjs';
import { isMain } from '../util/is-main.mjs';
// The one timeout, defined in criteria-runner.mjs (release review of 6.3.0, round 7).
@ -129,32 +129,25 @@ const notRun = (reason, extra = {}) => ({ status: 'not-run', reason, ...extra })
// made a true tests-only step look like it built something (release review of
// 6.3.0, round 7). A path the step changed outside `Files:` is the
// controller's scope finding (trekexecute Sub-step C, Hard rule 2), not this check's.
function testsOnly(o) {
if (!o.plan) return {};
let text;
try { text = readFileSync(o.plan, 'utf8'); }
catch (e) { return { tests_only: null, tests_only_note: `cannot read the plan: ${e.message}` }; }
const step = sliceSteps(text).find((s) => Number(s.n) === Number(o.step));
if (!step) return { tests_only: null, tests_only_note: `no "### Step ${o.step}:" in the plan` };
return { tests_only: stepTestsOnlyFromFields(parseStepFields(step.body), extractPlanVersion(text)).testsOnly };
function testsOnly(step) {
if (!step) return {};
if (step.error) return { tests_only: null, tests_only_note: step.error };
return { tests_only: stepTestsOnlyFromFields(step.fields, step.version).testsOnly };
}
// With `--plan` and `--step`: the step's test files as its `Test first:` File
// line names them (testFirstFiles, operator decision 20), read as `testsOnly`
// reads the plan. null = the plan or the step could not be read.
function planTests(o) {
// With `--plan` and `--step`: the step's parsed fields and the plan's version,
// read once for both the test list and `testsOnly`. null without `--plan`;
// `{error}` when the plan or the step could not be read.
function planStep(o) {
if (!o.plan) return null;
let text;
try { text = readFileSync(o.plan, 'utf8'); }
catch { return null; }
catch (e) { return { error: `cannot read the plan: ${e.message}` }; }
const step = sliceSteps(text).find((s) => Number(s.n) === Number(o.step));
if (!step) return null;
return testFirstFiles(parseStepFields(step.body), extractPlanVersion(text));
if (!step) return { error: `no "### Step ${o.step}:" in the plan` };
return { fields: parseStepFields(step.body), version: extractPlanVersion(text) };
}
/** A test path as the plan's list holds it: `/` separators, no leading `./`. */
const normTest = (p) => String(p).replace(/\\/g, '/').replace(/^\.\//, '');
// A program that could not start (a spawn error: not installed), or one this
// run stopped (the timeout, output past the buffer, a signal), is not a test
// verdict at all (criteria-runner `stoppedBy`). With no shell, an exit 126/127
@ -219,15 +212,17 @@ export function proveRed(o) {
if (!existsSync(given)) return notRun(`--cwd ${given} does not exist`);
const cwd = repoRoot(given);
if (!cwd) return notRun(`--cwd ${given} is not inside a git repository`);
// The step's own test list (review cf241526): read from the plan when none is
// given, and a given list that is another set is refused. An empty or
// unreadable plan list changes nothing.
const fromPlan = planTests(o);
if (fromPlan && fromPlan.length > 0) {
if (tests.length === 0) tests = [...fromPlan];
// The step's own test list (review cf241526): the step's `Test first:` File
// line as testFirstFiles reads it (operator decision 20), taken when none is
// given; a given list that is another set is refused. An empty or unreadable
// plan list changes nothing.
const step = planStep(o);
const fromPlan = step?.fields ? testFirstFiles(step.fields, step.version) : [];
if (fromPlan.length > 0) {
if (tests.length === 0) tests = fromPlan;
else {
const a = new Set(tests.map(normTest));
const b = new Set(fromPlan.map(normTest));
const a = new Set(tests.map((t) => normPlanPath(String(t))));
const b = new Set(fromPlan);
if (a.size !== b.size || [...a].some((t) => !b.has(t))) {
return notRun(`tests-mismatch: --tests names ${tests.join(',')}; the plan's Test first File line names ${fromPlan.join(',')}`, { plan_tests: fromPlan });
}
@ -270,7 +265,7 @@ export function proveRed(o) {
const baseBroken = unrunnable(base);
if (baseBroken) return notRun(`at the start point: ${baseBroken}`, { base, result, linked });
const out = { base_sha: baseSha, linked, base, result };
if (base.exit === 0) return { status: 'rejected', reason: 'not-red-at-base', ...testsOnly(o), ...out };
if (base.exit === 0) return { status: 'rejected', reason: 'not-red-at-base', ...testsOnly(step), ...out };
if (o.redOnly) return { status: 'proven', reason: 'red at the start point (--red-only: the result was not run)', red_kind: redKind(base), base_sha: baseSha, linked, base };
if (result.exit !== 0) return { status: 'rejected', reason: 'not-green-at-result', ...out };