#!/usr/bin/env python3
"""c3-dbox-identities.py -- the two exact identities behind the C3 central-rung-mass double box (row 7), verified at runtime
with a script-emitted receipt: the restriction of the 123-term weight-four t-sector table to the slice s = -1, and a rational
parametrisation of two square roots that an early maximal-cut scan returned (a record about those roots; they are not letters of
this family and play no part in the evaluator's result).  Companion of c3-dbox-evaluate.py in this directory (the evaluator
and c3-dbox-2var.json are read from the script's own directory and checked against their sha256 pins below; the identity
script never changes the evaluator's value path).

  IDENTITY A (a rational parametrisation of two square roots): r1 = sqrt(s(s+4)), r2 = sqrt(s(s-8)) (m = 1), the two roots an
  early loop-by-loop maximal-cut scan returned for this family, are rationalized simultaneously by one rational parameter u.  They
  belong to no master of the family (Schwanemann-Weinzierl arXiv:2412.07522 Table 1, topology C: no square root; no column of
  the weight-3/4 tables of c3-dbox-2var.json uses them), so the identity concerns the roots alone and plays no part in the
  evaluator's result; it is kept as an exact-arithmetic record.  The map is DERIVED from the roots:
  r1 = s*alpha, r2 = s*beta puts (alpha, beta) on the conic 2 alpha^2 + beta^2 = 3 through (1, 1);
  the pencil beta - 1 = u (alpha - 1) gives
      alpha(u) = (u^2 - 2u - 2)/(u^2 + 2),   s(u) = 4/(alpha^2 - 1) = -(u^2+2)^2/(u (u-1) (u+2)),
      r1(u) = s alpha = -(u^2+2)(u^2-2u-2)/(u (u-1) (u+2)),   r2(u) = s beta = (u^2+2)(u^2+4u-2)/(u (u-1) (u+2)),
      inverse u = (r2 - s)/(r1 - s).
  Checked EXACTLY (sympy cancel over Q(u)): r1^2 - s(s+4) == 0, r2^2 - s(s-8) == 0, and the inverse composes to u
  (birationality).  Control: a planted wrong coefficient does NOT cancel.  Numeric control: the residual at a sample point
  at 50 digits.  The map printed is one member of a Moebius family of parametrisations of the conic; any other differs by a
  Moebius transformation of u.

  IDENTITY B (the slice restriction of the 123-term table): the two-variable weight-four t-sector of c3-dbox-2var.json
  (w4t_2var, 123 columns) restricted to s = -1 must reproduce the 44-word slice form the evaluator uses on that slice
  (its W4_PURE / W4_LOG2 tables and the weight-two words of g4_slice), word for word and with exact rational arithmetic.
  At s = -1 the encoding's x-letters [0, -1, s, s/(1-s), -s^2] -> [0, -1, -1, -1/2, -1] merge onto the slice letters
  [0, -1, -1/2] (the fifth, -s^2, x-index 4, is addressed by no column; the evaluator transports the first four)
  (slice indices 0, 1, 2), y = -s -> 1, log y -> 0, and the y-words become constants.  With log(x)^k/k! = G(0^k; x) shuffled
  into the x-words, the 123 columns must reproduce:
     (B1) the 26 integer-coefficient weight-4 slice words          <- the 38 x-weight-4 columns
     (B2) the 9 log2 x weight-3 words                             <- the 24 x-weight-3 columns (the 9 with a log y power vanish at y = 1)
     (B3) the 9 pi^2 / log^2(2) coefficients on the 6 weight-2 words <- the 34 x-weight-2 columns, using only
          G(0,1;1) = -Li2(1) = -zeta2, G(0,-1;1) = -Li2(-1) = zeta2/2, G(-1,-1;1) = log^2(2)/2 (all three
          by definition: G(0,a;y) = -Li2(y/a), G(a,a;y) = log^2(1 - y/a)/2)
     and every merged word that is NOT a slice word must cancel to zero.
  Left NUMERIC: the x-weight-1 block (the zeta3 / log^3 2 slice words) and the constant need the weight-3/4 alternating
  values G(w;1), w over {0,1,-1}, which this script does not reduce exactly; they are controlled by the near-slice evaluation
  of the two-variable form against the slice form at s = -1 -+ delta (LIVE mode).
  Controls: a planted wrong coefficient (does not cancel); in LIVE mode the fixed-s t-differences of the weight-four data
  minus the t-sector over the evaluator's 21 comparison points at two precisions (the t-independence certification re-run).

Modes: --mode DRY runs the exact identities only (about one second); --mode LIVE adds the two numeric controls (about ten
minutes: the near-slice evaluation at dps 60 and the t-differences at dps 60 and 130 through the evaluator's own transport).
The receipt is written O_EXCL under --out (default: a fresh directory c3-dbox-identities-out_<stamp> under the current
working directory; a directory inside this bundle is refused by name).  Exit 0 when both identities are EXACT ZERO and the transcription guard holds,
3 otherwise, 2 on a pin mismatch, a missing input or a receipt directory inside the bundle (refused by name).  Optional record cross-checks (--record-json, the
two-variable tables' twin; --record-tex, the printed section's source; --record-plan) are reported by sha256 when the file is
given and readable and as 'not read' otherwise; they never decide the exit code.

Every stamp is read from `date -u` inside this process; every figure printed in the receipt is computed here and read back
by key; nothing is typed.
"""
import sys, os, json, hashlib, subprocess, resource, itertools, argparse, collections, math, re, time
from fractions import Fraction as Fr

def stamp():
    return subprocess.run(['date', '-u', '+%Y-%m-%dT%H:%M:%SZ'], capture_output=True, text=True, check=True).stdout.strip()

def sha256(p):
    h = hashlib.sha256()
    with open(p, 'rb') as f:
        for chunk in iter(lambda: f.read(1 << 20), b''):
            h.update(chunk)
    return h.hexdigest()

# the sha256 pins of the two inputs read from this script's own directory (the evaluator: the 2026-09-11 succession beside
# this file, whose _xylet carries the four x-letters the tables address; the data file: unchanged since 2026-09-06)
SERVED_EVAL_SHA = '9b75e668b7971c42ed2fe243449a7088e7ed21e56a128f3d6404448d05993e5e'
SERVED_JSON_SHA = '179a66360d498a97a1debec67c4e5ddc4b3b4075e574a7193be41584ab26a4da'

# ----------------------------------------------------------------------------------------------
# IDENTITY A — the birational map (sympy, exact over Q(u))
# ----------------------------------------------------------------------------------------------
def identity_A():
    import sympy as sp
    u = sp.symbols('u')
    alpha = (u**2 - 2*u - 2) / (u**2 + 2)
    beta = 1 + u * (alpha - 1)
    S = sp.cancel(4 / (alpha**2 - 1))
    P = sp.cancel(S * alpha)          # the rational image of r1 = sqrt(s(s+4))
    Q = sp.cancel(S * beta)           # the rational image of r2 = sqrt(s(s-8))
    e1 = sp.cancel(P**2 - S * (S + 4))
    e2 = sp.cancel(Q**2 - S * (S - 8))
    # birationality: the inverse u = (r2 - s)/(r1 - s) composed with the map
    uinv = sp.cancel((Q - S) / (P - S))
    e3 = sp.cancel(uinv - u)
    # term counts 'by the tool': numerator terms of the unexpanded differences before cancel
    n_before_1 = len(sp.Add.make_args(sp.expand(sp.numer(sp.together(P**2 - S*(S+4))))))
    n_before_2 = len(sp.Add.make_args(sp.expand(sp.numer(sp.together(Q**2 - S*(S-8))))))
    # control: a planted wrong coefficient (-2u -> -u in alpha) must NOT cancel
    alpha_bad = (u**2 - u - 2) / (u**2 + 2)
    beta_bad = 1 + u * (alpha_bad - 1)
    S_bad = sp.cancel(4 / (alpha_bad**2 - 1))
    e2_bad = sp.cancel((S_bad*beta_bad)**2 - S_bad*(S_bad - 8))
    # numeric control at a sample point where both roots are real (s >= 8): u = 1/2 -> s = 81/10
    import mpmath as mpm
    mpm.mp.dps = 50
    u0 = sp.Rational(1, 2)
    s0 = S.subs(u, u0); p0 = P.subs(u, u0); q0 = Q.subs(u, u0)
    s0m = mpm.mpf(int(s0.p)) / int(s0.q)
    r1m = mpm.sqrt(s0m * (s0m + 4)); r2m = mpm.sqrt(s0m * (s0m - 8))
    p0m = mpm.mpf(int(p0.p)) / int(p0.q); q0m = mpm.mpf(int(q0.p)) / int(q0.q)
    res1 = abs(abs(r1m) - abs(p0m)) / abs(r1m); res2 = abs(abs(r2m) - abs(q0m)) / abs(r2m)
    return {
        'map': {'alpha(u)': str(alpha), 'beta(u)': str(beta), 's(u)': str(sp.factor(S)),
                'r1(u) = s*alpha': str(sp.factor(P)), 'r2(u) = s*beta': str(sp.factor(Q)),
                'inverse u(s,r1,r2)': '(r2 - s)/(r1 - s)'},
        'identity_r1': {'expression': 'r1(u)^2 - s(u)*(s(u)+4)', 'numerator_terms_before_cancel': n_before_1,
                        'after_cancel': str(e1), 'exact_zero': bool(e1 == 0)},
        'identity_r2': {'expression': 'r2(u)^2 - s(u)*(s(u)-8)', 'numerator_terms_before_cancel': n_before_2,
                        'after_cancel': str(e2), 'exact_zero': bool(e2 == 0)},
        'birational': {'expression': '(r2(u) - s(u))/(r1(u) - s(u)) - u', 'after_cancel': str(e3), 'exact_zero': bool(e3 == 0)},
        'control_planted': {'planted': 'alpha -> (u^2 - u - 2)/(u^2 + 2)', 'residual_after_cancel': str(e2_bad),
                            'exact_zero': bool(e2_bad == 0), 'verdict': 'FAIL as required' if e2_bad != 0 else 'CONTROL BROKEN'},
        'numeric_control': {'convention': '| |sqrt(s(s+c))| - |rational image| | / |sqrt| at u = 1/2 (s = ' + str(s0) + '), mpmath dps 50',
                            'residual_r1': mpm.nstr(res1, 5), 'residual_r2': mpm.nstr(res2, 5),
                            'served_figure': '~1e-40 (tex L47)', 'at_or_below_served': bool(res1 < mpm.mpf('1e-40') and res2 < mpm.mpf('1e-40'))},
        'verdict': 'EXACT ZERO' if (e1 == 0 and e2 == 0 and e3 == 0) else 'NOT CLOSED',
    }

# ----------------------------------------------------------------------------------------------
# IDENTITY B — the slice restriction of the 123-term table, exact
# ----------------------------------------------------------------------------------------------
LMAP = {0: 0, 1: 1, 2: 1, 3: 2, 4: 1}      # 2-var x-letters [0,-1,s,s/(1-s),-s^2] at s=-1 -> slice [0,-1,-1/2]

def shuffle(u, v):
    """Multiset of shuffles of the tuples u and v (Counter)."""
    out = collections.Counter()
    if not u: out[tuple(v)] += 1; return out
    if not v: out[tuple(u)] += 1; return out
    for w, n in shuffle(u[1:], v).items(): out[(u[0],) + w] += n
    for w, n in shuffle(u, v[1:]).items(): out[(v[0],) + w] += n
    return out

def x_part_on_slice(kt, wx):
    """log(x)^kt/kt! * G(wx; x) = G(0^kt;x) sh G(wx;x), letters merged onto the slice: Counter of slice words."""
    out = collections.Counter()
    for w, n in shuffle((0,) * kt, tuple(wx)).items():
        out[tuple(LMAP[a] for a in w)] += n
    return out

# y-words at y = 1 that are exact by definition (weight <= 2), as {tag: Fraction}; tags: '1', 'L' (log 2),
# 'z2' (zeta2), 'L2' (log^2 2).  Constants tags multiply: const 'z2' -> zeta2, 'z3' -> zeta3.
Y_AT_ONE = {
    (): {'1': Fr(1)},
    (2,): {'L': Fr(1)},                 # G(-1; 1) = log(1 + 1) = log 2
    (0, 1): {'z2': Fr(-1)},             # G(0, 1; 1) = -Li2(1) = -zeta2
    (0, 2): {'z2': Fr(1, 2)},           # G(0, -1; 1) = -Li2(-1) = zeta2/2
    (2, 2): {'L2': Fr(1, 2)},           # G(-1, -1; 1) = log^2(2)/2
}

def restrict_block(table, xweight):
    """Restrict the columns of x-weight `xweight` to s = -1.  Returns (merged {(word, tag): Fraction},
    n_columns, n_words_before_merge, n_nonexact_columns, n_logy_columns)."""
    merged = collections.defaultdict(Fr)
    ncol = 0; nbefore = 0; nonexact = 0; nlogy = 0
    for c, kt, wx, ks, wy, const in table:
        if kt + len(wx) != xweight: continue
        ncol += 1
        if ks > 0:
            nlogy += 1; continue          # log(y)^ks -> 0 at y = 1 (times a log-power divergence at most: still 0)
        wy = tuple(wy)
        if wy not in Y_AT_ONE:
            nonexact += 1; continue      # weight-3 y-words: NOT exact here (see the docstring)
        xp = x_part_on_slice(kt, wx)
        for w, n in xp.items():
            nbefore += n
            for tag, val in Y_AT_ONE[wy].items():
                t = tag if const == '1' else (const if tag == '1' else tag + '*' + const)
                merged[(w, t)] += Fr(c) * n * val
    return merged, ncol, nbefore, nonexact, nlogy

def compare(merged, served):
    """merged and served: {(word, tag): Fraction}. Returns the residual dict and the cancelled-word list."""
    keys = set(merged) | set(served)
    resid = {k: merged.get(k, Fr(0)) - served.get(k, Fr(0)) for k in keys}
    resid = {k: v for k, v in resid.items() if v != 0}
    cancelled = sorted(k for k, v in merged.items() if v == 0)
    return resid, cancelled

def fmt_key(k):
    return '%s|%s' % (','.join(map(str, k[0])), k[1])

def identity_B(mod, D, mode):
    """mod: the served evaluator module (stamped copy); D: the served json."""
    T4 = [(Fr(c), kt, tuple(wx), ks, tuple(wy), const) for c, kt, wx, ks, wy, const in D['w4t_2var']]
    T3 = [(Fr(c), kt, tuple(wx), ks, tuple(wy), const) for c, kt, wx, ks, wy, const in D['w3_2var']]
    out = {}
    # ---- the served slice targets, read from the served module's own tables --------------------------
    W4_PURE = {(tuple(w), '1'): Fr(c) for c, w in mod.W4_PURE}
    W4_LOG2 = {(tuple(w), 'L'): Fr(c) for c, w in mod.W4_LOG2}
    # weight-2 slice words with pi^2 / log^2 2 coefficients, transcribed from g4_slice (served L251-262),
    # pi^2 = 6 zeta2; verified below against the served g4_slice numerically before use.
    W4_W2 = {((0, 1), 'z2'): Fr(16, 3) * 6, ((0, 2), 'z2'): Fr(-4) * 6, ((0, 2), 'L2'): Fr(24),
             ((1, 1), 'z2'): Fr(-10, 3) * 6, ((1, 2), 'z2'): Fr(4, 3) * 6, ((1, 2), 'L2'): Fr(-8),
             ((2, 1), 'z2'): Fr(-8, 3) * 6, ((2, 2), 'z2'): Fr(8, 3) * 6, ((2, 2), 'L2'): Fr(-16)}
    W3_PURE = {(tuple(w), '1'): Fr(c) for c, w in mod.W3_PURE}
    W3_LOG2 = {(tuple(w), 'L'): Fr(c) for c, w in mod.W3_LOG2}
    W3_W1 = {((1,), 'z2'): Fr(5, 3) * 6, ((2,), 'z2'): Fr(-2, 3) * 6, ((2,), 'L2'): Fr(4)}
    # transcription guard: rebuild g4_slice / g3_slice from the tables + these dicts and compare to the served functions
    from mpmath import mp, mpf, log, pi, zeta, polylog
    mp.dps = 30
    G = mod.gpl_transport([mpf(1) / 3])[mpf(1) / 3]     # x = -t = 1/3
    L2, Z2, Z3 = log(2), pi**2 / 6, zeta(3)
    cv = {'1': mpf(1), 'L': L2, 'z2': Z2, 'L2': L2**2}
    def rebuild(pure, log2w, mid, extra):
        v = mpf(0)
        for (w, tag), c in list(pure.items()) + list(log2w.items()) + list(mid.items()):
            v += mpf(c.numerator) / c.denominator * cv[tag] * G[w]
        return v + extra
    g4_re = rebuild(W4_PURE, W4_LOG2, W4_W2, mpf(140) / 3 * Z3 * G[(1,)] + (-8 * Z3 - mpf(16) / 3 * L2**3) * G[(2,)]
                    - pi**4 / 5 - 14 * Z3 * L2 + mpf(2) / 3 * pi**2 * L2**2 - mpf(2) / 3 * L2**4 - 16 * polylog(4, mpf(1) / 2))
    g3_re = rebuild(W3_PURE, W3_LOG2, W3_W1, -mpf(50) / 3 * Z3)
    guard4 = abs(g4_re - mod.g4_slice(G)); guard3 = abs(g3_re - mod.g3_slice(G))
    out['transcription_guard'] = {'point': 'x = -t = 1/3, dps 30', '|g4_rebuilt - served g4_slice|': mp.nstr(guard4, 3),
                                  '|g3_rebuilt - served g3_slice|': mp.nstr(guard3, 3),
                                  'ok': bool(guard4 < mpf('1e-25') and guard3 < mpf('1e-25'))}
    # ---- w4: the three exact blocks -------------------------------------------------------------------
    blocks = {}
    for name, xw, served in (('B1_w4_pure_words', 4, W4_PURE), ('B2_w4_log2_words', 3, W4_LOG2), ('B3_w4_weight2_pi2_log2sq_words', 2, W4_W2)):
        merged, ncol, nbefore, nonexact, nlogy = restrict_block(T4, xw)
        resid, cancelled = compare(merged, served)
        nz = {k: v for k, v in merged.items() if v != 0}
        blocks[name] = {
            'columns_in_block': ncol, 'log_y_columns_vanishing_at_y1': nlogy, 'nonexact_columns_skipped': nonexact,
            'words_before_merge': nbefore, 'distinct_words_after_merge': len(merged), 'nonzero_words_after_merge': len(nz),
            'words_cancelled_to_zero': len(cancelled), 'cancelled_words': [fmt_key(k) for k in cancelled],
            'served_words': len(served), 'residual_terms': len(resid),
            'residual': {fmt_key(k): str(v) for k, v in sorted(resid.items())},
            'exact_zero': len(resid) == 0}
    out['w4_blocks'] = blocks
    # the not-exact remainder of the 123 columns, by count
    rem = collections.Counter()
    for c, kt, wx, ks, wy, const in T4:
        xw = kt + len(wx)
        if xw == 1: rem['x_weight_1_columns_numeric_only'] += 1
    out['w4_columns_accounted'] = {'total_columns': len(T4), 'exact_blocks': sum(b['columns_in_block'] for b in blocks.values()),
                                   **rem}
    # the planted control: one coefficient of the w4 pure block moved by +1 must NOT cancel
    Tbad = list(T4)
    for i, col in enumerate(Tbad):
        if col[1] + len(col[2]) == 4:
            Tbad[i] = (col[0] + 1,) + col[1:]; planted = {'column_index': i, 'column': [str(col[0])] + [col[1], list(col[2]), col[3], list(col[4]), col[5]]}
            break
    merged_bad, *_ = restrict_block(Tbad, 4)
    resid_bad, _ = compare(merged_bad, W4_PURE)
    out['control_planted'] = {**planted, 'residual_terms': len(resid_bad), 'residual': {fmt_key(k): str(v) for k, v in sorted(resid_bad.items())},
                              'verdict': 'FAIL as required' if resid_bad else 'CONTROL BROKEN'}
    # ---- w3: the same three blocks as the positive control ---------------------------------------------
    b3 = {}
    for name, xw, served in (('w3_pure_words', 3, W3_PURE), ('w3_log2_words', 2, W3_LOG2), ('w3_weight1_pi2_log2sq_words', 1, W3_W1)):
        merged, ncol, nbefore, nonexact, nlogy = restrict_block(T3, xw)
        resid, cancelled = compare(merged, served)
        b3[name] = {'columns_in_block': ncol, 'words_before_merge': nbefore, 'distinct_words_after_merge': len(merged),
                    'words_cancelled_to_zero': len(cancelled), 'served_words': len(served), 'residual_terms': len(resid),
                    'residual': {fmt_key(k): str(v) for k, v in sorted(resid.items())}, 'exact_zero': len(resid) == 0}
    out['w3_control_blocks'] = b3
    all_exact = all(b['exact_zero'] for b in blocks.values()) and all(b['exact_zero'] for b in b3.values())
    out['verdict_exact_blocks'] = 'EXACT ZERO (w4 B1+B2+B3, w3 control)' if all_exact else 'NOT CLOSED'
    if mode == 'DRY':
        return out
    # ---- numeric control 1: the full slice identity near s = -1 (covers the x-weight-1 block and the constant) --
    from mpmath import mpc, fabs
    near = {}
    for dps, delta in ((60, '1e-20'),):
        mp.dps = dps
        t0 = mpf(-1) / 2
        gs = mod.evaluate(t0, dps)                # slice form (J coefficients)
        rows = []
        for sgn in ('+', '-'):
            s0 = mpf(-1) + mpf(delta) * (1 if sgn == '+' else -1)
            J2, imax = mod.evaluate2(s0, t0, dps)  # 2-var form
            rows.append({'s': '-1 ' + sgn + ' ' + delta, 'rel_diff_eps0(w4)': mp.nstr(fabs(J2[4] - gs[4]) / fabs(gs[4]), 3),
                         'rel_diff_eps-1(w3)': mp.nstr(fabs(J2[3] - gs[3]) / fabs(gs[3]), 3), 'imax': mp.nstr(imax, 3)})
        near['dps_%d' % dps] = {'t': '-1/2', 'delta': delta, 'rows': rows,
                                'expected_scale': 'O(delta log^3 delta) ~ 1e-16 from the s -> -1 limit itself'}
    out['numeric_control_near_slice'] = near
    # ---- numeric control 2: the served t-independence check, fixed-s t-differences of (g4_data - T) at two precisions --
    tind = {}
    for dps in (60, 130):
        mp.dps = dps
        cv2 = {'1': mpf(1), 'z2': pi**2 / 6, 'z3': zeta(3), 'z4': pi**4 / 90}
        by_s = collections.defaultdict(list)
        for p in mod.GATE_FEED:
            by_s[p['s']].append(p)
        rows = []; worst = None
        for sv, pts in sorted(by_s.items(), key=lambda kv: mpf(mod._parse_mpf(kv[0]))):
            if len(pts) < 2: continue
            s0 = mod._parse_mpf(sv)
            vals = []
            for p in pts:
                t0 = mod._parse_mpf(p['t'])
                xlet, ylet = mod._xylet(s0)
                gx = mod.gpl_transport_2var(xlet, 4, [-t0])[0]
                gy = mod._gy_transport(ylet, -s0)
                Tval = mod._term_sum(mod.W4T_2VAR, gx, gy, log(-t0), log(-s0), cv2)
                g4d = mod._parse_mpf(p['g4'])
                vals.append((p['t'], g4d, Tval, g4d - Tval))
            vals.sort(key=lambda v: mpf(mod._parse_mpf(v[0])))
            for (ta, ga, Ta, Ra), (tb, gb, Tb, Rb) in zip(vals, vals[1:]):
                dR = fabs(Ra - Rb) / max(fabs(ga), fabs(gb))
                dig = float(-mp.log10(dR)) if dR > 0 else float('inf')
                rows.append({'s': sv, 't_pair': [ta, tb], 'rel_residual': mp.nstr(dR, 3), 'digits': round(dig, 2)})
                worst = dig if worst is None else min(worst, dig)
        tind['dps_%d' % dps] = {'n_differences': len(rows), 'worst_digits': round(worst, 2) if worst is not None else None,
                                'served_oracle_string_digits': 110, 'rows': rows}
    out['numeric_control_t_independence'] = tind
    return out

# ----------------------------------------------------------------------------------------------
# the optional record cross-checks: reported, never deciding
# ----------------------------------------------------------------------------------------------
def record_cross_check(path, D=None):
    """A record file named on the command line: its sha256 when readable, 'not read' otherwise.  For the two-variable
    tables' twin (--record-json) also whether its three column tables equal the bundle's, as parsed objects."""
    if not path:
        return {'status': 'not read (no path given)'}
    if not os.path.isfile(path):
        return {'status': 'not read (absent)', 'file': os.path.basename(path)}
    out = {'status': 'read', 'file': os.path.basename(path), 'sha256': sha256(path)}
    if D is not None:
        try:
            R = json.load(open(path))
            out['tables_identical_to_bundle'] = all(D[k] == R.get(k) for k in ('w3_2var', 'w4t_2var', 'w4_purey_exact'))
        except (ValueError, KeyError) as e:
            out['tables_identical_to_bundle'] = None; out['parse'] = type(e).__name__
    return out

# ----------------------------------------------------------------------------------------------
def main():
    here = os.path.dirname(os.path.abspath(__file__))
    ap = argparse.ArgumentParser(description='the two exact identities of the C3 double box (row 7): the birational map and the slice restriction of the 123-term table; a script-emitted receipt')
    ap.add_argument('--mode', required=True, choices=['DRY', 'LIVE'], help='DRY: the exact identities only (seconds); LIVE: plus the two numeric controls (about ten minutes)')
    ap.add_argument('--out', default=None, help='receipt directory (default: a fresh c3-dbox-identities-out_<stamp> under the current working directory)')
    ap.add_argument('--stamp', default=None, help='receipt stamp (default: date -u, compact form)')
    ap.add_argument('--served-dir', default=here, help='directory holding c3-dbox-evaluate.py and c3-dbox-2var.json (default: this script\'s directory)')
    ap.add_argument('--record-json', default=None, help='optional: a twin of the two-variable tables to compare with the bundle (reported, never deciding)')
    ap.add_argument('--record-tex', default=None, help='optional: the printed section\'s source file, reported by sha256')
    ap.add_argument('--record-plan', default=None, help='optional: a record file, reported by sha256')
    a = ap.parse_args()
    t_start = stamp(); wall0 = time.monotonic()
    st = a.stamp or subprocess.run(['date', '-u', '+%Y%m%dT%H%M%SZ'], capture_output=True, text=True, check=True).stdout.strip()
    ev = os.path.join(a.served_dir, 'c3-dbox-evaluate.py'); js = os.path.join(a.served_dir, 'c3-dbox-2var.json')
    for p, want, what in ((ev, SERVED_EVAL_SHA, 'evaluator'), (js, SERVED_JSON_SHA, 'data file')):
        if not os.path.isfile(p):
            print('REFUSED: the %s %s is absent' % (what, p)); return 2
        got = sha256(p)
        if got != want:
            print('REFUSED: the %s %s has sha256 %s, the pin in this script is %s' % (what, os.path.basename(p), got, want)); return 2
    sys.dont_write_bytecode = True     # the evaluator is loaded from the bundle; no cache directory is written beside it
    import importlib.util
    spec = importlib.util.spec_from_file_location('c3eval', ev); mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
    D = json.load(open(js))
    A = identity_A()
    B = identity_B(mod, D, a.mode)
    wall = time.monotonic() - wall0
    ru = resource.getrusage(resource.RUSAGE_SELF)
    a_ok = A['verdict'] == 'EXACT ZERO'; b_ok = B['verdict_exact_blocks'].startswith('EXACT ZERO')
    n_numeric = B['w4_columns_accounted'].get('x_weight_1_columns_numeric_only', 0)
    rec = {
        'receipt': 'C3_DBOX_EXACT_IDENTITIES', 'mode': a.mode, 'stamp_start_utc': t_start, 'stamp_end_utc': stamp(),
        'PRODUCER': {'script': os.path.basename(os.path.abspath(__file__)), 'sha256': sha256(os.path.abspath(__file__))},
        'inputs': {'evaluator': {'file': os.path.basename(ev), 'sha256': SERVED_EVAL_SHA},
                   'data': {'file': os.path.basename(js), 'sha256': SERVED_JSON_SHA}},
        'record_cross_checks': {'tables_twin': record_cross_check(a.record_json, D),
                                'printed_section_source': record_cross_check(a.record_tex),
                                'plan': record_cross_check(a.record_plan)},
        'identity_A_birational_map': A,
        'identity_B_slice_restriction_of_the_123_term_table': B,
        'verdict': {'A': A['verdict'], 'B_exact_blocks': B['verdict_exact_blocks'],
                    'B_numeric_only': 'x-weight-1 block (%d columns) and the constant: NUMERIC control only (weight-3/4 alternating MZV ring not implemented)' % n_numeric},
        'run': {'wall_s': round(wall, 2), 'maxrss_kb': ru.ru_maxrss, 'python': sys.version.split()[0]},
    }
    outdir = a.out or os.path.join(os.getcwd(), 'c3-dbox-identities-out_' + st)
    _o = os.path.realpath(outdir); _h = os.path.realpath(here)
    if _o == _h or _o.startswith(_h + os.sep):
        print('REFUSED: the receipt directory %s lies inside this bundle directory %s; give --out outside it' % (outdir, here)); return 2
    os.makedirs(outdir, exist_ok=True)
    jpath = os.path.join(outdir, 'c3-dbox-identities-run_%s_%s.json' % (a.mode, st))
    fd = os.open(jpath, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
    with os.fdopen(fd, 'w') as f: json.dump(rec, f, indent=1)
    print('RECEIPT', jpath); print('VERDICT', json.dumps(rec['verdict']))
    print('A', json.dumps({k: A[k] for k in ('identity_r1', 'identity_r2', 'birational', 'control_planted', 'numeric_control')}, indent=1))
    print('B blocks', json.dumps({k: {kk: vv for kk, vv in v.items() if kk != 'cancelled_words'} for k, v in B['w4_blocks'].items()}, indent=1))
    print('B control', json.dumps(B['control_planted'])); print('B w3', json.dumps(B['w3_control_blocks']))
    print('guard', json.dumps(B['transcription_guard']))
    for k in ('numeric_control_near_slice', 'numeric_control_t_independence'):
        if k in B: print(k, json.dumps({kk: {x: y for x, y in vv.items() if x != 'rows'} for kk, vv in B[k].items()}))
    print('record cross-checks', json.dumps(rec['record_cross_checks']))
    print('run', json.dumps(rec['run']))
    return 0 if (a_ok and b_ok and B['transcription_guard']['ok']) else 3

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