feat(server): the review log, synced and append-only

ReviewLog keeps every rating of a card: the card, the rating (FSRS's 1 again to 4 easy), when (UTC) and how long answering took, as decided. It is never changed; each device derives a card's schedule by replaying its reviews, so reviews from different devices never conflict. No device id: the sync engine knows each row's device.

The migration (by CLI) only adds: the table, an index on spaceId for space deletion, one for a card's reviews in time order, and the foreign keys (from the space with ON DELETE CASCADE; from the card with ON DELETE CASCADE, DEFERRABLE INITIALLY DEFERRED).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-09-30 16:59:07 +02:00
commit 5334a0baa5
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -0,0 +1,25 @@
### One rating of a card, never changed once written: a card's schedule isn't
### stored but derived on each device by replaying its reviews in time order,
### so reviews from different devices never conflict (decided 2026-09-30).
### Synced; the review goes with its card.
class: ReviewLog
table: review_log
database: sync
fields:
### The card reviewed.
card: Card?, relation(onDelete=Cascade, deferred)
### How well it was recalled, FSRS's ratings: 1 again, 2 hard, 3 good,
### 4 easy.
rating: int
### When, in UTC.
reviewedAt: DateTime
### How long answering took, from showing the card to rating it, in
### milliseconds.
durationMs: int
indexes:
# Deleting a space finds its rows through this index (see studyset.spy.yaml).
review_log_space_idx:
fields: spaceId
# A card's reviews, in time order.
review_log_card_idx:
fields: cardId, reviewedAt