Server P6: durable books, snapshot ingest and portfolio alias
Merged
SU
11d ago+2787+2787+0123456789012345678901234567890123456789-718-718-012345678901234567890123456789
Follows #135 (P0), now merged; rebased onto main. Independent of the P1 curve PR (#136); whichever merges second is rebased onto the first.
What changes
Books become a durable resource in the existing ReferenceRepository, and a book management system can load a full position snapshot through the API. No new store is added.
- Store.
MasterKey::BookandMasterDefinition::Bookreuse the repository's existing versioning, history, idempotency receipts, atomic redb commits and backup.ReferenceRepository::list_books(after, limit)is new and implemented for redb and the memory adapter. - Book contents. Each book holds its owning system and its positions. Each position has an instrument (always resolved), a quantity and unit, and optionally an exact reference version.
- Size limit. At most 1,000 positions per book.
- A commit stores the book four times (current, history, command, receipt), about 3.6 MiB at worst against the 8 MiB transaction limit.
- The access scan refuses requests that scope more than 1,024 distinct instruments.
- Ingest.
PUT /api/v1/books/{id}takes a full snapshot and requiresIdempotency-Key.- Without
If-Matchit creates the book; withIf-Matchit replaces that exact version. - External identifiers resolve through the identifier mappings. Unknown or ambiguous identifiers are rejected, never guessed.
- Every row must resolve or nothing is committed; failed rows come back as 422
BOOK_ROWS_REJECTEDwith{index, code, reason}for each row. - A retry with the same key returns the original receipt; the same key with a different body returns 409. Batch portfolio creation compares every retained book with what the retry would create, so a changed or extended payload under the same key is also 409.
- Without
- Reads.
GET /api/v1/booksis paged with a limit of at most 500./books/{id}returnsETagandx-convex-reference-version. There are also/books/{id}/versions/{v}and/books/{id}/history. - Portfolios.
/api/v1/portfolios*is now a layer over books, with the same request and response shapes.- Position edits read the current version, change it and write it back; a concurrent change returns 409.
- DELETE ends the book's effective period instead of erasing it.
InMemoryPortfolioStoreand thecreate_router_with_portfolio_storeandcreate_router_with_access_and_portfoliosconstructors are removed.- Writes through the portfolio routes now require
Idempotency-Key. - A duplicate instrument in one portfolio is now a 400 instead of being appended twice.
- Analytics by book.
POST /api/v1/portfolio/analyticsalso accepts{book_id, version?}; inline requests work as before.- The request is refused as a whole with 422
BOOK_RESULTS_UNAVAILABLEwhen any position lacks a complete, valid runtime result. It never returns a partial portfolio. - When every result is present it returns 501
RUNTIME_RESULT_ADAPTER_UNAVAILABLE, because nothing converts canonical runtime results intoBondQuoteOutputyet. That converter is planned with P2/P7, when the runtime prices books. The placeholder is deliberate. - The other portfolio analytics endpoints still take inline positions only.
- The request is refused as a whole with 422
Test plan
-
crates/convex-server/tests/books.rs: 8 tests against the real composition over HTTP, covering:- create and versioned replace, and idempotent retries;
- all-or-nothing rows (ambiguous identifiers, pinned versions, the position limit);
- paging, history and restart;
- the portfolio layer's edit path;
- the by-book analytics refusal.
-
api_integration_tests::test_batch_create_portfolios_replays_only_the_same_payload: an exact retry replays; a renamed or extended payload under the same key is 409. - Repository conformance checks that duplicate-position and over-capacity books are refused through
commitwith nothing persisted, and that the page after a cursor is exactly the next book. -
application_path::book_snapshot_ingest_resolves_external_identifiersis no longer ignored. It also covers an ISIN-keyed replace underIf-Matchand rejection of an unknown ISIN. - The repository conformance test for books runs on both the memory and redb adapters.
- WSL:
cargo test --no-fail-fast --all-features -p convex-server -p convex-ports -p convex-ext-redb -p convex-ext-filepasses 376 tests with 0 failures. - WSL: clippy with
-D warningson those crates, fmt, andtools/assurance/check.pyall pass. - Combined with P1: P1 applies cleanly and fmt and clippy pass on the combined tree. The combined test run was stopped because the machine was low on memory and has not been repeated.
🤖 Generated with Claude Code