# ADR-050: Graph Transformer WASM and Node.js Bindings ## Status Accepted ## Date 2026-02-25 ## Context RuVector's existing crates ship WASM and Node.js bindings following a consistent pattern: a `-wasm` crate using `wasm-bindgen` and a `-node` crate using `napi-rs`. Examples include `ruvector-gnn-wasm` / `ruvector-gnn-node`, `ruvector-graph-wasm` / `ruvector-graph-node`, `ruvector-verified-wasm`, and `ruvector-mincut-wasm` / `ruvector-mincut-node`. The new `ruvector-graph-transformer` crate (ADR-046) needs equivalent bindings so that TypeScript/JavaScript applications can use proof-gated graph transformers in the browser (WASM) and on the server (Node.js via NAPI-RS). The challenge is deciding which subset of the Rust API to expose, managing the WASM binary size (target < 300 KB), and ensuring feature parity where feasible. ### Existing Binding Patterns From `crates/ruvector-gnn-wasm/Cargo.toml`: - `crate-type = ["cdylib", "rlib"]` - Dependencies: `ruvector-gnn` with `default-features = false, features = ["wasm"]` - Uses `serde-wasm-bindgen = "0.6"` for struct serialization - Release profile: `opt-level = "z"`, `lto = true`, `codegen-units = 1`, `panic = "abort"` From `crates/ruvector-gnn-node/Cargo.toml`: - `crate-type = ["cdylib"]` - Dependencies: `napi = { workspace = true }`, `napi-derive = { workspace = true }` - Build dependency: `napi-build = "2"` - Release profile: `lto = true`, `strip = true` From `crates/ruvector-verified-wasm/Cargo.toml`: - Dependencies: `ruvector-verified` with `features = ["ultra"]` - Uses `wasm-bindgen`, `serde-wasm-bindgen`, `js-sys`, `web-sys` - Release profile: `opt-level = "s"`, `lto = true` ## Decision We will create two binding crates following the established workspace patterns: - `crates/ruvector-graph-transformer-wasm/` -- WASM bindings via `wasm-bindgen` - `crates/ruvector-graph-transformer-node/` -- Node.js bindings via `napi-rs` (v2.16) ### API Surface: What to Expose Not all Rust functionality translates efficiently to WASM/JS. The binding surface is scoped to three tiers: **Tier 1 -- Core (both WASM and Node.js)**: | API | Rust Source | Binding | |-----|------------|---------| | `GraphTransformer::new(config)` | `lib.rs` | Constructor, takes JSON config | | `GraphTransformer::forward(batch)` | `lib.rs` | Returns `ProofGatedOutput` as JSON | | `GraphTransformer::mutate(op)` | `lib.rs` | Returns mutation result + attestation | | `ProofGate::unlock()` | `proof_gated/gate.rs` | Unlocks and returns inner value | | `ProofGate::is_satisfied()` | `proof_gated/gate.rs` | Boolean check | | `proof_chain()` | `proof_gated/mod.rs` | Returns attestation array as `Uint8Array[]` | | `coherence()` | via `ruvector-coherence` | Returns coherence snapshot as JSON | **Tier 2 -- Attention (both WASM and Node.js)**: | API | Rust Source | Binding | |-----|------------|---------| | `PprSampledAttention::new()` | `sublinear_attention/ppr.rs` | Constructor | | `LshSpectralAttention::new()` | `sublinear_attention/lsh.rs` | Constructor | | `certify_complexity()` | `sublinear_attention/mod.rs` | Returns complexity bound as JSON | | `SpectralSparsifier::sparsify()` | `sublinear_attention/spectral_sparsify.rs` | Returns sparsified edge list | **Tier 3 -- Training (Node.js only, not WASM)**: | API | Rust Source | Binding | |-----|------------|---------| | `VerifiedTrainer::new(config)` | `verified_training/pipeline.rs` | Constructor | | `VerifiedTrainer::step()` | `verified_training/pipeline.rs` | Single training step | | `VerifiedTrainer::seal()` | `verified_training/pipeline.rs` | Returns `TrainingCertificate` as JSON | | `RobustnessCertifier::certify()` | `verified_training/mod.rs` | Returns certificate as JSON | Training is excluded from WASM because: 1. Training requires `rayon` for parallelism (not available in WASM) 2. `ElasticWeightConsolidation` uses `ndarray` with BLAS, which adds ~500 KB to WASM size 3. Training workloads are server-side; inference is the browser use case ### WASM Crate Structure ``` crates/ruvector-graph-transformer-wasm/ Cargo.toml src/ lib.rs # wasm_bindgen entry points types.rs # TS-friendly wrapper types (JsValue serialization) proof_gate.rs # ProofGate WASM bindings attention.rs # Sublinear attention WASM bindings error.rs # Error conversion to JsValue tests/ web.rs # wasm-bindgen-test integration tests package.json # npm package metadata tsconfig.json # TypeScript configuration for generated types ``` ```toml # Cargo.toml [package] name = "ruvector-graph-transformer-wasm" version = "2.0.4" edition = "2021" rust-version = "1.77" license = "MIT" description = "WASM bindings for ruvector-graph-transformer: proof-gated graph transformers in the browser" [lib] crate-type = ["cdylib", "rlib"] [dependencies] ruvector-graph-transformer = { version = "2.0.4", path = "../ruvector-graph-transformer", default-features = false, features = ["proof-gated", "sublinear-attention"] } wasm-bindgen = { workspace = true } serde-wasm-bindgen = "0.6" serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } js-sys = { workspace = true } web-sys = { workspace = true, features = ["console"] } getrandom = { workspace = true, features = ["wasm_js"] } [dev-dependencies] wasm-bindgen-test = "0.3" [profile.release] opt-level = "z" lto = true codegen-units = 1 panic = "abort" [profile.release.package."*"] opt-level = "z" ``` ### WASM Binding Implementation ```rust // src/lib.rs use wasm_bindgen::prelude::*; use ruvector_graph_transformer::{GraphTransformer, GraphTransformerConfig}; #[wasm_bindgen] pub struct WasmGraphTransformer { inner: GraphTransformer, } #[wasm_bindgen] impl WasmGraphTransformer { /// Create a new graph transformer from JSON config. #[wasm_bindgen(constructor)] pub fn new(config_json: &str) -> Result { let config: GraphTransformerConfig = serde_json::from_str(config_json) .map_err(|e| JsValue::from_str(&e.to_string()))?; let inner = GraphTransformer::new(config, DefaultPropertyGraph::new()) .map_err(|e| JsValue::from_str(&e.to_string()))?; Ok(Self { inner }) } /// Run forward pass. Input and output are JSON-serialized. pub fn forward(&mut self, batch_json: &str) -> Result { // Deserialize, run forward, serialize result // ... } /// Get the proof attestation chain as an array of Uint8Arrays. pub fn proof_chain(&self) -> Result { let chain = self.inner.proof_chain(); let array = js_sys::Array::new(); for att in chain { let bytes = att.to_bytes(); let uint8 = js_sys::Uint8Array::from(&bytes[..]); array.push(&uint8); } Ok(array.into()) } /// Get coherence snapshot as JSON. pub fn coherence(&self) -> Result { let snapshot = self.inner.coherence(); serde_json::to_string(&snapshot) .map_err(|e| JsValue::from_str(&e.to_string())) } } ``` ### Node.js Crate Structure ``` crates/ruvector-graph-transformer-node/ Cargo.toml src/ lib.rs # napi-rs entry points types.rs # NAPI-RS type wrappers proof_gate.rs # ProofGate Node bindings attention.rs # Sublinear attention Node bindings training.rs # VerifiedTrainer Node bindings (Tier 3) build.rs # napi-build index.d.ts # TypeScript type declarations package.json # npm package metadata __test__/ index.spec.mjs # Node.js integration tests ``` ```toml # Cargo.toml [package] name = "ruvector-graph-transformer-node" version = "2.0.4" edition = "2021" rust-version = "1.77" license = "MIT" description = "Node.js bindings for ruvector-graph-transformer via NAPI-RS" [lib] crate-type = ["cdylib"] [dependencies] ruvector-graph-transformer = { version = "2.0.4", path = "../ruvector-graph-transformer", features = ["full"] } napi = { workspace = true } napi-derive = { workspace = true } serde_json = { workspace = true } [build-dependencies] napi-build = "2" [profile.release] lto = true strip = true ``` ### Node.js Binding Implementation (Training Example) ```rust // src/training.rs use napi::bindgen_prelude::*; use napi_derive::napi; use ruvector_graph_transformer::verified_training::{ VerifiedTrainer, VerifiedTrainerConfig, TrainingCertificate, }; #[napi(object)] pub struct JsTrainingCertificate { pub total_steps: u32, pub violations: u32, pub final_loss: f64, pub final_coherence: Option, pub attestation_hex: String, } #[napi] pub struct NodeVerifiedTrainer { inner: VerifiedTrainer, } #[napi] impl NodeVerifiedTrainer { #[napi(constructor)] pub fn new(config_json: String) -> Result { let config: VerifiedTrainerConfig = serde_json::from_str(&config_json) .map_err(|e| Error::from_reason(e.to_string()))?; let inner = VerifiedTrainer::new(config) .map_err(|e| Error::from_reason(e.to_string()))?; Ok(Self { inner }) } #[napi] pub fn step(&mut self, loss: f64, gradients_json: String) -> Result { // Deserialize gradients, run step, serialize attestation // ... } #[napi] pub fn seal(&mut self) -> Result { // Seal training run and return certificate // ... } } ``` ### WASM Size Budget Target: < 300 KB for the release `.wasm` binary (gzipped). Size breakdown estimate: | Component | Estimated Size | |-----------|---------------| | `ruvector-verified` (proof gates, arena, attestations) | ~40 KB | | `ruvector-solver` (forward-push, random-walk, neumann) | ~60 KB | | `ruvector-attention` (core attention only, no training) | ~80 KB | | `ruvector-coherence` (metrics, no spectral) | ~15 KB | | `wasm-bindgen` glue | ~20 KB | | Serde JSON | ~50 KB | | **Total (estimated)** | ~265 KB | Size is controlled by: 1. `opt-level = "z"` (optimize for size) 2. `lto = true` (dead code elimination across crates) 3. `panic = "abort"` (no unwinding machinery) 4. `default-features = false` on `ruvector-graph-transformer` (only `proof-gated` and `sublinear-attention`) 5. Excluding training and the `spectral` feature from `ruvector-coherence` If the target is exceeded, further reductions: - Replace `serde_json` with `miniserde` (-30 KB) - Strip `tracing` instrumentation via feature flag (-10 KB) - Use `wasm-opt -Oz` post-processing (-10-20%) ### TypeScript Types Both packages ship TypeScript type declarations. The WASM package generates types via `wasm-bindgen`'s `--typescript` flag. The Node.js package uses `napi-rs`'s automatic `.d.ts` generation from `#[napi]` attributes. Key TypeScript interfaces: ```typescript // Generated by wasm-bindgen / napi-rs export interface GraphTransformerConfig { proofGated: boolean; attentionMechanism: 'ppr' | 'lsh' | 'spectral-sparsify'; pprAlpha?: number; pprTopK?: number; lshTables?: number; lshBits?: number; spectralEpsilon?: number; } export interface ProofGatedOutput { value: T; satisfied: boolean; attestationHex: string; } export interface ComplexityBound { opsUpperBound: number; memoryUpperBound: number; complexityClass: string; } export interface TrainingCertificate { totalSteps: number; violations: number; finalLoss: number; finalCoherence: number | null; attestationHex: string; invariantStats: InvariantStats[]; } ``` ### Feature Parity Matrix | Feature | Rust | WASM | Node.js | |---------|------|------|---------| | ProofGate | Yes | Yes | Yes | | Three-tier routing | Yes | Yes | Yes | | Attestation chain | Yes | Yes | Yes | | PPR-sampled attention | Yes | Yes | Yes | | LSH spectral attention | Yes | Yes | Yes | | Spectral sparsification | Yes | Yes | Yes | | Hierarchical coarsening | Yes | No (1) | Yes | | Memory-mapped processing | Yes | No (2) | Yes | | VerifiedTrainer | Yes | No (3) | Yes | | Robustness certification | Yes | No (3) | Yes | | EWC continual learning | Yes | No (3) | Yes | | Coherence (spectral) | Yes | No (4) | Yes | | Coherence (basic) | Yes | Yes | Yes | Notes: 1. Hierarchical coarsening uses `rayon` parallelism, unavailable in WASM 2. `mmap` is not available in WASM environments 3. Training is server-side only (see rationale above) 4. Spectral coherence uses `ndarray` with heavy numerics; excluded for size ### Build Pipeline **WASM**: ```bash cd crates/ruvector-graph-transformer-wasm wasm-pack build --target web --release --out-dir ../../pkg/graph-transformer-wasm # Verify size ls -la ../../pkg/graph-transformer-wasm/*.wasm ``` **Node.js**: ```bash cd crates/ruvector-graph-transformer-node # NAPI-RS build for current platform npx napi build --release --platform # Cross-compile for CI (linux-x64-gnu, darwin-arm64, win32-x64-msvc) npx napi build --release --target x86_64-unknown-linux-gnu npx napi build --release --target aarch64-apple-darwin npx napi build --release --target x86_64-pc-windows-msvc ``` ### Testing Strategy **WASM** (`wasm-bindgen-test`): ```rust #[cfg(test)] mod tests { use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] fn test_graph_transformer_roundtrip() { let config = r#"{"proofGated": true, "attentionMechanism": "ppr"}"#; let gt = WasmGraphTransformer::new(config).unwrap(); assert!(gt.coherence().is_ok()); } #[wasm_bindgen_test] fn test_proof_chain_returns_uint8arrays() { // Verify attestation chain serialization } } ``` **Node.js** (via `jest` or `vitest`): ```javascript import { GraphTransformer, VerifiedTrainer } from '@ruvector/graph-transformer-node'; test('forward pass returns proof-gated output', () => { const gt = new GraphTransformer('{"proofGated": true, "attentionMechanism": "ppr"}'); const result = gt.forward(batchJson); expect(result.satisfied).toBe(true); expect(result.attestationHex).toHaveLength(164); // 82 bytes = 164 hex chars }); test('verified training produces certificate', () => { const trainer = new VerifiedTrainer(configJson); for (let i = 0; i < 10; i++) { trainer.step(loss, gradientsJson); } const cert = trainer.seal(); expect(cert.totalSteps).toBe(10); expect(cert.violations).toBe(0); }); ``` ### npm Package Names - WASM: `@ruvector/graph-transformer-wasm` - Node.js: `@ruvector/graph-transformer-node` Both published under the `ruvnet` npm account (already authenticated per `CLAUDE.md`). ## Consequences ### Positive - TypeScript/JavaScript developers get proof-gated graph transformers with zero Rust toolchain requirement - WASM < 300 KB enables browser-side inference with proof verification - Node.js bindings get full feature parity including verified training - Consistent binding patterns with existing `-wasm` and `-node` crates reduce maintenance burden - TypeScript types provide compile-time safety for JS consumers ### Negative - WASM lacks training, hierarchical coarsening, and spectral coherence -- feature gap may confuse users - Two binding crates double the CI build matrix - NAPI-RS cross-compilation requires platform-specific CI runners (or cross-rs) - Serialization overhead (JSON for config, `Uint8Array` for attestations) adds latency compared to native Rust ### Risks - WASM size may exceed 300 KB if `ruvector-solver` brings in unexpected transitive dependencies. Mitigated by `default-features = false` and `wasm-pack --release` size verification in CI - NAPI-RS version 2.16 may introduce breaking changes in minor releases. Mitigated by pinning to workspace version - Browser `WebAssembly.Memory` limits (4 GB on 64-bit, 2 GB on 32-bit) may be hit for large graphs. Mitigated by streaming processing and the `certify_complexity` API that rejects oversized graphs before execution ## Implementation 1. Create `crates/ruvector-graph-transformer-wasm/` following the structure above 2. Create `crates/ruvector-graph-transformer-node/` following the structure above 3. Add both to `[workspace.members]` in root `Cargo.toml` 4. Implement Tier 1 (core) bindings first, test with `wasm-bindgen-test` and Node.js 5. Implement Tier 2 (attention) bindings 6. Implement Tier 3 (training) in Node.js only 7. CI: add `wasm-pack build` and `napi build` to GitHub Actions workflow 8. Publish to npm: `@ruvector/graph-transformer-wasm` and `@ruvector/graph-transformer-node` ## References - ADR-046: Graph Transformer Unified Architecture (module structure, feature flags) - ADR-047: Proof-Gated Mutation Protocol (`ProofGate`, `ProofAttestation` serialization) - ADR-048: Sublinear Graph Attention (attention API surface) - ADR-049: Verified Training Pipeline (`VerifiedTrainer`, `TrainingCertificate`) - `crates/ruvector-gnn-wasm/Cargo.toml`: WASM binding pattern (opt-level "z", panic "abort") - `crates/ruvector-gnn-node/Cargo.toml`: NAPI-RS binding pattern (napi-build, cdylib) - `crates/ruvector-verified-wasm/Cargo.toml`: Verified WASM binding pattern (serde-wasm-bindgen) - `crates/ruvector-graph-wasm/Cargo.toml`: Graph WASM binding pattern - Workspace `Cargo.toml`: `wasm-bindgen = "0.2"`, `napi = { version = "2.16" }`, `napi-derive = "2.16"`