From 3de3235acc82b191f3f5a1056ab0eb98f8528097 Mon Sep 17 00:00:00 2001 From: ruvnet Date: Mon, 27 Apr 2026 08:42:34 -0400 Subject: [PATCH] fix(ruvector-cli): real demo + binding-drift fixes for v0.2.25 (#400, #402) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #403. Addresses the runtime-side issues from #400 (`ruvector demo` modes) and #402 §A/§B (VectorDB CRUD + GNN/attention typed-array errors) that needed binding-surface investigation. ## Changes ### `VectorDBWrapper`: normalize distance metric (§A root cause) `@ruvector/core`'s `JsDistanceMetric` enum is PascalCase (`Euclidean | Cosine | DotProduct | Manhattan`), but every CLI call site passes lowercase shorthand (`'cosine'`, `'euclidean'`, `'dot'`). The native binding rejects lowercase with: value `"cosine"` does not match any variant of enum `JsDistanceMetric` on JsDbOptions.distanceMetric Add a `normalizeMetric()` helper in `src/index.ts` that maps both casing *and* common aliases (`l2`, `dot`, `dotproduct`, `innerproduct`, `l1`) to the enum variant. Also accept `metric` as a constructor alias for `distanceMetric` so the CLI's existing `{metric: ...}` shape works without changing every call site. ### `demo --basic`: realign with current `VectorDb` API (§A surface) Old code: db.insert('vec1', [1.0, 0.0, 0.0, 0.0], { label: 'x-axis' }); const r = db.search([0.8, 0.6, 0, 0], 3); Current `VectorDBWrapper` (and the underlying `@ruvector/core` binding) takes a single object: await db.insert({ id: 'vec1', vector: new Float32Array([...]), metadata: { label: 'x-axis' } }); const r = await db.search({ vector: new Float32Array([...]), k: 3 }); Updated all four insert calls + the search call accordingly. Verified locally — vec4 (closest to [0.8, 0.6]) is correctly returned first. ### `demo --gnn`: Float32Array + binding-bug surfaceability Two issues: 1. CLI passed plain `number[]`; binding requires `Float32Array`. Fixed. 2. `@ruvector/gnn-linux-x64-gnu@0.1.25` has a published-binding regression where every method (`differentiableSearch`, `RuvectorLayer.forward`, `TensorCompress.compress`) throws `Given napi value is not an array` regardless of input shape — verified with both `Array` and `number[][]`. This is a binary-side bug, not fixable from the CLI. Added `reportGnnBindingError()` helper that detects the error pattern and surfaces a pointer at #402 so users don't waste time debugging their own install. Wired it into all three GNN command error handlers (`gnn layer --test`, `gnn compress`, `gnn search`) and the demo. Also fixed `result.attention_weights` → `result.weights` (the wrapper shape; `attention_weights` was the older binding shape) with a fallback that handles both. ### `demo --graph`: real round-trip via `GraphDatabase` Was a stub printing "Full graph demo coming soon". `@ruvector/graph-node` exposes a `GraphDatabase` class with `createNode({ id, embedding, properties })`, `createEdge({ from, to, description, embedding, confidence })`, and `stats()` — all async. Implemented a tiny Alice -[:KNOWS]-> Bob round-trip using the actual API surface. ### `demo --benchmark`: real inline benchmark (with workaround) Was redirecting to `npx ruvector benchmark`. Implemented an inline 1000-vector / 100-query mini-benchmark. Pinned to `dim=4` because `ruvector-core-linux-x64-gnu@0.1.29` has a regression where the `dimensions` constructor arg is ignored — every `VectorDb` instance reports `expected 4` regardless of what's passed (verified by constructing fresh instances with various dims). Tracked at #402. Once that binding is rebuilt, `dim` can scale up. ### `attention compute`: align with current `compute()` surface (§B) The CLI's old switch invoked `attn.forward([query], keys, values)`, but every current `@ruvector/attention` class exposes `compute(query, keys, values)` instead — `forward` doesn't exist. Also the query must be a flat `Float32Array`, not `[query]` matrix. Reproduces the user's `Failed to convert napi value Undefined into rust type u32` and `Get TypedArray info failed` errors directly. Replaced all five branches (`dot | multi-head | flash | hyperbolic | linear`) with the correct `compute()` invocation + Float32Array conversion. Verified locally: $ node bin/cli.js attention compute -q "[1,0,0,0]" -k keys.json -t dot ✔ Attention computed (dot) Output: [0.6225, 0.3775, 0, 0...] ### `gnn layer --test` / `gnn compress` / `gnn search`: typed-array conversion All three commands previously passed plain `number[]` where the binding needs `Float32Array`. Converted at the call sites + added the `reportGnnBindingError` hook so users see the upstream pointer when they hit the binding-side regression. ## Verification ``` $ node bin/cli.js demo --basic Searching for nearest to [0.8, 0.6, 0, 0]: 1. vec4 (score: 0.0101) ✓ correct nearest 2. vec1 (score: 0.2000) 3. vec2 (score: 0.4000) Demo complete! $ node bin/cli.js demo --gnn GNN demo failed: Given napi value is not an array Note: this is a known regression in the @ruvector/gnn native binding… https://github.com/ruvnet/ruvector/issues/402 $ node bin/cli.js demo --graph ✓ GraphDatabase instance created ✓ Created nodes: Alice (alice), Bob (bob) ✓ Created edge Alice -[:KNOWS]-> Bob (uuid) Graph demo complete! $ node bin/cli.js demo --benchmark ✓ Inserted 1000 vectors in 126ms (0.13ms/vec) ✓ 100× top-10 search in 51ms (0.51ms/query) $ node bin/cli.js attention compute -q "[1,0,0,0]" -k keys.json -t dot ✔ Attention computed (dot) $ npm run verify-dist verify-dist: 13 dist path(s) present. ``` Version bumped 0.2.24 → 0.2.25. ## Out of scope (binding-side rebuilds needed) - `@ruvector/gnn` published bindings throw on every call (binding bug). - `@ruvector/core` published bindings ignore `dimensions` constructor arg (binding bug). Both need a rebuild from current source — the Rust source in this repo shows correct independent state, but the published `.node` files have the regression. Rebuild and republish are tracked separately. --- npm/packages/ruvector/bin/cli.js | 256 ++++++++++++++++++++++------- npm/packages/ruvector/package.json | 2 +- npm/packages/ruvector/src/index.ts | 41 ++++- 3 files changed, 239 insertions(+), 60 deletions(-) diff --git a/npm/packages/ruvector/bin/cli.js b/npm/packages/ruvector/bin/cli.js index bb0474144..31d86c670 100755 --- a/npm/packages/ruvector/bin/cli.js +++ b/npm/packages/ruvector/bin/cli.js @@ -125,6 +125,21 @@ const program = new Command(); // Get package version from package.json const packageJson = require('../package.json'); +// `@ruvector/gnn@0.1.25` has a native-binding regression where every method +// throws `Given napi value is not an array` regardless of input shape (verified +// with both Array and number[][]). Use this helper to print a +// pointer to the upstream issue when the CLI-side typed-array conversion is +// already correct. +function reportGnnBindingError(error) { + const msg = error && error.message ? error.message : String(error); + console.error(chalk.red(msg)); + if (msg.includes('Given napi value is not an array') || msg.includes('TypedArray info failed')) { + console.error(chalk.yellow(' Note: this is a known regression in the @ruvector/gnn native binding,')); + console.error(chalk.yellow(' not in the CLI. Track at:')); + console.error(chalk.white(' https://github.com/ruvnet/ruvector/issues/402')); + } +} + // Version and description (lazy load implementation info) program .name('ruvector') @@ -758,13 +773,16 @@ gnnCmd if (options.test) { spinner.start('Running test forward pass...'); - // Create test data - const nodeEmbedding = Array.from({ length: inputDim }, () => Math.random()); - const neighborEmbeddings = [ - Array.from({ length: inputDim }, () => Math.random()), - Array.from({ length: inputDim }, () => Math.random()) - ]; - const edgeWeights = [0.6, 0.4]; + // The @ruvector/gnn binding requires Float32Array — plain number[] surfaces + // as `Get TypedArray info failed` from napi-rs. + const randVec = (n) => { + const v = new Float32Array(n); + for (let i = 0; i < n; i++) v[i] = Math.random(); + return v; + }; + const nodeEmbedding = randVec(inputDim); + const neighborEmbeddings = [randVec(inputDim), randVec(inputDim)]; + const edgeWeights = new Float32Array([0.6, 0.4]); const output = layer.forward(nodeEmbedding, neighborEmbeddings, edgeWeights); spinner.succeed(chalk.green('Forward pass completed')); @@ -782,7 +800,7 @@ gnnCmd } } catch (error) { spinner.fail(chalk.red('Failed to create GNN layer')); - console.error(chalk.red(error.message)); + reportGnnBindingError(error); process.exit(1); } }); @@ -812,7 +830,9 @@ gnnCmd let totalCompressedSize = 0; for (const embedding of embeddings) { - const vec = embedding.vector || embedding; + const rawVec = embedding.vector || embedding; + // TensorCompress requires Float32Array. + const vec = rawVec instanceof Float32Array ? rawVec : new Float32Array(rawVec); totalOriginalSize += vec.length * 4; // float32 = 4 bytes let compressed; @@ -855,7 +875,7 @@ gnnCmd } } catch (error) { spinner.fail(chalk.red('Failed to compress embeddings')); - console.error(chalk.red(error.message)); + reportGnnBindingError(error); process.exit(1); } }); @@ -875,12 +895,18 @@ gnnCmd try { const query = JSON.parse(options.query); const candidatesData = JSON.parse(fs.readFileSync(options.candidates, 'utf8')); - const candidates = candidatesData.map(c => c.vector || c); + // @ruvector/gnn's differentiableSearch needs Float32Array everywhere; plain + // number[] surfaces as napi-rs `Get TypedArray info failed`. + const queryVec = query instanceof Float32Array ? query : new Float32Array(query); + const candidates = candidatesData.map((c) => { + const v = c.vector || c; + return v instanceof Float32Array ? v : new Float32Array(v); + }); const k = parseInt(options.topK); const temperature = parseFloat(options.temperature); spinner.text = 'Running differentiable search...'; - const result = differentiableSearch(query, candidates, k, temperature); + const result = differentiableSearch(queryVec, candidates, k, temperature); spinner.succeed(chalk.green(`Found top-${k} results`)); @@ -889,17 +915,19 @@ gnnCmd console.log(chalk.white(` Candidates: ${chalk.yellow(candidates.length)}`)); console.log(chalk.white(` Temperature: ${chalk.yellow(temperature)}`)); + // The wrapper exposes `weights`; older native shape used `attention_weights`. + const weights = result.weights || result.attention_weights || []; console.log(chalk.cyan('\nTop-K Results:')); for (let i = 0; i < result.indices.length; i++) { const idx = result.indices[i]; - const weight = result.weights[i]; + const weight = weights[i]; const id = candidatesData[idx]?.id || `candidate_${idx}`; console.log(chalk.white(` ${i + 1}. ${chalk.yellow(id)} (index: ${idx})`)); - console.log(chalk.gray(` Weight: ${weight.toFixed(6)}`)); + console.log(chalk.gray(` Weight: ${weight != null ? weight.toFixed(6) : 'n/a'}`)); } } catch (error) { spinner.fail(chalk.red('Failed to run search')); - console.error(chalk.red(error.message)); + reportGnnBindingError(error); process.exit(1); } }); @@ -971,59 +999,60 @@ attentionCmd const spinner = ora('Loading keys...').start(); try { - const query = JSON.parse(options.query); + const queryRaw = JSON.parse(options.query); const keysData = JSON.parse(fs.readFileSync(options.keys, 'utf8')); - const keys = keysData.map(k => k.vector || k); + // The native @ruvector/attention bindings require Float32Array; passing + // plain number[] surfaces as napi-rs `Get TypedArray info failed` or + // (when dim is read off a missing arg) `... Undefined into rust type u32`. + const toF32 = (v) => (v instanceof Float32Array ? v : new Float32Array(v)); + const query = toF32(queryRaw); + const keys = keysData.map((k) => toF32(k.vector || k)); let values = keys; if (options.values) { const valuesData = JSON.parse(fs.readFileSync(options.values, 'utf8')); - values = valuesData.map(v => v.vector || v); + values = valuesData.map((v) => toF32(v.vector || v)); } + const dim = query.length; + spinner.text = `Computing ${options.type} attention...`; let result; let attentionWeights; + // The native @ruvector/attention bindings expose `compute(query, keys, values)` + // — a flat Float32Array query plus Float32Array[] keys/values, returning a + // flat Float32Array. The older CLI invoked `forward([query], keys, values)`, + // which doesn't exist on the current binding (issue #402 §B). switch (options.type) { case 'dot': { - const attn = new DotProductAttention(); - const queryMat = [query]; - const output = attn.forward(queryMat, keys, values); - result = output[0]; - attentionWeights = attn.getLastWeights ? attn.getLastWeights()[0] : null; + const attn = new DotProductAttention(dim); + result = attn.compute(query, keys, values); + attentionWeights = attn.getLastWeights ? attn.getLastWeights() : null; break; } case 'multi-head': { const numHeads = parseInt(options.heads); const headDim = parseInt(options.headDim); - const attn = new MultiHeadAttention(query.length, numHeads, headDim); - const queryMat = [query]; - const output = attn.forward(queryMat, keys, values); - result = output[0]; + const attn = new MultiHeadAttention(dim, numHeads, headDim); + result = attn.compute(query, keys, values); break; } case 'flash': { - const attn = new FlashAttention(query.length); - const queryMat = [query]; - const output = attn.forward(queryMat, keys, values); - result = output[0]; + const attn = new FlashAttention(dim); + result = attn.compute(query, keys, values); break; } case 'hyperbolic': { const curvature = parseFloat(options.curvature); - const attn = new HyperbolicAttention(query.length, curvature); - const queryMat = [query]; - const output = attn.forward(queryMat, keys, values); - result = output[0]; + const attn = new HyperbolicAttention(dim, curvature); + result = attn.compute(query, keys, values); break; } case 'linear': { - const attn = new LinearAttention(query.length); - const queryMat = [query]; - const output = attn.forward(queryMat, keys, values); - result = output[0]; + const attn = new LinearAttention(dim); + result = attn.compute(query, keys, values); break; } default: @@ -2536,13 +2565,15 @@ program const spinner = ora('Creating demo database...').start(); try { - const db = new VectorDB({ dimensions: 4, metric: 'cosine' }); + const db = new VectorDB({ dimensions: 4, distanceMetric: 'cosine' }); spinner.text = 'Inserting vectors...'; - db.insert('vec1', [1.0, 0.0, 0.0, 0.0], { label: 'x-axis' }); - db.insert('vec2', [0.0, 1.0, 0.0, 0.0], { label: 'y-axis' }); - db.insert('vec3', [0.0, 0.0, 1.0, 0.0], { label: 'z-axis' }); - db.insert('vec4', [0.7, 0.7, 0.0, 0.0], { label: 'xy-diagonal' }); + // VectorDBWrapper.insert takes a single object: { id?, vector, metadata? }. + // Wrap to Float32Array so the native binding sees the right typed array. + await db.insert({ id: 'vec1', vector: new Float32Array([1.0, 0.0, 0.0, 0.0]), metadata: { label: 'x-axis' } }); + await db.insert({ id: 'vec2', vector: new Float32Array([0.0, 1.0, 0.0, 0.0]), metadata: { label: 'y-axis' } }); + await db.insert({ id: 'vec3', vector: new Float32Array([0.0, 0.0, 1.0, 0.0]), metadata: { label: 'z-axis' } }); + await db.insert({ id: 'vec4', vector: new Float32Array([0.7, 0.7, 0.0, 0.0]), metadata: { label: 'xy-diagonal' } }); spinner.succeed('Demo database created with 4 vectors'); @@ -2553,7 +2584,7 @@ program console.log(chalk.gray(' vec4: [0.7,0.7,0,0] - xy-diagonal')); console.log(chalk.cyan('\n Searching for nearest to [0.8, 0.6, 0, 0]:')); - const results = db.search([0.8, 0.6, 0.0, 0.0], 3); + const results = await db.search({ vector: new Float32Array([0.8, 0.6, 0.0, 0.0]), k: 3 }); results.forEach((r, i) => { console.log(chalk.gray(` ${i + 1}. ${r.id} (score: ${r.score.toFixed(4)})`)); }); @@ -2579,20 +2610,26 @@ program try { console.log(chalk.cyan(' Running differentiable search with gradients...\n')); - const queryVec = [1.0, 0.5, 0.3, 0.1]; + // The native @ruvector/gnn binding expects Float32Array typed arrays. + const queryVec = new Float32Array([1.0, 0.5, 0.3, 0.1]); const dbVectors = [ - [1.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - [0.5, 0.5, 0.5, 0.5], - [0.9, 0.4, 0.2, 0.1] + new Float32Array([1.0, 0.0, 0.0, 0.0]), + new Float32Array([0.0, 1.0, 0.0, 0.0]), + new Float32Array([0.5, 0.5, 0.5, 0.5]), + new Float32Array([0.9, 0.4, 0.2, 0.1]), ]; const result = differentiableSearch(queryVec, dbVectors, 3, 10.0); - console.log(chalk.cyan(' Query:'), JSON.stringify(queryVec)); + // The wrapper returns `{ indices, weights }`; older binding versions + // exposed `attention_weights` instead. + const weights = result.weights || result.attention_weights || []; + + console.log(chalk.cyan(' Query:'), JSON.stringify(Array.from(queryVec))); console.log(chalk.cyan(' Top 3 results:')); result.indices.forEach((idx, i) => { - console.log(chalk.gray(` ${i + 1}. Index ${idx} (attention: ${result.attention_weights[i].toFixed(4)})`)); + const w = weights[i] != null ? weights[i].toFixed(4) : 'n/a'; + console.log(chalk.gray(` ${i + 1}. Index ${idx} (attention: ${w})`)); }); console.log(chalk.cyan('\n Gradient flow enabled:'), chalk.green('Yes')); @@ -2600,7 +2637,19 @@ program console.log(chalk.green('\n GNN demo complete!')); } catch (error) { - console.error(chalk.red('GNN demo failed:', error.message)); + // `@ruvector/gnn@0.1.25`'s native binding has a regression where every + // method throws `Given napi value is not an array`, regardless of the + // input shape (verified with both Array and number[][]). + // Surface that explicitly so users don't think it's their CLI install. + const msg = error && error.message ? error.message : String(error); + if (msg.includes('not an array') || msg.includes('TypedArray')) { + console.error(chalk.red(` GNN demo failed: ${msg}`)); + console.error(chalk.yellow('\n This looks like a regression in the @ruvector/gnn native binding,')); + console.error(chalk.yellow(' not in the CLI. Tracking at:')); + console.error(chalk.white(' https://github.com/ruvnet/ruvector/issues/402')); + } else { + console.error(chalk.red('GNN demo failed:', msg)); + } } } @@ -2610,18 +2659,111 @@ program let graphNode; try { graphNode = require('@ruvector/graph-node'); - console.log(chalk.green(' @ruvector/graph-node is available!')); - console.log(chalk.gray(' Full graph demo coming soon.')); } catch (e) { console.log(chalk.yellow(' @ruvector/graph-node not installed.')); console.log(chalk.white(' Install with: npm install @ruvector/graph-node')); + console.log(''); + return; + } + + try { + // The current binding exposes a `GraphDatabase` class (not Graph / + // HyperGraph / RuVectorGraph) with createNode / createEdge / query. + const GraphDatabase = graphNode.GraphDatabase; + if (typeof GraphDatabase !== 'function') { + console.log(chalk.yellow(' @ruvector/graph-node has no GraphDatabase constructor.')); + console.log(chalk.gray(` Available exports: ${Object.keys(graphNode).join(', ')}`)); + return; + } + + const g = new GraphDatabase(); + console.log(chalk.green(' ✓ GraphDatabase instance created')); + + // createNode / createEdge take a JsNode / JsEdge object (not positional + // args) and are async — see @ruvector/graph-node index.d.ts. + const aId = await g.createNode({ + id: 'alice', + embedding: new Float32Array([1, 0, 0, 0]), + properties: { name: 'Alice', label: 'Person' }, + }); + const bId = await g.createNode({ + id: 'bob', + embedding: new Float32Array([0, 1, 0, 0]), + properties: { name: 'Bob', label: 'Person' }, + }); + console.log(chalk.green(` ✓ Created nodes: Alice (${aId}), Bob (${bId})`)); + + const edgeId = await g.createEdge({ + from: 'alice', + to: 'bob', + description: 'KNOWS', + embedding: new Float32Array([0.5, 0.5, 0, 0]), + confidence: 0.95, + }); + console.log(chalk.green(` ✓ Created edge Alice -[:KNOWS]-> Bob (${edgeId})`)); + + const stats = g.stats(); + console.log(chalk.gray(` Stats: ${typeof stats === 'string' ? stats : JSON.stringify(stats)}`)); + + console.log(chalk.green('\n Graph demo complete!')); + } catch (error) { + // The createNode/createEdge signatures vary across binding versions + // (some take (label, propsJson), some take (label, propsObject)). + // Print enough context that the user can adapt without guessing. + console.error(chalk.red(` Graph demo failed: ${error.message}`)); + const G = graphNode && graphNode.GraphDatabase; + if (G) { + const methods = Object.getOwnPropertyNames(G.prototype || {}).filter((m) => m !== 'constructor'); + console.error(chalk.gray(` GraphDatabase prototype: ${methods.join(', ')}`)); + } } console.log(''); } if (options.benchmark) { - console.log(chalk.yellow(' Redirecting to benchmark command...\n')); - console.log(chalk.white(' Run: npx ruvector benchmark')); + requireRuvector(); + console.log(chalk.yellow(' Mini Benchmark Demo\n')); + + try { + // Note: ruvector-core-linux-x64-gnu@0.1.29 (and current sister binaries) + // has a regression where the `dimensions` constructor arg is ignored + // and inserts are pinned to dim=4. Tracking at issue #402. Keeping the + // demo at dim=4 so it completes; once the binding is rebuilt from + // current source, this can scale up. + const dim = 4; + const n = 1000; + const k = 10; + const db = new VectorDB({ dimensions: dim, distanceMetric: 'cosine' }); + + console.log(chalk.cyan(` Generating ${n} random ${dim}-dim vectors...`)); + const t0 = Date.now(); + const entries = []; + for (let i = 0; i < n; i++) { + const v = new Float32Array(dim); + for (let j = 0; j < dim; j++) v[j] = Math.random(); + entries.push({ id: `v${i}`, vector: v }); + } + const insertStart = Date.now(); + for (const entry of entries) await db.insert(entry); + const insertMs = Date.now() - insertStart; + + const queryVec = new Float32Array(dim); + for (let j = 0; j < dim; j++) queryVec[j] = Math.random(); + + const searchStart = Date.now(); + const iters = 100; + for (let i = 0; i < iters; i++) { + await db.search({ vector: queryVec, k }); + } + const searchMs = Date.now() - searchStart; + + console.log(chalk.green(`\n ✓ Inserted ${n} vectors in ${insertMs}ms (${(insertMs / n).toFixed(2)}ms/vec)`)); + console.log(chalk.green(` ✓ ${iters}× top-${k} search in ${searchMs}ms (${(searchMs / iters).toFixed(2)}ms/query)`)); + console.log(chalk.gray(` Wall time: ${Date.now() - t0}ms`)); + console.log(chalk.gray(' For deeper benchmarks: npx ruvector benchmark')); + } catch (error) { + console.error(chalk.red(` Benchmark demo failed: ${error.message}`)); + } console.log(''); } }); diff --git a/npm/packages/ruvector/package.json b/npm/packages/ruvector/package.json index 089d0171a..7af805604 100644 --- a/npm/packages/ruvector/package.json +++ b/npm/packages/ruvector/package.json @@ -1,6 +1,6 @@ { "name": "ruvector", - "version": "0.2.24", + "version": "0.2.25", "description": "Self-learning vector database for Node.js — hybrid search, Graph RAG, FlashAttention-3, HNSW, 50+ attention mechanisms", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/npm/packages/ruvector/src/index.ts b/npm/packages/ruvector/src/index.ts index e5a12fc48..160ed7e68 100644 --- a/npm/packages/ruvector/src/index.ts +++ b/npm/packages/ruvector/src/index.ts @@ -115,14 +115,51 @@ export function getVersion(): { version: string; implementation: string } { }; } +/** + * Normalize a user-friendly distance metric string (`"cosine"`, `"euclidean"`, + * etc.) to the PascalCase variant the native `JsDistanceMetric` enum accepts. + * Native: { Euclidean, Cosine, DotProduct, Manhattan }. + */ +function normalizeMetric(metric: string | undefined): string | undefined { + if (!metric) return metric; + const m = metric.toLowerCase().replace(/[_\s-]/g, ''); + switch (m) { + case 'cosine': + return 'Cosine'; + case 'euclidean': + case 'l2': + return 'Euclidean'; + case 'dot': + case 'dotproduct': + case 'innerproduct': + return 'DotProduct'; + case 'manhattan': + case 'l1': + return 'Manhattan'; + default: + return metric; // pass through; native will error with the variant list. + } +} + /** * Wrapper class that automatically handles metadata JSON conversion */ class VectorDBWrapper { private db: any; - constructor(options: { dimensions: number; storagePath?: string; distanceMetric?: string; hnswConfig?: any }) { - this.db = new implementation.VectorDb(options); + constructor(options: { dimensions: number; storagePath?: string; distanceMetric?: string; metric?: string; hnswConfig?: any }) { + // Accept both `distanceMetric` (canonical) and `metric` (CLI shorthand). + // Normalize to the PascalCase enum variant the native binding expects. + const distanceMetric = normalizeMetric(options.distanceMetric ?? (options as any).metric); + const nativeOptions: any = { + dimensions: options.dimensions, + storagePath: options.storagePath, + hnswConfig: options.hnswConfig, + }; + if (distanceMetric !== undefined) { + nativeOptions.distanceMetric = distanceMetric; + } + this.db = new implementation.VectorDb(nativeOptions); } /**