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>
124 lines
5.0 KiB
Swift
124 lines
5.0 KiB
Swift
//
|
|
// 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")
|
|
}
|
|
}
|
|
}
|