* 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:
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user