Skip to content

Latest commit

 

History

History
97 lines (67 loc) · 4.36 KB

File metadata and controls

97 lines (67 loc) · 4.36 KB

ADR-086: Neural Trader WASM Bindings

Status

Accepted

Date

2026-03-06

Deciders

ruv

Related

  • ADR-085 Neural Trader architecture (core, coherence, replay crates)
  • ADR-084 ruvllm-wasm publish & Rust 1.91 WASM codegen workaround
  • ADR-040 WASM programmable sensing
  • ADR-041 curated module registry

Context

The three Neural Trader Rust crates (neural-trader-core, neural-trader-coherence, neural-trader-replay) provide a coherence-gated market graph system with 12 passing tests. However, they have no browser/WASM bindings — meaning dashboards, backtesting UIs, and browser-based research tools cannot access the coherence gate or replay memory directly.

The repository has an established WASM crate pattern used by 15+ crates (ruvector-wasm, ruvector-gnn-wasm, ruvector-attention-wasm, ruvllm-wasm, etc.) all using wasm-bindgen + serde-wasm-bindgen + wasm-pack. We follow that pattern here.

Rust 1.91 WASM Codegen Bug

Rust 1.91 has a known WASM codegen bug in release mode. The workaround (same as ruvllm-wasm, documented in ADR-084) is:

CARGO_PROFILE_RELEASE_CODEGEN_UNITS=256 CARGO_PROFILE_RELEASE_LTO=off \
  wasm-pack build --target web --scope ruvector --release

Additionally, wasm-opt is disabled via [package.metadata.wasm-pack.profile.release] wasm-opt = false.

Decision

Create crates/neural-trader-wasm/ with a single src/lib.rs that wraps all three crates using the XxxWasm { inner: Xxx } pattern.

Type Mapping Strategy

Rust Type WASM Approach
C-style enums (EventType, Side, RegimeLabel, etc.) Mirror as #[wasm_bindgen] enums with From conversions
[u8; 16] fields (event IDs, hashes) Hex strings in JS (32-char), decoded at boundary
Simple structs (GateConfig) Direct field getters/setters via #[wasm_bindgen(getter/setter)]
Complex nested structs (MarketEvent, ReplaySegment) toJson()/fromJson() via serde-wasm-bindgen
Vec<T> and VecDeque<T> Serialized to JsValue via serde-wasm-bindgen
anyhow::Result Converted to Result<T, JsValue> at WASM boundary
Trait objects (CoherenceGate) Concrete ThresholdGateWasm wrapper (no dyn dispatch in WASM)

Exported Types

Source Crate WASM Type Key Methods
core MarketEventWasm new(), field getters/setters, toJson(), fromJson()
core GraphDeltaWasm new(), nodesAdded(), edgesAdded(), propertiesUpdated()
core EventTypeWasm, SideWasm, NodeKindWasm, EdgeKindWasm C-style enums
coherence GateConfigWasm new() with defaults, all threshold getters/setters
coherence ThresholdGateWasm new(config), evaluate(ctx)
coherence GateContextWasm new(...), field getters
coherence CoherenceDecisionWasm allowRetrieve, allowWrite, allowLearn, allowAct, reasons(), toJson()
coherence RegimeLabelWasm C-style enum
replay ReservoirStoreWasm new(maxSize), len(), isEmpty(), maybeWrite(segJson, decisionJson)
replay ReplaySegmentWasm toJson(), fromJson(), field getters
replay SegmentKindWasm C-style enum
version() Crate version string
healthCheck() Returns true

Build & Package

  • Built with wasm-pack build --target web --scope ruvector --release
  • Published to npm as @ruvector/neural-trader-wasm
  • publish = false in Cargo.toml (Rust crate not on crates.io)

Consequences

Positive

  • Browser dashboards can evaluate coherence gates without a backend roundtrip
  • Research notebooks (Observable, Jupyter) can use the replay memory directly
  • TypeScript types auto-generated by wasm-pack from #[wasm_bindgen] annotations
  • Follows the same pattern as 15+ existing WASM crates in the workspace

Negative

  • Trait objects (CoherenceGate, MemoryStore) cannot be passed across WASM boundary; only concrete implementations are exposed
  • [u8; 16] hex encoding adds a small overhead at the boundary (negligible vs. WASM call overhead)
  • ReservoirStore.maybeWrite() takes JSON values rather than typed structs due to WASM ownership constraints

Risks

  • Rust 1.91 WASM codegen bug requires env-var workaround; future Rust versions should fix this
  • serde-wasm-bindgen 0.6 is a newer dependency not yet in the workspace; pinned to avoid surprises