feat: link plans to context workspaces
This commit is contained in:
parent
6a54bccbb0
commit
ecca80f584
|
|
@ -44,6 +44,31 @@ describe("plan receipt", () => {
|
||||||
expect(receipt.reasons).toEqual([]);
|
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", () => {
|
it("writes a JSON receipt", () => {
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "plan-receipt-"));
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "plan-receipt-"));
|
||||||
const file = path.join(dir, "receipt.json");
|
const file = path.join(dir, "receipt.json");
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export interface PlanReceiptOptions {
|
||||||
requirements?: string[];
|
requirements?: string[];
|
||||||
questions?: string[];
|
questions?: string[];
|
||||||
phases: PlanPhase[];
|
phases: PlanPhase[];
|
||||||
|
contextWorkspaceReceipt?: string;
|
||||||
source?: string;
|
source?: string;
|
||||||
now?: Date;
|
now?: Date;
|
||||||
}
|
}
|
||||||
|
|
@ -25,6 +26,7 @@ export interface PlanReceipt {
|
||||||
requirements: string[];
|
requirements: string[];
|
||||||
questions: string[];
|
questions: string[];
|
||||||
phases: PlanPhase[];
|
phases: PlanPhase[];
|
||||||
|
contextWorkspaceReceipt?: string;
|
||||||
decision: "ready" | "needs_clarification" | "blocked";
|
decision: "ready" | "needs_clarification" | "blocked";
|
||||||
reasons: string[];
|
reasons: string[];
|
||||||
}
|
}
|
||||||
|
|
@ -37,6 +39,8 @@ export function createPlanReceipt(opts: PlanReceiptOptions): PlanReceipt {
|
||||||
if (opts.phases.length === 0) reasons.push("missing phases");
|
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.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");
|
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 {
|
return {
|
||||||
schema: "fable.plan.receipt.v1",
|
schema: "fable.plan.receipt.v1",
|
||||||
|
|
@ -46,6 +50,7 @@ export function createPlanReceipt(opts: PlanReceiptOptions): PlanReceipt {
|
||||||
requirements,
|
requirements,
|
||||||
questions,
|
questions,
|
||||||
phases: opts.phases,
|
phases: opts.phases,
|
||||||
|
contextWorkspaceReceipt: opts.contextWorkspaceReceipt,
|
||||||
decision: reasons.length ? "blocked" : questions.length ? "needs_clarification" : "ready",
|
decision: reasons.length ? "blocked" : questions.length ? "needs_clarification" : "ready",
|
||||||
reasons,
|
reasons,
|
||||||
};
|
};
|
||||||
|
|
@ -56,3 +61,15 @@ export function writePlanReceipt(file: string, receipt: PlanReceipt): string {
|
||||||
fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
||||||
return file;
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue