From 04ce4d32c4bb03970aea06043eea360f747662e5 Mon Sep 17 00:00:00 2001 From: ruvnet Date: Fri, 17 Jul 2026 12:38:05 -0400 Subject: [PATCH] fix: SONA zero-vector bug, RVF wasm persistence, metadata safety, doc gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #706: apply_micro_lora/apply_base_lora seeded their output buffer with zeros in napi.rs, napi_simple.rs, and wasm.rs, but the Rust LoRA forward pass has residual semantics (adds delta into the buffer). Cold queries collapsed to the zero vector, and post-feedback queries returned only the delta instead of input+delta. Fixed by seeding with a clone of the input in all three binding layers. Added regression tests. - #705: WasmBackend had no byte-level persistence. The underlying rvf_store_export/rvf_store_open C-ABI functions already existed — wired them into WasmBackend.exportBytes()/openBytes() and RvfDatabase.exportBytes()/openBytes(), verified against the real compiled .wasm binary. - #704: NodeBackend.ingestBatch() silently dropped RvfIngestEntry.metadata instead of forwarding it, and query() filter serialization omitted the native parser's required valueType. Since a full field-name-to-id design for metadata durability is a larger follow-up, applied the issue's own suggested interim fix: ingest now throws MetadataNotSupported instead of silently losing data. Filter serialization now infers and includes valueType. - #707: docs/api/NODEJS_API.md and docs/guides/ADVANCED_FEATURES.md advertised HybridSearch/FilteredSearch/MMRSearch/ConformalPredictor as Node exports; none are bound by crates/ruvector-node. Corrected with explicit "not yet shipped" notes pointing at the tracking issue. README corrections for #705/#707 (WasmRvfStore, HybridSearch docs) are staged separately — a repo hook requires a visual design-grade ritual for any README change that this session cannot complete (no browser/screenshot access), so they'll follow once that's resolved. Co-Authored-By: claude-flow --- crates/sona/src/napi.rs | 8 +- crates/sona/src/napi_simple.rs | 8 +- crates/sona/src/wasm.rs | 7 +- docs/api/NODEJS_API.md | 23 +- docs/guides/ADVANCED_FEATURES.md | 31 ++- examples/rvf/scripts/rvf-browser.html | 11 +- npm/packages/rvf/dist/backend.d.ts | 16 ++ npm/packages/rvf/dist/backend.js | 139 +++++++++++- npm/packages/rvf/dist/backend.js.map | 2 +- npm/packages/rvf/dist/database.d.ts | 20 ++ npm/packages/rvf/dist/database.js | 30 +++ npm/packages/rvf/dist/database.js.map | 2 +- npm/packages/rvf/dist/errors.d.ts | 3 +- npm/packages/rvf/dist/errors.js | 3 + npm/packages/rvf/dist/errors.js.map | 2 +- npm/packages/rvf/dist/types.d.ts | 11 +- npm/packages/rvf/src/backend.ts | 154 +++++++++++++- npm/packages/rvf/src/database.ts | 36 ++++ npm/packages/rvf/src/errors.ts | 4 + npm/packages/rvf/src/types.ts | 11 +- .../rvf/tests/test-metadata-and-filters.js | 201 ++++++++++++++++++ .../rvf/tests/test-wasm-persistence.js | 91 ++++++++ npm/packages/sona/test/basic.test.js | 76 +++++-- 23 files changed, 846 insertions(+), 43 deletions(-) create mode 100644 npm/packages/rvf/tests/test-metadata-and-filters.js create mode 100644 npm/packages/rvf/tests/test-wasm-persistence.js diff --git a/crates/sona/src/napi.rs b/crates/sona/src/napi.rs index 7ecd52937a..af0abd6c65 100644 --- a/crates/sona/src/napi.rs +++ b/crates/sona/src/napi.rs @@ -78,7 +78,10 @@ impl SonaEngine { #[napi] pub fn apply_micro_lora(&self, input: Vec) -> Vec { let input_f32: Vec = input.iter().map(|&x| x as f32).collect(); - let mut output = vec![0.0f32; input_f32.len()]; + // The Rust LoRA forward pass has residual semantics (it adds the + // learned delta into `output`), so `output` must start as a clone + // of `input` — not zeros — or callers only ever see the delta. + let mut output = input_f32.clone(); self.inner.apply_micro_lora(&input_f32, &mut output); output.iter().map(|&x| x as f64).collect() } @@ -90,7 +93,8 @@ impl SonaEngine { #[napi] pub fn apply_base_lora(&self, layer_idx: u32, input: Vec) -> Vec { let input_f32: Vec = input.iter().map(|&x| x as f32).collect(); - let mut output = vec![0.0f32; input_f32.len()]; + // Same residual semantics as apply_micro_lora — seed with input. + let mut output = input_f32.clone(); self.inner.apply_base_lora(layer_idx as usize, &input_f32, &mut output); output.iter().map(|&x| x as f64).collect() } diff --git a/crates/sona/src/napi_simple.rs b/crates/sona/src/napi_simple.rs index 2a4692e4db..cec5fb986a 100644 --- a/crates/sona/src/napi_simple.rs +++ b/crates/sona/src/napi_simple.rs @@ -142,7 +142,10 @@ impl SonaEngine { #[napi] pub fn apply_micro_lora(&self, input: Vec) -> Vec { let input_f32: Vec = input.iter().map(|&x| x as f32).collect(); - let mut output = vec![0.0f32; input_f32.len()]; + // The Rust LoRA forward pass has residual semantics (it adds the + // learned delta into `output`), so `output` must start as a clone + // of `input` — not zeros — or callers only ever see the delta. + let mut output = input_f32.clone(); self.inner.apply_micro_lora(&input_f32, &mut output); output.iter().map(|&x| x as f64).collect() } @@ -154,7 +157,8 @@ impl SonaEngine { #[napi] pub fn apply_base_lora(&self, layer_idx: u32, input: Vec) -> Vec { let input_f32: Vec = input.iter().map(|&x| x as f32).collect(); - let mut output = vec![0.0f32; input_f32.len()]; + // Same residual semantics as apply_micro_lora — seed with input. + let mut output = input_f32.clone(); self.inner .apply_base_lora(layer_idx as usize, &input_f32, &mut output); output.iter().map(|&x| x as f64).collect() diff --git a/crates/sona/src/wasm.rs b/crates/sona/src/wasm.rs index 60ed51e8cc..625ab6b490 100644 --- a/crates/sona/src/wasm.rs +++ b/crates/sona/src/wasm.rs @@ -249,7 +249,9 @@ impl WasmSonaEngine { /// ``` #[wasm_bindgen(js_name = applyLora)] pub fn apply_lora(&self, input: Vec) -> Vec { - let mut output = vec![0.0; input.len()]; + // Residual semantics: engine.apply_micro_lora adds the learned delta + // into `output`, so it must start as the input, not zeros. + let mut output = input.clone(); let engine = self.inner.read(); engine.apply_micro_lora(&input, &mut output); output @@ -265,7 +267,8 @@ impl WasmSonaEngine { /// Transformed vector as Float32Array #[wasm_bindgen(js_name = applyLoraLayer)] pub fn apply_lora_layer(&self, layer_idx: usize, input: Vec) -> Vec { - let mut output = vec![0.0; input.len()]; + // Same residual semantics as apply_lora — seed with input. + let mut output = input.clone(); let engine = self.inner.read(); engine.apply_base_lora(layer_idx, &input, &mut output); output diff --git a/docs/api/NODEJS_API.md b/docs/api/NODEJS_API.md index d441ea56ea..99e9ccf8a1 100644 --- a/docs/api/NODEJS_API.md +++ b/docs/api/NODEJS_API.md @@ -607,10 +607,21 @@ interface QuantizationConfig { ## Advanced Features -### HybridSearch +> **Not yet available in the `ruvector` Node package.** `HybridSearch`, +> `FilteredSearch`, and `MMRSearch` below exist in the Rust core +> (`ruvector-core::advanced_features`) but are **not bound or exported** by +> `crates/ruvector-node` / the published `ruvector` npm package — see +> [#707](https://github.com/ruvnet/RuVector/issues/707). Importing any of +> these names from `ruvector` in Node.js currently returns `undefined`. +> If you need hybrid/keyword search from JavaScript today, use AgentDB's +> `HybridSearch` / `KeywordIndex` (`ruvnet/agentdb`) instead. The snippets +> below describe the intended shape of a future Node binding, not a +> currently shipped API. + +### HybridSearch (Rust-only; not exported to Node) ```javascript -const { HybridSearch } = require('ruvector'); +const { HybridSearch } = require('ruvector'); // NOT YET SHIPPED — see #707 const hybrid = new HybridSearch(db, { vectorWeight: 0.7, @@ -626,10 +637,10 @@ const results = await hybrid.search( ); ``` -### FilteredSearch +### FilteredSearch (Rust-only; not exported to Node) ```javascript -const { FilteredSearch } = require('ruvector'); +const { FilteredSearch } = require('ruvector'); // NOT YET SHIPPED — see #707 const filtered = new FilteredSearch(db, 'preFilter'); @@ -641,10 +652,10 @@ const results = await filtered.search(queryVector, 10, { }); ``` -### MMRSearch +### MMRSearch (Rust-only; not exported to Node) ```javascript -const { MMRSearch } = require('ruvector'); +const { MMRSearch } = require('ruvector'); // NOT YET SHIPPED — see #707 const mmr = new MMRSearch(db, { lambda: 0.5, diff --git a/docs/guides/ADVANCED_FEATURES.md b/docs/guides/ADVANCED_FEATURES.md index fe943c20aa..9070dd3ca8 100644 --- a/docs/guides/ADVANCED_FEATURES.md +++ b/docs/guides/ADVANCED_FEATURES.md @@ -58,8 +58,14 @@ fn hybrid_search_example(db: &VectorDB) -> Result<(), Box ### Node.js +> **Not yet available in the `ruvector` Node package** — `HybridSearch` is +> implemented in `ruvector-core` but is not bound/exported by +> `crates/ruvector-node` yet ([#707](https://github.com/ruvnet/RuVector/issues/707)). +> `require('ruvector').HybridSearch` is `undefined` today; use AgentDB's +> `HybridSearch`/`KeywordIndex` from JavaScript in the meantime. + ```javascript -const { HybridSearch } = require('ruvector'); +const { HybridSearch } = require('ruvector'); // NOT YET SHIPPED — see #707 const hybrid = new HybridSearch(db, { vectorWeight: 0.7, @@ -153,8 +159,14 @@ FilterExpression::Not(Box::new(expr)) ### Node.js +> **Not yet available in the `ruvector` Node package** — `FilteredSearch` is +> implemented in `ruvector-core` but is not bound/exported by +> `crates/ruvector-node` yet ([#707](https://github.com/ruvnet/RuVector/issues/707)). +> `require('ruvector').FilteredSearch` is `undefined` today. `VectorDB.search()`'s +> built-in `filter` option covers basic pre-filtering in the meantime. + ```javascript -const { FilteredSearch } = require('ruvector'); +const { FilteredSearch } = require('ruvector'); // NOT YET SHIPPED — see #707 const filtered = new FilteredSearch(db, 'preFilter'); @@ -203,8 +215,13 @@ fn mmr_example(db: &VectorDB) -> Result<(), Box> { ### Node.js +> **Not yet available in the `ruvector` Node package** — `MMRSearch` is +> implemented in `ruvector-core` but is not bound/exported by +> `crates/ruvector-node` yet ([#707](https://github.com/ruvnet/RuVector/issues/707)). +> `require('ruvector').MMRSearch` is `undefined` today. + ```javascript -const { MMRSearch } = require('ruvector'); +const { MMRSearch } = require('ruvector'); // NOT YET SHIPPED — see #707 const mmr = new MMRSearch(db, { lambda: 0.5, @@ -322,8 +339,14 @@ fn conformal_prediction_example() -> Result<(), Box> { ### Node.js +> **Not yet available in the `ruvector` Node package** — `ConformalPredictor` +> is implemented in `ruvector-core` but is not bound/exported by +> `crates/ruvector-node` yet (same gap as [#707](https://github.com/ruvnet/RuVector/issues/707), +> which covers `HybridSearch`/`FilteredSearch`/`MMRSearch`). +> `require('ruvector').ConformalPredictor` is `undefined` today. + ```javascript -const { ConformalPredictor } = require('ruvector'); +const { ConformalPredictor } = require('ruvector'); // NOT YET SHIPPED const predictor = new ConformalPredictor({ alpha: 0.1, // 90% confidence diff --git a/examples/rvf/scripts/rvf-browser.html b/examples/rvf/scripts/rvf-browser.html index 7c8b3c5c02..b08b7f7688 100644 --- a/examples/rvf/scripts/rvf-browser.html +++ b/examples/rvf/scripts/rvf-browser.html @@ -29,11 +29,9 @@

RVF Browser Example

# or load from CDN (see script below)