Merge remote-tracking branch 'origin/main' into agent/fix-pr-723

This commit is contained in:
ruvnet 2026-07-27 12:40:01 -04:00
commit 6dfc47a17e
26 changed files with 3767 additions and 4848 deletions

View file

@ -62,8 +62,11 @@ jobs:
set +e
./node_modules/.bin/tsc 2>&1 | tail -5
TSC_EXIT=$?
mkdir -p dist/core/onnx/pkg
cp -r src/core/onnx/pkg/. dist/core/onnx/pkg/ 2>/dev/null || true
# TypeScript does not emit the JavaScript/MJS/WASM assets consumed by
# the ONNX contract tests and published package. Mirror the package
# build script by copying the complete runtime directory.
mkdir -p dist/core/onnx
cp -r src/core/onnx/. dist/core/onnx/
if [ ! -f dist/index.js ]; then
echo "::error::dist/index.js missing — build truly failed (not just pre-existing TS errors)"
exit 1
@ -95,7 +98,10 @@ jobs:
node-version: '20'
- name: Install
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --no-optional
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --include=optional
- name: Verify native backend is installed
run: node -e "require('ruvector-core-linux-x64-gnu')"
- name: Download dist
uses: actions/download-artifact@v4
@ -140,7 +146,10 @@ jobs:
node-version: '20'
- name: Install
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --no-optional
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --include=optional
- name: Verify native backend is installed
run: node -e "require('ruvector-core-linux-x64-gnu')"
- name: Download dist
uses: actions/download-artifact@v4
@ -237,7 +246,10 @@ jobs:
node-version: '20'
- name: Install
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --no-optional
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --include=optional
- name: Verify native backend is installed
run: node -e "require('ruvector-core-linux-x64-gnu')"
- name: Download dist
uses: actions/download-artifact@v4
@ -352,7 +364,10 @@ jobs:
node-version: '20'
- name: Install
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --no-optional
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --include=optional
- name: Verify native backend is installed
run: node -e "require('ruvector-core-linux-x64-gnu')"
- name: Download dist
uses: actions/download-artifact@v4
@ -460,7 +475,10 @@ jobs:
node-version: '20'
- name: Install
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --no-optional
run: npm install --no-audit --no-fund --legacy-peer-deps --no-workspaces --include=optional
- name: Verify native backend is installed
run: node -e "require('ruvector-core-linux-x64-gnu')"
- name: Download dist
uses: actions/download-artifact@v4
@ -659,6 +677,8 @@ jobs:
- tarball-integrity
if: always()
steps:
- uses: actions/checkout@v4
- name: Check all jobs passed
run: |
results='${{ toJSON(needs) }}'

View file

@ -58,10 +58,14 @@ jobs:
name: dependency-review (PRs only)
runs-on: ubuntu-22.04
if: github.event_name == 'pull_request'
continue-on-error: true
steps:
- uses: actions/checkout@v4
- name: dependency-review-action
# The repository dependency graph is not currently exposed by GitHub,
# so the action returns "Dependency review is not supported". Keep the
# other four supply-chain layers hard-gated while #689 tracks enabling
# this service-side prerequisite.
continue-on-error: true
uses: actions/dependency-review-action@v4
with:
fail-on-severity: moderate

View file

@ -5,6 +5,36 @@ All notable changes to RuVector will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [ruvector npm 0.2.37] - 2026-07-27
### Fixed
- Restore the release CI's native backend installation so functional,
learning, performance, and recall gates exercise HNSW instead of the
in-memory fallback.
- Preserve the Rust core's default HNSW behavior when JavaScript callers omit
`hnswConfig`; the wrapper previously selected the exact FlatIndex path.
- Normalize the package binary metadata and its integration assertion.
- Remove the npm workspace's critical `tar` advisory with a patched override.
- Correct the Shor factoring milestone table to distinguish a failed
35-factor experiment from the separate Schnorr/QAOA hybrid result.
## [ruvector npm 0.2.36] - 2026-07-26
### Fixed
- Start the stdio MCP transport when launched through `ruvector mcp start`
([#715](https://github.com/ruvnet/ruvector/issues/715)).
- Keep ONNX model-loader diagnostics on stderr so they cannot corrupt MCP
JSON-RPC frames ([#710](https://github.com/ruvnet/ruvector/issues/710)).
### Security
- Update `anyhow` to 1.0.104 and `memmap2` to 0.9.11, removing the
corresponding RustSec soundness exceptions.
- Update the MCP SDK to 1.29.x, clearing its shared-transport data-leak and
ReDoS advisories, and remove the unused `js-beautify` dependency.
## [hailo-backend] - 2026-05-03
Branch-only entry; not yet released to a versioned tag. 38 iters /

8
Cargo.lock generated
View file

@ -235,9 +235,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "approx"
@ -5371,9 +5371,9 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "memmap2"
version = "0.9.10"
version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3"
checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
dependencies = [
"libc",
"stable_deref_trait",

View file

@ -87,14 +87,20 @@ fn cmd_build(args: &[String]) {
.insert(id.to_string(), random_unit_vector(&mut rng, dim))
.expect("insert failed");
if id % 50_000 == 0 {
eprintln!("inserted {id}/{n} ({:.1}s elapsed)", t0.elapsed().as_secs_f64());
eprintln!(
"inserted {id}/{n} ({:.1}s elapsed)",
t0.elapsed().as_secs_f64()
);
}
}
eprintln!("insert phase done in {:.1}s", t0.elapsed().as_secs_f64());
let t1 = Instant::now();
index.build().expect("build failed"); // build() also calls save() since storage_path is set
eprintln!("build+save phase done in {:.1}s", t1.elapsed().as_secs_f64());
eprintln!(
"build+save phase done in {:.1}s",
t1.elapsed().as_secs_f64()
);
println!("BUILD_OK n={n} dim={dim} dir={}", dir.display());
}
@ -103,7 +109,9 @@ fn cmd_measure(args: &[String]) {
let dir = PathBuf::from(get_flag(args, "--dir", "/tmp/mmap674"));
let mode = get_flag(args, "--mode", "owned");
let dim: usize = get_flag(args, "--dim", "128").parse().expect("--dim");
let queries: usize = get_flag(args, "--queries", "1000").parse().expect("--queries");
let queries: usize = get_flag(args, "--queries", "1000")
.parse()
.expect("--queries");
let warmup: usize = get_flag(args, "--warmup", "50").parse().expect("--warmup");
let k: usize = get_flag(args, "--k", "10").parse().expect("--k");

View file

@ -123,19 +123,6 @@ ignore = [
# code, never reachable via untrusted input. Re-review 2026-08-01.
"RUSTSEC-2026-0192",
# memmap2 0.9.x — unsound `Unchecked pointer offset` API (RUSTSEC-2026-0186,
# "unsound", not an exploitable vuln). Transitive via safetensors/candle
# mmap loading and other crates; no fixed release on the 0.9 line yet.
# We don't pass attacker-controlled offsets to memmap2. Re-review 2026-08-01.
"RUSTSEC-2026-0186",
# anyhow 1.0.102 — unsoundness in `Error::downcast_mut()` (RUSTSEC-2026-0190,
# "unsound", not an exploitable vuln). Triggering it requires downcasting an
# `anyhow::Error` to a mismatched mutable type — a pattern we never use; our
# usage is construct/propagate/`Display`/read-only `downcast_ref`. Single
# version (1.0.102) in the tree. Bump to the patched anyhow once it ships and
# remove this entry. Re-review 2026-08-01.
"RUSTSEC-2026-0190",
]
# ─────────────────────────────────────────────────────────────────────

View file

@ -15,8 +15,8 @@
| Shor's original paper | 1994 | Theoretical | 0 |
| First experimental demo | 2001 | 15 = 3 × 5 | 7 (NMR) |
| Photonic factoring | 2012 | 21 = 3 × 7 | 10 |
| IBM superconducting | 2019 | 35 = 5 × 7 | 16 |
| Variational hybrid | 2023 | 261,980,999 (claim disputed) | 10 |
| IBM superconducting | 2019 | Attempted 35 (experiment did not factor it) | 16 |
| Schnorr/QAOA hybrid (not Shor) | 2023 | 261,980,999,226,229 | 10 |
| Current NISQ frontier | 2026 | ~1,000-10,000 range (noisy) | 50-100 |
| ruQu simulator | 2026 | ~32,767 (15-bit, clean sim) | 25 |

View file

@ -0,0 +1,163 @@
# RuVector SOTA, Security, and Package Review — 2026-07-26
## Scope and method
This review covers the complete tracked release surface at `main`
(`6a6c39e66`): 197 Rust workspace packages, 169 tracked npm manifests, 47
GitHub Actions workflows, and approximately 382,000 lines of Rust,
TypeScript, JavaScript, and native source under `crates/`, `npm/packages/`,
and `packages/`.
The review combined:
- manifest and release-workflow inventory;
- RustSec and `cargo-deny` dependency-graph checks;
- npm production-dependency audit;
- source-wide searches for unsafe Rust, process execution, unchecked panics,
path handling, protocol stdout, and release bypasses;
- targeted execution of the `ruvector` npm build, package verification, MCP
initialize handshake, signal cleanup, and policy tests;
- comparison with 20252026 work on filtered ANN, dynamic quantization,
compressed graph/ID storage, hybrid sparse+dense retrieval, and fresh
disk-backed ANN.
Raw pattern counts are triage signals, not vulnerability counts. Generated
bindings, tests, examples, and deliberately low-level SIMD/FFI code account
for much of the `unsafe`, `unwrap`, and process-execution footprint.
## Executive assessment
RuVector's algorithm portfolio is unusually broad and substantially aligned
with the current research frontier. The repository already contains HNSW,
DiskANN, SPANN, ACORN/filter-aware search, RaBitQ and other quantization,
hybrid BM25+dense fusion, sparse and multivector representations, GNN
reranking, Matryoshka evaluation, RVF persistence, Postgres integration,
WASM/N-API bindings, and dedicated SOTA benchmark runners.
The largest gap is not another ANN algorithm. It is productization across the
many implementations: durable metadata and filter semantics across every
binding, common recall/latency/memory benchmarks, streaming-update evaluation,
safe-by-default agent tool execution, and a release process that can prove
exactly what was built and published.
## Current SOTA alignment
| Area | RuVector coverage | Assessment |
|---|---|---|
| In-memory ANN | HNSW, learned and coherence variants, SIMD kernels | Strong |
| Disk / billion-scale ANN | DiskANN, SPANN, delta/LSM and repair research | Strong portfolio; needs one shared freshness benchmark |
| Filtered ANN | Filtered search and ACORN-family work | Algorithmically current; metadata durability and selectivity-aware planning remain the limiting integration gaps |
| Compression | PQ/scalar/binary paths, RaBitQ, Matryoshka runners | Strong; add graph-edge/vector-ID compression and streaming retraining measurements |
| Hybrid retrieval | BM25, sparse vectors, RRF/RSF/score fusion | Strong in Rust; Node's top-level `ruvector` API remains incomplete |
| Late interaction | Multivector support and reranking components | Present but not yet a single documented, benchmarked MaxSim product path |
| Freshness | Delta indexes, repair, LSM research, snapshot/raft components | Broad building blocks; no unified insert/delete/update SLO gate |
| Portability | Native Rust, N-API, WASM, Postgres, RVF | Excellent breadth; API parity is inconsistent |
| Evaluation | `ruvector-sota-bench`, VDBBench, MTEB and focused benchmarks | Good foundation; results need reproducible hardware/dataset manifests and regression budgets |
### Highest-value SOTA work
1. **Filtered ANN as a query-planning problem.** Choose exact scan, IVF,
graph traversal, or pre/inline/post-filtering from measured selectivity and
vector/filter correlation. Add difficult-filter datasets and recall
stability gates, not only unfiltered ANN recall.
2. **Fresh quantization under updates.** Measure quality drift after inserts,
deletes, and distribution shifts; trigger local codebook repair before a
global rebuild.
3. **Compress graph structure, not only vectors.** Vector IDs and HNSW/IVF
adjacency can dominate memory after aggressive vector quantization.
4. **One multistage retrieval contract.** Standardize dense+sparse candidate
generation, RRF/RSF fusion, optional MaxSim/GNN reranking, and provenance
of every score.
5. **Budget-aware serving.** Expose recall target, latency/evaluation budget,
and freshness target as the stable API; keep index-specific tuning internal.
## Security findings
### Fixed in this change
- **Critical availability — MCP launcher no-op (#715).** The CLI required
`mcp-server.js`, but that module only started when executed directly. The
module now exports `main()` and the CLI invokes it explicitly.
- **Protocol integrity — stdout corruption (#710).** All ONNX loader status
messages now use stderr; stdout remains JSON-RPC-only.
- **MCP command injection.** `workers_create` previously interpolated raw MCP
fields into `execSync`. It now uses `execFileSync` with an argument vector.
- **Rust soundness advisories.** `anyhow` is updated to 1.0.104 and `memmap2`
to 0.9.11; their RustSec exceptions are removed.
- **npm dependency exposure.** The MCP SDK is updated to 1.29.x and the unused
`js-beautify` dependency (and its vulnerable glob stack) is removed.
### Open risks requiring separate changes
1. **MCP defaults remain permissive.** With no environment policy, all tools
are exposed, including tools that mutate files or launch subprocesses.
Make `readonly` the default in the next semver-major release and require an
explicit profile for process-launching tools.
2. **Process execution is widespread.** Replace remaining `execSync` command
strings with `execFile`/`spawn` argument arrays, then enforce this with a
lint rule on MCP, CLI, deploy, and installer code.
3. **Release gates can report false success.** Several workflows append
`|| true` or `|| echo` to `npm publish`; several Cargo paths use
`--allow-dirty`. Publishing must fail closed and verify the registry
version, tarball digest, SBOM, provenance, and tag after upload.
4. **Workspace npm install is not reproducible on Linux.** `npm ci` currently
attempts to install a Darwin/ARM64 workspace package and exits with
`EBADPLATFORM`. Platform binaries need to be optional dependencies or
excluded from the root workspace install.
5. **The aggregate npm lock contains known advisories.** The workspace audit
reported 46 affected dependency nodes before package scoping. Triage by
reachable production package and update direct owners; do not hide the
aggregate result behind blanket audit exceptions.
6. **Post-quantum dependency maintenance.** RustSec now marks the PQClean-based
`pqcrypto-*` ecosystem unmaintained. Select and benchmark a maintained
ML-KEM/ML-DSA implementation before the next cryptography release.
7. **Residual dependency warnings.** `spin 0.9.8` is yanked but remains
transitive through `lazy_static`; the MCP SDK also carries a moderate Hono
Windows static-file advisory. RuVector's MCP package imports only the stdio
server path, so the Hono static-file handler is unreachable here, but the
dependency should still be upgraded as soon as the SDK accepts Hono 2.x.
8. **Unsafe/panic budgets are not centralized.** Add crate-level policy:
`unsafe` only in named FFI/SIMD modules with safety invariants, and no
`unwrap`/`expect` on production input paths.
## Package and API findings
- Issues #704#707 are already materially addressed on `main`: RVF rejects
unsupported metadata instead of dropping it, WASM byte export/open exists,
SONA N-API buffers preserve residual identity, and Node hybrid-search docs
disclose the unshipped API.
- RVF metadata still needs an end-to-end schema, durable segment storage,
reload, result retrieval, and identical filter typing across Rust, N-API,
WASM, and Postgres.
- `HybridSearch` remains Rust-only in the top-level Node package. Bind the
production Rust implementation rather than introducing another JavaScript
scoring implementation.
- The repository has many publishable manifests but release automation covers
only a subset. Generate a canonical package graph with owner, source,
registry, version, native platforms, test command, and publish workflow.
## Required release gates
Every published package should pass:
1. clean checkout and locked dependency install;
2. build, unit tests, integration tests, and package/tarball smoke test;
3. RustSec/npm audit with documented reachability for any exception;
4. secret scan, SBOM, license/source policy, and artifact checksums;
5. native/WASM API parity checks where applicable;
6. registry publish with provenance, followed by registry version and digest
verification;
7. rollback/deprecation instructions recorded before release.
For `ruvector@0.2.36`, the relevant focused gates are the end-to-end MCP
initialize test, stdout JSON purity, clean signal shutdown, full npm package
tests, TypeScript build, distribution verification, and `npm pack` smoke test.
## Primary references
- [DiskANN: fast, fresh, and filtered vector search](https://github.com/microsoft/DiskANN)
- [Survey of Filtered Approximate Nearest Neighbor Search (2025)](https://arxiv.org/abs/2505.06501)
- [Filtered ANN system design and performance analysis (2026)](https://arxiv.org/abs/2602.11443)
- [Quantization for vector search under streaming updates](https://arxiv.org/abs/2512.18335)
- [Lossless compression of vector IDs for ANN](https://arxiv.org/abs/2501.10479)
- [Qdrant data, filter, index, and quantization documentation](https://qdrant.tech/documentation/manage-data/)

8141
npm/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -38,6 +38,7 @@
"fast-xml-parser": ">=5.7.0",
"protobufjs": ">=7.5.6",
"@google-cloud/redis": ">=5.0.0",
"vm2": ">=3.11.5"
"vm2": ">=3.11.5",
"tar": "7.5.22"
}
}

View file

@ -35,7 +35,7 @@
"@google-cloud/monitoring": "^4.0.0",
"@google-cloud/compute": "^4.0.0",
"@google-cloud/cloud-sql-connector": "^1.3.0",
"@google-cloud/redis": "^3.0.0",
"@google-cloud/redis": "^5.3.0",
"@google-cloud/logging": "^11.0.0",
"node-cron": "^3.0.3"
},

View file

@ -8138,13 +8138,16 @@ const mcpCmd = program.command('mcp').description('MCP (Model Context Protocol)
mcpCmd.command('start')
.description('Start the RuVector MCP server')
.action(() => {
// Execute the mcp-server.js directly
const mcpServerPath = path.join(__dirname, 'mcp-server.js');
if (!fs.existsSync(mcpServerPath)) {
console.error(chalk.red('Error: MCP server not found at'), mcpServerPath);
process.exit(1);
}
require(mcpServerPath);
const { main } = require(mcpServerPath);
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
});
mcpCmd.command('info')
@ -10277,4 +10280,3 @@ if (require.main === module) {
module.exports = { atomicWriteFileSync, readIntelStoreSafe, Intelligence };
}

View file

@ -3183,13 +3183,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}
case 'workers_create': {
const name = args.name;
const preset = args.preset || 'quick-scan';
const triggers = args.triggers;
const name = String(args.name || '');
const preset = String(args.preset || 'quick-scan');
const triggers = args.triggers == null ? null : String(args.triggers);
try {
let cmd = `npx agentic-flow@alpha workers create "${name}" --preset ${preset}`;
if (triggers) cmd += ` --triggers "${triggers}"`;
const result = execSync(cmd, {
// Never interpolate MCP-controlled fields into a shell command.
// execFileSync passes each value as one argument, so names/triggers
// containing quotes or shell metacharacters cannot escape to a shell.
const command = process.platform === 'win32' ? 'npx.cmd' : 'npx';
const commandArgs = ['agentic-flow@alpha', 'workers', 'create', name, '--preset', preset];
if (triggers) commandArgs.push('--triggers', triggers);
const result = execFileSync(command, commandArgs, {
encoding: 'utf-8',
timeout: 30000,
stdio: ['pipe', 'pipe', 'pipe']
@ -4084,8 +4088,11 @@ async function main() {
process.stdin.on('end', () => process.exit(0));
}
module.exports = { main, loadBrainClient, BRAIN_MISSING_DEP_RESULT };
if (require.main === module) {
main().catch(console.error);
} else {
module.exports = { loadBrainClient, BRAIN_MISSING_DEP_RESULT };
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
}

View file

@ -1,18 +1,18 @@
{
"name": "ruvector",
"version": "0.2.35",
"version": "0.2.37",
"description": "Self-learning vector database for Node.js \u2014 hybrid search, Graph RAG, FlashAttention-3, HNSW, 50+ attention mechanisms",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"ruvector": "./bin/cli.js"
"ruvector": "bin/cli.js"
},
"scripts": {
"build": "tsc && node -e \"require('fs').cpSync('src/core/onnx','dist/core/onnx',{recursive:true})\"",
"verify-dist": "node scripts/verify-dist.js",
"prepack": "npm run build && npm run verify-dist",
"prepublishOnly": "npm run build && npm run verify-dist",
"test": "node test/integration.js && node test/cli-commands.js && node test/db-workflow.js && node test/sigterm-cleanup.js && node test/mcp-policy.js && node test/startup-budget.js"
"test": "node test/integration.js && node test/cli-commands.js && node test/db-workflow.js && node test/mcp-stdio.js && node test/sigterm-cleanup.js && node test/mcp-policy.js && node test/startup-budget.js"
},
"keywords": [
"vector",
@ -67,18 +67,17 @@
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/ruvnet/ruvector.git",
"url": "git+https://github.com/ruvnet/ruvector.git",
"directory": "npm/packages/ruvector"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@ruvector/attention": "^0.1.3",
"@ruvector/core": "^0.1.25",
"@ruvector/gnn": "^0.1.22",
"@ruvector/sona": "^0.1.4",
"chalk": "^4.1.2",
"commander": "^11.1.0",
"js-beautify": "^1.15.4",
"ora": "^5.4.1"
},
"optionalDependencies": {

View file

@ -150,7 +150,7 @@ export class ModelLoader {
}
}
console.log(`Loading model: ${modelConfig.name} (${modelConfig.size})`);
console.error(`Loading model: ${modelConfig.name} (${modelConfig.size})`);
const [modelBytes, tokenizerJson] = await Promise.all([
this.fetchWithCache(modelConfig.model, `${modelName}-model.onnx`, 'arraybuffer'),
@ -196,7 +196,7 @@ export class ModelLoader {
const modelBytes = new Uint8Array(fs.readFileSync(modelPath));
const tokenizerJson = fs.readFileSync(tokPath, 'utf8');
if (modelBytes.length === 0 || tokenizerJson.length === 0) return null;
console.log(` Disk cache hit: ${modelName}`);
console.error(` Disk cache hit: ${modelName}`);
return { modelBytes, tokenizerJson };
} catch {
return null;
@ -274,7 +274,7 @@ export class ModelLoader {
const cache = await caches.open(this.cacheStorage);
const cached = await cache.match(cacheKey);
if (cached) {
console.log(` Cache hit: ${cacheKey}`);
console.error(` Cache hit: ${cacheKey}`);
return responseType === 'arraybuffer'
? await cached.arrayBuffer()
: await cached.text();
@ -285,7 +285,7 @@ export class ModelLoader {
}
// Fetch from network
console.log(` Downloading: ${url}`);
console.error(` Downloading: ${url}`);
const response = await this.fetchWithProgress(url);
if (!response.ok) {
@ -365,7 +365,7 @@ export class ModelLoader {
async clearCache() {
if (typeof caches !== 'undefined') {
await caches.delete(this.cacheStorage);
console.log('Model cache cleared');
console.error('Model cache cleared');
}
}

View file

@ -97,7 +97,7 @@ export class ModelLoader {
throw new Error(`Unknown model: ${modelName}. Available: ${Object.keys(MODELS).join(', ')}`);
}
console.log(`Loading model: ${modelConfig.name} (${modelConfig.size})`);
console.error(`Loading model: ${modelConfig.name} (${modelConfig.size})`);
const [modelBytes, tokenizerJson] = await Promise.all([
this.fetchWithCache(modelConfig.model, `${modelName}-model.onnx`, 'arraybuffer'),
@ -161,7 +161,7 @@ export class ModelLoader {
const cache = await caches.open(this.cacheStorage);
const cached = await cache.match(cacheKey);
if (cached) {
console.log(` Cache hit: ${cacheKey}`);
console.error(` Cache hit: ${cacheKey}`);
return responseType === 'arraybuffer'
? await cached.arrayBuffer()
: await cached.text();
@ -172,7 +172,7 @@ export class ModelLoader {
}
// Fetch from network
console.log(` Downloading: ${url}`);
console.error(` Downloading: ${url}`);
const response = await this.fetchWithProgress(url);
if (!response.ok) {
@ -252,7 +252,7 @@ export class ModelLoader {
async clearCache() {
if (typeof caches !== 'undefined') {
await caches.delete(this.cacheStorage);
console.log('Model cache cleared');
console.error('Model cache cleared');
}
}

View file

@ -154,7 +154,19 @@ class VectorDBWrapper {
const nativeOptions: any = {
dimensions: options.dimensions,
storagePath: options.storagePath,
hnswConfig: options.hnswConfig,
// The N-API binding maps an omitted hnswConfig to `None`, which selects
// FlatIndex and unintentionally overrides ruvector-core's HNSW default.
// Pass the documented defaults explicitly so the high-level VectorDB
// remains an ANN database unless callers deliberately provide another
// HNSW configuration.
hnswConfig: options.hnswConfig === undefined
? {
m: 32,
efConstruction: 200,
efSearch: 100,
maxElements: 10_000_000,
}
: options.hnswConfig,
};
if (distanceMetric !== undefined) {
nativeOptions.distanceMetric = distanceMetric;

View file

@ -451,7 +451,7 @@ test('cli.js uses chalk with ESM compat', () => {
});
test('package.json bin entry points to cli.js', () => {
assert.strictEqual(packageJson.bin.ruvector, './bin/cli.js');
assert.strictEqual(packageJson.bin.ruvector, 'bin/cli.js');
});
test('package.json main entry points to dist/index.js', () => {

View file

@ -91,7 +91,7 @@ try {
assert(packageJson.version, 'Version should be set');
assert(packageJson.main === 'dist/index.js', 'Main entry should be dist/index.js');
assert(packageJson.types === 'dist/index.d.ts', 'Types entry should be dist/index.d.ts');
assert(packageJson.bin.ruvector === './bin/cli.js', 'CLI bin should be ./bin/cli.js');
assert(packageJson.bin.ruvector === 'bin/cli.js', 'CLI bin should be bin/cli.js');
console.log(' ✓ package.json structure correct');
const cliExists = fs.existsSync(path.join(__dirname, '../bin/cli.js'));

View file

@ -0,0 +1,117 @@
#!/usr/bin/env node
/**
* Regression coverage for issues #710 and #715.
*
* The CLI launcher must explicitly start the required MCP module, and stdout
* must remain exclusively JSON-RPC while the stdio transport is active.
*/
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const PACKAGE_ROOT = path.join(__dirname, '..');
const CLI = path.join(PACKAGE_ROOT, 'bin', 'cli.js');
const MCP_SERVER = path.join(PACKAGE_ROOT, 'bin', 'mcp-server.js');
const LOADER_PATHS = [
path.join(PACKAGE_ROOT, 'src', 'core', 'onnx', 'loader.js'),
path.join(PACKAGE_ROOT, 'src', 'core', 'onnx', 'pkg', 'loader.js'),
];
function waitForInitialize(child, timeoutMs = 10_000) {
return new Promise((resolve, reject) => {
let stdout = '';
let stderr = '';
const timer = setTimeout(() => {
reject(new Error(`Timed out waiting for MCP initialize response.\nstdout: ${stdout}\nstderr: ${stderr}`));
}, timeoutMs);
child.stdout.on('data', (chunk) => {
stdout += chunk.toString();
for (const line of stdout.split(/\r?\n/).filter(Boolean)) {
try {
const message = JSON.parse(line);
if (message.id === 1) {
clearTimeout(timer);
resolve({ message, stdout, stderr });
return;
}
} catch {
// Preserve the output; the stdout purity assertion below reports it.
}
}
});
child.stderr.on('data', (chunk) => {
stderr += chunk.toString();
});
child.once('error', (error) => {
clearTimeout(timer);
reject(error);
});
child.once('exit', (code, signal) => {
clearTimeout(timer);
reject(new Error(`MCP process exited before initialize: code=${code} signal=${signal}\nstderr: ${stderr}`));
});
});
}
async function main() {
// Inspect the module export without requiring it in this test process.
// Requiring it initializes the real intelligence worker pool, which is
// intentionally long-lived and would keep this regression test alive.
const serverSource = fs.readFileSync(MCP_SERVER, 'utf8');
assert.match(
serverSource,
/module\.exports\s*=\s*\{[^}]*\bmain\b/,
'mcp-server.js must export main() for the CLI launcher',
);
for (const loaderPath of LOADER_PATHS) {
const source = fs.readFileSync(loaderPath, 'utf8');
assert.doesNotMatch(
source,
/console\.log\((?:`Loading model:|`\s+(?:Disk )?Cache hit:|`\s+Downloading:|'Model cache cleared')/,
`${path.relative(PACKAGE_ROOT, loaderPath)} must send diagnostics to stderr`,
);
}
const child = spawn(process.execPath, [CLI, 'mcp', 'start'], {
cwd: PACKAGE_ROOT,
env: { ...process.env, NO_COLOR: '1' },
stdio: ['pipe', 'pipe', 'pipe'],
});
try {
child.stdin.write(`${JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: {},
clientInfo: { name: 'ruvector-mcp-regression', version: '1.0.0' },
},
})}\n`);
const { message, stdout } = await waitForInitialize(child);
assert.ok(message.result, `initialize returned an error: ${JSON.stringify(message)}`);
const frames = stdout.split(/\r?\n/).filter(Boolean);
assert.ok(frames.length > 0, 'MCP server must emit an initialize response');
for (const frame of frames) {
assert.doesNotThrow(() => JSON.parse(frame), `stdout contained non-JSON-RPC data: ${frame}`);
}
} finally {
child.stdin.end();
child.kill('SIGTERM');
}
console.log('MCP stdio launcher and stdout hygiene checks passed');
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});