#!/usr/bin/env python3 """Guard AWOOOI product navigation against orphaned frontend routes.""" from __future__ import annotations import argparse import json import re import sys from pathlib import Path ROUTE_RE = re.compile(r"href:\s*'([^']+)'") ROUTE_LIST_RE = re.compile(r"(?:aliases|relatedPaths):\s*\[([^\]]*)\]") LABEL_KEY_RE = re.compile(r"labelKey:\s*'([^']+)'") QUOTED_RE = re.compile(r"'([^']+)'") def locale_routes(root: Path) -> set[str]: app_root = root / "apps/web/src/app/[locale]" routes: set[str] = set() for page in app_root.rglob("page.tsx"): rel = page.relative_to(app_root) route_parts = rel.parts[:-1] if any(part.startswith("[") and part.endswith("]") for part in route_parts): continue route = "/" + "/".join(route_parts) routes.add("/" if route == "/" else route.rstrip("/")) return routes def navigation_routes(root: Path) -> set[str]: source = (root / "apps/web/src/lib/navigation/product-ia.ts").read_text( encoding="utf-8" ) routes = set(ROUTE_RE.findall(source)) for match in ROUTE_LIST_RE.findall(source): routes.update(QUOTED_RE.findall(match)) return {route.rstrip("/") or "/" for route in routes} def navigation_label_keys(root: Path) -> set[str]: source = (root / "apps/web/src/lib/navigation/product-ia.ts").read_text( encoding="utf-8" ) return set(LABEL_KEY_RE.findall(source)) def missing_nav_labels(root: Path, label_keys: set[str]) -> dict[str, list[str]]: missing: dict[str, list[str]] = {} for messages_file in sorted((root / "apps/web/messages").glob("*.json")): with messages_file.open(encoding="utf-8") as handle: payload = json.load(handle) nav = payload.get("nav") if isinstance(payload, dict) else None nav_keys = set(nav) if isinstance(nav, dict) else set() gaps = sorted(label_keys - nav_keys) if gaps: missing[messages_file.name] = gaps return missing def covered(route: str, nav_routes: set[str]) -> bool: if route in nav_routes: return True return any(parent != "/" and route.startswith(parent + "/") for parent in nav_routes) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--root", default=".", help="Repository root") args = parser.parse_args() root = Path(args.root).resolve() routes = locale_routes(root) nav_routes = navigation_routes(root) label_keys = navigation_label_keys(root) missing = sorted(route for route in routes if not covered(route, nav_routes)) missing_labels = missing_nav_labels(root, label_keys) sidebar = (root / "apps/web/src/components/layout/sidebar.tsx").read_text( encoding="utf-8" ) hidden_secondary_filter = "surface !== 'secondary'" in sidebar if missing or missing_labels or hidden_secondary_filter: if missing: print("NAVIGATION_COVERAGE_GUARD_MISSING_ROUTES") for route in missing: print(f"- {route}") if missing_labels: print("NAVIGATION_COVERAGE_GUARD_MISSING_LABEL_KEYS") for file_name, gaps in missing_labels.items(): print(f"- {file_name}: {', '.join(gaps)}") if hidden_secondary_filter: print("NAVIGATION_COVERAGE_GUARD_HIDDEN_SECONDARY_ITEMS") print("- Sidebar must not filter product nav items by surface !== 'secondary'.") return 1 print( "AWOOOI_NAVIGATION_COVERAGE_GUARD_OK " f"routes={len(routes)} nav_routes={len(nav_routes)} nav_label_keys={len(label_keys)}" ) return 0 if __name__ == "__main__": sys.exit(main())