From 78433b0e45de8c6bcd6f8b69bbfc33ba8defbfdd Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sat, 23 May 2026 00:13:50 -0700 Subject: [PATCH] feat(ci): one-way UI drift ratchet to prevent regressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ): 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) --- .github/workflows/ci.yml | 6 ++ docs/UI_UX_PLATFORM_CORE_ROADMAP.md | 15 ++-- package.json | 2 + scripts/ui-drift-baseline.json | 7 ++ scripts/ui-drift-ratchet.sh | 128 ++++++++++++++++++++++++++++ 5 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 scripts/ui-drift-baseline.json create mode 100755 scripts/ui-drift-ratchet.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc5f51b..9b27ab6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,12 @@ jobs: - name: Run release guard audit 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: name: Backend — typecheck + test + build runs-on: ubuntu-latest diff --git a/docs/UI_UX_PLATFORM_CORE_ROADMAP.md b/docs/UI_UX_PLATFORM_CORE_ROADMAP.md index 47c2986..ff0314a 100644 --- a/docs/UI_UX_PLATFORM_CORE_ROADMAP.md +++ b/docs/UI_UX_PLATFORM_CORE_ROADMAP.md @@ -190,13 +190,16 @@ The review surface is the highest-value pilot because it is operator-critical an ### 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. -- [ ] Keep only truly global layout/accessibility utilities such as focus-visible, `sr-only`, skip link, and motion preferences. -- [ ] Add CI guard for new raw UI drift: - - raw form controls outside approved primitives - - raw `.badge`/`.surface-*` usage +- [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: + - raw form controls (`&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 '/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" <&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."