import Foundation public protocol SongProjectValidating: Sendable { func validate(_ project: SongProject) -> [SongProjectValidationIssue] } public enum SongProjectValidationIssue: Equatable, Sendable { case invalidDuration case invalidBPM(Int) case duplicateSectionID(String) case invalidSectionTiming(String) case invalidInstrumentTiming(String) case unknownSectionReference(String) } public struct SongProjectValidator: SongProjectValidating { public init() {} public func validate(_ project: SongProject) -> [SongProjectValidationIssue] { var issues: [SongProjectValidationIssue] = [] let sectionIDs = Set(project.sections.map(\.id)) if project.duration?.seconds.map({ $0 <= 0 }) == true { issues.append(.invalidDuration) } if let bpm = project.bpm?.value, bpm <= 0 { issues.append(.invalidBPM(bpm)) } for section in project.sections { if project.sections.filter({ $0.id == section.id }).count > 1 { append(.duplicateSectionID(section.id), to: &issues) } if invalidTiming(start: section.startTime, end: section.endTime) { issues.append(.invalidSectionTiming(section.id)) } } for instrument in project.instruments { for placement in instrument.placements { if invalidTiming(start: placement.startTime, end: placement.endTime) { issues.append(.invalidInstrumentTiming(instrument.instrumentId)) } if let sectionID = placement.sectionId, !sectionIDs.contains(sectionID) { append(.unknownSectionReference(sectionID), to: &issues) } } } for vocalist in project.vocalists { for sectionID in vocalist.assignedSections where !sectionIDs.contains(sectionID) { append(.unknownSectionReference(sectionID), to: &issues) } } for point in project.emotionalArc { if let sectionID = point.sectionID, !sectionIDs.contains(sectionID) { append(.unknownSectionReference(sectionID), to: &issues) } } for direction in project.productionDirections { if let sectionID = direction.sectionID, !sectionIDs.contains(sectionID) { append(.unknownSectionReference(sectionID), to: &issues) } } return issues } private func invalidTiming(start: TimeInterval?, end: TimeInterval?) -> Bool { guard let start, let end else { return start.map { $0 < 0 } ?? end.map { $0 < 0 } ?? false } return start < 0 || end < 0 || end <= start } private func append(_ issue: SongProjectValidationIssue, to issues: inout [SongProjectValidationIssue]) { guard !issues.contains(issue) else { return } issues.append(issue) } }