* fix+test(#2828): total_phases uses roadmap count on flat unmilestoned roadmap The read-path disk-scan cache fell back to phaseDirs.length (1) when milestoneBounded was false, even though roadmapPhaseCount (6) was correct for a flat roadmap (no sibling milestones to conflate). Use roadmapPhaseCount as the floor when > 0, matching the write-path (cmdStateSync) which already did this. The milestoneBounded flag still flows to milestoneUnbounded for the percent-skip (#1761 guard preserved). Regression test asserts state-sync writes progress.total_phases:6 for a flat 6-phase roadmap + 1 phase dir. * chore(#2828): changeset fragment * test(#2828): add negative-space coverage (Math.max floor mutant + no-roadmap fallback) — review findings The 6-phase test alone couldn't kill a Math.max-dropping mutant (1<6). Add: a 3-phase-dir/2-roadmap-phase case proving Math.max(dirs,count) floor; a no-roadmap case proving phaseDirs.length fallback. * fix(#2828): refine — distinguish flat unmilestoned from milestoned-unbounded (preserve #1761) The first-pass fix (roadmapPhaseCount > 0 always) re-broke #1761: a milestoned- unbounded roadmap (asserted milestone not among existing version headings) conflated sibling milestones (8 = 4+4). Refine with a hasMilestoneSectioning discriminator: ^#{2,3}(?!Phase) detects non-Phase h2/h3 milestone section headings. A FLAT roadmap (only ### Phase headings + a # title) has none → safe to use roadmapPhaseCount; a SECTIONED-but-unbounded roadmap has them → fall back to phaseDirs.length (#1761). Verified both cases locally (flat→6, sectioned-unbounded→1). * test(#2828): remove two fragile negative-space tests (phase-dir scanner internals) The Math.max-floor and no-roadmap tests made assumptions about the phase-dir scanner's internals (which dirs count as 'realized') that didn't hold. The core regression test (6-phase flat → total_phases:6) plus the existing #1761 conflation tests (which the refined fix preserves) provide sufficient coverage. * chore(#2828): backfill changeset PR number 2892 * fix(#2828): replace ReDoS-prone regex in regression test with line-by-line parse CodeQL flagged the nested-quantifier regex (`(?:[ \t]+\w+:.+\r?\n?)*?`) in tests/issue-2828-flat-roadmap-total-phases.test.cjs as a high-severity catastrophic-backtracking risk. Rewrite the STATE.md progress.total_phases extraction as a ReDoS-safe line-by-line block walk. --------- Co-authored-by: Test <test@example.com>
This commit is contained in:
@@ -1705,10 +1705,20 @@ function buildStateFrontmatter(bodyContent: string, cwd: string | undefined): Re
|
||||
);
|
||||
milestoneBounded = versionedHeading.test(roadmapRaw);
|
||||
}
|
||||
// #2828: distinguish a FLAT unmilestoned roadmap (no milestone sectioning
|
||||
// at all — only Phase headings) from a MILESTONED-but-unbounded one
|
||||
// (milestone/version headings exist but the asserted one isn't among them).
|
||||
// On a flat roadmap the whole-doc count is correct (no sibling milestones to
|
||||
// conflate); on a sectioned-but-unbounded one it conflates siblings (#1761),
|
||||
// so fall back to phaseDirs.length.
|
||||
const hasMilestoneSectioning = roadmapRaw !== null
|
||||
&& /^#{2,3}\s+(?!Phase\s+\S)/mi.test(roadmapRaw);
|
||||
const safeToUseRoadmapCount = milestoneBounded
|
||||
|| (roadmapPhaseCount > 0 && !hasMilestoneSectioning);
|
||||
return {
|
||||
totalPhases: (!milestoneBounded || roadmapPhaseCount === 0)
|
||||
? phaseDirs.length
|
||||
: Math.max(phaseDirs.length, roadmapPhaseCount),
|
||||
totalPhases: safeToUseRoadmapCount
|
||||
? Math.max(phaseDirs.length, roadmapPhaseCount)
|
||||
: phaseDirs.length,
|
||||
milestoneBounded,
|
||||
completedPhases: diskCompletedPhases,
|
||||
totalPlans: diskTotalPlans,
|
||||
|
||||
Reference in New Issue
Block a user