diff --git a/CHANGELOG.md b/CHANGELOG.md index 061df9d..291b7d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,4 +6,5 @@ All notable changes to LexiconKit are documented here. ### Added -- Bootstrapped the Swift package, DocC catalog, CI/CD workflows, shared AgentGuidelines integration, and repository policy. +- Added persistence-friendly vocabulary entries, terms, definitions, provenance, tags, opaque asset references, and explicit timestamps. +- Added Codable round-trip and identity tests for the complete vocabulary model. diff --git a/README.md b/README.md index 74322a0..5156fe0 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,20 @@ LexiconKit is a reusable, UI-agnostic domain package for personal vocabulary collections, definitions, provenance, lightweight metadata, and opaque references to host-owned media. -The package is currently a bootstrapped foundation. Dictionary lookup, translation, exercises, persistence frameworks, synchronization, and UI remain outside its boundary. +LexiconKit provides persistence-friendly values for vocabulary entries, terms, definitions, definition provenance, tags, and opaque host-owned media references. Dictionary lookup, translation, exercises, persistence frameworks, synchronization, and UI remain outside its boundary. + +```swift +let entry = LexiconEntry( + term: LexiconTerm(text: "Haus", languageCode: "de"), + definitions: [ + LexiconDefinition( + text: "house", + languageCode: "en", + source: .user + ) + ] +) +``` ## Documentation diff --git a/Sources/LexiconKit/LexiconAssetReference.swift b/Sources/LexiconKit/LexiconAssetReference.swift new file mode 100644 index 0000000..546302a --- /dev/null +++ b/Sources/LexiconKit/LexiconAssetReference.swift @@ -0,0 +1,13 @@ +import Foundation + +/// An opaque stable reference to media owned and resolved by the host application. +public struct LexiconAssetReference: RawRepresentable, Codable, Hashable, Sendable { + public let rawValue: String + + /// Creates an opaque host-owned asset reference. + /// + /// - Parameter rawValue: The stable value understood by the host application. + public init(rawValue: String) { + self.rawValue = rawValue + } +} diff --git a/Sources/LexiconKit/LexiconDefinition.swift b/Sources/LexiconKit/LexiconDefinition.swift new file mode 100644 index 0000000..4855b0a --- /dev/null +++ b/Sources/LexiconKit/LexiconDefinition.swift @@ -0,0 +1,28 @@ +import Foundation + +/// A textual explanation associated with a vocabulary entry. +public struct LexiconDefinition: Identifiable, Codable, Hashable, Sendable { + public let id: UUID + public var languageCode: String? + public var source: LexiconDefinitionSource + public var text: String + + /// Creates a definition with stable identity and explicit provenance. + /// + /// - Parameters: + /// - id: The stable definition identifier. + /// - text: The definition text. + /// - languageCode: An optional BCP 47 language code for the text. + /// - source: The origin of the definition. + public init( + id: UUID = UUID(), + text: String, + languageCode: String? = nil, + source: LexiconDefinitionSource + ) { + self.id = id + self.languageCode = languageCode + self.source = source + self.text = text + } +} diff --git a/Sources/LexiconKit/LexiconDefinitionSource.swift b/Sources/LexiconKit/LexiconDefinitionSource.swift new file mode 100644 index 0000000..ce14929 --- /dev/null +++ b/Sources/LexiconKit/LexiconDefinitionSource.swift @@ -0,0 +1,10 @@ +import Foundation + +/// The provenance of a vocabulary definition. +public enum LexiconDefinitionSource: String, Codable, Hashable, Sendable { + /// Content supplied by the consuming application. + case application + + /// Content authored or captured by the person using the application. + case user +} diff --git a/Sources/LexiconKit/LexiconEntry.swift b/Sources/LexiconKit/LexiconEntry.swift new file mode 100644 index 0000000..bae2302 --- /dev/null +++ b/Sources/LexiconKit/LexiconEntry.swift @@ -0,0 +1,42 @@ +import Foundation + +/// A persistence-friendly vocabulary entry independent of storage and presentation frameworks. +public struct LexiconEntry: Identifiable, Codable, Hashable, Sendable { + public let id: UUID + public var assetReference: LexiconAssetReference? + public var createdAt: Date + public var definitions: [LexiconDefinition] + public var modifiedAt: Date + public var tags: Set + public var term: LexiconTerm + + /// Creates a vocabulary entry. + /// + /// When `modifiedAt` is omitted, it begins at the same instant as `createdAt`. + /// + /// - Parameters: + /// - id: The stable entry identifier. + /// - term: The collected lexical form or phrase. + /// - definitions: The definitions associated with the term. + /// - tags: Optional host-defined classification tags. + /// - assetReference: An optional opaque reference to host-owned media. + /// - createdAt: The instant the entry was created. + /// - modifiedAt: The instant the entry was last modified. + public init( + id: UUID = UUID(), + term: LexiconTerm, + definitions: [LexiconDefinition], + tags: Set = [], + assetReference: LexiconAssetReference? = nil, + createdAt: Date = Date(), + modifiedAt: Date? = nil + ) { + self.id = id + self.assetReference = assetReference + self.createdAt = createdAt + self.definitions = definitions + self.modifiedAt = modifiedAt ?? createdAt + self.tags = tags + self.term = term + } +} diff --git a/Sources/LexiconKit/LexiconKit.docc/LexiconKit.md b/Sources/LexiconKit/LexiconKit.docc/LexiconKit.md index 8a0b621..054da38 100644 --- a/Sources/LexiconKit/LexiconKit.docc/LexiconKit.md +++ b/Sources/LexiconKit/LexiconKit.docc/LexiconKit.md @@ -4,4 +4,25 @@ Model persistence-friendly vocabulary values without coupling consumers to UI, l ## Overview -The initial bootstrap intentionally contains no speculative public API. Add capabilities only when a concrete consumer establishes their requirements and tests. +LexiconKit models vocabulary independently from persistence, synchronization, user interface, and learning progression. Hosts decide how entries are validated, deduplicated, stored, synchronized, and presented. + +Create a ``LexiconEntry`` from a ``LexiconTerm`` and one or more ``LexiconDefinition`` values. Definitions retain their ``LexiconDefinitionSource`` so application-provided and user-provided content remain distinguishable after serialization. ``LexiconTag`` values and an optional ``LexiconAssetReference`` carry host-defined metadata without leaking application or media-framework types into the domain. + +All public values are `Codable`, `Hashable`, and `Sendable`. Stable entry and definition identifiers plus explicit creation and modification timestamps let a host persist and merge values using its own policy. + +## Topics + +### Entries + +- ``LexiconEntry`` +- ``LexiconTerm`` + +### Definitions + +- ``LexiconDefinition`` +- ``LexiconDefinitionSource`` + +### Host-owned metadata + +- ``LexiconTag`` +- ``LexiconAssetReference`` diff --git a/Sources/LexiconKit/LexiconKit.swift b/Sources/LexiconKit/LexiconKit.swift deleted file mode 100644 index 669cf48..0000000 --- a/Sources/LexiconKit/LexiconKit.swift +++ /dev/null @@ -1,2 +0,0 @@ -/// A namespace for LexiconKit capabilities. -public enum LexiconKit {} diff --git a/Sources/LexiconKit/LexiconTag.swift b/Sources/LexiconKit/LexiconTag.swift new file mode 100644 index 0000000..46d526e --- /dev/null +++ b/Sources/LexiconKit/LexiconTag.swift @@ -0,0 +1,13 @@ +import Foundation + +/// A host-defined vocabulary classification value. +public struct LexiconTag: RawRepresentable, Codable, Hashable, Sendable { + public let rawValue: String + + /// Creates a tag without imposing application-specific validation or normalization. + /// + /// - Parameter rawValue: The host-defined tag value. + public init(rawValue: String) { + self.rawValue = rawValue + } +} diff --git a/Sources/LexiconKit/LexiconTerm.swift b/Sources/LexiconKit/LexiconTerm.swift new file mode 100644 index 0000000..b7e153f --- /dev/null +++ b/Sources/LexiconKit/LexiconTerm.swift @@ -0,0 +1,17 @@ +import Foundation + +/// A collected lexical form or phrase and its language metadata. +public struct LexiconTerm: Codable, Hashable, Sendable { + public var languageCode: String + public var text: String + + /// Creates a vocabulary term. + /// + /// - Parameters: + /// - text: The collected lexical form or phrase. + /// - languageCode: The BCP 47 language code for the text. + public init(text: String, languageCode: String) { + self.languageCode = languageCode + self.text = text + } +} diff --git a/Tests/LexiconKitTests/LexiconEntryTests.swift b/Tests/LexiconKitTests/LexiconEntryTests.swift new file mode 100644 index 0000000..7d3d94d --- /dev/null +++ b/Tests/LexiconKitTests/LexiconEntryTests.swift @@ -0,0 +1,90 @@ +import Foundation +import Testing + +@testable import LexiconKit + +struct LexiconEntryTests { + @Test func completeEntryRoundTripsThroughCodable() throws { + // Given + let entryID = UUID(uuidString: "D2E60778-E44B-48BE-B539-268D1F15229A")! + let definitionID = UUID(uuidString: "707E18F0-4433-45BE-A2FA-4EE156615677")! + let createdAt = Date(timeIntervalSince1970: 1_789_300_000) + let modifiedAt = createdAt.addingTimeInterval(60) + let entry = LexiconEntry( + id: entryID, + term: LexiconTerm(text: "Haus", languageCode: "de"), + definitions: [ + LexiconDefinition( + id: definitionID, + text: "house", + languageCode: "en", + source: .user + ) + ], + tags: [LexiconTag(rawValue: "reading")], + assetReference: LexiconAssetReference(rawValue: "images/haus"), + createdAt: createdAt, + modifiedAt: modifiedAt + ) + + // When + let data = try JSONEncoder().encode(entry) + let decoded = try JSONDecoder().decode(LexiconEntry.self, from: data) + + // Then + #expect(decoded == entry) + #expect(decoded.id == entryID) + #expect(decoded.definitions.first?.id == definitionID) + #expect(decoded.definitions.first?.source == .user) + #expect(decoded.tags == [LexiconTag(rawValue: "reading")]) + #expect(decoded.assetReference == LexiconAssetReference(rawValue: "images/haus")) + } + + @Test func applicationDefinitionProvenanceRoundTrips() throws { + let definition = LexiconDefinition( + text: "a building for people to live in", + languageCode: "en", + source: .application + ) + + let data = try JSONEncoder().encode(definition) + let decoded = try JSONDecoder().decode(LexiconDefinition.self, from: data) + + #expect(decoded == definition) + #expect(decoded.source == .application) + } + + @Test func initialModifiedTimestampMatchesCreationTimestamp() { + let createdAt = Date(timeIntervalSince1970: 1_789_300_000) + + let entry = LexiconEntry( + term: LexiconTerm(text: "Maus", languageCode: "de"), + definitions: [], + createdAt: createdAt + ) + + #expect(entry.createdAt == createdAt) + #expect(entry.modifiedAt == createdAt) + } + + @Test func hostCanUpdateContentWithoutChangingIdentityOrCreationTimestamp() { + let createdAt = Date(timeIntervalSince1970: 1_789_300_000) + let modifiedAt = createdAt.addingTimeInterval(90) + var entry = LexiconEntry( + term: LexiconTerm(text: "Buch", languageCode: "de"), + definitions: [], + createdAt: createdAt + ) + let originalID = entry.id + + entry.definitions.append( + LexiconDefinition(text: "book", languageCode: "en", source: .user) + ) + entry.modifiedAt = modifiedAt + + #expect(entry.id == originalID) + #expect(entry.createdAt == createdAt) + #expect(entry.modifiedAt == modifiedAt) + #expect(entry.definitions.map(\.text) == ["book"]) + } +} diff --git a/Tests/LexiconKitTests/LexiconKitTests.swift b/Tests/LexiconKitTests/LexiconKitTests.swift deleted file mode 100644 index 72dc66a..0000000 --- a/Tests/LexiconKitTests/LexiconKitTests.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Testing -@testable import LexiconKit - -@Test("The package namespace is available") -func packageNamespaceIsAvailable() { - _ = LexiconKit.self -}