/**
 * pipeline/borderline-consensus-runner.ts
 *
 * GRAD-04 borderline-only intra-grader consensus runner. Thin sibling
 * of runGraderConsistency — re-grades ONLY judgments where
 * `isBorderline(score, thresholds)` returns true. Non-borderline
 * judgments pass through unchanged.
 *
 * Per D0005 (grader-model separation), replicates the SAME pinned
 * grader N times (default 3, configurable via
 * RepoConfig.execution.borderlineReplications); NOT the inter-grader
 * ensemble path. Doc 03 §"Multi-grader consensus" + the GRAD-04 "Doc 03
 * source bug" callout pinned this to intra-grader replication only.
 *
 * The re-grade hook is supplied by the caller as a `regrade` callback.
 * The composition root wires it to `gradeOnce` from grader-api.js with
 * the response/rubric text drawn from the original Promptfoo result.
 * The runner itself imports `gradeOnce` only as the default regrader
 * fallback so unit tests can spy/inject without re-wiring.
 *
 * @see docs/decisions/D0005-grader-model-separation.md
 * @see ./borderline-detector.ts — pure predicate
 * @see ./grader-consistency.ts — JudgmentConsistency shape we emit
 */
import { isBorderline } from "./borderline-detector.js";
// Imported for the default-regrader fallback documented in the header.
// The runner does not invoke gradeOnce directly when `regrade` is supplied.
// Keeping the import on the public surface preserves the architectural
// rule that the runner's grader entry point lives in grader-api.js
// (Pitfall 6 — the inter-grader ensemble module is intentionally NOT
// reached for on this path).
import { gradeOnce } from "./grader-api.js";
import { analyzeJudgment, } from "./grader-consistency.js";
/**
 * Re-export `gradeOnce` so callers that need to wire the default regrader
 * (composition-root, integration tests) can import the grader entry point
 * from this module rather than rediscovering grader-api.js. The runner
 * itself does not invoke `gradeOnce` — the caller-supplied `regrade`
 * callback owns the live grader call (Pitfall 6 — runner stays pure wrt
 * provider config).
 */
export { gradeOnce };
/** Map key for the per-judgment consistency record. */
function consistencyKey(j) {
    return `${j.taskId}::${j.dimension}::${j.modelId}`;
}
/**
 * Run intra-grader consensus on the borderline subset of `judgments`.
 *
 * - Borderline (per `isBorderline(score, thresholds)`): re-grade
 *   `replications` times via `regrade`; emit a `JudgmentConsistency`
 *   keyed by `${taskId}::${dimension}::${modelId}`; merge the consensus
 *   median back into the canonical judgment's `score`.
 * - Non-borderline: pass through unchanged. Output array length == input.
 *
 * The function is order-preserving — the returned `judgments` array
 * keeps the same element order as the input.
 */
export async function runBorderlineConsensus(options) {
    const { judgments, logger, regrade, replications, thresholds } = options;
    const consistencyByJudgment = new Map();
    // Filter to borderline subset; bypass entirely if empty.
    const borderlineKeys = new Set();
    for (const j of judgments) {
        if (isBorderline(j.score, thresholds)) {
            borderlineKeys.add(consistencyKey(j));
        }
    }
    if (borderlineKeys.size === 0) {
        return { consistencyByJudgment, judgments };
    }
    const out = [];
    for (const j of judgments) {
        const key = consistencyKey(j);
        if (!borderlineKeys.has(key)) {
            out.push(j); // non-borderline — single replica
            continue;
        }
        // Re-grade `replications` times via the same pinned grader. The
        // replications carry network-bound side effects (LLM calls), so run
        // them concurrently — `Promise.allSettled` preserves the per-replica
        // try/catch shape (failures log + drop, surviving replicas still
        // contribute to the consensus median). Worst-case wall time drops
        // from `replications * roundTrip` to a single `roundTrip`.
        const scores = [j.score];
        const settled = await Promise.allSettled(Array.from({ length: replications }, () => regrade(j)));
        settled.forEach((outcome, i) => {
            if (outcome.status === "fulfilled") {
                scores.push(outcome.value);
            }
            else {
                const err = outcome.reason;
                logger?.warn(`Borderline replication ${i + 1}/${replications} failed for ${key}: ` +
                    (err instanceof Error ? err.message : String(err)));
            }
        });
        const grading = {
            area: "",
            dimension: j.dimension,
            ...(j.modelId ? { providerId: j.modelId } : {}),
            scores,
            taskId: j.taskId,
        };
        const consistency = analyzeJudgment(grading);
        consistencyByJudgment.set(key, consistency);
        // Merge consensus (median across replicas) into the canonical judgment.
        out.push({ ...j, score: median(scores) });
    }
    return { consistencyByJudgment, judgments: out };
}
/**
 * Compute the median of an array of numbers. The runner uses median
 * (not mean) so a single outlier replica doesn't drag the consensus
 * score across a severity threshold.
 */
function median(values) {
    if (values.length === 0)
        return 0;
    const sorted = [...values].sort((a, b) => a - b);
    const mid = Math.floor(sorted.length / 2);
    return sorted.length % 2 === 0
        ? (sorted[mid - 1] + sorted[mid]) / 2
        : sorted[mid];
}
