learning_ai_notes/scripts/ui-drift-ratchet.sh
saravanakumardb1 78433b0e45 feat(ci): one-way UI drift ratchet to prevent regressions
UI8 deferred deleting the legacy global classes (.surface-card,
.surface-muted, .input-shell, .badge) because 69+ call sites in UI6/UI7
territory (dashboard, search, workspaces, notes detail, chat, palace)
still depend on them. Removing the globals before those screens migrate
would visually break the app.

Instead, ship a one-way ratchet that solves the actually-important
problem: prevent NEW legacy usage from creeping in while existing
sites get migrated.

- scripts/ui-drift-ratchet.sh — reads scripts/ui-drift-baseline.json
  and FAILS if any of the four UI drift categories regress above the
  tracked baseline. Pure bash, no jq required, works with grep or
  ripgrep. Uses the same patterns as scripts/ui-drift-audit.sh.
- scripts/ui-drift-baseline.json — checked-in baseline captured today:
  raw controls 38, legacy classes 92, hardcoded colors 0, direct imports 0.
- package.json — adds pnpm run audit:ui:ratchet and
  audit:ui:ratchet:update scripts.
- .github/workflows/ci.yml release-guards job — runs the ratchet as a
  required step plus the existing audit in report mode.
- docs/UI_UX_PLATFORM_CORE_ROADMAP.md — marks the CI-guard checklist
  item complete, documents the path to fully strict mode (drive
  baseline to zero, then delete globals.css legacy classes, then flip
  audit:ui:strict from advisory to required).

Verified:
- Ratchet at baseline: exits 0
- Synthetic regression (added a file with surface-card + raw <input>):
  ratchet correctly exits 1, reporting +1 in each affected category
- pnpm run verify: backend 380/380, web 96/96, mobile 97/97 (no
  behavior change)
2026-05-23 00:13:50 -07:00

129 lines
4.8 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# UI drift ratchet — a one-way gate that prevents UI legacy patterns from
# growing while UI5UI8 migrations are still in flight.
#
# How it works:
# - Reads tracked baseline counts from scripts/ui-drift-baseline.json
# - Counts current matches per category by re-using the same patterns
# as scripts/ui-drift-audit.sh
# - FAILS if any category COUNT is GREATER than its baseline
# - PASSES (and reports the delta) if counts equal or fall below baseline
#
# Usage:
# ./scripts/ui-drift-ratchet.sh # check against baseline (CI mode)
# ./scripts/ui-drift-ratchet.sh --update # rewrite baseline to current counts
# # (commit the result with a UI
# # migration that lowered them)
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT"
BASELINE_FILE="scripts/ui-drift-baseline.json"
if [[ ! -f "$BASELINE_FILE" ]]; then
echo "FAIL: missing $BASELINE_FILE" >&2
exit 2
fi
MODE="check"
if [[ "${1:-}" == "--update" ]]; then
MODE="update"
fi
# Detect ripgrep availability with a grep fallback (same logic as ui-drift-audit.sh).
if command -v rg >/dev/null 2>&1; then
SEARCH_BACKEND="rg"
else
SEARCH_BACKEND="grep"
fi
count_matches() {
local pattern="$1"
shift
local n
if [[ "$SEARCH_BACKEND" == "rg" ]]; then
n=$(rg -n "$pattern" "$@" --glob '!**/*.test.*' 2>/dev/null | wc -l | tr -d ' ')
else
n=$(grep -rnE "$pattern" "$@" \
--include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" \
--exclude="*.test.*" --exclude="*.spec.*" \
--exclude-dir=node_modules --exclude-dir=.next 2>/dev/null | wc -l | tr -d ' ')
fi
echo "$n"
}
# Read baseline values. Avoid jq dependency by using grep + sed.
read_baseline() {
local key="$1"
grep -E "\"$key\"" "$BASELINE_FILE" | head -1 | sed -E 's/.*: *([0-9]+).*/\1/'
}
BASE_RAW=$(read_baseline raw_interactive_controls)
BASE_LEGACY=$(read_baseline legacy_global_surface_classes)
BASE_COLORS=$(read_baseline hardcoded_color_literals)
BASE_IMPORTS=$(read_baseline direct_bytelyst_ui_imports_outside_adapter)
CUR_RAW=$(count_matches '<button|<input|<textarea|<select' web/src/app web/src/components)
CUR_LEGACY=$(count_matches 'className="[^"]*(badge|surface-card|surface-muted|input-shell)' web/src/app web/src/components)
CUR_COLORS=$(count_matches '#[0-9a-fA-F]{3,8}|rgba?\(' web/src/app web/src/components)
# For the adapter exclusion, filter out the adapter file post-hoc since we
# want a stable raw count.
CUR_IMPORTS_RAW=$(count_matches 'from "@bytelyst/ui"|from '\''@bytelyst/ui'\''' web/src/app web/src/components)
# Adapter file is allowed to import @bytelyst/ui; subtract one line per
# import line in Primitives.tsx so the ratchet doesn't drift just because
# the adapter grows its re-export list.
ADAPTER_LINES=$(grep -cE 'from "@bytelyst/ui"|from '\''@bytelyst/ui'\''' web/src/components/ui/Primitives.tsx 2>/dev/null || echo 0)
CUR_IMPORTS=$((CUR_IMPORTS_RAW - ADAPTER_LINES))
if [[ "$CUR_IMPORTS" -lt 0 ]]; then CUR_IMPORTS=0; fi
if [[ "$MODE" == "update" ]]; then
cat >"$BASELINE_FILE" <<JSON
{
"//": "Baseline UI drift counts. Updated $(date -u +%Y-%m-%dT%H:%M:%SZ) by scripts/ui-drift-ratchet.sh --update. Commit alongside the migration that lowered the counts.",
"raw_interactive_controls": $CUR_RAW,
"legacy_global_surface_classes": $CUR_LEGACY,
"hardcoded_color_literals": $CUR_COLORS,
"direct_bytelyst_ui_imports_outside_adapter": $CUR_IMPORTS
}
JSON
echo "Wrote new baseline to $BASELINE_FILE"
cat "$BASELINE_FILE"
exit 0
fi
# Compare and report.
failures=0
report() {
local title="$1"
local cur="$2"
local base="$3"
if [[ "$cur" -gt "$base" ]]; then
printf " ✗ %-50s %4d (baseline %d, +%d) — REGRESSION\n" "$title" "$cur" "$base" "$((cur - base))"
failures=$((failures + 1))
elif [[ "$cur" -lt "$base" ]]; then
printf " ↓ %-50s %4d (baseline %d, -%d) — progress, run --update\n" "$title" "$cur" "$base" "$((base - cur))"
else
printf " = %-50s %4d (at baseline)\n" "$title" "$cur"
fi
}
echo "UI drift ratchet (baseline: $BASELINE_FILE)"
report "raw interactive controls" "$CUR_RAW" "$BASE_RAW"
report "legacy global surface classes" "$CUR_LEGACY" "$BASE_LEGACY"
report "hardcoded color literals" "$CUR_COLORS" "$BASE_COLORS"
report "direct @bytelyst/ui imports outside adapter" "$CUR_IMPORTS" "$BASE_IMPORTS"
if [[ "$failures" -gt 0 ]]; then
echo
echo "FAIL: $failures category/categories regressed above baseline." >&2
echo " Migrate the new call site(s) to @bytelyst/ui primitives via" >&2
echo " @/components/ui/Primitives, or — if migration lowered other" >&2
echo " counts — run 'pnpm run audit:ui:ratchet:update' and commit." >&2
exit 1
fi
echo
echo "OK — no UI drift regressions above baseline."