// // IndexStore.swift // Focale // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // // SwiftData store for the photo index. Photos are referenced by // localIdentifier only — never duplicated, moved, or modified. // Target: < 500 MB for 50 000 photos; thumbnails come from PhotoKit, // never stored twice (CLAUDE.md §9). // import Foundation import SwiftData @Model final class PhotoRecord { @Attribute(.unique) var localIdentifier: String var captureDate: Date? // Stage progress var visionIndexedAt: Date? var semanticIndexedAt: Date? /// Model guardrails refused the image — a normal state, not an error /// (CLAUDE.md §8). Stage-1 data remains fully usable. var semanticRefused: Bool // Stage 1 — Vision (real data) var ocrText: String? var featurePrint: Data? var classificationLabels: [String] var faceCount: Int // Capture context (real data, free at shutter time) var contextJSON: Data? var projectName: String? var subjectHint: String? var placeName: String? var placeLocality: String? // Stage 2 — Foundation Models (generated data, displayed as such) var gist: String? var kindRawValue: String? var entities: [String] var hasActionableInfo: Bool var isFavorite: Bool init(localIdentifier: String, captureDate: Date?) { self.localIdentifier = localIdentifier self.captureDate = captureDate self.semanticRefused = false self.classificationLabels = [] self.faceCount = 0 self.entities = [] self.hasActionableInfo = false self.isFavorite = false } var captureContext: CaptureContext? { get { guard let contextJSON else { return nil } return try? JSONDecoder().decode(CaptureContext.self, from: contextJSON) } set { contextJSON = newValue.flatMap { try? JSONEncoder().encode($0) } projectName = newValue?.projectName subjectHint = newValue?.subjectHint placeName = newValue?.place?.name placeLocality = newValue?.place?.locality } } } /// Incremental resume point — indexing never restarts from scratch. @Model final class IndexCheckpoint { /// Creation date of the oldest photo already backfilled (newest → oldest). var oldestIndexedDate: Date? var updatedAt: Date init(oldestIndexedDate: Date? = nil) { self.oldestIndexedDate = oldestIndexedDate self.updatedAt = .now } } enum IndexStore { static let container: ModelContainer = { let schema = Schema([PhotoRecord.self, IndexCheckpoint.self]) let configuration = ModelConfiguration(schema: schema) do { return try ModelContainer(for: schema, configurations: [configuration]) } catch { fatalError("Failed to create index store: \(error)") } }() }