feat: add receipt evidence metadata

This commit is contained in:
artale 2026-06-25 17:27:21 +02:00
parent 98486d3a55
commit 975ef28b15
4 changed files with 84 additions and 4 deletions

View File

@ -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", () => {

View File

@ -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,
};
}

View File

@ -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 () => {

View File

@ -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<VerifyCycle
commit_sha: commit,
skill,
deploy: "8099/deploy gated",
evidence: {
container_count: factory.factory.containers,
factory_ok: factoryReady,
cron_present: rsi.facts.cron_present,
rsi_healthy: rsi.decision === "healthy",
deploy_target: "8099/deploy",
verify_cycle_passed: passed,
receipt_id: `verify-cycle:${commit}:${created}`,
},
rubric: receiptRubric(passed),
factory_ready: factoryReady,
factory,
rsi,
@ -99,3 +121,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,
};
}