/**
 * canary/drift.ts — Pure drift-statistic computation for the Tier 3
 * framework-tests-framework loop.
 *
 * Consumes the projection shape returned by Studio's `latestReportsQuery`
 * (we accept a slim subset so the function stays a pure-domain dependency
 * with no Studio-package import). Computes per-area Δscore between the
 * most-recent canary run and the trailing-N median, plus an overall
 * Δscore for the run as a whole. Output classifies each delta as `ok`,
 * `warn`, or `regression` against caller-provided thresholds.
 *
 * The function is total — it never throws. Edge cases (empty trailing
 * window, missing scores) surface as `verdict: "no-baseline"` so the
 * caller can decide whether to treat the missing baseline as a fail.
 *
 * @see docs/design-docs/testing-strategy.md — "Tier 3 — Live LLMs"
 * @see packages/studio/src/queries.ts — `latestReportsQuery`
 */
/**
 * Compute per-area + overall drift for a sequence of canary runs.
 *
 * `reports` must be ordered **newest-first** (matching `latestReportsQuery`'s
 * `order(completedAt desc)`). The most-recent run is `reports[0]`; the
 * trailing window is `reports.slice(1, 1 + trailingN)`.
 *
 * @throws never — all error states surface as `no-baseline` verdicts.
 */
export function computeCanaryDrift(reports, thresholds) {
    if (reports.length === 0)
        return null;
    const minBaseline = thresholds.minBaselineRuns ?? 1;
    const current = reports[0];
    const trailing = reports.slice(1, 1 + thresholds.trailingN);
    const overall = scoreDrift("overall", current.overall, trailing.map((r) => r.overall), thresholds, minBaseline);
    const byArea = [];
    for (const score of current.scores) {
        const trailingArea = [];
        for (const t of trailing) {
            const match = t.scores.find((s) => s.feature === score.feature);
            if (match)
                trailingArea.push(match.totalScore);
        }
        byArea.push(scoreDrift(score.feature, score.totalScore, trailingArea, thresholds, minBaseline));
    }
    const hasRegression = overall.verdict === "regression" ||
        byArea.some((e) => e.verdict === "regression");
    const hasMovement = hasRegression ||
        overall.verdict === "warn" ||
        byArea.some((e) => e.verdict === "warn");
    return {
        reportId: current.reportId,
        completedAt: current.completedAt,
        overall,
        byArea,
        hasRegression,
        hasMovement,
    };
}
function scoreDrift(feature, current, trailing, thresholds, minBaseline) {
    if (trailing.length < minBaseline) {
        return {
            feature,
            current,
            trailingMedian: null,
            delta: null,
            verdict: "no-baseline",
        };
    }
    const trailingMedian = median(trailing);
    const delta = current - trailingMedian;
    const drop = -delta;
    let verdict = "ok";
    if (drop >= thresholds.failDelta)
        verdict = "regression";
    else if (drop >= thresholds.warnDelta)
        verdict = "warn";
    return { feature, current, trailingMedian, delta, verdict };
}
function median(values) {
    const sorted = [...values].sort((a, b) => a - b);
    const mid = Math.floor(sorted.length / 2);
    if (sorted.length % 2 === 0) {
        return (sorted[mid - 1] + sorted[mid]) / 2;
    }
    return sorted[mid];
}
