/**
 * Rerun + compare primitives over grounded run artifacts
 * (mmnto-ai/totem#2100, strategy#474 slice 1).
 *
 * - **Rerun = same immutable bundle.** `rerunArtifact` re-invokes the
 *   recorded backend with the artifact's EXACT `inputBundle` — it never
 *   touches live retrieval (it imports none) and forces `fresh: true` so the
 *   response cache cannot replay a prior answer as if it were a new run.
 *   The rerun emits a NEW artifact; the source record is never mutated.
 *
 * - **Compare = artifact-vs-artifact diff, deterministic only.** Structural
 *   equality + content hashes + numeric metric deltas. Deliberately NO
 *   similarity scoring of any kind (#2100 review F3 / Tenet 9) — a scorer in
 *   the deterministic substrate would be an LLM-judge smuggled past the
 *   admission contract. Semantic comparison is eval-harness territory
 *   (#2103+), never a core primitive.
 */
import * as path from 'node:path';
import { ADMISSION_COMPLETION_ONLY, calculateDeterministicHash, loadRunArtifact, TotemOrchestratorError, } from '@mmnto/totem';
import { runOrchestrator } from '../utils.js';
/**
 * Replay a recorded run against its recorded backend with zero input drift.
 * Throws (via `loadRunArtifact`) before invoking anything when the source
 * hash is missing or invalid.
 */
export async function rerunArtifact(opts) {
    const configRoot = opts.configRoot ?? opts.cwd;
    const totemDirAbs = path.join(configRoot, opts.config.totemDir);
    const source = loadRunArtifact(totemDirAbs, opts.hash);
    let emitted;
    const content = await runOrchestrator({
        prompt: source.inputBundle.maskedPrompt,
        ...(source.inputBundle.maskedSystemPrompt !== undefined
            ? { systemPrompt: source.inputBundle.maskedSystemPrompt }
            : {}),
        tag: source.backend.taskProfile,
        // fresh: a response-cache replay is not a rerun (silent drift, same trap
        // class as live-retrieval leakage). model: the RESOLVED backend recorded
        // at emission, not whatever the config resolves to today.
        options: { fresh: true, model: source.backend.qualifiedModel },
        config: opts.config,
        cwd: opts.cwd,
        configRoot,
        ...(source.backend.temperature !== undefined
            ? { temperature: source.backend.temperature }
            : {}),
        // ── Admission replay (mmnto-ai/totem#2102) ──
        // The recorded contract replays verbatim: an elevated class re-enters the
        // admission gate (a `self_grounding_agent` rerun must fail loud when the
        // capability is no longer declared, never silently downgrade — Tenet 4).
        // `completion_only` stays omitted: the default IS the constant, keeping a
        // slice-1 rerun byte-identical to today (invariant 7).
        ...(source.backend.admissionClass !== ADMISSION_COMPLETION_ONLY
            ? { backendAdmissionClass: source.backend.admissionClass }
            : {}),
        ...(source.admission?.outputContract !== undefined
            ? { outputContract: source.admission.outputContract }
            : {}),
        ...(source.admission?.contextPolicy !== undefined
            ? { contextPolicy: source.admission.contextPolicy }
            : {}),
        ...(source.admission?.runMetadata !== undefined
            ? { runMetadata: source.admission.runMetadata }
            : {}),
        artifact: {
            // The rerun makes no new grounding claim — identity carried verbatim,
            // including the per-item bundle when the source artifact has one (mmnto-ai/totem#2101).
            groundingHash: source.grounding.hash,
            provenanceSummary: source.grounding.provenanceSummary,
            ...(source.grounding.bundle !== undefined ? { bundle: source.grounding.bundle } : {}),
            ...(source.inputBundle.diffScope !== undefined
                ? { diffScope: source.inputBundle.diffScope }
                : {}),
            ...(source.inputBundle.specContract !== undefined
                ? { specContract: source.inputBundle.specContract }
                : {}),
            onEmitted: (hash, artifactPath) => {
                emitted = { hash, path: artifactPath };
            },
        },
    });
    if (content === undefined || emitted === undefined) {
        // --raw can't happen (we don't pass it), so this is an emission failure —
        // the rerun ran but its record didn't land. Loud, not a silent partial.
        // TotemOrchestratorError per the CLI error taxonomy (GCA review on #2114).
        throw new TotemOrchestratorError(`Rerun of ${opts.hash.slice(0, 12)}… completed but no artifact was recorded.`, 'See the emission warning above for why the record did not land, then retry the rerun.');
    }
    return { sourceHash: opts.hash, hash: emitted.hash, path: emitted.path, content };
}
/** Backend fields compared one by one so the delta NAMES what changed. */
const BACKEND_FIELDS = [
    'provider',
    'model',
    'qualifiedModel',
    'admissionClass',
    'taskProfile',
    'temperature',
];
/**
 * Admission members compared one by one (mmnto-ai/totem#2102) — mirrors
 * {@link BACKEND_FIELDS}. Compile-time exhaustive against the artifact
 * `admission` group (mmnto-ai/totem#2148 round-1): `satisfies Record<AdmissionField, true>`
 * fails tsc when the schema gains a member this map doesn't name (missing
 * key) AND when this map names a member the schema lost (excess key) — a
 * schema field can never silently fall out of the compare delta.
 */
const ADMISSION_FIELD_SET = {
    outputContract: true,
    contextPolicy: true,
    runMetadata: true,
};
const ADMISSION_FIELDS = Object.keys(ADMISSION_FIELD_SET);
/**
 * Structural member equality via the deterministic hash (key-order
 * independent — the same identity primitive the artifacts use). Absent on
 * both sides is equal; absent on one side is a NAMED delta, never hidden.
 */
function sameAdmissionMember(a, b) {
    if (a === undefined || b === undefined)
        return a === b;
    return calculateDeterministicHash(a) === calculateDeterministicHash(b);
}
/** Numeric delta honoring honest-absent: null in, null out — never NaN. */
function tokenDelta(a, b) {
    if (typeof a !== 'number' || typeof b !== 'number')
        return null;
    return b - a;
}
/**
 * Deterministic artifact-vs-artifact diff. Pure function of its two inputs —
 * no I/O, no scoring, no randomness.
 */
export function compareRunArtifacts(a, b) {
    const backendDelta = BACKEND_FIELDS.filter((field) => a.backend[field] !== b.backend[field]);
    const admissionDelta = ADMISSION_FIELDS.filter((field) => !sameAdmissionMember(a.admission?.[field], b.admission?.[field]));
    const sameOutput = a.output.content === b.output.content;
    // Short-circuit the second sha256 pass when the contents are byte-identical
    // (Greptile review on #2114) — the equal-outputs fast path is explicit.
    const contentHashA = calculateDeterministicHash(a.output.content);
    const contentHashB = sameOutput ? contentHashA : calculateDeterministicHash(b.output.content);
    return {
        sameInput: a.inputHash === b.inputHash,
        sameGrounding: a.grounding.hash === b.grounding.hash &&
            a.grounding.provenanceSummary === b.grounding.provenanceSummary,
        sameBackend: backendDelta.length === 0,
        backendDelta,
        sameAdmission: admissionDelta.length === 0,
        admissionDelta,
        sameOutput,
        outputDelta: { contentHashA, contentHashB },
        metricsDelta: {
            durationMs: b.output.metrics.durationMs - a.output.metrics.durationMs,
            inputTokens: tokenDelta(a.output.metrics.inputTokens, b.output.metrics.inputTokens),
            outputTokens: tokenDelta(a.output.metrics.outputTokens, b.output.metrics.outputTokens),
        },
    };
}
/** Load-then-compare convenience for the CLI verb. */
export function compareArtifacts(totemDirAbs, hashA, hashB) {
    return compareRunArtifacts(loadRunArtifact(totemDirAbs, hashA), loadRunArtifact(totemDirAbs, hashB));
}
//# sourceMappingURL=run-artifacts.js.map