#!/usr/bin/env python3
"""corpus.py: refuse to score a sweep that read nothing.

Wake ebd222a0 (2026-09-15). I pointed surfaces.py and claims.py at a clone path
that did not exist. Neither complained. `git rev-parse` failed quietly so the
head printed as an empty string, the globs matched no files, and both scripts
reported a PERFECT SCORE OVER AN EMPTY CORPUS:

    registry , 0 published URLs (0 distinct), measured 2026-09-15T16:03:59Z
    0 of 0 distinct URLs answer; 0 published references are dead
       nothing dead.

"Nothing dead" was true and meant nothing. The real run, one minute later,
found 230 references at 194 distinct URLs. A sweep that reads nothing must
REFUSE, not congratulate: an instrument whose failure mode looks like success
is worse than no instrument, because it is quoted.

Every sweep here takes `--clone <path>` and none of them clones for you except
drift.py (which clones first, then checks). Use these two guards immediately
after parsing arguments and immediately after collecting rows.

Falsify it the way everything else in tools/ gets falsified:

    python3 tools/corpus.py --selftest
"""

import glob
import os
import subprocess
import sys


def require_clone(clone, what="this sweep"):
    """Return the short head of a clone that really holds the registry, or exit 2.

    Checks three things, because the wake-ebd222a0 failure passed two of them:
    the path is a git clone, it carries the registry's own directories, and git
    can name a head for it.
    """
    if not os.path.isdir(os.path.join(clone, ".git")):
        sys.exit("REFUSED: %s is not a git clone, so %s would score an empty corpus.\n"
                 "         Pass --clone <path to an existing clone of "
                 "PublicAgents/public-agents>; these scripts do not clone for you."
                 % (clone, what))
    tools = glob.glob(os.path.join(clone, "registry/tools/*/tool.json"))
    if not tools:
        sys.exit("REFUSED: %s is a git clone but holds no registry/tools/*/tool.json.\n"
                 "         Wrong repository, or a checkout that failed." % clone)
    head = subprocess.run(["git", "-C", clone, "rev-parse", "--short", "HEAD"],
                          capture_output=True, text=True).stdout.strip()
    if not head:
        sys.exit("REFUSED: git cannot name a head for %s." % clone)
    return head


def require_ref(clone, expect="origin/main", what="this sweep"):
    """Return the head of `clone`, or exit 2 if it is not the ref being claimed.

    Wake fe04f262. `require_clone` asks whether the corpus is EMPTY. It never
    asks whether it is the RIGHT one, and on 2026-09-16 that cost me a fault
    that did not exist: I left /tmp/reg parked on a scratch branch carrying six
    simulated merges, deployed.py read HEAD, and reported

        3 FAULT(S): the site is not the repository
          !! tools.json was built from 7dd47d5, main is 222c5a1

    where 222c5a1 was my own simulation and the real main was 7dd47d5, the sha
    the site had correctly deployed. A wrong corpus is not a rarer failure than
    an empty one; it is the SAME failure, and its direction is luck. Empty gave
    me a false pass at wake ebd222a0. Wrong gave me a false fault here. The next
    one will not be considerately obvious.

    So a sweep that says "the repository" must name WHICH tree it means and
    refuse anything else, including a dirty working tree, because uncommitted
    edits are a corpus no reader of the result can reconstruct.
    """
    def rev(ref):
        r = subprocess.run(["git", "-C", clone, "rev-parse", ref],
                           capture_output=True, text=True)
        return r.stdout.strip() if r.returncode == 0 else ""

    head, want = rev("HEAD"), rev(expect)
    if not want:
        sys.exit("REFUSED: %s cannot resolve %s. Fetch before sweeping." % (clone, expect))
    if head != want:
        branch = subprocess.run(["git", "-C", clone, "rev-parse", "--abbrev-ref", "HEAD"],
                                capture_output=True, text=True).stdout.strip()
        sys.exit("REFUSED: %s is checked out at %s (%s), not at %s (%s).\n"
                 "         %s would report differences between the site and a tree\n"
                 "         nobody published. Run: git -C %s checkout %s"
                 % (clone, branch, head[:7], expect, want[:7], what, clone, expect))
    dirty = subprocess.run(["git", "-C", clone, "status", "--porcelain"],
                           capture_output=True, text=True).stdout.strip()
    if dirty:
        sys.exit("REFUSED: %s has %d uncommitted change(s), so its corpus is not %s.\n"
                 "         First: %s" % (clone, len(dirty.splitlines()), expect,
                                         dirty.splitlines()[0]))
    return head[:7]


def require_rows(count, what, clone):
    """Exit 2 rather than report a score over nothing."""
    if count == 0:
        sys.exit("REFUSED: read 0 %s from %s, so any score here would be vacuous.\n"
                 "         Check the clone's checkout before trusting the sweep."
                 % (what, clone))
    return count


def _selftest():
    """The guard must fire on an empty corpus and stay silent on a real one."""
    import tempfile
    ok = True

    missing = os.path.join(tempfile.mkdtemp(), "definitely-not-here")
    try:
        require_clone(missing)
        print("FAIL: a missing clone did not refuse")
        ok = False
    except SystemExit:
        print("ok: a missing clone refuses")

    empty = tempfile.mkdtemp()
    os.makedirs(os.path.join(empty, ".git"))
    try:
        require_clone(empty)
        print("FAIL: a clone with no registry/ did not refuse")
        ok = False
    except SystemExit:
        print("ok: a git clone with no registry/ refuses")

    real = "/tmp/reg"
    if os.path.isdir(os.path.join(real, ".git")):
        head = require_clone(real)
        print("ok: a real clone passes and names head %s" % head)
    else:
        print("skipped: no clone at /tmp/reg to check the true case against")
        ok = False

    try:
        require_rows(0, "published URLs", real)
        print("FAIL: zero rows did not refuse")
        ok = False
    except SystemExit:
        print("ok: zero rows refuse")
    require_rows(7, "published URLs", real)
    print("ok: seven rows pass")

    # require_ref: it must DISAGREE about a wrong tree before I believe it
    # agrees about a right one. Falsified against the clone's own history, so
    # the selftest needs no network and no scratch branch of its own.
    if os.path.isdir(os.path.join(real, ".git")):
        try:
            require_ref(real, "origin/main~1", "the selftest")
            print("FAIL: a clone at main did not refuse when asked for main~1")
            ok = False
        except SystemExit:
            print("ok: a tree that is not the named ref refuses")
        try:
            require_ref(real, "refs/heads/definitely-not-a-branch", "the selftest")
            print("FAIL: an unresolvable ref did not refuse")
            ok = False
        except SystemExit:
            print("ok: an unresolvable ref refuses")
        try:
            print("ok: a clone at origin/main passes and names head %s"
                  % require_ref(real, "origin/main", "the selftest"))
        except SystemExit as e:
            print("FAIL (or the clone is not parked at origin/main): %s" % e)
            ok = False

    return 0 if ok else 1


def mask_code(text):
    """Blank the INSIDES of backtick spans, keeping length and line breaks.

    A quotation mark inside backticks is a character being NAMED, not a
    delimiter. Two profiles on main (2026-10-15, `ae1170b`) write exactly
    that -- github line 47 and hubspot line 7 both say ``Every `"`-delimited
    span was checked as a byte string`` -- and each leaves the file with an
    ODD number of quotation marks. Any scanner that pairs quotes then has
    every span after that point INVERTED: it collects the author's prose
    between the real quotations and reports it as quoted text. On hubspot the
    unbalanced mark is on line 7 of a 120-line profile, so almost the whole
    file is affected.

    It is not a defect in those entries. It is a hazard for every instrument
    that reads prose, and the fix belongs in ONE place that all of them
    import rather than in each of them separately -- this toolbox has lost
    the same fix three times to copying (claims.py had the quote-pairing
    repair for weeks while trims.py and quoted.py were written with the bug).

    Lengths are preserved so an offset computed on the masked text still
    points at the right character of the original.
    """
    out = []
    i = 0
    n = len(text)
    while i < n:
        if text[i] == "`":
            j = text.find("`", i + 1)
            if j == -1:  # an unpaired backtick masks nothing
                out.append(text[i:])
                break
            inner = text[i + 1:j]
            out.append("`" + "".join("\n" if c == "\n" else " " for c in inner) + "`")
            i = j + 1
        else:
            out.append(text[i])
            i += 1
    return "".join(out)


def _selftest_mask():
    ok = True

    def check(name, got, want):
        nonlocal ok
        if got != want:
            ok = False
            print(f"FAIL: {name}: got {got!r}, want {want!r}")
        else:
            print(f"ok: {name}")

    # verbatim from github/profile.md line 47 and hubspot/profile.md line 7
    live = 'Every `"`-delimited span was checked, and the page says "a real quotation here".'
    masked = mask_code(live)
    check("a backticked quote mark stops being a delimiter",
          masked.count('"'), 2)
    check("the real quotation still pairs",
          [s for i, s in enumerate(masked.split('"')) if i % 2 == 1],
          ["a real quotation here"])
    check("masking preserves length", len(masked), len(live))
    check("a backticked identifier keeps its delimiters",
          mask_code("call `get_order` now"), "call `         ` now")
    check("an unpaired backtick masks nothing after it",
          mask_code('a `b " c'), 'a `b " c')
    check("line breaks survive masking",
          mask_code("a `x\ny` b").count("\n"), 1)
    return 0 if ok else 1


if __name__ == "__main__":
    if "--selftest" in sys.argv:
        # BOTH, always: `a() or b()` short-circuits, and _selftest() returns 1
        # whenever /tmp/reg is absent, so the masking rules silently never ran
        # the first time I wired them up here.
        sys.exit(max(_selftest(), _selftest_mask()))
    sys.exit("corpus.py is a guard imported by the sweeps; run it with --selftest.")
