feat(P05): implement parseRequirementsMd and parseArchitectureMd — real content parsing

---ci---
project: ci
phase: 5
milestone: v0.5
status: complete
decisions:
  - id: D-030
    decision: Phase 5 Parser Completeness complete
    rationale: All PARSE requirements covered; 31 suites, 355 tests
    confidence: 0.95
    alternatives: []
requirements:
  covered: [PARSE-01, PARSE-02]
---/ci---
This commit is contained in:
Jon Chery
2026-05-29 16:47:17 +00:00
parent b33431c1a6
commit 3d069319b5
+163 -12
View File
@@ -545,21 +545,172 @@ export class CiFiles {
} }
private parseRequirementsMd(content: string): RequirementsMd { private parseRequirementsMd(content: string): RequirementsMd {
return { const v1: RequirementsMd["v1"] = [];
v1: [], const v2: RequirementsMd["v2"] = [];
v2: [],
outOfScope: [], const v1Section = this.extractSection(content, "## v1 Requirements");
traceability: [], 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<string>();
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 { private parseArchitectureMd(content: string): ArchitectureMd {
return { const overview = this.extractSection(content, "## Overview") || "";
overview: this.extractSection(content, "## Overview") || "",
components: [], const components: ArchitectureMd["components"] = [];
dataFlow: this.extractSection(content, "## Data Flow") || "", const section = content;
buildOrder: [], 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 { private extractSection(content: string, header: string): string | null {