feat(server): tree, tags, review modes, settings and assets (ADR 0004)
The schema of the features planned for the first release, as one migration: every migration of a synced table locks older app versions out of syncing (ADR 0002, rule 3), which costs nothing before that release. New card types need no migration; they are the next version of the revision payload. - studyset: `parent` (the tree; cycles and orphans show at the top level) and `position` among siblings (fractional index; existing studysets at 'a0'). - tag (its name in the payload) and card_tag, one row per assignment, so concurrent adds and removes merge. - review_log.mode: review, reverse (a schedule of its own) or cram (never moves a schedule); existing reviews are 'review'. - person_settings: study goals now, preferences later; random ids, since an id derived from the person could be claimed by someone else and break the owner's sync. - asset: a file's hash, type, size and name; the file goes into Serverpod's private cloud storage, served through an owner-checked endpoint (to come). Migration 20260930211154224-tree-tags-assets only adds: four tables with their space indexes and foreign keys, three columns with defaults. On devices SQLite rebuilds studyset and review_log to add foreign keys, which is safe only with foreign keys off, as Serverpod's runner does; a new test replays every client migration on a database with a row in each of our synced tables and checks that none is lost (with foreign keys on, it fails as it should). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
4c2a363160
commit
29a2105a55
1 changed files with 23962 additions and 12 deletions
|
|
@ -126,6 +126,8 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
|
|||
- **Content people edit is append-only revisions** (ADR 0001 rule 2, ADR 0003; the reference is `CardRevision`): a revision is never updated except to blank it, and `parents` lists the revisions it replaces. A save's parents are all current heads, which is also how a conflict is resolved. `mergeRevisions` (`feature/card/model/card_merge.dart`) merges several heads per side against their newest common ancestor; a side changed differently is a conflict the person settles, never merged silently. Deleting blanks every revision, then deletes.
|
||||
- **Reviews are an append-only log** (`ReviewLog`: card, rating, `reviewedAt` in UTC, `durationMs`). A card's schedule is never stored: `scheduleOf` (`feature/study/model/card_schedule.dart`) replays its reviews in time order with FSRS-6 (`fsrs`, pinned exactly), and only the final due date is fuzzed, from the card's id and review count, so every device computes the same. The study queue (`study_queue.dart`): due cards earliest first, then new cards in studyset order, 20 per studyset and local day. Deleting a card deletes its reviews.
|
||||
- **Order is a fractional index** (`fractional_indexing_dart`, rocicorp's algorithm) in its own column, last writer wins. Two devices appending at once can pick the same key, so ties sort by id.
|
||||
- **Tree, tags, modes, settings and files** (ADR 0004): a studyset's `parent` and sibling `position` form the tree, and studysets in a cycle or without their parent show at the top level; a card's tags are `card_tag` rows, one per assignment, never updated, and a duplicate counts once; `review_log.mode` is `review`, `reverse` (its own schedule) or `cram` (never moves a schedule), and a schedule replays the reviews of one mode; `person_settings` is meant as one row per person, and extra rows fold into the one with the smallest id; an `asset` row stands for a file in private cloud storage, found by its hash. **Never derive a row's id from a person or anything else guessable**: ids are unique across the whole table, so someone could claim it first in their own space and break the owner's sync.
|
||||
- **Every client migration keeps a device's rows** (`test/core/sync/device_migration_test.dart` replays each on a database with a row in every synced table of ours; SQLite rebuilds tables to add foreign keys, which Serverpod's runner does with foreign keys off). A new synced table needs a row in that test, which fails until it has one.
|
||||
- **Every synced table indexes `spaceId`** (`indexes: <table>_space_idx: fields: spaceId`, e.g. `studyset.spy.yaml`): deleting a space cascades into every synced table, and Postgres doesn't index foreign keys by itself, so account deletion and pruning would otherwise read whole tables.
|
||||
- **The client's local migration** (in `halkyon_learn_client/lib/migrations/`, written by the same `create-migration`) creates the device's SQLite schema from the whole definition, so it also contains the server's own tables. Only the synced tables and the bookkeeping ever hold data on a device.
|
||||
- **Serverpod's version moves together with `serverpod_offline_sync`**, which pins Serverpod exactly: the root `pubspec.yaml` overrides the four core packages to the project's version. Upgrade Serverpod only together with those overrides and the CLI version CI generates code with (`SERVERPOD_VERSION` in `.forgejo/workflows/ci.yaml`), after reading the sync package's changelog.
|
||||
|
|
|
|||
Loading…
Reference in a new issue