// // 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") } } }