#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""repo_gates.py — THE entry point for this repo's gates. Run from the repo root:

    python3 scripts/repo_gates.py            # every gate
    python3 scripts/repo_gates.py --fast     # only gates that touch no network and no container
                                             # runtime (what .githooks/pre-push runs)

Gates, in order (all must pass; **non-zero exit on any failure**):

  1. site            website HTML: BOM, emoji, nav/footer, analytics, CDN, tokens, cache-busting
  2. hostinstall     felhom-host-install.sh's five drill-swept invariants (+ R-94's absent-version)
  3. hub-confirm     no native confirm()/prompt() in hub templates
  4. manifest-bearer no bearer-shaped literal anywhere in manifests/
  5. reuse-refs      every path cited by this repo's REUSE.md still resolves
  6. instructions    CLAUDE.md length/versions/TEMPORARY, rule-file scoping, workspace-copy identity
  7. golden-currency a released controller has a golden carrying it (R-242)

WHY 7 IS HERE (2026-08-08, R-242). R-242 was filed as a rule with no mechanism — *a controller
release is not finished until a golden carries it* — and RECURRED THE NEXT DAY: v0.206.0 shipped
while the vouched golden still carried 0.205.0, so a machine installed that morning would have got
neither of the R-241 fixes. Two occurrences in two days, the first (R-239) invisible until a walk
measured it from the customer's side. It is `--fast` because both the pre-push hook and CI run only
`--fast`; a non-fast gate would run in neither, which is the R-29 failure this runner ended. That
constraint is why it checks the BAKE and not the vouch — the full reasoning is in its docstring.

WHY 6 IS HERE AND WAS NOT (2026-08-06, R-229 deferred leg). instructions_gate.py LIVES in this
repo's scripts/ and was registered in the controller and agent runners on the day it was written —
but not in this one, because this repo's own CLAUDE.md was still 27 lines over the ceiling and a
registered-but-failing gate refuses every push through .githooks/pre-push. The file was trimmed
(227 -> 115 effective lines, core + .claude/rules/) and the gate registered in the same session.
A check that does not run in the place it applies is the exact failure the R-29 gate census found.

WHY THIS FILE EXISTS (2026-08-02, closing R-29 leg (a) and half of leg (b)).

A census of all thirteen gate scripts across the four felhom repos found one clean correlation:
**every check a CLAUDE.md tells a person to run was passing, and two of the four nobody is told
to run were failing** — one since 14 July. Neither failure was harmful in effect, which was
checked line by line; nothing would have said so if they had been. The fix is not more gates, it
is one place to run them from. `app-catalog-felhom.eu/scripts/catalog_gates.py` is the canonical
shape (R-161) and this copies it deliberately rather than inventing a second one.

`site_gates.py` is a GATE — eight assertions in one file — and is NOT the model for this file. A
runner that invokes separate gates is the shape that survives; copying site_gates would just add
a ninth monolith.

FAIL-CLOSED. A gate script that is missing is a FAILURE, never a skip, and the exact path tried
is printed. A runner that quietly drops a gate is the inert-seam failure this project has shipped
four times.

EXIT CODES. Each gate returns 0 clean / 1 convicted / 2 inconclusive. This runner exits non-zero
if any gate is non-zero, and reports 2 distinctly as INCONCLUSIVE — an undetermined result is
never a pass, but it is not a conviction either, and the operator needs to know which they have.
"""
import os
import subprocess
import sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SCRIPTS = os.path.join(ROOT, "scripts")

# (label, absolute script path, args, fast)
GATES = [
    ("site",            os.path.join(SCRIPTS, "site_gates.py"),         [],       True),
    ("hostinstall",     os.path.join(SCRIPTS, "hostinstall_gates.py"),  [],       True),
    ("hub-confirm",     os.path.join(SCRIPTS, "hub_confirm_gate.py"),   [],       True),
    ("manifest-bearer", os.path.join(SCRIPTS, "manifest_bearer_gate.py"), [],     True),
    ("reuse-refs",      os.path.join(SCRIPTS, "reuse_refs_check.py"),   [ROOT],   True),
    ("instructions",    os.path.join(SCRIPTS, "instructions_gate.py"),  [ROOT],   True),
    ("golden-currency", os.path.join(SCRIPTS, "golden_currency_gate.py"), [],      True),
    ("wire-contract",   os.path.join(SCRIPTS, "wire_contract_gate.py"),   [],      True),
    # R-324 — the hub composes every customer e-mail and renders the binding pages, and until
    # 2026-08-13 no guard in either repo had ever looked at them. Fast: pure file reads.
    ("hub-copy",        os.path.join(SCRIPTS, "hub_copy_gate.py"),        [],      True),
]

VERDICT = {0: "OK", 1: "FAILED", 2: "INCONCLUSIVE"}


def hooks_armed_note(root):
    """Print a WARNING (never a failure) when this clone's pre-push hook is not switched on.

    core.hooksPath is local config and a clone does not carry it, so an unarmed clone is silent
    by construction — this is the only place it becomes visible.
    """
    try:
        val = subprocess.check_output(["git", "config", "--get", "core.hooksPath"],
                                      cwd=root, stderr=subprocess.DEVNULL).decode().strip()
    except Exception:
        val = ""
    norm = val.replace("\\", "/").rstrip("/")
    if norm == ".githooks" or norm.endswith("/.githooks"):
        return
    print("WARNING: this clone is UNARMED — core.hooksPath is %s, so the pre-push hook will not\n"
          "         run here. Switch it on once with:  git config core.hooksPath .githooks"
          % (("'" + val + "'") if val else "unset"))


def run_gate(label, path, args):
    if not os.path.exists(path):
        print("\nFAIL: gate '%s' is MISSING — tried %s" % (label, path))
        print("      A missing gate is a failure, never a skip (fail-closed).")
        return 1
    print("\n" + "=" * 78)
    print("== gate: %s   (%s%s)" % (label, os.path.basename(path),
                                    (" " + " ".join(args)) if args else ""))
    print("=" * 78, flush=True)
    # stream the gate's own output rather than capturing it — its diagnostics are the point,
    # and a runner that swallows them makes a conviction unreadable.
    return subprocess.call([sys.executable, path] + args, cwd=ROOT)


def main(argv):
    fast = "--fast" in argv
    unknown = [a for a in argv if a != "--fast"]
    if unknown:
        print("unknown argument(s): %s" % " ".join(unknown))
        print("usage: python3 scripts/repo_gates.py [--fast]")
        return 2

    selected = [g for g in GATES if g[3] or not fast]
    skipped = [g[0] for g in GATES if not (g[3] or not fast)]
    print("repo_gates (felhom.eu) — %d gate(s)%s" % (len(selected), "  [--fast]" if fast else ""))
    if skipped:
        print("  --fast SKIPPED (deliberate periodic runs, never in a hook): %s" % ", ".join(skipped))
    hooks_armed_note(ROOT)

    results = [(label, run_gate(label, path, args)) for label, path, args, _f in selected]

    print("\n" + "=" * 78)
    print("== summary")
    print("=" * 78)
    worst = 0
    for label, rc in results:
        print("  %-18s %-13s (exit %d)" % (label, VERDICT.get(rc, "ERROR"), rc))
        if rc != 0:
            worst = 1 if rc == 1 or worst == 1 else 2
    if worst == 0:
        print("\nall felhom.eu gates OK")
        return 0
    convicted = [l for l, rc in results if rc == 1]
    undecided = [l for l, rc in results if rc not in (0, 1)]
    if convicted:
        print("\nCONVICTED: %s" % ", ".join(convicted))
    if undecided:
        print("UNDETERMINED (never a pass): %s" % ", ".join(undecided))
    return worst


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