feat: add context workspace receipts
This commit is contained in:
parent
b905b98856
commit
6a54bccbb0
|
|
@ -0,0 +1,57 @@
|
|||
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 { createContextWorkspaceReceipt, writeContextWorkspaceReceipt } from "./context-workspace-receipt.js";
|
||||
|
||||
const stage = {
|
||||
name: "Research",
|
||||
folder: "stages/01_research",
|
||||
job: "Collect source material",
|
||||
inputs: [
|
||||
{ layer: "layer0_identity" as const, path: "CLAUDE.md" },
|
||||
{ layer: "layer1_routing" as const, path: "CONTEXT.md" },
|
||||
{ layer: "layer2_stage" as const, path: "stages/01_research/CONTEXT.md" },
|
||||
{ layer: "layer3_reference" as const, path: "references/style.md" },
|
||||
{ layer: "layer4_working" as const, path: "input/brief.md" },
|
||||
],
|
||||
outputs: ["stages/01_research/output/research.md"],
|
||||
};
|
||||
|
||||
describe("context workspace receipt", () => {
|
||||
it("records ICM-style stage contracts as editable filesystem context", () => {
|
||||
const receipt = createContextWorkspaceReceipt({
|
||||
workspace: "workspace",
|
||||
stages: [stage],
|
||||
now: new Date("2026-06-28T00:00:00.000Z"),
|
||||
});
|
||||
|
||||
expect(receipt.schema).toBe("fable.context_workspace.receipt.v1");
|
||||
expect(receipt.source).toContain("2603.16021");
|
||||
expect(receipt.decision).toBe("ready");
|
||||
expect(receipt.principles).toContain("plain text as interface");
|
||||
expect(receipt.stages[0].inputs.map((input) => input.layer)).toContain("layer3_reference");
|
||||
});
|
||||
|
||||
it("blocks vague stages without explicit inputs and outputs", () => {
|
||||
const receipt = createContextWorkspaceReceipt({
|
||||
workspace: "workspace",
|
||||
stages: [{ name: "x", folder: "", job: "", inputs: [], outputs: [] }],
|
||||
});
|
||||
|
||||
expect(receipt.decision).toBe("blocked");
|
||||
expect(receipt.reasons).toContain("each stage needs a folder");
|
||||
expect(receipt.reasons).toContain("each stage needs one job");
|
||||
expect(receipt.reasons).toContain("each stage needs explicit inputs");
|
||||
expect(receipt.reasons).toContain("each stage needs explicit outputs");
|
||||
});
|
||||
|
||||
it("writes a JSON receipt", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "context-workspace-"));
|
||||
const file = path.join(dir, "receipt.json");
|
||||
|
||||
writeContextWorkspaceReceipt(file, createContextWorkspaceReceipt({ workspace: "workspace", stages: [stage] }));
|
||||
|
||||
expect(fs.readFileSync(file, "utf-8")).toContain("fable.context_workspace.receipt.v1");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
export type ContextLayer = "layer0_identity" | "layer1_routing" | "layer2_stage" | "layer3_reference" | "layer4_working";
|
||||
|
||||
export interface ContextStageContract {
|
||||
name: string;
|
||||
folder: string;
|
||||
job: string;
|
||||
inputs: Array<{ layer: ContextLayer; path: string }>;
|
||||
outputs: string[];
|
||||
}
|
||||
|
||||
export interface ContextWorkspaceReceiptOptions {
|
||||
workspace: string;
|
||||
source?: string;
|
||||
stages: ContextStageContract[];
|
||||
now?: Date;
|
||||
}
|
||||
|
||||
export interface ContextWorkspaceReceipt {
|
||||
schema: "fable.context_workspace.receipt.v1";
|
||||
createdAt: string;
|
||||
source: string;
|
||||
workspace: string;
|
||||
stages: ContextStageContract[];
|
||||
principles: string[];
|
||||
decision: "ready" | "blocked";
|
||||
reasons: string[];
|
||||
}
|
||||
|
||||
export function createContextWorkspaceReceipt(opts: ContextWorkspaceReceiptOptions): ContextWorkspaceReceipt {
|
||||
const reasons: string[] = [];
|
||||
if (!opts.workspace.trim()) reasons.push("missing workspace");
|
||||
if (opts.stages.length === 0) reasons.push("missing stages");
|
||||
if (opts.stages.some((stage) => !stage.folder.trim())) reasons.push("each stage needs a folder");
|
||||
if (opts.stages.some((stage) => !stage.job.trim())) reasons.push("each stage needs one job");
|
||||
if (opts.stages.some((stage) => stage.inputs.length === 0)) reasons.push("each stage needs explicit inputs");
|
||||
if (opts.stages.some((stage) => stage.outputs.length === 0)) reasons.push("each stage needs explicit outputs");
|
||||
|
||||
return {
|
||||
schema: "fable.context_workspace.receipt.v1",
|
||||
createdAt: (opts.now ?? new Date()).toISOString(),
|
||||
source: opts.source ?? "ICM paper: https://arxiv.org/abs/2603.16021",
|
||||
workspace: opts.workspace,
|
||||
stages: opts.stages,
|
||||
principles: ["one stage, one job", "plain text as interface", "layered context loading", "outputs are edit surfaces"],
|
||||
decision: reasons.length ? "blocked" : "ready",
|
||||
reasons,
|
||||
};
|
||||
}
|
||||
|
||||
export function writeContextWorkspaceReceipt(file: string, receipt: ContextWorkspaceReceipt): string {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
||||
return file;
|
||||
}
|
||||
|
|
@ -96,6 +96,9 @@ export type { ModelAccessProbe, ModelAccessReceipt, ModelAccessReceiptOptions, M
|
|||
export { createPlanReceipt, writePlanReceipt } from "./plan-receipt.js";
|
||||
export type { PlanPhase, PlanReceipt, PlanReceiptOptions } from "./plan-receipt.js";
|
||||
|
||||
export { createContextWorkspaceReceipt, writeContextWorkspaceReceipt } from "./context-workspace-receipt.js";
|
||||
export type { ContextLayer, ContextStageContract, ContextWorkspaceReceipt, ContextWorkspaceReceiptOptions } from "./context-workspace-receipt.js";
|
||||
|
||||
export { auditGoalCompletion, writeGoalAuditReceipt } from "./goal-audit.js";
|
||||
export type { GoalAuditCriterion, GoalAuditReceipt } from "./goal-audit.js";
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue