Server P6: durable books, snapshot ingest and portfolio alias

Merged
SU
11d ago
+2787-718

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::Book and MasterDefinition::Book reuse 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 requires Idempotency-Key.
    • Without If-Match it creates the book; with If-Match it 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_REJECTED with {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.
  • Reads. GET /api/v1/books is paged with a limit of at most 500. /books/{id} returns ETag and x-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.
    • InMemoryPortfolioStore and the create_router_with_portfolio_store and create_router_with_access_and_portfolios constructors 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/analytics also accepts {book_id, version?}; inline requests work as before.
    • The request is refused as a whole with 422 BOOK_RESULTS_UNAVAILABLE when 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 into BondQuoteOutput yet. 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.

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 commit with nothing persisted, and that the page after a cursor is exactly the next book.
  • application_path::book_snapshot_ingest_resolves_external_identifiers is no longer ignored. It also covers an ISIN-keyed replace under If-Match and 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-file passes 376 tests with 0 failures.
  • WSL: clippy with -D warnings on those crates, fmt, and tools/assurance/check.py all 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