Part 3 of the Home refactor. Adds the durable coverage the plan called for:
- HomeActionMapperTest: 12 JVM cases over the lifted pure mapper (refresh states,
withHomeActions pairing/daily/loading/error paths, toHomeLabel mc-drop, secondary
cap of 3, C-HOME-001 primary/pending dedup) — locks the extraction as faithful.
- HomeContentRenderSmokeTest: instrumented render net for Home (via the VM-free
PairedHomePreviewScreen), light + dark — catches 'composes fine, crashes on
first paint'. Runs on a THROWAWAY only (uninstalls app-under-test).
JVM test green; androidTest compiles. Fills the 'Home has no UI test' gap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Part 2b (Compose stack advantage). Marks the three verified-deeply-immutable
card models @Immutable so their single-instance params get structural-equality
skipping (upgrade over the K2 reference-equality strong-skipping default).
HomeAnswerStats/HomeUiState left unannotated (LocalAnswer/Question/Set transitive
types not fully audited — a false @Immutable promise would cause stale UI).
Rendered output identical; only recomposition frequency drops. compile green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Part 2b. Moves the ~305 lines of pure HomeUiState action-derivation extensions
(refreshDailyQuestionState/withHomeActions internal; toHomeAction/
buildDailyQuestionAction/buildPendingActions/has*/toHomeLabel private) out of
HomeViewModel verbatim (de-indented, byte-identical bodies). Verified pure — no
this@HomeViewModel/repo refs. VM call-sites unchanged (same-package extensions).
compile + full unit suite green. HomeViewModel now 562 lines (from 1030).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Part 2a. Moves the Home data classes/enums + computeDailyQuestionState (kept
@VisibleForTesting internal) + gameRouteFor (widened private->internal, its only
caller loadHome is same-package) out of HomeViewModel into HomeModels.kt.
Same package -> no consumer import churn (AppNavigation, DailyQuestionStateTest
unchanged). Behavior-identical. compile + ui.home unit tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 9/9 of the UI split. StreakMilestoneDialog -> components/ (kept internal;
ArtPreviewScreen call updated to the new package). MomentCueCard was dead (no
callers) — removed. Orphaned imports cleaned. compileDebugKotlin green.
HomeScreen.kt now ~620 lines (from 1921).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 1/9 of the HomeScreen decomposition. Moves the 6 shared style decls
(homeActionGlyph, HomeGlyphIcon, homePrimaryArt, HomeActionColors,
HomeActionTone.actionColors, HomePill) verbatim into ui/home/components/, made
public per the components/ visibility idiom. No behavior change. compileDebugKotlin green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adding Android Lint to CI immediately caught two real crash bugs invisible to
all emulator QA (the fixture emulators run API 34+):
- SettingsViewModel + YourProgressViewModel called LocalDate.ofInstant, which
was only added in API 34 — but minSdk is 26. On every device running Android
8-13 (the bulk of the install base) these throw NoSuchMethodError and crash
the Settings and Your Progress screens. Fixed with the API-26-safe equivalent
Instant.atZone(zone).toLocalDate() (same result).
The other two Lint errors were false positives (ProduceStateDoesNotAssignValue
on two EncryptedChatImage composables that DO assign value inside the producer —
the check misfires on a suspend/?.let RHS) — explicitly @Suppress'd with a note,
so Lint reaches 0 errors legitimately rather than via a blanket baseline.
CI (android-ci.yml) gains two jobs:
- android-lint: ./gradlew :app:lintDebug (fails on error-severity; 113 existing
warnings are non-fatal and left for a separate burndown).
- release-build: first-ever R8 gate — builds :app:bundleRelease with a throwaway
keystore + dummy RC_API_KEY (satisfies the release guards; AAB not distributed),
so the minify/shrink/sign toolchain can never silently rot. Verified locally:
bundleRelease SUCCEEDS today (95MB AAB). A green build proves the toolchain,
not runtime survival of reflectively-loaded classes (Tink) — that stays a
release-APK QA item.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- scripts/git-hooks/pre-push: the fixed hook (dirty-tree guards kept; the
self-defeating 'unpushed commits' blocker replaced with a behind-remote
fail-fast + an informational listing of what a push will publish). Hooks
don't sync on clone, so scripts/install-git-hooks.sh installs them; verified
idempotent against the live .git/hooks copy.
- Future.md: scheduledOutcomesReminder .limit(200) no-pagination scaling cap
recorded with the fix pattern (assignDailyQuestion's page loop); npm-audit
entry re-verified 2026-07-11 (still 9 moderate, transitive; no lockfile churn).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every point-in-time doc now carries an explicit status header so no one works
from a dead plan or stale claim:
- CopyMigration.md: CLOSED — migration stopped by owner decision after Slice 1;
Slices 2-4 cancelled (several target strings since rewritten by the Closer
Couples prose rename anyway).
- store-assets.md + NameChange.md: the support-URL 'couplesconnect.app' claim
was stale — ExternalLinks.kt already uses closer.app/support; remaining action
is Play-console listing fields only.
- internal-testing-checklist.md: SUPERSEDED by store-assets.md + release_guide;
also corrected its applicationId error (app.closer -> closer.app, the same
namespace swap behind the 4e807cb0 deep-link bug).
- private-mvp-checklist.md: SUPERSEDED by the ClaudeQAPlan system.
- UI_UPGRADE / standardization-review / ui-review / date-planning-roadmap /
IOS_E2EE_STATUS / iphone ARCHITECTURE_AUDIT: stamped historical/paused with
pointers to the live sources (Future.md, ClaudeBrandingReview, ERM).
NOT touched: DEVELOPMENT_LOG.md (verified alive — newest entry 2026-07-08,
prepend-ordered; earlier 'log is dead' read was wrong) and the Engineering
Reference Manual (another agent's Phase-4 review owns its drift).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ExternalLinks.MANAGE_SUBSCRIPTION pointed Play's subscription manager at
package=app.closer (the code namespace); the Play package is the applicationId
closer.app. In production the Manage button would miss the app's subscription
entry. Invisible to all QA so far because no real subscription has ever existed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Self-review of the wildcard authoring spec caught two real issues:
1. Count contradiction: the new Wildcard section said '+12 free' while the pack's
authoritative counts (QUESTION_SCHEMA.md daily table + metadata, and the guide's
Required Counts + Final Production Gate 'count validation') still said 500/75. A
content agent running count validation would fail, or worse delete weekday
questions to hit 500. Reconciled all four locations to the weekday pack (frozen
500) + wildcard add-on -> 511 total / 86 free / 425 premium, and folded the
wildcard set into the Final Production Gate review sampling.
2. Rotation math: the picker indexes epochDay % poolSize and wildcard days fall ~10
apart, so a pool size sharing a factor with 10 (12 is even) only surfaces a
fraction of the questions per year. Pool size must be coprime with 10 -> 11, not
12. Documented as a hard constraint so it isn't 'rounded to 10' later.
Also hardened the section: net-new (not a patch), answer_config option mirror,
unique snake_case option ids, and a QUESTION_SCHEMA.md cross-reference.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The client's DailyModeResolver promotes ~10% of days (dayOfYear % 10 == 3) to a
'Wildcard' theme, but there were zero wildcard questions and the server never
assigned one — so those days showed the Wildcard banner over a normal weekday
question. This adds the server half:
- pickDailyQuestionId now detects wildcard days (new pure, tested dayOfYearUtc /
isWildcardDay helpers mirroring the client cadence) and prefers the mode_wildcard
pool. Until that content is seeded it falls back to the day's WEEKDAY mode (not
the whole pool), so it's a safe no-op pre-content. +3 unit tests (fn 80 -> 83).
- Authoring spec for the missing content added to
seed/questions/DAILY_SINGLE_CHOICE_WEEKDAY_SYSTEM.md (## Wildcard Mode): 12 free
single_choice, day-agnostic voice, REQUIRED mode_wildcard tag, id scheme, schema,
and the post-authoring rollout (asset-db data-only insert + Firestore pointer seed
+ deploy).
Content authoring handed to another agent per the guide. Takes effect after the
wildcard rows land in app.db + Firestore and assignDailyQuestion is deployed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After an APK reinstall a device registers a fresh FCM token alongside stale
ones (~33h). The smoke harness grabbed docs[0] (unordered) and could hit a
stale token -> FCM 'Requested entity was not found' -> false smoke FAIL.
Now sort fcmTokens by updatedAt and try newest-first across all tokens,
skipping dead ones, mirroring the app's sendToUser + pruneTokens behavior.
Also file two R31 QA-env observations to Future.md (add the RevenueCat test_
key to local.properties so QA can drive the live paywall; FCM token churn note).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The webhook verified an Ed25519 signature in an X-Signature header, but RevenueCat
offers no public-key signing — it sends HMAC-SHA256 in X-RevenueCat-Webhook-Signature
(t=<ts>,v1=<hex>) computed over "<ts>.<rawBody>". As written, every real event would
have 401'd and premium would never sync for the partner.
- Rewrite verification to HMAC-SHA256 with a +/-5-min timestamp replay guard and a
constant-time compare; extract a pure verifyWebhookSignature() for unit testing.
- Rename secret REVENUECAT_SIGNING_KEY -> REVENUECAT_WEBHOOK_SECRET (it is an HMAC
secret, not an Ed25519 key). Never deployed, so no migration.
- Uncomment the export in index.ts (deploy still gated on seeding the secret).
- Add revenueCatWebhook.test.ts: valid / tampered / wrong-secret / missing / stale.
- Reconcile Future.md + Engineering_Reference_Manual.md to the real scheme.
Verified against the live account: the entitlement identifier is now closer_premium,
so events match entitlementLogic. Build + 80 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The paywall and subscription screens showed two different premium benefit lists (centralizing
into CloserCopy surfaced the drift). Unified to a single CloserCopy.premiumBenefits used by both.
Also an accuracy fix: dropped "Exportable memories" — it directly contradicts the app's own
privacy copy ("Closer does not currently offer a data export", strings.xml privacy_no_export_body),
i.e. a benefit the app explicitly does not provide. Remaining items are all verified real features
(QuestionComposer, Connection Challenges, Desire Sync, Memory Lane, date planning, answer history).
compileDebugKotlin clean; single source of truth for both surfaces going forward.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
server/ was a standalone Express webhook/health service (couples-connect-server, 17 files)
that duplicated the Firebase Cloud Functions — entitlement logic, FCM sends, an answer
listener, a webhook route — and was documented as "not client-facing… most teams will not
need it." It hadn't been touched since 2026-06-20 (pre-dating the whole v2 Functions
migration), had no deploy/CI wiring, and was a standing source of "which backend is real?"
confusion. Cloud Functions are the sole backend.
Removed the directory + all doc references so nothing dangles: README + Engineering Reference
Manual directory trees and the "Optional Express server" section; the iOS ARCHITECTURE_AUDIT
intro, its Section 8 (now a "removed" note), and the file-count table.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 18+ min-age error was triplicated verbatim (SignUpViewModel, CreateProfileViewModel ×2);
consolidated to CloserCopy.AgeGate.ageError(minAge). Also lifted the two disclaimer variants
(kept distinct: brief on sign-up, with-reason on the DOB step — not unified) and the duplicated
"Please enter your date of birth." prompt. 6 call sites now source from the catalog; copy is
byte-identical (no behavior change), compile clean.
Adds docs/CopyMigration.md — the migration plan, scoped (after a gap review) to the ~15-25
brand-voice/duplicated lines, NOT all ~281 UI literals: the bulk stays inline until the single
i18n → strings.xml pass, since double-migrating to a Kotlin catalog first is wasted work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 2026-07-09 Tier 1 dead-file sweep (commit 21392ec2) removed
8 files the manual still cited. Each was verified unreferenced by
symbol-level git-grep + a clean :app:compileDebugKotlin + unit-test
compile. This patch aligns the manual with the post-sweep source.
Five stale references fixed:
1. Android Repository layout (line 102): dropped NotificationHelper +
NotificationPermissionHelper from the core/notifications/ file
list. Both removed (dead duplicates of NotificationChannelSetup).
2. Android Repository layout (line 108): dropped the data/questions/
package from the layout. The directory is gone (QuestionJsonParser
was the only file; the path is now empty + dead).
3. Android Repository layout (line 118): dropped PartnerNotificationScheduler
from the top-level notifications/ package list. Removed as
superseded by PartnerNotificationManager.
4. 'Note on the manual's older description' paragraph (line 147):
rewritten to record the swept files (NotificationHelper,
NotificationPermissionHelper, PartnerNotificationScheduler, the
domain/model/ Entitlment.kt / InviteStatus.kt / QuestionSessionStatus.kt
trio, and the data/questions/ package) with one-line reasons
pulled from the 21392ec2 commit body, and to point QuestionDao at
data/local/QuestionDao.kt as the now-canonical question-content
source.
5. Logging section (line 1220): replaced the dead QuestionJsonParser
BuildConfig.DEBUG example pointer with the live
analytics/CompositeRetentionAnalytics.kt (one of the few current
Android files that still guards android.util.Log with
BuildConfig.DEBUG). The historical QJP pointer is preserved as a
one-line breadcrumb so the next reader knows why it changed.
6. R23-DQ-001 landmine (line 1355): the parenthetical said
'QuestionDetailViewModel (pack questions) is local-only and not
affected' - QuestionDetailViewModel is gone, pack questions are
now read directly by QuestionPackLibraryViewModel. Updated the
parenthetical + added a one-line breadcrumb pointing at the
21392ec2 retirement.
Anchor check: 30/30 internal anchors still resolve (Python GFM slug
checker).
Deliberately NOT in this commit:
- No mention of the CloserCopy catalog added - that's a documentation
addition, not a stale-reference fix, and belongs in its own batch
(the brand/ package is currently undocumented in the manual; a
future pass should add CloserBrandCopy + CloserCopy to the
Android Repository layout).
- No mention of the strings.xml purge (59e03369) - the manual
doesn't reference any specific string resources, so the purge is
invisible to the manual.