uttrflow / uttrflow/uttrflow-swift

One entry an older build cannot decode sets aside the whole dictionary or history, silently: going back a version loses every learned word

Open
#617 0 comments 0 reactions 0 assignees View on GitHub
area:general enhancement needs-info P2
Dominant language
Swift
Stars
4
Forks
17
Avg merge
3h 32m
Merged PRs (30d)
277

Description

## What happens

The dictionary, history, snippet and clipboard stores each decode their whole file as one array (`LocalStore.read`, `Sources/UttrflowCore/Support/StoredList.swift:41-56`). If any single element fails to decode, the whole file is renamed to `.unreadable-` and the store answers empty (`PersonalDictionaryStore.load`, `Sources/UttrflowDictionary/PersonalDictionaryStore.swift:221-222`; `DictationHistoryStore.load`, `Sources/UttrflowHistory/DictationHistoryStore.swift:120-122`). The next write starts a fresh file beside it.

Entries carry enums with synthesized `Codable`, so a value added by a newer build is a decode failure for an older one. The concrete case today: 66e9fab added `WordOrigin.shipped` (`Sources/UttrflowDictionary/DictionaryEntry.swift:14-15`), and every dictionary gains a `shipped` entry for the product's own name. A user who installs 2026.9.14 and then goes back to an earlier build (a bad update, a rollback to a known-good version) gets:

- an empty Dictionary page, with every learned and added word moved into the set-aside file;
- no notice: only `ClipboardStore` ever checks `LocalStore.hasSetAside` (`Sources/UttrflowClipboard/ClipboardStore.swift:437`), and only to protect its picture sweep;
- nothing that brings the set-aside file back when they upgrade again, because by then a new `dictionary.v1.json` exists.

`Docs/history-store-file.md` explains the choice not to salvage record by record ("the realistic corruption is a whole file somebody mangled"). A file written by a newer version of the same app is a second realistic case the doc does not consider, and its damage is every entry, not one.

## Why it matters

The personal dictionary is the thing a user builds up over weeks so their names and terms come out right. Losing it silently makes dictation visibly worse with no explanation, and the history case removes what the Corrections and Insights pages are built from.

## How to reproduce (headless)

Write a `dictionary.v1.json` with three entries, one of which has `"origin": "someFutureOrigin"`, into a temporary directory and read it through `PersonalDictionaryStore(file:).allEntries()`. On main (26d7bc1) the answer is `[]` and the folder holds only `dictionary.v1.json.unreadable-`.

## Acceptance criteria (needs a product decision first)

Options, not mutually exclusive:

- decode element by element and keep the entries that decode, setting aside only the file's original bytes as today;
- decode unknown enum raw values to a fallback case rather than failing the entry;
- tell the user when a store has a set-aside file, and offer to bring it back when this build can read it.

Whichever is chosen, a test pins that one undecodable entry does not empty the dictionary or the history.

Contributor guide

Open the contributing guide

Research direction

Start with LocalStore.read and the load methods in Sources/UttrflowDictionary/PersonalDictionaryStore.swift and Sources/UttrflowHistory/DictationHistoryStore.swift, then run the headless reproduction described in the issue. After the product decision, implement the chosen behavior across the affected stores and add a regression test showing that one undecodable entry does not empty the dictionary or history.

Written by the indexing model from the issue text.

Assessment

Tech stack
swift
Domain
data, desktop-dev
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.