#!/usr/bin/env python3
r"""threshold_hankel_tail.py -- ANALYTIC threshold connection coefficients c_alpha of the
banana coalescence ladder from the non-oscillatory tail of the Bessel-representation kernel.

STAMP (date -u at emission): 2026-09-10T13:51:54Z
extends: threshold-banana-evaluate.py (rowscripts; the (1,1,1,9) K3 row-31 code set -- this file
         lives beside it and never overwrites it) and tools/coalescer/calpha_rings.py (the recorded
         closed structures and the ring grammar: the derived structure is emitted IN that grammar and
         compared to the recorded one; the record's PSLQ relation vectors are REPRODUCED here, never
         searched -- PSLQ is demoted to the check).
reference: an independent reference derivation of 2026-09-10, on file with the authors (the rotation
         to the Hankel kernel and the Mellin formula) -- the derivation below is the general-L form of
         that argument, worked exactly over Q and reduced to the calpha_rings grammar.

THE DERIVATION (every step exact over Q; only the final Gamma / pi / radical factors are named)
  L := number of propagators = len(msq); m_i = sqrt(M_i) integers (i = 1..L) with the coalescence
  m_L = sum_{i<L} m_i (the largest mass = the sum of the others; the pseudo-threshold sits at s = 0).
  REFUSED BY DESIGN: a tuple in which no sqrt(M_i) equals the sum of the other roots -- e.g. (1,4,4,9), whose
  roots 1 + 3 = 2 + 2 form a 2-2 split -- has no single coalescing line: such a --masses tuple is refused by
  name before any number is printed (`REFUSED (exit 2): --masses 1,4,4,9: masses [1, 2, 2, 3]: no mass
  equals the sum of the others ...`, the usage exit code).  The phase
  cancellation below (between (2) and T(y)) needs ONE line carrying the sum of the rest; 2-2 splits are
  outside this tool.
  For sqrt(s) > sum m_i the MUM-normalised holomorphic period is the Bessel moment
      varpi_0(s) = s * int_0^inf y K_0(sqrt(s) y) prod_i I_0(m_i y) dy                (1)
  (term by term: int y^{2n+1} K_0(sqrt(s) y) dy = 4^n n!^2 s^{-n-1} reproduces the
  multinomial-squared c_n; the s in front makes varpi_0 -> 1).  Continue s into Im s > 0 and rotate
  y -> -i y (the direction in which the integrand stays damped): K_0(-i u) = (i pi/2) H_0^(1)(u),
  I_0(-i u) = J_0(u), y dy -> -y dy:
      varpi_0(s) = -(i pi/2) s int_0^inf y H_0^(1)(sqrt(s) y) prod_i J_0(m_i y) dy.       (2)
  Fractional powers of s at s -> 0 come ONLY from the non-oscillatory power tail of the kernel
  prod J_0(m_i y) at large y.  With J_0(u) = (h(u) + conj h(u))/2, h(u) = sqrt(2/(pi u))
  e^{i(u - pi/4)} A(u), A(u) = sum_k i^k a_k u^{-k}, a_k = prod_{j=1}^{k} (-(2j-1)^2) / (k! 8^k)
  (the Hankel asymptotic series of order zero), the only combination without an oscillating
  factor is h(m_L y) prod_{i<L} conj h(m_i y) and its conjugate (the coalescence m_L = sum m_i is
  exactly the cancellation of the phases):
      prod_i J_0(m_i y)  >  T(y) = 2^{1-L} (2/(pi y))^{L/2} (prod m_i)^{-1/2}
                                   * Re[ e^{i pi (L-2)/4} A(m_L y) prod_{i<L} conj A(m_i y) ]
                              = sum_n tau_n y^{-L/2-n},
      tau_n = 2^{1-L} (2/pi)^{L/2} (prod m_i)^{-1/2} r_n cos(pi (L-2)/4 + n pi/2),
  where r_n in Q is the y^{-n} coefficient of the REAL series A(m_L y) prod conj A(m_i y) written
  in powers of (i/y): r_n = [w^n] sum_k a_k m_L^{-k} w^k * prod_{i<L} sum_k (-1)^k a_k m_i^{-k} w^k.
  The Mellin formula int_0^inf y^{lambda-1} H_0^(1)(mu y) dy = (2/(i pi)) 2^{lambda-2}
  (-i mu)^{-lambda} Gamma(lambda/2)^2 (Im mu > 0; continued in lambda) at lambda_n = 2 - L/2 - n,
  mu = sqrt(s), with s = e^{i pi} t (t > 0 reached from Im s > 0: -i sqrt(s) = t^{1/2}, -s = t)
  gives the t^{alpha_n} term of (2), alpha_n = L/4 + n/2:
      c_{alpha_n} = tau_n 2^{lambda_n - 2} Gamma(lambda_n/2)^2
                  = 2^{1-L-n} pi^{-L/2} (prod m_i)^{-1/2} r_n cos_n Gamma(1 - alpha_n)^2.        (3)
  Integer alpha_n (Gamma(1-alpha_n) at a pole) belong to the unipotent / logarithmic sector and are
  not connection coefficients of a fractional branch; they are named and skipped.
  The branch convention is the record's: Phi_alpha = t^alpha (1 + O(t)), t = e^{-i pi} s, the
  LOWER branch arg t = -pi.  In that convention every c_alpha of the ladder is REAL.

WHAT --check DOES (per family K3 (1,1,1,9), CY3 (1,1,1,1,16), CY4 (1,1,1,1,1,25), or --masses):
  1. the exact objects: a_k, r_n, cos_n, lambda_n, alpha_n (printed as rationals);
  2. the closed STRUCTURE of (3) reduced to the calpha_rings grammar (sign, rational, pi power,
     radicals, Gamma exponents) and its display; compared EXACTLY with the recorded structure of
     calpha_rings._FAMILIES (verdict STRUCTURE EQUAL / DIFFERENT by name);
  3. the value at --dps (>= 50) two ways (the structure through calpha_rings.ClosedForm.value() and
     the direct mpmath evaluation of (3) with mp.gamma at lambda/2), their agreement, and the
     agreement with the RECORD STRING read by object from the fixtures file (each string carries
     its source path + sha256; the comparison is capped at the string's certified length);
  5. (after 4) the internal identity c_{alpha+1}/c_alpha = phi_{alpha,1}: the tail's next order reproduces the
     record operator's exact-Q Frobenius branch coefficient (fixtures: branch_phi1 by object) -- EXACT or FAIL.
  4. the PSLQ pin demoted to a check: the record's integer relation vector on the record's basket is
     derived from the analytic structure (ClosedForm.log_vector) and must EQUAL the pinned vector,
     and the relation is verified numerically with the analytic value at --dps.
  Any miss is a named FAIL and exit 1; a fixture / pin mismatch is REFUSED (exit 3) before any
  number is read; a missing calpha_rings.py is exit 4.
--selftest runs --check on the three families and then three PLANTED controls that must FAIL by
  name: (a) the Hankel coefficient a_1 = -1/8 tampered to -1/7 (hits the n = 1 coefficients K3
  c_{3/2} and CY3 c_{7/4}; the n = 0 ones must still pass), (b) the rotation phase e^{i pi(L-2)/4}
  tampered to e^{i pi(L+2)/4} (the conjugate pair's sign; hits every coefficient), (c) one digit of a record string flipped
  (hits that string's comparison only).  Exit 0 iff the clean check passes AND every planted control
  fails exactly where planted.

USAGE
  python3 threshold_hankel_tail.py --check [--family K3|CY3|CY4|ALL] [--dps 60]
  python3 threshold_hankel_tail.py --check --masses 1,1,1,9 [--nmax 3]      (any coalescent tuple)
  python3 threshold_hankel_tail.py --selftest [--dps 60]
  python3 threshold_hankel_tail.py --derive --family CY3        (the derivation objects only, no records)
  env / flags: --calpha-rings PATH (default: calpha_rings.py BESIDE this file when present -- the served
  bundle's vendored copy, pinned by sha256 like every other input --, else CALPHA_RINGS, else
  tools/coalescer/calpha_rings.py under BOOTSTRAP_ROOT or the tree above this file), --fixtures PATH (default:
  fixtures/hankel_tail_fixtures.json beside this file), --allow-unpinned (print a loud line instead of refusing
  on a calpha_rings sha mismatch).
EXIT CODES: 0 pass; 1 a named FAIL; 2 usage, or a --masses tuple refused by design; 3 a pin / fixture
  mismatch (refused by name);
  4 calpha_rings.py not found.
Self-contained otherwise: python3 + mpmath (+ calpha_rings.py by path; in the served bundle the copy beside this
file and the fixtures/ directory travel with it, so no environment variable is needed).
"""
import argparse
import hashlib
import importlib.util
import json
import math
import os
import sys
from fractions import Fraction as Fr

import mpmath as mp

STAMP = "2026-09-10T13:51:54Z"
EXIT_FAIL, EXIT_USAGE, EXIT_PIN, EXIT_MISSING = 1, 2, 3, 4

# The house ring-table module this extends: the recorded closed structures, the grammar, the vectors.
def _calpha_rings_default():
    """calpha_rings.py beside this file first (the served bundle's vendored copy; pinned by CALPHA_RINGS_SHA256 like
    any other path); else CALPHA_RINGS env, else tools/coalescer/calpha_rings.py under BOOTSTRAP_ROOT or the tree above this file."""
    beside = os.path.join(os.path.dirname(os.path.abspath(__file__)), "calpha_rings.py")
    if os.path.exists(beside):
        return beside
    if os.environ.get("CALPHA_RINGS"):
        return os.environ["CALPHA_RINGS"]
    roots = [os.environ.get("BOOTSTRAP_ROOT", "")]
    h = os.path.dirname(os.path.abspath(__file__))
    for _ in range(10):
        roots.append(h); h = os.path.dirname(h)
    for r in roots:
        c = os.path.join(r, "tools", "coalescer", "calpha_rings.py") if r else ""
        if c and os.path.exists(c):
            return c
    return os.path.join(os.path.dirname(os.path.abspath(__file__)), "calpha_rings.py")


CALPHA_RINGS_DEFAULT = _calpha_rings_default()
CALPHA_RINGS_SHA256 = "f31b186255d2aa1f41166382ed00881d927eafde7c82ded8c9a30d14a745ce46"
FIXTURES_DEFAULT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures", "hankel_tail_fixtures.json")
FIXTURES_SHA256 = "3aca127c00cb0dd9e4dad3a441c5c801f3ce5a95ab816a4cbff6e3f2be783997"     # pinned at emission (the file is listed with this sha256 in MANIFEST.sha256 beside this module)

FAMILIES = {"K3": (1, 1, 1, 9), "CY3": (1, 1, 1, 1, 16), "CY4": (1, 1, 1, 1, 1, 25)}


# ------------------------------------------------------------------ exact objects
def hankel_a(k, tamper=None):
    """a_k(nu=0) = prod_{j=1}^{k} (4 nu^2 - (2j-1)^2) / (k! 8^k), exact."""
    if tamper and k in tamper:
        return Fr(tamper[k])
    num = Fr(1)
    for j in range(1, k + 1):
        num *= Fr(-(2 * j - 1) ** 2)
    return num / (Fr(math.factorial(k)) * Fr(8) ** k)


def isqrt_exact(n):
    r = math.isqrt(int(n))
    if r * r != n:
        raise ValueError(f"mass-squared {n} is not a perfect square")
    return r


def coalescence(msq):
    """(m_i integers, index of the coalescing mass): m_big = sum of the others (assert)."""
    m = [isqrt_exact(x) for x in msq]
    for L_idx in range(len(m)):
        if m[L_idx] == sum(m) - m[L_idx]:
            return m, L_idx
    raise ValueError(f"masses {m}: no mass equals the sum of the others (no coalescence -> no fractional threshold branch)")


def tail_series(m, L_idx, nmax, tamper=None):
    """r_n, n = 0..nmax: [w^n] A_{m_L}(w) prod_{i != L} Abar_{m_i}(w) with
    A_m(w) = sum_k a_k m^{-k} w^k, Abar_m(w) = sum_k (-1)^k a_k m^{-k} w^k (real series in w = i/y)."""
    def ser(mi, bar):
        return [(-1) ** k * hankel_a(k, tamper) / Fr(mi) ** k if bar else hankel_a(k, tamper) / Fr(mi) ** k
                for k in range(nmax + 1)]

    def mul(p, q):
        out = [Fr(0)] * (nmax + 1)
        for i, x in enumerate(p):
            if x == 0:
                continue
            for j, y in enumerate(q):
                if i + j > nmax:
                    break
                out[i + j] += x * y
        return out
    prod = ser(m[L_idx], False)
    for i, mi in enumerate(m):
        if i != L_idx:
            prod = mul(prod, ser(mi, True))
    return prod


def phase_cos(L, n, phase_tamper=0):
    """cos(pi (L-2)/4 + n pi/2) exactly as (sign, sqrt2_power): value = sign * 2^(sqrt2_power/2);
    returns (0, 0) for a vanishing cosine."""
    q = (L - 2 + phase_tamper + 2 * n) % 8
    table = {0: (1, 0), 1: (1, -1), 2: (0, 0), 3: (-1, -1), 4: (-1, 0), 5: (-1, -1), 6: (0, 0), 7: (1, -1)}
    return table[q]


def gamma_square_structure(x):
    """Gamma(x)^2 for x = j + r, r in {1/2, 1/4, 3/4} (the ladder's half/quarter integers):
    returns (rational, pi_power, radicals{p: Fraction}, gamma{Fraction: int}) or None when r is not tabulated.
    Gamma(r+j) = Gamma(r) * prod_{k=0}^{j-1} (r+k)  (j >= 0);  Gamma(r-|j|) = Gamma(r) / prod_{k=1}^{|j|} (r-k)."""
    x = Fr(x)
    r = x - math.floor(x)
    j = math.floor(x)
    if r not in (Fr(1, 2), Fr(1, 4), Fr(3, 4)):
        return None
    shift = Fr(1)
    if j >= 0:
        for k in range(j):
            shift *= (r + k)
    else:
        for k in range(1, -j + 1):
            shift /= (r - k)
    rational = shift * shift          # squared: positive
    pi_power, radicals, gamma = Fr(0), {}, {}
    if r == Fr(1, 2):                 # Gamma(1/2)^2 = pi
        pi_power = Fr(1)
    elif r == Fr(1, 4):               # Gamma(1/4)^2
        gamma[Fr(1, 4)] = 2
    else:                             # Gamma(3/4)^2 = 2 pi^2 / Gamma(1/4)^2
        pi_power = Fr(2)
        radicals[2] = Fr(1)
        gamma[Fr(1, 4)] = -2
    return rational, pi_power, radicals, gamma


def derive(msq, nmax=3, tamper=None, phase_tamper=0):
    """The derivation objects for a coalescent mass tuple.  Returns a dict with the exact objects and,
    per fractional alpha_n, the closed structure (calpha_rings grammar) + the direct formula pieces."""
    m, L_idx = coalescence(msq)
    L = len(m)
    r = tail_series(m, L_idx, nmax, tamper)
    prod_m = 1
    for mi in m:
        prod_m *= mi
    out = {"msq": list(msq), "m": m, "L": L, "coalescing_index": L_idx,
           "coalescence": f"m_{L_idx} = {m[L_idx]} = sum of the others {sum(m) - m[L_idx]}",
           "hankel_a": {k: str(hankel_a(k, tamper)) for k in range(nmax + 1)},
           "r_n": {n: str(r[n]) for n in range(nmax + 1)}, "prod_m": prod_m, "terms": []}
    for n in range(nmax + 1):
        lam = Fr(2) - Fr(L, 2) - n
        alpha = 1 - lam / 2
        sgn, s2 = phase_cos(L, n, phase_tamper)
        term = {"n": n, "lambda": str(lam), "alpha": str(alpha), "r_n": str(r[n]),
                "cos_sign": sgn, "cos_sqrt2_power": s2, "cos_value": f"{sgn} * 2^({s2}/2)" if sgn else "0"}
        if alpha.denominator == 1:
            term["skip"] = f"alpha_{n} = {alpha} is an integer: Gamma(1-alpha) at a pole -> the unipotent/log sector, not a fractional branch"
            out["terms"].append(term)
            continue
        if sgn == 0 or r[n] == 0:
            term["skip"] = f"tau_{n} = 0 (cos = 0 or r_n = 0): no t^{alpha} term from this order"
            out["terms"].append(term)
            continue
        gs = gamma_square_structure(1 - alpha)
        # c = 2^{1-L-n} pi^{-L/2} (prod m)^{-1/2} r_n cos_n Gamma(1-alpha)^2
        coef = Fr(2) ** (1 - L - n) * abs(r[n])
        sign = (1 if r[n] > 0 else -1) * sgn
        pi_power = Fr(-L, 2)
        radicals = {}
        if s2:                                    # cos carries 2^(s2/2)
            radicals[2] = radicals.get(2, Fr(0)) + Fr(s2, 2)
        # (prod m)^{-1/2}: per prime p^e -> exponent -e/2
        pm = prod_m
        p = 2
        while pm > 1:
            e = 0
            while pm % p == 0:
                pm //= p
                e += 1
            if e:
                ex = Fr(-e, 2)
                if ex.denominator == 1:
                    coef *= Fr(p) ** int(ex)
                else:
                    radicals[p] = radicals.get(p, Fr(0)) + ex
            p += 1
        gamma = {}
        if gs is None:
            term["structure"] = None
            term["note"] = f"Gamma(1-alpha)^2 with 1-alpha = {1-alpha}: base not in {{1/2, 1/4, 3/4}} -- structure not reduced; value below is numeric only"
        else:
            g_rat, g_pi, g_rad, g_gam = gs
            coef *= g_rat
            pi_power += g_pi
            for pp, e in g_rad.items():
                radicals[pp] = radicals.get(pp, Fr(0)) + e
            gamma = dict(g_gam)
        # fold integer parts of radical exponents into the rational
        rad_out = {}
        for pp, e in radicals.items():
            ip = math.floor(e)
            coef *= Fr(pp) ** ip
            fe = e - ip
            if fe:
                rad_out[pp] = fe
        term["structure"] = {"sign": sign, "rational": str(coef), "pi": str(pi_power),
                             "radicals": {str(pp): str(e) for pp, e in rad_out.items()},
                             "gamma": {str(a): g for a, g in gamma.items()}} if gs is not None else None
        term["direct_formula"] = {"tau_prefactor_2power": str(Fr(1 - L) + Fr(L, 2)), "lambda": str(lam), "L": L}
        out["terms"].append(term)
    return out


def direct_value(msq_derived, term, dps):
    """The value of (3) evaluated directly with mpmath (mp.gamma at lambda/2) -- the internal cross-check
    of the structure reduction; independent of calpha_rings."""
    with mp.workdps(dps + 10):
        L = msq_derived["L"]
        n = term["n"]
        lam = Fr(term["lambda"])
        rn = Fr(term["r_n"])
        sgn, s2 = term["cos_sign"], term["cos_sqrt2_power"]
        tau = (mp.mpf(2) ** (1 - L)) * (mp.mpf(2) / mp.pi) ** (mp.mpf(L) / 2) / mp.sqrt(mp.mpf(msq_derived["prod_m"]))
        tau *= mp.mpf(rn.numerator) / rn.denominator * sgn * mp.mpf(2) ** (mp.mpf(s2) / 2)
        lamf = mp.mpf(lam.numerator) / lam.denominator
        val = tau * mp.mpf(2) ** (lamf - 2) * mp.gamma(lamf / 2) ** 2
        return +val


# ------------------------------------------------------------------ house module + fixtures
def sha256_of(path):
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for ch in iter(lambda: f.read(1 << 20), b""):
            h.update(ch)
    return h.hexdigest()


def load_calpha_rings(path, allow_unpinned):
    cands = [path] if path else [CALPHA_RINGS_DEFAULT,
                                 os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "coalescer", "calpha_rings.py")]
    for c in cands:
        if c and os.path.exists(c):
            got = sha256_of(c)
            if got != CALPHA_RINGS_SHA256:
                msg = (f"calpha_rings.py at {c}: sha256 {got[:16]}... is not the pin {CALPHA_RINGS_SHA256[:16]}...")
                if not allow_unpinned:
                    sys.stderr.write("REFUSED (exit 3): " + msg + " (pass --allow-unpinned to proceed loudly)\n")
                    sys.exit(EXIT_PIN)
                print("UNPINNED calpha_rings.py: " + msg)
            spec = importlib.util.spec_from_file_location("calpha_rings", c)
            mod = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(mod)
            return mod, c, got
    sys.stderr.write("calpha_rings.py not found (exit 4); tried: %s\n" % cands)
    sys.exit(EXIT_MISSING)


def load_fixtures(path, allow_unpinned):
    if not os.path.exists(path):
        sys.stderr.write(f"fixtures file {path} missing (exit 4)\n")
        sys.exit(EXIT_MISSING)
    got = sha256_of(path)
    if got != FIXTURES_SHA256:   # 2026-09-11: unconditional (the self-arming template clause removed with its siblings')
        msg = f"fixtures {path}: sha256 {got[:16]}... is not the pin {FIXTURES_SHA256[:16]}..."
        if not allow_unpinned:
            sys.stderr.write("REFUSED (exit 3): " + msg + "\n")
            sys.exit(EXIT_PIN)
        print("UNPINNED fixtures: " + msg)
    return json.load(open(path)), got


def canonical(cf):
    """Canonical exact form of a calpha_rings.ClosedForm: (sign, prime exponents incl. radicals and the
    Gamma-reflection extras, pi power incl. extras, reduced Gamma exponents) -- two structures are the
    same number iff these agree."""
    ex = cf.prime_exponents()
    gam, extra_pi, extra_p = cf.gamma_reduced()
    for p, e in extra_p.items():
        ex[p] = ex.get(p, 0) + e
    ex = {int(p): str(Fr(e)) for p, e in sorted(ex.items()) if e}
    return {"sign": cf.sign, "primes": ex, "pi": str(Fr(cf.pi + extra_pi)), "gamma": {str(a): g for a, g in sorted(gam.items())}}


def agree_digits(a, b, cap):
    a, b = mp.mpf(a), mp.mpf(b)
    if b == 0:
        return float(cap) if a == 0 else min(float(cap), float(-mp.log10(abs(a))))
    d = abs(a - b) / abs(b)
    return min(float(cap), float(-mp.log10(d))) if d > 0 else float(cap)


def sig_digits(s):
    return len(s.replace("-", "").replace(".", "").lstrip("0"))


def flip_digit(s, k):
    """Flip the k-th significant digit of a decimal string (the planted-string control)."""
    out = list(s)
    seen = 0
    for i, ch in enumerate(out):
        if ch.isdigit() and (seen or ch != "0"):
            seen += 1
            if seen == k:
                out[i] = str((int(ch) + 1) % 10)
                return "".join(out)
    raise ValueError("string too short")


# ------------------------------------------------------------------ the check
def check_family(name, msq, dps, rings, fixtures, tamper=None, phase_tamper=0, string_flip=None, quiet=False):
    """Returns (fails: list of str, report: dict)."""
    fails, rep = [], {"family": name, "msq": list(msq), "coefficients": {}}
    d = derive(msq, nmax=max(3, 2), tamper=tamper, phase_tamper=phase_tamper)
    rep["derivation"] = {k: d[k] for k in ("m", "L", "coalescence", "hankel_a", "r_n", "prod_m")}
    fam = fixtures["families"].get(name)
    spec = None
    if fam and fam.get("calpha_rings_family"):
        spec = rings.family_spec(fam["calpha_rings_family"])
    if not quiet:
        print(f"\n== {name} msq={list(msq)}: m = {d['m']}, L = {d['L']} propagators, {d['coalescence']}")
        print(f"   Hankel a_k: {d['hankel_a']}")
        print(f"   tail r_n  : {d['r_n']}")
    for term in d["terms"]:
        alpha = term["alpha"]
        if term.get("skip"):
            if not quiet:
                print(f"   n={term['n']}: alpha={alpha}: {term['skip']}")
            continue
        entry = {"n": term["n"], "lambda": term["lambda"], "r_n": term["r_n"], "cos": term["cos_value"]}
        st = term["structure"]
        with mp.workdps(dps):
            v_direct = direct_value(d, term, dps)
            cf = None
            if st is not None:
                cf = rings.ClosedForm(st["sign"], st["rational"], st["pi"], st["radicals"], st["gamma"])
                v_struct = cf.value()
                entry["structure"] = st
                entry["display"] = cf.render()
                dd = agree_digits(v_struct, v_direct, dps)
                entry["structure_vs_direct_d"] = round(dd, 1)
                if dd < dps - 5:
                    fails.append(f"{name} c_{{{alpha}}}: structure value vs direct formula {dd:.1f} d < {dps-5} (reduction error)")
            else:
                v_struct = v_direct
                entry["display"] = "(structure not reduced)"
            entry["value"] = mp.nstr(v_struct, dps)
            if not quiet:
                print(f"   n={term['n']}: lambda={term['lambda']}, alpha={alpha}, r_n={term['r_n']}, cos={term['cos_value']}")
                print(f"      c_{{{alpha}}} = {entry['display']}")
                print(f"      value ({dps} d) = {entry['value']}")
                if cf is not None:
                    print(f"      structure vs direct mpmath formula: {entry['structure_vs_direct_d']} d")
            # recorded structure
            if spec is not None and cf is not None:
                rec_cf = rings.closed_form(spec, alpha)
                if rec_cf is None:
                    entry["record_structure"] = "none recorded for this alpha"
                    if not quiet:
                        print(f"      recorded structure: none for alpha={alpha}")
                else:
                    same = canonical(cf) == canonical(rec_cf)
                    entry["record_structure"] = {"display": rec_cf.display, "canonical": canonical(rec_cf), "EQUAL": same}
                    entry["derived_canonical"] = canonical(cf)
                    if not quiet:
                        print(f"      recorded structure {rec_cf.display}: STRUCTURE {'EQUAL' if same else 'DIFFERENT'}")
                    if not same:
                        fails.append(f"{name} c_{{{alpha}}}: derived structure {cf.render()} != recorded {rec_cf.display}")
            # record strings
            strings = (fam or {}).get("record_strings", {}).get(alpha, [])
            entry["record_strings"] = []
            for rs in strings:
                s = rs["value"]
                if string_flip and string_flip == (name, alpha, rs["label"]):
                    s = flip_digit(s, string_flip_digit(rs))
                cert = int(rs.get("certified_digits", sig_digits(s)))
                cap = min(cert, dps)
                dd = agree_digits(v_struct, s, cap)
                bar = min(50, cap - 2)
                ok = dd >= bar
                entry["record_strings"].append({"label": rs["label"], "source": rs["source"], "certified_digits": cert,
                                                "cap": cap, "agree_d": round(dd, 1), "bar": bar, "PASS": ok})
                if not quiet:
                    print(f"      vs record string [{rs['label']}] ({cert} certified d; {rs['source'].get('path','?')} {str(rs['source'].get('sha256','?'))[:16]}): {dd:.1f} d (cap {cap}, bar {bar}) {'PASS' if ok else 'FAIL'}")
                if not ok:
                    fails.append(f"{name} c_{{{alpha}}} vs record string [{rs['label']}]: {dd:.1f} d < bar {bar}")
            # PSLQ pin demoted to the check
            pin = (fam or {}).get("pslq_pins", {}).get(alpha)
            if pin and cf is not None:
                names = pin["names"]
                try:
                    vec = cf.log_vector(names)
                except Exception as e:   # noqa: BLE001
                    vec = None
                    fails.append(f"{name} c_{{{alpha}}}: log vector not in the pinned basket ({e})")
                pinned = list(rings.canonicalize(list(pin["vector"])))
                vec = list(vec) if vec is not None else None
                same = (vec == pinned)
                # numeric verification of the pinned relation with the analytic value
                with mp.workdps(dps):
                    T = mp.log(abs(v_struct))
                    resid = pinned[0] * T + sum(pinned[i + 1] * rings.member_value(n) for i, n in enumerate(names))
                    resid_d = float(-mp.log10(abs(resid))) if resid != 0 else float(dps)
                entry["pslq_pin"] = {"names": names, "pinned_vector": pinned, "derived_vector": vec, "EQUAL": same,
                                     "relation_residual_log10": round(-resid_d, 1), "source": pin.get("source")}
                if not quiet:
                    print(f"      PSLQ pin {pin.get('source','')}: pinned {pinned} derived {vec}: {'EQUAL' if same else 'DIFFERENT'}; relation residual 10^{-resid_d:.1f} with the analytic value")
                if not same:
                    fails.append(f"{name} c_{{{alpha}}}: derived relation vector {vec} != pinned {pinned}")
                if resid_d < dps - 8:
                    fails.append(f"{name} c_{{{alpha}}}: pinned relation residual 10^-{resid_d:.1f} at dps {dps}")
        rep["coefficients"][alpha] = entry
    # 5. the internal identity: the tail's next order reproduces the Frobenius branch series,
    #    c_{alpha+1} / c_alpha = phi_{alpha,1} EXACTLY (Phi_alpha = t^alpha (1 + phi_1 t + ...), the record operator's
    #    exact-Q branch recursion) -- a check of the derivation against the operator, independent of any value.
    phis = (fam or {}).get("branch_phi1", {})
    rep["phi1_identity"] = {}
    by_alpha = {Fr(t["alpha"]): t for t in d["terms"] if not t.get("skip") and t.get("structure")}
    for alpha_s, phi_s in phis.items():
        al = Fr(alpha_s)
        t0, t1 = by_alpha.get(al), by_alpha.get(al + 1)
        if t0 is None or t1 is None:
            rep["phi1_identity"][alpha_s] = "not derivable at nmax (raise --nmax)"
            continue
        s0, s1 = t0["structure"], t1["structure"]
        same_factors = (s0["pi"] == s1["pi"] and s0["radicals"] == s1["radicals"] and s0["gamma"] == s1["gamma"])
        ratio = Fr(s1["rational"]) / Fr(s0["rational"]) * s1["sign"] * s0["sign"] if same_factors else None
        ok = same_factors and ratio == Fr(phi_s)
        rep["phi1_identity"][alpha_s] = {"c_next_over_c": str(ratio) if ratio is not None else "factors differ", "phi_1_of_record": phi_s,
                                         "source": (fam or {}).get("branch_phi1_source"), "EQUAL": bool(ok)}
        if not quiet:
            print(f"   identity c_{{{al+1}}}/c_{{{al}}} = {ratio} vs the record operator's phi_{{{al},1}} = {phi_s}: {'EQUAL (exact)' if ok else 'DIFFERENT'}")
        if not ok:
            fails.append(f"{name} c_{{{alpha_s}}}: tail ratio c_{{{al+1}}}/c_{{{al}}} = {ratio} != phi_1 of record {phi_s}")
    rep["fails"] = fails
    return fails, rep


def string_flip_digit(rs):
    return 30


def run_check(families, dps, rings, fixtures, quiet=False, **kw):
    all_fails, reps = [], {}
    for name, msq in families:
        f, r = check_family(name, msq, dps, rings, fixtures, quiet=quiet, **kw)
        all_fails += f
        reps[name] = r
    return all_fails, reps


def main(argv=None):
    ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    ap.add_argument("--check", action="store_true")
    ap.add_argument("--selftest", action="store_true")
    ap.add_argument("--derive", action="store_true", help="print the derivation objects only")
    ap.add_argument("--family", default="ALL", help="K3 | CY3 | CY4 | ALL")
    ap.add_argument("--masses", help="m1^2,...,mL^2 (perfect squares, one the sum of the others' roots)")
    ap.add_argument("--nmax", type=int, default=3)
    ap.add_argument("--dps", type=int, default=60)
    ap.add_argument("--calpha-rings", default=None)
    ap.add_argument("--fixtures", default=FIXTURES_DEFAULT)
    ap.add_argument("--allow-unpinned", action="store_true")
    ap.add_argument("--json", default=None, help="write the check report here (O_EXCL)")
    a = ap.parse_args(argv)
    if a.dps < 50:
        ap.error("--dps must be >= 50 (the analytic value is printed beside the record at >= 50 digits)")
    if not (a.check or a.selftest or a.derive):
        ap.error("one of --check / --selftest / --derive")
    print(f"threshold_hankel_tail.py STAMP {STAMP}; mpmath {mp.__version__}; dps {a.dps}")
    if a.masses:
        # 2026-09-11: a tuple with no root equal to the sum of the others (e.g. 1,4,4,9: 1 + 3 = 2 + 2) is REFUSED BY
        # DESIGN by name with the usage exit code, before any number is printed (formerly a ValueError traceback).
        try:
            coalescence(tuple(int(x) for x in a.masses.split(",")))
        except ValueError as exc:
            sys.stderr.write(f"REFUSED (exit {EXIT_USAGE}): --masses {a.masses}: {exc}; this tool derives the fractional "
                             f"threshold branch of a ONE-line coalescence only (the largest root the sum of the others)\n")
            return EXIT_USAGE
    if a.derive:
        msq = tuple(int(x) for x in a.masses.split(",")) if a.masses else FAMILIES.get(a.family.upper())
        if msq is None:
            ap.error("--derive needs --masses or --family K3|CY3|CY4")
        d = derive(msq, nmax=a.nmax)
        print(json.dumps(d, indent=1))
        for t in d["terms"]:
            if not t.get("skip"):
                print(f"c_{{{t['alpha']}}} direct value ({a.dps} d):", mp.nstr(direct_value(d, t, a.dps), a.dps))
        return 0
    rings, rpath, rsha = load_calpha_rings(a.calpha_rings, a.allow_unpinned)
    fixtures, fsha = load_fixtures(a.fixtures, a.allow_unpinned)
    print(f"calpha_rings.py {rpath} sha256 {rsha[:16]}...; fixtures {a.fixtures} sha256 {fsha[:16]}...")
    if a.masses:
        fams = [("custom", tuple(int(x) for x in a.masses.split(",")))]
    elif a.family.upper() == "ALL":
        fams = list(FAMILIES.items())
    else:
        if a.family.upper() not in FAMILIES:
            ap.error("--family K3|CY3|CY4|ALL")
        fams = [(a.family.upper(), FAMILIES[a.family.upper()])]
    rc = 0
    report = {"stamp": STAMP, "dps": a.dps, "calpha_rings": {"path": rpath, "sha256": rsha}, "fixtures": {"path": a.fixtures, "sha256": fsha}}
    if a.check:
        fails, reps = run_check(fams, a.dps, rings, fixtures)
        report["check"] = reps
        print("\nCHECK:", "PASS" if not fails else "FAIL")
        for f in fails:
            print("  FAIL:", f)
        rc = EXIT_FAIL if fails else 0
    if a.selftest:
        print("\n== SELFTEST: clean check on K3, CY3, CY4")
        fails, reps = run_check(list(FAMILIES.items()), a.dps, rings, fixtures, quiet=True)
        clean_ok = not fails
        print(f"   clean check: {'PASS' if clean_ok else 'FAIL ' + str(fails)}")
        controls = {}
        # (a) Hankel a_1 tampered: -1/8 -> -1/7 ; must FAIL exactly on the n = 1 coefficients
        f_a, _ = run_check(list(FAMILIES.items()), a.dps, rings, fixtures, quiet=True, tamper={1: Fr(-1, 7)})
        hit = [x for x in f_a if ("K3 c_{3/2}" in x or "CY3 c_{7/4}" in x)]
        miss = [x for x in f_a if not ("K3 c_{3/2}" in x or "CY3 c_{7/4}" in x or "tail ratio" in x)]
        ok_a = bool(hit) and not miss
        controls["planted_a1"] = {"expected_fail_on": ["K3 c_{3/2}", "CY3 c_{7/4}"], "fails": f_a, "PASS": ok_a}
        print(f"   planted a_1 = -1/7: {'FAILED BY NAME where planted' if ok_a else 'NOT as expected'}: {len(hit)} named fails on K3 c_{{3/2}} / CY3 c_{{7/4}}, {len(miss)} elsewhere")
        # (b) phase tampered by pi (the conjugate pair's sign flipped): must FAIL on every coefficient
        f_b, _ = run_check(list(FAMILIES.items()), a.dps, rings, fixtures, quiet=True, phase_tamper=4)
        names_hit = {x.split(":")[0].split(" vs ")[0] for x in f_b}
        need = {"K3 c_{3/2}", "CY3 c_{5/4}", "CY3 c_{7/4}", "CY4 c_{3/2}"}
        ok_b = need <= names_hit
        controls["planted_phase"] = {"expected_fail_on": sorted(need), "fails": f_b, "PASS": ok_b}
        print(f"   planted phase e^{{i pi (L+2)/4}} (sign of the conjugate pair): {'FAILED BY NAME on all four' if ok_b else 'NOT as expected'}: {len(f_b)} named fails on {sorted(names_hit)}")
        # (c) a record string digit flipped (CY4 GATE_CY4 target, digit 30): must FAIL on that string only
        f_c, _ = run_check([("CY4", FAMILIES["CY4"])], a.dps, rings, fixtures, quiet=True, string_flip=("CY4", "3/2", "GATE_CY4.targets.3/2"))
        ok_c = len(f_c) == 1 and "record string [GATE_CY4.targets.3/2]" in f_c[0]
        controls["planted_string_digit"] = {"expected_fail_on": ["CY4 c_{3/2} vs record string [GATE_CY4.targets.3/2]"], "fails": f_c, "PASS": ok_c}
        print(f"   planted record-string digit (CY4, digit 30): {'FAILED BY NAME on that string only' if ok_c else 'NOT as expected'}: {f_c}")
        selftest_ok = clean_ok and ok_a and ok_b and ok_c
        report["selftest"] = {"clean": {"fails": fails, "PASS": clean_ok}, "controls": controls, "PASS": selftest_ok}
        print("SELFTEST:", "PASS" if selftest_ok else "FAIL")
        if not selftest_ok:
            rc = EXIT_FAIL
    if a.json:
        with open(a.json, "x") as f:
            json.dump(report, f, indent=1)
        print("report written:", a.json)
    return rc


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