Skip to content

errors: right-size seam error types and wire From conversions #50

Description

@mfw78

Sibling of #51. #51 gives stringly/anyhow surfaces typed error enums. This issue is the complement: the typed enums already exist across L1/L2/L3, but their cross-seam conversions are not wired, so callers bridge by hand with .map_err(...). Right-size each seam's return type and add the missing From impls so ? just works. Rooted on origin/dev/m1 (f9022398), pre-cleave flat monorepo. Note: videre-sdk is still nexum-venue-sdk on dev/m1 until nullislabs/shepherd#454 merges.

Problem

Seam methods return one error type while their callers need another, and no From edge is wired, so every call site hand-writes a bridge. The user's canonical case is submit(): VenueClient::submit returns VenueError, IntentClient::submit returns ClientError, but ClientError::Venue(VenueError) has no conversion, so five IntentClient/Quoted methods carry .map_err(ClientError::Venue).

// crates/nexum-venue-sdk/src/client.rs:78 today
pub fn submit<B: IntentBody>(&self, body: &B) -> Result<SubmitOutcome, ClientError> {
    let bytes = body.to_bytes()?;                 // BodyError -> ClientError via existing #[from]
    self.venues.submit(&self.venue, bytes).map_err(ClientError::Venue)   // <- circus
}
// after: add `impl From<VenueError> for ClientError`, then
pub fn submit<B: IntentBody>(&self, body: &B) -> Result<SubmitOutcome, ClientError> {
    let bytes = body.to_bytes()?;
    Ok(self.venues.submit(&self.venue, bytes)?)
}

The typed errors are correct; only the From edges are missing. The reference wiring already in-tree is nexum-sdk ChainError (host.rs:123-171): two #[from] in, one From<ChainError> for Fault out, so strategies aggregate chain calls with bare ?.

Error topology today

Layer Error type File State
L1 Fault (wire, 7-case) nexum-runtime/src/bindings.rs ratified ABI edge, per-cdylib
L1 host::Fault (SDK-neutral) nexum-sdk/src/host.rs:33 convergence type; missing From<FetchError/ConfigError/AddressParse>
L1 ChainError nexum-sdk/src/host.rs:123 fully wired, the reference idiom
L1 FetchError / ConfigError / AddressParse nexum-sdk/src/{http,config,address}.rs no From<_> for Fault; hand-folded per module
L2 VenueError (bindgen, no Display) nexum-venue-sdk/src/bindings.rs inbound Froms wired; outbound into ClientError missing
L2 ClientError nexum-venue-sdk/src/client.rs:129 Body(#[from]) ok; Venue(VenueError) has no From
L2 ActorFault -> VenueError (videre-host) videre-host/src/registry.rs folded by venue_fault helper, 5 sites
L3 CowApiError (host) shepherd-cow-host/src/cow_orderbook.rs:186 #[from] on Network/Decode already, but still hand-wrapped

Seams that mismatch and hand-bridge today:

  • IntentClient::{quote,submit,status,cancel} + Quoted::submit return ClientError, inner VenueClient::* return VenueError, no From. (client.rs:65, 78, 85, 92, 118)
  • VenueInvoker::{derive_header,quote,submit,status,cancel} return VenueError, inner actor.call returns ActorFault, folded by venue_fault. (registry.rs:201, 211, 224, 234, 244)
  • cow_orderbook.rs:148, 150, 171 re-wrap errors that already have #[from].
  • L1 example modules on_block export and the config_err/address shims.

Proposal

Right-size nothing that is already correct (every seam return type stays); add the missing From edges and delete the hand bridges.

1. From<VenueError> for ClientError (headline)

VenueError is a wit-bindgen enum with no Display and no std::error::Error impl, so #[from] will not compile (thiserror #[from] implies #[source], which requires Error). Keep the variant as-is and add a hand-written From:

// crates/nexum-venue-sdk/src/client.rs
#[error("venue error: {0:?}")]
Venue(VenueError),              // unchanged: no #[from], no #[source]

impl From<VenueError> for ClientError {
    fn from(e: VenueError) -> Self { ClientError::Venue(e) }
}

Deletes all 5 .map_err(ClientError::Venue). cow-venue::CowClient (returns ClientError verbatim) inherits the fix.

2. From<ActorFault> for VenueError in videre-host

videre-host owns its own bindgen VenueError (distinct from nexum_venue_sdk::VenueError), so this From<Foreign> for Local is orphan-legal here. venue_fault is a total uniform fold (VenueError::Unavailable(format!("adapter {fault}"))), so a From with the same body lets self.actor.call(..).await? fold at all 5 seams; delete the helper.

3. cow_orderbook.rs redundant wraps -> bare ?

Network(#[from] reqwest::Error) (:196) and Decode(#[from] serde_json::Error) (:198) already exist. Replace .map_err(CowApiError::Network) (:148, :150) and .map_err(CowApiError::Decode) (:171) with ?. Keep :132 BadPath(format!("{path:?}: {e}")) (adds path context).

4. Module on_block edge -> ?; Ok(())

modules/examples/{balance-tracker:61, http-probe:70, price-alert:78}/src/lib.rs end with strategy::on_block(...).map_err(Into::into). The SDK host::Fault -> wire Fault fold is a structural 7-case enum fold, From-backed, and ? applies it. stop-loss/src/lib.rs:68 already proves the shape:

strategy::on_block(&WitBindgenHost, block.chain_id, cfg)?;
Ok(())

Cosmetic, lowest priority; the conversion stays, only the explicit map_err goes.

5. builder.rs:55 + supervisor .map_err(Error::from)?

builder.rs:55 -> Ok(ProviderPool::from_config(ctx.config).await?) (anyhow blanket From). Supervisor .map_err(Error::from)? sites are dead (? already applies From); delete. These are the anyhow surface and overlap #51's remit.

6. Optional: From<{FetchError,ConfigError,AddressParse}> for host::Fault in nexum-sdk

Kills the duplicated config_err (two modules) and the invalid_input(e.to_string()) address shim. All three are L1-internal, orphan-legal, no new deps. Keep the context-injecting closures (fetch_err(url, &e) adds the URL; stop-loss/strategy.rs:190 adds a bespoke message) as closures.

Legitimate map_err to keep

Do not "fix" these. The conversion must exist; the spelling is correct.

  • WIT/ABI lowering: shepherd-cow-host/src/ext_cow.rs:218, 237 (cow_error_to_wit, a real lossy 7-to-3 projection), the generated mirror glue in nexum-sdk/src/wit_bindgen_macro.rs and shepherd-sdk/src/wit_bindgen_macro.rs. Per-cdylib bindgen types forbid a shared From. This is the ratified Fault-boundary pattern.
  • Semantic classifiers: host/error.rs From<ProviderError> for ChainError (JSON-RPC code -> variant) and From<StorageError> for Fault; faults.rs:61 From<host::Fault> for VenueError (caller-shaped faults -> retryable Unavailable, deliberately lossy, hand From not #[from]).
  • Context-adds: cow_orderbook.rs:132 BadPath, http-probe/strategy.rs:46 fetch_err (URL injection).
  • #[source]-with-context leaf construction: provider_pool.rs (chain/url fields), local_store_redb.rs (per-op tag), BuildError::{Chain,Store,Ext,Logs} at builder.rs:178-181 (four slots, one anyhow::Error source, #[from] cannot disambiguate), actor.rs:101-102 (Trap carries a mark_dead() side effect), classification_data.rs:69 (stringify to keep Clone+Eq).
  • Semantic dispatch: run.rs:138 match on CowApiError, registry.rs:432 charge-then-rethrow.

Cross-repo coordination

The orphan rule is per-crate, so no proposed From became illegal from the three-repo cleave. With the cleave done, the coordination shape is:

  • The cleanup deletes helpers (venue_fault, config_err, the .map_err closures) and rewrites call sites spanning L1+L2+L3. The L1 conversions and deletions land in this repo first; the L2 and L3 call-site rewrites follow as separate downstream PRs merged in dependency order (L1 -> L2 -> L3) with matching rev-pin bumps between each.
  • transport.rs folds (optional) convert L2-local sources into L1 targets, coupling L2 to the exact L1 error shape. With no single-workspace cargo check any more, the L2 PR's own build against the pinned L1 rev is what proves those Froms resolve and the deleted helpers' call sites still compile.
  • The #[from]-won't-compile-on-VenueError fact and the two-VenueError distinction (guest SDK vs videre-host bindgen) still hold and should be regression-guarded per repo.

Timing

The three-repo cleave is done. The mechanism (right-sized seam types and the L1-side From impls) lands in this repo; the videre-host and module call-site conversions follow as separate downstream PRs with rev-pin bumps in dependency order (L1 -> L2 -> L3).

Acceptance criteria

  • From<VenueError> for ClientError added (hand-written, not #[from]); the 5 .map_err(ClientError::Venue) sites use bare ?.
  • From<ActorFault> for VenueError added in videre-host; venue_fault deleted, 5 seams use ?.
  • cow_orderbook.rs:148/150/171 use bare ? over the existing #[from].
  • No removable .map_err(Into::into) remains at the module on_block edge or builder.rs:55.
  • ? works across the L1/L2/L3 seams with no hand bridge for a conversion a From can express.
  • Every remaining map_err is either an ABI-boundary lowering (WIT Fault/cow-api-error) or a documented semantic remap / context-add / #[source] leaf construction.
  • Distinct from errors: replace stringly and anyhow error paths with typed enums #51: no new typed enums introduced; only conversions wired.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:errorsTyped error/fault taxonomy, From wiring, Fault boundary conversionsdebtRefactor/cleanup: typed replacements for stringly code, dedup, right-sizing

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions