= [], pushedEntryEdits: [String: Date] = [:],
+ systemFields: [String: Data] = [:], profilePushed: Bool = false, lastFetched: Date? = nil
+ ) {
+ self.engine = engine
+ self.pushedEventIDs = pushedEventIDs
+ self.pushedEntryEdits = pushedEntryEdits
+ self.systemFields = systemFields
+ self.profilePushed = profilePushed
+ self.lastFetched = lastFetched
+ }
+
+ public static func load(from url: URL) -> SyncState {
+ guard let data = try? Data(contentsOf: url) else { return SyncState() }
+ return (try? decoder.decode(SyncState.self, from: data)) ?? SyncState()
+ }
+
+ public func save(to url: URL) throws {
+ try FileManager.default.createDirectory(
+ at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
+ try Self.encoder.encode(self).write(to: url, options: .atomic)
+ }
+
+ private static var encoder: JSONEncoder {
+ let encoder = JSONEncoder()
+ encoder.dateEncodingStrategy = .iso8601
+ return encoder
+ }
+
+ private static var decoder: JSONDecoder {
+ let decoder = JSONDecoder()
+ decoder.dateDecodingStrategy = .iso8601
+ return decoder
+ }
+}
diff --git a/packages/TimbreCloud/Tests/TimbreCloudTests/CloudRecordsTests.swift b/packages/TimbreCloud/Tests/TimbreCloudTests/CloudRecordsTests.swift
new file mode 100644
index 0000000..528b217
--- /dev/null
+++ b/packages/TimbreCloud/Tests/TimbreCloudTests/CloudRecordsTests.swift
@@ -0,0 +1,72 @@
+import CloudKit
+import Foundation
+import Testing
+import TimbreCore
+
+@testable import TimbreCloud
+
+struct CloudRecordsTests {
+
+ private let source = Provenance(
+ device: .iPhone, date: Date(timeIntervalSince1970: 1_700_000_000), how: .dictated, app: "Mail")
+
+ @Test func anEventRoundTrips() {
+ let event = LibraryEvent(
+ entry: "word:ephemeral", kind: .passed, date: Date(timeIntervalSince1970: 1_700_000_100),
+ device: .watch,
+ app: nil, drill: .spellIt)
+ let record = CloudRecords.record(for: event)
+ #expect(record.recordID.recordName == event.id)
+ #expect(record.recordID.zoneID.zoneName == "Library")
+ #expect(CloudRecords.event(from: record) == event)
+ }
+
+ @Test func anEntryRoundTripsWithEveryField() {
+ var entry = LibraryEntry(
+ kind: .acronym, term: "ADR", meaning: "Architecture Decision Record", example: "We wrote an ADR.",
+ tags: ["software"], source: source)!
+ entry.edited = Date(timeIntervalSince1970: 1_700_000_500)
+ let back = CloudRecords.entry(from: CloudRecords.record(for: entry))
+ #expect(back == entry)
+ #expect(back?.source.app == "Mail")
+ }
+
+ @Test func theProfileRoundTrips() {
+ let profile = Profile(
+ industries: ["software", "product"], role: "engineer", firstLanguage: "English")
+ #expect(CloudRecords.profile(from: CloudRecords.record(for: profile)) == profile)
+ #expect(CloudRecords.profile(from: CloudRecords.record(for: .empty)) == .empty)
+ }
+
+ @Test func aRecordOfAnotherTypeIsRefused() {
+ let stray = CKRecord(
+ recordType: "Something", recordID: CKRecord.ID(recordName: "x", zoneID: CloudRecords.zoneID))
+ #expect(CloudRecords.event(from: stray) == nil)
+ #expect(CloudRecords.entry(from: stray) == nil)
+ #expect(CloudRecords.profile(from: stray) == nil)
+ }
+
+ @Test func systemFieldsRoundTrip() {
+ let record = CloudRecords.record(for: Profile(industries: ["law"]))
+ let data = CloudRecords.systemFields(of: record)
+ let back = CloudRecords.record(fromSystemFields: data)
+ #expect(back?.recordID == record.recordID)
+ #expect(back?.recordType == CloudRecords.profileType)
+ // System fields only: the values were not carried.
+ #expect(back?["industries"] == nil)
+ }
+
+ @Test func syncStatePersists() throws {
+ let directory = URL(filePath: NSTemporaryDirectory()).appending(
+ path: UUID().uuidString, directoryHint: .isDirectory)
+ defer { try? FileManager.default.removeItem(at: directory) }
+ let url = directory.appending(path: "library-sync.json")
+ var state = SyncState()
+ state.pushedEventIDs = ["taught|word:a|1|mac||"]
+ state.pushedEntryEdits = ["word:a": Date(timeIntervalSince1970: 1_700_000_000)]
+ state.profilePushed = true
+ try state.save(to: url)
+ #expect(SyncState.load(from: url) == state)
+ #expect(SyncState.load(from: directory.appending(path: "missing.json")) == SyncState())
+ }
+}
diff --git a/packages/TimbreCore/Sources/TimbreCore/Library/Library.swift b/packages/TimbreCore/Sources/TimbreCore/Library/Library.swift
index 8ec0dc5..d66c003 100644
--- a/packages/TimbreCore/Sources/TimbreCore/Library/Library.swift
+++ b/packages/TimbreCore/Sources/TimbreCore/Library/Library.swift
@@ -259,9 +259,13 @@ public final class Library {
}
/// Called after every successful write, so a surface can follow the
- /// Library without polling — the word of the day reschedules its
- /// notification here.
- public var didSave: (@MainActor () -> Void)?
+ /// Library without polling: the word of the day rebuilds its card, the
+ /// sync queues what changed. Several may listen.
+ public func addSaveObserver(_ observer: @escaping @MainActor () -> Void) {
+ saveObservers.append(observer)
+ }
+
+ private var saveObservers: [@MainActor () -> Void] = []
// MARK: - Persistence
@@ -279,7 +283,7 @@ public final class Library {
at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true)
try data.write(to: fileURL, options: .atomic)
lastSaveError = nil
- didSave?()
+ for observer in saveObservers { observer() }
} catch {
lastSaveError = error
}
diff --git a/packages/TimbreCore/Tests/TimbreCoreTests/LibraryTests.swift b/packages/TimbreCore/Tests/TimbreCoreTests/LibraryTests.swift
index a8f2d21..534cf40 100644
--- a/packages/TimbreCore/Tests/TimbreCoreTests/LibraryTests.swift
+++ b/packages/TimbreCore/Tests/TimbreCoreTests/LibraryTests.swift
@@ -263,7 +263,7 @@ struct LibraryTests {
defer { cleanup() }
let library = Library(fileURL: file, legacy: defaults)
var saves = 0
- library.didSave = { saves += 1 }
+ library.addSaveObserver { saves += 1 }
library.add("one")
library.remove("one")
#expect(saves == 2)
diff --git a/packages/TimbreKit/Package.swift b/packages/TimbreKit/Package.swift
index 420f3c5..e9a44ec 100644
--- a/packages/TimbreKit/Package.swift
+++ b/packages/TimbreKit/Package.swift
@@ -24,11 +24,12 @@ let package = Package(
.package(path: "../TimbreSpeech"),
.package(path: "../TimbreModel"),
.package(path: "../TimbreReminders"),
+ .package(path: "../TimbreCloud"),
],
targets: [
.target(
name: "TimbreKit",
- dependencies: ["TimbreCore", "TimbreSpeech", "TimbreModel", "TimbreReminders"],
+ dependencies: ["TimbreCore", "TimbreSpeech", "TimbreModel", "TimbreReminders", "TimbreCloud"],
swiftSettings: concurrencySettings
),
.executableTarget(
diff --git a/packages/TimbreKit/Sources/TimbreKit/DictationController.swift b/packages/TimbreKit/Sources/TimbreKit/DictationController.swift
index e33f509..85cf858 100644
--- a/packages/TimbreKit/Sources/TimbreKit/DictationController.swift
+++ b/packages/TimbreKit/Sources/TimbreKit/DictationController.swift
@@ -1,6 +1,7 @@
import AVFoundation
import Foundation
import Observation
+import TimbreCloud
import TimbreCore
import TimbreReminders
import TimbreSpeech
@@ -147,6 +148,21 @@ public final class DictationController {
/// their own spoken input, so the hotkey pipeline is never borrowed.
@ObservationIgnored public private(set) lazy var practice = PracticeSession(
library: library, reader: speech)
+
+ /// The Library through the user's own iCloud (GDR-0023 decision 1, ADR-0017).
+ /// Off until turned on; the binary still makes no request.
+ @ObservationIgnored public private(set) lazy var cloud = CloudKitLibrarySync(
+ library: library,
+ stateURL: Library.defaultFileURL.deletingLastPathComponent().appending(path: "library-sync.json"))
+
+ /// The toggle. Stopping keeps what is on this Mac.
+ public var syncsThroughICloud: Bool {
+ get { UserDefaults.standard.bool(forKey: "librarySyncsThroughICloud") }
+ set {
+ UserDefaults.standard.set(newValue, forKey: "librarySyncsThroughICloud")
+ if newValue { cloud.start() } else { cloud.stop() }
+ }
+ }
private let dictationLog = DictationLog()
private let speech = SpeechReader()
private let transformer = TextTransformer()
@@ -197,8 +213,9 @@ public final class DictationController {
public func bootstrap() async {
// The word of the day follows the Library: every write rebuilds the
// card and the pending notification.
- library.didSave = { [weak self] in self?.wordOfTheDay.refresh() }
+ library.addSaveObserver { [weak self] in self?.wordOfTheDay.refresh() }
wordOfTheDay.refresh()
+ if syncsThroughICloud { cloud.start() }
// 1. Permissions. Microphone first and unconditionally: it is the one
// macOS grants in place, and gating it behind Accessibility meant
diff --git a/web/src/lib/decisions/records.json b/web/src/lib/decisions/records.json
index babc649..9096471 100644
--- a/web/src/lib/decisions/records.json
+++ b/web/src/lib/decisions/records.json
@@ -191,6 +191,36 @@
"plain": "Accepted (implementation pending).\nContext.\nGDR-0023 made the iPhone a companion: the Library in your pocket, practice,\nthe word of the day. It said nothing about the wrist, and the wrist is where\ntwo of Timbre's gestures want to be. The to-do key (GDR-0016) — hold, say\nit, it is in Reminders — is a thing you do while walking, not at a desk. And\na word you have just heard is banked best in the three seconds before it is\nforgotten, which is a raise of the wrist, not a trip to a Mac.\nThe platform was checked, not assumed. In the watchOS 27 SDK on the\ndevelopment Mac: EventKit, WatchConnectivity, AppIntents, WidgetKit and\nAVFAudio are present. Speech — and with it SpeechAnalyzer — is not.\nFoundationModels ships in the SDK, but whether the model is eligible on\nWatch hardware is unknown until it runs on one. On iPhone the on-device\nmodel needs Apple Intelligence (iPhone 15 Pro and later); an older iPhone\nhas none. Hugo will test on all four: an iPhone with the model, one without,\nan Apple Watch Ultra (Action Button) and a Series 9 or later (Double Tap, no\nAction Button). Each of those is therefore a real user, not a hypothetical.\nDecision.\nTimbre ships on Apple Watch as part of the companion, and nothing on the\nwrist needs the model.\nOn the wrist:\nThe to-do key. The Action Button on an Ultra, Double Tap on a Series 9\n or later, or the app's own button: say the to-do, and it lands in the\n Timbre list in Reminders with the date read by the same deterministic\n detector as on the Mac (GDR-0016). A haptic confirms. A tap reads today's\n list back in the chosen voice. This works with no phone in range and no\n model: the detector is Foundation and the store is EventKit, both on the\n Watch.\nThe word of the day at a glance. A complication and a Smart Stack\n card: the word, a tap for the meaning, and the day's state — met,\n practised, or waiting (GDR-0027).\nSpell it (GDR-0023, decision 5): the meaning shown, the word said and\n spelled, checked deterministically from the system dictation's text.\nHeard a word. Raise the wrist, say it, and it is in the Library with\n provenance: the Watch, the day, the time. The meaning fills in on the\n phone or the Mac.\nThe rules that make the degraded cases designed rather than discovered:\nDeterministic on the wrist, the model elsewhere. The Watch's speech\n input is the system's dictation, which returns text; SpeechAnalyzer is\n not on the platform, so there is no streaming, no live result and no\n vocabulary bias there. Anything that needs the model — Say it, an example\n sentence, a verdict — is done on the phone or the Mac and arrives by sync.\n The Watch says waiting for your phone rather than pretending.\nAn iPhone without Apple Intelligence shows the Library, the word of\n the day and Spell it. Say it is unavailable there, labelled as such, and\n model-written lines arrive from a device that has the model.\nZero network requests from the Watch binary, as from every other.\n Watch to phone is WatchConnectivity; the Library syncs by iCloud\n (GDR-0023, decision 1). Both are Apple's transports between the user's own\n devices.\nNo dictation into other apps and no command mode, on either device.\n GDR-0002's reasons stand; GDR-0023 said so for the phone and this record\n says so for the wrist.\nConsequences.\nGDR-0002's index entry now reads: \"no iOS app\" superseded by 0023 and\n 0026; dictation stays macOS-only. The record itself stays as written.\nThe Watch app is a target of the companion's project, importing\n TimbreCore alone (GDR-0023, decision 4). The to-do parser moves to Core\n with the Library, because the wrist runs it without TimbreKit.\nDistribution for iPhone and Watch is the App Store — review, a privacy\n label that says no data collected, TestFlight — while the Mac stays\n Developer ID and direct (ADR-0010, GDR-0013). Two paths, one team, one\n claim.\nMemory, battery and whether FoundationModels answers on a Watch are\n measured on hardware before any copy promises them. If the model does run\n there, the rule above still stands until a drill's corpus proves it should\n not.\nThe phases (product map §7) gain a step after the iPhone: the Watch, with\n the to-do key first, because it needs nothing new.\nAlternatives considered.\nThe Watch as a remote microphone for the Mac: rejected; the Mac is not\n where the user is when the wrist is the right surface, and audio between\n devices is a stream to maintain for a gesture that needs only text.\nSkip the Watch; the phone is enough: rejected; the to-do key's value is\n capture at the moment of the thought (GDR-0016), and the moment is on the\n wrist.\nRun the model on the Watch where available: deferred; not until it is\n seen to answer on hardware, and never for the to-do key, which must work in\n a tunnel.",
"html": "Context
\nGDR-0023 made the iPhone a companion: the Library in your pocket, practice,\nthe word of the day. It said nothing about the wrist, and the wrist is where\ntwo of Timbre's gestures want to be. The to-do key (GDR-0016) — hold, say\nit, it is in Reminders — is a thing you do while walking, not at a desk. And\na word you have just heard is banked best in the three seconds before it is\nforgotten, which is a raise of the wrist, not a trip to a Mac.
\nThe platform was checked, not assumed. In the watchOS 27 SDK on the\ndevelopment Mac: EventKit, WatchConnectivity, AppIntents, WidgetKit and\nAVFAudio are present. Speech — and with it SpeechAnalyzer — is not.\nFoundationModels ships in the SDK, but whether the model is eligible on\nWatch hardware is unknown until it runs on one. On iPhone the on-device\nmodel needs Apple Intelligence (iPhone 15 Pro and later); an older iPhone\nhas none. Hugo will test on all four: an iPhone with the model, one without,\nan Apple Watch Ultra (Action Button) and a Series 9 or later (Double Tap, no\nAction Button). Each of those is therefore a real user, not a hypothetical.
\nDecision
\nTimbre ships on Apple Watch as part of the companion, and nothing on the\nwrist needs the model.
\nOn the wrist:
\n\n- The to-do key. The Action Button on an Ultra, Double Tap on a Series 9\nor later, or the app's own button: say the to-do, and it lands in the\nTimbre list in Reminders with the date read by the same deterministic\ndetector as on the Mac (GDR-0016). A haptic confirms. A tap reads today's\nlist back in the chosen voice. This works with no phone in range and no\nmodel: the detector is Foundation and the store is EventKit, both on the\nWatch.
\n- The word of the day at a glance. A complication and a Smart Stack\ncard: the word, a tap for the meaning, and the day's state — met,\npractised, or waiting (GDR-0027).
\n- Spell it (GDR-0023, decision 5): the meaning shown, the word said and\nspelled, checked deterministically from the system dictation's text.
\n- Heard a word. Raise the wrist, say it, and it is in the Library with\nprovenance: the Watch, the day, the time. The meaning fills in on the\nphone or the Mac.
\n
\nThe rules that make the degraded cases designed rather than discovered:
\n\n- Deterministic on the wrist, the model elsewhere. The Watch's speech\ninput is the system's dictation, which returns text;
SpeechAnalyzer is\nnot on the platform, so there is no streaming, no live result and no\nvocabulary bias there. Anything that needs the model — Say it, an example\nsentence, a verdict — is done on the phone or the Mac and arrives by sync.\nThe Watch says waiting for your phone rather than pretending. \n- An iPhone without Apple Intelligence shows the Library, the word of\nthe day and Spell it. Say it is unavailable there, labelled as such, and\nmodel-written lines arrive from a device that has the model.
\n- Zero network requests from the Watch binary, as from every other.\nWatch to phone is WatchConnectivity; the Library syncs by iCloud\n(GDR-0023, decision 1). Both are Apple's transports between the user's own\ndevices.
\n- No dictation into other apps and no command mode, on either device.\nGDR-0002's reasons stand; GDR-0023 said so for the phone and this record\nsays so for the wrist.
\n
\nConsequences
\n\n- GDR-0002's index entry now reads: "no iOS app" superseded by 0023 and\n0026; dictation stays macOS-only. The record itself stays as written.
\n- The Watch app is a target of the companion's project, importing\n
TimbreCore alone (GDR-0023, decision 4). The to-do parser moves to Core\nwith the Library, because the wrist runs it without TimbreKit. \n- Distribution for iPhone and Watch is the App Store — review, a privacy\nlabel that says no data collected, TestFlight — while the Mac stays\nDeveloper ID and direct (ADR-0010, GDR-0013). Two paths, one team, one\nclaim.
\n- Memory, battery and whether FoundationModels answers on a Watch are\nmeasured on hardware before any copy promises them. If the model does run\nthere, the rule above still stands until a drill's corpus proves it should\nnot.
\n- The phases (product map §7) gain a step after the iPhone: the Watch, with\nthe to-do key first, because it needs nothing new.
\n
\nAlternatives considered
\n\n- The Watch as a remote microphone for the Mac: rejected; the Mac is not\nwhere the user is when the wrist is the right surface, and audio between\ndevices is a stream to maintain for a gesture that needs only text.
\n- Skip the Watch; the phone is enough: rejected; the to-do key's value is\ncapture at the moment of the thought (GDR-0016), and the moment is on the\nwrist.
\n- Run the model on the Watch where available: deferred; not until it is\nseen to answer on hardware, and never for the to-do key, which must work in\na tunnel.
\n
\n"
},
+ {
+ "id": "ADR-0017",
+ "kind": "ADR",
+ "number": "0017",
+ "slug": "the-librarys-cloudkit-schema",
+ "path": "docs/decisions/adr/0017-the-librarys-cloudkit-schema.md",
+ "title": "The Library's CloudKit schema: events as immutable records, entries by latest edit, one zone",
+ "status": "Accepted",
+ "statusKind": "accepted",
+ "statusNote": null,
+ "supersededBy": null,
+ "supersedes": [],
+ "date": "2026-10-18",
+ "relations": [
+ {
+ "label": "Builds on",
+ "summary": "GDR-0023 (decision 1: the Library syncs through iCloud, behind a sync…",
+ "refs": [
+ "GDR-0023",
+ "GDR-0010",
+ "ADR-0013",
+ "ADR-0014",
+ "ADR-0015"
+ ]
+ }
+ ],
+ "excerpt": "GDR-0023 chose CloudKit's private database and left the shape to a record\nwritten with the code. A CloudKit schema is forever: once deployed to\nproduction, a record type or field can be added but never renamed, retyped\nor removed, and every build a user ever ran must still read what a newer\none writes. The Library's own design (ADR-0014) points the way:…",
+ "plain": "Accepted.\nContext.\nGDR-0023 chose CloudKit's private database and left the shape to a record\nwritten with the code. A CloudKit schema is forever: once deployed to\nproduction, a record type or field can be added but never renamed, retyped\nor removed, and every build a user ever ran must still read what a newer\none writes. The Library's own design (ADR-0014) points the way: events are\nimmutable and only ever added; strength is never stored; entries are\nedited rarely; the profile is small.\nDecision.\nOne custom zone, three record types, every field additive, and the\nLibrary's merge rule doing the merging — never CloudKit's.\n Code sample omitted. \nCKSyncEngine carries the records; TimbreCloud is the package\n (CloudKit on all three platforms). It keeps a small state file beside the\n Library — the engine's serialization, which events and entry edits the\n server has, the server's system fields for each entry — and nothing in it\n is a word.\nMerge is a union of events, then entries by latest edit — the\n Library's merge(entries:events:), unchanged. A server-changed conflict\n on an entry is resolved by taking the server's record, merging, and\n sending ours again over the server's system fields.\nDeletions are events. No record is ever deleted; a forgot event is\n the tombstone, and the merge honours it on every device.\nThe profile is taken from the server only when this device's is\n empty; otherwise this device's is sent. Two profiles edited apart is a\n case we do not pretend to solve.\nOff by default; a toggle per device; stopping keeps what is local.\n The container is iCloud.dev.hugopretorius.Timbre, shared by the Mac,\n the iPhone and the Watch regardless of bundle identifier.\nConsequences.\nThe mapping to and from CKRecord is pure and tested offline; the engine\n is not, and the two-device proof the product map asks for — teach on one,\n see it on the other, nettop empty — is a manual test on hardware.\nAdding a field is a change to a record type and a default on read; a\n rename is forbidden and the code says so where the fields are named.\nThe iCloud entitlements join all three apps. A build without a team runs\n with the toggle off; with a team, Xcode's automatic signing registers the\n container the first time.\nStrength is still never stored, here or anywhere: a device that fetches\n a year of events recomputes it.\nAlternatives considered.\nSwiftData with CloudKit mirroring: rejected (ADR-0013); it cannot\n express a union of histories.\nOne record per entry holding its history as a list: rejected; two\n devices appending to one list is the conflict the event design avoids.\nThe key-value store, as GDR-0010 first chose: rejected; too small,\n whole-value replacement, no per-record merge.",
+ "html": "Context
\nGDR-0023 chose CloudKit's private database and left the shape to a record\nwritten with the code. A CloudKit schema is forever: once deployed to\nproduction, a record type or field can be added but never renamed, retyped\nor removed, and every build a user ever ran must still read what a newer\none writes. The Library's own design (ADR-0014) points the way: events are\nimmutable and only ever added; strength is never stored; entries are\nedited rarely; the profile is small.
\nDecision
\nOne custom zone, three record types, every field additive, and the\nLibrary's merge rule doing the merging — never CloudKit's.
\nZone "Library" in the private database (custom, so changes can be fetched\nby token; the default zone cannot).\n\nEvent recordName = the event's composed id (ADR-0014)\n entry, kind, date, device, app?, drill?\n Immutable: saved once, never modified, never deleted.\nEntry recordName = the entry's id\n kind, term, heard?, meaning?, example?, tags, sourceDevice,\n sourceDate, sourceHow, sourceApp?, edited\n Replaced whole on edit; the later `edited` wins.\nProfile recordName = "profile"\n industries, role?, firstLanguage?\n
\n\nCKSyncEngine carries the records; TimbreCloud is the package\n(CloudKit on all three platforms). It keeps a small state file beside the\nLibrary — the engine's serialization, which events and entry edits the\nserver has, the server's system fields for each entry — and nothing in it\nis a word. \n- Merge is a union of events, then entries by latest edit — the\nLibrary's
merge(entries:events:), unchanged. A server-changed conflict\non an entry is resolved by taking the server's record, merging, and\nsending ours again over the server's system fields. \n- Deletions are events. No record is ever deleted; a
forgot event is\nthe tombstone, and the merge honours it on every device. \n- The profile is taken from the server only when this device's is\nempty; otherwise this device's is sent. Two profiles edited apart is a\ncase we do not pretend to solve.
\n- Off by default; a toggle per device; stopping keeps what is local.\nThe container is
iCloud.dev.hugopretorius.Timbre, shared by the Mac,\nthe iPhone and the Watch regardless of bundle identifier. \n
\nConsequences
\n\n- The mapping to and from
CKRecord is pure and tested offline; the engine\nis not, and the two-device proof the product map asks for — teach on one,\nsee it on the other, nettop empty — is a manual test on hardware. \n- Adding a field is a change to a record type and a default on read; a\nrename is forbidden and the code says so where the fields are named.
\n- The iCloud entitlements join all three apps. A build without a team runs\nwith the toggle off; with a team, Xcode's automatic signing registers the\ncontainer the first time.
\n- Strength is still never stored, here or anywhere: a device that fetches\na year of events recomputes it.
\n
\nAlternatives considered
\n\n- SwiftData with CloudKit mirroring: rejected (ADR-0013); it cannot\nexpress a union of histories.
\n- One record per entry holding its history as a list: rejected; two\ndevices appending to one list is the conflict the event design avoids.
\n- The key-value store, as GDR-0010 first chose: rejected; too small,\nwhole-value replacement, no per-record merge.
\n
\n"
+ },
{
"id": "ADR-0016",
"kind": "ADR",
diff --git a/web/src/lib/design/design.json b/web/src/lib/design/design.json
index 526ea10..b6b7f96 100644
--- a/web/src/lib/design/design.json
+++ b/web/src/lib/design/design.json
@@ -11,6 +11,6 @@
"path": "docs/design/product-map.md",
"title": "Timbre, the whole product",
"status": "2026-10-11 · Status: the map; its five decisions were taken on 2026-10-14 and are recorded in GDR-0023; GDR-0026 to GDR-0028 (2026-10-18) extend it to the Watch, the word of the day's bar and the bundled words. Constraining records: GDR-0001, GDR-0006, GDR-0010, GDR-0013. This document is where the product is planned as one thing; each piece that lands still gets its own record.",
- "html": "2026-10-11 · Status: the map; its five decisions were taken on 2026-10-14\nand are recorded in GDR-0023; GDR-0026 to GDR-0028 (2026-10-18) extend it\nto the Watch, the word of the day's bar and the bundled words. Constraining\nrecords: GDR-0001, GDR-0006, GDR-0010, GDR-0013. This document is where the product is planned as one\nthing; each piece that lands still gets its own record.
\nTimbre started as a key you hold to talk. It is about to become the place\nwhere the words you use get better: spelling, vocabulary, phrases,\nacronyms. This map lays out every piece, which surface owns it, where its\ndata lives, and what crosses the one boundary the product is built on —\nso nothing is built twice and nothing falls between two surfaces.
\n1. Three pillars
\n\n\n\n| Pillar | \nWhat it is | \nToday | \n
\n\n\n| Speak | \nDictate, fix/shorten/plain/explain, read aloud, say a to-do. Everything on-device, nothing leaves the Mac. | \nShipped (0.3.x, 0.4.0 forming) | \n
\n\n| Learn | \nThe Library — words, acronyms, phrases, spellings you are collecting — plus a profile that makes "explain" right for your industry, a word of the day, and practice that uses your own voice. | \nVocabulary, corrections and definitions exist as three Settings lists. Nothing else yet. | \n
\n\n| Run | \nBackstage: releases, decisions, plans, evals, launch, architecture, brand. The business, in one place. | \nShipped | \n
\n
\nSpeak is the wedge and stays the wedge: dictation that never leaves your\nMac. Learn is the reason to open the app when you are not dictating, and\nthe reason to want it on your phone. Run is ours.
\n2. Surfaces
\n\n\n\n| Surface | \nOwns | \nDoes not do | \n
\n\n\n| Mac app | \nEverything in Speak. The Library's capture side: a word you taught, a correction, a definition, a word you had explained — each lands in the Library. The Mac is where words are met. | \nNothing cloud-side. Zero network requests (GDR-0001/0013). | \n
\n\n| iPhone app (planned) | \nThe Library in your pocket: review, practise, word of the day, add a word you heard. The phone is where words are kept. | \nNo dictation into other apps (iOS cannot), no commands. It is a companion, not a port. | \n
\n\n| Apple Watch app (planned, GDR-0026) | \nThe to-do key on the wrist; the word of the day on the face; Spell it; a word you heard. Deterministic, phone-optional. | \nAnything that needs the model: that is the phone's or the Mac's, and arrives by sync. | \n
\n\n| Website | \nThe claim, the download, the network list. | \nAccounts, sync, anything personal. | \n
\n\n| Backstage | \nRunning the product. | \nAnything a user sees. | \n
\n\n| Cross-platform client (open) | \nA Library on Android or the web, if ever. | \nOnly exists if there is an account to sign into — see §4. | \n
\n
\n3. The Library
\nOne store replaces three lists. Every entry has a kind, and the kinds\nare exactly the four things Hugo wants to get better at, plus the one the\napp already keeps:
\n\n\n\n| Kind | \nExample | \nWhere it comes from | \nWhat uses it | \n
\n\n\n| word | \nephemeral — short-lived | \nexplain on a word; added by hand; word of the day | \nthe polisher's vocabulary; practice | \n
\n\n| acronym | \nADR — Architecture Decision Record | \ndefine in Settings; explain's answer, kept | \nexplain answers from it first (GDR-0012) | \n
\n\n| phrase | \na canary in the coal mine — an early warning | \nexplain on a selection; added by hand | \npractice; the polisher leaves it intact | \n
\n\n| spelling | \ndefinately → definitely | \nfix corrected it; the spell checker fallback | \npractice ("spell it"); a correction if it keeps happening | \n
\n\n| correction | \ntimber kit → TimbreKit | \ntaught in Settings; one day from your own edits | \nCorrectionTable, before the model (GDR-0011) | \n
\n
\nAn entry carries: the term, its meaning, an example sentence, where it\ncame from (which app, dictated or typed, on which day), tags including the\nindustry it belongs to, a strength that practice raises and time lowers\n(the spaced-repetition number), and its history. Nothing in an entry is\never transmitted by the Mac app; the entry is the user's.
\nProfile. Industry (one or more), role, first language. Used in two\nplaces: explain's prompt ("the reader works in healthcare, so PA means\nphysician assistant before it means personal assistant"), and the word of\nthe day's choice of lists. Lives in the Library, syncs with it.
\n4. The one boundary, and the sync decision
\nThe Mac binary makes no network requests. That is the claim on the\nheadline, falsifiable with a network monitor, and the entire competitive\nwedge (GDR-0001, GDR-0006, GDR-0013). The Library wants to be on the phone.\nThere are two ways to get it there, and they are not equal:
\n\n\n\n | \nA. iCloud | \nB. A Timbre account | \n
\n\n\n| How | \nCloudKit, private database, the user's own iCloud. Apple's daemons do the syncing; the binary still makes zero requests. GDR-0010 already accepted this shape for the dictionary. | \nSupabase (already running Backstage's auth), the apps call it. | \n
\n\n| Reaches | \nMac and iPhone (and iPad, Watch). | \nAnything with a browser. | \n
\n\n| Needs | \nThe iCloud capability on the App ID (done), a CloudKit container, an entitlement. No server, no cost, no account to create. | \nSign-up, sessions, a schema with row-level security, a privacy policy, GDPR handling, uptime. | \n
\n\n| Costs the claim | \nNothing. /network stays at one entry. | \nThe headline needs a second asterisk: the app phones home when you sign in. Amends GDR-0013; /network gains entries. | \n
\n\n| When it is right | \nNow. | \nWhen a non-Apple client is real and wanted — not before. | \n
\n
\nRecommendation: A first, and design the Library's store so that B is a\nsecond backend behind the same interface, not a rewrite. "Apple pure" is\nnot a lite tier; it is the product. B becomes a decision the day there is\nan Android or web Library to sign into, and it gets its own record then.
\n5. Learning, using what Timbre already does
\nPractice should use Timbre's own abilities rather than becoming a flash-card\napp with a microphone bolted on:
\n\n- Spell it. The phone shows the meaning; you say the word, then spell it aloud. The on-device recogniser hears both.
\n- Say it. The phone shows a word or phrase; you use it in a sentence, out loud. The polisher's model judges whether it was used in its meaning — a guardrail-scored check, like every command (GDR-0012's rule for new verbs applies to new drills).
\n- Explain it. The card you saw on the Mac, now a question: what does ADR stand for?
\n- Word of the day. Chosen from the Library's weakest entries first, then from a bundled list for the profile's industry. Delivered as a local notification on Mac and iPhone. Saying it in a dictation that day marks it met. The bar it must clear is GDR-0027; where its facts come from is GDR-0028.
\n- Gamification, the private kind. Strength bars, streaks, a weekly review. No leaderboard, no sharing, nothing that needs a server.
\n
\n6. No gaps, no duplication
\n\n\n\n| Capability | \nOwner | \nStore | \nSync | \n
\n\n\n| Dictation, commands, read aloud, to-dos | \nMac app | \n— | \n— | \n
\n\n| Capturing a Library entry from use | \nMac app | \nLibrary (local) | \niCloud | \n
\n\n| Browsing, editing the Library | \nMac Settings → Library; iPhone | \nLibrary | \niCloud | \n
\n\n| Profile | \niPhone and Mac, same screen | \nLibrary | \niCloud | \n
\n\n| Practice, word of the day | \niPhone and Mac (a Practice window); the Watch for Spell it | \nLibrary (strength, history) | \niCloud | \n
\n\n| Usage count | \nMac app | \nUserDefaults | \nnever (GDR: this Mac only) | \n
\n\n| Dictation capture for tuning | \nMac app | \njsonl on disk | \nnever (GDR-0004) | \n
\n\n| Releases, decisions, plans, evals, launch, architecture, brand | \nBackstage | \nthe repo | \nCI | \n
\n
\nShared logic lives in one place: the Library's model, parsing and practice\nscoring move into a platform-neutral TimbreCore package that the Mac app\nand the iPhone app both import; TimbreKit keeps the macOS-only pipeline.\nThat split is a record of its own (an ADR) before the first iOS line.
\n7. Phases
\n\n\n\n| Phase | \nDelivers | \nProof | \n
\n\n\n| 0 | \n0.4.0: icon, disk image, to-dos, plain, live typing. Launch. | \nPublic. | \n
\n\n| 1 | \nThe Library on Mac. One store with kinds; Settings › Dictionary becomes Library; explain, fix and define write to it; export to a file. | \nEvery existing list migrates; nothing lost. | \n
\n\n| 2 | \nTimbreCore + iCloud sync. The split, the CloudKit container, sync on two Macs. | \nTeach on one Mac, see it on the other, nettop still empty. | \n
\n\n| 3 | \nTimbre for iPhone. The Library, profile, word of the day, Spell it. | \nOn the phone a minute after teaching on the Mac. | \n
\n\n| 4 | \nPractice that listens. Say it, Explain it, strength and streaks. | \nA drill corpus the harness can score. | \n
\n\n| 5 | \nProfile-aware explain on the Mac. | \nA commands corpus case per industry. | \n
\n\n| — | \nAccount and cross-platform. | \nDecided when phase 3 has users asking. | \n
\n
\nGDR-0026 adds the Watch after phase 3: the to-do key first, because it\nneeds nothing new; then the complication and Spell it.
\n8. Decisions this map asked for — taken 2026-10-14 (GDR-0023)
\n\n- Sync by iCloud first, account later or never (§4). Taken: iCloud first; the store built behind a sync interface.
\n- The iPhone app is a companion — the Library and practice — not a dictation app (§2). Taken: companion; no keyboard extension planned.
\n- One Library, five kinds, replacing the three Settings lists (§3). Taken: five kinds, migrated on the Mac first.
\nTimbreCore split before the first iOS file (§6). Taken: created with the Library. \n- Practice uses the voice — drills are scored like commands, with a corpus (§5). Taken: Spell it first.
\n
\n"
+ "html": "2026-10-11 · Status: the map; its five decisions were taken on 2026-10-14\nand are recorded in GDR-0023; GDR-0026 to GDR-0028 (2026-10-18) extend it\nto the Watch, the word of the day's bar and the bundled words. Constraining\nrecords: GDR-0001, GDR-0006, GDR-0010, GDR-0013. This document is where the product is planned as one\nthing; each piece that lands still gets its own record.
\nTimbre started as a key you hold to talk. It is about to become the place\nwhere the words you use get better: spelling, vocabulary, phrases,\nacronyms. This map lays out every piece, which surface owns it, where its\ndata lives, and what crosses the one boundary the product is built on —\nso nothing is built twice and nothing falls between two surfaces.
\n1. Three pillars
\n\n\n\n| Pillar | \nWhat it is | \nToday | \n
\n\n\n| Speak | \nDictate, fix/shorten/plain/explain, read aloud, say a to-do. Everything on-device, nothing leaves the Mac. | \nShipped (0.3.x, 0.4.0 forming) | \n
\n\n| Learn | \nThe Library — words, acronyms, phrases, spellings you are collecting — plus a profile that makes "explain" right for your industry, a word of the day, and practice that uses your own voice. | \nVocabulary, corrections and definitions exist as three Settings lists. Nothing else yet. | \n
\n\n| Run | \nBackstage: releases, decisions, plans, evals, launch, architecture, brand. The business, in one place. | \nShipped | \n
\n
\nSpeak is the wedge and stays the wedge: dictation that never leaves your\nMac. Learn is the reason to open the app when you are not dictating, and\nthe reason to want it on your phone. Run is ours.
\n2. Surfaces
\n\n\n\n| Surface | \nOwns | \nDoes not do | \n
\n\n\n| Mac app | \nEverything in Speak. The Library's capture side: a word you taught, a correction, a definition, a word you had explained — each lands in the Library. The Mac is where words are met. | \nNothing cloud-side. Zero network requests (GDR-0001/0013). | \n
\n\n| iPhone app (planned) | \nThe Library in your pocket: review, practise, word of the day, add a word you heard. The phone is where words are kept. | \nNo dictation into other apps (iOS cannot), no commands. It is a companion, not a port. | \n
\n\n| Apple Watch app (planned, GDR-0026) | \nThe to-do key on the wrist; the word of the day on the face; Spell it; a word you heard. Deterministic, phone-optional. | \nAnything that needs the model: that is the phone's or the Mac's, and arrives by sync. | \n
\n\n| Website | \nThe claim, the download, the network list. | \nAccounts, sync, anything personal. | \n
\n\n| Backstage | \nRunning the product. | \nAnything a user sees. | \n
\n\n| Cross-platform client (open) | \nA Library on Android or the web, if ever. | \nOnly exists if there is an account to sign into — see §4. | \n
\n
\n3. The Library
\nOne store replaces three lists. Every entry has a kind, and the kinds\nare exactly the four things Hugo wants to get better at, plus the one the\napp already keeps:
\n\n\n\n| Kind | \nExample | \nWhere it comes from | \nWhat uses it | \n
\n\n\n| word | \nephemeral — short-lived | \nexplain on a word; added by hand; word of the day | \nthe polisher's vocabulary; practice | \n
\n\n| acronym | \nADR — Architecture Decision Record | \ndefine in Settings; explain's answer, kept | \nexplain answers from it first (GDR-0012) | \n
\n\n| phrase | \na canary in the coal mine — an early warning | \nexplain on a selection; added by hand | \npractice; the polisher leaves it intact | \n
\n\n| spelling | \ndefinately → definitely | \nfix corrected it; the spell checker fallback | \npractice ("spell it"); a correction if it keeps happening | \n
\n\n| correction | \ntimber kit → TimbreKit | \ntaught in Settings; one day from your own edits | \nCorrectionTable, before the model (GDR-0011) | \n
\n
\nAn entry carries: the term, its meaning, an example sentence, where it\ncame from (which app, dictated or typed, on which day), tags including the\nindustry it belongs to, a strength that practice raises and time lowers\n(the spaced-repetition number), and its history. Nothing in an entry is\never transmitted by the Mac app; the entry is the user's.
\nProfile. Industry (one or more), role, first language. Used in two\nplaces: explain's prompt ("the reader works in healthcare, so PA means\nphysician assistant before it means personal assistant"), and the word of\nthe day's choice of lists. Lives in the Library, syncs with it.
\n4. The one boundary, and the sync decision
\nThe Mac binary makes no network requests. That is the claim on the\nheadline, falsifiable with a network monitor, and the entire competitive\nwedge (GDR-0001, GDR-0006, GDR-0013). The Library wants to be on the phone.\nThere are two ways to get it there, and they are not equal:
\n\n\n\n | \nA. iCloud | \nB. A Timbre account | \n
\n\n\n| How | \nCloudKit, private database, the user's own iCloud. Apple's daemons do the syncing; the binary still makes zero requests. GDR-0010 already accepted this shape for the dictionary. | \nSupabase (already running Backstage's auth), the apps call it. | \n
\n\n| Reaches | \nMac and iPhone (and iPad, Watch). | \nAnything with a browser. | \n
\n\n| Needs | \nThe iCloud capability on the App ID (done), a CloudKit container, an entitlement. No server, no cost, no account to create. | \nSign-up, sessions, a schema with row-level security, a privacy policy, GDPR handling, uptime. | \n
\n\n| Costs the claim | \nNothing. /network stays at one entry. | \nThe headline needs a second asterisk: the app phones home when you sign in. Amends GDR-0013; /network gains entries. | \n
\n\n| When it is right | \nNow. | \nWhen a non-Apple client is real and wanted — not before. | \n
\n
\nRecommendation: A first, and design the Library's store so that B is a\nsecond backend behind the same interface, not a rewrite. "Apple pure" is\nnot a lite tier; it is the product. B becomes a decision the day there is\nan Android or web Library to sign into, and it gets its own record then.
\n5. Learning, using what Timbre already does
\nPractice should use Timbre's own abilities rather than becoming a flash-card\napp with a microphone bolted on:
\n\n- Spell it. The phone shows the meaning; you say the word, then spell it aloud. The on-device recogniser hears both.
\n- Say it. The phone shows a word or phrase; you use it in a sentence, out loud. The polisher's model judges whether it was used in its meaning — a guardrail-scored check, like every command (GDR-0012's rule for new verbs applies to new drills).
\n- Explain it. The card you saw on the Mac, now a question: what does ADR stand for?
\n- Word of the day. Chosen from the Library's weakest entries first, then from a bundled list for the profile's industry. Delivered as a local notification on Mac and iPhone. Saying it in a dictation that day marks it met. The bar it must clear is GDR-0027; where its facts come from is GDR-0028.
\n- Gamification, the private kind. Strength bars, streaks, a weekly review. No leaderboard, no sharing, nothing that needs a server.
\n
\n6. No gaps, no duplication
\n\n\n\n| Capability | \nOwner | \nStore | \nSync | \n
\n\n\n| Dictation, commands, read aloud, to-dos | \nMac app | \n— | \n— | \n
\n\n| Capturing a Library entry from use | \nMac app | \nLibrary (local) | \niCloud | \n
\n\n| Browsing, editing the Library | \nMac Settings → Library; iPhone | \nLibrary | \niCloud | \n
\n\n| Profile | \niPhone and Mac, same screen | \nLibrary | \niCloud | \n
\n\n| Practice, word of the day | \niPhone and Mac (a Practice window); the Watch for Spell it | \nLibrary (strength, history) | \niCloud | \n
\n\n| Usage count | \nMac app | \nUserDefaults | \nnever (GDR: this Mac only) | \n
\n\n| Dictation capture for tuning | \nMac app | \njsonl on disk | \nnever (GDR-0004) | \n
\n\n| Releases, decisions, plans, evals, launch, architecture, brand | \nBackstage | \nthe repo | \nCI | \n
\n
\nShared logic lives in one place: the Library's model, parsing and practice\nscoring move into a platform-neutral TimbreCore package that the Mac app\nand the iPhone app both import; TimbreKit keeps the macOS-only pipeline.\nThat split is a record of its own (an ADR) before the first iOS line.
\n7. Phases
\n\n\n\n| Phase | \nDelivers | \nProof | \n
\n\n\n| 0 | \n0.4.0: icon, disk image, to-dos, plain, live typing. Launch. | \nPublic. | \n
\n\n| 1 | \nThe Library on Mac. One store with kinds; Settings › Dictionary becomes Library; explain, fix and define write to it; export to a file. | \nEvery existing list migrates; nothing lost. | \n
\n\n| 2 | \nTimbreCore + iCloud sync. The split (ADR-0013, ADR-0015), the CloudKit container and schema (ADR-0017), sync on two Macs. | \nTeach on one Mac, see it on the other, nettop still empty. | \n
\n\n| 3 | \nTimbre for iPhone. The Library, profile, word of the day, Spell it. | \nOn the phone a minute after teaching on the Mac. | \n
\n\n| 4 | \nPractice that listens. Say it, Explain it, strength and streaks. | \nA drill corpus the harness can score. | \n
\n\n| 5 | \nProfile-aware explain on the Mac. | \nA commands corpus case per industry. | \n
\n\n| — | \nAccount and cross-platform. | \nDecided when phase 3 has users asking. | \n
\n
\nGDR-0026 adds the Watch after phase 3: the to-do key first, because it\nneeds nothing new; then the complication and Spell it.
\n8. Decisions this map asked for — taken 2026-10-14 (GDR-0023)
\n\n- Sync by iCloud first, account later or never (§4). Taken: iCloud first; the store built behind a sync interface.
\n- The iPhone app is a companion — the Library and practice — not a dictation app (§2). Taken: companion; no keyboard extension planned.
\n- One Library, five kinds, replacing the three Settings lists (§3). Taken: five kinds, migrated on the Mac first.
\nTimbreCore split before the first iOS file (§6). Taken: created with the Library. \n- Practice uses the voice — drills are scored like commands, with a corpus (§5). Taken: Spell it first.
\n
\n"
}
]
diff --git a/web/src/lib/releases/releases.json b/web/src/lib/releases/releases.json
index a0e2791..9440a63 100644
--- a/web/src/lib/releases/releases.json
+++ b/web/src/lib/releases/releases.json
@@ -8,14 +8,14 @@
"sections": [
{
"name": "Added",
- "html": "\n- The Library. Settings › Dictionary is now Settings › Library: one store\nfor the words the model should prefer, your acronyms, phrases, spellings\nand corrections, each with a meaning, an example, where it came from and\na strength that practice raises and time lowers. Everything you had taught\nmoved over, in order. Export the whole thing as one file; import merges.
\n- A word of the day. The word in your Library that has faded furthest,\nchosen at the start of each day: in the menu bar, on a card with its\nfacts labelled by source (your meaning, Apple's dictionary, a sentence\nwritten by the on-device model), read aloud on request. Say it in a\ndictation and it counts as met. A morning notification, off unless you\nturn it on.
\n- About you. Your field, role and first language, in Settings › Library.\nChooses the word lists; will make explain right for your reader.
\n- Practice. Spell it, Say it and Explain it, spoken and scored on your\nMac: letters checked exactly, a sentence judged by the on-device model\ninside a guardrail, an explanation checked by its words first. Words come\nup as they fade. A streak, the week's count and the five words most worth\na minute, in Settings › Library. Nothing is ranked or shared.
\n
\n"
+ "html": "\nThe Library. Settings › Dictionary is now Settings › Library: one store\nfor the words the model should prefer, your acronyms, phrases, spellings\nand corrections, each with a meaning, an example, where it came from and\na strength that practice raises and time lowers. Everything you had taught\nmoved over, in order. Export the whole thing as one file; import merges.
\n \nA word of the day. The word in your Library that has faded furthest,\nchosen at the start of each day: in the menu bar, on a card with its\nfacts labelled by source (your meaning, Apple's dictionary, a sentence\nwritten by the on-device model), read aloud on request. Say it in a\ndictation and it counts as met. A morning notification, off unless you\nturn it on.
\n \nAbout you. Your field, role and first language, in Settings › Library.\nChooses the word lists; will make explain right for your reader.
\n \nPractice. Spell it, Say it and Explain it, spoken and scored on your\nMac: letters checked exactly, a sentence judged by the on-device model\ninside a guardrail, an explanation checked by its words first. Words come\nup as they fade. A streak, the week's count and the five words most worth\na minute, in Settings › Library. Nothing is ranked or shared.
\n \niCloud, if you want it. Settings › Library › Sync my Library through\niCloud: your words, meanings and practice history in your own iCloud\naccount, so your iPhone, Watch and other Macs have the same Library. Off\nby default. Timbre still sends nothing itself; macOS's iCloud daemon does\nthe syncing. Never a dictation, a capture or a usage count.
\n \n
\n"
},
{
"name": "Changed",
"html": "\n- The Library lives in its own package, TimbreCore, which the iPhone and\nWatch apps will share. Your words are in\n
~/Library/Application Support/Timbre/library.json; the old lists in\npreferences are left in place. \n- The menu bar shows the mark. The four bars from the icon, without\nthe tile, in the menu bar's own colour — and they carry the state: dimmed\nwhile setting up, in the retro colours while the mic is open, fading\nwhile the cleanup runs, all coral when something needs you. The\nmicrophone symbol is gone.
\n
\n"
}
],
- "items": 6
+ "items": 7
},
{
"version": "0.4.0",
diff --git a/web/src/lib/university/icloud-and-cloudkit.html b/web/src/lib/university/icloud-and-cloudkit.html
index 8ce89ed..e69cdcc 100644
--- a/web/src/lib/university/icloud-and-cloudkit.html
+++ b/web/src/lib/university/icloud-and-cloudkit.html
@@ -78,5 +78,5 @@ Testing without a server
In Timbre
-
LibrarySync is a protocol in TimbreCore; the CloudKit implementation lives where the framework is. Events are immutable records in a custom zone of the private database; merge is a union; edits take the latest; deletions are tombstones. Sync is off by default, described plainly, and syncs the Library and the profile — never a dictation, a capture or a usage count.
+
TimbreCloud (ADR-0017) is the implementation: CKSyncEngine over one custom zone, three record types — Event named by the event’s id and saved once, Entry replaced whole on edit with the later edited winning, one Profile — and the Library’s own merge doing the merging. A server-changed conflict takes the server’s copy, merges, and sends again over its system fields. The mapping to and from CKRecord is pure and tested offline; the two-device proof is a manual test with nettop open. Sync is off by default, described plainly, and carries the Library and the profile — never a dictation, a capture or a usage count.
diff --git a/web/src/routes/(public)/+page.svelte b/web/src/routes/(public)/+page.svelte
index b71b396..22ca83b 100644
--- a/web/src/routes/(public)/+page.svelte
+++ b/web/src/routes/(public)/+page.svelte
@@ -17,7 +17,7 @@
const faqs = [
{
q: 'Does Timbre send my voice anywhere?',
- a: 'No. Timbre sends nothing about you anywhere. Out of the box it makes no network requests at all; if you turn on update checks, it fetches one small version file from this website, carrying nothing. Speech recognition, text cleanup, read-aloud and the text commands all run on your Mac using Apple’s on-device models. You can verify this yourself with a network monitor such as Little Snitch.'
+ a: 'No. Timbre sends nothing about you anywhere. Out of the box it makes no network requests at all; if you turn on update checks, it fetches one small version file from this website, carrying nothing. Speech recognition, text cleanup, read-aloud and the text commands all run on your Mac using Apple’s on-device models. If you turn on iCloud sync for your Library — the words you teach it, never what you dictate — macOS’s own iCloud daemon moves it between your devices under your account; Timbre still makes no request. You can verify this yourself with a network monitor such as Little Snitch.'
},
{
q: 'Does it work offline?',
@@ -124,7 +124,7 @@
name="description"
content="Hold a key, speak, release. Cleaned-up text appears wherever you type. Tap another key to hear text read back; hold a third to fix, shorten or explain a selection, or to drop a to-do into Reminders. Speech recognition and AI run entirely on your Mac — zero network requests, no account, free."
/>
-
+
{@html `