#!/usr/bin/env python3
"""verify-source-census-both-ways.py — prove the source census reads both ways.

The wall (verify-source-census.py) must distinguish honest from planted, or
it proves nothing. This harness runs the honest estate (must exit 0) and then
plants one corruption at a time (each must exit 1, named by its generator, or
stay honest where the planted word is words, not a literal):

  1. honest estate                                    -> 0
  2. build-the-promise.js: plant a NEW literal (777)  -> 1 UNDECLARED
  3. build-ember-strip.js:  plant a NEW literal (777) -> 1 UNDECLARED
  4. build-family-line.py:  plant a NEW literal (777) -> 1 UNDECLARED
  5. build-reverse-walk.js: plant a NEW literal (777) -> 1 UNDECLARED
  6. verify-source-census.py: declare a PHANTOM (999) -> 1 UNREAD (declared ⊄ found)
  7. build-the-promise.js: plant a digit in a COMMENT after the shebang
     (the instrument's own ghost: the '!' operator made the shebang's /usr/bin
     parse as a regex and swallow comment text — the census must NOT read
     comment digits as literals)                        -> 0 (still honest)

The READERS' OWN RULE, one surface further in (closed 2026-08-25): the
census's own readers must respect the boundaries the lexer already reads —
a digit inside a string, a comment, a template literal, or a regex literal
is WORDS, not a literal the hand typed into the code. The class's own ghost,
found by building: a regex after a control-flow close paren (`if (x) /re/`)
was read as division, its digits leaked into the census as phantom literals
(77/-9/0 from `/77[0-9]/`), and an honest file failed the wake named by
numbers the hand never typed. The reader now walks back to the matching '('
and reads the control-flow keyword — the same rule the lexer already reads
for the keyword list. Each boundary is planted into every generator and must
stay honest:

  8-10.  each JS generator:  regex after `if (…)`       -> 0 (the heal's proof)
  11-13. each JS generator:  digit in a template literal -> 0
  14-17. each generator:     digit in a double-quoted string -> 0
  18-21. each generator:     digit in a single-quoted string -> 0
  22-25. each generator:     digit in a mid-file comment -> 0
  26.    build-family-line.py: digit in an f-string     -> 0
  27-29. PHANTOM (999) declared in the ember's, the family line's, and the
         reverse walk's own census sets -> 1 UNREAD (the reverse gate holds
         per generator, not only for the promise)

THE CENSUS'S OWN ROOM, one surface further in (closed 2026-08-25): the
wall now reads ITSELF. The census's own body is the eleventh target — its
68 literals (the instrument's machinery AND every number it types to name
the other sources: the declared sets' keys live in the census, so they are
the census's own numbers too) must all be named in its own room. Proven
both ways:

  30. verify-source-census.py: plant a NEW literal (777) into its own body
      -> 1 UNDECLARED (a number the hand typed into the census that the
      census does not name fails the wake, named by its own file)
  31. verify-source-census.py: plant a digit in a COMMENT in its own body
      -> 0 (the census's own comments are words, not literals — the
      instrument strips its own room the way it strips the sources')

The self-room's reverse gate cannot hold by construction — writing a name
writes the number (a declared key IS a literal in the census's body), so a
name for nothing is impossible in the census's own room; the lie it can
tell is the number it does not name.

THE FINISH GATE'S OWN ROOM, one surface further in (closed 2026-08-25):
verify-descs.py — the wall that carries EVERY gate in-process (the
source-census included) — is the twelfth target. The gate reads everything
and is read by nothing; the law now closes the class. Proven both ways:

  32. verify-descs.py: plant a NEW literal (777) into its own body
      -> 1 UNDECLARED (a number the hand typed into the finish gate that
      the census does not name fails the wake, named by its own file)
  33. verify-descs.py: plant a digit in a COMMENT in its own body
      -> 0 (the finish gate's comments are words, not literals — the
      instrument strips the gate's room the way it strips the sources')

THE READING OF THE READING, one surface further in (closed 2026-08-25): the
harness once asserted only the census's VERDICT (the exit code) — never its
READING (the counts it prints). A wake could silently drop a target from
TARGETS and the census would still exit 0 — one file fewer read, all still
honest — and no gate would know. The law now reads the reading:

  34. the reading's width, honest estate — the census must name TWELVE
      files (the four generators + the six proofs + the census itself + the
      finish gate): "12 files read" is asserted, not assumed.
  35. the reading's width, the lie planted — TARGETS[:11] drops the finish
      gate's room; the VERDICT stays 0 (11 files honest) but the READING
      names 11, and the harness catches what the exit code cannot.
  36. the reader's own machinery — a digit-bearing word planted INSIDE the
      reader function (_strip_js's own body) is words, not a literal: the
      law reads the reader's code path with the same instrument it applies
      to the sources. If the strip silently failed, 111 would be found and
      the wake would fail UNDECLARED; it stays honest — the reading of the
      reading: the reader is read, and the naming is still the hand's.

THE READING OF THE READING OF THE READING, one surface further in (closed
2026-08-25): the harness's own expectations were the hand's last place in
the reading's room — "12 files read"/"11 files read" were hand-typed
strings, and a wake could change what the census PRINTS without changing
what it READS (a cosmetic lie — the census exits 0 either way, the strings
still match). The law now derives the expected width from the census's OWN
computed state (check_all loaded in-process) and asserts the printed
reading equals the computed reading, line for line:

  37. the loop narrowed, the summary not — check_all iterates TARGETS[:11]
      but the summary still names len(TARGETS) (12 files read). The VERDICT
      stays 0 (11 files, all honest), the hand-typed string still matches
      the print, the census's own room still passes (11 is a declared
      own-room key) — only a width re-derived from the census's own
      per-file listing sees the lie: 11 listed, 12 named.
  38. the print narrowed, the compute not — main() prints lines[1:], one
      per-file line never printed. The summary string still matches, the
      verdict stays 0; print == compute is asserted line for line, and the
      law sees the missing line.

THE NAMING'S OWN DERIVATION, one surface further in (closed 2026-08-25):
EXPECTED_FILES was the hand's last place — a wake that dropped a file from
BOTH the census's TARGETS and the harness's list narrowed the naming
itself, and no instrument could read it, because the instrument's own
naming was the hand's. The law no longer types the twelve names: the
naming is DERIVED from the estate's own texts — the census's own TARGETS
(loaded in-process, the census names itself), the anchors the estate's own
texts speak (the finish gate's in-process import of the census, the finish
script's `--check` invocations and its verify-* wall), and the proofs' own
glob. A wake that narrows an anchored name from TARGETS leaves the
estate's own text naming it (39: the promise dropped, the finish script
still names it; 40: the census's own seat dropped, the finish gate's
import still names it) — the narrowing of the naming is now SEEN. The
residue — build-family-line.py and build-reverse-walk.py, named by no
estate text (41: dropped, the derivation follows the census, invisible by
construction) — is the voice: the claim's meaning cannot be derived, only
named. The derivation stops where the other instrument begins: the ring
reads itself (SELF_RECORDS), so the source census does not read it.

Each corruption is written to the REAL file, verified, then restored from the
git HEAD copy — the harness never leaves the estate dirty (and restores even
when a case fails: the corruption must not outlive its proof).
"""

import importlib.util
import re
import subprocess
import sys
from pathlib import Path

TOOLS = Path(__file__).resolve().parent
GATE = TOOLS / "verify-source-census.py"
JS_TARGETS = ["build-the-promise.js", "build-ember-strip.js",
              "build-reverse-walk.js"]
PY_TARGETS = ["build-family-line.py"]
TARGETS = JS_TARGETS + PY_TARGETS

# THE NAMING'S OWN DERIVATION (closed 2026-08-25): the twelve names are no
# longer typed by the harness. The law reads the estate's own texts:
#   - the census's own TARGETS names (loaded in-process: the census names
#     itself),
#   - the anchors the estate's own texts speak — the finish gate's
#     in-process import of the census (verify-descs.py's with_name must
#     name the census file) and the finish script's own invocations (the
#     `node … --check` drawing generators it gates, and the `verify-*` wall
#     it runs last) — filtered to the census's own shape (build-*/verify-*),
#     so a wake that drops an anchored name from TARGETS leaves the
#     estate's text naming it,
#   - the proofs' own class: the verify-*-both-ways.py glob (a proof
#     dropped from TARGETS is still named by its pattern).
# The ring's generator is excluded by a NAMED BOUNDARY: it reads itself
# through SELF_RECORDS (a different census), so the source census does not
# read it — the derivation stops where the other instrument begins.
# The residue — build-family-line.py and build-reverse-walk.py, named by
# no estate text — is the voice: the claim's meaning cannot be derived,
# only named.
FINISH_SCRIPT = TOOLS / "../../../../../ops/scripts/finish-vigo-wake.sh"


def _anchored_names() -> set[str]:
    """The names the estate's own texts speak: the finish gate's in-process
    import of the census, the finish script's --check generators, and the
    finish script's verify-* wall — filtered to the census's own shape."""
    names: set[str] = set()
    gate_text = (TOOLS / "verify-descs.py").read_text(encoding="utf-8")
    if re.search(r"with_name\('" + re.escape(GATE.name) + r"'\)", gate_text):
        names.add(GATE.name)
    if FINISH_SCRIPT.exists():
        script_text = FINISH_SCRIPT.read_text(encoding="utf-8")
        for m in re.finditer(r'node "\$SITE/tools/([^"]+)" --check', script_text):
            names.add(m.group(1))
        for m in re.finditer(r'python3 "\$SITE/tools/([^"]+)"', script_text):
            names.add(m.group(1))
    names = {n for n in names if n.startswith(("build-", "verify-"))}
    # the boundary, named: the ring reads itself (SELF_RECORDS derives its
    # own words; no source census) — the derivation stops where the other
    # instrument begins.
    names.discard("build-the-vocabularys-ring.js")
    return names


def derived_naming(mod) -> list[str]:
    """The naming, derived — the census's own TARGETS names (the census
    names itself), plus the anchors the estate's own texts speak, plus the
    proofs' own class (the both-ways glob). A wake that drops an anchored
    name or a proof from TARGETS leaves the estate's own text or the
    proofs' pattern naming it — the narrowing of the naming is seen."""
    names = {name for name, _lang, _declared in mod.TARGETS}
    names |= _anchored_names()
    names |= {p.name for p in TOOLS.glob("verify-*-both-ways.py")}
    return sorted(names)

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: (TOOLS / f).read_text(encoding="utf-8") for f in TARGETS + ["verify-source-census.py", "verify-descs.py"]}


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


def marker_for(f: str) -> str:
    if f.endswith(".py"):
        m = "from pathlib import Path\n"
    else:
        m = 'const path = require("path");\n'
    if m not in (TOOLS / f).read_text(encoding="utf-8"):
        m = "const "
    return m


def plant_after(f: str, line: str) -> None:
    """Insert `line` into the real generator right after its marker line."""
    p = TOOLS / f
    text = p.read_text(encoding="utf-8")
    planted = text.replace(marker_for(f), marker_for(f) + line + "\n", 1)
    p.write_text(planted, encoding="utf-8")


def add_phantom(fragment: str, phantom_line: str) -> None:
    """Insert a phantom declaration into the census after the line holding fragment."""
    gate = TOOLS / "verify-source-census.py"
    lines = gate.read_text(encoding="utf-8").splitlines(keepends=True)
    for i, line in enumerate(lines):
        if fragment in line:
            indent = line[: len(line) - len(line.lstrip())]
            lines.insert(i + 1, indent + phantom_line + "\n")
            gate.write_text("".join(lines), encoding="utf-8")
            return
    raise AssertionError(f"fragment not found in census: {fragment}")


def add_before(f: str, fragment: str, line: str) -> None:
    """Insert `line` into the real file right before the line holding fragment."""
    p = TOOLS / f
    lines = p.read_text(encoding="utf-8").splitlines(keepends=True)
    for i, l in enumerate(lines):
        if fragment in l:
            lines.insert(i, line + "\n")
            p.write_text("".join(lines), encoding="utf-8")
            return
    raise AssertionError(f"fragment not found in {f}: {fragment}")


def replace_in(f: str, old: str, new: str) -> None:
    """Replace the first occurrence of `old` with `new` in the real file."""
    p = TOOLS / f
    text = p.read_text(encoding="utf-8")
    if old not in text:
        raise AssertionError(f"old not found in {f}: {old}")
    p.write_text(text.replace(old, new, 1), encoding="utf-8")


def census_output() -> str:
    """Run the census and return its full output — the READING, not just the verdict."""
    r = subprocess.run([sys.executable, str(GATE)], capture_output=True, text=True)
    return r.stdout + r.stderr


# ------------------------------------------------ the reading of the reading
# of the reading (closed 2026-08-25): the harness's expectations are no
# longer hand-typed strings. The law loads the census IN-PROCESS, reads its
# own computed state (check_all's returned lines), re-derives the width by
# counting the per-file lines the census itself lists, and asserts the
# printed reading equals the computed reading, line for line.

def _load_census():
    """Load the census in-process — its own computed state, not its print."""
    spec = importlib.util.spec_from_file_location("verify_source_census", str(GATE))
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


def derived_width(computed_lines: list[str]) -> int:
    """Re-derive the width from the census's OWN per-file listing."""
    return len([l for l in computed_lines if l.startswith("== ")])


def summary_of(computed_lines: list[str]) -> str:
    """The census's own summary line — the last line check_all computes."""
    return computed_lines[-1] if computed_lines else ""


def reading_consistent(computed_lines: list[str], printed: str, width: int, mod) -> bool:
    """The printed reading equals the computed state: the width the census
    lists is the width it names, every computed line is printed, and every
    name in the DERIVED naming is still read — the naming is no longer the
    hand's list; it is the census's own TARGETS plus the anchors the
    estate's own texts speak plus the proofs' own glob."""
    return (
        f"{width} files read" in summary_of(computed_lines)
        and all(l in printed for l in computed_lines)
        and all(("== " + name) in printed for name in derived_naming(mod))
    )


def main() -> int:
    global passed, failed
    snap = snapshot()
    try:
        # 1. honest estate
        run_gate("honest estate", 0)

        # 2-5. plant a NEW literal in each generator's code: a number the hand
        #      typed and the census does not know must fail, named by its file.
        for f in TARGETS:
            p = TOOLS / f
            text = p.read_text(encoding="utf-8")
            if f.endswith(".py"):
                marker = "from pathlib import Path\n"
            else:
                marker = 'const path = require("path");\n'
            if marker not in text:
                marker = "const "
            planted = text.replace(marker, marker + f"GHOST_LITERAL_777 = 777;\n", 1)
            p.write_text(planted, encoding="utf-8")
            run_gate(f"{f}: planted literal 777 -> UNDECLARED", 1)
            restore(snap)

        # 6. declare a PHANTOM in the census itself: a name for nothing fails.
        gate = TOOLS / "verify-source-census.py"
        text = gate.read_text(encoding="utf-8")
        planted = text.replace(
            '0: "the success exit — process.exit(0) (the check\'s own pass)",',
            '0: "the success exit — process.exit(0) (the check\'s own pass)",\n'
            '            999: "GHOST — a name for nothing",',
            1,
        )
        gate.write_text(planted, encoding="utf-8")
        run_gate("verify-source-census.py: phantom declared 999 -> UNREAD", 1)
        restore(snap)

        # 7. comment digits after the shebang must NOT be read (the instrument's
        #    own ghost — the shebang's '!' made /usr/bin parse as a regex and
        #    swallowed comment text; 19/19 and Q17/Q20 once walked in as phantoms).
        p = TOOLS / "build-the-promise.js"
        text = p.read_text(encoding="utf-8")
        planted = text.replace(
            "#!/usr/bin/env node\n",
            "#!/usr/bin/env node\n// comment ghost digits: 19/19, Q17/Q20 — not code\n",
            1,
        )
        p.write_text(planted, encoding="utf-8")
        run_gate("build-the-promise.js: comment digits after shebang -> still honest", 0)
        restore(snap)

        # ---------------------------------------------------------------- the
        # readers' own rule — the boundaries must hold in EVERY generator: a
        # digit inside a string, comment, template, or regex is words, not a
        # literal the hand typed into the code.

        # 8-10. regex after a control-flow close paren — the reader's own blind
        #       spot, closed by building: `if (…) /77[0-9]/…` once leaked
        #       77/-9/0 into the census as phantoms (an honest file failed,
        #       named by numbers the hand never typed). The reader now walks
        #       back to the matching '(' and reads the control-flow keyword.
        for f in JS_TARGETS:
            plant_after(f, "if (EDGE) /77[0-9]/.test(String(EDGE));")
            run_gate(f"{f}: regex after control-flow ) -> still honest", 0)
            restore(snap)

        # 11-13. digits inside template literals are words, not literals.
        for f in JS_TARGETS:
            plant_after(f, "const NOTE = `the 111th line`;")
            run_gate(f"{f}: digit in template literal -> still honest", 0)
            restore(snap)

        # 14-17. digits inside double-quoted strings are words, not literals.
        for f in TARGETS:
            q = '"'
            plant_after(f, f"const NOTE = {q}the 777th line{q};" if not f.endswith(".py")
                        else f"NOTE = {q}the 777th line{q}")
            run_gate(f"{f}: digit in double-quoted string -> still honest", 0)
            restore(snap)

        # 18-21. digits inside single-quoted strings are words, not literals.
        for f in TARGETS:
            q = "'"
            plant_after(f, f"const NOTE = {q}the 888th line{q};" if not f.endswith(".py")
                        else f"NOTE = {q}the 888th line{q}")
            run_gate(f"{f}: digit in single-quoted string -> still honest", 0)
            restore(snap)

        # 22-25. digits inside mid-file comments are words, not literals.
        for f in TARGETS:
            plant_after(f, "// the 999th line, a comment ghost" if not f.endswith(".py")
                        else "# the 999th line, a comment ghost")
            run_gate(f"{f}: digit in mid-file comment -> still honest", 0)
            restore(snap)

        # 26. digits inside an f-string's braces are words, not literals.
        plant_after("build-family-line.py", 'NOTE = f"the {777}th line"')
        run_gate("build-family-line.py: digit in f-string -> still honest", 0)
        restore(snap)

        # 27-29. the reverse gate holds PER GENERATOR: a name for nothing in the
        #        ember's, the family line's, or the reverse walk's own census
        #        fails the wake — not only the promise's.
        for fragment, label in [
            ("the three-decimal face", "build-ember-strip.js"),
            ("the wrap search's step", "build-family-line.py"),
            ("the failure exit", "build-reverse-walk.js"),
        ]:
            add_phantom(fragment, '999: "GHOST — a name for nothing",')
            run_gate(f"verify-source-census.py: phantom 999 in {label}'s set -> UNREAD", 1)
            restore(snap)

        # ------------------------------------------------- the census's own
        # room (closed 2026-08-25): the wall reads ITSELF. The census's own
        # body is the eleventh target; its own 68 literals are declared in its
        # own room. A number the hand types into the census without naming it
        # must fail the wake, named by the census's own file.

        # 30. a NEW literal planted into the census's own body is UNDECLARED
        #     in its own room — the forward gate holds on the census itself.
        plant_after("verify-source-census.py", "GHOST_LITERAL_777 = 777")
        run_gate("verify-source-census.py: planted literal 777 in its own body -> UNDECLARED", 1)
        restore(snap)

        # 31. a digit inside a comment in the census's own body is words, not
        #     a literal — the instrument strips its own room the way it strips
        #     the sources'.
        plant_after("verify-source-census.py", "# the 111th line, a comment ghost in the census's own room")
        run_gate("verify-source-census.py: comment digit in its own body -> still honest", 0)
        restore(snap)

        # ----------------------------------------------- the finish gate's
        # own room (closed 2026-08-25): verify-descs.py — the wall that
        # carries EVERY gate in-process — is the twelfth target. The gate
        # reads everything and is read by nothing; the law now closes the
        # class. A number typed into the finish gate without naming it must
        # fail the wake, named by the finish gate's own file.

        # 32. a NEW literal planted into the finish gate's own body is
        #     UNDECLARED in its own room — the forward gate holds on the
        #     wall that carries every gate.
        plant_after("verify-descs.py", "GHOST_LITERAL_777 = 777")
        run_gate("verify-descs.py: planted literal 777 in its own body -> UNDECLARED", 1)
        restore(snap)

        # 33. a digit inside a comment in the finish gate's own body is words,
        #     not a literal — the instrument strips the gate's room the way
        #     it strips the sources'.
        plant_after("verify-descs.py", "# the 111th line, a comment ghost in the finish gate's own room")
        run_gate("verify-descs.py: comment digit in its own body -> still honest", 0)
        restore(snap)

        # --------------------------------------------- the reading of the
        # reading (closed 2026-08-25): the harness once asserted only the
        # census's VERDICT (the exit code) — never its READING (the counts it
        # prints). A wake could silently drop a target from TARGETS and the
        # census would still exit 0 — one file fewer read, all still honest —
        # and no gate would know. The law now reads the reading.

        # 34. the reading's width, honest estate — the census must name the
        #     TWELVE files: the four generators + the six proofs + the census
        #     itself + the finish gate. The verdict alone cannot say how many
        #     files were read; the reading can. The expected width is no
        #     longer hand-typed — the law loads the census IN-PROCESS,
        #     re-derives the width from its own per-file listing, and asserts
        #     the printed reading equals the computed reading, line for line.
        mod = _load_census()
        honest_failed, honest_lines = mod.check_all()
        honest_width = derived_width(honest_lines)
        out_wide = census_output()
        ok_wide = honest_failed == 0 and reading_consistent(honest_lines, out_wide, honest_width, mod)
        mark = "PASS" if ok_wide else "FAIL"
        if ok_wide:
            passed += 1
        else:
            failed += 1
        print(f"[{mark}] census reading: the honest estate names {honest_width} files read — the width re-derived from the census's own listing")
        if not ok_wide:
            print("   ", (out_wide.strip().splitlines() or [""])[-1])

        # 35. the reading's width, the lie planted — a wake that drops one
        #     target (TARGETS[:11], the finish gate's room gone) leaves the
        #     VERDICT unchanged (11 files all honest -> exit 0) but narrows
        #     the READING (11 files read). The exit-code-only harness would
        #     pass this lie; the reading catches it — and the reading's
        #     expectation now comes from the census's own computed state,
        #     not a hand-typed string.
        add_before("verify-source-census.py", "def check_all()",
                   "TARGETS = TARGETS[:11]  # the lie: one room gone")
        mod = _load_census()
        lie_failed, lie_lines = mod.check_all()
        lie_width = derived_width(lie_lines)
        out_lie = census_output()
        ok_lie = (
            lie_failed == 0                            # the VERDICT is blind — 11 files, all honest
            and f"{lie_width} files read" in summary_of(lie_lines)   # the lie names its own width
            and "== verify-descs.py" not in out_lie    # the dropped room is gone from the reading
            and all(l in out_lie for l in lie_lines)   # print == compute
        )
        mark = "PASS" if ok_lie else "FAIL"
        if ok_lie:
            passed += 1
        else:
            failed += 1
        print(f"[{mark}] census reading: a dropped target narrows the reading ({lie_width} files) — the law sees the lie")
        if not ok_lie:
            print("   ", (out_lie.strip().splitlines() or [""])[-1])
        restore(snap)

        # 36. the reader's own machinery — a digit-bearing word planted INSIDE
        #     the reader function (_strip_js's own body) is words, not a
        #     literal: the law reads the reader's code path with the same
        #     instrument it applies to the sources. If the strip silently
        #     failed, 111 would be found and the wake would fail UNDECLARED —
        #     the reading of the reading: the reader is read.
        add_before("verify-source-census.py", "def _strip_js(src: str) -> str:",
                   'READER_GUARD = "the 111th word, inside the reader\'s own machinery"')
        run_gate("verify-source-census.py: digit-bearing word INSIDE the reader's own body -> still honest", 0)
        restore(snap)

        # ------------------------------- the reading of the reading of the
        # reading (closed 2026-08-25): the harness's expectations are no
        # longer hand-typed strings ("12 files read"/"11 files read" were the
        # hand's last place in the reading's own room). The law now derives
        # the expected width from the census's OWN computed state (check_all
        # loaded in-process) and asserts the printed reading equals the
        # computed reading, line for line — a cosmetic lie the exit code
        # cannot see and the old strings still matched is now seen.

        # 37. the loop narrowed, the summary not — check_all iterates
        #     TARGETS[:11] but the summary still names len(TARGETS) (12
        #     files read). The VERDICT stays 0 (11 files, all honest); the
        #     hand-typed string "12 files read" still matches the print; the
        #     census's own room still passes (11 is a declared own-room
        #     key). Only a width re-derived from the census's own per-file
        #     listing sees the lie: 11 listed, 12 named.
        replace_in("verify-source-census.py",
                   "    for name, lang, declared in TARGETS:",
                   "    for name, lang, declared in TARGETS[:11]:  # the lie: the loop narrowed, the summary not")
        mod = _load_census()
        cos_failed, cos_lines = mod.check_all()
        cos_width = derived_width(cos_lines)
        out_cos = census_output()
        ok_cos = (
            cos_failed == 0                                  # the VERDICT is blind — 11 files, all honest
            and f"{cos_width} files read" not in summary_of(cos_lines)  # the listing's 11 is NOT the summary's 12
            and "== verify-descs.py" not in out_cos          # the dropped room is gone from the listing
            and all(l in out_cos for l in cos_lines)         # print == compute — the lie is in the census's own state
        )
        mark = "PASS" if ok_cos else "FAIL"
        if ok_cos:
            passed += 1
        else:
            failed += 1
        print(f"[{mark}] census reading: the loop narrowed, the summary not — the re-derived width sees the lie (listed {cos_width}, named 12)")
        if not ok_cos:
            print("   ", (out_cos.strip().splitlines() or [""])[-1])
        restore(snap)

        # 38. the print narrowed, the compute not — main() prints lines[1:],
        #     one per-file line never printed. The summary string still
        #     matches ("12 files read" is printed), the VERDICT stays 0;
        #     the reading-of-the-reading asserts print == compute, line for
        #     line, and sees the missing line — a drop the hand-typed-string
        #     harness passed by construction.
        replace_in("verify-source-census.py",
                   "    for l in lines:",
                   "    for l in lines[1:]:  # the lie: one per-file line never printed")
        mod = _load_census()
        pp_failed, pp_lines = mod.check_all()
        pp_width = derived_width(pp_lines)
        out_pp = census_output()
        ok_pp = (
            pp_failed == 0                                  # the VERDICT is blind
            and f"{pp_width} files read" in summary_of(pp_lines)   # the summary IS printed — the strings still match
            and "== build-the-promise.js" not in out_pp     # the dropped per-file line is gone from the print
            and not all(l in out_pp for l in pp_lines)      # print != compute — the law sees the missing line
        )
        mark = "PASS" if ok_pp else "FAIL"
        if ok_pp:
            passed += 1
        else:
            failed += 1
        print(f"[{mark}] census reading: one per-file line never printed — print != compute, the law sees the missing line")
        if not ok_pp:
            print("   ", (out_pp.strip().splitlines() or [""])[-1])
        restore(snap)

        # ------------------------------------------- the naming's own
        # derivation (closed 2026-08-25): EXPECTED_FILES was the hand's last
        # place — a wake that dropped a file from BOTH the census's TARGETS
        # and the harness's list narrowed the naming itself, and no
        # instrument could read it, because the instrument's own naming was
        # the hand's. The law no longer types the twelve names: it derives
        # them from the census's own TARGETS (in-process), the anchors the
        # estate's own texts speak (the finish gate's in-process import of
        # the census, the finish script's --check invocations and its
        # verify-* wall), and the proofs' own glob. A wake that narrows an
        # anchored name from TARGETS leaves the estate's own text naming
        # it — the narrowing of the naming is now SEEN.

        # 39. the anchored generator narrowed from both — the finish script
        #     still names it. A wake drops build-the-promise.js from TARGETS
        #     (and from the hand's list, which no longer exists); the census
        #     exits 0 (11 files, all honest), its own state is consistent —
        #     but the finish script still runs `node … build-the-promise.js
        #     --check`, so the DERIVED naming still names it, and the
        #     reading lacks it: the law sees the narrowing of the naming
        #     itself.
        add_before("verify-source-census.py", "def check_all()",
                   'TARGETS = [t for t in TARGETS if t[0] != "build-the-promise.js"]  # the lie: the anchored generator narrowed from both')
        mod = _load_census()
        an_failed, an_lines = mod.check_all()
        an_width = derived_width(an_lines)
        out_an = census_output()
        ok_an = (
            an_failed == 0                                  # the VERDICT is blind — 11 files, all honest
            and f"{an_width} files read" in summary_of(an_lines)  # the census's own state is consistent
            and all(l in out_an for l in an_lines)          # print == compute
            and not reading_consistent(an_lines, out_an, an_width, mod)  # but the DERIVED naming still names it — seen
        )
        mark = "PASS" if ok_an else "FAIL"
        if ok_an:
            passed += 1
        else:
            failed += 1
        print(f"[{mark}] naming: build-the-promise.js dropped from TARGETS — the finish script still names it, the law sees the narrowing of the naming itself")
        if not ok_an:
            print("   derived:", derived_naming(mod))
        restore(snap)

        # 40. the census's own seat narrowed from both — the finish gate
        #     still names it. A wake drops verify-source-census.py from
        #     TARGETS (its own room gone); the census exits 0 (11 files,
        #     all honest) — but the finish gate's own text still carries
        #     the in-process import (with_name('verify-source-census.py')),
        #     so the anchor still names the census's file, and the law sees
        #     the narrowing of the naming itself.
        add_before("verify-source-census.py", "def check_all()",
                   'TARGETS = [t for t in TARGETS if t[0] != "verify-source-census.py"]  # the lie: the census seat narrowed from both')
        mod = _load_census()
        cs_failed, cs_lines = mod.check_all()
        cs_width = derived_width(cs_lines)
        out_cs = census_output()
        ok_cs = (
            cs_failed == 0
            and f"{cs_width} files read" in summary_of(cs_lines)
            and all(l in out_cs for l in cs_lines)
            and not reading_consistent(cs_lines, out_cs, cs_width, mod)
        )
        mark = "PASS" if ok_cs else "FAIL"
        if ok_cs:
            passed += 1
        else:
            failed += 1
        print(f"[{mark}] naming: the census's own seat dropped from TARGETS — the finish gate's import still names it, the law sees it")
        if not ok_cs:
            print("   derived:", derived_naming(mod))
        restore(snap)

        # 41. the voice's residue — build-family-line.py narrowed from both,
        #     and NO estate text names it: the finish script does not run
        #     it, the finish gate does not import it, no glob reaches it.
        #     The derived naming follows the census; the narrowing is
        #     invisible BY CONSTRUCTION. This is the boundary argued, not
        #     derived: the claim's meaning (which files belong to the
        #     census) cannot be derived, only named — the naming of the
        #     family line is the voice.
        add_before("verify-source-census.py", "def check_all()",
                   'TARGETS = [t for t in TARGETS if t[0] != "build-family-line.py"]  # the voice: a file no estate text names')
        mod = _load_census()
        vc_failed, vc_lines = mod.check_all()
        vc_width = derived_width(vc_lines)
        out_vc = census_output()
        ok_vc = (
            vc_failed == 0
            and f"{vc_width} files read" in summary_of(vc_lines)
            and all(l in out_vc for l in vc_lines)
            and reading_consistent(vc_lines, out_vc, vc_width, mod)  # the law CANNOT see it — the voice
        )
        mark = "PASS" if ok_vc else "FAIL"
        if ok_vc:
            passed += 1
        else:
            failed += 1
        print(f"[{mark}] naming: build-family-line.py dropped from TARGETS — no estate text names it, the narrowing is invisible by construction (the voice, argued)")
        if not ok_vc:
            print("   derived:", derived_naming(mod))
        restore(snap)
    finally:
        restore(snap)

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


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