Server P1: one curve owner, price-by-ID with configured curves, ApiError
Follows #135 (P0), now merged; rebased onto main.
What changes
Curves now have one owner, the durable recipe CurveStore (redb). Before, there were three: the in-memory engine CurveBuilder, the recipe store, and curves embedded in runtime plans.
-
Store.
CurveStorekeeps immutable, versionedSnapshotCurveReciperecords. New methods:recipe_at(id, known_at)returns the highest revision recorded by the cutoff. It scans at most 10,000 versions.recipe_heads(after, limit ≤ 256)lists each curve's latest revision.
CurveConfig,CurveSnapshotand their eight unused methods are deleted. -
Engine. The
CurveBuilderDashMap cache andPricingEngine::curve_builder()are deleted;BuiltCurvestays. Newcurve_resolution::resolve_curves:- loads each root curve at
known_at, then each dependency whose stored native key is exactly the one the recipe names (the revision with that number when the label is decimal, otherwise the revision current at the cutoff; nothing else is searched); - applies the same freshness rule as core snapshot assembly;
- builds the result with
rebuild_curve_recipes.
PricingEngine::pricing_curvespicks the pricing configuration that applies to the bond (select_pricing_config: active,applies_tomatches, highest priority, ties refused). It then checks each curve's role and currency with the same check snapshot valuation uses. - loads each root curve at
-
Curve routes.
Route Behaviour POST /api/v1/curvesStores a recipe. Same handler as /api/v1/analytics/curves, which now works without the Unix-only core workflow. Returns 204, 409 on changed content, 400 on an invalid record.GET /api/v1/curvesLists curve heads, bounded and paged. GET /api/v1/curves/{id}?known_at=Returns the revision current at the cutoff and sets x-convex-curve-version.…/zero,…/discount,…/forwardBuild that revision and query it. A missing or stale curve gives 422 MISSING_INPUT.DELETE /api/v1/curves/{id}Removed: revisions are history. POST /api/v1/curves/bootstrapStateless: returns points and residuals and stores nothing. Rejects unknown or incomplete instruments, which were previously skipped silently, and non-finite or non-positive tenors and reversed FRAs. At most 128 instruments, the calibrators' bound. Now a Calculateaction in the access table; itscurve_idis an echoed label, not a scoped curve. -
Price by ID.
GET /api/v1/quotes/{id}?market_price=&known_at=:- reads the reference at
known_at; - uses the configured discount and swap-benchmark curves current at
known_at; - reports what it used in
x-convex-reference-version,x-convex-pricing-config,x-convex-curve-versionsandx-convex-known-at; - refuses with 422
MISSING_INPUTinstead of pricing without curves, and whenmarket_priceis absent; - broadcasts to WebSocket subscribers only when
known_atis omitted, so a historical quote is never sent as live.
- reads the reference at
-
Errors.
ApiError(error.rs) is the single body shape,{error, code, retryable}, for curve and quote handlers.reference_errornow delegates to it. Store scan bounds and missing adapter support on reads are 503, not client errors; parse and serialization failures are 500 and not retryable. -
CORS. The new provenance headers are exposed to browser clients.
-
Pricing configuration specificity now also counts
is_floating,is_inflation_linkedandtext_search, so a configuration using them outranks a generic fallback instead of tying with it. -
Showcase.
- Caddy allows
POST /api/v1/curves/bootstrapagain, so the curve calibration page works.POST /api/v1/curvesstays private. - The seed adds a
SHOWCASE-USDpricing configuration: USD bonds useD.OISas the discount curve andP.SOFRas the swap benchmark. convex-core-inputsaccepts optionalpricing_configs.- The seed is dated 2026-10-07, so the showcase prices with
known_at=2026-10-07T12:00:00Z. Nothing is re-stamped. At the current time the same request is refused as stale.
- Caddy allows
-
Demo frontend. The calibration page no longer sends Bond rows, which the server now rejects.
Known limits
- The pricing configuration is read as current; only its version is reported. A historical
known_atis paired with today's configuration. - Pricing configurations still have no HTTP write route; they are loaded by the importer. Management belongs with P8.
- The government and FRN-reference curve roles are not used by price-by-ID, which matches snapshot valuation.
- REST-triggered
bond_quoteWebSocket messages remain for live requests. They are retired in P3. - Includes the flaky-test fix from #135's review (
cancelled_waiter_retains_both_admissions_until_blocking_work_finisheswaits for the permit as well as the admission).
Test plan
- WSL:
cargo test --no-fail-fast --all-features -p convex-server -p convex-ports -p convex-ext-redb -p convex-engine. All pass exceptconvex-ext-redb --test recovery, which fails the same way locally on the unchanged #135 head. This PR changes that test's fixture, so CI is its first real run. - WSL:
cargo clippy --all-features --all-targetson the four crates with-D warnings,cargo fmt --all -- --check, andtools/assurance/check.pyplus--base mainall pass. - Windows:
cargo test -p convex-server --test application_pathpasses, andapi_integration_testspasses 163 of 164.production_readiness_environment_only_binary_startupfails on Windows only: that test clears the process environment, and the binary starts normally outside it. The other Windows failures need the Unix-only snapshot repository and also fail without this change. -
tests/application_path.rs:- imports the seed the way the container entrypoint does;
- retries the reference and curve writes over HTTP, which are idempotent;
- prices at the capture time with the expected version headers;
- gets 422 at the current time;
- lists curves and queries one;
- restarts and finds the same versions;
- gets 409 for a changed recipe.
- New
api_integration_tests:- cutoff reads and paging;
- immutability and validation;
- the discount factor matches
exp(-z·t); - stale and missing curves are refused;
- bootstrap stores nothing and rejects bad rows.
- Showcase simulation under dash with Linux Caddy 2.8.4:
- bootstrap returns 200 and
POST /api/v1/curvesreturns 403; - a quote at the capture time returns 200 with
D.OIS@1,P.SOFR@1andSHOWCASE-USD@1; - a quote at the current time returns
MISSING_INPUT; - the WebSocket upgrade returns 101;
- the crash and SIGTERM supervision behaviour is unchanged.
- bootstrap returns 200 and
- Review follow-ups:
curve_recipe_replay::stored_dependencies_resolve_by_native_key_label: a dependency labelledsupplied.recipe.v7resolves from the store, and an unknown label isMissing;access_control_tests::bootstrap_curve_label_is_not_a_scoped_curve: bootstrap with an ungranted label succeeds, and reading that curve stays forbidden;- WSL:
cargo teston convex-server, convex-ports and convex-engine has 0 failures; clippy-D warningsand fmt are clean.
-
demo/frontend:npx tsc --noEmitpasses.
🤖 Generated with Claude Code