Add factory reconciliation receipt
This commit is contained in:
parent
284c1bc95c
commit
265c42edbb
|
|
@ -207,6 +207,9 @@ fable-agent plinius godmode "improve explanation quality"
|
||||||
- `factory capabilities`
|
- `factory capabilities`
|
||||||
- `--file <path>`
|
- `--file <path>`
|
||||||
- `--run <id>`
|
- `--run <id>`
|
||||||
|
- `factory reconcile`
|
||||||
|
- `--output <path>`
|
||||||
|
- `--run <id>`
|
||||||
- `factory rsi`
|
- `factory rsi`
|
||||||
- `--host <host>`
|
- `--host <host>`
|
||||||
- `--port <n>`
|
- `--port <n>`
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import { CANONICAL_DEPLOY_ROUTE } from "./factory-routes.js";
|
||||||
|
import { createFactoryReconciliationReceipt, writeFactoryReconciliationReceipt } from "./factory-reconciliation.js";
|
||||||
|
|
||||||
|
const roots: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
function factoryResult(overrides = {}) {
|
||||||
|
return {
|
||||||
|
factory: { status: "ok", containers: 25 },
|
||||||
|
deploy: { status: "ok", service: "git-proxy-deploy" },
|
||||||
|
factoryAvailable: true,
|
||||||
|
deployAvailable: true,
|
||||||
|
errors: [],
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("factory reconciliation receipt", () => {
|
||||||
|
it("captures compact live evidence for the canonical 8099 deploy route", () => {
|
||||||
|
const receipt = createFactoryReconciliationReceipt(factoryResult(), { now: new Date("2026-06-27T00:00:00.000Z"), sourceCommand: "test command" });
|
||||||
|
|
||||||
|
expect(receipt).toMatchObject({
|
||||||
|
schema: "fable.factory.reconciliation.v1",
|
||||||
|
createdAt: "2026-06-27T00:00:00.000Z",
|
||||||
|
sourceCommand: "test command",
|
||||||
|
containerCount: 25,
|
||||||
|
deployRoute: { url: CANONICAL_DEPLOY_ROUTE, status: "ok", name: "git-proxy-deploy" },
|
||||||
|
decision: "ready",
|
||||||
|
reasons: [],
|
||||||
|
});
|
||||||
|
expect(receipt.deployRoute.url).toContain(":8099/deploy");
|
||||||
|
expect(receipt.deployRoute.url).not.toContain("8098");
|
||||||
|
expect(receipt.deployRoute.url).not.toContain("deploy-webhook");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails closed on stale or missing live factory evidence", () => {
|
||||||
|
const receipt = createFactoryReconciliationReceipt(factoryResult({ factory: { status: "stale" } }));
|
||||||
|
|
||||||
|
expect(receipt.decision).toBe("blocked");
|
||||||
|
expect(receipt.containerCount).toBe("unknown");
|
||||||
|
expect(receipt.reasons).toContain("factory status stale");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("writes the receipt to the requested path", async () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "fable-factory-reconcile-"));
|
||||||
|
roots.push(root);
|
||||||
|
const out = path.join(root, "receipt.json");
|
||||||
|
|
||||||
|
await writeFactoryReconciliationReceipt(out, { check: async () => factoryResult(), now: new Date("2026-06-27T00:00:00.000Z") });
|
||||||
|
|
||||||
|
expect(JSON.parse(fs.readFileSync(out, "utf-8"))).toMatchObject({ schema: "fable.factory.reconciliation.v1", decision: "ready" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { CANONICAL_DEPLOY_ROUTE } from "./factory-routes.js";
|
||||||
|
import { checkFactory, factoryGateReady, type FactoryCheckResult } from "./factory-status.js";
|
||||||
|
|
||||||
|
export interface FactoryReconciliationReceipt {
|
||||||
|
schema: "fable.factory.reconciliation.v1";
|
||||||
|
createdAt: string;
|
||||||
|
sourceCommand: string;
|
||||||
|
containerCount: number | "unknown";
|
||||||
|
deployRoute: { url: string; status: string; name: string };
|
||||||
|
decision: "ready" | "blocked";
|
||||||
|
reasons: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createFactoryReconciliationReceipt(result: FactoryCheckResult, opts: { now?: Date; sourceCommand?: string } = {}): FactoryReconciliationReceipt {
|
||||||
|
const reasons = reconciliationReasons(result);
|
||||||
|
return {
|
||||||
|
schema: "fable.factory.reconciliation.v1",
|
||||||
|
createdAt: (opts.now ?? new Date()).toISOString(),
|
||||||
|
sourceCommand: opts.sourceCommand ?? "fable-agent factory reconcile",
|
||||||
|
containerCount: result.factory.containers ?? "unknown",
|
||||||
|
deployRoute: {
|
||||||
|
url: CANONICAL_DEPLOY_ROUTE,
|
||||||
|
status: result.deploy.status ?? "unknown",
|
||||||
|
name: result.deploy.service ?? "unknown",
|
||||||
|
},
|
||||||
|
decision: reasons.length === 0 ? "ready" : "blocked",
|
||||||
|
reasons,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeFactoryReconciliationReceipt(file: string, opts: { now?: Date; sourceCommand?: string; check?: () => Promise<FactoryCheckResult> } = {}): Promise<FactoryReconciliationReceipt> {
|
||||||
|
const receipt = createFactoryReconciliationReceipt(await (opts.check ?? checkFactory)(), opts);
|
||||||
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||||
|
fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
||||||
|
return receipt;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconciliationReasons(result: FactoryCheckResult): string[] {
|
||||||
|
const reasons = [...result.errors];
|
||||||
|
if (!result.factoryAvailable) reasons.push("factory-status unavailable");
|
||||||
|
if (!result.deployAvailable) reasons.push("canonical 8099/deploy route unavailable");
|
||||||
|
if (result.factory.status !== "ok") reasons.push(`factory status ${result.factory.status ?? "unknown"}`);
|
||||||
|
if (result.deploy.status !== "ok") reasons.push(`deploy route status ${result.deploy.status ?? "unknown"}`);
|
||||||
|
return factoryGateReady(result) ? [] : [...new Set(reasons)];
|
||||||
|
}
|
||||||
|
|
@ -51,6 +51,9 @@ export type { CapabilityProbeResult, CapabilityReport, FactoryCapabilityService
|
||||||
export { deployToFactory, isAllowedFactoryCommand, writeFactoryDeployReceipt } from "./factory-deploy.js";
|
export { deployToFactory, isAllowedFactoryCommand, writeFactoryDeployReceipt } from "./factory-deploy.js";
|
||||||
export type { FactoryDeployOptions, FactoryDeployReceipt } from "./factory-deploy.js";
|
export type { FactoryDeployOptions, FactoryDeployReceipt } from "./factory-deploy.js";
|
||||||
|
|
||||||
|
export { createFactoryReconciliationReceipt, writeFactoryReconciliationReceipt } from "./factory-reconciliation.js";
|
||||||
|
export type { FactoryReconciliationReceipt } from "./factory-reconciliation.js";
|
||||||
|
|
||||||
export { reconcileRsi, writeRsiReconcileReceipt } from "./rsi-reconcile.js";
|
export { reconcileRsi, writeRsiReconcileReceipt } from "./rsi-reconcile.js";
|
||||||
export type { RsiReconcileOptions, RsiReconcileReceipt } from "./rsi-reconcile.js";
|
export type { RsiReconcileOptions, RsiReconcileReceipt } from "./rsi-reconcile.js";
|
||||||
|
|
||||||
|
|
|
||||||
15
src/index.ts
15
src/index.ts
|
|
@ -1564,6 +1564,21 @@ factory
|
||||||
if (!report.requiredOk) process.exit(1);
|
if (!report.requiredOk) process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
factory
|
||||||
|
.command("reconcile")
|
||||||
|
.description("Write a live factory reconciliation receipt")
|
||||||
|
.option("--output <path>", "Receipt output path", path.join(".fable", "factory", "reconciliation-live.json"))
|
||||||
|
.option("--run <id>", "Emit reconciliation receipt event to .runs/<id>/channel.jsonl")
|
||||||
|
.action(async (opts: { output?: string; run?: string }) => {
|
||||||
|
const { writeFactoryReconciliationReceipt } = await import("./fable5/factory-reconciliation.js");
|
||||||
|
const out = opts.output ?? path.join(".fable", "factory", "reconciliation-live.json");
|
||||||
|
const receipt = await writeFactoryReconciliationReceipt(out, { sourceCommand: `node ${process.argv.slice(1).join(" ")}` });
|
||||||
|
console.log(JSON.stringify(receipt, null, 2));
|
||||||
|
console.log(`\n Receipt: ${out}\n`);
|
||||||
|
await emitRunEvent(opts.run, { source: "gate", type: receipt.decision === "ready" ? "receipt" : "error", data: { kind: "factory-reconciliation", receipt, path: out } });
|
||||||
|
if (receipt.decision !== "ready") process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
factory
|
factory
|
||||||
.command("rsi")
|
.command("rsi")
|
||||||
.description("Reconcile live RSI/self-improvement evidence into a receipt")
|
.description("Reconcile live RSI/self-improvement evidence into a receipt")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue