// Author: Simon-Pierre Boucher — contact@spboucher.ai // // A training run: identity, config, directories, and the state machine // (RunSupervisor is the only mutator; every transition is persisted // atomically by RunStore). import Foundation enum RunState: String, Codable, CaseIterable { case queued, launching, running, finishing, finished, failed, stopped var isActive: Bool { switch self { case .launching, .running, .finishing: return true default: return false } } // Legal transitions — RunSupervisor refuses anything else. static let transitions: [RunState: Set] = [ .queued: [.launching, .stopped], .launching: [.running, .failed, .stopped], .running: [.finishing, .finished, .failed, .stopped], .finishing: [.finished, .failed, .stopped], .finished: [], .failed: [], .stopped: [], ] } struct Run: Codable, Identifiable, Equatable { var id: UUID = UUID() var name: String var createdAt: Date var state: RunState = .queued var config: ForgeConfig var datasetPath: String var outDirectory: String // /runs/- var resumeCheckpoint: String? // forge train --resume var pid: Int32? // live forge process (crash recovery) var lastStep: Int? var lastTrainLoss: Double? var bestValLoss: Double? var bestValStep: Int? var exitCode: Int32? var failureReason: String? var logCSVPath: String { outDirectory + "/log.csv" } var runLogPath: String { outDirectory + "/run.log" } var configPath: String { outDirectory + "/config.json" } var progress: Double { guard let step = lastStep, config.train.maxSteps > 0 else { return 0 } return min(1.0, Double(step + 1) / Double(config.train.maxSteps)) } }