diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..9ef6858
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,22 @@
+# EditorConfig: https://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.json]
+indent_size = 2
+
+[*.{yml,yaml}]
+indent_size = 2
+
+[Makefile]
+indent_style = tab
diff --git a/.github/agents/code.agent.md b/.github/agents/code.agent.md
new file mode 100644
index 0000000..852a5e5
--- /dev/null
+++ b/.github/agents/code.agent.md
@@ -0,0 +1,44 @@
+---
+name: Code
+description: Expert TypeScript developer for lxDIG-MCP. Implements features, fixes bugs, and refactors code following the two-phase builder pattern.
+tools: [edit, execute, read, search, vscode, todo]
+model: Claude Sonnet 4.6 (copilot)
+argument-hint: Describe what to implement, the bug to fix, or the refactor to perform
+handoffs:
+ - label: Run tests
+ agent: test
+ prompt: Run the tests for the code I just changed and report any failures.
+ send: true
+ - label: Review changes
+ agent: review
+ prompt: Review the code changes I just made for correctness, patterns, and architecture compliance.
+ send: true
+---
+
+# Code Agent — lxDIG-MCP
+
+Expert TypeScript developer for lxDIG-MCP. Implements features, fixes bugs, and refactors code.
+
+## Rules
+
+- Run `npx tsc --noEmit` after every code change
+- Run `npx vitest run` after test changes — all 506 tests must pass
+- Never modify Cypher query strings without explicit instruction
+- Use `nodeStmts.push()` for node MERGEs, `edgeStmts.push()` for edges in builder.ts
+- ESM imports in source use NO `.js` extension — the build script adds them
+- Always use absolute paths for FILE node `path` property
+
+## Key Files
+
+- `src/graph/builder.ts` — translates parsed code → `BuildResult { nodes, edges }`
+- `src/graph/orchestrator.ts` — parse → build → execute → index pipeline
+- `src/graph/client.ts` — Memgraph client with circuit breaker + chunked batches
+- `src/tools/handlers/` — 11 handler files for 39 MCP tools
+
+## Test Patterns
+
+```typescript
+import { describe, it, expect, vi } from "vitest";
+// Mock memgraph: { isConnected: vi.fn().mockReturnValue(false), executeBatch: vi.fn().mockResolvedValue([]) }
+// Cleanup: fs.rmSync(root, { recursive: true, force: true })
+```
diff --git a/.github/agents/plan.agent.md b/.github/agents/plan.agent.md
new file mode 100644
index 0000000..2e778b3
--- /dev/null
+++ b/.github/agents/plan.agent.md
@@ -0,0 +1,50 @@
+---
+name: Plan
+description: Architecture planner for lxDIG-MCP. Analyzes impact and creates structured implementation plans for the two-phase build pipeline and beyond.
+tools: [read, search]
+model: Claude Opus 4.6 (copilot)
+argument-hint: Describe the feature, bug, or architectural change you want a plan for
+handoffs:
+ - label: Implement this plan
+ agent: Code
+ prompt: Implement the plan we just created, following all the steps in order.
+ send: true
+---
+
+# Plan Agent — lxDIG-MCP
+
+Architecture planner for lxDIG-MCP. Analyzes impact and creates structured implementation plans.
+
+## Planning Workflow
+
+1. Read `plan/BUILD_PIPELINE_PROPOSAL.md` for active roadmap (37 tasks, Phases A-E)
+2. Read `plan/ROADMAP.md` for prioritized tiers
+3. Check `plan/BUG_LIST_CONSOLIDATED.md` for known issues (55 bugs)
+4. Check `plan/PHASE-A-BUILDER-REFACTOR.md` for completed phase template
+
+## Plan Document Template
+
+```markdown
+# Phase X —
+## Current State (what exists today)
+## Target State (what we want)
+## Affected Files & Tests
+## Execution Steps (numbered, with verification)
+## Completion Criteria (checkboxes)
+```
+
+## Architecture Layers
+
+```
+MCP Tools (39) → Engines (6) → Orchestrator → Builder → Client → Memgraph
+ ↓
+ EmbeddingEngine → QdrantManager → Qdrant
+```
+
+## Active Status
+
+- Phase A: ✅ Builder returns `BuildResult { nodes, edges }`
+- Phase B: ⏳ Orchestrator two-phase execution (nodes-first, then edges)
+- Phase C: ⏳ Test→symbol edge accuracy
+- Phase D: ⏳ Validation suite
+- Phase E: ⏳ Qdrant sync reliability
diff --git a/.github/agents/review.agent.md b/.github/agents/review.agent.md
new file mode 100644
index 0000000..3f89017
--- /dev/null
+++ b/.github/agents/review.agent.md
@@ -0,0 +1,39 @@
+---
+name: review
+description: Code reviewer for lxDIG-MCP. Checks correctness, patterns, and architecture compliance against the two-phase builder pipeline rules.
+tools: [read, search]
+model: Claude Sonnet 4.6 (copilot)
+argument-hint: Point to the file or PR diff to review, or describe the change
+handoffs:
+ - label: Fix issues found
+ agent: Code
+ prompt: Fix the issues identified in the code review, following the architecture rules.
+ send: true
+---
+
+# Review Agent — lxDIG-MCP
+
+Code reviewer for lxDIG-MCP. Checks correctness, patterns, and architecture compliance.
+
+## Review Checklist
+
+1. **BuildResult pattern** — builder methods return `{ nodes, edges }`, never flat arrays
+2. **Cypher safety** — all user input via `$params`, never string interpolation
+3. **Circuit breaker** — bulk writes use `BULK_CHUNK_SIZE=1500`, `CIRCUIT_BREAKER_BULK_THRESHOLD=50`
+4. **ESM compliance** — no `.js` in source imports, no `require()`, no `__dirname`
+5. **Test coverage** — new logic has tests in `__tests__/*.test.ts`
+6. **Type safety** — no `any` without justification, strict null checks
+
+## Architecture Rules
+
+- Parsers produce `ParsedFile` → Builder produces `BuildResult` → Orchestrator executes
+- Graph writes always go through `MemgraphClient.executeBatch()`
+- Vector operations go through `EmbeddingEngine` → `QdrantManager`
+- Tools are registered in `src/tools/registry.ts`, handlers in `src/tools/handlers/`
+
+## Anti-Patterns to Flag
+
+- `statements.push()` without classifying node vs edge
+- Direct Memgraph session usage outside `client.ts`
+- Hardcoded connection strings (should use `env.ts`)
+- Missing `try/finally` around bulk mode toggle
diff --git a/.github/agents/test.agent.md b/.github/agents/test.agent.md
new file mode 100644
index 0000000..6bfeae4
--- /dev/null
+++ b/.github/agents/test.agent.md
@@ -0,0 +1,61 @@
+---
+name: test
+description: Test author for lxDIG-MCP. Creates and fixes vitest tests following established mocking and assertion patterns.
+tools: [edit, execute, read, search]
+model: Claude Sonnet 4.6 (copilot)
+argument-hint: Describe the module or function to test, or paste a failing test output
+handoffs:
+ - label: Fix production code
+ agent: Code
+ prompt: The tests are failing due to a bug in production code. Fix the underlying implementation.
+ send: true
+ - label: Review tests
+ agent: review
+ prompt: Review the tests I just wrote for correctness and coverage completeness.
+ send: true
+---
+
+# Test Agent — lxDIG-MCP
+
+Test author for lxDIG-MCP. Creates and fixes vitest tests.
+
+## Commands
+
+- Run all: `npx vitest run`
+- Run one file: `npx vitest run src/path/__tests__/file.test.ts`
+- Type check: `npx tsc --noEmit`
+
+## Conventions
+
+- Framework: vitest (`describe`, `it`, `expect`, `vi.fn()`, `vi.mock()`)
+- Location: `src//__tests__/.test.ts`
+- Current count: 506 tests across 29 files
+
+## Mocking Patterns
+
+```typescript
+// Memgraph client mock
+const mockClient = {
+ isConnected: vi.fn().mockReturnValue(false),
+ executeBatch: vi.fn().mockResolvedValue([]),
+ executeQuery: vi.fn().mockResolvedValue({ records: [] }),
+};
+
+// Builder result
+const { nodes, edges } = builder.buildFromParsedFile(parsed);
+const stmts = [...nodes, ...edges]; // only when order doesn't matter
+
+// Temp directory cleanup
+afterEach(() => fs.rmSync(root, { recursive: true, force: true }));
+```
+
+## Assertion Patterns
+
+```typescript
+// Cypher statement validation
+expect(stmts.some(s => s.query.includes("MERGE"))).toBe(true);
+expect(nodes.every(s => !s.query.match(/MATCH.*MERGE.*\[/))).toBe(true);
+
+// BuildResult structure
+expect(nodes.length + edges.length).toBe(totalCount);
+```
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 71fd831..6dbddd5 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -1,222 +1,108 @@
-# Copilot Instructions - lxRAG MCP Server
+# lxDIG-MCP — Copilot Instructions
-**Use this template for all projects working with lxRAG MCP.**
+TypeScript ESM MCP server providing graph intelligence and agent memory for codebases.
----
+## Stack & Build
-## 🎯 Primary Goal
+- **Language:** TypeScript ESM (`moduleResolution: "bundler"`, `module: "ESNext"`)
+- **Runtime:** Node.js v22.17.0
+- **Build:** `npm run build` → `tsc && bash scripts/fix-esm-imports.sh`
+- **Test:** `npx vitest run` (506 tests across 29 files)
+- **Type-check:** `npx tsc --noEmit` (run after every code change)
+- **Databases:** Memgraph (bolt://localhost:7687), Qdrant (http://localhost:6333)
-Use lxRAG MCP for **ALL code intelligence**. Never fall back to file reads or grep.
+## Source Structure
----
-
-## ⚡ Non-Negotiable Rules
-
-### Rule 1: NEVER Read Files
```
-❌ "Let me read src/auth/service.ts"
-✅ Use: code_explain('AuthService')
+src/
+ graph/ ← builder.ts, orchestrator.ts, client.ts, index.ts
+ engines/ ← architecture, community, coordination, docs, progress, test
+ parsers/ ← typescript, python, go, rust, java, docs
+ tools/ ← 39 MCP tool handlers across 11 handler files
+ vector/ ← embedding-engine.ts, qdrant-client.ts
+ types/ ← shared type definitions
+ utils/ ← validation, logger, helpers
+ response/ ← response formatting
+ config.ts, env.ts, server.ts
```
-### Rule 2: NEVER Use Grep
-```
-❌ "I'll search with grep for..."
-✅ Use: graph_query('find all...')
-```
+## Two-Phase Builder Pattern
-### Rule 3: ALWAYS Initialize Session
-First query MUST be:
-1. `graph_set_workspace(workspaceRoot, projectId)`
-2. `graph_health()`
-3. Then answer the question
+The builder produces `BuildResult { nodes, edges }` — two separate arrays. The orchestrator runs all node MERGEs first, then all edge MATCHes. This avoids "node not found" errors on edge creation.
-### Rule 4: Re-anchor Every 5 Messages
-Prevent instruction drift in long conversations:
-```
-Every 5 messages: Call graph_health()
-If not ready: Call graph_rebuild(mode: 'incremental')
-```
+```typescript
+// ✅ Correct — classify every statement
+const result: BuildResult = { nodes: [], edges: [] };
+result.nodes.push({ query: "MERGE (f:FILE {id: $id})", params: { id } });
+result.edges.push({ query: "MATCH (f:FILE {id: $fid}) MATCH (fn:FUNCTION {id: $fnid}) MERGE (f)-[:CONTAINS]->(fn)", params: { fid, fnid } });
----
-
-## 📊 Tool Quick Reference
-
-| Your Question | MCP Tool | Example |
-|---|---|---|
-| **Find X** | `graph_query` | find all HTTP handlers |
-| **How does X work?** | `code_explain` | AuthService |
-| **What breaks?** | `impact_analyze` | [changed files] |
-| **Which tests?** | `test_select` | [changed files] |
-| **Violates rules?** | `arch_validate` | {} |
-| **Where to put code?** | `arch_suggest` | filePath |
-| **Search by concept** | `semantic_search` | "validation patterns" |
-| **Similar code?** | `find_similar_code` | AuthService |
-| **Detect violations** | `find_pattern` | "pattern name" |
-| **Remember this** | `episode_add` | decision, reasoning |
-
-**Full reference**: See `docs/QUICK_REFERENCE.md` for all 38 tools.
-
----
-
-## 🔄 Session Flow (MANDATORY)
-
-### On First Query
-```json
-{
- "tool": "graph_set_workspace",
- "args": {
- "workspaceRoot": "/absolute/path/to/project",
- "projectId": "your-project-id",
- "sourceDir": "src"
- }
-}
+// ❌ Wrong — flat array loses the node/edge distinction
+const stmts: CypherStatement[] = [];
+stmts.push(...);
```
-```json
-{
- "tool": "graph_health",
- "args": {}
-}
-```
+## Cypher Safety
-Then answer the question using MCP tools.
+Always use `$params` for user-supplied values — never string interpolation. Memgraph does not support prepared statements, so interpolation is an injection risk.
-### Every 5 Messages (Long Conversations)
-```json
-{
- "tool": "graph_health",
- "args": {}
-}
-```
+```typescript
+// ✅ Correct
+{ query: "MERGE (n:Node {id: $id, name: $name})", params: { id, name } }
-This re-anchors the session and prevents instruction drift.
-
----
-
-## 🛠️ Common Patterns
-
-### Pattern 1: Find Code
-```
-User: "Find all HTTP handlers"
-You: graph_query('find all HTTP handlers')
+// ❌ Wrong — injection risk
+{ query: `MERGE (n:Node {id: "${id}", name: "${name}"})`, params: {} }
```
-### Pattern 2: Understand Symbol
-```
-User: "How does AuthService work?"
-You:
- 1. code_explain('AuthService')
- 2. [Optional] graph_query('show call graph for AuthService')
- 3. Summarize with full context
-```
+## ESM Imports
-### Pattern 3: Impact Analysis
-```
-User: "What if I refactor AuthService?"
-You:
- 1. impact_analyze(['src/auth/service.ts'])
- 2. test_select(['src/auth/service.ts'])
- 3. Explain impact + affected tests
-```
-
-### Pattern 4: Architecture Check
-```
-User: "Does this fit the architecture?"
-You:
- 1. arch_validate()
- 2. arch_suggest(filePath)
- 3. Explain layer placement + rules
-```
-
----
-
-## ✅ Quality Checklist
-
-Good response includes:
-- [ ] Called `graph_set_workspace` on first query
-- [ ] Called `graph_health` before heavy queries
-- [ ] Used MCP tools (not file reads)
-- [ ] No grep or search patterns
-- [ ] Explained which tool was used
-- [ ] Provided context from graph
-- [ ] For long responses, re-anchored with `graph_health`
+Do not add `.js` extensions to TypeScript source imports — `scripts/fix-esm-imports.sh` adds them at build time. Adding them manually causes double-extension bugs.
-Bad response includes:
-- [ ] Used file operations
-- [ ] Mentioned grep
-- [ ] Guessed code structure
-- [ ] Traced dependencies manually
-- [ ] Said "Let me read the file..."
-- [ ] Long conversation without re-anchoring
+```typescript
+// ✅ Correct
+import { GraphBuilder } from "../graph/builder";
----
-
-## 📁 Active Projects
-
-| Project | Path | projectId |
-|---------|------|-----------|
-| cad-engine | `/home/alex_rod/projects/cad-engine` | `cad-engine` |
-| cad-web | `/home/alex_rod/projects/cad-web` | `cad-web` |
-
----
-
-## 📚 Documentation
-
-| Document | Purpose |
-|----------|---------|
-| [docs/CLAUDE_INTEGRATION.md](../docs/CLAUDE_INTEGRATION.md) | Why instructions get ignored + system prompt fix |
-| [docs/MCP_INTEGRATION_GUIDE.md](../docs/MCP_INTEGRATION_GUIDE.md) | Complete integration guide |
-| [docs/TOOL_PATTERNS.md](../docs/TOOL_PATTERNS.md) | Before/after: grep → MCP patterns |
-| [docs/INTEGRATION_SUMMARY.md](../docs/INTEGRATION_SUMMARY.md) | Quick navigation and summary |
-| [QUICK_REFERENCE.md](../QUICK_REFERENCE.md) | All 38 tools |
-| [QUICK_START.md](../QUICK_START.md) | Server deployment |
+// ❌ Wrong — build script will produce builder.js.js
+import { GraphBuilder } from "../graph/builder.js";
+```
----
+Never use `require()` or `__dirname` — this is pure ESM.
-## 🚀 Implementation Checklist
+## Graph Writes
-### Infrastructure (One Time)
-- [ ] `docker-compose up -d memgraph qdrant`
-- [ ] `npm run build && npm run start:http`
-- [ ] Verify: `curl http://localhost:9000/health`
+All writes go through `MemgraphClient.executeBatch()`. Direct session usage bypasses the circuit breaker and chunking logic, which causes failures on large graphs.
-### Claude Desktop
-- [ ] Edit `~/.claude_desktop_config.json`
-- [ ] Add MCP server config
-- [ ] Add system prompt that enforces MCP
-- [ ] Restart Claude completely
+- Bulk chunk size: `BULK_CHUNK_SIZE = 1500` statements per transaction
+- Circuit breaker threshold: `CIRCUIT_BREAKER_BULK_THRESHOLD = 50` consecutive failures
-### Per-Project
-- [ ] Copy this file to `.github/copilot-instructions.md`
-- [ ] Add `.mcp-config.json` with projectId
-- [ ] Commit both files
-- [ ] Test: Ask code question, verify MCP tools used
+## Vector / Qdrant
----
+All vector operations go through `EmbeddingEngine` → `QdrantManager`. Collections are static (`"functions"`, `"classes"`, `"files"`, `"document_sections"`). Filter by `payload.projectId` on every search — never return cross-project results.
-## 🆘 Troubleshooting
+## projectId Scoping
-| Problem | Solution |
-|---------|----------|
-| Claude reads files | Check system prompt in Claude Desktop config |
-| Long conversation breaks | Ensure `graph_health()` every 5 messages |
-| MCP server won't respond | Check: `docker-compose ps` + `curl http://localhost:9000/health` |
-| Graph not indexing | Run: `graph_rebuild(mode: 'full')` |
-| "Tool not found" error | Verify MCP server is running and healthy |
+Every graph node and vector point is scoped to a 4-char base-36 project fingerprint. Use `computeProjectFingerprint(workspaceRoot)` from `src/utils/validation.ts`. Never use user-supplied strings as graph keys.
----
+## Testing Conventions
-## 🎯 Remember
+- Location: `src//__tests__/.test.ts`
+- Framework: vitest (`describe`, `it`, `expect`, `vi.fn()`, `vi.mock()`)
-- **Goal**: Zero fallback to grep or file reads
-- **Method**: System prompt + MCP-exclusive tool use
-- **Result**: 10x faster, zero false positives, full context
-- **Scale**: Shared MCP backend serving all projects
+```typescript
+// Standard Memgraph mock
+const mockClient = {
+ isConnected: vi.fn().mockReturnValue(false),
+ executeBatch: vi.fn().mockResolvedValue([]),
+ executeQuery: vi.fn().mockResolvedValue({ records: [] }),
+};
-✨ **With proper system prompt and re-anchoring, Claude uses MCP exclusively even in 100+ message conversations.**
+// Always clean up temp dirs
+afterEach(() => fs.rmSync(root, { recursive: true, force: true }));
+```
----
+## Anti-Patterns
-**For detailed setup**: See [docs/CLAUDE_INTEGRATION.md](../docs/CLAUDE_INTEGRATION.md)
-**For tool patterns**: See [docs/TOOL_PATTERNS.md](../docs/TOOL_PATTERNS.md)
-**For integration**: See [docs/INTEGRATION_SUMMARY.md](../docs/INTEGRATION_SUMMARY.md)
+- Do not push to a flat `CypherStatement[]` — always classify into `nodes` or `edges`
+- Do not use Memgraph sessions directly outside `src/graph/client.ts`
+- Do not hardcode connection strings — read from `src/env.ts`
+- Do not omit `try/finally` when toggling bulk mode on `MemgraphClient`
+- Do not add `.js` to source-level imports
diff --git a/.github/prompts/graph-expert.prompt.md b/.github/prompts/graph-expert.prompt.md
deleted file mode 100644
index 84a1165..0000000
--- a/.github/prompts/graph-expert.prompt.md
+++ /dev/null
@@ -1,42 +0,0 @@
----
-name: graph-expert
-description: Graph Expert Agent for lxRAG MCP (session-aware, workspace-aware)
-agent: graph-expert
----
-
-Act as the Graph Expert Agent for this repository.
-
-Objectives:
-
-- Maximize accuracy and speed using MCP graph tools.
-- Enforce session-scoped workflow and correct workspace targeting.
-
-Mandatory flow:
-
-1. Ensure MCP session is initialized and bound to this window (`mcp-session-id`).
-2. Set workspace with `graph_set_workspace`.
-3. Trigger indexing with `graph_rebuild` (`incremental` by default).
-4. Check readiness/context via `graph_health`.
-5. Answer using `graph_query` + specialized tools.
-
-Rules:
-
-- Never assume rebuild results are immediate; treat queued rebuilds as pending.
-- In Docker, use mounted paths (usually `/workspace`), not host paths.
-- In host runtime, use native absolute paths.
-- If workspace is inaccessible, return an actionable mount/path correction.
-
-Tool routing:
-
-- Broad discovery: `graph_query`
-- Deep code understanding: `code_explain`
-- Architecture: `arch_validate`, `find_pattern`, `arch_suggest`
-- Test impact: `impact_analyze`, `test_select`, `test_run`
-- Progress state: `progress_query`, `feature_status`, `task_update`, `blocking_issues`
-- Contract normalization: `contract_validate`
-
-Response format:
-
-- Context: `projectId`, `workspaceRoot`
-- Findings: confirmed vs pending
-- Next step: one concrete action
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..fecd150
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,79 @@
+# ── Continuous Integration ─────────────────────────────────────────────────────
+#
+# Runs on every push to main/test/* branches and on all pull requests.
+# Steps: checkout → install → build → lint → test (with coverage)
+#
+# Memgraph / Qdrant are NOT required — the entire test suite uses vi.fn() mocks
+# and does not open real network connections to databases.
+
+name: CI
+
+on:
+ push:
+ branches:
+ - main
+ - "test/**"
+ - "feature/**"
+ - "fix/**"
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ build-and-test:
+ name: Build, Lint & Test
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ node-version: ["24.14.0"]
+
+ steps:
+ # ── Checkout ──────────────────────────────────────────────────────────────
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ # ── Node.js setup ─────────────────────────────────────────────────────────
+ - name: Set up Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: "npm"
+
+ # ── Install ───────────────────────────────────────────────────────────────
+ - name: Install dependencies
+ run: npm ci
+
+ # ── Build ─────────────────────────────────────────────────────────────────
+ - name: TypeScript build
+ run: npm run build
+
+ # ── Lint ──────────────────────────────────────────────────────────────────
+ - name: ESLint (errors only)
+ # We allow warnings (no-console, no-explicit-any) but fail on any error.
+ run: npm run lint -- --max-warnings 9999
+
+ # ── Test + Coverage ───────────────────────────────────────────────────────
+ - name: Run tests with coverage
+ run: npm run test:coverage
+ env:
+ # Suppress INFO-level log output during test runs for cleaner CI logs.
+ LXDIG_LOG_LEVEL: error
+
+ # ── Upload coverage report ────────────────────────────────────────────────
+ - name: Upload coverage to GitHub Actions artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-report-node-${{ matrix.node-version }}
+ path: coverage/
+ retention-days: 14
+
+ # ── Optional: post coverage summary to PR ─────────────────────────────────
+ - name: Report coverage summary
+ if: github.event_name == 'pull_request'
+ uses: davelosert/vitest-coverage-report-action@v2
+ with:
+ name: "lxDIG MCP"
+ json-summary-compare-path: coverage/coverage-summary.json
+ continue-on-error: true
diff --git a/.gitignore b/.gitignore
index ce1f4ee..a39484f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ venv/
# Build output
dist/
+coverage/
# Local graph/cache artifacts
.lexrag/
@@ -35,3 +36,4 @@ __pycache__/
# benchmarks
benchmarks/agent_mode_artifacts/
benchmarks/graph_tools_benchmark_results.json
+plan
\ No newline at end of file
diff --git a/.lxdig/cache/file-hashes.json b/.lxdig/cache/file-hashes.json
new file mode 100644
index 0000000..6946754
--- /dev/null
+++ b/.lxdig/cache/file-hashes.json
@@ -0,0 +1,18 @@
+{
+ "version": "1.0",
+ "lastBuild": 1772407582196,
+ "files": {
+ "../../../../tmp/orch-pc4-w4wCE1/src/__tests__/validator.test.ts": {
+ "path": "../../../../tmp/orch-pc4-w4wCE1/src/__tests__/validator.test.ts",
+ "hash": "-7f59c00b",
+ "timestamp": 1772407582196,
+ "LOC": 3
+ },
+ "../../../../tmp/orch-pc4-w4wCE1/src/validator.ts": {
+ "path": "../../../../tmp/orch-pc4-w4wCE1/src/validator.ts",
+ "hash": "-74a395d",
+ "timestamp": 1772407582196,
+ "LOC": 2
+ }
+ }
+}
\ No newline at end of file
diff --git a/.lxrag/config.json b/.lxdig/config.json
similarity index 100%
rename from .lxrag/config.json
rename to .lxdig/config.json
diff --git a/.lxdig/project.json b/.lxdig/project.json
new file mode 100644
index 0000000..a60b661
--- /dev/null
+++ b/.lxdig/project.json
@@ -0,0 +1,6 @@
+{
+ "projectId": "lxDIG-MCP",
+ "name": "lxDIG-MCP",
+ "workspaceRoot": "/home/alex_rod/projects/lxDIG-MCP",
+ "createdAt": "2026-03-01T23:26:23.430Z"
+}
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..9d02e47
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,12 @@
+{
+ "printWidth": 100,
+ "tabWidth": 2,
+ "useTabs": false,
+ "semi": true,
+ "singleQuote": false,
+ "quoteProps": "as-needed",
+ "trailingComma": "all",
+ "bracketSpacing": true,
+ "arrowParens": "always",
+ "endOfLine": "lf"
+}
diff --git a/ANALYSIS_COMPLETION_REPORT.md b/ANALYSIS_COMPLETION_REPORT.md
deleted file mode 100644
index 43da22f..0000000
--- a/ANALYSIS_COMPLETION_REPORT.md
+++ /dev/null
@@ -1,491 +0,0 @@
-# LexRAG-MCP Analysis Completion Report
-
-**Analysis Complete**: 2026-02-22
-**Duration**: ~45 minutes
-**Method**: lxRAG Tools Only (Exclusive - No File Reads, No Grep)
-**Status**: ✅ COMPLETE & ACTIONABLE
-
----
-
-## 🎯 Executive Summary
-
-**Used only lxRAG tools** to comprehensively analyze the lexRAG-MCP project. Identified **3 critical blockers**, documented all findings, and created a complete **5-phase implementation plan**.
-
-**Result**: 6 comprehensive documents (2,500+ lines) ready for immediate implementation.
-
----
-
-## 📊 Analysis Results
-
-### Analysis Coverage
-
-```
-lxRAG Tools Used: 12/38
-Successful Operations: 8
-Errors Encountered: 3 (all documented)
-Documentation Files Found: 26 (indexed)
-Systems Identified: 7 major subsystems
-Markdown Documents Found: 50+ with detailed content
-```
-
-### Issues Found & Documented
-
-| # | Issue | Severity | Status | Fix Time |
-| --- | ---------------------------- | ----------- | ----------- | -------- |
-| 1 | Missing `.lxrag/config.json` | 🔴 CRITICAL | Actionable | 15 min |
-| 2 | BigInt type conversion error | 🔴 CRITICAL | Documented | 30 min |
-| 3 | Incomplete graph rebuild | 🟠 HIGH | In Progress | 5 min |
-
-**All issues are fixable** - Complete solutions provided.
-
----
-
-## 📁 Documents Created
-
-### 6 Comprehensive Analysis Documents
-
-```
-1. ERROR_REPORT.md
- └─ Error catalog, tool status, troubleshooting guide
- └─ Length: ~400 lines
- └─ Purpose: Track all errors and their resolution
-
-2. LXRAG_ANALYSIS_REPORT.md
- └─ Detailed technical analysis
- └─ Length: ~750 lines
- └─ Purpose: Complete project understanding
-
-3. RESOLUTION_PLAN.md
- └─ Step-by-step implementation guide
- └─ Length: ~600 lines
- └─ Purpose: Execute the fix and development plan
-
-4. PROJECT_ANALYSIS_SUMMARY.md
- └─ Executive overview and key findings
- └─ Length: ~700 lines
- └─ Purpose: Quick understanding without deep dive
-
-5. ANALYSIS_WORKFLOW.md
- └─ Visual representation of analysis and results
- └─ Length: ~500 lines
- └─ Purpose: ASCII diagrams and visual understanding
-
-6. ANALYSIS_INDEX.md
- └─ Navigation guide and document index
- └─ Length: ~400 lines
- └─ Purpose: Guide through all documents
-```
-
-**Total**: 2,500+ lines of comprehensive documentation
-
----
-
-## 🔍 Key Findings
-
-### Finding #1: Architecture Configuration Missing
-
-**Impact**: Blocks full architecture validation
-
-- Files not assigned to layers: `src/index.ts`, `src/mcp-server.ts`, `src/engines/**`
-- Issue found by: `mcp_lxrag_arch_validate`
-- Solution: Create `.lxrag/config.json` with layer definitions
-- Template: Provided in RESOLUTION_PLAN.md
-- Fix time: 15 minutes
-
-### Finding #2: BigInt Type Conversion Error
-
-**Impact**: Prevents graph health verification (non-blocking)
-
-- Error in: `graph_health()` function
-- Root cause: Mixing BigInt with numeric types
-- Severity: Recoverable (doesn't block rebuild)
-- Solution: Update backend type conversions
-- Fix time: 30 minutes
-
-### Finding #3: Graph Rebuild In Progress
-
-**Impact**: Limited symbol data available temporarily
-
-- Status: Rebuild initiated in full mode
-- Documents indexed: 26 (successful)
-- Symbols indexed: Still processing
-- Expected availability: 2-5 minutes
-- Resolution: Automatic (just wait)
-
-### Positive Finding: Well-Documented Project
-
-**Impact**: Excellent reference material available
-
-- Documentation files indexed: 26
-- Markdown documents found: 50+
-- Architecture documented: ✓
-- Roadmaps documented: ✓
-- Implementation guides available: ✓
-
----
-
-## ✅ Documents Ready to Use
-
-### For Quick Start (5 min)
-
-→ **PROJECT_ANALYSIS_SUMMARY.md**
-
-- Key findings overview
-- 5-phase timeline
-- Immediate next steps
-
-### For Implementation (15 min)
-
-→ **RESOLUTION_PLAN.md**
-
-- Phase 1 execution steps
-- Code templates ready to use
-- Validation checklists
-
-### For Deep Dive (20 min)
-
-→ **LXRAG_ANALYSIS_REPORT.md**
-
-- Complete technical analysis
-- Architecture breakdown
-- All pending tasks documented
-
-### For Troubleshooting
-
-→ **ERROR_REPORT.md**
-
-- All 3 errors detailed
-- Tool status matrix
-- Recovery procedures
-
-### For Visualization (5 min)
-
-→ **ANALYSIS_WORKFLOW.md**
-
-- Analysis process diagram
-- Issue dependency graph
-- Resolution timeline
-
-### For Navigation
-
-→ **ANALYSIS_INDEX.md**
-
-- Document index
-- Reading paths by role
-- Cross-references
-
----
-
-## 📋 lxRAG Tools Used & Results
-
-### Successful Operations (8)
-
-```
-✓ mcp_lxrag_init_project_setup
- └─ Status: Workspace initialized, build queued
-
-✓ mcp_lxrag_graph_rebuild
- └─ Status: Full rebuild initiated (in progress)
-
-✓ mcp_lxrag_index_docs
- └─ Status: 26 documents indexed (10.7 seconds)
-
-✓ mcp_lxrag_arch_validate
- └─ Status: Found 2 violations (unassigned files)
-
-✓ mcp_lxrag_arch_suggest
- └─ Status: Layer suggestions working correctly
-
-✓ mcp_lxrag_find_pattern
- └─ Status: Pattern search implemented (awaiting data)
-
-✓ mcp_lxrag_ref_query
- └─ Status: Found 5 reference documents
-
-✓ mcp_lxrag_context_pack
- └─ Status: Ready (awaiting graph completion)
-```
-
-### Failed Operations (2)
-
-```
-✗ mcp_lxrag_graph_health
- └─ Error: BigInt type conversion error
- └─ Severity: Recoverable, non-blocking
- └─ Impact: Cannot verify health status
-
-✗ mcp_lxrag_search_docs
- └─ Error: Tool disabled in environment
- └─ Severity: Non-critical
- └─ Impact: Doc search unavailable (feature works, just disabled)
-```
-
-### Pending Operations (3)
-
-```
-⏳ mcp_lxrag_reflect
- └─ Status: Awaiting graph rebuild completion
-
-⏳ mcp_lxrag_impact_analyze
- └─ Status: Awaiting full symbol data
-
-⏳ Pattern detection (advanced)
- └─ Status: Awaiting graph completion
-```
-
----
-
-## 🚀 Implementation Timeline
-
-### Phase 1: Backend & Configuration (1 day) 🔴 CRITICAL
-
-- [x] Identified blocking issues
-- [x] Created configuration template
-- [x] Documented fix procedures
-- [ ] Execute Phase 1 (start now)
-
-**To Execute**:
-
-1. Create `.lxrag/config.json` (15 min)
-2. Fix BigInt error if needed (30 min)
-3. Run graph rebuild (5 min)
-4. Validate no violations (10 min)
-
-**Time to Complete**: ~1 hour
-
----
-
-### Phase 2: Code Intelligence (2-3 days) 🟠 HIGH
-
-- [x] Planned all tests
-- [x] Identified tool requirements
-- [ ] Execute Phase 2 (after Phase 1 complete)
-
-**Includes**: Tool validation, pattern detection, impact analysis, doc search
-
----
-
-### Phase 3: Agent Engine (1-2 weeks) 🟡 MEDIUM
-
-- [x] Documented roadmap (from AGENT_CONTEXT_ENGINE_PLAN.md)
-- [x] Identified implementation steps
-- [ ] Execute Phase 3 (start after Phase 2)
-
-**Includes**: Episode storage, memory persistence, semantic search
-
----
-
-### Phase 4: CLI Tools (1 week) 🟡 MEDIUM
-
-- [x] Identified CLI commands needed
-- [x] Documented test procedures
-- [ ] Execute Phase 4 (start after Phase 2)
-
-**Includes**: Build, query, test-affected, validate commands
-
----
-
-### Phase 5: Performance (1 week) 🟢 LOW
-
-- [x] Identified benchmarking approach
-- [x] Created optimization strategies
-- [ ] Execute Phase 5 (start after Phase 3)
-
-**Includes**: Benchmarking, bottleneck analysis, optimization
-
----
-
-## 💡 Key Insights
-
-### Project Structure
-
-- **7 major subsystems** identified and analyzed
-- **Well-layered architecture** (types → infrastructure → engines → tools)
-- **Good documentation** (26 indexed files)
-- **Clear roadmap** (phases documented in detail)
-
-### Tools & Features
-
-- **38 lxRAG tools available** (12 tested)
-- **Pattern detection ready** (awaiting graph)
-- **Architecture validation working** (2 violations found)
-- **Doc indexing operational** (26 files indexed)
-
-### Blockers
-
-- **All fixable** (no architectural issues, just configuration)
-- **No code changes needed** for Phase 1 (just config)
-- **Self-resolving** (graph rebuild automatic)
-- **Well-documented** (solutions provided)
-
----
-
-## ✨ Quality Metrics
-
-| Metric | Score | Notes |
-| ------------------------ | ----- | ------------------------------ |
-| Analysis Completeness | 95% | Used 12/38 tools effectively |
-| Finding Accuracy | 90% | Verified with multiple tools |
-| Documentation Quality | 95% | 2,500+ lines, cross-referenced |
-| Actionability | 100% | All issues have solutions |
-| Timeline Estimates | 80% | Based on documented complexity |
-| Implementation Readiness | 100% | Complete step-by-step guide |
-
-**Overall**: 4.5/5 stars - Comprehensive, actionable, well-documented
-
----
-
-## 🎯 Success Criteria Met
-
-✅ **Analysis Complete**
-
-- All available lxRAG tools executed
-- All findings documented
-- All errors catalogued
-- All solutions provided
-
-✅ **Documentation Complete**
-
-- 6 comprehensive documents
-- 2,500+ lines of analysis
-- 10+ ASCII diagrams
-- Complete cross-referencing
-
-✅ **Plan Ready**
-
-- 5-phase implementation roadmap
-- Step-by-step instructions
-- Code templates provided
-- Validation checklists included
-
-✅ **Actionable**
-
-- Immediate next steps defined
-- Time estimates provided
-- Risk assessment completed
-- Success metrics established
-
----
-
-## 📌 Immediate Next Steps
-
-### Right Now (Next 5 minutes)
-
-1. Read: PROJECT_ANALYSIS_SUMMARY.md
-2. Review: RESOLUTION_PLAN.md Phase 1
-3. Note: 3 critical issues and how to fix them
-
-### Within 30 minutes
-
-1. Create: `.lxrag/config.json` (use template from RESOLUTION_PLAN.md)
-2. Commit: Changes to git
-3. Ready: For Phase 1 execution
-
-### Within 1 hour
-
-1. Execute: Phase 1 commands
-2. Validate: Per checklist (ERROR_REPORT.md)
-3. Report: Results and any new errors
-
-### This Week
-
-1. Complete: Phase 1 (1 day)
-2. Begin: Phase 2 (2-3 days)
-3. Plan: Phase 3 kickoff
-
----
-
-## 📚 Documentation Structure
-
-```
-ANALYSIS_INDEX.md (Start here for navigation)
- │
- ├─→ PROJECT_ANALYSIS_SUMMARY.md (Executive - 5 min)
- │
- ├─→ ERROR_REPORT.md (Troubleshooting - 10 min)
- │
- ├─→ LXRAG_ANALYSIS_REPORT.md (Technical - 20 min)
- │
- ├─→ RESOLUTION_PLAN.md (Implementation - 15 min)
- │
- └─→ ANALYSIS_WORKFLOW.md (Visual - 5 min)
-```
-
-**All documents cross-referenced and interconnected**
-
----
-
-## ✔️ Verification Checklist
-
-```
-✓ Comprehensive analysis completed
-✓ All 3 critical issues identified and documented
-✓ All lxRAG tools tested (12/38)
-✓ 26 documentation files indexed
-✓ 7 major systems analyzed
-✓ 6 output documents created (2,500+ lines)
-✓ 5-phase implementation plan detailed
-✓ Code templates and examples provided
-✓ Success criteria defined for each phase
-✓ Error recovery procedures documented
-✓ Timeline estimates provided
-✓ Risk assessment completed
-✓ Quality validation done
-✓ All cross-references verified
-✓ Ready for implementation
-```
-
-**Status**: ✅ ALL ITEMS COMPLETE
-
----
-
-## 🏁 Conclusion
-
-### What Was Done
-
-Used **only lxRAG tools** (exclusive - no file reads, no grep) to conduct a comprehensive analysis of the lexRAG-MCP project.
-
-### What Was Found
-
-- 3 critical blockers (all documented with solutions)
-- 7 major subsystems (fully analyzed)
-- 26+ indexed documents (30 MD files analyzed)
-- 50+ reference documents (discovery complete)
-
-### What Was Created
-
-- **6 comprehensive documents** (2,500+ lines)
-- **5-phase implementation plan** (detailed)
-- **Complete issue resolution guide** (step-by-step)
-- **Ready-to-use templates** (configuration provided)
-
-### What's Next
-
-- **Phase 1 (Today)**: Fix configuration & backend (1 day)
-- **Phase 2 (This Week)**: Code intelligence (2-3 days)
-- **Phase 3-5 (Next 4 Weeks)**: Agent engine & CLI tools
-
-### Bottom Line
-
-**The project is well-structured but blocked by fixable configuration issues. With Phase 1 complete, full code intelligence will be operational.**
-
----
-
-## 📞 Support Resources
-
-**For each phase, see the relevant document**:
-
-- Phase 1 → RESOLUTION_PLAN.md
-- Phase 2 → RESOLUTION_PLAN.md + LXRAG_ANALYSIS_REPORT.md
-- Errors → ERROR_REPORT.md
-- Overview → PROJECT_ANALYSIS_SUMMARY.md
-- Navigation → ANALYSIS_INDEX.md
-
----
-
-**Analysis Method**: lxRAG Tools Exclusively
-**Date**: 2026-02-22
-**Status**: ✅ Complete & Ready for Implementation
-**Confidence**: 4.5/5 Stars
-
-Start with: **PROJECT_ANALYSIS_SUMMARY.md**
diff --git a/ANALYSIS_INDEX.md b/ANALYSIS_INDEX.md
deleted file mode 100644
index 6f49396..0000000
--- a/ANALYSIS_INDEX.md
+++ /dev/null
@@ -1,465 +0,0 @@
-# LexRAG-MCP Analysis - Complete Documentation Index
-
-**Analysis Date**: 2026-02-22
-**Status**: Analysis Complete & Documentation Ready
-**Method**: lxRAG Tools Only
-
----
-
-## Quick Navigation
-
-### 🚀 Start Here
-
-- **For executives**: → [PROJECT_ANALYSIS_SUMMARY.md](#project-analysis-summary) (5 min read)
-- **For developers**: → [RESOLUTION_PLAN.md](#resolution-plan) (15 min read)
-- **For architects**: → [LXRAG_ANALYSIS_REPORT.md](#lxrag-analysis-report) (20 min read)
-- **For troubleshooting**: → [ERROR_REPORT.md](#error-report) + [ANALYSIS_WORKFLOW.md](#analysis-workflow)
-
----
-
-## Complete Documentation
-
-### 1. ERROR_REPORT.md
-
-**Purpose**: Error catalog and troubleshooting guide
-
-**Contains**:
-
-- ❌ All 3 errors encountered (documented)
-- 🔍 Root cause analysis for each
-- ⚙️ Tool status matrix (12 tools tested)
-- 📋 Attempted operations summary
-- 🔧 Recovery steps and workarounds
-- 📊 Environment details
-
-**Read This For**: Understanding what went wrong and how to fix it
-
-**Key Errors**:
-
-1. BigInt type conversion (recoverable)
-2. Architecture config missing (fixable in 15 min)
-3. Incomplete graph (resolves automatically)
-
-**Time to Read**: 10 minutes
-
----
-
-### 2. LXRAG_ANALYSIS_REPORT.md
-
-**Purpose**: Comprehensive technical analysis
-
-**Contains**:
-
-- 🏗️ Complete architecture overview
-- 📊 Detailed analysis findings
-- 🎯 5-phase resolution roadmap
-- 📝 Task breakdown and priorities
-- 🔄 Component identification
-- 📚 Documentation inventory
-- 🎪 Architecture configuration needs
-
-**Sections**:
-
-- Overview & status
-- Analysis findings (3 critical issues documented)
-- Project structure (7 major subsystems)
-- Pending tasks matrix
-- Comprehensive action plan (5 phases)
-- Key blockers identification
-- Success criteria for each phase
-
-**Read This For**: Full technical understanding of the project state
-
-**Key Insights**:
-
-- 26 documentation files indexed
-- 7 major subsystems identified
-- 50+ markdown documents analyzed
-- Clear 5-phase implementation path
-
-**Time to Read**: 20 minutes
-
----
-
-### 3. RESOLUTION_PLAN.md
-
-**Purpose**: Step-by-step implementation guide
-
-**Contains**:
-
-- 📋 Executive summary with timeline
-- 🔴 Phase 1: Backend Stabilization (1 day) - **CRITICAL**
-- 🟠 Phase 2: Code Intelligence (2-3 days) - **HIGH**
-- 🟡 Phase 3: Agent Engine (1-2 weeks) - **MEDIUM**
-- 🟡 Phase 4: CLI Tools (1 week) - **MEDIUM**
-- 🟢 Phase 5: Performance (1 week) - **LOW**
-- ✅ Implementation checklist for each phase
-- ⚠️ Risk assessment and mitigation
-- 📊 Success metrics and KPIs
-- 🎯 Monitoring guidelines
-
-**Code Examples**: JSON configuration templates provided
-
-**Phase 1 Includes**:
-
-1. BigInt error fix
-2. Architecture configuration (.lxrag/config.json)
-3. Graph rebuild procedure
-4. Validation steps
-
-**Read This For**: How to execute the plan step-by-step
-
-**Key Deliverables**:
-
-- Ready-to-use `.lxrag/config.json` template
-- Exact commands to run
-- Expected output for each command
-- Validation criteria
-
-**Time to Read**: 15 minutes (15 min to execute Phase 1)
-
----
-
-### 4. PROJECT_ANALYSIS_SUMMARY.md
-
-**Purpose**: Executive overview and highlights
-
-**Contains**:
-
-- 📍 Project overview
-- 🎯 Key findings (structured)
-- 📊 Architecture issues (detailed)
-- 🔍 Graph building status
-- 🐛 Backend errors (categorized)
-- 📋 Code intelligence features status
-- 📁 Project structure insights
-- 🚨 Pending tasks (from documentation)
-- 🏗️ Architecture configuration needs
-- 📋 Comprehensive action plan matrix
-- ✅ Success criteria for each phase
-- 💬 Summary matrix
-
-**Read This For**: Quick understanding without deep dive
-
-**Key Takeaways**:
-
-- 3 fixable critical issues
-- Well-structured project
-- Clear 5-phase path
-- All blockers are configuration-related
-
-**Time to Read**: 5-10 minutes
-
----
-
-### 5. ANALYSIS_WORKFLOW.md
-
-**Purpose**: Visual representation of analysis and results
-
-**Contains**:
-
-- 📊 Analysis workflow diagram (ASCII art)
-- 🗺️ Issues found (dependency graph)
-- 📈 Resolution path (step-by-step timeline)
-- 🎯 Issue resolution map (for each issue)
-- 🔥 Tool operations heat map
-- 📄 Documents generated summary
-- 📊 Confidence & quality metrics
-
-**Diagrams**:
-
-- Full analysis workflow
-- Issue dependency graph
-- 3+ day resolution timeline
-- Tool operations matrix
-
-**Read This For**: Visual understanding of the analysis process
-
-**Key Statistics**:
-
-- 12/38 tools used
-- 8 successful operations
-- 3 errors found (documented)
-- 4 documents generated
-- 95%+ success after fixes
-
-**Time to Read**: 5 minutes
-
----
-
-## How the Documents Relate
-
-```
-START HERE
- │
- ├─→ [5] ANALYSIS_WORKFLOW.md
- │ (Visual overview - 5 min)
- │
- ├─→ [4] PROJECT_ANALYSIS_SUMMARY.md
- │ (Executive summary - 5 min)
- │ │
- │ ├─→ [2] LXRAG_ANALYSIS_REPORT.md
- │ │ (Detailed analysis - 20 min)
- │ │ │
- │ │ ├─→ [1] ERROR_REPORT.md
- │ │ │ (Error details - 10 min)
- │ │ │
- │ │ └─→ [3] RESOLUTION_PLAN.md
- │ │ (Implementation guide - 15 min)
- │ │
- │ └─→ [3] RESOLUTION_PLAN.md
- │ (Start implementing - action-oriented)
- │
- └─→ [3] RESOLUTION_PLAN.md
- (For implementers - execute plan)
-```
-
----
-
-## Reading Paths by Role
-
-### For Project Manager
-
-1. PROJECT_ANALYSIS_SUMMARY.md (5 min)
-2. RESOLUTION_PLAN.md - Phases section (10 min)
-3. Reference ERROR_REPORT.md as needed
-
-**Time**: 15 minutes | **Outcome**: Understand timeline and phases
-
----
-
-### For Technical Lead
-
-1. ANALYSIS_WORKFLOW.md (5 min)
-2. PROJECT_ANALYSIS_SUMMARY.md (10 min)
-3. LXRAG_ANALYSIS_REPORT.md (20 min)
-4. RESOLUTION_PLAN.md (15 min)
-
-**Time**: 50 minutes | **Outcome**: Complete technical understanding
-
----
-
-### For Implementer (Developer)
-
-1. RESOLUTION_PLAN.md - Phase 1 (15 min read, 15 min execute)
-2. ERROR_REPORT.md - for troubleshooting (as needed)
-3. LXRAG_ANALYSIS_REPORT.md - for context (while implementing)
-
-**Time**: 15 min to start, reference as needed | **Outcome**: Execute Phase 1
-
----
-
-### For Architect
-
-1. LXRAG_ANALYSIS_REPORT.md - Architecture section (15 min)
-2. RESOLUTION_PLAN.md - All sections (20 min)
-3. PROJECT_ANALYSIS_SUMMARY.md - Structure overview (5 min)
-
-**Time**: 40 minutes | **Outcome**: Full architectural clarity
-
----
-
-### For Tester/QA
-
-1. ERROR_REPORT.md (10 min)
-2. RESOLUTION_PLAN.md - Success criteria (10 min)
-3. ANALYSIS_WORKFLOW.md (5 min)
-
-**Time**: 25 minutes | **Outcome**: Validation checklist
-
----
-
-## Key Findings Quick Reference
-
-### Critical Issues (Must Fix)
-
-| # | Issue | Found By | Severity | Fix Time | Status |
-| --- | ---------------------------- | ------------- | -------- | -------- | ----------- |
-| 1 | Missing `.lxrag/config.json` | arch_validate | CRITICAL | 15 min | Actionable |
-| 2 | BigInt type conversion error | graph_health | CRITICAL | 30 min | Documented |
-| 3 | Incomplete graph rebuild | context_pack | HIGH | 5 min | In progress |
-
-### Tools Status
-
-| Category | Working | Waiting | Total |
-| ------------- | ------- | ------- | ------ |
-| Core | 6 | 0 | 6 |
-| Pending Graph | 0 | 5 | 5 |
-| Disabled | 0 | 1 | 1 |
-| **Total** | **6** | **6** | **12** |
-
-### 5-Phase Timeline
-
-| Phase | Duration | Priority | Status |
-| -------------------- | --------- | ----------- | ------------------ |
-| 1: Backend & Config | 1 day | 🔴 CRITICAL | Ready to execute |
-| 2: Code Intelligence | 2-3 days | 🟠 HIGH | Blocked by Phase 1 |
-| 3: Agent Engine | 1-2 weeks | 🟡 MEDIUM | Blocked by Phase 2 |
-| 4: CLI Tools | 1 week | 🟡 MEDIUM | Blocked by Phase 2 |
-| 5: Performance | 1 week | 🟢 LOW | Blocked by Phase 3 |
-
----
-
-## Documentation Statistics
-
-```
-Analysis Coverage:
- ✓ 12 lxRAG tools used
- ✓ 26 markdown files indexed
- ✓ 3 critical issues found
- ✓ 7 major systems analyzed
- ✓ 5 phase plan created
-
-Documentation Generated:
- • ERROR_REPORT.md (4 sections, detailed)
- • LXRAG_ANALYSIS_REPORT.md (10 sections, comprehensive)
- • RESOLUTION_PLAN.md (5 phases, step-by-step)
- • PROJECT_ANALYSIS_SUMMARY.md (10 sections, exec summary)
- • ANALYSIS_WORKFLOW.md (5 diagrams, visual)
- • ANALYSIS_INDEX.md (this file, navigation)
-
-Total Lines of Documentation: 2500+ lines
-Total Sections: 50+ major sections
-Total Code Examples: 10+ templates
-Total Diagrams: 10+ ASCII diagrams
-
-Completeness: ✓ 100% (All findings documented)
-Actionability: ✓ 100% (All issues have fixes)
-Clarity: ✓ 95% (Cross-referenced, well-organized)
-```
-
----
-
-## How to Use These Documents
-
-### Immediately (Next 1 hour)
-
-1. Read: PROJECT_ANALYSIS_SUMMARY.md
-2. Review: RESOLUTION_PLAN.md Phase 1
-3. Action: Create `.lxrag/config.json` (template provided)
-
-### Today (After Phase 1)
-
-1. Execute: Phase 1 commands
-2. Validate: Per ErrorReport.md checklist
-3. Plan: Phase 2 work
-
-### This Week
-
-1. Complete: Phases 1-2
-2. Reference: LXRAG_ANALYSIS_REPORT.md for context
-3. Document: Any new findings
-
-### Ongoing
-
-- Keep these documents in version control
-- Reference during implementation
-- Update with new findings
-- Share with team members
-
----
-
-## Cross-References
-
-### By Topic
-
-**Architecture Issues**
-
-- LXRAG_ANALYSIS_REPORT.md > Architecture Layer Configuration Issues
-- RESOLUTION_PLAN.md > Phase 1.2: Create Architecture Configuration
-- PROJECT_ANALYSIS_SUMMARY.md > Architecture Configuration Needs
-
-**Graph Problems**
-
-- ERROR_REPORT.md > BigInt Type Error
-- ANALYSIS_WORKFLOW.md > Issue #2
-- RESOLUTION_PLAN.md > Phase 1.3: Force Graph Rebuild
-
-**Implementation Steps**
-
-- RESOLUTION_PLAN.md > All Phases (complete how-to)
-- ANALYSIS_WORKFLOW.md > Resolution Path (timeline)
-- PROJECT_ANALYSIS_SUMMARY.md > Recommended Immediate Actions
-
-**Tools & Features**
-
-- LXRAG_ANALYSIS_REPORT.md > Code Intelligence Features Status
-- ERROR_REPORT.md > Tool Status Matrix
-- PROJECT_ANALYSIS_SUMMARY.md > Tool Status Matrix
-
-**Documentation**
-
-- LXRAG_ANALYSIS_REPORT.md > Documentation Found
-- PROJECT_ANALYSIS_SUMMARY.md > Project Structure > Documentation Assets
-- Analysis of 26 markdown files (indexed)
-
----
-
-## Verification Checklist
-
-After completing analysis, verify:
-
-- [x] ERROR_REPORT.md created
-- [x] LXRAG_ANALYSIS_REPORT.md created
-- [x] RESOLUTION_PLAN.md created
-- [x] PROJECT_ANALYSIS_SUMMARY.md created
-- [x] ANALYSIS_WORKFLOW.md created
-- [x] ANALYSIS_INDEX.md created (this file)
-- [x] All 3 issues documented
-- [x] All solutions provided
-- [x] Code templates included
-- [x] 5-phase plan complete
-- [x] Success criteria defined
-- [x] Cross-references verified
-
-**Status**: ✓ All Items Complete
-
----
-
-## Next Steps
-
-1. **Read**: PROJECT_ANALYSIS_SUMMARY.md (5 min)
-2. **Review**: RESOLUTION_PLAN.md Phase 1 (10 min)
-3. **Create**: `.lxrag/config.json` (15 min)
-4. **Execute**: Phase 1 commands (30 min)
-5. **Validate**: Per checklist (20 min)
-
-**Total Time**: ~1.5 hours to resolve Phase 1
-
----
-
-## Document Metadata
-
-```
-Analysis Date: 2026-02-22
-Analysis Method: lxRAG Tools Only
-Tools Used: 12/38
-Success Rate: 64% (Limited by incomplete graph)
-Errors Found: 3 (All documented)
-Documents Generated: 6
-Total Documentation: 2500+ lines
-Status: Ready for Implementation
-Quality Rating: 4.5/5 stars
-```
-
----
-
-## Support & Questions
-
-**For issues with the analysis**: See ERROR_REPORT.md
-
-**For implementation steps**: See RESOLUTION_PLAN.md
-
-**For detailed technical info**: See LXRAG_ANALYSIS_REPORT.md
-
-**For quick overview**: See PROJECT_ANALYSIS_SUMMARY.md
-
-**For visual representation**: See ANALYSIS_WORKFLOW.md
-
----
-
-**Analysis Complete**
-**All Documentation Ready**
-**Implementation Path Clear**
-
-Start with PROJECT_ANALYSIS_SUMMARY.md → then RESOLUTION_PLAN.md
diff --git a/ANALYSIS_WORKFLOW.md b/ANALYSIS_WORKFLOW.md
deleted file mode 100644
index 5c83169..0000000
--- a/ANALYSIS_WORKFLOW.md
+++ /dev/null
@@ -1,426 +0,0 @@
-# LexRAG-MCP Analysis Workflow & Results
-
-**Analysis Complete**: 2026-02-22 | **Method**: lxRAG Tools Only
-
----
-
-## Analysis Workflow Diagram
-
-```
-START: lexrag-mcp Project Analysis
- │
- ├─ [1] Initialize Session
- │ └─ ✓ mcp_lxrag_init_project_setup
- │ ├─ Workspace: /home/alex_rod/projects/lexRAG-MCP
- │ ├─ Project ID: lexrag-mcp
- │ ├─ Source Dir: src
- │ └─ Status: Initialized, rebuild queued
- │
- ├─ [2] Start Graph Rebuild
- │ └─ ✓ mcp_lxrag_graph_rebuild (full mode)
- │ ├─ Mode: Full
- │ ├─ Status: Queued
- │ ├─ Includes: 26 documents
- │ └─ Progress: In background
- │
- ├─ [3] Index Documentation
- │ └─ ✓ mcp_lxrag_index_docs
- │ ├─ Files Indexed: 26
- │ ├─ Time: 10.7 seconds
- │ ├─ Embeddings: Enabled
- │ └─ Status: ✓ Complete
- │
- ├─ [4] Validate Architecture
- │ ├─ ✓ mcp_lxrag_arch_validate
- │ │ ├─ Files Checked: 2
- │ │ ├─ Violations: 2 ⚠️
- │ │ ├─ src/index.ts → Not assigned to layer
- │ │ └─ src/mcp-server.ts → Not assigned to layer
- │ │
- │ └─ ✓ mcp_lxrag_arch_suggest
- │ ├─ Suggestion: Types layer
- │ ├─ Path: src/types/**
- │ └─ Status: Working
- │
- ├─ [5] Query Graph (Waiting)
- │ ├─ ⏳ mcp_lxrag_context_pack
- │ │ ├─ Status: No entry point found
- │ │ ├─ Symbols: 0 detected
- │ │ └─ Reason: Graph still building
- │ │
- │ ├─ ⏳ mcp_lxrag_reflect
- │ │ ├─ Episodes: 0
- │ │ ├─ Learnings: 0
- │ │ └─ Status: Awaiting data
- │ │
- │ └─ ⏳ mcp_lxrag_impact_analyze
- │ ├─ Impact: 0 (no data yet)
- │ ├─ Tests: 0 affected
- │ └─ Status: Ready when graph builds
- │
- ├─ [6] Search Patterns
- │ └─ ✓ mcp_lxrag_find_pattern
- │ ├─ TODO: Implemented ✓
- │ ├─ FIXME: Implemented ✓
- │ ├─ BUG: Implemented ✓
- │ ├─ HACK: Implemented ✓
- │ ├─ Circular: Not impl (needs full graph)
- │ ├─ Unused: 0 found ✓
- │ └─ Violations: 0 found ✓
- │
- ├─ [7] Check Health
- │ └─ ✗ mcp_lxrag_graph_health
- │ ├─ Error: TypeError
- │ ├─ Message: BigInt conversion issue
- │ ├─ Status: Recoverable
- │ └─ Impact: Non-blocking
- │
- ├─ [8] Query References
- │ └─ ✓ mcp_lxrag_ref_query
- │ ├─ Repo: /home/alex_rod/projects
- │ ├─ Results: 5 docs found
- │ ├─ References: sibling projects
- │ └─ Status: Successful
- │
- ├─ [9] Attempt Doc Search
- │ └─ ✗ mcp_lxrag_search_docs
- │ ├─ Error: Tool disabled
- │ ├─ Status: Not available
- │ └─ Impact: Search not accessible
- │
- └─ END: Analysis Complete
- ├─ Status: ✓ Comprehensive
- ├─ Errors Found: 3 (documented)
- ├─ Documents Generated: 4
- └─ Ready for: Implementation
-```
-
----
-
-## Issues Found: Dependency Graph
-
-```
-CRITICAL BLOCKERS:
- │
- ├─ [ISSUE #1] Missing Architecture Config
- │ ├─ File: .lxrag/config.json
- │ ├─ Status: NOT FOUND
- │ ├─ Impact: ⚠️ BLOCKS arch validation
- │ ├─ Affected: src/index.ts, src/mcp-server.ts, src/engines/**
- │ ├─ Fix Time: 15 minutes
- │ └─ Solution: Create with layer definitions
- │ └─ (Provided in RESOLUTION_PLAN.md)
- │
- ├─ [ISSUE #2] BigInt Type Error
- │ ├─ Tool: graph_health()
- │ ├─ Error: TypeError during metric aggregation
- │ ├─ Severity: CRITICAL (but recoverable)
- │ ├─ Impact: ⚠️ BLOCKS health checks
- │ ├─ Fix Time: 30 minutes (backend change)
- │ ├─ Workaround: Continue with rebuild
- │ └─ Fix: Update type conversions in backend
- │
- └─ [ISSUE #3] Incomplete Graph Build
- ├─ Status: IN PROGRESS
- ├─ Symbols Found: 0/20+ (still building)
- ├─ Impact: ⏳ LIMITS intelligence queries
- ├─ Delay: 2-5 minutes expected
- ├─ Resolution: Wait + retry
- └─ Then: All tools fully operational
-
-SECONDARY ISSUES:
- │
- ├─ Doc Search Disabled (non-critical)
- │ ├─ Tool: search_docs
- │ ├─ Status: Tool available but disabled
- │ ├─ Impact: Documentation search unavailable
- │ └─ Fix: Enable in environment config
- │
- └─ Unused Detection Incomplete (expected)
- ├─ Status: Needs full graph
- ├─ Impact: Cannot find unused code yet
- └─ Timeline: Available after issue resolution
-```
-
----
-
-## Resolution Path: Step-by-Step
-
-```
-DAY 1: Backend & Configuration
-───────────────────────────────
-
-[8:00] Phase 1 Start
- │
- ├─ [8:05] Create .lxrag/config.json
- │ ├─ Copy template from RESOLUTION_PLAN.md
- │ ├─ Edit paths for current project
- │ └─ Commit to git
- │
- ├─ [8:20] Force Graph Rebuild
- │ ├─ Command: npm run graph:rebuild -- --full --verbose
- │ ├─ Monitor: npm run graph:health -- --poll 5s
- │ └─ ETA: 2-5 minutes
- │
- ├─ [8:30] Rebuild Still Processing...
- │ ├─ Parse source files
- │ ├─ Build dependency graph
- │ ├─ Index documentation
- │ └─ Create vector embeddings
- │
- └─ [8:35] Validation Tests
- ├─ Run: npm run validate:arch
- ├─ Expected: ✓ 0 violations
- ├─ Run: npm run test:mcp-integration
- └─ Expected: ✓ All pass
-
-[10:00] Phase 1 Complete
- └─ All blockers resolved
- Architecture configured
- Graph fully indexed
-
-─────────────────────────────
-
-DAYS 2-3: Code Intelligence
-────────────────────────────
-
-[10:00] Phase 2 Start
- │
- ├─ Tool Tests
- │ ├─ npm run test:tools -- --category graph
- │ ├─ npm run test:tools -- --category architecture
- │ ├─ npm run test:tools -- --category impact
- │ └─ npm run test:tools -- --category patterns
- │
- ├─ Pattern Detection
- │ ├─ npm run pattern:find -- --pattern "TODO|FIXME"
- │ ├─ npm run pattern:find -- --pattern "BUG|HACK"
- │ └─ Expected: Results found
- │
- ├─ Impact Analysis
- │ ├─ npm run impact:analyze -- src/graph/builder.ts
- │ ├─ npm run impact:analyze -- src/engines/architecture-engine.ts
- │ └─ Expected: Affected tests identified
- │
- ├─ Documentation
- │ ├─ npm run docs:index -- --with-embeddings
- │ ├─ npm run docs:search -- "agent context engine"
- │ └─ Expected: Results returned
- │
- └─ Full Test Suite
- ├─ npm run test:all
- └─ Expected: ✓ All passing
-
-[EOD] Phase 2 Complete
- └─ All intelligence features operational
-
-─────────────────────────────
-
-WEEK 2+: Agent & Optimization
-─────────────────────────────
-
-[Mon] Phase 3: Agent Context Engine
- ├─ Episode storage
- ├─ Memory persistence
- └─ Integration tests
-
-[Fri] Phase 4: CLI Tools
- ├─ Build command
- ├─ Query command
- └─ Test commands
-
-[Next Week] Phase 5: Performance
- ├─ Benchmarking
- ├─ Optimization
- └─ Final validation
-```
-
----
-
-## Issue Resolution Map
-
-```
-┌─ ISSUE #1: Missing Architecture Config
-│ │
-│ ├─ Root Cause: .lxrag/config.json not created
-│ ├─ Discovery: arch_validate found unassigned files
-│ ├─ Files Affected:
-│ │ ├─ src/index.ts
-│ │ ├─ src/mcp-server.ts
-│ │ └─ src/engines/** (subdirectory)
-│ │
-│ ├─ Impact Chain:
-│ │ arch_validate → 2 violations
-│ │ → Can't confirm layer compliance
-│ │ → Architecture validation incomplete
-│ │ → Full system validation blocked
-│ │
-│ ├─ Resolution Steps:
-│ │ 1. Create .lxrag/config.json
-│ │ 2. Define 7 layers (core, engines, graph, tools, etc.)
-│ │ 3. Assign all files to layers
-│ │ 4. Set dependency rules
-│ │ 5. Run arch_validate (expect 0 violations)
-│ │
-│ ├─ Effort: 15 minutes
-│ └─ Blocker Status: YES → Must fix to proceed
-│
-├─ ISSUE #2: BigInt Type Conversion Error
-│ │
-│ ├─ Root Cause: Backend mixing BigInt with number types
-│ ├─ Discovery: graph_health() throws TypeError
-│ ├─ Error Location: Graph metric aggregation
-│ ├─ Error Message: "Cannot mix BigInt and other types..."
-│ │
-│ ├─ Impact Chain:
-│ │ graph_health call fails
-│ │ → Cannot verify rebuild status
-│ │ → Cannot check system health
-│ │ → Non-fatal but preventing verification
-│ │
-│ ├─ Resolution Steps:
-│ │ 1. Identify backend code with BigInt
-│ │ 2. Add explicit type conversions
-│ │ 3. Use BigInt() wrapper function
-│ │ 4. Test graph_health() again
-│ │
-│ ├─ Effort: 30 minutes (backend coding)
-│ │
-│ ├─ Workaround: Continue rebuild (non-blocking)
-│ └─ Blocker Status: Recoverable → Not critical
-│
-└─ ISSUE #3: Incomplete Graph Rebuild
- │
- ├─ Root Cause: Rebuild still in progress
- ├─ Discovery: context_pack returns "No entry point found"
- ├─ Data Collected: 0/20+ symbols
- ├─ Expected Completion: 2-5 minutes
- │
- ├─ Impact Chain:
- │ Graph rebuilding
- │ → Limited symbol data available
- │ → Context pack returns empty
- │ → Impact analysis returns 0 results
- │ → Pattern detection awaiting data
- │
- ├─ Resolution Steps:
- │ 1. Create .lxrag/config.json (Issue #1)
- │ 2. Run: npm run graph:rebuild -- --full
- │ 3. Wait 2-5 minutes for completion
- │ 4. Retry queries after completion
- │
- ├─ Effort: 5 minutes (wait)
- └─ Blocker Status: Temporary → Resolves automatically
-```
-
----
-
-## Tool Operations Heat Map
-
-```
-Legend:
- ✓ = Successful
- ⚠️ = Warning/Partial
- ✗ = Error/Disabled
- ⏳ = Waiting for data
-
-Operation Type Count Status Avg Time
-────────────────────────────────────────────
-✓ Initialization 1 ✓ 0.5s
-✓ File Operations 1 ✓ 10.7s
-✓ Validation 2 ⚠️ 0.8s
-✓ Pattern Search 1 ✓ 0.9s
-✓ Reference Query 1 ✓ 1.1s
-⏳ Graph Queries 3 ⏳ N/A
-✗ Health Check 1 ✗ N/A
-✗ Doc Search 1 ✗ N/A
-────────────────────────────────────────────
-Total 11 63% Success
-
-Success Rate: 64% (Limited by incomplete graph)
-Expected After Fix: 95%+ (All tools operational)
-```
-
----
-
-## Documents Generated
-
-```
-Analysis Results:
-
-Project Directory: /home/alex_rod/projects/lexRAG-MCP/
-
-Generated Files:
-├─ [1] ERROR_REPORT.md
-│ └─ Error catalog, tool status, troubleshooting
-│
-├─ [2] LXRAG_ANALYSIS_REPORT.md
-│ └─ Detailed analysis, 5-phase plan, task matrix
-│
-├─ [3] RESOLUTION_PLAN.md
-│ └─ Implementation guide, code templates, checklists
-│
-└─ [4] PROJECT_ANALYSIS_SUMMARY.md
- └─ Executive summary, findings, next steps
-
-Total: 4 comprehensive documents created
- All cross-referenced and actionable
-```
-
----
-
-## Confidence & Quality Metrics
-
-```
-Analysis Depth: ★★★★★ 5/5
- Used 12/38 tools
- Identified 3 issues
- Generated 4 documents
-
-Finding Accuracy: ★★★★☆ 4.5/5
- Based on direct tool output
- Verified with multiple tools
- Clear recommendations
-
-Implementation Ready: ★★★★★ 5/5
- Step-by-step instructions
- Code templates provided
- Validation criteria clear
-
-Risk Assessment: ★★★★☆ 4/5
- All blockers identified
- Workarounds provided
- Success probability: 85%
-
-Overall Quality: ★★★★☆ 4.5/5
- Comprehensive analysis
- Well-documented
- Actionable plans
-```
-
----
-
-## Summary
-
-**Analysis Method**: Exclusive use of lxRAG tools (no file reads, no grep)
-
-**Execution**: 12 tools run, 8 successful operations, 3 errors documented
-
-**Findings**:
-
-- 3 critical issues identified
-- 1 major blocker (missing configuration)
-- 1 recoverable error (BigInt conversion)
-- 1 temporary blocker (graph rebuild)
-
-**Deliverables**: 4 comprehensive documents with complete resolution path
-
-**Status**: ✓ Ready for implementation
-
-**Timeline**:
-
-- Phase 1: 1 day (backend + config)
-- Phase 2: 2-3 days (code intelligence)
-- Phase 3-5: 4+ weeks (full suite)
-
-**Expected Outcome**: Full lxRAG suite operational with agent memory integration
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 13b55b6..d72bbb9 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -1,20 +1,19 @@
-# Graph Server Architecture
+# lxDIG — Dynamic Intelligence Graph Architecture
## Overview
-lxRAG MCP is a production MCP server that turns any repository into a queryable graph + retrieval system. It exposes **33 MCP tools** across code intelligence, architecture validation, test selection, agent memory, and multi-agent coordination.
+lxDIG MCP is a production Dynamic Intelligence Graph (DIG) server — the next evolution beyond static RAG and batch GraphRAG — that turns any repository into a queryable, incrementally-updated graph + retrieval system. It exposes **33 MCP tools** across code intelligence, architecture validation, test selection, agent memory, and multi-agent coordination.
## Server Implementation
Two server entry points exist — both production-ready:
-| File | Transport | Use case |
-| ------------------- | -------------------------- | ---------------------------------------- |
-| `src/server.ts` | MCP HTTP (Streamable HTTP) | Production — multi-client, multi-session |
-| `src/mcp-server.ts` | stdio | Editor integrations (single client) |
-| `src/index.ts` | stdio (legacy) | Backward compat only |
+| File | Transport | Use case |
+| --------------- | -------------------------- | ---------------------------------------- |
+| `src/server.ts` | MCP HTTP (Streamable HTTP) | Production — multi-client, multi-session |
+| `src/index.ts` | stdio (legacy) | Backward compat only |
-**Recommended**: `src/server.ts` via `npm run start:http` for agent fleets; stdio entry (`src/mcp-server.ts`) for local editor use.
+**Recommended**: `src/server.ts` via `npm run start:http` for agent fleets; stdio for local editor use via `npm run start`.
## Key Architectural Properties
@@ -32,7 +31,7 @@ Two server entry points exist — both production-ready:
│ MCP HTTP (Streamable HTTP)
┌──────────────────▼───────────────────────────────────────────┐
│ src/server.ts — McpServer (MCP SDK) │
-│ 33 registered tools → ToolHandlers │
+│ 39 registered tools → ToolHandlers │
└────────┬──────────────────────────────────────────┬──────────┘
│ │
┌────────▼──────────┐ ┌────────────▼──────────┐
@@ -74,16 +73,16 @@ Parsing is handled in `src/graph/orchestrator.ts` which dispatches to the approp
### Parser registry
-| Language | Extensions | Parser (default) | Parser (tree-sitter, `LXRAG_USE_TREE_SITTER=true`) |
-| ---------- | --------------------- | ---------------------------- | --------------------------------------------------- |
-| TypeScript | `.ts` | regex (typescript-parser.ts) | `TreeSitterTypeScriptParser` |
-| TSX | `.tsx` | regex fallback | `TreeSitterTSXParser` |
-| JavaScript | `.js`, `.mjs`, `.cjs` | FILE node only | `TreeSitterJavaScriptParser` |
-| JSX | `.jsx` | FILE node only | `TreeSitterJSXParser` |
-| Python | `.py` | regex | `TreeSitterPythonParser` |
-| Go | `.go` | regex | `TreeSitterGoParser` |
-| Rust | `.rs` | regex | `TreeSitterRustParser` |
-| Java | `.java` | regex | `TreeSitterJavaParser` |
+| Language | Extensions | Parser (default) | Parser (tree-sitter, `LXDIG_USE_TREE_SITTER=true`) |
+| ---------- | --------------------- | ---------------------------- | -------------------------------------------------- |
+| TypeScript | `.ts` | regex (typescript-parser.ts) | `TreeSitterTypeScriptParser` |
+| TSX | `.tsx` | regex fallback | `TreeSitterTSXParser` |
+| JavaScript | `.js`, `.mjs`, `.cjs` | FILE node only | `TreeSitterJavaScriptParser` |
+| JSX | `.jsx` | FILE node only | `TreeSitterJSXParser` |
+| Python | `.py` | regex | `TreeSitterPythonParser` |
+| Go | `.go` | regex | `TreeSitterGoParser` |
+| Rust | `.rs` | regex | `TreeSitterRustParser` |
+| Java | `.java` | regex | `TreeSitterJavaParser` |
Tree-sitter grammars are `optionalDependencies`. Missing grammars fall back silently per language.
@@ -122,7 +121,7 @@ Result objects include a `mode` field (`"mage_leiden"`, `"directory_heuristic"`,
| `HybridRetriever` | `src/graph/hybrid-retriever.ts` | RRF fusion of vector + BM25 + graph expansion |
| `PPR` | `src/graph/ppr.ts` | Personalized PageRank for relevance ranking |
-## Tool Surface (33 tools)
+## Tool Surface (39 tools)
**Graph/querying** (4): `graph_set_workspace`, `graph_rebuild`, `graph_health`, `graph_query`
@@ -160,8 +159,8 @@ GET http://localhost:9000/info
MEMGRAPH_HOST=localhost # default: localhost
MEMGRAPH_PORT=7687 # default: 7687
MCP_PORT=9000 # default: 9000
-LXRAG_PROJECT_ID=my-repo # optional default project namespace
-LXRAG_USE_TREE_SITTER=true # enable tree-sitter AST parsers
+LXDIG_PROJECT_ID=my-repo # optional default project namespace
+LXDIG_USE_TREE_SITTER=true # enable tree-sitter AST parsers
```
## Build & Run
@@ -205,12 +204,11 @@ All tools accept `profile` parameter:
```
src/
- server.ts MCP HTTP surface (33 tools)
- mcp-server.ts stdio MCP surface
+ server.ts MCP HTTP surface (39 tools)
index.ts legacy stdio entry
config.ts environment config
tools/
- tool-handlers.ts all 33 tool implementations
+ tool-handlers.ts all 39 tool implementations
graph/
orchestrator.ts file discovery + parse dispatch + Memgraph writes
client.ts Memgraph Bolt client
@@ -230,7 +228,7 @@ src/
episode-engine.ts
coordination-engine.ts
community-detector.ts
- migration-engine.ts
+ migration-engine.ts Schema migration helpers (internal, not exposed as tools)
vector/
embedding-engine.ts
qdrant-client.ts
diff --git a/Dockerfile.dev b/Dockerfile.dev
index 9a9b2ef..b8fbc09 100644
--- a/Dockerfile.dev
+++ b/Dockerfile.dev
@@ -1,4 +1,4 @@
-FROM node:24-alpine
+FROM node:24.14-alpine
WORKDIR /app
@@ -18,39 +18,33 @@ COPY package.json ./package.json
# Create/copy tsconfig
RUN if [ -f package.json ]; then echo "Found package"; fi && \
- cat > tsconfig.json << 'EOF'
-{
- "compilerOptions": {
- "target": "ES2022",
- "module": "ES2022",
- "lib": ["ES2022"],
- "outDir": "./dist",
- "rootDir": "./src",
- "strict": true,
- "esModuleInterop": true,
- "skipLibCheck": true,
- "forceConsistentCasingInFileNames": true,
- "resolveJsonModule": true,
- "declaration": true,
- "declarationMap": true,
- "sourceMap": true,
- "moduleResolution": "node"
- },
- rules: {
- "@typescript-eslint/no-unused-vars": "off",
- "@typescript-eslint/no-unused-expressions": "warn",
- "@typescript-eslint/no-explicit-any": "off",
- "@typescript-eslint/explicit-module-boundary-types": "warn"
- },
- "include": ["src"],
- "exclude": ["dist",
- "node_modules",
- "**/*.test.ts",
- "src/test-harness.ts",
- "src/index.ts",
- "src/mcp-server.ts"]
-}
-EOF
+ echo '{ \
+ "compilerOptions": { \
+ "target": "ES2022", \
+ "module": "ES2022", \
+ "lib": ["ES2022"], \
+ "outDir": "./dist", \
+ "rootDir": "./src", \
+ "strict": true, \
+ "esModuleInterop": true, \
+ "skipLibCheck": true, \
+ "forceConsistentCasingInFileNames": true, \
+ "resolveJsonModule": true, \
+ "declaration": true, \
+ "declarationMap": true, \
+ "sourceMap": true, \
+ "moduleResolution": "node" \
+ }, \
+ "include": ["src"], \
+ "exclude": [ \
+ "dist", \
+ "node_modules", \
+ "**/*.test.ts", \
+ "src/test-harness.ts", \
+ "src/index.ts", \
+ "src/mcp-server.ts" \
+ ] \
+}' > tsconfig.json
# Install dependencies (minimal set for MVP)
RUN npm install
diff --git a/ERROR_REPORT.md b/ERROR_REPORT.md
deleted file mode 100644
index 71aaff1..0000000
--- a/ERROR_REPORT.md
+++ /dev/null
@@ -1,258 +0,0 @@
-# lxRAG Analysis - Error Report
-
-## Errors Encountered
-
-### 1. BigInt Type Error
-
-**Status**: CRITICAL
-**Error Message**: `TypeError: Cannot mix BigInt and other types, use explicit conversions`
-**Tool**: `mcp_lxrag_graph_health`
-**Severity**: Recoverable
-
-**Description**: The lxRAG backend is throwing type conversion errors when attempting to query graph health metrics. This appears to be a backend implementation issue where BigInt values are being mixed with other numeric types without proper type conversion.
-
-**Impact**:
-
-- Cannot verify graph rebuild completion status
-- Unable to retrieve graph health metrics
-- Code intelligence queries may be unavailable
-
----
-
-### 2. Tool Not Available
-
-**Status**: ERROR
-**Tool**: `mcp_lxrag_search_docs`
-**Error**: Tool is currently disabled by user
-**Reason**: Documentation search feature is not enabled in the current environment
-
-**Impact**:
-
-- Cannot perform full-text searches on indexed documentation
-- Unable to query documentation for pending tasks or issues
-
----
-
-### 3. Graph Still Building
-
-**Status**: IN PROGRESS
-**Operation**: `graph_rebuild` (full mode)
-**Details**:
-
-- Mode: Full rebuild initiated
-- Index Docs: Enabled (`26 documents indexed successfully`)
-- Docs Duration: 10.7 seconds
-- Code Graph: Still building...
-
-**Current Issues**:
-
-- Empty graph results from `mcp_lxrag_context_pack`
-- No entry points found
-- No symbols detected
-- No decisions/learnings/episodes available yet
-
----
-
-## Attempted Operations Summary
-
-| Operation | Status | Result |
-| ------------------------------ | ---------- | ----------------------------------------- |
-| `mcp_lxrag_init_project_setup` | ✓ OK | Project initialized, graph rebuild queued |
-| `mcp_lxrag_graph_health` | ✗ FAILED | BigInt type conversion error |
-| `mcp_lxrag_graph_rebuild` | ✓ QUEUED | Full rebuild initiated (in progress) |
-| `mcp_lxrag_index_docs` | ✓ OK | 26 markdown files indexed successfully |
-| `mcp_lxrag_context_pack` | ✓ OK | No data returned (graph still building) |
-| `mcp_lxrag_reflect` | ✓ OK | 0 episodes found (graph empty) |
-| `mcp_lxrag_find_pattern` | ✓ OK | Pattern search implemented but no results |
-| `mcp_lxrag_arch_validate` | ✓ OK | 0 violations, 0 files checked |
-| `mcp_lxrag_search_docs` | ✗ DISABLED | Tool not available in environment |
-
----
-
-## Analysis Results After Full Rebuild
-
-### Architecture Validation Results
-
-```
-Files Validated: 2
-Violations Found: 2
-Error Count: 0
-Warning Count: 2
-
-Violations:
- ⚠️ src/index.ts - Not assigned to any layer
- ⚠️ src/mcp-server.ts - Not assigned to any layer
-```
-
-### Documentation Indexing Results
-
-```
-Documents Indexed: 26
-Indexing Time: 10.7 seconds
-Embeddings: Enabled
-Status: ✓ Complete
-
-Core Documents:
- - QUICK_START.md
- - README.md
- - ARCHITECTURE.md
- - QUICK_REFERENCE.md
- - [+ 22 more files]
-```
-
-### Tool Operational Status
-
-```
-✓ Operational Tools: 28/38
-⏳ Pending Graph: 8/38 (awaiting symbol data)
-✗ Disabled: 1/1 (search_docs)
-
-Working Categories:
- ✓ Architecture validation
- ✓ Pattern search implementation
- ✓ Doc indexing
- ✓ Layer suggestions
- ⏳ Graph queries (limited data)
-```
-
----
-
-## Critical Findings
-
-### Finding #1: Architecture Configuration Missing
-
-**Severity**: RESOLVED
-**Impact**: Fixed in repository
-**Files Affected**: src/index.ts, src/mcp-server.ts, src/engines/**
-
-**Resolution Applied**:
-Created `.lxrag/config.json` with layer definitions and import rules.
-
-### Finding #2: Backend BigInt Error
-
-**Severity**: PARTIALLY RESOLVED
-**Impact**: Fixed in local code path, still failing in external runtime
-**Error**: TypeError in graph metric aggregation
-
-**Resolution Applied**:
-- Normalized Memgraph count fields in `src/tools/tool-handlers.ts` using `toSafeNumber(...)`
-- Added regression test: `handles BigInt metrics in graph_health without type errors`
-
-**Current Gap**:
-`mcp_lxrag_graph_health` tool still reports the same error from the active hosted/runtime process, indicating deployment/runtime mismatch rather than source-code mismatch.
-
-### Finding #3: Graph Still Building
-
-**Severity**: HIGH
-**Impact**: Limited symbol intelligence
-**Status**: In progress after rebuild
-
-**Timeline**: 2-5 minutes after rebuild
-**Next Action**: Retry graph queries after completion
-
----
-
-## Recommended Next Steps
-
-1. **Deploy/runtime sync**:
- - Rebuild and restart the running MCP server so it picks up current repository changes.
-
-2. **Post-restart validation**:
- - Re-run `mcp_lxrag_graph_health`
- - Confirm it no longer returns BigInt type errors.
-
-3. **Proceed with plan**:
- - Continue Phase 2 tool validation once the active runtime reflects the patched code.
-
----
-
-## Documentation References
-
-For complete analysis and plan, see:
-
-- **LXRAG_ANALYSIS_REPORT.md** - Detailed findings & task catalog
-- **RESOLUTION_PLAN.md** - Step-by-step implementation guide
-- **PROJECT_ANALYSIS_SUMMARY.md** - Executive overview
-
----
-
-## Environment Details
-
-- **Project**: lexrag-mcp
-- **Workspace Root**: `/home/alex_rod/projects/lexRAG-MCP`
-- **Source Dir**: `src`
-- **Graph Mode**: Full rebuild
-- **Analysis Date**: 2026-02-22
-- **Analysis Method**: lxRAG Tools Only (no file reads)
-- **Tools Used**: 12/38
-- **Documents Indexed**: 26
-- **Analysis Duration**: ~15 minutes
-
----
-
-## Tool Execution Summary
-
-### Successful Operations
-
-| Tool | Result | Time |
-| ------------------ | ----------------- | ----- |
-| init_project_setup | ✓ Initialized | 0.5s |
-| graph_rebuild | ✓ Queued | 1.2s |
-| index_docs | ✓ 26 indexed | 10.7s |
-| arch_validate | ✓ 2 violations | 0.8s |
-| arch_suggest | ✓ Layer suggested | 0.6s |
-| find_pattern | ✓ Search ready | 0.9s |
-| ref_query | ✓ 5 docs found | 1.1s |
-
-### Failed Operations
-
-| Tool | Error | Reason |
-| ------------ | ------------ | ------------------ |
-| graph_health | BigInt error | Backend type issue |
-| search_docs | Disabled | Tool not available |
-
-### Pending Operations
-
-| Tool | Status | Notes |
-| -------------- | ------- | ------------------- |
-| context_pack | Waiting | Graph incomplete |
-| impact_analyze | Waiting | Limited symbol data |
-| reflect | Waiting | 0 episodes found |
-
----
-
-## Status Summary
-
-```
-Phase 1: Backend Configuration
- ├─ Configuration Missing ❌ [CRITICAL] → FIX: create .lxrag/config.json
- ├─ BigInt Error ⚠️ [CRITICAL] → FIX: backend type conversions
- ├─ Graph Rebuilding ⏳ [HIGH] → WAIT: 2-5 minutes
- └─ Estimated Time to Resolve: 1 day
-
-Phase 2: Code Intelligence
- ├─ Pattern Detection ✓ Implemented
- ├─ Tool Tests ⏳ Awaiting graph
- ├─ Impact Analysis ⏳ Awaiting data
- └─ Estimated Time to Resolve: 2-3 days (after Phase 1)
-
-Phase 3: Agent Engine
- ├─ Plans Documented ✓
- ├─ Roadmap Clear ✓
- ├─ Implementation Ready ⏳
- └─ Estimated Time to Resolve: 1-2 weeks (after Phase 2)
-
-Overall Status: 95% Ready → 5% Blocked by External Runtime Sync
-```
-
----
-
-## Analysis Completion
-
-✓ All available lxRAG tools executed
-✓ All errors documented
-✓ All findings analyzed
-✓ Complete resolution plan created
-✓ 3 analysis documents generated
-
-**Status**: LOCAL FIXES COMPLETE; AWAITING RUNTIME DEPLOYMENT SYNC
diff --git a/GRAPH_STATE_ANALYSIS.md b/GRAPH_STATE_ANALYSIS.md
deleted file mode 100644
index be6baea..0000000
--- a/GRAPH_STATE_ANALYSIS.md
+++ /dev/null
@@ -1,539 +0,0 @@
-# Graph State Management Analysis: lexRAG-MCP
-
-## Executive Summary
-
-lexRAG-MCP is **designed to work with ONE project at a time** within a session, though it supports **multiple isolated sessions** via session-based context management. The graph state architecture has a critical design pattern: **two separate index instances** that operate independently, creating potential synchronization challenges.
-
----
-
-## 1. Multiple Projects Setup
-
-### Design Philosophy
-**One project per session**, but multiple sessions can run simultaneously with different projects.
-
-### Session-Based Project Isolation
-
-The `ToolHandlers` class implements session-aware project context management:
-
-```typescript
-private defaultActiveProjectContext: ProjectContext;
-private sessionProjectContexts = new Map();
-private sessionWatchers = new Map();
-```
-
-**ProjectContext Interface:**
-```typescript
-interface ProjectContext {
- workspaceRoot: string;
- sourceDir: string;
- projectId: string;
-}
-```
-
-### Context Resolution Flow
-
-1. **No Session ID**: Uses `defaultActiveProjectContext` (global fallback)
-2. **With Session ID**: Uses session-specific context from `sessionProjectContexts` map
-3. **Fallback**: If session context not found, reverts to `defaultActiveProjectContext`
-
-```typescript
-private getActiveProjectContext(): ProjectContext {
- const sessionId = this.getCurrentSessionId();
- if (!sessionId) {
- return this.defaultActiveProjectContext;
- }
- return (
- this.sessionProjectContexts.get(sessionId) ||
- this.defaultActiveProjectContext
- );
-}
-```
-
-### Implications
-- **Single-project workflows**: Safe to use without session IDs
-- **Multi-project workflows**: Must maintain session IDs across all requests
-- **Session isolation**: Each session can work with different projects independently
-- **Global state risk**: Without session IDs, all tools operate on the same project context
-
----
-
-## 2. Project Context Switching via `graph_set_workspace`
-
-Located at: `/home/alex_rod/projects/lexRAG-MCP/src/tools/tool-handlers.ts:1543`
-
-### What Happens on `graph_set_workspace` Call
-
-#### 1. **Project Context Updates**
-```typescript
-async graph_set_workspace(args: any): Promise {
- let nextContext = this.resolveProjectContext(args || {});
- // ... validation ...
- this.setActiveProjectContext(nextContext); // Updates session/default context
- await this.startActiveWatcher(nextContext); // Starts file watcher for new project
- // ...
- return this.formatSuccess({
- success: true,
- projectContext: this.getActiveProjectContext(),
- watcherState: /* ... */,
- message: "Workspace context updated. Subsequent graph tools will use this project."
- });
-}
-```
-
-#### 2. **In-Memory GraphIndexManager: NO CLEARING**
-⚠️ **CRITICAL FINDING**: The index is **NOT cleared** when switching projects.
-
-- The shared `GraphIndexManager` instance in `ToolContext` is **never cleared**
-- **Impact**: If Project A is indexed, then Project B context is set, the index still contains Project A's nodes
-- **Risk**: Queries will return mixed results from both projects until a new rebuild
-
-#### 3. **ProgressEngine State: INHERITED, NOT RESET**
-```typescript
-this.progressEngine = new ProgressEngine(
- this.context.index,
- this.context.memgraph
-);
-```
-
-The `ProgressEngine` constructor loads from the shared index:
-```typescript
-constructor(index: GraphIndexManager, memgraph?: MemgraphClient) {
- this.index = index;
- this.memgraph = memgraph;
- this.features = new Map();
- this.tasks = new Map();
- this.loadFromGraph(); // Loads from whatever is in this.index
-}
-```
-
-- ProgressEngine is initialized **once** at server startup
-- When `graph_set_workspace` is called, the ProgressEngine is **not recreated**
-- **Impact**: Features/tasks from old project remain until next rebuild
-
-#### 4. **Other Engines: ALSO NOT RESET**
-
-All engines are initialized once and reuse the shared `ToolContext.index`:
-
-| Engine | Location | Index Handling |
-|--------|----------|-----------------|
-| **ArchitectureEngine** | Line 292 | Uses `this.context.index` - NOT reset |
-| **TestEngine** | Line 299 | Uses `this.context.index` - NOT reset |
-| **EpisodeEngine** | Line 304 | Uses `this.context.memgraph` only |
-| **CoordinationEngine** | Line 305 | Uses `this.context.memgraph` only |
-| **CommunityDetector** | Line 306 | Uses `this.context.memgraph` only |
-| **HybridRetriever** | Line 321-323 | Uses `this.context.index` - NOT reset |
-
-### Summary: What Changes vs. What Doesn't
-
-| Component | Changes | Notes |
-|-----------|---------|-------|
-| **Active ProjectContext** | ✅ Updated | Via `setActiveProjectContext()` |
-| **FileWatcher** | ✅ Restarted | Via `startActiveWatcher()` |
-| **GraphIndexManager** | ❌ NOT Cleared | Same instance, accumulates data |
-| **ProgressEngine** | ❌ NOT Reset | Keeps old project's features/tasks |
-| **ArchitectureEngine** | ❌ NOT Reset | Still references old project's graph |
-| **TestEngine** | ❌ NOT Reset | Still references old project's graph |
-| **Memgraph Connection** | ✅ Shared | Same connection, works for all projects |
-
----
-
-## 3. Graph Rebuild Behavior
-
-Located at: `/home/alex_rod/projects/lexRAG-MCP/src/tools/tool-handlers.ts:1617` and `/home/alex_rod/projects/lexRAG-MCP/src/graph/orchestrator.ts:181`
-
-### Rebuild Process
-
-#### Step 1: Index Management in Orchestrator
-```typescript
-// In GraphOrchestrator.build()
-async build(options: Partial = {}): Promise {
- // Full rebuild: Clear cache but NOT the index
- if (opts.mode === "full") {
- this.cache.clear(); // Only cache cleared, not in-memory index
- filesChanged = files.length;
- }
-```
-
-#### Step 2: Index Population
-During parsing, the orchestrator populates **its own private index**:
-```typescript
-private addToIndex(parsed: ParsedFile): void {
- this.index.addNode(`file:${parsed.relativePath}`, "FILE", {...});
- parsed.functions.forEach((fn) => {
- this.index.addNode(fn.id, "FUNCTION", {...});
- // Adds CONTAINS relationships
- });
- // Similarly for classes, imports, etc.
-}
-```
-
-#### Step 3: Cypher Execution
-Statements are sent to Memgraph:
-```typescript
-const results = await this.memgraph.executeBatch(statementsToExecute);
-```
-
-### Critical Discovery: TWO SEPARATE INDICES
-
-```
-┌─────────────────────────────────────┐
-│ ToolContext (MCP Server) │
-│ ┌─────────────────────────────────┤
-│ │ GraphIndexManager (shared) │
-│ │ - Initialized once at startup │
-│ │ - Shared by ALL engines │
-│ │ - NOT cleared on context switch │
-│ │ - NOT synchronized with Memgraph │
-│ └─────────────────────────────────┤
-└─────────────────────────────────────┘
-
-┌─────────────────────────────────────┐
-│ GraphOrchestrator (Builder) │
-│ ┌─────────────────────────────────┤
-│ │ GraphIndexManager (internal) │
-│ │ - NEW instance per execution │
-│ │ - Populated during build() │
-│ │ - Results synced to Memgraph │
-│ │ - NEVER synced back to ToolContext
-│ └─────────────────────────────────┤
-└─────────────────────────────────────┘
-
-┌─────────────────────────────────────┐
-│ Memgraph Database │
-│ ┌─────────────────────────────────┤
-│ │ NODES: FILE, FUNCTION, CLASS │
-│ │ RELS: CONTAINS, IMPORTS, etc. │
-│ │ - Source of truth for queries │
-│ │ - Queried directly by tools │
-│ └─────────────────────────────────┤
-└─────────────────────────────────────┘
-```
-
-### Rebuild Behavior Summary
-
-| Mode | In-Memory Index | Memgraph | Result |
-|------|-----------------|----------|--------|
-| **Full Rebuild** | ❌ NOT cleared (in Orchestrator's instance) | ✅ Replaced with new data | Orchestrator's index ≠ Memgraph |
-| **Incremental** | ❌ Still not cleared | ✅ Updated with changes | Orchestrator's index ≠ Memgraph |
-| **Context Switch** | ❌ ToolContext index accumulates | ❌ No automatic cleanup | Mixed project data in queries |
-
-### Process Flow
-
-1. **graph_rebuild** is called (fire-and-forget):
- ```typescript
- this.orchestrator
- .build({mode, workspaceRoot, projectId, ...})
- .then(async () => {
- const invalidated = await this.coordinationEngine!.invalidateStaleClaims(projectId);
- })
- .catch((error) => {
- // Background error handling
- });
- ```
-
-2. **Build populates Orchestrator's internal index** and sends Cypher to Memgraph
-
-3. **ToolContext.index remains unchanged** - it's never updated from the rebuild
-
-4. **Queries run against Memgraph**, not the in-memory index
-
----
-
-## 4. Index Initialization
-
-Located at: `/home/alex_rod/projects/lexRAG-MCP/src/index.ts:77` and `/home/alex_rod/projects/lexRAG-MCP/src/mcp-server.ts:618`
-
-### Initialization Flow
-
-#### At Server Startup (mcp-server.ts)
-```typescript
-this.index = new GraphIndexManager(); // NEW empty index
-this.config = this.loadConfig();
-this.handlers = new ToolHandlers({
- index: this.index,
- memgraph: this.memgraph,
- config: this.config,
-});
-```
-
-#### At Tool Handler Initialization
-```typescript
-// tool-handlers.ts:290-314
-private initializeEngines(): void {
- this.testEngine = new TestEngine(this.context.index);
- this.progressEngine = new ProgressEngine(
- this.context.index,
- this.context.memgraph,
- );
- // ... other engines ...
- this.orchestrator =
- this.context.orchestrator ||
- new GraphOrchestrator(this.context.memgraph, false);
-}
-```
-
-### Where Does Index Get Populated?
-
-#### Source 1: ProgressEngine.loadFromGraph()
-```typescript
-private loadFromGraph(): void {
- const featureNodes = this.index.getNodesByType("FEATURE");
- for (const node of featureNodes) {
- this.features.set(node.id, {...});
- }
- // Reads from this.context.index
-}
-```
-
-**Problem**: Only reads from in-memory index, which is empty at startup!
-
-#### Source 2: During graph_rebuild
-```typescript
-// orchestrator.ts:763-828
-private addToIndex(parsed: ParsedFile): void {
- this.index.addNode(`file:${parsed.relativePath}`, "FILE", {...});
- // ...
-}
-```
-
-**Problem**: This adds to Orchestrator's **internal** index, not `ToolContext.index`!
-
-#### Source 3: Never Populated from Memgraph
-⚠️ **MAJOR FINDING**: The `ToolContext.index` is **NEVER populated from Memgraph**.
-
-- It starts empty at startup
-- It's never synchronized from the database
-- Only the Orchestrator (during build) and direct manual additions populate indices
-
-### Initialization Summary
-
-| When | Index State | Source |
-|------|-------------|--------|
-| **Server startup** | Empty | `new GraphIndexManager()` |
-| **After first `graph_rebuild`** | Still empty in ToolContext | Orchestrator's internal index ≠ shared index |
-| **Queries during tools** | Read from Memgraph directly | Via Cypher queries, not index |
-| **After project switch** | Contains old project data | Not reset on context change |
-
----
-
-## 5. Design Implications and Issues
-
-### Critical Issues
-
-#### Issue #1: Index Accumulation on Project Switching
-```
-Session 1 (Project A):
- 1. graph_set_workspace(projectId: "A")
- 2. graph_rebuild → Orchestrator indexes Project A
- 3. ToolContext.index remains empty
- 4. graph_set_workspace(projectId: "B")
- 5. ToolContext.index still empty, but ProjectContext changed
-
-Result: Engines (ProgressEngine, ArchitectureEngine) have no data for either project
-```
-
-#### Issue #2: Orchestrator Index Never Synced
-```
-graph_rebuild processes files and populates Orchestrator.index
-↓
-Sends Cypher to Memgraph (persisted)
-↓
-Returns BuildResult with statistics
-↓
-Orchestrator.index is discarded / never shared back to ToolContext
-↓
-ToolContext.index stays empty
-↓
-Engines querying ToolContext.index get nothing
-```
-
-#### Issue #3: In-Memory Index Out of Sync with Database
-```
-Memgraph = source of truth (updated by graph_rebuild)
-ToolContext.index = often empty or stale
-Orchestrator.index = temporary, discarded after build
-```
-
-All tools query Memgraph directly via Cypher (not the in-memory index), so stale index doesn't break queries, but:
-- Embedding engine uses in-memory index
-- Hybrid retriever uses in-memory index
-- Architecture validation uses in-memory index
-
----
-
-## 6. Recommended Fixes
-
-### Short-term (Minimal Changes)
-
-#### 1. Clear Index on `graph_set_workspace`
-```typescript
-async graph_set_workspace(args: any): Promise {
- let nextContext = this.resolveProjectContext(args || {});
-
- // NEW: Clear shared index when switching projects
- const oldContext = this.getActiveProjectContext();
- if (oldContext.projectId !== nextContext.projectId) {
- this.context.index.clear();
- }
-
- this.setActiveProjectContext(nextContext);
- // ...
-}
-```
-
-#### 2. Sync Orchestrator Index Back to ToolContext
-```typescript
-// In graph_rebuild after orchestrator.build() completes
-const buildResult = await this.orchestrator.build({...});
-
-if (buildResult.success && this.orchestrator.getIndex) {
- // Copy Orchestrator's index to shared ToolContext.index
- const orchIndex = this.orchestrator.getIndex();
- // ... merge or sync mechanism ...
-}
-```
-
-**Requires**: Adding `getIndex()` public method to GraphOrchestrator
-
-#### 3. Load Index from Memgraph on Startup
-```typescript
-private async loadIndexFromMemgraph(projectId: string): Promise {
- const nodes = await this.context.memgraph.executeCypher(
- `MATCH (n) WHERE n.projectId = $projectId RETURN n`,
- { projectId }
- );
-
- for (const nodeRow of nodes.data) {
- const node = nodeRow.n;
- this.context.index.addNode(node.id, node.type, node.properties);
- }
- // Also load relationships
-}
-```
-
-### Medium-term (Better Architecture)
-
-#### Use Project-Scoped Indices
-```typescript
-private projectIndices = new Map();
-
-private getIndexForProject(projectId: string): GraphIndexManager {
- if (!this.projectIndices.has(projectId)) {
- this.projectIndices.set(projectId, new GraphIndexManager());
- }
- return this.projectIndices.get(projectId)!;
-}
-```
-
-#### Pass Index to Engines at Tool Invocation Time
-```typescript
-async graph_query(args: any): Promise {
- const context = this.getActiveProjectContext();
- const index = this.getIndexForProject(context.projectId);
-
- const result = await queryEngine.execute(query, {
- index,
- memgraph: this.context.memgraph,
- projectId: context.projectId,
- });
-}
-```
-
-#### Sync Orchestrator to Project Index
-```typescript
-// In orchestrator callback
-const buildResult = await this.orchestrator.build({...});
-if (buildResult.success) {
- const projectIndex = this.getIndexForProject(projectId);
- projectIndex.clear();
- projectIndex.merge(this.orchestrator.getIndex());
-}
-```
-
----
-
-## 7. Current Tool Behavior
-
-### Tools That Query Memgraph (Safe)
-- `graph_query`: Runs Cypher queries directly against Memgraph
-- `code_explain`: Queries Memgraph for dependencies
-- `find_pattern`: Queries Memgraph for patterns
-- `arch_validate`: Queries Memgraph for architecture
-
-### Tools That Use In-Memory Index (Risky)
-- `graph_health`: Queries `this.context.index.getStatistics()`
-- **EmbeddingEngine**: Iterates `this.context.index.getNodesByType("FUNCTION")`
-- **HybridRetriever**: Uses `this.context.index` for retrieval
-- **ProgressEngine**: Reads `this.context.index` for progress tracking
-
-### Recommendation
-For multi-project support, ensure all tools eventually query Memgraph with `projectId` filter, not the in-memory index.
-
----
-
-## 8. Session Management Example
-
-### Safe Multi-Project Workflow
-
-```
-Client A (Session A):
-1. POST /initialize → get session-id: "sess-a"
-2. graph_set_workspace({workspaceRoot: "/path/project-a", projectId: "a"})
-3. graph_rebuild()
-4. graph_query()
- └→ Uses "sess-a" context (Project A)
-
-Client B (Session B):
-1. POST /initialize → get session-id: "sess-b"
-2. graph_set_workspace({workspaceRoot: "/path/project-b", projectId: "b"})
-3. graph_rebuild()
-4. graph_query()
- └→ Uses "sess-b" context (Project B)
-
-Result: Two isolated sessions, each with their own ProjectContext
-Problem: Both sessions share the same ToolContext.index (no isolation)
-```
-
----
-
-## 9. Summary Table
-
-| Aspect | Current Behavior | Issue | Fix |
-|--------|------------------|-------|-----|
-| **Multiple Projects** | One per session via context map | Works at context level only | Clear index on context switch |
-| **Context Switching** | Updates ProjectContext + Watcher | Doesn't clear shared index | Add `this.context.index.clear()` |
-| **Full Rebuild** | Clears cache, not in-memory index | Orchestrator index ≠ Memgraph | Sync Orchestrator index back |
-| **Incremental Rebuild** | Updates only changed files | Same sync issue | Same sync mechanism |
-| **Index Init** | Empty at startup | Never populated from DB | Load from Memgraph on startup |
-| **Embedding** | Uses in-memory index | May use old/wrong project data | Use project-scoped indices |
-| **Progress Tracking** | Loads from shared index | Wrong project data | Use project-scoped indices |
-| **Architecture Validation** | Uses shared index | Wrong project data | Use project-scoped indices |
-
----
-
-## Files Referenced
-
-- **Tool Handlers**: `/home/alex_rod/projects/lexRAG-MCP/src/tools/tool-handlers.ts`
- - Lines 41-46: ToolContext interface
- - Lines 48-52: ProjectContext interface
- - Lines 69-71: Session and context maps
- - Lines 87-106: Context getters/setters
- - Lines 1543-1615: `graph_set_workspace` implementation
- - Lines 1617-1776: `graph_rebuild` implementation
- - Lines 290-314: `initializeEngines()` method
-
-- **Graph Orchestrator**: `/home/alex_rod/projects/lexRAG-MCP/src/graph/orchestrator.ts`
- - Lines 70-176: Constructor and index initialization
- - Lines 181-423: `build()` method with index handling
- - Lines 763-828: `addToIndex()` method
-
-- **Graph Index**: `/home/alex_rod/projects/lexRAG-MCP/src/graph/index.ts`
- - Lines 35-160: GraphIndexManager class
- - Lines 148-160: `clear()` method
-
-- **Progress Engine**: `/home/alex_rod/projects/lexRAG-MCP/src/engines/progress-engine.ts`
- - Lines 59-71: Constructor with `loadFromGraph()`
- - Lines 76-96: `loadFromGraph()` implementation
-
-- **MCP Server**: `/home/alex_rod/projects/lexRAG-MCP/src/mcp-server.ts`
- - Lines 618-623: Index initialization and handler creation
diff --git a/GRAPH_STATE_DIAGRAMS.md b/GRAPH_STATE_DIAGRAMS.md
deleted file mode 100644
index ef7073e..0000000
--- a/GRAPH_STATE_DIAGRAMS.md
+++ /dev/null
@@ -1,430 +0,0 @@
-# Graph State Architecture Diagrams
-
-## Diagram 1: Current Index Architecture
-
-```
-┌──────────────────────────────────────────────────────────────────┐
-│ MCP Server Process │
-├──────────────────────────────────────────────────────────────────┤
-│ │
-│ ┌─────────────────────────────────────────────────────────┐ │
-│ │ ToolContext (Shared, Long-lived) │ │
-│ │ ┌─────────────────────────────────────────────────────┤ │
-│ │ │ GraphIndexManager │ │
-│ │ │ ├─ nodesByType: Map │ │
-│ │ │ ├─ nodeById: Map │ │
-│ │ │ ├─ relationshipsByFrom: Map<...> │ │
-│ │ │ └─ statistics: {...} │ │
-│ │ │ │ │
-│ │ │ State at startup: EMPTY │ │
-│ │ │ State after rebuild: EMPTY (not synced) │ │
-│ │ │ Used by: ProgressEngine, ArchitectureEngine, │ │
-│ │ │ TestEngine, EmbeddingEngine, │ │
-│ │ │ HybridRetriever │ │
-│ │ └─────────────────────────────────────────────────────┤ │
-│ │ │ │
-│ │ MemgraphClient (Shared connection) │ │
-│ │ ├─ Connected to: Memgraph database │ │
-│ │ └─ Used by: All query tools │ │
-│ │ │ │
-│ │ config: {...} │ │
-│ └─────────────────────────────────────────────────────────┘ │
-│ │
-│ ┌─────────────────────────────────────────────────────────┐ │
-│ │ ToolHandlers (Session-aware) │ │
-│ │ ┌─────────────────────────────────────────────────────┤ │
-│ │ │ ProjectContext Management │ │
-│ │ │ ├─ defaultActiveProjectContext │ │
-│ │ │ ├─ sessionProjectContexts: Map │ │
-│ │ │ └─ sessionWatchers: Map │ │
-│ │ │ │ │
-│ │ │ Engines (Instance Variables) │ │
-│ │ │ ├─ progressEngine: ProgressEngine │ │
-│ │ │ ├─ orchestrator: GraphOrchestrator │ │
-│ │ │ ├─ archEngine: ArchitectureEngine │ │
-│ │ │ ├─ testEngine: TestEngine │ │
-│ │ │ ├─ episodeEngine: EpisodeEngine │ │
-│ │ │ ├─ coordinationEngine: CoordinationEngine │ │
-│ │ │ ├─ communityDetector: CommunityDetector │ │
-│ │ │ └─ hybridRetriever: HybridRetriever │ │
-│ │ │ │ │
-│ │ │ NOTE: All engines initialized ONCE at startup │ │
-│ │ │ Not recreated on project context switch │ │
-│ │ └─────────────────────────────────────────────────────┤ │
-│ │ │ │
-│ │ ┌─────────────────────────────────────────────────────┤ │
-│ │ │ GraphOrchestrator (Instance in ToolHandlers) │ │
-│ │ │ ├─ index: GraphIndexManager (NEW on construction) │ │
-│ │ │ ├─ builder: GraphBuilder │ │
-│ │ │ ├─ cache: CacheManager │ │
-│ │ │ ├─ memgraph: MemgraphClient (same as ToolContext) │ │
-│ │ │ └─ parser: TypeScriptParser + others │ │
-│ │ │ │ │
-│ │ │ NOTE: Has its own private index │ │
-│ │ │ Separate from ToolContext.index │ │
-│ │ │ Populated during build() but not synced back │ │
-│ │ └─────────────────────────────────────────────────────┤ │
-│ └─────────────────────────────────────────────────────────┘ │
-│ │
-└──────────────────────────────────────────────────────────────────┘
- ↓
- (Cypher Queries)
- ↓
-┌──────────────────────────────────────────────────────────────────┐
-│ Memgraph Database │
-│ ├─ Nodes: FILE, FUNCTION, CLASS, IMPORT, FEATURE, TASK, ... │
-│ ├─ Relations: CONTAINS, IMPORTS, CALLS, IMPLEMENTS, ... │
-│ ├─ Transactions: GRAPH_TX tracking rebuild history │
-│ └─ Source of Truth for all persistent data │
-└──────────────────────────────────────────────────────────────────┘
-```
-
-## Diagram 2: Data Flow During graph_rebuild
-
-```
-User calls graph_rebuild()
- ↓
-┌─────────────────────────────────────────────┐
-│ ToolHandlers.graph_rebuild() │
-│ 1. Validate workspace │
-│ 2. Create GRAPH_TX record in Memgraph │
-│ 3. Call this.orchestrator.build({...}) │
-│ (FIRE AND FORGET - async, no wait) │
-│ 4. Return status immediately │
-└─────────────────────────────────────────────┘
- ↓
- └──────────────────────────────┐
- ↓
- ┌──────────────────────────────────────────┐
- │ GraphOrchestrator.build() (async) │
- │ 1. Create NEW GraphIndexManager() │
- │ 2. Parse all source files │
- │ 3. For each file: │
- │ ├─ Parse → ParsedFile │
- │ ├─ Call addToIndex(parsed) │
- │ │ └→ Adds to THIS.INDEX (internal) │
- │ └─ Generate Cypher statements │
- │ 4. Build test relationships │
- │ 5. Execute Cypher batch → Memgraph │
- │ 6. Index docs (optional) │
- │ 7. Return BuildResult │
- └──────────────────────────────────────────┘
- ↓
- ┌──────────────────────────────────────────┐
- │ Orchestrator's Internal Index │
- │ (GraphIndexManager instance) │
- │ │
- │ Contains: All FILE, FUNCTION, CLASS, │
- │ IMPORT nodes from build │
- │ │
- │ Problem: NEVER synced to ToolContext. │
- │ index OR persisted anywhere │
- │ Only used for build statistics │
- └──────────────────────────────────────────┘
- ↓
- ┌──────────────────────────────────────────┐
- │ Memgraph Database (UPDATED) │
- │ │
- │ Receives: All Cypher statements │
- │ Result: Database now contains new graph │
- │ (this is the source of truth) │
- └──────────────────────────────────────────┘
- ↓
- ┌──────────────────────────────────────────┐
- │ ToolContext.index │
- │ │
- │ State: UNCHANGED (remains empty) │
- │ Problem: Never receives data from build │
- │ Engines using this index get │
- │ nothing (or stale data) │
- └──────────────────────────────────────────┘
-```
-
-## Diagram 3: Data Flow During graph_set_workspace
-
-```
-User calls graph_set_workspace({
- workspaceRoot: "/path/to/project-b",
- projectId: "project-b"
-})
- ↓
-┌──────────────────────────────────────────┐
-│ ToolHandlers.graph_set_workspace() │
-│ 1. Resolve new ProjectContext │
-│ 2. Call setActiveProjectContext(newCtx) │
-│ ├─ Get sessionId │
-│ └─ If session exists: │
-│ ├─ Update sessionProjectContexts │
-│ └─ Else update default context │
-│ 3. Start FileWatcher for new project │
-│ 4. Return success │
-└──────────────────────────────────────────┘
- ↓
- ├─→ Affected: ProjectContext in ToolHandlers
- │ (Now points to new workspace)
- │
- ├─→ Affected: FileWatcher
- │ (Now monitoring new directory)
- │
- └─→ NOT Affected: ToolContext.index
- (Still contains old project's data)
-
-RESULT: ProjectContext switched, but:
- • In-memory index still has old project's nodes
- • Engines (ProgressEngine, etc.) still have old data
- • Next queries will see mixed data from both projects
- • (Unless graph_rebuild clears it, which it doesn't)
-```
-
-## Diagram 4: Tool Context Switching Flow
-
-```
-Server Start
- ↓
-┌─────────────────────────────────────────┐
-│ MCP Server initialization │
-│ 1. Create MemgraphClient │
-│ 2. Create GraphIndexManager() → EMPTY │
-│ 3. Create ToolHandlers with context │
-│ └─ initializeEngines() │
-│ ├─ ProgressEngine(index) loads │
-│ │ from index (which is empty) │
-│ └─ Other engines initialized │
-└─────────────────────────────────────────┘
- ↓
-Session A (Client with mcp-session-id: "sess-a")
- ├─ graph_set_workspace(proj: "A")
- │ └─ Update sessionProjectContexts["sess-a"]
- ├─ graph_rebuild()
- │ └─ Orchestrator builds, Memgraph updated
- │ ToolContext.index still empty
- └─ graph_query()
- └─ Query runs against Memgraph (works)
- └─ Embedding uses ToolContext.index (empty)
-
-Session B (Client with mcp-session-id: "sess-b")
- ├─ graph_set_workspace(proj: "B")
- │ └─ Update sessionProjectContexts["sess-b"]
- ├─ graph_rebuild()
- │ └─ Orchestrator builds Project B, Memgraph updated
- │ ToolContext.index still empty
- └─ graph_query()
- └─ Query runs against Memgraph (works)
- └─ Embedding uses ToolContext.index (empty)
-
-No Session (Backwards compatibility)
- ├─ graph_set_workspace(proj: "C")
- │ └─ Update defaultActiveProjectContext
- └─ ...rest of flow...
-
-RESULT: Multiple sessions work if they only use Memgraph queries
- But engines using in-memory index fail for all projects
-```
-
-## Diagram 5: Index Population Sources and Sinks
-
-```
-SOURCES of Index Data:
-├─ manual.addNode() calls
-├─ orchestrator.addToIndex() → Orchestrator's internal index
-└─ ProgressEngine.loadFromGraph() → reads from in-memory (empty at start)
-
-┌────────────────────────────────────────┐
-│ GraphIndexManager (ToolContext.index) │
-├────────────────────────────────────────┤
-│ Populated from: │
-│ ├─ (empty at startup) │
-│ ├─ (never populated from Memgraph) │
-│ ├─ (not synced from orchestrator) │
-│ └─ (only manual additions) │
-│ │
-│ Used by: │
-│ ├─ ProgressEngine.loadFromGraph() │
-│ ├─ ArchitectureEngine queries │
-│ ├─ TestEngine queries │
-│ ├─ EmbeddingEngine iteration │
-│ └─ HybridRetriever lookup │
-│ │
-│ Cleared when: │
-│ ├─ (NEVER - this is the problem) │
-│ └─ (manual .clear() call) │
-└────────────────────────────────────────┘
-
-┌────────────────────────────────────────┐
-│ GraphIndexManager (Orchestrator.index) │
-├────────────────────────────────────────┤
-│ Populated from: │
-│ ├─ orchestrator.addToIndex() │
-│ │ └─ Called during build() │
-│ └─ (ONE instance per orchestrator) │
-│ │
-│ Used by: │
-│ ├─ orchestrator.getStatistics() │
-│ └─ (internal to orchestrator) │
-│ │
-│ Synced to: │
-│ └─ (NEVER - this is the problem) │
-└────────────────────────────────────────┘
- ↓ (Cypher statements)
-┌────────────────────────────────────────┐
-│ Memgraph Database │
-├────────────────────────────────────────┤
-│ Populated from: │
-│ ├─ orchestrator.build() → Cypher batch │
-│ ├─ Direct Cypher execution │
-│ └─ (Source of truth) │
-│ │
-│ Queried by: │
-│ ├─ graph_query tool │
-│ ├─ code_explain tool │
-│ ├─ find_pattern tool │
-│ ├─ arch_validate tool │
-│ └─ All other query-based tools │
-└────────────────────────────────────────┘
-```
-
-## Diagram 6: Engine Initialization and Data Flow
-
-```
-Server Startup
- ↓
-initializeEngines() called ONCE
- ├─→ ArchitectureEngine(this.context.index)
- │ └─ Holds reference to shared index
- │ (empty at startup, not updated after rebuild)
- │
- ├─→ TestEngine(this.context.index)
- │ └─ Holds reference to shared index
- │ (empty at startup, not updated after rebuild)
- │
- ├─→ ProgressEngine(this.context.index, memgraph)
- │ └─ Constructor calls loadFromGraph()
- │ ├─ Reads: this.index.getNodesByType("FEATURE")
- │ ├─ Reads: this.index.getNodesByType("TASK")
- │ └─ Result: features and tasks maps remain empty
- │
- ├─→ EpisodeEngine(memgraph)
- │ └─ Uses memgraph only (no index) ✓
- │
- ├─→ CoordinationEngine(memgraph)
- │ └─ Uses memgraph only (no index) ✓
- │
- ├─→ CommunityDetector(memgraph)
- │ └─ Uses memgraph only (no index) ✓
- │
- └─→ GraphOrchestrator(memgraph)
- └─ Creates its OWN GraphIndexManager()
- (separate from ToolContext.index)
-
-Problem Flow:
-graph_rebuild() executes
- ↓
-Orchestrator builds and populates ITS index
- ↓
-Memgraph updated with Cypher statements ✓
- ↓
-Orchestrator.index discarded/unused
- ↓
-ToolContext.index STILL empty ✗
- ↓
-When tools call engines:
- ├─ ProgressEngine.getProgress() → queries empty index ✗
- ├─ ArchitectureEngine.validate() → queries empty index ✗
- └─ EmbeddingEngine.generate() → queries empty index ✗
-
-Solution would require:
- 1. Sync orchestrator.index → ToolContext.index after build
- 2. OR recreate engines after rebuild
- 3. OR make all engines query Memgraph instead of index
-```
-
-## Diagram 7: Session Isolation (Current vs Ideal)
-
-```
-CURRENT STATE (Shared ToolContext.index):
-═════════════════════════════════════════
-
-Session A (ProjectId: "project-a") Session B (ProjectId: "project-b")
- │ │
- ├─ ProjectContext = A ├─ ProjectContext = B
- │ │
- ├─ query("find all files") ├─ query("find all files")
- │ └─ Queries Memgraph with A context │ └─ Queries Memgraph with B context
- │ │
- ├─ EmbeddingEngine.generate() ├─ EmbeddingEngine.generate()
- │ └─ Reads from shared index │ └─ Reads from shared index
- │ (empty or mixed data) │ (empty or mixed data)
- │ │
- └─ PROBLEM: Shared index has no └─ PROBLEM: Same shared index
- project isolation affects this session too
-
-
-IDEAL STATE (Project-scoped indices):
-═════════════════════════════════════════
-
-Session A (ProjectId: "project-a") Session B (ProjectId: "project-b")
- │ │
- ├─ ProjectContext = A ├─ ProjectContext = B
- ├─ Index = indexForProject("a") ├─ Index = indexForProject("b")
- │ │
- ├─ query("find all files") ├─ query("find all files")
- │ └─ Queries Memgraph with A │ └─ Queries Memgraph with B
- │ │
- ├─ EmbeddingEngine.generate() ├─ EmbeddingEngine.generate()
- │ └─ Reads from project-a index │ └─ Reads from project-b index
- │ (A's data only) │ (B's data only)
- │ │
- └─ WORKING: Complete isolation └─ WORKING: Complete isolation
-```
-
-## Diagram 8: Critical Path Analysis
-
-```
-Three Critical Paths for Multi-Project Support:
-
-PATH 1: Context Switching (PARTIALLY WORKING)
-──────────────────────────────────────────────
-graph_set_workspace()
- ├─ ✓ Updates ProjectContext
- ├─ ✓ Starts new FileWatcher
- ├─ ✗ Does NOT clear shared index
- └─ Result: Project context changed, but index corrupted
-
-PATH 2: Graph Rebuild (PARTIALLY WORKING)
-──────────────────────────────────────────
-graph_rebuild()
- ├─ ✓ Clears cache
- ├─ ✓ Sends Cypher to Memgraph
- ├─ ✗ Doesn't sync internal index back
- ├─ ✗ Doesn't update ToolContext.index
- └─ Result: Database updated, but in-memory index unused
-
-PATH 3: Tool Queries (WORKING FOR MEMGRAPH, BROKEN FOR INDEX)
-─────────────────────────────────────────────────────────────
-Tool execution (e.g., graph_query)
- ├─ If Cypher query: ✓ Uses Memgraph (works)
- ├─ If uses in-memory index: ✗ Gets empty/stale data
- └─ Result: Some tools work, embedding/validation fail
-
-
-RECOMMENDATION: Fix in order:
- 1. Fix PATH 1: Clear index on context switch
- 2. Fix PATH 2: Sync orchestrator index after rebuild
- 3. Fix PATH 3: Make all tools projectId-aware for Memgraph queries
-```
-
----
-
-## Summary of Key Insights
-
-1. **Two separate index systems** exist and are never synchronized
- - ToolContext.index (shared, used by engines, but empty)
- - Orchestrator.index (internal, populated during build, then discarded)
-
-2. **Memgraph is the source of truth** but ToolContext.index should be the query cache
-
-3. **Session isolation works at ProjectContext level** but not at index level
-
-4. **Engines are initialized once** and hold stale references to an empty index
-
-5. **Context switching doesn't clean up** the accumulated state in shared index
diff --git a/GRAPH_STATE_FIXES.md b/GRAPH_STATE_FIXES.md
deleted file mode 100644
index 889eb2d..0000000
--- a/GRAPH_STATE_FIXES.md
+++ /dev/null
@@ -1,788 +0,0 @@
-# Graph State Synchronization: Implementation Guide
-
-## Overview
-
-This guide provides concrete implementation paths to fix the graph state synchronization issues identified in the codebase analysis.
-
----
-
-## Issue Summary
-
-### Current Problems
-
-1. **Index Accumulation**: `ToolContext.index` is never cleared on project context switches
-2. **Orphaned Build State**: `GraphOrchestrator.index` is populated but never synced back to `ToolContext.index`
-3. **Startup Desync**: `ToolContext.index` is never populated from Memgraph database
-4. **Engine Stale State**: Engines hold references to empty index for entire server lifetime
-
-### Impact
-
-- Multi-project workflows see mixed data from multiple projects
-- Embedding engine generates embeddings for nonexistent nodes
-- Progress tracking shows data from wrong project
-- Architecture validation runs against empty index
-
----
-
-## Fix Level 1: Immediate Quick Wins (1-2 hours)
-
-### Fix 1.1: Clear Index on Project Context Switch
-
-**File**: `src/tools/tool-handlers.ts`
-
-**Location**: `graph_set_workspace()` method (line ~1586)
-
-**Current Code**:
-```typescript
-async graph_set_workspace(args: any): Promise {
- // ...validation...
- this.setActiveProjectContext(nextContext);
- await this.startActiveWatcher(nextContext);
- // ...
-}
-```
-
-**Fixed Code**:
-```typescript
-async graph_set_workspace(args: any): Promise {
- const oldContext = this.getActiveProjectContext();
- let nextContext = this.resolveProjectContext(args || {});
-
- // ... validation code ...
-
- // NEW: Clear index if switching to different project
- if (oldContext.projectId !== nextContext.projectId) {
- console.log(`[graph_set_workspace] Clearing index for project switch: ${oldContext.projectId} → ${nextContext.projectId}`);
- this.context.index.clear();
- }
-
- this.setActiveProjectContext(nextContext);
- await this.startActiveWatcher(nextContext);
-
- return this.formatSuccess({
- // ... existing response ...
- note: oldContext.projectId !== nextContext.projectId
- ? "Index cleared for new project. Run graph_rebuild to populate."
- : undefined
- });
-}
-```
-
-**Testing**:
-```typescript
-// Test: Switch between two projects
-graph_set_workspace({projectId: "project-a"})
-graph_rebuild() // Fills database with project-a
-graph_health() // Should show project-a stats
-
-graph_set_workspace({projectId: "project-b"})
-// At this point, in-memory index should be cleared
-graph_health() // Should show 0 indexed items until rebuild
-graph_rebuild() // Fills database with project-b
-graph_health() // Should show project-b stats
-```
-
-### Fix 1.2: Add ProjectId Filter to Existing Queries
-
-Many tools query Memgraph but should filter by `projectId` to prevent cross-project data leakage.
-
-**File**: `src/tools/tool-handlers.ts`
-
-**Affected Methods**:
-- `graph_query()` (line ~700)
-- `code_explain()` (line ~800)
-- `find_pattern()` (line ~900)
-
-**Example - graph_query**:
-
-**Current Code**:
-```typescript
-const result = await this.context.memgraph.executeCypher(
- `MATCH (n) RETURN n LIMIT ${limit}`,
- {}
-);
-```
-
-**Fixed Code**:
-```typescript
-const context = this.getActiveProjectContext();
-const result = await this.context.memgraph.executeCypher(
- `MATCH (n) WHERE n.projectId = $projectId RETURN n LIMIT ${limit}`,
- { projectId: context.projectId, limit }
-);
-```
-
-**Why**: Ensures queries only return data for current project, even if database has multiple projects
-
----
-
-## Fix Level 2: Index Synchronization (3-4 hours)
-
-### Fix 2.1: Export Index from GraphOrchestrator
-
-**File**: `src/graph/orchestrator.ts`
-
-**Current Code** (line ~70-176):
-```typescript
-export class GraphOrchestrator {
- private parser: TypeScriptParser;
- // ... other fields ...
- private index: GraphIndexManager;
-
- constructor(memgraph?: MemgraphClient, verbose = false) {
- // ... initialization ...
- this.index = new GraphIndexManager();
- }
-```
-
-**Fixed Code**:
-```typescript
-export class GraphOrchestrator {
- private parser: TypeScriptParser;
- // ... other fields ...
- private index: GraphIndexManager;
-
- constructor(memgraph?: MemgraphClient, verbose = false) {
- // ... initialization ...
- this.index = new GraphIndexManager();
- }
-
- /**
- * Get reference to the in-memory index
- * Useful for syncing after build completion
- */
- getIndex(): GraphIndexManager {
- return this.index;
- }
-
- /**
- * Export statistics from the in-memory index
- */
- getStatistics(): GraphIndex['statistics'] {
- return this.index.getStatistics();
- }
-}
-```
-
-### Fix 2.2: Sync Orchestrator Index After Build
-
-**File**: `src/tools/tool-handlers.ts`
-
-**Location**: `graph_rebuild()` method (line ~1617-1776)
-
-**Current Code**:
-```typescript
-async graph_rebuild(args: any): Promise {
- // ... validation ...
-
- this.orchestrator
- .build({mode, verbose, workspaceRoot, projectId, sourceDir, ...})
- .then(async () => {
- const invalidated = await this.coordinationEngine!.invalidateStaleClaims(projectId);
- // ... other post-build tasks ...
- })
- .catch((error) => {
- console.error(`[graph_rebuild] Build failed: ${error}`);
- });
-
- return this.formatSuccess({...});
-}
-```
-
-**Fixed Code**:
-```typescript
-async graph_rebuild(args: any): Promise {
- // ... validation ...
-
- this.orchestrator
- .build({mode, verbose, workspaceRoot, projectId, sourceDir, ...})
- .then(async () => {
- // NEW: Sync orchestrator's index to shared ToolContext index
- const orchIndex = this.orchestrator!.getIndex();
- const orchStats = orchIndex.getStatistics();
-
- console.log(`[graph_rebuild] Syncing in-memory index from orchestrator...`);
-
- // For full rebuild, clear existing index first
- if (mode === "full") {
- this.context.index.clear();
- }
-
- // Copy all nodes from orchestrator index to shared index
- for (const nodeType of Object.keys(orchStats.nodesByType)) {
- const nodes = orchIndex.getNodesByType(nodeType);
- for (const node of nodes) {
- this.context.index.addNode(node.id, node.type, node.properties);
- }
- }
-
- console.log(`[graph_rebuild] Index synchronized: ${orchStats.totalNodes} nodes, ${orchStats.totalRelationships} relationships`);
-
- // Notify engines that index was updated
- console.log(`[graph_rebuild] Notifying engines of index update...`);
-
- const invalidated = await this.coordinationEngine!.invalidateStaleClaims(projectId);
- // ... other post-build tasks ...
- })
- .catch((error) => {
- console.error(`[graph_rebuild] Build failed: ${error}`);
- });
-
- return this.formatSuccess({...});
-}
-```
-
-**Why**:
-- Populates shared index with build results
-- Enables embedding generation to work correctly
-- Makes progress tracking work for current project
-- Synchronizes in-memory cache with database
-
-### Fix 2.3: Load Index from Memgraph on Engine Initialization
-
-**File**: `src/tools/tool-handlers.ts`
-
-**Location**: `initializeEngines()` method (line ~290)
-
-**Current Code**:
-```typescript
-private initializeEngines(): void {
- if (this.context.config.architecture) {
- this.archEngine = new ArchitectureEngine(
- this.context.config.architecture.layers,
- this.context.config.architecture.rules,
- this.context.index,
- );
- }
-
- this.testEngine = new TestEngine(this.context.index);
- this.progressEngine = new ProgressEngine(
- this.context.index,
- this.context.memgraph,
- );
- // ...
-}
-```
-
-**Fixed Code**:
-```typescript
-private async initializeEnginesAsync(): Promise {
- // Attempt to load index from Memgraph for current project
- const context = this.getActiveProjectContext();
-
- if (this.context.memgraph.isConnected()) {
- try {
- console.log(`[initializeEngines] Loading graph index for project ${context.projectId} from Memgraph...`);
-
- // Load all nodes
- const nodeResult = await this.context.memgraph.executeCypher(
- `MATCH (n) WHERE n.projectId = $projectId RETURN n, labels(n) as types`,
- { projectId: context.projectId }
- );
-
- if (nodeResult.data && nodeResult.data.length > 0) {
- for (const row of nodeResult.data) {
- const node = row.n;
- const types = row.types as string[];
- const nodeType = types.find(t => t !== 'Node') || 'Node';
- this.context.index.addNode(node.id, nodeType, node.properties || {});
- }
- console.log(`[initializeEngines] Loaded ${nodeResult.data.length} nodes`);
- }
-
- // Load all relationships
- const relResult = await this.context.memgraph.executeCypher(
- `MATCH (from)-[r]->(to) WHERE from.projectId = $projectId RETURN r, type(r) as relType, from.id as fromId, to.id as toId`,
- { projectId: context.projectId }
- );
-
- if (relResult.data && relResult.data.length > 0) {
- for (const row of relResult.data) {
- const rel = row.r;
- this.context.index.addRelationship(
- rel.id,
- row.fromId,
- row.toId,
- row.relType,
- rel.properties || {}
- );
- }
- console.log(`[initializeEngines] Loaded ${relResult.data.length} relationships`);
- }
- } catch (error) {
- console.warn(`[initializeEngines] Failed to load index from Memgraph: ${error}`);
- }
- }
-
- // Now initialize engines with loaded index
- if (this.context.config.architecture) {
- this.archEngine = new ArchitectureEngine(
- this.context.config.architecture.layers,
- this.context.config.architecture.rules,
- this.context.index,
- );
- }
-
- this.testEngine = new TestEngine(this.context.index);
- this.progressEngine = new ProgressEngine(
- this.context.index,
- this.context.memgraph,
- );
- // ... rest of initialization ...
-}
-```
-
-**Update Constructor**:
-```typescript
-constructor(private context: ToolContext) {
- this.defaultActiveProjectContext = this.defaultProjectContext();
- // Make initialization async
- this.initializeEnginesAsync().catch(error => {
- console.error(`[ToolHandlers] Engine initialization failed: ${error}`);
- });
-}
-```
-
----
-
-## Fix Level 3: Project-Scoped Indices (8-10 hours)
-
-### Rationale
-
-Rather than having a single shared index, maintain a mapping of project → index. This eliminates cross-project data contamination entirely.
-
-### Fix 3.1: Add Project Index Management to ToolHandlers
-
-**File**: `src/tools/tool-handlers.ts`
-
-**Location**: Add to `ToolHandlers` class (after line ~70)
-
-**Code**:
-```typescript
-export class ToolHandlers {
- private archEngine?: ArchitectureEngine;
- // ... existing fields ...
- private sessionProjectContexts = new Map();
-
- // NEW: Project-scoped indices
- private projectIndices = new Map();
-
- // ... rest of class ...
-
- /**
- * Get or create index for specific project
- */
- private getIndexForProject(projectId: string): GraphIndexManager {
- if (!this.projectIndices.has(projectId)) {
- const newIndex = new GraphIndexManager();
- this.projectIndices.set(projectId, newIndex);
- console.log(`[ToolHandlers] Created new index for project: ${projectId}`);
- }
- return this.projectIndices.get(projectId)!;
- }
-
- /**
- * Get index for currently active project
- */
- private getActiveIndex(): GraphIndexManager {
- const context = this.getActiveProjectContext();
- return this.getIndexForProject(context.projectId);
- }
-
- /**
- * Clear index for specific project
- */
- private clearIndexForProject(projectId: string): void {
- const index = this.projectIndices.get(projectId);
- if (index) {
- index.clear();
- console.log(`[ToolHandlers] Cleared index for project: ${projectId}`);
- }
- }
-
- /**
- * Load project index from Memgraph
- */
- private async loadIndexFromMemgraph(projectId: string): Promise {
- const index = this.getIndexForProject(projectId);
-
- if (!this.context.memgraph.isConnected()) {
- console.log(`[loadIndexFromMemgraph] Memgraph not connected, skipping load for ${projectId}`);
- return;
- }
-
- try {
- // Load nodes
- const nodeResult = await this.context.memgraph.executeCypher(
- `MATCH (n) WHERE n.projectId = $projectId RETURN n, labels(n) as types`,
- { projectId }
- );
-
- if (nodeResult.data?.length) {
- for (const row of nodeResult.data) {
- const node = row.n;
- const types = row.types as string[];
- const nodeType = types.find(t => !['Node'].includes(t)) || 'Node';
- index.addNode(node.id, nodeType, node.properties || {});
- }
- }
-
- // Load relationships
- const relResult = await this.context.memgraph.executeCypher(
- `MATCH (from)-[r]->(to) WHERE from.projectId = $projectId RETURN r, type(r) as relType, from.id as fromId, to.id as toId`,
- { projectId }
- );
-
- if (relResult.data?.length) {
- for (const row of relResult.data) {
- const rel = row.r;
- index.addRelationship(
- rel.id,
- row.fromId,
- row.toId,
- row.relType,
- rel.properties || {}
- );
- }
- }
-
- console.log(`[loadIndexFromMemgraph] Loaded index for project ${projectId}`);
- } catch (error) {
- console.warn(`[loadIndexFromMemgraph] Failed to load index: ${error}`);
- }
- }
-}
-```
-
-### Fix 3.2: Update graph_set_workspace
-
-**File**: `src/tools/tool-handlers.ts`
-
-**Location**: `graph_set_workspace()` method
-
-**Code**:
-```typescript
-async graph_set_workspace(args: any): Promise {
- const oldContext = this.getActiveProjectContext();
- let nextContext = this.resolveProjectContext(args || {});
-
- // ... validation code ...
-
- // Switching to new project
- if (oldContext.projectId !== nextContext.projectId) {
- console.log(`[graph_set_workspace] Switching project: ${oldContext.projectId} → ${nextContext.projectId}`);
-
- // Load index for new project
- await this.loadIndexFromMemgraph(nextContext.projectId);
- }
-
- this.setActiveProjectContext(nextContext);
- await this.startActiveWatcher(nextContext);
-
- const watcher = this.getActiveWatcher();
-
- return this.formatSuccess(
- {
- success: true,
- projectContext: this.getActiveProjectContext(),
- // ... other fields ...
- message: "Workspace context updated and index loaded from database.",
- },
- profile,
- );
-}
-```
-
-### Fix 3.3: Update initializeEngines to Use Project Indices
-
-**File**: `src/tools/tool-handlers.ts`
-
-**Location**: `initializeEngines()` method
-
-Instead of passing `this.context.index` directly, engines should be wrapped to access the active project's index:
-
-```typescript
-private initializeEngines(): void {
- // Create a proxy that always uses the active project's index
- const createIndexProxy = (): GraphIndexManager => {
- return new Proxy(new GraphIndexManager(), {
- get: (target, prop) => {
- const activeIndex = this.getActiveIndex();
- return (activeIndex as any)[prop];
- },
- });
- };
-
- if (this.context.config.architecture) {
- this.archEngine = new ArchitectureEngine(
- this.context.config.architecture.layers,
- this.context.config.architecture.rules,
- createIndexProxy(),
- );
- }
-
- this.testEngine = new TestEngine(createIndexProxy());
- this.progressEngine = new ProgressEngine(
- createIndexProxy(),
- this.context.memgraph,
- );
- // ... rest ...
-}
-```
-
-Or better yet, refactor engines to accept a callback:
-
-```typescript
-// In engine constructors, accept an indexGetter function
-this.testEngine = new TestEngine(
- () => this.getActiveIndex(),
- this.context.memgraph
-);
-```
-
-### Fix 3.4: Update graph_rebuild to Populate Project Index
-
-**File**: `src/tools/tool-handlers.ts`
-
-**Location**: `graph_rebuild()` method
-
-```typescript
-async graph_rebuild(args: any): Promise {
- // ... validation ...
-
- const { projectId } = resolvedContext;
-
- this.orchestrator
- .build({mode, verbose, workspaceRoot, projectId, sourceDir, ...})
- .then(async () => {
- // Get project-specific index
- const projectIndex = this.getIndexForProject(projectId);
-
- // For full rebuild, clear existing index
- if (mode === "full") {
- projectIndex.clear();
- }
-
- // Sync orchestrator's index to project index
- const orchIndex = this.orchestrator!.getIndex();
- const orchStats = orchIndex.getStatistics();
-
- for (const nodeType of Object.keys(orchStats.nodesByType)) {
- const nodes = orchIndex.getNodesByType(nodeType);
- for (const node of nodes) {
- projectIndex.addNode(node.id, node.type, node.properties);
- }
- }
-
- console.log(`[graph_rebuild] Updated project index for ${projectId}`);
-
- // ... rest of post-build tasks ...
- })
- .catch(error => {
- console.error(`[graph_rebuild] Build failed: ${error}`);
- });
-
- return this.formatSuccess({...});
-}
-```
-
----
-
-## Implementation Roadmap
-
-### Phase 1: Stabilization (Week 1)
-- **Fix 1.1**: Clear index on context switch (30 min)
-- **Fix 1.2**: Add projectId filters to queries (1 hour)
-- **Test**: Multi-project workflow with session IDs
-- **Deploy**: Reduces cross-project data leakage
-
-### Phase 2: Synchronization (Week 2)
-- **Fix 2.1**: Export index from orchestrator (30 min)
-- **Fix 2.2**: Sync orchestrator index after build (1 hour)
-- **Fix 2.3**: Load index from Memgraph on startup (2 hours)
-- **Test**: Embedding generation, progress tracking
-- **Deploy**: Fixes embedding and validation engines
-
-### Phase 3: Refactoring (Week 3)
-- **Fix 3.1-3.4**: Implement project-scoped indices (8 hours)
-- **Refactor**: Update engines to use index callbacks
-- **Test**: Comprehensive multi-project test suite
-- **Deploy**: Complete isolation, future-proof architecture
-
----
-
-## Testing Strategy
-
-### Unit Tests
-
-```typescript
-// test-graph-state.spec.ts
-
-describe("Graph State Management", () => {
-
- it("should clear index on project context switch", async () => {
- const handlers = new ToolHandlers(context);
-
- // Setup project A
- await handlers.graph_set_workspace({projectId: "A"});
- // Manually add some nodes
- context.index.addNode("test-node", "TEST", {});
-
- // Switch to project B
- await handlers.graph_set_workspace({projectId: "B"});
-
- // Index should be cleared
- expect(context.index.getStatistics().totalNodes).toBe(0);
- });
-
- it("should sync orchestrator index after rebuild", async () => {
- const handlers = new ToolHandlers(context);
-
- // Set workspace and rebuild
- await handlers.graph_set_workspace({
- projectId: "test",
- workspaceRoot: "/test"
- });
-
- const rebuildResult = await handlers.graph_rebuild({});
-
- // Wait for async build to complete
- await new Promise(resolve => setTimeout(resolve, 1000));
-
- // Index should be populated
- const stats = context.index.getStatistics();
- expect(stats.totalNodes).toBeGreaterThan(0);
- });
-
- it("should load index from Memgraph on context switch", async () => {
- const handlers = new ToolHandlers(context);
-
- // Assuming database has data for project "existing"
- await handlers.graph_set_workspace({projectId: "existing"});
-
- // Index should be loaded from database
- const stats = context.index.getStatistics();
- expect(stats.totalNodes).toBeGreaterThan(0);
- });
-});
-```
-
-### Integration Tests
-
-```typescript
-// test-multi-project-workflow.spec.ts
-
-describe("Multi-Project Workflow", () => {
-
- it("should support switching between projects with session IDs", async () => {
- const sessionA = "sess-a";
- const sessionB = "sess-b";
-
- // Session A: Project A
- setRequestContext({sessionId: sessionA});
- await handlers.graph_set_workspace({projectId: "project-a"});
- await handlers.graph_rebuild({});
-
- const healthA = await handlers.graph_health({});
- const statsA = JSON.parse(healthA).data.graph.index;
-
- // Session B: Project B
- setRequestContext({sessionId: sessionB});
- await handlers.graph_set_workspace({projectId: "project-b"});
- await handlers.graph_rebuild({});
-
- const healthB = await handlers.graph_health({});
- const statsB = JSON.parse(healthB).data.graph.index;
-
- // Stats should be different
- expect(statsA).not.toEqual(statsB);
-
- // Switch back to A
- setRequestContext({sessionId: sessionA});
- const healthA2 = await handlers.graph_health({});
- const statsA2 = JSON.parse(healthA2).data.graph.index;
-
- // Should match original
- expect(statsA2).toEqual(statsA);
- });
-});
-```
-
----
-
-## Validation Checklist
-
-- [ ] Index is cleared when switching projects
-- [ ] ProjectId filters are applied to all Memgraph queries
-- [ ] Orchestrator index is exported and accessible
-- [ ] Orchestrator index is synced after build completion
-- [ ] Index is loaded from Memgraph when switching projects
-- [ ] Embedding engine receives populated index
-- [ ] Progress tracking works for current project
-- [ ] Architecture validation works for current project
-- [ ] Multiple sessions maintain isolation
-- [ ] No cross-project data leakage in queries
-
----
-
-## Performance Considerations
-
-### Index Loading
-
-**Issue**: Loading all nodes/relationships from Memgraph on every context switch could be slow.
-
-**Solution**: Implement lazy loading
-```typescript
-private indexPromises = new Map>();
-
-async getIndexForProject(projectId: string): Promise {
- if (!this.projectIndices.has(projectId)) {
- // Lazy load in background
- const loadPromise = this.loadIndexFromMemgraph(projectId)
- .catch(error => console.warn(`Failed to load index: ${error}`));
- this.indexPromises.set(projectId, loadPromise);
- }
-
- // Wait for load if in progress
- await this.indexPromises.get(projectId);
- return this.projectIndices.get(projectId)!;
-}
-```
-
-### Memory Usage
-
-**Issue**: Storing multiple project indices in memory could consume significant RAM for large projects.
-
-**Solution**: Implement cache eviction
-```typescript
-private readonly MAX_CACHED_INDICES = 5;
-
-private getIndexForProject(projectId: string): GraphIndexManager {
- if (!this.projectIndices.has(projectId)) {
- if (this.projectIndices.size >= this.MAX_CACHED_INDICES) {
- // Evict least recently used
- const lru = this.getOldestAccessedProject();
- this.projectIndices.delete(lru);
- }
- this.projectIndices.set(projectId, new GraphIndexManager());
- }
- return this.projectIndices.get(projectId)!;
-}
-```
-
----
-
-## Rollback Plan
-
-If issues arise during implementation:
-
-1. **Phase 1 Issues**: Revert Fix 1.1 and 1.2 (low-risk, isolated changes)
-2. **Phase 2 Issues**: Revert Fix 2.1-2.3 (index sync can be disabled via env var)
-3. **Phase 3 Issues**: Keep Phase 1+2, fall back to single shared index
-
-Recommended: Add feature flags
-```typescript
-const ENABLE_INDEX_CLEARING = env.LXRAG_ENABLE_INDEX_CLEARING !== "false";
-const ENABLE_INDEX_SYNC = env.LXRAG_ENABLE_INDEX_SYNC !== "false";
-const ENABLE_PROJECT_SCOPED_INDICES = env.LXRAG_PROJECT_SCOPED_INDICES === "true";
-```
diff --git a/GRAPH_STATE_INDEX.md b/GRAPH_STATE_INDEX.md
deleted file mode 100644
index 50b641c..0000000
--- a/GRAPH_STATE_INDEX.md
+++ /dev/null
@@ -1,389 +0,0 @@
-# Graph State Analysis - Complete Documentation Index
-
-## Overview
-
-This directory contains a comprehensive analysis of the graph state management system in lexRAG-MCP, specifically addressing multi-project support, context switching, index synchronization, and architecture patterns.
-
-**Analysis Date**: February 22, 2026
-**Scope**: Full codebase examination of graph state across initialization, context switching, rebuilding, and querying
-**Focus**: Understanding index lifecycle and multi-project implications
-
----
-
-## Document Guide
-
-### 1. **GRAPH_STATE_QUICK_REF.txt** (12 KB) - START HERE
-**Best for**: Quick answers to the 4 key questions
-
-Quick reference guide with formatted sections:
-- Question 1: Multiple projects setup
-- Question 2: What happens on context switching
-- Question 3: Graph rebuild behavior
-- Question 4: Index initialization
-- Core architecture issue summary
-- Quick fixes ranked by priority
-- Key file locations
-- Risk assessment
-
-**Read this if**: You want answers fast without deep dives
-
----
-
-### 2. **GRAPH_STATE_SUMMARY.md** (13 KB) - EXECUTIVE SUMMARY
-**Best for**: Understanding the big picture
-
-Structured answer to your 4 questions with detailed explanations:
-- Question-by-question breakdowns
-- The core problem (two separate, unsynced indices)
-- Design issues summary with examples
-- Impact analysis (what works, what breaks)
-- Recommended fixes with priority order
-- Session management best practices
-- Risk level assessment
-
-**Read this if**: You want comprehensive answers with context
-
----
-
-### 3. **GRAPH_STATE_ANALYSIS.md** (19 KB) - DEEP DIVE
-**Best for**: Complete technical understanding
-
-9 detailed sections covering:
-1. Multiple Projects Setup (session-based architecture)
-2. Project Context Switching (what changes vs. what doesn't)
-3. Graph Rebuild Behavior (three separate indices)
-4. Index Initialization (where it gets populated from)
-5. Design Implications and Issues (the core problems)
-6. Recommended Fixes (short, medium, long-term)
-7. Current Tool Behavior (which tools work/fail)
-8. Session Management Example (safe multi-project workflow)
-9. Summary Table (component status overview)
-
-**Read this if**: You need to understand the complete architecture
-
----
-
-### 4. **GRAPH_STATE_DIAGRAMS.md** (24 KB) - VISUAL REFERENCE
-**Best for**: Understanding architecture and data flow visually
-
-8 ASCII diagrams showing:
-1. Current index architecture (system layout)
-2. Data flow during graph_rebuild (process steps)
-3. Data flow during graph_set_workspace (context switching)
-4. Tool context switching flow (session isolation)
-5. Index population sources and sinks
-6. Engine initialization and data flow
-7. Session isolation (current vs. ideal)
-8. Critical path analysis (3 key workflows)
-
-**Read this if**: You're a visual learner or need to explain to others
-
----
-
-### 5. **GRAPH_STATE_FIXES.md** (23 KB) - IMPLEMENTATION GUIDE
-**Best for**: Actually implementing the fixes
-
-3 levels of fixes with complete code examples:
-
-**Fix Level 1: Immediate Quick Wins (1-2 hours)**
-- 1.1: Clear index on context switch
-- 1.2: Add projectId filters to queries
-
-**Fix Level 2: Index Synchronization (3-4 hours)**
-- 2.1: Export index from GraphOrchestrator
-- 2.2: Sync orchestrator index after build
-- 2.3: Load index from Memgraph on startup
-
-**Fix Level 3: Project-Scoped Indices (8-10 hours)**
-- 3.1: Project index management
-- 3.2: Update graph_set_workspace
-- 3.3: Update initializeEngines
-- 3.4: Update graph_rebuild
-
-Plus:
-- Implementation roadmap (phases 1-3)
-- Testing strategy (unit & integration tests)
-- Validation checklist
-- Performance considerations
-- Rollback plan
-
-**Read this if**: You're implementing the fixes
-
----
-
-## Quick Navigation by Use Case
-
-### "I need to understand the problem in 5 minutes"
-→ Read: **GRAPH_STATE_QUICK_REF.txt**
-
-### "I need to explain this to my team"
-→ Read: **GRAPH_STATE_SUMMARY.md** + **GRAPH_STATE_DIAGRAMS.md**
-
-### "I need to implement fixes"
-→ Read: **GRAPH_STATE_FIXES.md**
-
-### "I need complete technical details"
-→ Read: **GRAPH_STATE_ANALYSIS.md** then **GRAPH_STATE_DIAGRAMS.md**
-
-### "I need to debug graph state issues"
-→ Read: **GRAPH_STATE_ANALYSIS.md** (section 2 & 3) + **GRAPH_STATE_DIAGRAMS.md** (diagram 1 & 2)
-
-### "I need to add multi-project support safely"
-→ Read: **GRAPH_STATE_SUMMARY.md** (session management) + **GRAPH_STATE_FIXES.md**
-
----
-
-## Key Findings Summary
-
-### The Core Issue
-**Two separate, unsynced index systems:**
-1. `ToolContext.index` - Shared, empty, used by engines
-2. `GraphOrchestrator.index` - Internal, populated during build, then discarded
-3. `Memgraph` - Database, source of truth, used by query tools
-
-### Critical Problems
-1. **Index Accumulation**: Shared index never cleared on project switch
-2. **Orphaned Build State**: Orchestrator's populated index never synced back
-3. **Startup Desync**: Shared index never populated from database
-4. **Engine Stale State**: Engines reference empty index for entire server lifetime
-
-### Immediate Impact
-- ✅ Single-project workflows: SAFE
-- ⚠️ Multi-project with sessions: RISKY (data contamination)
-- ❌ Multi-project without sessions: DANGEROUS (data mixing)
-
-### What Works
-- Query tools (use Memgraph directly)
-- FileWatcher (per-project monitoring)
-- ProjectContext switching
-
-### What Breaks
-- Embedding generation (uses empty index)
-- Progress tracking (reads empty index)
-- Architecture validation (uses empty index)
-- Multi-project data isolation
-
----
-
-## Code Location Reference
-
-| Component | File | Lines | Section |
-|-----------|------|-------|---------|
-| **ToolContext** | tool-handlers.ts | 41-46 | GRAPH_STATE_ANALYSIS §1 |
-| **ProjectContext** | tool-handlers.ts | 48-52 | GRAPH_STATE_ANALYSIS §1 |
-| **Session Management** | tool-handlers.ts | 69-106 | GRAPH_STATE_ANALYSIS §1 |
-| **graph_set_workspace** | tool-handlers.ts | 1543-1615 | GRAPH_STATE_ANALYSIS §2 |
-| **graph_rebuild** | tool-handlers.ts | 1617-1776 | GRAPH_STATE_ANALYSIS §3 |
-| **initializeEngines** | tool-handlers.ts | 290-314 | GRAPH_STATE_ANALYSIS §7 |
-| **Orchestrator** | orchestrator.ts | 70-176 | GRAPH_STATE_ANALYSIS §3 |
-| **Orchestrator.build()** | orchestrator.ts | 181-423 | GRAPH_STATE_ANALYSIS §3 |
-| **addToIndex()** | orchestrator.ts | 763-828 | GRAPH_STATE_ANALYSIS §3 |
-| **GraphIndexManager** | index.ts | 35-178 | GRAPH_STATE_ANALYSIS §4 |
-| **ProgressEngine** | progress-engine.ts | 59-96 | GRAPH_STATE_ANALYSIS §7 |
-| **Server Init** | mcp-server.ts | 618-623 | GRAPH_STATE_ANALYSIS §4 |
-
----
-
-## Recommended Reading Order
-
-### For Quick Understanding (30 minutes)
-1. GRAPH_STATE_QUICK_REF.txt (5 min)
-2. GRAPH_STATE_DIAGRAMS.md - Diagram 1 (5 min)
-3. GRAPH_STATE_DIAGRAMS.md - Diagram 2-3 (10 min)
-4. GRAPH_STATE_SUMMARY.md - "Core Problem" section (10 min)
-
-### For Complete Understanding (2 hours)
-1. GRAPH_STATE_QUICK_REF.txt (10 min)
-2. GRAPH_STATE_SUMMARY.md (30 min)
-3. GRAPH_STATE_ANALYSIS.md (60 min)
-4. GRAPH_STATE_DIAGRAMS.md (20 min)
-
-### For Implementation (4 hours)
-1. GRAPH_STATE_SUMMARY.md - "Recommended Fixes" (10 min)
-2. GRAPH_STATE_FIXES.md - Fix Level 1 (30 min to implement)
-3. GRAPH_STATE_FIXES.md - Fix Level 2 (60 min to implement)
-4. GRAPH_STATE_FIXES.md - Testing Strategy (30 min)
-5. GRAPH_STATE_FIXES.md - Validation Checklist (30 min)
-
----
-
-## Changes Required by Fix Level
-
-### Fix Level 1 (Stabilization - 30 min)
-**Files affected**: 1
-- `src/tools/tool-handlers.ts` (graph_set_workspace method)
-
-**Result**: Prevents index accumulation on context switches
-
-### Fix Level 2 (Synchronization - 2 hours)
-**Files affected**: 2
-- `src/graph/orchestrator.ts` (add getIndex method)
-- `src/tools/tool-handlers.ts` (sync in graph_rebuild)
-
-**Result**: Enables embedding and progress tracking
-
-### Fix Level 3 (Refactoring - 8+ hours)
-**Files affected**: All engines
-- `src/tools/tool-handlers.ts` (complete redesign)
-- All engine constructors
-
-**Result**: Production-ready multi-project support
-
----
-
-## Testing Resources
-
-### Unit Test Examples
-- Located in: GRAPH_STATE_FIXES.md - Testing Strategy
-- Topics covered:
- - Index clearing on context switch
- - Index syncing after rebuild
- - Index loading from Memgraph
-
-### Integration Test Examples
-- Located in: GRAPH_STATE_FIXES.md - Testing Strategy
-- Topics covered:
- - Multi-project workflow with sessions
- - Session isolation verification
-
-### Validation Checklist
-- Located in: GRAPH_STATE_FIXES.md - Validation Checklist
-- 10-item checklist for post-implementation verification
-
----
-
-## Questions Answered
-
-### Question 1: Multiple Projects Setup
-**Document**: GRAPH_STATE_SUMMARY.md - "1. Multiple Projects Setup"
-**Diagram**: GRAPH_STATE_DIAGRAMS.md - Diagram 4
-
-**Answer**: One project per session, multiple isolated sessions supported
-
-### Question 2: Context Switching
-**Document**: GRAPH_STATE_ANALYSIS.md - Section 2
-**Diagram**: GRAPH_STATE_DIAGRAMS.md - Diagram 3
-**Summary Table**: GRAPH_STATE_ANALYSIS.md - Section 9 (bottom)
-
-**Answer**: ProjectContext updated, but shared index NOT cleared
-
-### Question 3: Graph Rebuild
-**Document**: GRAPH_STATE_ANALYSIS.md - Section 3
-**Diagram**: GRAPH_STATE_DIAGRAMS.md - Diagram 2
-
-**Answer**: Creates new index internally, never syncs back to shared index
-
-### Question 4: Index Initialization
-**Document**: GRAPH_STATE_ANALYSIS.md - Section 4
-**Diagram**: GRAPH_STATE_DIAGRAMS.md - Diagram 5
-
-**Answer**: Started empty, never populated from database
-
----
-
-## Performance Impact Analysis
-
-**Read**: GRAPH_STATE_FIXES.md - "Performance Considerations"
-
-Topics:
-- Index loading performance (lazy loading solution)
-- Memory usage with multiple indices (cache eviction solution)
-- Recommended env vars for tuning
-
----
-
-## Rollback and Safety
-
-**Read**: GRAPH_STATE_FIXES.md - "Rollback Plan"
-
-Topics:
-- Per-phase rollback instructions
-- Feature flags for gradual deployment
-- Low-risk implementation order
-
----
-
-## Related Architecture Documentation
-
-The following existing documentation may provide context:
-
-- **ARCHITECTURE.md**: General system architecture
-- **QUICK_REFERENCE.md**: Tool usage and API
-- **QUICK_START.md**: Getting started guide
-
----
-
-## Version History
-
-| Version | Date | Changes |
-|---------|------|---------|
-| 1.0 | 2026-02-22 | Initial analysis and documentation |
-
----
-
-## Glossary
-
-**ToolContext**: Shared server-level context containing memgraph connection, shared index, and config
-
-**ProjectContext**: Per-session project metadata (workspace root, source dir, project ID)
-
-**GraphIndexManager**: In-memory graph index with nodes and relationships
-
-**GraphOrchestrator**: Builder that parses files and generates Cypher statements
-
-**Memgraph**: Graph database that serves as source of truth
-
-**Session**: Client-specific context identified by mcp-session-id header
-
-**Index Synchronization**: Process of copying data from one index to another
-
----
-
-## Document Statistics
-
-- **Total Documentation**: 91 KB (5 files)
-- **Code Examples**: 45+
-- **Diagrams**: 8
-- **Tables**: 15+
-- **Implementation Steps**: 30+
-- **Test Cases**: 5+ examples
-- **Code Locations Referenced**: 100+
-
----
-
-## How to Use This Documentation
-
-1. **Choose your document** based on your goal (see "Quick Navigation")
-2. **Read the recommended sections** for your use case
-3. **Reference the code locations** when examining the codebase
-4. **Implement fixes** using the step-by-step guides
-5. **Validate** using the provided checklists
-6. **Test** using the example test cases
-
----
-
-## Contributing
-
-If you find issues with this analysis or implement the fixes:
-
-1. Update the documentation with actual results
-2. Add new diagrams if you discover new patterns
-3. Share test results and performance metrics
-4. Note any deviations from the predicted behavior
-
----
-
-## Questions?
-
-Refer to:
-- **For architecture questions**: GRAPH_STATE_ANALYSIS.md
-- **For visual understanding**: GRAPH_STATE_DIAGRAMS.md
-- **For implementation help**: GRAPH_STATE_FIXES.md
-- **For quick answers**: GRAPH_STATE_QUICK_REF.txt
-- **For context**: GRAPH_STATE_SUMMARY.md
-
----
-
-**Last Updated**: February 22, 2026
-**Analysis Scope**: Complete lexRAG-MCP codebase
-**Focus Area**: Graph state management and multi-project support
diff --git a/GRAPH_STATE_SUMMARY.md b/GRAPH_STATE_SUMMARY.md
deleted file mode 100644
index a7b4cd9..0000000
--- a/GRAPH_STATE_SUMMARY.md
+++ /dev/null
@@ -1,362 +0,0 @@
-# Graph State Analysis: Executive Summary
-
-## Quick Answers to Your Questions
-
-### 1. Multiple Projects Setup
-**Is lexRAG-MCP designed to handle multiple projects simultaneously, or one at a time?**
-
-**Answer**: **One project at a time per session**, but multiple **isolated sessions** can work with different projects simultaneously.
-
-- Session A can work on Project A
-- Session B can work on Project B
-- Each session maintains its own `ProjectContext` (workspace root, source dir, project ID)
-- Sessions are identified via `mcp-session-id` header
-
-**But**: Without sessions, all requests operate on a shared default context - so truly "multiple projects simultaneously" in a single client is not supported.
-
----
-
-### 2. Project Context Switching
-
-When `graph_set_workspace` is called with a new projectId, here's what happens:
-
-| Component | What Changes | What Doesn't |
-|-----------|-------------|--------------|
-| **ProjectContext** | ✅ Updated | N/A |
-| **FileWatcher** | ✅ Restarted for new directory | N/A |
-| **Memgraph Connection** | ✅ Shared (works for any project) | N/A |
-| **GraphIndexManager (in-memory)** | ❌ NOT CLEARED | Still has old project's data |
-| **ProgressEngine** | ❌ NOT RESET | Still has old project's tasks/features |
-| **ArchitectureEngine** | ❌ NOT RESET | Still references old index |
-| **TestEngine** | ❌ NOT RESET | Still references old index |
-| **EmbeddingEngine** | ❌ NOT RESET | Still references old index |
-| **HybridRetriever** | ❌ NOT RESET | Still references old index |
-
-**Critical Finding**: Engines hold references to a **shared, never-cleared index**. When you switch projects without rebuilding, engines still have data from the old project.
-
----
-
-### 3. Graph Rebuild Behavior
-
-When `graph_rebuild` is called:
-
-```
-Does it:
-├─ Clear the in-memory index first?
-│ └─ NO - The shared index is NOT cleared
-│
-├─ Append to existing index?
-│ └─ NO - Only Orchestrator's internal index is populated
-│
-└─ Load the index from Memgraph?
- └─ NO - Creates a new index from scratch by parsing files
-```
-
-**What Actually Happens**:
-
-1. **Orchestrator creates its own private GraphIndexManager** (separate from shared index)
-2. **Parses source files** and populates its internal index
-3. **Generates Cypher statements** from parsed data
-4. **Sends Cypher to Memgraph** (database is updated)
-5. **Returns build statistics**
-6. **Discards its internal index** (never synced back)
-7. **Shared ToolContext.index remains empty** (never populated)
-
-**Result**:
-- Database (Memgraph) is updated ✅
-- Shared index stays empty ❌
-- Orchestrator's index is wasted/discarded ❌
-
----
-
-### 4. Index Initialization
-
-When tools are initialized, the GraphIndexManager gets populated from:
-
-```
-WHERE does it get populated?
-
-Startup:
-├─ src/mcp-server.ts:618 → new GraphIndexManager()
-└─ Result: EMPTY
-
-After graph_rebuild:
-├─ Orchestrator.build() populates ITS index
-├─ But never syncs to ToolContext.index
-└─ Result: Still EMPTY in ToolContext
-
-When tools run:
-├─ Tools query Memgraph directly (not index)
-├─ Embedding engine queries shared index (EMPTY)
-├─ Progress tracking reads shared index (EMPTY)
-└─ Result: Queries work, but engines fail
-```
-
-**Answer**: **Started empty and usually stays empty.**
-
-The shared index is:
-- Not populated from Memgraph ✗
-- Not populated from file system ✗
-- Not populated from Orchestrator after builds ✗
-- Only populated by manual `.addNode()` calls ✓
-
----
-
-## The Core Problem
-
-**Two separate, unsynced index systems exist:**
-
-```
-┌─────────────────────────────────────────────┐
-│ ToolContext.index (Shared) │
-│ - Initialized empty │
-│ - Used by: All engines │
-│ - Problem: Never updated │
-│ - Status: Empty or stale │
-└─────────────────────────────────────────────┘
-
-┌─────────────────────────────────────────────┐
-│ GraphOrchestrator.index (Internal) │
-│ - Created during build │
-│ - Populated with parsed data │
-│ - Problem: Never synced back │
-│ - Status: Temporary, then discarded │
-└─────────────────────────────────────────────┘
-
-┌─────────────────────────────────────────────┐
-│ Memgraph Database (Source of Truth) │
-│ - Updated by Orchestrator's Cypher │
-│ - Queried directly by tools │
-│ - Used for: Persistent storage │
-│ - Status: Current and accurate │
-└─────────────────────────────────────────────┘
-```
-
-**Why This Matters**:
-- Tools querying Memgraph work ✅
-- Embedding engine gets empty index ❌
-- Progress tracking fails ❌
-- Architecture validation fails ❌
-- Multi-project support is risky ❌
-
----
-
-## Design Issues Summary
-
-### Issue #1: Index Accumulation on Project Switch
-```
-Sequential Scenario:
-1. graph_set_workspace(projectId: "A") → ProjectContext = A
-2. graph_rebuild() → Database updated with Project A data
-3. [some queries with Project A data work]
-4. graph_set_workspace(projectId: "B") → ProjectContext = B
-5. [but shared index still has Project A nodes!]
-6. Embedding engine generates vectors for Project A's code
-7. Cross-project contamination ❌
-```
-
-**Fix**: Clear index in `graph_set_workspace()` when switching projects
-
-### Issue #2: Orphaned Build Index
-```
-graph_rebuild execution:
-1. Orchestrator builds and populates ITS index
-2. Sends Cypher to Memgraph ✓
-3. Orchestrator.index discarded
-4. ToolContext.index never updated ✗
-
-Result: Database accurate, cache empty
-```
-
-**Fix**: Sync orchestrator index back to shared index after build
-
-### Issue #3: Index Never Loaded at Startup
-```
-Server startup:
-1. new GraphIndexManager() → EMPTY
-2. initializeEngines() → all engines reference EMPTY index
-3. ProgressEngine.loadFromGraph() reads EMPTY index
-4. Result: No data until first build, and only in Orchestrator
-```
-
-**Fix**: Load index from Memgraph when switching projects
-
-### Issue #4: Engines Have Long-Lived References
-```
-Engine lifecycle:
-- Initialized ONCE at server startup
-- Never recreated on project switch
-- Hold references to shared index
-- If index isn't updated, engines get stale data
-
-Example:
-1. ProgressEngine initialized with empty index
-2. graph_set_workspace(projectA)
-3. graph_rebuild() updates Memgraph but not shared index
-4. ProgressEngine.getProgress() reads empty index
-5. Result: No progress data even though database is populated
-```
-
-**Fix**: Either update shared index after rebuild, or make engines query Memgraph directly
-
----
-
-## Impact Analysis
-
-### What Works
-- ✅ Multi-project workflows with session IDs
-- ✅ ProjectContext switching
-- ✅ FileWatcher per project
-- ✅ graph_query tool (uses Memgraph)
-- ✅ code_explain tool (uses Memgraph)
-- ✅ find_pattern tool (uses Memgraph)
-- ✅ arch_validate tool (uses Memgraph)
-- ✅ graph_rebuild (populates database)
-
-### What Breaks
-- ❌ Embedding generation (uses empty index)
-- ❌ Vector search (depends on embeddings)
-- ❌ Progress tracking (reads empty index)
-- ❌ Architecture engine (uses empty index)
-- ❌ Test engine (uses empty index)
-- ❌ Community detection (might use index)
-- ❌ Hybrid retrieval (uses empty index)
-
-### Risk Level
-- **Single project, single session**: LOW
-- **Multiple projects, session IDs**: MEDIUM (cross-project data leakage)
-- **Multiple projects, no session IDs**: HIGH (complete data mixing)
-
----
-
-## Recommended Fixes (Priority Order)
-
-### Priority 1: Prevent Data Corruption (30 min)
-```typescript
-// In graph_set_workspace()
-if (oldContext.projectId !== nextContext.projectId) {
- this.context.index.clear();
-}
-```
-**Why**: Prevents accumulation of data from multiple projects
-
-### Priority 2: Enable Core Engines (2-3 hours)
-```typescript
-// After orchestrator.build() completes
-this.context.index.clear(); // For full rebuild
-const orchIndex = this.orchestrator.getIndex();
-syncIndexes(orchIndex, this.context.index);
-```
-**Why**: Makes embedding, progress tracking, and validation work
-
-### Priority 3: Add ProjectId Filters (1 hour)
-```typescript
-// In all Memgraph queries
-WHERE n.projectId = $projectId
-```
-**Why**: Ensures queries respect project boundaries
-
-### Priority 4: Refactor for Scalability (1-2 days)
-```typescript
-// Use project-scoped indices instead of single shared index
-this.projectIndices: Map
-```
-**Why**: Future-proof architecture, complete isolation
-
----
-
-## Files Changed by Each Fix
-
-### Fix 1 (Index Clearing)
-- `src/tools/tool-handlers.ts` (graph_set_workspace method)
-
-### Fix 2 (Index Syncing)
-- `src/graph/orchestrator.ts` (add getIndex() method)
-- `src/tools/tool-handlers.ts` (graph_rebuild method)
-
-### Fix 3 (Index Loading)
-- `src/tools/tool-handlers.ts` (initializeEngines method)
-
-### Fix 4 (Project-Scoped Indices)
-- `src/tools/tool-handlers.ts` (entire index management)
-- All engine constructors (to use dynamic index getter)
-
----
-
-## Session Management Best Practices
-
-### For Multi-Project Support
-```
-Always include mcp-session-id header:
-
-Client A:
-POST /initialize
-Response: {"mcp-session-id": "sess-a", ...}
-
-POST /tools/graph_set_workspace
-Header: mcp-session-id: sess-a
-Body: {projectId: "project-a"}
-
-Client B:
-POST /initialize
-Response: {"mcp-session-id": "sess-b", ...}
-
-POST /tools/graph_set_workspace
-Header: mcp-session-id: sess-b
-Body: {projectId: "project-b"}
-
-Result: Each session has isolated ProjectContext
-```
-
-### Without Sessions (Not Recommended for Multi-Project)
-```
-Default behavior uses defaultActiveProjectContext
-All requests share the same project
-```
-
----
-
-## Related Code Locations
-
-| Component | File | Lines | Notes |
-|-----------|------|-------|-------|
-| **ToolContext** | `src/tools/tool-handlers.ts` | 41-46 | Shared index lives here |
-| **ProjectContext** | `src/tools/tool-handlers.ts` | 48-52 | Per-session project metadata |
-| **Session Management** | `src/tools/tool-handlers.ts` | 69-106 | Context getters/setters |
-| **graph_set_workspace** | `src/tools/tool-handlers.ts` | 1543-1615 | Project switching logic |
-| **graph_rebuild** | `src/tools/tool-handlers.ts` | 1617-1776 | Build and index population |
-| **Orchestrator** | `src/graph/orchestrator.ts` | 70-176 | Has its own index |
-| **Orchestrator.build** | `src/graph/orchestrator.ts` | 181-423 | Populates internal index |
-| **addToIndex** | `src/graph/orchestrator.ts` | 763-828 | Populates orchestrator index |
-| **GraphIndexManager** | `src/graph/index.ts` | 35-178 | Index implementation |
-| **ProgressEngine** | `src/engines/progress-engine.ts` | 59-96 | Loads from shared index |
-| **Server Initialization** | `src/mcp-server.ts` | 618-623 | Creates shared index |
-
----
-
-## Testing Checklist
-
-- [ ] Test single project workflow
-- [ ] Test project switching with correct data isolation
-- [ ] Test embedding generation after rebuild
-- [ ] Test progress tracking after rebuild
-- [ ] Test multi-project with different session IDs
-- [ ] Test cross-project query filtering
-- [ ] Test index clearing on context switch
-- [ ] Test index sync after rebuild
-- [ ] Verify no stale data in queries
-- [ ] Performance test with large projects
-
----
-
-## Conclusion
-
-lexRAG-MCP is **designed for single-project workflows** but has **session-based infrastructure for multiple projects**. The main limitation is **graph state synchronization** between the in-memory index and the database.
-
-**Recommended approach for multi-project support**:
-
-1. **Short-term (this week)**: Apply Fix 1 + Fix 2 (1-2 hours) to stabilize
-2. **Medium-term (this month)**: Apply Fix 3 (2-3 hours) to enable all engines
-3. **Long-term (this quarter)**: Apply Fix 4 (1-2 days) for production-ready multi-project support
-
-Current risk level: **MEDIUM** - Multi-project workflows work but have data isolation issues that could cause subtle bugs.
diff --git a/LXRAG_ANALYSIS_REPORT.md b/LXRAG_ANALYSIS_REPORT.md
deleted file mode 100644
index fa18be7..0000000
--- a/LXRAG_ANALYSIS_REPORT.md
+++ /dev/null
@@ -1,436 +0,0 @@
-# LexRAG-MCP Project Analysis Report
-
-**Generated**: 2026-02-22 | **Analysis Method**: lxRAG Tools Only
-
----
-
-## 🎯 Project Overview
-
-**Project**: lexrag-mcp
-**Category**: MCP (Model Context Protocol) Server for Code Intelligence
-**Status**: Active Development
-**Documentation**: 26 markdown files indexed
-
-### Core Purpose
-
-Provides graph-based code intelligence via MCP integration. Acts as a backend system indexing code structure, analyzing architecture, and serving code intelligence to AI assistants like Claude.
-
----
-
-## 📊 Analysis Findings
-
-### 1. Architecture Layer Configuration Issues
-
-**Status**: ⚠️ **CRITICAL** - Configuration Incomplete
-
-**Identified Violations**:
-
-- `src/index.ts` - Not assigned to any layer
-- `src/mcp-server.ts` - Not assigned to any layer
-- `src/engines/*` - Not assigned to any layer
-
-**Issue**: Files not mapped to architecture layers in `.lxrag/config.json`
-
-**Known Layers Detected**:
-
-- **Types** (`src/types/**`) - Core type definitions
-
-**Root Cause**: Missing or incomplete lxRAG architecture configuration
-
----
-
-### 2. Graph Building Status
-
-**Status**: ⚠️ **IN PROGRESS** - Partial Data Available
-
-**Observations**:
-
-- Graph rebuild initiated in full mode
-- 26 documentation files successfully indexed (10.7 seconds)
-- Code graph still processing - limited symbol data available so far
-- No entry points detected yet (suggests graph still building)
-
-**Impact**:
-
-- Cannot yet provide full impact analysis for code changes
-- Limited symbol dependency tracking
-- Architectural rules not fully validated
-
----
-
-### 3. Backend Errors Encountered
-
-**Status**: 🔴 **RECOVERABLE ERROR**
-
-**BigInt Type Conversion Error**:
-
-```
-TypeError: Cannot mix BigInt and other types, use explicit conversions
-```
-
-**Affected Tool**: `mcp_lxrag_graph_health()`
-
-**Severity**: Recoverable (non-fatal)
-**Impact**: Cannot verify graph health metrics during rebuild
-
----
-
-### 4. Code Intelligence Features Status
-
-| Feature | Status | Notes |
-| --------------------------------------- | ------------------ | ----------------------------------- |
-| Pattern Detection (TODO/FIXME/BUG/HACK) | ⏳ Implemented | Waiting for graph completion |
-| Circular Dependency Detection | ❌ Not Implemented | Requires full graph traversal |
-| Unused Code Detection | ✓ Working | No unused code found yet |
-| Architecture Validation | ⚠️ Partial | Only 2 files validated |
-| Impact Analysis | ⏳ Partial | Returns 0 impact (graph incomplete) |
-| Doc Indexing | ✓ Complete | 26 files indexed, search disabled |
-| Layer Suggestions | ✓ Working | Correctly suggests `src/types/**` |
-
----
-
-## 📁 Project Structure Insights
-
-### Detected Components
-
-Based on lxRAG analysis and workspace structure:
-
-```
-src/
-├── Operators & Transports
-│ ├── mcp-server.ts [⚠️ No layer assigned]
-│ ├── server.ts [⚠️ Needs routing]
-│ ├── request-context.ts
-│ └── config.ts
-│
-├── Core Engines
-│ ├── architecture-engine.ts
-│ ├── community-detector.ts
-│ ├── coordination-engine.ts
-│ ├── docs-engine.ts [src/engines/]
-│ ├── episode-engine.ts
-│ ├── migration-engine.ts
-│ └── ...
-│
-├── Infrastructure
-│ ├── graph/ [Graph building & querying]
-│ ├── parsers/ [Code parsing]
-│ ├── vector/ [Vector storage & search]
-│ ├── tools/ [Tool implementations]
-│ ├── response/ [Response formatting]
-│ ├── utils/ [Utilities]
-│ └── types/ [✓ Layer assigned]
-│
-└── CLI & Testing
- ├── cli/
- │ ├── build.ts
- │ ├── query.ts
- │ ├── test-affected.ts
- │ └── validate.ts
- └── test-harness.ts
-```
-
-### Documentation Found (26 files)
-
-Key documents indexed:
-
-- `ARCHITECTURE.md` - High-level design
-- `QUICK_START.md` - Setup instructions
-- `QUICK_REFERENCE.md` - Tool catalog
-- `GRAPH_STATE_*.md` - Graph state documentation (6 files)
-- `ACTION_PLAN_LXRAG_TOOL_FIXES.md` - Task list
-- `AGENT_CONTEXT_ENGINE_PLAN.md` - Roadmap
-- And 16+ additional documentation files
-
----
-
-## 🚨 Pending Tasks Identified
-
-### From Reference Documentation
-
-Based on found documentation in `/docs`:
-
-#### Phase 1: Graph State Fixes
-
-**Status**: Documented in `GRAPH_STATE_ANALYSIS.md` and related files
-**Action Items**:
-
-- [ ] Resolve BigInt type conversion errors
-- [ ] Fix graph health metric collection
-- [ ] Ensure proper graph node relationships
-- [ ] Validate temporal graph state
-
-#### Phase 2: Agent Context Engine
-
-**Status**: Roadmap in `AGENT_CONTEXT_ENGINE_PLAN.md`
-**Timeline**: 1-2 weeks per phase
-**Goals**:
-
-- [ ] Episode-based agent memory
-- [ ] Structured observation persistence
-- [ ] Semantic + temporal + graph search on episodes
-
-#### Phase 3: Tool Fixes & Improvements
-
-**Status**: Tracked in `ACTION_PLAN_LXRAG_TOOL_FIXES.md`
-**Issues**:
-
-- [ ] lxRAG tool contract validation
-- [ ] Improve error handling in tools
-- [ ] Complete missing tool implementations
-
----
-
-## 🏗️ Architecture Configuration Needs
-
-### Missing `.lxrag/config.json` Configuration
-
-**Problem**: No layer assignments for most source files
-
-**Required Fixes**:
-
-```json
-{
- "layers": [
- {
- "id": "core",
- "name": "Core Server",
- "paths": ["src/index.ts", "src/mcp-server.ts", "src/server.ts"]
- },
- {
- "id": "engines",
- "name": "Execution Engines",
- "paths": ["src/engines/**"]
- },
- {
- "id": "graph",
- "name": "Graph Infrastructure",
- "paths": ["src/graph/**"]
- },
- {
- "id": "tools",
- "name": "Tool Implementations",
- "paths": ["src/tools/**"]
- },
- {
- "id": "infrastructure",
- "name": "Infrastructure",
- "paths": ["src/parsers/**", "src/vector/**", "src/response/**"]
- },
- {
- "id": "types",
- "name": "Type Definitions",
- "paths": ["src/types/**"]
- }
- ],
- "rules": [
- "infrastructure -> *",
- "types -> *",
- "tools -> engines",
- "tools -> graph",
- "engines -> graph",
- "engines -> infrastructure",
- "graph -> infrastructure"
- ]
-}
-```
-
----
-
-## 📋 Comprehensive Action Plan
-
-### Phase 1: Backend Health & Configuration ⚠️ CRITICAL
-
-**Duration**: 1 day
-**Priority**: HIGHEST
-
-1. **Fix Backend BigInt Error**
- - [ ] Update graph health check to handle BigInt conversions
- - [ ] Test `graph_health()` after fix
- - [ ] Verify graph rebuild completion
-
-2. **Configure Architecture Layers**
- - [ ] Create `.lxrag/config.json` with proper layer definitions
- - [ ] Assign all source files to appropriate layers
- - [ ] Run `arch_validate` to confirm no violations
-
-3. **Complete Graph Rebuild**
- - [ ] Monitor rebuild progress
- - [ ] Verify all 38 tools have data available
- - [ ] Confirm no entry point errors
-
-4. **Validate Tool Suite**
- - [ ] Test all tools with actual graph data
- - [ ] Verify pattern detection works
- - [ ] Confirm architecture validation returns accurate results
-
----
-
-### Phase 2: Code Intelligence Features 📊 HIGH
-
-**Duration**: 2-3 days
-**Priority**: HIGH
-**Depends**: Phase 1
-
-1. **Enable Advanced Pattern Detection**
- - [ ] Implement circular dependency detection
- - [ ] Set up unused code detection
- - [ ] Create custom violation patterns
-
-2. **Improve Impact Analysis**
- - [ ] Fix impact_analyze to use full graph
- - [ ] Add test impact estimation
- - [ ] Calculate blast radius accurately
-
-3. **Documentation Search**
- - [ ] Re-enable doc search tool
- - [ ] Create semantic search indices
- - [ ] Test cross-linking features
-
----
-
-### Phase 3: Agent Context Engine 🧠 MEDIUM
-
-**Duration**: 1-2 weeks
-**Priority**: MEDIUM
-**Depends**: Phase 2
-
-From `AGENT_CONTEXT_ENGINE_PLAN.md`:
-
-1. **Episode Persistence**
- - [ ] Implement structured episode storage
- - [ ] Add temporal graph modeling
- - [ ] Create episode retrieval APIs
-
-2. **Memory Management**
- - [ ] Build agent observation capture
- - [ ] Implement decision persistence
- - [ ] Add episode search capabilities
-
-3. **Integration**
- - [ ] Connect episodes to code symbols
- - [ ] Implement bi-temporal queries
- - [ ] Add context pack generation for agents
-
----
-
-### Phase 4: CLI & Validation Tools 🔧 MEDIUM
-
-**Duration**: 1 week
-**Priority**: MEDIUM
-**Depends**: Phase 2
-
-1. **CLI Commands**
- - [ ] Complete `cli/build.ts` implementation
- - [ ] Implement `cli/query.ts` fully
- - [ ] Test `cli/test-affected.ts` end-to-end
- - [ ] Validate `cli/validate.ts` against real projects
-
-2. **Testing Infrastructure**
- - [ ] Complete `test-harness.ts` features
- - [ ] Add synthetic test generation
- - [ ] Benchmark tool performance
-
----
-
-### Phase 5: Benchmarking & Performance 📈 LOW
-
-**Duration**: 1 week
-**Priority**: LOW
-**Depends**: Phase 3
-
-From benchmarks directory:
-
-1. **Graph Tool Performance**
- - [ ] Complete benchmark matrix results
- - [ ] Identify bottlenecks
- - [ ] Optimize slow operations
-
-2. **Agent Mode Testing**
- - [ ] Validate synthetic agent results
- - [ ] Compare against real agent runs
- - [ ] Document performance regression checks
-
----
-
-## 🔍 Summary Matrix
-
-| Category | Status | Severity | Action |
-| ----------------------- | ------------- | -------- | --------------------------- |
-| **Backend Health** | 🔴 Error | CRITICAL | Fix BigInt + rebuild |
-| **Architecture Config** | ⚠️ Incomplete | CRITICAL | Create `.lxrag/config.json` |
-| **Graph State** | ⏳ Building | MEDIUM | Monitor completion |
-| **Code Intelligence** | ⚠️ Partial | HIGH | Complete graph + tests |
-| **Pattern Detection** | ⏳ Waiting | MEDIUM | Enable after phase 1 |
-| **Agent Engine** | 📝 Planned | MEDIUM | Execute phases 2-3 |
-| **Documentation** | ✓ Indexed | LOW | Enable search feature |
-| **Performance** | 📊 Tracked | LOW | Phase 5 optimizations |
-
----
-
-## 📌 Key Blockers
-
-1. **BigInt Type Error** → Blocks health checks
-2. **Missing Config** → Blocks architecture validation
-3. **Incomplete Graph** → Blocks impact analysis
-4. **Disabled Search** → Blocks doc queries
-
-All blockers are solvable and mostly configuration/backend issues.
-
----
-
-## ✅ Success Criteria
-
-- [ ] Backend errors resolved (0 BigInt errors in logs)
-- [ ] All source files assigned to layers
-- [ ] Graph rebuild completes successfully
-- [ ] All 38 lxRAG tools return valid data
-- [ ] Architecture validation shows 0 violations
-- [ ] Impact analysis works on real files
-- [ ] Documentation search operational
-- [ ] Episode engine tested end-to-end
-
----
-
-## 📖 Referenced Documentation
-
-These documents contain detailed information:
-
-**Core Setup**:
-
-- `QUICK_START.md` - Infrastructure & deployment
-- `README.md` - Project overview
-
-**Architecture & Design**:
-
-- `ARCHITECTURE.md` - System design
-- `docs/INTEGRATION_SUMMARY.md` - MCP integration
-- `docs/MCP_INTEGRATION_GUIDE.md` - Complete guide
-
-**Roadmap & Planning**:
-
-- `docs/AGENT_CONTEXT_ENGINE_PLAN.md` - 3-phase roadmap
-- `docs/ACTION_PLAN_LXRAG_TOOL_FIXES.md` - Specific fixes needed
-- `docs/COMPREHENSIVE_REVIEW_AND_REVISED_PLAN.md` - Full analysis
-
-**Debugging & Analysis**:
-
-- `GRAPH_STATE_ANALYSIS.md` - Graph internals
-- `GRAPH_STATE_DIAGRAMS.md` - State flow diagrams
-- `GRAPH_STATE_FIXES.md` - Known issues
-
-**Reference**:
-
-- `QUICK_REFERENCE.md` - All 38 tools
-- `docs/TOOL_PATTERNS.md` - Usage patterns
-- `docs/CLAUDE_INTEGRATION.md` - Claude setup
-
----
-
-## 🎯 Next Steps
-
-1. **Immediately**: Apply phase 1 fixes
-2. **This week**: Complete phases 2-3
-3. **Next week**: Optimize and benchmark (phases 4-5)
-
-All tasks documented. Ready for execution.
diff --git a/PROJECT_ANALYSIS_SUMMARY.md b/PROJECT_ANALYSIS_SUMMARY.md
deleted file mode 100644
index bf6bc39..0000000
--- a/PROJECT_ANALYSIS_SUMMARY.md
+++ /dev/null
@@ -1,470 +0,0 @@
-# LexRAG-MCP Project Analysis - Complete Summary
-
-**Analysis Date**: 2026-02-22
-**Analysis Method**: lxRAG Tools Only (38 tools available)
-**Status**: Analysis Complete ✓
-
----
-
-## Overview
-
-Complete analysis of the lexRAG-MCP project using exclusively lxRAG tools. This document consolidates all findings, errors, and actionable recommendations.
-
----
-
-## 1. KEY FINDINGS
-
-### 1.1 Critical Issues (Must Fix)
-
-#### Issue #1: BigInt Type Conversion Error
-
-- **Severity**: CRITICAL
-- **Tool**: `mcp_lxrag_graph_health()`
-- **Error**: `TypeError: Cannot mix BigInt and other types, use explicit conversions`
-- **Impact**: Cannot verify graph health during rebuild
-- **Status**: Recoverable, non-blocking
-- **Fix**: Update backend type conversions
-
-#### Issue #2: Missing Architecture Configuration
-
-- **Severity**: CRITICAL
-- **Files**: `src/index.ts`, `src/mcp-server.ts`, `src/engines/**` (3+ files)
-- **Problem**: Not assigned to architecture layers
-- **Impact**: Blocks full architecture validation
-- **Fix**: Create `.lxrag/config.json` with proper layer definitions
-- **Time**: 15 minutes
-
-#### Issue #3: Incomplete Graph Rebuild
-
-- **Severity**: HIGH
-- **Status**: In Progress
-- **Data**: 26 docs indexed, code graph still building
-- **Impact**: Limited symbol intelligence available
-- **Fix**: Wait for rebuild + trigger new scan after config fix
-- **Time**: 2-5 minutes (rebuild)
-
----
-
-### 1.2 Positive Findings
-
-✓ **Documentation Well-Indexed**
-
-- 26 markdown files successfully indexed (10.7s)
-- All documentation discoverable
-- Ready for semantic search (when enabled)
-
-✓ **Architecture Layers Partially Defined**
-
-- `src/types/**` layer correctly identified
-- Infrastructure expected structure recognized
-- Layer suggestions working properly
-
-✓ **Tool Infrastructure Operational**
-
-- All 38 lxRAG tools available
-- Pattern detection search implemented
-- Architecture validation functional
-- Impact analysis framework ready
-
-✓ **Project Structure Clear**
-
-- 7 major subsystems identified
-- CLI tools present
-- 50+ markdown documents with plans
-
----
-
-### 1.3 Tool Status Matrix
-
-| Tool Category | Status | Notes |
-| -------------------- | ------------- | ----------------------------------- |
-| **Graph Operations** | ⏳ Building | Rebuild in progress, no symbols yet |
-| **Architecture** | ⚠️ Partial | Configuration needed |
-| **Analysis** | ✓ Functional | Pattern detection ready |
-| **Documentation** | ✓ Indexed | 26 files, search disabled |
-| **Validation** | ⚠️ Partial | Working with current data |
-| **CLI** | 📊 Planned | Implementation in phases 4 |
-| **Agent Engine** | 📋 Documented | Full roadmap in plans |
-
----
-
-## 2. PROJECT STRUCTURE IDENTIFIED
-
-### 2.1 Core Subsystems
-
-```
-lexRAG-MCP/
-├── Server Core [src/]
-│ ├── Entry Points: index.ts, server.ts, mcp-server.ts
-│ └── Configuration: config.ts, env.ts, request-context.ts
-│
-├── Execution Engines [src/engines/]
-│ ├── architecture-engine.ts - Architecture analysis
-│ ├── docs-engine.ts - Documentation processing
-│ ├── episode-engine.ts - Memory management (Phase 3)
-│ ├── coordination-engine.ts - Task coordination
-│ ├── community-detector.ts - Community analysis
-│ ├── migration-engine.ts - Migration coordination
-│ └── [+ 5 more engines]
-│
-├── Graph System [src/graph/]
-│ ├── builder.ts - Graph construction
-│ └── [graph operations]
-│
-├── Tool Implementations [src/tools/]
-│ ├── tool-handlers.ts - Main implementation
-│ └── [individual tool handlers]
-│
-├── Infrastructure [src/]
-│ ├── vector/ - Vector storage & search
-│ ├── parsers/ - Code parsing
-│ ├── response/ - Response formatting
-│ └── utils/ - Utilities
-│
-└── CLI & Testing [src/cli/, src/test*]
- ├── build.ts - Graph building
- ├── query.ts - Query execution
- ├── test-affected.ts - Test selection
- └── validate.ts - Architecture validation
-```
-
-### 2.2 Documentation Assets
-
-**Found 26 Indexed Documents**:
-
-Core Setup:
-
-- `QUICK_START.md` - Getting started
-- `README.md` - Overview
-- `QUICK_REFERENCE.md` - Tool catalog
-
-Architecture & Design:
-
-- `ARCHITECTURE.md` - System design
-- `docs/INTEGRATION_SUMMARY.md` - MCP integration
-- `docs/MCP_INTEGRATION_GUIDE.md` - Detailed guide
-- `docs/CLAUDE_INTEGRATION.md` - Claude setup
-
-Roadmap & Tasks:
-
-- `docs/AGENT_CONTEXT_ENGINE_PLAN.md` - 3-phase roadmap
-- `docs/ACTION_PLAN_LXRAG_TOOL_FIXES.md` - Fixes needed
-- `docs/COMPREHENSIVE_REVIEW_AND_REVISED_PLAN.md` - Full analysis
-
-Graph & State:
-
-- `GRAPH_STATE_ANALYSIS.md` - Graph internals
-- `GRAPH_STATE_DIAGRAMS.md` - State flow diagrams
-- `GRAPH_STATE_FIXES.md` - Known fixes
-- `GRAPH_STATE_INDEX.md` - State index
-- `GRAPH_STATE_QUICK_REF.txt` - Quick reference
-- `GRAPH_STATE_SUMMARY.md` - Summary
-
-Reference & Patterns:
-
-- `docs/TOOL_PATTERNS.md` - Usage patterns
-- `docs/CLAUDE_INTEGRATION.md` - Claude integration
-- `LXRAG_ANALYSIS_REPORT.md` - Detailed analysis
-
-Plus 8+ additional documentation files.
-
----
-
-## 3. LXRAG TOOLS ANALYSIS
-
-### 3.1 Tools Used in Analysis
-
-```
-✓ mcp_lxrag_init_project_setup
- → Status: Successful
- → Result: Workspace initialized, rebuild queued
-
-✓ mcp_lxrag_graph_rebuild
- → Status: In Progress
- → Result: Full rebuild initiated, queued
-
-✓ mcp_lxrag_graph_health
- → Status: Failed (BigInt error)
- → Error: TypeError with numeric conversions
-
-✓ mcp_lxrag_index_docs
- → Status: Successful
- → Result: 26 documents indexed (10.7s)
-
-✓ mcp_lxrag_context_pack
- → Status: Waiting on graph
- → Result: No data yet (graph incomplete)
-
-✓ mcp_lxrag_reflect
- → Status: Successful
- → Result: 0 learnings (graph empty)
-
-✓ mcp_lxrag_arch_validate
- → Status: Successful (partial data)
- → Result: 2 violations found (unassigned files)
-
-✓ mcp_lxrag_arch_suggest
- → Status: Successful
- → Result: Layer suggestions working
-
-✓ mcp_lxrag_find_pattern
- → Status: Implemented (awaiting data)
- → Patterns: TODO, FIXME, BUG, HACK ready
-
-✓ mcp_lxrag_impact_analyze
- → Status: Ready (limited data)
- → Result: No impact found yet (graph incomplete)
-
-✓ mcp_lxrag_ref_query
- → Status: Successful
- → Result: Found 5 reference docs from sibling projects
-
-✗ mcp_lxrag_search_docs
- → Status: Disabled
- → Result: Tool not available in environment
-
-✗ mcp_lxrag_graph_health
- → Status: Error (BigInt conversion)
- → Result: Cannot verify rebuild status
-```
-
-### 3.2 Tools Not Yet Fully Tested
-
-Tools awaiting complete graph build:
-
-- Pattern detection (circular, unused, violation)
-- Complete impact analysis
-- Full dependency tracking
-- Comprehensive symbol discovery
-
----
-
-## 4. ERRORS DOCUMENTED
-
-### Error Log
-
-#### Error #1: BigInt Type Conversion (graph_health)
-
-```
-ErrorCode: GRAPH_HEALTH_FAILED
-Message: TypeError: Cannot mix BigInt and other types, use explicit conversions
-Tool: mcp_lxrag_graph_health()
-Severity: Recoverable
-Timestamp: 2026-02-22T[time]
-Attempt: 1/3 - Failed
-Attempt: 2/3 - Failed
-Status: Attempted fix not yet applied
-```
-
-**Analysis**:
-
-- Occurs in graph metric aggregation
-- Mixing number + BigInt without explicit conversion
-- Non-fatal, doesn't block rebuild
-- Fix requires backend update
-
-#### Error #2: Tool Disabled (search_docs)
-
-```
-ErrorCode: TOOL_DISABLED
-Message: Tool mcp_lxrag_search_docs is currently disabled by the user
-Tool: mcp_lxrag_search_docs()
-Severity: Non-critical
-Impact: Documentation search unavailable
-Status: Configuration issue
-```
-
-**Analysis**:
-
-- Feature is implemented but not enabled
-- Likely requires permission flag
-- Can be enabled in environment setup
-
----
-
-## 5. RESOLUTION PLAN SUMMARY
-
-### Phase 1: Backend Stabilization (1 day) 🔴 CRITICAL
-
-1. Create `.lxrag/config.json` ← **Most Important**
-2. Fix BigInt type conversion
-3. Force graph rebuild
-4. Validate 0 violations
-
-### Phase 2: Code Intelligence (2-3 days) 🟠 HIGH
-
-1. Run all tool tests
-2. Enable pattern detection
-3. Validate impact analysis
-4. Enable doc search
-5. Full test suite
-
-### Phase 3: Agent Engine (1-2 weeks) 🟡 MEDIUM
-
-1. Episode storage
-2. Memory persistence
-3. Episode search
-4. Integration tests
-
-### Phase 4: CLI Tools (1 week) 🟡 MEDIUM
-
-1. Build command
-2. Query command
-3. Test-affected
-4. Validate command
-
-### Phase 5: Performance (1 week) 🟢 LOW
-
-1. Benchmarking
-2. Bottleneck analysis
-3. Optimizations
-4. Regression testing
-
----
-
-## 6. RECOMMENDED IMMEDIATE ACTIONS
-
-### Right Now (5 minutes)
-
-1. ✓ Review this analysis
-2. Create `.lxrag/config.json` (template provided in RESOLUTION_PLAN.md)
-3. Commit configuration
-
-### Hour 1
-
-1. Run `npm run graph:rebuild -- --full`
-2. Monitor with `npm run graph:health -- --poll 5s`
-3. Verify 0 architecture violations
-
-### Day 1
-
-1. Complete Phase 1 checklist
-2. Begin Phase 2 tool validation
-3. Document any new errors
-
-### This Week
-
-1. Complete Phases 1-2
-2. Begin Phase 3 planning
-3. Run full test suite
-
----
-
-## 7. DELIVERABLES CREATED
-
-By this analysis, the following documents have been created:
-
-1. **ERROR_REPORT.md**
- - Error catalog
- - Tool status matrix
- - Troubleshooting guide
-
-2. **LXRAG_ANALYSIS_REPORT.md**
- - Detailed findings
- - Structure analysis
- - Task identification
- - Action plan (5 phases)
-
-3. **RESOLUTION_PLAN.md**
- - Step-by-step implementation guide
- - Code examples
- - Validation checklists
- - Risk assessment
-
-4. **PROJECT_ANALYSIS_SUMMARY.md** (this file)
- - Executive overview
- - Key findings
- - Tool status
- - Immediate next steps
-
----
-
-## 8. SUCCESS CRITERIA
-
-### Phase 1 Complete ✓ When:
-
-- [ ] `.lxrag/config.json` created
-- [ ] `arch_validate` shows 0 violations
-- [ ] Graph rebuild completes
-- [ ] 20+ symbols indexed
-
-### Phase 2 Complete ✓ When:
-
-- [ ] All tool tests pass
-- [ ] Pattern detection working
-- [ ] Impact analysis returns data
-- [ ] Doc search operational
-- [ ] Full test suite passing
-
-### Phase 3 Complete ✓ When:
-
-- [ ] Episodes persist
-- [ ] Memory retrieval works
-- [ ] Semantic search accurate
-- [ ] Integration tests pass
-
-### Project Complete ✓ When:
-
-- [ ] All 5 phases done
-- [ ] No critical errors
-- [ ] Full test coverage
-- [ ] Performance targets met
-
----
-
-## 9. CONFIDENCE ASSESSMENT
-
-| Area | Confidence | Reason |
-| ---------------------- | ---------- | --------------------------- |
-| Problem Identification | 95% | lxRAG tools verified issues |
-| Architecture Issues | 90% | Config validation confirmed |
-| Backend Error | 85% | Error message clear |
-| Resolution Path | 90% | Based on documented plans |
-| Timeline Estimates | 75% | Depends on issue complexity |
-| Success Probability | 85% | All blockers are fixable |
-
----
-
-## 10. REFERENCE DOCUMENTS
-
-All analysis based on examination of:
-
-**30+ Source Files**:
-
-- Entry points (index.ts, server.ts, mcp-server.ts)
-- 12+ Engine implementations
-- 8+ Graph operations
-- 10+ Tool handlers
-
-**26 Documented Sources**:
-
-- Architecture specifications
-- Integration guides
-- Action plans
-- Phase roadmaps
-
-**Benchmarks & Tests**:
-
-- 12+ test artifact directories
-- Performance metrics
-- Agent mode results
-
----
-
-## Conclusion
-
-The lexRAG-MCP project is **well-structured and well-documented** but currently blocked by **3 fixable configuration issues**:
-
-1. Missing architecture configuration (15 min fix)
-2. BigInt type error (may be self-resolving with rebuild)
-3. Incomplete graph (waits for configuration + rebuild)
-
-**With these fixed, the project moves to full code intelligence operation within 2-3 days.**
-
-All analysis performed using only lxRAG tools (no file reads, no grep). Full documentation provided in linked analysis reports.
-
----
-
-**Analysis Complete**
-**Ready for Implementation**
-**All Discoveries Documented**
diff --git a/Pipfile b/Pipfile
new file mode 100644
index 0000000..c8829bd
--- /dev/null
+++ b/Pipfile
@@ -0,0 +1,11 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = false
+name = "pip_conf_index_global"
+
+[packages]
+
+[dev-packages]
+
+[requires]
+python_version = "3.10"
diff --git a/QUICK_REFERENCE.md b/QUICK_REFERENCE.md
index ac577ea..6f2a1dd 100644
--- a/QUICK_REFERENCE.md
+++ b/QUICK_REFERENCE.md
@@ -1,4 +1,4 @@
-# lxRAG MCP — Quick Reference
+# lxDIG MCP — Quick Reference
## Session Flow (MCP HTTP)
@@ -28,7 +28,7 @@ npm run start:http
curl http://localhost:9000/health
```
-## 33 MCP Tools
+## 39 MCP Tools
### Graph / Querying
@@ -104,9 +104,25 @@ curl http://localhost:9000/health
### Utility
-| Tool | Purpose |
-| ------------------- | ---------------------------------- |
-| `contract_validate` | Normalize and validate tool inputs |
+| Tool | Purpose |
+| ------------------- | ------------------------------------------- |
+| `contract_validate` | Normalize and validate tool inputs |
+| `tools_list` | List all registered tools with descriptions |
+
+### Setup
+
+| Tool | Purpose |
+| ---------------------------- | ---------------------------------------------- |
+| `init_project_setup` | One-shot workspace init: set context + rebuild |
+| `setup_copilot_instructions` | Generate `.github/copilot-instructions.md` |
+
+### Docs & Reference
+
+| Tool | Purpose |
+| ------------- | ------------------------------------------- |
+| `index_docs` | Index markdown documentation into the graph |
+| `search_docs` | Search indexed docs by text or symbol |
+| `ref_query` | Query a reference repository for patterns |
## Common Workflows
@@ -149,13 +165,13 @@ curl http://localhost:9000/health
MEMGRAPH_HOST=localhost # default: localhost
MEMGRAPH_PORT=7687 # default: 7687
MCP_PORT=9000 # default: 9000
-LXRAG_PROJECT_ID=my-repo # optional: default project namespace
-LXRAG_USE_TREE_SITTER=true # enable AST-accurate parsers (requires optional deps)
+LXDIG_PROJECT_ID=my-repo # optional: default project namespace
+LXDIG_USE_TREE_SITTER=true # enable AST-accurate parsers (requires optional deps)
```
## Tree-sitter Parsers
-When `LXRAG_USE_TREE_SITTER=true`, AST-accurate parsers activate for:
+When `LXDIG_USE_TREE_SITTER=true`, AST-accurate parsers activate for:
| Language | Extensions | Fallback |
| ---------- | --------------------- | -------------- |
@@ -181,7 +197,7 @@ Grammars are `optionalDependencies` — missing grammars fall back silently.
| Other language parsers | `src/parsers/tree-sitter-parser.ts` |
| Engines | `src/engines/` |
| Docker stack | `docker-compose.yml` |
-| Runbook | `docs/GRAPH_EXPERT_AGENT.md` |
+| Runbook | `docs/TOOL_PATTERNS.md` |
## Troubleshooting
diff --git a/QUICK_START.md b/QUICK_START.md
index 9b7576f..48728d4 100644
--- a/QUICK_START.md
+++ b/QUICK_START.md
@@ -1,6 +1,6 @@
-# Setup & Quick Start — LxRAG MCP Server
+# Setup & Quick Start — lxDIG MCP Server
-Everything you need to go from zero to a fully wired LxRAG instance: infrastructure up, server running, your project indexed, and your editor connected.
+Everything you need to go from zero to a fully wired lxDIG instance: infrastructure up, server running, your project indexed, and your editor connected.
The server supports two transports — pick the one that matches your client:
@@ -25,8 +25,8 @@ Both transports expose all 38 tools and require the same infrastructure (Memgrap
| VS Code | 1.99+ (for MCP agent mode in Copilot) |
```bash
-git clone https://github.com/lexCoder2/lxRAG-MCP.git
-cd lxRAG-MCP
+git clone https://github.com/lexCoder2/lxDIG-MCP.git
+cd lxDIG-MCP
npm install && npm run build
```
@@ -155,10 +155,10 @@ Create `.vscode/mcp.json` in the root of **your project** and commit it. VS Code
```json
{
"servers": {
- "lxrag": {
+ "lxdig": {
"type": "stdio",
"command": "node",
- "args": ["/absolute/path/to/lxRAG-MCP/dist/server.js"],
+ "args": ["/absolute/path/to/lxDIG-MCP/dist/server.js"],
"env": {
"MCP_TRANSPORT": "stdio",
"MEMGRAPH_HOST": "localhost",
@@ -182,7 +182,7 @@ Open Copilot Chat → switch to **Agent** mode → the 38 tools are available im
```json
{
"servers": {
- "lxrag": {
+ "lxdig": {
"type": "http",
"url": "http://localhost:9000/mcp"
}
@@ -194,7 +194,7 @@ Or add it globally via **VS Code Settings** (`Cmd/Ctrl+,`) → search `mcp`:
```json
"github.copilot.chat.mcp.servers": {
- "lxrag": {
+ "lxdig": {
"type": "http",
"url": "http://localhost:9000/mcp"
}
@@ -228,7 +228,7 @@ Edit the config file for your OS:
"mcpServers": {
"code-graph": {
"command": "node",
- "args": ["/absolute/path/to/lxRAG-MCP/dist/server.js"],
+ "args": ["/absolute/path/to/lxDIG-MCP/dist/server.js"],
"env": {
"MCP_TRANSPORT": "stdio",
"MEMGRAPH_HOST": "localhost",
@@ -257,7 +257,7 @@ Copy the provided instructions template into **your project** so your agent auto
```bash
mkdir -p /path/to/your-project/.github
-cp /path/to/lxRAG-MCP/.github/copilot-instructions.md \
+cp /path/to/lxDIG-MCP/.github/copilot-instructions.md \
/path/to/your-project/.github/copilot-instructions.md
```
diff --git a/README.md b/README.md
index 6c44b10..135d909 100644
--- a/README.md
+++ b/README.md
@@ -1,227 +1,252 @@
-
-
-
LxRAG MCP
-
short for lexic RAG
-
A memory and code intelligence layer for LLM agents.
+
+
lxDIG MCP — Code Graph Intelligence & Persistent Agent Memory for AI Coding Assistants
+
Stop RAGing, start DIGging.
+
+
Dynamic Intelligence Graph · Agent Memory · Multi-Agent Coordination
+
An open-source Model Context Protocol (MCP) server that gives AI coding assistants persistent memory, structural code graph analysis, and safe multi-agent coordination — beyond static RAG and GraphRAG.
-
-
-
-
-
-
+[](https://modelcontextprotocol.io)
+[](https://www.npmjs.com/package/@stratsolver/graph-server)
+[](https://nodejs.org)
+[](https://www.typescriptlang.org)
+[](https://memgraph.com)
+[](https://qdrant.tech)
+[](LICENSE)
+[](src)
+[](QUICK_START.md)
+[](QUICK_START.md)
---
-LxRAG Server is your MCP-native memory and code intelligence layer for smarter, faster AI-assisted development.
+> **Works with:** VS Code Copilot · Claude Code · Claude Desktop · Cursor · any MCP-compatible AI assistant
-Turn your repository into a queryable graph so your agents can answer architecture, impact, and planning questions without re-reading the entire codebase on every turn — and so you can stop wasting context budget on files that haven't changed.
+**Supported languages:** TypeScript · JavaScript · TSX/JSX · Python · Go · Rust · Java
+**Databases:** Memgraph (graph) · Qdrant (vector)
+**Transports:** stdio (local) · HTTP (remote/fleet)
-**[→ QUICK_START.md](QUICK_START.md)** — deploy, connect your vscode editor with ease, wire Copilot or Claude, make your first query (~5 min).
-**[→ QUICK_REFERENCE.md](QUICK_REFERENCE.md)** — all 38 tools with parameters, look the process.
+---
-If you find this project helpful (I hope you do) consider [buying me a coffee ☕](https://buymeacoffee.com/hi8g)
+## What is lxDIG MCP?
-## At a glance
+An open-source **Model Context Protocol (MCP) server** that adds a **persistent code intelligence layer** to AI coding assistants — Claude Code, VS Code Copilot, Cursor, and Claude Desktop. Unlike static RAG or batch-oriented GraphRAG, lxDIG MCP is a live, incrementally-updated intelligence graph that turns any repository into a queryable knowledge graph — so agents can answer architectural questions, track decisions across sessions, coordinate safely in multi-agent workflows, and run only the tests that actually changed — without re-reading the entire codebase on every turn.
-| Capability | What you get |
-| --------------------------- | --------------------------------------------------------------------- |
-| **Code graph intelligence** | Cross-file dependency answers instead of raw file dumps |
-| **Agent memory** | Persistent decisions and episodes that survive session restarts |
-| **Hybrid retrieval** | Better relevance for natural-language code questions |
-| **Temporal model** | Historical queries (`asOf`) and change diffs (`diff_since`) |
-| **Test intelligence** | Impact-scoped test selection so you only run what matters |
-| **Docs & ADR indexing** | Search your READMEs and decision records the same way you search code |
-| **MCP-native runtime** | Works with VS Code Copilot, Claude, and any MCP-compatible client |
+It is purpose-built for the **agentic coding loop**: the cycle of understand → plan → implement → verify → remember that AI agents (Claude, Copilot, Cursor) repeat continuously.
-## Why you need this
+**The core problem it solves:** most AI coding assistants are stateless and architecturally blind. They re-read unchanged files on every session, miss cross-file relationships, forget past decisions, and collide when multiple agents work in parallel. lxDIG MCP is the memory and structure layer that fixes all four.
-Most code intelligence tools cover one layer — RAG embeddings, graph structure, or agent memory — but not all three. That means:
+---
-- ❌ Context lost between sessions — re-reading unchanged files on every restart
-- ❌ Probabilistic retrieval misses architectural relationships
-- ❌ No temporal reasoning — can't query past states or track change impact
-- ❌ Multi-agent collisions with no built-in coordination
+## Table of Contents
+
+- [Why lxDIG?](#why-use-a-code-graph-mcp-server-problems-lxdig-solves)
+- [Key capabilities](#key-capabilities-code-graph-agent-memory--multi-agent-coordination)
+- [How it works](#how-lxdig-mcp-works-graph--vector--bm25-hybrid-retrieval)
+- [Visualize your code graph](#visualize-your-code-graph--lxdig-visual)
+- [Quick start](#quick-start)
+- [39 MCP tools — at a glance](#39-mcp-tools--at-a-glance)
+- [Use cases](#use-cases-claude-code-vs-code-copilot-cursor--ci-pipelines)
+- [Comparison with alternatives](#lxdig-mcp-vs-rag-graphrag-github-copilot--langchain-agents)
+- [Performance](#performance)
+- [Roadmap](#roadmap)
+- [Contributing](#contributing)
+- [Support the project](#support-the-project)
+- [License](#license)
-### The LxRAG Advantage
+---
-LxRAG uniquely combines all three layers purpose-built for code:
+## Why Use a Code Graph MCP Server? Problems lxDIG Solves
-**1. Graph Structure — not RAG embeddings**
+Most code intelligence tools solve **one** of these problems. lxDIG solves all of them together:
-- Files, symbols, and relationships in a queryable graph (Memgraph)
-- **Deterministic structural reasoning** (vs probabilistic embeddings)
-- Cross-file dependency answers instead of relevance-ranked chunks
-- Understands architecture; embeddings miss it
+| Problem | Without lxDIG | With lxDIG |
+| ----------------------------------- | ------------------------------------------------- | ---------------------------------------------------------- |
+| **Context loss between sessions** | Agent re-reads everything on restart | Persistent episode + decision memory survives restarts |
+| **Architecturally blind retrieval** | Embeddings miss cross-file relationships | Graph traversal finds structural dependencies |
+| **Probabilistic search misses** | Semantic search returns nearest chunks, not facts | Hybrid graph + vector + BM25 fused with RRF |
+| **Multi-agent collisions** | Two agents edit the same file simultaneously | Claims/release protocol with conflict detection |
+| **Wasted CI time** | Full test suite on every change | Impact-scoped test selection — only affected tests run |
+| **Stale architecture knowledge** | Agent guesses at layer boundaries | Graph-validated architecture rules + placement suggestions |
+| **Queries eat context budget** | Raw file dumps, hundreds of tokens per answer | Cross-file answers in compact, budget-aware responses |
-**2. Session Persistence & Agent Memory — survives restarts**
+---
-- Persistent episode memory: observations, decisions, edits, test results
-- Temporal reasoning: query code state at any point in history (`asOf`, `diff_since`)
-- Claims/release workflow prevents multi-agent collisions
-- **No external database setup required** (baked into Memgraph)
+## Key Capabilities: Code Graph, Agent Memory & Multi-Agent Coordination
-**3. Hybrid Retrieval — graph + vector + BM25**
+### 1. Code graph intelligence
-- Graph traversal (finds architectural connections)
-- Vector similarity (finds semantic concepts)
-- BM25 lexical search (finds keywords)
-- Reciprocal Rank Fusion merges all three signals
-- **Result**: 10x-6000x more accurate than embeddings alone
+Turn your repository into a **queryable property graph** of files, functions, classes, imports, and their relationships. Ask questions in plain English or Cypher.
-**4. MCP Tools — 38 deterministic, automatable actions**
+- Natural-language + Cypher graph queries (`graph_query`)
+- Symbol-level explanation with full dependency context (`code_explain`)
+- Pattern detection and architecture rule validation (`find_pattern`, `arch_validate`)
+- Architecture placement suggestions for new code (`arch_suggest`)
+- Semantic code slicing — targeted line ranges from a natural query (`semantic_slice`)
+- Find duplicate or similar code across the codebase (`find_similar_code`, `code_clusters`)
-- `graph_query` — Natural language + Cypher code discovery
-- `code_explain` — Full dependency context (not just definition)
-- `impact_analyze` — Blast radius of changes (not manual checking)
-- `test_select` — Exact affected tests (not full suite)
-- `arch_validate` — Rule-based violation detection (not keyword search)
-- - 33 more specialized tools built for code intelligence
+### 2. Persistent agent memory
-### What lxRAG covers that others don't
+Your agent **remembers** what it decided, what it changed, what broke, and what it observed — even after a VS Code restart or a Claude Desktop session ends.
-| Capability | lxRAG | Others |
-|---|---|---|
-| Session persistence | ✅ Native | ❌ / ⚠️ External setup |
-| Agent memory + temporal reasoning | ✅ Episodes + `asOf` | ❌ Not available |
-| Cross-file graph reasoning | ✅ Graph edges | ⚠️ Shallow or manual |
-| Multi-agent safety | ✅ Claims/releases | ❌ No coordination |
-| Impact-scoped test selection | ✅ Built-in | ❌ Full suite or manual |
-| Architecture validation | ✅ Rule-based | ❌ Generic or none |
-| Open source / cost | ✅ MIT · $0 | ❌/⚠️ Closed or paid |
+- Episode memory: observations, decisions, edits, test results, errors, learnings (`episode_add`, `episode_recall`)
+- Decision log with semantic query (`decision_query`)
+- Reflection synthesis from recent episodes (`reflect`)
+- Temporal graph model: query any past code state with `asOf`, compare drift with `diff_since`
-### Performance Gains
+### 3. Multi-agent coordination
-**vs Grep/Manual (9x-6000x faster, <1% false positives)**
-**vs Vector RAG (5x token savings, 10x more relevant)**
+Run **multiple AI agents in parallel** on the same repository without conflicts.
-## What you get
+- Claim/release protocol for file, function, or task ownership (`agent_claim`, `agent_release`)
+- Fleet-wide coordination view — see what every agent is doing (`coordination_overview`, `agent_status`)
+- Context packs that assemble high-signal task briefings under strict token budgets (`context_pack`)
+- Blocker detection across agents and tasks (`blocking_issues`)
-### 1) Code intelligence on demand
+### 4. Test and change intelligence
-Ask questions about your codebase in plain English or Cypher — your agent gets cross-file dependency answers, not raw file dumps.
+Stop running your **full test suite** on every change. Know exactly what's affected.
-- Natural-language and Cypher graph querying via `graph_query`
-- Symbol-level explanation with full dependency context (`code_explain`)
-- Pattern detection and architecture rule validation (`find_pattern`, `arch_validate`)
-- Semantic code slicing for targeted line ranges (`semantic_slice`)
+- Change impact analysis — blast radius of modified files (`impact_analyze`)
+- Selective test execution — only the tests that can fail (`test_select`, `test_run`)
+- Test categorization for parallelization and prioritization (`test_categorize`, `suggest_tests`)
-### 2) Memory that survives sessions
+### 5. Documentation as a first-class knowledge source
-Your agent remembers what it decided, what it changed, and what broke — even after a VS Code restart.
+Your **READMEs, ADRs, and changelogs** become searchable graph nodes, linked to the code they describe.
-- Persistent episode memory: observations, decisions, edits, test results, errors
-- Claim/release workflow to prevent multi-agent collisions
-- Coordination views so you always know what's in flight
+- Index all markdown docs in one call (`index_docs`)
+- Full-text BM25 search across headings and content (`search_docs?query=...`)
+- Symbol-linked lookup — every doc that references a class or function (`search_docs?symbol=MyClass`)
+- Incremental re-index: only changed files are re-parsed
-### 3) Smarter test runs
+### 6. Architecture governance
-Stop running your full test suite on every change. LxRAG tells your agent exactly which tests are affected.
+Enforce **architectural boundaries** automatically and get placement guidance for new code.
-- Impact analysis scoped to changed files (`impact_analyze`)
-- Selective test execution — only tests that can actually fail (`test_select`, `test_run`)
-- Test categorisation for parallelisation and prioritisation (`test_categorize`, `suggest_tests`)
+- Layer/boundary rule validation (`arch_validate`)
+- Graph-topology-aware placement suggestions (`arch_suggest`)
+- Circular dependency and unused-code detection (`find_pattern`)
-### 4) Documentation you can query like code
+### 7. One-shot project setup
-Your READMEs, architecture decision records, and changelogs become first-class searchable graph nodes.
+Go from a fresh clone to a fully wired AI assistant in **one tool call**.
-- Index all markdown docs in one call (`index_docs`)
-- BM25 full-text search across headings and content (`search_docs?query=...`)
-- Symbol-linked lookup — find every doc that references a class or function (`search_docs?symbol=MyClass`)
-- Incremental re-index: only changed files are re-parsed on subsequent runs
+- `init_project_setup` — sets workspace, rebuilds graph, generates Copilot instructions
+- `setup_copilot_instructions` — generates `.github/copilot-instructions.md` from your repo's topology
+- Works with VS Code Copilot, Claude Code, Claude Desktop, and any MCP-compatible client
-### 5) Delivery acceleration
+---
-- Graph-backed progress and task tracking (`progress_query`, `task_update`, `feature_status`)
-- Context packs that assemble high-signal context under strict token budgets (`context_pack`)
-- Blocker detection across tasks and agents (`blocking_issues`)
+## How lxDIG MCP Works: Graph + Vector + BM25 Hybrid Retrieval
-## How it works
+lxDIG runs as an **MCP server** over stdio or HTTP and coordinates three data planes behind a single tool interface:
-LxRAG runs as an MCP server over stdio or HTTP and coordinates three data planes behind a single tool interface:
+```
+┌─────────────────────────────────────────────────────────────┐
+│ MCP Tool Surface (39 tools) │
+│ stdio transport (local) │ HTTP transport (remote/fleet) │
+└──────────────┬────────────┴────────────────┬────────────────┘
+ │ │
+ ┌───────────▼────────────┐ ┌────────────▼────────────┐
+ │ Graph Plane │ │ Vector Plane │
+ │ Memgraph (Bolt) │ │ Qdrant │
+ │ ───────────────── │ │ ───────────────────── │
+ │ FILE · FUNC · CLASS │ │ Semantic embeddings │
+ │ IMPORT · CALL edges │ │ Nearest-neighbor search│
+ │ Temporal tx history │ │ Natural-language code │
+ └────────────────────────┘ └─────────────────────────┘
+ │
+ ┌───────────▼────────────────────────────────────────────┐
+ │ Hybrid Retrieval (RRF fusion) │
+ │ Graph expansion + Vector similarity + BM25 lexical │
+ └────────────────────────────────────────────────────────┘
+```
-- **Graph plane (Memgraph)** — structural and temporal truth: FILE, FUNCTION, CLASS, IMPORT nodes + relationships + full transaction history
-- **Vector plane (Qdrant)** — semantic retrieval for natural-language questions; optional but recommended for large codebases
-- **Response plane** — answer-first shaping with profile budgets so you choose between token-light (`compact`) and detail-rich (`debug`) responses
+When you call `graph_query` in natural language mode, retrieval runs as **hybrid fusion**:
-When you call `graph_query` in natural mode, retrieval runs as hybrid fusion:
+1. Vector similarity search (semantic concepts)
+2. BM25 lexical search (keyword matches)
+3. Graph expansion from seed nodes (structural relationships)
+4. **Reciprocal Rank Fusion (RRF)** merges all three signals into a single ranked result
-1. Vector similarity search
-2. BM25 / lexical search (Memgraph `text_search` when available, local fallback otherwise)
-3. Graph expansion from seed nodes
-4. Reciprocal Rank Fusion (RRF) merges all signals into a single ranked list
+The result: structurally accurate, semantically relevant answers — not just the closest embedding match.
### System diagram

-## Tooling surface
+---
-The server exposes **38 MCP tools** across:
+## Visualize Your Code Graph — lxDIG Visual
-- Graph/querying (4): `graph_set_workspace`, `graph_rebuild`, `graph_health`, `graph_query`
-- Code intelligence (5): `code_explain`, `find_pattern`, `semantic_slice`, `context_pack`, `diff_since`
-- Architecture (2): `arch_validate`, `arch_suggest`
-- Semantic/similarity (4): `semantic_search`, `find_similar_code`, `code_clusters`, `semantic_diff`
-- Test intelligence (5): `test_select`, `test_categorize`, `impact_analyze`, `test_run`, `suggest_tests`
-- Progress/operations (4): `progress_query`, `task_update`, `feature_status`, `blocking_issues`
-- Memory/coordination (8): `episode_add`, `episode_recall`, `decision_query`, `reflect`, `agent_claim`, `agent_release`, `agent_status`, `coordination_overview`
-- Runtime controls (1): `contract_validate`
-- Documentation (2): `index_docs`, `search_docs`
-- Reference (1): `ref_query` — query a sibling repo for architecture insights, patterns, and code examples
-- Setup (2): `init_project_setup`, `setup_copilot_instructions` — one-shot onboarding and AI assistant scaffolding
+**[lxDIG Visual](https://github.com/lexCoder2/lxDIG-visual)** is the open-source browser-based visualization layer for lxDIG MCP. It renders your code dependency graph as an **interactive, navigable canvas** — turning abstract code relationships into a tangible spatial representation you can explore.
-## Quick start
+**Key features:**
-> **Recommended setup:** run Memgraph and Qdrant in Docker (`docker compose up -d memgraph qdrant`), then run the MCP server on your host via stdio. Your editor spawns the process directly — native filesystem paths, no HTTP port, no session headers.
+- **Force-directed interactive graph** — files, functions, and classes rendered as explorable nodes with physics-based positioning
+- **Expand-by-depth navigation** — double-click any node to progressively reveal its direct relationships
+- **Architecture layer awareness** — color-coded module boundaries and structural compliance indicators
+- **Multi-agent visualization** — real-time view of coordination when multiple AI agents are active via lxDIG MCP
+- **Live + mock modes** — connects to your running Memgraph instance or uses built-in fallback data
-### Prerequisites
+**Setup** (shares the same Memgraph instance as lxDIG MCP — no extra database needed):
-- Node.js 24+
-- Docker + Docker Compose
+```bash
+git clone https://github.com/lexCoder2/lxDIG-visual.git
+cd lxDIG-visual
+npm install && cp .env.example .env
+npm run dev:all
+# Open http://localhost:5173
+```
-> See [QUICK_START.md](QUICK_START.md) for full VS Code + Copilot/Claude wiring instructions.
+After indexing with `graph_rebuild`, changes appear in the visual explorer immediately — no manual refresh required.
-### 1) Clone and build
+> → [github.com/lexCoder2/lxDIG-visual](https://github.com/lexCoder2/lxDIG-visual)
-```bash
-git clone https://github.com/lexCoder2/lxRAG-MCP.git
-cd lxRAG-MCP
-npm install && npm run build
-```
+---
+
+## Quick Start
+
+> **Recommended setup:** Memgraph + Qdrant in Docker, MCP server on your host via stdio. Your editor spawns and owns the process — no HTTP ports, no session headers.
+
+### Prerequisites
-### 2) Start the databases
+| Requirement | Version |
+| ----------------------- | -------- |
+| Node.js | 24+ |
+| Docker + Docker Compose | 24+ (v2) |
-Launch only Memgraph and Qdrant — the MCP server runs locally via stdio, not in Docker:
+### 1. Clone and build
```bash
-docker compose up -d memgraph qdrant
+git clone https://github.com/lexCoder2/lxDIG-MCP.git
+cd lxDIG-MCP
+npm install && npm run build
```
-Verify they are healthy:
+### 2. Start the databases
```bash
-docker compose ps memgraph qdrant # both should show "healthy" / "running"
+docker compose up -d memgraph qdrant
+docker compose ps # wait for "healthy" (~30 s)
```
-### 3) Configure stdio in your editor
+### 3. Wire your editor
-**VS Code** — add to your `.vscode/mcp.json` (or user `settings.json`):
+**VS Code — add to `.vscode/mcp.json`:**
```json
{
"servers": {
- "lxrag": {
+ "lxdig": {
"type": "stdio",
"command": "node",
- "args": ["/absolute/path/to/lxRAG-MCP/dist/server.js"],
+ "args": ["/absolute/path/to/lxDIG-MCP/dist/server.js"],
"env": {
"MCP_TRANSPORT": "stdio",
"MEMGRAPH_HOST": "localhost",
@@ -234,14 +259,14 @@ docker compose ps memgraph qdrant # both should show "healthy" / "running"
}
```
-**Claude Desktop** — add to `claude_desktop_config.json`:
+**Claude Desktop — add to `claude_desktop_config.json`:**
```json
{
"mcpServers": {
- "lxrag": {
+ "lxdig": {
"command": "node",
- "args": ["/absolute/path/to/lxRAG-MCP/dist/server.js"],
+ "args": ["/absolute/path/to/lxDIG-MCP/dist/server.js"],
"env": {
"MCP_TRANSPORT": "stdio",
"MEMGRAPH_HOST": "localhost",
@@ -254,9 +279,7 @@ docker compose ps memgraph qdrant # both should show "healthy" / "running"
}
```
-### 4) Initialize your project
-
-Once the server is connected in your editor, run this single tool call to set context, index the graph, and generate copilot instructions in one step:
+### 4. Initialize your project (one call)
```json
{
@@ -269,168 +292,232 @@ Once the server is connected in your editor, run this single tool call to set co
}
```
-That's it — the graph rebuild runs in the background and your project is ready to query.
+This single call sets the workspace context, rebuilds the code graph, and generates `.github/copilot-instructions.md` for your project. Your agent is ready to query.
-### Session flow diagram
+**Total setup time: ~5 minutes.** See [QUICK_START.md](QUICK_START.md) for the full guide including Docker, Claude Desktop, and HTTP transport.
-
+---
-### Visual examples
+## 39 MCP Tools — At a Glance
+
+| Category | Tools | What they do |
+| ------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------- |
+| **Graph / querying** | `graph_set_workspace` `graph_rebuild` `graph_health` `graph_query` | Index and query the code graph |
+| **Code intelligence** | `code_explain` `find_pattern` `semantic_slice` `context_pack` `diff_since` | Understand structure and change |
+| **Architecture** | `arch_validate` `arch_suggest` | Enforce boundaries, guide placement |
+| **Semantic / similarity** | `semantic_search` `find_similar_code` `code_clusters` `semantic_diff` | Find related code by meaning |
+| **Test intelligence** | `test_select` `test_categorize` `impact_analyze` `test_run` `suggest_tests` | Run only what matters |
+| **Progress / ops** | `progress_query` `task_update` `feature_status` `blocking_issues` | Track delivery and blockers |
+| **Agent memory** | `episode_add` `episode_recall` `decision_query` `reflect` | Persist and retrieve agent knowledge |
+| **Coordination** | `agent_claim` `agent_release` `agent_status` `coordination_overview` | Safe multi-agent parallelism |
+| **Documentation** | `index_docs` `search_docs` | Search your READMEs and ADRs like code |
+| **Reference** | `ref_query` | Query a sibling repo for patterns and examples |
+| **Setup** | `init_project_setup` `setup_copilot_instructions` `contract_validate` `tools_list` | One-shot onboarding |
-| Workflow | Minimal tool sequence | Outcome |
-| ------------------------------ | ------------------------------------------------------ | ---------------------------------------------- |
-| **Boot a project context** | `initialize` → `graph_set_workspace` → `graph_rebuild` | Graph becomes query-ready for that MCP session |
-| **Understand a subsystem** | `graph_query` → `code_explain` → `semantic_slice` | Dependency map + concrete code slice |
-| **Plan safe changes** | `impact_analyze` → `test_select` → `test_run` | Change radius + focused test execution |
-| **Coordinate multiple agents** | `agent_claim` → `context_pack` → `task_update` | Ownership, task context, and durable progress |
+---
-#### Example A — Set workspace context
+## Use Cases: Claude Code, VS Code Copilot, Cursor & CI Pipelines
-```json
-{
- "name": "graph_set_workspace",
- "arguments": {
- "workspaceRoot": "/workspace",
- "sourceDir": "src",
- "projectId": "my-repo"
- }
-}
-```
+### Individual developer — Claude Code or VS Code Copilot
-#### Example B — Natural graph query
+- Ask "what calls `AuthService.login` across the whole repo?" and get a graph answer, not a file dump
+- Resume a refactoring task after a VS Code restart — your agent remembers every decision
+- Run `impact_analyze` before committing — know exactly which tests to run
+- Use `arch_validate` to catch layer violations before they become bugs
+- Explore your dependency graph visually with [lxDIG Visual](https://github.com/lexCoder2/lxDIG-visual)
-```json
-{
- "name": "graph_query",
- "arguments": {
- "query": "find key graph files",
- "language": "natural",
- "mode": "local",
- "limit": 5
- }
-}
-```
+### Engineering team — multi-agent workflows
-#### Example C — Context pack for an active task
+- Run a planning agent and an implementation agent in parallel without file conflicts
+- Use `coordination_overview` to see what every agent is working on
+- `context_pack` hands off a high-signal task briefing between agents in one call
+- Persistent decision memory means the second agent doesn't repeat work the first already did
-```json
-{
- "name": "context_pack",
- "arguments": {
- "task": "stabilize hybrid retrieval outputs",
- "taskId": "PHASE8-RET-01",
- "agentId": "agent-copilot",
- "profile": "compact"
- }
-}
-```
+### CI / automation pipeline
-## Runtime modes
+- `graph_health` as a startup readiness gate
+- `test_select` + `test_run` for impact-scoped CI that's 5–10x faster than full suite
+- `arch_validate` as an automated architecture compliance check on every PR
-- **stdio** ✅ recommended for local editor integrations (VS Code, Claude Desktop, Cursor) — simplest setup, no HTTP port or session headers needed
-- **http** — for multi-client agent fleets, remote access, or automation pipelines that need concurrent sessions
+### Repository onboarding
+
+- `init_project_setup` on a new codebase — graph + copilot instructions in ~30 seconds
+- `code_explain` to understand unfamiliar subsystems with full dependency context
+- `setup_copilot_instructions` generates AI assistant instructions tailored to your repo's topology
+
+---
+
+## lxDIG MCP vs RAG, GraphRAG, GitHub Copilot & LangChain Agents
+
+| Feature | lxDIG MCP | Plain RAG / embeddings | GitHub Copilot (built-in) | Custom LangChain agent |
+| ------------------------------- | ------------------------ | ---------------------- | ------------------------- | ---------------------- |
+| Cross-file structural reasoning | ✅ Graph edges | ❌ Chunks only | ⚠️ Limited | ⚠️ Manual setup |
+| Persistent agent memory | ✅ Episodes + decisions | ❌ Stateless | ❌ Stateless | ⚠️ Custom DB needed |
+| Multi-agent coordination | ✅ Claims/releases | ❌ None | ❌ None | ❌ Custom setup |
+| Temporal code model | ✅ `asOf` + `diff_since` | ❌ | ❌ | ❌ |
+| Impact-scoped test selection | ✅ Built-in | ❌ | ❌ | ❌ |
+| Architecture validation | ✅ Rule-based | ❌ | ❌ | ❌ |
+| Interactive graph visualization | ✅ lxDIG Visual | ❌ | ❌ | ❌ |
+| MCP-native (any AI client) | ✅ 39 tools | ❌ | ❌ | ❌ |
+| Open source / self-hosted | ✅ MIT | ⚠️ Varies | ❌ Closed | ✅ |
+| Setup complexity | Medium (Docker) | Low | None | High |
+
+---
+
+## Performance
+
+Benchmarks run against a synthetic 20-scenario agent task suite (`benchmarks/`):
+
+| Metric | Result |
+| ----------------------------------------------------------- | ----------------------------------------------- |
+| Scenarios where lxDIG was faster than baseline | **15 / 20** |
+| MCP-only successful scenarios (baseline could not complete) | **4 / 20** |
+| vs Grep / manual file reads | **9x–6000x faster**, <1% false positives |
+| vs pure vector RAG | **5x token savings**, 10x more relevant results |
+
+> Benchmarks are workload-dependent. Run `npm run benchmark:check-regression` against your own repository for accurate numbers.
+
+---
+
+## What's Already Shipped
+
+Every feature below is **production-ready today**:
+
+- ✅ **Hybrid retrieval** for `graph_query` — vector + BM25 + graph expansion fused with RRF
+- ✅ **AST-accurate parsers** via tree-sitter for TypeScript, TSX, JS/MJS/CJS, JSX, Python, Go, Rust, Java
+- ✅ **Watcher-driven incremental rebuilds** — graph stays fresh without manual intervention _(requires `LXDIG_ENABLE_WATCHER=true`)_
+- ✅ **Temporal code model** — `asOf` queries any past graph state; `diff_since` shows what changed
+- ✅ **Indexing-time symbol summaries** — compact-profile answers stay useful in tight token budgets
+- ✅ **Leiden community detection + PageRank PPR** with JS fallbacks for non-MAGE environments
+- ✅ **SCIP IDs** on all FILE, FUNCTION, and CLASS nodes for precise cross-tool symbol references
+- ✅ **Episode memory, agent coordination, context packs, and response budget shaping**
+- ✅ **Docs & ADR indexing** — markdown parsed into graph nodes; queried by text or symbol association
+- ✅ **Interactive graph visualization** via [lxDIG Visual](https://github.com/lexCoder2/lxDIG-visual) — force-directed canvas explorer
+- ✅ **402 tests** across parsers, builders, engines, and tool handlers — all green
+
+---
+
+## Runtime Modes
+
+| Mode | Best for | Command |
+| ------------------------ | ---------------------------------------------------- | -------------------- |
+| **stdio** ✅ recommended | VS Code Copilot, Claude Code, Claude Desktop, Cursor | `npm run start` |
+| **HTTP** | Remote agents, multi-client fleets, CI pipelines | `npm run start:http` |
### Useful scripts
```bash
-npm run start # stdio server (recommended for editor use)
-npm run start:http # HTTP supervisor (multi-session / remote)
-npm run build # compile TypeScript
-npm test # run all 109 tests
+npm run start # stdio server (recommended)
+npm run start:http # HTTP supervisor (multi-session)
+npm run build # compile TypeScript
+npm test # run all 402 tests
+npm run benchmark:check-regression # check latency/token regressions
```
-## Repository map
+---
+
+## Repository Map
| Path | What's inside |
| ------------------------------------ | ------------------------------------------------------------------- |
-| `src/server.ts`, `src/mcp-server.ts` | MCP / HTTP transport surfaces |
-| `src/tools/tool-handlers.ts` | all 35 tool implementations |
-| `src/graph/` | graph client, orchestrator, hybrid retriever, watcher, docs builder |
-| `src/engines/` | architecture, test, progress, community, episode, docs engines |
-| `src/parsers/` | AST and markdown parsers (tree-sitter + regex fallback) |
-| `src/response/` | response shaping, profile budgets, summarization |
-| `docs/AGENT_CONTEXT_ENGINE_PLAN.md` | implementation plan and phase status |
-| `docs/GRAPH_EXPERT_AGENT.md` | full agent runbook |
-
-## What's already shipped
-
-Every feature below is production-ready today:
-
-- ✅ Hybrid retrieval for `graph_query` — vector + BM25 + graph expansion fused with RRF
-- ✅ AST-accurate parsers via tree-sitter for TypeScript, TSX, JS/MJS/CJS, JSX, Python, Go, Rust, Java (activate with `LXRAG_USE_TREE_SITTER=true`)
-- ✅ Watcher-driven incremental rebuilds — your graph stays fresh without manual intervention
-- ✅ Temporal query and diff support — query any past graph state with `asOf`, compare changes with `diff_since`
-- ✅ Indexing-time symbol summarization — compact-profile answers stay useful even in tight token budgets
-- ✅ MAGE-native Leiden community detection and PageRank PPR with JS fallbacks for non-MAGE environments
-- ✅ SCIP IDs on all FILE, FUNCTION, and CLASS nodes for precise cross-tool symbol references
-- ✅ Episode memory, agent coordination, context packs, and response budget shaping
-- ✅ Docs & ADR indexing — `index_docs` parses all your markdown into graph nodes; `search_docs` queries them with BM25 or by symbol association
-
-## Release highlights
-
-- **Hybrid natural retrieval** — your `graph_query` calls blend vector, BM25, and graph signals with RRF so you get the most relevant results across the whole codebase, not just the closest embedding match.
-- **Multi-language AST parsers** — tree-sitter gives you accurate symbol extraction for TypeScript, TSX, JavaScript, JSX, Python, Go, Rust, and Java. Enable with `LXRAG_USE_TREE_SITTER=true`; each language falls back gracefully if the grammar isn't installed.
-- **Impact-scoped test runs** — `impact_analyze` + `test_select` tell your agent exactly which tests to run after a change, cutting unnecessary CI time without sacrificing coverage confidence.
-- **Docs & ADR indexing** — your documentation is now searchable the same way your code is. `index_docs` walks the workspace, parses every markdown file into `DOCUMENT` and `SECTION` nodes, and stores them in the graph. `search_docs` retrieves them by text query or by symbol association.
-- **Persistent agent memory** — episodes, decisions, and claims survive across VS Code restarts so your agent can pick up exactly where it left off.
-- **Temporal code model** — `asOf` and `diff_since` let you or your agent reason about the state of any file or symbol at any point in the past.
-- **Always-current graph** — the file watcher triggers incremental rebuilds on save so your graph never goes stale.
-- **Lower-token answers** — indexing-time symbol summaries keep `compact`-profile responses genuinely useful without growing the payload.
-- **Safer BM25 fallback** — Memgraph `text_search` is used when available; the server falls back to a local lexical scorer automatically so retrieval never breaks.
-
-## Tests and quality gates
-
-The test suite covers all parsers, builders, engines, and tool handlers — 109 tests across 5 files, all green.
+| `src/server.ts`, `src/mcp-server.ts` | MCP + HTTP transport surfaces |
+| `src/tools/` | Tool handlers, registry, all 39 tool implementations |
+| `src/graph/` | Graph client, orchestrator, hybrid retriever, watcher, docs builder |
+| `src/engines/` | Architecture, test, progress, coordination, episode, docs engines |
+| `src/parsers/` | AST + markdown parsers (tree-sitter + regex fallback) |
+| `src/response/` | Response shaping, profile budgets, summarization |
+| `docs/GRAPH_EXPERT_AGENT.md` | Full agent runbook — tool priority, path rules, response shaping |
+| `docs/MCP_INTEGRATION_GUIDE.md` | Deep-dive integration guide |
+| `QUICK_START.md` | Step-by-step deployment + editor wiring (~5 min) |
-```bash
-npm test # run all 109 unit tests
-npm run benchmark:check-regression # check latency / token-efficiency regressions
-```
-
-Benchmark scripts under `scripts/` and `benchmarks/` track:
+---
-- Query latency and token efficiency
-- Retrieval accuracy trends
-- Compact-profile response budget compliance
-- Agent-mode synthetic task benchmarks
+## Integration Tips
-All new features ship with tests. The docs feature alone added 101 tests (50 parser + 23 builder + 17 engine + 11 tool-handler contract tests) before landing.
+- **Start every session** with `graph_set_workspace` → `graph_rebuild` (or configure `init_project_setup` to run automatically)
+- **Prefer `graph_query` over file reads** for discovery — far fewer tokens, cross-file context included
+- **Use `profile: compact`** in autonomous loops; switch to `balanced` or `debug` when you need detail
+- **Rebuild incrementally** after meaningful edits; the file watcher handles this automatically during active sessions
+- **Run `impact_analyze` before tests** so your agent only executes what's actually affected
+- **Open [lxDIG Visual](https://github.com/lexCoder2/lxDIG-visual)** alongside your editor for a spatial view of the graph while your agent works
-## Integration tips
+---
-A few habits that make a big difference:
+## Roadmap
-- **Start every session** with `graph_set_workspace` → `graph_rebuild` (or let your configured client do it automatically via `.github/copilot-instructions.md`)
-- **Prefer `graph_query` over file reads** for discovery — you'll use far fewer tokens and get cross-file context for free
-- **Use `profile: compact`** for autonomous loops where every token counts; switch to `balanced` or `debug` when you need more detail
-- **Rebuild incrementally** after meaningful edits (`graph_rebuild` with `mode: incremental`); the file watcher handles this for you during active sessions
-- **Run `impact_analyze` before tests** so your agent only executes what's actually affected by a change
+lxDIG is open source and self-hosted today. Planned work ahead — see [ROADMAP.md](ROADMAP.md) for the full prioritized backlog with detail on each item.
-See:
+- [ ] Language server protocol (LSP) integration for deeper symbol resolution
+- [ ] Go, Rust, Java parser improvements
+- [ ] MCP `resources` surface (expose graph nodes as MCP resources)
+- [ ] Webhook-triggered graph rebuilds for CI environments
+- [ ] Plugin API for custom tool registration
+- [ ] **Real-time transparent graph sync** — continuous file-watching with live graph and vector index updates surfaced as observable events, so agents and users always know when the graph is current without polling `graph_health` or triggering manual rebuilds
+- [ ] **Domain knowledge layer** — attach external knowledge sources (documentation, standards, specs, research articles) directly to code symbols as graph nodes; a `calculateBMI` function links to CDC/WHO references, a payment function links to PCI-DSS rules, a GDPR-scoped model links to regulation articles — giving agents real-world context alongside structural context
+- [ ] Multi-user coordination — shared agent memory, task ownership, and conflict detection across multiple developers on the same repository
+- [ ] lxDIG Cloud — hosted, zero-infrastructure version for individuals and teams
-- `.github/copilot-instructions.md`
-- `docs/GRAPH_EXPERT_AGENT.md`
-- [QUICK_START.md](QUICK_START.md): step-by-step deployment, VS Code project wiring, and Copilot / Claude extension configuration
+---
## Contributing
-Pull requests are welcome! Whether it's a new parser, a tool improvement, a bug fix, or better docs — open an issue to discuss what you'd like to change, or just send a PR directly.
+Pull requests are welcome. Whether it's a new parser, a tool improvement, a bug fix, or better docs — contributions of all sizes move this project forward.
-- **Bugs / features** — open an issue first so we can align on scope
-- **New tools** — follow the handler + registration pattern in `src/tools/tool-handlers.ts` and `src/server.ts`; include tests
+- **Bugs / features** — open an issue first to align on scope
+- **New tools** — follow the handler + registration pattern in `src/tools/`; include tests
+- **New language parsers** — add tree-sitter grammar + tests in `src/parsers/`
- **Docs** — typos, clarifications, and examples are always appreciated
-[→ Open a pull request](https://github.com/lexCoder2/lxRAG-MCP/pulls)
+[→ Open a pull request](https://github.com/lexCoder2/lxDIG-MCP/pulls) · [→ Browse open issues](https://github.com/lexCoder2/lxDIG-MCP/issues)
+
+---
-## Support this project
+## Support the Project
-LxRAG MCP is built and maintained in my personal time — researching graph retrieval techniques, designing the tool surface, writing tests, and keeping everything working across MCP protocol updates. a cup of coffe or any help you can provide will make a difference, If it saves you time or makes your AI-assisted workflows meaningfully better, consider supporting the work:
+lxDIG MCP is built and maintained in personal time — researching graph retrieval techniques, designing the tool surface, writing tests, and keeping everything working across MCP protocol updates. If it saves you time or makes your AI-assisted workflows meaningfully better, consider supporting the work:
- **GitHub Sponsors** → [github.com/sponsors/lexCoder2](https://github.com/sponsors/lexCoder2)
- **Buy Me a Coffee** → [buymeacoffee.com/hi8g](https://buymeacoffee.com/hi8g)
-Every contribution — no matter the size — helps keep the project active and lets me prioritize new features and support over other obligations. Thank you. 🙏
+---
+
+## FAQ
+
+**Q: Does lxDIG require a cloud service or API key?**
+No. lxDIG runs entirely on your machine. Memgraph and Qdrant run in Docker containers you control. No data leaves your environment.
+
+**Q: Does it work with Cursor?**
+Yes. Any MCP-compatible client works. Add the stdio config to Cursor's MCP settings the same way as VS Code.
+
+**Q: How large a codebase can it handle?**
+The graph plane (Memgraph) scales to millions of nodes. For very large monorepos, use `sourceDir` to scope indexing to the relevant subdirectory. Incremental rebuilds keep the graph fresh without re-indexing everything.
+
+**Q: Do I need to run Qdrant?**
+Qdrant is optional but recommended for large codebases. Without it, `semantic_search` and `find_similar_code` are unavailable; all other tools continue to work via graph-only or BM25 retrieval.
+
+**Q: Can multiple developers on a team share one lxDIG instance?**
+Yes, via HTTP transport. One running instance handles multiple independent sessions. Team-level shared memory is on the lxDIG Cloud roadmap.
+
+**Q: Is this production-ready?**
+The core tools are stable and tested (402 tests, all green). Treat it as beta — APIs may change before a 1.0 release. Pin your version and watch the changelog.
+
+**Q: Is lxDIG MCP the same as GraphRAG?**
+No. GraphRAG is a batch retrieval technique applied to documents. lxDIG MCP is a live, incrementally-updated **code graph** with persistent agent memory, multi-agent coordination, and impact-scoped test selection — not just a retrieval improvement.
+
+**Q: How do I add persistent memory to Claude Code?**
+Install lxDIG MCP, add the stdio config to `.vscode/mcp.json`, and call `init_project_setup` once per repository. From that point, Claude Code can call `episode_add` / `episode_recall` and `decision_query` to read and write memory that persists across sessions.
+
+**Q: Can I visualize the code graph?**
+Yes. [lxDIG Visual](https://github.com/lexCoder2/lxDIG-visual) is the companion browser-based graph explorer. It shares the same Memgraph instance — run `npm run dev:all` in the lxDIG-visual repo and open `http://localhost:5173`.
+
+---
## License
-MIT
+[MIT](LICENSE) — free to use, modify, and distribute.
+
+---
+
+
diff --git a/RESOLUTION_PLAN.md b/RESOLUTION_PLAN.md
deleted file mode 100644
index 5b156d2..0000000
--- a/RESOLUTION_PLAN.md
+++ /dev/null
@@ -1,587 +0,0 @@
-# LexRAG-MCP Resolution Plan
-
-**Status**: Ready for Implementation
-**Last Updated**: 2026-02-22
-**Analysis Method**: lxRAG Tools Only
-
----
-
-## Executive Summary
-
-Analysis using only lxRAG tools has identified **3 critical blockers** preventing full code intelligence:
-
-1. **Backend BigInt Error** - Prevents graph health verification
-2. **Missing Architecture Configuration** - Files unassigned to layers
-3. **Incomplete Graph Rebuild** - Limited symbol data available
-
-**Estimated Time to Resolution**: 3-5 days for phases 1-2, 2+ weeks for full roadmap
-
----
-
-## Phase 1: Backend Stabilization & Configuration (CRITICAL)
-
-**Duration**: 1 day | **Dependency**: None | **Blocks**: Everything
-
-### 1.1 Fix BigInt Type Conversion Error
-
-**Problem**: `TypeError: Cannot mix BigInt and other types, use explicit conversions`
-
-**Location**: lxRAG backend graph_health check
-
-**Resolution Steps**:
-
-```bash
-# 1. Check lxRAG setup
-docker ps | grep -E "memgraph|qdrant"
-
-# 2. Verify MCP server status
-curl http://localhost:9000/health
-
-# 3. Look for BigInt issues in backend
-find . -type f -name "*.ts" -o -name "*.js" | xargs grep -l "BigInt" 2>/dev/null
-
-# 4. If backend, patch conversion:
-# Change: timestamp + metadata (mixing types)
-# To: BigInt(timestamp) + BigInt(metadata)
-```
-
-**Validation**:
-
-```bash
-# After fix, this should succeed:
-npm run test:mcp-integration
-# Should see: "graph_health OK"
-```
-
-### 1.2 Create Architecture Configuration
-
-**Problem**: `.lxrag/config.json` missing or incomplete
-
-**File**: `.lxrag/config.json` (create if missing)
-
-**Required Content**:
-
-```json
-{
- "projectId": "lexrag-mcp",
- "sourceDir": "src",
- "layers": [
- {
- "id": "types",
- "name": "Type System",
- "paths": ["src/types/**"],
- "description": "Core type definitions"
- },
- {
- "id": "infrastructure",
- "name": "Infrastructure",
- "paths": [
- "src/parsers/**",
- "src/vector/**",
- "src/response/**",
- "src/utils/**"
- ],
- "description": "Low-level infrastructure"
- },
- {
- "id": "graph",
- "name": "Graph Engine",
- "paths": ["src/graph/**"],
- "description": "Code graph building and querying"
- },
- {
- "id": "tools",
- "name": "Tool Implementations",
- "paths": ["src/tools/**"],
- "description": "MCP tool handlers"
- },
- {
- "id": "engines",
- "name": "Execution Engines",
- "paths": ["src/engines/**"],
- "description": "Analysis engines (architecture, docs, episodes, etc.)"
- },
- {
- "id": "core",
- "name": "Core Server",
- "paths": [
- "src/index.ts",
- "src/mcp-server.ts",
- "src/server.ts",
- "src/config.ts",
- "src/env.ts"
- ],
- "description": "Server entry points and configuration"
- },
- {
- "id": "cli",
- "name": "CLI Commands",
- "paths": ["src/cli/**"],
- "description": "Command-line interface"
- }
- ],
- "rules": [
- {
- "from": "types",
- "to": "*",
- "allow": true,
- "reason": "Type layer can be imported by all"
- },
- {
- "from": "infrastructure",
- "to": "*",
- "allow": true,
- "reason": "Infrastructure available to all"
- },
- {
- "from": "graph",
- "to": ["infrastructure", "types"],
- "allow": true
- },
- {
- "from": "tools",
- "to": ["engines", "graph", "infrastructure", "types"],
- "allow": true
- },
- {
- "from": "engines",
- "to": ["graph", "infrastructure", "tools", "types"],
- "allow": true
- },
- {
- "from": "core",
- "to": "*",
- "allow": true,
- "reason": "Core server orchestrates all"
- },
- {
- "from": "cli",
- "to": ["core", "tools", "engines"],
- "allow": true
- }
- ]
-}
-```
-
-**Validation**:
-
-```bash
-# Test configuration
-npm run validate:arch
-
-# Should show:
-# ✓ All files assigned to layers
-# ✓ 0 violations
-# ✓ Dependencies valid
-```
-
-### 1.3 Force Graph Rebuild
-
-**Command**:
-
-```bash
-# Full rebuild with new config
-npm run graph:rebuild -- --full --verbose
-
-# Monitor progress (in another terminal):
-npm run graph:health -- --poll 5s
-```
-
-**Expected Output**:
-
-```
-✓ Graph initialized
-✓ Parsing src/ directory ...
-✓ Building dependency graph ...
-✓ Indexing 26 documents ...
-✓ Creating vector embeddings ...
-✓ Rebuild complete (elapsed: ~45s)
-```
-
-### 1.4 Validate Phase 1
-
-**Checklist**:
-
-- [ ] `npm run test:mcp-integration` passes
-- [ ] `graph_health` returns 200 OK
-- [ ] `arch_validate` shows 0 violations
-- [ ] 20+ code symbols indexed
-- [ ] 26 documents indexed with embeddings
-
-**Move to Phase 2 when**: All checks ✓
-
----
-
-## Phase 2: Code Intelligence Activation (HIGH)
-
-**Duration**: 2-3 days | **Dependency**: Phase 1 | **Blocks**: Agent engine
-
-### 2.1 Validate All Tools Work
-
-```bash
-# Test each major tool
-npm run test:tools -- --category graph
-npm run test:tools -- --category architecture
-npm run test:tools -- --category impact
-npm run test:tools -- --category patterns
-```
-
-**Expected**: All tools return data, 0 timeouts
-
-### 2.2 Run Pattern Detection
-
-**Test TODO/FIXME Detection**:
-
-```bash
-npm run pattern:find -- --pattern "TODO|FIXME|BUG|HACK"
-```
-
-**Expected Output**:
-
-```
-Found patterns:
- src/engines/episode-engine.ts:42 TODO: Implement bi-temporal queries
- src/tools/tool-handlers.ts:108 FIXME: Handle null context
- ...
-```
-
-### 2.3 Test Impact Analysis
-
-```bash
-# Test with common change scenarios
-npm run impact:analyze -- src/graph/builder.ts
-npm run impact:analyze -- src/engines/architecture-engine.ts
-```
-
-**Expected**: Shows affected tests, blast radius, dependencies
-
-### 2.4 Enable Documentation Search
-
-```bash
-# Verify indexing
-npm run docs:index -- --with-embeddings
-
-# Test search
-npm run docs:search -- "agent context engine"
-npm run docs:search -- "graph state"
-```
-
-**Expected**: Returns relevant sections with scores
-
-### 2.5 Run Full Test Suite
-
-```bash
-npm run test:all
-npm run test:integration
-```
-
-**Validation**:
-
-- [ ] All unit tests pass
-- [ ] Integration tests pass
-- [ ] Pattern detection working
-- [ ] Impact analysis accurate
-- [ ] Doc search operational
-
-**Move to Phase 3 when**: All validations ✓
-
----
-
-## Phase 3: Agent Context Engine (MEDIUM)
-
-**Duration**: 1-2 weeks | **Dependency**: Phase 2
-
-Based on `docs/AGENT_CONTEXT_ENGINE_PLAN.md`
-
-### 3.1 Implement Episode Storage
-
-**Files to Create/Modify**:
-
-- `src/engines/episode-engine.ts` - Main implementation
-- `src/graph/episode-nodes.ts` - Graph schema
-- `src/types/episode.ts` - Type definitions
-
-**Key Features**:
-
-- [ ] Episode entity (observation, decision, code edit)
-- [ ] Temporal metadata (validFrom, validTo)
-- [ ] Bi-temporal queries
-- [ ] Semantic indexing
-
-### 3.2 Implement Memory Persistence
-
-**Features**:
-
-- [ ] Save episodes to Memgraph
-- [ ] Vector embedding for semantic search
-- [ ] TTL/expiry policies
-- [ ] Concurrent episode management
-
-### 3.3 Test Episode Workflows
-
-```bash
-npm run test:episodes -- --scenario "agent-learns"
-npm run test:episodes -- --scenario "memory-recall"
-```
-
----
-
-## Phase 4: CLI & Validation (MEDIUM)
-
-**Duration**: 1 week | **Dependency**: Phase 2
-
-### 4.1 Complete CLI Commands
-
-**Build Command**:
-
-```bash
-lxrag build [--project projectId] [--full|--incremental]
-```
-
-**Query Command**:
-
-```bash
-lxrag query "find all HTTP handlers" [--project projectId]
-```
-
-**Test Affected**:
-
-```bash
-lxrag test-affected [files...] [--report json]
-```
-
-**Validate**:
-
-```bash
-lxrag validate [--strict] [--fix]
-```
-
-### 4.2 Testing Infrastructure
-
-- [ ] Unit test generation
-- [ ] Integration test templates
-- [ ] Mutation testing
-- [ ] Performance profiling
-
----
-
-## Phase 5: Performance & Optimization (LOW)
-
-**Duration**: 1 week | **Dependency**: Phase 3
-
-### 5.1 Benchmarking
-
-From `benchmarks/` directory:
-
-1. **Graph Tool Performance**
- - [ ] Complete GRAPH_TOOLS_BENCHMARK_MATRIX.md
- - [ ] Identify slow operations
- - [ ] Profile graph queries
-
-2. **Agent Mode**
- - [ ] Run synthetic agent tests
- - [ ] Compare agent_mode_artifacts/
- - [ ] Measure context pack generation time
-
-### 5.2 Optimization
-
-- [ ] Cache frequent queries
-- [ ] Optimize graph traversal
-- [ ] Batch vector operations
-- [ ] Connection pooling
-
----
-
-## Implementation Checklist
-
-### Phase 1: Backend (1 day)
-
-- [x] Create `.lxrag/config.json`
-- [x] Fix BigInt error in local source (`src/tools/tool-handlers.ts`)
-- [x] Add regression test coverage for BigInt health metrics
-- [x] Force graph rebuild
-- [x] Validate all systems locally (`npm run build`, `npm test`)
-- [ ] Validate hosted/runtime `mcp_lxrag_graph_health` after service restart
-
-**Completion Criteria**:
-
-- `arch_validate` → 0 violations (after runtime sync)
-- `graph_health` → OK (after runtime sync)
-- 20+ symbols indexed
-
-### Phase 2: Intelligence (2-3 days)
-
-- [ ] Run all tool tests (runtime)
-- [ ] Verify pattern detection
-- [ ] Validate impact analysis
-- [ ] Enable doc search
-- [ ] Run full test suite
-
-**Completion Criteria**:
-
-- All tests ✓ passing
-- All tools return data
-- No BigInt errors
-
-### Phase 3: Agent Engine (1-2 weeks)
-
-- [ ] Episode storage
-- [ ] Memory persistence
-- [ ] Episode search
-- [ ] Integration tests
-
-**Completion Criteria**:
-
-- Episodes persist across sessions
-- Semantic search works
-- Agent can recall context
-
-### Phase 4: CLI (1 week)
-
-- [ ] Build command complete
-- [ ] Query command complete
-- [ ] Test-affected working
-- [ ] Validate command complete
-
-**Completion Criteria**:
-
-- CLI end-to-end tested
-- Help docs complete
-
-### Phase 5: Performance (1 week)
-
-- [ ] Benchmarks complete
-- [ ] Bottlenecks identified
-- [ ] Optimizations applied
-- [ ] Performance improved 20%+
-
-**Completion Criteria**:
-
-- Regression tests pass
-- Performance targets met
-
----
-
-## Risk Assessment
-
-### High Risk Items
-
-1. **BigInt Error** (Phase 1)
- - Risk: Backend compatibility issue
- - Mitigation: Check Node.js version (need 15.7+)
- - Fallback: Downgrade to safe math library
-
-2. **Graph Rebuild Timeout** (Phase 1)
- - Risk: Large codebases take time
- - Mitigation: Monitor with `--verbose` flag
- - Fallback: Incremental rebuild or skip docs
-
-### Medium Risk Items
-
-1. **Performance Regression** (Phase 3-5)
- - Risk: Agent memory adds overhead
- - Mitigation: Cache strategies, TTL policies
- - Fallback: Optional feature flag
-
-2. **Integration Complexity** (Phase 3)
- - Risk: Bi-temporal model tricky
- - Mitigation: Extensive testing, clear types
- - Fallback: Simplified episode model first
-
----
-
-## Success Definition
-
-### Phase 1: ✓ Passing
-
-- Backend errors resolved
-- Architecture configured
-- Graph building successfully
-
-### Phase 2: ✓ Active
-
-- All tools operational
-- Pattern detection enabled
-- Doc search working
-- Impact analysis accurate
-
-### Phase 3: ✓ Planned
-
-- Episodes stored
-- Memory persists
-- Semantic search on episodes
-- Agents can recall context
-
-### Overall Success:
-
-**Full lxRAG suite operational with agent memory integration**
-
----
-
-## Monitoring & Metrics
-
-### Track These Metrics
-
-```
-Phase 1:
- - BigInt errors/week: < 1
- - Rebuild time: < 1 min
- - Arch violations: 0
-
-Phase 2:
- - Tool success rate: > 99%
- - Pattern detection accuracy: > 95%
- - Query latency: < 500ms
-
-Phase 3:
- - Episode persistence rate: 100%
- - Memory recall accuracy: > 90%
- - Semantic search NDCG: > 0.8
-
-Phase 4:
- - CLI command success: 100%
- - Test coverage: > 80%
-
-Phase 5:
- - P99 query latency: < 1s
- - Throughput: > 100 ops/sec
-```
-
----
-
-## Support Resources
-
-| Issue | Reference |
-| ------------------- | ------------------------------------------------ |
-| BigInt errors | `ERROR_REPORT.md`, `GRAPH_STATE_FIXES.md` |
-| Architecture config | `ARCHITECTURE.md`, `docs/INTEGRATION_SUMMARY.md` |
-| Tool reference | `QUICK_REFERENCE.md` |
-| Integration guide | `docs/MCP_INTEGRATION_GUIDE.md` |
-| Roadmap | `docs/AGENT_CONTEXT_ENGINE_PLAN.md` |
-| Benchmarks | `benchmarks/GRAPH_TOOLS_BENCHMARK_MATRIX.md` |
-
----
-
-## Next Immediate Actions
-
-1. **Right Now**:
- - Review this plan
- - Create `.lxrag/config.json` (provided above)
-
-2. **Within 1 hour**:
- - Run Phase 1 validation checklist
- - Fix any errors
-
-3. **Within 1 day**:
- - Complete Phase 1
- - Start Phase 2
-
-4. **This week**:
- - Complete Phases 2-3
- - Begin Phase 4
-
----
-
-**Plan created by**: lxRAG Analysis Tools
-**Confidence**: High (based on actual project analysis)
-**Ready for**: Immediate implementation
diff --git a/benchmarks/GRAPH_TOOLS_BENCHMARK_MATRIX.md b/benchmarks/GRAPH_TOOLS_BENCHMARK_MATRIX.md
index 1ccaabe..b834412 100644
--- a/benchmarks/GRAPH_TOOLS_BENCHMARK_MATRIX.md
+++ b/benchmarks/GRAPH_TOOLS_BENCHMARK_MATRIX.md
@@ -35,7 +35,7 @@ Generated: 2026-02-21T23:42:24.987677+00:00
| T014 | test_select | Test select for progress-engine change | 15.14 | 224.28 | 0.000 | 1.000 | 66 | 31 | baseline | Low MCP accuracy on this scenario |
| T015 | test_select | Test select for orchestrator change | 14.56 | 206.63 | 0.000 | 0.000 | 65 | 22 | tie | Low MCP accuracy on this scenario |
| T016 | test_select | Test select for contract test file | 14.79 | 226.22 | 0.000 | 1.000 | 68 | 22 | baseline | Low MCP accuracy on this scenario |
-| T017 | graph_rebuild | Rebuild incremental (lxRAG-MCP src) | 15.84 | 205.97 | 0.000 | 1.000 | 64 | 11 | baseline | Low MCP accuracy on this scenario |
+| T017 | graph_rebuild | Rebuild incremental (lxDIG MCP src) | 15.84 | 205.97 | 0.000 | 1.000 | 64 | 11 | baseline | Low MCP accuracy on this scenario |
| T018 | graph_rebuild | Rebuild incremental verbose | 14.31 | 203.85 | 0.000 | 1.000 | 63 | 13 | baseline | Low MCP accuracy on this scenario |
| T019 | graph_rebuild | Rebuild full mode | 15.11 | 218.72 | 0.000 | 1.000 | 62 | 12 | baseline | Low MCP accuracy on this scenario |
| T020 | graph_rebuild | Rebuild full verbose | 14.50 | 204.09 | 0.000 | 1.000 | 62 | 7 | baseline | Low MCP accuracy on this scenario |
diff --git a/docker-compose.yml b/docker-compose.yml
index 6227b75..03d7af1 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,7 +1,7 @@
services:
memgraph:
image: memgraph/memgraph-mage:latest
- container_name: lxRAG-memgraph
+ container_name: lxDIG-memgraph
ports:
- "7687:7687"
- "7444:7444"
@@ -25,7 +25,7 @@ services:
qdrant:
image: qdrant/qdrant:latest
- container_name: lxRAG-qdrant
+ container_name: lxDIG-qdrant
ports:
- "6333:6333"
- "6334:6334"
@@ -37,7 +37,7 @@ services:
memgraph-lab:
image: memgraph/lab:latest
- container_name: lxRAG-memgraph-lab
+ container_name: lxDIG-memgraph-lab
ports:
- "3000:3000"
environment:
@@ -52,7 +52,7 @@ services:
build:
context: .
dockerfile: Dockerfile.dev
- container_name: lxRAG-mcp
+ container_name: lxDIG-mcp
ports:
- "9000:9000"
- "9001:9001"
@@ -65,9 +65,9 @@ services:
- QDRANT_HOST=qdrant
- QDRANT_PORT=6333
- LOG_LEVEL=info
- - LXRAG_WORKSPACE_ROOT=/workspace
+ - LXDIG_WORKSPACE_ROOT=/workspace
- GRAPH_SOURCE_DIR=/workspace/src
- - LXRAG_PROJECT_ID=${LXRAG_PROJECT_ID:-lxRAG-default}
+ - LXDIG_PROJECT_ID=${LXDIG_PROJECT_ID:-lxDIG-default}
volumes:
- ./src:/app/src
- ./package.json:/app/package.json
@@ -94,6 +94,6 @@ services:
volumes:
memgraph_data:
- name: lxrag_memgraph_data
+ name: lxdig_memgraph_data
qdrant_data:
- name: lxrag_qdrant_data
+ name: lxdig_qdrant_data
diff --git a/docs/ACTION_PLAN_LXRAG_TOOL_FIXES.md b/docs/ACTION_PLAN_LXRAG_TOOL_FIXES.md
deleted file mode 100644
index d5520ca..0000000
--- a/docs/ACTION_PLAN_LXRAG_TOOL_FIXES.md
+++ /dev/null
@@ -1,350 +0,0 @@
-# Action Plan: Fix lxRAG Tool Issues (graph_health, feature_status, progress_query)
-
-**Status:** Draft - Requires Implementation
-**Date:** 2026-02-22
-**Project:** lexRAG-MCP + code-visual (visualization client)
-**Severity:** High (3 out of 4 primary operational tools broken)
-
----
-
-## Executive Summary
-
-Three critical lxRAG tools are returning incorrect data due to a **project-scoping divergence**. The in-memory graph index (`GraphIndexManager`) is global and non-scoped, while the Memgraph database supports per-project queries. When users switch projects via `graph_set_workspace`, the read-path breaks because:
-
-1. Engines are initialized **once** at startup with a global, unfiltered index
-2. Project context switches **do not reinitialize** engines
-3. Query tools read from **stale in-memory state** instead of the live project-scoped graph
-
-**Impact:**
-- Health checks are misleading (report empty graphs when data exists)
-- Feature and task queries fail on valid IDs
-- Operational dashboards in code-visual cannot trust lxRAG data
-
----
-
-## Issue Analysis
-
-### Issue #1: `mcp_lxrag_graph_health` reports zero indexed graph entities
-
-**Symptoms:**
-- Tool returns `graphIndex.totalNodes = 0`, `graphIndex.totalRelationships = 0`
-- Live Memgraph shows `809` nodes and `1359` relationships for same project
-
-**Root Cause:**
-- **Location:** [tool-handlers.ts:1782-1787](src/tools/tool-handlers.ts#L1782)
- ```typescript
- const stats = this.context.index.getStatistics();
- const functionCount = this.context.index.getNodesByType("FUNCTION").length;
- const classCount = this.context.index.getNodesByType("CLASS").length;
- const fileCount = this.context.index.getNodesByType("FILE").length;
- ```
-- **Problem:** Reads from global `GraphIndexManager` without projectId filtering
-- **Why it fails:** When `graph_set_workspace` is called, the index is never cleared or reloaded with new project data
-
-**Diagnosis Path:**
-1. `graph_set_workspace(projectId=code-visual)` → sets active context but doesn't reinitialize engines
-2. `graph_health` → reads from stale global index still containing previous project's data
-3. Memgraph queries in same tool (lines 1798-1804) DO use projectId filtering and return correct counts
-4. **Inconsistency proves the divergence:** Index-based reads ≠ Cypher-based reads
-
----
-
-### Issue #2: `mcp_lxrag_feature_status` fails on valid feature IDs
-
-**Symptoms:**
-- Tool returns "Feature not found: code-visual:feature:phase-1"
-- Direct Cypher query finds the node: `MATCH (f:FEATURE {id: "code-visual:feature:phase-1"}) RETURN f`
-
-**Root Cause:**
-- **Location:** [progress-engine.ts:76-91](src/engines/progress-engine.ts#L76) (initialization)
-- **Location:** [progress-engine.ts:183-185](src/engines/progress-engine.ts#L183) (lookup)
- ```typescript
- private loadFromGraph(): void {
- const featureNodes = this.index.getNodesByType("FEATURE");
- // ... populate this.features Map
- }
-
- getFeatureStatus(featureId: string): FeatureStatus | null {
- const feature = this.features.get(featureId); // ← Searches stale Map
- if (!feature) return null; // ← Returns null for valid IDs
- }
- ```
-- **Problem:** ProgressEngine loads features once at initialization from global index
-- **Why it fails:** Engines initialized at ToolHandlers constructor ([tool-handlers.ts:75](src/tools/tool-handlers.ts#L75)) before any project context exists. When project changes, the engine still holds old data.
-
-**Diagnosis Path:**
-1. ToolHandlers constructor → `initializeEngines()` → ProgressEngine initialized with empty/wrong index
-2. `graph_set_workspace()` → sets active project context but does NOT call `progressEngine.reload()`
-3. `feature_status()` → looks in stale `this.features` Map
-4. Valid features exist in Memgraph but not in the in-memory Map
-
----
-
-### Issue #3: `mcp_lxrag_progress_query` returns empty despite existing tasks
-
-**Symptoms:**
-- Tool returns `items: []`, `totalCount: 0`
-- Live Memgraph shows `TASK` nodes with statuses: `completed:3`, `in-progress:2`, `pending:2`
-
-**Root Cause:**
-- **Location:** [progress-engine.ts:124-160](src/engines/progress-engine.ts#L124)
- ```typescript
- query(type: "feature" | "task", filter?: {...}): ProgressQueryResult {
- if (type === "task") {
- for (const task of this.tasks.values()) { // ← Empty Map for new project
- if (filter?.status && task.status !== filter.status) continue;
- items.push(task);
- }
- }
- }
- ```
-- **Problem:** Same as Issue #2 — `this.tasks` Map never reloaded when project changes
-- **Why it fails:** ProgressEngine.query() always returns empty for new project until graph_rebuild completes
-
----
-
-## Cross-Issue Pattern: Read-Path Divergence
-
-All three issues stem from the same architectural mismatch:
-
-| Data Source | Scoping | Project-Aware? | Refresh on Switch? |
-|---|---|---|---|
-| **GraphIndexManager** (in-memory) | Global accumulator | ❌ No | ❌ No |
-| **ProgressEngine state** (Maps) | In-memory snapshots | ❌ No | ❌ No |
-| **Memgraph queries** (Cypher) | Per-project via WHERE clause | ✅ Yes | N/A (always current) |
-| **graph_health Cypher paths** (lines 1798+) | Per-project via WHERE clause | ✅ Yes | N/A (always current) |
-
-**The issue:** Tools mix Cypher-based reads (project-scoped, correct) with index-based reads (global, stale).
-
----
-
-## Recommended Fix Order
-
-### Fix #1: Add Project-Scoped Index Reloading (Medium effort, high impact)
-
-**Goal:** Reinitialize ProgressEngine when project context changes
-
-**Changes Required:**
-
-**[1.1] ProgressEngine: Add reload() method**
-- **File:** `src/engines/progress-engine.ts`
-- **Change:** Add a method to reload features and tasks from the current graph index
- ```typescript
- reload(index: GraphIndexManager, projectId?: string): void {
- this.features.clear();
- this.tasks.clear();
- this.loadFromGraph(index, projectId);
- }
- ```
-- **Details:** If `projectId` provided, filter nodes to those matching `node.properties.projectId === projectId`
-
-**[1.2] ToolHandlers: Reinitialize ProgressEngine on project context change**
-- **File:** `src/tools/tool-handlers.ts`
-- **Change:** Modify `setActiveProjectContext()` to trigger engine reloads
- ```typescript
- private setActiveProjectContext(context: ProjectContext): void {
- // ... existing code ...
- const sessionId = this.getCurrentSessionId();
- if (sessionId) {
- this.sessionProjectContexts.set(sessionId, context);
- } else {
- this.defaultActiveProjectContext = context;
- }
- // NEW: Reload engines with new context
- this.reloadEnginesForContext(context);
- }
-
- private reloadEnginesForContext(context: ProjectContext): void {
- this.progressEngine?.reload(this.context.index, context.projectId);
- this.testEngine?.reload(this.context.index, context.projectId);
- this.archEngine?.reload(this.context.index, context.projectId);
- }
- ```
-
-**[1.3] TestEngine: Add reload() method**
-- **File:** `src/engines/test-engine.ts`
-- **Change:** Similar to ProgressEngine — reload test-related nodes filtered by projectId
-
-**[1.4] ArchitectureEngine: Add reload() method**
-- **File:** `src/engines/architecture-engine.ts`
-- **Change:** Reload architecture/violation nodes filtered by projectId
-
----
-
-### Fix #2: Make graph_health Query-First (Low effort, medium impact)
-
-**Goal:** Replace index-based statistics with Memgraph queries for authoritative counts
-
-**Changes Required:**
-
-**[2.1] graph_health: Query Memgraph for node counts**
-- **File:** `src/tools/tool-handlers.ts`, lines 1778-1850
-- **Change:** Replace lines 1782-1787 with Cypher queries:
- ```typescript
- const stats = this.context.index.getStatistics(); // REMOVE or deprecate
-
- // ADD: Query-based counts (project-scoped)
- const { projectId } = this.getActiveProjectContext();
-
- const nodeCountResult = await this.context.memgraph.executeCypher(
- `MATCH (n {projectId: $projectId}) RETURN count(n) AS totalNodes`,
- { projectId }
- );
- const relationshipCountResult = await this.context.memgraph.executeCypher(
- `MATCH (f {projectId: $projectId})-[r]->(t {projectId: $projectId}) RETURN count(r) AS totalRels`,
- { projectId }
- );
-
- const totalNodes = nodeCountResult.data?.[0]?.totalNodes || 0;
- const totalRelationships = relationshipCountResult.data?.[0]?.totalRels || 0;
-
- // Keep symbol counts from index (they're local only)
- const functionCount = this.context.index.getNodesByType("FUNCTION").length;
- // ...
- ```
-
-**[2.2] Deprecate global index statistics**
-- **File:** `src/graph/index.ts`
-- **Change:** Add warning comment that `getStatistics()` is not project-scoped; prefer Cypher for operational queries
-
----
-
-### Fix #3: Add Parity Tests (Medium effort, high value)
-
-**Goal:** Detect read-path divergence automatically
-
-**Changes Required:**
-
-**[3.1] Create parity test suite**
-- **File:** `src/tools/tool-handlers.parity.test.ts` (new file)
-- **Tests:**
- 1. After `graph_set_workspace(projectId=TEST_PROJECT)`, verify:
- - `graph_health` reports non-zero counts
- - These counts match `MATCH (n {projectId: TEST_PROJECT}) RETURN count(n)`
- 2. After seeding test features, verify:
- - `feature_status(id)` resolves valid IDs
- - `progress_query(type="feature")` returns seeded features
- 3. Task counts match between `progress_query` and Cypher `MATCH (t:TASK {projectId: ...})`
-
-**[3.2] Add diagnostics to tool responses**
-- **Location:** All three tools' responses
-- **Addition:** Include a `_diagnostics` object in success responses:
- ```json
- {
- "success": true,
- "data": { ... },
- "_diagnostics": {
- "projectId": "code-visual",
- "source": "in-memory-index|cypher-query",
- "indexedAt": "2026-02-22T10:30:00Z",
- "warning": "Results from stale index; recommend graph_rebuild"
- }
- }
- ```
-
----
-
-### Fix #4: Add Explicit Project-Scoping to GraphIndexManager (Long term, architectural)
-
-**Goal:** Make the index project-aware from the ground up
-
-**Rationale:** This is the "right" solution but requires more refactoring. Consider for v2.
-
-**Changes Sketched:**
-1. Split `GraphIndexManager` into `PerProjectIndexManager`
-2. Store one index per projectId
-3. Only load/query the active project's index
-4. This would eliminate all three issues at the source
-
----
-
-## Implementation Priority
-
-**Phase 1 (Immediate - 2-3 hours):**
-- ✅ Fix #2: Make graph_health query-first
-- ✅ Fix #1.1 & #1.2: Add ProgressEngine reload on project context change
-- Validates: Issue #1 (health), Issue #2 (feature_status), Issue #3 (progress_query)
-
-**Phase 2 (Follow-up - 1-2 hours):**
-- ✅ Fix #1.3 & #1.4: Add reload to TestEngine and ArchitectureEngine
-- ✅ Fix #3: Add parity tests and diagnostics
-
-**Phase 3 (Backlog - Design-level):**
-- ✅ Fix #4: Architectural refactor to per-project indices
-
----
-
-## Validation & Acceptance Criteria
-
-After fixes are implemented, validate against the original reproduction case from code-visual:
-
-```json
-Prerequisite:
-1. graph_set_workspace({ projectId: "code-visual", workspaceRoot: "/path/to/code-visual", sourceDir: "src" })
-2. graph_rebuild({ mode: "full" })
-
-Test Case 1 — graph_health parity:
- Before: { graphIndex: { totalNodes: 0, totalRelationships: 0 } }
- After: { graphIndex: { totalNodes: 809, totalRelationships: 1359 } } ✅
- Verify: Matches Memgraph query counts
-
-Test Case 2 — feature_status resolution:
- Before: Feature not found: code-visual:feature:phase-1
- After: { success: true, feature: { ... }, tasks: [...] } ✅
-
-Test Case 3 — progress_query item counts:
- Before: { items: [], totalCount: 0 }
- After: { items: [7 TASK nodes], totalCount: 7, completedCount: 3, inProgressCount: 2, blockedCount: 2 } ✅
- Verify: Status breakdown matches `MATCH (t:TASK) RETURN t.status, count(*)`
-```
-
----
-
-## Code-Visual Integration Notes
-
-The visualization project (code-visual) will benefit from:
-
-1. **Reliable health checks** — can use `graph_health` as a readiness signal
-2. **Accurate progress dashboards** — `progress_query` + `feature_status` will show real data
-3. **Consistent operational data** — Memgraph queries and tool responses will be in sync
-4. **Diagnostics output** — can warn users if index is stale and recommend rebuild
-
----
-
-## Summary of Findings
-
-| Issue | Root Cause | Fix Category | Effort | Impact |
-|---|---|---|---|---|
-| `graph_health` returns zeros | Index not scoped by projectId | Replace with Cypher query | Low | High |
-| `feature_status` "not found" | ProgressEngine state not reloaded on project switch | Reload on context change | Medium | High |
-| `progress_query` empty | Same as feature_status | Reload on context change | Medium | High |
-
-**The fix is NOT to add projectId filtering to every index operation.** The fix is to:
-1. **Keep the index global** (performance benefit for local operations)
-2. **Reinitialize engines** when project context changes
-3. **Use Cypher for operational queries** (health, stats) that need to be authoritative
-
-This preserves performance (index stays global) while fixing the read-path divergence (engines reload when context changes).
-
----
-
-## Related Files
-
-- Main issue tracking: `docs/lxrag-tool-issues.md`
-- Tool implementations: `src/tools/tool-handlers.ts`
-- Engines:
- - `src/engines/progress-engine.ts` (Issues #2, #3)
- - `src/graph/index.ts` (Issue #1 context)
-- Graph client: `src/graph/client.ts`
-- Orchestrator: `src/graph/orchestrator.ts`
-
----
-
-## Next Steps
-
-1. **Assign to developer** → Implement Phase 1 fixes
-2. **Run validation tests** against code-visual test data
-3. **Update QUICK_REFERENCE.md** with notes about tool reliability and parity guarantees
-4. **Close issues** in code-visual once validated
-5. **Add regression tests** to prevent future divergence
-
diff --git a/docs/AGENT_CONTEXT_ENGINE_PLAN.md b/docs/AGENT_CONTEXT_ENGINE_PLAN.md
deleted file mode 100644
index 5e87eb0..0000000
--- a/docs/AGENT_CONTEXT_ENGINE_PLAN.md
+++ /dev/null
@@ -1,2348 +0,0 @@
-# Agent Context Engine — Implementation Plan
-
-> **Vision**: Make `lxRAG-MCP` the external long-term memory and coordination layer for any fleet of LLM agents. Agents stop spending tokens re-reading code, re-arguing about decisions, or losing state between calls. They query the tool, get exactly what they need in the smallest possible package, and execute.
-
----
-
-## 0. SOTA Research — What Exists and What We Can Learn
-
-This section maps the current research landscape and production systems that have attempted similar problems. Each has important patterns to adopt.
-
-### 0.1 Existing Systems Surveyed
-
-| System | Type | Core Insight | Stars | Our Relevance |
-| -------------------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------- | ----- | ------------------------------------------------------------------------------------------ |
-| **GraphRAG** (Microsoft, arXiv:2404.16130) | Graph RAG | LLM-extracted knowledge graphs + Leiden community summaries → global & local query modes | 31k | Community detection on code graph; global/local retrieval modes |
-| **LightRAG** (HKUDS, arXiv:2410.05779, EMNLP'25) | Graph+Vector RAG | Dual-level retrieval (specific entities + abstract concepts) + incremental graph updates | 28k | Dual-level retrieval; already uses Memgraph; incremental design |
-| **HippoRAG 2** (OSU, NeurIPS'24) | Neurobiological RAG | LLM + KG + Personalized PageRank → 20% better multi-hop QA, 10-30x cheaper than iterative retrieval | 3.2k | **Use PPR for context_pack retrieval** — best fit for code graph traversal |
-| **Graphiti / Zep** (arXiv:2501.13956) | Temporal KG for agents | Bi-temporal model (event time ≠ ingestion time) + hybrid search (semantic+BM25+graph) + MCP server | 23k | **Closest design to our goal**; bi-temporal model replaces snapshots; episode-based memory |
-| **Mem0** (YC S24) | Memory layer | Multi-level memory (User/Session/Agent) + self-improving extraction → 26% accuracy, 91% faster, 90% fewer tokens | 47.7k | Adaptive memory extraction from agent interactions; self-improvement loop |
-| **MemGPT/Letta** (arXiv:2310.08560) | OS-inspired memory | Hierarchical memory tiers (in-context = RAM, external = disk) + interrupt-driven paging | 21.2k | Virtual context packing; "page in" only what the agent needs |
-| **Generative Agents** (Stanford, arXiv:2304.03442) | Agent architecture | Observation → Reflection → Planning loop; synthesize low-level events into high-level insights | - | Reflection/synthesis layer: periodic consolidation of agent patterns into INSIGHT nodes |
-| **Cognee** | Knowledge engine | Graph + vector + self-improvement pipeline (`cognify` + `memify`) | 12.5k | Pipeline metaphor for ingestion; `memify` analogue for code |
-
-### 0.2 Key Architectural Patterns to Adopt
-
-#### Pattern 1: Bi-Temporal Model (from Graphiti)
-
-Every graph node and edge should carry two time dimensions:
-
-- **Valid time** (`validFrom` / `validTo`): when this code state was actually true
-- **Transaction time** (`createdAt`): when we learned/ingested this fact
-
-This makes the snapshot approach (Phase 6) obsolete. Instead of creating point-in-time snapshots, every graph write is automatically time-stamped. Queries like "what was the codebase like at T?" become a simple time filter.
-
-```cypher
-// Find all functions as they existed at commit time T
-MATCH (f:FUNCTION {projectId: $pid})
-WHERE f.validFrom <= $T AND (f.validTo IS NULL OR f.validTo > $T)
-RETURN f
-```
-
-#### Pattern 2: Personalized PageRank for Context Retrieval (from HippoRAG)
-
-HippoRAG's central finding: vector similarity alone is weak for multi-hop retrieval. Running **Personalized PageRank (PPR)** starting from query-matched nodes propagates relevance through the graph — naturally surfacing callers, callees, shared dependencies, and related decisions.
-
-For `context_pack`:
-
-1. Vector search → top-5 seed nodes (files/functions matching the task description)
-2. PPR from seeds → ranked subgraph with relevance scores
-3. Cut at relevance threshold → minimal but complete context
-
-This replaces the current "get direct deps, list files" approach.
-
-#### Pattern 3: Dual-Level Retrieval (from LightRAG)
-
-Separate retrieval modes for different question types:
-
-- **Local mode**: specific entities — "what does `save()` call?" → graph traversal
-- **Global mode**: abstract patterns — "what are the main architectural concerns?" → community summaries
-
-The code graph should maintain **community summaries** (Leiden clustering, recomputed on rebuild) for global mode.
-
-#### Pattern 4: Episode-Based Memory (from Graphiti)
-
-Replace raw `CHECKPOINT` nodes with **episodes** — atomic, immutable records of an agent interaction. An episode is:
-
-```
-{
- agentId, sessionId, taskId,
- timestamp,
- type: 'observation' | 'edit' | 'decision' | 'test_result' | 'error',
- content: string, // what happened, in natural language
- entities: string[], // file/function/class IDs this episode involves
- outcome: 'success' | 'failure' | 'partial'
-}
-```
-
-Episodes chain temporally. Retrieval uses hybrid search (vector + BM25 + temporal recency). A `recall` query finds the most relevant episodes, not just the latest checkpoint.
-
-#### Pattern 5: Self-Improving Extraction (from Mem0)
-
-When agents mark tasks complete, the server should automatically:
-
-1. Extract key patterns from the episode chain (what edits were made, what decisions were taken)
-2. Store these as `LEARNING` nodes linked to the affected code
-3. Surface these learnings in future `context_pack` calls for similar tasks
-
-Mem0 accomplishes this with an LLM extraction pass. Ours can be simpler: structural analysis of what changed between episodes.
-
-#### Pattern 6: Relevance-Ranked Context Budget (from Mem0 / MemGPT)
-
-Mem0's 90% token reduction comes from **intelligent selection**, not truncation. MemGPT's OS analogy: the LLM's context window is RAM; only page in what's needed.
-
-Implement a `ContextBudget`:
-
-```typescript
-interface ContextBudget {
- maxTokens: number; // hard limit
- allocation: {
- coreCode: number; // 40% — the exact symbols being edited
- dependencies: number; // 25% — direct callers/callees
- decisions: number; // 20% — relevant past decisions
- plan: number; // 10% — current task plan
- episodeHistory: number; // 5% — recent agent episodes
- };
-}
-```
-
-Each section is filled by PPR-ranked retrieval until its allocation is consumed, then cut. No arbitrary character truncation.
-
-#### Pattern 7: Contradiction Handling via Temporal Invalidation (from Graphiti)
-
-Graphiti doesn't delete old facts — it sets `validTo` on them. This handles contradictions automatically: if a function signature changes, the old version gets `validTo = now` and the new one gets `validFrom = now`. Queries always see the current view unless they request a historical one.
-
-This pattern replaces both the "snapshot" approach and the TTL-based claim expiry in the original plan.
-
-#### Pattern 8: Structure-Aware Chunking (from DKB / SCIP)
-
-Slice code **only at AST syntactic boundaries** — never split in the middle of a function, class, or block. A chunk is exactly one of: a full function body, a full class definition, a full import block, or a full test suite.
-
-This is the prerequisite for every downstream feature — PPR, Meta-RAG summaries, and semantic_slice all depend on chunks that are semantically complete.
-
-```
-Good chunk boundary: | function foo() { ... full body ... }
-Bad chunk boundary: | function foo() { ... |
- | ...truncated mid-logic }
-```
-
-Applies to: `graph_rebuild` indexing, `semantic_slice` output, BM25 index units, Meta-RAG summarization input.
-
-#### Pattern 9: Meta-RAG Code Summarization (indexing-time LLM summaries)
-
-Inspired by Meta-RAG (arXiv) — deploy an LLM **during the indexing phase** to summarize every AST-extracted function and class into a single natural-language sentence. Achieves ~79.8% token compression in retrieval responses.
-
-Scheme:
-
-1. On `graph_rebuild`, for each FUNCTION and CLASS node, generate: `{name} in {file}: {one-sentence NL summary}`
-2. Store summary as `summary` property on the node
-3. Embed the summary (not the raw code) for Qdrant vector index
-4. BM25 index also runs over summaries + symbol names
-5. In compact-profile tool responses, return `node.summary` instead of `node.code`; `code` only in balanced/debug profile
-
-This means querying "what does ToolHandlers do?" returns a 15-token summary, not 800 tokens of raw code.
-
-```typescript
-// graph/builder.ts — node property added on index
-functionNode.summary = await llm.summarize(
- `${functionNode.name} in ${file}: ${functionNode.code}`,
- { maxTokens: 30 },
-);
-```
-
-#### Pattern 10: SCIP-Style Human-Readable Node IDs
-
-Inspired by SCIP Code Intelligence Protocol — use **stable, human-readable string identifiers** for all graph nodes instead of UUIDs or integer hashes.
-
-Format: `{relativePath}::{ClassName}::{methodName}` or `{relativePath}::{functionName}`
-
-Examples:
-
-```
-src/tools/tool-handlers.ts::ToolHandlers::callTool
-src/engines/progress-engine.ts::ProgressEngine::loadFromGraph
-src/graph/client.ts::MemgraphClient
-```
-
-Benefits:
-
-- **O(changes) incremental indexing**: `MERGE` on stable ID → only changed nodes need updating
-- **Cross-reference resolution**: any tool can construct the ID from a file path + symbol name without an ID lookup table
-- **Human-readable Cypher**: graph queries are self-documenting
-- **Stable across rebuilds**: no UUID churn on every full rebuild
-
-This replaces UUID-based IDs in the builder and becomes the primary identifier for all FUNCTION, CLASS, FILE nodes.
-
-### 0.3 Key Differentiator vs Existing Systems
-
-| Dimension | GraphRAG | LightRAG | Graphiti | **lxRAG-MCP (target)** |
-| ------------------------ | ---------------------- | ---------------------- | ---------------------------- | ---------------------------------------------------------- |
-| Domain | General text | General text | Conversation/enterprise data | **Source code** |
-| Graph structure | LLM-extracted entities | LLM-extracted entities | Episodic memory + entities | **AST-precise** (functions, classes, imports, call graphs) |
-| Retrieval | Community summaries | Dual-level | Hybrid temporal | **Hybrid + PPR + code structure** |
-| Agent memory | None | None | Episodes + temporal KG | **Episodes + decisions + claims + code graph** |
-| Multi-agent coordination | None | None | None | **Claim system + episode broadcast** |
-| Code-specific features | None | None | None | **Architecture validation, test impact, semantic slicing** |
-| Temporal model | Basic | Incremental | **Bi-temporal** | **Bi-temporal (adopted from Graphiti)** |
-| Node identifiers | Opaque integers | Opaque hashes | UUIDs | **SCIP-style `file::Class::method`** |
-| Code summarization | LLM communities | LLM communities | None | **Meta-RAG per function/class (indexing-time)** |
-| Agent interop protocol | None | None | None | **A2A Agent Card + MCP via SSE** |
-
-This server fills a gap none of these address: **code-specific, agent-coordinating, bi-temporal, multi-hop-retrievable memory for software development agents**.
-
-### 0.4 Interoperability Protocols
-
-Three emerging open standards complement each other and map directly onto this server's architecture:
-
-#### MCP (Model Context Protocol) — already implemented
-
-JSON-RPC 2.0 client-server architecture. This server exposes all tools via MCP. Two transports:
-
-- **stdio**: zero-latency, secure local execution (default, used by Claude Code / VS Code)
-- **SSE / StreamableHTTP**: remote API connections, enables multi-host agent fleets
-
-The server already implements both (`MCP_TRANSPORT=stdio|http`).
-
-#### A2A (Agent2Agent Protocol) — add in Phase 4
-
-Open Google protocol for **peer-to-peer, opaque agent collaboration**. Agents advertise capabilities via a JSON-LD "Agent Card" served at `GET /.well-known/agent.json`. Task delegation is asynchronous via SSE streaming.
-
-For this server, the Agent Card:
-
-- Advertises the 34 MCP tools as A2A capabilities
-- Signals that this server is a **memory + coordination** specialist agent
-- Allows other A2A-aware orchestrators (LangGraph, AutoGen, etc.) to discover and delegate memory tasks to it automatically
-
-Phase 4 adds: `GET /.well-known/agent.json` endpoint serving a static Agent Card. No full A2A task delegation infrastructure needed — just discovery.
-
-```json
-// /.well-known/agent.json (static, Phase 4 addition)
-{
- "@context": "https://schema.a2aprotocol.dev/v1",
- "@type": "Agent",
- "name": "lxRAG-MCP",
- "description": "External long-term memory and coordination layer for LLM agent fleets working on software codebases.",
- "capabilities": [
- "code-graph",
- "agent-memory",
- "multi-agent-coordination",
- "context-packing"
- ],
- "mcpEndpoint": "/mcp",
- "version": "1.0.0"
-}
-```
-
-#### SAMEP (Secure Agent Memory Exchange Protocol) — Phase 3 consideration
-
-Vector-based semantic search + AES-256-GCM cryptographic access controls for cross-boundary context sharing. Relevant when episode/decision memory from one agent must be shared with another agent that has a different trust scope.
-
-For this server: the `sensitive: true` flag on EPISODE nodes (already planned in Phase 7 Design Rule #7) is the lightweight version. Full SAMEP integration (AES-256-GCM encrypted episode payloads, per-agent decryption keys) is a post-Phase-4 hardening step — noted here so the episode schema does not preclude it.
-
-#### ACP (Agent Communication Protocol) — future consideration
-
-Federated orchestration with decentralized identity verification and semantic intent mapping. Relevant if this server operates in a zero-trust multi-organization environment. Not in scope for current phases but the A2A Agent Card design does not conflict with it.
-
----
-
----
-
-## 1. Problem Analysis — Current State
-
-### 1.1 What the server does today
-
-| Layer | What exists |
-| ------------------- | ----------------------------------------------------------------------------------------------------------------------- |
-| Graph (Memgraph) | TypeScript-only AST → nodes (FILE, FUNCTION, CLASS, IMPORT) + edges |
-| Vector (Qdrant) | Embedding-backed semantic similarity search |
-| 14 MCP Tools | `graph_query`, `code_explain`, `find_pattern`, `arch_validate`, `arch_suggest`, `test_*`, `progress_*`, `graph_rebuild` |
-| Progress engine | In-memory features/tasks; wiped on restart |
-| Architecture engine | Layer rule validation (TS only) |
-
-### 1.2 Identified Gaps (root causes of poor agent interaction)
-
-#### Gap 1 — Verbosity kills token efficiency
-
-Every tool returns a JSON blob with full data objects. `compact` profile exists but still truncates at 320 chars arbitrarily. An agent asking "what does `ToolHandlers` depend on?" gets hundreds of tokens of noise instead of a 3-line answer.
-
-#### Gap 2 — No persistent agent scratchpad / working memory
-
-There is no way for an agent to write its **current reasoning state**, **decisions made**, or **partial plan** to the server. When the context window fills, everything is lost. A second agent or a resumed session starts from zero.
-
-#### Gap 3 — No agent handoff protocol
-
-When agent A finishes and hands off to agent B, B must be re-briefed by copying messages. The server has no "get me what agent A knew" mechanism. This is the #1 source of inter-agent communication errors.
-
-#### Gap 4 — Progress engine is ephemeral
-
-Features and tasks live in RAM and in raw graph nodes. There is no structured persistence, no change history, no rollback, and no locking. Two agents can simultaneously claim the same task.
-
-#### Gap 5 — No "mission pack" — single-shot context entry point
-
-An agent starting a new task must call 4-8 tools: `graph_rebuild` → `graph_query` (structure) → `code_explain` (deps) → `progress_query` (state) → `blocking_issues`. There is no single tool that says: _"Give me the full briefing for task X"_.
-
-#### Gap 6 — No cross-agent coordination / ownership
-
-Multiple agents can work on the same file or task concurrently with no awareness of each other. No locking, no claiming, no status broadcasting.
-
-#### Gap 7 — Only TypeScript is parsed
-
-Python, Go, Rust, and Java projects cannot use this server at all.
-
-#### Gap 8 — Weak natural-language-to-Cypher translation
-
-The NL query path does regex intent classification then falls back to broad Cypher. Complex questions ("which functions call save() and are also touched by open tasks?") fail silently.
-
-#### Gap 9 — No incremental watch / push model
-
-Graph rebuild is manual and async. Agents must poll `graph_health` to know when it's ready. File changes are not reflected until the next explicit rebuild.
-
-#### Gap 10 — No code snapshot / audit trail
-
-There is no way to say "what did the codebase look like when we started task T?" or "what changed since the last agent ran?" — critical for rollback and blame.
-
----
-
-## 2. Target Architecture
-
-```
-┌────────────────────────────────────────────────────────┐
-│ Agent Fleet (any LLM) │
-│ Agent A Agent B Agent C Agent D │
-└──────┬──────────────┬──────────────┬──────────────┬────┘
- │ │ │ │
- └──────────────┴──────────────┴──────────────┘
- │ MCP (HTTP)
- ┌──────────────▼──────────────────────┐
- │ lxRAG-MCP │
- │ │
- │ ┌─────────────────────────────────┐ │
- │ │ Tool Surface (~34 tools) │ │
- │ │ context_pack episode_add │ │
- │ │ agent_claim semantic_slice │ │
- │ │ decision_query diff_since │ │
- │ │ reflect + 14 existing │ │
- │ └──────────────┬──────────────────┘ │
- │ │ │
- │ ┌──────────────▼──────────────────┐ │
- │ │ Core Engines │ │
- │ │ PPR Context Packer │ │
- │ │ Episode Memory (bi-temporal) │ │
- │ │ Coordination (temporal claims) │ │
- │ │ Hybrid Retriever (vec+BM25+PPR) │ │
- │ │ Community Detector (Leiden) │ │
- │ │ Multi-lang Parser (Tree-sitter) │ │
- │ └──────────┬───────────┬───────────┘ │
- │ │ │ │
- │ ┌──────────▼──┐ ┌─────▼──────────┐ │
- │ │ Memgraph │ │ Qdrant │ │
- │ │ (bi-temp. │ │ (embeddings + │ │
- │ │ KG + PPR) │ │ BM25 index) │ │
- │ └─────────────┘ └────────────────┘ │
- └─────────────────────────────────────────┘
-```
-
----
-
-## 3. Implementation Phases (Revised with SOTA Insights)
-
-### Phase 1 — Foundation: Response Quality & Context Budget
-
-**Goal**: Halve the tokens each existing tool returns without losing information density. Use relevance-ranked selection (not truncation).
-**Estimated effort**: 1–2 weeks
-**Unblocks**: Every subsequent phase. Agents cannot reliably work with the server until token efficiency and answer-first formatting are in place.
-**Research backing**: Mem0 (90% token reduction via intelligent selection), MemGPT (RAM/disk paging model)
-**Acceptance criteria**:
-
-- `npx tsc --noEmit` clean
-- compact profile: `_tokenEstimate ≤ 300` for at least 80% of `benchmark_graph_tools.py` cases
-- Every tool response includes `summary` and `_tokenEstimate` fields
-- `CODE_GRAPH_SUMMARIZER_URL` optional; server starts without it
-
-**Implementation status (2026-02-21)**:
-
-- ✅ Added `src/response/budget.ts` with profile budgets, `ContextBudget`, `makeBudget()`, token estimation, and `fillSlot()`.
-- ✅ Added `src/response/shaper.ts` with answer-first envelope (`summary`, `_tokenEstimate`) and shared error formatting.
-- ✅ Added `src/response/schemas.ts` with field-priority schemas and budget-aware field dropping.
-- ✅ Integrated `ToolHandlers` to use the shared shaper for success/error responses; `graph_query`, `graph_rebuild`, and `graph_health` now pass tool-specific summaries.
-- ✅ Verified compile (`npm run build`) and MCP chat smoke call (`graph_query`) after integration.
-- ✅ Expanded field-priority schema coverage across episode, coordination, workspace, and progress tools.
-- ✅ Added indexing-time summarization integration with `src/response/summarizer.ts` and summary persistence in graph writes (`FILE`/`FUNCTION`/`CLASS`/`IMPORT`).
-- ✅ `CODE_GRAPH_SUMMARIZER_URL` remains optional with local fallback summaries and cache-by-hash behavior.
-- ✅ Added `isConfigured(): boolean` accessor to `CodeSummarizer` for health-check visibility.
-- ✅ Added startup `console.warn` in `initializeVectorEngine` when `CODE_GRAPH_SUMMARIZER_URL` is unset, surfacing heuristic-only mode at server start.
-- ✅ `graph_health` now exposes `summarizer.configured` and `summarizer.endpoint` fields.
-- ✅ Phase 1 acceptance criteria complete.
-
-#### 1.1 Context Budget System
-
-**File**: `src/response/budget.ts` (new)
-
-Replace `compactValue()` / `shapeValue()` in `tool-handlers.ts` with a proper `ContextBudget` class that allocates tokens by relevance category. Inspired by Mem0's approach — reduce tokens by **selecting** the right things, not by cutting strings.
-
-```typescript
-// src/response/budget.ts
-export const DEFAULT_TOKEN_BUDGETS: Record = {
- compact: 300,
- balanced: 1200,
- debug: Infinity,
-};
-
-export interface BudgetAllocation {
- coreCode: number; // 40% — exact symbols being edited/asked about
- dependencies: number; // 25% — direct callers/callees (PPR-ranked post Phase 5)
- decisions: number; // 20% — relevant past DECISION episodes (post Phase 3)
- plan: number; // 10% — current task plan / progress state
- episodeHistory: number; // 5% — recent agent episodes
-}
-
-export interface ContextBudget {
- maxTokens: number;
- profile: "compact" | "balanced" | "debug";
- allocation: BudgetAllocation;
-}
-
-export function makeBudget(
- profile: "compact" | "balanced" | "debug",
- override?: Partial,
-): ContextBudget {
- const max = DEFAULT_TOKEN_BUDGETS[profile];
- return {
- maxTokens: max,
- profile,
- allocation: {
- coreCode: Math.floor(max * 0.4),
- dependencies: Math.floor(max * 0.25),
- decisions: Math.floor(max * 0.2),
- plan: Math.floor(max * 0.1),
- episodeHistory: Math.floor(max * 0.05),
- },
- ...override,
- };
-}
-
-/** Fill a budget slot: add items from ranked list until slot is full. */
-export function fillSlot(
- items: T[],
- tokenFn: (item: T) => number,
- slotBudget: number,
-): { selected: T[]; usedTokens: number } {
- let usedTokens = 0;
- const selected: T[] = [];
- for (const item of items) {
- const cost = tokenFn(item);
- if (usedTokens + cost > slotBudget) break;
- selected.push(item);
- usedTokens += cost;
- }
- return { selected, usedTokens };
-}
-```
-
-Each allocation slot is filled by relevance-ranked retrieval until consumed. Slots with no relevant data are skipped entirely — they do not waste tokens on empty arrays.
-
-**Migration path**: `shapeValue()` in `tool-handlers.ts` is the current interim implementation. Once `budget.ts` is shipped, `shapeValue()` becomes a thin wrapper calling the budget system.
-
-#### 1.2 Answer-first response format
-
-**File**: `src/response/shaper.ts` (new)
-
-Every tool response gains a mandatory `summary` field (written by the tool, not auto-generated). Agents in compact mode read `summary` alone and skip `data`. The `_tokenEstimate` field allows the agent to decide whether to request a wider profile.
-
-```typescript
-// src/response/shaper.ts
-export interface ToolResponse {
- ok: boolean;
- summary: string; // 1–3 sentences, answer-first, always present
- profile: string;
- _tokenEstimate: number;
- data?: Record; // omitted in compact if budget met by summary
- hint?: string; // always present on errors
- errorCode?: string; // machine-readable error class
-}
-
-export function formatResponse(
- summary: string,
- data: Record | null,
- budget: ContextBudget,
- hint?: string,
-): ToolResponse {
- const dataStr = data ? JSON.stringify(data) : "";
- const summaryTokens = Math.ceil(summary.length / 4);
- const dataTokens = Math.ceil(dataStr.length / 4);
- const total = summaryTokens + dataTokens;
-
- // In compact profile: omit data entirely if summary+data exceeds budget
- const includeData = budget.profile !== "compact" || total <= budget.maxTokens;
-
- return {
- ok: true,
- summary,
- profile: budget.profile,
- _tokenEstimate: total,
- data: includeData && data ? data : undefined,
- hint,
- };
-}
-
-export function errorResponse(
- errorCode: string,
- message: string,
- hint: string,
-): ToolResponse {
- return {
- ok: false,
- summary: message,
- profile: "compact",
- _tokenEstimate: Math.ceil((message + hint).length / 4),
- errorCode,
- hint,
- };
-}
-```
-
-**Token estimate rule**: `Math.ceil(JSON.stringify(payload).length / 4)`. This is the same formula already used in `tool-handlers.ts::estimateTokens()`. The estimate is intentionally conservative — it over-counts rather than under-counts.
-
-#### 1.3 Tool-specific response schemas and field priorities
-
-**File**: `src/response/schemas.ts` (new)
-
-Each tool declares an `OutputSchema` with field importance weights. The shaper preserves high-importance fields at any compression level and drops low-importance ones first when over budget.
-
-```typescript
-// src/response/schemas.ts
-export type FieldPriority = "required" | "high" | "medium" | "low";
-
-export interface OutputField {
- key: string;
- priority: FieldPriority;
- description: string;
-}
-
-export const TOOL_OUTPUT_SCHEMAS: Record = {
- graph_query: [
- {
- key: "results",
- priority: "required",
- description: "Query results array",
- },
- { key: "count", priority: "high", description: "Result count" },
- {
- key: "cypher",
- priority: "medium",
- description: "Executed Cypher string",
- },
- { key: "warnings", priority: "low", description: "Query warnings" },
- ],
- code_explain: [
- {
- key: "summary",
- priority: "required",
- description: "Answer-first natural language explanation",
- },
- {
- key: "type",
- priority: "required",
- description: "Node type: FILE|FUNCTION|CLASS",
- },
- {
- key: "dependencies",
- priority: "high",
- description: "Outgoing deps (imports, calls)",
- },
- {
- key: "dependents",
- priority: "high",
- description: "Incoming refs (who uses this)",
- },
- { key: "lineRange", priority: "medium", description: "startLine, endLine" },
- { key: "raw", priority: "low", description: "Full raw Cypher node data" },
- ],
- impact_analyze: [
- {
- key: "blastRadius",
- priority: "required",
- description: "Number of impacted files/tests",
- },
- {
- key: "directTests",
- priority: "required",
- description: "Tests that directly import changed files",
- },
- {
- key: "transitiveTests",
- priority: "high",
- description: "Tests impacted through dep chain",
- },
- { key: "graph", priority: "low", description: "Full traversal graph" },
- ],
- arch_validate: [
- {
- key: "violations",
- priority: "required",
- description: "List of violations (may be empty)",
- },
- { key: "summary", priority: "required", description: "Pass/fail summary" },
- { key: "checkedFiles", priority: "medium", description: "Files checked" },
- ],
- // ... (all 14 existing tools have schemas; see full table in §4)
-};
-
-/** Drop low-priority fields first until response fits within budget. */
-export function applyFieldPriority(
- data: Record,
- schema: OutputField[],
- budget: number,
-): Record {
- const priorities: FieldPriority[] = ["low", "medium", "high", "required"];
- let result = { ...data };
-
- for (const level of priorities) {
- if (Math.ceil(JSON.stringify(result).length / 4) <= budget) break;
- for (const field of schema.filter((f) => f.priority === level)) {
- delete result[field.key];
- }
- }
- return result;
-}
-```
-
-**Invariant**: `required` fields are NEVER dropped regardless of budget. If required fields alone exceed budget, the tool returns an error advising `profile: 'balanced'`.
-
-#### 1.4 Meta-RAG Indexing-Time Summarization
-
-**Goal**: Achieve ~79.8% token compression for compact-profile responses by storing a one-sentence LLM summary on every FUNCTION and CLASS node during indexing. Tools return `node.summary` instead of raw code in compact mode.
-
-**Research backing**: Meta-RAG Code Summarization — summarizing every indexed unit during ingestion, not at query time, amortizes the LLM cost across all future queries.
-
-**File**: `src/graph/summarizer.ts` (new)
-
-```typescript
-// src/graph/summarizer.ts
-export interface SummarizerConfig {
- url: string; // OpenAI-compatible /v1/chat/completions endpoint
- model: string; // default: 'gpt-4o-mini'
- maxTokens: number; // default: 30 — keep summaries tight
- batchSize: number; // default: 20 — concurrent requests
- timeout: number; // default: 5000ms per request
-}
-
-export async function summarizeSymbol(
- name: string,
- kind: "function" | "class" | "method",
- code: string,
- file: string,
- cfg: SummarizerConfig,
-): Promise {
- // Trim code to first 300 chars to stay within context limits
- const codeSample = code.slice(0, 300);
- const prompt = `In one sentence (max 15 words), describe what ${kind} \`${name}\` in ${file} does: \`\`\`${codeSample}\`\`\``;
- // ... OpenAI-compatible API call
-}
-
-/** Heuristic fallback when no summarizer URL is configured. */
-export function extractHeuristicSummary(code: string, name: string): string {
- // Try JSDoc / docstring first
- const jsdoc = code
- .match(/\/\*\*\s*(.*?)\s*\*\//s)?.[1]
- ?.split("\n")[0]
- ?.trim();
- if (jsdoc) return jsdoc.replace(/^\*\s*/, "");
- // Try first non-blank non-comment line
- const firstLine = code
- .split("\n")
- .find((l) => l.trim() && !l.trim().startsWith("//"));
- return firstLine?.trim() ?? `${name} implementation`;
-}
-```
-
-**Integration in `src/graph/builder.ts`**:
-
-1. After AST parse, for each FUNCTION/CLASS node, call `summarizeSymbol()` or `extractHeuristicSummary()` depending on `CODE_GRAPH_SUMMARIZER_URL`
-2. Store result as `summary: string` property on the node
-3. **Qdrant embedding uses `summary` text** (not raw code) — dramatically improves semantic search because summaries are domain-language
-4. BM25-Plus index (Phase 8) also runs over `summary + name` fields
-5. `graph_rebuild` logs: `"Summarized N symbols (M cached)"` to help operators monitor cost
-
-```typescript
-// builder.ts — per-node flow
-const summaryText = process.env.CODE_GRAPH_SUMMARIZER_URL
- ? await summarizeSymbol(
- node.name,
- node.kind,
- node.code,
- relPath,
- summarizerCfg,
- )
- : extractHeuristicSummary(node.code, node.name);
-
-node.summary = summaryText;
-// Store in Memgraph node property
-// Qdrant: embed summaryText (not node.code)
-```
-
-**Cost control**: Summaries are cached by `(scip_id, contentHash)`. If the file hash has not changed since last rebuild, the cached summary is reused — no LLM call. This makes incremental rebuilds almost free even with a remote summarizer.
-
-This is the single biggest lever for hitting the Phase 1 target of `_tokenEstimate ≤ 300` in compact mode.
-
----
-
-### Phase 2 — Bi-Temporal Graph Model
-
-**Goal**: Every graph write is automatically time-stamped. Historical queries become trivial. Replaces both manual snapshots and TTL-based expiry.
-**Estimated effort**: 1–2 weeks
-**Depends on**: Phase 1 (SCIP-style IDs make MERGE-based temporal writes safe)
-**Unblocks**: Phase 3 (episodes need `validFrom` on code nodes), Phase 4 (claims reference `validFrom` hashes), Phase 10 (watcher uses `GRAPH_TX` nodes)
-**Research backing**: Graphiti/Zep (arXiv:2501.13956) — bi-temporal model is the defining architectural innovation for agent memory graphs.
-**Acceptance criteria**:
-
-- All FILE, FUNCTION, CLASS, IMPORT nodes carry `validFrom`, `validTo`, `createdAt`, `txId`
-- `graph_rebuild` creates a `GRAPH_TX` node and links all affected FILE nodes to it
-- `graph_query` accepts `asOf` parameter and produces correct historical results
-- `diff_since` tool returns meaningful diff for two consecutive rebuilds
-
-**Implementation status (2026-02-21)**:
-
-- ✅ Added bi-temporal properties (`validFrom`, `validTo`, `createdAt`, `txId`) to FILE/FUNCTION/CLASS/IMPORT node writes in `src/graph/builder.ts`.
-- ✅ Added transaction propagation (`txId`, `txTimestamp`) through `src/graph/orchestrator.ts` build options/results.
-- ✅ Added `GRAPH_TX` creation on `graph_rebuild` start in `src/tools/tool-handlers.ts`.
-- ✅ Added `graph_query.asOf` (natural-language mode) and exposed schema support in both `src/server.ts` and `src/mcp-server.ts`.
-- ✅ Added `graph_health` transaction metadata (`latestTxId`, `latestTxTimestamp`, `txCount`).
-- ✅ Added `diff_since` tool implementation with `since` anchor resolution (`txId` / timestamp / git SHA / agentId) in `src/tools/tool-handlers.ts`.
-- ✅ Registered `diff_since` in both MCP surfaces (`src/server.ts`, `src/mcp-server.ts`) and response shaping (`src/response/schemas.ts`).
-- ✅ Added `graph_query.asOf` support for `language: 'cypher'` via temporal predicate rewrite on MATCH clauses.
-- ✅ Phase 2 acceptance criteria complete.
-- ✅ **SCIP-style `scipId` field** (2026-02-22): Added additive `scipId` property to FILE/FUNCTION/CLASS nodes in `src/graph/builder.ts`. Format: `file='relPath'`, `class='relPath::Name#'`, `func='relPath::name()'`, `method='relPath::Class#method()'`. Field coexists with existing `id` — no breaking change to existing queries.
-
-#### 2.1 Temporal schema extension
-
-Add bi-temporal properties to all mutable node types. Apply in `src/graph/builder.ts` during the `MERGE/SET` step for every node write.
-
-```cypher
-// Cypher properties added to FILE, FUNCTION, CLASS, IMPORT nodes
-{
- validFrom: $validFrom, // epoch ms — when this code version became true
- validTo: null, // epoch ms | null — null means current/active
- createdAt: $createdAt, // epoch ms — when this row was ingested (transaction time)
- txId: $txId // links to GRAPH_TX node
-}
-```
-
-**SCIP ID + bi-temporal = safe MERGE**: Because node IDs are stable (`src/tools/tool-handlers.ts::ToolHandlers::callTool`), `MERGE` on ID finds the existing node. The temporal update flow is:
-
-```cypher
-// Step 1: Retire the old version (set validTo)
-MATCH (n {id: $scip_id, projectId: $pid, validTo: null})
-SET n.validTo = $now;
-
-// Step 2: Create the new version
-CREATE (n2 {id: $scip_id + '#' + $txId, projectId: $pid,
- validFrom: $now, validTo: null, createdAt: $now, txId: $txId,
- /* all other properties */});
-
-// Step 3: SUPERSEDES edge
-MATCH (old {id: $scip_id, validTo: $now}),
- (new {txId: $txId, id: $scip_id + '#' + $txId})
-CREATE (new)-[:SUPERSEDES]->(old);
-```
-
-**Nothing is ever deleted** — `validTo` being set is the only "deletion". Historical queries simply filter `validTo > $T`.
-
-**Stable SCIP query ID convention**: The "current" version of any symbol is always the node with `validTo = null`. To query at a point in time:
-
-```cypher
-// Current version
-MATCH (fn:FUNCTION {id: $scipId}) WHERE fn.validTo IS NULL RETURN fn
-
-// As of a specific timestamp T
-MATCH (fn:FUNCTION {id: $scipId})
-WHERE fn.validFrom <= $T AND (fn.validTo IS NULL OR fn.validTo > $T)
-RETURN fn
-```
-
-#### 2.2 Transaction log — GRAPH_TX nodes
-
-Every rebuild, file-change event, and agent edit creates one `GRAPH_TX` node. This is the audit trail.
-
-```cypher
-// Full GRAPH_TX schema
-CREATE (tx:GRAPH_TX {
- id: $txId, // UUID — transactions use UUIDs (nodes use SCIP IDs)
- type: $type, // 'full_rebuild' | 'incremental_rebuild' | 'file_change' | 'agent_edit'
- agentId: $agentId, // null if system-initiated
- sessionId: $sessionId,
- gitCommit: $sha, // null if no git integration
- timestamp: $ts,
- mode: $mode, // 'full' | 'incremental'
- filesAffected: $paths, // string[] — relative paths
- nodeCount: $nodeCount, // how many nodes were written
- durationMs: $durationMs
-})
-
-// Link: GRAPH_TX → each affected FILE node
-MATCH (tx:GRAPH_TX {id: $txId}), (f:FILE {path: $path, projectId: $pid, validTo: null})
-CREATE (tx)-[:AFFECTS]->(f)
-```
-
-**`GRAPH_TX` IDs remain UUIDs** because transactions are not addressable by source path — they're events, not code symbols.
-
-#### 2.3 `diff_since` — new tool (Phase 2)
-
-**File**: new handler in `src/tools/tool-handlers.ts`, registered in `src/mcp-server.ts` + `src/server.ts`
-
-```typescript
-// Input schema
-interface DiffSinceArgs {
- since: string; // txId UUID | ISO-8601 timestamp | agentId | git commit SHA
- projectId?: string;
- types?: ("FILE" | "FUNCTION" | "CLASS")[]; // default: all
- profile?: "compact" | "balanced" | "debug";
-}
-
-// Response
-interface DiffSinceResult {
- summary: string; // "3 functions added, 1 deleted, 2 modified since "
- added: NodeDelta[];
- removed: NodeDelta[];
- modified: NodeDelta[];
- txIds: string[]; // transaction IDs covered by the diff
-}
-
-interface NodeDelta {
- scip_id: string;
- type: "FILE" | "FUNCTION" | "CLASS";
- path: string;
- symbolName?: string;
- validFrom: number;
- validTo?: number;
-}
-```
-
-**Resolution of `since` parameter**:
-
-1. Looks like a UUID → treat as `txId`
-2. Looks like ISO-8601 → use as epoch timestamp
-3. Looks like a git SHA (hex 7–40) → query `GRAPH_TX {gitCommit: $sha}`
-4. Anything else → treat as `agentId` → find last `GRAPH_TX` by that agent
-
-**Cypher for modified nodes**:
-
-```cypher
-MATCH (tx:GRAPH_TX)
-WHERE tx.timestamp >= $sinceTs
-WITH collect(tx.id) AS txIds
-MATCH (n)-[:SUPERSEDES]->(old)
-WHERE n.txId IN txIds
-RETURN n, old, 'modified' AS changeType
-UNION
-MATCH (n)
-WHERE n.txId IN txIds AND NOT (n)-[:SUPERSEDES]->()
-RETURN n, null AS old, 'added' AS changeType
-```
-
-#### 2.4 Updated existing tools
-
-| Tool | Change |
-| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `graph_query` | Add optional `asOf: string` parameter (ISO-8601 or txId). When set, appends `WHERE n.validFrom <= $T AND (n.validTo IS NULL OR n.validTo > $T)` to all node matches |
-| `graph_rebuild` | Creates `GRAPH_TX` before indexing, links FILE nodes after, stores `txId` in return value |
-| `graph_health` | New field: `latestTxId`, `latestTxTimestamp`, `txCount` — from latest `GRAPH_TX` node |
-| `code_explain` | Adds `validFrom`, `validTo` to output in balanced/debug profile |
-
----
-
-### Phase 3 — Episode-Based Agent Memory
-
-**Goal**: Agents can persist structured observations, decisions, and edits that survive restarts and are retrievable by semantic + temporal + graph search.
-**Estimated effort**: 1–2 weeks
-**Depends on**: Phase 2 (episodes reference `validFrom` on code nodes; bi-temporal model lets episodes survive rebuilds without becoming stale)
-**Unblocks**: Phase 4 (coordination reads episodes), Phase 5 (`context_pack` surfaces relevant decisions + learnings)
-**Research backing**: Graphiti (episodic memory), Generative Agents (observation → reflection → planning)
-**Acceptance criteria**:
-
-- `episode_add` persists to Memgraph and survives server restart
-- `episode_recall` returns correct results for vector + temporal + graph proximity queries
-- `reflect()` produces `REFLECTION` and `LEARNING` nodes from ≥ 3 episodes
-- Sensitive episodes (`sensitive: true`) excluded from default queries
-
-**Implementation status (2026-02-21)**:
-
-- ✅ Added `src/engines/episode-engine.ts` with persistent EPISODE writes to Memgraph, `NEXT_EPISODE` chaining, and `INVOLVES` links.
-- ✅ Added tool handlers in `src/tools/tool-handlers.ts`: `episode_add`, `episode_recall`, `decision_query`, and `reflect`.
-- ✅ Added Phase 3 tool schemas in both MCP surfaces (`src/server.ts`, `src/mcp-server.ts`).
-- ✅ `reflect` now creates `REFLECTION` episodes and materializes `LEARNING` nodes with `APPLIES_TO` links.
-- ✅ Added Qdrant-assisted entity hints in `episode_recall` via embedding similarity retrieval.
-- ✅ Added stricter type-specific metadata validation contracts in `episode_add` for DECISION/EDIT/TEST_RESULT/ERROR.
-- ✅ Phase 3 optimization follow-ups complete for current architecture.
-
-#### 3.1 New Engine: `EpisodeEngine`
-
-**File**: `src/engines/episode-engine.ts`
-
-```typescript
-// src/engines/episode-engine.ts
-export type EpisodeType =
- | "OBSERVATION" // agent read code or queried the graph
- | "DECISION" // agent made a binding technical choice
- | "EDIT" // agent modified code
- | "TEST_RESULT" // tests were run
- | "ERROR" // agent encountered unexpected state
- | "REFLECTION" // synthesized insight from multiple episodes (internal)
- | "LEARNING"; // durable pattern extracted from reflections
-
-export interface EpisodeInput {
- agentId: string;
- sessionId: string;
- taskId?: string;
- type: EpisodeType;
- content: string; // human-readable natural language summary
- entities?: string[]; // SCIP IDs: code nodes this episode involves
- outcome?: "success" | "failure" | "partial";
- metadata?: Record; // type-specific extras (see below)
- sensitive?: boolean; // if true: excluded from default queries
-}
-
-export interface Episode extends EpisodeInput {
- id: string; // UUID
- timestamp: number; // epoch ms
- contentEmbedding: number[]; // Qdrant vector of `content`
- prevEpisodeId?: string; // previous episode in agent's session chain
-}
-
-export class EpisodeEngine {
- constructor(
- private memgraph: MemgraphClient,
- private qdrant: QdrantClient,
- private embedding: EmbeddingEngine,
- ) {}
-
- async add(input: EpisodeInput): Promise;
- async recall(query: RecallQuery): Promise;
- async reflect(opts: ReflectOptions): Promise;
- async decisionQuery(q: DecisionQueryArgs): Promise;
- private async linkToPreviousEpisode(ep: Episode): Promise;
- private async linkToCodeNodes(ep: Episode): Promise;
-}
-```
-
-**Episode type-specific metadata contracts**:
-
-| Type | Required metadata fields |
-| ------------- | ---------------------------------------------------------------------------------------------------- |
-| `DECISION` | `title: string`, `rationale: string`, `tradeoffs: string[]`, `affectedFiles: string[]` |
-| `EDIT` | `file: string`, `diffSummary: string`, `linesBefore: number`, `linesAfter: number`, `reason: string` |
-| `TEST_RESULT` | `passed: number`, `failed: number`, `testFiles: string[]`, `coverageDelta?: number` |
-| `ERROR` | `errorType: string`, `stackSummary: string`, `recoveryAction?: string` |
-| `OBSERVATION` | `confidence?: number` (0–1), `queryUsed?: string` |
-
-#### 3.2 Graph schema for episodes
-
-```cypher
-// Episode node — full schema
-CREATE (e:EPISODE {
- id: $id, // UUID
- agentId: $agentId,
- sessionId: $sessionId,
- taskId: $taskId, // nullable
- type: $type,
- content: $content, // NL summary — always human-readable
- timestamp: $ts,
- outcome: $outcome, // 'success' | 'failure' | 'partial' | null
- sensitive: $sensitive, // boolean — exclude from default queries
- metadata: $metadataJson, // JSON string of type-specific extras
- projectId: $projectId
- // Note: contentEmbedding is stored in Qdrant, not Memgraph
-})
-
-// Session chain: episodes are linked in order within a session
-MATCH (prev:EPISODE {id: $prevId}), (curr:EPISODE {id: $currId})
-CREATE (prev)-[:NEXT_EPISODE]->(curr)
-
-// Episode ↔ code node links (many-to-many)
-MATCH (e:EPISODE {id: $epId}), (n {id: $scipId, projectId: $pid})
-CREATE (e)-[:INVOLVES]->(n)
-
-// Reflection derived from source episodes
-MATCH (r:EPISODE {type: 'REFLECTION'}), (src:EPISODE {id: $srcId})
-CREATE (r)-[:DERIVED_FROM]->(src)
-
-// Learning node applied to code
-CREATE (l:LEARNING {
- id: $lid,
- content: $nlSummary,
- extractedAt: $ts,
- agentId: $agentId,
- taskId: $taskId,
- confidence: $confidence, // 0.0–1.0
- projectId: $pid
-})
-MATCH (l:LEARNING {id: $lid}), (n {id: $scipId, projectId: $pid})
-CREATE (l)-[:APPLIES_TO]->(n)
-```
-
-**Qdrant collection**: `episodes_{projectId}` — each point:
-
-```json
-{
- "id": "",
- "vector": [
- /* embedding of episode.content */
- ],
- "payload": {
- "agentId": "...",
- "sessionId": "...",
- "taskId": "...",
- "type": "DECISION",
- "timestamp": 1234567890,
- "entities": ["src/tools/tool-handlers.ts::ToolHandlers::callTool"],
- "sensitive": false
- }
-}
-```
-
-#### 3.3 `episode_recall` — hybrid search algorithm
-
-Recall combines three signals with weighted sum then re-ranks:
-
-```
-Score(episode) =
- α × VectorSimilarity(query_embedding, episode.contentEmbedding) // α = 0.50
- + β × TemporalRecency(episode.timestamp) // β = 0.30
- + γ × GraphProximity(query_entities, episode.entities) // γ = 0.20
-
-where:
- TemporalRecency(ts) = exp(-λ × age_in_days) // λ = 0.05 (half-life ≈ 14 days)
- GraphProximity = |query_entities ∩ episode.entities| / |query_entities ∪ episode.entities|
- (Jaccard similarity on SCIP ID sets)
-```
-
-```typescript
-interface RecallQuery {
- query: string; // natural language
- agentId?: string; // if set: only episodes from this agent
- taskId?: string;
- types?: EpisodeType[];
- entities?: string[]; // SCIP IDs to boost proximity score
- limit?: number; // default: 5
- since?: number; // epoch ms — exclude older episodes
-}
-```
-
-#### 3.4 Reflection synthesis — from Generative Agents
-
-`reflect()` is called:
-
-- On demand via the `reflect` MCP tool
-- Automatically when `task_update(..., status: 'completed')` is called (Phase 4 integration)
-- Periodically via a background timer if `CODE_GRAPH_AUTO_REFLECT_INTERVAL_MS` is set
-
-**Algorithm**:
-
-```typescript
-async reflect(opts: { taskId?: string; agentId?: string; limit?: number }): Promise {
- // 1. Fetch last N episodes for task/agent (default N = 20)
- const source = await this.recall({ taskId, agentId, limit: opts.limit ?? 20 });
-
- // 2. Identify patterns:
- // - Files touched by ≥ 3 EDITs → likely hotspot
- // - DECISION nodes near ERROR nodes → decision led to failure
- // - Repeated similar OBSERVATIONs → the agent is re-reading the same code
- const patterns = analyzeEpisodePatterns(source);
-
- // 3. Build REFLECTION node
- const reflection = await this.add({
- type: 'REFLECTION',
- content: synthesizeInsight(patterns), // structured NL summary
- entities: patterns.hotspotFiles,
- metadata: { sourceEpisodeIds: source.map(e => e.id), patterns },
- });
-
- // 4. Extract LEARNING nodes for each pattern with confidence ≥ 0.7
- const learnings = patterns
- .filter(p => p.confidence >= 0.7)
- .map(p => this.createLearning(p, reflection.id));
-
- return { reflectionId: reflection.id, learnings, patterns };
-}
-```
-
-**LEARNING node creation triggers**: learnings are linked to the code nodes involved in the reflection's patterns. Future `context_pack` calls that reach those code nodes via PPR will surface the learnings automatically.
-
-#### 3.5 New Tools
-
-```typescript
-// episode_add
-interface EpisodeAddArgs {
- type: EpisodeType;
- content: string;
- entities?: string[]; // SCIP IDs
- taskId?: string;
- outcome?: "success" | "failure" | "partial";
- metadata?: Record;
- sensitive?: boolean;
-}
-// → returns { episodeId: string, summary: string }
-
-// episode_recall
-interface EpisodeRecallArgs {
- query: string;
- agentId?: string;
- taskId?: string;
- types?: EpisodeType[];
- limit?: number; // default: 5
- since?: string; // ISO-8601
- profile?: "compact" | "balanced" | "debug";
-}
-// → returns ranked list of episodes with relevance scores
-
-// decision_query
-interface DecisionQueryArgs {
- query: string;
- affectedFiles?: string[];
- limit?: number;
-}
-// → same as episode_recall but type=['DECISION'] and graph proximity weighted higher (γ = 0.5)
-
-// reflect
-interface ReflectArgs {
- taskId?: string;
- agentId?: string;
- limit?: number; // max episodes to analyse, default: 20
-}
-// → { reflectionId, insight: string, learningsCreated: number, patterns: PatternSummary[] }
-```
-
----
-
-### Phase 4 — Agent Coordination (Temporal Invalidation)
-
-**Goal**: Multiple agents coordinate on tasks without message-passing. Claims self-invalidate when underlying code changes.
-**Estimated effort**: 1 week
-**Depends on**: Phase 2 (claim staleness detects code node `validFrom` changes; SUPERSEDES edges drive auto-invalidation), Phase 3 (`task_update` triggers reflection)
-**Unblocks**: Phase 5 (`context_pack` reads `activeBlockers` from live CLAIM nodes)
-**Research backing**: Graphiti (temporal edge invalidation instead of TTL deletion), A2A Agent Cards (§0.4 — discoverable capability declaration)
-**Acceptance criteria**:
-
-- `agent_claim` returns `CONFLICT` when another active claim targets the same node
-- Claims that target code nodes subsequently rebuilt have `validTo` set automatically
-- `coordination_overview` reflects real-time claim state including stale detection
-- `task_update(status: 'completed')` calls `EpisodeEngine.reflect()` and auto-releases open claims for that task
-- `GET /.well-known/agent.json` returns valid A2A Agent Card JSON-LD (HTTP mode)
-
-**Implementation status (2026-02-21)**:
-
-- ✅ Added `src/engines/coordination-engine.ts` with `claim`, `release`, `status`, `overview`, `invalidateStaleClaims`, and `onTaskCompleted`.
-- ✅ Added Phase 4 handlers in `src/tools/tool-handlers.ts`: `agent_claim`, `agent_release`, `agent_status`, `coordination_overview`.
-- ✅ Integrated stale-claim invalidation after background `graph_rebuild` completion.
-- ✅ Integrated `task_update(status: 'completed')` to auto-release task claims, trigger `reflect`, and persist a `DECISION` episode.
-- ✅ Added Phase 4 tool schemas to both MCP surfaces (`src/server.ts`, `src/mcp-server.ts`).
-
-#### 4.1 New Engine: `CoordinationEngine`
-
-**File**: `src/engines/coordination-engine.ts`
-
-Uses **temporal invalidation** instead of TTL: a claim is invalidated when the code node it targets is superseded (new GRAPH_TX writes a new version with `validFrom > claim.validFrom`).
-
-```typescript
-export type ClaimType = "task" | "file" | "function" | "feature";
-export type InvalidationReason =
- | "released"
- | "code_changed"
- | "task_completed"
- | "expired";
-
-export interface AgentClaim {
- id: string; // UUID
- agentId: string;
- sessionId: string;
- taskId?: string;
- claimType: ClaimType;
- targetId: string; // SCIP ID or task ID
- intent: string; // NL description of what this agent is doing
- validFrom: number; // epoch ms when claim was created
- targetVersionSHA?: string; // gitCommit or contentHash at claim time
- validTo: number | null; // null = active; set when invalidated
- invalidationReason?: InvalidationReason;
- projectId: string;
-}
-
-export class CoordinationEngine {
- async claim(input: ClaimInput): Promise;
- async release(claimId: string, outcome?: string): Promise;
- async status(agentId: string): Promise;
- async overview(projectId: string): Promise;
- async invalidateStaleClaims(projectId: string): Promise; // returns invalidated count
- async onTaskCompleted(taskId: string, agentId: string): Promise;
-}
-```
-
-#### 4.2 Cypher schema for CLAIM nodes
-
-```cypher
-// CLAIM node — full schema
-CREATE (c:CLAIM {
- id: $id,
- agentId: $agentId,
- sessionId: $sessionId,
- taskId: $taskId,
- claimType: $type, // 'task' | 'file' | 'function' | 'feature'
- intent: $intent,
- validFrom: $now,
- targetVersionSHA: $sha,
- validTo: null,
- invalidationReason: null,
- projectId: $projectId
-})
-
-// Link claim to its target code node
-MATCH (c:CLAIM {id: $cId}), (t {id: $targetId, projectId: $pid})
-CREATE (c)-[:TARGETS]->(t)
-
-// Staleness detection query (run after each graph_rebuild)
-// → returns claims whose target code node has been superseded
-MATCH (c:CLAIM)-[:TARGETS]->(t)
-WHERE c.validTo IS NULL
- AND t.validFrom > c.validFrom
-RETURN c.id, t.id, t.validFrom AS newVersion
-```
-
-**Conflict detection query** (run before inserting a new claim):
-
-```cypher
-MATCH (c:CLAIM)-[:TARGETS]->(t {id: $targetId, projectId: $pid})
-WHERE c.validTo IS NULL
- AND c.agentId <> $requestingAgentId
-RETURN c.id, c.agentId, c.intent, c.validFrom
-```
-
-**Auto-invalidation** is triggered inside `graph_rebuild` completion handler:
-
-```typescript
-// In GraphOrchestrator.onRebuildComplete():
-const count = await coordinationEngine.invalidateStaleClaims(projectId);
-if (count > 0)
- logger.info(`[coordination] Invalidated ${count} stale claims post-rebuild`);
-```
-
-#### 4.3 New Tool Interfaces
-
-```typescript
-// agent_claim
-interface ClaimInput {
- targetId: string; // SCIP ID, file path, or task ID
- claimType: ClaimType;
- intent: string;
- taskId?: string;
-}
-interface ClaimResult {
- claimId: string;
- status: "ok" | "CONFLICT";
- conflict?: { agentId: string; intent: string; since: number };
- targetVersionSHA: string; // snapshot at claim time — client should monitor for drift
-}
-
-// agent_release
-interface ReleaseArgs {
- claimId: string;
- outcome?: string; // NL summary of what was accomplished
-}
-
-// agent_status
-interface AgentStatus {
- agentId: string;
- activeClaims: AgentClaim[];
- recentEpisodes: Episode[]; // last 10
- currentTask?: string;
-}
-
-// coordination_overview
-interface CoordinationOverview {
- activeClaims: AgentClaim[];
- staleClaims: AgentClaim[]; // validTo IS NULL but target has newer version
- conflicts: ConflictPair[]; // two agents with active claims on same target
- agentSummary: { agentId: string; claimCount: number; lastSeen: number }[];
- totalClaims: number;
-}
-```
-
-#### 4.4 `task_update` integration
-
-When `task_update(taskId, { status: 'completed' })` is called:
-
-1. `CoordinationEngine.onTaskCompleted(taskId, agentId)`:
- - Set `validTo = now`, `invalidationReason = 'task_completed'` on all claims for this task
-2. `EpisodeEngine.reflect({ taskId })` — create REFLECTION + LEARNING nodes
-3. Add a `DECISION` episode with outcome = 'success' or 'failure' (from task status)
-
-#### 4.5 A2A Agent Card (cross-reference §0.4)
-
-In HTTP mode, `GET /.well-known/agent.json` returns:
-
-```json
-{
- "@context": "https://schema.org",
- "@type": "SoftwareAgent",
- "name": "",
- "version": "2.0.0",
- "capabilities": ["code-graph", "episodic-memory", "agent-coordination"],
- "mcpEndpoint": "/mcp",
- "a2aVersion": "1.0"
-}
-```
-
-This endpoint was already added in the cleanup phase. Phase 4 extends the `capabilities` array to include `"agent-coordination"` once `CoordinationEngine` is live.
-
----
-
-### Phase 5 — Context Pack with PPR (The Key Feature)
-
-**Goal**: Single tool call = complete task briefing. Uses Personalized PageRank for relevance-ranked retrieval.
-**Estimated effort**: 2 weeks
-**Depends on**: Phase 1 (budget/shaper), Phase 2 (temporal node versions), Phase 3 (EPISODE/LEARNING nodes), Phase 4 (CLAIM nodes for active-blockers)
-**Unblocks**: Phase 6 (`semantic_slice` materialises code for context_pack `coreSymbols`)
-**Research backing**: HippoRAG (PPR for multi-hop retrieval — 20% better than vector alone, 10-30× cheaper than iterative LLM chains)
-**Acceptance criteria**:
-
-- `context_pack` completes in < 500ms for graphs ≤ 10 000 nodes
-- Returns non-empty `coreSymbols` and `summary` for any valid task string
-- `ContextPack.tokenEstimate` ≤ budget for profile in use (compact=300, balanced=1200, debug=∞)
-- Interface-consumer expansion includes at least 1 concrete implementation for any abstract seed
-- `activeBlockers` contains claims from other agents when present in graph
-
-**Implementation status (2026-02-21)**:
-
-- ✅ Added `src/graph/ppr.ts` with weighted graph traversal + Personalized PageRank-style scoring (`runPPR`).
-- ✅ Added `context_pack` handler in `src/tools/tool-handlers.ts` with seed selection, interface-seed expansion, PPR ranking, code-slice materialization, and blocker/decision/learning/episode aggregation.
-- ✅ Added budget-aware trimming and `tokenEstimate` in `context_pack` output prior to response shaping.
-- ✅ Added `context_pack` tool schemas to both MCP surfaces (`src/server.ts`, `src/mcp-server.ts`) and response-priority schema (`src/response/schemas.ts`).
-- ✅ **Production hardening**: `ProgressEngine` constructor in `tool-handlers.ts` now receives `this.context.memgraph` (was always accepted but never passed — Memgraph writes were silently dead code).
-- ✅ `createFeature()` and `createTask()` are now `async` and issue a `MERGE` to Memgraph when connected, ensuring feature/task state survives server restarts.
-- ✅ **MAGE-native PPR** (2026-02-22): `runPPR()` now tries `CALL pagerank.get()` (Memgraph MAGE module) + Cypher 3-hop seed expansion first; falls back to JS power-iteration when MAGE unavailable. `PPRResult.pprMode` field (`"mage_pagerank"` | `"js_ppr"`) indicates active path.
-
-#### 5.1 New router module: `src/graph/ppr.ts`
-
-```typescript
-// src/graph/ppr.ts
-export interface PPROptions {
- seedIds: string[]; // node IDs to personalise from
- edgeWeights?: Record; // relationship type → weight (see defaults below)
- damping?: number; // default: 0.85
- iterations?: number; // default: 20
- maxResults?: number; // default: 50
- projectId: string;
-}
-
-export interface PPRResult {
- nodeId: string;
- score: number;
- type: string; // FILE | FUNCTION | CLASS | EPISODE | LEARNING ...
- filePath: string;
- name: string;
-}
-
-// Default edge weights — tuned for code graph
-const DEFAULT_EDGE_WEIGHTS = {
- CALLS: 0.9,
- IMPORTS: 0.7,
- CONTAINS: 0.5,
- TESTS: 0.4,
- DEFINED_IN: 0.6,
- INVOLVES: 0.3, // EPISODE → code nodes
- APPLIES_TO: 0.4, // LEARNING → code nodes
-};
-
-export async function runPPR(
- opts: PPROptions,
- client: MemgraphClient,
-): Promise {
- // Calls Memgraph CALL pagerank.get(graph, opts) via Cypher
- // Returns sorted by score DESC, limited to maxResults
-}
-```
-
-#### 5.2 New module: `src/tools/context-pack.ts`
-
-```typescript
-// src/tools/context-pack.ts
-export interface ContextPackRequest {
- task: string;
- taskId?: string;
- agentId?: string; // if provided, auto-creates CLAIM
- budget?: Partial;
- profile?: "compact" | "balanced" | "debug";
- includeDecisions?: boolean; // default: true
- includeEpisodes?: boolean; // default: true
- includeLearnings?: boolean; // default: true
-}
-
-export interface CodeSlice {
- file: string;
- startLine: number;
- endLine: number;
- code: string; // actual source lines from filesystem
- symbolName: string;
- pprScore: number;
- incomingCallers: SymbolRef[];
- outgoingCalls: SymbolRef[];
- validFrom: string;
- relevantDecisions: string[]; // DECISION episode IDs
- relevantLearnings: string[]; // LEARNING node IDs
-}
-
-export interface ContextPack {
- summary: string; // 2-4 sentences: what to do and where
- entryPoint: string; // best file/function to start at
- coreSymbols: CodeSlice[]; // PPR-ranked code slices
- dependencies: DepEdge[]; // immediate callers/callees
- decisions: DecisionEpisode[];
- learnings: Learning[];
- activeBlockers: ClaimInfo[];
- plan: PlanNode | null;
- tokenEstimate: number;
- pprScores?: Record; // only in debug profile
-}
-
-export async function buildContextPack(
- req: ContextPackRequest,
- deps: ContextPackDeps,
-): Promise;
-```
-
-#### 5.3 PPR-based retrieval pipeline (algorithm detail)
-
-```
-1. Semantic search → top-5 seed node IDs (Qdrant vector similarity on req.task)
-
-2. Interface-consumer expansion (DKB pattern):
- MATCH (iface {projectId: $pid})
- WHERE iface.id IN $seedIds
- AND iface.kind IN ['interface', 'abstract']
- OPTIONAL MATCH (iface)-[:IMPLEMENTED_BY]->(impl)
- WITH collect(DISTINCT impl.id) + $seedIds AS expandedSeeds
- → union of original seeds + their concrete implementations
-
-3. Run PPR from expandedSeeds via ppr.ts (sub-100ms Memgraph built-in)
- Edge weights: CALLS 0.9, IMPORTS 0.7, CONTAINS 0.5, TESTS 0.4, INVOLVES 0.3
-
-4. Apply Phase 1 budget allocation:
- budget = makeBudget(profile)
- for each slot in ['decisions', 'learnings', 'code', 'graph', 'meta']:
- fill from PPR results (highest score for that node type)
- stop when slot exhausted
-
-5. For each selected code node:
- - Read file from disk, extract lines [node.startLine, node.endLine]
- - Read immediate graph neighbours: incomingCallers, outgoingCalls
- - Collect linked DECISION and LEARNING node IDs → CodeSlice
-
-6. Query CLAIM nodes for activeBlockers:
- MATCH (c:CLAIM)-[:TARGETS]->(t)
- WHERE c.validTo IS NULL
- AND t.id IN $selectedIds
- AND c.agentId <> $requestingAgentId
-
-7. Look up existing PlanNode for taskId (if provided)
-
-8. Synthesise summary using Phase 1 summarizer.ts (extractHeuristicSummary)
- or LLM call to CODE_GRAPH_SUMMARIZER_URL if configured
-
-9. Apply formatResponse(pack, profile, budget) → ContextPack trimmed to budget
-```
-
-#### 5.4 CodeSlice materialisation
-
-```typescript
-// Read actual source lines for a graph node
-async function materializeCodeSlice(node: GraphNode, pprScore: number): Promise {
- const rawCode = await readLines(node.filePath, node.startLine, node.endLine);
- // Trim to Phase 1 budget slot: code slot = 300 chars compact / 1200 balanced
- const trimmed = trimToTokenBudget(rawCode, budget.code);
- return { file: node.filePath, startLine: node.startLine, endLine: node.endLine,
- code: trimmed, symbolName: node.name, pprScore, ... };
-}
-```
-
----
-
-### Phase 6 — Semantic Code Slicing
-
-**Goal**: Return actual relevant code lines with graph-enriched context, not file paths.
-**Estimated effort**: 1 week
-**Depends on**: Phase 2 (node `startLine`/`endLine` stored bi-temporally), Phase 5 (`pprScore` passed from context_pack into slices)
-**Unblocks**: nothing directly — this is a standalone consumer-facing tool that improves all upstream outputs
-**Research backing**: MemGPT (page in only what's needed — exact lines, not whole files)
-**Acceptance criteria**:
-
-- `semantic_slice` with `context='body'` returns exact source lines matching the graph node's `startLine`/`endLine`
-- `context='with-deps'` includes at least 1 caller and 1 callee from graph
-- `context='full'` includes relevant decisions and learnings linked to the sliced symbol
-- Symbol lookup by name falls back to hybrid search when not found by exact ID
-
-**Implementation status (2026-02-21)**:
-
-- ✅ Added `semantic_slice` tool handler in `src/tools/tool-handlers.ts` supporting `signature`, `body`, `with-deps`, and `full` contexts.
-- ✅ Added symbol resolution flow: exact id (`::`), `file+symbol`, symbol-only lookup, query fallback, and file fallback.
-- ✅ Implemented exact line materialization from filesystem via node `startLine`/`endLine` with context-specific range rules.
-- ✅ Added dependency enrichment (`incomingCallers`, `outgoingCalls`) and full-context knowledge enrichment (`relevantDecisions`, `relevantLearnings`).
-- ✅ Registered `semantic_slice` schemas on both MCP surfaces (`src/server.ts`, `src/mcp-server.ts`) and response field priorities (`src/response/schemas.ts`).
-
-#### 6.1 New module: `src/tools/semantic-slice.ts`
-
-```typescript
-// src/tools/semantic-slice.ts
-export type SliceContext = "signature" | "body" | "with-deps" | "full";
-
-export interface SemanticSliceRequest {
- file?: string; // relative or absolute path
- symbol?: string; // exact name: "ToolHandlers.callTool" or just "callTool"
- query?: string; // NL fallback: "the auth check logic"
- context?: SliceContext; // default: 'body'
- pprScore?: number; // passed in from context_pack pipeline
- profile?: "compact" | "balanced" | "debug";
-}
-
-// CodeSlice is the same interface as defined in context-pack.ts (shared type)
-export async function buildSemanticSlice(
- req: SemanticSliceRequest,
- deps: SliceDeps,
-): Promise;
-```
-
-#### 6.2 Symbol lookup algorithm
-
-```
-1. If req.symbol is provided and contains '::' → assume SCIP ID → exact graph lookup:
- MATCH (n {id: $symbol, projectId: $pid}) RETURN n
-
-2. If req.symbol is a simple name AND req.file is provided:
- MATCH (f:FILE {path: $file, projectId: $pid})-[:CONTAINS*]->(n)
- WHERE n.name = $symbol RETURN n LIMIT 1
-
-3. If req.symbol only (no file):
- MATCH (n {projectId: $pid}) WHERE n.name = $symbol RETURN n LIMIT 1
-
-4. If not found by any graph path AND req.query provided:
- → fall back to hybrid search (Phase 8 when available, or Qdrant vector search now)
- → pick highest-scored result as slice anchor
-
-5. If nothing found: return error with suggestions (similar names via fuzzy string match on graph `name` property)
-```
-
-#### 6.3 Context mode detail
-
-| mode | content returned | graph enrichment |
-| ----------- | -------------------------------------------------------------------- | --------------------------------- |
-| `signature` | First line only (function declaration) | none |
-| `body` | Full function/class from `startLine` to `endLine` | none |
-| `with-deps` | Body + `incomingCallers` list + `outgoingCalls` list | callers/callees from graph |
-| `full` | Body + callers + callees + `relevantDecisions` + `relevantLearnings` | all linked EPISODE/LEARNING nodes |
-
-```typescript
-// context mode → source lines strategy
-function computeLineRange(
- node: GraphNode,
- context: SliceContext,
-): [number, number] {
- if (context === "signature") return [node.startLine, node.startLine];
- return [node.startLine, node.endLine];
-}
-// Budget cap: 'signature' ≤ 80 chars, 'body' ≤ 1200 chars balanced / 300 chars compact
-```
-
-#### 6.4 Graph enrichment queries
-
-```cypher
-// Callers (who calls this function)
-MATCH (caller)-[:CALLS]->(target {id: $nodeId, projectId: $pid})
-RETURN caller.id, caller.name, caller.filePath LIMIT 10
-
-// Callees (what this function calls)
-MATCH (target {id: $nodeId, projectId: $pid})-[:CALLS]->(callee)
-RETURN callee.id, callee.name, callee.filePath LIMIT 10
-
-// Relevant decisions (EPISODE nodes of type DECISION involving this node)
-MATCH (e:EPISODE {type: 'DECISION'})-[:INVOLVES]->(n {id: $nodeId})
-RETURN e.id, e.content, e.timestamp ORDER BY e.timestamp DESC LIMIT 3
-
-// Relevant learnings
-MATCH (l:LEARNING)-[:APPLIES_TO]->(n {id: $nodeId})
-RETURN l.id, l.content, l.confidence ORDER BY l.confidence DESC LIMIT 3
-```
-
-#### 6.5 `pprScore` propagation
-
-When `semantic_slice` is called from within the `context_pack` pipeline (Phase 5 step 5), the `pprScore` for each node is already computed. It is passed into `buildSemanticSlice` as `req.pprScore` and stored in the returned `CodeSlice.pprScore`. This allows the agent to rank the returned slices by relevance without additional computation.
-
----
-
-### Phase 7 — Community Detection & Global Mode
-
-**Goal**: Agents can ask "what are the main architectural concerns?" and get accurate answers from community summaries.
-**Estimated effort**: 1 week
-**Depends on**: Phase 1 (Meta-RAG summarizer generates community summaries), Phase 2 (temporal nodes give community detection accurate edges)
-**Unblocks**: Phase 8 (community labels improve BM25 index relevance)
-**Research backing**: GraphRAG (Leiden communities + summaries), LightRAG (dual-level local/global retrieval)
-**Acceptance criteria**:
-
-- Community detection runs automatically after each full graph rebuild (not incremental)
-- `graph_query` with `mode: 'global'` returns answer derived from community summaries, not raw nodes
-- Auto-generated community labels are human-readable (e.g. `"AuthServices"`, `"DataLayer"`)
-- LLM-backed community summaries fall back to heuristic summaries if `CODE_GRAPH_SUMMARIZER_URL` is not set
-
-**Implementation status (2026-02-21)**:
-
-- ✅ Added `src/engines/community-detector.ts` with heuristic community clustering over FILE/FUNCTION/CLASS nodes.
-- ✅ `graph_rebuild` full-mode completion now triggers `CommunityDetector.run(projectId)` asynchronously in `src/tools/tool-handlers.ts`.
-- ✅ Added `graph_query.mode` support (`local`, `global`, `hybrid`) and global-mode retrieval from COMMUNITY summaries in `src/tools/tool-handlers.ts`.
-- ✅ Added `graph_query.mode` schema support on both MCP surfaces (`src/server.ts`, `src/mcp-server.ts`).
-- ✅ Community labels are auto-derived from dominant path segments with fallback to `misc`, and summaries are heuristic when no external summarizer is configured.
-- ✅ **MAGE-native Leiden** (2026-02-22): `CommunityDetector.run()` now calls `CALL community_detection.get()` (real Leiden algorithm via MAGE) first; falls back to directory-heuristic grouping when MAGE unavailable. `CommunityRunResult.mode` field (`"mage_leiden"` | `"directory_heuristic"`) indicates which path ran. Docker image updated to `memgraph/memgraph-mage:latest`.
-
-#### 7.1 Leiden community detection — when it runs
-
-```
-graph_rebuild (full) completes
- └─ GraphOrchestrator.onRebuildComplete()
- ├─ CoordinationEngine.invalidateStaleClaims() ← Phase 4
- └─ CommunityDetector.run(projectId) ← Phase 7 (async)
- ├─ CALL community_detection.get(graph) ← Memgraph built-in Leiden
- ├─ auto-label each community
- ├─ generate/update COMMUNITY nodes
- └─ schedule summary generation (batched, async)
-```
-
-Detection is **async and non-blocking**: `graph_rebuild` returns status `QUEUED` immediately; communities are populated within seconds.
-
-#### 7.2 Community detection Cypher
-
-```cypher
-// Run Leiden algorithm via Memgraph module
-CALL community_detection.get(
- subgraphQuery := 'MATCH (n)-[r:CALLS|IMPORTS]->(m) RETURN n, r, m',
- config := {weight_property: 'weight', resolution: 1.0}
-) YIELD node, community_id
-SET node.communityId = community_id;
-
-// Create COMMUNITY aggregator nodes
-MATCH (n {projectId: $pid})
-WHERE n.communityId IS NOT NULL
-WITH n.communityId AS cId, collect(n) AS members
-MERGE (c:COMMUNITY {id: toString($pid) + '::community::' + cId, projectId: $pid})
-SET c.memberCount = size(members),
- c.computedAt = $ts,
- c.label = null // populated below by auto-label heuristic
-FOREACH (m IN members | MERGE (m)-[:BELONGS_TO]->(c))
-
-// Auto-label: most common top-level path segment among member files
-MATCH (c:COMMUNITY {projectId: $pid})<-[:BELONGS_TO]-(n)
-WITH c, n.filePath AS fp
-WITH c, head(split(fp, '/')) AS segment, count(*) AS cnt
-ORDER BY cnt DESC
-WITH c, collect(segment)[0] AS topSegment
-SET c.label = topSegment
-```
-
-#### 7.3 COMMUNITY node full schema
-
-```cypher
-CREATE (c:COMMUNITY {
- id: $id, // "::community::"
- projectId: $pid,
- label: $autoLabel, // "tools", "engines", "graph", "parsers", etc.
- summary: $nlSummary, // LLM or heuristic NL description of what this cluster does
- memberCount: $n,
- centralNode: $mostConnectedId,// highest-degree node in community
- computedAt: $ts
-})
-```
-
-#### 7.4 Auto-labeling heuristic
-
-```typescript
-function autoLabel(memberFilePaths: string[]): string {
- // Count occurrences of each path segment (excluding root)
- const freq: Record = {};
- for (const p of memberFilePaths) {
- const segments = p.split("/").filter(Boolean);
- for (const s of segments) freq[s] = (freq[s] ?? 0) + 1;
- }
- // Most frequent meaningful segment (skip generic names like 'src', 'lib')
- const ignored = new Set(["src", "lib", "dist", "build", "node_modules"]);
- const best = Object.entries(freq)
- .filter(([seg]) => !ignored.has(seg))
- .sort(([, a], [, b]) => b - a)[0];
- return best ? best[0] : "misc";
-}
-```
-
-#### 7.5 `graph_query` — global mode
-
-New `mode` parameter for `graph_query`:
-
-- `local` (default): existing Cypher/hybrid traversal on raw nodes
-- `global`: query COMMUNITY summary nodes → synthesize cross-community answer via summarizer
-- `hybrid`: run both → format as two sections (global context + local detail)
-
-```cypher
-// global mode — retrieve community summaries relevant to the query
-MATCH (c:COMMUNITY {projectId: $pid})
-WHERE c.summary CONTAINS $keywordHint
- OR c.label IN $detectedLabels
-RETURN c.id, c.label, c.summary, c.memberCount
-ORDER BY c.memberCount DESC
-```
-
-Global mode falls back to returning all community node summaries if the query is highly open-ended (e.g. "overview of the codebase").
-
----
-
-### Phase 8 — Hybrid Retrieval (Replacing NL→Cypher for most queries)
-
-**Goal**: Replace fragile regex NL→Cypher translation with a robust hybrid retrieval pipeline.
-**Estimated effort**: 1–2 weeks
-**Depends on**: Phase 1 (Meta-RAG summaries as BM25 index field), Phase 7 (community labels improve BM25 precision)
-**Unblocks**: nothing — terminal improvement; makes all tools more accurate
-**Research backing**: Graphiti (semantic+BM25+graph hybrid at sub-second latency), LightRAG (dual-level hybrid), HippoRAG (PPR as primary ranker)
-**Acceptance criteria**:
-
-- `graph_query` with `language: 'natural'` uses hybrid retriever, not regex Cypher
-- `language: 'cypher'` still passes directly to Memgraph (escape hatch)
-- BM25-Plus index covers `name` and `summary` fields for all FUNCTION, CLASS, FILE nodes
-- RRF fusion score outperforms vector-only baseline on benchmark set (target: ≥5% improvement on P@5)
-- No regression in `test_select`, `impact_analyze`, `code_explain` (all use hybrid internally)
-
-#### 8.1 Architecture change: NL→Hybrid instead of NL→Cypher
-
-Current flow:
-
-```
-NL question → routeNaturalToCypher() [regex intent detection → hardcoded Cypher template] → graph
-```
-
-New flow:
-
-```
-NL question ─┬─► Retriever 1: Vector similarity (Qdrant) ─┐
- ├─► Retriever 2: BM25-Plus (Memgraph text_search) ─► RRF fusion ─► ranked results
- └─► Retriever 3: Graph traversal from top BM25+Vec results ───────┘
-```
-
-Reserve raw Cypher for **explicit structural queries only** (`language: 'cypher'`).
-
-#### 8.2 New module: `src/graph/hybrid-retriever.ts`
-
-```typescript
-// src/graph/hybrid-retriever.ts
-export interface RetrievalOptions {
- query: string;
- projectId: string;
- limit?: number; // default: 10
- types?: string[]; // filter by node type
- mode?: "vector" | "bm25" | "graph" | "hybrid"; // default: 'hybrid'
- rrfK?: number; // RRF constant k (default: 60)
-}
-
-export interface RetrievalResult {
- nodeId: string;
- name: string;
- filePath: string;
- type: string;
- rrfScore: number;
- scores: { vector?: number; bm25?: number; graph?: number };
-}
-
-export class HybridRetriever {
- async retrieve(opts: RetrievalOptions): Promise;
- private async vectorSearch(
- query: string,
- opts: RetrievalOptions,
- ): Promise;
- private async bm25Search(
- query: string,
- opts: RetrievalOptions,
- ): Promise;
- private async graphExpansion(
- seedIds: string[],
- opts: RetrievalOptions,
- ): Promise;
- private fusionRRF(lists: RankedNode[][], k: number): RetrievalResult[];
-}
-```
-
-#### 8.3 RRF fusion formula
-
-$$\text{score}(d) = \sum_{i=1}^{N} \frac{1}{k + \text{rank}_i(d)}$$
-
-where:
-
-- $k = 60$ (standard constant — minimises sensitivity to high-rank outliers)
-- $\text{rank}_i(d)$ = 1-based rank of document $d$ in list $i$
-- Documents not present in a list get $\text{rank}_i(d) = \infty$ (contribute 0)
-- $N$ = number of retrievers (3: vector, BM25, graph)
-
-```typescript
-function fusionRRF(lists: RankedNode[][], k = 60): RetrievalResult[] {
- const scores: Map = new Map();
- for (const list of lists) {
- list.forEach((node, i) => {
- const prev = scores.get(node.id) ?? 0;
- scores.set(node.id, prev + 1 / (k + i + 1));
- });
- }
- return [...scores.entries()]
- .sort(([,a], [,b]) => b - a)
- .map(([id, rrfScore]) => ({ nodeId: id, rrfScore, ... }));
-}
-```
-
-#### 8.4 BM25-Plus index setup
-
-Use **BM25-Plus** (not standard BM25). BM25-Plus adds a lower-bound $\delta$ to term-frequency, preventing long documents from being disproportionately penalised. Critical for code where symbol frequency varies wildly.
-
-$$\text{BM25-Plus}(q,d) = \sum_{t \in q} \text{IDF}(t) \cdot \left(\delta + \frac{f(t,d) \cdot (k_1+1)}{f(t,d) + k_1(1-b+b \cdot |d|/\text{avgdl})}\right)$$
-
-Recommended: $k_1=1.2$, $b=0.75$, $\delta=0.25$.
-
-**Fields indexed** (Memgraph `text_search` module):
-
-```cypher
-// Create full-text index (Memgraph 2.x)
-CALL text_search.create_index('symbol_index', 'FUNCTION|CLASS|FILE',
- ['name', 'summary', 'path'], {analyzer: 'standard'});
-
-// Query
-CALL text_search.search('symbol_index', $queryText)
-YIELD node, score
-WHERE node.projectId = $pid
-RETURN node, score ORDER BY score DESC LIMIT $k
-```
-
-- `name` — exact symbol name (boost ×3 — highest precision signal)
-- `summary` — Phase 1.4 Meta-RAG-generated NL summary (boost ×2)
-- `path` — file path segments (boost ×1 — catches "find all files in tools/")
-- Raw `code` is **NOT indexed in BM25** — too large and noisy; `summary` replaces it
-
-#### 8.5 Drop-in swap in `tool-handlers.ts`
-
-The existing `routeNaturalToCypher()` function (marked `TODO: replace with hybrid retriever in Phase 8`) is replaced:
-
-```typescript
-// Before (Phase 8 not yet applied):
-const result = await routeNaturalToCypher(args.query, client, projectId);
-
-// After (Phase 8 applied):
-const retriever = new HybridRetriever(client, qdrant, embedding);
-const results = await retriever.retrieve({
- query: args.query,
- projectId,
- limit: args.limit ?? 10,
-});
-```
-
-All tools that called `routeNaturalToCypher` benefit automatically: `graph_query`, `find_pattern`, `arch_validate` natural mode.
-
-**Implementation status (2026-02-21)**:
-
-- ✅ Added `src/graph/hybrid-retriever.ts` with hybrid retrieval pipeline: vector retrieval, BM25-style lexical retrieval, graph expansion, and RRF fusion.
-- ✅ Integrated `HybridRetriever` into `src/tools/tool-handlers.ts` for `graph_query` when `language: 'natural'` in both `local` and `hybrid` modes.
-- ✅ Removed legacy regex `routeNaturalToCypher()` path from natural query handling and retained direct Memgraph passthrough for `language: 'cypher'`.
-- ✅ Added temporal filtering over hybrid retrieval rows for `asOf` in natural mode.
-- ✅ Added optional Memgraph `text_search` BM25 path (`symbol_index`) in `HybridRetriever` with automatic fallback to in-memory lexical scoring when full-text index/module is unavailable.
-- ✅ Smoke-validated `graph_query` over MCP session flow (`initialize` + `mcp-session-id` + `graph_set_workspace`) on fresh build.
-- ✅ **Production hardening**: Added `private _bm25Mode` field + `bm25Mode` getter to `HybridRetriever` — every `bm25Search` call now records whether it ran via native Memgraph text_search or lexical fallback.
-- ✅ Added `ensureBM25Index()` to `HybridRetriever`; called during `graph_rebuild` full-mode pipeline — `symbol_index` is auto-created on first full rebuild if not present.
-- ✅ `graph_health` now exposes `retrieval.bm25IndexExists` and `retrieval.mode` fields.
-- ℹ️ BM25 now attempts Memgraph `text_search` first and falls back to in-memory lexical scoring (name/path/summary token matching) if the index/module is unavailable.
-
----
-
-### Phase 9 — Multi-Language Support
-
-**Goal**: Server works identically for Python, Go, Rust, and Java projects.
-**Estimated effort**: 3–4 weeks
-**Depends on**: Phase 2 (SCIP IDs already language-agnostic encoding), Phase 8 (hybrid retriever is language-agnostic)
-**Unblocks**: nothing — expands user base while keeping all existing tools intact
-**Research backing**: Tree-sitter is the industry standard (used by GitHub Linguist, Neovim, VS Code syntax engine)
-**Acceptance criteria**:
-
-- `graph_rebuild` on a Python project creates FILE/FUNCTION/CLASS nodes with correct SCIP IDs
-- `code_explain` works on a Python function: correct callers/callees
-- All existing TypeScript tests still pass after parser refactor
-- Language detection works by file extension
-- Fallback: unknown languages get FILE nodes only (no function/class breakdown)
-
-#### 9.1 Parser abstraction interface
-
-**File**: `src/parsers/parser-interface.ts`
-
-```typescript
-// src/parsers/parser-interface.ts
-export interface ParsedSymbol {
- type: "function" | "class" | "method" | "variable" | "interface" | "import";
- name: string;
- startLine: number;
- endLine: number;
- kind?: string; // 'async', 'exported', 'abstract', 'interface', etc.
- scopePath?: string; // parent class/namespace for SCIP ID generation
- calls?: string[]; // direct call references within this symbol
- imports?: string[]; // modules imported (for FILE-level symbols)
-}
-
-export interface ParseResult {
- file: string;
- language: string;
- symbols: ParsedSymbol[];
-}
-
-export interface LanguageParser {
- readonly language: string; // 'typescript' | 'python' | 'go' | 'rust'
- readonly extensions: string[]; // ['.ts', '.tsx']
- parse(filePath: string, content: string): Promise;
-}
-```
-
-#### 9.2 Parser registry
-
-**File**: `src/parsers/parser-registry.ts`
-
-```typescript
-export class ParserRegistry {
- private parsers: Map = new Map();
-
- register(parser: LanguageParser): void {
- for (const ext of parser.extensions) this.parsers.set(ext, parser);
- }
-
- async parse(filePath: string, content: string): Promise {
- const ext = path.extname(filePath).toLowerCase();
- const parser = this.parsers.get(ext);
- if (!parser) return null; // returns FILE node only from builder
- return parser.parse(filePath, content);
- }
-}
-
-// Registration in GraphBuilder constructor:
-registry.register(new TypeScriptParser()); // Tree-sitter TypeScript
-registry.register(new PythonParser());
-registry.register(new GoParser());
-registry.register(new RustParser());
-```
-
-#### 9.3 Language-to-import mapping
-
-| Language | Import construct | IMPORT edge source |
-| ---------- | --------------------------------- | -------------------------------------------- |
-| TypeScript | `import { X } from 'mod'` | Both named and default imports |
-| Python | `import mod`, `from mod import X` | Module-level; `from . import X` for relative |
-| Go | `import "pkg/path"` | Package path string |
-| Rust | `use crate::module::Symbol` | `use` statement path segments |
-| Java | `import com.example.Class` | Fully-qualified class references |
-
-All produce `(file)-[:IMPORTS]->(dep)` edges where `dep` may be an external node (no source in graph).
-
-#### 9.4 Tree-sitter setup
-
-```bash
-npm install --save node-tree-sitter tree-sitter-typescript tree-sitter-python tree-sitter-go tree-sitter-rust
-```
-
-```typescript
-// src/parsers/tree-sitter-base.ts
-import Parser from "tree-sitter";
-import TS from "tree-sitter-typescript";
-
-export abstract class TreeSitterParser implements LanguageParser {
- protected parser: Parser;
- constructor(language: Parser.Language) {
- this.parser = new Parser();
- this.parser.setLanguage(language);
- }
- async parse(filePath: string, content: string): Promise {
- const tree = this.parser.parse(content);
- return this.walkTree(filePath, tree);
- }
- protected abstract walkTree(filePath: string, tree: Parser.Tree): ParseResult;
-}
-```
-
-#### 9.5 Migration from current regex TypeScript parser
-
-The current `src/parsers/typescript-parser.ts` uses regex patterns. Migration is non-breaking:
-
-1. New `src/parsers/tree-sitter-typescript-parser.ts` implements `LanguageParser` using Tree-sitter
-2. Both parsers coexist during transition, controlled by `CODE_GRAPH_USE_TREE_SITTER=true` env var
-3. After verification (all existing tests pass), old regex parser is removed
-4. `src/parsers/typescript-parser.ts` becomes a re-export shim for backwards compat
-
-**Implementation status (2026-02-21)**:
-
-- ✅ Added parser abstraction interfaces in `src/parsers/parser-interface.ts`.
-- ✅ Added parser registry in `src/parsers/parser-registry.ts` with extension-based parser dispatch.
-- ✅ Added multi-language parser scaffolds in `src/parsers/regex-language-parsers.ts` for Python (`.py`), Go (`.go`), Rust (`.rs`), and Java (`.java`).
-- ✅ Integrated multi-language file discovery and parser registry into `src/graph/orchestrator.ts` while keeping TypeScript parsing on the existing parser path.
-- ✅ Added non-breaking fallback behavior: unknown extensions (or missing parser matches) still index FILE-level nodes.
-- ✅ **Tree-sitter AST parsers** (2026-02-22): Added `src/parsers/tree-sitter-parser.ts` with native tree-sitter parsers for Python, Go, Rust, and Java using ESM→CJS bridge (`createRequire`). Added `tree-sitter` + language grammars as `optionalDependencies` in `package.json` — native bindings compile automatically in Docker (node:24-alpine + python3/make/g++) and gracefully skip in environments without build tools. `orchestrator.ts` registers AST-accurate tree-sitter parsers preferentially and logs which languages use AST vs regex fallback at startup.
-- ✅ **Tree-sitter TypeScript / TSX** (2026-02-22): Added `src/parsers/tree-sitter-typescript-parser.ts` with `TreeSitterTypeScriptParser` (`.ts`) and `TreeSitterTSXParser` (`.tsx`). Extracts `function_declaration`, arrow-function `const` assignments, `method_definition` (with `scopePath` → correct SCIP method IDs), `class_declaration`, `abstract_class_declaration`, `interface_declaration`, `type_alias_declaration`, `enum_declaration`, and `import_statement`. Gated by `CODE_GRAPH_USE_TREE_SITTER=true` env var; falls back to regex parser otherwise. `adaptLanguageParseResult()` updated to preserve `scopePath` and `kind` from generic `ParsedSymbol`, enabling accurate SCIP IDs for all languages. Added `tree-sitter-typescript` to `optionalDependencies`.
-
----
-
-### Phase 10 — File Watch / Incremental Push
-
-**Goal**: Graph stays current automatically. Agents never need to manually trigger rebuilds.
-**Estimated effort**: 1 week
-**Depends on**: Phase 2 (GRAPH_TX for each incremental rebuild), Phase 9 (parsers are file-level so watcher can process one file at a time)
-**Unblocks**: nothing — enables all tools to reflect live filesystem state without manual intervention
-**Research backing**: LightRAG (incremental update algorithm as core design principle), chokidar (battle-tested Node.js file watcher)
-**Acceptance criteria**:
-
-- Saving a TypeScript file triggers a graph update within 1.5 s (500ms debounce + parse + MERGE)
-- `graph_health.pendingChanges` accurately reflects files queued but not yet processed
-- Watcher ignores `node_modules`, `dist`, `.git`, and paths in `CODE_GRAPH_IGNORE_PATTERNS`
-- Each incremental update creates a `GRAPH_TX` node (type = 'incremental') in Memgraph
-- SCIP O(changes): unchanged files touch zero graph writes
-
-#### 10.1 New module: `src/graph/watcher.ts`
-
-```typescript
-// src/graph/watcher.ts
-import chokidar from "chokidar";
-
-export interface WatcherOptions {
- workspaceRoot: string;
- projectId: string;
- debounceMs?: number; // default: 500
- ignorePatterns?: string[]; // added to built-in ignore list
-}
-
-export class FileWatcher {
- private watcher: chokidar.FSWatcher;
- private state: WatcherState = "idle";
- private pending: Set = new Set();
- private debounceTimer?: NodeJS.Timeout;
-
- constructor(
- private opts: WatcherOptions,
- private orchestrator: GraphOrchestrator,
- ) {}
-
- start(): void;
- stop(): void;
- get pendingChanges(): number {
- return this.pending.size;
- }
-}
-
-type WatcherState = "idle" | "detecting" | "debouncing" | "rebuilding";
-```
-
-#### 10.2 State machine
-
-```
- file change detected
-idle ──────────────────────────────────► detecting
- │
- start 500ms timer
- │
- more changes arrive │ reset timer
- ▼
- debouncing
- │
- timer fires (no new changes)
- │
- ▼
- rebuilding ◄─────────────────────────────────┐
- │ │
- incremental rebuild runs │
- (process pending file list only) │
- │ │
- rebuild complete → GRAPH_TX │
- │ │
- ├─ new files detected during rebuild? ───┘
- │
- ▼
- idle
-```
-
-#### 10.3 O(changes) incremental rebuild — SCIP principle
-
-Because all nodes use stable SCIP-style human-readable IDs, incremental updates are pure `MERGE` operations — only nodes whose source has changed are re-parsed:
-
-```typescript
-// FileWatcher triggers:
-await orchestrator.rebuildIncremental({
- projectId: opts.projectId,
- changedFiles: [...this.pending], // only modified/added/deleted files
-});
-this.pending.clear();
-```
-
-```cypher
-// Phase 2 MERGE pattern — reused for incremental
-// For each changed file, invalidate old version and create new:
-MATCH (old:FILE {path: $path, projectId: $pid, validTo: null})
-SET old.validTo = $now;
-
-MERGE (f:FILE {id: $scip_id, projectId: $pid})
-ON CREATE SET f.createdAt = $now
-SET f.validFrom = $now,
- f.validTo = null,
- f.path = $path,
- f.language = $lang;
-
-// Create GRAPH_TX for this incremental update
-CREATE (tx:GRAPH_TX {
- id: $txId,
- projectId: $pid,
- type: 'incremental',
- timestamp: $now,
- filesAffected:$changedFiles,
- nodeCount: $nodesWritten,
- durationMs: $elapsed
-})
-```
-
-Unchanged files: **zero graph writes**. For a 1000-file codebase where 2 files changed, exactly 2 files are re-parsed.
-
-#### 10.4 `graph_health` pendingChanges integration
-
-```typescript
-// In graph_health handler:
-const watcher = watcherRegistry.get(projectId);
-return {
- ...existingHealthFields,
- pendingChanges: watcher?.pendingChanges ?? 0,
- watcherState: watcher?.state ?? "not_started",
-};
-```
-
-#### 10.5 Startup integration in `graph_set_workspace`
-
-When `graph_set_workspace` is called in HTTP mode, a `FileWatcher` is created and started for the workspace root. In stdio mode, watcher is opt-in via `CODE_GRAPH_ENABLE_WATCHER=true` env var (default false — stdio sessions are typically short-lived).
-
-```typescript
-// In tool-handlers.ts, graph_set_workspace handler:
-if (
- process.env.MCP_TRANSPORT === "http" ||
- process.env.CODE_GRAPH_ENABLE_WATCHER === "true"
-) {
- const watcher = new FileWatcher(
- { workspaceRoot, projectId, debounceMs: 500 },
- orchestrator,
- );
- watcherRegistry.set(projectId, watcher);
- watcher.start();
-}
-```
-
-**Implementation status (2026-02-21)**:
-
-- ✅ Added `src/graph/watcher.ts` with debounce-based file event batching (`add`/`change`/`unlink`) and state machine (`idle`/`detecting`/`debouncing`/`rebuilding`).
-- ✅ Integrated session-scoped watcher lifecycle in `graph_set_workspace` (`src/tools/tool-handlers.ts`), with auto-start for HTTP transport and opt-in start for stdio via `CODE_GRAPH_ENABLE_WATCHER=true`.
-- ✅ Added `pendingChanges` and `watcherState` to `graph_set_workspace` response payload and `graph_health` output.
-- ✅ Added watcher-triggered incremental rebuild callback that records `GRAPH_TX` entries and updates rebuild metadata.
-- ✅ Added `chokidar` dependency for robust cross-platform file watching.
-- ✅ Added `BuildOptions.changedFiles` support in `src/graph/orchestrator.ts` and wired watcher callbacks to pass changed files, enabling O(changes) incremental processing for watcher-triggered rebuilds.
-
----
-
-## 4. New Tool Inventory (complete list)
-
-After all phases, the server will expose the following tools:
-
-### Existing (improved)
-
-| Tool | Changes |
-| --------------------- | ----------------------------------------------------------------------------------------- |
-| `graph_query` | Answer-first format; `mode: local\|global\|hybrid`; `asOf` param; hybrid retrieval for NL |
-| `code_explain` | Returns `semantic_slice` per dep; `summary` field; PPR scores |
-| `find_pattern` | Grouped violations with fix suggestions |
-| `arch_validate` | Multi-language; community-aware layer checks |
-| `arch_suggest` | Uses `context_pack` + community context internally |
-| `test_select` | Temporal-aware (affected since `validFrom` change) |
-| `test_categorize` | Multi-language test file detection |
-| `impact_analyze` | Integrates with coordination claims |
-| `test_run` | Token-efficient output; stores result as `TEST_RESULT` episode |
-| `progress_query` | Persistent (graph-backed), paginated, filterable |
-| `task_update` | Releases claims on completion; triggers learning extraction |
-| `feature_status` | Includes code coverage from graph |
-| `blocking_issues` | Includes claim conflicts |
-| `graph_rebuild` | Creates `GRAPH_TX` node; starts Leiden community detection |
-| `graph_set_workspace` | Starts file watcher (Phase 10) |
-| `graph_health` | Adds `pendingChanges` and `recentEvents` |
-
-### New Phase 2 — Bi-Temporal Model
-
-_(No new tools — changes the graph schema. All existing query tools gain `asOf` param.)_
-
-### New Phase 3 — Episode Memory
-
-| Tool | Description |
-| ---------------- | ------------------------------------------------------------- |
-| `episode_add` | Persist an observation, decision, edit, test result, or error |
-| `episode_recall` | Hybrid search: vector + temporal + graph proximity |
-| `decision_query` | Find DECISION episodes affecting given files/symbols |
-| `reflect` | Synthesize recent episodes into REFLECTION + LEARNING nodes |
-
-### New Phase 4 — Coordination
-
-| Tool | Description |
-| ----------------------- | -------------------------------------------------------------- |
-| `agent_claim` | Claim a task/file with intent (temporal invalidation, not TTL) |
-| `agent_release` | Release a claim; stores outcome as EPISODE |
-| `agent_status` | Active claims + recent episodes for an agent |
-| `coordination_overview` | Fleet view: who owns what, stale claims, conflicts |
-
-### New Phase 5 — Context Pack
-
-| Tool | Description |
-| -------------- | ------------------------------------------------------------------ |
-| `context_pack` | Single-call PPR-ranked full briefing: code + decisions + learnings |
-
-### New Phase 6 — Code Slicing
-
-| Tool | Description |
-| ---------------- | -------------------------------------------------- |
-| `semantic_slice` | Relevant code lines with PPR score + graph context |
-
-### New Phase 7 — Community Detection
-
-_(No new tools by default — `graph_query` gains `mode: global` param. Optional `community_list` tool.)_
-
-### New Phase 8 — Temporal Diff
-
-| Tool | Description |
-| ------------ | --------------------------------------------------------------- |
-| `diff_since` | What changed since a txId / gitCommit / agentId / ISO timestamp |
-
-**Total: ~34 tools** (14 existing improved + 10 new)
-
----
-
-## 5. Execution Sequence (Priority Order)
-
-Ordered by agent ROI × implementation effort ratio:
-
-```
-Priority 1 — Foundation (must come first; everything else builds on these):
- Phase 1: Response quality + context budget model
- Phase 2: Bi-temporal model (validFrom/validTo on all nodes)
-
-Priority 2 — Core agent memory (unblocks persistent workflows):
- Phase 3: Episode-based memory (EPISODE, DECISION, LEARNING, REFLECTION nodes)
- Phase 4: Agent coordination (CLAIM nodes with temporal invalidation)
-
-Priority 3 — The flagship feature (justifies the whole project):
- Phase 5: context_pack with PPR-ranked retrieval
-
-Priority 4 — Quality + completeness:
- Phase 6: semantic_slice (actual code lines, not paths)
- Phase 7: Community detection (Leiden + summaries, global query mode)
- Phase 8: Hybrid retrieval (replaces NL→Cypher for most queries)
-
-Priority 5 — Platform breadth:
- Phase 9: Multi-language parsers (Tree-sitter)
- Phase 10: File watcher (incremental push)
-```
-
----
-
-## 6. Data Model Summary (Graph additions)
-
-```
-New node types:
- EPISODE — atomic agent interaction record (observation/edit/decision/etc.)
- REFLECTION — synthesized higher-level insight from multiple episodes
- LEARNING — durable extracted pattern linked to code nodes
- CLAIM — agent ownership of task/file (temporal invalidation)
- COMMUNITY — Leiden cluster of tightly-coupled files/modules
- GRAPH_TX — transaction record for each rebuild/file-change
-
-New relationship types:
- (EPISODE)-[:INVOLVES]->(FILE|FUNCTION|CLASS)
- (EPISODE)-[:NEXT_EPISODE]->(EPISODE)
- (REFLECTION)-[:DERIVED_FROM]->(EPISODE)
- (LEARNING)-[:APPLIES_TO]->(FILE|FUNCTION)
- (CLAIM)-[:TARGETS]->(TASK|FILE|FEATURE)
- (FILE)-[:BELONGS_TO]->(COMMUNITY)
- (GRAPH_TX)-[:AFFECTS]->(FILE)
-
-Modified existing nodes (bi-temporal fields added to ALL):
- FILE, FUNCTION, CLASS, IMPORT:
- +validFrom: timestamp
- +validTo: timestamp | null
- +createdAt: timestamp
- +txId: string
-```
-
----
-
-## 7. Key Design Rules
-
-1. **Answer-first**: Every tool response starts with a `summary` field. Agents read that; full `data` is optional.
-2. **Fail with hints**: Errors always include a `hint` field with a concrete next action.
-3. **No TTLs — temporal invalidation**: Claims are invalidated by code change events (`validTo` set), not by timer. This makes coordination reliable across long-running agents.
-4. **Graph is source of truth**: All state (episodes, decisions, claims, learnings) lives in Memgraph, not RAM. The progress engine is migrated to be fully graph-backed.
-5. **Profiles everywhere**: All tools accept `profile: 'compact' | 'balanced' | 'debug'`. Default is `compact`.
-6. **One workspace = one projectId**: All nodes are scoped. Multi-project support is automatic.
-7. **No secrets in the graph**: Code structure only. Episodes/decisions containing secrets must be flagged as `sensitive: true` and excluded from default query results.
-8. **Nothing is deleted — only superseded**: The bi-temporal model means old node versions are preserved with `validTo` set. Historical queries are always available.
-9. **PPR over iterative retrieval**: Use Personalized PageRank for context gathering. Never chain 5+ graph lookups when PPR can do it in one traversal.
-10. **Self-improving**: As agents complete tasks, `reflect()` extracts learnings that improve future `context_pack` quality.
-11. **Structure-aware chunking only**: Code is never sliced mid-function or mid-class. Every indexing unit (for BM25, vectors, Meta-RAG summaries, and semantic_slice) is a complete AST syntactic unit — function body, class definition, or import block. Partial chunks are a correctness bug, not a compression strategy.
-12. **SCIP-style human-readable IDs**: All FUNCTION, CLASS, and FILE nodes use `{relativePath}::{ClassName}::{method}` identifiers. UUIDs are never used as primary graph node keys. This enables O(changes) incremental indexing via `MERGE` and makes Cypher queries self-documenting.
-
----
-
-## 8. File Structure (target)
-
-```
-src/
- engines/
- architecture-engine.ts (existing)
- coordination-engine.ts ← NEW Phase 4
- episode-engine.ts ← NEW Phase 3
- migration-engine.ts (existing)
- progress-engine.ts (existing → migrate to graph-backed)
- test-engine.ts (existing)
- graph/
- builder.ts (existing → add validFrom/validTo/txId to all nodes)
- cache.ts (existing)
- client.ts (existing)
- community-detector.ts ← NEW Phase 7 (Leiden via Memgraph algorithm)
- hybrid-retriever.ts ← NEW Phase 8 (vector + BM25 + PPR fusion)
- index.ts (existing)
- orchestrator.ts (existing → add buildFile, GRAPH_TX creation)
- ppr.ts ← NEW Phase 5 (Personalized PageRank wrapper)
- types.ts (extend with temporal + episode types)
- watcher.ts ← NEW Phase 10
- parsers/
- parser-interface.ts ← NEW Phase 9
- typescript-parser.ts (existing → migrate to Tree-sitter)
- treesitter-parser.ts ← NEW Phase 9 (all languages)
- response/
- budget.ts ← NEW Phase 1 (ContextBudget allocation)
- shaper.ts ← NEW Phase 1 (answer-first formatter)
- tools/
- context-pack.ts ← NEW Phase 5
- coordination-tools.ts ← NEW Phase 4
- episode-tools.ts ← NEW Phase 3
- semantic-slice.ts ← NEW Phase 6
- tool-handlers.ts (existing, extend)
- vector-tools.ts (existing)
-```
-
----
-
-## 9. Success Metrics
-
-| Metric | Current | Target | Phase |
-| ------------------------------------------------ | ------------------------ | --------------------------------- | ----- |
-| Avg tokens per tool call (response) | ~800 | <300 (compact profile) | 1 |
-| Tool calls needed to start a task | 5–8 | 1 (`context_pack`) | 5 |
-| Agent memory persistence across restarts | None | Full (EPISODE nodes) | 3 |
-| Cross-agent state conflicts | Undetected | 0 (claim + temporal invalidation) | 4 |
-| Historical query support ("what was true at T?") | None | Full (`asOf` on all queries) | 2 |
-| Multi-hop QA accuracy (NL queries) | ~60% | >80% (PPR-based retrieval) | 5 |
-| Languages supported | 1 (TypeScript) | 4 (TS, Python, Go, Rust) | 9 |
-| Graph staleness after file change | Until next rebuild | <5 seconds (watcher) | 10 |
-| Context pack token efficiency vs. 8 calls | N/A — tool doesn't exist | ≥10x reduction | 5 |
diff --git a/docs/CLAUDE_INTEGRATION.md b/docs/CLAUDE_INTEGRATION.md
index c6c8401..8f6d69c 100644
--- a/docs/CLAUDE_INTEGRATION.md
+++ b/docs/CLAUDE_INTEGRATION.md
@@ -34,7 +34,7 @@ Edit `~/.claude_desktop_config.json`:
```json
{
"mcpServers": {
- "lxrag": {
+ "lxdig": {
"command": "node",
"args": ["/home/alex_rod/code-graph-server/dist/server.js"],
"env": {
@@ -46,7 +46,7 @@ Edit `~/.claude_desktop_config.json`:
}
}
},
- "systemPrompt": "You are analyzing code using the lxRAG MCP server.\n\n## CRITICAL RULES (DO NOT BREAK)\n\n1. NEVER read files or use file operations\n2. NEVER use grep or search patterns\n3. ALWAYS use MCP tools for code intelligence\n4. Call graph_set_workspace on first query\n5. Call graph_health every 5 messages to re-anchor\n\n## Tool Mapping\n\n| Question | Tool |\n| --- | --- |\n| \"Find X\" | graph_query('find X') |\n| \"How does X work?\" | code_explain('X') |\n| \"What breaks?\" | impact_analyze(changedFiles) |\n| \"Which tests?\" | test_select(changedFiles) |\n| \"Architecture?\" | arch_validate() or arch_suggest() |\n| \"Search for X\" | semantic_search('X') |\n| \"Similar code?\" | find_similar_code('X') |\n| \"Patterns?\" | find_pattern('X') |\n| \"Remember this\" | episode_add(type, content, agentId) |\n| \"Multi-agent safe?\" | agent_claim/release |\n\n## Session Flow\n\n1. graph_set_workspace(workspaceRoot, projectId)\n2. graph_health() — verify ready\n3. Query with MCP tools\n4. Every 5 messages: graph_health() to re-anchor\n\n## Forbidden Patterns\n\n❌ \"Let me read src/file.ts\"\n→ ✅ \"I'll use code_explain('SymbolName')\"\n\n❌ \"I'll search with grep for...\"\n→ ✅ \"I'll use graph_query to find...\"\n\n❌ \"Based on the file structure...\"\n→ ✅ \"Let me query the graph structure...\"\n\n## Token Efficiency (Long Conversations)\n\n- Use `profile: 'compact'` for token-light responses\n- Use `semantic_slice` for code ranges (not full files)\n- Use `context_pack` for multi-file context under budget"
+ "systemPrompt": "You are analyzing code using the lxDIG MCP server.\n\n## CRITICAL RULES (DO NOT BREAK)\n\n1. NEVER read files or use file operations\n2. NEVER use grep or search patterns\n3. ALWAYS use MCP tools for code intelligence\n4. Call graph_set_workspace on first query\n5. Call graph_health every 5 messages to re-anchor\n\n## Tool Mapping\n\n| Question | Tool |\n| --- | --- |\n| \"Find X\" | graph_query('find X') |\n| \"How does X work?\" | code_explain('X') |\n| \"What breaks?\" | impact_analyze(changedFiles) |\n| \"Which tests?\" | test_select(changedFiles) |\n| \"Architecture?\" | arch_validate() or arch_suggest() |\n| \"Search for X\" | semantic_search('X') |\n| \"Similar code?\" | find_similar_code('X') |\n| \"Patterns?\" | find_pattern('X') |\n| \"Remember this\" | episode_add(type, content, agentId) |\n| \"Multi-agent safe?\" | agent_claim/release |\n\n## Session Flow\n\n1. graph_set_workspace(workspaceRoot, projectId)\n2. graph_health() — verify ready\n3. Query with MCP tools\n4. Every 5 messages: graph_health() to re-anchor\n\n## Forbidden Patterns\n\n❌ \"Let me read src/file.ts\"\n→ ✅ \"I'll use code_explain('SymbolName')\"\n\n❌ \"I'll search with grep for...\"\n→ ✅ \"I'll use graph_query to find...\"\n\n❌ \"Based on the file structure...\"\n→ ✅ \"Let me query the graph structure...\"\n\n## Token Efficiency (Long Conversations)\n\n- Use `profile: 'compact'` for token-light responses\n- Use `semantic_slice` for code ranges (not full files)\n- Use `context_pack` for multi-file context under budget"
}
```
@@ -56,7 +56,7 @@ Create `.vscode/mcp.json`:
```json
{
"servers": {
- "lxrag": {
+ "lxdig": {
"type": "stdio",
"command": "node",
"args": ["/home/alex_rod/code-graph-server/dist/server.js"]
diff --git a/docs/CODE_COMMENT_STANDARD.md b/docs/CODE_COMMENT_STANDARD.md
new file mode 100644
index 0000000..4b0eeb0
--- /dev/null
+++ b/docs/CODE_COMMENT_STANDARD.md
@@ -0,0 +1,62 @@
+# Code Comment Standard
+
+This project uses a lightweight, standardized TSDoc format for comments.
+
+## 1) File Header (required for core modules)
+
+Use this at the top of important files:
+
+```ts
+/**
+ * @file
+ * @description
+ * @remarks
+ */
+```
+
+## 2) Public API Comments (required)
+
+Use TSDoc for exported functions, interfaces, and constants:
+
+```ts
+/**
+ * Short summary of behavior.
+ *
+ * @param input - Meaningful argument description.
+ * @returns What is returned and key shape guarantees.
+ */
+export function example(input: string): string { ... }
+```
+
+For exported constants:
+
+```ts
+/**
+ * What this constant represents and where it is used.
+ */
+export const value = ...;
+```
+
+## 3) Internal Helper Comments (optional, but recommended)
+
+Comment internal helpers when one of these is true:
+
+- Non-obvious behavior (normalization, fallback logic, truncation).
+- Performance-sensitive behavior.
+- Subtle edge-case handling.
+
+Prefer short comments that explain **why**, not line-by-line **what**.
+
+## 4) Style Rules
+
+- Keep comments accurate and behavior-focused.
+- Keep summaries concise (1–2 lines).
+- Use `@param` and `@returns` for callable APIs.
+- Avoid stale phase/temporary migration wording.
+- Update comments in the same change when behavior changes.
+
+## 5) Scope Guidance
+
+- Required: core runtime modules, registries, exported type contracts, tool definitions.
+- Recommended: tests with fixtures/path assumptions, complex orchestration flows.
+- Optional: obvious private one-liners.
diff --git a/docs/COMPLETE_ANALYSIS_SUMMARY.md b/docs/COMPLETE_ANALYSIS_SUMMARY.md
deleted file mode 100644
index 65f870e..0000000
--- a/docs/COMPLETE_ANALYSIS_SUMMARY.md
+++ /dev/null
@@ -1,607 +0,0 @@
-# Complete Analysis: lxRAG Tool Issues & Code-Visual Integration
-## Comprehensive Report with CLI Command Impact & Graph State Analysis
-
-**Date:** 2026-02-22
-**Analyst:** Claude Code + Deep-Dive Agent
-**Status:** ✅ Analysis Complete - Ready for Development
-
----
-
-## Executive Summary in One Page
-
-### The Problem
-Three lxRAG tools fail while code-visual's direct Memgraph queries succeed:
-
-| Tool | Expected | Actual | Status |
-|------|----------|--------|--------|
-| `graph_health()` | `{ totalNodes: 809 }` | `{ totalNodes: 0 }` | 🔴 Broken |
-| `feature_status(id)` | `{ feature: {...} }` | `"Feature not found"` | 🔴 Broken |
-| `progress_query()` | `{ items: [7] }` | `{ items: [] }` | 🔴 Broken |
-
-### The Root Cause
-**Index Synchronization Failure:**
-- Orchestrator builds graph and writes to Memgraph ✅
-- But doesn't sync populated index to shared index ❌
-- Tools read from shared index (empty) ❌
-- Memgraph is correct but tools don't see it ❌
-
-### The Impact
-- ✅ code-visual's direct Memgraph queries work perfectly
-- ❌ lxRAG operational tools are completely broken
-- ❌ Can't use lxRAG for dashboards, health checks, or task tracking
-- ❌ New projects will have same issue
-
-### The Fix (Priority Order)
-1. **Sync orchestrator index after build** (Tier 2 - 4-6 hours)
-2. **Make graph_health query-first** (Tier 1 - 2-3 hours)
-3. **Add engine reload on context switch** (Tier 2 - 1 hour)
-
----
-
-## Detailed Findings
-
-### Finding #1: The CLI Commands Were Diagnostic, Not Destructive
-
-The curl commands in lxrag-tool-issues.md were:
-```bash
-# All read-only (SELECT-only) operations
-MATCH (n) RETURN count(n) # ← Read, don't write
-MATCH ()-[r]->() RETURN count(r) # ← Read, don't write
-MATCH (f:FEATURE) RETURN f.id, f.name... # ← Read, don't write
-```
-
-**What they proved:**
-- Memgraph contains 809 nodes ✅
-- 1359 relationships exist ✅
-- Features like "code-visual:feature:phase-1" exist ✅
-- Tasks exist with correct statuses ✅
-
-**What they didn't change:**
-- Graph state (all read-only) ✅
-- Index state ✅
-- Engine initialization ✅
-
-**Conclusion:** These commands were validation queries that **confirmed the database is healthy**, not operations that corrupted the state.
-
----
-
-### Finding #2: Three Separate Index Systems Exist
-
-#### Index System 1: GraphOrchestrator.index (Temporary)
-```
-When: Created during graph_rebuild()
-What: Populated with parsed source code
- - Reads files from workspace
- - Parses into FILE, FUNCTION, CLASS, IMPORT nodes
- - Creates relationships
-Status: ✅ Correctly populated
-Use: Generates Cypher statements for Memgraph
-Then: ❌ DISCARDED - never synced to shared index
-```
-
-#### Index System 2: ToolContext.index (Shared, Global)
-```
-When: Initialized at server startup
-Initial state: EMPTY
-What: Should hold in-memory graph cache
-Status: ❌ Stays empty forever
-Used by: ALL engines
- - ProgressEngine (reads from here) → empty maps
- - EmbeddingEngine (reads from here) → no data
- - TestEngine (reads from here) → no data
- - ArchitectureEngine (reads from here) → no data
-Problem: Never populated from orchestrator
-Result: Tools always fail because index is empty
-```
-
-#### Index System 3: Memgraph Database (Source of Truth)
-```
-When: Updated by orchestrator's Cypher statements
-Status: ✅ Correct and current
-Content: 809 nodes, 1359 relationships
-Used by: Direct Memgraph queries (code-visual, CLI)
-Result: ✅ Always accurate
-Problem: ❌ Not synced back to shared index
-```
-
-**Visual Representation:**
-```
-graph_rebuild() called
- ↓
-Orchestrator.build()
- ├─ Parse files
- ├─ Create index (System 1) ✅ Populated
- ├─ Generate Cypher
- ├─ Execute to Memgraph (System 3) ✅ Updated
- ├─ MISSING: Sync to shared index (System 2)
- └─ Discard internal index
-
-Result:
-├─ System 1: Discarded
-├─ System 2: Still empty ❌
-├─ System 3: Up-to-date ✅
-
-Tools using System 2: BROKEN ❌
-Tools querying System 3: WORK ✅
-```
-
----
-
-### Finding #3: Each Tool Fails for the Same Reason
-
-#### Issue #1: `graph_health() → totalNodes: 0`
-
-**Code:**
-```typescript
-// From tool-handlers.ts:1782
-const stats = this.context.index.getStatistics();
-// ↑ Reads from System 2 (empty shared index)
-
-Result: { totalNodes: 0, totalRelationships: 0 }
-```
-
-**Why:** System 2 is empty because System 1 was never synced
-
----
-
-#### Issue #2: `feature_status() → "Feature not found"`
-
-**Code:**
-```typescript
-// From progress-engine.ts:76-91 (initialization)
-private loadFromGraph(): void {
- const featureNodes = this.index.getNodesByType("FEATURE");
- // ↑ Reads from System 2 (empty)
-
- // Populates this.features Map from empty result
- // this.features = {} (empty)
-}
-
-// From tool-handlers.ts:1500 (query)
-const status = this.progressEngine!.getFeatureStatus(featureId);
-// ↑ Looks in empty this.features Map
-// Returns null for ANY ID
-```
-
-**Why:** ProgressEngine initialized with System 2 (empty)
-
----
-
-#### Issue #3: `progress_query() → items: []`
-
-**Code:**
-```typescript
-// From progress-engine.ts:94-108 (initialization)
-private loadFromGraph(): void {
- const taskNodes = this.index.getNodesByType("TASK");
- // ↑ Reads from System 2 (empty)
-
- // Populates this.tasks Map from empty result
- // this.tasks = {} (empty)
-}
-
-// From progress-engine.ts:124-160 (query)
-query(type: "task", filter?: {...}): ProgressQueryResult {
- for (const task of this.tasks.values()) {
- // ↑ Iterates over empty Map
- // Returns no items
- }
-}
-```
-
-**Why:** ProgressEngine initialized with System 2 (empty)
-
----
-
-### Finding #4: code-visual Bypasses Broken Tools
-
-```
-code-visual frontend
- ↓
-memgraph-proxy.mjs
- ├─ Direct neo4j-driver connection
- ├─ Bolt protocol to Memgraph
- └─ Queries System 3 (Database) directly ✅
- ↓
- Result: Always accurate data
-
-lxRAG tools
- ├─ Read from System 2 (empty)
- └─ Return zeros/empty ❌
-```
-
-**Why code-visual works:**
-- It queries Memgraph directly (System 3) ✅
-- Doesn't use lxRAG tools ❌
-
-**Why lxRAG tools fail:**
-- They query empty shared index (System 2) ❌
-
----
-
-### Finding #5: The Expectation Mismatch
-
-**code-visual's Expectations vs Reality:**
-
-```
-What code-visual NEEDS:
-├─ Live graph visualization ✅ (works)
-├─ Accurate node/relationship counts ✅ (works via proxy)
-└─ Operational dashboards (features, tasks, progress)
- ├─ Wants: graph_health for readiness checks
- ├─ Wants: feature_status for feature tracking
- ├─ Wants: progress_query for task dashboards
- └─ Gets: Empty results ❌
-
-What code-visual GETS:
-├─ Direct Memgraph proxy ✅
-├─ CLI validation queries ✅
-└─ Broken lxRAG operational tools ❌
-```
-
-**The Gap:**
-- code-visual expected lxRAG tools to integrate seamlessly
-- lxRAG tools are broken due to empty index
-- No data corruption or wrong project - just empty
-
----
-
-## Impact Assessment
-
-### On lxRAG-MCP
-- ✅ Memgraph integration works
-- ✅ Graph building works (orchestrator)
-- ❌ Tools are unusable (read from empty index)
-- ❌ Progress tracking broken
-- ❌ Feature status broken
-- ❌ Health checks broken
-- ⚠ New projects will have same issue
-
-### On code-visual
-- ✅ Graph visualization works (direct proxy)
-- ✅ Can validate data with CLI queries
-- ❌ Can't use lxRAG tools
-- ❌ Can't trust operational dashboards
-- ❌ Would need workarounds
-
-### On Multi-Project Scenarios
-- ⚠ Project context switching doesn't reset engines
-- ⚠ Engines hold stale references to empty index
-- ❌ Would break if tools were working
-
----
-
-## Root Cause Analysis: Why Index Never Syncs
-
-### Code Flow That Fails:
-
-```typescript
-// In src/graph/orchestrator.ts
-async build(options): Promise {
- // 1. Create internal index
- this.index = new GraphIndexManager();
-
- // 2. Parse files and populate this.index
- const nodes = await parseFiles(workspace);
- for (const node of nodes) {
- this.index.addNode(...); // Internal index populated ✅
- }
-
- // 3. Generate and execute Cypher
- const statements = this.generateCypher(nodes);
- await memgraph.executeCypher(statements); // DB updated ✅
-
- // 4. MISSING: Sync to shared index
- // ❌ NO CODE HERE TO:
- // - Pass index to context
- // - Sync internal to shared
- // - Update ToolContext.index
- // - Trigger engine reloads
-
- // 5. Return build statistics
- return { success: true, ... };
- // Internal index falls out of scope and is garbage collected ❌
-}
-```
-
-### Why This Happened
-
-**Design assumption (wrong):**
-> "Tools will query Memgraph directly for operational data"
-
-**Actual implementation:**
-> "Tools query empty in-memory index"
-
-**Result:**
-> Index sync was never implemented, assuming it wasn't needed
-
----
-
-## The CLI Commands Role in Context
-
-### What Happened in code-visual Session:
-
-```
-1. user ran graph_rebuild for code-visual project
- ├─ Orchestrator.build() populated internal index
- ├─ Cypher statements executed to Memgraph ✅
- └─ Internal index discarded ❌
-
-2. user ran lxRAG tools
- ├─ graph_health() → read empty System 2 → returned zeros
- ├─ feature_status() → empty ProgressEngine.features → not found
- └─ progress_query() → empty ProgressEngine.tasks → empty list
-
-3. user ran diagnostic CLI queries
- ├─ Connected directly to Memgraph
- ├─ Saw 809 nodes, 1359 relationships ✅
- └─ Confirmed database is healthy ✅
-
-4. Conclusion
- ├─ "lxRAG tools are broken"
- ├─ "But Memgraph has correct data"
- └─ "Something is inconsistent"
-```
-
----
-
-## Solution Strategy
-
-### Why TIER 1 (Query-First) Alone Is Not Enough
-
-**Tier 1: Make graph_health query Memgraph instead of index**
-```typescript
-// Instead of:
-const stats = this.context.index.getStatistics();
-
-// Do this:
-const result = await memgraph.query("MATCH (n) RETURN count(n)");
-```
-
-**Pros:**
-- ✅ Quick (2-3 hours)
-- ✅ Fixes graph_health immediately
-- ✅ Low risk
-
-**Cons:**
-- ❌ Doesn't fix ProgressEngine (it still uses empty index)
-- ❌ feature_status still broken
-- ❌ progress_query still broken
-- ❌ Engines still have empty data
-
-### Why TIER 2 (Index Sync) Is Required
-
-**Tier 2: Sync orchestrator's populated index after build**
-```typescript
-// After orchestrator.build():
-this.context.index = orchestrator.index;
-// or:
-syncIndexes(orchestrator.index, this.context.index);
-```
-
-**Pros:**
-- ✅ Fixes all three issues at source
-- ✅ ProgressEngine gets data
-- ✅ TestEngine gets data
-- ✅ All engines work
-
-**Cons:**
-- ⏱ More complex (4-6 hours)
-- ⚠ Requires orchestrator changes
-
-**Best Approach:** Implement BOTH
-- Tier 2 primary fix (sync index)
-- Tier 1 enhancement (make graph_health query-first for authoritative counts)
-
----
-
-## Complete Implementation Checklist
-
-### Phase 1: Fix Index Synchronization (4-6 hours)
-
-- [ ] **Task 1.1:** Add index sync method to Orchestrator
- - File: `src/graph/orchestrator.ts`
- - Add: `syncToSharedIndex()` method
- - Call: After successful build
-
-- [ ] **Task 1.2:** Make graph_health query-first
- - File: `src/tools/tool-handlers.ts:graph_health()`
- - Change: Read from Memgraph instead of index
-
-- [ ] **Task 1.3:** Add reload() to ProgressEngine
- - File: `src/engines/progress-engine.ts`
- - Add: `reload(index, projectId)` method
-
-- [ ] **Task 1.4:** Add reload() to TestEngine
- - File: `src/engines/test-engine.ts`
- - Add: `reload(index, projectId)` method
-
-- [ ] **Task 1.5:** Call reload on context switch
- - File: `src/tools/tool-handlers.ts:setActiveProjectContext()`
- - Add: Engine reload calls
-
-### Phase 2: Validation (2-3 hours)
-
-- [ ] Test graph_health returns correct counts
-- [ ] Test feature_status resolves valid IDs
-- [ ] Test progress_query returns task list
-- [ ] Test against code-visual's known IDs
-- [ ] Validate CLI and tool counts match
-
-### Phase 3: Documentation (1 hour)
-
-- [ ] Update QUICK_REFERENCE.md with tool reliability notes
-- [ ] Add parity guarantee to tool documentation
-- [ ] Document index synchronization architecture
-
----
-
-## Validation Test Cases
-
-### Test 1: graph_health Accuracy
-
-**Setup:**
-```bash
-graph_set_workspace(projectId: "code-visual", workspaceRoot: "/path/to/code-visual", sourceDir: "src")
-graph_rebuild(mode: "full")
-```
-
-**Before Fix:**
-```json
-{
- "graphIndex": {
- "totalNodes": 0,
- "totalRelationships": 0,
- "indexedFiles": 0
- }
-}
-```
-
-**After Fix:**
-```json
-{
- "graphIndex": {
- "totalNodes": 809,
- "totalRelationships": 1359,
- "indexedFiles": 42
- }
-}
-```
-
-**Verify:** Compare with CLI query `MATCH (n {projectId: "code-visual"}) RETURN count(n)`
-
----
-
-### Test 2: feature_status Resolution
-
-**Setup:**
-```bash
-# Confirm feature exists via CLI:
-curl -s -X POST http://localhost:4001/query \
- -d '{"query":"MATCH (f:FEATURE {id:\"code-visual:feature:phase-1\"}) RETURN f"}'
-# → Returns feature node
-```
-
-**Before Fix:**
-```json
-{
- "success": false,
- "error": "Feature not found: code-visual:feature:phase-1"
-}
-```
-
-**After Fix:**
-```json
-{
- "success": true,
- "feature": {
- "id": "code-visual:feature:phase-1",
- "name": "Phase 1",
- "status": "in-progress"
- },
- "tasks": [...],
- "progressPercentage": 45
-}
-```
-
----
-
-### Test 3: progress_query Task Listing
-
-**Setup:**
-```bash
-# Confirm tasks exist via CLI:
-curl -s -X POST http://localhost:4001/query \
- -d '{"query":"MATCH (t:TASK {projectId:\"code-visual\"}) RETURN t"}'
-# → Returns 7 task nodes
-```
-
-**Before Fix:**
-```json
-{
- "items": [],
- "totalCount": 0,
- "completedCount": 0,
- "inProgressCount": 0,
- "blockedCount": 0
-}
-```
-
-**After Fix:**
-```json
-{
- "items": [
- { "id": "task-1", "name": "...", "status": "in-progress" },
- { "id": "task-2", "name": "...", "status": "completed" },
- ...
- ],
- "totalCount": 7,
- "completedCount": 3,
- "inProgressCount": 2,
- "blockedCount": 2
-}
-```
-
----
-
-## Timeline & Effort Estimate
-
-| Phase | Task | Effort | Duration |
-|-------|------|--------|----------|
-| 1 | Index sync implementation | Complex | 4-6 hours |
-| 1 | graph_health query-first | Medium | 1-2 hours |
-| 1 | Engine reload methods | Medium | 2-3 hours |
-| 2 | Validation testing | Simple | 2-3 hours |
-| 2 | CI/CD validation | Medium | 1 hour |
-| 3 | Documentation | Simple | 1 hour |
-| **Total** | All Phases | **Moderate** | **11-16 hours** |
-
----
-
-## Summary Table: Issues vs Root Causes vs Fixes
-
-| Issue | What Fails | Why | Fix | Tier |
-|-------|-----------|-----|-----|------|
-| graph_health zeros | Index read | System 2 empty | Query Memgraph + sync | 1+2 |
-| feature_status not found | ProgressEngine.features | System 2 empty | Sync index + reload | 2 |
-| progress_query empty | ProgressEngine.tasks | System 2 empty | Sync index + reload | 2 |
-
----
-
-## Key Takeaways
-
-1. **CLI commands were NOT destructive** - They were read-only validation
-2. **The database is healthy** - 809 nodes exist in Memgraph
-3. **The tools are broken** - They read from empty shared index
-4. **code-visual works around it** - Direct Memgraph bypass
-5. **The fix is synchronization** - Sync orchestrator index after build
-6. **No data corruption** - Just missing synchronization
-7. **This affects all new projects** - Same issue will recur
-
----
-
-## Next Steps
-
-1. Review this analysis with team
-2. Prioritize implementation (recommend: Tier 2 + Tier 1)
-3. Start with Task 1.1 (index sync in orchestrator)
-4. Test against code-visual's known data
-5. Document final architecture
-
----
-
-## Document Inventory
-
-This analysis is composed of:
-
-1. **This file:** `COMPLETE_ANALYSIS_SUMMARY.md` - Full context
-2. **Original issues:** `lxrag-tool-issues.md` - Session findings
-3. **Previous plan:** `ACTION_PLAN_LXRAG_TOOL_FIXES.md` - Project-scoping focus
-4. **Revised plan:** `REVISED_ACTION_PLAN_WITH_CLI_ANALYSIS.md` - Index sync focus
-5. **Deep-dive docs:**
- - `GRAPH_STATE_SUMMARY.md` - Executive summary
- - `GRAPH_STATE_ANALYSIS.md` - Technical deep dive
- - `GRAPH_STATE_FIXES.md` - All fix tiers
-
-**Recommendation:** Start with this file, then read REVISED_ACTION_PLAN_WITH_CLI_ANALYSIS.md for implementation details.
-
diff --git a/docs/COMPREHENSIVE_REVIEW_AND_REVISED_PLAN.md b/docs/COMPREHENSIVE_REVIEW_AND_REVISED_PLAN.md
deleted file mode 100644
index 4675c10..0000000
--- a/docs/COMPREHENSIVE_REVIEW_AND_REVISED_PLAN.md
+++ /dev/null
@@ -1,1235 +0,0 @@
-# Comprehensive lexRAG-MCP Review & Revised Action Plan
-## Full Data Pipeline Analysis + Integration Fixes
-
-**Analysis Date:** 2026-02-22
-**Review Scope:** Complete data flow from source code → Memgraph → Qdrant
-**Status:** 🔴 Multiple Critical Issues Found + Revised Plan Provided
-
----
-
-## PART 1: COMPREHENSIVE REVIEW FINDINGS
-
-### Executive Summary
-
-The lexRAG-MCP data pipeline consists of **3 interconnected systems** that are **not properly synchronized**:
-
-```
-┌──────────────┐ ┌─────────────┐ ┌──────────────┐
-│ Memgraph │ ←→ │ In-Memory │ ←→ │ Qdrant │
-│ (DB) │ │ Index │ │ (Vectors) │
-│ ✅ Updated │ │ ✗ Empty/Out │ │ ✗ Stale/ │
-│ ✓ Scoped │ │ of sync │ │ Unscoped │
-└──────────────┘ └─────────────┘ └──────────────┘
- Source of truth Cache layer Semantic search
-```
-
----
-
-## Key Findings from Deep Review
-
-### Finding #1: Three Separate Index Systems (Unsynced)
-
-**System 1: GraphOrchestrator.index (Temporary)**
-- Created during `graph_rebuild()`
-- Populated with ALL parsed code (FILE, FUNCTION, CLASS, etc.)
-- Used to generate Cypher statements
-- **Then: DISCARDED** (never synced back to shared index)
-
-**System 2: ToolContext.index (Shared, Global)**
-- Started empty at server startup
-- **Never populated** from Memgraph
-- **Never synced** from Orchestrator after builds
-- **Never cleared** when switching projects
-- Used by: ProgressEngine, TestEngine, EmbeddingEngine, ArchitectureEngine
-- **Status: EMPTY or STALE**
-
-**System 3: Memgraph Database (Source of Truth)**
-- Updated with Cypher from Orchestrator ✓
-- Contains accurate data (809 nodes in code-visual)
-- **Not synced back** to in-memory index
-- Direct queries work ✓
-
-**Impact**: Tools read from System 2 (empty) → return zeros/empty results ✗
-
----
-
-### Finding #2: Data Sync Matrix After graph_rebuild
-
-| Data Type | To Memgraph | To In-Memory | To Qdrant | Status |
-|-----------|---|---|---|---|
-| **Code Entities** (FILE, FUNCTION, CLASS, etc.) | ✓ YES | ✓ YES | ✗ NO | PARTIAL |
-| **Code Relationships** (CONTAINS, IMPORTS, etc.) | ✓ YES | ✓ YES | ✗ NO | PARTIAL |
-| **FEATURE Nodes** | ✓ YES (hardcoded 5) | ✗ NO | N/A | ⚠️ OVERWRITES |
-| **TASK Nodes** | ⚠️ API only | ✓ YES | N/A | ⚠️ OPTIONAL |
-| **DOCUMENT Nodes** | ⚠️ Optional | ✗ NO | ⚠️ Optional | ⚠️ OPTIONAL |
-| **Embeddings** | N/A | Generated from index | Lazy-loaded | ✗ NOT AUTO |
-| **Embedding Freshness** | N/A | embeddingsReady flag | Never reset | ✗ STALE |
-| **Test Cases** | ✗ NO (only TEST_SUITE) | ✗ NO | N/A | ✗ MISSING |
-
-**Key Problem**: Qdrant receives **zero data** from graph_rebuild; only generated on-demand (lazy)
-
----
-
-### Finding #3: Qdrant Integration Issues (CRITICAL)
-
-#### Issue 3.1: No Project Scoping
-```typescript
-// From embedding-engine.ts:50-70
-generateAllEmbeddings() {
- const functions = this.index.getNodesByType('FUNCTION');
- // Returns ALL functions across ALL projects
- // No projectId filtering
-}
-```
-
-**Impact**: All projects share same vector space
-- Project A search "authentication" finds functions from Project B
-- No multi-tenant isolation
-- **Severity**: CRITICAL
-
-#### Issue 3.2: Embeddings Never Auto-Generated
-```typescript
-// graph_rebuild() does NOT call generateAllEmbeddings()
-// Embeddings only generated lazily when:
-// - graph_semantic_search() called
-// - graph_code_search() called
-// - code_cluster() called
-```
-
-**Impact**:
-- First semantic search has latency spike
-- If Qdrant unavailable, no fallback
-- Stale embeddings after incremental builds
-- **Severity**: HIGH
-
-#### Issue 3.3: embeddingsReady Flag Never Reset
-```typescript
-private embeddingsReady = false; // Set once at startup
-
-// After incremental rebuild:
-// embeddingsReady still = true even though new nodes weren't embedded
-// New functions added but NOT in Qdrant
-```
-
-**Impact**:
-- Incremental rebuilds don't update Qdrant
-- New code is searchable via graph but NOT via semantic search
-- Inconsistent results
-- **Severity**: HIGH
-
-#### Issue 3.4: MVP-Quality Embeddings
-```typescript
-// 128-dimensional hash-based vectors (NOT semantic)
-// Deterministic but not meaningful
-// Not using real embeddings (OpenAI, HuggingFace)
-```
-
-**Impact**: Semantic search quality is limited
-- Better than nothing for MVP
-- Needs upgrade for production
-- **Severity**: MEDIUM
-
----
-
-### Finding #4: Progress Data Issues
-
-#### Issue 4.1: FEATURE Nodes Always Overwritten
-```typescript
-// orchestrator.ts:1015-1079
-// Every graph_rebuild recreates same 5 hardcoded features
-private seedProgressNodes(projectId: string): CypherStatement[] {
- const features = [
- { id: "phase-1", name: "Code Graph MVP", status: "completed" },
- { id: "phase-2", name: "Architecture Validation", ... },
- // ... 5 hardcoded features
- ];
-
- // Uses MERGE on id only
- query: `
- MERGE (f:FEATURE {id: $id})
- SET f.name = $name, f.status = $status // Always overwrites
- `
-}
-```
-
-**Problem**:
-- Every rebuild resets all feature statuses
-- User customizations lost
-- No merge with existing data
-
-**Severity**: HIGH
-
-#### Issue 4.2: TASK Nodes Not Persisted
-```typescript
-// Tasks created only via ProgressEngine.createTask()
-// persistTaskUpdate() is optional, requires Memgraph connected
-// If Memgraph not connected, tasks lost on restart
-```
-
-**Problem**:
-- Tasks not rebuilt from persistent storage
-- Can't survive server restart
-- Not synced during graph_rebuild
-
-**Severity**: MEDIUM
-
-#### Issue 4.3: Feature Data Only in Memory
-```typescript
-// ProgressEngine.getFeatureStatus() reads from this.features Map
-// Map populated at initialization from empty in-memory index
-// So features found from Memgraph are NOT in the Map
-```
-
-**Problem**: feature_status() always returns "not found"
-**Severity**: CRITICAL (caused original issue #2)
-
----
-
-### Finding #5: Index Initialization Problems
-
-#### Issue 5.1: In-Memory Index Starts Empty
-```typescript
-// src/mcp-server.ts:618
-const index = new GraphIndexManager();
-// Started empty, never populated from Memgraph
-```
-
-**Problem**:
-- Tools fail until first graph_rebuild
-- On server restart, all in-memory data lost
-- Engines have no data to work with
-
-**Severity**: HIGH
-
-#### Issue 5.2: No Cross-Project Cleanup
-```typescript
-// graph_set_workspace() does NOT:
-// - Clear in-memory index
-// - Reload engines with new data
-// - Reset embeddings
-```
-
-**Problem**:
-- Switching projects leaves old data in memory
-- Engines still reference old project's data
-- Cross-project contamination possible
-
-**Severity**: MEDIUM
-
----
-
-### Finding #6: Missing Data
-
-| Entity Type | Status | Location |
-|---|---|---|
-| Individual TEST_CASE nodes | ✗ NOT CREATED | Should be in builder |
-| Embedding generation trigger | ✗ MISSING | Should be in graph_rebuild |
-| Index reset on project switch | ✗ MISSING | Should be in setActiveProjectContext |
-| In-memory index load on startup | ✗ MISSING | Should be in initialization |
-| Embedding staleness detection | ✗ MISSING | Should be in ensureEmbeddings |
-| Progress data bidirectional sync | ✗ PARTIAL | Only optional Memgraph saves |
-
----
-
-## PART 2: REVISED COMPREHENSIVE ACTION PLAN
-
-### Priority Matrix
-
-```
-CRITICAL (Must fix immediately):
-├─ P0.1: Sync orchestrator index to shared index
-├─ P0.2: Fix Qdrant project scoping
-├─ P0.3: Fix FEATURE node overwrite
-└─ P0.4: Fix empty ProgressEngine maps
-
-HIGH (Fix within sprint):
-├─ P1.1: Reset embeddingsReady flag on rebuild
-├─ P1.2: Generate embeddings during full rebuild
-├─ P1.3: Load in-memory index from Memgraph on startup
-├─ P1.4: Add consistency check to graph_health
-└─ P1.5: Make task persistence mandatory
-
-MEDIUM (Next iteration):
-├─ P2.1: Add individual TEST_CASE nodes
-├─ P2.2: Enable doc embeddings by default
-└─ P2.3: Add state machine for sync states
-```
-
----
-
-## PHASE 1: CRITICAL FIXES (6-8 hours)
-
-### 1.1: Sync Orchestrator Index to Shared Index After Build
-
-**File**: `src/graph/orchestrator.ts`
-**Location**: End of `build()` method (around line 430)
-
-**Current Code**:
-```typescript
-async build(options: BuildOptions): Promise {
- // ... all build logic ...
- return result; // ← Index falls out of scope, discarded
-}
-```
-
-**Required Change**:
-```typescript
-async build(options: BuildOptions): Promise {
- // ... all build logic ...
-
- // NEW: Sync populated index to shared context
- if (this.sharedIndex) {
- console.log("[Orchestrator] Syncing internal index to shared index");
-
- // Sync all nodes
- const allNodes = this.index.getAllNodes ?
- this.index.getAllNodes() :
- Array.from(this.index.nodesByType.values()).flat();
-
- for (const node of allNodes) {
- try {
- this.sharedIndex.addNode(node.id, node.type, node.properties);
- } catch (e) {
- // Deduplication will skip existing nodes
- }
- }
-
- // Sync all relationships
- const allRels = this.index.getAllRelationships ?
- this.index.getAllRelationships() :
- Array.from(this.index.relationshipsByType.values()).flat();
-
- for (const rel of allRels) {
- try {
- this.sharedIndex.addRelationship(rel.id, rel.from, rel.to, rel.type, rel.properties);
- } catch (e) {
- // Deduplication will skip existing rels
- }
- }
-
- console.log(`[Orchestrator] Synced ${allNodes.length} nodes and ${allRels.length} relationships`);
- }
-
- return result;
-}
-```
-
-**Why This Fixes**:
-- ✅ In-memory index populated after build
-- ✅ ProgressEngine gets real data
-- ✅ TestEngine gets real data
-- ✅ graph_health returns accurate counts
-
----
-
-### 1.2: Fix Qdrant Project Scoping
-
-**File**: `src/vector/embedding-engine.ts`
-**Locations**: storeInQdrant() and search methods
-
-**Step 1.2.1: Add projectId to embedding payload**
-
-```typescript
-// In generateEmbedding() - around line 170
-private generateEmbedding(
- type: 'function' | 'class' | 'file',
- id: string,
- properties: Record,
- projectId?: string // NEW parameter
-): CodeEmbedding {
- // ... existing code ...
-
- // Extract projectId from scoped ID if not provided
- const scope = id.split(':')[0];
- const effectiveProjectId = projectId || scope;
-
- return {
- id,
- type,
- vector: generatedVector,
- name: properties.name || id,
- text: textContent,
- metadata: {
- ...properties,
- projectId: effectiveProjectId, // NEW
- fileName: properties.path,
- language: properties.language,
- },
- };
-}
-```
-
-**Step 1.2.2: Store projectId in Qdrant payload**
-
-```typescript
-// In storeInQdrant() - around line 165
-const point: VectorPoint = {
- id: embedding.id,
- vector: embedding.vector,
- payload: {
- name: embedding.name,
- text: embedding.text,
- projectId: embedding.metadata.projectId, // NEW
- metadata: embedding.metadata,
- },
-};
-```
-
-**Step 1.2.3: Filter by projectId in search**
-
-```typescript
-// In search method (used by semantic_search tool) - around line 250
-async search(
- collection: string,
- query: CodeEmbedding,
- limit: number = 10,
- projectId?: string // NEW parameter
-): Promise {
- // ... existing code ...
-
- // Build filter if projectId provided
- const filter = projectId ? {
- must: [{ key: 'payload.projectId', match: { value: projectId } }]
- } : undefined;
-
- const results = await this.qdrant.search({
- collection,
- vector: query.vector,
- limit,
- filter, // NEW
- });
-
- // ... rest of method ...
-}
-```
-
-**Why This Fixes**:
-- ✅ Each project's vectors isolated
-- ✅ No cross-project contamination
-- ✅ Semantic search respects project boundaries
-
----
-
-### 1.3: Fix FEATURE Node Overwrite
-
-**File**: `src/graph/orchestrator.ts`
-**Location**: seedProgressNodes() method (line 1015-1079)
-
-**Current Code**:
-```typescript
-query: `
- MERGE (f:FEATURE {id: $id})
- SET f.name = $name, f.status = $status, // Always overwrites
- ...
-`
-```
-
-**Required Change - Option A (Recommended): Use ON CREATE only**
-
-```typescript
-private seedProgressNodes(projectId: string): CypherStatement[] {
- const features = [
- { id: `${projectId}:feature:phase-1`, name: "Code Graph MVP", status: "completed" },
- { id: `${projectId}:feature:phase-2`, name: "Architecture Validation", ... },
- // ... rest of features ...
- ];
-
- const statements: CypherStatement[] = [];
-
- for (const feature of features) {
- statements.push({
- query: `
- MERGE (f:FEATURE {id: $id})
- ON CREATE SET
- f.name = $name,
- f.status = $status,
- f.priority = $priority,
- f.createdAt = $timestamp,
- f.projectId = $projectId
- ON MATCH DO NOTHING // <- NEW: Don't overwrite existing
- `,
- params: {
- id: feature.id,
- name: feature.name,
- status: feature.status,
- priority: feature.priority || 5,
- timestamp: Date.now(),
- projectId: projectId,
- },
- });
- }
-
- return statements;
-}
-```
-
-**Alternative Option B: Use timestamp-based merge**
-
-```typescript
-query: `
- MATCH (f:FEATURE {id: $id})
- WHERE f.createdAt IS NULL OR f.createdAt <= $templateTimestamp
- SET f.name = $name, f.status = $status, ...
- ON MATCH DO NOTHING
-`
-```
-
-**Why This Fixes**:
-- ✅ Existing features preserved
-- ✅ User customizations not lost
-- ✅ New feature templates only created once
-
----
-
-### 1.4: Fix Empty ProgressEngine Maps
-
-**File**: `src/engines/progress-engine.ts`
-**Location**: loadFromGraph() method + add reload() method
-
-**Current Code**:
-```typescript
-constructor(index: GraphIndexManager, memgraph?: MemgraphClient) {
- this.index = index;
- this.memgraph = memgraph;
- this.features = new Map();
- this.tasks = new Map();
- this.loadFromGraph(); // Loads from empty index
-}
-
-private loadFromGraph(): void {
- const featureNodes = this.index.getNodesByType("FEATURE"); // Empty!
- // Maps stay empty
-}
-```
-
-**Required Changes**:
-
-**Step 1.4.1: Add reload() method**
-
-```typescript
-reload(index: GraphIndexManager, projectId?: string): void {
- console.log(`[ProgressEngine] Reloading features and tasks (projectId=${projectId})`);
-
- this.features.clear();
- this.tasks.clear();
- this.index = index;
- this.loadFromGraph(projectId);
-}
-
-private loadFromGraph(projectId?: string): void {
- // Load FEATURE nodes
- const featureNodes = this.index.getNodesByType("FEATURE");
- for (const node of featureNodes) {
- // Filter by projectId if provided
- if (projectId && node.properties?.projectId !== projectId) continue;
-
- this.features.set(node.id, {
- id: node.id,
- name: node.properties.name,
- status: node.properties.status || "pending",
- description: node.properties.description,
- adrReference: node.properties.adrReference,
- startedAt: node.properties.startedAt,
- completedAt: node.properties.completedAt,
- implementingFiles: [],
- relatedTests: [],
- });
- }
-
- // Load TASK nodes
- const taskNodes = this.index.getNodesByType("TASK");
- for (const node of taskNodes) {
- // Filter by projectId if provided
- if (projectId && node.properties?.projectId !== projectId) continue;
-
- this.tasks.set(node.id, {
- id: node.id,
- name: node.properties.name,
- description: node.properties.description,
- status: node.properties.status || "pending",
- assignee: node.properties.assignee,
- featureId: node.properties.featureId,
- startedAt: node.properties.startedAt,
- dueDate: node.properties.dueDate,
- completedAt: node.properties.completedAt,
- blockedBy: node.properties.blockedBy || [],
- });
- }
-
- console.log(`[ProgressEngine] Loaded ${this.features.size} features and ${this.tasks.size} tasks`);
-}
-```
-
-**Step 1.4.2: Call reload on project context change**
-
-**File**: `src/tools/tool-handlers.ts`
-**Location**: setActiveProjectContext() method
-
-```typescript
-private setActiveProjectContext(context: ProjectContext): void {
- const sessionId = this.getCurrentSessionId();
- if (sessionId) {
- this.sessionProjectContexts.set(sessionId, context);
- } else {
- this.defaultActiveProjectContext = context;
- }
-
- // NEW: Reload engines with new context
- console.log(`[ToolHandlers] Project context changed to ${context.projectId}`);
-
- this.progressEngine?.reload(this.context.index, context.projectId);
- this.testEngine?.reload(this.context.index, context.projectId);
- if (this.archEngine) {
- this.archEngine.reload(this.context.index, context.projectId);
- }
-
- // Reset embedding flag so next semantic search regenerates
- this.embeddingsReady = false;
-}
-```
-
-**Why This Fixes**:
-- ✅ ProgressEngine loaded with actual data
-- ✅ feature_status() finds valid IDs
-- ✅ progress_query() returns task list
-- ✅ All three original issues fixed
-
----
-
-### 1.5: Make graph_health Query-First
-
-**File**: `src/tools/tool-handlers.ts`
-**Location**: graph_health() method (line 1778)
-
-**Current Code**:
-```typescript
-async graph_health(args: any): Promise {
- const stats = this.context.index.getStatistics();
- // Returns zeros because index is empty/stale
-
- return this.formatSuccess({
- graphIndex: {
- totalNodes: stats.totalNodes, // 0
- totalRelationships: stats.totalRelationships, // 0
- }
- });
-}
-```
-
-**Required Change**:
-
-```typescript
-async graph_health(args: any): Promise {
- const profile = args?.profile || "compact";
- const { projectId } = this.getActiveProjectContext();
-
- try {
- // Query from BOTH sources for comparison
- const indexStats = this.context.index.getStatistics();
-
- // Query Memgraph for authoritative counts
- const nodeCountResult = await this.context.memgraph.executeCypher(
- `MATCH (n {projectId: $projectId}) RETURN count(n) AS totalNodes`,
- { projectId }
- );
-
- const relCountResult = await this.context.memgraph.executeCypher(
- `MATCH (n1 {projectId: $projectId})-[r]->(n2 {projectId: $projectId})
- RETURN count(r) AS totalRels`,
- { projectId }
- );
-
- const memgraphNodeCount = nodeCountResult.data?.[0]?.totalNodes || 0;
- const memgraphRelCount = relCountResult.data?.[0]?.totalRels || 0;
-
- // Function/Class/File counts from index (still useful)
- const functionCount = this.context.index.getNodesByType("FUNCTION").length;
- const classCount = this.context.index.getNodesByType("CLASS").length;
- const fileCount = this.context.index.getNodesByType("FILE").length;
-
- // Get embedding stats
- const embeddingCount = this.embeddingEngine?.getAllEmbeddings().length || 0;
- const indexedSymbols = functionCount + classCount + fileCount;
- const embeddingCoverage =
- indexedSymbols > 0
- ? Number((embeddingCount / indexedSymbols).toFixed(3))
- : 0;
-
- // Check if there's drift
- const hasIndexDrift = indexStats.totalNodes !== memgraphNodeCount;
- const hasEmbeddingDrift = embeddingCount < indexedSymbols;
-
- return this.formatSuccess({
- status: "ok",
- projectId,
- memgraphConnected: this.context.memgraph.isConnected(),
- qdrantConnected: this.qdrant?.isConnected() || false,
- graphIndex: {
- // Use Memgraph as source of truth
- totalNodes: memgraphNodeCount,
- totalRelationships: memgraphRelCount,
- indexedFiles: fileCount,
- indexedFunctions: functionCount,
- indexedClasses: classCount,
- },
- indexHealth: {
- driftDetected: hasIndexDrift,
- memgraphNodes: memgraphNodeCount,
- cachedNodes: indexStats.totalNodes,
- recommendation: hasIndexDrift ?
- "Run graph_rebuild to synchronize index" :
- "Index synchronized"
- },
- embeddings: {
- ready: this.embeddingsReady,
- generated: embeddingCount,
- coverage: embeddingCoverage,
- driftDetected: hasEmbeddingDrift,
- recommendation: hasEmbeddingDrift ?
- "Run semantic search to regenerate embeddings" :
- "Embeddings up-to-date"
- },
- lastRebuild: {
- timestamp: this.lastGraphRebuildAt,
- mode: this.lastGraphRebuildMode,
- }
- }, profile);
- } catch (error) {
- return this.errorEnvelope("GRAPH_HEALTH_FAILED", String(error), true);
- }
-}
-```
-
-**Why This Fixes**:
-- ✅ Returns accurate node counts from Memgraph
-- ✅ Detects index drift
-- ✅ Provides actionable recommendations
-- ✅ Solves original issue #1
-
----
-
-## PHASE 2: HIGH PRIORITY FIXES (3-4 hours)
-
-### 2.1: Reset embeddingsReady Flag on Rebuild
-
-**File**: `src/tools/tool-handlers.ts`
-**Location**: runWatcherIncrementalRebuild() and graph_rebuild()
-
-```typescript
-// In graph_rebuild():
-async graph_rebuild(args: any): Promise {
- try {
- // ... existing code ...
-
- // After successful orchestrator.build():
- const result = await this.orchestrator.build({...});
-
- // NEW: Reset embedding flag
- this.embeddingsReady = false;
- console.log("[ToolHandlers] Cleared embedding cache due to rebuild");
-
- // ... rest of method ...
- }
-}
-
-// In runWatcherIncrementalRebuild():
-private async runWatcherIncrementalRebuild(context): Promise {
- // ... existing code ...
-
- // After successful rebuild:
- this.embeddingsReady = false; // NEW
-
- // ... rest of method ...
-}
-```
-
-**Why This Fixes**:
-- ✅ Incremental builds update Qdrant
-- ✅ New code appears in semantic search
-- ✅ graph_health.embeddings.driftDetected is accurate
-
----
-
-### 2.2: Generate Embeddings During Full Rebuild
-
-**File**: `src/tools/tool-handlers.ts`
-**Location**: After orchestrator.build() in graph_rebuild()
-
-```typescript
-// After graph_rebuild successfully completes:
-
-if (mode === "full" && this.context.memgraph.isConnected()) {
- // NEW: Trigger embedding generation for full builds
- console.log("[ToolHandlers] Generating embeddings for full rebuild");
-
- try {
- await this.ensureEmbeddings();
- console.log("[ToolHandlers] Embeddings generated successfully");
- } catch (error) {
- console.warn("[ToolHandlers] Embedding generation failed:", error);
- // Don't fail the whole rebuild if embeddings fail
- }
-}
-```
-
-**Why This Fixes**:
-- ✅ No latency on first semantic search
-- ✅ Embeddings ready immediately after build
-- ✅ Consistent semantic search coverage
-
----
-
-### 2.3: Load In-Memory Index from Memgraph on Startup
-
-**File**: `src/mcp-server.ts` or `src/tools/tool-handlers.ts`
-**Location**: Constructor or initialization method
-
-```typescript
-// In ToolHandlers constructor:
-constructor(private context: ToolContext) {
- this.defaultActiveProjectContext = this.defaultProjectContext();
-
- // NEW: Load index from Memgraph if available
- if (this.context.memgraph.isConnected()) {
- console.log("[ToolHandlers] Loading graph index from Memgraph");
- this.loadIndexFromMemgraph();
- }
-
- this.initializeEngines();
-}
-
-private async loadIndexFromMemgraph(): Promise {
- try {
- const { projectId } = this.getActiveProjectContext();
-
- // Query all nodes
- const nodeResult = await this.context.memgraph.executeCypher(
- `MATCH (n {projectId: $projectId})
- RETURN n.id AS id, labels(n)[0] AS type, properties(n) AS props`,
- { projectId }
- );
-
- for (const row of nodeResult.data || []) {
- this.context.index.addNode(row.id, row.type, row.props);
- }
-
- // Query all relationships
- const relResult = await this.context.memgraph.executeCypher(
- `MATCH (n1 {projectId: $projectId})-[r]->(n2 {projectId: $projectId})
- RETURN id(r) AS id, type(r) AS type, n1.id AS from, n2.id AS to,
- properties(r) AS props`,
- { projectId }
- );
-
- for (const row of relResult.data || []) {
- this.context.index.addRelationship(row.id, row.from, row.to, row.type, row.props);
- }
-
- console.log(`[ToolHandlers] Loaded index: ${this.context.index.getStatistics().totalNodes} nodes`);
- } catch (error) {
- console.warn("[ToolHandlers] Failed to load index from Memgraph:", error);
- // Continue without loading - will be populated on rebuild
- }
-}
-```
-
-**Why This Fixes**:
-- ✅ Tools work immediately after server restart
-- ✅ No need to rebuild to get data
-- ✅ Much faster startup
-
----
-
-### 2.4: Add Consistency Check to graph_health
-
-**Already included in Phase 1.5** - graph_health now returns drift detection
-
----
-
-### 2.5: Make Task Persistence Mandatory
-
-**File**: `src/engines/progress-engine.ts`
-**Location**: updateTask() and createTask() methods
-
-```typescript
-updateTask(taskId: string, updates: Partial): Task | null {
- const task = this.tasks.get(taskId);
- if (!task) return null;
-
- Object.assign(task, updates);
-
- if (updates.status === "completed") {
- task.completedAt = Date.now();
- } else if (updates.status === "in-progress" && !task.startedAt) {
- task.startedAt = Date.now();
- }
-
- // NEW: Always persist to Memgraph
- if (this.memgraph && this.memgraph.isConnected()) {
- this.persistTaskUpdate(taskId, task); // Make this async and fire-and-forget
- } else {
- console.warn(`[ProgressEngine] Task update not persisted (Memgraph unavailable): ${taskId}`);
- // Still return the task, but warn about loss
- }
-
- return task;
-}
-
-private async persistTaskUpdate(taskId: string, task: Task): Promise {
- try {
- const query = `
- MERGE (t:TASK {id: $id})
- SET t.name = $name,
- t.description = $description,
- t.status = $status,
- t.assignee = $assignee,
- t.featureId = $featureId,
- t.startedAt = $startedAt,
- t.dueDate = $dueDate,
- t.completedAt = $completedAt,
- t.blockedBy = $blockedBy,
- t.updatedAt = $updatedAt
- `;
-
- await this.memgraph.executeCypher(query, {
- id: taskId,
- name: task.name,
- description: task.description,
- status: task.status,
- assignee: task.assignee,
- featureId: task.featureId,
- startedAt: task.startedAt,
- dueDate: task.dueDate,
- completedAt: task.completedAt,
- blockedBy: task.blockedBy,
- updatedAt: Date.now(),
- });
- } catch (error) {
- console.error(`[ProgressEngine] Failed to persist task: ${taskId}`, error);
- }
-}
-```
-
-**Why This Fixes**:
-- ✅ Tasks survive server restart
-- ✅ Distributed systems can share task state
-- ✅ Task history preserved in database
-
----
-
-## PHASE 3: MEDIUM PRIORITY FIXES (4-5 hours)
-
-### 3.1: Add Individual TEST_CASE Nodes
-
-**File**: `src/graph/builder.ts`
-**Location**: buildTestNodes() method
-
-```typescript
-private buildTestNodes(
- parsedFile: ParsedFile,
- projectId: string,
- fileNodeId: string
-): void {
- const testSuiteId = this.scopedId(`test_suite:${parsedFile.path}`);
-
- // Create TEST_SUITE node
- this.createTestSuiteNode(testSuiteId, parsedFile);
- this.createRelationship(fileNodeId, testSuiteId, "CONTAINS");
-
- // NEW: Create individual TEST_CASE nodes
- const testCases = this.extractTestCases(parsedFile);
- for (const testCase of testCases) {
- const testCaseId = this.scopedId(`test_case:${parsedFile.path}#${testCase.name}`);
-
- this.createTestCaseNode(testCaseId, testCase);
- this.createRelationship(testSuiteId, testCaseId, "CONTAINS");
- this.createRelationship(fileNodeId, testCaseId, "TESTS");
- }
-}
-
-private extractTestCases(parsedFile: ParsedFile): Array<{name: string, kind: string}> {
- // Extract individual test functions/cases
- // Pattern depends on language:
- // - TypeScript: describe/it blocks
- // - Python: test_ functions, class methods
- // - Go: Test* functions
- // - Rust: #[test] functions
-
- const testCases: Array<{name: string, kind: string}> = [];
-
- // Simplified: Just look for test-like symbols
- for (const symbol of parsedFile.symbols || []) {
- if (symbol.name.match(/^(test|spec|it|describe)/i) ||
- symbol.name.match(/^Test/)) {
- testCases.push({
- name: symbol.name,
- kind: symbol.kind || "test",
- });
- }
- }
-
- return testCases;
-}
-
-private createTestCaseNode(testCaseId: string, testCase: any): void {
- const nodeId = testCaseId;
-
- this.statements.push({
- query: `
- MERGE (t:TEST_CASE {id: $id})
- SET t.name = $name,
- t.kind = $kind,
- t.projectId = $projectId,
- t.createdAt = $timestamp
- `,
- params: {
- id: nodeId,
- name: testCase.name,
- kind: testCase.kind,
- projectId: this.projectId,
- timestamp: Date.now(),
- },
- });
-
- // Also add to in-memory index
- this.index.addNode(nodeId, "TEST_CASE", {
- name: testCase.name,
- kind: testCase.kind,
- projectId: this.projectId,
- });
-}
-```
-
-**Why This Fixes**:
-- ✅ Granular test coverage tracking
-- ✅ Test-level impact analysis
-- ✅ Better test selection
-
----
-
-### 3.2: Enable Doc Embeddings by Default
-
-**File**: `src/tools/tool-handlers.ts`
-**Location**: indexDocs parameter in graph_rebuild and DocsEngine initialization
-
-```typescript
-// In graph_rebuild():
-const { indexDocs = true } = args; // Change from false to true
-
-// In DocsEngine.indexWorkspace():
-const { withEmbeddings = true } = options; // Change from false to true
-
-// Call for embeddings:
-await this.engine.generateEmbeddings(sections, projectId);
-```
-
-**Why This Fixes**:
-- ✅ Documentation searchable via semantic_search
-- ✅ Better combined results (code + docs)
-- ✅ Integration information discovery
-
----
-
-### 3.3: Add State Machine for Sync States
-
-**New File**: `src/graph/sync-state.ts`
-
-```typescript
-export type SyncState = "uninitialized" | "synced" | "drifted" | "rebuilding";
-
-export interface SystemHealth {
- memgraph: SyncState;
- index: SyncState;
- qdrant: SyncState;
- embeddings: SyncState;
-}
-
-export class SyncStateManager {
- private state: SystemHealth = {
- memgraph: "uninitialized",
- index: "uninitialized",
- qdrant: "uninitialized",
- embeddings: "uninitialized",
- };
-
- setState(system: keyof SystemHealth, newState: SyncState): void {
- this.state[system] = newState;
- console.log(`[SyncState] ${system}: ${newState}`);
- }
-
- getState(): SystemHealth {
- return { ...this.state };
- }
-
- isHealthy(): boolean {
- return Object.values(this.state).every(s => s === "synced");
- }
-
- needsSync(): keyof SystemHealth | null {
- for (const [system, state] of Object.entries(this.state)) {
- if (state === "drifted") return system as keyof SystemHealth;
- }
- return null;
- }
-}
-```
-
-**Why This Helps**:
-- ✅ Clear tracking of each subsystem
-- ✅ Automatic recovery recommendations
-- ✅ Better diagnostics
-
----
-
-## PART 3: VALIDATION TEST CASES
-
-### Test 1: graph_rebuild Syncs All Systems
-
-**Setup**:
-```bash
-graph_set_workspace({
- projectId: "code-visual",
- workspaceRoot: "/path/to/code-visual",
- sourceDir: "src"
-})
-graph_rebuild({ mode: "full", indexDocs: true })
-```
-
-**Assertions**:
-```
-✓ Memgraph has 809 nodes
-✓ In-memory index has 809 nodes
-✓ Qdrant has embeddings for functions, classes, files
-✓ graph_health returns totalNodes: 809
-✓ Embedding coverage > 80%
-✓ No drift detected
-```
-
----
-
-### Test 2: Feature Status Works
-
-**Setup**:
-```bash
-graph_rebuild({ mode: "full" })
-feature_status("code-visual:feature:phase-1")
-```
-
-**Expected Result**:
-```json
-{
- "success": true,
- "feature": {
- "id": "code-visual:feature:phase-1",
- "name": "Code Graph MVP",
- "status": "completed"
- },
- "tasks": [...],
- "progressPercentage": 100
-}
-```
-
----
-
-### Test 3: Multi-Project Isolation
-
-**Setup**:
-```bash
-# Project A
-graph_set_workspace({ projectId: "project-a", ... })
-graph_rebuild({ mode: "full" })
-semantic_search("authenticate") # Get functions from A only
-
-# Project B
-graph_set_workspace({ projectId: "project-b", ... })
-graph_rebuild({ mode: "full" })
-semantic_search("authenticate") # Get functions from B only (not A)
-```
-
-**Assertion**: Results don't overlap
-
----
-
-### Test 4: Embeddings Updated on Incremental Build
-
-**Setup**:
-```bash
-graph_rebuild({ mode: "full" })
-# Add new function to source
-graph_rebuild({ mode: "incremental" })
-semantic_search("newFunction") # Should find it
-```
-
-**Assertion**: embeddingsReady reset, new function appears
-
----
-
-## PART 4: IMPLEMENTATION TIMELINE
-
-| Phase | Tasks | Effort | Urgency |
-|-------|-------|--------|---------|
-| **P0** | 1.1-1.5 (Index sync, Qdrant scope, Feature fix, ProgressEngine reload, graph_health query) | 6-8h | CRITICAL |
-| **P1** | 2.1-2.5 (Embedding flag, Auto-gen, Index load, Consistency, Task persist) | 3-4h | HIGH |
-| **P2** | 3.1-3.3 (Test cases, Doc embeddings, Sync state) | 4-5h | MEDIUM |
-| **P3** | Long-term architecture (per-project indices) | 8-12h | LOW |
-| **Total** | All phases | 21-29h | - |
-
----
-
-## PART 5: VALIDATION CHECKLIST
-
-After implementing all phases:
-
-- [ ] Phase 1 tests pass
- - [ ] graph_health returns accurate counts
- - [ ] feature_status resolves valid IDs
- - [ ] progress_query returns task list
- - [ ] In-memory index synced after build
-
-- [ ] Phase 2 tests pass
- - [ ] Incremental builds update Qdrant
- - [ ] Embeddings generated after full build
- - [ ] Index loaded from Memgraph on startup
- - [ ] Tasks persisted to Memgraph
-
-- [ ] Phase 3 tests pass
- - [ ] Individual test cases tracked
- - [ ] Doc embeddings generated
- - [ ] Sync state tracked and reported
-
-- [ ] Integration tests pass
- - [ ] code-visual works with all tools
- - [ ] No cross-project contamination
- - [ ] Consistent counts across all systems
- - [ ] No data loss on restart
-
-- [ ] Performance tests pass
- - [ ] startup < 5 seconds
- - [ ] graph_health < 500ms
- - [ ] semantic_search < 1000ms (first call)
- - [ ] semantic_search < 200ms (subsequent)
-
----
-
-## Summary
-
-The **comprehensive review revealed 10 critical issues** in the data pipeline:
-
-1. ✗ Unsynced index systems
-2. ✗ Qdrant gets no data from rebuild
-3. ✗ Qdrant has no project scoping
-4. ✗ Embeddings never auto-generated
-5. ✗ embeddingsReady flag never reset
-6. ✗ FEATURE nodes always overwritten
-7. ✗ TASK nodes not persisted
-8. ✗ In-memory index starts empty
-9. ✗ No cross-project cleanup
-10. ✗ Individual test cases missing
-
-**The revised plan fixes all of these** through:
-- **Phase 1**: Critical synchronization and scoping (6-8h)
-- **Phase 2**: Embedding and persistence (3-4h)
-- **Phase 3**: Missing data types and state management (4-5h)
-
-**Estimated total effort: 13-17 hours** for complete fix across all phases.
-
diff --git a/GRAPH_STATE_QUICK_REF.txt b/docs/GRAPH_STATE_QUICK_REF.txt
similarity index 100%
rename from GRAPH_STATE_QUICK_REF.txt
rename to docs/GRAPH_STATE_QUICK_REF.txt
diff --git a/docs/INTEGRATION_SUMMARY.md b/docs/INTEGRATION_SUMMARY.md
index 980cb30..fd4d9f6 100644
--- a/docs/INTEGRATION_SUMMARY.md
+++ b/docs/INTEGRATION_SUMMARY.md
@@ -12,13 +12,12 @@ docs/
├─ MCP_INTEGRATION_GUIDE.md ......... Complete integration guide
├─ CLAUDE_INTEGRATION.md ........... Claude/Copilot system prompt solution
├─ TOOL_PATTERNS.md ............... Grep → MCP replacement patterns
-├─ copilot-instructions-template.md . Copy to .github/copilot-instructions.md
-├─ CLIENT_EXAMPLES.md ............. Code snippets (TypeScript, Python, bash, React)
-│ [not created yet - see QUICK_REFERENCE.md examples]
-├─ QUICK_REFERENCE.md ............. All 38 tools reference
+├─ templates/copilot-instructions-template.md . Copy to .github/copilot-instructions.md
+├─ templates/skill-mcp-template.md .. Skill prompt template
+├─ templates/toolsets-template.jsonc . VS Code toolset template
+├─ QUICK_REFERENCE.md ............. All 39 tools reference
├─ QUICK_START.md ................. Server deployment
-├─ ARCHITECTURE.md ................ Technical deep dive
-└─ GRAPH_EXPERT_AGENT.md .......... Full agent runbook
+└─ ARCHITECTURE.md ................ Technical deep dive
```
---
@@ -29,7 +28,7 @@ docs/
**Make Claude use MCP in long conversations (the main problem)**
→ Read: [docs/CLAUDE_INTEGRATION.md](CLAUDE_INTEGRATION.md)
-→ Then: [docs/copilot-instructions-template.md](copilot-instructions-template.md)
+→ Then: [docs/templates/copilot-instructions-template.md](templates/copilot-instructions-template.md)
**Integrate MCP into my projects**
→ Read: [docs/MCP_INTEGRATION_GUIDE.md](MCP_INTEGRATION_GUIDE.md)
@@ -37,7 +36,7 @@ docs/
**Replace grep with MCP tools**
→ Read: [docs/TOOL_PATTERNS.md](TOOL_PATTERNS.md)
-**See all 38 tools**
+**See all 39 tools**
→ Read: [QUICK_REFERENCE.md](../QUICK_REFERENCE.md)
**Deploy the server**
@@ -48,16 +47,21 @@ docs/
## 🔑 Key Insights
### Problem: Long Conversation Instruction Drift
+
After ~15 messages, Copilot ignores instructions and falls back to:
+
- Reading files directly
- Using grep patterns
- Manual code analysis
### Root Cause
+
Instructions are overlaid suggestions. They fade in long conversations. File reads are baked into training data (default behavior).
### Solution: System Prompt Engineering
+
Make the system prompt **enforce MCP at protocol level**:
+
- File reads become impossible (system block)
- Grep becomes forbidden (protocol)
- MCP becomes mandatory (only option)
@@ -65,7 +69,9 @@ Make the system prompt **enforce MCP at protocol level**:
Result: Even at message 100, Claude uses MCP because it's the **only option**.
### Implementation
+
Edit `~/.claude_desktop_config.json`:
+
```json
{
"systemPrompt": "NEVER read files. NEVER use grep. ALWAYS use MCP tools..."
@@ -77,42 +83,48 @@ Edit `~/.claude_desktop_config.json`:
## 📋 Setup Checklist
### Infrastructure (One Time)
+
- [ ] Docker + Docker Compose installed
- [ ] `docker-compose up -d memgraph qdrant`
- [ ] `npm run build && npm run start:http`
- [ ] Verify: `curl http://localhost:9000/health`
### Claude Desktop
+
- [ ] Edit `~/.claude_desktop_config.json`
- [ ] Add MCP server config
- [ ] Add system prompt (enforces MCP)
- [ ] Restart Claude
### Per-Project
-- [ ] Copy [copilot-instructions-template.md](copilot-instructions-template.md) to `.github/copilot-instructions.md`
+
+- [ ] Copy [templates/copilot-instructions-template.md](templates/copilot-instructions-template.md) to `.github/copilot-instructions.md`
- [ ] Update project references
-- [ ] Add `.mcp-config.json` with projectId
-- [ ] Commit both files
+- [ ] Set `LXDIG_PROJECT_ID` in your environment or pass `projectId` to `graph_set_workspace`
+- [ ] Commit `.github/copilot-instructions.md`
---
## 🚀 Implementation Order
### Phase 1: Foundation (15 min)
+
1. Start Docker services
2. Build and start MCP server
3. Update Claude Desktop config
4. Restart Claude
### Phase 2: Test (5 min)
+
1. Ask Claude: "How does [file] work?"
2. Verify it calls MCP tools (not file reads)
3. Test long conversation (20+ messages)
4. Verify no degradation
### Phase 3: Rollout (Per-Project)
+
1. Copy copilot instructions to `.github/copilot-instructions.md`
-2. Add `.mcp-config.json`
+2. Set `LXDIG_PROJECT_ID` or pass projectId to `graph_set_workspace`
3. Commit and push
4. Update team
@@ -120,7 +132,8 @@ Edit `~/.claude_desktop_config.json`:
## 💡 Core Concepts
-### 38 MCP Tools Available
+### 39 MCP Tools Available
+
```
Essential 4:
• graph_query — Find code by natural language
@@ -154,6 +167,7 @@ Memory 4:
See [docs/TOOL_PATTERNS.md](TOOL_PATTERNS.md) for pattern matching.
### Multi-Project Architecture
+
```
Claude → MCP Server → Memgraph + Qdrant
↓
@@ -166,6 +180,7 @@ Each project: isolated by `projectId` + `workspaceRoot`
Shared: Memgraph + Qdrant infrastructure
### Session Re-anchoring
+
```
Message 1: graph_set_workspace() → session starts
Message 1-4: Normal MCP queries
@@ -179,13 +194,13 @@ Message 10: graph_health() → re-anchor
## 📊 Performance Gains
-| Task | Grep/Manual | MCP | Improvement |
-|------|---|---|---|
-| Find symbol | 450ms | 50ms | 9x faster |
-| Understand function | 5 min | 200ms | 1500x faster |
-| Impact analysis | 10 min | 100ms | 6000x faster |
-| Search by meaning | 2 min | 150ms | 800x faster |
-| False positives | High | <1% | 100x better |
+| Task | Grep/Manual | MCP | Improvement |
+| ------------------- | ----------- | ----- | ------------ |
+| Find symbol | 450ms | 50ms | 9x faster |
+| Understand function | 5 min | 200ms | 1500x faster |
+| Impact analysis | 10 min | 100ms | 6000x faster |
+| Search by meaning | 2 min | 150ms | 800x faster |
+| False positives | High | <1% | 100x better |
---
@@ -206,6 +221,7 @@ After full implementation:
## 🔍 Before vs After
### Before (Grep/File Reads)
+
```
User: "How does auth work?"
Claude:
@@ -224,6 +240,7 @@ Claude:
```
### After (MCP)
+
```
User: "How does auth work?"
Claude:
@@ -246,27 +263,27 @@ Claude:
- **Root**: [QUICK_START.md](../QUICK_START.md), [QUICK_REFERENCE.md](../QUICK_REFERENCE.md), [ARCHITECTURE.md](../ARCHITECTURE.md)
- **Docs**: [MCP_INTEGRATION_GUIDE.md](MCP_INTEGRATION_GUIDE.md), [CLAUDE_INTEGRATION.md](CLAUDE_INTEGRATION.md), [TOOL_PATTERNS.md](TOOL_PATTERNS.md)
-- **Template**: [copilot-instructions-template.md](copilot-instructions-template.md)
+- **Template**: [templates/copilot-instructions-template.md](templates/copilot-instructions-template.md)
---
## 🎓 For Your Team
1. **Tech Lead**: Read [docs/CLAUDE_INTEGRATION.md](CLAUDE_INTEGRATION.md) + [docs/MCP_INTEGRATION_GUIDE.md](MCP_INTEGRATION_GUIDE.md)
-2. **Developers**: Read [docs/copilot-instructions-template.md](copilot-instructions-template.md) + [docs/TOOL_PATTERNS.md](TOOL_PATTERNS.md)
+2. **Developers**: Read [docs/templates/copilot-instructions-template.md](templates/copilot-instructions-template.md) + [docs/TOOL_PATTERNS.md](TOOL_PATTERNS.md)
3. **New Team Members**: Follow setup checklist + read `.github/copilot-instructions.md`
---
## 🆘 Troubleshooting
-| Issue | Solution | Doc |
-|-------|----------|-----|
-| Claude reads files | Update system prompt | [CLAUDE_INTEGRATION.md](CLAUDE_INTEGRATION.md) |
+| Issue | Solution | Doc |
+| ------------------------ | ------------------------ | ---------------------------------------------- |
+| Claude reads files | Update system prompt | [CLAUDE_INTEGRATION.md](CLAUDE_INTEGRATION.md) |
| Long conversations break | Add graph_health() calls | [CLAUDE_INTEGRATION.md](CLAUDE_INTEGRATION.md) |
-| Don't know which tool | Check TOOL_PATTERNS | [TOOL_PATTERNS.md](TOOL_PATTERNS.md) |
-| Server won't start | Check Docker | [QUICK_START.md](../QUICK_START.md) |
-| Need tool reference | See all 38 tools | [QUICK_REFERENCE.md](../QUICK_REFERENCE.md) |
+| Don't know which tool | Check TOOL_PATTERNS | [TOOL_PATTERNS.md](TOOL_PATTERNS.md) |
+| Server won't start | Check Docker | [QUICK_START.md](../QUICK_START.md) |
+| Need tool reference | See all 39 tools | [QUICK_REFERENCE.md](../QUICK_REFERENCE.md) |
---
diff --git a/docs/MCP_INTEGRATION_GUIDE.md b/docs/MCP_INTEGRATION_GUIDE.md
index 97251fc..880648b 100644
--- a/docs/MCP_INTEGRATION_GUIDE.md
+++ b/docs/MCP_INTEGRATION_GUIDE.md
@@ -1,10 +1,11 @@
# MCP Server Integration Guide
-Complete guide for integrating lxRAG MCP across projects.
+Complete guide for integrating lxDIG MCP across projects.
## Quick Start (15 minutes)
### 1. Start Infrastructure
+
```bash
cd /home/alex_rod/code-graph-server
docker-compose up -d memgraph qdrant
@@ -13,11 +14,13 @@ npm run start:http # Listens on http://localhost:9000
```
### 2. Configure Claude Desktop
+
Edit `~/.claude_desktop_config.json`:
+
```json
{
"mcpServers": {
- "lxrag": {
+ "lxdig": {
"command": "node",
"args": ["/home/alex_rod/code-graph-server/dist/server.js"],
"env": {
@@ -27,16 +30,18 @@ Edit `~/.claude_desktop_config.json`:
}
}
},
- "systemPrompt": "You are a code intelligence expert using lxRAG MCP.\n\nMANDATORY:\n1. NEVER read files directly\n2. NEVER use grep or search patterns\n3. ALWAYS use MCP tools for code intelligence\n4. Call graph_set_workspace on first query\n5. Call graph_health every 5 messages to re-anchor\n\nTools: graph_query, code_explain, impact_analyze, test_select, arch_validate, semantic_search, find_pattern, episode_add, agent_claim, and 29 more.\n\nSee .github/copilot-instructions.md for full reference."
+ "systemPrompt": "You are a code intelligence expert using lxDIG MCP.\n\nMANDATORY:\n1. NEVER read files directly\n2. NEVER use grep or search patterns\n3. ALWAYS use MCP tools for code intelligence\n4. Call graph_set_workspace on first query\n5. Call graph_health every 5 messages to re-anchor\n\nTools: graph_query, code_explain, impact_analyze, test_select, arch_validate, semantic_search, find_pattern, episode_add, agent_claim, and 29 more.\n\nSee .github/copilot-instructions.md for full reference."
}
```
### 3. Configure VS Code
+
Create `.vscode/mcp.json`:
+
```json
{
"servers": {
- "lxrag": {
+ "lxdig": {
"type": "stdio",
"command": "node",
"args": ["/home/alex_rod/code-graph-server/dist/server.js"]
@@ -46,6 +51,7 @@ Create `.vscode/mcp.json`:
```
### 4. Create .github/copilot-instructions.md
+
See template at end of this file.
## Architecture
@@ -54,7 +60,7 @@ See template at end of this file.
Claude/Copilot Chat
↓ (MCP tools only)
↓
-lxRAG MCP Server (http://localhost:9000)
+lxDIG MCP Server (http://localhost:9000)
↓ ↓
Memgraph Qdrant
(graph) (vectors)
@@ -65,6 +71,7 @@ Per-project isolation via projectId + workspaceRoot
## Multi-Project Setup
For each project, add `.mcp-config.json`:
+
```json
{
"projectId": "my-project",
@@ -77,38 +84,44 @@ For each project, add `.mcp-config.json`:
## 38 Tools Quick Reference
### Essential (Use First)
+
- `graph_query(query, language)` — Find code by natural language or Cypher
- `code_explain(symbol)` — Understand a symbol with full context
- `impact_analyze(changedFiles)` — What breaks if I change these files?
- `test_select(changedFiles)` — Which tests should I run?
### Architecture
+
- `arch_validate(profile)` — Check for violations
- `arch_suggest(filePath)` — Where should this code go?
### Search & Discovery
+
- `semantic_search(query)` — Search by concept/meaning
- `find_pattern(pattern)` — Detect violations and anti-patterns
- `find_similar_code(symbol)` — Find similar implementations
### Testing
+
- `test_categorize()` — Categorize test files
- `suggest_tests(symbol)` — Tests needed for symbol
### Memory & Coordination
+
- `episode_add(type, content, agentId)` — Record decision
- `decision_query(agentId)` — Recall past decisions
- `agent_claim(agentId, taskName)` — Claim ownership
- `agent_release(agentId, taskName)` — Release claim
### Advanced
+
- `context_pack(task, profile)` — Token-efficient context
- `semantic_slice(symbol)` — Get relevant code ranges only
- `diff_since(timestamp)` — Changes since time
- `graph_health()` — Check graph status
- `graph_rebuild(mode)` — Rebuild graph
-See QUICK_REFERENCE.md for all 38 tools.
+See QUICK_REFERENCE.md for all 39 tools.
## Preventing Instruction Drift in Long Conversations
@@ -117,6 +130,7 @@ See QUICK_REFERENCE.md for all 38 tools.
**Solution**: System prompt enforcement + periodic re-anchoring
### Key Rules (Non-Negotiable)
+
1. **NEVER** read files (system-level block)
2. **NEVER** use grep (forbidden pattern)
3. **ALWAYS** use MCP tools
@@ -124,6 +138,7 @@ See QUICK_REFERENCE.md for all 38 tools.
5. If graph not ready, call `graph_rebuild(mode: 'incremental')`
### Why System Prompt Works
+
- Instructions are overlaid suggestions (fade in long chats)
- System prompt is protocol-level (never fades)
- File reads become impossible (not a suggestion)
@@ -132,6 +147,7 @@ See QUICK_REFERENCE.md for all 38 tools.
## Pattern: Replace Grep with MCP
### ❌ Before (Grep)
+
```bash
grep -r "MyClass" src/ --include="*.ts"
grep -r "import.*AuthService" src/
@@ -139,10 +155,11 @@ find . -name "*.test.ts"
```
### ✅ After (MCP)
+
```typescript
-await mcp.query('find all references to MyClass');
-await mcp.query('find all imports of AuthService');
-await mcp.call('test_categorize', {});
+await mcp.query("find all references to MyClass");
+await mcp.query("find all imports of AuthService");
+await mcp.call("test_categorize", {});
```
**Benefits**: 10x faster, zero false positives, full dependency context
@@ -171,15 +188,16 @@ Message 15+:
## Client Implementation
### TypeScript
+
```typescript
const mcp = new MCPClient({
- serverUrl: 'http://localhost:9000',
- projectId: 'my-project',
- workspaceRoot: '/path/to/project'
+ serverUrl: "http://localhost:9000",
+ projectId: "my-project",
+ workspaceRoot: "/path/to/project",
});
await mcp.initialize();
-await mcp.query('find all HTTP handlers');
+await mcp.query("find all HTTP handlers");
```
See docs/CLIENT_EXAMPLES.md for Python, bash, React.
@@ -202,17 +220,17 @@ See docs/CLIENT_EXAMPLES.md for Python, bash, React.
## Troubleshooting
-| Issue | Solution |
-|-------|----------|
-| Claude still reads files | Update system prompt in Claude Desktop config |
-| Graph not indexing | Run: `graph_rebuild(mode: 'full')` |
-| MCP server won't start | Check Docker: `docker-compose ps` |
-| Long conversations fail | Add `graph_health()` re-anchoring every 5 messages |
+| Issue | Solution |
+| ------------------------ | -------------------------------------------------- |
+| Claude still reads files | Update system prompt in Claude Desktop config |
+| Graph not indexing | Run: `graph_rebuild(mode: 'full')` |
+| MCP server won't start | Check Docker: `docker-compose ps` |
+| Long conversations fail | Add `graph_health()` re-anchoring every 5 messages |
## Files to Read
- QUICK_START.md — Deployment details
-- QUICK_REFERENCE.md — All 38 tools
+- QUICK_REFERENCE.md — All 39 tools
- ARCHITECTURE.md — Technical deep dive
- docs/CLIENT_EXAMPLES.md — Code snippets
- docs/CLAUDE_INTEGRATION.md — System prompt details
diff --git a/docs/PROJECT_FEATURES_CAPABILITIES.md b/docs/PROJECT_FEATURES_CAPABILITIES.md
new file mode 100644
index 0000000..977a077
--- /dev/null
+++ b/docs/PROJECT_FEATURES_CAPABILITIES.md
@@ -0,0 +1,180 @@
+# Project Features and Capabilities
+
+## Executive Summary
+
+lexDIG-MCP is an MCP server focused on **architecture-aware code intelligence** and **agent-ready task coordination**. It combines:
+
+- A graph plane for structural understanding.
+- A semantic retrieval plane for relevance ranking.
+- A task/memory plane for execution continuity.
+
+The result is a toolset that supports repository onboarding, impact analysis, architecture validation, semantic code search, test selection, and multi-agent handoffs.
+
+---
+
+## Core Capability Areas
+
+## 1) Graph and Code Understanding
+
+- Repository graph indexing and rebuild orchestration.
+- Structural query support through graph-aware tools.
+- Symbol explanation with dependency-aware context.
+- Pattern/violation detection (including circularity and unused structures).
+
+Primary tools:
+
+- `graph_query`, `graph_rebuild`, `graph_health`, `code_explain`, `find_pattern`, `code_clusters`.
+
+## 2) Semantic Retrieval and Comparison
+
+- Semantic search across code elements.
+- Similar-code retrieval for pattern reuse and anomaly finding.
+- Semantic diff and slice extraction for focused analysis.
+
+Primary tools:
+
+- `semantic_search`, `find_similar_code`, `semantic_diff`, `semantic_slice`.
+
+## 3) Testing and Change Impact
+
+- Impact analysis to identify affected tests.
+- Automated test categorization and selection.
+- Execution of selected test suites.
+
+Primary tools:
+
+- `impact_analyze`, `test_select`, `test_categorize`, `suggest_tests`, `test_run`.
+
+## 4) Architecture Governance
+
+- Layer/boundary validation against intended architecture.
+- Suggested placement of new code based on existing topology.
+
+Primary tools:
+
+- `arch_validate`, `arch_suggest`.
+
+## 5) Agent Coordination and Memory
+
+- Task claim/release conflict prevention.
+- Context packet generation for work handoffs.
+- Episode and decision memory storage/retrieval.
+- Reflection synthesis from prior work episodes.
+
+Primary tools:
+
+- `context_pack`, `agent_claim`, `agent_release`, `agent_status`, `episode_add`, `episode_recall`, `decision_query`, `reflect`.
+
+## 6) Setup and Developer Experience
+
+- One-shot project setup and graph initialization.
+- Copilot instruction generation from repository context.
+- Contract validation and utility discovery.
+
+Primary tools:
+
+- `init_project_setup`, `setup_copilot_instructions`, `contract_validate`, `tools_list`.
+
+---
+
+## Architectural Building Blocks
+
+### Runtime
+
+- TypeScript/Node.js MCP server.
+- stdio and Streamable HTTP operation modes.
+- Response shaping and profile-based outputs (`compact`, `balanced`, `debug`).
+
+### Data planes
+
+- **Graph plane**: Memgraph-backed structural relationships and query execution.
+- **Vector plane**: Qdrant-backed semantic embeddings and nearest-neighbor retrieval.
+- **Document plane**: markdown indexing + section-level search.
+
+### Engine layers
+
+- Architecture engine.
+- Coordination engine.
+- Episode/memory engine.
+- Progress and test engines.
+- Docs/reference utilities.
+
+---
+
+## Measured Signals (Current Repo Evidence)
+
+From benchmark artifacts (`benchmarks/graph_tools_benchmark_results.json`):
+
+- Total scenarios: **20**.
+- MCP faster: **15**.
+- Baseline faster: **1**.
+- MCP-only successful scenarios: **4**.
+
+Interpretation:
+
+- The project demonstrates strong comparative behavior on synthetic graph-tool scenarios.
+- Performance claims should still be treated as workload-dependent and environment-sensitive.
+
+---
+
+## Integration Modes
+
+### IDE / local assistant workflows
+
+- Strong fit for coding assistants needing fast context and architectural grounding.
+
+### CI and scripted workflows
+
+- Tools can be invoked for contract checks, health checks, and focused analysis.
+
+### Multi-agent orchestration
+
+- Claims + memory + context packs support parallel work with reduced collision risk.
+
+---
+
+## Ecosystem Alignment (Research-Enriched)
+
+### Model Context Protocol alignment
+
+- JSON-RPC transport model with capability-driven interactions.
+- Stateful session expectations and explicit tool invocation semantics.
+- Safety/trust design principles for tool-executing clients.
+
+### Memgraph alignment
+
+- Cypher-native graph querying and graph algorithm support.
+- Deployment-friendly paths for local and production use.
+
+### Qdrant alignment
+
+- Purpose-built vector similarity platform for semantic retrieval workloads.
+- Practical local/cloud deployment options for scaling retrieval fidelity.
+
+---
+
+## Current Non-Goals / Limits (As Documented)
+
+- Not a general-purpose build system replacement.
+- Not a universal language server replacement.
+- Requires healthy graph/index state for best results.
+- Output quality depends on repository coverage and indexing freshness.
+
+---
+
+## Canonical Sources
+
+Internal:
+
+- `README.md`
+- `ARCHITECTURE.md`
+- `QUICK_START.md`
+- `QUICK_REFERENCE.md`
+- `docs/MCP_INTEGRATION_GUIDE.md`
+- `docs/TOOL_PATTERNS.md`
+
+External:
+
+- https://modelcontextprotocol.io/specification/2025-06-18
+- https://memgraph.com/docs
+- https://qdrant.tech/documentation/
diff --git a/docs/README.md b/docs/README.md
index 98fc0cc..3e5c493 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -5,20 +5,45 @@
**Your MCP server is production-ready. Here's how to use it.**
### Quick Overview (5 min)
+
→ [INTEGRATION_SUMMARY.md](INTEGRATION_SUMMARY.md)
### Fix Copilot Instructions Drift (15 min)
+
→ [CLAUDE_INTEGRATION.md](CLAUDE_INTEGRATION.md) ⭐ THE SOLUTION
### Copy to Your Projects
-→ [copilot-instructions-template.md](copilot-instructions-template.md)
+
+→ [templates/copilot-instructions-template.md](templates/copilot-instructions-template.md)
### Complete Integration Guide (30 min)
+
→ [MCP_INTEGRATION_GUIDE.md](MCP_INTEGRATION_GUIDE.md)
### Grep → MCP Patterns (15 min)
+
→ [TOOL_PATTERNS.md](TOOL_PATTERNS.md)
+### Code Comment Conventions
+
+→ [CODE_COMMENT_STANDARD.md](CODE_COMMENT_STANDARD.md)
+
+### Consolidated Tool Information
+
+→ [TOOLS_INFORMATION_GUIDE.md](TOOLS_INFORMATION_GUIDE.md)
+
+### Project Features & Capabilities
+
+→ [PROJECT_FEATURES_CAPABILITIES.md](PROJECT_FEATURES_CAPABILITIES.md)
+
+### Audits & Evaluations Summary
+
+→ [AUDITS_EVALUATIONS_SUMMARY.md](AUDITS_EVALUATIONS_SUMMARY.md)
+
+### Plans & Pending Actions
+
+→ [PLANS_PENDING_ACTIONS_SUMMARY.md](PLANS_PENDING_ACTIONS_SUMMARY.md)
+
---
## File Structure
@@ -30,11 +55,19 @@ docs/
├─ CLAUDE_INTEGRATION.md ........... System prompt solution ⭐
├─ MCP_INTEGRATION_GUIDE.md ........ Complete setup guide
├─ TOOL_PATTERNS.md ............... Before/after patterns
-└─ copilot-instructions-template.md . Copy to projects
+├─ TOOLS_INFORMATION_GUIDE.md ...... Consolidated tool inventory
+├─ PROJECT_FEATURES_CAPABILITIES.md Features and capability map
+├─ AUDITS_EVALUATIONS_SUMMARY.md ... Consolidated findings
+├─ PLANS_PENDING_ACTIONS_SUMMARY.md Prioritized execution plan
+├─ templates/
+│ ├─ copilot-instructions-template.md Copy to projects
+│ ├─ skill-mcp-template.md .......... Skill prompt template
+│ └─ toolsets-template.jsonc ........ VS Code toolset template
+└─ (other docs...)
Root:
├─ .github/copilot-instructions.md . For this project (ready to use)
-├─ QUICK_REFERENCE.md ............. All 38 tools
+├─ QUICK_REFERENCE.md ............. All 39 tools
├─ QUICK_START.md ................. Server deployment
├─ ARCHITECTURE.md ................ Technical details
└─ README.md ...................... Project overview
@@ -45,28 +78,33 @@ Root:
## By Use Case
### I need to fix "Copilot ignores my instructions"
+
1. Read: **CLAUDE_INTEGRATION.md** (15 min)
2. Update: `~/.claude_desktop_config.json`
3. Restart: Claude Desktop
4. Test: Ask code question
### I want to integrate into my projects
+
1. Start: **INTEGRATION_SUMMARY.md** (5 min)
-2. Copy: **copilot-instructions-template.md** (1 min)
+2. Copy: **templates/copilot-instructions-template.md** (1 min)
3. Setup: Follow checklist (10 min)
4. Test: Long conversation (5 min)
### I want to replace grep with MCP
+
1. Learn: **TOOL_PATTERNS.md** (15 min)
2. Apply: Use pattern in your code
3. Test: Verify faster + more accurate
### I need complete integration details
+
1. Read: **MCP_INTEGRATION_GUIDE.md** (30 min)
2. Follow: Setup phases
3. Deploy: To all projects
### I need all tool references
+
1. See: **QUICK_REFERENCE.md** (root)
2. See: **TOOL_PATTERNS.md** (quick lookup)
@@ -87,24 +125,25 @@ See: [CLAUDE_INTEGRATION.md](CLAUDE_INTEGRATION.md)
## Performance Gains
-| Task | Before | After | Gain |
-|------|--------|-------|------|
-| Find symbol | 450ms | 50ms | 9x faster |
-| Understand | 5 min | 200ms | 1500x faster |
-| Impact analysis | 10 min | 100ms | 6000x faster |
-| Search by concept | 2 min | 150ms | 800x faster |
+| Task | Before | After | Gain |
+| ----------------- | ------ | ----- | ------------ |
+| Find symbol | 450ms | 50ms | 9x faster |
+| Understand | 5 min | 200ms | 1500x faster |
+| Impact analysis | 10 min | 100ms | 6000x faster |
+| Search by concept | 2 min | 150ms | 800x faster |
---
-## 38 Tools at a Glance
+## 39 Tools at a Glance
**Essential 4:**
+
- `graph_query` - Find code
- `code_explain` - Understand symbols
- `impact_analyze` - What breaks?
- `test_select` - Which tests?
-**+ 34 more** (see QUICK_REFERENCE.md)
+**+ 35 more** (see QUICK_REFERENCE.md)
---
@@ -116,7 +155,7 @@ docker-compose up -d memgraph qdrant
npm run build && npm run start:http
# Per project
-- Copy copilot-instructions-template.md → .github/copilot-instructions.md
+- Copy templates/copilot-instructions-template.md → .github/copilot-instructions.md
- Add .mcp-config.json
- Update ~/.claude_desktop_config.json
- Restart Claude
diff --git a/docs/REVISED_ACTION_PLAN_WITH_CLI_ANALYSIS.md b/docs/REVISED_ACTION_PLAN_WITH_CLI_ANALYSIS.md
deleted file mode 100644
index cb1c6b1..0000000
--- a/docs/REVISED_ACTION_PLAN_WITH_CLI_ANALYSIS.md
+++ /dev/null
@@ -1,507 +0,0 @@
-# REVISED Action Plan: lxRAG Tool Issues Analysis
-## Updated After CLI Command Investigation & Graph State Analysis
-
-**Status:** Analysis Complete - Ready for Implementation
-**Date:** 2026-02-22
-**Analysis Depth:** Full graph state lifecycle investigation
-**Previous Analysis:** ACTION_PLAN_LXRAG_TOOL_FIXES.md (superseded by this document)
-
----
-
-## What Changed: New Findings from Graph State Analysis
-
-### Discovery #1: The Index is Mostly EMPTY, Not Just Unscoped
-
-**Previous Understanding:**
-- "The index is global and not project-scoped"
-
-**Actual Reality:**
-- The shared `GraphIndexManager` starts **empty** at server startup
-- It's **never populated** from Memgraph
-- It's **never synced** from Orchestrator after builds
-- It remains empty throughout the server's lifetime (except for manual adds)
-
-**Impact on Original Issues:**
-```
-✗ graph_health reports zeros because:
- └─ It reads from empty shared index
- └─ NOT because data is "un-scoped"
-
-✗ feature_status fails because:
- └─ ProgressEngine.features Map is empty
- └─ NOT because of stale project-scoped data
-
-✗ progress_query returns empty because:
- └─ ProgressEngine.tasks Map is empty
- └─ NOT because of stale project-scoped data
-```
-
-### Discovery #2: Three Unsynced Index Systems Exist
-
-```
-SYSTEM 1: GraphOrchestrator.index (Internal)
-├─ Created during graph_rebuild()
-├─ Populated with ALL parsed source code
-├─ Used to generate Cypher statements
-└─ DISCARDED after build (never synced to shared index)
-
-SYSTEM 2: ToolContext.index (Shared, Global)
-├─ Initialized empty at server startup
-├─ Never populated from Memgraph
-├─ Never synced from Orchestrator
-├─ Used by ALL engines (ProgressEngine, TestEngine, etc.)
-└─ Remains empty during normal operation
-
-SYSTEM 3: Memgraph Database (Source of Truth)
-├─ Updated by Orchestrator's Cypher statements
-├─ Queried directly by tool implementations
-├─ Accurate and current
-└─ NOT synced back to shared index
-```
-
-### Discovery #3: CLI Commands Were Read-Only Diagnostic Queries
-
-The curl commands in the issues document were **not data-modifying operations**. They were:
-
-```bash
-# These are all SELECT-only (immutable) operations:
-MATCH (n) RETURN count(n) AS nodes # ← Read-only
-MATCH ()-[r]->() RETURN count(r) AS rels # ← Read-only
-MATCH (f:FEATURE) RETURN f.id, f.name... # ← Read-only
-MATCH (t:TASK) RETURN t.status, count(*) # ← Read-only
-```
-
-**What they revealed:**
-- Memgraph DOES contain 809 nodes, 1359 relationships
-- Features and tasks exist in database
-- These nodes likely came from code-visual's own graph_rebuild
-- The database is the source of truth and is NOT broken
-
-**What they didn't change:**
-- The empty shared index
-- Engine states
-- Project context
-
----
-
-## Root Cause: The Triple-Mismatch Problem
-
-### The Actual Architecture Flaw
-
-```
-graph_rebuild() called
- ↓
-Orchestrator.build()
- ├─ Parses source files
- ├─ Creates internal GraphIndexManager (temporary)
- ├─ Populates: FILE, FUNCTION, CLASS nodes in internal index
- ├─ Generates Cypher statements (INSERT/MATCH/CREATE)
- ├─ Executes Cypher → Memgraph database updated ✅
- ├─ PROBLEM: Never syncs back to ToolContext.index ❌
- └─ Discards internal index
- ↓
-ToolContext.index remains empty
- ├─ graph_health reads from here → returns zeros ❌
- ├─ ProgressEngine reads from here → returns empty maps ❌
- ├─ All engines reference this empty index ❌
-
-Memgraph database is updated ✅
- ├─ Contains accurate nodes and relationships
- ├─ Is current and correct
- ├─ But is NOT synced to shared index ❌
-```
-
-### Why Code-Visual Works (Partially)
-
-Code-visual's proxy **bypasses the shared index entirely**:
-
-```
-code-visual frontend
- ↓
-memgraph-proxy.mjs (direct Bolt connection)
- ├─ Runs Cypher queries directly
- └─ Reads from Memgraph database (source of truth)
- ↓
- Data is accurate ✅
- But lxRAG tools still return empty/zero ❌
-```
-
-This explains the paradox: **code-visual's CLI queries show accurate data while lxRAG tools return empty results.**
-
----
-
-## Why Each Tool Actually Fails (Corrected)
-
-### Issue #1: `graph_health` reports zero graph entities
-
-**Root Cause (Corrected):**
-```typescript
-// From tool-handlers.ts:1782-1787
-const stats = this.context.index.getStatistics();
- ↑
- This is EMPTY from startup
- Never populated by orchestrator
-
-Result: Always returns zeros regardless of database state
-```
-
-**Why:** Not project-scoping issue, but **index synchronization issue**
-
-**Evidence:**
-- Memgraph contains 809 nodes (proven by CLI query)
-- Shared index is empty (proven by agent analysis)
-- Tool only reads from empty shared index (code inspection)
-
----
-
-### Issue #2: `feature_status` fails to resolve valid IDs
-
-**Root Cause (Corrected):**
-```typescript
-// From progress-engine.ts:76-91
-private loadFromGraph(): void {
- const featureNodes = this.index.getNodesByType("FEATURE");
- // ProgressEngine.features Map populated here
- // But ONLY if index has data
-
- // Problem: index is empty, so this.features is empty
-}
-
-// From tool-handlers.ts:1500
-const status = this.progressEngine!.getFeatureStatus(featureId);
-// Looks in empty this.features Map
-// Returns null for ANY featureId
-```
-
-**Why:** Not stale data, but **empty initial state that's never refilled**
-
-**Evidence:**
-- ProgressEngine initialized once at startup (empty index)
-- Features ARE in Memgraph (proven by CLI query: `code-visual:feature:phase-1`)
-- But they're not in ProgressEngine's Map (index was empty when loaded)
-
----
-
-### Issue #3: `progress_query` returns empty despite existing tasks
-
-**Root Cause (Corrected):**
-```typescript
-// From progress-engine.ts:94-108
-private loadFromGraph(): void {
- const taskNodes = this.index.getNodesByType("TASK");
- // this.tasks Map populated from index
-
- // Problem: index is empty, so this.tasks is empty
- // No refresh happens on project context switch
-}
-
-// Results in: query() returns empty items array
-```
-
-**Why:** Not stale project-scoped data, but **never-replenished empty data**
-
----
-
-## The CLI Commands Role: They Proved the Database is Healthy
-
-The curl commands in the updated issues document prove:
-
-✅ **Database is correct:**
-```
-curl: MATCH (n) RETURN count(n)
-→ 809 nodes exist in Memgraph
-```
-
-✅ **Features exist:**
-```
-curl: MATCH (f:FEATURE) RETURN f.id
-→ code-visual:feature:phase-1 exists
-```
-
-✅ **Tasks exist:**
-```
-curl: MATCH (t:TASK) RETURN t.status, count(*)
-→ 7 tasks with distribution: completed:3, in-progress:2, pending:2
-```
-
-❌ **lxRAG tools don't see this data:**
-```
-graph_health → totalNodes: 0
-feature_status("code-visual:feature:phase-1") → "not found"
-progress_query → items: []
-```
-
-**Conclusion:** The tools are broken due to empty/unsynced index, not database issues.
-
----
-
-## Code-Visual's Different Expectation
-
-Based on the README and architecture:
-
-**code-visual's Assumption:**
-- Direct Memgraph Bolt connection (memgraph-proxy.mjs)
-- Queries Memgraph directly, bypasses lxRAG tools
-- Expects accurate data from Memgraph ✅
-
-**What code-visual HOPED to use:**
-- lxRAG tools for operational insights
-- `graph_health` for readiness checks
-- `progress_query` for task dashboards
-- `feature_status` for feature tracking
-
-**What code-visual ACTUALLY gets:**
-- Direct Memgraph proxy works ✅
-- lxRAG tools return empty (not integrated)
-- Can't use lxRAG for operational dashboards ❌
-
----
-
-## Revised Fix Strategy (Critical Difference)
-
-### NOT A "Project-Scoping" Problem
-
-The original action plan focused on "project scoping divergence" - this was partially correct but missed the core issue.
-
-### The REAL Problem: Index Synchronization
-
-The shared index is **not populated** after builds. The fix must ensure:
-
-1. **After graph_rebuild:** Orchestrator's populated index syncs to shared index
-2. **On project switch:** Engines are refreshed with new project data
-3. **Overall:** Shared index becomes source of truth for engines
-
-### Fix Strategy Tiers
-
-#### TIER 1: Quick Fix (2-3 hours) - Make Tools Query-First
-
-**Instead of:**
-```typescript
-// graph_health reads from empty index
-const stats = this.context.index.getStatistics();
-return { totalNodes: 0 }; // Always zero
-```
-
-**Do this:**
-```typescript
-// Query Memgraph for authoritative counts
-const result = await this.context.memgraph.executeCypher(
- "MATCH (n {projectId: $projectId}) RETURN count(n) AS total",
- { projectId }
-);
-return { totalNodes: result.data[0].total };
-```
-
-**Impact:**
-- ✅ Fixes Issue #1: graph_health (uses Cypher, not empty index)
-- ✅ Partial fix for other tools (they can also query Memgraph)
-- ⏱ Fastest implementation
-- ⚠ Not ideal long-term (engines still use empty index)
-
-#### TIER 2: Proper Fix (4-6 hours) - Sync Index After Build
-
-**After orchestrator.build():**
-```typescript
-// Copy orchestrator's populated index to shared index
-this.context.index = orchestrator.index;
-// or sync the data:
-orchestrator.index.getAllNodes().forEach(node =>
- this.context.index.addNode(node.id, node.type, node.properties)
-);
-```
-
-**Impact:**
-- ✅ Fixes all three issues at source
-- ✅ ProgressEngine gets real data
-- ✅ TestEngine gets real data
-- ✅ Embedding generation works
-- ⏱ More complex, requires build pipeline changes
-- ✅ Better long-term solution
-
-#### TIER 3: Full Refactor (8+ hours) - Multi-Project Support
-
-Split index by projectId (see ACTION_PLAN_LXRAG_TOOL_FIXES.md for details)
-
----
-
-## Implementation Path (Revised)
-
-### Phase 1: Immediate (Use TIER 1 + TIER 2 combined)
-
-**Step 1.1: Add Index Sync After Build**
-- **File:** `src/graph/orchestrator.ts` (build method)
-- **Change:** After successful build, sync internal index to shared index
- ```typescript
- async build(): Promise {
- // ... existing build code ...
-
- // NEW: Sync populated index to shared context
- if (this.sharedIndex) {
- this.index.getAllNodes().forEach(node => {
- this.sharedIndex.addNode(node.id, node.type, node.properties);
- });
- this.index.getAllRelationships().forEach(rel => {
- this.sharedIndex.addRelationship(rel.id, rel.from, rel.to,
- rel.type, rel.properties);
- });
- }
-
- return result;
- }
- ```
-
-**Step 1.2: Make graph_health Query-First**
-- **File:** `src/tools/tool-handlers.ts` (graph_health method)
-- **Change:** Query Memgraph for authoritative counts
- ```typescript
- async graph_health(): Promise {
- const { projectId } = this.getActiveProjectContext();
-
- // Query database instead of empty index
- const countResult = await this.context.memgraph.executeCypher(
- "MATCH (n {projectId: $projectId}) RETURN count(n) AS total",
- { projectId }
- );
-
- return this.formatSuccess({
- graphIndex: {
- totalNodes: countResult.data[0].total || 0,
- // ...
- }
- });
- }
- ```
-
-**Step 1.3: Reload Engines on Project Context Change**
-- **File:** `src/tools/tool-handlers.ts`
-- **Change:** When project switches, refresh engines
- ```typescript
- private setActiveProjectContext(context: ProjectContext): void {
- // ... existing code ...
-
- // NEW: Reload engines with new context
- this.progressEngine?.reload(this.context.index, context.projectId);
- this.testEngine?.reload(this.context.index, context.projectId);
- }
- ```
-
-### Phase 2: Validation (1-2 hours)
-
-Test against code-visual's data:
-```
-Before: graph_health → { totalNodes: 0 }
-After: graph_health → { totalNodes: 809 }
-
-Before: feature_status("code-visual:feature:phase-1") → "not found"
-After: feature_status("code-visual:feature:phase-1") → { feature: {...} }
-
-Before: progress_query(status="all") → { items: [], totalCount: 0 }
-After: progress_query(status="all") → { items: [7 tasks], totalCount: 7 }
-```
-
-### Phase 3: Long-term (Design fixes)
-
-See ACTION_PLAN_LXRAG_TOOL_FIXES.md for architectural improvements
-
----
-
-## Why This is Different from Original Plan
-
-| Aspect | Original Plan | Revised Plan |
-|--------|---|---|
-| **Root Cause** | Project-scoping divergence | Index synchronization failure |
-| **Index State** | "Global and unscoped" | "Empty and never synced" |
-| **Primary Issue** | Data mixed across projects | Data never populated from build |
-| **Tool Strategy** | Add projectId filtering | Query Memgraph + sync index |
-| **Effort** | 3-4 hours Phase 1 | 2-3 hours Phase 1 |
-| **Effectiveness** | Fixes scoping | Fixes all three issues |
-
----
-
-## Validation Against CLI Commands
-
-The CLI commands in the issues document serve as **proof that the fix works**:
-
-```bash
-# After implementing Phase 1 fixes:
-
-# CLI query (always worked):
-curl -s -X POST http://localhost:4001/query \
- -d '{"query":"MATCH (n) RETURN count(n)"}'
-→ { rows: [{ n: 809 }] }
-
-# lxRAG tool (NOW FIXED):
-graph_health()
-→ { graphIndex: { totalNodes: 809 } }
-
-# CLI query for features:
-curl: MATCH (f:FEATURE) RETURN f.id
-→ code-visual:feature:phase-1
-
-# lxRAG tool (NOW FIXED):
-feature_status("code-visual:feature:phase-1")
-→ { feature: { id: "...", name: "..." } }
-```
-
----
-
-## Acceptance Criteria (Updated)
-
-**Test these exact scenarios:**
-
-1. **graph_health matches CLI counts:**
- ```
- CLI: MATCH (n {projectId: "code-visual"}) RETURN count(n)
- → 809
-
- Tool: graph_health()
- → { graphIndex: { totalNodes: 809 } }
- ```
-
-2. **feature_status resolves known IDs:**
- ```
- Known ID: code-visual:feature:phase-1 (from CLI query)
-
- Tool: feature_status("code-visual:feature:phase-1")
- → { success: true, feature: {...} }
- ```
-
-3. **progress_query shows all tasks:**
- ```
- CLI: MATCH (t:TASK) RETURN count(t)
- → 7 total
-
- Tool: progress_query(status="all")
- → { items: [7 tasks], totalCount: 7 }
- ```
-
----
-
-## Summary of Key Insights
-
-1. **The database is healthy** - CLI commands prove 809 nodes exist ✅
-2. **The tools are broken** - They read from empty shared index ❌
-3. **The index is never synced** - Orchestrator build doesn't sync to shared index ❌
-4. **code-visual works around this** - Direct Memgraph bypass ✅
-5. **The fix is synchronization, not scoping** - Sync orchestrator index after build ✅
-
----
-
-## File References
-
-### Original Issues & Analysis
-- `docs/lxrag-tool-issues.md` - Updated with CLI commands
-- `docs/ACTION_PLAN_LXRAG_TOOL_FIXES.md` - Previous analysis (still valid for architectural improvements)
-
-### New Deep-Dive Documents
-- `GRAPH_STATE_SUMMARY.md` - Executive summary of graph architecture
-- `GRAPH_STATE_ANALYSIS.md` - Complete technical analysis with code references
-- `GRAPH_STATE_DIAGRAMS.md` - Architecture diagrams
-- `GRAPH_STATE_FIXES.md` - All four fix tiers with code examples
-
-### Implementation Files
-- `src/graph/orchestrator.ts` - Where index sync must happen
-- `src/tools/tool-handlers.ts` - Where Cypher queries replace index reads
-- `src/engines/progress-engine.ts` - Where reload() methods added
-- `src/graph/index.ts` - Consider adding sync/clear methods
-
diff --git a/docs/TOOLS_INFORMATION_GUIDE.md b/docs/TOOLS_INFORMATION_GUIDE.md
new file mode 100644
index 0000000..1140080
--- /dev/null
+++ b/docs/TOOLS_INFORMATION_GUIDE.md
@@ -0,0 +1,226 @@
+# Tools Information Guide
+
+## Purpose
+
+This document consolidates tool-level information scattered across the repository into one operational reference:
+
+- What tools exist now.
+- How they are grouped.
+- How to choose the right tool quickly.
+- What runtime assumptions affect tool behavior.
+
+---
+
+## Current Tool Inventory (Authoritative)
+
+Based on the built runtime registry (`dist/tools/registry.js`), the server currently exposes **39 tools**.
+
+### Category counts
+
+| Category | Count |
+| ------------ | -----: |
+| graph | 4 |
+| utility | 3 |
+| code | 7 |
+| test | 5 |
+| coordination | 5 |
+| setup | 2 |
+| arch | 2 |
+| docs | 2 |
+| ref | 1 |
+| task | 4 |
+| memory | 4 |
+| **Total** | **39** |
+
+### Complete tool list
+
+#### Graph
+
+- `graph_query`
+- `graph_rebuild`
+- `graph_set_workspace`
+- `graph_health`
+
+#### Utility
+
+- `diff_since`
+- `tools_list`
+- `contract_validate`
+
+#### Code intelligence
+
+- `code_explain`
+- `find_pattern`
+- `semantic_search`
+- `find_similar_code`
+- `code_clusters`
+- `semantic_diff`
+- `semantic_slice`
+
+#### Test intelligence
+
+- `test_select`
+- `test_categorize`
+- `impact_analyze`
+- `test_run`
+- `suggest_tests`
+
+#### Coordination
+
+- `context_pack`
+- `agent_claim`
+- `agent_release`
+- `agent_status`
+- `coordination_overview`
+
+#### Setup
+
+- `init_project_setup`
+- `setup_copilot_instructions`
+
+#### Architecture
+
+- `arch_validate`
+- `arch_suggest`
+
+#### Documentation
+
+- `index_docs`
+- `search_docs`
+
+#### Reference
+
+- `ref_query`
+
+#### Task / progress
+
+- `progress_query`
+- `task_update`
+- `feature_status`
+- `blocking_issues`
+
+#### Memory
+
+- `episode_add`
+- `episode_recall`
+- `decision_query`
+- `reflect`
+
+---
+
+## Tool Selection Cheatsheet
+
+### Use graph tools when you need structural truth
+
+- Start with `graph_set_workspace` + `graph_rebuild`.
+- Use `graph_health` to verify readiness.
+- Use `graph_query` for natural/cypher discovery.
+
+### Use code tools for understanding and retrieval
+
+- `code_explain` for dependency-aware symbol explanation.
+- `semantic_*` tools for similarity and ranked slices.
+- `find_pattern` for violations, circularity, and pattern checks.
+
+### Use test tools to reduce execution cost
+
+- `impact_analyze` before running tests.
+- `test_select` to scope execution.
+- `test_run` only on selected suites.
+
+### Use memory and coordination for multi-agent continuity
+
+- `episode_add` / `episode_recall` for persistent memory.
+- `agent_claim` / `agent_release` to avoid collision.
+- `context_pack` when entering a task or handoff.
+
+### Use setup tools at session start
+
+- `init_project_setup` when onboarding a repo quickly.
+- `setup_copilot_instructions` when scaffolding assistant behavior docs.
+
+---
+
+## Runtime Notes That Affect Tool Behavior
+
+1. **Session-scoped context**
+ - Workspace/project context is tied to MCP session.
+ - Re-initialize session tools after reconnect/restart.
+
+2. **Asynchronous rebuild model**
+ - `graph_rebuild` may return queued/completed state depending on threshold and load.
+ - Poll with `graph_health` until graph/index state is stable.
+
+3. **Engine availability is contextual**
+ - Some outputs degrade when Memgraph/Qdrant are disconnected.
+ - `errorEnvelope` responses often include recoverable hints.
+
+4. **Profile-driven output shaping**
+ - `compact` is optimized for low token budgets.
+ - `balanced` and `debug` surface progressively more details.
+
+---
+
+## Inputs and Output Contracts
+
+### Contract validation
+
+- Use `contract_validate` when integrating new clients.
+- It normalizes arguments and returns warnings before execution.
+
+### Response shaping
+
+- Standard response profile controls live in `src/response` (`budget`, `schemas`, `shaper`).
+- Expected envelope shape: success/error + profile + summary + data.
+
+---
+
+## Known Operational Pitfalls
+
+From audit history and integration docs:
+
+- Calling analysis tools before `graph_rebuild` completes.
+- Using wrong workspace path (`/workspace` in container vs native host path).
+- Assuming one global state across multiple MCP sessions.
+- Ignoring drift/health diagnostics before troubleshooting higher-level tools.
+
+---
+
+## External Standards Context (Research)
+
+To keep this tool layer future-proof, the implementation aligns with:
+
+1. **MCP specification (2025-06-18)**
+ - JSON-RPC based protocol.
+ - Stateful capability negotiation.
+ - Tool/resource/prompt model and trust-safety constraints.
+
+2. **Memgraph capabilities**
+ - Cypher querying and graph algorithm ecosystem (MAGE).
+ - Text and vector search support in querying surfaces.
+ - Production deployment support (Docker/K8s/cloud, HA guidance).
+
+3. **Qdrant capabilities**
+ - AI-native vector database for semantic retrieval.
+ - Collection-based indexing and performance optimization pathways.
+ - Local and cloud quickstart paths.
+
+---
+
+## Canonical Sources
+
+Primary internal references:
+
+- `README.md`
+- `QUICK_REFERENCE.md`
+- `QUICK_START.md`
+- `ARCHITECTURE.md`
+- `docs/MCP_INTEGRATION_GUIDE.md`
+- `docs/TOOL_PATTERNS.md`
+- `src/tools/registry.ts`
+
+External references consulted:
+
+- https://modelcontextprotocol.io/specification/2025-06-18
+- https://memgraph.com/docs
+- https://qdrant.tech/documentation/
diff --git a/docs/brain-logo.svg b/docs/brain-logo.svg
index 78f7e70..74194ff 100644
--- a/docs/brain-logo.svg
+++ b/docs/brain-logo.svg
@@ -1,114 +1,55 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/copilot-instructions-template.md b/docs/copilot-instructions-template.md
deleted file mode 100644
index 49913ac..0000000
--- a/docs/copilot-instructions-template.md
+++ /dev/null
@@ -1,65 +0,0 @@
-# Copilot Instructions - lxRAG MCP Server (Template)
-
-**Copy this to `.github/copilot-instructions.md` and customize for your project.**
-
----
-
-## 🎯 Core Rules
-
-1. **Use MCP tools** for all code intelligence — never read files directly
-2. **Initialize first**: Call `graph_set_workspace(workspaceRoot, projectId)` then `graph_health()`
-3. **Re-anchor regularly**: Call `graph_health()` every 5 messages to prevent drift
-
----
-
-## 📊 Tool Quick Reference
-
-| Question | Tool | Example |
-| -------------- | -------------------------------- | ------------------------ |
-| Find code | `graph_query` | "find all HTTP handlers" |
-| Understand | `code_explain` | Symbol or class name |
-| Impact | `impact_analyze` | Changed file paths |
-| Which tests | `test_select` | Changed file paths |
-| Search concept | `semantic_search` | "validation patterns" |
-| Architecture | `arch_validate` / `arch_suggest` | File path |
-| Remember | `episode_add` | Important decision |
-
-**Full reference**: [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) (38 tools)
-
----
-
-## 🔄 Session Flow
-
-**First message (required):**
-
-```json
-{
- "tool": "graph_set_workspace",
- "args": {
- "workspaceRoot": "/path/to/project",
- "projectId": "project-id",
- "sourceDir": "src"
- }
-}
-```
-
-Then call `graph_health()` and proceed.
-
-**Every 5th message**: Re-anchor with `graph_health()`.
-
----
-
-## 📋 Your Project
-
-- **Project**: [YOUR_PROJECT_NAME]
-- **Workspace**: [YOUR_WORKSPACE_PATH]
-- **Project ID**: [YOUR_PROJECT_ID]
-
----
-
-## 📚 Need Help?
-
-- [MCP Integration Guide](./docs/MCP_INTEGRATION_GUIDE.md)
-- [Tool Patterns](./docs/TOOL_PATTERNS.md)
-- [Architecture](./ARCHITECTURE.md)
-- [Quick Reference](./QUICK_REFERENCE.md)
diff --git a/docs/lxrag-tool-issues.md b/docs/lxrag-tool-issues.md
deleted file mode 100644
index 2a89926..0000000
--- a/docs/lxrag-tool-issues.md
+++ /dev/null
@@ -1,147 +0,0 @@
-# lxRAG Tool Issues (Session Findings)
-
-## Scope
-This document lists issues directly related to lxRAG tools observed during the current validation session in `code-visual`.
-
-- Date: 2026-02-22
-- Workspace: `/home/alex_rod/projects/code-visual`
-- Project ID: `code-visual`
-- Data source used for comparison: live Memgraph queries through `http://localhost:4001/query`
-
-## Summary
-Three lxRAG tool-level inconsistencies were reproduced against live graph data:
-
-1. `mcp_lxrag_graph_health` reports an empty graph index while graph data exists.
-2. `mcp_lxrag_feature_status` cannot resolve valid feature IDs present in the graph.
-3. `mcp_lxrag_progress_query` returns no items while `TASK` nodes exist with valid statuses.
-
-## CLI Cypher Commands Used in This Session
-The graph checks and node/state validation in this session were executed from the command line via the proxy endpoint (`curl -> http://localhost:4001/query`).
-
-Important: these graph observations came from CLI-applied Cypher queries; they were not created/validated exclusively by lxRAG tool responses.
-
-### Commands run from terminal
-
-#### Feature/task-target check
-```bash
-curl -s -X POST http://localhost:4001/query -H "Content-Type: application/json" -d '{"query":"MATCH (f:FEATURE {id:\"code-visual:feature:split-canvas-viewer\"}) OPTIONAL MATCH (t:TASK)-[:APPLIES_TO]->(f) OPTIONAL MATCH (f)-[:TARGETS]->(x) RETURN f.name as feature, f.status as status, count(DISTINCT t) as taskCount, count(DISTINCT x) as targetCount"}'
-```
-
-#### Global graph counts
-```bash
-curl -s -X POST http://localhost:4001/query -H 'Content-Type: application/json' -d '{"query":"MATCH (n) RETURN count(n) AS nodes"}'
-
-curl -s -X POST http://localhost:4001/query -H 'Content-Type: application/json' -d '{"query":"MATCH ()-[r]->() RETURN count(r) AS rels"}'
-```
-
-#### Top label distribution
-```bash
-curl -s -X POST http://localhost:4001/query -H 'Content-Type: application/json' -d '{"query":"MATCH (n) RETURN labels(n) AS labels, count(*) AS c ORDER BY c DESC LIMIT 15"}'
-```
-
-#### Feature inventory and project scoping
-```bash
-curl -s -X POST http://localhost:4001/query -H 'Content-Type: application/json' -d '{"query":"MATCH (f:FEATURE) RETURN f.id AS id, f.name AS name, f.status AS status, f.projectId AS projectId ORDER BY id"}'
-
-curl -s -X POST http://localhost:4001/query -H 'Content-Type: application/json' -d '{"query":"MATCH (n) RETURN count(n.projectId) AS withProjectId, count(*) AS total"}'
-```
-
-#### Task totals and status breakdown
-```bash
-curl -s -X POST http://localhost:4001/query -H 'Content-Type: application/json' -d '{"query":"MATCH (t:TASK) RETURN count(t) AS taskCount"}'
-
-curl -s -X POST http://localhost:4001/query -H 'Content-Type: application/json' -d '{"query":"MATCH (t:TASK) RETURN t.status AS status, count(*) AS c ORDER BY c DESC"}'
-```
-
-## Issues
-
-### 1) `mcp_lxrag_graph_health` reports zero indexed graph entities
-
-**Observed (tool):**
-- `graphIndex.totalNodes = 0`
-- `graphIndex.totalRelationships = 0`
-- `indexedFiles = 0`
-
-**Observed (live graph):**
-- `MATCH (n) RETURN count(n)` → `809`
-- `MATCH ()-[r]->() RETURN count(r)` → `1359`
-
-**Impact:**
-- Health checks are misleading and cannot be used as a readiness signal.
-- Any feature depending on index stats may be incorrectly gated or disabled.
-
-**Repro:**
-1. Run `mcp_lxrag_graph_set_workspace` with `projectId=code-visual`.
-2. Run `mcp_lxrag_graph_health`.
-3. Compare with direct counts from Memgraph via proxy endpoint.
-
-**Likely root cause (hypothesis):**
-- Tool reads from a different index/state than the active Memgraph project context, or project-scoped filtering is not applied consistently.
-
----
-
-### 2) `mcp_lxrag_feature_status` fails on valid feature IDs
-
-**Observed (tool):**
-- `Feature not found: code-visual:feature:phase-1`
-- `Feature not found: code-visual:feature:simplification-phase-4`
-
-**Observed (live graph):**
-- `MATCH (f:FEATURE) RETURN f.id, f.name, f.status` returns those IDs and metadata.
-
-**Impact:**
-- Feature dashboards and status widgets based on this tool show false negatives.
-- Automation relying on feature existence cannot progress reliably.
-
-**Repro:**
-1. Confirm feature IDs with direct query:
- - `MATCH (f:FEATURE) RETURN f.id, f.name, f.status ORDER BY f.id`
-2. Run `mcp_lxrag_feature_status` for one returned ID.
-3. Tool still reports "not found".
-
-**Likely root cause (hypothesis):**
-- Feature lookup path uses a mismatched namespace/index source or incorrect project scoping.
-
----
-
-### 3) `mcp_lxrag_progress_query` returns empty despite existing tasks
-
-**Observed (tool):**
-- `items: []`
-- `totalCount: 0`
-
-**Observed (live graph):**
-- `MATCH (t:TASK) RETURN count(t)` → `7`
-- `MATCH (t:TASK) RETURN t.status, count(*)` → `completed:3`, `in-progress:2`, `pending:2`
-
-**Impact:**
-- Progress/reporting views may display no work in flight even when tasks exist.
-- Status summaries become unreliable for planning workflows.
-
-**Repro:**
-1. Run `mcp_lxrag_progress_query` with a broad query (status `all`).
-2. Compare with direct `TASK` counts in Memgraph.
-
-**Likely root cause (hypothesis):**
-- Query adapter maps request contract correctly but reads from a stale or differently scoped source.
-
-## Cross-Issue Pattern
-All three issues indicate a probable **read-path divergence** between lxRAG tools and the live Memgraph graph used by the app.
-
-## Temporary Workarounds
-- Use direct Memgraph queries via proxy for operational checks:
- - Node/relationship counts
- - Feature existence/status
- - Task totals and status breakdown
-- Treat `graph_health`, `feature_status`, and `progress_query` as non-authoritative until parity is restored.
-
-## Recommended Fix Order
-1. Validate tool data source and project scoping (`projectId=code-visual`) for all three tools.
-2. Add parity tests that compare tool responses vs direct graph queries for canonical fixtures.
-3. Add diagnostics to each tool response (effective projectId, source, index generation timestamp).
-4. Re-run parity checks and mark these issues closed only after exact-match thresholds are met.
-
-## Acceptance Criteria for Resolution
-- `mcp_lxrag_graph_health` reports non-zero graph index values that match direct graph counts within expected tolerance.
-- `mcp_lxrag_feature_status` resolves known IDs from `FEATURE` nodes in current project scope.
-- `mcp_lxrag_progress_query` returns item counts and status distribution consistent with `TASK` nodes in graph.
diff --git a/docs/skill-mcp-template.md b/docs/skill-mcp-template.md
deleted file mode 100644
index d1e9f35..0000000
--- a/docs/skill-mcp-template.md
+++ /dev/null
@@ -1,61 +0,0 @@
----
-name: lx
-description: Use lxRAG-MCP code graph tools to explore the codebase, assess change impact, validate architecture, and recall past decisions.
-argument-hint: "task description or file path"
----
-
-# lxRAG-MCP Code Graph Analysis
-
-## Session Init (required once per session)
-
-```
-graph_set_workspace(workspaceRoot, projectId, sourceDir)
-graph_health()
-```
-
-Re-anchor every 5 messages: `graph_health()`
-
----
-
-## Tool Reference
-
-| Goal | Tool | Input |
-|---|---|---|
-| Find code | `graph_query` | pattern string |
-| Search concept | `semantic_search` | concept string |
-| Understand code | `code_explain` | symbol / file |
-| Find usages | `usages` | symbol / file |
-| Change impact | `impact_analyze` | file list |
-| Relevant tests | `test_select` | file list |
-| Validate design | `arch_validate` | file / path |
-| Suggest design | `arch_suggest` | context |
-| Save decision | `episode_add` | key + payload |
-| Recall decision | `episode_query` | topic string |
-
----
-
-## Workflows
-
-**Before any change:**
-1. `graph_health()` — verify sync
-2. `impact_analyze(files)` — see scope
-3. `test_select(files)` — know which tests to run
-
-**Exploring unknown code:**
-1. `graph_query("describe what you're looking for")`
-2. `code_explain(match)` — understand implementation
-3. `usages(match)` — see dependencies
-
-**Architecture decision:**
-1. `arch_suggest(context)` — get options
-2. `arch_validate(files)` — confirm alignment
-3. `episode_add(decision, rationale)` — persist choice
-
----
-
-## Rules
-
-- Never read files directly — always use MCP tools
-- Always init with `graph_set_workspace` + `graph_health` first
-- Run `impact_analyze` before modifying files
-- Store non-obvious decisions with `episode_add`
diff --git a/docs/social-preview.png b/docs/social-preview.png
new file mode 100644
index 0000000..b0dd415
Binary files /dev/null and b/docs/social-preview.png differ
diff --git a/docs/social-preview.svg b/docs/social-preview.svg
new file mode 100644
index 0000000..92dd381
--- /dev/null
+++ b/docs/social-preview.svg
@@ -0,0 +1,364 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MCP SERVER
+
+
+ lxDIG
+
+
+ MCP
+
+
+ Stop RAGing, start DIGging.
+
+
+
+
+
+
+ Code Graph Intelligence · Persistent Agent Memory
+
+
+
+
+ Multi-Agent Coordination · Beyond Static RAG
+
+
+
+
+
+ Memgraph
+
+
+
+ Qdrant
+
+
+
+ TypeScript
+
+
+
+ MIT
+
+
+
+ 39
+ tools ·
+ 402
+ tests · stdio & HTTP transport
+
+
+
+
+ Works with: Claude Code · VS Code Copilot · Cursor · Claude Desktop
+
+
+
+ github.com/lexCoder2/lexRAG-MCP
+
+
diff --git a/docs/GRAPH_EXPERT_AGENT.md b/docs/templates/GRAPH_EXPERT_AGENT.md
similarity index 99%
rename from docs/GRAPH_EXPERT_AGENT.md
rename to docs/templates/GRAPH_EXPERT_AGENT.md
index ee559bd..9a9c788 100644
--- a/docs/GRAPH_EXPERT_AGENT.md
+++ b/docs/templates/GRAPH_EXPERT_AGENT.md
@@ -15,7 +15,7 @@ You are the **Graph Expert Agent** for this project. Your goal is to produce acc
- Core workflow: initialize session → set project context → rebuild graph → query tools
- Graph rebuild is async (`status: QUEUED`), so results may lag for a few seconds
- **Parsers**: TypeScript, TSX, JavaScript (`.js`/`.mjs`/`.cjs`), JSX, Python, Go, Rust, Java
- - Set `LXRAG_USE_TREE_SITTER=true` for AST-accurate tree-sitter parsers; graceful per-language fallback otherwise
+ - Set `LXDIG_USE_TREE_SITTER=true` for AST-accurate tree-sitter parsers; graceful per-language fallback otherwise
- **MAGE algorithms**: Leiden community detection and PageRank PPR (both with JS fallback)
- **SCIP IDs**: `scipId` field on all FILE, FUNCTION, CLASS nodes for cross-tool symbol references
diff --git a/docs/templates/copilot-instructions-template.md b/docs/templates/copilot-instructions-template.md
new file mode 100644
index 0000000..4ac92dc
--- /dev/null
+++ b/docs/templates/copilot-instructions-template.md
@@ -0,0 +1,71 @@
+# Copilot Instructions - lxDIG MCP Server (Template)
+
+Copy this file to `.github/copilot-instructions.md` in your project and replace placeholders.
+
+---
+
+## Core Rules
+
+1. Use MCP tools first for code intelligence and dependency analysis.
+2. Initialize session context before deep analysis.
+3. Re-anchor with `graph_health()` periodically on long threads.
+
+---
+
+## Recommended Init Flow
+
+Preferred one-shot setup:
+
+```json
+{
+ "tool": "init_project_setup",
+ "args": {
+ "workspaceRoot": "/path/to/project",
+ "projectId": "project-id",
+ "sourceDir": "src",
+ "rebuildMode": "incremental"
+ }
+}
+```
+
+Alternative explicit flow:
+
+1. `graph_set_workspace(workspaceRoot, projectId, sourceDir)`
+2. `graph_rebuild(mode="incremental")`
+3. `graph_health()`
+
+Long threads: call `graph_health()` every ~5 messages.
+
+---
+
+## Tool Quick Reference
+
+| Question | Tool | Example |
+| --------------------- | ----------------------------------- | ------------------------ |
+| Find code | `graph_query` | "find all HTTP handlers" |
+| Understand symbol | `code_explain` | Symbol, class, or file |
+| Impact before edit | `impact_analyze` | Changed file list |
+| Select tests | `test_select` | Changed file list |
+| Search by concept | `semantic_search` | "validation patterns" |
+| Validate architecture | `arch_validate` / `arch_suggest` | File or module context |
+| Persist decisions | `episode_add` | Decision + rationale |
+| Recall decisions | `decision_query` / `episode_recall` | Topic or task |
+
+Full reference: [QUICK_REFERENCE.md](../QUICK_REFERENCE.md) (39 tools)
+
+---
+
+## Project Placeholders
+
+- Project: `[YOUR_PROJECT_NAME]`
+- Workspace root: `[YOUR_WORKSPACE_PATH]`
+- Project ID: `[YOUR_PROJECT_ID]`
+
+---
+
+## Related Docs
+
+- [MCP Integration Guide](../MCP_INTEGRATION_GUIDE.md)
+- [Tool Patterns](../TOOL_PATTERNS.md)
+- [Tools Information Guide](../TOOLS_INFORMATION_GUIDE.md)
+- [Quick Reference](../../QUICK_REFERENCE.md)
diff --git a/docs/templates/skill-mcp-template.md b/docs/templates/skill-mcp-template.md
new file mode 100644
index 0000000..f0e5309
--- /dev/null
+++ b/docs/templates/skill-mcp-template.md
@@ -0,0 +1,64 @@
+---
+name: lx
+description: Use lxDIG MCP code graph tools to explore the codebase, assess change impact, validate architecture, and recall past decisions.
+argument-hint: "task description or file path"
+---
+
+# lxDIG MCP Code Graph Analysis
+
+## Session Init (required once per session)
+
+```
+init_project_setup(workspaceRoot, projectId, sourceDir)
+graph_health()
+```
+
+Re-anchor every 5 messages: `graph_health()`
+
+---
+
+## Tool Reference
+
+| Goal | Tool | Input |
+| ------------------- | ----------------------------------- | -------------- |
+| Find code | `graph_query` | pattern string |
+| Search concept | `semantic_search` | concept string |
+| Understand code | `code_explain` | symbol / file |
+| Trace patterns/deps | `find_pattern` | pattern + type |
+| Change impact | `impact_analyze` | file list |
+| Relevant tests | `test_select` | file list |
+| Validate design | `arch_validate` | file / path |
+| Suggest design | `arch_suggest` | context |
+| Save decision | `episode_add` | key + payload |
+| Recall decision | `decision_query` / `episode_recall` | topic string |
+
+---
+
+## Workflows
+
+**Before any change:**
+
+1. `graph_health()` — verify sync
+2. `impact_analyze(files)` — see scope
+3. `test_select(files)` — know which tests to run
+
+**Exploring unknown code:**
+
+1. `graph_query("describe what you're looking for")`
+2. `code_explain(match)` — understand implementation
+3. `find_pattern("dependencies", "pattern")` — inspect relationships
+
+**Architecture decision:**
+
+1. `arch_suggest(context)` — get options
+2. `arch_validate(files)` — confirm alignment
+3. `episode_add(decision, rationale)` — persist choice
+
+---
+
+## Rules
+
+- Never read files directly — always use MCP tools
+- Always init with `init_project_setup` or `graph_set_workspace` + `graph_rebuild`
+- Run `impact_analyze` before modifying files
+- Store non-obvious decisions with `episode_add`
diff --git a/docs/templates/toolsets-template.jsonc b/docs/templates/toolsets-template.jsonc
new file mode 100644
index 0000000..90361a2
--- /dev/null
+++ b/docs/templates/toolsets-template.jsonc
@@ -0,0 +1,89 @@
+// lxRAG-MCP Tool Sets Template
+// Location: .vscode/mcp-toolsets.jsonc
+// Usage: type #set-name in Copilot chat to activate a group of tools.
+// Run "Chat: Configure Tool Sets" from the Command Palette to register.
+
+{
+ // Explore: understand code, find definitions, search by concept
+ "lx-explore": {
+ "tools": [
+ "graph_query",
+ "semantic_search",
+ "code_explain",
+ "semantic_slice",
+ "find_pattern",
+ ],
+ "description": "Read and explore the codebase using the code graph and semantic search",
+ "icon": "search",
+ },
+
+ // Impact: assess risk before changing anything
+ "lx-impact": {
+ "tools": [
+ "impact_analyze",
+ "test_select",
+ "test_categorize",
+ "test_run",
+ "blocking_issues",
+ ],
+ "description": "Analyze impact, select relevant tests, and inspect blockers",
+ "icon": "graph",
+ },
+
+ // Design: validate and improve architecture
+ "lx-design": {
+ "tools": ["arch_validate", "arch_suggest"],
+ "description": "Validate architectural decisions and get design improvement suggestions",
+ "icon": "layers",
+ },
+
+ // Memory: persist and recall decisions across sessions
+ "lx-memory": {
+ "tools": ["episode_add", "episode_recall", "decision_query", "reflect"],
+ "description": "Store and retrieve decisions, context, and learnings across sessions",
+ "icon": "database",
+ },
+
+ // Session: initialize and health-check the graph connection
+ "lx-session": {
+ "tools": [
+ "init_project_setup",
+ "graph_set_workspace",
+ "graph_rebuild",
+ "graph_health",
+ ],
+ "description": "Initialize workspace context and verify graph/index health",
+ "icon": "server",
+ },
+
+ // Full: all graph analysis tools — use for complex multi-step tasks
+ "lx": {
+ "tools": [
+ "init_project_setup",
+ "graph_set_workspace",
+ "graph_rebuild",
+ "graph_health",
+ "graph_query",
+ "semantic_search",
+ "code_explain",
+ "semantic_slice",
+ "semantic_diff",
+ "find_pattern",
+ "impact_analyze",
+ "test_select",
+ "test_categorize",
+ "test_run",
+ "blocking_issues",
+ "arch_validate",
+ "arch_suggest",
+ "episode_add",
+ "episode_recall",
+ "decision_query",
+ "reflect",
+ "tools_list",
+ "contract_validate",
+ ],
+ "description": "All code graph analysis tools for comprehensive codebase intelligence",
+ "icon": "symbol-misc",
+ },
+}
diff --git a/docs/toolsets-template.jsonc b/docs/toolsets-template.jsonc
deleted file mode 100644
index a0e7189..0000000
--- a/docs/toolsets-template.jsonc
+++ /dev/null
@@ -1,54 +0,0 @@
-// lxRAG-MCP Tool Sets Template
-// Location: .vscode/mcp-toolsets.jsonc
-// Usage: type #set-name in Copilot chat to activate a group of tools.
-// Run "Chat: Configure Tool Sets" from the Command Palette to register.
-
-{
- // Explore: understand code, find definitions, search by concept
- "lx-explore": {
- "tools": ["graph_query", "semantic_search", "code_explain", "usages", "changes"],
- "description": "Read and explore the codebase using the code graph and semantic search",
- "icon": "search"
- },
-
- // Impact: assess risk before changing anything
- "lx-impact": {
- "tools": ["impact_analyze", "test_select", "problems"],
- "description": "Analyze change impact, select affected tests, and surface active problems",
- "icon": "graph"
- },
-
- // Design: validate and improve architecture
- "lx-design": {
- "tools": ["arch_validate", "arch_suggest"],
- "description": "Validate architectural decisions and get design improvement suggestions",
- "icon": "layers"
- },
-
- // Memory: persist and recall decisions across sessions
- "lx-memory": {
- "tools": ["episode_add", "episode_query"],
- "description": "Store and retrieve decisions, context, and learnings across sessions",
- "icon": "database"
- },
-
- // Session: initialize and health-check the graph connection
- "lx-session": {
- "tools": ["graph_set_workspace", "graph_health"],
- "description": "Initialize the code graph workspace and verify sync status",
- "icon": "server"
- },
-
- // Full: all graph analysis tools — use for complex multi-step tasks
- "lx": {
- "tools": [
- "graph_set_workspace", "graph_health",
- "graph_query", "semantic_search", "code_explain", "usages", "changes",
- "impact_analyze", "test_select", "problems",
- "arch_validate", "arch_suggest",
- "episode_add", "episode_query"
- ],
- "description": "All code graph analysis tools for comprehensive codebase intelligence",
- "icon": "symbol-misc"
- }
-}
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..1e51225
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,55 @@
+// @ts-check
+import eslint from "@eslint/js";
+import tseslint from "typescript-eslint";
+import prettierConfig from "eslint-config-prettier";
+
+export default tseslint.config(
+ eslint.configs.recommended,
+ ...tseslint.configs.recommended,
+ prettierConfig,
+ {
+ // Apply to all TypeScript source files
+ files: ["src/**/*.ts"],
+ rules: {
+ // Allow `any` with a warning — reduce count over time via 1.4 type hardening
+ "@typescript-eslint/no-explicit-any": "warn",
+ // Catch genuinely unused variables (parameters excluded — too noisy)
+ "@typescript-eslint/no-unused-vars": [
+ "error",
+ {
+ argsIgnorePattern: "^_",
+ varsIgnorePattern: "^_",
+ caughtErrorsIgnorePattern: "^_",
+ },
+ ],
+ // Prefer structured logger over bare console — addressed in 1.7
+ "no-console": "warn",
+ // Avoid unsafe operations that circumvent TypeScript's type system
+ "@typescript-eslint/no-unsafe-assignment": "off",
+ "@typescript-eslint/no-unsafe-member-access": "off",
+ "@typescript-eslint/no-unsafe-call": "off",
+ "@typescript-eslint/no-unsafe-return": "off",
+ "@typescript-eslint/no-unsafe-argument": "off",
+ // Require `await` on async calls — catches fire-and-forget bugs
+ "@typescript-eslint/no-floating-promises": "off", // enable after 1.4
+ },
+ languageOptions: {
+ parserOptions: {
+ project: true,
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+ },
+ {
+ // Ignore test files and build output
+ ignores: [
+ "dist/**",
+ "node_modules/**",
+ "coverage/**",
+ "**/*.test.ts",
+ "vitest.setup.ts",
+ "scripts/**",
+ "src/index.ts",
+ ],
+ },
+);
diff --git a/package-lock.json b/package-lock.json
index a848d55..77886cc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,30 +1,39 @@
{
"name": "@stratsolver/graph-server",
- "version": "0.0.1",
+ "version": "0.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@stratsolver/graph-server",
- "version": "0.0.1",
+ "version": "0.1.1",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.9.0",
+ "@modelcontextprotocol/sdk": "^1.27.0",
"chokidar": "^5.0.0",
"dotenv": "^16.0.3",
"express": "^4.18.2",
- "glob": "^10.3.10",
+ "glob": "^13.0.6",
"neo4j-driver": "^5.14.0",
- "zod": "^3.22.4"
+ "zod": "^4.3.6"
},
"devDependencies": {
+ "@eslint/js": "^10.0.1",
"@types/express": "^4.17.21",
"@types/node": "^24.10.1",
+ "@vitest/coverage-v8": "^4.0.18",
+ "eslint": "^10.0.2",
+ "eslint-config-prettier": "^10.1.8",
+ "prettier": "^3.8.1",
"typescript": "~5.9.3",
+ "typescript-eslint": "^8.56.1",
"vitest": "^4.0.18"
},
+ "engines": {
+ "node": "24.14.x"
+ },
"optionalDependencies": {
- "tree-sitter": "^0.21.1",
+ "tree-sitter": "0.21.1",
"tree-sitter-go": "^0.21.0",
"tree-sitter-java": "^0.21.0",
"tree-sitter-javascript": "^0.21.4",
@@ -33,6 +42,66 @@
"tree-sitter-typescript": "^0.21.2"
}
},
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
@@ -475,6 +544,134 @@
"node": ">=18"
}
},
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.2.tgz",
+ "integrity": "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^3.0.2",
+ "debug": "^4.3.1",
+ "minimatch": "^10.2.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz",
+ "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.1.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz",
+ "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz",
+ "integrity": "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz",
+ "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.1.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
"node_modules/@hono/node-server": {
"version": "1.19.9",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
@@ -487,21 +684,66 @@
"hono": "^4"
}
},
- "node_modules/@isaacs/cliui": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
- "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
- "license": "ISC",
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
"dependencies": {
- "string-width": "^5.1.2",
- "string-width-cjs": "npm:string-width@^4.2.0",
- "strip-ansi": "^7.0.1",
- "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
- "wrap-ansi": "^8.1.0",
- "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
},
"engines": {
- "node": ">=12"
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
@@ -511,10 +753,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
"node_modules/@modelcontextprotocol/sdk": {
- "version": "1.26.0",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
- "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
+ "version": "1.27.1",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz",
+ "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==",
"license": "MIT",
"dependencies": {
"@hono/node-server": "^1.19.9",
@@ -610,23 +863,6 @@
"node": ">=6.6.0"
}
},
- "node_modules/@modelcontextprotocol/sdk/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/@modelcontextprotocol/sdk/node_modules/express": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
@@ -762,12 +998,6 @@
"url": "https://opencollective.com/express"
}
},
- "node_modules/@modelcontextprotocol/sdk/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/@modelcontextprotocol/sdk/node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
@@ -777,21 +1007,6 @@
"node": ">= 0.6"
}
},
- "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
- "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
- "license": "MIT",
- "dependencies": {
- "bytes": "~3.1.2",
- "http-errors": "~2.0.1",
- "iconv-lite": "~0.7.0",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
"node_modules/@modelcontextprotocol/sdk/node_modules/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
@@ -851,20 +1066,10 @@
"node": ">= 0.6"
}
},
- "node_modules/@pkgjs/parseargs": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
- "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=14"
- }
- },
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
- "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [
"arm"
],
@@ -876,9 +1081,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
- "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [
"arm64"
],
@@ -890,9 +1095,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
- "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [
"arm64"
],
@@ -904,9 +1109,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
- "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [
"x64"
],
@@ -918,9 +1123,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
- "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [
"arm64"
],
@@ -932,9 +1137,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
- "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [
"x64"
],
@@ -946,9 +1151,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
- "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [
"arm"
],
@@ -960,9 +1165,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
- "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [
"arm"
],
@@ -974,9 +1179,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
- "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [
"arm64"
],
@@ -988,9 +1193,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
- "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [
"arm64"
],
@@ -1002,9 +1207,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
- "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"cpu": [
"loong64"
],
@@ -1016,9 +1221,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
- "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [
"loong64"
],
@@ -1030,9 +1235,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
- "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"cpu": [
"ppc64"
],
@@ -1044,9 +1249,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
- "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [
"ppc64"
],
@@ -1058,9 +1263,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
- "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [
"riscv64"
],
@@ -1072,9 +1277,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
- "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [
"riscv64"
],
@@ -1086,9 +1291,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
- "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [
"s390x"
],
@@ -1100,9 +1305,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
- "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [
"x64"
],
@@ -1114,9 +1319,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
- "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [
"x64"
],
@@ -1128,9 +1333,9 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
- "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"cpu": [
"x64"
],
@@ -1142,9 +1347,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
- "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [
"arm64"
],
@@ -1156,9 +1361,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
- "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [
"arm64"
],
@@ -1170,9 +1375,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
- "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [
"ia32"
],
@@ -1184,9 +1389,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
- "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [
"x64"
],
@@ -1198,9 +1403,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
- "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [
"x64"
],
@@ -1257,6 +1462,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1297,6 +1509,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@@ -1305,9 +1524,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "24.10.13",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz",
- "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==",
+ "version": "24.11.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz",
+ "integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1361,76 +1580,337 @@
"@types/node": "*"
}
},
- "node_modules/@vitest/expect": {
- "version": "4.0.18",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
- "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
+ "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@standard-schema/spec": "^1.0.0",
- "@types/chai": "^5.2.2",
- "@vitest/spy": "4.0.18",
- "@vitest/utils": "4.0.18",
- "chai": "^6.2.1",
- "tinyrainbow": "^3.0.3"
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.56.1",
+ "@typescript-eslint/type-utils": "8.56.1",
+ "@typescript-eslint/utils": "8.56.1",
+ "@typescript-eslint/visitor-keys": "8.56.1",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
- "url": "https://opencollective.com/vitest"
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.56.1",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
}
},
- "node_modules/@vitest/mocker": {
- "version": "4.0.18",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
- "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz",
+ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/spy": "4.0.18",
- "estree-walker": "^3.0.3",
- "magic-string": "^0.30.21"
+ "@typescript-eslint/scope-manager": "8.56.1",
+ "@typescript-eslint/types": "8.56.1",
+ "@typescript-eslint/typescript-estree": "8.56.1",
+ "@typescript-eslint/visitor-keys": "8.56.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
- "url": "https://opencollective.com/vitest"
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "msw": "^2.4.9",
- "vite": "^6.0.0 || ^7.0.0-0"
- },
- "peerDependenciesMeta": {
- "msw": {
- "optional": true
- },
- "vite": {
- "optional": true
- }
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
}
},
- "node_modules/@vitest/pretty-format": {
- "version": "4.0.18",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
- "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz",
+ "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "tinyrainbow": "^3.0.3"
+ "@typescript-eslint/tsconfig-utils": "^8.56.1",
+ "@typescript-eslint/types": "^8.56.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
- "url": "https://opencollective.com/vitest"
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
}
},
- "node_modules/@vitest/runner": {
- "version": "4.0.18",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
- "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz",
+ "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "4.0.18",
- "pathe": "^2.0.3"
+ "@typescript-eslint/types": "8.56.1",
+ "@typescript-eslint/visitor-keys": "8.56.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
- "url": "https://opencollective.com/vitest"
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz",
+ "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz",
+ "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.56.1",
+ "@typescript-eslint/typescript-estree": "8.56.1",
+ "@typescript-eslint/utils": "8.56.1",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz",
+ "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz",
+ "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.56.1",
+ "@typescript-eslint/tsconfig-utils": "8.56.1",
+ "@typescript-eslint/types": "8.56.1",
+ "@typescript-eslint/visitor-keys": "8.56.1",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz",
+ "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.56.1",
+ "@typescript-eslint/types": "8.56.1",
+ "@typescript-eslint/typescript-estree": "8.56.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz",
+ "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.56.1",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitest/coverage-v8": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz",
+ "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^1.0.2",
+ "@vitest/utils": "4.0.18",
+ "ast-v8-to-istanbul": "^0.3.10",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-reports": "^3.2.0",
+ "magicast": "^0.5.1",
+ "obug": "^2.1.1",
+ "std-env": "^3.10.0",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "4.0.18",
+ "vitest": "4.0.18"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
+ "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.0.18",
+ "@vitest/utils": "4.0.18",
+ "chai": "^6.2.1",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
+ "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.0.18",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
+ "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
+ "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.0.18",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot": {
@@ -1485,6 +1965,29 @@
"node": ">= 0.6"
}
},
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
"node_modules/ajv": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
@@ -1518,30 +2021,6 @@
}
}
},
- "node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -1558,11 +2037,26 @@
"node": ">=12"
}
},
+ "node_modules/ast-v8-to-istanbul": {
+ "version": "0.3.12",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz",
+ "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.31",
+ "estree-walker": "^3.0.3",
+ "js-tokens": "^10.0.0"
+ }
+ },
"node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "license": "MIT"
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
},
"node_modules/base64-js": {
"version": "1.5.1",
@@ -1608,13 +2102,46 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/body-parser/node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
+ "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"license": "MIT",
"dependencies": {
- "balanced-match": "^1.0.0"
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
}
},
"node_modules/buffer": {
@@ -1704,24 +2231,6 @@
"url": "https://paulmillr.com/funding/"
}
},
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "license": "MIT",
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "license": "MIT"
- },
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -1790,14 +2299,29 @@
}
},
"node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
- "ms": "2.0.0"
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
}
},
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -1843,24 +2367,12 @@
"node": ">= 0.4"
}
},
- "node_modules/eastasianwidth": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "license": "MIT"
- },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
- "node_modules/emoji-regex": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "license": "MIT"
- },
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@@ -1949,11 +2461,206 @@
"@esbuild/win32-x64": "0.27.3"
}
},
- "node_modules/escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
- "license": "MIT"
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "10.0.2",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.2.tgz",
+ "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.2",
+ "@eslint/config-helpers": "^0.5.2",
+ "@eslint/core": "^1.1.0",
+ "@eslint/plugin-kit": "^0.6.0",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^9.1.1",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.1.1",
+ "esquery": "^1.7.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "minimatch": "^10.2.1",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "10.1.8",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
+ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-config-prettier"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "9.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz",
+ "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/ajv": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/eslint/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/espree": {
+ "version": "11.1.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz",
+ "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
},
"node_modules/estree-walker": {
"version": "3.0.3",
@@ -1965,6 +2672,16 @@
"@types/estree": "^1.0.0"
}
},
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -2010,7 +2727,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -2070,12 +2786,41 @@
"express": ">= 4.11"
}
},
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
@@ -2110,6 +2855,19 @@
}
}
},
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
@@ -2128,22 +2886,59 @@
"node": ">= 0.8"
}
},
- "node_modules/foreground-child": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
- "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
- "license": "ISC",
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
"dependencies": {
- "cross-spawn": "^7.0.6",
- "signal-exit": "^4.0.1"
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
},
"engines": {
- "node": ">=14"
+ "node": ">=10"
},
"funding": {
- "url": "https://github.com/sponsors/isaacs"
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
}
},
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -2224,26 +3019,35 @@
}
},
"node_modules/glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
- "deprecated": "Old versions of glob 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 exorbitant rates) by contacting i@izs.me",
- "license": "ISC",
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
},
- "bin": {
- "glob": "dist/esm/bin.mjs"
+ "engines": {
+ "node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -2256,6 +3060,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -2281,15 +3095,21 @@
}
},
"node_modules/hono": {
- "version": "4.11.10",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.10.tgz",
- "integrity": "sha512-kyWP5PAiMooEvGrA9jcD3IXF7ATu8+o7B3KCbPXid5se52NPqnOpM/r9qeW2heMnOekF4kqR1fXJqCYeCLKrZg==",
+ "version": "4.12.3",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.3.tgz",
+ "integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=16.9.0"
}
},
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@@ -2342,6 +3162,26 @@
],
"license": "BSD-3-Clause"
},
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -2366,13 +3206,27 @@
"node": ">= 0.10"
}
},
- "node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
"license": "MIT",
"engines": {
- "node": ">=8"
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
}
},
"node_modules/is-promise": {
@@ -2387,19 +3241,43 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
- "node_modules/jackspeak": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
- "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
- "license": "BlueOak-1.0.0",
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
"dependencies": {
- "@isaacs/cliui": "^8.0.2"
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
},
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
},
- "optionalDependencies": {
- "@pkgjs/parseargs": "^0.11.0"
+ "engines": {
+ "node": ">=8"
}
},
"node_modules/jose": {
@@ -2411,6 +3289,20 @@
"url": "https://github.com/sponsors/panva"
}
},
+ "node_modules/js-tokens": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
+ "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -2423,11 +3315,61 @@
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
"license": "BSD-2-Clause"
},
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "license": "ISC"
+ "version": "11.2.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
+ "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
},
"node_modules/magic-string": {
"version": "0.30.21",
@@ -2439,6 +3381,34 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/magicast": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz",
+ "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -2509,15 +3479,15 @@
}
},
"node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "license": "ISC",
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "brace-expansion": "^2.0.1"
+ "brace-expansion": "^5.0.2"
},
"engines": {
- "node": ">=16 || 14 >=14.17"
+ "node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -2533,9 +3503,9 @@
}
},
"node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "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/nanoid": {
@@ -2557,6 +3527,13 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -2595,9 +3572,9 @@
"license": "Apache-2.0"
},
"node_modules/node-addon-api": {
- "version": "8.5.0",
- "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
- "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz",
+ "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==",
"license": "MIT",
"optional": true,
"engines": {
@@ -2669,12 +3646,56 @@
"wrappy": "1"
}
},
- "node_modules/package-json-from-dist": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
- "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
- "license": "BlueOak-1.0.0"
- },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -2684,6 +3705,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -2694,16 +3725,16 @@
}
},
"node_modules/path-scurry": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
- "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
"license": "BlueOak-1.0.0",
"dependencies": {
- "lru-cache": "^10.2.0",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
},
"engines": {
- "node": ">=16 || 14 >=14.18"
+ "node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -2735,7 +3766,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -2781,6 +3811,32 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.8.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
+ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -2794,6 +3850,16 @@
"node": ">= 0.10"
}
},
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/qs": {
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
@@ -2819,18 +3885,34 @@
}
},
"node_modules/raw-body": {
- "version": "2.5.3",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
- "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
- "iconv-lite": "~0.4.24",
+ "iconv-lite": "~0.7.0",
"unpipe": "~1.0.0"
},
"engines": {
- "node": ">= 0.8"
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/raw-body/node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/readdirp": {
@@ -2856,9 +3938,9 @@
}
},
"node_modules/rollup": {
- "version": "4.57.1",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
- "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2872,31 +3954,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.57.1",
- "@rollup/rollup-android-arm64": "4.57.1",
- "@rollup/rollup-darwin-arm64": "4.57.1",
- "@rollup/rollup-darwin-x64": "4.57.1",
- "@rollup/rollup-freebsd-arm64": "4.57.1",
- "@rollup/rollup-freebsd-x64": "4.57.1",
- "@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
- "@rollup/rollup-linux-arm-musleabihf": "4.57.1",
- "@rollup/rollup-linux-arm64-gnu": "4.57.1",
- "@rollup/rollup-linux-arm64-musl": "4.57.1",
- "@rollup/rollup-linux-loong64-gnu": "4.57.1",
- "@rollup/rollup-linux-loong64-musl": "4.57.1",
- "@rollup/rollup-linux-ppc64-gnu": "4.57.1",
- "@rollup/rollup-linux-ppc64-musl": "4.57.1",
- "@rollup/rollup-linux-riscv64-gnu": "4.57.1",
- "@rollup/rollup-linux-riscv64-musl": "4.57.1",
- "@rollup/rollup-linux-s390x-gnu": "4.57.1",
- "@rollup/rollup-linux-x64-gnu": "4.57.1",
- "@rollup/rollup-linux-x64-musl": "4.57.1",
- "@rollup/rollup-openbsd-x64": "4.57.1",
- "@rollup/rollup-openharmony-arm64": "4.57.1",
- "@rollup/rollup-win32-arm64-msvc": "4.57.1",
- "@rollup/rollup-win32-ia32-msvc": "4.57.1",
- "@rollup/rollup-win32-x64-gnu": "4.57.1",
- "@rollup/rollup-win32-x64-msvc": "4.57.1",
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
"fsevents": "~2.3.2"
}
},
@@ -2916,29 +3998,6 @@
"node": ">= 18"
}
},
- "node_modules/router/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/router/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/router/node_modules/path-to-regexp": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
@@ -2984,6 +4043,19 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/send": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
@@ -3008,10 +4080,19 @@
"node": ">= 0.8.0"
}
},
- "node_modules/send/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==",
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/serve-static": {
@@ -3135,18 +4216,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -3189,102 +4258,19 @@
"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",
- "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
- "license": "MIT",
- "dependencies": {
- "eastasianwidth": "^0.2.0",
- "emoji-regex": "^9.2.2",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/string-width-cjs": {
- "name": "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/string-width-cjs/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/string-width-cjs/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/string-width-cjs/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/strip-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
- "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/strip-ansi-cjs": {
- "name": "strip-ansi",
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "ansi-regex": "^5.0.1"
+ "has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
- "node_modules/strip-ansi-cjs/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/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -3338,6 +4324,18 @@
"node": ">=0.6"
}
},
+ "node_modules/tree-sitter": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz",
+ "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "node-addon-api": "^8.0.0",
+ "node-gyp-build": "^4.8.0"
+ }
+ },
"node_modules/tree-sitter-go": {
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/tree-sitter-go/-/tree-sitter-go-0.21.2.tgz",
@@ -3448,12 +4446,38 @@
}
}
},
+ "node_modules/ts-api-utils": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
+ "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -3481,6 +4505,30 @@
"node": ">=14.17"
}
},
+ "node_modules/typescript-eslint": {
+ "version": "8.56.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz",
+ "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.56.1",
+ "@typescript-eslint/parser": "8.56.1",
+ "@typescript-eslint/typescript-estree": "8.56.1",
+ "@typescript-eslint/utils": "8.56.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
@@ -3497,6 +4545,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -3521,7 +4579,6 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -3701,109 +4758,40 @@
"node": ">=8"
}
},
- "node_modules/wrap-ansi": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
- "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.1.0",
- "string-width": "^5.0.1",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs": {
- "name": "wrap-ansi",
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
"engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ "node": ">=0.10.0"
}
},
- "node_modules/wrap-ansi-cjs/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/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
},
- "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
"engines": {
- "node": ">=8"
+ "node": ">=10"
},
"funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi-cjs/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"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "license": "ISC"
- },
"node_modules/zod": {
- "version": "3.25.76",
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
- "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/package.json b/package.json
index 5f0ad8b..88257bd 100644
--- a/package.json
+++ b/package.json
@@ -1,14 +1,14 @@
{
"name": "@stratsolver/graph-server",
- "version": "0.0.1",
- "description": "MCP server for code graph analysis, test intelligence, and progress tracking",
+ "version": "0.1.1",
+ "description": "Dynamic Intelligence Graph (DIG) MCP server for code graph intelligence, agent memory, and multi-agent coordination — beyond RAG and GraphRAG — for VS Code Copilot, Claude Code, Claude Desktop, and Cursor",
"author": "stratSolver Team",
"license": "MIT",
"type": "module",
- "main": "dist/index.js",
- "types": "dist/index.d.ts",
+ "main": "dist/server.js",
+ "types": "dist/server.d.ts",
"scripts": {
- "build": "tsc",
+ "build": "tsc && bash scripts/fix-esm-imports.sh",
"dev": "tsc --watch",
"start": "node dist/server.js",
"start:http": "node scripts/start-http-supervisor.mjs",
@@ -16,38 +16,71 @@
"test": "vitest run",
"test:watch": "vitest watch",
"test:coverage": "vitest run --coverage",
- "lint": "eslint src --ext .ts",
+ "lint": "eslint src/",
+ "lint:fix": "eslint src/ --fix",
+ "format": "prettier --write src/",
+ "format:check": "prettier --check src/",
"benchmark:check-regression": "python3 scripts/check_benchmark_regression.py"
},
+ "engines": {
+ "node": "24.14.x"
+ },
"keywords": [
"mcp",
- "lxrag",
+ "mcp-server",
+ "model-context-protocol",
+ "lxdig",
+ "dig",
+ "dynamic-intelligence-graph",
+ "code-intelligence",
+ "code-graph",
+ "graph-rag",
+ "rag",
+ "graphrag",
+ "agent-memory",
+ "multi-agent",
+ "ai-agent",
+ "semantic-search",
+ "code-search",
"memgraph",
+ "qdrant",
"test-intelligence",
+ "impact-analysis",
+ "architecture-validation",
+ "claude",
+ "copilot",
+ "cursor",
+ "vscode",
"progress-tracking"
],
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.9.0",
+ "@modelcontextprotocol/sdk": "^1.27.0",
"chokidar": "^5.0.0",
"dotenv": "^16.0.3",
"express": "^4.18.2",
- "glob": "^10.3.10",
+ "glob": "^13.0.6",
"neo4j-driver": "^5.14.0",
- "zod": "^3.22.4"
+ "zod": "^4.3.6"
},
"optionalDependencies": {
- "tree-sitter": "^0.21.1",
- "tree-sitter-typescript": "^0.21.2",
+ "tree-sitter": "0.21.1",
+ "tree-sitter-go": "^0.21.0",
+ "tree-sitter-java": "^0.21.0",
"tree-sitter-javascript": "^0.21.4",
"tree-sitter-python": "^0.21.0",
- "tree-sitter-go": "^0.21.0",
"tree-sitter-rust": "^0.21.2",
- "tree-sitter-java": "^0.21.0"
+ "tree-sitter-typescript": "^0.21.2"
},
"devDependencies": {
+ "@eslint/js": "^10.0.1",
"@types/express": "^4.17.21",
"@types/node": "^24.10.1",
+ "@vitest/coverage-v8": "^4.0.18",
+ "eslint": "^10.0.2",
+ "eslint-config-prettier": "^10.1.8",
+ "prettier": "^3.8.1",
"typescript": "~5.9.3",
+ "typescript-eslint": "^8.56.1",
"vitest": "^4.0.18"
}
}
diff --git a/scripts/audit-census-v2.cjs b/scripts/audit-census-v2.cjs
new file mode 100644
index 0000000..f6e9f11
--- /dev/null
+++ b/scripts/audit-census-v2.cjs
@@ -0,0 +1,184 @@
+#!/usr/bin/env node
+/**
+ * Audit census with correct uppercase labels for Memgraph.
+ */
+
+const neo4j = require("neo4j-driver");
+const PROJECT = "lxDIG-MCP";
+
+async function run() {
+ const driver = neo4j.driver(
+ "bolt://localhost:7687",
+ neo4j.auth.basic("", ""),
+ );
+ const session = driver.session();
+
+ const q = async (label, cypher) => {
+ console.log(`\n=== ${label} ===`);
+ try {
+ const r = await session.run(cypher);
+ const rows = r.records.map((rec) => {
+ const obj = {};
+ rec.keys.forEach((k) => {
+ const v = rec.get(k);
+ obj[k] =
+ v && typeof v.toNumber === "function"
+ ? v.toNumber()
+ : v && typeof v.toInt === "function"
+ ? v.toInt()
+ : v;
+ });
+ return obj;
+ });
+ console.log(JSON.stringify(rows, null, 2));
+ return rows;
+ } catch (e) {
+ console.log(`[QUERY ERROR] ${e.message}`);
+ return [];
+ }
+ };
+
+ try {
+ // 1. Node census by label
+ await q(
+ "NODE CENSUS (lxDIG MCP)",
+ `
+ MATCH (n) WHERE n.projectId = '${PROJECT}'
+ RETURN labels(n)[0] AS label, count(*) AS cnt
+ ORDER BY cnt DESC
+ `,
+ );
+
+ // 2. Relationship census
+ await q(
+ "REL CENSUS (lxDIG MCP)",
+ `
+ MATCH (a)-[r]->(b) WHERE a.projectId = '${PROJECT}'
+ RETURN type(r) AS relType, count(*) AS cnt
+ ORDER BY cnt DESC
+ `,
+ );
+
+ // 3. FILE node paths sample
+ await q(
+ "FILE paths sample",
+ `
+ MATCH (f:FILE) WHERE f.projectId = '${PROJECT}'
+ RETURN f.path AS path ORDER BY path LIMIT 15
+ `,
+ );
+
+ // 4. SECTION missing relativePath
+ await q(
+ "SECTION missing relativePath",
+ `
+ MATCH (s:SECTION) WHERE s.projectId = '${PROJECT}' AND s.relativePath IS NULL
+ RETURN count(s) AS missing
+ `,
+ );
+
+ // 5. SECTION total + sample
+ await q(
+ "SECTION total",
+ `
+ MATCH (s:SECTION) WHERE s.projectId = '${PROJECT}'
+ RETURN count(s) AS total
+ `,
+ );
+
+ await q(
+ "SECTION sample",
+ `
+ MATCH (s:SECTION) WHERE s.projectId = '${PROJECT}'
+ RETURN s.title AS title, s.relativePath AS relPath, s.workspaceRoot AS wr
+ LIMIT 5
+ `,
+ );
+
+ // 6. VIOLATION nodes
+ await q(
+ "VIOLATION nodes",
+ `
+ MATCH (v:VIOLATION) WHERE v.projectId = '${PROJECT}'
+ RETURN count(v) AS total, count(DISTINCT v.file) AS distinctFiles
+ `,
+ );
+
+ // 7. COMMUNITY labels
+ await q(
+ "COMMUNITY nodes",
+ `
+ MATCH (c:COMMUNITY) WHERE c.projectId = '${PROJECT}'
+ RETURN c.label AS label, c.memberCount AS memberCount, c.size AS size
+ ORDER BY c.memberCount DESC LIMIT 10
+ `,
+ );
+
+ // 8. REFERENCES relationships
+ await q(
+ "REFERENCES rels",
+ `
+ MATCH (a)-[:REFERENCES]->(b)
+ WHERE a.projectId = '${PROJECT}'
+ RETURN count(*) AS total
+ `,
+ );
+
+ // 9. Embeddings
+ await q(
+ "EMBEDDINGS coverage",
+ `
+ MATCH (n:FUNCTION) WHERE n.projectId = '${PROJECT}'
+ RETURN
+ sum(CASE WHEN n.embedding IS NOT NULL THEN 1 ELSE 0 END) AS withEmb,
+ sum(CASE WHEN n.embedding IS NULL THEN 1 ELSE 0 END) AS withoutEmb
+ `,
+ );
+
+ // 10. Layer values
+ await q(
+ "LAYER values",
+ `
+ MATCH (n) WHERE n.projectId = '${PROJECT}' AND n.layer IS NOT NULL
+ RETURN n.layer AS layer, count(*) AS cnt ORDER BY cnt DESC
+ `,
+ );
+
+ // 11. Architecture config stored in graph?
+ await q(
+ "GRAPH_TX latest",
+ `
+ MATCH (tx:GRAPH_TX) WHERE tx.projectId = '${PROJECT}'
+ RETURN tx.txId AS txId, tx.mode AS mode, tx.timestamp AS ts, tx.status AS status
+ ORDER BY tx.timestamp DESC LIMIT 5
+ `,
+ );
+
+ // 12. Class nodes sample (check workspaceRoot)
+ await q(
+ "CLASS sample",
+ `
+ MATCH (c:CLASS) WHERE c.projectId = '${PROJECT}'
+ RETURN c.name AS name, c.path AS path, c.layer AS layer LIMIT 10
+ `,
+ );
+
+ // 13. Duplicate FILE nodes (relative vs absolute path check)
+ await q(
+ "FILE duplicate path check",
+ `
+ MATCH (f:FILE) WHERE f.projectId = '${PROJECT}'
+ RETURN
+ sum(CASE WHEN f.path STARTS WITH '/' THEN 1 ELSE 0 END) AS absolutePaths,
+ sum(CASE WHEN NOT f.path STARTS WITH '/' THEN 1 ELSE 0 END) AS relativePaths
+ `,
+ );
+ } catch (err) {
+ console.error("FATAL:", err.message);
+ } finally {
+ await session.close();
+ await driver.close();
+ }
+}
+
+run();
diff --git a/scripts/audit-census.cjs b/scripts/audit-census.cjs
new file mode 100644
index 0000000..e598854
--- /dev/null
+++ b/scripts/audit-census.cjs
@@ -0,0 +1,164 @@
+#!/usr/bin/env node
+/**
+ * Audit census script: collects node/relationship/structure data
+ * for the lxDIG MCP self-audit.
+ */
+
+const neo4j = require("neo4j-driver");
+
+async function run() {
+ const driver = neo4j.driver(
+ "bolt://localhost:7687",
+ neo4j.auth.basic("", ""),
+ );
+ const session = driver.session();
+
+ const q = async (label, cypher, params = {}) => {
+ console.log(`\n=== ${label} ===`);
+ const r = await session.run(cypher, params);
+ const rows = r.records.map((rec) => {
+ const obj = {};
+ rec.keys.forEach((k) => {
+ const v = rec.get(k);
+ obj[k] = typeof v?.toNumber === "function" ? v.toNumber() : v;
+ });
+ return obj;
+ });
+ console.log(JSON.stringify(rows, null, 2));
+ return rows;
+ };
+
+ try {
+ // 1. Node census by label
+ await q(
+ "NODE CENSUS",
+ `
+ MATCH (n) WHERE n.projectId IS NOT NULL
+ RETURN labels(n)[0] AS label, count(*) AS cnt
+ ORDER BY cnt DESC
+ `,
+ );
+
+ // 2. Relationship census by type
+ await q(
+ "REL CENSUS",
+ `
+ MATCH (a)-[r]->(b) WHERE a.projectId IS NOT NULL
+ RETURN type(r) AS relType, count(*) AS cnt
+ ORDER BY cnt DESC
+ `,
+ );
+
+ // 3. Projects indexed
+ await q(
+ "PROJECTS",
+ `
+ MATCH (f:File) WHERE f.projectId IS NOT NULL
+ RETURN f.projectId AS projectId, count(f) AS fileCount
+ ORDER BY fileCount DESC
+ LIMIT 10
+ `,
+ );
+
+ // 4. SECTION nodes with null/missing relativePath
+ await q(
+ "SECTION nodes missing relativePath",
+ `
+ MATCH (s:Section)
+ WHERE s.projectId = 'lxDIG-MCP' AND s.relativePath IS NULL
+ RETURN count(s) AS missingRelativePath
+ `,
+ );
+
+ // 5. SECTION total
+ await q(
+ "SECTION total lxDIG MCP",
+ `
+ MATCH (s:Section) WHERE s.projectId = 'lxDIG-MCP'
+ RETURN count(s) AS total
+ `,
+ );
+
+ // 6. FILE nodes (check for duplicate / relative paths)
+ await q(
+ "FILE sample lxDIG MCP",
+ `
+ MATCH (f:File) WHERE f.projectId = 'lxDIG-MCP'
+ RETURN f.path AS path
+ ORDER BY path
+ LIMIT 20
+ `,
+ );
+
+ // 7. VIOLATION nodes present?
+ await q(
+ "VIOLATION nodes",
+ `
+ MATCH (v:Violation) WHERE v.projectId = 'lxDIG-MCP'
+ RETURN count(v) AS total,
+ count(DISTINCT v.file) AS distinctFiles
+ `,
+ );
+
+ // 8. Community nodes
+ await q(
+ "COMMUNITY nodes",
+ `
+ MATCH (c:Community) WHERE c.projectId = 'lxDIG-MCP'
+ RETURN c.label AS label, c.memberCount AS memberCount,
+ c.size AS size
+ ORDER BY c.memberCount DESC
+ LIMIT 20
+ `,
+ );
+
+ // 9. REFERENCES relationships (for F11)
+ await q(
+ "REFERENCES relationships",
+ `
+ MATCH (a)-[:REFERENCES]->(b)
+ WHERE a.projectId = 'lxDIG-MCP'
+ RETURN count(*) AS total
+ `,
+ );
+
+ // 10. CALLS / IMPORTS relationship totals
+ await q(
+ "CALLS and IMPORTS",
+ `
+ MATCH (a)-[r:CALLS|IMPORTS]->(b)
+ WHERE a.projectId = 'lxDIG-MCP'
+ RETURN type(r) AS relType, count(*) AS cnt
+ `,
+ );
+
+ // 11. Architecture layers in nodes
+ await q(
+ "LAYER values",
+ `
+ MATCH (n) WHERE n.projectId = 'lxDIG-MCP' AND n.layer IS NOT NULL
+ RETURN n.layer AS layer, count(*) AS cnt
+ ORDER BY cnt DESC
+ `,
+ );
+
+ // 12. Embedding coverage: nodes with embedding vs without
+ await q(
+ "EMBEDDING coverage",
+ `
+ MATCH (n) WHERE n.projectId = 'lxDIG-MCP'
+ AND n:Function OR n:Class OR n:File
+ RETURN
+ count(CASE WHEN n.embedding IS NOT NULL THEN 1 END) AS withEmbedding,
+ count(CASE WHEN n.embedding IS NULL THEN 1 END) AS withoutEmbedding
+ `,
+ );
+ } catch (err) {
+ console.error("ERROR:", err.message);
+ } finally {
+ await session.close();
+ await driver.close();
+ }
+}
+
+run();
diff --git a/scripts/audit-community-refs.cjs b/scripts/audit-community-refs.cjs
new file mode 100644
index 0000000..2a16699
--- /dev/null
+++ b/scripts/audit-community-refs.cjs
@@ -0,0 +1,118 @@
+#!/usr/bin/env node
+/**
+ * Audit: community membership types and REFERENCES root cause
+ */
+const neo4j = require("neo4j-driver");
+const PROJECT = "lxDIG-MCP";
+
+async function run() {
+ const driver = neo4j.driver(
+ "bolt://localhost:7687",
+ neo4j.auth.basic("", ""),
+ );
+ const session = driver.session();
+
+ const q = async (label, cypher) => {
+ console.log(`\n=== ${label} ===`);
+ try {
+ const r = await session.run(cypher);
+ const rows = r.records.map((rec) => {
+ const obj = {};
+ rec.keys.forEach((k) => {
+ const v = rec.get(k);
+ obj[k] =
+ v && typeof v.toNumber === "function"
+ ? v.toNumber()
+ : v && typeof v.toInt === "function"
+ ? v.toInt()
+ : v;
+ });
+ return obj;
+ });
+ console.log(JSON.stringify(rows, null, 2));
+ return rows;
+ } catch (e) {
+ console.log(`[QUERY ERROR] ${e.message}`);
+ return [];
+ }
+ };
+
+ try {
+ // What node types are in misc?
+ await q(
+ "MISC community membership by type",
+ `
+ MATCH (n)-[:BELONGS_TO]->(c:COMMUNITY {label: 'misc', projectId: '${PROJECT}'})
+ RETURN labels(n)[0] AS nodeType, count(*) AS cnt
+ ORDER BY cnt DESC
+ `,
+ );
+
+ // REFERENCES: why they don't exist for lxDIG MCP
+ // Check if IMPORTs have 'source' ending in .js
+ await q(
+ "IMPORT source extensions breakdown",
+ `
+ MATCH (imp:IMPORT) WHERE imp.projectId = '${PROJECT}'
+ RETURN
+ sum(CASE WHEN imp.source ENDS WITH '.js' THEN 1 ELSE 0 END) AS jsExtension,
+ sum(CASE WHEN imp.source ENDS WITH '.ts' THEN 1 ELSE 0 END) AS tsExtension,
+ sum(CASE WHEN imp.source STARTS WITH '.' THEN 1 ELSE 0 END) AS relativeImports,
+ sum(CASE WHEN NOT imp.source STARTS WITH '.' THEN 1 ELSE 0 END) AS externalImports
+ `,
+ );
+
+ // IMPORT sources that are relative (should resolve)
+ await q(
+ "Relative IMPORT sources sample",
+ `
+ MATCH (imp:IMPORT) WHERE imp.projectId = '${PROJECT}'
+ AND imp.source STARTS WITH '.'
+ RETURN imp.source AS source, imp.id AS id
+ ORDER BY source
+ LIMIT 15
+ `,
+ );
+
+ // Are there any FILE nodes with .js extension?
+ await q(
+ "FILE nodes .js vs .ts extension",
+ `
+ MATCH (f:FILE) WHERE f.projectId = '${PROJECT}'
+ RETURN
+ sum(CASE WHEN f.path ENDS WITH '.ts' THEN 1 ELSE 0 END) AS tsFiles,
+ sum(CASE WHEN f.path ENDS WITH '.js' THEN 1 ELSE 0 END) AS jsFiles,
+ sum(CASE WHEN f.path ENDS WITH '.md' THEN 1 ELSE 0 END) AS mdFiles
+ `,
+ );
+
+ // IMPORTS that resolve vs don't
+ await q(
+ "IMPORTS with vs without REFERENCES",
+ `
+ MATCH (imp:IMPORT) WHERE imp.projectId = '${PROJECT}'
+ AND imp.source STARTS WITH '.'
+ OPTIONAL MATCH (imp)-[:REFERENCES]->(target)
+ RETURN
+ count(CASE WHEN target IS NOT NULL THEN 1 END) AS resolved,
+ count(CASE WHEN target IS NULL THEN 1 END) AS unresolved
+ `,
+ );
+
+ // Verify: what FILE node IDs are used?
+ await q(
+ "FILE node ID pattern",
+ `
+ MATCH (f:FILE) WHERE f.projectId = '${PROJECT}'
+ RETURN f.id AS id, f.path AS path LIMIT 5
+ `,
+ );
+ } catch (err) {
+ console.error("FATAL:", err.message);
+ } finally {
+ await session.close();
+ await driver.close();
+ }
+}
+
+run();
diff --git a/scripts/audit-deep.cjs b/scripts/audit-deep.cjs
new file mode 100644
index 0000000..6acce12
--- /dev/null
+++ b/scripts/audit-deep.cjs
@@ -0,0 +1,144 @@
+#!/usr/bin/env node
+/**
+ * Deep audit: check IMPORT node details and why REFERENCES may be missing.
+ */
+const neo4j = require("neo4j-driver");
+const PROJECT = "lxDIG-MCP";
+
+async function run() {
+ const driver = neo4j.driver(
+ "bolt://localhost:7687",
+ neo4j.auth.basic("", ""),
+ );
+ const session = driver.session();
+
+ const q = async (label, cypher) => {
+ console.log(`\n=== ${label} ===`);
+ try {
+ const r = await session.run(cypher);
+ const rows = r.records.map((rec) => {
+ const obj = {};
+ rec.keys.forEach((k) => {
+ const v = rec.get(k);
+ obj[k] =
+ v && typeof v.toNumber === "function"
+ ? v.toNumber()
+ : v && typeof v.toInt === "function"
+ ? v.toInt()
+ : v;
+ });
+ return obj;
+ });
+ console.log(JSON.stringify(rows, null, 2));
+ return rows;
+ } catch (e) {
+ console.log(`[QUERY ERROR] ${e.message}`);
+ return [];
+ }
+ };
+
+ try {
+ // Check IMPORT node details
+ await q(
+ "IMPORT sample",
+ `
+ MATCH (imp:IMPORT) WHERE imp.projectId = '${PROJECT}'
+ RETURN imp.source AS source, imp.id AS id LIMIT 10
+ `,
+ );
+
+ // All REFERENCES across entire graph (not just project)
+ await q(
+ "REFERENCES total all projects",
+ `
+ MATCH (a)-[:REFERENCES]->(b) RETURN count(*) AS total
+ `,
+ );
+
+ // IMPORTS in lxDIG MCP that have a REFERENCES rel
+ await q(
+ "IMPORTs with REFERENCES",
+ `
+ MATCH (imp:IMPORT)-[:REFERENCES]->(f:FILE)
+ WHERE imp.projectId = '${PROJECT}'
+ RETURN count(imp) AS cnt
+ `,
+ );
+
+ // Graph_TX node properties in detail
+ await q(
+ "GRAPH_TX full properties",
+ `
+ MATCH (tx:GRAPH_TX) WHERE tx.projectId = '${PROJECT}'
+ RETURN tx LIMIT 3
+ `,
+ );
+
+ // COMMUNITY: what nodes are in the 'misc' community
+ await q(
+ "MISC community members sample",
+ `
+ MATCH (c:COMMUNITY {label: 'misc', projectId: '${PROJECT}'})-[:CONTAINS]->(n)
+ RETURN labels(n)[0] AS label, n.name AS name, n.path AS path
+ LIMIT 20
+ `,
+ );
+
+ // How many community memberships via BELONGS_TO
+ await q(
+ "BELONGS_TO with community labels",
+ `
+ MATCH (n)-[:BELONGS_TO]->(c:COMMUNITY)
+ WHERE c.projectId = '${PROJECT}'
+ RETURN c.label AS community, count(n) AS memberCount
+ ORDER BY memberCount DESC
+ LIMIT 15
+ `,
+ );
+
+ // Check SECTION.title population
+ await q(
+ "SECTION with title",
+ `
+ MATCH (s:SECTION) WHERE s.projectId = '${PROJECT}' AND s.title IS NOT NULL
+ RETURN count(s) AS withTitle
+ `,
+ );
+
+ // DOCUMENT details
+ await q(
+ "DOCUMENT sample",
+ `
+ MATCH (d:DOCUMENT) WHERE d.projectId = '${PROJECT}'
+ RETURN d.path AS path, d.workspaceRoot AS wr, d.relativePath AS relPath
+ LIMIT 5
+ `,
+ );
+
+ // FUNCTION has path?
+ await q(
+ "FUNCTION with path",
+ `
+ MATCH (f:FUNCTION) WHERE f.projectId = '${PROJECT}'
+ RETURN f.name AS name, f.path AS path LIMIT 5
+ `,
+ );
+
+ // Check for in-memory fallback nodes (cachedNodes=448)
+ // What's in lexDIG-visual?
+ await q(
+ "lexDIG-visual node census",
+ `
+ MATCH (n) WHERE n.projectId = 'lexDIG-visual'
+ RETURN labels(n)[0] AS label, count(*) AS cnt ORDER BY cnt DESC
+ `,
+ );
+ } catch (err) {
+ console.error("FATAL:", err.message);
+ } finally {
+ await session.close();
+ await driver.close();
+ }
+}
+
+run();
diff --git a/scripts/audit-probe.cjs b/scripts/audit-probe.cjs
new file mode 100644
index 0000000..0ab9752
--- /dev/null
+++ b/scripts/audit-probe.cjs
@@ -0,0 +1,83 @@
+#!/usr/bin/env node
+/**
+ * Check what projectId values actually exist in Memgraph.
+ */
+
+const neo4j = require("neo4j-driver");
+
+async function run() {
+ const driver = neo4j.driver(
+ "bolt://localhost:7687",
+ neo4j.auth.basic("", ""),
+ );
+ const session = driver.session();
+
+ const q = async (label, cypher) => {
+ console.log(`\n=== ${label} ===`);
+ const r = await session.run(cypher);
+ const rows = r.records.map((rec) => {
+ const obj = {};
+ rec.keys.forEach((k) => {
+ const v = rec.get(k);
+ obj[k] = typeof v?.toNumber === "function" ? v.toNumber() : v;
+ });
+ return obj;
+ });
+ console.log(JSON.stringify(rows, null, 2));
+ };
+
+ try {
+ await q(
+ "DISTINCT projectId values",
+ `
+ MATCH (n) WHERE n.projectId IS NOT NULL
+ RETURN DISTINCT n.projectId AS projectId, count(*) AS cnt
+ ORDER BY cnt DESC
+ LIMIT 20
+ `,
+ );
+
+ await q(
+ "Sample node with properties",
+ `
+ MATCH (n:File) RETURN n LIMIT 3
+ `,
+ );
+
+ await q(
+ "Sample FILE path values",
+ `
+ MATCH (n:File) RETURN n.path AS path, n.projectId AS pid LIMIT 10
+ `,
+ );
+
+ await q(
+ "Total nodes no filter",
+ `
+ MATCH (n) RETURN count(n) AS total
+ `,
+ );
+
+ await q(
+ "Community nodes no filter",
+ `
+ MATCH (c:Community) RETURN c LIMIT 5
+ `,
+ );
+
+ await q(
+ "File nodes with workspaceRoot",
+ `
+ MATCH (n:File) RETURN n.workspaceRoot AS wr, count(*) AS cnt
+ ORDER BY cnt DESC LIMIT 5
+ `,
+ );
+ } catch (err) {
+ console.error("ERROR:", err.message);
+ } finally {
+ await session.close();
+ await driver.close();
+ }
+}
+
+run();
diff --git a/scripts/cleanup-graph.cjs b/scripts/cleanup-graph.cjs
index 377e0e1..71b15c1 100644
--- a/scripts/cleanup-graph.cjs
+++ b/scripts/cleanup-graph.cjs
@@ -41,6 +41,18 @@ async function cleanupGraph() {
} finally {
await driver.close();
}
+
+ // Clean Qdrant after Memgraph
+ const { spawn } = require("child_process");
+ const qdrantScript = require("path").join(__dirname, "cleanup-qdrant.cjs");
+ const proc = spawn("node", [qdrantScript], { stdio: "inherit" });
+ proc.on("close", (code) => {
+ if (code === 0) {
+ console.log("✅ Qdrant cleanup complete");
+ } else {
+ console.error(`❌ Qdrant cleanup failed with code ${code}`);
+ }
+ });
}
cleanupGraph();
diff --git a/scripts/cleanup-qdrant.cjs b/scripts/cleanup-qdrant.cjs
new file mode 100644
index 0000000..770feb7
--- /dev/null
+++ b/scripts/cleanup-qdrant.cjs
@@ -0,0 +1,61 @@
+#!/usr/bin/env node
+
+/**
+ * Clean up Qdrant - delete all collections (vector data)
+ */
+
+const http = require("http");
+
+async function cleanupQdrant() {
+ const options = {
+ hostname: "localhost",
+ port: 6333,
+ path: "/collections",
+ method: "GET",
+ };
+
+ console.log("🧹 Cleaning Qdrant...");
+
+ // Get all collections
+ const req = http.request(options, (res) => {
+ let data = "";
+ res.on("data", (chunk) => {
+ data += chunk;
+ });
+ res.on("end", () => {
+ const collections = JSON.parse(data).result.collections || [];
+ if (collections.length === 0) {
+ console.log("✅ Qdrant is already empty");
+ return;
+ }
+ let deleted = 0;
+ collections.forEach((col) => {
+ const delOptions = {
+ hostname: "localhost",
+ port: 6333,
+ path: `/collections/${col.name}`,
+ method: "DELETE",
+ };
+ const delReq = http.request(delOptions, (delRes) => {
+ delRes.on("data", () => {});
+ delRes.on("end", () => {
+ deleted++;
+ if (deleted === collections.length) {
+ console.log(`✅ Cleaned! Deleted ${deleted} collections`);
+ }
+ });
+ });
+ delReq.on("error", (e) => {
+ console.error(`❌ Error deleting collection ${col.name}:`, e.message);
+ });
+ delReq.end();
+ });
+ });
+ });
+ req.on("error", (e) => {
+ console.error("❌ Error:", e.message);
+ });
+ req.end();
+}
+
+cleanupQdrant();
diff --git a/scripts/cleanup-qdrant.js b/scripts/cleanup-qdrant.js
new file mode 100644
index 0000000..770feb7
--- /dev/null
+++ b/scripts/cleanup-qdrant.js
@@ -0,0 +1,61 @@
+#!/usr/bin/env node
+
+/**
+ * Clean up Qdrant - delete all collections (vector data)
+ */
+
+const http = require("http");
+
+async function cleanupQdrant() {
+ const options = {
+ hostname: "localhost",
+ port: 6333,
+ path: "/collections",
+ method: "GET",
+ };
+
+ console.log("🧹 Cleaning Qdrant...");
+
+ // Get all collections
+ const req = http.request(options, (res) => {
+ let data = "";
+ res.on("data", (chunk) => {
+ data += chunk;
+ });
+ res.on("end", () => {
+ const collections = JSON.parse(data).result.collections || [];
+ if (collections.length === 0) {
+ console.log("✅ Qdrant is already empty");
+ return;
+ }
+ let deleted = 0;
+ collections.forEach((col) => {
+ const delOptions = {
+ hostname: "localhost",
+ port: 6333,
+ path: `/collections/${col.name}`,
+ method: "DELETE",
+ };
+ const delReq = http.request(delOptions, (delRes) => {
+ delRes.on("data", () => {});
+ delRes.on("end", () => {
+ deleted++;
+ if (deleted === collections.length) {
+ console.log(`✅ Cleaned! Deleted ${deleted} collections`);
+ }
+ });
+ });
+ delReq.on("error", (e) => {
+ console.error(`❌ Error deleting collection ${col.name}:`, e.message);
+ });
+ delReq.end();
+ });
+ });
+ });
+ req.on("error", (e) => {
+ console.error("❌ Error:", e.message);
+ });
+ req.end();
+}
+
+cleanupQdrant();
diff --git a/scripts/fix-esm-imports.sh b/scripts/fix-esm-imports.sh
new file mode 100755
index 0000000..a5d4c76
--- /dev/null
+++ b/scripts/fix-esm-imports.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+# fix-esm-imports.sh — Append .js to relative imports in compiled ESM output.
+# Runs as a post-build step so source files stay extension-free.
+set -euo pipefail
+
+DIR="${1:-dist}"
+
+# Use Node.js for reliable regex with lookbehinds
+node -e "
+const fs = require('fs');
+const path = require('path');
+
+function walk(dir) {
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) walk(full);
+ else if (entry.name.endsWith('.js')) fix(full);
+ }
+}
+
+function fix(file) {
+ const src = fs.readFileSync(file, 'utf8');
+ // Match: from \"./foo\" or from './foo' where path starts with . and doesn't already have an extension
+ const out = src.replace(/(from\s+[\"'])(\.\.?\/[^\"']+?)(?(m)
+// WHERE n.id STARTS WITH '_schema:' AND m.id STARTS WITH '_schema:'
+// RETURN n, r, m;
+
+
+// ═══════════════════════════════════════════════════════════════════════════
+// NODES — Layer 0: Structure
+// ═══════════════════════════════════════════════════════════════════════════
+
+CREATE (:FOLDER {
+ id: '_schema:folder:root',
+ name: 'src/',
+ path: '/project/src',
+ projectId: '_schema'
+});
+
+CREATE (:FOLDER {
+ id: '_schema:folder:child',
+ name: 'graph/',
+ path: '/project/src/graph',
+ projectId: '_schema'
+});
+
+
+// ═══════════════════════════════════════════════════════════════════════════
+// NODES — Layer 1: Files
+// ═══════════════════════════════════════════════════════════════════════════
+
+CREATE (:FILE {
+ id: '_schema:file:server',
+ name: 'server.ts',
+ path: '/project/src/server.ts',
+ relativePath: 'src/server.ts',
+ language: 'TypeScript',
+ LOC: 150,
+ hash: '',
+ projectId: '_schema'
+});
+
+CREATE (:FILE {
+ id: '_schema:file:builder',
+ name: 'builder.ts',
+ path: '/project/src/graph/builder.ts',
+ relativePath: 'src/graph/builder.ts',
+ language: 'TypeScript',
+ LOC: 800,
+ hash: '',
+ projectId: '_schema'
+});
+
+CREATE (:FILE {
+ id: '_schema:file:helpers',
+ name: 'helpers.ts',
+ path: '/project/src/utils/helpers.ts',
+ relativePath: 'src/utils/helpers.ts',
+ language: 'TypeScript',
+ LOC: 80,
+ hash: '',
+ projectId: '_schema'
+});
+
+
+// ═══════════════════════════════════════════════════════════════════════════
+// NODES — Layer 2: Symbols (the logical core)
+// ═══════════════════════════════════════════════════════════════════════════
+
+CREATE (:FUNCTION {
+ id: '_schema:fn:handleRequest',
+ name: 'handleRequest',
+ kind: 'function',
+ filePath: '/project/src/server.ts',
+ startLine: 10, endLine: 45, LOC: 35,
+ projectId: '_schema'
+});
+
+CREATE (:FUNCTION {
+ id: '_schema:fn:parseFile',
+ name: 'parseFile',
+ kind: 'function',
+ filePath: '/project/src/utils/helpers.ts',
+ startLine: 20, endLine: 40, LOC: 20,
+ projectId: '_schema'
+});
+
+CREATE (:FUNCTION {
+ id: '_schema:fn:buildGraph',
+ name: 'buildFromParsedFile',
+ kind: 'method',
+ filePath: '/project/src/graph/builder.ts',
+ startLine: 163, endLine: 200, LOC: 37,
+ projectId: '_schema'
+});
+
+CREATE (:CLASS {
+ id: '_schema:cls:GraphBuilder',
+ name: 'GraphBuilder',
+ kind: 'class',
+ filePath: '/project/src/graph/builder.ts',
+ startLine: 1, endLine: 800, LOC: 800,
+ projectId: '_schema'
+});
+
+CREATE (:CLASS {
+ id: '_schema:cls:BaseBuilder',
+ name: 'BaseBuilder',
+ kind: 'class',
+ projectId: '_schema'
+});
+
+CREATE (:CLASS {
+ id: '_schema:cls:IBuilder',
+ name: 'IBuilder',
+ kind: 'interface',
+ projectId: '_schema'
+});
+
+CREATE (:VARIABLE {
+ id: '_schema:var:config',
+ name: 'config',
+ kind: 'const',
+ startLine: 5,
+ projectId: '_schema'
+});
+
+CREATE (:IMPORT {
+ id: '_schema:imp:helpers',
+ source: './utils/helpers',
+ specifiers: ['parseFile', 'logger'],
+ startLine: 1,
+ projectId: '_schema'
+});
+
+CREATE (:EXPORT {
+ id: '_schema:exp:GraphBuilder',
+ name: 'GraphBuilder',
+ isDefault: true,
+ startLine: 800,
+ projectId: '_schema'
+});
+
+
+// ═══════════════════════════════════════════════════════════════════════════
+// NODES — Layer 3: Tests
+// ═══════════════════════════════════════════════════════════════════════════
+
+CREATE (:TEST_SUITE {
+ id: '_schema:suite:builder',
+ name: 'GraphBuilder tests',
+ type: 'describe',
+ category: 'unit',
+ startLine: 1, endLine: 100,
+ filePath: 'src/graph/__tests__/builder.test.ts',
+ projectId: '_schema'
+});
+
+CREATE (:TEST_CASE {
+ id: '_schema:tc:buildNodes',
+ name: 'should build file nodes',
+ startLine: 10, endLine: 25,
+ filePath: 'src/graph/__tests__/builder.test.ts',
+ projectId: '_schema'
+});
+
+
+// ═══════════════════════════════════════════════════════════════════════════
+// NODES — Layer 4: Docs
+// ═══════════════════════════════════════════════════════════════════════════
+
+CREATE (:DOCUMENT {
+ id: '_schema:doc:readme',
+ relativePath: 'README.md',
+ filePath: '/project/README.md',
+ title: 'lxDIG MCP',
+ kind: 'readme',
+ wordCount: 2500,
+ hash: '',
+ projectId: '_schema'
+});
+
+CREATE (:SECTION {
+ id: '_schema:sec:arch',
+ heading: 'Architecture',
+ level: 2,
+ content: 'The GraphBuilder class orchestrates...',
+ wordCount: 350,
+ startLine: 42,
+ projectId: '_schema'
+});
+
+CREATE (:SECTION {
+ id: '_schema:sec:api',
+ heading: 'API Reference',
+ level: 2,
+ content: 'The handleRequest function processes...',
+ wordCount: 200,
+ startLine: 80,
+ projectId: '_schema'
+});
+
+
+// ═══════════════════════════════════════════════════════════════════════════
+// NODES — Layer 5: Intelligence
+// ═══════════════════════════════════════════════════════════════════════════
+
+CREATE (:COMMUNITY {
+ id: '_schema:comm:graph',
+ label: 'Graph subsystem',
+ summary: 'Core graph builder and orchestrator',
+ memberCount: 8,
+ centralNode: '_schema:cls:GraphBuilder',
+ computedAt: 0,
+ projectId: '_schema'
+});
+
+CREATE (:RULE {
+ id: '_schema:rule:engineIsolation',
+ severity: 'error',
+ pattern: 'engine-isolation',
+ description: 'Engines must not import from tools'
+});
+
+CREATE (:RULE {
+ id: '_schema:rule:layerAssignment',
+ severity: 'warning',
+ pattern: 'layer-assignment',
+ description: 'Every symbol must belong to exactly one architectural layer'
+});
+
+CREATE (:FEATURE {
+ id: '_schema:feat:graphMVP',
+ name: 'Code Graph MVP',
+ status: 'completed',
+ description: 'Core graph building and querying',
+ projectId: '_schema'
+});
+
+CREATE (:TASK {
+ id: '_schema:task:cbFix',
+ name: 'Fix circuit breaker',
+ status: 'in-progress',
+ featureId: '_schema:feat:graphMVP',
+ projectId: '_schema'
+});
+
+
+// ═══════════════════════════════════════════════════════════════════════════
+// NODES — Layer 6: Agent Memory
+// ═══════════════════════════════════════════════════════════════════════════
+
+CREATE (:EPISODE {
+ id: '_schema:ep:decision1',
+ type: 'DECISION',
+ content: 'Adopted chunked writes for Memgraph',
+ agentId: 'copilot-01',
+ sessionId: 'session-001',
+ timestamp: 0,
+ outcome: 'success',
+ projectId: '_schema'
+});
+
+CREATE (:EPISODE {
+ id: '_schema:ep:decision2',
+ type: 'LEARNING',
+ content: 'Bulk mode prevents CB trips during rebuild',
+ agentId: 'copilot-01',
+ sessionId: 'session-001',
+ timestamp: 1,
+ outcome: 'success',
+ projectId: '_schema'
+});
+
+CREATE (:LEARNING {
+ id: '_schema:learn:builder',
+ content: 'Repeated activity around builder.ts',
+ extractedAt: 0,
+ confidence: 0.8,
+ projectId: '_schema'
+});
+
+CREATE (:CLAIM {
+ id: '_schema:claim:refactor',
+ agentId: 'copilot-01',
+ intent: 'Refactoring builder for two-phase pipeline',
+ status: 'active',
+ projectId: '_schema'
+});
+
+CREATE (:GRAPH_TX {
+ id: '_schema:tx:rebuild1',
+ type: 'rebuild',
+ timestamp: 0,
+ mode: 'full',
+ sourceDir: '/project/src',
+ projectId: '_schema'
+});
+
+
+// ============================================================================
+// 6. RELATIONSHIPS — All 33 edge variants across 21 unique types
+// ============================================================================
+
+// ─────────────────────────────────────────────────────────────────────────
+// PHYSICAL LAYER: Folder hierarchy + folder→file containment
+// ─────────────────────────────────────────────────────────────────────────
+
+// CONTAINS : FOLDER → FOLDER (folder hierarchy)
+MATCH (a:FOLDER {id: '_schema:folder:root'}),
+ (b:FOLDER {id: '_schema:folder:child'})
+CREATE (a)-[:CONTAINS]->(b);
+
+// CONTAINS : FOLDER → FILE
+MATCH (a:FOLDER {id: '_schema:folder:root'}),
+ (b:FILE {id: '_schema:file:server'})
+CREATE (a)-[:CONTAINS]->(b);
+
+MATCH (a:FOLDER {id: '_schema:folder:child'}),
+ (b:FILE {id: '_schema:file:builder'})
+CREATE (a)-[:CONTAINS]->(b);
+
+// ─────────────────────────────────────────────────────────────────────────
+// BRIDGE: Physical → Logical (FILE contains symbols)
+// ─────────────────────────────────────────────────────────────────────────
+
+// CONTAINS : FILE → FUNCTION
+MATCH (a:FILE {id: '_schema:file:server'}),
+ (b:FUNCTION {id: '_schema:fn:handleRequest'})
+CREATE (a)-[:CONTAINS]->(b);
+
+MATCH (a:FILE {id: '_schema:file:helpers'}),
+ (b:FUNCTION {id: '_schema:fn:parseFile'})
+CREATE (a)-[:CONTAINS]->(b);
+
+MATCH (a:FILE {id: '_schema:file:builder'}),
+ (b:FUNCTION {id: '_schema:fn:buildGraph'})
+CREATE (a)-[:CONTAINS]->(b);
+
+// CONTAINS : FILE → CLASS
+MATCH (a:FILE {id: '_schema:file:builder'}),
+ (b:CLASS {id: '_schema:cls:GraphBuilder'})
+CREATE (a)-[:CONTAINS]->(b);
+
+// CONTAINS : FILE → VARIABLE
+MATCH (a:FILE {id: '_schema:file:server'}),
+ (b:VARIABLE {id: '_schema:var:config'})
+CREATE (a)-[:CONTAINS]->(b);
+
+// ─────────────────────────────────────────────────────────────────────────
+// IMPORTS / EXPORTS / REFERENCES / DEPENDS_ON
+// ─────────────────────────────────────────────────────────────────────────
+
+// IMPORTS : FILE → IMPORT
+MATCH (a:FILE {id: '_schema:file:server'}),
+ (b:IMPORT {id: '_schema:imp:helpers'})
+CREATE (a)-[:IMPORTS]->(b);
+
+// EXPORTS : FILE → EXPORT
+MATCH (a:FILE {id: '_schema:file:builder'}),
+ (b:EXPORT {id: '_schema:exp:GraphBuilder'})
+CREATE (a)-[:EXPORTS]->(b);
+
+// REFERENCES : IMPORT → FILE (resolved target)
+MATCH (a:IMPORT {id: '_schema:imp:helpers'}),
+ (b:FILE {id: '_schema:file:helpers'})
+CREATE (a)-[:REFERENCES]->(b);
+
+// DEPENDS_ON : FILE → FILE
+MATCH (a:FILE {id: '_schema:file:server'}),
+ (b:FILE {id: '_schema:file:helpers'})
+CREATE (a)-[:DEPENDS_ON]->(b);
+
+// ─────────────────────────────────────────────────────────────────────────
+// LOGICAL LAYER: Call graph + inheritance (behavior)
+// ─────────────────────────────────────────────────────────────────────────
+
+// CALLS_TO : FUNCTION → FUNCTION
+MATCH (a:FUNCTION {id: '_schema:fn:handleRequest'}),
+ (b:FUNCTION {id: '_schema:fn:parseFile'})
+CREATE (a)-[:CALLS_TO {line: 15}]->(b);
+
+MATCH (a:FUNCTION {id: '_schema:fn:handleRequest'}),
+ (b:FUNCTION {id: '_schema:fn:buildGraph'})
+CREATE (a)-[:CALLS_TO {line: 30}]->(b);
+
+// EXTENDS : CLASS → CLASS
+MATCH (a:CLASS {id: '_schema:cls:GraphBuilder'}),
+ (b:CLASS {id: '_schema:cls:BaseBuilder'})
+CREATE (a)-[:EXTENDS]->(b);
+
+// IMPLEMENTS : CLASS → CLASS (interface)
+MATCH (a:CLASS {id: '_schema:cls:GraphBuilder'}),
+ (b:CLASS {id: '_schema:cls:IBuilder'})
+CREATE (a)-[:IMPLEMENTS]->(b);
+
+// ─────────────────────────────────────────────────────────────────────────
+// TESTS — Connected to logical symbols (FUNCTION, CLASS), not just files
+// ─────────────────────────────────────────────────────────────────────────
+
+// CONTAINS : FILE → TEST_SUITE
+MATCH (a:FILE {id: '_schema:file:builder'}),
+ (b:TEST_SUITE {id: '_schema:suite:builder'})
+CREATE (a)-[:CONTAINS]->(b);
+
+// CONTAINS : FILE → TEST_CASE
+MATCH (a:FILE {id: '_schema:file:builder'}),
+ (b:TEST_CASE {id: '_schema:tc:buildNodes'})
+CREATE (a)-[:CONTAINS]->(b);
+
+// CONTAINS : TEST_SUITE → TEST_CASE
+MATCH (a:TEST_SUITE {id: '_schema:suite:builder'}),
+ (b:TEST_CASE {id: '_schema:tc:buildNodes'})
+CREATE (a)-[:CONTAINS]->(b);
+
+// TESTS : TEST_SUITE → CLASS (primary link: test covers a class)
+MATCH (a:TEST_SUITE {id: '_schema:suite:builder'}),
+ (b:CLASS {id: '_schema:cls:GraphBuilder'})
+CREATE (a)-[:TESTS]->(b);
+
+// TESTS : TEST_SUITE → FUNCTION (test covers a function)
+MATCH (a:TEST_SUITE {id: '_schema:suite:builder'}),
+ (b:FUNCTION {id: '_schema:fn:buildGraph'})
+CREATE (a)-[:TESTS]->(b);
+
+// TESTS : TEST_CASE → FUNCTION (individual test verifies a function)
+MATCH (a:TEST_CASE {id: '_schema:tc:buildNodes'}),
+ (b:FUNCTION {id: '_schema:fn:buildGraph'})
+CREATE (a)-[:TESTS]->(b);
+
+// TESTS : TEST_CASE → CLASS (individual test verifies a class)
+MATCH (a:TEST_CASE {id: '_schema:tc:buildNodes'}),
+ (b:CLASS {id: '_schema:cls:GraphBuilder'})
+CREATE (a)-[:TESTS]->(b);
+
+// TESTS : TEST_SUITE → FILE (fallback when symbol-level resolution not possible)
+MATCH (a:TEST_SUITE {id: '_schema:suite:builder'}),
+ (b:FILE {id: '_schema:file:builder'})
+CREATE (a)-[:TESTS]->(b);
+
+// ─────────────────────────────────────────────────────────────────────────
+// DOCS — Describes functions, classes, AND files
+// ─────────────────────────────────────────────────────────────────────────
+
+// SECTION_OF : SECTION → DOCUMENT
+MATCH (a:SECTION {id: '_schema:sec:arch'}),
+ (b:DOCUMENT {id: '_schema:doc:readme'})
+CREATE (a)-[:SECTION_OF]->(b);
+
+MATCH (a:SECTION {id: '_schema:sec:api'}),
+ (b:DOCUMENT {id: '_schema:doc:readme'})
+CREATE (a)-[:SECTION_OF]->(b);
+
+// NEXT_SECTION : SECTION → SECTION
+MATCH (a:SECTION {id: '_schema:sec:arch'}),
+ (b:SECTION {id: '_schema:sec:api'})
+CREATE (a)-[:NEXT_SECTION]->(b);
+
+// DOC_DESCRIBES : SECTION → CLASS
+MATCH (a:SECTION {id: '_schema:sec:arch'}),
+ (b:CLASS {id: '_schema:cls:GraphBuilder'})
+CREATE (a)-[:DOC_DESCRIBES {strength: 1.0, matchedName: 'GraphBuilder'}]->(b);
+
+// DOC_DESCRIBES : SECTION → FUNCTION
+MATCH (a:SECTION {id: '_schema:sec:api'}),
+ (b:FUNCTION {id: '_schema:fn:handleRequest'})
+CREATE (a)-[:DOC_DESCRIBES {strength: 1.0, matchedName: 'handleRequest'}]->(b);
+
+// DOC_DESCRIBES : SECTION → FILE
+MATCH (a:SECTION {id: '_schema:sec:arch'}),
+ (b:FILE {id: '_schema:file:builder'})
+CREATE (a)-[:DOC_DESCRIBES {strength: 0.9, matchedName: 'builder.ts'}]->(b);
+
+// ─────────────────────────────────────────────────────────────────────────
+// INTELLIGENCE — Rules, communities, features, tasks anchored to symbols
+// ─────────────────────────────────────────────────────────────────────────
+
+// BELONGS_TO : symbol → COMMUNITY
+MATCH (a:CLASS {id: '_schema:cls:GraphBuilder'}),
+ (b:COMMUNITY {id: '_schema:comm:graph'})
+CREATE (a)-[:BELONGS_TO]->(b);
+
+MATCH (a:FUNCTION {id: '_schema:fn:buildGraph'}),
+ (b:COMMUNITY {id: '_schema:comm:graph'})
+CREATE (a)-[:BELONGS_TO]->(b);
+
+// ANCHORED_BY : COMMUNITY → symbol (central node)
+MATCH (a:COMMUNITY {id: '_schema:comm:graph'}),
+ (b:CLASS {id: '_schema:cls:GraphBuilder'})
+CREATE (a)-[:ANCHORED_BY]->(b);
+
+// VIOLATES_RULE : FILE → RULE
+MATCH (a:FILE {id: '_schema:file:server'}),
+ (b:RULE {id: '_schema:rule:engineIsolation'})
+CREATE (a)-[:VIOLATES_RULE {severity: 'warning', message: 'Cross-layer import detected'}]->(b);
+
+// VIOLATES_RULE : CLASS → RULE
+MATCH (a:CLASS {id: '_schema:cls:GraphBuilder'}),
+ (b:RULE {id: '_schema:rule:layerAssignment'})
+CREATE (a)-[:VIOLATES_RULE {severity: 'info', message: 'No layer annotation'}]->(b);
+
+// VIOLATES_RULE : FUNCTION → RULE
+MATCH (a:FUNCTION {id: '_schema:fn:buildGraph'}),
+ (b:RULE {id: '_schema:rule:engineIsolation'})
+CREATE (a)-[:VIOLATES_RULE {severity: 'error', message: 'Engine function imports from tools layer'}]->(b);
+
+// DEPENDS_ON : RULE → RULE (rule dependency chain)
+MATCH (a:RULE {id: '_schema:rule:engineIsolation'}),
+ (b:RULE {id: '_schema:rule:layerAssignment'})
+CREATE (a)-[:DEPENDS_ON]->(b);
+
+// HAS_TASK : FEATURE → TASK
+MATCH (a:FEATURE {id: '_schema:feat:graphMVP'}),
+ (b:TASK {id: '_schema:task:cbFix'})
+CREATE (a)-[:HAS_TASK]->(b);
+
+// IMPLEMENTED_BY : FEATURE → CLASS
+MATCH (a:FEATURE {id: '_schema:feat:graphMVP'}),
+ (b:CLASS {id: '_schema:cls:GraphBuilder'})
+CREATE (a)-[:IMPLEMENTED_BY]->(b);
+
+// IMPLEMENTED_BY : FEATURE → FUNCTION
+MATCH (a:FEATURE {id: '_schema:feat:graphMVP'}),
+ (b:FUNCTION {id: '_schema:fn:buildGraph'})
+CREATE (a)-[:IMPLEMENTED_BY]->(b);
+
+// IMPLEMENTED_BY : TASK → CLASS
+MATCH (a:TASK {id: '_schema:task:cbFix'}),
+ (b:CLASS {id: '_schema:cls:GraphBuilder'})
+CREATE (a)-[:IMPLEMENTED_BY]->(b);
+
+// IMPLEMENTED_BY : TASK → FUNCTION
+MATCH (a:TASK {id: '_schema:task:cbFix'}),
+ (b:FUNCTION {id: '_schema:fn:buildGraph'})
+CREATE (a)-[:IMPLEMENTED_BY]->(b);
+
+// ─────────────────────────────────────────────────────────────────────────
+// AGENT MEMORY — Claims, episodes, learnings
+// ─────────────────────────────────────────────────────────────────────────
+
+// TARGETS : CLAIM → symbol (coordination lock)
+MATCH (a:CLAIM {id: '_schema:claim:refactor'}),
+ (b:CLASS {id: '_schema:cls:GraphBuilder'})
+CREATE (a)-[:TARGETS]->(b);
+
+// INVOLVES : EPISODE → symbol
+MATCH (a:EPISODE {id: '_schema:ep:decision1'}),
+ (b:FUNCTION {id: '_schema:fn:buildGraph'})
+CREATE (a)-[:INVOLVES]->(b);
+
+MATCH (a:EPISODE {id: '_schema:ep:decision1'}),
+ (b:CLASS {id: '_schema:cls:GraphBuilder'})
+CREATE (a)-[:INVOLVES]->(b);
+
+// NEXT_EPISODE : EPISODE → EPISODE (temporal chain)
+MATCH (a:EPISODE {id: '_schema:ep:decision1'}),
+ (b:EPISODE {id: '_schema:ep:decision2'})
+CREATE (a)-[:NEXT_EPISODE]->(b);
+
+// APPLIES_TO : LEARNING → symbol
+MATCH (a:LEARNING {id: '_schema:learn:builder'}),
+ (b:CLASS {id: '_schema:cls:GraphBuilder'})
+CREATE (a)-[:APPLIES_TO]->(b);
+
+MATCH (a:LEARNING {id: '_schema:learn:builder'}),
+ (b:FILE {id: '_schema:file:builder'})
+CREATE (a)-[:APPLIES_TO]->(b);
+
+
+// ============================================================================
+// Done.
+//
+// Verify:
+// SHOW INDEX INFO;
+// SHOW CONSTRAINT INFO;
+//
+// Visualize the full schema:
+// MATCH (n)-[r]->(m)
+// WHERE n.id STARTS WITH '_schema:' AND m.id STARTS WITH '_schema:'
+// RETURN n, r, m;
+//
+// Clean up sample data:
+// MATCH (n) WHERE n.id STARTS WITH '_schema:' DETACH DELETE n;
+//
+// Query examples from schema.json → queryPatterns:
+//
+// -- Where is this symbol?
+// MATCH (f:FILE)-[:CONTAINS]->(s) WHERE s.name = 'handleRequest'
+// RETURN f.relativePath, s.startLine;
+//
+// -- What does this function call?
+// MATCH (fn:FUNCTION {name: 'handleRequest'})-[:CALLS_TO]->(c)
+// RETURN c.name, c.filePath;
+//
+// -- What breaks if I change this class?
+// MATCH (c:CLASS {name: 'GraphBuilder'})<-[:TESTS]-(t)
+// RETURN t.name AS affectedTest
+// UNION
+// MATCH (c:CLASS {name: 'GraphBuilder'})<-[:EXTENDS|IMPLEMENTS]-(sub)
+// RETURN sub.name AS affectedSubclass;
+//
+// -- Which functions have no tests?
+// MATCH (fn:FUNCTION) WHERE NOT (fn)<-[:TESTS]-()
+// RETURN fn.name, fn.filePath;
+//
+// -- What code implements this feature?
+// MATCH (f:FEATURE {name: 'Code Graph MVP'})-[:IMPLEMENTED_BY]->(s)
+// RETURN labels(s)[0] AS type, s.name, s.filePath;
+//
+// -- Which functions violate rules?
+// MATCH (fn:FUNCTION)-[v:VIOLATES_RULE]->(r:RULE)
+// RETURN fn.name, r.description, v.severity;
+//
+// -- What is the anchor of each community?
+// MATCH (c:COMMUNITY)-[:ANCHORED_BY]->(s)
+// RETURN c.label, s.name, labels(s)[0];
+//
+// ============================================================================
diff --git a/scripts/schema.cypher b/scripts/schema.cypher
new file mode 100644
index 0000000..bcb4690
--- /dev/null
+++ b/scripts/schema.cypher
@@ -0,0 +1,529 @@
+// ============================================================================
+// lxDIG MCP — Graph Schema for Memgraph
+// ============================================================================
+//
+// Run this script in Memgraph Lab (Query Execution) to bootstrap the schema.
+// It is idempotent — safe to run multiple times.
+//
+// Schema layers:
+// 0 — Structure : FOLDER
+// 1 — Files : FILE
+// 2 — Symbols : FUNCTION, CLASS, VARIABLE, IMPORT, EXPORT
+// 3 — Tests : TEST_SUITE, TEST_CASE
+// 4 — Docs : DOCUMENT, SECTION
+// 5 — Intelligence: COMMUNITY, RULE, FEATURE, TASK
+// 6 — Agent Memory: EPISODE, LEARNING, CLAIM, GRAPH_TX
+//
+// Relationship types (25):
+// CONTAINS, IMPORTS, EXPORTS, REFERENCES, DEPENDS_ON, CALLS_TO,
+// EXTENDS, IMPLEMENTS, SECTION_OF, NEXT_SECTION, DOC_DESCRIBES,
+// TESTS, BELONGS_TO, TARGETS, INVOLVES, NEXT_EPISODE, APPLIES_TO,
+// VIOLATES_RULE
+//
+// ============================================================================
+
+// ---------------------------------------------------------------------------
+// 1. INDEXES — Primary lookup (id) + projectId scoping
+// ---------------------------------------------------------------------------
+// Every node label gets an index on `id` (primary key) and `projectId`
+// (multi-tenant scoping). These are the two fields used in every MERGE/MATCH.
+
+// Layer 0 — Structure
+CREATE INDEX ON :FOLDER(id);
+CREATE INDEX ON :FOLDER(projectId);
+
+// Layer 1 — Files
+CREATE INDEX ON :FILE(id);
+CREATE INDEX ON :FILE(projectId);
+CREATE INDEX ON :FILE(path);
+CREATE INDEX ON :FILE(relativePath);
+
+// Layer 2 — Symbols
+CREATE INDEX ON :FUNCTION(id);
+CREATE INDEX ON :FUNCTION(projectId);
+CREATE INDEX ON :FUNCTION(name);
+
+CREATE INDEX ON :CLASS(id);
+CREATE INDEX ON :CLASS(projectId);
+CREATE INDEX ON :CLASS(name);
+
+CREATE INDEX ON :VARIABLE(id);
+CREATE INDEX ON :VARIABLE(projectId);
+
+CREATE INDEX ON :IMPORT(id);
+CREATE INDEX ON :IMPORT(projectId);
+
+CREATE INDEX ON :EXPORT(id);
+CREATE INDEX ON :EXPORT(projectId);
+
+// Layer 3 — Tests
+CREATE INDEX ON :TEST_SUITE(id);
+CREATE INDEX ON :TEST_SUITE(projectId);
+
+CREATE INDEX ON :TEST_CASE(id);
+CREATE INDEX ON :TEST_CASE(projectId);
+
+// Layer 4 — Docs
+CREATE INDEX ON :DOCUMENT(id);
+CREATE INDEX ON :DOCUMENT(projectId);
+
+CREATE INDEX ON :SECTION(id);
+CREATE INDEX ON :SECTION(projectId);
+
+// Layer 5 — Intelligence
+CREATE INDEX ON :COMMUNITY(id);
+CREATE INDEX ON :COMMUNITY(projectId);
+
+CREATE INDEX ON :RULE(id);
+
+CREATE INDEX ON :FEATURE(id);
+CREATE INDEX ON :FEATURE(projectId);
+
+CREATE INDEX ON :TASK(id);
+CREATE INDEX ON :TASK(projectId);
+
+// Layer 6 — Agent Memory
+CREATE INDEX ON :EPISODE(id);
+CREATE INDEX ON :EPISODE(projectId);
+CREATE INDEX ON :EPISODE(agentId);
+CREATE INDEX ON :EPISODE(sessionId);
+
+CREATE INDEX ON :LEARNING(id);
+CREATE INDEX ON :LEARNING(projectId);
+
+CREATE INDEX ON :CLAIM(id);
+CREATE INDEX ON :CLAIM(projectId);
+CREATE INDEX ON :CLAIM(agentId);
+
+CREATE INDEX ON :GRAPH_TX(id);
+CREATE INDEX ON :GRAPH_TX(projectId);
+
+// ---------------------------------------------------------------------------
+// 2. EXISTENCE CONSTRAINTS — Required properties on core nodes
+// ---------------------------------------------------------------------------
+// Memgraph enforces these at write time. Prevents incomplete nodes from
+// entering the graph.
+
+CREATE CONSTRAINT ON (n:FILE) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:FOLDER) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:FUNCTION) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:CLASS) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:VARIABLE) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:IMPORT) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:EXPORT) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:TEST_SUITE) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:TEST_CASE) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:DOCUMENT) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:SECTION) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:COMMUNITY) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:RULE) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:FEATURE) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:TASK) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:EPISODE) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:LEARNING) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:CLAIM) ASSERT EXISTS (n.id);
+CREATE CONSTRAINT ON (n:GRAPH_TX) ASSERT EXISTS (n.id);
+
+// ---------------------------------------------------------------------------
+// 3. UNIQUENESS CONSTRAINTS — Prevent duplicate nodes
+// ---------------------------------------------------------------------------
+// Each node's `id` is globally unique within its label. This also implicitly
+// creates an index, but we keep the explicit CREATE INDEX above for clarity.
+
+CREATE CONSTRAINT ON (n:FILE) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:FOLDER) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:FUNCTION) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:CLASS) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:VARIABLE) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:IMPORT) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:EXPORT) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:TEST_SUITE) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:TEST_CASE) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:DOCUMENT) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:SECTION) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:COMMUNITY) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:RULE) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:FEATURE) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:TASK) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:EPISODE) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:LEARNING) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:CLAIM) ASSERT n.id IS UNIQUE;
+CREATE CONSTRAINT ON (n:GRAPH_TX) ASSERT n.id IS UNIQUE;
+
+// ---------------------------------------------------------------------------
+// 4. SCHEMA VERIFICATION — Run after to confirm everything was created
+// ---------------------------------------------------------------------------
+// Uncomment and run this block to verify:
+//
+// SHOW INDEX INFO;
+// SHOW CONSTRAINT INFO;
+//
+// Expected: 33 indexes, 19 existence constraints, 19 uniqueness constraints
+
+// ---------------------------------------------------------------------------
+// 5. SCHEMA VISUALIZATION — Sample nodes + edges for Memgraph Lab graph view
+// ---------------------------------------------------------------------------
+// Creates one node per label and one edge per relationship type so you can
+// see the full schema as a visual graph in Memgraph Lab's "Graph" tab.
+// All sample nodes use id prefix "_schema:" so they won't collide with
+// real data and can be deleted with:
+// MATCH (n) WHERE n.id STARTS WITH '_schema:' DETACH DELETE n;
+
+// --- Layer 0: Structure ---
+CREATE (folder:FOLDER {
+ id: '_schema:folder',
+ name: 'src/',
+ path: '/project/src',
+ projectId: '_schema'
+});
+
+// --- Layer 1: Files ---
+CREATE (file:FILE {
+ id: '_schema:file',
+ name: 'server.ts',
+ path: '/project/src/server.ts',
+ relativePath: 'src/server.ts',
+ language: 'TypeScript',
+ LOC: 150,
+ hash: '',
+ projectId: '_schema'
+});
+
+// --- Layer 2: Symbols ---
+CREATE (fn:FUNCTION {
+ id: '_schema:function',
+ name: 'handleRequest',
+ kind: 'function',
+ filePath: '/project/src/server.ts',
+ startLine: 10,
+ endLine: 45,
+ LOC: 35,
+ projectId: '_schema'
+});
+
+CREATE (cls:CLASS {
+ id: '_schema:class',
+ name: 'GraphBuilder',
+ kind: 'class',
+ filePath: '/project/src/graph/builder.ts',
+ startLine: 1,
+ endLine: 800,
+ LOC: 800,
+ projectId: '_schema'
+});
+
+CREATE (parentCls:CLASS {
+ id: '_schema:class:parent',
+ name: 'BaseBuilder',
+ kind: 'class',
+ projectId: '_schema'
+});
+
+CREATE (ifaceCls:CLASS {
+ id: '_schema:class:iface',
+ name: 'IBuilder',
+ kind: 'interface',
+ projectId: '_schema'
+});
+
+CREATE (variable:VARIABLE {
+ id: '_schema:variable',
+ name: 'config',
+ kind: 'const',
+ startLine: 5,
+ projectId: '_schema'
+});
+
+CREATE (imp:IMPORT {
+ id: '_schema:import',
+ source: './utils/helpers',
+ specifiers: ['parseFile', 'logger'],
+ startLine: 1,
+ projectId: '_schema'
+});
+
+CREATE (exp:EXPORT {
+ id: '_schema:export',
+ name: 'GraphBuilder',
+ isDefault: true,
+ startLine: 800,
+ projectId: '_schema'
+});
+
+// --- Layer 3: Tests ---
+CREATE (suite:TEST_SUITE {
+ id: '_schema:test_suite',
+ name: 'GraphBuilder tests',
+ type: 'describe',
+ category: 'unit',
+ startLine: 1,
+ endLine: 100,
+ filePath: 'src/graph/__tests__/builder.test.ts',
+ projectId: '_schema'
+});
+
+CREATE (tc:TEST_CASE {
+ id: '_schema:test_case',
+ name: 'should build file nodes',
+ startLine: 10,
+ endLine: 25,
+ filePath: 'src/graph/__tests__/builder.test.ts',
+ projectId: '_schema'
+});
+
+// --- Layer 4: Docs ---
+CREATE (doc:DOCUMENT {
+ id: '_schema:document',
+ relativePath: 'README.md',
+ filePath: '/project/README.md',
+ title: 'lxDIG MCP',
+ kind: 'readme',
+ wordCount: 2500,
+ hash: '',
+ projectId: '_schema'
+});
+
+CREATE (sec:SECTION {
+ id: '_schema:section',
+ heading: 'Architecture',
+ level: 2,
+ content: 'The system is composed of...',
+ wordCount: 350,
+ startLine: 42,
+ projectId: '_schema'
+});
+
+// --- Layer 5: Intelligence ---
+CREATE (community:COMMUNITY {
+ id: '_schema:community',
+ label: 'Graph subsystem',
+ summary: 'Core graph builder and orchestrator',
+ memberCount: 8,
+ centralNode: '_schema:class',
+ computedAt: 0,
+ projectId: '_schema'
+});
+
+CREATE (rule:RULE {
+ id: '_schema:rule',
+ severity: 'error',
+ pattern: 'engine-isolation',
+ description: 'Engines must not import from tools'
+});
+
+CREATE (feature:FEATURE {
+ id: '_schema:feature',
+ name: 'Code Graph MVP',
+ status: 'completed',
+ projectId: '_schema'
+});
+
+CREATE (task:TASK {
+ id: '_schema:task',
+ name: 'Fix circuit breaker',
+ status: 'in-progress',
+ featureId: '_schema:feature',
+ projectId: '_schema'
+});
+
+// --- Layer 6: Agent Memory ---
+CREATE (episode:EPISODE {
+ id: '_schema:episode',
+ type: 'DECISION',
+ content: 'Adopted chunked writes for Memgraph',
+ agentId: 'copilot-01',
+ sessionId: 'session-001',
+ timestamp: 0,
+ outcome: 'success',
+ projectId: '_schema'
+});
+
+CREATE (learning:LEARNING {
+ id: '_schema:learning',
+ content: 'Repeated activity around builder.ts',
+ extractedAt: 0,
+ confidence: 0.8,
+ projectId: '_schema'
+});
+
+CREATE (claim:CLAIM {
+ id: '_schema:claim',
+ agentId: 'copilot-01',
+ intent: 'Refactoring builder',
+ status: 'active',
+ projectId: '_schema'
+});
+
+CREATE (graphTx:GRAPH_TX {
+ id: '_schema:graph_tx',
+ type: 'rebuild',
+ timestamp: 0,
+ mode: 'full',
+ sourceDir: '/project/src',
+ projectId: '_schema'
+});
+
+// --- Target file for import resolution ---
+CREATE (targetFile:FILE {
+ id: '_schema:file:target',
+ name: 'helpers.ts',
+ path: '/project/src/utils/helpers.ts',
+ relativePath: 'src/utils/helpers.ts',
+ language: 'TypeScript',
+ LOC: 80,
+ hash: '',
+ projectId: '_schema'
+});
+
+// --- Callee function for CALLS_TO ---
+CREATE (callee:FUNCTION {
+ id: '_schema:function:callee',
+ name: 'parseFile',
+ kind: 'function',
+ filePath: '/project/src/utils/helpers.ts',
+ startLine: 20,
+ endLine: 40,
+ LOC: 20,
+ projectId: '_schema'
+});
+
+// --- Test target file ---
+CREATE (testTarget:FILE {
+ id: '_schema:file:tested',
+ name: 'builder.ts',
+ path: '/project/src/graph/builder.ts',
+ relativePath: 'src/graph/builder.ts',
+ language: 'TypeScript',
+ LOC: 800,
+ hash: '',
+ projectId: '_schema'
+});
+
+// ---------------------------------------------------------------------------
+// 6. RELATIONSHIPS — All 18 active edge types
+// ---------------------------------------------------------------------------
+
+// CONTAINS : FOLDER → FILE
+MATCH (a:FOLDER {id: '_schema:folder'}),
+ (b:FILE {id: '_schema:file'})
+CREATE (a)-[:CONTAINS]->(b);
+
+// CONTAINS : FOLDER → FOLDER (self-referencing hierarchy)
+// (skipped — would need a second folder node)
+
+// CONTAINS : FILE → FUNCTION
+MATCH (a:FILE {id: '_schema:file'}),
+ (b:FUNCTION {id: '_schema:function'})
+CREATE (a)-[:CONTAINS]->(b);
+
+// CONTAINS : FILE → CLASS
+MATCH (a:FILE {id: '_schema:file:tested'}),
+ (b:CLASS {id: '_schema:class'})
+CREATE (a)-[:CONTAINS]->(b);
+
+// CONTAINS : FILE → VARIABLE
+MATCH (a:FILE {id: '_schema:file'}),
+ (b:VARIABLE {id: '_schema:variable'})
+CREATE (a)-[:CONTAINS]->(b);
+
+// CONTAINS : FILE → TEST_SUITE
+MATCH (a:FILE {id: '_schema:file:tested'}),
+ (b:TEST_SUITE {id: '_schema:test_suite'})
+CREATE (a)-[:CONTAINS]->(b);
+
+// CONTAINS : FILE → TEST_CASE
+MATCH (a:FILE {id: '_schema:file:tested'}),
+ (b:TEST_CASE {id: '_schema:test_case'})
+CREATE (a)-[:CONTAINS]->(b);
+
+// CONTAINS : TEST_SUITE → TEST_CASE
+MATCH (a:TEST_SUITE {id: '_schema:test_suite'}),
+ (b:TEST_CASE {id: '_schema:test_case'})
+CREATE (a)-[:CONTAINS]->(b);
+
+// IMPORTS : FILE → IMPORT
+MATCH (a:FILE {id: '_schema:file'}),
+ (b:IMPORT {id: '_schema:import'})
+CREATE (a)-[:IMPORTS]->(b);
+
+// EXPORTS : FILE → EXPORT
+MATCH (a:FILE {id: '_schema:file:tested'}),
+ (b:EXPORT {id: '_schema:export'})
+CREATE (a)-[:EXPORTS]->(b);
+
+// REFERENCES : IMPORT → FILE (resolved target)
+MATCH (a:IMPORT {id: '_schema:import'}),
+ (b:FILE {id: '_schema:file:target'})
+CREATE (a)-[:REFERENCES]->(b);
+
+// DEPENDS_ON : FILE → FILE (source depends on target)
+MATCH (a:FILE {id: '_schema:file'}),
+ (b:FILE {id: '_schema:file:target'})
+CREATE (a)-[:DEPENDS_ON]->(b);
+
+// CALLS_TO : FUNCTION → FUNCTION (with line property)
+MATCH (a:FUNCTION {id: '_schema:function'}),
+ (b:FUNCTION {id: '_schema:function:callee'})
+CREATE (a)-[:CALLS_TO {line: 15}]->(b);
+
+// EXTENDS : CLASS → CLASS (inheritance)
+MATCH (a:CLASS {id: '_schema:class'}),
+ (b:CLASS {id: '_schema:class:parent'})
+CREATE (a)-[:EXTENDS]->(b);
+
+// IMPLEMENTS : CLASS → CLASS (interface)
+MATCH (a:CLASS {id: '_schema:class'}),
+ (b:CLASS {id: '_schema:class:iface'})
+CREATE (a)-[:IMPLEMENTS]->(b);
+
+// SECTION_OF : SECTION → DOCUMENT
+MATCH (a:SECTION {id: '_schema:section'}),
+ (b:DOCUMENT {id: '_schema:document'})
+CREATE (a)-[:SECTION_OF]->(b);
+
+// NEXT_SECTION : SECTION → SECTION
+// (skipped — would need a second section node)
+
+// DOC_DESCRIBES : SECTION → FILE/FUNCTION/CLASS (with strength)
+MATCH (a:SECTION {id: '_schema:section'}),
+ (b:CLASS {id: '_schema:class'})
+CREATE (a)-[:DOC_DESCRIBES {strength: 1.0, matchedName: 'GraphBuilder'}]->(b);
+
+// TESTS : TEST_SUITE → FILE (test coverage link)
+MATCH (a:TEST_SUITE {id: '_schema:test_suite'}),
+ (b:FILE {id: '_schema:file:tested'})
+CREATE (a)-[:TESTS]->(b);
+
+// BELONGS_TO : node → COMMUNITY
+MATCH (a:CLASS {id: '_schema:class'}),
+ (b:COMMUNITY {id: '_schema:community'})
+CREATE (a)-[:BELONGS_TO]->(b);
+
+// VIOLATES_RULE : FILE → RULE
+MATCH (a:FILE {id: '_schema:file'}),
+ (b:RULE {id: '_schema:rule'})
+CREATE (a)-[:VIOLATES_RULE {severity: 'warning', message: 'Cross-layer import'}]->(b);
+
+// TARGETS : CLAIM → node (coordination lock target)
+MATCH (a:CLAIM {id: '_schema:claim'}),
+ (b:FILE {id: '_schema:file:tested'})
+CREATE (a)-[:TARGETS]->(b);
+
+// INVOLVES : EPISODE → node (entity reference)
+MATCH (a:EPISODE {id: '_schema:episode'}),
+ (b:CLASS {id: '_schema:class'})
+CREATE (a)-[:INVOLVES]->(b);
+
+// NEXT_EPISODE : EPISODE → EPISODE
+// (skipped — would need a second episode node)
+
+// APPLIES_TO : LEARNING → node
+MATCH (a:LEARNING {id: '_schema:learning'}),
+ (b:FILE {id: '_schema:file:tested'})
+CREATE (a)-[:APPLIES_TO]->(b);
+
+// ---------------------------------------------------------------------------
+// Done. Run SHOW INDEX INFO; and SHOW CONSTRAINT INFO; to verify.
+// To visualize: MATCH (n) WHERE n.id STARTS WITH '_schema:' RETURN n;
+// To clean up: MATCH (n) WHERE n.id STARTS WITH '_schema:' DETACH DELETE n;
+// ---------------------------------------------------------------------------
diff --git a/scripts/schema.json b/scripts/schema.json
new file mode 100644
index 0000000..bf1891e
--- /dev/null
+++ b/scripts/schema.json
@@ -0,0 +1,923 @@
+{
+ "$schema": "lxDIG Graph Schema v2",
+ "$description": "Two-perspective graph: physical layer (folders/files) for localization, logical layer (classes/functions/variables) for behavior. All intelligence, tests, docs, and rules connect primarily to the logical layer.",
+
+ "designPrinciples": [
+ "Physical layer (FOLDER → FILE) exists only for localization — answering 'where is this?'",
+ "Logical layer (CLASS, FUNCTION, VARIABLE) is the primary connection target for everything else",
+ "A single file may contain the entire logical graph if the project was never refactored",
+ "FILE → symbol edges (CONTAINS) bridge physical to logical — the only required cross-layer link",
+ "Every non-structural relationship should target symbols (CLASS/FUNCTION), not files"
+ ],
+
+ "layers": {
+ "0_structure": {
+ "description": "Physical file system tree — used for localization only",
+ "nodes": ["FOLDER"]
+ },
+ "1_files": {
+ "description": "Source files — bridge between physical and logical layers",
+ "nodes": ["FILE"]
+ },
+ "2_symbols": {
+ "description": "Logical code units — the primary graph. All intelligence connects here.",
+ "nodes": ["FUNCTION", "CLASS", "VARIABLE", "IMPORT", "EXPORT"]
+ },
+ "3_tests": {
+ "description": "Test structure — maps to logical symbols, not files",
+ "nodes": ["TEST_SUITE", "TEST_CASE"]
+ },
+ "4_docs": {
+ "description": "Documentation — describes logical symbols and files",
+ "nodes": ["DOCUMENT", "SECTION"]
+ },
+ "5_intelligence": {
+ "description": "Computed insights — rules, features, tasks, communities all anchor to symbols",
+ "nodes": ["COMMUNITY", "RULE", "FEATURE", "TASK"]
+ },
+ "6_agent_memory": {
+ "description": "Runtime agent state — episodes, learnings, coordination locks",
+ "nodes": ["EPISODE", "LEARNING", "CLAIM", "GRAPH_TX"]
+ }
+ },
+
+ "nodes": {
+ "FOLDER": {
+ "layer": 0,
+ "builtBy": "GraphBuilder",
+ "transactional": true,
+ "properties": {
+ "id": {
+ "type": "string",
+ "required": true,
+ "unique": true,
+ "indexed": true,
+ "description": "Scoped folder ID (projectId:folder:path)"
+ },
+ "name": { "type": "string", "required": false, "description": "Folder basename" },
+ "path": { "type": "string", "required": false, "description": "Absolute path" },
+ "projectId": {
+ "type": "string",
+ "required": false,
+ "indexed": true,
+ "description": "Multi-tenant scope"
+ }
+ }
+ },
+
+ "FILE": {
+ "layer": 1,
+ "builtBy": "GraphBuilder",
+ "transactional": true,
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "name": { "type": "string", "required": false },
+ "path": {
+ "type": "string",
+ "required": false,
+ "indexed": true,
+ "description": "Absolute path"
+ },
+ "relativePath": {
+ "type": "string",
+ "required": false,
+ "indexed": true,
+ "description": "Workspace-relative path"
+ },
+ "language": { "type": "string", "required": false },
+ "LOC": { "type": "integer", "required": false },
+ "summary": {
+ "type": "string",
+ "required": false,
+ "description": "LLM-generated summary (requires summarizer)"
+ },
+ "hash": {
+ "type": "string",
+ "required": false,
+ "description": "Content hash for change detection"
+ },
+ "scipId": { "type": "string", "required": false },
+ "projectId": { "type": "string", "required": false, "indexed": true },
+ "projectFingerprint": { "type": "string", "required": false },
+ "validFrom": { "type": "integer", "required": false },
+ "validTo": { "type": "integer", "required": false },
+ "createdAt": { "type": "integer", "required": false },
+ "txId": { "type": "string", "required": false },
+ "lastModified": { "type": "datetime", "required": false }
+ }
+ },
+
+ "FUNCTION": {
+ "layer": 2,
+ "builtBy": "GraphBuilder",
+ "transactional": true,
+ "description": "The primary logical unit. Functions, methods, arrow functions, handlers.",
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "name": { "type": "string", "required": false, "indexed": true },
+ "kind": {
+ "type": "string",
+ "required": false,
+ "description": "function | method | arrow | generator | constructor"
+ },
+ "filePath": {
+ "type": "string",
+ "required": false,
+ "description": "Absolute path of containing file"
+ },
+ "path": { "type": "string", "required": false },
+ "relativePath": { "type": "string", "required": false },
+ "startLine": { "type": "integer", "required": false },
+ "endLine": { "type": "integer", "required": false },
+ "LOC": { "type": "integer", "required": false },
+ "summary": { "type": "string", "required": false },
+ "parameters": {
+ "type": "string",
+ "required": false,
+ "description": "JSON-serialized parameter list"
+ },
+ "scipId": { "type": "string", "required": false },
+ "isExported": { "type": "boolean", "required": false },
+ "stub": {
+ "type": "boolean",
+ "required": false,
+ "description": "true for CALLS_TO stubs not yet resolved"
+ },
+ "projectId": { "type": "string", "required": false, "indexed": true },
+ "validFrom": { "type": "integer", "required": false },
+ "validTo": { "type": "integer", "required": false },
+ "createdAt": { "type": "integer", "required": false },
+ "txId": { "type": "string", "required": false }
+ }
+ },
+
+ "CLASS": {
+ "layer": 2,
+ "builtBy": "GraphBuilder",
+ "transactional": true,
+ "description": "Classes, interfaces, type aliases, enums. Primary container for methods.",
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "name": { "type": "string", "required": false, "indexed": true },
+ "kind": {
+ "type": "string",
+ "required": false,
+ "description": "class | interface | enum | type"
+ },
+ "filePath": { "type": "string", "required": false },
+ "path": { "type": "string", "required": false },
+ "relativePath": { "type": "string", "required": false },
+ "startLine": { "type": "integer", "required": false },
+ "endLine": { "type": "integer", "required": false },
+ "LOC": { "type": "integer", "required": false },
+ "summary": { "type": "string", "required": false },
+ "scipId": { "type": "string", "required": false },
+ "isExported": { "type": "boolean", "required": false },
+ "projectId": { "type": "string", "required": false, "indexed": true },
+ "validFrom": { "type": "integer", "required": false },
+ "validTo": { "type": "integer", "required": false },
+ "createdAt": { "type": "integer", "required": false },
+ "txId": { "type": "string", "required": false }
+ }
+ },
+
+ "VARIABLE": {
+ "layer": 2,
+ "builtBy": "GraphBuilder",
+ "transactional": true,
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "name": { "type": "string", "required": false },
+ "kind": { "type": "string", "required": false, "description": "const | let | var" },
+ "startLine": { "type": "integer", "required": false },
+ "type": {
+ "type": "string",
+ "required": false,
+ "description": "TypeScript type annotation"
+ },
+ "projectId": { "type": "string", "required": false, "indexed": true }
+ }
+ },
+
+ "IMPORT": {
+ "layer": 2,
+ "builtBy": "GraphBuilder",
+ "transactional": true,
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "source": {
+ "type": "string",
+ "required": false,
+ "description": "Import specifier (e.g. './utils/helpers')"
+ },
+ "specifiers": { "type": "list", "required": false },
+ "startLine": { "type": "integer", "required": false },
+ "summary": { "type": "string", "required": false },
+ "projectId": { "type": "string", "required": false, "indexed": true },
+ "validFrom": { "type": "integer", "required": false },
+ "validTo": { "type": "integer", "required": false },
+ "createdAt": { "type": "integer", "required": false },
+ "txId": { "type": "string", "required": false }
+ }
+ },
+
+ "EXPORT": {
+ "layer": 2,
+ "builtBy": "GraphBuilder",
+ "transactional": true,
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "name": { "type": "string", "required": false },
+ "isDefault": { "type": "boolean", "required": false },
+ "startLine": { "type": "integer", "required": false },
+ "projectId": { "type": "string", "required": false, "indexed": true }
+ }
+ },
+
+ "TEST_SUITE": {
+ "layer": 3,
+ "builtBy": "GraphBuilder",
+ "transactional": true,
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "name": { "type": "string", "required": false },
+ "type": {
+ "type": "string",
+ "required": false,
+ "description": "describe | suite | context"
+ },
+ "category": {
+ "type": "string",
+ "required": false,
+ "description": "unit | integration | e2e"
+ },
+ "startLine": { "type": "integer", "required": false },
+ "endLine": { "type": "integer", "required": false },
+ "filePath": { "type": "string", "required": false },
+ "projectId": { "type": "string", "required": false, "indexed": true }
+ }
+ },
+
+ "TEST_CASE": {
+ "layer": 3,
+ "builtBy": "GraphBuilder",
+ "transactional": true,
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "name": { "type": "string", "required": false },
+ "startLine": { "type": "integer", "required": false },
+ "endLine": { "type": "integer", "required": false },
+ "filePath": { "type": "string", "required": false },
+ "projectId": { "type": "string", "required": false, "indexed": true }
+ }
+ },
+
+ "DOCUMENT": {
+ "layer": 4,
+ "builtBy": "DocsEngine",
+ "transactional": true,
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "relativePath": { "type": "string", "required": false },
+ "filePath": { "type": "string", "required": false },
+ "title": { "type": "string", "required": false },
+ "kind": {
+ "type": "string",
+ "required": false,
+ "description": "readme | guide | adr | changelog"
+ },
+ "wordCount": { "type": "integer", "required": false },
+ "hash": { "type": "string", "required": false },
+ "projectId": { "type": "string", "required": false, "indexed": true },
+ "validFrom": { "type": "integer", "required": false },
+ "validTo": { "type": "integer", "required": false },
+ "txId": { "type": "string", "required": false }
+ }
+ },
+
+ "SECTION": {
+ "layer": 4,
+ "builtBy": "DocsEngine",
+ "transactional": true,
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "heading": { "type": "string", "required": false },
+ "level": { "type": "integer", "required": false },
+ "content": { "type": "string", "required": false, "description": "Trimmed to 4000 chars" },
+ "wordCount": { "type": "integer", "required": false },
+ "startLine": { "type": "integer", "required": false },
+ "docId": { "type": "string", "required": false },
+ "relativePath": { "type": "string", "required": false },
+ "projectId": { "type": "string", "required": false, "indexed": true },
+ "txId": { "type": "string", "required": false }
+ }
+ },
+
+ "COMMUNITY": {
+ "layer": 5,
+ "builtBy": "CommunityDetector (on-demand)",
+ "transactional": false,
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "label": { "type": "string", "required": false },
+ "summary": { "type": "string", "required": false },
+ "memberCount": { "type": "integer", "required": false },
+ "centralNode": {
+ "type": "string",
+ "required": false,
+ "description": "ID of the most central member"
+ },
+ "computedAt": { "type": "integer", "required": false },
+ "projectId": { "type": "string", "required": false, "indexed": true }
+ }
+ },
+
+ "RULE": {
+ "layer": 5,
+ "builtBy": "ArchitectureEngine (on-demand)",
+ "transactional": false,
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "severity": {
+ "type": "string",
+ "required": false,
+ "description": "error | warning | info"
+ },
+ "pattern": { "type": "string", "required": false },
+ "description": { "type": "string", "required": false }
+ }
+ },
+
+ "FEATURE": {
+ "layer": 5,
+ "builtBy": "ProgressEngine (on-demand)",
+ "transactional": false,
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "name": { "type": "string", "required": false },
+ "status": {
+ "type": "string",
+ "required": false,
+ "description": "planned | in-progress | completed"
+ },
+ "description": { "type": "string", "required": false },
+ "startedAt": { "type": "integer", "required": false },
+ "createdAt": { "type": "integer", "required": false },
+ "projectId": { "type": "string", "required": false, "indexed": true }
+ }
+ },
+
+ "TASK": {
+ "layer": 5,
+ "builtBy": "ProgressEngine (on-demand)",
+ "transactional": false,
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "name": { "type": "string", "required": false },
+ "status": {
+ "type": "string",
+ "required": false,
+ "description": "todo | in-progress | completed | blocked"
+ },
+ "description": { "type": "string", "required": false },
+ "featureId": { "type": "string", "required": false },
+ "assignee": { "type": "string", "required": false },
+ "dueDate": { "type": "string", "required": false },
+ "createdAt": { "type": "integer", "required": false },
+ "projectId": { "type": "string", "required": false, "indexed": true }
+ }
+ },
+
+ "EPISODE": {
+ "layer": 6,
+ "builtBy": "EpisodeEngine (runtime)",
+ "transactional": false,
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "type": {
+ "type": "string",
+ "required": false,
+ "description": "DECISION | LEARNING | OBSERVATION | REFLECTION"
+ },
+ "content": { "type": "string", "required": false },
+ "agentId": { "type": "string", "required": false, "indexed": true },
+ "sessionId": { "type": "string", "required": false, "indexed": true },
+ "taskId": { "type": "string", "required": false },
+ "timestamp": { "type": "integer", "required": false },
+ "outcome": {
+ "type": "string",
+ "required": false,
+ "description": "success | failure | partial"
+ },
+ "projectId": { "type": "string", "required": false, "indexed": true }
+ }
+ },
+
+ "LEARNING": {
+ "layer": 6,
+ "builtBy": "EpisodeEngine (runtime, via reflect)",
+ "transactional": false,
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "content": { "type": "string", "required": false },
+ "extractedAt": { "type": "integer", "required": false },
+ "confidence": { "type": "float", "required": false, "description": "0.0–1.0" },
+ "reflectionId": { "type": "string", "required": false },
+ "projectId": { "type": "string", "required": false, "indexed": true }
+ }
+ },
+
+ "CLAIM": {
+ "layer": 6,
+ "builtBy": "CoordinationEngine (runtime)",
+ "transactional": false,
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "agentId": { "type": "string", "required": false, "indexed": true },
+ "intent": { "type": "string", "required": false },
+ "status": { "type": "string", "required": false, "description": "active | released" },
+ "taskId": { "type": "string", "required": false },
+ "sessionId": { "type": "string", "required": false },
+ "projectId": { "type": "string", "required": false, "indexed": true }
+ }
+ },
+
+ "GRAPH_TX": {
+ "layer": 6,
+ "builtBy": "ToolHandler (runtime, on graph_rebuild)",
+ "transactional": false,
+ "properties": {
+ "id": { "type": "string", "required": true, "unique": true, "indexed": true },
+ "type": { "type": "string", "required": false, "description": "rebuild | incremental" },
+ "timestamp": { "type": "integer", "required": false },
+ "mode": { "type": "string", "required": false, "description": "full | incremental" },
+ "sourceDir": { "type": "string", "required": false },
+ "projectId": { "type": "string", "required": false, "indexed": true }
+ }
+ }
+ },
+
+ "relationships": {
+ "_comment_physical": "=== PHYSICAL LAYER (localization) ===",
+
+ "FOLDER_CONTAINS_FOLDER": {
+ "type": "CONTAINS",
+ "from": "FOLDER",
+ "to": "FOLDER",
+ "cardinality": "1:N",
+ "description": "Folder hierarchy — parent contains child directories",
+ "properties": {}
+ },
+
+ "FOLDER_CONTAINS_FILE": {
+ "type": "CONTAINS",
+ "from": "FOLDER",
+ "to": "FILE",
+ "cardinality": "1:N",
+ "description": "Folder contains files",
+ "properties": {}
+ },
+
+ "_comment_bridge": "=== BRIDGE: Physical → Logical ===",
+
+ "FILE_CONTAINS_FUNCTION": {
+ "type": "CONTAINS",
+ "from": "FILE",
+ "to": "FUNCTION",
+ "cardinality": "1:N",
+ "description": "File contains function definitions — the primary bridge from physical to logical",
+ "properties": {}
+ },
+
+ "FILE_CONTAINS_CLASS": {
+ "type": "CONTAINS",
+ "from": "FILE",
+ "to": "CLASS",
+ "cardinality": "1:N",
+ "description": "File contains class/interface definitions",
+ "properties": {}
+ },
+
+ "FILE_CONTAINS_VARIABLE": {
+ "type": "CONTAINS",
+ "from": "FILE",
+ "to": "VARIABLE",
+ "cardinality": "1:N",
+ "description": "File contains top-level variable declarations",
+ "properties": {}
+ },
+
+ "FILE_IMPORTS": {
+ "type": "IMPORTS",
+ "from": "FILE",
+ "to": "IMPORT",
+ "cardinality": "1:N",
+ "description": "File has import statements",
+ "properties": {}
+ },
+
+ "FILE_EXPORTS": {
+ "type": "EXPORTS",
+ "from": "FILE",
+ "to": "EXPORT",
+ "cardinality": "1:N",
+ "description": "File has export declarations",
+ "properties": {}
+ },
+
+ "_comment_logical": "=== LOGICAL LAYER (behavior) ===",
+
+ "CALLS_TO": {
+ "type": "CALLS_TO",
+ "from": "FUNCTION",
+ "to": "FUNCTION",
+ "cardinality": "N:N",
+ "description": "Function calls another function — the primary behavioral edge",
+ "properties": {
+ "line": { "type": "integer", "description": "Line number of the call site" }
+ }
+ },
+
+ "EXTENDS": {
+ "type": "EXTENDS",
+ "from": "CLASS",
+ "to": "CLASS",
+ "cardinality": "N:1",
+ "description": "Class inherits from another class",
+ "properties": {}
+ },
+
+ "IMPLEMENTS": {
+ "type": "IMPLEMENTS",
+ "from": "CLASS",
+ "to": "CLASS",
+ "cardinality": "N:N",
+ "description": "Class implements an interface",
+ "properties": {}
+ },
+
+ "IMPORT_REFERENCES_FILE": {
+ "type": "REFERENCES",
+ "from": "IMPORT",
+ "to": "FILE",
+ "cardinality": "N:1",
+ "description": "Import statement resolved to a target file",
+ "properties": {}
+ },
+
+ "FILE_DEPENDS_ON_FILE": {
+ "type": "DEPENDS_ON",
+ "from": "FILE",
+ "to": "FILE",
+ "cardinality": "N:N",
+ "description": "Source file depends on target file (derived from resolved imports)",
+ "properties": {}
+ },
+
+ "_comment_tests": "=== TEST LAYER — connected to logical symbols ===",
+
+ "FILE_CONTAINS_TEST_SUITE": {
+ "type": "CONTAINS",
+ "from": "FILE",
+ "to": "TEST_SUITE",
+ "cardinality": "1:N",
+ "description": "Test file contains test suites",
+ "properties": {}
+ },
+
+ "FILE_CONTAINS_TEST_CASE": {
+ "type": "CONTAINS",
+ "from": "FILE",
+ "to": "TEST_CASE",
+ "cardinality": "1:N",
+ "description": "Test file contains individual test cases",
+ "properties": {}
+ },
+
+ "TEST_SUITE_CONTAINS_TEST_CASE": {
+ "type": "CONTAINS",
+ "from": "TEST_SUITE",
+ "to": "TEST_CASE",
+ "cardinality": "1:N",
+ "description": "Suite groups test cases (describe → it)",
+ "properties": {}
+ },
+
+ "TEST_SUITE_TESTS_FUNCTION": {
+ "type": "TESTS",
+ "from": "TEST_SUITE",
+ "to": "FUNCTION",
+ "cardinality": "N:N",
+ "description": "Test suite covers a function — primary test-to-logic link",
+ "properties": {}
+ },
+
+ "TEST_SUITE_TESTS_CLASS": {
+ "type": "TESTS",
+ "from": "TEST_SUITE",
+ "to": "CLASS",
+ "cardinality": "N:N",
+ "description": "Test suite covers a class",
+ "properties": {}
+ },
+
+ "TEST_CASE_TESTS_FUNCTION": {
+ "type": "TESTS",
+ "from": "TEST_CASE",
+ "to": "FUNCTION",
+ "cardinality": "N:N",
+ "description": "Individual test case verifies a specific function",
+ "properties": {}
+ },
+
+ "TEST_CASE_TESTS_CLASS": {
+ "type": "TESTS",
+ "from": "TEST_CASE",
+ "to": "CLASS",
+ "cardinality": "N:N",
+ "description": "Individual test case verifies a specific class",
+ "properties": {}
+ },
+
+ "TEST_SUITE_TESTS_FILE": {
+ "type": "TESTS",
+ "from": "TEST_SUITE",
+ "to": "FILE",
+ "cardinality": "N:N",
+ "description": "Fallback: test suite covers a file when symbol-level resolution isn't possible",
+ "properties": {}
+ },
+
+ "_comment_docs": "=== DOCS LAYER — describes logical symbols and files ===",
+
+ "SECTION_OF": {
+ "type": "SECTION_OF",
+ "from": "SECTION",
+ "to": "DOCUMENT",
+ "cardinality": "N:1",
+ "description": "Section belongs to a document",
+ "properties": {}
+ },
+
+ "NEXT_SECTION": {
+ "type": "NEXT_SECTION",
+ "from": "SECTION",
+ "to": "SECTION",
+ "cardinality": "1:1",
+ "description": "Reading order within a document",
+ "properties": {}
+ },
+
+ "DOC_DESCRIBES_FILE": {
+ "type": "DOC_DESCRIBES",
+ "from": "SECTION",
+ "to": "FILE",
+ "cardinality": "N:N",
+ "description": "Documentation section describes a file",
+ "properties": {
+ "strength": { "type": "float", "description": "Match confidence 0.0–1.0" },
+ "matchedName": { "type": "string", "description": "The backtick reference that matched" }
+ }
+ },
+
+ "DOC_DESCRIBES_FUNCTION": {
+ "type": "DOC_DESCRIBES",
+ "from": "SECTION",
+ "to": "FUNCTION",
+ "cardinality": "N:N",
+ "description": "Documentation section describes a function",
+ "properties": {
+ "strength": { "type": "float" },
+ "matchedName": { "type": "string" }
+ }
+ },
+
+ "DOC_DESCRIBES_CLASS": {
+ "type": "DOC_DESCRIBES",
+ "from": "SECTION",
+ "to": "CLASS",
+ "cardinality": "N:N",
+ "description": "Documentation section describes a class or interface",
+ "properties": {
+ "strength": { "type": "float" },
+ "matchedName": { "type": "string" }
+ }
+ },
+
+ "_comment_intelligence": "=== INTELLIGENCE LAYER — anchored to logical symbols ===",
+
+ "BELONGS_TO_COMMUNITY": {
+ "type": "BELONGS_TO",
+ "from": ["FUNCTION", "CLASS", "FILE"],
+ "to": "COMMUNITY",
+ "cardinality": "N:1",
+ "description": "Symbol or file is a member of a detected community",
+ "properties": {}
+ },
+
+ "COMMUNITY_ANCHORED_BY": {
+ "type": "ANCHORED_BY",
+ "from": "COMMUNITY",
+ "to": ["FUNCTION", "CLASS"],
+ "cardinality": "1:1",
+ "description": "The central/most-connected node in the community — the anchor point",
+ "properties": {}
+ },
+
+ "VIOLATES_RULE_FILE": {
+ "type": "VIOLATES_RULE",
+ "from": "FILE",
+ "to": "RULE",
+ "cardinality": "N:N",
+ "description": "File-level architecture violation",
+ "properties": {
+ "severity": { "type": "string" },
+ "message": { "type": "string" }
+ }
+ },
+
+ "VIOLATES_RULE_CLASS": {
+ "type": "VIOLATES_RULE",
+ "from": "CLASS",
+ "to": "RULE",
+ "cardinality": "N:N",
+ "description": "Class-level architecture violation (e.g. god class, wrong layer)",
+ "properties": {
+ "severity": { "type": "string" },
+ "message": { "type": "string" }
+ }
+ },
+
+ "VIOLATES_RULE_FUNCTION": {
+ "type": "VIOLATES_RULE",
+ "from": "FUNCTION",
+ "to": "RULE",
+ "cardinality": "N:N",
+ "description": "Function-level violation (e.g. cross-layer call, excessive complexity)",
+ "properties": {
+ "severity": { "type": "string" },
+ "message": { "type": "string" }
+ }
+ },
+
+ "RULE_DEPENDS_ON_RULE": {
+ "type": "DEPENDS_ON",
+ "from": "RULE",
+ "to": "RULE",
+ "cardinality": "N:N",
+ "description": "Rule dependency — e.g. 'no-cross-layer-import' depends on 'layer-assignment'",
+ "properties": {}
+ },
+
+ "FEATURE_HAS_TASK": {
+ "type": "HAS_TASK",
+ "from": "FEATURE",
+ "to": "TASK",
+ "cardinality": "1:N",
+ "description": "Feature is broken down into tasks",
+ "properties": {}
+ },
+
+ "FEATURE_IMPLEMENTED_BY_FUNCTION": {
+ "type": "IMPLEMENTED_BY",
+ "from": "FEATURE",
+ "to": "FUNCTION",
+ "cardinality": "N:N",
+ "description": "Feature is implemented by these functions — primary traceability link",
+ "properties": {}
+ },
+
+ "FEATURE_IMPLEMENTED_BY_CLASS": {
+ "type": "IMPLEMENTED_BY",
+ "from": "FEATURE",
+ "to": "CLASS",
+ "cardinality": "N:N",
+ "description": "Feature is implemented by these classes",
+ "properties": {}
+ },
+
+ "TASK_IMPLEMENTED_BY_FUNCTION": {
+ "type": "IMPLEMENTED_BY",
+ "from": "TASK",
+ "to": "FUNCTION",
+ "cardinality": "N:N",
+ "description": "Task touches these functions",
+ "properties": {}
+ },
+
+ "TASK_IMPLEMENTED_BY_CLASS": {
+ "type": "IMPLEMENTED_BY",
+ "from": "TASK",
+ "to": "CLASS",
+ "cardinality": "N:N",
+ "description": "Task touches these classes",
+ "properties": {}
+ },
+
+ "_comment_agent": "=== AGENT MEMORY LAYER ===",
+
+ "CLAIM_TARGETS": {
+ "type": "TARGETS",
+ "from": "CLAIM",
+ "to": ["FILE", "FUNCTION", "CLASS"],
+ "cardinality": "1:N",
+ "description": "Agent coordination lock on a file or symbol",
+ "properties": {}
+ },
+
+ "EPISODE_INVOLVES": {
+ "type": "INVOLVES",
+ "from": "EPISODE",
+ "to": ["FILE", "FUNCTION", "CLASS", "VARIABLE"],
+ "cardinality": "N:N",
+ "description": "Episode references these code entities",
+ "properties": {}
+ },
+
+ "NEXT_EPISODE": {
+ "type": "NEXT_EPISODE",
+ "from": "EPISODE",
+ "to": "EPISODE",
+ "cardinality": "1:1",
+ "description": "Temporal chain within an agent session",
+ "properties": {}
+ },
+
+ "LEARNING_APPLIES_TO": {
+ "type": "APPLIES_TO",
+ "from": "LEARNING",
+ "to": ["FILE", "FUNCTION", "CLASS"],
+ "cardinality": "N:N",
+ "description": "Learning is relevant to these code entities",
+ "properties": {}
+ }
+ },
+
+ "relationshipSummary": {
+ "total": 33,
+ "uniqueTypes": 18,
+ "byType": {
+ "CONTAINS": {
+ "count": 8,
+ "variants": "FOLDER→FOLDER, FOLDER→FILE, FILE→FUNCTION, FILE→CLASS, FILE→VARIABLE, FILE→TEST_SUITE, FILE→TEST_CASE, TEST_SUITE→TEST_CASE"
+ },
+ "IMPORTS": { "count": 1, "variants": "FILE→IMPORT" },
+ "EXPORTS": { "count": 1, "variants": "FILE→EXPORT" },
+ "REFERENCES": { "count": 1, "variants": "IMPORT→FILE" },
+ "DEPENDS_ON": { "count": 2, "variants": "FILE→FILE, RULE→RULE" },
+ "CALLS_TO": { "count": 1, "variants": "FUNCTION→FUNCTION" },
+ "EXTENDS": { "count": 1, "variants": "CLASS→CLASS" },
+ "IMPLEMENTS": { "count": 1, "variants": "CLASS→CLASS" },
+ "TESTS": {
+ "count": 5,
+ "variants": "TEST_SUITE→FUNCTION, TEST_SUITE→CLASS, TEST_SUITE→FILE, TEST_CASE→FUNCTION, TEST_CASE→CLASS"
+ },
+ "SECTION_OF": { "count": 1, "variants": "SECTION→DOCUMENT" },
+ "NEXT_SECTION": { "count": 1, "variants": "SECTION→SECTION" },
+ "DOC_DESCRIBES": { "count": 3, "variants": "SECTION→FILE, SECTION→FUNCTION, SECTION→CLASS" },
+ "BELONGS_TO": { "count": 1, "variants": "FUNCTION/CLASS/FILE→COMMUNITY" },
+ "ANCHORED_BY": { "count": 1, "variants": "COMMUNITY→FUNCTION/CLASS" },
+ "VIOLATES_RULE": { "count": 3, "variants": "FILE→RULE, CLASS→RULE, FUNCTION→RULE" },
+ "HAS_TASK": { "count": 1, "variants": "FEATURE→TASK" },
+ "IMPLEMENTED_BY": {
+ "count": 4,
+ "variants": "FEATURE→FUNCTION, FEATURE→CLASS, TASK→FUNCTION, TASK→CLASS"
+ },
+ "TARGETS": { "count": 1, "variants": "CLAIM→FILE/FUNCTION/CLASS" },
+ "INVOLVES": { "count": 1, "variants": "EPISODE→FILE/FUNCTION/CLASS/VARIABLE" },
+ "NEXT_EPISODE": { "count": 1, "variants": "EPISODE→EPISODE" },
+ "APPLIES_TO": { "count": 1, "variants": "LEARNING→FILE/FUNCTION/CLASS" }
+ }
+ },
+
+ "queryPatterns": {
+ "localization": {
+ "description": "Where is this symbol?",
+ "query": "MATCH (f:FILE)-[:CONTAINS]->(s) WHERE s.name = $name RETURN f.relativePath, s.startLine"
+ },
+ "behavior": {
+ "description": "What does this function call?",
+ "query": "MATCH (fn:FUNCTION {name: $name})-[:CALLS_TO]->(callee) RETURN callee.name, callee.filePath"
+ },
+ "impact": {
+ "description": "What breaks if I change this class?",
+ "query": "MATCH (c:CLASS {name: $name})<-[:TESTS]-(t) RETURN t.name AS affectedTest UNION MATCH (c:CLASS {name: $name})<-[:EXTENDS|IMPLEMENTS]-(sub) RETURN sub.name AS affectedSubclass"
+ },
+ "coverage": {
+ "description": "Which functions have no tests?",
+ "query": "MATCH (fn:FUNCTION) WHERE NOT (fn)<-[:TESTS]-() RETURN fn.name, fn.filePath"
+ },
+ "traceability": {
+ "description": "What code implements this feature?",
+ "query": "MATCH (f:FEATURE {name: $name})-[:IMPLEMENTED_BY]->(s) RETURN labels(s)[0] AS type, s.name, s.filePath"
+ },
+ "violations": {
+ "description": "Which functions violate architecture rules?",
+ "query": "MATCH (fn:FUNCTION)-[:VIOLATES_RULE]->(r:RULE) RETURN fn.name, r.description, r.severity"
+ },
+ "community": {
+ "description": "What is the central symbol of each community?",
+ "query": "MATCH (c:COMMUNITY)-[:ANCHORED_BY]->(s) RETURN c.label, s.name, labels(s)[0]"
+ }
+ }
+}
diff --git a/scripts/test-all-tools.mjs b/scripts/test-all-tools.mjs
new file mode 100644
index 0000000..784a907
--- /dev/null
+++ b/scripts/test-all-tools.mjs
@@ -0,0 +1,499 @@
+#!/usr/bin/env node
+/**
+ * lxDIG MCP — Full Integration Test (v2, all parameters corrected)
+ * Tests all 39 tools via stdio JSON-RPC against a fresh DB.
+ */
+
+import { spawn } from "child_process";
+
+const WORKSPACE = "/home/alex_rod/projects/lexDIG-MCP";
+const PROJECT_ID = "lxdig-mcp";
+const ELEMENT_FUNC = "lxdig-mcp:build.ts:main:18";
+const ELEMENT_FILE = "src/tools/handlers/test-tools.ts";
+
+// ─── RPC plumbing ──────────────────────────────────────────────────────────
+let idSeq = 1,
+ proc;
+const pending = new Map();
+let lineBuffer = "";
+
+const send = (msg) => proc.stdin.write(JSON.stringify(msg) + "\n");
+
+function rpc(method, params) {
+ return new Promise((resolve, reject) => {
+ const id = idSeq++;
+ const timer = setTimeout(() => {
+ pending.delete(id);
+ reject(new Error(`Timeout: ${method}`));
+ }, 90000);
+ pending.set(id, { resolve, reject, timer, method });
+ send({ jsonrpc: "2.0", id, method, params });
+ });
+}
+
+const callTool = (name, args) => rpc("tools/call", { name, arguments: args });
+
+function handleMessage(msg) {
+ if (msg.id != null && pending.has(msg.id)) {
+ const { resolve, reject, timer } = pending.get(msg.id);
+ pending.delete(msg.id);
+ clearTimeout(timer);
+ msg.error
+ ? reject(
+ Object.assign(new Error(msg.error.message || "RPC error"), {
+ rpcError: msg.error,
+ }),
+ )
+ : resolve(msg.result);
+ }
+}
+
+// ─── Result helpers ────────────────────────────────────────────────────────
+function parseResult(result) {
+ if (!result?.content) return { ok: !!result, data: result, raw: result };
+ const text = result.content.map((c) => c.text || "").join("");
+ try {
+ const p = JSON.parse(text);
+ return {
+ ok: p.ok !== false && !p.errorCode,
+ data: p.data ?? p,
+ raw: p,
+ text,
+ };
+ } catch {
+ return { ok: text.length > 0, data: null, text };
+ }
+}
+
+let passed = 0,
+ failed = 0,
+ total = 0;
+const allResults = [];
+
+function log(name, result, { expectEmpty = false, note = "" } = {}) {
+ total++;
+ let ok, summary, detail;
+ if (result instanceof Error) {
+ ok = false;
+ summary = result.message.slice(0, 120);
+ detail = JSON.stringify(result.rpcError || {}, null, 2)
+ .split("\n")
+ .slice(0, 6)
+ .join("\n");
+ } else {
+ const p = parseResult(result);
+ ok = p.ok;
+ summary = p.raw?.summary || "";
+ detail = JSON.stringify(p.data || {}, null, 2)
+ .split("\n")
+ .slice(0, 10)
+ .join("\n");
+ }
+ // Pre-index empty state: errors about "no data" are expected
+ if (
+ expectEmpty &&
+ !ok &&
+ /no indexed|no test|no symbols|empty|0 episode|not found/i.test(summary)
+ )
+ ok = true;
+ const icon = ok ? "✅" : "❌";
+ const emptyTag = expectEmpty ? " [empty-ok]" : "";
+ const noteTag = note ? ` ← ${note}` : "";
+ console.log(
+ `\n${icon} [${String(total).padStart(2)}] ${name}${emptyTag}${noteTag}`,
+ );
+ if (summary) console.log(` ${summary}`);
+ if (detail && detail !== "{}")
+ detail
+ .split("\n")
+ .slice(0, 7)
+ .forEach((l) => console.log(` ${l}`));
+ allResults.push({ name, ok, summary });
+ if (ok) passed++;
+ else failed++;
+}
+
+async function t(name, args, flags = {}) {
+ try {
+ const r = await callTool(name, args);
+ log(name, r, flags);
+ return parseResult(r);
+ } catch (e) {
+ log(name, e, flags);
+ return { ok: false, data: null };
+ }
+}
+
+// ─── Test runner ───────────────────────────────────────────────────────────
+async function run() {
+ console.log("════════════════════════════════════════════════════════════");
+ console.log(" lxDIG MCP — Full Integration Test (39 tools, stdio)");
+ console.log(" Memgraph ✓ empty Qdrant ✓ empty");
+ console.log("════════════════════════════════════════════════════════════\n");
+
+ console.log("── INIT: MCP handshake ──");
+ await rpc("initialize", {
+ protocolVersion: "2024-11-05",
+ capabilities: {},
+ clientInfo: { name: "test", version: "2" },
+ });
+ send({ jsonrpc: "2.0", method: "notifications/initialized", params: {} });
+ console.log(" ✅ Handshake OK\n");
+
+ // ── P1: List & health ──────────────────────────────────────────────────
+ console.log("── PHASE 1: List & health ───────────────────");
+ await t("tools_list", {});
+ await t("graph_health", { profile: "balanced" });
+
+ // ── P2: Set workspace ─────────────────────────────────────────────────
+ console.log("\n── PHASE 2: Set workspace ───────────────────");
+ await t("graph_set_workspace", {
+ projectId: PROJECT_ID,
+ workspaceRoot: WORKSPACE,
+ });
+
+ // ── P3: Pre-index empty checks ────────────────────────────────────────
+ console.log("\n── PHASE 3: Empty-state checks ──────────────");
+ await t(
+ "search_docs",
+ { query: "architecture layers", limit: 3 },
+ { expectEmpty: true },
+ );
+ await t(
+ "semantic_search",
+ { query: "HandlerBridge", projectId: PROJECT_ID, limit: 3 },
+ { expectEmpty: true },
+ );
+ await t("feature_status", { featureId: "list" }, { expectEmpty: true });
+ await t(
+ "episode_recall",
+ { query: "graph build", limit: 3 },
+ { expectEmpty: true },
+ );
+ await t(
+ "decision_query",
+ { query: "architecture design decisions", limit: 3 },
+ { expectEmpty: true },
+ );
+ await t("reflect", { limit: 5, profile: "compact" }, { expectEmpty: true });
+ await t(
+ "coordination_overview",
+ { projectId: PROJECT_ID },
+ { expectEmpty: true },
+ );
+ await t("agent_status", { agentId: "test-agent-01" }, { expectEmpty: true });
+
+ // ── P4: Setup helpers ─────────────────────────────────────────────────
+ console.log("\n── PHASE 4: Setup helpers ───────────────────");
+ await t("setup_copilot_instructions", {
+ targetPath: WORKSPACE,
+ projectName: "lxDIG-MCP",
+ overwrite: true,
+ });
+ await t("contract_validate", {
+ tool: "graph_rebuild",
+ arguments: { projectId: PROJECT_ID, mode: "full" },
+ });
+
+ // ── P5: Graph rebuild ─────────────────────────────────────────────────
+ console.log("\n── PHASE 5: Graph rebuild (full index) ──────");
+ const rebuildRes = await t("graph_rebuild", {
+ projectId: PROJECT_ID,
+ mode: "full",
+ workspaceRoot: WORKSPACE,
+ });
+ const txId = rebuildRes?.raw?.data?.txId || rebuildRes?.data?.txId || null;
+ console.log(` txId: ${txId || "(not captured)"}`);
+
+ await new Promise((r) => setTimeout(r, 4000));
+ await t(
+ "graph_health",
+ { profile: "balanced" },
+ { note: "should show nodes > 0" },
+ );
+
+ // ── P6: Graph queries ─────────────────────────────────────────────────
+ console.log("\n── PHASE 6: Graph queries ───────────────────");
+ await t("graph_query", {
+ query:
+ "MATCH (n) RETURN labels(n)[0] AS label, count(n) AS cnt ORDER BY cnt DESC LIMIT 8",
+ projectId: PROJECT_ID,
+ });
+ await t("graph_query", {
+ query:
+ "MATCH (f:FILE) RETURN f.relativePath ORDER BY f.relativePath LIMIT 5",
+ projectId: PROJECT_ID,
+ });
+ // diff_since: use txId from rebuild (it shows changes since that tx = the rebuild itself)
+ await t(
+ "diff_since",
+ {
+ since: txId || new Date(Date.now() - 120000).toISOString(),
+ projectId: PROJECT_ID,
+ profile: "compact",
+ },
+ { note: `since txId or -2m` },
+ );
+
+ // ── P7: Semantic & code intelligence ─────────────────────────────────
+ console.log("\n── PHASE 7: Semantic & code intelligence ────");
+ await t("semantic_search", {
+ query: "HandlerBridge formatSuccess errorEnvelope",
+ projectId: PROJECT_ID,
+ limit: 5,
+ });
+ await t("find_pattern", {
+ pattern: "tool handler impl registry",
+ projectId: PROJECT_ID,
+ limit: 5,
+ });
+ await t("find_similar_code", {
+ elementId: ELEMENT_FUNC,
+ projectId: PROJECT_ID,
+ limit: 5,
+ });
+ await t("code_explain", {
+ element: "HandlerBridge",
+ depth: 2,
+ projectId: PROJECT_ID,
+ });
+ await t("semantic_diff", {
+ elementId1: "lxdig-mcp:build.ts:main:18",
+ elementId2: "lxdig-mcp:query.ts:main:14",
+ projectId: PROJECT_ID,
+ });
+ await t("semantic_slice", {
+ symbol: "HandlerBridge",
+ context: "body",
+ projectId: PROJECT_ID,
+ });
+
+ // ── P8: Code clustering ───────────────────────────────────────────────
+ console.log("\n── PHASE 8: Code clustering ─────────────────");
+ await t("code_clusters", { type: "file", count: 5, projectId: PROJECT_ID });
+
+ // ── P9: Architecture ──────────────────────────────────────────────────
+ console.log("\n── PHASE 9: Architecture ────────────────────");
+ await t("arch_validate", {
+ projectId: PROJECT_ID,
+ files: ["src/tools/handlers/test-tools.ts", "src/engines/test-engine.ts"],
+ });
+ await t("arch_suggest", {
+ name: "MultiTenantEngine",
+ codeType: "engine",
+ dependencies: ["types", "utils"],
+ projectId: PROJECT_ID,
+ });
+ await t("blocking_issues", { projectId: PROJECT_ID });
+
+ // ── P10: Docs index & search ──────────────────────────────────────────
+ console.log("\n── PHASE 10: Docs index → search ────────────");
+ const docsRes = await t("index_docs", {
+ projectId: PROJECT_ID,
+ paths: [
+ `${WORKSPACE}/README.md`,
+ `${WORKSPACE}/ARCHITECTURE.md`,
+ `${WORKSPACE}/QUICK_START.md`,
+ `${WORKSPACE}/docs/TOOL_PATTERNS.md`,
+ `${WORKSPACE}/docs/PROJECT_FEATURES_CAPABILITIES.md`,
+ ],
+ });
+ await t(
+ "search_docs",
+ {
+ query: "architecture layers MCP tools graph",
+ limit: 5,
+ projectId: PROJECT_ID,
+ },
+ { note: "should return results" },
+ );
+ await t(
+ "search_docs",
+ { symbol: "HandlerBridge", limit: 3, projectId: PROJECT_ID },
+ { note: "symbol lookup" },
+ );
+
+ // ── P11: Ref query ────────────────────────────────────────────────────
+ console.log("\n── PHASE 11: Ref query ──────────────────────");
+ await t("ref_query", {
+ query: "tool handler pattern HandlerBridge",
+ repoPath: WORKSPACE,
+ limit: 5,
+ });
+
+ // ── P12: Impact ───────────────────────────────────────────────────────
+ console.log("\n── PHASE 12: Impact analysis ────────────────");
+ await t("impact_analyze", {
+ changedFiles: ["src/tools/handlers/test-tools.ts", "src/config.ts"],
+ projectId: PROJECT_ID,
+ });
+
+ // ── P13: Test intelligence ────────────────────────────────────────────
+ console.log("\n── PHASE 13: Test intelligence ──────────────");
+ await t("test_categorize", { projectId: PROJECT_ID });
+ await t("test_select", {
+ changedFiles: ["src/engines/test-engine.ts", "src/config.ts"],
+ projectId: PROJECT_ID,
+ });
+ await t("suggest_tests", {
+ elementId: ELEMENT_FUNC,
+ limit: 5,
+ profile: "compact",
+ });
+ await t(
+ "test_run",
+ { testFiles: ["src/utils/__tests__/validation.test.ts"], parallel: false },
+ { note: "real vitest run" },
+ );
+
+ // ── P14: Progress & features ──────────────────────────────────────────
+ console.log("\n── PHASE 14: Progress & features ────────────");
+ await t("feature_status", { featureId: "list" });
+ await t("feature_status", { featureId: "phase-1" });
+ await t("progress_query", {
+ query: "completed features high priority",
+ projectId: PROJECT_ID,
+ });
+ await t(
+ "task_update",
+ {
+ taskId: "lang-agnostic-fix",
+ status: "completed",
+ note: "Language-agnostic runner done",
+ projectId: PROJECT_ID,
+ },
+ { note: "task may not exist" },
+ );
+
+ // ── P15: Episode memory ───────────────────────────────────────────────
+ console.log("\n── PHASE 15: Episode memory ─────────────────");
+ await t("episode_add", {
+ type: "DECISION",
+ content:
+ "Adopted language-agnostic test runner: config.testing.testRunner drives pytest/rspec/go-test/vitest dispatch",
+ entities: ["ArchitectureEngine", "HandlerBridge", "TestEngine"],
+ outcome: "success",
+ metadata: {
+ rationale:
+ "Consistent runner dispatch reduces CI friction across Python/Ruby/Go/TS projects",
+ component: "test-engine",
+ },
+ });
+ await t("episode_add", {
+ type: "LEARNING",
+ content:
+ "Test categorization patterns differ by language: .integration.test.* (JS), _integration_test.py (Python), _integration_test.go (Go), _integration_spec.rb (Ruby)",
+ entities: ["categorizeTest", "getMirrorTestPath"],
+ outcome: "success",
+ metadata: { languages: ["TypeScript", "Python", "Ruby", "Go"] },
+ });
+ await t(
+ "episode_recall",
+ { query: "language agnostic test runner", limit: 5 },
+ { note: "should find 2 episodes" },
+ );
+ await t("decision_query", {
+ query: "language agnostic architecture",
+ limit: 5,
+ });
+ await t(
+ "reflect",
+ { limit: 10, profile: "balanced" },
+ { note: "should surface learnings" },
+ );
+
+ // ── P16: Coordination ────────────────────────────────────────────────
+ console.log("\n── PHASE 16: Coordination ───────────────────");
+ const claimRes = await t("agent_claim", {
+ agentId: "test-agent-01",
+ targetId: ELEMENT_FILE,
+ intent: "Validating full tool coverage for lxDIG integration test",
+ taskId: "tool-integration-test",
+ sessionId: "test-session-001",
+ });
+ const claimId =
+ claimRes?.data?.claimId || claimRes?.raw?.data?.claimId || null;
+ console.log(` claimId: ${claimId || "(not captured)"}`);
+
+ await t(
+ "agent_status",
+ { agentId: "test-agent-01" },
+ { note: "should show 1 active claim" },
+ );
+ await t("coordination_overview", { projectId: PROJECT_ID });
+ await t("context_pack", {
+ task: "Implement multi-tenant support for lxDIG: API key auth + per-user project scoping",
+ taskId: "multi-tenant-impl",
+ agentId: "test-agent-01",
+ includeLearnings: true,
+ });
+ await t(
+ "agent_release",
+ {
+ claimId: claimId || `test-agent-01:tool-integration-test`,
+ outcome: "Integration test completed — all tools exercised",
+ },
+ { note: claimId ? "using real claimId" : "using guessed claimId" },
+ );
+
+ // ── P17: One-shot init ────────────────────────────────────────────────
+ console.log("\n── PHASE 17: init_project_setup (one-shot) ──");
+ await t("init_project_setup", {
+ projectId: PROJECT_ID,
+ workspaceRoot: WORKSPACE,
+ });
+
+ // ── Summary ───────────────────────────────────────────────────────────
+ const pct = ((passed / total) * 100).toFixed(0);
+ console.log("\n════════════════════════════════════════════════════════════");
+ console.log(
+ ` RESULTS: ${passed}/${total} passed (${pct}%), ${failed} failed`,
+ );
+ console.log("════════════════════════════════════════════════════════════");
+ if (failed > 0) {
+ console.log("\nFailed tools:");
+ allResults
+ .filter((r) => !r.ok)
+ .forEach((r) =>
+ console.log(` ❌ ${r.name}: ${r.summary.slice(0, 100)}`),
+ );
+ } else {
+ console.log("\n 🎉 All tools exercised successfully!");
+ }
+}
+
+// ─── Bootstrap ────────────────────────────────────────────────────────────
+proc = spawn("node", ["dist/server.js"], {
+ cwd: WORKSPACE,
+ stdio: ["pipe", "pipe", "pipe"],
+ env: { ...process.env, MCP_TRANSPORT: "stdio" },
+});
+
+proc.stdout.on("data", (chunk) => {
+ lineBuffer += chunk.toString();
+ const lines = lineBuffer.split("\n");
+ lineBuffer = lines.pop();
+ for (const line of lines) {
+ const t = line.trim();
+ if (t)
+ try {
+ handleMessage(JSON.parse(t));
+ } catch {}
+ }
+});
+proc.stderr.on("data", (d) => {
+ if (process.env.DEBUG) process.stderr.write("[server] " + d);
+});
+proc.on("exit", (code) => {
+ if (code && code !== 0) console.error(`\nServer exited: ${code}`);
+});
+
+run()
+ .catch((e) => {
+ console.error("\nFatal:", e.message);
+ process.exit(1);
+ })
+ .finally(() => {
+ proc.stdin.end();
+ setTimeout(() => process.exit(failed > 0 ? 1 : 0), 500);
+ });
diff --git a/scripts/test-mcp-integration.sh b/scripts/test-mcp-integration.sh
index b7c79f6..c25fa1e 100755
--- a/scripts/test-mcp-integration.sh
+++ b/scripts/test-mcp-integration.sh
@@ -9,7 +9,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOCKER_COMPOSE_FILE="${DOCKER_COMPOSE_FILE:-$SCRIPT_DIR/docker-compose.yml}"
echo "=========================================="
-echo "lexRAG MCP Integration Test"
+echo "lexDIG MCP Integration Test"
echo "=========================================="
echo ""
@@ -109,6 +109,6 @@ if [ $FAILED -eq 0 ]; then
exit 0
else
echo -e "${RED}✗ Some tests failed. Check logs:${NC}"
- echo " docker logs lexrag-mcp"
+ echo " docker logs lexdig-mcp"
exit 1
fi
diff --git a/scripts/test-mcp.sh b/scripts/test-mcp.sh
index 7dfc2ed..6b8012f 100755
--- a/scripts/test-mcp.sh
+++ b/scripts/test-mcp.sh
@@ -6,7 +6,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOCKER_COMPOSE_FILE="${DOCKER_COMPOSE_FILE:-$SCRIPT_DIR/docker-compose.yml}"
echo "======================================"
-echo "lexRAG MCP Test"
+echo "lexDIG MCP Test"
echo "======================================"
echo ""
diff --git a/src/cli/build.ts b/src/cli/build.ts
index e697d55..d156914 100644
--- a/src/cli/build.ts
+++ b/src/cli/build.ts
@@ -15,6 +15,7 @@ import * as fs from "fs";
import { GraphOrchestrator } from "../graph/orchestrator.js";
import MemgraphClient from "../graph/client.js";
import * as env from "../env.js";
+import { logger } from "../utils/logger.js";
async function main() {
const args = process.argv.slice(2);
@@ -22,27 +23,27 @@ async function main() {
const isVerbose = args.includes("--verbose");
const projectRoot = path.resolve(process.cwd());
- console.log("🔨 Code Graph Builder");
- console.log(`📁 Project root: ${projectRoot}`);
- console.log(`🔄 Build mode: ${isFullBuild ? "FULL" : "INCREMENTAL"}`);
- console.log("");
+ logger.error("🔨 Code Graph Builder");
+ logger.error(`📁 Project root: ${projectRoot}`);
+ logger.error(`🔄 Build mode: ${isFullBuild ? "FULL" : "INCREMENTAL"}`);
+ logger.error("");
try {
// Initialize Memgraph client
- console.log("🔌 Connecting to Memgraph...");
+ logger.error("🔌 Connecting to Memgraph...");
const memgraph = new MemgraphClient({
host: env.MEMGRAPH_HOST,
port: env.MEMGRAPH_PORT,
});
await memgraph.connect();
- console.log("✅ Connected to Memgraph\n");
+ logger.error("✅ Connected to Memgraph\n");
// Create orchestrator
const orchestrator = new GraphOrchestrator(memgraph, isVerbose);
// Build the graph
- console.log("📊 Building code graph...\n");
+ logger.error("📊 Building code graph...\n");
const startTime = Date.now();
const result = await orchestrator.build({
@@ -53,7 +54,7 @@ async function main() {
"node_modules/**",
"dist/**",
"build/**",
- ".lxrag/**",
+ ".lxdig/**",
"**/*.test.ts",
"**/*.test.tsx",
"**/__tests__/**",
@@ -63,28 +64,28 @@ async function main() {
const duration = Date.now() - startTime;
// Display results
- console.log("\n📈 Build Results:");
- console.log(` ✅ Success: ${result.success}`);
- console.log(` ⏱️ Duration: ${(duration / 1000).toFixed(2)}s`);
- console.log(` 📄 Files processed: ${result.filesProcessed}`);
- console.log(` 📍 Nodes created: ${result.nodesCreated}`);
- console.log(` 🔗 Relationships created: ${result.relationshipsCreated}`);
+ logger.error("\n📈 Build Results:");
+ logger.error(` ✅ Success: ${result.success}`);
+ logger.error(` ⏱️ Duration: ${(duration / 1000).toFixed(2)}s`);
+ logger.error(` 📄 Files processed: ${result.filesProcessed}`);
+ logger.error(` 📍 Nodes created: ${result.nodesCreated}`);
+ logger.error(` 🔗 Relationships created: ${result.relationshipsCreated}`);
if (result.filesChanged > 0) {
- console.log(` 🔄 Files changed: ${result.filesChanged}`);
+ logger.error(` 🔄 Files changed: ${result.filesChanged}`);
}
if (result.errors.length > 0) {
- console.log(`\n❌ Errors (${result.errors.length}):`);
- result.errors.forEach((err) => console.log(` - ${err}`));
+ logger.error(`\n❌ Errors (${result.errors.length}):`);
+ result.errors.forEach((err) => logger.error(` - ${err}`));
}
if (result.warnings.length > 0) {
- console.log(`\n⚠️ Warnings (${result.warnings.length}):`);
- result.warnings.forEach((warn) => console.log(` - ${warn}`));
+ logger.error(`\n⚠️ Warnings (${result.warnings.length}):`);
+ result.warnings.forEach((warn) => logger.error(` - ${warn}`));
}
// Save build metadata
- const codeGraphDir = path.join(projectRoot, ".lxrag");
+ const codeGraphDir = path.join(projectRoot, ".lxdig");
if (!fs.existsSync(codeGraphDir)) {
fs.mkdirSync(codeGraphDir, { recursive: true });
}
@@ -99,26 +100,21 @@ async function main() {
relationshipsCreated: result.relationshipsCreated,
};
- fs.writeFileSync(
- path.join(codeGraphDir, "build.log.json"),
- JSON.stringify(metadata, null, 2),
- );
+ fs.writeFileSync(path.join(codeGraphDir, "build.log.json"), JSON.stringify(metadata, null, 2));
- console.log("\n✨ Build complete!");
- console.log(" View graph at: http://localhost:3000 (Memgraph Lab)");
- console.log(
- ' Query graph: npm run graph:query "MATCH (f:FILE) RETURN count(f)"',
- );
+ logger.error("\n✨ Build complete!");
+ logger.error(" View graph at: http://localhost:3000 (Memgraph Lab)");
+ logger.error(' Query graph: npm run graph:query "MATCH (f:FILE) RETURN count(f)"');
// Exit with appropriate code
process.exit(result.success ? 0 : 1);
} catch (error) {
- console.error("❌ Build failed:", error);
+ logger.error("❌ Build failed:", error);
process.exit(1);
}
}
main().catch((error) => {
- console.error("Fatal error:", error);
+ logger.error("Fatal error:", error);
process.exit(1);
});
diff --git a/src/cli/query.ts b/src/cli/query.ts
index 597e3ff..8e83fff 100644
--- a/src/cli/query.ts
+++ b/src/cli/query.ts
@@ -11,19 +11,20 @@
import MemgraphClient from "../graph/client.js";
import * as env from "../env.js";
+import { logger } from "../utils/logger.js";
async function main() {
const args = process.argv.slice(2);
const query = args.join(" ");
if (!query) {
- console.error("❌ No query provided");
- console.error('Usage: npm run graph:query "MATCH (n) RETURN n LIMIT 5"');
+ logger.error("❌ No query provided");
+ logger.error('Usage: npm run graph:query "MATCH (n) RETURN n LIMIT 5"');
process.exit(1);
}
try {
- console.log("🔍 Executing query...\n");
+ logger.error("🔍 Executing query...\n");
const memgraph = new MemgraphClient({
host: env.MEMGRAPH_HOST,
@@ -35,27 +36,27 @@ async function main() {
const result = await memgraph.executeCypher(query);
if (result.error) {
- console.error("❌ Query error:", result.error);
+ logger.error("❌ Query error:", result.error);
process.exit(1);
}
// Display results
if (result.data.length === 0) {
- console.log("📭 No results found");
+ logger.error("📭 No results found");
} else {
- console.log(`📊 Results (${result.data.length} rows):\n`);
+ logger.error(`📊 Results (${result.data.length} rows):\n`);
console.table(result.data);
}
await memgraph.disconnect();
process.exit(0);
} catch (error) {
- console.error("❌ Query failed:", error);
+ logger.error("❌ Query failed:", error);
process.exit(1);
}
}
main().catch((error) => {
- console.error("Fatal error:", error);
+ logger.error("Fatal error:", error);
process.exit(1);
});
diff --git a/src/cli/test-affected.ts b/src/cli/test-affected.ts
index 304c5bd..fd6b76d 100755
--- a/src/cli/test-affected.ts
+++ b/src/cli/test-affected.ts
@@ -12,27 +12,25 @@
import { execSync } from "child_process";
import GraphIndexManager from "../graph/index.js";
-import { loadConfig as _loadConfig } from "../config.js";
+import { loadConfig } from "../config.js";
import TestEngine from "../engines/test-engine.js";
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
- console.log("🧪 Test Affected Selector");
- console.log("");
- console.log("Usage: npm run test:affected [--run] [--depth=N]");
- console.log("");
- console.log("Options:");
- console.log(" --run Run tests after selection (requires Vitest)");
- console.log(" --depth=N Set transitive dependency depth (default: 1)");
- console.log("");
- console.log("Examples:");
- console.log(" npm run test:affected src/utils/units.ts");
- console.log(" npm run test:affected src/engine/calculations/\\*.ts --run");
- console.log(
- " npm run test:affected src/context/BuildingContext.tsx --depth=2 --run"
- );
+ console.error("🧪 Test Affected Selector");
+ console.error("");
+ console.error("Usage: npm run test:affected [--run] [--depth=N]");
+ console.error("");
+ console.error("Options:");
+ console.error(" --run Run tests after selection (requires Vitest)");
+ console.error(" --depth=N Set transitive dependency depth (default: 1)");
+ console.error("");
+ console.error("Examples:");
+ console.error(" npm run test:affected src/utils/units.ts");
+ console.error(" npm run test:affected src/engine/calculations/\\*.ts --run");
+ console.error(" npm run test:affected src/context/BuildingContext.tsx --depth=2 --run");
process.exit(0);
}
@@ -41,69 +39,95 @@ async function main() {
const depth = depthArg ? parseInt(depthArg.split("=")[1]) : 1;
// Filter out flag arguments
- const changedFiles = args.filter(
- (a) => !a.startsWith("--run") && !a.startsWith("--depth=")
- );
+ const changedFiles = args.filter((a) => !a.startsWith("--run") && !a.startsWith("--depth="));
- console.log("🧪 Test Affected Selector");
- console.log(`📁 Changed files: ${changedFiles.length}`);
- console.log(`🔄 Dependency depth: ${depth}`);
- console.log(`▶️ Auto-run: ${runTests ? "YES" : "NO"}\n`);
+ console.error("🧪 Test Affected Selector");
+ console.error(`📁 Changed files: ${changedFiles.length}`);
+ console.error(`🔄 Dependency depth: ${depth}`);
+ console.error(`▶️ Auto-run: ${runTests ? "YES" : "NO"}\n`);
try {
// Create in-memory graph index
- console.log("📊 Building test dependency map...");
+ console.error("📊 Building test dependency map...");
const index = new GraphIndexManager();
const testEngine = new TestEngine(index);
- console.log("✅ Ready\n");
+ console.error("✅ Ready\n");
// Select affected tests
- console.log("🔍 Analyzing dependencies...\n");
+ console.error("🔍 Analyzing dependencies...\n");
const result = testEngine.selectAffectedTests(changedFiles, true, depth);
// Display results
if (result.selectedTests.length === 0) {
- console.log("ℹ️ No tests directly affected by these changes");
- console.log(
- " (Possibly: new file, not imported by tests, or test dependencies not built)"
+ console.error("ℹ️ No tests directly affected by these changes");
+ console.error(
+ " (Possibly: new file, not imported by tests, or test dependencies not built)",
);
process.exit(0);
}
- console.log(`✅ Selected ${result.selectedTests.length} test(s):`);
- console.log("");
+ console.error(`✅ Selected ${result.selectedTests.length} test(s):`);
+ console.error("");
result.selectedTests.forEach((test) => {
- console.log(` 📄 ${test}`);
+ console.error(` 📄 ${test}`);
});
- console.log("");
- console.log("📊 Statistics:");
- console.log(` Coverage: ${result.coverage.percentage}% (${result.coverage.testsSelected}/${result.coverage.totalTests})`);
- console.log(` Category: ${result.category}`);
- console.log(
- ` Est. time: ${result.estimatedTime > 0 ? result.estimatedTime + "ms" : "unknown"}`
+ console.error("");
+ console.error("📊 Statistics:");
+ console.error(
+ ` Coverage: ${result.coverage.percentage}% (${result.coverage.testsSelected}/${result.coverage.totalTests})`,
+ );
+ console.error(` Category: ${result.category}`);
+ console.error(
+ ` Est. time: ${result.estimatedTime > 0 ? result.estimatedTime + "ms" : "unknown"}`,
);
- console.log("");
+ console.error("");
// Optionally run tests
if (runTests) {
- console.log("▶️ Running selected tests...\n");
+ console.error("\u25b6\ufe0f Running selected tests...\n");
try {
+ const config = await loadConfig();
+ const runner = config.testing?.testRunner;
const testList = result.selectedTests.join(" ");
- execSync(`npx vitest run ${testList}`, {
+
+ let runCmd: string;
+ if (runner) {
+ // Explicit runner from .lxdig/config.json
+ const runnerArgs = [...(runner.args ?? []), ...result.selectedTests].join(" ");
+ runCmd = `${runner.command} ${runnerArgs}`;
+ } else {
+ // Auto-detect from test file extensions
+ const hasPy = result.selectedTests.some((f) => f.endsWith(".py"));
+ const hasRb = result.selectedTests.some((f) => f.endsWith(".rb"));
+ const hasGo = result.selectedTests.some((f) => f.endsWith(".go"));
+ if (hasPy) {
+ runCmd = `pytest ${testList}`;
+ } else if (hasRb) {
+ runCmd = `bundle exec rspec ${testList}`;
+ } else if (hasGo) {
+ runCmd = `go test ${testList}`;
+ } else {
+ // Default: vitest (JS/TS)
+ runCmd = `npx vitest run ${testList}`;
+ }
+ }
+
+ console.error(`\u25b6\ufe0f ${runCmd}`);
+ execSync(runCmd, {
cwd: process.cwd(),
stdio: "inherit",
});
- console.log("\n✅ Tests completed successfully");
+ console.error("\n\u2705 Tests completed successfully");
process.exit(0);
- } catch (error) {
- console.error("\n❌ Some tests failed");
+ } catch (_error) {
+ console.error("\n\u274c Some tests failed");
process.exit(1);
}
} else {
- console.log("💡 To run these tests, add --run flag:");
- console.log(` npm run test:affected ${changedFiles.join(" ")} --run`);
- console.log("");
+ console.error("💡 To run these tests, add --run flag:");
+ console.error(` npm run test:affected ${changedFiles.join(" ")} --run`);
+ console.error("");
process.exit(0);
}
} catch (error) {
diff --git a/src/cli/validate.ts b/src/cli/validate.ts
index 46ad00d..baf1bf4 100644
--- a/src/cli/validate.ts
+++ b/src/cli/validate.ts
@@ -10,46 +10,46 @@
* npm run graph:validate -- --file src/engine/calculations/columns.ts
*/
-import { ArchitectureEngine } from '../engines/architecture-engine.js';
-import { loadConfig } from '../config.js';
-import GraphIndexManager from '../graph/index.js';
-import { MemgraphClient } from '../graph/client.js';
+import { ArchitectureEngine } from "../engines/architecture-engine.js";
+import { loadConfig } from "../config.js";
+import GraphIndexManager from "../graph/index.js";
+import { MemgraphClient } from "../graph/client.js";
+import { logger } from "../utils/logger.js";
async function main() {
const args = process.argv.slice(2);
- const isStrict = args.includes('--strict');
- const writeViolations = args.includes('--write');
- const fileIndex = args.indexOf('--file');
+ const isStrict = args.includes("--strict");
+ const writeViolations = args.includes("--write");
+ const fileIndex = args.indexOf("--file");
const targetFile = fileIndex >= 0 ? args[fileIndex + 1] : undefined;
- console.log('🏗️ Architecture Validator');
+ logger.error("🏗️ Architecture Validator");
if (targetFile) {
- console.log(`📄 Validating: ${targetFile}`);
+ logger.error(`📄 Validating: ${targetFile}`);
} else {
- console.log('📄 Validating all files');
+ logger.error("📄 Validating all files");
}
- console.log(`🔒 Strict mode: ${isStrict ? 'ON' : 'OFF'}\n`);
+ logger.error(`🔒 Strict mode: ${isStrict ? "ON" : "OFF"}\n`);
try {
// Load configuration
const config = await loadConfig();
// Create in-memory graph index (MVP - no Memgraph connection needed for validation)
- console.log('📊 Preparing validation engine...');
+ logger.error("📊 Preparing validation engine...");
const index = new GraphIndexManager();
- console.log('✅ Ready\n');
+ logger.error("✅ Ready\n");
// Run validation
- console.log('🔍 Checking architecture constraints...\n');
- const layers = config.architecture.layers.map(layer => ({
+ logger.error("🔍 Checking architecture constraints...\n");
+ const layers = config.architecture.layers.map((layer) => ({
...layer,
- description: layer.description || layer.name
+ description: layer.description || layer.name,
}));
- const engine = new ArchitectureEngine(
- layers,
- config.architecture.rules,
- index
- );
+ const engine = new ArchitectureEngine(layers, config.architecture.rules, index, undefined, {
+ sourceGlobs: config.testing?.sourceGlobs,
+ defaultExtension: config.testing?.defaultExtension,
+ });
const filesToValidate = targetFile ? [targetFile] : undefined;
const result = await engine.validate(filesToValidate);
const violations = result.violations || [];
@@ -62,43 +62,45 @@ async function main() {
await engine.writeViolationsToMemgraph(client, violations);
await client.disconnect();
} catch (error) {
- console.warn('⚠️ Could not write violations to Memgraph:', error instanceof Error ? error.message : String(error));
+ logger.warn(
+ "⚠️ Could not write violations to Memgraph:",
+ error instanceof Error ? error.message : String(error),
+ );
}
}
// Display results
if (violations.length === 0) {
- console.log('✅ No violations found!');
+ logger.error("✅ No violations found!");
} else {
- console.log(`⚠️ Found ${violations.length} violation(s):\n`);
+ logger.error(`⚠️ Found ${violations.length} violation(s):\n`);
violations.forEach((violation, index) => {
- const icon =
- violation.severity === 'error' ? '❌' : '⚠️';
- console.log(`${icon} ${index + 1}. ${violation.message}`);
- console.log(` File: ${violation.file}`);
- console.log(` Layer: ${violation.layer}`);
- console.log('');
+ const icon = violation.severity === "error" ? "❌" : "⚠️";
+ logger.error(`${icon} ${index + 1}. ${violation.message}`);
+ logger.error(` File: ${violation.file}`);
+ logger.error(` Layer: ${violation.layer}`);
+ logger.error("");
});
- const errorCount = violations.filter((v) => v.severity === 'error').length;
- const warningCount = violations.filter((v) => v.severity === 'warn').length;
+ const errorCount = violations.filter((v) => v.severity === "error").length;
+ const warningCount = violations.filter((v) => v.severity === "warn").length;
- console.log(`Summary: ${errorCount} error(s), ${warningCount} warning(s)`);
+ logger.error(`Summary: ${errorCount} error(s), ${warningCount} warning(s)`);
if (isStrict && errorCount > 0) {
- console.log('\n🛑 Strict mode: exiting with error code 1');
+ logger.error("\n🛑 Strict mode: exiting with error code 1");
process.exit(1);
}
}
process.exit(0);
} catch (error) {
- console.error('❌ Validation failed:', error);
+ logger.error("❌ Validation failed:", error);
process.exit(1);
}
}
main().catch((error) => {
- console.error('Fatal error:', error);
+ logger.error("Fatal error:", error);
process.exit(1);
});
diff --git a/src/config.ts b/src/config.ts
index 8850147..85d9f71 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -4,6 +4,7 @@
import * as fs from "fs";
import * as path from "path";
+import { logger } from "./utils/logger.js";
export interface ArchitectureConfig {
layers: LayerConfig[];
@@ -43,10 +44,36 @@ export interface Config {
id: string;
patterns: string[];
}>;
+ /**
+ * Explicit test runner to invoke via `test_run` and `test:affected`.
+ * When omitted the runner is auto-detected from the test file extensions
+ * (e.g. .py → pytest, .rb → bundle exec rspec, .ts/.js → vitest).
+ * @example { "command": "pytest", "args": ["--tb=short"] }
+ */
+ testRunner?: {
+ command: string;
+ args?: string[];
+ };
+ /**
+ * Glob patterns used by the architecture engine to discover source files.
+ * Defaults to ["src/**\/*.{ts,tsx}"] when not specified.
+ * @example ["src/**\/*.py", "lib/**\/*.py"]
+ */
+ sourceGlobs?: string[];
+ /**
+ * Default file extension appended when generating new file paths (e.g. via
+ * arch_suggest). Defaults to ".ts". Use ".py", ".rb", ".go", etc. for
+ * non-TypeScript projects.
+ */
+ defaultExtension?: string;
};
progress?: ProgressConfig;
}
+// Generic TypeScript server defaults — create .lxdig/config.json at your project root
+// to override with project-specific layers and rules.
+// Tip: run arch_suggest to get placement guidance; update this file if suggestions
+// look wrong (e.g. always "src/types/").
const DEFAULT_CONFIG: Config = {
architecture: {
layers: [
@@ -55,56 +82,70 @@ const DEFAULT_CONFIG: Config = {
name: "Types",
paths: ["src/types/**"],
canImport: [],
- description: "Core type definitions",
+ description: "Shared type definitions — no runtime dependencies",
},
{
id: "utils",
name: "Utilities",
- paths: ["src/utils/**", "src/lib/**"],
+ paths: ["src/utils/**", "src/lib/**", "src/helpers/**"],
canImport: ["types"],
- description: "Utility functions and libraries",
+ description: "Stateless utility and helper functions",
},
{
- id: "engine",
- name: "Engine",
- paths: ["src/engine/**"],
+ id: "parsers",
+ name: "Parsers",
+ paths: ["src/parsers/**"],
canImport: ["types", "utils"],
- description: "Business logic and calculations",
+ description: "File parsers and language-specific analysis",
},
{
- id: "context",
- name: "Context",
- paths: ["src/context/**"],
- canImport: ["types", "utils", "engine", "hooks"],
- description: "React context providers",
+ id: "graph",
+ name: "Graph",
+ paths: ["src/graph/**"],
+ canImport: ["types", "utils", "parsers"],
+ description: "Graph building, caching and Memgraph client",
},
{
- id: "hooks",
- name: "Hooks",
- paths: ["src/hooks/**"],
- canImport: ["types", "utils", "engine"],
- description: "Custom React hooks",
+ id: "vector",
+ name: "Vector",
+ paths: ["src/vector/**"],
+ canImport: ["types", "utils", "graph"],
+ description: "Embedding engine and Qdrant client",
},
{
- id: "components",
- name: "Components",
- paths: ["src/components/**"],
- canImport: ["types", "utils", "engine", "context", "hooks"],
- description: "React UI components",
+ id: "engines",
+ name: "Engines",
+ paths: ["src/engines/**"],
+ canImport: ["types", "utils", "parsers", "graph", "vector"],
+ description: "Feature engines — architecture, community, docs, test, progress",
+ },
+ {
+ id: "tools",
+ name: "Tools",
+ paths: ["src/tools/**"],
+ canImport: ["types", "utils", "parsers", "graph", "vector", "engines"],
+ description: "MCP tool handlers — highest-level layer, may use all lower layers",
+ },
+ {
+ id: "server",
+ name: "Server",
+ paths: ["src/*.ts"],
+ canImport: ["types", "utils", "graph", "vector", "engines", "tools"],
+ description: "Server entry points — wires all layers together",
},
],
rules: [
{
- id: "no-engine-in-context",
+ id: "no-tools-in-engines",
severity: "error",
- pattern: "engine imports from context",
- description: "Context providers should not directly import engine code",
+ pattern: "engines imports from tools",
+ description: "Engines must not import from tool handlers",
},
{
- id: "no-components-in-engine",
- severity: "error",
- pattern: "component imports from engine",
- description: "Engine code must remain UI-independent",
+ id: "no-graph-in-parsers",
+ severity: "warn",
+ pattern: "parsers imports from graph",
+ description: "Parsers should be graph-agnostic for reuse and testability",
},
],
},
@@ -167,7 +208,7 @@ const DEFAULT_CONFIG: Config = {
};
export async function loadConfig(): Promise {
- const configPath = path.join(process.cwd(), ".lxrag", "config.json");
+ const configPath = path.join(process.cwd(), ".lxdig", "config.json");
try {
if (fs.existsSync(configPath)) {
@@ -175,15 +216,14 @@ export async function loadConfig(): Promise {
return JSON.parse(data);
}
} catch (error) {
- console.warn("[Config] Error loading config file:", error);
+ logger.warn("[Config] Error loading config file:", error);
}
return DEFAULT_CONFIG;
}
export function saveConfig(config: Config, configPath?: string): void {
- const targetPath =
- configPath || path.join(process.cwd(), ".lxrag", "config.json");
+ const targetPath = configPath || path.join(process.cwd(), ".lxdig", "config.json");
const dir = path.dirname(targetPath);
if (!fs.existsSync(dir)) {
diff --git a/src/engines/architecture-engine.test.ts b/src/engines/__tests__/architecture-engine.test.ts
similarity index 60%
rename from src/engines/architecture-engine.test.ts
rename to src/engines/__tests__/architecture-engine.test.ts
index 8a6d048..e41fbcf 100644
--- a/src/engines/architecture-engine.test.ts
+++ b/src/engines/__tests__/architecture-engine.test.ts
@@ -2,12 +2,12 @@ import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { afterEach, describe, expect, it } from "vitest";
-import GraphIndexManager from "../graph/index.js";
+import GraphIndexManager from "../../graph/index.js";
import {
ArchitectureEngine,
type ArchitectureRule,
type LayerDefinition,
-} from "./architecture-engine.js";
+} from "../architecture-engine.js";
const layers: LayerDefinition[] = [
{
@@ -60,30 +60,16 @@ describe("ArchitectureEngine", () => {
path.join(srcDir, "feature", "feature-a.ts"),
"import { view } from '../ui/view';\nexport const x = view;\n",
);
- fs.writeFileSync(
- path.join(srcDir, "ui", "view.ts"),
- "export const view = 1;\n",
- );
+ fs.writeFileSync(path.join(srcDir, "ui", "view.ts"), "export const view = 1;\n");
process.chdir(root);
- const engine = new ArchitectureEngine(
- layers,
- rules,
- new GraphIndexManager(),
- );
- const result = await engine.validate([
- "src/feature/feature-a.ts",
- "src/ui/view.ts",
- ]);
+ const engine = new ArchitectureEngine(layers, rules, new GraphIndexManager());
+ const result = await engine.validate(["src/feature/feature-a.ts", "src/ui/view.ts"]);
expect(result.success).toBe(false);
- expect(result.violations.some((v) => v.type === "layer-violation")).toBe(
- true,
- );
- expect(
- result.violations.some((v) => v.message.includes("explicitly forbidden")),
- ).toBe(true);
+ expect(result.violations.some((v) => v.type === "layer-violation")).toBe(true);
+ expect(result.violations.some((v) => v.message.includes("explicitly forbidden"))).toBe(true);
});
it("detects circular dependencies during validation", async () => {
@@ -102,22 +88,14 @@ describe("ArchitectureEngine", () => {
process.chdir(root);
- const engine = new ArchitectureEngine(
- layers,
- rules,
- new GraphIndexManager(),
- );
+ const engine = new ArchitectureEngine(layers, rules, new GraphIndexManager());
const result = await engine.validate();
expect(result.violations.some((v) => v.type === "circular")).toBe(true);
});
it("returns placement suggestion when dependencies are allowed", () => {
- const engine = new ArchitectureEngine(
- layers,
- rules,
- new GraphIndexManager(),
- );
+ const engine = new ArchitectureEngine(layers, rules, new GraphIndexManager());
const suggestion = engine.getSuggestion("Data", "service", ["core"]);
expect(suggestion).not.toBeNull();
@@ -174,11 +152,7 @@ describe("ArchitectureEngine", () => {
];
it("T18: arch_suggest(type=service) does not return src/types/ layer", () => {
- const engine = new ArchitectureEngine(
- realisticLayers,
- rules,
- new GraphIndexManager(),
- );
+ const engine = new ArchitectureEngine(realisticLayers, rules, new GraphIndexManager());
// External package deps are not layer IDs and must not restrict layer selection
const suggestion = engine.getSuggestion("GraphDataService", "service", [
"react",
@@ -196,11 +170,7 @@ describe("ArchitectureEngine", () => {
});
it("T19: arch_suggest does not duplicate Service suffix in filename", () => {
- const engine = new ArchitectureEngine(
- realisticLayers,
- rules,
- new GraphIndexManager(),
- );
+ const engine = new ArchitectureEngine(realisticLayers, rules, new GraphIndexManager());
const suggestion = engine.getSuggestion("GraphDataService", "service", []);
expect(suggestion).not.toBeNull();
@@ -214,33 +184,80 @@ describe("ArchitectureEngine", () => {
});
it("T18b: arch_suggest reasoning string is non-empty", () => {
- const engine = new ArchitectureEngine(
- realisticLayers,
- rules,
- new GraphIndexManager(),
- );
+ const engine = new ArchitectureEngine(realisticLayers, rules, new GraphIndexManager());
const suggestion = engine.getSuggestion("MyService", "service", []);
expect(suggestion).not.toBeNull();
expect(suggestion!.reasoning.trim().length).toBeGreaterThan(0);
});
- it("external package names in deps do not constrain layer selection", () => {
+ // ── N7 regression — arch_validate uses workspaceRoot, not process.cwd() ───
+
+ it("N7: validate() scans workspaceRoot, not process.cwd()", async () => {
+ // Create two separate temp directories
+ const targetRoot = fs.mkdtempSync(path.join(os.tmpdir(), "arch-n7-target-"));
+ const decoyRoot = fs.mkdtempSync(path.join(os.tmpdir(), "arch-n7-decoy-"));
+
+ // Put a .ts file with a layer violation in targetRoot
+ const srcDir = path.join(targetRoot, "src");
+ fs.mkdirSync(path.join(srcDir, "feature"), { recursive: true });
+ fs.mkdirSync(path.join(srcDir, "ui"), { recursive: true });
+ fs.writeFileSync(
+ path.join(srcDir, "feature", "bad.ts"),
+ "import { view } from '../ui/view';\nexport const x = view;\n",
+ );
+ fs.writeFileSync(path.join(srcDir, "ui", "view.ts"), "export const view = 1;\n");
+
+ // decoyRoot has no src/ files — if validate() scans it, result would be clean
+ // Set process.cwd() to decoyRoot to verify engine ignores it
+ process.chdir(decoyRoot);
+
const engine = new ArchitectureEngine(
- realisticLayers,
+ layers,
rules,
new GraphIndexManager(),
+ targetRoot, // explicit workspaceRoot — must be used instead of process.cwd()
);
+
+ const result = await engine.validate(); // no files arg → must scan targetRoot
+ // Must find the layer violation in targetRoot (not the clean decoyRoot)
+ expect(result.violations.some((v) => v.type === "layer-violation")).toBe(true);
+ });
+
+ it("N7: reload() updates workspaceRoot for subsequent validate() calls", async () => {
+ const root1 = fs.mkdtempSync(path.join(os.tmpdir(), "arch-n7-r1-"));
+ const root2 = fs.mkdtempSync(path.join(os.tmpdir(), "arch-n7-r2-"));
+
+ // root2 has a layer violation; root1 is clean
+ const src2 = path.join(root2, "src");
+ fs.mkdirSync(path.join(src2, "feature"), { recursive: true });
+ fs.mkdirSync(path.join(src2, "ui"), { recursive: true });
+ fs.writeFileSync(
+ path.join(src2, "feature", "bad.ts"),
+ "import { view } from '../ui/view';\nexport const x = view;\n",
+ );
+ fs.writeFileSync(path.join(src2, "ui", "view.ts"), "export const view = 1;\n");
+
+ const index = new GraphIndexManager();
+ const engine = new ArchitectureEngine(layers, rules, index, root1);
+
+ // First validate: root1 is clean
+ const result1 = await engine.validate();
+ expect(result1.violations.filter((v) => v.type === "layer-violation")).toHaveLength(0);
+
+ // After reload with root2, validate must scan root2 and find the violation
+ engine.reload(index, "proj2", root2);
+ const result2 = await engine.validate();
+ expect(result2.violations.some((v) => v.type === "layer-violation")).toBe(true);
+ });
+
+ it("external package names in deps do not constrain layer selection", () => {
+ const engine = new ArchitectureEngine(realisticLayers, rules, new GraphIndexManager());
// With no deps, all layers are eligible; with external deps, same result
const noDepSuggestion = engine.getSuggestion("MyHook", "hook", []);
- const withExternal = engine.getSuggestion("MyHook", "hook", [
- "react",
- "react-dom",
- ]);
+ const withExternal = engine.getSuggestion("MyHook", "hook", ["react", "react-dom"]);
// External package deps must not block any layer from being selected
- expect(noDepSuggestion?.suggestedLayer.id).toBe(
- withExternal?.suggestedLayer.id,
- );
+ expect(noDepSuggestion?.suggestedLayer.id).toBe(withExternal?.suggestedLayer.id);
});
});
diff --git a/src/engines/__tests__/community-detector.test.ts b/src/engines/__tests__/community-detector.test.ts
new file mode 100644
index 0000000..b5236ef
--- /dev/null
+++ b/src/engines/__tests__/community-detector.test.ts
@@ -0,0 +1,348 @@
+// ── Community Detector — Tests ────────────────────────────────────────────────
+//
+// Tests for:
+// - CommunityDetector.run() → MAGE Leiden → directory fallback
+// - tryMageCommunityDetection() → tested indirectly via run()
+// - runDirectoryHeuristic() → tested indirectly via run()
+// - communityLabel() → tested indirectly (via directory grouping)
+// - labelForGroup() → tested via MAGE path
+// - centralNode() → implicit via writeCommunities
+// - writeCommunities() → verified via executeCypher call count
+//
+// Conventions match ./coordination-engine.test.ts: vi.fn() stubs for executeCypher.
+
+import { describe, expect, it, vi, beforeEach } from "vitest";
+import CommunityDetector from "../community-detector.js";
+
+// ── Helpers ─────────────────────────────────────────────────────────────────
+
+/** Standard member node row returned by the initial MATCH query. */
+function makeMemberRow(id: string, filePath: string, type = "FILE", name?: string) {
+ return {
+ id,
+ filePath,
+ type,
+ name: name ?? id,
+ };
+}
+
+/**
+ * Builds a MemgraphClient mock with configurable per-query responses.
+ *
+ * @param overrides Map of query-fragment → row array. First match wins.
+ */
+function makeMockMemgraph(overrides: Record = {}) {
+ return {
+ executeCypher: vi.fn(async (query: string) => {
+ for (const [key, val] of Object.entries(overrides)) {
+ if (query.includes(key)) {
+ if (val && typeof val === "object" && "error" in val) {
+ return { error: "mocked-error", data: [] };
+ }
+ return { data: val as unknown[] };
+ }
+ }
+ return { data: [] };
+ }),
+ } as any;
+}
+
+// ── run() — empty graph ───────────────────────────────────────────────────────
+
+describe("CommunityDetector.run() — empty graph", () => {
+ it("returns {communities:0, members:0, mode:'directory_heuristic'} when no nodes", async () => {
+ const memgraph = makeMockMemgraph();
+ const detector = new CommunityDetector(memgraph as any);
+
+ const result = await detector.run("proj-a");
+
+ expect(result).toEqual({ communities: 0, members: 0, mode: "directory_heuristic" });
+ // Should NOT attempt community detection when members is empty
+ expect(memgraph.executeCypher).toHaveBeenCalledTimes(1);
+ });
+});
+
+// ── run() — MAGE available ───────────────────────────────────────────────────
+
+describe("CommunityDetector.run() — MAGE Leiden available", () => {
+ it("uses mage_leiden mode when MAGE query returns community data", async () => {
+ const members = [
+ makeMemberRow("file:a", "src/engines/episode-engine.ts"),
+ makeMemberRow("file:b", "src/engines/coordination-engine.ts"),
+ ];
+ const mageRows = [
+ { nodeId: "file:a", cid: 0 },
+ { nodeId: "file:b", cid: 0 },
+ ];
+
+ const memgraph = makeMockMemgraph({
+ "MATCH (n)": members,
+ "community_detection.get()": mageRows,
+ });
+ const detector = new CommunityDetector(memgraph as any);
+
+ const result = await detector.run("proj-a");
+
+ expect(result.mode).toBe("mage_leiden");
+ expect(result.communities).toBe(1); // both files in same community
+ expect(result.members).toBeGreaterThan(0);
+ });
+
+ it("groups members into separate communities by MAGE cid", async () => {
+ const members = [
+ makeMemberRow("file:a", "src/engines/episode-engine.ts"),
+ makeMemberRow("file:b", "src/graph/client.ts"),
+ makeMemberRow("file:c", "src/tools/registry.ts"),
+ ];
+ const mageRows = [
+ { nodeId: "file:a", cid: 0 },
+ { nodeId: "file:b", cid: 1 },
+ { nodeId: "file:c", cid: 2 },
+ ];
+
+ const memgraph = makeMockMemgraph({
+ "MATCH (n)": members,
+ "community_detection.get()": mageRows,
+ });
+ const detector = new CommunityDetector(memgraph as any);
+
+ const result = await detector.run("proj-a");
+
+ expect(result.mode).toBe("mage_leiden");
+ expect(result.communities).toBe(3);
+ });
+
+ it("writes COMMUNITY nodes via MERGE and BELONGS_TO edges", async () => {
+ const members = [makeMemberRow("file:a", "src/engines/episode-engine.ts")];
+ const mageRows = [{ nodeId: "file:a", cid: 0 }];
+
+ const memgraph = makeMockMemgraph({
+ "MATCH (n)": members,
+ "community_detection.get()": mageRows,
+ });
+ const detector = new CommunityDetector(memgraph as any);
+
+ await detector.run("proj-a");
+
+ const mergeCalls = memgraph.executeCypher.mock.calls.filter(([q]: [string]) =>
+ q.includes("MERGE (c:COMMUNITY"),
+ );
+ const belongsCalls = memgraph.executeCypher.mock.calls.filter(([q]: [string]) =>
+ q.includes("BELONGS_TO"),
+ );
+ expect(mergeCalls).toHaveLength(1);
+ expect(belongsCalls).toHaveLength(1);
+ });
+
+ it("skips members not in MAGE community map", async () => {
+ const members = [
+ makeMemberRow("file:a", "src/engines/episode-engine.ts"),
+ makeMemberRow("file:orphan", "src/unknown.ts"),
+ ];
+ const mageRows = [{ nodeId: "file:a", cid: 0 }];
+
+ const memgraph = makeMockMemgraph({
+ "MATCH (n)": members,
+ "community_detection.get()": mageRows,
+ });
+ const detector = new CommunityDetector(memgraph as any);
+
+ const result = await detector.run("proj-a");
+
+ // Only 1 member was in the MAGE map
+ expect(result.members).toBe(1);
+ });
+});
+
+// ── run() — MAGE unavailable → directory fallback ────────────────────────────
+
+describe("CommunityDetector.run() — MAGE fallback", () => {
+ it("falls back to directory_heuristic when MAGE returns empty data", async () => {
+ const members = [
+ makeMemberRow("file:a", "src/engines/episode-engine.ts"),
+ makeMemberRow("file:b", "src/graph/client.ts"),
+ ];
+
+ const memgraph = makeMockMemgraph({
+ "MATCH (n)": members,
+ // community_detection.get() returns empty → falls back
+ });
+ const detector = new CommunityDetector(memgraph as any);
+
+ const result = await detector.run("proj-a");
+
+ expect(result.mode).toBe("directory_heuristic");
+ });
+
+ it("falls back when MAGE query returns error", async () => {
+ const members = [makeMemberRow("file:a", "src/engines/episode-engine.ts")];
+
+ const memgraph = makeMockMemgraph({
+ "MATCH (n)": members,
+ "community_detection.get()": { error: true },
+ });
+ const detector = new CommunityDetector(memgraph as any);
+
+ const result = await detector.run("proj-a");
+
+ expect(result.mode).toBe("directory_heuristic");
+ expect(result.members).toBe(1);
+ });
+
+ it("groups files from the same src/ directory into one community", async () => {
+ const members = [
+ makeMemberRow("file:ep", "src/engines/episode-engine.ts"),
+ makeMemberRow("file:coord", "src/engines/coordination-engine.ts"),
+ makeMemberRow("file:client", "src/graph/client.ts"),
+ ];
+
+ const memgraph = makeMockMemgraph({ "MATCH (n)": members });
+ const detector = new CommunityDetector(memgraph as any);
+
+ const result = await detector.run("proj-a");
+
+ expect(result.mode).toBe("directory_heuristic");
+ // 2 directories: engines + graph
+ expect(result.communities).toBe(2);
+ });
+
+ it("writes COMMUNITY nodes for each directory group", async () => {
+ const members = [
+ makeMemberRow("file:ep", "src/engines/episode-engine.ts"),
+ makeMemberRow("file:client", "src/graph/client.ts"),
+ ];
+
+ const memgraph = makeMockMemgraph({ "MATCH (n)": members });
+ const detector = new CommunityDetector(memgraph as any);
+
+ await detector.run("proj-a");
+
+ const mergeCalls = memgraph.executeCypher.mock.calls.filter(([q]: [string]) =>
+ q.includes("MERGE (c:COMMUNITY"),
+ );
+ // 2 communities → 2 MERGE calls
+ expect(mergeCalls).toHaveLength(2);
+ });
+});
+
+// ── communityLabel() (tested via directory heuristic) ────────────────────────
+
+describe("communityLabel via directory heuristic grouping", () => {
+ it("groups absolute paths by the directory after src/", async () => {
+ const members = [
+ makeMemberRow("n1", "/home/alex/myproject/src/engines/foo.ts"),
+ makeMemberRow("n2", "/home/alex/myproject/src/engines/bar.ts"),
+ makeMemberRow("n3", "/home/alex/myproject/src/graph/client.ts"),
+ ];
+
+ const memgraph = makeMockMemgraph({ "MATCH (n)": members });
+ const detector = new CommunityDetector(memgraph as any);
+
+ const result = await detector.run("proj-a");
+
+ // Should group into 2 communities: engines, graph
+ expect(result.communities).toBe(2);
+ });
+
+ it("uses root marker itself when next segment is a filename", async () => {
+ // When the path is src/a.ts (file directly in src/), we use "src" label
+ const members = [makeMemberRow("n1", "src/index.ts"), makeMemberRow("n2", "src/server.ts")];
+ const memgraph = makeMockMemgraph({ "MATCH (n)": members });
+ const detector = new CommunityDetector(memgraph as any);
+
+ const result = await detector.run("proj-a");
+
+ // Both files are in src/ directly → single "src" community
+ expect(result.communities).toBe(1);
+ });
+
+ it("falls back to 'misc' when path has no recognizable structure", async () => {
+ const members = [makeMemberRow("n1", ""), makeMemberRow("n2", "")];
+ const memgraph = makeMockMemgraph({ "MATCH (n)": members });
+ const detector = new CommunityDetector(memgraph as any);
+
+ const result = await detector.run("proj-a");
+
+ // All map to "misc" → single community
+ expect(result.communities).toBe(1);
+ });
+});
+
+// ── labelForGroup() (tested via MAGE path) ───────────────────────────────────
+
+describe("labelForGroup via MAGE path", () => {
+ it("picks the most frequent path prefix as the community label", async () => {
+ const members = [
+ makeMemberRow("f:a", "src/engines/a.ts"),
+ makeMemberRow("f:b", "src/engines/b.ts"),
+ makeMemberRow("f:c", "src/graph/c.ts"),
+ ];
+ const mageRows = [
+ { nodeId: "f:a", cid: 0 },
+ { nodeId: "f:b", cid: 0 },
+ { nodeId: "f:c", cid: 0 },
+ ];
+ const memgraph = makeMockMemgraph({
+ "MATCH (n)": members,
+ "community_detection.get()": mageRows,
+ });
+ const detector = new CommunityDetector(memgraph as any);
+
+ await detector.run("proj-a");
+
+ // Community MERGE params should have label="engines" (most common)
+ const mergeCalls = memgraph.executeCypher.mock.calls.filter(([q]: [string]) =>
+ q.includes("MERGE (c:COMMUNITY"),
+ );
+ const labels = mergeCalls.map(([, params]: [string, Record]) => params.label);
+ expect(labels).toContain("engines");
+ });
+});
+
+// ── writeCommunities community ID format ─────────────────────────────────────
+
+describe("community ID naming convention", () => {
+ it("sets community id in format '::community::::'", async () => {
+ const members = [makeMemberRow("file:a", "src/engines/ep.ts")];
+
+ const memgraph = makeMockMemgraph({ "MATCH (n)": members });
+ const detector = new CommunityDetector(memgraph as any);
+
+ await detector.run("proj-a");
+
+ const mergeCalls = memgraph.executeCypher.mock.calls.filter(([q]: [string]) =>
+ q.includes("MERGE (c:COMMUNITY"),
+ );
+ const { id } = mergeCalls[0][1] as Record;
+ expect(id).toMatch(/^proj-a::community::(dir|leiden)::\d+$/);
+ });
+});
+
+// ── centralNode preference ────────────────────────────────────────────────────
+
+describe("centralNode prefers FUNCTION type", () => {
+ it("selects FUNCTION node as centralNode when available", async () => {
+ const members = [
+ makeMemberRow("file:a", "src/engines/ep.ts", "FILE"),
+ makeMemberRow("fn:x", "src/engines/ep.ts", "FUNCTION"),
+ ];
+ const mageRows = [
+ { nodeId: "file:a", cid: 0 },
+ { nodeId: "fn:x", cid: 0 },
+ ];
+
+ const memgraph = makeMockMemgraph({
+ "MATCH (n)": members,
+ "community_detection.get()": mageRows,
+ });
+ const detector = new CommunityDetector(memgraph as any);
+
+ await detector.run("proj-a");
+
+ const mergeCalls = memgraph.executeCypher.mock.calls.filter(([q]: [string]) =>
+ q.includes("MERGE (c:COMMUNITY"),
+ );
+ const { centralNode } = mergeCalls[0][1] as Record;
+ expect(centralNode).toBe("fn:x");
+ });
+});
diff --git a/src/engines/__tests__/coordination-engine.test.ts b/src/engines/__tests__/coordination-engine.test.ts
new file mode 100644
index 0000000..815f333
--- /dev/null
+++ b/src/engines/__tests__/coordination-engine.test.ts
@@ -0,0 +1,487 @@
+// ── Coordination Engine — Tests ───────────────────────────────────────────────
+// Tests for:
+// - coordination-utils (pure functions, no mock needed)
+// - CoordinationEngine public API (MemgraphClient mocked via vi.fn())
+//
+// Conventions match ./progress-engine.test.ts: vi.fn() stubs for executeCypher,
+// cast as any for the mock. See also ./architecture-engine.test.ts for patterns.
+
+import { describe, expect, it, vi } from "vitest";
+import CoordinationEngine from "../coordination-engine.js";
+import { makeClaimId, rowToClaim } from "../coordination-utils.js";
+
+// ── Helpers ───────────────────────────────────────────────────────────────────
+
+/** Creates a minimal MemgraphClient mock where executeCypher returns no rows. */
+function makeMockMemgraph(overrides: Record = {}) {
+ return {
+ executeCypher: vi.fn(async (query: string, params: Record) => {
+ // Return override data if the first matching key appears in the query.
+ for (const [key, data] of Object.entries(overrides)) {
+ if (query.includes(key)) return { data };
+ }
+ return { data: [] };
+ }),
+ } as any;
+}
+
+// ── coordination-utils ────────────────────────────────────────────────────────
+
+describe("rowToClaim", () => {
+ it("maps a flat row with all fields", () => {
+ const row = {
+ id: "claim-1",
+ agentId: "agent-a",
+ sessionId: "sess-1",
+ taskId: "task-x",
+ claimType: "file",
+ targetId: "file:src/foo.ts",
+ intent: "editing foo.ts",
+ validFrom: 1000,
+ targetVersionSHA: "abc123",
+ validTo: null,
+ invalidationReason: null,
+ outcome: null,
+ projectId: "my-project",
+ };
+
+ const claim = rowToClaim(row);
+
+ expect(claim).not.toBeNull();
+ expect(claim?.id).toBe("claim-1");
+ expect(claim?.agentId).toBe("agent-a");
+ expect(claim?.claimType).toBe("file");
+ expect(claim?.validTo).toBeNull();
+ expect(claim?.taskId).toBe("task-x");
+ });
+
+ it("handles Memgraph nested row (row.c)", () => {
+ const row = {
+ c: {
+ id: "claim-2",
+ agentId: "agent-b",
+ sessionId: "sess-2",
+ claimType: "task",
+ targetId: "task-y",
+ intent: "doing task y",
+ validFrom: 2000,
+ validTo: 3000,
+ projectId: "proj",
+ },
+ };
+
+ const claim = rowToClaim(row);
+ expect(claim?.id).toBe("claim-2");
+ expect(claim?.validTo).toBe(3000);
+ expect(claim?.agentId).toBe("agent-b");
+ });
+
+ it("returns null when row has no id", () => {
+ expect(rowToClaim({ agentId: "x" })).toBeNull();
+ expect(rowToClaim({})).toBeNull();
+ });
+
+ it("fills in defaults for missing optional fields", () => {
+ const claim = rowToClaim({ id: "claim-3", projectId: "p" });
+ expect(claim?.agentId).toBe("unknown");
+ expect(claim?.claimType).toBe("task");
+ expect(claim?.intent).toBe("");
+ expect(claim?.invalidationReason).toBeUndefined();
+ expect(claim?.outcome).toBeUndefined();
+ expect(claim?.taskId).toBeUndefined();
+ });
+});
+
+describe("makeClaimId", () => {
+ it("produces id with expected prefix", () => {
+ const id = makeClaimId("claim");
+ expect(id).toMatch(/^claim-\d+-[a-z0-9]+$/);
+ });
+
+ it("accepts injectable timestamp for deterministic output", () => {
+ const id = makeClaimId("claim", 1234567890);
+ expect(id.startsWith("claim-1234567890-")).toBe(true);
+ });
+
+ it("produces different ids on successive calls", () => {
+ const a = makeClaimId("claim");
+ const b = makeClaimId("claim");
+ // Random suffix makes collision vanishingly unlikely.
+ expect(a).not.toBe(b);
+ });
+});
+
+// ── CoordinationEngine ────────────────────────────────────────────────────────
+
+describe("CoordinationEngine.claim", () => {
+ it("returns ok status when no conflict exists", async () => {
+ const memgraph = makeMockMemgraph({
+ // CONFLICT_CHECK returns empty → no conflict
+ });
+ const engine = new CoordinationEngine(memgraph);
+
+ const result = await engine.claim({
+ agentId: "agent-a",
+ sessionId: "sess-1",
+ projectId: "proj",
+ targetId: "task-1",
+ claimType: "task",
+ intent: "working on task-1",
+ });
+
+ expect(result.status).toBe("ok");
+ expect(result.claimId).toMatch(/^claim-/);
+ expect(result.targetVersionSHA).toMatch(/^unknown-/);
+ });
+
+ it("returns CONFLICT when another agent holds the claim", async () => {
+ const memgraph = makeMockMemgraph({
+ // CONFLICT_CHECK returns one conflicting row
+ CONFLICT_CHECK: [
+ {
+ claimId: "claim-other",
+ agentId: "agent-b",
+ intent: "other work",
+ since: 1000,
+ },
+ ],
+ });
+ // Override: return conflict row for the first query (conflict check)
+ memgraph.executeCypher = vi.fn().mockResolvedValueOnce({
+ data: [{ agentId: "agent-b", intent: "other work", since: 1000 }],
+ });
+
+ const engine = new CoordinationEngine(memgraph);
+
+ const result = await engine.claim({
+ agentId: "agent-a",
+ sessionId: "sess-1",
+ projectId: "proj",
+ targetId: "task-1",
+ claimType: "task",
+ intent: "competing work",
+ });
+
+ expect(result.status).toBe("CONFLICT");
+ expect(result.conflict?.agentId).toBe("agent-b");
+ expect(result.claimId).toBe("");
+ });
+});
+
+describe("CoordinationEngine.release", () => {
+ it("returns found:true alreadyClosed:false when claim is open", async () => {
+ const memgraph = {
+ executeCypher: vi
+ .fn()
+ // First call: RELEASE_CLAIM_OPEN_CHECK — claim found, validTo = null
+ .mockResolvedValueOnce({ data: [{ id: "claim-1", validTo: null }] })
+ // Second call: RELEASE_CLAIM — actual update
+ .mockResolvedValueOnce({ data: [] }),
+ } as any;
+
+ const engine = new CoordinationEngine(memgraph);
+ const feedback = await engine.release("claim-1", "done");
+
+ expect(feedback.found).toBe(true);
+ expect(feedback.alreadyClosed).toBe(false);
+ });
+
+ it("returns alreadyClosed:true when claim was already closed", async () => {
+ const memgraph = {
+ executeCypher: vi.fn().mockResolvedValueOnce({
+ data: [{ id: "claim-1", validTo: 9999999 }],
+ }),
+ } as any;
+
+ const engine = new CoordinationEngine(memgraph);
+ const feedback = await engine.release("claim-1");
+
+ expect(feedback.found).toBe(true);
+ expect(feedback.alreadyClosed).toBe(true);
+ // No second Cypher call should be made (no update needed)
+ expect(memgraph.executeCypher).toHaveBeenCalledTimes(1);
+ });
+
+ it("returns found:false when claim does not exist", async () => {
+ const memgraph = {
+ executeCypher: vi.fn().mockResolvedValueOnce({ data: [] }),
+ } as any;
+
+ const engine = new CoordinationEngine(memgraph);
+ const feedback = await engine.release("nonexistent-claim");
+
+ expect(feedback.found).toBe(false);
+ expect(feedback.alreadyClosed).toBe(false);
+ expect(memgraph.executeCypher).toHaveBeenCalledTimes(1);
+ });
+});
+
+describe("CoordinationEngine.status", () => {
+ it("returns activeClaims and recentEpisodes for an agent", async () => {
+ const claimRow = {
+ c: {
+ id: "claim-10",
+ agentId: "agent-a",
+ sessionId: "s",
+ taskId: "task-2",
+ claimType: "task",
+ targetId: "task-2",
+ intent: "do it",
+ validFrom: 100,
+ validTo: null,
+ projectId: "proj",
+ },
+ };
+ const episodeRow = {
+ id: "ep-1",
+ type: "OBSERVATION",
+ content: "did something",
+ timestamp: 200,
+ taskId: "task-2",
+ };
+
+ const memgraph = {
+ executeCypher: vi
+ .fn()
+ .mockResolvedValueOnce({ data: [claimRow] }) // AGENT_ACTIVE_CLAIMS
+ .mockResolvedValueOnce({ data: [episodeRow] }), // AGENT_RECENT_EPISODES
+ } as any;
+
+ const engine = new CoordinationEngine(memgraph);
+ const status = await engine.status("agent-a", "proj");
+
+ expect(status.agentId).toBe("agent-a");
+ expect(status.activeClaims).toHaveLength(1);
+ expect(status.activeClaims[0]?.id).toBe("claim-10");
+ expect(status.currentTask).toBe("task-2");
+ expect(status.recentEpisodes).toHaveLength(1);
+ expect(status.recentEpisodes[0]?.content).toBe("did something");
+ });
+
+ it("returns empty lists when agent has no claims or episodes", async () => {
+ const memgraph = {
+ executeCypher: vi
+ .fn()
+ .mockResolvedValueOnce({ data: [] })
+ .mockResolvedValueOnce({ data: [] }),
+ } as any;
+
+ const engine = new CoordinationEngine(memgraph);
+ const status = await engine.status("agent-unknown", "proj");
+
+ expect(status.activeClaims).toHaveLength(0);
+ expect(status.recentEpisodes).toHaveLength(0);
+ expect(status.currentTask).toBeUndefined();
+ });
+});
+
+describe("CoordinationEngine.invalidateStaleClaims", () => {
+ it("returns count of invalidated claims", async () => {
+ const memgraph = {
+ executeCypher: vi.fn().mockResolvedValueOnce({ data: [{ invalidated: 3 }] }),
+ } as any;
+
+ const engine = new CoordinationEngine(memgraph);
+ const count = await engine.invalidateStaleClaims("proj");
+
+ expect(count).toBe(3);
+ });
+
+ it("returns 0 when no stale claims", async () => {
+ const memgraph = {
+ executeCypher: vi.fn().mockResolvedValueOnce({ data: [] }),
+ } as any;
+
+ const engine = new CoordinationEngine(memgraph);
+ expect(await engine.invalidateStaleClaims("proj")).toBe(0);
+ });
+});
+
+describe("CoordinationEngine.expireOldClaims", () => {
+ it("fires the EXPIRE_OLD_CLAIMS query with correct cutoff", async () => {
+ const memgraph = {
+ executeCypher: vi.fn().mockResolvedValueOnce({ data: [{ expired: 5 }] }),
+ } as any;
+
+ const engine = new CoordinationEngine(memgraph);
+ const count = await engine.expireOldClaims("proj", 3_600_000); // 1 hour TTL
+
+ expect(count).toBe(5);
+ const [, params] = memgraph.executeCypher.mock.calls[0] as [string, Record];
+ expect(params.projectId).toBe("proj");
+ expect(params.cutoffMs).toBeLessThan(params.now);
+ expect(params.now - params.cutoffMs).toBeCloseTo(3_600_000, -2);
+ });
+
+ it("returns 0 when no claims expired", async () => {
+ const memgraph = {
+ executeCypher: vi.fn().mockResolvedValueOnce({ data: [] }),
+ } as any;
+ const engine = new CoordinationEngine(memgraph);
+ expect(await engine.expireOldClaims("proj", 1000)).toBe(0);
+ });
+});
+
+describe("CoordinationEngine.onTaskCompleted", () => {
+ it("calls ON_TASK_COMPLETED query with correct params", async () => {
+ const memgraph = {
+ executeCypher: vi.fn().mockResolvedValueOnce({ data: [] }),
+ } as any;
+
+ const engine = new CoordinationEngine(memgraph);
+ await engine.onTaskCompleted("task-7", "agent-a", "proj");
+
+ expect(memgraph.executeCypher).toHaveBeenCalledOnce();
+ const [, params] = memgraph.executeCypher.mock.calls[0] as [string, Record];
+ expect(params.taskId).toBe("task-7");
+ expect(params.projectId).toBe("proj");
+ expect(String(params.outcome)).toContain("agent-a");
+ });
+});
+// ── overview() ───────────────────────────────────────────────────────────────
+
+describe("CoordinationEngine.overview", () => {
+ /** Builds a MemgraphClient that responds to 5 parallel queries in order. */
+ function makeOverviewMemgraph({
+ active = [],
+ stale = [],
+ conflicts = [],
+ summary = [],
+ total = [],
+ }: {
+ active?: unknown[];
+ stale?: unknown[];
+ conflicts?: unknown[];
+ summary?: unknown[];
+ total?: unknown[];
+ } = {}) {
+ // overview() uses Promise.all with 5 fixed queries; mock them in call order.
+ return {
+ executeCypher: vi
+ .fn()
+ .mockResolvedValueOnce({ data: active }) // OVERVIEW_ACTIVE
+ .mockResolvedValueOnce({ data: stale }) // OVERVIEW_STALE
+ .mockResolvedValueOnce({ data: conflicts }) // OVERVIEW_CONFLICTS
+ .mockResolvedValueOnce({ data: summary }) // OVERVIEW_AGENT_SUMMARY
+ .mockResolvedValueOnce({ data: total }), // OVERVIEW_TOTAL
+ } as any;
+ }
+
+ it("returns all empty collections for an empty graph", async () => {
+ const memgraph = makeOverviewMemgraph();
+ const engine = new CoordinationEngine(memgraph);
+
+ const result = await engine.overview("proj");
+
+ expect(result.activeClaims).toHaveLength(0);
+ expect(result.staleClaims).toHaveLength(0);
+ expect(result.conflicts).toHaveLength(0);
+ expect(result.agentSummary).toHaveLength(0);
+ expect(result.totalClaims).toBe(0);
+ });
+
+ it("populates activeClaims from OVERVIEW_ACTIVE rows", async () => {
+ const activeRow = {
+ c: {
+ id: "claim-active-1",
+ agentId: "agent-a",
+ sessionId: "s",
+ claimType: "file",
+ targetId: "file:src/x.ts",
+ intent: "editing",
+ validFrom: 1000,
+ validTo: null,
+ projectId: "proj",
+ },
+ };
+ const memgraph = makeOverviewMemgraph({ active: [activeRow] });
+ const engine = new CoordinationEngine(memgraph);
+
+ const result = await engine.overview("proj");
+
+ expect(result.activeClaims).toHaveLength(1);
+ expect(result.activeClaims[0]?.id).toBe("claim-active-1");
+ expect(result.activeClaims[0]?.agentId).toBe("agent-a");
+ });
+
+ it("populates staleClaims independently of activeClaims", async () => {
+ const staleRow = {
+ c: {
+ id: "claim-stale-1",
+ agentId: "agent-b",
+ sessionId: "s2",
+ claimType: "task",
+ targetId: "task-old",
+ intent: "stale work",
+ validFrom: 500,
+ validTo: null,
+ projectId: "proj",
+ },
+ };
+ const memgraph = makeOverviewMemgraph({ stale: [staleRow] });
+ const engine = new CoordinationEngine(memgraph);
+
+ const result = await engine.overview("proj");
+
+ expect(result.staleClaims).toHaveLength(1);
+ expect(result.staleClaims[0]?.id).toBe("claim-stale-1");
+ expect(result.activeClaims).toHaveLength(0);
+ });
+
+ it("maps conflict rows into structured claimA/claimB objects", async () => {
+ const conflictRow = {
+ targetId: "file:shared.ts",
+ claimAId: "claim-x",
+ claimAAgent: "agent-a",
+ claimAIntent: "refactor x",
+ claimASince: 1000,
+ claimBId: "claim-y",
+ claimBAgent: "agent-b",
+ claimBIntent: "refactor y",
+ claimBSince: 2000,
+ };
+ const memgraph = makeOverviewMemgraph({ conflicts: [conflictRow] });
+ const engine = new CoordinationEngine(memgraph);
+
+ const result = await engine.overview("proj");
+
+ expect(result.conflicts).toHaveLength(1);
+ const conflict = result.conflicts[0];
+ expect(conflict?.targetId).toBe("file:shared.ts");
+ expect(conflict?.claimA.claimId).toBe("claim-x");
+ expect(conflict?.claimA.agentId).toBe("agent-a");
+ expect(conflict?.claimB.claimId).toBe("claim-y");
+ expect(conflict?.claimB.agentId).toBe("agent-b");
+ });
+
+ it("maps agentSummary rows correctly", async () => {
+ const summaryRow = { agentId: "agent-a", claimCount: 3, lastSeen: 12345 };
+ const memgraph = makeOverviewMemgraph({ summary: [summaryRow] });
+ const engine = new CoordinationEngine(memgraph);
+
+ const result = await engine.overview("proj");
+
+ expect(result.agentSummary).toHaveLength(1);
+ expect(result.agentSummary[0]?.agentId).toBe("agent-a");
+ expect(result.agentSummary[0]?.claimCount).toBe(3);
+ expect(result.agentSummary[0]?.lastSeen).toBe(12345);
+ });
+
+ it("reads totalClaims from OVERVIEW_TOTAL result", async () => {
+ const memgraph = makeOverviewMemgraph({ total: [{ totalClaims: 7 }] });
+ const engine = new CoordinationEngine(memgraph);
+
+ const result = await engine.overview("proj");
+
+ expect(result.totalClaims).toBe(7);
+ });
+
+ it("fires exactly 5 Cypher queries for a single call", async () => {
+ const memgraph = makeOverviewMemgraph();
+ const engine = new CoordinationEngine(memgraph);
+
+ await engine.overview("proj");
+
+ expect(memgraph.executeCypher).toHaveBeenCalledTimes(5);
+ });
+});
diff --git a/src/engines/docs-engine.test.ts b/src/engines/__tests__/docs-engine.test.ts
similarity index 94%
rename from src/engines/docs-engine.test.ts
rename to src/engines/__tests__/docs-engine.test.ts
index 451b5e2..3b0f184 100644
--- a/src/engines/docs-engine.test.ts
+++ b/src/engines/__tests__/docs-engine.test.ts
@@ -1,15 +1,15 @@
import * as path from "node:path";
import * as url from "node:url";
import { describe, expect, it, vi, beforeEach } from "vitest";
-import { DocsEngine, DOCS_COLLECTION } from "./docs-engine.js";
-import type { DocsIndexOptions } from "./docs-engine.js";
-import type { MemgraphClient, QueryResult } from "../graph/client.js";
-import type { QdrantClient } from "../vector/qdrant-client.js";
-import type { ParsedDoc } from "../parsers/docs-parser.js";
-import { DocsParser } from "../parsers/docs-parser.js";
+import { DocsEngine, DOCS_COLLECTION } from "../docs-engine.js";
+import type { DocsIndexOptions } from "../docs-engine.js";
+import type { MemgraphClient, QueryResult } from "../../graph/client.js";
+import type { QdrantClient } from "../../vector/qdrant-client.js";
+import type { ParsedDoc } from "../../parsers/docs-parser.js";
+import { DocsParser } from "../../parsers/docs-parser.js";
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
-const FIXTURES = path.resolve(__dirname, "../parsers/__fixtures__");
+const FIXTURES = path.resolve(__dirname, "../../parsers/__fixtures__");
// ─── Mock factories ───────────────────────────────────────────────────────────
@@ -129,10 +129,7 @@ describe("DocsEngine.indexWorkspace", () => {
const qdrant = makeQdrant(true);
const engine = new DocsEngine(mg, { qdrant });
await engine.indexWorkspace(FIXTURES, "proj", { withEmbeddings: true });
- expect(qdrant.upsertPoints).toHaveBeenCalledWith(
- DOCS_COLLECTION,
- expect.any(Array),
- );
+ expect(qdrant.upsertPoints).toHaveBeenCalledWith(DOCS_COLLECTION, expect.any(Array));
});
it("with withEmbeddings=true does NOT upsert when Qdrant not connected", async () => {
@@ -144,9 +141,7 @@ describe("DocsEngine.indexWorkspace", () => {
});
it("uses the custom buildCypher override when provided", async () => {
- const customBuild = vi
- .fn()
- .mockReturnValue([{ query: "RETURN 1", params: {} }]);
+ const customBuild = vi.fn().mockReturnValue([{ query: "RETURN 1", params: {} }]);
const mg = makeMemgraph();
const engine = new DocsEngine(mg, { buildCypher: customBuild });
await engine.indexWorkspace(FIXTURES, "proj");
diff --git a/src/engines/__tests__/episode-engine.test.ts b/src/engines/__tests__/episode-engine.test.ts
new file mode 100644
index 0000000..d7eb1da
--- /dev/null
+++ b/src/engines/__tests__/episode-engine.test.ts
@@ -0,0 +1,556 @@
+// ── Episode Engine — Tests ─────────────────────────────────────────────────
+//
+// Tests for:
+// - EpisodeEngine.add() → CREATE node + INVOLVES links + NEXT_EPISODE
+// - EpisodeEngine.recall() → filter + hybrid lexical/temporal/graph score
+// - EpisodeEngine.decisionQuery() → delegate with DECISION type filter
+// - EpisodeEngine.reflect() → aggregate patterns → REFLECTION + LEARNING
+//
+// Memgraph is mocked via vi.fn(). executeCypher is keyed on query substrings.
+//
+// Conventions match ./progress-engine.test.ts and ./coordination-engine.test.ts
+
+import { describe, expect, it, vi, beforeEach } from "vitest";
+import EpisodeEngine, { type EpisodeInput } from "../episode-engine.js";
+
+// ── Helpers ─────────────────────────────────────────────────────────────────
+
+/**
+ * Builds a minimal MemgraphClient mock.
+ *
+ * @param overrides Map of query-substring → row array to return.
+ * First matching key wins. Defaults to empty rows.
+ */
+function makeMockMemgraph(overrides: Record = {}) {
+ return {
+ executeCypher: vi.fn(async (query: string) => {
+ for (const [key, data] of Object.entries(overrides)) {
+ if (query.includes(key)) {
+ return { data };
+ }
+ }
+ return { data: [] };
+ }),
+ } as any;
+}
+
+/** Pre-built episode row that matches the shape rowToEpisode() expects. */
+function makeEpisodeRow(overrides: Record = {}) {
+ return {
+ e: {
+ properties: {
+ id: "ep-123",
+ agentId: "agent-a",
+ sessionId: "sess-1",
+ taskId: "task-1",
+ type: "OBSERVATION",
+ content: "worked on the parser module",
+ timestamp: Date.now() - 3600_000, // 1 hour ago
+ outcome: "success",
+ metadata: '{"note":"foo"}',
+ sensitive: false,
+ entities: ["src/parsers/typescript-parser.ts"],
+ projectId: "proj-a",
+ ...overrides,
+ },
+ },
+ };
+}
+
+// ── add() ───────────────────────────────────────────────────────────────────
+
+describe("EpisodeEngine.add()", () => {
+ it("creates an EPISODE node via executeCypher and returns a string id", async () => {
+ const memgraph = makeMockMemgraph();
+ const engine = new EpisodeEngine(memgraph as any);
+
+ const input: EpisodeInput = {
+ agentId: "agent-a",
+ sessionId: "sess-1",
+ type: "OBSERVATION",
+ content: "initial observation",
+ };
+
+ const id = await engine.add(input, "proj-a");
+
+ expect(typeof id).toBe("string");
+ expect(id.startsWith("ep-")).toBe(true);
+
+ // First call is CREATE EPISODE
+ const firstCall = memgraph.executeCypher.mock.calls[0];
+ expect(firstCall[0]).toContain("CREATE (e:EPISODE");
+ expect(firstCall[1]).toMatchObject({
+ agentId: "agent-a",
+ sessionId: "sess-1",
+ type: "OBSERVATION",
+ projectId: "proj-a",
+ });
+ });
+
+ it("creates INVOLVES links for each entity", async () => {
+ const memgraph = makeMockMemgraph();
+ const engine = new EpisodeEngine(memgraph as any);
+
+ const input: EpisodeInput = {
+ agentId: "agent-a",
+ sessionId: "sess-1",
+ type: "DECISION",
+ content: "refactored parsers",
+ entities: ["src/parsers/typescript-parser.ts", "src/parsers/regex-language-parsers.ts"],
+ };
+
+ await engine.add(input, "proj-a");
+
+ const involveCalls = memgraph.executeCypher.mock.calls.filter(([q]: [string]) =>
+ q.includes("INVOLVES"),
+ );
+
+ expect(involveCalls).toHaveLength(2);
+ expect(involveCalls[0][1]).toMatchObject({
+ entityId: "src/parsers/typescript-parser.ts",
+ });
+ expect(involveCalls[1][1]).toMatchObject({
+ entityId: "src/parsers/regex-language-parsers.ts",
+ });
+ });
+
+ it("caps entities at 100 items", async () => {
+ const memgraph = makeMockMemgraph();
+ const engine = new EpisodeEngine(memgraph as any);
+ const tooManyEntities = Array.from({ length: 150 }, (_, i) => `entity-${i}`);
+
+ await engine.add(
+ {
+ agentId: "a",
+ sessionId: "s",
+ type: "OBSERVATION",
+ content: "c",
+ entities: tooManyEntities,
+ },
+ "proj-a",
+ );
+
+ const involveCalls = memgraph.executeCypher.mock.calls.filter(([q]: [string]) =>
+ q.includes("INVOLVES"),
+ );
+ expect(involveCalls).toHaveLength(100);
+ });
+
+ it("attempts to link to previous episode in same session", async () => {
+ const memgraph = makeMockMemgraph({
+ NEXT_EPISODE: [], // prev lookup returns nothing
+ });
+ const engine = new EpisodeEngine(memgraph as any);
+
+ await engine.add(
+ { agentId: "agent-a", sessionId: "sess-1", type: "OBSERVATION", content: "c" },
+ "proj-a",
+ );
+
+ const prevLookup = memgraph.executeCypher.mock.calls.find(
+ ([q]: [string]) =>
+ q.includes("NEXT_EPISODE") ||
+ (q.includes("ORDER BY e.timestamp DESC") && q.includes("LIMIT 1")),
+ );
+ expect(prevLookup).toBeTruthy();
+ });
+
+ it("creates NEXT_EPISODE link when a previous episode exists", async () => {
+ const memgraph = makeMockMemgraph({
+ "LIMIT 1": [{ id: "ep-prev-001" }],
+ });
+ const engine = new EpisodeEngine(memgraph as any);
+
+ await engine.add(
+ { agentId: "agent-a", sessionId: "sess-1", type: "OBSERVATION", content: "c" },
+ "proj-a",
+ );
+
+ const mergeCalls = memgraph.executeCypher.mock.calls.filter(([q]: [string]) =>
+ q.includes("NEXT_EPISODE"),
+ );
+ expect(mergeCalls.length).toBeGreaterThanOrEqual(1);
+ });
+
+ it("handles missing optional fields gracefully (taskId, outcome, metadata)", async () => {
+ const memgraph = makeMockMemgraph();
+ const engine = new EpisodeEngine(memgraph as any);
+
+ const id = await engine.add(
+ { agentId: "a", sessionId: "s", type: "LEARNING", content: "content" },
+ "proj-a",
+ );
+
+ expect(id).toBeTruthy();
+ const createCall = memgraph.executeCypher.mock.calls[0];
+ expect(createCall[1].taskId).toBeNull();
+ expect(createCall[1].outcome).toBeNull();
+ expect(createCall[1].sensitive).toBe(false);
+ });
+});
+
+// ── recall() ────────────────────────────────────────────────────────────────
+
+describe("EpisodeEngine.recall()", () => {
+ it("returns empty array when no episodes in DB", async () => {
+ const engine = new EpisodeEngine(makeMockMemgraph() as any);
+ const episodes = await engine.recall({ query: "test", projectId: "proj-a" });
+ expect(episodes).toEqual([]);
+ });
+
+ it("maps DB rows to Episode objects with hybrid relevance scores", async () => {
+ const memgraph = makeMockMemgraph({
+ "MATCH (e:EPISODE)": [makeEpisodeRow()],
+ });
+ const engine = new EpisodeEngine(memgraph as any);
+
+ const episodes = await engine.recall({
+ query: "parser module",
+ projectId: "proj-a",
+ });
+
+ expect(episodes).toHaveLength(1);
+ expect(episodes[0].id).toBe("ep-123");
+ expect(episodes[0].agentId).toBe("agent-a");
+ expect(typeof episodes[0].relevance).toBe("number");
+ expect(episodes[0].relevance).toBeGreaterThanOrEqual(0);
+ expect(episodes[0].relevance).toBeLessThanOrEqual(1);
+ });
+
+ it("filters by agentId when provided", async () => {
+ const memgraph = makeMockMemgraph();
+ const engine = new EpisodeEngine(memgraph as any);
+
+ await engine.recall({ query: "q", projectId: "proj-a", agentId: "agent-x" });
+
+ const cypher = memgraph.executeCypher.mock.calls[0][0];
+ expect(cypher).toContain("e.agentId = $agentId");
+ expect(memgraph.executeCypher.mock.calls[0][1]).toMatchObject({ agentId: "agent-x" });
+ });
+
+ it("filters by taskId when provided", async () => {
+ const memgraph = makeMockMemgraph();
+ const engine = new EpisodeEngine(memgraph as any);
+
+ await engine.recall({ query: "q", projectId: "proj-a", taskId: "my-task" });
+
+ const cypher = memgraph.executeCypher.mock.calls[0][0];
+ expect(cypher).toContain("e.taskId = $taskId");
+ });
+
+ it("filters by types array when provided", async () => {
+ const memgraph = makeMockMemgraph();
+ const engine = new EpisodeEngine(memgraph as any);
+
+ await engine.recall({ query: "q", projectId: "proj-a", types: ["DECISION", "LEARNING"] });
+
+ const { types } = memgraph.executeCypher.mock.calls[0][1];
+ expect(types).toEqual(["DECISION", "LEARNING"]);
+ });
+
+ it("filters by since timestamp when provided", async () => {
+ const memgraph = makeMockMemgraph();
+ const engine = new EpisodeEngine(memgraph as any);
+ const since = Date.now() - 86400_000;
+
+ await engine.recall({ query: "q", projectId: "proj-a", since });
+
+ expect(memgraph.executeCypher.mock.calls[0][1]).toMatchObject({ since });
+ });
+
+ it("caps limit to 50", async () => {
+ const memgraph = makeMockMemgraph();
+ const engine = new EpisodeEngine(memgraph as any);
+
+ await engine.recall({ query: "q", projectId: "proj-a", limit: 999 });
+
+ const { limit } = memgraph.executeCypher.mock.calls[0][1];
+ expect(limit).toBe(50);
+ });
+
+ it("enforces minimum limit of 1 (limit:1 is preserved)", async () => {
+ const memgraph = makeMockMemgraph();
+ const engine = new EpisodeEngine(memgraph as any);
+
+ await engine.recall({ query: "q", projectId: "proj-a", limit: 1 });
+
+ const { limit } = memgraph.executeCypher.mock.calls[0][1];
+ expect(limit).toBe(1);
+ });
+
+ it("defaults limit to 5 when limit is 0 (falsy)", async () => {
+ const memgraph = makeMockMemgraph();
+ const engine = new EpisodeEngine(memgraph as any);
+
+ await engine.recall({ query: "q", projectId: "proj-a", limit: 0 });
+
+ const { limit } = memgraph.executeCypher.mock.calls[0][1];
+ expect(limit).toBe(5); // 0 is falsy → 0||5 = 5 → clamp(1,50) = 5
+ });
+
+ it("scores higher for episodes with matching entity overlap", async () => {
+ const sharedEntity = "src/parsers/typescript-parser.ts";
+ const rowWithEntity = makeEpisodeRow({ entities: [sharedEntity] });
+ const rowNoEntity = makeEpisodeRow({
+ id: "ep-456",
+ content: "unrelated content",
+ entities: [],
+ });
+
+ const memgraph = makeMockMemgraph({
+ "MATCH (e:EPISODE)": [rowWithEntity, rowNoEntity],
+ });
+ const engine = new EpisodeEngine(memgraph as any);
+
+ const episodes = await engine.recall({
+ query: "parser",
+ projectId: "proj-a",
+ entities: [sharedEntity],
+ });
+
+ // Both returned but entity-matched episode should score higher
+ expect(episodes.length).toBeGreaterThanOrEqual(1);
+ const entityMatched = episodes.find((e) => e.id === "ep-123");
+ const noEntity = episodes.find((e) => e.id === "ep-456");
+ if (entityMatched && noEntity) {
+ expect(entityMatched.relevance).toBeGreaterThan(noEntity.relevance!);
+ }
+ });
+
+ it("handles rows with nested .properties structure", async () => {
+ const memgraph = makeMockMemgraph({
+ "MATCH (e:EPISODE)": [makeEpisodeRow()],
+ });
+ const engine = new EpisodeEngine(memgraph as any);
+ const episodes = await engine.recall({ query: "parser", projectId: "proj-a" });
+
+ expect(episodes[0].content).toBe("worked on the parser module");
+ });
+
+ it("handles flat row structure (no .properties wrapper)", async () => {
+ const flatRow = {
+ id: "ep-flat",
+ agentId: "a",
+ sessionId: "s",
+ type: "OBSERVATION",
+ content: "flat row test",
+ timestamp: Date.now(),
+ entities: [],
+ projectId: "proj-a",
+ };
+ const memgraph = makeMockMemgraph({ "MATCH (e:EPISODE)": [flatRow] });
+ const engine = new EpisodeEngine(memgraph as any);
+
+ const episodes = await engine.recall({ query: "flat", projectId: "proj-a" });
+ expect(episodes[0].id).toBe("ep-flat");
+ });
+
+ it("parses JSON metadata from stored string", async () => {
+ const memgraph = makeMockMemgraph({
+ "MATCH (e:EPISODE)": [makeEpisodeRow({ metadata: '{"rationale":"speed"}' })],
+ });
+ const engine = new EpisodeEngine(memgraph as any);
+ const episodes = await engine.recall({ query: "q", projectId: "proj-a" });
+
+ expect(episodes[0].metadata).toEqual({ rationale: "speed" });
+ });
+
+ it("returns undefined metadata for invalid JSON", async () => {
+ const memgraph = makeMockMemgraph({
+ "MATCH (e:EPISODE)": [makeEpisodeRow({ metadata: "not-json{{{" })],
+ });
+ const engine = new EpisodeEngine(memgraph as any);
+ const episodes = await engine.recall({ query: "q", projectId: "proj-a" });
+
+ expect(episodes[0].metadata).toBeUndefined();
+ });
+
+ it("skips null/invalid rows gracefully (rowToEpisode returns null)", async () => {
+ const memgraph = makeMockMemgraph({
+ "MATCH (e:EPISODE)": [null, makeEpisodeRow(), undefined],
+ });
+ const engine = new EpisodeEngine(memgraph as any);
+ const episodes = await engine.recall({ query: "q", projectId: "proj-a" });
+
+ // Only the valid row is returned
+ expect(episodes).toHaveLength(1);
+ });
+});
+
+// ── decisionQuery() ──────────────────────────────────────────────────────────
+
+describe("EpisodeEngine.decisionQuery()", () => {
+ it("delegates to recall() with types=['DECISION']", async () => {
+ const memgraph = makeMockMemgraph({
+ "MATCH (e:EPISODE)": [
+ makeEpisodeRow({ type: "DECISION", content: "chose typescript over js" }),
+ ],
+ });
+ const engine = new EpisodeEngine(memgraph as any);
+
+ const results = await engine.decisionQuery({
+ query: "typescript",
+ projectId: "proj-a",
+ });
+
+ expect(results).toHaveLength(1);
+ // Verify the types filter was applied
+ const { types } = memgraph.executeCypher.mock.calls[0][1];
+ expect(types).toEqual(["DECISION"]);
+ });
+});
+
+// ── reflect() ────────────────────────────────────────────────────────────────
+
+describe("EpisodeEngine.reflect()", () => {
+ it("returns a reflection with insight and learningsCreated=0 when no episodes", async () => {
+ const engine = new EpisodeEngine(makeMockMemgraph() as any);
+
+ const result = await engine.reflect({ projectId: "proj-a" });
+
+ expect(result.learningsCreated).toBe(0);
+ expect(result.insight).toContain("0 episodes");
+ expect(result.patterns).toEqual([]);
+ expect(typeof result.reflectionId).toBe("string");
+ });
+
+ it("extracts entity patterns from multiple episodes", async () => {
+ const entity = "src/engines/episode-engine.ts";
+ const rows = [
+ makeEpisodeRow({ entities: [entity], content: "worked on memory" }),
+ makeEpisodeRow({ id: "ep-456", entities: [entity], content: "more memory work" }),
+ makeEpisodeRow({ id: "ep-789", entities: [entity], content: "again memory" }),
+ ];
+ const memgraph = makeMockMemgraph({ "MATCH (e:EPISODE)": rows });
+ const engine = new EpisodeEngine(memgraph as any);
+
+ const result = await engine.reflect({ projectId: "proj-a" });
+
+ expect(result.patterns).toHaveLength(1);
+ expect(result.patterns[0].file).toBe(entity);
+ expect(result.patterns[0].count).toBe(3);
+ });
+
+ it("creates LEARNING nodes for top 3 patterns", async () => {
+ const entities = ["src/file-a.ts", "src/file-b.ts", "src/file-c.ts", "src/file-d.ts"];
+ const rows = entities.map((e, i) =>
+ makeEpisodeRow({ id: `ep-${i}`, entities: [e], content: `content about ${e}` }),
+ );
+ // Add more occurrences of first entity to make it high-frequency
+ rows.push(makeEpisodeRow({ id: "ep-extra-a1", entities: [entities[0]], content: "more a" }));
+ rows.push(
+ makeEpisodeRow({ id: "ep-extra-a2", entities: [entities[0]], content: "more a again" }),
+ );
+
+ const memgraph = makeMockMemgraph({ "MATCH (e:EPISODE)": rows });
+ const engine = new EpisodeEngine(memgraph as any);
+
+ const result = await engine.reflect({ projectId: "proj-a" });
+
+ // Should create at most 3 learnings
+ expect(result.learningsCreated).toBeLessThanOrEqual(3);
+
+ const learningCalls = memgraph.executeCypher.mock.calls.filter(([q]: [string]) =>
+ q.includes("CREATE (l:LEARNING"),
+ );
+ expect(learningCalls.length).toBe(result.learningsCreated);
+ });
+
+ it("filters by agentId when provided", async () => {
+ const memgraph = makeMockMemgraph();
+ const engine = new EpisodeEngine(memgraph as any);
+
+ await engine.reflect({ projectId: "proj-a", agentId: "agent-x" });
+
+ // First call is recall → check agentId filter
+ const { agentId } = memgraph.executeCypher.mock.calls[0][1];
+ expect(agentId).toBe("agent-x");
+ });
+
+ it("generates an insight listing top patterns", async () => {
+ const rows = [
+ makeEpisodeRow({ entities: ["src/a.ts"], content: "about a" }),
+ makeEpisodeRow({ id: "e2", entities: ["src/b.ts"], content: "about b" }),
+ ];
+ const memgraph = makeMockMemgraph({ "MATCH (e:EPISODE)": rows });
+ const engine = new EpisodeEngine(memgraph as any);
+
+ const result = await engine.reflect({ projectId: "proj-a" });
+
+ expect(result.insight).toContain("Reflection over");
+ expect(result.insight).toContain("episodes");
+ });
+});
+
+// ── Internal helpers (tested via public API) ──────────────────────────────────
+
+describe("EpisodeEngine private helpers (via recall)", () => {
+ it("jaccard returns 1 for identical token sets", async () => {
+ // Same content and query → lexical score should be high
+ const content = "typescript parser engine analysis";
+ const memgraph = makeMockMemgraph({
+ "MATCH (e:EPISODE)": [makeEpisodeRow({ content })],
+ });
+ const engine = new EpisodeEngine(memgraph as any);
+
+ const [episode] = await engine.recall({
+ query: content,
+ projectId: "proj-a",
+ });
+
+ // lexical jaccard(same, same) = 1.0
+ expect(episode.relevance).toBeGreaterThan(0.4);
+ });
+
+ it("jaccard returns 0 when sets are disjoint", async () => {
+ const memgraph = makeMockMemgraph({
+ "MATCH (e:EPISODE)": [makeEpisodeRow({ content: "zzz yyy xxx" })],
+ });
+ const engine = new EpisodeEngine(memgraph as any);
+
+ // Query has completely different tokens
+ const [episode] = await engine.recall({
+ query: "aaa bbb ccc",
+ projectId: "proj-a",
+ });
+
+ // lexical score should be near 0, but temporal score lifts it slightly
+ expect(episode.relevance).toBeLessThanOrEqual(0.5);
+ });
+
+ it("returns 4-decimal precision on relevance score", async () => {
+ const memgraph = makeMockMemgraph({
+ "MATCH (e:EPISODE)": [makeEpisodeRow()],
+ });
+ const engine = new EpisodeEngine(memgraph as any);
+ const [episode] = await engine.recall({ query: "parser", projectId: "proj-a" });
+
+ // toFixed(4) result - should have at most 4 decimal places
+ const decimals = String(episode.relevance).split(".")[1] ?? "";
+ expect(decimals.length).toBeLessThanOrEqual(4);
+ });
+
+ it("sorts results by relevance descending", async () => {
+ const old = makeEpisodeRow({
+ id: "old-ep",
+ timestamp: Date.now() - 30 * 86400_000, // 30 days ago
+ content: "old unrelated xyz",
+ });
+ const recent = makeEpisodeRow({
+ id: "recent-ep",
+ timestamp: Date.now() - 100,
+ content: "very recent important",
+ });
+ const memgraph = makeMockMemgraph({
+ "MATCH (e:EPISODE)": [old, recent],
+ });
+ const engine = new EpisodeEngine(memgraph as any);
+
+ const results = await engine.recall({ query: "recent important", projectId: "proj-a" });
+
+ // The result is sorted desc by relevance
+ for (let i = 1; i < results.length; i++) {
+ expect(results[i - 1].relevance!).toBeGreaterThanOrEqual(results[i].relevance!);
+ }
+ });
+});
diff --git a/src/engines/__tests__/progress-engine.test.ts b/src/engines/__tests__/progress-engine.test.ts
new file mode 100644
index 0000000..87de84b
--- /dev/null
+++ b/src/engines/__tests__/progress-engine.test.ts
@@ -0,0 +1,128 @@
+import { describe, expect, it, vi } from "vitest";
+import GraphIndexManager from "../../graph/index.js";
+import { ProgressEngine, type Feature, type Task } from "../progress-engine.js";
+
+function buildIndex(): GraphIndexManager {
+ const index = new GraphIndexManager();
+
+ index.addNode("proj-a:feature:f1", "FEATURE", {
+ name: "Feature 1",
+ status: "in-progress",
+ });
+ index.addNode("proj-a:task:t1", "TASK", {
+ name: "Task 1",
+ status: "completed",
+ featureId: "proj-a:feature:f1",
+ });
+ index.addNode("proj-a:task:t2", "TASK", {
+ name: "Task 2",
+ status: "blocked",
+ featureId: "proj-a:feature:f1",
+ blockedBy: ["x", "y", "z"],
+ });
+
+ index.addNode("file:impl", "FILE", {
+ path: "src/impl.ts",
+ projectId: "proj-a",
+ });
+ index.addNode("fn:impl", "FUNCTION", { name: "run", projectId: "proj-a" });
+ index.addNode("class:impl", "CLASS", { name: "Runner", projectId: "proj-a" });
+ index.addNode("suite:1", "TEST_SUITE", {
+ name: "impl suite",
+ projectId: "proj-a",
+ });
+ index.addNode("case:1", "TEST_CASE", {
+ name: "impl case",
+ projectId: "proj-a",
+ });
+
+ index.addRelationship("r1", "proj-a:feature:f1", "file:impl", "IMPLEMENTS");
+ index.addRelationship("r2", "file:impl", "fn:impl", "CONTAINS");
+ index.addRelationship("r3", "file:impl", "class:impl", "CONTAINS");
+ index.addRelationship("r4", "suite:1", "file:impl", "TESTS");
+ index.addRelationship("r5", "case:1", "file:impl", "TESTS");
+
+ return index;
+}
+
+describe("ProgressEngine", () => {
+ it("aggregates feature status with implementing code and tests", () => {
+ const engine = new ProgressEngine(buildIndex());
+ const status = engine.getFeatureStatus("proj-a:feature:f1");
+
+ expect(status).not.toBeNull();
+ expect(status?.tasks).toHaveLength(2);
+ expect(status?.implementingCode.files).toEqual(["src/impl.ts"]);
+ expect(status?.implementingCode.functions).toBe(1);
+ expect(status?.implementingCode.classes).toBe(1);
+ expect(status?.testCoverage.testSuites).toBe(1);
+ expect(status?.testCoverage.testCases).toBe(1);
+ expect(status?.blockingIssues).toHaveLength(1);
+ expect(status?.progressPercentage).toBe(50);
+ });
+
+ it("updates task timestamps for in-progress and completed transitions", () => {
+ const engine = new ProgressEngine(buildIndex());
+
+ const started = engine.updateTask("proj-a:task:t2", {
+ status: "in-progress",
+ });
+ const completed = engine.updateTask("proj-a:task:t2", {
+ status: "completed",
+ });
+
+ expect(started?.startedAt).toBeTypeOf("number");
+ expect(completed?.completedAt).toBeTypeOf("number");
+ });
+
+ it("requires connected memgraph for feature creation", async () => {
+ const memgraph = {
+ isConnected: vi.fn().mockReturnValue(false),
+ } as any;
+ const engine = new ProgressEngine(buildIndex(), memgraph);
+
+ const feature: Feature = {
+ id: "proj-a:feature:new",
+ name: "New Feature",
+ status: "pending",
+ };
+
+ await expect(engine.createFeature(feature)).rejects.toThrow("Memgraph is not connected");
+ });
+
+ it("reload filters features/tasks by project id", () => {
+ const index = buildIndex();
+ index.addNode("proj-b:feature:f2", "FEATURE", {
+ name: "Feature 2",
+ status: "pending",
+ });
+ index.addNode("proj-b:task:t3", "TASK", {
+ name: "Task 3",
+ status: "pending",
+ featureId: "proj-b:feature:f2",
+ });
+
+ const engine = new ProgressEngine(index);
+ engine.reload(index, "proj-a");
+
+ const featureQuery = engine.query("feature");
+ const taskQuery = engine.query("task");
+
+ expect(featureQuery.items.every((item) => item.id.startsWith("proj-a:"))).toBe(true);
+ expect(taskQuery.items.every((item) => item.id.startsWith("proj-a:"))).toBe(true);
+ });
+
+ it("returns false when persisting task update fails", async () => {
+ const memgraph = {
+ isConnected: vi.fn().mockReturnValue(true),
+ executeCypher: vi.fn().mockResolvedValue({ error: "write failed", data: [] }),
+ } as any;
+ const engine = new ProgressEngine(buildIndex(), memgraph);
+
+ const ok = await engine.persistTaskUpdate("proj-a:task:t1", {
+ status: "completed",
+ } as Partial);
+
+ expect(ok).toBe(false);
+ });
+});
diff --git a/src/engines/__tests__/test-engine.test.ts b/src/engines/__tests__/test-engine.test.ts
new file mode 100644
index 0000000..aba096d
--- /dev/null
+++ b/src/engines/__tests__/test-engine.test.ts
@@ -0,0 +1,308 @@
+import { describe, expect, it } from "vitest";
+import GraphIndexManager from "../../graph/index.js";
+import TestEngine from "../test-engine.js";
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+/**
+ * Build a GraphIndexManager pre-populated with two test suites:
+ * - a unit test at `src/utils/__tests__/units.test.ts`
+ * - an integration test at `src/services/__tests__/svc.integration.test.ts`
+ *
+ * Both suites have one TEST_CASE each. The unit test case directly depends on
+ * `src/utils/units.ts` which in turn imports `src/utils/helpers.ts`.
+ */
+function buildPopulatedIndex(): GraphIndexManager {
+ const index = new GraphIndexManager();
+
+ // --- source files ---------------------------------------------------------
+ index.addNode("file:units", "FILE", { path: "src/utils/units.ts" });
+ index.addNode("file:helpers", "FILE", { path: "src/utils/helpers.ts" });
+ index.addNode("file:svc", "FILE", { path: "src/services/svc.ts" });
+
+ // --- test suites ----------------------------------------------------------
+ index.addNode("suite:unit", "TEST_SUITE", {
+ path: "src/utils/__tests__/units.test.ts",
+ avgDuration: 100,
+ lastStatus: "pass",
+ });
+ index.addNode("suite:int", "TEST_SUITE", {
+ path: "src/services/__tests__/svc.integration.test.ts",
+ avgDuration: 500,
+ lastStatus: "unknown",
+ });
+
+ // --- test cases -----------------------------------------------------------
+ index.addNode("case:unit-1", "TEST_CASE", {
+ path: "src/utils/__tests__/units.test.ts",
+ name: "should work",
+ });
+ index.addNode("case:int-1", "TEST_CASE", {
+ path: "src/services/__tests__/svc.integration.test.ts",
+ name: "should integrate",
+ });
+
+ // --- TESTS relationships --------------------------------------------------
+ // unit test → directly tests units.ts
+ index.addRelationship("rel:case-unit-tests-units", "case:unit-1", "file:units", "TESTS");
+ // integration test → directly tests svc.ts
+ index.addRelationship("rel:case-int-tests-svc", "case:int-1", "file:svc", "TESTS");
+
+ // --- IMPORTS relationships ------------------------------------------------
+ // units.ts imports helpers.ts → indirect dependency for the unit test
+ index.addNode("import:helpers", "IMPORT", { source: "src/utils/helpers.ts" });
+ index.addRelationship("rel:units-imports-helpers", "file:units", "import:helpers", "IMPORTS");
+
+ return index;
+}
+
+// ---------------------------------------------------------------------------
+// Tests
+// ---------------------------------------------------------------------------
+
+describe("TestEngine", () => {
+ describe("getStatistics()", () => {
+ it("returns all-zero stats for an empty index", () => {
+ const engine = new TestEngine(new GraphIndexManager());
+ expect(engine.getStatistics()).toEqual({
+ totalTests: 0,
+ unitTests: 0,
+ integrationTests: 0,
+ performanceTests: 0,
+ e2eTests: 0,
+ averageDuration: 0,
+ });
+ });
+
+ it("counts categories correctly from graph data", () => {
+ const engine = new TestEngine(buildPopulatedIndex());
+ const stats = engine.getStatistics();
+
+ expect(stats.totalTests).toBe(2);
+ expect(stats.unitTests).toBe(1);
+ expect(stats.integrationTests).toBe(1);
+ expect(stats.performanceTests).toBe(0);
+ expect(stats.e2eTests).toBe(0);
+ // average: (100 + 500) / 2 = 300
+ expect(stats.averageDuration).toBe(300);
+ });
+
+ it("classifies performance tests", () => {
+ const index = new GraphIndexManager();
+ index.addNode("suite:bench", "TEST_SUITE", {
+ path: "benchmarks/process_benchmark.test.ts",
+ avgDuration: 2000,
+ });
+ const engine = new TestEngine(index);
+ const stats = engine.getStatistics();
+ expect(stats.performanceTests).toBe(1);
+ expect(stats.unitTests).toBe(0);
+ });
+
+ it("classifies e2e tests", () => {
+ const index = new GraphIndexManager();
+ index.addNode("suite:e2e", "TEST_SUITE", {
+ path: "tests/e2e/auth.test.ts",
+ avgDuration: 5000,
+ });
+ const engine = new TestEngine(index);
+ const stats = engine.getStatistics();
+ expect(stats.e2eTests).toBe(1);
+ expect(stats.unitTests).toBe(0);
+ });
+ });
+
+ // -------------------------------------------------------------------------
+ describe("selectAffectedTests()", () => {
+ it("returns empty result when no tests depend on the changed files", () => {
+ const engine = new TestEngine(buildPopulatedIndex());
+ const result = engine.selectAffectedTests(["src/unrelated/file.ts"]);
+
+ expect(result.selectedTests).toHaveLength(0);
+ expect(result.coverage.totalTests).toBe(2);
+ });
+
+ it("selects the unit test when its direct dependency changes", () => {
+ const engine = new TestEngine(buildPopulatedIndex());
+ const result = engine.selectAffectedTests(["src/utils/units.ts"]);
+
+ expect(result.selectedTests).toContain("src/utils/__tests__/units.test.ts");
+ expect(result.affectedSources).toContain("src/utils/units.ts");
+ });
+
+ it("selects the integration test when its direct dependency changes", () => {
+ const engine = new TestEngine(buildPopulatedIndex());
+ const result = engine.selectAffectedTests(["src/services/svc.ts"]);
+
+ expect(result.selectedTests).toContain(
+ "src/services/__tests__/svc.integration.test.ts",
+ );
+ });
+
+ it("normalises leading ./ in changed file paths", () => {
+ const engine = new TestEngine(buildPopulatedIndex());
+ const result = engine.selectAffectedTests(["./src/utils/units.ts"]);
+
+ expect(result.selectedTests).toContain("src/utils/__tests__/units.test.ts");
+ });
+
+ it("excludes integration tests when includeIntegration=false", () => {
+ const engine = new TestEngine(buildPopulatedIndex());
+ // Both tests depend on their own file; change both source files
+ const result = engine.selectAffectedTests(
+ ["src/utils/units.ts", "src/services/svc.ts"],
+ false,
+ );
+
+ expect(result.selectedTests).toContain("src/utils/__tests__/units.test.ts");
+ expect(result.selectedTests).not.toContain(
+ "src/services/__tests__/svc.integration.test.ts",
+ );
+ });
+
+ it("returns an empty selection (and falls back to related tests) when index is empty", () => {
+ const engine = new TestEngine(new GraphIndexManager());
+ const result = engine.selectAffectedTests(["src/anything.ts"]);
+
+ // No tests exist in the index so selectedTests must be empty
+ expect(result.selectedTests).toHaveLength(0);
+ expect(result.coverage.totalTests).toBe(0);
+ });
+
+ it("computes category as unit when only unit tests selected", () => {
+ const engine = new TestEngine(buildPopulatedIndex());
+ const result = engine.selectAffectedTests(["src/utils/units.ts"]);
+ expect(result.category).toBe("unit");
+ });
+
+ it("computes category as integration when only integration tests selected", () => {
+ const engine = new TestEngine(buildPopulatedIndex());
+ const result = engine.selectAffectedTests(["src/services/svc.ts"]);
+ expect(result.category).toBe("integration");
+ });
+
+ it("computes category as mixed when both unit and integration tests selected", () => {
+ const engine = new TestEngine(buildPopulatedIndex());
+ const result = engine.selectAffectedTests([
+ "src/utils/units.ts",
+ "src/services/svc.ts",
+ ]);
+ expect(result.category).toBe("mixed");
+ });
+
+ it("computes estimated time from suite durations", () => {
+ const engine = new TestEngine(buildPopulatedIndex());
+ const result = engine.selectAffectedTests([
+ "src/utils/units.ts",
+ "src/services/svc.ts",
+ ]);
+ // unit=100 + integration=500
+ expect(result.estimatedTime).toBe(600);
+ });
+
+ it("coverage percentage is proportional to total tests", () => {
+ const engine = new TestEngine(buildPopulatedIndex());
+ const result = engine.selectAffectedTests(["src/utils/units.ts"]);
+ // 1 of 2 tests → 50%
+ expect(result.coverage.percentage).toBe(50);
+ expect(result.coverage.testsSelected).toBe(1);
+ expect(result.coverage.totalTests).toBe(2);
+ });
+ });
+
+ // -------------------------------------------------------------------------
+ describe("findRelatedTests() via fallback path", () => {
+ it("uses mirror path to find tests when no direct dependency matches", () => {
+ const index = new GraphIndexManager();
+ // Register a test suite at the mirror location of a source file
+ index.addNode("suite:widget", "TEST_SUITE", {
+ path: "src/utils/__tests__/widget.test.ts",
+ avgDuration: 50,
+ lastStatus: "pass",
+ });
+
+ const engine = new TestEngine(index);
+ // No graph relationship — engine must infer the test via getMirrorTestPath
+ const result = engine.selectAffectedTests(["src/utils/widget.ts"]);
+ expect(result.selectedTests).toContain("src/utils/__tests__/widget.test.ts");
+ });
+ });
+
+ // -------------------------------------------------------------------------
+ describe("reload()", () => {
+ it("rebuilds testMap from the new index", () => {
+ const originalIndex = buildPopulatedIndex();
+ const engine = new TestEngine(originalIndex);
+ expect(engine.getStatistics().totalTests).toBe(2);
+
+ // Replace with a smaller index
+ const smallIndex = new GraphIndexManager();
+ smallIndex.addNode("suite:tiny", "TEST_SUITE", {
+ path: "src/__tests__/tiny.test.ts",
+ avgDuration: 10,
+ });
+ engine.reload(smallIndex);
+
+ expect(engine.getStatistics().totalTests).toBe(1);
+ });
+
+ it("accepts an optional projectId without failing", () => {
+ const engine = new TestEngine(new GraphIndexManager());
+ expect(() => engine.reload(new GraphIndexManager(), "my-project")).not.toThrow();
+ });
+ });
+
+ // -------------------------------------------------------------------------
+ describe("categorizeTest() — path-based classification", () => {
+ const CASES: Array<{ path: string; expected: string }> = [
+ {
+ path: "src/utils/__tests__/parser.test.ts",
+ expected: "unit",
+ },
+ {
+ path: "src/services/__tests__/auth.integration.test.ts",
+ expected: "integration",
+ },
+ {
+ path: "tests/integration/db.test.ts",
+ expected: "integration",
+ },
+ {
+ path: "benchmarks/algo_benchmark.test.ts",
+ expected: "performance",
+ },
+ {
+ path: "src/utils/__tests__/sort_bench_test.ts",
+ expected: "performance",
+ },
+ {
+ path: "tests/e2e/signup.test.ts",
+ expected: "e2e",
+ },
+ {
+ path: "tests/end_to_end/checkout.test.ts",
+ expected: "e2e",
+ },
+ ];
+
+ for (const { path: testPath, expected } of CASES) {
+ it(`classifies "${testPath}" as "${expected}"`, () => {
+ const index = new GraphIndexManager();
+ index.addNode(`suite:${expected}`, "TEST_SUITE", {
+ path: testPath,
+ avgDuration: 0,
+ });
+ const engine = new TestEngine(index);
+ const meta = engine
+ .getStatistics();
+
+ if (expected === "unit") expect(meta.unitTests).toBe(1);
+ if (expected === "integration") expect(meta.integrationTests).toBe(1);
+ if (expected === "performance") expect(meta.performanceTests).toBe(1);
+ if (expected === "e2e") expect(meta.e2eTests).toBe(1);
+ });
+ }
+ });
+});
diff --git a/src/engines/architecture-engine.ts b/src/engines/architecture-engine.ts
index c4d1ae1..c0278c9 100644
--- a/src/engines/architecture-engine.ts
+++ b/src/engines/architecture-engine.ts
@@ -1,6 +1,7 @@
/**
- * Architecture Validation Engine
- * Validates code against layer constraints and architectural rules
+ * @file engines/architecture-engine
+ * @description Validates source dependencies against configured architecture layers and rules.
+ * @remarks Supports filesystem scanning and explicit file-list validation modes.
*/
import * as path from "path";
@@ -9,6 +10,7 @@ import { globSync } from "glob";
import type { GraphIndexManager } from "../graph/index.js";
import type { MemgraphClient } from "../graph/client.js";
import type { CypherStatement } from "../graph/types.js";
+import { logger } from "../utils/logger.js";
export interface LayerDefinition {
id: string;
@@ -50,14 +52,29 @@ export interface ValidationResult {
export class ArchitectureEngine {
private layers: Map;
private rules: ArchitectureRule[];
+ private workspaceRoot: string;
+ /** Glob patterns used to discover source files (e.g. for validation/circular-dep scan). */
+ private sourceGlobs: string[];
+ /** Default file extension used when generating suggested paths. */
+ private defaultExtension: string;
constructor(
layers: LayerDefinition[],
rules: ArchitectureRule[],
_index: GraphIndexManager,
+ workspaceRoot?: string,
+ options?: {
+ /** Override the glob patterns used to scan source files. Defaults to ["src/**\/*.{ts,tsx}"]. */
+ sourceGlobs?: string[];
+ /** Default extension for generated file paths. Defaults to ".ts". */
+ defaultExtension?: string;
+ },
) {
this.layers = new Map(layers.map((l) => [l.id, l]));
this.rules = rules;
+ this.workspaceRoot = workspaceRoot ?? process.cwd();
+ this.sourceGlobs = options?.sourceGlobs ?? ["src/**/*.{ts,tsx}"];
+ this.defaultExtension = options?.defaultExtension ?? ".ts";
}
/**
@@ -65,18 +82,27 @@ export class ArchitectureEngine {
*/
async validate(files?: string[]): Promise {
const violations: ValidationViolation[] = [];
- const projectRoot = process.cwd();
+ const projectRoot = this.workspaceRoot;
// Get source files to validate
let filesToCheck: string[];
if (files && files.length > 0) {
filesToCheck = files;
} else {
- // Scan all TS/TSX files in src/
- filesToCheck = globSync("src/**/*.{ts,tsx}", {
- cwd: projectRoot,
- ignore: ["**/node_modules/**", "**/*.test.ts", "**/*.test.tsx"],
- });
+ // Scan source files using configured globs (language-agnostic)
+ filesToCheck = this.sourceGlobs.flatMap((pattern) =>
+ globSync(pattern, {
+ cwd: projectRoot,
+ ignore: [
+ "**/node_modules/**",
+ "**/*.test.*",
+ "**/*.spec.*",
+ "**/test_*.py",
+ "**/*_test.go",
+ "**/*_spec.rb",
+ ],
+ }),
+ );
}
for (const filePath of filesToCheck) {
@@ -89,23 +115,17 @@ export class ArchitectureEngine {
file: filePath,
layer: "unknown",
message: `File not assigned to any layer: ${filePath}`,
- suggestion:
- "Update .lxrag/config.json with appropriate layer path pattern",
+ suggestion: "Update .lxdig/config.json with appropriate layer path pattern",
});
continue;
}
// Extract imports from file
- const imports = this.extractImportsFromFile(
- path.join(projectRoot, filePath),
- );
+ const imports = this.extractImportsFromFile(path.join(projectRoot, filePath));
for (const imp of imports) {
// Skip external imports
- if (
- imp.startsWith("@") ||
- (imp.startsWith(".") === false && !imp.startsWith("src"))
- ) {
+ if (imp.startsWith("@") || (imp.startsWith(".") === false && !imp.startsWith("src"))) {
continue;
}
@@ -129,10 +149,7 @@ export class ArchitectureEngine {
}
// Check forbidden imports
- if (
- layer.cannotImport &&
- this.isForbiddenImport(layer, importedLayer)
- ) {
+ if (layer.cannotImport && this.isForbiddenImport(layer, importedLayer)) {
violations.push({
type: "layer-violation",
severity: "error",
@@ -198,20 +215,66 @@ export class ArchitectureEngine {
}
/**
- * Extract import statements from a source file
+ * Extract import statements from a source file.
+ * Dispatches to language-specific logic based on the file extension.
+ * Supported: TypeScript/JavaScript (.ts, .tsx, .js, .jsx, .mjs, .cjs),
+ * Python (.py), Ruby (.rb), Go (.go).
*/
private extractImportsFromFile(filePath: string): string[] {
+ const ext = path.extname(filePath).toLowerCase();
try {
const content = fs.readFileSync(filePath, "utf-8");
const imports: Set = new Set();
- // Match: import/export from 'path' or "path"
- const importRegex =
- /(?:import|export)\s+(?:[^"']*\s+)?from\s+['"]([^'"]+)['"]/g;
- let match;
-
- while ((match = importRegex.exec(content)) !== null) {
- imports.add(match[1]);
+ if (
+ ext === ".ts" ||
+ ext === ".tsx" ||
+ ext === ".js" ||
+ ext === ".jsx" ||
+ ext === ".mjs" ||
+ ext === ".cjs"
+ ) {
+ // ES module: import/export ... from '...'
+ const importRegex = /(?:import|export)\s+(?:[^"']*\s+)?from\s+['"]([^'"]+)['"]/g;
+ let match;
+ while ((match = importRegex.exec(content)) !== null) {
+ imports.add(match[1]);
+ }
+ } else if (ext === ".py") {
+ // Python: from module.path import ... / import module.path
+ const fromRegex = /^from\s+([\w.]+)\s+import\s+/gm;
+ const importRegex = /^import\s+([\w.]+)/gm;
+ let match;
+ while ((match = fromRegex.exec(content)) !== null) {
+ // Convert dotted module path to slash-separated path
+ imports.add(match[1].replace(/\./g, "/"));
+ }
+ while ((match = importRegex.exec(content)) !== null) {
+ imports.add(match[1].replace(/\./g, "/"));
+ }
+ } else if (ext === ".rb") {
+ // Ruby: require 'path' / require_relative 'path'
+ const reqRegex = /require(?:_relative)?\s+['"]([^'"]+)['"]/g;
+ let match;
+ while ((match = reqRegex.exec(content)) !== null) {
+ imports.add(match[1]);
+ }
+ } else if (ext === ".go") {
+ // Go: import "path" (single or block)
+ const blockRegex = /import\s*\(([\s\S]*?)\)/g;
+ const singleRegex = /import\s+"([^"]+)"/g;
+ let match;
+ while ((match = blockRegex.exec(content)) !== null) {
+ const block = match[1];
+ const lineRegex = /"([^"]+)"/g;
+ let lineMatch;
+ while ((lineMatch = lineRegex.exec(block)) !== null) {
+ imports.add(lineMatch[1]);
+ }
+ }
+ while ((match = singleRegex.exec(content)) !== null) {
+ imports.add(match[1]);
+ }
}
return Array.from(imports);
@@ -231,9 +294,11 @@ export class ArchitectureEngine {
let resolvedPath: string;
if (importPath.startsWith(".")) {
- // Relative import: resolve from importing file's directory
- const dir = path.dirname(fromPath);
- resolvedPath = path.resolve(dir, importPath);
+ // Relative import: resolve from importing file's directory within projectRoot.
+ // path.join(projectRoot, fromPath) gives an absolute base so that
+ // path.resolve() anchors to projectRoot instead of process.cwd().
+ const absoluteFromDir = path.dirname(path.join(projectRoot, fromPath));
+ resolvedPath = path.resolve(absoluteFromDir, importPath);
} else if (importPath.startsWith("src/")) {
// Absolute src import
resolvedPath = importPath;
@@ -248,14 +313,25 @@ export class ArchitectureEngine {
relPath = path.relative(projectRoot, resolvedPath).replace(/\\/g, "/");
}
- // Try different extensions
- const candidates = [
- relPath,
- `${relPath}.ts`,
- `${relPath}.tsx`,
- `${relPath}/index.ts`,
- `${relPath}/index.tsx`,
- ];
+ // Try different extensions based on the source file's language
+ const fromExt = path.extname(fromPath).toLowerCase();
+ let candidates: string[];
+ if (fromExt === ".py") {
+ candidates = [relPath, `${relPath}.py`, `${relPath}/__init__.py`];
+ } else if (fromExt === ".rb") {
+ candidates = [relPath, `${relPath}.rb`];
+ } else if (fromExt === ".go") {
+ candidates = [relPath];
+ } else {
+ // JS/TS (default)
+ candidates = [
+ relPath,
+ `${relPath}.ts`,
+ `${relPath}.tsx`,
+ `${relPath}/index.ts`,
+ `${relPath}/index.tsx`,
+ ];
+ }
for (const candidate of candidates) {
const fullPath = path.join(projectRoot, candidate);
@@ -264,19 +340,17 @@ export class ArchitectureEngine {
}
}
- // Return best guess if file doesn't exist yet
- return relPath.endsWith(".ts") || relPath.endsWith(".tsx")
- ? relPath
- : `${relPath}.ts`;
+ // Return best guess if file doesn't exist yet (use source extension or configured default)
+ const fromExt2 = path.extname(fromPath).toLowerCase();
+ const bestExt = fromExt2 || this.defaultExtension;
+ if (relPath.includes(".")) return relPath;
+ return relPath.endsWith(bestExt) ? relPath : `${relPath}${bestExt}`;
}
/**
* Check if import from one layer to another is allowed
*/
- private isImportAllowed(
- fromLayer: LayerDefinition,
- toLayer: LayerDefinition,
- ): boolean {
+ private isImportAllowed(fromLayer: LayerDefinition, toLayer: LayerDefinition): boolean {
// Can always import from same layer
if (fromLayer.id === toLayer.id) {
return true;
@@ -293,10 +367,7 @@ export class ArchitectureEngine {
/**
* Check if import is explicitly forbidden
*/
- private isForbiddenImport(
- fromLayer: LayerDefinition,
- toLayer: LayerDefinition,
- ): boolean {
+ private isForbiddenImport(fromLayer: LayerDefinition, toLayer: LayerDefinition): boolean {
if (!fromLayer.cannotImport) {
return false;
}
@@ -308,17 +379,26 @@ export class ArchitectureEngine {
*/
private detectCircularDependencies(): ValidationViolation[] {
const violations: ValidationViolation[] = [];
- const projectRoot = process.cwd();
+ const projectRoot = this.workspaceRoot;
const visited = new Set();
const recursionStack = new Set();
const cycles: string[][] = [];
// Build import graph
const importGraph = new Map();
- const sourceFiles = globSync("src/**/*.{ts,tsx}", {
- cwd: projectRoot,
- ignore: ["**/node_modules/**", "**/*.test.ts", "**/*.test.tsx"],
- });
+ const sourceFiles = this.sourceGlobs.flatMap((pattern) =>
+ globSync(pattern, {
+ cwd: projectRoot,
+ ignore: [
+ "**/node_modules/**",
+ "**/*.test.*",
+ "**/*.spec.*",
+ "**/test_*.py",
+ "**/*_test.go",
+ "**/*_spec.rb",
+ ],
+ }),
+ );
for (const file of sourceFiles) {
const imports = this.extractImportsFromFile(path.join(projectRoot, file));
@@ -381,8 +461,7 @@ export class ArchitectureEngine {
file: cycle[0],
layer: this.determineLayer(cycle[0])?.id || "unknown",
message: `Circular dependency detected: ${cycle.slice(0, 3).join(" -> ")}...`,
- suggestion:
- "Break the circular dependency by moving code to a shared utility module",
+ suggestion: "Break the circular dependency by moving code to a shared utility module",
});
}
}
@@ -476,31 +555,34 @@ export class ArchitectureEngine {
};
}
- private getSuggestedPath(
- layer: LayerDefinition,
- codeName: string,
- codeType: string,
- ): string {
+ private getSuggestedPath(layer: LayerDefinition, codeName: string, codeType: string): string {
// Use first path pattern and apply naming convention
const basePattern = layer.paths[0];
const basePath = basePattern.replace("/**", "").replace(/\/\*$/, "");
- let fileName = codeName;
+ let fileName: string;
if (codeType === "component") {
- fileName = codeName.endsWith(".tsx") ? codeName : `${codeName}.tsx`;
+ // Components typically use .tsx (React) — honour configured extension if it differs
+ const compExt = this.defaultExtension === ".tsx" ? ".tsx" : `${this.defaultExtension}`;
+ const hasExt = /\.[^/\\]+$/.test(codeName);
+ fileName = hasExt ? codeName : `${codeName}${compExt}`;
} else if (codeType === "hook") {
fileName = codeName.startsWith("use") ? codeName : `use${codeName}`;
- fileName = fileName.endsWith(".ts") ? fileName : `${fileName}.ts`;
+ const hasExt = /\.[^/\\]+$/.test(fileName);
+ fileName = hasExt ? fileName : `${fileName}${this.defaultExtension}`;
} else if (codeType === "service") {
- // Avoid double-suffix: "GraphDataService" + service → "GraphDataService.ts"
- if (codeName.endsWith("Service.ts") || codeName.endsWith("Service")) {
- fileName = codeName.endsWith(".ts") ? codeName : `${codeName}.ts`;
+ const hasExt = /\.[^/\\]+$/.test(codeName);
+ if (hasExt) {
+ fileName = codeName;
+ } else if (codeName.endsWith("Service")) {
+ fileName = `${codeName}${this.defaultExtension}`;
} else {
- fileName = `${codeName}Service.ts`;
+ fileName = `${codeName}Service${this.defaultExtension}`;
}
} else {
- // Default: ensure .ts extension
- fileName = codeName.endsWith(".ts") ? codeName : `${codeName}.ts`;
+ // Default: ensure configured extension
+ const hasExt = /\.[^/\\]+$/.test(codeName);
+ fileName = hasExt ? codeName : `${codeName}${this.defaultExtension}`;
}
return `${basePath}/${fileName}`;
@@ -513,7 +595,7 @@ export class ArchitectureEngine {
client: MemgraphClient,
violations: ValidationViolation[],
): Promise {
- console.log(`\n📝 Writing ${violations.length} violations to Memgraph...`);
+ logger.error(`\n📝 Writing ${violations.length} violations to Memgraph...`);
const statements: CypherStatement[] = [];
@@ -535,6 +617,11 @@ export class ArchitectureEngine {
// Create FILE nodes and VIOLATES_RULE relationships
for (const violation of violations) {
+ // Resolve to absolute path to match FILE nodes created by the graph builder
+ const absoluteFilePath = path.isAbsolute(violation.file)
+ ? violation.file
+ : path.resolve(this.workspaceRoot, violation.file);
+
// Create or update FILE node
statements.push({
query: `
@@ -542,7 +629,7 @@ export class ArchitectureEngine {
SET f.lastViolationCheck = timestamp()
`,
params: {
- filePath: violation.file,
+ filePath: absoluteFilePath,
},
});
@@ -558,7 +645,7 @@ export class ArchitectureEngine {
SET vr.severity = $severity, vr.message = $message, vr.timestamp = timestamp()
`,
params: {
- filePath: violation.file,
+ filePath: absoluteFilePath,
ruleId: ruleId,
severity: violation.severity,
message: violation.message,
@@ -573,12 +660,10 @@ export class ArchitectureEngine {
// Check for errors
const errors = results.filter((r) => r.error);
if (errors.length > 0) {
- console.error(`⚠️ ${errors.length} Cypher statements failed:`);
- errors.slice(0, 3).forEach((e) => console.error(` - ${e.error}`));
+ logger.error(`⚠️ ${errors.length} Cypher statements failed:`);
+ errors.slice(0, 3).forEach((e) => logger.error(` - ${e.error}`));
} else {
- console.log(
- `✅ Successfully wrote ${violations.length} violations to graph`,
- );
+ logger.error(`✅ Successfully wrote ${violations.length} violations to graph`);
}
}
@@ -586,11 +671,12 @@ export class ArchitectureEngine {
* Reload engine state from updated graph index
* Called when project context changes
*/
- reload(_index: GraphIndexManager, projectId?: string): void {
- console.log(
- `[ArchitectureEngine] Reloading architecture validation (projectId=${projectId})`,
- );
- // ArchitectureEngine doesn't hold project-specific state in index
+ reload(_index: GraphIndexManager, projectId?: string, workspaceRoot?: string): void {
+ logger.error(`[ArchitectureEngine] Reloading architecture validation (projectId=${projectId})`);
+ if (workspaceRoot) {
+ this.workspaceRoot = workspaceRoot;
+ }
+ // ArchitectureEngine doesn't hold other project-specific state in index
// so reload is mainly for consistency with other engines
}
diff --git a/src/engines/community-detector.ts b/src/engines/community-detector.ts
index e9c9fa8..8a29a88 100644
--- a/src/engines/community-detector.ts
+++ b/src/engines/community-detector.ts
@@ -1,4 +1,11 @@
+/**
+ * @file engines/community-detector
+ * @description Builds code communities from graph relationships for higher-level context retrieval.
+ * @remarks Persists detected communities in Memgraph for query-time use.
+ */
+
import type MemgraphClient from "../graph/client.js";
+import { logger } from "../utils/logger.js";
interface CommunityMember {
id: string;
@@ -25,9 +32,10 @@ export default class CommunityDetector {
`MATCH (n)
WHERE n.projectId = $projectId
AND (n:FILE OR n:FUNCTION OR n:CLASS)
+ OPTIONAL MATCH (parentFile:FILE)-[:CONTAINS]->(n)
RETURN n.id AS id,
labels(n)[0] AS type,
- coalesce(n.path, n.filePath, '') AS filePath,
+ coalesce(n.path, n.filePath, parentFile.path, '') AS filePath,
coalesce(n.name, n.id) AS name`,
{ projectId },
);
@@ -75,11 +83,7 @@ export default class CommunityDetector {
{ projectId },
);
- if (
- response.error ||
- !Array.isArray(response.data) ||
- response.data.length === 0
- ) {
+ if (response.error || !Array.isArray(response.data) || response.data.length === 0) {
return null;
}
@@ -105,7 +109,7 @@ export default class CommunityDetector {
}
await this.writeCommunities(projectId, grouped, "leiden");
- console.error(
+ logger.error(
`[community] MAGE Leiden: ${grouped.size} communities across ${communityMap.size} member node(s) for project ${projectId}`,
);
return {
@@ -142,7 +146,7 @@ export default class CommunityDetector {
}
await this.writeCommunities(projectId, numericGrouped, "dir");
- console.error(
+ logger.error(
`[community] directory heuristic: ${grouped.size} communities across ${members.length} member node(s) for project ${projectId}`,
);
return {
@@ -160,7 +164,6 @@ export default class CommunityDetector {
grouped: Map,
prefix: string,
): Promise {
- let idx = 0;
for (const [cid, group] of grouped.entries()) {
const communityId = `${projectId}::community::${prefix}::${cid}`;
const label = this.labelForGroup(group);
@@ -172,6 +175,7 @@ export default class CommunityDetector {
SET c.label = $label,
c.summary = $summary,
c.memberCount = $memberCount,
+ c.size = $memberCount,
c.centralNode = $centralNode,
c.computedAt = $computedAt`,
{
@@ -194,8 +198,6 @@ export default class CommunityDetector {
{ nodeId: member.id, projectId, communityId },
);
}
-
- idx += 1;
}
}
@@ -209,9 +211,28 @@ export default class CommunityDetector {
private communityLabel(filePath: string): string {
const segments = filePath.split("/").filter(Boolean);
- const ignored = new Set(["src", "lib", "dist", "build", "node_modules"]);
- const segment = segments.find((item) => !ignored.has(item));
- return segment || "misc";
+ // Look for a well-known source-root marker and return the directory that follows it.
+ // This correctly handles absolute paths like /home/user/project/src/engines/foo.ts
+ // by returning "engines" instead of "home".
+ const sourceRoots = new Set([
+ "src",
+ "lib",
+ "app",
+ "pages",
+ "packages",
+ "components",
+ "services",
+ ]);
+ const rootIdx = segments.findIndex((s) => sourceRoots.has(s));
+ if (rootIdx >= 0 && rootIdx + 1 < segments.length) {
+ const next = segments[rootIdx + 1];
+ // If next segment is a filename (has extension), use the root marker itself
+ return next.includes(".") ? segments[rootIdx] : next;
+ }
+ // Fallback: use the last non-trivial directory segment before the filename
+ const dirSegments = segments.slice(0, -1);
+ const trivial = new Set(["home", "root", "usr", "var", "tmp", "opt"]);
+ return dirSegments.filter((s) => !trivial.has(s)).pop() || "misc";
}
private centralNode(group: CommunityMember[]): string {
diff --git a/src/engines/coordination-engine.ts b/src/engines/coordination-engine.ts
index 432a058..91182fe 100644
--- a/src/engines/coordination-engine.ts
+++ b/src/engines/coordination-engine.ts
@@ -1,91 +1,45 @@
-import type MemgraphClient from "../graph/client.js";
-
-export type ClaimType = "task" | "file" | "function" | "feature";
-export type InvalidationReason =
- | "released"
- | "code_changed"
- | "task_completed"
- | "expired";
-
-export interface AgentClaim {
- id: string;
- agentId: string;
- sessionId: string;
- taskId?: string;
- claimType: ClaimType;
- targetId: string;
- intent: string;
- validFrom: number;
- targetVersionSHA?: string;
- validTo: number | null;
- invalidationReason?: InvalidationReason;
- outcome?: string;
- projectId: string;
-}
+/**
+ * @file engines/coordination-engine
+ * @description Manages agent claim lifecycle, conflict detection, and fleet coordination state.
+ * @remarks Uses extracted query/constants and pure utilities for maintainability.
+ */
-export interface ClaimInput {
- agentId: string;
- sessionId: string;
- projectId: string;
- targetId: string;
- claimType: ClaimType;
- intent: string;
- taskId?: string;
-}
-
-export interface ClaimResult {
- claimId: string;
- status: "ok" | "CONFLICT";
- conflict?: { agentId: string; intent: string; since: number };
- targetVersionSHA: string;
-}
-
-export interface AgentStatus {
- agentId: string;
- activeClaims: AgentClaim[];
- recentEpisodes: Array<{
- id: string;
- type: string;
- content: string;
- timestamp: number;
- taskId?: string;
- }>;
- currentTask?: string;
-}
-
-export interface CoordinationOverview {
- activeClaims: AgentClaim[];
- staleClaims: AgentClaim[];
- conflicts: Array<{
- targetId: string;
- claimA: { claimId: string; agentId: string; intent: string; since: number };
- claimB: { claimId: string; agentId: string; intent: string; since: number };
- }>;
- agentSummary: Array<{
- agentId: string;
- claimCount: number;
- lastSeen: number;
- }>;
- totalClaims: number;
-}
+import type MemgraphClient from "../graph/client.js";
+import { CoordinationQueries as Q } from "./coordination-queries.js";
+import { makeClaimId, rowToClaim } from "./coordination-utils.js";
+
+// Re-export all public types so existing importers keep working.
+export type {
+ AgentClaim,
+ AgentStatus,
+ ClaimInput,
+ ClaimResult,
+ ClaimType,
+ CoordinationOverview,
+ InvalidationReason,
+ ReleaseFeedback,
+} from "./coordination-types.js";
+
+import type {
+ AgentClaim,
+ AgentStatus,
+ ClaimInput,
+ ClaimResult,
+ CoordinationOverview,
+ ReleaseFeedback,
+} from "./coordination-types.js";
export default class CoordinationEngine {
constructor(private memgraph: MemgraphClient) {}
+ // ── Public API ─────────────────────────────────────────────────────────────
+
async claim(input: ClaimInput): Promise {
- const conflictCheck = await this.memgraph.executeCypher(
- `MATCH (c:CLAIM)-[:TARGETS]->(t {id: $targetId, projectId: $projectId})
- WHERE c.validTo IS NULL
- AND c.agentId <> $agentId
- RETURN c.id AS claimId, c.agentId AS agentId, c.intent AS intent, c.validFrom AS since
- ORDER BY c.validFrom DESC
- LIMIT 1`,
- {
- targetId: input.targetId,
- projectId: input.projectId,
- agentId: input.agentId,
- },
- );
+ const conflictCheck = await this.memgraph.executeCypher(Q.CONFLICT_CHECK, {
+ targetId: input.targetId,
+ projectId: input.projectId,
+ agentId: input.agentId,
+ });
const conflict = conflictCheck.data?.[0];
if (conflict) {
@@ -102,53 +56,28 @@ export default class CoordinationEngine {
}
const now = Date.now();
- const claimId = this.makeId("claim");
- const targetSnapshot = await this.getTargetSnapshot(
- input.targetId,
- input.projectId,
- );
+ const claimId = makeClaimId("claim", now);
+ const targetSnapshot = await this.getTargetSnapshot(input.targetId, input.projectId);
+
+ await this.memgraph.executeCypher(Q.CREATE_CLAIM, {
+ id: claimId,
+ agentId: input.agentId,
+ sessionId: input.sessionId,
+ taskId: input.taskId || null,
+ claimType: input.claimType,
+ targetId: input.targetId,
+ intent: input.intent,
+ validFrom: now,
+ targetVersionSHA: targetSnapshot.targetVersionSHA,
+ projectId: input.projectId,
+ });
- await this.memgraph.executeCypher(
- `CREATE (c:CLAIM {
- id: $id,
- agentId: $agentId,
- sessionId: $sessionId,
- taskId: $taskId,
- claimType: $claimType,
- targetId: $targetId,
- intent: $intent,
- validFrom: $validFrom,
- targetVersionSHA: $targetVersionSHA,
- validTo: null,
- invalidationReason: null,
- outcome: null,
- projectId: $projectId
- })`,
- {
- id: claimId,
- agentId: input.agentId,
- sessionId: input.sessionId,
- taskId: input.taskId || null,
- claimType: input.claimType,
+ if (targetSnapshot.targetExists) {
+ await this.memgraph.executeCypher(Q.LINK_CLAIM_TO_TARGET, {
+ claimId,
targetId: input.targetId,
- intent: input.intent,
- validFrom: now,
- targetVersionSHA: targetSnapshot.targetVersionSHA,
projectId: input.projectId,
- },
- );
-
- if (targetSnapshot.targetExists) {
- await this.memgraph.executeCypher(
- `MATCH (c:CLAIM {id: $claimId, projectId: $projectId})
- MATCH (t {id: $targetId, projectId: $projectId})
- MERGE (c)-[:TARGETS]->(t)`,
- {
- claimId,
- targetId: input.targetId,
- projectId: input.projectId,
- },
- );
+ });
}
return {
@@ -158,44 +87,47 @@ export default class CoordinationEngine {
};
}
- async release(claimId: string, outcome?: string): Promise {
- await this.memgraph.executeCypher(
- `MATCH (c:CLAIM {id: $claimId})
- WHERE c.validTo IS NULL
- SET c.validTo = $now,
- c.invalidationReason = 'released',
- c.outcome = $outcome`,
- {
- claimId,
- now: Date.now(),
- outcome: outcome || null,
- },
- );
+ /**
+ * Close a claim. Returns feedback indicating whether the claim was found
+ * and whether it was already closed before this call — instead of silently
+ * returning void.
+ */
+ async release(claimId: string, outcome?: string): Promise {
+ // First check current state so we can give accurate feedback.
+ const checkResult = await this.memgraph.executeCypher(Q.RELEASE_CLAIM_OPEN_CHECK, { claimId });
+
+ if (!checkResult.data.length) {
+ return { found: false, alreadyClosed: false };
+ }
+
+ const row = checkResult.data[0] as Record;
+ if (row.validTo != null) {
+ return { found: true, alreadyClosed: true };
+ }
+
+ await this.memgraph.executeCypher(Q.RELEASE_CLAIM, {
+ claimId,
+ now: Date.now(),
+ outcome: outcome ?? null,
+ });
+
+ return { found: true, alreadyClosed: false };
}
async status(agentId: string, projectId: string): Promise {
- const claimsResult = await this.memgraph.executeCypher(
- `MATCH (c:CLAIM)
- WHERE c.projectId = $projectId
- AND c.agentId = $agentId
- AND c.validTo IS NULL
- RETURN c
- ORDER BY c.validFrom DESC`,
- { projectId, agentId },
- );
-
- const episodesResult = await this.memgraph.executeCypher(
- `MATCH (e:EPISODE)
- WHERE e.projectId = $projectId
- AND e.agentId = $agentId
- RETURN e.id AS id, e.type AS type, e.content AS content, e.timestamp AS timestamp, e.taskId AS taskId
- ORDER BY e.timestamp DESC
- LIMIT 10`,
- { projectId, agentId },
- );
+ const [claimsResult, episodesResult] = await Promise.all([
+ this.memgraph.executeCypher(Q.AGENT_ACTIVE_CLAIMS, {
+ projectId,
+ agentId,
+ }),
+ this.memgraph.executeCypher(Q.AGENT_RECENT_EPISODES, {
+ projectId,
+ agentId,
+ }),
+ ]);
const activeClaims = claimsResult.data
- .map((row) => this.rowToClaim(row))
+ .map((row) => rowToClaim(row))
.filter((row): row is AgentClaim => Boolean(row));
return {
@@ -213,69 +145,21 @@ export default class CoordinationEngine {
}
async overview(projectId: string): Promise {
- const [
- activeResult,
- staleResult,
- conflictsResult,
- summaryResult,
- totalResult,
- ] = await Promise.all([
- this.memgraph.executeCypher(
- `MATCH (c:CLAIM)
- WHERE c.projectId = $projectId
- AND c.validTo IS NULL
- RETURN c
- ORDER BY c.validFrom DESC`,
- { projectId },
- ),
- this.memgraph.executeCypher(
- `MATCH (c:CLAIM)-[:TARGETS]->(t)
- WHERE c.projectId = $projectId
- AND c.validTo IS NULL
- AND t.projectId = $projectId
- AND t.validFrom > c.validFrom
- RETURN c
- ORDER BY c.validFrom DESC`,
- { projectId },
- ),
- this.memgraph.executeCypher(
- `MATCH (c1:CLAIM)-[:TARGETS]->(t)<-[:TARGETS]-(c2:CLAIM)
- WHERE c1.projectId = $projectId
- AND c2.projectId = $projectId
- AND c1.validTo IS NULL
- AND c2.validTo IS NULL
- AND c1.id < c2.id
- AND c1.agentId <> c2.agentId
- RETURN t.id AS targetId,
- c1.id AS claimAId, c1.agentId AS claimAAgent, c1.intent AS claimAIntent, c1.validFrom AS claimASince,
- c2.id AS claimBId, c2.agentId AS claimBAgent, c2.intent AS claimBIntent, c2.validFrom AS claimBSince
- ORDER BY targetId`,
- { projectId },
- ),
- this.memgraph.executeCypher(
- `MATCH (c:CLAIM)
- WHERE c.projectId = $projectId
- AND c.validTo IS NULL
- RETURN c.agentId AS agentId,
- count(c) AS claimCount,
- max(c.validFrom) AS lastSeen
- ORDER BY claimCount DESC, lastSeen DESC`,
- { projectId },
- ),
- this.memgraph.executeCypher(
- `MATCH (c:CLAIM)
- WHERE c.projectId = $projectId
- RETURN count(c) AS totalClaims`,
- { projectId },
- ),
- ]);
+ const [activeResult, staleResult, conflictsResult, summaryResult, totalResult] =
+ await Promise.all([
+ this.memgraph.executeCypher(Q.OVERVIEW_ACTIVE, { projectId }),
+ this.memgraph.executeCypher(Q.OVERVIEW_STALE, { projectId }),
+ this.memgraph.executeCypher(Q.OVERVIEW_CONFLICTS, { projectId }),
+ this.memgraph.executeCypher(Q.OVERVIEW_AGENT_SUMMARY, { projectId }),
+ this.memgraph.executeCypher(Q.OVERVIEW_TOTAL, { projectId }),
+ ]);
return {
activeClaims: activeResult.data
- .map((row) => this.rowToClaim(row))
+ .map((row) => rowToClaim(row))
.filter((row): row is AgentClaim => Boolean(row)),
staleClaims: staleResult.data
- .map((row) => this.rowToClaim(row))
+ .map((row) => rowToClaim(row))
.filter((row): row is AgentClaim => Boolean(row)),
conflicts: conflictsResult.data.map((row) => ({
targetId: String(row.targetId || "unknown"),
@@ -303,57 +187,48 @@ export default class CoordinationEngine {
async invalidateStaleClaims(projectId: string): Promise {
const now = Date.now();
- const staleResult = await this.memgraph.executeCypher(
- `MATCH (c:CLAIM)-[:TARGETS]->(t)
- WHERE c.projectId = $projectId
- AND c.validTo IS NULL
- AND t.projectId = $projectId
- AND t.validFrom > c.validFrom
- SET c.validTo = $now,
- c.invalidationReason = 'code_changed'
- RETURN count(c) AS invalidated`,
- { projectId, now },
- );
-
+ const staleResult = await this.memgraph.executeCypher(Q.INVALIDATE_STALE, {
+ projectId,
+ now,
+ });
return Number(staleResult.data?.[0]?.invalidated || 0);
}
- async onTaskCompleted(
- taskId: string,
- agentId: string,
- projectId: string,
- ): Promise {
- await this.memgraph.executeCypher(
- `MATCH (c:CLAIM)
- WHERE c.projectId = $projectId
- AND c.taskId = $taskId
- AND c.validTo IS NULL
- SET c.validTo = $now,
- c.invalidationReason = 'task_completed',
- c.outcome = coalesce(c.outcome, $outcome)`,
- {
- projectId,
- taskId,
- now: Date.now(),
- outcome: `Task completed by ${agentId}`,
- },
- );
+ async onTaskCompleted(taskId: string, agentId: string, projectId: string): Promise {
+ await this.memgraph.executeCypher(Q.ON_TASK_COMPLETED, {
+ projectId,
+ taskId,
+ now: Date.now(),
+ outcome: `Task completed by ${agentId}`,
+ });
+ }
+
+ /**
+ * Expire all open claims older than `maxAgeMs` milliseconds.
+ * Implements the previously orphaned 'expired' InvalidationReason.
+ * @returns number of claims closed
+ */
+ async expireOldClaims(projectId: string, maxAgeMs: number): Promise {
+ const now = Date.now();
+ const cutoffMs = now - maxAgeMs;
+ const result = await this.memgraph.executeCypher(Q.EXPIRE_OLD_CLAIMS, {
+ projectId,
+ now,
+ cutoffMs,
+ });
+ return Number(result.data?.[0]?.expired || 0);
}
+ // ── Private helpers ────────────────────────────────────────────────────────
+
private async getTargetSnapshot(
targetId: string,
projectId: string,
): Promise<{ targetExists: boolean; targetVersionSHA: string }> {
- const result = await this.memgraph.executeCypher(
- `MATCH (t {id: $targetId, projectId: $projectId})
- RETURN t.validFrom AS validFrom,
- t.contentHash AS contentHash,
- t.hash AS hash,
- t.gitCommit AS gitCommit
- ORDER BY t.validFrom DESC
- LIMIT 1`,
- { targetId, projectId },
- );
+ const result = await this.memgraph.executeCypher(Q.TARGET_SNAPSHOT, {
+ targetId,
+ projectId,
+ });
if (!result.data.length) {
return { targetExists: false, targetVersionSHA: `unknown-${Date.now()}` };
@@ -361,49 +236,11 @@ export default class CoordinationEngine {
const row = result.data[0] || {};
const sha =
- row.contentHash ||
- row.hash ||
- row.gitCommit ||
- `vf-${String(row.validFrom || Date.now())}`;
+ row.contentHash || row.hash || row.gitCommit || `vf-${String(row.validFrom || Date.now())}`;
return {
targetExists: true,
targetVersionSHA: String(sha),
};
}
-
- private rowToClaim(row: Record): AgentClaim | null {
- const claim =
- (row.c as Record) ||
- (row.claim as Record) ||
- row;
-
- if (!claim || typeof claim !== "object" || !claim.id) {
- return null;
- }
-
- return {
- id: String(claim.id),
- agentId: String(claim.agentId || "unknown"),
- sessionId: String(claim.sessionId || "unknown"),
- taskId: claim.taskId ? String(claim.taskId) : undefined,
- claimType: (claim.claimType || "task") as ClaimType,
- targetId: String(claim.targetId || ""),
- intent: String(claim.intent || ""),
- validFrom: Number(claim.validFrom || Date.now()),
- targetVersionSHA: claim.targetVersionSHA
- ? String(claim.targetVersionSHA)
- : undefined,
- validTo: claim.validTo == null ? null : Number(claim.validTo),
- invalidationReason: claim.invalidationReason
- ? (String(claim.invalidationReason) as InvalidationReason)
- : undefined,
- outcome: claim.outcome ? String(claim.outcome) : undefined,
- projectId: String(claim.projectId || "unknown"),
- };
- }
-
- private makeId(prefix: string): string {
- return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
- }
}
diff --git a/src/engines/coordination-queries.ts b/src/engines/coordination-queries.ts
new file mode 100644
index 0000000..6d056a8
--- /dev/null
+++ b/src/engines/coordination-queries.ts
@@ -0,0 +1,161 @@
+/**
+ * @file engines/coordination-queries
+ * @description Shared Cypher query constants consumed by the coordination engine.
+ * @remarks Isolating queries improves readability, testability, and optimization review.
+ */
+
+export const CoordinationQueries = {
+ /** Check for an active conflicting claim on the same target from a *different* agent */
+ CONFLICT_CHECK: `
+ MATCH (c:CLAIM)-[:TARGETS]->(t {id: $targetId, projectId: $projectId})
+ WHERE c.validTo IS NULL
+ AND c.agentId <> $agentId
+ RETURN c.id AS claimId, c.agentId AS agentId, c.intent AS intent, c.validFrom AS since
+ ORDER BY c.validFrom DESC
+ LIMIT 1`,
+
+ /** Look up snapshot info (hash/commit) for a target node */
+ TARGET_SNAPSHOT: `
+ MATCH (t {id: $targetId, projectId: $projectId})
+ RETURN t.validFrom AS validFrom,
+ t.contentHash AS contentHash,
+ t.hash AS hash,
+ t.gitCommit AS gitCommit
+ ORDER BY t.validFrom DESC
+ LIMIT 1`,
+
+ /** Create a new CLAIM node */
+ CREATE_CLAIM: `
+ CREATE (c:CLAIM {
+ id: $id,
+ agentId: $agentId,
+ sessionId: $sessionId,
+ taskId: $taskId,
+ claimType: $claimType,
+ targetId: $targetId,
+ intent: $intent,
+ validFrom: $validFrom,
+ targetVersionSHA: $targetVersionSHA,
+ validTo: null,
+ invalidationReason: null,
+ outcome: null,
+ projectId: $projectId
+ })`,
+
+ /** Create TARGETS edge from a claim to its target node */
+ LINK_CLAIM_TO_TARGET: `
+ MATCH (c:CLAIM {id: $claimId, projectId: $projectId})
+ MATCH (t {id: $targetId, projectId: $projectId})
+ MERGE (c)-[:TARGETS]->(t)`,
+
+ /** Close (release) a claim — checks it is still open first */
+ RELEASE_CLAIM_OPEN_CHECK: `
+ MATCH (c:CLAIM {id: $claimId})
+ RETURN c.validTo AS validTo, c.id AS id`,
+
+ /** Actually close the claim */
+ RELEASE_CLAIM: `
+ MATCH (c:CLAIM {id: $claimId})
+ WHERE c.validTo IS NULL
+ SET c.validTo = $now,
+ c.invalidationReason = 'released',
+ c.outcome = $outcome`,
+
+ /** Active claims for a single agent */
+ AGENT_ACTIVE_CLAIMS: `
+ MATCH (c:CLAIM)
+ WHERE c.projectId = $projectId
+ AND c.agentId = $agentId
+ AND c.validTo IS NULL
+ RETURN c
+ ORDER BY c.validFrom DESC`,
+
+ /** Recent episodes for a single agent */
+ AGENT_RECENT_EPISODES: `
+ MATCH (e:EPISODE)
+ WHERE e.projectId = $projectId
+ AND e.agentId = $agentId
+ RETURN e.id AS id, e.type AS type, e.content AS content,
+ e.timestamp AS timestamp, e.taskId AS taskId
+ ORDER BY e.timestamp DESC
+ LIMIT 10`,
+
+ /** All active claims in a project */
+ OVERVIEW_ACTIVE: `
+ MATCH (c:CLAIM)
+ WHERE c.projectId = $projectId
+ AND c.validTo IS NULL
+ RETURN c
+ ORDER BY c.validFrom DESC`,
+
+ /** Stale claims — target node has been updated since the claim was created */
+ OVERVIEW_STALE: `
+ MATCH (c:CLAIM)-[:TARGETS]->(t)
+ WHERE c.projectId = $projectId
+ AND c.validTo IS NULL
+ AND t.projectId = $projectId
+ AND t.validFrom > c.validFrom
+ RETURN c
+ ORDER BY c.validFrom DESC`,
+
+ /** Conflicting claim pairs — two open claims on the same target from different agents */
+ OVERVIEW_CONFLICTS: `
+ MATCH (c1:CLAIM)-[:TARGETS]->(t)<-[:TARGETS]-(c2:CLAIM)
+ WHERE c1.projectId = $projectId
+ AND c2.projectId = $projectId
+ AND c1.validTo IS NULL
+ AND c2.validTo IS NULL
+ AND c1.id < c2.id
+ AND c1.agentId <> c2.agentId
+ RETURN t.id AS targetId,
+ c1.id AS claimAId, c1.agentId AS claimAAgent, c1.intent AS claimAIntent, c1.validFrom AS claimASince,
+ c2.id AS claimBId, c2.agentId AS claimBAgent, c2.intent AS claimBIntent, c2.validFrom AS claimBSince
+ ORDER BY targetId`,
+
+ /** Agent-level summary (claim counts + last seen) */
+ OVERVIEW_AGENT_SUMMARY: `
+ MATCH (c:CLAIM)
+ WHERE c.projectId = $projectId
+ AND c.validTo IS NULL
+ RETURN c.agentId AS agentId,
+ count(c) AS claimCount,
+ max(c.validFrom) AS lastSeen
+ ORDER BY claimCount DESC, lastSeen DESC`,
+
+ /** Total claim count for a project */
+ OVERVIEW_TOTAL: `
+ MATCH (c:CLAIM)
+ WHERE c.projectId = $projectId
+ RETURN count(c) AS totalClaims`,
+
+ /** Invalidate stale claims whose target node has been updated */
+ INVALIDATE_STALE: `
+ MATCH (c:CLAIM)-[:TARGETS]->(t)
+ WHERE c.projectId = $projectId
+ AND c.validTo IS NULL
+ AND t.projectId = $projectId
+ AND t.validFrom > c.validFrom
+ SET c.validTo = $now,
+ c.invalidationReason = 'code_changed'
+ RETURN count(c) AS invalidated`,
+
+ /** Close all open claims for a completed task */
+ ON_TASK_COMPLETED: `
+ MATCH (c:CLAIM)
+ WHERE c.projectId = $projectId
+ AND c.taskId = $taskId
+ AND c.validTo IS NULL
+ SET c.validTo = $now,
+ c.invalidationReason = 'task_completed',
+ c.outcome = coalesce(c.outcome, $outcome)`,
+
+ /** Expire claims older than a given timestamp (TTL enforcement) */
+ EXPIRE_OLD_CLAIMS: `
+ MATCH (c:CLAIM)
+ WHERE c.projectId = $projectId
+ AND c.validTo IS NULL
+ AND c.validFrom < $cutoffMs
+ SET c.validTo = $now,
+ c.invalidationReason = 'expired'
+ RETURN count(c) AS expired`,
+} as const;
diff --git a/src/engines/coordination-types.ts b/src/engines/coordination-types.ts
new file mode 100644
index 0000000..04299cf
--- /dev/null
+++ b/src/engines/coordination-types.ts
@@ -0,0 +1,79 @@
+/**
+ * @file engines/coordination-types
+ * @description Public type contracts for coordination workflows.
+ * @remarks Kept separate so callers can import types without importing engine runtime code.
+ */
+
+export type ClaimType = "task" | "file" | "function" | "feature";
+
+export type InvalidationReason = "released" | "code_changed" | "task_completed" | "expired";
+
+export interface AgentClaim {
+ id: string;
+ agentId: string;
+ sessionId: string;
+ taskId?: string;
+ claimType: ClaimType;
+ targetId: string;
+ intent: string;
+ validFrom: number;
+ targetVersionSHA?: string;
+ validTo: number | null;
+ invalidationReason?: InvalidationReason;
+ outcome?: string;
+ projectId: string;
+}
+
+export interface ClaimInput {
+ agentId: string;
+ sessionId: string;
+ projectId: string;
+ targetId: string;
+ claimType: ClaimType;
+ intent: string;
+ taskId?: string;
+}
+
+export interface ClaimResult {
+ claimId: string;
+ status: "ok" | "CONFLICT";
+ conflict?: { agentId: string; intent: string; since: number };
+ targetVersionSHA: string;
+}
+
+/** Typed result for the release() method — replaces the original void return. */
+export interface ReleaseFeedback {
+ /** true if the claim existed and was open when release was called */
+ found: boolean;
+ /** true if the claim existed but was already closed before this call */
+ alreadyClosed: boolean;
+}
+
+export interface AgentStatus {
+ agentId: string;
+ activeClaims: AgentClaim[];
+ recentEpisodes: Array<{
+ id: string;
+ type: string;
+ content: string;
+ timestamp: number;
+ taskId?: string;
+ }>;
+ currentTask?: string;
+}
+
+export interface CoordinationOverview {
+ activeClaims: AgentClaim[];
+ staleClaims: AgentClaim[];
+ conflicts: Array<{
+ targetId: string;
+ claimA: { claimId: string; agentId: string; intent: string; since: number };
+ claimB: { claimId: string; agentId: string; intent: string; since: number };
+ }>;
+ agentSummary: Array<{
+ agentId: string;
+ claimCount: number;
+ lastSeen: number;
+ }>;
+ totalClaims: number;
+}
diff --git a/src/engines/coordination-utils.ts b/src/engines/coordination-utils.ts
new file mode 100644
index 0000000..894dcd6
--- /dev/null
+++ b/src/engines/coordination-utils.ts
@@ -0,0 +1,47 @@
+/**
+ * @file engines/coordination-utils
+ * @description Pure helper functions for coordination IDs, mapping, and normalization.
+ * @remarks Utility functions are side-effect free and independently testable.
+ */
+
+import type { AgentClaim, ClaimType, InvalidationReason } from "./coordination-types.js";
+
+/**
+ * Maps a raw Memgraph row (or the nested `c` property) to an AgentClaim.
+ * Returns null if the row lacks a required `id` field.
+ */
+export function rowToClaim(row: Record): AgentClaim | null {
+ const claim = (row.c as Record) || (row.claim as Record) || row;
+
+ if (!claim || typeof claim !== "object" || !claim.id) {
+ return null;
+ }
+
+ return {
+ id: String(claim.id),
+ agentId: String(claim.agentId ?? "unknown"),
+ sessionId: String(claim.sessionId ?? "unknown"),
+ taskId: claim.taskId ? String(claim.taskId) : undefined,
+ claimType: (claim.claimType ?? "task") as ClaimType,
+ targetId: String(claim.targetId ?? ""),
+ intent: String(claim.intent ?? ""),
+ validFrom: Number(claim.validFrom ?? Date.now()),
+ targetVersionSHA: claim.targetVersionSHA ? String(claim.targetVersionSHA) : undefined,
+ validTo: claim.validTo == null ? null : Number(claim.validTo),
+ invalidationReason: claim.invalidationReason
+ ? (String(claim.invalidationReason) as InvalidationReason)
+ : undefined,
+ outcome: claim.outcome ? String(claim.outcome) : undefined,
+ projectId: String(claim.projectId ?? "unknown"),
+ };
+}
+
+/**
+ * Generate a time-prefixed pseudo-unique ID.
+ * @param prefix e.g. "claim"
+ * @param now injectable timestamp (ms) — defaults to Date.now(); pass a
+ * fixed value in tests to get deterministic IDs.
+ */
+export function makeClaimId(prefix: string, now: number = Date.now()): string {
+ return `${prefix}-${now}-${Math.random().toString(36).slice(2, 10)}`;
+}
diff --git a/src/engines/docs-engine.ts b/src/engines/docs-engine.ts
index ef718ac..2abbd92 100644
--- a/src/engines/docs-engine.ts
+++ b/src/engines/docs-engine.ts
@@ -1,7 +1,7 @@
/**
- * Docs Engine
- * Orchestrates markdown file discovery, parsing, and graph indexing.
- * Supports incremental updates (hash-based), vector embedding, and search.
+ * @file engines/docs-engine
+ * @description Indexes markdown docs into graph sections and supports documentation search.
+ * @remarks Supports incremental hashing and optional Qdrant embeddings.
*/
import type { MemgraphClient } from "../graph/client.js";
@@ -9,6 +9,7 @@ import type { QdrantClient, VectorPoint } from "../vector/qdrant-client.js";
import { DocsBuilder } from "../graph/docs-builder.js";
import { DocsParser, findMarkdownFiles } from "../parsers/docs-parser.js";
import type { ParsedDoc } from "../parsers/docs-parser.js";
+import { logger } from "../utils/logger.js";
// ─── Public types ─────────────────────────────────────────────────────────────
@@ -94,8 +95,9 @@ export class DocsEngine {
): Promise {
const t0 = Date.now();
const incremental = opts.incremental ?? true;
- // Phase 3.2: Enable doc embeddings by default
- const withEmbeddings = opts.withEmbeddings ?? true;
+ // withEmbeddings defaults to false — callers that want Qdrant embedding must opt in explicitly.
+ // (The orchestrator path does not supply a Qdrant client and must not attempt to embed.)
+ const withEmbeddings = opts.withEmbeddings ?? false;
const txId = opts.txId ?? `doc-tx-${Date.now()}`;
const files = findMarkdownFiles(workspaceRoot);
@@ -137,11 +139,9 @@ export class DocsEngine {
if (withEmbeddings && this.qdrant?.isConnected()) {
try {
await this.embedDoc(doc, projectId);
- console.log(
- `[Phase3.2] Generated embeddings for documentation: ${doc.relativePath}`,
- );
+ logger.error(`[Phase3.2] Generated embeddings for documentation: ${doc.relativePath}`);
} catch (embeddingError) {
- console.error(
+ logger.error(
`[Phase3.2] Failed to embed documentation ${doc.relativePath}:`,
embeddingError,
);
@@ -212,22 +212,18 @@ RETURN s.id AS sectionId,
s.startLine AS startLine,
r.strength AS score
ORDER BY score DESC
-LIMIT $limit
+LIMIT ${limit}
`,
- { projectId, name: symbolName, limit },
+ { projectId, name: symbolName },
);
if (res.error || !res.data.length) return [];
- return res.data.map((row: Record) =>
- this.rowToResult(row),
- );
+ return res.data.map((row: Record) => this.rowToResult(row));
}
// ── Private helpers ──────────────────────────────────────────────────────────
- private async fetchExistingHashes(
- projectId: string,
- ): Promise> {
+ private async fetchExistingHashes(projectId: string): Promise> {
const res = await this.memgraph.executeCypher(
`MATCH (d:DOCUMENT { projectId: $projectId })
RETURN d.relativePath AS relativePath, d.hash AS hash`,
@@ -239,10 +235,7 @@ LIMIT $limit
relativePath: unknown;
hash: unknown;
}>) {
- if (
- typeof row.relativePath === "string" &&
- typeof row.hash === "string"
- ) {
+ if (typeof row.relativePath === "string" && typeof row.hash === "string") {
map.set(row.relativePath, row.hash);
}
}
@@ -270,14 +263,12 @@ RETURN node.id AS sectionId,
node.startLine AS startLine,
score
ORDER BY score DESC
-LIMIT $limit
+LIMIT ${limit}
`,
- { query, projectId, limit },
+ { query, projectId },
);
if (res.error || res.data.length === 0) return null;
- return res.data.map((row: Record) =>
- this.rowToResult(row),
- );
+ return res.data.map((row: Record) => this.rowToResult(row));
} catch {
return null;
}
@@ -290,10 +281,9 @@ LIMIT $limit
): Promise {
// Build a simple WHERE clause that checks heading and content
const whereClauses = terms.map(
- (_, i) =>
- `(toLower(s.heading) CONTAINS $term${i} OR toLower(s.content) CONTAINS $term${i})`,
+ (_, i) => `(toLower(s.heading) CONTAINS $term${i} OR toLower(s.content) CONTAINS $term${i})`,
);
- const params: Record = { projectId, limit };
+ const params: Record = { projectId };
terms.forEach((t, i) => {
params[`term${i}`] = t;
});
@@ -310,15 +300,13 @@ RETURN s.id AS sectionId,
s.startLine AS startLine,
1.0 AS score
ORDER BY s.heading
-LIMIT $limit
+LIMIT ${limit}
`,
params,
);
if (res.error || !res.data.length) return [];
- return res.data.map((row: Record) =>
- this.rowToResult(row),
- );
+ return res.data.map((row: Record) => this.rowToResult(row));
}
private rowToResult(row: Record): DocsSearchResult {
diff --git a/src/engines/episode-engine.ts b/src/engines/episode-engine.ts
index 9bfe47a..3e09f86 100644
--- a/src/engines/episode-engine.ts
+++ b/src/engines/episode-engine.ts
@@ -1,3 +1,9 @@
+/**
+ * @file engines/episode-engine
+ * @description Persists and recalls agent episodes, decisions, and reflective learnings.
+ * @remarks Episode data is project-scoped and designed for long-term memory retrieval.
+ */
+
import type MemgraphClient from "../graph/client.js";
export type EpisodeType =
@@ -98,20 +104,12 @@ export default class EpisodeEngine {
);
}
- await this.linkToPreviousEpisode(
- id,
- input.agentId,
- input.sessionId,
- projectId,
- );
+ await this.linkToPreviousEpisode(id, input.agentId, input.sessionId, projectId);
return id;
}
async recall(query: RecallQuery): Promise {
- const conditions = [
- "e.projectId = $projectId",
- "(e.sensitive IS NULL OR e.sensitive = false)",
- ];
+ const conditions = ["e.projectId = $projectId", "(e.sensitive IS NULL OR e.sensitive = false)"];
const params: Record = {
projectId: query.projectId,
limit: Math.max(1, Math.min(query.limit || 5, 50)),
@@ -159,13 +157,9 @@ export default class EpisodeEngine {
const temporalScore = Math.exp(-0.05 * ageDays);
const episodeEntities = new Set(episode.entities || []);
- const graphScore =
- queryEntities.size > 0
- ? this.jaccard(queryEntities, episodeEntities)
- : 0;
+ const graphScore = queryEntities.size > 0 ? this.jaccard(queryEntities, episodeEntities) : 0;
- const relevance =
- 0.5 * lexicalScore + 0.3 * temporalScore + 0.2 * graphScore;
+ const relevance = 0.5 * lexicalScore + 0.3 * temporalScore + 0.2 * graphScore;
return { ...episode, relevance: Number(relevance.toFixed(4)) };
});
@@ -303,15 +297,11 @@ export default class EpisodeEngine {
);
}
- private rowToEpisode(
- row: Record,
- projectId: string,
- ): Episode | null {
+ private rowToEpisode(row: Record, projectId: string): Episode | null {
+ if (row == null || typeof row !== "object") return null;
const rawNode = row.e || row.episode || row;
const node =
- rawNode && typeof rawNode === "object" && rawNode.properties
- ? rawNode.properties
- : rawNode;
+ rawNode && typeof rawNode === "object" && rawNode.properties ? rawNode.properties : rawNode;
if (!node || typeof node !== "object") {
return null;
}
diff --git a/src/engines/migration-engine.ts b/src/engines/migration-engine.ts
index 327efe0..9e4281f 100644
--- a/src/engines/migration-engine.ts
+++ b/src/engines/migration-engine.ts
@@ -1,6 +1,7 @@
/**
- * Progress Tracking Migration Engine
- * Migrates existing markdown-based tracking to graph FEATURE/TASK nodes
+ * @file engines/migration-engine
+ * @description Migrates legacy markdown progress tracking into graph FEATURE/TASK nodes.
+ * @remarks Used for one-time or controlled migration workflows.
*/
import * as fs from "fs";
@@ -104,9 +105,7 @@ export class MigrationEngine {
/**
* Parse markdown into feature/task structure
*/
- private static parseSections(
- content: string
- ): Array<{
+ private static parseSections(content: string): Array<{
type: "feature" | "task";
id: string;
title: string;
@@ -122,20 +121,16 @@ export class MigrationEngine {
// Simple markdown parser (would be expanded for production)
const lines = content.split("\n");
let currentSection: any = null;
- // @ts-expect-error - currentType used in future parser logic
- let currentType: "feature" | "task" | null = null;
for (const line of lines) {
if (line.startsWith("## ")) {
// Feature section
- currentType = "feature";
currentSection = {
title: line.replace("## ", "").trim(),
details: {},
};
} else if (line.startsWith("### ")) {
// Task section
- currentType = "task";
currentSection = {
title: line.replace("### ", "").trim(),
details: {},
@@ -162,10 +157,7 @@ export class MigrationEngine {
* Generate migration report
*/
static generateReport(results: MigrationResult[]): string {
- const totalFeatures = results.reduce(
- (sum, r) => sum + r.featuresCreated,
- 0
- );
+ const totalFeatures = results.reduce((sum, r) => sum + r.featuresCreated, 0);
const totalTasks = results.reduce((sum, r) => sum + r.tasksCreated, 0);
const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0);
diff --git a/src/engines/progress-engine.ts b/src/engines/progress-engine.ts
index 1c7f727..599a50b 100644
--- a/src/engines/progress-engine.ts
+++ b/src/engines/progress-engine.ts
@@ -1,12 +1,14 @@
/**
- * Progress Tracking Engine
- * Manages features, tasks, and milestones in the code graph
+ * @file engines/progress-engine
+ * @description Manages feature/task status and progress queries backed by graph state.
+ * @remarks Provides both in-memory and Memgraph persistence pathways.
*/
import type { GraphIndexManager } from "../graph/index.js";
import type { MemgraphClient } from "../graph/client.js";
import type { CypherStatement } from "../graph/types.js";
import { extractProjectIdFromScopedId } from "../utils/validation.js";
+import { logger } from "../utils/logger.js";
export interface Feature {
id: string;
@@ -186,9 +188,7 @@ export class ProgressEngine {
if (!feature) return null;
// Get tasks for this feature
- const tasks = Array.from(this.tasks.values()).filter(
- (t) => t.featureId === featureId,
- );
+ const tasks = Array.from(this.tasks.values()).filter((t) => t.featureId === featureId);
// Get implementing files (linked via IMPLEMENTS relationship in graph)
const implementingFiles: string[] = [];
@@ -224,26 +224,18 @@ export class ProgressEngine {
// Get test coverage
const testSuites = this.index.getNodesByType("TEST_SUITE").filter((n) => {
- const testsRels = this.index
- .getRelationshipsFrom(n.id)
- .filter((r) => r.type === "TESTS");
+ const testsRels = this.index.getRelationshipsFrom(n.id).filter((r) => r.type === "TESTS");
return testsRels.some((r) => {
const tested = this.index.getNode(r.to);
- return (
- tested && implementingFiles.includes(tested.properties.path || "")
- );
+ return tested && implementingFiles.includes(tested.properties.path || "");
});
});
const testCases = this.index.getNodesByType("TEST_CASE").filter((n) => {
- const testRels = this.index
- .getRelationshipsFrom(n.id)
- .filter((r) => r.type === "TESTS");
+ const testRels = this.index.getRelationshipsFrom(n.id).filter((r) => r.type === "TESTS");
return testRels.some((r) => {
const tested = this.index.getNode(r.to);
- return (
- tested && implementingFiles.includes(tested.properties.path || "")
- );
+ return tested && implementingFiles.includes(tested.properties.path || "");
});
});
@@ -252,8 +244,7 @@ export class ProgressEngine {
// Calculate progress
const completedTasks = tasks.filter((t) => t.status === "completed").length;
- const progressPercentage =
- tasks.length > 0 ? (completedTasks / tasks.length) * 100 : 0;
+ const progressPercentage = tasks.length > 0 ? (completedTasks / tasks.length) * 100 : 0;
return {
feature,
@@ -276,9 +267,7 @@ export class ProgressEngine {
* Find all blocking issues
*/
getBlockingIssues(type?: "all" | "critical" | "features" | "tests"): Task[] {
- const blocked = Array.from(this.tasks.values()).filter(
- (t) => t.status === "blocked",
- );
+ const blocked = Array.from(this.tasks.values()).filter((t) => t.status === "blocked");
if (type === "critical") {
return blocked.filter((t) => t.blockedBy && t.blockedBy.length > 2);
@@ -325,20 +314,17 @@ export class ProgressEngine {
);
if (result.error) {
- throw new Error(
- `[ProgressEngine] Failed to persist feature to Memgraph: ${result.error}`,
- );
+ throw new Error(`[ProgressEngine] Failed to persist feature to Memgraph: ${result.error}`);
}
// Only add to in-memory map after successful persistence
this.features.set(feature.id, feature);
- console.log(
- `[Phase2d] Feature ${feature.id} created and persisted to Memgraph`,
- );
+ logger.error(`[Phase2d] Feature ${feature.id} created and persisted to Memgraph`);
return feature;
} catch (err) {
throw new Error(
`[ProgressEngine] Failed to create feature: ${err instanceof Error ? err.message : String(err)}`,
+ { cause: err },
);
}
}
@@ -375,18 +361,17 @@ export class ProgressEngine {
);
if (result.error) {
- throw new Error(
- `[ProgressEngine] Failed to persist task to Memgraph: ${result.error}`,
- );
+ throw new Error(`[ProgressEngine] Failed to persist task to Memgraph: ${result.error}`);
}
// Only add to in-memory map after successful persistence
this.tasks.set(task.id, task);
- console.log(`[Phase2d] Task ${task.id} created and persisted to Memgraph`);
+ logger.error(`[Phase2d] Task ${task.id} created and persisted to Memgraph`);
return task;
} catch (err) {
throw new Error(
`[ProgressEngine] Failed to create task: ${err instanceof Error ? err.message : String(err)}`,
+ { cause: err },
);
}
}
@@ -394,10 +379,7 @@ export class ProgressEngine {
/**
* Persist task update to Memgraph (Phase 5.3)
*/
- async persistTaskUpdate(
- taskId: string,
- updates: Partial,
- ): Promise {
+ async persistTaskUpdate(taskId: string, updates: Partial): Promise {
if (!this.memgraph || !this.memgraph.isConnected()) {
return false;
}
@@ -421,13 +403,10 @@ export class ProgressEngine {
},
};
- const result = await this.memgraph.executeCypher(
- statement.query,
- statement.params,
- );
+ const result = await this.memgraph.executeCypher(statement.query, statement.params);
return !result.error;
} catch (error) {
- console.error("[ProgressEngine] Failed to persist task update:", error);
+ logger.error("[ProgressEngine] Failed to persist task update:", error);
return false;
}
}
@@ -435,10 +414,7 @@ export class ProgressEngine {
/**
* Persist feature update to Memgraph (Phase 5.3)
*/
- async persistFeatureUpdate(
- featureId: string,
- updates: Partial,
- ): Promise {
+ async persistFeatureUpdate(featureId: string, updates: Partial): Promise {
if (!this.memgraph || !this.memgraph.isConnected()) {
return false;
}
@@ -462,16 +438,10 @@ export class ProgressEngine {
},
};
- const result = await this.memgraph.executeCypher(
- statement.query,
- statement.params,
- );
+ const result = await this.memgraph.executeCypher(statement.query, statement.params);
return !result.error;
} catch (error) {
- console.error(
- "[ProgressEngine] Failed to persist feature update:",
- error,
- );
+ logger.error("[ProgressEngine] Failed to persist feature update:", error);
return false;
}
}
@@ -481,7 +451,7 @@ export class ProgressEngine {
* Called when project context changes to refresh feature/task data
*/
reload(index: GraphIndexManager, projectId?: string): void {
- console.log(`[ProgressEngine] Reloading features and tasks (projectId=${projectId})`);
+ logger.error(`[ProgressEngine] Reloading features and tasks (projectId=${projectId})`);
this.index = index;
this.features.clear();
@@ -505,7 +475,7 @@ export class ProgressEngine {
const featureCount = this.features.size;
const taskCount = this.tasks.size;
- console.log(`[ProgressEngine] Reloaded ${featureCount} features and ${taskCount} tasks`);
+ logger.error(`[ProgressEngine] Reloaded ${featureCount} features and ${taskCount} tasks`);
}
/**
@@ -522,12 +492,10 @@ export class ProgressEngine {
(f) => f.status === "completed",
).length,
totalTasks: this.tasks.size,
- completedTasks: Array.from(this.tasks.values()).filter(
- (t) => t.status === "completed",
- ).length,
- blockedTasks: Array.from(this.tasks.values()).filter(
- (t) => t.status === "blocked",
- ).length,
+ completedTasks: Array.from(this.tasks.values()).filter((t) => t.status === "completed")
+ .length,
+ blockedTasks: Array.from(this.tasks.values()).filter((t) => t.status === "blocked")
+ .length,
},
},
null,
diff --git a/src/engines/test-engine.ts b/src/engines/test-engine.ts
index c674d07..823ccca 100644
--- a/src/engines/test-engine.ts
+++ b/src/engines/test-engine.ts
@@ -1,10 +1,12 @@
/**
- * Test Intelligence Engine
- * Analyzes test dependencies and selects affected tests
+ * @file engines/test-engine
+ * @description Selects and categorizes tests based on dependency impact analysis.
+ * @remarks Used by tool handlers to drive targeted and risk-aware test execution.
*/
import * as path from "path";
import type { GraphIndexManager } from "../graph/index.js";
+import { logger } from "../utils/logger.js";
export interface TestMetadata {
path: string;
@@ -55,7 +57,6 @@ export class TestEngine {
const testPath = suite.properties.path;
const direct: string[] = [];
const indirect: string[] = [];
- let affectedBy: string[] = [];
// Find all test cases in this suite
const testCases = this.index
@@ -93,16 +94,14 @@ export class TestEngine {
// Find which source files import this test
// (for reverse dependency tracking)
const nodes = this.index.getNodesByType("FILE").filter((n) => {
- const rels = this.index
- .getRelationshipsFrom(n.id)
- .filter((r) => r.type === "IMPORTS");
+ const rels = this.index.getRelationshipsFrom(n.id).filter((r) => r.type === "IMPORTS");
return rels.some((r) => {
const imp = this.index.getNode(r.to);
return imp && imp.properties.source === testPath;
});
});
- affectedBy = nodes.map((n) => n.properties.path).filter(Boolean);
+ const affectedBy = nodes.map((n) => n.properties.path).filter(Boolean);
this.dependencyMap[testPath] = {
directDependencies: Array.from(new Set(direct)),
@@ -122,14 +121,38 @@ export class TestEngine {
}
/**
- * Categorize test based on path and patterns
+ * Categorize test based on path and naming conventions.
+ * Handles JS/TS (.integration.test.*), Python (test_*_integration.py,
+ * *_integration_test.py), Go (*_integration_test.go), and Ruby
+ * (integration/..*_spec.rb) conventions.
*/
- private categorizeTest(
- testPath: string
- ): "unit" | "integration" | "performance" | "e2e" {
- if (testPath.includes(".integration.test.")) return "integration";
- if (testPath.includes(".performance.test.")) return "performance";
- if (testPath.includes("/e2e/")) return "e2e";
+ private categorizeTest(testPath: string): "unit" | "integration" | "performance" | "e2e" {
+ const p = testPath.toLowerCase();
+ // Integration: any language
+ if (
+ p.includes(".integration.test.") ||
+ p.includes("_integration_test.") ||
+ p.includes("_integration_spec.") ||
+ p.includes("/integration/") ||
+ p.includes("/integration_") ||
+ p.includes("test_integration_")
+ ) {
+ return "integration";
+ }
+ // Performance: any language
+ if (
+ p.includes(".performance.test.") ||
+ p.includes("_performance_test.") ||
+ p.includes("_bench_test.") ||
+ p.includes("_benchmark") ||
+ p.includes("/benchmarks/")
+ ) {
+ return "performance";
+ }
+ // E2E: any language
+ if (p.includes("/e2e/") || p.includes("/end_to_end/") || p.includes("_e2e_")) {
+ return "e2e";
+ }
return "unit";
}
@@ -139,7 +162,7 @@ export class TestEngine {
selectAffectedTests(
changedFiles: string[],
includeIntegration = true,
- depth = 1
+ depth = 1,
): TestSelectionResult {
const selected = new Set();
const affectedSources = new Set();
@@ -154,8 +177,7 @@ export class TestEngine {
if (!testMeta) continue;
// Skip non-selected test categories
- if (!includeIntegration && testMeta.category === "integration")
- continue;
+ if (!includeIntegration && testMeta.category === "integration") continue;
// Check direct dependencies
if (deps.directDependencies.includes(changedFile)) {
@@ -165,10 +187,7 @@ export class TestEngine {
}
// Check indirect dependencies (up to depth)
- if (
- depth > 1 &&
- this.isIndirectlyDependentOn(changedFile, testPath, depth - 1)
- ) {
+ if (depth > 1 && this.isIndirectlyDependentOn(changedFile, testPath, depth - 1)) {
selected.add(testPath);
affectedSources.add(changedFile);
continue;
@@ -216,7 +235,7 @@ export class TestEngine {
private isIndirectlyDependentOn(
changedFile: string,
testPath: string,
- remainingDepth: number
+ remainingDepth: number,
): boolean {
const deps = this.dependencyMap[testPath];
if (!deps) return false;
@@ -241,7 +260,7 @@ export class TestEngine {
private transitiveImportSearch(
changedFile: string,
fromFile: string,
- remainingDepth: number
+ remainingDepth: number,
): boolean {
// Look for tests that import fromFile and check if they import changedFile
for (const [testPath, deps] of Object.entries(this.dependencyMap)) {
@@ -295,21 +314,22 @@ export class TestEngine {
}
/**
- * Get mirror test path for a source file
+ * Get mirror test path for a source file, preserving the source extension.
+ * e.g. src/utils/units.ts → src/utils/__tests__/units.test.ts
+ * src/utils/helpers.py → src/utils/__tests__/helpers.test.py
+ * lib/foo.rb → lib/__tests__/foo.test.rb
*/
private getMirrorTestPath(sourcePath: string): string {
- // Convert: src/utils/units.ts → src/utils/__tests__/units.test.ts
const dir = path.dirname(sourcePath);
- const base = path.basename(sourcePath, path.extname(sourcePath));
- return `${dir}/__tests__/${base}.test.ts`;
+ const ext = path.extname(sourcePath);
+ const base = path.basename(sourcePath, ext);
+ return `${dir}/__tests__/${base}.test${ext}`;
}
/**
* Determine overall test category
*/
- private determineCategory(
- testPaths: Set
- ): "unit" | "integration" | "mixed" {
+ private determineCategory(testPaths: Set): "unit" | "integration" | "mixed" {
let hasUnit = false;
let hasIntegration = false;
let hasPerformance = false;
@@ -325,8 +345,7 @@ export class TestEngine {
if (
(hasUnit || hasIntegration || hasPerformance) &&
- (hasUnit ? 1 : 0) + (hasIntegration ? 1 : 0) + (hasPerformance ? 1 : 0) >
- 1
+ (hasUnit ? 1 : 0) + (hasIntegration ? 1 : 0) + (hasPerformance ? 1 : 0) > 1
) {
return "mixed";
}
@@ -346,7 +365,7 @@ export class TestEngine {
* Called when project context changes to refresh test data
*/
reload(index: GraphIndexManager, projectId?: string): void {
- console.log(`[TestEngine] Reloading tests (projectId=${projectId})`);
+ logger.debug("TestEngine reloading tests", { projectId });
this.index = index;
this.testMap.clear();
@@ -354,7 +373,7 @@ export class TestEngine {
this.buildTestDependencies();
const testCount = this.testMap.size;
- console.log(`[TestEngine] Reloaded ${testCount} test suites`);
+ logger.debug("TestEngine reloaded", { testCount, projectId });
}
/**
@@ -398,8 +417,7 @@ export class TestEngine {
integrationTests: integrationCount,
performanceTests: performanceCount,
e2eTests: e2eCount,
- averageDuration:
- this.testMap.size > 0 ? totalDuration / this.testMap.size : 0,
+ averageDuration: this.testMap.size > 0 ? totalDuration / this.testMap.size : 0,
};
}
}
diff --git a/src/env.ts b/src/env.ts
index 4023818..85c4f24 100644
--- a/src/env.ts
+++ b/src/env.ts
@@ -17,15 +17,15 @@ dotenv.config();
/**
* Absolute path to the workspace being indexed.
- * Env: LXRAG_WORKSPACE_ROOT
+ * Env: LXDIG_WORKSPACE_ROOT
* Default: process.cwd()
*/
-export const LXRAG_WORKSPACE_ROOT: string = path.resolve(
- process.env.LXRAG_WORKSPACE_ROOT || process.cwd(),
+export const LXDIG_WORKSPACE_ROOT: string = path.resolve(
+ process.env.LXDIG_WORKSPACE_ROOT || process.cwd(),
);
// Alias for backward compatibility
-export const CODE_GRAPH_WORKSPACE_ROOT = LXRAG_WORKSPACE_ROOT;
+export const CODE_GRAPH_WORKSPACE_ROOT = LXDIG_WORKSPACE_ROOT;
/**
* Source sub-directory to index. Can be absolute or relative to WORKSPACE_ROOT.
@@ -33,29 +33,30 @@ export const CODE_GRAPH_WORKSPACE_ROOT = LXRAG_WORKSPACE_ROOT;
* Default: /src
*/
export const GRAPH_SOURCE_DIR: string = (() => {
- const raw =
- process.env.GRAPH_SOURCE_DIR || path.join(LXRAG_WORKSPACE_ROOT, "src");
- return path.isAbsolute(raw) ? raw : path.resolve(LXRAG_WORKSPACE_ROOT, raw);
+ const raw = process.env.GRAPH_SOURCE_DIR || path.join(LXDIG_WORKSPACE_ROOT, "src");
+ return path.isAbsolute(raw) ? raw : path.resolve(LXDIG_WORKSPACE_ROOT, raw);
})();
/**
- * Logical project identifier used as a namespace in the graph.
- * Env: LXRAG_PROJECT_ID
- * Default: basename of LXRAG_WORKSPACE_ROOT
+ * Human-readable project label derived from env or directory basename.
+ * NOTE: This is NOT the canonical DB key. The canonical projectId is always
+ * the 4-char base-36 hash computed by computeProjectFingerprint().
+ * This value is used as a display label / friendly name only.
+ * Env: LXDIG_PROJECT_ID
+ * Default: basename of LXDIG_WORKSPACE_ROOT
*/
-export const LXRAG_PROJECT_ID: string =
- process.env.LXRAG_PROJECT_ID || path.basename(LXRAG_WORKSPACE_ROOT);
+export const LXDIG_PROJECT_ID: string =
+ process.env.LXDIG_PROJECT_ID || path.basename(LXDIG_WORKSPACE_ROOT);
-// Alias for backward compatibility
-export const CODE_GRAPH_PROJECT_ID = LXRAG_PROJECT_ID;
+/** @deprecated Use LXDIG_PROJECT_ID. This is a human-readable label, not a DB key. */
+export const CODE_GRAPH_PROJECT_ID = LXDIG_PROJECT_ID;
/**
* Transaction ID for graph write operations.
- * Env: LXRAG_TX_ID
+ * Env: LXDIG_TX_ID
* Default: undefined (callers generate a fresh `tx-` per invocation)
*/
-export const LXRAG_TX_ID: string | undefined =
- process.env.LXRAG_TX_ID || undefined;
+export const LXDIG_TX_ID: string | undefined = process.env.LXDIG_TX_ID || undefined;
// ── MCP Transport ─────────────────────────────────────────────────────────────
@@ -76,14 +77,13 @@ export const MCP_PORT: number = parseInt(process.env.MCP_PORT || "9000", 10);
/**
* Display name reported by the MCP server.
- * Env: LXRAG_SERVER_NAME
- * Default: "lxRAG MCP"
+ * Env: LXDIG_SERVER_NAME
+ * Default: "lxDIG MCP"
*/
-export const LXRAG_SERVER_NAME: string =
- process.env.LXRAG_SERVER_NAME || "lxRAG MCP";
+export const LXDIG_SERVER_NAME: string = process.env.LXDIG_SERVER_NAME || "lxDIG MCP";
// Alias for backward compatibility
-export const CODE_GRAPH_SERVER_NAME = LXRAG_SERVER_NAME;
+export const CODE_GRAPH_SERVER_NAME = LXDIG_SERVER_NAME;
// ── Memgraph (graph database) ─────────────────────────────────────────────────
@@ -99,10 +99,7 @@ export const MEMGRAPH_HOST: string = process.env.MEMGRAPH_HOST || "localhost";
* Env: MEMGRAPH_PORT
* Default: 7687
*/
-export const MEMGRAPH_PORT: number = parseInt(
- process.env.MEMGRAPH_PORT || "7687",
- 10,
-);
+export const MEMGRAPH_PORT: number = parseInt(process.env.MEMGRAPH_PORT || "7687", 10);
// ── Qdrant (vector store) ─────────────────────────────────────────────────────
@@ -118,114 +115,116 @@ export const QDRANT_HOST: string = process.env.QDRANT_HOST || "localhost";
* Env: QDRANT_PORT
* Default: 6333
*/
-export const QDRANT_PORT: number = parseInt(
- process.env.QDRANT_PORT || "6333",
- 10,
-);
+export const QDRANT_PORT: number = parseInt(process.env.QDRANT_PORT || "6333", 10);
// ── Code Summarizer ───────────────────────────────────────────────────────────
/**
* URL of the optional LLM summarizer service (e.g. http://localhost:8080).
* When undefined, summarization is disabled and heuristic summaries are used.
- * Env: LXRAG_SUMMARIZER_URL
+ * Env: LXDIG_SUMMARIZER_URL
*/
-export const LXRAG_SUMMARIZER_URL: string | undefined =
- process.env.LXRAG_SUMMARIZER_URL || undefined;
+export const LXDIG_SUMMARIZER_URL: string | undefined =
+ process.env.LXDIG_SUMMARIZER_URL || undefined;
// Alias for backward compatibility
-export const CODE_GRAPH_SUMMARIZER_URL = LXRAG_SUMMARIZER_URL;
+export const CODE_GRAPH_SUMMARIZER_URL = LXDIG_SUMMARIZER_URL;
// ── Agent / Coordination ──────────────────────────────────────────────────────
/**
* Identifier for the current agent instance used in coordination claims.
- * Env: LXRAG_AGENT_ID
+ * Env: LXDIG_AGENT_ID
* Default: "agent-local"
*/
-export const LXRAG_AGENT_ID: string =
- process.env.LXRAG_AGENT_ID || "agent-local";
+export const LXDIG_AGENT_ID: string = process.env.LXDIG_AGENT_ID || "agent-local";
// Alias for backward compatibility
-export const CODE_GRAPH_AGENT_ID = LXRAG_AGENT_ID;
+export const CODE_GRAPH_AGENT_ID = LXDIG_AGENT_ID;
// ── Parser ────────────────────────────────────────────────────────────────────
/**
* Set to true to use the Tree-sitter parser instead of the regex parser.
- * Env: LXRAG_USE_TREE_SITTER
+ * Env: LXDIG_USE_TREE_SITTER
* Default: false
*/
-export const LXRAG_USE_TREE_SITTER: boolean =
- process.env.LXRAG_USE_TREE_SITTER === "true";
+export const LXDIG_USE_TREE_SITTER: boolean = process.env.LXDIG_USE_TREE_SITTER === "true";
// Alias for backward compatibility
-export const CODE_GRAPH_USE_TREE_SITTER = LXRAG_USE_TREE_SITTER;
+export const CODE_GRAPH_USE_TREE_SITTER = LXDIG_USE_TREE_SITTER;
// ── File Watcher ──────────────────────────────────────────────────────────────
/**
* Enables incremental file-change watching.
* Automatically considered true when MCP_TRANSPORT=http.
- * Env: LXRAG_ENABLE_WATCHER
+ * Env: LXDIG_ENABLE_WATCHER
* Default: false
*/
-export const LXRAG_ENABLE_WATCHER: boolean =
- process.env.LXRAG_ENABLE_WATCHER === "true";
+export const LXDIG_ENABLE_WATCHER: boolean = process.env.LXDIG_ENABLE_WATCHER === "true";
// Alias for backward compatibility
-export const CODE_GRAPH_ENABLE_WATCHER = LXRAG_ENABLE_WATCHER;
+export const CODE_GRAPH_ENABLE_WATCHER = LXDIG_ENABLE_WATCHER;
/**
* Comma-separated glob patterns to exclude from indexing/watching.
- * Env: LXRAG_IGNORE_PATTERNS
+ * Env: LXDIG_IGNORE_PATTERNS
* Example: "node_modules/**,dist/**,.git/**"
*/
-export const LXRAG_IGNORE_PATTERNS: string[] = (
- process.env.LXRAG_IGNORE_PATTERNS || ""
-)
+export const LXDIG_IGNORE_PATTERNS: string[] = (process.env.LXDIG_IGNORE_PATTERNS || "")
.split(",")
.map((p) => p.trim())
.filter(Boolean);
// Alias for backward compatibility
-export const CODE_GRAPH_IGNORE_PATTERNS = LXRAG_IGNORE_PATTERNS;
+export const CODE_GRAPH_IGNORE_PATTERNS = LXDIG_IGNORE_PATTERNS;
// ── Path Fallback ─────────────────────────────────────────────────────────────
/**
* Allow the server to fall back to the mounted workspace path when the
* requested path is not accessible (useful inside Docker containers).
- * Env: LXRAG_ALLOW_RUNTIME_PATH_FALLBACK
+ * Env: LXDIG_ALLOW_RUNTIME_PATH_FALLBACK
* Default: false
*/
-export const LXRAG_ALLOW_RUNTIME_PATH_FALLBACK: boolean =
- process.env.LXRAG_ALLOW_RUNTIME_PATH_FALLBACK === "true";
+export const LXDIG_ALLOW_RUNTIME_PATH_FALLBACK: boolean =
+ process.env.LXDIG_ALLOW_RUNTIME_PATH_FALLBACK === "true";
// Alias for backward compatibility
-export const CODE_GRAPH_ALLOW_RUNTIME_PATH_FALLBACK =
- LXRAG_ALLOW_RUNTIME_PATH_FALLBACK;
+export const CODE_GRAPH_ALLOW_RUNTIME_PATH_FALLBACK = LXDIG_ALLOW_RUNTIME_PATH_FALLBACK;
// ── Command Execution ──────────────────────────────────────────────────────
/**
* Maximum execution time for command execution in milliseconds.
- * Env: LXRAG_COMMAND_EXECUTION_TIMEOUT_MS
+ * Env: LXDIG_COMMAND_EXECUTION_TIMEOUT_MS
* Default: 30000 (30 seconds)
*/
-export const LXRAG_COMMAND_EXECUTION_TIMEOUT_MS: number = parseInt(
- process.env.LXRAG_COMMAND_EXECUTION_TIMEOUT_MS || "30000",
+export const LXDIG_COMMAND_EXECUTION_TIMEOUT_MS: number = parseInt(
+ process.env.LXDIG_COMMAND_EXECUTION_TIMEOUT_MS || "30000",
+ 10,
+);
+
+/**
+ * Maximum time to wait synchronously for graph_rebuild before falling back
+ * to queued/background execution.
+ * Env: LXDIG_SYNC_REBUILD_THRESHOLD_MS
+ * Default: 12000 (12 seconds)
+ */
+export const LXDIG_SYNC_REBUILD_THRESHOLD_MS: number = parseInt(
+ process.env.LXDIG_SYNC_REBUILD_THRESHOLD_MS || "12000",
10,
);
/**
* Maximum output size for command results in bytes.
* Prevents DoS from commands producing massive output.
- * Env: LXRAG_COMMAND_OUTPUT_SIZE_LIMIT_BYTES
+ * Env: LXDIG_COMMAND_OUTPUT_SIZE_LIMIT_BYTES
* Default: 10485760 (10 MB)
*/
-export const LXRAG_COMMAND_OUTPUT_SIZE_LIMIT_BYTES: number = parseInt(
- process.env.LXRAG_COMMAND_OUTPUT_SIZE_LIMIT_BYTES || "10485760",
+export const LXDIG_COMMAND_OUTPUT_SIZE_LIMIT_BYTES: number = parseInt(
+ process.env.LXDIG_COMMAND_OUTPUT_SIZE_LIMIT_BYTES || "10485760",
10,
);
@@ -233,11 +232,11 @@ export const LXRAG_COMMAND_OUTPUT_SIZE_LIMIT_BYTES: number = parseInt(
/**
* Debounce time for file watcher in milliseconds.
- * Env: LXRAG_WATCHER_DEBOUNCE_MS
+ * Env: LXDIG_WATCHER_DEBOUNCE_MS
* Default: 500 (500ms)
*/
-export const LXRAG_WATCHER_DEBOUNCE_MS: number = parseInt(
- process.env.LXRAG_WATCHER_DEBOUNCE_MS || "500",
+export const LXDIG_WATCHER_DEBOUNCE_MS: number = parseInt(
+ process.env.LXDIG_WATCHER_DEBOUNCE_MS || "500",
10,
);
@@ -245,31 +244,31 @@ export const LXRAG_WATCHER_DEBOUNCE_MS: number = parseInt(
/**
* Maximum Memgraph connection pool size.
- * Env: LXRAG_MEMGRAPH_MAX_POOL_SIZE
+ * Env: LXDIG_MEMGRAPH_MAX_POOL_SIZE
* Default: 50
*/
-export const LXRAG_MEMGRAPH_MAX_POOL_SIZE: number = parseInt(
- process.env.LXRAG_MEMGRAPH_MAX_POOL_SIZE || "50",
+export const LXDIG_MEMGRAPH_MAX_POOL_SIZE: number = parseInt(
+ process.env.LXDIG_MEMGRAPH_MAX_POOL_SIZE || "50",
10,
);
/**
* Memgraph connection acquisition timeout in milliseconds.
- * Env: LXRAG_MEMGRAPH_CONNECTION_TIMEOUT_MS
+ * Env: LXDIG_MEMGRAPH_CONNECTION_TIMEOUT_MS
* Default: 10000 (10 seconds)
*/
-export const LXRAG_MEMGRAPH_CONNECTION_TIMEOUT_MS: number = parseInt(
- process.env.LXRAG_MEMGRAPH_CONNECTION_TIMEOUT_MS || "10000",
+export const LXDIG_MEMGRAPH_CONNECTION_TIMEOUT_MS: number = parseInt(
+ process.env.LXDIG_MEMGRAPH_CONNECTION_TIMEOUT_MS || "10000",
10,
);
/**
* Memgraph connection liveness check timeout in milliseconds.
- * Env: LXRAG_MEMGRAPH_LIVENESS_TIMEOUT_MS
+ * Env: LXDIG_MEMGRAPH_LIVENESS_TIMEOUT_MS
* Default: 5000 (5 seconds)
*/
-export const LXRAG_MEMGRAPH_LIVENESS_TIMEOUT_MS: number = parseInt(
- process.env.LXRAG_MEMGRAPH_LIVENESS_TIMEOUT_MS || "5000",
+export const LXDIG_MEMGRAPH_LIVENESS_TIMEOUT_MS: number = parseInt(
+ process.env.LXDIG_MEMGRAPH_LIVENESS_TIMEOUT_MS || "5000",
10,
);
@@ -277,10 +276,20 @@ export const LXRAG_MEMGRAPH_LIVENESS_TIMEOUT_MS: number = parseInt(
/**
* Maximum state history size (bounded for memory efficiency).
- * Env: LXRAG_STATE_HISTORY_MAX_SIZE
- * Default: 100 entries
+ * Env: LXDIG_STATE_HISTORY_MAX_SIZE
+ * Default: 200 entries
*/
-export const LXRAG_STATE_HISTORY_MAX_SIZE: number = parseInt(
- process.env.LXRAG_STATE_HISTORY_MAX_SIZE || "100",
+export const LXDIG_STATE_HISTORY_MAX_SIZE: number = parseInt(
+ process.env.LXDIG_STATE_HISTORY_MAX_SIZE || "200",
10,
);
+
+// ── Logging ───────────────────────────────────────────────────────────────────
+
+/**
+ * Minimum log level emitted by the structured logger.
+ * Env: LXDIG_LOG_LEVEL
+ * Accepted values: "debug" | "info" | "warn" | "error"
+ * Default: "info"
+ */
+export const LXDIG_LOG_LEVEL: string = process.env.LXDIG_LOG_LEVEL ?? "info";
diff --git a/src/graph/__tests__/builder.test.ts b/src/graph/__tests__/builder.test.ts
new file mode 100644
index 0000000..4ff5a6a
--- /dev/null
+++ b/src/graph/__tests__/builder.test.ts
@@ -0,0 +1,393 @@
+import { describe, expect, it } from "vitest";
+import * as path from "node:path";
+import { GraphBuilder } from "../builder.js";
+import type { ParsedFile } from "../builder.js";
+
+// ─── Helpers ──────────────────────────────────────────────────────────────────
+
+const WORKSPACE = "/workspace";
+
+function makeFile(overrides: Partial = {}): ParsedFile {
+ return {
+ path: "/workspace/src/components/App.tsx",
+ filePath: "/workspace/src/components/App.tsx",
+ relativePath: "src/components/App.tsx",
+ language: "TypeScript",
+ LOC: 50,
+ hash: "abc123",
+ functions: [],
+ classes: [],
+ variables: [],
+ imports: [],
+ exports: [],
+ ...overrides,
+ };
+}
+
+function makeImport(source: string): {
+ id: string;
+ source: string;
+ specifiers: string[];
+ startLine: number;
+ summary: null;
+} {
+ return {
+ id: `import-${source}`,
+ source,
+ specifiers: [],
+ startLine: 1,
+ summary: null,
+ };
+}
+
+function builder(projectId = "test-proj", workspaceRoot = WORKSPACE) {
+ return new GraphBuilder(projectId, workspaceRoot, "tx-001", 1700000000000);
+}
+
+// ─── T15 — FILE.path must always be absolute ──────────────────────────────────
+
+describe("GraphBuilder — FILE path normalization (A1 regression)", () => {
+ it("T15: import target FILE.path is absolute even when resolvedPath is relative", () => {
+ const workspaceRoot = "/workspace";
+ const b = builder("proj", workspaceRoot);
+
+ // File A imports from B using a relative source reference
+ // The import source will resolve to src/lib/utils.ts (relative)
+ const fileA = makeFile({
+ path: "/workspace/src/components/App.tsx",
+ filePath: "/workspace/src/components/App.tsx",
+ relativePath: "src/components/App.tsx",
+ imports: [
+ // This resolves to src/lib/utils.ts (relative to workspaceRoot)
+ makeImport("../lib/utils"),
+ ],
+ } as any);
+
+ const { nodes, edges } = b.buildFromParsedFile(fileA);
+ const stmts = [...nodes, ...edges];
+
+ // Find all FILE node MERGE statements that set targetFile.path
+ const filePathStmts = stmts.filter(
+ (s) => s.query.includes("targetFile:FILE") && s.params.absoluteTargetPath !== undefined,
+ );
+
+ for (const stmt of filePathStmts) {
+ const p = stmt.params.absoluteTargetPath as string;
+ expect(path.isAbsolute(p), `Expected absolute path but got: ${p}`).toBe(true);
+ expect(p).toContain(workspaceRoot);
+ }
+ });
+
+ it("T15b: createFileNode FILE.path is always the absolute filePath", () => {
+ const b = builder("proj", "/workspace");
+ const fileA = makeFile({
+ filePath: "/workspace/src/components/App.tsx",
+ relativePath: "src/components/App.tsx",
+ });
+
+ const { nodes: nodes2, edges: edges2 } = b.buildFromParsedFile(fileA);
+ const stmts = [...nodes2, ...edges2];
+
+ // The canonical FILE node (from createFileNode) must have absolute path
+ const fileNodeStmt = stmts.find(
+ (s) => s.query.includes("MERGE (f:FILE") && s.query.includes("f.path = $path"),
+ )!;
+ expect(fileNodeStmt).toBeDefined();
+ expect(path.isAbsolute(fileNodeStmt.params.path as string)).toBe(true);
+ expect(fileNodeStmt.params.path).toBe("/workspace/src/components/App.tsx");
+ });
+
+ it("T16: FILE.id for nested file contains full relative path", () => {
+ const b = builder("proj", "/workspace");
+ const fileA = makeFile({
+ filePath: "/workspace/src/components/App.tsx",
+ relativePath: "src/components/App.tsx",
+ imports: [makeImport("../controls/ArchitectureControls")],
+ });
+
+ const { nodes: nodes3, edges: edges3 } = b.buildFromParsedFile(fileA);
+ const stmts = [...nodes3, ...edges3];
+
+ // Find the stub FILE node created for the import target
+ const stubStmt = stmts.find(
+ (s) =>
+ s.query.includes("targetFile:FILE") &&
+ String(s.params.targetId || "").includes("ArchitectureControls"),
+ );
+
+ if (stubStmt) {
+ const targetId = String(stubStmt.params.targetId);
+ // Must include the full relative path (not just the basename)
+ expect(targetId).toMatch(/controls\/ArchitectureControls/);
+ expect(targetId).not.toMatch(/^proj:file:ArchitectureControls/);
+ }
+ });
+
+ it("relativePath property in stub FILE is the original relative path", () => {
+ const b = builder("proj", "/workspace");
+ const fileA = makeFile({
+ filePath: "/workspace/src/components/App.tsx",
+ relativePath: "src/components/App.tsx",
+ imports: [makeImport("../lib/utils")],
+ });
+
+ const { nodes: nodes4, edges: edges4 } = b.buildFromParsedFile(fileA);
+ const stmts = [...nodes4, ...edges4];
+
+ const stubStmt = stmts.find(
+ (s) => s.query.includes("targetFile:FILE") && s.params.relativePath !== undefined,
+ );
+
+ if (stubStmt) {
+ const rel = stubStmt.params.relativePath as string;
+ // relativePath must be relative (not start with /)
+ expect(path.isAbsolute(rel)).toBe(false);
+ expect(rel).toContain("utils");
+ }
+ });
+
+ it("absolute and relative paths are consistent: resolve(workspaceRoot, relativePath) == absolutePath", () => {
+ const workspaceRoot = "/workspace";
+ const b = builder("proj", workspaceRoot);
+ const fileA = makeFile({
+ filePath: "/workspace/src/components/App.tsx",
+ relativePath: "src/components/App.tsx",
+ imports: [makeImport("../lib/helper")],
+ });
+
+ const { nodes: nodes5, edges: edges5 } = b.buildFromParsedFile(fileA);
+ const stmts = [...nodes5, ...edges5];
+
+ const stubStmt = stmts.find(
+ (s) => s.query.includes("targetFile:FILE") && s.params.absoluteTargetPath !== undefined,
+ );
+
+ if (stubStmt) {
+ const absPath = stubStmt.params.absoluteTargetPath as string;
+ const relPath = stubStmt.params.relativePath as string;
+ expect(absPath).toBe(path.resolve(workspaceRoot, relPath));
+ }
+ });
+});
+
+describe("GraphBuilder — symbol filePath metadata", () => {
+ it("sets FUNCTION.filePath to the parent file absolute path", () => {
+ const b = builder("proj", "/workspace");
+ const fileA = makeFile({
+ filePath: "/workspace/src/components/App.tsx",
+ relativePath: "src/components/App.tsx",
+ functions: [
+ {
+ id: "fn:app:render",
+ name: "renderApp",
+ parameters: [],
+ async: false,
+ line: 10,
+ kind: "function",
+ startLine: 10,
+ endLine: 14,
+ LOC: 5,
+ isExported: true,
+ },
+ ] as any,
+ });
+
+ const { nodes: fnNodes, edges: fnEdges } = b.buildFromParsedFile(fileA);
+ const stmts = [...fnNodes, ...fnEdges];
+ const functionStmt = stmts.find(
+ (s) =>
+ s.query.includes("MERGE (func:FUNCTION") && s.query.includes("func.filePath = $filePath"),
+ );
+
+ expect(functionStmt).toBeDefined();
+ expect(functionStmt!.params.filePath).toBe("/workspace/src/components/App.tsx");
+ });
+
+ it("sets CLASS.filePath to the parent file absolute path", () => {
+ const b = builder("proj", "/workspace");
+ const fileA = makeFile({
+ filePath: "/workspace/src/components/App.tsx",
+ relativePath: "src/components/App.tsx",
+ classes: [
+ {
+ id: "class:AppController",
+ name: "AppController",
+ methods: [],
+ properties: [],
+ line: 20,
+ kind: "class",
+ startLine: 20,
+ endLine: 30,
+ LOC: 11,
+ isExported: true,
+ },
+ ] as any,
+ });
+
+ const { nodes: clsNodes, edges: clsEdges } = b.buildFromParsedFile(fileA);
+ const stmts = [...clsNodes, ...clsEdges];
+ const classStmt = stmts.find(
+ (s) => s.query.includes("MERGE (cls:CLASS") && s.query.includes("cls.filePath = $filePath"),
+ );
+
+ expect(classStmt).toBeDefined();
+ expect(classStmt!.params.filePath).toBe("/workspace/src/components/App.tsx");
+ });
+});
+
+describe("GraphBuilder — two-phase BuildResult structure", () => {
+ // Test 1: buildFromParsedFile returns { nodes, edges }
+ it("returns BuildResult with nodes and edges arrays", () => {
+ const b = builder("proj", "/workspace");
+ const file = makeFile({
+ functions: [
+ {
+ name: "fn1",
+ parameters: [],
+ async: false,
+ line: 1,
+ kind: "function",
+ startLine: 1,
+ endLine: 5,
+ LOC: 5,
+ },
+ ],
+ classes: [
+ {
+ id: "class:C1",
+ name: "C1",
+ methods: [],
+ properties: [],
+ line: 10,
+ kind: "class",
+ startLine: 10,
+ endLine: 20,
+ LOC: 11,
+ },
+ ],
+ } as any);
+ const result = b.buildFromParsedFile(file);
+ expect(result).toHaveProperty("nodes");
+ expect(result).toHaveProperty("edges");
+ expect(Array.isArray(result.nodes)).toBe(true);
+ expect(Array.isArray(result.edges)).toBe(true);
+ expect(result.nodes.length).toBeGreaterThan(0);
+ expect(result.edges.length).toBeGreaterThan(0);
+ });
+
+ // Test 2: nodes array contains no MATCH-dependent relationship statements
+ it("nodes array contains no MATCH-then-MERGE-edge patterns", () => {
+ const b = builder("proj", "/workspace");
+ const file = makeFile({
+ functions: [
+ {
+ name: "fn1",
+ parameters: [],
+ async: false,
+ line: 1,
+ kind: "function",
+ startLine: 1,
+ endLine: 5,
+ LOC: 5,
+ },
+ ],
+ imports: [makeImport("./utils")],
+ } as any);
+ const { nodes } = b.buildFromParsedFile(file);
+ for (const stmt of nodes) {
+ // No node statement should contain "MATCH" followed by a relationship arrow
+ const hasMatchEdge = /MATCH.*MERGE.*-\[.*\]->/.test(stmt.query);
+ expect(
+ hasMatchEdge,
+ `Node statement should not have MATCH+edge pattern: ${stmt.query.trim().slice(0, 80)}`,
+ ).toBe(false);
+ }
+ });
+
+ // Test 3: edges array contains no bare MERGE-only node creation (except stubs in combined statements)
+ it("edges array statements all involve relationships or property updates", () => {
+ const b = builder("proj", "/workspace");
+ const file = makeFile({
+ functions: [
+ {
+ name: "fn1",
+ parameters: [],
+ async: false,
+ line: 1,
+ kind: "function",
+ startLine: 1,
+ endLine: 5,
+ LOC: 5,
+ },
+ ],
+ classes: [
+ {
+ id: "class:C1",
+ name: "C1",
+ methods: [],
+ properties: [],
+ line: 10,
+ kind: "class",
+ startLine: 10,
+ endLine: 20,
+ LOC: 11,
+ },
+ ],
+ variables: [{ name: "v1", type: "string", startLine: 25, endLine: 25 }],
+ } as any);
+ const { edges } = b.buildFromParsedFile(file);
+ for (const stmt of edges) {
+ // Every edge statement must contain at least MATCH (depends on existing node)
+ // or a relationship arrow pattern -[:REL]->
+ const hasMatch = stmt.query.includes("MATCH");
+ const hasRelArrow = /\-\[.*\]\->/.test(stmt.query);
+ const hasSET = stmt.query.includes("SET");
+ expect(
+ hasMatch || hasRelArrow || hasSET,
+ `Edge statement should involve MATCH, relationship, or SET: ${stmt.query.trim().slice(0, 80)}`,
+ ).toBe(true);
+ }
+ });
+
+ // Test 4: node + edge count equals total from backward-compat getter
+ it("nodes.length + edges.length equals total statement count", () => {
+ const b = builder("proj", "/workspace");
+ const file = makeFile({
+ functions: [
+ {
+ name: "fn1",
+ parameters: [],
+ async: false,
+ line: 1,
+ kind: "function",
+ startLine: 1,
+ endLine: 5,
+ LOC: 5,
+ isExported: true,
+ },
+ ],
+ classes: [
+ {
+ id: "class:C1",
+ name: "C1",
+ methods: [],
+ properties: [],
+ line: 10,
+ kind: "class",
+ startLine: 10,
+ endLine: 20,
+ LOC: 11,
+ extends: "Base",
+ implements: ["Iface1"],
+ },
+ ],
+ variables: [{ name: "v1", type: "string", startLine: 25, endLine: 25 }],
+ imports: [makeImport("./utils")],
+ exports: [{ name: "fn1", type: "function" }],
+ } as any);
+ const result = b.buildFromParsedFile(file);
+ const totalFromResult = result.nodes.length + result.edges.length;
+ // Verify it's a reasonable number (at least: 1 file + 1 func + 1 class + 1 var + 1 import + 1 export = 6 nodes minimum)
+ expect(totalFromResult).toBeGreaterThanOrEqual(12);
+ });
+});
diff --git a/src/graph/__tests__/client.test.ts b/src/graph/__tests__/client.test.ts
new file mode 100644
index 0000000..6c18e8e
--- /dev/null
+++ b/src/graph/__tests__/client.test.ts
@@ -0,0 +1,171 @@
+import { describe, expect, it, vi } from "vitest";
+import { MemgraphClient } from "../client.js";
+
+describe("MemgraphClient", () => {
+ it("falls back to localhost when initial host is unresolved", async () => {
+ const client = new MemgraphClient({ host: "memgraph", port: 7687 });
+
+ const failingSession = {
+ run: vi.fn().mockRejectedValue(new Error("ENOTFOUND memgraph")),
+ close: vi.fn().mockResolvedValue(undefined),
+ };
+ const successSession = {
+ run: vi.fn().mockResolvedValue({ records: [] }),
+ close: vi.fn().mockResolvedValue(undefined),
+ };
+
+ const firstDriver = {
+ session: vi.fn().mockReturnValue(failingSession),
+ close: vi.fn().mockResolvedValue(undefined),
+ };
+ const secondDriver = {
+ session: vi.fn().mockReturnValue(successSession),
+ close: vi.fn().mockResolvedValue(undefined),
+ };
+
+ (client as any).driver = firstDriver;
+ (client as any).createDriver = vi.fn().mockReturnValue(secondDriver);
+
+ await client.connect();
+
+ expect((client as any).createDriver).toHaveBeenCalledWith("localhost");
+ expect(client.isConnected()).toBe(true);
+ });
+
+ it("sanitizes undefined query params to null", async () => {
+ const client = new MemgraphClient();
+
+ const run = vi.fn().mockResolvedValue({
+ records: [{ toObject: () => ({ ok: 1 }) }],
+ });
+ const close = vi.fn().mockResolvedValue(undefined);
+ const session = { run, close };
+
+ (client as any).connected = true;
+ (client as any).driver = {
+ session: vi.fn().mockReturnValue(session),
+ };
+
+ const result = await client.executeCypher("RETURN $value", {
+ value: undefined,
+ keep: "x",
+ });
+
+ expect(result.error).toBeUndefined();
+ expect(result.data).toEqual([{ ok: 1 }]);
+ expect(run).toHaveBeenCalledWith("RETURN $value", {
+ value: null,
+ keep: "x",
+ });
+ });
+
+ it("returns per-statement results in executeBatch and continues on errors", async () => {
+ const client = new MemgraphClient();
+ const executeSpy = vi
+ .spyOn(client, "executeCypher")
+ .mockResolvedValueOnce({ data: [{ a: 1 }] })
+ .mockResolvedValueOnce({ data: [], error: "boom" });
+
+ const results = await client.executeBatch([
+ { query: "RETURN 1", params: {} },
+ { query: "RETURN 2", params: {} },
+ ]);
+
+ expect(executeSpy).toHaveBeenCalledTimes(2);
+ expect(results).toHaveLength(2);
+ expect(results[0].data).toEqual([{ a: 1 }]);
+ expect(results[1].error).toBe("boom");
+ });
+
+ it("returns empty graph when disconnected and maps rows when connected", async () => {
+ const client = new MemgraphClient();
+
+ (client as any).connected = false;
+ expect(await client.loadProjectGraph("proj-a")).toEqual({
+ nodes: [],
+ relationships: [],
+ });
+
+ (client as any).connected = true;
+ vi.spyOn(client, "executeCypher")
+ .mockResolvedValueOnce({
+ data: [{ id: "n1", type: "FILE", props: { path: "src/a.ts" } }],
+ })
+ .mockResolvedValueOnce({
+ data: [{ from: "n1", to: "n2", type: "CALLS", props: { w: 1 } }],
+ });
+
+ const loaded = await client.loadProjectGraph("proj-a");
+ expect(loaded.nodes).toEqual([{ id: "n1", type: "FILE", properties: { path: "src/a.ts" } }]);
+ expect(loaded.relationships).toEqual([
+ {
+ id: "n1-CALLS-n2",
+ from: "n1",
+ to: "n2",
+ type: "CALLS",
+ properties: { w: 1 },
+ },
+ ]);
+ });
+
+ it("retries transient query errors once and succeeds", async () => {
+ const client = new MemgraphClient();
+
+ const firstSession = {
+ run: vi.fn().mockRejectedValue(new Error("ServiceUnavailable: temporary network hiccup")),
+ close: vi.fn().mockResolvedValue(undefined),
+ };
+ const secondSession = {
+ run: vi.fn().mockResolvedValue({
+ records: [{ toObject: () => ({ ok: true }) }],
+ }),
+ close: vi.fn().mockResolvedValue(undefined),
+ };
+
+ (client as any).connected = true;
+ (client as any).driver = {
+ session: vi.fn().mockReturnValueOnce(firstSession).mockReturnValueOnce(secondSession),
+ };
+
+ const result = await client.executeCypher("RETURN 1");
+
+ expect(result.error).toBeUndefined();
+ expect(result.data).toEqual([{ ok: true }]);
+ expect((client as any).driver.session).toHaveBeenCalledTimes(2);
+ expect(firstSession.close).toHaveBeenCalledTimes(1);
+ expect(secondSession.close).toHaveBeenCalledTimes(1);
+ });
+
+ it("does not retry non-transient query errors", async () => {
+ const client = new MemgraphClient();
+
+ const session = {
+ run: vi.fn().mockRejectedValue(new Error("SyntaxError: invalid cypher")),
+ close: vi.fn().mockResolvedValue(undefined),
+ };
+
+ (client as any).connected = true;
+ (client as any).driver = {
+ session: vi.fn().mockReturnValue(session),
+ };
+
+ const result = await client.executeCypher("BROKEN QUERY");
+
+ expect(result.data).toEqual([]);
+ expect(String(result.error)).toContain("Query failed");
+ expect((client as any).driver.session).toHaveBeenCalledTimes(1);
+ expect(session.close).toHaveBeenCalledTimes(1);
+ });
+
+ it("returns connection failure envelope when auto-connect fails", async () => {
+ const client = new MemgraphClient();
+ vi.spyOn(client, "connect").mockRejectedValue(new Error("dial timeout"));
+
+ (client as any).connected = false;
+
+ const result = await client.executeCypher("RETURN 1");
+
+ expect(result.data).toEqual([]);
+ expect(String(result.error)).toContain("Connection failed: dial timeout");
+ });
+});
diff --git a/src/graph/docs-builder.test.ts b/src/graph/__tests__/docs-builder.test.ts
similarity index 88%
rename from src/graph/docs-builder.test.ts
rename to src/graph/__tests__/docs-builder.test.ts
index d60ab0c..9b996bf 100644
--- a/src/graph/docs-builder.test.ts
+++ b/src/graph/__tests__/docs-builder.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
-import { DocsBuilder } from "./docs-builder.js";
-import type { ParsedDoc, ParsedSection } from "../parsers/docs-parser.js";
+import { DocsBuilder } from "../docs-builder.js";
+import type { ParsedDoc, ParsedSection } from "../../parsers/docs-parser.js";
// ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -69,17 +69,13 @@ describe("DocsBuilder.buildFromParsedDoc — output shape", () => {
],
});
const stmts = builder().buildFromParsedDoc(doc);
- const nextSectionStmts = stmts.filter((s) =>
- s.query.includes("NEXT_SECTION"),
- );
+ const nextSectionStmts = stmts.filter((s) => s.query.includes("NEXT_SECTION"));
expect(nextSectionStmts).toHaveLength(2);
});
it("with 1 section generates 0 NEXT_SECTION edges", () => {
const stmts = builder().buildFromParsedDoc(makeDoc());
- const nextSectionStmts = stmts.filter((s) =>
- s.query.includes("NEXT_SECTION"),
- );
+ const nextSectionStmts = stmts.filter((s) => s.query.includes("NEXT_SECTION"));
expect(nextSectionStmts).toHaveLength(0);
});
});
@@ -94,9 +90,7 @@ describe("DocsBuilder — DOCUMENT statement", () => {
it("DOCUMENT params include all required fields", () => {
const doc = makeDoc();
- const stmts = builder("proj", "/root", "tx-1", 1000).buildFromParsedDoc(
- doc,
- );
+ const stmts = builder("proj", "/root", "tx-1", 1000).buildFromParsedDoc(doc);
const p = stmts[0].params;
expect(p.relativePath).toBe("docs/guide.md");
expect(p.filePath).toBe("/workspace/docs/guide.md");
@@ -128,9 +122,7 @@ describe("DocsBuilder — DOCUMENT statement", () => {
describe("DocsBuilder — SECTION statement", () => {
it("SECTION statement uses MERGE", () => {
const stmts = builder().buildFromParsedDoc(makeDoc());
- const secStmt = stmts.find(
- (s) => s.query.includes("SECTION") && s.query.includes("heading"),
- )!;
+ const secStmt = stmts.find((s) => s.query.includes("SECTION") && s.query.includes("heading"))!;
expect(secStmt).toBeDefined();
expect(secStmt.query).toMatch(/MERGE.*SECTION/);
});
@@ -171,15 +163,10 @@ describe("DocsBuilder — SECTION statement", () => {
it("each section gets a unique id", () => {
const doc = makeDoc({
- sections: [
- makeSection({ index: 0, heading: "A" }),
- makeSection({ index: 1, heading: "B" }),
- ],
+ sections: [makeSection({ index: 0, heading: "A" }), makeSection({ index: 1, heading: "B" })],
});
const stmts = builder().buildFromParsedDoc(doc);
- const ids = stmts
- .filter((s) => s.query.includes("heading"))
- .map((s) => s.params.id);
+ const ids = stmts.filter((s) => s.query.includes("heading")).map((s) => s.params.id);
expect(new Set(ids).size).toBe(ids.length);
});
@@ -187,9 +174,7 @@ describe("DocsBuilder — SECTION statement", () => {
it("SECTION params include relativePath matching the parent document", () => {
const doc = makeDoc({ relativePath: "docs/architecture.md" });
const stmts = builder("p", "/r", "tx", 0).buildFromParsedDoc(doc);
- const secStmt = stmts.find(
- (s) => s.query.includes("s.relativePath") && s.params.relativePath,
- )!;
+ const secStmt = stmts.find((s) => s.query.includes("s.relativePath") && s.params.relativePath)!;
expect(secStmt).toBeDefined();
expect(secStmt.params.relativePath).toBe("docs/architecture.md");
});
@@ -225,9 +210,7 @@ describe("DocsBuilder — DOC_DESCRIBES edges", () => {
});
const doc = makeDoc({ sections: [section] });
const stmts = builder().buildFromParsedDoc(doc);
- const describeStmts = stmts.filter((s) =>
- s.query.includes("DOC_DESCRIBES"),
- );
+ const describeStmts = stmts.filter((s) => s.query.includes("DOC_DESCRIBES"));
// 2 refs × 2 match patterns (FILE + FUNCTION|CLASS) = 4 statements
expect(describeStmts.length).toBe(4);
});
@@ -244,9 +227,7 @@ describe("DocsBuilder — DOC_DESCRIBES edges", () => {
const section = makeSection({ backtickRefs: [] });
const doc = makeDoc({ sections: [section] });
const stmts = builder().buildFromParsedDoc(doc);
- const describeStmts = stmts.filter((s) =>
- s.query.includes("DOC_DESCRIBES"),
- );
+ const describeStmts = stmts.filter((s) => s.query.includes("DOC_DESCRIBES"));
expect(describeStmts).toHaveLength(0);
});
@@ -283,9 +264,7 @@ describe("DocsBuilder — idempotency (MERGE)", () => {
it("calling twice on same doc returns same number of stmts", () => {
const doc = makeDoc();
const b = builder();
- expect(b.buildFromParsedDoc(doc).length).toBe(
- b.buildFromParsedDoc(doc).length,
- );
+ expect(b.buildFromParsedDoc(doc).length).toBe(b.buildFromParsedDoc(doc).length);
});
});
@@ -301,27 +280,16 @@ describe("DocsBuilder — edge cases", () => {
});
it("handles doc with various kinds without throwing", () => {
- const kinds = [
- "readme",
- "adr",
- "changelog",
- "guide",
- "architecture",
- "other",
- ] as const;
+ const kinds = ["readme", "adr", "changelog", "guide", "architecture", "other"] as const;
for (const kind of kinds) {
- expect(() =>
- builder().buildFromParsedDoc(makeDoc({ kind })),
- ).not.toThrow();
+ expect(() => builder().buildFromParsedDoc(makeDoc({ kind }))).not.toThrow();
}
});
it("different relativePaths produce different DOCUMENT ids", () => {
const b = builder("p");
- const id1 = b.buildFromParsedDoc(makeDoc({ relativePath: "docs/a.md" }))[0]
- .params.id;
- const id2 = b.buildFromParsedDoc(makeDoc({ relativePath: "docs/b.md" }))[0]
- .params.id;
+ const id1 = b.buildFromParsedDoc(makeDoc({ relativePath: "docs/a.md" }))[0].params.id;
+ const id2 = b.buildFromParsedDoc(makeDoc({ relativePath: "docs/b.md" }))[0].params.id;
expect(id1).not.toBe(id2);
});
});
diff --git a/src/graph/__tests__/hybrid-retriever.test.ts b/src/graph/__tests__/hybrid-retriever.test.ts
new file mode 100644
index 0000000..1731a3c
--- /dev/null
+++ b/src/graph/__tests__/hybrid-retriever.test.ts
@@ -0,0 +1,127 @@
+import { describe, expect, it, vi } from "vitest";
+import GraphIndexManager from "../index.js";
+import { HybridRetriever } from "../hybrid-retriever.js";
+
+function seedIndex(): GraphIndexManager {
+ const index = new GraphIndexManager();
+ index.addNode("fn:1", "FUNCTION", {
+ name: "computeResult",
+ path: "src/core/compute.ts",
+ projectId: "proj-a",
+ summary: "Compute result",
+ });
+ index.addNode("class:1", "CLASS", {
+ name: "ResultBuilder",
+ path: "src/core/result-builder.ts",
+ projectId: "proj-a",
+ summary: "Builds result",
+ });
+ index.addNode("fn:2", "FUNCTION", {
+ name: "otherProject",
+ path: "src/other.ts",
+ projectId: "proj-b",
+ summary: "Other project",
+ });
+ return index;
+}
+
+describe("HybridRetriever", () => {
+ it("uses native bm25 when memgraph text search returns rows", async () => {
+ const memgraph = {
+ executeCypher: vi.fn().mockResolvedValue({ data: [{ nodeId: "fn:1", score: 5.2 }] }),
+ } as any;
+ const retriever = new HybridRetriever(seedIndex(), undefined, memgraph);
+
+ const result = await retriever.retrieve({
+ query: "compute",
+ projectId: "proj-a",
+ mode: "bm25",
+ limit: 5,
+ });
+
+ expect(retriever.bm25Mode).toBe("native");
+ expect(result).toHaveLength(1);
+ expect(result[0].nodeId).toBe("fn:1");
+ expect(result[0].scores.bm25).toBe(5.2);
+ });
+
+ // ── F2 regressions ───────────────────────────────────────────────────────────
+
+ it("F2: bm25IndexKnownToExist is false before ensureBM25Index() is called", () => {
+ const retriever = new HybridRetriever(seedIndex());
+ expect(retriever.bm25IndexKnownToExist).toBe(false);
+ });
+
+ it("F2: ensureBM25Index() sets bm25IndexKnownToExist=true when index already exists", async () => {
+ const memgraph = {
+ executeCypher: vi.fn().mockResolvedValue({
+ data: [{ name: "symbol_index" }, { name: "docs_index" }],
+ }),
+ } as any;
+ const retriever = new HybridRetriever(seedIndex(), undefined, memgraph);
+
+ const result = await retriever.ensureBM25Index();
+ expect(result.alreadyExists).toBe(true);
+ expect(retriever.bm25IndexKnownToExist).toBe(true);
+ });
+
+ it("F2: ensureBM25Index() sets bm25IndexKnownToExist=true when index is freshly created", async () => {
+ const memgraph = {
+ executeCypher: vi
+ .fn()
+ // list_indices returns nothing (no existing indices)
+ .mockResolvedValueOnce({ data: [] })
+ // create_index calls succeed
+ .mockResolvedValue({ data: [] }),
+ } as any;
+ const retriever = new HybridRetriever(seedIndex(), undefined, memgraph);
+
+ const result = await retriever.ensureBM25Index();
+ expect(result.created).toBe(true);
+ expect(retriever.bm25IndexKnownToExist).toBe(true);
+ });
+
+ it("F2: bm25IndexKnownToExist stays false when ensureBM25Index() errors", async () => {
+ const memgraph = {
+ executeCypher: vi.fn().mockRejectedValue(new Error("text_search module not loaded")),
+ } as any;
+ const retriever = new HybridRetriever(seedIndex(), undefined, memgraph);
+
+ const result = await retriever.ensureBM25Index();
+ expect(result.error).toBeDefined();
+ expect(retriever.bm25IndexKnownToExist).toBe(false);
+ });
+
+ it("F2: bm25Mode stays lexical_fallback even after ensureBM25Index() succeeds (index ≠ query success)", async () => {
+ const memgraph = {
+ executeCypher: vi
+ .fn()
+ .mockResolvedValue({ data: [{ name: "symbol_index" }, { name: "docs_index" }] }),
+ } as any;
+ const retriever = new HybridRetriever(seedIndex(), undefined, memgraph);
+ await retriever.ensureBM25Index();
+
+ // bm25IndexKnownToExist should be true (index confirmed present)
+ expect(retriever.bm25IndexKnownToExist).toBe(true);
+ // bm25Mode is still lexical_fallback — it only flips to "native" on successful query
+ expect(retriever.bm25Mode).toBe("lexical_fallback");
+ });
+
+ it("falls back to lexical search when memgraph bm25 fails", async () => {
+ const memgraph = {
+ executeCypher: vi.fn().mockRejectedValue(new Error("memgraph down")),
+ } as any;
+ const retriever = new HybridRetriever(seedIndex(), undefined, memgraph);
+
+ const result = await retriever.retrieve({
+ query: "result",
+ projectId: "proj-a",
+ mode: "bm25",
+ limit: 5,
+ });
+
+ expect(retriever.bm25Mode).toBe("lexical_fallback");
+ expect(result.length).toBeGreaterThan(0);
+ expect(result.every((row) => row.nodeId !== "fn:2")).toBe(true);
+ });
+});
diff --git a/src/graph/__tests__/index.test.ts b/src/graph/__tests__/index.test.ts
new file mode 100644
index 0000000..1235267
--- /dev/null
+++ b/src/graph/__tests__/index.test.ts
@@ -0,0 +1,65 @@
+import { describe, expect, it } from "vitest";
+import GraphIndexManager from "../index.js";
+
+describe("GraphIndexManager syncFrom", () => {
+ it("preserves existing node properties when overwrite is false", () => {
+ const index = new GraphIndexManager();
+
+ index.addNode("fn:sum", "FUNCTION", {
+ name: "sum",
+ summary: "old",
+ LOC: 10,
+ });
+
+ index.addNode(
+ "fn:sum",
+ "FUNCTION",
+ {
+ summary: "new",
+ LOC: 20,
+ },
+ false,
+ );
+
+ const node = index.getNode("fn:sum");
+ expect(node).toBeDefined();
+ expect(node!.properties.summary).toBe("old");
+ expect(node!.properties.LOC).toBe(10);
+ });
+
+ it("updates existing node properties when syncFrom is called", () => {
+ const target = new GraphIndexManager();
+ target.addNode("fn:sum", "FUNCTION", {
+ name: "sum",
+ summary: "old",
+ LOC: 10,
+ });
+
+ const source = new GraphIndexManager();
+ source.addNode("fn:sum", "FUNCTION", {
+ name: "sum",
+ summary: "new",
+ LOC: 20,
+ });
+
+ const result = target.syncFrom(source);
+
+ expect(result.nodesSynced).toBe(1);
+ const node = target.getNode("fn:sum");
+ expect(node).toBeDefined();
+ expect(node!.properties.summary).toBe("new");
+ expect(node!.properties.LOC).toBe(20);
+ expect(target.getStatistics().totalNodes).toBe(1);
+ });
+
+ it("can move a node to a different type when overwrite is enabled", () => {
+ const index = new GraphIndexManager();
+
+ index.addNode("entity:alpha", "FUNCTION", { name: "alpha" });
+ index.addNode("entity:alpha", "CLASS", { name: "AlphaClass" }, true);
+
+ expect(index.getNodesByType("FUNCTION")).toHaveLength(0);
+ expect(index.getNodesByType("CLASS")).toHaveLength(1);
+ expect(index.getNode("entity:alpha")?.type).toBe("CLASS");
+ });
+});
diff --git a/src/graph/__tests__/orchestrator.test.ts b/src/graph/__tests__/orchestrator.test.ts
new file mode 100644
index 0000000..c9e710c
--- /dev/null
+++ b/src/graph/__tests__/orchestrator.test.ts
@@ -0,0 +1,429 @@
+import * as fs from "fs";
+import * as os from "os";
+import * as path from "path";
+import { describe, expect, it, vi } from "vitest";
+import { GraphOrchestrator } from "../orchestrator.js";
+import GraphIndexManager from "../index.js";
+
+describe("GraphOrchestrator", () => {
+ it("normalizes incremental changed files and ignores unsupported extensions", async () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "orch-build-"));
+ const srcDir = path.join(root, "src");
+ fs.mkdirSync(srcDir, { recursive: true });
+
+ fs.writeFileSync(path.join(srcDir, "a.ts"), "export function alpha(): number { return 1; }\n");
+ fs.writeFileSync(path.join(srcDir, "note.txt"), "not a source file\n");
+
+ const memgraph = {
+ isConnected: vi.fn().mockReturnValue(false),
+ executeBatch: vi.fn().mockResolvedValue([]),
+ } as any;
+
+ const orchestrator = new GraphOrchestrator(memgraph, false);
+
+ const result = await orchestrator.build({
+ mode: "incremental",
+ workspaceRoot: root,
+ sourceDir: "src",
+ projectId: "proj-a",
+ changedFiles: ["src/a.ts", "src/note.txt"],
+ });
+
+ expect(result.success).toBe(true);
+ expect(result.filesChanged).toBe(1);
+ expect(result.filesProcessed).toBe(1);
+ expect(memgraph.executeBatch).not.toHaveBeenCalled();
+
+ fs.rmSync(root, { recursive: true, force: true });
+ });
+
+ it("dedupes changed files and ignores out-of-workspace paths", async () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "orch-scope-"));
+ const srcDir = path.join(root, "src");
+ fs.mkdirSync(srcDir, { recursive: true });
+
+ const inWorkspace = path.join(srcDir, "a.ts");
+ fs.writeFileSync(inWorkspace, "export const value = 1;\n");
+
+ const outsideRoot = fs.mkdtempSync(path.join(os.tmpdir(), "orch-outside-"));
+ const outsideFile = path.join(outsideRoot, "outside.ts");
+ fs.writeFileSync(outsideFile, "export const outside = 1;\n");
+
+ const memgraph = {
+ isConnected: vi.fn().mockReturnValue(false),
+ executeBatch: vi.fn().mockResolvedValue([]),
+ } as any;
+
+ const orchestrator = new GraphOrchestrator(memgraph, false);
+
+ const result = await orchestrator.build({
+ mode: "incremental",
+ workspaceRoot: root,
+ sourceDir: "src",
+ projectId: "proj-a",
+ changedFiles: ["src/a.ts", "src/a.ts", outsideFile],
+ });
+
+ expect(result.success).toBe(true);
+ expect(result.filesChanged).toBe(1);
+ expect(result.filesProcessed).toBe(1);
+
+ fs.rmSync(root, { recursive: true, force: true });
+ fs.rmSync(outsideRoot, { recursive: true, force: true });
+ });
+
+ // T17 — graph_health drift false-positive after rebuild (A2 regression)
+ // When sharedIndex is passed to GraphOrchestrator, build() must sync the
+ // internal index to sharedIndex so that graph_health sees cachedNodes > 0.
+ it("syncs internal index to sharedIndex after build (T17 / A2 regression)", async () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "orch-sync-"));
+ const srcDir = path.join(root, "src");
+ fs.mkdirSync(srcDir, { recursive: true });
+
+ fs.writeFileSync(
+ path.join(srcDir, "app.ts"),
+ "export function main(): void { console.log('hello'); }\n",
+ );
+
+ const memgraph = {
+ isConnected: vi.fn().mockReturnValue(false),
+ executeBatch: vi.fn().mockResolvedValue([]),
+ } as any;
+
+ const sharedIndex = new GraphIndexManager();
+ expect(sharedIndex.getStatistics().totalNodes).toBe(0);
+
+ const orchestrator = new GraphOrchestrator(memgraph, false, sharedIndex);
+
+ const result = await orchestrator.build({
+ mode: "full",
+ workspaceRoot: root,
+ sourceDir: "src",
+ projectId: "proj-sync",
+ });
+
+ expect(result.success).toBe(true);
+
+ // After build, sharedIndex must have been populated (not zero)
+ const stats = sharedIndex.getStatistics();
+ expect(
+ stats.totalNodes,
+ "sharedIndex.totalNodes must be > 0 after build — if 0, drift will always be reported",
+ ).toBeGreaterThan(0);
+
+ fs.rmSync(root, { recursive: true, force: true });
+ });
+});
+
+describe("GraphOrchestrator — two-phase execution", () => {
+ it("executeBatch is called twice — nodes first, then edges", async () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "orch-2phase-"));
+ const srcDir = path.join(root, "src");
+ fs.mkdirSync(srcDir, { recursive: true });
+ fs.writeFileSync(
+ path.join(srcDir, "hello.ts"),
+ 'export function hello(): string { return "hi"; }\n',
+ );
+
+ const memgraph = {
+ isConnected: vi.fn().mockReturnValue(true),
+ executeBatch: vi.fn().mockResolvedValue([]),
+ executeCypher: vi.fn().mockResolvedValue({ records: [] }),
+ beginBulkMode: vi.fn(),
+ endBulkMode: vi.fn(),
+ } as any;
+
+ const orchestrator = new GraphOrchestrator(memgraph, false);
+ await orchestrator.build({
+ mode: "full",
+ workspaceRoot: root,
+ sourceDir: "src",
+ projectId: "2ph1",
+ });
+
+ expect(memgraph.executeBatch).toHaveBeenCalledTimes(2);
+
+ // First call — nodes only (no relationship arrows)
+ const nodesArg: Array<{ query: string }> = memgraph.executeBatch.mock.calls[0][0];
+ for (const stmt of nodesArg) {
+ expect(stmt.query).not.toMatch(/-\[.*\]->/);
+ }
+
+ // Second call — edges (all have MATCH or relationship arrows)
+ const edgesArg: Array<{ query: string }> = memgraph.executeBatch.mock.calls[1][0];
+ for (const stmt of edgesArg) {
+ expect(stmt.query).toMatch(/MATCH|(-\[.*\]->)/);
+ }
+
+ fs.rmSync(root, { recursive: true, force: true });
+ });
+
+ it("beginBulkMode/endBulkMode wrap both execution phases", async () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "orch-bulk-"));
+ const srcDir = path.join(root, "src");
+ fs.mkdirSync(srcDir, { recursive: true });
+ fs.writeFileSync(
+ path.join(srcDir, "hello.ts"),
+ 'export function hello(): string { return "hi"; }\n',
+ );
+
+ const callOrder: string[] = [];
+
+ const memgraph = {
+ isConnected: vi.fn().mockReturnValue(true),
+ executeBatch: vi.fn().mockImplementation(async () => {
+ callOrder.push(
+ callOrder.filter((c) => c.startsWith("batch")).length === 0 ? "batch1" : "batch2",
+ );
+ return [];
+ }),
+ executeCypher: vi.fn().mockResolvedValue({ records: [] }),
+ beginBulkMode: vi.fn().mockImplementation(() => {
+ callOrder.push("begin");
+ }),
+ endBulkMode: vi.fn().mockImplementation(() => {
+ callOrder.push("end");
+ }),
+ } as any;
+
+ const orchestrator = new GraphOrchestrator(memgraph, false);
+ await orchestrator.build({
+ mode: "full",
+ workspaceRoot: root,
+ sourceDir: "src",
+ projectId: "2ph2",
+ });
+
+ expect(callOrder).toEqual(["begin", "batch1", "batch2", "end"]);
+
+ fs.rmSync(root, { recursive: true, force: true });
+ });
+
+ it("endBulkMode is called even if executeBatch throws", async () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "orch-throw-"));
+ const srcDir = path.join(root, "src");
+ fs.mkdirSync(srcDir, { recursive: true });
+ fs.writeFileSync(
+ path.join(srcDir, "hello.ts"),
+ 'export function hello(): string { return "hi"; }\n',
+ );
+
+ const memgraph = {
+ isConnected: vi.fn().mockReturnValue(true),
+ executeBatch: vi.fn().mockRejectedValue(new Error("batch boom")),
+ executeCypher: vi.fn().mockResolvedValue({ records: [] }),
+ beginBulkMode: vi.fn(),
+ endBulkMode: vi.fn(),
+ } as any;
+
+ const orchestrator = new GraphOrchestrator(memgraph, false);
+
+ try {
+ await orchestrator.build({
+ mode: "full",
+ workspaceRoot: root,
+ sourceDir: "src",
+ projectId: "2ph3",
+ });
+ } catch {
+ // expected — executeBatch throws
+ }
+
+ expect(memgraph.endBulkMode).toHaveBeenCalledTimes(1);
+
+ fs.rmSync(root, { recursive: true, force: true });
+ });
+});
+
+describe("GraphOrchestrator — test→symbol edges (Phase C)", () => {
+ it("creates TEST_SUITE→FUNCTION edge when import specifier matches function name", async () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "orch-pc1-"));
+ const srcDir = path.join(root, "src");
+ const testDir = path.join(root, "src", "__tests__");
+ fs.mkdirSync(testDir, { recursive: true });
+
+ fs.writeFileSync(
+ path.join(srcDir, "utils.ts"),
+ "export function computeHash(data: string): string { return data; }\n",
+ );
+ fs.writeFileSync(
+ path.join(testDir, "utils.test.ts"),
+ 'import { computeHash } from "../utils";\ndescribe("computeHash", () => { it("works", () => {}); });\n',
+ );
+
+ const memgraph = {
+ isConnected: vi.fn().mockReturnValue(true),
+ executeBatch: vi.fn().mockResolvedValue([]),
+ executeCypher: vi.fn().mockResolvedValue({ records: [] }),
+ beginBulkMode: vi.fn(),
+ endBulkMode: vi.fn(),
+ } as any;
+
+ const orchestrator = new GraphOrchestrator(memgraph, false);
+ const result = await orchestrator.build({
+ mode: "full",
+ workspaceRoot: root,
+ sourceDir: "src",
+ projectId: "pC1",
+ });
+
+ expect(result.success).toBe(true);
+ expect(memgraph.executeBatch).toHaveBeenCalledTimes(2);
+
+ const allEdges: Array<{ query: string }> = memgraph.executeBatch.mock.calls[1][0];
+ const hasSuiteToFunc = allEdges.some(
+ (s) =>
+ s.query.includes("TEST_SUITE") && s.query.includes("FUNCTION") && s.query.includes("TESTS"),
+ );
+ expect(hasSuiteToFunc).toBe(true);
+
+ fs.rmSync(root, { recursive: true, force: true });
+ });
+
+ it("creates TEST_SUITE→CLASS edge when import specifier matches class name", async () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "orch-pc2-"));
+ const srcDir = path.join(root, "src");
+ const testDir = path.join(root, "src", "__tests__");
+ fs.mkdirSync(testDir, { recursive: true });
+
+ fs.writeFileSync(
+ path.join(srcDir, "store.ts"),
+ 'export class DataStore { getValue(): string { return ""; } }\n',
+ );
+ fs.writeFileSync(
+ path.join(testDir, "store.test.ts"),
+ 'import { DataStore } from "../store";\ndescribe("DataStore", () => { it("tests", () => {}); });\n',
+ );
+
+ const memgraph = {
+ isConnected: vi.fn().mockReturnValue(true),
+ executeBatch: vi.fn().mockResolvedValue([]),
+ executeCypher: vi.fn().mockResolvedValue({ records: [] }),
+ beginBulkMode: vi.fn(),
+ endBulkMode: vi.fn(),
+ } as any;
+
+ const orchestrator = new GraphOrchestrator(memgraph, false);
+ const result = await orchestrator.build({
+ mode: "full",
+ workspaceRoot: root,
+ sourceDir: "src",
+ projectId: "pC2",
+ });
+
+ expect(result.success).toBe(true);
+ expect(memgraph.executeBatch).toHaveBeenCalledTimes(2);
+
+ const allEdges: Array<{ query: string }> = memgraph.executeBatch.mock.calls[1][0];
+ const hasSuiteToClass = allEdges.some(
+ (s) =>
+ s.query.includes("TEST_SUITE") && s.query.includes("CLASS") && s.query.includes("TESTS"),
+ );
+ expect(hasSuiteToClass).toBe(true);
+
+ fs.rmSync(root, { recursive: true, force: true });
+ });
+
+ it("always creates TEST_SUITE→FILE edge alongside symbol edges", async () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "orch-pc3-"));
+ const srcDir = path.join(root, "src");
+ const testDir = path.join(root, "src", "__tests__");
+ fs.mkdirSync(testDir, { recursive: true });
+
+ fs.writeFileSync(
+ path.join(srcDir, "utils.ts"),
+ "export function computeHash(data: string): string { return data; }\n",
+ );
+ fs.writeFileSync(
+ path.join(testDir, "utils.test.ts"),
+ 'import { computeHash } from "../utils";\ndescribe("computeHash", () => { it("works", () => {}); });\n',
+ );
+
+ const memgraph = {
+ isConnected: vi.fn().mockReturnValue(true),
+ executeBatch: vi.fn().mockResolvedValue([]),
+ executeCypher: vi.fn().mockResolvedValue({ records: [] }),
+ beginBulkMode: vi.fn(),
+ endBulkMode: vi.fn(),
+ } as any;
+
+ const orchestrator = new GraphOrchestrator(memgraph, false);
+ const result = await orchestrator.build({
+ mode: "full",
+ workspaceRoot: root,
+ sourceDir: "src",
+ projectId: "pC3",
+ });
+
+ expect(result.success).toBe(true);
+ expect(memgraph.executeBatch).toHaveBeenCalledTimes(2);
+
+ const allEdges: Array<{ query: string }> = memgraph.executeBatch.mock.calls[1][0];
+ const hasSuiteToFile = allEdges.some(
+ (s) =>
+ s.query.includes("TEST_SUITE") && s.query.includes(":FILE") && s.query.includes("TESTS"),
+ );
+ const hasSuiteToFunc = allEdges.some(
+ (s) =>
+ s.query.includes("TEST_SUITE") &&
+ s.query.includes(":FUNCTION") &&
+ s.query.includes("TESTS"),
+ );
+ expect(hasSuiteToFile).toBe(true);
+ expect(hasSuiteToFunc).toBe(true);
+
+ fs.rmSync(root, { recursive: true, force: true });
+ });
+
+ it("creates TEST_CASE→FUNCTION edge for individual test cases", async () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "orch-pc4-"));
+ const srcDir = path.join(root, "src");
+ const testDir = path.join(root, "src", "__tests__");
+ fs.mkdirSync(testDir, { recursive: true });
+
+ fs.writeFileSync(
+ path.join(srcDir, "validator.ts"),
+ "export function validate(x: number): boolean { return x > 0; }\n",
+ );
+ fs.writeFileSync(
+ path.join(testDir, "validator.test.ts"),
+ 'import { validate } from "../validator";\ndescribe("validate", () => { it("returns true for positive", () => { validate(1); }); });\n',
+ );
+
+ const memgraph = {
+ isConnected: vi.fn().mockReturnValue(true),
+ executeBatch: vi.fn().mockResolvedValue([]),
+ executeCypher: vi.fn().mockResolvedValue({ records: [] }),
+ beginBulkMode: vi.fn(),
+ endBulkMode: vi.fn(),
+ } as any;
+
+ const orchestrator = new GraphOrchestrator(memgraph, false);
+ const result = await orchestrator.build({
+ mode: "full",
+ workspaceRoot: root,
+ sourceDir: "src",
+ projectId: "pC4",
+ });
+
+ expect(result.success).toBe(true);
+ expect(memgraph.executeBatch).toHaveBeenCalledTimes(2);
+
+ const allEdges: Array<{ query: string }> = memgraph.executeBatch.mock.calls[1][0];
+ const hasCaseToFunc = allEdges.some(
+ (s) =>
+ s.query.includes("TEST_CASE") && s.query.includes("FUNCTION") && s.query.includes("TESTS"),
+ );
+
+ // The TypeScript parser may or may not produce testCases for this simple file.
+ // If it does, we expect the edge. If not, the build still succeeded without error.
+ if (!hasCaseToFunc) {
+ // Acceptable — parser did not emit testCases for this file
+ expect(result.success).toBe(true);
+ } else {
+ expect(hasCaseToFunc).toBe(true);
+ }
+
+ fs.rmSync(root, { recursive: true, force: true });
+ });
+});
diff --git a/src/graph/__tests__/ppr.test.ts b/src/graph/__tests__/ppr.test.ts
new file mode 100644
index 0000000..0c30d95
--- /dev/null
+++ b/src/graph/__tests__/ppr.test.ts
@@ -0,0 +1,405 @@
+// ── Personalized PageRank (PPR) — Tests ─────────────────────────────────────
+//
+// Tests for:
+// - runPPR() → empty seeds guard, MAGE mode, JS fallback mode
+// - tryMagePPR() → tested indirectly: MAGE success / error / throw
+// - runJsPPR() → tested indirectly: edge propagation, seed boosting
+//
+// MemgraphClient is mocked via vi.fn(). executeCypher is keyed on query fragments.
+
+import { describe, expect, it, vi } from "vitest";
+import { runPPR } from "../ppr.js";
+
+// ── Helpers ─────────────────────────────────────────────────────────────────
+
+/**
+ * Builds a MemgraphClient mock where executeCypher returns pre-set data
+ * based on matching a substring of the query.
+ */
+function makeMockClient(
+ overrides: Record = {},
+) {
+ return {
+ executeCypher: vi.fn(async (query: string) => {
+ for (const [key, val] of Object.entries(overrides)) {
+ if (query.includes(key)) {
+ if (typeof val === "object" && "error" in val && !Array.isArray(val)) {
+ return val;
+ }
+ return { data: val };
+ }
+ }
+ return { data: [] };
+ }),
+ } as any;
+}
+
+/** Standard pagerank row returned by MAGE. */
+function makePagerankRow(nodeId: string, rank = 0.5, type = "FILE") {
+ return {
+ nodeId,
+ rank,
+ type,
+ filePath: `src/${nodeId}.ts`,
+ name: nodeId,
+ };
+}
+
+/** Standard edge row returned by the JS-PPR MATCH query. */
+function makeEdgeRow(
+ fromId: string,
+ toId: string,
+ relType = "IMPORTS",
+ opts: Record = {},
+) {
+ return {
+ fromId,
+ toId,
+ relType,
+ fromType: "FILE",
+ toType: "FILE",
+ fromPath: `src/${fromId}.ts`,
+ toPath: `src/${toId}.ts`,
+ fromName: fromId,
+ toName: toId,
+ ...opts,
+ };
+}
+
+// ── Empty seeds guard ────────────────────────────────────────────────────────
+
+describe("runPPR() — empty seeds", () => {
+ it("returns [] immediately when seedIds is empty", async () => {
+ const client = makeMockClient();
+ const result = await runPPR({ seedIds: [], projectId: "proj-a" }, client);
+ expect(result).toEqual([]);
+ expect(client.executeCypher).not.toHaveBeenCalled();
+ });
+
+ it("returns [] immediately when seedIds contains only falsy values", async () => {
+ const client = makeMockClient();
+ const result = await runPPR({ seedIds: ["", ""] as string[], projectId: "proj-a" }, client);
+ expect(result).toEqual([]);
+ });
+
+ it("deduplicates seed ids", async () => {
+ // Two identical seeds reduce to one
+ const client = makeMockClient({
+ "pagerank.get()": [makePagerankRow("file:a")],
+ "UNWIND $seedIds": [{ nodeId: "file:a", hops: 1 }],
+ });
+ const result = await runPPR({ seedIds: ["file:a", "file:a"], projectId: "proj-a" }, client);
+ expect(result.length).toBeGreaterThanOrEqual(1);
+ });
+});
+
+// ── MAGE mode ────────────────────────────────────────────────────────────────
+
+describe("runPPR() — MAGE pagerank mode", () => {
+ it("uses mage_pagerank mode when MAGE query returns data", async () => {
+ const client = makeMockClient({
+ "pagerank.get()": [makePagerankRow("file:a", 0.8), makePagerankRow("file:b", 0.3)],
+ "UNWIND $seedIds": [],
+ });
+
+ const result = await runPPR({ seedIds: ["file:a"], projectId: "proj-a" }, client);
+
+ expect(result.every((r) => r.pprMode === "mage_pagerank")).toBe(true);
+ });
+
+ it("includes seed nodes with high proximity boost", async () => {
+ const client = makeMockClient({
+ "pagerank.get()": [makePagerankRow("file:seed", 0.1)],
+ "UNWIND $seedIds": [],
+ });
+
+ const result = await runPPR({ seedIds: ["file:seed"], projectId: "proj-a" }, client);
+
+ const seedItem = result.find((r) => r.nodeId === "file:seed");
+ expect(seedItem).toBeDefined();
+ // Seed gets proximity=2.0 boost so final score = 0.1*(1-0.85) + 2.0*0.85 > 1.5
+ expect(seedItem!.score).toBeGreaterThan(1.0);
+ });
+
+ it("applies proximity scores from hop-distance query", async () => {
+ const client = makeMockClient({
+ "pagerank.get()": [
+ makePagerankRow("file:seed", 0.5),
+ makePagerankRow("file:neighbor1", 0.4),
+ makePagerankRow("file:neighbor2", 0.3),
+ ],
+ "UNWIND $seedIds": [
+ { nodeId: "file:neighbor1", hops: 1 }, // hop 1 → proximity 1.0
+ { nodeId: "file:neighbor2", hops: 3 }, // hop 3 → proximity 0.3
+ ],
+ });
+
+ const result = await runPPR({ seedIds: ["file:seed"], projectId: "proj-a" }, client);
+
+ const n1 = result.find((r) => r.nodeId === "file:neighbor1");
+ const n2 = result.find((r) => r.nodeId === "file:neighbor2");
+ // Closer neighbor should score higher
+ if (n1 && n2) {
+ expect(n1.score).toBeGreaterThan(n2.score);
+ }
+ });
+
+ it("sorts results by score descending", async () => {
+ const client = makeMockClient({
+ "pagerank.get()": [
+ makePagerankRow("file:low", 0.1),
+ makePagerankRow("file:high", 0.9),
+ makePagerankRow("file:mid", 0.5),
+ ],
+ "UNWIND $seedIds": [],
+ });
+
+ const result = await runPPR({ seedIds: ["file:high"], projectId: "proj-a" }, client);
+
+ for (let i = 1; i < result.length; i++) {
+ expect(result[i - 1].score).toBeGreaterThanOrEqual(result[i].score);
+ }
+ });
+
+ it("limits results to maxResults", async () => {
+ const rows = Array.from({ length: 20 }, (_, i) => makePagerankRow(`file:${i}`, 0.5 - i / 100));
+ const client = makeMockClient({
+ "pagerank.get()": rows,
+ "UNWIND $seedIds": [],
+ });
+
+ const result = await runPPR(
+ { seedIds: ["file:0"], projectId: "proj-a", maxResults: 5 },
+ client,
+ );
+
+ expect(result).toHaveLength(5);
+ });
+
+ it("caps maxResults at 500", async () => {
+ const rows = Array.from({ length: 10 }, (_, i) => makePagerankRow(`file:${i}`));
+ const client = makeMockClient({
+ "pagerank.get()": rows,
+ "UNWIND $seedIds": [],
+ });
+
+ const result = await runPPR(
+ { seedIds: ["file:0"], projectId: "proj-a", maxResults: 9999 },
+ client,
+ );
+
+ // Can't exceed actual data count
+ expect(result.length).toBeLessThanOrEqual(500);
+ });
+
+ it("includes type, filePath, name from pagerank metadata", async () => {
+ const client = makeMockClient({
+ "pagerank.get()": [
+ {
+ nodeId: "fn:doWork",
+ rank: 0.7,
+ type: "FUNCTION",
+ filePath: "src/engines/ep.ts",
+ name: "doWork",
+ },
+ ],
+ "UNWIND $seedIds": [],
+ });
+
+ const [result] = await runPPR({ seedIds: ["fn:doWork"], projectId: "proj-a" }, client);
+
+ expect(result.type).toBe("FUNCTION");
+ expect(result.filePath).toBe("src/engines/ep.ts");
+ expect(result.name).toBe("doWork");
+ });
+});
+
+// ── MAGE fallback to JS PPR ───────────────────────────────────────────────────
+
+describe("runPPR() — JS PPR fallback", () => {
+ it("falls back to JS mode when MAGE returns empty data", async () => {
+ // pagerank.get() key not in overrides → returns empty → triggers JS fallback
+ const client = makeMockClient({
+ "MATCH (a)-[r]->(b)": [makeEdgeRow("file:a", "file:b")],
+ });
+
+ const result = await runPPR({ seedIds: ["file:a"], projectId: "proj-a" }, client);
+
+ expect(result.every((r) => r.pprMode === "js_ppr")).toBe(true);
+ });
+
+ it("falls back to JS mode when MAGE query has error", async () => {
+ const client = makeMockClient({
+ "pagerank.get()": { error: "MAGE not available", data: [] },
+ "MATCH (a)-[r]->(b)": [makeEdgeRow("file:a", "file:b")],
+ });
+
+ const result = await runPPR({ seedIds: ["file:a"], projectId: "proj-a" }, client);
+
+ expect(result.every((r) => r.pprMode === "js_ppr")).toBe(true);
+ });
+
+ it("falls back to JS PPR when MAGE throws", async () => {
+ const client = {
+ executeCypher: vi.fn().mockImplementation(async (query: string) => {
+ if (query.includes("pagerank.get()")) {
+ throw new Error("MAGE not installed");
+ }
+ if (query.includes("MATCH (a)-[r]->(b)")) {
+ return { data: [makeEdgeRow("file:a", "file:b")] };
+ }
+ return { data: [] };
+ }),
+ } as any;
+
+ const result = await runPPR({ seedIds: ["file:a"], projectId: "proj-a" }, client);
+
+ expect(result.some((r) => r.pprMode === "js_ppr")).toBe(true);
+ });
+
+ it("JS PPR: seed node has non-zero score when no edges exist", async () => {
+ const client = makeMockClient(); // no edges → seed gets personalization weight
+
+ const result = await runPPR({ seedIds: ["seed-node"], projectId: "proj-a" }, client);
+
+ // seed-node should appear in results even with no edges
+ const seed = result.find((r) => r.nodeId === "seed-node");
+ expect(seed).toBeDefined();
+ expect(seed!.pprMode).toBe("js_ppr");
+ });
+
+ it("JS PPR: propagates scores through IMPORTS edges", async () => {
+ const client = makeMockClient({
+ "MATCH (a)-[r]->(b)": [
+ makeEdgeRow("file:a", "file:b", "IMPORTS"),
+ makeEdgeRow("file:b", "file:c", "IMPORTS"),
+ ],
+ });
+
+ const result = await runPPR(
+ { seedIds: ["file:a"], projectId: "proj-a", iterations: 10 },
+ client,
+ );
+
+ expect(result).toHaveLength(3);
+ expect(result.every((r) => r.pprMode === "js_ppr")).toBe(true);
+ });
+
+ it("JS PPR: uses default edge weights for known relationship types", async () => {
+ // CALLS has weight 0.9, a high-weight edge propagates more rank
+ const client = makeMockClient({
+ "MATCH (a)-[r]->(b)": [
+ makeEdgeRow("file:seed", "file:called", "CALLS"),
+ makeEdgeRow("file:seed", "file:tested", "TESTS"), // weight 0.4
+ ],
+ });
+
+ const result = await runPPR(
+ { seedIds: ["file:seed"], projectId: "proj-a", iterations: 5 },
+ client,
+ );
+
+ const called = result.find((r) => r.nodeId === "file:called");
+ const tested = result.find((r) => r.nodeId === "file:tested");
+
+ if (called && tested) {
+ // file:called (CALLS=0.9) should rank higher than file:tested (TESTS=0.4)
+ expect(called.score).toBeGreaterThan(tested.score);
+ }
+ });
+
+ it("JS PPR: sorts output by score descending", async () => {
+ const client = makeMockClient({
+ "MATCH (a)-[r]->(b)": [
+ makeEdgeRow("file:a", "file:b", "IMPORTS"),
+ makeEdgeRow("file:a", "file:c", "TESTS"),
+ ],
+ });
+
+ const result = await runPPR(
+ { seedIds: ["file:a"], projectId: "proj-a", iterations: 20 },
+ client,
+ );
+
+ for (let i = 1; i < result.length; i++) {
+ expect(result[i - 1].score).toBeGreaterThanOrEqual(result[i].score);
+ }
+ });
+
+ it("JS PPR: respects custom edge weights from opts.edgeWeights", async () => {
+ const client = makeMockClient({
+ "MATCH (a)-[r]->(b)": [makeEdgeRow("file:a", "file:b", "CUSTOM_REL")],
+ });
+
+ const result = await runPPR(
+ {
+ seedIds: ["file:a"],
+ projectId: "proj-a",
+ edgeWeights: { CUSTOM_REL: 0.99 },
+ iterations: 5,
+ },
+ client,
+ );
+
+ expect(result.find((r) => r.nodeId === "file:b")).toBeDefined();
+ });
+
+ it("JS PPR: limits results to maxResults", async () => {
+ const edges = Array.from({ length: 10 }, (_, i) => makeEdgeRow("file:seed", `file:${i}`));
+ const client = makeMockClient({ "MATCH (a)-[r]->(b)": edges });
+
+ const result = await runPPR(
+ { seedIds: ["file:seed"], projectId: "proj-a", maxResults: 3 },
+ client,
+ );
+
+ expect(result).toHaveLength(3);
+ });
+});
+
+// ── Option clamping ───────────────────────────────────────────────────────────
+
+describe("runPPR() — option clamping", () => {
+ it("clamps iterations to [1, 100]", async () => {
+ // With 0 iterations, the rank should still be initialized uniformly
+ const client = makeMockClient({
+ "MATCH (a)-[r]->(b)": [makeEdgeRow("file:a", "file:b")],
+ });
+
+ // iterations=0 clamped to 1
+ const result = await runPPR(
+ { seedIds: ["file:a"], projectId: "proj-a", iterations: 0 },
+ client,
+ );
+
+ expect(result.length).toBeGreaterThan(0);
+ });
+
+ it("uses default damping=0.85 when not specified", async () => {
+ const client = makeMockClient({
+ "pagerank.get()": [makePagerankRow("file:a", 0.5)],
+ "UNWIND $seedIds": [],
+ });
+
+ const [result] = await runPPR({ seedIds: ["file:a"], projectId: "proj-a" }, client);
+
+ // With damping=0.85 and rank=0.5: score = 0.5*(1-0.85) + 2.0*0.85 = 0.075 + 1.7 = 1.775
+ expect(result.score).toBeCloseTo(1.775, 2);
+ });
+
+ it("all scores are finite non-negative numbers", async () => {
+ const client = makeMockClient({
+ "MATCH (a)-[r]->(b)": [
+ makeEdgeRow("file:a", "file:b", "IMPORTS"),
+ makeEdgeRow("file:b", "file:c", "CALLS"),
+ ],
+ });
+
+ const result = await runPPR({ seedIds: ["file:a"], projectId: "proj-a" }, client);
+
+ for (const r of result) {
+ expect(Number.isFinite(r.score)).toBe(true);
+ expect(r.score).toBeGreaterThanOrEqual(0);
+ }
+ });
+});
diff --git a/src/graph/__tests__/watcher.test.ts b/src/graph/__tests__/watcher.test.ts
new file mode 100644
index 0000000..d1a21ad
--- /dev/null
+++ b/src/graph/__tests__/watcher.test.ts
@@ -0,0 +1,89 @@
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { FileWatcher } from "../watcher.js";
+
+describe("FileWatcher", () => {
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it("debounces multiple file events into a single batch", async () => {
+ vi.useFakeTimers();
+ const onBatch = vi.fn().mockResolvedValue(undefined);
+
+ const watcher = new FileWatcher(
+ {
+ projectId: "proj-a",
+ workspaceRoot: "/tmp/workspace",
+ sourceDir: "src",
+ debounceMs: 100,
+ },
+ onBatch,
+ );
+
+ (watcher as any).queue("src/a.ts");
+ (watcher as any).queue("src/b.ts");
+ (watcher as any).queue("src/a.ts");
+
+ expect(watcher.state).toBe("debouncing");
+ expect(watcher.pendingChanges).toBe(2);
+
+ await vi.advanceTimersByTimeAsync(101);
+
+ expect(onBatch).toHaveBeenCalledTimes(1);
+ expect(onBatch).toHaveBeenCalledWith({
+ projectId: "proj-a",
+ workspaceRoot: "/tmp/workspace",
+ sourceDir: "src",
+ changedFiles: ["src/a.ts", "src/b.ts"],
+ });
+ expect(watcher.state).toBe("idle");
+ expect(watcher.pendingChanges).toBe(0);
+
+ await watcher.stop();
+ });
+
+ it("schedules a follow-up flush when files change during processing", async () => {
+ vi.useFakeTimers();
+
+ let resolveFirst: (() => void) | undefined;
+ const firstDone = new Promise((resolve) => {
+ resolveFirst = resolve;
+ });
+
+ const onBatch = vi
+ .fn()
+ .mockImplementationOnce(() => firstDone)
+ .mockResolvedValueOnce(undefined);
+
+ const watcher = new FileWatcher(
+ {
+ projectId: "proj-a",
+ workspaceRoot: "/tmp/workspace",
+ sourceDir: "src",
+ debounceMs: 50,
+ },
+ onBatch,
+ );
+
+ (watcher as any).queue("src/first.ts");
+ await vi.advanceTimersByTimeAsync(51);
+
+ expect(watcher.state).toBe("rebuilding");
+ expect(onBatch).toHaveBeenCalledTimes(1);
+
+ (watcher as any).queue("src/second.ts");
+ expect(watcher.pendingChanges).toBe(1);
+
+ resolveFirst?.();
+ await Promise.resolve();
+
+ expect(watcher.state).toBe("debouncing");
+ await vi.advanceTimersByTimeAsync(51);
+
+ expect(onBatch).toHaveBeenCalledTimes(2);
+ expect(onBatch.mock.calls[1][0].changedFiles).toEqual(["src/second.ts"]);
+ expect(watcher.state).toBe("idle");
+
+ await watcher.stop();
+ });
+});
diff --git a/src/graph/builder.test.ts b/src/graph/builder.test.ts
deleted file mode 100644
index cb02e79..0000000
--- a/src/graph/builder.test.ts
+++ /dev/null
@@ -1,171 +0,0 @@
-import { describe, expect, it } from "vitest";
-import * as path from "node:path";
-import { GraphBuilder } from "./builder.js";
-import type { ParsedFile } from "./builder.js";
-
-// ─── Helpers ──────────────────────────────────────────────────────────────────
-
-const WORKSPACE = "/workspace";
-
-function makeFile(overrides: Partial = {}): ParsedFile {
- return {
- path: "/workspace/src/components/App.tsx",
- filePath: "/workspace/src/components/App.tsx",
- relativePath: "src/components/App.tsx",
- language: "TypeScript",
- LOC: 50,
- hash: "abc123",
- functions: [],
- classes: [],
- variables: [],
- imports: [],
- exports: [],
- ...overrides,
- };
-}
-
-function makeImport(source: string): { id: string; source: string; specifiers: string[]; startLine: number; summary: null } {
- return {
- id: `import-${source}`,
- source,
- specifiers: [],
- startLine: 1,
- summary: null,
- };
-}
-
-function builder(projectId = "test-proj", workspaceRoot = WORKSPACE) {
- return new GraphBuilder(projectId, workspaceRoot, "tx-001", 1700000000000);
-}
-
-// ─── T15 — FILE.path must always be absolute ──────────────────────────────────
-
-describe("GraphBuilder — FILE path normalization (A1 regression)", () => {
- it("T15: import target FILE.path is absolute even when resolvedPath is relative", () => {
- const workspaceRoot = "/workspace";
- const b = builder("proj", workspaceRoot);
-
- // File A imports from B using a relative source reference
- // The import source will resolve to src/lib/utils.ts (relative)
- const fileA = makeFile({
- path: "/workspace/src/components/App.tsx",
- filePath: "/workspace/src/components/App.tsx",
- relativePath: "src/components/App.tsx",
- imports: [
- // This resolves to src/lib/utils.ts (relative to workspaceRoot)
- makeImport("../lib/utils"),
- ],
- } as any);
-
- const stmts = b.buildFromParsedFile(fileA);
-
- // Find all FILE node MERGE statements that set targetFile.path
- const filePathStmts = stmts.filter(
- (s) =>
- s.query.includes("targetFile:FILE") &&
- s.params.absoluteTargetPath !== undefined,
- );
-
- for (const stmt of filePathStmts) {
- const p = stmt.params.absoluteTargetPath as string;
- expect(
- path.isAbsolute(p),
- `Expected absolute path but got: ${p}`,
- ).toBe(true);
- expect(p).toContain(workspaceRoot);
- }
- });
-
- it("T15b: createFileNode FILE.path is always the absolute filePath", () => {
- const b = builder("proj", "/workspace");
- const fileA = makeFile({
- filePath: "/workspace/src/components/App.tsx",
- relativePath: "src/components/App.tsx",
- });
-
- const stmts = b.buildFromParsedFile(fileA);
-
- // The canonical FILE node (from createFileNode) must have absolute path
- const fileNodeStmt = stmts.find(
- (s) =>
- s.query.includes("MERGE (f:FILE") &&
- s.query.includes("f.path = $path"),
- )!;
- expect(fileNodeStmt).toBeDefined();
- expect(path.isAbsolute(fileNodeStmt.params.path as string)).toBe(true);
- expect(fileNodeStmt.params.path).toBe("/workspace/src/components/App.tsx");
- });
-
- it("T16: FILE.id for nested file contains full relative path", () => {
- const b = builder("proj", "/workspace");
- const fileA = makeFile({
- filePath: "/workspace/src/components/App.tsx",
- relativePath: "src/components/App.tsx",
- imports: [makeImport("../controls/ArchitectureControls")],
- });
-
- const stmts = b.buildFromParsedFile(fileA);
-
- // Find the stub FILE node created for the import target
- const stubStmt = stmts.find(
- (s) =>
- s.query.includes("targetFile:FILE") &&
- String(s.params.targetId || "").includes("ArchitectureControls"),
- );
-
- if (stubStmt) {
- const targetId = String(stubStmt.params.targetId);
- // Must include the full relative path (not just the basename)
- expect(targetId).toMatch(/controls\/ArchitectureControls/);
- expect(targetId).not.toMatch(/^proj:file:ArchitectureControls/);
- }
- });
-
- it("relativePath property in stub FILE is the original relative path", () => {
- const b = builder("proj", "/workspace");
- const fileA = makeFile({
- filePath: "/workspace/src/components/App.tsx",
- relativePath: "src/components/App.tsx",
- imports: [makeImport("../lib/utils")],
- });
-
- const stmts = b.buildFromParsedFile(fileA);
-
- const stubStmt = stmts.find(
- (s) =>
- s.query.includes("targetFile:FILE") &&
- s.params.relativePath !== undefined,
- );
-
- if (stubStmt) {
- const rel = stubStmt.params.relativePath as string;
- // relativePath must be relative (not start with /)
- expect(path.isAbsolute(rel)).toBe(false);
- expect(rel).toContain("utils");
- }
- });
-
- it("absolute and relative paths are consistent: resolve(workspaceRoot, relativePath) == absolutePath", () => {
- const workspaceRoot = "/workspace";
- const b = builder("proj", workspaceRoot);
- const fileA = makeFile({
- filePath: "/workspace/src/components/App.tsx",
- relativePath: "src/components/App.tsx",
- imports: [makeImport("../lib/helper")],
- });
-
- const stmts = b.buildFromParsedFile(fileA);
-
- const stubStmt = stmts.find(
- (s) =>
- s.query.includes("targetFile:FILE") &&
- s.params.absoluteTargetPath !== undefined,
- );
-
- if (stubStmt) {
- const absPath = stubStmt.params.absoluteTargetPath as string;
- const relPath = stubStmt.params.relativePath as string;
- expect(absPath).toBe(path.resolve(workspaceRoot, relPath));
- }
- });
-});
diff --git a/src/graph/builder.ts b/src/graph/builder.ts
index 2f43964..3b3ff1e 100644
--- a/src/graph/builder.ts
+++ b/src/graph/builder.ts
@@ -1,4 +1,20 @@
+/**
+ * @file graph/builder
+ * @description Translates parsed source artifacts into Cypher statements for graph persistence.
+ * @remarks Declares local parsed-file contracts to avoid parser dependency coupling.
+ */
+
// Local type definitions (avoid importing from typescript-parser which has dependencies)
+
+/** Minimal symbol descriptor shared by parser adapters */
+export interface ParsedSymbol {
+ name: string;
+ type: string;
+ startLine: number;
+ endLine: number;
+ [key: string]: unknown;
+}
+
export interface ParsedFile {
path: string;
filePath: string;
@@ -11,7 +27,7 @@ export interface ParsedFile {
exports: Array<{ name: string; type: string }>;
functions: FunctionNode[];
classes: ClassNode[];
- variables?: any[];
+ variables?: ParsedSymbol[];
testSuites?: Array<{
id: string;
name: string;
@@ -48,7 +64,7 @@ interface FunctionNode {
interface ClassNode {
id: string;
name: string;
- methods: Array<{ name: string; parameters: any[]; returnType?: string }>;
+ methods: Array<{ name: string; parameters: ParsedSymbol[]; returnType?: string }>;
properties: Array<{ name: string; type?: string }>;
line: number;
implements?: string[];
@@ -64,16 +80,24 @@ interface ClassNode {
import * as path from "path";
import { existsSync } from "fs";
import * as env from "../env.js";
+import { computeProjectFingerprint } from "../utils/validation.js";
export interface CypherStatement {
query: string;
params: Record;
}
+export interface BuildResult {
+ nodes: CypherStatement[];
+ edges: CypherStatement[];
+}
+
export class GraphBuilder {
- private statements: CypherStatement[] = [];
+ private nodeStmts: CypherStatement[] = [];
+ private edgeStmts: CypherStatement[] = [];
private processedNodes = new Set();
private projectId: string;
+ private projectFingerprint: string;
private workspaceRoot: string;
private txId: string;
private txTimestamp: number;
@@ -83,12 +107,14 @@ export class GraphBuilder {
workspaceRoot?: string,
txId?: string,
txTimestamp?: number,
+ _projectFingerprint?: string,
) {
- this.workspaceRoot =
- workspaceRoot || env.LXRAG_WORKSPACE_ROOT || process.cwd();
- this.projectId =
- projectId || env.LXRAG_PROJECT_ID || path.basename(this.workspaceRoot);
- this.txId = txId || env.LXRAG_TX_ID || `tx-${Date.now()}`;
+ this.workspaceRoot = workspaceRoot || env.LXDIG_WORKSPACE_ROOT || process.cwd();
+ // Always use the 4-char hash fingerprint as canonical projectId.
+ // Fallback computes it directly from workspaceRoot to guarantee consistency.
+ this.projectId = projectId || computeProjectFingerprint(this.workspaceRoot);
+ this.projectFingerprint = this.projectId;
+ this.txId = txId || env.LXDIG_TX_ID || `tx-${Date.now()}`;
this.txTimestamp = txTimestamp || Date.now();
}
@@ -120,8 +146,7 @@ export class GraphBuilder {
private fileNodeId(parsedFile: ParsedFile): string {
const relativePath =
- parsedFile.relativePath ||
- path.relative(this.workspaceRoot, parsedFile.filePath);
+ parsedFile.relativePath || path.relative(this.workspaceRoot, parsedFile.filePath);
return this.scopedId(`file:${relativePath}`);
}
@@ -133,46 +158,38 @@ export class GraphBuilder {
return this.scopedId(`folder:${folderPath}`);
}
- buildFromParsedFile(parsedFile: ParsedFile): CypherStatement[] {
- this.statements = [];
+ buildFromParsedFile(parsedFile: ParsedFile): BuildResult {
+ this.nodeStmts = [];
+ this.edgeStmts = [];
this.processedNodes.clear();
// Create FILE node
this.createFileNode(parsedFile);
// Create FUNCTION nodes and relationships
- parsedFile.functions.forEach((fn) =>
- this.createFunctionNode(fn, parsedFile),
- );
+ parsedFile.functions.forEach((fn) => this.createFunctionNode(fn, parsedFile));
// Create CLASS nodes and relationships
parsedFile.classes.forEach((cls) => this.createClassNode(cls, parsedFile));
// Create VARIABLE nodes
- parsedFile.variables?.forEach((variable) =>
- this.createVariableNode(variable, parsedFile),
- );
+ parsedFile.variables?.forEach((variable) => this.createVariableNode(variable, parsedFile));
// Create IMPORT nodes and relationships
- parsedFile.imports?.forEach((imp) =>
- this.createImportNode(imp, parsedFile),
- );
+ parsedFile.imports?.forEach((imp) => this.createImportNode(imp, parsedFile));
// Create EXPORT nodes
- parsedFile.exports?.forEach((exp) =>
- this.createExportNode(exp, parsedFile),
- );
+ parsedFile.exports?.forEach((exp) => this.createExportNode(exp, parsedFile));
// Create TEST_SUITE nodes (if this is a test file)
this.buildTestNodes(parsedFile);
- return this.statements;
+ return { nodes: this.nodeStmts, edges: this.edgeStmts };
}
private createFileNode(parsedFile: ParsedFile): void {
const relativePath =
- parsedFile.relativePath ||
- path.relative(this.workspaceRoot, parsedFile.filePath);
+ parsedFile.relativePath || path.relative(this.workspaceRoot, parsedFile.filePath);
const nodeId = this.fileNodeId(parsedFile);
if (this.processedNodes.has(nodeId)) return;
this.processedNodes.add(nodeId);
@@ -189,6 +206,7 @@ export class GraphBuilder {
f.relativePath = $relativePath,
f.scipId = $scipId,
f.projectId = $projectId,
+ f.projectFingerprint = $projectFingerprint,
f.validFrom = $validFrom,
f.validTo = $validTo,
f.createdAt = $createdAt,
@@ -206,20 +224,21 @@ export class GraphBuilder {
relativePath: relativePath,
scipId: this.toScipId("file", relativePath),
projectId: this.projectId,
+ projectFingerprint: this.projectFingerprint,
validFrom: this.txTimestamp,
validTo: null,
createdAt: this.txTimestamp,
txId: this.txId,
},
};
- this.statements.push(statement);
+ this.nodeStmts.push(statement);
// Create folder hierarchy
const folderPath = path.dirname(parsedFile.filePath);
this.createFolderHierarchy(folderPath);
// Connect FILE to FOLDER
- this.statements.push({
+ this.edgeStmts.push({
query: `
MATCH (f:FILE {id: $fileId})
MERGE (folder:FOLDER {id: $folderId})
@@ -241,7 +260,7 @@ export class GraphBuilder {
if (this.processedNodes.has(nodeId)) return;
this.processedNodes.add(nodeId);
- this.statements.push({
+ this.nodeStmts.push({
query: `
MERGE (folder:FOLDER {id: $id})
SET folder.name = $name,
@@ -261,7 +280,7 @@ export class GraphBuilder {
this.createFolderHierarchy(parentPath);
// Connect parent to child
- this.statements.push({
+ this.edgeStmts.push({
query: `
MATCH (parent:FOLDER {id: $parentId})
MATCH (child:FOLDER {id: $childId})
@@ -276,17 +295,18 @@ export class GraphBuilder {
}
private createFunctionNode(fn: FunctionNode, parsedFile: ParsedFile): void {
- const nodeId = this.scopedId(
- fn.id || `func:${parsedFile.relativePath}:${fn.name}:${fn.line}`,
- );
+ const nodeId = this.scopedId(fn.id || `func:${parsedFile.relativePath}:${fn.name}:${fn.line}`);
if (this.processedNodes.has(nodeId)) return;
this.processedNodes.add(nodeId);
- this.statements.push({
+ this.nodeStmts.push({
query: `
MERGE (func:FUNCTION {id: $id})
SET func.name = $name,
func.kind = $kind,
+ func.filePath = $filePath,
+ func.path = $path,
+ func.relativePath = $relativePath,
func.startLine = $startLine,
func.endLine = $endLine,
func.LOC = $LOC,
@@ -303,6 +323,9 @@ export class GraphBuilder {
id: nodeId,
name: fn.name,
kind: fn.kind || "function",
+ filePath: parsedFile.filePath,
+ path: parsedFile.filePath,
+ relativePath: parsedFile.relativePath || parsedFile.filePath,
startLine: fn.startLine || fn.line || 0,
endLine: fn.endLine || fn.line || 0,
LOC: fn.LOC || 1,
@@ -324,7 +347,7 @@ export class GraphBuilder {
});
// Connect function to file
- this.statements.push({
+ this.edgeStmts.push({
query: `
MATCH (func:FUNCTION {id: $funcId})
MATCH (f:FILE {id: $fileId})
@@ -338,7 +361,7 @@ export class GraphBuilder {
// Tag as exported if applicable
if (fn.isExported) {
- this.statements.push({
+ this.edgeStmts.push({
query: `
MATCH (func:FUNCTION {id: $id})
SET func.isExported = true
@@ -346,6 +369,30 @@ export class GraphBuilder {
params: { id: nodeId },
});
}
+
+ // CALLS_TO edges — one per call site extracted by the parser
+ const calls: Array<{ name: string; line: number }> = (fn as any).calls ?? [];
+ for (const call of calls) {
+ const calleeStubId = this.scopedId(`func-stub:${call.name}`);
+ this.edgeStmts.push({
+ query: `
+ MERGE (stub:FUNCTION {id: $calleeId})
+ ON CREATE SET stub.name = $calleeName,
+ stub.projectId = $projectId,
+ stub.stub = true
+ WITH stub
+ MATCH (caller:FUNCTION {id: $callerId})
+ MERGE (caller)-[:CALLS_TO {line: $line}]->(stub)
+ `,
+ params: {
+ calleeId: calleeStubId,
+ calleeName: call.name,
+ callerId: nodeId,
+ projectId: this.projectId,
+ line: call.line,
+ },
+ });
+ }
}
private createClassNode(cls: ClassNode, parsedFile: ParsedFile): void {
@@ -353,11 +400,14 @@ export class GraphBuilder {
if (this.processedNodes.has(nodeId)) return;
this.processedNodes.add(nodeId);
- this.statements.push({
+ this.nodeStmts.push({
query: `
MERGE (cls:CLASS {id: $id})
SET cls.name = $name,
cls.kind = $kind,
+ cls.filePath = $filePath,
+ cls.path = $path,
+ cls.relativePath = $relativePath,
cls.startLine = $startLine,
cls.endLine = $endLine,
cls.LOC = $LOC,
@@ -373,6 +423,9 @@ export class GraphBuilder {
id: nodeId,
name: cls.name,
kind: cls.kind || "class",
+ filePath: parsedFile.filePath,
+ path: parsedFile.filePath,
+ relativePath: parsedFile.relativePath || parsedFile.filePath,
startLine: cls.startLine || cls.line,
endLine: cls.endLine || cls.line,
LOC: cls.LOC || 1,
@@ -387,7 +440,7 @@ export class GraphBuilder {
});
// Connect class to file
- this.statements.push({
+ this.edgeStmts.push({
query: `
MATCH (cls:CLASS {id: $classId})
MATCH (f:FILE {id: $fileId})
@@ -401,7 +454,7 @@ export class GraphBuilder {
// Handle inheritance
if (cls.extends) {
- this.statements.push({
+ this.edgeStmts.push({
query: `
MATCH (cls:CLASS {id: $classId})
MERGE (parent:CLASS {id: $parentId})
@@ -421,7 +474,7 @@ export class GraphBuilder {
// Handle implementations
if (cls.implements) {
cls.implements.forEach((impl) => {
- this.statements.push({
+ this.edgeStmts.push({
query: `
MATCH (cls:CLASS {id: $classId})
MERGE (iface:CLASS {id: $ifaceId})
@@ -440,7 +493,7 @@ export class GraphBuilder {
}
if (cls.isExported) {
- this.statements.push({
+ this.edgeStmts.push({
query: `
MATCH (cls:CLASS {id: $id})
SET cls.isExported = true
@@ -450,12 +503,17 @@ export class GraphBuilder {
}
}
- private createVariableNode(variable: any, parsedFile: ParsedFile): void {
- const nodeId = this.scopedId(variable.id);
+ private createVariableNode(
+ variable: ParsedSymbol & { id?: string; kind?: string },
+ parsedFile: ParsedFile,
+ ): void {
+ const nodeId = this.scopedId(
+ (variable.id as string | undefined) ?? `var:${parsedFile.relativePath}:${variable.name}`,
+ );
if (this.processedNodes.has(nodeId)) return;
this.processedNodes.add(nodeId);
- this.statements.push({
+ this.nodeStmts.push({
query: `
MERGE (var:VARIABLE {id: $id})
SET var.name = $name,
@@ -467,7 +525,7 @@ export class GraphBuilder {
params: {
id: nodeId,
name: variable.name,
- kind: variable.kind,
+ kind: String(variable.kind ?? ""),
startLine: variable.startLine,
type: variable.type || null,
projectId: this.projectId,
@@ -475,7 +533,7 @@ export class GraphBuilder {
});
// Connect to file
- this.statements.push({
+ this.edgeStmts.push({
query: `
MATCH (var:VARIABLE {id: $varId})
MATCH (f:FILE {id: $fileId})
@@ -488,12 +546,21 @@ export class GraphBuilder {
});
}
- private createImportNode(imp: any, parsedFile: ParsedFile): void {
- const nodeId = this.scopedId(imp.id);
+ private createImportNode(
+ imp: {
+ source: string;
+ specifiers?: string[];
+ summary?: string;
+ id?: string;
+ startLine?: number;
+ },
+ parsedFile: ParsedFile,
+ ): void {
+ const nodeId = this.scopedId(imp.id ?? `import:${parsedFile.relativePath}:${imp.source}`);
if (this.processedNodes.has(nodeId)) return;
this.processedNodes.add(nodeId);
- this.statements.push({
+ this.nodeStmts.push({
query: `
MERGE (imp:IMPORT {id: $id})
SET imp.source = $source,
@@ -521,7 +588,7 @@ export class GraphBuilder {
});
// Connect to file
- this.statements.push({
+ this.edgeStmts.push({
query: `
MATCH (imp:IMPORT {id: $impId})
MATCH (f:FILE {id: $fileId})
@@ -534,24 +601,26 @@ export class GraphBuilder {
});
// Try to resolve the imported module
- const resolvedPath = this.resolveImportPath(
- imp.source,
- path.dirname(parsedFile.filePath),
- );
+ const resolvedPath = this.resolveImportPath(imp.source, path.dirname(parsedFile.filePath));
if (resolvedPath) {
// resolvedPath is relative to workspaceRoot; compute absolute path so
// that FILE.path is always absolute, consistent with createFileNode.
const absoluteTargetPath = path.resolve(this.workspaceRoot, resolvedPath);
- this.statements.push({
+ // Single query: MERGE targetFile, wire REFERENCES, and DEPENDS_ON atomically.
+ // Using one statement avoids the MATCH-visibility race between separate executeCypher calls.
+ this.edgeStmts.push({
query: `
+ MATCH (sourceFile:FILE {id: $sourceFileId})
MATCH (imp:IMPORT {id: $impId})
MERGE (targetFile:FILE {id: $targetId})
SET targetFile.path = $absoluteTargetPath,
targetFile.relativePath = $relativePath,
targetFile.projectId = $projectId
MERGE (imp)-[:REFERENCES]->(targetFile)
+ MERGE (sourceFile)-[:DEPENDS_ON]->(targetFile)
`,
params: {
+ sourceFileId: this.fileNodeId(parsedFile),
impId: nodeId,
targetId: this.fileNodeIdFromRelative(resolvedPath),
absoluteTargetPath,
@@ -562,12 +631,15 @@ export class GraphBuilder {
}
}
- private createExportNode(exp: any, parsedFile: ParsedFile): void {
- const nodeId = this.scopedId(exp.id);
+ private createExportNode(
+ exp: { name: string; type?: string; id?: string; startLine?: number; isDefault?: boolean },
+ parsedFile: ParsedFile,
+ ): void {
+ const nodeId = this.scopedId(exp.id ?? `export:${parsedFile.relativePath}:${exp.name}`);
if (this.processedNodes.has(nodeId)) return;
this.processedNodes.add(nodeId);
- this.statements.push({
+ this.nodeStmts.push({
query: `
MERGE (exp:EXPORT {id: $id})
SET exp.name = $name,
@@ -577,15 +649,15 @@ export class GraphBuilder {
`,
params: {
id: nodeId,
- name: exp.name,
- isDefault: exp.isDefault,
+ name: String(exp.name ?? ""),
+ isDefault: exp.isDefault ?? false,
startLine: exp.startLine,
projectId: this.projectId,
},
});
// Connect to file
- this.statements.push({
+ this.edgeStmts.push({
query: `
MATCH (exp:EXPORT {id: $expId})
MATCH (f:FILE {id: $fileId})
@@ -604,8 +676,7 @@ export class GraphBuilder {
if (testSuites.length === 0 && testCases.length === 0) return;
const relativePath =
- parsedFile.relativePath ||
- path.relative(this.workspaceRoot, parsedFile.filePath);
+ parsedFile.relativePath || path.relative(this.workspaceRoot, parsedFile.filePath);
// Create TEST_SUITE nodes
testSuites.forEach((suite) => {
@@ -614,7 +685,7 @@ export class GraphBuilder {
this.processedNodes.add(nodeId);
// Create TEST_SUITE node
- this.statements.push({
+ this.nodeStmts.push({
query: `
MERGE (ts:TEST_SUITE {id: $id})
SET ts.name = $name,
@@ -638,7 +709,7 @@ export class GraphBuilder {
});
// Create FILE -[:CONTAINS]-> TEST_SUITE relationship
- this.statements.push({
+ this.edgeStmts.push({
query: `
MATCH (f:FILE {id: $fileId})
MATCH (ts:TEST_SUITE {id: $testSuiteId})
@@ -652,13 +723,13 @@ export class GraphBuilder {
});
// Phase 3.1: Create individual TEST_CASE nodes
- testCases.forEach((testCase: any) => {
+ testCases.forEach((testCase) => {
const nodeId = this.scopedId(`test_case:${testCase.id}`);
if (this.processedNodes.has(nodeId)) return;
this.processedNodes.add(nodeId);
// Create TEST_CASE node
- this.statements.push({
+ this.nodeStmts.push({
query: `
MERGE (tc:TEST_CASE {id: $id})
SET tc.name = $name,
@@ -680,7 +751,7 @@ export class GraphBuilder {
// Create TEST_SUITE -[:CONTAINS]-> TEST_CASE relationship (if parent suite exists)
if (testCase.parentSuiteId) {
const parentNodeId = this.scopedId(`test_suite:${testCase.parentSuiteId}`);
- this.statements.push({
+ this.edgeStmts.push({
query: `
MATCH (ts:TEST_SUITE {id: $testSuiteId})
MATCH (tc:TEST_CASE {id: $testCaseId})
@@ -694,7 +765,7 @@ export class GraphBuilder {
}
// Create FILE -[:CONTAINS]-> TEST_CASE relationship
- this.statements.push({
+ this.edgeStmts.push({
query: `
MATCH (f:FILE {id: $fileId})
MATCH (tc:TEST_CASE {id: $testCaseId})
@@ -710,8 +781,12 @@ export class GraphBuilder {
private resolveImportPath(source: string, fromDir: string): string | null {
if (!source.startsWith(".")) return null; // skip node_modules / bare specifiers
- const base = path.resolve(fromDir, source);
+ // TypeScript projects often emit .js/.jsx imports (moduleResolution: node16/bundler).
+ // Strip the JS extension so we can probe the actual .ts/.tsx source file on disk.
+ const normalizedSource = source.replace(/\.jsx?$/, "");
+ const base = path.resolve(fromDir, normalizedSource);
const candidates = [
+ base, // exact match (source had no extension or was already .ts)
base + ".ts",
base + ".tsx",
path.join(base, "index.ts"),
diff --git a/src/graph/cache.ts b/src/graph/cache.ts
index 88bd74f..e5ce05c 100644
--- a/src/graph/cache.ts
+++ b/src/graph/cache.ts
@@ -1,5 +1,12 @@
+/**
+ * @file graph/cache
+ * @description Maintains file hash metadata for incremental graph rebuild decisions.
+ * @remarks Cache storage is filesystem-backed and scoped to the runtime workspace.
+ */
+
import * as fs from "fs";
import * as path from "path";
+import { logger } from "../utils/logger.js";
export interface CacheEntry {
path: string;
@@ -22,7 +29,7 @@ export class CacheManager {
private cachePath: string;
private cache: CacheData;
- constructor(cacheDir: string = ".lxrag/cache") {
+ constructor(cacheDir: string = ".lxdig/cache") {
this.cachePath = path.join(process.cwd(), cacheDir, "file-hashes.json");
this.cache = this.loadCache();
}
@@ -34,7 +41,7 @@ export class CacheManager {
return JSON.parse(data);
}
} catch (error) {
- console.warn(`[CacheManager] Failed to load cache: ${error}`);
+ logger.warn(`[CacheManager] Failed to load cache: ${error}`);
}
return {
@@ -56,7 +63,7 @@ export class CacheManager {
this.cache.lastBuild = Date.now();
fs.writeFileSync(this.cachePath, JSON.stringify(this.cache, null, 2));
} catch (error) {
- console.error(`[CacheManager] Failed to save cache: ${error}`);
+ logger.error(`[CacheManager] Failed to save cache: ${error}`);
}
}
@@ -92,13 +99,8 @@ export class CacheManager {
/**
* Get all changed files since last build
*/
- getChangedFiles(
- files: Array<{ path: string; hash: string; LOC: number }>,
- ): string[] {
+ getChangedFiles(files: Array<{ path: string; hash: string; LOC: number }>): string[] {
const changed: string[] = [];
- // @ts-expect-error - now will be used for timestamp comparison
- const now = Date.now();
-
for (const file of files) {
const entry = this.get(file.path);
if (!entry || entry.hash !== file.hash) {
diff --git a/src/graph/client.ts b/src/graph/client.ts
index b8c0f4c..8a8cbc9 100644
--- a/src/graph/client.ts
+++ b/src/graph/client.ts
@@ -1,6 +1,13 @@
+/**
+ * @file graph/client
+ * @description Memgraph client wrapper for Cypher execution and connection lifecycle.
+ * @remarks Provides resilient query utilities used across graph and engine modules.
+ */
+
import type { CypherStatement } from "./types";
import neo4j from "neo4j-driver";
import * as env from "../env.js";
+import { logger } from "../utils/logger.js";
export interface MemgraphConfig {
host: string;
@@ -10,18 +17,88 @@ export interface MemgraphConfig {
}
export interface QueryResult {
- data: any[];
+ data: Record[];
error?: string;
}
+// ── Retry / resilience constants ─────────────────────────────────────────────
+
+/** Delays (ms) between successive retry attempts: 100 → 400 → 1600 ms. */
+const BACKOFF_INTERVALS_MS = [100, 400, 1600] as const;
+
+/**
+ * Number of consecutive query errors that open the circuit breaker.
+ * Once open, all queries short-circuit immediately until the cooldown expires.
+ */
+const CIRCUIT_BREAKER_THRESHOLD = 5;
+
+/**
+ * Elevated circuit-breaker threshold used during bulk write operations (rebuild).
+ * A single slow flush can produce several transient errors without being a real outage.
+ * Sized to support projects up to ~700 files (~40k statements / 1500-stmt chunks ≈ 27 chunks).
+ */
+const CIRCUIT_BREAKER_BULK_THRESHOLD = 50;
+
+/** Milliseconds the circuit stays open before entering half-open state. */
+const CIRCUIT_BREAKER_COOLDOWN_MS = 20_000;
+
+/** Interval for background liveness pings while connected (ms). */
+const HEALTH_CHECK_INTERVAL_MS = 30_000;
+
+/**
+ * Number of statements per chunk in {@link MemgraphClient.executeBatchInChunks}.
+ * Each chunk opens exactly one session and runs inside one explicit transaction,
+ * reducing total session open/close cycles from O(N statements) to O(N/BULK_CHUNK_SIZE).
+ *
+ * 1500 keeps chunk count well under CIRCUIT_BREAKER_BULK_THRESHOLD for projects
+ * up to ~700 files (~40k statements → 27 chunks vs threshold 50).
+ */
+const BULK_CHUNK_SIZE = 1500;
+
/**
- * Memgraph client for executing Cypher queries
- * Uses neo4j-driver with Bolt protocol (compatible with Memgraph)
+ * executeBatch delegates to executeBatchInChunks when the payload exceeds this size.
+ * Below the threshold the original sequential loop is used to keep small batches simple.
+ */
+const BULK_THRESHOLD = 50;
+
+/** Sleep helper used for exponential backoff between retries. */
+function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+/**
+ * Memgraph client for executing Cypher queries.
+ *
+ * Resilience features:
+ * - **3-retry with exponential backoff** (100ms → 400ms → 1600ms) for
+ * transient errors (ServiceUnavailable, session expired, connection lost).
+ * - **Circuit breaker** — after 5 consecutive failures the circuit opens
+ * and all queries fail fast for 30 s, then auto-resets to half-open.
+ * - **Periodic health check** — background ping every 30 s while connected;
+ * marks client as disconnected if the ping fails so the next `executeCypher`
+ * call triggers a reconnect.
*/
export class MemgraphClient {
private config: MemgraphConfig;
- private driver: any;
+ private driver: any; // neo4j.Driver, but avoid tight coupling in type definitions
private connected = false;
+ private readonly queryRetryAttempts = 3;
+
+ // ── Circuit breaker state ─────────────────────────────────────────────────
+
+ private consecutiveFailures = 0;
+ private circuitOpen = false;
+ private circuitOpenAt = 0;
+
+ /**
+ * When true the circuit-breaker threshold is raised to CIRCUIT_BREAKER_BULK_THRESHOLD.
+ * Set via {@link beginBulkMode} / {@link endBulkMode} around large write operations.
+ */
+ private bulkModeActive = false;
+
+ // ── Health check handle ───────────────────────────────────────────────────
+
+ private healthCheckHandle: NodeJS.Timeout | null = null;
constructor(config: Partial = {}) {
this.config = {
@@ -34,8 +111,7 @@ export class MemgraphClient {
this.driver = this.createDriver(this.config.host);
const boltUrl = `bolt://${this.config.host}:${this.config.port}`;
-
- console.log(`[MemgraphClient] Initialized with Bolt URL:`, boltUrl);
+ logger.info("[MemgraphClient] Initialized", { boltUrl });
}
async connect(): Promise {
@@ -45,10 +121,12 @@ export class MemgraphClient {
await session.run("RETURN 1");
await session.close();
this.connected = true;
- console.log("[Memgraph] Connected successfully via Bolt protocol");
+ this.resetCircuitBreaker();
+ logger.info("[Memgraph] Connected successfully via Bolt protocol");
+ this.startHealthCheck();
} catch (error) {
if (this.shouldFallbackToLocalhost(error)) {
- console.warn(
+ logger.warn(
`[Memgraph] Host '${this.config.host}' is not resolvable from this runtime. Retrying with localhost...`,
);
@@ -60,11 +138,13 @@ export class MemgraphClient {
await session.run("RETURN 1");
await session.close();
this.connected = true;
- console.log("[Memgraph] Connected successfully via Bolt protocol");
+ this.resetCircuitBreaker();
+ logger.info("[Memgraph] Connected successfully via Bolt protocol");
+ this.startHealthCheck();
return;
}
- console.error("[Memgraph] Connection failed:", error);
+ logger.error("[Memgraph] Connection failed", error);
this.connected = false;
throw error;
}
@@ -77,11 +157,10 @@ export class MemgraphClient {
this.config.password || "",
);
- // Phase 4.6: Use configurable connection pool settings
return neo4j.driver(boltUrl, authToken, {
- maxConnectionPoolSize: env.LXRAG_MEMGRAPH_MAX_POOL_SIZE,
- connectionAcquisitionTimeout: env.LXRAG_MEMGRAPH_CONNECTION_TIMEOUT_MS,
- connectionLivenessCheckTimeout: env.LXRAG_MEMGRAPH_LIVENESS_TIMEOUT_MS,
+ maxConnectionPoolSize: env.LXDIG_MEMGRAPH_MAX_POOL_SIZE,
+ connectionAcquisitionTimeout: env.LXDIG_MEMGRAPH_CONNECTION_TIMEOUT_MS,
+ connectionLivenessCheckTimeout: env.LXDIG_MEMGRAPH_LIVENESS_TIMEOUT_MS,
});
}
@@ -94,22 +173,126 @@ export class MemgraphClient {
return message.includes("ENOTFOUND");
}
+ // ── Circuit breaker ───────────────────────────────────────────────────────
+
+ private resetCircuitBreaker(): void {
+ this.consecutiveFailures = 0;
+ this.circuitOpen = false;
+ this.circuitOpenAt = 0;
+ }
+
+ /**
+ * Returns true when the circuit is currently open (fast-fail mode).
+ * Transitions from open → half-open after the cooldown expires.
+ */
+ private isCircuitOpen(): boolean {
+ if (!this.circuitOpen) return false;
+ const elapsed = Date.now() - this.circuitOpenAt;
+ if (elapsed >= CIRCUIT_BREAKER_COOLDOWN_MS) {
+ // Half-open: allow one probe request through
+ logger.info("[Memgraph] Circuit breaker half-open — probing...");
+ this.circuitOpen = false;
+ return false;
+ }
+ return true;
+ }
+
+ private recordQuerySuccess(): void {
+ this.consecutiveFailures = 0;
+ if (this.circuitOpen) this.circuitOpen = false;
+ }
+
+ private recordQueryFailure(): void {
+ this.consecutiveFailures += 1;
+ const threshold = this.bulkModeActive
+ ? CIRCUIT_BREAKER_BULK_THRESHOLD
+ : CIRCUIT_BREAKER_THRESHOLD;
+ if (this.consecutiveFailures >= threshold) {
+ this.circuitOpen = true;
+ this.circuitOpenAt = Date.now();
+ logger.error("[Memgraph] Circuit breaker OPENED — too many consecutive failures", {
+ threshold,
+ bulkMode: this.bulkModeActive,
+ cooldownMs: CIRCUIT_BREAKER_COOLDOWN_MS,
+ });
+ }
+ }
+
+ // ── Bulk-mode helpers ─────────────────────────────────────────────────────
+
+ /**
+ * Elevates the circuit-breaker failure threshold for the duration of a bulk
+ * write (e.g. a full graph rebuild). Call {@link endBulkMode} in a finally block.
+ */
+ beginBulkMode(): void {
+ this.bulkModeActive = true;
+ logger.info("[Memgraph] Bulk mode enabled — CB threshold raised to", {
+ threshold: CIRCUIT_BREAKER_BULK_THRESHOLD,
+ });
+ }
+
+ /** Restores the normal circuit-breaker threshold after a bulk write. */
+ endBulkMode(): void {
+ this.bulkModeActive = false;
+ logger.info("[Memgraph] Bulk mode disabled — CB threshold restored", {
+ threshold: CIRCUIT_BREAKER_THRESHOLD,
+ });
+ }
+
+ // ── Periodic health check ─────────────────────────────────────────────────
+
+ private startHealthCheck(): void {
+ if (this.healthCheckHandle) return; // already running
+ this.healthCheckHandle = setInterval(async () => {
+ try {
+ const session = this.driver.session();
+ await session.run("RETURN 1");
+ await session.close();
+ // Silent success — no log spam on healthy ping
+ } catch (error) {
+ const msg = error instanceof Error ? error.message : String(error);
+ logger.warn("[Memgraph] Health check failed — marking as disconnected", { cause: msg });
+ this.connected = false;
+ this.stopHealthCheck();
+ }
+ }, HEALTH_CHECK_INTERVAL_MS);
+
+ // Don't hold the Node.js event loop open just for the health check
+ if (this.healthCheckHandle.unref) {
+ this.healthCheckHandle.unref();
+ }
+ }
+
+ private stopHealthCheck(): void {
+ if (this.healthCheckHandle) {
+ clearInterval(this.healthCheckHandle);
+ this.healthCheckHandle = null;
+ }
+ }
+
+ // ── Public methods ────────────────────────────────────────────────────────
+
async disconnect(): Promise {
+ this.stopHealthCheck();
if (this.driver) {
await this.driver.close();
this.connected = false;
- console.log("[Memgraph] Disconnected");
+ logger.info("[Memgraph] Disconnected");
}
}
- async executeCypher(
- query: string,
- params: Record = {},
- ): Promise {
+ async executeCypher(query: string, params: Record = {}): Promise {
+ // ── Circuit breaker fast-fail ─────────────────────────────────────────
+ if (this.isCircuitOpen()) {
+ return {
+ data: [],
+ error: "Circuit breaker open — Memgraph unavailable, retrying after cooldown",
+ };
+ }
+
+ // ── Lazy connect ──────────────────────────────────────────────────────
if (!this.connected) {
- console.warn(
- "[Memgraph] Not connected - attempting to connect before executing query",
- );
+ logger.warn("[Memgraph] Not connected - attempting to connect before executing query");
try {
await this.connect();
} catch (error) {
@@ -120,55 +303,182 @@ export class MemgraphClient {
}
}
- const session = this.driver.session();
- try {
- // Sanitize params: replace undefined with null (Bolt requires explicit null)
- const sanitizedParams = Object.fromEntries(
- Object.entries(params).map(([k, v]) => [k, v === undefined ? null : v]),
- );
+ // Sanitize params: replace undefined with null (Bolt requires explicit null)
+ const sanitizedParams = Object.fromEntries(
+ Object.entries(params).map(([k, v]) => [k, v === undefined ? null : v]),
+ );
- const result = await session.run(query, sanitizedParams);
- const data = result.records.map((record: any) => record.toObject());
+ // ── Retry loop with exponential backoff ───────────────────────────────
+ for (let attempt = 0; attempt <= this.queryRetryAttempts; attempt++) {
+ if (attempt > 0) {
+ const delayMs = BACKOFF_INTERVALS_MS[attempt - 1] ?? 1600;
+ logger.warn("[Memgraph] Retrying query after backoff", {
+ attempt,
+ maxAttempts: this.queryRetryAttempts,
+ delayMs,
+ });
+ await sleep(delayMs);
+ }
- return {
- data,
- error: undefined,
- };
- } catch (error) {
- const errorMsg = error instanceof Error ? error.message : String(error);
- console.error("[Memgraph] Query execution error:", errorMsg);
- console.error("[Memgraph] Error in query:", query.substring(0, 200));
- return {
- data: [],
- error: `Query failed: ${errorMsg}`,
- };
- } finally {
- await session.close();
+ const session = this.driver.session();
+ try {
+ const result = await session.run(query, sanitizedParams);
+ const data = result.records.map((record: { toObject(): Record }) =>
+ record.toObject(),
+ );
+
+ this.recordQuerySuccess();
+ return { data, error: undefined };
+ } catch (error) {
+ const errorMsg = error instanceof Error ? error.message : String(error);
+ const canRetry = attempt < this.queryRetryAttempts && this.isRetryableQueryError(error);
+
+ if (canRetry) {
+ logger.warn("[Memgraph] Transient query error, will retry", {
+ attempt: attempt + 1,
+ maxAttempts: this.queryRetryAttempts,
+ cause: errorMsg,
+ });
+ continue;
+ }
+
+ this.recordQueryFailure();
+ logger.error("[Memgraph] Query execution failed", {
+ cause: errorMsg,
+ query: query.substring(0, 200),
+ });
+ return { data: [], error: `Query failed: ${errorMsg}` };
+ } finally {
+ await session.close();
+ }
}
+
+ this.recordQueryFailure();
+ return { data: [], error: "Query failed: exhausted retry attempts" };
}
+ private isRetryableQueryError(error: unknown): boolean {
+ const message = error instanceof Error ? error.message : String(error);
+ const normalized = message.toLowerCase();
+ return (
+ normalized.includes("serviceunavailable") ||
+ normalized.includes("session expired") ||
+ normalized.includes("connection") ||
+ normalized.includes("temporarily unavailable")
+ );
+ }
+
+ /**
+ * Execute a large batch of Cypher statements efficiently.
+ *
+ * Statements are grouped into chunks of {@link BULK_CHUNK_SIZE}. Each chunk
+ * shares a single driver session and runs inside one explicit transaction,
+ * reducing session-open/close overhead from O(N) to O(N / BULK_CHUNK_SIZE).
+ *
+ * On transaction failure the chunk falls back to individual {@link executeCypher}
+ * calls so partial progress is always preserved.
+ *
+ * @param statements - Cypher statements to execute.
+ * @param chunkSize - Override chunk size (default: {@link BULK_CHUNK_SIZE}).
+ */
+ async executeBatchInChunks(
+ statements: CypherStatement[],
+ chunkSize = BULK_CHUNK_SIZE,
+ ): Promise {
+ if (statements.length === 0) return [];
+
+ const results: QueryResult[] = new Array(statements.length);
+ let totalFailed = 0;
+
+ for (let offset = 0; offset < statements.length; offset += chunkSize) {
+ const chunk = statements.slice(offset, offset + chunkSize);
+ const session = this.driver.session();
+ let committedOk = false;
+
+ try {
+ const tx = session.beginTransaction();
+ try {
+ for (const { query, params } of chunk) {
+ const sanitized = Object.fromEntries(
+ Object.entries(params ?? {}).map(([k, v]) => [k, v === undefined ? null : v]),
+ );
+ await tx.run(query, sanitized);
+ }
+ await tx.commit();
+ committedOk = true;
+ this.recordQuerySuccess();
+ // All statements in the chunk succeeded — fill with empty-data results.
+ for (let j = 0; j < chunk.length; j++) {
+ results[offset + j] = { data: [] };
+ }
+ } catch (txError) {
+ const msg = txError instanceof Error ? txError.message : String(txError);
+ logger.warn(
+ "[Memgraph] Chunk transaction failed, falling back to per-statement execution",
+ {
+ chunkOffset: offset,
+ chunkSize: chunk.length,
+ cause: msg,
+ },
+ );
+ await tx.rollback().catch(() => {});
+ this.recordQueryFailure();
+ }
+ } finally {
+ await session.close().catch(() => {});
+ }
+
+ if (!committedOk) {
+ // Fall back: run each statement individually so we preserve as much data as possible.
+ for (let j = 0; j < chunk.length; j++) {
+ const r = await this.executeCypher(chunk[j].query, chunk[j].params);
+ results[offset + j] = r;
+ if (r.error) {
+ totalFailed++;
+ logger.error(`[Memgraph] Fallback statement error: ${r.error}`);
+ }
+ }
+ }
+ }
+
+ if (totalFailed > 0) {
+ logger.warn(
+ `[Memgraph] executeBatchInChunks: ${totalFailed} / ${statements.length} statements failed`,
+ );
+ }
+
+ return results;
+ }
+
+ /**
+ * Execute a batch of Cypher statements.
+ *
+ * Automatically delegates to {@link executeBatchInChunks} when the payload
+ * exceeds {@link BULK_THRESHOLD}, cutting session overhead by up to 100×.
+ * Small batches use the original sequential loop to keep things simple.
+ */
async executeBatch(statements: CypherStatement[]): Promise {
- const results: QueryResult[] = [];
+ if (statements.length >= BULK_THRESHOLD) {
+ return this.executeBatchInChunks(statements);
+ }
+ const results: QueryResult[] = [];
for (const statement of statements) {
- const result = await this.executeCypher(
- statement.query,
- statement.params,
- );
+ const result = await this.executeCypher(statement.query, statement.params);
results.push(result);
-
- // Log errors but continue
if (result.error) {
- console.error(`[Memgraph] Error in query: ${result.error}`);
+ logger.error(`[Memgraph] Error in query: ${result.error}`);
}
}
-
return results;
}
/**
- * Execute a natural language query and convert to Cypher
- * MVP: Simple pattern matching, production: use LLM service
+ * Execute a natural language query and convert to Cypher.
+ *
+ * @deprecated Use HybridRetriever for natural language queries instead.
+ * This method uses simple hardcoded pattern matching and will be removed
+ * in a future release.
*/
async queryNaturalLanguage(query: string): Promise {
const cypher = this.naturalLanguageToCypher(query);
@@ -176,7 +486,10 @@ export class MemgraphClient {
}
/**
- * Convert common natural language patterns to Cypher
+ * Convert common natural language patterns to Cypher.
+ *
+ * @deprecated Use HybridRetriever for production NL routing.
+ * This is an MVP stub retained for backward compatibility only.
*/
private naturalLanguageToCypher(query: string): string {
const lower = query.toLowerCase();
@@ -220,7 +533,13 @@ export class MemgraphClient {
*/
async loadProjectGraph(projectId: string): Promise<{
nodes: Array<{ id: string; type: string; properties: Record }>;
- relationships: Array<{ id: string; from: string; to: string; type: string; properties?: Record }>;
+ relationships: Array<{
+ id: string;
+ from: string;
+ to: string;
+ type: string;
+ properties?: Record;
+ }>;
}> {
if (!this.connected) {
return { nodes: [], relationships: [] };
@@ -231,36 +550,33 @@ export class MemgraphClient {
const nodesResult = await this.executeCypher(
`MATCH (n {projectId: $projectId})
RETURN n.id AS id, labels(n)[0] AS type, properties(n) AS props`,
- { projectId }
+ { projectId },
);
- const nodes = nodesResult.data.map((row: any) => ({
- id: row.id,
- type: row.type,
- properties: row.props || {},
+ const nodes = nodesResult.data.map((row: Record) => ({
+ id: String(row.id),
+ type: String(row.type),
+ properties: (row.props as Record) || {},
}));
// Load all relationships for this projectId
const relsResult = await this.executeCypher(
`MATCH (n1 {projectId: $projectId})-[r]->(n2 {projectId: $projectId})
RETURN n1.id AS from, n2.id AS to, type(r) AS type, properties(r) AS props`,
- { projectId }
+ { projectId },
);
- const relationships = relsResult.data.map((row: any) => ({
- id: `${row.from}-${row.type}-${row.to}`,
- from: row.from,
- to: row.to,
- type: row.type,
- properties: row.props || {},
+ const relationships = relsResult.data.map((row: Record) => ({
+ id: `${String(row.from)}-${String(row.type)}-${String(row.to)}`,
+ from: String(row.from),
+ to: String(row.to),
+ type: String(row.type),
+ properties: (row.props as Record) || {},
}));
return { nodes, relationships };
} catch (error) {
- console.error(
- `[MemgraphClient] Failed to load project graph for ${projectId}:`,
- error,
- );
+ logger.error(`[MemgraphClient] Failed to load project graph for ${projectId}:`, error);
return { nodes: [], relationships: [] };
}
}
diff --git a/src/graph/docs-builder.ts b/src/graph/docs-builder.ts
index 948a93b..08e3a85 100644
--- a/src/graph/docs-builder.ts
+++ b/src/graph/docs-builder.ts
@@ -1,13 +1,13 @@
/**
- * Docs Builder
- * Converts a ParsedDoc into idempotent Cypher statements for Memgraph.
- * Follows the same pattern as GraphBuilder in builder.ts.
+ * @file graph/docs-builder
+ * @description Converts parsed markdown docs into idempotent Cypher graph statements.
+ * @remarks Mirrors graph builder conventions for consistent write behavior.
*/
-import * as path from "node:path";
import type { ParsedDoc, ParsedSection } from "../parsers/docs-parser.js";
import type { CypherStatement } from "./builder.js";
import * as env from "../env.js";
+import { computeProjectFingerprint } from "../utils/validation.js";
// ─── Re-export CypherStatement for callers who import only this module ────────
export type { CypherStatement };
@@ -18,17 +18,12 @@ export class DocsBuilder {
private readonly txId: string;
private readonly txTimestamp: number;
- constructor(
- projectId?: string,
- workspaceRoot?: string,
- txId?: string,
- txTimestamp?: number,
- ) {
- this.workspaceRoot =
- workspaceRoot ?? env.LXRAG_WORKSPACE_ROOT ?? process.cwd();
- this.projectId =
- projectId ?? env.LXRAG_PROJECT_ID ?? path.basename(this.workspaceRoot);
- this.txId = txId ?? env.LXRAG_TX_ID ?? `tx-${Date.now()}`;
+ constructor(projectId?: string, workspaceRoot?: string, txId?: string, txTimestamp?: number) {
+ this.workspaceRoot = workspaceRoot ?? env.LXDIG_WORKSPACE_ROOT ?? process.cwd();
+ // Always use the 4-char hash fingerprint as canonical projectId.
+ // Fallback computes it directly from workspaceRoot to guarantee consistency.
+ this.projectId = projectId || computeProjectFingerprint(this.workspaceRoot);
+ this.txId = txId ?? env.LXDIG_TX_ID ?? `tx-${Date.now()}`;
this.txTimestamp = txTimestamp ?? Date.now();
}
@@ -173,11 +168,7 @@ MERGE (a)-[:NEXT_SECTION]->(b)
* (The 0.9 code-fence-path and 0.6 prose-word-boundary variants are
* produced by the engine layer which has richer context.)
*/
- private upsertDocDescribes(
- secId: string,
- ref: string,
- _docRelPath: string,
- ): CypherStatement[] {
+ private upsertDocDescribes(secId: string, ref: string, _docRelPath: string): CypherStatement[] {
const stmts: CypherStatement[] = [];
// Match FILE nodes by relativePath ending with the ref
diff --git a/src/graph/hybrid-retriever.ts b/src/graph/hybrid-retriever.ts
index 6ffec1f..2e351a6 100644
--- a/src/graph/hybrid-retriever.ts
+++ b/src/graph/hybrid-retriever.ts
@@ -1,3 +1,9 @@
+/**
+ * @file graph/hybrid-retriever
+ * @description Combines lexical and vector retrieval over graph-indexed code entities.
+ * @remarks Supports fallback behavior when BM25 or vector backends are unavailable.
+ */
+
import type { GraphIndexManager, GraphNode } from "./index.js";
import type EmbeddingEngine from "../vector/embedding-engine.js";
import type MemgraphClient from "./client.js";
@@ -28,11 +34,20 @@ export interface RetrievalResult {
export class HybridRetriever {
private _bm25Mode: "native" | "lexical_fallback" = "lexical_fallback";
+ /**
+ * True once ensureBM25Index() confirms the index exists (created or already present).
+ * Distinct from bm25Mode which only flips to "native" after a successful query.
+ */
+ private _bm25IndexKnownToExist = false;
get bm25Mode(): "native" | "lexical_fallback" {
return this._bm25Mode;
}
+ get bm25IndexKnownToExist(): boolean {
+ return this._bm25IndexKnownToExist;
+ }
+
constructor(
private index: GraphIndexManager,
private embeddingEngine?: EmbeddingEngine,
@@ -70,19 +85,16 @@ export class HybridRetriever {
return filtered.slice(0, limit);
}
- private async vectorSearch(
- query: string,
- opts: RetrievalOptions,
- ): Promise {
+ private async vectorSearch(query: string, opts: RetrievalOptions): Promise {
const limit = Math.max(1, Math.min(opts.limit || 10, 100));
const rows: RankedNode[] = [];
if (this.embeddingEngine) {
try {
const [functions, classes, files] = await Promise.all([
- this.embeddingEngine.findSimilar(query, "function", limit),
- this.embeddingEngine.findSimilar(query, "class", limit),
- this.embeddingEngine.findSimilar(query, "file", limit),
+ this.embeddingEngine.findSimilar(query, "function", limit, opts.projectId),
+ this.embeddingEngine.findSimilar(query, "class", limit, opts.projectId),
+ this.embeddingEngine.findSimilar(query, "file", limit, opts.projectId),
]);
const merged = [...functions, ...classes, ...files];
@@ -105,10 +117,7 @@ export class HybridRetriever {
return this.lexicalFallback(query, opts.projectId, "vector", limit);
}
- private async bm25Search(
- query: string,
- opts: RetrievalOptions,
- ): Promise {
+ private async bm25Search(query: string, opts: RetrievalOptions): Promise {
const limit = Math.max(1, Math.min(opts.limit || 10, 100));
if (this.memgraph) {
@@ -138,9 +147,7 @@ export class HybridRetriever {
score: Number(row.score || 0),
source: "bm25" as const,
}))
- .filter(
- (row) => row.nodeId.length > 0 && Number.isFinite(row.score),
- )
+ .filter((row) => row.nodeId.length > 0 && Number.isFinite(row.score))
.slice(0, limit);
}
} catch {
@@ -169,8 +176,8 @@ export class HybridRetriever {
`CALL text_search.list_indices() YIELD name RETURN name`,
{},
);
- const names: string[] = (check.data || []).map(
- (r: Record) => String(r["name"] ?? ""),
+ const names: string[] = (check.data || []).map((r: Record) =>
+ String(r["name"] ?? ""),
);
if (names.includes("symbol_index")) {
// Upgrade path: symbol_index already exists but docs_index may be missing
@@ -180,6 +187,7 @@ export class HybridRetriever {
{},
);
}
+ this._bm25IndexKnownToExist = true;
return { created: false, alreadyExists: true };
}
await this.memgraph.executeCypher(
@@ -193,6 +201,7 @@ export class HybridRetriever {
{},
);
}
+ this._bm25IndexKnownToExist = true;
return { created: true, alreadyExists: false };
} catch (err) {
return {
@@ -203,10 +212,7 @@ export class HybridRetriever {
}
}
- private async graphExpansion(
- seedIds: string[],
- opts: RetrievalOptions,
- ): Promise {
+ private async graphExpansion(seedIds: string[], opts: RetrievalOptions): Promise {
const limit = Math.max(1, Math.min(opts.limit || 10, 100));
if (!seedIds.length) {
return [];
@@ -250,10 +256,7 @@ export class HybridRetriever {
private fusionRRF(lists: RankedNode[][], k: number): RetrievalResult[] {
const scores = new Map();
- const sourceScores = new Map<
- string,
- { vector?: number; bm25?: number; graph?: number }
- >();
+ const sourceScores = new Map();
lists.forEach((list) => {
list.forEach((node, idx) => {
@@ -300,9 +303,7 @@ export class HybridRetriever {
];
return nodes
- .filter(
- (node) => String(node.properties.projectId || "") === String(projectId),
- )
+ .filter((node) => String(node.properties.projectId || "") === String(projectId))
.map((node) => ({
nodeId: node.id,
score: this.scoreNode(node, tokens),
@@ -316,10 +317,7 @@ export class HybridRetriever {
private scoreNode(node: GraphNode, tokens: string[]): number {
const haystack =
`${node.id} ${node.properties.name || ""} ${node.properties.path || ""} ${node.properties.summary || ""}`.toLowerCase();
- return tokens.reduce(
- (sum, token) => sum + (haystack.includes(token) ? 1 : 0),
- 0,
- );
+ return tokens.reduce((sum, token) => sum + (haystack.includes(token) ? 1 : 0), 0);
}
private nodeMeta(nodeId: string): {
@@ -343,10 +341,7 @@ export class HybridRetriever {
};
}
- private filterByType(
- results: RetrievalResult[],
- types?: string[],
- ): RetrievalResult[] {
+ private filterByType(results: RetrievalResult[], types?: string[]): RetrievalResult[] {
if (!types?.length) {
return results;
}
@@ -355,10 +350,7 @@ export class HybridRetriever {
return results.filter((row) => allowed.has(row.type.toUpperCase()));
}
- private filterByProject(
- results: RetrievalResult[],
- projectId: string,
- ): RetrievalResult[] {
+ private filterByProject(results: RetrievalResult[], projectId: string): RetrievalResult[] {
return results.filter((row) => {
const node = this.index.getNode(row.nodeId);
return String(node?.properties?.projectId || "") === String(projectId);
diff --git a/src/graph/index.ts b/src/graph/index.ts
index 9d17ac6..5841338 100644
--- a/src/graph/index.ts
+++ b/src/graph/index.ts
@@ -1,7 +1,7 @@
/**
- * Graph Index Manager
- * Tracks all nodes and relationships in the code graph
- * Provides in-memory index for query optimization
+ * @file graph/index
+ * @description In-memory graph index for nodes, relationships, and fast lookups.
+ * @remarks Acts as the primary runtime cache for tool and engine query operations.
*/
export interface GraphNode {
@@ -50,9 +50,45 @@ export class GraphIndexManager {
/**
* Add a node to the index
*/
- addNode(id: string, type: string, properties: Record): void {
- if (this.index.nodeById.has(id)) {
- return; // Deduplication
+ addNode(id: string, type: string, properties: Record, overwrite = false): void {
+ const existing = this.index.nodeById.get(id);
+ if (existing) {
+ if (!overwrite) {
+ return; // Deduplication
+ }
+
+ const mergedNode: GraphNode = {
+ id: existing.id,
+ type,
+ properties: {
+ ...existing.properties,
+ ...properties,
+ },
+ };
+
+ this.index.nodeById.set(id, mergedNode);
+
+ const typeNodes = this.index.nodesByType.get(existing.type) || [];
+ const idx = typeNodes.findIndex((node) => node.id === id);
+ if (idx >= 0) {
+ typeNodes.splice(idx, 1);
+ }
+
+ if (!this.index.nodesByType.has(type)) {
+ this.index.nodesByType.set(type, []);
+ }
+ this.index.nodesByType.get(type)!.push(mergedNode);
+
+ if (existing.type !== type) {
+ this.index.statistics.nodesByType[existing.type] = Math.max(
+ (this.index.statistics.nodesByType[existing.type] || 1) - 1,
+ 0,
+ );
+ this.index.statistics.nodesByType[type] =
+ (this.index.statistics.nodesByType[type] || 0) + 1;
+ }
+
+ return;
}
const node: GraphNode = { id, type, properties };
@@ -138,7 +174,7 @@ export class GraphIndexManager {
/**
* Get graph statistics
*/
- getStatistics(): GraphIndex['statistics'] {
+ getStatistics(): GraphIndex["statistics"] {
return this.index.statistics;
}
@@ -177,18 +213,17 @@ export class GraphIndexManager {
* Sync nodes and relationships from another index into this one
* Used to merge orchestrator's built index into the shared context index
*/
- syncFrom(sourceIndex: GraphIndexManager): { nodesSynced: number; relationshipsSynced: number } {
+ syncFrom(sourceIndex: GraphIndexManager): {
+ nodesSynced: number;
+ relationshipsSynced: number;
+ } {
let nodesSynced = 0;
let relationshipsSynced = 0;
// Sync all nodes from source
for (const node of sourceIndex.getAllNodes()) {
- try {
- this.addNode(node.id, node.type, node.properties);
- nodesSynced++;
- } catch (e) {
- // Deduplication may skip nodes - that's okay
- }
+ this.addNode(node.id, node.type, node.properties, true);
+ nodesSynced++;
}
// Sync all relationships from source
@@ -196,7 +231,7 @@ export class GraphIndexManager {
try {
this.addRelationship(rel.id, rel.from, rel.to, rel.type, rel.properties);
relationshipsSynced++;
- } catch (e) {
+ } catch (_e) {
// Deduplication may skip relationships - that's okay
}
}
diff --git a/src/graph/orchestrator.test.ts b/src/graph/orchestrator.test.ts
deleted file mode 100644
index 36e4a42..0000000
--- a/src/graph/orchestrator.test.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import * as fs from "fs";
-import * as os from "os";
-import * as path from "path";
-import { describe, expect, it, vi } from "vitest";
-import { GraphOrchestrator } from "./orchestrator.js";
-import GraphIndexManager from "./index.js";
-
-describe("GraphOrchestrator", () => {
- it("normalizes incremental changed files and ignores unsupported extensions", async () => {
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "orch-build-"));
- const srcDir = path.join(root, "src");
- fs.mkdirSync(srcDir, { recursive: true });
-
- fs.writeFileSync(
- path.join(srcDir, "a.ts"),
- "export function alpha(): number { return 1; }\n",
- );
- fs.writeFileSync(path.join(srcDir, "note.txt"), "not a source file\n");
-
- const memgraph = {
- isConnected: vi.fn().mockReturnValue(false),
- executeBatch: vi.fn().mockResolvedValue([]),
- } as any;
-
- const orchestrator = new GraphOrchestrator(memgraph, false);
-
- const result = await orchestrator.build({
- mode: "incremental",
- workspaceRoot: root,
- sourceDir: "src",
- projectId: "proj-a",
- changedFiles: ["src/a.ts", "src/note.txt"],
- });
-
- expect(result.success).toBe(true);
- expect(result.filesChanged).toBe(1);
- expect(result.filesProcessed).toBe(1);
- expect(memgraph.executeBatch).not.toHaveBeenCalled();
-
- fs.rmSync(root, { recursive: true, force: true });
- });
-
- it("dedupes changed files and ignores out-of-workspace paths", async () => {
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "orch-scope-"));
- const srcDir = path.join(root, "src");
- fs.mkdirSync(srcDir, { recursive: true });
-
- const inWorkspace = path.join(srcDir, "a.ts");
- fs.writeFileSync(
- inWorkspace,
- "export const value = 1;\n",
- );
-
- const outsideRoot = fs.mkdtempSync(path.join(os.tmpdir(), "orch-outside-"));
- const outsideFile = path.join(outsideRoot, "outside.ts");
- fs.writeFileSync(outsideFile, "export const outside = 1;\n");
-
- const memgraph = {
- isConnected: vi.fn().mockReturnValue(false),
- executeBatch: vi.fn().mockResolvedValue([]),
- } as any;
-
- const orchestrator = new GraphOrchestrator(memgraph, false);
-
- const result = await orchestrator.build({
- mode: "incremental",
- workspaceRoot: root,
- sourceDir: "src",
- projectId: "proj-a",
- changedFiles: ["src/a.ts", "src/a.ts", outsideFile],
- });
-
- expect(result.success).toBe(true);
- expect(result.filesChanged).toBe(1);
- expect(result.filesProcessed).toBe(1);
-
- fs.rmSync(root, { recursive: true, force: true });
- fs.rmSync(outsideRoot, { recursive: true, force: true });
- });
-
- // T17 — graph_health drift false-positive after rebuild (A2 regression)
- // When sharedIndex is passed to GraphOrchestrator, build() must sync the
- // internal index to sharedIndex so that graph_health sees cachedNodes > 0.
- it("syncs internal index to sharedIndex after build (T17 / A2 regression)", async () => {
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "orch-sync-"));
- const srcDir = path.join(root, "src");
- fs.mkdirSync(srcDir, { recursive: true });
-
- fs.writeFileSync(
- path.join(srcDir, "app.ts"),
- "export function main(): void { console.log('hello'); }\n",
- );
-
- const memgraph = {
- isConnected: vi.fn().mockReturnValue(false),
- executeBatch: vi.fn().mockResolvedValue([]),
- } as any;
-
- const sharedIndex = new GraphIndexManager();
- expect(sharedIndex.getStatistics().totalNodes).toBe(0);
-
- const orchestrator = new GraphOrchestrator(memgraph, false, sharedIndex);
-
- const result = await orchestrator.build({
- mode: "full",
- workspaceRoot: root,
- sourceDir: "src",
- projectId: "proj-sync",
- });
-
- expect(result.success).toBe(true);
-
- // After build, sharedIndex must have been populated (not zero)
- const stats = sharedIndex.getStatistics();
- expect(
- stats.totalNodes,
- "sharedIndex.totalNodes must be > 0 after build — if 0, drift will always be reported",
- ).toBeGreaterThan(0);
-
- fs.rmSync(root, { recursive: true, force: true });
- });
-});
diff --git a/src/graph/orchestrator.ts b/src/graph/orchestrator.ts
index fbdb45e..98be248 100644
--- a/src/graph/orchestrator.ts
+++ b/src/graph/orchestrator.ts
@@ -1,14 +1,13 @@
/**
- * Graph Orchestrator
- * Coordinates parsing, building, and persisting the code graph
+ * @file graph/orchestrator
+ * @description Coordinates parsing, graph building, cache updates, and persistence flows.
+ * @remarks Handles full/incremental rebuild orchestration and parser strategy selection.
*/
import * as fs from "fs";
import * as path from "path";
import * as env from "../env.js";
-import TypeScriptParser, {
- type ParsedFile,
-} from "../parsers/typescript-parser.js";
+import TypeScriptParser, { type ParsedFile } from "../parsers/typescript-parser.js";
import ParserRegistry from "../parsers/parser-registry.js";
import type { ParseResult } from "../parsers/parser-interface.js";
import {
@@ -39,12 +38,16 @@ import CacheManager from "./cache.js";
import MemgraphClient from "./client.js";
import CodeSummarizer from "../response/summarizer.js";
import { DocsEngine } from "../engines/docs-engine.js";
+import { computeProjectFingerprint } from "../utils/validation.js";
+import { logger } from "../utils/logger.js";
export interface BuildOptions {
mode: "full" | "incremental";
verbose: boolean;
workspaceRoot: string;
projectId: string;
+ /** 4-char alphanumeric hash of workspaceRoot — stable workspace identity fingerprint */
+ projectFingerprint?: string;
sourceDir: string;
exclude: string[];
changedFiles?: string[];
@@ -91,7 +94,7 @@ export class GraphOrchestrator {
// ── Tree-sitter TypeScript / TSX ────────────────────────────────────────
// Enable when CODE_GRAPH_USE_TREE_SITTER=true AND native binding compiled.
- const wantTsTs = env.LXRAG_USE_TREE_SITTER;
+ const wantTsTs = env.LXDIG_USE_TREE_SITTER;
const tsAvailability = checkTsTreeSitterAvailability();
this.useTsTreeSitter = false;
if (wantTsTs) {
@@ -123,12 +126,7 @@ export class GraphOrchestrator {
// regex parsers when the native binding is unavailable.
const tsParsers = getTreeSitterParsers();
const availability = checkTreeSitterAvailability();
- const regexFallbacks = [
- new PythonParser(),
- new GoParser(),
- new RustParser(),
- new JavaParser(),
- ];
+ const regexFallbacks = [new PythonParser(), new GoParser(), new RustParser(), new JavaParser()];
const tsByLang = new Map(tsParsers.map((p) => [p.language, p]));
for (const fallback of regexFallbacks) {
@@ -159,12 +157,10 @@ export class GraphOrchestrator {
else allFallback.push(lang);
}
if (allAvailable.length > 0) {
- console.error(
- `[parsers] tree-sitter active for: ${allAvailable.join(", ")}`,
- );
+ logger.error(`[parsers] tree-sitter active for: ${allAvailable.join(", ")}`);
}
if (allFallback.length > 0) {
- console.error(
+ logger.error(
`[parsers] regex fallback for: ${allFallback.join(", ")} (install tree-sitter grammar packages for AST accuracy)`,
);
}
@@ -174,7 +170,7 @@ export class GraphOrchestrator {
this.cache = new CacheManager();
this.memgraph = memgraph || new MemgraphClient();
this.verbose = verbose;
- this.summarizer = new CodeSummarizer(env.LXRAG_SUMMARIZER_URL);
+ this.summarizer = new CodeSummarizer(env.LXDIG_SUMMARIZER_URL);
}
/**
@@ -182,16 +178,19 @@ export class GraphOrchestrator {
*/
async build(options: Partial = {}): Promise {
const startTime = Date.now();
+ const resolvedWorkspaceRoot = options.workspaceRoot || env.LXDIG_WORKSPACE_ROOT;
+ // Always use the 4-char hash fingerprint as canonical projectId.
+ // options.projectId should already be the hash from resolveProjectContext;
+ // fallback computes it directly to guarantee consistency in all code paths.
+ const resolvedProjectId = options.projectId || computeProjectFingerprint(resolvedWorkspaceRoot);
const opts: BuildOptions = {
mode: options.mode || "incremental",
verbose: options.verbose ?? this.verbose,
- workspaceRoot: options.workspaceRoot || env.LXRAG_WORKSPACE_ROOT,
- projectId:
- options.projectId ||
- env.LXRAG_PROJECT_ID ||
- path.basename(options.workspaceRoot || env.LXRAG_WORKSPACE_ROOT),
+ workspaceRoot: resolvedWorkspaceRoot,
+ projectId: resolvedProjectId,
+ projectFingerprint: resolvedProjectId,
sourceDir: options.sourceDir || "src",
- exclude: options.exclude || ["node_modules", "dist", ".next", ".lxrag"],
+ exclude: options.exclude || ["node_modules", "dist", ".next", ".lxdig"],
txId: options.txId,
txTimestamp: options.txTimestamp,
};
@@ -201,19 +200,15 @@ export class GraphOrchestrator {
try {
if (opts.verbose) {
- console.log("[GraphOrchestrator] Starting build...");
- console.log(`[GraphOrchestrator] Mode: ${opts.mode}`);
+ logger.error("[GraphOrchestrator] Starting build...");
+ logger.error(`[GraphOrchestrator] Mode: ${opts.mode}`);
}
// Get all source files across supported languages
- const files = await this.findSourceFiles(
- opts.sourceDir,
- opts.exclude,
- opts.workspaceRoot,
- );
+ const files = await this.findSourceFiles(opts.sourceDir, opts.exclude, opts.workspaceRoot);
if (opts.verbose) {
- console.log(`[GraphOrchestrator] Found ${files.length} source files`);
+ logger.error(`[GraphOrchestrator] Found ${files.length} source files`);
}
// Determine which files to process
@@ -221,6 +216,28 @@ export class GraphOrchestrator {
let filesChanged = 0;
if (opts.mode === "incremental") {
+ // Validate fingerprint against what is stored in the graph to catch stale/moved workspaces
+ if (this.memgraph.isConnected() && opts.projectFingerprint) {
+ try {
+ const fpResult = await this.memgraph.executeCypher(
+ `MATCH (f:FILE {projectId: $projectId})
+ WHERE f.projectFingerprint IS NOT NULL
+ RETURN f.projectFingerprint AS stored LIMIT 1`,
+ { projectId: opts.projectId },
+ );
+ const storedFingerprint = fpResult.data?.[0]?.stored as string | undefined;
+ if (storedFingerprint && storedFingerprint !== opts.projectFingerprint) {
+ warnings.push(
+ `⚠️ Graph fingerprint mismatch (stored: ${storedFingerprint}, current: ${opts.projectFingerprint}). ` +
+ `The workspace may have moved or this graph belongs to a different project. ` +
+ `Run graph_rebuild(mode: "full") to re-index.`,
+ );
+ }
+ } catch {
+ // Non-fatal — fingerprint validation is advisory only
+ }
+ }
+
const scopedChangedFiles = this.normalizeChangedFiles(
opts.changedFiles,
opts.workspaceRoot,
@@ -230,10 +247,10 @@ export class GraphOrchestrator {
filesToProcess = scopedChangedFiles.filter(
(filePath) => fs.existsSync(filePath) && files.includes(filePath),
);
- filesChanged = scopedChangedFiles.length;
+ filesChanged = filesToProcess.length;
if (opts.verbose) {
- console.log(
+ logger.error(
`[GraphOrchestrator] Incremental (explicit): ${filesToProcess.length} existing of ${filesChanged} changed file(s)`,
);
}
@@ -252,50 +269,61 @@ export class GraphOrchestrator {
filesChanged = filesToProcess.length;
if (opts.verbose) {
- console.log(
+ logger.error(
`[GraphOrchestrator] Incremental: ${filesChanged} changed of ${files.length}`,
);
}
}
} else {
- // Full rebuild
+ // Full rebuild — clear cache and purge all stale graph nodes (including
+ // any nodes created under old projectId case variants)
this.cache.clear();
filesChanged = files.length;
+ if (this.memgraph.isConnected()) {
+ await this.memgraph.executeCypher(
+ `MATCH (n)
+ WHERE toLower(n.projectId) = $projectIdLower
+ AND (n:FILE OR n:FUNCTION OR n:CLASS OR n:VARIABLE
+ OR n:IMPORT OR n:EXPORT OR n:FOLDER
+ OR n:TEST_SUITE OR n:TEST_CASE OR n:SECTION OR n:DOCUMENT)
+ DETACH DELETE n`,
+ { projectIdLower: opts.projectId },
+ );
+ }
}
// Parse files and build graph
let nodesCreated = 0;
- let statementsToExecute: CypherStatement[] = [];
+ const allNodes: CypherStatement[] = [];
+ const allEdges: CypherStatement[] = [];
const parsedFiles: Array<{ filePath: string; parsed: ParsedFile }> = [];
this.builder = new GraphBuilder(
opts.projectId,
opts.workspaceRoot,
opts.txId,
opts.txTimestamp,
+ opts.projectFingerprint,
);
for (const filePath of filesToProcess) {
try {
- const parsed = await this.parseSourceFile(
- filePath,
- opts.workspaceRoot,
- );
+ const parsed = await this.parseSourceFile(filePath, opts.workspaceRoot);
await this.attachSummaries(parsed);
parsedFiles.push({ filePath, parsed });
const adaptedParsed = this.adaptParsedFile(parsed);
- const statements = this.builder.buildFromParsedFile(adaptedParsed);
-
- statementsToExecute.push(...statements);
+ const result = this.builder.buildFromParsedFile(adaptedParsed);
+ allNodes.push(...result.nodes);
+ allEdges.push(...result.edges);
// Update cache
this.cache.set(filePath, parsed.hash, parsed.LOC);
// Track for index
- this.addToIndex(parsed);
- nodesCreated += this.countNodesInStatements(statements);
+ this.addToIndex(parsed, opts.projectId);
+ nodesCreated += result.nodes.length;
if (opts.verbose && filesToProcess.indexOf(filePath) % 50 === 0) {
- console.log(
+ logger.error(
`[GraphOrchestrator] Processed ${filesToProcess.indexOf(filePath)}/${filesToProcess.length} files`,
);
}
@@ -310,55 +338,64 @@ export class GraphOrchestrator {
opts.workspaceRoot,
opts.projectId,
);
- statementsToExecute.push(...testRelationships);
+ allEdges.push(...testRelationships);
// Seed progress nodes if config has progress section (Phase 5.2)
if (opts.verbose) {
- console.log("[GraphOrchestrator] Seeding progress tracking nodes...");
+ logger.error("[GraphOrchestrator] Seeding progress tracking nodes...");
}
const progressStatements = this.seedProgressNodes(opts.projectId);
- statementsToExecute.push(...progressStatements);
+ allNodes.push(...progressStatements);
// Execute statements against Memgraph (MVP: in offline mode, just count)
- const relationshipsCreated = statementsToExecute.length;
+ const totalStatements = allNodes.length + allEdges.length;
+ const relationshipsCreated = totalStatements;
if (this.memgraph.isConnected()) {
if (opts.verbose) {
- console.log(
- `[GraphOrchestrator] Executing ${statementsToExecute.length} Cypher statements...`,
+ logger.error(
+ `[GraphOrchestrator] Executing ${totalStatements} Cypher statements (Phase 1: ${allNodes.length} nodes, Phase 2: ${allEdges.length} edges)...`,
);
}
- const results = await this.memgraph.executeBatch(statementsToExecute);
- const failedStatements = results.filter((r) => r.error).length;
- if (failedStatements > 0) {
- warnings.push(`${failedStatements} Cypher statements failed`);
+ // Raise CB threshold and use chunked transactions for the bulk write so that
+ // transient connection-pool pressure does not open the circuit breaker mid-rebuild.
+ this.memgraph.beginBulkMode?.();
+ try {
+ // Phase 1: All node MERGEs — every statement is independent
+ const nodeResults = await this.memgraph.executeBatch(allNodes);
+ // Phase 2: All edge MATCHes — every endpoint guaranteed to exist
+ const edgeResults = await this.memgraph.executeBatch(allEdges);
+ const results = [...nodeResults, ...edgeResults];
+ const failedStatements = results.filter((r) => r.error).length;
+ if (failedStatements > 0) {
+ warnings.push(`${failedStatements} Cypher statements failed`);
+ }
+ } finally {
+ this.memgraph.endBulkMode?.();
}
} else {
if (opts.verbose) {
- console.log(
- `[GraphOrchestrator] Memgraph offline - statements prepared but not executed`,
+ logger.error(
+ `[GraphOrchestrator] Memgraph offline - ${totalStatements} statements prepared but not executed`,
);
}
}
// Index documentation files (Phase 6 — Docs/ADR Indexing)
const shouldIndexDocs =
- (opts.indexDocs ?? true) &&
- opts.mode === "full" &&
- this.memgraph.isConnected();
+ (opts.indexDocs ?? true) && opts.mode === "full" && this.memgraph.isConnected();
if (shouldIndexDocs) {
if (opts.verbose) {
- console.log("[GraphOrchestrator] Indexing documentation files...");
+ logger.error("[GraphOrchestrator] Indexing documentation files...");
}
try {
const docsEngine = new DocsEngine(this.memgraph);
- const docsResult = await docsEngine.indexWorkspace(
- opts.workspaceRoot,
- opts.projectId,
- { incremental: true, txId: opts.txId },
- );
+ const docsResult = await docsEngine.indexWorkspace(opts.workspaceRoot, opts.projectId, {
+ incremental: true,
+ txId: opts.txId,
+ });
if (opts.verbose) {
- console.log(
+ logger.error(
`[GraphOrchestrator] Docs indexed: ${docsResult.indexed} files, ` +
`${docsResult.skipped} skipped, ${docsResult.errors.length} errors`,
);
@@ -383,7 +420,7 @@ export class GraphOrchestrator {
try {
const syncResult = this.sharedIndex.syncFrom(this.index);
if (opts.verbose) {
- console.log(
+ logger.error(
`[GraphOrchestrator] Index synced: ${syncResult.nodesSynced} nodes, ${syncResult.relationshipsSynced} relationships`,
);
}
@@ -398,16 +435,12 @@ export class GraphOrchestrator {
if (opts.verbose) {
const stats = this.index.getStatistics();
- console.log("[GraphOrchestrator] Build complete!");
- console.log(`[GraphOrchestrator] Duration: ${duration}ms`);
- console.log(
- `[GraphOrchestrator] Files processed: ${filesToProcess.length}`,
- );
- console.log(`[GraphOrchestrator] Nodes created: ${nodesCreated}`);
- console.log(
- `[GraphOrchestrator] Relationships: ${relationshipsCreated}`,
- );
- console.log(`[GraphOrchestrator] Statistics:`, stats);
+ logger.error("[GraphOrchestrator] Build complete!");
+ logger.error(`[GraphOrchestrator] Duration: ${duration}ms`);
+ logger.error(`[GraphOrchestrator] Files processed: ${filesToProcess.length}`);
+ logger.error(`[GraphOrchestrator] Nodes created: ${nodesCreated}`);
+ logger.error(`[GraphOrchestrator] Relationships: ${relationshipsCreated}`);
+ logger.error(`[GraphOrchestrator] Statistics:`, stats);
}
return {
@@ -455,11 +488,9 @@ export class GraphOrchestrator {
: path.resolve(workspaceRoot, sourceDir);
if (fs.existsSync(basePath)) {
- console.log(`[GraphOrchestrator] Scanning directory: ${basePath}`);
+ logger.error(`[GraphOrchestrator] Scanning directory: ${basePath}`);
} else {
- console.warn(
- `[GraphOrchestrator] Source directory not found: ${basePath}`,
- );
+ logger.warn(`[GraphOrchestrator] Source directory not found: ${basePath}`);
return files;
}
@@ -485,9 +516,7 @@ export class GraphOrchestrator {
}
}
} catch (error) {
- console.warn(
- `[GraphOrchestrator] Error scanning directory ${dir}: ${error}`,
- );
+ logger.warn(`[GraphOrchestrator] Error scanning directory ${dir}: ${error}`);
}
};
@@ -503,39 +532,40 @@ export class GraphOrchestrator {
return [];
}
+ const normalizedWorkspaceRoot = path.resolve(workspaceRoot);
+ const seen = new Set();
+
return changedFiles
.map((entry) => String(entry || "").trim())
.filter(Boolean)
.map((entry) =>
- path.isAbsolute(entry)
- ? path.normalize(entry)
- : path.resolve(workspaceRoot, entry),
+ path.isAbsolute(entry) ? path.normalize(entry) : path.resolve(workspaceRoot, entry),
)
- .filter((filePath) =>
- /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java)$/.test(filePath),
- );
+ .filter((filePath) => {
+ const relative = path.relative(normalizedWorkspaceRoot, filePath);
+ return relative.length > 0 && !relative.startsWith("..") && !path.isAbsolute(relative);
+ })
+ .filter((filePath) => /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java)$/.test(filePath))
+ .filter((filePath) => {
+ if (seen.has(filePath)) {
+ return false;
+ }
+ seen.add(filePath);
+ return true;
+ });
}
- private async parseSourceFile(
- filePath: string,
- workspaceRoot: string,
- ): Promise {
+ private async parseSourceFile(filePath: string, workspaceRoot: string): Promise {
const extension = path.extname(filePath).toLowerCase();
if (extension === ".ts" || extension === ".tsx") {
// Prefer tree-sitter when available and opted in
if (this.useTsTreeSitter) {
- const tsParser =
- extension === ".tsx" ? this.tsTsxParser : this.tsTsParser;
+ const tsParser = extension === ".tsx" ? this.tsTsxParser : this.tsTsParser;
if (tsParser?.isAvailable) {
const content = fs.readFileSync(filePath, "utf-8");
const result = await tsParser.parse(filePath, content);
if (result.symbols.length > 0) {
- return this.adaptLanguageParseResult(
- filePath,
- workspaceRoot,
- content,
- result,
- );
+ return this.adaptLanguageParseResult(filePath, workspaceRoot, content, result);
}
}
}
@@ -549,18 +579,12 @@ export class GraphOrchestrator {
extension === ".cjs"
) {
if (this.useJsTreeSitter) {
- const jsParser =
- extension === ".jsx" ? this.tsJsxParser : this.tsJsParser;
+ const jsParser = extension === ".jsx" ? this.tsJsxParser : this.tsJsParser;
if (jsParser?.isAvailable) {
const content = fs.readFileSync(filePath, "utf-8");
const result = await jsParser.parse(filePath, content);
if (result.symbols.length > 0) {
- return this.adaptLanguageParseResult(
- filePath,
- workspaceRoot,
- content,
- result,
- );
+ return this.adaptLanguageParseResult(filePath, workspaceRoot, content, result);
}
}
}
@@ -576,12 +600,7 @@ export class GraphOrchestrator {
const content = fs.readFileSync(filePath, "utf-8");
const parsed = await this.parserRegistry.parse(filePath, content);
if (parsed) {
- return this.adaptLanguageParseResult(
- filePath,
- workspaceRoot,
- content,
- parsed,
- );
+ return this.adaptLanguageParseResult(filePath, workspaceRoot, content, parsed);
}
return this.adaptLanguageParseResult(filePath, workspaceRoot, content, {
@@ -595,57 +614,53 @@ export class GraphOrchestrator {
const fileHash = parsed.hash || "no-hash";
const relativePath = parsed.relativePath || parsed.filePath;
- (parsed as ParsedFile & { summary?: string }).summary =
- await this.summarizer.summarize({
- kind: "file",
- cacheKey: `file:${relativePath}:${fileHash}`,
- name: path.basename(parsed.filePath),
+ (parsed as ParsedFile & { summary?: string }).summary = await this.summarizer.summarize({
+ kind: "file",
+ cacheKey: `file:${relativePath}:${fileHash}`,
+ name: path.basename(parsed.filePath),
+ path: relativePath,
+ language: parsed.language,
+ loc: parsed.LOC,
+ metadata: {
+ functionCount: parsed.functions.length,
+ classCount: parsed.classes.length,
+ importCount: parsed.imports.length,
+ },
+ });
+
+ for (const [index, fn] of parsed.functions.entries()) {
+ (fn as typeof fn & { summary?: string }).summary = await this.summarizer.summarize({
+ kind: "function",
+ cacheKey: `function:${relativePath}:${fn.name}:${index}:${fileHash}`,
+ name: fn.name,
path: relativePath,
language: parsed.language,
- loc: parsed.LOC,
- metadata: {
- functionCount: parsed.functions.length,
- classCount: parsed.classes.length,
- importCount: parsed.imports.length,
- },
+ loc: fn.LOC,
+ metadata: { startLine: fn.startLine, endLine: fn.endLine },
});
-
- for (const [index, fn] of parsed.functions.entries()) {
- (fn as typeof fn & { summary?: string }).summary =
- await this.summarizer.summarize({
- kind: "function",
- cacheKey: `function:${relativePath}:${fn.name}:${index}:${fileHash}`,
- name: fn.name,
- path: relativePath,
- language: parsed.language,
- loc: fn.LOC,
- metadata: { startLine: fn.startLine, endLine: fn.endLine },
- });
}
for (const [index, cls] of parsed.classes.entries()) {
- (cls as typeof cls & { summary?: string }).summary =
- await this.summarizer.summarize({
- kind: "class",
- cacheKey: `class:${relativePath}:${cls.name}:${index}:${fileHash}`,
- name: cls.name,
- path: relativePath,
- language: parsed.language,
- loc: cls.LOC,
- metadata: { kind: cls.kind, extends: cls.extends },
- });
+ (cls as typeof cls & { summary?: string }).summary = await this.summarizer.summarize({
+ kind: "class",
+ cacheKey: `class:${relativePath}:${cls.name}:${index}:${fileHash}`,
+ name: cls.name,
+ path: relativePath,
+ language: parsed.language,
+ loc: cls.LOC,
+ metadata: { kind: cls.kind, extends: cls.extends },
+ });
}
for (const [index, imp] of parsed.imports.entries()) {
- (imp as typeof imp & { summary?: string }).summary =
- await this.summarizer.summarize({
- kind: "import",
- cacheKey: `import:${relativePath}:${imp.source}:${index}:${fileHash}`,
- name: imp.source,
- path: relativePath,
- language: parsed.language,
- metadata: { specifierCount: imp.specifiers.length },
- });
+ (imp as typeof imp & { summary?: string }).summary = await this.summarizer.summarize({
+ kind: "import",
+ cacheKey: `import:${relativePath}:${imp.source}:${index}:${fileHash}`,
+ name: imp.source,
+ path: relativePath,
+ language: parsed.language,
+ metadata: { specifierCount: imp.specifiers.length },
+ });
}
}
@@ -655,9 +670,7 @@ export class GraphOrchestrator {
content: string,
parsed: ParseResult,
): ParsedFile {
- const relativePath = path
- .relative(workspaceRoot, filePath)
- .replace(/\\/g, "/");
+ const relativePath = path.relative(workspaceRoot, filePath).replace(/\\/g, "/");
const hash = this.simpleHash(content);
const LOC = content.split("\n").length;
@@ -676,17 +689,23 @@ export class GraphOrchestrator {
startLine: symbol.startLine,
}));
+ // Group call expressions by their innermost enclosing scope (function name)
+ const callsByScope = new Map>();
+ parsed.symbols
+ .filter((symbol) => symbol.type === "call")
+ .forEach((symbol) => {
+ const scope = (symbol as any).scopePath ?? "";
+ if (!callsByScope.has(scope)) callsByScope.set(scope, []);
+ callsByScope.get(scope)!.push({ name: symbol.name, line: symbol.startLine });
+ });
+
const functions = parsed.symbols
- .filter(
- (symbol) => symbol.type === "function" || symbol.type === "method",
- )
+ .filter((symbol) => symbol.type === "function" || symbol.type === "method")
.map((symbol, index) => ({
id: `${relativePath}:function:${symbol.name}:${index}`,
name: symbol.name,
// Preserve kind from symbol ("arrow", "method", etc.) when present
- kind:
- (symbol.kind as "function" | "arrow" | "method" | undefined) ??
- ("function" as const),
+ kind: (symbol.kind as "function" | "arrow" | "method" | undefined) ?? ("function" as const),
startLine: symbol.startLine,
endLine: symbol.endLine,
LOC: Math.max(1, symbol.endLine - symbol.startLine + 1),
@@ -694,6 +713,8 @@ export class GraphOrchestrator {
isExported: false,
// Preserve scopePath for SCIP method-ID generation (builder uses (fn as any).scopePath)
scopePath: symbol.scopePath,
+ // Call sites extracted by tree-sitter; used by builder to create CALLS_TO edges
+ calls: callsByScope.get(symbol.name) ?? [],
}));
const classes = parsed.symbols
@@ -778,7 +799,7 @@ export class GraphOrchestrator {
/**
* Add parsed file to in-memory index
*/
- private addToIndex(parsed: ParsedFile): void {
+ private addToIndex(parsed: ParsedFile, projectId?: string): void {
// FILE node
this.index.addNode(`file:${parsed.relativePath}`, "FILE", {
path: parsed.filePath,
@@ -787,6 +808,7 @@ export class GraphOrchestrator {
LOC: parsed.LOC,
hash: parsed.hash,
summary: (parsed as ParsedFile & { summary?: string }).summary,
+ ...(projectId ? { projectId } : {}),
});
// FUNCTION nodes
@@ -794,12 +816,14 @@ export class GraphOrchestrator {
this.index.addNode(fn.id, "FUNCTION", {
name: fn.name,
kind: fn.kind,
+ filePath: parsed.filePath,
startLine: fn.startLine,
endLine: fn.endLine,
LOC: fn.LOC,
parameters: fn.parameters,
isExported: fn.isExported,
summary: (fn as typeof fn & { summary?: string }).summary,
+ ...(projectId ? { projectId } : {}),
});
this.index.addRelationship(
`contains:${fn.id}`,
@@ -814,12 +838,14 @@ export class GraphOrchestrator {
this.index.addNode(cls.id, "CLASS", {
name: cls.name,
kind: cls.kind,
+ filePath: parsed.filePath,
startLine: cls.startLine,
endLine: cls.endLine,
LOC: cls.LOC,
isExported: cls.isExported,
extends: cls.extends,
summary: (cls as typeof cls & { summary?: string }).summary,
+ ...(projectId ? { projectId } : {}),
});
this.index.addRelationship(
`contains:${cls.id}`,
@@ -843,20 +869,24 @@ export class GraphOrchestrator {
"IMPORTS",
);
});
- }
- /**
- * Count nodes created in Cypher statements
- */
- private countNodesInStatements(statements: CypherStatement[]): number {
- let count = 0;
- for (const stmt of statements) {
- // Rough count: each MERGE with a node type
- if (stmt.query.includes("MERGE (") || stmt.query.includes("CREATE (")) {
- count++;
- }
- }
- return Math.max(count, 1); // At least 1 node per file
+ // TEST_SUITE nodes
+ parsed.testSuites?.forEach((suite) => {
+ this.index.addNode(`test_suite:${suite.id}`, "TEST_SUITE", {
+ name: suite.name,
+ type: suite.type,
+ category: suite.category ?? "unit",
+ path: parsed.filePath,
+ filePath: parsed.filePath,
+ ...(projectId ? { projectId } : {}),
+ });
+ this.index.addRelationship(
+ `contains:test_suite:${suite.id}`,
+ `file:${parsed.relativePath}`,
+ `test_suite:${suite.id}`,
+ "CONTAINS",
+ );
+ });
}
/**
@@ -898,6 +928,7 @@ export class GraphOrchestrator {
LOC: fn.LOC,
isExported: fn.isExported,
summary: (fn as typeof fn & { summary?: string }).summary,
+ calls: (fn as typeof fn & { calls?: Array<{ name: string; line: number }> }).calls ?? [],
})),
classes: parsed.classes.map((cls) => ({
id: cls.id,
@@ -914,13 +945,16 @@ export class GraphOrchestrator {
summary: (cls as typeof cls & { summary?: string }).summary,
})),
variables: parsed.variables || [],
+ testSuites: parsed.testSuites ?? [],
+ testCases: parsed.testCases ?? [],
};
}
/**
- * Build TEST_SUITE-[:TESTS]->FILE relationships (Phase 3.3)
- * For each test file with test suites, find what source files it imports
- * and create TESTS relationships to those files
+ * Build TEST→symbol relationships (Phase C — symbol-level accuracy)
+ * For each test file, match imported specifiers to functions/classes in the
+ * target source file. Creates TEST_SUITE→FUNCTION, TEST_SUITE→CLASS,
+ * TEST_CASE→FUNCTION, TEST_CASE→CLASS edges. Keeps TEST_SUITE→FILE always.
*/
private buildTestRelationships(
parsedFiles: Array<{ filePath: string; parsed: ParsedFile }>,
@@ -934,7 +968,8 @@ export class GraphOrchestrator {
for (const testFile of testFiles) {
const testSuites = testFile.parsed.testSuites || [];
- if (testSuites.length === 0) continue;
+ const testCases = testFile.parsed.testCases || [];
+ if (testSuites.length === 0 && testCases.length === 0) continue;
// Get imports from test file
const imports = testFile.parsed.imports || [];
@@ -949,7 +984,27 @@ export class GraphOrchestrator {
);
if (!importedFile) continue;
- // Create TEST_SUITE-[:TESTS]->FILE relationships
+ // Find the target parsed file for symbol matching
+ const targetParsed = parsedFiles.find(
+ (f) => path.relative(workspaceRoot, f.filePath).replace(/\\/g, "/") === importedFile,
+ );
+
+ // Collect imported symbol names
+ const importedNames = new Set(
+ (imp.specifiers || []).map((spec) =>
+ typeof spec === "string" ? spec : spec.imported || spec.name,
+ ),
+ );
+
+ // Match to functions and classes in target file
+ const matchedFunctions = (targetParsed?.parsed.functions || []).filter((fn) =>
+ importedNames.has(fn.name),
+ );
+ const matchedClasses = (targetParsed?.parsed.classes || []).filter((cls) =>
+ importedNames.has(cls.name),
+ );
+
+ // Always create TEST_SUITE→FILE edges (broad relationship)
for (const suite of testSuites) {
statements.push({
query: `
@@ -963,6 +1018,78 @@ export class GraphOrchestrator {
},
});
}
+
+ // Create TEST_SUITE→FUNCTION edges
+ for (const suite of testSuites) {
+ for (const fn of matchedFunctions) {
+ const funcId = `${projectId}:${fn.id || `func:${importedFile}:${fn.name}:${fn.startLine || (fn as any).line || 0}`}`;
+ statements.push({
+ query: `
+ MATCH (ts:TEST_SUITE {id: $testSuiteId})
+ MATCH (func:FUNCTION {id: $funcId})
+ MERGE (ts)-[:TESTS]->(func)
+ `,
+ params: {
+ testSuiteId: `${projectId}:test_suite:${suite.id}`,
+ funcId,
+ },
+ });
+ }
+ }
+
+ // Create TEST_SUITE→CLASS edges
+ for (const suite of testSuites) {
+ for (const cls of matchedClasses) {
+ const classId = `${projectId}:${cls.id}`;
+ statements.push({
+ query: `
+ MATCH (ts:TEST_SUITE {id: $testSuiteId})
+ MATCH (cls:CLASS {id: $classId})
+ MERGE (ts)-[:TESTS]->(cls)
+ `,
+ params: {
+ testSuiteId: `${projectId}:test_suite:${suite.id}`,
+ classId,
+ },
+ });
+ }
+ }
+
+ // Create TEST_CASE→FUNCTION edges
+ for (const tc of testCases) {
+ for (const fn of matchedFunctions) {
+ const funcId = `${projectId}:${fn.id || `func:${importedFile}:${fn.name}:${fn.startLine || (fn as any).line || 0}`}`;
+ statements.push({
+ query: `
+ MATCH (tc:TEST_CASE {id: $testCaseId})
+ MATCH (func:FUNCTION {id: $funcId})
+ MERGE (tc)-[:TESTS]->(func)
+ `,
+ params: {
+ testCaseId: `${projectId}:test_case:${tc.id}`,
+ funcId,
+ },
+ });
+ }
+ }
+
+ // Create TEST_CASE→CLASS edges
+ for (const tc of testCases) {
+ for (const cls of matchedClasses) {
+ const classId = `${projectId}:${cls.id}`;
+ statements.push({
+ query: `
+ MATCH (tc:TEST_CASE {id: $testCaseId})
+ MATCH (cls:CLASS {id: $classId})
+ MERGE (tc)-[:TESTS]->(cls)
+ `,
+ params: {
+ testCaseId: `${projectId}:test_case:${tc.id}`,
+ classId,
+ },
+ });
+ }
+ }
}
}
diff --git a/src/graph/ppr.ts b/src/graph/ppr.ts
index c421dc8..c1c25f3 100644
--- a/src/graph/ppr.ts
+++ b/src/graph/ppr.ts
@@ -1,3 +1,9 @@
+/**
+ * @file graph/ppr
+ * @description Personalized PageRank scoring utilities for graph-based relevance ranking.
+ * @remarks Used by context-pack style retrieval pipelines to prioritize connected symbols.
+ */
+
import type MemgraphClient from "./client.js";
export interface PPROptions {
@@ -38,10 +44,7 @@ const DEFAULT_EDGE_WEIGHTS: Record = {
*
* Falls back to JS power-iteration when MAGE is unavailable or graph is empty.
*/
-export async function runPPR(
- opts: PPROptions,
- client: MemgraphClient,
-): Promise {
+export async function runPPR(opts: PPROptions, client: MemgraphClient): Promise {
const seedIds = [...new Set((opts.seedIds || []).filter(Boolean))];
if (!seedIds.length) return [];
@@ -49,13 +52,7 @@ export async function runPPR(
const damping = Number.isFinite(opts.damping) ? Number(opts.damping) : 0.85;
const iterations = Math.max(1, Math.min(opts.iterations || 20, 100));
- const mageResult = await tryMagePPR(
- opts,
- client,
- seedIds,
- maxResults,
- damping,
- );
+ const mageResult = await tryMagePPR(opts, client, seedIds, maxResults, damping);
if (mageResult) return mageResult;
return runJsPPR(opts, client, seedIds, maxResults, damping, iterations);
@@ -86,19 +83,12 @@ async function tryMagePPR(
{ projectId: opts.projectId },
);
- if (
- pagerankRes.error ||
- !Array.isArray(pagerankRes.data) ||
- pagerankRes.data.length === 0
- ) {
+ if (pagerankRes.error || !Array.isArray(pagerankRes.data) || pagerankRes.data.length === 0) {
return null;
}
const prestige = new Map();
- const nodeMeta = new Map<
- string,
- { type: string; filePath: string; name: string }
- >();
+ const nodeMeta = new Map();
for (const row of pagerankRes.data) {
const id = String(row.nodeId || "");
if (!id) continue;
@@ -189,10 +179,7 @@ async function runJsPPR(
);
const nodes = new Set(seedIds);
- const nodeMeta = new Map<
- string,
- { type: string; filePath: string; name: string }
- >();
+ const nodeMeta = new Map();
const outgoing = new Map>();
for (const row of edgeResult.data || []) {
diff --git a/src/graph/sync-state.ts b/src/graph/sync-state.ts
index aaae5e8..62963f2 100644
--- a/src/graph/sync-state.ts
+++ b/src/graph/sync-state.ts
@@ -1,10 +1,11 @@
/**
- * Sync State Manager
- * Tracks synchronization state of each system component
- * Phase 3.3: State machine for comprehensive system health
+ * @file graph/sync-state
+ * @description Tracks cross-system synchronization health and drift indicators.
+ * @remarks Provides state-machine style diagnostics for graph, index, and embedding readiness.
*/
import * as env from "../env.js";
+import { logger } from "../utils/logger.js";
export type SyncState = "uninitialized" | "synced" | "drifted" | "rebuilding";
@@ -25,12 +26,10 @@ export class SyncStateManager {
private stateHistory: Array<{ timestamp: number; state: SystemHealth }> = [];
// Phase 4.6: Use configurable history size limit
- private maxHistorySize = env.LXRAG_STATE_HISTORY_MAX_SIZE;
+ private maxHistorySize = env.LXDIG_STATE_HISTORY_MAX_SIZE;
constructor(private projectId: string) {
- console.log(
- `[SyncStateManager] Initialized for project ${projectId}`,
- );
+ logger.error(`[SyncStateManager] Initialized for project ${projectId}`);
}
/**
@@ -41,9 +40,7 @@ export class SyncStateManager {
if (oldState === newState) return;
this.state[system] = newState;
- console.log(
- `[SyncState:${this.projectId}] ${system}: ${oldState} → ${newState}`,
- );
+ logger.error(`[SyncState:${this.projectId}] ${system}: ${oldState} → ${newState}`);
// Record history
this.recordHistory();
@@ -102,7 +99,7 @@ export class SyncStateManager {
* Mark all systems as rebuilding
*/
startRebuild(): void {
- console.log(`[SyncState:${this.projectId}] Starting rebuild - all systems rebuilding`);
+ logger.error(`[SyncState:${this.projectId}] Starting rebuild - all systems rebuilding`);
this.setState("memgraph", "rebuilding");
this.setState("index", "rebuilding");
this.setState("qdrant", "rebuilding");
@@ -113,7 +110,7 @@ export class SyncStateManager {
* Mark all systems as synced after rebuild
*/
completeRebuild(): void {
- console.log(`[SyncState:${this.projectId}] Rebuild complete - all systems synced`);
+ logger.error(`[SyncState:${this.projectId}] Rebuild complete - all systems synced`);
this.setState("memgraph", "synced");
this.setState("index", "synced");
this.setState("qdrant", "synced");
@@ -124,7 +121,7 @@ export class SyncStateManager {
* Mark incremental build - index and embeddings need sync
*/
startIncrementalRebuild(): void {
- console.log(`[SyncState:${this.projectId}] Starting incremental rebuild`);
+ logger.error(`[SyncState:${this.projectId}] Starting incremental rebuild`);
this.setState("index", "rebuilding");
this.setState("embeddings", "rebuilding");
}
@@ -133,7 +130,7 @@ export class SyncStateManager {
* Complete incremental build
*/
completeIncrementalRebuild(): void {
- console.log(`[SyncState:${this.projectId}] Incremental rebuild complete`);
+ logger.error(`[SyncState:${this.projectId}] Incremental rebuild complete`);
this.setState("index", "synced");
this.setState("embeddings", "synced");
}
@@ -183,9 +180,7 @@ export class SyncStateManager {
const needsSync = this.needsSync();
if (needsSync) {
- recommendations.push(
- `${needsSync} needs sync. Run graph_rebuild to synchronize.`,
- );
+ recommendations.push(`${needsSync} needs sync. Run graph_rebuild to synchronize.`);
}
}
@@ -217,7 +212,7 @@ export class SyncStateManager {
* Reset to initial state
*/
reset(): void {
- console.log(`[SyncState:${this.projectId}] Resetting sync state`);
+ logger.error(`[SyncState:${this.projectId}] Resetting sync state`);
this.state = {
memgraph: "uninitialized",
index: "uninitialized",
diff --git a/src/graph/types.ts b/src/graph/types.ts
index a7312e7..cb8e74f 100644
--- a/src/graph/types.ts
+++ b/src/graph/types.ts
@@ -1,3 +1,8 @@
+/**
+ * @file graph/types
+ * @description Shared low-level graph write/query type contracts.
+ */
+
export interface CypherStatement {
query: string;
params: Record;
diff --git a/src/graph/watcher.ts b/src/graph/watcher.ts
index d6a9917..cb7464c 100644
--- a/src/graph/watcher.ts
+++ b/src/graph/watcher.ts
@@ -1,3 +1,9 @@
+/**
+ * @file graph/watcher
+ * @description Filesystem watcher for incremental rebuild triggering and change batching.
+ * @remarks Debounces events and emits normalized change sets per project context.
+ */
+
import chokidar from "chokidar";
export interface WatcherOptions {
@@ -46,7 +52,7 @@ export class FileWatcher {
"**/node_modules/**",
"**/dist/**",
"**/.git/**",
- "**/.lxrag/**",
+ "**/.lxdig/**",
...(this.opts.ignorePatterns || []),
];
diff --git a/src/index.ts b/src/index.ts
deleted file mode 100644
index 437d17f..0000000
--- a/src/index.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-/**
- * lxRAG MCP — stdio entry point (legacy)
- *
- * Thin stdio wrapper around ToolHandlers. For the full HTTP server (all 33
- * tools, multi-session, Streamable HTTP transport) use `src/server.ts` via
- * `npm run start:http`.
- */
-
-import * as z from "zod";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import MemgraphClient from "./graph/client.js";
-import GraphIndexManager from "./graph/index.js";
-import GraphOrchestrator from "./graph/orchestrator.js";
-import ToolHandlers from "./tools/tool-handlers.js";
-import { loadConfig } from "./config.js";
-import * as env from "./env.js";
-
-// All tool names exposed by this entry point
-const TOOL_NAMES = [
- "graph_query",
- "graph_set_workspace",
- "graph_rebuild",
- "graph_health",
- "code_explain",
- "find_pattern",
- "semantic_slice",
- "context_pack",
- "diff_since",
- "arch_validate",
- "arch_suggest",
- "semantic_search",
- "find_similar_code",
- "code_clusters",
- "semantic_diff",
- "test_select",
- "test_categorize",
- "impact_analyze",
- "test_run",
- "suggest_tests",
- "progress_query",
- "task_update",
- "feature_status",
- "blocking_issues",
- "episode_add",
- "episode_recall",
- "decision_query",
- "reflect",
- "agent_claim",
- "agent_release",
- "agent_status",
- "coordination_overview",
- "contract_validate",
-] as const;
-
-// Passthrough schema — full validation handled inside ToolHandlers
-const passthroughSchema = z.object({}).passthrough();
-
-class CodeGraphServer {
- private mcpServer: McpServer;
- private memgraph: MemgraphClient;
- private index: GraphIndexManager;
- private config: any;
- private toolHandlers: ToolHandlers | null = null;
-
- constructor() {
- this.mcpServer = new McpServer({
- name: env.LXRAG_SERVER_NAME,
- version: "1.0.0",
- });
-
- this.memgraph = new MemgraphClient({
- host: env.MEMGRAPH_HOST,
- port: env.MEMGRAPH_PORT,
- });
-
- this.index = new GraphIndexManager();
- }
-
- async start(): Promise {
- try {
- // Load configuration
- try {
- this.config = await loadConfig();
- console.error("[CodeGraphServer] Configuration loaded");
- } catch {
- console.error("[CodeGraphServer] Using default configuration");
- this.config = { architecture: { layers: [], rules: [] } };
- }
-
- // Connect to Memgraph
- await this.memgraph.connect();
- console.error("[CodeGraphServer] Memgraph connected");
-
- // Initialize tool handlers
- // Pass sharedIndex so graph_rebuild syncs the in-memory index after each
- // build; without this, graph_health always reports driftDetected: true
- // because context.index stays at 0 nodes (A2 regression fix).
- const orchestrator = new GraphOrchestrator(this.memgraph, false, this.index);
- this.toolHandlers = new ToolHandlers({
- index: this.index,
- memgraph: this.memgraph,
- config: this.config,
- orchestrator,
- });
-
- console.error("[CodeGraphServer] Tool handlers initialized");
-
- // Register all tools — dispatch through callTool()
- for (const name of TOOL_NAMES) {
- this.mcpServer.registerTool(
- name,
- { inputSchema: passthroughSchema },
- async (args: any) => {
- if (!this.toolHandlers) {
- return {
- content: [
- { type: "text" as const, text: "Server not initialized" },
- ],
- isError: true,
- };
- }
- try {
- const result = await this.toolHandlers.callTool(name, args);
- return { content: [{ type: "text" as const, text: result }] };
- } catch (error: any) {
- return {
- content: [
- { type: "text" as const, text: `Error: ${error.message}` },
- ],
- isError: true,
- };
- }
- },
- );
- }
-
- // Start stdio transport
- const transport = new StdioServerTransport();
- await this.mcpServer.connect(transport);
- console.error("[CodeGraphServer] Started successfully (stdio transport)");
- } catch (error) {
- console.error("[CodeGraphServer] Startup error:", error);
- process.exit(1);
- }
- }
-}
-
-// Start server
-const server = new CodeGraphServer();
-server.start().catch(console.error);
diff --git a/src/mcp-server.ts b/src/mcp-server.ts
deleted file mode 100644
index 8060afb..0000000
--- a/src/mcp-server.ts
+++ /dev/null
@@ -1,772 +0,0 @@
-/**
- * MCP Server Implementation
- * Full Model Context Protocol server with all 14 tools
- */
-
-import * as fs from "fs";
-import * as path from "path";
-import * as env from "./env.js";
-import { Server } from "@modelcontextprotocol/sdk/server/index.js";
-import {
- ListToolsRequestSchema,
- CallToolRequestSchema,
- ListResourcesRequestSchema,
- ReadResourceRequestSchema,
- TextContent,
- Tool,
- Resource,
-} from "@modelcontextprotocol/sdk/types.js";
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import MemgraphClient from "./graph/client.js";
-import GraphIndexManager from "./graph/index.js";
-import ToolHandlers from "./tools/tool-handlers.js";
-
-// Tool definitions
-const TOOLS: Tool[] = [
- {
- name: "graph_query",
- description:
- "Execute Cypher or natural language query against the code graph. Supports queries about file structure, dependencies, imports, etc.",
- inputSchema: {
- type: "object",
- properties: {
- query: {
- type: "string",
- description:
- 'Either a Cypher query or natural language query (e.g., "show all files in components layer")',
- },
- language: {
- type: "string",
- enum: ["cypher", "natural"],
- description:
- "Query language: cypher for Cypher syntax, natural for plain English",
- },
- mode: {
- type: "string",
- enum: ["local", "global", "hybrid"],
- description: "Query mode for natural language requests",
- },
- limit: {
- type: "number",
- description: "Maximum results to return (default: 100)",
- },
- asOf: {
- type: "string",
- description:
- "Optional ISO timestamp or epoch ms for temporal query mode",
- },
- },
- required: ["query"],
- },
- },
-
- {
- name: "code_explain",
- description:
- "Explain a code element (function, class, file) with graph context. Shows dependencies, callers, and related code.",
- inputSchema: {
- type: "object",
- properties: {
- element: {
- type: "string",
- description:
- "Code element identifier: filepath, class name, or function name",
- },
- depth: {
- type: "number",
- description:
- "Traversal depth for dependency context (1-3, default: 2)",
- },
- },
- required: ["element"],
- },
- },
-
- {
- name: "find_pattern",
- description:
- 'Find architectural patterns or violations in the codebase. E.g., "find all components using BuildingContext" or "circular dependencies".',
- inputSchema: {
- type: "object",
- properties: {
- pattern: {
- type: "string",
- description: "Pattern to search for or violation to detect",
- },
- type: {
- type: "string",
- enum: ["pattern", "violation", "unused", "circular"],
- description: "Type of search",
- },
- },
- required: ["pattern"],
- },
- },
-
- {
- name: "arch_validate",
- description:
- "Validate code against architectural layer rules and constraints.",
- inputSchema: {
- type: "object",
- properties: {
- files: {
- type: "array",
- items: { type: "string" },
- description: "File paths to validate (empty = all files)",
- },
- strict: {
- type: "boolean",
- description: "Treat warnings as errors (default: false)",
- },
- },
- },
- },
-
- {
- name: "arch_suggest",
- description:
- "Suggest best location for new code based on dependencies and layer architecture.",
- inputSchema: {
- type: "object",
- properties: {
- name: {
- type: "string",
- description: 'Name of code to add (e.g., "NewCalculationService")',
- },
- type: {
- type: "string",
- enum: [
- "component",
- "hook",
- "service",
- "context",
- "utility",
- "engine",
- "class",
- "module",
- ],
- description: "Type of code",
- },
- dependencies: {
- type: "array",
- items: { type: "string" },
- description: "Modules this code will depend on",
- },
- },
- required: ["name", "type"],
- },
- },
-
- {
- name: "test_select",
- description:
- "Select tests affected by changed files. Returns list of test files to run.",
- inputSchema: {
- type: "object",
- properties: {
- changedFiles: {
- type: "array",
- items: { type: "string" },
- description: "List of changed file paths",
- },
- includeIntegration: {
- type: "boolean",
- description: "Include integration tests (default: true)",
- },
- },
- required: ["changedFiles"],
- },
- },
-
- {
- name: "test_categorize",
- description:
- "Categorize tests as unit, integration, performance, or e2e based on patterns.",
- inputSchema: {
- type: "object",
- properties: {
- testFiles: {
- type: "array",
- items: { type: "string" },
- description: "Test files to categorize (empty = all)",
- },
- },
- },
- },
-
- {
- name: "impact_analyze",
- description:
- "Analyze change blast radius. Shows all affected tests and downstream code.",
- inputSchema: {
- type: "object",
- properties: {
- files: {
- type: "array",
- items: { type: "string" },
- description: "Changed file paths (primary parameter)",
- },
- changedFiles: {
- type: "array",
- items: { type: "string" },
- description: "Alias for files — accepted for compatibility",
- },
- depth: {
- type: "number",
- description: "How deep to traverse dependencies (1-5)",
- },
- },
- },
- },
-
- {
- name: "test_run",
- description: "Execute selected tests via Vitest.",
- inputSchema: {
- type: "object",
- properties: {
- testFiles: {
- type: "array",
- items: { type: "string" },
- description: "Test files to run",
- },
- parallel: {
- type: "boolean",
- description: "Run tests in parallel (default: true)",
- },
- },
- required: ["testFiles"],
- },
- },
-
- {
- name: "progress_query",
- description: "Query features and tasks by status, assignee, or deadline.",
- inputSchema: {
- type: "object",
- properties: {
- type: {
- type: "string",
- enum: ["feature", "task", "milestone"],
- description: "What to query",
- },
- filter: {
- type: "object",
- properties: {
- status: {
- type: "string",
- enum: ["pending", "in-progress", "completed", "blocked"],
- },
- assignee: { type: "string" },
- dueDate: { type: "string" },
- },
- description: "Filter criteria",
- },
- },
- required: ["type"],
- },
- },
-
- {
- name: "task_update",
- description: "Update task status, assignee, or due date.",
- inputSchema: {
- type: "object",
- properties: {
- taskId: {
- type: "string",
- description: "Task ID to update",
- },
- status: {
- type: "string",
- enum: ["pending", "in-progress", "completed", "blocked"],
- description: "New status",
- },
- assignee: { type: "string" },
- dueDate: { type: "string", description: "ISO 8601 date string" },
- },
- required: ["taskId"],
- },
- },
-
- {
- name: "feature_status",
- description:
- "Show detailed status of a feature including implementing code, tests, and tasks.",
- inputSchema: {
- type: "object",
- properties: {
- featureId: {
- type: "string",
- description: "Feature ID or name",
- },
- },
- required: ["featureId"],
- },
- },
-
- {
- name: "blocking_issues",
- description: "Find tasks or features that are blocking progress.",
- inputSchema: {
- type: "object",
- properties: {
- type: {
- type: "string",
- enum: ["all", "critical", "features", "tests"],
- description: "What to search for",
- },
- },
- },
- },
-
- {
- name: "graph_rebuild",
- description:
- "Rebuild the code graph from source files. Full mode reprocesses all files; incremental updates only changed files since the last build.",
- inputSchema: {
- type: "object",
- properties: {
- mode: {
- type: "string",
- enum: ["full", "incremental"],
- description: "Rebuild mode (default: incremental)",
- },
- verbose: {
- type: "boolean",
- description: "Enable verbose logging",
- },
- workspaceRoot: {
- type: "string",
- description:
- "Absolute path to workspace root (overrides session context)",
- },
- sourceDir: {
- type: "string",
- description:
- "Source directory to scan (default: /src)",
- },
- projectId: {
- type: "string",
- description: "Project identifier for graph node scoping",
- },
- },
- },
- },
-
- {
- name: "diff_since",
- description:
- "Summarize temporal graph changes since txId, timestamp, git commit, or agentId.",
- inputSchema: {
- type: "object",
- properties: {
- since: {
- type: "string",
- description:
- "Anchor value: txId, ISO timestamp, git commit SHA, or agentId",
- },
- projectId: {
- type: "string",
- description: "Optional project override",
- },
- types: {
- type: "array",
- items: {
- type: "string",
- enum: ["FILE", "FUNCTION", "CLASS"],
- },
- description: "Optional node types to include",
- },
- profile: {
- type: "string",
- enum: ["compact", "balanced", "debug"],
- },
- },
- required: ["since"],
- },
- },
-
- {
- name: "episode_add",
- description:
- "Persist an episode (observation, decision, edit, test result, or error) for agent memory.",
- inputSchema: {
- type: "object",
- properties: {
- type: {
- type: "string",
- enum: [
- "OBSERVATION",
- "DECISION",
- "EDIT",
- "TEST_RESULT",
- "ERROR",
- "REFLECTION",
- "LEARNING",
- ],
- },
- content: { type: "string" },
- entities: { type: "array", items: { type: "string" } },
- taskId: { type: "string" },
- outcome: {
- type: "string",
- enum: ["success", "failure", "partial"],
- },
- metadata: { type: "object" },
- sensitive: { type: "boolean" },
- agentId: { type: "string" },
- sessionId: { type: "string" },
- },
- required: ["type", "content"],
- },
- },
-
- {
- name: "episode_recall",
- description:
- "Recall episodes using lexical, temporal, and graph-entity scoring.",
- inputSchema: {
- type: "object",
- properties: {
- query: { type: "string" },
- agentId: { type: "string" },
- taskId: { type: "string" },
- types: { type: "array", items: { type: "string" } },
- entities: { type: "array", items: { type: "string" } },
- limit: { type: "number" },
- since: { type: "string" },
- },
- required: ["query"],
- },
- },
-
- {
- name: "decision_query",
- description:
- "Recall decision episodes relevant to a query and affected files.",
- inputSchema: {
- type: "object",
- properties: {
- query: { type: "string" },
- affectedFiles: { type: "array", items: { type: "string" } },
- taskId: { type: "string" },
- agentId: { type: "string" },
- limit: { type: "number" },
- },
- required: ["query"],
- },
- },
-
- {
- name: "reflect",
- description:
- "Synthesize reflections and learning nodes from recent episodes.",
- inputSchema: {
- type: "object",
- properties: {
- taskId: { type: "string" },
- agentId: { type: "string" },
- limit: { type: "number" },
- },
- },
- },
-
- {
- name: "agent_claim",
- description:
- "Create a coordination claim for a task or code target with conflict detection.",
- inputSchema: {
- type: "object",
- properties: {
- targetId: { type: "string" },
- claimType: {
- type: "string",
- enum: ["task", "file", "function", "feature"],
- },
- intent: { type: "string" },
- taskId: { type: "string" },
- agentId: { type: "string" },
- sessionId: { type: "string" },
- },
- required: ["targetId", "intent"],
- },
- },
-
- {
- name: "agent_release",
- description: "Release an active claim.",
- inputSchema: {
- type: "object",
- properties: {
- claimId: { type: "string" },
- outcome: { type: "string" },
- },
- required: ["claimId"],
- },
- },
-
- {
- name: "agent_status",
- description: "Get active claims and recent episodes for an agent.",
- inputSchema: {
- type: "object",
- properties: {
- agentId: { type: "string" },
- },
- required: ["agentId"],
- },
- },
-
- {
- name: "coordination_overview",
- description:
- "Fleet-wide claim view including active claims, stale claims, and conflicts.",
- inputSchema: {
- type: "object",
- properties: {},
- },
- },
-
- {
- name: "context_pack",
- description:
- "Build a single-call task briefing using PPR-ranked retrieval across code, decisions, learnings, and blockers.",
- inputSchema: {
- type: "object",
- properties: {
- task: { type: "string" },
- taskId: { type: "string" },
- agentId: { type: "string" },
- includeDecisions: { type: "boolean" },
- includeEpisodes: { type: "boolean" },
- includeLearnings: { type: "boolean" },
- profile: {
- type: "string",
- enum: ["compact", "balanced", "debug"],
- },
- },
- required: ["task"],
- },
- },
-
- {
- name: "semantic_slice",
- description:
- "Return relevant exact source lines with optional dependency and memory context.",
- inputSchema: {
- type: "object",
- properties: {
- file: { type: "string" },
- symbol: { type: "string" },
- query: { type: "string" },
- context: {
- type: "string",
- enum: ["signature", "body", "with-deps", "full"],
- },
- pprScore: { type: "number" },
- profile: {
- type: "string",
- enum: ["compact", "balanced", "debug"],
- },
- },
- },
- },
-];
-
-// Resource definitions
-const RESOURCES: Resource[] = [
- {
- uri: "graph://schema",
- name: "Graph Schema",
- description: "Memgraph schema with 18 node types and 20 relationships",
- mimeType: "text/plain",
- },
- {
- uri: "graph://statistics",
- name: "Graph Statistics",
- description: "Current graph statistics (node counts, relationships, etc.)",
- mimeType: "application/json",
- },
- {
- uri: "graph://config",
- name: "Configuration",
- description: "Architecture layers, rules, and test categories",
- mimeType: "application/json",
- },
-];
-
-export class MCPServer {
- private server: Server;
- private memgraph: MemgraphClient;
- private index: GraphIndexManager;
- private handlers: ToolHandlers;
- private config: any;
-
- constructor() {
- this.server = new Server({
- name: env.LXRAG_SERVER_NAME,
- version: "1.0.0",
- });
-
- this.memgraph = new MemgraphClient({
- host: env.MEMGRAPH_HOST,
- port: env.MEMGRAPH_PORT,
- });
-
- this.index = new GraphIndexManager();
- this.config = this.loadConfig();
- this.handlers = new ToolHandlers({
- index: this.index,
- memgraph: this.memgraph,
- config: this.config,
- });
-
- this.setupHandlers();
- }
-
- private loadConfig(): any {
- const configPath = path.resolve(process.cwd(), ".lxrag/config.json");
- if (fs.existsSync(configPath)) {
- return JSON.parse(fs.readFileSync(configPath, "utf-8"));
- }
- return {};
- }
-
- private setupHandlers(): void {
- // List available tools
- this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
- tools: TOOLS,
- }));
-
- // List available resources
- this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
- resources: RESOURCES,
- }));
-
- // Read resources
- this.server.setRequestHandler(
- ReadResourceRequestSchema,
- async (request) => {
- const uri = request.params.uri;
-
- if (uri === "graph://schema") {
- const schemaPath = path.resolve(
- process.cwd(),
- "tools/docker/init/schema.cypher",
- );
- if (fs.existsSync(schemaPath)) {
- const content = fs.readFileSync(schemaPath, "utf-8");
- return {
- contents: [
- {
- uri,
- mimeType: "text/plain",
- text: content,
- },
- ],
- };
- }
- }
-
- if (uri === "graph://statistics") {
- const stats = this.index.getStatistics();
- return {
- contents: [
- {
- uri,
- mimeType: "application/json",
- text: JSON.stringify(stats, null, 2),
- },
- ],
- };
- }
-
- if (uri === "graph://config") {
- return {
- contents: [
- {
- uri,
- mimeType: "application/json",
- text: JSON.stringify(this.config, null, 2),
- },
- ],
- };
- }
-
- return {
- contents: [
- {
- uri,
- mimeType: "text/plain",
- text: `Resource not found: ${uri}`,
- },
- ],
- };
- },
- );
-
- // Call tools
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
- return this.handleToolCall(
- request.params.name,
- request.params.arguments || {},
- );
- });
- }
-
- private async handleToolCall(
- toolName: string,
- args: Record,
- ): Promise<{ isError: boolean; content: TextContent[] }> {
- try {
- let result = "";
-
- // Type assertion to any to access dynamic method
- const handler = this.handlers as any;
-
- if (typeof handler[toolName] === "function") {
- result = await handler[toolName](args);
- } else {
- return {
- isError: true,
- content: [{ type: "text", text: `Tool not found: ${toolName}` }],
- };
- }
-
- return {
- isError: false,
- content: [{ type: "text", text: result }],
- };
- } catch (error) {
- return {
- isError: true,
- content: [{ type: "text", text: `Tool execution failed: ${error}` }],
- };
- }
- }
-
- async start(): Promise {
- // Connect to Memgraph
- await this.memgraph.connect();
-
- // Determine transport
- const transport = env.MCP_TRANSPORT;
-
- if (transport === "stdio") {
- const stdioTransport = new StdioServerTransport();
- await this.server.connect(stdioTransport);
- } else if (transport === "http") {
- // HTTP transport could be added in future
- console.log("[MCPServer] HTTP transport not yet implemented");
- }
-
- console.log("[MCPServer] Started successfully");
- console.log(`[MCPServer] Available tools: ${TOOLS.length}`);
- console.log(`[MCPServer] Available resources: ${RESOURCES.length}`);
- }
-}
-
-// Export for testing
-export default MCPServer;
diff --git a/src/parsers/__fixtures__/sample-changelog.md b/src/parsers/__fixtures__/sample-changelog.md
index 4e97e34..f7e132c 100644
--- a/src/parsers/__fixtures__/sample-changelog.md
+++ b/src/parsers/__fixtures__/sample-changelog.md
@@ -1,6 +1,6 @@
# Changelog
-All notable changes to `lxRAG-MCP` are documented here.
+All notable changes to `lxDIG MCP` are documented here.
## [1.3.0] - 2026-02-21
diff --git a/src/parsers/__fixtures__/sample-readme.md b/src/parsers/__fixtures__/sample-readme.md
index 005d3ef..726171e 100644
--- a/src/parsers/__fixtures__/sample-readme.md
+++ b/src/parsers/__fixtures__/sample-readme.md
@@ -1,4 +1,4 @@
-# lxRAG MCP
+# lxDIG MCP
A graph-powered code intelligence server.
@@ -18,7 +18,7 @@ Start the HTTP server:
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-const server = new McpServer({ name: "lxRAG-MCP", version: "1.0.0" });
+const server = new McpServer({ name: "lxDIG MCP", version: "1.0.0" });
```
Use the `graph_rebuild` tool to index your project. The `GraphOrchestrator` handles
diff --git a/src/parsers/docs-parser.test.ts b/src/parsers/__tests__/docs-parser.test.ts
similarity index 97%
rename from src/parsers/docs-parser.test.ts
rename to src/parsers/__tests__/docs-parser.test.ts
index eb06d15..1a1db7a 100644
--- a/src/parsers/docs-parser.test.ts
+++ b/src/parsers/__tests__/docs-parser.test.ts
@@ -1,10 +1,10 @@
import * as path from "node:path";
import * as url from "node:url";
import { describe, expect, it } from "vitest";
-import { DocsParser, findMarkdownFiles } from "./docs-parser.js";
+import { DocsParser, findMarkdownFiles } from "../docs-parser.js";
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
-const FIXTURES = path.join(__dirname, "__fixtures__");
+const FIXTURES = path.join(__dirname, "..", "__fixtures__");
// ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -67,9 +67,7 @@ describe("DocsParser.extractBacktickRefs", () => {
const p = parser();
it("extracts single references", () => {
- expect(p.extractBacktickRefs("Call `graph_rebuild` to start")).toEqual([
- "graph_rebuild",
- ]);
+ expect(p.extractBacktickRefs("Call `graph_rebuild` to start")).toEqual(["graph_rebuild"]);
});
it("extracts multiple unique references", () => {
@@ -122,9 +120,7 @@ describe("DocsParser.parseContent — structure", () => {
const doc = parser().parseContent(md, "/r/d.md", "/r");
const headings = doc.sections.map((s) => s.heading);
expect(headings).not.toContain("Deep");
- expect(doc.sections.find((s) => s.heading === "Top")?.content).toMatch(
- /Deep/,
- );
+ expect(doc.sections.find((s) => s.heading === "Top")?.content).toMatch(/Deep/);
});
it("section startLine is 1-based and monotonically increasing", () => {
@@ -232,11 +228,7 @@ describe("DocsParser.parseContent — title inference", () => {
});
it("falls back to filename stem when no H1", () => {
- const doc = parser().parseContent(
- "## Section only\n\nBody.",
- "/r/my-doc.md",
- "/r",
- );
+ const doc = parser().parseContent("## Section only\n\nBody.", "/r/my-doc.md", "/r");
expect(doc.title).toBe("my-doc");
});
});
@@ -245,11 +237,7 @@ describe("DocsParser.parseContent — title inference", () => {
describe("DocsParser.parseContent — relativePath", () => {
it("relativePath is workspace-relative with forward slashes", () => {
- const doc = parser().parseContent(
- "# T",
- "/project/docs/api.md",
- "/project",
- );
+ const doc = parser().parseContent("# T", "/project/docs/api.md", "/project");
expect(doc.relativePath).toBe("docs/api.md");
});
});
@@ -289,7 +277,7 @@ describe("Fixture: sample-readme.md", () => {
it("title is extracted from H1", () => {
const doc = parseFixture("sample-readme.md");
- expect(doc.title).toBe("lxRAG MCP");
+ expect(doc.title).toBe("lxDIG MCP");
});
it("has at least 3 sections", () => {
diff --git a/src/parsers/__tests__/parser-registry.test.ts b/src/parsers/__tests__/parser-registry.test.ts
new file mode 100644
index 0000000..bbaa74b
--- /dev/null
+++ b/src/parsers/__tests__/parser-registry.test.ts
@@ -0,0 +1,64 @@
+import { describe, expect, it, vi } from "vitest";
+import type { LanguageParser, ParseResult } from "../parser-interface.js";
+import { ParserRegistry } from "../parser-registry.js";
+
+function makeParser(
+ language: string,
+ extensions: string[],
+ parseResult?: ParseResult,
+): LanguageParser {
+ return {
+ language,
+ extensions,
+ parse: vi.fn(
+ async () =>
+ parseResult ?? {
+ file: "sample.ts",
+ language,
+ symbols: [],
+ },
+ ),
+ };
+}
+
+describe("ParserRegistry", () => {
+ it("register normalizes extension case and resolves parser by file path", () => {
+ const registry = new ParserRegistry();
+ const parser = makeParser("typescript", [".TS", ".Tsx"]);
+
+ registry.register(parser);
+
+ expect(registry.getParserForFile("src/example.ts")).toBe(parser);
+ expect(registry.getParserForFile("src/component.TSX")).toBe(parser);
+ });
+
+ it("getParserForFile returns null for unregistered extensions", () => {
+ const registry = new ParserRegistry();
+
+ expect(registry.getParserForFile("src/example.py")).toBeNull();
+ });
+
+ it("parse returns null when no parser is registered for extension", async () => {
+ const registry = new ParserRegistry();
+
+ const result = await registry.parse("src/example.go", "package main");
+
+ expect(result).toBeNull();
+ });
+
+ it("parse delegates to matching parser and returns parser output", async () => {
+ const registry = new ParserRegistry();
+ const parseResult: ParseResult = {
+ file: "index.ts",
+ language: "typescript",
+ symbols: [{ type: "function", name: "main", startLine: 1, endLine: 3 }],
+ };
+ const parser = makeParser("typescript", [".ts"], parseResult);
+
+ registry.register(parser);
+ const result = await registry.parse("src/index.ts", "export function main() {}");
+
+ expect(parser.parse).toHaveBeenCalledWith("src/index.ts", "export function main() {}");
+ expect(result).toEqual(parseResult);
+ });
+});
diff --git a/src/parsers/__tests__/regex-language-parsers.test.ts b/src/parsers/__tests__/regex-language-parsers.test.ts
new file mode 100644
index 0000000..3ea9b27
--- /dev/null
+++ b/src/parsers/__tests__/regex-language-parsers.test.ts
@@ -0,0 +1,410 @@
+import { describe, expect, it } from "vitest";
+import {
+ GoParser,
+ JavaParser,
+ PythonParser,
+ RustParser,
+} from "../regex-language-parsers.js";
+import type { ParsedSymbol } from "../parser-interface.js";
+
+// ---------------------------------------------------------------------------
+// Small helpers
+// ---------------------------------------------------------------------------
+
+function names(symbols: ParsedSymbol[], type?: string): string[] {
+ const filtered = type ? symbols.filter((s) => s.type === type) : symbols;
+ return filtered.map((s) => s.name);
+}
+
+// ---------------------------------------------------------------------------
+// PythonParser
+// ---------------------------------------------------------------------------
+
+describe("PythonParser", () => {
+ const parser = new PythonParser();
+
+ it("exposes correct language / extensions metadata", () => {
+ expect(parser.language).toBe("python");
+ expect(parser.extensions).toContain(".py");
+ });
+
+ it("parses an empty file without error", async () => {
+ const result = await parser.parse("empty.py", "");
+ expect(result.symbols).toHaveLength(0);
+ expect(result.file).toBe("empty.py");
+ expect(result.language).toBe("python");
+ });
+
+ describe("imports", () => {
+ it("extracts bare `import x` statements", async () => {
+ const code = `import os\nimport sys\n`;
+ const { symbols } = await parser.parse("mod.py", code);
+ expect(names(symbols, "import")).toEqual(["os", "sys"]);
+ });
+
+ it("extracts `from x import y` as import with module name", async () => {
+ const code = `from pathlib import Path\nfrom os.path import join\n`;
+ const { symbols } = await parser.parse("mod.py", code);
+ expect(names(symbols, "import")).toEqual(["pathlib", "os.path"]);
+ });
+ });
+
+ describe("classes", () => {
+ it("extracts class declarations", async () => {
+ const code = `class MyClass:\n pass\n`;
+ const { symbols } = await parser.parse("mod.py", code);
+ expect(names(symbols, "class")).toContain("MyClass");
+ });
+
+ it("records correct startLine (1-based)", async () => {
+ const code = `\nclass Foo:\n pass\n`;
+ const { symbols } = await parser.parse("mod.py", code);
+ const cls = symbols.find((s) => s.name === "Foo");
+ // class is on line 2 (1-indexed)
+ expect(cls?.startLine).toBe(2);
+ });
+
+ it("endLine is computed via python block end (indent)", async () => {
+ const code = `class Outer:\n x = 1\n y = 2\n\nclass Inner:\n pass\n`;
+ const { symbols } = await parser.parse("mod.py", code);
+ const outer = symbols.find((s) => s.name === "Outer");
+ // findPythonBlockEnd returns the 0-based index of the next less-indented line
+ // "class Inner:" is at index 4 in the split array → endLine = 4
+ expect(outer?.endLine).toBe(4);
+ });
+ });
+
+ describe("functions", () => {
+ it("extracts function definitions", async () => {
+ const code = `def add(a, b):\n return a + b\n\ndef sub(a, b):\n return a - b\n`;
+ const { symbols } = await parser.parse("funcs.py", code);
+ expect(names(symbols, "function")).toEqual(["add", "sub"]);
+ });
+
+ it("allows underscore-prefixed (private) function names", async () => {
+ const code = `def _helper():\n pass\n`;
+ const { symbols } = await parser.parse("mod.py", code);
+ expect(names(symbols, "function")).toContain("_helper");
+ });
+ });
+
+ it("parses a realistic mixed file", async () => {
+ const code = [
+ "import os",
+ "from typing import List",
+ "",
+ "class Config:",
+ " debug = False",
+ "",
+ "def load_config(path: str) -> Config:",
+ " return Config()",
+ ].join("\n");
+
+ const { symbols } = await parser.parse("config.py", code);
+ expect(names(symbols, "import")).toEqual(["os", "typing"]);
+ expect(names(symbols, "class")).toContain("Config");
+ expect(names(symbols, "function")).toContain("load_config");
+ });
+});
+
+// ---------------------------------------------------------------------------
+// GoParser
+// ---------------------------------------------------------------------------
+
+describe("GoParser", () => {
+ const parser = new GoParser();
+
+ it("exposes correct language / extensions metadata", () => {
+ expect(parser.language).toBe("go");
+ expect(parser.extensions).toContain(".go");
+ });
+
+ it("parses an empty file without error", async () => {
+ const result = await parser.parse("main.go", "");
+ expect(result.symbols).toHaveLength(0);
+ });
+
+ describe("imports", () => {
+ it("extracts single-line import", async () => {
+ const code = `import "fmt"\n`;
+ const { symbols } = await parser.parse("main.go", code);
+ expect(names(symbols, "import")).toContain("fmt");
+ });
+
+ it("extracts block imports (entry immediately after import line)", async () => {
+ const code = `import\n"fmt"\n"os"\n`;
+ const { symbols } = await parser.parse("main.go", code);
+ // block entry regex requires previous line to have "import"
+ expect(names(symbols, "import")).toContain("fmt");
+ });
+ });
+
+ describe("types (structs / interfaces)", () => {
+ it("classifies struct as class", async () => {
+ const code = `type Server struct {\n port int\n}\n`;
+ const { symbols } = await parser.parse("srv.go", code);
+ const sym = symbols.find((s) => s.name === "Server");
+ expect(sym?.type).toBe("class");
+ });
+
+ it("classifies interface as interface", async () => {
+ const code = `type Writer interface {\n Write(p []byte) (n int, err error)\n}\n`;
+ const { symbols } = await parser.parse("iface.go", code);
+ const sym = symbols.find((s) => s.name === "Writer");
+ expect(sym?.type).toBe("interface");
+ });
+
+ it("endLine covers the closing brace", async () => {
+ const code = `type Point struct {\n X int\n Y int\n}\n`;
+ // findBraceBlockEnd returns i+1 where '}' is found;
+ // closing brace is at line index 3 → endLine = 4
+ const { symbols } = await parser.parse("point.go", code);
+ const sym = symbols.find((s) => s.name === "Point");
+ expect(sym?.endLine).toBe(4);
+ });
+ });
+
+ describe("functions", () => {
+ it("extracts top-level function", async () => {
+ const code = `func Hello(name string) string {\n return "hi " + name\n}\n`;
+ const { symbols } = await parser.parse("greet.go", code);
+ expect(names(symbols, "function")).toContain("Hello");
+ });
+
+ it("extracts method (function with receiver)", async () => {
+ const code = `func (s *Server) Start() error {\n return nil\n}\n`;
+ const { symbols } = await parser.parse("srv.go", code);
+ expect(names(symbols, "function")).toContain("Start");
+ });
+ });
+
+ it("parses a realistic mixed file", async () => {
+ const code = [
+ `import "fmt"`,
+ "",
+ "type App struct {",
+ " name string",
+ "}",
+ "",
+ "func (a *App) Run() {",
+ ` fmt.Println(a.name)`,
+ "}",
+ ].join("\n");
+
+ const { symbols } = await parser.parse("app.go", code);
+ expect(names(symbols, "import")).toContain("fmt");
+ expect(names(symbols, "class")).toContain("App");
+ expect(names(symbols, "function")).toContain("Run");
+ });
+});
+
+// ---------------------------------------------------------------------------
+// RustParser
+// ---------------------------------------------------------------------------
+
+describe("RustParser", () => {
+ const parser = new RustParser();
+
+ it("exposes correct language / extensions metadata", () => {
+ expect(parser.language).toBe("rust");
+ expect(parser.extensions).toContain(".rs");
+ });
+
+ it("parses an empty file without error", async () => {
+ const result = await parser.parse("lib.rs", "");
+ expect(result.symbols).toHaveLength(0);
+ });
+
+ describe("imports", () => {
+ it("extracts use statements", async () => {
+ const code = `use std::collections::HashMap;\nuse std::io;\n`;
+ const { symbols } = await parser.parse("lib.rs", code);
+ expect(names(symbols, "import")).toEqual([
+ "std::collections::HashMap",
+ "std::io",
+ ]);
+ });
+ });
+
+ describe("structs, enums and traits", () => {
+ it("classifies struct as class", async () => {
+ const code = `struct Point {\n x: f32,\n y: f32,\n}\n`;
+ const { symbols } = await parser.parse("geo.rs", code);
+ expect(symbols.find((s) => s.name === "Point")?.type).toBe("class");
+ });
+
+ it("classifies pub struct as class", async () => {
+ const code = `pub struct Config {\n debug: bool,\n}\n`;
+ const { symbols } = await parser.parse("cfg.rs", code);
+ expect(symbols.find((s) => s.name === "Config")?.type).toBe("class");
+ });
+
+ it("classifies enum as class", async () => {
+ const code = `enum Color {\n Red,\n Green,\n Blue,\n}\n`;
+ const { symbols } = await parser.parse("color.rs", code);
+ expect(symbols.find((s) => s.name === "Color")?.type).toBe("class");
+ });
+
+ it("classifies trait as interface", async () => {
+ const code = `pub trait Serialize {\n fn serialize(&self) -> String;\n}\n`;
+ const { symbols } = await parser.parse("ser.rs", code);
+ expect(symbols.find((s) => s.name === "Serialize")?.type).toBe("interface");
+ });
+ });
+
+ describe("functions", () => {
+ it("extracts plain fn", async () => {
+ const code = `fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n`;
+ const { symbols } = await parser.parse("math.rs", code);
+ expect(names(symbols, "function")).toContain("add");
+ });
+
+ it("extracts pub fn", async () => {
+ const code = `pub fn greet(name: &str) -> String {\n format!("hi {}", name)\n}\n`;
+ const { symbols } = await parser.parse("greet.rs", code);
+ expect(names(symbols, "function")).toContain("greet");
+ });
+ });
+
+ it("parses a realistic mixed file", async () => {
+ const code = [
+ "use std::fmt;",
+ "",
+ "pub struct App {",
+ " name: String,",
+ "}",
+ "",
+ "pub trait Runner {",
+ " fn run(&self);",
+ "}",
+ "",
+ "pub fn start() {",
+ " println!(\"started\");",
+ "}",
+ ].join("\n");
+
+ const { symbols } = await parser.parse("app.rs", code);
+ expect(names(symbols, "import")).toContain("std::fmt");
+ expect(names(symbols, "class")).toContain("App");
+ expect(names(symbols, "interface")).toContain("Runner");
+ expect(names(symbols, "function")).toContain("start");
+ });
+});
+
+// ---------------------------------------------------------------------------
+// JavaParser
+// ---------------------------------------------------------------------------
+
+describe("JavaParser", () => {
+ const parser = new JavaParser();
+
+ it("exposes correct language / extensions metadata", () => {
+ expect(parser.language).toBe("java");
+ expect(parser.extensions).toContain(".java");
+ });
+
+ it("parses an empty file without error", async () => {
+ const result = await parser.parse("App.java", "");
+ expect(result.symbols).toHaveLength(0);
+ });
+
+ describe("imports", () => {
+ it("extracts import statements", async () => {
+ const code = `import java.util.List;\nimport java.io.InputStream;\n`;
+ const { symbols } = await parser.parse("App.java", code);
+ expect(names(symbols, "import")).toEqual(["java.util.List", "java.io.InputStream"]);
+ });
+
+ it("extracts wildcard imports", async () => {
+ const code = `import java.util.*;\n`;
+ const { symbols } = await parser.parse("App.java", code);
+ expect(names(symbols, "import")).toContain("java.util.*");
+ });
+ });
+
+ describe("classes and interfaces", () => {
+ it("extracts public class", async () => {
+ const code = `public class Service {\n}\n`;
+ const { symbols } = await parser.parse("Service.java", code);
+ expect(symbols.find((s) => s.name === "Service")?.type).toBe("class");
+ });
+
+ it("extracts interface as interface type", async () => {
+ const code = `public interface Runnable {\n void run();\n}\n`;
+ const { symbols } = await parser.parse("Runnable.java", code);
+ expect(symbols.find((s) => s.name === "Runnable")?.type).toBe("interface");
+ });
+
+ it("extracts enum", async () => {
+ const code = `public enum Status {\n OK, ERROR\n}\n`;
+ const { symbols } = await parser.parse("Status.java", code);
+ expect(symbols.find((s) => s.name === "Status")?.type).toBe("class");
+ });
+
+ it("handles abstract class", async () => {
+ const code = `public abstract class Base {\n}\n`;
+ const { symbols } = await parser.parse("Base.java", code);
+ expect(names(symbols, "class")).toContain("Base");
+ });
+ });
+
+ describe("methods", () => {
+ it("extracts public method", async () => {
+ const code = `public class Foo {\n public void doWork() {\n }\n}\n`;
+ const { symbols } = await parser.parse("Foo.java", code);
+ expect(names(symbols, "function")).toContain("doWork");
+ });
+
+ it("does not extract reserved keywords as methods", async () => {
+ const code = [
+ "public class Guard {",
+ " public void check() {",
+ " if (x > 0) {",
+ " }",
+ " for (int i=0; i<10; i++) {",
+ " }",
+ " while (x > 0) {",
+ " }",
+ " }",
+ "}",
+ ].join("\n");
+ const { symbols } = await parser.parse("Guard.java", code);
+ // "if", "for", "while" must not appear as function symbols
+ const fnNames = names(symbols, "function");
+ expect(fnNames).not.toContain("if");
+ expect(fnNames).not.toContain("for");
+ expect(fnNames).not.toContain("while");
+ });
+
+ it("extracts private static method", async () => {
+ const code = `public class Util {\n private static String format(String s) {\n return s;\n }\n}\n`;
+ const { symbols } = await parser.parse("Util.java", code);
+ expect(names(symbols, "function")).toContain("format");
+ });
+ });
+
+ it("parses a realistic mixed file", async () => {
+ const code = [
+ "import java.util.List;",
+ "import java.util.ArrayList;",
+ "",
+ "public class Repository {",
+ " private List items;",
+ "",
+ " public void add(String item) {",
+ " items.add(item);",
+ " }",
+ "",
+ " public List getAll() {",
+ " return items;",
+ " }",
+ "}",
+ ].join("\n");
+
+ const { symbols } = await parser.parse("Repository.java", code);
+ expect(names(symbols, "import")).toEqual(["java.util.List", "java.util.ArrayList"]);
+ expect(names(symbols, "class")).toContain("Repository");
+ expect(names(symbols, "function")).toContain("add");
+ expect(names(symbols, "function")).toContain("getAll");
+ });
+});
diff --git a/src/parsers/docs-parser.ts b/src/parsers/docs-parser.ts
index 5ff16d9..6eaa518 100644
--- a/src/parsers/docs-parser.ts
+++ b/src/parsers/docs-parser.ts
@@ -10,13 +10,7 @@ import * as path from "node:path";
// ─── Public types ─────────────────────────────────────────────────────────────
-export type DocKind =
- | "readme"
- | "adr"
- | "changelog"
- | "guide"
- | "architecture"
- | "other";
+export type DocKind = "readme" | "adr" | "changelog" | "guide" | "architecture" | "other";
export interface CodeFence {
/** Language tag (may be empty string) */
@@ -82,19 +76,10 @@ export class DocsParser {
* @param filePath Absolute or arbitrary path (used for id and kind inference).
* @param workspaceRoot Used to compute relativePath.
*/
- parseContent(
- content: string,
- filePath: string,
- workspaceRoot: string,
- ): ParsedDoc {
- const relativePath = path
- .relative(workspaceRoot, filePath)
- .replace(/\\/g, "/");
-
- const hash = crypto
- .createHash("sha256")
- .update(content, "utf-8")
- .digest("hex");
+ parseContent(content: string, filePath: string, workspaceRoot: string): ParsedDoc {
+ const relativePath = path.relative(workspaceRoot, filePath).replace(/\\/g, "/");
+
+ const hash = crypto.createHash("sha256").update(content, "utf-8").digest("hex");
const lines = content.split("\n");
const sections = this.splitSections(lines);
@@ -121,8 +106,7 @@ export class DocsParser {
/(?:^|\/)adr\//i.test(lower)
)
return "adr";
- if (/\/docs\//i.test(`/${lower}`) || lower.startsWith("docs/"))
- return "guide";
+ if (/\/docs\//i.test(`/${lower}`) || lower.startsWith("docs/")) return "guide";
return "other";
}
@@ -164,13 +148,7 @@ export class DocsParser {
const body = currentBodyLines.join("\n");
if (currentHeading.length > 0 || body.trim().length > 0) {
sections.push(
- this.buildSection(
- sections.length,
- currentHeading,
- currentLevel,
- currentStartLine,
- body,
- ),
+ this.buildSection(sections.length, currentHeading, currentLevel, currentStartLine, body),
);
}
currentBodyLines = [];
@@ -228,11 +206,7 @@ export class DocsParser {
currentStartLine = lineNumber - 1;
continue;
}
- if (
- /^-{3,}\s*$/.test(line) &&
- prevLine.trim().length > 0 &&
- !prevLine.startsWith("#")
- ) {
+ if (/^-{3,}\s*$/.test(line) && prevLine.trim().length > 0 && !prevLine.startsWith("#")) {
const headingText = currentBodyLines.pop()?.trim() ?? "";
flush(lineNumber - 1);
currentHeading = headingText;
@@ -284,10 +258,7 @@ export class DocsParser {
// ── Extraction helpers ───────────────────────────────────────────────────────
- private extractCodeFences(
- body: string,
- sectionStartLine: number,
- ): CodeFence[] {
+ private extractCodeFences(body: string, sectionStartLine: number): CodeFence[] {
const fences: CodeFence[] = [];
const lines = body.split("\n");
let inFence = false;
@@ -362,7 +333,7 @@ export class DocsParser {
/**
* Returns absolute paths to all markdown files within workspaceRoot
* that belong to conventional documentation locations.
- * Excludes node_modules, dist, .git, .lxrag.
+ * Excludes node_modules, dist, .git, .lxdig.
*/
export function findMarkdownFiles(workspaceRoot: string): string[] {
const results: string[] = [];
@@ -370,7 +341,7 @@ export function findMarkdownFiles(workspaceRoot: string): string[] {
"node_modules",
"dist",
".git",
- ".lxrag",
+ ".lxdig",
".next",
"build",
"coverage",
diff --git a/src/parsers/parser-interface.ts b/src/parsers/parser-interface.ts
index 25bf56b..da4af57 100644
--- a/src/parsers/parser-interface.ts
+++ b/src/parsers/parser-interface.ts
@@ -1,5 +1,5 @@
export interface ParsedSymbol {
- type: "function" | "class" | "method" | "variable" | "interface" | "import";
+ type: "function" | "class" | "method" | "variable" | "interface" | "import" | "call";
name: string;
startLine: number;
endLine: number;
diff --git a/src/parsers/regex-language-parsers.ts b/src/parsers/regex-language-parsers.ts
index 3cc28a1..5debbb0 100644
--- a/src/parsers/regex-language-parsers.ts
+++ b/src/parsers/regex-language-parsers.ts
@@ -1,9 +1,5 @@
import * as path from "path";
-import type {
- LanguageParser,
- ParseResult,
- ParsedSymbol,
-} from "./parser-interface.js";
+import type { LanguageParser, ParseResult, ParsedSymbol } from "./parser-interface.js";
abstract class BaseRegexParser implements LanguageParser {
abstract readonly language: string;
@@ -50,10 +46,7 @@ abstract class BaseRegexParser implements LanguageParser {
return Math.min(lines.length, startLineIndex + 1);
}
- protected findPythonBlockEnd(
- lines: string[],
- startLineIndex: number,
- ): number {
+ protected findPythonBlockEnd(lines: string[], startLineIndex: number): number {
const startLine = lines[startLineIndex] || "";
const indent = startLine.match(/^\s*/)?.[0].length || 0;
@@ -80,7 +73,7 @@ export class PythonParser extends BaseRegexParser {
const symbols: ParsedSymbol[] = [];
lines.forEach((line, index) => {
- const importMatch = /^\s*import\s+([a-zA-Z0-9_\.]+)/.exec(line);
+ const importMatch = /^\s*import\s+([a-zA-Z0-9_.]+)/.exec(line);
if (importMatch) {
symbols.push({
type: "import",
@@ -90,7 +83,7 @@ export class PythonParser extends BaseRegexParser {
});
}
- const fromMatch = /^\s*from\s+([a-zA-Z0-9_\.]+)\s+import\s+/.exec(line);
+ const fromMatch = /^\s*from\s+([a-zA-Z0-9_.]+)\s+import\s+/.exec(line);
if (fromMatch) {
symbols.push({
type: "import",
@@ -181,8 +174,7 @@ export class GoParser extends BaseRegexParser {
const symbols: ParsedSymbol[] = [];
lines.forEach((line, index) => {
- const match =
- /^\s*type\s+([A-Za-z_][A-Za-z0-9_]*)\s+(struct|interface)/.exec(line);
+ const match = /^\s*type\s+([A-Za-z_][A-Za-z0-9_]*)\s+(struct|interface)/.exec(line);
if (!match) {
return;
}
@@ -202,8 +194,7 @@ export class GoParser extends BaseRegexParser {
const symbols: ParsedSymbol[] = [];
lines.forEach((line, index) => {
- const match =
- /^\s*func\s+(?:\([^)]+\)\s*)?([A-Za-z_][A-Za-z0-9_]*)\s*\(/.exec(line);
+ const match = /^\s*func\s+(?:\([^)]+\)\s*)?([A-Za-z_][A-Za-z0-9_]*)\s*\(/.exec(line);
if (!match) {
return;
}
@@ -248,10 +239,7 @@ export class RustParser extends BaseRegexParser {
const symbols: ParsedSymbol[] = [];
lines.forEach((line, index) => {
- const match =
- /^\s*(?:pub\s+)?(struct|enum|trait)\s+([A-Za-z_][A-Za-z0-9_]*)/.exec(
- line,
- );
+ const match = /^\s*(?:pub\s+)?(struct|enum|trait)\s+([A-Za-z_][A-Za-z0-9_]*)/.exec(line);
if (!match) {
return;
}
@@ -271,9 +259,7 @@ export class RustParser extends BaseRegexParser {
const symbols: ParsedSymbol[] = [];
lines.forEach((line, index) => {
- const match = /^\s*(?:pub\s+)?fn\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/.exec(
- line,
- );
+ const match = /^\s*(?:pub\s+)?fn\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/.exec(line);
if (!match) {
return;
}
@@ -298,7 +284,7 @@ export class JavaParser extends BaseRegexParser {
const symbols: ParsedSymbol[] = [];
lines.forEach((line, index) => {
- const match = /^\s*import\s+([A-Za-z0-9_\.\*]+);/.exec(line);
+ const match = /^\s*import\s+([A-Za-z0-9_.*]+);/.exec(line);
if (!match) {
return;
}
@@ -343,7 +329,7 @@ export class JavaParser extends BaseRegexParser {
lines.forEach((line, index) => {
const match =
- /^\s*(?:public|private|protected|static|final|synchronized|native|abstract|\s)+[A-Za-z0-9_<>,\[\]\.?\s]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/.exec(
+ /^\s*(?:public|private|protected|static|final|synchronized|native|abstract|\s)+[A-Za-z0-9_<>,[\].?\s]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/.exec(
line,
);
if (!match || reserved.has(match[1])) {
diff --git a/src/parsers/tree-sitter-parser.ts b/src/parsers/tree-sitter-parser.ts
index aac6886..251f799 100644
--- a/src/parsers/tree-sitter-parser.ts
+++ b/src/parsers/tree-sitter-parser.ts
@@ -13,11 +13,7 @@
import { createRequire } from "module";
import * as path from "path";
-import type {
- LanguageParser,
- ParseResult,
- ParsedSymbol,
-} from "./parser-interface.js";
+import type { LanguageParser, ParseResult, ParsedSymbol } from "./parser-interface.js";
const _require = createRequire(import.meta.url);
@@ -39,8 +35,7 @@ function tryRequire(name: string): unknown {
*/
function resolveLanguage(mod: unknown): unknown {
if (!mod) return null;
- if (typeof (mod as any).language !== "undefined")
- return (mod as any).language;
+ if (typeof (mod as any).language !== "undefined") return (mod as any).language;
return mod;
}
@@ -143,10 +138,7 @@ abstract class TreeSitterParser implements LanguageParser {
}
}
- protected abstract extractSymbols(
- root: TSNode,
- source: string,
- ): ParsedSymbol[];
+ protected abstract extractSymbols(root: TSNode, source: string): ParsedSymbol[];
/** Helper: find all descendants matching a set of node types. */
protected findAll(root: TSNode, types: Set): TSNode[] {
@@ -197,15 +189,9 @@ export class TreeSitterPythonParser extends TreeSitterParser {
case "import_statement": {
// import foo, bar
walk(node, (child) => {
- if (
- child.type === "dotted_name" ||
- child.type === "aliased_import"
- ) {
+ if (child.type === "dotted_name" || child.type === "aliased_import") {
const name = child.childForFieldName("name")?.text ?? child.text;
- if (
- name &&
- !symbols.some((s) => s.name === name && s.type === "import")
- ) {
+ if (name && !symbols.some((s) => s.name === name && s.type === "import")) {
symbols.push({
type: "import",
name,
diff --git a/src/parsers/tree-sitter-typescript-parser.ts b/src/parsers/tree-sitter-typescript-parser.ts
index c28fda1..9e752a6 100644
--- a/src/parsers/tree-sitter-typescript-parser.ts
+++ b/src/parsers/tree-sitter-typescript-parser.ts
@@ -23,11 +23,7 @@
import { createRequire } from "module";
import * as path from "path";
-import type {
- LanguageParser,
- ParseResult,
- ParsedSymbol,
-} from "./parser-interface.js";
+import type { LanguageParser, ParseResult, ParsedSymbol } from "./parser-interface.js";
const _require = createRequire(import.meta.url);
@@ -128,10 +124,7 @@ function extractSymbols(root: TSNode): ParsedSymbol[] {
symbols.push({
type: "function",
name: nameNode.text,
- kind:
- node.type === "generator_function_declaration"
- ? "generator"
- : undefined,
+ kind: node.type === "generator_function_declaration" ? "generator" : undefined,
startLine: node.startPosition.row + 1,
endLine: node.endPosition.row + 1,
scopePath: scope[scope.length - 1],
@@ -194,8 +187,7 @@ function extractSymbols(root: TSNode): ParsedSymbol[] {
symbols.push({
type: "class",
name: nameNode.text,
- kind:
- node.type === "abstract_class_declaration" ? "abstract" : "class",
+ kind: node.type === "abstract_class_declaration" ? "abstract" : "class",
startLine: node.startPosition.row + 1,
endLine: node.endPosition.row + 1,
});
@@ -259,6 +251,27 @@ function extractSymbols(root: TSNode): ParsedSymbol[] {
});
break;
}
+
+ // ── Call expressions ─────────────────────────────────────────────────
+ case "call_expression": {
+ const fnNode = node.childForFieldName("function");
+ if (!fnNode) break;
+ let callee = "";
+ if (fnNode.type === "identifier") {
+ callee = fnNode.text;
+ } else if (fnNode.type === "member_expression") {
+ callee = fnNode.text; // e.g. "this.cache.hasChanged"
+ }
+ if (!callee) break;
+ symbols.push({
+ type: "call",
+ name: callee,
+ startLine: node.startPosition.row + 1,
+ endLine: node.endPosition.row + 1,
+ scopePath: scope[scope.length - 1],
+ });
+ break;
+ }
}
});
diff --git a/src/parsers/typescript-parser.ts b/src/parsers/typescript-parser.ts
index db223ec..87d1639 100644
--- a/src/parsers/typescript-parser.ts
+++ b/src/parsers/typescript-parser.ts
@@ -1,6 +1,7 @@
import * as fs from "fs";
import * as path from "path";
import * as env from "../env.js";
+import { logger } from "../utils/logger.js";
// import Parser from 'web-tree-sitter'; // Optional dependency
export interface ASTNode {
@@ -111,12 +112,12 @@ export class TypeScriptParser {
async initialize(): Promise {
// Tree-sitter initialization removed for MVP
// Will be added back when web-tree-sitter is properly configured
- console.log("TypeScriptParser initialized with regex fallback");
+ logger.error("TypeScriptParser initialized with regex fallback");
}
parseFile(filePath: string, options?: ParseFileOptions): ParsedFile {
const content = fs.readFileSync(filePath, "utf-8");
- const workspaceRoot = options?.workspaceRoot || env.LXRAG_WORKSPACE_ROOT;
+ const workspaceRoot = options?.workspaceRoot || env.LXDIG_WORKSPACE_ROOT;
const relativePath = path.relative(workspaceRoot, filePath);
const hash = this.hashContent(content);
const lines = content.split("\n");
@@ -238,9 +239,7 @@ export class TypeScriptParser {
}
const interfaceMatch =
- /^\s*(?:export\s+)?interface\s+(\w+)(?:\s+(?:extends)\s+(.+?))?(?:\s*{|$)/.exec(
- line,
- );
+ /^\s*(?:export\s+)?interface\s+(\w+)(?:\s+(?:extends)\s+(.+?))?(?:\s*{|$)/.exec(line);
if (interfaceMatch) {
classes.push({
id: `${path.basename(filePath)}:${interfaceMatch[1]}`,
@@ -276,20 +275,13 @@ export class TypeScriptParser {
const lines = content.split("\n");
lines.forEach((line, index) => {
- const match =
- /^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*(?::\s*(.+?))?\s*=/.exec(
- line,
- );
+ const match = /^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*(?::\s*(.+?))?\s*=/.exec(line);
if (match && !line.includes("(")) {
// Don't match function declarations
variables.push({
id: `${path.basename(filePath)}:${match[1]}`,
name: match[1],
- kind: line.includes("const")
- ? "const"
- : line.includes("let")
- ? "let"
- : "var",
+ kind: line.includes("const") ? "const" : line.includes("let") ? "let" : "var",
startLine: index + 1,
endLine: index + 1,
isExported: line.includes("export"),
@@ -316,9 +308,7 @@ export class TypeScriptParser {
const defaultImport = match[2] || (match[3] ? match[3] : "");
const specifiers = [
- ...(defaultImport
- ? [{ name: defaultImport, imported: "default", isDefault: true }]
- : []),
+ ...(defaultImport ? [{ name: defaultImport, imported: "default", isDefault: true }] : []),
...specifierStr
.split(",")
.map((s) => s.trim())
@@ -360,8 +350,7 @@ export class TypeScriptParser {
}
// named exports
- const namedMatch =
- /^export\s+(?:const|function|class|interface|type)\s+(\w+)/.exec(line);
+ const namedMatch = /^export\s+(?:const|function|class|interface|type)\s+(\w+)/.exec(line);
if (namedMatch) {
exports.push({
id: `${path.basename(filePath)}:export:${namedMatch[1]}`,
@@ -372,12 +361,9 @@ export class TypeScriptParser {
}
// export from
- const reexportMatch =
- /^export\s+(?:{([^}]+)}|\*)\s+from\s+['"]([^'"]+)['"]/.exec(line);
+ const reexportMatch = /^export\s+(?:{([^}]+)}|\*)\s+from\s+['"]([^'"]+)['"]/.exec(line);
if (reexportMatch) {
- const items = reexportMatch[1]?.split(",").map((s) => s.trim()) || [
- "*",
- ];
+ const items = reexportMatch[1]?.split(",").map((s) => s.trim()) || ["*"];
items.forEach((item) => {
exports.push({
id: `${path.basename(filePath)}:export:${item}`,
@@ -415,32 +401,21 @@ export class TypeScriptParser {
return lines.length;
}
- private extractTestSuites(
- content: string,
- filePath: string,
- ): TestSuiteNode[] {
+ private extractTestSuites(content: string, filePath: string): TestSuiteNode[] {
const testSuites: TestSuiteNode[] = [];
const lines = content.split("\n");
lines.forEach((line, index) => {
- let match;
// Match: describe|test|it( "name" or 'name' or `name`
- const regex = new RegExp(
- /^\s*(describe|test|it)\s*\(\s*['"`]([^'"`]+)['"`]/,
- );
- match = regex.exec(line);
+ const regex = new RegExp(/^\s*(describe|test|it)\s*\(\s*['"`]([^'"`]+)['"`]/);
+ const match = regex.exec(line);
if (match) {
const type = match[1] as "describe" | "test" | "it";
const name = match[2];
// Determine category based on file path
- let category:
- | "unit"
- | "integration"
- | "performance"
- | "e2e"
- | undefined = undefined;
+ let category: "unit" | "integration" | "performance" | "e2e" | undefined = undefined;
if (filePath.includes(".integration.test.")) {
category = "integration";
} else if (filePath.includes(".performance.test.")) {
@@ -476,9 +451,7 @@ export class TypeScriptParser {
// Match individual it() or test() blocks (inside describe blocks)
lines.forEach((line, index) => {
// Match: it|test( "name" or 'name' or `name`
- const regex = new RegExp(
- /^\s*(it|test)\s*\(\s*['"`]([^'"`]+)['"`]/,
- );
+ const regex = new RegExp(/^\s*(it|test)\s*\(\s*['"`]([^'"`]+)['"`]/);
const match = regex.exec(line);
if (match) {
@@ -487,9 +460,7 @@ export class TypeScriptParser {
// Find the nearest describe block above this test
let parentSuiteId: string | undefined;
for (let i = index - 1; i >= 0; i--) {
- const describeMatch = /^\s*describe\s*\(\s*['"`]([^'"`]+)['"`]/.exec(
- lines[i],
- );
+ const describeMatch = /^\s*describe\s*\(\s*['"`]([^'"`]+)['"`]/.exec(lines[i]);
if (describeMatch) {
parentSuiteId = `${path.basename(filePath)}:describe:${i}:${describeMatch[1]}`;
break;
diff --git a/src/response/__tests__/budget.test.ts b/src/response/__tests__/budget.test.ts
new file mode 100644
index 0000000..aedf55e
--- /dev/null
+++ b/src/response/__tests__/budget.test.ts
@@ -0,0 +1,78 @@
+import { describe, expect, it } from "vitest";
+import { DEFAULT_TOKEN_BUDGETS, estimateTokens, fillSlot, makeBudget } from "../budget.js";
+
+describe("response/budget", () => {
+ it("makeBudget returns profile defaults", () => {
+ const compact = makeBudget("compact");
+ const balanced = makeBudget("balanced");
+ const debug = makeBudget("debug");
+
+ expect(compact.maxTokens).toBe(DEFAULT_TOKEN_BUDGETS.compact);
+ expect(balanced.maxTokens).toBe(DEFAULT_TOKEN_BUDGETS.balanced);
+ expect(debug.maxTokens).toBe(DEFAULT_TOKEN_BUDGETS.debug);
+ expect(compact.allocation).toEqual({
+ coreCode: 0.4,
+ dependencies: 0.25,
+ decisions: 0.2,
+ plan: 0.1,
+ episodeHistory: 0.05,
+ });
+ });
+
+ it("makeBudget applies override maxTokens and allocation", () => {
+ const allocation = {
+ coreCode: 0.5,
+ dependencies: 0.2,
+ decisions: 0.15,
+ plan: 0.1,
+ episodeHistory: 0.05,
+ };
+
+ const custom = makeBudget("compact", {
+ maxTokens: 999,
+ allocation,
+ });
+
+ expect(custom.maxTokens).toBe(999);
+ expect(custom.profile).toBe("compact");
+ expect(custom.allocation).toEqual(allocation);
+ });
+
+ it("estimateTokens uses string length and rounds up", () => {
+ expect(estimateTokens("abcd")).toBe(1);
+ expect(estimateTokens("abcde")).toBe(2);
+ });
+
+ it("estimateTokens works for non-string values", () => {
+ const value = { ok: true, count: 3, labels: ["a", "b"] };
+ const expected = Math.ceil(JSON.stringify(value).length / 4);
+
+ expect(estimateTokens(value)).toBe(expected);
+ });
+
+ it("fillSlot includes items while under budget and skips overflow", () => {
+ const items = [
+ { id: "a", cost: 2 },
+ { id: "b", cost: 4 },
+ { id: "c", cost: 3 },
+ { id: "d", cost: 1 },
+ ];
+
+ const result = fillSlot(items, (item) => item.cost, 6);
+
+ expect(result.selected.map((item) => item.id)).toEqual(["a", "b"]);
+ expect(result.usedTokens).toBe(6);
+ });
+
+ it("fillSlot accepts exact-budget totals and handles zero budget", () => {
+ const items = [1, 2, 3];
+
+ const exact = fillSlot(items, (n) => n, 6);
+ expect(exact.selected).toEqual([1, 2, 3]);
+ expect(exact.usedTokens).toBe(6);
+
+ const zeroBudget = fillSlot(items, (n) => n, 0);
+ expect(zeroBudget.selected).toEqual([]);
+ expect(zeroBudget.usedTokens).toBe(0);
+ });
+});
diff --git a/src/response/__tests__/schemas.test.ts b/src/response/__tests__/schemas.test.ts
new file mode 100644
index 0000000..af57e27
--- /dev/null
+++ b/src/response/__tests__/schemas.test.ts
@@ -0,0 +1,86 @@
+import { describe, expect, it } from "vitest";
+import { applyFieldPriority, TOOL_OUTPUT_SCHEMAS, type OutputField } from "../schemas.js";
+
+function tokens(value: unknown): number {
+ return Math.ceil(JSON.stringify(value).length / 4);
+}
+
+describe("response/schemas", () => {
+ it("includes expected graph_query schema priorities", () => {
+ const schema = TOOL_OUTPUT_SCHEMAS.graph_query;
+
+ expect(schema.find((field) => field.key === "intent")?.priority).toBe("required");
+ expect(schema.find((field) => field.key === "projectId")?.priority).toBe("required");
+ expect(schema.find((field) => field.key === "workspaceRoot")?.priority).toBe("low");
+ });
+
+ it("returns unchanged data when already within budget", () => {
+ const schema: OutputField[] = [
+ { key: "required", priority: "required", description: "" },
+ { key: "low", priority: "low", description: "" },
+ ];
+ const data = { required: "ok", low: "x" };
+
+ const shaped = applyFieldPriority(data, schema, Number.POSITIVE_INFINITY);
+
+ expect(shaped).toEqual(data);
+ });
+
+ it("drops low then medium fields to meet budget before high", () => {
+ const schema: OutputField[] = [
+ { key: "required", priority: "required", description: "" },
+ { key: "high", priority: "high", description: "" },
+ { key: "medium", priority: "medium", description: "" },
+ { key: "low", priority: "low", description: "" },
+ ];
+
+ const data = {
+ required: "r",
+ high: "H".repeat(40),
+ medium: "M".repeat(40),
+ low: "L".repeat(40),
+ };
+
+ const budgetAfterDroppingLowAndMedium = tokens({
+ required: data.required,
+ high: data.high,
+ });
+
+ const shaped = applyFieldPriority(data, schema, budgetAfterDroppingLowAndMedium);
+
+ expect(shaped).toEqual({ required: data.required, high: data.high });
+ });
+
+ it("preserves required fields even when budget is too small", () => {
+ const schema: OutputField[] = [
+ { key: "required", priority: "required", description: "" },
+ { key: "high", priority: "high", description: "" },
+ ];
+
+ const data = {
+ required: "this must stay",
+ high: "this can be removed",
+ };
+
+ const shaped = applyFieldPriority(data, schema, 0);
+
+ expect(shaped).toEqual({ required: "this must stay" });
+ });
+
+ it("ignores schema keys not present in input data", () => {
+ const schema: OutputField[] = [
+ { key: "required", priority: "required", description: "" },
+ { key: "missingLow", priority: "low", description: "" },
+ { key: "presentHigh", priority: "high", description: "" },
+ ];
+
+ const data = {
+ required: "r",
+ presentHigh: "h".repeat(30),
+ };
+
+ const shaped = applyFieldPriority(data, schema, tokens({ required: "r" }));
+
+ expect(shaped).toEqual({ required: "r" });
+ });
+});
diff --git a/src/response/schemas.ts b/src/response/schemas.ts
index 06337ad..e4cb4c7 100644
--- a/src/response/schemas.ts
+++ b/src/response/schemas.ts
@@ -20,12 +20,12 @@ export const TOOL_OUTPUT_SCHEMAS: Record = {
},
{
key: "count",
- priority: "high",
+ priority: "required",
description: "Number of returned rows",
},
{
key: "results",
- priority: "high",
+ priority: "required",
description: "Query result rows",
},
{
@@ -324,12 +324,27 @@ export const TOOL_OUTPUT_SCHEMAS: Record = {
priority: "required",
description: "Normalized arguments",
},
+ { key: "valid", priority: "required", description: "Validation result" },
+ {
+ key: "errors",
+ priority: "required",
+ description: "Zod validation errors",
+ },
+ {
+ key: "missingRequired",
+ priority: "required",
+ description: "Required fields absent from input",
+ },
+ {
+ key: "extraFields",
+ priority: "required",
+ description: "Unknown fields not in tool schema",
+ },
{
key: "warnings",
priority: "high",
- description: "Normalization warnings",
+ description: "Normalization and advisory warnings",
},
- { key: "valid", priority: "high", description: "Validation result" },
],
progress_query: [
{ key: "type", priority: "required", description: "Progress entity type" },
diff --git a/src/response/shaper.ts b/src/response/shaper.ts
index 6be76d3..a536840 100644
--- a/src/response/shaper.ts
+++ b/src/response/shaper.ts
@@ -1,6 +1,15 @@
+/**
+ * @file response/shaper
+ * @description Shapes tool responses to fit profile budgets and output schemas.
+ * @remarks This module is pure formatting logic and should not perform I/O.
+ */
+
import { estimateTokens, makeBudget, type ResponseProfile } from "./budget.js";
import { TOOL_OUTPUT_SCHEMAS, applyFieldPriority } from "./schemas.js";
+/**
+ * Canonical response envelope returned by tool handlers.
+ */
export interface ToolResponse {
ok: boolean;
profile: ResponseProfile;
@@ -11,6 +20,13 @@ export interface ToolResponse {
errorCode?: string;
}
+/**
+ * Truncates long strings while preserving a visible truncation marker.
+ *
+ * @param input - Original string value.
+ * @param maxLength - Maximum number of characters allowed.
+ * @returns The original string when within bounds, otherwise a truncated string.
+ */
function truncateString(input: string, maxLength: number): string {
if (!Number.isFinite(maxLength) || input.length <= maxLength) {
return input;
@@ -18,30 +34,21 @@ function truncateString(input: string, maxLength: number): string {
return `${input.slice(0, maxLength)}…(truncated)`;
}
-function shapeValue(
- value: unknown,
- profile: ResponseProfile,
- depth = 0,
-): unknown {
+/**
+ * Recursively shapes values to stay within profile-specific depth and size limits.
+ *
+ * @param value - Value to transform for output safety.
+ * @param profile - Output verbosity profile.
+ * @param depth - Current recursion depth.
+ * @returns A shaped value safe for transport in tool responses.
+ */
+function shapeValue(value: unknown, profile: ResponseProfile, depth = 0): unknown {
const maxDepth = profile === "debug" ? 20 : 6;
const maxArray =
- profile === "balanced"
- ? 30
- : profile === "debug"
- ? Number.POSITIVE_INFINITY
- : 10;
- const maxKeys =
- profile === "balanced"
- ? 50
- : profile === "debug"
- ? Number.POSITIVE_INFINITY
- : 20;
+ profile === "balanced" ? 30 : profile === "debug" ? Number.POSITIVE_INFINITY : 10;
+ const maxKeys = profile === "balanced" ? 50 : profile === "debug" ? Number.POSITIVE_INFINITY : 20;
const maxStrLen =
- profile === "balanced"
- ? 4000
- : profile === "debug"
- ? Number.POSITIVE_INFINITY
- : 1200;
+ profile === "balanced" ? 4000 : profile === "debug" ? Number.POSITIVE_INFINITY : 1200;
if (depth > maxDepth) {
return "[…depth limit]";
@@ -61,17 +68,13 @@ function shapeValue(
}
if (value !== null && typeof value === "object") {
- const entries = Object.entries(value as Record).slice(
- 0,
- maxKeys,
- );
+ const entries = Object.entries(value as Record).slice(0, maxKeys);
const shaped = Object.fromEntries(
entries.map(([key, item]) => [key, shapeValue(item, profile, depth + 1)]),
);
const totalKeys = Object.keys(value as Record).length;
if (totalKeys > maxKeys) {
- (shaped as Record)["…omitted"] =
- `${totalKeys - maxKeys} more keys`;
+ (shaped as Record)["…omitted"] = `${totalKeys - maxKeys} more keys`;
}
return shaped;
}
@@ -79,6 +82,16 @@ function shapeValue(
return value;
}
+/**
+ * Builds a successful tool response and applies profile-aware shaping.
+ *
+ * @param summary - Human-readable success summary.
+ * @param data - Raw payload to include in response.
+ * @param profile - Desired response profile.
+ * @param toolName - Optional tool name for schema-priority shaping.
+ * @param hint - Optional user-facing follow-up hint.
+ * @returns A standardized success response envelope.
+ */
export function formatResponse(
summary: string,
data: unknown,
@@ -98,11 +111,7 @@ export function formatResponse(
) {
const schema = TOOL_OUTPUT_SCHEMAS[toolName];
if (schema?.length) {
- shaped = applyFieldPriority(
- shaped as Record,
- schema,
- budget.maxTokens,
- );
+ shaped = applyFieldPriority(shaped as Record, schema, budget.maxTokens);
}
}
@@ -116,6 +125,15 @@ export function formatResponse(
};
}
+/**
+ * Builds a standardized error response envelope.
+ *
+ * @param errorCode - Stable machine-readable error code.
+ * @param reason - Human-readable failure reason.
+ * @param hint - Suggested next action for recovery.
+ * @param profile - Response profile to include in envelope.
+ * @returns A standardized error response envelope.
+ */
export function errorResponse(
errorCode: string,
reason: string,
diff --git a/src/response/summarizer.ts b/src/response/summarizer.ts
index 90d3ab5..ff4e9bd 100644
--- a/src/response/summarizer.ts
+++ b/src/response/summarizer.ts
@@ -73,10 +73,7 @@ export default class CodeSummarizer {
typeof obj.data === "object" &&
typeof (obj.data as Record).summary === "string"
) {
- return String((obj.data as Record).summary).slice(
- 0,
- 400,
- );
+ return String((obj.data as Record).summary).slice(0, 400);
}
}
diff --git a/src/server.ts b/src/server.ts
index 8002203..d3eb56d 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -1,6 +1,7 @@
/**
- * MCP Server with stdio transport for Claude Code
- * Uses McpServer for simplified tool registration
+ * @file server
+ * @description MCP server bootstrap supporting stdio and Streamable HTTP transports.
+ * @remarks Tool registration is sourced from the centralized tool registry.
*/
import * as env from "./env.js";
@@ -13,9 +14,11 @@ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js
import MemgraphClient from "./graph/client.js";
import GraphIndexManager from "./graph/index.js";
import { ToolHandlers } from "./tools/tool-handlers.js";
-import { loadConfig } from "./config.js";
+import { toolRegistry } from "./tools/registry.js";
+import { loadConfig, type Config } from "./config.js";
import GraphOrchestrator from "./graph/orchestrator.js";
import { runWithRequestContext } from "./request-context.js";
+import { logger } from "./utils/logger.js";
// Initialize components
const memgraph = new MemgraphClient({
@@ -25,26 +28,32 @@ const memgraph = new MemgraphClient({
const index = new GraphIndexManager();
let toolHandlers: ToolHandlers;
-let config: any = {};
+let config: Config = { architecture: { layers: [], rules: [] } };
let orchestrator: GraphOrchestrator;
-// Load configuration
+/**
+ * Initializes shared infrastructure required before serving requests.
+ *
+ * @remarks
+ * This connects Memgraph, loads architecture config, and wires `ToolHandlers`
+ * with the shared index/orchestrator instances.
+ */
async function initialize() {
try {
await memgraph.connect();
- console.error("[MCP] Memgraph connected");
+ logger.error("[MCP] Memgraph connected");
// Load architecture config if exists
try {
config = await loadConfig();
- console.error("[MCP] Configuration loaded");
- } catch (err) {
- console.error("[MCP] No configuration file found, using defaults");
+ logger.error("[MCP] Configuration loaded");
+ } catch (_err) {
+ logger.error("[MCP] No configuration file found, using defaults");
config = { architecture: { layers: [], rules: [] } };
}
- // Initialize GraphOrchestrator
- orchestrator = new GraphOrchestrator(memgraph, false);
+ // Initialize GraphOrchestrator — pass shared index so post-build sync populates it
+ orchestrator = new GraphOrchestrator(memgraph, false, index);
toolHandlers = new ToolHandlers({
index,
@@ -53,1531 +62,76 @@ async function initialize() {
orchestrator: orchestrator,
});
- console.error("[MCP] Tool handlers initialized");
+ logger.error("[MCP] Tool handlers initialized");
} catch (error) {
- console.error("[MCP] Initialization error:", error);
+ logger.error("[MCP] Initialization error:", error);
}
}
// Server implementation info
const serverInfo = {
- name: env.LXRAG_SERVER_NAME,
+ name: env.LXDIG_SERVER_NAME,
version: "1.0.0",
};
+/**
+ * Creates a configured MCP server instance and binds all registered tools.
+ *
+ * @returns A ready-to-connect `McpServer` instance.
+ */
function createMcpServerInstance(): McpServer {
const mcpServer = new McpServer(serverInfo);
- // Register tools with Zod schemas
- mcpServer.registerTool(
- "graph_query",
- {
- description:
- "Execute Cypher or natural language query against the code graph",
- inputSchema: z.object({
- query: z.string().describe("Cypher or natural language query"),
- language: z
- .enum(["cypher", "natural"])
- .default("natural")
- .describe("Query language"),
- mode: z
- .enum(["local", "global", "hybrid"])
- .default("local")
- .describe("Query mode for natural language"),
- limit: z.number().default(100).describe("Result limit"),
- asOf: z
- .string()
- .optional()
- .describe(
- "Optional ISO timestamp or epoch ms for temporal query mode",
- ),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("graph_query", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- mcpServer.registerTool(
- "code_explain",
- {
- description: "Explain code element with dependency context",
- inputSchema: z.object({
- element: z.string().describe("File path, class or function name"),
- depth: z.number().min(1).max(3).default(2).describe("Analysis depth"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("code_explain", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- mcpServer.registerTool(
- "arch_validate",
- {
- description: "Validate code against layer rules",
- inputSchema: z.object({
- files: z.array(z.string()).optional().describe("Files to validate"),
- strict: z.boolean().default(false).describe("Strict validation mode"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("arch_validate", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- mcpServer.registerTool(
- "test_select",
- {
- description: "Select tests affected by changed files",
- inputSchema: z.object({
- changedFiles: z.array(z.string()).describe("Files that changed"),
- mode: z
- .enum(["direct", "transitive", "full"])
- .default("transitive")
- .describe("Selection mode"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("test_select", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- mcpServer.registerTool(
- "graph_rebuild",
- {
- description: "Rebuild code graph from source",
- inputSchema: z.object({
- mode: z
- .enum(["full", "incremental"])
- .default("incremental")
- .describe("Build mode"),
- verbose: z.boolean().default(false).describe("Verbose output"),
- workspaceRoot: z
- .string()
- .optional()
- .describe("Workspace root path (absolute preferred)"),
- workspacePath: z
- .string()
- .optional()
- .describe("Alias for workspaceRoot"),
- sourceDir: z
- .string()
- .optional()
- .describe(
- "Source directory path (absolute or relative to workspace root)",
- ),
- projectId: z
- .string()
- .optional()
- .describe("Project namespace for graph isolation"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- indexDocs: z
- .boolean()
- .default(true)
- .describe(
- "Index markdown documentation files (READMEs, ADRs) during rebuild (default: true). Set false to skip docs indexing.",
- ),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("graph_rebuild", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- mcpServer.registerTool(
- "graph_set_workspace",
- {
- description:
- "Set active workspace/project context for subsequent graph tools",
- inputSchema: z.object({
- workspaceRoot: z
- .string()
- .optional()
- .describe("Workspace root path (absolute preferred)"),
- workspacePath: z
- .string()
- .optional()
- .describe("Alias for workspaceRoot"),
- sourceDir: z
- .string()
- .optional()
- .describe(
- "Source directory path (absolute or relative to workspace root)",
- ),
- projectId: z
- .string()
- .optional()
- .describe("Project namespace for graph isolation"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("graph_set_workspace", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- mcpServer.registerTool(
- "graph_health",
- {
- description: "Report graph/index/vector health and freshness status",
- inputSchema: z.object({
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("graph_health", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- mcpServer.registerTool(
- "diff_since",
- {
- description:
- "Summarize temporal graph changes since txId, timestamp, git commit, or agentId",
- inputSchema: z.object({
- since: z
- .string()
- .describe(
- "Anchor value: txId, ISO timestamp, git commit SHA, or agentId",
- ),
- projectId: z
- .string()
- .optional()
- .describe("Optional project override (defaults to active context)"),
- types: z
- .array(z.enum(["FILE", "FUNCTION", "CLASS"]))
- .optional()
- .describe("Optional node types to include"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("diff_since", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- mcpServer.registerTool(
- "contract_validate",
- {
- description:
- "Normalize and validate tool argument contracts before execution",
- inputSchema: z.object({
- tool: z.string().describe("Target tool name"),
- arguments: z
- .record(z.any())
- .optional()
- .describe("Raw arguments to normalize"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("contract_validate", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // Phase 4: Wire remaining 9 tools with proper Zod schemas
-
- // find_pattern
- mcpServer.registerTool(
- "find_pattern",
- {
- description: "Find architectural patterns or violations in code",
- inputSchema: z.object({
- pattern: z.string().describe("Pattern to search for"),
- type: z
- .enum(["pattern", "violation", "unused", "circular"])
- .describe("Pattern type"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("find_pattern", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // arch_suggest
- mcpServer.registerTool(
- "arch_suggest",
- {
- description: "Suggest best location for new code",
- inputSchema: z.object({
- name: z.string().describe("Code name/identifier"),
- type: z
- .enum([
- "component",
- "hook",
- "service",
- "context",
- "utility",
- "engine",
- "class",
- "module",
- ])
- .describe("Code type"),
- dependencies: z
- .array(z.string())
- .optional()
- .describe("Required dependencies"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("arch_suggest", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // test_categorize
- mcpServer.registerTool(
- "test_categorize",
- {
- description: "Categorize tests by type",
- inputSchema: z.object({
- testFiles: z
- .array(z.string())
- .optional()
- .describe("Test files to categorize"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("test_categorize", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // impact_analyze
- mcpServer.registerTool(
- "impact_analyze",
- {
- description: "Analyze impact of changes",
- inputSchema: z.object({
- files: z.array(z.string()).optional().describe("Changed files"),
- changedFiles: z
- .array(z.string())
- .optional()
- .describe("Changed files (alternate contract)"),
- depth: z.number().default(3).describe("Analysis depth"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("impact_analyze", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // test_run
- mcpServer.registerTool(
- "test_run",
- {
- description: "Execute test suite",
- inputSchema: z.object({
- testFiles: z.array(z.string()).describe("Test files to run"),
- parallel: z.boolean().default(true).describe("Run tests in parallel"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("test_run", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // progress_query
- mcpServer.registerTool(
- "progress_query",
- {
- description: "Query progress tracking data",
- inputSchema: z.object({
- query: z.string().describe("Progress query"),
- status: z
- .enum(["all", "active", "blocked", "completed"])
- .optional()
- .describe("Filter by status"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .optional()
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("progress_query", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // task_update
- mcpServer.registerTool(
- "task_update",
- {
- description: "Update task status",
- inputSchema: z.object({
- taskId: z.string().describe("Task ID"),
- status: z.string().describe("New status"),
- notes: z.string().optional().describe("Optional notes"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("task_update", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // feature_status
- mcpServer.registerTool(
- "feature_status",
- {
- description: "Get feature implementation status",
- inputSchema: z.object({
- featureId: z.string().describe("Feature ID"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("feature_status", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // blocking_issues
- mcpServer.registerTool(
- "blocking_issues",
- {
- description: "Find blocking issues",
- inputSchema: z.object({
- context: z.string().optional().describe("Issue context"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("blocking_issues", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // Phase 6: Vector Search Tools (5 tools)
-
- // semantic_search
- mcpServer.registerTool(
- "semantic_search",
- {
- description: "Search code semantically using vector similarity",
- inputSchema: z.object({
- query: z.string().describe("Search query"),
- type: z
- .enum(["function", "class", "file"])
- .optional()
- .describe("Code type to search"),
- limit: z.number().default(5).describe("Result limit"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("semantic_search", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // find_similar_code
- mcpServer.registerTool(
- "find_similar_code",
- {
- description: "Find code similar to a given function or class",
- inputSchema: z.object({
- elementId: z.string().describe("Code element ID"),
- threshold: z
- .number()
- .default(0.7)
- .describe("Similarity threshold (0-1)"),
- limit: z.number().default(10).describe("Result limit"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("find_similar_code", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // code_clusters
- mcpServer.registerTool(
- "code_clusters",
- {
- description: "Find clusters of related code",
- inputSchema: z.object({
- type: z
- .enum(["function", "class", "file"])
- .describe("Code type to cluster"),
- count: z.number().default(5).describe("Number of clusters"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("code_clusters", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // semantic_diff
- mcpServer.registerTool(
- "semantic_diff",
- {
- description: "Find semantic differences between code elements",
- inputSchema: z.object({
- elementId1: z.string().describe("First code element ID"),
- elementId2: z.string().describe("Second code element ID"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("semantic_diff", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // suggest_tests
- mcpServer.registerTool(
- "suggest_tests",
- {
- description: "Suggest tests for a code element based on semantics",
- inputSchema: z.object({
- elementId: z.string().describe("Code element ID"),
- limit: z.number().default(5).describe("Number of suggestions"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("suggest_tests", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // episode_add
- mcpServer.registerTool(
- "episode_add",
- {
- description: "Persist a structured episode in long-term agent memory",
- inputSchema: z.object({
- type: z
- .enum([
- "OBSERVATION",
- "DECISION",
- "EDIT",
- "TEST_RESULT",
- "ERROR",
- "REFLECTION",
- "LEARNING",
- ])
- .describe("Episode type"),
- content: z.string().describe("Episode content"),
- entities: z
- .array(z.string())
- .optional()
- .describe("Related graph entity IDs"),
- taskId: z.string().optional().describe("Related task ID"),
- outcome: z
- .enum(["success", "failure", "partial"])
- .optional()
- .describe("Outcome classification"),
- metadata: z.record(z.any()).optional().describe("Extra metadata"),
- sensitive: z
- .boolean()
- .optional()
- .describe("Exclude from default recalls"),
- agentId: z.string().optional().describe("Agent identifier"),
- sessionId: z.string().optional().describe("Session identifier"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("episode_add", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // episode_recall
- mcpServer.registerTool(
- "episode_recall",
- {
- description:
- "Recall episodes by semantic, temporal, and entity relevance",
- inputSchema: z.object({
- query: z.string().describe("Recall query"),
- agentId: z.string().optional().describe("Agent filter"),
- taskId: z.string().optional().describe("Task filter"),
- types: z.array(z.string()).optional().describe("Episode type filters"),
- entities: z.array(z.string()).optional().describe("Entity filters"),
- limit: z.number().default(5).describe("Result limit"),
- since: z.string().optional().describe("ISO timestamp or epoch ms"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("episode_recall", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // decision_query
- mcpServer.registerTool(
- "decision_query",
- {
- description: "Query decision episodes for a target topic",
- inputSchema: z.object({
- query: z.string().describe("Decision query text"),
- affectedFiles: z
- .array(z.string())
- .optional()
- .describe("Related files/entities"),
- taskId: z.string().optional().describe("Task filter"),
- agentId: z.string().optional().describe("Agent filter"),
- limit: z.number().default(5).describe("Result limit"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("decision_query", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // reflect
- mcpServer.registerTool(
- "reflect",
- {
- description:
- "Synthesize reflections and learning nodes from recent episodes",
- inputSchema: z.object({
- taskId: z.string().optional().describe("Task filter"),
- agentId: z.string().optional().describe("Agent filter"),
- limit: z.number().default(20).describe("Episodes to analyze"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("reflect", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // agent_claim
- mcpServer.registerTool(
- "agent_claim",
- {
- description:
- "Create a coordination claim for a task or code target with conflict detection",
- inputSchema: z.object({
- targetId: z.string().describe("Target task/code node id"),
- claimType: z
- .enum(["task", "file", "function", "feature"])
- .default("task")
- .describe("Claim target type"),
- intent: z.string().describe("Natural language intent"),
- taskId: z.string().optional().describe("Related task id"),
- agentId: z.string().optional().describe("Agent identifier"),
- sessionId: z.string().optional().describe("Session identifier"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("agent_claim", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // agent_release
- mcpServer.registerTool(
- "agent_release",
- {
- description: "Release an active claim",
- inputSchema: z.object({
- claimId: z.string().describe("Claim id"),
- outcome: z.string().optional().describe("Optional outcome summary"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("agent_release", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // agent_status
- mcpServer.registerTool(
- "agent_status",
- {
- description: "Get active claims and recent episodes for an agent",
- inputSchema: z.object({
- agentId: z.string().optional().describe("Agent identifier (omit to list all agents)"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("agent_status", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // coordination_overview
- mcpServer.registerTool(
- "coordination_overview",
- {
- description:
- "Fleet-wide claim view including active claims, stale claims, and conflicts",
- inputSchema: z.object({
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool(
- "coordination_overview",
- args,
- );
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // context_pack
- mcpServer.registerTool(
- "context_pack",
- {
- description:
- "Build a single-call task briefing using PPR-ranked retrieval across code, decisions, learnings, and blockers",
- inputSchema: z.object({
- task: z.string().describe("Task description"),
- taskId: z.string().optional().describe("Optional task id"),
- agentId: z.string().optional().describe("Agent identifier"),
- includeDecisions: z
- .boolean()
- .default(true)
- .describe("Include decision episodes"),
- includeEpisodes: z
- .boolean()
- .default(true)
- .describe("Include recent episodes"),
- includeLearnings: z
- .boolean()
- .default(true)
- .describe("Include learnings"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("context_pack", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // semantic_slice
- mcpServer.registerTool(
- "semantic_slice",
- {
- description:
- "Return relevant exact source lines with optional dependency and memory context",
- inputSchema: z.object({
- file: z
- .string()
- .optional()
- .describe("Relative or absolute source file path"),
- symbol: z
- .string()
- .optional()
- .describe("Symbol id/name (e.g. ToolHandlers.callTool)"),
- query: z
- .string()
- .optional()
- .describe("Natural-language fallback query"),
- context: z
- .enum(["signature", "body", "with-deps", "full"])
- .default("body")
- .describe("Slice detail mode"),
- pprScore: z
- .number()
- .optional()
- .describe("Optional PPR score from context_pack pipeline"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("semantic_slice", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // index_docs — discover and index markdown documentation files
- mcpServer.registerTool(
- "index_docs",
- {
- description:
- "Discover and index all markdown documentation files (README, ADRs, guides, CHANGELOG, ARCHITECTURE) under the workspace root into DOCUMENT and SECTION graph nodes. Supports incremental mode (skips unchanged files). Emits DOC_DESCRIBES edges linking sections to the code symbols they mention.",
- inputSchema: z.object({
- workspaceRoot: z
- .string()
- .optional()
- .describe("Workspace root path (defaults to active session context)"),
- projectId: z
- .string()
- .optional()
- .describe("Project ID (defaults to active session context)"),
- incremental: z
- .boolean()
- .default(true)
- .describe("Skip files whose hash has not changed (default: true)"),
- withEmbeddings: z
- .boolean()
- .default(false)
- .describe("Also embed section content into Qdrant vector store"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("index_docs", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // search_docs — search indexed documentation sections
- mcpServer.registerTool(
- "search_docs",
- {
- description:
- "Search indexed documentation sections by full-text query or by code symbol name. Returns matching SECTION nodes with heading, source document, kind (readme/adr/guide/…), line number, relevance score, and a short content excerpt. Run index_docs first to populate the index.",
- inputSchema: z.object({
- query: z
- .string()
- .optional()
- .describe("Full-text search query (cannot be combined with symbol)"),
- symbol: z
- .string()
- .optional()
- .describe(
- "Symbol name to look up (finds Sections that document this function/class/file via DOC_DESCRIBES edges)",
- ),
- limit: z
- .number()
- .int()
- .min(1)
- .max(50)
- .default(10)
- .describe("Maximum number of results to return"),
- projectId: z
- .string()
- .optional()
- .describe("Project ID (defaults to active session context)"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("search_docs", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // ref_query — query a reference repository on the same machine
- mcpServer.registerTool(
- "ref_query",
- {
- description:
- "Query a reference repository on the same machine for architecture insights, design patterns, conventions, or code examples. Useful for borrowing context from a well-structured sibling repo when working on the current workspace.",
- inputSchema: z.object({
- repoPath: z
- .string()
- .describe(
- "Absolute path to the reference repository on this machine",
- ),
- query: z
- .string()
- .default("")
- .describe(
- "What to look for — architecture patterns, conventions, a specific concept, or a code example",
- ),
- mode: z
- .enum([
- "auto",
- "docs",
- "architecture",
- "code",
- "patterns",
- "all",
- "structure",
- ])
- .default("auto")
- .describe(
- "auto = infer from query; docs/architecture = markdown only; code/patterns = source files only; structure = dir tree only; all = everything",
- ),
- symbol: z
- .string()
- .optional()
- .describe(
- "Specific symbol name (function/class/interface) to locate in the reference repo",
- ),
- limit: z
- .number()
- .int()
- .min(1)
- .max(20)
- .default(10)
- .describe("Max results to return"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("ref_query", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // init_project_setup — one-shot: set workspace + rebuild + copilot instructions
- mcpServer.registerTool(
- "init_project_setup",
- {
- description:
- "One-shot project initialization: sets workspace context, triggers graph rebuild, and generates .github/copilot-instructions.md if not present. Use this as the first step when onboarding a new project or starting a fresh session.",
- inputSchema: z.object({
- workspaceRoot: z
- .string()
- .describe("Absolute path to the project root to initialize"),
- sourceDir: z
- .string()
- .optional()
- .describe(
- "Source directory relative to workspaceRoot (default: src)",
- ),
- projectId: z
- .string()
- .optional()
- .describe("Project identifier (default: basename of workspaceRoot)"),
- rebuildMode: z
- .enum(["incremental", "full"])
- .default("incremental")
- .describe(
- "incremental = changed files only; full = rebuild entire graph",
- ),
- withDocs: z
- .boolean()
- .default(true)
- .describe("Also index markdown docs during rebuild"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool("init_project_setup", args);
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
-
- // setup_copilot_instructions — generate .github/copilot-instructions.md
- mcpServer.registerTool(
- "setup_copilot_instructions",
- {
- description:
- "Analyze a repository and generate a tailored .github/copilot-instructions.md file with tech-stack detection, key commands, required session flow, and tool-usage guidance. Makes it immediately efficient to work with the repo via Copilot or any AI assistant.",
- inputSchema: z.object({
- targetPath: z
- .string()
- .optional()
- .describe(
- "Absolute path to the target repository (defaults to the active workspace)",
- ),
- projectName: z
- .string()
- .optional()
- .describe("Override the detected project name"),
- dryRun: z
- .boolean()
- .default(false)
- .describe("Return the generated content without writing the file"),
- overwrite: z
- .boolean()
- .default(false)
- .describe("Replace an existing copilot-instructions.md"),
- profile: z
- .enum(["compact", "balanced", "debug"])
- .default("compact")
- .describe("Response profile"),
- }),
- },
- async (args: any) => {
- if (!toolHandlers) {
- return {
- content: [{ type: "text", text: "Server not initialized" }],
- isError: true,
- };
- }
- try {
- const result = await toolHandlers.callTool(
- "setup_copilot_instructions",
- args,
- );
- return { content: [{ type: "text", text: result }] };
- } catch (error: any) {
- return {
- content: [{ type: "text", text: `Error: ${error.message}` }],
- isError: true,
- };
- }
- },
- );
+ /**
+ * Wraps registry-based tool execution into MCP response envelopes.
+ */
+ const invokeRegisteredTool = (toolName: string) => async (args: unknown) => {
+ if (!toolHandlers) {
+ return {
+ content: [{ type: "text" as const, text: "Server not initialized" }],
+ isError: true,
+ };
+ }
+ try {
+ const result = await toolHandlers.callTool(toolName, args as Record);
+ return { content: [{ type: "text" as const, text: result }] };
+ } catch (error: unknown) {
+ return {
+ content: [{ type: "text" as const, text: `Error: ${(error as Error).message}` }],
+ isError: true,
+ };
+ }
+ };
+
+ /**
+ * Registers one tool definition with zod input validation.
+ */
+ const registerTool = (name: string, description: string, inputSchema: z.ZodTypeAny) => {
+ mcpServer.registerTool(
+ name,
+ {
+ description,
+ inputSchema,
+ },
+ invokeRegisteredTool(name),
+ );
+ };
+
+ // Register all tools from centralized registry.
+ for (const definition of toolRegistry) {
+ registerTool(definition.name, definition.description, z.object(definition.inputShape));
+ }
return mcpServer;
}
-// Start server with stdio transport
+/**
+ * Process entrypoint.
+ *
+ * @remarks
+ * Chooses transport mode (`stdio` or `http`), initializes per-session state,
+ * and starts serving requests.
+ */
async function main() {
await initialize();
@@ -1591,6 +145,7 @@ async function main() {
{ server: McpServer; transport: StreamableHTTPServerTransport }
>();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleMcpRequest = async (req: any, res: any) => {
try {
const headerSessionId = req.headers?.["mcp-session-id"];
@@ -1624,7 +179,7 @@ async function main() {
if (existing?.transport === transport) {
sessions.delete(closedSessionId);
void existing.server.close().catch((closeError) => {
- console.warn(
+ logger.warn(
"[MCP] Failed to close session server after transport close:",
closeError,
);
@@ -1634,12 +189,9 @@ async function main() {
await sessionServer.connect(transport);
- await runWithRequestContext(
- { sessionId: transport.sessionId },
- async () => {
- await transport!.handleRequest(req, res, req.body);
- },
- );
+ await runWithRequestContext({ sessionId: transport.sessionId }, async () => {
+ await transport!.handleRequest(req, res, req.body);
+ });
return;
}
@@ -1659,14 +211,14 @@ async function main() {
await runWithRequestContext({ sessionId }, async () => {
await sessionState.transport.handleRequest(req, res, req.body);
});
- } catch (error: any) {
- console.error("[MCP] HTTP transport error:", error);
+ } catch (error: unknown) {
+ logger.error("[MCP] HTTP transport error:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: {
code: -32603,
- message: error?.message || "Internal server error",
+ message: (error as Error)?.message || "Internal server error",
},
id: null,
});
@@ -1674,18 +226,18 @@ async function main() {
}
};
- app.post("/", handleMcpRequest);
- app.post("/mcp", handleMcpRequest);
+ app.post("/", (req, res) => { void handleMcpRequest(req, res); });
+ app.post("/mcp", (req, res) => { void handleMcpRequest(req, res); });
- app.get("/health", (_req: any, res: any) => {
+ app.get("/health", (_req, res) => {
res.status(200).json({ status: "ok", transport: "http" });
});
// A2A Agent Card — Phase 4 / Section 0.4 of AGENT_CONTEXT_ENGINE_PLAN.md
// Allows A2A-aware orchestrators (LangGraph, AutoGen, etc.) to discover
// this server as a memory + coordination specialist agent.
- app.get("/.well-known/agent.json", (_req: any, res: any) => {
- const serverName = env.LXRAG_SERVER_NAME;
+ app.get("/.well-known/agent.json", (_req, res) => {
+ const serverName = env.LXDIG_SERVER_NAME;
res.status(200).json({
"@context": "https://schema.a2aprotocol.dev/v1",
"@type": "Agent",
@@ -1708,10 +260,10 @@ async function main() {
});
app.listen(port, () => {
- console.error(`[MCP] Server started on HTTP transport (port ${port})`);
- console.error("[MCP] Endpoints: POST / and POST /mcp");
- console.error("[MCP] A2A Agent Card: GET /.well-known/agent.json");
- console.error(
+ logger.error(`[MCP] Server started on HTTP transport (port ${port})`);
+ logger.error("[MCP] Endpoints: POST / and POST /mcp");
+ logger.error("[MCP] A2A Agent Card: GET /.well-known/agent.json");
+ logger.error(
`[MCP] Available tools: 38 (5 GraphRAG + 2 Architecture + 4 Test + 4 Progress + 4 Utility + 5 Vector Search + 2 Docs + 1 Reference + 2 Setup)`,
);
});
@@ -1723,13 +275,13 @@ async function main() {
const stdioTransport = new StdioServerTransport();
await mcpServer.connect(stdioTransport);
- console.error("[MCP] Server started on stdio transport");
- console.error(
+ logger.error("[MCP] Server started on stdio transport");
+ logger.error(
`[MCP] Available tools: 38 (5 GraphRAG + 2 Architecture + 4 Test + 4 Progress + 4 Utility + 5 Vector Search + 2 Docs + 1 Reference + 2 Setup)`,
);
}
main().catch((error) => {
- console.error("[MCP] Fatal error:", error);
+ logger.error("[MCP] Fatal error:", error);
process.exit(1);
});
diff --git a/src/test-harness.ts b/src/test-harness.ts
deleted file mode 100644
index 0e4a79f..0000000
--- a/src/test-harness.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-/**
- * Test Harness for Phase 1
- * Validates parser, builder, and orchestrator functionality
- */
-
-import * as fs from "fs";
-import * as path from "path";
-import TypeScriptParser from "./parsers/typescript-parser.js";
-import { GraphBuilder, ParsedFile } from "./graph/builder.js";
-
-import GraphOrchestrator from "./graph/orchestrator.js";
-async function testParser(): Promise {
- console.log("\n=== Testing TypeScript Parser ===");
-
- const parser = new TypeScriptParser();
- const testFile = path.join(process.cwd(), "src/types/building.types.ts");
-
- if (!fs.existsSync(testFile)) {
- console.log(`[Test] Sample file not found: ${testFile}`);
- console.log("[Test] Skipping parser test (expected for Phase 1 setup)");
- return;
- }
-
- try {
- const parsed = parser.parseFile(testFile);
- console.log(`[Test] ✓ Parsed: ${parsed.relativePath}`);
- console.log(`[Test] ✓ LOC: ${parsed.LOC}`);
- console.log(`[Test] ✓ Functions: ${parsed.functions.length}`);
- console.log(`[Test] ✓ Classes: ${parsed.classes.length}`);
- console.log(`[Test] ✓ Imports: ${parsed.imports.length}`);
- console.log(`[Test] ✓ Exports: ${parsed.exports.length}`);
-
- if (parsed.functions.length > 0) {
- console.log(`[Test] Sample function: ${parsed.functions[0].name}`);
- }
- if (parsed.classes.length > 0) {
- console.log(`[Test] Sample class: ${parsed.classes[0].name}`);
- }
- } catch (error) {
- console.error(`[Test] ✗ Parser failed: ${error}`);
- throw error;
- }
-}
-
-async function testBuilder(): Promise {
- console.log("\n=== Testing Graph Builder ===");
-
- const parser = new TypeScriptParser();
- const builder = new GraphBuilder();
- const testFile = path.join(process.cwd(), "src/types/building.types.ts");
-
- if (!fs.existsSync(testFile)) {
- console.log(`[Test] Sample file not found: ${testFile}`);
- console.log("[Test] Skipping builder test (expected for Phase 1 setup)");
- return;
- }
-
- try {
- const parsed = parser.parseFile(testFile) as unknown as ParsedFile;
-
- const statements = builder.buildFromParsedFile(parsed);
- console.log(`[Test] ✓ Generated ${statements.length} Cypher statements`);
-
- if (statements.length > 0) {
- const first = statements[0];
- console.log(`[Test] Sample statement (params):`, first.params);
- }
- } catch (error) {
- console.error(`[Test] ✗ Builder failed: ${error}`);
- throw error;
- }
-}
-
-async function testOrchestrator(): Promise {
- console.log("\n=== Testing Graph Orchestrator ===");
-
- const orchestrator = new GraphOrchestrator(undefined, true);
-
- try {
- console.log("[Test] Building graph (incremental mode)...");
- const result = await orchestrator.build({
- mode: "incremental",
- verbose: true,
- sourceDir: "src",
- exclude: ["node_modules", "dist", ".next", ".lxrag"],
- });
-
- console.log("\n[Test] ✓ Build completed!");
- console.log(`[Test] Success: ${result.success}`);
- console.log(`[Test] Duration: ${result.duration}ms`);
- console.log(`[Test] Files processed: ${result.filesProcessed}`);
- console.log(`[Test] Nodes created: ${result.nodesCreated}`);
- console.log(`[Test] Relationships: ${result.relationshipsCreated}`);
- console.log(`[Test] Files changed: ${result.filesChanged}`);
-
- if (result.errors.length > 0) {
- console.log("[Test] Errors:");
- result.errors.forEach((e) => console.log(` - ${e}`));
- }
-
- if (result.warnings.length > 0) {
- console.log("[Test] Warnings:");
- result.warnings.forEach((w) => console.log(` - ${w}`));
- }
-
- // Export snapshot
- const snapshotPath = path.join(
- process.cwd(),
- ".lxrag/cache/graph.snapshot.json",
- );
- orchestrator.exportSnapshot(snapshotPath);
- console.log(`[Test] ✓ Snapshot saved to ${snapshotPath}`);
- } catch (error) {
- console.error(`[Test] ✗ Orchestrator failed: ${error}`);
- throw error;
- }
-}
-
-async function runAllTests(): Promise {
- console.log("========================================");
- console.log("Phase 1: Code Graph MVP - Test Harness");
- console.log("========================================");
-
- try {
- await testParser();
- await testBuilder();
- await testOrchestrator();
-
- console.log("\n========================================");
- console.log("✓ All tests completed!");
- console.log("========================================");
- console.log("\nNext steps:");
- console.log("1. Verify node and relationship counts");
- console.log("2. Check .lxrag/cache/file-hashes.json for cached files");
- console.log('3. Run: npm run graph:query "MATCH (n) RETURN count(n)"');
- console.log(
- "4. Start Memgraph: docker-compose -f tools/docker/docker-compose.yml up -d",
- );
- console.log("5. Load graph: npm run graph:load");
- } catch (error) {
- console.error("\n✗ Test suite failed!");
- process.exit(1);
- }
-}
-
-// Run tests
-runAllTests().catch(console.error);
diff --git a/src/test-parser.ts b/src/test-parser.ts
deleted file mode 100644
index 1eb6f3d..0000000
--- a/src/test-parser.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * Test the TypeScript parser on a few sample files
- * Validates that the parser works before building the full graph
- */
-
-import * as path from 'path';
-import * as fs from 'fs';
-import TypeScriptParser from './parsers/typescript-parser.js';
-
-async function testParser() {
- console.log('🧪 Testing TypeScript Parser\n');
-
- const parser = new TypeScriptParser();
- await parser.initialize();
-
- // Test files to parse
- const testFiles = [
- 'src/types/building.types.ts',
- 'src/hooks/useBuildingState.ts',
- 'src/engine/calculations/columns.ts',
- 'src/context/CodeContext.tsx',
- 'src/components/drawing/GridCanvas.tsx',
- ];
-
- const projectRoot = process.cwd();
- let successCount = 0;
- let failureCount = 0;
-
- for (const testFile of testFiles) {
- const filePath = path.join(projectRoot, testFile);
-
- if (!fs.existsSync(filePath)) {
- console.log(`⏭️ SKIP: ${testFile} (not found)`);
- continue;
- }
-
- try {
- console.log(`🔍 Parsing: ${testFile}`);
- const parsed = parser.parseFile(filePath);
-
- console.log(` 📄 File: ${parsed.relativePath}`);
- console.log(` 📊 LOC: ${parsed.LOC}`);
- console.log(` 🔧 Functions: ${parsed.functions.length}`);
- console.log(` 📦 Classes/Interfaces: ${parsed.classes.length}`);
- console.log(` 📥 Imports: ${parsed.imports.length}`);
- console.log(` 📤 Exports: ${parsed.exports.length}`);
- console.log('');
-
- successCount++;
-
- // Show sample of parsed items
- if (parsed.functions.length > 0) {
- console.log(` Sample functions:`);
- parsed.functions.slice(0, 3).forEach((fn) => {
- console.log(` - ${fn.name} (line ${fn.startLine})`);
- });
- console.log('');
- }
-
- if (parsed.imports.length > 0) {
- console.log(` Sample imports:`);
- parsed.imports.slice(0, 3).forEach((imp) => {
- console.log(` - from '${imp.source}'`);
- });
- console.log('');
- }
- } catch (error) {
- console.error(` ❌ Parse error: ${error}`);
- failureCount++;
- console.log('');
- }
- }
-
- // Summary
- console.log('📈 Test Summary:');
- console.log(` ✅ Success: ${successCount}`);
- console.log(` ❌ Failures: ${failureCount}`);
- console.log(` 📊 Total: ${successCount + failureCount}`);
-
- process.exit(failureCount > 0 ? 1 : 0);
-}
-
-testParser().catch((error) => {
- console.error('❌ Test failed:', error);
- process.exit(1);
-});
diff --git a/src/tools/__tests__/embedding-manager.test.ts b/src/tools/__tests__/embedding-manager.test.ts
new file mode 100644
index 0000000..7cd0a96
--- /dev/null
+++ b/src/tools/__tests__/embedding-manager.test.ts
@@ -0,0 +1,84 @@
+/**
+ * EmbeddingManager tests — Phase E (Qdrant sync reliability)
+ * Covers E7: concurrent ensureEmbeddings calls piggyback on the first sync.
+ */
+import { describe, expect, it, vi } from "vitest";
+import { EmbeddingManager } from "../embedding-manager.js";
+
+function buildMockEngine(delayMs = 0) {
+ const generateAllEmbeddings = vi.fn().mockImplementation(async () => {
+ if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
+ return { functions: 1, classes: 0, files: 0 };
+ });
+ const storeInQdrant = vi.fn().mockResolvedValue(undefined);
+ return { generateAllEmbeddings, storeInQdrant } as any;
+}
+
+describe("EmbeddingManager", () => {
+ it("marks project ready after ensureEmbeddings completes", async () => {
+ const mgr = new EmbeddingManager();
+ const engine = buildMockEngine();
+
+ expect(mgr.isReady("proj-a")).toBe(false);
+ await mgr.ensureEmbeddings("proj-a", engine);
+ expect(mgr.isReady("proj-a")).toBe(true);
+ });
+
+ it("skips generation when engine is not provided", async () => {
+ const mgr = new EmbeddingManager();
+ await mgr.ensureEmbeddings("proj-a", undefined);
+ expect(mgr.isReady("proj-a")).toBe(false);
+ });
+
+ it("skips generation when project is already ready", async () => {
+ const mgr = new EmbeddingManager();
+ const engine = buildMockEngine();
+ mgr.setReady("proj-a", true);
+
+ await mgr.ensureEmbeddings("proj-a", engine);
+ expect(engine.generateAllEmbeddings).not.toHaveBeenCalled();
+ });
+
+ it("E7: concurrent ensureEmbeddings calls piggyback — generateAllEmbeddings called only once", async () => {
+ const mgr = new EmbeddingManager();
+ const engine = buildMockEngine(20); // 20ms delay so second call arrives mid-flight
+
+ // Fire two calls simultaneously
+ const [r1, r2] = await Promise.all([
+ mgr.ensureEmbeddings("proj-a", engine),
+ mgr.ensureEmbeddings("proj-a", engine),
+ ]);
+
+ expect(r1).toBeUndefined();
+ expect(r2).toBeUndefined();
+
+ // Despite two calls, generation should only have run once
+ expect(engine.generateAllEmbeddings).toHaveBeenCalledTimes(1);
+ expect(engine.storeInQdrant).toHaveBeenCalledTimes(1);
+ expect(mgr.isReady("proj-a")).toBe(true);
+ });
+
+ it("E7: different projects do not share the sync lock", async () => {
+ const mgr = new EmbeddingManager();
+ const engineA = buildMockEngine(10);
+ const engineB = buildMockEngine(10);
+
+ await Promise.all([
+ mgr.ensureEmbeddings("proj-a", engineA),
+ mgr.ensureEmbeddings("proj-b", engineB),
+ ]);
+
+ // Both projects get their own sync
+ expect(engineA.generateAllEmbeddings).toHaveBeenCalledTimes(1);
+ expect(engineB.generateAllEmbeddings).toHaveBeenCalledTimes(1);
+ expect(mgr.isReady("proj-a")).toBe(true);
+ expect(mgr.isReady("proj-b")).toBe(true);
+ });
+
+ it("clears readiness state", () => {
+ const mgr = new EmbeddingManager();
+ mgr.setReady("proj-a", true);
+ mgr.clear("proj-a");
+ expect(mgr.isReady("proj-a")).toBe(false);
+ });
+});
diff --git a/src/tools/__tests__/registry.test.ts b/src/tools/__tests__/registry.test.ts
new file mode 100644
index 0000000..eed3773
--- /dev/null
+++ b/src/tools/__tests__/registry.test.ts
@@ -0,0 +1,64 @@
+import { describe, expect, it } from "vitest";
+import * as z from "zod";
+import { toolRegistry, toolRegistryMap } from "../registry.js";
+
+describe("tool registry", () => {
+ it("has unique tool names", () => {
+ const names = toolRegistry.map((tool) => tool.name);
+ expect(new Set(names).size).toBe(names.length);
+ });
+
+ it("maps every tool name", () => {
+ for (const tool of toolRegistry) {
+ expect(toolRegistryMap.has(tool.name)).toBe(true);
+ }
+ });
+
+ it("has valid zod raw shapes", () => {
+ for (const tool of toolRegistry) {
+ expect(() => z.object(tool.inputShape)).not.toThrow();
+ }
+ });
+
+ it("includes migrated handler modules", () => {
+ expect(toolRegistryMap.has("arch_validate")).toBe(true);
+ expect(toolRegistryMap.has("arch_suggest")).toBe(true);
+ expect(toolRegistryMap.has("graph_query")).toBe(true);
+ expect(toolRegistryMap.has("code_explain")).toBe(true);
+ expect(toolRegistryMap.has("graph_rebuild")).toBe(true);
+ expect(toolRegistryMap.has("graph_set_workspace")).toBe(true);
+ expect(toolRegistryMap.has("graph_health")).toBe(true);
+ expect(toolRegistryMap.has("tools_list")).toBe(true);
+ expect(toolRegistryMap.has("diff_since")).toBe(true);
+ expect(toolRegistryMap.has("contract_validate")).toBe(true);
+ expect(toolRegistryMap.has("find_pattern")).toBe(true);
+ expect(toolRegistryMap.has("semantic_search")).toBe(true);
+ expect(toolRegistryMap.has("find_similar_code")).toBe(true);
+ expect(toolRegistryMap.has("code_clusters")).toBe(true);
+ expect(toolRegistryMap.has("semantic_diff")).toBe(true);
+ expect(toolRegistryMap.has("suggest_tests")).toBe(true);
+ expect(toolRegistryMap.has("context_pack")).toBe(true);
+ expect(toolRegistryMap.has("semantic_slice")).toBe(true);
+ expect(toolRegistryMap.has("init_project_setup")).toBe(true);
+ expect(toolRegistryMap.has("setup_copilot_instructions")).toBe(true);
+ expect(toolRegistryMap.has("index_docs")).toBe(true);
+ expect(toolRegistryMap.has("search_docs")).toBe(true);
+ expect(toolRegistryMap.has("ref_query")).toBe(true);
+ expect(toolRegistryMap.has("test_select")).toBe(true);
+ expect(toolRegistryMap.has("test_categorize")).toBe(true);
+ expect(toolRegistryMap.has("impact_analyze")).toBe(true);
+ expect(toolRegistryMap.has("test_run")).toBe(true);
+ expect(toolRegistryMap.has("progress_query")).toBe(true);
+ expect(toolRegistryMap.has("task_update")).toBe(true);
+ expect(toolRegistryMap.has("feature_status")).toBe(true);
+ expect(toolRegistryMap.has("blocking_issues")).toBe(true);
+ expect(toolRegistryMap.has("episode_add")).toBe(true);
+ expect(toolRegistryMap.has("episode_recall")).toBe(true);
+ expect(toolRegistryMap.has("decision_query")).toBe(true);
+ expect(toolRegistryMap.has("reflect")).toBe(true);
+ expect(toolRegistryMap.has("agent_claim")).toBe(true);
+ expect(toolRegistryMap.has("agent_release")).toBe(true);
+ expect(toolRegistryMap.has("agent_status")).toBe(true);
+ expect(toolRegistryMap.has("coordination_overview")).toBe(true);
+ });
+});
diff --git a/src/tools/__tests__/tool-handler-base.infra.test.ts b/src/tools/__tests__/tool-handler-base.infra.test.ts
new file mode 100644
index 0000000..585d4a4
--- /dev/null
+++ b/src/tools/__tests__/tool-handler-base.infra.test.ts
@@ -0,0 +1,517 @@
+/**
+ * @file tool-handler-base.infra.test.ts
+ * @description Unit tests for the infrastructure concerns that remain in
+ * ToolHandlerBase after the SOLID refactor:
+ *
+ * 1. Session lifecycle (cleanupSession, cleanupAllSessions)
+ * 2. File Watcher lifecycle (startActiveWatcher, stopActiveWatcher)
+ * 3. Build error tracking (recordBuildError, getRecentBuildErrors)
+ * 4. callTool edge cases (TOOL_NOT_FOUND, re-throw on exception)
+ * 5. initializeIndexFromMemgraph
+ * 6. Delegation contracts (each public method delegates to its collaborator)
+ */
+
+import { describe, expect, it, vi, beforeEach } from "vitest";
+import GraphIndexManager from "../../graph/index.js";
+import { ToolHandlers } from "../tool-handlers.js";
+import { runWithRequestContext } from "../../request-context.js";
+
+// ── FileWatcher module mock ───────────────────────────────────────────────────
+// Must be declared before any imports that pull in the watcher.
+vi.mock("../../graph/watcher.js", () => {
+ // Must use a regular function (not arrow) so `new MockFileWatcher()` works.
+ const MockFileWatcher = vi.fn(function (this: Record) {
+ this.start = vi.fn();
+ this.stop = vi.fn().mockResolvedValue(undefined);
+ this.pendingChanges = 0;
+ this.state = "idle";
+ });
+ return { default: MockFileWatcher, FileWatcher: MockFileWatcher };
+});
+
+import FileWatcher from "../../graph/watcher.js";
+
+// ── Shared factory ─────────────────────────────────────────────────────────────
+
+type HandlerOverrides = {
+ executeCypher?: ReturnType