#!/usr/bin/env python3
"""verify-claim-patterns-both-ways.py — prove the claim patterns' own
boundaries read both ways (the seed the 04:45 wake left open, closed by
building 2026-08-17).

The READERS healed one surface in (iter_desc_tags / iter_text_tags read
comment-aware and quote-aware), but the CLAIM PATTERNS that count the
drawings' elements still read with the hand's whole-file regexes:

  - comment-blindness: a '<circle cx="999" cy="300" r="6">' the hand wrote
    ABOUT the drawing inside a comment counted as a real fix point — the
    fix-point claim ("three fix points") would FAIL on an honest drawing
    whose comments merely mention circles.
  - the [^>]* under-read: the thin-lines claim's
    '<line[^>]*stroke-width="0\.8"' stopped at the FIRST '>' it saw, even
    when that '>' sat inside a quoted attribute value (data-x="a > b") —
    a perfectly legal line the wall could not read, the same ghost the
    image wall closed for <img> one surface over, still the hand's in the
    claim patterns' own room.
  - the [\s\S]*? over-read: the liturgy-fade claim's
    '<path d="M 0 -([0-9.]+) A [^"]*"[\s\S]*?opacity="([0-9.]+)"' reached
    ACROSS element boundaries — a path whose own opacity was missing could
    be paired with a DIFFERENT element's opacity, and the fade claim would
    pass on a value that was never the arc's.

The claim patterns now read through the same instrument the readers
healed (iter_open_tags + element_attrs): a '<name' inside a comment is
words, not an element; a '>' inside a quoted attribute value is part of
the value; and a radius's opacity must live in the SAME element's
attributes. This harness runs the honest estate (must exit 0) and then
plants one corruption at a time (each must exit as expected, named by its
file):

  1. honest estate                                   -> 0
  2. dead-reckoning.svg: a fix-point-shaped circle   -> 0 (comment words are
     inside a comment (the hand's words about          not drawing elements —
     the drawing)                                       the count stays 3)
  3. dead-reckoning.svg: a real fix dot REMOVED     -> 1 (the reverse gate
     (its circle deleted, a comment says so)            holds: 2 real fixes,
                                                       the claim says three)
  4. the-checkbox-trap.svg: every thin connector     -> 0 (the reading goes
     line gains data-x="a > b" before its               through the boundary —
     stroke-width (the [^>]* ghost)                     the thin lines still count)
  5. liturgy-not-config.svg: arc 7 loses its own     -> 1 (the over-read
     opacity and the r=7 circle carries 0.22            cannot steal a foreign
     (the [\s\S]*? ghost)                               opacity — a claim about
                                                       the arc must be proven
                                                       by the arc's own attrs)
  6. honest estate again (all restorations)          -> 0

Each corruption is written to the REAL file, verified, then restored from
the in-memory snapshot — the harness never leaves the estate dirty.

Deterministic: same files, same bytes, same verdict.
"""

import subprocess
import sys
from pathlib import Path

TOOLS = Path(__file__).resolve().parent
ASSETS = TOOLS.parent / "assets"
GATE = TOOLS / "verify-desc-claims.py"
FILES = [
    "dead-reckoning.svg",
    "liturgy-not-config.svg",
    "the-checkbox-trap.svg",
    "the-faithful-sentinel.svg",
    "two-fixes.svg",
]

passed = 0
failed = 0


def run_gate(label: str, expect: int) -> None:
    global passed, failed
    r = subprocess.run([sys.executable, str(GATE)], capture_output=True, text=True)
    ok = r.returncode == expect
    mark = "PASS" if ok else "FAIL"
    if ok:
        passed += 1
    else:
        failed += 1
    print(f"[{mark}] {label} (exit {r.returncode}, expected {expect})")
    if not ok:
        tail = (r.stdout.strip().splitlines() or r.stderr.strip().splitlines() or [""])[-1]
        print("   ", tail)


def snapshot() -> dict[str, str]:
    return {f: (ASSETS / f).read_text(encoding="utf-8") for f in FILES}


def restore(snap: dict[str, str]) -> None:
    for f, content in snap.items():
        (ASSETS / f).write_text(content, encoding="utf-8")


def main() -> int:
    snap = snapshot()
    try:
        # 1. honest estate
        run_gate("honest estate (5/5 pieces read through the instrument)", 0)

        # 2. a fix-point-shaped circle inside a comment — words, not a fix
        dr = (ASSETS / "dead-reckoning.svg").read_text(encoding="utf-8")
        (ASSETS / "dead-reckoning.svg").write_text(
            dr.replace("</svg>",
                       "<!-- the hand's words about the drawing: "
                       '<circle cx="999" cy="300" r="6"/> -->\n</svg>'),
            encoding="utf-8")
        run_gate("fix-point-shaped circle inside a comment — words are not fixes (count stays 3)", 0)
        restore(snap)

        # 3. a real fix dot REMOVED — the reverse gate must hold
        dr = (ASSETS / "dead-reckoning.svg").read_text(encoding="utf-8")
        real_fix = '<circle cx="220" cy="300" r="6" fill="#7c7cf0"/>'
        assert real_fix in dr, "fix dot not found — the harness's plant is stale"
        (ASSETS / "dead-reckoning.svg").write_text(
            dr.replace(real_fix,
                       "<!-- fix 1 was here: <circle cx=\"220\" cy=\"300\" r=\"6\"/> -->"),
            encoding="utf-8")
        run_gate("real fix dot removed (only a comment remains) — 2 real fixes, claim says three", 1)
        restore(snap)

        # 4. every thin connector line gains data-x="a > b" BEFORE its
        #    stroke-width — the [^>]* ghost: the tag is read whole
        cb = (ASSETS / "the-checkbox-trap.svg").read_text(encoding="utf-8")
        thin = ['<line x1="-45" y1="-35" x2="5" y2="-10" stroke="#3a3a50" stroke-width="0.8" opacity="0.3"/>',
                '<line x1="5" y1="-10" x2="55" y2="8" stroke="#3a3a50" stroke-width="0.8" opacity="0.3"/>',
                '<line x1="5" y1="-10" x2="20" y2="22" stroke="#3a3a50" stroke-width="0.8" opacity="0.2"/>',
                '<line x1="-25" y1="18" x2="-60" y2="5" stroke="#3a3a50" stroke-width="0.8" opacity="0.2"/>']
        planted = 0
        for t in thin:
            if t in cb:
                cb = cb.replace(t, t.replace(' stroke="#3a3a50"',
                                              ' data-x="a > b" stroke="#3a3a50"'))
                planted += 1
        assert planted == 4, f"thin lines not found — planted {planted}/4, harness plant stale"
        (ASSETS / "the-checkbox-trap.svg").write_text(cb, encoding="utf-8")
        run_gate("thin connector lines carry '>' inside a quoted value — the tag is read whole (thin lines still count)", 0)
        restore(snap)

        # 5. arc 7 loses its own opacity; the r=7 circle carries 0.22 —
        #    the [\s\S]*? ghost: the over-read must NOT steal a foreign
        #    opacity, so the fade claim fails honestly (the arc itself is
        #    no longer provably fading)
        lt = (ASSETS / "liturgy-not-config.svg").read_text(encoding="utf-8")
        arc7 = '          fill="none" stroke="#7c7cf0" stroke-width="2.5" stroke-linecap="round" opacity="0.22"/>'
        assert arc7 in lt, "arc 7 not found — the harness's plant is stale"
        lt = lt.replace(arc7, '          fill="none" stroke="#7c7cf0" stroke-width="2.5" stroke-linecap="round"/>')
        ring = '<circle cx="0" cy="0" r="7" fill="none" stroke="#7c7cf0" stroke-width="1" opacity="0.5"/>'
        assert ring in lt, "r=7 ring circle not found — the harness's plant is stale"
        lt = lt.replace(ring, '<circle cx="0" cy="0" r="7" fill="none" stroke="#7c7cf0" stroke-width="1" opacity="0.22"/>')
        (ASSETS / "liturgy-not-config.svg").write_text(lt, encoding="utf-8")
        run_gate("arc 7 loses its own opacity — a foreign 0.22 cannot prove the arc's fade", 1)
        restore(snap)

        # 6. honest estate again — the harness never leaves the estate dirty
        run_gate("honest estate after all restorations", 0)
    finally:
        restore(snap)

    print(f"\nclaim-patterns both-ways: {passed} passed, {failed} failed")
    return 1 if failed else 0


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