#!/usr/bin/env python3 """Reject executable v1 compatibility paths and symbols from the v2 tree.""" from __future__ import annotations import argparse import re import sys from pathlib import Path LEGACY_PATHS = ( Path("backend/app"), Path("backend/backup"), Path("backend/tests"), Path("frontend/src/api/client.ts"), ) SCAN_ROOTS = ( Path("backend/src"), Path("frontend/src"), Path("openapi"), Path("config"), Path(".github/workflows"), ) SCAN_FILES = ( Path("backend/Dockerfile"), Path("frontend/Dockerfile"), Path("frontend/nginx.conf"), Path("frontend/vite.config.ts"), Path("docker-compose.yml"), Path("compose.yaml"), Path("backend/pyproject.toml"), Path("frontend/package.json"), ) TEXT_SUFFIXES = {".json", ".py", ".toml", ".ts", ".tsx", ".yaml", ".yml"} FORBIDDEN = ( re.compile(r"/api/v1(?:/|\b)"), re.compile( r"\b(?:legacy|v1)[_-]?(?:database|db|payload|backup)?[_-]?(?:reader|importer?|converter?)\b", re.IGNORECASE, ), re.compile( r"\b(?:read|open|load|import|convert)[_-]?(?:legacy|v1)[_-]?(?:database|db|payload|backup)\b", re.IGNORECASE, ), re.compile(r"[\"']backup_tool\.db[\"']", re.IGNORECASE), re.compile(r"%Y-%m-%d_%H%M%S"), re.compile(r"\btimestamp[_-]?(?:directory|parser)\b", re.IGNORECASE), re.compile(r"\b(?:app\.main|backup\.engine)\b"), ) def scan(root: Path) -> list[str]: findings: list[str] = [] for relative in LEGACY_PATHS: if (root / relative).exists(): findings.append(f"legacy path exists: {relative}") candidates = {root / relative for relative in SCAN_FILES if (root / relative).is_file()} for relative_root in SCAN_ROOTS: scan_root = root / relative_root if scan_root.exists(): candidates.update( path for path in scan_root.rglob("*") if path.is_file() and path.suffix in TEXT_SUFFIXES ) for path in sorted(candidates): try: text = path.read_text(encoding="utf-8") except (OSError, UnicodeError) as error: findings.append(f"cannot read {path.relative_to(root)}: {error}") continue for pattern in FORBIDDEN: if pattern.search(text): findings.append(f"forbidden symbol {pattern.pattern!r}: {path.relative_to(root)}") return findings def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("root", nargs="?", default=".", type=Path) args = parser.parse_args() root = args.root.resolve() findings = scan(root) if findings: print("v1 compatibility scan failed:", file=sys.stderr) for finding in findings: print(f"- {finding}", file=sys.stderr) return 1 print("v1 compatibility scan: OK") return 0 if __name__ == "__main__": raise SystemExit(main())