// 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. // // Raw options on step() write a field the generated form cannot (the // plan_version 1.9 grammar tests): `filesRaw` is the `Files:` value written // verbatim after `- **Files:** `; `testFirstRaw` is the whole `Test first:` // `File:` line written verbatim in place of ` - File: `` (new)`. Without // them the output is unchanged. 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}`}`, '', opts.filesRaw !== undefined ? `- **Files:** ${opts.filesRaw}` : `- **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:**', opts.testFirstRaw !== undefined ? opts.testFirstRaw : ` - 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} [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'); }