Jump to content

The Principia Machina/TPC-PY tools/tpm sandbox.py

From The Invariance Itself
Revision as of 15:12, 30 September 2026 by DeltaS Zero (talk | contribs) (New page: tpm_sandbox.py 0.2.0, the non-normative local UWC sandbox, byte for byte)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
# -*- coding: utf-8 -*-
"""
tpm_sandbox.py 0.2.0 --- a local UWC sandbox over a published TPC-PY
=====================================================================
NON-NORMATIVE. SANDBOX ONLY. NOT TPX. NOTHING HERE IS OF RECORD.

What it is: the thinnest runnable path through The Principia Machina. It plays
TPX's part on your own disk so that anyone, a person or an AI with a terminal,
can create a UWC and work its register before TPX exists. Every check it runs
is a function of TPC-PY, called unchanged; this file only sequences those
calls, stores the results and prints them. It runs only on a TPC-PY tree whose
digest it knows (KNOWN_TREES below) and says in every header whether that
version is declared or proposed.

Published on https://invarians.org as the page
"The Principia Machina/TPC-PY tools/tpm_sandbox.py"; how to use it:
"The Principia Machina/Hub/Outside AI".

Standard library only; Python 3.8 or later; Windows and Linux.

SET-UP: put this file in a folder that holds tpc/, the TPC-PY tree that
verify_tpc_py.py fetched and checked (or name another with --tpc DIR, before
the command). The UWCs it makes go in uwcs/ beside this file.

COMMANDS (on Windows use "py -3" in place of "python3")
  python3 tpm_sandbox.py check DRAFT.json
      the new-UWC gate's checks (TPC check_draft), nothing stored
  python3 tpm_sandbox.py new DRAFT.json --prefix PFX [--initiator NAME]
      runs the gate; on PASS writes the CANDIDATE certificate in uwcs/PFX/
  python3 tpm_sandbox.py declare PFX --by NAME --confirm
      THE HUMAN'S ACT (H-DECL's part), typed by a person, never by an AI.
      Without --confirm it only prints what is being declared. With it: the
      UWC is DECLARED, the register opens (MASTER at PFX-1, one NAMED_GAP per
      IC2 or UNDER gap after it)
  python3 tpm_sandbox.py push PFX RECORDS.json --block B [--account A]
                                               [--kind WORK|EOM-0|REPRODUCTION]
      lands a push: every record checked at PUSH, TPX's fields written,
      checked again at LANDED, citations checked, promotions, EOM-0
      settlement and SR3 screening run; all or nothing
  python3 tpm_sandbox.py next-id PFX
  python3 tpm_sandbox.py difference PFX VERIFICATION.json
      computes a VERIFICATION's DIFFERENCE with TPC and prints the record
  python3 tpm_sandbox.py compare PFX RID REPRODUCTION.json
      compares a reproduction with a landed RESULT (lands nothing)
  python3 tpm_sandbox.py withdraw PFX RID --account A
      the author's own withdrawal (cause AUTHOR, WITHDRAWN only)
  python3 tpm_sandbox.py status PFX
  python3 tpm_sandbox.py show PFX RID
  python3 tpm_sandbox.py verify PFX
      the digest gate and the local chain, recomputed from the files
  python3 tpm_sandbox.py report PFX
      writes uwcs/PFX/REPORT.txt: everything about the UWC in one file
  python3 tpm_sandbox.py example NAME
      prints a worked example: draft (the spring draft from tpc/examples),
      push_1, push_2, push_bad_id, verification_draft or reproduction
  python3 tpm_sandbox.py selftest
      runs the spring example end to end in a temporary folder

Every command prints one block from "=== TPM SANDBOX" to "=== END". Commands
on a UWC also save the block under uwcs/PFX/log/.

WHAT THE SANDBOX DOES NOT DO (by design; each is a TPX duty tested later):
wiki pages, the chain pages, leases and VOID lines, register volumes, the lock
and collaboration pages, CORROBORATION landing (use `compare`), strikes (SR3
contradictions are reported, never struck), holds, probes, canaries, falcons,
scores, the bibliography, ENVIRONMENT MODULES, the governor, accounts and
certification. Identifiers are taken in strict sequence from one mark; TPX's
own records (WITHDRAWAL, PROMOTION) take the next identifiers after a push's
records. These limits are not findings.
"""
import datetime
import hashlib
import json
import math
import os
import shutil
import sys
import tempfile
import uuid

sys.dont_write_bytecode = True           # never write __pycache__ into the declared tree

SANDBOX_VERSION = "0.2.0"
HERE = os.path.dirname(os.path.abspath(__file__))
TPC_DIR = os.path.join(HERE, "tpc")
UWCS = os.path.join(HERE, "uwcs")

# The TPC-PY trees this sandbox runs on, by tree digest (TPC-PY page, "Versions").
# D_IS is tpc_digest d_is over the TPS version and D(TPS) with no other member.
KNOWN_TREES = {
    "2998ac4c24dff83bd047a691081b7d27bb25c9d26512bffe519f5c2b6ba127e2": {
        "TPC_PY": "0.2.0", "TPC_PY_STATUS": "declared 2026-09-30",
        "TPS": "0.4.0", "TPS_STATUS": "declared 2026-09-30",
        "D_TPS": "5a012a7378236044b88d543451bbe2e23a39ab48d8fec555937fad17d42a7e9c",
        "D_IS": "2c22079d7395ed729f0e2468ee659a24830551dcb364516779c0f8364727a57e"},
    "63ef0f2decb5b2c15c19274026b2f9a95160d0f54fee3f58e273088901f564f1": {
        "TPC_PY": "0.3.0", "TPC_PY_STATUS": "PROPOSED, not declared on 2026-09-30",
        "TPS": "0.5.0", "TPS_STATUS": "PROPOSED, not declared on 2026-09-30",
        "D_TPS": "e45ef3a89f40c1335e2750dd7568e2be92b392717f90d23c917e0ee184688a0b",
        "D_IS": "662a69d8a947ecb2c11caae2fc3f9de461d2e9b61e6a3fdaec02225b7935292e"},
}
PUSH_CLASSES = ("RESULT", "ASSUMPTION", "VERIFICATION", "ENTRY_STATUS")
KINDS = ("WORK", "EOM-0", "REPRODUCTION")


class Stop(Exception):
    """A refusal: the message is printed, nothing is written, exit 1."""


# ---- output ---------------------------------------------------------------------------
class Out:
    def __init__(self, cmd, args):
        self.lines = ["=== TPM SANDBOX %s | %s %s | %s ===" % (SANDBOX_VERSION, cmd, " ".join(args), now())]

    def bound(self, b):
        self.lines.append("(TPC-PY %s, %s; TPS %s, %s. Non-normative sandbox: nothing here is of record.)" % (
            b["TPC_PY"], b["TPC_PY_STATUS"], b["TPS"], b["TPS_STATUS"]))

    def __call__(self, s=""):
        self.lines.extend(str(s).split("\n"))

    def text(self, code):
        return "\n".join(self.lines + ["=== END (exit %d) ===" % code]) + "\n"


def now():
    return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def today():
    return now()[:10]


def cj(v):
    """Canonical JSON for hashing and for small printed values."""
    return json.dumps(v, sort_keys=True, separators=(",", ":"), ensure_ascii=False)


def sha256_bytes(b):
    return hashlib.sha256(b).hexdigest()


def write_json(path, value):
    tmp = path + ".tmp"
    with open(tmp, "w", encoding="utf-8", newline="\n") as f:
        f.write(json.dumps(value, indent=2, ensure_ascii=False) + "\n")
    os.replace(tmp, path)


def read_json(path):
    with open(path, "r", encoding="utf-8") as f:
        return json.load(f, object_pairs_hook=_no_dupes)


# ---- the declared calculator -----------------------------------------------------------
def load_binding():
    """The versions this run is bound to, read from the tree digest of TPC_DIR."""
    if not os.path.isdir(TPC_DIR):
        raise Stop("TPC-PY not found at %s. Fetch it with verify_tpc_py.py --fetch into a folder named tpc "
                   "beside this file, or name the folder with --tpc DIR before the command." % TPC_DIR)
    got = tree_digest()
    b = KNOWN_TREES.get(got)
    if b is None:
        raise Stop("TPC-PY's tree digest here is %s, which this sandbox does not know (known: %s). Either "
                   "a file under %s was changed or added, or TPC-PY has a newer version than this sandbox; "
                   "fetch the tree again with verify_tpc_py.py, or use the sandbox published for that "
                   "version." % (got, ", ".join("%s (%s)" % (v["TPC_PY"], k[:16]) for k, v in
                                                KNOWN_TREES.items()), TPC_DIR))
    b = dict(b)
    b["TPC_PY_TREE"] = got
    return b


def tree_digest():
    """verify_tpc_py 0.3.0's tree digest over the files found under tpc/."""
    fields = ["TPC-PY-TREE", "1"]
    entries = []
    for root, dirs, files in os.walk(TPC_DIR):
        dirs[:] = [d for d in dirs if d != "__pycache__"]
        for n in files:
            if n.endswith(".pyc"):
                continue
            p = os.path.join(root, n)
            rel = os.path.relpath(p, TPC_DIR).replace(os.sep, "/")
            with open(p, "rb") as f:
                entries.append((rel, sha256_bytes(f.read())))
    h = hashlib.sha256()
    for path, sha in sorted(entries):
        fields += [path, sha]
    for x in fields:
        b = x.encode("utf-8")
        h.update(str(len(b)).encode("ascii") + b":" + b)
    return h.hexdigest()


def load_tpc(binding):
    sys.path.insert(0, TPC_DIR)
    import check_draft as CD      # noqa
    import tpc_zone as Z          # noqa
    import tpc_tests as K         # noqa
    import tpc_eom as E           # noqa
    import tpc_record as RC       # noqa
    import tpc_strike as S        # noqa
    import tpc_units as U         # noqa
    import tpc_probe as P         # noqa
    import tpc_digest as G        # noqa
    return dict(CD=CD, Z=Z, K=K, E=E, RC=RC, S=S, U=U, P=P, G=G)


# ---- the UWC folder ---------------------------------------------------------------------
def uwc_dir(prefix):
    return os.path.join(UWCS, prefix)


def load_uwc(prefix):
    d = uwc_dir(prefix)
    if not os.path.isdir(d):
        raise Stop("no UWC with prefix %s under %s" % (prefix, UWCS))
    u = {"dir": d,
         "draft": read_json(os.path.join(d, "draft.json")),
         "cert": read_json(os.path.join(d, "certificate.json"))}
    rp = os.path.join(d, "register.json")
    u["register"] = read_json(rp) if os.path.exists(rp) else None
    mp = os.path.join(d, "meta.json")
    u["meta"] = read_json(mp) if os.path.exists(mp) else None
    return u


def chain_lines(d):
    p = os.path.join(d, "chain.jsonl")
    if not os.path.exists(p):
        return []
    with open(p, "r", encoding="utf-8") as f:
        return [json.loads(x) for x in f.read().splitlines() if x.strip()]


def chain_append(d, event, detail):
    lines = chain_lines(d)
    prev = lines[-1]["hash"] if lines else "GENESIS"
    rp = os.path.join(d, "register.json")
    reg_sha = sha256_bytes(open(rp, "rb").read()) if os.path.exists(rp) else ""
    line = {"seq": len(lines) + 1, "time": now(), "event": event, "detail": detail,
            "register_sha256": reg_sha, "prev": prev}
    line["hash"] = sha256_bytes(cj(line).encode("utf-8"))
    with open(os.path.join(d, "chain.jsonl"), "a", encoding="utf-8", newline="\n") as f:
        f.write(cj(line) + "\n")
    return line


def save_log(d, cmd, text):
    ld = os.path.join(d, "log")
    os.makedirs(ld, exist_ok=True)
    n = len([x for x in os.listdir(ld) if x.endswith(".txt")]) + 1
    with open(os.path.join(ld, "%04d_%s.txt" % (n, cmd)), "w", encoding="utf-8", newline="\n") as f:
        f.write(text)


def rid(prefix, n):
    return "%s-%d" % (prefix, n)


def rid_num(r):
    return int(r.rsplit("-", 1)[1])


def keyed(register):
    return [r for r in register if isinstance(r, dict)]


def problems_text(res):
    """A TPC ["REFUSED", problems] as lines."""
    if res[0] == "REFUSED" and isinstance(res[1], list):
        return ["  %s %s: %s" % tuple(p) if isinstance(p, list) and len(p) == 3 else "  %s" % (p,)
                for p in res[1]]
    return ["  %s" % (res,)]


# ---- the gate ---------------------------------------------------------------------------
def gate(T, draft, out):
    """check_draft's check, then the parts the certificate needs."""
    if "MODULES_USED" in draft:
        out("DRAFT: MODULES_USED is not read by the sandbox at 0.1.0: no ENVIRONMENT MODULE is declared yet "
            "(the interview's answer is NONE). Remove MODULES_USED and write the entries yourself.")
        return None
    ok = T["CD"].check(draft, out)
    if not ok:
        return None
    Z, K, E = T["Z"], T["K"], T["E"]
    fin = K.final_checks(draft)
    ic2 = sorted(g.split("DOF ", 1)[1].rsplit(" is untied", 1)[0] for g in fin[2] if g.startswith("IC2: DOF "))
    f = E.eom_findings(draft)
    c = E.eom_class(draft)
    params = draft["TMS"].get("PARAMETERS", {})
    sc = params.get("STRUCTURE_CHECK", "ON")
    under = sorted(fd[1] for fd in (f[1] if f[0] == "OK" else []) if fd[0] == "UNDER") if sc == "ON" else []
    line = "STRUCTURE " + c[1] + ("" if len(c) == 2 else ": " + ", ".join(c[2]))
    owed = list(T["CD"].OWED_INSPECTIONS)
    findings = [fd[0] + ": " + fd[-1] for fd in (f[1] if f[0] == "OK" else [])] if sc == "ON" else []
    return {"D_TPK": Z.d_tpk(draft["TPK"]), "D_TRM": Z.d_trm(draft["TrM"]), "D_TMS": Z.d_tms(draft["TMS"]),
            "D_TFK": Z.d_tfk_of(draft), "IC2": ic2, "UNDER": under, "STRUCTURE_LINE": line,
            "STRUCTURE_FINDINGS": findings, "INSPECTIONS_OWED": owed}


# ---- commands ---------------------------------------------------------------------------
def cmd_check(T, B, args, out):
    if len(args) != 1:
        raise Stop("usage: check DRAFT.json")
    draft = T["CD"].load_draft(args[0])
    g = gate(T, draft, out)
    out("")
    out("SANDBOX GATE: " + ("PASS" if g else "REFUSED"))
    return 0 if g else 1


def cmd_new(T, B, args, out):
    opts, pos = parse(args, {"--prefix": 1, "--initiator": 1})
    if len(pos) != 1 or "--prefix" not in opts:
        raise Stop("usage: new DRAFT.json --prefix PFX [--initiator NAME]")
    prefix = opts["--prefix"]
    if not T["RC"]._m(T["RC"]._IDENT, prefix) or "-" in prefix:
        raise Stop("prefix %r is not an identifier ([A-Za-z_][A-Za-z0-9_]*)" % prefix)
    if os.path.exists(uwc_dir(prefix)):
        raise Stop("prefix %s is already used in this sandbox (uwcs/%s exists); prefixes are unique" % (prefix, prefix))
    raw = open(pos[0], "rb").read()
    draft = T["CD"].load_draft(pos[0])
    g = gate(T, draft, out)
    if not g:
        out("")
        out("SANDBOX GATE: REFUSED. No UWC was created. Fix the draft and run new again.")
        return 1
    cert = {"STATE": "CANDIDATE", "SANDBOX": "NOT OF RECORD: a local sandbox; no TPX, no chain page, no wiki",
            "UUID": str(uuid.uuid4()), "PREFIX": prefix,
            "D_TPK": g["D_TPK"], "D_TRM": g["D_TRM"], "D_TMS": g["D_TMS"], "D_TFK": g["D_TFK"],
            "STRUCTURE_LINE": g["STRUCTURE_LINE"], "STRUCTURE_FINDINGS": g["STRUCTURE_FINDINGS"],
            "IC2_GAPS": g["IC2"], "UNDER_GAPS": g["UNDER"],
            "MODULES_USED": "NONE", "INTERVIEW": "NONE (no ENVIRONMENT MODULE is declared)",
            "ACCESS_POLICY": "CLOSED (sandbox)", "AUDIT_TRIGGER": "ON REQUEST (sandbox)",
            "BIBLIOGRAPHY_SETTING": "OFF", "INITIATOR": opts.get("--initiator", "unnamed"),
            "CREATED": now(), "TPS": B["TPS"], "D_TPS": B["D_TPS"], "D_IS": B["D_IS"],
            "TPC_PY": B["TPC_PY"], "TPC_PY_TREE": B["TPC_PY_TREE"],
            "INSPECTIONS_OWED": g["INSPECTIONS_OWED"],
            "CLOSED_WORLD_QUESTION": "Is every interaction not written in the TPK absent from the system modelled?"}
    d = uwc_dir(prefix)
    os.makedirs(os.path.join(d, "pushes"))
    with open(os.path.join(d, "draft.json"), "wb") as f:
        f.write(raw)
    write_json(os.path.join(d, "certificate.json"), cert)
    chain_append(d, "NEW", {"prefix": prefix, "uuid": cert["UUID"], "d_tfk": cert["D_TFK"],
                            "draft_sha256": sha256_bytes(raw)})
    out("")
    out("CANDIDATE CERTIFICATE written: uwcs/%s/certificate.json" % prefix)
    out("  UUID %s  PREFIX %s" % (cert["UUID"], prefix))
    out("  D(TFK) %s" % cert["D_TFK"])
    out("  %s" % cert["STRUCTURE_LINE"])
    out("  named gaps at declaration: IC2 %s; UNDER %s" % (g["IC2"] or "none", g["UNDER"] or "none"))
    out("NEXT: the human reads the certificate and declares it (only the human runs this):")
    out("  py -3 tpm_sandbox.py declare %s --by <your name> --confirm" % prefix)
    return 0


def cmd_declare(T, B, args, out):
    opts, pos = parse(args, {"--by": 1, "--confirm": 0})
    if len(pos) != 1 or "--by" not in opts:
        raise Stop("usage: declare PFX --by NAME --confirm")
    u = load_uwc(pos[0])
    cert, d = u["cert"], u["dir"]
    if cert["STATE"] != "CANDIDATE":
        raise Stop("UWC %s is %s, not CANDIDATE" % (pos[0], cert["STATE"]))
    draft = u["draft"]
    if T["Z"].d_tfk_of(draft) != cert["D_TFK"]:
        raise Stop("DIGEST GATE: draft.json no longer digests to the candidate's D(TFK); nothing declared")
    out("DECLARING UWC %s (UUID %s)" % (cert["PREFIX"], cert["UUID"]))
    out("  D(TFK) %s" % cert["D_TFK"])
    out("  %s" % cert["STRUCTURE_LINE"])
    for fd in cert["STRUCTURE_FINDINGS"]:
        out("  finding: " + fd)
    out("  inspections owed to the declaring authority:")
    for i in cert["INSPECTIONS_OWED"]:
        out("    - " + i)
    out("  closed-world question: " + cert["CLOSED_WORLD_QUESTION"])
    if "--confirm" not in opts:
        out("")
        out("NOT DECLARED: add --confirm to declare, after reading the lines above.")
        return 1
    stamp = now()
    cert.update(STATE="DECLARED", DECLARED_BY=opts["--by"], DECLARED=stamp)
    RC = T["RC"]
    prefix = cert["PREFIX"]
    master = {"ID": rid(prefix, 1), "CLASS": "MASTER", "GRADE": "NOT GRADED", "LANDED_BY": "TPX", "LANDED": stamp,
              "TPS": B["TPS"], "UUID": cert["UUID"], "PREFIX": prefix,
              "D_TPK": cert["D_TPK"], "D_TRM": cert["D_TRM"], "D_TMS": cert["D_TMS"], "D_TFK": cert["D_TFK"],
              "CERTIFICATE": "SANDBOX certificate %s declared %s by %s" % (cert["UUID"], stamp[:10], opts["--by"]),
              "STRUCTURE_LINE": cert["STRUCTURE_LINE"], "MODULES_USED": "NONE"}
    dg = RC.record_digest(master)
    if dg[0] != "OK":
        raise Stop("MASTER not digestible:\n" + "\n".join(problems_text(dg)))
    master["DIGEST"] = dg[1]
    chk = RC.record_check(master, draft, "OFF", "LANDED")
    if chk[0] != "OK":
        raise Stop("MASTER refused at LANDED:\n" + "\n".join(problems_text(chk)))
    trig = "the certificate %s, %s" % (cert["UUID"], cert["STRUCTURE_LINE"])
    gaps = RC.gap_records(prefix, cert["IC2_GAPS"], cert["UNDER_GAPS"], trig, stamp, B["TPS"])
    if gaps[0] != "OK":
        raise Stop("gap records refused:\n" + "\n".join(problems_text(gaps)))
    register = [master] + gaps[1]
    meta = {"mark": 1 + len(gaps[1]), "pushes": 0, "records": {}, "setting": "OFF"}
    for r in register:
        meta["records"][r["ID"]] = {"push": 0, "block": "INSTANTIATION"}
    write_json(os.path.join(d, "register.json"), register)
    write_json(os.path.join(d, "meta.json"), meta)
    write_json(os.path.join(d, "certificate.json"), cert)
    chain_append(d, "DECLARE", {"by": opts["--by"], "master_digest": master["DIGEST"],
                                "gaps": [g["ID"] for g in gaps[1]]})
    out("")
    out("DECLARED. Register opened: %s (MASTER)%s" % (master["ID"], "".join(
        ", %s (NAMED_GAP %s %s)" % (g["ID"], g["SOURCE"], g["AGAINST"][0]) for g in gaps[1])))
    if cert["UNDER_GAPS"]:
        out("EOM-0 is OPEN: only pushes with --kind EOM-0 or --kind REPRODUCTION land until each UNDER gap settles.")
    out("NEXT FREE IDENTIFIER: %s" % rid(prefix, meta["mark"] + 1))
    return 0


def _declared(u):
    if u["cert"]["STATE"] != "DECLARED" or u["register"] is None:
        raise Stop("UWC %s is not DECLARED; the human declares it first" % u["cert"]["PREFIX"])


def digest_gate(T, u):
    """Before every write: the draft digests to the certificate's D(TFK) and the MASTER carries it."""
    d = T["Z"].d_tfk_of(u["draft"])
    m = u["register"][0]
    if d != u["cert"]["D_TFK"] or m.get("D_TFK") != d:
        raise Stop("DIGEST GATE FAILED: D(TFK) of draft.json is %s; certificate %s; MASTER %s. Writes halt."
                   % (d, u["cert"]["D_TFK"], m.get("D_TFK")))


def words_of(T, register):
    w = T["RC"].status_words(register)
    if w[0] != "OK":
        raise Stop("register unreadable:\n" + "\n".join(problems_text(w)))
    return w[1]


def load_push(T, path):
    v = T["CD"]._load_json(path)
    naming = []
    if isinstance(v, dict):
        extra = sorted(k for k in v if k not in ("RECORDS", "EOM0_SETTLEMENT"))
        if extra or "RECORDS" not in v:
            raise Stop("a push file is a JSON list of records, or an object with RECORDS "
                       "(and optionally EOM0_SETTLEMENT); found keys %s" % sorted(v))
        naming = v.get("EOM0_SETTLEMENT", [])
        v = v["RECORDS"]
    if not isinstance(v, list) or not v or not all(isinstance(r, dict) for r in v):
        raise Stop("a push holds a non-empty list of record objects")
    return v, naming


def cmd_push(T, B, args, out):
    opts, pos = parse(args, {"--block": 1, "--account": 1, "--kind": 1})
    if len(pos) != 2 or "--block" not in opts:
        raise Stop("usage: push PFX RECORDS.json --block B [--account A] [--kind WORK|EOM-0|REPRODUCTION]")
    kind = opts.get("--kind", "WORK")
    if kind not in KINDS:
        raise Stop("--kind %s is not one of %s" % (kind, ", ".join(KINDS)))
    account = opts.get("--account", "tester")
    block = opts["--block"]
    u = load_uwc(pos[0])
    _declared(u)
    digest_gate(T, u)
    RC, S = T["RC"], T["S"]
    draft, cert, meta = u["draft"], u["cert"], u["meta"]
    prefix = cert["PREFIX"]
    register = list(u["register"])
    raw = open(pos[1], "rb").read()
    records, naming = load_push(T, pos[1])
    eo = RC.eom0_open(register)
    if eo[0] == "OK" and eo[1] and kind not in ("EOM-0", "REPRODUCTION"):
        raise Stop("EOM-0 OPEN: this UWC lands only blocks of kind EOM-0 or REPRODUCTION until its UNDER gaps "
                   "settle (TPS 3.3.16). Re-run with --kind EOM-0 if this push settles them.")
    stamp = now()
    push_no = meta["pushes"] + 1
    mark = meta["mark"]
    probs, landed, insp_notes = [], [], []
    for i, rec in enumerate(records, 1):
        tag = "record %d (%s)" % (i, rec.get("ID", "no ID"))
        cls = rec.get("CLASS")
        if cls == "CORROBORATION":
            probs.append(tag + ": CORROBORATION is not landed by the sandbox at 0.1.0 (TPX writes OUTCOME, "
                               "DIFFERENCE, INDEPENDENCE and TARGET_STATUS after its own comparison); run compare")
            continue
        chk = RC.record_check(rec, draft, "OFF", "PUSH")
        if chk[0] != "OK":
            probs.append(tag + ": refused at PUSH")
            probs.extend(problems_text(chk))
            continue
        if cls not in PUSH_CLASSES:
            probs.append(tag + ": CLASS %s is not pushed" % cls)
            continue
        form_probs = []
        if cls in ("RESULT", "ASSUMPTION"):
            fc = RC.record_form_check(rec, draft)          # parse, names, T4, D variable, ROLE (TPS 3.2.x)
            if fc[0] != "OK":
                form_probs += problems_text(fc)
        if "DOMAIN_CONDITION" in rec:
            dc = RC.domain_check(rec, draft)               # T6 totality and dimensions of DOMAIN_CONDITION
            if dc[0] != "OK":
                form_probs += problems_text(dc)
        if cls == "RESULT" and rec.get("REGIME") == "M":
            bc = RC.budget_check(rec)                      # U_C against its BUDGET at the last written digit
            if bc[0] == "REFUSED":
                form_probs += problems_text(bc)
            elif bc[0] in ("INSPECT", "CONVENTION"):
                insp_notes.append("%s: BUDGET %s: %s" % (rec.get("ID"), bc[0], bc[1]))
        if form_probs:
            probs.append(tag + ": refused by the declared-form, domain or budget check")
            probs.extend(form_probs)
            continue
        want = rid(prefix, mark + 1)
        if rec["ID"] != want:
            probs.append(tag + ": ID must be the next free identifier %s (identifiers are taken in sequence; "
                               "run next-id)" % want)
            continue
        mark += 1
        L = dict(rec)
        L["GRADE"] = RC.T["landgrade"][cls]
        if RC.T["statuses"].get(cls):
            L["STATUS"] = "UNMARKED"
        L.update(BLOCK=block, AUTHOR=account, LANDED_BY=account, LANDED=stamp, TPS=B["TPS"], D_IS=B["D_IS"])
        cc = RC.citation_check(L, register, [], False)
        if cc[0] != "OK":
            probs.append(tag + ": citation check refused")
            probs.extend(problems_text(cc))
            continue
        if cls == "VERIFICATION":
            tgt = next((r for r in keyed(register) if r["ID"] == L.get("TARGET")), None)
            if tgt is None:
                probs.append(tag + ": TARGET %s is not a landed record (a VERIFICATION follows its TARGET's push)"
                             % L.get("TARGET"))
                continue
            vo = RC.verification_order(meta["records"][tgt["ID"]]["push"], push_no, None)
            if vo[0] not in ("OK", "INSPECT"):
                probs.append(tag + ": order refused")
                probs.extend(problems_text(vo))
                continue
            vd = RC.verification_difference(tgt, L, draft["TMS"]["PARAMETERS"]["K"])
            if vd[0] != "OK":
                probs.append(tag + ": DIFFERENCE refused (compute it with: difference %s FILE)" % prefix)
                probs.extend(problems_text(vd) if vd[0] == "REFUSED" else ["  " + str(vd)])
                continue
        dg = RC.record_digest(L)
        if dg[0] != "OK":
            probs.append(tag + ": not digestible")
            probs.extend(problems_text(dg))
            continue
        L["DIGEST"] = dg[1]
        chk2 = RC.record_check(L, draft, "OFF", "LANDED")
        if chk2[0] != "OK":
            probs.append(tag + ": refused at LANDED")
            probs.extend(problems_text(chk2))
            continue
        register.append(L)
        landed.append(L)
        meta["records"][L["ID"]] = {"push": push_no, "block": block}
    if probs:
        out("PUSH REFUSED. Nothing landed; the register is unchanged. Problems:")
        for p in probs:
            out("  " + p)
        out("NEXT FREE IDENTIFIER: %s" % rid(prefix, meta["mark"] + 1))
        return 1
    notes = ["INSPECTION " + n for n in insp_notes]
    # supersessions: one WITHDRAWAL per superseded record, TPX-written, WITHDRAWN
    for L in landed:
        old = L.get("SUPERSEDES")
        if not old:
            continue
        words = words_of(T, register)
        sc = RC.withdrawal_scope(register, old, [])
        if sc[0] != "OK":
            raise Stop("withdrawal scope refused:\n" + "\n".join(problems_text(sc)))
        cascade = [c for c in sc[1] if c[0] != L["ID"]]
        act = {"cause": "SUPERSEDED", "withdrawn": old, "withdrawn_by": account, "status_set": "WITHDRAWN",
               "prior_status": words[old], "superseded_by": L["ID"], "locus_gap": "", "still_stands": [],
               "cascade": cascade, "tps_version": B["TPS"], "d_tms": cert["D_TMS"]}
        mark += 1
        wr = RC.withdrawal_record(rid(prefix, mark), act)
        if wr[0] != "OK":
            raise Stop("withdrawal record refused:\n" + "\n".join(problems_text(wr)))
        register.append(wr[1])
        meta["records"][wr[1][0]] = {"push": push_no, "block": block}
        notes.append("WITHDRAWAL %s: %s SUPERSEDED by %s, WITHDRAWN; cascade %s"
                     % (wr[1][0], old, L["ID"], [c[0] for c in cascade] or "none"))
    # EOM-0 settlement
    settled = settle_eom0(T, draft, register, naming, stamp, notes)
    # promotion
    words = words_of(T, register)
    pp = RC.promotion_pass(register, words, draft, [])
    if pp[0] != "OK":
        raise Stop("promotion pass refused:\n" + "\n".join(problems_text(pp)))
    if pp[1]:
        mark += 1
        pr = RC.promotion_record(rid(prefix, mark), block, pp[1])
        if pr[0] != "OK":
            raise Stop("promotion record refused:\n" + "\n".join(problems_text(pr)))
        register.append(pr[1])
        meta["records"][pr[1][0]] = {"push": push_no, "block": block}
        notes.append("PROMOTION %s: %s ESTABLISHED" % (pr[1][0], ", ".join(t[0] for t in pp[1])))
    meta["mark"] = mark
    meta["pushes"] = push_no
    d = u["dir"]
    with open(os.path.join(d, "pushes", "%04d_%s.json" % (push_no, safe(block))), "wb") as f:
        f.write(raw)
    write_json(os.path.join(d, "register.json"), register)
    write_json(os.path.join(d, "meta.json"), meta)
    chain_append(d, "PUSH", {"push": push_no, "block": block, "account": account, "kind": kind,
                             "landed": [L["ID"] for L in landed], "push_sha256": sha256_bytes(raw)})
    out("PUSH %d LANDED (block %s, account %s, kind %s):" % (push_no, block, account, kind))
    for L in landed:
        out("  %s %s %s DIGEST %s" % (L["ID"], L["CLASS"], L["GRADE"], L["DIGEST"][:16]))
    for n in notes:
        out("  " + n)
    screen(T, draft, register, out)
    out("NEXT FREE IDENTIFIER: %s" % rid(prefix, mark + 1))
    return 0


def safe(s):
    return "".join(ch if ch.isalnum() or ch in "-_" else "_" for ch in s)[:40] or "block"


def settle_eom0(T, draft, register, naming, stamp, notes):
    RC, E = T["RC"], T["E"]
    eo = RC.eom0_open(register)
    if eo[0] != "OK" or not eo[1]:
        if naming:
            raise Stop("EOM0_SETTLEMENT given but EOM-0 is not OPEN")
        return False
    words = words_of(T, register)
    st = E.settlement_check(draft, keyed(register), words, naming)
    if st[0] != "OK":
        raise Stop("EOM-0 settlement refused:\n" + "\n".join(problems_text(st)))
    res = st[1]
    by = {}
    for unk in res["BY_TEST"]:
        by[unk] = sorted(res["USED"]) or ["-"]
    for unk, r in res["BY_NAMING"]:
        by.setdefault(unk, []).append(r)
    for g in keyed(register):
        if g["CLASS"] == "NAMED_GAP" and g["SOURCE"] == "UNDER" and g["STATE"] == "OPEN" and g["AGAINST"][0] in by:
            ids = [x for x in by[g["AGAINST"][0]] if x != "-"]
            gs = RC.gap_state("SETTLED", stamp[:10], sorted(set(ids)))
            if gs[0] != "OK":
                raise Stop("gap state refused:\n" + "\n".join(problems_text(gs)))
            g["STATE"] = gs[1]
            notes.append("NAMED_GAP %s (UNDER %s): %s" % (g["ID"], g["AGAINST"][0], gs[1]))
    eo2 = RC.eom0_open(register)
    if eo2[0] == "OK" and not eo2[1]:
        notes.append("EOM-0 CLOSED")
    return True


def screen(T, draft, register, out):
    """SR3 over every TrM entry: reported, never struck (sandbox)."""
    S, U, RC = T["S"], T["U"], T["RC"]
    words = words_of(T, register)
    recs = keyed(register)
    k = float(draft["TMS"]["PARAMETERS"]["K"])
    byid = dict((r["ID"], r) for r in recs)
    found = []
    for e in draft["TrM"]["ENTRIES"]:
        q = e["NAME"]
        cs = S.comparison_set(recs, words, draft, q)
        if cs[0] != "OK":
            found.append("SR3 %s: not computed: %s" % (q, cs[1:]))
            continue
        if not cs[1]:
            continue
        groups = S.sr8_groups(recs, words, draft, q)
        for a, b, mode in cs[1]:
            if mode != "TEST":
                found.append("SR3 %s: %s and %s by INSPECTION (%s)" % (q, a, b, mode))
                continue
            ra, rb = byid[a], byid[b]

            def norm(r):
                return U.normalize_measurement(r["VALUE"], r.get("U_C", "0") if r.get("REGIME") == "M" else "0",
                                               r["UNIT"])
            na, nb = norm(ra), norm(rb)
            if na[0] != "OK" or nb[0] != "OK":
                found.append("SR3 %s: %s and %s not normalized" % (q, a, b))
                continue
            sf = 1.0
            if groups[0] == "OK":
                for gm, ids in groups[1]:
                    if a in ids and b in ids and len(ids) >= 2:
                        ms = []
                        for x in ids:
                            nx = norm(byid[x])
                            ms.append({"x": nx[1], "u": nx[2]})
                        sf = S.sr8_scale_factor(ms)
            if ra.get("REGIME") == "L" or rb.get("REGIME") == "L":
                l_, m_ = (na, nb) if ra.get("REGIME") == "L" else (nb, na)
                c = S.lm_contradicts(l_[1], m_[1], m_[2], k, sf)
            else:
                c = S.mm_contradicts(na[1], na[2], nb[1], nb[2], k, sf)
            if c:
                found.append("SR3 CONTRADICTION on %s: %s and %s (k %s, S %.4g). The sandbox reports it; TPX "
                             "would apply SR5 or SR7. Not struck." % (q, a, b, k, sf))
    if found:
        out("SCREENING:")
        for f in found:
            out("  " + f)


def cmd_next_id(T, B, args, out):
    if len(args) != 1:
        raise Stop("usage: next-id PFX")
    u = load_uwc(args[0])
    _declared(u)
    out("NEXT FREE IDENTIFIER: %s" % rid(u["cert"]["PREFIX"], u["meta"]["mark"] + 1))
    return 0


def cmd_difference(T, B, args, out):
    if len(args) != 2:
        raise Stop("usage: difference PFX VERIFICATION.json")
    u = load_uwc(args[0])
    _declared(u)
    RC, U = T["RC"], T["U"]
    v = T["CD"]._load_json(args[1])
    if not isinstance(v, dict) or v.get("CLASS") != "VERIFICATION":
        raise Stop("the file holds one VERIFICATION record object")
    tgt = next((r for r in keyed(u["register"]) if r["ID"] == v.get("TARGET")), None)
    if tgt is None:
        raise Stop("TARGET %s is not a landed record" % v.get("TARGET"))
    ext = v.get("EXTERNAL", {})
    nt = U.normalize_measurement(tgt.get("VALUE"), tgt.get("U_C", "0") if tgt.get("REGIME") == "M" else "0",
                                 tgt.get("UNIT"))
    ne = U.normalize_measurement(ext.get("VALUE"), ext.get("U_C"), ext.get("UNIT"))
    if nt[0] != "OK" or ne[0] != "OK":
        raise Stop("values not normalized: target %s; external %s" % (nt, ne))
    val = nt[1] - ne[1]
    uu = math.sqrt(nt[2] * nt[2] + ne[2] * ne[2])
    if uu == 0.0:
        raise Stop("U is 0: both uncertainties are 0 (REC-95)")
    v["DIFFERENCE"] = {"VALUE": RC._cd(val), "U": RC._cd(uu), "ZETA": RC._cd(val / uu)}
    vd = RC.verification_difference(tgt, v, u["draft"]["TMS"]["PARAMETERS"]["K"])
    if vd[0] != "OK":
        out("TPC refused the verification:")
        for p in problems_text(vd) if vd[0] == "REFUSED" else [str(vd)]:
            out(p)
        return 1
    out("DIFFERENCE computed by TPC-PY (target minus external, coherent SI): %s" % cj(v["DIFFERENCE"]))
    out("SR3 test between TARGET and EXTERNAL at k %s: %s" % (u["draft"]["TMS"]["PARAMETERS"]["K"],
                                                               "CONTRADICTS" if vd[2] else "does not contradict"))
    out("The record with its DIFFERENCE, ready to push (after its TARGET's push):")
    out(json.dumps(v, indent=2, ensure_ascii=False))
    return 0


def cmd_compare(T, B, args, out):
    if len(args) != 3:
        raise Stop("usage: compare PFX RID REPRODUCTION.json")
    u = load_uwc(args[0])
    _declared(u)
    RC, P, S, U = T["RC"], T["P"], T["S"], T["U"]
    orig = next((r for r in keyed(u["register"]) if r["ID"] == args[1]), None)
    if orig is None or orig.get("CLASS") != "RESULT":
        raise Stop("%s is not a landed RESULT" % args[1])
    rep = T["CD"]._load_json(args[2])
    if not isinstance(rep, dict) or rep.get("CLASS") != "RESULT":
        raise Stop("the reproduction is one RESULT record object")
    if orig.get("REGIME") == "L":
        lp = RC.l_finding_pair(orig, rep)
        if lp[0] != "OK":
            out("NOT COMPARED: " + " ".join(problems_text(lp) if lp[0] == "REFUSED" else [str(lp)]))
            return 1
        c = P.compare("L", lp[1], lp[2], None, None, None)
        out("L COMPARISON over %s: %s" % (lp[1]["finding"]["type"], c["outcome"]))
        out("  original     %s" % c["original"])
        out("  reproduction %s" % c["reproduction"])
    else:
        a = U.normalize_measurement(orig["VALUE"], orig["U_C"], orig["UNIT"])
        b = U.normalize_measurement(rep.get("VALUE"), rep.get("U_C"), rep.get("UNIT"))
        if a[0] != "OK" or b[0] != "OK" or a[3] != b[3]:
            out("NOT COMPARED: %s / %s" % (a, b))
            return 1
        k = float(u["draft"]["TMS"]["PARAMETERS"]["K"])
        stat = abs(a[1] - b[1]) / math.sqrt(a[2] ** 2 + b[2] ** 2)
        c = S.mm_contradicts(a[1], a[2], b[1], b[2], k, 1.0)
        out("M COMPARISON (SR3, S 1): %s; statistic %.6g against k %s"
            % ("NON-CORROBORATION" if c else "MATCH", stat, k))
    out("Nothing landed: the sandbox does not write CORROBORATION records at 0.1.0.")
    return 0


def cmd_withdraw(T, B, args, out):
    opts, pos = parse(args, {"--account": 1})
    if len(pos) != 2 or "--account" not in opts:
        raise Stop("usage: withdraw PFX RID --account A")
    u = load_uwc(pos[0])
    _declared(u)
    digest_gate(T, u)
    RC = T["RC"]
    register, meta, cert = list(u["register"]), u["meta"], u["cert"]
    target = next((r for r in keyed(register) if r["ID"] == pos[1]), None)
    if target is None:
        raise Stop("%s is not a keyed record of the register" % pos[1])
    if target.get("AUTHOR") != opts["--account"]:
        raise Stop("only the record's AUTHOR (%s) withdraws it (cause AUTHOR)" % target.get("AUTHOR"))
    words = words_of(T, register)
    if words[pos[1]] not in ("PROVISIONAL", "ESTABLISHED", "HELD"):
        raise Stop("%s does not stand (%s)" % (pos[1], words[pos[1]]))
    sc = RC.withdrawal_scope(register, pos[1], [])
    if sc[0] != "OK":
        raise Stop("\n".join(problems_text(sc)))
    act = {"cause": "AUTHOR", "withdrawn": pos[1], "withdrawn_by": opts["--account"], "status_set": "WITHDRAWN",
           "prior_status": words[pos[1]], "superseded_by": "", "locus_gap": "", "still_stands": [],
           "cascade": sc[1], "tps_version": B["TPS"], "d_tms": cert["D_TMS"]}
    meta["mark"] += 1
    wr = RC.withdrawal_record(rid(cert["PREFIX"], meta["mark"]), act)
    if wr[0] != "OK":
        raise Stop("\n".join(problems_text(wr)))
    register.append(wr[1])
    meta["pushes"] += 1
    meta["records"][wr[1][0]] = {"push": meta["pushes"], "block": "WITHDRAWAL"}
    write_json(os.path.join(u["dir"], "register.json"), register)
    write_json(os.path.join(u["dir"], "meta.json"), meta)
    chain_append(u["dir"], "WITHDRAW", {"record": pos[1], "withdrawal": wr[1][0], "by": opts["--account"]})
    out("WITHDRAWAL %s: %s WITHDRAWN (cause AUTHOR); cascade %s"
        % (wr[1][0], pos[1], [c[0] for c in sc[1]] or "none"))
    return 0


def cmd_status(T, B, args, out):
    if len(args) != 1:
        raise Stop("usage: status PFX")
    u = load_uwc(args[0])
    cert = u["cert"]
    out("UWC %s  UUID %s  STATE %s" % (cert["PREFIX"], cert["UUID"], cert["STATE"]))
    out("  D(TFK) %s" % cert["D_TFK"])
    out("  %s" % cert["STRUCTURE_LINE"])
    out("  TPS %s  D(IS) %s  TPC-PY %s" % (cert["TPS"], cert["D_IS"][:16], cert["TPC_PY"]))
    if u["register"] is None:
        out("  register: not opened (declare first)")
        return 0
    words = words_of(T, u["register"])
    out("  register: %d records; next free identifier %s" % (len(u["register"]),
                                                             rid(cert["PREFIX"], u["meta"]["mark"] + 1)))
    for r in u["register"]:
        if isinstance(r, dict):
            extra = ""
            if r["CLASS"] == "NAMED_GAP":
                extra = " %s %s: %s" % (r["SOURCE"], r["AGAINST"], r["STATE"])
            elif r["CLASS"] == "VERIFICATION":
                extra = " by %s: TARGET %s, DIFFERENCE %s" % (r.get("AUTHOR"), r.get("TARGET"), cj(r.get("DIFFERENCE")))
            elif r["CLASS"] in PUSH_CLASSES:
                extra = " by %s: %s" % (r.get("AUTHOR"), (r.get("STATEMENT") or r.get("PROPOSITION") or
                                                          r.get("ENTRY") or "")[:70])
            out("  %-10s %-12s %-12s%s" % (r["ID"], r["CLASS"], words.get(r["ID"], "NOT GRADED"), extra))
        else:
            out("  %-10s %-12s %s" % (r[0], r[1], cj(r[2:5])[:90]))
    eo = T["RC"].eom0_open(u["register"])
    out("  EOM-0: %s" % ("OPEN" if eo[0] == "OK" and eo[1] else "not open"))
    screen(T, u["draft"], u["register"], out)
    return 0


def cmd_show(T, B, args, out):
    if len(args) != 2:
        raise Stop("usage: show PFX RID")
    u = load_uwc(args[0])
    _declared(u)
    for r in u["register"]:
        if (isinstance(r, dict) and r["ID"] == args[1]) or (isinstance(r, list) and r[0] == args[1]):
            out(json.dumps(r, indent=2, ensure_ascii=False))
            return 0
    raise Stop("%s is not in the register" % args[1])


def cmd_verify(T, B, args, out):
    if len(args) != 1:
        raise Stop("usage: verify PFX")
    u = load_uwc(args[0])
    RC, Z = T["RC"], T["Z"]
    bad = 0
    d = Z.d_tfk_of(u["draft"])
    ok = d == u["cert"]["D_TFK"]
    out("draft D(TFK) %s: %s" % (d, "matches the certificate" if ok else "DIFFERS from the certificate"))
    bad += not ok
    lines = chain_lines(u["dir"])
    prev = "GENESIS"
    for ln in lines:
        body = dict(ln)
        h = body.pop("hash")
        if body["prev"] != prev or sha256_bytes(cj(body).encode("utf-8")) != h:
            out("chain line %d BROKEN" % ln["seq"])
            bad += 1
        prev = h
    out("chain: %d lines%s" % (len(lines), "" if bad == 0 else " (see above)"))
    if u["register"] is not None:
        rp = os.path.join(u["dir"], "register.json")
        last = next((ln for ln in reversed(lines) if ln["event"] in ("DECLARE", "PUSH", "WITHDRAW")), None)
        if last and last["register_sha256"] != sha256_bytes(open(rp, "rb").read()):
            out("register.json DIFFERS from the last chain line's hash (edited outside the sandbox?)")
            bad += 1
        # STATE LINEs change in place and are outside every digest; only chained pushes rewrite the file
        if u["register"][0].get("D_TFK") != d:
            out("MASTER D_TFK DIFFERS from the draft")
            bad += 1
        n = 0
        for r in u["register"]:
            if isinstance(r, dict):
                c = RC.record_check(r, u["draft"], "OFF", "LANDED")
                n += 1
                if c[0] != "OK":
                    out("%s refused at LANDED:" % r["ID"])
                    for p in problems_text(c):
                        out(p)
                    bad += 1
        out("records re-checked at LANDED, digests recomputed: %d" % n)
        m = 0
        for r in u["register"]:
            if isinstance(r, list):
                m += 1
                if r[1] == "PROMOTION":
                    again = RC.promotion_record(r[0], r[2], r[3])
                else:
                    keys = RC.ACT_KEYS
                    act = dict(zip(["cause", "withdrawn", "withdrawn_by", "status_set", "prior_status",
                                    "superseded_by", "locus_gap", "still_stands", "cascade", "tps_version",
                                    "d_tms"], r[2:13]))
                    again = RC.withdrawal_record(r[0], act) if sorted(act) == sorted(keys) else ["REFUSED", []]
                if again[0] != "OK" or again[1] != r:
                    out("%s %s: its digest does not recompute" % (r[0], r[1]))
                    bad += 1
        out("TPX-written list records recomputed: %d" % m)
    out("VERIFY: %s" % ("PASS" if bad == 0 else "FAIL (%d)" % bad))
    return 0 if bad == 0 else 1


def cmd_report(T, B, args, out):
    if len(args) != 1:
        raise Stop("usage: report PFX")
    u = load_uwc(args[0])
    d = u["dir"]
    parts = ["TPM SANDBOX REPORT %s | UWC %s | %s" % (SANDBOX_VERSION, u["cert"]["PREFIX"], now()), "",
             "== certificate.json ==", json.dumps(u["cert"], indent=2, ensure_ascii=False), "",
             "== draft.json ==", open(os.path.join(d, "draft.json"), encoding="utf-8").read(), ""]
    if u["register"] is not None:
        parts += ["== register.json ==", json.dumps(u["register"], indent=2, ensure_ascii=False), ""]
    parts += ["== chain.jsonl ==", open(os.path.join(d, "chain.jsonl"), encoding="utf-8").read()]
    ld = os.path.join(d, "log")
    if os.path.isdir(ld):
        for n in sorted(os.listdir(ld)):
            parts += ["== log/%s ==" % n, open(os.path.join(ld, n), encoding="utf-8").read()]
    parts.append("END OF REPORT")
    p = os.path.join(d, "REPORT.txt")
    with open(p, "w", encoding="utf-8", newline="\n") as f:
        f.write("\n".join(parts) + "\n")
    out("REPORT written: uwcs/%s/REPORT.txt (%d bytes). Attach that file when you report back." %
        (u["cert"]["PREFIX"], os.path.getsize(p)))
    return 0


def cmd_selftest(T, B, args, out):
    """The spring example end to end, in a temporary folder."""
    global UWCS
    keep = UWCS
    tmp = tempfile.mkdtemp(prefix="tpm_sandbox_selftest_")
    UWCS = os.path.join(tmp, "uwcs")
    ex = os.path.join(tmp, "examples")
    os.makedirs(ex)
    for name in EXAMPLES:
        write_json(os.path.join(ex, name + ".json"), EXAMPLES[name])
    shutil.copyfile(os.path.join(TPC_DIR, "examples", "spring_draft.json"), os.path.join(ex, "draft.json"))
    steps = [("new", [os.path.join(ex, "draft.json"), "--prefix", "SPR", "--initiator", "selftest"], 0),
             ("push", ["SPR", os.path.join(ex, "push_1.json"), "--block", "B1"], 1),
             ("declare", ["SPR", "--by", "selftest", "--confirm"], 0),
             ("push", ["SPR", os.path.join(ex, "push_bad_id.json"), "--block", "B0"], 1),
             ("push", ["SPR", os.path.join(ex, "push_1.json"), "--block", "B1"], 0),
             ("difference", ["SPR", os.path.join(ex, "verification_draft.json")], 0),
             ("push", ["SPR", os.path.join(ex, "push_2.json"), "--block", "B2"], 0),
             ("compare", ["SPR", "SPR-3", os.path.join(ex, "reproduction.json")], 0),
             ("withdraw", ["SPR", "SPR-2", "--account", "tester"], 0),
             ("status", ["SPR"], 0),
             ("verify", ["SPR"], 0)]
    fails = 0
    try:
        for cmd, a, want in steps:
            sub = Out(cmd, a)
            code = run_cmd(T, B, cmd, a, sub)
            ok = code == want
            fails += not ok
            out("%-4s %s %s -> exit %d (want %d)" % ("ok" if ok else "FAIL", cmd, " ".join(
                os.path.basename(x) for x in a), code, want))
            if not ok:
                out(sub.text(code))
        reg = read_json(os.path.join(UWCS, "SPR", "register.json"))
        words = words_of(T, reg)
        expect = {"SPR-2": "WITHDRAWN", "SPR-3": "ESTABLISHED", "SPR-4": "WITHDRAWN", "SPR-6": "PROVISIONAL"}
        for k, v in sorted(expect.items()):
            ok = words.get(k) == v
            fails += not ok
            out("%-4s %s is %s (want %s)" % ("ok" if ok else "FAIL", k, words.get(k), v))
        # tamper: edit a landed record by hand; verify must fail and push must refuse nothing silently
        rp = os.path.join(UWCS, "SPR", "register.json")
        reg[2]["STATEMENT"] = reg[2]["STATEMENT"] + " (edited by hand)"
        write_json(rp, reg)
        sub = Out("verify", ["SPR"])
        code = run_cmd(T, B, "verify", ["SPR"], sub)
        ok = code == 1
        fails += not ok
        out("%-4s a record edited by hand fails verify -> exit %d (want 1)" % ("ok" if ok else "FAIL", code))
    finally:
        UWCS = keep
        shutil.rmtree(tmp, ignore_errors=True)
    out("SELFTEST: %s" % ("PASS" if fails == 0 else "FAIL (%d)" % fails))
    return 0 if fails == 0 else 1


# ---- worked examples ------------------------------------------------------------------
# The spring example's records, as the selftest pushes them. `example NAME` prints one.
EXAMPLES = {
    "push_1": [
        {
            "ID": "SPR-2",
            "CLASS": "ASSUMPTION",
            "CITES": [],
            "ASSUMPTIONS": [],
            "PROPOSITION": "At t = 0 the mass is released at x = 0.02 m.",
            "REFS": [
                "x"
            ],
            "SITUATION": "INITIAL",
            "DOF": "x",
            "VALUE": "0.02",
            "UNIT": "m",
            "AT": [
                {
                    "DOF": "t",
                    "VALUE": "0",
                    "UNIT": "s"
                }
            ]
        },
        {
            "ID": "SPR-3",
            "CLASS": "RESULT",
            "CITES": [],
            "ASSUMPTIONS": [],
            "STATEMENT": "Dividing EOM_X by m, which A_mpos makes positive, gives dv/dt = -(k_s/m) x.",
            "REGIME": "L",
            "DERIVATION": {
                "STEPS": 1,
                "TEXT": "EOM_X: m D(v,t) = -k_s x. Divide both sides by m (m > 0 by A_mpos)."
            },
            "FALSIFIER": "A measured acceleration of the mass not proportional to x with ratio -k_s/m inside the domain.",
            "FALSIFIER_CLASS": "F3",
            "DOMAIN": "|x| < x_max",
            "DOMAIN_CONDITION": "abs(x) < x_max",
            "REFS": [
                "v",
                "t",
                "k_s",
                "m",
                "x",
                "x_max"
            ],
            "EQUATION": "D(v, t) = -k_s / m * x",
            "ENTRIES": [
                "E_x",
                "E_v"
            ]
        },
        {
            "ID": "SPR-4",
            "CLASS": "RESULT",
            "CITES": [],
            "ASSUMPTIONS": [
                "SPR-2"
            ],
            "STATEMENT": "At t = 0 the displacement is x = 0.02 m, by the initial value SPR-2.",
            "REGIME": "L",
            "DERIVATION": {
                "STEPS": 1,
                "TEXT": "Read x at t = 0 from SPR-2."
            },
            "FALSIFIER": "A measured displacement at release different from 0.02 m beyond its uncertainty.",
            "FALSIFIER_CLASS": "F3",
            "QUANTITY": "E_x",
            "VALUE": "0.02",
            "UNIT": "m",
            "AT": [
                {
                    "DOF": "t",
                    "VALUE": "0",
                    "UNIT": "s"
                }
            ]
        }
    ],
    "push_2": [
        {
            "ID": "SPR-6",
            "CLASS": "VERIFICATION",
            "CITES": [],
            "ASSUMPTIONS": [],
            "REGIME": "M",
            "TARGET": "SPR-4",
            "EXTERNAL": {
                "VALUE": "0.0199",
                "U_C": "0.0002",
                "UNIT": "m",
                "QUANTITY": "E_x"
            },
            "SOURCE": "Bench measurement of the release displacement, lab notebook entry 2026-09-30 (example).",
            "AT": [
                {
                    "DOF": "t",
                    "VALUE": "0",
                    "UNIT": "s"
                }
            ],
            "DIFFERENCE": {
                "VALUE": "1e-4",
                "U": "2e-4",
                "ZETA": "5e-1"
            }
        }
    ],
    "push_bad_id": [
        {
            "ID": "SPR-9",
            "CLASS": "ASSUMPTION",
            "CITES": [],
            "ASSUMPTIONS": [],
            "PROPOSITION": "The spring is massless.",
            "REFS": []
        }
    ],
    "reproduction": {
        "CLASS": "RESULT",
        "CITES": [],
        "ASSUMPTIONS": [],
        "STATEMENT": "An independent derivation: dv/dt = -(k_s/m) x.",
        "REGIME": "L",
        "DERIVATION": {
            "STEPS": 1,
            "TEXT": "From EOM_X, divide by m."
        },
        "FALSIFIER": "As the original.",
        "FALSIFIER_CLASS": "F3",
        "REFS": [
            "v",
            "t",
            "k_s",
            "m",
            "x"
        ],
        "EQUATION": "D(v,t) = -k_s/m*x"
    },
    "verification_draft": {
        "ID": "SPR-6",
        "CLASS": "VERIFICATION",
        "CITES": [],
        "ASSUMPTIONS": [],
        "REGIME": "M",
        "TARGET": "SPR-4",
        "EXTERNAL": {
            "VALUE": "0.0199",
            "U_C": "0.0002",
            "UNIT": "m",
            "QUANTITY": "E_x"
        },
        "SOURCE": "Bench measurement of the release displacement, lab notebook entry 2026-09-30 (example).",
        "AT": [
            {
                "DOF": "t",
                "VALUE": "0",
                "UNIT": "s"
            }
        ]
    }
}


def cmd_example(T, B, args, out):
    if len(args) != 1:
        raise Stop("usage: example NAME   (draft, %s)" % ", ".join(sorted(EXAMPLES)))
    name = args[0]
    if name == "draft":
        value = read_json(os.path.join(TPC_DIR, "examples", "spring_draft.json"))
    elif name in EXAMPLES:
        value = EXAMPLES[name]
    else:
        raise Stop("no example %r; the examples are draft, %s" % (name, ", ".join(sorted(EXAMPLES))))
    out(json.dumps(value, indent=2, ensure_ascii=False))
    return 0


def _no_dupes(pairs):
    d = {}
    for k, val in pairs:
        if k in d:
            raise ValueError("duplicate key %r" % k)
        d[k] = val
    return d


COMMANDS = {"check": cmd_check, "new": cmd_new, "declare": cmd_declare, "push": cmd_push,
            "next-id": cmd_next_id, "difference": cmd_difference, "compare": cmd_compare,
            "withdraw": cmd_withdraw, "status": cmd_status, "show": cmd_show, "verify": cmd_verify,
            "report": cmd_report, "example": cmd_example, "selftest": cmd_selftest}
LOGGED = ("new", "declare", "push", "difference", "compare", "withdraw", "status", "verify", "next-id", "show")


def parse(args, spec):
    opts, pos, i = {}, [], 0
    while i < len(args):
        a = args[i]
        if a in spec:
            if spec[a] == 1:
                if i + 1 >= len(args):
                    raise Stop("%s needs a value" % a)
                opts[a] = args[i + 1]
                i += 2
            else:
                opts[a] = True
                i += 1
        elif a.startswith("--"):
            raise Stop("unknown option %s" % a)
        else:
            pos.append(a)
            i += 1
    return opts, pos


def run_cmd(T, B, cmd, args, out):
    try:
        return COMMANDS[cmd](T, B, args, out)
    except Stop as e:
        out("REFUSED: %s" % e)
        return 1
    except (OSError, ValueError) as e:
        out("REFUSED: %s: %s" % (type(e).__name__, e))
        return 2


def main(argv):
    global TPC_DIR
    if hasattr(sys.stdout, "reconfigure"):
        sys.stdout.reconfigure(errors="backslashreplace")
    argv = list(argv)
    if len(argv) >= 3 and argv[1] == "--tpc":
        TPC_DIR = os.path.abspath(argv[2])
        del argv[1:3]
    elif os.environ.get("TPM_TPC"):
        TPC_DIR = os.path.abspath(os.environ["TPM_TPC"])
    if len(argv) < 2 or argv[1] not in COMMANDS:
        print(__doc__)
        return 2
    return dispatch(argv[1], argv[2:])


def dispatch(cmd, args, quiet=False):
    out = Out(cmd, args)
    try:
        B = load_binding()
        T = load_tpc(B)
        out.bound(B)
    except Stop as e:
        out("REFUSED: %s" % e)
        if quiet:
            return 1, out.text(1)
        print(out.text(1), end="")
        return 1
    code = run_cmd(T, B, cmd, args, out)
    text = out.text(code)
    if not quiet:
        print(text, end="")
    prefix = None
    if cmd == "new":
        try:
            prefix = parse(args, {"--prefix": 1, "--initiator": 1})[0].get("--prefix")
        except Stop:
            prefix = None
    elif cmd in LOGGED and args:
        prefix = args[0]
    if prefix and os.path.isdir(uwc_dir(prefix)):
        save_log(uwc_dir(prefix), cmd, text)
    return (code, text) if quiet else code


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

# END OF FILE --- tpm_sandbox.py 0.2.0. If this line is absent, the file arrived truncated.