/**
 * scoring-bridge.ts — Bridge between Promptfoo raw results and the
 * 4-tier scoring engine.
 *
 * Converts Promptfoo `ComponentResult[]` (from test results) into the
 * scoring engine's `AssertionScore[]` format, then delegates aggregation
 * to `aggregateDimensions` and `computeTaskScore` from core.
 *
 * This bridge replaces the three legacy scoring primitives in
 * `calculate-scores.ts`:
 *   - `accumulateDimensions`  → `convertToAssertionScores` + `aggregateDimensions`
 *   - `averageDimensions`     → (handled internally by `aggregateDimensions`)
 *   - `weightedComposite`     → `computeTaskScore`
 *
 * The bridge preserves the existing 0–100 output scale. The 4-tier
 * engine works in [0, 1]; this module handles the conversion at
 * boundaries.
 *
 * W0198 Phase 5 — when a `preflightForTest` callback is provided and
 * returns a `SymbolPreflightReport`, the bridge synthesizes one extra
 * `AssertionScore` per test in the `code-correctness` dimension. The
 * deterministic preflight and the LLM rubric merge through D0010's
 * weighted dimension aggregation; the relative share is set by
 * `preflightWeight` in `[0, 1]`.
 *
 * The consumer-verification lane rides the same mechanism at
 * `consumerVerificationWeight`; when both lanes report, the rubric share is
 * `1 - preflightWeight - consumerVerificationWeight`, clamped at 0 (D0047).
 *
 * @see packages/core/src/services/scoring-engine.ts — the 4-tier engine
 * @see packages/eval/src/pipeline/calculate-scores.ts — the consumer
 * @see docs/archive/exec-plans/architecture-overhaul/phase-7-migrate-literacy.md
 * @see docs/design-docs/two-stage-grader-symbol-preflight.md — W0198 design
 */
import { aggregateDimensions, computeTaskScore, DEFAULT_CONSUMER_VERIFICATION_CODE_CORRECTNESS_WEIGHT, DEFAULT_PREFLIGHT_CODE_CORRECTNESS_WEIGHT, normalizeScore, } from "../../_vendor/ailf-core/index.js";
import { classifyRubric, parseRubricScore } from "../../_vendor/ailf-core/index.js";
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Score a group of test results using the 4-tier scoring engine.
 *
 * This replaces the legacy `accumulateDimensions → averageDimensions →
 * weightedComposite` chain with the new engine's `aggregateDimensions →
 * computeTaskScore` chain.
 *
 * @param tests    Pre-filtered test results (e.g., all gold or all baseline)
 * @param profile  Weight profile mapping kebab-case dimension names to weights
 *                 (e.g., `{ "task-completion": 0.4, "code-correctness": 0.35, "doc-coverage": 0.25 }`)
 * @param taskId   Optional task identifier for traceability in TaskScore output
 * @param options  Optional deterministic-lane integration (W0198 preflight
 *                 and/or consumer verification) merged into code-correctness
 * @returns        Dimensions (0–100) and composite (0–100), matching legacy output format
 */
export function scoreTestGroup(tests, profile, taskId, options) {
    let totalCost = 0;
    const preflightForTest = options?.preflightForTest;
    const preflightWeight = clampWeight(options?.preflightWeight ?? DEFAULT_PREFLIGHT_CODE_CORRECTNESS_WEIGHT);
    const preflightActive = typeof preflightForTest === "function" && preflightWeight > 0;
    const consumerVerificationForTest = options?.consumerVerificationForTest;
    const consumerVerificationWeight = clampWeight(options?.consumerVerificationWeight ??
        DEFAULT_CONSUMER_VERIFICATION_CODE_CORRECTNESS_WEIGHT);
    const consumerVerificationActive = typeof consumerVerificationForTest === "function" &&
        consumerVerificationWeight > 0;
    // Step 1: Convert all ComponentResults into AssertionScore[] (0–1 scale)
    //
    // Four assertion sources contribute to scoring:
    //   - llm-rubric: dimension from metadata, score from grader (0–100 → [0,1])
    //   - javascript: mapped to "assertion-pass-rate" dimension (pass=1, fail=0)
    //   - preflight (W0198): synthesized per test from SymbolPreflightReport,
    //     dimension "code-correctness", weight = preflightWeight.
    //   - consumerVerification: synthesized per test from the consumer-
    //     verification lane's ConsumerVerificationReport, dimension
    //     "code-correctness", weight = consumerVerificationWeight.
    //
    // Rubric weight reduction is per-test, not global: a test's
    // `code-correctness` rubric assertion only sheds a lane's share when
    // that lane actually contributes a paired report for that test.
    // Without this gate, tests with no lane coverage would have their
    // rubric authority silently downweighted with nothing to compensate, so
    // partial-coverage runs would systematically bias the dimension toward
    // tests that DO have deterministic-lane data.
    //
    // Other types (cost, trajectory, contains, etc.) are metadata or guards —
    // they don't produce dimension scores.
    const assertionScores = [];
    for (const test of tests) {
        totalCost += test.cost;
        const report = preflightActive ? preflightForTest(test) : undefined;
        const consumerVerification = consumerVerificationActive
            ? consumerVerificationForTest(test)
            : undefined;
        const ccRubricWeight = clampWeight(1 -
            (report ? preflightWeight : 0) -
            (consumerVerification ? consumerVerificationWeight : 0));
        for (const comp of test.gradingResult.componentResults) {
            const converted = componentToScore(comp, ccRubricWeight);
            if (converted)
                assertionScores.push(converted);
        }
        if (report) {
            assertionScores.push(preflightToScore(report, preflightWeight));
        }
        if (consumerVerification) {
            assertionScores.push(consumerVerificationToScore(consumerVerification, consumerVerificationWeight));
        }
    }
    // Step 2: Aggregate into DimensionScores (0–1 scale).
    //
    // Use `weighted-mean` so the W0198 preflight / rubric weights inside
    // `code-correctness` are honored. With everything at weight=1.0 the
    // result is identical to plain `mean`, so behavior outside the
    // code-correctness merge is unchanged. See `aggregateScores` in
    // `packages/core/src/services/scoring-engine.ts` for the equivalence.
    const dimensionLabels = {
        "assertion-pass-rate": "Assertion Pass Rate",
        "code-correctness": "Code Correctness",
        "doc-coverage": "Doc Coverage",
        "task-completion": "Task Completion",
    };
    const rawDimensions = aggregateDimensions(assertionScores, {
        defaultAggregation: "weighted-mean",
        dimensionLabels,
    });
    // Step 3: Compute weighted composite via TaskScore (0–1 scale)
    const taskScoreResult = computeTaskScore(rawDimensions, {
        taskId: taskId ?? "aggregate",
        weights: profile,
        weightSource: "scoring-bridge",
    });
    // Step 4: Convert back to 0–100 scale for legacy compatibility
    const dimensions = {};
    for (const dim of rawDimensions) {
        // Map kebab-case dimension IDs to camelCase for legacy compatibility
        const camelKey = kebabToCamel(dim.dimensionId);
        dimensions[camelKey] = Math.round(dim.score * 100);
    }
    return {
        composite: Math.round(taskScoreResult.score * 100),
        dimensions,
        rawDimensions,
        totalCost,
    };
}
// ---------------------------------------------------------------------------
// Conversion helpers
// ---------------------------------------------------------------------------
/**
 * Route a ComponentResult to the appropriate scoring conversion.
 *
 * Dispatches by assertion type:
 *   - llm-rubric  → dimension from metadata, grader score (0–100 → [0,1])
 *   - javascript   → "assertion-pass-rate" dimension, binary (pass=1, fail=0)
 *   - everything else → null (not a scoring-relevant assertion)
 *
 * This replaces the previous llm-rubric-only filter that caused agent-harness
 * javascript assertions to be invisible to the scoring engine (DOC-2029).
 */
function componentToScore(comp, rubricCodeCorrectnessWeight) {
    const type = comp.assertion?.type;
    if (type === "llm-rubric") {
        return llmRubricToScore(comp, rubricCodeCorrectnessWeight);
    }
    if (type === "javascript") {
        return javascriptAssertionToScore(comp);
    }
    // Other types (cost, trajectory, contains, etc.) don't produce scores
    return null;
}
/**
 * Convert an LLM-rubric ComponentResult into an AssertionScore.
 *
 * The dimension comes from metadata (set during rubric template resolution).
 * Returns null if the component doesn't map to any dimension.
 *
 * For the `code-correctness` dimension specifically, the assertion's
 * weight is reduced when W0198's deterministic preflight is also feeding
 * the same dimension; the complementary share belongs to the preflight.
 */
function llmRubricToScore(comp, rubricCodeCorrectnessWeight) {
    const dim = classifyRubric(comp);
    if (!dim)
        return null;
    // Parse the raw score (0–100 from the grader) and normalize to [0, 1]
    const rawScore = parseRubricScore(comp);
    const normalized = normalizeScore(rawScore, "llm-rubric");
    const weight = dim === "code-correctness" ? rubricCodeCorrectnessWeight : 1.0;
    return {
        assertionType: comp.assertion?.type ?? "llm-rubric",
        dimension: dim,
        latencyMs: 0,
        pass: comp.pass,
        reason: comp.reason ?? "",
        score: normalized,
        weight,
    };
}
/**
 * Convert a javascript assertion ComponentResult into an AssertionScore.
 *
 * Javascript assertions (fileExists, fileContains, commandSucceeds, etc.)
 * produce binary pass/fail results. They map to the "assertion-pass-rate"
 * dimension — the fraction of structural assertions that passed.
 *
 * Diagnostic-only assertions (like URL extraction) are flagged by the
 * compiler with `metadata.scoring: false` and excluded from scoring. Before
 * D0083 this exclusion rode the `weight === 0` sentinel on the assertion;
 * assertion-level `weight` no longer exists, so the marker is explicit.
 */
function javascriptAssertionToScore(comp) {
    // Skip diagnostic-only assertions the compiler marked non-scoring
    // (e.g. URL extraction). See D0083.
    if (comp.assertion?.metadata?.scoring === false)
        return null;
    return {
        assertionType: "javascript",
        dimension: "assertion-pass-rate",
        latencyMs: 0,
        pass: comp.pass,
        reason: comp.reason ?? "",
        score: comp.pass ? 1.0 : 0.0,
        weight: 1.0,
    };
}
/**
 * Synthesize a `code-correctness` AssertionScore from a W0198 symbol-
 * preflight report.
 *
 * The score is `1 - min(1, total / cap)`. With the default
 * `{ perMissing: 20, cap: 60 }` config: 0 missing → 1.0, 1 missing → 0.667,
 * 2 missing → 0.333, ≥3 missing → 0.0.
 *
 * Edge cases for the deduction config:
 * - `cap === 0` (measurement-only config): score is 1.0 — divide-by-zero
 *   would NaN otherwise.
 * - `cap < 0` (misconfigured): score is 1.0 and the merge silently
 *   collapses to rubric-only on this dimension. The Phase 3 Zod schema
 *   gates against this upstream so it should never reach here, but the
 *   guard preserves the never-deduct invariant if it does.
 *
 * `unresolved` findings never deduct (the preflight's never-deduct rule):
 * they're not part of `total`, so they fall through to the LLM rubric.
 */
export function preflightToScore(report, weight) {
    const { perMissing, cap, total } = report.deduction;
    const score = cap > 0 ? 1 - Math.min(1, total / cap) : 1;
    const counts = countLanes(report);
    return {
        assertionType: "preflight",
        dimension: "code-correctness",
        latencyMs: 0,
        pass: total === 0,
        reason: `preflight: ${counts.exists} exists, ${counts.missing} missing, ${counts.unresolved} unresolved (deduction ${total}/${cap}, ${perMissing} per missing)`,
        score,
        weight,
    };
}
function countLanes(report) {
    let exists = 0;
    let missing = 0;
    let unresolved = 0;
    for (const f of report.findings) {
        if (f.result === "exists")
            exists++;
        else if (f.result === "missing")
            missing++;
        else
            unresolved++;
    }
    return { exists, missing, unresolved };
}
/**
 * Synthesize a `code-correctness` AssertionScore from a consumer-
 * verification report. The verifier already scored in `[0, 1]`, so the
 * verdict passes through unchanged (D0047: sibling, never a deduction).
 */
export function consumerVerificationToScore(report, weight) {
    return {
        assertionType: "consumerVerification",
        dimension: "code-correctness",
        latencyMs: report.durationMs,
        pass: report.pass,
        reason: `consumerVerification: ${report.reason}`,
        score: report.score,
        weight,
    };
}
function clampWeight(w) {
    if (!Number.isFinite(w))
        return 0;
    if (w < 0)
        return 0;
    if (w > 1)
        return 1;
    return w;
}
/** Convert kebab-case dimension key to camelCase (e.g., "task-completion" → "taskCompletion") */
function kebabToCamel(kebab) {
    return kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
}
