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:
parent
d7db590c26
commit
bcf3adbba7
11 changed files with 90 additions and 127 deletions
|
|
@ -373,9 +373,14 @@ const BUILD_FILE = /^(?!Profile$)(?:[A-Z][a-z]+|GNUmake)file$|^(?:make|rake|just
|
|||
// Where an entry's description starts, and the words that start a list item.
|
||||
const DESCRIPTION_DASH = /^(?:\u2014|\u2013|-|--)$/;
|
||||
const LIST_JOINER = /^(?:and|or|\+|&)$/i;
|
||||
const LIST_ITEM = /^\s*(?:[-*+]|\d+[.)])\s+/;
|
||||
// The list markers a plan line may open with; LIST_ITEM and the 1.9 `File:`
|
||||
// line are both built from it.
|
||||
const LIST_MARKER = String.raw`(?:[-*+]|\d+[.)])`;
|
||||
const LIST_ITEM = new RegExp(String.raw`^\s*${LIST_MARKER}\s+`);
|
||||
const lastSegment = (p) => p.replace(/\/$/, '').split('/').pop();
|
||||
const norm = (p) => p.replace(/\\/g, '/').replace(/^\.\//, '');
|
||||
/** A plan path as the readers hold it: `/` separators, no leading `./`. */
|
||||
export const normPlanPath = (p) => p.replace(/\\/g, '/').replace(/^\.\//, '');
|
||||
const norm = normPlanPath;
|
||||
// A backticked span that is a path, not a URL, an option or a call (`parse()`, `p.parse()`).
|
||||
const tickedPath = (s) => /[./\\]/.test(s) && !/^https?:/.test(s) && !s.startsWith('-') && !/^[\w$.]+\(.*\)$/.test(s);
|
||||
// A plain word without the brackets and punctuation around it, `\` read as `/`.
|
||||
|
|
@ -622,10 +627,10 @@ export function stepTestsOnly(body, version) {
|
|||
|
||||
// A line of the `Test first:` field that names its test files (6.3.1's
|
||||
// selector); global, so read it with matchAll only: one match per `File:`.
|
||||
const TEST_FIRST_FILE_LINE = /\bFile:\**[ \t]*`/gi;
|
||||
const TEST_FIRST_FILE_LINE = /\bFile:\**[ \t]*(?=`)/gi;
|
||||
// From plan_version 1.9: a line that opens with `File:` (after an optional
|
||||
// list marker of the set LIST_ITEM accepts, and `**`), backticked or not.
|
||||
const TEST_FIRST_FILE_LINE_19 = /^\s*(?:(?:[-*+]|\d+[.)])\s+)?(?:\*\*)?File:(?:\*\*)?/i;
|
||||
// list marker (LIST_MARKER) and `**`), backticked or not.
|
||||
const TEST_FIRST_FILE_LINE_19 = new RegExp(String.raw`^\s*(?:${LIST_MARKER}\s+)?(?:\*\*)?File:(?:\*\*)?`, 'i');
|
||||
|
||||
/**
|
||||
* plan_version 1.9: the rest of each `Test first:` `File:` line (after `File:`
|
||||
|
|
@ -692,7 +697,7 @@ export function testFirstFiles(fields, version) {
|
|||
}
|
||||
for (const line of String(fields.get('Test first').text ?? '').split('\n')) {
|
||||
for (const at of line.matchAll(TEST_FIRST_FILE_LINE)) {
|
||||
for (const s of leadingSpans(line.slice(at.index + at[0].length - 1))) {
|
||||
for (const s of leadingSpans(line.slice(at.index + at[0].length))) {
|
||||
if (!(tickedPath(s) || BUILD_FILE.test(s))) continue;
|
||||
const p = norm(s);
|
||||
if (!out.includes(p)) out.push(p);
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
// Reuses: SEVERITY_VALUES / RULE_KEYS / getRule (rule-catalogue.mjs),
|
||||
// computeFindingId (finding-id.mjs, triplet), validateFindings
|
||||
// (findings-schema.mjs). Triplet key format mirrors
|
||||
// scripts/bakeoff-armA-merge.mjs:33; raised_by provenance mirrors
|
||||
// scripts/bakeoff-armA-merge.mjs `dedupByTriplet`; raised_by provenance mirrors
|
||||
// lib/review/plan-review-dedup.mjs.
|
||||
|
||||
import { SEVERITY_VALUES, RULE_KEYS, getRule } from './rule-catalogue.mjs';
|
||||
|
|
@ -262,18 +262,10 @@ export function dedupByTriplet(findings) {
|
|||
}
|
||||
const out = [];
|
||||
for (const group of groups.values()) {
|
||||
// A brief-conflict mark is not carried over from a group member to the
|
||||
// survivor: when the survivor is unmarked, the group stays unmarked and is
|
||||
// judged at its full severity. The survivor is ranked by catalogue tier
|
||||
// first; for a catalogue key the tier always ties inside one triplet (the
|
||||
// rule_key is shared), so a reviewer-agent finding survives over an engine
|
||||
// finding (decision 13, operator 12.10; review bc7f4e23), then the higher
|
||||
// asserted severity among findings of one kind, then the conformance
|
||||
// reviewer, else the first-seen one stays. The survivor is written with the
|
||||
// highest severity any finding in its group asserted, so an engine's mark
|
||||
// never lowers what a reviewer agent asserted, whatever the payload order
|
||||
// (plan 2026-10-11-files-backticks-trial4, ADR "Pass 1 survivor order and
|
||||
// group severity").
|
||||
// Ranked as the JSDoc above says. A brief-conflict mark is not carried over
|
||||
// from a group member to the survivor: when the survivor is unmarked, the
|
||||
// group stays unmarked and is judged at its full severity. For a catalogue
|
||||
// key the tier always ties inside one triplet (the rule_key is shared).
|
||||
const tier = (f) => severityRank(getRule(f.rule_key)?.severity ?? f.severity);
|
||||
let survivor = group[0];
|
||||
for (const f of group.slice(1)) {
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export const IMPROVEMENT_CATEGORIES = Object.freeze(new Set(['simplification', '
|
|||
*/
|
||||
export function toReviewerPayload(input, opts = {}) {
|
||||
const errors = [];
|
||||
const spec = Object.hasOwn(ENGINES, String(input?.engine ?? '')) ? ENGINES[input.engine] : null;
|
||||
const spec = Object.hasOwn(ENGINES, input?.engine ?? '') ? ENGINES[input.engine] : null;
|
||||
if (!spec) return { ok: false, errors: [`engine must be one of ${Object.keys(ENGINES).join('|')}, got ${JSON.stringify(input?.engine)}`] };
|
||||
if (!Array.isArray(input.findings)) return { ok: false, errors: ['findings must be an array'] };
|
||||
if (opts.scopeFiles && opts.scopeFiles.length === 0) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue