# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-when-litmus-derived-adversarial-suites-silently-lose-discriminative-power-after-skill-injection.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-when-litmus-derived-adversarial-suites-silently-lose-discriminative-power-after-skill-injection.warrant.json
# Markdown SHA-256:   059b262adc197a3449439096cf391d70bac91703838f910fee484bcbf5d8ecac
# Code AST SHA-256:   342204fbaf55803148fe9a777c8c4a0173efd4beed503561acd0c7d4ba2dac2e
# Verified Classes:   SuiteDiscriminativePowerError, SuiteHealthReport
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from collections import Counter
from math import log
from typing import Iterable, Sequence


class SuiteDiscriminativePowerError(RuntimeError):
    """Raised when a LITMUS-derived suite no longer separates skill from no-skill."""


@dataclass(frozen=True)
class SuiteHealthReport:
    family_entropy: float
    cross_seed_jaccard: float
    skill_control_delta: float
    verdict: str


def _entropy(counts: Iterable[int]) -> float:
    counts = [c for c in counts if c > 0]
    total = sum(counts)
    if total == 0:
        return 0.0
    return -sum((c / total) * log(c / total) for c in counts)


def _jaccard(a: Sequence[str], b: Sequence[str]) -> float:
    sa, sb = set(a), set(b)
    if not sa and not sb:
        return 1.0
    return len(sa & sb) / len(sa | sb)


def assess_suite(
    failing_families: Sequence[str],
    failing_seed_a: Sequence[str],
    failing_seed_b: Sequence[str],
    skill_pass_rate: float,
    control_pass_rate: float,
    min_detectable_effect: float = 0.05,
) -> SuiteHealthReport:
    if not failing_families:
        raise SuiteDiscriminativePowerError(
            "No failing cases recorded: suite has zero discriminative signal."
        )

    entropy = _entropy(Counter(failing_families).values())
    jaccard = _jaccard(failing_seed_a, failing_seed_b)
    delta = skill_pass_rate - control_pass_rate

    if entropy < 0.8 or delta < min_detectable_effect:
        verdict = "retire"
    elif entropy < 1.5 or jaccard > 0.6:
        verdict = "reweight"
    else:
        verdict = "keep"

    if verdict == "retire":
        raise SuiteDiscriminativePowerError(
            f"Suite collapsed: entropy={entropy:.2f} nats, "
            f"jaccard={jaccard:.2f}, delta={delta:.3f}. "
            "Regenerate from base-model failure modes before gating."
        )

    return SuiteHealthReport(
        family_entropy=entropy,
        cross_seed_jaccard=jaccard,
        skill_control_delta=delta,
        verdict=verdict,
    )
