Skip to content

feat(ruvector-rabitq-wasm): WASM bindings for RaBitQ#390

Open
ruvnet wants to merge 2 commits intomainfrom
feature/rabitq-wasm
Open

feat(ruvector-rabitq-wasm): WASM bindings for RaBitQ#390
ruvnet wants to merge 2 commits intomainfrom
feature/rabitq-wasm

Conversation

@ruvnet
Copy link
Copy Markdown
Owner

@ruvnet ruvnet commented Apr 26, 2026

Summary

Closes the WASM gap called out in docs/research/rabitq-integration/ Tier 2 ("WASM / edge: 32× compression makes on-device RAG feasible") and ADR-157's Phase 2 goal. New ruvector-rabitq-wasm sibling crate exposes RabitqIndex to JavaScript/TypeScript callers via wasm-bindgen.

import init, { RabitqIndex } from "ruvector-rabitq";
await init();

const dim = 768;
const n = 10_000;
const vectors = new Float32Array(n * dim);  // populate
const idx = RabitqIndex.build(vectors, dim, 42, 20);
const query = new Float32Array(dim);
const results = idx.search(query, 10);  // [{id, distance}, ...]

Surface

  • RabitqIndex.build(vectors, dim, seed, rerank_factor) — flat Float32Array of length n * dim.
  • idx.search(query, k)SearchResult[].
  • idx.len, idx.isEmpty.
  • version() — crate version baked at build time.
  • SearchResult { id: u32, distance: f32 } — mirrors the Python SDK M1 shape (PR feat(ruvector-py): Python SDK M1 — RaBitQ wheel #381) so callers porting between languages get identical structures.

Native compatibility tweak

ruvector-rabitq had one rayon call site in from_vectors_parallel_with_rotation. Gated that on cfg(not(target_arch = "wasm32")) with a sequential .into_iter() fallback for WASM. Output is bit-identical because the rotation matrix is deterministic (ADR-154) — parallel ordering doesn't affect bytes.

rayon is now [target.'cfg(not(target_arch = "wasm32"))'.dependencies]. Native behavior unchanged (39 / 39 lib tests still pass).

Verification (native)

  • cargo build --workspace → 0 errors
  • cargo build -p ruvector-rabitq-wasm → clean
  • cargo clippy -p ruvector-rabitq-wasm --all-targets --no-deps -- -D warnings → exit 0
  • cargo test -p ruvector-rabitq → 39 / 39 (unchanged)
  • cargo fmt --all --check → clean

WASM target build (wasm32-unknown-unknown) requires rustup target add wasm32-unknown-unknown — not exercised here; will be covered by a follow-up CI job.

Testing strategy note

Native tests of WASM bindings panic by design — JsValue::from_str calls into a wasm-bindgen runtime stub that's unimplemented!() on non-wasm32 targets (since 0.2.117). The right testing path is wasm-pack test --node or wasm-pack test --headless --chrome. Follow-up.

The numerical correctness is already covered by ruvector-rabitq's own test suite (39 tests, untouched).

Refs

🤖 Generated with claude-flow

ruvnet and others added 2 commits April 25, 2026 23:37
Unblocks the 7 stacked PRs (#381-#387) and turns `main`'s CI green
for the first time in days. Two issues fixed:

## Failure 1 — Security audit (was: 8 vulnerabilities)

`cargo audit` is now exit 0. 4 of the 5 critical advisories were
fixed by version bumps; only the unfixable one is ignored.

**Dep-bumped:**
- `rustls-webpki 0.101.7` + `0.103.10` → `0.103.13` via
  `cargo update -p rustls-webpki@0.103.10`. Patches:
    RUSTSEC-2026-0098 (URI name constraints)
    RUSTSEC-2026-0099 (wildcard name constraints)
    RUSTSEC-2026-0104 (CRL parsing panic)
- `idna 0.5.0` → `1.1.0` via `validator 0.18 → 0.20` in
  `examples/scipix`. Patches RUSTSEC-2024-0421 (Punycode acceptance).
- Bonus: `reqwest 0.11 → 0.12` (in `ruvector-core` + `examples/benchmarks`)
  and `hf-hub 0.3 → 0.4` (in `ruvector-core` + `ruvllm` +
  `ruvllm-cli`). Removes the entire legacy `rustls 0.21` /
  `rustls-webpki 0.101.7` subtree from the lockfile.

**Ignored** (single advisory, with rationale):
- `RUSTSEC-2023-0071` (rsa Marvin timing sidechannel) — no upstream
  fix available; we don't expose RSA decryption services. Documented
  in `.cargo/audit.toml`.

**Unmaintained warnings** (16 total — proc-macro-error, derivative,
instant, paste, bincode 1, pqcrypto-{kyber,dilithium}, rustls-pemfile 1,
rusttype, wee_alloc, number_prefix, rand_os, core2, lru, pprof, rand) —
each given a one-line justification in `.cargo/audit.toml` so CI stays
green on them while the team decides whether to chase upstream
replacements.

## Failure 2 — Tests timeout (was: 30-min job timeout cancellation)

`.github/workflows/ci.yml` `test` job is now a `matrix` with
`fail-fast: false` and `timeout-minutes: 45`. Six parallel shards
under `cargo nextest run` (installed via `taiki-e/install-action@v2`)
plus a separate `cargo test --doc` step (nextest doesn't run
doctests):

  | Shard            | Crates                                      |
  |------------------|---------------------------------------------|
  | vector-index     | rabitq, rulake, diskann, graph, gnn, cnn    |
  | rvagent          | 10 rvagent-* crates                         |
  | ruvix            | 16 ruvix-* crates                           |
  | ruqu-quantum     | 5 ruqu* crates                              |
  | ml-research      | attention, mincut, scipix, fpga-transformer,|
  |                  | sparse-inference, sparsifier, solver,       |
  |                  | graph-transformer, domain-expansion,        |
  |                  | robotics                                    |
  | core-and-rest    | --workspace minus the above                 |

`Swatinem/rust-cache@v2` is keyed per shard. Audit job switched to
`taiki-e/install-action` for `cargo-audit` (faster than
`cargo install --locked`).

## Verification

  cargo audit                                                   → exit 0
  cargo build --workspace --exclude ruvector-postgres           → clean
  cargo clippy --workspace --exclude ruvector-postgres --no-deps -- -D warnings → exit 0
  cargo fmt --all --check                                       → exit 0

## Cargo.lock churn

166-line diff, net ~120 lines removed (more deletions than
additions). Removed: `idna 0.5.0`, `rustls-webpki 0.101.7`,
`validator 0.18`, `validator_derive 0.18`, `proc-macro-error 1.0.4`.
Added: `rustls-webpki 0.103.13`, `validator 0.20`,
`proc-macro-error2`, `hf-hub 0.4.3`, `reqwest 0.12.28`. No
suspicious crates.

## Recommended merge order

1. **This PR first** — unblocks every other PR's CI.
2. After this lands and main is green, rebase the 7 open PRs
   (#381-#387) one at a time. The DiskANN stack (#383#384#385#386)
   must merge in numeric order. #381 (Python SDK), #382 (research),
   #387 (graph property index) are independent and can merge in
   any order after their CI goes green on the rebase.

Co-Authored-By: claude-flow <ruv@ruv.net>
Closes the WASM gap from `docs/research/rabitq-integration/` Tier 2
("WASM / edge: 32× compression makes on-device RAG feasible") and
ADR-157 ("VectorKernel WASM kernel as a Phase 2 goal"). Adds a
`ruvector-rabitq-wasm` sibling crate that exposes `RabitqIndex` to
JavaScript/TypeScript callers (browsers, Cloudflare Workers, Deno,
Bun) via wasm-bindgen.

```js
import init, { RabitqIndex } from "ruvector-rabitq";
await init();

const dim = 768;
const n = 10_000;
const vectors = new Float32Array(n * dim);  // populate
const idx = RabitqIndex.build(vectors, dim, 42, 20);
const query = new Float32Array(dim);
const results = idx.search(query, 10);  // [{id, distance}, ...]
```

## Surface

- `RabitqIndex.build(vectors: Float32Array, dim, seed, rerank_factor)`
- `idx.search(query: Float32Array, k) → SearchResult[]`
- `idx.len`, `idx.isEmpty`
- `version()` — crate version baked at build time
- `SearchResult { id: u32, distance: f32 }` — mirrors the Python SDK
  (PR #381) shape so callers porting code between languages get
  identical structures.

## Native compatibility tweak

`ruvector-rabitq` had one rayon call site in
`from_vectors_parallel_with_rotation`. WASM is single-threaded — gated
that path on `cfg(not(target_arch = "wasm32"))` with a sequential
`.into_iter()` fallback for wasm. Output is bit-identical because the
rotation matrix is deterministic (ADR-154); parallel ordering doesn't
affect bytes.

`rayon` is now `[target.'cfg(not(target_arch = "wasm32"))'.dependencies]`
so the wasm build doesn't pull it in. Native build behavior unchanged
(39 / 39 lib tests still pass).

## Crate layout

  crates/ruvector-rabitq-wasm/
    Cargo.toml      cdylib + rlib, wasm-bindgen 0.2, abi-3-friendly
    src/lib.rs      ~150 LoC of bindings; tests gated to wasm32 via
                    wasm_bindgen_test (native test would panic in
                    wasm-bindgen 0.2.117's runtime stub).

## Testing strategy

Native tests of WASM bindings panic by design — `JsValue::from_str`
calls into a wasm-bindgen runtime stub that's `unimplemented!()` on
non-wasm32 targets (since 0.2.117). The right path is
`wasm-pack test --node` or `wasm-pack test --headless --chrome`,
which we'll wire into CI as a follow-up.

The numerical correctness is already covered by `ruvector-rabitq`'s
own test suite. This crate only adds the JS-facing surface.

## Verification (native)

  cargo build --workspace                                              → 0 errors
  cargo build -p ruvector-rabitq-wasm                                  → clean
  cargo clippy -p ruvector-rabitq-wasm --all-targets --no-deps -- -D warnings → exit 0
  cargo test -p ruvector-rabitq                                        → 39 / 39 (unchanged)
  cargo fmt --all --check                                              → clean

WASM target build (`wasm32-unknown-unknown`) requires `rustup target
add wasm32-unknown-unknown` — not exercised in this PR; will be
covered by a follow-up CI job.

Refs: docs/research/rabitq-integration/ Tier 2, ADR-157
("Optional Accelerator Plane"), PR #381 (Python SDK shape mirror).

Co-Authored-By: claude-flow <ruv@ruv.net>
ruvnet added a commit that referenced this pull request Apr 26, 2026
PR #388's matrix-split CI exposed two pre-existing failures hidden
by the previous 30-minute Tests-job timeout. Both have surprising
root causes worth recording.

## Failure 1 — `rvagent-cli::a2a_cli::a2a_serve_discover_and_send_task`

Symptom: `unrecognized subcommand 'a2a'` from the spawned `rvagent`
binary; test panicked at the `expect(server closed before emitting
listening line)` site.

Root cause: **PR #380's `main.rs` and `Cargo.toml` changes were
silently lost during merge.** The new `crates/rvAgent/rvagent-cli/src/a2a.rs`
file landed, but:
  - `mod a2a;` was never added to `main.rs`
  - The `A2a(A2aCommand)` variant was never added to the `Commands`
    enum
  - The dispatch arm was never wired in
  - `Cargo.toml` was never updated with the new deps
    (`rvagent-a2a` path dep, `ed25519-dalek`, `rand_core`, `axum`,
    `reqwest`, `hex`, plus tokio's `signal`/`process`/`time`/`io-*`
    /`fs`/`net` features)

So `rvagent` shipped with `a2a.rs` orphaned: the file compiled into
the lib via `lib.rs` but the binary's `main.rs` never knew about it.

Fix:
  - `main.rs`: add `mod a2a;`, add `A2a(a2a::A2aCommand)` variant,
    add `is_tui_mode` arm, add dispatch arm using
    `cli.command.take()` to own the variant (avoids needing to
    derive Clone on every clap struct in `a2a.rs`).
  - `Cargo.toml`: restore the deps and tokio features PR #380
    intended.

Diagnostic improvement: also extended the test to drain the
server's stderr in the background and dump it on every panic
path. Without that I'd never have seen `unrecognized subcommand
'a2a'` — the future-me debugging this would have spent hours.

Verified locally: `cargo test -p rvagent-cli --test a2a_cli` →
`1 passed; 0 failed`.

## Failure 2 — `ruqu-wasm::tests::test_circuit_rejects_too_many_qubits`

Symptom: panic inside `wasm-bindgen-0.2.117/src/lib.rs:1280`
("function not implemented on non-wasm32 targets").

Root cause: the test module was `#[cfg(test)]` (runs on every
`cargo test`) but called into wasm-bindgen-wrapped types
(`WasmQuantumCircuit::new`), which since wasm-bindgen 0.2.117
panic when called from a non-wasm runtime.

Fix: gate the tests module on `#[cfg(all(test, target_arch =
"wasm32"))]`. WASM-binding tests run via `wasm-pack test`; the
underlying `ruqu-core` numeric logic is already covered by its
own native test suite.

This is the same pattern PR #390 (RaBitQ WASM) used proactively.

## Verification

  cargo build -p rvagent-cli                                 → clean
  cargo test  -p rvagent-cli --test a2a_cli                  → 1/1 pass
  cargo build -p ruqu-wasm                                   → clean
  cargo test  -p ruqu-wasm                                   → 0 native tests
                                                                (wasm-only path)
  cargo clippy -p rvagent-cli -p ruqu-wasm --all-targets
       --no-deps -- -D warnings                              → exit 0
  cargo fmt --all --check                                    → exit 0

After this lands, PR #388's Tests (rvagent) and Tests (ruqu-quantum)
shards should go green.

Co-Authored-By: claude-flow <ruv@ruv.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant