uttrflow / uttrflow/uttrflow-swift
Retention trusts the wall clock with no guard: a dictation dated in the future outlives the promise, and one read at a clock that jumped ahead deletes the whole history
- Dominant language
- Swift
- Stars
- 4
- Forks
- 17
- Avg merge
- 3h 32m
- Merged PRs (30d)
- 277
Description
## What happens
Every store decides "deleted after N days" from the record's stored date against `Date()` at the time of reading, and applies the answer to the disk on read:
- History: `DictationRecord.survives(days:now:)` is `when + days * 86_400 > now` (`Sources/UttrflowHistory/DictationRecord.swift:54-57`), and `DictationHistoryStore.records(keeping:)` rewrites the file whenever that drops a record (`Sources/UttrflowHistory/DictationHistoryStore.swift:38-43`). The main window calls it on every refresh (`Sources/Uttrflow/AppDelegate.swift:1369`).
- Clipboard: `ClipboardStore.survives` is the same arithmetic on `copiedAt` (`Sources/UttrflowClipboard/ClipboardStore.swift:374-382`), applied on read by `clips(keeping:)`.
- Recordings: `RecordingStore.waiting(now:)` keeps a file while `now.timeIntervalSince(creationDate) < 24 h` and deletes the rest (`Sources/UttrflowAudio/RecordingStore.swift:103-107`).
Two consequences, both measured:
1. **Clock set back.** A dictation made while the clock read a year ahead is kept for a year plus the window after the clock is corrected. A negative age also passes the recordings check, so a recording dated ahead is never pruned until the clock catches up.
2. **Clock jumps ahead once.** A single refresh while the clock is wrong deletes every record older than the window from disk. When the clock comes back, the history is gone.
## Why it matters
The retention setting is a privacy promise ("deleted after N days"), and #2 is irreversible data loss triggered by something outside the app: a manual date change, a Mac whose clock is wrong after a dead battery before the network syncs, a test of another app. The first is the promise quietly not kept for clips that can include a secret.
## How to reproduce (headless)
A throwaway test in `Tests/UttrflowHistoryTests/DictationHistoryStoreTests.swift` with its `Sandbox`:
- Seed `[record dated epoch + 365 days, record dated epoch − 1 day]`, read with `Retention(days: 7, now: epoch + 30 days)`: the future-dated record is still returned.
- Seed `[record dated epoch − 1 day]`, read once with `Retention(days: 7, now: epoch + 400 days)`, then read with `Retention(days: 7, now: epoch)`: the result is `[]` and the file is gone.
Both on main at 26d7bc1.
## Acceptance criteria
- A record dated after `now` (beyond a small tolerance) is treated as dated `now` for retention, in all three stores, so it cannot outlive the window.
- Reading does not permanently delete on the strength of a clock that moved far forward in one step; for example the read filters what is shown but leaves the disk tidy-up to a write, or refuses to tidy when `now` is implausibly far past the newest record.
- Tests cover both directions for history, clips and recordings.
Contributor guide
Research direction
Start with the retention paths named in Sources/UttrflowHistory/DictationRecord.swift, DictationHistoryStore.swift, Sources/UttrflowClipboard/ClipboardStore.swift, and Sources/UttrflowAudio/RecordingStore.swift, then run the Sandbox tests in Tests/UttrflowHistoryTests/DictationHistoryStoreTests.swift. Trace how reads filter and rewrite stored data. Done means future dates cannot outlive retention, clock jumps do not cause irreversible read-time deletion, and tests cover both directions for all three stores.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- macos, swift
- Domain
- desktop, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 56/100