fix(#3036): accept non-numeric-leading phase ids in roadmap.analyze (#3117)

* fix(#3036): accept non-numeric-leading phase ids in roadmap.analyze

roadmap.analyze's phase-heading discovery and checklist discovery regexes
required a digit-first id (\d+...). A project using letter-prefixed ids
(e.g. B7, P0.3-2) got phase_count: 0, current_phase: null, next_phase:
null — even though get-phase and execute-phase resolved the same ids fine.

Widened all three regex sites (phasePattern, checklistPattern, nextHeader
section boundary) to accept an optional leading letter prefix
([A-Za-z]?\d+...). Existing numeric-leading ids are unchanged.

* chore(#3036): backfill changeset PR number 3117

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-06 08:07:24 -04:00
committed by GitHub
parent e7ce60fd21
commit 077028584f
2 changed files with 16 additions and 4 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3117
---
**`roadmap.analyze` now discovers non-numeric-leading phase ids** — the phase-heading and checklist discovery regexes required a digit-first id (e.g. `07`), so a project using letter-prefixed ids (e.g. `B7`) got `phase_count: 0` even though `get-phase`/`execute-phase` resolved the same ids fine. The regexes now accept an optional leading letter prefix. (#3036)

View File

@@ -315,7 +315,11 @@ function cmdRoadmapAnalyze(cwd: string, raw: boolean): void {
// Extract all phase headings: ## Phase N: Name or ### Phase N: Name
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
// phase-id-owner: uses the [.-] (dot-or-dash) separator variant, not the canonical dot-only token; a swap to PHASE_NUMBER_TOKEN_SOURCE would drop hyphenated phase-id matches.
const phasePattern = /#{2,4}\s*(?:\[[^\]]{1,200}\]\s*)?Phase\s+(\d+[A-Z]?(?:[.-]\d+)*)(?:\s*\([^)\n]{0,200}\))?\s*:\s*([^\n]+)/gi;
// #3036: widen the id capture to accept non-numeric-leading ids (e.g. B7, P0.3-2)
// that get-phase/execute-phase already resolve. An optional leading letter prefix
// ([A-Za-z]?) covers letter-prefixed ids without breaking numeric-leading ones.
// phase-id-owner: uses the [.-] (dot-or-dash) separator variant, not the canonical dot-only token; a swap to PHASE_NUMBER_TOKEN_SOURCE would drop hyphenated phase-id matches.
const phasePattern = /#{2,4}\s*(?:\[[^\]]{1,200}\]\s*)?Phase\s+([A-Za-z]?\d+[A-Z]?(?:[.-]\d+)*)(?:\s*\([^)\n]{0,200}\))?\s*:\s*([^\n]+)/gi;
const phases: Array<{
number: string;
name: string;
@@ -359,8 +363,9 @@ function cmdRoadmapAnalyze(cwd: string, raw: boolean): void {
const sectionStart = match.index;
const restOfContent = content.slice(sectionStart);
// #3691: `\d` → `\d[\d.]*` so decimal phase headings (e.g. `### Phase 02.3:`) are
// recognised as section boundaries.
const nextHeader = restOfContent.match(/\n#{2,4}\s+(?:\[[^\]]{1,200}\]\s*)?Phase\s+\d[\d.-]*/i);
// recognised as section boundaries. #3036: `[A-Za-z]?\d` so non-numeric-leading ids
// (e.g. B7) are also recognised.
const nextHeader = restOfContent.match(/\n#{2,4}\s+(?:\[[^\]]{1,200}\]\s*)?Phase\s+[A-Za-z]?\d[\d.-]*/i);
const sectionEnd = nextHeader ? sectionStart + nextHeader.index! : content.length;
const section = content.slice(sectionStart, sectionEnd);
@@ -459,7 +464,9 @@ function cmdRoadmapAnalyze(cwd: string, raw: boolean): void {
// IDs (e.g. `1-01`) match the detail-heading scanner above; otherwise they truncate
// at the dash (`1-01` -> `1`) and every such phase reports a phantom missing detail.
// phase-id-owner: uses the [.-] (dot-or-dash) separator variant, not the canonical dot-only token; a swap to PHASE_NUMBER_TOKEN_SOURCE would drop hyphenated phase-id matches.
const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+[A-Z]?(?:[.-]\d+)*)/gi;
// #3036: widen to accept non-numeric-leading ids (same widening as the detail-heading pattern above).
// phase-id-owner: uses the [.-] (dot-or-dash) separator variant, not the canonical dot-only token; a swap to PHASE_NUMBER_TOKEN_SOURCE would drop hyphenated phase-id matches.
const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+([A-Za-z]?\d+[A-Z]?(?:[.-]\d+)*)/gi;
const checklistPhases = new Set<string>();
let checklistMatch: RegExpExecArray | null;
while ((checklistMatch = checklistPattern.exec(content)) !== null) {