#!/usr/bin/env python3 from __future__ import annotations import argparse import json import re import sys from pathlib import Path from typing import Dict, List REPO_ROOT = Path(__file__).resolve().parents[2] COMPARATOR_RE = re.compile(r"(==|~=|>=|<=|!=|>|<)") def _validate_requirements(path: Path) -> List[str]: errors: List[str] = [] if not path.exists(): return [f"Missing requirements file: {path}"] lines = path.read_text(encoding="utf-8", errors="ignore").splitlines() for idx, raw in enumerate(lines, start=1): line = raw.strip() if not line or line.startswith("#"): continue if line.startswith(("-r", "--requirement", "--extra-index-url", "--index-url")): continue if "git+" in line.lower() or line.startswith(("-e ", "--editable ")): errors.append(f"{path}:{idx}: disallowed editable/git dependency: {line}") continue if "@" in line and "://" in line: errors.append(f"{path}:{idx}: disallowed URL dependency: {line}") continue if not COMPARATOR_RE.search(line): errors.append(f"{path}:{idx}: dependency must be version constrained: {line}") return errors def _validate_npm_lock(path: Path) -> List[str]: errors: List[str] = [] if not path.exists(): errors.append(f"Missing npm lockfile: {path}") return errors def main() -> int: parser = argparse.ArgumentParser(description="Checks dependency policy for backend/frontend lock hygiene.") parser.add_argument( "--requirements", default=str(REPO_ROOT / "backend" / "requirements.txt"), help="Path to backend requirements.txt", ) parser.add_argument( "--npm-lock", default=str(REPO_ROOT / "frontend" / "package-lock.json"), help="Path to frontend package-lock.json", ) parser.add_argument("--out", default="", help="Optional output JSON path") args = parser.parse_args() req_path = Path(args.requirements) lock_path = Path(args.npm_lock) req_errors = _validate_requirements(req_path) lock_errors = _validate_npm_lock(lock_path) errors = req_errors + lock_errors report: Dict[str, object] = { "ok": not errors, "requirements_path": str(req_path), "npm_lock_path": str(lock_path), "errors": errors, } payload = json.dumps(report, indent=2) if args.out: out = Path(args.out) out.parent.mkdir(parents=True, exist_ok=True) out.write_text(payload + "\n", encoding="utf-8") print(f"Wrote dependency policy report: {out}") print(payload) return 0 if not errors else 2 if __name__ == "__main__": raise SystemExit(main())