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,123 @@
//
// CognitiveDistortionDetector.swift
// Reflect
//
// Detects common cognitive distortions in a user's automatic-thought response.
// Used by the guided reflection flow to route to a distortion-specific reframe prompt.
//
// This is deterministic keyword matching, not ML chosen for offline support,
// privacy, and predictability. Keywords are sourced from localized strings so
// each language can tune its own detection rules.
//
import Foundation
enum CognitiveDistortionDetector {
/// Detects the most likely cognitive distortion in the given text.
/// Returns `.unknown` if no keywords match the caller should fall back
/// to the generic perspective-check prompt in that case.
///
/// When multiple distortions match, the first one in the priority order below wins.
/// This ordering puts more specific distortions before more general ones.
static func detect(in text: String) -> CognitiveDistortion {
let normalized = text.lowercased()
guard !normalized.trimmingCharacters(in: .whitespaces).isEmpty else {
return .unknown
}
// Priority order: specific general. First hit wins.
let checks: [(CognitiveDistortion, String)] = [
(.catastrophizing, "distortion_catastrophizing_keywords"),
(.mindReading, "distortion_mind_reading_keywords"),
(.personalization, "distortion_personalization_keywords"),
(.labeling, "distortion_labeling_keywords"),
(.shouldStatement, "distortion_should_keywords"),
(.overgeneralization, "distortion_overgeneralization_keywords"),
]
for (distortion, key) in checks {
let keywords = keywordList(forLocalizedKey: key)
if keywords.contains(where: { normalized.contains($0) }) {
return distortion
}
}
return .unknown
}
/// Loads a localized comma-separated keyword list, splits it, and lowercases each entry.
/// Whitespace around entries is trimmed.
private static func keywordList(forLocalizedKey key: String) -> [String] {
let raw = String(localized: String.LocalizationValue(key))
// Guard against an unresolved localization returning the key itself.
guard raw != key else { return [] }
return raw
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespaces).lowercased() }
.filter { !$0.isEmpty }
}
}
// MARK: - Distortion-Specific Question Templates
extension CognitiveDistortion {
/// Returns the perspective-check question template (Q3 in the negative path)
/// tailored to this distortion. The template takes the automatic-thought answer
/// as its `%@` placeholder (placeholderRef: 1).
var perspectiveCheckTemplate: QuestionTemplate {
switch self {
case .overgeneralization:
return QuestionTemplate(
text: String(localized: "guided_reflection_negative_q3_overgeneralization"),
placeholderRef: 1
)
case .shouldStatement:
return QuestionTemplate(
text: String(localized: "guided_reflection_negative_q3_should"),
placeholderRef: 1
)
case .labeling:
return QuestionTemplate(
text: String(localized: "guided_reflection_negative_q3_labeling"),
placeholderRef: 1
)
case .personalization:
return QuestionTemplate(
text: String(localized: "guided_reflection_negative_q3_personalization"),
placeholderRef: 1
)
case .catastrophizing:
return QuestionTemplate(
text: String(localized: "guided_reflection_negative_q3_catastrophizing"),
placeholderRef: 1
)
case .mindReading:
return QuestionTemplate(
text: String(localized: "guided_reflection_negative_q3_mind_reading"),
placeholderRef: 1
)
case .unknown:
// Fallback the generic "what would you tell a friend" prompt.
return QuestionTemplate(
text: String(localized: "guided_reflection_negative_q3_templated"),
placeholderRef: 1
)
}
}
/// A short, user-facing name for the distortion used as the step label above
/// the perspective-check question so users learn the CBT vocabulary.
var stepLabel: String {
switch self {
case .overgeneralization: return String(localized: "Overgeneralization")
case .shouldStatement: return String(localized: "Should Statement")
case .labeling: return String(localized: "Labeling")
case .personalization: return String(localized: "Personalization")
case .catastrophizing: return String(localized: "Catastrophizing")
case .mindReading: return String(localized: "Mind Reading")
case .unknown: return String(localized: "Perspective Check")
}
}
}