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>
This commit is contained in:
27
.gitea/workflows/lint.yaml
Normal file
27
.gitea/workflows/lint.yaml
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
| `pc.sh` | bash | **wrapper לכל קריאות Paperclip API מסוכנים** — מוסיף Authorization, X-Paperclip-Run-Id (audit trail), Content-Type, base URL. תחביר: `pc.sh <METHOD> <PATH> [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 <file>...` = קבצים נתונים (ל-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). | ידני אחרי כל שינוי |
|
||||
|
||||
56
scripts/check_undefined_names.py
Normal file
56
scripts/check_undefined_names.py
Normal file
@@ -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())
|
||||
Reference in New Issue
Block a user