"""RUN-007 - Round 04: the candidate-cover compiler.

Round 04 makes the cover itself finite: a universal cover holds a disc of radius
1/2 (so it can be anchored), an area bound confines it to a disc of radius
R = 2A-bar, and upward-quantized support heights on M directions give an outer
polygon P_{M,q}(U) with explicit Hausdorff error eps_{M,q} and area inflation
beta_{M,q}; the finite polygon dictionary brackets a_Leb within beta. The
theorems are read in the report; here the outer polygon is built by this tree's
own halfplane clipping on test covers, and every finite claim recomputed.

Checks
  A0 anchors      the formulas below occur in Round 04 as written
  A1 citations    its references (all verified before) still match
  C1 radius       Thm 4.1: the triangle conv{x, (0,r), (0,-r)} has area r|x|; R = 2 A-bar
  C2 containment  Thm 8.1: U inside P_{M,q}(U) on three test covers
  C3 eps-derived  Thm 10.1's eps = q + (R + R_P) d_M assembled from the proof's three steps
  C4 eps-measured the measured Hausdorff excess of P over U is within eps (global and true R)
  C5 area         Thm 12: area inflation within beta; sec. 22: Steiner's formula for P + gamma B
  C6 dictionary   the level set reaches R + q, so every quantized cover is a dictionary member
  C7 sanity       sec. 33 and the package's VALIDATION.json, from this tree's own polygon
  C8 package      the package's self-description of its documents matches the archive
  R1 registry     Round 04's judgments reviewed, first-used statuses defined

Usage: python code/run007_round04_cover_compiler.py [--refresh] [--drill] [--blind CID]
"""
import argparse
import datetime
import json
import math

import numpy as np
import sympy as sp

import gate
import registry
from corpus import SOURCES, TREE, source_bytes, walk_sources

PKG = "main.zip!/AMRAL_LUC_FC_Round_04_Package_v0.1.zip!/"
DOC = PKG + "AMRAL_Lebesgue_Finite_Closure_Round_04_Candidate_Cover_Compiler_v0.1.md"
CRYSTAL = PKG + "AMRAL_LUC_FC_State_Crystal_Round_04_v0.1.md"
VALIDATION = PKG + "AMRAL_LUC_FC_Round_04_VALIDATION.json"
# The lagged AI's audit of Round 04 lives in the DLMVC track, not in this package. It is
# deliberately not read here: this round is done blind to it, and compared when DLMVC is read.
SNAPSHOT = TREE / "data" / "external" / "round04-citations.json"
LOG = TREE / "data" / "gate-logs" / "run007-round04-cover-compiler.json"
MANIFEST = TREE / "data" / "SOURCE_MANIFEST.v1.json"
ABAR = 0.8440935944

ANCHORS = {
    "C1": [r"U\subseteqB_{A/r}(0).", r"\operatorname{Area}(T)=rR_x.", r"R=1.6881871888."],
    "C3": [r"\varepsilon_{M,q}=q+(R+R_P)d_M.", r"R_P:=\frac{R+q}{\cos\Delta}.", r"d_M=2\sin\frac{\Delta}{2}."],
    "C5": [r"\beta_{M,q}=2\piR\varepsilon_{M,q}+\pi\varepsilon_{M,q}^2.",
           r"\operatorname{Area}(P^\gamma)\le\operatorname{Area}(P)+2\piR_P\gamma+\pi\gamma^2."],
    "C6": [r"K_q=\left\lceil\frac{R-\frac12}{q}\right\rceil+1.", r"|\mathscrP_{M,q}|\le(K_q+1)^M."],
    "C7": [r"h_U(\theta)=0.515+0.01\cos2\theta.", r"M=128,\qquadq=5\times10^{-4}"],
}
REFS = {1: {"kind": "arxiv", "id": "1502.01251"}, 2: {"kind": "arxiv", "id": "1810.10089"},
        3: {"kind": "arxiv", "id": "2608.30538"}, 4: {"kind": "arxiv", "id": "2609.01284"},
        5: {"kind": "crossref", "doi": "10.1017/CBO9781139003858"}}
CITE_OPTS = {1: {}, 2: {}, 3: {}, 4: {}, 5: {}}


# ---------------------------------------------------------------- geometry, this tree's own

def trig_cover(a0, modes):
    """h = a0 + sum_n (a_n cos n t + b_n sin n t); returns (h, h', exact area)"""
    h = lambda t: a0 + sum(a * np.cos(n * t) + b * np.sin(n * t) for n, (a, b) in modes.items())
    dh = lambda t: sum(-n * a * np.sin(n * t) + n * b * np.cos(n * t) for n, (a, b) in modes.items())
    area = math.pi * a0 ** 2 + (math.pi / 2) * sum((1 - n * n) * (a * a + b * b) for n, (a, b) in modes.items())
    return h, area


def hexagon():
    """Pal's regular hexagon of inradius 1/2 (width 1), edges normal to k pi/3"""
    V = np.array([[math.cos(math.pi / 6 + k * math.pi / 3), math.sin(math.pi / 6 + k * math.pi / 3)]
                  for k in range(6)]) / math.sqrt(3)
    return (lambda t: np.max(V @ np.stack([np.cos(t), np.sin(t)]), axis=0)), math.sqrt(3) / 2


def clip(poly, n, b):
    """Sutherland-Hodgman: keep the part of poly with x.n <= b"""
    out = []
    for i in range(len(poly)):
        p, q = poly[i], poly[(i + 1) % len(poly)]
        fp, fq = p @ n - b, q @ n - b
        if fp <= 0:
            out.append(p)
        if (fp < 0 < fq) or (fq < 0 < fp):
            out.append(p + (q - p) * (fp / (fp - fq)))
    return np.array(out)


def outer_polygon(h, M, q, quantize):
    th = 2 * np.pi * np.arange(M) / M
    hj = h(th)
    bj = quantize(hj, q)
    big = 100.0
    poly = np.array([[-big, -big], [big, -big], [big, big], [-big, big]])
    for t, b in zip(th, bj):
        poly = clip(poly, np.array([math.cos(t), math.sin(t)]), b)
    return poly, hj, bj


def up(hj, q):
    return 0.5 + q * np.ceil((hj - 0.5) / q - 1e-12)


def shoelace(P):
    x, y = P[:, 0], P[:, 1]
    return 0.5 * abs(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1)))


def perimeter(P):
    return float(np.sum(np.linalg.norm(P - np.roll(P, -1, axis=0), axis=1)))


def support_of(P, t):
    return np.max(P @ np.stack([np.cos(t), np.sin(t)]), axis=0)


def excess(P, h, n=400000):
    t = 2 * np.pi * np.arange(n) / n
    return float(np.max(support_of(P, t) - h(t)))


def eps(R, M, q):
    D = math.pi / M
    return q + (R + (R + q) / math.cos(D)) * 2 * math.sin(D / 2)


def beta(R, e):
    return 2 * math.pi * R * e + math.pi * e * e


def test_covers():
    h33, a33 = trig_cover(0.515, {2: (0.01, 0.0)})
    hx, ax = hexagon()
    hc, ac = trig_cover(0.53, {2: (0.01 * math.cos(0.3), -0.01 * math.sin(0.3)),
                               3: (0.006 * math.cos(1.1), -0.006 * math.sin(1.1))})
    return {"sec33": (h33, a33), "hexagon": (hx, ax), "chiral": (hc, ac)}


def true_radius(h):
    t = np.linspace(0, 2 * np.pi, 200001)
    return float(np.max(h(t))) + (2 * np.pi / 200000) / 2        # h is 1-Lipschitz in angle here


# ---------------------------------------------------------------- checks

X, R_, r_ = sp.symbols("x R r", positive=True)


def c1_radius(w):
    out = []
    # triangle with vertices (x, 0), (0, r), (0, -r): area by the shoelace formula
    V = [(X, 0), (0, r_), (0, -r_)]
    area = sp.Abs(sum(V[i][0] * V[(i + 1) % 3][1] - V[(i + 1) % 3][0] * V[i][1] for i in range(3))) / 2
    if sp.simplify(area - w["triangle_area"](r_, X)) != 0:
        out.append(f"triangle area is {sp.simplify(area)}")
    if abs(w["R_global"] - 2 * ABAR) > 1e-15 or f"{2 * ABAR:.10f}" != "1.6881871888":
        out.append(f"R = 2 A-bar is {2 * ABAR!r}, claimed {w['R_global']}")
    # random convex bodies holding B_{1/2}(0): hull of the disc and random points
    rng = np.random.default_rng(424242)
    for _ in range(200):
        pts = rng.normal(size=(rng.integers(1, 6), 2)) * rng.uniform(0.5, 1.5)
        disc = 0.5 * np.stack([np.cos(np.linspace(0, 2 * np.pi, 720, endpoint=False)),
                               np.sin(np.linspace(0, 2 * np.pi, 720, endpoint=False))], axis=1)
        P = np.vstack([disc, pts])
        from scipy.spatial import ConvexHull
        hull = ConvexHull(P)
        far = float(np.max(np.linalg.norm(P[hull.vertices], axis=1)))
        if far > hull.volume / 0.5 + 1e-9:
            out.append(f"a body of area {hull.volume:.4f} reaches {far:.4f} > A/r")
            break
    return out


def c2_containment(w):
    out = []
    for name, (h, _) in test_covers().items():
        P, hj, bj = outer_polygon(h, 128, 5e-4, w["quantize"])
        t = 2 * np.pi * np.arange(200000) / 200000
        gap = float(np.min(support_of(P, t) - h(t)))
        if gap < -1e-12:
            out.append(f"{name}: U pokes out of P by {-gap:.3e}")
    return out


D_, Q_ = sp.symbols("Delta q", positive=True)


def c3_eps_derived(w):
    """Thm 10.1: x.v <= b_j + ||x|| ||v - u_j|| <= (h_U(u_j) + q) + R_P d, and
    h_U(u_j) <= h_U(v) + R d. The bound is the sum of the three excesses."""
    d = 2 * sp.sin(D_ / 2)
    RP = (R_ + Q_) / sp.cos(D_)
    derived = Q_ + RP * d + R_ * d
    return [] if sp.simplify(derived - w["eps"](R_, D_, Q_)) == 0 else ["eps is not q + R_P d + R d"]


def c4_eps_measured(w):
    out, rows = [], {}
    for name, (h, _) in test_covers().items():
        P, _, _ = outer_polygon(h, 128, 5e-4, up)
        ex = excess(P, h)
        e_glob = float(w["eps"](2 * ABAR, math.pi / 128, 5e-4))
        e_true = float(w["eps"](true_radius(h), math.pi / 128, 5e-4))
        rows[name] = {"excess": ex, "eps_global_R": e_glob, "eps_true_R": e_true}
        if ex > min(e_glob, e_true) + 1e-12:
            out.append(f"{name}: Hausdorff excess {ex:.6f} > eps {min(e_glob, e_true):.6f}")
    w["c4_rows"] = rows
    return out


def c5_area(w):
    out, rows = [], {}
    for name, (h, aU) in test_covers().items():
        P, _, _ = outer_polygon(h, 128, 5e-4, up)
        aP = shoelace(P)
        for label, R in (("global", 2 * ABAR), ("true", true_radius(h))):
            e = float(w["eps"](R, math.pi / 128, 5e-4))
            if aP - aU > float(w["beta"](R, e)) + 1e-12:
                out.append(f"{name}: inflation {aP - aU:.3e} > beta({label} R)")
        # sec. 22: Steiner's formula for P + gamma B, against the support-function area of P + gamma B
        gam = 0.01
        n = 2_000_000
        t = 2 * np.pi * np.arange(n) / n
        hp = support_of(P, t) + gam
        dh = np.gradient(hp, t[1] - t[0])
        a_sup = 0.5 * float(np.sum(hp ** 2 - dh ** 2)) * (2 * np.pi / n)
        a_steiner = aP + gam * perimeter(P) + math.pi * gam ** 2
        RP = (2 * ABAR + 5e-4) / math.cos(math.pi / 128)
        rows[name] = {"area_U": aU, "area_P": aP, "inflation": aP - aU,
                      "area_P_gamma_support": a_sup, "area_P_gamma_steiner": a_steiner}
        if abs(a_sup - a_steiner) > 1e-6:
            out.append(f"{name}: Steiner area {a_steiner:.8f} vs support-function area {a_sup:.8f}")
        if a_steiner > aP + float(w["overhead"](RP, gam)) + 1e-12:
            out.append(f"{name}: safety-inflation overhead exceeds 2 pi R_P gamma + pi gamma^2")
    w["c5_rows"] = rows
    return out


def c6_dictionary(w):
    out = []
    for q in (5e-4, 1e-3, 1 / 4096, 1 / 16384):
        R = 2 * ABAR
        K = w["K_q"](R, q)
        if 0.5 + K * q < R + q - 1e-12:
            out.append(f"q={q}: top level {0.5 + K * q:.6f} below R+q = {R + q:.6f}")
    return out


# the package's sanity polygon: the definition's outer polygon with six on-grid heights
# pushed one extra level - floating-point ceiling at heights exactly on a level
PACKAGE_BUMPED = (0, 16, 48, 64, 80, 112)


def c7_sanity(w):
    out, v = [], w["validation"]["sanity_check"]
    h, aU = test_covers()["sec33"]
    M, q = v["M"], v["q"]
    R = v["Rglobal"]
    e = float(w["eps"](R, math.pi / M, q))
    checks = {"true_area": aU, "analytic_epsilon_bound": e, "analytic_beta_area_bound": float(w["beta"](R, e))}
    for key, mine in checks.items():
        if abs(v[key] - mine) > 1e-12:
            out.append(f"{key}: recorded {v[key]}, recomputed {mine}")
    for row in v["analytic_convergence_table"]:
        e2 = float(w["eps"](R, math.pi / row["M"], row["q"]))
        if abs(row["epsilon"] - e2) > 1e-12 or abs(row["beta"] - float(w["beta"](R, e2))) > 1e-12:
            out.append(f"convergence row M={row['M']}: recorded eps/beta differ")
    th = 2 * np.pi * np.arange(M) / M
    b = up(h(th), q)
    b[list(w["bumped"])] += q
    P = np.array([[-100.0, -100.0], [100.0, -100.0], [100.0, 100.0], [-100.0, 100.0]])
    for t, bb in zip(th, b):
        P = clip(P, np.array([math.cos(t), math.sin(t)]), bb)
    if abs(shoelace(P) - v["polygon_area"]) > 1e-12 or abs(excess(P, h) - v["hausdorff_outer_excess_numeric"]) > 1e-12:
        out.append("the recorded polygon is not reproduced by the definition plus the pinned extra levels")
    P0, _, _ = outer_polygon(h, M, q, up)
    w["c7"] = {"definition_area": shoelace(P0), "definition_excess": excess(P0, h),
               "recorded_area": v["polygon_area"], "recorded_excess": v["hausdorff_outer_excess_numeric"],
               "pinned_extra_levels_at": list(w["bumped"])}
    return out


def c8_package(w):
    out, leaves = [], {row[0]: row for row in w["manifest"]["leaves"]}
    for rec, leaf, text in ((w["validation"]["round04"], DOC, w["text"]),
                            (w["validation"]["state_crystal"], CRYSTAL, w["crystal"])):
        _, size, digest = leaves[leaf]
        if rec["sha256"] != digest or rec["bytes"] != size:
            out.append(f"{rec['file']}: recorded sha256/bytes differ from the archive")
        if rec["double_dollar_count"] != text.count("$$"):
            out.append(f"{rec['file']}: recorded {rec['double_dollar_count']} $$, document has {text.count('$$')}")
    return out


# ---------------------------------------------------------------- registry (Round 04)

R04_DEFINITIONS = {
    "ESTABLISHED": ("contextual", "sec. 0: the global finite-resolution proof graph now exists"),
    "PROOF SCHEMA ESTABLISHED": ("in-line", "sec. 28: a procedure whose soundness is argued, not a computation carried out"),
}
PBCT = "PASS by cited theorem"
R04_REVIEWS = [
    ("PROVED", "Universal disk anchor", "analytic", "PASS", "sec. 2; the disc is invariant under O(2)"),
    ("PROVED", "Uniform radius bound", "analytic", "PASS", "Thm 4.1; C1"),
    ("PROVED", "Finite outward support compiler", "analytic", "PASS", "Thm 8.1; C2"),
    ("PROVED", "Universality monotonicity", "analytic", "PASS", "Cor 8.2"),
    ("PROVED", "Explicit Hausdorff error", "analytic", "PASS", "Thm 10.1; C3 derivation, C4 measurement"),
    ("PROVED", "Explicit area inflation", "theorem-application", PBCT, "Steiner's formula; perimeter monotone under inclusion; C5"),
    ("PROVED", "Finite candidate dictionary", "analytic", "PASS", "sec. 13-14; C6"),
    ("PROVED", "Global finite polygon bracket", "analytic", PBCT, "Thm 15.1; the lower side uses beta (Steiner)"),
    ("PROVED", "Cover-margin Hausdorff stability", "analytic", "PASS", "Thm 17.1"),
    ("PROVED", "Candidate-cell prune rule", "analytic", "PASS", "Thm 19.1"),
    ("PASS", "SANITY", "computation", "PASS",
     "sec. 33's assertions hold, but its recorded polygon is the definition plus one extra level at six on-grid "
     "normals (floating-point ceiling); the definition's own polygon has area 0.83363916 and excess 0.00062249 (C7)"),
    ("PASS", "Numerical sanity", "computation", "PASS", "= SANITY"),
    ("CLOSED", "CANDIDATE-COVER FINITE COMPILER", "analytic", PBCT, "sec. 7-16"),
    ("COMPUTE-DEFERRED", "C04-4：Area lower-cell bound", "n/a", "n/a", "C04-1..4 correctly deferred"),
    ("OPEN", "Exact finite saturation", "n/a", "n/a", ""),
    ("NONE", "New numerical bound", "reading", "PASS", "none claimed"),
    ("YES", "finite-resolution global closure", "analytic", PBCT, "Thm 15.1 + sec. 16"),
    ("ESTABLISHED", "GLOBAL FINITE-RESOLUTION PROOF GRAPH", "analytic", PBCT, "Rounds 01-04 chained"),
    ("PROOF SCHEMA ESTABLISHED", "Certificate-safe surgery", "reading", "PASS", "the schema is sound as a schema"),
]


def r1_registry(w):
    return registry.completeness(w["registry"], "R04", w["definitions"], w["reviews"])


# ---------------------------------------------------------------- world, checks, drill

CHECKS = {"A0": gate.anchors_check(ANCHORS), "A1": gate.citations_check(CITE_OPTS), "C1": c1_radius,
          "C2": c2_containment, "C3": c3_eps_derived, "C4": c4_eps_measured, "C5": c5_area,
          "C6": c6_dictionary, "C7": c7_sanity, "C8": c8_package, "R1": r1_registry}
DEPENDS = {"A0": {"text"}, "A1": {"text", "snapshot"}, "C1": {"triangle_area", "R_global"},
           "C2": {"quantize"}, "C3": {"eps"}, "C4": {"eps"}, "C5": {"eps", "beta", "overhead"},
           "C6": {"K_q"}, "C7": {"validation", "eps", "beta", "bumped"},
           "C8": {"validation", "text", "crystal", "manifest"},
           "R1": {"registry", "definitions", "reviews"}}


def eps_sym(R, D, q):
    """Thm 10.1 as stated: q + (R + R_P) d_M with R_P = (R+q)/cos(Delta), d_M = 2 sin(Delta/2)"""
    cos, sin = (sp.cos, sp.sin) if isinstance(D, sp.Basic) else (math.cos, math.sin)
    return q + (R + (R + q) / cos(D)) * 2 * sin(D / 2)


def claims():
    return {
        "triangle_area": lambda r, x: r * x,
        "R_global": 1.6881871888,
        "quantize": up,
        "eps": eps_sym,
        "beta": lambda R, e: 2 * math.pi * R * e + math.pi * e * e,
        "overhead": lambda RP, g: 2 * math.pi * RP * g + math.pi * g * g,
        "K_q": lambda R, q: math.ceil((R - 0.5) / q) + 1,
        "bumped": PACKAGE_BUMPED,
    }


def load_inputs():
    blobs = {n: source_bytes(n) for n in SOURCES}
    want = {DOC, CRYSTAL, VALIDATION}
    got = {leaf.id: d for leaf, d in walk_sources(blobs, keep=lambda l: l.id in want) if d is not None}
    return (got[DOC].decode("utf-8"), got[CRYSTAL].decode("utf-8"), json.loads(got[VALIDATION]),
            json.loads(MANIFEST.read_text(encoding="utf-8")))


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--refresh", action="store_true")
    ap.add_argument("--drill", action="store_true")
    ap.add_argument("--blind", metavar="CID")
    args = ap.parse_args()
    if args.refresh:
        recs = {str(n): (gate.fetch_arxiv(r["id"]) if r["kind"] == "arxiv" else gate.fetch_crossref(doi=r["doi"]))
                for n, r in REFS.items()}
        SNAPSHOT.write_text(json.dumps({"retrieved": datetime.date.today().isoformat(), "records": recs},
                                       ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
    snapshot = json.loads(SNAPSHOT.read_text(encoding="utf-8"))
    text, crystal, validation, manifest = load_inputs()

    def build():
        return {"text": text, "crystal": crystal, "validation": json.loads(json.dumps(validation)),
                "manifest": manifest, "snapshot": json.loads(json.dumps(snapshot)), "registry": registry.load(),
                "definitions": dict(R04_DEFINITIONS), "reviews": list(R04_REVIEWS), **claims()}

    if args.blind:
        CHECKS[args.blind] = lambda w: []
        args.drill = True
    w = build()
    results = {c: fn(w) for c, fn in CHECKS.items()}
    for c, f in results.items():
        print(f"{c} {'PASS' if not f else 'FAIL'}", *(f"\n     {x}" for x in f[:6]))
    print("C4", w.get("c4_rows")); print("C7", w.get("c7"))
    if not args.drill:
        return
    val = validation["sanity_check"]
    sanity = lambda key, value: {"validation": {**validation, "sanity_check": {**val, key: value}}}
    cases = [
        ("K0-unmodified", "control", {}, set()),
        ("K1-eps-rewritten", "control",
         {"eps": lambda R, D, q: q + 2 * (R + (R + q) / (sp.cos(D) if isinstance(D, sp.Basic) else math.cos(D)))
          * (sp.sin(D / 2) if isinstance(D, sp.Basic) else math.sin(D / 2))}, set()),
        ("K2-quantize-by-floor-plus-one", "control",
         {"quantize": lambda hj, q: 0.5 + q * (np.floor((hj - 0.5) / q) + 1)}, set()),
        ("K3-doc-crlf", "control", {"text": text.replace("\n", "\r\n")}, set()),
        ("M1-triangle-area-doubled", "mutation", {"triangle_area": lambda r, x: 2 * r * x}, {"C1"}),
        ("M2-R-arithmetic", "mutation", {"R_global": 1.6881871889}, {"C1"}),
        ("M3-quantize-downward", "mutation", {"quantize": lambda hj, q: 0.5 + q * np.floor((hj - 0.5) / q)}, {"C2"}),
        ("M4-eps-missing-R-d", "mutation",
         {"eps": lambda R, D, q: q + ((R + q) / (sp.cos(D) if isinstance(D, sp.Basic) else math.cos(D)))
          * 2 * (sp.sin(D / 2) if isinstance(D, sp.Basic) else math.sin(D / 2))}, {"C3", "C7"}),
        ("M5-eps-without-q", "mutation",
         {"eps": lambda R, D, q: (R + (R + q) / (sp.cos(D) if isinstance(D, sp.Basic) else math.cos(D)))
          * 2 * (sp.sin(D / 2) if isinstance(D, sp.Basic) else math.sin(D / 2))}, {"C3", "C7"}),
        ("M6-beta-without-square", "mutation", {"beta": lambda R, e: 2 * math.pi * R * e}, {"C7"}),
        # the measurement can only see a gross error: an eps below the actual excess
        ("M6b-eps-tiny", "mutation", {"eps": lambda R, D, q: 1e-6 + 0 * q}, {"C3", "C4", "C5", "C7"}),
        ("M7-overhead-too-small", "mutation", {"overhead": lambda RP, g: 0.0}, {"C5"}),
        ("M8-K_q-short", "mutation", {"K_q": lambda R, q: math.ceil((R - 0.5) / q) - 1}, {"C6"}),
        ("M9-recorded-area", "mutation", sanity("polygon_area", val["polygon_area"] + 1e-9), {"C7"}),
        ("M10-pin-dropped", "mutation", {"bumped": ()}, {"C7"}),
        ("M11-doc-eps-edited", "mutation", {"text": text.replace("(R+R_P)d_M", "R_Pd_M")}, {"A0"}),
        ("M12-package-sha", "mutation",
         {"validation": {**validation, "round04": {**validation["round04"], "sha256": "0" * 64}}}, {"C8"}),
        ("M13-ref-year", "mutation", {"snapshot": {**snapshot, "records": {
            **snapshot["records"], "2": {**snapshot["records"]["2"], "years": [2019]}}}}, {"A1"}),
        ("M14-review-dropped", "mutation", {"reviews": list(R04_REVIEWS[1:])}, {"R1"}),
    ]
    if args.blind:
        cases = [c for c in cases if args.blind in c[3]]
    d = gate.run_drill(build, CHECKS, DEPENDS, cases)
    summary = gate.summarize(d, CHECKS)
    print(gate.report_drill(summary))
    for x in d:
        if not x["ok"]:
            print("   DRILL FAIL", x)
    if args.blind:
        n = sum(1 for x in d if args.blind in x["expected"] and args.blind not in x["tripped"])
        print(f"blind {args.blind}: drill reported it blind in {n} cases (nothing written)")
        return
    log = {"run": "RUN-007", "subject": "Round 04: the candidate-cover compiler", "document": DOC,
           "snapshot_retrieved": snapshot["retrieved"],
           "checks": {c: {"pass": not f, "failures": f} for c, f in results.items()},
           "c4_rows": w.get("c4_rows"), "c5_rows": w.get("c5_rows"), "c7": w.get("c7"), "drill": summary}
    LOG.write_text(json.dumps(log, ensure_ascii=False, indent=1, default=float) + "\n", encoding="utf-8")
    if not results["R1"]:
        registry.record("R04", "RUN-007", R04_DEFINITIONS, R04_REVIEWS)
        print(f"registry: {len(R04_DEFINITIONS)} definitions, {len(R04_REVIEWS)} reviews recorded for R04")


if __name__ == "__main__":
    main()
