From ecca80f5842b7c992c3c7c1f70f09ba16007644a Mon Sep 17 00:00:00 2001 From: artale Date: Sun, 28 Jun 2026 15:42:05 +0200 Subject: [PATCH] feat: link plans to context workspaces --- src/fable5/plan-receipt.test.ts | 25 +++++++++++++++++++++++++ src/fable5/plan-receipt.ts | 17 +++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/fable5/plan-receipt.test.ts b/src/fable5/plan-receipt.test.ts index 8719a9b..c8d1a14 100644 --- a/src/fable5/plan-receipt.test.ts +++ b/src/fable5/plan-receipt.test.ts @@ -44,6 +44,31 @@ describe("plan receipt", () => { expect(receipt.reasons).toEqual([]); }); + + it("can cite a ready context workspace receipt", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "plan-context-")); + const workspace = path.join(dir, "context-workspace.json"); + fs.writeFileSync(workspace, `${JSON.stringify({ schema: "fable.context_workspace.receipt.v1", decision: "ready" })} +`); + + const receipt = createPlanReceipt({ task: "Add feature", contextWorkspaceReceipt: workspace, phases: [phase] }); + + expect(receipt.decision).toBe("ready"); + expect(receipt.contextWorkspaceReceipt).toBe(workspace); + }); + + it("blocks plans that cite blocked context workspaces", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "plan-context-blocked-")); + const workspace = path.join(dir, "context-workspace.json"); + fs.writeFileSync(workspace, `${JSON.stringify({ schema: "fable.context_workspace.receipt.v1", decision: "blocked" })} +`); + + const receipt = createPlanReceipt({ task: "Add feature", contextWorkspaceReceipt: workspace, phases: [phase] }); + + expect(receipt.decision).toBe("blocked"); + expect(receipt.reasons).toContain("context workspace receipt is blocked"); + }); + it("writes a JSON receipt", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "plan-receipt-")); const file = path.join(dir, "receipt.json"); diff --git a/src/fable5/plan-receipt.ts b/src/fable5/plan-receipt.ts index bb12b54..bebae66 100644 --- a/src/fable5/plan-receipt.ts +++ b/src/fable5/plan-receipt.ts @@ -13,6 +13,7 @@ export interface PlanReceiptOptions { requirements?: string[]; questions?: string[]; phases: PlanPhase[]; + contextWorkspaceReceipt?: string; source?: string; now?: Date; } @@ -25,6 +26,7 @@ export interface PlanReceipt { requirements: string[]; questions: string[]; phases: PlanPhase[]; + contextWorkspaceReceipt?: string; decision: "ready" | "needs_clarification" | "blocked"; reasons: string[]; } @@ -37,6 +39,8 @@ export function createPlanReceipt(opts: PlanReceiptOptions): PlanReceipt { 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"); + const workspaceReason = opts.contextWorkspaceReceipt ? invalidContextWorkspaceReceiptReason(opts.contextWorkspaceReceipt) : undefined; + if (workspaceReason) reasons.push(workspaceReason); return { schema: "fable.plan.receipt.v1", @@ -46,6 +50,7 @@ export function createPlanReceipt(opts: PlanReceiptOptions): PlanReceipt { requirements, questions, phases: opts.phases, + contextWorkspaceReceipt: opts.contextWorkspaceReceipt, decision: reasons.length ? "blocked" : questions.length ? "needs_clarification" : "ready", reasons, }; @@ -56,3 +61,15 @@ export function writePlanReceipt(file: string, receipt: PlanReceipt): string { fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`); return file; } + +function invalidContextWorkspaceReceiptReason(file: string): string | undefined { + if (!fs.existsSync(file)) return "context workspace receipt missing"; + try { + const value = JSON.parse(fs.readFileSync(file, "utf-8")) as { schema?: unknown; decision?: unknown }; + if (value.schema !== "fable.context_workspace.receipt.v1") return "context workspace receipt schema invalid"; + if (value.decision === "blocked") return "context workspace receipt is blocked"; + return undefined; + } catch { + return "context workspace receipt JSON invalid"; + } +}