learning_ai_notes/scripts/ui-drift-audit.sh
saravanakumardb1 aba7152097 fix(scripts): make ui-drift-audit work without ripgrep
The audit script silently passed on hosts without ripgrep installed
because 'rg -n ...' would fail, '|| true' swallowed the failure,
'matches' would be empty, and report() would print 'ok: no matches'.
This hid genuine UI drift from local 'pnpm run audit:ui' runs.

Changes:
- Detect ripgrep availability at startup and emit a stderr note when
  falling back.
- Add a grep-based fallback that translates rg '--glob !path' exclusions
  into 'grep --exclude=<basename>' so caller-side exclusions (e.g. the
  @bytelyst/ui adapter file at Primitives.tsx) still apply.
- Guard the optional 'extra_excludes' array expansion against 'set -u'
  when no exclusions are configured.

Result: on this host (no rg) the audit now correctly reports
2 categories with matches — raw interactive controls and legacy global
surface classes — instead of the false 'all green' it produced before.
'pnpm run audit:ui:strict' exits non-zero when matches remain, ready to
wire into CI once UI5–UI8 finish migrating the remaining call sites.
2026-05-22 23:51:47 -07:00

92 lines
3.0 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
STRICT=0
if [[ "${1:-}" == "--strict" ]]; then
STRICT=1
fi
ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT"
# Detect whether ripgrep is available. Fall back to grep so the audit cannot
# silently pass on hosts without rg (which was hiding regressions until now).
if command -v rg >/dev/null 2>&1; then
SEARCH_BACKEND="rg"
else
SEARCH_BACKEND="grep"
echo "[ui-drift-audit] note: ripgrep not found, falling back to grep" >&2
fi
# Build a search command that maps rg --glob exclusions to grep --exclude.
search() {
local pattern="$1"
shift
if [[ "$SEARCH_BACKEND" == "rg" ]]; then
rg -n "$pattern" "$@" --glob '!**/*.test.*' || true
else
# grep equivalent: -rnE with --exclude for test files. Translate
# rg-style --glob '!path' arguments into grep --exclude entries so the
# caller's exclusions (e.g. the @bytelyst/ui adapter file) still apply.
local targets=()
local extra_excludes=()
while [[ $# -gt 0 ]]; do
case "$1" in
--glob)
local glob_arg="${2:-}"
if [[ "$glob_arg" == "!"* ]]; then
local glob_path="${glob_arg#!}"
# grep --exclude matches against the basename, so reduce the
# exclusion to a filename pattern. If callers exclude paths
# like 'web/src/components/ui/Primitives.tsx', the basename
# 'Primitives.tsx' is the exclusion key.
extra_excludes+=("--exclude=$(basename "$glob_path")")
fi
shift 2
;;
*)
targets+=("$1")
shift
;;
esac
done
grep -rnE "$pattern" "${targets[@]}" \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" \
--exclude="*.test.*" --exclude="*.spec.*" \
--exclude-dir=node_modules --exclude-dir=.next \
${extra_excludes[@]+"${extra_excludes[@]}"} 2>/dev/null || true
fi
}
report() {
local title="$1"
local pattern="$2"
shift 2
local matches
matches="$(search "$pattern" "$@")"
echo "=== $title ==="
if [[ -z "$matches" ]]; then
echo "ok: no matches"
return 0
fi
echo "$matches"
echo
return 1
}
failures=0
report "Raw interactive controls" '<button|<input|<textarea|<select' web/src/app web/src/components || failures=$((failures + 1))
report "Legacy global surface classes" 'className="[^"]*(badge|surface-card|surface-muted|input-shell)' web/src/app web/src/components || failures=$((failures + 1))
report "Hardcoded color literals" '#[0-9a-fA-F]{3,8}|rgba?\(' web/src/app web/src/components || failures=$((failures + 1))
report "Direct @bytelyst/ui imports outside adapter" 'from "@bytelyst/ui"|from '\''@bytelyst/ui'\''' web/src/app web/src/components --glob '!web/src/components/ui/Primitives.tsx' || failures=$((failures + 1))
if [[ "$STRICT" == "1" && "$failures" -gt 0 ]]; then
echo "UI drift audit failed in strict mode with $failures category/categories containing matches." >&2
exit 1
fi
echo "UI drift audit completed with $failures non-empty category/categories."