Accepted
2026-02-25
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.
From crates/ruvector-gnn-wasm/Cargo.toml:
crate-type = ["cdylib", "rlib"]- Dependencies:
ruvector-gnnwithdefault-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-verifiedwithfeatures = ["ultra"] - Uses
wasm-bindgen,serde-wasm-bindgen,js-sys,web-sys - Release profile:
opt-level = "s",lto = true
We will create two binding crates following the established workspace patterns:
crates/ruvector-graph-transformer-wasm/-- WASM bindings viawasm-bindgencrates/ruvector-graph-transformer-node/-- Node.js bindings vianapi-rs(v2.16)
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:
- Training requires
rayonfor parallelism (not available in WASM) ElasticWeightConsolidationusesndarraywith BLAS, which adds ~500 KB to WASM size- Training workloads are server-side; inference is the browser use case
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
# 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"// 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<WasmGraphTransformer, JsValue> {
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<JsValue, JsValue> {
// Deserialize, run forward, serialize result
// ...
}
/// Get the proof attestation chain as an array of Uint8Arrays.
pub fn proof_chain(&self) -> Result<JsValue, JsValue> {
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<String, JsValue> {
let snapshot = self.inner.coherence();
serde_json::to_string(&snapshot)
.map_err(|e| JsValue::from_str(&e.to_string()))
}
}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
# 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// 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<f64>,
pub attestation_hex: String,
}
#[napi]
pub struct NodeVerifiedTrainer {
inner: VerifiedTrainer,
}
#[napi]
impl NodeVerifiedTrainer {
#[napi(constructor)]
pub fn new(config_json: String) -> Result<Self> {
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<String> {
// Deserialize gradients, run step, serialize attestation
// ...
}
#[napi]
pub fn seal(&mut self) -> Result<JsTrainingCertificate> {
// Seal training run and return certificate
// ...
}
}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:
opt-level = "z"(optimize for size)lto = true(dead code elimination across crates)panic = "abort"(no unwinding machinery)default-features = falseonruvector-graph-transformer(onlyproof-gatedandsublinear-attention)- Excluding training and the
spectralfeature fromruvector-coherence
If the target is exceeded, further reductions:
- Replace
serde_jsonwithminiserde(-30 KB) - Strip
tracinginstrumentation via feature flag (-10 KB) - Use
wasm-opt -Ozpost-processing (-10-20%)
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:
// 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<T> {
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 | 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:
- Hierarchical coarsening uses
rayonparallelism, unavailable in WASM mmapis not available in WASM environments- Training is server-side only (see rationale above)
- Spectral coherence uses
ndarraywith heavy numerics; excluded for size
WASM:
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/*.wasmNode.js:
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-msvcWASM (wasm-bindgen-test):
#[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):
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);
});- WASM:
@ruvector/graph-transformer-wasm - Node.js:
@ruvector/graph-transformer-node
Both published under the ruvnet npm account (already authenticated per CLAUDE.md).
- 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
-wasmand-nodecrates reduce maintenance burden - TypeScript types provide compile-time safety for JS consumers
- 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,
Uint8Arrayfor attestations) adds latency compared to native Rust
- WASM size may exceed 300 KB if
ruvector-solverbrings in unexpected transitive dependencies. Mitigated bydefault-features = falseandwasm-pack --releasesize verification in CI - NAPI-RS version 2.16 may introduce breaking changes in minor releases. Mitigated by pinning to workspace version
- Browser
WebAssembly.Memorylimits (4 GB on 64-bit, 2 GB on 32-bit) may be hit for large graphs. Mitigated by streaming processing and thecertify_complexityAPI that rejects oversized graphs before execution
- Create
crates/ruvector-graph-transformer-wasm/following the structure above - Create
crates/ruvector-graph-transformer-node/following the structure above - Add both to
[workspace.members]in rootCargo.toml - Implement Tier 1 (core) bindings first, test with
wasm-bindgen-testand Node.js - Implement Tier 2 (attention) bindings
- Implement Tier 3 (training) in Node.js only
- CI: add
wasm-pack buildandnapi buildto GitHub Actions workflow - Publish to npm:
@ruvector/graph-transformer-wasmand@ruvector/graph-transformer-node
- ADR-046: Graph Transformer Unified Architecture (module structure, feature flags)
- ADR-047: Proof-Gated Mutation Protocol (
ProofGate<T>,ProofAttestationserialization) - 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"