liseur-sync/docs/adr/0003-catalog-work-identity.md
Chmouel Boudjnah 9699ca3308
Some checks are pending
build / binary (push) Waiting to run
build / container (push) Waiting to run
test / test (push) Waiting to run
test / lint-openapi (push) Waiting to run
docs: write down how a fresh device starts up
The batch resolve route joins docs/integrating.md and the OpenAPI
description, and ADR-0035 records why a route for many books exists
when one for a single book already does.

Alongside it, the sequence a device follows on its first day is now
spelled out: name everything, ask where each book stands through
/v1/heads, then sync normally. Every client wants that order and none
of it was written down, which is part of why the first attempt at it
took several refreshes to settle.
2026-09-07 09:22:23 +02:00

5.4 KiB

ADR-0003: Catalog and sync work identity

  • Status: Implemented (archived; every phase is built and nothing is outstanding, so the record stands as written and is no longer a plan)
  • Date: 2026-08-12
  • Depends on: ADR-0002

Context

The existing works, editions, and aliases are user-scoped because positions and sessions are private. The new catalog can be shared across users. Resolving a shared file directly to one work would either leak one user's work graph or force works to become shared, contradicting the current tenant-isolation model.

A second, unrelated identity system would be equally harmful: catalog downloads and sync records would drift apart.

Decision

Catalog identity and sync identity remain separate layers joined explicitly:

libraries -> books -> book_files -> blobs
                    |
                    +-> user_book_works(user_id, book_id, work_id)
                                      |
                                      +-> works -> editions -> aliases

user_book_works has composite foreign keys that prove the work belongs to the same user as the mapping. Store methods always require user_id.

Resolution, work/edition creation, alias promotion, and mapping insertion are one store transaction. Phase 1 refactors the existing resolve endpoint onto this transaction as well, so catalog and client resolution cannot diverge. Alias uniqueness races are re-read inside the transaction: aliases resolving to another work return 409, never conflict-ignore success or a 500.

Catalog browse and download are non-mutating and require only library-read. They never create a work or fail because of a sync-identity conflict.

Before a client syncs or reports sessions for a catalog book, it performs an explicit resolution authorized by both library-read and sync:

  1. The server collects the book file's identifiers: SHA-256, KOReader partial MD5, EPUB dc:identifier, normalized title/author, and the stable catalog alias liseur-sync:<book_id>.
  2. It invokes the existing resolution algorithm in that user's namespace. All matching aliases must resolve to one work; matches spanning multiple works return 409 without mutation.
  3. It inserts the user_book_works mapping transactionally after a high-confidence or confirmed resolution.
  4. Later requests reuse the mapping.

A title/author-only (ta:) match remains low-confidence. The resolver does not promote stronger supplied aliases or create the catalog mapping until the user confirms it. A rejected match remains recorded as rejected, matching the existing client/server repair semantics.

The same shared catalog book may therefore map to distinct work IDs for two users. That is correct and preserves existing privacy. Physical blob deduplication is not semantic work sharing.

If the catalog metadata or file changes, the mapping is not silently rewritten. A changed file becomes another edition or invokes the existing merge/split repair flow only after strong embedded identity or explicit confirmation. A different publication found at the same watched path gets a new book_id and no inherited mapping. Manual split and merge operations update or invalidate affected mappings explicitly.

Existing works require no eager migration. The first catalog resolution finds matching aliases and backfills the join. A maintenance command may pre-resolve mappings for one user, but it must use the same public resolver.

Consequences

Catalog browsing and download need no work ID. Work creation is lazy, so importing a large shared library does not create per-user copies of every work and OPDS downloads cannot mutate sync state.

Some API responses will expose both book_id and the current user's optional work_id. Clients must not persist another user's work ID as catalog identity.

Implementation phases

  1. Add the mapping table, composite constraints, atomic resolver transaction, and tenant-isolation/concurrency tests. Implemented.
  2. Add an explicit catalog-book resolve operation for sync-capable clients. Implemented as POST /v1/books/{id}/resolve, which requires both library-read and sync. ADR-0035 adds POST /v1/books/resolve for a whole shelf at once; it goes through the same resolver and decides nothing differently.
  3. Add maintenance backfill and mapping repair for split/merge. Implemented. Split and merge repair cover unavailable current files, and liseur-sync admin backfill-works <user> pre-resolves a user's whole catalog through the same resolver the API uses. It never confirms a title/author match: the operator running it is not the reader who can say whether two similarly named books are the same one, so those are counted and left for a client to confirm.

Acceptance criteria

  • Two users resolving one shared book receive user-owned work IDs.
  • Browse, GET, and HEAD downloads do not mutate works or mappings.
  • No user can read, create, or replace another user's mapping.
  • Existing aliases are reused rather than duplicated into a parallel graph.
  • A fuzzy match cannot acquire stronger aliases or a catalog mapping before confirmation.
  • Conflicting aliases return 409 and leave aliases and mappings unchanged.
  • Concurrent resolutions cannot split aliases across works; uniqueness races become deterministic success-on-one-work or 409.
  • Split and merge operations leave mappings valid and auditable.