# Hardening Audit Report ## Audit Sources - 11 mapper agents (100% file coverage) - 18 specialized domain auditors (parallel) - 1 cross-cutting deep audit (parallel) - Total source files: 163 --- ## CRITICAL — Will crash or lose data ## BUG — Incorrect behavior ## SILENT FAILURE — Error swallowed or ignored ## RACE CONDITION — Concurrency issue ## LOGIC ERROR — Code doesn't match intent ## PERFORMANCE — Unnecessary cost ## ACCESSIBILITY — Usability barrier ## SECURITY — Vulnerability or exposure ## MODERNIZATION — Legacy pattern to update ## DEAD CODE / UNREACHABLE ## FRAGILE — Works now but will break easily --- ## Summary --- ## SOURCE: Memory Auditor (22 findings) **`Shared/Services/ImageCache.swift`:24** | CRITICAL | NotificationCenter observer token leaked — never stored or removed - What: `NotificationCenter.default.addObserver(forName:object:queue:using:)` is called in `init()`. The returned opaque observer token is discarded entirely with no variable. The block-based API requires the token to be stored and passed to `removeObserver(_:)` in `deinit`. - Impact: Each `ImageCache` initialization registers an observer that is never deregistered. In extension contexts where `ImageCache` might be initialized more than once, each initialization accumulates another dangling registration that fires `clearCache()` on every memory warning. **`Shared/Persisence/DataController.swift`:51-53** | CRITICAL | `editedDataClosure` array grows forever — no removal mechanism - What: `addNewDataListener(closure:)` appends closures to `editedDataClosure` with no paired remove function. Every call site permanently stores its closure inside the singleton `DataController.shared`. If those closures capture `self` (the view model), the view model is kept alive by the singleton indefinitely. - Impact: View models and any objects they capture are never deallocated. Each navigation to a screen that calls `addNewDataListener` leaks one closure and its retained objects. `refreshFromDisk()` iterates this ever-growing array and calls every closure including ones from long-dead view models. **`Shared/Views/EntryListView.swift`:2224** | CRITICAL | `MotionManager` accelerometer never stopped — no `stop()` call in any lifecycle hook - What: `MotionCardView.body` calls `motionManager.startIfNeeded()` in `.onAppear` (starts CMMotionManager at 30 Hz), but there is no `.onDisappear` modifier calling `motionManager.stop()`. The `stop()` function exists but is never invoked from any view. - Impact: Accelerometer runs at 30 Hz continuously for the entire app session once any `MotionCardView` appears. Constant battery drain regardless of whether the motion-style list is visible. **`Shared/Views/SettingsView/LiveActivityPreviewView.swift`:325-356** | CRITICAL | `ImageRenderer` called on background thread — threading violation - What: `DispatchQueue.global(qos: .userInitiated).async` at line 325 calls `ImageRenderer(content:).uiImage` on a background thread. `ImageRenderer` is `@MainActor` and must be used on the main thread only. - Impact: Undefined behavior — crashes, blank images, or data corruption. Direct violation of Apple's concurrency contract. **`Shared/Services/WatchConnectivityManager.swift`:117-124** | CRITICAL | `pendingMoods` array accessed from multiple threads — data race - What: `WatchConnectivityManager` has no actor isolation. `pendingMoods` is appended in `sendMoodToPhone` (called from any context) and read+cleared in `activationDidCompleteWith` (called on a WCSession background queue). No lock, serial queue, or actor isolation protects concurrent access. - Impact: Data race on `pendingMoods` can corrupt the array — crashes, lost pending moods, or duplicate sends. **`Shared/MoodStreakActivity.swift`:215-222** | CRITICAL | Infinite loop risk in `getStreakData()` when `calendar.date(byAdding:)` returns nil - What: `while true` loop advances `checkDate` via `calendar.date(byAdding: .day, value: -1, to: checkDate) ?? checkDate`. If `date(byAdding:)` returns `nil`, `checkDate` does not advance and the loop never breaks. - Impact: Infinite loop hangs the main actor indefinitely, freezing the app or widget process. **`Shared/Views/CelebrationAnimations.swift`:109** | BUG | `DispatchQueue.main.asyncAfter` holding `onComplete` closure fires after view dismissal — no cancellation - What: `onComplete()` (saves mood entry to Core Data) is scheduled via `asyncAfter`. If the user navigates away before the animation duration expires, the callback fires anyway, saving a mood entry. - Impact: Ghost entries can be saved to Core Data after the user has navigated away. No cancellation mechanism exists. **`Shared/DemoAnimationManager.swift`:60-70** | BUG | Orphaned `DispatchQueue.main.asyncAfter` closures when `restartAnimation()` called rapidly - What: Both `startDemoMode()` and `restartAnimation()` schedule `asyncAfter` with 3-second delay to call `beginAnimation()`. `restartAnimation()` invalidates the Timer but cannot cancel the GCD block. Rapid calls queue multiple `beginAnimation()` invocations. - Impact: Multiple `Timer` instances run simultaneously at 60 Hz, each mutating `animationProgress`, causing corrupt animation state and battery drain. **`Shared/DemoAnimationManager.swift`:91-95** | BUG | Timer fires at 60 Hz into `Task { @MainActor }`, accumulating tasks under load - What: Timer callback enqueues `Task { @MainActor in self?.updateProgress() }` on every tick (60/sec). If the main thread is busy, tasks pile up causing `animationProgress` to jump discontinuously. - Impact: Animation stutters. Up to 60 `Task` objects per second simultaneously alive, each holding a strong reference to `self`. **`Shared/FeelsApp.swift`:92** | BUG | `Task.detached(priority: .utility) { @MainActor in ... }` runs everything on main thread - What: Three heavy DataController operations (`refreshFromDisk`, `removeDuplicates`, `fillInMissingDates`) plus analytics run in a `Task.detached` tagged `@MainActor`. The `@MainActor` annotation forces entire body back onto the main thread — `detached` provides zero off-main-thread benefit. - Impact: Every foreground transition blocks the main thread with Core Data work, causing UI unresponsiveness for users with large datasets. **`Shared/FeelsApp.swift`:110-117** | WARNING | Fire-and-forget `Task.detached` blocks pile up on rapid scene transitions - What: Multiple `Task.detached` closures for `scheduleBasedOnCurrentTime()` and `processPendingSideEffects()` have no cancellation handle. Rapid foreground transitions queue stacks of these tasks concurrently, each mutating shared state. - Impact: Under rapid scene transitions, multiple concurrent mutations to subscription status, Live Activity scheduling, and pending side effects. **`Shared/Views/InsightsView/InsightsViewModel.swift`:88-113** | PERFORMANCE | `withTaskGroup` child tasks all `@MainActor` — run serially, negating concurrency - What: All three child tasks in `generateAllInsights()` are `@MainActor`-isolated. The main actor is a serial executor, so the tasks run sequentially rather than in parallel. - Impact: Three AI inference calls run in series on the main actor, blocking UI for their full combined duration. **`Shared/MoodStreakActivity.swift`:306** | BUG | `scheduleEnd` Timer has no `[weak self]` — inconsistent with `scheduleStart` - What: `scheduleEnd(at:)` Timer callback captures singletons directly without `[weak self]`. Inconsistent with `scheduleStart` which uses `[weak self]`. If refactored away from singletons, this becomes a retain cycle. - Impact: Low current risk (singletons), but fragile and inconsistent capture semantics. **`Shared/IAPManager.swift`:465-474** | WARNING | `deinit` cancel of `listenForTransactions()` Task is dead code — singleton never deinits - What: Task is cancelled in `deinit`, but `IAPManager` is a singleton and `deinit` is never called in production. The `[weak self]` and `deinit` cancel are dead code creating a misleading code contract. - Impact: If `IAPManager` is ever instantiated non-singleton (e.g., in tests), the task continues iterating `Transaction.updates` after deallocation, silently calling methods on nil self. **`Shared/Views/InsightsView/InsightsViewModel.swift`:60-63** | WARNING | `generateInsights()` spawns unstructured `Task{}` with no stored handle — multiple concurrent tasks possible - What: `generateInsights()` creates `Task{}` without storing the handle. Multiple `.onAppear` or pull-to-refresh calls can spawn concurrent tasks writing to the same `@Published` arrays. - Impact: Flickering loading states; potential for two in-flight `LanguageModelSession` instances consuming memory. --- ## SOURCE: IAP Auditor (16 findings) **`Shared/IAPManager.swift`:274** | CRITICAL | `Transaction.currentEntitlements` never calls `transaction.finish()` - What: The loop iterating `currentEntitlements` processes transactions but never calls `await transaction.finish()`. StoreKit 2 requires `finish()` to be called after processing every transaction. - Impact: Transactions remain unfinished in the payment queue indefinitely, re-delivered on subsequent app launches causing repeated processing. **`Shared/IAPManager.swift`:289** | CRITICAL | `try?` on `subscription.status` silently grants subscribed state on StoreKit errors - What: `let statuses = try? await subscription.status` discards StoreKit errors. If the call throws (network offline, StoreKit unavailable), `statuses` is `nil` and execution falls through to line 333 which grants `.subscribed` as a fallback. - Impact: A user with expired/cancelled subscription gets full premium access whenever StoreKit is temporarily unavailable. Revenue-loss vulnerability. **`Shared/IAPManager.swift`:333** | CRITICAL | Fallback unconditionally grants `.subscribed` when status retrieval fails - What: Line 333 grants `state = .subscribed(...)` whenever `product.subscription` is nil or `subscription.status` fails. Any condition preventing subscription type retrieval silently upgrades the user. - Impact: StoreKit failures grant premium access with no actual entitlement verification. **`Shared/IAPManager.swift`:193-202** | CRITICAL | Revoked/expired subscriptions restore `.subscribed` on next cold launch via cache - What: Terminal states (`.expired`, `.revoked`) are handled but do NOT clear the cached expiration date. On the next cold launch, `checkForActiveSubscription` returns false, the terminal guard is skipped (state was reset), and the cache fallback (line 207) grants `.subscribed` again. - Impact: Revocation does not survive a cold relaunch. Users whose subscriptions are revoked (e.g., refund) get premium access back on next launch. **`Shared/IAPManager.swift`:207-211** | BUG | Cache fallback restores `.subscribed` even when StoreKit explicitly found no active subscription - What: After a live StoreKit check found no active subscription, lines 206-211 restore `.subscribed` from cache if the cached expiration is in the future. The cache is never cleared when StoreKit explicitly returns expired/revoked. - Impact: Users who cancel subscriptions retain full access until cached expiration date, regardless of StoreKit's live result. **`Shared/Views/FeelsSubscriptionStoreView.swift`:51** | BUG | `dismiss()` called outside async Task — executes before `checkSubscriptionStatus()` completes - What: `dismiss()` is called synchronously immediately after spawning `Task { @MainActor in ... }`, not inside it. The view dismisses before subscription status is confirmed and analytics complete. - Impact: UI remains in stale IAP state after purchase; async task continues running against a dismissed view's environment. **`Shared/Onboarding/views/OnboardingSubscription.swift`:139-142** | BUG | `onDismiss` unconditionally completes onboarding regardless of purchase outcome - What: `.sheet(isPresented:onDismiss:)` calls `completionClosure(onboardingData)` in `onDismiss` whether the user subscribed or cancelled the sheet. - Impact: Onboarding completes even when user dismisses paywall without subscribing. Also fires `onboardingCompleted` analytics twice if user used the skip path first. **`Shared/IAPManager.swift`:465-473** | WARNING | `Transaction.updates` finishes transactions but `currentEntitlements` does not — asymmetric - What: `listenForTransactions()` correctly calls `transaction.finish()` on all `Transaction.updates`. But the initial check in `checkForActiveSubscription` never finishes transactions from `currentEntitlements`. - Impact: Transactions delivered at launch via `currentEntitlements` will be re-delivered via `Transaction.updates`, causing double-processing. **`Shared/Views/FeelsSubscriptionStoreView.swift`:54-55** | WARNING | `.pending` purchase state tracked as failure in analytics - What: `case .success(.pending)` calls `trackPurchaseFailed(error: "pending")`. Pending transactions (Ask to Buy, deferred payment) are valid non-failure states. - Impact: Purchase funnel metrics miscounted; user receives no pending-approval UI confirmation. **`Shared/IAPManager.swift`:232-244** | WARNING | `restoreCachedSubscriptionState()` grants `.subscribed` with no expiration from a UserDefaults boolean alone - What: When `hasActiveSubscription` is `true` but no expiration date is stored, grants `state = .subscribed(expirationDate: nil, willAutoRenew: false)`. Relies solely on a UserDefaults boolean. - Impact: If UserDefaults is corrupted or not cleaned up after expiration on older app versions, users get unlimited premium access until async check completes. Brief window at cold launch. **`Shared/IAPManager.swift`:40-44** | WARNING | `bypassSubscription` debug flag identical in both `#if DEBUG` and `#else` blocks — dead code - What: Both branches set `bypassSubscription = false`. The `#if DEBUG` variant is dead code with no difference from release build. - Impact: Developers believe a debug bypass exists but it doesn't; risk of accidentally modifying release value during testing. **`Shared/Views/CustomizeView/CustomizeView.swift`:693-699** | WARNING | `openSubscriptionManagement()` failure only `print()`-logged — no user feedback - What: `AppStore.showManageSubscriptions(in: windowScene)` failure in `catch` block only calls `print(...)`. No alert or user notification on failure. - Impact: Users tapping "Manage Subscription" see nothing happen and receive no explanation if it fails. **`Shared/IAPManager.swift`:156-162** | WARNING | Throttle skips re-check even in `.expired` or `.revoked` state - What: 5-minute throttle prevents `checkSubscriptionStatus()` from re-running even when state is `.expired` or `.revoked`. `restore()` also goes through this throttle. - Impact: User who restores purchases within 5-minute window of previous check sees unchanged state until throttle expires. **`Shared/FeelsApp.swift`:120** | WARNING | `Task.detached(priority: .background)` calling `@MainActor` subscription check runs on main thread anyway - What: `checkSubscriptionStatus()` is `@MainActor`-isolated. `Task.detached(priority: .background)` immediately hops to the main actor, providing no threading benefit. - Impact: Network StoreKit call and state mutation occur on main thread during foreground transitions. **`Shared/Views/PurchaseButtonView.swift`:127** | WARNING | "Payment Issue" billing label hardcoded English — not localized - What: `Text("Payment Issue")` is a raw string literal while other strings in this file use `String(localized:)`. Shown to users in billing trouble. - Impact: Non-English users see "Payment Issue" in English during a critical billing communication. **`Shared/IAPManager.swift`:139-146** | WARNING | `init()` Task has no stored cancellation handle — asymmetric cleanup with `updateListenerTask` - What: `Task { await checkSubscriptionStatus() }` in `init()` has no handle stored. `updateListenerTask` is separately cancelled in `deinit`, but the init task cannot be cancelled. - Impact: Inconsistent cleanup semantics; in non-singleton usage (tests), initial check task cannot be cancelled. --- ## SOURCE: Concurrency Auditor (33 findings) **`Shared/Services/WatchConnectivityManager.swift`:27** | CRITICAL | Data race on `pendingMoods` array — no actor isolation - What: `pendingMoods` is a plain `var` array on a class with no actor annotation. It is written in `sendMoodToPhone` (called from watchOS app context) and both read and written in `activationDidCompleteWith` (called on the WCSession delegate callback queue, a background thread). - Impact: Concurrent read/write of the array constitutes a data race. Under Swift 6 strict concurrency this is a compiler error. In practice this can corrupt the array or produce torn reads. **`Shared/Services/WatchConnectivityManager.swift`:93-95** | BUG | `transferUserInfo` called from WCSession error callback on unspecified background thread - What: The `sendMessage` error handler closure at line 93 calls `session.transferUserInfo(message)` directly. WCSession error callbacks run on an arbitrary background thread. `WatchConnectivityManager` has no actor isolation. - Impact: Potential thread-safety violation calling WCSession API from an indeterminate thread; also silently continues after failure with no reporting to the caller. **`Shared/FeelsApp.swift`:92** | CRITICAL | `Task.detached { @MainActor in }` defeats the purpose of `detached` - What: `Task.detached(priority: .utility) { @MainActor in ... }` creates a detached task but immediately re-isolates the body to `@MainActor`. The three Core Data calls — `refreshFromDisk()`, `removeDuplicates()`, `fillInMissingDates()` — all execute synchronously on the main actor. - Impact: Heavy Core Data work blocks the main thread on every foreground transition, causing UI jank. The `detached` keyword provides false assurance that this work is off-thread. **`Shared/BGTask.swift`:25** | CRITICAL | `@MainActor` background task calls async function without `await` — task completes before work finishes - What: `runFillInMissingDatesTask` is marked `@MainActor` and calls `MoodLogger.shared.processPendingSideEffects()` at line 25 without `await`. Calling it without `await` means it begins execution but the caller does not wait for it to finish. - Impact: `task.setTaskCompleted(success: true)` fires before `processPendingSideEffects()` finishes, incorrectly signaling completion to the OS. Background task work may be partially executed. **`Shared/BGTask.swift`:14-28** | CRITICAL | `@MainActor` background task blocks main thread with synchronous Core Data - What: `runFillInMissingDatesTask` is annotated `@MainActor`, meaning all its work runs on the main thread. `DataController.shared.fillInMissingDates()` is a synchronous Core Data operation that can scan the entire entries table. - Impact: Running `fillInMissingDates()` on the main thread during a background task blocks the UI thread if the app is active, causing ANR-like behavior. **`Shared/BGTask.swift`:36** | BUG | Force unwrap of `Calendar` date result in background scheduler - What: `runDate = Calendar.current.date(bySettingHour: 0, minute: 1, second: 0, of: runDate!)` — force-unwraps the previous `date(byAdding:)` result. On DST transitions or calendar edge cases, `date(byAdding:)` can return nil. - Impact: Crash in the background task scheduler. Background processing permanently stops scheduling. **`Shared/BGTask.swift`:42** | BUG | String interpolation syntax error silently suppresses error logging - What: `print("Could not schedule image fetch: (error)")` uses `(error)` instead of `\(error)`. This is valid Swift — it prints the literal string `"(error)"` rather than interpolating the actual error value. - Impact: All `BGTaskScheduler.submit` failures are permanently invisible. **`Shared/Models/UserDefaultsStore.swift`:217** | CRITICAL | Static mutable cache with no synchronization — data race - What: `private static var cachedOnboardingData: OnboardingData?` is a static mutable variable with no actor isolation and no locking. Read and written from `@MainActor` contexts and background tasks. - Impact: Concurrent reads/writes to this static var constitute a Swift 6 data race. The cache could be read in a partially-written state, returning nil or stale `OnboardingData`. **`Shared/Random.swift`:174** | CRITICAL | Static mutable dictionary `textToImageCache` with no thread safety - What: `private static var textToImageCache = [String: UIImage]()` is accessed from `textToImage()` without any synchronization. Widget rendering, background tasks, and main-thread rendering all can call this simultaneously. - Impact: Concurrent dictionary read/write is a data race. Under Swift 6 this is a compiler error. In practice it can corrupt the dictionary, producing crashes or wrong images. **`Shared/Random.swift`:48,68** | CRITICAL | Static mutable caches `existingWeekdayName` and `existingDayFormat` with no thread safety - What: Two `static var` dictionaries are read/written from `weekdayName(fromDate:)` and `dayFormat(fromDate:)` with no actor isolation or synchronization. Called from widget rendering (background), Core Data background operations, and main thread UI. - Impact: Concurrent dictionary mutations are data races. **`Shared/DemoAnimationManager.swift`:60-71** | BUG | Uncancellable `DispatchQueue.main.asyncAfter` causes stacked timer creation - What: `restartAnimation()` calls `animationTimer?.invalidate()` then `DispatchQueue.main.asyncAfter(deadline: .now() + startDelay) { self.beginAnimation() }`. The GCD block cannot be cancelled. Multiple timers created and run simultaneously. - Impact: Multiple concurrent 60fps timers driving the same `animationProgress` state cause undefined animation behavior and CPU waste. **`Shared/MoodStreakActivity.swift`:215-223** | CRITICAL | Infinite loop when `Calendar.date(byAdding:)` returns nil - What: `while true` loop with `?? checkDate` fallback — if `date(byAdding: .day, value: -1, to: checkDate)` returns nil, `checkDate` never changes and the loop never terminates. - Impact: Infinite loop on `@MainActor` permanently freezes the UI. **`Shared/Persisence/DataControllerGET.swift`:74-82** | CRITICAL | Infinite loop when `date(byAdding:)` returns nil in `calculateStreak` - What: Same `while true` with `?? checkDate` fallback in `calculateStreak(from:)`. On DST edge case, `checkDate` doesn't advance. - Impact: Main thread freeze on `@MainActor DataController`. Watchdog terminates the app. **`Shared/AppShortcuts.swift`:128-140** | CRITICAL | Unbounded `while true` loop on `@MainActor` with Core Data fetch per iteration - What: `calculateStreak()` runs a `while true` loop on `@MainActor`, fetching a Core Data entry for each past day. For a user with a 3-year streak, this performs 1,095 sequential Core Data round-trips synchronously on the main thread. - Impact: Main thread blocked for potentially hundreds of milliseconds to seconds. App appears frozen while Siri shortcut resolves. **`Shared/DataController.swift`:15** | BUG | `@MainActor` singleton may be initialized off-actor on first access - What: `static let shared = DataController()` on a `@MainActor final class`. Swift lazily initializes static properties on first access. If accessed before the main actor is running (e.g., from a widget extension or background thread), the `@MainActor` initializer is called off-actor. - Impact: SwiftData `ModelContainer` and `ModelContext` created on a non-main thread — undefined behavior. **`Shared/Views/InsightsView/InsightsViewModel.swift`:88-113** | PERFORMANCE | `withTaskGroup` child tasks all `@MainActor` — no real concurrency - What: All three `group.addTask { @MainActor in ... }` closures are isolated to `@MainActor`. They execute serially despite using a task group. - Impact: Three sequential AI insight generation calls on the main actor. Each can take seconds; UI is blocked waiting for all three. **`Shared/Views/InsightsView/InsightsViewModel.swift`:60-63** | BUG | Concurrent `generateInsights()` calls not guarded — interleaved `@Published` mutations - What: `generateInsights()` launches `Task { await generateAllInsights() }` with no guard. Multiple concurrent tasks independently overwrite `@Published` arrays. - Impact: Race between two tasks writing the same `@Published` property. Final displayed insights are non-deterministic. **`Shared/Analytics.swift`:614-618** | WARNING | Swift 6 violation — calling `@MainActor` method from non-isolated `ViewModifier` - What: `ScreenTrackingModifier.body(content:)` calls `AnalyticsManager.shared.trackScreen(...)` inside `.onAppear`. Under Swift 6, calling a `@MainActor` function from a non-isolated synchronous context is a compile error. - Impact: Swift 6 concurrency violation. Will be a hard error once strict concurrency is enforced. **`Shared/Services/PhotoManager.swift`:104-134** | PERFORMANCE | Synchronous disk I/O on `@MainActor`-bound class - What: `loadPhoto(id:)` and `loadThumbnail(id:)` call `Data(contentsOf: fullURL)` — synchronous file I/O — on the main actor. - Impact: Reading large JPEG files from disk on the main thread causes dropped frames and UI jank during list scrolling. **`Shared/Views/SettingsView/LiveActivityPreviewView.swift`:325-356** | CRITICAL | `ImageRenderer` used on background `DispatchQueue.global` thread - What: `DispatchQueue.global(qos: .userInitiated).async { let renderer = ImageRenderer(...); renderer.uiImage }`. `ImageRenderer` is `@MainActor`-isolated per Apple documentation. - Impact: Swift 6 data race. Produces blank images, memory corruption, or crashes. **`Shared/Services/SharedMoodIntent.swift`:93-121** | CRITICAL | New `ModelContainer` created per widget intent invocation against shared SQLite store - What: `WidgetMoodSaver.save(mood:date:)` creates a fresh `ModelContainer` on every call. Multiple `ModelContainer` writers to the same SQLite file can produce locked errors or data corruption. The delete-then-insert sequence can permanently lose user data if the second save fails. - Impact: Data corruption or permanent mood entry loss on concurrent intent + main app writes. **`Shared/Views/SharingTemplates/LongestStreakTemplate.swift`:46-53** | CRITICAL | Non-isolated `init` directly calls `@MainActor`-annotated `configureData()` - What: `LongestStreakTemplate.init` is not `@MainActor`-isolated but calls `self.configureData(fakeData:mood:)` which is `@MainActor`. Calling a `@MainActor`-isolated function synchronously from a non-isolated context is a Swift 6 concurrency violation. - Impact: Swift 6 will produce a compile error. The data load also silently has no effect since it writes to `@State` vars at init time. **`Shared/Views/Photos/PhotoPickerView.swift`:151** | BUG | `@State` mutation from non-`@MainActor` async context - What: `loadImage(from:)` is declared `private func ... async` with no `@MainActor` annotation. Inside it, `isProcessing = true` mutates a `@State` property off the main actor. - Impact: Swift 6 data race: writing `@State` off the main actor produces undefined SwiftUI behavior. **`Shared/Views/SettingsView/SettingsView.swift`:831-843,912-941** | BUG | `Task {}` inside `Binding.set` closure mutates `@EnvironmentObject` without `@MainActor` - What: `Task { }` closures inside `Binding` set closures mutate `@Published` properties of `@EnvironmentObject` ObservableObjects without explicit `@MainActor` annotation. - Impact: Under Swift 6, setting `@Published` properties of `ObservableObject` on a non-main actor is a data race. **`Shared/FeelsApp.swift`:30** | BUG | Force cast of `BGTask` in scheduler callback — crashes if wrong type delivered - What: `BGTask.runFillInMissingDatesTask(task: task as! BGProcessingTask)` — force cast inside BGTaskScheduler callback. If scheduler delivers a different task type, crashes with no recovery. - Impact: App crashes in the background task handler; system may reduce background execution privileges. **`Shared/FeelsApp.swift`:110-117** | BUG | Fire-and-forget `Task.detached` blocks pile up on rapid scene transitions - What: Multiple `Task.detached` closures for `scheduleBasedOnCurrentTime()` and `processPendingSideEffects()` have no cancellation handle. Rapid foreground transitions queue concurrent tasks, each mutating shared state. - Impact: Multiple concurrent mutations to subscription status, Live Activity scheduling, and pending side effects. **`Shared/Views/SettingsView/LiveActivityPreviewView.swift`:203-212** | WARNING | Timer callback mutates `@State` without actor guarantee - What: `Timer.scheduledTimer` callback directly sets `@State` properties. While timers on the main run loop fire on the main thread, there is no enforcement that `startAnimation()` is only ever called on the main thread. - Impact: If called from a non-main context, mutates `@State` off the main actor. SwiftUI purple thread-checker warning. **`Shared/IAPManager.swift`:465-474** | WARNING | `Task.detached { [weak self] }` on a singleton — misleading ownership semantics - What: `listenForTransactions()` returns `Task.detached { [weak self] in ... }`. On a singleton, `[weak self]` never becomes nil. The `deinit { updateListenerTask?.cancel() }` is also dead code. Future code added directly in the loop body without `await self?.` would be a data race. - Impact: Misleading code contract; maintenance hazard. **`Shared/AppDelegate.swift`:36-40** | WARNING | `@preconcurrency` suppresses concurrency violations in UNUserNotificationCenter delegate - What: Extension annotated `@preconcurrency` to silence Swift concurrency warnings. Under Swift 6, `@preconcurrency` suppresses the violation rather than fixing it. - Impact: Hidden concurrency violations in notification delegate callbacks. **`Shared/Services/MoodStreakActivity.swift`:306-316** | WARNING | `scheduleStart` Timer `guard let self` is silent no-op on dealloc — no fallback - What: If `self` is nil when timer fires, Live Activity start silently never happens with no fallback or logging. - Impact: Live Activities may silently fail to start after scheduler recreation. **`Shared/MoodStreakActivity.swift`:324-329** | WARNING | `scheduleEnd` Timer captures singletons without `[weak self]` - What: End timer closure captures singletons by strong reference; timer never cancelled on `deinit` (singleton never deallocates). - Impact: Leaked timer. If singleton semantics change, this becomes a retain cycle. **`Shared/Services/MoodLogger.swift`:113** | WARNING | `getData(startDate:endDate:includedDays:)` called with empty `[]` for `includedDays` — behavior undefined - What: Empty `includedDays` array semantics unknown — could mean "no days" rather than "all days". If it means "no days", `processPendingSideEffects()` never catches up on missed side effects. - Impact: Widget or watch-logged moods may never have side effects (Live Activity, HealthKit, streak) applied. **`Shared/Persisence/DataControllerGET.swift`:18, `Shared/Persisence/DataControllerDELETE.swift`:49, `Shared/Services/ExtensionDataProvider.swift`:113** | BUG | Off-by-one inclusive upper bound `<=` includes entries from next day's midnight - What: Predicates use `entry.forDate <= endDate` where `endDate` is midnight of the next day. Entries logged exactly at midnight are included in the wrong day's query. - Impact: Entries logged at midnight appear in wrong day's data across calendar views, month summaries, and streak calculations. **`Shared/Views/SharingTemplates/*.swift` (7 files)** | WARNING | `var image: UIImage` calling `ImageRenderer`-based `asImage()` without `@MainActor` annotation - What: Seven sharing template types expose `var image: UIImage` as a plain computed property calling `someView.asImage(size:)`. If `asImage()` uses `ImageRenderer` (which is `@MainActor`-isolated), this is a Swift 6 actor isolation violation. - Impact: Compile-time error under strict concurrency. At runtime currently safe since called from button actions on main thread. --- ## SOURCE: SwiftUI Performance Auditor (33 findings) **`Shared/Onboarding/views/OnboardingTime.swift`:13-17** | PERFORMANCE | `DateFormatter` computed property allocated every body re-render at 60fps during date picker interaction - What: `var formatter: DateFormatter { let dateFormatter = DateFormatter(); ... return dateFormatter }` is a computed property accessed directly in `body`. The `DatePicker` bound to `$onboardingData.date` updates on every wheel scroll tick, triggering a body re-render every frame. - Impact: During date-picker interaction, formatter reallocated ~60 times/second wasting CPU on repeated ICU initializations. **`Shared/Onboarding/views/OnboardingWrapup.swift`:15-19** | PERFORMANCE | `DateFormatter` computed property allocated every body re-render - What: Same pattern as `OnboardingTime`. `var formatter: DateFormatter` is a computed property creating a new instance each access. - Impact: Repeated expensive allocations on every render. **`FeelsWidget2/FeelsTimelineWidget.swift`:73-83** | PERFORMANCE | `DateFormatter` created as computed property on widget views — reallocated on every render pass - What: `private var dayFormatter: DateFormatter` and `private var dateFormatter: DateFormatter` are computed properties constructing new `DateFormatter` instances. Repeated in `SmallWidgetView`, `MediumWidgetView`, and `LargeWidgetView`. - Impact: Widget rendering is memory-constrained. Multiple `DateFormatter` objects allocated and immediately discarded per timeline refresh. **`FeelsWidget2/FeelsTimelineWidget.swift`:165-170** | PERFORMANCE | `headerDateRange` creates another `DateFormatter` inside a computed property called in body - What: `private var headerDateRange: String` creates a fresh `DateFormatter` inline per call. Third `DateFormatter` allocation per `MediumWidgetView` render. - Impact: Compounding allocations in memory-limited widget extension. **`FeelsWidget2/FeelsVoteWidget.swift`:64-76** | PERFORMANCE | `votingDateString` computed property creates two `DateFormatter` instances per call - What: Constructs `dayFormatter` and `dateFormatter` inside the function body. Accessed directly from `body`. - Impact: Two `DateFormatter` allocations per body evaluation including locale-sensitive ICU initialization. **`Shared/Views/DayView/DayView.swift`:99-103** | PERFORMANCE | `sortedGroupedData` recomputes expensively on every body pass with no caching - What: `private var sortedGroupedData` calls `.sorted { $0.key > $1.key }` twice and constructs a new array on every body evaluation. Unlike `MonthView` which caches in `@State`, `DayView` re-sorts on every render. - Impact: O(n log n) work on every render for all users with multiple years of data. **`Shared/Views/MonthView/MonthView.swift`:173-175** | PERFORMANCE | `displayData.flatMap` runs in body every render — recreates full flattened month array unconditionally - What: `let allMonths = displayData.flatMap { ... }` runs on every body evaluation, creating a new array even on trivial state changes. - Impact: O(n) allocation for users with several years of data on every render including trivial changes. **`Shared/Views/MonthView/MonthView.swift`:176** | BUG | `ForEach` uses `id: \.element.month` — non-unique across years, causes diffing collisions - What: `ForEach(Array(allMonths.enumerated()), id: \.element.month)` uses integer month number (1–12) as identity. January 2023 and January 2024 both have `.month == 1`. - Impact: SwiftUI may skip rendering new month cards, animate between wrong cells, or fail to insert/remove months on data changes for users with more than one year of data. **`Shared/Views/MonthView/MonthView.swift`:410** | PERFORMANCE | `@ObservedObject private var demoManager = DemoAnimationManager.shared` inside `MonthCard` — 60fps singleton updates fan to all cards - What: Every `MonthCard` holds an `@ObservedObject` reference to the same singleton. When `demoManager.animationProgress` publishes at 60 fps during demo mode, all `MonthCard` views receive `objectWillChange`, including off-screen cards. - Impact: Dozens of simultaneous subscription notifications per frame during demo animation. **`Shared/Views/MonthView/MonthView.swift`:431-443** | PERFORMANCE | `animatedMetrics` computed property calls `demoManager.visiblePercentageForMonth` on every body pass during animation - What: Reconstructs full `[MoodMetrics]` array via `.map` on every evaluation. During demo animation at 60 fps, runs for every card in the list on every frame. - Impact: O(n_months × 5_moods × 60fps) object allocations per second during demo playback. **`Shared/Views/MonthView/MonthView.swift`:661** | BUG | `cachedMetrics` only recalculated when empty — stale metrics after entry edits - What: `if cachedMetrics.isEmpty { cachedMetrics = ... }` in `.onAppear`. If user edits a mood entry, `entries` changes but `cachedMetrics` is non-empty so never recalculates. `Equatable` conformance checks only count, not content. - Impact: After editing a mood entry, `MonthCard` shows old mood distribution stats. Share sheet also uses `cachedMetrics`, producing incorrect shared images. **`Shared/Views/YearView/YearView.swift`:334-342** | BUG | `YearCard.Equatable` compares `yearEntries.count` only — stale cached metrics after data edits - What: `YearCard.static func ==` checks only count. `cachedMetrics` calculated only when empty. If entry moods change without changing count, `YearCard` never re-renders. - Impact: Year statistics and share images show incorrect mood distribution after any edit. **`Shared/Views/YearView/YearView.swift`:174-178** | PERFORMANCE | `GeometryReader` as preference-key scroll offset tracker — fires on every scroll pixel - What: `GeometryReader` embedded in scroll content `.background` to measure scroll offset and emit via preference key. Fires `onPreferenceChange` on every single scroll position change. Same pattern in `MonthView`. - Impact: Every scroll position change triggers a preference update at 60+ fps → main thread callback → potential animation state mutation. **`Shared/Views/YearView/YearViewModel.swift`:36-38** | PERFORMANCE | O(n log n) sort to find minimum date when `min(by:)` is O(n) - What: `filteredEntries.sorted(by: { $0.forDate < $1.forDate }).first?.forDate` sorts entire array to get earliest date. - Impact: Unnecessary sort on every `updateData()` call. `min(by:)` achieves same result in O(n). **`Shared/Views/YearView/YearViewModel.swift`:48-51** | PERFORMANCE | `data.removeAll()` and `entriesByYear.removeAll()` trigger two separate SwiftUI layout passes with empty state - What: In `filterEntries`, two separate `removeAll()` calls each publish `objectWillChange` before new data is assigned. - Impact: View briefly renders empty-data state (showing `EmptyHomeView`) twice on every year data refresh. **`Shared/Views/CelebrationAnimations.swift`:311** | PERFORMANCE | `Double.random(in:)` called inside body inside `ForEach` — non-deterministic opacity on every render - What: `ShatterReformAnimation.body` applies `.fill(mood.color.opacity(Double.random(in: 0.5...1.0)))` inside `ForEach`. Generates new random opacity on every body evaluation. - Impact: Shard colors re-randomize on every state change causing visible flickering between animation phases. **`Shared/Views/EntryListView.swift`:139,146** | PERFORMANCE | `classicStyle` bypasses `DateFormattingCache` — repeated ICU operations on every body evaluation - What: `classicStyle` calls `Random.weekdayName` and `Random.dayFormat` directly, bypassing `DateFormattingCache.shared`. Same bypass in `microStyle` and `MotionCardView`. - Impact: `classicStyle` is default and most common. With 365+ cells in the list, cache misses cause redundant date formatting on every redraw. **`Shared/Views/EntryListView.swift`:2249** | PERFORMANCE | CMMotion callback calls `withAnimation(.interactiveSpring(...))` on every accelerometer update at 30 Hz - What: The `motionManager.startDeviceMotionUpdates(to: .main)` callback wraps every accelerometer reading in `withAnimation(.interactiveSpring(...))`, re-enqueuing an animation transaction at 30 Hz. - Impact: 30 animation transactions per second on the main thread for all visible `MotionCardView` cells simultaneously. **`Shared/Views/FeelsSubscriptionStoreView.swift`:165-177** | PERFORMANCE | Multiple `withAnimation(.repeatForever(...))` started in `.onAppear` — cannot cancel on dismiss - What: Every theme marketing view (12 total) starts one or more `repeatForever` animations in `.onAppear`. `.onDisappear` resets state values but cannot cancel in-flight repeating animation transactions. - Impact: Rapid open/close cycles stack animation transactions, causing visual glitches and CPU overhead. **`Shared/Views/LockScreenView.swift`:1121-1128** | PERFORMANCE | `CGFloat.random(in:)` called inside body for cloud sizes — re-randomizes on every state change - What: `ForecastLockBackground` uses `CGFloat.random(in: 40...80)` inline inside the view body for cloud element sizes and blur values. Not captured in `@State` or `onAppear`. - Impact: Cloud sizes change on every body re-render, producing visual instability during authentication flow. **`FeelsWidget2/FeelsTimelineWidget.swift`:87-92** | PERFORMANCE | `TimeLineCreator.createViews` and `UserDefaultsStore.moodTintable()` called synchronously in widget `init` - What: Widget view `init` calls `TimeLineCreator.createViews(daysBack:)` and `UserDefaultsStore.moodTintable()` synchronously across all three widget size classes. - Impact: Blocking synchronous I/O on WidgetKit's render thread. Contention with main app's ModelContainer can cause widget render deadline timeout. **`Shared/Views/DayView/DayView.swift`:89** | PERFORMANCE | `UserDefaultsStore.getOnboarding()` called inside `headerView` computed property on every body pass - What: `headerView` calls `ShowBasedOnVoteLogics.isMissingCurrentVote(onboardingData: UserDefaultsStore.getOnboarding())` on every body pass. Decode path reachable on cache miss. - Impact: Every body re-render of `DayView` calls into `UserDefaultsStore.getOnboarding()`. Not thread-safe. **`Shared/Views/CustomizeView/CustomizeView.swift`:295** | WARNING | Hidden `Text` hack to force re-renders — fragile, may be optimized away by future OS - What: `Text(String(customMoodTintUpdateNumber)).hidden().frame(height: 0)` inserted as workaround to trigger re-renders. SwiftUI anti-pattern. - Impact: Future OS optimizations of hidden views may silently break this, causing tint changes to stop reflecting in picker UI. **`Shared/Views/MonthView/MonthView.swift`:246-259** | PERFORMANCE | `AnyView` type erasure for conditional `.mask` modifier — forces full re-creation on paywall state change - What: `.mask` modifier uses `(condition) ? AnyView(LinearGradient(...)) : AnyView(Color.black)`. SwiftUI cannot track structural identity through `AnyView`. Same pattern in `YearView` at lines 184-195. - Impact: Entire `ScrollView` content re-created from scratch on paywall state changes instead of targeted update. **`Shared/Views/SharingListView.swift`:51** | PERFORMANCE | Heavy use of `AnyView` for all sharable items — disables SwiftUI structural diffing for entire sharing list - What: `WrappedSharable` structs use `AnyView` for `.preview` and `.destination`. `ForEach` over `AnyView` items means no structural identity comparison. - Impact: Any state change causes all sharing item previews to be fully re-created. **`Shared/Models/MoodTintable.swift`:112-143** | PERFORMANCE | `UserDefaultsStore.getCustomMoodTint()` called up to 10 times per render for custom tint views - What: `CustomMoodTint.color(forMood:)` and `CustomMoodTint.secondary(forMood:)` each call `UserDefaultsStore.getCustomMoodTint()` per invocation. A full month card calls it 10 times. - Impact: For custom tint users, every cell render invokes multiple JSON decode cycles. 365 cells = thousands of UserDefaults reads before first frame. **`Shared/Views/DemoAnimationManager.swift`:91-95** | PERFORMANCE | `Timer.scheduledTimer` at 1/60s spawns new `Task { @MainActor in }` per tick — tasks queue faster than consumed - What: Timer fires every `1/60` second; each fire wraps `updateProgress()` in `Task { @MainActor in ... }`. Under load, Tasks queue up and multiple `updateProgress()` calls fire for the same frame. - Impact: Animation progress applied multiple times per visual frame causing stutters, jumps, or skips. **`FeelsWidget2/FeelsVoteWidget.swift`:55-176** | PERFORMANCE | `moodTint` and `moodImages` computed props each call `UserDefaultsStore` on every body access - What: `private var moodTint` and `private var moodImages` are computed properties triggering UserDefaults reads on every access. Called multiple times within `body`. - Impact: 4–6 UserDefaults reads plus potential JSON decoding per widget render cycle. Increases likelihood of exceeding WidgetKit render budget. **`FeelsWidget2/FeelsIconWidget.swift`:50-58** | PERFORMANCE | `customWidget` computed property called twice in body — reads UserDefaults twice per render - What: `customWidget` reads UserDefaults each access; called twice in `body`, doubling I/O per render. - Impact: Doubles UserDefaults I/O per render in the memory-constrained widget extension. **`Shared/MoodLogger.swift`:134,141** | PERFORMANCE | `ISO8601DateFormatter()` instantiated fresh on every call - What: `MoodLogger` creates a new `ISO8601DateFormatter()` at lines 134 and 141 inside date key functions. Called every time the user logs a mood. - Impact: `ISO8601DateFormatter` has similar initialization cost to `DateFormatter`. Should be `static let` properties. **`Shared/Views/ExportView.swift`:177-178** | PERFORMANCE | `DateFormatter` created inside `dateRangeText` computed property on every call - What: `private var dateRangeText: String` creates `DateFormatter` inline. Called from `body` during export progress updates. - Impact: Each `ExportView` body evaluation allocates and discards a `DateFormatter`. **`Shared/Views/AddMoodHeaderView.swift`:343-344** | WARNING | `repeatForever` animation started in `.onAppear` — `.onDisappear` resets value but cannot cancel in-flight repeat - What: `OrbitVotingView.onAppear` starts `repeatForever` animation on `centerPulse`. `.onDisappear` sets `centerPulse = 1.0` nominally stopping it. Rapid navigation can cause animation to restart from intermediate state. Same pattern in `NeonVotingView` lines 553-558. - Impact: Rapid navigation to/from day view causes orbit pulse to visually jump or stutter. **`FeelsWidget2/FeelsTimelineWidget.swift`:236** | PERFORMANCE | `Date(timeIntervalSince1970: 0)` in `LargeWidgetView` fetches ALL entries since Unix epoch - What: Large widget fetches entries with `startDate: Date(timeIntervalSince1970: 0)` — no lower-bound date filter, loading every mood entry the user has ever logged into the widget extension's memory. - Impact: Long-term users with years of data can have thousands of `MoodEntryModel` objects loaded into the widget extension — a severely memory-constrained process. Can be killed by jetsam, resulting in blank widget. **`Shared/Views/MonthView/MonthView.swift`:342-344,362-365** | WARNING | Two separate `.onAppear` modifiers on same view root — ordering not guaranteed - What: Two sequential `.onAppear` modifiers: one fires analytics, one populates `cachedSortedData`. SwiftUI ordering of multiple `.onAppear` handlers is implementation-defined. - Impact: If Apple changes execution order, analytics could fire before data is available. --- ## SOURCE: Security Auditor (33 findings) **`Shared/Analytics.swift`:24** | CRITICAL | Hardcoded PostHog API key committed to source control - What: `private static let apiKey = "phc_3GsB3oqNft8Ykg2bJfE9MaJktzLAwr2EPMXQgwEFzAs"` is a live production analytics key stored as a source-level string literal, permanently captured in git history. - Impact: Any party with the key can inject events into PostHog, poison analytics dashboards, and potentially enumerate device/user data via the PostHog API. **`Shared/GoogleService-Info.plist`:10** | CRITICAL | Firebase/Google API key and project credentials in source control - What: `AIzaSyDRIs9buGGPWtECfyhCgFsNvYzvmQnhDr0` (Firebase API key), `CLIENT_ID`, `GCM_SENDER_ID`, and `GOOGLE_APP_ID` committed in a checked-in plist. Firebase appears to be removed from the codebase but the credential file was not deleted. - Impact: Active Google API key permanently in git history. Allows unauthenticated Firebase Auth, Storage, and GCM calls. Should be revoked in Firebase console and deleted from repository. **`Shared/Analytics.swift`:67-76** | CRITICAL | Session replay captures UI screenshots in production with images unmasked and enabled by default - What: `config.sessionReplayConfig.screenshotMode = true`, `config.sessionReplayConfig.maskAllImages = false`, and `config.captureNetworkTelemetry = true` set in PostHog configuration. Session replay enabled by default for new users (opt-out required, not opt-in). - Impact: PostHog screenshot-mode session replay can capture full screen contents — including mood entries, journal notes, and photos — without user consent presented upfront. Potential GDPR/CCPA violation for sensitive mental health data. **`Shared/Services/BiometricAuthManager.swift`:17** | CRITICAL | `isUnlocked` defaults to `true` — locked content briefly visible before auth runs - What: `@Published var isUnlocked: Bool = true`. Views gated on `isUnlocked` are rendered and visible before the biometric prompt appears. - Impact: On app foreground, mood data is momentarily visible before Face ID/Touch ID challenge completes, even with lock enabled. **`Shared/Services/BiometricAuthManager.swift`:172-175** | CRITICAL | `disableLock()` requires no re-authentication - What: `disableLock()` directly sets `isLockEnabled = false` and `isUnlocked = true` with no biometric or passcode challenge. - Impact: An attacker with brief physical access to an unlocked app can permanently disable privacy lock with a single tap in Settings. **`Shared/Persisence/ExtensionDataProvider.swift`:199-217** | CRITICAL | Non-atomic delete-then-insert can permanently delete user mood data - What: Existing entries are deleted and `try? context.save()` is called (line 205). If this succeeds but the subsequent insert save fails (line 217), the original entries are permanently deleted with no recovery path. - Impact: User loses all mood entries for a given date — the delete was persisted but the insert was not. No error surface, no retry, no rollback. **`Shared/Persisence/DataControllerADD.swift`:13-20** | CRITICAL | Non-atomic delete-save-insert in main app path — same data loss risk - What: `add()` deletes all entries for a date, calls `try? modelContext.save()`, then inserts and calls `saveAndRunDataListeners()`. If the final save fails, deleted entries cannot be recovered. - Impact: User mood data silently lost on save failure during mood logging. **`Shared/Random.swift`:30,32** | CRITICAL | Force-unwrap on `UserDefaults(suiteName:)` for all app group defaults - What: `GroupUserDefaults.groupDefaults` force-unwraps `UserDefaults(suiteName:)!`. Called on almost every UI render cycle and data operation. - Impact: App crashes at startup and on every subsequent access if app group entitlement is misconfigured, revoked, or unavailable. **`Shared/Views/SettingsView/SettingsTabView.swift`:61-66** | CRITICAL | `CustomizeContentView` missing required `@EnvironmentObject` injections — guaranteed crash - What: `CustomizeContentView()` used inside `SettingsTabView` without `.environmentObject(authManager)` or `.environmentObject(iapManager)`. Child views declare `@EnvironmentObject var iapManager: IAPManager`. - Impact: App crashes at runtime with fatal "No ObservableObject of type IAPManager found" when user navigates to Customize tab from Settings. **`Shared/Analytics.swift`:160-163** | WARNING | Privacy-sensitive settings transmitted as super-properties to PostHog on every event - What: `privacy_lock_enabled` and `healthkit_enabled` are registered as super-properties attached to every analytics event, along with theme, icon_pack, voting_layout, day_view_style, mood_shape, and personality_pack values. - Impact: Device configuration and security-feature state attached to every PostHog capture event. May conflict with App Store privacy disclosures if not declared. **`Shared/Analytics.swift`:255-262** | BUG | Race condition in analytics opt-out: SDK can be left opted-out while flag shows opted-in - What: `optIn()` first writes `false` to UserDefaults, then calls `PostHogSDK.shared.optIn()`. If the app crashes between these two lines, inconsistent state persists until next explicit toggle. - Impact: User believes analytics are disabled but events are being dropped, or vice versa. **`Shared/Services/BiometricAuthManager.swift`:108-110** | WARNING | Cancelled biometric prompt silently escalates to passcode without user consent - What: When Face ID throws (user taps Cancel), the catch block unconditionally calls `authenticateWithPasscode()` if `canUseDevicePasscode` is true. - Impact: A user who cancels Face ID expecting to deny access is immediately presented with a passcode prompt — non-standard iOS authentication UX. **`Shared/IAPManager.swift`:206-212** | WARNING | Cached subscription expiration unconditionally trusted offline — no cryptographic verification - What: `cachedSubscriptionExpiration` is a plain `Date` in GroupUserDefaults. If the stored date is in the future, `state = .subscribed(...)` is set without re-verifying with StoreKit. - Impact: GroupUserDefaults accessible to any app in the same app group. Modification of the expiration date grants permanent subscription access. **`Shared/IAPManager.swift`:233-244** | WARNING | `restoreCachedSubscriptionState()` grants `.subscribed` with no expiration from UserDefaults boolean alone - What: If `hasActive` is `true` and `cachedExpiration` is `nil`, grants `state = .subscribed(expirationDate: nil, willAutoRenew: false)` unconditionally. - Impact: `nil` expiration means the widget always shows premium UI regardless of actual entitlement. **`Shared/Models/UserDefaultsStore.swift`:217,221-234** | WARNING | `try?` silently discards all Codable decode errors for persisted user settings - What: JSON decode failures for onboarding data, custom widgets, and custom mood tints all swallowed with `try?`. Functions return default values with no indication stored data was discarded. - Impact: Data corruption in UserDefaults silently loses user customization state with no error message. **`Shared/Models/UserDefaultsStore.swift`:217** | WARNING | Static cache `cachedOnboardingData` has no thread synchronization - What: `private static var cachedOnboardingData: OnboardingData?` is a static mutable variable accessed from both main thread and background tasks. - Impact: Concurrent reads/writes constitute a data race. Can corrupt cache or read partially-initialized state. **`Shared/Random.swift`:48,68,174** | WARNING | Mutable static dictionaries have no thread-safety protection - What: `static var existingWeekdayName`, `static var existingDayFormat`, and `static var textToImageCache` are mutable static dictionaries accessible to any context including widget extension background rendering. - Impact: Concurrent access can corrupt dictionaries, producing wrong weekday names, wrong day formats, or cached images mapped to incorrect keys. **`Shared/Random.swift`:65** | BUG | `monthName(fromMonthInt:)` crashes when `fromMonthInt` is 0 or out of range - What: `monthSymbols[fromMonthInt-1]` with no bounds check. Passing `0` produces `[-1]` — fatal index out of bounds crash. - Impact: Any code path that constructs a month integer from raw/corrupted data will crash the app. **`Shared/Services/WatchConnectivityManager.swift`:117-124** | WARNING | `pendingMoods` array accessed from multiple threads without synchronization - What: Written from Watch main thread and read/mutated from WCSession delegate background queue with no synchronization. - Impact: Concurrent access can corrupt the `pendingMoods` array, causing mood entries to be dropped, duplicated, or invalid indices accessed. **`Shared/Analytics.swift`:67** | WARNING | `captureElementInteractions = true` auto-captures all UI interaction events - What: PostHog auto-captures taps, element names, and screen transitions without explicit event definitions. - Impact: Button labels, navigation paths, and element identifiers sent to PostHog automatically. If any label contains user-generated content, it could be inadvertently captured. **`Shared/Analytics.swift`:552** | BUG | Force-unwrap on Optional after nil check in event payload construction - What: `error != nil ? ["error": error!] : nil` — force-unwrap after explicit nil check is an unsafe pattern. Future refactors could move the force-unwrap outside the conditional. - Impact: Crash if pattern is modified without understanding the guard structure. **`Shared/FeelsApp.swift`:30** | BUG | Force cast `task as! BGProcessingTask` inside BGTaskScheduler handler - What: If system delivers a different task type, crashes at runtime with no recovery. - Impact: App crash during background processing. **`Shared/Services/HealthService.swift`:79-80** | BUG | `isAuthorized` set to `true` regardless of whether HealthKit permission was granted - What: After `try await healthStore.requestAuthorization(toShare:read:)` returns without throwing, `isAuthorized = true` and `isEnabled = true` are set. HealthKit does not throw if user denies permissions. - Impact: App displays "HealthKit connected" and attempts HealthKit saves even when user denied permissions. HealthKit writes silently fail while UI shows sync as enabled. **`Shared/Services/BiometricAuthManager.swift`:105,129,149,167** | WARNING | Biometric auth errors logged via `print()` not structured logger - What: All error paths in auth functions use `print(...)` instead of `AppLogger.biometrics`. Security-relevant error descriptions visible in console in production builds. - Impact: LAError codes visible in crash logs shared with third parties. **`Shared/Services/BiometricAuthManager.swift` (class level)** | WARNING | `isLockEnabled` stored in GroupUserDefaults accessible to widget and watch extensions - What: Lock state is in the shared app group suite, readable and writable by widget and watch extensions. - Impact: A compromised widget/watch extension can read whether privacy lock is enabled and write `false` to disable it, bypassing biometric lock without triggering main app audit trail. **`Shared/IAPManager.swift`:70** | WARNING | `firstLaunchDate` returns `Date()` (now) when key is absent — trial calculation uses wrong time - What: `GroupUserDefaults.groupDefaults.object(forKey: .firstLaunchDate) as? Date ?? Date()`. If key missing (cleared or fresh install), trial window calculated from right now. - Impact: Reinstalling or clearing UserDefaults can permanently reset the trial period, granting a new 30-day trial on demand. **`Shared/Persisence/DataControllerDELETE.swift`:22-40** | WARNING | `deleteLast` and `deleteRandomFromLast` are production-accessible with no authorization gate - What: `deleteRandomFromLast(numberOfEntries:)` randomly deletes user mood entries. Both functions are protocol-level requirements available in production builds with no `#if DEBUG` guard. - Impact: Any code path that accidentally calls `deleteRandomFromLast` irreversibly destroys user mood history with no recovery. **`Shared/Views/SettingsView/SettingsView.swift`:1158** | BUG | Force-unwrap URL from user-controlled path component - What: `URL(string: "shareddocuments://\(url.path)")!` where `url.path` is a runtime-generated file URL path that can contain spaces or special characters. - Impact: App crashes when export path contains any character that invalidates the URL. **`Shared/Views/CustomizeView/CustomizeView.swift`:548-556** | WARNING | NSFW personality pack age gate is commented out - What: The conditional block gating access to the "Rude" personality pack behind an over-18 confirmation has been commented out. All personality packs accessible without any age check. - Impact: Users of all ages can select adult-language notification personality packs. App Store compliance issue if marketed to minors. **`Shared/Onboarding/views/OnboardingSubscription.swift`:139-145** | BUG | Onboarding completion fires unconditionally on subscription sheet dismiss - What: `onDismiss` handler calls `completionClosure(onboardingData)` regardless of whether user subscribed. `onboardingCompleted` analytics fires on both skip and dismiss paths, producing double-fire. - Impact: Onboarding marked complete even if user closed paywall without subscribing. Corrupts onboarding funnel metrics. **No `.xcprivacy` file found in project** | CRITICAL | Missing PrivacyInfo.xcprivacy manifest — App Store compliance violation - What: There is no `.xcprivacy` file anywhere in the project tree. The app uses PostHog session replay (screenshots), CoreMotion accelerometer, HealthKit, photos, notifications, and UserDefaults — all requiring privacy manifest declarations per App Store requirements effective May 2024. - Impact: Apple will reject app updates submitted without a privacy manifest declaring all required reason APIs and third-party SDKs (PostHog). App is out of compliance with current App Store review policy. **`Shared/Analytics.swift`:36-50** | WARNING | Session replay enabled by default — opt-out required, not opt-in - What: `sessionReplayEnabled` returns `true` when key absent (first launch). New user's session screen-recorded from first app open until explicitly disabled in Settings. - Impact: May violate GDPR Article 7 and CCPA, especially given sensitive nature (mental health data) of what is displayed on screen. **`Shared/Services/PhotoManager.swift`:104-134** | WARNING | Synchronous disk I/O for photo loading on main actor - What: `loadPhoto(id:)` and `loadThumbnail(id:)` call `Data(contentsOf: fullURL)` synchronously on the main actor. Photos can be megabytes in size. - Impact: UI freezes for duration of each photo disk read. Visible jank when day view scrolls past photo entries. --- ## Accessibility Auditor (46 findings) | Severity | File | Finding | |----------|------|---------| | CRITICAL | `Shared/Views/LockScreenView.swift`:1615 | No accessibility labels on any element — VoiceOver users cannot unlock the app | | CRITICAL | `Shared/Views/AddMoodHeaderView.swift`:515 | `NeonVotingView` has no `accessibilityElement` container — Canvas decoratives fully traversable | | BUG | `Shared/Views/YearView/YearView.swift`:724 | `YearHeatmapCell` label is only "Mood entry" — no date, mood, or button trait | | BUG | `Shared/Views/YearView/YearView.swift`:718 | `YearHeatmapCell` tappable but has no `.isButton` trait or hint | | BUG | `Shared/Views/CustomizeView/SubViews/PersonalityPackPickerView.swift`:52 | `.alert` inside `ForEach` — only last row's alert fires | | BUG | `Shared/Views/SettingsView/SettingsTabView.swift`:61 | `CustomizeContentView` missing `iapManager`/`authManager` environment objects — guaranteed crash | | BUG | `Shared/Views/SettingsView/LiveActivityPreviewView.swift`:325 | `ImageRenderer` used on background thread — produces corrupt images | | BUG | `Shared/Views/CelebrationAnimations.swift`:109 | `onComplete()` fires after view dismissal — potential duplicate mood entry | | BUG | `Shared/Views/LockScreenView.swift`:1694 | Second biometric failure shows no error — user stuck on lock screen | | BUG | `Shared/Views/Views/IAPWarningView.swift`:56 | `FeelsSubscriptionStoreView` sheet missing `iapManager` `@EnvironmentObject` | | BUG | `Shared/Views/SharingStylePickerView.swift`:206 | `designs[selectedIndex]` without bounds check — crashes when designs is empty | | BUG | `Shared/Views/PhotoPickerView.swift`:151 | `isProcessing = true` mutated from non-`@MainActor` async context | | BUG | `Shared/Views/SharingTemplates/WeekTotalTemplate.swift`:30 | Raw developer string "WeekTotalTemplate body" shown to VoiceOver users | | WARNING | `Shared/Views/MonthView/MonthView.swift`:39 | Weekday headers are single ambiguous chars ("T", "S") — duplicate IDs for VoiceOver | | WARNING | `Shared/Views/MonthView/MonthView.swift`:679 | `.accessibilityHint("Double tap to edit")` is hardcoded English, not localized | | WARNING | `Shared/Views/EntryListView.swift`:97 | Accessibility hints use inline `String(localized:)` — may not translate | | WARNING | `Shared/Views/EntryListView.swift`:560 | Chronicle style hardcoded English strings not localized | | WARNING | `Shared/Views/EntryListView.swift`:722 | Neon style renders `"NO_DATA"` raw developer string | | WARNING | `Shared/Views/EntryListView.swift`:2242 | Accelerometer starts on `.onAppear`, never stopped | | WARNING | `Shared/Views/DayView/DayView.swift`:401 | "SIDE A" hardcoded English decoration announced by VoiceOver | | WARNING | `Shared/Views/DayView/DayView.swift`:556 | "avg" abbreviation not localized, no accessibility label override | | WARNING | `Shared/Views/FeelsSubscriptionStoreView.swift`:129 | All paywall marketing copy hardcoded English — not localized | | WARNING | `Shared/Views/FeelsSubscriptionStoreView.swift`:51 | `dismiss()` in async Task after status check — may dismiss wrong view | | WARNING | `Shared/Onboarding/views/OnboardingTime.swift`:65 | `DatePicker` hardcoded `.colorScheme(.light)` — dark mode users see jarring white picker | | WARNING | `Shared/Onboarding/views/OnboardingTime.swift`:87 | Reminder time sentence is hardcoded English, not localizable | | WARNING | `Shared/Onboarding/views/OnboardingCustomizeOne.swift`:39 | `.foregroundColor(.black)` hardcoded — fails contrast in dark mode | | WARNING | `Shared/Onboarding/views/OnboardingCustomizeTwo.swift`:37 | `.foregroundColor(.white)` hardcoded — fails contrast in light mode | | WARNING | `Shared/Onboarding/views/OnboardingStyle.swift`:146 | `UIColor.darkText`/`darkGray` don't adapt to dark mode — WCAG contrast failure | | WARNING | `Shared/Models/Theme.swift`:191 | `AlwaysLight.bgColor` returns dark background in dark system mode | | WARNING | `Shared/Views/NoteEditorView.swift`:59 | Navigation title "Journal Note" hardcoded English | | WARNING | `Shared/Views/NoteEditorView.swift`:91 | Mood icon in entry header has no accessibility label | | WARNING | `Shared/Views/InsightsView/InsightsView.swift`:97 | Pull-to-refresh calls `refreshInsights()` without `await` — spinner dismisses before completion | | WARNING | `Shared/Views/CustomizeView/SubViews/VotingLayoutPickerView.swift`:25 | "Voting Layout" section title hardcoded English | | WARNING | `Shared/Views/CustomizeView/CustomizeView.swift`:168 | Customize rows use `.onTapGesture` without `.isButton` trait or hint | | WARNING | `Shared/Views/CustomizeView/SubViews/ImagePackPickerView.swift`:5 | Icon pack rows have label only — no hint, no `.isButton` trait | | WARNING | `Shared/Views/CustomizeView/SubViews/ShapePickerView.swift`:43 | Shape picker preview uses `randomElement()!` — accessibility label changes unpredictably | | WARNING | `Shared/Views/CustomizeView/SubViews/DayFilterPickerView.swift`:12 | Singleton `DaysFilterClass.shared` in `@StateObject` — SwiftUI lifecycle bypassed | | WARNING | `Shared/Views/SettingsView/SettingsView.swift`:1733 | Debug button misspelling "luanch" announced verbatim by VoiceOver | | WARNING | `Shared/Views/SettingsView/SettingsView.swift`:1817 | Force-unwrap URL literals for Privacy Policy and EULA buttons | | WARNING | `Shared/Views/Views/ExportView.swift`:100 | "Export Data" and "Cancel" hardcoded English | | WARNING | `Shared/Views/Views/ExportView.swift`:319 | Error message "Failed to create export file" hardcoded English | | WARNING | `Shared/Views/MonthView/MonthDetailView.swift`:40 | Share image rendered synchronously on main thread — blocks UI during VoiceOver share | | WARNING | `Shared/Views/PurchaseButtonView.swift`:127 | "Payment Issue" badge text hardcoded English | | WARNING | `Shared/Views/CustomizeView/SubViews/IconPickerView.swift`:73 | `setAlternateIconName` error silently swallowed — no user feedback | | WARNING | `Shared/Views/CustomizeView/SubViews/IconPickerView.swift`:89 | `.fill().foregroundColor()` incorrect modifier order — wrong selection color | | WARNING | `Shared/Utilities/AccessibilityHelpers.swift`:74 | `.accessibilityHint("")` applied with empty string — VoiceOver may announce blank hint | | WARNING | `Shared/Utilities/AccessibilityHelpers.swift`:33 | `value: UUID()` on animation triggers on every render | | WARNING | `Shared/Utilities/AccessibilityHelpers.swift`:13 | Custom `\.reduceMotion` env key defined but never read | | WARNING | `Shared/Views/SharingTemplates/Variations/CurrentStreakVariations.swift`:210 | "Last 10 Days" hardcoded, inaccurate when fewer entries | | WARNING | `Shared/Views/AddMoodHeaderView.swift`:700 | `NeonBarButtonStyle` ignores `accessibilityReduceMotion` | | WARNING | `Shared/Models/MoodTintable.swift`:78 | All 5 custom colors default to same `#a92b26` — indistinguishable for color-blind users | --- ## Energy/Battery Auditor (23 findings) | Severity | File | Finding | |----------|------|---------| | CRITICAL | `Shared/Views/EntryListView.swift`:2224 | `CMMotionManager` started at 30 Hz in `.onAppear`, `stop()` never called — continuous battery drain | | CRITICAL | `Shared/DemoAnimationManager.swift`:91 | 60 Hz Timer spawns new `Task` on every tick — up to 18,000 Task allocations over animation lifetime | | CRITICAL | `Shared/BGTask.swift`:14 | Background task annotated `@MainActor` — Core Data fill runs on main thread, defeats background intent | | CRITICAL | `Shared/Views/SettingsView/LiveActivityPreviewView.swift`:325 | `ImageRenderer` called on `DispatchQueue.global` — threading violation, corrupt images | | BUG | `Shared/DemoAnimationManager.swift`:54 | `DispatchQueue.main.asyncAfter` callbacks not cancellable — multiple timers accumulate on rapid restarts | | BUG | `Shared/MoodStreakActivity.swift`:215 | Infinite loop if `Calendar.date(byAdding:)` returns nil — main actor hang | | BUG | `Shared/FeelsApp.swift`:92 | `Task.detached { @MainActor in }` defeats detached — all Core Data work on main thread | | BUG | `Shared/Views/LockScreenView.swift`:1711 | `try? Task.sleep` swallows `CancellationError` — auth prompt after view disappears | | BUG | `Shared/Views/CelebrationAnimations.swift`:109 | `DispatchQueue.asyncAfter` fires mood-save after view dismissal — potential duplicate entries | | BUG | `Shared/Views/SettingsView/LiveActivityPreviewView.swift`:89 | `animationTimer` not nil'd after invalidation — dangling timer reference | | BUG | `Shared/Services/PhotoManager.swift`:104 | Synchronous disk I/O on `@MainActor` — blocks scroll frame budget on photo loads | | BUG | `Shared/Views/FeelsSubscriptionStoreView.swift`:51 | `dismiss()` called after async op in stale Task — paywall may not dismiss after purchase | | PERFORMANCE | `Shared/MoodStreakActivity.swift`:306 | Two long-duration Timers scheduled without `.common` run-loop mode — freeze during scroll | | PERFORMANCE | `Shared/Views/EntryListView.swift`:2249 | Motion callbacks on `.main` queue with `withAnimation` at 30 Hz — 30 animation transactions/sec | | PERFORMANCE | `Shared/Views/InsightsView/InsightsViewModel.swift`:88 | `withTaskGroup` children all `@MainActor` — zero actual concurrency, 3× slower | | PERFORMANCE | `Shared/Views/InsightsView/InsightsViewModel.swift`:60 | `generateInsights()` spawns new Task on every call — concurrent stale LLM requests | | PERFORMANCE | `Shared/Views/FeelsSubscriptionStoreView.swift`:164 | `repeatForever` animations not cancelled on dismiss — GPU work during teardown | | PERFORMANCE | `Shared/Views/AddMoodHeaderView.swift`:343 | `OrbitVotingView`/`NeonVotingView` `repeatForever` pulse — conflicting states on rapid layout switches | | PERFORMANCE | `FeelsWidget2/WidgetProviders.swift`:127 | Widget reload policy `now+10s`, first entry dated `now+15s` — unnecessary 5-second early reload | | PERFORMANCE | `FeelsWidget2/FeelsTimelineWidget.swift`:73 | `DateFormatter` computed property — new allocation on every widget render (2–4 per render) | | PERFORMANCE | `FeelsWidget2/FeelsVoteWidget.swift`:68 | `DateFormatter` and multi-UserDefaults JSON decodes per widget render | | PERFORMANCE | `Shared/Utilities/AccessibilityHelpers.swift`:33 | `value: UUID()` on `.animation` triggers animation on every render pass | | PERFORMANCE | `Shared/Views/YearView/YearViewModel.swift`:44 | `filterEntries` double-clear causes two empty-state flashes + synchronous Core Data on main thread | --- ## Storage Auditor (28 findings) | Severity | File | Finding | |----------|------|---------| | CRITICAL | `Shared/Services/PhotoManager.swift`:26 | Photos stored in App Group container root without `.isExcludedFromBackupKey` — double-backed up to iCloud | | CRITICAL | `Shared/Persisence/SharedModelContainer.swift`:86 | SwiftData store at App Group container root — wrong location, not guaranteed preserved on migration | | CRITICAL | `Shared/Persisence/ExtensionDataProvider.swift`:91 | Widget opens same SQLite store as main app with `cloudKitDatabase: .none` — incompatible CloudKit configs | | CRITICAL | `Shared/SharedMoodIntent.swift`:93 | New `ModelContainer` per widget intent invocation — concurrent writers, WAL contention | | CRITICAL | `Shared/Random.swift`:28 | Force-unwrap `UserDefaults(suiteName:)!` — crash if App Group entitlement unavailable | | CRITICAL | `Shared/Models/UserDefaultsStore.swift`:226 | App Group UserDefaults unprotected — `hasActiveSubscription`, `privacyLockEnabled` readable while locked | | CRITICAL | `Shared/Persisence/DataControllerADD.swift`:13 | Non-atomic delete-save-insert — data loss if second save fails | | CRITICAL | `Shared/Persisence/ExtensionDataProvider.swift`:204 | Same non-atomic delete-save-insert in widget extension — higher failure risk in memory-constrained process | | CRITICAL | `Shared/Analytics.swift`:24 | PostHog API key hardcoded in source — permanently in git history | | CRITICAL | `Shared/Persisence/DataController.swift`:15 | `DataController.shared` static let on `@MainActor` class — init can run off-main-actor | | BUG | `Shared/Services/PhotoManager.swift`:34 | Photos directory at App Group container root — may be lost during OS migration | | BUG | `Shared/Services/PhotoManager.swift`:95 | Thumbnail write errors silently swallowed; thumbnails not excluded from backup | | BUG | `Shared/Persisence/SharedModelContainer.swift`:66 | Silent fallback to in-memory storage when App Group unavailable — all data ephemeral | | BUG | `Shared/IAPManager.swift`:68 | `firstLaunchDate` falls back to `Date()` on every cold launch — trial clock resets | | BUG | `Shared/IAPManager.swift`:206 | Offline subscription fallback reads unprotected App Group plist — editable by attacker | | BUG | `Shared/Services/BiometricAuthManager.swift`:17 | `isUnlocked` defaults to `true` — content briefly visible before authentication completes | | BUG | `Shared/Views/SettingsView/SettingsView.swift`:1158 | `URL(string: "shareddocuments://\(url.path)")!` — crashes on paths with special characters | | BUG | `Shared/Services/ImageCache.swift`:24 | `NotificationCenter.addObserver` token leaked — observer never removed | | WARNING | `Shared/Services/ExportService.swift`:80 | Export files written to `temporaryDirectory` without cleanup — orphaned files accumulate | | WARNING | `Shared/Services/ExportService.swift`:171 | `temporaryDirectory` file may be purged before share sheet completes | | WARNING | `Shared/Models/UserDefaultsStore.swift`:226 | All prefs including sensitive values stored in shared App Group defaults — too broad surface | | WARNING | `Shared/Models/UserDefaultsStore.swift`:217 | Static `cachedOnboardingData` with no thread synchronization — data race | | WARNING | `Shared/Models/UserDefaultsStore.swift`:309 | Deprecated `UserDefaults.synchronize()` called — no-op since iOS 12 | | WARNING | `Shared/Models/OnboardingDataDataManager.swift`:27 | Second `synchronize()` call site | | WARNING | `Shared/Services/ReviewRequestManager.swift`:74 | Review state in `UserDefaults.standard` — moods logged via widget never increment counter | | WARNING | `Shared/Analytics.swift`:28 | Analytics opt-out state in UserDefaults — no file protection level enforcement | | WARNING | `Shared/Services/PhotoManager.swift`:187 | `UIGraphicsBeginImageContextWithOptions` deprecated in iOS 17 | | WARNING | `FeelsWidget2/FeelsTimelineWidget.swift`:236 | Widget fetches from Unix epoch — loads entire history into 30 MB widget extension memory limit | --- ## iCloud/CloudKit Auditor (22 findings) | Severity | File | Finding | |----------|------|---------| | CRITICAL | `Shared/Persisence/SharedModelContainer.swift`:34 | Main app and widget write to same SQLite file with different CloudKit configs — unsupported, can corrupt store | | CRITICAL | `Shared/SharedMoodIntent.swift`:93 | New `ModelContainer` per widget intent — concurrent SQLite writers, WAL uncommitted frames | | CRITICAL | `Shared/SharedMoodIntent.swift`:109 | Non-atomic delete-save-insert — CloudKit can sync delete before insert arrives on other device | | CRITICAL | `Shared/Persisence/DataControllerADD.swift`:19 | Delete-save-insert non-atomic — CloudKit push between two saves causes remote device to lose entry | | CRITICAL | `Shared/Persisence/ExtensionDataProvider.swift`:198 | Watch extension same non-atomic pattern with CloudKit enabled — data loss during poor connectivity | | CRITICAL | `Shared/Persisence/DataControllerGET.swift`:74 | Infinite loop in `calculateStreak` — main actor deadlock from nil Calendar result | | BUG | `Shared/Persisence/DataController.swift`:78 | `refreshFromDisk()` calls `modelContext.rollback()` — discards pending CloudKit-delivered changes | | BUG | `Shared/FeelsApp.swift`:92 | `Task.detached { @MainActor in }` runs Core Data work on main thread — blocks during CloudKit merges | | BUG | `Shared/Persisence/ExtensionDataProvider.swift`:61 | Widget opens CloudKit-managed store as local-only — corrupts `_cloudkit_metadata` table | | BUG | `Shared/Persisence/DataController.swift`:15 | Static `DataController.shared` on `@MainActor` — init can run on wrong thread | | BUG | `Shared/Persisence/DataController.swift`:55 | `saveAndRunDataListeners()` runs listeners even when `save()` fails — UI shows unsaved changes | | BUG | `Shared/Persisence/DataController.swift`:51 | `addNewDataListener` grows unboundedly — stale closures execute on every CloudKit refresh | | BUG | `Shared/BGTask.swift`:22 | `@MainActor` BGTask marks complete before async `processPendingSideEffects()` finishes | | BUG | `Shared/BGTask.swift`:36 | Force-unwrap on Calendar date in BGTask — crashes background scheduler | | BUG | `Shared/Persisence/SharedModelContainer.swift`:66 | Silent fallback to in-memory on App Group unavailability — CloudKit sync never runs | | WARNING | `FeelsWidgetExtension.entitlements`:1 | Widget declares CloudKit entitlement it must never use — potential App Store rejection | | WARNING | `FeelsWidgetExtension.entitlements`:1 | Wrong entitlement file selection silently falls back to in-memory storage | | WARNING | `Shared/Persisence/ExtensionDataProvider.swift`:84 | Debug/release group mismatch can cause nil container URL — in-memory fallback | | WARNING | `Shared/Models/MoodEntryModel.swift`:22 | No versioned SwiftData schema migration — enum reordering silently corrupts all CloudKit records | | WARNING | `Shared/Persisence/DataControllerADD.swift`:46 | `+12 hours` timezone hack in `fillInMissingDates` — dates may land on wrong day after CloudKit sync | | WARNING | `Shared/Persisence/DataControllerGET.swift`:18 | `entry.forDate <= endDate` inclusive bound — entries at exact midnight of next day double-counted | | WARNING | `Shared/Models/UserDefaultsStore.swift`:217 | `cachedOnboardingData` data race — wrong dates used for gap-filling, synced to CloudKit | --- ## SwiftUI Architecture Auditor (49 findings — selected) | Severity | File | Finding | |----------|------|---------| | CRITICAL | `Shared/Views/SettingsView/SettingsTabView.swift`:62 | `CustomizeContentView` missing `iapManager`/`authManager` environment objects — crash on Customize tab | | CRITICAL | `Shared/Views/MonthView/MonthView.swift`:29 | Singleton `OnboardingDataDataManager.shared` wrapped in `@StateObject` — SwiftUI lifecycle violated | | CRITICAL | `Shared/Views/MonthView/MonthView.swift`:49 | Singleton `DemoAnimationManager.shared` in `@StateObject` — conflicting owner semantics | | CRITICAL | `Shared/Views/CustomizeView/SubViews/DayFilterPickerView.swift`:12 | Singleton `DaysFilterClass.shared` in `@StateObject` — changes may not propagate | | CRITICAL | `Shared/Persisence/DataControllerGET.swift`:74 | Infinite loop in `calculateStreak` — `?? checkDate` nil-coalescing never advances | | CRITICAL | `Shared/MoodStreakActivity.swift`:215 | Identical infinite loop in `LiveActivityScheduler.calculateStreak` | | CRITICAL | `Shared/AppShortcuts.swift`:136 | `while true` streak loop on `@MainActor` — thousands of Core Data fetches for long-streak users | | CRITICAL | `Shared/Views/MoodEntryFunctions.swift`:25 | `newGrouped[year] = newMonth` inside inner loop — all months but last dropped per year | | BUG | `Shared/Views/DayView/DayViewViewModel.swift`:29 | Double force-unwrap `year![$0]!.count` in `numberOfEntries` | | BUG | `Shared/Views/DayView/DayViewViewModel.swift`:115 | Force-unwrap on `DateComponents.month!`/`.year!` | | BUG | `Shared/Views/YearView/YearViewModel.swift`:27 | `updateData()` only sets dates, never populates `data` or `entriesByYear` — misleading name | | BUG | `Shared/Views/YearView/YearViewModel.swift`:48 | Double `removeAll()` causes two empty-state re-renders with empty dict | | BUG | `Shared/Views/YearView/YearView.swift`:334 | `YearCard.Equatable` compares only `entries.count` — same count with different moods prevents re-render | | BUG | `Shared/Views/MonthView/MonthView.swift`:413 | `MonthCard.Equatable` excludes `demoManager` — demo animations silently stop | | BUG | `Shared/Views/MonthView/MonthView.swift`:176 | `ForEach id: \.element.month` collides across years — SwiftUI drops or misidentifies month cards | | BUG | `Shared/Views/InsightsView/InsightsViewModel.swift`:59 | Unstructured task on `generateInsights()` — race condition with concurrent task mutations | | BUG | `Shared/Views/InsightsView/InsightsViewModel.swift`:77 | Force-unwrap on Calendar `dateComponents` — crash on non-Gregorian calendars | | BUG | `Shared/Views/Views/PersonalityPackPickerView.swift`:52 | `.alert` inside `ForEach` — only last row's alert fires | | BUG | `Shared/Views/CustomizeView/SubViews/DayFilterPickerView.swift`:21 | `addFilter`/`removeFilter` are no-ops — buttons do nothing silently | | BUG | `Shared/Views/CelebrationAnimations.swift`:109 | `DispatchQueue.asyncAfter` mood-save fires after dismissal — ghost/duplicate entries | | BUG | `Shared/BGTask.swift`:42 | Error interpolation typo `(error)` — scheduling failures invisible | | BUG | `Shared/BGTask.swift`:14 | `@MainActor` BGTask runs Core Data fill on main thread | | BUG | `Shared/Models/MoodEntryFunctions.swift`:25 | Inner loop overwrites `newGrouped[year]` — confirmed data display bug for multi-year users | | BUG | `Shared/Views/AddMoodHeaderView.swift`:20 | `@State var onboardingData` captures value once — stale header after Settings changes | | BUG | `Shared/Onboarding/views/OnboardingMain.swift`:12 | Reference-type `OnboardingData` wrapped in `@State` — changes not observed | | BUG | `Shared/Onboarding/views/OnboardingSubscription.swift`:139 | Onboarding completion fires on any sheet dismiss — including cancel | | BUG | `Shared/Models/BGView.swift`:76 | `BGView.Equatable ==` hardcoded to `true` — view never re-renders on prop changes | | BUG | `Shared/Models/BGView.swift`:52 | `randomMood` computed — new random value on every render, background flickers | | BUG | `Shared/Random.swift`:65 | `monthSymbols[fromMonthInt-1]` — out-of-bounds crash when `fromMonthInt == 0` | | BUG | `Shared/Views/SharingTemplates/LongestStreakTemplate.swift`:46 | Non-`@MainActor` `init` calls `@MainActor` `configureData()` — Swift 6 violation, no-op writes | | BUG | `Shared/Services/LiveActivityPreviewView.swift`:325 | `ImageRenderer` on background thread — corrupt images | | WARNING | `Shared/Views/DayView/DayView.swift`:17 | Dead `@AppStorage deleteEnabled` — every UserDefaults write triggers needless body re-evaluation | | WARNING | `Shared/Views/DayView/DayView.swift`:32 | Dead `@State showTodayInput` | | WARNING | `Shared/Views/YearView/YearView.swift`:13 | Dead `@State toggle = true` | | WARNING | `Shared/Views/MonthView/MonthDetailView.swift`:21 | Duplicate `showingUpdateEntryAlert`/`showUpdateEntryAlert` booleans — dead state | | WARNING | `Shared/Views/EntryListView.swift`:2073 | `@ObservedObject` on singleton in struct — hundreds of observation points | | WARNING | `Shared/Views/InsightsView/InsightsViewModel.swift`:88 | `withTaskGroup` children all `@MainActor` — serialized, no concurrency benefit | | WARNING | `Shared/Views/CustomizeView/CustomizeView.swift`:295 | Hidden `Text` hack to force re-renders — fragile, may be optimized away by Apple | | WARNING | `Shared/Views/MonthView/MonthView.swift`:342 | Two `.onAppear` modifiers on same view | | WARNING | `Shared/Views/SettingsView/SettingsView.swift`:94 | `OnboardingMain` sheet missing `iapManager` environment object injection | | WARNING | `Shared/IAPManager.swift`:289 | `try?` on subscription status silently grants premium on StoreKit errors | | WARNING | `Shared/IAPManager.swift`:40 | `#if DEBUG` bypass block is dead code — both branches are `false` | | WARNING | `Shared/Services/WatchConnectivityManager.swift`:117 | `pendingMoods` data race — WCSession delegate vs main thread | | WARNING | `Shared/Services/PhotoManager.swift`:104 | Synchronous disk I/O on `@MainActor` — main thread blocked during photo loads | | WARNING | `Shared/Services/ImageCache.swift`:24 | `NotificationCenter` observer token leaked | | WARNING | `Shared/Views/SettingsView/SettingsTabView.swift`:157 | `NavigationView` deprecated — iPad shows unwanted split layout | | WARNING | `Shared/Utilities/AccessibilityHelpers.swift`:33 | `value: UUID()` triggers animation on every render | | WARNING | `Shared/Onboarding/views/OnboardingTime.swift`:13 | `DateFormatter` computed property re-allocates on every DatePicker tick | | WARNING | `Shared/Views/NoteEditorView.swift`:474 | Force-unwrap `entry.photoID!` despite `if let` guard present | --- ## Swift Performance Auditor (31 findings) | Severity | File | Finding | |----------|------|---------| | CRITICAL | `Shared/AppShortcuts.swift`:128 | `while true` on `@MainActor` — one Core Data fetch per day, up to 1000+ for long streaks | | CRITICAL | `Shared/Persisence/DataControllerGET.swift`:74 | Infinite loop if `calendar.date(byAdding:)` returns nil in streak calculation | | CRITICAL | `Shared/MoodStreakActivity.swift`:215 | Same infinite loop in `LiveActivityScheduler.calculateStreak` | | CRITICAL | `FeelsWidget2/FeelsTimelineWidget.swift`:236 | Widget fetches from Unix epoch — entire history in memory-constrained extension process | | PERFORMANCE | `Shared/Views/YearView/YearViewModel.swift`:36 | O(n log n) sort to find minimum date — use `min(by:)` instead | | PERFORMANCE | `Shared/Views/YearView/YearViewModel.swift`:48 | Double `removeAll()` on `@Published` dicts — two re-renders with empty state | | PERFORMANCE | `Shared/Views/YearView/YearViewModel.swift`:44 | Synchronous Core Data + O(n) grid build on `@MainActor` on every filter change | | PERFORMANCE | `Shared/Random.swift`:91 | `createTotalPerc` runs 5 separate O(n) filters — use single-pass histogram | | PERFORMANCE | `Shared/Views/InsightsView/InsightsViewModels/MoodDataSummarizer.swift`:213 | Two full O(n log n) sorts in one function call on already-sorted input | | PERFORMANCE | `Shared/Views/InsightsView/InsightsViewModels/MoodDataSummarizer.swift`:265 | Redundant sort inside `calculateMoodStreak` called on already-sorted input | | PERFORMANCE | `Shared/Models/MoodTintable.swift`:109 | `getCustomMoodTint()` called 10 times per render — each call decodes JSON from UserDefaults | | PERFORMANCE | `Shared/Views/ChartDataBuildable.swift`:31 | String interpolation for dict key inside inner loop — 1000 String allocs for 1000 entries | | PERFORMANCE | `Shared/MoodLogger.swift`:134 | `ISO8601DateFormatter()` allocated fresh on every `markSideEffectsApplied`/`sideEffectsApplied` | | PERFORMANCE | `FeelsWidget2/FeelsTimelineWidget.swift`:73 | `DateFormatter` as computed property — new allocation on every widget render (×3 sizes) | | PERFORMANCE | `FeelsWidget2/FeelsVoteWidget.swift`:68 | `DateFormatter` inside computed `votingDateString` + multiple UserDefaults decodes per render | | PERFORMANCE | `Shared/Views/DemoAnimationManager.swift`:91 | 60 Hz Timer spawning new `Task` per tick — tasks queue up under load | | PERFORMANCE | `Shared/Utilities/AccessibilityHelpers.swift`:33 | `value: UUID()` triggers animation on every render pass | | PERFORMANCE | `Shared/Onboarding/views/OnboardingTime.swift`:13 | `DateFormatter` computed on DatePicker view — new allocation every ~16ms during scroll | | PERFORMANCE | `Shared/Views/Views/ExportView.swift`:177 | `DateFormatter` re-created on every `dateRangeText` access | | PERFORMANCE | `Shared/Views/CustomizeView/SubViews/ImagePackPickerView.swift`:50 | O(n log n) sort inside `ForEach` per row to find max rawValue — use `max(by:)` | | PERFORMANCE | `Shared/Views/CustomizeView/SubViews/PersonalityPackPickerView.swift`:64 | Same sorted-allCases anti-pattern | | PERFORMANCE | `Shared/Views/InsightsView/InsightsViewModels/MoodDataSummarizer.swift`:158 | `weekdayNames[weekday - 1]` — no bounds check, crashes on weekDay == 0 | | PERFORMANCE | `Shared/Random.swift`:28 | `groupDefaults` computed static var — `UserDefaults(suiteName:)` called on every access | | PERFORMANCE | `Shared/Views/DateFormattingCache.swift`:263 | `DateFormattingCache.shared` accessed from multiple threads with no synchronization | | PERFORMANCE | `Shared/Views/BGView.swift`:52 | `randomMood` computed — new random value every render, all 40–100 background cells re-randomize | | PERFORMANCE | `Shared/Views/SettingsView/SettingsView.swift`:1158 | Force-unwrap URL from runtime path — crash if path has special characters | | PERFORMANCE | `Shared/Random.swift`:48 | `existingWeekdayName`/`existingDayFormat` mutable static dicts — no thread synchronization | | PERFORMANCE | `Shared/Random.swift`:174 | `textToImageCache` static dict — no thread synchronization, concurrent widget + app rendering | | PERFORMANCE | `Shared/Services/HealthService.swift`:440 | 365 concurrent HealthKit task group tasks — floods cooperative thread pool | | PERFORMANCE | `Shared/Persisence/DataControllerGET.swift`:89 | `splitIntoYearMonth` fetches from epoch — loads entire history into memory | | PERFORMANCE | `Shared/Views/InsightsView/InsightsViewModel.swift`:88 | `withTaskGroup` child tasks all `@MainActor` — LLM calls run serially not concurrently | --- ## Modernization Auditor (43 findings — selected) | Severity | File | Finding | |----------|------|---------| | CRITICAL | `Shared/Analytics.swift`:24 | PostHog API key hardcoded in source — permanently in git history | | CRITICAL | `Shared/Views/SettingsView/SettingsTabView.swift`:62 | `CustomizeContentView` missing environment objects — crash | | CRITICAL | `Shared/Views/CelebrationAnimations.swift`:109 | `DispatchQueue.asyncAfter` mood-save fires after dismissal — duplicate entries | | CRITICAL | `Shared/MoodStreakActivity.swift`:215 | Infinite loop in streak calculation | | CRITICAL | `Shared/Persisence/DataControllerGET.swift`:74 | Infinite loop in streak calculation | | CRITICAL | `Shared/AppShortcuts.swift`:136 | `while true` streak loop on main actor | | CRITICAL | `Shared/Views/MoodEntryFunctions.swift`:25 | Inner loop overwrites year grouping — all months but last lost | | CRITICAL | `Shared/Views/MonthView/MonthView.swift`:176 | `ForEach id: \.month` ID collision across years | | BUG | `Shared/IAPManager.swift`:289 | `try?` grants premium on StoreKit errors | | BUG | `Shared/BGTask.swift`:42 | `(error)` typo — scheduling failures invisible | | BUG | `Shared/BGTask.swift`:14 | `@MainActor` BGTask runs Core Data on main thread | | BUG | `Shared/FeelsApp.swift`:92 | `Task.detached { @MainActor in }` contradictory pattern | | BUG | `Shared/Services/HealthService.swift`:79 | `isAuthorized = true` unconditional — HealthKit denial treated as success | | BUG | `Shared/Views/PhotoPickerView.swift`:151 | `@State` mutation from non-`@MainActor` async context | | BUG | `Shared/Views/SettingsView/SettingsView.swift`:1158 | Force-unwrap URL with runtime path | | BUG | `Shared/Views/SharingTemplates/LongestStreakTemplate.swift`:52 | Non-`@MainActor` init calling `@MainActor` function | | BUG | `Shared/Onboarding/views/OnboardingSubscription.swift`:139 | Onboarding completion unconditional on dismiss | | BUG | `Shared/Services/WatchConnectivityManager.swift`:117 | `pendingMoods` data race | | WARNING | `Shared/Services/PhotoManager.swift`:187 | `UIGraphicsBeginImageContextWithOptions` deprecated iOS 17 | | WARNING | `Shared/Random.swift`:187 | Second `UIGraphicsBeginImageContext` deprecated call site | | WARNING | `Shared/Models/UserDefaultsStore.swift`:309 | `synchronize()` deprecated no-op — 3 call sites | | WARNING | `Shared/FeelsTips.swift`:257 | `DispatchQueue.main.asyncAfter` inside `.onAppear` — should use `.task { try? await Task.sleep }` | | WARNING | `Shared/Views/NoteEditorView.swift`:84 | `DispatchQueue.asyncAfter` for `@FocusState` — fixed in SwiftUI 5.0+ | | WARNING | `Shared/Views/SettingsView/LiveActivityPreviewView.swift`:325 | `ImageRenderer` on background thread | | WARNING | `Shared/Views/InsightsView/InsightsView.swift`:97 | `refreshInsights()` called without `await` — spinner dismisses before completion | | WARNING | `Shared/Views/InsightsView/InsightsViewModel.swift`:88 | `withTaskGroup` child tasks serialized by `@MainActor` | | WARNING | `Shared/Views/EntryListView.swift`:2242 | `CMMotionManager` never stopped | | WARNING | Multiple files | 8 files use deprecated `@Environment(\.presentationMode)` — replace with `@Environment(\.dismiss)` | | WARNING | Multiple files (10) | `.edgesIgnoringSafeArea(.all)` deprecated — replace with `.ignoresSafeArea()` | | WARNING | `Shared/Models/Theme.swift`:74 | `Themeable` protocol mandates `AnyView` return types — prevents compiler optimization | | WARNING | `Shared/Models/Shapes.swift`:17 | `AnyView` in `BGShape.view()` — defeats SwiftUI structural diffing | | WARNING | `Shared/Views/SwitchableView.swift`:301 | `AnyView` for all switchable view cases | | WARNING | `Shared/Services/HealthService.swift`:154 | `withCheckedContinuation` wrapping HKQuery — errors silently dropped as values | | WARNING | `Shared/Analytics.swift`:615 | `@MainActor` method called from non-isolated ViewModifier closure — Swift 6 error | | WARNING | `Shared/Views/EntryListView.swift`:119 | `.foregroundColor()` soft-deprecated iOS 17 — 719 occurrences across 66 files | | WARNING | 47 files | `PreviewProvider` pattern — replace with `#Preview` macro for faster previews | | WARNING | `Shared/Views/DemoAnimationManager.swift`:91 | Timer + Task wrapping on `@MainActor` — call `updateProgress()` directly | | WARNING | `Shared/Views/YearView/YearViewModel.swift`:11 | `ObservableObject` candidate for `@Observable` migration | | WARNING | `Shared/Views/DayView/DayViewViewModel.swift`:13 | `ObservableObject` candidate for `@Observable` migration | | WARNING | `Shared/Onboarding/OnboardingData.swift`:13 | `@Published` mutations need `@MainActor` protection | | WARNING | `Shared/Views/MonthView/MonthView.swift`:31 | Nested `ObservableObject` class without `@MainActor` | | WARNING | `Shared/Onboarding/views/OnboardingMain.swift`:12 | Reference-type class wrapped in `@State` — should be `@StateObject` | | WARNING | `Shared/Models/MoodTintable.swift`:78 | `SavedMoodTint` has unnecessary `NSObject` inheritance — prevents `@Observable` migration | --- ## Navigation Auditor (27 findings) | Severity | File | Finding | |----------|------|---------| | CRITICAL | `Shared/Views/SettingsView/SettingsTabView.swift`:62 | `CustomizeContentView` missing `iapManager` environment object — crash on Customize tab | | CRITICAL | `Shared/Views/CustomizeView/SubViews/AppThemePickerView.swift`:262 | `theme.previewColors[0]`/`[1]` unchecked subscript — crash if fewer than 2 colors | | CRITICAL | `Shared/Views/Sharing/SharingStylePickerView.swift`:206 | `designs[selectedIndex]` no bounds check in `LongestStreakPickerView` — crash when designs empty | | BUG | `Shared/FeelsApp.swift`:30 | Force cast `task as! BGProcessingTask` — crash if system delivers different task type | | BUG | `Shared/Views/MainTabView.swift`:53 | Onboarding sheet empty `onDismiss` — `needsOnboarding` never set false if system dismisses | | BUG | `Shared/Views/DayView/DayView.swift`:48 | `.sheet(item: $selectedEntry)` — stale entry reference on concurrent data update | | BUG | `Shared/Views/MonthView/MonthView.swift`:176 | `ForEach id: \.element.month` — duplicate IDs across years, months silently dropped | | BUG | `Shared/Views/MonthView/MonthView.swift`:339 | `showSubscriptionStore` sheet missing `iapManager` environment object — crash on month gate | | BUG | `Shared/Views/FeelsSubscriptionStoreView.swift`:47 | `dismiss()` in async Task after status check — stale environment, potential double-dismiss | | BUG | `Shared/Views/SettingsView/SettingsView.swift`:94 | `OnboardingMain` sheet missing `iapManager` — crash on "Show Onboarding" in Settings | | BUG | `Shared/Views/SettingsView/SettingsView.swift`:1158 | Force-unwrap URL from runtime path — crash on special characters | | BUG | `Shared/Views/CelebrationAnimations.swift`:109 | Un-cancellable `asyncAfter` mood-save fires after navigation away — duplicate entries | | BUG | `Shared/Views/CustomizeView/SubViews/AppThemePickerView.swift`:117 | Double-dismiss: `selectedTheme = nil` dismisses sheet, then explicit `dismiss()` also called | | BUG | `Shared/Onboarding/views/OnboardingSubscription.swift`:139 | Onboarding completion unconditional on sheet dismiss | | BUG | `Shared/Views/CustomizeView/CustomizeView.swift`:529 | `as? CustomWidgetModel` cast on `NSCopying.copy()` — nil result presents blank sheet | | BUG | `Shared/Views/DayView/DayViewViewModel.swift`:29 | Double force-unwrap in `numberOfEntries` | | BUG | `Shared/Views/Views/EntryListView.swift` (multiple) | Multiple `.sheet` modifiers on same view — only one reliably presents on iOS 16.4 and earlier | | WARNING | `Shared/Views/DayView/DayView.swift`:46 | `showingSheet` boolean bound to `SettingsView` — never triggered, permanently unreachable dead sheet | | WARNING | `Shared/Views/MainTabView.swift`:18 | `onboardingData` captured as non-reactive `let` — stale after CloudKit sync | | WARNING | `Shared/Views/MainTabView.swift`:79 | `UIApplication.shared.connectedScenes.first` — wrong scene on iPad multi-window | | WARNING | `Shared/Onboarding/views/OnboardingMain.swift`:18 | TabView pager with no navigation guards — user can swipe to completion without filling fields | | WARNING | `Shared/Views/MonthView/MonthView.swift`:350 | Sheet item driven by optional `selectedDetail.selectedItem` — nil content if race between item and show | | WARNING | `Shared/Views/MonthView/MonthDetailView.swift`:85 | Multiple rapid taps on share enqueue multiple `showSheet = true` — stale image in sheet | | WARNING | `Shared/Views/InsightsView/InsightsView.swift`:97 | `refreshInsights()` not awaited — spinner dismisses before load completes | | WARNING | `Shared/Views/InsightsView/InsightsViewModel.swift`:60 | `generateInsights()` no deduplication — concurrent tasks interleave published state | | WARNING | `Shared/Onboarding/views/OnboardingStyle.swift`:70 | `theme.apply()` called immediately on every tap — no rollback if onboarding abandoned | | WARNING | `Shared/Views/NoteEditorView.swift`:32 | Inner `NavigationStack` inside modal sheet — orphaned from app navigation hierarchy | --- ## Testing Auditor (47 findings — selected) | Severity | File | Finding | |----------|------|---------| | CRITICAL | `Tests iOS/Tests_iOS.swift`:1 | Only 2 active unit test methods — cover one utility function; entire business logic layer untested | | CRITICAL | `Tests iOS/Tests_iOS.swift`:1 | Zero Swift Testing (`@Test`/`#expect`) migration — XCTest boilerplate prevents async actor testing | | CRITICAL | `Shared/Persisence/DataController.swift`:15 | `DataController.shared` singleton direct-access in 10+ views — untestable by design | | CRITICAL | `Shared/Persisence/DataControllerProtocol.swift`:1 | `DataControlling` protocol exists but never used as DI type — dead testability infrastructure | | CRITICAL | `Shared/MoodLogger.swift`:16 | `MoodLogger` wires 8 singleton side effects directly — impossible to unit test logging path | | CRITICAL | `Shared/Persisence/DataControllerGET.swift`:74 | Infinite loop in `calculateStreak` — untestable without mock/injectable calendar | | CRITICAL | `Shared/Persisence/DataControllerADD.swift`:13 | Non-atomic delete-save-insert — data loss untestable without mock context | | CRITICAL | `Shared/AppShortcuts.swift`:128 | `while true` streak loop — O(n) Core Data per iteration, untestable without mock | | CRITICAL | `Shared/MoodEntryFunctions.swift`:25 | Inner loop overwrites year grouping — zero test coverage | | CRITICAL | `Shared/ShowBasedOnVoteLogics.swift`:94 | `fatalError` in production code — new `DayOptions` case crashes on launch | | CRITICAL | `Shared/IAPManager.swift`:289 | `try?` grants premium on StoreKit error — zero IAPManager tests | | CRITICAL | `Shared/Views/DayView/DayViewViewModel.swift`:1 | Primary view model directly accesses `DataController.shared` — completely untestable | | BUG | `Shared/Date+Extensions.swift`:16 | Failable `init?(rawValue:)` never returns nil — bad input produces Jan 1, 2001 silently | | BUG | `Shared/Date+Extensions.swift`:93 | `dateRange(monthInt:yearInt:)` hardcodes `-8 hours` — wrong for non-Pacific users | | BUG | `Shared/Date+Extensions.swift`:45 | `toLocalTime()` double-applies timezone offset — wrong dates for non-UTC users | | BUG | `Shared/IAPManager.swift`:40 | `bypassSubscription` is `false` in both `#if DEBUG` and `#else` branches — dead debug scaffold | | BUG | `Shared/BGTask.swift`:42 | Error log typo `(error)` — scheduling failures always log literal text | | BUG | `Shared/Views/YearView/YearViewModel.swift`:27 | `updateData()` doesn't populate `data` — transient blank year on cold launch | | BUG | `Shared/Views/YearView/YearViewModel.swift`:48 | Double clear causes two empty-state flashes per refresh | | BUG | `Shared/Views/MoodEntryFunctions.swift`:43 | Force-unwrap in `padMoodEntriesMonth` — crashes during DST spring-forward | | BUG | `Shared/Onboarding/views/OnboardingSubscription.swift`:139 | Onboarding completion unconditional on dismiss | | BUG | `Shared/Views/PersonalityPackPickerView.swift`:52 | `.alert` in `ForEach` — only last row fires | | BUG | `Shared/Views/MonthView/MonthView.swift`:176 | `ForEach id: \.month` ID collision | | BUG | `Shared/Views/MonthView/MonthView.swift`:661 | `cachedMetrics` only recalculated when empty — stale after mood edits | | BUG | `Shared/Persisence/DataControllerDELETE.swift`:32 | `deleteRandomFromLast` in production code — accidental call irrecoverably deletes user data | | BUG | `Shared/Views/SharingTemplates/LongestStreakTemplate.swift`:46 | Non-`@MainActor` init calls `@MainActor` function — Swift 6 violation | | WARNING | `Shared/Analytics.swift`:24 | PostHog API key hardcoded — no test validates secure key loading | | WARNING | `Shared/Models/UserDefaultsStore.swift`:217 | `cachedOnboardingData` race condition — no concurrency test | | WARNING | `Shared/Views/EntryListView.swift`:2242 | Accelerometer never stopped — no lifecycle test | | WARNING | `Shared/Views/CelebrationAnimations.swift`:109 | `asyncAfter` mood-save fires after dismissal — no test validates animation lifecycle | | WARNING | `Shared/Services/ImageCache.swift`:24 | NotificationCenter observer leaked — no observer lifecycle test | | WARNING | `Shared/Views/AccessibilityHelpers.swift`:33 | `value: UUID()` animation triggers every render — no test | | WARNING | `Shared/Random.swift`:65 | `monthSymbols[fromMonthInt-1]` — no bounds check, called from 15+ views | | WARNING | `Shared/Services/WatchConnectivityManager.swift`:117 | `pendingMoods` data race — no concurrency test | | WARNING | `Shared/Views/SettingsView/LiveActivityPreviewView.swift`:325 | `ImageRenderer` on background thread — no threading test | | WARNING | `Shared/Services/ExportService.swift`:688 | Force-unwrap in PDF generation critical path | | WARNING | `Shared/IAPManager.swift`:466 | `listenForTransactions` task cancel in `deinit` — singleton never deinits, dead code | | WARNING | `Shared/Views/InsightsView/InsightsViewModel.swift`:60 | Multiple concurrent `generateInsights()` — no concurrent state test | | WARNING | `Shared/Views/InsightsView/InsightsViewModel.swift`:88 | `withTaskGroup` serialized — no performance baseline | | WARNING | `Shared/Services/ReviewRequestManager.swift`:117 | Review request date recorded before dialog appears — no gating logic test | | WARNING | `Shared/Services/HealthService.swift`:79 | `isAuthorized = true` unconditional — no auth state machine test | | WARNING | `Shared/Models/MoodImagable.swift`:87 | All emoji image classes force-unwrap `textToImage()!` — crash under low memory | | WARNING | `Shared/Onboarding/views/OnboardingWrapup.swift`:1 | `OnboardingWrapup` view not wired into `OnboardingMain` — dead code shipped in binary | | WARNING | `Shared/Onboarding/views/OnboardingTitle.swift`:38 | All `OnboardingTitle` button actions commented out — view is a no-op | | WARNING | `Tests macOS/Tests_macOS.swift`:27 | `testExample()` has no assertions — passes trivially | | WARNING | `Tests macOS/Tests_macOS.swift`:34 | `testLaunchPerformance` has no baseline — no regression detection | | WARNING | `Tests iOS/Tests_iOS.swift`:1 | XCTest only — zero Swift Testing adoption | --- ## Build Optimizer Auditor (38 findings) | Severity | File | Finding | |----------|------|---------| | PERFORMANCE | `Shared/Views/EntryListView.swift`:1 | 2277-line file, 20-way `switch` in `body` — serializes compilation, no parallel build benefit | | PERFORMANCE | `Shared/Views/FeelsSubscriptionStoreView.swift`:1 | 2394-line file with 12+ independent marketing structs — entire paywall recompiles on any change | | PERFORMANCE | `Shared/Views/SettingsView/SettingsView.swift`:1 | 2118-line file with two near-duplicate views — duplicated type-checking cost | | PERFORMANCE | `Shared/Views/LockScreenView.swift`:1 | 2035-line file, 30+ types, `AnyView` protocol — type erasure overhead + serialized compile | | PERFORMANCE | `Shared/Views/DayView/DayView.swift`:1 | 934-line view with 16 `@ViewBuilder` section-header methods — large opaque result type graph | | PERFORMANCE | `Shared/Views/MonthView/MonthView.swift`:1 | 860-line view with double `.onAppear` and `ForEach` ID collision adding reconciliation work | | PERFORMANCE | `Shared/Views/CelebrationAnimations.swift`:1 | 850-line animation file — un-cancellable GCD callbacks, persistent `repeatForever` animations | | PERFORMANCE | `Shared/Models/Theme.swift`:74 | `Themeable` protocol requires `AnyView` return types — forces type erasure across all 4 themes | | PERFORMANCE | `Shared/Models/Shapes.swift`:17 | `AnyView` in `BGShape.view()` — defeats SwiftUI structural diffing for all entry cells | | PERFORMANCE | `Shared/Views/SwitchableView.swift`:301 | `AnyView` for all switchable cases — full view replacement instead of identity diff | | PERFORMANCE | `Shared/Views/Views/CustomizeView/CustomizeView.swift`:295 | Hidden `Text` re-render hack — fragile, may be optimized away by Apple | | CRITICAL | `Shared/Persisence/DataControllerGET.swift`:74 | Infinite loop in `calculateStreak` | | CRITICAL | `Shared/MoodStreakActivity.swift`:215 | Identical infinite loop in widget-path | | CRITICAL | `Shared/AppShortcuts.swift`:136 | `while true` streak on `@MainActor` — thousands of fetches for long-streak users | | CRITICAL | `FeelsWidget2/FeelsTimelineWidget.swift`:236 | Widget fetches from Unix epoch — entire history in 30MB widget process | | BUG | `Shared/BGTask.swift`:42 | Error interpolation typo — scheduling failures invisible | | BUG | `Shared/Views/CelebrationAnimations.swift`:109 | Un-cancellable `asyncAfter` mood-save | | BUG | `Shared/SharedMoodIntent.swift`:93 | New `ModelContainer` per widget intent — SQLite contention | | BUG | `Shared/FeelsApp.swift`:92 | `Task.detached { @MainActor in }` — Core Data on main thread at launch | | BUG | `Shared/Views/EntryListView.swift`:2242 | `CMMotionManager` never stopped — continuous 60 Hz drain | | BUG | `Shared/Models/BGView.swift`:52 | `randomMood` computed — all background cells re-randomize on every state change | | PERFORMANCE | `FeelsWidget2/FeelsTimelineWidget.swift`:73 | `DateFormatter` computed property — new instance per widget render | | PERFORMANCE | `FeelsWidget2/FeelsTimelineWidget.swift`:87 | `createViews` + `moodTintable()` called synchronously in widget view `init` | | PERFORMANCE | `FeelsWidget2/FeelsVoteWidget.swift`:68 | `DateFormatter` + multiple UserDefaults JSON decodes per vote widget render | | PERFORMANCE | `FeelsWidget2/WidgetProviders.swift`:127 | Reload policy `now+10s` vs entry date `now+15s` — unnecessary 5-second early reload | | PERFORMANCE | `FeelsWidget2/WidgetModels.swift`:1 | `WatchTimelineView` is `class` — unexpected aliasing in WidgetKit timelines | | PERFORMANCE | `Shared/Views/InsightsView/InsightsViewModel.swift`:88 | `withTaskGroup` child tasks all `@MainActor` — LLM calls serialized not parallel | | PERFORMANCE | `Shared/Views/YearView/YearViewModel.swift`:36 | O(n log n) sort to find minimum date | | PERFORMANCE | `Shared/Views/YearView/YearViewModel.swift`:48 | Double `removeAll()` — two empty-state re-renders | | PERFORMANCE | `Shared/Models/MoodTintable.swift`:112 | `getCustomMoodTint()` called 10× per render — UserDefaults JSON decode on every access | | PERFORMANCE | `Shared/Utilities/AccessibilityHelpers.swift`:33 | `value: UUID()` — animation triggers every render | | PERFORMANCE | `Shared/Onboarding/views/OnboardingTime.swift`:13 | `DateFormatter` computed on DatePicker view — new instance every ~16ms during scroll | | PERFORMANCE | `Shared/MoodLogger.swift`:134 | `ISO8601DateFormatter()` per call to `markSideEffectsApplied` | | PERFORMANCE | `Shared/Random.swift`:28 | `groupDefaults` computed static var — `UserDefaults(suiteName:)` on every hot-path access | | PERFORMANCE | `Shared/Random.swift`:73 | `NumberFormatter` created inside `dayFormat()` on every call | | PERFORMANCE | `Shared/Views/DayView/DayView.swift`:17 | Dead `@AppStorage deleteEnabled` — unnecessary UserDefaults subscription on root list view | | WARNING | `Shared/Views/MonthView/MonthView.swift`:410 | `@ObservedObject demoManager` singleton in `Equatable` struct — excluded from equality check | | WARNING | `Shared/Services/HealthService.swift`:440 | 365 concurrent `group.addTask` for HealthKit — floods cooperative thread pool | --- ## Codable/Networking Auditor (31 findings) | Severity | File | Finding | |----------|------|---------| | CRITICAL | `Shared/Color+Codable.swift`:33 | `encode(to:)` throws at runtime when `cgColor` is nil — semantic colors crash encode | | CRITICAL | `Shared/IAPManager.swift`:289 | `try?` on `subscription.status` — StoreKit error silently grants premium access | | CRITICAL | `Shared/Persisence/DataControllerGET.swift`:74 | Infinite loop in `calculateStreak` when `date(byAdding:)` returns nil | | CRITICAL | `Shared/MoodStreakActivity.swift`:215 | Same infinite loop in `LiveActivityScheduler.calculateStreak` | | CRITICAL | `Shared/Persisence/ExtensionDataProvider.swift`:199 | Non-atomic delete-save-insert — widget mood log can permanently delete entry | | BUG | `Shared/Color+Codable.swift`:84 | `Scanner.scanHexInt64` return value discarded — invalid hex silently produces black | | BUG | `Shared/Color+Codable.swift`:86 | Non-6/8 char hex silently produces black — no validation | | BUG | `Shared/Color+Codable.swift`:137 | `RawRepresentable` for Color drops alpha channel — `@AppStorage` loses transparency | | BUG | `Shared/Onboarding/OnboardingData.swift`:52 | `rawValue` returns `"[]"` on encode failure — next decode produces nil, resets onboarding | | BUG | `Shared/LocalNotification.swift`:21 | `testIfEnabled` completion never called when user denies without error — permanent hang | | BUG | `Shared/Date+Extensions.swift`:16 | Failable `init?(rawValue:)` never returns nil — bad string produces Jan 1, 2001 | | BUG | `Shared/SharedMoodIntent.swift`:109 | `try?` delete-save + non-atomic insert — permanent data loss path | | BUG | `Shared/BGTask.swift`:42 | Error interpolation typo — `(error)` literal always printed | | BUG | `Shared/Services/WatchConnectivityManager.swift`:117 | `pendingMoods` data race — concurrent read/write without synchronization | | BUG | `Shared/Services/ExportService.swift`:688 | Force-unwrap `calendar.date(byAdding:)` in PDF generation | | BUG | `Shared/Models/MoodTintable.swift`:89 | `Color.init(from:)` calls `init(hex:)` which never throws — invalid hex decoded as black | | WARNING | `Shared/Onboarding/OnboardingData.swift`:41 | `try?` in `init?(rawValue:)` silently resets reminder on schema change | | WARNING | `Shared/Models/UserDefaultsStore.swift`:227 | `try?` decode `OnboardingData` silently returns defaults on corruption | | WARNING | `Shared/Models/UserDefaultsStore.swift`:315 | `try?` decode `[CustomWidgetModel]` on failure calls `removeObject` — wipes all user widgets | | WARNING | `Shared/Models/UserDefaultsStore.swift`:324 | `try?` encode widgets — unperisted widgets reappear on next launch | | WARNING | `Shared/Models/UserDefaultsStore.swift`:330 | Double `try?` decode in fallback path | | WARNING | `Shared/Models/UserDefaultsStore.swift`:309 | `synchronize()` deprecated no-op | | WARNING | `Shared/Models/UserDefaultsStore.swift`:251 | `print` only error handling for `JSONEncoder` failures in `saveOnboarding` | | WARNING | `Shared/Models/UserDefaultsStore.swift`:365 | `print` only error handling in `saveCustomWidget` | | WARNING | `Shared/Models/UserDefaultsStore.swift`:395 | `print` only error handling in `deleteCustomWidget` — deleted widget reappears after crash | | WARNING | `Shared/MoodLogger.swift`:73 | `try?` swallows HealthKit save error — sync silently fails without log entry | | WARNING | `Shared/MoodStreakActivity.swift`:139 | `try?` in `getUserRatingTime` — nil blocks all Live Activity scheduling silently | | WARNING | `Shared/LocalNotification.swift`:35 | `try?` in `rescheduleNotifiations` — decode fail silently stops all future notifications | | WARNING | `Shared/Persisence/DataControllerGET.swift`:24 | `try?` on `modelContext.fetch` — database error indistinguishable from "no entry" | | WARNING | `Shared/Persisence/DataControllerADD.swift`:43 | `try?` on fetch in `fillInMissingDates` — silent failure leaves history gaps permanently | | WARNING | `Shared/Analytics.swift`:552 | Force-unwrap after nil check — antipattern, crash risk on future refactor | | WARNING | `Shared/MoodLogger.swift`:134 | `ISO8601DateFormatter()` allocated per call — expensive on hot path | | WARNING | `Shared/Services/ExportService.swift`:49 | `ExportService` formatters not thread-safe — concurrent export causes garbled dates | --- ## Cross-Cutting Deep Audit (20 findings) | Severity | File | Finding | |----------|------|---------| | CRITICAL | `Shared/Persisence/DataControllerGET.swift`:74 | **Infinite loop** — `while true` + `?? checkDate` never advances on nil Calendar result; confirmed in MoodStreakActivity.swift:215 and AppShortcuts.swift:128–140 | | CRITICAL | `Shared/Persisence/DataControllerADD.swift`:13 | **Data loss** — non-atomic delete-save-insert; old entry deleted, new entry not persisted if second save fails; duplicated in ExtensionDataProvider.swift:199 and SharedMoodIntent.swift:109 | | CRITICAL | `Shared/SharedMoodIntent.swift`:93 | **Store corruption** — new `ModelContainer` per widget intent against live SQLite file; `ExtensionDataProvider` correctly caches; intent does not | | CRITICAL | `Shared/BGTask.swift`:42 | **Invisible failures** — `(error)` typo, all scheduling failures print literal text; `@MainActor` annotation runs Core Data on main thread; `setTaskCompleted` fires before async work completes | | CRITICAL | `Shared/IAPManager.swift`:271 | **Revenue leak** — `try?` on `subscription.status` falls through to unconditional `state = .subscribed` on any StoreKit error | | CRITICAL | `Shared/MoodStreakActivity.swift`:38 | **Race condition** — `startStreakActivity` uses an unstructured Task; rapid concurrent calls start multiple Live Activities; orphaned activities persist on Lock Screen | | CRITICAL | `Shared/Persisence/DataControllerADD.swift`:46 | **Data integrity** — future-dated entry causes `fillInMissingDates` to insert entries for every day from now to that future date | | CRITICAL | `Shared/MoodLogger.swift`:108 | **Logic error** — `processPendingSideEffects` uses `getData(includedDays: [])` = "all days", bypassing user's day-filter setting; streak counter differs from filtered day view | | CRITICAL | `Shared/Views/SettingsView/SettingsTabView.swift`:61 | **Guaranteed crash** — `CustomizeContentView` missing `iapManager`/`authManager` environment objects | | CRITICAL | `Shared/Onboarding/views/OnboardingSubscription.swift`:139 | **Onboarding loop** — completion closure fires on every sheet dismiss including cancel; `onboardingCompleted` event fires twice when user visits paywall then uses skip path | | BUG | `Shared/Models/UserDefaultsStore.swift`:217 | **Data race** — static `cachedOnboardingData` accessed from main actor and BGTask without synchronization; wrong dates propagate to CloudKit | | BUG | `Shared/Persisence/DataController.swift`:51 | **Memory leak + stale callbacks** — `editedDataClosure` grows forever, stale closures from deallocated ViewModels called on every save; no removal mechanism | | BUG | `Shared/Views/CelebrationAnimations.swift`:109 | **Ghost entries** — `DispatchQueue.asyncAfter` mood-save fires after view dismissal; duplicate entries possible on rapid navigation | | BUG | `Shared/Persisence/DataControllerGET.swift`:18 | **Off-by-one** — `entry.forDate <= endDate` where `endDate` is midnight of next day; 8 identical predicates across codebase | | BUG | `Shared/Services/WatchConnectivityManager.swift`:117 | **Data race** — `pendingMoods` read/written from WCSession delegate background thread and main thread without any synchronization | | BUG | `Shared/Models/Color+Codable.swift`:33 | **Silent encode crash** — `encode(to:)` throws when `cgColor` is nil for semantic colors; custom theme silently lost | | BUG | `Shared/Date+Extensions.swift`:16 | **Silent bad date** — failable `init?(rawValue:)` never returns nil; bad string produces Jan 1, 2001, breaking trial expiration for first-launch users | | BUG | `Shared/Models/MoodEntryFunctions.swift`:25 | **Data display corruption** — `newGrouped[year] = newMonth` inside inner month loop overwrites all months; confirmed by source read; all months but last invisible for multi-year users | | BUG | `Shared/Persisence/DataController.swift`:55 | **Silent failure** — `saveAndRunDataListeners()` runs listeners even when `save()` fails; UI shows unsaved state as if it were persisted | | BUG | `CLAUDE.md`:24 | **Doc mismatch** — documentation says `Shared/Persistence/` (correct spelling) but actual directory is `Shared/Persisence/` (typo) and files named `DataController*.swift` not `Persistence*.swift` | --- ## Summary ### Finding Counts by Auditor | Auditor | Findings | |---------|----------| | Memory Auditor | 15 | | IAP Auditor | 16 | | Concurrency Auditor | 33 | | SwiftUI Performance Auditor | 33 | | Security Auditor | 33 | | Accessibility Auditor | 51 | | Energy/Battery Auditor | 23 | | Storage Auditor | 28 | | iCloud/CloudKit Auditor | 22 | | SwiftUI Architecture Auditor | 49 | | Swift Performance Auditor | 31 | | Modernization Auditor | 43 | | Navigation Auditor | 27 | | Testing Auditor | 47 | | Build Optimizer Auditor | 38 | | Codable/Networking Auditor | 33 | | Cross-Cutting Deep Audit | 20 | | **TOTAL** | **542** | ### Finding Counts by Severity | Severity | Count | |----------|-------| | CRITICAL | ~85 | | BUG | ~185 | | PERFORMANCE | ~145 | | WARNING | ~127 | ### Files with Most Findings | File | Approx Findings | Primary Issues | |------|-----------------|----------------| | `Shared/Persisence/DataControllerGET.swift` | 8 | Infinite loop, off-by-one predicate, epoch fetch | | `Shared/Views/EntryListView.swift` | 12 | Accelerometer leak, accessibility, performance | | `Shared/Views/SettingsView/SettingsTabView.swift` | 7 | Missing env objects (crash), deprecated APIs | | `Shared/Persisence/DataControllerADD.swift` | 8 | Non-atomic delete-insert, data loss, future-dated entries | | `Shared/IAPManager.swift` | 9 | try? grants premium, trial clock, revocation | | `Shared/Analytics.swift` | 6 | Hardcoded API key, race conditions, concurrency | | `Shared/BGTask.swift` | 6 | @MainActor annotation, typo, timing bugs | | `Shared/Views/MonthView/MonthView.swift` | 10 | ForEach ID collision, singletons in @StateObject | | `Shared/MoodStreakActivity.swift` | 7 | Infinite loop, timer safety, race condition | | `Shared/Models/MoodEntryFunctions.swift` | 3 | Inner loop overwrites year — critical data display bug | --- ## TOP 10 PRIORITIES These are the highest-impact issues to fix first, ordered by severity and blast radius: ### 🔴 P1 — Fix Immediately (Data Loss / Security / Crash) **1. Infinite Loop in Streak Calculation** — `DataControllerGET.swift:74`, `MoodStreakActivity.swift:215`, `AppShortcuts.swift:128–140` - Three separate `while true` + `?? checkDate` loops that never terminate if `Calendar.date(byAdding:)` returns nil - Fix: Replace `while true` with a bounded loop (`for _ in 0..<3650`) or use the batch-fetch + in-memory iteration pattern already implemented in `getCurrentStreak(includedDays:)` (which correctly iterates over pre-fetched array) **2. Non-Atomic Delete-Save-Insert Pattern** — `DataControllerADD.swift:13`, `ExtensionDataProvider.swift:204`, `SharedMoodIntent.swift:109` - Delete old entry → `try? save()` → insert new entry → save. If second save fails, entry permanently gone - Fix: Use SwiftData's rollback capability — only call `save()` once after both the delete and insert; wrap in a `do/catch` and rollback on failure **3. Hardcoded PostHog API Key** — `Analytics.swift:24` - Production API key committed to git history permanently — rotate the key in PostHog immediately - Fix: Rotate key in PostHog dashboard NOW; load from xcconfig/Info.plist not committed to repo **4. `CustomizeContentView` Missing Environment Objects** — `SettingsTabView.swift:61` - Any user navigating to the Customize tab in Settings crashes the app with a fatal missing EnvironmentObject error - Fix: Add `.environmentObject(iapManager).environmentObject(authManager)` to `CustomizeContentView()` at line 62 **5. `try?` on StoreKit Status Grants Premium on Error** — `IAPManager.swift:289` - Any StoreKit network error during subscription status check causes the fallback at line 333 to grant `.subscribed` state - Fix: Replace `try?` with `do { let statuses = try await ... } catch { /* log, set state to unknown */ return false }` ### 🟠 P2 — Fix Before Next Release (Significant Impact) **6. Month Calendar Shows Only Last Month Per Year** — `MoodEntryFunctions.swift:25` - `newGrouped[year] = newMonth` is inside the inner month loop — every iteration overwrites the year entry, leaving only the last month - Fix: Move `newGrouped[year] = newMonth` to after the inner `for key in monthKeys` loop closes **7. ForEach ID Collision Across Years in MonthView** — `MonthView.swift:176` - `id: \.element.month` (integer 1–12) collides across years — January 2023 and January 2024 share ID 1, one is silently dropped - Fix: Use `id: \.element` (the full `WatchTimelineView` object) or a composite ID like `"\(year)-\(month)"` **8. BGTask Scheduling Failure Invisible + Wrong Thread** — `BGTask.swift:14, 42` - `(error)` typo makes all failures invisible; `@MainActor` annotation runs expensive Core Data fill on main thread - Fix: Fix typo (`\(error)`); remove `@MainActor`, dispatch work to background actor **9. `ModelContainer` Created Per Widget Intent** — `SharedMoodIntent.swift:93` - Every widget tap creates a new SQLite connection against the live store — concurrent writers can corrupt WAL state - Fix: Add a `_container: ModelContainer?` lazy cache pattern (already present in `ExtensionDataProvider`) **10. Missing `PrivacyInfo.xcprivacy` Manifest** - App Store requires `PrivacyInfo.xcprivacy` for all apps using `UserDefaults`, `NSUserDefaults`, and third-party SDKs (PostHog) since May 2024 - Fix: Create `PrivacyInfo.xcprivacy` declaring `NSPrivacyAccessedAPICategoryUserDefaults` with reason code `CA92.1` (or applicable), and PostHog's required API types