docs: record user-layer decisions, core module map and known gaps
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7154ca0a61
commit
00fc0180b0
@@ -4,6 +4,48 @@ Append-only. Never edit or delete an entry. Supersede it with a new one that lin
|
||||
One entry per decision a future maintainer would otherwise have to reverse-engineer.
|
||||
No index: `grep -n '^## ' docs/DECISIONS.md` lists every entry (date, title, line number).
|
||||
|
||||
## 2026-10-30: User layer: per-user moves merge entries, keep aliases, and roll the library up by hierarchy
|
||||
**Status:** accepted
|
||||
**Context:** Task 6 built the user layer (`packages/core/src/{entries,engagements,history,lists,copies}.ts`).
|
||||
Several behaviours are not obvious from the spec and would otherwise have to be reverse-engineered.
|
||||
**Decision:**
|
||||
- **Merge on move.** `moveEntryToWork` onto a work where the user already has an entry merges the two:
|
||||
- Live target: the target's values win. Empty target fields are filled from the moved entry, carrying
|
||||
the moved entry's `field_sources`. Tags are unioned and custom fields and overrides merged; their
|
||||
`field_sources` is `"user"` if either side's was, otherwise the target's.
|
||||
- Differing non-empty `review`/`private_notes` are concatenated (target, `\n\n---\n\n`, moved).
|
||||
- A conflicting rating keeps the target's; the moved rating is recorded in the history row's `before`.
|
||||
- Engagements are re-parented, each with an `entryId` history row (restorable), and the moved entry is
|
||||
soft-deleted.
|
||||
- Soft-deleted target: revived with the moved entry's values and `field_sources` as-is.
|
||||
- **Alias bookkeeping.** A move writes `work_aliases (user, old → target)`, re-points aliases that led to
|
||||
the old work, drops any alias leaving the target (no cycles), tombstones the user's own private draft,
|
||||
and re-points this user's copies (with history) and list items. Writes to an aliased or merged work
|
||||
(`upsertEntry`, copies, list items) resolve to the target. **Undoing a move** via `restoreVersion` deletes
|
||||
the forward alias and writes no reverse alias, so later syncs onto the target land on the target.
|
||||
- **Library cursor.** `listEntries` cursors are base64 JSON `{ sort, sortValue, id }`. The sort is carried
|
||||
so a cursor cannot be replayed under another sort. For the title sort, `sortValue` is the SQL
|
||||
`lower(w.title)` of the last row, never a JS lowercase, so SQL and cursor agree on non-ASCII titles.
|
||||
- **Custom fields patches replace the whole map** (`customFields` in an entry, engagement or copy patch).
|
||||
This makes restore exact. `field_sources` tracks the key `customFields` as a whole. Restore does not
|
||||
re-check values against today's definitions, because it is the user's own past data.
|
||||
- **List items are hard-deleted.** `list_items` has no `deleted_at`. The list's `user_history` snapshot
|
||||
holds the ordered items, so `restoreVersion` brings removed items back.
|
||||
- **Hierarchy visibility.** `childSummary` counts the user's live entries up to three levels below the
|
||||
entry's work (child, grandchild, great-grandchild). The default library view (`parentId: null`) lists
|
||||
entries with no ancestor, up to three levels, on which the user has a live entry. An episode whose show
|
||||
is untracked therefore stays visible, and is rolled up once the show gets an entry.
|
||||
- **Concurrent creates.** `upsertEntry` inserts with `ON CONFLICT (user_id, work_id) DO NOTHING`, then
|
||||
patches through the update path, so a concurrent creator never aborts the caller's transaction.
|
||||
|
||||
**Alternatives:**
|
||||
- Refusing a move onto an existing entry pushes the merge onto the user.
|
||||
- Recursive CTEs for hierarchy are portable but heavier; three levels cover show → season → episode.
|
||||
- A JS-lowercased cursor breaks on SQLite, whose `lower()` is ASCII-only.
|
||||
|
||||
**Consequences:** Undoing a move does not move copies or list items back, and hierarchy deeper than three
|
||||
levels is not rolled up (both in `docs/TODO.md`).
|
||||
|
||||
## 2026-10-30: A single Kysely dialect over Bun.SQL for SQLite and Postgres
|
||||
**Status:** accepted
|
||||
**Context:** Kysely ships no driver for Bun. `Bun.SQL` speaks both `sqlite://` and `postgres://` behind one API.
|
||||
|
||||
Reference in New Issue
Block a user