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