fix(#2641): recognize <details><summary> as active-milestone anchor
`extractCurrentMilestone` only matched markdown headings (## v0.9, ### v0.9)
to find the active milestone slice. Projects that wrap their active
milestone's phase details inside `<details><summary>vX.Y …</summary>`
(a common GitHub-friendly collapse pattern, e.g. FAMP) fell through to
`stripShippedMilestones`, which strips ALL `<details>` blocks indiscriminately.
Net effect: `roadmapGetPhase` returned `{found:false}` for phases that ARE in
the active ROADMAP. The `init.phase-op` safety guard at `init.ts:133`
('drop archived disk match when phase is in current ROADMAP') depends on
`roadmapPhase.found`, so it didn't fire. `init.phase-op` then returned a
`phase_dir` pointing at an ARCHIVED milestone's same-numbered phase —
silently routing downstream workflows (e.g. /gsd-discuss-phase) into
completed phases.
Fix: when no markdown heading matches the active version, try matching
`<details\b[^>]*><summary>...vX.Y...</summary>`. Returns the inner content
of the matching block. Purely additive — `stripShippedMilestones` behavior
and its tests are unchanged.
The `\b[^>]*>` form tolerates attributes like `<details open>` or
`<details class="...">` (GitHub commonly emits `<details open>` for
default-expanded sections). Lazy `[\s\S]*?` matches up to the first
`</details>`; nested `<details>` inside the active milestone are not
expected and would mis-anchor (acceptable; falls through to the existing
`stripShippedMilestones` path with no regression vs. today's behavior).
Closes #2641. Distinct from the closed #2642 which bundled three orthogonal
changes (parser fix + checkbox-scan fix + STATE.md counting auth) into one
PR; this PR addresses only the parser anchoring bug, leaving
`stripShippedMilestones`, `roadmapAnalyze`, and `initMilestoneOp` untouched.
Tests added (3, all in `roadmap.test.ts`):
- `bug-2641: finds active milestone wrapped in <details><summary>vX.Y …</summary>`
- `bug-2641: finds active milestone in <details open><summary>vX.Y …</summary>`
- `bug-2641: returns found:true for phase inside <details>-wrapped active milestone` (end-to-end via `roadmapGetPhase`)
All existing `roadmap.test.ts` tests pass (39/39). Real-world repro
verified against an FAMP-style ROADMAP: before the fix,
`gsd-sdk query roadmap.get-phase 3` returned `{found:false}` despite the
phase being at line 113 of the active ROADMAP; after the fix, it returns
the correct phase metadata, and `init.phase-op 3` no longer returns the
v0.8 archived `phase_dir`.
This commit is contained in:
@@ -181,7 +181,40 @@ export async function extractCurrentMilestone(content: string, projectDir: strin
|
||||
);
|
||||
const sectionMatch = content.match(sectionPattern);
|
||||
|
||||
if (!sectionMatch || sectionMatch.index === undefined) return stripShippedMilestones(content);
|
||||
if (!sectionMatch || sectionMatch.index === undefined) {
|
||||
// Fallback: <details><summary> matching the active version (issue #2641).
|
||||
//
|
||||
// Many projects (GitHub-friendly collapse pattern) wrap the active
|
||||
// milestone's phase details inside a collapsible block whose <summary>
|
||||
// names the version, e.g.:
|
||||
//
|
||||
// <details>
|
||||
// <summary>v0.9 Local-First Bus (active) — Phase Details</summary>
|
||||
// ### Phase 1: ...
|
||||
// </details>
|
||||
//
|
||||
// The markdown-heading lookup above misses this because <summary> is HTML,
|
||||
// not a heading. Without this fallback, control falls through to
|
||||
// stripShippedMilestones() which removes ALL <details> blocks
|
||||
// indiscriminately — including the active milestone's — causing
|
||||
// roadmapGetPhase() to return {found:false} for phases that ARE in the
|
||||
// active ROADMAP. The init.phase-op safety guard then misfires and can
|
||||
// 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).
|
||||
const detailsPattern = new RegExp(
|
||||
`<details\\b[^>]*>\\s*<summary>[^<]*${escapedVersion}[^<]*</summary>([\\s\\S]*?)</details>`,
|
||||
'i'
|
||||
);
|
||||
const detailsMatch = content.match(detailsPattern);
|
||||
if (detailsMatch) return detailsMatch[1];
|
||||
return stripShippedMilestones(content);
|
||||
}
|
||||
|
||||
const sectionStart = sectionMatch.index;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user