// ─── ADR-112 §5.3/§6/§8 — the AUTHORED window-tunnel scorer (slice D3) ────────
//
// The sibling scorer for an AUTHORED rule window. Per strategy's Q1 ruling
// (2026-07-01) it REDUCES to the mined `scoreWindtunnel`, it does NOT fork it — a
// fork would violate the §5.3 content-blind-scorer invariant + §9 one-PASS/FAIL-
// meaning (contract-illegal). The mined cascade is reused BYTE-UNCHANGED; this
// layer only (a) normalizes the §6 authored controls into a `ScorerInput`, (b)
// applies a pre-scorer non-emission gate, and (c) appends a verdict-inert O3
// generalization metric.
//
// It is INERT like D1–D2.6: nothing here is wired into a cert run. The CLI
// `spine-windtunnel` command stays on the mined scorer until slice D4 flips the
// authored path reachable (D4 owes the whole-path couple-on-merge — scorer + the
// #793 no-mint gate + the §6 deriver, end-to-end). D3 is the pure inert core,
// offline-unit-tested in `@mmnto/core`.
//
// The three reduction moves (converged 4-seat panel, design in
// `.totem/specs/d3-authored-scorer.md`):
//   1. [REVISED by the option-(i) ruling, operator 2026-07-04 (#2291) — the D3
//      projection was structurally unsatisfiable]: the §4 preimage-differential
//      recorded AT EMISSION is the authored exercise + non-vacuity proof — only
//      `differential-holds` fixtures reach `positive[]` (discharged in
//      authored-controls.ts). The mined target-must-FIRE machinery is the MINED
//      sibling's proof and is never consulted here: authored anchors may be
//      strictly pre-window (§5.2), whose diffs are not window substrate BY
//      DESIGN, so a projected target could never fire (every authored run would
//      vacuous-FAIL). `positiveControlsExercised` := |authoredControls.positive|
//      (the §6 emission channel); `positiveControlTargets` passed to the mined
//      cascade is [] — its Step-3 check is vacuously true, and the authored
//      vacuity guard is Move 2 below plus the exercised floor.
//   2. Non-emission gate (codex's load-bearing fold): a culled differential (empty
//      `positive[]` + a recorded `nonEmissions[]`) must NOT reach the mined scorer
//      as `positiveControlTargets: []` and silently PASS. An `illegitimate` class
//      is a Gate-1 FAIL-equivalent; `undecidable`/`deferred` is not-certifiable
//      (HONEST-NEGATIVE). The gate is DEMOTE-ONLY (mirrors the mined "a guard may
//      demote a would-be PASS, never upgrade a FAIL") and PER-non-emission (a
//      holding control on one fixture does not launder an illegitimate sibling).
//   3. O3 metric (verdict-inert post-pass): `heldOutActivationsByRule` — per
//      authored rule under test, the count of its non-control (`corpus`) firings on
//      held-out PRs. Keys are JOIN-BACK-derived from the positive controls'
//      `targetRuleId` (Tenet-20; a firing-only derivation would drop a rule with
//      zero held-out activations — the rare-defect case). Strategy Q2: the metric
//      is NEVER consulted for the verdict, and the Gate-2-eligible SET derivation
//      is DEFERRED to D4 (D3 emits only the raw metric).
import { scoreWindtunnel } from './windtunnel-scorer.js';
// ─── Verdict-severity ladder (demote-only combination) ───────────────────────
/** PASS < HONEST-NEGATIVE < FAIL. The gate may only raise severity, never lower it. */
const VERDICT_SEVERITY = {
    PASS: 0,
    'HONEST-NEGATIVE': 1,
    FAIL: 2,
};
// ─── Public API ─────────────────────────────────────
/**
 * Score an AUTHORED wind-tunnel run. Pure function: no IO, no clock, no
 * randomness, byte-identical across re-runs for identical inputs (Tenet-15).
 * Reduces to `scoreWindtunnel` after normalizing the §6 controls, applies the
 * non-emission gate (demote-only), and appends the verdict-inert O3 metric.
 */
export function scoreAuthoredWindtunnel(input) {
    const { firings, groundTruth, mintedRuleIds, cullRateThreshold, exposureFloors, actualExposure, authoredControls, heldOutPrs, } = input;
    // Move 1 (option-(i) ruling) — the §6 emission channel IS the exercise proof:
    // `positiveControlsExercised` = |positive[]| (each entry is differential-held
    // by construction). The input's exercised leg is a mined-lane artifact of the
    // shared `ScorerInput` shape and is OVERRIDDEN here — the authored scorer owns
    // this leg (Tenet-20 single source; a caller-supplied count would be a second
    // home). No targets go to the mined cascade: its fire-on-window-diff proof is
    // unsatisfiable for pre-window anchors (§5.2) — the run-level `nonVacuity`
    // field therefore reads vacuously true on an authored verdict; the authored
    // vacuity guards are the Move-2 non-emission gate + the exercised floor.
    const mined = scoreWindtunnel({
        firings,
        groundTruth,
        positiveControlTargets: [],
        mintedRuleIds,
        cullRateThreshold,
        exposureFloors,
        actualExposure: {
            ...actualExposure,
            positiveControlsExercised: authoredControls.positive.length,
        },
    });
    // Move 2 — the non-emission gate. Tally the classes, then combine the gate's
    // implied verdict with the mined verdict on the severity ladder (demote-only).
    let illegitimate = 0;
    let undecidable = 0;
    let deferred = 0;
    for (const ne of authoredControls.nonEmissions) {
        switch (ne.class) {
            case 'illegitimate':
                illegitimate += 1;
                break;
            case 'undecidable':
                undecidable += 1;
                break;
            case 'deferred':
                deferred += 1;
                break;
            default: {
                // Exhaustiveness guard (Tenet-4; mirrors authored-controls.ts's
                // NON_EMISSION_CLASS_BY_OUTCOME Record-over-Exclude). A future 4th class
                // breaks THIS build instead of silently laundering into `deferred` → a
                // spurious HONEST-NEGATIVE. The Zod boundary rejects it at runtime; the
                // `never` binding makes a schema addition a COMPILE error here.
                const _exhaustive = ne.class;
                throw new Error(`[Totem Error] scoreAuthoredWindtunnel: unhandled non-emission class '${String(_exhaustive)}'`);
            }
        }
    }
    // The gate's implied verdict: an illegitimate control is a Gate-1 FAIL-equivalent
    // (outranks undecidable/deferred); an undecidable/deferred control is not-
    // certifiable → HONEST-NEGATIVE. No blocking non-emission ⇒ no constraint.
    let gateKind = null;
    let gateLabel = null;
    if (illegitimate > 0) {
        gateKind = 'FAIL';
        gateLabel = 'fail-illegitimate';
    }
    else if (undecidable + deferred > 0) {
        gateKind = 'HONEST-NEGATIVE';
        gateLabel = 'honest-negative-not-certifiable';
    }
    // Demote-only: the gate applies ONLY when strictly more severe than the mined
    // verdict. When equal (e.g. an illegitimate non-emission co-occurring with a real
    // FP FAIL), the mined verdict is kept — a structural control defect does not erase
    // a real FP measurement (build-altitude micro-decision, flagged for strategy).
    let verdict = mined;
    let effect = 'none';
    if (gateKind !== null && VERDICT_SEVERITY[gateKind] > VERDICT_SEVERITY[mined.verdict]) {
        // A gate demotion is a structural, no-claim verdict → precision null (#2189):
        // a 0 would falsely read as an all-FP measurement.
        verdict = { ...mined, verdict: gateKind, precision: null };
        effect = gateLabel;
    }
    // Move 3 — the verdict-inert O3 metric. Keys are the authored rules under test
    // (join-back from the positive controls), minus any culled rule. Values count each
    // rule's non-control (`corpus`) firings on held-out PRs — 0 is a valid observable
    // (the rare-defect case), so a keyed rule always appears even with no activation.
    const culled = new Set(mined.cullLedger.map((e) => e.ruleId));
    const heldOutActivationsByRule = {};
    for (const c of authoredControls.positive) {
        if (culled.has(c.targetRuleId))
            continue; // (d) cull symmetry — out of the metric AND its keys
        if (!Object.hasOwn(heldOutActivationsByRule, c.targetRuleId)) {
            heldOutActivationsByRule[c.targetRuleId] = 0;
        }
    }
    for (const f of firings) {
        if (f.controlKind !== 'corpus')
            continue; // (c) a rule's own control firing is excluded
        if (!heldOutPrs.has(f.pr))
            continue; // held-out slice only
        if (culled.has(f.ruleId))
            continue; // (d) culled rule contributes nothing
        if (!Object.hasOwn(heldOutActivationsByRule, f.ruleId))
            continue; // only rules with a positive control
        heldOutActivationsByRule[f.ruleId] += 1;
    }
    return {
        ...verdict,
        heldOutActivationsByRule,
        authoredControlGate: { illegitimate, undecidable, deferred, effect },
    };
}
/**
 * The AUTHORED sibling of `computePerRuleControlResults` (option-(i) ruling,
 * operator 2026-07-04 / #2291): a rule's per-rule positive-control verdict is
 * the §4 preimage-differential HELD AT EMISSION — the rule has ≥1 entry in
 * `authoredControls.positive[]` (only `differential-holds` fixtures reach it) —
 * never fire-on-window-diff (pre-window anchors have no window substrate by
 * §5.2 design). The negative leg and the cull axis are IDENTICAL to the mined
 * sibling: a rule firing on any negative control is culled (not a survivor,
 * never stamped); survivors fired on none, so `negativeControl` is invariantly
 * true. `evidenceRefs` carry §6-emission locus refs (`§6-emission:` prefixed) —
 * record-only, deliberately NOT shaped like 64-hex firing labelIds so they can
 * never be mistaken for (or joined against) the ground-truth key space.
 */
export function computeAuthoredPerRuleControlResults(input) {
    const { firings, mintedRuleIds, authoredControls } = input;
    const culled = new Set();
    for (const f of firings) {
        if (f.controlKind === 'negative')
            culled.add(f.ruleId);
    }
    const emissionRefsByRule = new Map();
    for (const c of authoredControls.positive) {
        const refs = emissionRefsByRule.get(c.targetRuleId) ?? [];
        refs.push(`§6-emission:${c.pr}:${c.filePath}:${c.matchedSpan}`);
        emissionRefsByRule.set(c.targetRuleId, refs);
    }
    const result = new Map();
    for (const ruleId of mintedRuleIds) {
        if (culled.has(ruleId))
            continue; // not a survivor — never stamped (mined parity)
        const refs = emissionRefsByRule.get(ruleId) ?? [];
        result.set(ruleId, {
            positiveControl: refs.length > 0,
            negativeControl: true,
            evidenceRefs: refs,
        });
    }
    return result;
}
//# sourceMappingURL=windtunnel-scorer-authored.js.map