#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/harness/lib.sh" main() { cd "$ROOT_DIR" local uv_bin uv_bin="$(harness_require_tool uv)" harness_run "$uv_bin" run --frozen --project "$ROOT_DIR" python - <<'PY' import json import re from pathlib import Path from urllib.parse import urlsplit root = Path.cwd() failures: list[str] = [] warnings: list[str] = [] def fail(message: str) -> None: failures.append(message) def warn(message: str) -> None: warnings.append(message) def read_text(path: str) -> str: return (root / path).read_text(encoding="utf-8") def check_package_manager() -> None: for path in ("package.json", "frontend/package.json"): payload = json.loads(read_text(path)) package_manager = payload.get("packageManager", "") if not str(package_manager).startswith("bun@"): fail(f"{path}: packageManager must stay Bun-only, got {package_manager!r}") for script_name, command in payload.get("scripts", {}).items(): if re.search(r"\b(npm|pnpm|yarn)\b", command): fail(f"{path} script {script_name!r} uses a forbidden package manager: {command}") forbidden_lockfiles = [ "frontend/package-lock.json", "frontend/pnpm-lock.yaml", "frontend/yarn.lock", ] for path in forbidden_lockfiles: if (root / path).exists(): fail(f"{path}: forbidden frontend lockfile; frontend package management is Bun-only") def extract_admin_routes() -> tuple[set[str], set[str]]: admin_routes_text = read_text("frontend/src/admin/AdminRoutes.tsx") manifest_text = read_text("frontend/src/admin/routes/manifest.tsx") route_paths = set(re.findall(r' None: route_paths, manifest_paths = extract_admin_routes() public_manifest_paths = {"/earth", "/docs"} redirect_only_routes = {"/alerts"} missing_routes = sorted(manifest_paths - route_paths - public_manifest_paths) if missing_routes: fail( "admin route manifest points to paths that AdminRoutes does not render: " + ", ".join(missing_routes) ) missing_manifest = sorted(route_paths - manifest_paths - redirect_only_routes) if missing_manifest: fail( "AdminRoutes renders paths that are missing from admin route manifest: " + ", ".join(missing_manifest) ) def known_frontend_routes() -> set[str]: app_text = read_text("frontend/src/App.tsx") admin_text = read_text("frontend/src/admin/AdminRoutes.tsx") manifest_text = read_text("frontend/src/admin/routes/manifest.tsx") routes = set(re.findall(r' None: routes = known_frontend_routes() allowed_prefixes = ( "/api/", "/ws", "/assets/", "/earth/", ) literal_link_pattern = re.compile(r"\b(?:to|href)\s*=\s*['\"](/[^'\"#]+(?:#[^'\"]*)?)['\"]") for path in (root / "frontend/src").rglob("*.tsx"): text = path.read_text(encoding="utf-8") rel = path.relative_to(root) for match in literal_link_pattern.finditer(text): href = match.group(1) route_path = urlsplit(href).path if route_path in routes: continue if route_path.startswith("/docs/"): continue if any(route_path.startswith(prefix) for prefix in allowed_prefixes): continue line_no = jsx_line_number(text, match.start()) fail(f"{rel}:{line_no}: literal internal link points to an unknown frontend route: {href}") def check_admin_search_route_targets() -> None: routes = known_frontend_routes() text = read_text("frontend/src/admin/search/indexers.ts") for match in re.finditer(r"\broutePath:\s*'([^']+)'", text): route_path = match.group(1) if route_path in routes: continue line_no = jsx_line_number(text, match.start()) fail( "frontend/src/admin/search/indexers.ts:" f"{line_no}: admin search routePath points to an unknown frontend route: {route_path}" ) def iter_frontend_src_files() -> list[Path]: return [ path for path in (root / "frontend/src").rglob("*") if path.is_file() and path.suffix in {".ts", ".tsx", ".js", ".jsx", ".css"} ] def collect_inline_style_context(lines: list[str], start_index: int) -> str: block = [] for line in lines[start_index:start_index + 8]: block.append(line.strip()) if "}}" in line or "} as CSSProperties" in line: break return " ".join(block) def inline_style_is_dynamic(context: str) -> bool: dynamic_needles = ( "--", "CSSProperties", "transform:", "translate", "scale(", "width:", "height:", "thumbSize", "thumbOffset", "trackSize", "header.getSize", "summaryWidth", "detailWidth", "progress", "pan.x", "pan.y", "zoom", ) return any(needle in context for needle in dynamic_needles) def iter_css_blocks(text: str) -> list[tuple[int, str, str]]: text = re.sub(r"/\*.*?\*/", "", text, flags=re.S) blocks = [] start_line = 1 selector_lines = [] body_lines = [] in_block = False for line_no, line in enumerate(text.splitlines(), 1): if not in_block: selector_lines.append(line) if "{" in line: in_block = True start_line = line_no before, after = line.split("{", 1) selector_lines[-1] = before body_lines = [after] continue if "}" in line: before, _after = line.split("}", 1) body_lines.append(before) blocks.append((start_line, " ".join(selector_lines).strip(), "\n".join(body_lines))) selector_lines = [] body_lines = [] in_block = False else: body_lines.append(line) return blocks def overflow_hidden_has_explicit_owner(selector: str, body: str) -> bool: owner_needles = ( "min-height: 0", "height: 100%", "height: 100vh", "display: grid", "display: flex", "text-overflow: ellipsis", "border-radius:", "position: fixed", "position: absolute", "scrollbar", ) selector_needles = ( "html", "body", "#root", "tui-scrollbar", "table-scroll", "segmented-control__label", "docs-page", "docs-shell", "docs-content-layout", "docs-toc", "admin", "an-page", "an-resource", "an-data", "an-panel", "an-playground", "markdown-renderer", "meta", "select", ) return any(needle in body for needle in owner_needles) or any( needle in selector for needle in selector_needles ) def check_debug_output() -> None: for path in iter_frontend_src_files(): text = path.read_text(encoding="utf-8") rel = path.relative_to(root) for line_no, line in enumerate(text.splitlines(), 1): if "console.log(" in line or re.search(r"\bdebugger\b", line): fail(f"{rel}:{line_no}: remove console.log/debugger from frontend source") if "console." in line and "token" in line.lower(): fail(f"{rel}:{line_no}: console output must not include token material") def jsx_line_number(text: str, index: int) -> int: return text.count("\n", 0, index) + 1 def iter_opening_tags(text: str, tag_name: str) -> list[tuple[int, str]]: tags: list[tuple[int, str]] = [] needle = f"<{tag_name}" index = 0 while True: start = text.find(needle, index) if start == -1: return tags next_char_index = start + len(needle) if next_char_index < len(text) and (text[next_char_index].isalnum() or text[next_char_index] in "_-"): index = next_char_index continue quote: str | None = None brace_depth = 0 cursor = next_char_index while cursor < len(text): char = text[cursor] if quote: if char == "\\": cursor += 2 continue if char == quote: quote = None cursor += 1 continue if char in ('"', "'", "`"): quote = char elif char == "{": brace_depth += 1 elif char == "}": brace_depth = max(0, brace_depth - 1) elif char == ">" and brace_depth == 0: tags.append((start, text[start:cursor + 1])) index = cursor + 1 break cursor += 1 else: return tags def check_native_button_safety() -> None: for path in (root / "frontend/src").rglob("*.tsx"): text = path.read_text(encoding="utf-8") rel = path.relative_to(root) for start, tag in iter_opening_tags(text, "button"): line_no = jsx_line_number(text, start) if not re.search(r"\btype\s*=", tag): fail(f"{rel}:{line_no}: native