voyage/tests/helpers/plan-v18-builder.mjs
Kjell Tore Guttormsen c56f852ab6
fix(validate): a check that can never run is caught when the brief or plan is written
voyage-lekerepo end-to-end trial (2026-10-06), report points 2, 3, 4, 9:
11 of 13 brief criteria were NOT RUN at review with no warning in /trekbrief;
a plan `git grep` criterion passed validator, gate and three critic rounds,
then felled the run in Phase 7; the End-to-end field and Phase 7's text
search never met; a prose Verify left the test command undefined.

- criteria-runner: runnableCheck() screens a declared check without running
  it (same allowlist; an npm run script is not looked up at write time);
  /bin/bash tests/<script> allowed; the plan report carries end_to_end,
  verified when a passed criterion runs a step's End-to-end command (the
  older text form still counts).
- plan-validator 1.8: PLAN_VERIFICATION_NOT_RUNNABLE,
  PLAN_STEP_VERIFY_NOT_RUNNABLE, PLAN_END_TO_END_NOT_RUNNABLE,
  PLAN_END_TO_END_NOT_VERIFIED (the gate inherits them).
- brief-validator: BRIEF_SC_NOT_RUNNABLE warning; /trekbrief offers a
  tests/ script; brief-reviewer no longer proposes unrunnable commands.
- trekexecute: Phase 7 copies end_to_end from the runner; Verify's test
  command is its first backticked span.

Measured on the trial's own brief and plan: 11 warnings (the 11 NOT RUN),
and the plan stops on V5 git grep and the unlisted End-to-end command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 06:42:33 +02:00

131 lines
4.4 KiB
JavaScript

// tests/helpers/plan-v18-builder.mjs
// Builds plan_version 1.8 plans for the plan-contract tests (K3 gate, K4 step
// fields, the design part, the scope sweep). One builder, many variants, so a
// test states only the difference it is about.
function manifest(paths) {
return [
'- **Manifest:**',
' ```yaml',
' manifest:',
' expected_paths:',
...paths.map(p => ` - ${p}`),
` min_file_count: ${paths.length}`,
' commit_message_pattern: "^feat\\\\(x\\\\):"',
' bash_syntax_check: []',
' forbidden_paths: []',
' must_contain: []',
' ```',
].join('\n');
}
export function step(n, opts = {}) {
const file = opts.file ?? `src/mod${n}.mjs`;
const test = opts.test ?? `tests/mod${n}.test.mjs`;
const lines = [
`### Step ${n}: ${opts.title ?? `Build module ${n}`}`,
'',
`- **Files:** \`${file}\``,
`- **Changes:** Add \`run${n}()\` to \`${file}\`. (new file)`,
];
if (opts.interfaceIn !== undefined) lines.push(`- **Interface in:** ${opts.interfaceIn}`);
if (opts.interfaceOut !== undefined) lines.push(`- **Interface out:** ${opts.interfaceOut}`);
if (opts.reviewFocus !== undefined) lines.push(`- **Review focus:** ${opts.reviewFocus}`);
if (opts.testFirst !== false) {
lines.push(
'- **Test first:**',
` - File: \`${test}\` (new)`,
` - Verifies: run${n}() returns the value the next step reads`,
' - Pattern: `tests/lib/plan-schema.test.mjs`',
);
}
if (opts.endToEnd) lines.push(`- **End-to-end:** \`${opts.endToEnd}\` → expected: exit 0 and the CLI prints a result`);
lines.push(
`- **Verify:** \`node --test ${test}\` → expected: pass`,
`- **On failure:** revert — \`git checkout -- ${file}\``,
`- **Checkpoint:** \`git commit -m "feat(x): step ${n}"\``,
manifest(opts.manifestPaths ?? [file, test]),
);
return lines.join('\n');
}
export const DESIGN = [
'## Design',
'',
'### Architecture',
'Two modules: a parser and a CLI on top of it.',
'',
'### Data Flow',
'CLI argv → parser → result JSON on stdout.',
'',
'### Interfaces',
'`parse(text) → {ok, items}`',
'',
'### Data Models',
'`Item = {name: string, n: number}`',
'',
'### Error Handling',
'Unreadable input → exit 2; invalid input → exit 1.',
'',
'### Module Map',
'```mermaid',
'graph TD',
' cli[src/cli.mjs] --> parser[src/parser.mjs]',
' parser --> util[src/util.mjs]',
'```',
'',
'### Architecture Rules',
'| Rule | Enforced by | Proof |',
'|------|-------------|-------|',
'| The parser never imports the CLI | checked by `node_modules/.bin/depcruise --config .dependency-cruiser.cjs src` | Step 1 |',
'| Prefer pure functions in the parser | advisory — `.claude/rules/parser.md` | — |',
'',
'### Requirements Check',
'No conflicts or gaps found between the brief\'s requirements.',
].join('\n');
/**
* Build a plan.
* @param {object} o
* @param {string} [o.version='1.8']
* @param {string|null} [o.ceremony] - header value; null = omit (→ architectural)
* @param {boolean} [o.design=true]
* @param {string} [o.designText]
* @param {Array<object>} [o.steps] - step() option objects
*/
export function buildPlan(o = {}) {
const version = o.version ?? '1.8';
const ceremony = o.ceremony === undefined ? 'architectural' : o.ceremony;
const header = ceremony === null
? `> Generated by trekplan v6.1.1 on 2026-10-06 — \`plan_version: ${version}\``
: `> Generated by trekplan v6.1.1 on 2026-10-06 — \`plan_version: ${version}\` · \`ceremony: ${ceremony}\``;
const stepOpts = o.steps ?? [{}, { endToEnd: 'bash tests/e2e.sh' }];
const parts = [
'# Test plan',
'',
'> **Plan quality: A** (92/100) — APPROVE',
'>',
header,
'',
'## Context',
'',
'Why: the brief asks for a parser CLI.',
'',
];
if (o.design !== false) parts.push(o.designText ?? DESIGN, '');
parts.push('## Implementation Plan', '');
stepOpts.forEach((s, i) => parts.push(step(i + 1, s), ''));
parts.push(
'## Test Strategy',
'',
'- **Framework:** node:test',
'',
'## Verification',
'',
'- [ ] `npm test` → expected: exit 0',
// Each End-to-end command also stands here: Phase 7 runs this section.
...[...new Set(stepOpts.filter(s => s.endToEnd).map(s => s.endToEnd))].map(c => `- [ ] \`${c}\` → expected: exit 0`),
'',
);
return parts.join('\n');
}