From 0a3bc35623ced532e72038dcdb5585e32a87e1db Mon Sep 17 00:00:00 2001 From: Chaim Date: Sun, 14 Jun 2026 09:58:45 +0000 Subject: [PATCH] ci: gate undefined names (pyflakes F821) + fix latent NameError in db.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .gitea/workflows/lint.yaml | 27 ++++++++++++ mcp-server/src/legal_mcp/services/db.py | 2 +- scripts/SCRIPTS.md | 1 + scripts/check_undefined_names.py | 56 +++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 .gitea/workflows/lint.yaml create mode 100644 scripts/check_undefined_names.py diff --git a/.gitea/workflows/lint.yaml b/.gitea/workflows/lint.yaml new file mode 100644 index 0000000..19341da --- /dev/null +++ b/.gitea/workflows/lint.yaml @@ -0,0 +1,27 @@ +name: Lint — undefined names + +# High-signal static gate for the bug class behind PR #249 (case-rename 500): +# a name referenced but never imported/defined. Invisible to tests when it sits +# in a rarely-hit branch or a fire-and-forget background task — it only +# NameErrors at runtime. pyflakes catches it before merge. Gates ONLY on +# undefined names (not unused imports / f-strings — those are noise). Uses a +# throwaway venv so it is immune to PEP-668 externally-managed environments. + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + undefined-names: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run undefined-name guard + run: | + python3 -m venv /tmp/lintvenv + /tmp/lintvenv/bin/pip install --quiet pyflakes==3.4.0 + /tmp/lintvenv/bin/python scripts/check_undefined_names.py diff --git a/mcp-server/src/legal_mcp/services/db.py b/mcp-server/src/legal_mcp/services/db.py index bf856ae..0c14c0b 100644 --- a/mcp-server/src/legal_mcp/services/db.py +++ b/mcp-server/src/legal_mcp/services/db.py @@ -7,7 +7,7 @@ import hashlib import json import logging import re -from datetime import date +from datetime import date, datetime from uuid import UUID, uuid4 import asyncpg diff --git a/scripts/SCRIPTS.md b/scripts/SCRIPTS.md index cec299a..c870ec7 100644 --- a/scripts/SCRIPTS.md +++ b/scripts/SCRIPTS.md @@ -11,6 +11,7 @@ | `pc.sh` | bash | **wrapper לכל קריאות Paperclip API מסוכנים** — מוסיף Authorization, X-Paperclip-Run-Id (audit trail), Content-Type, base URL. תחביר: `pc.sh [BODY_JSON]`. אסור `curl` ישיר ל-`$PAPERCLIP_API_URL`. ראה `HEARTBEAT.md §0`. counterpart ב-Python: `web/paperclip_api.py`. | נקרא ע"י סוכנים | | `spec-guard.sh` | bash | **PreToolUse hook לאכיפת "פרוטוקול כתיבת-קוד"** (CLAUDE.md §פרוטוקול כתיבת-קוד) — בכל Edit/Write/MultiEdit על נתיב-קוד (`web/`, `mcp-server/`, `web-ui/src/`, `scripts/`, `adapters/`) מזריק תזכורת ל-Claude לקרוא את `docs/spec/00-constitution.md`+ספ-התחום ולוודא קיום G1–G12 — לפני שכותבים. **+ leak-guard בזמן-אמת (G12):** על כתיבה ל-`mcp-server/src/*` בודק את התוכן-הנכתב (`new_string`/`content`) ומזהיר אם מוזרק מונח-Paperclip לשכבת-האינטליגנציה (לא-deduped). המקבילה האינטראקטיבית ל-INV-AG1. קלט JSON ב-stdin, פלט `hookSpecificOutput.additionalContext` (non-blocking, exit 0). Dedup פעם-בסשן לתזכורת-הספ. רשום ב-`.claude/settings.json`. | נקרא אוטומטית ע"י Claude Code (hook) | | `leak_guard.py` | python | **המאכף הקנוני של INV-G12 (שער-הפלטפורמה / docs/spec/X15 §4 / R4).** שני כללים קשיחים: (1) `mcp-server/src` ללא סמלי-Paperclip (allowlist מנומק לפי substring); (2) רק `web/agent_platform_port.py` (+ קבצי-המעטפת) מייבאים את לקוח-Paperclip. stdlib-בלבד (אין venv). `leak_guard.py` = סריקת-repo (exit 1 על הפרה); `leak_guard.py ...` = קבצים נתונים (ל-hook). משותף ל-spec-guard.sh (hook), ל-CI (`.gitea/workflows/leak-guard.yaml`) ול-`mcp-server/tests/test_platform_port_leak_guard.py`. | CI + hook + pytest | +| `check_undefined_names.py` | python | **CI gate ל-undefined names (מחלקת ה-NameError).** מריץ pyflakes על `web`, `mcp-server/src`, `scripts` ומפיל build (exit 1) רק על "undefined name"/"may be undefined" — לא על imports-לא-בשימוש/f-strings (רעש). זו בדיוק מחלקת-הבאג של PR #249 (שינוי-שם תיק → 500): שם שמופנה אך לא מיובא/מוגדר, חבוי בתוך `background_tasks` עד זמן-ריצה. דורש pyflakes (ה-workflow מתקין ל-venv זמני). משותף ל-CI (`.gitea/workflows/lint.yaml`). | CI | | `migrate_gap51_outcomes.py` | python | **GAP-51 (FU-14)** — נרמול ערכי `outcome` לאוצר הקנוני (rejected→rejection, accepted→full_acceptance, partial→partial_acceptance) ב-`decisions.outcome` + `cases.expected_outcome`. `betterment_levy` לא ממופה (practice_area, לא outcome). `--dry-run` (ברירת-מחדל) / `--apply` (גיבוי ל-`data/audit/gap51-outcome-backup-*.csv` + UPDATE טרנזקציוני). דורש POSTGRES_URL. בוצע 2026-06-06 (9 שורות). נוגע רק ב-cases/decisions — בטוח במקביל לחילוץ. | חד-פעמי (בוצע) | | `sync_missing_agent_skills.py` | python | סקריפט "אל-כשל" להוספת `paperclipSkillSync` ל-`הגהת מסמכים` ו-`מנתח משפטי` שפיספסו את ה-sync ההיסטורי (Gap #28). תומך `--verify`/`--dry-run`/`--apply`. גיבוי אוטומטי ל-`agents-pre-skill-sync-*.sql`. דורש `PAPERCLIP_BOARD_API_KEY` (Infisical /paperclip ב-nautilus env). idempotent. | חד-פעמי (בוצע 2026-05-04). שמור לרפרנס | | `sync_agents_across_companies.py` | python | **סנכרון סוכנים מ-CMP (1xxx, master) ל-CMPA (8xxx, mirror)** — Gap #25. משווה adapter_config (model/timeout/instructions/skills/etc), runtime_config (heartbeat), ושדות top-level (budget/metadata/icon/title/role). מסנן אוטומטית local skills שלא קיימים ב-mirror. לוגיקת subset (mirror יכול להחזיק יותר skills כי ה-API מוסיף required runtime skills). תומך `--verify`/`--dry-run`/`--apply [--only NAME]`. גיבוי אוטומטי. דורש `PAPERCLIP_BOARD_API_KEY`. **להריץ אחרי כל שינוי הגדרות ב-CMP.** **⚠ אם `adapter_type` שונה בין CMP ל-CMPA — `--apply` מדלג על הסוכן; `--verify` מדווח אותו רם כ-DRIFT.** בעת מעבר adapter (למשל ל-`deepseek_local`) חובה לעדכן ידנית בשתי החברות. **`--verify` יוצא exit≠0 על כל drift** (needs-sync / adapter-mismatch / missing-in-mirror) — שמיש כ-gate ל-cron/CI (GAP-21/FU-8a). | ידני אחרי כל שינוי | diff --git a/scripts/check_undefined_names.py b/scripts/check_undefined_names.py new file mode 100644 index 0000000..6f6f4bb --- /dev/null +++ b/scripts/check_undefined_names.py @@ -0,0 +1,56 @@ +#!/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())