fix(#3357): one phase-pinned resolver for verification-report discovery (#3513)

A phase directory can hold more than one `*-VERIFICATION.md` — an ad-hoc `03-CORRECTION-VERIFICATION.md` worksheet beside the real `03-VERIFICATION.md`. Discovery took the alphabetically-first match, so the worksheet won and the phase could report `missing` while a passing report sat next to it.

The issue named two copies. There were seven, in four grammars: two `.sort()[0]` sites in the verification module, three `.find()` over UNSORTED readdir order (phase status, and `verification_path` twice — filesystem-dependent, so two machines on one commit could disagree), and two in shell. All seven now route through one exported `resolveVerificationFile`; the shell copies via a new `verification resolve-file` verb rather than hand-rolling the rule an eighth time.

Fixing five of seven would have been worse than fixing none: the verify-work workflow is a WRITER that stamps `status: passed` onto the file it picks, so canonical-aware readers plus an alphabetical writer means the human_needed→passed canonicalization silently no-ops forever while the worksheet gets stamped. That divergence did not exist on next.

The first resolver was itself a regression — it preferred ANY canonically-shaped name over the phase's own report, so a stray cross-phase or sentinel-numbered file outranked it. The global-canonical preference was removed rather than narrowed; the rule is pinned to the phase token via `PHASE_NUMBER_TOKEN_SOURCE`, its existing owner.

Fixed in passing: the transition workflow's awk guarded on `NR==1` instead of `FNR==1`, so across a multi-file glob it armed only on the first file — a leading worksheet with no frontmatter blocked transition even when the canonical report passed. Also removed a U+00AD soft hyphen introduced earlier on this branch.

Five broader-grammar AGGREGATE scans are deliberately out of scope — a different defect class (phase-unscoped scanning), tracked as #3511.

Closes #3357

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-08-14 19:18:29 -04:00
committed by GitHub
parent c66b010052
commit ddf852873c
13 changed files with 597 additions and 65 deletions

View File

@@ -2215,6 +2215,31 @@ describe('stats command', () => {
assert.strictEqual(stats.plan_percent, 67);
});
// #3473 F2 (companion to #3357): determinePhaseStatus now resolves its
// *-VERIFICATION.md via the shared resolveVerificationFile resolver instead
// of a hand-rolled `.find()` over unsorted readdir() order. Before this fix,
// which of a canonical report and an ad-hoc `-CORRECTION-VERIFICATION.md`
// worksheet "won" was filesystem-dependent; the canonical report must now
// win deterministically regardless of directory-listing order.
test('#3473 F2: phase status resolves the canonical report over a -CORRECTION- worksheet, not readdir order', () => {
const p1 = path.join(tmpDir, '.planning', 'phases', '03-api');
fs.mkdirSync(p1, { recursive: true });
fs.writeFileSync(path.join(p1, '03-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p1, '03-01-SUMMARY.md'), '# Summary');
// The ad-hoc worksheet reports gaps_found; if it won the pick, the phase
// would read 'Executed', not 'Complete'.
fs.writeFileSync(path.join(p1, '03-CORRECTION-VERIFICATION.md'), '---\nstatus: gaps_found\n---\n# Correction worksheet');
fs.writeFileSync(path.join(p1, '03-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 === '03');
assert.ok(phase, 'phase 03 must be present in stats output');
assert.strictEqual(phase.status, 'Complete', 'the canonical 03-VERIFICATION.md must win over the CORRECTION worksheet');
});
test('counts requirements from REQUIREMENTS.md', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'REQUIREMENTS.md'),