#!/bin/bash # Source-tree hygiene guard. # # Blocks two failure modes that an AI coding session previously introduced in # src/wallet/ (the lite-wallet "_plan"/"_batch" churn): pathologically long # filenames (which also break the Windows MAX_PATH 260-char limit during the # cross-build) and the runaway "receipt/custody/handoff/stewardship" naming # explosion where each session wrapped the previous artifact in one more layer. # # Usage: # scripts/check-source-hygiene.sh # check working-tree src/ # scripts/check-source-hygiene.sh --staged # check staged files (pre-commit) # # Install as a git pre-commit hook: # ln -sf ../../scripts/check-source-hygiene.sh .git/hooks/pre-commit # # (the hook invokes it with --staged automatically when named pre-commit) set -euo pipefail MAX_LEN=80 # Naming-explosion tokens. Two or more chained in one basename is the smell. CHURN_RE='receipt|custody|handoff|stewardship|promotion_activation|acceptance_confirmation|archive_handoff|post_closure' mode="${1:-}" if [[ "$mode" == "--staged" || "$(basename "$0")" == "pre-commit" ]]; then mapfile -t files < <(git diff --cached --name-only --diff-filter=AR | grep -E '\.(cpp|h|hpp|cc)$' || true) else mapfile -t files < <(git ls-files 'src/**/*.cpp' 'src/**/*.h' 2>/dev/null; \ find src -type f \( -name '*.cpp' -o -name '*.h' \) 2>/dev/null) # de-dup mapfile -t files < <(printf '%s\n' "${files[@]}" | sort -u) fi fail=0 for f in "${files[@]}"; do [[ -z "$f" ]] && continue base="$(basename "$f")" len=${#base} if (( len > MAX_LEN )); then echo "✗ filename too long ($len > $MAX_LEN chars): $f" >&2 fail=1 fi # count distinct churn tokens in the basename ( || true: grep exits 1 on no match) n=$(printf '%s' "$base" | grep -oE "$CHURN_RE" | sort -u | wc -l || true) if (( n >= 2 )); then echo "✗ runaway naming pattern ($n churn tokens) — refactor in place, don't add a layer: $f" >&2 fail=1 fi done # ── Content guards (UX-hygiene threads) ────────────────────────────────────── # Low-false-positive regression checks for debt the UX audit cleared, so it can't # silently regrow. Each is deliberately narrow; widen only with an allowlist. for f in "${files[@]}"; do [[ "$f" == *.cpp ]] || continue [[ -f "$f" ]] || continue # app_sweep.cpp is the dev screenshot-sweep harness: its fixture strings are never shipped UI. [[ "$(basename "$f")" == "app_sweep.cpp" ]] && continue # (1) Tactile button sized with a raw multi-digit pixel literal that ignores HiDPI. # Real sizes multiply by ui::Layout::dpiScale() / dp / hScale, or come from schema. if hits=$(grep -nE 'Tactile(Small)?Button\([^;]*ImVec2\([1-9][0-9]+(\.[0-9]+f?)?,' "$f" \ | grep -vE 'dpiScale|\* *dp\b|\* *hdpi|hScale|schema|Scale\('); then while IFS= read -r line; do echo "✗ unscaled button size (multiply the px by ui::Layout::dpiScale()): $f:$line" >&2 fail=1 done <<< "$hits" fi # (2) User-visible migrate/sweep status assigned a bare English literal instead of TR(...). # "__SENTINEL__" comparisons and empty strings are fine. if hits=$(grep -nE '(sweep_status_|seed_migration_status_)[[:space:]]*=[[:space:]]*"[A-Za-z]' "$f" \ | grep -vE 'TR\(|"__'); then while IFS= read -r line; do echo "✗ untranslated status string (route through TR(...)): $f:$line" >&2 fail=1 done <<< "$hits" fi done if (( fail )); then echo "" >&2 echo "Source hygiene check failed. See docs in scripts/check-source-hygiene.sh." >&2 exit 1 fi echo "source hygiene OK (${#files[@]} files checked)"