#!/usr/bin/env python3
"""kite-boundary.py -- DERIVE the s=-2 boundary seed of the unequal-mass kite.

Companion to kite-evaluate.py (which TRANSPORTS from s=-2 and gates at held-out
oracle points). This script removes the last AMFlow input of the kite page: the
130-digit boundary Laurent vector at s=-2 stored in kite-boundary-sm2.json.
Here that entire 42-component vector (eps^-2..eps^0 of all 14 masters) is
COMPUTED from scratch -- closed-form vacuum values at s=0 plus exact-DE
transport -- and kite-boundary-sm2.json is demoted to a HELD-OUT GATE.

Two-loop self-energy, propagators
    D1 = k1^2 - 1,  D2 = k2^2,  D3 = (k1-k2)^2 - 1,  D4 = (k1-p)^2,  D5 = (k2-p)^2 - 2,
p^2 = s (Euclidean s < 0), measure  int d^dk/(i pi^{d/2}) per loop, d = 4 - 2 eps, mu = 1.
The sunrise sub-block (D1,D3,D5) has squared masses (1,1,2); its maximal cut is a
NON-MODULAR elliptic curve (at s=-2: y^2 = x^3 - 756 x + 7344, the u=3 scaling of the
minimal model y^2 = x^3 + x^2 - 9x + 7, Cremona 128a2, conductor 128, non-CM), so -- by the
kernel-modularity probe of this work -- no finite constant-coefficient eMPL form in the
fixed three-puncture word bases exists (an eMPL form with s-dependent punctures for three
distinct masses is in Broedel-Duhr-Dulat-Penante-Tancredi, arXiv:1902.09971 Sec. 5); the
terminal form here is ODE transport, and the boundary is the construction below.

THE AMFLOW-FREE BOUNDARY DEFINITION (what is implemented here)
==============================================================

  J^0_top and the 13 sub-masters are the UNIQUE solution of the exact rational Kira IBP
  connection  dM/ds = A(4-2eps, s) M  (14x14, embedded below; graded over eps^-2..eps^0)
  that remains BOUNDED at the regular singular point s = 0, with s=0 value given in
  CLOSED FORM by the two-loop vacuum ring:

    T(M)          = Gamma(1+eps) M^(1-eps) / (eps(1-eps))                    [tadpole]
    V(M1,M2,M3)   = two-loop vacuum sunset; with c = M1+M2+M3, Li = ln Mi,
                    L1 = sum Mi Li, L2 = sum Mi Li^2, g = gamma_E:
      V[eps^-2] = c/2
      V[eps^-1] = (3/2 - g) c - L1
      V[eps^0]  = -b0 + 2 g L1 - 3 g c + g^2 c,
      b0 = -(1/2) [ L2 - 6 L1 + (M2+M3-M1) L2L3-term + cyclic + xi + c (7 + zeta(2)) ]
           (i.e. Ford-Jack-Jones NPB 387 (1992) 373 eq. (4.20) at Q^2=1; equivalently
            S.P. Martin hep-ph/0111209 eqs. (2.19)-(2.28) + TSIL hep-ph/0501132 eq. (2.34),
            converted exactly by V = -e^{-2 g eps} I_bold|_{Q^2=1})
      xi(M1,M2,M3) for lambda = sum Mi^2 - 2 sum MiMj < 0 (mass triangle exists):
      xi = -2 sqrt(-lambda) [ Cl2(2 a1) + Cl2(2 a2) + Cl2(2 a3) ],  a_i = triangle angles.
      SPECIAL VALUE used here:  xi(1,1,2) = -8 * Catalan   (right isosceles triangle).

  Vacuum values of the 14 masters at s=0 (derived by partial fractions at p=0):
    M0 = M2 = T(1)T(2)          M1 = M3 = T(1)T(2)/2       M4 = M7 = T(1)^2
    M5 = -M6 = V(0,1,0)         M8  = V(1,1,2)             M9  = T(1)^2 + 2 V(1,1,2)
    M10 = 4 T(1)^2 + 4 V(1,1,2) M11 = V(0,1,2)             M12 = [V(0,1,2)-V(0,1,0)]/2
    M13 = [V(1,1,2) - V(1,1,0) - V(0,1,2) + V(0,1,0)]/2    (eps^-2, eps^-1 parts vanish
                                                            identically: the kite is finite)
  Every constant is CLASSICAL: gamma_E, zeta(2), ln 2, Catalan.

  WHY this fixes everything (the analytic boundary condition that kills c1, c2):
  the graded residue matrix B0 = s A|_{s=0} (42x42) has eigenvalues {0 (x15), -1 (x24),
  -2 (x3)} with the 0-eigenspace SEMISIMPLE; hence (i) bounded-at-0 solutions form exactly
  the 15-dim analytic branch, (ii) there are NO positive integer resonances, so the s=0
  value determines every Taylor coefficient, and (iii) ker(B0) cap Im(B0) = 0 excludes
  log s admixtures with bounded limits. The physical masters are bounded at s -> 0^- (no
  Landau threshold at s=0; nearest singularities s=1, 2, 6-4 sqrt2) with vacuum limits as
  above. Uniqueness then delivers J^0_top(s) everywhere on the negative axis by:
    (1) the Frobenius series at s=0 (radius 1), summed at s_exit = -1/10;
    (2) adaptive high-order Taylor stepping of the same exact connection.

  The boundary constant of the old A- record is now a THEOREM of this construction:
    N_kite := 4 s J^0_top(s)|_{s=-2} = -8 M13[eps^0](-2),
  computed here from classical constants + convergent series only. (PSLQ naming of
  N_kite in classical rings remains honestly negative; value-fittability FACT-2.)

Transporter safeguards baked in (TOOL_CHANGELOG 2026-07-03):
  - per-step Taylor truncation ~ SAFETY^order: SAFETY = 0.25 and
    order = 1.7*dps + 20  >=  working_dps / log10(1/SAFETY)  with ~2.4x margin;
  - flint/arb ball radii are trimmed to midpoints (.mid()) after every ladder
    level and step (correctness comes from --check + the held-out gate, not balls);
  - mp.dps is set INSIDE main() after argparse (module-level mpf at import
    dps=15 would silently truncate every constant).

AMFlow's role: HELD-OUT GATE ONLY. kite-boundary-sm2.json (the 130-digit
AMFlow boundary vector of the original computation, vendored in this
directory) is loaded, if present, purely as the gate reference; it
never enters the construction. Mutation test: --mutate perturbs the Catalan-
bearing top-seed component by 1e-30 and the gate collapses to ~30 d, proving
the gate actually consumes the derived seed.

Verification history: the original full-precision run (2026-07-03) reproduces
the 130 d references to 125.0-125.2 d at --dps 100; this vendored copy
re-measures its own agreement on every run.

Requirements: python3 + mpmath + the sibling kite_exact_parse.py (the exact parser
of the connection strings; no sympy at runtime since 2026-09-05) and the sibling
kite-connection-A14-graded.json (the eps-graded coefficient lists the transport
consumes -- the lists the earlier sympy parse produced, frozen once -- checked
against the embedded connection entry by entry at every start), both sha256-pinned
below (a byte change is REFUSED, exit 3); python-flint (arb) used automatically if
present (3-5x faster); NO AMFlow / Kira / Mathematica / network. The files read are
those two pinned siblings and the OPTIONAL sibling kite-boundary-sm2.json (gate);
the only file write is the OPTIONAL --emit output.

CLI:
  python3 kite-boundary.py --dps 100                  # derive seed, gate vs stored JSON
  python3 kite-boundary.py --dps 100 --check          # two-precision rule: rerun at dps+60, diff
  python3 kite-boundary.py --dps 100 --emit out.json  # write derived seed, kite-boundary-sm2.json schema
  python3 kite-boundary.py --dps 100 --mutate         # mutation test (gate must collapse)
"""

import argparse
import json
import os
import time

import hashlib
import importlib.util
import sys
from fractions import Fraction

import mpmath as mp

try:
    from flint import arb, arb_mat, ctx as _fctx, fmpz
    HAVE_FLINT = True
except Exception:
    HAVE_FLINT = False

# ---------------------------------------------------------------------------
# Embedded EXACT rational 14x14 IBP connection A(d, s) (Kira; analytic formula
# -- the exact reduction of this family, vendored inline).
# ---------------------------------------------------------------------------
CONN = {
 "masters": [
  [
   1,
   0,
   0,
   0,
   1
  ],
  [
   1,
   1,
   0,
   0,
   1
  ],
  [
   1,
   0,
   0,
   1,
   1
  ],
  [
   1,
   1,
   0,
   1,
   1
  ],
  [
   1,
   0,
   1,
   0,
   0
  ],
  [
   0,
   1,
   1,
   1,
   0
  ],
  [
   -1,
   1,
   1,
   1,
   0
  ],
  [
   1,
   0,
   1,
   1,
   0
  ],
  [
   1,
   0,
   1,
   0,
   1
  ],
  [
   1,
   -1,
   1,
   0,
   1
  ],
  [
   1,
   -2,
   1,
   0,
   1
  ],
  [
   0,
   0,
   1,
   1,
   1
  ],
  [
   0,
   1,
   1,
   1,
   1
  ],
  [
   1,
   1,
   1,
   1,
   1
  ]
 ],
 "A": [
  [
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0"
  ],
  [
   "(2 - d)/(2*s**2 - 4*s)",
   "(d*s + 2*d - 4*s - 4)/(2*s**2 - 4*s)",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0"
  ],
  [
   "(2 - d)/(2*s**2 - 2*s)",
   "0",
   "(d*s + d - 4*s - 2)/(2*s**2 - 2*s)",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0"
  ],
  [
   "0",
   "(2 - d)/(2*s**2 - 2*s)",
   "(2 - d)/(2*s**2 - 4*s)",
   "(d*s**2 - 2*d - 4*s**2 + 3*s + 4)/(s**3 - 3*s**2 + 2*s)",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0"
  ],
  [
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0"
  ],
  [
   "0",
   "0",
   "0",
   "0",
   "0",
   "(d*s + 3*d - 4*s - 6)/(2*s**2 - 2*s)",
   "(3*d - 6)/(2*s**2 - 2*s)",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0"
  ],
  [
   "0",
   "0",
   "0",
   "0",
   "0",
   "(d - 2)/s",
   "(d - 2)/s",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0"
  ],
  [
   "0",
   "0",
   "0",
   "0",
   "(2 - d)/(2*s**2 - 2*s)",
   "0",
   "0",
   "(d*s + d - 4*s - 2)/(2*s**2 - 2*s)",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0"
  ],
  [
   "(-d*s - 2*d + 2*s + 4)/(s**3 - 14*s**2 + 28*s - 8)",
   "0",
   "0",
   "0",
   "(-d*s**2 + d*s - 2*d + 2*s**2 - 2*s + 4)/(2*s**4 - 28*s**3 + 56*s**2 - 16*s)",
   "0",
   "0",
   "0",
   "(d*s**3 - 10*d*s**2 - 20*d*s + 8*d - 4*s**3 + 34*s**2 + 16*s - 8)/(2*s**4 - 28*s**3 + 56*s**2 - 16*s)",
   "(9*d*s**2 + 40*d*s - 20*d - 12*s**2 - 60*s + 24)/(4*s**4 - 56*s**3 + 112*s**2 - 32*s)",
   "(-9*d*s + 6*d + 12*s - 8)/(4*s**4 - 56*s**3 + 112*s**2 - 32*s)",
   "0",
   "0",
   "0"
  ],
  [
   "(-d*s + 6*d + 2*s - 12)/(s**2 - 12*s + 4)",
   "0",
   "0",
   "0",
   "(-5*d*s + 6*d + 10*s - 12)/(2*s**3 - 24*s**2 + 8*s)",
   "0",
   "0",
   "0",
   "(-3*s**2 + 8*s - 4)/(s**3 - 12*s**2 + 4*s)",
   "(5*d*s**2 + 12*d - 8*s**2 + 12*s - 8)/(4*s**3 - 48*s**2 + 16*s)",
   "(-3*d*s - 6*d + 4*s + 8)/(4*s**3 - 48*s**2 + 16*s)",
   "0",
   "0",
   "0"
  ],
  [
   "(-d*s**2 + 8*d*s + 20*d + 2*s**2 - 16*s - 40)/(s**2 - 12*s + 4)",
   "0",
   "0",
   "0",
   "(-d*s**2 - 8*d*s + 20*d + 2*s**2 + 16*s - 40)/(2*s**3 - 24*s**2 + 8*s)",
   "0",
   "0",
   "0",
   "(-s**3 + 2*s**2 + 4*s - 8)/(s**3 - 12*s**2 + 4*s)",
   "(d*s**3 + 6*d*s**2 + 28*d*s + 40*d - 36*s**2 + 96*s - 48)/(4*s**3 - 48*s**2 + 16*s)",
   "(d*s**2 - 24*d*s - 20*d + 16*s + 32)/(4*s**3 - 48*s**2 + 16*s)",
   "0",
   "0",
   "0"
  ],
  [
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0",
   "0"
  ],
  [
   "0",
   "0",
   "0",
   "0",
   "0",
   "(d*s + d - 3*s - 2)/(s**3 - 3*s**2 + 2*s)",
   "(3*d - 6)/(2*s**3 - 6*s**2 + 4*s)",
   "0",
   "0",
   "0",
   "0",
   "(2 - d)/(2*s**2 - 4*s)",
   "(d*s + 2*d - 4*s - 4)/(2*s**2 - 4*s)",
   "0"
  ],
  [
   "(-d*s**2 + 2*d*s - 8*d + 2*s**2 - 4*s + 16)/(2*s**5 - 34*s**4 + 144*s**3 - 240*s**2 + 160*s - 32)",
   "(d - 2)/(2*s**2 - 6*s + 4)",
   "0",
   "(3 - d)/(s**2 - 3*s + 2)",
   "(-2*d**2*s**3 + 18*d**2*s**2 - 34*d**2*s + 4*d**2 + 9*d*s**3 - 76*d*s**2 + 142*d*s - 12*d - 10*s**3 + 80*s**2 - 148*s + 8)/(4*d*s**6 - 68*d*s**5 + 288*d*s**4 - 480*d*s**3 + 320*d*s**2 - 64*d*s - 12*s**6 + 204*s**5 - 864*s**4 + 1440*s**3 - 960*s**2 + 192*s)",
   "(-d*s**2 + 7*d*s + 2*d + 2*s**2 - 18*s - 4)/(2*s**4 - 8*s**3 + 10*s**2 - 4*s)",
   "(3*d*s + 3*d - 6*s - 6)/(2*s**4 - 8*s**3 + 10*s**2 - 4*s)",
   "(d - 2)/(2*s**2 - 6*s + 4)",
   "(-d*s**4 + 18*d*s**3 - 76*d*s**2 + 24*d*s + 2*s**4 - 48*s**3 + 196*s**2 - 96*s + 16)/(4*s**6 - 68*s**5 + 288*s**4 - 480*s**3 + 320*s**2 - 64*s)",
   "(4*d*s**3 + 5*d*s**2 + 32*d*s - 20*d - 6*s**3 - 60*s + 24)/(4*s**6 - 68*s**5 + 288*s**4 - 480*s**3 + 320*s**2 - 64*s)",
   "(-3*d*s - 6*d + 4*s + 8)/(4*s**5 - 64*s**4 + 224*s**3 - 256*s**2 + 64*s)",
   "(2 - d)/(2*s**3 - 6*s**2 + 4*s)",
   "0",
   "(d*s + 2*d - 6*s - 4)/(2*s**2 - 4*s)"
  ]
 ],
 "TOP_idx": 13
}

# ---------------------------------------------------------------------------
# Held-out AMFlow gate (GATE ONLY -- never used in the construction):
# the stored 130-digit boundary vector kite-boundary-sm2.json in this directory
# (the AMFlow boundary vector of the original computation; see its
# _provenance field). Loaded lazily; if absent, the
# derivation still runs and the gate is reported as SKIPPED.
# ---------------------------------------------------------------------------
HERE = os.path.dirname(os.path.abspath(__file__))
GATE_JSON = os.path.join(HERE, "kite-boundary-sm2.json")
SCRIPT = "kite-boundary"
# sha256 of the two pinned siblings (computed by the producer that cut this file, never typed)
PINS = {
    "kite_exact_parse.py": "b2ee04d293a03e9c0aee40b155ca2c2e771486ede68db34cae3c5da4a9f97398",
    "kite-connection-A14-graded.json": "0dc5bc8f7fa8aeb014278643878e37fe885cfced281e15f7134a9bf1c9d5af71",
}


def _pinned_path(name_or_path):
    """Path of a sibling file; a PINNED file (the exact parser module, a shipped graded file) is refused on
    any byte change (exit 3, the recorded and recomputed sha256 named)."""
    path = name_or_path if os.path.isabs(name_or_path) else os.path.join(HERE, name_or_path)
    want = PINS.get(os.path.basename(path))
    if want is not None:
        if not os.path.exists(path):
            sys.stderr.write(f"{SCRIPT} REFUSED: pinned file {os.path.basename(path)} is missing (recorded sha256 {want})\n")
            raise SystemExit(3)
        have = hashlib.sha256(open(path, "rb").read()).hexdigest()
        if have != want:
            pos = next((k + 1 for k, (x, y) in enumerate(zip(want, have)) if x != y), 0)
            sys.stderr.write(f"{SCRIPT} REFUSED: {os.path.basename(path)} integrity pin mismatch (recorded {want}, "
                             f"recomputed {have}; first differing hex position {pos} of 64, 1-based) -- the shipped file "
                             f"was altered\n")
            raise SystemExit(3)
    return path


def _import_pinned_module(name):
    """Import a sibling module through the pin check (a byte change is REFUSED, exit 3)."""
    spec = importlib.util.spec_from_file_location(name[:-3], _pinned_path(name))
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


kx = _import_pinned_module("kite_exact_parse.py")   # the exact parser of the connection strings (no sympy)
GRADED_PATH = _pinned_path("kite-connection-A14-graded.json")   # the eps-graded coefficient lists (pinned)


def load_gate():
    """Return ({(master, eps_order): mpf_string}, dps) or (None, 0) if absent.
    Only eps^-2..eps^0 entries are gate targets (higher orders in the stored
    file are outside the eps^-2..eps^0 grading derived here)."""
    if not os.path.exists(GATE_JSON):
        return None, 0
    bnd = json.load(open(GATE_JSON))
    assert int(bnd["s"]) == -2
    ref = {}
    for i_str, co in bnd["boundary"].items():
        for o_str, v in co.items():
            if KMIN <= int(o_str) <= KMAX:
                ref[(int(i_str), int(o_str))] = v
    return ref, int(bnd["dps"])

KMIN, KMAX = -2, 0
NEPS = KMAX - KMIN


# ---------------------------------------------------------------------------
# 1. Closed-form vacuum seed (the derived boundary condition at s = 0)
# ---------------------------------------------------------------------------
def T_tadpole(M, order=2):
    """[eps^-1 .. eps^order] of T(M) = Gamma(1+eps) M^(1-eps)/(eps(1-eps)).
    Exact exponential form: T = (M/eps) exp((1-g-lnM) eps + sum_{k>=2}(1+(-1)^k zeta(k)) eps^k/k)."""
    n = order + 1
    with mp.extradps(15):
        Mv = mp.mpf(M)
        cs = [mp.mpf(0), 1 - mp.euler - mp.log(Mv)]
        for k in range(2, n + 1):
            cs.append((1 + (-1) ** k * mp.zeta(k)) / k)
        p = [mp.mpf(1)]
        for j in range(1, n + 1):
            p.append(sum(k * cs[k] * p[j - k] for k in range(1, j + 1)) / j)
        return [Mv * pj for pj in p]


def _cl2(theta):
    return mp.polylog(2, mp.exp(mp.mpc(0, 1) * theta)).imag


def _xi(x, y, z):
    """Martin (2.20)/(2.21) at Q^2=1 for x,y <= z, all > 0; lambda<0 via the
    Clausen (FJJ 4.22) form. xi(1,1,2) = -8 Catalan."""
    lam = x * x + y * y + z * z - 2 * (x * y + y * z + z * x)
    if lam == 0:
        return mp.mpf(0)
    if lam > 0:
        R = mp.sqrt(lam)
        u = (z + x - y - R) / (2 * z)
        v = (z + y - x - R) / (2 * z)
        return R * (2 * mp.log(u) * mp.log(v) - mp.log(x / z) * mp.log(y / z)
                    - 2 * mp.polylog(2, u) - 2 * mp.polylog(2, v) + mp.pi ** 2 / 3)
    r = mp.sqrt(-lam)
    ax = mp.acos((y + z - x) / (2 * mp.sqrt(y * z)))
    ay = mp.acos((z + x - y) / (2 * mp.sqrt(z * x)))
    az = mp.acos((x + y - z) / (2 * mp.sqrt(x * y)))
    return -2 * r * (_cl2(2 * ax) + _cl2(2 * ay) + _cl2(2 * az))


def _I_finite(x, y, z):
    """Martin hep-ph/0111209 eqs. (2.19), (2.26)-(2.28) at Q^2=1; x<=y<=z."""
    nz = sum(1 for m in (x, y, z) if m == 0)
    if nz == 3:
        return mp.mpf(0)
    if nz == 2:
        L = mp.log(z)
        return z * (-L * L / 2 + 2 * L - mp.mpf(5) / 2 - mp.pi ** 2 / 6)
    if nz == 1:
        a, b = z, y
        La, Lb = mp.log(a), mp.log(b)
        if a == b:
            return -a * La * La + 4 * a * La - 5 * a
        return ((a - b) * (mp.polylog(2, b / a) - mp.log(a / b) * mp.log(a - b)
                           + La * La / 2 - mp.pi ** 2 / 6)
                - mp.mpf(5) / 2 * (a + b) + 2 * a * La + 2 * b * Lb - a * La * Lb)
    Lx, Ly, Lz = mp.log(x), mp.log(y), mp.log(z)
    return ((x - y - z) / 2 * Ly * Lz + (y - x - z) / 2 * Lx * Lz
            + (z - x - y) / 2 * Lx * Ly + 2 * (x * Lx + y * Ly + z * Lz)
            - mp.mpf(5) / 2 * (x + y + z) - _xi(x, y, z) / 2)


def V_vacuum(M1, M2, M3):
    """{-2,-1,0}: Laurent of the 2-loop vacuum sunset V(M1,M2,M3), my normalization.
    V = -e^{-2 gamma eps} I_bold|_{Q^2=1}; I_bold from TSIL eq. (2.34)."""
    with mp.extradps(20):
        x, y, z = sorted(mp.mpf(m) for m in (M1, M2, M3))
        c = x + y + z
        A = [m * (mp.log(m) - 1) if m > 0 else mp.mpf(0) for m in (x, y, z)]
        Aeps = [m * (-1 - mp.pi ** 2 / 12 + mp.log(m) - mp.log(m) ** 2 / 2) if m > 0
                else mp.mpf(0) for m in (x, y, z)]
        # I_bold Laurent: [-2] = -c/2, [-1] = sum A - c/2, [0] = I + sum Aeps
        Ib2 = -c / 2
        Ib1 = sum(A) - c / 2
        Ib0 = _I_finite(x, y, z) + sum(Aeps)
        g = mp.euler
        # V = -(1 - 2g eps + 2g^2 eps^2 + O(eps^3)) * (Ib2/eps^2 + Ib1/eps + Ib0 + ...)
        out = {-2: -Ib2,
               -1: -(Ib1 - 2 * g * Ib2),
               0: -(Ib0 - 2 * g * Ib1 + 2 * g * g * Ib2)}
        return {k: +v for k, v in out.items()}


def closed_form_seed(mutate=None):
    """The 42-component boundary vector y(0): eps^-2..eps^0 of all 14 masters at s=0.
    mutate: optional (i, K, delta) to perturb one component (mutation test)."""
    T1 = T_tadpole(1, 2)
    T2 = T_tadpole(2, 2)

    def prod2(a, b):
        out = {k: mp.mpf(0) for k in (-2, -1, 0)}
        for i, xx in enumerate(a[:3]):
            for j, yy in enumerate(b[:3]):
                k = (i - 1) + (j - 1)
                if k <= 0:
                    out[k] += xx * yy
        return out

    V010 = V_vacuum(0, 1, 0)
    V110 = V_vacuum(1, 1, 0)
    V012 = V_vacuum(0, 1, 2)
    V112 = V_vacuum(1, 1, 2)
    T1T2 = prod2(T1, T2)
    T1T1 = prod2(T1, T1)
    h = mp.mpf(1) / 2
    seed = {0: T1T2, 1: {k: v * h for k, v in T1T2.items()}, 2: dict(T1T2),
            3: {k: v * h for k, v in T1T2.items()}, 4: T1T1, 5: V010,
            6: {k: -v for k, v in V010.items()}, 7: dict(T1T1), 8: V112,
            9: {k: T1T1[k] + 2 * V112[k] for k in (-2, -1, 0)},
            10: {k: 4 * T1T1[k] + 4 * V112[k] for k in (-2, -1, 0)},
            11: V012, 12: {k: (V012[k] - V010[k]) * h for k in (-2, -1, 0)},
            13: {-2: mp.mpf(0), -1: mp.mpf(0),
                 0: (V112[0] - V012[0] - V110[0] + V010[0]) * h}}
    # (13,-2), (13,-1): the V-combinations vanish identically (kite finiteness);
    # set exactly 0 (the eps^-1 cancellation is also verified numerically in selftest).
    if mutate is not None:
        i, K, delta = mutate
        seed[i][K] += mp.mpf(delta)
    return seed


# ---------------------------------------------------------------------------
# 2. Graded system, exact Taylor data (exact rationals: the parsed connection)
# ---------------------------------------------------------------------------
def build_graded():
    """The eps-graded connection: Aco[(i, j, k)] = (pc, qc), the eps^k coefficient of
    A_ij(4 - 2 eps, s) as the exact rational function P(s)/Q(s) (ascending Fraction lists),
    READ from the pinned graded file (the lists the earlier sympy parse produced, frozen
    2026-09-05, so the numerics consume the same integers as before) and CHECKED against the
    embedded connection strings by the exact parser at every call: every entry re-derived
    and asserted equal as a rational function, the key sets equal (a mismatch is REFUSED by
    name, exit 3)."""
    N = len(CONN["masters"])
    try:
        Aco = kx.load_graded(CONN["A"], GRADED_PATH, NEPS)
    except kx.ParseError as ex:
        sys.stderr.write(f"{SCRIPT} REFUSED: {os.path.basename(GRADED_PATH)} vs the embedded connection: {ex}\n")
        raise SystemExit(3)
    STATE = [(i, K) for i in range(N) for K in range(KMIN, KMAX + 1)]
    NS = len(STATE)
    ENT = []
    for n, (i, K) in enumerate(STATE):
        for m, (j, Kp) in enumerate(STATE):
            k = K - Kp
            if 0 <= k <= NEPS and (i, j, k) in Aco:
                pc, qc = Aco[(i, j, k)]
                ENT.append((n, m, pc, qc))
    IDX = {sK: n for n, sK in enumerate(STATE)}
    return STATE, IDX, NS, ENT


def b_taylor_exact(pc, qc, nmax):
    """Exact Taylor coefficients (Fractions), orders 0..nmax, of s*P/Q at s=0."""
    v = 0
    while qc[v] == 0:
        v += 1
    qt = qc[v:]
    c = [Fraction(0)] * (nmax + 2)
    for n in range(nmax + 2):
        t = pc[n] if n < len(pc) else Fraction(0)
        for k in range(1, min(n, len(qt) - 1) + 1):
            t -= qt[k] * c[n - k]
        c[n] = t / qt[0]
    shift = 1 - v
    out = [Fraction(0)] * (nmax + 1)
    for n in range(nmax + 1 - shift):
        out[n + shift] = c[n]
    return out


# ---------------------------------------------------------------------------
# 3. Numeric backends (flint arb preferred; pure mpmath fallback)
# ---------------------------------------------------------------------------
class Backend:
    def __init__(self, use_flint):
        self.flint = use_flint and HAVE_FLINT

    def setup(self):
        if self.flint:
            _fctx.prec = int(mp.mp.dps * 3.3322) + 80

    def from_rat(self, r):
        if self.flint:
            return arb(fmpz(r.numerator)) / arb(fmpz(r.denominator))
        return mp.mpf(r.numerator) / mp.mpf(r.denominator)

    def from_mpf(self, x):
        x = mp.mpf(x)
        if not self.flint:
            return x
        sgn, man, e, _ = x._mpf_
        if man == 0:
            return arb(0)
        a = arb(fmpz(int(man))) * (arb(2) ** int(e))
        return -a if sgn else a

    def to_mpf(self, a):
        if not self.flint:
            return a
        return mp.mpf(a.str(mp.mp.dps + 15, radius=False))

    def zero(self):
        return arb(0) if self.flint else mp.mpf(0)

    def trim(self, x):
        """Discard the arb error ball (keep midpoint). Cures the ball-radius
        blow-up of deep recurrences (dependency problem); correctness is
        guaranteed by the two-precision rule + held-out gate, not by balls."""
        return x.mid() if self.flint else x

    def trim_vec(self, v):
        if not self.flint:
            return v
        return [x.mid() for x in v]

    def solve_shifted(self, B0rows, n, rhs, NS):
        """solve (n I - B0) x = rhs. B0rows: dense list of lists (backend numbers)."""
        if self.flint:
            M = arb_mat(NS, NS)
            for i in range(NS):
                for j in range(NS):
                    M[i, j] = -B0rows[i][j]
                M[i, i] += arb(n)
            R = arb_mat([[v] for v in rhs])
            S = M.solve(R)
            return [S[i, 0] for i in range(NS)]
        M = mp.matrix(NS, NS)
        for i in range(NS):
            for j in range(NS):
                M[i, j] = -B0rows[i][j]
            M[i, i] += n
        sol = mp.lu_solve(M, mp.matrix(rhs))
        return [sol[i] for i in range(NS)]


def frobenius_eval(be, STATE, NS, ENT, seed_vec, s_exit, nmax):
    """Sum the Frobenius series of the bounded solution at s_exit (|s_exit| < 1)."""
    serx = [(r, c, b_taylor_exact(pc, qc, nmax)) for (r, c, pc, qc) in ENT]
    B0rows = [[be.zero() for _ in range(NS)] for _ in range(NS)]
    for (r, c, t) in serx:
        if t[0] != 0:
            B0rows[r][c] += be.from_rat(t[0])
    ser = [(r, c, [be.from_rat(x) for x in t[1:]]) for (r, c, t) in serx]
    Y = [list(seed_vec)]
    for n in range(1, nmax + 1):
        rhs = [be.zero()] * NS
        for (r, c, t) in ser:
            kmax = min(n, len(t))
            acc = be.zero()
            for k in range(1, kmax + 1):
                acc += t[k - 1] * Y[n - k][c]
            rhs[r] += acc
        Y.append(be.trim_vec(be.solve_shifted(B0rows, n, rhs, NS)))
    sa = be.from_mpf(s_exit)
    acc = [be.zero()] * NS
    p = None
    for n in range(len(Y)):
        if p is None:
            p = sa * 0 + 1  # backend one
        for i in range(NS):
            acc[i] += Y[n][i] * p
        p = be.trim(p * sa)
    return be.trim_vec(acc), B0rows


def _shift_poly(be, coeffs, x0):
    c = list(coeffs)[::-1]
    n = len(c)
    out = []
    for j in range(n):
        for i in range(1, n - j):
            c[i] += c[i - 1] * x0
        out.append(c[n - 1 - j])
        c = c[:n - 1 - j]
    return out


def _series_rat(be, pc, qc, x0, order):
    ps, qs = _shift_poly(be, pc, x0), _shift_poly(be, qc, x0)
    c = [be.zero()] * (order + 1)
    q0 = qs[0]
    for n in range(order + 1):
        t = ps[n] if n < len(ps) else be.zero()
        for k in range(1, min(n, len(qs) - 1) + 1):
            t -= qs[k] * c[n - k]
        c[n] = be.trim(t / q0)
    return c


def make_transporter(be, ENT, NS, order):
    RATb = [(r, c, [be.from_rat(x) for x in pc], [be.from_rat(x) for x in qc])
            for (r, c, pc, qc) in ENT]
    sqrt2 = mp.sqrt(2)
    poles = [mp.mpf(0), mp.mpf(1), mp.mpf(2), 6 - 4 * sqrt2, 6 + 4 * sqrt2]

    def step(y0, x0m, hm):
        x0 = be.from_mpf(x0m)
        h = be.from_mpf(hm)
        tc = [(r, cc, _series_rat(be, pc, qc, x0, order)) for (r, cc, pc, qc) in RATb]
        a = [list(y0)]
        for n in range(order + 1):
            ssum = [be.zero()] * NS
            for (r, cc, t) in tc:
                acc = be.zero()
                for k in range(n + 1):
                    acc += t[k] * a[n - k][cc]
                ssum[r] += acc
            a.append(be.trim_vec([v / (n + 1) for v in ssum]))
        y = [be.zero()] * NS
        hp = h * 0 + 1
        for n in range(order + 2):
            for i in range(NS):
                y[i] += a[n][i] * hp
            hp = be.trim(hp * h)
        return be.trim_vec(y)

    def transport(y0, x0, x1, safety=mp.mpf("0.25")):
        y, t, x1 = list(y0), mp.mpf(x0), mp.mpf(x1)
        dirn = 1 if x1 > t else -1
        while abs(x1 - t) > mp.mpf(10) ** (-mp.mp.dps + 8):
            r = min(abs(t - p) for p in poles)
            h = dirn * min(safety * r, abs(x1 - t))
            y = step(y, t, h)
            t += h
        return y

    return transport


# ---------------------------------------------------------------------------
# 4. Full pipeline at current mp.dps: derive the 42-component seed at s = -2
# ---------------------------------------------------------------------------
def run_pipeline(mutate=None, use_flint=True):
    """Returns dict: {'seed_m2': {(i,K): mpf}, 'N_kite': mpf, 'checks': ..., 'timings': ...}"""
    t0 = time.time()
    be = Backend(use_flint)
    be.setup()
    STATE, IDX, NS, ENT = build_graded()
    seedd = closed_form_seed(mutate=mutate)
    seq = [be.from_mpf(seedd[i][K]) for (i, K) in STATE]
    # runtime structural checks (computed, not stored)
    checks = {}
    checks["xi112_is_m8G"] = float(-mp.log10(abs(_xi(mp.mpf(1), mp.mpf(1), mp.mpf(2))
                                                 + 8 * mp.catalan) + mp.mpf(10) ** (-mp.mp.dps)))
    V22 = V_vacuum(0, 0, 5)
    with mp.extradps(20):
        # eps^2 V(0,0,M) = -M^(1-2e) G(1+e)G(1-e)^2 G(1+2e) / (2(2e-1)G(2-e))  [e-analytic]
        ex = mp.taylor(lambda e: -mp.power(5, 1 - 2 * e) * mp.gamma(1 + e) * mp.gamma(1 - e) ** 2
                       * mp.gamma(1 + 2 * e) / (2 * (2 * e - 1) * mp.gamma(2 - e)),
                       0, 2)
    checks["V00M_vs_Gamma"] = float(min(-mp.log10(abs(V22[k] - ex[k + 2])
                                                  + mp.mpf(10) ** (-mp.mp.dps)) for k in (-2, -1, 0)))
    # eps^-1 cancellation of the top seed (kite finiteness, numeric witness)
    V010 = V_vacuum(0, 1, 0); V110 = V_vacuum(1, 1, 0)
    V012 = V_vacuum(0, 1, 2); V112 = V_vacuum(1, 1, 2)
    checks["top_seed_epsm1_cancel"] = float(-mp.log10(
        abs(V112[-1] - V012[-1] - V110[-1] + V010[-1]) + mp.mpf(10) ** (-mp.mp.dps)))
    t_build = time.time() - t0

    nmax = mp.mp.dps + 30
    s_exit = mp.mpf(-1) / 10
    t1 = time.time()
    yexit_b, B0rows = frobenius_eval(be, STATE, NS, ENT, seq, s_exit, nmax)
    # kernel membership |B0 y0| (structural check of the seed)
    r = [sum((B0rows[i][j] * seq[j] for j in range(NS)), be.zero()) for i in range(NS)]
    checks["B0y0_max"] = float(max(abs(be.to_mpf(x)) for x in r))
    t_frob = time.time() - t1

    t2 = time.time()
    # per-step truncation ~ safety^order must clear working precision:
    # log10(1/0.25) = 0.602 digits/order unit; 1.7 x dps gives ~2.4x margin
    order = max(60, int(1.7 * mp.mp.dps) + 20)
    transport = make_transporter(be, ENT, NS, order)
    ym2 = transport(yexit_b, s_exit, mp.mpf(-2))
    t_tr2 = time.time() - t2

    seed_m2 = {(i, K): be.to_mpf(ym2[IDX[(i, K)]]) for (i, K) in STATE}
    NK = -8 * seed_m2[(13, 0)]
    timings = {"build": t_build, "frobenius": t_frob, "to_-2": t_tr2,
               "total": time.time() - t0}
    return {"seed_m2": seed_m2, "N_kite": NK, "checks": checks, "timings": timings,
            "backend": "flint-arb" if be.flint else "mpmath"}


def digits_agree(a, ref):
    """Agreement digits vs a reference; ref == 0 (or absent) -> absolute digits
    (the component is exactly zero analytically; report -log10|computed|)."""
    a = mp.mpf(a)
    ref = mp.mpf(ref) if ref is not None else mp.mpf(0)
    if ref == 0:
        if a == 0:
            return float(mp.mp.dps)
        return float(-mp.log10(abs(a)))
    if a == ref:
        return float(mp.mp.dps)
    return float(-mp.log10(abs((a - ref) / ref)))


def emit_json(path, seed_m2, dps):
    """Write the derived seed in the kite-boundary-sm2.json schema (orders
    eps^-2..eps^0). Components below 10^-(dps-15) are analytically zero
    (kite finiteness / eps^-2 of the doubled-ISP master at s=-2) and omitted,
    mirroring the stored file."""
    thr = mp.mpf(10) ** (-(dps - 15))
    boundary = {}
    for (i, K), v in sorted(seed_m2.items()):
        if abs(v) < thr:
            continue
        boundary.setdefault(str(i), {})[str(K)] = mp.nstr(v, dps)
    out = {
        "_provenance": ("DERIVED boundary vector at s=-2 (eps^-2..eps^0), computed by "
                        "kite-boundary.py: closed-form two-loop vacuum seed at s=0 "
                        "(tadpole/sunset ring, xi(1,1,2)=-8*Catalan) + bounded-branch "
                        "Frobenius at the regular singular point s=0 + exact-DE Taylor "
                        "transport to s=-2. NO AMFlow input; gate reference is "
                        "kite-boundary-sm2.json (held out)."),
        "s": -2, "dps": dps,
        "masters": [[list(m)] for m in CONN["masters"]],
        "boundary": boundary,
    }
    with open(path, "w") as f:
        json.dump(out, f, indent=1)


def main():
    ap = argparse.ArgumentParser(
        description="Unequal-mass kite: DERIVE the s=-2 boundary seed (de-AMFlowed)")
    ap.add_argument("--dps", type=int, default=100)
    ap.add_argument("--check", action="store_true", help="two-precision rule: rerun at dps+60")
    ap.add_argument("--emit", metavar="PATH",
                    help="write derived seed JSON (kite-boundary-sm2.json schema)")
    ap.add_argument("--mutate", action="store_true",
                    help="mutation test: perturb Catalan-bearing seed comp by 1e-30")
    ap.add_argument("--no-flint", action="store_true")
    args = ap.parse_args()

    mp.mp.dps = args.dps + 25          # working guard; reported digits vs args.dps context
    mutate = (13, 0, "1e-30") if args.mutate else None

    print("Unequal-mass kite s=-2 boundary seed -- DERIVED "
          "(vacuum ring + s=0 regularity + exact-DE transport); AMFlow = held-out gate only")
    print(f"dps={args.dps} (+25 guard) backend="
          f"{'mpmath' if (args.no_flint or not HAVE_FLINT) else 'flint-arb'}"
          f"{'  [MUTATED SEED]' if mutate else ''}\n")
    r = run_pipeline(mutate=mutate, use_flint=not args.no_flint)

    print("runtime structural checks (computed now, nothing stored):")
    print(f"  xi(1,1,2) + 8*Catalan == 0        : {r['checks']['xi112_is_m8G']:.1f} d")
    print(f"  V(0,0,M) vs exact Gamma form      : {r['checks']['V00M_vs_Gamma']:.1f} d")
    print(f"  top-seed eps^-1 cancellation      : {r['checks']['top_seed_epsm1_cancel']:.1f} d")
    print(f"  |B0 y0| (seed in ker B0)          : {r['checks']['B0y0_max']:.1e}")
    print()

    ref, ref_dps = load_gate()
    if ref is None:
        print(f"[gate SKIPPED: {os.path.basename(GATE_JSON)} not found; derivation only]\n")
    else:
        print(f"held-out gate: {os.path.basename(GATE_JSON)} "
              f"({ref_dps} d AMFlow boundary vector; never used above)\n")
    print(f"{'master':>7} {'eps':>4} | {'derived value at s=-2 (computed now)':>44} | gate")
    worst = None
    for (i, K) in sorted(r["seed_m2"]):
        v = r["seed_m2"][(i, K)]
        if ref is None:
            print(f"{i:>7} {K:>4} | {mp.nstr(v, 40):>44} |   --")
            continue
        rv = ref.get((i, K))
        d = digits_agree(v, rv)
        worst = d if worst is None else min(worst, d)
        tag = "" if rv is not None else "  (ref absent -> 0; absolute)"
        print(f"{i:>7} {K:>4} | {mp.nstr(v, 40):>44} | {d:6.1f} d{tag}")
    NK = r["N_kite"]
    print(f"\nN_kite = 4s J^0_top|_(s=-2) = -8*M13[eps^0](-2) = {mp.nstr(NK, min(50, args.dps))}")
    if ref is not None:
        dN = digits_agree(NK, -8 * mp.mpf(ref[(13, 0)]))
        print(f"  vs held-out ref (-8 x stored M13[eps^0]): {dN:.1f} digits")
        print(f"\nGATE (worst of 42 components): {worst:.1f} digits vs {ref_dps} d AMFlow "
              f"(cap = min(working dps {mp.mp.dps}, ref {ref_dps}))")
    tt = r["timings"]
    print(f"\ntimings [s]: build {tt['build']:.1f}, frobenius {tt['frobenius']:.1f}, "
          f"exit->-2 {tt['to_-2']:.1f}, TOTAL {tt['total']:.1f}  (backend {r['backend']})")

    if args.emit:
        emit_json(args.emit, r["seed_m2"], args.dps)
        print(f"\nderived seed written: {args.emit}  (schema of kite-boundary-sm2.json, "
              f"orders eps^-2..eps^0, {args.dps} d)")

    if args.check:
        print(f"\n--check: rerunning at dps {args.dps + 60} ...")
        seed1 = dict(r["seed_m2"])
        NK1 = r["N_kite"]
        mp.mp.dps = args.dps + 60 + 25
        r2 = run_pipeline(mutate=mutate, use_flint=not args.no_flint)
        tgt = args.dps - 10
        dNk = digits_agree(NK1, r2["N_kite"])
        ok = dNk >= tgt
        dmin = None
        zero_thr = mp.mpf(10) ** (-(args.dps - 15))   # analytically-zero comps
        for k in seed1:
            ref2 = r2["seed_m2"][k]
            dk = digits_agree(seed1[k], ref2 if abs(ref2) >= zero_thr else None)
            dmin = dk if dmin is None else min(dmin, dk)
            ok = ok and dk >= tgt
        print(f"--check result: N_kite stable to {dNk:.1f} d; worst seed component "
              f"stable to {dmin:.1f} d (target >= {tgt})")
        print(f"--check verdict: {'PASS' if ok else 'FAIL'}")


if __name__ == "__main__":
    main()
