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.');
|
expect(result).toContain('Add polish.');
|
||||||
// Shipped milestone phases must not bleed in
|
// Shipped milestone phases must not bleed in
|
||||||
expect(result).not.toContain('Old phase');
|
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 ───
|
// ─── Bug #2641 (CodeRabbit follow-up): quoted YAML version normalization ───
|
||||||
@@ -488,6 +492,7 @@ describe('extractCurrentMilestone', () => {
|
|||||||
|
|
||||||
expect(result).toContain('### Phase 3: Polish');
|
expect(result).toContain('### Phase 3: Polish');
|
||||||
expect(result).toContain('Add polish.');
|
expect(result).toContain('Add polish.');
|
||||||
|
expect(result).toMatch(/^##\s+v0\.9 Local-First Bus/m);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Bug #2422: same-version sub-heading truncation ───────────────────
|
// ─── 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);
|
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) ──────
|
// ─── extractPhasesFromSection + extractNextMilestoneSection (#2497) ──────
|
||||||
|
|||||||
@@ -208,17 +208,28 @@ export async function extractCurrentMilestone(content: string, projectDir: strin
|
|||||||
// route phase lookups into archived milestones.
|
// route phase lookups into archived milestones.
|
||||||
//
|
//
|
||||||
// <details\b[^>]*> tolerates attributes like <details open> and
|
// <details\b[^>]*> tolerates attributes like <details open> and
|
||||||
// <details class="...">. The lazy [\s\S]*? terminates on the first
|
// <details class="...">. <summary\b[^>]*> tolerates the same on the
|
||||||
// </details>; nested <details> inside the active milestone are not
|
// <summary> tag. The lazy [\s\S]*? terminates on the first </details>;
|
||||||
// expected and would mis-anchor (acceptable; FAMP-style ROADMAPs do not
|
// nested <details> inside the active milestone are not expected and would
|
||||||
// nest, and any project that does will fall through to the existing
|
// mis-anchor (acceptable; FAMP-style ROADMAPs do not nest, and any project
|
||||||
// stripShippedMilestones path with no regression vs. today's behavior).
|
// 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(
|
const detailsPattern = new RegExp(
|
||||||
`<details\\b[^>]*>\\s*<summary>[^<]*${escapedVersion}[^<]*</summary>([\\s\\S]*?)</details>`,
|
`<details\\b[^>]*>\\s*<summary\\b[^>]*>([^<]*${escapedVersion}[^<]*)</summary>([\\s\\S]*?)</details>`,
|
||||||
'i'
|
'i'
|
||||||
);
|
);
|
||||||
const detailsMatch = content.match(detailsPattern);
|
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);
|
return stripShippedMilestones(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user