diff --git a/scripts/audit_providers_source_of_truth.py b/scripts/audit_providers_source_of_truth.py new file mode 100644 index 00000000..122e1c3d --- /dev/null +++ b/scripts/audit_providers_source_of_truth.py @@ -0,0 +1,43 @@ +"""Audit: fail if PROVIDERS is declared (as a literal list) anywhere +except src/ai_client.py. + +The follow-up track's invariant: PROVIDERS lives in src/ai_client.py +because it's the AI-client system constant (per the AGENTS.md HARD +RULE on src/ files). The src/models.py re-export via __getattr__ +is allowed (it's lazy-loaded, not a literal declaration). + +This audit catches accidental PROVIDERS literals that creep back +in (e.g., a contributor adds a new vendor to src/models.py:PROVIDERS +instead of src/ai_client.py:PROVIDERS). + +Usage: uv run python scripts/audit_providers_source_of_truth.py +Exit code: 0 = pass; 1 = violation found. +""" +import re +import sys +from pathlib import Path + +ALLOWED_DECLARATION = Path("src/ai_client.py") +PROVIDERS_LITERAL = re.compile(r"^PROVIDERS\s*:\s*List\[str\]\s*=\s*\[", re.MULTILINE) + +def main() -> int: + violation: str = "" + for path in Path("src").rglob("*.py"): + text = path.read_text(encoding="utf-8") + for match in PROVIDERS_LITERAL.finditer(text): + if path != ALLOWED_DECLARATION: + line_no = text[:match.start()].count("\n") + 1 + violation = f"{path}:{line_no}: {match.group(0)}" + break + if violation: + break + if violation: + print(f"FAIL: PROVIDERS declared outside {ALLOWED_DECLARATION}:") + print(f" {violation}") + print(f" Add the new vendor to {ALLOWED_DECLARATION} instead.") + return 1 + print(f"OK: PROVIDERS only declared in {ALLOWED_DECLARATION}") + return 0 + +if __name__ == "__main__": + sys.exit(main())