From 896691fee3b9f84e3bd24e25b656c378ee8fd52f Mon Sep 17 00:00:00 2001 From: null Date: Sun, 28 Jun 2026 11:30:21 -0500 Subject: [PATCH] tools: add wiring-scan.sh dead-feature / orphan-wiring scanner for Pass N --- scripts/wiring-scan.sh | 111 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100755 scripts/wiring-scan.sh diff --git a/scripts/wiring-scan.sh b/scripts/wiring-scan.sh new file mode 100755 index 00000000..db257af1 --- /dev/null +++ b/scripts/wiring-scan.sh @@ -0,0 +1,111 @@ +#!/bin/bash +# +# CloserApp — automated WIRING / dead-feature scanner (Pass N + discovery pre-check) +# +# Catches the "silent dead feature" class that QA found live (N-001 Bucket List, N-002 +# Date Builder): a feature looks like an empty/initial state but is actually non-functional +# because a required id is never wired, a setter is never called, or saved data is never read. +# +# ⛔ CLAUDE: This is a LIVING tool — IMPROVE IT whenever you discover a new dead-wiring / orphan +# failure mode. Add the new grep, keep the script self-contained + runnable from the project +# root, write findings to stdout + SCAN_OUTPUT, and update this header with what you added. +# Do not remove an existing check unless it is provably wrong (note why in the header). +# +# Usage: +# ./scripts/wiring-scan.sh > /tmp/claude-wiring-scan-$(date +%Y%m%d).md +# cat /tmp/claude-wiring-scan-$(date +%Y%m%d).md +# +# Tiers: +# 🔴 CRITICAL — a `fun setX(...)` in a *ViewModel.kt with ZERO callers. The screen/nav never +# pushes the value, so any op gated on it silently no-ops (N-001 class). +# 🟠 MAJOR — a repository/data-source READ method (`fun observe*/get*/load*`) with no caller +# in `ui/`. Data the app can write but never displays → orphan feature (N-002 class). +# 🟡 REVIEW — a `if (x.isEmpty()/== null) return` / `?: return` bail-guard inside a *ViewModel. +# Legitimate, BUT confirm something actually provides `x` — an un-provided guard is +# exactly how a feature goes silently dead. Verify each by persisting real data and +# reading it back from Firestore (admin), not by trusting the empty-state render. +# +# Findings are HINTS, not proofs — a flagged item can be intentional (e.g. a setter used only in a +# @Preview, or a read method genuinely pending UI). Confirm each against live behavior + ground truth. +# +set -uo pipefail + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SRC_DIR="$PROJECT_ROOT/app/src/main/java/app/closer" +UI_DIR="$SRC_DIR/ui" +SCAN_OUTPUT="${1:-/tmp/claude-wiring-scan-$(date +%Y%m%d).md}" +: > "$SCAN_OUTPUT" + +log() { echo "$1" | tee -a "$SCAN_OUTPUT"; } + +crit=0; major=0; review=0 + +log "# Wiring / dead-feature scan — $(date '+%Y-%m-%d %H:%M')" +log "" +log "> Hints, not proofs. Confirm each against live behavior + a Firestore admin read." +log "" + +# ── Tier 1: dead setters (CRITICAL) ─────────────────────────────────────────── +log "## 🔴 CRITICAL — ViewModel \`setX(...)\` with no caller (N-001 class)" +log "" +while IFS= read -r decl; do + file="${decl%%:*}" + name="$(echo "$decl" | grep -oE 'fun set[A-Z][A-Za-z0-9]*' | head -1 | sed 's/^fun //')" + [ -z "$name" ] && continue + # callers = references to `name(` or `::name` anywhere under SRC, minus the declaration itself + callers="$(grep -rEn "(\.|::| )${name}\(|::${name}\b" "$SRC_DIR" 2>/dev/null | grep -v "fun ${name}(" | grep -cv '^$')" + if [ "${callers:-0}" -eq 0 ]; then + log "- 🔴 \`${name}()\` — **no callers** — ${file#$PROJECT_ROOT/}" + crit=$((crit+1)) + fi +done < <(grep -rEn 'fun set[A-Z][A-Za-z0-9]*\(' "$SRC_DIR" --include=*ViewModel.kt 2>/dev/null) +[ "$crit" -eq 0 ] && log "- none ✅" +log "" + +# ── Tier 2: orphan readers (MAJOR) ──────────────────────────────────────────── +log "## 🟠 MAJOR — repository/data-source read method never called from \`ui/\` (N-002 class)" +log "" +# Scan REPOSITORY INTERFACES only (*Repository.kt, which the glob excludes *RepositoryImpl.kt +# + *DataSource.kt) — those are the read entry points a VM/screen would call. A repo read method +# with no ui/ caller means the app can fetch the data but no screen ever shows it (N-002). +while IFS= read -r decl; do + file="${decl%%:*}" + name="$(echo "$decl" | grep -oE 'fun (observe|get|load)[A-Z][A-Za-z0-9]*' | head -1 | sed 's/^fun //')" + [ -z "$name" ] && continue + ui_callers="$(grep -rEn "(\.|::| )${name}\(|::${name}\b" "$UI_DIR" 2>/dev/null | grep -cv '^$')" + if [ "${ui_callers:-0}" -eq 0 ]; then + log "- 🟠 \`${name}()\` — **no \`ui/\` caller** (written data may never be displayed) — ${file#$PROJECT_ROOT/}" + major=$((major+1)) + fi +done < <(grep -rEn 'fun (observe|get|load)[A-Z][A-Za-z0-9]*\(' "$SRC_DIR" --include=*Repository.kt 2>/dev/null) +[ "$major" -eq 0 ] && log "- none ✅" +log "" + +# ── Tier 3: silent bail-guards in ViewModels (REVIEW) ───────────────────────── +log "## 🟡 REVIEW — \`if (x.isEmpty()/==null) return\` / \`?: return\` bail-guards in ViewModels" +log "" +log "Each is a point where the feature silently does nothing if state was never initialized." +log "Confirm something provides the value (persist real data → read it back via admin)." +log "Auth-presence guards (\`currentUserId ?: return\`) are filtered out — they're never the wiring gap;" +log "the risky ones gate on a STATE field a \`setX\`/resolver is supposed to populate (e.g. coupleId)." +log "" +while IFS= read -r hit; do + log "- 🟡 ${hit#$PROJECT_ROOT/}" + review=$((review+1)) +done < <(grep -rEn 'if \([a-zA-Z0-9_.]+\.(isEmpty\(\)|isBlank\(\))\) return|[a-zA-Z0-9_.]+ \?: return\b' "$SRC_DIR" --include=*ViewModel.kt 2>/dev/null \ + | grep -viE 'currentUser|currentUserId|authRepository|FirebaseAuth|firstOrNull|\.find ?\{|getOrNull') +[ "$review" -eq 0 ] && log "- none" +log "" + +log "## Summary" +log "" +log "| Tier | Count |" +log "|---|---|" +log "| 🔴 CRITICAL (dead setters) | $crit |" +log "| 🟠 MAJOR (orphan readers) | $major |" +log "| 🟡 REVIEW (bail-guards) | $review |" +log "" +log "_Record these counts in ClaudeQACoverage.md under Pass N before driving the interactive features._" + +# Exit non-zero only on CRITICAL so CI/automation can gate on it. +[ "$crit" -gt 0 ] && exit 1 || exit 0