Expand guided reflection with CBT thought record and distortion routing

Adds a 5-step negative-mood reflection flow with an evidence-examination
step, Socratic templated questions that back-reference prior answers, and
a deterministic cognitive-distortion detector that routes the perspective-
check prompt to a distortion-specific reframe. Includes CBT plan docs,
flowchart, stats research notes, and MCP config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-14 18:49:39 -05:00
parent e6a34a0f25
commit cc4143d3ea
9 changed files with 2235 additions and 33 deletions

View File

@@ -0,0 +1,233 @@
# Guided Reflection — CBT-Aligned Adaptive Questioning Plan
## Context
The current guided reflection flow (`GuidedReflection.swift`, `GuidedReflectionView.swift`) asks 3-4 static questions based on mood category (positive → Behavioral Activation, neutral → ACT Defusion, negative → CBT Thought Record). Questions do not reference prior answers, do not adapt to cognitive distortions, and skip the evidence-examination step that is the actual mechanism of change in CBT.
This plan makes the reflection **more CBT-aligned, not less**, by introducing adaptive sequencing — which is the defining characteristic of Socratic questioning and guided discovery in CBT. Every phase is additive and ships independently.
---
## Phase 1 — Template Substitution + Intensity + Translation Fix
No AI dependency. Works offline. Fully localizable.
### 1a. Reference previous answers in question text
Update `GuidedReflection.questions(for:)` to return **question templates** with placeholders, then resolve them at render time using the user's prior answers.
**Files:**
- `Shared/Models/GuidedReflection.swift` — add `QuestionTemplate` struct with `template: String` and `placeholderRef: Int?` (index of question whose answer to inject)
- `Shared/Views/GuidedReflectionView.swift` — resolve templates against `draft.steps` when building the `Step.question` text at navigation time (not init time, so Q3 shows Q2's answer even if user goes back and edits)
- `Reflect/Localizable.xcstrings` — add new localized keys for templated questions using standard `%@` format specifier so each locale controls where the quoted answer appears grammatically
**Example — negative path:**
```
Q1 template: "What happened today that affected your mood?"
Q2 template: "What thought kept coming back about it?"
Q3 template: "If a friend told you they had the thought '%@', what would you tell them?" [inject Q2 answer]
Q4 template: "Looking at '%@' again — what's a more balanced way to see it?" [inject Q2 answer]
```
**Answer truncation:** if the referenced answer is > 60 characters, truncate to the first sentence or 60 chars + "…". Keep a helper `GuidedReflection.truncatedForInjection(_:)` in the model.
**Edge cases:**
- If referenced answer is empty (user skipped back), fall back to the current static question text.
- If user edits an earlier answer, the later question text updates on next navigation to it.
### 1b. Add emotion intensity rating (pre and post)
CBT measures emotional intensity before and after the thought work. This is the single most CBT-faithful addition.
**Files:**
- `Shared/Models/GuidedReflection.swift` — add `preIntensity: Int?` (0-10) and `postIntensity: Int?` (0-10) to `GuidedReflection` struct. Update `CodingKeys` and `isComplete` logic.
- `Shared/Views/GuidedReflectionView.swift` — render an intensity slider before Q1 (pre) and after the last question (post). Use a 0-10 discrete scale with labels ("barely", "intense") localized.
- `Shared/Services/FoundationModelsReflectionService.swift` — include `preIntensity` and `postIntensity` in the prompt so AI feedback can reference the shift ("you moved from an 8 to a 5").
### 1c. Fix stale localized question strings
The German, Spanish, French, Japanese, Korean, and Portuguese-BR translations in `Localizable.xcstrings` for `guided_reflection_{negative,neutral,positive}_q{1..4}` translate **older** English question text. Example: German Q1 is "Was belastet dich heute?" ("What's weighing on you?") but English is "What happened today that affected your mood?".
**File:** `Reflect/Localizable.xcstrings`
Retranslate all existing guided reflection question keys to match current English text. Flag state as `translated` only after review.
### 1d. Specificity probe on Q1
If the Q1 answer is < 25 characters or exactly matches a vague-phrase list (e.g., "idk", "everything", "nothing", "same as always"), surface a soft follow-up bubble below the editor: "Can you remember a specific moment? What happened just before you noticed the feeling?" Non-blocking — user can ignore it.
**Files:**
- `Shared/Views/GuidedReflectionView.swift` — add `needsSpecificityProbe(for:)` helper and conditional hint view below the Q1 editor
- `Reflect/Localizable.xcstrings` — add `guided_reflection_specificity_probe` key
---
## Phase 2 — Rule-Based Distortion Detection (Negative Path)
No AI dependency. Adds the most-impactful CBT mechanism: matching the reframe to the specific cognitive distortion.
### 2a. Distortion detection
Classify the Q2 answer into a cognitive distortion type using localized keyword rules.
**New file:** `Shared/Services/CognitiveDistortionDetector.swift`
```swift
enum CognitiveDistortion: String, Codable {
case overgeneralization // "always", "never", "everyone", "no one"
case shouldStatement // "should", "must", "have to"
case labeling // "I am [negative trait]"
case personalization // "my fault", "because of me"
case catastrophizing // "will never", "ruined", "can't recover"
case mindReading // "thinks I'm", "hates me", "judging me"
case unknown
}
@MainActor
enum CognitiveDistortionDetector {
static func detect(in text: String, locale: Locale = .current) -> CognitiveDistortion
}
```
Per-locale keyword lists live in localized strings (`distortion_overgeneralization_keywords` = comma-separated list). This stays localizable and avoids hardcoding English-only logic.
### 2b. Distortion-specific Q3 reframe prompt
Update the negative-path Q3 question resolution to switch on the detected distortion:
| Distortion | Q3 prompt (localized key) |
|---|---|
| overgeneralization | "Can you think of one counter-example to '%@'?" |
| shouldStatement | "Where did the rule 'I should …' come from? Is it still serving you?" |
| labeling | "Is '%@' something you *are*, or something you *did*?" |
| personalization | "What other factors, besides you, contributed to this?" |
| catastrophizing | "What's the worst case? What's the most likely case?" |
| mindReading | "What evidence do you have for that interpretation? What else could it mean?" |
| unknown | Current static Q3 (fallback) |
**Files:**
- `Shared/Models/GuidedReflection.swift` — add `detectedDistortion: CognitiveDistortion?` to persist the classification on the response
- `Shared/Views/GuidedReflectionView.swift` — call detector when transitioning from Q2 → Q3, pick template, render
- `Reflect/Localizable.xcstrings` — add 6 new localized question templates
### 2c. Add an evidence-examination step (negative path only)
Currently the negative path skips the core CBT Thought Record mechanism: examining evidence for/against the thought. Insert a new step between the current Q3 and Q4.
New flow for negative (5 questions instead of 4):
1. Situation (Q1)
2. Automatic thought (Q2)
3. Perspective check (Q3 — distortion-specific from 2b)
4. **Evidence examination (NEW Q4)**: "What evidence supports this thought, and what challenges it?"
5. Balanced reframe (Q5, formerly Q4)
**Files:**
- `Shared/Models/GuidedReflection.swift` — bump `MoodCategory.negative.questionCount` to 5, update `stepLabels`, update `questions(for:)`
- `Reflect/Localizable.xcstrings` — add `guided_reflection_negative_q_evidence` key (localized to all 7 languages)
- Migration: existing saved reflections with 4 responses still `isComplete` — use version-tolerant decoding (already Codable, but verify no crash on old JSON)
---
## Phase 3 — AI-Enhanced Final Question (Premium, iOS 26+)
Use Foundation Models to generate a personalized final reframe question based on the entire reflection so far. Falls back to Phase 2 rule-based prompt if AI unavailable.
### 3a. Adaptive final-question service
**New file:** `Shared/Services/FoundationModelsReflectionPrompterService.swift`
```swift
@available(iOS 26, *)
@MainActor
class FoundationModelsReflectionPrompterService {
func generateFinalQuestion(
moodCategory: MoodCategory,
priorResponses: [GuidedReflection.Response],
detectedDistortion: CognitiveDistortion?
) async throws -> String
}
```
System instructions enforce:
- One question only, under 25 words
- Must reference at least one specific phrase from a prior answer
- Must follow CBT principles (Socratic, non-leading, non-interpretive)
- Must map to the active therapeutic framework (Thought Record / ACT / BA)
Use `LanguageModelSession` with a constrained `Generable` output schema (just `{ question: String }`).
### 3b. Integration
- Gate behind `IAPManager.shared.shouldShowPaywall == false && iOS 26 && Apple Intelligence available`
- On transition to the final step, kick off generation with a 1.5s timeout. If timeout or error, fall back to the Phase 2 deterministic question.
- Show a brief "generating your question…" shimmer on the step card during generation — but pre-populate with the fallback text so the user can start reading/typing immediately if they want.
- Persist which question text was actually shown on `GuidedReflection.Response.question` so the AI feedback stage sees what the user actually saw.
### 3c. Update `FoundationModelsReflectionService`
Enhance the existing feedback service to reference:
- The intensity shift (pre → post)
- Which cognitive distortion was detected (if any)
- The fact that the final question was AI-adapted to them
---
## Files Modified / Created
### Modified
- `Shared/Models/GuidedReflection.swift` — templates, intensity, distortion, evidence step
- `Shared/Views/GuidedReflectionView.swift` — resolve templates, intensity sliders, specificity probe, distortion routing, AI prompt integration
- `Shared/Services/FoundationModelsReflectionService.swift` — consume intensity shift + distortion in feedback prompt
- `Reflect/Localizable.xcstrings` — retranslate existing keys + add ~15 new ones
### New
- `Shared/Services/CognitiveDistortionDetector.swift` (Phase 2)
- `Shared/Services/FoundationModelsReflectionPrompterService.swift` (Phase 3)
### Tests
- `ReflectTests/GuidedReflectionTemplatingTests.swift` — template resolution, answer truncation, edge cases (empty/edited prior answer)
- `ReflectTests/CognitiveDistortionDetectorTests.swift` — per-distortion detection with English fixtures (extend to other locales when translations land)
- `ReflectTests/GuidedReflectionMigrationTests.swift` — decode old 4-question JSON without crashing, handle missing intensity fields
---
## Verification
### Phase 1
1. Log a negative mood, start reflection
2. Answer Q1 with a specific event ("My boss criticized my presentation")
3. Answer Q2 with a thought ("I'm not cut out for this job")
4. Navigate to Q3 — verify the question text quotes the Q2 answer
5. Go back to Q2, change the answer, navigate forward — verify Q3 text updates
6. Verify pre-intensity slider appears before Q1 and post-intensity appears after the last question
7. Change device language to German — verify all question templates render grammatically correct German with quoted answers
8. Answer Q1 with "idk" — verify specificity probe appears
9. Answer Q1 with a full sentence — verify no probe
### Phase 2
1. Answer Q2 with "I always mess everything up" — verify Q3 shows the overgeneralization-specific prompt ("Can you think of one counter-example to...")
2. Answer Q2 with "I should have done better" — verify shouldStatement prompt
3. Answer Q2 with "I'm such a failure" — verify labeling prompt
4. Answer Q2 with a neutral thought (no distortion keywords) — verify fallback to the static Q3
5. Verify negative path now has 5 steps (progress shows 1/5)
6. Load an existing saved negative reflection with 4 responses — verify it still opens without crashing and shows as complete
### Phase 3
1. On iOS 26 device with Apple Intelligence + active subscription: complete Q1-Q4, navigate to Q5 — verify AI-generated question references specific wording from earlier answers
2. Turn off Apple Intelligence — verify fallback to Phase 2 deterministic question (no delay, no error banner)
3. On iOS 25 or non-subscribed user — verify Phase 2 prompt renders immediately (no AI path attempted)
4. Verify AI feedback at the end of the reflection references the intensity shift and (if detected) the cognitive distortion
### Cross-phase
- Run `xcodebuild test -only-testing:"ReflectTests"` — all new tests pass
- Manual run through all 3 mood categories (positive / neutral / negative) on English + 1 non-English locale
- Verify existing saved reflections from before this change still decode and display correctly
---
## Out of Scope
- Restructuring the positive (BA) or neutral (ACT) paths beyond Phase 1 templating. Those frameworks don't use distortion detection or evidence examination — their mechanisms are activity scheduling and values clarification, which work fine with static questions + templating.
- Changing chip suggestions. The current chip library is solid and orthogonal to this work.
- Personality-pack variants of the distortion prompts. Phase 2 ships with the "Default" voice only; other packs can be layered later using the same infrastructure.

File diff suppressed because it is too large Load Diff