feat: add plan receipts
This commit is contained in:
parent
3c4684f4c5
commit
c0d2511279
|
|
@ -93,6 +93,9 @@ export type { RedTeamHarnessDecision, RedTeamHarnessReceipt, RedTeamHarnessRecei
|
||||||
export { createModelAccessReceipt, writeModelAccessReceipt } from "./model-access-receipt.js";
|
export { createModelAccessReceipt, writeModelAccessReceipt } from "./model-access-receipt.js";
|
||||||
export type { ModelAccessProbe, ModelAccessReceipt, ModelAccessReceiptOptions, ModelAccessStatus, ModelAccessSurface } 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 { auditGoalCompletion, writeGoalAuditReceipt } from "./goal-audit.js";
|
||||||
export type { GoalAuditCriterion, GoalAuditReceipt } from "./goal-audit.js";
|
export type { GoalAuditCriterion, GoalAuditReceipt } from "./goal-audit.js";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue