"""RUN-052 - LESR Paper 02: group-orbit shape expansion and symmetry breaking.

LESR Paper 02 (2026-09-20) builds the series' group layer. It corrects Papers 00-01's E(2) = SO(2) x R^2 to SE(2),
with E(2) = O(2) x R^2; writes rigid motions and reflections on support functions; fixes the translation gauge
a_1 = b_1 = 0; splits perturbations into the O(2) Fourier modes V_m, on which the convexity operator L = 1 + d^2
acts by 1 - m^2; averages over a finite stabilizer to split a perturbation into its symmetry-preserving and
symmetry-breaking parts; and states a gcd law for the stabilizer of a D_n seed perturbed by cos m theta. On an
H-invariant seed a pure breaking mode has zero first area variation, and the second variation is
pi (1 - m^2)(a_m^2 + b_m^2) on V_m. It ends in a mode compiler with six outputs. It ships a schema, a compiler
scaffold, a state record and VALIDATION. It is evidence, not a canonical document. Here the formulas are checked
symbolically; the projectors, the gcd law and the convexity caps are computed; and the first-variation theorem and
the paper's plan for the current seed (sec. 54) are tested on symmetric bodies and on the Lebesgue kernel's own
best placement.

Checks
  A0 anchors      the formulas and laws below occur in the document as written
  W1 actions      sec. 1-7, 9-11, 17-23: the erratum against Papers 00-01; rigid motions and reflections on support
                  functions (point sets); the translation gauge and the Steiner point; the O(2) matrices on V_m; L's
                  eigenvalues and equivariance; the averaging projectors of C_n and D_n on the Fourier modes to 24
  W2 variations   sec. 33-37: the second variation's Fourier diagonal; the first variation of breaking and
                  preserving modes on symmetric bodies - two Reuleaux polygons and the kernel's hull on the mirror
                  slice - and on the recorded seed, which is not symmetric
  W3 gcd law      sec. 26-27: the stabilizer of D_n seeds perturbed by cos m theta, read off the sampled bodies,
                  n = 2..8, m = 1..24
  W4 caps         sec. 12-14: the exact convexity cap of pure modes on a smooth seed against sec. 12's bound; the
                  pure modes admissible at all on bodies with corners - two Reuleaux polygons and the kernel's hull
  W5 seed         sec. 39, 54: the kernel's recorded best placement against its mirror-symmetric polish; the area
                  along the breaking directions there; strict local minimality; the shared supporting lines
  W6 records      the schema and the state against the sections that carry them
  W7 scaffold     the compiler scaffold, read after this rebuild and never run
  P1 package      the VALIDATION record against the bytes and the archive

Usage: python code/run052_lesr_paper02.py [--drill] [--blind CID]
"""
import argparse
import copy
import hashlib
import inspect
import json
import math
import re

import numpy as np
import sympy as sp

import gate
import run044_uesfcm_cycles_01_03 as r44
import run046_uesfcm_cycles_07_09 as r46
from corpus import SOURCES, TREE, source_bytes, walk_sources
from run008_round05_saturation_gate import envelope, segments, verify_segments, wrap
from run009_round06_witness_exchange import placed_reuleaux

FOLDER = "main.zip!/LESR_Skew_Field_Variational/"
PKG = FOLDER + "LESR_Paper02_Package_v0.1.zip!/"
DOC = PKG + "LESR_Paper02_Group_Orbit_Shape_Expansion_and_Symmetry_Breaking_v0.1.md"
SCHEMA = PKG + "LESR_Paper02_Group_Orbit_Schema_v0.1.json"
SCAFFOLD = PKG + "LESR_Paper02_Group_Orbit_Compiler_Scaffold.py"
STATE = PKG + "LESR_State_Paper02_v0.1.json"
VALIDATION = PKG + "LESR_Paper02_VALIDATION.json"
PAPER00 = FOLDER + "LESR_Skew_Field_Variational_Series_Paper00_Package_v0.1.zip!/LESR_Paper00_From_Skew_Fields_to_Variational_Shape_Closure_v0.1.md"
PAPER01 = FOLDER + "LESR_Paper01_Package_v0.1.zip!/LESR_Paper01_Topology_Envelope_and_Stratified_Shape_Space_v0.1.md"
PAPER03 = FOLDER + "LESR_Paper03_Package_v0.1.zip!/LESR_Paper03_Support_Function_Variational_Calculus_v0.1.md"
LOG = TREE / "data" / "gate-logs" / "run052-lesr-paper02.json"
MANIFEST = TREE / "data" / "SOURCE_MANIFEST.v1.json"
CACHE = TREE / "data" / "cache" / "run052-searches.json"     # data/cache is not committed
# Round 07's best exhibited placement q-dagger (x3, y3, phi5, x5, y5), as RUN-010 read it from the package's log
QDAG = [-0.01215883209899, -3.708198611978e-08, 1.256661190387, 0.02299660691706, -3.896981517063e-06]
P5 = 2 * math.pi / 5
MODES = 24


def load_inputs():
    blobs = {n: source_bytes(n) for n in SOURCES}
    want = (DOC, SCHEMA, SCAFFOLD, STATE, VALIDATION)
    others = (PAPER00, PAPER01, PAPER03)
    got = {leaf.id: d for leaf, d in walk_sources(blobs, keep=lambda l: l.id in want + others) if d is not None}
    rows = {row[0]: row for row in json.loads(MANIFEST.read_text(encoding="utf-8"))["leaves"]}
    return {"text": got[DOC].decode("utf-8"), "schema": json.loads(got[SCHEMA]), "scaffold": got[SCAFFOLD].decode("utf-8"),
            "state": json.loads(got[STATE]), "validation": json.loads(got[VALIDATION]), "bytes": {k: got[k] for k in want},
            "manifest_rows": {k: rows[k] for k in want}, "paper00": got[PAPER00].decode("utf-8"),
            "paper01": got[PAPER01].decode("utf-8"), "paper03": got[PAPER03].decode("utf-8")}


# ---------------------------------------------------------------- the group on Fourier coefficients

def rotation_matrix(psi, M=MODES):
    """(R h)(theta) = h(theta - psi) on the coefficients (a_0, a_1, b_1, ..., a_M, b_M): each (a_m, b_m) turns by m psi"""
    R = np.zeros((2 * M + 1, 2 * M + 1))
    R[0, 0] = 1.0
    for m in range(1, M + 1):
        c, s = math.cos(m * psi), math.sin(m * psi)
        R[2 * m - 1:2 * m + 1, 2 * m - 1:2 * m + 1] = [[c, -s], [s, c]]
    return R


def reflection_matrix(alpha, M=MODES):
    """(sigma_alpha h)(theta) = h(2 alpha - theta): the mirror at 0 (b_m -> -b_m), then a turn by 2 alpha"""
    S = np.eye(2 * M + 1)
    for m in range(1, M + 1):
        S[2 * m, 2 * m] = -1.0
    return rotation_matrix(2 * alpha, M) @ S


def projector(n, dihedral, alpha=0.0, M=MODES):
    """sec. 17: the average over C_n, or over D_n with a mirror axis at alpha (its mirrors are R_{2 pi j / n} sigma_alpha)"""
    G = [rotation_matrix(2 * math.pi * j / n, M) for j in range(n)]
    if dihedral:
        G = G + [g @ reflection_matrix(alpha, M) for g in G]
    return sum(G) / len(G)


def hull_ccw(P):
    pts = sorted(map(tuple, P))
    cross = lambda o, a, b: (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
    lower, upper = [], []
    for p in pts:
        while len(lower) >= 2 and cross(lower[-2], lower[-1], p) <= 0:
            lower.pop()
        lower.append(p)
    for p in reversed(pts):
        while len(upper) >= 2 and cross(upper[-2], upper[-1], p) <= 0:
            upper.pop()
        upper.append(p)
    return np.array(lower[:-1] + upper[:-1])


def steiner_gap(P):
    """sec. 6: (a_1, b_1) = (1/pi) int h u d theta, in closed form over the vertices' normal cones, against the Steiner
    point sum alpha_i v_i / 2 pi (alpha_i the exterior angles) - the translation gauge puts it at the origin"""
    V = hull_ccw(P)
    n = len(V)
    beta = [math.atan2(-(V[(i + 1) % n][0] - V[i][0]), V[(i + 1) % n][1] - V[i][1]) for i in range(n)]
    s1, s2 = np.zeros(2), np.zeros(2)
    for i in range(n):
        lo = beta[i - 1]
        al = (beta[i] - lo) % (2 * math.pi)
        hi = lo + al
        cc = (hi - lo) / 2 + (math.sin(2 * hi) - math.sin(2 * lo)) / 4
        cs = (math.sin(hi) ** 2 - math.sin(lo) ** 2) / 2
        ss = (hi - lo) / 2 - (math.sin(2 * hi) - math.sin(2 * lo)) / 4
        s1 += np.array([[cc, cs], [cs, ss]]) @ V[i] / math.pi
        s2 += al * V[i] / (2 * math.pi)
    return float(np.max(np.abs(s1 - s2)))


def actions(M=MODES):
    """sec. 2-7, 9-11, 17-23: the formulas in sympy; placements with turns and mirrors on point sets; the Steiner
    point; the projectors of C_n and D_n on the modes to M, their ranks and images, and a general mirror axis"""
    t, p, al = sp.symbols("theta phi alpha", real=True)
    A, B = sp.symbols("A B", real=True)                     # A = m theta, B = m phi
    m = sp.symbols("m", integer=True, positive=True)
    c = sp.symbols("c0:5", real=True)
    d = sp.symbols("d1:5", real=True)
    out = {}
    # sec. 7, rows the images of cos m theta and sin m theta: turn [[cos, sin], [-sin, cos]] (m phi); mirror diag(1, -1)
    turn = [[sp.cos(B), sp.sin(B)], [-sp.sin(B), sp.cos(B)]]
    mirror = [[1, 0], [0, -1]]
    out["matrices"] = [sp.simplify(sp.expand_trig(sp.cos(A - B)) - turn[0][0] * sp.cos(A) - turn[0][1] * sp.sin(A)) == 0,
                       sp.simplify(sp.expand_trig(sp.sin(A - B)) - turn[1][0] * sp.cos(A) - turn[1][1] * sp.sin(A)) == 0,
                       sp.simplify(sp.cos(-A) - mirror[0][0] * sp.cos(A) - mirror[0][1] * sp.sin(A)) == 0,
                       sp.simplify(sp.sin(-A) - mirror[1][0] * sp.cos(A) - mirror[1][1] * sp.sin(A)) == 0]
    phi = c[0] + sum(c[k] * sp.cos(k * t) + d[k - 1] * sp.sin(k * t) for k in range(1, 5))
    L = lambda f: f + sp.diff(f, t, 2)
    out["eigen"] = [sp.simplify(L(sp.cos(m * t)) - (1 - m ** 2) * sp.cos(m * t)) == 0, sp.simplify(L(sp.sin(m * t)) - (1 - m ** 2) * sp.sin(m * t)) == 0]
    out["equivariant"] = [sp.simplify(L(phi.subs(t, t - p)) - L(phi).subs(t, t - p)) == 0,
                          sp.simplify(L(phi.subs(t, 2 * al - t)) - L(phi).subs(t, 2 * al - t)) == 0]
    out["generator"] = sp.simplify(sp.diff(phi.subs(t, t - p), p).subs(p, 0) + sp.diff(phi, t)) == 0
    out = {k: ([bool(x) for x in v] if isinstance(v, list) else bool(v)) for k, v in out.items()}
    # sec. 2, 5: h_{gK}(u) = h_K(R^T u) + t.u for turns and mirrors; (sigma_a h)(theta) = h(2a - theta)
    rng = np.random.default_rng(52)
    worst = 0.0
    for _ in range(60):
        P = rng.normal(size=(12, 2))
        th, ps, a = rng.uniform(0, 2 * np.pi), rng.uniform(0, 2 * np.pi), rng.uniform(0, np.pi)
        tv = rng.normal(size=2)
        u = np.array([math.cos(th), math.sin(th)])
        Rm = np.array([[math.cos(ps), -math.sin(ps)], [math.sin(ps), math.cos(ps)]])
        Sm = np.array([[math.cos(2 * a), math.sin(2 * a)], [math.sin(2 * a), -math.cos(2 * a)]])
        for G in (Rm, Sm):
            worst = max(worst, abs(float(np.max((P @ G.T + tv) @ u)) - float(np.max(P @ (G.T @ u))) - float(tv @ u)))
        worst = max(worst, abs(float(np.max((P @ Sm.T) @ u)) - float(np.max(P @ np.array([math.cos(2 * a - th), math.sin(2 * a - th)])))))
    out["placements"] = worst
    out["steiner"] = max(steiner_gap(rng.normal(size=(9, 2))) for _ in range(40))
    # sec. 17-23: the projectors, their ranks and images; a general axis
    ranks_c, ranks_d, idem, images, axis = [], [], 0.0, True, True
    for n in range(1, 9):
        for dih, ranks in ((False, ranks_c), (True, ranks_d)):
            Pm = projector(n, dih, 0.0, M)
            idem = max(idem, float(np.max(np.abs(Pm @ Pm - Pm))), float(np.max(np.abs(Pm - Pm.T))))
            ranks.append(int(round(float(np.trace(Pm)))))
            for k in range(M + 1):
                for which in ((0,) if k == 0 else (0, 1)):
                    e = np.zeros(2 * M + 1)
                    e[0 if k == 0 else 2 * k - 1 + which] = 1.0
                    kept = k % n == 0 and not (dih and which == 1)
                    images &= bool(np.max(np.abs(Pm @ e - (e if kept else 0 * e))) < 1e-12)
        Pa = projector(n, True, 0.3, M)
        for k in range(1, M // n + 1):
            i, q = 2 * k * n - 1, k * n * 0.3
            v, w = np.zeros(2 * M + 1), np.zeros(2 * M + 1)
            v[i], v[i + 1] = math.cos(q), math.sin(q)            # cos kn(theta - alpha)
            w[i], w[i + 1] = -math.sin(q), math.cos(q)           # sin kn(theta - alpha)
            axis &= bool(max(np.max(np.abs(Pa @ v - v)), np.max(np.abs(Pa @ w))) < 1e-12)
        axis &= int(round(float(np.trace(Pa)))) == 1 + M // n
    out["projectors"] = {"ranks_C": ranks_c, "ranks_D": ranks_d, "idempotent": idem, "images": images, "axis": axis}
    return out


# ---------------------------------------------------------------- the first and second variations

def integral_2pi(expr, t):
    """the integral over [0, 2 pi] of a trigonometric polynomial: 2 pi times its constant term"""
    e = sp.expand(sp.expand(expr).rewrite(sp.exp))
    return sp.simplify(sum((term for term in sp.Add.make_args(e) if not term.has(t)), sp.Integer(0)) * 2 * sp.pi)


def gram(Mq=5):
    """sec. 36: int (phi psi - phi' psi') on 1, cos m theta, sin m theta (m <= Mq): its diagonal over pi and the
    number of nonzero entries off it"""
    t = sp.symbols("theta", real=True)
    basis = [sp.Integer(1)] + [f(k * t) for k in range(1, Mq + 1) for f in (sp.cos, sp.sin)]
    n = len(basis)
    G = [[None] * n for _ in range(n)]
    for i in range(n):
        for j in range(i, n):
            G[i][j] = G[j][i] = integral_2pi(basis[i] * basis[j] - sp.diff(basis[i], t) * sp.diff(basis[j], t), t)
    return {"diagonal": [int(sp.nsimplify(G[i][i] / sp.pi)) for i in range(n)], "off": sum(1 for i in range(n) for j in range(n) if i != j and G[i][j] != 0)}


def segs_of(pieces):
    """the hull's verified segments (start, end, piece) for support pieces, with the grid retries of r46.hull"""
    C = np.array([p[0] for p in pieces], dtype=float)
    rho = np.array([p[1] for p in pieces], dtype=float)
    ranged = [(k, p[2]) for k, p in enumerate(pieces) if p[2] is not None]

    def owner(t):
        V = C @ np.stack([np.cos(t), np.sin(t)]) + rho[:, None]
        for k, (mid, half) in ranged:
            V[k, np.abs(wrap(t - mid)) > half] = -np.inf
        return np.argmax(V, axis=0)
    for n in (16384, 16381, 12289, 8191, 4096):
        try:
            cuts, own = envelope(owner, n, len(pieces))
            return C, rho, sorted(verify_segments(C, rho, [p[2] for p in pieces], list(segments(cuts, own))))
        except RuntimeError:
            continue
    raise RuntimeError("no grid")


def first_variation(C, rho, segs, m, kind):
    """delta A[h; phi] = int phi d mu_h for phi = cos m theta or sin m theta: the density rho_k on each segment, and at
    each hand-over the atom h'_next - h'_prev, the length of the edge with that normal"""
    f = (lambda t: math.cos(m * t)) if kind == "cos" else (lambda t: math.sin(m * t))
    F = (lambda t: math.sin(m * t) / m) if kind == "cos" else (lambda t: -math.cos(m * t) / m)
    dh = lambda k, t: -C[k, 0] * math.sin(t) + C[k, 1] * math.cos(t)
    total, n = 0.0, len(segs)
    for i, (a, b, k) in enumerate(segs):
        total += rho[k] * (F(b) - F(a))
        total += (dh(segs[(i + 1) % n][2], b) - dh(k, b)) * f(b)
    return total


def by_parts(C, rho, segs, m, kind, q=40):
    """the same as int (h phi - h' phi') d theta, by Gauss-Legendre on each segment"""
    x, wts = np.polynomial.legendre.leggauss(q)
    total = 0.0
    for a, b, k in segs:
        th = (a + b) / 2 + (b - a) / 2 * x
        h = C[k, 0] * np.cos(th) + C[k, 1] * np.sin(th) + rho[k]
        dh = -C[k, 0] * np.sin(th) + C[k, 1] * np.cos(th)
        f, df = (np.cos(m * th), -m * np.sin(m * th)) if kind == "cos" else (np.sin(m * th), m * np.cos(m * th))
        total += float(np.sum(wts * (h * f - dh * df))) * (b - a) / 2
    return total


def variations(top=12):
    """sec. 33-37: the second variation's Gram matrix; on three bodies with a mirror at 0 and n-fold symmetry (the
    Reuleaux triangle and pentagon, n = 3, 5; the kernel's hull on the mirror slice through q-dagger, n = 1) the
    first variation of the breaking modes (sin m theta, and cos m theta with n not dividing m), of the preserving
    ones (cos m theta, n | m) and of the translations, m <= top; and at q-dagger itself, whose hull is not symmetric,
    the sine modes'"""
    bodies = {"reuleaux3": (placed_reuleaux(3, 0.0, (0.0, 0.0)), 3), "reuleaux5": (placed_reuleaux(5, 0.0, (0.0, 0.0)), 5),
              "kernel": (r44.base_pieces([QDAG[0], 0.0, P5, QDAG[3], 0.0]), 1)}
    out = {"gram": gram(), "bodies": {}}
    for name, (pieces, n) in bodies.items():
        C, rho, segs = segs_of(pieces)
        fv = {(m, kind): first_variation(C, rho, segs, m, kind) for m in range(1, top + 1) for kind in ("cos", "sin")}
        parts = max(abs(fv[k] - by_parts(C, rho, segs, *k)) for k in fv)
        out["bodies"][name] = {"breaking": max(abs(v) for (m, kind), v in fv.items() if m > 1 and (kind == "sin" or m % n)),
                               "preserving": [round(fv[(m, "cos")], 6) + 0.0 for m in range(2, top + 1) if m % n == 0],
                               "translation": max(abs(fv[(1, "cos")]), abs(fv[(1, "sin")])), "parts": parts}
    C, rho, segs = segs_of(r44.base_pieces(QDAG))
    out["qdag"] = max(abs(first_variation(C, rho, segs, m, "sin")) for m in range(2, top + 1))
    return out


# ---------------------------------------------------------------- the gcd law

def stabilizer(h, N):
    """the elements of the dihedral group of order 2N that fix the sampled function h (to 1e-12): the order of the
    rotation subgroup (the largest divisor r of N whose turn fixes h) and the number of mirror lines"""
    rot = max(r for r in range(1, N + 1) if N % r == 0 and np.max(np.abs(np.roll(h, N // r) - h)) < 1e-12)
    idx = np.arange(N)
    return rot, sum(1 for j in range(N) if np.max(np.abs(h[(j - idx) % N] - h)) < 1e-12)


def gcd_scan(N=5040, M=MODES):
    """sec. 26-27: D_n seeds h0 = 1 + a cos n theta + b cos 2n theta (a, b small enough to stay convex) plus
    eps cos m theta; the stabilizer of each body on the translation gauge (a_1 = b_1 = 0), and for m = 1 also before
    the gauge; N = 5040 holds every order that can occur"""
    th = 2 * np.pi * np.arange(N) / N
    out = {"law": [0, 0], "misses": [], "m1": [], "examples": {}}
    for n in range(2, 9):
        h0 = 1 + 0.2 / (n * n - 1) * np.cos(n * th) + 0.05 / (4 * n * n - 1) * np.cos(2 * n * th)
        for m in range(1, M + 1):
            h = h0 + (0.1 / (m * m - 1) if m > 1 else 0.1) * np.cos(m * th)
            a1, b1 = 2 * float(np.mean(h * np.cos(th))), 2 * float(np.mean(h * np.sin(th)))
            got = stabilizer(h - a1 * np.cos(th) - b1 * np.sin(th), N)
            if m == 1:
                out["m1"].append([n, list(got), list(stabilizer(h, N))])
                continue
            g = math.gcd(n, m)
            out["law"][1] += 1
            if got == (g, g):
                out["law"][0] += 1
            else:
                out["misses"].append([n, m, list(got)])
            if n == 6 and m in (12, 4, 3, 5):
                out["examples"][str(m)] = got[0] if got[0] == got[1] else None
    return out


# ---------------------------------------------------------------- the convexity caps

def smooth_caps(top=60, phases=8, N=1 << 16):
    """sec. 12-13 on the smooth seed h = 1 + 0.02 cos 3t + 0.01 sin 5t + 0.004 cos 7t (rho = h + h'' > 0): for
    each pure mode cos m(t - c), both signs, the exact cap max{eps : rho + eps (1 - m^2) phi >= 0}, as the ratio
    cap (m^2 - 1) / kappa_0 - sec. 12's bound is the ratio 1; over all m, and at m = top"""
    th = 2 * np.pi * np.arange(N) / N
    rho = 1 - 0.16 * np.cos(3 * th) - 0.24 * np.sin(5 * th) - 0.192 * np.cos(7 * th)
    k0 = float(rho.min())
    lo, hi, last = math.inf, 0.0, [math.inf, 0.0]
    for m in range(2, top + 1):
        for j in range(phases):
            phi = np.cos(m * (th - 2 * math.pi * j / (phases * m)))
            for s in (1.0, -1.0):
                w = s * phi
                r = float(np.min(rho[w > 0] / ((m * m - 1) * w[w > 0]))) * (m * m - 1) / k0
                lo, hi = min(lo, r), max(hi, r)
                if m == top:
                    last = [min(last[0], r), max(last[1], r)]
    return {"kappa0": k0, "rhomax": float(rho.max()), "ratio": [lo, hi], "last": last}


def zero_stretches(rho, segs, tie=1e-9):
    """the arcs of normals where mu_h has no density - a corner's normal cone, or several corners' cones meeting at
    edges - as (start, length); segments shorter than the tie tolerance are dropped (pieces that touch the hull
    only in floating point)"""
    S = [(a, b, k) for a, b, k in segs if b - a > tie]
    zero = [rho[k] == 0 for _, _, k in S]
    if all(zero):
        return [(S[0][0], 2 * math.pi)]
    start = next(i for i in range(len(S)) if not zero[i])
    out, cur = [], None
    for j in range(1, len(S) + 1):
        i = (start + j) % len(S)
        a, b, _ = S[i]
        if zero[i]:
            cur = [a, b - a] if cur is None else [cur[0], cur[1] + (b - a)]
        elif cur is not None:
            out.append(tuple(cur))
            cur = None
    if cur is not None:
        out.append(tuple(cur))
    return out


def admissible(stretches, m, tol=1e-6):
    """sec. 14 for a pure mode cos m(theta - c), m >= 2: mu_h + eps (1 - m^2) phi >= 0 needs eps phi <= 0 wherever
    mu_h has no density, so phi must keep one sign s on every stretch. For each s, the phases c (mod 2 pi / m) that
    all stretches allow - each allows an arc of half-width (pi / m - length) / 2 - found among the arcs' ends. The
    tolerance is wide because a tangent hand-over (vertex to arc) is located only to ~1e-8: the two pieces differ
    there to second order"""
    P = 2 * math.pi / m
    dist = lambda x, c: abs((x - c + P / 2) % P - P / 2)
    out = {}
    for s in (1, -1):
        arcs = [(a + ln / 2 + (0 if s > 0 else math.pi / m), (math.pi / m - ln) / 2) for a, ln in stretches]
        if any(hw < -tol for _, hw in arcs):
            out[s] = []
            continue
        cands = [c + e * max(hw, 0.0) for c, hw in arcs for e in (-1, 1)]
        out[s] = sorted({round(x % P, 9) for x in cands if all(dist(x, c) <= max(hw, 0.0) + tol for c, hw in arcs)})
    return out


def corner_caps(pieces, M=MODES, N=1 << 14):
    """the pure modes m = 2..M that keep one sign on every zero-density stretch of the body, and for each the largest
    admissible amplitude at the first phase found: the least rho_k / ((m^2 - 1) |phi|) where phi has the wrong sign"""
    C, rho, segs = segs_of(pieces)
    st = zero_stretches(rho, segs)
    ok, caps = [], []
    for m in range(2, M + 1):
        adm = admissible(st, m)
        s = next((s for s in (1, -1) if adm[s]), None)
        if s is None:
            continue
        c = adm[s][0]
        best = math.inf
        for a, b, k in segs:
            if rho[k] > 0 and b - a > 1e-9:
                th = np.linspace(a, b, N)
                w = -s * np.cos(m * (th - c))
                if np.any(w > 0):
                    best = min(best, float(np.min(rho[k] / ((m * m - 1) * w[w > 0]))))
        ok.append(m)
        caps.append(round(best, 6))
    return {"stretches": len(st), "widest": round(max(ln for _, ln in st), 6), "admissible": ok, "caps": caps}


def caps_scan():
    return {"smooth": smooth_caps(), "bodies": {"reuleaux3": corner_caps(placed_reuleaux(3, 0.0, (0.0, 0.0))),
                                                "reuleaux5": corner_caps(placed_reuleaux(5, 0.0, (0.0, 0.0))),
                                                "kernel": corner_caps(r44.base_pieces([QDAG[0], 0.0, P5, QDAG[3], 0.0])),
                                                "qdag": corner_caps(r44.base_pieces(QDAG))}}


# ---------------------------------------------------------------- the kernel's own seed

def seed_scan(samples=1000, seed=52, N=5040):
    """sec. 39 and 54 on the kernel. The family is mirror-symmetric: (x3, y3, phi5, x5, y5) -> (x3, -y3, -phi5, x5,
    -y5) keeps the area. q-dagger's area and its mirror image's; the mirror slice y3 = y5 = 0, phi5 = 2 pi / 5
    through q-dagger, polished in (x3, x5) by golden sections to Z; the one-sided slopes along y3, phi5, y5 at Z
    (step 1e-6, both ways); the least rise per unit over random directions at three radii; the area along the
    segment q-dagger -> Z; each hull's stabilizer on the translation gauge; the supports at the normals +-pi/2 of
    the disc and the corners B3v1, B5v0 (above) and B3v2, B5v3 (below)"""
    A = r46.area
    aq, am = A(QDAG), A([QDAG[0], -QDAG[1], 2 * P5 - QDAG[2], QDAG[3], -QDAG[4]])
    x3, x5 = QDAG[0], QDAG[3]
    for _ in range(3):
        x3, _ = r44.golden_min(lambda q: A([q, 0.0, P5, x5, 0.0]), x3 - 2e-6, x3 + 2e-6)
        x5, _ = r44.golden_min(lambda q: A([x3, 0.0, P5, q, 0.0]), x5 - 2e-6, x5 + 2e-6)
    Z = [x3, 0.0, P5, x5, 0.0]
    a0 = A(Z)
    kinks = {}
    for name, i in (("y3", 1), ("phi5", 2), ("y5", 4)):
        kinks[name] = []
        for sgn in (1, -1):
            v = list(Z)
            v[i] += sgn * 1e-6
            kinks[name].append((A(v) - a0) / 1e-6)
    rng = np.random.default_rng(seed)
    least = {}
    for r in (1e-7, 1e-6, 1e-5):
        worst = math.inf
        for _ in range(samples):
            dv = rng.normal(size=5)
            dv /= np.linalg.norm(dv)
            worst = min(worst, (A([Z[i] + r * dv[i] for i in range(5)]) - a0) / r)
        least[f"{r:.0e}"] = worst
    line = [A([QDAG[i] + s * (Z[i] - QDAG[i]) for i in range(5)]) - a0 for s in (0.0, 0.25, 0.5, 0.75, 1.0)]
    th = 2 * np.pi * np.arange(N) / N
    stab = {}
    for name, v in (("z", Z), ("qdag", QDAG)):
        h = r44.support(r44.base_pieces(v))(th)
        a1, b1 = 2 * float(np.mean(h * np.cos(th))), 2 * float(np.mean(h * np.sin(th)))
        stab[name] = list(stabilizer(h - a1 * np.cos(th) - b1 * np.sin(th), N))
    P = r44.base_pieces(Z)
    up, down = np.array([0.0, 1.0]), np.array([0.0, -1.0])
    sup = [float(P[k][0] @ u + P[k][1]) for k, u in ((0, up), (2, up), (7, up), (0, down), (3, down), (10, down))]
    return {"aq": aq, "am": am, "z": Z, "a0": a0, "kinks": kinks, "least": least, "line": line, "stabilizers": stab,
            "supports": max(abs(x - 0.5) for x in sup), "labels": [r46.PIECE[k] for k in (0, 2, 7, 0, 3, 10)]}


# ---------------------------------------------------------------- the cache

def _key(name, fn, extra=""):
    import run008_round05_saturation_gate as r8
    import run009_round06_witness_exchange as r9
    digest = hashlib.sha256(inspect.getsource(fn).encode())
    for helper in HELPERS[name]:
        digest.update(inspect.getsource(helper).encode())
    for path in (r8.__file__, r9.__file__):
        digest.update(open(path, "rb").read())
    digest.update(extra.encode())
    return digest.hexdigest()


HELPERS = {"actions": (rotation_matrix, reflection_matrix, projector, hull_ccw, steiner_gap),
           "variations": (integral_2pi, gram, segs_of, first_variation, by_parts, r44.base_pieces),
           "gcd": (stabilizer,), "caps": (smooth_caps, zero_stretches, admissible, corner_caps, segs_of, r44.base_pieces),
           "seed": (r46.area, r46.hull, r44.golden_min, r44.golden_max, r44.support, r44.base_pieces, stabilizer)}


def searches():
    cached = json.loads(CACHE.read_text(encoding="utf-8")) if CACHE.exists() else {}
    out = {}
    for name, fn in (("actions", actions), ("variations", variations), ("gcd", gcd_scan), ("caps", caps_scan), ("seed", seed_scan)):
        key = _key(name, fn)
        if cached.get(name, {}).get("key") != key:
            cached[name] = {"key": key, "value": json.loads(json.dumps(fn()))}
            CACHE.parent.mkdir(parents=True, exist_ok=True)
            CACHE.write_text(json.dumps(cached, indent=1) + "\n", encoding="utf-8")
        out[name] = cached[name]["value"]
    return out


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

ANCHORS = {
    "A0": [r"\boxed{SE(2)=SO(2)\ltimes\mathbbR^2}", r"E(2)=O(2)\ltimes\mathbbR^2.", r"h_{gK}(u)=h_K(R^Tu)+t\cdotu.",
           r"(R_\phi\cdoth)(\theta)=h(\theta-\phi).", r"X_{\mathrm{rot}}h=-h'.", r"(T_th)(\theta)=h(\theta)+a\cos\theta+b\sin\theta.",
           r"(\sigma_\alpha\cdoth)(\theta)=h(2\alpha-\theta).", r"a_1=b_1=0.", r"L\phi_m=(1-m^2)\phi_m.", r"L(g\cdot\phi)=g\cdotL\phi.",
           r"|\varepsilon|\le\frac{\kappa_0}{(m^2-1)\|\phi_m\|_\infty}.", r"H_h=\{g\inO(2):g\cdot[h]=[h]\}.",
           r"P_H\phi=\frac1{|H|}\sum_{g\inH}g\cdot\phi.", r"m\equiv0\pmodn", r"h(\theta)=a_0+\sum_{k\ge1}a_{kn}\cos(kn\theta).",
           r"C_{\gcd(n,m)}.", r"\boxed{D_{\gcd(n,m)}.}", r"D_6\text{preserved};", r"\boxed{\deltaA[h;\phi]=0.}",
           r"\delta^2A[\phi_m]=\pi(1-m^2)(a_m^2+b_m^2).", r"\delta^2A=2\pia_0^2>0."],
}
# each completed item of the state, with the section that carries it (number and the start of its heading, squashed)
STATE_MAP = {"SE(2)/E(2) notation correction": (1, "記號修正"), "support-function rigid-motion action": (2, "支撐函數上的群作用"),
             "translation gauge m=1": (6, "IntrinsicGauge"), "O(2) Fourier mode decomposition": (7, "$O(2)$的FourierModeDecomposition"),
             "equivariance of L=1+d2": (10, "群作用與ConvexityOperator交換"),
             "convexity amplitude cap for strictly convex smooth seeds": (12, "SmoothStrictlyConvexSeed的ModeAmplitudeBound"),
             "finite/compact group averaging projector": (17, "FiniteStabilizerGroup的AveragingProjector"),
             "symmetry-preserving/breaking decomposition": (19, "Symmetry-Preserving與Symmetry-BreakingDecomposition"),
             "C_n and D_n Fourier invariance rules": (21, "CyclicSymmetry$C_n$"), "aligned pure-mode gcd stabilizer law": (26, "AlignedPure-ModeGCDLaw"),
             "isotropy lattice": (28, "IsotropyLattice"), "contact-orbit splitting": (32, "SymmetryBreaking如何分裂ContactOrbit"),
             "vanishing first area variation on pure breaking modes": (34, "Theorem—PureSymmetry-BreakingModes的AreaFirstVariation消失"),
             "Fourier diagonal second area variation": (36, "AreaSecondVariation在FourierModes上也對角化"),
             "finite group-generated skew basis": (41, "Group-GeneratedSkewBasis"), "symmetry-breaking graph": (50, "Symmetry-BreakingGraph")}
# each 'not claimed' item of the state, with the text's own disclaimer (squashed), where it has one
DISCLAIMERS = {"all symmetry-breaking branches are captured by a finite Fourier cutoff": "本篇不證infinite-ordercompleteness",
               "generic stabilizer equals D_gcd(n,m) without excluding accidental symmetry": "若無accidentalextrasymmetry",
               "unconstrained negative area second variation implies feasible improvement": "只表示areafunctional本身偏好某些高階deformation"}
CLAIMS = {
    "actions_claim": {"erratum": [True, True], "matrices": [True, True, True, True], "eigen": [True, True], "equivariant": [True, True], "generator": True,
                      "placements": True, "steiner": True, "ranks_C": [49, 25, 17, 13, 9, 9, 7, 7], "ranks_D": [25, 13, 9, 7, 5, 5, 4, 4],
                      "idempotent": True, "images": True, "axis": True},
    "variations_claim": {"gram": {"diagonal": [2, 0, 0, -3, -3, -8, -8, -15, -15, -24, -24], "off": 0},
                         "bodies": {"reuleaux3": {"breaking": True, "translation": True, "parts": True, "preserving": [-2.0, 0.0, 0.666667, 0.0]},
                                    "reuleaux5": {"breaking": True, "translation": True, "parts": True, "preserving": [-2.0, 0.0]},
                                    "kernel": {"breaking": True, "translation": True, "parts": True,
                                               "preserving": [0.103384, -0.80166, 0.134764, -0.528747, -2.068638, 0.45059, 0.507126, 0.372525, -0.960988,
                                                              -0.248561, 1.717064]}},
                         "qdag": 0.00015},
    "gcd_claim": {"law": [161, 161], "misses": [], "m1": [[n, [n, n], [1, 1]] for n in range(2, 9)], "examples": {"3": 3, "4": 2, "5": 1, "12": 6}},
    "caps_claim": {"smooth": {"kappa0": 0.5851, "ratio": [1.0, 2.004], "last": [1.0, 1.009]},
                   "bodies": {"reuleaux3": {"stretches": 3, "widest": 1.047198, "admissible": [3], "caps": [0.125]},
                              "reuleaux5": {"stretches": 5, "widest": 0.628319, "admissible": [5], "caps": [0.041667]},
                              "kernel": {"stretches": 5, "widest": 1.675516, "admissible": [], "caps": []},
                              "qdag": {"stretches": 4, "widest": 1.675492, "admissible": [], "caps": []}}},
    "seed_claim": {"mirror": True, "z": 0.8347809458213, "lower_by": 9.03e-11, "kinks": {"y3": [0.1327, 0.1327], "phi5": [0.0442, 0.0442], "y5": [0.2733, 0.2733]},
                   "rise": True, "least": {"1e-07": 0.0087, "1e-06": 0.0033, "1e-05": 0.0073}, "line": True,
                   "stabilizers": {"z": [1, 1], "qdag": [1, 0]}, "supports": True},
    "records_claim": {"outputs": True, "modes": True, "eigenvalue": True, "prediction": True, "erratum": True, "unmapped": [], "completed": 16,
                      "disclaimed": [False, True, True, True], "next": [True, True]},
    "scaffold_facts": {"lines": 62, "defs": ["mode_class", "convexity_eigenvalue", "aligned_dihedral_prediction", "smooth_convexity_amplitude_cap", "compile_mode"],
                       "classes": ["ModeState"], "outputs": ["PRESERVE_H", "BREAK_TO_K", "GAUGE_MODE"], "cap_used": False, "rotation_gauge": False,
                       "scale_mode": True},
    "package_claim": {"digests": [True, True, True, True], "utf8": True, "delimiters": ["PASS", []], "manifest": True},
}


def sig(x, n=2):
    return float(f"{x:.{n - 1}e}")


def a0_anchors(w):
    text = gate.squash(w["text"])
    return [f"anchor not found: {a[:60]}" for a in ANCHORS["A0"] if a not in text]


def w1_actions(w):
    """sec. 1: Papers 00 and 01 do write E(2) = SO(2) x R^2 (not SE(2)); sec. 2-7, 9-11, 17-23 as rebuilt"""
    s = w["search"]["actions"]
    old = lambda text: bool(re.search(r"(?<!S)E\(2\)=SO\(2\)\\ltimes\\mathbbR\^2", gate.squash(text)))
    pr = s["projectors"]
    got = {"erratum": [old(w["paper00"]), old(w["paper01"])], "matrices": s["matrices"], "eigen": s["eigen"], "equivariant": s["equivariant"],
           "generator": s["generator"], "placements": s["placements"] < 1e-14, "steiner": s["steiner"] < 1e-14, "ranks_C": pr["ranks_C"],
           "ranks_D": pr["ranks_D"], "idempotent": pr["idempotent"] < 1e-12, "images": pr["images"], "axis": pr["axis"]}
    w["w1"] = got
    return [] if got == w["actions_claim"] else [f"the actions: {got}"]


def w2_variations(w):
    """sec. 34: on an H-invariant body every breaking mode has delta A = 0; sec. 36-37: the Fourier diagonal"""
    s = w["search"]["variations"]
    got = {"gram": s["gram"], "bodies": {k: {"breaking": b["breaking"] < 1e-14, "translation": b["translation"] < 1e-14, "parts": b["parts"] < 1e-12,
                                             "preserving": b["preserving"]} for k, b in s["bodies"].items()}, "qdag": sig(s["qdag"])}
    w["w2"] = got
    return [] if got == w["variations_claim"] else [f"the variations: {got}"]


def w3_gcd(w):
    s = w["search"]["gcd"]
    got = {"law": s["law"], "misses": s["misses"], "m1": s["m1"], "examples": s["examples"]}
    w["w3"] = got
    return [] if got == w["gcd_claim"] else [f"the gcd law: {got}"]


def w4_caps(w):
    s = w["search"]["caps"]
    sm = s["smooth"]
    got = {"smooth": {"kappa0": round(sm["kappa0"], 4), "ratio": [round(x, 3) for x in sm["ratio"]], "last": [round(x, 3) for x in sm["last"]]},
           "bodies": s["bodies"]}
    w["w4"] = got
    return [] if got == w["caps_claim"] else [f"the caps: {got}"]


def w5_seed(w):
    s = w["search"]["seed"]
    line = s["line"]
    got = {"mirror": abs(s["aq"] - s["am"]) < 1e-14, "z": round(s["a0"], 13), "lower_by": sig(s["aq"] - s["a0"], 3),
           "kinks": {k: [round(x, 4) for x in v] for k, v in s["kinks"].items()}, "rise": all(x > 0 for x in s["least"].values()),
           "least": {k: sig(v) for k, v in s["least"].items()}, "line": all(line[i] > line[i + 1] for i in range(len(line) - 1)),
           "stabilizers": s["stabilizers"], "supports": s["supports"] < 1e-15}
    w["w5"] = got
    return [] if got == w["seed_claim"] else [f"the seed: {got}"]


def w6_records(w):
    """the schema's outputs are sec. 44's; its mode classes sec. 8's; its operators and gcd prediction sec. 9, 17, 26's;
    each completed item of the state has its section; which 'not claimed' items the text itself disclaims"""
    sc, st, text = w["schema"], w["state"], gate.squash(w["text"])
    m44 = re.search(r"#\s*44\.\s*Compiler Output\s*```text\s*(.*?)```", w["text"], re.S)
    got = {"outputs": m44 is not None and sc["compiler_outputs"] == m44.group(1).split(),
           "modes": sorted(sc["mode_classes"]) == ["m0", "m1", "m_ge_2"], "eigenvalue": sc["operators"]["mode_eigenvalue"] == "1-m^2",
           "prediction": sc["symmetry_branch"]["aligned_Dn_pure_mode_prediction"] == "D_gcd(n,m)",
           "erratum": sc["notation_erratum"] == {"orientation_preserving_euclidean_group": "SE(2)=SO(2)⋉R^2", "full_euclidean_group": "E(2)=O(2)⋉R^2"},
           "unmapped": [x for x in st["completed"] if x not in STATE_MAP or f"#{STATE_MAP[x][0]}.{STATE_MAP[x][1]}" not in text],
           "completed": len(st["completed"]), "disclaimed": [x in DISCLAIMERS and DISCLAIMERS[x] in text for x in st["not_claimed"]],
           "next": [sc["next_paper"] in w["paper03"][:300], st["next"] == "Paper 03 — " + sc["next_paper"]]}
    w["w6"] = got
    return [] if got == w["records_claim"] else [f"the records: {got}"]


def w7_scaffold(w):
    """the scaffold, read: which of sec. 44's six outputs it can return; whether the convexity cap it computes is
    used; whether it knows the rotation gauge (sec. 46) - for a D_n seed 1 + a cos n theta the turn's tangent is
    sin n theta, which it would call BREAK_TO_K; its m = 0 class"""
    code = w["scaffold"]
    m44 = re.search(r"#\s*44\.\s*Compiler Output\s*```text\s*(.*?)```", w["text"], re.S)
    got = {"lines": code.count("\n"), "defs": re.findall(r"^def (\w+)", code, re.M), "classes": re.findall(r"^class (\w+)", code, re.M),
           "outputs": [o for o in (m44.group(1).split() if m44 else []) if f'"{o}"' in code], "cap_used": len(re.findall(r"\bcap\b", code)) > 1,
           "rotation_gauge": bool(re.search(r"rotat|tangent", code, re.I)), "scale_mode": bool(re.search(r'if m == 0:\s*return "SCALE_MODE"', code))}
    w["w7"] = got
    return [] if got == w["scaffold_facts"] else [f"the scaffold: {got}"]


def p1_package(w):
    v, B = w["validation"], w["bytes"]
    sha = lambda b: hashlib.sha256(b).hexdigest()
    body = B[DOC].decode("utf-8")
    got = {"digests": [v["paper_sha256"] == sha(B[DOC]), v["schema_sha256"] == sha(B[SCHEMA]), v["scaffold_sha256"] == sha(B[SCAFFOLD]),
                       v["state_sha256"] == sha(B[STATE])],
           "utf8": v["utf8"] is True, "delimiters": [v["math_delimiters"], sorted(set(re.findall(r"\\\(|\\\)|\\\[|\\\]", body)))],
           "manifest": all(w["manifest_rows"][k][2] == sha(b) for k, b in B.items())}
    w["p1"] = got
    return [] if got == w["package_claim"] else [f"the package: {got}"]


CHECKS = {"A0": a0_anchors, "W1": w1_actions, "W2": w2_variations, "W3": w3_gcd, "W4": w4_caps, "W5": w5_seed, "W6": w6_records,
          "W7": w7_scaffold, "P1": p1_package}
DEPENDS = {"A0": {"text"}, "W1": {"search", "paper00", "paper01", "actions_claim"}, "W2": {"search", "variations_claim"},
           "W3": {"search", "gcd_claim"}, "W4": {"search", "caps_claim"}, "W5": {"search", "seed_claim"},
           "W6": {"schema", "state", "text", "paper03", "records_claim"}, "W7": {"scaffold", "text", "scaffold_facts"},
           "P1": {"validation", "bytes", "manifest_rows", "package_claim"}}


def drill_cases(base):
    def edit(obj, fn):
        x = copy.deepcopy(obj)
        fn(x)
        return x

    text, s = base["text"], base["search"]
    return [
        ("K0-unmodified", "control", {}, set()),
        ("K1-crlf", "control", {"text": text.replace("\n", "\r\n"), "scaffold": base["scaffold"].replace("\n", "\r\n")}, set()),
        ("K2-validation-keys-sorted", "control", {"validation": json.loads(json.dumps(base["validation"], sort_keys=True))}, set()),
        ("M1-gcd-text", "mutation", {"text": text.replace("D_{\\gcd(n,m)}.\n}", "D_{\\mathrm{lcm}(n,m)}.\n}", 1)}, {"A0"}),
        ("M2-erratum-source", "mutation", {"paper01": base["paper01"].replace("SO(2)\\ltimes\\mathbb R^2", "O(2)\\ltimes\\mathbb R^2", 1)}, {"W1"}),
        ("M3-projector-rank", "mutation", {"search": edit(s, lambda x: x["actions"]["projectors"]["ranks_D"].__setitem__(3, 8))}, {"W1"}),
        ("M4-breaking-first-variation", "mutation", {"search": edit(s, lambda x: x["variations"]["bodies"]["kernel"].update(breaking=1e-6))}, {"W2"}),
        ("M5-gram-off-diagonal", "mutation", {"search": edit(s, lambda x: x["variations"]["gram"].update(off=2))}, {"W2"}),
        ("M6-gcd-miss", "mutation", {"search": edit(s, lambda x: x["gcd"]["law"].__setitem__(0, x["gcd"]["law"][0] - 1))}, {"W3"}),
        ("M7-translation-intrinsic", "mutation", {"search": edit(s, lambda x: x["gcd"]["m1"][0].__setitem__(1, [1, 1]))}, {"W3"}),
        ("M8-reuleaux-admissible", "mutation", {"search": edit(s, lambda x: x["caps"]["bodies"]["reuleaux3"]["admissible"].insert(0, 2))}, {"W4"}),
        ("M9-smooth-ratio", "mutation", {"search": edit(s, lambda x: x["caps"]["smooth"]["ratio"].__setitem__(0, 0.99))}, {"W4"}),
        ("M10-seed-lower", "mutation", {"search": edit(s, lambda x: x["seed"].update(aq=x["seed"]["a0"] - 1e-12))}, {"W5"}),
        ("M11-flat-breaking", "mutation", {"search": edit(s, lambda x: x["seed"]["kinks"].update(y3=[0.0, 0.0]))}, {"W5"}),
        ("M12-schema-output", "mutation", {"schema": edit(base["schema"], lambda x: x["compiler_outputs"].append("REOPEN"))}, {"W6"}),
        ("M13-state-completed", "mutation", {"state": edit(base["state"], lambda x: x["completed"].append("orbit closure theorem"))}, {"W6"}),
        ("M14-scaffold-blocked", "mutation", {"scaffold": base["scaffold"] + '\ndef blocked(cap):\n    return "CONVEXITY_BLOCKED" if cap == 0 else None\n'}, {"W7"}),
        ("M15-scaffold-m0", "mutation", {"scaffold": base["scaffold"].replace('"SCALE_MODE"', '"PARALLEL_BODY_MODE"')}, {"W7"}),
        ("M16-validation-digest", "mutation", {"validation": {**base["validation"], "scaffold_sha256": "0" * 64}}, {"P1"}),
        ("M17-state-bytes", "mutation", {"bytes": {**base["bytes"], STATE: base["bytes"][STATE] + b" "}}, {"P1"}),
    ]


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--drill", action="store_true")
    ap.add_argument("--blind", metavar="CID")
    args = ap.parse_args()
    base = {**load_inputs(), "search": searches()}

    def build():
        return {**copy.deepcopy(base), **copy.deepcopy(CLAIMS)}

    if args.blind:
        CHECKS[args.blind] = lambda w: []
        args.drill = True
    w = build()
    results = {}
    for c, fn in CHECKS.items():
        try:
            results[c] = fn(w)
        except KeyError as e:
            results[c] = [f"claim missing: {e}"]
    for c, f in results.items():
        print(f"{c} {'PASS' if not f else 'FAIL'}", *(f"\n     {x}" for x in f[:6]))
    for key in ("w1", "w2", "w3", "w4", "w5", "w6", "w7", "p1"):
        print(key, "=", json.dumps(w.get(key), default=str, ensure_ascii=False))
    if not args.drill:
        return
    cases = drill_cases(base)
    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-052", "subject": "LESR Paper 02: group-orbit shape expansion and symmetry breaking", "document": DOC,
           "checks": {c: {"pass": not f, "failures": f} for c, f in results.items()},
           **{k: w.get(k) for k in ("w1", "w2", "w3", "w4", "w5", "w6", "w7", "p1")}, "drill": summary}
    LOG.write_text(json.dumps(log, ensure_ascii=False, indent=1, default=str) + "\n", encoding="utf-8")


if __name__ == "__main__":
    main()
