Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
9
vendor/ruvector/npm/packages/rvlite/.rvlite/db.json
vendored
Normal file
9
vendor/ruvector/npm/packages/rvlite/.rvlite/db.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"vectors": {},
|
||||
"graph": {
|
||||
"nodes": {},
|
||||
"edges": {}
|
||||
},
|
||||
"triples": [],
|
||||
"nextId": 1
|
||||
}
|
||||
332
vendor/ruvector/npm/packages/rvlite/README.md
vendored
Normal file
332
vendor/ruvector/npm/packages/rvlite/README.md
vendored
Normal file
@@ -0,0 +1,332 @@
|
||||
# RvLite
|
||||
|
||||
Lightweight vector database with SQL, SPARQL, and Cypher - runs everywhere (Node.js, Browser, Edge).
|
||||
|
||||
Built on WebAssembly for maximum performance and portability. Only ~850KB!
|
||||
|
||||
## Features
|
||||
|
||||
- **Vector Search** - Semantic similarity with cosine, euclidean, or dot product distance
|
||||
- **SQL** - Query vectors with standard SQL plus distance operations
|
||||
- **Cypher** - Property graph queries (Neo4j-compatible syntax)
|
||||
- **SPARQL** - RDF triple store with W3C SPARQL queries
|
||||
- **Persistence** - Save/load to file (Node.js) or IndexedDB (browser)
|
||||
- **Tiny** - ~850KB WASM bundle, no external dependencies
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install rvlite
|
||||
```
|
||||
|
||||
## CLI Usage
|
||||
|
||||
```bash
|
||||
# Initialize a new database
|
||||
npx rvlite init
|
||||
|
||||
# Insert a vector
|
||||
npx rvlite insert "[0.1, 0.2, 0.3, ...]" --metadata '{"text": "Hello"}'
|
||||
|
||||
# Search for similar vectors
|
||||
npx rvlite search "[0.1, 0.2, 0.3, ...]" --top 5
|
||||
|
||||
# SQL queries
|
||||
npx rvlite sql "SELECT * FROM vectors LIMIT 10"
|
||||
|
||||
# Cypher queries
|
||||
npx rvlite cypher "CREATE (p:Person {name: 'Alice'})"
|
||||
npx rvlite cypher "MATCH (p:Person) RETURN p"
|
||||
|
||||
# SPARQL queries
|
||||
npx rvlite triple "http://example.org/alice" "http://xmlns.com/foaf/0.1/name" "Alice"
|
||||
npx rvlite sparql "SELECT ?s ?p ?o WHERE { ?s ?p ?o }"
|
||||
|
||||
# Interactive REPL
|
||||
npx rvlite repl
|
||||
|
||||
# Database stats
|
||||
npx rvlite stats
|
||||
|
||||
# Export/Import
|
||||
npx rvlite export backup.json
|
||||
npx rvlite import backup.json
|
||||
```
|
||||
|
||||
### REPL Commands
|
||||
|
||||
```
|
||||
.sql - Switch to SQL mode
|
||||
.cypher - Switch to Cypher mode
|
||||
.sparql - Switch to SPARQL mode
|
||||
.stats - Show database statistics
|
||||
.save - Save database
|
||||
.exit - Exit REPL
|
||||
```
|
||||
|
||||
## SDK Usage
|
||||
|
||||
### Basic Vector Operations
|
||||
|
||||
```typescript
|
||||
import { RvLite, createRvLite } from 'rvlite';
|
||||
|
||||
// Create instance
|
||||
const db = await createRvLite({ dimensions: 384 });
|
||||
|
||||
// Insert vectors
|
||||
const id = await db.insert([0.1, 0.2, 0.3, ...], { text: "Hello world" });
|
||||
|
||||
// Insert with custom ID
|
||||
await db.insertWithId("my-doc-1", [0.1, 0.2, ...], { source: "article" });
|
||||
|
||||
// Search similar
|
||||
const results = await db.search([0.1, 0.2, ...], 5);
|
||||
// [{ id: "...", score: 0.95, metadata: {...} }, ...]
|
||||
|
||||
// Get by ID
|
||||
const item = await db.get("my-doc-1");
|
||||
|
||||
// Delete
|
||||
await db.delete("my-doc-1");
|
||||
```
|
||||
|
||||
### SQL Queries
|
||||
|
||||
```typescript
|
||||
// Create table and insert
|
||||
await db.sql("CREATE TABLE documents (id TEXT, title TEXT, embedding VECTOR)");
|
||||
await db.sql("INSERT INTO documents VALUES ('doc1', 'Hello', '[0.1, 0.2, ...]')");
|
||||
|
||||
// Query with vector distance
|
||||
const results = await db.sql(`
|
||||
SELECT id, title, distance(embedding, '[0.1, 0.2, ...]') as dist
|
||||
FROM documents
|
||||
WHERE dist < 0.5
|
||||
ORDER BY dist
|
||||
`);
|
||||
```
|
||||
|
||||
### Cypher Graph Queries
|
||||
|
||||
```typescript
|
||||
// Create nodes
|
||||
await db.cypher("CREATE (alice:Person {name: 'Alice', age: 30})");
|
||||
await db.cypher("CREATE (bob:Person {name: 'Bob', age: 25})");
|
||||
|
||||
// Create relationships
|
||||
await db.cypher(`
|
||||
MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
|
||||
CREATE (a)-[:KNOWS {since: 2020}]->(b)
|
||||
`);
|
||||
|
||||
// Query
|
||||
const friends = await db.cypher(`
|
||||
MATCH (p:Person)-[:KNOWS]->(friend:Person)
|
||||
WHERE p.name = 'Alice'
|
||||
RETURN friend.name
|
||||
`);
|
||||
```
|
||||
|
||||
### SPARQL RDF Queries
|
||||
|
||||
```typescript
|
||||
// Add triples
|
||||
await db.addTriple(
|
||||
"http://example.org/alice",
|
||||
"http://xmlns.com/foaf/0.1/name",
|
||||
"Alice"
|
||||
);
|
||||
|
||||
await db.addTriple(
|
||||
"http://example.org/alice",
|
||||
"http://xmlns.com/foaf/0.1/knows",
|
||||
"http://example.org/bob"
|
||||
);
|
||||
|
||||
// Query
|
||||
const results = await db.sparql(`
|
||||
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
|
||||
SELECT ?name WHERE {
|
||||
<http://example.org/alice> foaf:name ?name
|
||||
}
|
||||
`);
|
||||
```
|
||||
|
||||
### Persistence
|
||||
|
||||
```typescript
|
||||
// Node.js - Export to file
|
||||
const state = await db.exportJson();
|
||||
fs.writeFileSync('db.json', JSON.stringify(state));
|
||||
|
||||
// Node.js - Import from file
|
||||
const data = JSON.parse(fs.readFileSync('db.json'));
|
||||
await db.importJson(data);
|
||||
|
||||
// Browser - Save to IndexedDB
|
||||
await db.save();
|
||||
|
||||
// Browser - Load from IndexedDB
|
||||
const db = await RvLite.load({ dimensions: 384 });
|
||||
|
||||
// Browser - Clear storage
|
||||
await RvLite.clearStorage();
|
||||
```
|
||||
|
||||
### Semantic Memory for AI
|
||||
|
||||
```typescript
|
||||
import { RvLite, SemanticMemory, createRvLite } from 'rvlite';
|
||||
|
||||
// Create with embedding provider
|
||||
const db = await createRvLite({ dimensions: 1536 });
|
||||
const memory = new SemanticMemory(db);
|
||||
|
||||
// Store memories with embeddings
|
||||
await memory.store("conv-1", "User asked about weather", embedding1);
|
||||
await memory.store("conv-2", "Discussed travel plans", embedding2);
|
||||
|
||||
// Add relationships
|
||||
await memory.addRelation("conv-1", "LEADS_TO", "conv-2");
|
||||
|
||||
// Query by similarity
|
||||
const similar = await memory.query("What was the weather question?", queryEmbedding);
|
||||
|
||||
// Find related through graph
|
||||
const related = await memory.findRelated("conv-1", 2);
|
||||
```
|
||||
|
||||
## RVF Storage Backend
|
||||
|
||||
RvLite can use [RVF (RuVector Format)](https://github.com/ruvnet/ruvector/tree/main/crates/rvf) as a persistent storage backend. When the optional `@ruvector/rvf-wasm` package is installed, rvlite gains file-backed persistence using the `.rvf` cognitive container format.
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
npm install rvlite @ruvector/rvf-wasm
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
import { createRvLite } from 'rvlite';
|
||||
|
||||
// rvlite auto-detects @ruvector/rvf-wasm when installed
|
||||
const db = await createRvLite({ dimensions: 384 });
|
||||
|
||||
// All operations persist to RVF format
|
||||
await db.insert([0.1, 0.2, ...], { text: "Hello world" });
|
||||
const results = await db.search([0.1, 0.2, ...], 5);
|
||||
```
|
||||
|
||||
### Platform Support
|
||||
|
||||
The RVF backend works everywhere rvlite runs:
|
||||
|
||||
| Platform | RVF Backend | Notes |
|
||||
|----------|-------------|-------|
|
||||
| Node.js (Linux, macOS, Windows) | Native or WASM | Auto-detected |
|
||||
| Browser (Chrome, Firefox, Safari) | WASM | IndexedDB + RVF |
|
||||
| Deno | WASM | Via `npm:` specifier |
|
||||
| Cloudflare Workers / Edge | WASM | Stateless queries |
|
||||
|
||||
### Rust Feature Flag
|
||||
|
||||
If building from source, enable the `rvf-backend` feature in `crates/rvlite`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rvlite = { version = "0.1", features = ["rvf-backend"] }
|
||||
```
|
||||
|
||||
This enables epoch-based reconciliation between RVF and metadata stores:
|
||||
- Monotonic epoch counter shared between RVF and metadata
|
||||
- On startup, compares epochs and rebuilds the lagging side
|
||||
- RVF file is source of truth; metadata (IndexedDB) is rebuildable cache
|
||||
|
||||
### Download Example .rvf Files
|
||||
|
||||
```bash
|
||||
# Download pre-built examples to test with
|
||||
curl -LO https://raw.githubusercontent.com/ruvnet/ruvector/main/examples/rvf/output/basic_store.rvf
|
||||
curl -LO https://raw.githubusercontent.com/ruvnet/ruvector/main/examples/rvf/output/semantic_search.rvf
|
||||
curl -LO https://raw.githubusercontent.com/ruvnet/ruvector/main/examples/rvf/output/agent_memory.rvf
|
||||
|
||||
# 45 examples available at:
|
||||
# https://github.com/ruvnet/ruvector/tree/main/examples/rvf/output
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration with claude-flow
|
||||
|
||||
RvLite can enhance claude-flow's memory system with semantic search:
|
||||
|
||||
```typescript
|
||||
import { RvLite, SemanticMemory } from 'rvlite';
|
||||
|
||||
// In your claude-flow hooks
|
||||
const db = await createRvLite({ dimensions: 1536 });
|
||||
|
||||
// Pre-task hook: Find relevant context
|
||||
async function preTask(task) {
|
||||
const embedding = await getEmbedding(task.description);
|
||||
const context = await db.search(embedding, 5);
|
||||
return { ...task, context };
|
||||
}
|
||||
|
||||
// Post-task hook: Store results
|
||||
async function postTask(task, result) {
|
||||
const embedding = await getEmbedding(result.summary);
|
||||
await db.insert(embedding, {
|
||||
task: task.id,
|
||||
result: result.summary,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### RvLite Class
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `insert(vector, metadata?)` | Insert vector, returns ID |
|
||||
| `insertWithId(id, vector, metadata?)` | Insert with custom ID |
|
||||
| `search(query, k)` | Find k nearest vectors |
|
||||
| `get(id)` | Get vector by ID |
|
||||
| `delete(id)` | Delete vector by ID |
|
||||
| `len()` | Count of vectors |
|
||||
| `sql(query)` | Execute SQL query |
|
||||
| `cypher(query)` | Execute Cypher query |
|
||||
| `cypherStats()` | Get graph statistics |
|
||||
| `sparql(query)` | Execute SPARQL query |
|
||||
| `addTriple(s, p, o, graph?)` | Add RDF triple |
|
||||
| `tripleCount()` | Count of triples |
|
||||
| `exportJson()` | Export state to JSON |
|
||||
| `importJson(data)` | Import state from JSON |
|
||||
| `save()` | Save to IndexedDB (browser) |
|
||||
| `RvLite.load(config)` | Load from IndexedDB |
|
||||
| `RvLite.clearStorage()` | Clear IndexedDB |
|
||||
|
||||
### CLI Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `rvlite init` | Initialize database |
|
||||
| `rvlite insert <vector>` | Insert vector |
|
||||
| `rvlite search <vector>` | Search similar |
|
||||
| `rvlite sql <query>` | Execute SQL |
|
||||
| `rvlite cypher <query>` | Execute Cypher |
|
||||
| `rvlite sparql <query>` | Execute SPARQL |
|
||||
| `rvlite triple <s> <p> <o>` | Add triple |
|
||||
| `rvlite stats` | Show statistics |
|
||||
| `rvlite export <file>` | Export to JSON |
|
||||
| `rvlite import <file>` | Import from JSON |
|
||||
| `rvlite repl` | Start interactive mode |
|
||||
|
||||
## License
|
||||
|
||||
MIT OR Apache-2.0
|
||||
1685
vendor/ruvector/npm/packages/rvlite/bin/cli.js
vendored
Executable file
1685
vendor/ruvector/npm/packages/rvlite/bin/cli.js
vendored
Executable file
File diff suppressed because it is too large
Load Diff
88
vendor/ruvector/npm/packages/rvlite/package.json
vendored
Normal file
88
vendor/ruvector/npm/packages/rvlite/package.json
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "rvlite",
|
||||
"version": "0.2.4",
|
||||
"type": "module",
|
||||
"description": "Lightweight vector database with SQL, SPARQL, and Cypher - runs everywhere (Node.js, Browser, Edge)",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"rvlite": "bin/cli.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./wasm": {
|
||||
"import": "./dist/wasm/rvlite.js",
|
||||
"types": "./dist/wasm/rvlite.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"bin",
|
||||
"wasm"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npm run build:wasm && npm run build:sdk",
|
||||
"build:wasm": "cd ../../../crates/rvlite && wasm-pack build --target web --release && cp -r pkg/* ../../npm/packages/rvlite/dist/wasm/",
|
||||
"build:sdk": "tsc && esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js --format=cjs && esbuild src/index.ts --bundle --platform=node --outfile=dist/index.mjs --format=esm",
|
||||
"test": "node --test test/*.test.js",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"vector-database",
|
||||
"embeddings",
|
||||
"semantic-search",
|
||||
"sql",
|
||||
"sparql",
|
||||
"cypher",
|
||||
"graph-database",
|
||||
"wasm",
|
||||
"ai",
|
||||
"llm",
|
||||
"rag",
|
||||
"knowledge-graph"
|
||||
],
|
||||
"author": "RuVector Contributors",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ruvnet/ruvector.git",
|
||||
"directory": "npm/packages/rvlite"
|
||||
},
|
||||
"homepage": "https://github.com/ruvnet/ruvector/tree/main/npm/packages/rvlite",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ruvnet/ruvector/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^12.0.0",
|
||||
"chalk": "^5.3.0",
|
||||
"ora": "^8.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.0",
|
||||
"esbuild": "^0.20.0",
|
||||
"@types/node": "^20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@anthropic-ai/sdk": ">=0.20.0",
|
||||
"@ruvector/rvf-wasm": ">=0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@anthropic-ai/sdk": {
|
||||
"optional": true
|
||||
},
|
||||
"@ruvector/rvf-wasm": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@ruvector/rvf-wasm": "^0.1.0"
|
||||
}
|
||||
}
|
||||
362
vendor/ruvector/npm/packages/rvlite/src/cli-rvf.ts
vendored
Normal file
362
vendor/ruvector/npm/packages/rvlite/src/cli-rvf.ts
vendored
Normal file
@@ -0,0 +1,362 @@
|
||||
/**
|
||||
* cli-rvf.ts - RVF migration and rebuild CLI commands
|
||||
*
|
||||
* Two commands:
|
||||
* rvf-migrate — Convert existing rvlite data to RVF format
|
||||
* rvf-rebuild — Reconstruct metadata from an RVF file
|
||||
*
|
||||
* Usage (via the rvlite CLI binary or directly):
|
||||
* rvlite rvf-migrate --source .rvlite/db.json --dest data.rvf [--dry-run] [--verify]
|
||||
* rvlite rvf-rebuild --source data.rvf [--dest .rvlite/db.json]
|
||||
*/
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Shape of the JSON-based rvlite database state (as saved by the CLI). */
|
||||
interface RvLiteDbState {
|
||||
vectors: Record<string, {
|
||||
vector: number[];
|
||||
metadata?: Record<string, unknown>;
|
||||
norm?: number;
|
||||
}>;
|
||||
graph?: {
|
||||
nodes?: Record<string, unknown>;
|
||||
edges?: Record<string, unknown>;
|
||||
};
|
||||
triples?: Array<{ subject: string; predicate: string; object: string }>;
|
||||
nextId?: number;
|
||||
config?: {
|
||||
dimensions?: number;
|
||||
metric?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/** JSON-based RVF file envelope. */
|
||||
interface RvfFileEnvelope {
|
||||
rvf_version: number;
|
||||
magic: 'RVF1';
|
||||
created_at: string;
|
||||
dimensions: number;
|
||||
distance_metric: string;
|
||||
payload: RvLiteDbState;
|
||||
}
|
||||
|
||||
/** Summary report returned by migrate / rebuild. */
|
||||
export interface MigrateReport {
|
||||
vectorsMigrated: number;
|
||||
triplesMigrated: number;
|
||||
graphNodesMigrated: number;
|
||||
graphEdgesMigrated: number;
|
||||
skipped: boolean;
|
||||
dryRun: boolean;
|
||||
verifyPassed?: boolean;
|
||||
}
|
||||
|
||||
export interface RebuildReport {
|
||||
vectorsRecovered: number;
|
||||
triplesRecovered: number;
|
||||
graphNodesRecovered: number;
|
||||
graphEdgesRecovered: number;
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
function vectorsClose(a: number[], b: number[], tolerance: number): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (Math.abs(a[i] - b[i]) > tolerance) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Migrate ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Convert an existing rvlite JSON database into an RVF file.
|
||||
*
|
||||
* @param sourcePath - Path to the rvlite JSON database (e.g., .rvlite/db.json).
|
||||
* @param destPath - Destination path for the RVF file.
|
||||
* @param options - Migration options.
|
||||
* @returns A report summarising the migration.
|
||||
*/
|
||||
export async function rvfMigrate(
|
||||
sourcePath: string,
|
||||
destPath: string,
|
||||
options: { dryRun?: boolean; verify?: boolean } = {}
|
||||
): Promise<MigrateReport> {
|
||||
const fs = await import('fs');
|
||||
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
throw new Error(`Source file not found: ${sourcePath}`);
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(sourcePath, 'utf-8');
|
||||
const state: RvLiteDbState = JSON.parse(raw);
|
||||
|
||||
// Idempotency: if dest already exists and is a valid RVF file whose
|
||||
// payload matches the source, treat as a no-op.
|
||||
if (fs.existsSync(destPath)) {
|
||||
try {
|
||||
const existing = JSON.parse(fs.readFileSync(destPath, 'utf-8')) as RvfFileEnvelope;
|
||||
if (existing.magic === 'RVF1') {
|
||||
const existingVecCount = Object.keys(existing.payload?.vectors ?? {}).length;
|
||||
const sourceVecCount = Object.keys(state.vectors ?? {}).length;
|
||||
if (existingVecCount === sourceVecCount) {
|
||||
return {
|
||||
vectorsMigrated: 0,
|
||||
triplesMigrated: 0,
|
||||
graphNodesMigrated: 0,
|
||||
graphEdgesMigrated: 0,
|
||||
skipped: true,
|
||||
dryRun: options.dryRun ?? false,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// File exists but is not valid RVF — proceed with migration.
|
||||
}
|
||||
}
|
||||
|
||||
const vectorCount = Object.keys(state.vectors ?? {}).length;
|
||||
const tripleCount = (state.triples ?? []).length;
|
||||
const nodeCount = Object.keys(state.graph?.nodes ?? {}).length;
|
||||
const edgeCount = Object.keys(state.graph?.edges ?? {}).length;
|
||||
|
||||
if (options.dryRun) {
|
||||
return {
|
||||
vectorsMigrated: vectorCount,
|
||||
triplesMigrated: tripleCount,
|
||||
graphNodesMigrated: nodeCount,
|
||||
graphEdgesMigrated: edgeCount,
|
||||
skipped: false,
|
||||
dryRun: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Build the RVF envelope.
|
||||
const envelope: RvfFileEnvelope = {
|
||||
rvf_version: 1,
|
||||
magic: 'RVF1',
|
||||
created_at: new Date().toISOString(),
|
||||
dimensions: state.config?.dimensions ?? 384,
|
||||
distance_metric: state.config?.metric ?? 'cosine',
|
||||
payload: state,
|
||||
};
|
||||
|
||||
const path = await import('path');
|
||||
const dir = path.dirname(destPath);
|
||||
if (dir && !fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(destPath, JSON.stringify(envelope, null, 2), 'utf-8');
|
||||
|
||||
// Optionally verify round-trip fidelity.
|
||||
let verifyPassed: boolean | undefined;
|
||||
if (options.verify) {
|
||||
const reRead = JSON.parse(fs.readFileSync(destPath, 'utf-8')) as RvfFileEnvelope;
|
||||
verifyPassed = true;
|
||||
|
||||
for (const [id, entry] of Object.entries(state.vectors ?? {})) {
|
||||
const rvfEntry = reRead.payload.vectors?.[id];
|
||||
if (!rvfEntry) {
|
||||
verifyPassed = false;
|
||||
break;
|
||||
}
|
||||
if (!vectorsClose(entry.vector, rvfEntry.vector, 1e-6)) {
|
||||
verifyPassed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
vectorsMigrated: vectorCount,
|
||||
triplesMigrated: tripleCount,
|
||||
graphNodesMigrated: nodeCount,
|
||||
graphEdgesMigrated: edgeCount,
|
||||
skipped: false,
|
||||
dryRun: false,
|
||||
verifyPassed,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Rebuild ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Reconstruct metadata from an RVF file.
|
||||
*
|
||||
* Reads the RVF envelope, extracts vectors, and rebuilds
|
||||
* SQL / Cypher / SPARQL metadata from vector metadata fields.
|
||||
*
|
||||
* @param sourcePath - Path to the RVF file.
|
||||
* @param destPath - Optional destination for the rebuilt JSON state.
|
||||
* @returns A report summarising the recovered data.
|
||||
*/
|
||||
export async function rvfRebuild(
|
||||
sourcePath: string,
|
||||
destPath?: string
|
||||
): Promise<RebuildReport> {
|
||||
const fs = await import('fs');
|
||||
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
throw new Error(`RVF file not found: ${sourcePath}`);
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(sourcePath, 'utf-8');
|
||||
const envelope = JSON.parse(raw) as RvfFileEnvelope;
|
||||
|
||||
if (envelope.magic !== 'RVF1') {
|
||||
throw new Error(`Invalid RVF file: expected magic "RVF1", got "${envelope.magic}"`);
|
||||
}
|
||||
|
||||
const state = envelope.payload;
|
||||
|
||||
// Rebuild graph nodes from vectors that have graph-like metadata.
|
||||
const recoveredNodes: Record<string, unknown> = {};
|
||||
const recoveredEdges: Record<string, unknown> = {};
|
||||
const recoveredTriples: Array<{ subject: string; predicate: string; object: string }> = [];
|
||||
|
||||
for (const [id, entry] of Object.entries(state.vectors ?? {})) {
|
||||
const meta = entry.metadata;
|
||||
if (!meta) continue;
|
||||
|
||||
// Recover graph nodes: metadata with a `_label` field.
|
||||
if (typeof meta._label === 'string') {
|
||||
recoveredNodes[id] = { label: meta._label, properties: meta };
|
||||
}
|
||||
|
||||
// Recover graph edges: metadata with `_from` and `_to`.
|
||||
if (typeof meta._from === 'string' && typeof meta._to === 'string') {
|
||||
recoveredEdges[id] = {
|
||||
from: meta._from,
|
||||
to: meta._to,
|
||||
type: meta._type ?? 'RELATED',
|
||||
properties: meta,
|
||||
};
|
||||
}
|
||||
|
||||
// Recover triples: metadata with `_subject`, `_predicate`, `_object`.
|
||||
if (
|
||||
typeof meta._subject === 'string' &&
|
||||
typeof meta._predicate === 'string' &&
|
||||
typeof meta._object === 'string'
|
||||
) {
|
||||
recoveredTriples.push({
|
||||
subject: meta._subject,
|
||||
predicate: meta._predicate,
|
||||
object: meta._object,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Merge recovered data with any existing data in the envelope.
|
||||
const existingTriples = state.triples ?? [];
|
||||
const allTriples = [...existingTriples, ...recoveredTriples];
|
||||
|
||||
const existingNodes = state.graph?.nodes ?? {};
|
||||
const existingEdges = state.graph?.edges ?? {};
|
||||
const allNodes = { ...existingNodes, ...recoveredNodes };
|
||||
const allEdges = { ...existingEdges, ...recoveredEdges };
|
||||
|
||||
const rebuiltState: RvLiteDbState = {
|
||||
vectors: state.vectors ?? {},
|
||||
graph: { nodes: allNodes, edges: allEdges },
|
||||
triples: allTriples,
|
||||
nextId: state.nextId ?? Object.keys(state.vectors ?? {}).length + 1,
|
||||
config: {
|
||||
dimensions: envelope.dimensions,
|
||||
metric: envelope.distance_metric,
|
||||
},
|
||||
};
|
||||
|
||||
if (destPath) {
|
||||
const path = await import('path');
|
||||
const dir = path.dirname(destPath);
|
||||
if (dir && !fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(destPath, JSON.stringify(rebuiltState, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
return {
|
||||
vectorsRecovered: Object.keys(state.vectors ?? {}).length,
|
||||
triplesRecovered: allTriples.length,
|
||||
graphNodesRecovered: Object.keys(allNodes).length,
|
||||
graphEdgesRecovered: Object.keys(allEdges).length,
|
||||
};
|
||||
}
|
||||
|
||||
// ── CLI Entry Point ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Register rvf-migrate and rvf-rebuild commands on a Commander program
|
||||
* instance. This allows the main rvlite CLI to integrate these commands
|
||||
* without duplicating code.
|
||||
*/
|
||||
export function registerRvfCommands(program: any): void {
|
||||
program
|
||||
.command('rvf-migrate')
|
||||
.description('Convert existing rvlite data to RVF format')
|
||||
.requiredOption('-s, --source <path>', 'Path to source rvlite JSON database')
|
||||
.requiredOption('-d, --dest <path>', 'Destination RVF file path')
|
||||
.option('--dry-run', 'Report what would be migrated without writing', false)
|
||||
.option('--verify', 'Verify vectors match within 1e-6 tolerance after migration', false)
|
||||
.action(async (options: { source: string; dest: string; dryRun: boolean; verify: boolean }) => {
|
||||
try {
|
||||
const report = await rvfMigrate(options.source, options.dest, {
|
||||
dryRun: options.dryRun,
|
||||
verify: options.verify,
|
||||
});
|
||||
|
||||
if (report.skipped) {
|
||||
console.log('Migration skipped: destination already contains matching RVF data (idempotent).');
|
||||
return;
|
||||
}
|
||||
|
||||
if (report.dryRun) {
|
||||
console.log('Dry run — no files written.');
|
||||
}
|
||||
|
||||
console.log(`Vectors migrated: ${report.vectorsMigrated}`);
|
||||
console.log(`Triples migrated: ${report.triplesMigrated}`);
|
||||
console.log(`Graph nodes migrated: ${report.graphNodesMigrated}`);
|
||||
console.log(`Graph edges migrated: ${report.graphEdgesMigrated}`);
|
||||
|
||||
if (report.verifyPassed !== undefined) {
|
||||
console.log(`Verification: ${report.verifyPassed ? 'PASSED' : 'FAILED'}`);
|
||||
if (!report.verifyPassed) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`Error: ${msg}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('rvf-rebuild')
|
||||
.description('Reconstruct metadata from RVF file')
|
||||
.requiredOption('-s, --source <path>', 'Path to source RVF file')
|
||||
.option('-d, --dest <path>', 'Destination JSON file for rebuilt state')
|
||||
.action(async (options: { source: string; dest?: string }) => {
|
||||
try {
|
||||
const report = await rvfRebuild(options.source, options.dest);
|
||||
|
||||
console.log(`Vectors recovered: ${report.vectorsRecovered}`);
|
||||
console.log(`Triples recovered: ${report.triplesRecovered}`);
|
||||
console.log(`Graph nodes recovered: ${report.graphNodesRecovered}`);
|
||||
console.log(`Graph edges recovered: ${report.graphEdgesRecovered}`);
|
||||
|
||||
if (options.dest) {
|
||||
console.log(`Rebuilt state written to: ${options.dest}`);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`Error: ${msg}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
1
vendor/ruvector/npm/packages/rvlite/src/index.d.ts.map
vendored
Normal file
1
vendor/ruvector/npm/packages/rvlite/src/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}
|
||||
1
vendor/ruvector/npm/packages/rvlite/src/index.js.map
vendored
Normal file
1
vendor/ruvector/npm/packages/rvlite/src/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
917
vendor/ruvector/npm/packages/rvlite/src/index.ts
vendored
Normal file
917
vendor/ruvector/npm/packages/rvlite/src/index.ts
vendored
Normal file
@@ -0,0 +1,917 @@
|
||||
/**
|
||||
* RvLite - Lightweight Vector Database SDK
|
||||
*
|
||||
* A unified database combining:
|
||||
* - Vector similarity search
|
||||
* - SQL queries with vector distance operations
|
||||
* - Cypher property graph queries
|
||||
* - SPARQL RDF triple queries
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { RvLite } from 'rvlite';
|
||||
*
|
||||
* const db = new RvLite({ dimensions: 384 });
|
||||
*
|
||||
* // Insert vectors
|
||||
* db.insert([0.1, 0.2, ...], { text: "Hello world" });
|
||||
*
|
||||
* // Search similar
|
||||
* const results = db.search([0.1, 0.2, ...], 5);
|
||||
*
|
||||
* // SQL with vector distance
|
||||
* db.sql("SELECT * FROM vectors WHERE distance(embedding, ?) < 0.5");
|
||||
*
|
||||
* // Cypher graph queries
|
||||
* db.cypher("CREATE (p:Person {name: 'Alice'})");
|
||||
*
|
||||
* // SPARQL RDF queries
|
||||
* db.sparql("SELECT ?s ?p ?o WHERE { ?s ?p ?o }");
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Re-export WASM module for advanced usage
|
||||
export * from '../dist/wasm/rvlite.js';
|
||||
|
||||
// ── RVF Backend Detection ─────────────────────────────────────────────────
|
||||
|
||||
let rvfWasmAvailable: boolean | null = null;
|
||||
|
||||
/**
|
||||
* Check if @ruvector/rvf-wasm is installed for persistent RVF storage.
|
||||
*/
|
||||
export function isRvfAvailable(): boolean {
|
||||
if (rvfWasmAvailable !== null) return rvfWasmAvailable;
|
||||
try {
|
||||
require.resolve('@ruvector/rvf-wasm');
|
||||
rvfWasmAvailable = true;
|
||||
} catch {
|
||||
rvfWasmAvailable = false;
|
||||
}
|
||||
return rvfWasmAvailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active storage backend.
|
||||
*/
|
||||
export function getStorageBackend(): 'rvf' | 'indexeddb' | 'memory' {
|
||||
if (isRvfAvailable()) return 'rvf';
|
||||
if (typeof indexedDB !== 'undefined') return 'indexeddb';
|
||||
return 'memory';
|
||||
}
|
||||
|
||||
export interface RvLiteConfig {
|
||||
dimensions?: number;
|
||||
distanceMetric?: 'cosine' | 'euclidean' | 'dotproduct';
|
||||
/** Force a specific storage backend. Auto-detected if omitted. */
|
||||
backend?: 'rvf' | 'indexeddb' | 'memory' | 'auto';
|
||||
/** Path to RVF file for persistent storage. */
|
||||
rvfPath?: string;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
id: string;
|
||||
score: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface QueryResult {
|
||||
columns?: string[];
|
||||
rows?: unknown[][];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main RvLite class - wraps the WASM module with a friendly API
|
||||
*/
|
||||
export class RvLite {
|
||||
private wasm: any;
|
||||
private config: RvLiteConfig;
|
||||
private initialized: boolean = false;
|
||||
|
||||
constructor(config: RvLiteConfig = {}) {
|
||||
this.config = {
|
||||
dimensions: config.dimensions || 384,
|
||||
distanceMetric: config.distanceMetric || 'cosine',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the WASM module (called automatically on first use)
|
||||
*/
|
||||
async init(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
|
||||
// Dynamic import to support both Node.js and browser
|
||||
// Use 'as any' for WASM interop: generated types conflict with SDK types
|
||||
const wasmModule = await import('../dist/wasm/rvlite.js') as any;
|
||||
await wasmModule.default();
|
||||
|
||||
this.wasm = new wasmModule.RvLite({
|
||||
dimensions: this.config.dimensions,
|
||||
distance_metric: this.config.distanceMetric,
|
||||
});
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
private async ensureInit(): Promise<void> {
|
||||
if (!this.initialized) {
|
||||
await this.init();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Vector Operations ============
|
||||
|
||||
/**
|
||||
* Insert a vector with optional metadata
|
||||
*/
|
||||
async insert(
|
||||
vector: number[],
|
||||
metadata?: Record<string, unknown>
|
||||
): Promise<string> {
|
||||
await this.ensureInit();
|
||||
return this.wasm.insert(vector, metadata || null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a vector with a specific ID
|
||||
*/
|
||||
async insertWithId(
|
||||
id: string,
|
||||
vector: number[],
|
||||
metadata?: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
await this.ensureInit();
|
||||
this.wasm.insert_with_id(id, vector, metadata || null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for similar vectors
|
||||
*/
|
||||
async search(query: number[], k: number = 5): Promise<SearchResult[]> {
|
||||
await this.ensureInit();
|
||||
return this.wasm.search(query, k);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a vector by ID
|
||||
*/
|
||||
async get(id: string): Promise<{ vector: number[]; metadata?: Record<string, unknown> } | null> {
|
||||
await this.ensureInit();
|
||||
return this.wasm.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a vector by ID
|
||||
*/
|
||||
async delete(id: string): Promise<boolean> {
|
||||
await this.ensureInit();
|
||||
return this.wasm.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of vectors
|
||||
*/
|
||||
async len(): Promise<number> {
|
||||
await this.ensureInit();
|
||||
return this.wasm.len();
|
||||
}
|
||||
|
||||
// ============ SQL Operations ============
|
||||
|
||||
/**
|
||||
* Execute a SQL query
|
||||
*
|
||||
* Supports vector distance operations:
|
||||
* - distance(col, vector) - Calculate distance
|
||||
* - vec_search(col, vector, k) - Find k nearest
|
||||
*/
|
||||
async sql(query: string): Promise<QueryResult> {
|
||||
await this.ensureInit();
|
||||
return this.wasm.sql(query);
|
||||
}
|
||||
|
||||
// ============ Cypher Operations ============
|
||||
|
||||
/**
|
||||
* Execute a Cypher graph query
|
||||
*
|
||||
* Supports:
|
||||
* - CREATE (n:Label {props})
|
||||
* - MATCH (n:Label) WHERE ... RETURN n
|
||||
* - CREATE (a)-[:REL]->(b)
|
||||
*/
|
||||
async cypher(query: string): Promise<QueryResult> {
|
||||
await this.ensureInit();
|
||||
return this.wasm.cypher(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Cypher graph statistics
|
||||
*/
|
||||
async cypherStats(): Promise<{ node_count: number; edge_count: number }> {
|
||||
await this.ensureInit();
|
||||
return this.wasm.cypher_stats();
|
||||
}
|
||||
|
||||
// ============ SPARQL Operations ============
|
||||
|
||||
/**
|
||||
* Execute a SPARQL query
|
||||
*
|
||||
* Supports SELECT, ASK queries over RDF triples
|
||||
*/
|
||||
async sparql(query: string): Promise<QueryResult> {
|
||||
await this.ensureInit();
|
||||
return this.wasm.sparql(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an RDF triple
|
||||
*/
|
||||
async addTriple(
|
||||
subject: string,
|
||||
predicate: string,
|
||||
object: string,
|
||||
graph?: string
|
||||
): Promise<void> {
|
||||
await this.ensureInit();
|
||||
this.wasm.add_triple(subject, predicate, object, graph || null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of triples
|
||||
*/
|
||||
async tripleCount(): Promise<number> {
|
||||
await this.ensureInit();
|
||||
return this.wasm.triple_count();
|
||||
}
|
||||
|
||||
// ============ Persistence ============
|
||||
|
||||
/**
|
||||
* Export database state to JSON
|
||||
*/
|
||||
async exportJson(): Promise<unknown> {
|
||||
await this.ensureInit();
|
||||
return this.wasm.export_json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Import database state from JSON
|
||||
*/
|
||||
async importJson(data: unknown): Promise<void> {
|
||||
await this.ensureInit();
|
||||
this.wasm.import_json(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save to IndexedDB (browser only)
|
||||
*/
|
||||
async save(): Promise<void> {
|
||||
await this.ensureInit();
|
||||
return this.wasm.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load from IndexedDB (browser only)
|
||||
*/
|
||||
static async load(config: RvLiteConfig = {}): Promise<RvLite> {
|
||||
const instance = new RvLite(config);
|
||||
await instance.init();
|
||||
|
||||
// Dynamic import for WASM (cast to any: generated types conflict with SDK types)
|
||||
const wasmModule = await import('../dist/wasm/rvlite.js') as any;
|
||||
instance.wasm = await wasmModule.RvLite.load(config);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear IndexedDB storage (browser only)
|
||||
*/
|
||||
static async clearStorage(): Promise<void> {
|
||||
const wasmModule = await import('../dist/wasm/rvlite.js') as any;
|
||||
return wasmModule.RvLite.clear_storage();
|
||||
}
|
||||
|
||||
// ============ RVF Persistence ============
|
||||
|
||||
/**
|
||||
* Factory method: create an RvLite instance backed by an RVF file.
|
||||
*
|
||||
* Opens or creates an RVF file at the given path, initialises the WASM
|
||||
* module, and (when available) uses `@ruvector/rvf-wasm` for vector storage.
|
||||
* Falls back to standard WASM + JSON-based RVF if the optional package is
|
||||
* not installed.
|
||||
*
|
||||
* @param config - Standard RvLiteConfig plus a required `rvfPath`.
|
||||
* @returns A fully-initialised RvLite instance with data loaded from the
|
||||
* RVF file (if it already exists).
|
||||
*/
|
||||
static async createWithRvf(
|
||||
config: RvLiteConfig & { rvfPath: string }
|
||||
): Promise<RvLite> {
|
||||
const instance = new RvLite(config);
|
||||
instance.rvfPath = config.rvfPath;
|
||||
|
||||
// Attempt to use @ruvector/rvf-wasm for native RVF I/O
|
||||
try {
|
||||
const rvfWasm = await import('@ruvector/rvf-wasm' as string);
|
||||
instance.rvfModule = rvfWasm;
|
||||
} catch {
|
||||
// Optional dependency not available — fall back to JSON-based RVF.
|
||||
}
|
||||
|
||||
await instance.init();
|
||||
|
||||
// If the file exists on disk, load its content.
|
||||
if (typeof globalThis.process !== 'undefined') {
|
||||
try {
|
||||
const fs = await import('fs' as string);
|
||||
if (fs.existsSync(config.rvfPath)) {
|
||||
await instance.loadFromRvf(config.rvfPath);
|
||||
}
|
||||
} catch {
|
||||
// Browser or other environment — skip file check.
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the current vector state to an RVF file.
|
||||
*
|
||||
* When `@ruvector/rvf-wasm` is available the export uses the native RVF
|
||||
* binary writer. Otherwise the method falls back to a JSON payload
|
||||
* wrapped with RVF header metadata so the file can be identified as RVF.
|
||||
*
|
||||
* @param filePath - Destination path for the RVF file.
|
||||
*/
|
||||
async saveToRvf(filePath: string): Promise<void> {
|
||||
await this.ensureInit();
|
||||
|
||||
const jsonState = await this.exportJson();
|
||||
|
||||
// Prefer native RVF writer when available.
|
||||
if (this.rvfModule && typeof this.rvfModule.writeRvf === 'function') {
|
||||
await this.rvfModule.writeRvf(filePath, jsonState);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: JSON with RVF envelope
|
||||
const rvfEnvelope: RvfFileEnvelope = {
|
||||
rvf_version: 1,
|
||||
magic: 'RVF1',
|
||||
created_at: new Date().toISOString(),
|
||||
dimensions: this.config.dimensions ?? 384,
|
||||
distance_metric: this.config.distanceMetric ?? 'cosine',
|
||||
payload: jsonState,
|
||||
};
|
||||
|
||||
if (typeof globalThis.process !== 'undefined') {
|
||||
const fs = await import('fs' as string);
|
||||
const path = await import('path' as string);
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(filePath, JSON.stringify(rvfEnvelope, null, 2), 'utf-8');
|
||||
} else {
|
||||
throw new Error(
|
||||
'saveToRvf is only supported in Node.js environments. ' +
|
||||
'Use exportJson() for browser-side persistence.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import vector data from an RVF file.
|
||||
*
|
||||
* Parses the RVF format (either native binary via `@ruvector/rvf-wasm` or
|
||||
* the JSON-based fallback envelope) and loads vectors + metadata into the
|
||||
* current instance.
|
||||
*
|
||||
* @param filePath - Source path of the RVF file to import.
|
||||
*/
|
||||
async loadFromRvf(filePath: string): Promise<void> {
|
||||
await this.ensureInit();
|
||||
|
||||
// Prefer native RVF reader.
|
||||
if (this.rvfModule && typeof this.rvfModule.readRvf === 'function') {
|
||||
const data = await this.rvfModule.readRvf(filePath);
|
||||
await this.importJson(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: read JSON envelope.
|
||||
if (typeof globalThis.process !== 'undefined') {
|
||||
const fs = await import('fs' as string);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`RVF file not found: ${filePath}`);
|
||||
}
|
||||
const raw = fs.readFileSync(filePath, 'utf-8');
|
||||
const envelope = JSON.parse(raw) as RvfFileEnvelope;
|
||||
|
||||
if (envelope.magic !== 'RVF1') {
|
||||
throw new Error(
|
||||
`Invalid RVF file: expected magic "RVF1", got "${envelope.magic}"`
|
||||
);
|
||||
}
|
||||
|
||||
await this.importJson(envelope.payload);
|
||||
} else {
|
||||
throw new Error(
|
||||
'loadFromRvf is only supported in Node.js environments. ' +
|
||||
'Use importJson() for browser-side persistence.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal handle to optional @ruvector/rvf-wasm module */
|
||||
private rvfModule: any = null;
|
||||
/** @internal path to the RVF backing file (set by createWithRvf) */
|
||||
private rvfPath: string | null = null;
|
||||
}
|
||||
|
||||
// ============ Convenience Functions ============
|
||||
|
||||
/**
|
||||
* Create a new RvLite instance (async factory).
|
||||
*
|
||||
* When `@ruvector/rvf-wasm` is installed, persistence uses RVF format.
|
||||
* Override with `config.backend` to force a specific backend.
|
||||
*/
|
||||
export async function createRvLite(config: RvLiteConfig = {}): Promise<RvLite> {
|
||||
const requestedBackend = config.backend || 'auto';
|
||||
const actualBackend = requestedBackend === 'auto' ? getStorageBackend() : requestedBackend;
|
||||
|
||||
// Log backend selection (useful for debugging)
|
||||
if (typeof process !== 'undefined' && process.env && process.env.RVLITE_DEBUG) {
|
||||
console.log(`[rvlite] storage backend: ${actualBackend} (requested: ${requestedBackend}, rvf available: ${isRvfAvailable()})`);
|
||||
}
|
||||
|
||||
const db = new RvLite(config);
|
||||
await db.init();
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate embeddings using various providers
|
||||
*/
|
||||
export interface EmbeddingProvider {
|
||||
embed(text: string): Promise<number[]>;
|
||||
embedBatch(texts: string[]): Promise<number[][]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an embedding provider using Anthropic Claude
|
||||
*/
|
||||
export function createAnthropicEmbeddings(apiKey?: string): EmbeddingProvider {
|
||||
// Note: Claude doesn't have native embeddings, this is a placeholder
|
||||
// Users should use their own embedding provider
|
||||
throw new Error(
|
||||
'Anthropic does not provide embeddings. Use createOpenAIEmbeddings or a custom provider.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a string for safe use in Cypher queries.
|
||||
*/
|
||||
function sanitizeCypher(value: string): string {
|
||||
return value
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/'/g, "\\'")
|
||||
.replace(/[\x00-\x1f\x7f]/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a Cypher relationship type (alphanumeric + underscores only).
|
||||
*/
|
||||
function validateRelationType(rel: string): string {
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(rel)) {
|
||||
throw new Error(`Invalid relation type: ${rel}`);
|
||||
}
|
||||
return rel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Semantic Memory - Higher-level API for AI memory applications
|
||||
*
|
||||
* Combines vector search with knowledge graph storage
|
||||
*/
|
||||
export class SemanticMemory {
|
||||
private db: RvLite;
|
||||
private embedder?: EmbeddingProvider;
|
||||
|
||||
constructor(db: RvLite, embedder?: EmbeddingProvider) {
|
||||
this.db = db;
|
||||
this.embedder = embedder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a memory with semantic embedding
|
||||
*/
|
||||
async store(
|
||||
key: string,
|
||||
content: string,
|
||||
embedding?: number[],
|
||||
metadata?: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
let vector = embedding;
|
||||
if (!vector && this.embedder) {
|
||||
vector = await this.embedder.embed(content);
|
||||
}
|
||||
|
||||
if (vector) {
|
||||
await this.db.insertWithId(key, vector, { content, ...metadata });
|
||||
}
|
||||
|
||||
// Also store as graph node
|
||||
const safeKey = sanitizeCypher(key);
|
||||
const safeContent = sanitizeCypher(content);
|
||||
await this.db.cypher(
|
||||
`CREATE (m:Memory {key: "${safeKey}", content: "${safeContent}", timestamp: ${Date.now()}})`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query memories by semantic similarity
|
||||
*/
|
||||
async query(
|
||||
queryText: string,
|
||||
embedding?: number[],
|
||||
k: number = 5
|
||||
): Promise<SearchResult[]> {
|
||||
let vector = embedding;
|
||||
if (!vector && this.embedder) {
|
||||
vector = await this.embedder.embed(queryText);
|
||||
}
|
||||
|
||||
if (!vector) {
|
||||
throw new Error('No embedding provided and no embedder configured');
|
||||
}
|
||||
|
||||
return this.db.search(vector, k);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a relationship between memories
|
||||
*/
|
||||
async addRelation(
|
||||
fromKey: string,
|
||||
relation: string,
|
||||
toKey: string
|
||||
): Promise<void> {
|
||||
const safeFrom = sanitizeCypher(fromKey);
|
||||
const safeTo = sanitizeCypher(toKey);
|
||||
const safeRel = validateRelationType(relation);
|
||||
await this.db.cypher(
|
||||
`MATCH (a:Memory {key: "${safeFrom}"}), (b:Memory {key: "${safeTo}"}) CREATE (a)-[:${safeRel}]->(b)`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find related memories through graph traversal
|
||||
*/
|
||||
async findRelated(key: string, depth: number = 2): Promise<QueryResult> {
|
||||
const safeKey = sanitizeCypher(key);
|
||||
const safeDepth = Math.max(1, Math.min(10, Math.floor(depth)));
|
||||
return this.db.cypher(
|
||||
`MATCH (m:Memory {key: "${safeKey}"})-[*1..${safeDepth}]-(related:Memory) RETURN DISTINCT related`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── RVF File Envelope ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* JSON-based RVF file structure used when `@ruvector/rvf-wasm` is not
|
||||
* available. The envelope wraps the standard export_json() payload with
|
||||
* header metadata so the file is self-describing.
|
||||
*/
|
||||
export interface RvfFileEnvelope {
|
||||
/** RVF format version (currently 1). */
|
||||
rvf_version: number;
|
||||
/** Magic identifier — always "RVF1". */
|
||||
magic: 'RVF1';
|
||||
/** ISO-8601 timestamp of when the file was created. */
|
||||
created_at: string;
|
||||
/** Vector dimensions stored in this file. */
|
||||
dimensions: number;
|
||||
/** Distance metric used. */
|
||||
distance_metric: string;
|
||||
/** The full database state (as returned by `exportJson()`). */
|
||||
payload: unknown;
|
||||
}
|
||||
|
||||
// ── Browser Writer Lease ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Browser-side writer lease that uses IndexedDB for lock coordination.
|
||||
*
|
||||
* Only one writer may hold the lease for a given `storeId` at a time.
|
||||
* The holder sends heartbeats (timestamp updates) every 10 seconds so
|
||||
* that other tabs / windows can detect stale leases.
|
||||
*
|
||||
* Auto-releases on `beforeunload` to avoid dangling locks.
|
||||
*/
|
||||
export class BrowserWriterLease {
|
||||
private heartbeatInterval: number | null = null;
|
||||
private storeId: string | null = null;
|
||||
private static readonly DB_NAME = '_rvlite_locks';
|
||||
private static readonly STORE_NAME = 'locks';
|
||||
private static readonly HEARTBEAT_MS = 10_000;
|
||||
private static readonly DEFAULT_STALE_MS = 30_000;
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
private static openDb(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open(BrowserWriterLease.DB_NAME, 1);
|
||||
req.onupgradeneeded = () => {
|
||||
const db = req.result;
|
||||
if (!db.objectStoreNames.contains(BrowserWriterLease.STORE_NAME)) {
|
||||
db.createObjectStore(BrowserWriterLease.STORE_NAME, { keyPath: 'id' });
|
||||
}
|
||||
};
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
private static idbPut(db: IDBDatabase, record: unknown): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(BrowserWriterLease.STORE_NAME, 'readwrite');
|
||||
const store = tx.objectStore(BrowserWriterLease.STORE_NAME);
|
||||
const req = store.put(record);
|
||||
req.onsuccess = () => resolve();
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
private static idbGet(db: IDBDatabase, key: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(BrowserWriterLease.STORE_NAME, 'readonly');
|
||||
const store = tx.objectStore(BrowserWriterLease.STORE_NAME);
|
||||
const req = store.get(key);
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
private static idbDelete(db: IDBDatabase, key: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(BrowserWriterLease.STORE_NAME, 'readwrite');
|
||||
const store = tx.objectStore(BrowserWriterLease.STORE_NAME);
|
||||
const req = store.delete(key);
|
||||
req.onsuccess = () => resolve();
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- public API ----
|
||||
|
||||
/**
|
||||
* Try to acquire the writer lease for the given store.
|
||||
*
|
||||
* @param storeId - Unique identifier for the rvlite store being locked.
|
||||
* @param timeout - Maximum time in ms to wait for the lease (default 5000).
|
||||
* @returns `true` if the lease was acquired, `false` on timeout.
|
||||
*/
|
||||
async acquire(storeId: string, timeout: number = 5000): Promise<boolean> {
|
||||
if (typeof indexedDB === 'undefined') {
|
||||
throw new Error('BrowserWriterLease requires IndexedDB');
|
||||
}
|
||||
|
||||
const deadline = Date.now() + timeout;
|
||||
const db = await BrowserWriterLease.openDb();
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
const existing = await BrowserWriterLease.idbGet(db, storeId);
|
||||
|
||||
if (!existing || await BrowserWriterLease.isStale(storeId)) {
|
||||
// Write our lock record.
|
||||
await BrowserWriterLease.idbPut(db, {
|
||||
id: storeId,
|
||||
holder: this.holderId(),
|
||||
ts: Date.now(),
|
||||
});
|
||||
|
||||
// Re-read to confirm we won (poor-man's CAS).
|
||||
const confirm = await BrowserWriterLease.idbGet(db, storeId);
|
||||
if (confirm && confirm.holder === this.holderId()) {
|
||||
this.storeId = storeId;
|
||||
this.startHeartbeat(db);
|
||||
this.registerUnloadHandler();
|
||||
db.close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Back off before retrying.
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
db.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the currently held lease.
|
||||
*/
|
||||
async release(): Promise<void> {
|
||||
this.stopHeartbeat();
|
||||
|
||||
if (this.storeId === null) return;
|
||||
|
||||
try {
|
||||
const db = await BrowserWriterLease.openDb();
|
||||
await BrowserWriterLease.idbDelete(db, this.storeId);
|
||||
db.close();
|
||||
} catch {
|
||||
// Best-effort release.
|
||||
}
|
||||
|
||||
this.storeId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the lease for `storeId` is stale (the holder has stopped
|
||||
* sending heartbeats).
|
||||
*
|
||||
* @param storeId - Store identifier.
|
||||
* @param thresholdMs - Staleness threshold (default 30 000 ms).
|
||||
*/
|
||||
static async isStale(
|
||||
storeId: string,
|
||||
thresholdMs: number = BrowserWriterLease.DEFAULT_STALE_MS
|
||||
): Promise<boolean> {
|
||||
if (typeof indexedDB === 'undefined') return true;
|
||||
|
||||
const db = await BrowserWriterLease.openDb();
|
||||
const record = await BrowserWriterLease.idbGet(db, storeId);
|
||||
db.close();
|
||||
|
||||
if (!record) return true;
|
||||
return Date.now() - record.ts > thresholdMs;
|
||||
}
|
||||
|
||||
// ---- private helpers ----
|
||||
|
||||
private _holderId: string | null = null;
|
||||
|
||||
private holderId(): string {
|
||||
if (!this._holderId) {
|
||||
this._holderId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
return this._holderId;
|
||||
}
|
||||
|
||||
private startHeartbeat(db: IDBDatabase): void {
|
||||
this.stopHeartbeat();
|
||||
const storeId = this.storeId!;
|
||||
const holder = this.holderId();
|
||||
|
||||
const beat = async () => {
|
||||
try {
|
||||
const freshDb = await BrowserWriterLease.openDb();
|
||||
await BrowserWriterLease.idbPut(freshDb, {
|
||||
id: storeId,
|
||||
holder,
|
||||
ts: Date.now(),
|
||||
});
|
||||
freshDb.close();
|
||||
} catch {
|
||||
// Heartbeat failures are non-fatal.
|
||||
}
|
||||
};
|
||||
|
||||
this.heartbeatInterval = setInterval(
|
||||
beat,
|
||||
BrowserWriterLease.HEARTBEAT_MS
|
||||
) as unknown as number;
|
||||
}
|
||||
|
||||
private stopHeartbeat(): void {
|
||||
if (this.heartbeatInterval !== null) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
private registerUnloadHandler(): void {
|
||||
if (typeof globalThis.addEventListener === 'function') {
|
||||
const handler = () => {
|
||||
this.stopHeartbeat();
|
||||
// Synchronous best-effort release — IndexedDB is unavailable during
|
||||
// unload in some browsers so we just stop the heartbeat, letting the
|
||||
// lease expire via staleness detection.
|
||||
};
|
||||
globalThis.addEventListener('beforeunload', handler, { once: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Epoch Sync ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Describes the synchronisation state between the RVF vector store epoch
|
||||
* and the metadata (SQL / Cypher / SPARQL) epoch.
|
||||
*/
|
||||
export interface EpochState {
|
||||
/** Monotonic epoch counter for the RVF vector store. */
|
||||
rvfEpoch: number;
|
||||
/** Monotonic epoch counter for metadata stores. */
|
||||
metadataEpoch: number;
|
||||
/** Human-readable sync status. */
|
||||
status: 'synchronized' | 'rvf_ahead' | 'metadata_ahead';
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect the current epoch state of an RvLite instance.
|
||||
*
|
||||
* The epochs are stored as metadata keys inside the database itself
|
||||
* (`_rvlite_rvf_epoch` and `_rvlite_metadata_epoch`).
|
||||
*
|
||||
* @param db - An initialised RvLite instance.
|
||||
* @returns The current epoch state.
|
||||
*/
|
||||
export async function checkEpochSync(db: RvLite): Promise<EpochState> {
|
||||
const rvfEntry = await db.get('_rvlite_rvf_epoch');
|
||||
const metaEntry = await db.get('_rvlite_metadata_epoch');
|
||||
|
||||
const rvfEpoch = rvfEntry?.metadata?.epoch as number ?? 0;
|
||||
const metadataEpoch = metaEntry?.metadata?.epoch as number ?? 0;
|
||||
|
||||
let status: EpochState['status'];
|
||||
if (rvfEpoch === metadataEpoch) {
|
||||
status = 'synchronized';
|
||||
} else if (rvfEpoch > metadataEpoch) {
|
||||
status = 'rvf_ahead';
|
||||
} else {
|
||||
status = 'metadata_ahead';
|
||||
}
|
||||
|
||||
return { rvfEpoch, metadataEpoch, status };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconcile mismatched epochs by advancing the lagging store to match
|
||||
* the leading one.
|
||||
*
|
||||
* - **rvf_ahead**: bumps the metadata epoch to match the RVF epoch.
|
||||
* - **metadata_ahead**: bumps the RVF epoch to match the metadata epoch.
|
||||
* - **synchronized**: no-op.
|
||||
*
|
||||
* @param db - An initialised RvLite instance.
|
||||
* @param state - The epoch state (as returned by `checkEpochSync`).
|
||||
*/
|
||||
export async function reconcileEpochs(
|
||||
db: RvLite,
|
||||
state: EpochState
|
||||
): Promise<void> {
|
||||
if (state.status === 'synchronized') return;
|
||||
|
||||
const targetEpoch = Math.max(state.rvfEpoch, state.metadataEpoch);
|
||||
const dummyVector = [0]; // minimal placeholder vector
|
||||
|
||||
// Upsert both epoch sentinel records to the target epoch.
|
||||
// We use insertWithId so the key is deterministic.
|
||||
try { await db.delete('_rvlite_rvf_epoch'); } catch { /* may not exist */ }
|
||||
try { await db.delete('_rvlite_metadata_epoch'); } catch { /* may not exist */ }
|
||||
|
||||
await db.insertWithId('_rvlite_rvf_epoch', dummyVector, { epoch: targetEpoch });
|
||||
await db.insertWithId('_rvlite_metadata_epoch', dummyVector, { epoch: targetEpoch });
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience helper: increment the RVF epoch by 1.
|
||||
* Call this after every successful vector-store mutation.
|
||||
*/
|
||||
export async function bumpRvfEpoch(db: RvLite): Promise<number> {
|
||||
const current = await checkEpochSync(db);
|
||||
const next = current.rvfEpoch + 1;
|
||||
const dummyVector = [0];
|
||||
try { await db.delete('_rvlite_rvf_epoch'); } catch { /* ignore */ }
|
||||
await db.insertWithId('_rvlite_rvf_epoch', dummyVector, { epoch: next });
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience helper: increment the metadata epoch by 1.
|
||||
* Call this after every successful metadata mutation (SQL / Cypher / SPARQL).
|
||||
*/
|
||||
export async function bumpMetadataEpoch(db: RvLite): Promise<number> {
|
||||
const current = await checkEpochSync(db);
|
||||
const next = current.metadataEpoch + 1;
|
||||
const dummyVector = [0];
|
||||
try { await db.delete('_rvlite_metadata_epoch'); } catch { /* ignore */ }
|
||||
await db.insertWithId('_rvlite_metadata_epoch', dummyVector, { epoch: next });
|
||||
return next;
|
||||
}
|
||||
|
||||
export default RvLite;
|
||||
20
vendor/ruvector/npm/packages/rvlite/tsconfig.json
vendored
Normal file
20
vendor/ruvector/npm/packages/rvlite/tsconfig.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "bin"]
|
||||
}
|
||||
Reference in New Issue
Block a user