fix: fall through to prose slice parser when checkbox parser yields empty under ## Slices (#1744)
When the ## Slices section exists but contains H3 prose headers instead of checkboxes, parseRoadmapSlices returned an empty array because the prose fallback was only invoked when the ## Slices heading was entirely absent. Now, when the checkbox parser finds zero slices, it falls through to parseProseSliceHeaders as a second-chance fallback. Also adds a missing_slice_dir diagnostic in doctor.ts when resolveSlicePath returns null, with auto-fix via mkdir. Fixes #1711 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a7ad0caf9f
commit
897237ab0a
4 changed files with 84 additions and 1 deletions
|
|
@ -57,6 +57,7 @@ export type DoctorIssueCode =
|
|||
// GSD state structural checks
|
||||
| "circular_slice_dependency"
|
||||
| "orphaned_slice_directory"
|
||||
| "missing_slice_dir"
|
||||
| "duplicate_task_id"
|
||||
| "task_file_not_in_plan"
|
||||
| "stale_replan_file"
|
||||
|
|
|
|||
|
|
@ -706,7 +706,26 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|||
}
|
||||
|
||||
const slicePath = resolveSlicePath(basePath, milestoneId, slice.id);
|
||||
if (!slicePath) continue;
|
||||
if (!slicePath) {
|
||||
const expectedPath = relSlicePath(basePath, milestoneId, slice.id);
|
||||
issues.push({
|
||||
severity: slice.done ? "warning" : "error",
|
||||
code: "missing_slice_dir",
|
||||
scope: "slice",
|
||||
unitId,
|
||||
message: slice.done
|
||||
? `Missing slice directory for ${unitId} (slice is complete — cosmetic only)`
|
||||
: `Missing slice directory for ${unitId}`,
|
||||
file: expectedPath,
|
||||
fixable: true,
|
||||
});
|
||||
if (fix) {
|
||||
const absoluteSliceDir = join(milestonePath, "slices", slice.id);
|
||||
mkdirSync(absoluteSliceDir, { recursive: true });
|
||||
fixesApplied.push(`created ${absoluteSliceDir}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const tasksDir = resolveTasksDir(basePath, milestoneId, slice.id);
|
||||
if (!tasksDir) {
|
||||
|
|
|
|||
|
|
@ -184,6 +184,14 @@ export function parseRoadmapSlices(content: string): RoadmapSliceEntry[] {
|
|||
}
|
||||
|
||||
if (currentSlice) slices.push(currentSlice);
|
||||
|
||||
// When the ## Slices section exists but the checkbox parser found nothing
|
||||
// (e.g. the LLM used H3 prose headers instead of checkboxes), fall through
|
||||
// to the prose-header parser as a second-chance fallback.
|
||||
if (slices.length === 0) {
|
||||
return parseProseSliceHeaders(content);
|
||||
}
|
||||
|
||||
return slices;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -198,3 +198,58 @@ Not done.
|
|||
assert.equal(slices[0]?.title, "Done Slice");
|
||||
assert.equal(slices[1]?.done, false);
|
||||
});
|
||||
|
||||
// ── Regression tests for #1711 ─────────────────────────────────────────────
|
||||
|
||||
test("parseRoadmapSlices: H3 prose headers under ## Slices section triggers prose fallback (#1711)", () => {
|
||||
const proseUnderSlices = `# M010: My Milestone
|
||||
|
||||
**Vision:** Ship it.
|
||||
|
||||
## Slices
|
||||
|
||||
### S01 — Setup Environment
|
||||
Set up the dev environment and tooling.
|
||||
|
||||
### S02 — Build Core
|
||||
Implement the core logic.
|
||||
**Depends on:** S01
|
||||
|
||||
### S03 — Polish UI
|
||||
Final polish and theming.
|
||||
**Depends on:** S01, S02
|
||||
`;
|
||||
const slices = parseRoadmapSlices(proseUnderSlices);
|
||||
assert.equal(slices.length, 3, "should find 3 slices from H3 prose headers under ## Slices");
|
||||
assert.equal(slices[0]?.id, "S01");
|
||||
assert.equal(slices[0]?.title, "Setup Environment");
|
||||
assert.equal(slices[1]?.id, "S02");
|
||||
assert.deepEqual(slices[1]?.depends, ["S01"]);
|
||||
assert.equal(slices[2]?.id, "S03");
|
||||
assert.deepEqual(slices[2]?.depends, ["S01", "S02"]);
|
||||
});
|
||||
|
||||
test("parseRoadmapSlices: ## Slices with valid checkboxes does NOT invoke prose fallback", () => {
|
||||
const slices = parseRoadmapSlices(content);
|
||||
assert.equal(slices.length, 3);
|
||||
assert.equal(slices[0]?.id, "S01");
|
||||
assert.equal(slices[0]?.done, true);
|
||||
});
|
||||
|
||||
test("parseRoadmapSlices: ## Slices with only non-matching lines returns prose fallback results", () => {
|
||||
const weirdContent = `# M020: Odd
|
||||
|
||||
## Slices
|
||||
Some introductory text that is not a checkbox or a slice header.
|
||||
|
||||
### S01: First Thing
|
||||
Do the first thing.
|
||||
|
||||
### S02: Second Thing
|
||||
Do the second thing.
|
||||
`;
|
||||
const slices = parseRoadmapSlices(weirdContent);
|
||||
assert.equal(slices.length, 2, "should fall through to prose parser");
|
||||
assert.equal(slices[0]?.id, "S01");
|
||||
assert.equal(slices[1]?.id, "S02");
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue