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:
@@ -435,6 +435,10 @@ describe('extractCurrentMilestone', () => {
|
||||
expect(result).toContain('Add polish.');
|
||||
// Shipped milestone phases must not bleed in
|
||||
expect(result).not.toContain('Old phase');
|
||||
// The <summary> text is normalized as a `## ` milestone heading so
|
||||
// downstream consumers (e.g. roadmapAnalyze's data.milestones scan) see
|
||||
// the active milestone anchor — not just the body.
|
||||
expect(result).toMatch(/^##\s+v0\.9 Local-First Bus \(active\) — Phase Details/m);
|
||||
});
|
||||
|
||||
// ─── Bug #2641 (CodeRabbit follow-up): quoted YAML version normalization ───
|
||||
@@ -488,6 +492,7 @@ describe('extractCurrentMilestone', () => {
|
||||
|
||||
expect(result).toContain('### Phase 3: Polish');
|
||||
expect(result).toContain('Add polish.');
|
||||
expect(result).toMatch(/^##\s+v0\.9 Local-First Bus/m);
|
||||
});
|
||||
|
||||
// ─── Bug #2422: same-version sub-heading truncation ───────────────────
|
||||
@@ -674,6 +679,48 @@ describe('roadmapAnalyze', () => {
|
||||
|
||||
expect((data1.phases as unknown[]).length).toBe((data2.phases as unknown[]).length);
|
||||
});
|
||||
|
||||
// ─── Bug #2641 (regression): roadmapAnalyze populates milestones array
|
||||
// for <details>-wrapped active milestones via the synthesized `## ` heading. ───
|
||||
it('bug-2641: data.milestones contains the active milestone when wrapped in <details>', async () => {
|
||||
// Without the synthesized heading injected by extractCurrentMilestone's
|
||||
// <details>-aware fallback, the milestone-heading scan at the bottom of
|
||||
// roadmapAnalyze (`/##\s*(.*v(\d+(?:\.\d+)+)[^(\n]*)/gi`) would find
|
||||
// nothing useful inside the body of a <details>-wrapped active milestone
|
||||
// and `data.milestones` would be empty / wrong.
|
||||
const roadmap = `# Roadmap
|
||||
|
||||
## Milestones
|
||||
- 📋 **v0.9 Local-First Bus** — active
|
||||
|
||||
<details>
|
||||
<summary>v0.9 Local-First Bus (active) — Phase Details</summary>
|
||||
|
||||
### Phase 1: Library
|
||||
**Goal:** Build the library.
|
||||
|
||||
### Phase 3: Polish
|
||||
**Goal:** Add polish.
|
||||
</details>
|
||||
`;
|
||||
const state = `---\nmilestone: v0.9\n---\n# State\n`;
|
||||
await writeFile(join(tmpDir, '.planning', 'STATE.md'), state);
|
||||
await writeFile(join(tmpDir, '.planning', 'ROADMAP.md'), roadmap);
|
||||
|
||||
const result = await roadmapAnalyze([], tmpDir);
|
||||
const data = result.data as Record<string, unknown>;
|
||||
const milestones = data.milestones as Array<{ heading: string; version: string }>;
|
||||
|
||||
// Active milestone surfaces with correct version
|
||||
expect(milestones.some(m => m.version === 'v0.9')).toBe(true);
|
||||
expect(milestones.some(m => m.heading.includes('Local-First Bus'))).toBe(true);
|
||||
|
||||
// Phases are also surfaced (the original bug)
|
||||
const phases = data.phases as Array<Record<string, unknown>>;
|
||||
expect(phases.length).toBe(2);
|
||||
expect(phases.some(p => p.number === '1')).toBe(true);
|
||||
expect(phases.some(p => p.number === '3')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── extractPhasesFromSection + extractNextMilestoneSection (#2497) ──────
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user