fix(#1445,#1446): exclude 999.x backlog from milestone totals; allow total_phases downward correction (#1490)

* fix(#1445,#1446): exclude 999.x backlog phases from milestone totals; allow total_phases downward correction

#1445: deriveProgressFromRoadmap (phase-lifecycle.cts), the roadmapPhaseCount
loop (state.cts), and getMilestonePhaseFilter (roadmap-parser.cts) all now
skip phase tokens matching /^999\b/ — consistent with the existing init.cts
filter. 999.x backlog dirs are consequently excluded from phaseDirs too.

#1446: shouldPreserveExistingProgress (state-document.cts) no longer includes
total_phases in its ratchet check. total_phases always takes the freshly
derived value; only completed_phases, total_plans, and completed_plans retain
ratchet behaviour.

Regression tests added for both bugs.

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

* chore: add changeset for #1445/#1446 progress-backlog-exclusion-and-ratchet

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

* fix(#1445,#1446): rename test files to fix-NNN convention; fix changeset pr: null

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-06-20 13:37:32 -04:00
committed by GitHub
parent 33ccf5f89d
commit 4719b36d41
7 changed files with 354 additions and 8 deletions

View File

@@ -54,10 +54,16 @@ export function deriveProgressFromRoadmap(roadmapContent: string): RoadmapProgre
);
if (progressTableMatch) {
const tableText = progressTableMatch[0];
// Count data rows (rows starting with pipe then a phase number)
const dataRowPattern = /^\|\s*\d+/gm;
const dataRows = tableText.match(dataRowPattern);
totalPhases = dataRows ? dataRows.length : null;
// Count data rows (rows starting with pipe then a phase number),
// excluding 999.x backlog phases. Mirrors init.cts /^999(?:\.|$)/ filter.
const dataRowPattern = /^\|\s*(\d+[^|]*)\|/gm;
let dataRowCount = 0;
let drm: RegExpExecArray | null;
while ((drm = dataRowPattern.exec(tableText)) !== null) {
if (/^999\b/.test(drm[1].trim())) continue;
dataRowCount++;
}
totalPhases = dataRowCount > 0 ? dataRowCount : null;
}
// Sum plan counts from M/N columns in progress table

View File

@@ -408,7 +408,8 @@ function getMilestonePhaseFilter(cwd: string, versionOverride?: string | null, p
for (const h of tokenizeHeadings(roadmap)) {
if (h.level < 2 || h.level > 4) continue;
const pm = phaseHeadingPattern.exec(h.text);
if (pm) milestonePhaseNums.add(pm[1]);
// Exclude 999.x backlog phases from milestone phase set. Mirrors init.cts filter.
if (pm && !/^999\b/.test(pm[1])) milestonePhaseNums.add(pm[1]);
}
} catch { /* intentionally empty */ }

View File

@@ -171,8 +171,10 @@ export function shouldPreserveExistingProgress(existingProgress: unknown, derive
return false;
const existing = existingProgress as ProgressRecord;
const derived = derivedProgress as ProgressRecord;
return (existingProgressExceedsDerived(existing, derived, 'total_phases') ||
existingProgressExceedsDerived(existing, derived, 'completed_phases') ||
// total_phases is intentionally excluded from the ratchet: it must always
// take the freshly derived value so it can correct downward (#1446).
// Only completed_phases, total_plans, and completed_plans keep ratchet behaviour.
return (existingProgressExceedsDerived(existing, derived, 'completed_phases') ||
existingProgressExceedsDerived(existing, derived, 'total_plans') ||
existingProgressExceedsDerived(existing, derived, 'completed_plans'));
}

View File

@@ -1402,7 +1402,8 @@ function buildStateFrontmatter(bodyContent: string, cwd: string | undefined): Re
// Only count tokens that contain at least one digit — excludes
// pure-word section headings (Overview, Details) while keeping
// numeric phases (01, 05.1) and project-code IDs (PROJ-42).
if (/\d/.test(m[1])) roadmapPhaseCount++;
// Also exclude 999.x backlog phases. Mirrors init.cts filter.
if (/\d/.test(m[1]) && !/^999\b/.test(m[1])) roadmapPhaseCount++;
}
}
} catch { /* fall through: phaseDirs.length used as sole count */ }