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
131 lines
5.0 KiB
Bash
Executable File
131 lines
5.0 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)
|
||
# Same word-boundary pattern as scripts/ui-drift-audit.sh — must not match
|
||
# inside Tailwind arbitrary values like `bg-[color:var(--nl-surface-muted)]`.
|
||
CUR_LEGACY=$(count_matches 'className="(badge|surface-card|surface-muted|input-shell)[" ]|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."
|