diff --git a/src/core/ci-files.ts b/src/core/ci-files.ts index 70baca6..a8230e1 100644 --- a/src/core/ci-files.ts +++ b/src/core/ci-files.ts @@ -545,21 +545,172 @@ export class CiFiles { } private parseRequirementsMd(content: string): RequirementsMd { - return { - v1: [], - v2: [], - outOfScope: [], - traceability: [], - }; + const v1: RequirementsMd["v1"] = []; + const v2: RequirementsMd["v2"] = []; + + const v1Section = this.extractSection(content, "## v1 Requirements"); + if (v1Section) { + const categoryBlocks = v1Section.split(/\n### /).filter(Boolean); + for (const block of categoryBlocks) { + const lines = block.split("\n"); + const category = lines[0].trim(); + const items: Array<{ id: string; description: string }> = []; + + for (const line of lines.slice(1)) { + const tableMatch = line.match(/^\|\s*([A-Z]+-\d+)\s*\|\s*(.+?)\s*\|/); + if (tableMatch) { + items.push({ id: tableMatch[1], description: tableMatch[2] }); + continue; + } + const listMatch = line.match(/^\s*-?\s*\*?\s*\[?\s*\*?\s*([A-Z]+-\d+)[\]:\s*]*(.+)/); + if (listMatch) { + items.push({ id: listMatch[1], description: listMatch[2].trim() }); + } + } + + if (items.length > 0) { + v1.push({ category, items }); + } + } + } + + const v2Section = this.extractSection(content, "## v2 Requirements"); + if (v2Section) { + const categoryBlocks = v2Section.split(/\n### /).filter(Boolean); + for (const block of categoryBlocks) { + const lines = block.split("\n"); + const category = lines[0].trim(); + const items: Array<{ id: string; description: string }> = []; + + for (const line of lines.slice(1)) { + const tableMatch = line.match(/^\|\s*([A-Z]+-\d+)\s*\|\s*(.+?)\s*\|/); + if (tableMatch) { + items.push({ id: tableMatch[1], description: tableMatch[2] }); + continue; + } + const listMatch = line.match(/^\s*-?\s*\*?\s*\[?\s*\*?\s*([A-Z]+-\d+)[\]:\s*]*(.+)/); + if (listMatch) { + items.push({ id: listMatch[1], description: listMatch[2].trim() }); + } + } + + if (items.length > 0) { + v2.push({ category, items }); + } + } + } + + const outOfScope: RequirementsMd["outOfScope"] = []; + const outSection = this.extractSection(content, "## Out of Scope"); + if (outSection) { + const tableRows = outSection.split("\n").filter((line) => /^\|/.test(line) && !line.includes("---") && !line.includes("Feature")); + for (const row of tableRows) { + const cols = row.split("|").map((c) => c.trim()).filter(Boolean); + if (cols.length >= 2) { + outOfScope.push({ feature: cols[0], reason: cols[1] }); + } + } + if (outOfScope.length === 0) { + const listItems = this.extractListItems(content, "## Out of Scope"); + for (const item of listItems) { + outOfScope.push({ feature: item, reason: "" }); + } + } + } + + const traceability: RequirementsMd["traceability"] = []; + const traceSection = this.extractSection(content, "## Traceability"); + if (traceSection) { + const activeHeader = traceSection.includes("Active Milestone") + ? "## v0.5 Requirements (Active Milestone)" + : content.includes("## v1 Requirements") + ? "## v1 Requirements" + : undefined; + + const tableRows = traceSection.split("\n").filter((line) => /^\|/.test(line) && !line.includes("---") && !line.includes("Requirement") && !line.includes("REQ-ID")); + for (const row of tableRows) { + const cols = row.split("|").map((c) => c.trim()).filter(Boolean); + if (cols.length >= 3) { + const req = cols[0]; + const phaseStr = cols[1]; + const phaseMatch = phaseStr.match(/(\d+)/); + const phase = phaseMatch ? parseInt(phaseMatch[1], 10) : 0; + const statusStr = cols[2].toLowerCase(); + const status = ["pending", "in_progress", "complete", "blocked", "covered"].includes(statusStr) + ? (statusStr === "covered" ? "complete" : statusStr as "pending" | "in_progress" | "complete" | "blocked") + : "pending"; + traceability.push({ requirement: req, phase, status }); + } + } + } + + const allReqIds = new Set(); + for (const cat of [...v1, ...v2]) { + for (const item of cat.items) { + allReqIds.add(item.id); + } + } + for (const t of traceability) { + allReqIds.add(t.requirement); + } + const coveredInTrace = new Set(traceability.filter((t) => t.status === "complete").map((t) => t.requirement)); + for (const reqId of allReqIds) { + if (!coveredInTrace.has(reqId)) { + traceability.push({ requirement: reqId, phase: 0, status: "pending" }); + } + } + + return { v1, v2, outOfScope, traceability }; } private parseArchitectureMd(content: string): ArchitectureMd { - return { - overview: this.extractSection(content, "## Overview") || "", - components: [], - dataFlow: this.extractSection(content, "## Data Flow") || "", - buildOrder: [], - }; + const overview = this.extractSection(content, "## Overview") || ""; + + const components: ArchitectureMd["components"] = []; + const section = content; + const componentRegex = /###\s+(.+)/g; + let compMatch; + + const h3Positions: Array<{ name: string; start: number }> = []; + while ((compMatch = componentRegex.exec(section)) !== null) { + h3Positions.push({ name: compMatch[1].trim(), start: compMatch.index + compMatch[0].length }); + } + + for (let i = 0; i < h3Positions.length; i++) { + const name = h3Positions[i].name; + const start = h3Positions[i].start; + const end = i + 1 < h3Positions.length ? h3Positions[i + 1].start - (content.substring(h3Positions[i + 1].start - 4, h3Positions[i + 1].start) === "### " ? 4 : 0) : content.length; + const block = content.slice(start, end); + + const descMatch = block.match(/[-*]\s*\*?\*?(?:Description|description)\*?\*?\s*[::]\s*(.+)/); + const boundaryMatch = block.match(/[-*]\s*\*?\*?(?:Boundaries|boundaries)\*?\*?\s*[::]\s*(.+)/); + const depsMatch = block.match(/[-*]\s*\*?\*?(?:Depends on|depends on|Dependencies)\*?\*?\s*[::]\s*(.+)/); + + components.push({ + name, + description: descMatch ? descMatch[1].trim() : "", + boundaries: boundaryMatch ? boundaryMatch[1].trim() : "", + dependsOn: depsMatch + ? depsMatch[1].split(",").map((d: string) => d.trim().replace(/\*\*/g, "")).filter(Boolean) + : [], + }); + } + + const dataFlow = this.extractSection(content, "## Data Flow") + || this.extractSection(content, "## Data flow") + || ""; + + const buildOrder: string[] = []; + const buildSection = this.extractSection(content, "## Build Order"); + if (buildSection) { + const listItems = buildSection + .split("\n") + .filter((line) => /^\d+\./.test(line.trim())) + .map((line) => line.trim().replace(/^\d+\.\s*/, "")); + buildOrder.push(...listItems); + } + + return { overview, components, dataFlow, buildOrder }; } private extractSection(content: string, header: string): string | null {