fix(web): restore product navigation coverage
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 24s
CD Pipeline / build-and-deploy (push) Successful in 4m52s
CD Pipeline / post-deploy-checks (push) Successful in 58s
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 24s
CD Pipeline / build-and-deploy (push) Successful in 4m52s
CD Pipeline / post-deploy-checks (push) Successful in 58s
This commit is contained in:
107
scripts/dev/awoooi-navigation-coverage-guard.py
Normal file
107
scripts/dev/awoooi-navigation-coverage-guard.py
Normal file
@@ -0,0 +1,107 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user