diff --git a/.changeset/lucky-newts-squeak.md b/.changeset/lucky-newts-squeak.md new file mode 100644 index 000000000..8ece2c69a --- /dev/null +++ b/.changeset/lucky-newts-squeak.md @@ -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) diff --git a/src/roadmap.cts b/src/roadmap.cts index 60f0b0ccf..bcbd1c5e0 100644 --- a/src/roadmap.cts +++ b/src/roadmap.cts @@ -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(); let checklistMatch: RegExpExecArray | null; while ((checklistMatch = checklistPattern.exec(content)) !== null) {