/**
 * OFFLINE TOOL. NOT a centerpiece. Use for ex-post analysis only —
 * never wire into live runtime.
 *
 * replayCounterfactual — counterfactual replay primitive.
 *
 * Given an original runId, replays the same cycle with an alternative compute
 * strategy (or verifier / temperature override). The original run's
 * clock/random/cache are kept fixed — only the compute strategy is swapped.
 *
 * Purpose: offline "what-if" analysis. E.g. "what if this cycle had used
 * best-of-n instead of single-shot?"
 *
 * Design invariants:
 *   - Does NOT mutate the original trace or any live execution path.
 *   - ReplayRunner.run() receives a ReplayContext augmented with strategyOverride
 *     injected into the runner's compute config.
 *   - "outputsCoherent" = same runtime type AND same top-level keys (for objects).
 *   - "outputsIdentical" = deep structural equality via JSON serialisation.
 *
 * @module counterfactual/replay-with-alt
 */
import { RecordedClock } from "../replay/clock.js";
import { RecordedRandom } from "../replay/random.js";
/** All valid strategy names as a frozen set for runtime validation. */
const VALID_STRATEGY_NAMES = new Set([
    "single-shot",
    "best-of-n",
    "bon-mav",
]);
// ─── InvalidStrategyNameError ─────────────────────────────────────────────────
/**
 * Thrown when alt_strategy is not one of the known ComputeStrategyName values.
 */
export class InvalidStrategyNameError extends Error {
    name;
    constructor(name) {
        super(`replayCounterfactual: unknown alt_strategy "${name}". ` +
            `Valid values: ${[...VALID_STRATEGY_NAMES].join(", ")}.`);
        this.name = name;
        this.name = "InvalidStrategyNameError";
    }
}
// ─── Coherence check ─────────────────────────────────────────────────────────
/**
 * Determine structural coherence between two outputs.
 *
 * Two outputs are "coherent" when:
 *   1. Both have the same typeof (e.g. both "string", both "object").
 *   2. If both are non-null plain objects: same set of top-level keys
 *      (set equality, order-independent).
 *   3. Special case: both null → coherent (typeof null === "object" in JS,
 *      but we treat null explicitly).
 */
function areCoherent(a, b) {
    // Handle null explicitly (typeof null === "object" would be misleading)
    if (a === null && b === null)
        return true;
    if (a === null || b === null)
        return false;
    const ta = typeof a;
    const tb = typeof b;
    if (ta !== tb)
        return false;
    // Both non-null objects: compare top-level key sets
    if (ta === "object") {
        const keysA = Object.keys(a).sort();
        const keysB = Object.keys(b).sort();
        if (keysA.length !== keysB.length)
            return false;
        for (let i = 0; i < keysA.length; i++) {
            if (keysA[i] !== keysB[i])
                return false;
        }
        return true;
    }
    // Primitives (string, number, boolean, etc.) — same typeof is sufficient
    return true;
}
// ─── replayCounterfactual ─────────────────────────────────────────────────────
/**
 * Replay the original cycle twice — once with its original config, once with
 * the alt overrides — and compare outputs.
 *
 * Both runs use the SAME virtualized clock, random, and LLM cache derived from
 * the original run's recorded artifacts. Only the compute strategy (and optional
 * verifier / temperature) differs.
 *
 * OFFLINE TOOL. Do NOT call from any live execution path.
 *
 * @param originalRunId  runId of the run to analyse counterfactually.
 * @param loader         Loader for the original trace + recorded artifacts.
 * @param runner         Runner that accepts CounterfactualReplayContext.
 *                       Contract: runner.run() MUST honour ctx.strategyOverride
 *                       when present.
 * @param opts           Counterfactual overrides (at least one should differ
 *                       from the original to be meaningful).
 */
export async function replayCounterfactual(originalRunId, loader, runner, opts) {
    // ── Validate alt_strategy early ────────────────────────────────────────────
    if (opts.alt_strategy !== undefined && !VALID_STRATEGY_NAMES.has(opts.alt_strategy)) {
        throw new InvalidStrategyNameError(opts.alt_strategy);
    }
    // ── Load original artifacts ─────────────────────────────────────────────────
    const [originalTrace, artifacts] = await Promise.all([
        loader.loadOriginalTrace(originalRunId),
        loader.loadCacheEntries(originalRunId),
    ]);
    // ── Determine original strategy from trace metadata (best-effort) ──────────
    // Runners may embed the strategy name in trace steps or config.
    // We look for it in originalTrace.config as a convention.
    const traceConfig = originalTrace.config;
    const originalStrategyName = typeof traceConfig?.strategy === "string" ? traceConfig.strategy : "unknown";
    // ── Build two independent virtualised contexts ──────────────────────────────
    // Each gets a fresh RecordedClock/RecordedRandom so they don't share cursors.
    const buildCtx = (replayRunId, overrides) => {
        const clock = new RecordedClock([...artifacts.recordedTs]);
        const random = new RecordedRandom([...artifacts.recordedNext], [...artifacts.recordedUuids], "strict");
        return {
            originalRunId,
            replayRunId,
            mode: "strict",
            clock,
            random,
            cache: artifacts.cache,
            ...overrides,
        };
    };
    const originalReplayRunId = globalThis.crypto.randomUUID();
    const counterfactualRunId = globalThis.crypto.randomUUID();
    const originalCtx = buildCtx(originalReplayRunId, {
    // No overrides — reproduce original behaviour
    });
    const counterfactualCtx = buildCtx(counterfactualRunId, {
        strategyOverride: opts.alt_strategy,
        verifierOverride: opts.alt_verifier,
        temperatureOverride: opts.alt_temperature,
    });
    // ── Run both scenarios ──────────────────────────────────────────────────────
    const t0Original = performance.now();
    const originalReplayTrace = await runner.run(originalCtx, originalTrace);
    const originalLatency = performance.now() - t0Original;
    const t0Cf = performance.now();
    const counterfactualTrace = await runner.run(counterfactualCtx, originalTrace);
    const cfLatency = performance.now() - t0Cf;
    // ── Extract outputs ─────────────────────────────────────────────────────────
    const originalOutput = originalReplayTrace.output ?? originalReplayTrace;
    const counterfactualOutput = counterfactualTrace.output ?? counterfactualTrace;
    // ── Compute comparisons ─────────────────────────────────────────────────────
    const outputsIdentical = JSON.stringify(originalOutput) === JSON.stringify(counterfactualOutput);
    const outputsCoherent = outputsIdentical || areCoherent(originalOutput, counterfactualOutput);
    // ── Build metadata ──────────────────────────────────────────────────────────
    const strategySwapped = opts.alt_strategy !== undefined
        ? { from: originalStrategyName, to: opts.alt_strategy }
        : undefined;
    const originalCostUsd = originalReplayTrace.cost_usd;
    const cfCostUsd = counterfactualTrace.cost_usd;
    const cost_delta_usd = typeof originalCostUsd === "number" && typeof cfCostUsd === "number"
        ? cfCostUsd - originalCostUsd
        : undefined;
    // Use runner-reported latency when available, fall back to wall-clock
    const origLatencyMs = typeof originalReplayTrace.latency_ms === "number"
        ? originalReplayTrace.latency_ms
        : originalLatency;
    const cfLatencyMs = typeof counterfactualTrace.latency_ms === "number"
        ? counterfactualTrace.latency_ms
        : cfLatency;
    const latency_delta_ms = cfLatencyMs - origLatencyMs;
    const cacheHits = typeof counterfactualTrace.cacheHits === "number"
        ? counterfactualTrace.cacheHits
        : 0;
    const cacheMisses = typeof counterfactualTrace.cacheMisses === "number"
        ? counterfactualTrace.cacheMisses
        : 0;
    return {
        originalRunId,
        counterfactualRunId,
        originalOutput,
        counterfactualOutput,
        outputsCoherent,
        outputsIdentical,
        metadata: {
            strategySwapped,
            cost_delta_usd,
            latency_delta_ms,
            cacheHits,
            cacheMisses,
        },
    };
}
//# sourceMappingURL=replay-with-alt.js.map