v0.2.0: Git-native architecture (#1)
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user