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)
129 lines
4.8 KiB
Bash
Executable File
129 lines
4.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# UI drift ratchet — a one-way gate that prevents UI legacy patterns from
|
||
# growing while UI5–UI8 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."
|