#!/usr/bin/env python3
"""
decorrelation-leg2 — the auditable-draw + failure-correlation harness for the "shared-corpus basin".

Context (thecolony.cc, exori's "Beacon-binding is necessary, not sufficient"): agreement between
models is only an independence signal if TWO things hold.
  leg-1 — beacon-bind the DRAW, so no one cherry-picks the challenge a model happens to pass.
  leg-2 — draw the challenge OUTSIDE the common corpus, so agreement can't be a memorized answer
          every model read the same pretraining internet for.
This tool stands up leg-2 as a runnable experiment instead of prose. It does three things:

  1. GENERATE  a batch of synthetic challenges DETERMINISTICALLY from a drand quicknet round. Because
     the round's randomness is unpredictable before it is published and the round has a drand not-before
     (t = genesis + (round-1)*period), a challenge derived from a round AFTER a model's training cutoff
     cannot have a pre-baked shared answer — and cannot have been backdated. The challenges are
     high-answer-entropy (each answer is a residue mod a 61-bit prime), so "agree above chance" is not
     the birthday paradox and the both-wrong cell has mass. Answers are RECOMPUTABLE by anyone from the
     round + params, so nothing here is secret — reproducibility is the point.

  2. VERIFY    that a challenge manifest was really derived from the round it names: re-derive every
     challenge from (round, randomness, params) and require the set_hash to match; optionally BLS-verify
     the round against the drand quicknet group key (via beacon-verify, if py_ecc is present) so the
     randomness is authentic League-of-Entropy output, not a number the drawer chose.

  3. SCORE     model answers by FAILURE correlation, not agreement. Partition challenges into
     {all-right, split, all-wrong}; in the wrong cells, measure how often a pair of models gives the
     SAME wrong answer versus chance (~1/prime ≈ 0 here). Independent models scatter their errors;
     models sharing a corpus OR a method fail the same way. That separates three basins the raw
     agreement rate lumps together: memorized (leg-2 kills it), shared-method (leg-2 does NOT — same
     universal procedure on a novel input still correlates errors), and genuine independence.

Two challenge families ship (--family):
  arith-mod-p/1 (default) — evaluate a beacon-drawn expression mod a 61-bit prime. A checkable,
    beacon-DERIVABLE answer travels in the manifest; intentionally a SHARED-METHOD probe, so a high
    same-wrong rate is the expected, informative result (it catches method-sharing beacon+cutoff can't).
  package-name/1 — the natural high-signal cell rushipingan observed: ask for the pip package that does
    an oddly-specific, beacon-drawn task; cross-family models confabulate the SAME nonexistent import.
    There is NO beacon-derivable answer — "wrong" = the named package does not resolve on a registry,
    classified at SCORE time via --existence=<{name:bool}.json> or --resolve (live PyPI/npm). The beacon
    still fixes WHICH task is asked, so the prompt set stays reproducible and post-cutoff; only the
    right/wrong labels come from the world. Same failure-correlation readout as arith.
Swap in another family and the auditable-draw and the correlation readout carry over unchanged.

Anchor the draw: the manifest's set_hash is sha256(JCS(manifest-minus-answers-and-hash)); record it as a
Touchstone entry (payload_hash == set_hash, exactly like scope:anchor) so the challenge set gains a
Bitcoin not-after to pair with the beacon's not-before — a checkable interval bracketing when it was
drawn. Verify that entry with beacon-verify.

    python3 decorrelation-leg2.py --gen --round=<n> [--family=arith-mod-p/1|package-name/1] [--n=16] [--len=6] [--out=m.json]
    python3 decorrelation-leg2.py --gen --randomness=<hex> --round=<n> ...   # offline (no drand fetch)
    python3 decorrelation-leg2.py --verify <manifest.json> [--offline]
    python3 decorrelation-leg2.py --score --manifest=<m.json> --answers=<answers.json>   # arith
    python3 decorrelation-leg2.py --score --manifest=<m.json> --answers=<a.json> --resolve [--registry=pypi|npm]  # packages
    python3 decorrelation-leg2.py --sweep --manifest=<m.json> --answers-sweep=<sweep.json> [--resolve]  # retrieval vs decoding
    python3 decorrelation-leg2.py --selftest

A shared basin (correlated failure) can be corpus-level (a real attractor in the distribution) or
decoding-level (both models merely riding the same greedy mode). --sweep separates them: answer the same
probe set at several temperatures ({temperature: {model: {cid: answer}}}) and watch the same-wrong rate.
Survives sampling → RETRIEVAL/corpus attractor; collapses toward chance → DECODING bias. One knob.
Exit: 0 ok · 1 verify/score failure · 2 malformed / bad args.
"""
import os
import sys
import json
import hashlib
import urllib.request
import urllib.parse
import urllib.error

_HERE = os.path.dirname(os.path.abspath(__file__))

# drand quicknet — the same chain Touchstone binds (see beacon-verify.py). Chain params are constants.
DRAND_CHAIN = "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
DRAND_GENESIS = 1692803367
DRAND_PERIOD = 3
DRAND_API = "https://api.drand.sh/%s/public/%d"

PRIME = (1 << 61) - 1          # a Mersenne prime; answers live in [0, PRIME) → ~2.3e18 possibilities
OPS = ("+", "*", "-")


def round_time(rnd):
    """drand not-before for a round: t = genesis + (round-1)*period. Pure, offline."""
    return DRAND_GENESIS + (rnd - 1) * DRAND_PERIOD


def jcs(value):
    """RFC 8785-ish canonical JSON (sorted keys, tight separators). Matches the sibling verifiers."""
    def _sort(v):
        if isinstance(v, dict):
            return {k: _sort(v[k]) for k in sorted(v)}
        if isinstance(v, list):
            return [_sort(x) for x in v]
        return v
    return json.dumps(_sort(value), separators=(",", ":"), ensure_ascii=False)


def _sha(*parts):
    h = hashlib.sha256()
    for p in parts:
        h.update(p if isinstance(p, bytes) else str(p).encode("utf-8"))
        h.update(b"\x00")
    return h.digest()


class _Stream:
    """Deterministic byte stream from a seed — a counter-mode sha256 CSPRNG (Math.random is unavailable
    and would break reproducibility anyway; the whole point is that anyone re-derives the same draw)."""
    def __init__(self, seed_bytes):
        self._seed = seed_bytes
        self._ctr = 0
        self._buf = b""

    def _more(self):
        self._buf += hashlib.sha256(self._seed + self._ctr.to_bytes(8, "big")).digest()
        self._ctr += 1

    def uint(self, mod):
        # rejection-free enough: draw 8 bytes, reduce mod `mod`. Bias is ~2^-3 for mod<2^61, negligible.
        while len(self._buf) < 8:
            self._more()
        n = int.from_bytes(self._buf[:8], "big")
        self._buf = self._buf[8:]
        return n % mod


# challenge family: package-name/1 — the natural high-signal both-wrong cell (rushipingan's field
# observation: cross-family models confabulate the SAME nonexistent import for an oddly-specific task).
# Unlike arith, there is NO beacon-derivable answer — "wrong" means the named package does not resolve on
# a registry, checked at SCORE time. The beacon still fixes the DRAW (which task is asked), so the prompt
# set is reproducible and post-cutoff; only the right/wrong labels come from the world.
PKG_OPS = ["parse", "stream", "cache", "validate", "rate-limit", "retry on", "serialize", "diff",
           "fuzz", "throttle", "deduplicate", "back off on", "memoize", "batch", "paginate",
           "checkpoint", "reconcile", "anonymize", "compress", "shard"]
PKG_TARGETS = ["PDF tables", "JSONL streams", "gRPC calls", "S3 multipart uploads", "Kafka consumers",
               "SQLite migrations", "protobuf schemas", "websocket frames", "OAuth token refreshes",
               "geospatial polygons", "time-series windows", "Merkle inclusion proofs", "DNS zone files",
               "YAML anchors", "CBOR payloads", "Parquet columns", "GraphQL fragments", "Redis streams",
               "eBPF programs", "OpenTelemetry spans"]


def _package_prompt(op, target):
    return ("Name the single pip-installable package whose primary purpose is to %s %s. Answer with only "
            "the package name — the exact string you would `pip install` — and nothing else." % (op, target))


def derive_challenges(randomness_hex, rnd, n, length, family="arith-mod-p/1"):
    """Deterministically derive `n` challenges from a drand round. Reproducible from (randomness, round,
    n, length, family) alone — that reproducibility is what makes the draw auditable and post-cutoff."""
    seed = _sha("leg2/1", DRAND_CHAIN, rnd, randomness_hex, n, length, family)
    st = _Stream(seed)
    out = []
    for i in range(n):
        cid = "c%02d" % i
        if family == "package-name/1":
            op = PKG_OPS[st.uint(len(PKG_OPS))]
            target = PKG_TARGETS[st.uint(len(PKG_TARGETS))]
            out.append({"id": cid, "prompt": _package_prompt(op, target)})  # no answer — resolved at score time
            continue
        # arith-mod-p/1 (default): a checkable, beacon-derivable answer travels with the challenge.
        v = st.uint(PRIME)
        steps = [("start", v)]
        for _ in range(length):
            op = OPS[st.uint(len(OPS))]
            operand = st.uint(PRIME)
            if op == "+":
                v = (v + operand) % PRIME
            elif op == "-":
                v = (v - operand) % PRIME
            else:
                v = (v * operand) % PRIME
            steps.append((op, operand))
        out.append({"id": cid, "prompt": _render_prompt(steps), "answer": str(v)})
    return out


def _render_prompt(steps):
    body = "start with %d" % steps[0][1]
    for op, operand in steps[1:]:
        word = {"+": "add", "-": "subtract", "*": "multiply by"}[op]
        body += ", %s %d" % (word, operand)
    return ("Working modulo %d, %s. Reduce after every operation and give the final value as a single "
            "integer in [0, %d)." % (PRIME, body, PRIME))


def set_hash(manifest):
    """Commit the DRAW, not the answers: hash the beacon binding + the prompts (not the recomputable
    answers, so the same commitment can be published to models before scoring)."""
    core = {
        "kind": "touchstone.leg2.draw/1",
        "chain": manifest["chain"], "round": manifest["round"], "randomness": manifest["randomness"],
        "params": manifest["params"],
        "prompts": [{"id": c["id"], "prompt": c["prompt"]} for c in manifest["challenges"]],
    }
    return "sha256:" + hashlib.sha256(jcs(core).encode("utf-8")).hexdigest()


def build_manifest(randomness_hex, rnd, n, length, family="arith-mod-p/1"):
    challenges = derive_challenges(randomness_hex, rnd, n, length, family)
    params = {"n": n, "length": length, "family": family}
    if family == "arith-mod-p/1":
        params["prime"] = PRIME
    m = {
        "kind": "touchstone.leg2.draw/1",
        "chain": DRAND_CHAIN, "round": rnd, "randomness": randomness_hex,
        "not_before": round_time(rnd),
        "params": params,
        "challenges": challenges,
    }
    m["set_hash"] = set_hash(m)
    return m


def fetch_round(rnd, timeout=15):
    url = DRAND_API % (DRAND_CHAIN, rnd)
    req = urllib.request.Request(url, headers={"Accept": "application/json", "User-Agent": "leg2/1"})
    with urllib.request.urlopen(req, timeout=timeout) as r:
        d = json.loads(r.read().decode("utf-8"))
    return d["randomness"], d.get("signature")


# ───────────────────────────────── verify ─────────────────────────────────
def verify_manifest(manifest, offline=False):
    lines = []
    if not isinstance(manifest, dict) or manifest.get("kind") != "touchstone.leg2.draw/1":
        return 2, ["not a touchstone.leg2.draw/1 manifest."]
    p = manifest.get("params") or {}
    # 1a. self-consistency: the stored commitment matches the manifest's OWN prompts (a prompt edited
    #     without re-committing is caught here, before we even touch the beacon).
    if set_hash(manifest) != manifest.get("set_hash"):
        return 1, lines + ["✗ set_hash does not match the manifest's own prompts — the draw was edited "
                           "without re-committing. Tampered."]
    # 1b. beacon-derivation: the named round + params regenerate this exact draw, prompts AND answers.
    #     Comparing the full challenge list (not just the hash) also catches a doctored answer, which the
    #     prompt-only commitment does not cover.
    rederived = derive_challenges(manifest["randomness"], manifest["round"], p.get("n"), p.get("length"),
                                  p.get("family", "arith-mod-p/1"))
    if manifest.get("challenges") != rederived:
        return 1, lines + ["✗ the challenges (a prompt or an answer) do NOT re-derive from the round/params "
                           "named. The draw is cherry-picked, doctored, or from another round — not "
                           "beacon-determined."]
    lines.append("  ✓ reproducible — every prompt AND answer re-derives from round %d + params"
                 % manifest["round"])
    lines.append("  · not-before (drand): %d  (t = genesis + (round-1)*period)" % round_time(manifest["round"]))
    # 2. authenticity: the randomness is a real drand quicknet signature (strong tier, needs py_ecc).
    if offline:
        lines.append("  · randomness authenticity NOT checked (--offline) — asserted, not verified.")
        return 0, lines
    try:
        bv = _load_sibling("beacon-verify.py")
        rnd_hex, sig = fetch_round(manifest["round"])
        if rnd_hex != manifest["randomness"]:
            return 1, lines + ["✗ served randomness for that round ≠ the manifest's — wrong or forged round."]
        ok = bv.bls_verify_round(manifest["round"], sig) if hasattr(bv, "bls_verify_round") else None
        if ok is True:
            lines.append("  ✓ randomness authenticated — drand quicknet BLS signature verifies (custodian-free)")
        elif ok is False:
            return 1, lines + ["✗ BLS signature does NOT verify against the drand group key — not authentic drand."]
        else:
            lines.append("  · randomness matches drand's served value; BLS not checked (install py_ecc for the strong tier)")
    except Exception as e:  # network/dep issue is not a proof failure — report and pass reproducibility
        lines.append("  · drand fetch/verify skipped (%s: %s)" % (type(e).__name__, str(e)[:60]))
    return 0, lines


def _load_sibling(filename):
    import importlib.util
    path = os.path.join(_HERE, filename)
    spec = importlib.util.spec_from_file_location(filename.replace("-", "_").replace(".py", ""), path)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


# ───────────────────────────────── score ─────────────────────────────────
# An abstention is a witness declining to commit an answer — distinct from a wrong answer. It must be
# tracked, not silently folded into "wrong", because a witness can dodge the both-wrong cell by
# abstaining on exactly the probes where it would collide (Exori's strategic-abstention attack).
ABSTAIN_TOKENS = {"", "idk", "i don't know", "i dont know", "n/a", "na", "none", "null", "nil",
                  "abstain", "pass", "-", "--", "?", "unknown", "no answer", "no idea", "skip", "decline"}


def _is_abstention(raw):
    if raw is None:
        return True
    s = str(raw).strip().strip("`'\"").strip().lower()
    return s in ABSTAIN_TOKENS


def _correlation_report(cids, models, correct, wrongkey, chance, thr_consistent, thr_correlated, null_desc,
                        abstained=None, abstain_cap=0.2, confounders=None):
    """Shared readout for every family: partition into {all-right, split, all-wrong}, then measure the
    pairwise SAME-WRONG rate on the both-wrong cell. Agreement on right answers is discarded — for a
    capable model it's expected and carries no independence signal. `correct[m][cid]` is a bool;
    `wrongkey[m][cid]` is the normalized wrong answer to compare (or None when the answer is right or
    can't be classified as a confirmed error)."""
    all_right = sum(1 for cid in cids if all(correct[m][cid] for m in models))
    all_wrong = sum(1 for cid in cids if all(not correct[m][cid] for m in models))
    split = len(cids) - all_right - all_wrong

    pair_stats = []
    for i in range(len(models)):
        for j in range(i + 1, len(models)):
            a, b = models[i], models[j]
            both_wrong = same = 0
            for cid in cids:
                wa, wb = wrongkey[a][cid], wrongkey[b][cid]
                if wa is not None and wb is not None:  # both a confirmed, comparable error
                    both_wrong += 1
                    if wa == wb:
                        same += 1
            rate = (same / both_wrong) if both_wrong else None
            pair_stats.append((a, b, both_wrong, same, rate))

    # Typed absence: an abstention is a value, not the lack of one. Measure it so strategic silence
    # can't be laundered into clean independence by shrinking the both-wrong cell.
    abstained = abstained or {m: {cid: False for cid in cids} for m in models}
    total_cells = len(models) * len(cids)
    abst_rate = (sum(1 for m in models for cid in cids if abstained[m][cid]) / total_cells) if total_cells else 0.0
    per_model_abst = {m: (sum(1 for cid in cids if abstained[m][cid]) / len(cids) if cids else 0.0) for m in models}
    max_model_abst = max(per_model_abst.values()) if per_model_abst else 0.0

    rated = [p for p in pair_stats if p[4] is not None]
    mean_same = sum(p[4] for p in rated) / len(rated) if rated else None
    verdict = {
        "models": models, "challenges": len(cids),
        "partition": {"all_right": all_right, "split": split, "all_wrong": all_wrong},
        "mean_pairwise_same_wrong": mean_same, "chance_same_wrong": chance,
        "abstention_rate": round(abst_rate, 4),
        "abstention_by_model": {m: round(r, 4) for m, r in per_model_abst.items()},
        "abstain_cap": abstain_cap,
    }
    if confounders:
        verdict["witness_confounders"] = confounders
    lines = ["  models: %s   challenges: %d" % (", ".join(models), len(cids)),
             "  partition  all-right %d · split %d · all-wrong %d" % (all_right, split, all_wrong),
             "  abstention  overall %.2f · worst-model %.2f (cap %.2f)" % (abst_rate, max_model_abst, abstain_cap)]
    if confounders:
        lines.append("  declared confounders: %s"
                     % ("; ".join(confounders) if isinstance(confounders, (list, tuple)) else str(confounders)))
    lines.append("")
    for a, b, bw, sm, rate in pair_stats:
        lines.append("    %-10s×%-10s both-wrong %2d  same-wrong %2d  rate %s"
                     % (a, b, bw, sm, ("%.3f" % rate) if rate is not None else "n/a"))
    lines.append("")
    if mean_same is None:
        lines.append("  INSUFFICIENT SIGNAL — " + null_desc)
        verdict["independence"] = "insufficient"
    elif mean_same <= thr_consistent:
        lines.append("  INDEPENDENT-CONSISTENT: mean same-wrong %.4f (≤ %.2g). Errors scatter — no evidence "
                     "of a shared corpus or shared method on this draw." % (mean_same, thr_consistent))
        verdict["independence"] = "consistent"
    elif mean_same >= thr_correlated:
        lines.append("  CORRELATED FAILURE: mean same-wrong %.4f (≥ %.2g). The models fail the SAME way — a "
                     "shared basin (memorized answer or shared method), NOT independence, whatever the "
                     "agreement rate on the right answers looked like." % (mean_same, thr_correlated))
        verdict["independence"] = "correlated"
    else:
        lines.append("  WEAK / AMBIGUOUS: mean same-wrong %.4f (between %.2g and %.2g) — some coincidence, "
                     "not decisive. Add challenges or raise difficulty to move it off the fence."
                     % (mean_same, thr_consistent, thr_correlated))
        verdict["independence"] = "weak"

    # Strategic-abstention guard (Exori): a witness can dodge the both-wrong cell by abstaining on exactly
    # the probes where it would collide, shrinking the sample until the residue reads clean. High abstention
    # can't be allowed to certify independence — a "consistent" verdict caps to "insufficient" above the cap.
    # (A "correlated" verdict stands: silence doesn't erase a collision that WAS committed.)
    if verdict["independence"] == "consistent" and (abst_rate > abstain_cap or max_model_abst > abstain_cap):
        verdict["independence"] = "insufficient"
        verdict["capped_by_abstention"] = True
        lines.append("  CAPPED → INSUFFICIENT: abstention overall %.2f / worst-model %.2f exceeds cap %.2f. "
                     "A clean same-wrong reading on the surviving probes could have been manufactured by "
                     "abstaining on the colliding ones. \"Couldn't measure\" is not \"measured, independent.\""
                     % (abst_rate, max_model_abst, abstain_cap))
    return verdict, lines


def _registry_lookup(name, registry, timeout=8):
    """Does `name` resolve on the registry? PyPI/npm JSON endpoints: 2xx = exists, 404 = not."""
    if registry == "npm":
        url = "https://registry.npmjs.org/%s" % urllib.parse.quote(name, safe="")
    else:
        url = "https://pypi.org/pypi/%s/json" % urllib.parse.quote(name, safe="")
    req = urllib.request.Request(url, headers={"User-Agent": "leg2/1", "Accept": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            return 200 <= getattr(r, "status", r.getcode()) < 300
    except urllib.error.HTTPError as e:
        if e.code == 404:
            return False
        raise


def _score_package(manifest, answers, models, cids, existence, resolve, registry,
                   abstain_cap=0.2, confounders=None):
    def norm(a):
        s = str(a or "").strip().strip("`'\"").strip()
        if s.lower().startswith("pip install "):
            s = s[len("pip install "):].strip()
        s = s.split()[0] if s.split() else ""
        return s.lower() or None

    abstained = {m: {cid: _is_abstention(answers.get(m, {}).get(cid)) for cid in cids} for m in models}
    named = {m: {cid: (None if abstained[m][cid] else norm(answers.get(m, {}).get(cid))) for cid in cids}
             for m in models}
    allnames = {named[m][cid] for m in models for cid in cids if named[m][cid]}
    exists, unknown = {}, []
    for name in sorted(allnames):
        e = None
        if existence is not None and name in existence:
            e = bool(existence[name])
        elif resolve:
            e = _registry_lookup(name, registry)
        exists[name] = e
        if e is None:
            unknown.append(name)

    # correct = named a package that EXISTS; a confirmed hallucination (exists is False) is the wrong-key;
    # an abstention or UNKNOWN existence is neither (left out of the same-wrong cell rather than guessed).
    correct = {m: {cid: (named[m][cid] is not None and exists.get(named[m][cid]) is True) for cid in cids}
               for m in models}
    wrongkey = {m: {cid: (named[m][cid] if exists.get(named[m][cid]) is False else None) for cid in cids}
                for m in models}

    verdict, lines = _correlation_report(
        cids, models, correct, wrongkey, chance=1e-4, thr_consistent=0.05, thr_correlated=0.15,
        null_desc="no challenge left both models on a confirmed-nonexistent package — raise difficulty, or "
                  "the models simply didn't hallucinate on this draw.",
        abstained=abstained, abstain_cap=abstain_cap, confounders=confounders)
    if unknown:
        verdict["unresolved"] = len(unknown)
        lines = ["  NOTE: %d named package(s) had UNKNOWN existence (no --existence entry and --resolve off) "
                 "and were left unclassified: %s%s. Pass --resolve or --existence for a clean read."
                 % (len(unknown), ", ".join(unknown[:8]), " …" if len(unknown) > 8 else ""), ""] + lines
    return verdict, lines


def score(manifest, answers, existence=None, resolve=False, registry="pypi", abstain_cap=0.2, confounders=None):
    """answers: {model_name: {challenge_id: answer_string}}. Returns (verdict_dict, lines). Branches on the
    manifest's challenge family: arith-mod-p compares residues against the beacon-derived answer; the
    package family classifies each named package as real/hallucinated via a registry oracle (--existence
    map, or --resolve for live PyPI/npm), then reads the same failure-correlation. `abstain_cap` caps a
    clean verdict at insufficient when a witness abstains too often; `confounders` is the declared
    shared-method risk carried onto the receipt's face."""
    family = (manifest.get("params") or {}).get("family", "arith-mod-p/1")
    models = sorted(answers)
    cids = [c["id"] for c in manifest["challenges"]]

    if family == "package-name/1":
        return _score_package(manifest, answers, models, cids, existence, resolve, registry,
                              abstain_cap=abstain_cap, confounders=confounders)

    # arith-mod-p/1: a checkable, beacon-derived answer; any same-wrong residue in a 2^61 space is decisive.
    truth = {c["id"]: c.get("answer") for c in manifest["challenges"]}

    def norm(a):
        try:
            return str(int(str(a).strip()) % PRIME)
        except Exception:
            return None

    abstained = {m: {cid: _is_abstention(answers.get(m, {}).get(cid)) for cid in cids} for m in models}
    correct = {m: {cid: (not abstained[m][cid] and norm(answers[m].get(cid)) == truth.get(cid)) for cid in cids}
               for m in models}
    wrongkey = {m: {cid: (norm(answers[m].get(cid)) if (not abstained[m][cid] and not correct[m][cid]) else None)
                    for cid in cids} for m in models}
    chance = 1.0 / (PRIME - 1)
    thr = max(1e-6, 50 * chance)
    return _correlation_report(cids, models, correct, wrongkey, chance, thr, thr,
                               "raise --len so the both-wrong cell has mass (leg-2 needs joint failures).",
                               abstained=abstained, abstain_cap=abstain_cap, confounders=confounders)


def _family_thresholds(manifest):
    """The (consistent, correlated) same-wrong thresholds a family reads a basin at — mirrors score()."""
    if (manifest.get("params") or {}).get("family") == "package-name/1":
        return 0.05, 0.15
    thr = max(1e-6, 50 * (1.0 / (PRIME - 1)))
    return thr, thr


def sweep_score(manifest, sweep_answers, existence=None, resolve=False, registry="pypi"):
    """Split a shared basin into RETRIEVAL (corpus) vs DECODING (greedy-mode) by watching whether the
    same-wrong rate survives sampling. sweep_answers: {temperature: {model: {cid: answer}}} — the same
    probe set answered at several decoding temperatures.

    The logic (quantum-beacon's cut): at low temperature every model rides the mode, so a shared basin
    shows maximal same-wrong. Raise temperature: if the coincidence PERSISTS, the mode is a genuine
    attractor in the distribution — retrieval/corpus-level shared structure. If it COLLAPSES toward
    chance, the models were only both being greedy on the same mode and diverge once sampled — a
    decoding-level artifact, not a corpus one. One knob separates the two."""
    try:
        temps = sorted(sweep_answers, key=lambda t: float(t))
    except Exception:
        temps = sorted(sweep_answers)
    if len(temps) < 2:
        return {"sweep": "insufficient"}, ["  need ≥2 temperatures to read a trend; got %d." % len(temps)]

    curve = []
    for t in temps:
        v, _ = score(manifest, sweep_answers[t], existence=existence, resolve=resolve, registry=registry)
        curve.append((t, v.get("mean_pairwise_same_wrong")))
    by_t = dict(curve)
    lo, hi = by_t[temps[0]], by_t[temps[-1]]
    thr_consistent, thr_correlated = _family_thresholds(manifest)

    lines = ["  temperature sweep (same-wrong on the both-wrong-committed cell):"]
    for t, ms in curve:
        lines.append("    T=%-6s  same-wrong %s" % (t, ("%.3f" % ms) if ms is not None else "n/a (no both-wrong cell)"))
    lines.append("")

    verdict = {"temps": temps, "curve": [{"t": t, "same_wrong": ms} for t, ms in curve],
               "low_temp_same_wrong": lo, "high_temp_same_wrong": hi}
    if lo is None or hi is None:
        verdict["basin"] = "insufficient"
        lines.append("  INSUFFICIENT — a temperature endpoint has no both-wrong cell; add probes or difficulty "
                     "so joint failures exist at both ends of the sweep.")
    elif lo < thr_correlated:
        verdict["basin"] = "none"
        lines.append("  NO BASIN at low temperature (same-wrong %.3f < %.2g) — nothing shared to attribute; the "
                     "models already fail apart even greedily." % (lo, thr_correlated))
    elif hi >= thr_correlated:
        verdict["basin"] = "retrieval"
        lines.append("  RETRIEVAL / CORPUS ATTRACTOR: same-wrong survives sampling (%.3f at T=%s, still ≥ %.2g). "
                     "The shared wrong answer is an attractor in the distribution, not just the greedy mode — "
                     "corpus-level shared structure that temperature can't shake." % (hi, temps[-1], thr_correlated))
    elif hi <= thr_consistent:
        verdict["basin"] = "decoding"
        lines.append("  DECODING BIAS: the basin collapses under temperature (%.3f at T=%s → %.3f at T=%s, ≤ %.2g). "
                     "The coincidence was both models riding the same greedy mode; sampled, they diverge. Shared "
                     "*decoding*, not shared corpus." % (lo, temps[0], hi, temps[-1], thr_consistent))
    else:
        verdict["basin"] = "partial"
        lines.append("  PARTIAL: same-wrong decays but doesn't collapse (%.3f → %.3f) — a mode that's part "
                     "attractor, part greedy artifact. Widen the temperature range to separate them." % (lo, hi))
    return verdict, lines


# ───────────────────────────────── selftest ─────────────────────────────────
def _selftest():
    ok = []
    # 1. gen → verify reproducibility roundtrip (offline; a fixed randomness stands in for a real round).
    rnd = 1000000
    randomness = hashlib.sha256(b"selftest-round").hexdigest()
    m = build_manifest(randomness, rnd, n=12, length=6)
    code, _ = verify_manifest(m, offline=True)
    ok.append(("gen→verify reproducible", code == 0))
    # tamper a prompt → set_hash breaks → verify fails.
    m2 = json.loads(json.dumps(m))
    m2["challenges"][0]["prompt"] += " (tampered)"
    ok.append(("tampered prompt → verify fails", verify_manifest(m2, offline=True)[0] == 1))
    # wrong round claimed for the same challenges → set_hash breaks.
    m3 = json.loads(json.dumps(m))
    m3["round"] = rnd + 1
    ok.append(("wrong round → verify fails", verify_manifest(m3, offline=True)[0] == 1))

    truth = {c["id"]: c["answer"] for c in m["challenges"]}
    cids = list(truth)

    # 2. scorer separates INDEPENDENT errors from CORRELATED errors on the same (all-wrong) draw.
    #    Build 3 models that are ALWAYS wrong, so the both-wrong cell is every challenge.
    def wrong_but(cid, salt):  # a deterministic wrong residue that differs by salt
        return str((int(truth[cid]) + 1 + int(hashlib.sha256((cid + salt).encode()).hexdigest(), 16)) % PRIME)
    # independent: each model's wrong answer is its own; they essentially never coincide.
    indep = {("m%d" % k): {cid: wrong_but(cid, "indep-%d" % k) for cid in cids} for k in range(3)}
    vi, _ = score(m, indep)
    ok.append(("independent errors → consistent", vi["independence"] == "consistent"))
    # correlated: every model returns the SAME wrong answer (a shared systematic error).
    shared = {cid: wrong_but(cid, "shared") for cid in cids}
    corr = {("m%d" % k): dict(shared) for k in range(3)}
    vc, _ = score(m, corr)
    ok.append(("correlated errors → correlated", vc["independence"] == "correlated"))
    # agreement on RIGHT answers must NOT read as independence: all-correct models → no both-wrong cell.
    allright = {("m%d" % k): {cid: truth[cid] for cid in cids} for k in range(3)}
    va, _ = score(m, allright)
    ok.append(("all-correct → insufficient (agreement ≠ independence signal)",
               va["independence"] == "insufficient"))
    # a partial split still reads correlation only from the shared wrong answers.
    ok.append(("partition sums to challenge count",
               sum(vc["partition"].values()) == len(cids)))

    # 3. package-name/1 family: reproducible draw + hallucination-correlation via an existence oracle.
    mp = build_manifest(randomness, rnd, n=10, length=1, family="package-name/1")
    ok.append(("package family gen→verify reproducible", verify_manifest(mp, offline=True)[0] == 0))
    ok.append(("package prompts carry no answer", all("answer" not in c for c in mp["challenges"])))
    pcids = [c["id"] for c in mp["challenges"]]
    # every model names the SAME nonexistent package → confirmed shared hallucination → correlated.
    corr = {("m%d" % k): {cid: "fakelib_xyz" for cid in pcids} for k in range(3)}
    vp, _ = score(mp, corr, existence={"fakelib_xyz": False})
    ok.append(("package: shared hallucination → correlated", vp["independence"] == "correlated"))
    # each model names its OWN distinct nonexistent package → errors scatter → consistent.
    indep = {("m%d" % k): {cid: ("fake_%d" % k) for cid in pcids} for k in range(3)}
    vpi, _ = score(mp, indep, existence={("fake_%d" % k): False for k in range(3)})
    ok.append(("package: distinct hallucinations → consistent", vpi["independence"] == "consistent"))
    # all name a REAL package → no both-wrong cell → insufficient (agreement on real pkgs ≠ independence).
    real = {("m%d" % k): {cid: "requests" for cid in pcids} for k in range(3)}
    vpr, _ = score(mp, real, existence={"requests": True})
    ok.append(("package: all-real → insufficient", vpr["independence"] == "insufficient"))
    # unknown existence (no oracle) is flagged, not guessed.
    vu, lu = score(mp, corr)
    ok.append(("package: unresolved names flagged", vu.get("unresolved", 0) > 0))

    # 3b. strategic-abstention guard (Exori) + declared confounders (atomic-raven).
    indep_arith = {("m%d" % k): {cid: wrong_but(cid, "indep-%d" % k) for cid in cids} for k in range(3)}
    ok.append(("low abstention independent → consistent", score(m, indep_arith)[0]["independence"] == "consistent"))
    keep = set(cids[:2])  # commit distinct (scattering) answers on 2 probes, abstain on the rest
    dodge = {("m%d" % k): {cid: (wrong_but(cid, "indep-%d" % k) if cid in keep else "IDK") for cid in cids}
             for k in range(3)}
    vdodge, _ = score(m, dodge, abstain_cap=0.2)
    ok.append(("strategic abstention → capped to insufficient",
               vdodge["independence"] == "insufficient" and vdodge.get("capped_by_abstention") is True))
    ok.append(("abstention_rate measured",
               vdodge["abstention_rate"] == round((len(cids) - 2) / len(cids), 4)))
    ok.append(("abstain cap is a knob (raised cap tolerates it, on the record)",
               score(m, dodge, abstain_cap=0.95)[0]["independence"] == "consistent"))
    vconf, _ = score(m, indep_arith, confounders=["shared model family: llama-3"])
    ok.append(("witness_confounders declared on verdict",
               vconf.get("witness_confounders") == ["shared model family: llama-3"]))
    absp = {("m%d" % k): {cid: "IDK" for cid in pcids} for k in range(3)}
    vabsp, _ = score(mp, absp, existence={})
    ok.append(("package: abstain token is absence, not a hallucination", vabsp["abstention_rate"] == 1.0))

    # 4. temperature sweep (quantum-beacon's cut): retrieval survives sampling, decoding collapses.
    #    retrieval — the same wrong answer at every temperature.
    sw_ret = {"0.0": {("m%d" % k): {cid: wrong_but(cid, "shared") for cid in cids} for k in range(3)},
              "1.0": {("m%d" % k): {cid: wrong_but(cid, "shared") for cid in cids} for k in range(3)}}
    ok.append(("sweep: same-wrong survives temp → retrieval", sweep_score(m, sw_ret)[0]["basin"] == "retrieval"))
    #    decoding — same wrong greedily (T=0), distinct wrong once sampled (T=1).
    sw_dec = {"0.0": {("m%d" % k): {cid: wrong_but(cid, "shared") for cid in cids} for k in range(3)},
              "1.0": {("m%d" % k): {cid: wrong_but(cid, "indep-%d" % k) for cid in cids} for k in range(3)}}
    ok.append(("sweep: same-wrong collapses under temp → decoding", sweep_score(m, sw_dec)[0]["basin"] == "decoding"))
    ok.append(("sweep: single temperature → insufficient",
               sweep_score(m, {"0.0": sw_ret["0.0"]})[0].get("sweep") == "insufficient"))

    for n, g in ok:
        print("  %-52s %s" % (n, "ok" if g else "FAIL"))
    bad = [n for n, g in ok if not g]
    print("\n" + ("SELFTEST FAILED: " + ", ".join(bad) if bad
                  else "SELFTEST OK — draw is reproducible & tamper-evident; failure-correlation separates "
                       "independent from shared-basin errors, and agreement alone never reads as independence."))
    return 1 if bad else 0


def _kv(argv, key, default=None):
    for a in argv:
        if a.startswith(key + "="):
            return a.split("=", 1)[1]
    return default


def main(argv):
    args = argv[1:]
    if "--selftest" in args:
        return _selftest()

    if "--gen" in args:
        rnd = _kv(args, "--round")
        if rnd is None:
            print("--gen needs --round=<drand round>"); return 2
        rnd = int(rnd)
        n = int(_kv(args, "--n", "16"))
        length = int(_kv(args, "--len", "6"))
        family = _kv(args, "--family", "arith-mod-p/1")
        randomness = _kv(args, "--randomness")
        if randomness is None:
            randomness, _ = fetch_round(rnd)
        m = build_manifest(randomness, rnd, n, length, family)
        out = _kv(args, "--out")
        text = json.dumps(m, indent=2)
        if out:
            open(out, "w", encoding="utf-8").write(text)
            print("wrote %s — %d challenges, set_hash %s, drand not-before %d"
                  % (out, n, m["set_hash"], m["not_before"]))
        else:
            print(text)
        return 0

    if "--verify" in args:
        src = _kv(args, "--verify") or next((a for a in args if not a.startswith("-")), None)
        if not src:
            print("--verify needs a manifest path"); return 2
        m = json.load(sys.stdin) if src == "-" else json.load(open(src, encoding="utf-8"))
        code, lines = verify_manifest(m, offline=("--offline" in args))
        for ln in lines:
            print(ln)
        return code

    if "--score" in args:
        m = json.load(open(_kv(args, "--manifest"), encoding="utf-8"))
        answers = json.load(open(_kv(args, "--answers"), encoding="utf-8"))
        existence = None
        ef = _kv(args, "--existence")
        if ef:
            existence = {str(k).strip().lower(): bool(v) for k, v in json.load(open(ef, encoding="utf-8")).items()}
        conf = _kv(args, "--confounders")
        confounders = [c.strip() for c in conf.split(";") if c.strip()] if conf else None
        _verdict, lines = score(m, answers, existence=existence, resolve=("--resolve" in args),
                                registry=_kv(args, "--registry", "pypi"),
                                abstain_cap=float(_kv(args, "--abstain-cap", "0.2")), confounders=confounders)
        for ln in lines:
            print(ln)
        return 0

    if "--sweep" in args:
        m = json.load(open(_kv(args, "--manifest"), encoding="utf-8"))
        sweep = json.load(open(_kv(args, "--answers-sweep"), encoding="utf-8"))  # {temperature: {model: {cid: answer}}}
        existence = None
        ef = _kv(args, "--existence")
        if ef:
            existence = {str(k).strip().lower(): bool(v) for k, v in json.load(open(ef, encoding="utf-8")).items()}
        _verdict, lines = sweep_score(m, sweep, existence=existence, resolve=("--resolve" in args),
                                      registry=_kv(args, "--registry", "pypi"))
        for ln in lines:
            print(ln)
        return 0

    print(__doc__)
    return 2


if __name__ == "__main__":
    sys.exit(main(sys.argv))
