"""Every figure and every quotation in "Zero Agents in 144 Pages", recomputed from the saved primaries and asserted.

Inputs (beside this script): sources/ (every primary as bravo fetched it on 2026-09-21), fr_ai_agents_c6281.json and
fr_ai_agents_docs_c6281.out.txt (the Federal Register API pull, 2026-09-21T14:09:01Z), agent_word_census_c6281.json
(bravo's census; the counts below are RECOMPUTED from the PDFs and texts with the census's own extraction and regexes,
and the JSON is only checked for agreement).

The script re-extracts the texts with PyMuPDF exactly as the census did (page text, pages joined by a space, whitespace
collapsed), recounts the words with the same regexes, asserts every count, every verbatim quotation and every locator the
essay carries, and runs the two-sided control (the regex family that returns 0 for "agent" on the EU text must return
non-zero for words that ARE there). A figure that drifts from its source fails the run.

Run: python figures_c6281.py                    (no network)
     python figures_c6281.py --sources <dir>    (where the primaries are)

The captures live in the RUN DIRECTORY, not beside the published copy of this script: a code folder on the website
carries scripts, receipts and a SOURCES.md manifest, never raw copies of other people's pages (ruling
website-code-folder-no-raw-captures, 2026-09-21). With no --sources the script reads $C6281_SOURCES, then a sources/
directory beside this file (that is how it ran before the ruling), then the run directory's sources/, found by walking
up. SOURCES.md beside this script lists every primary with its URL, fetch time, size and sha256, so a reader can fetch
the same documents and check they have the same bytes.
"""
import html as html_mod
import json
import os
import re
import sys
from pathlib import Path

import fitz  # PyMuPDF

HERE = Path(__file__).resolve().parent
RUN_ID = "content-c6281-000"


def _sources_dir(argv):
    """--sources <dir>, else $C6281_SOURCES, else a sources/ beside this file, else the run dir's sources/.

    No absolute path is written down here. The run directory is found by walking up and looking for a folder named
    after the run, so this works from the run dir and from a checkout and fails loudly anywhere else.
    """
    if "--sources" in argv:
        return Path(argv[argv.index("--sources") + 1]).expanduser().resolve()
    env = os.environ.get("C6281_SOURCES")
    if env:
        return Path(env).expanduser().resolve()
    beside = HERE / "sources"
    if beside.is_dir():
        return beside
    for parent in [HERE, *HERE.parents]:
        for cand in (parent / RUN_ID / "sources", parent / "sources"):
            if cand.is_dir():
                return cand
    return beside


SRC = _sources_dir(sys.argv[1:])
if not SRC.is_dir():
    print("no sources directory: %s" % SRC)
    print("pass --sources <dir> — the primaries live in the run dir; SOURCES.md lists what belongs there")
    sys.exit(2)
print("sources: %s" % SRC)
FAILS = []


def check(cond, label):
    print(("  ok   " if cond else "  FAIL ") + label)
    if not cond:
        FAILS.append(label)


def norm(s):
    return re.sub(r"\s+", " ", s)


def pdf_pages(name):
    doc = fitz.open(SRC / name)
    return [p.get_text("text") for p in doc]


def full_of(pages):
    return norm(" ".join(pages))


def txt(name):
    raw = (SRC / name).read_text(encoding="utf-8", errors="replace")
    if name.endswith((".htm", ".html")):
        raw = re.sub(r"(?is)<(script|style)[^>]*>.*?</\1>", " ", raw)
        raw = html_mod.unescape(re.sub(r"<[^>]+>", " ", raw))
    return norm(raw)


AGENT = re.compile(r"\bagents?\b", re.I)
AGENTIC = re.compile(r"\bagentic\b", re.I)
AUTONOM = re.compile(r"\bautonom\w*", re.I)
AISYS = re.compile(r"\bAI systems?\b|\bartificial intelligence systems?\b", re.I)


def q(s):
    """quotation normaliser: curly quotes to straight, whitespace collapsed"""
    return norm(s.replace("’", "'").replace("‘", "'").replace("“", '"').replace("”", '"'))


def has(text, phrase):
    return q(phrase) in q(text)


print("== 1. The census, recomputed from the PDFs with the census's own extraction")
eu_pages = pdf_pages("eu_ai_act_2024_1689.pdf")
eu = full_of(eu_pages)
check(len(eu_pages) == 144, "EU AI Act PDF has 144 pages (OJ page = PDF page)")
check(len(eu) == 595707, "EU AI Act normalised text is 595,707 characters (got %d)" % len(eu))
check(len(AGENT.findall(eu)) == 0, "EU AI Act: 0 occurrences of agent/agents")
check(len(AGENTIC.findall(eu)) == 0, "EU AI Act: 0 occurrences of agentic")
check(len(AISYS.findall(eu)) == 1122, "EU AI Act: 1,122 occurrences of 'AI system(s)' (got %d)" % len(AISYS.findall(eu)))
check(len(AUTONOM.findall(eu)) == 11, "EU AI Act: 11 occurrences of autonom* (got %d)" % len(AUTONOM.findall(eu)))
check("OJ L, 12.7.2024" in eu_pages[45] and "46/144" in eu_pages[45], "EU PDF page 46 carries the OJ footer 46/144")
# the two-sided control: the same tokenizer finds the words that are there
n_rep = len(re.findall(r"\bauthorised representatives?\b", eu, re.I))
n_op = len(re.findall(r"\boperators?\b", eu, re.I))
n_agenc = len(re.findall(r"agenc", eu, re.I))
print("     control counts on the EU text: 'authorised representative(s)' %d, 'operator(s)' %d, 'agenc' %d" % (n_rep, n_op, n_agenc))
CONTROL = {"authorised representative": n_rep, "operator": n_op, "agenc": n_agenc}
check(n_rep > 0 and n_op > 0 and n_agenc > 0, "EU control: every control word returns a non-zero count under the same tokenizer")
check(n_rep == 39, "EU control: 'authorised representative(s)' 39 (got %d) — the number the essay quotes" % n_rep)
check(n_op == 58, "EU control: 'operator(s)' 58 (got %d) — the number the essay quotes" % n_op)
check(n_agenc == 50, "EU control: 'agenc' 50 (got %d) — the number the essay quotes" % n_agenc)
# (bravo's research.md quoted 37 / 58 / 45 for the same three controls from an ad-hoc count with patterns it did not
#  record; the essay quotes THIS script's counts, whose patterns are the three regexes above)

nist_pages = pdf_pages("nist_ai_100_1.pdf"); nist = full_of(nist_pages)
check(len(nist_pages) == 48 and len(nist) == 106475, "NIST AI 100-1: 48 pages, 106,475 characters (got %d, %d)" % (len(nist_pages), len(nist)))
check(len(AGENT.findall(nist)) == 0 and len(AGENTIC.findall(nist)) == 0, "NIST AI RMF 1.0: 0 agent, 0 agentic")
check(len(AISYS.findall(nist)) == 238, "NIST AI RMF 1.0: 238 'AI system(s)' (got %d)" % len(AISYS.findall(nist)))

co = full_of(pdf_pages("colorado_sb24_205.pdf"))
check(len(AGENT.findall(co)) == 0 and len(AGENTIC.findall(co)) == 0 and len(AUTONOM.findall(co)) == 0, "Colorado SB 24-205: 0 agent, 0 agentic, 0 autonom*")
check(len(AISYS.findall(co)) == 124, "Colorado SB 24-205: 124 'AI system(s)' (got %d)" % len(AISYS.findall(co)))
tx = full_of(pdf_pages("texas_hb149.pdf"))
check(len(AGENT.findall(tx)) == 0 and len(AGENTIC.findall(tx)) == 0 and len(AUTONOM.findall(tx)) == 0, "Texas HB 149: 0 agent, 0 agentic, 0 autonom*")
check(len(AISYS.findall(tx)) == 72, "Texas HB 149: 72 'AI system(s)' (got %d)" % len(AISYS.findall(tx)))
ut = full_of(pdf_pages("utah_sb149_2024_enrolled.pdf"))
check(len(AGENT.findall(ut)) == 0 and len(AGENTIC.findall(ut)) == 0 and len(AISYS.findall(ut)) == 0, "Utah SB 149 (2024): 0 agent, 0 agentic, 0 'AI system'")
omb = full_of(pdf_pages("omb_m_25_21.pdf"))
check(len(AGENT.findall(omb)) == 1 and has(omb, "biological agents"), "OMB M-25-21: 1 'agent' hit, 'biological agents'")
check(len(AISYS.findall(omb)) == 10, "OMB M-25-21: 10 'AI system(s)' (got %d)" % len(AISYS.findall(omb)))
ca = txt("california_sb53.html")
check(len(AGENT.findall(ca)) == 1 and has(ca, "employers and their agents"), "California SB 53: 1 'agents' hit, 'employers and their agents' (the digest)")
check(len(AISYS.findall(ca)) == 5, "California SB 53: 5 'AI system(s)' (got %d)" % len(AISYS.findall(ca)))
ec = full_of(pdf_pages("ec_guidelines_ai_system_definition_c2025_924.pdf"))
check(len(AGENT.findall(ec)) == 1 and len(AUTONOM.findall(ec)) == 16, "Commission Guidelines: 1 'agent' hit, 16 autonom* (got %d, %d)" % (len(AGENT.findall(ec)), len(AUTONOM.findall(ec))))
check(len(AISYS.findall(ec)) == 110, "Commission Guidelines: 110 'AI system(s)' (got %d)" % len(AISYS.findall(ec)))
iso = full_of(pdf_pages("iso_iec_22989_2022_iteh_sample.pdf"))
check(len(AGENT.findall(iso)) == 2 and len(AISYS.findall(iso)) == 27, "ISO/IEC 22989 preview: 2 'agent', 27 'AI system(s)' (got %d, %d)" % (len(AGENT.findall(iso)), len(AISYS.findall(iso))))
census = json.load(open(HERE / "agent_word_census_c6281.json", encoding="utf-8"))
check(census["eu_ai_act"]["counts"]["agent"] == 0 and census["eu_ai_act"]["counts"]["AI system"] == 1122 and census["eu_ai_act"]["chars"] == 595707, "bravo's census JSON agrees on the EU counts")
zero_docs = [k for k in ("eu_ai_act", "nist_rmf", "colorado", "texas_hb149", "utah_sb149") if census[k]["counts"]["agent"] == 0 and census[k]["counts"]["agentic"] == 0]
check(len(zero_docs) == 5, "five binding texts with zero 'agent' and zero 'agentic': EU Act, NIST RMF, Colorado, Texas, Utah")

print("== 2. The definitions, verbatim, with locators")
check(has(eu_pages[45], "'AI system' means a machine-based system that is designed to operate with varying levels of autonomy and that may exhibit adaptiveness after deployment, and that, for explicit or implicit objectives, infers, from the input it receives, how to generate outputs such as predictions, content, recommendations, or decisions that can influence physical or virtual environments"), "EU Art. 3(1) definition verbatim on OJ page 46")
check(has(eu_pages[3], "AI systems are designed to operate with varying levels of autonomy, meaning that they have some degree of independence of actions from human involvement and of capabilities to operate without human intervention"), "EU Recital 12 autonomy sentence verbatim on OJ page 4")
check(has(eu_pages[59], "commensurate with the risks, level of autonomy and context of use of the high-risk AI system"), "EU Art. 14(3) oversight sentence on OJ page 60")
check(has(eu_pages[54], "the extent to which the AI system acts autonomously and the possibility for a human to override a decision"), "EU Art. 7(2)(d) criterion on OJ page 55")
check(has(nist.replace("- ", ""), "an engineered or machine-based system that can, for a given set of objectives, generate outputs such as predictions, recommendations, or decisions influencing real or virtual environments. AI systems are designed to operate with varying levels of autonomy"), "NIST AI RMF definition verbatim (the PDF breaks 'recommenda-tions' across a line; the line-break hyphen is joined before comparing)")
check(has(nist, "Adapted from: OECD Recommendation on AI:2019; ISO/IEC 22989:2022"), "NIST cites the OECD and ISO/IEC 22989 by name")
check(has(nist_pages[5], "engineered or machine-based system"), "the NIST definition sits on PDF page 6 (the report's page 1)")
check(has(co, '"ARTIFICIAL INTELLIGENCE SYSTEM" MEANS ANY MACHINE-BASED SYSTEM THAT, FOR ANY EXPLICIT OR IMPLICIT OBJECTIVE, INFERS FROM THE INPUTS THE SYSTEM RECEIVES HOW TO GENERATE OUTPUTS, INCLUDING CONTENT, DECISIONS, PREDICTIONS, OR RECOMMENDATIONS, THAT CAN INFLUENCE PHYSICAL OR VIRTUAL ENVIRONMENTS'), "Colorado definition verbatim (enrolled text, capitals)")
check(has(tx, '"Artificial intelligence system" means any machine-based system that, for any explicit or implicit objective, infers from the inputs the system receives how to generate outputs, including content, decisions, predictions, or recommendations, that can influence physical or virtual environments'), "Texas definition verbatim, identical wording to Colorado's")
check(has(ca, '"Artificial intelligence model" means an engineered or machine-based system that varies in its level of autonomy and that can, for explicit or implicit objectives, infer from the input it receives how to generate outputs that can influence physical or virtual environments'), "California SB 53 definition verbatim")
check(has(ut, "generates non-scripted outputs similar to outputs created by a human, with") and has(ut, "limited or no human oversight"), "Utah SB 149 'limited or no human oversight' verbatim (the enrolled text carries line numbers between the two halves)")
check(has(omb, "This definition includes systems that are fully autonomous, partially autonomous, and not autonomous, and it includes systems that operate both with and without human oversight"), "OMB M-25-21 scope note verbatim")
ndaa = txt("plaw_115_232.htm")
check(has(ndaa, "An artificial system designed to act rationally, including an intelligent software agent or embodied robot that achieves goals using perception, planning, reasoning, learning, communicating, decision making, and acting"), "Pub. L. 115-232 sec. 238(g)(5) 'intelligent software agent' verbatim")
check(has(iso, "3.1.1 AI agent automated (3.1.7) entity that senses and responds to its environment and takes actions to achieve its goals"), "ISO/IEC 22989 clause 3.1.1 'AI agent' verbatim (the preview's terms clause)")
check(has(iso, "3.1.4 artificial intelligence system AI system engineered system that generates outputs such as content, forecasts, recommendations or decisions for a given set of human-defined objectives"), "ISO/IEC 22989 clause 3.1.4 'artificial intelligence system / AI system' verbatim — three entries after 3.1.1")

print("== 3. The Federal Register counts and documents")
fr = json.load(open(HERE / "fr_ai_agents_c6281.json", encoding="utf-8"))
check(fr['"AI agents"'] == {"2019": 0, "2020": 0, "2021": 0, "2022": 0, "2023": 0, "2024": 0, "2025": 2, "2026": 3}, "'AI agents' by year: 0 through 2024, 2 in 2025, 3 in 2026 (5 documents ever)")
check(fr['"agentic"'] == {"2019": 0, "2020": 0, "2021": 0, "2022": 0, "2023": 0, "2024": 0, "2025": 4, "2026": 4}, "'agentic' by year: 0 through 2024, 4 and 4 (8 documents)")
check(fr['"autonomous agents"'] == {y: 0 for y in ("2019", "2020", "2021", "2022", "2023", "2024", "2025", "2026")}, "'autonomous agents': 0 every year")
check(fr['"artificial intelligence"'] == {"2019": 53, "2020": 87, "2021": 98, "2022": 99, "2023": 170, "2024": 287, "2025": 174, "2026": 239}, "control 'artificial intelligence': 53, 87, 98, 99, 170, 287, 174, 239")
check(all(v == 0 for v in fr['"zxqv agentoid"'].values()), "nonsense-phrase control: 0 every year")
docs = (HERE / "fr_ai_agents_docs_c6281.out.txt").read_text(encoding="utf-8", errors="replace")
sec = docs.split('== "AI agents"')[1].split('== "AI agent"')[0]
check(sec.count("\n   20") == 5, "five 'AI agents' documents listed")
for needle, label in (("2025-01-15", "BIS AI Diffusion rule, Jan. 15, 2025"), ("EO14363", "EO 14363 Genesis Mission"), ("2026-00206", "NIST/CAISI RFI 2026-00206"), ("EO14409", "EO 14409"), ("2026-12811", "OPM SES rule 2026-12811")):
    check(needle in sec, "'AI agents' document: " + label)
sec8 = docs.split('== "agentic"')[1]
check(sec8.count("\n   20") == 8, "eight 'agentic' documents listed")

print("== 4. The executive orders, the RFI and the footnote, verbatim")
eo363 = txt("eo14363.txt")
check(has(eo363, "create AI agents to test new hypotheses"), "EO 14363 sec. 1: 'create AI agents to test new hypotheses'")
check(has(eo363, "AI agents to explore design spaces"), "EO 14363 sec. 3(a)(ii): 'AI agents to explore design spaces'")
check(len(AGENT.findall(eo363)) == 3, "EO 14363: 3 'agent' hits (two AI agents, one boilerplate)")
eo409 = txt("eo14409.txt")
check(has(eo409, "employing AI agents to unlawfully access data or information that is subsequently used for a criminal or unlawful purpose"), "EO 14409 sec. 4: 'employing AI agents to unlawfully access data' verbatim")
check(len(AGENT.findall(eo409)) == 2, "EO 14409: 2 'agent' hits (one AI agents, one boilerplate)")
check(not re.search(r"\bAI agents?\b.{0,80}\bmeans\b", eo409, re.I | re.S), "EO 14409 defines 'AI agents' nowhere")
check(has(eo409, "National Security Systems"), "EO 14409's one 'as defined in' concerns National Security Systems")
for name in ("eo14179.txt", "eo14277.txt", "eo14365.txt"):
    t = txt(name)
    check(len(AGENT.findall(t)) == 1 and has(t, "officers, employees, or agents"), name + ": the one 'agents' is the General Provisions boilerplate")
rfi = txt("fr_nist_rfi_agents.txt")
check(has(rfi, "AI agent systems consist of at least one generative AI model and scaffolding software that equips the model with tools to take a range of discretionary actions"), "NIST/CAISI RFI working description verbatim")
check(has(rfi, "These systems may be more expansive, containing multiple sub-agents with software that orchestrates their interactions. They can be deployed with little to no human oversight. Other terms used to refer to AI agent systems include AI agents and agentic AI"), "NIST/CAISI RFI: sub-agents, orchestrator, 'AI agents and agentic AI' verbatim")
check(len(AGENT.findall(rfi)) == 68, "NIST/CAISI RFI: 68 'agent' hits (got %d)" % len(AGENT.findall(rfi)))
check(has(rfi, "March 9, 2026"), "NIST/CAISI RFI: comments closed March 9, 2026")
hhs = txt("fr_hhs_onc.txt")
check(has(hhs, "is often referred to a ``agentic AI'' because these systems' capabilities involve them acting independently"), "HHS/ONC footnote 66 verbatim (with its 'referred to a' slip)")
check(has(hhs, "Merriam- Webster ``agentic'' (https://www.merriam-webster.com/slang/"), "HHS/ONC footnote 66 cites Merriam-Webster's slang page")
check(has(hhs, "Agentic AI: Autonomous Intelligence for Complex Goals"), "HHS/ONC footnote 66 cites the IEEE Access survey")
check(has(hhs, "autonomous artificial intelligence systems (``agentic artificial intelligence'' or ``agentic AI'')"), "HHS/ONC proposed text: 'autonomous artificial intelligence systems (agentic artificial intelligence or agentic AI)'")
check(len(AGENTIC.findall(hhs)) == 10 and len(AGENT.findall(hhs)) == 1, "HHS/ONC NPRM: 10 'agentic', 1 'agent'")

print("== 5. Derived figures the essay quotes")
check(round(1122 / 144, 1) == 7.8, "1,122 'AI system' over 144 OJ pages is about eight a page (7.8)")
check(sum(fr['"AI agents"'].values()) == 5 and sum(fr['"agentic"'].values()) == 8, "5 and 8 documents in total")
check(sum(fr['"artificial intelligence"'].values()) == 1207, "the control phrase appears in 1,207 documents over the same years")

print()
print("CONTROL COUNTS (quote these):", CONTROL)
print("FAILS: %d" % len(FAILS))
for f in FAILS:
    print("  - " + f)
sys.exit(1 if FAILS else 0)
