import Foundation public enum RegulationFamily: String, Codable, Sendable { case breathe case ground case recenter } public enum RegulationMode: String, Codable, Sendable { case haptic case liveHeartRate } public enum RegulationPhaseKind: String, Codable, Sendable { case inhale case hold case exhale case anchor } public struct RegulationPhase: Codable, Equatable, Sendable, Identifiable { public var id: String { "\(kind.rawValue)-\(durationSeconds)-\(label)" } public let kind: RegulationPhaseKind public let durationSeconds: TimeInterval public let label: String public init(kind: RegulationPhaseKind, durationSeconds: TimeInterval, label: String) { self.kind = kind self.durationSeconds = durationSeconds self.label = label } } public struct WatchRegulationProtocol: Codable, Equatable, Sendable, Identifiable { public let id: String public let version: Int public let family: RegulationFamily public let mode: RegulationMode public let eyebrow: String public let title: String public let summary: String public let durationSeconds: TimeInterval public let phases: [RegulationPhase] public init( id: String, version: Int = 1, family: RegulationFamily, mode: RegulationMode, eyebrow: String, title: String, summary: String, durationSeconds: TimeInterval, phases: [RegulationPhase] ) { self.id = id self.version = version self.family = family self.mode = mode self.eyebrow = eyebrow self.title = title self.summary = summary self.durationSeconds = durationSeconds self.phases = phases } } public enum WatchProtocolCatalog { private struct Manifest: Decodable { let schemaVersion: Int let protocols: [ManifestProtocol] } private struct ManifestProtocol: Decodable { let kind: String let family: RegulationFamily let mode: String let eyebrow: String let title: String let description: String let durationSeconds: TimeInterval let phases: [RegulationPhase] var watchProtocol: WatchRegulationProtocol { WatchRegulationProtocol( id: kind, version: 1, family: family, mode: mode == "haptic" ? .haptic : .liveHeartRate, eyebrow: eyebrow, title: title, summary: description, durationSeconds: durationSeconds, phases: phases ) } } public static let protocols: [WatchRegulationProtocol] = loadBundledManifest() ?? fallbackProtocols private static let fallbackProtocols: [WatchRegulationProtocol] = [ .init(id: "myCoherence", family: .breathe, mode: .liveHeartRate, eyebrow: "BIOFEEDBACK", title: "Ma Cohérence", summary: "Respiration visuelle 5–5 avec tendance cardiaque.", durationSeconds: 180, phases: [ .init(kind: .inhale, durationSeconds: 5, label: "Inspirez"), .init(kind: .exhale, durationSeconds: 5, label: "Expirez"), ]), .init(id: "coherence55", family: .breathe, mode: .haptic, eyebrow: "ÉQUILIBRE", title: "Cohérence 5–5", summary: "Une respiration régulière pour retrouver un rythme calme.", durationSeconds: 300, phases: [ .init(kind: .inhale, durationSeconds: 5, label: "Inspirez"), .init(kind: .exhale, durationSeconds: 5, label: "Expirez lentement"), ]), .init(id: "box4444", family: .breathe, mode: .haptic, eyebrow: "STABILITÉ", title: "Respiration carrée", summary: "Quatre temps égaux pour structurer l’attention.", durationSeconds: 240, phases: [ .init(kind: .inhale, durationSeconds: 4, label: "Inspirez"), .init(kind: .hold, durationSeconds: 4, label: "Gardez l’air"), .init(kind: .exhale, durationSeconds: 4, label: "Expirez"), .init(kind: .hold, durationSeconds: 4, label: "Restez vide"), ]), .init(id: "breathing478", family: .breathe, mode: .haptic, eyebrow: "RALENTISSEMENT", title: "Respiration 4–7–8", summary: "Un cycle court avec une expiration longue.", durationSeconds: 76, phases: [ .init(kind: .inhale, durationSeconds: 4, label: "Inspirez"), .init(kind: .hold, durationSeconds: 7, label: "Gardez l’air"), .init(kind: .exhale, durationSeconds: 8, label: "Expirez lentement"), ]), .init(id: "relax46", family: .breathe, mode: .haptic, eyebrow: "RELÂCHEMENT", title: "Expiration 4–6", summary: "Une expiration plus longue pour accompagner la détente.", durationSeconds: 180, phases: [ .init(kind: .inhale, durationSeconds: 4, label: "Inspirez"), .init(kind: .exhale, durationSeconds: 6, label: "Expirez lentement"), ]), .init(id: "sosStress", family: .recenter, mode: .haptic, eyebrow: "RETOUR AU CALME", title: "SOS tension", summary: "Trois impulsions puis un repère toutes les dix secondes.", durationSeconds: 62, phases: [ .init(kind: .anchor, durationSeconds: 62, label: "Restez avec la sensation"), ]), .init(id: "microAnchor", family: .ground, mode: .haptic, eyebrow: "FORMAT COURT", title: "Micro-ancrage", summary: "Un repère tactile discret pour revenir aux sensations.", durationSeconds: 37, phases: [ .init(kind: .anchor, durationSeconds: 37, label: "Sentez les points de contact"), ]), ] public static func protocolWithID(_ id: String) -> WatchRegulationProtocol? { protocols.first { $0.id == id } } private static func loadBundledManifest(bundle: Bundle = .main) -> [WatchRegulationProtocol]? { guard let url = bundle.url(forResource: "regulation-protocols.v1", withExtension: "json"), let data = try? Data(contentsOf: url), let manifest = try? JSONDecoder().decode(Manifest.self, from: data), manifest.schemaVersion == 1, manifest.protocols.count == 7 else { return nil } return manifest.protocols.map(\.watchProtocol) } } public struct RegulationPhasePosition: Equatable, Sendable { public let phaseIndex: Int public let phase: RegulationPhase public let progress: Double public let cycleIndex: Int } public enum RegulationPhaseEngine { public static func position( in regulation: WatchRegulationProtocol, elapsedSeconds: TimeInterval ) -> RegulationPhasePosition? { guard !regulation.phases.isEmpty else { return nil } let cycleDuration = regulation.phases.reduce(0) { $0 + $1.durationSeconds } guard cycleDuration > 0 else { return nil } let clamped = max(0, min(elapsedSeconds, max(0, regulation.durationSeconds - 0.001))) let cycleElapsed = clamped.truncatingRemainder(dividingBy: cycleDuration) var cursor: TimeInterval = 0 for (index, phase) in regulation.phases.enumerated() { let end = cursor + phase.durationSeconds if cycleElapsed < end || index == regulation.phases.count - 1 { return .init( phaseIndex: index, phase: phase, progress: min(1, max(0, (cycleElapsed - cursor) / phase.durationSeconds)), cycleIndex: Int(clamped / cycleDuration) ) } cursor = end } return nil } }