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:
123
Shared/Services/CognitiveDistortionDetector.swift
Normal file
123
Shared/Services/CognitiveDistortionDetector.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user