import * as fs from "node:fs"; import * as path from "node:path"; import { writeFile, readFile, ensureDir, fileExists } from "../utils/file.js"; import { PipelineStage } from "../types/pipeline.js"; const CI_DIR = ".ci"; export interface ProjectMd { name: string; coreValue: string; requirements: { validated: string[]; active: string[]; outOfScope: string[]; }; constraints: string[]; context: string; keyDecisions: Array<{ decision: string; rationale: string; outcome: string; }>; } export interface RoadmapMd { overview: string; phases: Array<{ number: number; name: string; description: string; status: "not_started" | "in_progress" | "complete" | "deferred"; dependsOn: number[]; requirements: string[]; successCriteria: string[]; }>; } export interface RequirementsMd { v1: Array<{ category: string; items: Array<{ id: string; description: string }>; }>; v2: Array<{ category: string; items: Array<{ id: string; description: string }>; }>; outOfScope: Array<{ feature: string; reason: string }>; traceability: Array<{ requirement: string; phase: number; status: "pending" | "in_progress" | "complete" | "blocked"; }>; } export interface ArchitectureMd { overview: string; components: Array<{ name: string; description: string; boundaries: string; dependsOn: string[]; }>; dataFlow: string; buildOrder: string[]; } export class CiFiles { private projectPath: string; constructor(projectPath: string) { this.projectPath = projectPath; } private get ciDir(): string { return path.join(this.projectPath, CI_DIR); } ensureCIDir(): void { ensureDir(this.ciDir); } isInitialized(): boolean { return fileExists(path.join(this.ciDir, "config.json")); } readProjectMd(): ProjectMd | null { const content = readFile(path.join(this.ciDir, "PROJECT.md")); if (!content) return null; return this.parseProjectMd(content); } writeProjectMd(project: ProjectMd, reason: string): void { this.ensureCIDir(); const lines: string[] = [ `# ${project.name}`, "", "## What This Is", "", project.coreValue, "", "## Requirements", "", "### Validated", "", ...project.requirements.validated.map((r) => `- ✓ ${r}`), "", "### Active", "", ...project.requirements.active.map((r) => `- [ ] ${r}`), "", "### Out of Scope", "", ...project.requirements.outOfScope.map((r) => `- ${r}`), "", "## Context", "", project.context, "", "## Constraints", "", ...project.constraints.map((c) => `- ${c}`), "", "## Key Decisions", "", "| Decision | Rationale | Outcome |", "|----------|-----------|---------|", ...project.keyDecisions.map( (d) => `| ${d.decision} | ${d.rationale} | ${d.outcome} |` ), "", ]; writeFile(path.join(this.ciDir, "PROJECT.md"), lines.join("\n")); } readRoadmapMd(): RoadmapMd | null { const content = readFile(path.join(this.ciDir, "ROADMAP.md")); if (!content) return null; return this.parseRoadmapMd(content); } writeRoadmapMd(roadmap: RoadmapMd): void { this.ensureCIDir(); const lines: string[] = [ "# Roadmap", "", "## Overview", "", roadmap.overview, "", "## Phases", "", ...roadmap.phases.map( (p) => `- [${p.status === "complete" ? "x" : " "}] **Phase ${p.number}: ${p.name}** - ${p.description}` ), "", "## Phase Details", "", ]; for (const phase of roadmap.phases) { lines.push(`### Phase ${phase.number}: ${phase.name}`); lines.push(`**Goal**: ${phase.description}`); lines.push(`**Depends on**: ${phase.dependsOn.length > 0 ? phase.dependsOn.map((d) => `Phase ${d}`).join(", ") : "Nothing"}`); lines.push(`**Requirements**: ${phase.requirements.length > 0 ? phase.requirements.join(", ") : "None"}`); lines.push("**Success Criteria**:"); for (const sc of phase.successCriteria) { lines.push(`1. ${sc}`); } lines.push(`**Status**: ${phase.status}`); lines.push(""); } writeFile(path.join(this.ciDir, "ROADMAP.md"), lines.join("\n")); } readRequirementsMd(): RequirementsMd | null { const content = readFile(path.join(this.ciDir, "REQUIREMENTS.md")); if (!content) return null; return this.parseRequirementsMd(content); } writeRequirementsMd(requirements: RequirementsMd): void { this.ensureCIDir(); const lines: string[] = [ "# Requirements", "", "## v1 Requirements", "", ]; for (const cat of requirements.v1) { lines.push(`### ${cat.category}`); lines.push(""); for (const item of cat.items) { lines.push(`- [ ] **${item.id}**: ${item.description}`); } lines.push(""); } lines.push("## v2 Requirements"); lines.push(""); for (const cat of requirements.v2) { lines.push(`### ${cat.category}`); lines.push(""); for (const item of cat.items) { lines.push(`- **${item.id}**: ${item.description}`); } lines.push(""); } lines.push("## Out of Scope"); lines.push(""); lines.push("| Feature | Reason |"); lines.push("|---------|--------|"); for (const item of requirements.outOfScope) { lines.push(`| ${item.feature} | ${item.reason} |`); } lines.push(""); lines.push("## Traceability"); lines.push(""); lines.push("| Requirement | Phase | Status |"); lines.push("|-------------|-------|--------|"); for (const t of requirements.traceability) { lines.push(`| ${t.requirement} | Phase ${t.phase} | ${t.status} |`); } writeFile(path.join(this.ciDir, "REQUIREMENTS.md"), lines.join("\n")); } readArchitectureMd(): ArchitectureMd | null { const content = readFile(path.join(this.ciDir, "ARCHITECTURE.md")); if (!content) return null; return this.parseArchitectureMd(content); } writeArchitectureMd(architecture: ArchitectureMd): void { this.ensureCIDir(); const lines: string[] = [ "# Architecture", "", "## Overview", "", architecture.overview, "", "## Components", "", ]; for (const comp of architecture.components) { lines.push(`### ${comp.name}`); lines.push(`- **Description**: ${comp.description}`); lines.push(`- **Boundaries**: ${comp.boundaries}`); lines.push(`- **Depends on**: ${comp.dependsOn.length > 0 ? comp.dependsOn.join(", ") : "None"}`); lines.push(""); } lines.push("## Data Flow"); lines.push(""); lines.push(architecture.dataFlow); lines.push(""); lines.push("## Build Order"); lines.push(""); for (const step of architecture.buildOrder) { lines.push(`1. ${step}`); } writeFile(path.join(this.ciDir, "ARCHITECTURE.md"), lines.join("\n")); } updateRequirementStatus(reqId: string, status: "pending" | "in_progress" | "complete" | "blocked"): void { const reqs = this.readRequirementsMd(); if (!reqs) return; for (const t of reqs.traceability) { if (t.requirement === reqId) { t.status = status; } } this.writeRequirementsMd(reqs); } updatePhaseStatus(phaseNumber: number, status: "not_started" | "in_progress" | "complete" | "deferred"): void { const roadmap = this.readRoadmapMd(); if (!roadmap) return; for (const phase of roadmap.phases) { if (phase.number === phaseNumber) { phase.status = status; } } this.writeRoadmapMd(roadmap); } private parseProjectMd(content: string): ProjectMd { return { name: this.extractSection(content, "# ") || "Unknown", coreValue: this.extractSection(content, "## What This Is") || "", requirements: { validated: this.extractListItems(content, "### Validated"), active: this.extractListItems(content, "### Active"), outOfScope: this.extractListItems(content, "### Out of Scope"), }, constraints: this.extractListItems(content, "## Constraints"), context: this.extractSection(content, "## Context") || "", keyDecisions: [], }; } private parseRoadmapMd(content: string): RoadmapMd { return { overview: this.extractSection(content, "## Overview") || "", phases: [], }; } private parseRequirementsMd(content: string): RequirementsMd { return { v1: [], v2: [], outOfScope: [], traceability: [], }; } private parseArchitectureMd(content: string): ArchitectureMd { return { overview: this.extractSection(content, "## Overview") || "", components: [], dataFlow: this.extractSection(content, "## Data Flow") || "", buildOrder: [], }; } private extractSection(content: string, header: string): string | null { const headerIdx = content.indexOf(header); if (headerIdx < 0) return null; const startIdx = headerIdx + header.length; const nextHeaderIdx = content.indexOf("\n## ", startIdx); const endIdx = nextHeaderIdx >= 0 ? nextHeaderIdx : content.length; return content.slice(startIdx, endIdx).trim(); } private extractListItems(content: string, header: string): string[] { const section = this.extractSection(content, header); if (!section) return []; return section .split("\n") .filter((line) => line.trim().startsWith("-")) .map((line) => line.replace(/^-\s*(?:\[[ x]\]\s*)?(?:✓\s*)?/, "").trim()) .filter(Boolean); } }