feat: add plan receipts

This commit is contained in:
artale 2026-06-28 11:49:15 +02:00
parent 3c4684f4c5
commit c0d2511279
3 changed files with 116 additions and 0 deletions

View File

@ -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";

View File

@ -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");
});
});

View File

@ -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;
}