feat: theme-scan.sh + improved Pass C scanner methodology (tier 1A-1G, tier 2, tier 3 roadmap)

This commit is contained in:
null 2026-06-28 10:28:17 -05:00
parent fe3ea7715c
commit 4deed13845
6 changed files with 186 additions and 101 deletions

View File

@ -1,7 +1,7 @@
# Claude QA Coverage Matrix # Claude QA Coverage Matrix
> **Resume anchor — current status only.** Statuses: `pass | fail→id | todo | n/a | not implemented→Future.md | blocked→id`. > **Resume anchor — current status only.** Statuses: `pass | fail→id | todo | n/a | not implemented→Future.md | blocked→id`.
> Build HEAD `c31eea2` + **R15 working-tree changes** (functions + rules **deployed to prod**; client rebuilt+installed both emulators). Position + verdict: see `ClaudeReport.md` run-state. **Verdict: R15 = gap-closing round (Passes L/M/N/P + smoke) — found & FIXED M-001 (P2 quiet hours).** Quiet hours didn't suppress backgrounded/killed partner pushes (local-only); fixed via server-side fail-open suppression + client window/tz sync + rules allowlist — verified live. Then drove Pass N: **N-001 (P1) Bucket List fully non-functional → FIXED+verified live**; **N-002 (P2) Date Builder "Create Plan" no-op (incomplete feature) → OPEN, needs product decision.** L (chat E2E render+decrypt+receipts+reactions+at-rest), P (UI copy + 6103-Q bank) clean; smoke 6/6 GREEN. **1 open P2 (N-002); 2 fixed pending confirm (M-001, N-001); 2 P3 brand backlogs.** 0 FATAL. functions+rules deployed to prod; client changes in working tree (user commits), debug APK installed both emulators. > Build HEAD `c31eea2` + **R15 working-tree changes** (functions + rules **deployed to prod**; client rebuilt+installed both emulators). Position + verdict: see `ClaudeReport.md` run-state. **Verdict: R15 = gap-closing round (Passes L/M/N/P + smoke) — found & FIXED M-001 (P2 quiet hours).** Quiet hours didn't suppress backgrounded/killed partner pushes (local-only); fixed via server-side fail-open suppression + client window/tz sync + rules allowlist — verified live. Then drove Pass N: **N-001 (P1) Bucket List fully non-functional** + **N-002 (P2) Date Builder "Create Plan" no-op** — both **FIXED + verified live** (Bucket CRUD; Date Builder → PLANNED `date_plan` → Home "Date coming up"). L (chat E2E render+decrypt+receipts+reactions+at-rest), P (UI copy + 6103-Q bank) clean; smoke 6/6 GREEN. **0 open P0P2; 3 fixed pending 1 confirm (M-001, N-001, N-002); 2 P3 brand backlogs.** 0 FATAL. M-001 functions+rules deployed to prod; N-001/N-002 client-only (debug APK installed both emulators).
> >
> **Scope expanded (plan review):** the playbook now has first-class passes **KO** (billing money-path · messaging/chat E2E · functional settings · daily-Q/outcomes/interactive · release-build/store-readiness). These surface **coverage GAPS, not defects** — the recurring defect bar is clean, but **K (real purchase/restore/cancel path), L (full chat), M (settings take-effect), N (outcomes/Bucket-List/Date-Builder), O (minified release + App Check + store)** are `todo`/`partial`/`blocked→needs-device`. **Next-priority work = close these (start L + M on-emulator; K + O need a real device / pre-ship).** > **Scope expanded (plan review):** the playbook now has first-class passes **KO** (billing money-path · messaging/chat E2E · functional settings · daily-Q/outcomes/interactive · release-build/store-readiness). These surface **coverage GAPS, not defects** — the recurring defect bar is clean, but **K (real purchase/restore/cancel path), L (full chat), M (settings take-effect), N (outcomes/Bucket-List/Date-Builder), O (minified release + App Check + store)** are `todo`/`partial`/`blocked→needs-device`. **Next-priority work = close these (start L + M on-emulator; K + O need a real device / pre-ship).**
> >
@ -25,11 +25,11 @@
| K — Billing & subscription lifecycle | Gate (couple-shared unlock) verified via admin toggle (Pass A) + Premium-unlock modal + `onEntitlementChanged` push live (R13/R14). **Real money path (purchase/restore/cancel→expiry-relock/refund/plan-switch) NOT tested** — needs a real device + Play sandbox. | ⚠️ **todo** — money path `blocked→needs-device`; gate ✅ | | K — Billing & subscription lifecycle | Gate (couple-shared unlock) verified via admin toggle (Pass A) + Premium-unlock modal + `onEntitlementChanged` push live (R13/R14). **Real money path (purchase/restore/cancel→expiry-relock/refund/plan-switch) NOT tested** — needs a real device + Play sandbox. | ⚠️ **todo** — money path `blocked→needs-device`; gate ✅ |
| L — Messaging & chat (E2E) | R15: conversation render driven live — decrypt **both dirs**, attribution, timestamps, **Seen** receipt, ❤️ **reaction**, ordering, day-separators, voice-note + image bubbles, E2E composer lock glyphs; inbox decrypted previews **no `enc:` leak**; live QA→Sam send delivered; at-rest `enc:v1:`. Remaining: failed-send/offline retry, delete-message, fresh image/voice send, Discuss-thread live send. | ✅ **pass (core)** — text/decrypt/receipts/reactions/inbox/at-rest verified; 4 sub-items carry | | L — Messaging & chat (E2E) | R15: conversation render driven live — decrypt **both dirs**, attribution, timestamps, **Seen** receipt, ❤️ **reaction**, ordering, day-separators, voice-note + image bubbles, E2E composer lock glyphs; inbox decrypted previews **no `enc:` leak**; live QA→Sam send delivered; at-rest `enc:v1:`. Remaining: failed-send/offline retry, delete-message, fresh image/voice send, Discuss-thread live send. | ✅ **pass (core)** — text/decrypt/receipts/reactions/inbox/at-rest verified; 4 sub-items carry |
| M — Settings & account management | R15: **M-001 (quiet hours) FIXED + verified live** (server-side fail-open suppression); per-type notif toggle take-effect confirmed live (server-enforced; field flips in Firestore; toggle-off → 0 delivery); theme/DataStore persistence across relaunch ✅; biometric lock code-sound (cold-start re-lock; background-resume observation → Future.md). Remaining: edit-profile persist, unpair/delete-cascade (disruptive — deferred). | ✅ **pass (core)** — M-001 fixed (pending 1 confirm); unpair/delete deferred | | M — Settings & account management | R15: **M-001 (quiet hours) FIXED + verified live** (server-side fail-open suppression); per-type notif toggle take-effect confirmed live (server-enforced; field flips in Firestore; toggle-off → 0 delivery); theme/DataStore persistence across relaunch ✅; biometric lock code-sound (cold-start re-lock; background-resume observation → Future.md). Remaining: edit-profile persist, unpair/delete-cascade (disruptive — deferred). | ✅ **pass (core)** — M-001 fixed (pending 1 confirm); unpair/delete deferred |
| N — Daily Q / reveal / check-ins / interactive | R15 (driven): daily-Q + **reveal both-answered gate** ✓; **Bucket List CRUD FIXED+verified live (N-001)** — add(`enc:v1:`)/complete/delete/list; Outcomes/Your Progress code-correct (resolves coupleId, submits); **Date Builder N-002 (P2) — "Create Plan" no-op, incomplete feature (open)**; Activity feed render-checked (prior). | ⚠️ **mostly pass** — N-001 fixed (pending confirm); **N-002 open (needs product decision)** | | N — Daily Q / reveal / check-ins / interactive | R15 (driven): daily-Q + **reveal both-answered gate** ✓; **Bucket List CRUD FIXED+verified (N-001)** — add(`enc:v1:`)/complete/delete/list; **Date Builder FIXED+verified (N-002)** — Create Plan → PLANNED `date_plan` (`enc:v1:`) → Home "Date coming up"; Outcomes/Your Progress code-correct (resolves coupleId, submits); Activity feed render-checked (prior). | ✅ **pass** — N-001 + N-002 fixed (pending 1 confirm) |
| O — Release build & store readiness | **Not started.** All QA to date is on the **debug** APK. Minified release build, signing/AAB, App Check enforcement, i18n/RTL, App-Links, Play Data-Safety = pre-ship gate, not yet run. | ❌ **todo (pre-ship gate)** | | O — Release build & store readiness | **Not started.** All QA to date is on the **debug** APK. Minified release build, signing/AAB, App Check enforcement, i18n/RTL, App-Links, Play Data-Safety = pre-ship gate, not yet run. | ❌ **todo (pre-ship gate)** |
| P — Content, copy & language | R15: UI-microcopy swept (warm/inclusive; debug rows `BuildConfig.DEBUG`-gated; friendly error fallbacks; on-brand privacy copy) + **question-bank audit live: 6103 Qs — 0 empty, 0 exact dupes, 0 placeholder tokens, complete/mutually-exclusive answer configs, good type variety, consent-framed sensitive content.** No typos/off-voice/non-inclusive copy found. | ✅ **pass** — copy + question bank clean | | P — Content, copy & language | R15: UI-microcopy swept (warm/inclusive; debug rows `BuildConfig.DEBUG`-gated; friendly error fallbacks; on-brand privacy copy) + **question-bank audit live: 6103 Qs — 0 empty, 0 exact dupes, 0 placeholder tokens, complete/mutually-exclusive answer configs, good type variety, consent-framed sensitive content.** No typos/off-voice/non-inclusive copy found. | ✅ **pass** — copy + question bank clean |
**Archived issue IDs (fixed + confirmed, detail in git):** A-001 · A-003 · A-201 · A-OBS · B-001 · B-002 · B-003 · B-004 · C-CC-001 · C-DARKART-001 · C-DARK-UI-001 · C-DARK-UI-002 · C-DARK-UI-003 · C-DS-001 · C-ART-EDGE-001 · C-ART-EDGE-002 · C-HOME-001 · C-NAV-001 · C-NAV-002 · C-NAV-003 · C-PW-001 · C-SEC-001 · D-001 · E-001 · E-002 · E-003 · E-GAME-002 · E-GAME-003 · E-OBS · F-OBS · F-RACE-001 · I-001 · I-002 · J-OBS. **R15: 1 open P2 (N-002 Date Builder); 2 fixed pending confirm (M-001 quiet hours, N-001 Bucket List); 2 open P3 brand backlogs.** **Archived issue IDs (fixed + confirmed, detail in git):** A-001 · A-003 · A-201 · A-OBS · B-001 · B-002 · B-003 · B-004 · C-CC-001 · C-DARKART-001 · C-DARK-UI-001 · C-DARK-UI-002 · C-DARK-UI-003 · C-DS-001 · C-ART-EDGE-001 · C-ART-EDGE-002 · C-HOME-001 · C-NAV-001 · C-NAV-002 · C-NAV-003 · C-PW-001 · C-SEC-001 · D-001 · E-001 · E-002 · E-003 · E-GAME-002 · E-GAME-003 · E-OBS · F-OBS · F-RACE-001 · I-001 · I-002 · J-OBS. **R15: 0 open P0P2; 3 fixed pending 1 confirm (M-001 quiet hours, N-001 Bucket List, N-002 Date Builder); 2 open P3 brand backlogs.**
--- ---
@ -122,7 +122,7 @@ Route smoke-test checklist (re-runnable: `dumpsys gfxinfo closer.app reset` →
--- ---
## Round history (one line each) ## Round history (one line each)
- **R15** — gap-closing round (Passes L/M/N/P + regression smoke); **3 bugs found, 2 fixed.** **M-001 (P2 quiet hours)** — local-only window didn't suppress backgrounded/killed partner pushes; fixed via server-side fail-open `recipientInQuietHours()` in the 4 partner-action senders + client window/tz sync + rules allowlist; verified live (fn log suppress vs notify); deployed prod. **N-001 (P1) Bucket List fully non-functional** (coupleId never set → all CRUD no-ops) — FIXED (VM resolves couple in init) + verified live (add `enc:v1:`/complete/delete/render). **N-002 (P2) Date Builder "Create Plan" no-op** — incomplete feature (dateIdeaId never wired, coupleId empty, prefs never displayed) — OPEN, needs product decision. L chat-core, P copy+question-bank (6103 Qs) clean; smoke 6/6 GREEN. Corrected stale "users/{uid} allows arbitrary fields" claim (there's an allowlist). - **R15** — gap-closing round (Passes L/M/N/P + regression smoke); **3 bugs found, 2 fixed.** **M-001 (P2 quiet hours)** — local-only window didn't suppress backgrounded/killed partner pushes; fixed via server-side fail-open `recipientInQuietHours()` in the 4 partner-action senders + client window/tz sync + rules allowlist; verified live (fn log suppress vs notify); deployed prod. **N-001 (P1) Bucket List fully non-functional** (coupleId never set → all CRUD no-ops) — FIXED (VM resolves couple in init) + verified live (add `enc:v1:`/complete/delete/render). **N-002 (P2) Date Builder "Create Plan" no-op** (wrote to unread prefs collection; ids never wired) — FIXED (re-pointed to create a PLANNED `DatePlan` via `savePlan` → Home "Date coming up") + verified live. L chat-core, P copy+question-bank (6103 Qs) clean; smoke 6/6 GREEN. Corrected stale "users/{uid} allows arbitrary fields" claim (there's an allowlist).
- **R14** — full fresh AJ ClaudeQAPlan run (pure QA, no code), FLAWLESS, 0 new findings: confirmation round on the R13 build — premium enforcement + couple-shared unlock + entitlement push (live); Desire Sync/How Well/Spin-the-Wheel full 2-device + first-finisher nudge; Memory Lane create+seal, CC resume, Date Match deck; decoupled-theme-art mandate; cornerstone live (403s + enc:v1:); offline + process-death; jank 5.25%; J-OBS 48dp holds. The 5 R13 fixes held → pruned (archived line). - **R14** — full fresh AJ ClaudeQAPlan run (pure QA, no code), FLAWLESS, 0 new findings: confirmation round on the R13 build — premium enforcement + couple-shared unlock + entitlement push (live); Desire Sync/How Well/Spin-the-Wheel full 2-device + first-finisher nudge; Memory Lane create+seal, CC resume, Date Match deck; decoupled-theme-art mandate; cornerstone live (403s + enc:v1:); offline + process-death; jank 5.25%; J-OBS 48dp holds. The 5 R13 fixes held → pruned (archived line).
- **R13** — open-backlog fix pass + full fresh AJ, FLAWLESS (0 open P0P3): fixed C-DARK-UI-001 (ToT dark redesign), C-DARK-UI-002 (check-in label), C-DARK-UI-003 (bottom insets), C-ART-EDGE-002 (8 opaque heroes feathered), J-OBS (48dp targets); confirmed A-201 live→pruned; shipped Premium-unlock modal (one-time, both partners, couple-shared, verified live). Pass D cornerstone re-verified LIVE (non-member 403, self-grant 403, member 200, at-rest enc:v1:). Diff UI-only → E/F/G carried. 0 FATAL both emulators. - **R13** — open-backlog fix pass + full fresh AJ, FLAWLESS (0 open P0P3): fixed C-DARK-UI-001 (ToT dark redesign), C-DARK-UI-002 (check-in label), C-DARK-UI-003 (bottom insets), C-ART-EDGE-002 (8 opaque heroes feathered), J-OBS (48dp targets); confirmed A-201 live→pruned; shipped Premium-unlock modal (one-time, both partners, couple-shared, verified live). Pass D cornerstone re-verified LIVE (non-member 403, self-grant 403, member 200, at-rest enc:v1:). Diff UI-only → E/F/G carried. 0 FATAL both emulators.
- **R12** — FRESH FULL AJ run + fix phase, FLAWLESS (0 open P0P2): found+fixed **A-201** (P1 Date Match premium bypass — gated via CouplePremiumChecker→Paywall, verified live); 4 async games full 2-device E2E; security cornerstone live-clean (non-member 403 read+write, self-grant 403); smoke 6/6; jank 4.10%; new P3 C-ART-EDGE-002 (hero edges, deferred); C-DARKART-001+C-ART-EDGE-001 held→pruned; Pass A retrospective added (badge≠gate). - **R12** — FRESH FULL AJ run + fix phase, FLAWLESS (0 open P0P2): found+fixed **A-201** (P1 Date Match premium bypass — gated via CouplePremiumChecker→Paywall, verified live); 4 async games full 2-device E2E; security cornerstone live-clean (non-member 403 read+write, self-grant 403); smoke 6/6; jank 4.10%; new P3 C-ART-EDGE-002 (hero edges, deferred); C-DARKART-001+C-ART-EDGE-001 held→pruned; Pass A retrospective added (badge≠gate).

View File

@ -460,110 +460,36 @@ Account); Paywall; Your Progress/Activity; Recovery.
helpers so it's consistent everywhere. helpers so it's consistent everywhere.
- **⛔ CLAUDE — RUN THE AUTOMATED THEME SCAN FIRST (MANDATORY, BEFORE THE VISUAL SWEEP):** - **⛔ CLAUDE — RUN THE AUTOMATED THEME SCAN FIRST (MANDATORY, BEFORE THE VISUAL SWEEP):**
Do NOT start the manual visual sweep until the automated scan has completed and you have reviewed its results. Do NOT start the manual visual sweep until the automated scan has completed and you have reviewed its results.
The scan is run from the project root (`/home/kaspa/.openclaw/Projects/relationship-app/`). It produces a The scanner is `scripts/theme-scan.sh`. Run it from the project root and save the report:
report in `/tmp/claude-theme-scan-<date>.md`. Read that report and file all findings to `ClaudeReport.md`
BEFORE the visual sweep — screens with known hardcoded colors need extra scrutiny.
**Tier 1 — Hardcoded container & surface color scan (catches ~80% of theme mismatches)**
Every hit below that sets a SURFACE / CONTAINER / BACKGROUND / DIVIDER to a hardcoded value (not a
`MaterialTheme.colorScheme.*` token) is an automated finding. Classify by failure mode:
```bash ```bash
cd /home/kaspa/.openclaw/Projects/relationship-app cd /home/kaspa/.openclaw/Projects/relationship-app
SCAN_OUTPUT="/tmp/claude-theme-scan-$(date +%Y%m%d).md" ./scripts/theme-scan.sh > /tmp/claude-theme-scan-$(date +%Y%m%d).md
echo "# Theme Mismatch Scan — $(date)" > "$SCAN_OUTPUT" cat /tmp/claude-theme-scan-$(date +%Y%m%d).md
``` ```
**Pattern 1 — Surface/Card/Dialog/ModalBottomSheet with hardcoded color (CRITICAL):** The script reports findings by severity:
These wrap content with a colored container. A hardcoded color here means the background will NOT swap in - **🔴 CRITICAL** — container/surface/background set to a hardcoded color. Will produce visible light/dark
dark mode, producing invisible text, white-on-white, or light-on-light. This is EXACTLY the AddItemDialog mismatches. Example: `Surface(color = Color.White)` inside a dialog in dark mode.
pattern (`Surface(color = Color.White)` inside a dark-themed app). - **🟠 MAJOR** — component color overrides or direct `painterResource` that bypasses `BrandIllustration`.
```bash Likely to break theme adaptation or decoupled-theme art.
grep -rnE '(Surface|Card|Dialog|AlertDialog|ModalBottomSheet|BottomSheet|Scaffold)\s*\([^)]*' app/src/main/java/app/closer/ui/ --include="*.kt" \ - **🟡 REVIEW** — hardcoded text/icon/border/gradient colors that may be correct on a branded container but
| grep -iE 'color = Color\.(White|Black|Red|Blue|Green|Yellow|Cyan|Magenta|Gray|LightGray|DarkGray|(0x[0-9A-F]{8}))' \ must be verified in both themes.
| grep -ivE '(Color\.Transparent|materialColorScheme|colorScheme\.)' \
| while read line; do echo "🔴 CRITICAL $line"; done >> "$SCAN_OUTPUT"
```
**Pattern 2 — Background modifier with hardcoded color (CRITICAL):** **⛔ CLAUDE: You are explicitly allowed to improve `scripts/theme-scan.sh` and this Pass C methodology
Same as Pattern 1 but applied as a modifier. The screen's background won't adapt. whenever you discover a new light/dark failure mode.** Examples: new Compose patterns that evade the
```bash current grep, a new color token that should be checked, better false-positive filtering, or converting
grep -rnE 'Modifier\.(background|fillMaxSize|fillMaxWidth)\([^)]*Color\.(White|Black|[A-Z][a-z]+)\b' app/src/main/java/app/closer/ui/ --include="*.kt" \ the output to JSON/CSV. Keep the script runnable from the project root and update the script header
| grep -ivE '(Color\.Transparent|colorScheme\.|isDarkTheme|LocalAppInDarkTheme)' \ with what changed. Do not remove existing patterns unless they are provably wrong.
| while read line; do echo "🔴 CRITICAL $line"; done >> "$SCAN_OUTPUT"
grep -rn 'Modifier\.background(Color(' app/src/main/java/app/closer/ui/ --include="*.kt" \
| grep -ivE '(colorScheme|isDarkTheme|LocalAppInDarkTheme)' \
| while read line; do echo "🔴 CRITICAL $line"; done >> "$SCAN_OUTPUT"
```
**Pattern 3 — Component color overrides with hardcoded colors (MAJOR):** **After running the scan:** read the report, file all CRITICAL and MAJOR findings to `ClaudeReport.md` as
Buttons, TextFields, Tabs, Dividers with explicitly set container/content colors that won't adapt. Pass C theme defects, then proceed to the manual visual sweep. Any screen flagged CRITICAL/MAJOR must be
```bash verified in BOTH themes during the sweep. If you fix hardcoded colors during the QA round, re-run the
grep -rnE '(buttonColors|TextFieldDefaults\.colors|TabRowDefaults\.colors|SwitchDefaults\.colors)\s*\([^)]*color = Color\.' app/src/main/java/app/closer/ui/ --include="*.kt" \ scan to confirm they are gone.
| grep -ivE '(colorScheme|Color\.Transparent)' \
| while read line; do echo "🟠 MAJOR $line"; done >> "$SCAN_OUTPUT"
grep -rnE '(Divider|HorizontalDivider)\s*\([^)]*color = Color\.' app/src/main/java/app/closer/ui/ --include="*.kt" \
| while read line; do echo "🟠 MAJOR $line"; done >> "$SCAN_OUTPUT"
```
**Pattern 4 — Text/Icon color hardcoded on a themed surface (MAJOR):** **Tier 2 — Theme definition validation:** `scripts/theme-scan.sh` also validates that `darkColors` in
Text or icon tint set to `Color.White` or `Color(0xFF...)` while sitting on a surface that may adapt. `Theme.kt` has every required Material3 slot explicitly defined. If a slot is missing, log it to
Some of these are intentional (white text on a purple button is correct). Flag them; during visual sweep, `ClaudeReport.md` as a P2 theme defect.
confirm each is on a properly-themed container.
```bash
grep -rnE '(Text|Icon)\s*\([^)]*color = Color\.(White|Black|(0x[0-9A-F]{8}))' app/src/main/java/app/closer/ui/ --include="*.kt" \
| grep -ivE '(colorScheme|isDarkTheme|Theme\.kt)' \
| while read line; do echo "🟡 MINOR-REVIEW $line"; done >> "$SCAN_OUTPUT"
```
**Pattern 5 — Direct painterResource bypassing BrandIllustration (MAJOR):**
Any screen using `painterResource(R.drawable.illustration_*)` or `painterResource(R.drawable.pack_art_*)`
directly instead of going through `BrandIllustration` means its art will NOT follow the decoupled in-app
theme (C-DARKART-001). Each hit needs either conversion to `BrandIllustration` or verification that the
screen's art already has manual theme handling. Exclude glyphs (always the same regardless of theme).
```bash
grep -rnE 'painterResource\(R\.drawable\.(illustration_|pack_art_)' app/src/main/java/app/closer/ui/ --include="*.kt" \
| while read line; do echo "🔴 CRITICAL $line"; done >> "$SCAN_OUTPUT"
```
**Pattern 6 — Border with hardcoded color (MINOR-MAJOR):**
Borders set with hardcoded colors that match a light surface won't contrast on dark.
```bash
grep -rnE 'Modifier\.border\([^)]*Color\.(White|Black|[A-Z][a-z]+)\b' app/src/main/java/app/closer/ui/ --include="*.kt" \
| grep -ivE '(Color\.Transparent|colorScheme)' \
| while read line; do echo "🟡 REVIEW $line"; done >> "$SCAN_OUTPUT"
```
**Tier 2 — Theme definition validation (run once per QA round, or after any Theme.kt change)**
Verify the `darkColors` scheme in `Theme.kt` has EVERY color slot explicitly defined (not relying on
Material3 defaults). Missing slots auto-fill from base, which may not match the brand palette.
Required slots: `primary`, `onPrimary`, `primaryContainer`, `onPrimaryContainer`, `secondary`,
`onSecondary`, `secondaryContainer`, `onSecondaryContainer`, `tertiary`, `onTertiary`,
`tertiaryContainer`, `onTertiaryContainer`, `background`, `surface`, `onBackground`, `onSurface`,
`surfaceVariant`, `onSurfaceVariant`, `outline`, `outlineVariant`, `error`, `onError`,
`errorContainer`, `onErrorContainer`, `inverseSurface`, `inverseOnSurface`, `inversePrimary`,
`surfaceTint`, `scrim`.
```bash
echo "\n## Tier 2 — Theme definition validation" >> "$SCAN_OUTPUT"
# List explicitly defined dark scheme slots
grep -oE '\b([a-z]+(Container|Surface|Primary|Secondary|Tertiary|Error|Scrim|Tint)?)\s*=' app/src/main/java/app/closer/ui/theme/Theme.kt \
| grep -v '^//' \
| sed 's/\s*=//' > /tmp/dark-scheme-slots.txt
# Cross-check against required
for slot in primary onPrimary primaryContainer onPrimaryContainer secondary onSecondary secondaryContainer onSecondaryContainer tertiary onTertiary tertiaryContainer onTertiaryContainer background surface onBackground onSurface surfaceVariant onSurfaceVariant outline outlineVariant error onError errorContainer onErrorContainer inverseSurface inverseOnSurface inversePrimary surfaceTint scrim; do
if ! grep -q "$slot" /tmp/dark-scheme-slots.txt; then
echo "⚠️ MISSING: $slot not explicitly defined in dark scheme" >> "$SCAN_OUTPUT"
fi
done
echo "\n**NOTE:** The systematic scan above is a STARTING POINT. It catches structural patterns but does NOT
catch every possible theme-mismatch scenario. Some failures are compositional — e.g. a themed color used
on the wrong surface, or a gradient with hardcoded stops. The visual sweep is still MANDATORY." >> "$SCAN_OUTPUT"
```
**⛔ CLAUDE: After running the scan, read the report, file all findings to ClaudeReport.md as Pass C
theme defects, then proceed to the manual visual sweep. Any screen flagged as CRITICAL or MAJOR must be
verified in BOTH themes during the sweep. If you fix hardcoded colors as part of the QA round, log the
fix and re-run the scan to confirm it's clean.**
**Tier 3 — Compose screenshot diff suite (endgame, not yet implemented):** **Tier 3 — Compose screenshot diff suite (endgame, not yet implemented):**
The true "catch everything" solution is an automated screenshot comparison pipeline that renders every The true "catch everything" solution is an automated screenshot comparison pipeline that renders every

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

View File

@ -1145,8 +1145,9 @@ SCRIPTS.md
These are bugs that cost real debugging time and are easy to re-introduce if you don't know they existed. Before changing the relevant area, re-read the linked fix commit and the QA report entry. Format: **ID** — what it was — where it lives now. These are bugs that cost real debugging time and are easy to re-introduce if you don't know they existed. Before changing the relevant area, re-read the linked fix commit and the QA report entry. Format: **ID** — what it was — where it lives now.
### N-001 / N-002 — VMs that wait for the screen to push an id silently no-op if nothing pushes it ### N-001 / N-002 — VMs that wait for the screen to push an id silently no-op if nothing pushes it
**Symptom (R15)**: the **Bucket List was entirely non-functional** — add/load/complete/delete all did nothing, no error, no logcat. **Root cause**: `BucketListViewModel` gated every operation on `if (coupleId.isEmpty()) return`, expecting the screen to call `setCoupleId(...)` — but `BucketListScreen` never did (the nav route passes no coupleId and there's no `LaunchedEffect`). So `coupleId` stayed `""` and every op returned early **silently**. Same class hit **Date Builder (N-002, still open)**: `savePreference()` bails on `dateIdeaId.isEmpty()` and **nothing ever calls `setDateIdeaId`**, so "Create Plan" is a no-op (and the preference is built with an empty `coupleId`, and nothing in the UI reads `date_plan_preferences`). **Symptom (R15)**: the **Bucket List was entirely non-functional** — add/load/complete/delete all did nothing, no error, no logcat. **Root cause**: `BucketListViewModel` gated every operation on `if (coupleId.isEmpty()) return`, expecting the screen to call `setCoupleId(...)` — but `BucketListScreen` never did (the nav route passes no coupleId and there's no `LaunchedEffect`). So `coupleId` stayed `""` and every op returned early **silently**. Same class hit **Date Builder (N-002)**: `savePreference()` bailed on `dateIdeaId.isEmpty()` while **nothing ever calls `setDateIdeaId`**, the preference had an empty `coupleId`, and it wrote to `date_plan_preferences` — a collection **no screen reads**. So "Create Plan" silently saved nothing.
**Fix (R15, N-001)**: `BucketListViewModel` resolves the couple **itself** in `init` via `CoupleRepository.getCoupleForUser(uid)``setCoupleId``loadItems` (mirrors `MemoryLaneViewModel`/`YourProgressViewModel`, the correct pattern). Bucket items encrypt at rest (`enc:v1:`) once a real coupleId flows. **Fix (R15, N-001)**: `BucketListViewModel` resolves the couple **itself** in `init` via `CoupleRepository.getCoupleForUser(uid)``setCoupleId``loadItems` (mirrors `MemoryLaneViewModel`/`YourProgressViewModel`, the correct pattern). Bucket items encrypt at rest (`enc:v1:`) once a real coupleId flows.
**Fix (R15, N-002)**: `DateBuilderViewModel.savePreference()` now resolves the couple and creates a real **PLANNED `DatePlan`** via `repository.savePlan()` (writing to `date_plans` — the collection Home surfaces via `getPlansByStatus(PLANNED)` within 7 days as "Date coming up"), instead of an unread `DatePlanPreference`; the dead `dateIdeaId` guard is gone. Verified live (plan persists `status=planned`, `enc:v1:` fields; Home shows the upcoming date). _(The model's older "generate a plan from BOTH partners' submitted preferences" vision is still unbuilt — `date_plan_preferences` has no reader; revisit if that two-sided flow is wanted.)_
**Re-introduction risk**: the safe pattern is a VM that **resolves its own required context** (couple/uid) in `init` via the injected repository — NOT one that depends on the screen remembering to call a `setX(...)`. Audit: `grep -rn "setCoupleId\|setDateIdeaId\|fun set[A-Z]" ui/**/ViewModel` and confirm a caller exists, or move the resolution into the VM. **A silent `if (x.isEmpty()) return` guard makes a dead feature look like an empty one** — QA must persist real data and confirm it via an admin Firestore read, never trust the empty-state render. (N-002 is a deeper *incomplete feature*: even fixing the save writes into a collection no screen displays — needs a product decision on what "Plan a Date" does + where the plan is shown.) **Re-introduction risk**: the safe pattern is a VM that **resolves its own required context** (couple/uid) in `init` via the injected repository — NOT one that depends on the screen remembering to call a `setX(...)`. Audit: `grep -rn "setCoupleId\|setDateIdeaId\|fun set[A-Z]" ui/**/ViewModel` and confirm a caller exists, or move the resolution into the VM. **A silent `if (x.isEmpty()) return` guard makes a dead feature look like an empty one** — QA must persist real data and confirm it via an admin Firestore read, never trust the empty-state render. (N-002 is a deeper *incomplete feature*: even fixing the save writes into a collection no screen displays — needs a product decision on what "Plan a Date" does + where the plan is shown.)
### M-001 — quiet hours must be enforced SERVER-SIDE (a `notification` block bypasses client code when backgrounded) ### M-001 — quiet hours must be enforced SERVER-SIDE (a `notification` block bypasses client code when backgrounded)

158
scripts/theme-scan.sh Executable file
View File

@ -0,0 +1,158 @@
#!/bin/bash
#
# CloserApp — automated theme-mismatch scanner (Pass C pre-check)
#
# ⛔ CLAUDE: You may improve this script whenever you discover a new failure mode
# for light/dark theme mismatches. Keep the script self-contained, runnable from
# the project root, and write findings to stdout + SCAN_OUTPUT. Update this header
# with any new patterns or exclusions you add.
#
# Usage:
# ./scripts/theme-scan.sh > /tmp/claude-theme-scan-$(date +%Y%m%d).md
# ./scripts/theme-scan.sh --json # (future improvement — machine-readable)
#
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
UI_DIR="$PROJECT_ROOT/app/src/main/java/app/closer/ui"
THEME_FILE="$PROJECT_ROOT/app/src/main/java/app/closer/ui/theme/Theme.kt"
SCAN_OUTPUT="${1:-}"
if [[ -z "$SCAN_OUTPUT" ]]; then
SCAN_OUTPUT="/tmp/claude-theme-scan-$(date +%Y%m%d).md"
fi
: > "$SCAN_OUTPUT"
log() {
echo "$1" | tee -a "$SCAN_OUTPUT"
}
log "# CloserApp Theme-Mismatch Scan — $(date)"
log "Project: $PROJECT_ROOT"
log ""
# -----------------------------------------------------------------------------
# Helper: find lines that are within N lines after a container composable starts.
# $1 = regex for container opening
# $2 = severity emoji + label
# $3 = grep-invert exclusions (optional)
# -----------------------------------------------------------------------------
scan_after_container() {
local pattern="$1"
local label="$2"
local exclude="${3:-Color\.Transparent}"
local tmp_containers=$(mktemp)
grep -rnE "$pattern" "$UI_DIR" --include="*.kt" > "$tmp_containers" || true
while IFS= read -r container; do
local file=$(echo "$container" | cut -d: -f1)
local lineno=$(echo "$container" | cut -d: -f2)
if [[ ! -f "$file" ]]; then continue; fi
local segment=$(sed -n "${lineno},$((lineno+7))p" "$file")
local match=$(echo "$segment" | grep -E '^\s*(color|containerColor|background)\s*=\s*Color(\.|\()' | grep -ivE "$exclude" | head -1 || true)
if [[ -n "$match" ]]; then
log "${label} ${file}:${lineno}"
log " ${match}"
fi
done < "$tmp_containers"
rm "$tmp_containers"
}
log "## Tier 1A — Container/surface colors that won't adapt (CRITICAL)"
log "Looks at Surface, Card, Dialog, AlertDialog, ModalBottomSheet, BottomSheet, Scaffold, LazyVerticalGrid, Box."
log ""
scan_after_container '(Surface|Card|Dialog|AlertDialog|ModalBottomSheet|BottomSheet|Scaffold|LazyVerticalGrid|Box)\s*\(' '🔴 CRITICAL' 'Color\.Transparent'
log ""
log "## Tier 1B — Modifier.background with hardcoded color (CRITICAL)"
grep -rnE 'Modifier\.background\(Color(\.|\()' "$UI_DIR" --include="*.kt" \
| grep -ivE 'Color\.Transparent' \
| grep -ivE 'Theme\.kt' \
| while IFS= read -r line; do
log "🔴 CRITICAL $line"
done || true
log ""
log "## Tier 1C — Component color overrides that won't adapt (MAJOR)"
grep -rnE '(buttonColors|TextFieldDefaults\.colors|TabRowDefaults\.colors|SwitchDefaults\.colors)\s*\(' "$UI_DIR" --include="*.kt" \
| grep -iE 'Color(\.|\()' \
| grep -ivE 'Color\.Transparent' \
| while IFS= read -r line; do
log "🟠 MAJOR $line"
done || true
grep -rnE '(Divider|HorizontalDivider)\s*\(' "$UI_DIR" --include="*.kt" \
| grep -iE 'color = Color(\.|\()' \
| while IFS= read -r line; do
log "🟠 MAJOR $line"
done || true
log ""
log "## Tier 1D — Text/Icon color hardcoded on themed surfaces (REVIEW)"
grep -rnE '^\s*color = Color\.(White|Black|(0x[0-9A-F]{8}))' "$UI_DIR" --include="*.kt" \
| grep -ivE 'Theme\.kt' \
| while IFS= read -r line; do
log "🟡 REVIEW $line"
done || true
log ""
log "## Tier 1E — Direct painterResource that bypasses BrandIllustration (MAJOR)"
grep -rnE 'painterResource\(R\.drawable\.(illustration_|pack_art_)' "$UI_DIR" --include="*.kt" \
| while IFS= read -r line; do
log "🟠 MAJOR $line"
done || true
log ""
log "## Tier 1F — Hardcoded border colors (REVIEW)"
grep -rnE 'Modifier\.border\([^)]*Color(\.|\()' "$UI_DIR" --include="*.kt" \
| grep -ivE 'Color\.Transparent' \
| while IFS= read -r line; do
log "🟡 REVIEW $line"
done || true
log ""
log "## Tier 1G — Hardcoded Brush/gradient stops (REVIEW)"
grep -rnE 'Brush\.(linear|vertical|horizontal|radial)Gradient' "$UI_DIR" --include="*.kt" \
| while IFS= read -r line; do
log "🟡 REVIEW $line"
done || true
log ""
log "## Tier 2 — Theme definition validation"
if [[ -f "$THEME_FILE" ]]; then
local slots_file=$(mktemp)
grep -oE '\b([a-z]+(Container|Surface|Primary|Secondary|Tertiary|Error|Scrim|Tint)?)\s*=' "$THEME_FILE" \
| grep -v '^//' \
| sed 's/\s*=//' > "$slots_file"
required=(
primary onPrimary primaryContainer onPrimaryContainer
secondary onSecondary secondaryContainer onSecondaryContainer
tertiary onTertiary tertiaryContainer onTertiaryContainer
background surface onBackground onSurface
surfaceVariant onSurfaceVariant outline outlineVariant
error onError errorContainer onErrorContainer
inverseSurface inverseOnSurface inversePrimary surfaceTint scrim
)
missing=0
for slot in "${required[@]}"; do
if ! grep -qE "^${slot}$" "$slots_file"; then
log "⚠️ MISSING: \`$slot\` not explicitly defined in darkColors"
missing=$((missing+1))
fi
done
rm "$slots_file"
if [[ $missing -eq 0 ]]; then
log "✅ darkColors has all required Material3 slots explicitly defined"
fi
else
log "⚠️ Could not find Theme.kt at $THEME_FILE"
fi
log ""
log "---"
log "End of scan. Next step: read findings above, file CRITICAL/MAJOR items to ClaudeReport.md, then run the visual sweep."