Add full repo README with feature docs and CBT reflection deep dive

Covers every feature with how-it-works, selling-point framing, and
clickable source links — targeted at both developers and business
partners. Includes an in-depth section on the guided reflection flow
(Socratic templating, cognitive-distortion routing, evidence step).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-14 19:08:12 -05:00
parent f4a3d83304
commit 7683717240

476
README.md Normal file
View File

@@ -0,0 +1,476 @@
# Reflect
A private, on-device iOS mood tracker that turns a five-second daily check-in into durable self-knowledge. Users rate their day on a 5-point scale, and the app turns that signal into calendars, trends, Apple-Intelligence-powered insights, guided CBT reflections, watch complications, widgets, Live Activities, and shareable summaries.
> **Platform:** iOS 16+ · iPadOS · watchOS · macOS (Catalyst)
> **Language:** Swift / SwiftUI
> **Data:** SwiftData + CloudKit (private database)
> **AI:** Apple Foundation Models (on-device, iOS 26+)
> **Monetization:** 30-day free trial → monthly / yearly subscription (StoreKit 2)
---
## Table of Contents
- [Elevator pitch](#elevator-pitch)
- [Feature map](#feature-map)
- [Core features in depth](#core-features-in-depth)
- [Daily mood logging](#1-daily-mood-logging)
- [Day / Month / Year views](#2-day--month--year-views)
- [Insights (AI)](#3-insights-ai)
- [Weekly Digest (AI)](#4-weekly-digest-ai)
- [Auto-tagging (AI)](#5-auto-tagging-ai)
- [AI Reports + PDF export](#6-ai-reports--pdf-export)
- [Guided Reflection (CBT / ACT / BA)](#7-guided-reflection--deep-dive)
- [Widgets](#8-widgets)
- [Live Activities](#9-live-activities)
- [Watch app + complications](#10-watch-app--complications)
- [Control Center, Siri, App Intents, Shortcuts](#11-control-center-siri-app-intents-shortcuts)
- [HealthKit State of Mind](#12-healthkit-state-of-mind)
- [WeatherKit correlation](#13-weatherkit-correlation)
- [Photos on entries](#14-photos-on-entries)
- [Face ID / Touch ID lock](#15-face-id--touch-id-lock)
- [Sharing templates](#16-sharing-templates)
- [Customization](#17-customization)
- [Notifications + personality packs](#18-notifications--personality-packs)
- [Export / Import](#19-export--import)
- [Subscriptions](#20-subscriptions)
- [Guided Reflection — deep dive](#guided-reflection--deep-dive)
- [Architecture](#architecture)
- [Data model](#data-model)
- [Directory map](#directory-map)
- [Build, run, test](#build-run-test)
- [Localization](#localization)
- [Privacy & security](#privacy--security)
- [Configuration reference](#configuration-reference)
---
## Elevator pitch
Most mood trackers collect data. **Reflect converts it into insight.** The five-second daily tap is only the entry point — behind it sits a full behavioral-health stack:
- **On-device Apple Intelligence** (Foundation Models) writes your weekly digest, surfaces patterns, and runs a cognitive-behavioral-therapy-aligned reflection flow. Nothing leaves the phone.
- **CloudKit sync** keeps iPhone and Apple Watch in step without an account.
- **HealthKit State of Mind** writes your rating back into Apple Health and reads activity / sleep / HRV so the app can correlate mood with the rest of your life.
- **A CBT-aligned Guided Reflection** detects cognitive distortions in your automatic thoughts and rewrites the follow-up question to match the specific distortion — the actual mechanism of change in evidence-based CBT.
- **Four surfaces to log from**: app, home/lock-screen widget, Control Center widget, Apple Watch complication. One tap, anywhere.
The thesis: a mood tracker is only useful if the patterns it surfaces can drive behavior change. Reflect is designed from the top down around the CBT/ACT evidence base, not screen time.
---
## Feature map
| Area | What it is | Why it sells | Source |
|---|---|---|---|
| 5-point mood log | Horrible → Great daily rating | The lowest-friction signal that still captures variance | [`Shared/Models/Mood.swift`](Shared/Models/Mood.swift) |
| Day view | Chronological list with notes, photos, weather | Diary + heatmap in one scroll | [`Shared/Views/DayView/`](Shared/Views/DayView) |
| Month view | Calendar heatmap | "What kind of month was this?" in one glance | [`Shared/Views/MonthView/`](Shared/Views/MonthView) |
| Year view | 12-month heatmap + filters | See multi-month seasonality | [`Shared/Views/YearView/`](Shared/Views/YearView) |
| AI Insights | Patterns, predictions, advice | Turns data into behavior change | [`Shared/Views/InsightsView/`](Shared/Views/InsightsView) · [`FoundationModelsInsightService.swift`](Shared/Services/FoundationModelsInsightService.swift) |
| Weekly Digest | AI-written summary of the last 7 days | Sunday-night ritual, builds habit | [`FoundationModelsDigestService.swift`](Shared/Services/FoundationModelsDigestService.swift) |
| Auto-tagging | AI extracts themes (work, sleep, family…) | Enables mood↔theme correlation | [`FoundationModelsTagService.swift`](Shared/Services/FoundationModelsTagService.swift) |
| AI Reports + PDF | Clinical multi-week reports | Shareable with therapist | [`ReportPDFGenerator.swift`](Shared/Services/ReportPDFGenerator.swift) |
| Guided Reflection | CBT Thought Record / ACT Defusion / Behavioral Activation | Evidence-based, not a mood journal | [`GuidedReflectionView.swift`](Shared/Views/GuidedReflectionView.swift) · [`GuidedReflection.swift`](Shared/Models/GuidedReflection.swift) |
| Cognitive distortion detector | Routes reframes to specific distortion | The mechanism of change in CBT | [`CognitiveDistortionDetector.swift`](Shared/Services/CognitiveDistortionDetector.swift) |
| Widgets | 6 widget families | Logging from the home screen | [`ReflectWidget/`](ReflectWidget) |
| Live Activities | Lock Screen + Dynamic Island streak | Keeps the habit present | [`ReflectLiveActivity.swift`](ReflectWidget/ReflectLiveActivity.swift) · [`MoodStreakActivity.swift`](Shared/MoodStreakActivity.swift) |
| Watch app | Log and view on wrist | Frictionless logging | [`Reflect Watch App/`](Reflect%20Watch%20App) |
| Complications | Streak / last mood on watch face | Ambient nudge to log | [`ReflectComplication.swift`](Reflect%20Watch%20App/ReflectComplication.swift) |
| Control Center | iOS 18 Control Center widget | One-tap log from anywhere | [`ReflectMoodControlWidget.swift`](ReflectWidget/ReflectMoodControlWidget.swift) |
| Siri / Shortcuts | App Intents for "Log mood great" | Hands-free logging | [`AppShortcuts.swift`](Shared/AppShortcuts.swift) · [`SharedMoodIntent.swift`](Shared/SharedMoodIntent.swift) |
| HealthKit | State of Mind write + activity read | Deep Apple Health integration | [`HealthKitManager.swift`](Shared/HealthKitManager.swift) · [`HealthService.swift`](Shared/Services/HealthService.swift) |
| WeatherKit | Attach weather to each entry | Mood↔weather correlation in Insights | [`WeatherManager.swift`](Shared/Services/WeatherManager.swift) |
| Photos | Attach a photo per day | Visual journal | [`PhotoManager.swift`](Shared/Services/PhotoManager.swift) |
| Biometric lock | Face ID / Touch ID / Optic ID | Private data stays private | [`BiometricAuthManager.swift`](Shared/Services/BiometricAuthManager.swift) |
| Sharing templates | Mood-style social cards | Organic growth channel | [`Shared/Views/Sharing/`](Shared/Views/Sharing) · [`Shared/Views/SharingTemplates/`](Shared/Views/SharingTemplates) |
| Custom icon widget | User-designed widget layout | Personalization as retention | [`Shared/Views/CustomWidget/`](Shared/Views/CustomWidget) |
| Theming | Color sets, icon packs, shapes, personality | Make it feel like yours | [`Shared/Views/CustomizeView/`](Shared/Views/CustomizeView) |
| Export | CSV / JSON / PDF | No lock-in — builds trust | [`ExportService.swift`](Shared/Services/ExportService.swift) |
| Subscription | Monthly / yearly + 30-day trial | StoreKit 2, offer codes | [`IAPManager.swift`](Shared/IAPManager.swift) |
---
## Core features in depth
### 1. Daily mood logging
- **What:** A 5-point ordinal scale — Horrible, Bad, Average, Good, Great — defined in [`Shared/Models/Mood.swift`](Shared/Models/Mood.swift).
- **How:** Every mood entry — from the app, a widget, the watch, Siri, the lock-screen Live Activity, a push-notification action, or an imported CSV row — funnels through a single choke point: **[`MoodLogger.shared.logMood(...)`](Shared/MoodLogger.swift)**. That one method runs the write to SwiftData, the HealthKit sync, streak recomputation, widget-timeline reload, Watch Connectivity nudge, TipKit parameter update, Live Activity refresh, and analytics event.
- **Why 5 points:** Enough to capture variance, few enough to fit on a widget. The ordinal scale is what lets us run [`MoodMetrics.swift`](Shared/Models/MoodMetrics.swift) over it without arguing about arithmetic on emoji.
- **Source-of-truth rule:** never insert into `modelContext` directly — always go through `MoodLogger`, or side effects silently break. This invariant is enforced in [`CLAUDE.md`](CLAUDE.md#mutation--write-patterns).
### 2. Day / Month / Year views
Three zoom levels over the same data, selected via the tab bar in [`MainTabView.swift`](Shared/Views/MainTabView.swift).
- **[Day](Shared/Views/DayView)** — Chronological list of entries with notes, photo, weather card, and the "open reflection" CTA. Backed by [`DayViewViewModel`](Shared/Views/DayView).
- **[Month](Shared/Views/MonthView)** — Classic calendar grid, each cell tinted by its mood. Tap to drill into that date. Shape is user-configurable (circle / square / rounded / diamond).
- **[Year](Shared/Views/YearView)** — 12 mini calendars stacked for seasonality. Users can filter by weekday (e.g. "only show Mondays") to isolate work-week patterns.
### 3. Insights (AI)
- **What:** An on-device AI pass over your mood history that returns a short, human-readable list of patterns, predictions, advice, and achievements, tinted to the dominant mood.
- **How:** [`FoundationModelsInsightService.swift`](Shared/Services/FoundationModelsInsightService.swift) uses Apple Foundation Models (`LanguageModelSession` with a `@Generable` schema defined in [`AIInsight.swift`](Shared/Models/AIInsight.swift)). The prompt is built by [`MoodDataSummarizer.swift`](Shared/Services/MoodDataSummarizer.swift), which precomputes mood distribution, day-of-week patterns, streaks, weather correlations, and HealthKit correlations so the model only has to narrate facts we already have.
- **Caching:** 1-hour result cache — regenerating is one tap.
- **Selling point:** Zero network, zero account, zero data leaves the device.
### 4. Weekly Digest (AI)
- **What:** Every week the app writes you a one-paragraph summary with a headline, a highlight ("your best day was Thursday — walk after work"), and an intention for next week.
- **How:** [`FoundationModelsDigestService.swift`](Shared/Services/FoundationModelsDigestService.swift) generates against the [`AIWeeklyDigest`](Shared/Models/AIWeeklyDigest.swift) schema. Scheduled by [`BGTask.runWeeklyDigestTask`](Shared/BGTask.swift) so it's ready on Sunday night without the user opening the app. Delivered via a local notification; rendered in [`WeeklyDigestCardView.swift`](Shared/Views/InsightsView).
- **Why it sells:** A weekly "here's what happened" ritual is how habits become durable.
### 5. Auto-tagging (AI)
- **What:** Themes like *work / sleep / family / exercise / stress* are extracted automatically from each entry's notes and reflection.
- **How:** [`FoundationModelsTagService.swift`](Shared/Services/FoundationModelsTagService.swift) returns an [`AIEntryTags`](Shared/Models/AIEntryTags.swift). Insights cross-reference the tag with the mood to answer questions like "what happens to your mood on days you write about sleep?".
- **Why it sells:** Users don't have to tag manually — the signal is free.
### 6. AI Reports + PDF export
- **What:** A multi-week clinical-style report with overview stats, week-by-week breakdowns, and visualizations, exportable as a PDF to share with a therapist or doctor.
- **How:** [`ReportPDFGenerator.swift`](Shared/Services/ReportPDFGenerator.swift) renders an HTML template through WKWebView and rasterizes to PDF. Data shape is [`AIReport`](Shared/Models/AIReport.swift); UI lives in [`Shared/Views/InsightsView/ReportsView.swift`](Shared/Views/InsightsView).
- **Why it sells:** Unlocks clinical use cases — a real moat vs. tap-to-log competitors.
### 7. Guided Reflection — deep dive
See the dedicated [Guided Reflection section below](#guided-reflection--deep-dive).
### 8. Widgets
All six live in [`ReflectWidget/`](ReflectWidget) and are bundled by [`WidgetBundle.swift`](ReflectWidget/WidgetBundle.swift).
| Widget | File | Purpose |
|---|---|---|
| Vote | [`ReflectVoteWidget.swift`](ReflectWidget/ReflectVoteWidget.swift) | Interactive 5-button logger — small/medium, no app launch |
| Timeline | [`ReflectTimelineWidget.swift`](ReflectWidget/ReflectTimelineWidget.swift) | Recent-days strip; pre-vote shows buttons, post-vote shows stats |
| Graphic | [`ReflectGraphicWidget.swift`](ReflectWidget/ReflectGraphicWidget.swift) | Large mood art widget |
| Icon | [`ReflectIconWidget.swift`](ReflectWidget/ReflectIconWidget.swift) | User-customized widget (see [CustomWidget](Shared/Views/CustomWidget)) |
| Mood Control | [`ReflectMoodControlWidget.swift`](ReflectWidget/ReflectMoodControlWidget.swift) | **Control Center** widget — iOS 18 |
| Live Activity | [`ReflectLiveActivity.swift`](ReflectWidget/ReflectLiveActivity.swift) | Lock Screen + Dynamic Island |
Widgets can't reach CloudKit or HealthKit from their process, so they read through [`ExtensionDataProvider.swift`](Shared/Persisence/ExtensionDataProvider.swift), which talks to the App Group container directly. When the widget writes a vote via an `AppIntent`, the main app picks it up and runs the deferred `MoodLogger` side effects next launch.
### 9. Live Activities
- **What:** A streak card on the Lock Screen and in the Dynamic Island, updating in real time when you log or when the voting window opens.
- **How:** [`MoodStreakActivity.swift`](Shared/MoodStreakActivity.swift) starts the `ActivityKit` activity; [`ReflectLiveActivity.swift`](ReflectWidget/ReflectLiveActivity.swift) is the view. [`LiveActivityScheduler`](Shared/MoodStreakActivity.swift) decides when the activity should be on-screen (typically after the daily reminder fires until the day is logged).
- **Why it sells:** Continuous visible nudge without being a notification — drives logging consistency.
### 10. Watch app + complications
- **[`Reflect Watch App/ContentView.swift`](Reflect%20Watch%20App/ContentView.swift)** — Full voting UI on the wrist. Writes go through the shared CloudKit database; UI refresh for the phone happens over [`WatchConnectivityManager`](Shared/Services/WatchConnectivityManager.swift).
- **[`ReflectComplication.swift`](Reflect%20Watch%20App/ReflectComplication.swift)** — Circular / rectangular / corner complications showing streak, last mood, or graphic.
- **Selling point:** the watch is the lowest-friction surface — users raise their wrist, tap, done.
### 11. Control Center, Siri, App Intents, Shortcuts
- **[`AppShortcuts.swift`](Shared/AppShortcuts.swift)** — Exposes "Log a mood" and "Open Reflect" as App Intents so Siri and Shortcuts can call them.
- **[`SharedMoodIntent.swift`](Shared/SharedMoodIntent.swift)** — The intent shared across widget buttons and voice actions.
- **[`ReflectMoodControlWidget.swift`](ReflectWidget/ReflectMoodControlWidget.swift)** — iOS 18 Control Center widget that opens the app pre-scrolled to the log screen.
### 12. HealthKit State of Mind
- **[`HealthKitManager.swift`](Shared/HealthKitManager.swift)** — Writes each mood into Apple Health's State of Mind store (valence mapped from the 5-point scale).
- **[`HealthService.swift`](Shared/Services/HealthService.swift)** — Reads activity, exercise time, heart rate, HRV, and sleep and folds them into the Insights prompt for mood↔body correlation.
- **Why it sells:** Reflect becomes the primary State-of-Mind logger for users already bought into Apple Health.
### 13. WeatherKit correlation
- **[`WeatherManager.swift`](Shared/Services/WeatherManager.swift)** — Pulls current conditions from WeatherKit using a one-shot location from [`LocationManager.swift`](Shared/Services/LocationManager.swift) (10-min cache, 15 s timeout) and attaches a [`WeatherData`](Shared/Models/WeatherData.swift) snapshot to the entry.
- **Retry:** failures queue for [`BGTask.runWeatherRetryTask`](Shared/BGTask.swift) so weather fills in overnight.
### 14. Photos on entries
- **[`PhotoManager.swift`](Shared/Services/PhotoManager.swift)** — JPEG at 0.8, stored in the App Group container, 200×200 thumbnail cached for list rendering.
- **Picker:** [`PhotoPickerView.swift`](Shared/Views/PhotoPickerView.swift) / [`ImagePickerGridView.swift`](Shared/Views/ImagePickerGridView.swift).
### 15. Face ID / Touch ID lock
- **[`BiometricAuthManager.swift`](Shared/Services/BiometricAuthManager.swift)** — Detects Face ID / Touch ID / Optic ID, gracefully falls back to the device passcode, and gates the whole app behind a biometric unlock if the user opts in.
### 16. Sharing templates
- **[`Shared/Views/Sharing/`](Shared/Views/Sharing)** — The sharing flow.
- **[`Shared/Views/SharingTemplates/`](Shared/Views/SharingTemplates)** — Visual templates (monthly recap, streak brag, mood pie).
- **[`SharingScreenshotExporter.swift`](Shared/Services/SharingScreenshotExporter.swift)** — Debug tool that renders every template for marketing.
- **Why it sells:** Shareable cards are a zero-cost organic-growth channel — each user who shares is a free ad.
### 17. Customization
[`Shared/Views/CustomizeView/`](Shared/Views/CustomizeView) lets users pick:
| Axis | Protocol | Options |
|---|---|---|
| Color set | [`MoodTintable`](Shared/Models/MoodTintable.swift) | Default, Pastel, Neon, Ocean, Forest, Sunset |
| Icon / emoji | [`MoodImagable`](Shared/Models/MoodImagable.swift) | FontAwesome, Emoji, Hand Emoji |
| Shape | [`Shapes.swift`](Shared/Models/Shapes.swift) | Circle, Square, Diamond, Rounded |
| Theme | [`Theme.swift`](Shared/Models/Theme.swift) | System / Light / Dark / iFeel gradient |
| Personality | [`PersonalityPackable`](Shared/Models/PersonalityPackable.swift) | Default, Coach, Zen, Best Friend, Data Analyst |
| App icon | [`Shared/Views/CustomIcon/`](Shared/Views/CustomIcon) | Multiple home-screen icons |
### 18. Notifications + personality packs
- **[`LocalNotification.swift`](Shared/LocalNotification.swift)** — Daily reminder with inline mood buttons; notification copy is written by the selected [`PersonalityPackable`](Shared/Models/PersonalityPackable.swift) so the "Nice" pack asks gently and the "Rude" pack heckles you.
- **[`AppDelegate.swift`](Shared/AppDelegate.swift)** — Handles the action buttons on the notification to route the vote through `MoodLogger`.
### 19. Export / Import
- **[`ExportService.swift`](Shared/Services/ExportService.swift)** — CSV, JSON, and PDF export of the full entry history with notes, weather, reflections, photos, and entry source preserved.
- **Why it sells:** No lock-in. Users know their data can leave, which makes them trust it going in.
### 20. Subscriptions
- **[`IAPManager.swift`](Shared/IAPManager.swift)** — StoreKit 2, monthly (`com.88oakapps.reflect.IAP.subscriptions.monthly`) and yearly, 30-day free trial counted from `firstLaunchDate`, paywall UI in [`ReflectSubscriptionStoreView.swift`](Shared/Views/ReflectSubscriptionStoreView.swift).
- **Gated behind paywall:** AI Insights, Weekly Digest, AI Reports, Guided Reflection AI feedback, HealthKit correlation insights, Apple Watch premium complications. Core logging + history are free forever.
---
## Guided Reflection — deep dive
> **Source files:**
> — Model: [`Shared/Models/GuidedReflection.swift`](Shared/Models/GuidedReflection.swift)
> — Distortion detector: [`Shared/Services/CognitiveDistortionDetector.swift`](Shared/Services/CognitiveDistortionDetector.swift)
> — AI feedback: [`Shared/Services/FoundationModelsReflectionService.swift`](Shared/Services/FoundationModelsReflectionService.swift)
> — Feedback model: [`Shared/Models/AIReflectionFeedback.swift`](Shared/Models/AIReflectionFeedback.swift)
> — UI: [`Shared/Views/GuidedReflectionView.swift`](Shared/Views/GuidedReflectionView.swift)
> — Feedback UI: [`Shared/Views/ReflectionFeedbackView.swift`](Shared/Views/ReflectionFeedbackView.swift)
> — Info sheet: [`Shared/Views/GuidedReflectionInfoView.swift`](Shared/Views/GuidedReflectionInfoView.swift)
> — Plan doc: [`docs/guided-reflection-cbt-plan.md`](docs/guided-reflection-cbt-plan.md)
> — Flowchart: [`docs/guided-reflection-flowchart.html`](docs/guided-reflection-flowchart.html)
### Why this exists
Most mood trackers stop at logging. Reflect's reflection flow is a short, mood-adaptive, **CBT-aligned** guided exercise. The goal is not to make the user journal more — it's to run a known therapeutic mechanism that has evidence for changing the relationship with a thought.
Three mood tiers → three different evidence-based frameworks:
| Mood | Framework | Mechanism | Question count |
|---|---|---|---|
| **Great / Good** | Behavioral Activation (BA) | Savor + plan to repeat | 3 |
| **Average** | ACT Cognitive Defusion | Notice thought, loosen its grip, re-orient to values | 4 |
| **Bad / Horrible** | CBT Thought Record | Identify situation, surface automatic thought, check for distortion, examine evidence, generate a balanced reframe | 5 |
These are routed from the mood value via [`MoodCategory(from:)`](Shared/Models/GuidedReflection.swift) and rendered as a stepped sheet from [`GuidedReflectionView.swift`](Shared/Views/GuidedReflectionView.swift).
### The Socratic template system
The defining feature of Socratic questioning in CBT is that **each question builds on the user's previous answer**. Static question lists are not Socratic. We implemented this with [`QuestionTemplate`](Shared/Models/GuidedReflection.swift) — each question has a `text` with an optional `%@` placeholder and a `placeholderRef: Int?` pointing at which earlier question's answer to inject.
Example (negative path):
```
Q1: "What happened today that affected your mood?"
→ user: "My boss criticized my presentation in front of the team"
Q2: "What thought kept coming back about 'My boss criticized my presentation…'?"
→ user: "I'm not cut out for this job"
Q3 (distortion-specific, see below): "Is 'I'm not cut out for this job' something you are, or something you did?"
Q4 (evidence): "What evidence supports 'I'm not cut out for this job', and what challenges it?"
Q5 (reframe): "Looking at 'I'm not cut out for this job' again — what's a more balanced way to see it?"
```
Injections are truncated to a sentence boundary or 60 characters by [`GuidedReflection.truncatedForInjection`](Shared/Models/GuidedReflection.swift) so long answers never break grammar. Templates are fully localized — each language controls its own `%@` position so grammar stays natural.
### Cognitive distortion detection
[`CognitiveDistortionDetector.swift`](Shared/Services/CognitiveDistortionDetector.swift) classifies the Q2 answer into one of seven types using localized keyword lists (deterministic, offline, privacy-preserving — **not ML**). This was a deliberate product choice: rule-based is inspectable, predictable, and works with zero latency.
| Distortion | Example phrasing | Q3 reframe prompt |
|---|---|---|
| `overgeneralization` | "always", "never", "everyone" | "Can you think of one counter-example to '%@'?" |
| `shouldStatement` | "should", "must", "have to" | "Where did the rule 'I should …' come from? Is it still serving you?" |
| `labeling` | "I am [trait]" | "Is '%@' something you *are*, or something you *did*?" |
| `personalization` | "my fault", "because of me" | "What other factors, besides you, contributed to this?" |
| `catastrophizing` | "ruined", "can't recover" | "What's the worst case? What's the most likely case?" |
| `mindReading` | "thinks I'm", "hates me" | "What evidence do you have for that interpretation?" |
| `unknown` | (no match) | Falls back to the generic perspective check |
Keywords live in [`Localizable.xcstrings`](Reflect/Localizable.xcstrings) under `distortion_*_keywords` keys so each locale tunes its own detection rules. Priority order is specific → general so that "I always ruin everything" classifies as catastrophizing first, overgeneralization second.
### Evidence examination
The negative path explicitly inserts a dedicated evidence step (Q4) — supporting and contradicting evidence both. This is the single most load-bearing step in a real CBT Thought Record and was previously missing.
### Intensity tracking
[`GuidedReflection`](Shared/Models/GuidedReflection.swift) carries optional `preIntensity` and `postIntensity` (010). CBT literature emphasizes measuring the emotion's intensity before and after the thought work — the delta is the efficacy signal. The AI feedback stage references the shift ("you moved from an 8 to a 5") when it narrates the reflection.
### AI feedback stage
On completion [`FoundationModelsReflectionService`](Shared/Services/FoundationModelsReflectionService.swift) generates an [`AIReflectionFeedback`](Shared/Models/AIReflectionFeedback.swift) with three slots — **affirmation**, **observation**, **takeaway** — rendered by [`ReflectionFeedbackView.swift`](Shared/Views/ReflectionFeedbackView.swift). Tone is driven by the user's selected [`PersonalityPackable`](Shared/Models/PersonalityPackable.swift) (Coach / Zen / Best Friend / Data Analyst / Default). This is gated behind the subscription + iOS 26 Apple Intelligence — when unavailable, the reflection still saves normally and the feedback view degrades gracefully.
### Back-compat
Old reflections saved with 4 negative-path responses (pre-evidence step) still decode and still count as complete. [`GuidedReflection.isComplete`](Shared/Models/GuidedReflection.swift) detects the legacy shape so users never lose completed work across updates.
### Full flow diagram
Open [`docs/guided-reflection-flowchart.html`](docs/guided-reflection-flowchart.html) in a browser for the visual flow. The plan doc in [`docs/guided-reflection-cbt-plan.md`](docs/guided-reflection-cbt-plan.md) walks through the phased rationale — phase 1 (templates + intensity), phase 2 (distortion detection + evidence), phase 3 (AI-generated final question).
---
## Architecture
**Pattern:** MVVM + SwiftUI, singletons for cross-cutting concerns.
**Entry point:** [`Shared/ReflectApp.swift`](Shared/ReflectApp.swift)
### Layers
```
┌─────────────────────────────────────────────────────────────────┐
│ Views (SwiftUI) Shared/Views/ │
│ ViewModels (@MainActor) colocated with views │
├─────────────────────────────────────────────────────────────────┤
│ Services (singletons) Shared/Services/ │
│ MoodLogger AnalyticsManager IAPManager │
│ HealthKitManager WeatherManager LocationManager │
│ FoundationModels{Insight,Digest,Reflection,Tag}Service │
│ CognitiveDistortionDetector │
├─────────────────────────────────────────────────────────────────┤
│ Persistence Shared/Persisence/ │
│ DataController (GET/ADD/UPDATE/DELETE) │
│ SharedModelContainer — SwiftData + CloudKit + App Group │
│ ExtensionDataProvider — widget/watch read path │
├─────────────────────────────────────────────────────────────────┤
│ Models Shared/Models/ │
│ MoodEntryModel (@Model) Mood GuidedReflection │
│ AIInsight AIWeeklyDigest AIReflectionFeedback AIReport │
└─────────────────────────────────────────────────────────────────┘
```
### Data flow for a single mood log
```
tap → DayViewViewModel.add()
└─► MoodLogger.shared.logMood() ← ALL entry points converge here
├─► DataController.shared.add() — SwiftData insert + save
├─► HealthKitManager.write() — State of Mind
├─► streak recompute
├─► WidgetCenter.reloadAllTimelines()
├─► WatchConnectivityManager.nudge()
├─► LiveActivityScheduler.update()
├─► TipKit parameter update
└─► AnalyticsManager.shared.track()
```
Widgets and the watch bypass `MoodLogger` when the main app isn't running and write through `ExtensionDataProvider`; `MoodLogger.applySideEffects()` catches up on next launch. Detailed rules: [`CLAUDE.md`](CLAUDE.md).
---
## Data model
Primary entity: [`MoodEntryModel`](Shared/Models/MoodEntryModel.swift) — a SwiftData `@Model` with:
| Field | Type | Notes |
|---|---|---|
| `moodValue` | Int | 04 (Horrible → Great), 5 = missing, 6 = placeholder |
| `forDate` | Date | The *logical* day being rated |
| `timestamp` | Date | When the entry was written |
| `weekDay` | Int | 17, denormalized for fast filtering |
| `entryType` | Int | listView / widget / watch / shortcut / filledInMissing / notification / header / siri / controlCenter / liveActivity |
| `notes` | String? | Free text |
| `photoID` | String? | Path into App Group container |
| `weatherJSON` | String? | [`WeatherData`](Shared/Models/WeatherData.swift) |
| `reflectionJSON` | String? | [`GuidedReflection`](Shared/Models/GuidedReflection.swift) |
| `tagsJSON` | String? | [`AIEntryTags`](Shared/Models/AIEntryTags.swift) |
All fields have defaults — required by CloudKit. Sync is automatic via SwiftData's built-in CloudKit integration.
---
## Directory map
| Path | Contents |
|---|---|
| [`Shared/`](Shared) | All cross-platform code |
| [`Shared/Models/`](Shared/Models) | Domain types, SwiftData models, AI schemas |
| [`Shared/Views/`](Shared/Views) | SwiftUI views, grouped by feature |
| [`Shared/Services/`](Shared/Services) | Singletons for AI, HealthKit, Weather, Export, etc. |
| [`Shared/Persisence/`](Shared/Persisence) | SwiftData layer *(note: directory name has a typo — intentional, for historic reasons)* |
| [`Shared/Onboarding/`](Shared/Onboarding) | First-launch flow |
| [`Shared/Utilities/`](Shared/Utilities) | Small helpers |
| [`ReflectWidget/`](ReflectWidget) | Widget + Control Center + Live Activity extension |
| [`Reflect Watch App/`](Reflect%20Watch%20App) | watchOS app + complications |
| [`Reflect/`](Reflect) | iOS app target assets, Info.plist, Localizable.xcstrings |
| [`Tests iOS/`](Tests%20iOS) | XCUITest suites — see [`docs/XCUITest-Authoring.md`](docs/XCUITest-Authoring.md) |
| [`ReflectTests/`](ReflectTests) | XCTest unit tests |
| [`Tests macOS/`](Tests%20macOS) | macOS target tests |
| [`landing_page/`](landing_page) | Marketing site |
| [`docs/`](docs) | Design docs, ASO, QA plan, CBT plan, competitors |
| [`scripts/`](scripts) | Build/dev utilities |
| [`ads/`](ads) | Marketing creative |
---
## Build, run, test
```bash
# Build
xcodebuild -project Reflect.xcodeproj \
-scheme "Reflect (iOS)" \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' build
# Run all tests
xcodebuild -project Reflect.xcodeproj \
-scheme "Reflect (iOS)" \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' test
# Run one UI suite
xcodebuild -project Reflect.xcodeproj \
-scheme "Reflect (iOS)" \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
-only-testing:"Tests iOS/Tests_iOS" test
```
UI test conventions are mandatory — see [`docs/XCUITest-Authoring.md`](docs/XCUITest-Authoring.md), [`Tests iOS/Helpers/BaseUITestCase.swift`](Tests%20iOS/Helpers/BaseUITestCase.swift), and [`Shared/AccessibilityIdentifiers.swift`](Shared/AccessibilityIdentifiers.swift).
---
## Localization
Format: **String Catalog** at [`Reflect/Localizable.xcstrings`](Reflect/Localizable.xcstrings).
Shipping languages: English · German · Spanish · French · Japanese · Korean · Portuguese (Brazil).
All user-facing strings use `String(localized:)`. "Reflect" is a brand name — untranslated. Distortion-detection keywords are per-locale (see [Cognitive distortion detection](#cognitive-distortion-detection)).
---
## Privacy & security
- **No account, no backend, no third-party server** ever sees mood data. Data lives in SwiftData, synced through the user's own private CloudKit database.
- **AI is 100% on-device** — Foundation Models runs locally; nothing is sent to Anthropic, OpenAI, Apple servers, or us.
- **Analytics** (PostHog via [`AnalyticsManager`](Shared/Analytics.swift)) captures UI events, never mood content, and the user can opt out in settings.
- **Optional biometric lock** ([`BiometricAuthManager`](Shared/Services/BiometricAuthManager.swift)) gates the app behind Face ID / Touch ID / Optic ID.
- **HealthKit** read/write is permission-gated and only triggered on explicit user opt-in.
---
## Configuration reference
| Setting | Value |
|---|---|
| Bundle ID (iOS) | `com.88oakapps.reflect` |
| App Group (prod) | `group.com.88oakapps.reflect` |
| App Group (debug) | `group.com.88oakapps.reflect.debug` |
| CloudKit (prod) | `iCloud.com.88oakapps.reflect` |
| CloudKit (debug) | `iCloud.com.88oakapps.reflect.debug` |
| Subscription group | `21951685` |
| Monthly product ID | `com.88oakapps.reflect.IAP.subscriptions.monthly` |
| Yearly product ID | `com.88oakapps.reflect.IAP.subscriptions.yearly` |
| Free trial | 30 days from `firstLaunchDate` |
| URL scheme | `reflect://` (e.g. `reflect://subscribe`) |
| BGTask — missing dates | `com.88oakapps.reflect.dbUpdateMissing` |
| BGTask — weather retry | `com.88oakapps.reflect.weatherRetry` |
| BGTask — weekly digest | `com.88oakapps.reflect.weeklyDigest` |
| Logger subsystem | `com.88oakapps.reflect` |
---
*For deeper architectural rules, concurrency constraints, data-access invariants, and edge-case gotchas, see [`CLAUDE.md`](CLAUDE.md). For ASO strategy and screenshot plans see [`docs/`](docs).*