Decouple transcripts from note text; end-to-end watch transfer acks

Schema v3: transcription attempts accumulate on AudioInfo (text, locale,
confidence, forced flag) — latest attempt is displayed, payload.text is
purely user writing, v2 notes migrate at read time. Editor shows the
transcript in its own live-updating section with insert-into-note; search
matches transcripts weighted by confidence. Watch recordings are now
deleted only on the phone's durable-ingest ack (transferUserInfo), closing
every phone-side loss window; failed sidecar writes unstage instead of
orphaning, and unreadable inbox recordings are surfaced in logs.

Claude-Session: https://claude.ai/code/session_014esDWi42URLEC6Cj17hGQ3
This commit is contained in:
2026-07-16 12:29:49 -04:00
parent c29d25748a
commit c5c90e9441
15 changed files with 546 additions and 55 deletions
+16
View File
@@ -80,4 +80,20 @@ struct AudioNotePayloadTests {
@Test func decodeRejectsEmptyMetadata() {
#expect(AudioNotePayload(metadata: [:]) == nil)
}
// MARK: - IngestAck
@Test func ackRoundTrips() {
let ack = IngestAck(id: "01J8Z3M9K7QW2R4T6Y8B0C1D2E")
let decoded = IngestAck(userInfo: ack.userInfo())
#expect(decoded == ack)
}
@Test func ackRejectsNewerVersionAndForeignUserInfo() {
var dict = IngestAck(id: "01J8Z3M9K7QW2R4T6Y8B0C1D2E").userInfo()
dict["ackV"] = IngestAck.currentVersion + 1
#expect(IngestAck(userInfo: dict) == nil)
#expect(IngestAck(userInfo: [:]) == nil)
#expect(IngestAck(userInfo: ["unrelated": true]) == nil)
}
}