2026-06-28 11:30:21 -05:00
|
|
|
#!/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"; }
|
|
|
|
|
|
2026-06-30 00:38:06 -05:00
|
|
|
crit=0; major=0; review=0; notifdead=0
|
2026-06-28 11:30:21 -05:00
|
|
|
|
|
|
|
|
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 ""
|
|
|
|
|
|
2026-06-30 00:38:06 -05:00
|
|
|
# ── Tier 4: dead / unenforced notification settings (N-Notif class) ───────────
|
|
|
|
|
log "## 🔴 CRITICAL — notification pref written to users/{uid} but read by NO Cloud Function"
|
|
|
|
|
log ""
|
|
|
|
|
log "A toggle whose mirrored field no function reads is a DEAD setting — the Daily-Reminder /"
|
|
|
|
|
log "Streak-Reminder class this check was added for: the UI flips it, it reaches Firestore, and"
|
|
|
|
|
log "nothing server-side enforces it. Every \`notif*\` field written in FirestoreUserDataSource MUST"
|
|
|
|
|
log "be read by at least one sender under functions/src (gate \`=== false\` / \`!== false\`)."
|
|
|
|
|
log ""
|
|
|
|
|
FUNCTIONS_SRC="$PROJECT_ROOT/functions/src"
|
|
|
|
|
USER_DS="$SRC_DIR/data/remote/FirestoreUserDataSource.kt"
|
|
|
|
|
if [ -d "$FUNCTIONS_SRC" ] && [ -f "$USER_DS" ]; then
|
|
|
|
|
for field in $(grep -oE '"notif[A-Za-z0-9]+"' "$USER_DS" 2>/dev/null | tr -d '"' | sort -u); do
|
|
|
|
|
readers="$(grep -rEl "\\b${field}\\b" "$FUNCTIONS_SRC" 2>/dev/null | grep -c .)"
|
|
|
|
|
if [ "${readers:-0}" -eq 0 ]; then
|
|
|
|
|
log "- 🔴 \`${field}\` — mirrored to users/{uid} but **no functions/src reader** → dead toggle"
|
|
|
|
|
notifdead=$((notifdead+1))
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
[ "$notifdead" -eq 0 ] && log "- none ✅"
|
|
|
|
|
else
|
|
|
|
|
log "- (skipped — functions/src or FirestoreUserDataSource.kt not found)"
|
|
|
|
|
fi
|
|
|
|
|
log ""
|
|
|
|
|
|
2026-06-28 11:30:21 -05:00
|
|
|
log "## Summary"
|
|
|
|
|
log ""
|
|
|
|
|
log "| Tier | Count |"
|
|
|
|
|
log "|---|---|"
|
|
|
|
|
log "| 🔴 CRITICAL (dead setters) | $crit |"
|
|
|
|
|
log "| 🟠 MAJOR (orphan readers) | $major |"
|
|
|
|
|
log "| 🟡 REVIEW (bail-guards) | $review |"
|
2026-06-30 00:38:06 -05:00
|
|
|
log "| 🔴 dead notif settings | $notifdead |"
|
2026-06-28 11:30:21 -05:00
|
|
|
log ""
|
|
|
|
|
log "_Record these counts in ClaudeQACoverage.md under Pass N before driving the interactive features._"
|
|
|
|
|
|
2026-06-30 00:38:06 -05:00
|
|
|
# Exit non-zero on any CRITICAL class so CI/automation can gate on it.
|
|
|
|
|
{ [ "$crit" -gt 0 ] || [ "$notifdead" -gt 0 ]; } && exit 1 || exit 0
|