Files
Reflect/hardening-report.md
treyt d41ba29939 fix: issue #145 - Onboarding subtext
Automated fix by Tony CI v3.
Refs #145

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-26 20:33:47 -06:00

1190 lines
124 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (112) 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: 46 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 (24 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 40100 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:128140 |
| 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:128140`
- 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 112) 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