#!/usr/bin/env python3
"""
scope-verify — a verifier that signs what it did NOT check, so "valid" can't smuggle in a promise.

anp2network's point (thecolony.cc): "A verifier should sign the checks it skipped as well as the ones
it ran. Otherwise 'valid' becomes a social promise smuggled into a narrow integrity result. Null has to
mean 'not exercised,' and omission has to be unforgeable." A bare VALID silently implies complete, or
unique, or fresh, when it may have checked only one of those. This makes the SCOPE of a verification a
signed, checkable object.

A scope manifest is
  {kind:"touchstone.scope/1", subject, check_set, checks:[{check, status, tier?}], verifier, at?, sig}
where status is pass | fail | skipped, and the checks account for the FULL canonical check-set named by
check_set (below). Omission is unforgeable at two layers: the signature covers the whole checks list (flip
a status or drop an entry → the sig breaks), and a canonical check MISSING from the list is an INCOMPLETE
manifest, not a silent pass — a verifier can't narrow its claim by simply not listing what it skipped.

This tool VERIFIES a manifest (dep-free) and, with the [sign] path (PyNaCl), MINTS one. It never reduces
a manifest to "valid": it reports exactly what was checked, what was skipped, and what failed, and leaves
the gating to the relier.

THE RELIER PINS THE SET (anp2network's push, closing the narrowing attack one level up): the canonical
check-set lives in the verifier, so two verifiers can each emit validly signed, complete-looking manifests
against DIFFERENT canonical sets — nobody can drop a field any more, but a verifier can still pick a
smaller set and sign it honestly. The fix is relier-side: pass --expect-set=<name> and a manifest whose
check_set is anything else — smaller, larger, foreign, or absent — is INCOMPLETE, exit 2, never silent.
Same additive-only contract as the tier registry: under a pinned set, EXTRA checks are fine (a verifier
may exceed the floor); a different set string is not (a verifier may not redefine it). Reliers should
always pin; the unpinned mode only attests that SOME recognized set was fully accounted.

CONTENT-ADDRESS THE SET (closes the residual the name-pin can't reach — anp2network's own throttle-thread
rule, one floor up): --expect-set pins a *name*, but the name's MEANING — its member list — lives in each
verifier's code. Two verifiers whose CHECK_SETS["touchstone-receipt/1"] have drifted apart each emit a
validly signed, "fully accounted" manifest under the SAME name against DIFFERENT member lists, and a name
pin accepts both — the drift moved one floor up and went quiet. So the set must travel as a HASH, not a
name: a manifest carries `check_set_hash` = sha256 over the JCS of {kind, check_set, sorted(members)}, and
because it's a signed field, a verifier can't lie about which universe it accounted for. This verifier
recomputes that commitment from its OWN definition of the named set; a manifest whose `check_set_hash`
disagrees is SET_DRIFT (same name, different set — caught here, invisible to a name pin). And a relier can
pin the universe itself with --expect-set-hash=<sha256:…>: a manifest committing any other set-hash, or
none, is INCOMPLETE. The name says what the set is called; the hash says what it contains. Pin the hash.

ANCHOR THE SET (closes backdating — the last floor): content-addressing catches a set that DIFFERS,
but not one minted LATER. The honest and the shrunk set both commit to some hash; neither carries a date.
So the canonical check-sets are anchored on Touchstone (server-side `app:scope:anchor`): each is recorded
as an entry whose body is exactly {kind:"touchstone.scope.set/1", check_set, sorted(members)}. Because a
Touchstone payload_hash is itself sha256(JCS(body)), that entry's payload_hash EQUALS the check_set_hash
this tool prints — the anchor and the verifier commit to the same bytes, no second scheme. After a
checkpoint the entry folds to Bitcoin, giving the set a not-before. Confirm a pinned set is anchored by
finding an entry whose payload_hash equals the digest --set-hash prints, then folding it with the sibling
entry verifier (the checkpoint reaches Bitcoin once its OpenTimestamps proof confirms — the usual lag):
    python3 scope-verify.py --set-hash=touchstone-receipt/1        # -> sha256:<hex> == an anchored entry
    python3 beacon-verify.py <.well-known/touchstone/checkpoints/…/entry/SEQ>

FRESHNESS (closes decay — the axis a hash can't reach): content-addressing pins WHICH checks, not WHEN
the set was authored, and a pinned set decays as the world adds checkable things it doesn't name. The
producer never has to narrow the set — it only has to wait (anp2network). So each canonical set declares
`valid_as_of` (verifier-side metadata, NOT in the commitment, so hashes/anchors are unchanged), an
optional `expires_at`, and an optional `supersedes`. A relier passes --max-age=<90d> and a set older than
that — or past its expiry, or (with --reject-superseded) replaced by a newer set — is STALE_SET /
SUPERSEDED: a checkable rejection, not a silent "still probably covers it." An undeclared age fails closed.

    python3 scope-verify.py --verify <file|-|url> [--expect-set=…] [--expect-set-hash=sha256:…] [--expect-set-anchor=<entry-url>] [--max-age=90d] [--reject-superseded]
      # --expect-set-anchor binds the manifest's set to a PUBLISHED Touchstone entry (payload_hash ==
      # check_set_hash): proves the set is really recorded, not just matching your local copy, and once
      # checkpointed gives it a Bitcoin not-before. Hash-mismatch → ANCHOR_MISMATCH; unreachable → fail.
    python3 scope-verify.py --set-hash=touchstone-receipt/1     # print the content commitment to pin
    python3 scope-verify.py --sign --key-file=<{seed_b64}> --subject=<s> --pass=integrity --skip=uniqueness,freshness
    python3 scope-verify.py --selftest
Exit: 0 = signature valid + canonical set fully accounted (scope attested; matching any pin, and fresh
      under --max-age) · 1 = a load-bearing check FAILED in the attested scope · 2 = forged signature /
      incomplete (a canonical check unaccounted, check_set ≠ the pinned name, or check_set_hash ≠ the
      pinned hash) / set drift (committed hash ≠ this verifier's definition) / STALE_SET (expired or past
      --max-age) / SUPERSEDED / unknown set / malformed.
"""
import os
import re
import sys
import json
import hashlib
import datetime
import importlib.util

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

# The canonical check-sets. A manifest names one, and MUST account for every check in it (missing → a
# silent omission, rejected). The definition lives here, in code — a check-set in a spec but not a
# verifier is testimony about coverage that nobody enforces. `touchstone-receipt/1` is the applicable set
# for a Touchstone disclosure: the four axes of the primitive family.
# A canonical check-set carries its members AND its validity in time. Content-addressing pins WHICH
# checks; it says nothing about WHEN the set was authored — and a pinned set decays as the world adds
# checkable things it doesn't name (anp2network). So each set declares `valid_as_of` (when it was last
# authored/reviewed), an optional `expires_at`, and an optional `supersedes`. These are verifier-side
# metadata, deliberately NOT folded into check_set_commitment — the commitment stays members-only, so
# existing hashes and anchors are unchanged; freshness is a relier policy checked against the set's
# declared age, not a new content-hash. A bare list is still accepted (treated as no-time).
CHECK_SETS = {
    "touchstone-receipt/1": {
        "checks": ["integrity", "existence", "uniqueness", "completeness"],
        "valid_as_of": "2026-07-13",
        "expires_at": None,
        "supersedes": None,
    },
}
STATUSES = {"pass", "fail", "skipped"}


def _members(entry):
    """The check list for a CHECK_SETS entry, tolerating both the dict form and a bare list."""
    return entry["checks"] if isinstance(entry, dict) else entry


def _meta(name):
    """Full metadata for a named set: checks + validity fields. A bare list defaults to no-time."""
    e = CHECK_SETS.get(name)
    if e is None:
        return None
    if isinstance(e, dict):
        return {"checks": e["checks"], "valid_as_of": e.get("valid_as_of"),
                "expires_at": e.get("expires_at"), "supersedes": e.get("supersedes")}
    return {"checks": e, "valid_as_of": None, "expires_at": None, "supersedes": None}


def _utcnow():
    return datetime.datetime.now(datetime.timezone.utc)


def _parse_ts(s):
    """Parse an ISO date/datetime to an aware UTC datetime, or None."""
    if not s:
        return None
    try:
        dt = datetime.datetime.fromisoformat(str(s).strip().replace("Z", "+00:00"))
        return dt if dt.tzinfo else dt.replace(tzinfo=datetime.timezone.utc)
    except Exception:
        return None


def _parse_age(s):
    """Parse a coverage window like 90d / 12h / 2w / 30m / 45s to a timedelta, or None."""
    m = re.fullmatch(r"\s*(\d+)\s*([smhdw])\s*", s or "")
    if not m:
        return None
    n, unit = int(m.group(1)), m.group(2)
    return {"s": datetime.timedelta(seconds=n), "m": datetime.timedelta(minutes=n),
            "h": datetime.timedelta(hours=n), "d": datetime.timedelta(days=n),
            "w": datetime.timedelta(weeks=n)}[unit]


def check_set_commitment(sv, name, members):
    """Content-address a named check-set: sha256 over the JCS of its name bound to its sorted members.

    This is the object a relier pins with --expect-set-hash, and the value a minted manifest carries as
    `check_set_hash`. Binding the NAME into the preimage makes it a commitment to the name→members
    mapping, so drift (same name, different members) yields a different hash. Members are sorted, so the
    commitment is order-independent — it addresses the set, not a listing of it."""
    body = {"kind": "touchstone.scope.set/1", "check_set": name, "checks": sorted(set(members))}
    return "sha256:" + hashlib.sha256(sv.jcs(body).encode("utf-8")).hexdigest()


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 _signing_bytes(sv, manifest):
    """The exact bytes the verifier signs — the manifest minus its own `sig`, JCS-canonical."""
    return sv.jcs({k: v for k, v in manifest.items() if k != "sig"}).encode("utf-8")


def verify_manifest(sv, manifest, expect_set=None, expect_set_hash=None,
                    now=None, max_age=None, reject_superseded=False, expect_set_anchor=None):
    """Returns (exit_code, verdict, lines). Refuses to reduce a manifest to 'valid': reports the scope.

    expect_set: the relier's pinned check-set NAME. A validly-signed manifest against any OTHER set — a
    verifier-chosen smaller set, a foreign set, or none at all — is INCOMPLETE, never silent. The
    signature says "this verifier accounted for the set it chose"; only the pin says the set it chose
    is the one YOUR decision needs.

    expect_set_hash: the relier's pinned check-set CONTENT (sha256:…). Strictly stronger than the name
    pin — it survives a verifier whose definition of the named set has drifted, because it addresses the
    member list itself. A manifest committing a different set-hash, or none, is INCOMPLETE.

    max_age (timedelta) / reject_superseded (bool): freshness policy. Content-addressing pins WHICH
    checks but not WHEN the set was authored, and a pinned set decays as the world adds checkable things
    it doesn't name. If the set is past its `expires_at`, or older than `max_age`, or a successor set
    exists and the relier rejects superseded coverage, the result is STALE_SET / SUPERSEDED — a checkable
    rejection, not a silent "still probably covers it." `now` overrides the clock (for tests)."""
    lines = []
    if not isinstance(manifest, dict) or manifest.get("kind") != "touchstone.scope/1":
        return 2, "MALFORMED", ["not a touchstone.scope/1 manifest."]

    # 1. the signature — covers the whole checks list, so a flipped status or a dropped entry breaks it.
    sig_ok = sv.ed_verify(manifest.get("verifier", ""), _signing_bytes(sv, manifest), manifest.get("sig", ""))
    lines.append("  %s signature (verifier %s…)" % ("✓" if sig_ok else "✗", (manifest.get("verifier") or "")[:16]))
    if sig_ok is not True:
        return 2, "FORGED", lines + ["the manifest is not signed by the key it names — do not trust its scope claim."]

    # 2. the relier's pin — a smaller set signed honestly is the residual narrowing attack, and it can
    #    only be closed on this side: the verifier picked its set; only the relier knows the needed one.
    cset = manifest.get("check_set")
    if expect_set is not None and cset != expect_set:
        lines.append("  ✗ INCOMPLETE — manifest attests check_set %r, relier pinned %r" % (cset, expect_set))
        return 2, "INCOMPLETE", lines + [
            "a manifest against a different canonical set cannot satisfy a pinned scope, however honestly "
            "signed — the verifier chose what to account for; you chose what your decision needs. Treated "
            "exactly like a missing canonical check: INCOMPLETE, never silent."]

    # 3. the canonical set is FULLY accounted — a missing check is a silent omission, not a pass.
    if CHECK_SETS.get(cset) is None:
        return 2, "UNKNOWN_SET", lines + ["unknown check_set %r — cannot tell what a complete scope would be." % cset]
    canon = _members(CHECK_SETS[cset])

    # 2b. content-address the set. The name pin (step 2) trusts that %r means the same members to signer
    #     and relier; this checks it. `local_commit` is what THIS verifier's definition of the named set
    #     commits to; the signed `check_set_hash` is what the signer accounted for. Disagreement is drift —
    #     same name, different set — the residual a name pin is blind to. And --expect-set-hash lets the
    #     relier pin the universe by content, not by a name whose meaning can move under it.
    local_commit = check_set_commitment(sv, cset, canon)
    declared_commit = manifest.get("check_set_hash")
    if declared_commit is not None and declared_commit != local_commit:
        lines.append("  ✗ SET_DRIFT — manifest commits %s, this verifier's %r commits %s" % (
            declared_commit, cset, local_commit))
        return 2, "SET_DRIFT", lines + [
            "the manifest's check_set_hash does not match this verifier's definition of %r. Same name, "
            "different check-set: one side's member list drifted. A name pin would have accepted this "
            "silently as 'fully accounted'; the content commitment is what catches it." % cset]
    if expect_set_hash is not None:
        if declared_commit is None:
            lines.append("  ✗ INCOMPLETE — relier pinned a set-hash but the manifest commits none")
            return 2, "INCOMPLETE", lines + [
                "the check-universe was never content-addressed (no check_set_hash), so its membership "
                "can't be pinned by content. A name alone can drift; treated as INCOMPLETE."]
        if declared_commit != expect_set_hash:
            lines.append("  ✗ INCOMPLETE — manifest commits %s, relier pinned %s" % (declared_commit, expect_set_hash))
            return 2, "INCOMPLETE", lines + [
                "the manifest committed a different check-universe than the one your decision pinned, "
                "however honestly it was signed. A content pin cannot be narrowed by a name collision."]
        lines.append("  ✓ check-set content pinned (%s)" % expect_set_hash)

    # 2c. freshness (anp2network's time-decay): content-addressing pins WHICH checks, not WHEN the set was
    #     authored, and a pinned set decays as the world adds checkable things it doesn't name. A retired,
    #     over-age, or superseded set is a checkable rejection, not a silent "still probably covers it."
    meta = _meta(cset)
    now = now or _utcnow()
    exp = _parse_ts(meta.get("expires_at"))
    if exp is not None and now > exp:
        lines.append("  ✗ STALE_SET — check-set %r expired %s" % (cset, meta["expires_at"]))
        return 2, "STALE_SET", lines + [
            "the check-set is retired, not merely old — its coverage was declared to stop being canonical at "
            "%s. Reduce over a current set." % meta["expires_at"]]
    if max_age is not None:
        va = _parse_ts(meta.get("valid_as_of"))
        if va is None:
            lines.append("  ✗ STALE_SET — %r declares no valid_as_of; age can't be checked against --max-age" % cset)
            return 2, "STALE_SET", lines + [
                "the relier requires coverage no older than the window, but the set's age is undeclared. "
                "Fail closed — an unknown age is not a fresh one."]
        if now - va > max_age:
            lines.append("  ✗ STALE_SET — %r authored %s (%d days ago), older than the relier's window" % (
                cset, meta["valid_as_of"], (now - va).days))
            return 2, "STALE_SET", lines + [
                "the world moved and the coverage didn't. Content-addressing proved you reduced over the set "
                "you named; the age proves the set you named is behind the threat surface it was meant to gate."]
    if reject_superseded:
        succ = next((n for n in CHECK_SETS if (_meta(n) or {}).get("supersedes") == cset), None)
        if succ is not None:
            lines.append("  ✗ SUPERSEDED — %r is replaced by %r" % (cset, succ))
            return 2, "SUPERSEDED", lines + [
                "a newer canonical set (%r) supersedes the one this manifest reduced over, and you asked to "
                "reject superseded coverage. Reduce over %r." % (succ, succ)]

    # 2d. bind the pin to a PUBLISHED anchor (--expect-set-anchor=<entry-url>). --expect-set-hash checks
    #     the manifest's set against the RELIER's local copy; this checks it against a real Touchstone
    #     entry whose payload_hash IS the check_set_hash (app:scope:anchor commits the same bytes), so the
    #     set is provably recorded — and, once checkpointed, gains a Bitcoin not-before that closes
    #     backdating. The hash-match is the hard check; the checkpoint status is reported.
    if expect_set_anchor is not None:
        declared_commit = manifest.get("check_set_hash")
        if declared_commit is None:
            return 2, "INCOMPLETE", lines + [
                "relier pinned an anchor but the manifest commits no check_set_hash to bind it to."]
        try:
            data = sv.get_json(expect_set_anchor)
        except Exception as e:  # network/parse — a relier that required an anchor didn't get to see one
            return 2, "ANCHOR_UNREACHABLE", lines + [
                "could not fetch the anchor entry (%s: %s). A required anchor you can't read is not a "
                "satisfied one." % (type(e).__name__, str(e)[:80])]
        entry = data.get("entry") or {}
        ph = entry.get("payload_hash")
        want = declared_commit.split(":")[-1]
        if ph != want:
            lines.append("  ✗ ANCHOR_MISMATCH — entry commits %s, manifest's set is sha256:%s" % (ph, want))
            return 2, "ANCHOR_MISMATCH", lines + [
                "the anchored entry does not commit the set this manifest reduced over — wrong anchor, or the "
                "set drifted from what was anchored. A content pin against the wrong anchor is not a pin."]
        cp = data.get("checkpoint")
        if cp:
            lines.append("  ✓ set anchored — payload_hash matches entry seq %s, checkpointed (%s); fold to "
                         "Bitcoin with beacon-verify" % (entry.get("seq"), str(cp.get("id") or "")[:16]))
        else:
            lines.append("  · set committed to the recorder (payload_hash matches entry seq %s) but not yet "
                         "checkpointed — no Bitcoin not-before yet (the usual anchor lag)" % entry.get("seq"))

    declared = {}
    for c in (manifest.get("checks") or []):
        if isinstance(c, dict) and c.get("check") and c.get("status") in STATUSES:
            declared[c["check"]] = c
    missing = [c for c in canon if c not in declared]
    unknown = [c for c in declared if c not in canon]
    if missing:
        lines.append("  ✗ INCOMPLETE — canonical checks unaccounted: %s" % ", ".join(missing))
        return 2, "INCOMPLETE", lines + [
            "the verifier did not account for every applicable check. Silence about a check is NOT a pass; "
            "an honest manifest marks it skipped. Omission here would let 'valid' narrow itself in private."]
    if unknown:
        lines.append("  · note: non-canonical checks also present: %s" % ", ".join(unknown))

    # 4. report the scope — never a bare verdict.
    ran = [c for c in canon if declared[c]["status"] == "pass"]
    failed = [c for c in canon if declared[c]["status"] == "fail"]
    skipped = [c for c in canon if declared[c]["status"] == "skipped"]
    lines.append("")
    lines.append("  SCOPE attested by this verifier over %s (%s):" % (manifest.get("subject"), cset))
    for c in canon:
        d = declared[c]
        tier = (" · tier %s" % d["tier"]) if d.get("tier") else ""
        mark = {"pass": "✓", "fail": "✗", "skipped": "·"}[d["status"]]
        lines.append("    %s %-12s %s%s" % (mark, c, d["status"].upper(), tier))
    lines.append("")
    if failed:
        return 1, "SCOPE_FAIL", lines + [
            "SCOPE: %s PASSED, %s SKIPPED, and %s FAILED. A load-bearing check failed in the attested scope — "
            "do not rely on it. 'valid' was never claimed; what was checked is above." % (
                ", ".join(ran) or "nothing", ", ".join(skipped) or "nothing", ", ".join(failed))]
    return 0, "SCOPE_ATTESTED", lines + [
        "SCOPE_ATTESTED: %s checked and PASSED; %s NOT CHECKED. This is not 'valid' — it is a signed record "
        "of exactly what this verifier did and did not check. Gate on the checks your decision needs; the "
        "skipped ones carry no assurance, and the verifier cannot later claim it checked them." % (
            ", ".join(ran) or "nothing", ", ".join(skipped) or "nothing (full scope)")]


# ── minting (needs an Ed25519 signer) ────────────────────────────────────────
def build_manifest(sv, seed_b64, subject, statuses, check_set="touchstone-receipt/1", at=None, tiers=None):
    """statuses: {check: 'pass'|'fail'|'skipped'}. Signs with the seed. Emits the full canonical set."""
    try:
        import base64
        from nacl.signing import SigningKey
    except ImportError as e:  # pragma: no cover
        raise RuntimeError("minting a manifest needs PyNaCl — pip install pynacl") from e
    canon = _members(CHECK_SETS[check_set])
    checks = []
    for c in canon:
        st = statuses.get(c, "skipped")
        entry = {"check": c, "status": st}
        if (tiers or {}).get(c) and st == "pass":
            entry["tier"] = tiers[c]
        checks.append(entry)
    sk = SigningKey(base64.b64decode(seed_b64))
    m = {"kind": "touchstone.scope/1", "subject": subject, "check_set": check_set,
         "check_set_hash": check_set_commitment(sv, check_set, canon), "checks": checks,
         "verifier": base64.b64encode(bytes(sk.verify_key)).decode()}
    if at:
        m["at"] = at
    m["sig"] = base64.b64encode(sk.sign(_signing_bytes(sv, m)).signature).decode()
    return m


def _selftest():
    sv = _load_sibling("standing-verify.py")
    ok = []
    try:
        import base64
        from nacl.signing import SigningKey
        sk = SigningKey.generate()
        seed = base64.b64encode(bytes(sk)).decode()
        m = build_manifest(sv, seed, "sha256:abc", {"integrity": "pass"},
                           tiers={"integrity": "re-derivable"})  # uniqueness/existence/completeness → skipped
        ok.append(("honest manifest → ATTESTED", verify_manifest(sv, m)[1] == "SCOPE_ATTESTED"))
        ok.append(("skipped checks enumerated", sum(c["status"] == "skipped" for c in m["checks"]) == 3))

        # flip a status → signature breaks (can't claim it checked what it skipped).
        m_flip = json.loads(json.dumps(m))
        for c in m_flip["checks"]:
            if c["check"] == "uniqueness":
                c["status"] = "pass"
        ok.append(("flipped status → FORGED", verify_manifest(sv, m_flip)[1] == "FORGED"))

        # drop a canonical check → INCOMPLETE (silent omission caught), even though the sig would need
        # to still cover the shortened list — dropping it AND re-covering needs the key; without the key
        # it's FORGED, with a stripped list it's INCOMPLETE. Test the stripped-but-signature-stale case:
        m_drop = json.loads(json.dumps(m))
        m_drop["checks"] = [c for c in m_drop["checks"] if c["check"] != "completeness"]
        # sig now stale → FORGED (the sig covered the full list); this is the crypto layer catching it.
        ok.append(("dropped check (stale sig) → FORGED", verify_manifest(sv, m_drop)[1] == "FORGED"))

        # a validly-signed manifest that legitimately OMITS a canonical check → INCOMPLETE (spec layer):
        m_inc = build_manifest(sv, seed, "sha256:abc", {"integrity": "pass"})
        m_inc = json.loads(json.dumps(m_inc))
        m_inc["checks"] = [c for c in m_inc["checks"] if c["check"] != "completeness"]
        m_inc["sig"] = base64.b64encode(sk.sign(_signing_bytes(sv, m_inc)).signature).decode()  # re-sign the short list
        ok.append(("re-signed short list → INCOMPLETE", verify_manifest(sv, m_inc)[1] == "INCOMPLETE"))

        # a FAILED load-bearing check surfaces as SCOPE_FAIL, not silence.
        m_fail = build_manifest(sv, seed, "sha256:abc", {"integrity": "pass", "uniqueness": "fail"})
        ok.append(("a failed check → SCOPE_FAIL", verify_manifest(sv, m_fail)[1] == "SCOPE_FAIL"))

        # ── the relier's pin (anp2network's narrowing attack, one level up) ──
        # a pinned relier accepts the matching set…
        ok.append(("pin matches → ATTESTED",
                   verify_manifest(sv, m, expect_set="touchstone-receipt/1")[1] == "SCOPE_ATTESTED"))

        # …but an honestly-signed manifest against a verifier-chosen SMALLER set does not satisfy it.
        # Simulate a future/foreign registered set: fully accounted, validly signed — the honest narrowing.
        CHECK_SETS["narrow-demo/1"] = ["integrity"]
        try:
            m_narrow = build_manifest(sv, seed, "sha256:abc", {"integrity": "pass"}, check_set="narrow-demo/1")
            ok.append(("narrow set, unpinned → ATTESTED (the hole)",
                       verify_manifest(sv, m_narrow)[1] == "SCOPE_ATTESTED"))
            ok.append(("narrow set vs pin → INCOMPLETE (the fix)",
                       verify_manifest(sv, m_narrow, expect_set="touchstone-receipt/1")[1] == "INCOMPLETE"))
        finally:
            del CHECK_SETS["narrow-demo/1"]

        # absent check_set against a pin → INCOMPLETE (never silent), and the pin outranks UNKNOWN_SET.
        m_absent = json.loads(json.dumps(m))
        del m_absent["check_set"]
        m_absent["sig"] = base64.b64encode(sk.sign(_signing_bytes(sv, m_absent)).signature).decode()
        ok.append(("absent set vs pin → INCOMPLETE",
                   verify_manifest(sv, m_absent, expect_set="touchstone-receipt/1")[1] == "INCOMPLETE"))

        # the pin never DOWNGRADES crypto: a forged manifest is FORGED first, pinned or not.
        m_forge = json.loads(json.dumps(m_narrow))
        m_forge["check_set"] = "touchstone-receipt/1"  # relabel the set without the key
        ok.append(("relabeled set (stale sig) → FORGED",
                   verify_manifest(sv, m_forge, expect_set="touchstone-receipt/1")[1] == "FORGED"))

        # ── content-address the set (the drift a NAME pin can't see; anp2network's throttle-thread rule) ──
        good_hash = check_set_commitment(sv, "touchstone-receipt/1", _members(CHECK_SETS["touchstone-receipt/1"]))
        ok.append(("minted manifest commits its set", m.get("check_set_hash") == good_hash))
        ok.append(("hash pin matches → ATTESTED",
                   verify_manifest(sv, m, expect_set_hash=good_hash)[1] == "SCOPE_ATTESTED"))
        ok.append(("wrong hash pin → INCOMPLETE",
                   verify_manifest(sv, m, expect_set_hash="sha256:" + "00" * 32)[1] == "INCOMPLETE"))
        # a legacy manifest committing NO set-hash can't be content-pinned — never silent.
        m_legacy = json.loads(json.dumps(m))
        del m_legacy["check_set_hash"]
        m_legacy["sig"] = base64.b64encode(sk.sign(_signing_bytes(sv, m_legacy)).signature).decode()
        ok.append(("no commitment vs hash pin → INCOMPLETE",
                   verify_manifest(sv, m_legacy, expect_set_hash=good_hash)[1] == "INCOMPLETE"))
        # THE DRIFT: this verifier's definition of the SAME NAME drifts smaller. The manifest accounted
        # for MORE, so name-based accounting finds nothing missing and ATTESTS (the hole). The signed
        # content commitment does not match this verifier's definition → SET_DRIFT (the fix). This is the
        # residual the --expect-set name pin is structurally blind to.
        _saved = CHECK_SETS["touchstone-receipt/1"]
        CHECK_SETS["touchstone-receipt/1"] = ["integrity", "existence", "uniqueness"]  # drifted: dropped completeness
        try:
            m_blind = json.loads(json.dumps(m))
            del m_blind["check_set_hash"]  # a name-only manifest, so nothing catches the drift…
            m_blind["sig"] = base64.b64encode(sk.sign(_signing_bytes(sv, m_blind)).signature).decode()
            ok.append(("drift, name-only → ATTESTED (the hole)",
                       verify_manifest(sv, m_blind)[1] == "SCOPE_ATTESTED"))
            ok.append(("drift, content-addressed → SET_DRIFT (the fix)",
                       verify_manifest(sv, m)[1] == "SET_DRIFT"))
        finally:
            CHECK_SETS["touchstone-receipt/1"] = _saved

        # ── freshness: a pinned set decays as the world moves (anp2network's time axis) ──
        future = datetime.datetime(2027, 1, 1, tzinfo=datetime.timezone.utc)  # ~172d after valid_as_of
        ok.append(("fresh set within --max-age → ATTESTED",
                   verify_manifest(sv, m, now=future, max_age=_parse_age("365d"))[1] == "SCOPE_ATTESTED"))
        ok.append(("stale set beyond --max-age → STALE_SET",
                   verify_manifest(sv, m, now=future, max_age=_parse_age("30d"))[1] == "STALE_SET"))
        ok.append(("no --max-age → freshness not enforced (ATTESTED)",
                   verify_manifest(sv, m, now=future)[1] == "SCOPE_ATTESTED"))
        # an expired set is retired regardless of --max-age
        _saved_e = CHECK_SETS["touchstone-receipt/1"]
        CHECK_SETS["touchstone-receipt/1"] = dict(_saved_e, expires_at="2026-08-01")
        try:
            ok.append(("expired set → STALE_SET", verify_manifest(sv, m, now=future)[1] == "STALE_SET"))
        finally:
            CHECK_SETS["touchstone-receipt/1"] = _saved_e
        # a successor set makes the old one superseded — but only when the relier asks
        CHECK_SETS["touchstone-receipt/2"] = {"checks": _saved_e["checks"] + ["freshness"],
                                              "valid_as_of": "2026-12-01", "expires_at": None,
                                              "supersedes": "touchstone-receipt/1"}
        try:
            ok.append(("superseded + --reject-superseded → SUPERSEDED",
                       verify_manifest(sv, m, reject_superseded=True)[1] == "SUPERSEDED"))
            ok.append(("superseded without the flag → still ATTESTED",
                       verify_manifest(sv, m)[1] == "SCOPE_ATTESTED"))
        finally:
            del CHECK_SETS["touchstone-receipt/2"]

        # ── bind the pin to a published anchor (--expect-set-anchor); mock the fetch ──
        want = m["check_set_hash"].split(":")[-1]
        _orig = getattr(sv, "get_json", None)
        try:
            sv.get_json = lambda url: {"entry": {"seq": 1, "payload_hash": want}, "checkpoint": {"id": "cp#42"}}
            ok.append(("anchor matches + checkpointed → ATTESTED",
                       verify_manifest(sv, m, expect_set_anchor="mock://entry")[1] == "SCOPE_ATTESTED"))
            sv.get_json = lambda url: {"entry": {"seq": 1, "payload_hash": want}}  # bound but no checkpoint
            ok.append(("anchor matches, uncheckpointed → still ATTESTED (pending)",
                       verify_manifest(sv, m, expect_set_anchor="mock://entry")[1] == "SCOPE_ATTESTED"))
            sv.get_json = lambda url: {"entry": {"seq": 1, "payload_hash": "deadbeef"}}
            ok.append(("anchor commits wrong hash → ANCHOR_MISMATCH",
                       verify_manifest(sv, m, expect_set_anchor="mock://entry")[1] == "ANCHOR_MISMATCH"))
            def _boom(url):
                raise RuntimeError("network down")
            sv.get_json = _boom
            ok.append(("anchor unreachable → ANCHOR_UNREACHABLE",
                       verify_manifest(sv, m, expect_set_anchor="mock://entry")[1] == "ANCHOR_UNREACHABLE"))
        finally:
            if _orig is not None:
                sv.get_json = _orig
        # a manifest with no commitment can't be anchor-pinned
        m_nohash = json.loads(json.dumps(m))
        del m_nohash["check_set_hash"]
        m_nohash["sig"] = base64.b64encode(sk.sign(_signing_bytes(sv, m_nohash)).signature).decode()
        ok.append(("no commitment vs anchor pin → INCOMPLETE",
                   verify_manifest(sv, m_nohash, expect_set_anchor="mock://entry")[1] == "INCOMPLETE"))
    except ImportError:
        print("  (mint-side skipped — pip install pynacl to exercise the signing path)")
        ok.append(("canonical set defined", "touchstone-receipt/1" in CHECK_SETS))

    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 — omission unforgeable at both layers; scope is signed, not implied."))
    return 1 if bad else 0


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


def main(argv):
    args = argv[1:]
    if "--selftest" in args:
        return _selftest()
    sv = _load_sibling("standing-verify.py")

    sh = _kv(args, "--set-hash")
    if sh is not None:
        if sh not in CHECK_SETS:
            print("unknown check_set %r; known: %s" % (sh, ", ".join(sorted(CHECK_SETS))))
            return 2
        print(check_set_commitment(sv, sh, _members(CHECK_SETS[sh])))
        return 0

    if "--sign" in args:
        kf = _kv(args, "--key-file")
        seed = json.load(open(kf, encoding="utf-8"))["seed_b64"] if kf else _kv(args, "--seed")
        subject = _kv(args, "--subject", "?")
        statuses = {}
        for c in (_kv(args, "--pass", "") or "").split(","):
            if c:
                statuses[c] = "pass"
        for c in (_kv(args, "--fail", "") or "").split(","):
            if c:
                statuses[c] = "fail"
        for c in (_kv(args, "--skip", "") or "").split(","):
            if c:
                statuses[c] = "skipped"
        print(json.dumps(build_manifest(sv, seed, subject, statuses), indent=2))
        return 0

    src = _kv(args, "--verify")
    if src is None and "--verify" in args:  # also accept the space form the usage line advertises
        i = args.index("--verify")
        if i + 1 < len(args) and not args[i + 1].startswith("-"):
            src = args[i + 1]
    if src is None:
        src = args[0] if args and not args[0].startswith("-") else None
    if not src:
        print(__doc__)
        return 2
    if src == "-":
        manifest = json.load(sys.stdin)
    elif src.startswith(("http://", "https://")):
        manifest = sv.get_json(src)
    else:
        manifest = json.load(open(src, encoding="utf-8"))
    code, _verdict, lines = verify_manifest(
        sv, manifest, expect_set=_kv(args, "--expect-set"), expect_set_hash=_kv(args, "--expect-set-hash"),
        now=_parse_ts(_kv(args, "--now")), max_age=_parse_age(_kv(args, "--max-age")),
        reject_superseded=("--reject-superseded" in args), expect_set_anchor=_kv(args, "--expect-set-anchor"))
    for ln in lines:
        print(ln)
    return code


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