"""Documentation that must follow the code (the code audit of 2026-09-21, follow-up of 2026-09-22: 'Remaining doc drift'). Offline; reads the source tree and RUNBOOK.md. - RUNBOOK's 'Environment variables' table lists every variable the code reads (os.environ / env.get / getenv in canlex/ and tools/) and every one the Dockerfile sets for CanLex or the Hugging Face library; it names no CANLEX_* variable nothing reads or sets. (28 were undocumented, among them CANLEX_HTTP and CANLEX_SEE_ALSO_DRAFT.) - The refresh choreography names every ingester (irb_guidelines and conduct were missing) and says every ingester takes --allow-shrink. - The call-log section names every calllog.RESULT_KEYS field (it named five of twelve). - No RUNBOOK line cites a scratchpad script that is not in the repo (three cited the a14/a15 before/after harness). - canlex_list_acts' 'Bilingual' line counts French LEGISLATION only (it counted 124 instruments -- the 80 Acts and regulations plus 44 French guidance documents -- and 26,829 'articles' for 20,680). """ import re import unittest from pathlib import Path from canlex import calllog, server from canlex.index import LegislationIndex ROOT = Path(__file__).resolve().parent.parent RUNBOOK = (ROOT / "RUNBOOK.md").read_text(encoding="utf-8") # os.environ.get("X"), os.environ["X"], env.get("X") (calllog.configure's # env = os.environ), env["X"] (sweep.py), os.getenv("X"), and a module # constant naming the variable a read uses, ENV_X = "CANLEX_X" # (canlex/ratelimit.py reads its two variables through ENV_PER_MIN / ENV_BURST) _ENV_READ = re.compile( r"""\b(?:environ|env)(?:\.get\(|\[)\s*["']([A-Z][A-Z0-9_]+)["']""" r"""|\bgetenv\(\s*["']([A-Z][A-Z0-9_]+)["']""" r"""|^ENV_[A-Z0-9_]+\s*=\s*["']((?:CANLEX|HF|CANLII)_[A-Z0-9_]+)["']""", re.M) _DOCKER_ENV = re.compile(r"^\s*(?:ENV\s+)?((?:CANLEX|HF|CANLII)_[A-Z0-9_]+|PORT)=", re.M) def env_vars_read(): """{name: [files]} for every environment variable the code reads.""" found = {} for top in ("canlex", "tools"): for path in sorted((ROOT / top).rglob("*.py")): for m in _ENV_READ.finditer(path.read_text(encoding="utf-8")): name = m.group(1) or m.group(2) or m.group(3) found.setdefault(name, []).append(path.relative_to(ROOT).as_posix()) return found def section(heading, level="##"): """The RUNBOOK text under `heading` up to the next heading of its level or above.""" start = RUNBOOK.index(heading) stops = [m.start() for m in re.finditer(r"^#{1,%d} " % len(level), RUNBOOK, re.M) if m.start() > start] return RUNBOOK[start:stops[0] if stops else len(RUNBOOK)] def table_names(): body = section("### Environment variables", "###") return set(re.findall(r"^\| `([A-Z][A-Z0-9_]+)` \|", body, re.M)) class EnvironmentTableTests(unittest.TestCase): def test_the_grep_finds_the_known_readers(self): found = env_vars_read() for name in ("CANLEX_HTTP", "CANLEX_SEE_ALSO_DRAFT", "CANLEX_LOG_DIR", "CANLII_API_KEY", "PORT", "CANLEX_MCP_URL"): self.assertIn(name, found) self.assertGreaterEqual(len(found), 35) def test_every_variable_the_code_reads_is_in_the_table(self): missing = {n: f for n, f in env_vars_read().items() if n not in table_names()} self.assertEqual(missing, {}, "add these to RUNBOOK 'Environment variables'") def test_every_variable_the_dockerfile_sets_is_in_the_table(self): docker = set(_DOCKER_ENV.findall((ROOT / "Dockerfile").read_text(encoding="utf-8"))) self.assertTrue({"HF_HOME", "HF_HUB_OFFLINE", "CANLEX_HTTP"} <= docker) self.assertEqual(docker - table_names(), set()) def test_the_table_names_nothing_stale(self): docker = set(_DOCKER_ENV.findall((ROOT / "Dockerfile").read_text(encoding="utf-8"))) stale = {n for n in table_names() if n.startswith("CANLEX_") and n not in env_vars_read() and n not in docker} self.assertEqual(stale, set()) def test_the_two_local_only_variables_say_so(self): body = section("### Environment variables", "###") http = next(l for l in body.splitlines() if l.startswith("| `CANLEX_HTTP`")) draft = next(l for l in body.splitlines() if l.startswith("| `CANLEX_SEE_ALSO_DRAFT`")) self.assertIn("Local use only", http) self.assertIn("never set it on the Space", draft) class ChoreographyTests(unittest.TestCase): def ingesters(self): """Every canlex module whose command line takes --allow-shrink.""" return sorted(p.stem for p in (ROOT / "canlex").glob("*.py") if '"--allow-shrink" in sys.argv' in p.read_text(encoding="utf-8")) def test_every_ingester_is_in_the_choreography(self): block = section("## The refresh choreography") self.assertIn("irb_guidelines", self.ingesters()) missing = [m for m in self.ingesters() if f"py -m canlex.{m}" not in block] self.assertEqual(missing, []) def test_allow_shrink_is_documented_there(self): self.assertIn("--allow-shrink", section("## The refresh choreography")) class CallLogFieldTests(unittest.TestCase): def test_every_result_key_is_named(self): body = section("## Call log (2026-09-16)") missing = [k for k in calllog.RESULT_KEYS if f"`{k}`" not in body] self.assertEqual(missing, []) def test_the_call_log_note_is_current(self): # the 2026-09-21 audit's 'the live Space predates the call log' is gone self.assertNotIn("live Space predates the call log", RUNBOOK) self.assertIn("the live Space carries the call log", RUNBOOK) class ScratchpadCitationTests(unittest.TestCase): def test_no_line_cites_a_scratchpad_script(self): cited = re.findall(r"scratchpad[^\n]{0,40}?\b\w+\.py", RUNBOOK) self.assertEqual(cited, []) def _chunk(cid, act_code, lang="en", doc_type="legislation", **extra): c = {"id": cid, "doc_type": doc_type, "act_code": act_code, "act_short": act_code, "act_name": act_code, "section": "1", "marginal_note": "t", "part": "", "division": "", "heading": "", "text": "x", "history": "", "last_amended": "", "in_force": "", "status": "in force", "current_to": "2026-03-31", "citation": f"{act_code}, s. 1", "source_url": "https://example.test"} if lang != "en": c["lang"] = lang c.update(extra) return c class ListActsBilingualLineTests(unittest.TestCase): """canlex_list_acts' coverage line (server.py; HELD: canlex_list_acts is REVIEWED). Before: every French chunk counted, guidance included.""" def setUp(self): idx = LegislationIndex.__new__(LegislationIndex) idx.chunks = [ _chunk("I-2.5-s1", "I-2.5"), _chunk("I-2.5-fr-s1", "I-2.5", "fr"), _chunk("I-2.5-fr-s2", "I-2.5", "fr"), _chunk("C-46-s1", "C-46"), _chunk("C-46-fr-s1", "C-46", "fr"), _chunk("dmemo-fr-1", "D-Memo", "fr", "memorandum", part="Mémorandum"), _chunk("enf-fr-5-1", "ENF-5", "fr", "memorandum", part="ENF 5"), _chunk("directive-fr-1", "d1", "fr", "directive"), ] idx.related = lambda c: {} server._INDEX = idx self.addCleanup(setattr, server, "_INDEX", None) def test_only_french_legislation_is_counted(self): out = server.canlex_list_acts() self.assertIn("_Bilingual: 2 instruments also carry their parallel French " "text (3 articles)", out) self.assertNotIn("5 instruments", out) # the count before the fix