refactor(simplify): one source per rule, shared test helpers, a faster isolation test [skip-docs]

/simplify over a97efb6..HEAD (the plan_version 1.9 / trial-4 delivery).

- coordinator-contract: engine names come from engine-findings ENGINES;
  one markedMajorCodeReview predicate under both lowering checks;
  isEngineFinding everywhere; the unreachable branch (c) of
  illegalBriefLowering folded into (e).
- armB workflow: RULE_KEYS = Object.keys(RULE_TIERS); the engine map is a
  literal pinned equal to ENGINES by the workflow test.
- plan-schema: usesFilesGrammar() is the one 1.9 check (validator, sweep,
  testFirstFiles, stepTestsOnlyFromFields); opensEntry built on opensChunk.
- plan-validator: one issue builder for the Files and Test first grammar.
- plan-scope-sweep: the 1.9 and the 6.2.0 reader are one if/else.
- artifact-commit: TRAILER_RE/trailerProblem no longer exported (no
  importer); the order comment says the CLI finds the repo root first.
- tests: the lock drivers in session-id-isolation (d) run side by side per
  mode (~100 s -> ~30 s); a shared artifact-repo helper; severityRank and
  extractFindingsBlock reused; childEnv(env) without a spread; one copy of
  the PLAN_VERSION_CURRENT check.

Plan readers: validator errors/warnings, plannedPaths, fieldPaths,
testFirstFiles and stepTestsOnlyFromFields compared byte for byte against
HEAD over 510 plans / 1692 steps plus 210 synthetic 1.7-1.9 cases: same.
Suite 2384 (2382/0/2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-12 21:06:47 +02:00
commit ed6699da58
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
23 changed files with 201 additions and 204 deletions

View file

@ -53,7 +53,7 @@ import { readFileSync, existsSync } from 'node:fs';
import {
sliceSteps, validatePlanHeadings, extractPlanVersion, extractPlanCeremony,
parseStepFields, findSection, splitSections, findModuleCycle, parseMermaid, fencedBlocks,
versionAtLeast, PLAN_VERSION_CURRENT, PLAN_VERSIONS_SUPPORTED, CEREMONY_VALUES,
versionAtLeast, usesFilesGrammar, PLAN_VERSION_CURRENT, PLAN_VERSIONS_SUPPORTED, CEREMONY_VALUES,
ON_FAILURE_ACTIONS, onFailureAction, stepTestsOnlyFromFields, entryPaths, untickedPaths,
testFirstFileEntries, testFirstFiles,
} from '../parsers/plan-schema.mjs';
@ -77,7 +77,7 @@ const BACKTICKED = /`[^`\n]+`/;
function checkSteps(sections, planVersion) {
const out = [];
let endToEnd = 0;
const grammar = versionAtLeast(planVersion, '1.9');
const grammar = usesFilesGrammar(planVersion);
for (const s of sections) {
const f = parseStepFields(s.body);
const tf = f.get('Test first');
@ -217,42 +217,38 @@ function checkRecovery(sections, opts, planVersion) {
* names no file. Each `Test first:` `File:` line (plan-schema
* `testFirstFileEntries`) is one entry under the same grammar.
*/
// How checkPathGrammar words an issue in each of the two places the grammar applies.
const FILES_WORDING = {
subject: 'a `Files:` entry', none: '`Files:` names no file', spans: 'an entry are its files',
opens: 'each `Files:` entry opens with its backticked path(s)',
example: '`` `src/a.mjs` - the parser ``', tail: ' A step that changes no file names none.',
};
const TEST_FIRST_WORDING = {
subject: 'a `Test first:` `File:` line', none: 'a `Test first:` `File:` line names no file',
spans: 'the `File:` line are the test files', opens: 'the `File:` line opens with its backticked test file(s)',
example: '`` File: `tests/a.test.mjs` (new) ``', tail: '',
};
function pathGrammarIssue(n, code, shown, w) {
if (code === 'PLAN_STEP_PATH_NOT_A_PATH') {
return issue(code, `Step ${n}: ${w.subject} opens with a backticked span that is no path: ${shown}`,
`plan_version 1.9: the leading backticked spans of ${w.spans}; a call (\`parse()\`), an option ` +
'or a URL is description and goes after the path.');
}
return issue('PLAN_STEP_PATH_NOT_FIRST',
shown === '' ? `Step ${n}: ${w.none}` : `Step ${n}: ${w.subject} does not open with a backticked path: ${shown}`,
`plan_version 1.9: ${w.opens}, separated by commas or spaces; the description follows (${w.example}).${w.tail}`);
}
function checkPathGrammar(sections) {
const out = [];
for (const s of sections) {
const f = parseStepFields(s.body);
const { issues } = entryPaths(f.has('Files') ? f.get('Files').text : '');
for (const i of issues) {
if (i.code === 'PLAN_STEP_PATH_NOT_A_PATH') {
out.push(issue('PLAN_STEP_PATH_NOT_A_PATH',
`Step ${s.n}: a \`Files:\` entry opens with a backticked span that is no path: ${i.entry}`,
'plan_version 1.9: the leading backticked spans of an entry are its files; a call (`parse()`), an option ' +
'or a URL is description and goes after the path.'));
} else {
out.push(issue('PLAN_STEP_PATH_NOT_FIRST',
i.entry === ''
? `Step ${s.n}: \`Files:\` names no file`
: `Step ${s.n}: a \`Files:\` entry does not open with a backticked path: ${i.entry}`,
'plan_version 1.9: each `Files:` entry opens with its backticked path(s), separated by commas or spaces; ' +
'the description follows (`` `src/a.mjs` - the parser ``). A step that changes no file names none.'));
}
for (const i of entryPaths(f.has('Files') ? f.get('Files').text : '').issues) {
out.push(pathGrammarIssue(s.n, i.code, i.entry, FILES_WORDING));
}
for (const entry of testFirstFileEntries(f)) {
for (const i of entryPaths(entry).issues) {
if (i.code === 'PLAN_STEP_PATH_NOT_A_PATH') {
out.push(issue('PLAN_STEP_PATH_NOT_A_PATH',
`Step ${s.n}: a \`Test first:\` \`File:\` line opens with a backticked span that is no path: ${entry}`,
'plan_version 1.9: the leading backticked spans of the `File:` line are the test files; a call ' +
'(`parse()`), an option or a URL is description and goes after the path.'));
} else {
out.push(issue('PLAN_STEP_PATH_NOT_FIRST',
entry === ''
? `Step ${s.n}: a \`Test first:\` \`File:\` line names no file`
: `Step ${s.n}: a \`Test first:\` \`File:\` line does not open with a backticked path: ${entry}`,
'plan_version 1.9: the `File:` line opens with its backticked test file(s), separated by commas or ' +
'spaces; the description follows (`` File: `tests/a.test.mjs` (new) ``).'));
}
}
for (const i of entryPaths(entry).issues) out.push(pathGrammarIssue(s.n, i.code, entry, TEST_FIRST_WORDING));
}
}
return out;
@ -369,7 +365,7 @@ export function validatePlanContent(rawText, opts = {}) {
warnings.push(...recovery.warnings);
// The `Files:` grammar (decision 16): refused from 1.9, a warning below it.
if (versionAtLeast(planVersion, '1.9')) {
if (usesFilesGrammar(planVersion)) {
const grammar = checkPathGrammar(sections);
if (strict) errors.push(...grammar);
else warnings.push(...grammar);
@ -397,7 +393,7 @@ export function validatePlanContent(rawText, opts = {}) {
if (stepsFrom !== null) {
const appended = sections.filter((s) => Number(s.n) >= stepsFrom);
const stepIssues = versionAtLeast(planVersion, '1.8') ? [] : [
...checkSteps(appended, planVersion).filter((i) => i.code !== 'PLAN_NO_END_TO_END'),
...checkSteps(appended).filter((i) => i.code !== 'PLAN_NO_END_TO_END'),
...checkRunnable(text, appended, { cwd: opts.cwd, stepsOnly: true }),
];
for (const s of appended) {