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:
parent
30a30ceb0f
commit
78433b0e45
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
7
scripts/ui-drift-baseline.json
Normal file
7
scripts/ui-drift-baseline.json
Normal 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
128
scripts/ui-drift-ratchet.sh
Executable file
@ -0,0 +1,128 @@
|
|||||||
|
#!/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."
|
||||||
Loading…
Reference in New Issue
Block a user