diff --git a/src/fable5/index.ts b/src/fable5/index.ts index fe38da9..47eb4a4 100644 --- a/src/fable5/index.ts +++ b/src/fable5/index.ts @@ -93,6 +93,9 @@ export type { RedTeamHarnessDecision, RedTeamHarnessReceipt, RedTeamHarnessRecei export { createModelAccessReceipt, writeModelAccessReceipt } from "./model-access-receipt.js"; export type { ModelAccessProbe, ModelAccessReceipt, ModelAccessReceiptOptions, ModelAccessStatus, ModelAccessSurface } from "./model-access-receipt.js"; +export { createPlanReceipt, writePlanReceipt } from "./plan-receipt.js"; +export type { PlanPhase, PlanReceipt, PlanReceiptOptions } from "./plan-receipt.js"; + export { auditGoalCompletion, writeGoalAuditReceipt } from "./goal-audit.js"; export type { GoalAuditCriterion, GoalAuditReceipt } from "./goal-audit.js"; diff --git a/src/fable5/plan-receipt.test.ts b/src/fable5/plan-receipt.test.ts new file mode 100644 index 0000000..8719a9b --- /dev/null +++ b/src/fable5/plan-receipt.test.ts @@ -0,0 +1,55 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { describe, expect, it } from "vitest"; +import { createPlanReceipt, writePlanReceipt } from "./plan-receipt.js"; + +const phase = { + name: "Implement", + goal: "Add the smallest useful receipt schema", + acceptanceCriteria: ["schema is typed", "receipt is writable"], + verification: ["npm run -s test -- src/fable5/plan-receipt.test.ts"], +}; + +describe("plan receipt", () => { + it("marks complete structured plans ready", () => { + const receipt = createPlanReceipt({ + task: "Add plan receipts", + requirements: ["phases", "acceptance criteria", "verification"], + phases: [phase], + now: new Date("2026-06-28T00:00:00.000Z"), + }); + + expect(receipt.schema).toBe("fable.plan.receipt.v1"); + expect(receipt.decision).toBe("ready"); + expect(receipt.source).toContain("youtu.be/DzbqeO_diOQ"); + expect(receipt.reasons).toEqual([]); + }); + + it("requires acceptance criteria and verification per phase", () => { + const receipt = createPlanReceipt({ + task: "vague task", + phases: [{ name: "Phase 1", goal: "Do thing", acceptanceCriteria: [], verification: [] }], + }); + + expect(receipt.decision).toBe("blocked"); + expect(receipt.reasons).toContain("each phase needs acceptance criteria"); + expect(receipt.reasons).toContain("each phase needs verification steps"); + }); + + it("separates clarifying questions from blocked plans", () => { + const receipt = createPlanReceipt({ task: "Add feature", questions: ["Which API?"], phases: [phase] }); + + expect(receipt.decision).toBe("needs_clarification"); + expect(receipt.reasons).toEqual([]); + }); + + it("writes a JSON receipt", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "plan-receipt-")); + const file = path.join(dir, "receipt.json"); + + writePlanReceipt(file, createPlanReceipt({ task: "x", phases: [phase] })); + + expect(fs.readFileSync(file, "utf-8")).toContain("fable.plan.receipt.v1"); + }); +}); diff --git a/src/fable5/plan-receipt.ts b/src/fable5/plan-receipt.ts new file mode 100644 index 0000000..bb12b54 --- /dev/null +++ b/src/fable5/plan-receipt.ts @@ -0,0 +1,58 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; + +export interface PlanPhase { + name: string; + goal: string; + acceptanceCriteria: string[]; + verification: string[]; +} + +export interface PlanReceiptOptions { + task: string; + requirements?: string[]; + questions?: string[]; + phases: PlanPhase[]; + source?: string; + now?: Date; +} + +export interface PlanReceipt { + schema: "fable.plan.receipt.v1"; + createdAt: string; + source: string; + task: string; + requirements: string[]; + questions: string[]; + phases: PlanPhase[]; + decision: "ready" | "needs_clarification" | "blocked"; + reasons: string[]; +} + +export function createPlanReceipt(opts: PlanReceiptOptions): PlanReceipt { + const requirements = opts.requirements ?? []; + const questions = opts.questions ?? []; + const reasons: string[] = []; + if (!opts.task.trim()) reasons.push("missing task"); + if (opts.phases.length === 0) reasons.push("missing phases"); + if (opts.phases.some((phase) => phase.acceptanceCriteria.length === 0)) reasons.push("each phase needs acceptance criteria"); + if (opts.phases.some((phase) => phase.verification.length === 0)) reasons.push("each phase needs verification steps"); + + return { + schema: "fable.plan.receipt.v1", + createdAt: (opts.now ?? new Date()).toISOString(), + source: opts.source ?? "plan skill inspiration: https://youtu.be/DzbqeO_diOQ", + task: opts.task, + requirements, + questions, + phases: opts.phases, + decision: reasons.length ? "blocked" : questions.length ? "needs_clarification" : "ready", + reasons, + }; +} + +export function writePlanReceipt(file: string, receipt: PlanReceipt): string { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`); + return file; +}