76 lines
2.7 KiB
Bash
Executable File
76 lines
2.7 KiB
Bash
Executable File
#!/usr/bin/env sh
|
|
set -eu
|
|
|
|
MODE="${1:-report}"
|
|
ROOT_DIR="$(CDPATH= cd "$(dirname "$0")/.." && pwd -P)"
|
|
WEB_SRC="${ROOT_DIR}/web/src"
|
|
|
|
if [ ! -d "${WEB_SRC}" ]; then
|
|
echo "error: web source directory not found: ${WEB_SRC}" >&2
|
|
exit 2
|
|
fi
|
|
|
|
python3 - "${WEB_SRC}" "${MODE}" <<'PY'
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
web_src = pathlib.Path(sys.argv[1])
|
|
mode = sys.argv[2]
|
|
extensions = {".ts", ".tsx", ".css"}
|
|
excluded_suffixes = (".test.ts", ".test.tsx", ".dom.test.tsx")
|
|
excluded_parts = {"test", "assets"}
|
|
|
|
patterns = [
|
|
("raw interactive controls outside approved primitives", "Raw Interactive Controls", re.compile(r"<(button|input|textarea|select)(\s|>)"), True),
|
|
("legacy global surface classes", "Legacy Global Surface Classes", re.compile(r"\b(surface-card|surface-muted|badge|input-shell)\b"), False),
|
|
("hardcoded color literals", "Hardcoded Color Literals", re.compile(r"(#[0-9a-f]{3,8}\b|rgba?\()", re.IGNORECASE), False),
|
|
("direct @bytelyst/ui imports outside product adapter", "Direct Common UI Imports Outside Adapter", re.compile(r"@bytelyst/ui"), True),
|
|
]
|
|
|
|
def included(path: pathlib.Path) -> bool:
|
|
if path.suffix not in extensions:
|
|
return False
|
|
name = path.name
|
|
if name.endswith(excluded_suffixes):
|
|
return False
|
|
rel_parts = set(path.relative_to(web_src).parts)
|
|
return rel_parts.isdisjoint(excluded_parts)
|
|
|
|
def approved_ui(path: pathlib.Path) -> bool:
|
|
return "components" in path.parts and "ui" in path.parts
|
|
|
|
def collect(pattern: re.Pattern[str], exclude_approved_ui: bool) -> list[tuple[str, int, str]]:
|
|
matches = []
|
|
for path in sorted(web_src.rglob("*")):
|
|
if not path.is_file() or not included(path):
|
|
continue
|
|
if exclude_approved_ui and approved_ui(path):
|
|
continue
|
|
try:
|
|
lines = path.read_text(encoding="utf-8").splitlines()
|
|
except UnicodeDecodeError:
|
|
continue
|
|
rel = path.relative_to(web_src).as_posix()
|
|
for line_number, line in enumerate(lines, start=1):
|
|
if pattern.search(line):
|
|
matches.append((rel, line_number, line.rstrip()))
|
|
return matches
|
|
|
|
results = [(label, title, collect(pattern, exclude_approved_ui)) for label, title, pattern, exclude_approved_ui in patterns]
|
|
total = sum(len(matches) for _, _, matches in results)
|
|
|
|
print(f"UI drift audit mode={mode}")
|
|
for label, _, matches in results:
|
|
print(f"{label}: {len(matches)}")
|
|
|
|
for _, title, matches in results:
|
|
print(f"\n== {title} ==")
|
|
for rel, line_number, line in matches:
|
|
print(f"{rel}:{line_number}:{line}")
|
|
|
|
if mode == "strict" and total > 0:
|
|
print(f"error: strict UI drift audit failed with {total} findings", file=sys.stderr)
|
|
sys.exit(1)
|
|
PY
|