"""RUN-004 - Round 03 (v0.3): mirror-symmetric five- and seven-link families on the exact triangle formula.

Round 03 restricts the search to mirror-symmetric polylines - five links with directions (alpha, beta, 0, -beta,
-alpha) and lengths (l1, l2, l3, l2, l1), seven links with (alpha, beta, gamma, 0, -gamma, -beta, -alpha) and (l1, l2,
l3, l4, l3, l2, l1) - and maximizes the least exact scale over all rotations in T/1.0048 (RUN-003's formula). It
reports s3 = 0.998574374711 (Round 02's congruence three-link), s5 = 0.998754371668 and s7 = 0.998674157956, a
four-contact structure (both endpoints on the hypotenuse, one interior vertex on each leg), resolution tables, phase
metrics, a contact-pressure entropy, and concludes that seven links do not improve on five because the added links
degenerate into near-parallel pieces.

Checks (each returns a list of failures; empty = pass)
  Z1 values         each logged curve is its stated parameters (lengths summing to 1); its least exact scale over
                    all phases is the stated s, at the stated phase, to 1e-10; the five- and seven-link curves'
                    mirror images give the same
  Z2 contacts       at each curve's minimizing phase, the vertices on the leg x = 0, the leg y = 0 and the
                    hypotenuse are the recorded ones, with the recorded multiplicity; for the five- and seven-link
                    curves these are the stated skeleton: both endpoints on the hypotenuse, one interior vertex on
                    each leg
  Z3 derived        the stated differences and distances to 1 are the differences they name
  Z4 resolution     each row of the resolution table: the strict local minima of the exact scale on that phase grid
                    number as stated, and the refined scale and phase are this run's to 1e-11 and 1e-8; the summary
                    holds the same table, and each curve's count is its finest row's
  Z5 metrics        each curve's "metrics" block - minimum, maximum, barrier, local minima, count - is the unrefined
                    exact scale on an 8,192-phase grid
  Z6 nesting        each family's reported optimum is at least the value of every curve of a family it contains: the
                    seven-link family holds every five-link curve (gamma = beta, the second side split in two
                    collinear pieces) and both hold the symmetric three-link with equal thirds
  Z7 entropy        the stated contact-pressure entropy is the normalized entropy of the balance weights of the three
                    constraint rows as the exact formula writes them (x >= 0, y >= 0, x/A + y/B <= s): (1/A, 1/B, 1)
  Z8 consistency    the README and the report state the data's numbers, parameters and contact ledgers
  Z9 method         METHOD.md and the report state the same exact formula, and METHOD's "the curve equals its mirror
                    up to parameter reversal" holds for the logged curves

Drill: each plant names the checks that must trip - trip meaning a failure the unplanted package does not already
have; controls must trip nothing.

Usage:
  python code/run004_round03_symmetric_links.py [--drill [--log PATH] | --blind Z#]
"""
import argparse
import copy
import csv
import io
import json
import math
import pathlib
import re

import numpy as np
from scipy.optimize import minimize

from corpus import TREE, members, sha256, source_bytes
import gate
from run003_round02_phase_scan_chirality import exact_min, exact_scale, golden, legs, mirror

RID = "round-v0.3"
LOG = TREE / "data" / "gate-logs" / "run004-round03-symmetric-links.json"
VALUE_TOL = 1e-10
CONTACT_TOL = 1e-9
CURVES = ("baseline_3link", "best_5link", "best_7link")


# ---------------------------------------------------------------- curves

def polyline(dirs_deg, lengths):
    pts = [np.zeros(2)]
    for d, l in zip(dirs_deg, lengths):
        pts.append(pts[-1] + l * np.array([math.cos(math.radians(d)), math.sin(math.radians(d))]))
    return np.array(pts)


def five_link(l1, l2, l3, a, b):
    return polyline([a, b, 0, -b, -a], [l1, l2, l3, l2, l1])


def seven_link(l1, l2, l3, l4, a, b, g):
    return polyline([a, b, g, 0, -g, -b, -a], [l1, l2, l3, l4, l3, l2, l1])


def three_sym(alpha, l=1 / 3):
    return polyline([alpha, 0, -alpha], [l, 1 - 2 * l, l])


# ---------------------------------------------------------------- the world

def load(rid):
    return {m.path.split("/", 1)[1]: b for m, b in members(rid, source_bytes(rid), keep=lambda m: True)}


def same_B(B):
    # one function object for every world: exact_min's memo keys on its id
    return B


def build_world():
    f, f2 = load(RID), load("round-v0.2.2")
    xy = lambda b: np.array([[float(r["x"]), float(r["y"])] for r in csv.DictReader(io.StringIO(b.decode("utf-8")))])
    return {
        "readme": f["README.md"].decode("utf-8"), "report": f["reports/ROUND_03_REPORT.md"].decode("utf-8"),
        "method": f["src/METHOD.md"].decode("utf-8"),
        "summary": json.loads(f["data/round03_summary.json"]),
        "resolution": json.loads(f["data/resolution_stability.json"]),
        # the baseline is Round 02's congruence three-link (its orientation-preserving value is Round 03's s3)
        "curves": {"best_5link": xy(f["data/best_5link_curve.csv"]), "best_7link": xy(f["data/best_7link_curve.csv"]),
                   "baseline_3link": xy(f2["data/best_congruence_three_link_curve.csv"])},
        "legs": ((3 + 4 * math.sqrt(3)) / 18, (4 + math.sqrt(3)) / 6),
        "cert_scale_divisor": 1.0048,
        "rot_sign": 1,
        "formula_B": same_B,
        "metrics_grid": 8192,
    }


def stated_curve(w, name):
    """the curve its stated parameters build"""
    prm = w["summary"][name]["parameters"]
    if name == "best_5link":
        return five_link(prm["l1"], prm["l2"], prm["l3"], prm["outer_angle_degrees"], prm["inner_angle_degrees"])
    return seven_link(prm["l1"], prm["l2"], prm["l3"], prm["l4"], prm["outer_angle_degrees"],
                      prm["middle_angle_degrees"], prm["inner_angle_degrees"])


# ---------------------------------------------------------------- the checks

def z1_values(w):
    failures = []
    S = w["summary"]
    for name in ("best_5link", "best_7link"):
        p = w["curves"][name]
        q = stated_curve(w, name)
        if (p - p[0]).shape != q.shape or np.abs((p - p[0]) - q).max() > 1e-12:
            failures.append(f"{name}: the logged curve is not its stated parameters")
        total = np.linalg.norm(np.diff(p, axis=0), axis=1).sum()
        if abs(total - 1) > 1e-12:
            failures.append(f"{name}: length {total!r}")
    for name in CURVES:
        p = w["curves"][name]
        s, at = exact_min(w, p)
        if abs(s - S[name]["scale"]) > VALUE_TOL:
            failures.append(f"{name}: least exact scale {s:.12f}, stated {S[name]['scale']:.12f}")
        if abs(at - S[name]["phase"]) > 1e-8:
            failures.append(f"{name}: minimizing phase {at:.10f}, stated {S[name]['phase']:.10f}")
        if name != "baseline_3link":
            m = exact_min(w, mirror(p))[0]
            if abs(m - s) > 1e-12:
                failures.append(f"{name}: mirror image gives {m!r}, the curve {s!r} - not mirror-symmetric")
    return failures


def contacts(w, p, phi, tol=CONTACT_TOL):
    """vertex indices on the leg x = 0, the leg y = 0 and the hypotenuse at phase phi (RUN-003's placement)"""
    A, B = legs(w)
    c, s = math.cos(w["rot_sign"] * phi), math.sin(w["rot_sign"] * phi)
    q = p @ np.array([[c, s], [-s, c]])
    x, y = q[:, 0], q[:, 1]
    hyp = x / A + y / B
    return (np.flatnonzero(x - x.min() < tol).tolist(), np.flatnonzero(y - y.min() < tol).tolist(),
            np.flatnonzero(hyp.max() - hyp < tol).tolist())


def z2_contacts(w):
    failures = []
    for name in CURVES:
        led = w["summary"][name]["contact_ledger"]
        p = w["curves"][name]
        _, at = exact_min(w, p)
        left, bottom, hyp = contacts(w, p, at)
        got = (left, bottom, hyp, len(left) + len(bottom) + len(hyp))
        want = (led["left_contact_vertices"], led["bottom_contact_vertices"], led["hypotenuse_contact_vertices"],
                led["contact_multiplicity"])
        if got != want:
            failures.append(f"{name}: contacts {got}, recorded {want}")
        last = len(p) - 1
        if name != "baseline_3link" and not (hyp == [0, last] and len(left) == 1 and len(bottom) == 1
                                             and 0 < left[0] < last and 0 < bottom[0] < last):
            failures.append(f"{name}: contacts {got[:3]} are not both endpoints on the hypotenuse and one interior "
                            f"vertex on each leg")
    return failures


def z3_derived(w):
    failures = []
    S = w["summary"]
    s3, s5, s7 = (S[n]["scale"] for n in CURVES)
    for label, got, want in (("5 over 3", S["best_5link"]["improvement_over_3link"], s5 - s3),
                             ("7 over 3", S["best_7link"]["improvement_over_3link"], s7 - s3),
                             ("7 over 5", S["best_7link"]["improvement_over_5link"], s7 - s5),
                             ("5 to one", S["best_5link"]["distance_to_one"], 1 - s5),
                             ("7 to one", S["best_7link"]["distance_to_one"], 1 - s7)):
        if abs(got - want) > 1e-15:
            failures.append(f"{label}: stated {got!r}, the difference is {want!r}")
    return failures


def grid_values(w, p, n):
    return np.array([exact_scale(w, p, 2 * math.pi * k / n) for k in range(n)])


def strict_minima(vals):
    return vals[(vals < np.roll(vals, 1)) & (vals < np.roll(vals, -1))]


def z4_resolution(w):
    failures = []
    for name, rows in w["resolution"].items():
        p = w["curves"][name]
        s, at = exact_min(w, p)
        for r in rows:
            n = strict_minima(grid_values(w, p, r["phase_grid"])).size
            if n != r["local_minimum_count"]:
                failures.append(f"{name} on {r['phase_grid']} phases: {n} strict local minima, stated {r['local_minimum_count']}")
            if abs(r["scale"] - s) > 1e-11 or abs(r["phase"] - at) > 1e-8:
                failures.append(f"{name} on {r['phase_grid']} phases: refined ({r['scale']:.13f}, {r['phase']:.10f}) "
                                f"vs this run's ({s:.13f}, {at:.10f})")
        finest = max(rows, key=lambda r: r["phase_grid"])
        if w["summary"][name]["local_minimum_count"] != finest["local_minimum_count"]:
            failures.append(f"{name}: the summary's count {w['summary'][name]['local_minimum_count']} is not its "
                            f"finest row's {finest['local_minimum_count']}")
    if w["resolution"] != w["summary"]["resolution_stability"]:
        failures.append("the summary's resolution table is not resolution_stability.json")
    return failures


def grid_metrics(w, p, n):
    vals = grid_values(w, p, n)
    lm = strict_minima(vals)
    return {"minimum": float(vals.min()), "maximum": float(vals.max()), "barrier": float(vals.max() - vals.min()),
            "local_minimum_count": int(lm.size), "best_local_minima": sorted(float(x) for x in lm)}


def z5_metrics(w):
    failures = []
    for name in CURVES:
        stated = w["summary"][name]["metrics"]
        got = grid_metrics(w, w["curves"][name], w["metrics_grid"])
        for key in ("minimum", "maximum", "barrier", "local_minimum_count"):
            if abs(got[key] - stated[key]) > 1e-12:
                failures.append(f"{name}: metrics {key} {stated[key]!r}, the {w['metrics_grid']}-phase grid gives {got[key]!r}")
        if len(got["best_local_minima"]) != len(stated["best_local_minima"]) or \
                max(abs(a - b) for a, b in zip(got["best_local_minima"], stated["best_local_minima"])) > 1e-12:
            failures.append(f"{name}: the metrics' local minima are not the {w['metrics_grid']}-phase grid's")
    return failures


def five_in_seven(p5):
    """a five-link curve as a member of the seven-link family: gamma = beta, its second side split in two"""
    return seven_link(p5["l1"], p5["l2"] / 2, p5["l2"] / 2, p5["l3"], p5["outer_angle_degrees"],
                      p5["inner_angle_degrees"], p5["inner_angle_degrees"])


def z6_nesting(w):
    """a family's optimum is at least the value of any curve it contains"""
    failures = []
    S = w["summary"]
    as7 = five_in_seven(S["best_5link"]["parameters"])
    v5in7 = exact_min(w, as7)[0]
    if S["best_7link"]["scale"] < v5in7 - 1e-12:
        failures.append(f"the seven-link family holds the reported five-link (gamma = beta, its second side split), at "
                        f"{v5in7:.12f}; its reported optimum {S['best_7link']['scale']:.12f} is below it")
    a3 = w["thirds_alpha"]
    v3 = exact_min(w, three_sym(a3))[0]
    for name, member in (("best_5link", five_link(1 / 6, 1 / 6, 1 / 3, a3, a3)),
                         ("best_7link", seven_link(1 / 9, 1 / 9, 1 / 9, 1 / 3, a3, a3, a3))):
        v = exact_min(w, member)[0]
        if abs(v - v3) > 1e-12:
            failures.append(f"{name}: the three-link written in this family gives {v!r}, the three-link {v3!r}")
        if S[name]["scale"] < v - 1e-12:
            failures.append(f"{name}: the family holds the symmetric three-link with equal thirds and turns "
                            f"{a3:.8f} deg, at {v:.12f}; its reported optimum {S[name]['scale']:.12f} is below it")
    return failures


def entropy(weights):
    q = np.array(weights, dtype=float) / sum(weights)
    return float(-(q * np.log(q)).sum() / math.log(3))


def z7_entropy(w):
    A, B = legs(w)
    H = entropy((1 / A, 1 / B, 1.0))
    stated = w["summary"]["triangle_contact_pressure_entropy"]
    return [] if abs(H - stated) < 1e-12 else [f"the rows' balance weights (1/A, 1/B, 1) give H = {H:.13f}, stated {stated:.13f}"]


def num(pattern, text):
    m = re.search(pattern, text)
    return float(m.group(1).rstrip(".")) if m else None


def z8_consistency(w):
    failures = []
    S, rd, rp = w["summary"], gate.squash(w["readme"]), gate.squash(w["report"])

    def same(label, got, want, digits):
        if got is None or abs(got - want) > 0.6 * 10 ** -digits:
            failures.append(f"{label}: stated {got}, the data {want!r}")
    s3, s5, s7 = (S[n]["scale"] for n in CURVES)
    same("README s3", num(r"三連桿基準：(\d+\.\d+)", rd), s3, 12)
    same("README s5", num(r"最佳五連桿：(\d+\.\d+)", rd), s5, 12)
    same("README s7", num(r"最佳七連桿：(\d+\.\d+)", rd), s7, 12)
    same("README 5 over 3", num(r"五對三增益：(-?\d+\.\d+e-?\d+)", rd), s5 - s3, 15)
    same("README 7 over 5", num(r"七對五增益：(-?\d+\.\d+e-?\d+)", rd), s7 - s5, 15)
    same("report s3", num(r"s_3=(\d+\.\d+)", rp), s3, 12)
    same("report phi3", num(r"\\phi_3=(\d+\.\d+)", rp), S["baseline_3link"]["phase"], 12)
    same("report s5", num(r"s_5=(\d+\.\d+)", rp), s5, 12)
    same("report phi5", num(r"\\phi_5=(\d+\.\d+)", rp), S["best_5link"]["phase"], 12)
    same("report s7", num(r"s_7=(\d+\.\d+)", rp), s7, 12)
    same("report 5 over 3", num(r"s_5-s_3=(-?\d+\.\d+e-?\d+)", rp), s5 - s3, 15)
    same("report 7 over 3", num(r"s_7-s_3=(-?\d+\.\d+e-?\d+)", rp), s7 - s3, 15)
    same("report 7 over 5", num(r"s_7-s_5=(-?\d+\.\d+e-?\d+)", rp), s7 - s5, 15)
    same("report 1-s5", num(r"1-s_5=(\d+\.\d+e-?\d+)", rp), 1 - s5, 15)
    same("report H", num(r"H_\\lambda=(\d+\.\d+)", rp), S["triangle_contact_pressure_entropy"], 12)
    for key, pat in (("best_5link", r"\(l_1,l_2,l_3,l_2,l_1\)=\(([\d.,]+)\)"),
                     ("best_7link", r"\(l_1,l_2,l_3,l_4,l_3,l_2,l_1\)=\(([\d.,]+)\)")):
        m = re.search(pat, rp)
        prm = S[key]["parameters"]
        ls = [prm["l1"], prm["l2"], prm["l3"]] + ([prm["l4"]] if "l4" in prm else [])
        full = ls + ls[-2::-1]
        if not m or len(m.group(1).split(",")) != len(full) or \
                any(abs(float(a) - b) > 6e-11 for a, b in zip(m.group(1).split(","), full)):
            failures.append(f"{key}: the report's length structure is not the data's")
    for key, title, labels in (("best_5link", "五連桿結果", (("alpha", "outer"), ("beta", "inner"))),
                               ("best_7link", "七連桿結果", (("alpha", "outer"), ("beta", "middle"), ("gamma", "inner")))):
        part = rp[rp.index(title):]
        for sym, k in labels:
            same(f"{key} {sym}", num(rf"\\{sym}=(\d+\.\d+)\^", part), S[key]["parameters"][f"{k}_angle_degrees"], 8)
    for key, title in (("baseline_3link", "三連桿基準"), ("best_5link", "五連桿結果"), ("best_7link", "七連桿結果")):
        part = w["report"][w["report"].index(title):]
        m = re.search(r"左邊界頂點 \[(.*?)\]；底邊頂點 \[(.*?)\]；斜邊頂點 \[(.*?)\]", part)
        led = S[key]["contact_ledger"]
        want = [led["left_contact_vertices"], led["bottom_contact_vertices"], led["hypotenuse_contact_vertices"]]
        got = [[int(x) for x in g.split(",")] for g in m.groups()] if m else None
        if got != want:
            failures.append(f"{key}: the report's contact ledger {got} vs the data's {want}")
    return failures


FORMULA = ("s(\\phi)=\\max_i\\left(\\frac{x_i(\\phi)}A+\\frac{y_i(\\phi)}B\\right)-\\frac{\\min_ix_i(\\phi)}A"
           "-\\frac{\\min_iy_i(\\phi)}B")


def z9_method(w):
    failures = []
    for label, text in (("METHOD.md", w["method"]), ("the report", w["report"])):
        if FORMULA not in gate.squash(text):
            failures.append(f"{label} does not state the exact three-facet formula")
    for name in ("best_5link", "best_7link"):
        p = w["curves"][name]
        r = (p * np.array([-1.0, 1.0]))[::-1]           # reflected across the y-axis, traversed backwards
        if np.abs((r - r[0]) - (p - p[0])).max() > 1e-12:
            failures.append(f"{name}: not its own mirror image up to parameter reversal")
    return failures


CHECKS = {"Z1": z1_values, "Z2": z2_contacts, "Z3": z3_derived, "Z4": z4_resolution, "Z5": z5_metrics,
          "Z6": z6_nesting, "Z7": z7_entropy, "Z8": z8_consistency, "Z9": z9_method}
GEOM = {"legs", "cert_scale_divisor", "rot_sign", "formula_B"}
DEPENDS = {
    "Z1": {"curves", "summary"} | GEOM,
    "Z2": {"curves", "summary"} | GEOM,
    "Z3": {"summary"},
    "Z4": {"curves", "resolution", "summary"} | GEOM,
    "Z5": {"curves", "summary", "metrics_grid"} | GEOM,
    "Z6": {"summary", "thirds_alpha"} | GEOM,
    "Z7": {"summary", "legs", "cert_scale_divisor"},
    "Z8": {"readme", "report", "summary"},
    "Z9": {"method", "report", "curves"},
}


# ---------------------------------------------------------------- findings

def family_search(w, kind, starts):
    """this run's own search of a family for its largest least exact scale (Nelder-Mead on 360-phase scans with
    golden refinement; each result re-evaluated on the full 3,600-phase scan, the winner also on 65,536 phases)"""
    def build(x):
        if kind == 5:
            l1, l2, a, b = x
            return five_link(l1, l2, 1 - 2 * l1 - 2 * l2, a, b), min(l1, l2, 1 - 2 * l1 - 2 * l2)
        l1, l2, l3, a, b, g = x
        return seven_link(l1, l2, l3, 1 - 2 * (l1 + l2 + l3), a, b, g), min(l1, l2, l3, 1 - 2 * (l1 + l2 + l3))

    def neg(x):
        p, short = build(x)
        return 0.0 if short <= 1e-4 else -exact_min(w, p, scan=360)[0]
    runs = []
    for x0 in starts:
        r = minimize(neg, x0, method="Nelder-Mead", options={"xatol": 1e-10, "fatol": 1e-13, "maxiter": 3000})
        p, _ = build(r.x)
        runs.append({"start": [float(v) for v in x0], "parameters": [float(v) for v in r.x],
                     "value": exact_min(w, p)[0]})
    top = max(runs, key=lambda x: x["value"])
    p, _ = build(top["parameters"])
    # the winner on a 65,536-phase scan, every local minimum refined, against a minimum narrower than the scan
    branches = branch_minima(w, p)
    return {"runs": runs, "best": top["value"], "best_on_65536_phases": exact_min(w, p, scan=65536)[0],
            "best_branches": branches,
            # the vertices on the leg x = 0, the leg y = 0 and the hypotenuse at each branch level with the least
            "best_level_branch_contacts": [[v, f, contacts(w, p, f * math.pi)] for v, f in branches
                                           if v - branches[0][0] < 1e-9]}


def branch_minima(w, p, n=8192, k=5):
    """the k lowest local minima over phases, each refined by golden section: (value, phase / pi)"""
    phis = 2 * math.pi * np.arange(n) / n
    vals = grid_values(w, p, n)
    step = 2 * math.pi / n
    out = []
    for i in range(n):
        if vals[i] <= vals[i - 1] and vals[i] <= vals[(i + 1) % n]:
            v, x = golden(lambda t: exact_scale(w, p, t), phis[i] - step, phis[i] + step)
            out.append([float(v), float((x % (2 * math.pi)) / math.pi)])
    return sorted(out)[:k]


def findings(w):
    S = w["summary"]
    s3, s5, s7 = (S[n]["scale"] for n in CURVES)
    a3 = w["thirds_alpha"]
    v3 = exact_min(w, three_sym(a3))[0]
    p5, p7 = S["best_5link"]["parameters"], S["best_7link"]["parameters"]
    five = family_search(w, 5, [(p5["l1"], p5["l2"], p5["outer_angle_degrees"], p5["inner_angle_degrees"]),
                                (1 / 6, 1 / 6, a3, a3)])
    top5 = max(five["runs"], key=lambda x: x["value"])["parameters"]
    seven = family_search(w, 7, [(p7["l1"], p7["l2"], p7["l3"], p7["outer_angle_degrees"], p7["middle_angle_degrees"],
                                  p7["inner_angle_degrees"]),
                                 (top5[0], top5[1] / 2, top5[1] / 2, top5[2], top5[3], top5[3])])
    A, B = legs(w)
    base = w["curves"]["baseline_3link"]
    axis = {}
    for n in CURVES:
        # at phase 2 pi/3 a curve symmetric about the y-axis has its axis along the hypotenuse's normal (30 deg), so
        # its two endpoints, mirror images, have equal hypotenuse values
        p = w["curves"][n]
        s, at = exact_min(w, p)
        c, si = math.cos(2 * math.pi / 3), math.sin(2 * math.pi / 3)
        q = p @ np.array([[c, si], [-si, c]])
        h = q[:, 0] / A + q[:, 1] / B
        e = 1e-6
        axis[n] = {"phase_minus_2pi_3": at - 2 * math.pi / 3,
                   "scale_at_2pi_3_minus_minimum": exact_scale(w, p, 2 * math.pi / 3) - s,
                   "endpoint_hypotenuse_gap_at_2pi_3": float(h[0] - h[-1]),
                   "one_sided_slopes": [(s - exact_scale(w, p, at - e)) / e, (exact_scale(w, p, at + e) - s) / e]}
    deviation = {}
    for n, rows in w["resolution"].items():
        s, at = exact_min(w, w["curves"][n])
        deviation[n] = {"scale": max(abs(r["scale"] - s) for r in rows), "phase": max(abs(r["phase"] - at) for r in rows)}
    # the round's five-link with l1 raised by 1e-4 and l3 lowered by 2e-4 (drill plant M15's parameters)
    nudged = five_link(p5["l1"] + 1e-4, p5["l2"], p5["l3"] - 2e-4, p5["outer_angle_degrees"], p5["inner_angle_degrees"])
    a0, b0 = w["legs"]
    return {
        "exact_minima": {n: list(exact_min(w, w["curves"][n])) for n in CURVES},
        "resolution_max_deviation": deviation,
        "five_link_nudged": {"value": exact_min(w, nudged)[0], "minus_reported": exact_min(w, nudged)[0] - s5},
        "axis_phase": axis,
        "round_branches": {n: branch_minima(w, w["curves"][n]) for n in ("best_5link", "best_7link")},
        "five_in_seven": {"value": exact_min(w, five_in_seven(p5))[0], "reported_seven": s7},
        "equal_thirds_three_link": {"alpha_degrees": a3, "value": v3, "above_s3": v3 - s3, "above_s7": v3 - s7,
                                    "s5_over_it": s5 - v3},
        "five_link_search": dict(five, reported=s5, best_minus_reported=five["best"] - s5),
        "seven_link_search": dict(seven, reported=s7, best_minus_reported=seven["best"] - s7),
        "baseline_handedness": {"orientation_preserving": exact_min(w, base)[0],
                                "mirror": exact_min(w, mirror(base))[0],
                                "congruent": min(exact_min(w, base)[0], exact_min(w, mirror(base))[0])},
        "entropy_readings": {"formula_rows_1/A_1/B_1": entropy((1 / A, 1 / B, 1.0)),
                             "unit_normals_edge_lengths": entropy((A, B, math.hypot(A, B))),
                             "formula_rows_undivided_T": entropy((1 / a0, 1 / b0, 1.0)),
                             "stated": S["triangle_contact_pressure_entropy"]},
        "metrics_minimum_minus_refined": {n: S[n]["metrics"]["minimum"] - S[n]["scale"] for n in CURVES},
    }


# ---------------------------------------------------------------- the drill

def drill_cases(base):
    def edit(key, fn):
        v = copy.deepcopy(base[key])
        fn(v)
        return v
    s_scale = edit("summary", lambda s: s["best_5link"].__setitem__("scale", s["best_5link"]["scale"] + 1e-9))
    s_contact = edit("summary", lambda s: s["best_7link"]["contact_ledger"].__setitem__("left_contact_vertices", [3]))
    s_gain = edit("summary", lambda s: s["best_7link"].__setitem__("improvement_over_5link", -8.02137e-05))
    s_metric = edit("summary", lambda s: s["best_5link"]["metrics"].__setitem__("barrier", 0.2508482241))
    s_entropy = edit("summary", lambda s: s.__setitem__("triangle_contact_pressure_entropy",
                                                       s["triangle_contact_pressure_entropy"] + 1e-9))
    s_five_low = edit("summary", lambda s: s["best_5link"].__setitem__("scale", 0.99869))

    def add_hyp(s):
        led = s["best_5link"]["contact_ledger"]
        led["hypotenuse_contact_vertices"] = [0, 4, 5]
        led["contact_multiplicity"] = 5
    s_hyp = edit("summary", add_hyp)

    def shift_l1(s):
        prm = s["best_5link"]["parameters"]
        prm["l1"] += 1e-4
        prm["l3"] -= 2e-4
    s_param = edit("summary", shift_l1)
    res = edit("resolution", lambda r: r["best_7link"][1].__setitem__("local_minimum_count", 8))
    cv = edit("curves", lambda c: c["best_5link"].__setitem__((2, 1), c["best_5link"][2, 1] + 1e-6))
    rep = base["report"].replace("\\beta=80.87459061^\\circ", "\\beta=80.87459071^\\circ")
    readme = base["readme"].replace("最佳五連桿：0.998754371668", "最佳五連桿：0.998754371669")
    method = base["method"].replace("-\\frac{\\min_i y_i(\\phi)}B", "+\\frac{\\min_i y_i(\\phi)}B")
    for a, b in ((rep, base["report"]), (readme, base["readme"]), (method, base["method"])):
        assert a != b, "a plant must hit the raw text"
    a, b = base["legs"]
    return [
        ("K0-unmodified", "control", {}, set()),
        ("K1-report-crlf", "control", {"report": base["report"].replace("\n", "\r\n")}, set()),
        ("K2-curves-copied", "control", {"curves": copy.deepcopy(base["curves"])}, set()),
        ("K3-readme-trailing-spaces", "control", {"readme": "\n".join(l + " " for l in base["readme"].split("\n"))}, set()),
        ("M1-triangle-leg", "mutation", {"legs": (a * 1.0005, b)}, {"Z1", "Z4", "Z5", "Z7"}),
        ("M2-five-scale", "mutation", {"summary": s_scale}, {"Z1", "Z3", "Z8"}),
        ("M3-seven-contact", "mutation", {"summary": s_contact}, {"Z2", "Z8"}),
        ("M4-seven-gain", "mutation", {"summary": s_gain}, {"Z3"}),
        ("M5-metrics-barrier", "mutation", {"summary": s_metric}, {"Z5"}),
        ("M6-entropy", "mutation", {"summary": s_entropy}, {"Z7", "Z8"}),
        ("M7-five-below-three", "mutation", {"summary": s_five_low}, {"Z1", "Z3", "Z6", "Z8"}),
        ("M8-resolution-count", "mutation", {"resolution": res}, {"Z4"}),
        ("M9-curve-point", "mutation", {"curves": cv}, {"Z1", "Z4", "Z5", "Z9"}),
        ("M10-report-angle", "mutation", {"report": rep}, {"Z8"}),
        ("M11-readme-digit", "mutation", {"readme": readme}, {"Z8"}),
        ("M12-method-sign", "mutation", {"method": method}, {"Z9"}),
        ("M13-metrics-grid", "mutation", {"metrics_grid": 4096}, {"Z5"}),
        ("M14-formula-term", "mutation", {"formula_B": lambda B: 2 * B}, {"Z1", "Z4", "Z5"}),
        ("M15-five-parameters", "mutation", {"summary": s_param}, {"Z1", "Z6", "Z8"}),
        ("M16-hypotenuse-contact", "mutation", {"summary": s_hyp}, {"Z2", "Z8"}),
    ]


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--drill", action="store_true")
    ap.add_argument("--blind", metavar="CID")
    ap.add_argument("--log", default=None)
    args = ap.parse_args()
    if args.blind:
        CHECKS[args.blind] = lambda w: []
        args.drill = True
    probe = build_world()
    # the equal-thirds three-link's best turn (RUN-003: 82.435472 deg), recomputed here, then held fixed as data
    _, a3 = golden(lambda x: -exact_min(probe, three_sym(x))[0], 82.40, 82.47, iters=60)

    def world():
        w = build_world()
        w["thirds_alpha"] = a3
        return w
    w = world()
    results = {cid: fn(w) for cid, fn in CHECKS.items()}
    for cid, f in results.items():
        print(f"{cid} {'PASS' if not f else 'FAIL'}  ({len(f)} failures)")
        for x in f[:6]:
            print("    ", x)
    log = {"run": "RUN-004",
           "subject": "Round 03 (v0.3): mirror-symmetric five- and seven-link families on the exact triangle formula",
           "package": {"id": RID, "sha256": sha256(source_bytes(RID))},
           "checks": {cid: {"pass": not f, "failures": f} for cid, f in results.items()},
           "findings": None if args.blind else findings(w)}
    if log["findings"]:
        print(json.dumps(log["findings"], indent=1)[:4000])
    if args.drill:
        drill = gate.run_drill(world, CHECKS, DEPENDS, drill_cases(world()))
        log["drill"] = gate.summarize(drill, CHECKS)
        print(gate.report_drill(log["drill"]))
        for d in drill:
            if not d["ok"]:
                print("   DRILL FAIL", d)
    if args.blind:
        seen = sum(1 for d in log["drill"]["cases"] if args.blind in d["expected"] and args.blind not in d["tripped"])
        print(f"blind {args.blind}: reported blind in {seen} cases (log not written)")
        return
    if args.drill:
        path = pathlib.Path(args.log) if args.log else LOG
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(json.dumps(log, ensure_ascii=False, indent=1) + "\n", encoding="utf-8", newline="\n")


if __name__ == "__main__":
    main()
