Files
awoooi/scripts/dev/awoooi-navigation-coverage-guard.py
Your Name c04a72c2a6
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
fix(web): restore product navigation coverage
2026-06-30 02:39:51 +08:00

108 lines
3.6 KiB
Python

#!/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())