learning_ai_notes/scripts/ui-drift-audit.sh
saravanakumardb1 0c982de7e6 feat(web/ui8): remove legacy global classes + tighten audit regex + lock CI gate
UI8 closes the migration cycle started by UI0. The four legacy global
classes (.surface-card, .surface-muted, .badge, .input-shell) are
removed from web/src/app/globals.css and the CI ratchet now enforces
zero new occurrences across three of the four drift categories.

Changes:

1. Audit regex precision (scripts/ui-drift-audit.sh, scripts/ui-drift-ratchet.sh)

   The previous pattern 'className="[^"]*(badge|surface-card|surface-muted|input-shell)'
   matched the literal token anywhere inside className, which caused 21
   false positives against Tailwind arbitrary values like
   'bg-[color:var(--nl-surface-muted)]' where the legacy name appears
   inside a 'var(--nl-...)' reference.

   New pattern requires the legacy class to be a whole class token —
   either at the start of className, or preceded by a space, and
   followed by a space or closing quote. Result: 21 false positives
   eliminated; the ratchet now reports an honest 0 for the legacy
   category.

2. globals.css cleanup (web/src/app/globals.css)

   Removed .surface-card, .surface-muted, .badge, .input-shell rules.
   Only truly global utilities remain (typography, focus-visible,
   sr-only, skip-link, motion preferences, layout grids). A header
   comment documents that re-introductions should be solved at the
   call-site with a primitive, not by restoring the global rule.

3. Ratchet baseline (scripts/ui-drift-baseline.json)

   Final counts after UI5–UI8 across the session:
     raw interactive controls       14   (was 38 at start)
     legacy global surface classes  0    (was 92 at start)
     hardcoded color literals       0    (no change, was already 0)
     direct @bytelyst/ui imports    0    (no change, was already 0)

   The 14 remaining raw controls are intentional and tracked:
     NoteEditor toolbar buttons (10)
     ArtifactPanel hidden file input (1)
     search/page radio inputs (2)
     NoteVersionsPanel disclosure button (1)

4. CI gate (.github/workflows/ci.yml release-guards job)

   Documented that the ratchet is the canonical gate post-UI8: because
   legacy/colors/imports baselines are 0, any new occurrence in those
   three categories now fails CI. The strict-audit script is kept as
   a local diagnostic tool but not wired as a gate (would fail on the
   14 intentional raw controls).

5. Roadmap (docs/UI_UX_PLATFORM_CORE_ROADMAP.md)

   Marked UI5, UI6, UI7, UI8 all complete with per-phase commit hashes
   and explicit deliverables.

Cumulative migration impact (from initial baseline):
   raw interactive controls       38 → 14   (-24, -63%)
   legacy global surface classes  92 → 0    (-92, -100%)

Verified:
- pnpm run verify: backend 380/380, web 96/96, mobile 97/97
- bash scripts/ui-drift-ratchet.sh: all four categories at baseline
- bash scripts/ui-drift-audit.sh: only "Raw interactive controls"
  category has matches (intentional, tracked above)
- Live Docker stack at http://localhost:3050 still serves 200,
  backend health 200
2026-05-23 01:55:36 -07:00

97 lines
3.4 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))
# Match the legacy class only when it appears as its own class token
# (preceded by start-of-className, a space, or a quote, AND followed by
# a word boundary). This excludes Tailwind arbitrary-value references
# like `bg-[color:var(--nl-surface-muted)]` where the legacy name
# appears inside a `var(--nl-…)` reference.
report "Legacy global surface classes" 'className="(badge|surface-card|surface-muted|input-shell)[" ]|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."