fix(#2641): inject normalized ## heading from <details><summary> capture

Address CodeRabbit review on PR #3046: the prior commit returned only the
body inside <details>...</details>, which fixed the `roadmapGetPhase`
miss but left `roadmapAnalyze`'s downstream `data.milestones` scan
(`/##\s*(.*v(\d+(?:\.\d+)+)[^(\n]*)/gi` at the bottom of roadmap.ts)
without an active-milestone anchor in the returned slice.

Now capture the <summary> text and prepend it as a synthesized `##`
heading on the returned slice. This makes both `data.phases` (the
original bug) AND `data.milestones` (the downstream consumer) surface
the active milestone correctly for <details>-wrapped ROADMAPs.

Also widened the inner tag to `<summary\b[^>]*>` for symmetry with the
outer `<details\b[^>]*>` — both now tolerate attributes.

Verified end-to-end against FAMP's v0.9 ROADMAP:
- Before this commit (after PR #3046 base):
    milestones: [{heading: '# Phase 1: ... (v0.5.2 atomic bump)', version: 'v0.5.2'}]
- After this commit:
    milestones: [{heading: 'v0.9 Local-First Bus', version: 'v0.9'},
                 {heading: '# Phase 1: ... (v0.5.2 ...)', version: 'v0.5.2'}]

(The v0.5.2 entry is pre-existing noise from the loose `##\s*` regex
matching the `### Phase 1: famp-bus (v0.5.2 atomic bump)` body heading;
unrelated to this fix and out of scope for this PR.)

Tests:
- Updated the two `<details><summary>` tests to assert the synthesized
  `## v0.9 Local-First Bus` heading is present on the returned slice.
- Added a 4th regression test (`roadmapAnalyze`) confirming
  `data.milestones` now contains the active milestone for
  <details>-wrapped ROADMAPs.
- All 40 roadmap.test.ts tests pass.
This commit is contained in:
Ben Lamm
2026-05-02 23:17:36 -04:00
committed by Tom Boucher
parent ba6a3efc3e
commit c8239f67f8
2 changed files with 65 additions and 7 deletions

View File

@@ -208,17 +208,28 @@ export async function extractCurrentMilestone(content: string, projectDir: strin
// route phase lookups into archived milestones.
//
// <details\b[^>]*> tolerates attributes like <details open> and
// <details class="...">. The lazy [\s\S]*? terminates on the first
// </details>; nested <details> inside the active milestone are not
// expected and would mis-anchor (acceptable; FAMP-style ROADMAPs do not
// nest, and any project that does will fall through to the existing
// stripShippedMilestones path with no regression vs. today's behavior).
// <details class="...">. <summary\b[^>]*> tolerates the same on the
// <summary> tag. The lazy [\s\S]*? terminates on the first </details>;
// nested <details> inside the active milestone are not expected and would
// mis-anchor (acceptable; FAMP-style ROADMAPs do not nest, and any project
// that does will fall through to the existing stripShippedMilestones path
// with no regression vs. today's behavior).
//
// We capture the <summary> text and prepend it as a normalized `##`
// milestone heading on the returned slice. This keeps downstream consumers
// that scan for `##` milestone headings (e.g. roadmapAnalyze's
// data.milestones loop later in this file) producing a meaningful entry
// for the active milestone instead of seeing an unanchored body.
const detailsPattern = new RegExp(
`<details\\b[^>]*>\\s*<summary>[^<]*${escapedVersion}[^<]*</summary>([\\s\\S]*?)</details>`,
`<details\\b[^>]*>\\s*<summary\\b[^>]*>([^<]*${escapedVersion}[^<]*)</summary>([\\s\\S]*?)</details>`,
'i'
);
const detailsMatch = content.match(detailsPattern);
if (detailsMatch) return detailsMatch[1];
if (detailsMatch) {
const summary = detailsMatch[1].trim();
const body = detailsMatch[2];
return `## ${summary}\n${body}`;
}
return stripShippedMilestones(content);
}