fix(roadmap): handle numbered, bracketed, and indented prose H3 headers in slice parser (#3063)
The prose slice header fallback parser failed to extract slices when LLMs generated common formatting variants: numbered prefixes (### 1. S01), parenthetical numbering (### (1) S01), bracketed IDs (### [S01]), or indented headings ( ### S01). This caused auto-mode to permanently block with "No slice eligible" when the plan-milestone prompt produced these formats inside a ## Slices section. Broadened the parseProseSliceHeaders regex to accept optional leading whitespace, numeric prefixes, parenthetical numbering, and square brackets around slice IDs. Closes #2567 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8b680179e2
commit
e78dca41d4
2 changed files with 102 additions and 4 deletions
|
|
@ -219,13 +219,14 @@ export function parseRoadmapSlices(content: string): RoadmapSliceEntry[] {
|
|||
function parseProseSliceHeaders(content: string): RoadmapSliceEntry[] {
|
||||
const slices: RoadmapSliceEntry[] = [];
|
||||
// Match H1-H4 headers containing S<digits> with optional "Slice" prefix, bold markers,
|
||||
// and optional checkmark completion marker before the slice ID.
|
||||
// numeric prefixes (e.g., "1.", "(1)"), bracketed IDs (e.g., "[S01]"),
|
||||
// optional checkmark completion marker, and optional leading indentation.
|
||||
// Separator after the ID is flexible: colon, dash, em/en dash, dot, or just whitespace.
|
||||
const headerPattern = /^#{1,4}\s+\*{0,2}(?:\u2713\s+)?(?:Slice\s+)?(S\d+)\*{0,2}[:\s.\u2014\u2013-]*\s*(.+)/gm;
|
||||
const headerPattern = /^\s*#{1,4}\s+\*{0,2}(?:\u2713\s+)?(?:\d+[.)]\s+)?(?:\(\d+\)\s+)?(?:Slice\s+)?\[?(S\d+)\]?\*{0,2}[:\s.\u2014\u2013-]*\s*(.+)/gm;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
// Check for checkmark before the slice ID (e.g., "## checkmark S01: Title")
|
||||
const prefixCheckPattern = /^#{1,4}\s+\*{0,2}\u2713\s+/;
|
||||
const prefixCheckPattern = /^\s*#{1,4}\s+\*{0,2}\u2713\s+/;
|
||||
|
||||
while ((match = headerPattern.exec(content)) !== null) {
|
||||
const id = match[1]!;
|
||||
|
|
@ -251,7 +252,7 @@ function parseProseSliceHeaders(content: string): RoadmapSliceEntry[] {
|
|||
|
||||
// Try to extract depends from prose: "Depends on: S01" or "**Depends on:** S01, S02"
|
||||
const afterHeader = content.slice(match.index + match[0].length);
|
||||
const nextHeader = afterHeader.search(/^#{1,4}\s/m);
|
||||
const nextHeader = afterHeader.search(/^\s*#{1,4}\s/m);
|
||||
const section = nextHeader !== -1 ? afterHeader.slice(0, nextHeader) : afterHeader.slice(0, 500);
|
||||
|
||||
const depsMatch = section.match(/\*{0,2}Depends\s+on:?\*{0,2}\s*(.+)/i);
|
||||
|
|
|
|||
|
|
@ -296,3 +296,100 @@ Do the second thing.
|
|||
assert.equal(slices[0]?.id, "S01");
|
||||
assert.equal(slices[1]?.id, "S02");
|
||||
});
|
||||
|
||||
// ── Regression tests for #2567 ─────────────────────────────────────────────
|
||||
// Prose H3 parser fails on common LLM-generated patterns: numbered prefixes,
|
||||
// parenthetical numbering, bracketed IDs, and indented headings.
|
||||
|
||||
test("parseRoadmapSlices: numbered H3 headers under ## Slices (#2567)", () => {
|
||||
const numberedContent = `# M002: My Milestone
|
||||
|
||||
**Vision:** Ship the product.
|
||||
|
||||
## Slices
|
||||
|
||||
### 1. S01: Setup Environment
|
||||
Set up the dev environment and tooling.
|
||||
|
||||
### 2. S02: Build Core
|
||||
Implement the core logic.
|
||||
**Depends on:** S01
|
||||
|
||||
### 3. S03: Polish UI
|
||||
Final polish and theming.
|
||||
**Depends on:** S01, S02
|
||||
`;
|
||||
const slices = parseRoadmapSlices(numberedContent);
|
||||
assert.equal(slices.length, 3, "should parse 3 slices from numbered H3 headers");
|
||||
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: parenthetical-numbered H3 headers (#2567)", () => {
|
||||
const parenContent = `# M002: Milestone
|
||||
|
||||
**Vision:** Ship.
|
||||
|
||||
## Slices
|
||||
|
||||
### (1) S01: Setup
|
||||
Setup work.
|
||||
|
||||
### (2) S02: Build
|
||||
Build work.
|
||||
**Depends on:** S01
|
||||
`;
|
||||
const slices = parseRoadmapSlices(parenContent);
|
||||
assert.equal(slices.length, 2, "should parse slices with parenthetical numbering");
|
||||
assert.equal(slices[0]?.id, "S01");
|
||||
assert.equal(slices[0]?.title, "Setup");
|
||||
assert.equal(slices[1]?.id, "S02");
|
||||
assert.deepEqual(slices[1]?.depends, ["S01"]);
|
||||
});
|
||||
|
||||
test("parseRoadmapSlices: bracketed slice IDs in H3 headers (#2567)", () => {
|
||||
const bracketContent = `# M002: Milestone
|
||||
|
||||
**Vision:** Ship.
|
||||
|
||||
## Slices
|
||||
|
||||
### [S01] Setup Environment
|
||||
Setup work.
|
||||
|
||||
### [S02] Build Core
|
||||
Build work.
|
||||
**Depends on:** S01
|
||||
`;
|
||||
const slices = parseRoadmapSlices(bracketContent);
|
||||
assert.equal(slices.length, 2, "should parse slices with bracketed IDs");
|
||||
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"]);
|
||||
});
|
||||
|
||||
test("parseRoadmapSlices: indented H3 headers under ## Slices (#2567)", () => {
|
||||
const indentedContent = `# M002: Milestone
|
||||
|
||||
**Vision:** Ship.
|
||||
|
||||
## Slices
|
||||
|
||||
### S01: Setup
|
||||
Setup work.
|
||||
|
||||
### S02: Build
|
||||
Build work.
|
||||
`;
|
||||
const slices = parseRoadmapSlices(indentedContent);
|
||||
assert.equal(slices.length, 2, "should parse slices from indented H3 headers");
|
||||
assert.equal(slices[0]?.id, "S01");
|
||||
assert.equal(slices[0]?.title, "Setup");
|
||||
assert.equal(slices[1]?.id, "S02");
|
||||
assert.equal(slices[1]?.title, "Build");
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue