diff --git a/ClaudeReport.md b/ClaudeReport.md index dab637be..045e9ddc 100644 --- a/ClaudeReport.md +++ b/ClaudeReport.md @@ -18,7 +18,7 @@ > to the archived-ID line below (full detail stays in git history). See **Report hygiene** in `ClaudeQAPlan.md`. ## Run-state (current) -- **R24-d (2026-06-30) — two restore-flow UX fixes per user report ("there needs to be a back button on help your partner restore. also tapping recovery phrase does nothing"). Both LIVE-verified on QA/Sam; 244 unit tests green (no regressions).** **Why:** the manual "Help my partner restore" consent screen (R24-c) opened with no way back except gesture, and on an emulator/device with **no enrolled lock**, tapping "Recovery phrase" launched `BiometricPrompt` which silently errored (only `onAuthenticationSucceeded` was overridden) → felt like a dead tap. **What (uncommitted):** **(1) Back button** — `RestoreScreens.RestoreScaffold` gained an optional `onBack` param rendering a `CloserGlyphs.Back` `IconButton`; `RestoreRequestScreen`/`RestoreConsentScreen` accept `onBack`, wired in `AppNavigation` to `navigateBackOrHome` (`popBackStack()` else Home). **(2) Lock-less reveal** — `SecurityScreen.launchBiometricForPhrase` now checks `BiometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) != BIOMETRIC_SUCCESS` (or null activity) and, when there's no lock to prompt, reveals the phrase directly (`viewModel.revealRecoveryPhrase()`) instead of firing a prompt that no-ops. When a lock *is* enrolled, behaviour is unchanged (still gated). **LIVE-verified:** ✅ QA (no PIN) — tapping Recovery phrase now immediately shows the dialog with the 12-word phrase ("lion fair card like foot good full fame disk flat"); ✅ Sam — Settings → Security → "Help my partner restore" opens the consent screen **with a back arrow**, and the empty-state card renders ("There's no restore request waiting right now…" — no active QA request), and tapping back returns to Security. **Verification:** `:app:assembleDebug` OK + `:app:testDebugUnitTest` **244 green** (UI-only change, no test delta). Uncommitted: `ui/pairing/RestoreScreens.kt`, `ui/settings/SecurityScreen.kt`, `core/navigation/AppNavigation.kt`. +- **R24-d (2026-06-30) — three restore-flow UX fixes per user reports ("there needs to be a back button on help your partner restore. also tapping recovery phrase does nothing" → then "add a copy button for the recovery phrase"). All LIVE-verified on QA/Sam; 244 unit tests green (no regressions).** **Why:** the manual "Help my partner restore" consent screen (R24-c) opened with no way back except gesture; on a device with **no enrolled lock**, tapping "Recovery phrase" launched `BiometricPrompt` which silently errored (only `onAuthenticationSucceeded` was overridden) → felt like a dead tap; and once revealed there was no way to copy the phrase (12 words, error-prone to hand-transcribe). **What (uncommitted):** **(1) Back button** — `RestoreScreens.RestoreScaffold` gained an optional `onBack` param rendering a `CloserGlyphs.Back` `IconButton`; `RestoreRequestScreen`/`RestoreConsentScreen` accept `onBack`, wired in `AppNavigation` to `navigateBackOrHome` (`popBackStack()` else Home). **(2) Lock-less reveal** — `SecurityScreen.launchBiometricForPhrase` now checks `BiometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) != BIOMETRIC_SUCCESS` (or null activity) and, when there's no lock to prompt, reveals the phrase directly (`viewModel.revealRecoveryPhrase()`) instead of firing a prompt that no-ops. When a lock *is* enrolled, behaviour is unchanged (still gated). **(3) Copy button** — the Recovery-phrase `AlertDialog` gained a `dismissButton` "Copy" (with `CloserGlyphs.Copy` icon) that writes the phrase to the clipboard and toasts "Recovery phrase copied". The `ClipData` is flagged **`IS_SENSITIVE`** (via `description.extras` PersistableBundle) so the phrase is **redacted from the Android 13+ clipboard-preview chip and excluded from clipboard history** — copy convenience without leaking the secret into the preview UI. **LIVE-verified:** ✅ QA (no PIN) — tapping Recovery phrase immediately shows the dialog with the 12-word phrase ("lion fair card like foot good full fame disk flat"); ✅ **Copy** → toast fires and the clipboard-preview chip shows masked dots `••••••` (sensitive flag confirmed working); ✅ Sam — Settings → Security → "Help my partner restore" opens the consent screen **with a back arrow** + the empty-state card ("There's no restore request waiting right now…"), and tapping back returns to Security. **Verification:** `:app:assembleDebug` OK + `:app:testDebugUnitTest` **244 green** (UI-only change, no test delta). Uncommitted: `ui/pairing/RestoreScreens.kt`, `ui/settings/SecurityScreen.kt`, `core/navigation/AppNavigation.kt`. - **R24-b (2026-06-30) — hardened partner-assisted restore per user's "check the account matches the email" concern: recipient identity + active confirm gate on consent, two request-lifecycle bug fixes, and owner security alerts. 243 unit tests green; consent identity + confirm gate + re-request fix + full loop LIVE-verified on QA/Sam.** **Why:** when the partner approves, they hand over the couple key — the consent screen never showed *who* was receiving it, and the recipient's own account got no alert. Investigating surfaced two real robustness bugs too. **What (uncommitted):** **(1) Consent identity + confirm gate** (`RestoreViewModel`/`RestoreScreens`) — Sam now sees the recipient's **email (plaintext anchor) + display name (decrypted locally via the couple key; the server can't, so identity is necessarily client-side)**, plus an explicit **"I reached them directly and this is their account"** checkbox; **Approve is gated on `code.length==6 && confirmed`**. Added distinct **no-request / expired** empty states (the screen previously showed the form unconditionally on a stale deep-link). **(2) Two lifecycle bugs found + fixed** (`RestoreManager`): **Bug A** — `createRestoreRequest`'s `set()` over an existing request is a rule-denied key-changing update → a **retry silently `PERMISSION_DENIED`s**; fixed by delete-then-create. **Bug B** — `expiresAt` was never enforced; `fulfillRestore` now **rejects an expired request** before wrapping the key. **(3) Owner security alerts** (`onRestoreRequested` + new `onRestoreFulfilled`) — a **"was this you?" alert to the recipient's OWN devices** at request time and on key transfer (the real anti-takeover signal for a phished password when the owner still has a device); quiet-hours bypassed (security), deduped via `couples/{id}.lastRestoreSelfAlertAt` (60s) against create-loops, each notify branch independently try/caught; client `RESTORE_SELF_ALERT` type wired (`isEnabled` true, `routeFor` `AppRoute.SECURITY`, `fromRemoteType`). **Honest framing (SECURITY.md):** the code is the crypto root of trust; identity+confirm stop *accidental* approval + add social-engineering friction (they do NOT stop takeover — the couple email matches); the owner self-alert is the takeover catch; the strongest control (email-verification challenge) needs mail infra → Future.md. **Verification:** `:app:assembleDebug` + **243 unit tests** (+10: `RestoreViewModelTest` ×6 — identity/fallback/expired/no-request/approve-gating; `RestoreManagerTest` ×4 — delete-then-create order, expiry + wrong-code rejection, no-expiry-legacy allowed; functions `onRestoreRequested.test.ts` ×6 — dedupe window + READY-transition guard as extracted pure helpers) + functions **build green** + `wiring-scan` **🔴0 dead setters / 0 dead notif settings**. **LIVE on QA(5554)/Sam(5556), fresh APK:** ✅ Sam's consent shows QA's real **email `qa_1782321603516@closertest.com` + name "QA"** + the confirm checkbox using the name; ✅ **Approve stays disabled with code-only**, enables only after checking the box; ✅ **re-request after a prior request created a fresh code (370690) with NO `PERMISSION_DENIED`** (Bug A fix); ✅ approve → **QA restored key (couple_crypto recreated) + 34 messages with no phrase** and returned Home (R24 regression through the new gate). **FOUND + FIXED — misleading Recovery-phrase empty-state on a partner-restored device (user-asked "ensure Recovery Phrase displays as it should").** After partner-assist restore, `storeTransferredKeyset` intentionally transfers the key **but not the phrase** (the phrase is one-way from the couple key), so QA's Settings → Security correctly greyed the "Recovery phrase" row — but the copy read *"…will appear here once your couple is set up on this device,"* which is **false** (QA is fully set up, chat working). Fixed `SecurityScreen`/`SecurityViewModel` to detect **key-present-but-no-phrase** (`encryptionManager.aeadFor(coupleId) != null` with `recoveryPhrase == null`) and show the accurate partner-restore copy ("You set this device up with your partner's help, so the recovery phrase isn't saved here. Ask your partner to open Settings → Security and read you theirs — either partner's phrase can restore your history."). **LIVE-verified both devices:** ✅ **Sam (original device)** — Recovery phrase row enabled → PIN-gated reveal shows the correct **12-word phrase** ("lion fair card like foot good full fame disk flat") with the right explanatory copy; ✅ **QA (partner-restored)** — greyed row now shows the accurate partner-restore message (not the "not set up" copy). **Navigation (answered):** recipient reaches restore via the `NEEDS_RECOVERY` "Unlock your history" screen → **"No phrase? Ask your partner to restore this device"** → RestoreRequestScreen; the helper reaches consent **only via the `restore_requested` notification** (OS push + in-app inbox entry once the function is deployed) → RESTORE_CONSENT — no manual Settings entry exists (candidate follow-on). **BUILT (R24-c, user-asked "we will build that entry backup") — partner-assist now also restores the recovery phrase + a manual helper entry.** After confirming a partner-restored device was fully restored (key + content) but deliberately *phrase-less*, added: **(1) phrase transfer** — the `keybox` ECIES payload became an envelope `ckx:v1:{keyset, phrase?}` (`CoupleKeyTransfer.wrapCoupleKey(...recoveryPhrase)` → `unwrapCoupleKey` returns `TransferredKey(keyset, phrase?)` → `CoupleEncryptionManager.storeTransferredKeyset(coupleId, handle, phrase?)`); `RestoreManager.fulfillRestore` includes the sender's phrase, `completeRestore` stores it. Backward-compatible (a prefix-less legacy keybox → key-only). The phrase rides inside the same OOB-code-gated ECIES ciphertext as the key — server stays blind; both partners are meant to hold the shared phrase, so this restores parity, not exposure. **(2) Manual "Help my partner restore"** row in Settings → Security (gated on holding the couple key) → RESTORE_CONSENT, so the approver can reach consent even if the notification never arrives. **LIVE-verified QA/Sam:** ✅ full re-run (QA wiped → requested code 462926 → **Sam approved via the new manual Settings entry**, not a notification) restored key + 34 msgs; ✅ QA's Settings → Security **Recovery phrase row now enabled**, and PIN-gated reveal shows the **identical phrase** as Sam ("lion fair card like foot good full fame disk flat") — the greyed/partner-restore state is gone; ✅ `couple_crypto_secure.xml` grew 1646→1872 B (phrase now stored). Unit: `CoupleKeyTransferTest` +1 (phrase round-trip) + return-type update → **244 total green**. **GATED (user deploys):** `onRestoreRequested`+`onRestoreFulfilled` (self-alert pushes) + still-pending `storage.rules` (snapshot compaction) + R23 date functions. All uncommitted (user commits): `ui/pairing/{RestoreViewModel,RestoreScreens}.kt`, `ui/settings/SecurityScreen.kt`, `crypto/{CoupleKeyTransfer,CoupleEncryptionManager}.kt`, `data/backup/RestoreManager.kt`, `notifications/PartnerNotificationManager.kt`, `functions/src/backup/onRestoreRequested.ts` + `index.ts`, `{RestoreViewModel,RestoreManager,CoupleKeyTransfer}Test.kt`, `onRestoreRequested.test.ts`, + docs (SECURITY.md, Engineering_Reference_Manual R24-b/c, Future.md, ClaudeiOSPlan, this entry). - **R24 (2026-06-30) — built E2EE conversation backup + full partner-assisted restore (foundation for Option B). Code-complete, 233 unit tests green, deploy-gated for live verify.** **What (uncommitted):** devices keep a couple-key-encrypted backup of all conversations so a new/wiped device restores history, and a partner can **fully restore for the other (key + content) with no recovery phrase**. **Phase A** — schema (`FirestoreCollections` backup/restore consts) + `BackupCodec` (JSON→`enc:v1:` envelope, checksum, reactions) + `FirestoreBackupDataSource` (manifest `generation`-CAS transactions, chunk append, snapshot finalize w/ delete-after-commit, restore_requests) + `firestore.rules` (backup manifest/chunks + restore_requests, new `isPublicKey` helper) + `storage.rules` (`users/{uid}/backups/` — uploader-scoped, reuses existing `onUserDelete` cleanup). **Phase B** — `BackupManager` (incremental `appendSince` + full-state `compact` that captures deletes/reactions; resolved-timestamp-only; throttled/single-flighted) + conversation backup-reads on `FirestoreConversationDataSource`; opportunistic trigger from `HomeViewModel.loadHome`. **Phase C** — separate `ConversationCacheDatabase` (Room; kept OFF the asset-backed `AppDatabase` to protect its schema hash) + `BackupRestoreManager` (download snapshot+chunks → decrypt → checksum-validate → upsert dedup-by-id). **Phase D (headline)** — `CoupleKeyTransfer` (ECIES-wrap the couple keyset to a recovering device's fresh pubkey; context `"{coupleId}|restore|{sender}|{recipient}|{nonce}"`; **6-digit OOB code = SHA-256(pubkey‖nonce)**) + `RestoreManager` (A request→complete, B fulfil-after-code) + `RestoreViewModel`/`RestoreRequestScreen`/`RestoreConsentScreen` + RecoveryScreen "Ask your partner to restore" entry + nav. **Phase E** — `onRestoreRequested` Cloud Function (notifies partner; high-signal, not toggle-gated; audit-log no key material) + `RESTORE_REQUESTED` notif type wired (`fromRemoteType`/`routeFor`/isEnabled). **Verification:** `:app:assembleDebug` + **233 unit tests** (9 new: `CoupleKeyTransferTest` proves the couple key round-trips only to the right device+context, wrong key/nonce fail, OOB code binds to pubkey+nonce for swap-detection; `BackupCodecTest` round-trip/checksum/forward-compat/cursor) + functions **tsc green** + `wiring-scan` **🔴0**. **Security posture:** server holds only `enc:v1:` ciphertext + `keybox:v1:` blobs (AEAD tamper-evident); the OOB-code entry gate defeats a server/MITM pubkey swap + remote account-takeover; unpair revokes (rules require active couple); threat model + residual risks (no forward secrecy, rollback-freshness, client-enforced verification) documented in SECURITY.md. **Docs:** SECURITY.md (E2EE list + 2 threat rows + roadmap), Eng Ref Manual (**R24-BACKUP** landmine w/ wire formats + hazards), ClaudeQAPlan (Pass D backup/restore + rules-negative + Pass E `restore_requested`), ClaudeiOSPlan (byte-compat parity item), Future.md (Option B + FS + freshness + WorkManager/backfill + Settings indicator/opt-out). **LIVE-VERIFIED (2026-06-30) on QA(5554)/Sam(5556), fresh APK, `-gpu swiftshader_indirect`:** simulated device loss on QA by deleting **only** the secure prefs (`couple_crypto_secure.xml` = couple key + `user_key_secure.xml` = ECIES key) via `run-as` (NO `pm clear` — App Check token preserved) + backgrounding (KEYCODE_HOME) + `am kill` to drop the in-memory key → cold-launch QA lands on `NEEDS_RECOVERY`. **Full partner-assist (no phrase):** ✅ QA "Ask your partner to restore this device" → RestoreRequestScreen "Start restore" → published a fresh `pub:v1:` + created `restore_requests/{QA}` (rules **allowed**, no `PERMISSION_DENIED`) → showed OOB code **940687**. ✅ Sam driven to RESTORE_CONSENT (deep-link, function-independent) → typed **940687** → "Approve restore" → `fulfillRestore` verified code == `SHA-256(pubkey‖nonce)`, wrapped the couple keyset to QA's fresh pubkey (`keybox:v1:`), set status READY. ✅ **QA auto-completed with NO recovery phrase** — `couple_crypto_secure.xml` **recreated at 20:00** via `unwrapCoupleKey`+`storeTransferredKeyset`; QA navigated past `NEEDS_RECOVERY` to Home ("Connected with Sam"). ✅ **Content restored: 34 messages** across all 4 conversations (`main`:28 + 3 `q_daily_fun_*` discuss threads) upserted into QA's separate `conversation_cache.db` (confirmed via on-device `sqlite3` count) — sourced from the **Firestore `enc:v1:` chunks** (server-blind). ✅ Restored key **decrypts live content**: QA Messages inbox + main thread render the full history in plaintext ("hi"/"hey", R18 test msgs, "gu", today's pings) — not 🔒 placeholders. ✅ `completeRestore` deleted the request post-unwrap (no lingering keybox). **DEPLOY GAP FOUND (not a code bug): `storage.rules` was NOT deployed** (only `firestore.rules` was — which is why every Firestore-side op above worked). Both devices' `backupNow`→`compact` snapshot upload 403s (`GetDownloadUrlTask` "Permission denied") because the deployed storage.rules lacks the `users/{uid}/backups/` block → snapshot compaction can't run. **Backup + restore still work fully via the Firestore chunks** (the 34-msg restore proves it); only the Storage snapshot/compaction (chunk-folding) is blocked. **Fix = deploy `firebase deploy --only storage`** (rules file is correct + structurally identical to the working `chat_media` block); after deploy, compaction folds chunks → snapshot automatically on the next `backupNow`. **Verdict: R24 — headline full partner-assisted restore (key + 34 msgs, NO phrase, OOB-code-gated) VERIFIED LIVE end-to-end + server-blind; 0 FATAL. One outstanding DEPLOY-gate: user runs `firebase deploy --only storage` to enable snapshot compaction (+ still-pending `onRestoreRequested` for the partner push, and the R23 date functions). Board unchanged: 0 open P0/P1.** All uncommitted (user commits): `data/backup/*`, `data/remote/{FirestoreBackupDataSource,BackupCodec,FirebaseStorageDataSource,FirestoreConversationDataSource,FirestoreCollections}.kt`, `crypto/{CoupleKeyTransfer,CoupleEncryptionManager}.kt`, `data/local/ConversationCache*`, `di/DatabaseModule.kt`, `domain/model/ConversationBackup.kt`, `ui/pairing/{RestoreViewModel,RestoreScreens,RecoveryScreen}.kt`, `ui/home/HomeViewModel.kt`, `notifications/PartnerNotificationManager.kt`, `core/navigation/{AppRoute,AppNavigation}.kt`, `functions/src/backup/onRestoreRequested.ts` + `index.ts`, `firestore.rules`, `storage.rules`, `app/build.gradle.kts`, tests, + the docs above. - **R23 (2026-06-30) — built Date Memories & Replay (the date-roadmap killer feature) end-to-end, then LIVE-verified the whole loop on QA↔Sam. 0 defects in the new feature, 0 FATAL.** **Work (uncommitted):** the private→reveal loop applied to real dates — mark a mutual match **"We did this"** → `couples/{c}/date_history/{matchId}` (PLAINTEXT coarse metadata, idempotent doc-id=matchId merge) → **`date_reflections/{dateId}/answers/{uid}` (+ read-gated `secure/payload`) E2EE**, mirroring the daily-question couple-key gated reveal exactly. New: `DateMemory`/`DateReflection` models, `FirestoreDateMemoryDataSource`/`FirestoreDateReflectionDataSource`, `DateReflectionScreen`+VM (EDIT→AWAITING→REVEALED), `DateMemoriesScreen`+VM (Replay timeline), `DateMatchesScreen` "We did this" + "Your date memories" entry, **Phase C Home nudge** (`HomePriorityEngine.DATE_REFLECTION_PENDING` value-action + `HomeViewModel` pending computation + `HomeActionTarget.DateMemories` + `glyph_date_replay`), 2 Cloud Functions (`onDateReflectionWritten`/`onDateHistoryCreated`), partner-sheet emoji→brand-glyph retrofit, `firestore.rules` (`date_history`+`date_reflections`, **DEPLOYED by user**), docs (SECURITY/iOS-parity/QA-plan Pass E+N). **Cheap gates GREEN:** `:app:compileDebugKotlin` + `assembleDebug` (128MB APK) + `HomePriorityEngineTest` **25/25** (2 new DATE_REFLECTION_PENDING cases) + functions `tsc`; `wiring-scan` **🔴0 dead setters / 0 dead notif settings** for the date feature. **LIVE on QA(5554)/Sam(5556), fresh APK, software-GL:** ✅ mark-done → `date_history` synced to **both** timelines · ✅ QA reflects → AWAITING ("Saved privately 💜 — waiting for Sam"), **privacy gate holds** (QA can't see Sam pre-both) · ✅ Sam reflects → **mutual reveal side-by-side**, QA screen **live-flipped with no refresh** (observeReflected), **bidirectional E2EE decrypt** with correct You/partner labels · ✅ empty-state art (`illustration_date_memories_empty`) · ✅ timeline newest-first, per-row chips **Reflect/View** · ✅ **Home nudge** "Reflect on your date with Sam 💭" appears in BOTH the *Also waiting* (pending) and *More ways to connect* (secondary) surfaces with `glyph_date_replay`, routes to the timeline, clears after reflecting · ✅ **mark-done idempotency** (both partners tapped "We did this" on the same date → timeline shows it ONCE) · ✅ **light + dark** both date screens · ✅ cold-start no crash (nav restores back-stack) · ✅ partner-sheet retrofit renders all 5 actions as brand glyphs (no emoji-as-icons). **FOUND + FIXED — R23-DQ-001 (P2) daily-question silent re-answer data loss:** answering an already-answered daily Q logged `Write failed at …/daily_question/{date}/answers/{uid}/secure/payload: PERMISSION_DENIED` — the **immutable-answer guard** (`secure allow update:false`) correctly rejecting an overwrite. Root cause: the daily-Q screen + Home sourced "answered?" from **local Room only**, so on a fresh device / cleared DB (Room empty while Firestore still holds the answer) Home showed a **stale "your turn"** and the screen offered an **editable re-answer** — and a *changed* pick would be silently dropped (the secure overwrite is denied; reveal keeps the old content). NOT caused by the date work (`git diff` = only `date_*` rule additions; daily-Q rules/datasource untouched). **Fix (uncommitted):** new **Room-first** `reconcileLocalAnswerFromFirestore` (`ui/questions/LocalAnswerMapping.kt`) — if Room lacks the answer but Firestore has it, rebuild from the read-gated couple-key payload (owner can always read their own), map option-texts, and write it back to Room; persists only when the payload actually decrypts (a transient key-miss never poisons Room). Wired into `DailyQuestionViewModel.loadDailyQuestion` (awaited → screen shows submitted/reveal, never a lossy re-answer) and `HomeViewModel.loadHome` (non-blocking heal → no stale "your turn"). Room-first ⇒ the normal answered path is byte-identical. **Covered by 5 new unit tests** (`ReconcileLocalAnswerTest`: Room-hit short-circuit, heal+persist+option-text-map, no-prior-answer→null, assignment-rotation guard, undecryptable→answered-but-not-persisted). Full suite **224 green**, APK builds. Live fresh-device repro is `pm clear`-gated (forbidden — App Check token) so verified by unit tests + the provably-equivalent `withLocalAnswer` render path. `QuestionDetailViewModel` (pack questions) audited — **local-only writes, no gated-secure path → not vulnerable** (no change). The **date feature is also immune** (its `hasReflected` reads Firestore, not Room). **GATED (user-only):** notification pushes (`date_reflection_partner`/`date_reflection_ready`/`date_logged`) need the 2 new functions **deployed** — code-complete + `tsc`-green, not yet live. **Verdict: R23 — Date Memories & Replay shipped + LIVE-verified flawless across the full loop + Home nudge + idempotency + light/dark; 0 defects in the feature, 0 FATAL. Board unchanged: 0 open P0/P1; 1 P2 (O-AGE-001) + 1 P3 (BRAND-DARK-COVERAGE), both user-blocked. Pending: user deploys the 2 date functions → verify the 3 date pushes live.** Uncommitted (user commits): the new date `*.kt` + models + datasources + screens, `HomePriorityEngine.kt`/`HomeViewModel.kt`/`HomeScreen.kt`, `DateMatchesScreen.kt`/`DateMatchesViewModel.kt`, `AppRoute.kt`/`AppNavigation.kt`, `FirestoreCollections.kt`, `PartnerNotificationManager.kt`/`AppMessagingService.kt`, `functions/src/dates/*` + `index.ts`, `firestore.rules`, `glyph_date_replay.xml` + `illustration_date_memories_empty.png` (+night), `HomePriorityEngineTest.kt`, `SECURITY.md`/`ClaudeiOSPlan.md`/`ClaudeQAPlan.md`/`ClaudeReport.md`.