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