You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(my-festival): migrate to LogEntry check-in model (schema v2) (#463)
* feat(my-festival): migrate to LogEntry check-in model (schema v2)
Make the check-in the primary My Festival entity (ADR 0006 Phases 1-2).
Storage moves from per-drink UserDrinkState blobs to per-entry LogEntry
records plus a per-festival want-to-try set; UserDrinkState becomes a
derived per-drink view so no UI consumer changes in this phase.
- New LogEntry value object (stable UUID id, when, optional drinkId/title,
note, photoIds, rating?, wouldRecommend?). A tasting is drinkId != null;
the kind is derived, not stored. Edit/delete key off id.
- UserDataStore v2: entry API (readEntries/writeEntry/removeEntry) plus a
per-drink want-to-try flag. read/readAll now derive the aggregate
(rating/notes/photos = most recent tasting; pours = tasting count) via a
single-pass drinkId -> entries index.
- One-time, flag-gated, idempotent, crash-safe v1 -> v2 migration
(migrateToLogEntries): want-to-try -> plan set; each tasting timestamp ->
a LogEntry (when preserved to ms); drink-level rating/notes/photos attach
to the most recent tasting, or synthesise one at updatedAt if the drink
was rated but never tasted. Deterministic v5 ids make re-runs overwrite
rather than duplicate; blobs are deleted only after their entries are
written. Rollback fail-safe: a newer-than-build payload is quarantined,
never crashes.
- Repository mutators re-routed to the entry API, still returning the
derived UserDrinkState (#410/#447 shape). Adds a uuid dependency.
Golden-tested migration (synthesis, ms carry, idempotency, partial-run
recovery, quarantine). Existing model/provider/screen tests stay green.
Relates to #315. Fixes#461.
* refactor(my-festival): keep drink rating/notes independent of tastings
Address review: a rating is personal tracking, not a claim that a drink
was drunk — so rating (and notes) must be settable without marking a
drink tasted, and clearing the tasting log must never wipe a rating.
Store drink-level rating/notes/photos in a per-drink detail record
(drink_detail_{festivalId}_{drinkId}) separate from the LogEntry tasting
timeline. UserDrinkState derives rating/notes/photos from that record and
pours from the tasting entries. This restores exact v1 behaviour:
- Rating or noting an untasted drink no longer synthesises a tasting; the
drink stays not-tasted.
- Toggling tasted off (or deleting all tastings) preserves the rating.
- Clearing rating+notes prunes the record to empty, as before.
Migration is now lossless and behaviour-preserving: rating/notes/photos
move to the detail record, tasting timestamps become bare pour entries,
and a rated-but-never-tasted drink keeps isTasted == false. Diverges from
ADR 0006's "rating = most recent tasting" derivation per maintainer call;
ADR to be updated.
Relates to #315. Fixes#461.
* docs(adr): amend 0006 — rating/notes are drink-level, not per-tasting
Record the sub-decision reversal from #461/#463: a drink's rating and
notes are drink-level and independent of the tasting timeline, so a drink
can be rated without being marked tasted and clearing tastings never wipes
a rating. Supersedes ADR 0006's "rating = most recent tasting" derivation
and the rated-but-never-tasted synthesis; the check-in-as-primary-entity
decision itself stands. Adds an Amendments section and updates the index.
Relates to #461.
* fix(my-festival): don't prune want-to-try state on removeTasting
Address review (PR #463): removeTasting returned null when a drink had no
tastings, even if it was want-to-try or rated — causing the provider to
prune still-valid in-memory state. Return the derived state instead, which
is null only when the drink has no signal at all.
Also harden LogEntry.copyWith to not accept an id: identity is assigned
once and storage keys off it, so edits must reuse the same id.
Relates to #461.
* docs(my-festival): clarify LogEntry doc after rating decoupling
Address review (PR #463): the class doc implied a tasting carries the
UI-facing rating/notes. After ADR 0006's amendment those are drink-level
(a separate detail record); the entry's per-pour rating/note/photo fields
are reserved for the #415/#417 capture flow and not populated yet.
Relates to #461.
* test(my-festival): cover LogEntry.toString and legacy merge-onto-blob
Close the patch-coverage gaps on new code: a LogEntry.toString assertion,
and a migrateLegacyData test that folds a legacy favourite onto a
pre-existing v1 blob and asserts neither the blob's rating nor the folded
want-to-try is lost (re-adds coverage for the merge safety path).
Relates to #461.
* docs(adr): tighten 0006 amendment banner
Address review (PR #463): the banner pointed at the already-corrected
"Decided" list and lumped wouldRecommend into the change. Point it at the
sections that remain superseded (Decision bullet 4, Consequences, Open
Question 1) and clarify the amendment covers rating/notes only —
wouldRecommend stays a reserved per-pour field.
Relates to #461.
* refactor(my-festival): drop dead empty-prune branch in _writeV1Blob
The private migration helper is only ever called with a non-empty merged
state, so the empty-prune guard was unreachable. Removing it simplifies the
helper and closes the last patch-coverage gap.
Relates to #461.
---------
Co-authored-by: Claude <noreply@anthropic.com>
0 commit comments