docs: record user-layer decisions, core module map and known gaps
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7154ca0a61
commit
00fc0180b0
@@ -16,7 +16,7 @@ user layer or files a Changes-inbox event.
|
||||
|---|---|---|
|
||||
| `apps/server` | `Bun.serve`: `/api/v1`, `/ingest/<plugin>`, job worker, static web | planned |
|
||||
| `apps/web` | React app (HTML imports) | planned |
|
||||
| `packages/core` | domain services: entries, matching, change engine, history, export | planned |
|
||||
| `packages/core` | domain services, one module per area (import by path, no barrel): `auth`, `crypto`, `works`, `matching`, `entries` (entries, library listing, quick add, per-user moves, match suggestions), `engagements`, `history` (`user_history`, restore), `lists`, `copies`, `custom-fields`, `attachments`. Change engine and export planned | exists (partial) |
|
||||
| `packages/db` | Kysely over one Bun.SQL dialect (SQLite + Postgres), ids, JSON helpers, migrations, `forEachDialect` test harness | exists (schema pending) |
|
||||
| `packages/plugin-sdk` | plugin contract, `SourceRecord`, http/rate-limit helpers, fixture test harness | planned |
|
||||
| `plugins/<id>` | one source integration each (hardcover, trakt, mangabaka, listenbrainz, lastfm, csv) | planned |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -27,4 +27,9 @@ Small, specific, independently actionable. Remove only when actually done.
|
||||
- [ ] Admin merge does not merge colliding children recursively; colliding children stay under the tombstoned parent.
|
||||
- [ ] Matching: a shared parent resolved without external IDs or position (title/year reuse in
|
||||
`packages/core/src/matching.ts`) has no unique key, so concurrent matches can create it twice.
|
||||
- [ ] Library search (`q`) and the title sort use the work's title, not the user's `overrides.title`
|
||||
(overrides are a JSON text column). The `tag` filter matches the JSON text with LIKE.
|
||||
- [ ] Library hierarchy rollup and orphan detection look three levels deep; deeper trees are not rolled up.
|
||||
- [ ] Undoing a per-user move does not move copies or list items back, nor restore aliases that the move
|
||||
re-pointed.
|
||||
- [ ] `bun run typecheck` fails with TS18003 until source files exist under `apps/`, `packages/` or `plugins/`.
|
||||
|
||||
Reference in New Issue
Block a user