#!/usr/bin/env python3
"""Lint the Task26 v4 Golden Path and Recovery Runbook pair (runbook-rebind task st_01a0054e).

v4 linter: carries all v3 checks forward (v1-v3 runbooks are preserved byte-identical and checked by
their own linters) and adds the v4 groups: path-existence (on-disk hash/mode/ownership verification),
no-secret (raw token and unapproved IDs absent), obsolete-path-negative (session-file references only
inside prohibition statements), and archive-CLI (exact verifier command with the actual seal path).
"""
from __future__ import annotations

import hashlib
import json
import os
import re
import stat
import sys
from pathlib import Path

BASE = Path(__file__).resolve().parent
REPO = BASE.parents[3]
GP = BASE / "task26-golden-path-2e0894ea-v4.md"
RC = BASE / "task26-recovery-runbook-2e0894ea-v4.md"
RECEIPT = BASE / "runbook-binding-receipt-v4.json"

CANDIDATE = "2e0894eac92bc396cc4723bf1f18ebc653b95018dd41574df435941c235da925"
WHEEL = "af4a9d0a1ffffb6eb7551c1d6dc2b32853ca6d024332a4f8f5702bbf992f141b"
PLAN = "7ace03c6dad33d2fc3ef223621cbca68a150fde8429932138e252fb8498ac582"
ACTOR = "8527916639"

# v3 pins (unchanged evidence) plus the v4 G14 pins.
PINS = [
    "b852482350129dd025f0c4c79539e04a71c4758c9ec336e2c5bcda44676791f6",
    "cd9e080ab886d503a205e7a7335eb23942b161daeaa2028a6e8f63492a99ba68",
    "8278dde4efa8bcd366fce873ccc07f651818275238b67452b9e6cb93c67afd6d",
    "6aeaaab42e0030b0f626037d7b9ef6661ec93fb57fd991c843967c1fc695905c",
    "16acf1b355715379ad526433803353e1f34c418e9560fc1466f3f77f951df919",
    "7d163cb01708869d617efeadd852f7e6ec4f963a94c50d19b104cc849483c171",
    "f9a46172386333a0067f43695fb1429a043a606f04baceb06e8c59b90c36235c",
    "0e9b4b1f0449d8351602598fead3ef01b8616e230c147e44db79667284ceae49",
    "2826b1baf97918829f80d41722ec6534f933e57efa52345353cdb366cac314fb",
    "c4e857894ebd8b2e96289ea70d7cdcdd38cdad1c72cc7b1c86f33a5d2de7d171",
    "c7fc477141259087e8e9dd6520b2066eda3bab20dc6e2f625942431b6830c187",
    "c2a6393f2ccf67ab6872f83e03793af72ea21b7df2e616c7cc2256c566c6b2c0",
    "68f6f72f773265a3e889b5ab2024425caa7eb1fd0cf4f612379fd7203293185b",
    "23845bda574c085e16b6d316e5e84e54b3bc6db3a8803c6b3b93ed7c4b86b049",
    "145515d5e110dcebb94fcaa554bcee29544b3a75dcfe058fae944ea3b70042b2",
    "8b03fa714304b34a7f19dc9b077d8fbd54e7f7bc46232bf8d5bc65ae7b873ff5",
    "7b6a1aa54b58d6e7e6733a963772e220a903ffae5a3e0e6d089283af13e097db",
    "cb06310270868da5dfc3c1d292b0ccedd5f3021d0f211576449c96649fa4730b",
    "90fcb04be789fd519d6bb83f9ba659927721e9d272594de84329c42c3529ed18",
    "d8d818cdfd2a22b088064b216136bfdee050b096a5b7fdcc9fb6f5958312dd05",
    "fab6322753cccca614e1ee83bf5221013185662a74c86f5d19563e38f0fc0d2b",
    "769db2bd5c39e405176ecb438d05ad0f10c538ee53c6c7849e809123f91ecb88",
    "78d7474df421f26ec4d106fa378270f60e57178553c3423fad717698c0fd5b22",
    "e6888d62915b4b57b22b2769ebf46a7a1ee57d0d4ad7c4d5c1502bffc895ebdf",
    "bd051dda8666ce9e014ec79c58acd4d7b8df2c27fad01a7a78c76e46c3388baf",
    "4128cece087f3f4eb84c3917207299fae6bd1ac9971d7e4e8676549106bb7c20",
    "8a290b11cac5b6957c772366abe875c7f635b8a3e7956a665471ffaa90b6c495",
    "2fc323f8b00c18f21598f717fb6a19a7e342b7786909e59adcf4bc3955bcef99",
    "776e90301a63dce8912bf8a4110cd037f833c3a0e875a3ddd1289c0c9ee2ac9e",
    "12b97aa72e2df86719dbb7b80b477237d590aaaea81a9f812f321a1566936528",
    "29db87f2855c4804dbbc69244e68d9fb9f51808f7f7759fb592ed68f5f090185",
    "28ff72fdb7925cbdcd8e7dd7e8c058bcca42cab3377c34078164766197ed6968",
    "597a37e4912d729b3b5f6022bccff6dc12a73b024128805980f5ab1ade4fb83d",
    "d2879b128573a9d75a55958d2fa3e1a24be1636f0a7a20affe8f2ef9b94dfd33",
    "a8191eed20ca75abcb03c33ac0c0b915cd810249466e816c6a1a55ba154af188",
    "f5d51008f1e8c8ee102df930fa68c7945a3276a249187adc7b7f6ba4b1e8e134",
    "3eca06827bfd62a4f07498bf7fed54e3b2b47a9f62e1f5b81701d1453e88806e",
    "cc0e4b697633150f3626daa8cf5bb4825bc96243f1a3dfadb6c9f540cad9906c",
    "6ab725042cbed536d8a8f3faee29e431470617f21d83c795e19574a27fcd032f",
    "1f55b8f967c6564113574d41bf1c15ce977d35c38b5687f1623ba37f235a7bdb",
    "f4f2d2347b4b68a080b9c437a813accbef8c4bcc471cbf21d67ffff31e56f89b",
    "633d5c25ce70cb9ab5d19c434122b14b46fb7108962ab6e453ce6ab3ede1848f",
    "6024c26ccae3cad7fcf46a77bca8607d73293a92ddb3dcf8fa5a278361869f1d",
    "ff230719eb5a69ae821bb0a4d58356d59b2803c490d692659a197e22083793c7",  # v3 GP
    "e62691493d2a75c312dd248a07bbc52f7f1d98d4acd701b824bbaf5edf8bdd3a",  # v3 RC
    "32a379d855c6e5af978bd9886e3bf49c616c7c20f1c1d5f100c19d8adfc5eb4a",
    "ae2f5f9046c06f8f0b42693be5aa0d9c31cada8024ae1e5ab8ba19a3cf10f6fc",
    "f93106b16643227e2ef9dec67a5bbd497e1d353e779da62287898a087071af87",
    "0b46e887fc12c45f12814f4ef025c10101ed36e5df0ac35f70abb1862c0c1db2",
    "28420d4aa8fc1ee1298c9fe93a69e47aa187c0d2777ca672fb603cfbebc569cf",
    "9eb1b5ae", "7004dac9", "0bd0afb9", "68de185a",
    # v4 G14 additions
    "154f588c89758c6ca6a1e06cc39ce9489d5c3ff8ace94e10e71cce919252bcca",  # .env
    "d0aacf0f4bdbb7c04e12769947c498ee869248972b36f099800a3d3c27c0a3a8",  # token sha256 (no-secret proof)
    "b41060dea28eb3bbb83217068f5218d5df2c879dba00e73c9ca4e577a6049dad",  # telegram.py
    "849791bc3b31145a691b8ef4defacba72aeb5de0fca12427988ec1ddd4e9fffa",  # nutrition_coaching_config.py
    "b48e2b49f0939316e7e4368c940492d1b8a94863da7e5c8141f7f67a1fe08efb",  # telegram_nutrition_addresses.py
]

STALE_PATTERNS = [
    r"python -m hermes_cli\.nutrition_readiness --customer",
    r"request-generation --customer",
    r"record-generation",
    r"customer_admin --registry \S+ create",
    r"customer_admin --registry \S+ reset",
    r"customer_admin --registry \S+ delete",
    r"customer_admin --registry \S+ set-next-checkin",
    r"disable <[^>]*> --reason",
    r"/home/cube/\.hermes/profiles/dualcoachtest/\.venv",
    r"/etc/systemd/system/hermes-gateway-dualcoachtest",
    r"dry-run, verify, execute",
    r"gateway/cron/scheduler\.py",
    r"telegram_customer_onboarding\.py",
    r"eff71467",
    r"exit `?2`?,? stdout JSON `?status`? == `?PASS",
    r"(?<!retained-)archive-verifier-permission-seal",   # stale seal filename (v3 error)
    r"Telegram auth files under",                        # deleted G14 assertion form (v1-v3)
    r"api_id",                                           # unapproved raw ID form
]

# On-disk path-existence contracts (read-only verification).
PATH_CHECKS = [
    (REPO / ".omo/evidence/task26/retained-archive-verifier-permission-seal.json",
     "28ff72fdb7925cbdcd8e7dd7e8c058bcca42cab3377c34078164766197ed6968", 0o400),
    (REPO / ".omo/evidence/task26/verify_retained_archives.py",
     "12b97aa72e2df86719dbb7b80b477237d590aaaea81a9f812f321a1566936528", 0o500),
    (REPO / ".omo/evidence/task26/retained-archive-schema-inventory.json",
     "29db87f2855c4804dbbc69244e68d9fb9f51808f7f7759fb592ed68f5f090185", 0o400),
    (Path("/home/cube/.hermes/profiles/dualcoachtest/.env"),
     "154f588c89758c6ca6a1e06cc39ce9489d5c3ff8ace94e10e71cce919252bcca", 0o600),
    (Path("/home/cube/.hermes/profiles/dualcoachtest/config.yaml"),
     "f93106b16643227e2ef9dec67a5bbd497e1d353e779da62287898a087071af87", 0o600),
    (Path("/home/cube/.config/systemd/user/hermes-gateway-dualcoachtest.service"),
     "0b46e887fc12c45f12814f4ef025c10101ed36e5df0ac35f70abb1862c0c1db2", None),
]

OBSOLETE_NEEDLES = ("auth_39664143", ".session-journal", ".local/share/hermes/telegram")
PROHIBITION_MARKERS = ("never", "obsolete", "prohibit", "forbidden", "absent", "deleted")

results: list[dict] = []


def check(name: str, ok: bool, detail: str = "") -> None:
    results.append({"check": name, "ok": bool(ok), "detail": detail})


def sha256_of(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def main() -> int:
    check("file-pair-exists", GP.exists() and RC.exists(), f"{GP.name}, {RC.name}")
    if not (GP.exists() and RC.exists()):
        return report()
    gp = GP.read_text(encoding="utf-8")
    rc = RC.read_text(encoding="utf-8")

    # --- binding ---------------------------------------------------------------
    for label, text in (("gp", gp), ("rc", rc)):
        check(f"{label}-candidate-full-hash", CANDIDATE in text)
        check(f"{label}-wheel-hash", WHEEL in text)
        check(f"{label}-plan-hash", PLAN in text)
        check(f"{label}-actor-id", ACTOR in text)
        check(f"{label}-candidate-mentions>=8", text.count(CANDIDATE) + text.count("2e0894ea") >= 8,
              f"{text.count(CANDIDATE) + text.count('2e0894ea')}")
        check(f"{label}-version-marker", "v4" in text and "2026-08-15" in text)

    for pin in PINS:
        check(f"pin-{pin[:8]}", pin in gp or pin in rc, pin)

    # --- stale/incorrect content must be absent --------------------------------
    for pat in STALE_PATTERNS:
        for label, text in (("gp", gp), ("rc", rc)):
            m = re.search(pat, text)
            check(f"{label}-stale-absent:{pat[:36]}", m is None, m.group(0)[:80] if m else "")

    # --- stale candidate + forbidden reused identifiers -------------------------
    stale_candidate = "19ed0e6047f0a4d7650c47a7413ee0298a243f768a63bcb8fb9cffe44d140a1a"
    check("gp-stale-candidate-superseded", gp.count(stale_candidate) >= 1 and "supersede" in gp.lower())
    check("rc-stale-candidate-superseded", rc.count(stale_candidate) >= 1)
    forbidden_ids = ["task22_dm_rehearsal", "task26_synthetic_rehearsal", "task26_same_actor_rehearsal",
                     "cb_2NQV5sbkN-M6awycJH7X5g", "cb_6S6RABpDgZ165V02A7qEzw",
                     "cb_9yTz0oNwdU8s8hradru8BA", "cb_rmDnfrqA6gkmjoQEdwxu0g",
                     "wizard_995f04a3b8bc256fa13ff407", "3f44a18ea620d963"]
    for fid in forbidden_ids:
        check(f"gp-forbidden-id-{fid[:24]}", fid in gp)
    overlap = [f for f in forbidden_ids if f in rc]
    check("rc-forbidden-id-overlap-scan", not overlap, f"overlap: {overlap}")
    for word in ["<link>", "<token>", "<nonce>", "TBD", "placeholder", "FIXME"]:
        check(f"gp-no-placeholder-{word}", word not in gp)
        check(f"rc-no-placeholder-{word}", word not in rc)

    # --- structural -------------------------------------------------------------
    for label, text in (("gp", gp), ("rc", rc)):
        check(f"{label}-heading", text.lstrip().startswith("# "))
        check(f"{label}-has-verdict-language", "PASS" in text and "FAIL" in text)
        check(f"{label}-private-evidence-modes", "600" in text and "700" in text)
    check("gp-evidence-pins-verifier-exit0", "Exit `0`" in gp and "verifier-input.json" in gp)
    check("gp-deadlines-table", "120 seconds" in gp and "600 seconds" in gp and "90 seconds" in gp)
    check("rc-supported-command-table", "| Interface | Supported command shape |" in rc)
    check("rc-void-interface-section", "Void interfaces" in rc)
    check("rc-abort-matrix", "Abort decision matrix" in rc)

    # --- semantic deadlock / correctness checks ---------------------------------
    g8_line = next((ln for ln in gp.splitlines() if ln.startswith("| G8 |")), "")
    check("gp-g8-row-found", bool(g8_line))
    check("gp-g8-no-absence-demand", "absent" not in g8_line.lower())
    check("gp-g8-holder-check", "flock" in g8_line and "no active holder" in g8_line.lower())
    check("gp-g8-scope-check", "approved_clear_scopes" in g8_line)
    g15_line = next((ln for ln in gp.splitlines() if ln.startswith("| G15 |")), "")
    check("gp-g15-row-found", bool(g15_line))
    check("gp-g15-lock-absent", "absent" in g15_line.lower() and "gateway.lock" in g15_line)
    for label, text in (("gp", gp), ("rc", rc)):
        check(f"{label}-no-manual-lock-deletion", "manual `gateway.lock` deletion" in text
              and re.search(r"\brm [^\n]*gateway\.lock|delete (the )?lock file by hand", text) is None)
    check("rc-lock-disposal-rule", "Any other deletion of the lock file is forbidden" in rc
          and "sealed pre-reset" in rc)
    chain = ["Pre-reset arm", "Pre-reset execute and verify", "Invite harness `dry-run` and `verify`",
             "Exact-wheel deployment", "Observe before prepare/Start", "Lifecycle:",
             "Disable and service stop", "Cleanup execute and verify"]
    positions = [gp.find(c) for c in chain]
    check("gp-ordering-chain-complete", all(p > 0 for p in positions), f"positions: {positions}")
    check("gp-ordering-chain-ordered", positions == sorted(positions), f"positions: {positions}")
    check("gp-mode-order-cleanup", "`dry-run` -> `execute` -> `verify`" in rc)
    check("gp-approval-literal-phrase", "'TASK26_ARCHIVE_FIRST_PROFILE_RESET_APPROVED'" in gp)
    check("gp-cleanup-approval-phrase", "TASK26_POST_LIFECYCLE_ARCHIVE_CLEANUP_APPROVED" in gp and
          "TASK26_POST_LIFECYCLE_ARCHIVE_CLEANUP_APPROVED" in rc)
    check("gp-one-prepare", "invite_prepare_maximum` is 1" in gp or "one-prepare maximum" in rc)
    check("gp-one-start", "accepted_start_claim_maximum: 1" in gp)
    check("gp-one-delivery", "exactly one customer DM" in gp and "exactly one `adaptive_plan_delivered`" in gp)
    check("rc-no-recovery-shortcuts", "recovery shortcut" in rc)
    check("rc-expire-mode", "invite_harness.py expire" in rc)
    check("gp-no-open-gates", [m.start() for m in re.finditer(r"\bOPEN\b", gp)] ==
          [gp.find("No gate is OPEN") + len("No gate is ")])
    check("gp-b3-closed", "(B3 closed)" in gp)
    check("gp-g12-closed", "(G12 closed)" in gp)
    check("gp-b6-closed", "(B6 closed)" in gp)
    check("gp-decision-complete", "No gate is OPEN" in gp)
    check("rc-matrix-decision-complete", "no row is blocked" in rc)
    check("gp-readiness-cli-correct", "checkin_cli.readiness_cli --profile-root" in gp and "--customer-key" in gp)
    check("gp-activate-cli-correct", "customer_admin --registry" in gp and "--checklist-evidence" in gp)
    check("gp-disable-cli-correct", "disable <customer_key>" in gp)
    check("gp-venv-correct", "/home/cube/projects/richard/hermes-agent/.venv" in gp)
    check("gp-unit-path-correct", "/home/cube/.config/systemd/user/hermes-gateway-dualcoachtest.service" in gp)
    check("gp-generation-event-driven", "no generation CLI" in gp)
    check("gp-hermes-home-binding", "HERMES_HOME=/home/cube/.hermes/profiles/dualcoachtest" in gp)
    check("gp-digest-domain-clarification", "different digest domain" in gp or "different domain" in gp)
    check("gp-pre-reset-command", "reset_controller.py' execute --profile" in gp.replace("\n", " "))
    check("gp-cleanup-commands", "cleanup_controller.py' dry-run" in gp and "cleanup_controller.py' verify" in gp)
    check("gp-harness-commands", '"$PY" -B "$D/invite_harness.py" dry-run' in gp)

    # --- v4: path-existence checks (read-only, on disk) --------------------------
    for path, want_sha, want_mode in PATH_CHECKS:
        name = f"path-{path.name[:40]}"
        if not path.exists():
            check(name, False, "missing")
            continue
        st = path.lstat()
        ok = (not stat.S_ISLNK(st.st_mode)) and stat.S_ISREG(st.st_mode) and sha256_of(path) == want_sha
        if want_mode is not None:
            ok = ok and (stat.S_IMODE(st.st_mode) == want_mode)
        ok = ok and (st.st_uid == os.getuid())
        check(name, ok, f"mode {oct(stat.S_IMODE(st.st_mode))}")
    unit_text = (Path("/home/cube/.config/systemd/user/hermes-gateway-dualcoachtest.service")
                 .read_text(encoding="utf-8"))
    check("path-unit-execstart-profile", "--profile dualcoachtest" in unit_text)
    check("path-unit-hermes-home-exact", 'Environment="HERMES_HOME=/home/cube/.hermes/profiles/dualcoachtest"'
          in unit_text)
    check("path-unit-no-token-env", "TELEGRAM_BOT_TOKEN" not in unit_text)

    # --- v4: no-secret checks -----------------------------------------------------
    token_pattern = re.compile(r"\b\d{8,12}:[A-Za-z0-9_-]{35}\b")
    check("gp-no-secret-token-pattern", token_pattern.search(gp) is None)
    check("rc-no-secret-token-pattern", token_pattern.search(rc) is None)
    env_raw = Path("/home/cube/.hermes/profiles/dualcoachtest/.env").read_text(encoding="utf-8")
    m = re.findall(r"(?m)^\s*TELEGRAM_BOT_TOKEN\s*=\s*(\S+)\s*$", env_raw)
    check("env-exactly-one-token", len(m) == 1 and len(m[0]) == 46 and
          hashlib.sha256(m[0].encode()).hexdigest() ==
          "d0aacf0f4bdbb7c04e12769947c498ee869248972b36f099800a3d3c27c0a3a8")
    if m:
        check("gp-raw-token-absent", m[0] not in gp)
        check("rc-raw-token-absent", m[0] not in rc)
    for label, text in (("gp", gp), ("rc", rc)):
        bad_lines = [ln for ln in text.splitlines()
                     if "39664143" in ln and not any(k in ln.lower() for k in PROHIBITION_MARKERS)]
        check(f"{label}-no-unapproved-api-id", not bad_lines, f"{len(bad_lines)} lines")

    # --- v4: obsolete-path-negative checks ----------------------------------------
    for label, text in (("gp", gp), ("rc", rc)):
        bad = [ln for ln in text.splitlines()
               if any(n in ln for n in OBSOLETE_NEEDLES)
               and not any(k in ln.lower() for k in PROHIBITION_MARKERS)]
        check(f"{label}-obsolete-only-in-prohibition", not bad, f"{len(bad)} lines")
    check("gp-g14-prohibits-session-files", "never create, copy, restore, or require the obsolete session files"
          in gp)

    # --- v4: archive CLI checks -----------------------------------------------------
    exact_cli = ("verify_retained_archives.py .omo/evidence/task26/retained-archive-schema-inventory.json "
                 "--permission-seal .omo/evidence/task26/retained-archive-verifier-permission-seal.json")
    check("rc-archive-cli-exact", exact_cli in rc)
    check("rc-archive-cli-exitcodes", "exit 0 = PASS, exit 1 = FAIL" in rc)
    check("gp-archive-seal-pins", "28ff72fd" in gp and "12b97aa7" in gp and "29db87f2" in gp
          and "597a37e4" in gp)

    # --- v4: G14 predicate content --------------------------------------------------
    check("gp-g14-row-bot-api", "| G14 | Bot API authority" in gp)
    check("gp-g14-section-12", "### 1.2 Bot API authority predicates" in gp)
    check("gp-g14-token-proof", "exactly one nonempty `TELEGRAM_BOT_TOKEN`" in gp and "length 46" in gp)
    check("gp-g14-loaded-source-equality", "Loaded/source equality" in gp or "loaded/source equality" in gp)
    check("gp-g14-topic-zero", "topic_id `0`" in gp)
    check("gp-g14-review-topic-59", "topic_id `59`" in gp)
    check("gp-g14-five-modules", all(h in gp for h in [
        "a8191eed20ca75abcb03c33ac0c0b915cd810249466e816c6a1a55ba154af188",
        "f5d51008f1e8c8ee102df930fa68c7945a3276a249187adc7b7f6ba4b1e8e134",
        "b41060dea28eb3bbb83217068f5218d5df2c879dba00e73c9ca4e577a6049dad",
        "849791bc3b31145a691b8ef4defacba72aeb5de0fca12427988ec1ddd4e9fffa",
        "b48e2b49f0939316e7e4368c940492d1b8a94863da7e5c8141f7f67a1fe08efb"]))
    check("gp-g14-zero-obsolete-scan", "Zero obsolete-path references" in gp)
    check("rc-g14-matrix-row", "| Bot API authority G14 | all Golden Path Section 1.2 predicates hold |" in rc)
    check("rc-g14-prohibition", "never create, copy, restore, or require the obsolete session files" in rc)

    # --- receipt (v4) ------------------------------------------------------------
    if RECEIPT.exists():
        try:
            data = json.loads(RECEIPT.read_text(encoding="utf-8"))
        except json.JSONDecodeError as exc:
            data = None
            check("receipt-valid-json", False, str(exc))
        if data is not None:
            check("receipt-valid-json", True)
            check("receipt-schema", data.get("schema") == "task26-runbook-binding-receipt-v4")
            check("receipt-candidate", data.get("candidate") == CANDIDATE)
            check("receipt-wheel", data.get("wheel_sha256") == WHEEL)
            check("receipt-plan", data.get("plan_sha256") == PLAN)
            check("receipt-actor", data.get("actor") == ACTOR)
            dumped = json.dumps(data)
            check("receipt-supersedes-v3", "ff230719eb5a69ae821bb0a4d58356d59b2803c490d692659a197e22083793c7" in dumped)
            check("receipt-supersedes-v2", "633d5c25ce70cb9ab5d19c434122b14b46fb7108962ab6e453ce6ab3ede1848f" in dumped)
            check("receipt-self-hash-present", bool(data.get("self_sha256")))
            check("receipt-verdict", data.get("verdict", {}).get("task_verdict") == "PASS")
            check("receipt-ready-token", data.get("verdict", {}).get("readiness_token") == "READY_FOR_REHEARSAL_PREFLIGHT")
            check("receipt-no-open-blockers", not data.get("verdict", {}).get("open_blockers"))
    else:
        check("receipt-present", True, "skip: receipt written after lint")

    return report()


def report() -> int:
    failed = [r for r in results if not r["ok"]]
    for r in results:
        if not r["ok"]:
            print(f"FAIL {r['check']}: {r['detail']}")
    out = BASE / "lint-results-v4.json"
    out.write_text(json.dumps({"checks": results, "total": len(results),
                               "failed": len(failed)}, indent=2) + "\n", encoding="utf-8")
    print(f"{'PASS' if not failed else 'FAIL'} ({len(results) - len(failed)}/{len(results)} checks) -> {out.name}")
    return 0 if not failed else 1


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