* 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:
@@ -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,
|
||||
|
||||
@@ -27,7 +27,8 @@ const {
|
||||
} = phaseIdMod;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import roadmapParserMod = require('./roadmap-parser.cjs');
|
||||
const { getMilestoneInfo, extractCurrentMilestone, isMilestoneBoundedInRoadmap, hasMilestoneSectioning } = roadmapParserMod;
|
||||
// #3642: hasMilestoneSectioning no longer consumed here — its >=2 semantics answered sibling conflation, but this branch asks asserted-vs-section (>=1). It stays exported from roadmap-parser.cjs for its unit pins.
|
||||
const { getMilestoneInfo, extractCurrentMilestone, isMilestoneBoundedInRoadmap, hasAnyMilestoneSection } = roadmapParserMod;
|
||||
import { platformWriteSync, platformReadSync, platformEnsureDir, retryRenameSync, toPosixPath, execGit } from './shell-command-projection.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import planningWorkspace = require('./planning-workspace.cjs');
|
||||
@@ -2233,10 +2234,19 @@ function buildStateFrontmatter(bodyContent: string, cwd: string | undefined, sto
|
||||
// deliberately weaker than isMilestoneBoundedInRoadmap above (no
|
||||
// version-token requirement); see hasMilestoneSectioning's own
|
||||
// doc comment for why that distinction is load-bearing.
|
||||
const roadmapHasMilestoneSectioning = roadmapRaw !== null
|
||||
&& hasMilestoneSectioning(roadmapRaw);
|
||||
// #3642: the flat test uses the >=1 sibling (hasAnyMilestoneSection),
|
||||
// not the >=2 predicate. >=2 under-answers the question this branch
|
||||
// asks: with EXACTLY ONE milestone section and an asserted milestone
|
||||
// absent from the ROADMAP, >=2 read "flat" and the whole-document
|
||||
// count — which IS that single section's phases — was written as the
|
||||
// asserted milestone's total, silently clobbering the stored value.
|
||||
// The >=2 threshold governs SIBLING conflation; asserted-vs-section
|
||||
// needs only one section to go wrong. Zero sections (genuinely flat)
|
||||
// keeps the whole-document count, per #2828.
|
||||
const roadmapHasAnyMilestoneSection = roadmapRaw !== null
|
||||
&& hasAnyMilestoneSection(roadmapRaw);
|
||||
const safeToUseRoadmapCount = milestoneBounded
|
||||
|| (roadmapPhaseCount > 0 && !roadmapHasMilestoneSectioning);
|
||||
|| (roadmapPhaseCount > 0 && !roadmapHasAnyMilestoneSection);
|
||||
// #3354: the milestoned-but-unbounded sibling of the #2828/#3204
|
||||
// shapes. The whole-document roadmapPhaseCount is rightly rejected
|
||||
// above (it would conflate sibling milestones, #1761), but the
|
||||
@@ -2252,10 +2262,10 @@ function buildStateFrontmatter(bodyContent: string, cwd: string | undefined, sto
|
||||
// The degenerate un-sectioned zero-heading case keeps the
|
||||
// phaseDirs.length fallback — with nothing declared anywhere else,
|
||||
// the disk count is the only source and remains correct.
|
||||
const milestonedButUnbounded = !milestoneBounded && roadmapHasMilestoneSectioning;
|
||||
const milestonedButUnbounded = !milestoneBounded && roadmapHasAnyMilestoneSection;
|
||||
if (milestonedButUnbounded) {
|
||||
process.stderr.write(
|
||||
`gsd: warning — milestone '${String(assertedMilestoneVersion ?? '').trim()}' is asserted in STATE.md but matches no ROADMAP heading, and the ROADMAP carries multiple milestone sections; the on-disk phase-directory count would understate the declared total, so progress.total_phases is left at its stored value. (#3354)\n`
|
||||
`gsd: warning — milestone '${String(assertedMilestoneVersion ?? '').trim()}' is asserted in STATE.md but matches no ROADMAP heading, and the ROADMAP carries milestone section(s) — one (#3642) or several (#3354) — none matching it; the whole-document count would attribute a foreign section's phases to this milestone and the on-disk phase-directory count would understate the declared total, so progress.total_phases is left at its stored value. (#3354/#3642)\n`
|
||||
);
|
||||
}
|
||||
// #3573: the roadmap-absent sibling of the #3354 shape. With ROADMAP.md
|
||||
|
||||
Reference in New Issue
Block a user