"""RUN-008 - Round 07 (v0.7): segmented curvature wings and the constant-curvature arc wing against the event five-link.

Round 07 keeps Round 05's event five-link (RUN-006) as an explicit control and compares two smoothings of its wings,
all mirror-symmetric with a horizontal centre segment: wings of m = 2, 3, 4, 6, 8 straight pieces (lengths in a
geometric progression set by a "length skew", directions from alpha to beta shaped by a "curvature power"), found by
one heuristic search each; and wings that are circular arcs, their tangent turning linearly from alpha to beta, with
an analytic support computation, equalized on three phase branches at 0.998862397495093, 4.14e-5 below the event
scale. It audits its own search: the m = 2 family contains the event five-link, yet the search found 3.73e-4 less.

Checks (each returns a list of failures; empty = pass)
  C1 control        the control curve is Round 05's event five-link; its least scale is the event scale at a stated
                    phase that reaches it, with the nine stated local minima
  C2 segments       each segmented curve is its parameters: piece lengths in ratio exp(skew/(m-1)), directions
                    beta + delta (1 - k/(m-1))^power, wing and centre lengths as stated, total length 1
  C3 values         each segmented curve's least scale, its phase and its local-minimum count on 32,768 phases are
                    the stated ones
  C4 best           the m = 6 candidate's contact signature at its least phase, and each row of its resolution audit
  C5 arcs           both arc wings (before and after equalization): curvature -delta/w and centre 1 - 2w; their exact
                    least scales, a stated phase that reaches it, and the seven local minima; the equalized arc's
                    tracked branches and their spread
  C6 sample         the logged arc sample is the chord polyline of the arc wing before equalization (179 chords per
                    wing, 31 on the centre), rescaled to length 1, point for point to 1e-14
  C7 derived        the gains, distances, the ranking and the manifest's figures are the values they name
  C8 consistency    the README, the report, the interpretation note and the search audit state the data's numbers,
                    tables and signature

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/run008_round07_curvature_wings.py [--drill [--log PATH] | --blind C#]
"""
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, sha256, source_bytes
import gate
from run001_corpus_identity import restore_escapes
from run003_round02_phase_scan_chirality import exact_min, exact_scale, golden, legs
from run004_round03_symmetric_links import contacts, five_link, load, same_B, strict_minima
from run005_round04_branch_equalization import num, table
from run007_round06_chirality_escape import fast_grid

RID = "round-v0.7"
LOG = TREE / "data" / "gate-logs" / "run008-round07-curvature-wings.json"
MS = ("2", "3", "4", "6", "8")
COUNT_GRID = 32768


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

def build_world():
    f, f5 = load(RID), load("round-v0.5")
    md = lambda n: restore_escapes(f[n].decode("utf-8"))
    xy = lambda n: np.array([[float(r["x"]), float(r["y"])] for r in csv.DictReader(io.StringIO(f[n].decode("utf-8")))])
    return {
        "readme": md("README.md"), "report": md("reports/ROUND_07_REPORT.md"),
        "interpretation": md("reports/ROUND_07_INTERPRETATION.md"), "search_audit": md("reports/ROUND_07_SEARCH_AUDIT.md"),
        "manifest": json.loads(f["MANIFEST.json"]),
        "summary": json.loads(f["data/round07_summary.json"]),
        "segmented": json.loads(f["data/segmented_results.json"]),
        "arc_pre": json.loads(f["data/circular_result.json"]),
        "arc_eq": json.loads(f["data/circular_equalized_result.json"]),
        "curves": {m: xy(f"data/segmented_m{m}.csv") for m in MS},
        "control_curve": xy("data/event_five_link_control.csv"),
        "arc_sample": xy("data/continuous_circular_wing_sample.csv"),
        "round05": json.loads(f5["data/round05_summary.json"]),
        "legs": ((3 + 4 * math.sqrt(3)) / 18, (4 + math.sqrt(3)) / 6),
        "cert_scale_divisor": 1.0048,
        "rot_sign": 1,
        "formula_B": same_B,
    }


# ---------------------------------------------------------------- the curves

def segmented(m, P):
    """the m-piece wing: lengths in geometric progression, ratio exp(skew/(m-1)), summing to the wing length;
    directions beta + delta (1 - k/(m-1))^power from the endpoint inward; centre horizontal; mirrored"""
    m = int(m)
    k = np.arange(m)
    t = k / (m - 1)
    L = np.exp(P["length_skew"] * t)
    L = P["wing_length"] * L / L.sum()
    b, d = math.radians(P["beta_degrees"]), math.radians(P["delta_degrees"])
    th = b + d * (1 - t) ** P["curvature_power"]
    dirs = list(th) + [0.0] + list(-th[::-1])
    lens = list(L) + [P["center_length"]] + list(L[::-1])
    pts = [np.zeros(2)]
    for a, l in zip(dirs, lens):
        pts.append(pts[-1] + l * np.array([math.cos(a), math.sin(a)]))
    return np.array(pts)


def arc_geometry(P):
    """(w, beta, alpha, kappa) of the arc wing: tangent alpha at the endpoint turning linearly to beta"""
    wl = P["wing_length"]
    b, a = math.radians(P["beta_degrees"]), math.radians(P["alpha_degrees"])
    return wl, b, a, (b - a) / wl


def arc_point(P, th):
    wl, b, a, k = arc_geometry(P)
    return np.array([(math.sin(th) - math.sin(a)) / k, -(math.cos(th) - math.cos(a)) / k])


def arc_junctions(P):
    wl, b, a, k = arc_geometry(P)
    P1 = arc_point(P, b)
    P2 = P1 + np.array([1 - 2 * wl, 0.0])
    return np.array([[0.0, 0.0], P1, P2, [P2[0] + P1[0], 0.0]]), (P1[0] + P2[0]) / 2


def arc_scale(w, P, phi):
    """the exact scale of the arc wing at phase phi: each of the three linear functionals takes its extreme at a
    junction or at an arc point whose tangent is perpendicular to it"""
    A, B = legs(w)
    wl, b, a, k = arc_geometry(P)
    J, mid = arc_junctions(P)
    ph = w["rot_sign"] * phi
    c, s = math.cos(ph), math.sin(ph)
    ux, uy = np.array([c, -s]), np.array([s, c])          # x' = ux . q, y' = uy . q
    pts = [J]
    for u in (ux, uy, ux / A + uy / B):
        th0 = math.atan2(-u[0], u[1])
        for th in (th0, th0 + math.pi, th0 - math.pi):
            if b <= th <= a:
                pts.append(arc_point(P, th)[None, :])
            if b <= -th <= a:
                q = arc_point(P, -th)
                pts.append(np.array([[2 * mid - q[0], q[1]]]))
    Q = np.concatenate(pts)
    x = Q[:, 0] * c - Q[:, 1] * s
    y = Q[:, 0] * s + Q[:, 1] * c
    return float((x / A + y / B).max() - x.min() / A - y.min() / w["formula_B"](B))


def arc_minima(w, P, scan=3600):
    """every local minimum over phases of the arc wing's exact scale, refined by golden section: [(phase, value)]"""
    f = lambda x: arc_scale(w, P, x)
    phis = 2 * math.pi * np.arange(scan) / scan
    vals = np.array([f(x) for x in phis])
    st = 2 * math.pi / scan
    out = []
    for i in range(scan):
        if vals[i] <= vals[i - 1] and vals[i] <= vals[(i + 1) % scan]:
            v, x = golden(f, phis[i] - st, phis[i] + st)
            out.append((float(x % (2 * math.pi)), float(v)))
    return sorted(out)


def nearest(mins, phase):
    return min(mins, key=lambda m: abs(math.remainder(m[0] - phase, 2 * math.pi)))


def minima(w, p):
    """every local minimum over phases of a polyline, from an 8,192-phase scan refined by golden section"""
    vals = fast_grid(w, p, 8192)
    st = 2 * math.pi / 8192
    out = []
    for i in range(8192):
        if vals[i] <= vals[i - 1] and vals[i] <= vals[(i + 1) % 8192]:
            v, x = golden(lambda t: exact_scale(w, p, t), 2 * math.pi * i / 8192 - st, 2 * math.pi * i / 8192 + st)
            out.append((float(x % (2 * math.pi)), float(v)))
    return sorted(out)


def reaches(w, value_at, phase, least, tol=1e-12):
    """the stated phase reaches the least scale and is a local minimum there (tied branches make any of them right)"""
    v = value_at(phase)
    return abs(v - least) <= tol and v <= min(value_at(phase - 1e-6), value_at(phase + 1e-6))


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

def c1_control(w):
    failures = []
    C = w["segmented"]["results"]["2_control_event"]
    P = C["parameters"]
    E5 = w["round05"]["event_solution"]["parameters"]
    for k, k5 in (("outer_length", "l1"), ("inner_length", "l2"), ("center_length", "l3"), ("beta_degrees", "beta_degrees"),
                  ("delta_degrees", "delta_degrees"), ("alpha_degrees", "alpha_degrees")):
        if abs(P[k] - E5[k5]) > 1e-15:
            failures.append(f"control {k} {P[k]!r} is not Round 05's {E5[k5]!r}")
    if abs(P["wing_length"] - (P["outer_length"] + P["inner_length"])) > 1e-15:
        failures.append("control wing length is not outer + inner")
    p = five_link(E5["l1"], E5["l2"], E5["l3"], E5["alpha_degrees"], E5["beta_degrees"])
    q = w["control_curve"]
    if q.shape != p.shape or np.abs((q - q[0]) - p).max() > 1e-15:
        failures.append("the control curve is not Round 05's event five-link")
    s, _ = exact_min(w, p)
    R = C["refined"]
    if abs(R["scale"] - s) > 1e-13:
        failures.append(f"control least scale {s!r}, stated {R['scale']!r}")
    if not reaches(w, lambda t: exact_scale(w, p, t), R["phase"], s, 1e-13):
        failures.append(f"control: the stated phase {R['phase']!r} does not reach the least scale")
    mins = minima(w, p)
    if len(mins) != R["local_minimum_count"] or len(mins) != len(R["local_minima"]):
        failures.append(f"control: {len(mins)} local minima, stated {R['local_minimum_count']}")
    for m in R["local_minima"]:
        ph, v = nearest(mins, m["phase"])
        if abs(v - m["scale"]) > 1e-12 or abs(math.remainder(ph - m["phase"], 2 * math.pi)) > 1e-8:
            failures.append(f"control minimum at {m['phase']:.10f}: this run ({ph:.10f}, {v!r}), stated {m['scale']!r}")
    return failures


def c2_segments(w):
    failures = []
    for m in MS:
        R = w["segmented"]["results"][m]
        P = R["parameters"]
        p = segmented(m, P)
        q = w["curves"][m]
        if q.shape != p.shape or np.abs((q - q[0]) - p).max() > 1e-14:
            failures.append(f"m = {m}: the logged curve is not its parameters")
        if R["pieces_per_wing"] != int(m) or R["total_segments"] != 2 * int(m) + 1:
            failures.append(f"m = {m}: pieces {R['pieces_per_wing']}, segments {R['total_segments']}")
        if abs(P["center_length"] - (1 - 2 * P["wing_length"])) > 1e-15 or \
                abs(P["alpha_degrees"] - P["beta_degrees"] - P["delta_degrees"]) > 1e-12:
            failures.append(f"m = {m}: centre length or alpha is not the family's")
        total = np.linalg.norm(np.diff(q, axis=0), axis=1).sum()
        if abs(total - 1) > 1e-12:
            failures.append(f"m = {m}: length {total!r}")
    return failures


def c3_values(w):
    failures = []
    for m in MS:
        R = w["segmented"]["results"][m]["refined"]
        p = w["curves"][m]
        s, at = exact_min(w, p)
        if abs(s - R["scale"]) > 1e-12:
            failures.append(f"m = {m}: least scale {s!r}, stated {R['scale']!r}")
        if not reaches(w, lambda t: exact_scale(w, p, t), R["phase"], s):
            failures.append(f"m = {m}: the stated phase {R['phase']!r} does not reach the least scale (this run's {at!r})")
        n = strict_minima(fast_grid(w, p, COUNT_GRID)).size
        if n != R["local_minimum_count"]:
            failures.append(f"m = {m}: {n} local minima on {COUNT_GRID} phases, stated {R['local_minimum_count']}")
    if w["summary"]["segmented_results"] != w["segmented"]["results"]:
        failures.append("the summary's segmented results are not segmented_results.json")
    return failures


def c4_best(w):
    failures = []
    B6 = w["summary"]["best_heuristic_segmented"]
    if B6["key"] != "6" or B6["result"] != w["segmented"]["results"]["6"]:
        failures.append("the best heuristic is not the m = 6 result")
    p = w["curves"]["6"]
    left, bottom, hyp = contacts(w, p, B6["result"]["refined"]["phase"])
    if {"left": left, "bottom": bottom, "hypotenuse": hyp} != B6["signature"]:
        failures.append(f"m = 6 signature {left, bottom, hyp}, recorded {B6['signature']}")
    s, _ = exact_min(w, p)
    for row in B6["resolution_audit"]:
        n = strict_minima(fast_grid(w, p, row["phase_grid"])).size
        if n != row["local_minimum_count"]:
            failures.append(f"m = 6 on {row['phase_grid']} phases: {n} local minima, stated {row['local_minimum_count']}")
        if abs(row["scale"] - s) > 1e-12 or abs(row["phase"] - B6["result"]["refined"]["phase"]) > 1e-8:
            failures.append(f"m = 6 on {row['phase_grid']} phases: ({row['scale']!r}, {row['phase']!r})")
    return failures


def c5_arcs(w):
    failures = []
    for tag, R, block in (("before", w["arc_pre"], "analytic_refined"), ("after", w["arc_eq"], "full_analytic_audit")):
        P = R["parameters"]
        wl, b, a, k = arc_geometry(P)
        if abs(P["constant_curvature"] - k) > 1e-15 or abs(P["center_length"] - (1 - 2 * wl)) > 1e-15 or \
                abs(P["alpha_degrees"] - P["beta_degrees"] - P["delta_degrees"]) > 1e-12:
            failures.append(f"arc {tag}: curvature, centre or alpha is not the family's")
        mins = arc_minima(w, P)
        least = min(v for _, v in mins)
        S = R[block]
        if abs(least - S["scale"]) > 1e-12:
            failures.append(f"arc {tag}: least scale {least!r}, stated {S['scale']!r}")
        if not reaches(w, lambda t: arc_scale(w, P, t), S["phase"], least):
            failures.append(f"arc {tag}: the stated phase {S['phase']!r} does not reach the least scale")
        if len(mins) != S["local_minimum_count"] or len(mins) != len(S["local_minima"]):
            failures.append(f"arc {tag}: {len(mins)} local minima, stated {S['local_minimum_count']}")
        for m in S["local_minima"]:
            ph, v = nearest(mins, m["phase"])
            if abs(v - m["scale"]) > 1e-12 or abs(math.remainder(ph - m["phase"], 2 * math.pi)) > 1e-7:
                failures.append(f"arc {tag} minimum at {m['phase']:.9f}: this run ({ph:.9f}, {v!r}), stated {m['scale']!r}")
        if tag == "after":
            T = R["tracked_branches"]
            mins3 = [nearest(mins, ph)[1] for ph in (T["low_phase"], 2 * math.pi / 3, 3 * math.pi / 2)]
            if max(abs(a_ - b_) for a_, b_ in zip(mins3, T["values"])) > 1e-12:
                failures.append(f"arc after: tracked branches {T['values']}, this run {mins3}")
            if abs(T["spread"] - (max(T["values"]) - min(T["values"]))) > 1e-18:
                failures.append("arc after: the spread is not the tracked values'")
    S = w["summary"]
    if S["continuous_circular_wing"] != w["arc_pre"] or S["continuous_circular_wing_equalized"] != w["arc_eq"]:
        failures.append("the summary's arcs are not the two arc files")
    return failures


def sample_model(P, q):
    """the arc wing sampled as the logged sample is: the wing's chords at equal arc length, the centre in equal steps,
    the mirrored wing; then the whole polyline rescaled to length 1"""
    d = np.diff(q, axis=0)
    zero = np.flatnonzero(np.abs(np.arctan2(d[:, 1], d[:, 0])) < 1e-12)
    if not len(zero):
        return None, None
    nw = int(zero.min())
    wl, b, a, k = arc_geometry(P)
    th = a + k * np.linspace(0, wl, nw + 1)
    wing = np.stack([(np.sin(th) - math.sin(a)) / k, -(np.cos(th) - math.cos(a)) / k], 1)
    J, mid = arc_junctions(P)
    full = np.concatenate([wing, np.stack([np.linspace(J[1][0], J[2][0], len(zero) + 1)[1:-1], np.full(len(zero) - 1, J[1][1])], 1),
                           np.stack([2 * mid - wing[::-1, 0], wing[::-1, 1]], 1)])
    raw = full.copy()
    full = full / np.linalg.norm(np.diff(full, axis=0), axis=1).sum()
    return full, raw


def c6_sample(w):
    P = w["arc_pre"]["parameters"]
    q = w["arc_sample"]
    full, _ = sample_model(P, q)
    if full is None:
        return ["the sample has no centre segment"]
    if full.shape != q.shape:
        return [f"the sample has {len(q)} points; the arc wing sampled the same way has {len(full)}"]
    dev = np.abs((q - q[0]) - (full - full[0])).max()
    return [] if dev <= 1e-14 else [f"the sample differs from the rescaled chord polyline of the arc before equalization by {dev:.2e}"]


def c7_derived(w):
    failures = []
    ref = w["summary"]["reference_event_scale"]
    if ref != w["round05"]["event_solution"]["common_scale"]:
        failures.append("the reference is not Round 05's event scale")
    res = w["segmented"]["results"]
    for key, R in res.items():
        for label, got, want in (("gain", R["gain_over_reference"], R["refined"]["scale"] - ref),
                                 ("distance", R["distance_to_one"], 1 - R["refined"]["scale"])):
            if abs(got - want) > 1e-18:
                failures.append(f"m = {key} {label}: stated {got!r}, the difference is {want!r}")
    pre, eq = w["arc_pre"], w["arc_eq"]
    for label, got, want in (("arc before gain", pre["gain_over_reference"], pre["analytic_refined"]["scale"] - ref),
                             ("arc before distance", pre["distance_to_one"], 1 - pre["analytic_refined"]["scale"]),
                             ("arc after gain", eq["gain_over_event"], eq["full_analytic_audit"]["scale"] - ref),
                             ("arc after distance", eq["distance_to_one"], 1 - eq["full_analytic_audit"]["scale"]),
                             ("arc gain over previous", eq["gain_over_previous_arc"], eq["full_analytic_audit"]["scale"] - pre["analytic_refined"]["scale"])):
        if abs(got - want) > 1e-18:
            failures.append(f"{label}: stated {got!r}, the difference is {want!r}")
    rank = w["summary"]["ranking"]
    vals = {"event_control": res["2_control_event"]["refined"]["scale"],
            "continuous_circular_wing_equalized": eq["full_analytic_audit"]["scale"]}
    vals.update({f"segmented_m{m}": res[m]["refined"]["scale"] for m in MS})
    if [r[0] for r in rank] != sorted(vals, key=lambda k: -vals[k]) or any(abs(r[1] - vals[r[0]]) > 1e-15 for r in rank):
        failures.append(f"the ranking {rank} is not the values in order")
    M = w["manifest"]
    if M["reference_scale"] != ref or M["continuous_arc_scale"] != eq["full_analytic_audit"]["scale"]:
        failures.append("the manifest's figures are not the data's")
    return failures


def c8_consistency(w):
    failures = []
    res = w["segmented"]["results"]
    eq, ref = w["arc_eq"], w["summary"]["reference_event_scale"]
    s_arc, s6 = eq["full_analytic_audit"]["scale"], res["6"]["refined"]["scale"]
    rd, rp = gate.squash(w["readme"]), gate.squash(w["report"])
    it, au = gate.squash(w["interpretation"]), gate.squash(w["search_audit"])

    def same(label, got, want, tol):
        if got is None or abs(got - want) > tol:
            failures.append(f"{label}: stated {got}, the data {want!r}")
    same("README event", num(r"離散事件五連桿：(\d+\.\d+)", rd), res["2_control_event"]["refined"]["scale"], 6e-16)
    same("README arc", num(r"解析等高常曲率翼：(\d+\.\d+)", rd), s_arc, 6e-16)
    same("README m6", num(r"最佳分段啟發式候選（m=6）：(\d+\.\d+)", rd), s6, 6e-16)
    same("README gap", num(r"\$\$(\d+\.\d+e-?\d+)\.\$\$", rd), ref - s_arc, 6e-18)
    same("interpretation event", num(r"離散事件五連桿：(\d+\.\d+)", it), res["2_control_event"]["refined"]["scale"], 6e-16)
    same("interpretation arc", num(r"解析等高常曲率翼：(\d+\.\d+)", it), s_arc, 6e-16)
    same("interpretation gap", num(r"差距：\$\$(\d+\.\d+e-?\d+)", it), ref - s_arc, 6e-18)
    same("audit control", num(r"事件五連桿：\$\$s=(\d+\.\d+)", au), ref, 6e-16)
    same("audit m2", num(r"結果\$\$s=(\d+\.\d+)", au), res["2"]["refined"]["scale"], 6e-16)
    same("audit m2 gap", num(r"差距：\$\$(-\d+\.\d+e-?\d+)", au), res["2"]["gain_over_reference"], 6e-17)
    for row in table(w["report"], "每側段數"):
        m = row[0]
        R = res[m]
        same(f"report m={m} scale", float(row[2]), R["refined"]["scale"], 6e-16)
        same(f"report m={m} gain", float(row[3]), R["gain_over_reference"], 6e-4 * abs(R["gain_over_reference"]))
        same(f"report m={m} distance", float(row[4]), R["distance_to_one"], 6e-4 * R["distance_to_one"])
        same(f"report m={m} alpha", float(row[5]), R["parameters"]["alpha_degrees"], 6e-7)
        same(f"report m={m} beta", float(row[6]), R["parameters"]["beta_degrees"], 6e-7)
        if int(row[1]) != R["total_segments"]:
            failures.append(f"report m={m}: segments")
    same("report s_seg", num(r"s_\{\\mathrm\{seg\}\}=(\d+\.\d+)", rp), s6, 6e-16)
    same("report s_seg gap", num(r"s_\{\\mathrm\{seg\}\}-s_\{\\mathrm\{event\}\}=(-\d+\.\d+e-?\d+)", rp), res["6"]["gain_over_reference"], 6e-18)
    m = re.search(r"```json(.*?)```", w["report"], re.S)
    if not m or json.loads(m.group(1)) != w["summary"]["best_heuristic_segmented"]["signature"]:
        failures.append("the report's signature block is not the data's")
    P = eq["parameters"]
    same("report w", num(r"w=(\d+\.\d+),", rp), P["wing_length"], 6e-13)
    same("report l0", num(r"l_0=(\d+\.\d+),", rp), P["center_length"], 6e-13)
    same("report beta", num(r"\\beta=(\d+\.\d+)\^\\circ,\$\$\$\$\\alpha", rp), P["beta_degrees"], 6e-10)
    same("report alpha", num(r"\\alpha=(\d+\.\d+)\^\\circ,\$\$\$\$\\alpha-\\beta", rp), P["alpha_degrees"], 6e-10)
    same("report delta", num(r"\\alpha-\\beta=(\d+\.\d+)\^\\circ", rp), P["delta_degrees"], 6e-10)
    same("report kappa", num(r"\\kappa=(-\d+\.\d+)", rp), P["constant_curvature"], 6e-13)
    for i, v in enumerate(eq["tracked_branches"]["values"]):
        same(f"report m_{i + 1}", num(rf"m_{i + 1}=(\d+\.\d+)", rp), v, 6e-16)
    same("report spread", num(r"分支差距：\$\$(\d+\.\d+e-?\d+)", rp), eq["tracked_branches"]["spread"], 6e-28)
    same("report s_arc", num(r"s_\{\\mathrm\{arc\}\}=(\d+\.\d+)", rp), s_arc, 6e-16)
    same("report arc gap", num(r"s_\{\\mathrm\{arc\}\}-s_\{\\mathrm\{event\}\}=(-\d+\.\d+e-?\d+)", rp), eq["gain_over_event"], 6e-18)
    same("report arc distance", num(r"1-s_\{\\mathrm\{arc\}\}=(\d+\.\d+e-?\d+)", rp), eq["distance_to_one"], 6e-16)
    names = {"event_control": res["2_control_event"]["refined"]["scale"], "continuous_circular_wing_equalized": s_arc}
    names.update({f"segmented_m{k}": res[k]["refined"]["scale"] for k in MS})
    for row in table(w["report"], "| 排名 | 模型 |"):
        same(f"report ranking {row[1]}", float(row[2]), names[row[1]], 6e-16)
    for row, a in zip(table(w["report"], "初始相位網格"), w["summary"]["best_heuristic_segmented"]["resolution_audit"]):
        if int(row[0]) != a["phase_grid"] or abs(float(row[1]) - a["scale"]) > 6e-16 or abs(float(row[2]) - a["phase"]) > 6e-13 \
                or int(row[3]) != a["local_minimum_count"]:
            failures.append(f"report resolution row {row[0]} is not the data's")
    return failures


CHECKS = {"C1": c1_control, "C2": c2_segments, "C3": c3_values, "C4": c4_best, "C5": c5_arcs, "C6": c6_sample,
          "C7": c7_derived, "C8": c8_consistency}
GEOM = {"legs", "cert_scale_divisor", "rot_sign", "formula_B"}
DEPENDS = {
    "C1": {"segmented", "round05", "control_curve"} | GEOM,
    "C2": {"segmented", "curves"},
    "C3": {"segmented", "curves", "summary"} | GEOM,
    "C4": {"summary", "segmented", "curves"} | GEOM,
    "C5": {"arc_pre", "arc_eq", "summary"} | GEOM,
    "C6": {"arc_pre", "arc_sample"},
    "C7": {"summary", "segmented", "arc_pre", "arc_eq", "round05", "manifest"},
    "C8": {"readme", "report", "interpretation", "search_audit", "segmented", "arc_eq", "summary"},
}


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

def findings(w):
    res = w["segmented"]["results"]
    ref = w["summary"]["reference_event_scale"]
    E5 = w["round05"]["event_solution"]["parameters"]
    l1, l2 = E5["l1"], E5["l2"]
    # every segmented family approaches the event five-link as the curvature power grows: the first piece keeps
    # direction alpha and the rest tend to beta; the skew is chosen so the first piece has length l1
    limit = {}
    for m in ("3", "4", "6", "8"):
        k = int(m)
        # first piece / wing = 1 / sum r^j with r = exp(skew/(k-1)): solve for r
        target = (l1 + l2) / l1
        r = 0.5
        for _ in range(200):
            g = sum(r ** j for j in range(k)) - target
            dg = sum(j * r ** (j - 1) for j in range(1, k))
            r -= g / dg
        skew = (k - 1) * math.log(r)
        vals = {}
        for power in (10.0, 40.0, 160.0):
            P = {"wing_length": l1 + l2, "center_length": E5["l3"], "beta_degrees": E5["beta_degrees"],
                 "delta_degrees": E5["delta_degrees"], "length_skew": skew, "curvature_power": power}
            vals[str(power)] = exact_min(w, segmented(m, P))[0] - ref
        limit[m] = {"skew": skew, "least_minus_event": vals, "heuristic_minus_event": res[m]["gain_over_reference"]}
    # the m = 6 candidate's least phase: which link is parallel to the leg x = 0
    P6 = res["6"]["parameters"]
    t = np.arange(6) / 5
    th = np.degrees(math.radians(P6["beta_degrees"]) + math.radians(P6["delta_degrees"]) * (1 - t) ** P6["curvature_power"])
    phi6 = math.degrees(res["6"]["refined"]["phase"])
    # the arc family: local search from the equalized arc, and from several starts
    eq = w["arc_eq"]["parameters"]
    def arc_least(x, scan=360):
        P = {"wing_length": x[0], "beta_degrees": math.degrees(x[1]), "alpha_degrees": math.degrees(x[1] + x[2])}
        if not 0.2 < x[0] < 0.45 or x[2] <= 1e-4:
            return 0.0
        return min(v for _, v in arc_minima(w, P, scan))
    x0 = np.array([eq["wing_length"], math.radians(eq["beta_degrees"]), math.radians(eq["delta_degrees"])])
    r0 = minimize(lambda x: -arc_least(x), x0, method="Nelder-Mead", options={"xatol": 1e-11, "fatol": 1e-14, "maxiter": 1500})
    starts = [x0 + np.array([dw, db, dd]) for dw in (-0.01, 0.01) for db in (-0.02, 0.02) for dd in (-0.02, 0.02)]
    multi = []
    for s0 in starts:
        r = minimize(lambda x: -arc_least(x), s0, method="Nelder-Mead", options={"xatol": 1e-9, "fatol": 1e-13, "maxiter": 800})
        multi.append(float(-r.fun))
    pre = w["arc_pre"]["parameters"]
    dev = {}
    E5p = w["round05"]["event_solution"]["parameters"]
    pc = five_link(E5p["l1"], E5p["l2"], E5p["l3"], E5p["alpha_degrees"], E5p["beta_degrees"])
    C = res["2_control_event"]["refined"]
    cm = minima(w, pc)
    dev["control_least"] = exact_min(w, pc)[0] - C["scale"]
    dev["control_minima_value"] = max(abs(nearest(cm, m["phase"])[1] - m["scale"]) for m in C["local_minima"])
    dev["segment_curves_vs_parameters"] = max(float(np.abs((w["curves"][m] - w["curves"][m][0]) - segmented(m, res[m]["parameters"])).max()) for m in MS)
    dev["segment_least"] = {m: exact_min(w, w["curves"][m])[0] - res[m]["refined"]["scale"] for m in MS}
    s6 = exact_min(w, w["curves"]["6"])[0]
    dev["m6_resolution_rows"] = max(abs(r["scale"] - s6) for r in w["summary"]["best_heuristic_segmented"]["resolution_audit"])
    for tag, R, block in (("arc_before", w["arc_pre"], "analytic_refined"), ("arc_after", w["arc_eq"], "full_analytic_audit")):
        mins = arc_minima(w, R["parameters"])
        dev[tag + "_least"] = min(v for _, v in mins) - R[block]["scale"]
        dev[tag + "_minima_value"] = max(abs(nearest(mins, m["phase"])[1] - m["scale"]) for m in R[block]["local_minima"])
    q = w["arc_sample"]
    d = np.diff(q, axis=0)
    zero = np.flatnonzero(np.abs(np.arctan2(d[:, 1], d[:, 0])) < 1e-12)
    dev["arc_sample_wing_and_centre_segments"] = [int(zero.min()), int(len(zero))]
    full, raw = sample_model(w["arc_pre"]["parameters"], q)
    dev["arc_sample_minus_chords_unscaled"] = float(np.abs((q - q[0]) - (raw - raw[0])).max())
    dev["arc_sample_chord_length_before_rescaling"] = float(np.linalg.norm(np.diff(raw, axis=0), axis=1).sum())
    dev["arc_sample_minus_chords_rescaled"] = float(np.abs((q - q[0]) - (full - full[0])).max())
    full_eq, _ = sample_model(w["arc_eq"]["parameters"], q)
    dev["arc_sample_minus_equalized_arc_rescaled"] = float(np.abs((q - q[0]) - (full_eq - full_eq[0])).max())
    p6 = w["curves"]["6"]
    dev["m6_minima_with_signatures"] = [[ph, v, list(contacts(w, p6, ph))] for ph, v in minima(w, p6)]
    return {
        "reproduction_deviations": dev,
        "segmented_limit_power_to_infinity": limit,
        "m6_least_phase_link_angles_plus_phase_degrees": [float(a + phi6) for a in th],
        "arc_local_search_from_equalized": {"gain": float(-r0.fun - w["arc_eq"]["full_analytic_audit"]["scale"]),
                                            "moved": float(np.abs(r0.x - x0).max())},
        "arc_multistart_best_minus_equalized": max(multi) - w["arc_eq"]["full_analytic_audit"]["scale"],
        "arc_multistart_values": multi,
        "arc_sample_is_before_equalization": not c6_sample(w),
        "arc_before_vs_after_parameters": {k: eq[k] - pre[k] for k in ("wing_length", "beta_degrees", "delta_degrees")},
    }


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

def drill_cases(base):
    def edit(key, fn):
        v = copy.deepcopy(base[key])
        fn(v)
        return v
    R = lambda s, m: s["results"][m]
    seg_ctl = edit("segmented", lambda s: R(s, "2_control_event")["refined"]["local_minima"][4].__setitem__("scale", 0.9989037572325222))
    seg_par = edit("segmented", lambda s: R(s, "4")["parameters"].__setitem__("curvature_power", R(s, "4")["parameters"]["curvature_power"] + 1e-6))
    seg_val = edit("segmented", lambda s: R(s, "8")["refined"].__setitem__("scale", R(s, "8")["refined"]["scale"] + 1e-10))
    seg_cnt = edit("segmented", lambda s: R(s, "3")["refined"].__setitem__("local_minimum_count", 8))
    seg_gain = edit("segmented", lambda s: R(s, "6").__setitem__("gain_over_reference", -6.4579486223e-05))
    summ_sig = edit("summary", lambda s: s["best_heuristic_segmented"]["signature"].__setitem__("left", [4]))
    summ_res = edit("summary", lambda s: s["best_heuristic_segmented"]["resolution_audit"][2].__setitem__("local_minimum_count", 8))
    pre = edit("arc_pre", lambda a: a["parameters"].__setitem__("constant_curvature", a["parameters"]["constant_curvature"] + 1e-9))
    eq = edit("arc_eq", lambda a: a["full_analytic_audit"]["local_minima"][1].__setitem__("scale", 1.02878305437425))
    eq2 = edit("arc_eq", lambda a: a["tracked_branches"]["values"].__setitem__(1, a["tracked_branches"]["values"][1] - 1e-9))
    man = edit("manifest", lambda m: m.__setitem__("continuous_arc_scale", 0.998862397495094))
    cv = copy.deepcopy(base["curves"])
    cv["6"][3, 0] += 1e-9
    ctl = base["control_curve"].copy()
    ctl[2, 0] += 1e-10
    smp = base["arc_sample"].copy()
    smp[50, 1] += 1e-8
    rep = base["report"].replace("\\kappa=-0.225356173125", "\\kappa=-0.225356173135")
    readme = base["readme"].replace("解析等高常曲率翼：0.998862397495093", "解析等高常曲率翼：0.998862397495083")
    interp = base["interpretation"].replace("4.135963741603e-05", "4.135963741613e-05")
    audit = base["search_audit"].replace("-3.730202267003e-04", "-3.730202267013e-04")
    for a, b in ((rep, base["report"]), (readme, base["readme"]), (interp, base["interpretation"]), (audit, base["search_audit"])):
        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)}, {"C1", "C3", "C4", "C5"}),
        ("M2-control-minimum", "mutation", {"segmented": seg_ctl}, {"C1", "C3"}),
        ("M3-segment-power", "mutation", {"segmented": seg_par}, {"C2", "C3"}),
        ("M4-segment-value", "mutation", {"segmented": seg_val}, {"C3", "C7", "C8"}),
        ("M5-segment-count", "mutation", {"segmented": seg_cnt}, {"C3"}),
        ("M6-segment-gain", "mutation", {"segmented": seg_gain}, {"C3", "C4", "C7", "C8"}),
        ("M7-best-signature", "mutation", {"summary": summ_sig}, {"C4", "C8"}),
        ("M8-resolution-count", "mutation", {"summary": summ_res}, {"C4", "C8"}),
        ("M9-arc-curvature", "mutation", {"arc_pre": pre}, {"C5"}),
        ("M10-arc-minimum", "mutation", {"arc_eq": eq}, {"C5"}),
        ("M11-arc-tracked", "mutation", {"arc_eq": eq2}, {"C5", "C8"}),
        ("M12-manifest", "mutation", {"manifest": man}, {"C7"}),
        ("M13-curve-point", "mutation", {"curves": cv}, {"C2", "C3", "C4"}),
        ("M14-arc-sample", "mutation", {"arc_sample": smp}, {"C6"}),
        ("M15-report-digit", "mutation", {"report": rep}, {"C8"}),
        ("M16-readme-digit", "mutation", {"readme": readme}, {"C8"}),
        ("M17-interpretation-digit", "mutation", {"interpretation": interp}, {"C8"}),
        ("M18-audit-digit", "mutation", {"search_audit": audit}, {"C8"}),
        ("M19-formula-term", "mutation", {"formula_B": lambda B: 2 * B}, {"C1", "C3", "C4", "C5"}),
        ("M20-control-curve", "mutation", {"control_curve": ctl}, {"C1"}),
    ]


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
    w = build_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-008",
           "subject": "Round 07 (v0.7): segmented curvature wings and the constant-curvature arc wing against the event five-link",
           "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)[:6000])
    if args.drill:
        drill = gate.run_drill(build_world, CHECKS, DEPENDS, drill_cases(build_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()
