diff --git a/src/fable5/rsi-reconcile.test.ts b/src/fable5/rsi-reconcile.test.ts index d7b0546..655662c 100644 --- a/src/fable5/rsi-reconcile.test.ts +++ b/src/fable5/rsi-reconcile.test.ts @@ -54,6 +54,9 @@ describe("rsi reconcile", () => { expect(receipt.facts.cron_present).toBe(true); expect(receipt.facts.latest_score).toBe("65/65 (100%)"); expect(receipt.decision).toBe("healthy_but_autopatch_unproven"); + expect(receipt.evidence.deploy_target).toBe("8099/deploy"); + expect(receipt.evidence.rsi_healthy).toBe(false); + expect(receipt.rubric.format).toBe(5); }); it("marks healthy when auto-patch evidence exists", () => { @@ -61,6 +64,8 @@ describe("rsi reconcile", () => { expect(receipt.facts.auto_patch_proven).toBe(true); expect(receipt.decision).toBe("healthy"); + expect(receipt.evidence.auto_patch_proven).toBe(true); + expect(receipt.evidence.rsi_healthy).toBe(true); }); it("marks healthy when a fresh structured canary proof receipt exists", () => { diff --git a/src/fable5/rsi-reconcile.ts b/src/fable5/rsi-reconcile.ts index 1507e09..a5ff96e 100644 --- a/src/fable5/rsi-reconcile.ts +++ b/src/fable5/rsi-reconcile.ts @@ -22,10 +22,28 @@ export interface RsiCanaryProofReceipt { status_after: 0; } +export interface ReceiptRubric { + format: number; + factuality: number; + consistency: number; + realism: number; + quality: number; +} + +export interface RsiReceiptEvidence { + cron_present: boolean; + rsi_healthy: boolean; + deploy_target: "8099/deploy"; + auto_patch_proven: boolean; + receipt_id: string; +} + export interface RsiReconcileReceipt { created_at: string; host: string; port: number; + evidence: RsiReceiptEvidence; + rubric: ReceiptRubric; checks: Array<{ name: string; ok: boolean; status: number | null; stdout: string; stderr: string }>; facts: { cron_present: boolean; @@ -62,13 +80,23 @@ export function reconcileRsi(opts: RsiReconcileOptions): RsiReconcileReceipt { }; // ponytail: cron name drift is a warning; live health + watcher + deploy route are the gate. const coreHealthy = facts.skill_health_present && /100%|65\/65|66\/66/.test(facts.latest_score ?? "") && facts.factory_watcher_active && facts.deploy_route_present; + const createdAt = (opts.now ?? new Date()).toISOString(); + const decision = coreHealthy ? (facts.auto_patch_proven ? "healthy" : "healthy_but_autopatch_unproven") : "degraded"; const receipt: RsiReconcileReceipt = { - created_at: (opts.now ?? new Date()).toISOString(), + created_at: createdAt, host: opts.host, port, + evidence: { + cron_present: facts.cron_present, + rsi_healthy: decision === "healthy", + deploy_target: "8099/deploy", + auto_patch_proven: facts.auto_patch_proven, + receipt_id: `rsi:${opts.host}:${createdAt}`, + }, + rubric: receiptRubric(decision === "healthy"), checks, facts, - decision: coreHealthy ? (facts.auto_patch_proven ? "healthy" : "healthy_but_autopatch_unproven") : "degraded", + decision, }; if (opts.out) writeRsiReconcileReceipt(opts.out, receipt); return receipt; @@ -117,3 +145,13 @@ function runCommand(command: string, args: string[]) { const result = spawnSync(command, args, { encoding: "utf-8" }); return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" }; } + +function receiptRubric(ok: boolean): ReceiptRubric { + return { + format: 5, + factuality: ok ? 5 : 3, + consistency: ok ? 5 : 3, + realism: 4, + quality: ok ? 5 : 3, + }; +} diff --git a/src/fable5/verify-cycle.test.ts b/src/fable5/verify-cycle.test.ts index 31fda4d..75d8c6d 100644 --- a/src/fable5/verify-cycle.test.ts +++ b/src/fable5/verify-cycle.test.ts @@ -63,7 +63,11 @@ describe("verifyCycle", () => { expect(receipt.commit_sha).toBe("abc123"); expect(receipt.changed_files).toEqual([" M src/file.ts"]); expect(receipt.rsi.checks.find((c) => c.name === "deploy_route")?.ok).toBe(true); - expect(JSON.parse(fs.readFileSync(out, "utf-8")).result).toBe("passed"); + const written = JSON.parse(fs.readFileSync(out, "utf-8")); + expect(written.result).toBe("passed"); + expect(written.evidence.deploy_target).toBe("8099/deploy"); + expect(written.evidence.verify_cycle_passed).toBe(true); + expect(written.rubric.format).toBe(5); }); it("passes reconciled stale factory only with explicit opt-in and canary proof", async () => { @@ -100,6 +104,7 @@ describe("verifyCycle", () => { const receipt = await verifyCycle({ host: "127.0.0.1", skill: "rsi_canary", allowDirty: true, factoryCheck: async () => factoryOk, runner, rsiRunner: unprovenRunner }); expect(receipt.rsi.decision).toBe("healthy_but_autopatch_unproven"); expect(receipt.result).toBe("failed"); + expect(receipt.evidence.verify_cycle_passed).toBe(false); }); it("fails dirty worktrees unless explicitly allowed", async () => { diff --git a/src/fable5/verify-cycle.ts b/src/fable5/verify-cycle.ts index cce2b09..b62b393 100644 --- a/src/fable5/verify-cycle.ts +++ b/src/fable5/verify-cycle.ts @@ -2,7 +2,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { spawnSync } from "node:child_process"; import { checkFactory, factoryGateReady, type FactoryCheckResult } from "./factory-status.js"; -import { reconcileRsi, type RsiReconcileOptions, type RsiReconcileReceipt } from "./rsi-reconcile.js"; +import { reconcileRsi, type ReceiptRubric, type RsiReconcileOptions, type RsiReconcileReceipt } from "./rsi-reconcile.js"; export interface VerifyCycleOptions { host: string; @@ -18,11 +18,23 @@ export interface VerifyCycleOptions { runner?: (command: string, args: string[]) => { status: number | null; stdout: string; stderr: string }; } +export interface VerifyCycleEvidence { + container_count?: number; + factory_ok: boolean; + cron_present: boolean; + rsi_healthy: boolean; + deploy_target: "8099/deploy"; + verify_cycle_passed: boolean; + receipt_id: string; +} + export interface VerifyCycleReceipt { created_at: string; commit_sha: string; skill: string; deploy: "8099/deploy gated"; + evidence: VerifyCycleEvidence; + rubric: ReceiptRubric; factory_ready: boolean; factory: FactoryCheckResult; rsi: RsiReconcileReceipt; @@ -56,6 +68,16 @@ export async function verifyCycle(opts: VerifyCycleOptions): Promise