/**
 * @sanity/ailf-core — Report-validity detector (D0059)
 *
 * The detection brain for the report trustworthiness model. A pure, versioned,
 * I/O-free rules engine that computes both trustworthiness axes for a report —
 * intent (`classification`, D0037) and data health (`validity`) — with a
 * confidence tier so it never mislabels ambiguous data.
 *
 * Reused unchanged by the eval write path (`W-stamp-validity-write-path`,
 * stamps every new report) and the one-shot backfill
 * (`W-backfill-report-validity`, labels the historical store).
 *
 * **Confidence tiers (D0059 §2):**
 *
 *   - **Tier 1 — auto.** High-confidence, unambiguous rules. Intent:
 *     `scheduled + github-actions → official`; `testing-ground` team /
 *     `env-override` source / generated-id executor → `test`/`experimental`;
 *     de-drift `ad-hoc → adhoc`. Validity: `degraded → "degraded"`.
 *   - **Tier 2 — review list.** Anything resting on shape-sensitive content
 *     signals (empty `testResults`, `testCount` mismatch) is *emitted to a
 *     review list rather than guessed* — 34%/66% of the historical store hits
 *     these and is dominated by report-shape evolution (GCS-externalized
 *     per-test data, per-model granularity), NOT defects. The engine never
 *     auto-flags them invalid; a human confirms in batch and the verdict is
 *     written `method:"manual"`.
 *
 * A `method:"manual"` verdict is authoritative — re-running the detector never
 * overwrites it.
 *
 * @see docs/decisions/D0059-report-validity-axis-and-trustworthiness-gate.md
 * @see docs/design-docs/report-trustworthiness-model.md
 */
import { isKnownExecutorIdentity, looksLikeGeneratedExecutorId, normalizeOwnerTeam, normalizeRunClassification, } from "../../ailf-shared/index.js";
/**
 * Detector ruleset version. Bump on any rule or threshold recalibration so a
 * later re-assessment is a comparable, auditable re-run (D0059 §2). Stamped
 * onto every `validity` verdict the engine produces.
 */
export const REPORT_VALIDITY_RULESET_VERSION = "1.0.0";
/**
 * Tier-1 intent detection. Returns a `RunClassification` only when the engine
 * has a positive opinion; `undefined` means "leave the existing value".
 */
function detectIntent(provenance) {
    const { classification, executor, owner, source, trigger } = provenance;
    // Highest-confidence positive: a scheduled run executed by GitHub Actions is
    // the canonical official series.
    if (trigger.type === "scheduled" &&
        executor.type === "system" &&
        executor.name === "github-actions") {
        return "official";
    }
    // Explicit non-canonical contexts.
    if (normalizeOwnerTeam(owner.team) === "testing-ground")
        return "test";
    if (source.name === "env-override")
        return "experimental";
    if (executor.type === "user" &&
        !isKnownExecutorIdentity(executor.name) &&
        looksLikeGeneratedExecutorId(executor.name)) {
        return "experimental";
    }
    // No rule fired — de-drift the existing value, returning it only when it
    // actually changes.
    const normalized = normalizeRunClassification(classification);
    return normalized === classification ? undefined : normalized;
}
/**
 * Tier-2 review flags. Surfaces shape-sensitive signals for human batch-review
 * — it never decides validity itself. Skipped entirely for already-`degraded`
 * reports, where missing/empty test data is already explained.
 */
function collectReviewFlags(summary) {
    const flags = [];
    const testResultCount = summary.testResults?.length ?? 0;
    const summedTestCount = (summary.scores ?? []).reduce((total, score) => total + (score.testCount ?? 0), 0);
    if (summedTestCount > 0 && testResultCount === 0) {
        flags.push({
            reason: "empty-test-results",
            detail: `${summedTestCount} scored tests but testResults is empty — may be GCS-externalized (W0329) rather than a defect; needs human review.`,
        });
    }
    else if (testResultCount > 0 &&
        summedTestCount > 0 &&
        testResultCount !== summedTestCount) {
        flags.push({
            reason: "test-count-mismatch",
            detail: `testResults length (${testResultCount}) ≠ summed testCount (${summedTestCount}) — per-model/sampling granularity difference, not necessarily a defect.`,
        });
    }
    return flags;
}
/**
 * Assess a report's trustworthiness — its intent (`classification`) and data
 * health (`validity`) — plus any Tier-2 signals that need human review.
 *
 * Pure and deterministic: same input + `assessedAt` → identical output.
 */
export function assessReportValidity(report, options) {
    const classification = detectIntent(report.provenance);
    // A human verdict is authoritative — never overwrite a manual data-health
    // assessment on a re-run. Intent normalization is still surfaced: it is
    // idempotent and orthogonal to the data-health review.
    if (report.validity?.method === "manual") {
        return { classification, validity: report.validity, reviewFlags: [] };
    }
    // Tier-1 validity: the only auto-assigned non-`ok` status is `degraded`
    // (subsumes the legacy flag). `incomplete`/`suspect` rest on the shape
    // caveat and require human review — they are written `method:"manual"`.
    const reasons = [];
    let status = "ok";
    if (report.degraded) {
        status = "degraded";
        reasons.push(`degraded:${report.degraded.reason}`);
    }
    const validity = {
        status,
        reasons,
        method: "auto",
        rulesetVersion: REPORT_VALIDITY_RULESET_VERSION,
        assessedAt: options.assessedAt,
    };
    return {
        classification,
        validity,
        reviewFlags: status === "degraded" ? [] : collectReviewFlags(report.summary),
    };
}
