Files
wifi-densepose/docs/adr/ADR-050-graph-transformer-bindings.md
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

490 lines
17 KiB
Markdown

# 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<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()))
}
}
```
### 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<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
// ...
}
}
```
### 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<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 Parity Matrix
| Feature | Rust | WASM | Node.js |
|---------|------|------|---------|
| ProofGate<T> | 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<T>`, `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"`