import Foundation import IndieSync /// The context snapshot captured at the moment a note is taken — the raw /// material the relevance ranking later matches against "where am I, what /// time is it" at recall time. Every field is optional: capture is /// best-effort and a note taken with location off is still a note. struct CaptureContext: Codable, Sendable, Equatable { var latitude: Double? var longitude: Double? var horizontalAccuracy: Double? /// Point-of-interest or venue name from reverse geocoding ("Blue Bottle /// Coffee"). Stored as text so recall works even when geocoding is /// unavailable later. var placeName: String? var thoroughfare: String? var locality: String? var administrativeArea: String? var country: String? /// Time zone the note was taken in; local hour / weekday are derived /// from `meta.createdAt` under this zone rather than stored. var timeZoneID: String? /// "iPhone" / "Mac" — which device captured the note. var device: String? static let empty = CaptureContext() var hasCoordinate: Bool { latitude != nil && longitude != nil } /// Short human summary for rows and the composer ("Blue Bottle Coffee · Oakland"). var summary: String? { let parts = [placeName, locality].compactMap { $0 } return parts.isEmpty ? nil : parts.joined(separator: " · ") } } /// One note = one JSON file in the iCloud container, filed under /// `Records/YYYY/MM/.json` (UTC-bucketed by the ULID's timestamp). struct Note: SyncDocument, VersionedDocument, Equatable { static let currentSchemaVersion = 1 struct Meta: Codable, Sendable, Equatable { var id: ULID var schemaVersion: Int var createdAt: Date var modifiedAt: Date } /// How the note's text came to be. `transcribed` is reserved for the /// planned voice-capture path. enum Source: String, Codable, Sendable { case typed case transcribed } struct Payload: Codable, Sendable, Equatable { var text: String var source: Source var tags: [String] var context: CaptureContext } var meta: Meta var payload: Payload var id: ULID { meta.id } var schemaVersion: Int { meta.schemaVersion } var relativePath: String { Self.relativePath(forID: meta.id) } static func relativePath(forID id: ULID) -> String { TimeBucketedLayout.relativePath(for: id) } static func create( text: String, source: Source = .typed, tags: [String] = [], context: CaptureContext ) -> Note { let now = Date() return Note( meta: Meta(id: ULID(), schemaVersion: currentSchemaVersion, createdAt: now, modifiedAt: now), payload: Payload(text: text, source: source, tags: tags, context: context) ) } /// First line of the text, used as the display title. var title: String { payload.text .split(separator: "\n", omittingEmptySubsequences: true) .first.map(String.init) ?? "" } }