docs(manual): review pass — missing dirs + 3 notif prefs + B6c/B6d mix-up

After pushing the Phase 2 sync, evidence-first review against the live
repo caught:

- Repository layout (Android) was missing directories that have shipped
  since v0.2.1: widget/ (Glance Today, R29), core/firebase/, core/media/,
  data/backup/ (R24 E2EE backup + partner-assist), data/local/{converters,
  entity,mapper} subdirs, data/security/, domain/usecase/ (resolver +
  GameSessionManager + SoloAnswerMigrator), top-level notifications/
  package, and ui/{recap,messages,questions}/ + components/ subdirs.
- Repository layout (iOS) was missing Crypto/Resources/ (wordlist) and
  the Crypto/ design notes (SCHEMA_VERSION_DECISION.md, SPEC.md).
- User doc field list + per-collection enforcement were missing the 3
  R20 notif preferences: notifDailyReminder, notifStreakReminder,
  notifPromotional. Verified all 5 are mirrored by
  FirestoreUserDataSource.updateNotificationPrefs() and listed in the
  firestore.rules user-doc allowlist.
- One copy fix: 'The B6d split of onGameSessionUpdate' -> 'The B6c
  split' (B6c did the part-finished split; B6d was the logger finish).

Anchors verified clean (30/30). DEVELOPMENT_LOG.md is gitignored so
the local review notes stay on this box.
This commit is contained in:
null 2026-07-08 22:10:40 -05:00
parent 1242799001
commit d31b58d238
1 changed files with 20 additions and 7 deletions

View File

@ -90,17 +90,21 @@ A subscription is **per couple**, not per user. The Firestore rules for `users/{
app/src/main/java/app/closer/
├── MainActivity.kt
├── analytics/ # RetentionEvent + RetentionAnalytics (separate from core/analytics; used for product funnel)
├── widget/ # Glance Today home-screen widget (R29)
├── core/
│ ├── analytics/ # Firebase Analytics + Crashlytics wrappers
│ ├── billing/ # EntitlementChecker + FirestoreEntitlementChecker
│ ├── billing/ # EntitlementChecker + FirestoreEntitlementChecker + CouplePremiumChecker
│ ├── crash/ # CrashReporter abstraction
│ ├── feature/ # (reserved for feature flags; no directory exists today - no feature-flag code in repo)
│ ├── firebase/ # FirebaseInitializer (manual App Check / init path)
│ ├── media/ # MediaCompressor (image/video compression before encrypted upload)
│ ├── navigation/ # AppRoute constants, NavHost, ExternalLinks
│ └── notifications/ # AppMessagingService, NotificationHelper, NotificationPermissionHelper, QuietHours, TokenRegistrar
├── crypto/ # E2EE: Tink AEAD, BouncyCastle Argon2id, key stores
├── data/
│ ├── backup/ # BackupManager + BackupRestoreManager + RestoreManager (R24 E2EE backup + partner-assist restore)
│ ├── challenges/ # Connection Challenges data sources
│ ├── local/ # Room DAOs, DataStore, EncryptedSharedPreferences (RecoveryPhraseStore, SecurePreferencesFactory, SettingsDataStore)
│ ├── local/ # Room DAOs, DataStore, EncryptedSharedPreferences (RecoveryPhraseStore, SecurePreferencesFactory, SettingsDataStore); also converters/, entity/, mapper/ subdirs
│ ├── questions/ # Question pack data sources (`QuestionJsonParser`)
│ ├── remote/ # Firestore data sources, Cloud Functions callable wrappers
│ ├── repository/ # Repository implementations
@ -109,7 +113,9 @@ app/src/main/java/app/closer/
├── domain/
│ ├── model/ # Plain data classes
│ ├── repository/ # Repository interfaces
│ └── security/ # AuthRateLimiter, DeviceIntegrityChecker (interface for PlayIntegrityChecker)
│ ├── security/ # AuthRateLimiter, DeviceIntegrityChecker (interface for PlayIntegrityChecker)
│ └── usecase/ # Cross-VM use cases: DailyQuestionResolver, GameSessionManager, SoloAnswerMigrator
├── notifications/ # Notification surfaces: ActiveGameSessionMonitor, GamePromptController, PartnerNotificationManager, PartnerNotificationScheduler, QuietHoursManager, NotificationChannelSetup, NotificationRateLimiter, MessageBubbleController, ActiveThreadMonitor
└── ui/ # Compose screens + ViewModels
├── activity/ # Together / Activity screen
├── answers/ # Answer write/reveal/history
@ -124,13 +130,15 @@ app/src/main/java/app/closer/
├── home/ # Home dashboard + partner state
├── howwell/ # How Well Do You Know Me game
├── memorylane/ # Time capsules
├── messages/ # E2E chat (ConversationScreen, MessagesInboxScreen) + components/
├── onboarding/ # Onboarding screens
├── outcomes/ # 30/60/90 day check-ins
├── pairing/ # Invite create/accept/confirm/recovery
├── paywall/ # Subscription paywall
├── play/ # Play hub
├── questions/ # Daily question, packs, history
├── settings/ # All settings screens (account, privacy, security, subscription, ...)
├── questions/ # Daily question, packs, history + components/
├── recap/ # Weekly recap screen + share card
├── settings/ # All settings screens (account, privacy, security, subscription, …)
├── theme/ # CloserTheme
├── thisorthat/ # This or That game
└── wheel/ # Spin the wheel
@ -161,6 +169,8 @@ iphone/
├── Crypto/ # CryptoKit E2EE parity (R24+) - see iphone/Closer/Crypto/SPEC.md for the wire format
│ # AnswerCrypto.swift, CoupleEncryptionManager.swift, CoupleKeyStore.swift, DeviceKeyStatus.swift,
│ # FieldEncryptor.swift, Keybox.swift, RecoveryKeyManager.swift, SealedAnswer.swift, Wordlist.swift
│ # Resources/ (248-word recovery phrase wordlist, verbatim copy of Android's)
│ # SCHEMA_VERSION_DECISION.md, SPEC.md
├── Models/ # Codable Firestore + domain types
├── Services/FirestoreService.swift # Firestore + callable wrappers
├── Theme/CloserTheme.swift # Colors, typography, spacing
@ -248,7 +258,7 @@ functions/src/
**v2 migration (B0-B6d, 2026-07-08)**: every function except `onUserDelete` runs as **2nd gen** in `closer-app-22014` under a single shared set of global options (see `functions/src/options.ts`): `region: us-central1` (pinned to match `FirebaseFunctions.getInstance()` on Android; do not change without pinning the client), `maxInstances: 5` (runaway-bill guardrail + Cloud Run CPU-quota constraint), `cpu: 'gcf_gen1'` (gen1 fractional tiers, ~16 vCPU - chosen because the project's default regional Cloud Run CPU quota could not fit ~35 services at v2's default 1 vCPU/instance; the deploy container healthchecks failed with *"Quota exceeded for total allowable CPU per project per region"*), `concurrency: 1` (required when `cpu < 1`). At launch: request the Cloud Run CPU quota increase, then drop `cpu`/`concurrency` to restore 1 vCPU + concurrency 80. `onUserDelete` is intentionally v1 because gen 2 has no `auth.user().onDelete`; pin its runtime in lockstep with the v2 ones.
**Shared infrastructure (B6d)**: B6d collapsed ~10 copies of "read the user's tokens" and ~19 copies of "send to each token, log failures, prune the dead ones" into `notifications/push.ts` (`getUserTokens`, `sendPushToUser`, `batchResponseToResults`, `redactToken`) and `notifications/idempotency.ts` (`claimOnce` - gRPC `ALREADY_EXISTS` (6) dedupe, fail-OPEN on unexpected errors). `notifications/quietHours.ts` is the single server-side quiet-hour reader; `notifications/time.ts` is the shared date-key helper. Per-function log lines are routed through `log.ts` (re-exports `firebase-functions/logger` for v2 trace/execution context). The B6d split of `onGameSessionUpdate` + the 4 per-game part-finished triggers is a structural change - see [Game session push semantics](#game-session-push-semantics-idempotent-flag-claim).
**Shared infrastructure (B6d)**: B6d collapsed ~10 copies of "read the user's tokens" and ~19 copies of "send to each token, log failures, prune the dead ones" into `notifications/push.ts` (`getUserTokens`, `sendPushToUser`, `batchResponseToResults`, `redactToken`) and `notifications/idempotency.ts` (`claimOnce` - gRPC `ALREADY_EXISTS` (6) dedupe, fail-OPEN on unexpected errors). `notifications/quietHours.ts` is the single server-side quiet-hour reader; `notifications/time.ts` is the shared date-key helper. Per-function log lines are routed through `log.ts` (re-exports `firebase-functions/logger` for v2 trace/execution context). The B6c split of `onGameSessionUpdate` + the 4 per-game part-finished triggers is a structural change - see [Game session push semantics](#game-session-push-semantics-idempotent-flag-claim).
There is no `auth/` module. Authentication is handled entirely by the Firebase Auth client SDK; the Admin SDK is used in the `users/onUserDelete.ts` trigger and in `couples/acceptInviteCallable.ts` to read user docs.
@ -586,6 +596,9 @@ Thread questions follow the same sealed flow but use a different path:
platform: 'android' | 'ios' | null
notifPartnerAnswered: bool
notifChatMessage: bool
notifDailyReminder: bool # NEW (R20) - server-side gate for sendDailyQuestionProactiveReminder
notifStreakReminder: bool # NEW (R20) - server-side gate for sendStreakReminder (default true)
notifPromotional: bool # NEW (R20) - server-side opt-out for reengagement + challenge-day nudge
quietHoursEnabled: bool
quietHoursStartMinutes: int
quietHoursEndMinutes: int
@ -764,7 +777,7 @@ isUpdatingCoupleRhythm() only streakCount/lastAnsweredAt
### Per-collection enforcement
**`users/{uid}`** - owner can read/create/update their own doc. Root-level premium state is **not** stored here; the client-writable `plan` field is informational only, and authoritative premium state lives in the server-only `entitlements/premium` subdoc. `entitlements/`, `notification_queue/`, and `outcomes/` are server-only writes; `entitlements/` is also readable by the user's current couple partner (couple-shared premium; see [Couple-shared premium](#couple-shared-premium)), and `notification_queue/` is readable by the owner for the in-app activity feed (the owner can flip the `read` flag). `fcmTokens/` and `devices/` are owner-writable; the `devices/` public key is readable by the user's current couple partner only (to wrap release keys). Quiet-hours fields (`quietHoursEnabled`, `quietHoursStartMinutes`, `quietHoursEndMinutes`, `timezone`) and notification preferences (`notifPartnerAnswered`, `notifChatMessage`) are client-writable so Cloud Functions can honor them server-side.
**`users/{uid}`** - owner can read/create/update their own doc. Root-level premium state is **not** stored here; the client-writable `plan` field is informational only, and authoritative premium state lives in the server-only `entitlements/premium` subdoc. `entitlements/`, `notification_queue/`, and `outcomes/` are server-only writes; `entitlements/` is also readable by the user's current couple partner (couple-shared premium; see [Couple-shared premium](#couple-shared-premium)), and `notification_queue/` is readable by the owner for the in-app activity feed (the owner can flip the `read` flag). `fcmTokens/` and `devices/` are owner-writable; the `devices/` public key is readable by the user's current couple partner only (to wrap release keys). Quiet-hours fields (`quietHoursEnabled`, `quietHoursStartMinutes`, `quietHoursEndMinutes`, `timezone`) and notification preferences (`notifPartnerAnswered`, `notifChatMessage`, `notifDailyReminder`, `notifStreakReminder`, `notifPromotional`) are client-writable so Cloud Functions can honor them server-side.
**`date_ideas/`** - read-only for any signed-in user; writes are admin-only.