mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
refactor: migrate from KuzuDB to LadybugDB v0.15 (#275)
* refactor: migrate from KuzuDB to LadybugDB v0.15 KuzuDB was archived (Apple acquisition, Oct 2025). LadybugDB is the community fork with full API compatibility. - Package swap: kuzu → @ladybugdb/core, kuzu-wasm → @ladybugdb/wasm-core - Rename all internal paths: kuzu → lbug (adapters, schema, storage) - Storage path: .gitnexus/kuzu → .gitnexus/lbug (with auto-cleanup) - Add explicit VECTOR extension loading (required in v0.15) - Update CI workflow, documentation, and all tests - 1151 unit + 27 integration tests passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address code review findings (P1-P3) P1: Fix WASM adapter to use getAll() API, wire cleanupOldKuzuFiles into analyze command, add symlink path traversal protection. P2: Cache VECTOR extension load state, batch augmentation engine queries (20→4), fix web getCopyQuery for multi-language tables, fix stale KuzuDB references, correct brainstorm package names. P3: Complete lbug-wasm.d.ts type declarations, batch semantic search per-label, update stale BM25 comment. * chore: remove outdated KuzuDB migration brainstorming document * fix: load FTS extension in MCP pool adapter on init The read-only pool adapter never loaded the FTS extension, so all QUERY_FTS_INDEX calls failed silently. This broke search-pool and augmentation integration tests, and caused empty results in the web UI server mode. * feat: implement shared Database caching and connection reference counting * feat: enhance KuzuDB migration handling and status reporting * fix: mock cleanupOldKuzuFiles in local backend callTool tests * fix: update mock for cleanupOldKuzuFiles and adjust imports in callTool tests --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
62242d5f44
commit
5a5850832c
67 changed files with 1208 additions and 1358 deletions
36
.github/workflows/ci-integration.yml
vendored
36
.github/workflows/ci-integration.yml
vendored
|
|
@ -12,29 +12,29 @@ on:
|
|||
jobs:
|
||||
# ── Integration test matrix ─────────────────────────────────────────
|
||||
# Each test-group runs on a SEPARATE runner per OS, giving full process
|
||||
# isolation for the KuzuDB native C++ addon.
|
||||
# isolation for the LadybugDB native C++ addon.
|
||||
# 3 OS x 4 groups = 12 parallel jobs.
|
||||
#
|
||||
# Groups:
|
||||
# kuzu-db — 7 files using withTestKuzuDB / kuzu-adapter (native addon)
|
||||
# lbug-db — 7 files using withTestLbugDB / lbug-adapter (native addon)
|
||||
# Each file runs as its own `vitest run` invocation for full
|
||||
# process isolation. KuzuDB's native N-API addon registers
|
||||
# process isolation. LadybugDB's native N-API addon registers
|
||||
# persistent handles that prevent fork workers from exiting
|
||||
# on Linux, and its C++ destructors segfault during
|
||||
# process.exit(). Running each file in its own process lets
|
||||
# the OS reclaim all resources cleanly.
|
||||
# pipeline — 13 files: ingestion pipeline + csv + 10 resolver tests
|
||||
# e2e — 2 files: child-process only (spawnSync), no in-process kuzu
|
||||
# standalone — 4 files: pure logic, no kuzu, no child processes
|
||||
# pipeline — 12 files: ingestion pipeline + csv + 9 resolver tests
|
||||
# e2e — 2 files: child-process only (spawnSync), no in-process lbug
|
||||
# standalone — 4 files: pure logic, no lbug, no child processes
|
||||
test-matrix:
|
||||
name: integration (${{ matrix.os }} / ${{ matrix.test-group }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
test-group: [kuzu-db, pipeline, e2e, standalone]
|
||||
test-group: [lbug-db, pipeline, e2e, standalone]
|
||||
include:
|
||||
- test-group: kuzu-db
|
||||
- test-group: lbug-db
|
||||
# Marker — actual files are listed in the run step below
|
||||
test-glob: ''
|
||||
- test-group: pipeline
|
||||
|
|
@ -72,18 +72,18 @@ jobs:
|
|||
with:
|
||||
build: 'true'
|
||||
|
||||
# kuzu-db: run each file in its own vitest process for full isolation.
|
||||
# KuzuDB's native addon hangs fork workers on Linux — process isolation
|
||||
# lbug-db: run each file in its own vitest process for full isolation.
|
||||
# LadybugDB's native addon hangs fork workers on Linux — process isolation
|
||||
# is the only reliable fix boundary.
|
||||
- name: Run integration tests — kuzu-db (process-isolated)
|
||||
if: matrix.test-group == 'kuzu-db'
|
||||
- name: Run integration tests — lbug-db (process-isolated)
|
||||
if: matrix.test-group == 'lbug-db'
|
||||
working-directory: gitnexus
|
||||
shell: bash
|
||||
run: |
|
||||
set -e
|
||||
files=(
|
||||
test/integration/kuzu-core-adapter.test.ts
|
||||
test/integration/kuzu-pool.test.ts
|
||||
test/integration/lbug-core-adapter.test.ts
|
||||
test/integration/lbug-pool.test.ts
|
||||
test/integration/local-backend.test.ts
|
||||
test/integration/local-backend-calltool.test.ts
|
||||
test/integration/search-core.test.ts
|
||||
|
|
@ -101,9 +101,9 @@ jobs:
|
|||
done
|
||||
exit $exit_code
|
||||
|
||||
# Non-kuzu groups: run all files in a single vitest invocation
|
||||
# Non-lbug groups: run all files in a single vitest invocation
|
||||
- name: Run integration tests — ${{ matrix.test-group }}
|
||||
if: matrix.test-group != 'kuzu-db'
|
||||
if: matrix.test-group != 'lbug-db'
|
||||
shell: bash
|
||||
env:
|
||||
TEST_GLOB: ${{ matrix.test-glob }}
|
||||
|
|
@ -111,9 +111,9 @@ jobs:
|
|||
working-directory: gitnexus
|
||||
|
||||
# ── Coverage collection (ubuntu only) ─────────────────────────────────
|
||||
# Runs non-kuzu integration tests with coverage enabled so the PR report
|
||||
# Runs non-lbug integration tests with coverage enabled so the PR report
|
||||
# can merge integration + unit coverage for a combined view.
|
||||
# kuzu-db tests are excluded because each file must run in its own vitest
|
||||
# lbug-db tests are excluded because each file must run in its own vitest
|
||||
# process (native addon isolation) which prevents single-run coverage merge.
|
||||
coverage:
|
||||
name: integration (ubuntu / coverage)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
All notable changes to GitNexus will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
- Migrated from KuzuDB to LadybugDB v0.15 (`@ladybugdb/core`, `@ladybugdb/wasm-core`)
|
||||
- Renamed all internal paths from `kuzu` to `lbug` (storage: `.gitnexus/kuzu` → `.gitnexus/lbug`)
|
||||
- Added automatic cleanup of stale KuzuDB index files
|
||||
- LadybugDB v0.15 requires explicit VECTOR extension loading for semantic search
|
||||
|
||||
## [1.4.0] - 2026-03-13
|
||||
|
||||
### Added
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -51,7 +51,7 @@ https://github.com/user-attachments/assets/172685ba-8e54-4ea7-9ad1-e31a3398da72
|
|||
| **For** | Daily development with Cursor, Claude Code, Windsurf, OpenCode | Quick exploration, demos, one-off analysis |
|
||||
| **Scale** | Full repos, any size | Limited by browser memory (~5k files), or unlimited via backend mode |
|
||||
| **Install** | `npm install -g gitnexus` | No install —[gitnexus.vercel.app](https://gitnexus.vercel.app) |
|
||||
| **Storage** | KuzuDB native (fast, persistent) | KuzuDB WASM (in-memory, per session) |
|
||||
| **Storage** | LadybugDB native (fast, persistent) | LadybugDB WASM (in-memory, per session) |
|
||||
| **Parsing** | Tree-sitter native bindings | Tree-sitter WASM |
|
||||
| **Privacy** | Everything local, no network | Everything in-browser, no server |
|
||||
|
||||
|
|
@ -224,8 +224,8 @@ flowchart TD
|
|||
Server["server.ts"]
|
||||
Backend["LocalBackend"]
|
||||
Pool["Connection Pool"]
|
||||
ConnA["KuzuDB conn A"]
|
||||
ConnB["KuzuDB conn B"]
|
||||
ConnA["LadybugDB conn A"]
|
||||
ConnB["LadybugDB conn B"]
|
||||
end
|
||||
|
||||
Setup -->|"writes global MCP config"| CursorConfig["~/.cursor/mcp.json"]
|
||||
|
|
@ -242,7 +242,7 @@ flowchart TD
|
|||
ConnB -->|"queries"| RepoB
|
||||
```
|
||||
|
||||
**How it works:** Each `gitnexus analyze` stores the index in `.gitnexus/` inside the repo (portable, gitignored) and registers a pointer in `~/.gitnexus/registry.json`. When an AI agent starts, the MCP server reads the registry and can serve any indexed repo. KuzuDB connections are opened lazily on first query and evicted after 5 minutes of inactivity (max 5 concurrent). If only one repo is indexed, the `repo` parameter is optional on all tools — agents don't need to change anything.
|
||||
**How it works:** Each `gitnexus analyze` stores the index in `.gitnexus/` inside the repo (portable, gitignored) and registers a pointer in `~/.gitnexus/registry.json`. When an AI agent starts, the MCP server reads the registry and can serve any indexed repo. LadybugDB connections are opened lazily on first query and evicted after 5 minutes of inactivity (max 5 concurrent). If only one repo is indexed, the `repo` parameter is optional on all tools — agents don't need to change anything.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -263,7 +263,7 @@ npm install
|
|||
npm run dev
|
||||
```
|
||||
|
||||
The web UI uses the same indexing pipeline as the CLI but runs entirely in WebAssembly (Tree-sitter WASM, KuzuDB WASM, in-browser embeddings). It's great for quick exploration but limited by browser memory for larger repos.
|
||||
The web UI uses the same indexing pipeline as the CLI but runs entirely in WebAssembly (Tree-sitter WASM, LadybugDB WASM, in-browser embeddings). It's great for quick exploration but limited by browser memory for larger repos.
|
||||
|
||||
**Local Backend Mode:** Run `gitnexus serve` and open the web UI locally — it auto-detects the server and shows all your indexed repos, with full AI chat support. No need to re-upload or re-index. The agent's tools (Cypher queries, search, code navigation) route through the backend HTTP API automatically.
|
||||
|
||||
|
|
@ -482,7 +482,7 @@ The wiki generator reads the indexed graph structure, groups files into modules
|
|||
| ------------------------- | ------------------------------------- | --------------------------------------- |
|
||||
| **Runtime** | Node.js (native) | Browser (WASM) |
|
||||
| **Parsing** | Tree-sitter native bindings | Tree-sitter WASM |
|
||||
| **Database** | KuzuDB native | KuzuDB WASM |
|
||||
| **Database** | LadybugDB native | LadybugDB WASM |
|
||||
| **Embeddings** | HuggingFace transformers.js (GPU/CPU) | transformers.js (WebGPU/WASM) |
|
||||
| **Search** | BM25 + semantic + RRF | BM25 + semantic + RRF |
|
||||
| **Agent Interface** | MCP (stdio) | LangChain ReAct agent |
|
||||
|
|
@ -523,7 +523,7 @@ The wiki generator reads the indexed graph structure, groups files into modules
|
|||
## Acknowledgments
|
||||
|
||||
- [Tree-sitter](https://tree-sitter.github.io/) — AST parsing
|
||||
- [KuzuDB](https://kuzudb.com/) — Embedded graph database with vector support
|
||||
- [LadybugDB](https://ladybugdb.com/) — Embedded graph database with vector support (formerly KuzuDB)
|
||||
- [Sigma.js](https://www.sigmajs.org/) — WebGL graph rendering
|
||||
- [transformers.js](https://huggingface.co/docs/transformers.js) — Browser ML
|
||||
- [Graphology](https://graphology.github.io/) — Graph data structures
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ Each mode has a `system_{mode}.jinja` + `instance_{mode}.jinja` pair. The agent
|
|||
|
||||
1. Docker container starts with SWE-bench instance (repo at specific commit)
|
||||
2. **GitNexus setup**: Node.js + gitnexus installed, `gitnexus analyze` runs (or restores from cache)
|
||||
3. **Eval-server starts**: `gitnexus eval-server` daemon (persistent HTTP server, keeps KuzuDB warm)
|
||||
3. **Eval-server starts**: `gitnexus eval-server` daemon (persistent HTTP server, keeps LadybugDB warm)
|
||||
4. **Standalone tool scripts installed** in `/usr/local/bin/` — works with `subprocess.run` (no `.bashrc` needed)
|
||||
5. Agent runs with the configured model + system prompt + GitNexus tools
|
||||
6. Agent's patch is extracted as a git diff
|
||||
|
|
@ -167,7 +167,7 @@ Each tool script in `/usr/local/bin/` is standalone — no sourcing, no env inhe
|
|||
### Eval-server
|
||||
|
||||
The eval-server is a lightweight HTTP daemon that:
|
||||
- Keeps KuzuDB warm in memory (no cold start per tool call)
|
||||
- Keeps LadybugDB warm in memory (no cold start per tool call)
|
||||
- Returns LLM-friendly text (not raw JSON — saves tokens)
|
||||
- Includes next-step hints to guide tool chaining (query → context → impact → fix)
|
||||
- Auto-shuts down after idle timeout
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ function handlePreToolUse(input) {
|
|||
* PostToolUse handler — detect index staleness after git mutations.
|
||||
*
|
||||
* Instead of spawning a full `gitnexus analyze` synchronously (which blocks
|
||||
* the agent for up to 120s and risks KuzuDB corruption on timeout), we do a
|
||||
* the agent for up to 120s and risks LadybugDB corruption on timeout), we do a
|
||||
* lightweight staleness check: compare `git rev-parse HEAD` against the
|
||||
* lastCommit stored in `.gitnexus/meta.json`. If they differ, notify the
|
||||
* agent so it can decide when to reindex.
|
||||
|
|
|
|||
51
gitnexus-web/package-lock.json
generated
51
gitnexus-web/package-lock.json
generated
|
|
@ -10,6 +10,7 @@
|
|||
"dependencies": {
|
||||
"@huggingface/transformers": "^3.0.0",
|
||||
"@isomorphic-git/lightning-fs": "^4.6.2",
|
||||
"@ladybugdb/wasm-core": "^0.15.1",
|
||||
"@langchain/anthropic": "^1.3.10",
|
||||
"@langchain/core": "^1.1.15",
|
||||
"@langchain/google-genai": "^2.1.10",
|
||||
|
|
@ -30,7 +31,6 @@
|
|||
"graphology-utils": "^2.3.0",
|
||||
"isomorphic-git": "^1.36.1",
|
||||
"jszip": "^3.10.1",
|
||||
"kuzu-wasm": "^0.11.1",
|
||||
"langchain": "^1.2.10",
|
||||
"lru-cache": "^11.2.4",
|
||||
"lucide-react": "^0.562.0",
|
||||
|
|
@ -1643,6 +1643,30 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@ladybugdb/wasm-core": {
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@ladybugdb/wasm-core/-/wasm-core-0.15.1.tgz",
|
||||
"integrity": "sha512-dHEq8inJQBkHnJrqZMKGdltSfeSv9OHECkzWQixqDLApXXGlbJ5Ugq5rRfk2PLJuZ74LVHT0cZvcn4JLmsnAIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"threads": "^1.7.0",
|
||||
"tiny-worker": "^2.3.0",
|
||||
"uuid": "^11.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@ladybugdb/wasm-core/node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/anthropic": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.3.10.tgz",
|
||||
|
|
@ -6194,31 +6218,6 @@
|
|||
"resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz",
|
||||
"integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
|
||||
},
|
||||
"node_modules/kuzu-wasm": {
|
||||
"version": "0.11.3",
|
||||
"resolved": "https://registry.npmjs.org/kuzu-wasm/-/kuzu-wasm-0.11.3.tgz",
|
||||
"integrity": "sha512-+bLOqXgYZJJ2dHJG1y9LTLyb9ZB73eLxErRZahZz2rPokfIdyLaktTJFzJH7wX39hgyukKn8QxeRNobH6gl27g==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"threads": "^1.7.0",
|
||||
"tiny-worker": "^2.3.0",
|
||||
"uuid": "^11.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/kuzu-wasm/node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/langchain": {
|
||||
"version": "1.2.10",
|
||||
"resolved": "https://registry.npmjs.org/langchain/-/langchain-1.2.10.tgz",
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
"graphology-layout-noverlap": "^0.4.2",
|
||||
"isomorphic-git": "^1.36.1",
|
||||
"jszip": "^3.10.1",
|
||||
"kuzu-wasm": "^0.11.1",
|
||||
"@ladybugdb/wasm-core": "^0.15.1",
|
||||
"langchain": "^1.2.10",
|
||||
"lru-cache": "^11.2.4",
|
||||
"lucide-react": "^0.562.0",
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -83,7 +83,7 @@ export const EmbeddingStatus = () => {
|
|||
<button
|
||||
onClick={handleTestArrayParams}
|
||||
className="flex items-center gap-1 px-2 py-1.5 bg-surface border border-border-subtle rounded-lg text-xs text-text-muted hover:bg-hover hover:text-text-secondary transition-all"
|
||||
title="Test if KuzuDB supports array params"
|
||||
title="Test if LadybugDB supports array params"
|
||||
>
|
||||
<FlaskConical className="w-3 h-3" />
|
||||
{testResult || 'Test'}
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ export const embedBatch = async (texts: string[]): Promise<Float32Array[]> => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Convert Float32Array to regular number array (for KuzuDB storage)
|
||||
* Convert Float32Array to regular number array (for LadybugDB storage)
|
||||
*/
|
||||
export const embeddingToArray = (embedding: Float32Array): number[] => {
|
||||
return Array.from(embedding);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
* Embedding Pipeline Module
|
||||
*
|
||||
* Orchestrates the background embedding process:
|
||||
* 1. Query embeddable nodes from KuzuDB
|
||||
* 1. Query embeddable nodes from LadybugDB
|
||||
* 2. Generate text representations
|
||||
* 3. Batch embed using transformers.js
|
||||
* 4. Update KuzuDB with embeddings
|
||||
* 4. Update LadybugDB with embeddings
|
||||
* 5. Create vector index for semantic search
|
||||
*/
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ import {
|
|||
export type EmbeddingProgressCallback = (progress: EmbeddingProgress) => void;
|
||||
|
||||
/**
|
||||
* Query all embeddable nodes from KuzuDB
|
||||
* Query all embeddable nodes from LadybugDB
|
||||
* Uses table-specific queries (File has different schema than code elements)
|
||||
*/
|
||||
const queryEmbeddableNodes = async (
|
||||
|
|
@ -102,9 +102,23 @@ const batchInsertEmbeddings = async (
|
|||
* Create the vector index for semantic search
|
||||
* Now indexes the separate CodeEmbedding table
|
||||
*/
|
||||
let vectorExtensionLoaded = false;
|
||||
|
||||
const createVectorIndex = async (
|
||||
executeQuery: (cypher: string) => Promise<any[]>
|
||||
): Promise<void> => {
|
||||
// LadybugDB v0.15+ requires explicit VECTOR extension loading (once per session)
|
||||
if (!vectorExtensionLoaded) {
|
||||
try {
|
||||
await executeQuery('INSTALL VECTOR');
|
||||
await executeQuery('LOAD EXTENSION VECTOR');
|
||||
vectorExtensionLoaded = true;
|
||||
} catch {
|
||||
// Extension may already be loaded — CREATE_VECTOR_INDEX will fail clearly if not
|
||||
vectorExtensionLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
const cypher = `
|
||||
CALL CREATE_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx', 'embedding', metric := 'cosine')
|
||||
`;
|
||||
|
|
@ -122,7 +136,7 @@ const createVectorIndex = async (
|
|||
/**
|
||||
* Run the embedding pipeline
|
||||
*
|
||||
* @param executeQuery - Function to execute Cypher queries against KuzuDB
|
||||
* @param executeQuery - Function to execute Cypher queries against LadybugDB
|
||||
* @param executeWithReusedStatement - Function to execute with reused prepared statement
|
||||
* @param onProgress - Callback for progress updates
|
||||
* @param config - Optional configuration override
|
||||
|
|
@ -206,7 +220,7 @@ export const runEmbeddingPipeline = async (
|
|||
// Embed the batch
|
||||
const embeddings = await embedBatch(texts);
|
||||
|
||||
// Update KuzuDB with embeddings
|
||||
// Update LadybugDB with embeddings
|
||||
const updates = batch.map((node, i) => ({
|
||||
id: node.id,
|
||||
embedding: embeddingToArray(embeddings[i]),
|
||||
|
|
@ -313,51 +327,64 @@ export const semanticSearch = async (
|
|||
return [];
|
||||
}
|
||||
|
||||
// Get metadata for each result by querying each node table
|
||||
const results: SemanticSearchResult[] = [];
|
||||
|
||||
// Group results by label for batched metadata queries
|
||||
const byLabel = new Map<string, Array<{ nodeId: string; distance: number }>>();
|
||||
for (const embRow of embResults) {
|
||||
const nodeId = embRow.nodeId ?? embRow[0];
|
||||
const distance = embRow.distance ?? embRow[1];
|
||||
|
||||
// Extract label from node ID (format: Label:path:name)
|
||||
const labelEndIdx = nodeId.indexOf(':');
|
||||
const label = labelEndIdx > 0 ? nodeId.substring(0, labelEndIdx) : 'Unknown';
|
||||
|
||||
// Query the specific table for this node
|
||||
// File nodes don't have startLine/endLine
|
||||
if (!byLabel.has(label)) byLabel.set(label, []);
|
||||
byLabel.get(label)!.push({ nodeId, distance });
|
||||
}
|
||||
|
||||
// Batch-fetch metadata per label
|
||||
const results: SemanticSearchResult[] = [];
|
||||
|
||||
for (const [label, items] of byLabel) {
|
||||
const idList = items.map(i => `'${i.nodeId.replace(/'/g, "''")}'`).join(', ');
|
||||
try {
|
||||
let nodeQuery: string;
|
||||
if (label === 'File') {
|
||||
nodeQuery = `
|
||||
MATCH (n:File {id: '${nodeId.replace(/'/g, "''")}'})
|
||||
RETURN n.name AS name, n.filePath AS filePath
|
||||
MATCH (n:File) WHERE n.id IN [${idList}]
|
||||
RETURN n.id AS id, n.name AS name, n.filePath AS filePath
|
||||
`;
|
||||
} else {
|
||||
nodeQuery = `
|
||||
MATCH (n:${label} {id: '${nodeId.replace(/'/g, "''")}'})
|
||||
RETURN n.name AS name, n.filePath AS filePath,
|
||||
MATCH (n:${label}) WHERE n.id IN [${idList}]
|
||||
RETURN n.id AS id, n.name AS name, n.filePath AS filePath,
|
||||
n.startLine AS startLine, n.endLine AS endLine
|
||||
`;
|
||||
}
|
||||
const nodeRows = await executeQuery(nodeQuery);
|
||||
if (nodeRows.length > 0) {
|
||||
const nodeRow = nodeRows[0];
|
||||
results.push({
|
||||
nodeId,
|
||||
name: nodeRow.name ?? nodeRow[0] ?? '',
|
||||
label,
|
||||
filePath: nodeRow.filePath ?? nodeRow[1] ?? '',
|
||||
distance,
|
||||
startLine: label !== 'File' ? (nodeRow.startLine ?? nodeRow[2]) : undefined,
|
||||
endLine: label !== 'File' ? (nodeRow.endLine ?? nodeRow[3]) : undefined,
|
||||
});
|
||||
const rowMap = new Map<string, any>();
|
||||
for (const row of nodeRows) {
|
||||
const id = row.id ?? row[0];
|
||||
rowMap.set(id, row);
|
||||
}
|
||||
for (const item of items) {
|
||||
const nodeRow = rowMap.get(item.nodeId);
|
||||
if (nodeRow) {
|
||||
results.push({
|
||||
nodeId: item.nodeId,
|
||||
name: nodeRow.name ?? nodeRow[1] ?? '',
|
||||
label,
|
||||
filePath: nodeRow.filePath ?? nodeRow[2] ?? '',
|
||||
distance: item.distance,
|
||||
startLine: label !== 'File' ? (nodeRow.startLine ?? nodeRow[3]) : undefined,
|
||||
endLine: label !== 'File' ? (nodeRow.endLine ?? nodeRow[4]) : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Table might not exist, skip
|
||||
}
|
||||
}
|
||||
|
||||
// Re-sort by distance since batch queries may have mixed order
|
||||
results.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export interface SemanticSearchResult {
|
|||
}
|
||||
|
||||
/**
|
||||
* Node data for embedding (minimal structure from KuzuDB query)
|
||||
* Node data for embedding (minimal structure from LadybugDB query)
|
||||
*/
|
||||
export interface EmbeddableNode {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* CSV Generator for KuzuDB Hybrid Schema
|
||||
* CSV Generator for LadybugDB Hybrid Schema
|
||||
*
|
||||
* Generates separate CSV files for each node table and one relation CSV.
|
||||
* This enables efficient bulk loading via COPY FROM for hybrid schema.
|
||||
|
|
@ -18,10 +18,10 @@ import { NODE_TABLES, NodeTableName } from './schema';
|
|||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Sanitize string to ensure valid UTF-8 and safe CSV content for KuzuDB
|
||||
* Sanitize string to ensure valid UTF-8 and safe CSV content for LadybugDB
|
||||
* Removes or replaces invalid characters that would break CSV parsing.
|
||||
*
|
||||
* Critical: KuzuDB's CSV parser can misinterpret \r\n inside quoted fields.
|
||||
* Critical: LadybugDB's CSV parser can misinterpret \r\n inside quoted fields.
|
||||
* We normalize all line endings to \n only.
|
||||
*/
|
||||
const sanitizeUTF8 = (str: string): string => {
|
||||
|
|
@ -213,7 +213,7 @@ const generateCommunityCSV = (nodes: GraphNode[]): string => {
|
|||
for (const node of nodes) {
|
||||
if (node.label !== 'Community') continue;
|
||||
|
||||
// Handle keywords array - convert to KuzuDB array format
|
||||
// Handle keywords array - convert to LadybugDB array format
|
||||
const keywords = (node.properties as any).keywords || [];
|
||||
const keywordsStr = `[${keywords.map((k: string) => `'${k.replace(/'/g, "''")}'`).join(',')}]`;
|
||||
|
||||
|
|
@ -221,7 +221,7 @@ const generateCommunityCSV = (nodes: GraphNode[]): string => {
|
|||
escapeCSVField(node.id),
|
||||
escapeCSVField(node.properties.name || ''), // label is stored in name
|
||||
escapeCSVField(node.properties.heuristicLabel || ''),
|
||||
keywordsStr, // Array format for KuzuDB
|
||||
keywordsStr, // Array format for LadybugDB
|
||||
escapeCSVField((node.properties as any).description || ''),
|
||||
escapeCSVField((node.properties as any).enrichedBy || 'heuristic'),
|
||||
escapeCSVNumber(node.properties.cohesion, 0),
|
||||
|
|
@ -1,51 +1,51 @@
|
|||
/**
|
||||
* KuzuDB Adapter
|
||||
*
|
||||
* Manages the KuzuDB WASM instance for client-side graph database operations.
|
||||
* LadybugDB Adapter
|
||||
*
|
||||
* Manages the LadybugDB WASM instance for client-side graph database operations.
|
||||
* Uses the "Snapshot / Bulk Load" pattern with COPY FROM for performance.
|
||||
*
|
||||
*
|
||||
* Multi-table schema: separate tables for File, Function, Class, etc.
|
||||
*/
|
||||
|
||||
import { KnowledgeGraph } from '../graph/types';
|
||||
import {
|
||||
NODE_TABLES,
|
||||
import {
|
||||
NODE_TABLES,
|
||||
REL_TABLE_NAME,
|
||||
SCHEMA_QUERIES,
|
||||
SCHEMA_QUERIES,
|
||||
EMBEDDING_TABLE_NAME,
|
||||
NodeTableName,
|
||||
} from './schema';
|
||||
import { generateAllCSVs } from './csv-generator';
|
||||
|
||||
// Holds the reference to the dynamically loaded module
|
||||
let kuzu: any = null;
|
||||
let lbug: any = null;
|
||||
let db: any = null;
|
||||
let conn: any = null;
|
||||
|
||||
/**
|
||||
* Initialize KuzuDB WASM module and create in-memory database
|
||||
* Initialize LadybugDB WASM module and create in-memory database
|
||||
*/
|
||||
export const initKuzu = async () => {
|
||||
if (conn) return { db, conn, kuzu };
|
||||
export const initLbug = async () => {
|
||||
if (conn) return { db, conn, lbug };
|
||||
|
||||
try {
|
||||
if (import.meta.env.DEV) console.log('🚀 Initializing KuzuDB...');
|
||||
if (import.meta.env.DEV) console.log('🚀 Initializing LadybugDB...');
|
||||
|
||||
// 1. Dynamic Import (Fixes the "not a function" bundler issue)
|
||||
const kuzuModule = await import('kuzu-wasm');
|
||||
|
||||
const lbugModule = await import('@ladybugdb/wasm-core');
|
||||
|
||||
// 2. Handle Vite/Webpack "default" wrapping
|
||||
kuzu = kuzuModule.default || kuzuModule;
|
||||
lbug = lbugModule.default || lbugModule;
|
||||
|
||||
// 3. Initialize WASM
|
||||
await kuzu.init();
|
||||
|
||||
// 4. Create Database with 512MB buffer pool
|
||||
await lbug.init();
|
||||
|
||||
// 4. Create Database with 512MB buffer manager
|
||||
const BUFFER_POOL_SIZE = 512 * 1024 * 1024; // 512MB
|
||||
db = new kuzu.Database(':memory:', BUFFER_POOL_SIZE);
|
||||
conn = new kuzu.Connection(db);
|
||||
|
||||
if (import.meta.env.DEV) console.log('✅ KuzuDB WASM Initialized');
|
||||
db = new lbug.Database(':memory:', BUFFER_POOL_SIZE);
|
||||
conn = new lbug.Connection(db);
|
||||
|
||||
if (import.meta.env.DEV) console.log('✅ LadybugDB WASM Initialized');
|
||||
|
||||
// 5. Initialize Schema (all node tables, then rel tables, then embedding table)
|
||||
for (const schemaQuery of SCHEMA_QUERIES) {
|
||||
|
|
@ -58,60 +58,60 @@ export const initKuzu = async () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) console.log('✅ KuzuDB Multi-Table Schema Created');
|
||||
|
||||
return { db, conn, kuzu };
|
||||
if (import.meta.env.DEV) console.log('✅ LadybugDB Multi-Table Schema Created');
|
||||
|
||||
return { db, conn, lbug };
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('❌ KuzuDB Initialization Failed:', error);
|
||||
if (import.meta.env.DEV) console.error('❌ LadybugDB Initialization Failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Load a KnowledgeGraph into KuzuDB using COPY FROM (bulk load)
|
||||
* Load a KnowledgeGraph into LadybugDB using COPY FROM (bulk load)
|
||||
* Uses batched CSV writes and COPY statements for optimal performance
|
||||
*/
|
||||
export const loadGraphToKuzu = async (
|
||||
graph: KnowledgeGraph,
|
||||
export const loadGraphToLbug = async (
|
||||
graph: KnowledgeGraph,
|
||||
fileContents: Map<string, string>
|
||||
) => {
|
||||
const { conn, kuzu } = await initKuzu();
|
||||
|
||||
const { conn, lbug } = await initLbug();
|
||||
|
||||
try {
|
||||
if (import.meta.env.DEV) console.log(`KuzuDB: Generating CSVs for ${graph.nodeCount} nodes...`);
|
||||
|
||||
if (import.meta.env.DEV) console.log(`LadybugDB: Generating CSVs for ${graph.nodeCount} nodes...`);
|
||||
|
||||
// 1. Generate all CSVs (per-table)
|
||||
const csvData = generateAllCSVs(graph, fileContents);
|
||||
|
||||
const fs = kuzu.FS;
|
||||
|
||||
|
||||
const fs = lbug.FS;
|
||||
|
||||
// 2. Write all node CSVs to virtual filesystem
|
||||
const nodeFiles: Array<{ table: NodeTableName; path: string }> = [];
|
||||
for (const [tableName, csv] of csvData.nodes.entries()) {
|
||||
// Skip empty CSVs (only header row)
|
||||
if (csv.split('\n').length <= 1) continue;
|
||||
|
||||
|
||||
const path = `/${tableName.toLowerCase()}.csv`;
|
||||
try { await fs.unlink(path); } catch {}
|
||||
await fs.writeFile(path, csv);
|
||||
nodeFiles.push({ table: tableName, path });
|
||||
}
|
||||
|
||||
|
||||
// 3. Parse relation CSV and prepare for INSERT (COPY FROM doesn't work with multi-pair tables)
|
||||
const relLines = csvData.relCSV.split('\n').slice(1).filter(line => line.trim());
|
||||
const relCount = relLines.length;
|
||||
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(`KuzuDB: Wrote ${nodeFiles.length} node CSVs, ${relCount} relations to insert`);
|
||||
console.log(`LadybugDB: Wrote ${nodeFiles.length} node CSVs, ${relCount} relations to insert`);
|
||||
}
|
||||
|
||||
|
||||
// 4. COPY all node tables (must complete before rels due to FK constraints)
|
||||
for (const { table, path } of nodeFiles) {
|
||||
const copyQuery = getCopyQuery(table, path);
|
||||
await conn.query(copyQuery);
|
||||
}
|
||||
|
||||
|
||||
// 5. INSERT relations one by one (COPY doesn't work with multi-pair REL tables)
|
||||
// Build a set of valid table names for fast lookup
|
||||
const validTables = new Set<string>(NODE_TABLES as readonly string[]);
|
||||
|
|
@ -135,13 +135,13 @@ export const loadGraphToKuzu = async (
|
|||
// Format: "from","to","type",confidence,"reason",step
|
||||
const match = line.match(/"([^"]*)","([^"]*)","([^"]*)",([0-9.]+),"([^"]*)",([0-9-]+)/);
|
||||
if (!match) continue;
|
||||
|
||||
|
||||
const [, fromId, toId, relType, confidenceStr, reason, stepStr] = match;
|
||||
|
||||
const fromLabel = getNodeLabel(fromId);
|
||||
const toLabel = getNodeLabel(toId);
|
||||
|
||||
// Skip relationships where either node's label doesn't have a table in KuzuDB
|
||||
// Skip relationships where either node's label doesn't have a table in LadybugDB
|
||||
// Querying a non-existent table causes a fatal native crash
|
||||
if (!validTables.has(fromLabel) || !validTables.has(toLabel)) {
|
||||
skippedRels++;
|
||||
|
|
@ -150,7 +150,7 @@ export const loadGraphToKuzu = async (
|
|||
|
||||
const confidence = parseFloat(confidenceStr) || 1.0;
|
||||
const step = parseInt(stepStr) || 0;
|
||||
|
||||
|
||||
const insertQuery = `
|
||||
MATCH (a:${escapeLabel(fromLabel)} {id: '${fromId.replace(/'/g, "''")}'}),
|
||||
(b:${escapeLabel(toLabel)} {id: '${toId.replace(/'/g, "''")}'})
|
||||
|
|
@ -167,38 +167,39 @@ export const loadGraphToKuzu = async (
|
|||
const toLabel = getNodeLabel(toId);
|
||||
const key = `${relType}:${fromLabel}->` + toLabel;
|
||||
skippedRelStats.set(key, (skippedRelStats.get(key) || 0) + 1);
|
||||
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn(`⚠️ Skipped: ${key} | "${fromId}" → "${toId}" | ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(`KuzuDB: Inserted ${insertedRels}/${relCount} relations`);
|
||||
console.log(`LadybugDB: Inserted ${insertedRels}/${relCount} relations`);
|
||||
if (skippedRels > 0) {
|
||||
const topSkipped = Array.from(skippedRelStats.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10);
|
||||
console.warn(`KuzuDB: Skipped ${skippedRels}/${relCount} relations (top by kind/pair):`, topSkipped);
|
||||
console.warn(`LadybugDB: Skipped ${skippedRels}/${relCount} relations (top by kind/pair):`, topSkipped);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 6. Verify results
|
||||
let totalNodes = 0;
|
||||
for (const tableName of NODE_TABLES) {
|
||||
try {
|
||||
const countRes = await conn.query(`MATCH (n:${tableName}) RETURN count(n) AS cnt`);
|
||||
const countRow = await countRes.getNext();
|
||||
const countRows = await countRes.getAll();
|
||||
const countRow = countRows[0];
|
||||
const count = countRow ? (countRow.cnt ?? countRow[0] ?? 0) : 0;
|
||||
totalNodes += Number(count);
|
||||
} catch {
|
||||
// Table might be empty, skip
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) console.log(`✅ KuzuDB Bulk Load Complete. Total nodes: ${totalNodes}, edges: ${insertedRels}`);
|
||||
|
||||
if (import.meta.env.DEV) console.log(`✅ LadybugDB Bulk Load Complete. Total nodes: ${totalNodes}, edges: ${insertedRels}`);
|
||||
|
||||
// 7. Cleanup CSV files
|
||||
for (const { path } of nodeFiles) {
|
||||
|
|
@ -208,12 +209,12 @@ export const loadGraphToKuzu = async (
|
|||
return { success: true, count: totalNodes };
|
||||
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('❌ KuzuDB Bulk Load Failed:', error);
|
||||
if (import.meta.env.DEV) console.error('❌ LadybugDB Bulk Load Failed:', error);
|
||||
return { success: false, count: 0 };
|
||||
}
|
||||
};
|
||||
|
||||
// KuzuDB default ESCAPE is '\' (backslash), but our CSV uses RFC 4180 escaping ("" for literal quotes).
|
||||
// LadybugDB default ESCAPE is '\' (backslash), but our CSV uses RFC 4180 escaping ("" for literal quotes).
|
||||
// Source code content is full of backslashes which confuse the auto-detection.
|
||||
// We MUST explicitly set ESCAPE='"' and disable auto_detect.
|
||||
const COPY_CSV_OPTS = `(HEADER=true, ESCAPE='"', DELIM=',', QUOTE='"', PARALLEL=false, auto_detect=false)`;
|
||||
|
|
@ -229,6 +230,9 @@ const escapeTableName = (table: string): string => {
|
|||
return BACKTICK_TABLES.has(table) ? `\`${table}\`` : table;
|
||||
};
|
||||
|
||||
/** Tables with isExported column (TypeScript/JS-native types) */
|
||||
const TABLES_WITH_EXPORTED = new Set<string>(['Function', 'Class', 'Interface', 'Method', 'CodeElement']);
|
||||
|
||||
/**
|
||||
* Get the COPY query for a node table with correct column mapping
|
||||
*/
|
||||
|
|
@ -246,8 +250,12 @@ const getCopyQuery = (table: NodeTableName, path: string): string => {
|
|||
if (table === 'Process') {
|
||||
return `COPY ${t}(id, label, heuristicLabel, processType, stepCount, communities, entryPointId, terminalId) FROM "${path}" ${COPY_CSV_OPTS}`;
|
||||
}
|
||||
// Code element tables (Function, Class, Interface, Method, CodeElement, and multi-language)
|
||||
return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content) FROM "${path}" ${COPY_CSV_OPTS}`;
|
||||
// TypeScript/JS code element tables have isExported; multi-language tables do not
|
||||
if (TABLES_WITH_EXPORTED.has(table)) {
|
||||
return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content) FROM "${path}" ${COPY_CSV_OPTS}`;
|
||||
}
|
||||
// Multi-language tables (Struct, Impl, Trait, Macro, etc.)
|
||||
return `COPY ${t}(id, name, filePath, startLine, endLine, content) FROM "${path}" ${COPY_CSV_OPTS}`;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -256,12 +264,12 @@ const getCopyQuery = (table: NodeTableName, path: string): string => {
|
|||
*/
|
||||
export const executeQuery = async (cypher: string): Promise<any[]> => {
|
||||
if (!conn) {
|
||||
await initKuzu();
|
||||
await initLbug();
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const result = await conn.query(cypher);
|
||||
|
||||
|
||||
// Extract column names from RETURN clause
|
||||
const returnMatch = cypher.match(/RETURN\s+(.+?)(?:\s+ORDER|\s+LIMIT|\s+SKIP|\s*$)/is);
|
||||
let columnNames: string[] = [];
|
||||
|
|
@ -284,12 +292,11 @@ export const executeQuery = async (cypher: string): Promise<any[]> => {
|
|||
return col.replace(/[^a-zA-Z0-9_]/g, '_');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Collect all rows
|
||||
const allRows = await result.getAll();
|
||||
const rows: any[] = [];
|
||||
while (await result.hasNext()) {
|
||||
const row = await result.getNext();
|
||||
|
||||
for (const row of allRows) {
|
||||
// Convert tuple to named object if we have column names and row is array
|
||||
if (Array.isArray(row) && columnNames.length === row.length) {
|
||||
const namedRow: Record<string, any> = {};
|
||||
|
|
@ -302,7 +309,7 @@ export const executeQuery = async (cypher: string): Promise<any[]> => {
|
|||
rows.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return rows;
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Query execution failed:', error);
|
||||
|
|
@ -313,7 +320,7 @@ export const executeQuery = async (cypher: string): Promise<any[]> => {
|
|||
/**
|
||||
* Get database statistics
|
||||
*/
|
||||
export const getKuzuStats = async (): Promise<{ nodes: number; edges: number }> => {
|
||||
export const getLbugStats = async (): Promise<{ nodes: number; edges: number }> => {
|
||||
if (!conn) {
|
||||
return { nodes: 0, edges: 0 };
|
||||
}
|
||||
|
|
@ -324,43 +331,45 @@ export const getKuzuStats = async (): Promise<{ nodes: number; edges: number }>
|
|||
for (const tableName of NODE_TABLES) {
|
||||
try {
|
||||
const nodeResult = await conn.query(`MATCH (n:${tableName}) RETURN count(n) AS cnt`);
|
||||
const nodeRow = await nodeResult.getNext();
|
||||
const nodeRows = await nodeResult.getAll();
|
||||
const nodeRow = nodeRows[0];
|
||||
totalNodes += Number(nodeRow?.cnt ?? nodeRow?.[0] ?? 0);
|
||||
} catch {
|
||||
// Table might not exist or be empty
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Count edges from single relation table
|
||||
let totalEdges = 0;
|
||||
try {
|
||||
const edgeResult = await conn.query(`MATCH ()-[r:${REL_TABLE_NAME}]->() RETURN count(r) AS cnt`);
|
||||
const edgeRow = await edgeResult.getNext();
|
||||
const edgeRows = await edgeResult.getAll();
|
||||
const edgeRow = edgeRows[0];
|
||||
totalEdges = Number(edgeRow?.cnt ?? edgeRow?.[0] ?? 0);
|
||||
} catch {
|
||||
// Table might not exist or be empty
|
||||
}
|
||||
|
||||
|
||||
return { nodes: totalNodes, edges: totalEdges };
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn('Failed to get Kuzu stats:', error);
|
||||
console.warn('Failed to get LadybugDB stats:', error);
|
||||
}
|
||||
return { nodes: 0, edges: 0 };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if KuzuDB is initialized and has data
|
||||
* Check if LadybugDB is initialized and has data
|
||||
*/
|
||||
export const isKuzuReady = (): boolean => {
|
||||
export const isLbugReady = (): boolean => {
|
||||
return conn !== null && db !== null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Close the database connection (cleanup)
|
||||
*/
|
||||
export const closeKuzu = async (): Promise<void> => {
|
||||
export const closeLbug = async (): Promise<void> => {
|
||||
if (conn) {
|
||||
try {
|
||||
await conn.close();
|
||||
|
|
@ -373,7 +382,7 @@ export const closeKuzu = async (): Promise<void> => {
|
|||
} catch {}
|
||||
db = null;
|
||||
}
|
||||
kuzu = null;
|
||||
lbug = null;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -387,24 +396,20 @@ export const executePrepared = async (
|
|||
params: Record<string, any>
|
||||
): Promise<any[]> => {
|
||||
if (!conn) {
|
||||
await initKuzu();
|
||||
await initLbug();
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const stmt = await conn.prepare(cypher);
|
||||
if (!stmt.isSuccess()) {
|
||||
const errMsg = await stmt.getErrorMessage();
|
||||
throw new Error(`Prepare failed: ${errMsg}`);
|
||||
}
|
||||
|
||||
|
||||
const result = await conn.execute(stmt, params);
|
||||
|
||||
const rows: any[] = [];
|
||||
while (await result.hasNext()) {
|
||||
const row = await result.getNext();
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
|
||||
const rows = await result.getAll();
|
||||
|
||||
await stmt.close();
|
||||
return rows;
|
||||
} catch (error) {
|
||||
|
|
@ -421,22 +426,22 @@ export const executeWithReusedStatement = async (
|
|||
paramsList: Array<Record<string, any>>
|
||||
): Promise<void> => {
|
||||
if (!conn) {
|
||||
await initKuzu();
|
||||
await initLbug();
|
||||
}
|
||||
|
||||
|
||||
if (paramsList.length === 0) return;
|
||||
|
||||
|
||||
const SUB_BATCH_SIZE = 4;
|
||||
|
||||
|
||||
for (let i = 0; i < paramsList.length; i += SUB_BATCH_SIZE) {
|
||||
const subBatch = paramsList.slice(i, i + SUB_BATCH_SIZE);
|
||||
|
||||
|
||||
const stmt = await conn.prepare(cypher);
|
||||
if (!stmt.isSuccess()) {
|
||||
const errMsg = await stmt.getErrorMessage();
|
||||
throw new Error(`Prepare failed: ${errMsg}`);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
for (const params of subBatch) {
|
||||
await conn.execute(stmt, params);
|
||||
|
|
@ -444,7 +449,7 @@ export const executeWithReusedStatement = async (
|
|||
} finally {
|
||||
await stmt.close();
|
||||
}
|
||||
|
||||
|
||||
if (i + SUB_BATCH_SIZE < paramsList.length) {
|
||||
await new Promise(r => setTimeout(r, 0));
|
||||
}
|
||||
|
|
@ -456,65 +461,67 @@ export const executeWithReusedStatement = async (
|
|||
*/
|
||||
export const testArrayParams = async (): Promise<{ success: boolean; error?: string }> => {
|
||||
if (!conn) {
|
||||
await initKuzu();
|
||||
await initLbug();
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const testEmbedding = new Array(384).fill(0).map((_, i) => i / 384);
|
||||
|
||||
|
||||
// Get any node ID to test with (try File first, then others)
|
||||
let testNodeId: string | null = null;
|
||||
for (const tableName of NODE_TABLES) {
|
||||
try {
|
||||
const nodeResult = await conn.query(`MATCH (n:${tableName}) RETURN n.id AS id LIMIT 1`);
|
||||
const nodeRow = await nodeResult.getNext();
|
||||
const nodeRows = await nodeResult.getAll();
|
||||
const nodeRow = nodeRows[0];
|
||||
if (nodeRow) {
|
||||
testNodeId = nodeRow.id ?? nodeRow[0];
|
||||
break;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
|
||||
if (!testNodeId) {
|
||||
return { success: false, error: 'No nodes found to test with' };
|
||||
}
|
||||
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('🧪 Testing array params with node:', testNodeId);
|
||||
}
|
||||
|
||||
|
||||
// First create an embedding entry
|
||||
const createQuery = `CREATE (e:${EMBEDDING_TABLE_NAME} {nodeId: $nodeId, embedding: $embedding})`;
|
||||
const stmt = await conn.prepare(createQuery);
|
||||
|
||||
|
||||
if (!stmt.isSuccess()) {
|
||||
const errMsg = await stmt.getErrorMessage();
|
||||
return { success: false, error: `Prepare failed: ${errMsg}` };
|
||||
}
|
||||
|
||||
|
||||
await conn.execute(stmt, {
|
||||
nodeId: testNodeId,
|
||||
embedding: testEmbedding,
|
||||
});
|
||||
|
||||
|
||||
await stmt.close();
|
||||
|
||||
|
||||
// Verify it was stored
|
||||
const verifyResult = await conn.query(
|
||||
`MATCH (e:${EMBEDDING_TABLE_NAME} {nodeId: '${testNodeId}'}) RETURN e.embedding AS emb`
|
||||
);
|
||||
const verifyRow = await verifyResult.getNext();
|
||||
const verifyRows = await verifyResult.getAll();
|
||||
const verifyRow = verifyRows[0];
|
||||
const storedEmb = verifyRow?.emb ?? verifyRow?.[0];
|
||||
|
||||
|
||||
if (storedEmb && Array.isArray(storedEmb) && storedEmb.length === 384) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('✅ Array params WORK! Stored embedding length:', storedEmb.length);
|
||||
}
|
||||
return { success: true };
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: `Embedding not stored correctly. Got: ${typeof storedEmb}, length: ${storedEmb?.length}`
|
||||
return {
|
||||
success: false,
|
||||
error: `Embedding not stored correctly. Got: ${typeof storedEmb}, length: ${storedEmb?.length}`
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* KuzuDB Schema Definitions
|
||||
* LadybugDB Schema Definitions
|
||||
*
|
||||
* Hybrid Schema:
|
||||
* - Separate node tables for each code element type (File, Function, Class, etc.)
|
||||
|
|
@ -17,7 +17,7 @@ import { z } from 'zod';
|
|||
import { WebGPUNotAvailableError, embedText, embeddingToArray, initEmbedder, isEmbedderReady } from '../embeddings/embedder';
|
||||
|
||||
/**
|
||||
* Tool factory - creates tools bound to the KuzuDB query functions
|
||||
* Tool factory - creates tools bound to the LadybugDB query functions
|
||||
*/
|
||||
export const createGraphRAGTools = (
|
||||
executeQuery: (cypher: string) => Promise<any[]>,
|
||||
|
|
@ -975,7 +975,7 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
// For code elements (Function, Class, etc.), use the direct id
|
||||
const isFileTarget = targetType === 'File';
|
||||
|
||||
// Query each depth level separately (KuzuDB doesn't support list comprehensions on paths)
|
||||
// Query each depth level separately (LadybugDB doesn't support list comprehensions on paths)
|
||||
// For depth 1: direct connections only
|
||||
// For depth 2+: chain multiple single-hop queries
|
||||
const depthQueries: Promise<any[]>[] = [];
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ export interface AgentStep {
|
|||
* Graph schema information for LLM context
|
||||
*/
|
||||
export const GRAPH_SCHEMA_DESCRIPTION = `
|
||||
KUZU GRAPH DATABASE SCHEMA (Multi-Table):
|
||||
LADYBUG GRAPH DATABASE SCHEMA (Multi-Table):
|
||||
|
||||
NODE TABLES:
|
||||
1. File - Source files
|
||||
|
|
|
|||
|
|
@ -1,28 +1,35 @@
|
|||
declare module 'kuzu-wasm' {
|
||||
declare module '@ladybugdb/wasm-core' {
|
||||
export function init(): Promise<void>;
|
||||
export class Database {
|
||||
constructor(path: string);
|
||||
constructor(path: string, bufferPoolSize?: number);
|
||||
close(): Promise<void>;
|
||||
}
|
||||
export class Connection {
|
||||
constructor(db: Database);
|
||||
query(cypher: string): Promise<QueryResult>;
|
||||
prepare(cypher: string): Promise<PreparedStatement>;
|
||||
execute(stmt: PreparedStatement, params?: Record<string, any>): Promise<QueryResult>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
export interface QueryResult {
|
||||
getAll(): Promise<any[]>;
|
||||
hasNext(): Promise<boolean>;
|
||||
getNext(): Promise<any>;
|
||||
}
|
||||
export interface PreparedStatement {
|
||||
isSuccess(): boolean;
|
||||
getErrorMessage(): Promise<string>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
export const FS: {
|
||||
writeFile(path: string, data: string): Promise<void>;
|
||||
unlink(path: string): Promise<void>;
|
||||
};
|
||||
const kuzu: {
|
||||
const lbug: {
|
||||
init: typeof init;
|
||||
Database: typeof Database;
|
||||
Connection: typeof Connection;
|
||||
FS: typeof FS;
|
||||
};
|
||||
export default kuzu;
|
||||
export default lbug;
|
||||
}
|
||||
|
||||
|
|
@ -26,13 +26,13 @@ import {
|
|||
type HybridSearchResult,
|
||||
} from '../core/search';
|
||||
|
||||
// Lazy import for Kuzu to avoid breaking worker if SharedArrayBuffer unavailable
|
||||
let kuzuAdapter: typeof import('../core/kuzu/kuzu-adapter') | null = null;
|
||||
const getKuzuAdapter = async () => {
|
||||
if (!kuzuAdapter) {
|
||||
kuzuAdapter = await import('../core/kuzu/kuzu-adapter');
|
||||
// Lazy import for LadybugDB to avoid breaking worker if SharedArrayBuffer unavailable
|
||||
let lbugAdapter: typeof import('../core/lbug/lbug-adapter') | null = null;
|
||||
const getLbugAdapter = async () => {
|
||||
if (!lbugAdapter) {
|
||||
lbugAdapter = await import('../core/lbug/lbug-adapter');
|
||||
}
|
||||
return kuzuAdapter;
|
||||
return lbugAdapter;
|
||||
};
|
||||
|
||||
// Embedding state
|
||||
|
|
@ -172,52 +172,52 @@ const workerApi = {
|
|||
console.log(`🔍 BM25 index built: ${bm25DocCount} documents`);
|
||||
}
|
||||
|
||||
// Load graph into KuzuDB for querying (optional - gracefully degrades)
|
||||
// Load graph into LadybugDB for querying (optional - gracefully degrades)
|
||||
try {
|
||||
onProgress({
|
||||
phase: 'complete',
|
||||
percent: 98,
|
||||
message: 'Loading into KuzuDB...',
|
||||
message: 'Loading into LadybugDB...',
|
||||
stats: {
|
||||
filesProcessed: result.graph.nodeCount,
|
||||
totalFiles: result.graph.nodeCount,
|
||||
nodesCreated: result.graph.nodeCount,
|
||||
},
|
||||
});
|
||||
|
||||
const kuzu = await getKuzuAdapter();
|
||||
await kuzu.loadGraphToKuzu(result.graph, result.fileContents);
|
||||
|
||||
|
||||
const lbug = await getLbugAdapter();
|
||||
await lbug.loadGraphToLbug(result.graph, result.fileContents);
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
const stats = await kuzu.getKuzuStats();
|
||||
console.log('KuzuDB loaded:', stats);
|
||||
const stats = await lbug.getLbugStats();
|
||||
console.log('LadybugDB loaded:', stats);
|
||||
console.log('📁 Stored', storedFileContents.size, 'files for grep/read tools');
|
||||
}
|
||||
} catch {
|
||||
// KuzuDB is optional - silently continue without it
|
||||
// LadybugDB is optional - silently continue without it
|
||||
}
|
||||
|
||||
|
||||
// Store clustering config for background enrichment (runs after graph loads)
|
||||
if (clusteringConfig) {
|
||||
pendingEnrichmentConfig = clusteringConfig;
|
||||
console.log('📋 Clustering config saved for background enrichment');
|
||||
}
|
||||
|
||||
|
||||
// Convert to serializable format for transfer back to main thread
|
||||
return serializePipelineResult(result);
|
||||
},
|
||||
|
||||
/**
|
||||
* Execute a Cypher query against the KuzuDB database
|
||||
* Execute a Cypher query against the LadybugDB database
|
||||
* @param cypher - The Cypher query string
|
||||
* @returns Query results as an array of objects
|
||||
*/
|
||||
async runQuery(cypher: string): Promise<any[]> {
|
||||
const kuzu = await getKuzuAdapter();
|
||||
if (!kuzu.isKuzuReady()) {
|
||||
const lbug = await getLbugAdapter();
|
||||
if (!lbug.isLbugReady()) {
|
||||
throw new Error('Database not ready. Please load a repository first.');
|
||||
}
|
||||
return kuzu.executeQuery(cypher);
|
||||
return lbug.executeQuery(cypher);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -225,8 +225,8 @@ const workerApi = {
|
|||
*/
|
||||
async isReady(): Promise<boolean> {
|
||||
try {
|
||||
const kuzu = await getKuzuAdapter();
|
||||
return kuzu.isKuzuReady();
|
||||
const lbug = await getLbugAdapter();
|
||||
return lbug.isLbugReady();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -237,8 +237,8 @@ const workerApi = {
|
|||
*/
|
||||
async getStats(): Promise<{ nodes: number; edges: number }> {
|
||||
try {
|
||||
const kuzu = await getKuzuAdapter();
|
||||
return kuzu.getKuzuStats();
|
||||
const lbug = await getLbugAdapter();
|
||||
return lbug.getLbugStats();
|
||||
} catch {
|
||||
return { nodes: 0, edges: 0 };
|
||||
}
|
||||
|
|
@ -276,29 +276,29 @@ const workerApi = {
|
|||
console.log(`🔍 BM25 index built: ${bm25DocCount} documents`);
|
||||
}
|
||||
|
||||
// Load graph into KuzuDB for querying (optional - gracefully degrades)
|
||||
// Load graph into LadybugDB for querying (optional - gracefully degrades)
|
||||
try {
|
||||
onProgress({
|
||||
phase: 'complete',
|
||||
percent: 98,
|
||||
message: 'Loading into KuzuDB...',
|
||||
message: 'Loading into LadybugDB...',
|
||||
stats: {
|
||||
filesProcessed: result.graph.nodeCount,
|
||||
totalFiles: result.graph.nodeCount,
|
||||
nodesCreated: result.graph.nodeCount,
|
||||
},
|
||||
});
|
||||
|
||||
const kuzu = await getKuzuAdapter();
|
||||
await kuzu.loadGraphToKuzu(result.graph, result.fileContents);
|
||||
|
||||
|
||||
const lbug = await getLbugAdapter();
|
||||
await lbug.loadGraphToLbug(result.graph, result.fileContents);
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
const stats = await kuzu.getKuzuStats();
|
||||
console.log('KuzuDB loaded:', stats);
|
||||
const stats = await lbug.getLbugStats();
|
||||
console.log('LadybugDB loaded:', stats);
|
||||
console.log('📁 Stored', storedFileContents.size, 'files for grep/read tools');
|
||||
}
|
||||
} catch {
|
||||
// KuzuDB is optional - silently continue without it
|
||||
// LadybugDB is optional - silently continue without it
|
||||
}
|
||||
|
||||
// Store clustering config for background enrichment (runs after graph loads)
|
||||
|
|
@ -325,8 +325,8 @@ const workerApi = {
|
|||
onProgress: (progress: EmbeddingProgress) => void,
|
||||
forceDevice?: 'webgpu' | 'wasm'
|
||||
): Promise<void> {
|
||||
const kuzu = await getKuzuAdapter();
|
||||
if (!kuzu.isKuzuReady()) {
|
||||
const lbug = await getLbugAdapter();
|
||||
if (!lbug.isLbugReady()) {
|
||||
throw new Error('Database not ready. Please load a repository first.');
|
||||
}
|
||||
|
||||
|
|
@ -343,8 +343,8 @@ const workerApi = {
|
|||
};
|
||||
|
||||
await runEmbeddingPipeline(
|
||||
kuzu.executeQuery,
|
||||
kuzu.executeWithReusedStatement,
|
||||
lbug.executeQuery,
|
||||
lbug.executeWithReusedStatement,
|
||||
progressCallback,
|
||||
forceDevice ? { device: forceDevice } : {}
|
||||
);
|
||||
|
|
@ -400,15 +400,15 @@ const workerApi = {
|
|||
k: number = 10,
|
||||
maxDistance: number = 0.5
|
||||
): Promise<SemanticSearchResult[]> {
|
||||
const kuzu = await getKuzuAdapter();
|
||||
if (!kuzu.isKuzuReady()) {
|
||||
const lbug = await getLbugAdapter();
|
||||
if (!lbug.isLbugReady()) {
|
||||
throw new Error('Database not ready. Please load a repository first.');
|
||||
}
|
||||
if (!isEmbeddingComplete) {
|
||||
throw new Error('Embeddings not ready. Please wait for embedding pipeline to complete.');
|
||||
}
|
||||
|
||||
return doSemanticSearch(kuzu.executeQuery, query, k, maxDistance);
|
||||
return doSemanticSearch(lbug.executeQuery, query, k, maxDistance);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -424,15 +424,15 @@ const workerApi = {
|
|||
k: number = 5,
|
||||
hops: number = 2
|
||||
): Promise<any[]> {
|
||||
const kuzu = await getKuzuAdapter();
|
||||
if (!kuzu.isKuzuReady()) {
|
||||
const lbug = await getLbugAdapter();
|
||||
if (!lbug.isLbugReady()) {
|
||||
throw new Error('Database not ready. Please load a repository first.');
|
||||
}
|
||||
if (!isEmbeddingComplete) {
|
||||
throw new Error('Embeddings not ready. Please wait for embedding pipeline to complete.');
|
||||
}
|
||||
|
||||
return doSemanticSearchWithContext(kuzu.executeQuery, query, k, hops);
|
||||
return doSemanticSearchWithContext(lbug.executeQuery, query, k, hops);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -458,9 +458,9 @@ const workerApi = {
|
|||
let semanticResults: SemanticSearchResult[] = [];
|
||||
if (isEmbeddingComplete) {
|
||||
try {
|
||||
const kuzu = await getKuzuAdapter();
|
||||
if (kuzu.isKuzuReady()) {
|
||||
semanticResults = await doSemanticSearch(kuzu.executeQuery, query, k * 3, 0.5);
|
||||
const lbug = await getLbugAdapter();
|
||||
if (lbug.isLbugReady()) {
|
||||
semanticResults = await doSemanticSearch(lbug.executeQuery, query, k * 3, 0.5);
|
||||
}
|
||||
} catch {
|
||||
// Semantic search failed, continue with BM25 only
|
||||
|
|
@ -516,15 +516,15 @@ const workerApi = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Test if KuzuDB supports array parameters in prepared statements
|
||||
* Test if LadybugDB supports array parameters in prepared statements
|
||||
* This is a diagnostic function
|
||||
*/
|
||||
async testArrayParams(): Promise<{ success: boolean; error?: string }> {
|
||||
const kuzu = await getKuzuAdapter();
|
||||
if (!kuzu.isKuzuReady()) {
|
||||
const lbug = await getLbugAdapter();
|
||||
if (!lbug.isLbugReady()) {
|
||||
return { success: false, error: 'Database not ready' };
|
||||
}
|
||||
return kuzu.testArrayParams();
|
||||
return lbug.testArrayParams();
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -539,8 +539,8 @@ const workerApi = {
|
|||
*/
|
||||
async initializeAgent(config: ProviderConfig, projectName?: string): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const kuzu = await getKuzuAdapter();
|
||||
if (!kuzu.isKuzuReady()) {
|
||||
const lbug = await getLbugAdapter();
|
||||
if (!lbug.isLbugReady()) {
|
||||
return { success: false, error: 'Database not ready. Please load a repository first.' };
|
||||
}
|
||||
|
||||
|
|
@ -549,31 +549,31 @@ const workerApi = {
|
|||
if (!isEmbeddingComplete) {
|
||||
throw new Error('Embeddings not ready');
|
||||
}
|
||||
return doSemanticSearch(kuzu.executeQuery, query, k, maxDistance);
|
||||
return doSemanticSearch(lbug.executeQuery, query, k, maxDistance);
|
||||
};
|
||||
|
||||
const semanticSearchWithContextWrapper = async (query: string, k?: number, hops?: number) => {
|
||||
if (!isEmbeddingComplete) {
|
||||
throw new Error('Embeddings not ready');
|
||||
}
|
||||
return doSemanticSearchWithContext(kuzu.executeQuery, query, k, hops);
|
||||
return doSemanticSearchWithContext(lbug.executeQuery, query, k, hops);
|
||||
};
|
||||
|
||||
// Hybrid search wrapper - combines BM25 + semantic
|
||||
const hybridSearchWrapper = async (query: string, k?: number) => {
|
||||
// Get BM25 results (always available after ingestion)
|
||||
const bm25Results = searchBM25(query, (k ?? 10) * 3);
|
||||
|
||||
|
||||
// Get semantic results if embeddings are ready
|
||||
let semanticResults: any[] = [];
|
||||
if (isEmbeddingComplete) {
|
||||
try {
|
||||
semanticResults = await doSemanticSearch(kuzu.executeQuery, query, (k ?? 10) * 3, 0.5);
|
||||
semanticResults = await doSemanticSearch(lbug.executeQuery, query, (k ?? 10) * 3, 0.5);
|
||||
} catch {
|
||||
// Semantic search failed, continue with BM25 only
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Merge with RRF
|
||||
return mergeWithRRF(bm25Results, semanticResults, k ?? 10);
|
||||
};
|
||||
|
|
@ -586,7 +586,7 @@ const workerApi = {
|
|||
|
||||
let codebaseContext;
|
||||
try {
|
||||
codebaseContext = await buildCodebaseContext(kuzu.executeQuery, resolvedProjectName);
|
||||
codebaseContext = await buildCodebaseContext(lbug.executeQuery, resolvedProjectName);
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('📊 Codebase context built:', {
|
||||
files: codebaseContext.stats.fileCount,
|
||||
|
|
@ -600,7 +600,7 @@ const workerApi = {
|
|||
|
||||
currentAgent = createGraphRAGAgent(
|
||||
config,
|
||||
kuzu.executeQuery,
|
||||
lbug.executeQuery,
|
||||
semanticSearchWrapper,
|
||||
semanticSearchWithContextWrapper,
|
||||
hybridSearchWrapper,
|
||||
|
|
@ -627,7 +627,7 @@ const workerApi = {
|
|||
|
||||
/**
|
||||
* Initialize the Graph RAG agent in backend mode (HTTP-backed tools).
|
||||
* Uses HTTP wrappers instead of local KuzuDB for all tool queries.
|
||||
* Uses HTTP wrappers instead of local LadybugDB for all tool queries.
|
||||
* @param config - Provider configuration for the LLM
|
||||
* @param backendUrl - Base URL of the gitnexus serve backend
|
||||
* @param repoName - Repository name on the backend
|
||||
|
|
@ -848,9 +848,9 @@ const workerApi = {
|
|||
}
|
||||
});
|
||||
|
||||
// Update KuzuDB with new data
|
||||
// Update LadybugDB with new data
|
||||
try {
|
||||
const kuzu = await getKuzuAdapter();
|
||||
const lbug = await getLbugAdapter();
|
||||
|
||||
onProgress(enrichments.size, enrichments.size); // Done
|
||||
|
||||
|
|
@ -872,11 +872,11 @@ const workerApi = {
|
|||
c.enrichedBy = "llm"
|
||||
`;
|
||||
|
||||
await kuzu.executeQuery(query);
|
||||
await lbug.executeQuery(query);
|
||||
}
|
||||
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to update KuzuDB with enrichment:', err);
|
||||
console.error('Failed to update LadybugDB with enrichment:', err);
|
||||
}
|
||||
|
||||
// Convert Map to Record for serialization
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ export default defineConfig({
|
|||
tailwindcss(),
|
||||
wasm(),
|
||||
topLevelAwait(),
|
||||
// Copy kuzu-wasm worker file to assets folder for production
|
||||
// Copy lbug-wasm worker file to assets folder for production
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
src: 'node_modules/kuzu-wasm/kuzu_wasm_worker.js',
|
||||
src: 'node_modules/@ladybugdb/wasm-core/lbug_wasm_worker.js',
|
||||
dest: 'assets'
|
||||
}
|
||||
]
|
||||
|
|
@ -35,12 +35,12 @@ export default defineConfig({
|
|||
define: {
|
||||
global: 'globalThis',
|
||||
},
|
||||
// Optimize deps - exclude kuzu-wasm from pre-bundling (it has WASM files)
|
||||
// Optimize deps - exclude lbug-wasm from pre-bundling (it has WASM files)
|
||||
optimizeDeps: {
|
||||
exclude: ['kuzu-wasm'],
|
||||
exclude: ['@ladybugdb/wasm-core'],
|
||||
include: ['buffer'],
|
||||
},
|
||||
// Required for KuzuDB WASM (SharedArrayBuffer needs Cross-Origin Isolation)
|
||||
// Required for LadybugDB WASM (SharedArrayBuffer needs Cross-Origin Isolation)
|
||||
server: {
|
||||
headers: {
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ GitNexus builds a complete knowledge graph of your codebase through a multi-phas
|
|||
5. **Processes** — Traces execution flows from entry points through call chains
|
||||
6. **Search** — Builds hybrid search indexes for fast retrieval
|
||||
|
||||
The result is a **KuzuDB graph database** stored locally in `.gitnexus/` with full-text search and semantic embeddings.
|
||||
The result is a **LadybugDB graph database** stored locally in `.gitnexus/` with full-text search and semantic embeddings.
|
||||
|
||||
## MCP Tools
|
||||
|
||||
|
|
|
|||
627
gitnexus/package-lock.json
generated
627
gitnexus/package-lock.json
generated
|
|
@ -11,6 +11,7 @@
|
|||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
"dependencies": {
|
||||
"@huggingface/transformers": "^3.0.0",
|
||||
"@ladybugdb/core": "^0.15.1",
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
"commander": "^12.0.0",
|
||||
|
|
@ -20,7 +21,6 @@
|
|||
"graphology": "^0.25.4",
|
||||
"graphology-indices": "^0.17.0",
|
||||
"graphology-utils": "^2.3.0",
|
||||
"kuzu": "^0.11.3",
|
||||
"lru-cache": "^11.0.0",
|
||||
"mnemonist": "^0.39.0",
|
||||
"pandemonium": "^2.4.0",
|
||||
|
|
@ -1148,6 +1148,133 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@ladybugdb/core": {
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@ladybugdb/core/-/core-0.15.1.tgz",
|
||||
"integrity": "sha512-a+jhzIlS2+57Y2YWXlta7Dq5A3577dQ8YO7DzPCFZxozeiGIZn0K9v0ROO+ws4PW9BwuQYI5BXQxTEtaa1Otlg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cmake-js": "^8.0.0",
|
||||
"node-addon-api": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ladybugdb/core/node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@ladybugdb/core/node_modules/cmake-js": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cmake-js/-/cmake-js-8.0.0.tgz",
|
||||
"integrity": "sha512-YbUP88RDwCvoQkZhRtGURYm9RIpWdtvZuhT87fKNoLjk8kIFIFeARpKfuZQGdwfH99GZpUmqSfcDrK62X7lTgg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"fs-extra": "^11.3.3",
|
||||
"node-api-headers": "^1.8.0",
|
||||
"rc": "1.2.8",
|
||||
"semver": "^7.7.3",
|
||||
"tar": "^7.5.6",
|
||||
"url-join": "^4.0.1",
|
||||
"which": "^6.0.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"cmake-js": "bin/cmake-js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ladybugdb/core/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ladybugdb/core/node_modules/isexe": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
|
||||
"integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@ladybugdb/core/node_modules/minizlib": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
||||
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@ladybugdb/core/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ladybugdb/core/node_modules/tar": {
|
||||
"version": "7.5.11",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
|
||||
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
"minipass": "^7.1.2",
|
||||
"minizlib": "^3.1.0",
|
||||
"yallist": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@ladybugdb/core/node_modules/which": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
|
||||
"integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/which.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ladybugdb/core/node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.25.3",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.3.tgz",
|
||||
|
|
@ -2274,26 +2401,6 @@
|
|||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/aproba": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
|
||||
"integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/are-we-there-yet": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
|
||||
"integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"delegates": "^1.0.0",
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
|
|
@ -2322,23 +2429,6 @@
|
|||
"js-tokens": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
|
||||
"integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
|
|
@ -2433,15 +2523,6 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-progress": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
|
||||
|
|
@ -2582,55 +2663,6 @@
|
|||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cmake-js": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/cmake-js/-/cmake-js-7.4.0.tgz",
|
||||
"integrity": "sha512-Lw0JxEHrmk+qNj1n9W9d4IvkDdYTBn7l2BW6XmtLj7WPpIo2shvxUy+YokfjMxAAOELNonQwX3stkPhM5xSC2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.5",
|
||||
"debug": "^4",
|
||||
"fs-extra": "^11.2.0",
|
||||
"memory-stream": "^1.0.0",
|
||||
"node-api-headers": "^1.1.0",
|
||||
"npmlog": "^6.0.2",
|
||||
"rc": "^1.2.7",
|
||||
"semver": "^7.5.4",
|
||||
"tar": "^6.2.0",
|
||||
"url-join": "^4.0.1",
|
||||
"which": "^2.0.2",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"cmake-js": "bin/cmake-js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmake-js/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmake-js/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
|
@ -2649,27 +2681,6 @@
|
|||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-support": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"color-support": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
|
|
@ -2679,12 +2690,6 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
|
|
@ -2804,21 +2809,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delegates": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
|
|
@ -2931,21 +2921,6 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es6-error": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||
|
|
@ -3205,26 +3180,6 @@
|
|||
"integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
|
|
@ -3241,22 +3196,6 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
|
|
@ -3289,30 +3228,6 @@
|
|||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
|
|
@ -3337,73 +3252,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gauge": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz",
|
||||
"integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"aproba": "^1.0.3 || ^2.0.0",
|
||||
"color-support": "^1.1.3",
|
||||
"console-control-strings": "^1.1.0",
|
||||
"has-unicode": "^2.0.1",
|
||||
"signal-exit": "^3.0.7",
|
||||
"string-width": "^4.2.3",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wide-align": "^1.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gauge/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/gauge/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gauge/node_modules/signal-exit": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/gauge/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/gauge/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
|
|
@ -3619,27 +3467,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-unicode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
|
|
@ -3843,18 +3670,6 @@
|
|||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/kuzu": {
|
||||
"version": "0.11.3",
|
||||
"resolved": "https://registry.npmjs.org/kuzu/-/kuzu-0.11.3.tgz",
|
||||
"integrity": "sha512-4+hD3Y+YMV3e0uiqTv1/GUal47D04l8qluw1WFWg8Nx3k7rLsHG1Pmq9WHIOlf1742svxQvTYQiuY6oS1qxAZA==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cmake-js": "^7.3.0",
|
||||
"node-addon-api": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
|
|
@ -3938,15 +3753,6 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/memory-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memory-stream/-/memory-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-Wm13VcsPIMdG96dzILfij09PvuS3APtcKNh7M28FsCA/w6+1mjR7hhPmfFNoilX9xU7wTdhsH5lJAm6XNzdtww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
|
|
@ -4031,43 +3837,6 @@
|
|||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mnemonist": {
|
||||
"version": "0.39.8",
|
||||
"resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz",
|
||||
|
|
@ -4134,22 +3903,6 @@
|
|||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/npmlog": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",
|
||||
"integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"are-we-there-yet": "^3.0.0",
|
||||
"console-control-strings": "^1.1.0",
|
||||
"gauge": "^4.0.3",
|
||||
"set-blocking": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
|
@ -4470,12 +4223,6 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
|
|
@ -4546,20 +4293,6 @@
|
|||
"rc": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
|
|
@ -4803,12 +4536,6 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
|
|
@ -5010,15 +4737,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
|
|
@ -5137,33 +4855,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||
"deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
"minipass": "^5.0.0",
|
||||
"minizlib": "^2.1.1",
|
||||
"mkdirp": "^1.0.3",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/minipass": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
|
|
@ -5708,12 +5399,6 @@
|
|||
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
|
|
@ -5930,56 +5615,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wide-align": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
|
||||
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^1.0.2 || 2 || 3 || 4"
|
||||
}
|
||||
},
|
||||
"node_modules/wide-align/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wide-align/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wide-align/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wide-align/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
|
|
@ -6086,12 +5721,6 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@
|
|||
"graphology": "^0.25.4",
|
||||
"graphology-indices": "^0.17.0",
|
||||
"graphology-utils": "^2.3.0",
|
||||
"kuzu": "^0.11.3",
|
||||
"@ladybugdb/core": "^0.15.1",
|
||||
"lru-cache": "^11.0.0",
|
||||
"mnemonist": "^0.39.0",
|
||||
"pandemonium": "^2.4.0",
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ import { execFileSync } from 'child_process';
|
|||
import v8 from 'v8';
|
||||
import cliProgress from 'cli-progress';
|
||||
import { runPipelineFromRepo } from '../core/ingestion/pipeline.js';
|
||||
import { initKuzu, loadGraphToKuzu, getKuzuStats, executeQuery, executeWithReusedStatement, closeKuzu, createFTSIndex, loadCachedEmbeddings } from '../core/kuzu/kuzu-adapter.js';
|
||||
import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, createFTSIndex, loadCachedEmbeddings } from '../core/lbug/lbug-adapter.js';
|
||||
// Embedding imports are lazy (dynamic import) so onnxruntime-node is never
|
||||
// loaded when embeddings are not requested. This avoids crashes on Node
|
||||
// versions whose ABI is not yet supported by the native binary (#89).
|
||||
// disposeEmbedder intentionally not called — ONNX Runtime segfaults on cleanup (see #38)
|
||||
import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath } from '../storage/repo-manager.js';
|
||||
import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath, cleanupOldKuzuFiles } from '../storage/repo-manager.js';
|
||||
import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
|
||||
import { generateAIContextFiles } from './ai-context.js';
|
||||
import { generateSkillFiles, type GeneratedSkillInfo } from './skill-gen.js';
|
||||
|
|
@ -63,7 +63,7 @@ const PHASE_LABELS: Record<string, string> = {
|
|||
communities: 'Detecting communities',
|
||||
processes: 'Detecting processes',
|
||||
complete: 'Pipeline complete',
|
||||
kuzu: 'Loading into KuzuDB',
|
||||
lbug: 'Loading into LadybugDB',
|
||||
fts: 'Creating search indexes',
|
||||
embeddings: 'Generating embeddings',
|
||||
done: 'Done',
|
||||
|
|
@ -100,7 +100,15 @@ export const analyzeCommand = async (
|
|||
return;
|
||||
}
|
||||
|
||||
const { storagePath, kuzuPath } = getStoragePaths(repoPath);
|
||||
const { storagePath, lbugPath } = getStoragePaths(repoPath);
|
||||
|
||||
// Clean up stale KuzuDB files from before the LadybugDB migration.
|
||||
// If kuzu existed but lbug doesn't, we're doing a migration re-index — say so.
|
||||
const kuzuResult = await cleanupOldKuzuFiles(storagePath);
|
||||
if (kuzuResult.found && kuzuResult.needsReindex) {
|
||||
console.log(' Migrating from KuzuDB to LadybugDB — rebuilding index...\n');
|
||||
}
|
||||
|
||||
const currentCommit = getCurrentCommit(repoPath);
|
||||
const existingMeta = await loadMeta(storagePath);
|
||||
|
||||
|
|
@ -130,7 +138,7 @@ export const analyzeCommand = async (
|
|||
aborted = true;
|
||||
bar.stop();
|
||||
console.log('\n Interrupted — cleaning up...');
|
||||
closeKuzu().catch(() => {}).finally(() => process.exit(130));
|
||||
closeLbug().catch(() => {}).finally(() => process.exit(130));
|
||||
};
|
||||
process.on('SIGINT', sigintHandler);
|
||||
|
||||
|
|
@ -180,13 +188,13 @@ export const analyzeCommand = async (
|
|||
if (options?.embeddings && existingMeta && !options?.force) {
|
||||
try {
|
||||
updateBar(0, 'Caching embeddings...');
|
||||
await initKuzu(kuzuPath);
|
||||
await initLbug(lbugPath);
|
||||
const cached = await loadCachedEmbeddings();
|
||||
cachedEmbeddingNodeIds = cached.embeddingNodeIds;
|
||||
cachedEmbeddings = cached.embeddings;
|
||||
await closeKuzu();
|
||||
await closeLbug();
|
||||
} catch {
|
||||
try { await closeKuzu(); } catch {}
|
||||
try { await closeLbug(); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -197,25 +205,25 @@ export const analyzeCommand = async (
|
|||
updateBar(scaled, phaseLabel);
|
||||
});
|
||||
|
||||
// ── Phase 2: KuzuDB (60–85%) ──────────────────────────────────────
|
||||
updateBar(60, 'Loading into KuzuDB...');
|
||||
// ── Phase 2: LadybugDB (60–85%) ──────────────────────────────────────
|
||||
updateBar(60, 'Loading into LadybugDB...');
|
||||
|
||||
await closeKuzu();
|
||||
const kuzuFiles = [kuzuPath, `${kuzuPath}.wal`, `${kuzuPath}.lock`];
|
||||
for (const f of kuzuFiles) {
|
||||
await closeLbug();
|
||||
const lbugFiles = [lbugPath, `${lbugPath}.wal`, `${lbugPath}.lock`];
|
||||
for (const f of lbugFiles) {
|
||||
try { await fs.rm(f, { recursive: true, force: true }); } catch {}
|
||||
}
|
||||
|
||||
const t0Kuzu = Date.now();
|
||||
await initKuzu(kuzuPath);
|
||||
let kuzuMsgCount = 0;
|
||||
const kuzuResult = await loadGraphToKuzu(pipelineResult.graph, pipelineResult.repoPath, storagePath, (msg) => {
|
||||
kuzuMsgCount++;
|
||||
const progress = Math.min(84, 60 + Math.round((kuzuMsgCount / (kuzuMsgCount + 10)) * 24));
|
||||
const t0Lbug = Date.now();
|
||||
await initLbug(lbugPath);
|
||||
let lbugMsgCount = 0;
|
||||
const lbugResult = await loadGraphToLbug(pipelineResult.graph, pipelineResult.repoPath, storagePath, (msg) => {
|
||||
lbugMsgCount++;
|
||||
const progress = Math.min(84, 60 + Math.round((lbugMsgCount / (lbugMsgCount + 10)) * 24));
|
||||
updateBar(progress, msg);
|
||||
});
|
||||
const kuzuTime = ((Date.now() - t0Kuzu) / 1000).toFixed(1);
|
||||
const kuzuWarnings = kuzuResult.warnings;
|
||||
const lbugTime = ((Date.now() - t0Lbug) / 1000).toFixed(1);
|
||||
const lbugWarnings = lbugResult.warnings;
|
||||
|
||||
// ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
|
||||
updateBar(85, 'Creating search indexes...');
|
||||
|
|
@ -249,7 +257,7 @@ export const analyzeCommand = async (
|
|||
}
|
||||
|
||||
// ── Phase 4: Embeddings (90–98%) ──────────────────────────────────
|
||||
const stats = await getKuzuStats();
|
||||
const stats = await getLbugStats();
|
||||
let embeddingTime = '0.0';
|
||||
let embeddingSkipped = true;
|
||||
let embeddingSkipReason = 'off (use --embeddings to enable)';
|
||||
|
|
@ -334,7 +342,7 @@ export const analyzeCommand = async (
|
|||
processes: pipelineResult.processResult?.stats.totalProcesses,
|
||||
}, generatedSkills);
|
||||
|
||||
await closeKuzu();
|
||||
await closeLbug();
|
||||
// Note: we intentionally do NOT call disposeEmbedder() here.
|
||||
// ONNX Runtime's native cleanup segfaults on macOS and some Linux configs.
|
||||
// Since the process exits immediately after, Node.js reclaims everything.
|
||||
|
|
@ -355,7 +363,7 @@ export const analyzeCommand = async (
|
|||
const embeddingsCached = cachedEmbeddings.length > 0;
|
||||
console.log(`\n Repository indexed successfully (${totalTime}s)${embeddingsCached ? ` [${cachedEmbeddings.length} embeddings cached]` : ''}\n`);
|
||||
console.log(` ${stats.nodes.toLocaleString()} nodes | ${stats.edges.toLocaleString()} edges | ${pipelineResult.communityResult?.stats.totalCommunities || 0} clusters | ${pipelineResult.processResult?.stats.totalProcesses || 0} flows`);
|
||||
console.log(` KuzuDB ${kuzuTime}s | FTS ${ftsTime}s | Embeddings ${embeddingSkipped ? embeddingSkipReason : embeddingTime + 's'}`);
|
||||
console.log(` LadybugDB ${lbugTime}s | FTS ${ftsTime}s | Embeddings ${embeddingSkipped ? embeddingSkipReason : embeddingTime + 's'}`);
|
||||
console.log(` ${repoPath}`);
|
||||
|
||||
if (aiContext.files.length > 0) {
|
||||
|
|
@ -363,12 +371,12 @@ export const analyzeCommand = async (
|
|||
}
|
||||
|
||||
// Show a quiet summary if some edge types needed fallback insertion
|
||||
if (kuzuWarnings.length > 0) {
|
||||
const totalFallback = kuzuWarnings.reduce((sum, w) => {
|
||||
if (lbugWarnings.length > 0) {
|
||||
const totalFallback = lbugWarnings.reduce((sum, w) => {
|
||||
const m = w.match(/\((\d+) edges\)/);
|
||||
return sum + (m ? parseInt(m[1]) : 0);
|
||||
}, 0);
|
||||
console.log(` Note: ${totalFallback} edges across ${kuzuWarnings.length} types inserted via fallback (schema will be updated in next release)`);
|
||||
console.log(` Note: ${totalFallback} edges across ${lbugWarnings.length} types inserted via fallback (schema will be updated in next release)`);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -379,7 +387,7 @@ export const analyzeCommand = async (
|
|||
|
||||
console.log('');
|
||||
|
||||
// KuzuDB's native module holds open handles that prevent Node from exiting.
|
||||
// LadybugDB's native module holds open handles that prevent Node from exiting.
|
||||
// ONNX Runtime also registers native atexit hooks that segfault on some
|
||||
// platforms (#38, #40). Force-exit to ensure clean termination.
|
||||
process.exit(0);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export async function augmentCommand(pattern: string): Promise<void> {
|
|||
|
||||
if (result) {
|
||||
// IMPORTANT: Write to stderr, NOT stdout.
|
||||
// KuzuDB's native module captures stdout fd at OS level during init,
|
||||
// LadybugDB's native module captures stdout fd at OS level during init,
|
||||
// which makes stdout permanently broken in subprocess contexts.
|
||||
// stderr is never captured, so it works reliably everywhere.
|
||||
// The hook reads from the subprocess's stderr.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Eval Server — Lightweight HTTP server for SWE-bench evaluation
|
||||
*
|
||||
* Keeps KuzuDB warm in memory so tool calls from the agent are near-instant.
|
||||
* Keeps LadybugDB warm in memory so tool calls from the agent are near-instant.
|
||||
* Designed to run inside Docker containers during SWE-bench evaluation.
|
||||
*
|
||||
* KEY DESIGN: Returns LLM-friendly text, not raw JSON.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { LocalBackend } from '../mcp/local/local-backend.js';
|
|||
|
||||
export const mcpCommand = async () => {
|
||||
// Prevent unhandled errors from crashing the MCP server process.
|
||||
// KuzuDB lock conflicts and transient errors should degrade gracefully.
|
||||
// LadybugDB lock conflicts and transient errors should degrade gracefully.
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error(`GitNexus MCP: uncaught exception — ${err.message}`);
|
||||
// Process is in an undefined state after uncaughtException — exit after flushing
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@
|
|||
* Shows the indexing status of the current repository.
|
||||
*/
|
||||
|
||||
import { findRepo } from '../storage/repo-manager.js';
|
||||
import { getCurrentCommit, isGitRepo } from '../storage/git.js';
|
||||
import { findRepo, getStoragePaths, hasKuzuIndex } from '../storage/repo-manager.js';
|
||||
import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
|
||||
|
||||
export const statusCommand = async () => {
|
||||
const cwd = process.cwd();
|
||||
|
||||
|
||||
if (!isGitRepo(cwd)) {
|
||||
console.log('Not a git repository.');
|
||||
return;
|
||||
|
|
@ -17,8 +17,16 @@ export const statusCommand = async () => {
|
|||
|
||||
const repo = await findRepo(cwd);
|
||||
if (!repo) {
|
||||
console.log('Repository not indexed.');
|
||||
console.log('Run: gitnexus analyze');
|
||||
// Check if there's a stale KuzuDB index that needs migration
|
||||
const repoRoot = getGitRoot(cwd) ?? cwd;
|
||||
const { storagePath } = getStoragePaths(repoRoot);
|
||||
if (await hasKuzuIndex(storagePath)) {
|
||||
console.log('Repository has a stale KuzuDB index from a previous version.');
|
||||
console.log('Run: gitnexus analyze (rebuilds the index with LadybugDB)');
|
||||
} else {
|
||||
console.log('Repository not indexed.');
|
||||
console.log('Run: gitnexus analyze');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
* gitnexus impact --target "AuthService" --direction upstream
|
||||
* gitnexus cypher "MATCH (n:Function) RETURN n.name LIMIT 10"
|
||||
*
|
||||
* Note: Output goes to stderr because KuzuDB's native module captures stdout
|
||||
* Note: Output goes to stderr because LadybugDB's native module captures stdout
|
||||
* at the OS level during init. This is consistent with augment.ts.
|
||||
*/
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ async function getBackend(): Promise<LocalBackend> {
|
|||
|
||||
function output(data: any): void {
|
||||
const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
||||
// stderr because KuzuDB captures stdout at OS level
|
||||
// stderr because LadybugDB captures stdout at OS level
|
||||
process.stderr.write(text + '\n');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export const wikiCommand = async (
|
|||
}
|
||||
|
||||
// ── Check for existing index ────────────────────────────────────────
|
||||
const { storagePath, kuzuPath } = getStoragePaths(repoPath);
|
||||
const { storagePath, lbugPath } = getStoragePaths(repoPath);
|
||||
const meta = await loadMeta(storagePath);
|
||||
|
||||
if (!meta) {
|
||||
|
|
@ -247,7 +247,7 @@ export const wikiCommand = async (
|
|||
const generator = new WikiGenerator(
|
||||
repoPath,
|
||||
storagePath,
|
||||
kuzuPath,
|
||||
lbugPath,
|
||||
llmConfig,
|
||||
wikiOptions,
|
||||
(phase, percent, detail) => {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { listRegisteredRepos } from '../../storage/repo-manager.js';
|
|||
async function findRepoForCwd(cwd: string): Promise<{
|
||||
name: string;
|
||||
storagePath: string;
|
||||
kuzuPath: string;
|
||||
lbugPath: string;
|
||||
} | null> {
|
||||
try {
|
||||
const entries = await listRegisteredRepos({ validate: true });
|
||||
|
|
@ -66,7 +66,7 @@ async function findRepoForCwd(cwd: string): Promise<{
|
|||
return {
|
||||
name: bestMatch.name,
|
||||
storagePath: bestMatch.storagePath,
|
||||
kuzuPath: path.join(bestMatch.storagePath, 'kuzu'),
|
||||
lbugPath: path.join(bestMatch.storagePath, 'lbug'),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
|
@ -92,19 +92,19 @@ export async function augment(pattern: string, cwd?: string): Promise<string> {
|
|||
const repo = await findRepoForCwd(workDir);
|
||||
if (!repo) return '';
|
||||
|
||||
// Lazy-load kuzu adapter (skip unnecessary init)
|
||||
const { initKuzu, executeQuery, isKuzuReady } = await import('../../mcp/core/kuzu-adapter.js');
|
||||
const { searchFTSFromKuzu } = await import('../search/bm25-index.js');
|
||||
|
||||
// Lazy-load lbug adapter (skip unnecessary init)
|
||||
const { initLbug, executeQuery, isLbugReady } = await import('../../mcp/core/lbug-adapter.js');
|
||||
const { searchFTSFromLbug } = await import('../search/bm25-index.js');
|
||||
|
||||
const repoId = repo.name.toLowerCase();
|
||||
|
||||
// Init KuzuDB if not already
|
||||
if (!isKuzuReady(repoId)) {
|
||||
await initKuzu(repoId, repo.kuzuPath);
|
||||
|
||||
// Init LadybugDB if not already
|
||||
if (!isLbugReady(repoId)) {
|
||||
await initLbug(repoId, repo.lbugPath);
|
||||
}
|
||||
|
||||
|
||||
// Step 1: BM25 search (fast, no embeddings)
|
||||
const bm25Results = await searchFTSFromKuzu(pattern, 10, repoId);
|
||||
const bm25Results = await searchFTSFromLbug(pattern, 10, repoId);
|
||||
|
||||
if (bm25Results.length === 0) return '';
|
||||
|
||||
|
|
@ -140,8 +140,90 @@ export async function augment(pattern: string, cwd?: string): Promise<string> {
|
|||
|
||||
if (symbolMatches.length === 0) return '';
|
||||
|
||||
// Step 3: For top matches, fetch callers/callees/processes
|
||||
// Also get cluster cohesion internally for ranking
|
||||
// Step 3: Batch-fetch callers/callees/processes/cohesion for top matches
|
||||
// Uses batched WHERE n.id IN [...] queries instead of per-symbol queries
|
||||
const uniqueSymbols = symbolMatches.slice(0, 5).filter((sym, i, arr) =>
|
||||
arr.findIndex(s => s.nodeId === sym.nodeId) === i
|
||||
);
|
||||
|
||||
if (uniqueSymbols.length === 0) return '';
|
||||
|
||||
const idList = uniqueSymbols.map(s => `'${s.nodeId.replace(/'/g, "''")}'`).join(', ');
|
||||
|
||||
// Batch fetch callers
|
||||
const callersMap = new Map<string, string[]>();
|
||||
try {
|
||||
const rows = await executeQuery(repoId, `
|
||||
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(n)
|
||||
WHERE n.id IN [${idList}]
|
||||
RETURN n.id AS targetId, caller.name AS name
|
||||
LIMIT 15
|
||||
`);
|
||||
for (const r of rows) {
|
||||
const tid = r.targetId || r[0];
|
||||
const name = r.name || r[1];
|
||||
if (tid && name) {
|
||||
if (!callersMap.has(tid)) callersMap.set(tid, []);
|
||||
callersMap.get(tid)!.push(name);
|
||||
}
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
|
||||
// Batch fetch callees
|
||||
const calleesMap = new Map<string, string[]>();
|
||||
try {
|
||||
const rows = await executeQuery(repoId, `
|
||||
MATCH (n)-[:CodeRelation {type: 'CALLS'}]->(callee)
|
||||
WHERE n.id IN [${idList}]
|
||||
RETURN n.id AS sourceId, callee.name AS name
|
||||
LIMIT 15
|
||||
`);
|
||||
for (const r of rows) {
|
||||
const sid = r.sourceId || r[0];
|
||||
const name = r.name || r[1];
|
||||
if (sid && name) {
|
||||
if (!calleesMap.has(sid)) calleesMap.set(sid, []);
|
||||
calleesMap.get(sid)!.push(name);
|
||||
}
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
|
||||
// Batch fetch processes
|
||||
const processesMap = new Map<string, string[]>();
|
||||
try {
|
||||
const rows = await executeQuery(repoId, `
|
||||
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
||||
WHERE n.id IN [${idList}]
|
||||
RETURN n.id AS nodeId, p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
|
||||
`);
|
||||
for (const r of rows) {
|
||||
const nid = r.nodeId || r[0];
|
||||
const label = r.label || r[1];
|
||||
const step = r.step || r[2];
|
||||
const stepCount = r.stepCount || r[3];
|
||||
if (nid && label) {
|
||||
if (!processesMap.has(nid)) processesMap.set(nid, []);
|
||||
processesMap.get(nid)!.push(`${label} (step ${step}/${stepCount})`);
|
||||
}
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
|
||||
// Batch fetch cohesion
|
||||
const cohesionMap = new Map<string, number>();
|
||||
try {
|
||||
const rows = await executeQuery(repoId, `
|
||||
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
||||
WHERE n.id IN [${idList}]
|
||||
RETURN n.id AS nodeId, c.cohesion AS cohesion
|
||||
`);
|
||||
for (const r of rows) {
|
||||
const nid = r.nodeId || r[0];
|
||||
const coh = r.cohesion ?? r[1] ?? 0;
|
||||
if (nid) cohesionMap.set(nid, coh);
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
|
||||
// Assemble enriched results
|
||||
const enriched: Array<{
|
||||
name: string;
|
||||
filePath: string;
|
||||
|
|
@ -150,72 +232,15 @@ export async function augment(pattern: string, cwd?: string): Promise<string> {
|
|||
processes: string[];
|
||||
cohesion: number;
|
||||
}> = [];
|
||||
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const sym of symbolMatches.slice(0, 5)) {
|
||||
if (seen.has(sym.nodeId)) continue;
|
||||
seen.add(sym.nodeId);
|
||||
|
||||
const escaped = sym.nodeId.replace(/'/g, "''");
|
||||
|
||||
// Callers
|
||||
let callers: string[] = [];
|
||||
try {
|
||||
const rows = await executeQuery(repoId, `
|
||||
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(n {id: '${escaped}'})
|
||||
RETURN caller.name AS name
|
||||
LIMIT 3
|
||||
`);
|
||||
callers = rows.map((r: any) => r.name || r[0]).filter(Boolean);
|
||||
} catch { /* skip */ }
|
||||
|
||||
// Callees
|
||||
let callees: string[] = [];
|
||||
try {
|
||||
const rows = await executeQuery(repoId, `
|
||||
MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'CALLS'}]->(callee)
|
||||
RETURN callee.name AS name
|
||||
LIMIT 3
|
||||
`);
|
||||
callees = rows.map((r: any) => r.name || r[0]).filter(Boolean);
|
||||
} catch { /* skip */ }
|
||||
|
||||
// Processes
|
||||
let processes: string[] = [];
|
||||
try {
|
||||
const rows = await executeQuery(repoId, `
|
||||
MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
||||
RETURN p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
|
||||
`);
|
||||
processes = rows.map((r: any) => {
|
||||
const label = r.label || r[0];
|
||||
const step = r.step || r[1];
|
||||
const stepCount = r.stepCount || r[2];
|
||||
return `${label} (step ${step}/${stepCount})`;
|
||||
}).filter(Boolean);
|
||||
} catch { /* skip */ }
|
||||
|
||||
// Cluster cohesion (internal ranking signal)
|
||||
let cohesion = 0;
|
||||
try {
|
||||
const rows = await executeQuery(repoId, `
|
||||
MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
||||
RETURN c.cohesion AS cohesion
|
||||
LIMIT 1
|
||||
`);
|
||||
if (rows.length > 0) {
|
||||
cohesion = (rows[0].cohesion ?? rows[0][0]) || 0;
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
|
||||
|
||||
for (const sym of uniqueSymbols) {
|
||||
enriched.push({
|
||||
name: sym.name,
|
||||
filePath: sym.filePath,
|
||||
callers,
|
||||
callees,
|
||||
processes,
|
||||
cohesion,
|
||||
callers: (callersMap.get(sym.nodeId) || []).slice(0, 3),
|
||||
callees: (calleesMap.get(sym.nodeId) || []).slice(0, 3),
|
||||
processes: processesMap.get(sym.nodeId) || [],
|
||||
cohesion: cohesionMap.get(sym.nodeId) || 0,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ export const embedBatch = async (texts: string[]): Promise<Float32Array[]> => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Convert Float32Array to regular number array (for KuzuDB storage)
|
||||
* Convert Float32Array to regular number array (for LadybugDB storage)
|
||||
*/
|
||||
export const embeddingToArray = (embedding: Float32Array): number[] => {
|
||||
return Array.from(embedding);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
* Embedding Pipeline Module
|
||||
*
|
||||
* Orchestrates the background embedding process:
|
||||
* 1. Query embeddable nodes from KuzuDB
|
||||
* 1. Query embeddable nodes from LadybugDB
|
||||
* 2. Generate text representations
|
||||
* 3. Batch embed using transformers.js
|
||||
* 4. Update KuzuDB with embeddings
|
||||
* 4. Update LadybugDB with embeddings
|
||||
* 5. Create vector index for semantic search
|
||||
*/
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ const isDev = process.env.NODE_ENV === 'development';
|
|||
export type EmbeddingProgressCallback = (progress: EmbeddingProgress) => void;
|
||||
|
||||
/**
|
||||
* Query all embeddable nodes from KuzuDB
|
||||
* Query all embeddable nodes from LadybugDB
|
||||
* Uses table-specific queries (File has different schema than code elements)
|
||||
*/
|
||||
const queryEmbeddableNodes = async (
|
||||
|
|
@ -104,9 +104,23 @@ const batchInsertEmbeddings = async (
|
|||
* Create the vector index for semantic search
|
||||
* Now indexes the separate CodeEmbedding table
|
||||
*/
|
||||
let vectorExtensionLoaded = false;
|
||||
|
||||
const createVectorIndex = async (
|
||||
executeQuery: (cypher: string) => Promise<any[]>
|
||||
): Promise<void> => {
|
||||
// LadybugDB v0.15+ requires explicit VECTOR extension loading (once per session)
|
||||
if (!vectorExtensionLoaded) {
|
||||
try {
|
||||
await executeQuery('INSTALL VECTOR');
|
||||
await executeQuery('LOAD EXTENSION VECTOR');
|
||||
vectorExtensionLoaded = true;
|
||||
} catch {
|
||||
// Extension may already be loaded — CREATE_VECTOR_INDEX will fail clearly if not
|
||||
vectorExtensionLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
const cypher = `
|
||||
CALL CREATE_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx', 'embedding', metric := 'cosine')
|
||||
`;
|
||||
|
|
@ -124,7 +138,7 @@ const createVectorIndex = async (
|
|||
/**
|
||||
* Run the embedding pipeline
|
||||
*
|
||||
* @param executeQuery - Function to execute Cypher queries against KuzuDB
|
||||
* @param executeQuery - Function to execute Cypher queries against LadybugDB
|
||||
* @param executeWithReusedStatement - Function to execute with reused prepared statement
|
||||
* @param onProgress - Callback for progress updates
|
||||
* @param config - Optional configuration override
|
||||
|
|
@ -219,7 +233,7 @@ export const runEmbeddingPipeline = async (
|
|||
// Embed the batch
|
||||
const embeddings = await embedBatch(texts);
|
||||
|
||||
// Update KuzuDB with embeddings
|
||||
// Update LadybugDB with embeddings
|
||||
const updates = batch.map((node, i) => ({
|
||||
id: node.id,
|
||||
embedding: embeddingToArray(embeddings[i]),
|
||||
|
|
@ -326,51 +340,64 @@ export const semanticSearch = async (
|
|||
return [];
|
||||
}
|
||||
|
||||
// Get metadata for each result by querying each node table
|
||||
const results: SemanticSearchResult[] = [];
|
||||
|
||||
// Group results by label for batched metadata queries
|
||||
const byLabel = new Map<string, Array<{ nodeId: string; distance: number }>>();
|
||||
for (const embRow of embResults) {
|
||||
const nodeId = embRow.nodeId ?? embRow[0];
|
||||
const distance = embRow.distance ?? embRow[1];
|
||||
|
||||
// Extract label from node ID (format: Label:path:name)
|
||||
const labelEndIdx = nodeId.indexOf(':');
|
||||
const label = labelEndIdx > 0 ? nodeId.substring(0, labelEndIdx) : 'Unknown';
|
||||
|
||||
// Query the specific table for this node
|
||||
// File nodes don't have startLine/endLine
|
||||
if (!byLabel.has(label)) byLabel.set(label, []);
|
||||
byLabel.get(label)!.push({ nodeId, distance });
|
||||
}
|
||||
|
||||
// Batch-fetch metadata per label
|
||||
const results: SemanticSearchResult[] = [];
|
||||
|
||||
for (const [label, items] of byLabel) {
|
||||
const idList = items.map(i => `'${i.nodeId.replace(/'/g, "''")}'`).join(', ');
|
||||
try {
|
||||
let nodeQuery: string;
|
||||
if (label === 'File') {
|
||||
nodeQuery = `
|
||||
MATCH (n:File {id: '${nodeId.replace(/'/g, "''")}'})
|
||||
RETURN n.name AS name, n.filePath AS filePath
|
||||
MATCH (n:File) WHERE n.id IN [${idList}]
|
||||
RETURN n.id AS id, n.name AS name, n.filePath AS filePath
|
||||
`;
|
||||
} else {
|
||||
nodeQuery = `
|
||||
MATCH (n:${label} {id: '${nodeId.replace(/'/g, "''")}'})
|
||||
RETURN n.name AS name, n.filePath AS filePath,
|
||||
MATCH (n:${label}) WHERE n.id IN [${idList}]
|
||||
RETURN n.id AS id, n.name AS name, n.filePath AS filePath,
|
||||
n.startLine AS startLine, n.endLine AS endLine
|
||||
`;
|
||||
}
|
||||
const nodeRows = await executeQuery(nodeQuery);
|
||||
if (nodeRows.length > 0) {
|
||||
const nodeRow = nodeRows[0];
|
||||
results.push({
|
||||
nodeId,
|
||||
name: nodeRow.name ?? nodeRow[0] ?? '',
|
||||
label,
|
||||
filePath: nodeRow.filePath ?? nodeRow[1] ?? '',
|
||||
distance,
|
||||
startLine: label !== 'File' ? (nodeRow.startLine ?? nodeRow[2]) : undefined,
|
||||
endLine: label !== 'File' ? (nodeRow.endLine ?? nodeRow[3]) : undefined,
|
||||
});
|
||||
const rowMap = new Map<string, any>();
|
||||
for (const row of nodeRows) {
|
||||
const id = row.id ?? row[0];
|
||||
rowMap.set(id, row);
|
||||
}
|
||||
for (const item of items) {
|
||||
const nodeRow = rowMap.get(item.nodeId);
|
||||
if (nodeRow) {
|
||||
results.push({
|
||||
nodeId: item.nodeId,
|
||||
name: nodeRow.name ?? nodeRow[1] ?? '',
|
||||
label,
|
||||
filePath: nodeRow.filePath ?? nodeRow[2] ?? '',
|
||||
distance: item.distance,
|
||||
startLine: label !== 'File' ? (nodeRow.startLine ?? nodeRow[3]) : undefined,
|
||||
endLine: label !== 'File' ? (nodeRow.endLine ?? nodeRow[4]) : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Table might not exist, skip
|
||||
}
|
||||
}
|
||||
|
||||
// Re-sort by distance since batch queries may have mixed order
|
||||
results.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export interface SemanticSearchResult {
|
|||
}
|
||||
|
||||
/**
|
||||
* Node data for embedding (minimal structure from KuzuDB query)
|
||||
* Node data for embedding (minimal structure from LadybugDB query)
|
||||
*/
|
||||
export interface EmbeddableNode {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* CSV Generator for KuzuDB Hybrid Schema
|
||||
* CSV Generator for LadybugDB Hybrid Schema
|
||||
*
|
||||
* Streams CSV rows directly to disk files in a single pass over graph nodes.
|
||||
* File contents are lazy-read from disk per-node to avoid holding the entire
|
||||
|
|
@ -2,7 +2,7 @@ import fs from 'fs/promises';
|
|||
import { createReadStream } from 'fs';
|
||||
import { createInterface } from 'readline';
|
||||
import path from 'path';
|
||||
import kuzu from 'kuzu';
|
||||
import lbug from '@ladybugdb/core';
|
||||
import { KnowledgeGraph } from '../graph/types.js';
|
||||
import {
|
||||
NODE_TABLES,
|
||||
|
|
@ -13,12 +13,12 @@ import {
|
|||
} from './schema.js';
|
||||
import { streamAllCSVsToDisk } from './csv-generator.js';
|
||||
|
||||
let db: kuzu.Database | null = null;
|
||||
let conn: kuzu.Connection | null = null;
|
||||
let db: lbug.Database | null = null;
|
||||
let conn: lbug.Connection | null = null;
|
||||
let currentDbPath: string | null = null;
|
||||
let ftsLoaded = false;
|
||||
|
||||
// Global session lock for operations that touch module-level kuzu globals.
|
||||
// Global session lock for operations that touch module-level lbug globals.
|
||||
// This guarantees no DB switch can happen while an operation is running.
|
||||
let sessionLock: Promise<void> = Promise.resolve();
|
||||
|
||||
|
|
@ -39,30 +39,30 @@ const runWithSessionLock = async <T>(operation: () => Promise<T>): Promise<T> =>
|
|||
|
||||
const normalizeCopyPath = (filePath: string): string => filePath.replace(/\\/g, '/');
|
||||
|
||||
export const initKuzu = async (dbPath: string) => {
|
||||
return runWithSessionLock(() => ensureKuzuInitialized(dbPath));
|
||||
export const initLbug = async (dbPath: string) => {
|
||||
return runWithSessionLock(() => ensureLbugInitialized(dbPath));
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute multiple queries against one repo DB atomically.
|
||||
* While the callback runs, no other request can switch the active DB.
|
||||
*/
|
||||
export const withKuzuDb = async <T>(dbPath: string, operation: () => Promise<T>): Promise<T> => {
|
||||
export const withLbugDb = async <T>(dbPath: string, operation: () => Promise<T>): Promise<T> => {
|
||||
return runWithSessionLock(async () => {
|
||||
await ensureKuzuInitialized(dbPath);
|
||||
await ensureLbugInitialized(dbPath);
|
||||
return operation();
|
||||
});
|
||||
};
|
||||
|
||||
const ensureKuzuInitialized = async (dbPath: string) => {
|
||||
const ensureLbugInitialized = async (dbPath: string) => {
|
||||
if (conn && currentDbPath === dbPath) {
|
||||
return { db, conn };
|
||||
}
|
||||
await doInitKuzu(dbPath);
|
||||
await doInitLbug(dbPath);
|
||||
return { db, conn };
|
||||
};
|
||||
|
||||
const doInitKuzu = async (dbPath: string) => {
|
||||
const doInitLbug = async (dbPath: string) => {
|
||||
// Different database requested — close the old one first
|
||||
if (conn || db) {
|
||||
try { if (conn) await conn.close(); } catch {}
|
||||
|
|
@ -73,32 +73,36 @@ const doInitKuzu = async (dbPath: string) => {
|
|||
ftsLoaded = false;
|
||||
}
|
||||
|
||||
// kuzu v0.11 stores the database as a single file (not a directory).
|
||||
// If the path already exists, it must be a valid kuzu database file.
|
||||
// LadybugDB stores the database as a single file (not a directory).
|
||||
// If the path already exists, it must be a valid LadybugDB database file.
|
||||
// Remove stale empty directories or files from older versions.
|
||||
try {
|
||||
const stat = await fs.stat(dbPath);
|
||||
if (stat.isDirectory()) {
|
||||
// Old-style directory database or empty leftover - remove it
|
||||
const files = await fs.readdir(dbPath);
|
||||
if (files.length === 0) {
|
||||
await fs.rmdir(dbPath);
|
||||
} else {
|
||||
// Non-empty directory from older kuzu version - remove entire directory
|
||||
await fs.rm(dbPath, { recursive: true, force: true });
|
||||
const stat = await fs.lstat(dbPath);
|
||||
if (stat.isSymbolicLink()) {
|
||||
// Never follow symlinks — just remove the link itself
|
||||
await fs.unlink(dbPath);
|
||||
} else if (stat.isDirectory()) {
|
||||
// Verify path is within expected storage directory before deleting
|
||||
const realPath = await fs.realpath(dbPath);
|
||||
const parentDir = path.dirname(dbPath);
|
||||
const realParent = await fs.realpath(parentDir);
|
||||
if (!realPath.startsWith(realParent + path.sep) && realPath !== realParent) {
|
||||
throw new Error(`Refusing to delete ${dbPath}: resolved path ${realPath} is outside storage directory`);
|
||||
}
|
||||
// Old-style directory database or empty leftover - remove it
|
||||
await fs.rm(dbPath, { recursive: true, force: true });
|
||||
}
|
||||
// If it's a file, assume it's an existing kuzu database - kuzu will open it
|
||||
// If it's a file, assume it's an existing LadybugDB database - LadybugDB will open it
|
||||
} catch {
|
||||
// Path doesn't exist, which is what kuzu wants for a new database
|
||||
// Path doesn't exist, which is what LadybugDB wants for a new database
|
||||
}
|
||||
|
||||
// Ensure parent directory exists
|
||||
const parentDir = path.dirname(dbPath);
|
||||
await fs.mkdir(parentDir, { recursive: true });
|
||||
|
||||
db = new kuzu.Database(dbPath);
|
||||
conn = new kuzu.Connection(db);
|
||||
db = new lbug.Database(dbPath);
|
||||
conn = new lbug.Connection(db);
|
||||
|
||||
for (const schemaQuery of SCHEMA_QUERIES) {
|
||||
try {
|
||||
|
|
@ -116,16 +120,16 @@ const doInitKuzu = async (dbPath: string) => {
|
|||
return { db, conn };
|
||||
};
|
||||
|
||||
export type KuzuProgressCallback = (message: string) => void;
|
||||
export type LbugProgressCallback = (message: string) => void;
|
||||
|
||||
export const loadGraphToKuzu = async (
|
||||
export const loadGraphToLbug = async (
|
||||
graph: KnowledgeGraph,
|
||||
repoPath: string,
|
||||
storagePath: string,
|
||||
onProgress?: KuzuProgressCallback
|
||||
onProgress?: LbugProgressCallback
|
||||
) => {
|
||||
if (!conn) {
|
||||
throw new Error('KuzuDB not initialized. Call initKuzu first.');
|
||||
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
||||
}
|
||||
|
||||
const log = onProgress || (() => {});
|
||||
|
|
@ -142,7 +146,7 @@ export const loadGraphToKuzu = async (
|
|||
return nodeId.split(':')[0];
|
||||
};
|
||||
|
||||
// Bulk COPY all node CSVs (sequential — KuzuDB allows only one write txn at a time)
|
||||
// Bulk COPY all node CSVs (sequential — LadybugDB allows only one write txn at a time)
|
||||
const nodeFiles = [...csvResult.nodeFiles.entries()];
|
||||
const totalSteps = nodeFiles.length + 1; // +1 for relationships
|
||||
let stepsDone = 0;
|
||||
|
|
@ -167,7 +171,7 @@ export const loadGraphToKuzu = async (
|
|||
}
|
||||
}
|
||||
|
||||
// Bulk COPY relationships — split by FROM→TO label pair (KuzuDB requires it)
|
||||
// Bulk COPY relationships — split by FROM→TO label pair (LadybugDB requires it)
|
||||
// Stream-read the relation CSV line by line to avoid exceeding V8 max string length
|
||||
let relHeader = '';
|
||||
const relsByPair = new Map<string, string[]>();
|
||||
|
|
@ -258,10 +262,10 @@ export const loadGraphToKuzu = async (
|
|||
return { success: true, insertedRels, skippedRels, warnings };
|
||||
};
|
||||
|
||||
// KuzuDB default ESCAPE is '\' (backslash), but our CSV uses RFC 4180 escaping ("" for literal quotes).
|
||||
// LadybugDB default ESCAPE is '\' (backslash), but our CSV uses RFC 4180 escaping ("" for literal quotes).
|
||||
// Source code content is full of backslashes which confuse the auto-detection.
|
||||
// We MUST explicitly set ESCAPE='"' to use RFC 4180 escaping, and disable auto_detect to prevent
|
||||
// KuzuDB from overriding our settings based on sample rows.
|
||||
// LadybugDB from overriding our settings based on sample rows.
|
||||
const COPY_CSV_OPTS = `(HEADER=true, ESCAPE='"', DELIM=',', QUOTE='"', PARALLEL=false, auto_detect=false)`;
|
||||
|
||||
// Multi-language table names that were created with backticks in CODE_ELEMENT_BASE
|
||||
|
|
@ -340,12 +344,12 @@ const getCopyQuery = (table: NodeTableName, filePath: string): string => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Insert a single node to KuzuDB
|
||||
* Insert a single node to LadybugDB
|
||||
* @param label - Node type (File, Function, Class, etc.)
|
||||
* @param properties - Node properties
|
||||
* @param dbPath - Path to KuzuDB database (optional if already initialized)
|
||||
* @param dbPath - Path to LadybugDB database (optional if already initialized)
|
||||
*/
|
||||
export const insertNodeToKuzu = async (
|
||||
export const insertNodeToLbug = async (
|
||||
label: string,
|
||||
properties: Record<string, any>,
|
||||
dbPath?: string
|
||||
|
|
@ -353,7 +357,7 @@ export const insertNodeToKuzu = async (
|
|||
// Use provided dbPath or fall back to module-level db
|
||||
const targetDbPath = dbPath || (db ? undefined : null);
|
||||
if (!targetDbPath && !db) {
|
||||
throw new Error('KuzuDB not initialized. Provide dbPath or call initKuzu first.');
|
||||
throw new Error('LadybugDB not initialized. Provide dbPath or call initLbug first.');
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -380,11 +384,11 @@ export const insertNodeToKuzu = async (
|
|||
const descPart = properties.description ? `, description: ${escapeValue(properties.description)}` : '';
|
||||
query = `CREATE (n:${t} {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${escapeValue(properties.content || '')}${descPart}})`;
|
||||
}
|
||||
|
||||
|
||||
// Use per-query connection if dbPath provided (avoids lock conflicts)
|
||||
if (targetDbPath) {
|
||||
const tempDb = new kuzu.Database(targetDbPath);
|
||||
const tempConn = new kuzu.Connection(tempDb);
|
||||
const tempDb = new lbug.Database(targetDbPath);
|
||||
const tempConn = new lbug.Connection(tempDb);
|
||||
try {
|
||||
await tempConn.query(query);
|
||||
return true;
|
||||
|
|
@ -397,7 +401,7 @@ export const insertNodeToKuzu = async (
|
|||
await conn.query(query);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
} catch (e: any) {
|
||||
// Node may already exist or other error
|
||||
|
|
@ -407,36 +411,36 @@ export const insertNodeToKuzu = async (
|
|||
};
|
||||
|
||||
/**
|
||||
* Batch insert multiple nodes to KuzuDB using a single connection
|
||||
* Batch insert multiple nodes to LadybugDB using a single connection
|
||||
* @param nodes - Array of {label, properties} to insert
|
||||
* @param dbPath - Path to KuzuDB database
|
||||
* @param dbPath - Path to LadybugDB database
|
||||
* @returns Object with success count and error count
|
||||
*/
|
||||
export const batchInsertNodesToKuzu = async (
|
||||
export const batchInsertNodesToLbug = async (
|
||||
nodes: Array<{ label: string; properties: Record<string, any> }>,
|
||||
dbPath: string
|
||||
): Promise<{ inserted: number; failed: number }> => {
|
||||
if (nodes.length === 0) return { inserted: 0, failed: 0 };
|
||||
|
||||
|
||||
const escapeValue = (v: any): string => {
|
||||
if (v === null || v === undefined) return 'NULL';
|
||||
if (typeof v === 'number') return String(v);
|
||||
// Escape backslashes first (for Windows paths), then single quotes
|
||||
return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "''")}'`;
|
||||
};
|
||||
|
||||
|
||||
// Open a single connection for all inserts
|
||||
const tempDb = new kuzu.Database(dbPath);
|
||||
const tempConn = new kuzu.Connection(tempDb);
|
||||
|
||||
const tempDb = new lbug.Database(dbPath);
|
||||
const tempConn = new lbug.Connection(tempDb);
|
||||
|
||||
let inserted = 0;
|
||||
let failed = 0;
|
||||
|
||||
|
||||
try {
|
||||
for (const { label, properties } of nodes) {
|
||||
try {
|
||||
let query: string;
|
||||
|
||||
|
||||
// Use MERGE instead of CREATE for upsert behavior (handles duplicates gracefully)
|
||||
const t = escapeTableName(label);
|
||||
if (label === 'File') {
|
||||
|
|
@ -450,7 +454,7 @@ export const batchInsertNodesToKuzu = async (
|
|||
const descPart = properties.description ? `, n.description = ${escapeValue(properties.description)}` : '';
|
||||
query = `MERGE (n:${t} {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.content = ${escapeValue(properties.content || '')}${descPart}`;
|
||||
}
|
||||
|
||||
|
||||
await tempConn.query(query);
|
||||
inserted++;
|
||||
} catch (e: any) {
|
||||
|
|
@ -462,17 +466,17 @@ export const batchInsertNodesToKuzu = async (
|
|||
try { await tempConn.close(); } catch {}
|
||||
try { await tempDb.close(); } catch {}
|
||||
}
|
||||
|
||||
|
||||
return { inserted, failed };
|
||||
};
|
||||
|
||||
export const executeQuery = async (cypher: string): Promise<any[]> => {
|
||||
if (!conn) {
|
||||
throw new Error('KuzuDB not initialized. Call initKuzu first.');
|
||||
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
||||
}
|
||||
|
||||
const queryResult = await conn.query(cypher);
|
||||
// kuzu v0.11 uses getAll() instead of hasNext()/getNext()
|
||||
// LadybugDB uses getAll() instead of hasNext()/getNext()
|
||||
// Query returns QueryResult for single queries, QueryResult[] for multi-statement
|
||||
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
||||
const rows = await result.getAll();
|
||||
|
|
@ -484,7 +488,7 @@ export const executeWithReusedStatement = async (
|
|||
paramsList: Array<Record<string, any>>
|
||||
): Promise<void> => {
|
||||
if (!conn) {
|
||||
throw new Error('KuzuDB not initialized. Call initKuzu first.');
|
||||
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
||||
}
|
||||
if (paramsList.length === 0) return;
|
||||
|
||||
|
|
@ -504,11 +508,11 @@ export const executeWithReusedStatement = async (
|
|||
// Log the error and continue with next batch
|
||||
console.warn('Batch execution error:', e);
|
||||
}
|
||||
// Note: kuzu 0.8.2 PreparedStatement doesn't require explicit close()
|
||||
// Note: LadybugDB PreparedStatement doesn't require explicit close()
|
||||
}
|
||||
};
|
||||
|
||||
export const getKuzuStats = async (): Promise<{ nodes: number; edges: number }> => {
|
||||
export const getLbugStats = async (): Promise<{ nodes: number; edges: number }> => {
|
||||
if (!conn) return { nodes: 0, edges: 0 };
|
||||
|
||||
let totalNodes = 0;
|
||||
|
|
@ -541,7 +545,7 @@ export const getKuzuStats = async (): Promise<{ nodes: number; edges: number }>
|
|||
};
|
||||
|
||||
/**
|
||||
* Load cached embeddings from KuzuDB before a rebuild.
|
||||
* Load cached embeddings from LadybugDB before a rebuild.
|
||||
* Returns all embedding vectors so they can be re-inserted after the graph is reloaded,
|
||||
* avoiding expensive re-embedding of unchanged nodes.
|
||||
*/
|
||||
|
|
@ -575,7 +579,7 @@ export const loadCachedEmbeddings = async (): Promise<{
|
|||
return { embeddingNodeIds, embeddings };
|
||||
};
|
||||
|
||||
export const closeKuzu = async (): Promise<void> => {
|
||||
export const closeLbug = async (): Promise<void> => {
|
||||
if (conn) {
|
||||
try {
|
||||
await conn.close();
|
||||
|
|
@ -592,41 +596,41 @@ export const closeKuzu = async (): Promise<void> => {
|
|||
ftsLoaded = false;
|
||||
};
|
||||
|
||||
export const isKuzuReady = (): boolean => conn !== null && db !== null;
|
||||
export const isLbugReady = (): boolean => conn !== null && db !== null;
|
||||
|
||||
|
||||
/**
|
||||
* Delete all nodes (and their relationships) for a specific file from KuzuDB
|
||||
* Delete all nodes (and their relationships) for a specific file from LadybugDB
|
||||
* @param filePath - The file path to delete nodes for
|
||||
* @param dbPath - Optional path to KuzuDB for per-query connection
|
||||
* @param dbPath - Optional path to LadybugDB for per-query connection
|
||||
* @returns Object with counts of deleted nodes
|
||||
*/
|
||||
export const deleteNodesForFile = async (filePath: string, dbPath?: string): Promise<{ deletedNodes: number }> => {
|
||||
const usePerQuery = !!dbPath;
|
||||
|
||||
|
||||
// Set up connection (either use existing or create per-query)
|
||||
let tempDb: kuzu.Database | null = null;
|
||||
let tempConn: kuzu.Connection | null = null;
|
||||
let targetConn: kuzu.Connection | null = conn;
|
||||
|
||||
let tempDb: lbug.Database | null = null;
|
||||
let tempConn: lbug.Connection | null = null;
|
||||
let targetConn: lbug.Connection | null = conn;
|
||||
|
||||
if (usePerQuery) {
|
||||
tempDb = new kuzu.Database(dbPath);
|
||||
tempConn = new kuzu.Connection(tempDb);
|
||||
tempDb = new lbug.Database(dbPath);
|
||||
tempConn = new lbug.Connection(tempDb);
|
||||
targetConn = tempConn;
|
||||
} else if (!conn) {
|
||||
throw new Error('KuzuDB not initialized. Provide dbPath or call initKuzu first.');
|
||||
throw new Error('LadybugDB not initialized. Provide dbPath or call initLbug first.');
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
let deletedNodes = 0;
|
||||
const escapedPath = filePath.replace(/'/g, "''");
|
||||
|
||||
|
||||
// Delete nodes from each table that has filePath
|
||||
// DETACH DELETE removes the node and all its relationships
|
||||
for (const tableName of NODE_TABLES) {
|
||||
// Skip tables that don't have filePath (Community, Process)
|
||||
if (tableName === 'Community' || tableName === 'Process') continue;
|
||||
|
||||
|
||||
try {
|
||||
// First count how many we'll delete
|
||||
const tn = escapeTableName(tableName);
|
||||
|
|
@ -648,7 +652,7 @@ export const deleteNodesForFile = async (filePath: string, dbPath?: string): Pro
|
|||
// Some tables may not support this query, skip
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Also delete any embeddings for nodes in this file
|
||||
try {
|
||||
await targetConn!.query(
|
||||
|
|
@ -657,7 +661,7 @@ export const deleteNodesForFile = async (filePath: string, dbPath?: string): Pro
|
|||
} catch {
|
||||
// Embedding table may not exist or nodeId format may differ
|
||||
}
|
||||
|
||||
|
||||
return { deletedNodes };
|
||||
} finally {
|
||||
// Close per-query connection if used
|
||||
|
|
@ -683,7 +687,7 @@ export const getEmbeddingTableName = (): string => EMBEDDING_TABLE_NAME;
|
|||
export const loadFTSExtension = async (): Promise<void> => {
|
||||
if (ftsLoaded) return;
|
||||
if (!conn) {
|
||||
throw new Error('KuzuDB not initialized. Call initKuzu first.');
|
||||
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
||||
}
|
||||
try {
|
||||
await conn.query('INSTALL fts');
|
||||
|
|
@ -713,7 +717,7 @@ export const createFTSIndex = async (
|
|||
stemmer: string = 'porter'
|
||||
): Promise<void> => {
|
||||
if (!conn) {
|
||||
throw new Error('KuzuDB not initialized. Call initKuzu first.');
|
||||
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
||||
}
|
||||
|
||||
await loadFTSExtension();
|
||||
|
|
@ -747,24 +751,24 @@ export const queryFTS = async (
|
|||
conjunctive: boolean = false
|
||||
): Promise<Array<{ nodeId: string; name: string; filePath: string; score: number; [key: string]: any }>> => {
|
||||
if (!conn) {
|
||||
throw new Error('KuzuDB not initialized. Call initKuzu first.');
|
||||
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
||||
}
|
||||
|
||||
|
||||
// Escape backslashes and single quotes to prevent Cypher injection
|
||||
const escapedQuery = query.replace(/\\/g, '\\\\').replace(/'/g, "''");
|
||||
|
||||
|
||||
const cypher = `
|
||||
CALL QUERY_FTS_INDEX('${tableName}', '${indexName}', '${escapedQuery}', conjunctive := ${conjunctive})
|
||||
RETURN node, score
|
||||
ORDER BY score DESC
|
||||
LIMIT ${limit}
|
||||
`;
|
||||
|
||||
|
||||
try {
|
||||
const queryResult = await conn.query(cypher);
|
||||
const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
||||
const rows = await result.getAll();
|
||||
|
||||
|
||||
return rows.map((row: any) => {
|
||||
const node = row.node || row[0] || {};
|
||||
const score = row.score ?? row[1] ?? 0;
|
||||
|
|
@ -790,9 +794,9 @@ export const queryFTS = async (
|
|||
*/
|
||||
export const dropFTSIndex = async (tableName: string, indexName: string): Promise<void> => {
|
||||
if (!conn) {
|
||||
throw new Error('KuzuDB not initialized. Call initKuzu first.');
|
||||
throw new Error('LadybugDB not initialized. Call initLbug first.');
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
await conn.query(`CALL DROP_FTS_INDEX('${tableName}', '${indexName}')`);
|
||||
} catch {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* KuzuDB Schema Definitions
|
||||
* LadybugDB Schema Definitions
|
||||
*
|
||||
* Hybrid Schema:
|
||||
* - Separate node tables for each code element type (File, Function, Class, etc.)
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
/**
|
||||
* Full-Text Search via KuzuDB FTS
|
||||
*
|
||||
* Uses KuzuDB's built-in full-text search indexes for keyword-based search.
|
||||
* Full-Text Search via LadybugDB FTS
|
||||
*
|
||||
* Uses LadybugDB's built-in full-text search indexes for keyword-based search.
|
||||
* Always reads from the database (no cached state to drift).
|
||||
*/
|
||||
|
||||
import { queryFTS } from '../kuzu/kuzu-adapter.js';
|
||||
import { queryFTS } from '../lbug/lbug-adapter.js';
|
||||
|
||||
export interface BM25SearchResult {
|
||||
filePath: string;
|
||||
|
|
@ -15,7 +15,7 @@ export interface BM25SearchResult {
|
|||
|
||||
/**
|
||||
* Execute a single FTS query via a custom executor (for MCP connection pool).
|
||||
* Returns the same shape as core queryFTS.
|
||||
* Returns the same shape as core queryFTS (from LadybugDB adapter).
|
||||
*/
|
||||
async function queryFTSViaExecutor(
|
||||
executor: (cypher: string) => Promise<any[]>,
|
||||
|
|
@ -48,24 +48,24 @@ async function queryFTSViaExecutor(
|
|||
}
|
||||
|
||||
/**
|
||||
* Search using KuzuDB's built-in FTS (always fresh, reads from disk)
|
||||
*
|
||||
* Search using LadybugDB's built-in FTS (always fresh, reads from disk)
|
||||
*
|
||||
* Queries multiple node tables (File, Function, Class, Method) in parallel
|
||||
* and merges results by filePath, summing scores for the same file.
|
||||
*
|
||||
*
|
||||
* @param query - Search query string
|
||||
* @param limit - Maximum results
|
||||
* @param repoId - If provided, queries will be routed via the MCP connection pool
|
||||
* @returns Ranked search results from FTS indexes
|
||||
*/
|
||||
export const searchFTSFromKuzu = async (query: string, limit: number = 20, repoId?: string): Promise<BM25SearchResult[]> => {
|
||||
export const searchFTSFromLbug = async (query: string, limit: number = 20, repoId?: string): Promise<BM25SearchResult[]> => {
|
||||
let fileResults: any[], functionResults: any[], classResults: any[], methodResults: any[], interfaceResults: any[];
|
||||
|
||||
if (repoId) {
|
||||
// Use MCP connection pool via dynamic import
|
||||
// IMPORTANT: KuzuDB uses a single connection per repo — queries must be sequential
|
||||
// to avoid deadlocking. Do NOT use Promise.all here.
|
||||
const { executeQuery } = await import('../../mcp/core/kuzu-adapter.js');
|
||||
// IMPORTANT: FTS queries run sequentially to avoid connection contention.
|
||||
// The MCP pool supports multiple connections, but FTS is best run serially.
|
||||
const { executeQuery } = await import('../../mcp/core/lbug-adapter.js');
|
||||
const executor = (cypher: string) => executeQuery(repoId, cypher);
|
||||
fileResults = await queryFTSViaExecutor(executor, 'File', 'file_fts', query, limit);
|
||||
functionResults = await queryFTSViaExecutor(executor, 'Function', 'function_fts', query, limit);
|
||||
|
|
@ -73,7 +73,7 @@ export const searchFTSFromKuzu = async (query: string, limit: number = 20, repoI
|
|||
methodResults = await queryFTSViaExecutor(executor, 'Method', 'method_fts', query, limit);
|
||||
interfaceResults = await queryFTSViaExecutor(executor, 'Interface', 'interface_fts', query, limit);
|
||||
} else {
|
||||
// Use core kuzu adapter (CLI / pipeline context) — also sequential for safety
|
||||
// Use core lbug adapter (CLI / pipeline context) — also sequential for safety
|
||||
fileResults = await queryFTS('File', 'file_fts', query, limit, false).catch(() => []);
|
||||
functionResults = await queryFTS('Function', 'function_fts', query, limit, false).catch(() => []);
|
||||
classResults = await queryFTS('Class', 'class_fts', query, limit, false).catch(() => []);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
* production search systems.
|
||||
*/
|
||||
|
||||
import { searchFTSFromKuzu, type BM25SearchResult } from './bm25-index.js';
|
||||
import { searchFTSFromLbug, type BM25SearchResult } from './bm25-index.js';
|
||||
import type { SemanticSearchResult } from '../embeddings/types.js';
|
||||
|
||||
/**
|
||||
|
|
@ -114,11 +114,11 @@ export const mergeWithRRF = (
|
|||
|
||||
/**
|
||||
* Check if hybrid search is available
|
||||
* KuzuDB FTS is always available once the database is initialized.
|
||||
* LadybugDB FTS is always available once the database is initialized.
|
||||
* Semantic search is optional - hybrid works with just FTS if embeddings aren't ready.
|
||||
*/
|
||||
export const isHybridSearchReady = (): boolean => {
|
||||
return true; // FTS is always available via KuzuDB when DB is open
|
||||
return true; // FTS is always available via LadybugDB when DB is open
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -146,7 +146,7 @@ export const formatHybridResults = (results: HybridSearchResult[]): string => {
|
|||
|
||||
/**
|
||||
* Execute BM25 + semantic search and merge with RRF.
|
||||
* Uses KuzuDB FTS for always-fresh BM25 results (no cached data).
|
||||
* Uses LadybugDB FTS for always-fresh BM25 results (no cached data).
|
||||
* The semanticSearch function is injected to keep this module environment-agnostic.
|
||||
*/
|
||||
export const hybridSearch = async (
|
||||
|
|
@ -155,8 +155,8 @@ export const hybridSearch = async (
|
|||
executeQuery: (cypher: string) => Promise<any[]>,
|
||||
semanticSearch: (executeQuery: (cypher: string) => Promise<any[]>, query: string, k?: number) => Promise<SemanticSearchResult[]>
|
||||
): Promise<HybridSearchResult[]> => {
|
||||
// Use KuzuDB FTS for always-fresh BM25 results
|
||||
const bm25Results = await searchFTSFromKuzu(query, limit);
|
||||
// Use LadybugDB FTS for always-fresh BM25 results
|
||||
const bm25Results = await searchFTSFromLbug(query, limit);
|
||||
const semanticResults = await semanticSearch(executeQuery, query, limit);
|
||||
return mergeWithRRF(bm25Results, semanticResults, limit);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export class WikiGenerator {
|
|||
private repoPath: string;
|
||||
private storagePath: string;
|
||||
private wikiDir: string;
|
||||
private kuzuPath: string;
|
||||
private lbugPath: string;
|
||||
private llmConfig: LLMConfig;
|
||||
private maxTokensPerModule: number;
|
||||
private concurrency: number;
|
||||
|
|
@ -104,7 +104,7 @@ export class WikiGenerator {
|
|||
constructor(
|
||||
repoPath: string,
|
||||
storagePath: string,
|
||||
kuzuPath: string,
|
||||
lbugPath: string,
|
||||
llmConfig: LLMConfig,
|
||||
options: WikiOptions = {},
|
||||
onProgress?: ProgressCallback,
|
||||
|
|
@ -112,7 +112,7 @@ export class WikiGenerator {
|
|||
this.repoPath = repoPath;
|
||||
this.storagePath = storagePath;
|
||||
this.wikiDir = path.join(storagePath, WIKI_DIR);
|
||||
this.kuzuPath = kuzuPath;
|
||||
this.lbugPath = lbugPath;
|
||||
this.options = options;
|
||||
this.llmConfig = llmConfig;
|
||||
this.maxTokensPerModule = options.maxTokensPerModule ?? DEFAULT_MAX_TOKENS_PER_MODULE;
|
||||
|
|
@ -171,7 +171,7 @@ export class WikiGenerator {
|
|||
|
||||
// Init graph
|
||||
this.onProgress('init', 2, 'Connecting to knowledge graph...');
|
||||
await initWikiDb(this.kuzuPath);
|
||||
await initWikiDb(this.lbugPath);
|
||||
|
||||
let result: { pagesGenerated: number; mode: 'full' | 'incremental' | 'up-to-date'; failedModules: string[] };
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
/**
|
||||
* Graph Queries for Wiki Generation
|
||||
*
|
||||
*
|
||||
* Encapsulated Cypher queries against the GitNexus knowledge graph.
|
||||
* Uses the MCP-style pooled kuzu-adapter for connection management.
|
||||
* Uses the MCP-style pooled lbug-adapter for connection management.
|
||||
*/
|
||||
|
||||
import { initKuzu, executeQuery, closeKuzu } from '../../mcp/core/kuzu-adapter.js';
|
||||
import { initLbug, executeQuery, closeLbug } from '../../mcp/core/lbug-adapter.js';
|
||||
|
||||
const REPO_ID = '__wiki__';
|
||||
|
||||
|
|
@ -35,17 +35,17 @@ export interface ProcessInfo {
|
|||
}
|
||||
|
||||
/**
|
||||
* Initialize the KuzuDB connection for wiki generation.
|
||||
* Initialize the LadybugDB connection for wiki generation.
|
||||
*/
|
||||
export async function initWikiDb(kuzuPath: string): Promise<void> {
|
||||
await initKuzu(REPO_ID, kuzuPath);
|
||||
export async function initWikiDb(lbugPath: string): Promise<void> {
|
||||
await initLbug(REPO_ID, lbugPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the KuzuDB connection.
|
||||
* Close the LadybugDB connection.
|
||||
*/
|
||||
export async function closeWikiDb(): Promise<void> {
|
||||
await closeKuzu(REPO_ID);
|
||||
await closeLbug(REPO_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,36 +1,48 @@
|
|||
/**
|
||||
* KuzuDB Adapter (Connection Pool)
|
||||
*
|
||||
* Manages a pool of KuzuDB databases keyed by repoId, each with
|
||||
* LadybugDB Adapter (Connection Pool)
|
||||
*
|
||||
* Manages a pool of LadybugDB databases keyed by repoId, each with
|
||||
* multiple Connection objects for safe concurrent query execution.
|
||||
*
|
||||
* KuzuDB Connections are NOT thread-safe — a single Connection
|
||||
*
|
||||
* LadybugDB Connections are NOT thread-safe — a single Connection
|
||||
* segfaults if concurrent .query() calls hit it simultaneously.
|
||||
* This adapter provides a checkout/return connection pool so each
|
||||
* concurrent query gets its own Connection from the same Database.
|
||||
*
|
||||
* @see https://docs.kuzudb.com/concurrency — multiple Connections
|
||||
*
|
||||
* @see https://docs.ladybugdb.com/concurrency — multiple Connections
|
||||
* from the same Database is the officially supported concurrency pattern.
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import kuzu from 'kuzu';
|
||||
import lbug from '@ladybugdb/core';
|
||||
|
||||
/** Per-repo pool: one Database, many Connections */
|
||||
interface PoolEntry {
|
||||
db: kuzu.Database;
|
||||
db: lbug.Database;
|
||||
/** Available connections ready for checkout */
|
||||
available: kuzu.Connection[];
|
||||
available: lbug.Connection[];
|
||||
/** Number of connections currently checked out */
|
||||
checkedOut: number;
|
||||
/** Queued waiters for when all connections are busy */
|
||||
waiters: Array<(conn: kuzu.Connection) => void>;
|
||||
waiters: Array<(conn: lbug.Connection) => void>;
|
||||
lastUsed: number;
|
||||
dbPath: string;
|
||||
}
|
||||
|
||||
const pool = new Map<string, PoolEntry>();
|
||||
|
||||
/**
|
||||
* Shared Database cache keyed by resolved dbPath.
|
||||
* Multiple repoIds pointing to the same path share one native Database
|
||||
* object to avoid exhausting the buffer manager's mmap budget.
|
||||
*/
|
||||
interface SharedDB {
|
||||
db: lbug.Database;
|
||||
refCount: number;
|
||||
ftsLoaded: boolean;
|
||||
}
|
||||
const dbCache = new Map<string, SharedDB>();
|
||||
|
||||
/** Max repos in the pool (LRU eviction) */
|
||||
const MAX_POOL_SIZE = 5;
|
||||
/** Idle timeout before closing a repo's connections */
|
||||
|
|
@ -42,7 +54,7 @@ const INITIAL_CONNS_PER_REPO = 2;
|
|||
|
||||
let idleTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
/** Saved real stdout.write — used to silence KuzuDB native output without race conditions */
|
||||
/** Saved real stdout.write — used to silence LadybugDB native output without race conditions */
|
||||
const realStdoutWrite = process.stdout.write.bind(process.stdout);
|
||||
let stdoutSilenceCount = 0;
|
||||
|
||||
|
|
@ -84,14 +96,21 @@ function evictLRU(): void {
|
|||
}
|
||||
|
||||
/**
|
||||
* Remove a repo from the pool without calling native close methods.
|
||||
* Remove a repo from the pool and release its shared Database ref.
|
||||
*
|
||||
* KuzuDB's native .closeSync() triggers N-API destructor hooks that
|
||||
* LadybugDB's native .closeSync() triggers N-API destructor hooks that
|
||||
* segfault on Linux/macOS. Pool databases are opened read-only, so
|
||||
* there is no WAL to flush — just deleting the pool entry and letting
|
||||
* the GC (or process exit) reclaim native resources is safe.
|
||||
*/
|
||||
function closeOne(repoId: string): void {
|
||||
const entry = pool.get(repoId);
|
||||
if (entry) {
|
||||
const shared = dbCache.get(entry.dbPath);
|
||||
if (shared && shared.refCount > 0) {
|
||||
shared.refCount--;
|
||||
}
|
||||
}
|
||||
pool.delete(repoId);
|
||||
}
|
||||
|
||||
|
|
@ -112,10 +131,10 @@ function restoreStdout(): void {
|
|||
}
|
||||
}
|
||||
|
||||
function createConnection(db: kuzu.Database): kuzu.Connection {
|
||||
function createConnection(db: lbug.Database): lbug.Connection {
|
||||
silenceStdout();
|
||||
try {
|
||||
return new kuzu.Connection(db);
|
||||
return new lbug.Connection(db);
|
||||
} finally {
|
||||
restoreStdout();
|
||||
}
|
||||
|
|
@ -133,7 +152,7 @@ const LOCK_RETRY_DELAY_MS = 2000;
|
|||
* Initialize (or reuse) a Database + connection pool for a specific repo.
|
||||
* Retries on lock errors (e.g., when `gitnexus analyze` is running).
|
||||
*/
|
||||
export const initKuzu = async (repoId: string, dbPath: string): Promise<void> => {
|
||||
export const initLbug = async (repoId: string, dbPath: string): Promise<void> => {
|
||||
const existing = pool.get(repoId);
|
||||
if (existing) {
|
||||
existing.lastUsed = Date.now();
|
||||
|
|
@ -144,49 +163,71 @@ export const initKuzu = async (repoId: string, dbPath: string): Promise<void> =>
|
|||
try {
|
||||
await fs.stat(dbPath);
|
||||
} catch {
|
||||
throw new Error(`KuzuDB not found at ${dbPath}. Run: gitnexus analyze`);
|
||||
throw new Error(`LadybugDB not found at ${dbPath}. Run: gitnexus analyze`);
|
||||
}
|
||||
|
||||
evictLRU();
|
||||
|
||||
// Open in read-only mode — MCP server never writes to the database.
|
||||
// This allows multiple MCP server instances to read concurrently, and
|
||||
// avoids lock conflicts when `gitnexus analyze` is writing.
|
||||
let lastError: Error | null = null;
|
||||
for (let attempt = 1; attempt <= LOCK_RETRY_ATTEMPTS; attempt++) {
|
||||
silenceStdout();
|
||||
try {
|
||||
const db = new kuzu.Database(
|
||||
dbPath,
|
||||
0, // bufferManagerSize (default)
|
||||
false, // enableCompression (default)
|
||||
true, // readOnly
|
||||
);
|
||||
restoreStdout();
|
||||
|
||||
// Pre-create a small pool of connections
|
||||
const available: kuzu.Connection[] = [];
|
||||
for (let i = 0; i < INITIAL_CONNS_PER_REPO; i++) {
|
||||
available.push(createConnection(db));
|
||||
// Reuse an existing native Database if another repoId already opened this path.
|
||||
// This prevents buffer manager exhaustion from multiple mmap regions on the same file.
|
||||
let shared = dbCache.get(dbPath);
|
||||
if (!shared) {
|
||||
// Open in read-only mode — MCP server never writes to the database.
|
||||
// This allows multiple MCP server instances to read concurrently, and
|
||||
// avoids lock conflicts when `gitnexus analyze` is writing.
|
||||
let lastError: Error | null = null;
|
||||
for (let attempt = 1; attempt <= LOCK_RETRY_ATTEMPTS; attempt++) {
|
||||
silenceStdout();
|
||||
try {
|
||||
const db = new lbug.Database(
|
||||
dbPath,
|
||||
0, // bufferManagerSize (default)
|
||||
false, // enableCompression (default)
|
||||
true, // readOnly
|
||||
);
|
||||
restoreStdout();
|
||||
shared = { db, refCount: 0, ftsLoaded: false };
|
||||
dbCache.set(dbPath, shared);
|
||||
break;
|
||||
} catch (err: any) {
|
||||
restoreStdout();
|
||||
lastError = err instanceof Error ? err : new Error(String(err));
|
||||
const isLockError = lastError.message.includes('Could not set lock')
|
||||
|| lastError.message.includes('lock');
|
||||
if (!isLockError || attempt === LOCK_RETRY_ATTEMPTS) break;
|
||||
await new Promise(resolve => setTimeout(resolve, LOCK_RETRY_DELAY_MS * attempt));
|
||||
}
|
||||
}
|
||||
|
||||
pool.set(repoId, { db, available, checkedOut: 0, waiters: [], lastUsed: Date.now(), dbPath });
|
||||
ensureIdleTimer();
|
||||
return;
|
||||
} catch (err: any) {
|
||||
restoreStdout();
|
||||
lastError = err instanceof Error ? err : new Error(String(err));
|
||||
const isLockError = lastError.message.includes('Could not set lock')
|
||||
|| lastError.message.includes('lock');
|
||||
if (!isLockError || attempt === LOCK_RETRY_ATTEMPTS) break;
|
||||
await new Promise(resolve => setTimeout(resolve, LOCK_RETRY_DELAY_MS * attempt));
|
||||
if (!shared) {
|
||||
throw new Error(
|
||||
`LadybugDB unavailable for ${repoId}. Another process may be rebuilding the index. ` +
|
||||
`Retry later. (${lastError?.message || 'unknown error'})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`KuzuDB unavailable for ${repoId}. Another process may be rebuilding the index. ` +
|
||||
`Retry later. (${lastError?.message || 'unknown error'})`
|
||||
);
|
||||
shared.refCount++;
|
||||
const db = shared.db;
|
||||
|
||||
// Pre-create a small pool of connections
|
||||
const available: lbug.Connection[] = [];
|
||||
for (let i = 0; i < INITIAL_CONNS_PER_REPO; i++) {
|
||||
available.push(createConnection(db));
|
||||
}
|
||||
|
||||
pool.set(repoId, { db, available, checkedOut: 0, waiters: [], lastUsed: Date.now(), dbPath });
|
||||
ensureIdleTimer();
|
||||
|
||||
// Load FTS extension once per shared Database
|
||||
if (!shared.ftsLoaded) {
|
||||
try {
|
||||
await available[0].query('LOAD EXTENSION fts');
|
||||
shared.ftsLoaded = true;
|
||||
} catch {
|
||||
// Extension may not be installed — FTS queries will fail gracefully
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -194,7 +235,7 @@ export const initKuzu = async (repoId: string, dbPath: string): Promise<void> =>
|
|||
* Returns an available connection, or creates a new one if under the cap.
|
||||
* If all connections are busy and at cap, queues the caller until one is returned.
|
||||
*/
|
||||
function checkout(entry: PoolEntry): Promise<kuzu.Connection> {
|
||||
function checkout(entry: PoolEntry): Promise<lbug.Connection> {
|
||||
// Fast path: grab an available connection
|
||||
if (entry.available.length > 0) {
|
||||
entry.checkedOut++;
|
||||
|
|
@ -209,8 +250,8 @@ function checkout(entry: PoolEntry): Promise<kuzu.Connection> {
|
|||
}
|
||||
|
||||
// At capacity — queue the caller with a timeout.
|
||||
return new Promise<kuzu.Connection>((resolve, reject) => {
|
||||
const waiter = (conn: kuzu.Connection) => {
|
||||
return new Promise<lbug.Connection>((resolve, reject) => {
|
||||
const waiter = (conn: lbug.Connection) => {
|
||||
clearTimeout(timer);
|
||||
resolve(conn);
|
||||
};
|
||||
|
|
@ -228,7 +269,7 @@ function checkout(entry: PoolEntry): Promise<kuzu.Connection> {
|
|||
* If there are queued waiters, hand the connection directly to the next one
|
||||
* instead of putting it back in the available array (avoids race conditions).
|
||||
*/
|
||||
function checkin(entry: PoolEntry, conn: kuzu.Connection): void {
|
||||
function checkin(entry: PoolEntry, conn: lbug.Connection): void {
|
||||
if (entry.waiters.length > 0) {
|
||||
// Hand directly to the next waiter — no intermediate available state
|
||||
const waiter = entry.waiters.shift()!;
|
||||
|
|
@ -255,7 +296,7 @@ function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise
|
|||
export const executeQuery = async (repoId: string, cypher: string): Promise<any[]> => {
|
||||
const entry = pool.get(repoId);
|
||||
if (!entry) {
|
||||
throw new Error(`KuzuDB not initialized for repo "${repoId}". Call initKuzu first.`);
|
||||
throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
|
||||
}
|
||||
|
||||
entry.lastUsed = Date.now();
|
||||
|
|
@ -282,7 +323,7 @@ export const executeParameterized = async (
|
|||
): Promise<any[]> => {
|
||||
const entry = pool.get(repoId);
|
||||
if (!entry) {
|
||||
throw new Error(`KuzuDB not initialized for repo "${repoId}". Call initKuzu first.`);
|
||||
throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
|
||||
}
|
||||
|
||||
entry.lastUsed = Date.now();
|
||||
|
|
@ -308,7 +349,7 @@ export const executeParameterized = async (
|
|||
* If repoId is provided, close only that repo's connections.
|
||||
* If omitted, close all repos.
|
||||
*/
|
||||
export const closeKuzu = async (repoId?: string): Promise<void> => {
|
||||
export const closeLbug = async (repoId?: string): Promise<void> => {
|
||||
if (repoId) {
|
||||
closeOne(repoId);
|
||||
return;
|
||||
|
|
@ -328,4 +369,4 @@ export const closeKuzu = async (repoId?: string): Promise<void> => {
|
|||
/**
|
||||
* Check if a specific repo's pool is active
|
||||
*/
|
||||
export const isKuzuReady = (repoId: string): boolean => pool.has(repoId);
|
||||
export const isLbugReady = (repoId: string): boolean => pool.has(repoId);
|
||||
|
|
@ -3,18 +3,19 @@
|
|||
*
|
||||
* Provides tool implementations using local .gitnexus/ indexes.
|
||||
* Supports multiple indexed repositories via a global registry.
|
||||
* KuzuDB connections are opened lazily per repo on first query.
|
||||
* LadybugDB connections are opened lazily per repo on first query.
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { initKuzu, executeQuery, executeParameterized, closeKuzu, isKuzuReady } from '../core/kuzu-adapter.js';
|
||||
import { initLbug, executeQuery, executeParameterized, closeLbug, isLbugReady } from '../core/lbug-adapter.js';
|
||||
// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
|
||||
// at MCP server startup — crashes on unsupported Node ABI versions (#89)
|
||||
// git utilities available if needed
|
||||
// import { isGitRepo, getCurrentCommit, getGitRoot } from '../../storage/git.js';
|
||||
import {
|
||||
listRegisteredRepos,
|
||||
cleanupOldKuzuFiles,
|
||||
type RegistryEntry,
|
||||
} from '../../storage/repo-manager.js';
|
||||
// AI context generation is CLI-only (gitnexus analyze)
|
||||
|
|
@ -37,7 +38,7 @@ export function isTestFilePath(filePath: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
/** Valid KuzuDB node labels for safe Cypher query construction */
|
||||
/** Valid LadybugDB node labels for safe Cypher query construction */
|
||||
export const VALID_NODE_LABELS = new Set([
|
||||
'File', 'Folder', 'Function', 'Class', 'Interface', 'Method', 'CodeElement',
|
||||
'Community', 'Process', 'Struct', 'Enum', 'Macro', 'Typedef', 'Union',
|
||||
|
|
@ -77,7 +78,7 @@ interface RepoHandle {
|
|||
name: string;
|
||||
repoPath: string;
|
||||
storagePath: string;
|
||||
kuzuPath: string;
|
||||
lbugPath: string;
|
||||
indexedAt: string;
|
||||
lastCommit: string;
|
||||
stats?: RegistryEntry['stats'];
|
||||
|
|
@ -102,7 +103,7 @@ export class LocalBackend {
|
|||
/**
|
||||
* Re-read the global registry and update the in-memory repo map.
|
||||
* New repos are added, existing repos are updated, removed repos are pruned.
|
||||
* KuzuDB connections for removed repos are NOT closed (they idle-timeout naturally).
|
||||
* LadybugDB connections for removed repos are NOT closed (they idle-timeout naturally).
|
||||
*/
|
||||
private async refreshRepos(): Promise<void> {
|
||||
const entries = await listRegisteredRepos({ validate: true });
|
||||
|
|
@ -113,14 +114,21 @@ export class LocalBackend {
|
|||
freshIds.add(id);
|
||||
|
||||
const storagePath = entry.storagePath;
|
||||
const kuzuPath = path.join(storagePath, 'kuzu');
|
||||
const lbugPath = path.join(storagePath, 'lbug');
|
||||
|
||||
// Clean up any leftover KuzuDB files from before the LadybugDB migration.
|
||||
// If kuzu exists but lbug doesn't, warn so the user knows to re-analyze.
|
||||
const kuzu = await cleanupOldKuzuFiles(storagePath);
|
||||
if (kuzu.found && kuzu.needsReindex) {
|
||||
console.error(`GitNexus: "${entry.name}" has a stale KuzuDB index. Run: gitnexus analyze ${entry.path}`);
|
||||
}
|
||||
|
||||
const handle: RepoHandle = {
|
||||
id,
|
||||
name: entry.name,
|
||||
repoPath: entry.path,
|
||||
storagePath,
|
||||
kuzuPath,
|
||||
lbugPath,
|
||||
indexedAt: entry.indexedAt,
|
||||
lastCommit: entry.lastCommit,
|
||||
stats: entry.stats,
|
||||
|
|
@ -128,7 +136,7 @@ export class LocalBackend {
|
|||
|
||||
this.repos.set(id, handle);
|
||||
|
||||
// Build lightweight context (no KuzuDB needed)
|
||||
// Build lightweight context (no LadybugDB needed)
|
||||
const s = entry.stats || {};
|
||||
this.contextCache.set(id, {
|
||||
projectName: entry.name,
|
||||
|
|
@ -235,17 +243,17 @@ export class LocalBackend {
|
|||
return null; // Multiple repos, no param — ambiguous
|
||||
}
|
||||
|
||||
// ─── Lazy KuzuDB Init ────────────────────────────────────────────
|
||||
// ─── Lazy LadybugDB Init ────────────────────────────────────────────
|
||||
|
||||
private async ensureInitialized(repoId: string): Promise<void> {
|
||||
// Always check the actual pool — the idle timer may have evicted the connection
|
||||
if (this.initializedRepos.has(repoId) && isKuzuReady(repoId)) return;
|
||||
if (this.initializedRepos.has(repoId) && isLbugReady(repoId)) return;
|
||||
|
||||
const handle = this.repos.get(repoId);
|
||||
if (!handle) throw new Error(`Unknown repo: ${repoId}`);
|
||||
|
||||
try {
|
||||
await initKuzu(repoId, handle.kuzuPath);
|
||||
await initLbug(repoId, handle.lbugPath);
|
||||
this.initializedRepos.add(repoId);
|
||||
} catch (err: any) {
|
||||
// If lock error, mark as not initialized so next call retries
|
||||
|
|
@ -534,13 +542,13 @@ export class LocalBackend {
|
|||
}
|
||||
|
||||
/**
|
||||
* BM25 keyword search helper - uses KuzuDB FTS for always-fresh results
|
||||
* BM25 keyword search helper - uses LadybugDB FTS for always-fresh results
|
||||
*/
|
||||
private async bm25Search(repo: RepoHandle, query: string, limit: number): Promise<any[]> {
|
||||
const { searchFTSFromKuzu } = await import('../../core/search/bm25-index.js');
|
||||
const { searchFTSFromLbug } = await import('../../core/search/bm25-index.js');
|
||||
let bm25Results;
|
||||
try {
|
||||
bm25Results = await searchFTSFromKuzu(query, limit, repo.id);
|
||||
bm25Results = await searchFTSFromLbug(query, limit, repo.id);
|
||||
} catch (err: any) {
|
||||
console.error('GitNexus: BM25/FTS search failed (FTS indexes may not exist) -', err.message);
|
||||
return [];
|
||||
|
|
@ -669,8 +677,8 @@ export class LocalBackend {
|
|||
private async cypher(repo: RepoHandle, params: { query: string }): Promise<any> {
|
||||
await this.ensureInitialized(repo.id);
|
||||
|
||||
if (!isKuzuReady(repo.id)) {
|
||||
return { error: 'KuzuDB not ready. Index may be corrupted.' };
|
||||
if (!isLbugReady(repo.id)) {
|
||||
return { error: 'LadybugDB not ready. Index may be corrupted.' };
|
||||
}
|
||||
|
||||
// Block write operations (defense-in-depth — DB is already read-only)
|
||||
|
|
@ -719,7 +727,7 @@ export class LocalBackend {
|
|||
/**
|
||||
* Aggregate same-named clusters: group by heuristicLabel, sum symbols,
|
||||
* weighted-average cohesion, filter out tiny clusters (<5 symbols).
|
||||
* Raw communities stay intact in KuzuDB for Cypher queries.
|
||||
* Raw communities stay intact in LadybugDB for Cypher queries.
|
||||
*/
|
||||
private aggregateClusters(clusters: any[]): any[] {
|
||||
const groups = new Map<string, { ids: string[]; totalSymbols: number; weightedCohesion: number; largest: any }>();
|
||||
|
|
@ -1622,7 +1630,7 @@ export class LocalBackend {
|
|||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
await closeKuzu(); // close all connections
|
||||
await closeLbug(); // close all connections
|
||||
// Note: we intentionally do NOT call disposeEmbedder() here.
|
||||
// ONNX Runtime's native cleanup segfaults on macOS and some Linux configs,
|
||||
// and importing the embedder module on Node v24+ crashes if onnxruntime
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ import cors from 'cors';
|
|||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import { loadMeta, listRegisteredRepos } from '../storage/repo-manager.js';
|
||||
import { executeQuery, closeKuzu, withKuzuDb } from '../core/kuzu/kuzu-adapter.js';
|
||||
import { NODE_TABLES } from '../core/kuzu/schema.js';
|
||||
import { executeQuery, closeLbug, withLbugDb } from '../core/lbug/lbug-adapter.js';
|
||||
import { NODE_TABLES } from '../core/lbug/schema.js';
|
||||
import { GraphNode, GraphRelationship } from '../core/graph/types.js';
|
||||
import { searchFTSFromKuzu } from '../core/search/bm25-index.js';
|
||||
import { searchFTSFromLbug } from '../core/search/bm25-index.js';
|
||||
import { hybridSearch } from '../core/search/hybrid-search.js';
|
||||
// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
|
||||
// at server startup — crashes on unsupported Node ABI versions (#89)
|
||||
|
|
@ -179,8 +179,8 @@ export const createServer = async (port: number, host: string = '127.0.0.1') =>
|
|||
res.status(404).json({ error: 'Repository not found' });
|
||||
return;
|
||||
}
|
||||
const kuzuPath = path.join(entry.storagePath, 'kuzu');
|
||||
const graph = await withKuzuDb(kuzuPath, async () => buildGraph());
|
||||
const lbugPath = path.join(entry.storagePath, 'lbug');
|
||||
const graph = await withLbugDb(lbugPath, async () => buildGraph());
|
||||
res.json(graph);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message || 'Failed to build graph' });
|
||||
|
|
@ -201,8 +201,8 @@ export const createServer = async (port: number, host: string = '127.0.0.1') =>
|
|||
res.status(404).json({ error: 'Repository not found' });
|
||||
return;
|
||||
}
|
||||
const kuzuPath = path.join(entry.storagePath, 'kuzu');
|
||||
const result = await withKuzuDb(kuzuPath, () => executeQuery(cypher));
|
||||
const lbugPath = path.join(entry.storagePath, 'lbug');
|
||||
const result = await withLbugDb(lbugPath, () => executeQuery(cypher));
|
||||
res.json({ result });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message || 'Query failed' });
|
||||
|
|
@ -223,20 +223,20 @@ export const createServer = async (port: number, host: string = '127.0.0.1') =>
|
|||
res.status(404).json({ error: 'Repository not found' });
|
||||
return;
|
||||
}
|
||||
const kuzuPath = path.join(entry.storagePath, 'kuzu');
|
||||
const lbugPath = path.join(entry.storagePath, 'lbug');
|
||||
const parsedLimit = Number(req.body.limit ?? 10);
|
||||
const limit = Number.isFinite(parsedLimit)
|
||||
? Math.max(1, Math.min(100, Math.trunc(parsedLimit)))
|
||||
: 10;
|
||||
|
||||
const results = await withKuzuDb(kuzuPath, async () => {
|
||||
const results = await withLbugDb(lbugPath, async () => {
|
||||
const { isEmbedderReady } = await import('../core/embeddings/embedder.js');
|
||||
if (isEmbedderReady()) {
|
||||
const { semanticSearch } = await import('../core/embeddings/embedding-pipeline.js');
|
||||
return hybridSearch(query, limit, executeQuery, semanticSearch);
|
||||
}
|
||||
// FTS-only fallback when embeddings aren't loaded
|
||||
return searchFTSFromKuzu(query, limit);
|
||||
return searchFTSFromLbug(query, limit);
|
||||
});
|
||||
res.json({ results });
|
||||
} catch (err: any) {
|
||||
|
|
@ -347,11 +347,11 @@ export const createServer = async (port: number, host: string = '127.0.0.1') =>
|
|||
console.log(`GitNexus server running on http://${host}:${port}`);
|
||||
});
|
||||
|
||||
// Graceful shutdown — close Express + KuzuDB cleanly
|
||||
// Graceful shutdown — close Express + LadybugDB cleanly
|
||||
const shutdown = async () => {
|
||||
server.close();
|
||||
await cleanupMcp();
|
||||
await closeKuzu();
|
||||
await closeLbug();
|
||||
await backend.disconnect();
|
||||
process.exit(0);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* Mounts the GitNexus MCP server on Express using StreamableHTTP transport.
|
||||
* Each connecting client gets its own stateful session; the LocalBackend
|
||||
* is shared across all sessions (thread-safe — lazy KuzuDB per repo).
|
||||
* is shared across all sessions (thread-safe — lazy LadybugDB per repo).
|
||||
*
|
||||
* Sessions are cleaned up on explicit close or after SESSION_TTL_MS of inactivity
|
||||
* (guards against network drops that never trigger onclose).
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export interface RepoMeta {
|
|||
export interface IndexedRepo {
|
||||
repoPath: string;
|
||||
storagePath: string;
|
||||
kuzuPath: string;
|
||||
lbugPath: string;
|
||||
metaPath: string;
|
||||
meta: RepoMeta;
|
||||
}
|
||||
|
|
@ -62,11 +62,60 @@ export const getStoragePaths = (repoPath: string) => {
|
|||
const storagePath = getStoragePath(repoPath);
|
||||
return {
|
||||
storagePath,
|
||||
kuzuPath: path.join(storagePath, 'kuzu'),
|
||||
lbugPath: path.join(storagePath, 'lbug'),
|
||||
metaPath: path.join(storagePath, 'meta.json'),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Check whether a KuzuDB index exists in the given storage path.
|
||||
* Non-destructive — safe to call from status commands.
|
||||
*/
|
||||
export const hasKuzuIndex = async (storagePath: string): Promise<boolean> => {
|
||||
try {
|
||||
await fs.stat(path.join(storagePath, 'kuzu'));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean up stale KuzuDB files after migration to LadybugDB.
|
||||
*
|
||||
* Returns:
|
||||
* found — true if .gitnexus/kuzu existed and was deleted
|
||||
* needsReindex — true if kuzu existed but lbug does not (re-analyze required)
|
||||
*
|
||||
* Callers own the user-facing messaging; this function only deletes files.
|
||||
*/
|
||||
export const cleanupOldKuzuFiles = async (
|
||||
storagePath: string,
|
||||
): Promise<{ found: boolean; needsReindex: boolean }> => {
|
||||
const oldPath = path.join(storagePath, 'kuzu');
|
||||
const newPath = path.join(storagePath, 'lbug');
|
||||
try {
|
||||
await fs.stat(oldPath);
|
||||
// Old kuzu file/dir exists — determine if lbug is already present
|
||||
let needsReindex = false;
|
||||
try {
|
||||
await fs.stat(newPath);
|
||||
} catch {
|
||||
needsReindex = true;
|
||||
}
|
||||
// Delete kuzu database file and its sidecars (.wal, .lock)
|
||||
for (const suffix of ['', '.wal', '.lock']) {
|
||||
try { await fs.unlink(oldPath + suffix); } catch {}
|
||||
}
|
||||
// Also handle the case where kuzu was stored as a directory
|
||||
try { await fs.rm(oldPath, { recursive: true, force: true }); } catch {}
|
||||
return { found: true, needsReindex };
|
||||
} catch {
|
||||
// Old path doesn't exist — nothing to do
|
||||
return { found: false, needsReindex: false };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Load metadata from an indexed repo
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export interface PipelineProgress {
|
|||
// Original result type (used internally in pipeline)
|
||||
export interface PipelineResult {
|
||||
graph: KnowledgeGraph;
|
||||
/** Absolute path to the repo root — used for lazy file reads during KuzuDB loading */
|
||||
/** Absolute path to the repo root — used for lazy file reads during LadybugDB loading */
|
||||
repoPath: string;
|
||||
/** Total files scanned (for stats) */
|
||||
totalFileCount: number;
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
/**
|
||||
* Vitest globalSetup — runs once in the MAIN process before any forks.
|
||||
*
|
||||
* Creates a single shared KuzuDB with full schema so that forked test
|
||||
* Creates a single shared LadybugDB with full schema so that forked test
|
||||
* files only need to clear + reseed data instead of recreating the
|
||||
* entire schema each time (~29 DDL queries per file eliminated).
|
||||
*
|
||||
* The dbPath is shared with test files via vitest's provide/inject API.
|
||||
*/
|
||||
import path from 'path';
|
||||
import kuzu from 'kuzu';
|
||||
import lbug from '@ladybugdb/core';
|
||||
import type { GlobalSetupContext } from 'vitest/node';
|
||||
import { createTempDir } from './helpers/test-db.js';
|
||||
import {
|
||||
NODE_SCHEMA_QUERIES,
|
||||
REL_SCHEMA_QUERIES,
|
||||
EMBEDDING_SCHEMA,
|
||||
} from '../src/core/kuzu/schema.js';
|
||||
} from '../src/core/lbug/schema.js';
|
||||
|
||||
export default async function setup({ provide }: GlobalSetupContext) {
|
||||
const tmpHandle = await createTempDir('gitnexus-shared-');
|
||||
const dbPath = path.join(tmpHandle.dbPath, 'kuzu');
|
||||
const dbPath = path.join(tmpHandle.dbPath, 'lbug');
|
||||
|
||||
// Create DB with full schema
|
||||
const db = new kuzu.Database(dbPath);
|
||||
const conn = new kuzu.Connection(db);
|
||||
const db = new lbug.Database(dbPath);
|
||||
const conn = new lbug.Connection(db);
|
||||
|
||||
for (const q of NODE_SCHEMA_QUERIES) {
|
||||
await conn.query(q);
|
||||
|
|
@ -50,8 +50,8 @@ export default async function setup({ provide }: GlobalSetupContext) {
|
|||
db.close();
|
||||
}
|
||||
|
||||
// Share the dbPath with all test files via inject('kuzuDbPath')
|
||||
provide('kuzuDbPath', dbPath);
|
||||
// Share the dbPath with all test files via inject('lbugDbPath')
|
||||
provide('lbugDbPath', dbPath);
|
||||
|
||||
// Teardown: remove temp directory after all tests complete
|
||||
return async () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Test helper: Temporary KuzuDB factory
|
||||
* Test helper: Temporary LadybugDB factory
|
||||
*
|
||||
* Creates a temp directory, initializes KuzuDB with schema, and
|
||||
* Creates a temp directory, initializes LadybugDB with schema, and
|
||||
* optionally loads minimal test data. Returns a cleanup function.
|
||||
*/
|
||||
import fs from 'fs/promises';
|
||||
|
|
@ -14,7 +14,7 @@ export interface TestDBHandle {
|
|||
}
|
||||
|
||||
/**
|
||||
* Create a temporary directory for KuzuDB tests.
|
||||
* Create a temporary directory for LadybugDB tests.
|
||||
* Returns the path and a cleanup function.
|
||||
*/
|
||||
export async function createTempDir(prefix: string = 'gitnexus-test-'): Promise<TestDBHandle> {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Test helper: In-memory knowledge graph builder
|
||||
*
|
||||
* Provides a convenient API for constructing test graphs
|
||||
* without touching the filesystem or KuzuDB.
|
||||
* without touching the filesystem or LadybugDB.
|
||||
*/
|
||||
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
|
||||
import type { KnowledgeGraph, GraphNode, NodeLabel, RelationshipType } from '../../src/core/graph/types.js';
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
/**
|
||||
* Test helper: Indexed KuzuDB lifecycle manager
|
||||
* Test helper: Indexed LadybugDB lifecycle manager
|
||||
*
|
||||
* Uses a shared KuzuDB created by globalSetup (test/global-setup.ts).
|
||||
* Uses a shared LadybugDB created by globalSetup (test/global-setup.ts).
|
||||
* Each test file clears all data, reseeds, and initializes adapters —
|
||||
* avoiding per-file schema creation overhead.
|
||||
*
|
||||
* Cleanup is intentionally a no-op: CI runs each KuzuDB test file in its
|
||||
* Cleanup is intentionally a no-op: CI runs each LadybugDB test file in its
|
||||
* own vitest process, so the OS reclaims all native resources on exit.
|
||||
*
|
||||
* Each test file gets a unique repoId to prevent MCP pool map collisions.
|
||||
|
|
@ -18,10 +18,10 @@ import type { TestDBHandle } from './test-db.js';
|
|||
import {
|
||||
NODE_TABLES,
|
||||
EMBEDDING_TABLE_NAME,
|
||||
} from '../../src/core/kuzu/schema.js';
|
||||
} from '../../src/core/lbug/schema.js';
|
||||
|
||||
export interface IndexedDBHandle {
|
||||
/** Path to the KuzuDB database file */
|
||||
/** Path to the LadybugDB database file */
|
||||
dbPath: string;
|
||||
/** Unique repoId for MCP pool adapter — prevents cross-file collisions */
|
||||
repoId: string;
|
||||
|
|
@ -33,7 +33,7 @@ export interface IndexedDBHandle {
|
|||
|
||||
let repoCounter = 0;
|
||||
|
||||
/** FTS index definition for withTestKuzuDB */
|
||||
/** FTS index definition for withTestLbugDB */
|
||||
export interface FTSIndexDef {
|
||||
table: string;
|
||||
indexName: string;
|
||||
|
|
@ -41,12 +41,12 @@ export interface FTSIndexDef {
|
|||
}
|
||||
|
||||
/**
|
||||
* Options for withTestKuzuDB lifecycle.
|
||||
* Options for withTestLbugDB lifecycle.
|
||||
*
|
||||
* Lifecycle: initKuzu → loadFTS → dropFTS → clearData → seed
|
||||
* → createFTS → [closeCoreKuzu + poolInitKuzu] → afterSetup
|
||||
* Lifecycle: initLbug → loadFTS → dropFTS → clearData → seed
|
||||
* → createFTS → [closeCoreLbug + poolInitLbug] → afterSetup
|
||||
*/
|
||||
export interface WithTestKuzuDBOptions {
|
||||
export interface WithTestLbugDBOptions {
|
||||
/** Cypher CREATE queries to insert seed data (runs before core adapter opens). */
|
||||
seed?: string[];
|
||||
/** FTS indexes to create after seeding. */
|
||||
|
|
@ -60,34 +60,34 @@ export interface WithTestKuzuDBOptions {
|
|||
}
|
||||
|
||||
/**
|
||||
* Manages the full KuzuDB test lifecycle using the shared global DB:
|
||||
* Manages the full LadybugDB test lifecycle using the shared global DB:
|
||||
* data clearing, reseeding, FTS indexes, adapter init/teardown.
|
||||
*
|
||||
* All data operations go through the core adapter's writable connection —
|
||||
* no raw kuzu.Database() connections are opened. This avoids file-lock
|
||||
* no raw lbug.Database() connections are opened. This avoids file-lock
|
||||
* conflicts with orphaned native objects from previous test files.
|
||||
*
|
||||
* Each call is wrapped in its own `describe` block to isolate lifecycle
|
||||
* hooks — safe to call multiple times in the same file.
|
||||
*/
|
||||
export function withTestKuzuDB(
|
||||
export function withTestLbugDB(
|
||||
prefix: string,
|
||||
fn: (handle: IndexedDBHandle) => void,
|
||||
options?: WithTestKuzuDBOptions,
|
||||
options?: WithTestLbugDBOptions,
|
||||
): void {
|
||||
const ref: { handle: IndexedDBHandle | undefined } = { handle: undefined };
|
||||
const timeout = options?.timeout ?? 30000;
|
||||
|
||||
const setup = async () => {
|
||||
// Get shared DB path from globalSetup (created once with full schema)
|
||||
const dbPath = inject<'kuzuDbPath'>('kuzuDbPath');
|
||||
const dbPath = inject<'lbugDbPath'>('lbugDbPath');
|
||||
const repoId = `test-${prefix}-${Date.now()}-${repoCounter++}`;
|
||||
|
||||
const adapter = await import('../../src/core/kuzu/kuzu-adapter.js');
|
||||
const adapter = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
|
||||
// 1. Init core adapter (writable) — reuses existing connection if
|
||||
// already open for this dbPath (no new native objects created).
|
||||
await adapter.initKuzu(dbPath);
|
||||
await adapter.initLbug(dbPath);
|
||||
|
||||
// 2. Load FTS extension (idempotent — skips if already loaded)
|
||||
await adapter.loadFTSExtension();
|
||||
|
|
@ -120,26 +120,26 @@ export function withTestKuzuDB(
|
|||
}
|
||||
|
||||
// 7. Close core adapter (Windows only), then open pool adapter (read-only).
|
||||
// On Windows, KuzuDB enforces file locks — writable + read-only
|
||||
// On Windows, LadybugDB enforces file locks — writable + read-only
|
||||
// can't coexist on the same path, so we must close the core first.
|
||||
// On Linux/macOS, .close() deadlocks or segfaults via N-API
|
||||
// destructor hooks, but concurrent Database instances on the same
|
||||
// path are allowed, so we skip the close entirely.
|
||||
if (options?.poolAdapter) {
|
||||
if (process.platform === 'win32') {
|
||||
await adapter.closeKuzu();
|
||||
await adapter.closeLbug();
|
||||
}
|
||||
const { initKuzu: poolInitKuzu } = await import('../../src/mcp/core/kuzu-adapter.js');
|
||||
await poolInitKuzu(repoId, dbPath);
|
||||
const { initLbug: poolInitLbug } = await import('../../src/mcp/core/lbug-adapter.js');
|
||||
await poolInitLbug(repoId, dbPath);
|
||||
}
|
||||
|
||||
// Cleanup: intentionally a no-op. We do NOT call detachKuzu() here
|
||||
// because .closeSync() segfaults on Linux (KuzuDB N-API destructor bug).
|
||||
// CI runs each KuzuDB test file in its own vitest process, so the OS
|
||||
// Cleanup: intentionally a no-op. We do NOT call detachLbug() here
|
||||
// because .closeSync() segfaults on Linux (LadybugDB N-API destructor bug).
|
||||
// CI runs each LadybugDB test file in its own vitest process, so the OS
|
||||
// reclaims all native resources on process exit — no explicit cleanup needed.
|
||||
const cleanup = async () => {};
|
||||
|
||||
// tmpHandle.dbPath → parent temp dir (not the kuzu file) so tests
|
||||
// tmpHandle.dbPath → parent temp dir (not the lbug file) so tests
|
||||
// that create sibling directories (e.g. 'storage') still work.
|
||||
const tmpDir = path.dirname(dbPath);
|
||||
const tmpHandle: TestDBHandle = { dbPath: tmpDir, cleanup: async () => {} };
|
||||
|
|
@ -153,14 +153,14 @@ export function withTestKuzuDB(
|
|||
|
||||
const lazyHandle = new Proxy({} as IndexedDBHandle, {
|
||||
get(_target, prop) {
|
||||
if (!ref.handle) throw new Error('withTestKuzuDB: handle not initialized — beforeAll has not run yet');
|
||||
if (!ref.handle) throw new Error('withTestLbugDB: handle not initialized — beforeAll has not run yet');
|
||||
return (ref.handle as any)[prop];
|
||||
},
|
||||
});
|
||||
|
||||
// Wrap in describe to scope beforeAll/afterAll — prevents lifecycle
|
||||
// collisions when multiple withTestKuzuDB calls share the same file.
|
||||
describe(`withTestKuzuDB(${prefix})`, () => {
|
||||
// collisions when multiple withTestLbugDB calls share the same file.
|
||||
describe(`withTestLbugDB(${prefix})`, () => {
|
||||
beforeAll(setup, timeout);
|
||||
afterAll(async () => { if (ref.handle) await ref.handle.cleanup(); });
|
||||
fn(lazyHandle);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
/**
|
||||
* Integration Tests: Augmentation Engine
|
||||
*
|
||||
* augment() against a real indexed KuzuDB
|
||||
* augment() against a real indexed LadybugDB
|
||||
* - Matching pattern returns non-empty string with callers/callees
|
||||
* - Non-matching pattern returns empty string
|
||||
* - Pattern shorter than 3 chars returns empty string
|
||||
*/
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { withTestKuzuDB } from '../helpers/test-indexed-db.js';
|
||||
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
|
||||
|
||||
// ─── Seed data & FTS indexes for augmentation ────────
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ vi.mock('../../src/storage/repo-manager.js', () => ({
|
|||
|
||||
let augment: (pattern: string, cwd?: string) => Promise<string>;
|
||||
|
||||
withTestKuzuDB('augment', (handle) => {
|
||||
withTestLbugDB('augment', (handle) => {
|
||||
describe('augment()', () => {
|
||||
it('returns non-empty string with relationship info for a matching pattern', async () => {
|
||||
const result = await augment('login', handle.dbPath);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import fs from 'fs/promises';
|
|||
import path from 'path';
|
||||
import { createTempDir, type TestDBHandle } from '../helpers/test-db.js';
|
||||
import { buildTestGraph } from '../helpers/test-graph.js';
|
||||
import { streamAllCSVsToDisk } from '../../src/core/kuzu/csv-generator.js';
|
||||
import { streamAllCSVsToDisk } from '../../src/core/lbug/csv-generator.js';
|
||||
|
||||
let tmpHandle: TestDBHandle;
|
||||
let csvDir: string;
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
/**
|
||||
* P0 Integration Tests: Core KuzuDB Adapter
|
||||
* P0 Integration Tests: Core LadybugDB Adapter
|
||||
*
|
||||
* Tests: loadGraphToKuzu CSV round-trip, createFTSIndex, getKuzuStats.
|
||||
* Tests: loadGraphToLbug CSV round-trip, createFTSIndex, getLbugStats.
|
||||
*
|
||||
* IMPORTANT: All core adapter tests share ONE coreHandle and ONE coreInitKuzu
|
||||
* IMPORTANT: All core adapter tests share ONE coreHandle and ONE coreInitLbug
|
||||
* call because the core adapter is a module-level singleton. Calling
|
||||
* coreInitKuzu with a different path would close the previous native DB
|
||||
* coreInitLbug with a different path would close the previous native DB
|
||||
* handle, which segfaults in forked processes. Sharing a single handle
|
||||
* avoids this entirely.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { withTestKuzuDB } from '../helpers/test-indexed-db.js';
|
||||
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
|
||||
|
||||
// ─── Core KuzuDB Adapter ─────────────────────────────────────────────
|
||||
// ─── Core LadybugDB Adapter ─────────────────────────────────────────────
|
||||
|
||||
withTestKuzuDB('core-adapter', (handle) => {
|
||||
withTestLbugDB('core-adapter', (handle) => {
|
||||
describe('core adapter', () => {
|
||||
it('loadGraphToKuzu: loads a minimal graph and node counts match', async () => {
|
||||
const { executeQuery: coreExecuteQuery } = await import('../../src/core/kuzu/kuzu-adapter.js');
|
||||
it('loadGraphToLbug: loads a minimal graph and node counts match', async () => {
|
||||
const { executeQuery: coreExecuteQuery } = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
|
||||
// createMinimalTestGraph has 2 File, 2 Function, 1 Class, 1 Folder = 6 nodes
|
||||
const fileRows = await coreExecuteQuery('MATCH (n:File) RETURN n.id AS id');
|
||||
|
|
@ -36,17 +36,17 @@ withTestKuzuDB('core-adapter', (handle) => {
|
|||
});
|
||||
|
||||
it('createFTSIndex: creates FTS index on Function table without error', async () => {
|
||||
const { createFTSIndex } = await import('../../src/core/kuzu/kuzu-adapter.js');
|
||||
const { createFTSIndex } = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
|
||||
await expect(
|
||||
createFTSIndex('Function', 'function_fts', ['name', 'content']),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('getKuzuStats: returns correct node and edge counts for seeded data', async () => {
|
||||
const { getKuzuStats } = await import('../../src/core/kuzu/kuzu-adapter.js');
|
||||
it('getLbugStats: returns correct node and edge counts for seeded data', async () => {
|
||||
const { getLbugStats } = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
|
||||
const stats = await getKuzuStats();
|
||||
const stats = await getLbugStats();
|
||||
|
||||
// createMinimalTestGraph: 6 nodes (2 File, 2 Function, 1 Class, 1 Folder)
|
||||
expect(stats.nodes).toBe(6);
|
||||
|
|
@ -57,14 +57,14 @@ withTestKuzuDB('core-adapter', (handle) => {
|
|||
|
||||
describe('unhappy path', () => {
|
||||
it('throws on malformed Cypher query', async () => {
|
||||
const { executeQuery } = await import('../../src/core/kuzu/kuzu-adapter.js');
|
||||
const { executeQuery } = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
|
||||
// Deliberately broken syntax: MATCH without a pattern clause
|
||||
await expect(executeQuery('MATCH RETURN 1')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('returns empty results for query matching no nodes', async () => {
|
||||
const { executeQuery } = await import('../../src/core/kuzu/kuzu-adapter.js');
|
||||
const { executeQuery } = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
|
||||
// Valid Cypher, but the id will never exist in the seeded graph
|
||||
const rows = await executeQuery(
|
||||
|
|
@ -74,9 +74,9 @@ withTestKuzuDB('core-adapter', (handle) => {
|
|||
});
|
||||
|
||||
it('handles query with non-existent table/node label', async () => {
|
||||
const { executeQuery } = await import('../../src/core/kuzu/kuzu-adapter.js');
|
||||
const { executeQuery } = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
|
||||
// KuzuDB throws when the node table does not exist in the schema
|
||||
// LadybugDB throws when the node table does not exist in the schema
|
||||
await expect(
|
||||
executeQuery('MATCH (n:GhostTable) RETURN n'),
|
||||
).rejects.toThrow();
|
||||
|
|
@ -85,7 +85,7 @@ withTestKuzuDB('core-adapter', (handle) => {
|
|||
|
||||
describe('error handling', () => {
|
||||
it('createFTSIndex handles already-existing index gracefully', async () => {
|
||||
const { createFTSIndex } = await import('../../src/core/kuzu/kuzu-adapter.js');
|
||||
const { createFTSIndex } = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
|
||||
// First call creates the index (may already exist from earlier test)
|
||||
await createFTSIndex('Function', 'function_fts_dup', ['name', 'content']);
|
||||
|
|
@ -96,11 +96,11 @@ withTestKuzuDB('core-adapter', (handle) => {
|
|||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('getKuzuStats returns valid counts', async () => {
|
||||
const { getKuzuStats } = await import('../../src/core/kuzu/kuzu-adapter.js');
|
||||
it('getLbugStats returns valid counts', async () => {
|
||||
const { getLbugStats } = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
|
||||
// getKuzuStats NEVER throws — it has silent catch blocks per table
|
||||
const stats = await getKuzuStats();
|
||||
// getLbugStats NEVER throws — it has silent catch blocks per table
|
||||
const stats = await getLbugStats();
|
||||
expect(typeof stats.nodes).toBe('number');
|
||||
expect(typeof stats.edges).toBe('number');
|
||||
expect(stats.nodes).toBeGreaterThanOrEqual(0);
|
||||
|
|
@ -108,14 +108,14 @@ withTestKuzuDB('core-adapter', (handle) => {
|
|||
});
|
||||
|
||||
it('executeQuery with empty string rejects', async () => {
|
||||
const { executeQuery } = await import('../../src/core/kuzu/kuzu-adapter.js');
|
||||
const { executeQuery } = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
|
||||
// KuzuDB throws on empty query string
|
||||
// LadybugDB throws on empty query string
|
||||
await expect(executeQuery('')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('deleteNodesForFile with non-existent path returns zero deleted', async () => {
|
||||
const { deleteNodesForFile } = await import('../../src/core/kuzu/kuzu-adapter.js');
|
||||
const { deleteNodesForFile } = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
|
||||
// deleteNodesForFile has per-query try/catch, returns {deletedNodes: 0} for missing paths
|
||||
const result = await deleteNodesForFile('/absolutely/nonexistent/path/file.ts');
|
||||
|
|
@ -126,13 +126,13 @@ withTestKuzuDB('core-adapter', (handle) => {
|
|||
}, {
|
||||
afterSetup: async (handle) => {
|
||||
// Load a minimal graph via CSV round-trip (core adapter is already initialized by wrapper)
|
||||
const { loadGraphToKuzu } = await import('../../src/core/kuzu/kuzu-adapter.js');
|
||||
const { loadGraphToLbug } = await import('../../src/core/lbug/lbug-adapter.js');
|
||||
const { createMinimalTestGraph } = await import('../helpers/test-graph.js');
|
||||
|
||||
const graph = createMinimalTestGraph();
|
||||
const storagePath = path.join(handle.tmpHandle.dbPath, 'storage');
|
||||
await fs.mkdir(storagePath, { recursive: true });
|
||||
|
||||
await loadGraphToKuzu(graph, '/test/repo', storagePath);
|
||||
await loadGraphToLbug(graph, '/test/repo', storagePath);
|
||||
},
|
||||
});
|
||||
|
|
@ -1,19 +1,19 @@
|
|||
/**
|
||||
* P0 Integration Tests: KuzuDB Connection Pool
|
||||
* P0 Integration Tests: LadybugDB Connection Pool
|
||||
*
|
||||
* Tests: initKuzu, executeQuery, executeParameterized, closeKuzu lifecycle
|
||||
* Tests: initLbug, executeQuery, executeParameterized, closeLbug lifecycle
|
||||
* Covers hardening fixes: parameterized queries, query timeout,
|
||||
* waiter queue timeout, idle eviction guards, stdout silencing race
|
||||
*/
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import {
|
||||
initKuzu,
|
||||
initLbug,
|
||||
executeQuery,
|
||||
executeParameterized,
|
||||
closeKuzu,
|
||||
isKuzuReady,
|
||||
} from '../../src/mcp/core/kuzu-adapter.js';
|
||||
import { withTestKuzuDB } from '../helpers/test-indexed-db.js';
|
||||
closeLbug,
|
||||
isLbugReady,
|
||||
} from '../../src/mcp/core/lbug-adapter.js';
|
||||
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
|
||||
|
||||
const POOL_SEED_DATA = [
|
||||
`CREATE (f:File {id: 'file:index.ts', name: 'index.ts', filePath: 'src/index.ts', content: ''})`,
|
||||
|
|
@ -26,20 +26,20 @@ const POOL_SEED_DATA = [
|
|||
|
||||
// ─── Pool lifecycle tests — test the pool adapter API directly ───────
|
||||
|
||||
withTestKuzuDB('kuzu-pool', (handle) => {
|
||||
withTestLbugDB('lbug-pool', (handle) => {
|
||||
afterEach(async () => {
|
||||
try { await closeKuzu('test-repo'); } catch { /* best-effort */ }
|
||||
try { await closeKuzu('repo1'); } catch { /* best-effort */ }
|
||||
try { await closeKuzu('repo2'); } catch { /* best-effort */ }
|
||||
try { await closeKuzu(''); } catch { /* best-effort */ }
|
||||
try { await closeLbug('test-repo'); } catch { /* best-effort */ }
|
||||
try { await closeLbug('repo1'); } catch { /* best-effort */ }
|
||||
try { await closeLbug('repo2'); } catch { /* best-effort */ }
|
||||
try { await closeLbug(''); } catch { /* best-effort */ }
|
||||
});
|
||||
|
||||
// ─── Lifecycle: init → query → close ─────────────────────────────────
|
||||
|
||||
describe('pool lifecycle', () => {
|
||||
it('initKuzu + executeQuery + closeKuzu', async () => {
|
||||
await initKuzu('test-repo', handle.dbPath);
|
||||
expect(isKuzuReady('test-repo')).toBe(true);
|
||||
it('initLbug + executeQuery + closeLbug', async () => {
|
||||
await initLbug('test-repo', handle.dbPath);
|
||||
expect(isLbugReady('test-repo')).toBe(true);
|
||||
|
||||
const rows = await executeQuery('test-repo', 'MATCH (n:Function) RETURN n.name AS name');
|
||||
expect(rows.length).toBeGreaterThanOrEqual(2);
|
||||
|
|
@ -47,32 +47,32 @@ withTestKuzuDB('kuzu-pool', (handle) => {
|
|||
expect(names).toContain('main');
|
||||
expect(names).toContain('helper');
|
||||
|
||||
await closeKuzu('test-repo');
|
||||
expect(isKuzuReady('test-repo')).toBe(false);
|
||||
await closeLbug('test-repo');
|
||||
expect(isLbugReady('test-repo')).toBe(false);
|
||||
});
|
||||
|
||||
it('initKuzu reuses existing pool entry', async () => {
|
||||
await initKuzu('test-repo', handle.dbPath);
|
||||
await initKuzu('test-repo', handle.dbPath); // second call should be no-op
|
||||
expect(isKuzuReady('test-repo')).toBe(true);
|
||||
it('initLbug reuses existing pool entry', async () => {
|
||||
await initLbug('test-repo', handle.dbPath);
|
||||
await initLbug('test-repo', handle.dbPath); // second call should be no-op
|
||||
expect(isLbugReady('test-repo')).toBe(true);
|
||||
});
|
||||
|
||||
it('closeKuzu is idempotent', async () => {
|
||||
await initKuzu('test-repo', handle.dbPath);
|
||||
await closeKuzu('test-repo');
|
||||
await closeKuzu('test-repo'); // second close should not throw
|
||||
expect(isKuzuReady('test-repo')).toBe(false);
|
||||
it('closeLbug is idempotent', async () => {
|
||||
await initLbug('test-repo', handle.dbPath);
|
||||
await closeLbug('test-repo');
|
||||
await closeLbug('test-repo'); // second close should not throw
|
||||
expect(isLbugReady('test-repo')).toBe(false);
|
||||
});
|
||||
|
||||
it('closeKuzu with no args closes all repos', async () => {
|
||||
await initKuzu('repo1', handle.dbPath);
|
||||
await initKuzu('repo2', handle.dbPath);
|
||||
expect(isKuzuReady('repo1')).toBe(true);
|
||||
expect(isKuzuReady('repo2')).toBe(true);
|
||||
it('closeLbug with no args closes all repos', async () => {
|
||||
await initLbug('repo1', handle.dbPath);
|
||||
await initLbug('repo2', handle.dbPath);
|
||||
expect(isLbugReady('repo1')).toBe(true);
|
||||
expect(isLbugReady('repo2')).toBe(true);
|
||||
|
||||
await closeKuzu();
|
||||
expect(isKuzuReady('repo1')).toBe(false);
|
||||
expect(isKuzuReady('repo2')).toBe(false);
|
||||
await closeLbug();
|
||||
expect(isLbugReady('repo1')).toBe(false);
|
||||
expect(isLbugReady('repo2')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ withTestKuzuDB('kuzu-pool', (handle) => {
|
|||
|
||||
describe('executeParameterized', () => {
|
||||
it('works with parameterized query', async () => {
|
||||
await initKuzu('test-repo', handle.dbPath);
|
||||
await initLbug('test-repo', handle.dbPath);
|
||||
const rows = await executeParameterized(
|
||||
'test-repo',
|
||||
'MATCH (n:Function) WHERE n.name = $name RETURN n.name AS name',
|
||||
|
|
@ -91,7 +91,7 @@ withTestKuzuDB('kuzu-pool', (handle) => {
|
|||
});
|
||||
|
||||
it('injection attempt is harmless with parameterized query', async () => {
|
||||
await initKuzu('test-repo', handle.dbPath);
|
||||
await initLbug('test-repo', handle.dbPath);
|
||||
const rows = await executeParameterized(
|
||||
'test-repo',
|
||||
'MATCH (n:Function) WHERE n.name = $name RETURN n.name AS name',
|
||||
|
|
@ -111,12 +111,12 @@ withTestKuzuDB('kuzu-pool', (handle) => {
|
|||
});
|
||||
|
||||
it('throws when db path does not exist', async () => {
|
||||
await expect(initKuzu('bad-repo', '/nonexistent/path/kuzu'))
|
||||
await expect(initLbug('bad-repo', '/nonexistent/path/lbug'))
|
||||
.rejects.toThrow();
|
||||
});
|
||||
|
||||
it('read-only mode: write query throws', async () => {
|
||||
await initKuzu('test-repo', handle.dbPath);
|
||||
await initLbug('test-repo', handle.dbPath);
|
||||
await expect(executeQuery('test-repo', "CREATE (n:Function {id: 'new', name: 'new', filePath: '', startLine: 0, endLine: 0, isExported: false, content: '', description: ''})"))
|
||||
.rejects.toThrow();
|
||||
});
|
||||
|
|
@ -126,7 +126,7 @@ withTestKuzuDB('kuzu-pool', (handle) => {
|
|||
|
||||
describe('relationship queries', () => {
|
||||
it('can query relationships', async () => {
|
||||
await initKuzu('test-repo', handle.dbPath);
|
||||
await initLbug('test-repo', handle.dbPath);
|
||||
const rows = await executeQuery(
|
||||
'test-repo',
|
||||
`MATCH (a:Function)-[r:CodeRelation {type: 'CALLS'}]->(b:Function) RETURN a.name AS caller, b.name AS callee`,
|
||||
|
|
@ -147,13 +147,13 @@ withTestKuzuDB('kuzu-pool', (handle) => {
|
|||
});
|
||||
|
||||
it('executeQuery rejects invalid Cypher syntax', async () => {
|
||||
await initKuzu('test-repo', handle.dbPath);
|
||||
await initLbug('test-repo', handle.dbPath);
|
||||
await expect(executeQuery('test-repo', 'THIS IS NOT CYPHER'))
|
||||
.rejects.toThrow();
|
||||
});
|
||||
|
||||
it('executeParameterized rejects when referenced parameter is missing', async () => {
|
||||
await initKuzu('test-repo', handle.dbPath);
|
||||
await initLbug('test-repo', handle.dbPath);
|
||||
await expect(executeParameterized(
|
||||
'test-repo',
|
||||
'MATCH (n:Function) WHERE n.name = $name RETURN n',
|
||||
|
|
@ -161,23 +161,23 @@ withTestKuzuDB('kuzu-pool', (handle) => {
|
|||
)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('closeKuzu with unknown repoId does not throw', async () => {
|
||||
await expect(closeKuzu('never-existed-repo')).resolves.toBeUndefined();
|
||||
it('closeLbug with unknown repoId does not throw', async () => {
|
||||
await expect(closeLbug('never-existed-repo')).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('isKuzuReady returns false for unknown repoId', () => {
|
||||
expect(isKuzuReady('never-existed-repo')).toBe(false);
|
||||
it('isLbugReady returns false for unknown repoId', () => {
|
||||
expect(isLbugReady('never-existed-repo')).toBe(false);
|
||||
});
|
||||
|
||||
it('initKuzu with empty string repoId stores entry under empty key', async () => {
|
||||
await initKuzu('', handle.dbPath);
|
||||
expect(isKuzuReady('')).toBe(true);
|
||||
await closeKuzu('');
|
||||
expect(isKuzuReady('')).toBe(false);
|
||||
it('initLbug with empty string repoId stores entry under empty key', async () => {
|
||||
await initLbug('', handle.dbPath);
|
||||
expect(isLbugReady('')).toBe(true);
|
||||
await closeLbug('');
|
||||
expect(isLbugReady('')).toBe(false);
|
||||
});
|
||||
|
||||
it('executeQuery with empty query string rejects', async () => {
|
||||
await initKuzu('test-repo', handle.dbPath);
|
||||
await initLbug('test-repo', handle.dbPath);
|
||||
await expect(executeQuery('test-repo', '')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,23 +1,24 @@
|
|||
/**
|
||||
* P0 Integration Tests: Local Backend — callTool dispatch
|
||||
*
|
||||
* Tests the full LocalBackend.callTool() dispatch with a real KuzuDB
|
||||
* Tests the full LocalBackend.callTool() dispatch with a real LadybugDB
|
||||
* instance, verifying cypher, context, impact, and query tools work
|
||||
* end-to-end against seeded graph data with FTS indexes.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, vi } from 'vitest';
|
||||
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
|
||||
import { listRegisteredRepos } from '../../src/storage/repo-manager.js';
|
||||
import { withTestKuzuDB } from '../helpers/test-indexed-db.js';
|
||||
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
|
||||
import { LOCAL_BACKEND_SEED_DATA, LOCAL_BACKEND_FTS_INDEXES } from '../fixtures/local-backend-seed.js';
|
||||
|
||||
vi.mock('../../src/storage/repo-manager.js', () => ({
|
||||
listRegisteredRepos: vi.fn().mockResolvedValue([]),
|
||||
cleanupOldKuzuFiles: vi.fn().mockResolvedValue({ found: false, needsReindex: false }),
|
||||
}));
|
||||
|
||||
// ─── Block 2: callTool dispatch tests ────────────────────────────────
|
||||
|
||||
withTestKuzuDB('local-backend-calltool', (handle) => {
|
||||
withTestLbugDB('local-backend-calltool', (handle) => {
|
||||
|
||||
describe('callTool dispatch with real DB', () => {
|
||||
let backend: LocalBackend;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* P0 Integration Tests: Local Backend
|
||||
*
|
||||
* Tests tool implementations via direct KuzuDB queries.
|
||||
* Tests tool implementations via direct LadybugDB queries.
|
||||
* The full LocalBackend.callTool() requires a global registry,
|
||||
* so here we test the security-critical behaviors directly:
|
||||
* - Write-operation blocking in cypher
|
||||
|
|
@ -17,18 +17,18 @@ import { describe, it, expect } from 'vitest';
|
|||
import {
|
||||
executeQuery,
|
||||
executeParameterized,
|
||||
} from '../../src/mcp/core/kuzu-adapter.js';
|
||||
} from '../../src/mcp/core/lbug-adapter.js';
|
||||
import {
|
||||
CYPHER_WRITE_RE,
|
||||
VALID_RELATION_TYPES,
|
||||
isWriteQuery,
|
||||
} from '../../src/mcp/local/local-backend.js';
|
||||
import { withTestKuzuDB } from '../helpers/test-indexed-db.js';
|
||||
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
|
||||
import { LOCAL_BACKEND_SEED_DATA } from '../fixtures/local-backend-seed.js';
|
||||
|
||||
// ─── Block 1: Pool adapter tests ─────────────────────────────────────
|
||||
|
||||
withTestKuzuDB('local-backend', (handle) => {
|
||||
withTestLbugDB('local-backend', (handle) => {
|
||||
|
||||
// ─── Cypher write blocking ───────────────────────────────────────────
|
||||
|
||||
|
|
@ -219,7 +219,7 @@ withTestKuzuDB('local-backend', (handle) => {
|
|||
|
||||
describe('query error handling via pool', () => {
|
||||
it('returns empty rows for unknown node label', async () => {
|
||||
// KuzuDB throws a Binder exception for unknown node labels
|
||||
// LadybugDB throws a Binder exception for unknown node labels
|
||||
await expect(
|
||||
executeQuery(handle.repoId, 'MATCH (n:NonExistentTable) RETURN n.name AS name')
|
||||
).rejects.toThrow();
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
/**
|
||||
* P0 Integration Tests: BM25/FTS Search against real KuzuDB
|
||||
* P0 Integration Tests: BM25/FTS Search against real LadybugDB
|
||||
*
|
||||
* Tests: searchFTSFromKuzu via core adapter (no repoId) path against
|
||||
* Tests: searchFTSFromLbug via core adapter (no repoId) path against
|
||||
* indexed test data. Verifies ranked result ordering, score merging,
|
||||
* and empty-match behavior.
|
||||
*
|
||||
* Uses withTestKuzuDB wrapper for full lifecycle management.
|
||||
* Uses withTestLbugDB wrapper for full lifecycle management.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { withTestKuzuDB } from '../helpers/test-indexed-db.js';
|
||||
import { searchFTSFromKuzu } from '../../src/core/search/bm25-index.js';
|
||||
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
|
||||
import { searchFTSFromLbug } from '../../src/core/search/bm25-index.js';
|
||||
import { SEARCH_SEED_DATA, SEARCH_FTS_INDEXES } from '../fixtures/search-seed.js';
|
||||
|
||||
// ─── Core adapter path (no repoId) ──────────────────────────────────
|
||||
|
||||
withTestKuzuDB('search-core', (_handle) => {
|
||||
describe('searchFTSFromKuzu — core adapter (no repoId)', () => {
|
||||
withTestLbugDB('search-core', (_handle) => {
|
||||
describe('searchFTSFromLbug — core adapter (no repoId)', () => {
|
||||
it('returns ranked results for a matching query', async () => {
|
||||
const results = await searchFTSFromKuzu('user authentication', 10);
|
||||
const results = await searchFTSFromLbug('user authentication', 10);
|
||||
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ withTestKuzuDB('search-core', (_handle) => {
|
|||
});
|
||||
|
||||
it('results are ordered by descending score', async () => {
|
||||
const results = await searchFTSFromKuzu('user authentication', 10);
|
||||
const results = await searchFTSFromLbug('user authentication', 10);
|
||||
|
||||
for (let i = 1; i < results.length; i++) {
|
||||
expect(results[i - 1].score).toBeGreaterThanOrEqual(results[i].score);
|
||||
|
|
@ -46,7 +46,7 @@ withTestKuzuDB('search-core', (_handle) => {
|
|||
});
|
||||
|
||||
it('auth-related files rank higher than unrelated files', async () => {
|
||||
const results = await searchFTSFromKuzu('user authentication', 10);
|
||||
const results = await searchFTSFromLbug('user authentication', 10);
|
||||
const filePaths = results.map((r) => r.filePath);
|
||||
|
||||
expect(filePaths).toContain('src/auth.ts');
|
||||
|
|
@ -59,7 +59,7 @@ withTestKuzuDB('search-core', (_handle) => {
|
|||
});
|
||||
|
||||
it('merges scores from multiple node types for the same filePath', async () => {
|
||||
const results = await searchFTSFromKuzu('user authentication', 20);
|
||||
const results = await searchFTSFromLbug('user authentication', 20);
|
||||
|
||||
const authResult = results.find((r) => r.filePath === 'src/auth.ts');
|
||||
expect(authResult).toBeDefined();
|
||||
|
|
@ -71,12 +71,12 @@ withTestKuzuDB('search-core', (_handle) => {
|
|||
});
|
||||
|
||||
it('respects limit parameter', async () => {
|
||||
const results = await searchFTSFromKuzu('user authentication', 2);
|
||||
const results = await searchFTSFromLbug('user authentication', 2);
|
||||
expect(results.length).toBeLessThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('returns empty array for a non-matching query', async () => {
|
||||
const results = await searchFTSFromKuzu('xyzzyplughtwisty', 10);
|
||||
const results = await searchFTSFromLbug('xyzzyplughtwisty', 10);
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -85,32 +85,32 @@ withTestKuzuDB('search-core', (_handle) => {
|
|||
|
||||
describe('unhappy paths', () => {
|
||||
it('returns empty array for empty query string', async () => {
|
||||
const results = await searchFTSFromKuzu('', 10);
|
||||
const results = await searchFTSFromLbug('', 10);
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array for whitespace-only query', async () => {
|
||||
const results = await searchFTSFromKuzu(' ', 10);
|
||||
const results = await searchFTSFromLbug(' ', 10);
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles special characters in query gracefully', async () => {
|
||||
const results = await searchFTSFromKuzu('user* OR auth+', 10);
|
||||
const results = await searchFTSFromLbug('user* OR auth+', 10);
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
});
|
||||
|
||||
it('handles limit of 0', async () => {
|
||||
const results = await searchFTSFromKuzu('user authentication', 0);
|
||||
const results = await searchFTSFromLbug('user authentication', 0);
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles negative limit gracefully', async () => {
|
||||
const results = await searchFTSFromKuzu('user authentication', -1);
|
||||
const results = await searchFTSFromLbug('user authentication', -1);
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
});
|
||||
|
||||
it('handles very large limit', async () => {
|
||||
const results = await searchFTSFromKuzu('user authentication', 100000);
|
||||
const results = await searchFTSFromLbug('user authentication', 100000);
|
||||
expect(results.length).toBeLessThanOrEqual(100000);
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
/**
|
||||
* P0 Integration Tests: BM25/FTS Search against real KuzuDB
|
||||
* P0 Integration Tests: BM25/FTS Search against real LadybugDB
|
||||
*
|
||||
* Tests: searchFTSFromKuzu via MCP pool adapter (with repoId) path
|
||||
* Tests: searchFTSFromLbug via MCP pool adapter (with repoId) path
|
||||
* against indexed test data. Verifies ranked result ordering and
|
||||
* empty-match behavior through the pool adapter.
|
||||
*
|
||||
* Uses withTestKuzuDB wrapper for full lifecycle management.
|
||||
* Uses withTestLbugDB wrapper for full lifecycle management.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { withTestKuzuDB } from '../helpers/test-indexed-db.js';
|
||||
import { searchFTSFromKuzu } from '../../src/core/search/bm25-index.js';
|
||||
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
|
||||
import { searchFTSFromLbug } from '../../src/core/search/bm25-index.js';
|
||||
import { SEARCH_SEED_DATA, SEARCH_FTS_INDEXES } from '../fixtures/search-seed.js';
|
||||
|
||||
// ─── MCP pool adapter path (with repoId) ────────────────────────────
|
||||
|
||||
withTestKuzuDB('search-pool', (handle) => {
|
||||
describe('searchFTSFromKuzu — MCP pool adapter (with repoId)', () => {
|
||||
withTestLbugDB('search-pool', (handle) => {
|
||||
describe('searchFTSFromLbug — MCP pool adapter (with repoId)', () => {
|
||||
it('returns ranked results via pool adapter', async () => {
|
||||
const results = await searchFTSFromKuzu('user authentication', 10, handle.repoId);
|
||||
const results = await searchFTSFromLbug('user authentication', 10, handle.repoId);
|
||||
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ withTestKuzuDB('search-pool', (handle) => {
|
|||
});
|
||||
|
||||
it('results are ordered by descending score via pool adapter', async () => {
|
||||
const results = await searchFTSFromKuzu('user authentication', 10, handle.repoId);
|
||||
const results = await searchFTSFromLbug('user authentication', 10, handle.repoId);
|
||||
|
||||
for (let i = 1; i < results.length; i++) {
|
||||
expect(results[i - 1].score).toBeGreaterThanOrEqual(results[i].score);
|
||||
|
|
@ -41,12 +41,12 @@ withTestKuzuDB('search-pool', (handle) => {
|
|||
});
|
||||
|
||||
it('returns empty array for non-matching query via pool adapter', async () => {
|
||||
const results = await searchFTSFromKuzu('xyzzyplughtwisty', 10, handle.repoId);
|
||||
const results = await searchFTSFromLbug('xyzzyplughtwisty', 10, handle.repoId);
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it('respects limit parameter via pool adapter', async () => {
|
||||
const results = await searchFTSFromKuzu('user authentication', 1, handle.repoId);
|
||||
const results = await searchFTSFromLbug('user authentication', 1, handle.repoId);
|
||||
expect(results.length).toBeLessThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -55,22 +55,22 @@ withTestKuzuDB('search-pool', (handle) => {
|
|||
|
||||
describe('unhappy paths', () => {
|
||||
it('returns empty array for empty query via pool', async () => {
|
||||
const results = await searchFTSFromKuzu('', 10, handle.repoId);
|
||||
const results = await searchFTSFromLbug('', 10, handle.repoId);
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array for whitespace-only query via pool', async () => {
|
||||
const results = await searchFTSFromKuzu(' ', 10, handle.repoId);
|
||||
const results = await searchFTSFromLbug(' ', 10, handle.repoId);
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles special characters in query via pool', async () => {
|
||||
const results = await searchFTSFromKuzu('user* OR auth+', 10, handle.repoId);
|
||||
const results = await searchFTSFromLbug('user* OR auth+', 10, handle.repoId);
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
});
|
||||
|
||||
it('handles limit of 0 via pool', async () => {
|
||||
const results = await searchFTSFromKuzu('user authentication', 0, handle.repoId);
|
||||
const results = await searchFTSFromLbug('user authentication', 0, handle.repoId);
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@
|
|||
*
|
||||
* Unref's all active handles after each test file so the event loop can
|
||||
* drain naturally. For non-native test files this is sufficient to let
|
||||
* the fork exit. For KuzuDB test files, native C++ handles may not expose
|
||||
* the fork exit. For LadybugDB test files, native C++ handles may not expose
|
||||
* .unref() — CI handles this via process isolation (one vitest invocation
|
||||
* per KuzuDB test file) so the OS reclaims everything on process exit.
|
||||
* per LadybugDB test file) so the OS reclaims everything on process exit.
|
||||
*
|
||||
* IMPORTANT: We do NOT import kuzu-adapter here. Importing it would load
|
||||
* the native addon even in non-KuzuDB test files, registering persistent
|
||||
* IMPORTANT: We do NOT import lbug-adapter here. Importing it would load
|
||||
* the native addon even in non-LadybugDB test files, registering persistent
|
||||
* handles that prevent the fork from exiting.
|
||||
*
|
||||
* IMPORTANT: We do NOT call process.exit() here. On Linux, process.exit()
|
||||
* triggers N-API destructor hooks in the KuzuDB native addon that segfault
|
||||
* triggers N-API destructor hooks in the LadybugDB native addon that segfault
|
||||
* (SIGSEGV), crashing the fork before it can send results back via IPC.
|
||||
*/
|
||||
import { afterAll } from 'vitest';
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { searchFTSFromKuzu, type BM25SearchResult } from '../../src/core/search/bm25-index.js';
|
||||
import { searchFTSFromLbug, type BM25SearchResult } from '../../src/core/search/bm25-index.js';
|
||||
|
||||
describe('BM25 search', () => {
|
||||
describe('searchFTSFromKuzu', () => {
|
||||
it('returns empty array when KuzuDB is not initialized', async () => {
|
||||
// Without KuzuDB init, search should return empty (not crash)
|
||||
const results = await searchFTSFromKuzu('test query');
|
||||
describe('searchFTSFromLbug', () => {
|
||||
it('returns empty array when LadybugDB is not initialized', async () => {
|
||||
// Without LadybugDB init, search should return empty (not crash)
|
||||
const results = await searchFTSFromLbug('test query');
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
expect(results).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('handles empty query', async () => {
|
||||
const results = await searchFTSFromKuzu('');
|
||||
const results = await searchFTSFromLbug('');
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts custom limit parameter', async () => {
|
||||
const results = await searchFTSFromKuzu('test', 5);
|
||||
const results = await searchFTSFromLbug('test', 5);
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,29 +2,30 @@
|
|||
* Unit Tests: LocalBackend callTool dispatch & lifecycle
|
||||
*
|
||||
* Tests the callTool dispatch logic, resolveRepo, init/disconnect,
|
||||
* error cases, and silent failure patterns — all with mocked KuzuDB.
|
||||
* error cases, and silent failure patterns — all with mocked LadybugDB.
|
||||
*
|
||||
* These are pure unit tests that mock the KuzuDB layer to test
|
||||
* These are pure unit tests that mock the LadybugDB layer to test
|
||||
* the dispatch and error handling logic in isolation.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// We need to mock the KuzuDB adapter and repo-manager BEFORE importing LocalBackend
|
||||
vi.mock('../../src/mcp/core/kuzu-adapter.js', () => ({
|
||||
initKuzu: vi.fn().mockResolvedValue(undefined),
|
||||
// We need to mock the LadybugDB adapter and repo-manager BEFORE importing LocalBackend
|
||||
vi.mock('../../src/mcp/core/lbug-adapter.js', () => ({
|
||||
initLbug: vi.fn().mockResolvedValue(undefined),
|
||||
executeQuery: vi.fn().mockResolvedValue([]),
|
||||
executeParameterized: vi.fn().mockResolvedValue([]),
|
||||
closeKuzu: vi.fn().mockResolvedValue(undefined),
|
||||
isKuzuReady: vi.fn().mockReturnValue(true),
|
||||
closeLbug: vi.fn().mockResolvedValue(undefined),
|
||||
isLbugReady: vi.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/storage/repo-manager.js', () => ({
|
||||
listRegisteredRepos: vi.fn().mockResolvedValue([]),
|
||||
cleanupOldKuzuFiles: vi.fn().mockResolvedValue({ found: false, needsReindex: false }),
|
||||
}));
|
||||
|
||||
// Also mock the search modules to avoid loading onnxruntime
|
||||
vi.mock('../../src/core/search/bm25-index.js', () => ({
|
||||
searchFTSFromKuzu: vi.fn().mockResolvedValue([]),
|
||||
searchFTSFromLbug: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/mcp/core/embedder.js', () => ({
|
||||
|
|
@ -32,9 +33,9 @@ vi.mock('../../src/mcp/core/embedder.js', () => ({
|
|||
getEmbeddingDims: vi.fn().mockReturnValue(384),
|
||||
}));
|
||||
|
||||
import { LocalBackend, isWriteQuery, CYPHER_WRITE_RE } from '../../src/mcp/local/local-backend.js';
|
||||
import { listRegisteredRepos } from '../../src/storage/repo-manager.js';
|
||||
import { initKuzu, executeQuery, executeParameterized, isKuzuReady, closeKuzu } from '../../src/mcp/core/kuzu-adapter.js';
|
||||
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
|
||||
import { listRegisteredRepos, cleanupOldKuzuFiles } from '../../src/storage/repo-manager.js';
|
||||
import { initLbug, executeQuery, executeParameterized, isLbugReady, closeLbug } from '../../src/mcp/core/lbug-adapter.js';
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -110,11 +111,11 @@ describe('LocalBackend.disconnect', () => {
|
|||
await expect(backend.disconnect()).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('calls closeKuzu on disconnect', async () => {
|
||||
it('calls closeLbug on disconnect', async () => {
|
||||
setupSingleRepo();
|
||||
await backend.init();
|
||||
await backend.disconnect();
|
||||
expect(closeKuzu).toHaveBeenCalled();
|
||||
expect(closeLbug).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -384,7 +385,7 @@ describe('LocalBackend.getContext', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ─── KuzuDB lazy initialization ──────────────────────────────────────
|
||||
// ─── LadybugDB lazy initialization ──────────────────────────────────────
|
||||
|
||||
describe('ensureInitialized', () => {
|
||||
let backend: LocalBackend;
|
||||
|
|
@ -396,26 +397,26 @@ describe('ensureInitialized', () => {
|
|||
await backend.init();
|
||||
});
|
||||
|
||||
it('calls initKuzu on first tool call', async () => {
|
||||
it('calls initLbug on first tool call', async () => {
|
||||
(executeParameterized as any).mockResolvedValue([]);
|
||||
await backend.callTool('query', { query: 'test' });
|
||||
expect(initKuzu).toHaveBeenCalled();
|
||||
expect(initLbug).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('retries initKuzu if connection was evicted', async () => {
|
||||
it('retries initLbug if connection was evicted', async () => {
|
||||
(executeParameterized as any).mockResolvedValue([]);
|
||||
// First call initializes
|
||||
await backend.callTool('query', { query: 'test' });
|
||||
expect(initKuzu).toHaveBeenCalledTimes(1);
|
||||
expect(initLbug).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Simulate idle eviction
|
||||
(isKuzuReady as any).mockReturnValueOnce(false);
|
||||
(isLbugReady as any).mockReturnValueOnce(false);
|
||||
await backend.callTool('query', { query: 'test' });
|
||||
expect(initKuzu).toHaveBeenCalledTimes(2);
|
||||
expect(initLbug).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('handles initKuzu failure gracefully', async () => {
|
||||
(initKuzu as any).mockRejectedValueOnce(new Error('DB locked'));
|
||||
it('handles initLbug failure gracefully', async () => {
|
||||
(initLbug as any).mockRejectedValueOnce(new Error('DB locked'));
|
||||
await expect(backend.callTool('query', { query: 'test' }))
|
||||
.rejects.toThrow('DB locked');
|
||||
});
|
||||
|
|
@ -503,9 +504,9 @@ describe('LocalBackend.listRepos', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ─── Cypher KuzuDB not ready ────────────────────────────────────────
|
||||
// ─── Cypher LadybugDB not ready ────────────────────────────────────────
|
||||
|
||||
describe('cypher tool KuzuDB not ready', () => {
|
||||
describe('cypher tool LadybugDB not ready', () => {
|
||||
let backend: LocalBackend;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
|
@ -515,19 +516,19 @@ describe('cypher tool KuzuDB not ready', () => {
|
|||
await backend.init();
|
||||
});
|
||||
|
||||
it('returns error when KuzuDB is not ready', async () => {
|
||||
(isKuzuReady as any).mockReturnValue(false);
|
||||
// initKuzu will succeed but isKuzuReady returns false after ensureInitialized
|
||||
// Actually ensureInitialized checks isKuzuReady and re-inits — let's make that pass
|
||||
// then the cypher method checks isKuzuReady again
|
||||
(isKuzuReady as any)
|
||||
it('returns error when LadybugDB is not ready', async () => {
|
||||
(isLbugReady as any).mockReturnValue(false);
|
||||
// initLbug will succeed but isLbugReady returns false after ensureInitialized
|
||||
// Actually ensureInitialized checks isLbugReady and re-inits — let's make that pass
|
||||
// then the cypher method checks isLbugReady again
|
||||
(isLbugReady as any)
|
||||
.mockReturnValueOnce(false) // ensureInitialized check
|
||||
.mockReturnValueOnce(false); // cypher's own check
|
||||
|
||||
const result = await backend.callTool('cypher', {
|
||||
query: 'MATCH (n) RETURN n LIMIT 1',
|
||||
});
|
||||
expect(result.error).toContain('KuzuDB not ready');
|
||||
expect(result.error).toContain('LadybugDB not ready');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -540,9 +541,10 @@ describe('cypher result formatting', () => {
|
|||
// Full reset of all mocks to prevent state leaking from other tests
|
||||
vi.resetAllMocks();
|
||||
(listRegisteredRepos as any).mockResolvedValue([MOCK_REPO_ENTRY]);
|
||||
(initKuzu as any).mockResolvedValue(undefined);
|
||||
(isKuzuReady as any).mockReturnValue(true);
|
||||
(closeKuzu as any).mockResolvedValue(undefined);
|
||||
(cleanupOldKuzuFiles as any).mockResolvedValue({ found: false, needsReindex: false });
|
||||
(initLbug as any).mockResolvedValue(undefined);
|
||||
(isLbugReady as any).mockReturnValue(true);
|
||||
(closeLbug as any).mockResolvedValue(undefined);
|
||||
(executeParameterized as any).mockResolvedValue([]);
|
||||
|
||||
backend = new LocalBackend();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
escapeCSVNumber,
|
||||
sanitizeUTF8,
|
||||
isBinaryContent,
|
||||
} from '../../src/core/kuzu/csv-generator.js';
|
||||
} from '../../src/core/lbug/csv-generator.js';
|
||||
|
||||
// ─── escapeCSVField ──────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -36,16 +36,16 @@ describe('getStoragePath', () => {
|
|||
// ─── getStoragePaths ─────────────────────────────────────────────────
|
||||
|
||||
describe('getStoragePaths', () => {
|
||||
it('returns storagePath, kuzuPath, metaPath', () => {
|
||||
it('returns storagePath, lbugPath, metaPath', () => {
|
||||
const paths = getStoragePaths('/home/user/project');
|
||||
expect(paths.storagePath).toContain('.gitnexus');
|
||||
expect(paths.kuzuPath).toContain('kuzu');
|
||||
expect(paths.lbugPath).toContain('lbug');
|
||||
expect(paths.metaPath).toContain('meta.json');
|
||||
});
|
||||
|
||||
it('all paths are under storagePath', () => {
|
||||
const paths = getStoragePaths('/home/user/project');
|
||||
expect(paths.kuzuPath.startsWith(paths.storagePath)).toBe(true);
|
||||
expect(paths.lbugPath.startsWith(paths.storagePath)).toBe(true);
|
||||
expect(paths.metaPath.startsWith(paths.storagePath)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ import {
|
|||
RELATION_SCHEMA,
|
||||
EMBEDDING_SCHEMA,
|
||||
CREATE_VECTOR_INDEX_QUERY,
|
||||
} from '../../src/core/kuzu/schema.js';
|
||||
} from '../../src/core/lbug/schema.js';
|
||||
|
||||
describe('KuzuDB Schema', () => {
|
||||
describe('LadybugDB Schema', () => {
|
||||
describe('NODE_TABLES', () => {
|
||||
it('includes all core node types', () => {
|
||||
const core = ['File', 'Folder', 'Function', 'Class', 'Interface', 'Method', 'CodeElement', 'Community', 'Process'];
|
||||
|
|
|
|||
2
gitnexus/test/vitest.d.ts
vendored
2
gitnexus/test/vitest.d.ts
vendored
|
|
@ -2,6 +2,6 @@ import 'vitest';
|
|||
|
||||
declare module 'vitest' {
|
||||
export interface ProvidedContext {
|
||||
kuzuDbPath: string;
|
||||
lbugDbPath: string;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default defineConfig({
|
|||
globals: true,
|
||||
setupFiles: ['test/setup.ts'],
|
||||
teardownTimeout: 3000,
|
||||
dangerouslyIgnoreUnhandledErrors: true, // KuzuDB N-API destructor segfaults on fork exit — not a test failure
|
||||
dangerouslyIgnoreUnhandledErrors: true, // LadybugDB N-API destructor segfaults on fork exit — not a test failure
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
include: ['src/**/*.ts'],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue