pan-wizard 3.35.0 → 3.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/agents/pan-designer.md +1 -1
- package/agents/pan-executor.md +25 -0
- package/agents/pan-hardener.md +2 -0
- package/agents/pan-phase-researcher.md +14 -0
- package/agents/pan-plan-checker.md +28 -1
- package/agents/pan-planner.md +11 -1
- package/agents/pan-previewer.md +2 -0
- package/agents/pan-project-researcher.md +8 -0
- package/agents/pan-reviewer.md +5 -0
- package/agents/pan-verifier.md +2 -2
- package/commands/pan/cost.md +21 -1
- package/commands/pan/preview.md +1 -1
- package/package.json +1 -1
- package/pan-wizard-core/bin/lib/config.cjs +10 -3
- package/pan-wizard-core/bin/lib/core.cjs +9 -4
- package/pan-wizard-core/bin/lib/cost.cjs +146 -0
- package/pan-wizard-core/bin/lib/doc-lint.cjs +5 -3
- package/pan-wizard-core/bin/lib/findings.cjs +115 -6
- package/pan-wizard-core/bin/lib/git.cjs +1 -1
- package/pan-wizard-core/bin/lib/mutate.cjs +257 -0
- package/pan-wizard-core/bin/lib/phase-gate.cjs +15 -9
- package/pan-wizard-core/bin/lib/phase-remove.cjs +91 -11
- package/pan-wizard-core/bin/lib/phase.cjs +37 -9
- package/pan-wizard-core/bin/lib/plan-lint.cjs +161 -0
- package/pan-wizard-core/bin/lib/preview.cjs +3 -0
- package/pan-wizard-core/bin/lib/research-lint.cjs +121 -0
- package/pan-wizard-core/bin/lib/roadmap.cjs +1 -0
- package/pan-wizard-core/bin/lib/test-gate.cjs +433 -0
- package/pan-wizard-core/bin/lib/usage.cjs +104 -0
- package/pan-wizard-core/bin/lib/verify-plan-set.cjs +1 -1
- package/pan-wizard-core/bin/lib/verify-red.cjs +138 -0
- package/pan-wizard-core/bin/lib/verify-scope.cjs +143 -2
- package/pan-wizard-core/bin/lib/verify.cjs +27 -27
- package/pan-wizard-core/bin/pan-tools.cjs +94 -6
- package/pan-wizard-core/mcp/orchestrator.cjs +4 -1
- package/pan-wizard-core/references/design-methodology.md +1 -1
- package/pan-wizard-core/references/handoff-decisions.md +1 -1
- package/pan-wizard-core/templates/config.json +5 -1
- package/pan-wizard-core/templates/design.md +5 -4
- package/pan-wizard-core/templates/phase-prompt.md +7 -0
- package/pan-wizard-core/templates/research-project/architecture.md +6 -0
- package/pan-wizard-core/templates/research-project/features.md +6 -0
- package/pan-wizard-core/templates/research-project/pitfalls.md +6 -0
- package/pan-wizard-core/templates/research-project/stack.md +6 -0
- package/pan-wizard-core/templates/research.md +10 -0
- package/pan-wizard-core/templates/summary-complex.md +5 -1
- package/pan-wizard-core/templates/summary-minimal.md +4 -0
- package/pan-wizard-core/templates/summary-standard.md +5 -1
- package/pan-wizard-core/templates/summary.md +8 -1
- package/pan-wizard-core/workflows/exec-phase.md +43 -7
- package/pan-wizard-core/workflows/execute-plan.md +3 -1
- package/pan-wizard-core/workflows/help.md +103 -44
- package/pan-wizard-core/workflows/plan-phase.md +20 -2
- package/pan-wizard-core/workflows/remove-phase.md +11 -1
- package/pan-wizard-core/workflows/settings.md +17 -0
- package/pan-wizard-core/workflows/verify-phase.md +39 -46
- package/scripts/build-help.cjs +106 -0
- package/scripts/mutation-probe.cjs +7 -80
- package/scripts/test-surface.cjs +168 -6
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* RED and catch proof (program V4): does each test this phase wrote actually test what
|
|
4
|
+
* the phase built?
|
|
5
|
+
*
|
|
6
|
+
* A test that passes before the phase's code exists cannot have caught anything: it is
|
|
7
|
+
* tautological, or it tests something the phase did not change. So, independent of the
|
|
8
|
+
* order the commits landed in (Meta's "weak catch"):
|
|
9
|
+
*
|
|
10
|
+
* base the parent of the phase's oldest plan commit (`{type}({phase}-{plan}):`)
|
|
11
|
+
* tests the test files the phase's plan commits added or changed, as they are now
|
|
12
|
+
* red in a throwaway worktree at the base, with the test file copied in from HEAD,
|
|
13
|
+
* the test must FAIL (a crash on a missing module counts: the code is not there)
|
|
14
|
+
* green at HEAD it must PASS
|
|
15
|
+
*
|
|
16
|
+
* caught red at the base, green at HEAD
|
|
17
|
+
* not_red passes without the phase's code: it cannot have caught a regression
|
|
18
|
+
* not_green fails at HEAD: the test gate reports that, not this check
|
|
19
|
+
* not_checked no command can run that one file (cargo, Maven, Gradle, make), with why
|
|
20
|
+
*
|
|
21
|
+
* Each file runs on its own (`workflow.test_file_command`, else derived from the suite's
|
|
22
|
+
* runner: test-gate.cjs `fileCommand`). The worktree links the project's node_modules,
|
|
23
|
+
* which git does not track, so a JS runner resolves the same at the base as at HEAD.
|
|
24
|
+
*/
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const os = require('os');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const { execGit, findPhaseInternal, output, error } = require('./core.cjs');
|
|
29
|
+
const { committedFiles } = require('./verify-scope.cjs');
|
|
30
|
+
const { isTestFile } = require('./verify-integrity.cjs');
|
|
31
|
+
const TG = require('./test-gate.cjs');
|
|
32
|
+
|
|
33
|
+
const DEFAULT_FILE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The commit the phase started from: the parent of its oldest plan commit.
|
|
37
|
+
* @returns {{base: string, files: Set<string>, commits: number}|{base: null, reason: string}}
|
|
38
|
+
*/
|
|
39
|
+
function phaseBase(cwd, phase) {
|
|
40
|
+
const c = committedFiles(cwd, phase);
|
|
41
|
+
if (!c.git) return { base: null, reason: 'git is unavailable' };
|
|
42
|
+
if (!c.commits) return { base: null, reason: 'the phase has no plan commits (subjects like feat(03-01): …)' };
|
|
43
|
+
const oldest = c.hashes[c.hashes.length - 1];
|
|
44
|
+
const r = execGit(cwd, ['rev-parse', '--verify', '-q', `${oldest}^`]);
|
|
45
|
+
if (r.exitCode !== 0 || !r.stdout) return { base: null, reason: 'the phase starts at the first commit of the repository: there is no base to run against' };
|
|
46
|
+
return { base: r.stdout.trim(), files: c.files, commits: c.commits };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Remove a directory link (a junction or a symlink) without touching what it points to. */
|
|
50
|
+
function unlinkDir(p) {
|
|
51
|
+
try { fs.unlinkSync(p); } catch { try { fs.rmdirSync(p); } catch { /* already gone */ } }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Run `fn(dir)` in a detached worktree at `rev`, then remove the worktree. The project's
|
|
56
|
+
* node_modules is linked in (and unlinked before git removes the worktree, so the removal
|
|
57
|
+
* can never reach the real one).
|
|
58
|
+
* @returns {{value?: *, error?: string}}
|
|
59
|
+
*/
|
|
60
|
+
function withWorktree(cwd, rev, fn) {
|
|
61
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pan-wt-'));
|
|
62
|
+
const dir = path.join(root, 'wt');
|
|
63
|
+
const add = execGit(cwd, ['worktree', 'add', '--detach', '-q', dir, rev]);
|
|
64
|
+
if (add.exitCode !== 0) {
|
|
65
|
+
try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
66
|
+
return { error: `git worktree add failed: ${add.stderr || add.stdout}` };
|
|
67
|
+
}
|
|
68
|
+
const link = path.join(dir, 'node_modules');
|
|
69
|
+
let linked = false;
|
|
70
|
+
try {
|
|
71
|
+
const modules = path.join(cwd, 'node_modules');
|
|
72
|
+
if (fs.existsSync(modules) && !fs.existsSync(link)) {
|
|
73
|
+
try { fs.symlinkSync(modules, link, 'junction'); linked = true; } catch { /* runs without it */ }
|
|
74
|
+
}
|
|
75
|
+
return { value: fn(dir) };
|
|
76
|
+
} finally {
|
|
77
|
+
if (linked) unlinkDir(link);
|
|
78
|
+
execGit(cwd, ['worktree', 'remove', '--force', dir]);
|
|
79
|
+
execGit(cwd, ['worktree', 'prune']);
|
|
80
|
+
try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const tail = (s) => TG.stripAnsi(String(s || '')).replace(/\r\n?/g, '\n').trim().split('\n').slice(-6).join('\n');
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The RED/catch proof for one phase.
|
|
88
|
+
* @param {string} cwd
|
|
89
|
+
* @param {string} phase
|
|
90
|
+
* @param {{timeoutMs?: number}} [opts]
|
|
91
|
+
* @returns {Object} `{phase, checked, ok, base, files: [{file, verdict, command, base_exit, head_exit, detail}], reason?}` or `{error, reason}`
|
|
92
|
+
*/
|
|
93
|
+
function checkRed(cwd, phase, opts = {}) {
|
|
94
|
+
const info = findPhaseInternal(cwd, phase);
|
|
95
|
+
if (!info) return { error: 'phase_not_found', reason: `no phase directory for phase ${phase}` };
|
|
96
|
+
const phaseNumber = info.phase_number || String(phase);
|
|
97
|
+
const b = phaseBase(cwd, phaseNumber);
|
|
98
|
+
if (!b.base) return { phase: phaseNumber, checked: false, ok: true, files: [], reason: b.reason };
|
|
99
|
+
const tests = [...b.files].filter((f) => isTestFile(f) && fs.existsSync(path.join(cwd, f))).sort();
|
|
100
|
+
if (!tests.length) return { phase: phaseNumber, checked: false, ok: true, base: b.base, files: [], reason: 'the phase changed no test file' };
|
|
101
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_FILE_TIMEOUT_MS;
|
|
102
|
+
const files = [];
|
|
103
|
+
const wt = withWorktree(cwd, b.base, (dir) => {
|
|
104
|
+
for (const file of tests) {
|
|
105
|
+
const fc = TG.fileCommand(cwd, file);
|
|
106
|
+
if (!fc.command) { files.push({ file, verdict: 'not_checked', detail: fc.reason }); continue; }
|
|
107
|
+
const dest = path.join(dir, file);
|
|
108
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
109
|
+
fs.copyFileSync(path.join(cwd, file), dest);
|
|
110
|
+
const atBase = TG.runCommand(dir, fc.command, timeoutMs);
|
|
111
|
+
const atHead = TG.runCommand(cwd, fc.command, timeoutMs);
|
|
112
|
+
const red = atBase.exit !== 0;
|
|
113
|
+
const green = atHead.exit === 0;
|
|
114
|
+
const verdict = red && green ? 'caught' : (!red ? 'not_red' : 'not_green');
|
|
115
|
+
const detail = verdict === 'not_red'
|
|
116
|
+
? 'passes without the phase\'s code: it is tautological, or tests something the phase did not change'
|
|
117
|
+
: verdict === 'not_green' ? `fails at HEAD: ${tail(atHead.output)}` : null;
|
|
118
|
+
files.push({ file, verdict, command: fc.command, base_exit: atBase.exit, head_exit: atHead.exit, ...(detail ? { detail } : {}) });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
if (wt.error) return { phase: phaseNumber, checked: false, ok: true, base: b.base, files: [], reason: wt.error };
|
|
122
|
+
const ran = files.filter((f) => f.verdict !== 'not_checked');
|
|
123
|
+
return {
|
|
124
|
+
phase: phaseNumber, checked: ran.length > 0, ok: ran.every((f) => f.verdict === 'caught'), base: b.base,
|
|
125
|
+
caught: ran.filter((f) => f.verdict === 'caught').length, files,
|
|
126
|
+
...(ran.length ? {} : { reason: 'no changed test file could be run on its own' }),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** `pan-tools verify red <phase> [--timeout <ms>]`: exit 1 when a test is not_red or not_green. */
|
|
131
|
+
function cmdVerifyRed(cwd, phase, raw, opts = {}) {
|
|
132
|
+
if (!phase) error('Usage: verify red <phase> [--timeout <ms>]');
|
|
133
|
+
const r = checkRed(cwd, phase, { timeoutMs: opts.timeout ? Number(opts.timeout) : undefined });
|
|
134
|
+
if (r.error) error(r.reason);
|
|
135
|
+
output(r, raw, !r.checked ? 'not_checked' : (r.ok ? 'caught' : 'not_caught'), r.ok ? 0 : 1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { checkRed, cmdVerifyRed, phaseBase, withWorktree };
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { safeReadFile, execGit, findPhaseInternal, toPosix, output, escapeRegex } = require('./core.cjs');
|
|
20
|
-
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
20
|
+
const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
|
|
21
21
|
const { isPlanFile, isSummaryFile, isVerificationFile } = require('./constants.cjs');
|
|
22
22
|
const { planningRel } = require('./utils.cjs');
|
|
23
23
|
|
|
@@ -151,6 +151,147 @@ function scopePhase(cwd, phase) {
|
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
// ─── Review scope (program R1) ───────────────────────────────────────────────
|
|
155
|
+
// The reviewer used to get the summaries' key-files: what the executor said it touched.
|
|
156
|
+
// A file the executor changed and did not list was never reviewed. The review scope is
|
|
157
|
+
// the diff instead (the phase's plan commits, plus work not yet committed), with the
|
|
158
|
+
// intent the code is judged against: each plan's objective, must_haves and Review Focus,
|
|
159
|
+
// and the text of the requirements the plans claim.
|
|
160
|
+
|
|
161
|
+
/** Files modified, staged or untracked in the working tree, relative to cwd. */
|
|
162
|
+
function workingTreeFiles(cwd) {
|
|
163
|
+
// Porcelain v2 with -z: each entry starts with its type (`1`, `2`, `u`, `?`), never a
|
|
164
|
+
// space, and paths come unquoted. v1 starts an unstaged edit with a space (" M a.js"),
|
|
165
|
+
// which execGit's trim() cut off the first line, and the path lost its first letter.
|
|
166
|
+
const r = execGit(cwd, ['status', '--porcelain=v2', '-z', '--untracked-files=all', '--', '.']);
|
|
167
|
+
if (r.exitCode !== 0) return { git: false, uncommitted: [], untracked: [] };
|
|
168
|
+
const top = execGit(cwd, ['rev-parse', '--show-prefix']);
|
|
169
|
+
const prefix = top.exitCode === 0 ? normalisePath(top.stdout.trim()) : '';
|
|
170
|
+
const rel = (p) => { const n = normalisePath(p); return prefix && n.startsWith(prefix) ? n.slice(prefix.length) : n; };
|
|
171
|
+
const uncommitted = [];
|
|
172
|
+
const untracked = [];
|
|
173
|
+
const entries = r.stdout.split('\0');
|
|
174
|
+
for (let i = 0; i < entries.length; i++) {
|
|
175
|
+
const e = entries[i];
|
|
176
|
+
if (!e) continue;
|
|
177
|
+
const f = e.split(' ');
|
|
178
|
+
if (e[0] === '?') untracked.push(rel(e.slice(2)));
|
|
179
|
+
else if (e[0] === '1') { if (!f[1].includes('D')) uncommitted.push(rel(f.slice(8).join(' '))); }
|
|
180
|
+
else if (e[0] === '2') { if (!f[1].includes('D')) uncommitted.push(rel(f.slice(9).join(' '))); i++; } // the next entry is the old path
|
|
181
|
+
else if (e[0] === 'u') uncommitted.push(rel(f.slice(10).join(' ')));
|
|
182
|
+
}
|
|
183
|
+
return { git: true, uncommitted, untracked };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** A markdown section's body, from its `## <title>` heading to the next heading of the same or higher level. */
|
|
187
|
+
function sectionBody(text, title) {
|
|
188
|
+
const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
|
|
189
|
+
const re = new RegExp(`^(#{1,6})\\s+${escapeRegex(title)}\\s*$`, 'i');
|
|
190
|
+
const at = lines.findIndex((l) => re.test(l.trim()));
|
|
191
|
+
if (at === -1) return null;
|
|
192
|
+
const level = lines[at].trim().match(/^#+/)[0].length;
|
|
193
|
+
const out = [];
|
|
194
|
+
for (let i = at + 1; i < lines.length; i++) {
|
|
195
|
+
const h = lines[i].match(/^(#{1,6})\s/);
|
|
196
|
+
if (h && h[1].length <= level) break;
|
|
197
|
+
out.push(lines[i]);
|
|
198
|
+
}
|
|
199
|
+
const body = out.join('\n').trim();
|
|
200
|
+
return body || null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The text between `<tag>` and `</tag>`, trimmed, or null. */
|
|
204
|
+
function tagBody(text, tag) {
|
|
205
|
+
const m = String(text || '').match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`));
|
|
206
|
+
return m && m[1].trim() ? m[1].trim() : null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* What a reviewer of one phase reads: the diff and the intent.
|
|
211
|
+
* @returns {Object} `{phase, directory, base, files: [{path, sources, listed}], unlisted, excluded,
|
|
212
|
+
* intent: {plans: [{plan, objective, must_haves, review_focus, requirements}], requirements: [{id, text}], missing_requirements}, git, commits}`
|
|
213
|
+
* or `{error, reason}`. `sources` is any of commit · summary · uncommitted · untracked;
|
|
214
|
+
* `listed` is whether a summary's key-files names it; `unlisted` lists the files that are not.
|
|
215
|
+
*/
|
|
216
|
+
function reviewScope(cwd, phase) {
|
|
217
|
+
const info = findPhaseInternal(cwd, phase);
|
|
218
|
+
if (!info) return { error: 'phase_not_found', reason: `no phase directory for phase ${phase}` };
|
|
219
|
+
const dir = path.join(cwd, info.directory);
|
|
220
|
+
const sources = new Map();
|
|
221
|
+
const note = (file, source) => {
|
|
222
|
+
if (!file) return;
|
|
223
|
+
if (!sources.has(file)) sources.set(file, new Set());
|
|
224
|
+
sources.get(file).add(source);
|
|
225
|
+
};
|
|
226
|
+
const listed = new Set();
|
|
227
|
+
for (const f of info.summaries.filter(isSummaryFile)) {
|
|
228
|
+
const fm = extractFrontmatter(safeReadFile(path.join(dir, f)) || '');
|
|
229
|
+
const kf = fm['key-files'] || fm.key_files || {};
|
|
230
|
+
for (const p of [...asList(kf.created), ...asList(kf.modified)]) { const n = normalisePath(p); if (n) { listed.add(n); note(n, 'summary'); } }
|
|
231
|
+
}
|
|
232
|
+
const git = committedFiles(cwd, info.phase_number);
|
|
233
|
+
for (const p of git.files) note(p, 'commit');
|
|
234
|
+
const wt = workingTreeFiles(cwd);
|
|
235
|
+
for (const p of wt.uncommitted) note(p, 'uncommitted');
|
|
236
|
+
for (const p of wt.untracked) note(p, 'untracked');
|
|
237
|
+
|
|
238
|
+
const planningPrefix = normalisePath(planningRel());
|
|
239
|
+
const files = [];
|
|
240
|
+
const excluded = [];
|
|
241
|
+
for (const file of [...sources.keys()].sort()) {
|
|
242
|
+
const why = exclusionReason(file, planningPrefix);
|
|
243
|
+
if (why) { excluded.push({ path: file, reason: why }); continue; }
|
|
244
|
+
// A file deleted by the phase has nothing left to read.
|
|
245
|
+
if (!fs.existsSync(path.join(cwd, file))) continue;
|
|
246
|
+
files.push({ path: file, sources: [...sources.get(file)].sort(), listed: listed.has(file) });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const plans = [];
|
|
250
|
+
const reqIds = new Set();
|
|
251
|
+
for (const f of info.plans.filter(isPlanFile).sort()) {
|
|
252
|
+
const text = safeReadFile(path.join(dir, f)) || '';
|
|
253
|
+
const fm = extractFrontmatter(text);
|
|
254
|
+
const requirements = asList(fm.requirements).map((s) => s.trim()).filter(Boolean);
|
|
255
|
+
for (const id of requirements) reqIds.add(id);
|
|
256
|
+
const mh = fm.must_haves && typeof fm.must_haves === 'object' ? fm.must_haves : {};
|
|
257
|
+
plans.push({
|
|
258
|
+
plan: f,
|
|
259
|
+
objective: tagBody(text, 'objective'),
|
|
260
|
+
must_haves: { truths: asList(mh.truths), artifacts: parseMustHavesBlock(text, 'artifacts'), key_links: parseMustHavesBlock(text, 'key_links') },
|
|
261
|
+
review_focus: sectionBody(text, 'Review Focus'),
|
|
262
|
+
requirements,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const { requirementLines } = require('./roadmap.cjs');
|
|
266
|
+
const reqText = safeReadFile(path.join(cwd, planningRel(), 'requirements.md')) || '';
|
|
267
|
+
const ids = [...reqIds].sort();
|
|
268
|
+
const requirements = ids.map((id) => ({ id, text: requirementLines(reqText, [id]).lines.join('\n').trim() || null }));
|
|
269
|
+
|
|
270
|
+
let base = null;
|
|
271
|
+
if (git.hashes.length) {
|
|
272
|
+
const r = execGit(cwd, ['rev-parse', '--verify', '-q', `${git.hashes[git.hashes.length - 1]}^`]);
|
|
273
|
+
if (r.exitCode === 0 && r.stdout) base = r.stdout.trim();
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
phase: info.phase_number,
|
|
277
|
+
directory: info.directory,
|
|
278
|
+
base,
|
|
279
|
+
files,
|
|
280
|
+
unlisted: files.filter((f) => !f.listed).map((f) => f.path),
|
|
281
|
+
excluded,
|
|
282
|
+
intent: { plans, requirements, missing_requirements: requirements.filter((r) => !r.text).map((r) => r.id) },
|
|
283
|
+
git: git.git,
|
|
284
|
+
commits: git.commits,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** `pan-tools review scope <phase>`: the JSON scope; `--raw` prints the file paths, one per line. */
|
|
289
|
+
function cmdReviewScope(cwd, phase, raw) {
|
|
290
|
+
if (!phase) { output({ error: 'phase_required', reason: 'Usage: review scope <phase>' }, raw); return; }
|
|
291
|
+
const r = reviewScope(cwd, phase);
|
|
292
|
+
output(r, raw, r.error ? undefined : r.files.map((f) => f.path).join('\n'));
|
|
293
|
+
}
|
|
294
|
+
|
|
154
295
|
function cmdVerifyScope(cwd, phase, raw) {
|
|
155
296
|
if (!phase) { output({ error: 'phase_required', reason: 'Usage: verify scope <phase>' }, raw); return; }
|
|
156
297
|
const r = scopePhase(cwd, phase);
|
|
@@ -224,4 +365,4 @@ function cmdVerifyStale(cwd, phase, raw) {
|
|
|
224
365
|
output(r, raw, r.error ? undefined : r.state);
|
|
225
366
|
}
|
|
226
367
|
|
|
227
|
-
module.exports = { scopePhase, cmdVerifyScope, verificationFreshness, cmdVerifyStale, coveredFiles, committedFiles, LOCKFILES, TEST_PATH_RE, TEST_FILE_RE };
|
|
368
|
+
module.exports = { scopePhase, cmdVerifyScope, reviewScope, cmdReviewScope, workingTreeFiles, sectionBody, verificationFreshness, cmdVerifyStale, coveredFiles, committedFiles, LOCKFILES, TEST_PATH_RE, TEST_FILE_RE };
|
|
@@ -572,8 +572,21 @@ function reconcilePhase(cwd, phaseNum) {
|
|
|
572
572
|
contradictions.push(`verification status "${status}" but ${keyLinks.total - keyLinks.verified}/${keyLinks.total} key-link(s) UNWIRED`);
|
|
573
573
|
}
|
|
574
574
|
}
|
|
575
|
+
// The engine's test run (V2) is the record the verifier copies its test gate from: a
|
|
576
|
+
// verification that says otherwise, or passes over a failed run, contradicts it.
|
|
577
|
+
const testRun = require('./test-gate.cjs').readTestRun(cwd, phaseNum);
|
|
578
|
+
const writtenGate = (verRaw.match(/^test_gate:\s*([A-Za-z_]+)/m) || [])[1] || null;
|
|
579
|
+
if (testRun && testRun.status) {
|
|
580
|
+
const why = testRun.reason ? ` (${testRun.reason})` : '';
|
|
581
|
+
if (writtenGate && writtenGate.toLowerCase() !== testRun.status) {
|
|
582
|
+
contradictions.push(`verification test_gate "${writtenGate}" but the engine's test run recorded "${testRun.status}"${why}`);
|
|
583
|
+
} else if (!writtenGate && claimsPass && testRun.status === 'failed') {
|
|
584
|
+
contradictions.push(`verification status "${status}" but the engine's test run failed${why}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
575
587
|
return {
|
|
576
588
|
phase: String(phaseNum), found: true, verification_status: status, claims_pass: claimsPass,
|
|
589
|
+
test_run: testRun ? { status: testRun.status, head: testRun.head || null, regressions: (testRun.regressions || []).length } : undefined,
|
|
577
590
|
mechanical_signals: signals, artifacts, key_links: keyLinks, contradictions,
|
|
578
591
|
reconciled: contradictions.length === 0,
|
|
579
592
|
note: signals === 0 ? 'no must_haves declared — mechanical reconciliation unavailable; verdict trusted' : undefined,
|
|
@@ -1557,35 +1570,22 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1557
1570
|
}
|
|
1558
1571
|
|
|
1559
1572
|
/**
|
|
1560
|
-
* Run
|
|
1573
|
+
* Run the project's tests through the engine's test gate (V2): any language, the
|
|
1574
|
+
* project's own command, and a crash reads failed. `pass` is null when there was nothing
|
|
1575
|
+
* to run (no test command, or one that ran no tests).
|
|
1561
1576
|
*/
|
|
1562
1577
|
function runFullTestCheck(cwd) {
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
const passMatch = result.match(/[#ℹ]\s*pass\s+(\d+)/);
|
|
1575
|
-
return {
|
|
1576
|
-
pass: true,
|
|
1577
|
-
exitCode: 0,
|
|
1578
|
-
tests: testMatch ? parseInt(testMatch[1], 10) : null,
|
|
1579
|
-
passing: passMatch ? parseInt(passMatch[1], 10) : null,
|
|
1580
|
-
};
|
|
1581
|
-
} catch (err) {
|
|
1582
|
-
return {
|
|
1583
|
-
pass: false,
|
|
1584
|
-
exitCode: err.status || 1,
|
|
1585
|
-
tests: null,
|
|
1586
|
-
passing: null,
|
|
1587
|
-
};
|
|
1588
|
-
}
|
|
1578
|
+
const rec = require('./test-gate.cjs').runTests(cwd, { timeoutMs: 120000 });
|
|
1579
|
+
return {
|
|
1580
|
+
pass: rec.status === 'passed' ? true : (rec.status === 'failed' ? false : null),
|
|
1581
|
+
exitCode: rec.exit,
|
|
1582
|
+
tests: rec.total,
|
|
1583
|
+
passing: rec.passed,
|
|
1584
|
+
status: rec.status,
|
|
1585
|
+
command: rec.command,
|
|
1586
|
+
reason: rec.reason || undefined,
|
|
1587
|
+
failing: rec.failing && rec.failing.length ? rec.failing.slice(0, 20) : undefined,
|
|
1588
|
+
};
|
|
1589
1589
|
}
|
|
1590
1590
|
|
|
1591
1591
|
/**
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
* phase next-decimal <phase> Calculate next decimal phase number
|
|
42
42
|
* phase add <description> Append new phase to roadmap + create dir
|
|
43
43
|
* phase insert <after> <description> Insert decimal phase after existing
|
|
44
|
-
* phase remove <phase> [--force]
|
|
44
|
+
* phase remove <phase> [--force] [--with-decimals]
|
|
45
|
+
* Remove phase, renumber all subsequent
|
|
45
46
|
* phase complete <phase> Mark phase done, update state + roadmap
|
|
46
47
|
*
|
|
47
48
|
* Roadmap Operations:
|
|
@@ -92,6 +93,26 @@
|
|
|
92
93
|
* verify commits <h1> [h2] ... Batch verify commit hashes
|
|
93
94
|
* verify artifacts <plan-file> Check must_haves.artifacts
|
|
94
95
|
* verify key-links <plan-file> Check must_haves.key_links
|
|
96
|
+
* verify red <phase> Each test the phase changed fails at the
|
|
97
|
+
* [--timeout <ms>] phase's base and passes at HEAD (V4)
|
|
98
|
+
* verify mutate --phase N Mutation report on the phase's changed lines
|
|
99
|
+
* [--max n] [--seed n] (V5): which mutants the suite does not kill;
|
|
100
|
+
* [--timeout <ms>] writes {NN}-mutation.json
|
|
101
|
+
* verify review-focus <phase> Every plan has a Review Focus: <=5 items,
|
|
102
|
+
* each naming its task and test (P4)
|
|
103
|
+
* verify decisions <phase> Plans cite only DD-N ids the design records,
|
|
104
|
+
* and every recorded one is cited (P2)
|
|
105
|
+
* usage Local census: installed commands and agents
|
|
106
|
+
* used, never used, unfinished (L9)
|
|
107
|
+
* research lint <phase|path> Claims table: source and date on every claim,
|
|
108
|
+
* [--now YYYY-MM-DD] stale ones per type window (P3)
|
|
109
|
+
*
|
|
110
|
+
* Test Gate (V2):
|
|
111
|
+
* test run [--phase N] [--baseline] Run the project's tests, any language; with
|
|
112
|
+
* [--command "<cmd>"] --phase, write {NN}-test-run.json (or the
|
|
113
|
+
* [--timeout <ms>] [--rebaseline] --baseline record) to the phase directory; an
|
|
114
|
+
* existing baseline is kept unless --rebaseline
|
|
115
|
+
* test detect The test command the gate would run, and why
|
|
95
116
|
*
|
|
96
117
|
* Template Fill:
|
|
97
118
|
* template fill summary --phase N Create pre-filled summary.md
|
|
@@ -240,6 +261,10 @@ const findings = require('./lib/findings.cjs');
|
|
|
240
261
|
const verifyPlanSet = require('./lib/verify-plan-set.cjs');
|
|
241
262
|
const verifyIntegrity = require('./lib/verify-integrity.cjs');
|
|
242
263
|
const phaseGate = require('./lib/phase-gate.cjs');
|
|
264
|
+
const testGate = require('./lib/test-gate.cjs');
|
|
265
|
+
const verifyRed = require('./lib/verify-red.cjs');
|
|
266
|
+
const verifyScope = require('./lib/verify-scope.cjs');
|
|
267
|
+
const mutate = require('./lib/mutate.cjs');
|
|
243
268
|
const knowledge = require('./lib/knowledge.cjs');
|
|
244
269
|
const skillAlign = require('./lib/skill-align.cjs');
|
|
245
270
|
const hygiene = require('./lib/hygiene.cjs');
|
|
@@ -365,7 +390,7 @@ async function main() {
|
|
|
365
390
|
+ ' --planning-dir <path> act on an arbitrary project-relative planning tree\n'
|
|
366
391
|
+ ' --all-tracks (hygiene) act on the root tree AND every discovered track\n'
|
|
367
392
|
+ ' env: PAN_TRACK, PAN_PLANNING_DIR (flags win)\n'
|
|
368
|
-
+ '\nCommands: state, resolve-model, estimate-cost, find-phase, git, distill, experiment, commit, verify-summary, template, frontmatter, verify, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, config-set, config-get, history-digest, phases, roadmap, requirements, phase, milestone, validate, progress, context-budget, todo, scaffold, init, phase-plan-index, state-snapshot, summary-extract, rollback-snapshot, batch-commit, websearch, focus, preflight, dashboard, hud, report, learnings, deps, drift-check, memory, bridge, whatif, knowledge, skills, hygiene, review-deep, findings, preview, cost, models, squad, worktree, campaign, bus, cache, retro, codebase, standards, optimize, doc-lint, learn, links, version';
|
|
393
|
+
+ '\nCommands: state, resolve-model, estimate-cost, find-phase, git, distill, experiment, commit, verify-summary, template, frontmatter, verify, test, review, research, usage, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, config-set, config-get, history-digest, phases, roadmap, requirements, phase, milestone, validate, progress, context-budget, todo, scaffold, init, phase-plan-index, state-snapshot, summary-extract, rollback-snapshot, batch-commit, websearch, focus, preflight, dashboard, hud, report, learnings, deps, drift-check, memory, bridge, whatif, knowledge, skills, hygiene, review-deep, findings, preview, cost, models, squad, worktree, campaign, bus, cache, retro, codebase, standards, optimize, doc-lint, learn, links, version';
|
|
369
394
|
|
|
370
395
|
if (!command) {
|
|
371
396
|
error(USAGE);
|
|
@@ -630,6 +655,24 @@ async function main() {
|
|
|
630
655
|
break;
|
|
631
656
|
}
|
|
632
657
|
|
|
658
|
+
case 'test': {
|
|
659
|
+
const subcommand = args[1];
|
|
660
|
+
if (subcommand === 'run') {
|
|
661
|
+
testGate.cmdTestRun(cwd, {
|
|
662
|
+
phase: getArgValue(args, '--phase'),
|
|
663
|
+
baseline: args.includes('--baseline'),
|
|
664
|
+
rebaseline: args.includes('--rebaseline'),
|
|
665
|
+
command: getArgValue(args, '--command'),
|
|
666
|
+
timeout: getArgValue(args, '--timeout'),
|
|
667
|
+
}, raw);
|
|
668
|
+
} else if (subcommand === 'detect') {
|
|
669
|
+
testGate.cmdTestDetect(cwd, raw);
|
|
670
|
+
} else {
|
|
671
|
+
error('Unknown test subcommand. Available: run, detect');
|
|
672
|
+
}
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
|
|
633
676
|
case 'verify': {
|
|
634
677
|
const subcommand = args[1];
|
|
635
678
|
if (subcommand === 'plan-structure') {
|
|
@@ -656,8 +699,21 @@ async function main() {
|
|
|
656
699
|
verifyPlanSet.cmdVerifyPlanSet(cwd, args[2], raw);
|
|
657
700
|
} else if (subcommand === 'test-integrity') {
|
|
658
701
|
verifyIntegrity.cmdVerifyTestIntegrity(cwd, args[2], raw);
|
|
702
|
+
} else if (subcommand === 'red') {
|
|
703
|
+
verifyRed.cmdVerifyRed(cwd, args[2], raw, { timeout: getArgValue(args, '--timeout') });
|
|
704
|
+
} else if (subcommand === 'review-focus') {
|
|
705
|
+
require('./lib/plan-lint.cjs').cmdVerifyReviewFocus(cwd, args[2], raw);
|
|
706
|
+
} else if (subcommand === 'decisions') {
|
|
707
|
+
require('./lib/plan-lint.cjs').cmdVerifyDecisions(cwd, args[2], raw);
|
|
708
|
+
} else if (subcommand === 'mutate') {
|
|
709
|
+
mutate.cmdVerifyMutate(cwd, {
|
|
710
|
+
phase: getArgValue(args, '--phase') || (args[2] && !args[2].startsWith('--') ? args[2] : null),
|
|
711
|
+
max: getArgValue(args, '--max'),
|
|
712
|
+
seed: getArgValue(args, '--seed'),
|
|
713
|
+
timeout: getArgValue(args, '--timeout'),
|
|
714
|
+
}, raw);
|
|
659
715
|
} else {
|
|
660
|
-
error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, reconcile, stubs, scope, stale, plan-set, test-integrity');
|
|
716
|
+
error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, reconcile, stubs, scope, stale, plan-set, test-integrity, red, mutate, review-focus, decisions');
|
|
661
717
|
}
|
|
662
718
|
break;
|
|
663
719
|
}
|
|
@@ -769,7 +825,7 @@ async function main() {
|
|
|
769
825
|
} else if (subcommand === 'remove') {
|
|
770
826
|
if (!args[2]) error('phase remove requires a phase number');
|
|
771
827
|
const forceFlag = args.includes('--force');
|
|
772
|
-
phase.cmdPhaseRemove(cwd, args[2], { force: forceFlag }, raw);
|
|
828
|
+
phase.cmdPhaseRemove(cwd, args[2], { force: forceFlag, withDecimals: args.includes('--with-decimals') }, raw);
|
|
773
829
|
} else if (subcommand === 'complete') {
|
|
774
830
|
if (!args[2]) error('phase complete requires a phase number');
|
|
775
831
|
const noCommit = args.includes('--no-commit');
|
|
@@ -1257,6 +1313,34 @@ async function main() {
|
|
|
1257
1313
|
break;
|
|
1258
1314
|
}
|
|
1259
1315
|
|
|
1316
|
+
case 'review': {
|
|
1317
|
+
// Program R1: the diff a reviewer reads, with the plans' intent beside it.
|
|
1318
|
+
const subcommand = args[1];
|
|
1319
|
+
if (subcommand === 'scope') {
|
|
1320
|
+
verifyScope.cmdReviewScope(cwd, args[2], raw);
|
|
1321
|
+
} else {
|
|
1322
|
+
error('Unknown review subcommand. Available: scope');
|
|
1323
|
+
}
|
|
1324
|
+
break;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
case 'usage': {
|
|
1328
|
+
// Program L9: which installed commands and agents this project used, from its own files.
|
|
1329
|
+
require('./lib/usage.cjs').cmdUsage(cwd, raw);
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
case 'research': {
|
|
1334
|
+
// Program P3: research claims carry a source and a checked date.
|
|
1335
|
+
const subcommand = args[1];
|
|
1336
|
+
if (subcommand === 'lint') {
|
|
1337
|
+
require('./lib/research-lint.cjs').cmdResearchLint(cwd, args[2], { now: getArgValue(args, '--now') }, raw);
|
|
1338
|
+
} else {
|
|
1339
|
+
error('Unknown research subcommand. Available: lint');
|
|
1340
|
+
}
|
|
1341
|
+
break;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1260
1344
|
case 'review-deep': {
|
|
1261
1345
|
const subcommand = args[1];
|
|
1262
1346
|
const phaseNum = args[2];
|
|
@@ -1307,8 +1391,10 @@ async function main() {
|
|
|
1307
1391
|
}, raw);
|
|
1308
1392
|
} else if (subcommand === 'debt') {
|
|
1309
1393
|
findings.cmdFindingsDebt(cwd, { milestone: filters.milestone }, raw);
|
|
1394
|
+
} else if (subcommand === 'suppressions') {
|
|
1395
|
+
findings.cmdFindingsSuppressions(cwd, { phase: filters.phase, agent: filters.agent }, raw);
|
|
1310
1396
|
} else {
|
|
1311
|
-
error('Unknown findings subcommand. Available: record, list, dispose, debt');
|
|
1397
|
+
error('Unknown findings subcommand. Available: record, list, dispose, debt, suppressions');
|
|
1312
1398
|
}
|
|
1313
1399
|
break;
|
|
1314
1400
|
}
|
|
@@ -1359,8 +1445,10 @@ async function main() {
|
|
|
1359
1445
|
}, raw);
|
|
1360
1446
|
} else if (subcommand === 'limits') {
|
|
1361
1447
|
cost.cmdCostLimits(cwd, raw);
|
|
1448
|
+
} else if (subcommand === 'budget') {
|
|
1449
|
+
cost.cmdCostBudget(cwd, getArgValue(args, '--phase') || (args[2] && !args[2].startsWith('--') ? args[2] : null), raw);
|
|
1362
1450
|
} else {
|
|
1363
|
-
error('Unknown cost subcommand. Available: report, append, clear, rebuild, limits');
|
|
1451
|
+
error('Unknown cost subcommand. Available: report, append, clear, rebuild, limits, budget');
|
|
1364
1452
|
}
|
|
1365
1453
|
break;
|
|
1366
1454
|
}
|
|
@@ -101,7 +101,10 @@ function normalizeStatus(status) {
|
|
|
101
101
|
function isPhaseOpen(phase) {
|
|
102
102
|
if (!phase || typeof phase !== 'object') return false;
|
|
103
103
|
const status = normalizeStatus(phase.status);
|
|
104
|
-
|
|
104
|
+
// The legacy `verified` alias stands for complete-and-verified: once merged, that phase
|
|
105
|
+
// is finished too. Without this a caller sending it got request_merge on every cycle,
|
|
106
|
+
// for a phase already merged (found by H7's nextAction fuzz).
|
|
107
|
+
if (status !== COMPLETE_STATUS) return !(PHASE_NEXT[status] === 'request_merge' && phase.merged === true);
|
|
105
108
|
// Complete on disk, but the run facts decide whether it is actually finished.
|
|
106
109
|
return !(phase.verified === true && phase.merged === true);
|
|
107
110
|
}
|
|
@@ -52,7 +52,7 @@ ordered; a tier makes a contiguous prefix (plus its named additions) mandatory.
|
|
|
52
52
|
- **Components & boundaries** — what modules are added/touched; confirm the design honors the discovered boundaries (no layer violations).
|
|
53
53
|
- **Interface contract** — exact invocation + input/output schema + status/exit codes, per project convention.
|
|
54
54
|
- **State & filesystem scope** — reads from / writes to (must stay within project root / `.planning/`), state mutations, side effects.
|
|
55
|
-
- **Design decisions** — each
|
|
55
|
+
- **Design decisions** — each decision that passes the three-question test (hard to reverse, a reasonable engineer could choose otherwise, more than one task depends on it): its id (`DD-N`, which plans cite), what was decided, why, alternatives rejected.
|
|
56
56
|
|
|
57
57
|
### 4. ADR (`phase`+)
|
|
58
58
|
- A right-sized Architecture Decision Record: Status · Date · Context (problem, forces, current state, requirements traceability) · Decision (summary + numbered sub-decisions with rationale + integration) · Consequences (positive **and** negative, every negative with a mitigation or explicit acceptance).
|
|
@@ -83,7 +83,7 @@ Lives in `summary.md`, after `## Files Created/Modified`:
|
|
|
83
83
|
- DT-1: Chose <option> for O-N. Reason: <rationale>.
|
|
84
84
|
|
|
85
85
|
### Deviations (from plan; must explain)
|
|
86
|
-
- DV-1: Plan said <X>; I did <Y>. Reason: <rationale>. Verification: <how I confirmed Y is acceptable>.
|
|
86
|
+
- DV-1: Plan said <X>; I did <Y>. Reason: <rationale>. Verification: <how I confirmed Y is acceptable>. Cost if wrong: <low | medium | high> — <what breaks if this call was wrong>.
|
|
87
87
|
|
|
88
88
|
### Open questions for verifier
|
|
89
89
|
- Q-1: <question>. Why it matters: <stake>.
|
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
"auto_advance": false,
|
|
9
9
|
"nyquist_validation": false,
|
|
10
10
|
"phase_record_compact": false,
|
|
11
|
-
"phase_gate": "
|
|
11
|
+
"phase_gate": "enforce",
|
|
12
|
+
"mutation_gate": "report",
|
|
12
13
|
"phase_reports": { "enabled": false, "open": false, "theme": "auto", "index": true }
|
|
13
14
|
},
|
|
15
|
+
"cost": {
|
|
16
|
+
"phase_budget_usd": 20
|
|
17
|
+
},
|
|
14
18
|
"planning": {
|
|
15
19
|
"commit_docs": true,
|
|
16
20
|
"search_gitignored": false
|
|
@@ -66,10 +66,11 @@ tier may omit them.
|
|
|
66
66
|
- Writes to: [paths — within .planning/ or project root]
|
|
67
67
|
- Side effects: [git ops, dir creation, etc.]
|
|
68
68
|
|
|
69
|
-
**Design decisions:**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
**Design decisions:** record a decision here only when it passes all three questions: (1) is it hard to reverse once code depends on it? (2) could a reasonable engineer have chosen otherwise? (3) does more than one task or plan depend on it? A choice that fails any of them is the planner's to make, not a design decision. Each recorded decision gets an id (`DD-1`, `DD-2` …) that the plans implementing it cite; `pan-tools verify decisions` flags a plan citing an id not recorded here and a decision no plan cites.
|
|
70
|
+
|
|
71
|
+
| ID | Decision | Chosen | Alternative rejected | Rationale |
|
|
72
|
+
|----|----------|--------|----------------------|-----------|
|
|
73
|
+
| DD-1 | [point] | [what] | [alt] | [why] |
|
|
73
74
|
</architecture>
|
|
74
75
|
|
|
75
76
|
<adr>
|
|
@@ -71,6 +71,13 @@ Output: [What artifacts will be created]
|
|
|
71
71
|
### Considered and rejected
|
|
72
72
|
- R-1: [alternative]. Rejected because: [reason].
|
|
73
73
|
|
|
74
|
+
<!-- A Locked decision that implements a design decision cites its id: "Source: design DD-2". -->
|
|
75
|
+
|
|
76
|
+
## Review Focus
|
|
77
|
+
<!-- At most five inputs or failure modes the requirements imply that no test exercises yet,
|
|
78
|
+
each pinned to the task that owns the code. If genuinely none: "None — [why]". -->
|
|
79
|
+
- RF-1: [input or failure mode]. Test: Task [N] adds [test that fails if it is mishandled].
|
|
80
|
+
|
|
74
81
|
<tasks>
|
|
75
82
|
|
|
76
83
|
<task type="auto">
|
|
@@ -161,6 +161,12 @@ src/
|
|
|
161
161
|
|----------|---------------|-------|
|
|
162
162
|
| [module A ↔ module B] | [API/events/direct] | [considerations] |
|
|
163
163
|
|
|
164
|
+
## Claims
|
|
165
|
+
|
|
166
|
+
| Claim | Type | Source | Checked |
|
|
167
|
+
|-------|------|--------|---------|
|
|
168
|
+
| [what is true] | [version / api / pricing / security / practice / general] | [URL, doc section or Context7 id] | [YYYY-MM-DD] |
|
|
169
|
+
|
|
164
170
|
## Sources
|
|
165
171
|
|
|
166
172
|
- [Architecture references]
|
|
@@ -104,6 +104,12 @@ Features to defer until product-market fit is established.
|
|
|
104
104
|
| [feature] | [how they do it] | [how they do it] | [our plan] |
|
|
105
105
|
| [feature] | [how they do it] | [how they do it] | [our plan] |
|
|
106
106
|
|
|
107
|
+
## Claims
|
|
108
|
+
|
|
109
|
+
| Claim | Type | Source | Checked |
|
|
110
|
+
|-------|------|--------|---------|
|
|
111
|
+
| [what is true] | [version / api / pricing / security / practice / general] | [URL, doc section or Context7 id] | [YYYY-MM-DD] |
|
|
112
|
+
|
|
107
113
|
## Sources
|
|
108
114
|
|
|
109
115
|
- [Competitor products analyzed]
|