fix(#3642): stop the single-section total_phases leak into an absent milestone (#3727)

* test(#3642): failing-first single-section leak rows

* fix(#3642): gate the unbounded total on any-milestone-section, not >=2

* test(#3642): rewrite the 3185 wrapper row to the withhold contract

* docs(#3642): glossary amendment for the >=1 sibling; changeset

* chore(#3642): backfill changeset pr number

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-20 22:10:52 -04:00
committed by GitHub
parent 072b97d276
commit 95f7c14413
5 changed files with 213 additions and 31 deletions

View File

@@ -354,7 +354,7 @@ const MILESTONE_HEADING_SIGNAL_PATTERN = /v\d+\.\d+|✅|📋|🚧|\bMilestone\b/
* template or the #3204/#1761/#3185 reports exercises that shape; it is
* recorded here rather than hidden.
*/
function hasMilestoneSectioning(content: string): boolean {
function countMilestoneHeadings(content: string): number {
const isPhaseHeading = (text: string): boolean => /^Phase\s+\S/i.test(text);
let milestoneHeadingCount = 0;
for (const heading of tokenizeHeadings(content)) {
@@ -362,9 +362,31 @@ function hasMilestoneSectioning(content: string): boolean {
if (isPhaseHeading(heading.text)) continue;
if (!MILESTONE_HEADING_SIGNAL_PATTERN.test(heading.text)) continue;
milestoneHeadingCount++;
if (milestoneHeadingCount >= 2) return true;
}
return false;
return milestoneHeadingCount;
}
function hasMilestoneSectioning(content: string): boolean {
// The >=2 short-circuit the inline walk used to have is gone — a ROADMAP's
// heading count is small and tokenizeHeadings materializes the full token
// array regardless, so the shared walk pays nothing for it.
return countMilestoneHeadings(content) >= 2;
}
/**
* #3642: the >=1 sibling of `hasMilestoneSectioning`. The >=2 predicate
* answers SIBLING-conflation ("could two sections' phases mix") and is
* unchanged; but `buildStateFrontmatter`'s unbounded branch asks a question
* >=2 under-answers: "is there ANY milestone section whose phases a
* whole-document count would attribute to a milestone that matches no
* heading?" With exactly ONE section and an asserted milestone absent from
* the ROADMAP, >=2 said "flat" and the single section's phases leaked into
* the asserted milestone's total_phases (silent clobber of the stored
* value). Same walk, same vocabulary, threshold 1 — exported for that
* consumer only; every other consumer keeps the >=2 semantics.
*/
function hasAnyMilestoneSection(content: string): boolean {
return countMilestoneHeadings(content) >= 1;
}
/**
@@ -1694,6 +1716,8 @@ export = {
sliceMilestoneWindow,
hasVersionedMilestones,
hasMilestoneSectioning,
// #3642: the >=1 sibling buildStateFrontmatter's unbounded branch consumes.
hasAnyMilestoneSection,
// #1956: sole owner of the #2012 decoy-avoidance scope for the
// `drift-guard phase-status` CLI seam.
findRoadmapProgressTable,