fix(#416): return null when active milestone has no archive; tighten **Milestone:** regex (#419)

* fix(#416): return null when active milestone has no archive (no fall-through to prior milestone's archive)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#416): handle bold-formatted Milestone: field in archive dir resolver

The STATE.md regex in getActiveMilestoneArchiveDir failed to extract the
version from **Milestone:** vX.Y format (bold wraps the label+colon).
The old pattern captured '**' instead of the version, causing the
milestone→archive lookup to produce a false candidate path, then return
null (post-fix behavior) instead of falling through to the version-sort
fallback — breaking the #3164 consistency scanner tests.

Fix: extend the regex to skip optional trailing '**' after the colon so
both 'milestone: vX.Y' and '**Milestone:** vX.Y' parse correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-27 22:57:42 -04:00
committed by GitHub
parent ec0a32ea07
commit e4aca8dab0
3 changed files with 236 additions and 5 deletions

View File

@@ -441,24 +441,38 @@ function forEachArchivedPhaseToken(planBase, onPhase) {
}
function getActiveMilestoneArchiveDir(planBase) {
// Knuth invariant: the resolver answers exactly one question —
// "what archive directory holds the active milestone's phases?"
// Answer space: <dir> | null.
//
// When STATE.md is present and names a milestone:
// - If a matching milestones/<vX.Y>-phases/ directory exists → return it.
// - If no matching directory exists → return null. The active milestone
// has no archive yet (phases live in flat phases/). Falling through to
// an older milestone's archive is wrong and produces W007 false positives.
//
// The version-sort fallback to the newest archive fires ONLY when STATE.md is
// absent or unparseable — not when it cleanly names an unarchived milestone.
const archiveDirs = listMilestoneArchiveDirs(planBase);
if (archiveDirs.length === 0) return null;
// Prefer STATE.md milestone when it maps to an on-disk archive dir.
// STATE.md present and parseable: match wins, no-match returns null.
try {
const statePath = path.join(planBase, 'STATE.md');
if (fs.existsSync(statePath)) {
const state = fs.readFileSync(statePath, 'utf-8');
const m = state.match(/^\s*(?:\*\*)?milestone(?:\*\*)?:\s*([^\s\r\n#]+).*$/mi);
const m = state.match(/^\s*(?:\*\*)?milestone(?:\*\*)?:\s*\*{0,2}\s*([^\s*\r\n#][^\s\r\n#]*)/mi);
if (m && m[1]) {
const milestone = m[1].trim();
const candidate = path.join(planBase, 'milestones', `${milestone}-phases`);
if (archiveDirs.includes(candidate)) return candidate;
// Return the matching archive, or null if the active milestone has no archive yet.
return archiveDirs.includes(candidate) ? candidate : null;
}
}
} catch { /* intentionally empty */ }
} catch { /* intentionally empty — fall through to version-sort below */ }
// Fallback when STATE.md is absent/stale: highest (most recent) archive by version-ish name.
// Fallback: STATE.md is absent or unparseable — highest (most recent) archive by version-ish name.
return archiveDirs[archiveDirs.length - 1];
}