Wave 1, order 3 (P3 plan mode as the start, P2 design as its own part, P9
plan side). Contracts K3-K6 for the later merges.
- plan_version 1.8 (1.7 plans stay valid): Test first on every step, one
End-to-end ("run the thing") step, optional Interface in / Interface out /
Review focus fields, a ceremony class in the header (absent =
architectural), and for architectural plans a ## Design part: module map
without cycles, architecture rules that name their enforcer and the step
that proves them, a requirements check asked as keep / change.
- K3 lib/validators/plan-gate.mjs: the critic verdict is recorded INSIDE the
plan (Claude Code's plan approval hands a hook only the plan file's text),
bound to the hash taken before the reviewers read it; 0 go / 1 stop / 2
could not run; at most 3 critic rounds, then stop and ask.
- K5 lib/verification/arch-rules.mjs: runs the target repo's own
dependency-cruiser or import-linter (installs nothing; 2 = not measured,
never green); --prove --rule shows pass -> fail on a planted violation of
that rule -> pass. Verified against dependency-cruiser 18.5.0 and
import-linter 2.15; madge 8.0.0 left out (missed a planted .mjs cycle).
- K6 lib/verification/plan-scope-sweep.mjs: changed files no step names.
- /trekplan Phases 8-10: design part, bounded critic loop, the gate before
"execute", a plan-mode path through ExitPlanMode (not yet run end to end
inside plan mode). plan-critic 10 -> 12 dimensions; scope-guardian checks
the ceremony.
- Fixed on the way: plan-validator and plan-review-dedup used the
file://argv[1] main guard (no-op under a symlink or a space); CRLF plans
failed every manifest; the dedup dropped finding severity.
Borrowed, credited in place: obra/superpowers, Kiro (docs), addyosmani/
agent-skills, mattpocock/skills, GitHub Spec Kit, bashebr/ai-native-sdlc,
Boris Cherny's public threads.
Suite 1497 (1495 / 0 / 2), 86 new tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
4.2 KiB
JavaScript
129 lines
4.2 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: 'node src/cli.mjs --demo' }];
|
|
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',
|
|
'',
|
|
);
|
|
return parts.join('\n');
|
|
}
|