/**
 * pipeline/extract-grader-judgments-resilient.ts
 *
 * Resilient grader-judgment extraction for the `calculate-scores` persist
 * junction.
 *
 * Background: `calculateAndWriteScores` extracts grader judgments from the
 * eval results file(s), then writes `grader-judgments.json` only when the
 * array is non-empty (the `judgments.length > 0` guard). A runtime anomaly was
 * observed where `extractGraderJudgments` returned 0 judgments for a results
 * file that demonstrably contained classifiable llm-rubric components — the
 * same file, read tens of milliseconds later by `extractStoredTestResults`,
 * yielded the full set (entries with populated dimensions). The empty array
 * silently skipped the write, so gap-analysis and compute-attribution skipped
 * and the report shipped with a score but no tests.
 *
 * The committed code reads the file via a pure `readFileSync` with identical
 * classification on both sides, so the divergence is not reproducible from the
 * source + captured artifacts — it is a transient read anomaly at the live
 * junction. This wrapper does not pretend to know the mechanism; it makes the
 * junction observable and recovers from the transient:
 *
 *  1. **Instruments** — logs the resolved path(s), file size/mtime, parsed
 *     result count, and judgment count on every run (never silent on 0), so a
 *     future empty-judgments persist is diagnosable from the run log alone.
 *  2. **Self-heals** — when extraction yields 0 judgments but a results file
 *     exists, it re-extracts with bounded retries. A later read that yields
 *     judgments proves the initial 0 was transient; the recovered judgments
 *     are returned. If every attempt yields 0, severity is decided by an
 *     independent classifiable-component count: a genuinely judgment-free run
 *     (all api-errors / no llm-rubric) logs a warning, while 0 judgments
 *     against present classifiable components logs an error (the downstream
 *     gap-analysis fail-loud guard is the backstop).
 *
 * The extractor and fs helpers are injected so the wrapper is unit-testable
 * without importing the ~3000-line scoring module (which would be circular)
 * or touching the real filesystem.
 */
import { existsSync, statSync } from "node:fs";
const defaultStat = (path) => {
    if (!existsSync(path))
        return { exists: false, mtimeMs: 0, size: 0 };
    const s = statSync(path);
    return { exists: true, mtimeMs: s.mtimeMs, size: s.size };
};
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/**
 * Extract grader judgments across one or more results files, instrumented and
 * self-healing. See the module header for the rationale.
 *
 * @param resultsPaths  One or more results files (e.g. baseline + agentic in
 *   literacy full mode). Missing paths are skipped.
 * @param telemetry  Shared reliability sink threaded into every extraction.
 * @param log  Pipeline logger — the junction is logged here on every run.
 */
export async function extractGraderJudgmentsResilient(resultsPaths, telemetry, log, options) {
    const { extract } = options.deps;
    const statFile = options.deps.statFile ?? defaultStat;
    const sleep = options.deps.sleep ?? defaultSleep;
    const { countResults, countClassifiable } = options.deps;
    const maxAttempts = Math.max(1, options.maxAttempts ?? 3);
    const delayMs = options.delayMs ?? 200;
    const present = resultsPaths.filter((p) => statFile(p).exists);
    const extractAll = () => {
        const all = [];
        for (const p of present) {
            all.push(...extract(p, telemetry));
        }
        return all;
    };
    const diag = (path) => {
        const st = statFile(path);
        return {
            mtimeMs: st.mtimeMs,
            path,
            sizeBytes: st.size,
            ...(countResults ? { resultCount: countResults(path) } : {}),
        };
    };
    // Attempt 1 — always instrument the junction so 0 is never silent.
    let judgments = extractAll();
    for (const p of present) {
        log.info("Grader judgments — persist junction read", diag(p));
    }
    log.info(`Grader judgments extracted: ${judgments.length} total`, {
        judgmentCount: judgments.length,
        paths: present,
    });
    if (judgments.length > 0)
        return judgments;
    // 0 judgments and no results file present → genuinely nothing to grade.
    // A missing file cannot become non-empty within the retry window.
    if (present.length === 0) {
        log.info("No grader judgments — no results file present (nothing to grade)");
        return judgments;
    }
    // Results file(s) exist but extraction yielded 0 judgments — a suspected
    // transient read anomaly. Loud diagnostic, then bounded self-heal retries;
    // the same file read tens of ms later has been observed to yield the full set.
    log.warn("Grader extraction returned 0 judgments despite present results file(s) — suspected transient read anomaly; attempting self-heal", { paths: present.map(diag) });
    for (let attempt = 2; attempt <= maxAttempts; attempt++) {
        await sleep(delayMs);
        judgments = extractAll();
        log.warn(`Grader self-heal attempt ${attempt}/${maxAttempts}: ${judgments.length} judgment(s)`);
        if (judgments.length > 0) {
            log.warn(`Grader self-heal recovered ${judgments.length} grader judgment(s) on attempt ${attempt} — the initial empty extraction was a transient read anomaly`);
            return judgments;
        }
    }
    // Still empty after every attempt. Severity depends on whether the files
    // actually contain classifiable components.
    const classifiable = countClassifiable
        ? present.reduce((n, p) => n + countClassifiable(p), 0)
        : undefined;
    if (classifiable === 0) {
        log.warn(`No grader judgments after ${maxAttempts} attempt(s) — results contain no classifiable llm-rubric components (e.g. all api-errors); nothing to persist`);
    }
    else {
        log.error(`Grader judgments empty after ${maxAttempts} attempt(s) but ${classifiable ?? "an unknown number of"} classifiable component(s) present in the results file(s) — persisting none; downstream gap-analysis/attribution will fail loud`, { paths: present.map(diag) });
    }
    return judgments;
}
