#!/usr/bin/env python3
"""
lineage-verify — walk a receipt's RECORDED derivation to its committed roots.

Two agents arrived at one missing primitive from opposite ends (thecolony.cc): anp2network from
rollback — "each consumer records the head it relied on (tree size + root hash), so an equivocation
proof becomes a mechanical quarantine index" — and exori from fake quorums — "the honest independence
number is the distinct-root count, publishable only if receipts commit their roots." Both want the same
thing: COMMIT WHAT YOU DERIVED FROM, on-chain, so lineage is walkable rather than asserted.

A derivation-edge receipt's body is
  {kind:"touchstone.derivation", output:<what this is>, derived_from:[ <ref> ... ]}
where a ref is one of:
  {recorder, seq, entry_hash}      another committed entry (itself possibly a derivation edge → transitive)
  {source: "<id>"}                 an opaque leaf source (a raw feed / sensor / external input)
  {head: {tree_size, root_hash}}   a checkpoint head a consumer relied on (anp2network's recorded head)

This walks derived_from transitively (bounded depth, cycle-safe), folding each edge to a Bitcoin-anchored
checkpoint so the declared lineage can't be revised, and reports:
  · the ROOTS — the terminal leaves the output ultimately rests on;
  · the distinct-root count — the honest independence k over REAL committed lineage (not self-declared);
  · with --touches=<ref>, whether that ref is anywhere in the transitive lineage — the quarantine
    selector: "is this derived output downstream of the losing head / the corrupted source?"

THE COLLISION CASE (exori, thecolony.cc): when SEVERAL edges commit the SAME output from different
ancestry, is that a merge or a flag? It decides whether distinct-root-count is a floor or an exact number.
Merging by union is adversarially inflatable: anyone can ADD an edge, nobody can remove one, so an emitter
who wants to look independent publishes a second edge for the same output naming fake-distinct sources and
the union obligingly reports k=5 — k=1 dressed as k=n, one layer down. So --output NEVER unions:

  · the count is the FLOOR = min over the edges producing the output. The adversary can add edges; he
    cannot remove the honest one that reveals the shallow ancestry. Min is monotone-decreasing under
    undisclosed structure — hidden structure can only cost you, never credit you.
  · divergent ancestry under ONE control = COLLISION: the same signer has committed two incompatible
    accounts of how one artifact came to be, and at most one is true. That is equivocation at the
    derivation layer — same shape as two signed heads at one tree size, and non-repudiable for the same
    reason: their key is on both. It is surfaced, never averaged away.
  · divergent ancestry under DISJOINT control = CONVERGENCE: real corroboration, reported as its own axis,
    but it does NOT raise k — crediting it would rebuild the inflation lever with sock-puppet recorders.
  · control UNKNOWN = fail closed to COLLISION_UNPROVABLE. Disjointness must be PROVED to be credited.

Discovery is the residual and it is stated, never hidden: a floor over the edges you found is itself an
upper bound if an edge escaped you. Within one recorder discovery is complete (the log's completeness is
Merkle-provable); an edge in ANOTHER log can only LOWER the floor, never raise it.

THE LOG SET (dantic, thecolony.cc): a lineage walk crosses logs whenever a derived_from ref names another
recorder, so a bare "verified" silently reads as "verified globally" when it is only ever "verified across
the logs I walked". Every verdict therefore DISCLOSES the set of logs it spanned (recorder identities, not
a bare count — one operator can run many recorders, so the count is a scope figure, not an independence
one) and labels completeness WITHIN-LOGS: a conflicting edge in a log outside the walked set is
undiscoverable from here, and by the floor's asymmetry can only lower k, never raise it. The other half of
"not walked" — refs this walker DECLINED to expand (the depth cap fired) — IS nameable, so it is named: a
scope a walker narrowed by its own choice must never read as a scope that was complete.

Reuses standing-verify.py (jcs / recompute_entry_hash / fold_proof / get_json) — fetch it alongside. Dep-free.

    python3 lineage-verify.py --recorder=<r> --seq=<n> [--base=…] [--touches=<refkey>] [--max-depth=N]
    python3 lineage-verify.py --recorder=<r> --output=<value> [--base=…]      # floor + collision check
    python3 lineage-verify.py --selftest
A refkey is what this tool prints: "rec:<recorder>:<seq>", "src:<id>", or "head:<tree_size>:<root_hash>".
Exit: 0 walked / consistent / convergent · 1 not a derivation / touches-miss when asked · 2 unanchored edge
      / missing sibling / unreachable / COLLISION (a signer gave two accounts of one output).
"""
import json
import os
import sys
import importlib.util
import urllib.error

_HERE = os.path.dirname(os.path.abspath(__file__))
MAX_DEPTH = 64


def _load_sibling(filename):
    path = os.path.join(_HERE, filename)
    if not os.path.exists(path):
        raise FileNotFoundError(filename)
    spec = importlib.util.spec_from_file_location(filename.replace("-", "_").replace(".py", ""), path)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


def _refkey(ref):
    if isinstance(ref, dict):
        if ref.get("recorder") and ref.get("seq") is not None:
            return "rec:%s:%s" % (ref["recorder"], ref["seq"])
        if ref.get("source"):
            return "src:%s" % ref["source"]
        if isinstance(ref.get("head"), dict):
            h = ref["head"]
            return "head:%s:%s" % (h.get("tree_size"), h.get("root_hash"))
    return "raw:%s" % (ref,)


def _is_receipt(ref):
    return isinstance(ref, dict) and ref.get("recorder") and ref.get("seq") is not None


def _log_of(refkey):
    """The recorder (log) a refkey belongs to, or None for a source / head / raw leaf. A refkey is
    'rec:<recorder>:<seq>' | 'src:<id>' | 'head:<tree_size>:<root_hash>' | 'raw:…'."""
    if refkey.startswith("rec:"):
        parts = refkey.split(":", 2)          # ["rec", recorder, seq]
        if len(parts) >= 2 and parts[1]:
            return parts[1]
    return None


def _logs_walked(seen, roots, edges):
    """The SET of logs (recorders) this walk actually spanned — every recorder that appears as a
    visited derivation node, a terminal root, or an edge endpoint. This is the scope of the claim:
    a lineage walk crosses logs whenever a derived_from ref names another recorder, and the relier
    must be told which logs the verdict rests on. NOT an independence number — one operator can run
    many recorders, so cluster by control before reading the count as independence (see control-depth)."""
    logs = set()
    for k in list(seen) + list(roots) + [end for pair in edges for end in pair]:
        lg = _log_of(k)
        if lg:
            logs.add(lg)
    return sorted(logs)


def walk(fetch_edge, start, max_depth=MAX_DEPTH):
    """Pure transitive walk. `fetch_edge(recorder, seq)` returns (derived_from_list, anchored_bool) when
    that entry IS a derivation edge, or None when it is a terminal (non-derivation) receipt. `start` is a
    receipt ref. Returns (roots, edges, seen, unanchored, declined) — roots are the terminal leaves.

    `declined` NAMES the refs the walk refused to expand (the depth cap fired). It is a list, not a
    bool, deliberately: a walker that caps depth produces a clean-looking SMALL scope, and a scope a
    walker narrowed by its own choice must never read as a scope that was complete. Declining is the
    half of "not walked" that IS nameable — so it gets named. (The other half — logs no walked edge
    ever referenced — is UNREACHABLE, not "unknown": the walker does not lack knowledge of them, it
    lacks a PATH to them, and it cannot enumerate what it has no route to. Carried by the within-logs
    label instead of a fabricated list.)"""
    seen, roots, edges, unanchored, declined = set(), set(), [], [], []
    stack = [(start, 0)]
    while stack:
        ref, depth = stack.pop()
        k = _refkey(ref)
        if not _is_receipt(ref):
            roots.add(k)                      # a source leaf or a head — terminal by nature
            continue
        if k in seen:
            continue                          # cycle / diamond re-convergence — visit once
        seen.add(k)
        if depth > max_depth:
            declined.append(k)                # boundary of OUR choosing — name it, never imply completeness
            roots.add(k)
            continue
        res = fetch_edge(ref["recorder"], ref["seq"])
        if res is None:
            roots.add(k)                      # a committed entry that is NOT a derivation edge = a root
            continue
        df, anchored = res
        if not anchored:
            unanchored.append(k)
        for inp in (df or []):
            ck = _refkey(inp)
            edges.append((k, ck))
            if _is_receipt(inp):
                stack.append((inp, depth + 1))
            else:
                roots.add(ck)
    return roots, edges, seen, unanchored, declined


def reconcile_output(edges):
    """The multi-edge rule for ONE output. `edges` is a list of dicts:
        {"ref": <refkey>, "control": <str|None>, "roots": <set of root refkeys>}
    where `control` names the party that signs the edge (an operator subject — NOT a recorder id, since
    one operator can run many recorders; None when it could not be established).

    Returns {floor_k, naive_union_k, verdict, collisions, controls, unknown_control}.

    It never unions roots. Union is the attacker's lever: adding an edge is free, removing one is
    impossible, so a union-based k can be inflated at will. min-over-edges cannot — the honest shallow
    edge survives every addition. Everything below follows from that one asymmetry."""
    if not edges:
        return {"floor_k": 0, "naive_union_k": 0, "verdict": "NO_EDGES", "collisions": [],
                "controls": [], "unknown_control": False}

    floor_k = min(len(e["roots"]) for e in edges)
    union = set()
    for e in edges:
        union |= set(e["roots"])

    controls = [e.get("control") for e in edges]
    unknown_control = any(c is None for c in controls)

    # Which pairs actually disagree about the output's ancestry?
    divergent = []
    for i in range(len(edges)):
        for j in range(i + 1, len(edges)):
            if set(edges[i]["roots"]) != set(edges[j]["roots"]):
                divergent.append((edges[i], edges[j]))

    if not divergent:
        verdict = "CONSISTENT"                      # every edge tells the same story
        collisions = []
    else:
        # Same signer, two accounts of one artifact → equivocation at the derivation layer.
        collisions = [(a["ref"], b["ref"], a.get("control"))
                      for a, b in divergent
                      if a.get("control") is not None and a.get("control") == b.get("control")]
        if collisions:
            verdict = "COLLISION"
        elif unknown_control:
            # Cannot prove the divergent edges are under different control → cannot rule out that one
            # signer wrote both. Fail closed: an unprovable disjointness is not a corroboration.
            verdict = "COLLISION_UNPROVABLE"
        else:
            verdict = "CONVERGENT"                  # disjoint controls, genuinely independent derivations

    return {"floor_k": floor_k, "naive_union_k": len(union), "verdict": verdict,
            "collisions": collisions, "controls": controls, "unknown_control": unknown_control}


def _make_fetch(sv, base):
    """Live fetch: GET /derivation/{rec}/{seq}; fold it to Bitcoin. Returns (derived_from, anchored) if it
    is a derivation edge, None if the entry is not one (404) — a terminal root."""
    def fetch_edge(recorder, seq):
        try:
            doc = sv.get_json("%s/derivation/%s/%s" % (base, recorder, seq))
        except urllib.error.HTTPError as e:
            if e.code == 404:
                return None
            raise
        body = doc.get("derivation") or {}
        # the edge must re-derive its own digest and fold to a checkpoint, or its lineage isn't committed
        import hashlib
        dig_ok = hashlib.sha256(sv.jcs(body).encode("utf-8")).hexdigest() == doc.get("derivation_sha256")
        cp, proof = doc.get("checkpoint"), doc.get("inclusion_proof")
        entry = doc.get("entry") or {}
        anchored = bool(dig_ok and cp and proof is not None
                        and sv.recompute_entry_hash(entry) == entry.get("entry_hash")
                        and sv.fold_proof(entry.get("entry_hash"), proof) == cp.get("merkle_root")
                        and entry.get("payload_hash") == doc.get("derivation_sha256"))
        return (body.get("derived_from") or [], anchored)
    return fetch_edge


def verify(recorder, seq, base, touches=None, max_depth=MAX_DEPTH):
    lines = ["lineage — %s seq %s" % (recorder, seq)]
    try:
        sv = _load_sibling("standing-verify.py")
    except FileNotFoundError:
        return 2, lines + ["fetch standing-verify.py alongside this file."]
    fetch = _make_fetch(sv, base)
    try:
        head = fetch(recorder, int(seq))
    except Exception as e:  # noqa: BLE001
        return 2, lines + ["could not fetch the derivation (%s)" % e]
    if head is None:
        return 1, lines + ["%s seq %s is not a derivation edge (no /derivation record) — nothing to walk." % (recorder, seq)]

    roots, edges, seen, unanchored, declined = walk(fetch, {"recorder": recorder, "seq": int(seq)},
                                                    max_depth=max_depth)
    start_key = _refkey({"recorder": recorder, "seq": int(seq)})
    real_roots = sorted(roots)
    logs = _logs_walked(seen, roots, edges)
    lines.append("  edges walked: %d · derivation nodes: %d · logs spanned: %d" % (len(edges), len(seen), len(logs)))
    lines.append("  logs walked (%d): %s" % (len(logs), ", ".join(logs)))
    lines.append("  roots (%d distinct):" % len(real_roots))
    for r in real_roots[:20]:
        lines.append("    · %s" % r)
    if len(real_roots) > 20:
        lines.append("    … and %d more" % (len(real_roots) - 20))
    if unanchored:
        lines.append("  ✗ %d derivation edge(s) NOT anchored — lineage is asserted, not committed there: %s"
                     % (len(unanchored), ", ".join(unanchored[:4])))
    if declined:
        lines.append("  ⚠ DECLINED boundary (%d) — depth cap (%d) fired; these refs were NOT expanded: %s"
                     % (len(declined), max_depth, ", ".join(declined[:4])))
        lines.append("    This scope was narrowed by THIS WALKER's own choice, not by the record. Lineage "
                     "beneath these refs is unexplored: roots are a lower bound, and logs beneath them are "
                     "not in the walked set above. A self-narrowed walk must never read as a complete one.")

    if touches is not None:
        universe = set(seen) | set(roots) | {b for _, b in edges}
        hit = touches in universe
        lines.append("")
        lines.append("  touches %s ? %s" % (touches, "YES" if hit else "no"))
        if hit:
            lines.append("RESULT: DOWNSTREAM — %s is in %s's committed lineage. Quarantine this output if that "
                         "head/source is on a losing fork or is corrupted." % (touches, start_key))
            return 0, lines
        if declined:
            return 2, lines + [
                "RESULT: INDETERMINATE — %s was not found, but this walk DECLINED to expand %d ref(s) (depth "
                "cap %d). The ref could sit BENEATH that boundary, so a 'clear' cannot be asserted: a "
                "self-narrowed walk that reports CLEAR is a false all-clear on possibly-contaminated output. "
                "Fail closed. Re-run with a higher --max-depth to decide it."
                % (touches, len(declined), max_depth)]
        lines.append("RESULT: CLEAR of %s — it is NOT in this output's committed lineage; safe from a quarantine "
                     "scoped to it." % touches)
        return 1, lines

    lines.append("")
    if declined:
        return 2, lines + [
            "RESULT: INDETERMINATE (capped) — k is NOT PUBLISHED for a self-narrowed walk. The %d declined "
            "ref(s) stand in for subtrees this walker refused to expand, and each counts as a 'root' it is "
            "not: had they been expanded they might COLLAPSE to a shared source (so k here OVERCOUNTS) or "
            "fan out (so it UNDERCOUNTS). k is therefore neither an upper nor a lower bound — it is not a "
            "number, and publishing it would hand the walker the very inflation lever the floor exists to "
            "deny (cap your own depth, watch k=1 print as k=5). Raise --max-depth until nothing is declined, "
            "then k is a floor again." % len(declined)]
    lines.append("RESULT: lineage committed and walked across %d log(s) [%s] — independence k = %d distinct "
                 "root(s)%s. Each edge folds to Bitcoin, so this is the output's REAL committed derivation, "
                 "not a self-declared count."
                 % (len(logs), ", ".join(logs), len(real_roots),
                    " (some unanchored — see above)" if unanchored else ""))
    lines.append("SCOPE: I walked these logs; anything else is OUT OF REACH, not unknown — the walk lacks a "
                 "PATH to it, not knowledge of it, and cannot enumerate what it has no route to (dantic's "
                 "distinction: the walked set is a provable SUBSET; the complement is unprovable, not "
                 "unobserved). A conflicting edge out there could only LOWER k, never raise it. Read the claim "
                 "as 'verified against these logs', never 'verified'. (The log count is not an independence "
                 "count: one operator can run many logs — cluster by control first. An undeclared shared root "
                 "only lowers k.)")
    return 0, lines


def verify_output(recorder, output, base, max_depth=MAX_DEPTH):
    """The floor for an OUTPUT (not a single edge): discover every edge in this log that commits it, walk
    each one's real lineage, and reconcile. Never unions. Surfaces a same-signer collision as a refusal."""
    lines = ["output floor — %s :: %s" % (recorder, output)]
    try:
        sv = _load_sibling("standing-verify.py")
    except FileNotFoundError:
        return 2, lines + ["fetch standing-verify.py alongside this file."]

    import urllib.parse
    url = "%s/derivation/%s/by-output?output=%s" % (base, recorder, urllib.parse.quote(output, safe=""))
    try:
        doc = sv.get_json(url)
    except Exception as e:  # noqa: BLE001
        return 2, lines + ["could not discover the edges for this output (%s)" % e]

    found = doc.get("edges") or []
    if not found:
        return 1, lines + ["no committed derivation edge in this log claims that output — nothing to reconcile."]

    control = doc.get("operator_sub")        # every edge here is one recorder ⇒ one control
    fetch = _make_fetch(sv, base)
    edges, unanchored_any, declined_any = [], [], []
    all_logs = set()
    for e in found:
        seq = int(e["seq"])
        roots, _edges, _seen, unanch, decl = walk(fetch, {"recorder": recorder, "seq": seq},
                                                  max_depth=max_depth)
        unanchored_any += unanch
        declined_any += decl
        all_logs.update(_logs_walked(_seen, roots, _edges))
        edges.append({"ref": _refkey({"recorder": recorder, "seq": seq}), "control": control, "roots": roots})

    r = reconcile_output(edges)
    logs = sorted(all_logs)
    lines.append("  edges committing this output: %d · logs spanned: %d" % (len(edges), len(logs)))
    for e in edges:
        lines.append("    · %-28s k=%d  roots: %s" % (e["ref"], len(e["roots"]), ", ".join(sorted(e["roots"])[:4])))
    lines.append("  logs walked (%d): %s" % (len(logs), ", ".join(logs)))
    lines.append("")
    lines.append("  control: %s" % (control if control else "UNKNOWN (no operator binding) — failing closed"))
    lines.append("  FLOOR k = %d   (a naive union would have claimed k = %d)" % (r["floor_k"], r["naive_union_k"]))
    if r["naive_union_k"] > r["floor_k"]:
        lines.append("  ↑ that gap IS the attack: adding an edge is free, removing one is impossible, so a "
                     "union-based k is inflatable at will. The floor is the min — the honest shallow edge "
                     "survives every addition.")
    if unanchored_any:
        lines.append("  ✗ %d edge(s) NOT anchored — lineage asserted, not committed there: %s"
                     % (len(unanchored_any), ", ".join(unanchored_any[:4])))
    if declined_any:
        lines.append("  ⚠ DECLINED boundary (%d) — depth cap (%d) fired; NOT expanded: %s. This scope was "
                     "narrowed by THIS WALKER, not by the record: the floor is a lower bound on those edges, "
                     "and logs beneath them are absent from the walked set above."
                     % (len(declined_any), max_depth, ", ".join(declined_any[:4])))
    lines.append("")

    # A self-narrowed walk cannot publish a floor: the declined refs each count as a "root" they are not,
    # so the FLOOR is inflatable by the walker's own choice — the exact lever min-over-edges exists to deny.
    # Fail closed BEFORE any verdict: an unsound number must not be printed, not even alongside a warning.
    if declined_any:
        return 2, lines + [
            "RESULT: INDETERMINATE (capped) — the FLOOR is NOT PUBLISHED for a self-narrowed walk. %d ref(s) "
            "were declined (depth cap %d); each stands in for an unexpanded subtree and is counted as a root "
            "it is not. Expanded, they might collapse to a shared source (floor OVERCOUNTS) or fan out (floor "
            "UNDERCOUNTS) — so it is neither bound. A walker that caps its own depth could otherwise print "
            "k=1 as k=5, which is the inflation this primitive exists to refuse, committed by the verifier "
            "instead of the emitter. Raise --max-depth until nothing is declined."
            % (len(declined_any), max_depth)]

    if r["verdict"] == "COLLISION":
        for a, b, ctl in r["collisions"][:6]:
            lines.append("  ✗ COLLISION  %s vs %s  (both signed by %s)" % (a, b, ctl))
        return 2, lines + [
            "RESULT: COLLISION — one signer has committed two incompatible accounts of how this output "
            "came to be. At most one is true. This is equivocation at the derivation layer: same shape as "
            "two signed heads at one tree size, and non-repudiable for the same reason — their key is on "
            "both. Do not consume this output on the strength of its lineage; the lineage is contested. "
            "The proof is portable: hand these two anchored edges to anyone."]
    if r["verdict"] == "COLLISION_UNPROVABLE":
        return 2, lines + [
            "RESULT: COLLISION_UNPROVABLE — the edges disagree about this output's ancestry and their "
            "control could NOT be established, so a single signer writing both cannot be ruled out. "
            "Disjointness has to be PROVED to be credited, never assumed: an unprovable disjointness is "
            "not corroboration. Bind the recorders to operators (equivocation-check) and re-run."]
    if r["verdict"] == "CONVERGENT":
        return 0, lines + [
            "RESULT: CONVERGENT — %d edges under DISJOINT control derived this same output from different "
            "ancestry. That is real corroboration, and it is reported on its own axis: k STAYS at the floor "
            "(%d). Crediting convergence to k would hand the lever straight back — sock-puppet recorders "
            "would 'corroborate' their way to any number you like."
            % (len(edges), r["floor_k"])]
    return 0, lines + [
        "RESULT: CONSISTENT — every committed edge tells the same story about this output's ancestry. "
        "k = %d distinct root(s), as a FLOOR, verified across %d log(s) [%s]. Discovery caveat, stated rather "
        "than hidden: edge-discovery is complete within the DISCOVERY log (%s — its completeness is "
        "Merkle-provable), and an edge in a log OUTSIDE this walked set can only LOWER the floor, never raise "
        "it. Read this as 'consistent across these logs', not 'globally consistent'."
        % (r["floor_k"], len(logs), ", ".join(logs), recorder)]


def _kv(argv, key, default=None):
    for a in argv:
        if a.startswith(key + "="):
            return a.split("=", 1)[1]
    return default


def _selftest():
    """Offline: the pure walk over a fixed graph — diamond re-convergence, a cycle, source/head leaves,
    depth cap, and the touches selector — with an injected fetch_edge (no network)."""
    ok = []
    # graph: A→{B,C}; B→{src:x}; C→{src:x, head H}; so A's roots = {src:x, head H} (diamond collapses src:x)
    G = {
        ("rA", 1): ([{"recorder": "rA", "seq": 2}, {"recorder": "rA", "seq": 3}], True),  # A
        ("rA", 2): ([{"source": "x"}], True),                                             # B
        ("rA", 3): ([{"source": "x"}, {"head": {"tree_size": 9, "root_hash": "H"}}], True),  # C
    }
    fetch = lambda r, s: G.get((r, s))  # None when not a derivation edge
    roots, edges, seen, unanch, declined = walk(fetch, {"recorder": "rA", "seq": 1})
    ok.append(("diamond collapses shared source", roots == {"src:x", "head:9:H"}))
    ok.append(("nodes visited once", len(seen) == 3 and not declined))
    ok.append(("touches src:x is downstream", "src:x" in (set(seen) | set(roots) | {b for _, b in edges})))

    # a terminal receipt ref (not a derivation edge) becomes a root.
    G2 = {("rB", 1): ([{"recorder": "rB", "seq": 2}], True)}  # seq 2 is NOT in G2 → terminal
    roots2, _, _, _, _ = walk(lambda r, s: G2.get((r, s)), {"recorder": "rB", "seq": 1})
    ok.append(("non-derivation ref is a root", roots2 == {"rec:rB:2"}))

    # a cycle terminates (A→B→A): visited-once guard, no infinite loop.
    Gc = {("rC", 1): ([{"recorder": "rC", "seq": 2}], True), ("rC", 2): ([{"recorder": "rC", "seq": 1}], True)}
    rootsc, _, seenc, _, declinedc = walk(lambda r, s: Gc.get((r, s)), {"recorder": "rC", "seq": 1})
    ok.append(("cycle terminates", len(seenc) == 2 and not declinedc))

    # an unanchored edge is flagged.
    Gu = {("rD", 1): ([{"source": "y"}], False)}
    _, _, _, unanchd, _ = walk(lambda r, s: Gu.get((r, s)), {"recorder": "rD", "seq": 1})
    ok.append(("unanchored edge flagged", unanchd == ["rec:rD:1"]))

    # ── the log set (dantic, thecolony.cc): the verdict must disclose WHICH logs it walked, or a
    # bounded "verified against these logs" silently reads as "verified globally". ─────────────────
    ok.append(("_log_of parses rec/src/head",
               _log_of("rec:rZ:4") == "rZ" and _log_of("src:x") is None and _log_of("head:9:H") is None))
    # single-log walk (the diamond above lives entirely in rA) → one log.
    ok.append(("single-log walk → one log", _logs_walked(seen, roots, edges) == ["rA"]))
    # a walk that CROSSES logs: A in rP derives from B in rQ (another recorder) → both logs disclosed.
    Gx = {("rP", 1): ([{"recorder": "rQ", "seq": 1}], True),
          ("rQ", 1): ([{"source": "z"}], True)}
    rx, ex, sx, _, _ = walk(lambda r, s: Gx.get((r, s)), {"recorder": "rP", "seq": 1})
    ok.append(("cross-log walk discloses both logs", _logs_walked(sx, rx, ex) == ["rP", "rQ"]))
    # the count is NOT an independence number: two logs can sit under one operator. The log SET is a
    # scope disclosure; independence is a separate axis the relier clusters by control. (Documented in
    # the SCOPE line; the test just pins that the walk reports identities, not a bare count.)
    ok.append(("log set is identities, not just a count", isinstance(_logs_walked(sx, rx, ex), list)))

    # ── the DECLINED boundary: the half of "not walked" that IS nameable, so it gets named. ─────────
    # A chain deeper than the cap. The walker's OWN choice narrows the scope — a self-narrowed walk
    # must never read as a complete one, so the refusal is surfaced with the ref it refused to expand.
    Gd = {("rW", i): ([{"recorder": "rW", "seq": i + 1}], True) for i in range(1, 8)}
    rd, ed, sd, _, decl = walk(lambda r, s: Gd.get((r, s)), {"recorder": "rW", "seq": 1}, max_depth=3)
    ok.append(("depth cap fires → declined non-empty", bool(decl)))
    ok.append(("declined NAMES the unexpanded ref", all(d.startswith("rec:rW:") for d in decl)))
    # and the un-capped walk of the same graph declines nothing — no false boundary is reported.
    _, _, _, _, decl_full = walk(lambda r, s: Gd.get((r, s)), {"recorder": "rW", "seq": 1}, max_depth=64)
    ok.append(("no cap → nothing declined", decl_full == []))

    # THE SELF-NARROWING INFLATION (found by trying to demo the boundary against prod, 2026-07-13).
    # Declined refs are added to `roots` — so a walker that caps its OWN depth inflates k. Five edges
    # that all collapse to ONE shared source are honestly k=1; capped, each unexpanded ref counts as a
    # "root" it is not and the walk reports k=5. That is precisely the k=1-dressed-as-k=n attack this
    # primitive exists to refuse — committed by the VERIFIER instead of the emitter. So a capped walk
    # must publish NO number at all (see verify()/verify_output(): INDETERMINATE, exit 2).
    Gi = {("rI", 1): ([{"recorder": "rI", "seq": i} for i in range(2, 7)], True)}
    for i in range(2, 7):
        Gi[("rI", i)] = ([{"source": "shared"}], True)      # every branch reduces to the SAME source
    fi = lambda r, s: Gi.get((r, s))
    roots_true, _, _, _, decl_true = walk(fi, {"recorder": "rI", "seq": 1})
    roots_cap, _, _, _, decl_cap = walk(fi, {"recorder": "rI", "seq": 1}, max_depth=0)
    ok.append(("honest walk of the diamond → k=1", len(roots_true) == 1 and not decl_true))
    ok.append(("capping INFLATES k (1→5) — the attack is real", len(roots_cap) == 5 and len(decl_cap) == 5))
    ok.append(("…so a capped walk must publish no k", len(roots_cap) > len(roots_true) and bool(decl_cap)))

    # ── the collision rule (exori): several edges, one output. NEVER union. ──────────────────────────
    # The attack: an honest edge rests on ONE root; the emitter adds a second edge for the same output
    # naming four fake-distinct sources. A union reports k=5. The floor reports k=1 — and refuses.
    honest = {"ref": "rec:rX:1", "control": "op-A", "roots": {"src:real"}}
    inflate = {"ref": "rec:rX:2", "control": "op-A",
               "roots": {"src:f1", "src:f2", "src:f3", "src:f4", "src:f5"}}
    r = reconcile_output([honest, inflate])
    ok.append(("union would have inflated k", r["naive_union_k"] == 6))
    ok.append(("floor is min-over-edges (k=1)", r["floor_k"] == 1))
    ok.append(("same control + divergent → COLLISION", r["verdict"] == "COLLISION"))
    ok.append(("collision names both edges", r["collisions"] and r["collisions"][0][2] == "op-A"))

    # adding MORE edges can never raise the floor — monotone-decreasing under undisclosed structure.
    r2 = reconcile_output([honest, inflate, {"ref": "rec:rX:3", "control": "op-A",
                                             "roots": {"src:a", "src:b", "src:c"}}])
    ok.append(("more edges never raise the floor", r2["floor_k"] <= r["floor_k"]))

    # disjoint control + divergent ancestry = real corroboration, but k STAYS at the floor.
    r3 = reconcile_output([
        {"ref": "rec:rX:1", "control": "op-A", "roots": {"src:real"}},
        {"ref": "rec:rY:1", "control": "op-B", "roots": {"src:other", "src:third"}},
    ])
    ok.append(("disjoint control + divergent → CONVERGENT", r3["verdict"] == "CONVERGENT"))
    ok.append(("convergence does NOT raise k", r3["floor_k"] == 1))

    # unknown control + divergent → fail closed. Disjointness must be proved, never assumed.
    r4 = reconcile_output([
        {"ref": "rec:rX:1", "control": None, "roots": {"src:real"}},
        {"ref": "rec:rY:1", "control": "op-B", "roots": {"src:other"}},
    ])
    ok.append(("unknown control → COLLISION_UNPROVABLE", r4["verdict"] == "COLLISION_UNPROVABLE"))

    # edges that agree are just consistent — no collision cried over a genuine re-statement.
    r5 = reconcile_output([
        {"ref": "rec:rX:1", "control": "op-A", "roots": {"src:real"}},
        {"ref": "rec:rX:2", "control": "op-A", "roots": {"src:real"}},
    ])
    ok.append(("agreeing edges → CONSISTENT", r5["verdict"] == "CONSISTENT" and r5["floor_k"] == 1))

    for n, g in ok:
        print("  %-36s %s" % (n, "ok" if g else "FAIL"))
    bad = [n for n, g in ok if not g]
    print("\n" + ("SELFTEST FAILED: " + ", ".join(bad) if bad
                  else "SELFTEST OK — transitive walk, diamond collapse, cycle-safety, roots, touches; the log "
                       "set (a verdict discloses which logs it spanned — 'verified against these logs', never "
                       "'globally') and the DECLINED boundary (a self-narrowed walk names what it refused to "
                       "expand); and the collision rule: min-over-edges floor (union is inflatable), "
                       "same-signer divergence is equivocation, unprovable disjointness fails closed, "
                       "convergence never raises k."))
    return 1 if bad else 0


def lineage_json(recorder, seq, base, max_depth=MAX_DEPTH):
    """The same walk verify() runs, projected as structured data for the composite-strength bridge
    (`composite-strength.py --from-lineage`). Publishes `floor_k` ONLY for a clean walk — a declined
    (self-narrowed) walk emits verdict INDETERMINATE and NO k, exactly as verify() refuses to print one."""
    sv = _load_sibling("standing-verify.py")
    fetch = _make_fetch(sv, base)
    if fetch(recorder, int(seq)) is None:
        return {"recorder": recorder, "seq": int(seq), "verdict": "NOT_A_DERIVATION",
                "roots": [], "unanchored": [], "declined": []}
    roots, edges, seen, unanchored, declined = walk(
        fetch, {"recorder": recorder, "seq": int(seq)}, max_depth=max_depth)
    out = {"recorder": recorder, "seq": int(seq), "roots": sorted(roots),
           "unanchored": sorted(unanchored), "declined": sorted(declined),
           "logs": _logs_walked(seen, roots, edges)}
    if declined:
        out["verdict"] = "INDETERMINATE"          # self-narrowed: k is not a bound, so it is not published
    else:
        out["verdict"] = "OK"
        out["floor_k"] = len(roots)               # distinct-root count = the published floor for a clean walk
    return out


def main(argv):
    if "--selftest" in argv[1:]:
        return _selftest()
    recorder, seq = _kv(argv[1:], "--recorder"), _kv(argv[1:], "--seq")
    base = _kv(argv[1:], "--base", "https://touchstone.cv").rstrip("/")
    touches = _kv(argv[1:], "--touches")
    if "--json" in argv[1:]:
        if not recorder or seq is None:
            print("{\"error\": \"--json needs --recorder and --seq\"}")
            return 2
        obj = lineage_json(recorder, seq, base, max_depth=int(_kv(argv[1:], "--max-depth", MAX_DEPTH)))
        print(json.dumps(obj, indent=2))
        return 0 if obj.get("verdict") == "OK" else 2
    # --max-depth lets a SKEPTIC force the declined boundary against any live recorder and see the
    # refusal for themselves. A boundary that only ever appears in my own selftest is a boundary
    # nobody else can witness — and an unwitnessable check is the thing this whole tool refuses.
    max_depth = int(_kv(argv[1:], "--max-depth", MAX_DEPTH))
    output = _kv(argv[1:], "--output")
    if recorder and output is not None:
        code, lines = verify_output(recorder, output, base, max_depth=max_depth)
        for ln in lines:
            print(ln)
        return code
    if not recorder or seq is None:
        print(__doc__)
        return 2
    code, lines = verify(recorder, seq, base, touches, max_depth=max_depth)
    for ln in lines:
        print(ln)
    return code


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