fix: an imported backup no longer rolls progress back

The progress store merged like the mark stores do, keeping whichever copy had
the earlier timestamp. That is right for a mark — `at` is when the reader made
it, membership is the whole fact, and the oldest stamp is the truest one — and
wrong here, because this record *is* the data. A status, an effort, a note and
whether an event repeats all live in it, and `at` says when one of those last
changed.

So the earlier copy winning discarded every edit made after it, in both of the
directions an import actually happens in: restoring a backup taken before an
evening's work undid the evening, and importing an old file onto a device with
newer progress rolled the device back. Neither is recoverable — there is no
account and no server holding a second copy.

The later record wins now. Nothing is removed in either direction, and taking
the maximum of two timestamps is as order-independent and idempotent as the rule
it replaces. `mergeProgress` came out of the hook to be tested, which is also
how the ignored store's opposite rule is now written down rather than assumed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-20 06:37:11 +02:00
co-authored by Claude Opus 5
parent 7bb7dc843c
commit f9ef21a366
3 changed files with 129 additions and 11 deletions
+12 -1
View File
@@ -563,7 +563,18 @@ event whose lane is missing.
Import **merges** every set: a mark present in either the file or the current device survives, and
import never removes one. `daily` merges as a **union of days per ID** — every day either side
recorded is a day the reader actually played. An export written before daily checklists existed
recorded is a day the reader actually played.
**Which copy wins a conflict differs by store, and the difference is the shape of the data.** For
`ignored` (and the legacy `completions`) the **earlier** mark wins: `at` records when the reader made
it, membership in the set is the whole fact, and the oldest timestamp is the truest answer to when
they said it. For `progress` the **later** record wins, because there the record *is* the data — a
status, an effort, a note, whether it repeats — and `at` is when one of those last changed. Keeping
the earlier copy there discards every edit made after it, in both directions an import happens in:
restoring a backup taken before an evening's work would roll that evening back, and importing an old
file onto a device with newer progress would roll the device back. Taking the maximum of the two
timestamps keeps the merge order-independent and idempotent either way, and removes nothing.
`mergeProgress` is pure and `test/progress.test.ts` pins it. An export written before daily checklists existed
simply has no `daily` key, which is not an error. Losing a user's marks to a bad import is unrecoverable, so the merge is
deliberately one-directional. A file whose `format` is unrecognised is refused outright rather than
partly applied.