"""RUN-003 - Round 00's governance rules, measured on the corpus they govern.

Round 00 fixes two rules every later document is bound by:

  sec. 25  canonical source: UTF-8; math only as $...$ and $$...$$; delimiters
           paired; no other math delimiters; no LaTeX turned into Unicode math;
           no unicode_escape round-trips; no secrets, keys or private data.
  sec. 21.6  claim ledger: every important result carries exactly one of ten
           statuses.

The first is mechanical and is checked as a gate on the 42 canonical documents.
The second is a census: which status words the corpus actually uses, where each
first appears, and whether it is one of the ten. The census is written out as a
status registry this tree fills in, round by round, with each word's definition
when the round that introduces it is read.

Checks (per canonical document; failures listed per document)
  G1 utf8           strict UTF-8 decode, no byte order mark
  G2 display-pairs  an even number of $$ outside code
  G3 inline-pairs   an even number of $ in every paragraph, outside code and $$ blocks
  G4 foreign-delims no \\( \\) \\[ \\] and no equation/align environments outside $$
  G5 escapes        no literal \\uXXXX outside code, no cp1252 mojibake
  G6 private        no API keys, private keys, e-mail addresses, local paths or chat URLs
Measured, not gated:
  M1 unicode-math   Unicode math characters in prose (outside math and code)
  M2 status census  see above

Usage: python code/run003_round00_governance.py [--drill] [--blind CID]
"""
import argparse
import collections
import json
import re

from corpus import SOURCES, TREE, source_bytes, walk_sources

LOG = TREE / "data" / "gate-logs" / "run003-round00-governance.json"
REGISTRY = TREE / "data" / "STATUS_REGISTRY.v1.json"
ALLOWED = ["PROVED-ANALYTIC", "VERIFIED-COMPUTATIONAL", "VERIFIED-THEOREM-APPLICATION",
           "DERIVED-CANDIDATE", "NUMERICAL-EVIDENCE", "CONDITIONAL", "COMPUTE-DEFERRED",
           "OPEN", "REJECTED", "CORRECTED"]
GATE_WORDS = {"OPEN", "CLOSED"}

FENCE = re.compile(r"^(```|~~~).*?^\1[^\n]*$", re.S | re.M)
INLINE_CODE = re.compile(r"`[^`\n]*`")
DISPLAY = re.compile(r"\$\$.*?\$\$", re.S)
INLINE = re.compile(r"(?<!\\)\$[^$\n]*?(?<!\\)\$")
FOREIGN = re.compile(r"\\\(|\\\)|\\\[|\\\]")
ENV = re.compile(r"\\begin\{(equation|align|gather|multline|displaymath|eqnarray)\*?\}")
UESC = re.compile(r"\\u[0-9a-fA-F]{4}")
MOJIBAKE = re.compile("\u00c3[\u0080-\u00bf]|\u00e2\u20ac|\u00ef\u00bc|\u00c2[\u00a0-\u00bf]")
PRIVATE = {
    # prefixes must start a token: "disk-to-Reuleaux-triangle" is prose, not a key
    "api-key": re.compile(r"(?<![A-Za-z0-9])(?:sk-[A-Za-z0-9_\-]{20,}|AKIA[0-9A-Z]{16}"
                          r"|gh[pousr]_[A-Za-z0-9]{30,}|AIza[0-9A-Za-z\-_]{35}|xox[abprs]-[A-Za-z0-9\-]{10,})"),
    "private-key": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
    "email": re.compile(r"[A-Za-z0-9._%+\-]+@[A-Za-z0-9\-]+(?:\.[A-Za-z0-9\-]+)*\.[A-Za-z]{2,}"),
    "local-path": re.compile(r"\b[A-Za-z]:\\[^\\\s]+\\|\b[A-Za-z]:/[^/\s]+/|/(?:Users|home)/[A-Za-z0-9._\-]+/"),
    "chat-url": re.compile(r"https?://(?:chatgpt\.com|chat\.openai\.com)/(?:c|share|g)/"
                           r"|https?://claude\.ai/(?:chat|share)/|https?://gemini\.google\.com/app/"),
}
UNICODE_MATH = re.compile("[\u2190-\u21ff\u2200-\u22ff\u27c0-\u27ef\u2980-\u29ff\U0001d400-\U0001d7ff]")

STATUS_TOKEN = re.compile(r"(?<![A-Za-z0-9_\-])[A-Z][A-Z0-9]+(?:-[A-Z0-9]{2,})+(?![A-Za-z0-9_\-])")
NOT_STATUS = re.compile(r"^(AMRAL|LUC|GPT|UTF|SHA|MPFR|EML|PROGRAM|R\d|P\d)|-R\d\d$|^[A-Z]-\d+$")
BOXED_JUDGMENT = re.compile(r"\\boxed\{\s*\\text\{([^{}]+?):\s*([^{}]+?)\}\s*\}")
# the corpus's commonest form: a heading naming the claim, then a line holding
# only the status in backticks - or the status itself as a heading (R10's ledger).
# Inside the backticks the claim may name itself first: `HANDOFF-A: PASS`.
# (Census v2, 2026-09-24: v1 missed the `NAME: STATUS` form and table cells.)
# (Census v4, 2026-09-25, found reading Round 18: a status may join words with '+' -
# `IMPLEMENTED + VERIFIED` - which v3's character class did not admit.)
CODE_LINE_STATUS = re.compile(r"^(#{1,6}\s*)?`(?:([^`:\n]+?):\s*)?([A-Z][A-Z0-9 \-/+]*[A-Z0-9])`[.。]?[ \t]*$", re.M)
# (Census v3, 2026-09-25, found reading Round 17: v2 missed a boxed judgment whose
# \text{NAME: STATUS} follows other content - \boxed{\text{GLOBAL }0.8350\text{
# CERTIFICATE: STILL OPEN}} - and checklist lines "- name — `STATUS`".)
BOXED_AFTER = re.compile(r"\\boxed\{\s*((?:[^{}]|\{[^{}]*\})*?)\\text\{([^{}:]*?):\s*([A-Z][A-Z0-9 \-/]*[A-Z0-9])\s*\}\s*\}")
DASH_LINE = re.compile(r"^[ \t]*[-*][ \t]+(.+?)[ \t]+—[ \t]+`([A-Z][A-Z0-9 \-/]*[A-Z0-9])`[.。]?[ \t]*$", re.M)
TABLE_ROW = re.compile(r"^\|(.+)\|[ \t]*$", re.M)
TABLE_STATUS = re.compile(r"`([A-Z][A-Z0-9\-]*(?:[ /][A-Z][A-Z0-9\-]*)*)`")
HEADING = re.compile(r"^#{1,6}\s+(.+?)\s*$", re.M)
LINE_STATUS = re.compile(r"(?:狀態|判定|[Ss]tatus)\s*[：:]\s*\**\s*(?:`([^`\n]+)`|([A-Z][A-Z0-9\-]{2,60}))")
SUMMARY_LINE = re.compile(r"^\*\*([^*\n]+?)[：:]\s*([A-Z][A-Z0-9\- /]*?)\*\*\s*$", re.M)
# (Census v5, 2026-09-25. Reading Round 19 showed one more missed form. Before patching
# it, the whole corpus was swept for every position v1-v4 read in one wrapping but not in
# another; each form below is a position already read, written differently.
# - A set-off line - a \boxed{...} line, or a display line outside any box - whose last
#   element is a status, the claim before it: a_{\mathrm{Leb}}\ge0.835\text{ NOT
#   CERTIFIED}; 9278/9278\text{ PASS}; \texttt{CORE}; cell68\to\texttt{...}. Not a
#   judgment there: an implication or relation (\Rightarrow, \neq, \parallel ...), a
#   chain or list of statuses, a count (2766\text{ HARD}), names joined by '+'.
# - A label line "X: `STATUS`" or "X：STATUS", list marker optional (Rounds 20-21's ledgers).
# - A line holding only a plain status, "PASS。" (Rounds 11-14 and 30).
# - A heading holding only a status, "# 34. COMPUTE-DEFERRED".
# - A bold summary whose status carries '+' or a closing 。, or lists math-named items.
# - A status-only code line closed by ， or ；.
# An unmarked capital word counts as a status only if it is hyphenated, has more than one
# word, is one of Round 00's words, or is marked as a status somewhere in the corpus.)
CODE_LINE_STATUS_V5 = re.compile(r"^`([A-Z][A-Z0-9 \-/+]*[A-Z0-9])`[，,；;][ \t]*$", re.M)
WORD = r"[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*"
RUN_AT_END = re.compile(rf"(?<![A-Za-z0-9_\-])({WORD}(?:\s*[+/]\s*{WORD}|\s+{WORD})*)$")
CAPS_RUN = re.compile(rf"(?<![A-Za-z0-9_\-])({WORD}(?:\s*[+/]\s*{WORD}|\s+{WORD})*)(?![A-Za-z0-9_\-])")
IDENT = re.compile(r"^(?:[A-Z]|[A-Z]\d+|[A-Z]\d+-\d+|ROUND\d.*)$")
TEXTLIKE = re.compile(r"\\(texttt|text|textbf|textsf|textrm|mathrm|mathsf|mathtt|mathbf|operatorname)\s*\{([^{}]*)\}")
RULE_OPS = "⇒⇏≠≫∥⊂"
COPULA = re.compile(r"\s*(?:仍為|仍然|仍是|仍|為|是|remains|is|are)$")
LABEL_LINE = re.compile(r"^(?:[-*][ \t]+|\d+[.)][ \t]+)?(?P<name>[^\n`*#|][^\n`]*?)[ \t]*[：:][ \t]*"
                        r"(?:[\u4e00-\u9fff]{1,3}[ \t]*)?(?:`(?P<code>[A-Z][A-Z0-9 \-/+]*[A-Z0-9])`"
                        r"|(?P<plain>[A-Z][A-Z0-9 \-/+]*[A-Z0-9]))[ \t]*[.。,，;；]?[ \t]*$", re.M)
PLAIN_LINE = re.compile(r"^([A-Z][A-Z0-9 \-/+]*[A-Z0-9])[.。]?[ \t]*$", re.M)
PLAIN_HEADING = re.compile(r"^#{1,6}[ \t]+(\d+(?:\.\d+)*\.?[ \t]+)?(?:仍完全|仍為|仍然|仍)?[ \t]*"
                           r"([A-Z][A-Z0-9 \-/+]*[A-Z0-9])[ \t]*$", re.M)
SUMMARY_V5 = re.compile(r"^\*\*([^*\n]+?)[：:][ \t]*([^*\n]+?)\*\*[ \t]*$", re.M)


def boxes(body):
    """(start, end, inner) of every \\boxed{...}, braces balanced to any depth"""
    out, i = [], 0
    while True:
        i = body.find("\\boxed{", i)
        if i < 0:
            return out
        j, depth = i + len("\\boxed{"), 1
        while j < len(body) and depth:
            if body[j - 1] != "\\":
                depth += {"{": 1, "}": -1}.get(body[j], 0)
            j += 1
        out.append((i, j, body[i + len("\\boxed{"):j - 1]))
        i = j


def render(part):
    """a set-off line as plain text, and the contents of its \\texttt{...} pieces"""
    s = part.replace("\n", " ")
    s = re.sub(r"\\begin\{[^{}]*\}|\\end\{[^{}]*\}|&", " ", s)
    s = re.sub(r"\\not\s*\\Rightarrow", " ⇏ ", s)
    s = re.sub(r"\\xRightarrow\{(?:[^{}]|\{[^{}]*\})*\}|\\Longrightarrow|\\Rightarrow|\\implies", " ⇒ ", s)
    s = re.sub(r"\\(?:longrightarrow|rightarrow|mapsto|to)(?![A-Za-z])", " → ", s)
    s = re.sub(r"\\(?:neq|ne)(?![A-Za-z])", " ≠ ", s)
    s = re.sub(r"\\gg(?![A-Za-z])", " ≫ ", s)
    s = re.sub(r"\\parallel(?![A-Za-z])", " ∥ ", s)
    s = re.sub(r"\\subseteq(?![A-Za-z])|\\subset(?![A-Za-z])", " ⊂ ", s)
    s = re.sub(r"\\(?:geq|ge)(?![A-Za-z])", "≥", s)
    s = re.sub(r"\\(?:leq|le)(?![A-Za-z])", "≤", s)
    s = re.sub(r"\\(?:qquad|quad)(?![A-Za-z])|\\[,;:! ]|~|\\rm(?![A-Za-z])", " ", s).replace("\\%", "%")
    explicit = set()
    while True:
        m = TEXTLIKE.search(s)
        if not m:
            break
        if m.group(1) == "texttt":
            explicit.add(m.group(2).strip().rstrip("."))
        s = s[:m.start()] + m.group(2) + s[m.end():]
    return re.sub(r"\s+", " ", s.replace("\\_", "_")).strip(), explicit


def is_status(phrase, known, explicit=()):
    """a capital run that the census counts as a status (see the v5 note above)"""
    if IDENT.match(phrase) or (NOT_STATUS.search(phrase) and not re.search(r"[ +/]", phrase)):
        return False
    if any(len(w) < 2 for w in re.split(r"[\s+/\-]+", phrase) if w):
        return False                   # U-L, C/G: the letters of a formula
    return (phrase in explicit or phrase in known or "-" in phrase or bool(re.search(r"[ +/]", phrase)))


def end_status(t, explicit, known):
    """(claim, status) when a set-off line's last element is a status, else None"""
    t = re.sub(r"\s*(?:\([^()]*\)|（[^（）]*）)\s*[.。]?\s*$", "", t)
    t = re.sub(r"[\s.。,，;；]+$", "", t)
    m = RUN_AT_END.search(t)
    if not m:
        return None
    subject, status = t[:m.start()], m.group(1)
    while True:                        # 7856/7856 MPFR PASS: the backend's name is the claim's
        lead = re.match(rf"({WORD})\s+(?=\S)", status)
        if not lead or not NOT_STATUS.search(lead.group(1)):
            break
        subject, status = subject + lead.group(0), status[lead.end():]
    if not is_status(status, known, explicit):
        return None
    if any(op in subject for op in RULE_OPS):
        return None                    # an implication or a relation, not a verdict
    bare = subject.rstrip(" :：=")
    if bare.endswith("+"):
        return None                    # names joined by '+'
    if re.search(r"(?:^|[\s,])\d[\d.]*%?(?:\s+(?:fewer|more|raw))?$", bare):
        return None                    # a count: 2766 HARD, 32.73% fewer HARD
    runs = [r for r in CAPS_RUN.findall(subject) if is_status(r, known, explicit)]
    if runs and not re.sub(r"[\s,，;；→+/.]", "", CAPS_RUN.sub("", subject)):
        return None                    # a chain or a list of statuses
    subject = COPULA.sub("", bare).rstrip(" :：=,，;；")
    return subject, status


def set_off(t, explicit, known):
    """the judgments on one set-off line: one, or one per part of a '+' conjunction"""
    parts = re.split(r"\s*\+\s*", t)
    if len(parts) > 1:
        each = [end_status(p, explicit, known) for p in parts]
        if all(e and e[0] for e in each):
            return each
    one = end_status(t, explicit, known)
    return [one] if one else []


def without_code(text):
    text = FENCE.sub(lambda m: "\n" * m.group(0).count("\n"), text)
    return INLINE_CODE.sub(lambda m: " " * len(m.group(0)), text)


def without_math(text):
    text = DISPLAY.sub(lambda m: "\n" * m.group(0).count("\n"), text)
    return INLINE.sub(" ", text)


def prose_only(text):
    return without_math(without_code(text))


def line_of(text, pos):
    return text.count("\n", 0, pos) + 1


# ---------------------------------------------------------------- the gate

def g1_utf8(docs):
    out = []
    for name, data in docs.items():
        if data.startswith(b"\xef\xbb\xbf"):
            out.append(f"{name}: byte order mark")
        try:
            data.decode("utf-8", "strict")
        except UnicodeDecodeError as e:
            out.append(f"{name}: not UTF-8 at byte {e.start}")
    return out


def decoded(docs):
    return {n: d.decode("utf-8", "replace") for n, d in docs.items()}


def g2_display_pairs(docs):
    out = []
    for name, text in decoded(docs).items():
        n = without_code(text).count("$$")
        if n % 2:
            out.append(f"{name}: {n} display delimiters")
    return out


def g3_inline_pairs(docs):
    out = []
    for name, text in decoded(docs).items():
        body = DISPLAY.sub(lambda m: "\n" * m.group(0).count("\n"), without_code(text))
        # a stray $$ is G2's to report; this check counts single dollars only
        body = body.replace("$$", "  ").replace("\\$", "  ")
        pos = 0
        for para in re.split(r"(\n\s*\n)", body):
            if para.count("$") % 2:
                out.append(f"{name}: odd $ count in paragraph at line {line_of(body, pos)}")
            pos += len(para)
    return out


def g4_foreign_delims(docs):
    out = []
    for name, text in decoded(docs).items():
        body = without_code(text)
        for m in FOREIGN.finditer(body):
            out.append(f"{name}: {m.group(0)} at line {line_of(body, m.start())}")
        outside = DISPLAY.sub(lambda m: "\n" * m.group(0).count("\n"), body)
        for m in ENV.finditer(outside):
            out.append(f"{name}: \\begin{{{m.group(1)}}} outside $$ at line {line_of(outside, m.start())}")
    return out


def g5_escapes(docs):
    out = []
    for name, text in decoded(docs).items():
        body = without_code(text)
        for m in UESC.finditer(body):
            out.append(f"{name}: literal {m.group(0)} at line {line_of(body, m.start())}")
        for m in MOJIBAKE.finditer(body):
            out.append(f"{name}: mojibake at line {line_of(body, m.start())}")
    return out


def g6_private(docs):
    """Scans everything except math - code blocks included, since a path in a
    reproducibility snippet is as much a leak as one in prose. Math is excluded
    only because notation such as f:\\mathbb{R}\\to reads like a drive path."""
    out = []
    for name, text in decoded(docs).items():
        body = without_math(text)
        for kind, rx in PRIVATE.items():
            hits = rx.findall(body)
            if hits:
                # the tree records the kind and count, never the matched text
                out.append(f"{name}: {len(hits)} {kind}")
    return out


CHECKS = {"G1": g1_utf8, "G2": g2_display_pairs, "G3": g3_inline_pairs,
          "G4": g4_foreign_delims, "G5": g5_escapes, "G6": g6_private}


def run_checks(docs):
    return {cid: fn(docs) for cid, fn in CHECKS.items()}


# ---------------------------------------------------------------- measurements

def unicode_math(docs):
    out = {}
    for name, text in decoded(docs).items():
        chars = UNICODE_MATH.findall(prose_only(text))
        if chars:
            out[name] = {"count": len(chars),
                         "distinct": "".join(sorted(set(chars)))}
    return out


def round_no(name):
    m = re.search(r"Round_(\d+)", name)
    return int(m.group(1)) if m else None


SINGLE_STATUS = {"OPEN", "CLOSED", "PASS", "FAIL", "PROVED", "CERTIFIED", "VERIFIED", "PARTIAL",
                 "CANDIDATE", "DEFERRED", "CONDITIONAL", "REJECTED", "CORRECTED", "PENDING",
                 "BLOCKED", "UNMEASURED", "YES", "NO"}
ANSWER_WORDS = {"YES", "NO"}


def plain_tex(s):
    """a boxed claim's name with its \\text{...} wrappers taken off"""
    return re.sub(r"\s+", " ", re.sub(r"\\text\{([^{}]*)\}", r"\1", s)).strip()


def merge_registry(old, new):
    """census v3 into a registry already filled in: occurrences, counts, first rounds and
    classes come from the new census; every definition and review is kept"""
    st, changed = old["statuses"], []
    for s, e in new.items():
        if s not in st:
            st[s] = e
            changed.append(s)
            continue
        keep = {k: st[s][k] for k in ("definition", "reviews") if k in st[s]}
        fresh = {k: e[k] for k in ("first", "judgment_uses", "class", "occurrences")}
        if any(st[s].get(k) != v for k, v in fresh.items()):
            changed.append(s)
        st[s].update(fresh)
        st[s].update(keep)
    missing = sorted(set(st) - set(new))
    return old, changed, missing


def status_census(docs):
    """Status words by use and by position. Judgment positions: boxed
    \\text{X: S}; 'status:' / 狀態： / 判定： lines; bold summary lines **X: S**;
    and, from census v5, the forms described above CODE_LINE_STATUS_V5.
    Fenced code is skipped; inline code is kept, since status words are often
    written in backticks."""
    uses = collections.Counter()
    judged = collections.Counter()
    first = {}
    judgments = []
    order = lambda kv: (round_no(kv[0]) is None, round_no(kv[0]) or 0, kv[0])
    docs_in_order = []
    for name, text in sorted(decoded(docs).items(), key=order):
        r = round_no(name)
        where = f"R{r:02d}" if r is not None else name.split("_")[0]
        body = FENCE.sub(lambda m: "\n" * m.group(0).count("\n"), text)
        docs_in_order.append((where, body, *marked_forms(body)))
    # an unmarked capital word is a status if the corpus marks it as one somewhere
    known = set(ALLOWED) | GATE_WORDS | ANSWER_WORDS | SINGLE_STATUS
    known |= {status for _, _, found, _ in docs_in_order for _, _, status in found}
    for where, body, found, spans in docs_in_order:
        words = [t for t in STATUS_TOKEN.findall(body) if not NOT_STATUS.search(t)]
        words += re.findall(r"(?<![A-Za-z0-9_\-])(" + "|".join(sorted(SINGLE_STATUS)) +
                            r")(?![A-Za-z0-9_\-])", body)
        for w in words:
            uses[w] += 1
            first.setdefault(w, where)
        for form, subject, status in found + v5_forms(body, spans, known):
            # a status is a whole phrase: "NOT YET COMPUTED" is one status, not three
            judged[status] += 1
            first.setdefault(status, where)
            judgments.append({"where": where, "form": form, "subject": subject[:80], "status": status,
                              "round00_vocabulary": status in ALLOWED or status in GATE_WORDS
                                                    or status in ANSWER_WORDS})
    registry = {}
    for s in sorted(judged, key=lambda x: (-judged[x], x)):
        registry[s] = {
            "first": first.get(s),
            "judgment_uses": judged[s],
            "class": "round-00-claim-status" if s in ALLOWED else
                     "round-00-gate-status" if s in GATE_WORDS else
                     "round-00-answer" if s in ANSWER_WORDS else "outside-round-00",
            "occurrences": [[j["where"], j["form"], j["subject"]] for j in judgments if j["status"] == s],
            "definition": None,
        }
    return registry, judgments


def subjects_by_heading(body):
    headings = [(m.start(), m.group(1)) for m in HEADING.finditer(body)]

    def subject_before(pos):
        prior = [h for p, h in headings if p < pos]
        return prior[-1] if prior else ""
    return subject_before


def marked_forms(body):
    """the judgment forms of census v1-v4, and the spans they read"""
    subject_before = subjects_by_heading(body)
    found, boxed_spans = [], []
    for m in BOXED_JUDGMENT.finditer(body):
        found.append(("boxed", m.group(1).strip(), m.group(2).strip()))
        boxed_spans.append((m.start(), m.end()))
    for m in BOXED_AFTER.finditer(body):
        if m.group(1).strip():        # an empty prefix is the form above, already read
            found.append(("boxed", plain_tex(m.group(1) + m.group(2)), m.group(3)))
            boxed_spans.append((m.start(), m.end()))
    found += [("dash-line", m.group(1).strip(), m.group(2)) for m in DASH_LINE.finditer(body)]
    colon_spans = []
    for m in LINE_STATUS.finditer(body):
        if any(a <= m.start() < b for a, b in boxed_spans):
            continue          # "\text{Round 01 status: CLOSED}" is already one boxed judgment
        if m.group(1) is not None:
            name, sep, status = m.group(1).rpartition(":")
            name, status = (name.strip(), status.strip()) if sep else ("", m.group(1).strip())
        else:
            name, status = "", m.group(2)
        if not re.fullmatch(r"[A-Z][A-Z0-9\- /]*", status):
            continue
        found.append(("colon-line", name, status))
        colon_spans.append((m.start(), m.end()))
    found += [("summary-line", m.group(1).strip(), m.group(2).strip()) for m in SUMMARY_LINE.finditer(body)
              if re.fullmatch(r"[A-Z][A-Z0-9\- /]*", m.group(2).strip())]
    for m in CODE_LINE_STATUS.finditer(body):
        if any(a <= m.start() < b for a, b in colon_spans):
            continue
        named = (m.group(2) or "").strip()
        if m.group(1):
            found.append(("code-heading", named, m.group(3)))
        else:
            found.append(("code-line", named or subject_before(m.start()), m.group(3)))
    for row in TABLE_ROW.finditer(body):
        if re.fullmatch(r"[\s|:\-]*", row.group(1)):
            continue
        cells = [c.strip() for c in row.group(1).split("|")]
        for cell in cells[1:]:
            for m in TABLE_STATUS.finditer(cell):
                found.append(("table-cell", cells[0][:80], m.group(1)))
    return found, {"boxed": boxed_spans, "colon": colon_spans}


def v5_forms(body, spans, known):
    """the forms census v5 adds (see the note above CODE_LINE_STATUS_V5)"""
    subject_before = subjects_by_heading(body)
    found = []
    all_boxes = boxes(body)
    for a, b, inner in all_boxes:
        if any(x <= a < y for x, y in spans["boxed"]):
            continue                   # read by the v1-v4 boxed forms
        for part in re.split(r"\\\\", inner):
            t, explicit = render(part)
            found += [("set-off", *j) for j in set_off(t, explicit, known)]
    for m in DISPLAY.finditer(body):
        inner, cut = [], m.start() + 2
        for a, b, _ in all_boxes:
            if m.start() <= a < m.end():
                inner.append(body[cut:a])
                cut = b
        inner.append(body[cut:m.end() - 2])
        for part in re.split(r"\\\\", " ".join(inner)):
            if part.strip():
                t, explicit = render(part)
                found += [("set-off", *j) for j in set_off(t, explicit, known)]
    # text lines: math blanked in place, so offsets and line starts are kept
    text = DISPLAY.sub(lambda m: re.sub(r"[^\n]", " ", m.group(0)), body)
    for m in LABEL_LINE.finditer(text):
        if any(a <= m.start() < b or m.start() <= a < m.end() for a, b in spans["colon"]):
            continue                   # a 狀態： / status: line, already read
        name = m.group("name").strip()
        if re.fullmatch(r"(?:狀態|判定|[Ss]tatus)", name):
            continue
        if m.group("code"):
            found.append(("label-line", name, m.group("code")))
        elif is_status(m.group("plain"), known):
            found.append(("label-line", name, m.group("plain")))
    for m in PLAIN_LINE.finditer(text):
        if is_status(m.group(1), known | {m.group(1)}):   # the position marks it: "PINNED。"
            found.append(("plain-line", subject_before(m.start()), m.group(1)))
    for m in PLAIN_HEADING.finditer(text):
        if is_status(m.group(2), known):
            found.append(("plain-heading", (m.group(1) or "").strip(), m.group(2)))
    read = {m.start() for m in SUMMARY_LINE.finditer(text) if re.fullmatch(r"[A-Z][A-Z0-9\- /]*", m.group(2).strip())}
    for m in SUMMARY_V5.finditer(text):
        if m.start() in read:
            continue
        items = [re.match(r"^(.*?)\s*([A-Z][A-Z0-9 \-/+]*[A-Z0-9])$", i.strip())
                 for i in re.split(r"[；;]", m.group(2).strip().rstrip("。."))]
        if items and all(i and is_status(i.group(2), known) for i in items):
            found += [("summary-line", (m.group(1).strip() + " " + i.group(1)).strip(), i.group(2)) for i in items]
    found += [("code-line", subject_before(m.start()), m.group(1)) for m in CODE_LINE_STATUS_V5.finditer(text)]
    return found


# ---------------------------------------------------------------- drill

def drill(docs):
    target = next(n for n in sorted(docs) if "Round_01_" in n)
    base = docs[target]

    def plant(fn):
        d = dict(docs)
        d[target] = fn(base)
        return d

    para_end = base.index(b"\n\n", 2000)
    ins = lambda s: (lambda b: b[:para_end] + s + b[para_end:])
    cases = [
        ("K0-unmodified", "control", docs, set()),
        ("K1-dollar-in-fenced-code", "control", plant(ins(b"\n\n```\ncost = $5 and $$x\n```\n")), set()),
        ("K2-escaped-dollar-in-prose", "control", plant(ins(b" price \\$5 ")), set()),
        ("K3-bracket-inside-code-span", "control", plant(ins(b" `\\[x\\]` ")), set()),
        ("K4-arxiv-url", "control", plant(ins(b" https://arxiv.org/abs/2608.30538 ")), set()),
        ("K5-colon-backslash-in-math", "control", plant(ins(b" $f:\\mathbb{R}\\to\\mathbb{R}$ ")), set()),
        ("K6-hyphenated-prose-ending-in-sk", "control", plant(ins(b" the disk-to-Reuleaux-triangle-violation-branch ")), set()),
        ("M1-invalid-utf8", "mutation", plant(ins(b" \xff\xfe ")), {"G1"}),
        ("M2-bom", "mutation", plant(lambda b: b"\xef\xbb\xbf" + b), {"G1"}),
        ("M3-display-unpaired", "mutation", plant(ins(b"\n\n$$\nx\n\n")), {"G2"}),
        ("M4-inline-unpaired", "mutation", plant(ins(b" cost $x ")), {"G3"}),
        ("M5-paren-delims", "mutation", plant(ins(b" \\(x\\) ")), {"G4"}),
        ("M6-bracket-delims", "mutation", plant(ins(b"\n\n\\[x\\]\n")), {"G4"}),
        ("M7-equation-env", "mutation", plant(ins(b"\n\n\\begin{equation}x\\end{equation}\n")), {"G4"}),
        ("M8-unicode-escape", "mutation", plant(ins(b" \\u4e2d ")), {"G5"}),
        ("M9-mojibake", "mutation", plant(ins("\u00e2\u20ac\u2122".encode("utf-8"))), {"G5"}),
        ("M10-api-key", "mutation", plant(ins(b" sk-" + b"A1b2C3d4E5f6G7h8I9j0K1l2 ")), {"G6"}),
        ("M11-email", "mutation", plant(ins(b" someone@example.org ")), {"G6"}),
        ("M12-windows-path", "mutation", plant(ins(b" C:\\Users\\someone\\file.md ")), {"G6"}),
        ("M13-posix-home", "mutation", plant(ins(b" /home/someone/work ")), {"G6"}),
        ("M14-chat-url", "mutation", plant(ins(b" https://chatgpt.com/c/0000-1111 ")), {"G6"}),
        # planted secrets are assembled at run time so no scanner mistakes this file for one
        ("M15-private-key", "mutation", plant(ins(b"\n\n-----BEGIN RSA " + b"PRIVATE KEY-----\n")), {"G6"}),
    ]
    out = []
    for cid, kind, world, expected in cases:
        got = run_checks(world)
        tripped = {c for c, f in got.items() if f}
        out.append({"case": cid, "kind": kind, "expected": sorted(expected),
                    "tripped": sorted(tripped), "ok": tripped == expected})
    return out


def census_drill(docs):
    """The census is a measurement, not a gate - but it must see what is
    planted, classify it correctly, and not see what is fenced off."""
    target = next(n for n in sorted(docs) if "Round_05_" in n)
    tail = docs[target]
    base = collections.Counter((j["status"], j["round00_vocabulary"]) for j in status_census(docs)[1])

    def delta(snippet):
        d = dict(docs)
        d[target] = tail + snippet
        now = collections.Counter((j["status"], j["round00_vocabulary"]) for j in status_census(d)[1])
        return now - base, base - now

    cases = [
        ("C1-ten-word-status-seen", b"\n\n## planted claim\n\n`PROVED-ANALYTIC`\n",
         collections.Counter({("PROVED-ANALYTIC", True): 1})),
        ("C2-new-status-seen-as-outside", b"\n\n## planted claim\n\n`MADE-UP-STATUS`\n",
         collections.Counter({("MADE-UP-STATUS", False): 1})),
        ("C3-boxed-gate-seen", b"\n\n$$\n\\boxed{\n\\text{PLANTED GATE: CLOSED}\n}\n$$\n",
         collections.Counter({("CLOSED", True): 1})),
        ("C4-fenced-status-ignored", b"\n\n```\n`MADE-UP-STATUS`\n```\n", collections.Counter()),
        ("C5-named-code-line", b"\n\n`PLANTED-GATE: MADE-UP-STATUS`\n",
         collections.Counter({("MADE-UP-STATUS", False): 1})),
        ("C6-status-line-with-name", "\n\n狀態：\n\n`PLANTED-NAME: MADE-UP-STATUS`\n".encode("utf-8"),
         collections.Counter({("MADE-UP-STATUS", False): 1})),
        ("C7-table-cell", b"\n\n| item | verdict |\n|---|---|\n| planted | `MADE-UP-STATUS` |\n",
         collections.Counter({("MADE-UP-STATUS", False): 1})),
        ("C8-boxed-after-math", b"\n\n$$\n\\boxed{\n\\text{PLANTED }0.1\\text{ CLAIM: MADE-UP-STATUS}\n}\n$$\n",
         collections.Counter({("MADE-UP-STATUS", False): 1})),
        ("C9-dash-checklist-line", "\n\n- planted claim — `MADE-UP-STATUS`\n".encode("utf-8"),
         collections.Counter({("MADE-UP-STATUS", False): 1})),
        ("C10-plus-joined-status", b"\n\n## planted claim\n\n`MADE-UP + STATUS`\n",
         collections.Counter({("MADE-UP + STATUS", False): 1})),
        # census v5: one case per added form, and one per thing a set-off line can be
        # without being a judgment
        ("C11-set-off-claim-then-status", b"\n\n$$\n\\boxed{a_{\\mathrm{Leb}}\\ge0.1\\text{ MADE-UP-STATUS}.}\n$$\n",
         collections.Counter({("MADE-UP-STATUS", False): 1})),
        ("C12-set-off-conjunction", b"\n\n$$\n\\text{root MADE-UP-STATUS}+\\text{upper MADE-UP-STATUS}.\n$$\n",
         collections.Counter({("MADE-UP-STATUS", False): 2})),
        ("C13-set-off-count-ignored", b"\n\n$$\n\\boxed{2766\\text{ MADE-UP-STATUS}}\n$$\n", collections.Counter()),
        ("C14-set-off-implication-ignored", b"\n\n$$\n\\text{MADE-UP-STATUS}\\Rightarrow\\text{PROVED}\n$$\n",
         collections.Counter()),
        ("C15-label-line", b"\n\n- planted claim: `MADE-UP-STATUS`\n",
         collections.Counter({("MADE-UP-STATUS", False): 1})),
        ("C16-plain-status-line", "\n\n## planted claim\n\nMADE-UP-STATUS。\n".encode("utf-8"),
         collections.Counter({("MADE-UP-STATUS", False): 1})),
        ("C17-plain-status-heading", b"\n\n# 99. COMPUTE-DEFERRED\n",
         collections.Counter({("COMPUTE-DEFERRED", True): 1})),
        ("C18-bold-summary-with-plus", "\n\n**planted claim：MADE-UP + STATUS。**\n".encode("utf-8"),
         collections.Counter({("MADE-UP + STATUS", False): 1})),
        ("C19-vocabulary-list-ignored", b"\n\n- `MADE-UP-STATUS`\n- `OTHER-MADE-UP`\n", collections.Counter()),
    ]
    out = []
    for cid, snippet, expected in cases:
        added, removed = delta(snippet)
        out.append({"case": cid, "ok": added == expected and not removed,
                    "added": {f"{s} ({'in' if v else 'out'})": n for (s, v), n in added.items()}})
    return out


# ---------------------------------------------------------------- main

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--drill", action="store_true")
    ap.add_argument("--blind", metavar="CID")
    args = ap.parse_args()
    manifest = json.loads((TREE / "data" / "gate-logs" / "run001-corpus-identity.json").read_text(encoding="utf-8"))
    canonical_ids = {row["archived"][0] for row in manifest["correspondence"]}
    blobs = {n: source_bytes(n) for n in SOURCES}
    docs = {}
    for leaf, data in walk_sources(blobs, keep=lambda l: l.id in canonical_ids):
        if data is not None:
            docs[leaf.name] = data
    assert len(docs) == 42, len(docs)
    if args.blind:
        CHECKS[args.blind] = lambda d: []
        args.drill = True

    results = run_checks(docs)
    for cid, f in results.items():
        print(f"{cid} {'PASS' if not f else 'FAIL'}  ({len(f)})")
        for x in f[:12]:
            print("     ", x)
    registry, judgments = status_census(docs)
    umath = unicode_math(docs)
    log = {
        "run": "RUN-003",
        "subject": "Round 00 sec. 25 and sec. 21.6 on the 42 canonical documents",
        "checks": {cid: {"pass": not f, "failures": f} for cid, f in results.items()},
        "unicode_math_in_prose": umath,
        "judgments": judgments,
    }
    if args.drill:
        d = drill(docs)
        plants = [x for x in d if x["kind"] == "mutation"]
        controls = [x for x in d if x["kind"] == "control"]
        held = sum(1 for x in d for c in CHECKS if c not in x["expected"] and c not in x["tripped"])
        quiet = sum(1 for x in d for c in CHECKS if c not in x["expected"])
        log["drill"] = {"plants": len(plants), "plants_caught_by_named_checks": sum(x["ok"] for x in plants),
                        "controls": len(controls), "controls_undisturbed": sum(x["ok"] for x in controls),
                        "must_stay_green_assertions": quiet, "must_stay_green_held": held, "cases": d}
        print(f"drill: {log['drill']['plants_caught_by_named_checks']}/{len(plants)} plants caught by exactly the "
              f"named checks; {log['drill']['controls_undisturbed']}/{len(controls)} controls undisturbed; "
              f"{held}/{quiet} must-stay-green assertions held")
        for x in d:
            if not x["ok"]:
                print("   DRILL FAIL", x)
        cd = census_drill(docs)
        log["census_drill"] = cd
        print(f"census drill: {sum(x['ok'] for x in cd)}/{len(cd)}")
        for x in cd:
            if not x["ok"]:
                print("   CENSUS DRILL FAIL", x)
        if args.blind:
            n = sum(1 for x in d if args.blind in x["expected"] and args.blind not in x["tripped"])
            print(f"blind {args.blind}: drill reported it blind in {n} cases (nothing written)")
            return
    LOG.write_text(json.dumps(log, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
    if not REGISTRY.exists():
        REGISTRY.write_text(json.dumps({
            "note": ("every status phrase found in a judgment position, by an approximate "
                     "extractor (some entries are identifiers, not statuses); 'definition' is "
                     "filled in by the round of this tree that reads the document introducing it"),
            "round_00_claim_statuses": ALLOWED,
            "statuses": registry}, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
        print(f"wrote status registry: {len(registry)} status phrases")
    elif args.drill:
        reg, changed, missing = merge_registry(json.loads(REGISTRY.read_text(encoding="utf-8")), registry)
        if changed:
            REGISTRY.write_text(json.dumps(reg, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
        print(f"status registry: {len(changed)} phrases updated from the census {sorted(changed)}; "
              f"{len(missing)} no longer found {missing}")


if __name__ == "__main__":
    main()
