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)
This commit is contained in:
saravanakumardb1 2026-05-23 00:13:50 -07:00
parent 30a30ceb0f
commit 78433b0e45
5 changed files with 152 additions and 6 deletions

View File

@ -34,6 +34,12 @@ jobs:
- name: Run release guard audit - name: Run release guard audit
run: COMMON_PLAT="$GITHUB_WORKSPACE/learning_ai_common_plat" bash scripts/release-guard-audit.sh run: COMMON_PLAT="$GITHUB_WORKSPACE/learning_ai_common_plat" bash scripts/release-guard-audit.sh
- name: Run UI drift ratchet (one-way gate vs scripts/ui-drift-baseline.json)
run: bash scripts/ui-drift-ratchet.sh
- name: Run UI drift audit (report mode, informational)
run: bash scripts/ui-drift-audit.sh || true
backend: backend:
name: Backend — typecheck + test + build name: Backend — typecheck + test + build
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -190,13 +190,16 @@ The review surface is the highest-value pilot because it is operator-critical an
### Phase UI8 — Remove Legacy Globals ### Phase UI8 — Remove Legacy Globals
- [ ] Remove or greatly reduce global `.surface-card`, `.surface-muted`, `.badge`, and `.input-shell` after migrated screens no longer depend on them. - [x] **CI guard for new raw UI drift** — ratchet shipped May 23, 2026. `scripts/ui-drift-ratchet.sh` reads tracked counts from `scripts/ui-drift-baseline.json` and FAILS in CI (`release-guards` job) if any of the four categories regress above baseline:
- [ ] Keep only truly global layout/accessibility utilities such as focus-visible, `sr-only`, skip link, and motion preferences. - raw form controls (`<button|<input|<textarea|<select`) outside approved primitives
- [ ] Add CI guard for new raw UI drift: - raw `className="… (badge|surface-card|surface-muted|input-shell) …"` usage
- raw form controls outside approved primitives
- raw `.badge`/`.surface-*` usage
- hardcoded hex/rgb colors - hardcoded hex/rgb colors
- direct `@bytelyst/ui` imports outside the adapter unless explicitly allowed - direct `@bytelyst/ui` imports outside `web/src/components/ui/Primitives.tsx`
Lower the baseline by running `pnpm run audit:ui:ratchet:update` alongside the migration commit that reduced the count.
- [ ] Drive baseline counts to zero by completing UI5 remainder, UI6, and UI7. Current baseline (2026-05-23): raw controls 38, legacy classes 92, colors 0, direct imports 0.
- [ ] Remove or greatly reduce global `.surface-card`, `.surface-muted`, `.badge`, and `.input-shell` from `web/src/app/globals.css` once the ratchet baseline reaches zero for both raw-controls and legacy-classes categories.
- [ ] Keep only truly global layout/accessibility utilities such as focus-visible, `sr-only`, skip link, and motion preferences.
- [ ] Flip `audit:ui:strict` from advisory to required CI gate when baseline reaches zero.
- [ ] Verify full web test/typecheck/lint, E2E, and visual smoke. - [ ] Verify full web test/typecheck/lint, E2E, and visual smoke.
## Proposed Component Ownership Matrix ## Proposed Component Ownership Matrix

View File

@ -14,6 +14,8 @@
"audit:release-guards": "bash scripts/release-guard-audit.sh", "audit:release-guards": "bash scripts/release-guard-audit.sh",
"audit:ui": "bash scripts/ui-drift-audit.sh", "audit:ui": "bash scripts/ui-drift-audit.sh",
"audit:ui:strict": "bash scripts/ui-drift-audit.sh --strict", "audit:ui:strict": "bash scripts/ui-drift-audit.sh --strict",
"audit:ui:ratchet": "bash scripts/ui-drift-ratchet.sh",
"audit:ui:ratchet:update": "bash scripts/ui-drift-ratchet.sh --update",
"dependency:health": "bash scripts/dependency-health.sh", "dependency:health": "bash scripts/dependency-health.sh",
"verify": "pnpm run typecheck && pnpm run test && pnpm run build", "verify": "pnpm run typecheck && pnpm run test && pnpm run build",
"prepare": "husky" "prepare": "husky"

View File

@ -0,0 +1,7 @@
{
"//": "Baseline UI drift counts captured 2026-05-23 after UI5 settings + modals slice. ui-drift-ratchet.sh compares current counts against these and FAILS if any category EXCEEDS its baseline. To lower the baseline after migrating screens, run scripts/ui-drift-ratchet.sh --update.",
"raw_interactive_controls": 38,
"legacy_global_surface_classes": 92,
"hardcoded_color_literals": 0,
"direct_bytelyst_ui_imports_outside_adapter": 0
}

128
scripts/ui-drift-ratchet.sh Executable file
View File

@ -0,0 +1,128 @@
#!/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."