v0.2.0: Git-native architecture (#1)

This commit was merged in pull request #1.
This commit is contained in:
2026-05-29 12:59:45 +00:00
parent 9cf5c000d9
commit 6e637e4af0
50 changed files with 5852 additions and 135 deletions
+38
View File
@@ -0,0 +1,38 @@
import { createClarifyQuestion, ClarifyQuestion } from "../types/clarify.js";
describe("createClarifyQuestion", () => {
it("creates a question with generated id", () => {
const question = createClarifyQuestion({
question: "What database should we use?",
context: "Database choice affects architecture",
default_answer: "PostgreSQL",
rationale: "Strong ecosystem, ACID compliance",
impact: "high",
category: "architecture",
});
expect(question.id).toMatch(/^Q-\d+-[a-z0-9]+$/);
expect(question.question).toBe("What database should we use?");
expect(question.context).toBe("Database choice affects architecture");
expect(question.default_answer).toBe("PostgreSQL");
expect(question.rationale).toBe("Strong ecosystem, ACID compliance");
expect(question.impact).toBe("high");
expect(question.category).toBe("architecture");
expect(question.answered).toBe(false);
});
it("does not require answer field", () => {
const question = createClarifyQuestion({
question: "Test?",
context: "Test context",
default_answer: "Default",
rationale: "Test rationale",
impact: "low",
category: "test",
});
expect(question.answered).toBe(false);
expect(question.answer).toBeUndefined();
expect(question.agent_interpretation).toBeUndefined();
});
});
+116
View File
@@ -0,0 +1,116 @@
import { PipelineStage } from "./pipeline.js";
export type CommitType =
| "feat"
| "fix"
| "test"
| "refactor"
| "docs"
| "chore"
| "perf"
| "wip"
| "decision"
| "compound"
| "escalation"
| "verify"
| "note"
| "todo";
export interface CommitScope {
phase: number;
plan?: string;
task?: string;
isInit: boolean;
isMilestone: boolean;
}
export interface CommitDecision {
id: string;
decision: string;
rationale: string;
confidence: number;
alternatives: string[];
}
export interface CommitEscalation {
id: string;
type: string;
description: string;
resolution: "pending" | "timeout" | "human" | "auto";
}
export interface CommitRequirements {
covered: string[];
partial: string[];
}
export interface CommitCompoundMeta {
category: string;
problem: string;
solution: string;
}
export interface CiMetadata {
phase: number;
milestone: string;
plan?: string;
task?: string;
status: PipelineStage;
decisions?: CommitDecision[];
escalations?: CommitEscalation[];
requirements?: CommitRequirements;
lessons?: string[];
compound?: CommitCompoundMeta;
}
export interface ParsedCiCommit {
hash: string;
type: CommitType;
scope: string;
subject: string;
ci: CiMetadata | null;
body: string;
}
export function parseCommitType(type: string): CommitType {
const valid: CommitType[] = [
"feat", "fix", "test", "refactor", "docs", "chore", "perf",
"wip", "decision", "compound", "escalation", "verify", "note", "todo",
];
return valid.includes(type as CommitType) ? (type as CommitType) : "chore";
}
export function parseCommitScope(scope: string): CommitScope {
if (scope === "init") {
return { phase: 0, isInit: true, isMilestone: false };
}
if (scope === "milestone") {
return { phase: 0, isInit: false, isMilestone: true };
}
const phaseMatch = scope.match(/^P(\d+)/);
const phase = phaseMatch ? parseInt(phaseMatch[1], 10) : 0;
const parts = scope.split("-");
let plan: string | undefined;
let task: string | undefined;
if (parts.length >= 2 && /^\d+$/.test(parts[1])) {
plan = `${String(phase).padStart(2, "0")}-${parts[1]}`;
}
if (parts.length >= 3 && /^\d+$/.test(parts[2])) {
task = `${plan}-${parts[2]}`;
}
return { phase, plan, task, isInit: false, isMilestone: false };
}
export function formatCommitScope(scope: CommitScope): string {
if (scope.isInit) return "init";
if (scope.isMilestone) return "milestone";
const phaseStr = `P${String(scope.phase).padStart(2, "0")}`;
if (scope.task) return `${phaseStr}-${scope.task.split("-").slice(1).join("-")}`;
if (scope.plan) return `${phaseStr}-${scope.plan.split("-").slice(1).join("-")}`;
return phaseStr;
}
+49
View File
@@ -0,0 +1,49 @@
import { CIConfig, DEFAULT_CI_CONFIG, AutonomyLevel, ModelProfile } from "../types/config.js";
describe("CIConfig", () => {
it("DEFAULT_CI_CONFIG has all required fields", () => {
expect(DEFAULT_CI_CONFIG.autonomy.level).toBe("full");
expect(DEFAULT_CI_CONFIG.autonomy.clarify_budget).toBe(10);
expect(DEFAULT_CI_CONFIG.autonomy.decision_confidence_threshold).toBe(0.6);
expect(DEFAULT_CI_CONFIG.autonomy.max_revision_iterations).toBe(3);
expect(DEFAULT_CI_CONFIG.autonomy.max_verification_retries).toBe(2);
expect(DEFAULT_CI_CONFIG.autonomy.escalation_timeout_ms).toBe(300000);
expect(DEFAULT_CI_CONFIG.autonomy.escalation_hooks).toContain("deploy");
expect(DEFAULT_CI_CONFIG.model_profile).toBe("quality");
expect(DEFAULT_CI_CONFIG.parallelization.enabled).toBe(true);
expect(DEFAULT_CI_CONFIG.verification.automated_only).toBe(true);
expect(DEFAULT_CI_CONFIG.security.auto_accept_low_severity).toBe(true);
expect(DEFAULT_CI_CONFIG.git.auto_commit).toBe(true);
expect(DEFAULT_CI_CONFIG.git.auto_push).toBe(false);
});
it("AutonomyLevel accepts all valid levels", () => {
const levels: AutonomyLevel[] = ["full", "supervised", "guided"];
for (const level of levels) {
const config: CIConfig = {
...DEFAULT_CI_CONFIG,
autonomy: { ...DEFAULT_CI_CONFIG.autonomy, level },
};
expect(config.autonomy.level).toBe(level);
}
});
it("ModelProfile accepts all valid profiles", () => {
const profiles: ModelProfile[] = ["quality", "speed", "balanced"];
for (const profile of profiles) {
const config: CIConfig = {
...DEFAULT_CI_CONFIG,
model_profile: profile,
};
expect(config.model_profile).toBe(profile);
}
});
it("escalation_hooks defaults include expected items", () => {
expect(DEFAULT_CI_CONFIG.autonomy.escalation_hooks).toEqual([
"deploy",
"delete_data",
"merge_to_main",
]);
});
});
+42
View File
@@ -0,0 +1,42 @@
import { confidenceToLevel, shouldEscalate, DecisionCategory } from "../types/decisions.js";
describe("confidenceToLevel", () => {
it("returns 'high' for confidence > 0.85", () => {
expect(confidenceToLevel(0.86)).toBe("high");
expect(confidenceToLevel(0.95)).toBe("high");
expect(confidenceToLevel(1.0)).toBe("high");
});
it("returns 'medium' for confidence between 0.60 and 0.85", () => {
expect(confidenceToLevel(0.60)).toBe("medium");
expect(confidenceToLevel(0.70)).toBe("medium");
expect(confidenceToLevel(0.85)).toBe("medium");
});
it("returns 'low' for confidence < 0.60", () => {
expect(confidenceToLevel(0.59)).toBe("low");
expect(confidenceToLevel(0.30)).toBe("low");
expect(confidenceToLevel(0.0)).toBe("low");
});
it("boundary values", () => {
expect(confidenceToLevel(0.85)).toBe("medium");
expect(confidenceToLevel(0.86)).toBe("high");
expect(confidenceToLevel(0.60)).toBe("medium");
expect(confidenceToLevel(0.59)).toBe("low");
});
});
describe("shouldEscalate", () => {
it("returns true when confidence is below threshold", () => {
expect(shouldEscalate(0.5, 0.6)).toBe(true);
expect(shouldEscalate(0.3, 0.6)).toBe(true);
expect(shouldEscalate(0.59, 0.6)).toBe(true);
});
it("returns false when confidence meets or exceeds threshold", () => {
expect(shouldEscalate(0.6, 0.6)).toBe(false);
expect(shouldEscalate(0.7, 0.6)).toBe(false);
expect(shouldEscalate(1.0, 0.6)).toBe(false);
});
});
+21
View File
@@ -0,0 +1,21 @@
import { ESCALATION_TYPES } from "../types/escalation.js";
describe("ESCALATION_TYPES", () => {
it("contains all 5 escalation types", () => {
const types = Object.keys(ESCALATION_TYPES);
expect(types).toHaveLength(5);
expect(types).toContain("irreversible_action");
expect(types).toContain("verification_failure");
expect(types).toContain("low_confidence_decision");
expect(types).toContain("security_escalation");
expect(types).toContain("specification_ambiguity");
});
it("maps types to human-readable labels", () => {
expect(ESCALATION_TYPES.irreversible_action).toBe("Irreversible Action");
expect(ESCALATION_TYPES.verification_failure).toBe("Verification Failure");
expect(ESCALATION_TYPES.low_confidence_decision).toBe("Low Confidence Decision");
expect(ESCALATION_TYPES.security_escalation).toBe("Security Escalation");
expect(ESCALATION_TYPES.specification_ambiguity).toBe("Specification Ambiguity");
});
});
+45
View File
@@ -0,0 +1,45 @@
import { getNextStage, createInitialPipelineState, STAGE_ORDER } from "../types/pipeline.js";
import { confidenceToLevel, shouldEscalate } from "../types/decisions.js";
import { ESCALATION_TYPES } from "../types/escalation.js";
import { parseSpecification } from "../types/specification.js";
import { createClarifyQuestion } from "../types/clarify.js";
import { DEFAULT_CI_CONFIG } from "../types/config.js";
describe("Type exports", () => {
it("pipeline types are importable and functional", () => {
expect(STAGE_ORDER).toHaveLength(7);
expect(getNextStage("specify")).toBe("clarify");
const state = createInitialPipelineState("/tmp/test");
expect(state.current_stage).toBe("specify");
});
it("decision types are importable and functional", () => {
expect(confidenceToLevel(0.9)).toBe("high");
expect(shouldEscalate(0.5, 0.6)).toBe(true);
});
it("escalation types are importable and functional", () => {
expect(Object.keys(ESCALATION_TYPES)).toHaveLength(5);
});
it("specification parser is importable and functional", () => {
const spec = parseSpecification("# Test\n## Objective\nBuild it.", "inline");
expect(spec.title).toBe("Test");
});
it("clarify question factory is importable and functional", () => {
const q = createClarifyQuestion({
question: "Test?",
context: "Test",
default_answer: "Yes",
rationale: "Why not",
impact: "low",
category: "test",
});
expect(q.id).toMatch(/^Q-/);
});
it("config defaults are importable", () => {
expect(DEFAULT_CI_CONFIG.autonomy.level).toBe("full");
});
});
+2 -1
View File
@@ -3,4 +3,5 @@ export * from "./decisions.js";
export * from "./escalation.js";
export * from "./pipeline.js";
export * from "./clarify.js";
export * from "./specification.js";
export * from "./specification.js";
export * from "./commit-meta.js";
+59
View File
@@ -0,0 +1,59 @@
import {
PipelineStage,
PipelineState,
PhaseResult,
STAGE_ORDER,
getNextStage,
createInitialPipelineState,
} from "../types/pipeline.js";
describe("STAGE_ORDER", () => {
it("has 7 stages in correct order", () => {
expect(STAGE_ORDER).toEqual([
"specify",
"clarify",
"research",
"plan",
"execute",
"verify",
"complete",
]);
});
});
describe("getNextStage", () => {
it("returns the next stage in sequence", () => {
expect(getNextStage("specify")).toBe("clarify");
expect(getNextStage("clarify")).toBe("research");
expect(getNextStage("research")).toBe("plan");
expect(getNextStage("plan")).toBe("execute");
expect(getNextStage("execute")).toBe("verify");
expect(getNextStage("verify")).toBe("complete");
});
it("returns null for the last stage", () => {
expect(getNextStage("complete")).toBeNull();
});
it("returns null for unknown stage", () => {
expect(getNextStage("unknown" as PipelineStage)).toBeNull();
});
});
describe("createInitialPipelineState", () => {
it("creates a valid initial state", () => {
const state = createInitialPipelineState("/test/project");
expect(state.project_path).toBe("/test/project");
expect(state.current_stage).toBe("specify");
expect(state.current_phase).toBe(0);
expect(state.specification_loaded).toBe(false);
expect(state.clarify_completed).toBe(false);
expect(state.research_completed).toBe(false);
expect(state.plan_completed).toBe(false);
expect(state.execute_completed).toBe(false);
expect(state.verify_completed).toBe(false);
expect(state.errors).toHaveLength(0);
expect(state.started_at).toBeTruthy();
expect(state.last_updated).toBeTruthy();
});
});
+94
View File
@@ -0,0 +1,94 @@
import { parseSpecification } from "../types/specification.js";
describe("parseSpecification", () => {
it("parses a full specification with all sections", () => {
const content = `# Project: Task API
## Objective
Build a REST API for task management with real-time notifications.
## Requirements
- User authentication (JWT-based)
- CRUD operations for tasks
- Real-time notifications via WebSocket
- PostgreSQL database
## Constraints
- Must use Node.js
- Must be production-ready
- No Docker
## Out of Scope
- Admin dashboard
- Payment integration
`;
const spec = parseSpecification(content, "inline");
expect(spec.title).toBe("Project: Task API");
expect(spec.objective).toContain("REST API");
expect(spec.requirements).toHaveLength(4);
expect(spec.requirements).toContain("User authentication (JWT-based)");
expect(spec.constraints).toHaveLength(3);
expect(spec.constraints).toContain("Must use Node.js");
expect(spec.out_of_scope).toHaveLength(2);
expect(spec.source).toBe("inline");
expect(spec.raw_content).toBe(content);
});
it("handles specification with only objective (no list items)", () => {
const content = `# My Project
## Objective
This is a simple project that does one thing well.
`;
const spec = parseSpecification(content, "file");
expect(spec.title).toBe("My Project");
expect(spec.objective).toBe("This is a simple project that does one thing well.");
expect(spec.requirements).toHaveLength(0);
expect(spec.constraints).toHaveLength(0);
expect(spec.out_of_scope).toHaveLength(0);
expect(spec.source).toBe("file");
});
it("defaults title when no heading", () => {
const content = `## Objective
Just build something.
## Requirements
- Feature A
`;
const spec = parseSpecification(content, "inline");
expect(spec.title).toBe("Untitled Project");
expect(spec.requirements).toContain("Feature A");
});
it("defaults objective from content when no objective section", () => {
const content = `# Test Project
## Requirements
- Feature A
`;
const spec = parseSpecification(content, "inline");
expect(spec.objective.length).toBeGreaterThan(0);
});
it("parses specification from clarify source", () => {
const content = `# Quick Task
## Objective
Do something fast.
`;
const spec = parseSpecification(content, "clarify");
expect(spec.source).toBe("clarify");
expect(spec.title).toBe("Quick Task");
});
it("sets created_at timestamp", () => {
const content = "# Test\n## Objective\nTest.";
const spec = parseSpecification(content, "inline");
expect(spec.created_at).toBeTruthy();
expect(new Date(spec.created_at).getTime()).not.toBeNaN();
});
});