/*
RFC-1031: Shadow execution engine — runs candidate alongside active
component in parallel, compares results, and records CandidateEvidenceV1.
- Does not route traffic — that is the canary router's job.
- Does not make promotion decisions — the agent evaluates the evidence.
- RFC-1031: initial implementation — shadow executor for parallel invocation and comparison.
*/
import type { Sha256Digest } from "@warpgogol/werkstatt-shared/fingerprint";
import { byteHash } from "@warpgogol/werkstatt-shared/fingerprint";
import type { CandidateEvidenceV1 } from "./contracts.ts";
export interface ShadowExecutor {
execute(
candidateId: string,
comparisonMetric: string,
candidateFn: (input: unknown) => unknown,
activeFn: (input: unknown) => unknown,
input: unknown,
): Promise;
}
export function createShadowExecutor(): ShadowExecutor {
return {
async execute(
candidateId: string,
comparisonMetric: string,
candidateFn: (input: unknown) => unknown,
activeFn: (input: unknown) => unknown,
input: unknown,
): Promise {
const canonicalInput = JSON.stringify(input);
const scenarioHash = byteHash(canonicalInput) as Sha256Digest;
const [candidateResult, activeResult] = await Promise.all([
Promise.resolve(candidateFn(input)),
Promise.resolve(activeFn(input)),
]);
const candidateValue = typeof candidateResult === "number" ? candidateResult : 0;
const activeValue = typeof activeResult === "number" ? activeResult : 0;
const delta = candidateValue - activeValue;
return {
schema: "werkstatt/candidate-evidence@1",
phase: "shadow",
metric: comparisonMetric,
candidateValue,
activeValue,
delta,
timestamp: new Date().toISOString(),
scenarioHash,
};
},
};
}