#!/usr/bin/env python3
"""Build-failing gate for the report builder.  Run: python3 _build_selftest.py  (exit 0 = green)

Catches the failure classes that have actually bitten this suite:
  1. a SyntaxError in _build.py (it is f-string-heavy — one bad nested quote breaks the whole build)
  2. a build that crashes at runtime (so _refresh.py would print "rebuilt" over stale files)
  3. a STALE HARDCODED COUNT — any "<n> scars / lessons / L-scars" in a rendered page that disagrees
     with len(scars.json).  This is the "119 vs 121" trap: the count must be DERIVED, never typed.
Wire this into ship-kit.sh (or run before serving) so a stale-count regression fails the build."""
import ast, json, os, re, subprocess, sys

os.chdir(os.path.dirname(os.path.abspath(__file__)))
FAILS = []
check = lambda name, ok, detail="": (print(f"  {'PASS' if ok else 'FAIL'}  {name}{'' if ok else '  — ' + detail}"),
                                     FAILS.append(name) if not ok else None)

# 1) _build.py parses
syntax_ok, syntax_err = True, ""
try:
    ast.parse(open("_build.py").read())
except SyntaxError as e:
    syntax_ok, syntax_err = False, str(e)
check("#1 _build.py parses (no syntax error)", syntax_ok, syntax_err)

# 2) the build runs clean
r = subprocess.run([sys.executable, "_build.py"], capture_output=True, text=True)
check("#2 _build.py runs (exit 0)", r.returncode == 0, r.stderr[-600:])

# 3) ground truth from the data the builder reads
NS = len(json.load(open("scars.json")))
KIT = json.load(open("_kit-data.json"))
COMMANDS = ["/" + c["name"] for c in KIT["commands"]]
ORCH_COMMANDS = ["/" + c["name"] for c in KIT["commands"] if not c["god_only"]]
GOD_ONLY = ["/" + c["name"] for c in KIT["commands"] if c["god_only"]]
NC = len(COMMANDS)
STAMP = f"generated {KIT['generated_date']} · {NS} scars · {NC} commands"
check("#3 canonical command metadata is non-empty + exact-prefix classified",
      bool(COMMANDS) and all(c["god_only"] == c["description"].startswith("GOD-KIT ONLY") for c in KIT["commands"]))

# 4) every rendered page exists, is non-trivial, and carries NO scar/lesson count != NS.
#    Strip <script>…</script> first: the embedded SEQS JSON contains → (→) escapes whose
#    digits ("…2192 lessons.yaml") would false-positive the count regex.  Visible copy only.
SUITE_PAGES = ["index.html", "kit-atlas.html", "scar-codex.html", "god-kit.html",
               "orchestration-kit.html", "entabeni-kit.html", "terminus-kit.html",
               "terminus-findings.html", "kit-drift.html"]
GENERATED_PAGES = [p for p in SUITE_PAGES if p != "terminus-findings.html"]
# Two shapes carry a scar count: inline text ("· 121 scars", "All 121 lessons") and a stat tile
# (<b data-n=121>0</b><span>scars</span> — number and label split by tags, so \s+ never matches it).
# The stat form captures the LABEL so we can skip Entabeni's "EL-scars" (its own id-space, ≠ NS).
COUNT = re.compile(r"(\d+)\s+(?:scars|lessons|L-scars)\b")
STAT = re.compile(r"data-n=(\d+)>0</b><span>([^<]*)</span>")
is_godkit_scar_label = lambda lbl: lbl.strip().lower() in {"scars", "lessons", "l-scars", "same scars"}
CANONICAL_COUNT_PAGES = {"index.html", "kit-atlas.html", "scar-codex.html",
                         "god-kit.html", "orchestration-kit.html"}
for p in SUITE_PAGES:
    if not os.path.exists(p):
        check(f"#4 {p} rendered", False, "missing")
        continue
    txt = open(p).read()
    check(f"#4 {p}: non-empty", len(txt) > 500, f"small ({len(txt)}B)")
    if p not in GENERATED_PAGES:
        continue
    visible = re.sub(r"<script.*?</script>", "", txt, flags=re.S)
    # Historical findings, derived-kit TL/EL prose, drift-instance counts, and era-subset stats are
    # legitimate non-canonical numbers.  The invariant applies to canonical page chrome only.
    cuts = [x for marker in ('<div class="card', '<h2>Every file') if (x := visible.find(marker)) != -1]
    scan = visible[:min(cuts)] if cuts else visible
    nums = [int(n) for n in COUNT.findall(scan)] if p in CANONICAL_COUNT_PAGES else []
    nums += [int(n) for n, lbl in STAT.findall(scan) if is_godkit_scar_label(lbl)] if p in CANONICAL_COUNT_PAGES else []
    stale = sorted({n for n in nums if n != NS})
    check(f"#4 {p}: canonical count + generated stamp consistent",
          not stale and STAMP in txt and "frozen July" not in txt and "frozen july" not in txt,
          f"stale count(s) {stale} != {NS}, missing stamp, or frozen marker remains")

# 5) the index scar-strip renders exactly one tick per lesson
ticks = open("index.html").read().count("class=tick") if os.path.exists("index.html") else -1
check(f"#5 index scar strip has {NS} ticks", ticks == NS, f"found {ticks}")

# 6) Command cards are the canonical manifests, not a hand-maintained allowlist.  The JSON sequence
#    payload must cover every card; commands without a diagram carry the explicit honest fallback.
god = open("god-kit.html").read() if os.path.exists("god-kit.html") else ""
orch = open("orchestration-kit.html").read() if os.path.exists("orchestration-kit.html") else ""
cards = re.findall(r'data-cmd="(/[^"]+)"', god)
orch_cards = re.findall(r'data-cmd="(/[^"]+)"', orch)
check(f"#6 god command grid exactly matches {NC} canonical manifests", cards == COMMANDS,
      f"rendered {len(cards)} cards: {cards}")
check(f"#6 orchestration grid excludes exactly {len(GOD_ONLY)} GOD-KIT ONLY commands",
      orch_cards == ORCH_COMMANDS, f"rendered {len(orch_cards)} cards")
check("#6 required fresh commands are visible", all(c in cards for c in ("/debrief", "/docker-purge", "/boot-cost")))
check("#6 god-only chip/badge count is derived", god.count("<span class=scope>GOD-KIT ONLY</span>") == len(GOD_ONLY))
seq_match = re.search(r"const SEQS=(\{.*?\});const th=", god, re.S)
try:
    seqs = json.loads(seq_match.group(1)) if seq_match else {}
except json.JSONDecodeError:
    seqs = {}
fallback_ok = all(c in seqs and (seqs[c].get("recorded", True) or
                  any(step and step[0] == "no firing sequence recorded yet" for step in seqs[c].get("steps", [])))
                  for c in COMMANDS)
check("#6 every command has a firing diagram or honest fallback detail", fallback_ok)

# 7) Historical/hand-authored surfaces must identify themselves as snapshots.
check("#7 findings page is visibly static", "static as of July 20, 2026" in open("terminus-findings.html").read())
check("#7 derived-kit snapshots are visibly dated",
      "static as of" in open("entabeni-kit.html").read() and "static as of" in open("terminus-kit.html").read())
check("#7 hand-authored atlas/diagram metadata is visibly dated",
      all("static as of" in open(p).read() for p in
          ("kit-atlas.html", "scar-codex.html", "god-kit.html", "orchestration-kit.html")))
check("#7 drift data exposes its independent scan timestamp",
      "scanned " + json.load(open("drift.json"))["scanned"] in open("kit-drift.html").read())

if FAILS:
    print(f"\nBUILDER GATE RED — {len(FAILS)} failing: {', '.join(FAILS)}")
    sys.exit(1)
print(f"\nBUILDER GATE GREEN — reports consistent at {NS} scars")
