     1→//
     2→//  BlockStep.swift
     3→//  96Blocks-iOS
     4→//
     5→//  Subtask/step model for breaking 15-minute blocks into smaller actions
     6→//
     7→
     8→import Foundation
     9→import SwiftData
    10→
    11→@Model
    12→final class BlockStep {
    13→    var id: UUID = UUID()
    14→    var blockId: UUID = UUID()           // Reference to parent Block (default for CloudKit)
    15→    var title: String = ""
    16→    var durationSeconds: Int = 300       // Default 5 minutes
    17→    var orderIndex: Int = 0              // Sort order: 0, 1, 2...
    18→    var isCompleted: Bool = false
    19→    var completedAt: Date? = nil
    20→    var createdAt: Date = Date()
    21→
    22→    init(
    23→        id: UUID = UUID(),
    24→        blockId: UUID,
    25→        title: String,
    26→        durationSeconds: Int = 300,
    27→        orderIndex: Int = 0,
    28→        isCompleted: Bool = false
    29→    ) {
    30→        self.id = id
    31→        self.blockId = blockId
    32→        self.title = title.trimmingCharacters(in: .whitespacesAndNewlines)
    33→        self.durationSeconds = max(1, durationSeconds)  // Minimum 1 second
    34→        self.orderIndex = orderIndex
    35→        self.isCompleted = isCompleted
    36→        self.createdAt = Date()
    37→    }
    38→
    39→    // MARK: - Computed Properties
    40→
    41→    /// Duration formatted as MM:SS or HH:MM:SS
    42→    var durationString: String {
    43→        if durationSeconds >= 3600 {
    44→            let hours = durationSeconds / 3600
    45→            let minutes = (durationSeconds % 3600) / 60
    46→            let seconds = durationSeconds % 60
    47→            return String(format: "%d:%02d:%02d", hours, minutes, seconds)
    48→        } else {
    49→            let minutes = durationSeconds / 60
    50→            let seconds = durationSeconds % 60
    51→            return String(format: "%d:%02d", minutes, seconds)
    52→        }
    53→    }
    54→
    55→    /// Duration in minutes (for display)
    56→    var durationMinutes: Double {
    57→        Double(durationSeconds) / 60.0
    58→    }
    59→
    60→    /// Short duration label (e.g., "5m" or "30s")
    61→    var shortDurationLabel: String {
    62→        if durationSeconds >= 60 {
    63→            let minutes = durationSeconds / 60
    64→            let seconds = durationSeconds % 60
    65→            if seconds == 0 {
    66→                return "\(minutes)m"
    67→            } else {
    68→                return "\(minutes)m\(seconds)s"
    69→            }
    70→        } else {
    71→            return "\(durationSeconds)s"
    72→        }
    73→    }
    74→}
    75→
    76→// MARK: - Step Templates
    77→
    78→struct StepTemplate: Identifiable, Codable {
    79→    var id: UUID = UUID()
    80→    var name: String
    81→    var icon: String
    82→    var steps: [TemplateStepItem]
    83→
    84→    struct TemplateStepItem: Codable, Identifiable {
    85→        var id: UUID = UUID()
    86→        var title: String
    87→        var durationSeconds: Int
    88→    }
    89→
    90→    /// Total duration of all steps in seconds
    91→    var totalDurationSeconds: Int {
    92→        steps.reduce(0) { $0 + $1.durationSeconds }
    93→    }
    94→
    95→    /// Total duration formatted
    96→    var totalDurationString: String {
    97→        let minutes = totalDurationSeconds / 60
    98→        return "\(minutes) min"
    99→    }
   100→}
   101→
   102→extension StepTemplate {
   103→    /// Pre-defined system templates
   104→    static var systemTemplates: [StepTemplate] {
   105→        [
   106→            pomodoro5_5_5,
   107→            focus10_5,
   108→            deepWork15,
   109→            meetingAgenda,
   110→            quickTask3x5,
   111→            microSprint
   112→        ]
   113→    }
   114→
   115→    /// Pomodoro 5-5-5: Work, Break, Work (total 15 min)
   116→    static var pomodoro5_5_5: StepTemplate {
   117→        StepTemplate(
   118→            name: "Pomodoro 5-5-5",
   119→            icon: "timer",
   120→            steps: [
   121→                .init(title: "Foco 1", durationSeconds: 300),      // 5 min
   122→                .init(title: "Intervalo", durationSeconds: 300),   // 5 min
   123→                .init(title: "Foco 2", durationSeconds: 300)       // 5 min
   124→            ]
   125→        )
   126→    }
   127→
   128→    /// Focus 10 + Break 5
   129→    static var focus10_5: StepTemplate {
   130→        StepTemplate(
   131→            name: "Foco 10 + Pausa 5",
   132→            icon: "brain.head.profile",
   133→            steps: [
   134→                .init(title: "Trabalho Focado", durationSeconds: 600), // 10 min
   135→                .init(title: "Pausa Curta", durationSeconds: 300)      // 5 min
   136→            ]
   137→        )
   138→    }
   139→
   140→    /// Single Deep Work (no breaks)
   141→    static var deepWork15: StepTemplate {
   142→        StepTemplate(
   143→            name: "Foco Profundo",
   144→            icon: "bolt.fill",
   145→            steps: [
   146→                .init(title: "Foco Profundo", durationSeconds: 900)    // 15 min
   147→            ]
   148→        )
   149→    }
   150→
   151→    /// Meeting agenda pattern
   152→    static var meetingAgenda: StepTemplate {
   153→        StepTemplate(
   154→            name: "Agenda de Reuniao",
   155→            icon: "person.3.fill",
   156→            steps: [
   157→                .init(title: "Abertura", durationSeconds: 120),        // 2 min
   158→                .init(title: "Discussao", durationSeconds: 600),       // 10 min
   159→                .init(title: "Conclusoes", durationSeconds: 180)       // 3 min
   160→            ]
   161→        )
   162→    }
   163→
   164→    /// Quick tasks: 3 x 5 minutes
   165→    static var quickTask3x5: StepTemplate {
   166→        StepTemplate(
   167→            name: "3 Tarefas Rapidas",
   168→            icon: "checklist",
   169→            steps: [
   170→                .init(title: "Tarefa 1", durationSeconds: 300),        // 5 min
   171→                .init(title: "Tarefa 2", durationSeconds: 300),        // 5 min
   172→                .init(title: "Tarefa 3", durationSeconds: 300)         // 5 min
   173→            ]
   174→        )
   175→    }
   176→
   177→    /// Micro Sprint: 5 x 3 minutes (for granular tasks)
   178→    static var microSprint: StepTemplate {
   179→        StepTemplate(
   180→            name: "Micro Sprint",
   181→            icon: "hare.fill",
   182→            steps: [
   183→                .init(title: "Sprint 1", durationSeconds: 180),        // 3 min
   184→                .init(title: "Sprint 2", durationSeconds: 180),        // 3 min
   185→                .init(title: "Sprint 3", durationSeconds: 180),        // 3 min
   186→                .init(title: "Sprint 4", durationSeconds: 180),        // 3 min
   187→                .init(title: "Sprint 5", durationSeconds: 180)         // 3 min
   188→            ]
   189→        )
   190→    }
   191→}
   192→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//
//  BlockStep.swift
//  96Blocks-iOS
//
//  Subtask/step model for breaking 15-minute blocks into smaller actions
//

import Foundation
import SwiftData

@Model
final class BlockStep {
    var id: UUID = UUID()
    var blockId: UUID = UUID()           // Reference to parent Block (default for CloudKit)
    var title: String = ""
    var durationSeconds: Int = 300       // Default 5 minutes
    var orderIndex: Int = 0              // Sort order: 0, 1, 2...
    var isCompleted: Bool = false
    var completedAt: Date? = nil
    var createdAt: Date = Date()

    init(
        id: UUID = UUID(),
        blockId: UUID,
        title: String,
        durationSeconds: Int = 300,
        orderIndex: Int = 0,
        isCompleted: Bool = false
    ) {
        self.id = id
        self.blockId = blockId
        self.title = title.trimmingCharacters(in: .whitespacesAndNewlines)
        self.durationSeconds = max(1, durationSeconds)  // Minimum 1 second
        self.orderIndex = orderIndex
        self.isCompleted = isCompleted
        self.createdAt = Date()
    }

    // MARK: - Computed Properties

    /// Duration formatted as MM:SS or HH:MM:SS
    var durationString: String {
        if durationSeconds >= 3600 {
            let hours = durationSeconds / 3600
            let minutes = (durationSeconds % 3600) / 60
            let seconds = durationSeconds % 60
            return String(format: "%d:%02d:%02d", hours, minutes, seconds)
        } else {
            let minutes = durationSeconds / 60
            let seconds = durationSeconds % 60
            return String(format: "%d:%02d", minutes, seconds)
        }
    }

    /// Duration in minutes (for display)
    var durationMinutes: Double {
        Double(durationSeconds) / 60.0
    }

    /// Short duration label (e.g., "5m" or "30s")
    var shortDurationLabel: String {
        if durationSeconds >= 60 {
            let minutes = durationSeconds / 60
            let seconds = durationSeconds % 60
            if seconds == 0 {
                return "\(minutes)m"
            } else {
                return "\(minutes)m\(seconds)s"
            }
        } else {
            return "\(durationSeconds)s"
        }
    }
}

// MARK: - Step Templates

struct StepTemplate: Identifiable, Codable {
    var id: UUID = UUID()
    var name: String
    var icon: String
    var steps: [TemplateStepItem]

    struct TemplateStepItem: Codable, Identifiable {
        var id: UUID = UUID()
        var title: String
        var durationSeconds: Int
    }

    /// Total duration of all steps in seconds
    var totalDurationSeconds: Int {
        steps.reduce(0) { $0 + $1.durationSeconds }
    }

    /// Total duration formatted
    var totalDurationString: String {
        let minutes = totalDurationSeconds / 60
        return "\(minutes) min"
    }
}

extension StepTemplate {
    /// Pre-defined system templates
    static var systemTemplates: [StepTemplate] {
        [
            pomodoro5_5_5,
            focus10_5,
            deepWork15,
            meetingAgenda,
            quickTask3x5,
            microSprint
        ]
    }

    /// Pomodoro 5-5-5: Work, Break, Work (total 15 min)
    static var pomodoro5_5_5: StepTemplate {
        StepTemplate(
            name: "Pomodoro 5-5-5",
            icon: "timer",
            steps: [
                .init(title: "Foco 1", durationSeconds: 300),      // 5 min
                .init(title: "Intervalo", durationSeconds: 300),   // 5 min
                .init(title: "Foco 2", durationSeconds: 300)       // 5 min
            ]
        )
    }

    /// Focus 10 + Break 5
    static var focus10_5: StepTemplate {
        StepTemplate(
            name: "Foco 10 + Pausa 5",
            icon: "brain.head.profile",
            steps: [
                .init(title: "Trabalho Focado", durationSeconds: 600), // 10 min
                .init(title: "Pausa Curta", durationSeconds: 300)      // 5 min
            ]
        )
    }

    /// Single Deep Work (no breaks)
    static var deepWork15: StepTemplate {
        StepTemplate(
            name: "Foco Profundo",
            icon: "bolt.fill",
            steps: [
                .init(title: "Foco Profundo", durationSeconds: 900)    // 15 min
            ]
        )
    }

    /// Meeting agenda pattern
    static var meetingAgenda: StepTemplate {
        StepTemplate(
            name: "Agenda de Reuniao",
            icon: "person.3.fill",
            steps: [
                .init(title: "Abertura", durationSeconds: 120),        // 2 min
                .init(title: "Discussao", durationSeconds: 600),       // 10 min
                .init(title: "Conclusoes", durationSeconds: 180)       // 3 min
            ]
        )
    }

    /// Quick tasks: 3 x 5 minutes
    static var quickTask3x5: StepTemplate {
        StepTemplate(
            name: "3 Tarefas Rapidas",
            icon: "checklist",
            steps: [
                .init(title: "Tarefa 1", durationSeconds: 300),        // 5 min
                .init(title: "Tarefa 2", durationSeconds: 300),        // 5 min
                .init(title: "Tarefa 3", durationSeconds: 300)         // 5 min
            ]
        )
    }

    /// Micro Sprint: 5 x 3 minutes (for granular tasks)
    static var microSprint: StepTemplate {
        StepTemplate(
            name: "Micro Sprint",
            icon: "hare.fill",
            steps: [
                .init(title: "Sprint 1", durationSeconds: 180),        // 3 min
                .init(title: "Sprint 2", durationSeconds: 180),        // 3 min
                .init(title: "Sprint 3", durationSeconds: 180),        // 3 min
                .init(title: "Sprint 4", durationSeconds: 180),        // 3 min
                .init(title: "Sprint 5", durationSeconds: 180)         // 3 min
            ]
        )
    }
}


Perfect! Now let me check the project configuration to understand iOS version and dependencies:


     1→<?xml version="1.0" encoding="UTF-8"?>
     2→<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
     3→<plist version="1.0">
     4→<dict>
     5→	<key>CFBundleDevelopmentRegion</key>
     6→	<string>$(DEVELOPMENT_LANGUAGE)</string>
     7→	<key>CFBundleExecutable</key>
     8→	<string>$(EXECUTABLE_NAME)</string>
     9→	<key>CFBundleIdentifier</key>
    10→	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    11→	<key>CFBundleInfoDictionaryVersion</key>
    12→	<string>6.0</string>
    13→	<key>CFBundleName</key>
    14→	<string>$(PRODUCT_NAME)</string>
    15→	<key>CFBundlePackageType</key>
    16→	<string>APPL</string>
    17→	<key>CFBundleShortVersionString</key>
    18→	<string>1.0</string>
    19→	<key>CFBundleVersion</key>
    20→	<string>1</string>
    21→	<key>UIBackgroundModes</key>
    22→	<array>
    23→		<string>remote-notification</string>
    24→		<string>fetch</string>
    25→		<string>processing</string>
    26→	</array>
    27→	<key>BGTaskSchedulerPermittedIdentifiers</key>
    28→	<array>
    29→		<string>com.neog.96Blocks-iOS.timer-refresh</string>
    30→		<string>com.neog.96Blocks-iOS.data-sync</string>
    31→	</array>
    32→	<key>NSLocalNetworkUsageDescription</key>
    33→	<string>96Blocks precisa acessar a rede local para conectar ao servidor LM Studio para geracao de planos com IA.</string>
    34→	<key>NSBonjourServices</key>
    35→	<array>
    36→		<string>_http._tcp</string>
    37→		<string>_https._tcp</string>
    38→	</array>
    39→</dict>
    40→</plist>
    41→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>
	<string>$(DEVELOPMENT_LANGUAGE)</string>
	<key>CFBundleExecutable</key>
	<string>$(EXECUTABLE_NAME)</string>
	<key>CFBundleIdentifier</key>
	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>$(PRODUCT_NAME)</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>1.0</string>
	<key>CFBundleVersion</key>
	<string>1</string>
	<key>UIBackgroundModes</key>
	<array>
		<string>remote-notification</string>
		<string>fetch</string>
		<string>processing</string>
	</array>
	<key>BGTaskSchedulerPermittedIdentifiers</key>
	<array>
		<string>com.neog.96Blocks-iOS.timer-refresh</string>
		<string>com.neog.96Blocks-iOS.data-sync</string>
	</array>
	<key>NSLocalNetworkUsageDescription</key>
	<string>96Blocks precisa acessar a rede local para conectar ao servidor LM Studio para geracao de planos com IA.</string>
	<key>NSBonjourServices</key>
	<array>
		<string>_http._tcp</string>
		<string>_https._tcp</string>
	</array>
</dict>
</plist>


     1→name: 96Blocks-iOS
     2→options:
     3→  bundleIdPrefix: com.neog
     4→  deploymentTarget:
     5→    iOS: "17.0"
     6→  xcodeVersion: "15.0"
     7→  createIntermediateGroups: true
     8→  generateEmptyDirectories: true
     9→
    10→settings:
    11→  base:
    12→    SWIFT_VERSION: "5.0"
    13→    GENERATE_INFOPLIST_FILE: YES
    14→    CURRENT_PROJECT_VERSION: 1
    15→    MARKETING_VERSION: 1.0.0
    16→
    17→targets:
    18→  96Blocks-iOS:
    19→    type: application
    20→    platform: iOS
    21→    deploymentTarget: "17.0"
    22→    sources:
    23→      - path: App
    24→      - path: Models
    25→      - path: Services
    26→      - path: Views
    27→      - path: Components
    28→    entitlements:
    29→      path: 96Blocks-iOS.entitlements
    30→    info:
    31→      path: Info.plist
    32→      properties:
    33→        UIBackgroundModes: [remote-notification]
    34→    settings:
    35→      base:
    36→        INFOPLIST_KEY_CFBundleDisplayName: "96 Blocks"
    37→        INFOPLIST_KEY_UIApplicationSceneManifest_Generation: YES
    38→        INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents: YES
    39→        INFOPLIST_KEY_UILaunchScreen_Generation: YES
    40→        INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"
    41→        INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad: "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"
    42→        SUPPORTS_MACCATALYST: YES
    43→        CODE_SIGN_STYLE: Automatic
    44→        CODE_SIGN_ENTITLEMENTS: 96Blocks-iOS.entitlements
    45→        ENABLE_PREVIEWS: YES
    46→        PRODUCT_BUNDLE_IDENTIFIER: com.neog.96Blocks-iOS
    47→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

name: 96Blocks-iOS
options:
  bundleIdPrefix: com.neog
  deploymentTarget:
    iOS: "17.0"
  xcodeVersion: "15.0"
  createIntermediateGroups: true
  generateEmptyDirectories: true

settings:
  base:
    SWIFT_VERSION: "5.0"
    GENERATE_INFOPLIST_FILE: YES
    CURRENT_PROJECT_VERSION: 1
    MARKETING_VERSION: 1.0.0

targets:
  96Blocks-iOS:
    type: application
    platform: iOS
    deploymentTarget: "17.0"
    sources:
      - path: App
      - path: Models
      - path: Services
      - path: Views
      - path: Components
    entitlements:
      path: 96Blocks-iOS.entitlements
    info:
      path: Info.plist
      properties:
        UIBackgroundModes: [remote-notification]
    settings:
      base:
        INFOPLIST_KEY_CFBundleDisplayName: "96 Blocks"
        INFOPLIST_KEY_UIApplicationSceneManifest_Generation: YES
        INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents: YES
        INFOPLIST_KEY_UILaunchScreen_Generation: YES
        INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"
        INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad: "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"
        SUPPORTS_MACCATALYST: YES
        CODE_SIGN_STYLE: Automatic
        CODE_SIGN_ENTITLEMENTS: 96Blocks-iOS.entitlements
        ENABLE_PREVIEWS: YES
        PRODUCT_BUNDLE_IDENTIFIER: com.neog.96Blocks-iOS


Excellent! Let me now get a quick overview of the validation services to understand the patterns:


     1→//
     2→//  ValidationResult.swift
     3→//  96Blocks-iOS
     4→//
     5→//  Created on 2025-12-08.
     6→//
     7→
     8→import Foundation
     9→
    10→// MARK: - Validation Error Code
    11→
    12→enum ValidationErrorCode: String, CaseIterable {
    13→    case required = "REQUIRED"
    14→    case invalidRange = "INVALID_RANGE"
    15→    case invalidFormat = "INVALID_FORMAT"
    16→    case invalidReference = "INVALID_REFERENCE"
    17→    case businessRule = "BUSINESS_RULE"
    18→    case duplicate = "DUPLICATE"
    19→    case tooLong = "TOO_LONG"
    20→    case tooShort = "TOO_SHORT"
    21→    case invalidDate = "INVALID_DATE"
    22→    case negativeValue = "NEGATIVE_VALUE"
    23→
    24→    var localizedDescription: String {
    25→        switch self {
    26→        case .required: return "Campo obrigatorio"
    27→        case .invalidRange: return "Valor fora do intervalo permitido"
    28→        case .invalidFormat: return "Formato invalido"
    29→        case .invalidReference: return "Referencia invalida"
    30→        case .businessRule: return "Regra de negocio violada"
    31→        case .duplicate: return "Valor duplicado"
    32→        case .tooLong: return "Valor muito longo"
    33→        case .tooShort: return "Valor muito curto"
    34→        case .invalidDate: return "Data invalida"
    35→        case .negativeValue: return "Valor nao pode ser negativo"
    36→        }
    37→    }
    38→}
    39→
    40→// MARK: - Validation Error
    41→
    42→struct ValidationError: Error, Identifiable, Equatable {
    43→    let id: UUID
    44→    let field: String
    45→    let message: String
    46→    let code: ValidationErrorCode
    47→
    48→    init(field: String, message: String, code: ValidationErrorCode) {
    49→        self.id = UUID()
    50→        self.field = field
    51→        self.message = message
    52→        self.code = code
    53→    }
    54→
    55→    static func == (lhs: ValidationError, rhs: ValidationError) -> Bool {
    56→        lhs.field == rhs.field && lhs.code == rhs.code
    57→    }
    58→}
    59→
    60→// MARK: - Validation Result
    61→
    62→enum ValidationResult<T> {
    63→    case valid(T)
    64→    case invalid([ValidationError])
    65→
    66→    var isValid: Bool {
    67→        if case .valid = self { return true }
    68→        return false
    69→    }
    70→
    71→    var errors: [ValidationError] {
    72→        if case .invalid(let errors) = self { return errors }
    73→        return []
    74→    }
    75→
    76→    var value: T? {
    77→        if case .valid(let value) = self { return value }
    78→        return nil
    79→    }
    80→
    81→    var firstError: ValidationError? {
    82→        errors.first
    83→    }
    84→
    85→    var errorMessages: [String] {
    86→        errors.map { $0.message }
    87→    }
    88→
    89→    var errorSummary: String {
    90→        errorMessages.joined(separator: "; ")
    91→    }
    92→
    93→    // MARK: - Combinators
    94→
    95→    func map<U>(_ transform: (T) -> U) -> ValidationResult<U> {
    96→        switch self {
    97→        case .valid(let value):
    98→            return .valid(transform(value))
    99→        case .invalid(let errors):
   100→            return .invalid(errors)
   101→        }
   102→    }
   103→
   104→    func flatMap<U>(_ transform: (T) -> ValidationResult<U>) -> ValidationResult<U> {
   105→        switch self {
   106→        case .valid(let value):
   107→            return transform(value)
   108→        case .invalid(let errors):
   109→            return .invalid(errors)
   110→        }
   111→    }
   112→
   113→    // Combine two validation results
   114→    static func combine<A, B>(_ r1: ValidationResult<A>, _ r2: ValidationResult<B>) -> ValidationResult<(A, B)> {
   115→        switch (r1, r2) {
   116→        case (.valid(let a), .valid(let b)):
   117→            return .valid((a, b))
   118→        case (.invalid(let e1), .invalid(let e2)):
   119→            return .invalid(e1 + e2)
   120→        case (.invalid(let e), _), (_, .invalid(let e)):
   121→            return .invalid(e)
   122→        }
   123→    }
   124→}
   125→
   126→// MARK: - Validation Builder
   127→
   128→class ValidationBuilder<T> {
   129→    private var errors: [ValidationError] = []
   130→    private let value: T
   131→
   132→    init(_ value: T) {
   133→        self.value = value
   134→    }
   135→
   136→    @discardableResult
   137→    func validate(_ condition: Bool, field: String, message: String, code: ValidationErrorCode) -> ValidationBuilder<T> {
   138→        if !condition {
   139→            errors.append(ValidationError(field: field, message: message, code: code))
   140→        }
   141→        return self
   142→    }
   143→
   144→    @discardableResult
   145→    func validateNotEmpty(_ keyPath: KeyPath<T, String>, field: String, message: String? = nil) -> ValidationBuilder<T> {
   146→        let str = value[keyPath: keyPath].trimmingCharacters(in: .whitespacesAndNewlines)
   147→        if str.isEmpty {
   148→            errors.append(ValidationError(
   149→                field: field,
   150→                message: message ?? "\(field) e obrigatorio",
   151→                code: .required
   152→            ))
   153→        }
   154→        return self
   155→    }
   156→
   157→    @discardableResult
   158→    func validateRange<V: Comparable>(_ keyPath: KeyPath<T, V>, field: String, min: V, max: V, message: String? = nil) -> ValidationBuilder<T> {
   159→        let val = value[keyPath: keyPath]
   160→        if val < min || val > max {
   161→            errors.append(ValidationError(
   162→                field: field,
   163→                message: message ?? "\(field) deve estar entre \(min) e \(max)",
   164→                code: .invalidRange
   165→            ))
   166→        }
   167→        return self
   168→    }
   169→
   170→    @discardableResult
   171→    func validateNonNegative<V: Numeric & Comparable>(_ keyPath: KeyPath<T, V>, field: String, message: String? = nil) -> ValidationBuilder<T> {
   172→        let val = value[keyPath: keyPath]
   173→        let zero: V = 0
   174→        if val < zero {
   175→            errors.append(ValidationError(
   176→                field: field,
   177→                message: message ?? "\(field) nao pode ser negativo",
   178→                code: .negativeValue
   179→            ))
   180→        }
   181→        return self
   182→    }
   183→
   184→    @discardableResult
   185→    func validatePositive<V: Numeric & Comparable>(_ keyPath: KeyPath<T, V>, field: String, message: String? = nil) -> ValidationBuilder<T> {
   186→        let val = value[keyPath: keyPath]
   187→        let zero: V = 0
   188→        if val <= zero {
   189→            errors.append(ValidationError(
   190→                field: field,
   191→                message: message ?? "\(field) deve ser maior que zero",
   192→                code: .invalidRange
   193→            ))
   194→        }
   195→        return self
   196→    }
   197→
   198→    @discardableResult
   199→    func validateDateNotInPast(_ keyPath: KeyPath<T, Date?>, field: String, allowToday: Bool = true, message: String? = nil) -> ValidationBuilder<T> {
   200→        guard let date = value[keyPath: keyPath] else { return self }
   201→
   202→        let comparison = allowToday ? Calendar.current.startOfDay(for: Date()) : Date()
   203→        if date < comparison {
   204→            errors.append(ValidationError(
   205→                field: field,
   206→                message: message ?? "\(field) nao pode estar no passado",
   207→                code: .invalidDate
   208→            ))
   209→        }
   210→        return self
   211→    }
   212→
   213→    @discardableResult
   214→    func validateMaxLength(_ keyPath: KeyPath<T, String>, field: String, max: Int, message: String? = nil) -> ValidationBuilder<T> {
   215→        let str = value[keyPath: keyPath]
   216→        if str.count > max {
   217→            errors.append(ValidationError(
   218→                field: field,
   219→                message: message ?? "\(field) deve ter no maximo \(max) caracteres",
   220→                code: .tooLong
   221→            ))
   222→        }
   223→        return self
   224→    }
   225→
   226→    func build() -> ValidationResult<T> {
   227→        if errors.isEmpty {
   228→            return .valid(value)
   229→        } else {
   230→            return .invalid(errors)
   231→        }
   232→    }
   233→}
   234→
   235→// MARK: - Extensions
   236→
   237→extension ValidationResult: CustomStringConvertible {
   238→    var description: String {
   239→        switch self {
   240→        case .valid:
   241→            return "Valid"
   242→        case .invalid(let errors):
   243→            return "Invalid: \(errors.map { $0.message }.joined(separator: ", "))"
   244→        }
   245→    }
   246→}
   247→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//
//  ValidationResult.swift
//  96Blocks-iOS
//
//  Created on 2025-12-08.
//

import Foundation

// MARK: - Validation Error Code

enum ValidationErrorCode: String, CaseIterable {
    case required = "REQUIRED"
    case invalidRange = "INVALID_RANGE"
    case invalidFormat = "INVALID_FORMAT"
    case invalidReference = "INVALID_REFERENCE"
    case businessRule = "BUSINESS_RULE"
    case duplicate = "DUPLICATE"
    case tooLong = "TOO_LONG"
    case tooShort = "TOO_SHORT"
    case invalidDate = "INVALID_DATE"
    case negativeValue = "NEGATIVE_VALUE"

    var localizedDescription: String {
        switch self {
        case .required: return "Campo obrigatorio"
        case .invalidRange: return "Valor fora do intervalo permitido"
        case .invalidFormat: return "Formato invalido"
        case .invalidReference: return "Referencia invalida"
        case .businessRule: return "Regra de negocio violada"
        case .duplicate: return "Valor duplicado"
        case .tooLong: return "Valor muito longo"
        case .tooShort: return "Valor muito curto"
        case .invalidDate: return "Data invalida"
        case .negativeValue: return "Valor nao pode ser negativo"
        }
    }
}

// MARK: - Validation Error

struct ValidationError: Error, Identifiable, Equatable {
    let id: UUID
    let field: String
    let message: String
    let code: ValidationErrorCode

    init(field: String, message: String, code: ValidationErrorCode) {
        self.id = UUID()
        self.field = field
        self.message = message
        self.code = code
    }

    static func == (lhs: ValidationError, rhs: ValidationError) -> Bool {
        lhs.field == rhs.field && lhs.code == rhs.code
    }
}

// MARK: - Validation Result

enum ValidationResult<T> {
    case valid(T)
    case invalid([ValidationError])

    var isValid: Bool {
        if case .valid = self { return true }
        return false
    }

    var errors: [ValidationError] {
        if case .invalid(let errors) = self { return errors }
        return []
    }

    var value: T? {
        if case .valid(let value) = self { return value }
        return nil
    }

    var firstError: ValidationError? {
        errors.first
    }

    var errorMessages: [String] {
        errors.map { $0.message }
    }

    var errorSummary: String {
        errorMessages.joined(separator: "; ")
    }

    // MARK: - Combinators

    func map<U>(_ transform: (T) -> U) -> ValidationResult<U> {
        switch self {
        case .valid(let value):
            return .valid(transform(value))
        case .invalid(let errors):
            return .invalid(errors)
        }
    }

    func flatMap<U>(_ transform: (T) -> ValidationResult<U>) -> ValidationResult<U> {
        switch self {
        case .valid(let value):
            return transform(value)
        case .invalid(let errors):
            return .invalid(errors)
        }
    }

    // Combine two validation results
    static func combine<A, B>(_ r1: ValidationResult<A>, _ r2: ValidationResult<B>) -> ValidationResult<(A, B)> {
        switch (r1, r2) {
        case (.valid(let a), .valid(let b)):
            return .valid((a, b))
        case (.invalid(let e1), .invalid(let e2)):
            return .invalid(e1 + e2)
        case (.invalid(let e), _), (_, .invalid(let e)):
            return .invalid(e)
        }
    }
}

// MARK: - Validation Builder

class ValidationBuilder<T> {
    private var errors: [ValidationError] = []
    private let value: T

    init(_ value: T) {
        self.value = value
    }

    @discardableResult
    func validate(_ condition: Bool, field: String, message: String, code: ValidationErrorCode) -> ValidationBuilder<T> {
        if !condition {
            errors.append(ValidationError(field: field, message: message, code: code))
        }
        return self
    }

    @discardableResult
    func validateNotEmpty(_ keyPath: KeyPath<T, String>, field: String, message: String? = nil) -> ValidationBuilder<T> {
        let str = value[keyPath: keyPath].trimmingCharacters(in: .whitespacesAndNewlines)
        if str.isEmpty {
            errors.append(ValidationError(
                field: field,
                message: message ?? "\(field) e obrigatorio",
                code: .required
            ))
        }
        return self
    }

    @discardableResult
    func validateRange<V: Comparable>(_ keyPath: KeyPath<T, V>, field: String, min: V, max: V, message: String? = nil) -> ValidationBuilder<T> {
        let val = value[keyPath: keyPath]
        if val < min || val > max {
            errors.append(ValidationError(
                field: field,
                message: message ?? "\(field) deve estar entre \(min) e \(max)",
                code: .invalidRange
            ))
        }
        return self
    }

    @discardableResult
    func validateNonNegative<V: Numeric & Comparable>(_ keyPath: KeyPath<T, V>, field: String, message: String? = nil) -> ValidationBuilder<T> {
        let val = value[keyPath: keyPath]
        let zero: V = 0
        if val < zero {
            errors.append(ValidationError(
                field: field,
                message: message ?? "\(field) nao pode ser negativo",
                code: .negativeValue
            ))
        }
        return self
    }

    @discardableResult
    func validatePositive<V: Numeric & Comparable>(_ keyPath: KeyPath<T, V>, field: String, message: String? = nil) -> ValidationBuilder<T> {
        let val = value[keyPath: keyPath]
        let zero: V = 0
        if val <= zero {
            errors.append(ValidationError(
                field: field,
                message: message ?? "\(field) deve ser maior que zero",
                code: .invalidRange
            ))
        }
        return self
    }

    @discardableResult
    func validateDateNotInPast(_ keyPath: KeyPath<T, Date?>, field: String, allowToday: Bool = true, message: String? = nil) -> ValidationBuilder<T> {
        guard let date = value[keyPath: keyPath] else { return self }

        let comparison = allowToday ? Calendar.current.startOfDay(for: Date()) : Date()
        if date < comparison {
            errors.append(ValidationError(
                field: field,
                message: message ?? "\(field) nao pode estar no passado",
                code: .invalidDate
            ))
        }
        return self
    }

    @discardableResult
    func validateMaxLength(_ keyPath: KeyPath<T, String>, field: String, max: Int, message: String? = nil) -> ValidationBuilder<T> {
        let str = value[keyPath: keyPath]
        if str.count > max {
            errors.append(ValidationError(
                field: field,
                message: message ?? "\(field) deve ter no maximo \(max) caracteres",
                code: .tooLong
            ))
        }
        return self
    }

    func build() -> ValidationResult<T> {
        if errors.isEmpty {
            return .valid(value)
        } else {
            return .invalid(errors)
        }
    }
}

// MARK: - Extensions

extension ValidationResult: CustomStringConvertible {
    var description: String {
        switch self {
        case .valid:
            return "Valid"
        case .invalid(let errors):
            return "Invalid: \(errors.map { $0.message }.joined(separator: ", "))"
        }
    }
}


     1→//
     2→//  GoalValidator.swift
     3→//  96Blocks-iOS
     4→//
     5→//  Created on 2025-12-08.
     6→//
     7→
     8→import Foundation
     9→
    10→struct GoalValidator {
    11→
    12→    // MARK: - Constants
    13→
    14→    static let minTitleLength = 1
    15→    static let maxTitleLength = 200
    16→    static let maxDescriptionLength = 2000
    17→    static let maxPastDays = 30  // Allow goals to start up to 30 days in the past
    18→
    19→    // MARK: - Main Validation
    20→
    21→    static func validate(_ goal: Goal) -> ValidationResult<Goal> {
    22→        var errors: [ValidationError] = []
    23→
    24→        // 1. Title is required
    25→        if goal.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
    26→            errors.append(ValidationError(
    27→                field: "title",
    28→                message: "Titulo da meta e obrigatorio",
    29→                code: .required
    30→            ))
    31→        }
    32→
    33→        // 2. Title length
    34→        if goal.title.count > maxTitleLength {
    35→            errors.append(ValidationError(
    36→                field: "title",
    37→                message: "Titulo deve ter no maximo \(maxTitleLength) caracteres",
    38→                code: .tooLong
    39→            ))
    40→        }
    41→
    42→        // 3. Target value must be positive
    43→        if goal.targetValueBRL <= 0 {
    44→            errors.append(ValidationError(
    45→                field: "targetValueBRL",
    46→                message: "Valor alvo deve ser maior que zero",
    47→                code: .invalidRange
    48→            ))
    49→        }
    50→
    51→        // 4. Current value (valueGeneratedBRL) cannot be negative
    52→        if goal.valueGeneratedBRL < 0 {
    53→            errors.append(ValidationError(
    54→                field: "valueGeneratedBRL",
    55→                message: "Valor gerado nao pode ser negativo",
    56→                code: .negativeValue
    57→            ))
    58→        }
    59→
    60→        // 5. Current value should not exceed target
    61→        if goal.valueGeneratedBRL > goal.targetValueBRL {
    62→            errors.append(ValidationError(
    63→                field: "valueGeneratedBRL",
    64→                message: "Valor gerado nao pode exceder o valor alvo",
    65→                code: .invalidRange
    66→            ))
    67→        }
    68→
    69→        // 6. Due date validation (if set, should be in future for active goals)
    70→        if let dueDate = goal.dueDate, goal.status == .active {
    71→            if dueDate < Date() {
    72→                errors.append(ValidationError(
    73→                    field: "dueDate",
    74→                    message: "Data limite ja passou",
    75→                    code: .invalidDate
    76→                ))
    77→            }
    78→        }
    79→
    80→        // 7. Created date should not be too far in the past
    81→        if !isStartDateValid(goal.createdAt) {
    82→            errors.append(ValidationError(
    83→                field: "createdAt",
    84→                message: "Data de criacao muito antiga (mais de \(maxPastDays) dias)",
    85→                code: .invalidDate
    86→            ))
    87→        }
    88→
    89→        // 8. Description length
    90→        if goal.goalDescription.count > maxDescriptionLength {
    91→            errors.append(ValidationError(
    92→                field: "goalDescription",
    93→                message: "Descricao deve ter no maximo \(maxDescriptionLength) caracteres",
    94→                code: .tooLong
    95→            ))
    96→        }
    97→
    98→        // 9. If completed, value should meet target
    99→        if goal.status == .completed && goal.valueGeneratedBRL < goal.targetValueBRL {
   100→            errors.append(ValidationError(
   101→                field: "valueGeneratedBRL",
   102→                message: "Meta concluida deve ter valor gerado igual ou maior que o alvo",
   103→                code: .businessRule
   104→            ))
   105→        }
   106→
   107→        return errors.isEmpty ? .valid(goal) : .invalid(errors)
   108→    }
   109→
   110→    // MARK: - Quick Validations
   111→
   112→    static func validateTitle(_ title: String) -> ValidationResult<String> {
   113→        let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines)
   114→
   115→        if trimmed.isEmpty {
   116→            return .invalid([ValidationError(
   117→                field: "title",
   118→                message: "Titulo da meta e obrigatorio",
   119→                code: .required
   120→            )])
   121→        }
   122→
   123→        if trimmed.count > maxTitleLength {
   124→            return .invalid([ValidationError(
   125→                field: "title",
   126→                message: "Titulo deve ter no maximo \(maxTitleLength) caracteres",
   127→                code: .tooLong
   128→            )])
   129→        }
   130→
   131→        return .valid(trimmed)
   132→    }
   133→
   134→    static func validateDateRange(created: Date, due: Date?) -> ValidationResult<(Date, Date?)> {
   135→        var errors: [ValidationError] = []
   136→
   137→        if let dueDate = due, dueDate <= created {
   138→            errors.append(ValidationError(
   139→                field: "dueDate",
   140→                message: "Data limite deve ser posterior a data de criacao",
   141→                code: .invalidDate
   142→            ))
   143→        }
   144→
   145→        if !isStartDateValid(created) {
   146→            errors.append(ValidationError(
   147→                field: "createdAt",
   148→                message: "Data de criacao muito antiga",
   149→                code: .invalidDate
   150→            ))
   151→        }
   152→
   153→        return errors.isEmpty ? .valid((created, due)) : .invalid(errors)
   154→    }
   155→
   156→    static func validateProgress(generated: Decimal, target: Decimal) -> ValidationResult<Double> {
   157→        var errors: [ValidationError] = []
   158→
   159→        if generated < 0 {
   160→            errors.append(ValidationError(
   161→                field: "valueGeneratedBRL",
   162→                message: "Valor gerado nao pode ser negativo",
   163→                code: .negativeValue
   164→            ))
   165→        }
   166→
   167→        if target <= 0 {
   168→            errors.append(ValidationError(
   169→                field: "targetValueBRL",
   170→                message: "Valor alvo deve ser maior que zero",
   171→                code: .invalidRange
   172→            ))
   173→        }
   174→
   175→        if generated > target {
   176→            errors.append(ValidationError(
   177→                field: "valueGeneratedBRL",
   178→                message: "Valor gerado nao pode exceder o valor alvo",
   179→                code: .invalidRange
   180→            ))
   181→        }
   182→
   183→        let progress = target > 0 ? Double(truncating: generated as NSNumber) / Double(truncating: target as NSNumber) : 0
   184→        return errors.isEmpty ? .valid(progress) : .invalid(errors)
   185→    }
   186→
   187→    // MARK: - Batch Validation
   188→
   189→    static func validateAll(_ goals: [Goal]) -> [(Goal, ValidationResult<Goal>)] {
   190→        return goals.map { ($0, validate($0)) }
   191→    }
   192→
   193→    static func findInvalidGoals(_ goals: [Goal]) -> [Goal] {
   194→        return goals.filter { !validate($0).isValid }
   195→    }
   196→
   197→    // MARK: - Helper Methods
   198→
   199→    private static func isStartDateValid(_ date: Date) -> Bool {
   200→        guard let pastLimit = Calendar.current.date(byAdding: .day, value: -maxPastDays, to: Date()) else {
   201→            return true
   202→        }
   203→        return date >= pastLimit
   204→    }
   205→
   206→    // MARK: - Logging Integration
   207→
   208→    static func validateAndLog(_ goal: Goal) -> ValidationResult<Goal> {
   209→        let result = validate(goal)
   210→
   211→        if !result.isValid {
   212→            logWarning(.validation, "Meta invalida: \(result.errorSummary)", metadata: [
   213→                "goalId": goal.id.uuidString,
   214→                "title": goal.title,
   215→                "errors": "\(result.errors.count)"
   216→            ])
   217→        }
   218→
   219→        return result
   220→    }
   221→}
   222→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//
//  GoalValidator.swift
//  96Blocks-iOS
//
//  Created on 2025-12-08.
//

import Foundation

struct GoalValidator {

    // MARK: - Constants

    static let minTitleLength = 1
    static let maxTitleLength = 200
    static let maxDescriptionLength = 2000
    static let maxPastDays = 30  // Allow goals to start up to 30 days in the past

    // MARK: - Main Validation

    static func validate(_ goal: Goal) -> ValidationResult<Goal> {
        var errors: [ValidationError] = []

        // 1. Title is required
        if goal.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            errors.append(ValidationError(
                field: "title",
                message: "Titulo da meta e obrigatorio",
                code: .required
            ))
        }

        // 2. Title length
        if goal.title.count > maxTitleLength {
            errors.append(ValidationError(
                field: "title",
                message: "Titulo deve ter no maximo \(maxTitleLength) caracteres",
                code: .tooLong
            ))
        }

        // 3. Target value must be positive
        if goal.targetValueBRL <= 0 {
            errors.append(ValidationError(
                field: "targetValueBRL",
                message: "Valor alvo deve ser maior que zero",
                code: .invalidRange
            ))
        }

        // 4. Current value (valueGeneratedBRL) cannot be negative
        if goal.valueGeneratedBRL < 0 {
            errors.append(ValidationError(
                field: "valueGeneratedBRL",
                message: "Valor gerado nao pode ser negativo",
                code: .negativeValue
            ))
        }

        // 5. Current value should not exceed target
        if goal.valueGeneratedBRL > goal.targetValueBRL {
            errors.append(ValidationError(
                field: "valueGeneratedBRL",
                message: "Valor gerado nao pode exceder o valor alvo",
                code: .invalidRange
            ))
        }

        // 6. Due date validation (if set, should be in future for active goals)
        if let dueDate = goal.dueDate, goal.status == .active {
            if dueDate < Date() {
                errors.append(ValidationError(
                    field: "dueDate",
                    message: "Data limite ja passou",
                    code: .invalidDate
                ))
            }
        }

        // 7. Created date should not be too far in the past
        if !isStartDateValid(goal.createdAt) {
            errors.append(ValidationError(
                field: "createdAt",
                message: "Data de criacao muito antiga (mais de \(maxPastDays) dias)",
                code: .invalidDate
            ))
        }

        // 8. Description length
        if goal.goalDescription.count > maxDescriptionLength {
            errors.append(ValidationError(
                field: "goalDescription",
                message: "Descricao deve ter no maximo \(maxDescriptionLength) caracteres",
                code: .tooLong
            ))
        }

        // 9. If completed, value should meet target
        if goal.status == .completed && goal.valueGeneratedBRL < goal.targetValueBRL {
            errors.append(ValidationError(
                field: "valueGeneratedBRL",
                message: "Meta concluida deve ter valor gerado igual ou maior que o alvo",
                code: .businessRule
            ))
        }

        return errors.isEmpty ? .valid(goal) : .invalid(errors)
    }

    // MARK: - Quick Validations

    static func validateTitle(_ title: String) -> ValidationResult<String> {
        let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines)

        if trimmed.isEmpty {
            return .invalid([ValidationError(
                field: "title",
                message: "Titulo da meta e obrigatorio",
                code: .required
            )])
        }

        if trimmed.count > maxTitleLength {
            return .invalid([ValidationError(
                field: "title",
                message: "Titulo deve ter no maximo \(maxTitleLength) caracteres",
                code: .tooLong
            )])
        }

        return .valid(trimmed)
    }

    static func validateDateRange(created: Date, due: Date?) -> ValidationResult<(Date, Date?)> {
        var errors: [ValidationError] = []

        if let dueDate = due, dueDate <= created {
            errors.append(ValidationError(
                field: "dueDate",
                message: "Data limite deve ser posterior a data de criacao",
                code: .invalidDate
            ))
        }

        if !isStartDateValid(created) {
            errors.append(ValidationError(
                field: "createdAt",
                message: "Data de criacao muito antiga",
                code: .invalidDate
            ))
        }

        return errors.isEmpty ? .valid((created, due)) : .invalid(errors)
    }

    static func validateProgress(generated: Decimal, target: Decimal) -> ValidationResult<Double> {
        var errors: [ValidationError] = []

        if generated < 0 {
            errors.append(ValidationError(
                field: "valueGeneratedBRL",
                message: "Valor gerado nao pode ser negativo",
                code: .negativeValue
            ))
        }

        if target <= 0 {
            errors.append(ValidationError(
                field: "targetValueBRL",
                message: "Valor alvo deve ser maior que zero",
                code: .invalidRange
            ))
        }

        if generated > target {
            errors.append(ValidationError(
                field: "valueGeneratedBRL",
                message: "Valor gerado nao pode exceder o valor alvo",
                code: .invalidRange
            ))
        }

        let progress = target > 0 ? Double(truncating: generated as NSNumber) / Double(truncating: target as NSNumber) : 0
        return errors.isEmpty ? .valid(progress) : .invalid(errors)
    }

    // MARK: - Batch Validation

    static func validateAll(_ goals: [Goal]) -> [(Goal, ValidationResult<Goal>)] {
        return goals.map { ($0, validate($0)) }
    }

    static func findInvalidGoals(_ goals: [Goal]) -> [Goal] {
        return goals.filter { !validate($0).isValid }
    }

    // MARK: - Helper Methods

    private static func isStartDateValid(_ date: Date) -> Bool {
        guard let pastLimit = Calendar.current.date(byAdding: .day, value: -maxPastDays, to: Date()) else {
            return true
        }
        return date >= pastLimit
    }

    // MARK: - Logging Integration

    static func validateAndLog(_ goal: Goal) -> ValidationResult<Goal> {
        let result = validate(goal)

        if !result.isValid {
            logWarning(.validation, "Meta invalida: \(result.errorSummary)", metadata: [
                "goalId": goal.id.uuidString,
                "title": goal.title,
                "errors": "\(result.errors.count)"
            ])
        }

        return result
    }
}


Perfect! Now I have a comprehensive understanding of the 96Blocks-iOS project. Let me compile the findings into a detailed report.

## 96Blocks-iOS Project Exploration Report

### Project Overview

**96Blocks-iOS** is a comprehensive iOS app for day planning and ROI tracking. The core concept divides each day into 96 blocks of 15 minutes each, allowing users to plan, track, and measure the monetary value (in Brazilian Real - R$) of their time.

**Location:** `/Users/neog/Apps/iOS/PROD-96Blocks/`

---

### Architecture & Tech Stack

**Framework & Dependencies:**
- **UI Framework:** SwiftUI (iOS 17+ only, no UIKit)
- **Data Persistence:** SwiftData with CloudKit auto-sync
- **Architecture Pattern:** MVVM with @MainActor for thread safety
- **Platforms:** iPhone, iPad, Mac Catalyst
- **Dependencies:** None - pure Apple frameworks only
- **iOS Deployment Target:** iOS 17.0
- **Xcode Version:** 15.0
- **Swift Version:** 5.0

**Key Architectural Pattern:**
```swift
@MainActor
class DataManager: ObservableObject {
    static let shared = DataManager()
    let modelContainer: ModelContainer
}
```

The app uses singleton pattern for data management with @MainActor for UI thread safety.

---

### Core Data Models

#### 1. **Block** (15-minute time unit)
- `blockIndex`: 0-95 (represents 00:00 to 23:45, 24h × 4 blocks/hour)
- `blockDate`: Date for the block
- `title` & `blockDescription`: Task description
- `category`: work, personal, health, learning, meeting, focus, breakTime, sleep, other
- `estimatedValueBRL` / `actualValueBRL`: Monetary value (Decimal precision)
- `status`: planned, inProgress, completed, skipped
- `isAISuggested`: Boolean flag for AI-generated blocks
- `startedAt` / `completedAt`: Time tracking
- `taskId`: Optional reference to parent Task96
- `hasSteps`: Boolean for subtasks

**Key Computed Properties:**
- `timeString`: Formatted time (e.g., "09:15")
- `timeRangeString`: e.g., "09:15 - 09:30"
- `isCurrentBlock`: Boolean for current 15-min block
- `isPast`: Boolean for elapsed blocks
- `roiEfficiency`: Double (actual / estimated)

#### 2. **Goal** (High-level objectives)
- `title` & `goalDescription`: Goal details
- `targetValueBRL`: Target monetary value (Decimal)
- `timeframe`: weekly, monthly, quarterly, yearly
- `blocksAllocated` / `blocksCompleted`: Time allocation tracking
- `valueGeneratedBRL`: Progress value
- `status`: active, completed, paused, archived
- `dueDate`: Optional deadline
- `createdAt`: Creation timestamp

**Key Computed Properties:**
- `progressPercentage`: (0-100) calculated from values
- `blocksCompletionRate`: Percentage of blocks completed
- `remainingValueBRL`: Target - Generated
- `daysRemaining`: Days until due date
- `isOverdue`: Boolean check

#### 3. **Task96** (Backlog tasks)
- `title` & `taskDescription`: Task details
- `priority`: low, medium, high, urgent (with sortOrder)
- `category`: Same as Block categories
- `estimatedBlocks`: Number of 15-min blocks needed
- `estimatedValueBRL` / `actualValueBRL`: ROI values
- `status`: backlog, scheduled, inProgress, completed, archived
- `dueDate`: Optional deadline
- `goalId`: Optional link to parent Goal
- `tags`: JSON-encoded array of strings

**Key Computed Properties:**
- `estimatedDurationMinutes` / `estimatedDurationString`: Time display
- `isOverdue`: Boolean check
- `roiPerBlock`: Value per 15-min block

#### 4. **BlockStep** (Subtasks within a block)
- `blockId`: Reference to parent Block
- `title`: Step description
- `durationSeconds`: Default 300 (5 min)
- `orderIndex`: Sort order (0, 1, 2...)
- `isCompleted`: Boolean
- `completedAt`: Completion timestamp

**Includes Pre-defined Step Templates:**
- Pomodoro 5-5-5 (5 min focus, 5 min break, 5 min focus)
- Focus 10 + Break 5
- Deep Work 15 (continuous focus)
- Meeting Agenda (2 min opening, 10 min discussion, 3 min conclusion)
- Quick Tasks 3x5 (three 5-minute tasks)
- Micro Sprint (five 3-minute sprints)

#### 5. **DailySummary** (Analytics)
- Daily productivity metrics
- ROI calculations
- Category breakdown

---

### Project Structure

```
96Blocks-iOS/
├── App/
│   ├── 96BlocksApp.swift         # @main entry point, background tasks
│   └── MainTabView.swift         # Tab navigation controller
├── Models/
│   ├── Block.swift               # 15-min block model
│   ├── BlockStep.swift           # Subtask/step model
│   ├── Task.swift                # Backlog task model (Task96)
│   ├── Goal.swift                # Goal tracking
│   ├── Template.swift            # Day templates
│   └── DailySummary.swift        # Analytics summary
├── Services/
│   ├── DataManager.swift         # SwiftData + CloudKit manager
│   ├── AIService.swift           # AI plan generation (Claude, OpenAI, Grok, LM Studio)
│   ├── TimerService.swift        # Block/step timer with background support
│   ├── NotificationService.swift # Push notifications & reminders
│   ├── AuthManager.swift         # Authentication
│   ├── IntegrityService.swift    # Data consistency checks
│   ├── KeychainService.swift     # Secure API key storage
│   ├── LoggingService.swift      # Analytics/event logging
│   └── Validation/
│       ├── ValidationResult.swift # Generic validation framework
│       ├── BlockValidator.swift
│       ├── GoalValidator.swift
│       └── TaskValidator.swift
├── Views/
│   ├── DayPlanner/
│   │   ├── DayPlannerView.swift   # Main timeline view
│   │   ├── BlockRow.swift         # Individual block cell
│   │   ├── TimerView.swift        # Active timer display
│   │   ├── StepListView.swift     # Subtask view
│   │   └── GeneratePlanView.swift # AI plan generation UI
│   ├── Backlog/
│   │   └── BacklogView.swift      # Task management
│   ├── Analytics/
│   │   └── AnalyticsView.swift    # ROI dashboard
│   ├── Settings/
│   │   ├── SettingsView.swift     # Configuration
│   │   └── SystemHealthView.swift # System info
│   └── Onboarding/
│       └── OnboardingView.swift   # First-run setup
├── Components/
│   ├── HapticManager.swift        # Vibration feedback
│   └── AppTheme.swift             # Design system
├── Extensions/
│   └── (Various helpers)
└── Configuration
    ├── 96Blocks-iOS.xcodeproj
    ├── Info.plist                 # Background modes, local network
    ├── 96Blocks-iOS.entitlements  # iCloud + local network
    └── project.yml                # XcodeGen config
```

---

### Key Services Architecture

#### **DataManager (Singleton)**
- Manages SwiftData ModelContainer with CloudKit sync
- Published properties: blocks, tasks, goals, templates, dailySummaries, blockSteps
- Statistics caching (5-second validity) for performance
- Debounced CloudKit sync handling
- Methods:
  - `getBlocks(for: Date) -> [Block]`
  - `getTodayROI() -> (estimated: Decimal, actual: Decimal)`
  - `getProductivityScore() -> Int`

#### **AIService**
- Supports multiple AI providers:
  - Claude (Anthropic)
  - OpenAI (GPT-4)
  - Grok 4 / Grok 4.1 (xAI)
  - LM Studio (local, at 192.168.1.96:1234 by default)
- API keys stored securely in Keychain
- Generates day plans based on goals and templates

#### **TimerService (Singleton)**
- Tracks block/step execution time
- Background execution support with BGAppRefreshTask
- Properties:
  - `isRunning: Bool`
  - `currentBlockId: UUID?`
  - `timeElapsed: TimeInterval`

#### **NotificationService**
- Morning reminder (default 7:00 AM)
- Daily summary (default 8:00 PM)
- Custom notification categories
- Background notification handling

#### **Validation Framework**
- Generic `ValidationResult<T>` enum (valid/invalid with errors)
- `ValidationBuilder<T>` for fluent validation chains
- Error codes: required, invalidRange, invalidFormat, businessRule, duplicate, tooLong, etc.
- Field-level and batch validation methods

---

### Key Features Implemented

1. **96-Block Timeline View**
   - Scrollable vertical timeline with current block highlighting
   - Day progress bar (completed/skipped/lost blocks)
   - ROI summary display

2. **Task Backlog**
   - Priority-based task management
   - Time estimation in blocks
   - Value estimation per task
   - Task-to-block assignment

3. **Goals Tracking**
   - Monetary target goals
   - Progress tracking (value and blocks)
   - Multiple timeframes (weekly/monthly/quarterly/yearly)
   - Due date management

4. **AI-Powered Planning**
   - Automatic day plan generation using Claude/OpenAI/Grok/LM Studio
   - Integration with goals and templates
   - Intelligent block allocation

5. **Analytics Dashboard**
   - Daily productivity score
   - ROI efficiency calculation
   - Category breakdown
   - Progress visualization

6. **Templates**
   - Pre-defined day schedules (Workday, Weekend, Focus Day)
   - Block step templates (Pomodoro, Deep Work, etc.)
   - Custom template creation

7. **Background Features**
   - Timer refresh in background (BGAppRefreshTask)
   - Data sync to iCloud (BGProcessingTask)
   - Push notifications with custom actions

---

### Notable Patterns & Best Practices

#### 1. **Decimal for Money Values**
```swift
var estimatedValueBRL: Decimal = 0
var actualValueBRL: Decimal? = nil
```
Uses `Decimal` type for precise monetary calculations, not Float/Double.

#### 2. **Enum-Backed Raw Strings in SwiftData**
```swift
var categoryRaw: String = BlockCategory.other.rawValue
@Transient var category: BlockCategory {
    get { BlockCategory(rawValue: categoryRaw) ?? .other }
    set { categoryRaw = newValue.rawValue }
}
```
CloudKit-compatible approach storing enums as strings with transient computed properties.

#### 3. **JSON Encoding for Collections**
```swift
var tagsJSON: String = "[]"
@Transient var tags: [String] {
    get {
        guard let data = tagsJSON.data(using: .utf8),
              let decoded = try? JSONDecoder().decode([String].self, from: data) else {
            return []
        }
        return decoded
    }
    set { ... }
}
```
Handles arrays in SwiftData by JSON encoding (needed for CloudKit compatibility).

#### 4. **Block Index Calculation**
```swift
let hour = blockIndex / 4        // 0-23
let minute = (blockIndex % 4) * 15  // 0, 15, 30, 45
let currentIndex = hour * 4 + minute / 15
```
Elegant math for 15-minute block indexing.

#### 5. **Validation Result Monad Pattern**
```swift
enum ValidationResult<T> {
    case valid(T)
    case invalid([ValidationError])

    func map<U>(_ transform: (T) -> U) -> ValidationResult<U>
    func flatMap<U>(_ transform: (T) -> ValidationResult<U>) -> ValidationResult<U>
}
```
Functional validation with composable error handling.

#### 6. **Scene Phase Handling**
```swift
.onChange(of: scenePhase) { oldPhase, newPhase in
    handleScenePhaseChange(from: oldPhase, to: newPhase)
}
```
Properly manages timer state on app background/foreground transitions.

---

### Build & Deployment

**Build Command:**
```bash
cd ~/Apps/iOS/PROD-96Blocks
ruby generate-xcode-project.rb
xcodebuild -project 96Blocks-iOS.xcodeproj \
  -scheme 96Blocks-iOS \
  -sdk iphonesimulator \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro'
```

**App Capabilities:**
- Remote notifications (push)
- Background fetch
- Background processing
- Local network access (for LM Studio)
- iCloud CloudKit sync (private database)

**Bundle Identifier:** `com.neog.96Blocks-iOS`

---

### Relevant Patterns for Goal/Plan Tracking Apps

1. **Time-based Atomic Units:** The 96-block concept is excellent for granular time tracking and ROI calculation
2. **Hierarchical Structure:** Goals → Tasks → Blocks → Steps creates clear scope hierarchy
3. **Decimal Money Handling:** Critical for financial accuracy
4. **CloudKit Sync:** Seamless iCloud integration for cross-device sync
5. **Validation Framework:** Reusable error handling pattern
6. **Background Task Support:** Keeps timers and syncs working in background
7. **AI Integration:** Clean abstraction over multiple AI providers
8. **Step Templates:** Pre-defined patterns for common time-blocking strategies
9. **Analytics with Computed Properties:** Real-time progress calculation without database queries

---

### Documentation Location

Full project documentation: `/Users/neog/Apps/iOS/PROD-96Blocks/CLAUDE.md`

This exploration shows a mature, production-ready iOS app with sophisticated data modeling, clean architecture, and enterprise-grade features like CloudKit sync, background processing, and multi-provider AI integration.