fix(stats): require verification for Complete status, add Executed state
Phases with all summaries but no passing VERIFICATION.md now show as "Executed" instead of "Complete", preventing false progress reporting. Adds determinePhaseStatus() helper used by both cmdStats() and cmdProgressRender(). Also fixes duplicate phase directory accumulation in cmdStats() — plans/summaries from directories sharing the same phase number are now summed instead of silently overwritten. New statuses: Executed (summaries done, no verification), Needs Review (verification exists with human_needed status). Closes #1459 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,33 @@ const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, com
|
||||
const { extractFrontmatter } = require('./frontmatter.cjs');
|
||||
const { MODEL_PROFILES } = require('./model-profiles.cjs');
|
||||
|
||||
/**
|
||||
* Determine phase status by checking plan/summary counts AND verification state.
|
||||
* Introduces "Executed" for phases with all summaries but no passing verification.
|
||||
*/
|
||||
function determinePhaseStatus(plans, summaries, phaseDir, defaultPending) {
|
||||
if (plans === 0) return defaultPending;
|
||||
if (summaries < plans && summaries > 0) return 'In Progress';
|
||||
if (summaries < plans) return 'Planned';
|
||||
|
||||
// summaries >= plans — check verification
|
||||
try {
|
||||
const files = fs.readdirSync(phaseDir);
|
||||
const verificationFile = files.find(f => f === 'VERIFICATION.md' || f.endsWith('-VERIFICATION.md'));
|
||||
if (verificationFile) {
|
||||
const content = fs.readFileSync(path.join(phaseDir, verificationFile), 'utf-8');
|
||||
if (/status:\s*passed/i.test(content)) return 'Complete';
|
||||
if (/status:\s*human_needed/i.test(content)) return 'Needs Review';
|
||||
if (/status:\s*gaps_found/i.test(content)) return 'Executed';
|
||||
// Verification exists but unrecognized status — treat as executed
|
||||
return 'Executed';
|
||||
}
|
||||
} catch { /* directory read failed — fall through */ }
|
||||
|
||||
// No verification file — executed but not verified
|
||||
return 'Executed';
|
||||
}
|
||||
|
||||
function cmdGenerateSlug(text, raw) {
|
||||
if (!text) {
|
||||
error('text required for slug generation');
|
||||
@@ -528,11 +555,7 @@ function cmdProgressRender(cwd, format, raw) {
|
||||
totalPlans += plans;
|
||||
totalSummaries += summaries;
|
||||
|
||||
let status;
|
||||
if (plans === 0) status = 'Pending';
|
||||
else if (summaries >= plans) status = 'Complete';
|
||||
else if (summaries > 0) status = 'In Progress';
|
||||
else status = 'Planned';
|
||||
const status = determinePhaseStatus(plans, summaries, path.join(phasesDir, dir), 'Pending');
|
||||
|
||||
phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
|
||||
}
|
||||
@@ -828,18 +851,14 @@ function cmdStats(cwd, format, raw) {
|
||||
totalPlans += plans;
|
||||
totalSummaries += summaries;
|
||||
|
||||
let status;
|
||||
if (plans === 0) status = 'Not Started';
|
||||
else if (summaries >= plans) status = 'Complete';
|
||||
else if (summaries > 0) status = 'In Progress';
|
||||
else status = 'Planned';
|
||||
const status = determinePhaseStatus(plans, summaries, path.join(phasesDir, dir), 'Not Started');
|
||||
|
||||
const existing = phasesByNumber.get(phaseNum);
|
||||
phasesByNumber.set(phaseNum, {
|
||||
number: phaseNum,
|
||||
name: existing?.name || phaseName,
|
||||
plans,
|
||||
summaries,
|
||||
plans: (existing?.plans || 0) + plans,
|
||||
summaries: (existing?.summaries || 0) + summaries,
|
||||
status,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1409,11 +1409,12 @@ describe('stats command', () => {
|
||||
fs.mkdirSync(p1, { recursive: true });
|
||||
fs.mkdirSync(p2, { recursive: true });
|
||||
|
||||
// Phase 1: 2 plans, 2 summaries (complete)
|
||||
// Phase 1: 2 plans, 2 summaries, passing verification (complete)
|
||||
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
||||
fs.writeFileSync(path.join(p1, '01-02-PLAN.md'), '# Plan');
|
||||
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
|
||||
fs.writeFileSync(path.join(p1, '01-02-SUMMARY.md'), '# Summary');
|
||||
fs.writeFileSync(path.join(p1, 'VERIFICATION.md'), '---\nstatus: passed\n---\n# Verification');
|
||||
|
||||
// Phase 2: 1 plan, 0 summaries (planned)
|
||||
fs.writeFileSync(path.join(p2, '02-01-PLAN.md'), '# Plan');
|
||||
@@ -1485,8 +1486,10 @@ describe('stats command', () => {
|
||||
fs.mkdirSync(p2, { recursive: true });
|
||||
fs.writeFileSync(path.join(p1, '14-01-PLAN.md'), '# Plan');
|
||||
fs.writeFileSync(path.join(p1, '14-01-SUMMARY.md'), '# Summary');
|
||||
fs.writeFileSync(path.join(p1, 'VERIFICATION.md'), '---\nstatus: passed\n---\n# Verified');
|
||||
fs.writeFileSync(path.join(p2, '15-01-PLAN.md'), '# Plan');
|
||||
fs.writeFileSync(path.join(p2, '15-01-SUMMARY.md'), '# Summary');
|
||||
fs.writeFileSync(path.join(p2, 'VERIFICATION.md'), '---\nstatus: passed\n---\n# Verified');
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
@@ -1569,6 +1572,7 @@ describe('stats command', () => {
|
||||
fs.mkdirSync(p1, { recursive: true });
|
||||
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
||||
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
|
||||
fs.writeFileSync(path.join(p1, 'VERIFICATION.md'), '---\nstatus: passed\n---\n# Verified');
|
||||
|
||||
const result = runGsdTools('stats table', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
@@ -1580,4 +1584,78 @@ describe('stats command', () => {
|
||||
assert.ok(parsed.rendered.includes('| 1 |'), 'should include phase row');
|
||||
assert.ok(parsed.rendered.includes('1/1 phases'), 'should report phase progress');
|
||||
});
|
||||
|
||||
test('phase with summaries but no verification is Executed, not Complete', () => {
|
||||
const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth');
|
||||
fs.mkdirSync(p1, { recursive: true });
|
||||
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
||||
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
|
||||
const result = runGsdTools('stats', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const stats = JSON.parse(result.output);
|
||||
const phase = stats.phases.find(p => p.number === '01' || p.number === '1');
|
||||
assert.strictEqual(phase.status, 'Executed', 'should be Executed without verification');
|
||||
assert.strictEqual(stats.phases_completed, 0, 'unverified phase should not count as completed');
|
||||
});
|
||||
|
||||
test('phase with passing verification is Complete', () => {
|
||||
const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth');
|
||||
fs.mkdirSync(p1, { recursive: true });
|
||||
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
||||
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
|
||||
fs.writeFileSync(path.join(p1, 'VERIFICATION.md'), '---\nstatus: passed\n---\n# Verification');
|
||||
const result = runGsdTools('stats', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const stats = JSON.parse(result.output);
|
||||
const phase = stats.phases.find(p => p.number === '01' || p.number === '1');
|
||||
assert.strictEqual(phase.status, 'Complete', 'should be Complete with passing verification');
|
||||
assert.strictEqual(stats.phases_completed, 1);
|
||||
});
|
||||
|
||||
test('phase with gaps_found verification is Executed', () => {
|
||||
const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth');
|
||||
fs.mkdirSync(p1, { recursive: true });
|
||||
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
||||
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
|
||||
fs.writeFileSync(path.join(p1, 'VERIFICATION.md'), '---\nstatus: gaps_found\n---\n# Verification');
|
||||
const result = runGsdTools('stats', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const stats = JSON.parse(result.output);
|
||||
const phase = stats.phases.find(p => p.number === '01' || p.number === '1');
|
||||
assert.strictEqual(phase.status, 'Executed', 'gaps_found should show as Executed');
|
||||
});
|
||||
|
||||
test('phase with human_needed verification shows Needs Review', () => {
|
||||
const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth');
|
||||
fs.mkdirSync(p1, { recursive: true });
|
||||
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
||||
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
|
||||
fs.writeFileSync(path.join(p1, 'VERIFICATION.md'), '---\nstatus: human_needed\n---\n# Verification');
|
||||
const result = runGsdTools('stats', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const stats = JSON.parse(result.output);
|
||||
const phase = stats.phases.find(p => p.number === '01' || p.number === '1');
|
||||
assert.strictEqual(phase.status, 'Needs Review', 'human_needed should show as Needs Review');
|
||||
});
|
||||
|
||||
test('progress command also uses verification-aware status', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
`# Roadmap v1.0 MVP\n`
|
||||
);
|
||||
const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth');
|
||||
fs.mkdirSync(p1, { recursive: true });
|
||||
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
||||
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
|
||||
|
||||
const result = runGsdTools('progress json', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.phases[0].status, 'Executed', 'progress should show Executed without verification');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user