fix(#3262): guard phase writes against milestone-scope headings (#3446)

* fix(#3262): guard phase writes against milestone-scope headings

* fix(#3262): fill changeset pr with 3446

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-14 01:44:54 -04:00
committed by GitHub
parent 8757080676
commit fd4715f80f
12 changed files with 553 additions and 40 deletions

View File

@@ -26,7 +26,7 @@ const { SCOPE } = planningScopeMod;
type Scope = planningScopeMod.Scope;
// eslint-disable-next-line @typescript-eslint/no-require-imports
import roadmapParserModule = require('./roadmap-parser.cjs');
const { stripShippedMilestones, extractCurrentMilestone, extractCurrentMilestoneScoped, replaceInCurrentMilestone, listMilestoneHeadings } = roadmapParserModule;
const { stripShippedMilestones, extractCurrentMilestone, extractCurrentMilestoneScoped, replaceInCurrentMilestone, listMilestoneHeadings, scanMilestonePhaseIds } = roadmapParserModule;
import { tokenizeHeadings } from './markdown-sectionizer.cjs';
import { updateTableCell } from './markdown-table.cjs';
import { clampPercent } from './phase-lifecycle.cjs';
@@ -622,6 +622,40 @@ function cmdRoadmapAnalyze(cwd: string, raw: boolean): void {
output(result, raw, undefined);
}
// ─── cmdRoadmapMilestoneScope ────────────────────────────────────────────────
/**
* #3262 (write-time milestone-scope guard): read-only probe emitting the
* current milestone window's IDENTITY — its scope classification and the
* phase ids it declares — so the edit-phase workflow can capture it before
* its in-place section write, re-derive it after, and roll back on any
* change. This is the milestone-scope sibling of the workflow's existing
* `depends_on` gate, expressed as a command because the workflow's write is
* assistant-driven free-text surgery, not a code path.
*
* Deliberately NOT `cmdRoadmapAnalyze`: analyze's #3165 recovery re-populates
* `phases` from the shipped-milestone-stripped document when the scoped
* window is suspect, which is right for a human-facing progress report and
* wrong for a before/after equality probe — the refill would mask exactly
* the narrowing this guard exists to detect. This probe reports the RAW
* window (`extractCurrentMilestoneScoped` + `scanMilestonePhaseIds`), no
* fallback, so a narrowed window is always visible as a changed phase set.
*/
function cmdRoadmapMilestoneScope(cwd: string, raw: boolean): void {
const roadmapPath = planningPaths(cwd).roadmap;
if (!fs.existsSync(roadmapPath)) {
output({ error: 'ROADMAP.md not found', scope: SCOPE.UNREADABLE, phases: [], phase_count: 0 }, raw, undefined);
return;
}
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
const { value: window, scope } = extractCurrentMilestoneScoped(rawContent, cwd);
// Document order (Set insertion order) — deterministic for a given document.
const phases = [...scanMilestonePhaseIds(window)];
output({ scope, phases, phase_count: phases.length }, raw, undefined);
}
// ─── cmdRoadmapUpdatePlanProgress ─────────────────────────────────────────────
/**
@@ -1153,6 +1187,7 @@ export = {
cmdRoadmapGetPhase,
getRoadmapPhaseWithFallback,
cmdRoadmapAnalyze,
cmdRoadmapMilestoneScope,
cmdRoadmapUpdatePlanProgress,
cmdRoadmapAnnotateDependencies,
buildPhaseHeadingRegex,