Files
legal-ai/scripts/check_undefined_names.py
Chaim 0a3bc35623
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
ci: gate undefined names (pyflakes F821) + fix latent NameError in db.py
Prevents recurrence of the case-rename 500 (PR #249), whose root cause was
an undefined name (`paperclip_client`) sitting in a background_tasks callable
— invisible until that code path ran in production.

- scripts/check_undefined_names.py: runs pyflakes on web/, mcp-server/src,
  scripts/ and fails ONLY on "undefined name" / "may be undefined" (the
  runtime-crash class). Unused imports / f-strings are NOT gated — keeps the
  check high-signal and green.
- .gitea/workflows/lint.yaml: runs the guard on every PR and push to main,
  in a throwaway venv (PEP-668 safe).
- db.py: `from datetime import date` → `date, datetime`. The guard surfaced a
  real latent undefined name — `insert_panel_round`'s `round_ts: datetime`
  annotation referenced an unimported `datetime` (benign only because of
  `from __future__ import annotations`; now correct).
- SCRIPTS.md: documented the new guard.

Verified: clean tree → exit 0; injected undefined name → exit 1.

Invariants: engineering rule §6 (no silent failures shipping to runtime).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 09:58:45 +00:00

57 lines
2.0 KiB
Python

#!/usr/bin/env python3
"""CI guard: fail on undefined-name references (the pyflakes F821 class).
This is the exact bug class behind the case-rename 500 (PR #249): a name
referenced but never imported/defined. It is invisible to tests when it sits
inside a rarely-hit branch or a fire-and-forget ``background_tasks`` callable —
it only NameErrors when that code path runs in production. pyflakes catches it
statically, before merge.
Scope is deliberately narrow — we gate ONLY on undefined names, not on the
other pyflakes findings (unused imports, f-strings without placeholders, unused
locals). Those are style noise, not runtime crashes; gating on them would make
the check too noisy to keep green. Keep this gate high-signal.
Requires pyflakes importable by the running interpreter (the workflow installs
it into a throwaway venv and runs this script with that venv's python).
"""
from __future__ import annotations
import subprocess
import sys
# Paths that ship into the running app / are executed operationally.
TARGETS = ["web", "mcp-server/src", "scripts"]
# pyflakes messages that mean "this reference will NameError at runtime".
FATAL_MARKERS = ("undefined name", "may be undefined")
def main() -> int:
proc = subprocess.run(
[sys.executable, "-m", "pyflakes", *TARGETS],
capture_output=True,
text=True,
)
# pyflakes exits non-zero whenever it has ANY finding; we re-classify so
# that only the fatal class fails the build.
lines = (proc.stdout + proc.stderr).splitlines()
fatal = [ln for ln in lines if any(m in ln for m in FATAL_MARKERS)]
if fatal:
print("❌ undefined name(s) detected — these crash at runtime:\n")
for ln in fatal:
print(f" {ln}")
print(
f"\n{len(fatal)} undefined-name finding(s). Import or define the "
"name, or delete the dead reference."
)
return 1
print(f"✓ no undefined names in: {', '.join(TARGETS)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())