fix(#1588): ignore fenced and backlog roadmap phases (#1845)

* fix(#1588): ignore fenced and backlog roadmap phases

* chore(#1588): add changeset fragment

* chore(#1588): fix changeset body

* test(#1588): fold init regression into init suite
This commit is contained in:
Tom Boucher
2026-06-30 20:43:25 -04:00
committed by GitHub
parent 8161fcc667
commit 9e32462dd8
6 changed files with 159 additions and 46 deletions

View File

@@ -209,19 +209,22 @@ interface RoadmapPhaseResult {
}
function findRoadmapPhaseInContent(content: string, phaseNum: unknown, phaseSource?: string): RoadmapPhaseResult | null {
const phasePattern = new RegExp(
`#{2,4}\\s*(?:\\[[^\\]]+\\]\\s*)?Phase\\s+${phaseSource ?? phaseMarkdownRegexSource(phaseNum)}:\\s*([^\\n]+)`,
const headingPattern = new RegExp(
`^(?:\\[[^\\]]+\\]\\s*)?Phase\\s+${phaseSource ?? phaseMarkdownRegexSource(phaseNum)}:\\s*(.+)$`,
'i'
);
const headerMatch = content.match(phasePattern);
const headings = tokenizeHeadings(content);
const headingIndex = headings.findIndex((heading) => headingPattern.test(heading.text));
if (headingIndex === -1) return null;
const heading = headings[headingIndex];
const headerMatch = heading.text.match(headingPattern);
if (!headerMatch) return null;
const phaseName = headerMatch[1].trim();
const headerIndex = headerMatch.index!;
const restOfContent = content.slice(headerIndex);
const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+(?:\[[^\]]+\]\s*)?Phase\s+[\w]/i);
const sectionEnd = nextHeaderMatch ? headerIndex + nextHeaderMatch.index! : content.length;
const section = content.slice(headerIndex, sectionEnd).trim();
const nextHeading = headings.slice(headingIndex + 1).find((candidate) => candidate.level <= heading.level);
const sectionEnd = nextHeading ? nextHeading.offset : content.length;
const section = content.slice(heading.offset, sectionEnd).trim();
const goalMatch = section.match(/\*\*Goal(?:\*\*:|\*?\*?:\*\*)\s*([^\n]+)/i);
const goal = goalMatch ? goalMatch[1].trim() : null;
@@ -254,6 +257,8 @@ function roadmapPhaseLookupSources(phaseNum: unknown): string[] {
function getRoadmapPhaseInternal(cwd: string, phaseNum: unknown): RoadmapPhaseResult | null {
if (!phaseNum) return null;
const normalizedPhase = stripProjectCodePrefix(phaseNum);
if (/^999(?:\.|$)/.test(normalizedPhase)) return null;
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
if (!fs.existsSync(roadmapPath)) return null;

View File

@@ -14,13 +14,14 @@ import ioMod = require('./io.cjs');
const { output, error } = ioMod;
// eslint-disable-next-line @typescript-eslint/no-require-imports
import phaseIdMod = require('./phase-id.cjs');
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, phaseMarkdownRegexSourceExact, phaseTokenMatches } = phaseIdMod;
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, phaseMarkdownRegexSourceExact, phaseTokenMatches, stripProjectCodePrefix } = phaseIdMod;
// eslint-disable-next-line @typescript-eslint/no-require-imports
import phaseLocatorMod = require('./phase-locator.cjs');
const { findPhaseInternal } = phaseLocatorMod;
// eslint-disable-next-line @typescript-eslint/no-require-imports
import roadmapParserModule = require('./roadmap-parser.cjs');
const { stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone } = roadmapParserModule;
import { tokenizeHeadings } from './markdown-sectionizer.cjs';
import { platformWriteSync } from './shell-command-projection.cjs';
// eslint-disable-next-line @typescript-eslint/no-require-imports
import planningWorkspace = require('./planning-workspace.cjs');
@@ -117,12 +118,13 @@ function countPhasePlansAndSummaries(phaseDir: string): PhasePlansAndSummaries {
* checklist-only match), or null if the phase is not present at all.
*/
function searchPhaseInContent(content: string, escapedPhase: string, phaseNum: string): PhaseSearchResult | null {
// Match "## Phase X:", "### Phase X:", or "#### Phase X:" with optional name
const phasePattern = new RegExp(
`#{2,4}\\s*(?:\\[[^\\]]+\\]\\s*)?Phase\\s+${escapedPhase}:\\s*([^\\n]+)`,
const headingPattern = new RegExp(
`^(?:\\[[^\\]]+\\]\\s*)?Phase\\s+${escapedPhase}:\\s*(.+)$`,
'i'
);
const headerMatch = content.match(phasePattern);
const headings = tokenizeHeadings(content);
const headingIndex = headings.findIndex((heading) => headingPattern.test(heading.text));
const headerMatch = headingIndex === -1 ? null : headings[headingIndex].text.match(headingPattern);
if (!headerMatch) {
// Fallback: check if phase exists in summary list but missing detail section
@@ -146,15 +148,13 @@ function searchPhaseInContent(content: string, escapedPhase: string, phaseNum: s
}
const phaseName = headerMatch[1].trim();
const headerIndex = headerMatch.index!;
const headerIndex = headings[headingIndex].offset;
// Find the end of this section (next ## or ### phase header, or end of file).
// Also matches bracket-prefixed headings like ### [GSD] Phase 2-01:.
const restOfContent = content.slice(headerIndex);
const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+(?:\[[^\]]+\]\s*)?Phase\s+[\w][\w.-]*/i);
const sectionEnd = nextHeaderMatch
? headerIndex + nextHeaderMatch.index!
: content.length;
const currentHeading = headings[headingIndex];
const nextHeading = headings
.slice(headingIndex + 1)
.find((candidate) => candidate.level <= currentHeading.level);
const sectionEnd = nextHeading ? nextHeading.offset : content.length;
const section = content.slice(headerIndex, sectionEnd).trim();
@@ -200,6 +200,7 @@ function searchPhaseInContent(content: string, escapedPhase: string, phaseNum: s
* phase resolution as `roadmap.get-phase` — not a milestone-only subset.
*/
function getRoadmapPhaseWithFallback(cwd: string, phaseNum: string): string | null {
if (/^999(?:\.|$)/.test(stripProjectCodePrefix(phaseNum))) return null;
const roadmapPath = planningPaths(cwd).roadmap;
if (!fs.existsSync(roadmapPath)) return null;
@@ -228,6 +229,10 @@ function getRoadmapPhaseWithFallback(cwd: string, phaseNum: string): string | nu
// ─── cmdRoadmapGetPhase ───────────────────────────────────────────────────────
function cmdRoadmapGetPhase(cwd: string, phaseNum: string, raw: boolean): void {
if (/^999(?:\.|$)/.test(stripProjectCodePrefix(phaseNum))) {
output({ found: false, phase_number: phaseNum }, raw, '');
return;
}
const roadmapPath = planningPaths(cwd).roadmap;
if (!fs.existsSync(roadmapPath)) {