Quellcode durchsuchen

Enhances code extraction and project indexing

Adds support for Dart and Liquid languages with tree-sitter parsing.
Improves accuracy of code symbol extraction for existing languages.
Indexes project files to enhance code navigation features.
Migrates build system to facilitate code contributions.
Removes git hook functionality.
Integrates Sentry for error tracking and reporting.
Enhances project initialization and configuration loading.
Colby McHenry vor 4 Monaten
Ursprung
Commit
d0ee6f7fc4
50 geänderte Dateien mit 3410 neuen und 3314 gelöschten Zeilen
  1. 14 0
      .gitignore
  2. 8 1
      CLAUDE.md
  3. 3 3
      IMPLEMENTATION_PLAN.md
  4. 0 303
      __tests__/evaluation/evaluation.test.ts
  5. 0 79
      __tests__/evaluation/fixtures/python-project/auth.py
  6. 0 54
      __tests__/evaluation/fixtures/python-project/database.py
  7. 0 305
      __tests__/evaluation/fixtures/python-project/ground-truth.ts
  8. 0 34
      __tests__/evaluation/fixtures/python-project/models.py
  9. 0 72
      __tests__/evaluation/fixtures/python-project/tasks.py
  10. 0 27
      __tests__/evaluation/fixtures/python-project/validation.py
  11. 0 366
      __tests__/evaluation/fixtures/typescript-project/ground-truth.ts
  12. 0 90
      __tests__/evaluation/fixtures/typescript-project/src/auth.ts
  13. 0 75
      __tests__/evaluation/fixtures/typescript-project/src/database.ts
  14. 0 11
      __tests__/evaluation/fixtures/typescript-project/src/index.ts
  15. 0 115
      __tests__/evaluation/fixtures/typescript-project/src/order.ts
  16. 0 60
      __tests__/evaluation/fixtures/typescript-project/src/payment.ts
  17. 0 47
      __tests__/evaluation/fixtures/typescript-project/src/types.ts
  18. 0 50
      __tests__/evaluation/fixtures/typescript-project/src/user.ts
  19. 0 21
      __tests__/evaluation/fixtures/typescript-project/src/utils/crypto.ts
  20. 0 35
      __tests__/evaluation/fixtures/typescript-project/src/utils/validation.ts
  21. 0 374
      __tests__/evaluation/runner.ts
  22. 0 163
      __tests__/evaluation/types.ts
  23. 949 0
      __tests__/extraction.test.ts
  24. 3 3
      __tests__/foundation.test.ts
  25. 3 243
      __tests__/sync.test.ts
  26. 275 137
      package-lock.json
  27. 5 4
      package.json
  28. 112 0
      scripts/patch-tree-sitter-dart.js
  29. 3 0
      scripts/postinstall.js
  30. 274 131
      src/bin/codegraph.ts
  31. 203 12
      src/context/index.ts
  32. 6 0
      src/db/index.ts
  33. 110 28
      src/db/queries.ts
  34. 10 9
      src/db/schema.sql
  35. 49 7
      src/directory.ts
  36. 6 1
      src/errors.ts
  37. 56 46
      src/extraction/grammars.ts
  38. 17 0
      src/extraction/index.ts
  39. 673 19
      src/extraction/tree-sitter.ts
  40. 45 65
      src/index.ts
  41. 0 8
      src/installer/index.ts
  42. 20 4
      src/mcp/index.ts
  43. 347 15
      src/mcp/tools.ts
  44. 2 0
      src/mcp/transport.ts
  45. 31 4
      src/resolution/index.ts
  46. 167 0
      src/sentry.ts
  47. 0 284
      src/sync/git-hooks.ts
  48. 6 7
      src/sync/index.ts
  49. 11 0
      src/types.ts
  50. 2 2
      src/vectors/embedder.ts

+ 14 - 0
.gitignore

@@ -17,6 +17,7 @@ Thumbs.db
 
 # Test coverage
 coverage/
+.nyc_output/
 
 # Environment
 .env
@@ -27,5 +28,18 @@ coverage/
 *.log
 npm-debug.log*
 
+# TypeScript build info
+*.tsbuildinfo
+
+# SQLite WAL mode files
+*.db-wal
+*.db-shm
+
+# Local Claude settings
+.claude/settings.local.json
+
 # CodeGraph data directories (in test projects)
 .codegraph/
+
+# Test language repos for manual testing
+test-languages/

+ 8 - 1
CLAUDE.md

@@ -68,10 +68,17 @@ src/
 ├── sync/                 # Incremental update system
 │   ├── index.ts
 │   └── git-hooks.ts      # Post-commit hook management
+├── installer/            # Interactive installer
+│   ├── index.ts          # Installer orchestrator
+│   ├── banner.ts         # ASCII art banner
+│   ├── claude-md-template.ts # CLAUDE.md template generator
+│   ├── config-writer.ts  # Configuration file writing
+│   └── prompts.ts        # User prompts
 ├── mcp/                  # Model Context Protocol server
 │   ├── index.ts          # MCPServer class
 │   ├── tools.ts          # MCP tool definitions
 │   └── transport.ts      # Stdio transport
+├── sentry.ts             # Error tracking/reporting
 └── bin/codegraph.ts      # CLI entry point
 ```
 
@@ -99,7 +106,7 @@ SQLite database with:
 
 ### Supported Languages
 
-TypeScript, JavaScript, TSX, JSX, Python, Go, Rust, Java, C, C++, C#, PHP, Ruby, Swift, Kotlin
+TypeScript, JavaScript, TSX, JSX, Python, Go, Rust, Java, C, C++, C#, PHP, Ruby, Swift, Kotlin, Dart, Liquid
 
 ### Node and Edge Types
 

+ 3 - 3
IMPLEMENTATION_PLAN.md

@@ -7,7 +7,7 @@ CodeGraph is a local-first code intelligence system that builds a semantic knowl
 **Type:** Headless library (no UI components — purely an API)  
 **Runtime:** Node.js (works standalone, in Electron, or any Node environment)  
 **Distribution:** npm package, installable in any project  
-**Per-Project Data:** `.codegraph/` directory in each indexed project  
+**Per-Project Data:** `.codegraph/` directory in each indexed project
 **Core Principle:** Deterministic extraction from AST, not AI-generated summaries
 
 ### Use Cases
@@ -59,7 +59,7 @@ CodeGraph is a local-first code intelligence system that builds a semantic knowl
 │  ┌─────────────────────────────────────────────────────────────┐│
 │  │                   STORAGE LAYER                             ││
 │  │         SQLite + sqlite-vss (per project)                   ││
-│  │              .codegraph/graph.db                            ││
+│  │              .codegraph/graph.db                        ││
 │  └─────────────────────────────────────────────────────────────┘│
 │                          ▲                                      │
 │                          │                                      │
@@ -83,7 +83,7 @@ CodeGraph is a local-first code intelligence system that builds a semantic knowl
 Per-Project Installation (created by codegraph init):
 ┌─────────────────────────────────────────────────────────────────┐
 │  my-laravel-app/                                                │
-│  ├── .codegraph/                                                
+│  ├── .codegraph/                                           │
 │  │   ├── graph.db            # SQLite database with vectors     │
 │  │   ├── config.json         # Project-specific settings        │
 │  │   └── .gitignore          # Ignore db, keep config           │

+ 0 - 303
__tests__/evaluation/evaluation.test.ts

@@ -1,303 +0,0 @@
-/**
- * Evaluation Tests
- *
- * Runs the evaluation suite as part of the test suite.
- * Use `npm run test:eval` to run just these tests.
- */
-
-import { describe, it, expect, beforeAll, afterAll } from 'vitest';
-import * as path from 'path';
-import * as fs from 'fs';
-import CodeGraph from '../../src/index';
-import type { TestCase, TestCaseResult } from './types';
-import { typescriptFixture } from './fixtures/typescript-project/ground-truth';
-import { pythonFixture } from './fixtures/python-project/ground-truth';
-
-/**
- * Extract symbol names from nodes
- */
-function extractSymbolNames(nodes: { name: string }[]): Set<string> {
-  return new Set(nodes.map(n => n.name.toLowerCase()));
-}
-
-/**
- * Normalize symbol name
- */
-function normalizeSymbol(symbol: string): string {
-  return symbol.split('.').pop()?.toLowerCase() || symbol.toLowerCase();
-}
-
-/**
- * Check if symbol matches
- */
-function symbolMatches(symbol: string, candidates: Set<string>): boolean {
-  const normalized = normalizeSymbol(symbol);
-  for (const candidate of candidates) {
-    if (normalizeSymbol(candidate) === normalized) return true;
-  }
-  return false;
-}
-
-/**
- * Find a target node by name, supporting qualified names like "ClassName.methodName"
- */
-function findTargetNode(cg: CodeGraph, targetSymbol: string): { id: string; name: string } | null {
-  // Check if it's a qualified name (e.g., "OrderService.createOrder")
-  const parts = targetSymbol.split('.');
-
-  if (parts.length === 2) {
-    const [className, methodName] = parts;
-    // Search for the method name and filter by qualified name containing the class
-    const results = cg.searchNodes(methodName!, { limit: 20 });
-    for (const r of results) {
-      if (r.node.qualifiedName.includes(className!) && r.node.name === methodName) {
-        return { id: r.node.id, name: r.node.name };
-      }
-    }
-  }
-
-  // Fall back to simple search
-  const results = cg.searchNodes(targetSymbol, { limit: 1 });
-  if (results.length > 0 && results[0]) {
-    return { id: results[0].node.id, name: results[0].node.name };
-  }
-
-  return null;
-}
-
-/**
- * Run a single test case and return metrics
- */
-async function runSingleTest(cg: CodeGraph, testCase: TestCase): Promise<TestCaseResult> {
-  let retrievedNodes: { name: string; id: string }[] = [];
-
-  switch (testCase.type) {
-    case 'search': {
-      const results = cg.searchNodes(testCase.query, { limit: 20 });
-      retrievedNodes = results.map(r => ({ name: r.node.name, id: r.node.id }));
-      break;
-    }
-
-    case 'context': {
-      // Use buildContext to get semantic search + graph traversal
-      const context = await cg.buildContext(testCase.query, {
-        maxNodes: 30,
-        traversalDepth: 2,
-        searchLimit: 5,
-        format: 'object',
-      });
-      // Extract nodes from the subgraph
-      if (typeof context !== 'string' && context.subgraph) {
-        retrievedNodes = Array.from(context.subgraph.nodes.values()).map(n => ({
-          name: n.name,
-          id: n.id,
-        }));
-      }
-      break;
-    }
-
-    case 'callers': {
-      if (testCase.targetSymbol) {
-        const targetNode = findTargetNode(cg, testCase.targetSymbol);
-        if (targetNode) {
-          const callers = cg.getCallers(targetNode.id);
-          retrievedNodes = callers.map(c => ({ name: c.node.name, id: c.node.id }));
-        }
-      }
-      break;
-    }
-
-    case 'callees': {
-      if (testCase.targetSymbol) {
-        const targetNode = findTargetNode(cg, testCase.targetSymbol);
-        if (targetNode) {
-          const callees = cg.getCallees(targetNode.id);
-          retrievedNodes = callees.map(c => ({ name: c.node.name, id: c.node.id }));
-        }
-      }
-      break;
-    }
-
-    case 'impact': {
-      if (testCase.targetSymbol) {
-        const targetNode = findTargetNode(cg, testCase.targetSymbol);
-        if (targetNode) {
-          const impact = cg.getImpactRadius(targetNode.id, 2);
-          retrievedNodes = Array.from(impact.nodes.values()).map(n => ({ name: n.name, id: n.id }));
-        }
-      }
-      break;
-    }
-  }
-
-  // Calculate metrics
-  const retrievedSymbols = extractSymbolNames(retrievedNodes);
-
-  const truePositives: string[] = [];
-  const falsePositives: string[] = [];
-
-  for (const symbol of retrievedSymbols) {
-    if (symbolMatches(symbol, new Set(testCase.expectedSymbols))) {
-      truePositives.push(symbol);
-    } else if (symbolMatches(symbol, new Set(testCase.irrelevantSymbols))) {
-      falsePositives.push(symbol);
-    }
-  }
-
-  const falseNegatives: string[] = [];
-  for (const expected of testCase.expectedSymbols) {
-    if (!symbolMatches(expected, retrievedSymbols)) {
-      falseNegatives.push(expected);
-    }
-  }
-
-  const totalRetrieved = truePositives.length + falsePositives.length;
-  const precision = totalRetrieved > 0 ? truePositives.length / totalRetrieved : 0;
-
-  const totalRelevant = testCase.expectedSymbols.length;
-  const recall = totalRelevant > 0 ? truePositives.length / totalRelevant : 0;
-
-  const f1Score = precision + recall > 0
-    ? 2 * (precision * recall) / (precision + recall)
-    : 0;
-
-  // Check if passed thresholds (with 20% margin)
-  const passedRecall = !testCase.minRecall || recall >= testCase.minRecall * 0.8;
-  const passedPrecision = !testCase.minPrecision || precision >= testCase.minPrecision * 0.8;
-
-  return {
-    testCaseId: testCase.id,
-    passed: passedRecall && passedPrecision,
-    precision,
-    recall,
-    f1Score,
-    truePositives,
-    falsePositives,
-    falseNegatives,
-    contextTokens: 0,
-    executionTimeMs: 0,
-  };
-}
-
-/**
- * Print a results table
- */
-function printResultsTable(results: TestCaseResult[], fixtureName: string): void {
-  console.log(`\n${'='.repeat(80)}`);
-  console.log(`  ${fixtureName} Results`);
-  console.log('='.repeat(80));
-  console.log('');
-  console.log('  Test ID                              Type       Prec    Recall  F1     Status');
-  console.log('  ' + '-'.repeat(76));
-
-  for (const r of results) {
-    const id = r.testCaseId.padEnd(35);
-    const type = r.testCaseId.split('-')[1]?.padEnd(10) || ''.padEnd(10);
-    const prec = `${(r.precision * 100).toFixed(0)}%`.padStart(5);
-    const recall = `${(r.recall * 100).toFixed(0)}%`.padStart(6);
-    const f1 = `${(r.f1Score * 100).toFixed(0)}%`.padStart(5);
-    const status = r.passed ? '✓' : '✗';
-    console.log(`  ${id} ${type} ${prec}   ${recall}  ${f1}    ${status}`);
-  }
-
-  const avgPrecision = results.reduce((sum, r) => sum + r.precision, 0) / results.length;
-  const avgRecall = results.reduce((sum, r) => sum + r.recall, 0) / results.length;
-  const avgF1 = results.reduce((sum, r) => sum + r.f1Score, 0) / results.length;
-  const passRate = results.filter(r => r.passed).length / results.length;
-
-  console.log('  ' + '-'.repeat(76));
-  console.log(`  ${'AVERAGE'.padEnd(35)} ${''.padEnd(10)} ${`${(avgPrecision * 100).toFixed(0)}%`.padStart(5)}   ${`${(avgRecall * 100).toFixed(0)}%`.padStart(6)}  ${`${(avgF1 * 100).toFixed(0)}%`.padStart(5)}    ${(passRate * 100).toFixed(0)}%`);
-  console.log('');
-}
-
-describe('CodeGraph Evaluation', () => {
-  describe('TypeScript Fixture', () => {
-    let cg: CodeGraph;
-    const fixturePath = path.resolve(__dirname, 'fixtures/typescript-project');
-    const results: TestCaseResult[] = [];
-
-    beforeAll(async () => {
-      // Clean up any existing index
-      const codegraphDir = path.join(fixturePath, '.codegraph');
-      if (fs.existsSync(codegraphDir)) {
-        fs.rmSync(codegraphDir, { recursive: true });
-      }
-
-      // Initialize and index
-      cg = await CodeGraph.init(fixturePath, { index: true });
-
-      // Initialize embeddings for semantic search
-      await cg.initializeEmbeddings();
-      await cg.generateEmbeddings();
-    }, 120000);
-
-    afterAll(() => {
-      // Print summary table after all tests
-      printResultsTable(results, 'TypeScript');
-
-      if (cg) {
-        cg.destroy();
-      }
-    });
-
-    it('should index all files', () => {
-      const stats = cg.getStats();
-      expect(stats.fileCount).toBeGreaterThanOrEqual(typescriptFixture.totalFiles);
-    });
-
-    // Generate test for each test case - collect results but don't fail
-    for (const testCase of typescriptFixture.testCases) {
-      it(`${testCase.id}: ${testCase.description}`, async () => {
-        const result = await runSingleTest(cg, testCase);
-        results.push(result);
-        // Don't assert - just collect results
-        expect(true).toBe(true);
-      });
-    }
-  });
-
-  describe('Python Fixture', () => {
-    let cg: CodeGraph;
-    const fixturePath = path.resolve(__dirname, 'fixtures/python-project');
-    const results: TestCaseResult[] = [];
-
-    beforeAll(async () => {
-      // Clean up any existing index
-      const codegraphDir = path.join(fixturePath, '.codegraph');
-      if (fs.existsSync(codegraphDir)) {
-        fs.rmSync(codegraphDir, { recursive: true });
-      }
-
-      // Initialize and index
-      cg = await CodeGraph.init(fixturePath, { index: true });
-
-      // Initialize embeddings for semantic search
-      await cg.initializeEmbeddings();
-      await cg.generateEmbeddings();
-    }, 120000);
-
-    afterAll(() => {
-      // Print summary table after all tests
-      printResultsTable(results, 'Python');
-
-      if (cg) {
-        cg.destroy();
-      }
-    });
-
-    it('should index all files', () => {
-      const stats = cg.getStats();
-      expect(stats.fileCount).toBeGreaterThanOrEqual(pythonFixture.totalFiles);
-    });
-
-    // Generate test for each test case - collect results but don't fail
-    for (const testCase of pythonFixture.testCases) {
-      it(`${testCase.id}: ${testCase.description}`, async () => {
-        const result = await runSingleTest(cg, testCase);
-        results.push(result);
-        // Don't assert - just collect results
-        expect(true).toBe(true);
-      });
-    }
-  });
-});

+ 0 - 79
__tests__/evaluation/fixtures/python-project/auth.py

@@ -1,79 +0,0 @@
-"""Authentication service."""
-
-import hashlib
-import secrets
-from datetime import datetime
-from typing import Optional, Tuple
-
-from models import User
-from database import db
-from validation import validate_email, validate_password
-
-
-def hash_password(password: str) -> str:
-    """Hash a password for storage."""
-    salt = secrets.token_hex(16)
-    hash_obj = hashlib.sha256((password + salt).encode())
-    return f"{salt}:{hash_obj.hexdigest()}"
-
-
-def verify_password(password: str, password_hash: str) -> bool:
-    """Verify a password against its hash."""
-    salt, stored_hash = password_hash.split(":")
-    hash_obj = hashlib.sha256((password + salt).encode())
-    return hash_obj.hexdigest() == stored_hash
-
-
-def generate_token() -> str:
-    """Generate a secure random token."""
-    return secrets.token_urlsafe(32)
-
-
-class AuthService:
-    def __init__(self):
-        self.tokens: dict = {}
-
-    def register(self, email: str, password: str, name: str) -> Tuple[bool, str]:
-        """Register a new user."""
-        if not validate_email(email):
-            return False, "Invalid email format"
-
-        if not validate_password(password):
-            return False, "Password too weak"
-
-        if db.get_user_by_email(email):
-            return False, "Email already registered"
-
-        user = User(
-            id=generate_token(),
-            email=email,
-            name=name,
-            password_hash=hash_password(password),
-            created_at=datetime.now(),
-        )
-        db.create_user(user)
-        return True, user.id
-
-    def login(self, email: str, password: str) -> Optional[str]:
-        """Authenticate user and return token."""
-        user = db.get_user_by_email(email)
-        if not user:
-            return None
-
-        if not verify_password(password, user.password_hash):
-            return None
-
-        token = generate_token()
-        self.tokens[token] = user.id
-        return token
-
-    def logout(self, token: str) -> None:
-        """Invalidate a token."""
-        self.tokens.pop(token, None)
-
-    def get_user_id(self, token: str) -> Optional[str]:
-        """Get user ID from token."""
-        return self.tokens.get(token)
-
-
-auth_service = AuthService()

+ 0 - 54
__tests__/evaluation/fixtures/python-project/database.py

@@ -1,54 +0,0 @@
-"""Database operations."""
-
-from typing import Optional, List, Dict
-from models import User, Task, Project
-
-
-class Database:
-    def __init__(self):
-        self.users: Dict[str, User] = {}
-        self.tasks: Dict[str, Task] = {}
-        self.projects: Dict[str, Project] = {}
-
-    def get_user(self, user_id: str) -> Optional[User]:
-        return self.users.get(user_id)
-
-    def get_user_by_email(self, email: str) -> Optional[User]:
-        for user in self.users.values():
-            if user.email == email:
-                return user
-        return None
-
-    def create_user(self, user: User) -> None:
-        self.users[user.id] = user
-
-    def get_task(self, task_id: str) -> Optional[Task]:
-        return self.tasks.get(task_id)
-
-    def get_user_tasks(self, user_id: str) -> List[Task]:
-        return [t for t in self.tasks.values() if t.user_id == user_id]
-
-    def create_task(self, task: Task) -> None:
-        self.tasks[task.id] = task
-
-    def update_task(self, task_id: str, **updates) -> Optional[Task]:
-        task = self.tasks.get(task_id)
-        if task:
-            for key, value in updates.items():
-                setattr(task, key, value)
-        return task
-
-    def delete_task(self, task_id: str) -> bool:
-        if task_id in self.tasks:
-            del self.tasks[task_id]
-            return True
-        return False
-
-    def get_project(self, project_id: str) -> Optional[Project]:
-        return self.projects.get(project_id)
-
-    def create_project(self, project: Project) -> None:
-        self.projects[project.id] = project
-
-
-db = Database()

+ 0 - 305
__tests__/evaluation/fixtures/python-project/ground-truth.ts

@@ -1,305 +0,0 @@
-/**
- * Ground truth definitions for the Python task management fixture
- */
-
-import { FixtureGroundTruth } from '../../types';
-
-export const pythonFixture: FixtureGroundTruth = {
-  name: 'python-taskmanager',
-  path: '__tests__/evaluation/fixtures/python-project',
-  language: 'python',
-  totalFiles: 5,
-  approximateTokens: 1200, // Rough estimate
-
-  testCases: [
-    // =========================================================================
-    // Search Tests
-    // =========================================================================
-    {
-      id: 'py-search-auth',
-      description: 'Search for authentication functionality',
-      query: 'authentication login',
-      type: 'search',
-      expectedSymbols: ['AuthService', 'AuthService.login', 'AuthService.register', 'verify_password'],
-      irrelevantSymbols: ['TaskService', 'validate_task_title', 'Project'],
-      minRecall: 0.7,
-      minPrecision: 0.5,
-    },
-    {
-      id: 'py-search-task',
-      description: 'Search for task management',
-      query: 'task create complete',
-      type: 'search',
-      expectedSymbols: ['TaskService', 'TaskService.create_task', 'TaskService.complete_task', 'Task'],
-      irrelevantSymbols: ['AuthService', 'validate_email', 'hash_password'],
-      minRecall: 0.7,
-      minPrecision: 0.5,
-    },
-    {
-      id: 'py-search-validation',
-      description: 'Search for validation',
-      query: 'validate',
-      type: 'search',
-      expectedSymbols: ['validate_email', 'validate_password', 'validate_task_title'],
-      irrelevantSymbols: ['hash_password', 'generate_token', 'TaskService'],
-      minRecall: 0.8,
-      minPrecision: 0.6,
-    },
-
-    // =========================================================================
-    // Context Tests
-    // =========================================================================
-    {
-      id: 'py-context-login-bug',
-      description: 'Build context for fixing login issues',
-      query: 'debug why users cannot log in',
-      type: 'context',
-      expectedSymbols: [
-        'AuthService.login',
-        'verify_password',
-        'db.get_user_by_email',
-        'User',
-        'hash_password',
-      ],
-      irrelevantSymbols: [
-        'TaskService',
-        'validate_task_title',
-        'Project',
-        'Task',
-      ],
-      minRecall: 0.8,
-      minPrecision: 0.6,
-    },
-    {
-      id: 'py-context-task-creation',
-      description: 'Build context for task creation flow',
-      query: 'understand how tasks are created',
-      type: 'context',
-      expectedSymbols: [
-        'TaskService.create_task',
-        'validate_task_title',
-        'auth_service.get_user_id',
-        'db.create_task',
-        'Task',
-        'generate_token',
-      ],
-      irrelevantSymbols: [
-        'validate_email',
-        'hash_password',
-        'AuthService.register',
-        'Project',
-      ],
-      minRecall: 0.7,
-      minPrecision: 0.5,
-    },
-    {
-      id: 'py-context-user-registration',
-      description: 'Build context for user registration',
-      query: 'add email confirmation to registration',
-      type: 'context',
-      expectedSymbols: [
-        'AuthService.register',
-        'validate_email',
-        'validate_password',
-        'hash_password',
-        'db.create_user',
-        'User',
-      ],
-      irrelevantSymbols: [
-        'TaskService',
-        'validate_task_title',
-        'Task',
-        'Project',
-      ],
-      minRecall: 0.7,
-      minPrecision: 0.6,
-    },
-
-    // =========================================================================
-    // Callers Tests
-    // =========================================================================
-    {
-      id: 'py-callers-get_user_id',
-      description: 'Find all callers of auth_service.get_user_id',
-      query: 'get_user_id',
-      type: 'callers',
-      targetSymbol: 'get_user_id',
-      expectedSymbols: [
-        'TaskService.create_task',
-        'TaskService.get_task',
-        'TaskService.get_user_tasks',
-      ],
-      irrelevantSymbols: [
-        'AuthService.login',
-        'validate_email',
-        'hash_password',
-      ],
-      minRecall: 1.0,
-      minPrecision: 1.0,
-    },
-    {
-      id: 'py-callers-validate_email',
-      description: 'Find all callers of validate_email',
-      query: 'validate_email',
-      type: 'callers',
-      targetSymbol: 'validate_email',
-      expectedSymbols: [
-        'AuthService.register',
-      ],
-      irrelevantSymbols: [
-        'TaskService',
-        'validate_password',
-        'hash_password',
-      ],
-      minRecall: 1.0,
-      minPrecision: 1.0,
-    },
-    {
-      id: 'py-callers-generate_token',
-      description: 'Find all callers of generate_token',
-      query: 'generate_token',
-      type: 'callers',
-      targetSymbol: 'generate_token',
-      expectedSymbols: [
-        'AuthService.register',
-        'AuthService.login',
-        'TaskService.create_task',
-      ],
-      irrelevantSymbols: [
-        'validate_email',
-        'validate_password',
-        'db.get_user',
-      ],
-      minRecall: 1.0,
-      minPrecision: 1.0,
-    },
-
-    // =========================================================================
-    // Callees Tests
-    // =========================================================================
-    {
-      id: 'py-callees-login',
-      description: 'Find what AuthService.login calls',
-      query: 'login',
-      type: 'callees',
-      targetSymbol: 'login',
-      expectedSymbols: [
-        'db.get_user_by_email',
-        'verify_password',
-        'generate_token',
-      ],
-      irrelevantSymbols: [
-        'validate_email',
-        'hash_password',
-        'validate_task_title',
-      ],
-      minRecall: 1.0,
-      minPrecision: 1.0,
-    },
-    {
-      id: 'py-callees-create_task',
-      description: 'Find what TaskService.create_task calls',
-      query: 'create_task',
-      type: 'callees',
-      targetSymbol: 'TaskService.create_task',
-      expectedSymbols: [
-        'auth_service.get_user_id',
-        'validate_task_title',
-        'generate_token',
-        'db.create_task',
-      ],
-      irrelevantSymbols: [
-        'validate_email',
-        'hash_password',
-        'db.get_user',
-      ],
-      minRecall: 0.8,
-      minPrecision: 0.8,
-    },
-
-    // =========================================================================
-    // Impact Tests
-    // =========================================================================
-    {
-      id: 'py-impact-generate_token',
-      description: 'Impact of changing generate_token',
-      query: 'generate_token',
-      type: 'impact',
-      targetSymbol: 'generate_token',
-      expectedSymbols: [
-        // Direct callers
-        'AuthService.register',
-        'AuthService.login',
-        'TaskService.create_task',
-      ],
-      irrelevantSymbols: [
-        'validate_email',
-        'validate_task_title',
-        'db.get_project',
-      ],
-      minRecall: 0.8,
-      minPrecision: 0.7,
-    },
-    {
-      id: 'py-impact-get_user_id',
-      description: 'Impact of changing get_user_id',
-      query: 'get_user_id',
-      type: 'impact',
-      targetSymbol: 'get_user_id',
-      expectedSymbols: [
-        'TaskService.create_task',
-        'TaskService.get_task',
-        'TaskService.get_user_tasks',
-        'TaskService.complete_task',
-        'TaskService.delete_task',
-      ],
-      irrelevantSymbols: [
-        'AuthService.register',
-        'validate_email',
-        'hash_password',
-      ],
-      minRecall: 0.8,
-      minPrecision: 0.7,
-    },
-  ],
-
-  // Known call graph edges for validation
-  callGraph: [
-    // Auth -> Database
-    { caller: 'AuthService.register', callee: 'db.get_user_by_email' },
-    { caller: 'AuthService.register', callee: 'db.create_user' },
-    { caller: 'AuthService.login', callee: 'db.get_user_by_email' },
-
-    // Auth -> Crypto
-    { caller: 'AuthService.register', callee: 'hash_password' },
-    { caller: 'AuthService.register', callee: 'generate_token' },
-    { caller: 'AuthService.login', callee: 'verify_password' },
-    { caller: 'AuthService.login', callee: 'generate_token' },
-
-    // Auth -> Validation
-    { caller: 'AuthService.register', callee: 'validate_email' },
-    { caller: 'AuthService.register', callee: 'validate_password' },
-
-    // Task -> Auth
-    { caller: 'TaskService.create_task', callee: 'auth_service.get_user_id' },
-    { caller: 'TaskService.get_task', callee: 'auth_service.get_user_id' },
-    { caller: 'TaskService.get_user_tasks', callee: 'auth_service.get_user_id' },
-
-    // Task -> Database
-    { caller: 'TaskService.create_task', callee: 'db.create_task' },
-    { caller: 'TaskService.get_task', callee: 'db.get_task' },
-    { caller: 'TaskService.get_user_tasks', callee: 'db.get_user_tasks' },
-    { caller: 'TaskService.complete_task', callee: 'db.update_task' },
-    { caller: 'TaskService.delete_task', callee: 'db.delete_task' },
-
-    // Task -> Crypto
-    { caller: 'TaskService.create_task', callee: 'generate_token' },
-
-    // Task -> Validation
-    { caller: 'TaskService.create_task', callee: 'validate_task_title' },
-
-    // Task -> Task (internal)
-    { caller: 'TaskService.complete_task', callee: 'TaskService.get_task' },
-    { caller: 'TaskService.delete_task', callee: 'TaskService.get_task' },
-  ],
-};

+ 0 - 34
__tests__/evaluation/fixtures/python-project/models.py

@@ -1,34 +0,0 @@
-"""Data models for the application."""
-
-from dataclasses import dataclass
-from datetime import datetime
-from typing import Optional, List
-
-
-@dataclass
-class User:
-    id: str
-    email: str
-    name: str
-    password_hash: str
-    created_at: datetime
-
-
-@dataclass
-class Task:
-    id: str
-    user_id: str
-    title: str
-    description: Optional[str]
-    completed: bool
-    created_at: datetime
-    completed_at: Optional[datetime] = None
-
-
-@dataclass
-class Project:
-    id: str
-    user_id: str
-    name: str
-    tasks: List[str]  # Task IDs
-    created_at: datetime

+ 0 - 72
__tests__/evaluation/fixtures/python-project/tasks.py

@@ -1,72 +0,0 @@
-"""Task management service."""
-
-from datetime import datetime
-from typing import Optional, List
-
-from models import Task
-from database import db
-from auth import auth_service, generate_token
-from validation import validate_task_title
-
-
-class TaskService:
-    def create_task(
-        self, token: str, title: str, description: Optional[str] = None
-    ) -> Optional[Task]:
-        """Create a new task."""
-        user_id = auth_service.get_user_id(token)
-        if not user_id:
-            return None
-
-        if not validate_task_title(title):
-            return None
-
-        task = Task(
-            id=generate_token(),
-            user_id=user_id,
-            title=title,
-            description=description,
-            completed=False,
-            created_at=datetime.now(),
-        )
-        db.create_task(task)
-        return task
-
-    def get_task(self, token: str, task_id: str) -> Optional[Task]:
-        """Get a task by ID."""
-        user_id = auth_service.get_user_id(token)
-        if not user_id:
-            return None
-
-        task = db.get_task(task_id)
-        if task and task.user_id == user_id:
-            return task
-        return None
-
-    def get_user_tasks(self, token: str) -> List[Task]:
-        """Get all tasks for the authenticated user."""
-        user_id = auth_service.get_user_id(token)
-        if not user_id:
-            return []
-
-        return db.get_user_tasks(user_id)
-
-    def complete_task(self, token: str, task_id: str) -> bool:
-        """Mark a task as completed."""
-        task = self.get_task(token, task_id)
-        if not task:
-            return False
-
-        db.update_task(task_id, completed=True, completed_at=datetime.now())
-        return True
-
-    def delete_task(self, token: str, task_id: str) -> bool:
-        """Delete a task."""
-        task = self.get_task(token, task_id)
-        if not task:
-            return False
-
-        return db.delete_task(task_id)
-
-
-task_service = TaskService()

+ 0 - 27
__tests__/evaluation/fixtures/python-project/validation.py

@@ -1,27 +0,0 @@
-"""Validation utilities."""
-
-import re
-
-
-def validate_email(email: str) -> bool:
-    """Validate email format."""
-    pattern = r'^[^\s@]+@[^\s@]+\.[^\s@]+$'
-    return bool(re.match(pattern, email))
-
-
-def validate_password(password: str) -> bool:
-    """Validate password strength."""
-    if len(password) < 8:
-        return False
-    if not re.search(r'[A-Z]', password):
-        return False
-    if not re.search(r'[a-z]', password):
-        return False
-    if not re.search(r'[0-9]', password):
-        return False
-    return True
-
-
-def validate_task_title(title: str) -> bool:
-    """Validate task title."""
-    return bool(title and len(title.strip()) >= 1 and len(title) <= 200)

+ 0 - 366
__tests__/evaluation/fixtures/typescript-project/ground-truth.ts

@@ -1,366 +0,0 @@
-/**
- * Ground truth definitions for the TypeScript e-commerce fixture
- */
-
-import { FixtureGroundTruth } from '../../types';
-
-export const typescriptFixture: FixtureGroundTruth = {
-  name: 'typescript-ecommerce',
-  path: '__tests__/evaluation/fixtures/typescript-project',
-  language: 'typescript',
-  totalFiles: 9,
-  approximateTokens: 2500, // Rough estimate
-
-  testCases: [
-    // =========================================================================
-    // Search Tests
-    // =========================================================================
-    {
-      id: 'ts-search-login',
-      description: 'Search for login functionality',
-      query: 'login',
-      type: 'search',
-      expectedSymbols: ['AuthService.login', 'AuthService'],
-      irrelevantSymbols: ['PaymentService', 'OrderService', 'calculateTotal'],
-      minRecall: 0.8,
-      minPrecision: 0.5,
-    },
-    {
-      id: 'ts-search-validation',
-      description: 'Search for validation functions',
-      query: 'validate',
-      type: 'search',
-      expectedSymbols: ['validateEmail', 'validatePassword', 'validateQuantity', 'validatePrice', 'validateToken'],
-      irrelevantSymbols: ['hashPassword', 'generateToken', 'calculateTotal'],
-      minRecall: 0.6,
-      minPrecision: 0.6,
-    },
-    {
-      id: 'ts-search-payment',
-      description: 'Search for payment processing',
-      query: 'payment process',
-      type: 'search',
-      expectedSymbols: ['PaymentService', 'processPayment', 'payOrder'],
-      irrelevantSymbols: ['AuthService', 'UserService', 'validateEmail'],
-      minRecall: 0.7,
-      minPrecision: 0.5,
-    },
-
-    // =========================================================================
-    // Context Tests (simulating Claude asking for context)
-    // =========================================================================
-    {
-      id: 'ts-context-login-bug',
-      description: 'Build context for fixing a login bug',
-      query: 'fix the bug where login fails with valid credentials',
-      type: 'context',
-      expectedSymbols: [
-        'AuthService.login',
-        'verifyPassword',
-        'db.findUserByEmail',
-        'User',
-        'AuthToken',
-      ],
-      irrelevantSymbols: [
-        'OrderService',
-        'PaymentService',
-        'calculateTotal',
-        'validateQuantity',
-        'Product',
-      ],
-      minRecall: 0.8,
-      minPrecision: 0.6,
-    },
-    {
-      id: 'ts-context-order-creation',
-      description: 'Build context for understanding order creation flow',
-      query: 'understand how orders are created and validated',
-      type: 'context',
-      expectedSymbols: [
-        'OrderService.createOrder',
-        'validateQuantity',
-        'db.findProductById',
-        'db.createOrder',
-        'paymentService.calculateTotal',
-        'Order',
-        'OrderItem',
-      ],
-      irrelevantSymbols: [
-        'AuthService.register',
-        'validateEmail',
-        'hashPassword',
-        'UserService',
-      ],
-      minRecall: 0.7,
-      minPrecision: 0.5,
-    },
-    {
-      id: 'ts-context-add-refund',
-      description: 'Build context for adding refund functionality',
-      query: 'add ability to request a refund for paid orders',
-      type: 'context',
-      expectedSymbols: [
-        'PaymentService.refundPayment',
-        'OrderService.cancelOrder',
-        'db.updateOrderStatus',
-        'Order',
-        'PaymentResult',
-      ],
-      irrelevantSymbols: [
-        'AuthService.register',
-        'validateEmail',
-        'hashPassword',
-        'UserService.updateProfile',
-      ],
-      minRecall: 0.7,
-      minPrecision: 0.5,
-    },
-    {
-      id: 'ts-context-user-registration',
-      description: 'Build context for user registration flow',
-      query: 'implement email verification during user registration',
-      type: 'context',
-      expectedSymbols: [
-        'AuthService.register',
-        'validateEmail',
-        'hashPassword',
-        'db.createUser',
-        'db.findUserByEmail',
-        'User',
-      ],
-      irrelevantSymbols: [
-        'OrderService',
-        'PaymentService',
-        'calculateTotal',
-        'Product',
-      ],
-      minRecall: 0.7,
-      minPrecision: 0.6,
-    },
-
-    // =========================================================================
-    // Callers Tests
-    // =========================================================================
-    {
-      id: 'ts-callers-validateEmail',
-      description: 'Find all callers of validateEmail',
-      query: 'validateEmail',
-      type: 'callers',
-      targetSymbol: 'validateEmail',
-      expectedSymbols: [
-        'AuthService.register',
-        'UserService.updateProfile',
-      ],
-      irrelevantSymbols: [
-        'OrderService',
-        'PaymentService',
-        'validateQuantity',
-      ],
-      minRecall: 1.0, // Should find all callers
-      minPrecision: 1.0,
-    },
-    {
-      id: 'ts-callers-findUserByEmail',
-      description: 'Find all callers of db.findUserByEmail',
-      query: 'findUserByEmail',
-      type: 'callers',
-      targetSymbol: 'findUserByEmail',
-      expectedSymbols: [
-        'AuthService.register',
-        'AuthService.login',
-        'UserService.getUserByEmail',
-        'UserService.updateProfile',
-      ],
-      irrelevantSymbols: [
-        'OrderService',
-        'PaymentService',
-        'findProductById',
-      ],
-      minRecall: 1.0,
-      minPrecision: 1.0,
-    },
-    {
-      id: 'ts-callers-generateToken',
-      description: 'Find all callers of generateToken',
-      query: 'generateToken',
-      type: 'callers',
-      targetSymbol: 'generateToken',
-      expectedSymbols: [
-        'AuthService.register',
-        'AuthService.createToken',
-        'PaymentService.processPayment',
-        'PaymentService.refundPayment',
-      ],
-      irrelevantSymbols: [
-        'validateEmail',
-        'validateQuantity',
-        'calculateTotal',
-      ],
-      minRecall: 1.0,
-      minPrecision: 1.0,
-    },
-
-    // =========================================================================
-    // Callees Tests
-    // =========================================================================
-    {
-      id: 'ts-callees-login',
-      description: 'Find what AuthService.login calls',
-      query: 'login',
-      type: 'callees',
-      targetSymbol: 'login',
-      expectedSymbols: [
-        'db.findUserByEmail',
-        'verifyPassword',
-        'createToken',
-      ],
-      irrelevantSymbols: [
-        'hashPassword',
-        'validateQuantity',
-        'calculateTotal',
-      ],
-      minRecall: 1.0,
-      minPrecision: 1.0,
-    },
-    {
-      id: 'ts-callees-createOrder',
-      description: 'Find what OrderService.createOrder calls',
-      query: 'createOrder',
-      type: 'callees',
-      targetSymbol: 'OrderService.createOrder',
-      expectedSymbols: [
-        'authService.validateToken',
-        'validateQuantity',
-        'db.findProductById',
-        'paymentService.calculateTotal',
-        'generateOrderId',
-        'db.createOrder',
-        'db.updateProductStock',
-      ],
-      irrelevantSymbols: [
-        'validateEmail',
-        'hashPassword',
-        'refundPayment',
-      ],
-      minRecall: 0.8,
-      minPrecision: 0.8,
-    },
-
-    // =========================================================================
-    // Impact Tests
-    // =========================================================================
-    {
-      id: 'ts-impact-generateToken',
-      description: 'Impact of changing generateToken',
-      query: 'generateToken',
-      type: 'impact',
-      targetSymbol: 'generateToken',
-      expectedSymbols: [
-        // Direct callers
-        'AuthService.register',
-        'AuthService.createToken',
-        'PaymentService.processPayment',
-        'PaymentService.refundPayment',
-        // Indirect (callers of callers)
-        'AuthService.login',
-        'AuthService.refreshToken',
-        'OrderService.payOrder',
-        'OrderService.cancelOrder',
-      ],
-      irrelevantSymbols: [
-        'validateQuantity',
-        'validatePrice',
-        'UserService.getUser',
-      ],
-      minRecall: 0.7,
-      minPrecision: 0.6,
-    },
-    {
-      id: 'ts-impact-validateToken',
-      description: 'Impact of changing validateToken',
-      query: 'validateToken',
-      type: 'impact',
-      targetSymbol: 'validateToken',
-      expectedSymbols: [
-        // Direct callers
-        'AuthService.refreshToken',
-        'OrderService.createOrder',
-        'OrderService.getOrder',
-        'OrderService.getUserOrders',
-        'OrderService.payOrder',
-        'OrderService.cancelOrder',
-      ],
-      irrelevantSymbols: [
-        'validateEmail',
-        'validateQuantity',
-        'hashPassword',
-        'PaymentService.calculateTotal',
-      ],
-      minRecall: 0.8,
-      minPrecision: 0.7,
-    },
-  ],
-
-  // Known call graph edges for validation
-  callGraph: [
-    // Auth -> Database
-    { caller: 'AuthService.register', callee: 'db.findUserByEmail' },
-    { caller: 'AuthService.register', callee: 'db.createUser' },
-    { caller: 'AuthService.login', callee: 'db.findUserByEmail' },
-
-    // Auth -> Crypto
-    { caller: 'AuthService.register', callee: 'hashPassword' },
-    { caller: 'AuthService.register', callee: 'generateToken' },
-    { caller: 'AuthService.login', callee: 'verifyPassword' },
-    { caller: 'AuthService.createToken', callee: 'generateToken' },
-
-    // Auth -> Validation
-    { caller: 'AuthService.register', callee: 'validateEmail' },
-
-    // User -> Database
-    { caller: 'UserService.getUser', callee: 'db.findUserById' },
-    { caller: 'UserService.getUserByEmail', callee: 'db.findUserByEmail' },
-    { caller: 'UserService.updateProfile', callee: 'db.findUserById' },
-    { caller: 'UserService.updateProfile', callee: 'db.findUserByEmail' },
-    { caller: 'UserService.updateProfile', callee: 'db.updateUser' },
-    { caller: 'UserService.deleteUser', callee: 'db.findUserById' },
-    { caller: 'UserService.deleteUser', callee: 'db.updateUser' },
-
-    // User -> Validation
-    { caller: 'UserService.updateProfile', callee: 'validateEmail' },
-
-    // Order -> Auth
-    { caller: 'OrderService.createOrder', callee: 'authService.validateToken' },
-    { caller: 'OrderService.getOrder', callee: 'authService.validateToken' },
-    { caller: 'OrderService.getUserOrders', callee: 'authService.validateToken' },
-
-    // Order -> Database
-    { caller: 'OrderService.createOrder', callee: 'db.findProductById' },
-    { caller: 'OrderService.createOrder', callee: 'db.createOrder' },
-    { caller: 'OrderService.createOrder', callee: 'db.updateProductStock' },
-    { caller: 'OrderService.getOrder', callee: 'db.findOrderById' },
-    { caller: 'OrderService.getUserOrders', callee: 'db.findOrdersByUserId' },
-    { caller: 'OrderService.cancelOrder', callee: 'db.updateOrderStatus' },
-
-    // Order -> Payment
-    { caller: 'OrderService.createOrder', callee: 'paymentService.calculateTotal' },
-    { caller: 'OrderService.payOrder', callee: 'paymentService.processPayment' },
-    { caller: 'OrderService.cancelOrder', callee: 'paymentService.refundPayment' },
-
-    // Order -> Validation
-    { caller: 'OrderService.createOrder', callee: 'validateQuantity' },
-
-    // Order -> Crypto
-    { caller: 'OrderService.createOrder', callee: 'generateOrderId' },
-
-    // Payment -> Database
-    { caller: 'PaymentService.processPayment', callee: 'db.findOrderById' },
-    { caller: 'PaymentService.processPayment', callee: 'db.updateOrderStatus' },
-    { caller: 'PaymentService.refundPayment', callee: 'db.findOrderById' },
-    { caller: 'PaymentService.refundPayment', callee: 'db.updateOrderStatus' },
-
-    // Payment -> Crypto
-    { caller: 'PaymentService.processPayment', callee: 'generateToken' },
-    { caller: 'PaymentService.refundPayment', callee: 'generateToken' },
-  ],
-};

+ 0 - 90
__tests__/evaluation/fixtures/typescript-project/src/auth.ts

@@ -1,90 +0,0 @@
-/**
- * Authentication service
- */
-
-import { User, AuthToken } from './types';
-import { db } from './database';
-import { hashPassword, verifyPassword, generateToken } from './utils/crypto';
-import { validateEmail } from './utils/validation';
-
-export class AuthService {
-  private tokens: Map<string, AuthToken> = new Map();
-
-  async register(email: string, password: string, name: string): Promise<User> {
-    if (!validateEmail(email)) {
-      throw new Error('Invalid email format');
-    }
-
-    const existing = await db.findUserByEmail(email);
-    if (existing) {
-      throw new Error('Email already registered');
-    }
-
-    const passwordHash = await hashPassword(password);
-    const user: User = {
-      id: generateToken(),
-      email,
-      name,
-      passwordHash,
-      createdAt: new Date(),
-    };
-
-    await db.createUser(user);
-    return user;
-  }
-
-  async login(email: string, password: string): Promise<AuthToken> {
-    const user = await db.findUserByEmail(email);
-    if (!user) {
-      throw new Error('Invalid credentials');
-    }
-
-    const valid = await verifyPassword(password, user.passwordHash);
-    if (!valid) {
-      throw new Error('Invalid credentials');
-    }
-
-    const token = this.createToken(user.id);
-    return token;
-  }
-
-  async logout(token: string): Promise<void> {
-    this.tokens.delete(token);
-  }
-
-  async validateToken(token: string): Promise<string | null> {
-    const authToken = this.tokens.get(token);
-    if (!authToken) {
-      return null;
-    }
-
-    if (authToken.expiresAt < new Date()) {
-      this.tokens.delete(token);
-      return null;
-    }
-
-    return authToken.userId;
-  }
-
-  async refreshToken(token: string): Promise<AuthToken | null> {
-    const userId = await this.validateToken(token);
-    if (!userId) {
-      return null;
-    }
-
-    this.tokens.delete(token);
-    return this.createToken(userId);
-  }
-
-  private createToken(userId: string): AuthToken {
-    const token: AuthToken = {
-      token: generateToken(),
-      userId,
-      expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
-    };
-    this.tokens.set(token.token, token);
-    return token;
-  }
-}
-
-export const authService = new AuthService();

+ 0 - 75
__tests__/evaluation/fixtures/typescript-project/src/database.ts

@@ -1,75 +0,0 @@
-/**
- * Database abstraction layer
- */
-
-import { User, Product, Order } from './types';
-
-export class Database {
-  private users: Map<string, User> = new Map();
-  private products: Map<string, Product> = new Map();
-  private orders: Map<string, Order> = new Map();
-
-  async findUserById(id: string): Promise<User | null> {
-    return this.users.get(id) || null;
-  }
-
-  async findUserByEmail(email: string): Promise<User | null> {
-    for (const user of this.users.values()) {
-      if (user.email === email) {
-        return user;
-      }
-    }
-    return null;
-  }
-
-  async createUser(user: User): Promise<void> {
-    this.users.set(user.id, user);
-  }
-
-  async updateUser(id: string, updates: Partial<User>): Promise<void> {
-    const user = this.users.get(id);
-    if (user) {
-      this.users.set(id, { ...user, ...updates });
-    }
-  }
-
-  async findProductById(id: string): Promise<Product | null> {
-    return this.products.get(id) || null;
-  }
-
-  async updateProductStock(id: string, quantity: number): Promise<void> {
-    const product = this.products.get(id);
-    if (product) {
-      product.stock -= quantity;
-      this.products.set(id, product);
-    }
-  }
-
-  async createOrder(order: Order): Promise<void> {
-    this.orders.set(order.id, order);
-  }
-
-  async findOrderById(id: string): Promise<Order | null> {
-    return this.orders.get(id) || null;
-  }
-
-  async findOrdersByUserId(userId: string): Promise<Order[]> {
-    const orders: Order[] = [];
-    for (const order of this.orders.values()) {
-      if (order.userId === userId) {
-        orders.push(order);
-      }
-    }
-    return orders;
-  }
-
-  async updateOrderStatus(id: string, status: Order['status']): Promise<void> {
-    const order = this.orders.get(id);
-    if (order) {
-      order.status = status;
-      this.orders.set(id, order);
-    }
-  }
-}
-
-export const db = new Database();

+ 0 - 11
__tests__/evaluation/fixtures/typescript-project/src/index.ts

@@ -1,11 +0,0 @@
-/**
- * E-commerce application entry point
- */
-
-export { authService, AuthService } from './auth';
-export { userService, UserService } from './user';
-export { orderService, OrderService } from './order';
-export { paymentService, PaymentService } from './payment';
-export { db, Database } from './database';
-
-export * from './types';

+ 0 - 115
__tests__/evaluation/fixtures/typescript-project/src/order.ts

@@ -1,115 +0,0 @@
-/**
- * Order management service
- */
-
-import { Order, OrderItem, Product } from './types';
-import { db } from './database';
-import { paymentService } from './payment';
-import { authService } from './auth';
-import { generateOrderId } from './utils/crypto';
-import { validateQuantity } from './utils/validation';
-
-export class OrderService {
-  async createOrder(token: string, items: OrderItem[]): Promise<Order> {
-    const userId = await authService.validateToken(token);
-    if (!userId) {
-      throw new Error('Invalid or expired token');
-    }
-
-    // Validate items
-    for (const item of items) {
-      if (!validateQuantity(item.quantity)) {
-        throw new Error(`Invalid quantity for product ${item.productId}`);
-      }
-
-      const product = await db.findProductById(item.productId);
-      if (!product) {
-        throw new Error(`Product not found: ${item.productId}`);
-      }
-
-      if (product.stock < item.quantity) {
-        throw new Error(`Insufficient stock for product ${item.productId}`);
-      }
-    }
-
-    // Calculate total
-    const total = paymentService.calculateTotal(items);
-
-    // Create order
-    const order: Order = {
-      id: generateOrderId(),
-      userId,
-      items,
-      total,
-      status: 'pending',
-      createdAt: new Date(),
-    };
-
-    await db.createOrder(order);
-
-    // Update stock
-    for (const item of items) {
-      await db.updateProductStock(item.productId, item.quantity);
-    }
-
-    return order;
-  }
-
-  async getOrder(token: string, orderId: string): Promise<Order | null> {
-    const userId = await authService.validateToken(token);
-    if (!userId) {
-      throw new Error('Invalid or expired token');
-    }
-
-    const order = await db.findOrderById(orderId);
-    if (!order || order.userId !== userId) {
-      return null;
-    }
-
-    return order;
-  }
-
-  async getUserOrders(token: string): Promise<Order[]> {
-    const userId = await authService.validateToken(token);
-    if (!userId) {
-      throw new Error('Invalid or expired token');
-    }
-
-    return db.findOrdersByUserId(userId);
-  }
-
-  async payOrder(token: string, orderId: string): Promise<boolean> {
-    const order = await this.getOrder(token, orderId);
-    if (!order) {
-      throw new Error('Order not found');
-    }
-
-    if (order.status !== 'pending') {
-      throw new Error('Order already processed');
-    }
-
-    const result = await paymentService.processPayment(orderId, order.total);
-    return result.success;
-  }
-
-  async cancelOrder(token: string, orderId: string): Promise<boolean> {
-    const order = await this.getOrder(token, orderId);
-    if (!order) {
-      throw new Error('Order not found');
-    }
-
-    if (order.status === 'shipped' || order.status === 'delivered') {
-      throw new Error('Cannot cancel shipped or delivered orders');
-    }
-
-    if (order.status === 'paid') {
-      const refund = await paymentService.refundPayment(orderId);
-      return refund.success;
-    }
-
-    await db.updateOrderStatus(orderId, 'cancelled');
-    return true;
-  }
-}
-
-export const orderService = new OrderService();

+ 0 - 60
__tests__/evaluation/fixtures/typescript-project/src/payment.ts

@@ -1,60 +0,0 @@
-/**
- * Payment processing service
- */
-
-import { PaymentResult, Order } from './types';
-import { db } from './database';
-import { generateToken } from './utils/crypto';
-
-export class PaymentService {
-  async processPayment(orderId: string, amount: number): Promise<PaymentResult> {
-    const order = await db.findOrderById(orderId);
-    if (!order) {
-      return { success: false, error: 'Order not found' };
-    }
-
-    if (order.status !== 'pending') {
-      return { success: false, error: 'Order already processed' };
-    }
-
-    if (order.total !== amount) {
-      return { success: false, error: 'Amount mismatch' };
-    }
-
-    // Simulate payment processing
-    const success = Math.random() > 0.1; // 90% success rate
-
-    if (success) {
-      await db.updateOrderStatus(orderId, 'paid');
-      return {
-        success: true,
-        transactionId: generateToken(),
-      };
-    }
-
-    return { success: false, error: 'Payment declined' };
-  }
-
-  async refundPayment(orderId: string): Promise<PaymentResult> {
-    const order = await db.findOrderById(orderId);
-    if (!order) {
-      return { success: false, error: 'Order not found' };
-    }
-
-    if (order.status !== 'paid') {
-      return { success: false, error: 'Order not eligible for refund' };
-    }
-
-    await db.updateOrderStatus(orderId, 'cancelled');
-    return {
-      success: true,
-      transactionId: generateToken(),
-    };
-  }
-
-  calculateTotal(items: { price: number; quantity: number }[]): number {
-    return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
-  }
-}
-
-export const paymentService = new PaymentService();

+ 0 - 47
__tests__/evaluation/fixtures/typescript-project/src/types.ts

@@ -1,47 +0,0 @@
-/**
- * Core types for the e-commerce application
- */
-
-export interface User {
-  id: string;
-  email: string;
-  name: string;
-  passwordHash: string;
-  createdAt: Date;
-}
-
-export interface Product {
-  id: string;
-  name: string;
-  price: number;
-  stock: number;
-}
-
-export interface OrderItem {
-  productId: string;
-  quantity: number;
-  price: number;
-}
-
-export interface Order {
-  id: string;
-  userId: string;
-  items: OrderItem[];
-  total: number;
-  status: OrderStatus;
-  createdAt: Date;
-}
-
-export type OrderStatus = 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled';
-
-export interface PaymentResult {
-  success: boolean;
-  transactionId?: string;
-  error?: string;
-}
-
-export interface AuthToken {
-  token: string;
-  userId: string;
-  expiresAt: Date;
-}

+ 0 - 50
__tests__/evaluation/fixtures/typescript-project/src/user.ts

@@ -1,50 +0,0 @@
-/**
- * User management service
- */
-
-import { User } from './types';
-import { db } from './database';
-import { validateEmail } from './utils/validation';
-
-export class UserService {
-  async getUser(id: string): Promise<User | null> {
-    return db.findUserById(id);
-  }
-
-  async getUserByEmail(email: string): Promise<User | null> {
-    return db.findUserByEmail(email);
-  }
-
-  async updateProfile(userId: string, updates: { name?: string; email?: string }): Promise<User> {
-    const user = await db.findUserById(userId);
-    if (!user) {
-      throw new Error('User not found');
-    }
-
-    if (updates.email && updates.email !== user.email) {
-      if (!validateEmail(updates.email)) {
-        throw new Error('Invalid email format');
-      }
-
-      const existing = await db.findUserByEmail(updates.email);
-      if (existing) {
-        throw new Error('Email already in use');
-      }
-    }
-
-    await db.updateUser(userId, updates);
-    return { ...user, ...updates };
-  }
-
-  async deleteUser(userId: string): Promise<void> {
-    const user = await db.findUserById(userId);
-    if (!user) {
-      throw new Error('User not found');
-    }
-
-    // In a real app, we'd also delete orders, etc.
-    await db.updateUser(userId, { email: `deleted_${userId}@deleted.com` });
-  }
-}
-
-export const userService = new UserService();

+ 0 - 21
__tests__/evaluation/fixtures/typescript-project/src/utils/crypto.ts

@@ -1,21 +0,0 @@
-/**
- * Cryptographic utilities
- */
-
-export async function hashPassword(password: string): Promise<string> {
-  // Simulated password hashing
-  return `hashed_${password}_${Date.now()}`;
-}
-
-export async function verifyPassword(password: string, hash: string): Promise<boolean> {
-  // Simulated password verification
-  return hash.startsWith(`hashed_${password}_`);
-}
-
-export function generateToken(): string {
-  return Math.random().toString(36).substring(2) + Date.now().toString(36);
-}
-
-export function generateOrderId(): string {
-  return `ORD-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
-}

+ 0 - 35
__tests__/evaluation/fixtures/typescript-project/src/utils/validation.ts

@@ -1,35 +0,0 @@
-/**
- * Validation utilities
- */
-
-export function validateEmail(email: string): boolean {
-  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
-  return emailRegex.test(email);
-}
-
-export function validatePassword(password: string): { valid: boolean; errors: string[] } {
-  const errors: string[] = [];
-
-  if (password.length < 8) {
-    errors.push('Password must be at least 8 characters');
-  }
-  if (!/[A-Z]/.test(password)) {
-    errors.push('Password must contain an uppercase letter');
-  }
-  if (!/[a-z]/.test(password)) {
-    errors.push('Password must contain a lowercase letter');
-  }
-  if (!/[0-9]/.test(password)) {
-    errors.push('Password must contain a number');
-  }
-
-  return { valid: errors.length === 0, errors };
-}
-
-export function validateQuantity(quantity: number): boolean {
-  return Number.isInteger(quantity) && quantity > 0;
-}
-
-export function validatePrice(price: number): boolean {
-  return typeof price === 'number' && price >= 0;
-}

+ 0 - 374
__tests__/evaluation/runner.ts

@@ -1,374 +0,0 @@
-/**
- * Evaluation Runner
- *
- * Runs test cases against CodeGraph fixtures and measures precision/recall.
- */
-
-import * as path from 'path';
-import * as fs from 'fs';
-import CodeGraph from '../../src/index';
-import type { Node, SearchResult, NodeKind } from '../../src/types';
-import type {
-  TestCase,
-  TestCaseResult,
-  FixtureGroundTruth,
-  FixtureEvaluationResult,
-  EvaluationSummary,
-} from './types';
-
-// Import fixtures
-import { typescriptFixture } from './fixtures/typescript-project/ground-truth';
-import { pythonFixture } from './fixtures/python-project/ground-truth';
-
-/**
- * Simple token counter (approximation using word count * 1.3)
- */
-function countTokens(text: string): number {
-  const words = text.split(/\s+/).filter(w => w.length > 0);
-  return Math.ceil(words.length * 1.3);
-}
-
-/**
- * Extract symbol names from CodeGraph results
- */
-function extractSymbolNames(nodes: Node[]): Set<string> {
-  const names = new Set<string>();
-  for (const node of nodes) {
-    // Add the simple name
-    names.add(node.name);
-
-    // Add qualified name if we have parent info (Class.method format)
-    // This is a simplification - real implementation would use containment edges
-    if (node.kind === 'method' || node.kind === 'function') {
-      // Try to infer class from file path or other context
-      const fileName = path.basename(node.filePath, path.extname(node.filePath));
-      names.add(`${fileName}.${node.name}`);
-    }
-  }
-  return names;
-}
-
-/**
- * Normalize symbol name for comparison
- */
-function normalizeSymbol(symbol: string): string {
-  // Remove common prefixes and normalize
-  return symbol
-    .replace(/^(db\.|authService\.|paymentService\.|auth_service\.|task_service\.)/, '')
-    .toLowerCase();
-}
-
-/**
- * Check if a symbol matches any in a set (with fuzzy matching)
- */
-function symbolMatches(symbol: string, candidates: Set<string>): boolean {
-  const normalized = normalizeSymbol(symbol);
-
-  for (const candidate of candidates) {
-    const normalizedCandidate = normalizeSymbol(candidate);
-
-    // Exact match
-    if (normalized === normalizedCandidate) return true;
-
-    // Partial match (e.g., "login" matches "AuthService.login")
-    if (normalizedCandidate.endsWith(`.${normalized}`)) return true;
-    if (normalized.endsWith(`.${normalizedCandidate}`)) return true;
-
-    // Simple name match
-    const simpleName = normalized.split('.').pop();
-    const simpleCandidateName = normalizedCandidate.split('.').pop();
-    if (simpleName === simpleCandidateName) return true;
-  }
-
-  return false;
-}
-
-/**
- * Run a single test case
- */
-async function runTestCase(
-  cg: CodeGraph,
-  testCase: TestCase,
-  fixtureTokens: number
-): Promise<TestCaseResult> {
-  const startTime = Date.now();
-
-  let retrievedNodes: Node[] = [];
-  let contextText = '';
-
-  try {
-    switch (testCase.type) {
-      case 'search': {
-        const results = cg.searchNodes(testCase.query, { limit: 20 });
-        retrievedNodes = results.map(r => r.node);
-        break;
-      }
-
-      case 'context': {
-        const context = await cg.buildContext(testCase.query, {
-          maxNodes: 30,
-          includeCode: true,
-          format: 'markdown',
-        });
-        contextText = typeof context === 'string' ? context : '';
-
-        // Also get the nodes that were used to build context
-        const results = cg.searchNodes(testCase.query, { limit: 30 });
-        retrievedNodes = results.map(r => r.node);
-        break;
-      }
-
-      case 'callers': {
-        if (testCase.targetSymbol) {
-          const results = cg.searchNodes(testCase.targetSymbol, { limit: 1 });
-          if (results.length > 0 && results[0]) {
-            const callers = cg.getCallers(results[0].node.id);
-            retrievedNodes = callers.map(c => c.node);
-          }
-        }
-        break;
-      }
-
-      case 'callees': {
-        if (testCase.targetSymbol) {
-          const results = cg.searchNodes(testCase.targetSymbol, { limit: 1 });
-          if (results.length > 0 && results[0]) {
-            const callees = cg.getCallees(results[0].node.id);
-            retrievedNodes = callees.map(c => c.node);
-          }
-        }
-        break;
-      }
-
-      case 'impact': {
-        if (testCase.targetSymbol) {
-          const results = cg.searchNodes(testCase.targetSymbol, { limit: 1 });
-          if (results.length > 0 && results[0]) {
-            const impact = cg.getImpactRadius(results[0].node.id, 2);
-            retrievedNodes = Array.from(impact.nodes.values());
-          }
-        }
-        break;
-      }
-    }
-  } catch (err) {
-    console.error(`Error running test case ${testCase.id}:`, err);
-  }
-
-  const executionTimeMs = Date.now() - startTime;
-
-  // Extract retrieved symbol names
-  const retrievedSymbols = extractSymbolNames(retrievedNodes);
-
-  // Calculate metrics
-  const expectedSet = new Set(testCase.expectedSymbols.map(s => normalizeSymbol(s)));
-  const irrelevantSet = new Set(testCase.irrelevantSymbols.map(s => normalizeSymbol(s)));
-
-  const truePositives: string[] = [];
-  const falsePositives: string[] = [];
-
-  for (const symbol of retrievedSymbols) {
-    const normalized = normalizeSymbol(symbol);
-
-    if (symbolMatches(symbol, new Set(testCase.expectedSymbols))) {
-      truePositives.push(symbol);
-    } else if (symbolMatches(symbol, new Set(testCase.irrelevantSymbols))) {
-      falsePositives.push(symbol);
-    }
-    // Symbols not in either list are ignored (neutral)
-  }
-
-  // Find false negatives (expected but not retrieved)
-  const falseNegatives: string[] = [];
-  for (const expected of testCase.expectedSymbols) {
-    if (!symbolMatches(expected, retrievedSymbols)) {
-      falseNegatives.push(expected);
-    }
-  }
-
-  // Calculate precision and recall
-  const totalRetrieved = truePositives.length + falsePositives.length;
-  const precision = totalRetrieved > 0 ? truePositives.length / totalRetrieved : 0;
-
-  const totalRelevant = testCase.expectedSymbols.length;
-  const recall = totalRelevant > 0 ? truePositives.length / totalRelevant : 0;
-
-  const f1Score = precision + recall > 0
-    ? 2 * (precision * recall) / (precision + recall)
-    : 0;
-
-  // Count context tokens
-  const contextTokens = contextText
-    ? countTokens(contextText)
-    : retrievedNodes.reduce((sum, node) => {
-        // Estimate tokens from node info
-        return sum + countTokens(node.name + ' ' + (node.signature || ''));
-      }, 0);
-
-  // Determine if test passed
-  const meetsRecall = !testCase.minRecall || recall >= testCase.minRecall;
-  const meetsPrecision = !testCase.minPrecision || precision >= testCase.minPrecision;
-  const passed = meetsRecall && meetsPrecision;
-
-  return {
-    testCaseId: testCase.id,
-    passed,
-    precision,
-    recall,
-    f1Score,
-    truePositives,
-    falsePositives,
-    falseNegatives,
-    contextTokens,
-    executionTimeMs,
-  };
-}
-
-/**
- * Run evaluation on a single fixture
- */
-async function evaluateFixture(
-  fixture: FixtureGroundTruth
-): Promise<FixtureEvaluationResult> {
-  const fixturePath = path.resolve(process.cwd(), fixture.path);
-  const startTime = Date.now();
-
-  console.log(`\nEvaluating fixture: ${fixture.name}`);
-  console.log(`  Path: ${fixturePath}`);
-
-  // Initialize CodeGraph for this fixture
-  let cg: CodeGraph;
-
-  if (CodeGraph.isInitialized(fixturePath)) {
-    console.log('  Opening existing index...');
-    cg = await CodeGraph.open(fixturePath);
-  } else {
-    console.log('  Initializing and indexing...');
-    cg = await CodeGraph.init(fixturePath, { index: true });
-  }
-
-  const stats = cg.getStats();
-  console.log(`  Indexed ${stats.fileCount} files, ${stats.nodeCount} nodes`);
-
-  // Run all test cases
-  const testCaseResults: TestCaseResult[] = [];
-
-  for (const testCase of fixture.testCases) {
-    console.log(`  Running: ${testCase.id}...`);
-    const result = await runTestCase(cg, testCase, fixture.approximateTokens);
-    testCaseResults.push(result);
-
-    const status = result.passed ? '✓' : '✗';
-    console.log(`    ${status} P=${(result.precision * 100).toFixed(0)}% R=${(result.recall * 100).toFixed(0)}% F1=${(result.f1Score * 100).toFixed(0)}%`);
-  }
-
-  // Close CodeGraph
-  cg.destroy();
-
-  // Calculate aggregate metrics
-  const totalTimeMs = Date.now() - startTime;
-  const passedTestCases = testCaseResults.filter(r => r.passed).length;
-
-  const averagePrecision = testCaseResults.reduce((sum, r) => sum + r.precision, 0) / testCaseResults.length;
-  const averageRecall = testCaseResults.reduce((sum, r) => sum + r.recall, 0) / testCaseResults.length;
-  const averageF1Score = testCaseResults.reduce((sum, r) => sum + r.f1Score, 0) / testCaseResults.length;
-  const averageContextTokens = testCaseResults.reduce((sum, r) => sum + r.contextTokens, 0) / testCaseResults.length;
-
-  const tokenReductionPercent = fixture.approximateTokens > 0
-    ? ((fixture.approximateTokens - averageContextTokens) / fixture.approximateTokens) * 100
-    : 0;
-
-  return {
-    fixtureName: fixture.name,
-    totalTestCases: testCaseResults.length,
-    passedTestCases,
-    averagePrecision,
-    averageRecall,
-    averageF1Score,
-    fullCodebaseTokens: fixture.approximateTokens,
-    averageContextTokens,
-    tokenReductionPercent,
-    testCaseResults,
-    totalTimeMs,
-  };
-}
-
-/**
- * Run full evaluation across all fixtures
- */
-export async function runEvaluation(): Promise<EvaluationSummary> {
-  console.log('╔════════════════════════════════════════════════════════════════╗');
-  console.log('║              CodeGraph Evaluation Suite                        ║');
-  console.log('╚════════════════════════════════════════════════════════════════╝');
-
-  const fixtures: FixtureGroundTruth[] = [
-    typescriptFixture,
-    pythonFixture,
-  ];
-
-  const fixtureResults: FixtureEvaluationResult[] = [];
-
-  for (const fixture of fixtures) {
-    const result = await evaluateFixture(fixture);
-    fixtureResults.push(result);
-  }
-
-  // Calculate overall metrics
-  const totalTests = fixtureResults.reduce((sum, r) => sum + r.totalTestCases, 0);
-  const totalPassed = fixtureResults.reduce((sum, r) => sum + r.passedTestCases, 0);
-
-  const overallPrecision = fixtureResults.reduce((sum, r) => sum + r.averagePrecision, 0) / fixtureResults.length;
-  const overallRecall = fixtureResults.reduce((sum, r) => sum + r.averageRecall, 0) / fixtureResults.length;
-  const overallF1Score = fixtureResults.reduce((sum, r) => sum + r.averageF1Score, 0) / fixtureResults.length;
-  const overallTokenReduction = fixtureResults.reduce((sum, r) => sum + r.tokenReductionPercent, 0) / fixtureResults.length;
-
-  // Print summary
-  console.log('\n╔════════════════════════════════════════════════════════════════╗');
-  console.log('║                      EVALUATION SUMMARY                         ║');
-  console.log('╚════════════════════════════════════════════════════════════════╝');
-
-  console.log(`\nTest Results: ${totalPassed}/${totalTests} passed`);
-  console.log(`\nOverall Metrics:`);
-  console.log(`  Precision:        ${(overallPrecision * 100).toFixed(1)}%`);
-  console.log(`  Recall:           ${(overallRecall * 100).toFixed(1)}%`);
-  console.log(`  F1 Score:         ${(overallF1Score * 100).toFixed(1)}%`);
-  console.log(`  Token Reduction:  ${overallTokenReduction.toFixed(1)}%`);
-
-  console.log('\nPer-Fixture Results:');
-  for (const result of fixtureResults) {
-    console.log(`  ${result.fixtureName}:`);
-    console.log(`    Tests: ${result.passedTestCases}/${result.totalTestCases} passed`);
-    console.log(`    P=${(result.averagePrecision * 100).toFixed(0)}% R=${(result.averageRecall * 100).toFixed(0)}% F1=${(result.averageF1Score * 100).toFixed(0)}%`);
-  }
-
-  const summary: EvaluationSummary = {
-    timestamp: new Date(),
-    version: '0.1.0',
-    fixtureResults,
-    overallPrecision,
-    overallRecall,
-    overallF1Score,
-    overallTokenReduction,
-  };
-
-  // Save results to file
-  const resultsPath = path.join(__dirname, 'results', `eval-${Date.now()}.json`);
-  const resultsDir = path.dirname(resultsPath);
-  if (!fs.existsSync(resultsDir)) {
-    fs.mkdirSync(resultsDir, { recursive: true });
-  }
-  fs.writeFileSync(resultsPath, JSON.stringify(summary, null, 2));
-  console.log(`\nResults saved to: ${resultsPath}`);
-
-  return summary;
-}
-
-// Run if called directly
-if (require.main === module) {
-  runEvaluation()
-    .then(() => process.exit(0))
-    .catch(err => {
-      console.error('Evaluation failed:', err);
-      process.exit(1);
-    });
-}

+ 0 - 163
__tests__/evaluation/types.ts

@@ -1,163 +0,0 @@
-/**
- * Evaluation Framework Types
- */
-
-/**
- * A test case with expected ground truth
- */
-export interface TestCase {
-  /** Unique identifier for this test case */
-  id: string;
-
-  /** Human-readable description */
-  description: string;
-
-  /** The query/task to test */
-  query: string;
-
-  /** Type of operation being tested */
-  type: 'search' | 'callers' | 'callees' | 'impact' | 'context';
-
-  /** For callers/callees/impact: the symbol to analyze */
-  targetSymbol?: string;
-
-  /** Symbols that MUST be in the results (for recall) */
-  expectedSymbols: string[];
-
-  /** Symbols that should NOT be in the results (for precision) */
-  irrelevantSymbols: string[];
-
-  /** Minimum acceptable recall (0-1) */
-  minRecall?: number;
-
-  /** Minimum acceptable precision (0-1) */
-  minPrecision?: number;
-}
-
-/**
- * Ground truth for a test fixture
- */
-export interface FixtureGroundTruth {
-  /** Fixture name */
-  name: string;
-
-  /** Path to the fixture directory */
-  path: string;
-
-  /** Language of the fixture */
-  language: string;
-
-  /** Total files in the fixture */
-  totalFiles: number;
-
-  /** Approximate total tokens in the fixture */
-  approximateTokens: number;
-
-  /** Test cases for this fixture */
-  testCases: TestCase[];
-
-  /** Known call graph edges for validation */
-  callGraph: {
-    caller: string;  // qualified name
-    callee: string;  // qualified name
-  }[];
-}
-
-/**
- * Results from evaluating a single test case
- */
-export interface TestCaseResult {
-  /** Test case ID */
-  testCaseId: string;
-
-  /** Whether the test passed */
-  passed: boolean;
-
-  /** Precision: relevant retrieved / total retrieved */
-  precision: number;
-
-  /** Recall: relevant retrieved / total relevant */
-  recall: number;
-
-  /** F1 score: 2 * (precision * recall) / (precision + recall) */
-  f1Score: number;
-
-  /** Symbols that were correctly retrieved */
-  truePositives: string[];
-
-  /** Irrelevant symbols that were incorrectly retrieved */
-  falsePositives: string[];
-
-  /** Expected symbols that were missed */
-  falseNegatives: string[];
-
-  /** Tokens in the retrieved context */
-  contextTokens: number;
-
-  /** Execution time in ms */
-  executionTimeMs: number;
-}
-
-/**
- * Results from evaluating a fixture
- */
-export interface FixtureEvaluationResult {
-  /** Fixture name */
-  fixtureName: string;
-
-  /** Total test cases */
-  totalTestCases: number;
-
-  /** Passed test cases */
-  passedTestCases: number;
-
-  /** Average precision across all tests */
-  averagePrecision: number;
-
-  /** Average recall across all tests */
-  averageRecall: number;
-
-  /** Average F1 score */
-  averageF1Score: number;
-
-  /** Total tokens in the full codebase */
-  fullCodebaseTokens: number;
-
-  /** Average tokens in retrieved context */
-  averageContextTokens: number;
-
-  /** Token reduction percentage */
-  tokenReductionPercent: number;
-
-  /** Individual test case results */
-  testCaseResults: TestCaseResult[];
-
-  /** Total evaluation time in ms */
-  totalTimeMs: number;
-}
-
-/**
- * Overall evaluation summary
- */
-export interface EvaluationSummary {
-  /** Timestamp of the evaluation */
-  timestamp: Date;
-
-  /** CodeGraph version */
-  version: string;
-
-  /** Results per fixture */
-  fixtureResults: FixtureEvaluationResult[];
-
-  /** Overall average precision */
-  overallPrecision: number;
-
-  /** Overall average recall */
-  overallRecall: number;
-
-  /** Overall average F1 */
-  overallF1Score: number;
-
-  /** Overall token reduction */
-  overallTokenReduction: number;
-}

+ 949 - 0
__tests__/extraction.test.ts

@@ -83,6 +83,10 @@ describe('Language Detection', () => {
     expect(detectLanguage('build.gradle.kts')).toBe('kotlin');
   });
 
+  it('should detect Dart files', () => {
+    expect(detectLanguage('main.dart')).toBe('dart');
+  });
+
   it('should return unknown for unsupported extensions', () => {
     expect(detectLanguage('styles.css')).toBe('unknown');
     expect(detectLanguage('data.json')).toBe('unknown');
@@ -110,6 +114,7 @@ describe('Language Support', () => {
     expect(languages).toContain('ruby');
     expect(languages).toContain('swift');
     expect(languages).toContain('kotlin');
+    expect(languages).toContain('dart');
   });
 });
 
@@ -532,6 +537,950 @@ suspend fun loadData(): List<String> {
   });
 });
 
+describe('Dart Extraction', () => {
+  it('should extract class declarations', () => {
+    const code = `
+class UserService {
+  final Database _db;
+
+  Future<User> findById(String id) async {
+    return await _db.query(id);
+  }
+
+  void _privateMethod() {}
+}
+`;
+    const result = extractFromSource('service.dart', code);
+
+    const classNode = result.nodes.find((n) => n.kind === 'class');
+    expect(classNode).toBeDefined();
+    expect(classNode?.name).toBe('UserService');
+    expect(classNode?.visibility).toBe('public');
+
+    const methodNodes = result.nodes.filter((n) => n.kind === 'method');
+    expect(methodNodes.length).toBeGreaterThanOrEqual(2);
+
+    const findById = methodNodes.find((m) => m.name === 'findById');
+    expect(findById).toBeDefined();
+    expect(findById?.isAsync).toBe(true);
+
+    const privateMethod = methodNodes.find((m) => m.name === '_privateMethod');
+    expect(privateMethod).toBeDefined();
+    expect(privateMethod?.visibility).toBe('private');
+  });
+
+  it('should extract top-level function declarations', () => {
+    const code = `
+void topLevelFunction(String name) {
+  print(name);
+}
+`;
+    const result = extractFromSource('utils.dart', code);
+
+    const funcNode = result.nodes.find((n) => n.kind === 'function');
+    expect(funcNode).toBeDefined();
+    expect(funcNode?.name).toBe('topLevelFunction');
+    expect(funcNode?.language).toBe('dart');
+  });
+
+  it('should extract enum declarations', () => {
+    const code = `
+enum Status { active, inactive, pending }
+`;
+    const result = extractFromSource('models.dart', code);
+
+    const enumNode = result.nodes.find((n) => n.kind === 'enum');
+    expect(enumNode).toBeDefined();
+    expect(enumNode?.name).toBe('Status');
+  });
+
+  it('should extract mixin declarations', () => {
+    const code = `
+mixin LoggerMixin {
+  void log(String message) {}
+}
+`;
+    const result = extractFromSource('mixins.dart', code);
+
+    const classNode = result.nodes.find((n) => n.kind === 'class');
+    expect(classNode).toBeDefined();
+    expect(classNode?.name).toBe('LoggerMixin');
+
+    const methodNode = result.nodes.find((n) => n.kind === 'method');
+    expect(methodNode).toBeDefined();
+    expect(methodNode?.name).toBe('log');
+  });
+
+  it('should extract extension declarations', () => {
+    const code = `
+extension StringExt on String {
+  bool get isBlank => trim().isEmpty;
+}
+`;
+    const result = extractFromSource('extensions.dart', code);
+
+    const classNode = result.nodes.find((n) => n.kind === 'class');
+    expect(classNode).toBeDefined();
+    expect(classNode?.name).toBe('StringExt');
+  });
+
+  it('should detect static methods', () => {
+    const code = `
+class Utils {
+  static void doWork() {}
+}
+`;
+    const result = extractFromSource('utils.dart', code);
+
+    const methodNode = result.nodes.find((n) => n.kind === 'method');
+    expect(methodNode).toBeDefined();
+    expect(methodNode?.name).toBe('doWork');
+    expect(methodNode?.isStatic).toBe(true);
+  });
+
+  it('should detect async functions', () => {
+    const code = `
+Future<String> fetchData() async {
+  return await http.get('/data');
+}
+`;
+    const result = extractFromSource('api.dart', code);
+
+    const funcNode = result.nodes.find((n) => n.kind === 'function');
+    expect(funcNode).toBeDefined();
+    expect(funcNode?.name).toBe('fetchData');
+    expect(funcNode?.isAsync).toBe(true);
+  });
+
+  it('should detect private visibility via underscore convention', () => {
+    const code = `
+void _privateHelper() {}
+
+void publicFunction() {}
+`;
+    const result = extractFromSource('helpers.dart', code);
+
+    const functions = result.nodes.filter((n) => n.kind === 'function');
+    const privateFunc = functions.find((f) => f.name === '_privateHelper');
+    const publicFunc = functions.find((f) => f.name === 'publicFunction');
+
+    expect(privateFunc?.visibility).toBe('private');
+    expect(publicFunc?.visibility).toBe('public');
+  });
+});
+
+describe('Import Extraction', () => {
+  describe('TypeScript/JavaScript imports', () => {
+    it('should extract default imports', () => {
+      const code = `import React from 'react';`;
+      const result = extractFromSource('app.tsx', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('react');
+      expect(importNode?.signature).toBe("import React from 'react';");
+    });
+
+    it('should extract named imports', () => {
+      const code = `import { Bug, Database } from '@phosphor-icons/react';`;
+      const result = extractFromSource('icons.tsx', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('@phosphor-icons/react');
+      expect(importNode?.signature).toContain('Bug');
+      expect(importNode?.signature).toContain('Database');
+    });
+
+    it('should extract namespace imports', () => {
+      const code = `import * as Icons from '@phosphor-icons/react';`;
+      const result = extractFromSource('icons.tsx', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('@phosphor-icons/react');
+      expect(importNode?.signature).toContain('* as Icons');
+    });
+
+    it('should extract side-effect imports', () => {
+      const code = `import './styles.css';`;
+      const result = extractFromSource('app.tsx', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('./styles.css');
+    });
+
+    it('should extract mixed imports (default + named)', () => {
+      const code = `import React, { useState, useEffect } from 'react';`;
+      const result = extractFromSource('app.tsx', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('react');
+      expect(importNode?.signature).toContain('React');
+      expect(importNode?.signature).toContain('useState');
+      expect(importNode?.signature).toContain('useEffect');
+    });
+
+    it('should extract multiple import statements', () => {
+      const code = `
+import React from 'react';
+import { Button } from './components';
+import './styles.css';
+`;
+      const result = extractFromSource('app.tsx', code);
+
+      const importNodes = result.nodes.filter((n) => n.kind === 'import');
+      expect(importNodes.length).toBe(3);
+
+      const names = importNodes.map((n) => n.name);
+      expect(names).toContain('react');
+      expect(names).toContain('./components');
+      expect(names).toContain('./styles.css');
+    });
+
+    it('should extract type imports', () => {
+      const code = `import type { FC, ReactNode } from 'react';`;
+      const result = extractFromSource('types.ts', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('react');
+      expect(importNode?.signature).toContain('type');
+      expect(importNode?.signature).toContain('FC');
+    });
+
+    it('should extract aliased named imports', () => {
+      const code = `import { useState as useStateAlias } from 'react';`;
+      const result = extractFromSource('hooks.ts', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('react');
+      expect(importNode?.signature).toContain('useState');
+      expect(importNode?.signature).toContain('useStateAlias');
+    });
+
+    it('should extract relative path imports', () => {
+      const code = `import { helper } from '../utils/helper';`;
+      const result = extractFromSource('components/Button.tsx', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('../utils/helper');
+      expect(importNode?.signature).toContain('helper');
+    });
+  });
+
+  describe('Python imports', () => {
+    it('should extract simple import statement', () => {
+      const code = `import json`;
+      const result = extractFromSource('utils.py', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('json');
+    });
+
+    it('should extract from import statement', () => {
+      const code = `from os import path`;
+      const result = extractFromSource('utils.py', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('os');
+      expect(importNode?.signature).toContain('path');
+    });
+
+    it('should extract multiple imports from same module', () => {
+      const code = `from typing import List, Dict, Optional`;
+      const result = extractFromSource('types.py', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('typing');
+      expect(importNode?.signature).toContain('List');
+      expect(importNode?.signature).toContain('Dict');
+    });
+
+    it('should extract multiple import statements', () => {
+      const code = `
+import os
+import sys
+`;
+      const result = extractFromSource('main.py', code);
+
+      const importNodes = result.nodes.filter((n) => n.kind === 'import');
+      expect(importNodes.length).toBe(2);
+
+      const names = importNodes.map((n) => n.name);
+      expect(names).toContain('os');
+      expect(names).toContain('sys');
+    });
+
+    it('should extract aliased import', () => {
+      const code = `import numpy as np`;
+      const result = extractFromSource('data.py', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('numpy');
+      expect(importNode?.signature).toContain('as np');
+    });
+
+    it('should extract relative import', () => {
+      const code = `from .utils import helper`;
+      const result = extractFromSource('module.py', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('.utils');
+      expect(importNode?.signature).toContain('helper');
+    });
+
+    it('should extract wildcard import', () => {
+      const code = `from typing import *`;
+      const result = extractFromSource('types.py', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('typing');
+      expect(importNode?.signature).toContain('*');
+    });
+  });
+
+  describe('Rust imports', () => {
+    it('should extract simple use declaration', () => {
+      const code = `use std::io;`;
+      const result = extractFromSource('main.rs', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('std');
+      expect(importNode?.signature).toBe('use std::io;');
+    });
+
+    it('should extract scoped use list', () => {
+      const code = `use std::{ffi::OsStr, io, path::Path};`;
+      const result = extractFromSource('main.rs', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('std');
+      expect(importNode?.signature).toContain('ffi::OsStr');
+      expect(importNode?.signature).toContain('path::Path');
+    });
+
+    it('should extract crate imports', () => {
+      const code = `use crate::error::Error;`;
+      const result = extractFromSource('lib.rs', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('crate');
+    });
+
+    it('should extract super imports', () => {
+      const code = `use super::utils;`;
+      const result = extractFromSource('submod.rs', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('super');
+    });
+
+    it('should extract external crate imports', () => {
+      const code = `use serde::{Serialize, Deserialize};`;
+      const result = extractFromSource('types.rs', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('serde');
+      expect(importNode?.signature).toContain('Serialize');
+      expect(importNode?.signature).toContain('Deserialize');
+    });
+  });
+
+  describe('Go imports', () => {
+    it('should extract single import', () => {
+      const code = `
+package main
+
+import "fmt"
+`;
+      const result = extractFromSource('main.go', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('fmt');
+    });
+
+    it('should extract grouped imports', () => {
+      const code = `
+package main
+
+import (
+	"fmt"
+	"os"
+	"encoding/json"
+)
+`;
+      const result = extractFromSource('main.go', code);
+
+      const importNodes = result.nodes.filter((n) => n.kind === 'import');
+      expect(importNodes.length).toBe(3);
+
+      const names = importNodes.map((n) => n.name);
+      expect(names).toContain('fmt');
+      expect(names).toContain('os');
+      expect(names).toContain('encoding/json');
+    });
+
+    it('should extract aliased import', () => {
+      const code = `
+package main
+
+import f "fmt"
+`;
+      const result = extractFromSource('main.go', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('fmt');
+      expect(importNode?.signature).toContain('f');
+    });
+
+    it('should extract dot import', () => {
+      const code = `
+package main
+
+import . "math"
+`;
+      const result = extractFromSource('main.go', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('math');
+      expect(importNode?.signature).toContain('.');
+    });
+
+    it('should extract blank import', () => {
+      const code = `
+package main
+
+import _ "github.com/go-sql-driver/mysql"
+`;
+      const result = extractFromSource('main.go', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('github.com/go-sql-driver/mysql');
+      expect(importNode?.signature).toContain('_');
+    });
+  });
+
+  describe('Swift imports', () => {
+    it('should extract simple import', () => {
+      const code = `import Foundation`;
+      const result = extractFromSource('main.swift', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('Foundation');
+      expect(importNode?.signature).toBe('import Foundation');
+    });
+
+    it('should extract @testable import', () => {
+      const code = `@testable import Alamofire`;
+      const result = extractFromSource('Tests.swift', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('Alamofire');
+      expect(importNode?.signature).toContain('@testable');
+    });
+
+    it('should extract @preconcurrency import', () => {
+      const code = `@preconcurrency import Security`;
+      const result = extractFromSource('Auth.swift', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('Security');
+    });
+
+    it('should extract multiple imports', () => {
+      const code = `
+import Foundation
+import UIKit
+import Alamofire
+`;
+      const result = extractFromSource('App.swift', code);
+
+      const importNodes = result.nodes.filter((n) => n.kind === 'import');
+      expect(importNodes.length).toBe(3);
+
+      const names = importNodes.map((n) => n.name);
+      expect(names).toContain('Foundation');
+      expect(names).toContain('UIKit');
+      expect(names).toContain('Alamofire');
+    });
+  });
+
+  describe('Kotlin imports', () => {
+    it('should extract simple import', () => {
+      const code = `import java.io.IOException`;
+      const result = extractFromSource('Main.kt', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('java.io.IOException');
+      expect(importNode?.signature).toBe('import java.io.IOException');
+    });
+
+    it('should extract aliased import', () => {
+      const code = `import okhttp3.Request.Builder as RequestBuilder`;
+      const result = extractFromSource('Utils.kt', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('okhttp3.Request.Builder');
+      expect(importNode?.signature).toContain('as RequestBuilder');
+    });
+
+    it('should extract wildcard import', () => {
+      const code = `import java.util.concurrent.TimeUnit.*`;
+      const result = extractFromSource('Time.kt', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('java.util.concurrent.TimeUnit');
+      expect(importNode?.signature).toContain('.*');
+    });
+
+    it('should extract multiple imports', () => {
+      const code = `
+import java.io.IOException
+import kotlin.test.assertFailsWith
+import okhttp3.OkHttpClient
+`;
+      const result = extractFromSource('Test.kt', code);
+
+      const importNodes = result.nodes.filter((n) => n.kind === 'import');
+      expect(importNodes.length).toBe(3);
+
+      const names = importNodes.map((n) => n.name);
+      expect(names).toContain('java.io.IOException');
+      expect(names).toContain('kotlin.test.assertFailsWith');
+      expect(names).toContain('okhttp3.OkHttpClient');
+    });
+  });
+
+  describe('Java imports', () => {
+    it('should extract simple import', () => {
+      const code = `import java.util.List;`;
+      const result = extractFromSource('Main.java', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('java.util.List');
+      expect(importNode?.signature).toBe('import java.util.List;');
+    });
+
+    it('should extract static import', () => {
+      const code = `import static java.util.Collections.emptyList;`;
+      const result = extractFromSource('Utils.java', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('java.util.Collections.emptyList');
+      expect(importNode?.signature).toContain('static');
+    });
+
+    it('should extract wildcard import', () => {
+      const code = `import java.util.*;`;
+      const result = extractFromSource('App.java', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('java.util');
+      expect(importNode?.signature).toContain('.*');
+    });
+
+    it('should extract nested class import', () => {
+      const code = `import java.util.Map.Entry;`;
+      const result = extractFromSource('MapUtil.java', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('java.util.Map.Entry');
+    });
+
+    it('should extract multiple imports', () => {
+      const code = `
+import java.util.List;
+import java.util.Map;
+import java.io.IOException;
+`;
+      const result = extractFromSource('Service.java', code);
+
+      const importNodes = result.nodes.filter((n) => n.kind === 'import');
+      expect(importNodes.length).toBe(3);
+
+      const names = importNodes.map((n) => n.name);
+      expect(names).toContain('java.util.List');
+      expect(names).toContain('java.util.Map');
+      expect(names).toContain('java.io.IOException');
+    });
+  });
+
+  describe('C# imports', () => {
+    it('should extract simple using', () => {
+      const code = `using System;`;
+      const result = extractFromSource('Program.cs', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('System');
+      expect(importNode?.signature).toBe('using System;');
+    });
+
+    it('should extract qualified using', () => {
+      const code = `using System.Collections.Generic;`;
+      const result = extractFromSource('Utils.cs', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('System.Collections.Generic');
+    });
+
+    it('should extract static using', () => {
+      const code = `using static System.Console;`;
+      const result = extractFromSource('App.cs', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('System.Console');
+      expect(importNode?.signature).toContain('static');
+    });
+
+    it('should extract alias using', () => {
+      const code = `using MyList = System.Collections.Generic.List<int>;`;
+      const result = extractFromSource('Types.cs', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('System.Collections.Generic.List<int>');
+      expect(importNode?.signature).toContain('MyList =');
+    });
+
+    it('should extract multiple usings', () => {
+      const code = `
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+`;
+      const result = extractFromSource('Service.cs', code);
+
+      const importNodes = result.nodes.filter((n) => n.kind === 'import');
+      expect(importNodes.length).toBe(3);
+
+      const names = importNodes.map((n) => n.name);
+      expect(names).toContain('System');
+      expect(names).toContain('System.Threading.Tasks');
+      expect(names).toContain('Microsoft.Extensions.DependencyInjection');
+    });
+  });
+
+  describe('PHP imports', () => {
+    it('should extract simple use', () => {
+      const code = `<?php use PHPUnit\\Framework\\TestCase;`;
+      const result = extractFromSource('Test.php', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('PHPUnit\\Framework\\TestCase');
+    });
+
+    it('should extract aliased use', () => {
+      const code = `<?php use Mockery as m;`;
+      const result = extractFromSource('Test.php', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('Mockery');
+      expect(importNode?.signature).toContain('as m');
+    });
+
+    it('should extract function use', () => {
+      const code = `<?php use function Illuminate\\Support\\env;`;
+      const result = extractFromSource('helpers.php', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('Illuminate\\Support\\env');
+      expect(importNode?.signature).toContain('function');
+    });
+
+    it('should extract grouped use', () => {
+      const code = `<?php use Illuminate\\Database\\{Model, Builder};`;
+      const result = extractFromSource('Models.php', code);
+
+      const importNodes = result.nodes.filter((n) => n.kind === 'import');
+      expect(importNodes.length).toBe(2);
+
+      const names = importNodes.map((n) => n.name);
+      expect(names).toContain('Illuminate\\Database\\Model');
+      expect(names).toContain('Illuminate\\Database\\Builder');
+    });
+
+    it('should extract multiple uses', () => {
+      const code = `<?php
+use Illuminate\\Support\\Collection;
+use Illuminate\\Support\\Str;
+use Closure;
+`;
+      const result = extractFromSource('Service.php', code);
+
+      const importNodes = result.nodes.filter((n) => n.kind === 'import');
+      expect(importNodes.length).toBe(3);
+
+      const names = importNodes.map((n) => n.name);
+      expect(names).toContain('Illuminate\\Support\\Collection');
+      expect(names).toContain('Illuminate\\Support\\Str');
+      expect(names).toContain('Closure');
+    });
+  });
+
+  describe('Ruby imports', () => {
+    it('should extract require', () => {
+      const code = `require 'json'`;
+      const result = extractFromSource('app.rb', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('json');
+      expect(importNode?.signature).toBe("require 'json'");
+    });
+
+    it('should extract require with path', () => {
+      const code = `require 'active_support/core_ext/string'`;
+      const result = extractFromSource('config.rb', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('active_support/core_ext/string');
+    });
+
+    it('should extract require_relative', () => {
+      const code = `require_relative '../test_helper'`;
+      const result = extractFromSource('test/my_test.rb', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('../test_helper');
+      expect(importNode?.signature).toContain('require_relative');
+    });
+
+    it('should not extract non-require calls', () => {
+      const code = `puts 'hello'`;
+      const result = extractFromSource('app.rb', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeUndefined();
+    });
+
+    it('should extract multiple requires', () => {
+      const code = `
+require 'json'
+require 'yaml'
+require_relative 'helper'
+`;
+      const result = extractFromSource('lib.rb', code);
+
+      const importNodes = result.nodes.filter((n) => n.kind === 'import');
+      expect(importNodes.length).toBe(3);
+
+      const names = importNodes.map((n) => n.name);
+      expect(names).toContain('json');
+      expect(names).toContain('yaml');
+      expect(names).toContain('helper');
+    });
+  });
+
+  describe('C/C++ imports', () => {
+    it('should extract system include', () => {
+      const code = `#include <iostream>`;
+      const result = extractFromSource('main.cpp', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('iostream');
+      expect(importNode?.signature).toBe('#include <iostream>');
+    });
+
+    it('should extract system include with path', () => {
+      const code = `#include <nlohmann/json.hpp>`;
+      const result = extractFromSource('app.cpp', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('nlohmann/json.hpp');
+    });
+
+    it('should extract local include', () => {
+      const code = `#include "myheader.h"`;
+      const result = extractFromSource('main.cpp', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('myheader.h');
+    });
+
+    it('should extract C header', () => {
+      const code = `#include <stdio.h>`;
+      const result = extractFromSource('main.c', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('stdio.h');
+    });
+
+    it('should extract multiple includes', () => {
+      const code = `
+#include <iostream>
+#include <vector>
+#include "config.h"
+`;
+      const result = extractFromSource('app.cpp', code);
+
+      const importNodes = result.nodes.filter((n) => n.kind === 'import');
+      expect(importNodes.length).toBe(3);
+
+      const names = importNodes.map((n) => n.name);
+      expect(names).toContain('iostream');
+      expect(names).toContain('vector');
+      expect(names).toContain('config.h');
+    });
+  });
+
+  describe('Dart imports', () => {
+    it('should extract dart: import', () => {
+      const code = `import 'dart:async';`;
+      const result = extractFromSource('main.dart', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('dart:async');
+      expect(importNode?.signature).toBe("import 'dart:async';");
+    });
+
+    it('should extract package import', () => {
+      const code = `import 'package:flutter/material.dart';`;
+      const result = extractFromSource('app.dart', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('package:flutter/material.dart');
+    });
+
+    it('should extract aliased import', () => {
+      const code = `import 'package:http/http.dart' as http;`;
+      const result = extractFromSource('api.dart', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('package:http/http.dart');
+      expect(importNode?.signature).toContain('as http');
+    });
+
+    it('should extract multiple imports', () => {
+      const code = `
+import 'dart:async';
+import 'dart:convert';
+import 'package:flutter/material.dart';
+`;
+      const result = extractFromSource('main.dart', code);
+
+      const importNodes = result.nodes.filter((n) => n.kind === 'import');
+      expect(importNodes.length).toBe(3);
+
+      const names = importNodes.map((n) => n.name);
+      expect(names).toContain('dart:async');
+      expect(names).toContain('dart:convert');
+      expect(names).toContain('package:flutter/material.dart');
+    });
+
+    it('should extract relative import', () => {
+      const code = `import '../utils/helpers.dart';`;
+      const result = extractFromSource('lib/main.dart', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('../utils/helpers.dart');
+    });
+  });
+
+  describe('Liquid imports', () => {
+    it('should extract render tag', () => {
+      const code = `{% render 'loading-spinner' %}`;
+      const result = extractFromSource('template.liquid', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('loading-spinner');
+      expect(importNode?.signature).toContain('render');
+    });
+
+    it('should extract section tag', () => {
+      const code = `{% section 'header' %}`;
+      const result = extractFromSource('layout/theme.liquid', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('header');
+      expect(importNode?.signature).toContain('section');
+    });
+
+    it('should extract include tag', () => {
+      const code = `{% include 'icon-cart' %}`;
+      const result = extractFromSource('snippets/header.liquid', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('icon-cart');
+      expect(importNode?.signature).toContain('include');
+    });
+
+    it('should extract render with whitespace control', () => {
+      const code = `{%- render 'price' -%}`;
+      const result = extractFromSource('snippets/product.liquid', code);
+
+      const importNode = result.nodes.find((n) => n.kind === 'import');
+      expect(importNode).toBeDefined();
+      expect(importNode?.name).toBe('price');
+    });
+
+    it('should extract multiple imports', () => {
+      const code = `
+{% section 'header' %}
+{% render 'loading-spinner' %}
+{% render 'cart-drawer' %}
+`;
+      const result = extractFromSource('layout/theme.liquid', code);
+
+      const importNodes = result.nodes.filter((n) => n.kind === 'import');
+      expect(importNodes.length).toBe(3);
+
+      const names = importNodes.map((n) => n.name);
+      expect(names).toContain('header');
+      expect(names).toContain('loading-spinner');
+      expect(names).toContain('cart-drawer');
+    });
+  });
+});
+
 describe('Full Indexing', () => {
   let tempDir: string;
 

+ 3 - 3
__tests__/foundation.test.ts

@@ -48,7 +48,7 @@ describe('CodeGraph Foundation', () => {
       cg.close();
     });
 
-    it('should create .gitignore in .codegraph directory', () => {
+    it('should create .gitignore in .CodeGraph directory', () => {
       const cg = CodeGraph.initSync(tempDir);
 
       const gitignorePath = path.join(getCodeGraphDir(tempDir), '.gitignore');
@@ -225,7 +225,7 @@ describe('CodeGraph Foundation', () => {
   });
 
   describe('Uninitialize', () => {
-    it('should remove .codegraph directory', () => {
+    it('should remove .CodeGraph directory', () => {
       const cg = CodeGraph.initSync(tempDir);
 
       cg.uninitialize();
@@ -236,7 +236,7 @@ describe('CodeGraph Foundation', () => {
   });
 
   describe('Close/Destroy', () => {
-    it('should close database but keep .codegraph directory', () => {
+    it('should close database but keep .CodeGraph directory', () => {
       const cg = CodeGraph.initSync(tempDir);
 
       cg.destroy(); // destroy is alias for close

+ 3 - 243
__tests__/sync.test.ts

@@ -1,7 +1,9 @@
 /**
  * Sync Module Tests
  *
- * Tests for git hooks installation and sync functionality.
+ * Tests for sync functionality (incremental updates).
+ * Note: Git hooks functionality has been removed in favor of codegraph's
+ * Claude Code hooks integration.
  */
 
 import { describe, it, expect, beforeEach, afterEach } from 'vitest';
@@ -11,248 +13,6 @@ import * as os from 'os';
 import CodeGraph from '../src/index';
 
 describe('Sync Module', () => {
-  describe('Git Hooks', () => {
-    let testDir: string;
-    let cg: CodeGraph;
-
-    beforeEach(() => {
-      testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-sync-test-'));
-
-      // Create a sample source file
-      const srcDir = path.join(testDir, 'src');
-      fs.mkdirSync(srcDir);
-      fs.writeFileSync(
-        path.join(srcDir, 'index.ts'),
-        `export function hello() { return 'world'; }`
-      );
-
-      // Initialize CodeGraph
-      cg = CodeGraph.initSync(testDir, {
-        config: {
-          include: ['**/*.ts'],
-          exclude: [],
-        },
-      });
-    });
-
-    afterEach(() => {
-      if (cg) {
-        cg.destroy();
-      }
-      if (fs.existsSync(testDir)) {
-        fs.rmSync(testDir, { recursive: true, force: true });
-      }
-    });
-
-    describe('isGitRepository()', () => {
-      it('should return false for non-git directory', () => {
-        expect(cg.isGitRepository()).toBe(false);
-      });
-
-      it('should return true for git directory', () => {
-        // Initialize git
-        fs.mkdirSync(path.join(testDir, '.git'));
-
-        expect(cg.isGitRepository()).toBe(true);
-      });
-    });
-
-    describe('isGitHookInstalled()', () => {
-      it('should return false when no hook is installed', () => {
-        // Initialize git
-        fs.mkdirSync(path.join(testDir, '.git'));
-
-        expect(cg.isGitHookInstalled()).toBe(false);
-      });
-
-      it('should return false for non-codegraph hook', () => {
-        // Initialize git with a custom hook
-        const hooksDir = path.join(testDir, '.git', 'hooks');
-        fs.mkdirSync(path.join(testDir, '.git'));
-        fs.mkdirSync(hooksDir);
-        fs.writeFileSync(
-          path.join(hooksDir, 'post-commit'),
-          '#!/bin/sh\necho "custom hook"'
-        );
-
-        expect(cg.isGitHookInstalled()).toBe(false);
-      });
-
-      it('should return true when codegraph hook is installed', () => {
-        // Initialize git
-        fs.mkdirSync(path.join(testDir, '.git'));
-
-        // Install hook
-        cg.installGitHooks();
-
-        expect(cg.isGitHookInstalled()).toBe(true);
-      });
-    });
-
-    describe('installGitHooks()', () => {
-      it('should fail if not a git repository', () => {
-        const result = cg.installGitHooks();
-
-        expect(result.success).toBe(false);
-        expect(result.message).toContain('Not a git repository');
-      });
-
-      it('should install hook in git repository', () => {
-        // Initialize git
-        fs.mkdirSync(path.join(testDir, '.git'));
-
-        const result = cg.installGitHooks();
-
-        expect(result.success).toBe(true);
-        expect(result.message).toContain('installed');
-
-        // Verify hook file exists
-        const hookPath = path.join(testDir, '.git', 'hooks', 'post-commit');
-        expect(fs.existsSync(hookPath)).toBe(true);
-
-        // Verify hook content contains marker
-        const content = fs.readFileSync(hookPath, 'utf-8');
-        expect(content).toContain('CodeGraph auto-sync hook');
-        expect(content).toContain('codegraph sync');
-      });
-
-      it('should create hooks directory if missing', () => {
-        // Initialize git without hooks directory
-        fs.mkdirSync(path.join(testDir, '.git'));
-
-        const result = cg.installGitHooks();
-
-        expect(result.success).toBe(true);
-        expect(fs.existsSync(path.join(testDir, '.git', 'hooks'))).toBe(true);
-      });
-
-      it('should backup existing non-codegraph hook', () => {
-        // Initialize git with a custom hook
-        const hooksDir = path.join(testDir, '.git', 'hooks');
-        fs.mkdirSync(path.join(testDir, '.git'));
-        fs.mkdirSync(hooksDir);
-        const customHookContent = '#!/bin/sh\necho "custom hook"';
-        fs.writeFileSync(
-          path.join(hooksDir, 'post-commit'),
-          customHookContent
-        );
-
-        const result = cg.installGitHooks();
-
-        expect(result.success).toBe(true);
-        expect(result.previousHookBackedUp).toBe(true);
-
-        // Verify backup exists
-        const backupPath = path.join(hooksDir, 'post-commit.codegraph-backup');
-        expect(fs.existsSync(backupPath)).toBe(true);
-        expect(fs.readFileSync(backupPath, 'utf-8')).toBe(customHookContent);
-      });
-
-      it('should update existing codegraph hook without backup', () => {
-        // Initialize git
-        fs.mkdirSync(path.join(testDir, '.git'));
-
-        // Install hook first time
-        cg.installGitHooks();
-
-        // Install again (update)
-        const result = cg.installGitHooks();
-
-        expect(result.success).toBe(true);
-        expect(result.message).toContain('updated');
-        expect(result.previousHookBackedUp).toBeUndefined();
-      });
-
-      it('should make hook executable', () => {
-        // Initialize git
-        fs.mkdirSync(path.join(testDir, '.git'));
-
-        cg.installGitHooks();
-
-        const hookPath = path.join(testDir, '.git', 'hooks', 'post-commit');
-        const stats = fs.statSync(hookPath);
-
-        // Check executable bit (at least for owner)
-        expect(stats.mode & 0o100).toBeTruthy();
-      });
-    });
-
-    describe('removeGitHooks()', () => {
-      it('should succeed if no hook exists', () => {
-        // Initialize git
-        fs.mkdirSync(path.join(testDir, '.git'));
-
-        const result = cg.removeGitHooks();
-
-        expect(result.success).toBe(true);
-        expect(result.message).toContain('No post-commit hook found');
-      });
-
-      it('should not remove non-codegraph hook', () => {
-        // Initialize git with a custom hook
-        const hooksDir = path.join(testDir, '.git', 'hooks');
-        fs.mkdirSync(path.join(testDir, '.git'));
-        fs.mkdirSync(hooksDir);
-        fs.writeFileSync(
-          path.join(hooksDir, 'post-commit'),
-          '#!/bin/sh\necho "custom hook"'
-        );
-
-        const result = cg.removeGitHooks();
-
-        expect(result.success).toBe(false);
-        expect(result.message).toContain('not installed by CodeGraph');
-
-        // Verify hook still exists
-        expect(fs.existsSync(path.join(hooksDir, 'post-commit'))).toBe(true);
-      });
-
-      it('should remove codegraph hook', () => {
-        // Initialize git
-        fs.mkdirSync(path.join(testDir, '.git'));
-
-        // Install then remove
-        cg.installGitHooks();
-        const result = cg.removeGitHooks();
-
-        expect(result.success).toBe(true);
-        expect(result.message).toContain('removed');
-
-        // Verify hook is gone
-        const hookPath = path.join(testDir, '.git', 'hooks', 'post-commit');
-        expect(fs.existsSync(hookPath)).toBe(false);
-      });
-
-      it('should restore backup when removing', () => {
-        // Initialize git with a custom hook
-        const hooksDir = path.join(testDir, '.git', 'hooks');
-        fs.mkdirSync(path.join(testDir, '.git'));
-        fs.mkdirSync(hooksDir);
-        const customHookContent = '#!/bin/sh\necho "custom hook"';
-        fs.writeFileSync(
-          path.join(hooksDir, 'post-commit'),
-          customHookContent
-        );
-
-        // Install (backs up custom hook) then remove
-        cg.installGitHooks();
-        const result = cg.removeGitHooks();
-
-        expect(result.success).toBe(true);
-        expect(result.restoredFromBackup).toBe(true);
-
-        // Verify original hook is restored
-        const hookPath = path.join(hooksDir, 'post-commit');
-        expect(fs.existsSync(hookPath)).toBe(true);
-        expect(fs.readFileSync(hookPath, 'utf-8')).toBe(customHookContent);
-
-        // Verify backup is gone
-        const backupPath = path.join(hooksDir, 'post-commit.codegraph-backup');
-        expect(fs.existsSync(backupPath)).toBe(false);
-      });
-    });
-  });
-
   describe('Sync Functionality', () => {
     let testDir: string;
     let cg: CodeGraph;

+ 275 - 137
package-lock.json

@@ -1,14 +1,16 @@
 {
   "name": "@colbymchenry/codegraph",
-  "version": "0.2.6",
+  "version": "0.3.2",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@colbymchenry/codegraph",
-      "version": "0.2.6",
+      "version": "0.3.2",
+      "hasInstallScript": true,
       "license": "MIT",
       "dependencies": {
+        "@sengac/tree-sitter-dart": "^1.1.6",
         "@xenova/transformers": "^2.17.0",
         "better-sqlite3": "^11.0.0",
         "commander": "^14.0.2",
@@ -38,7 +40,7 @@
         "@types/figlet": "^1.5.8",
         "@types/node": "^20.19.30",
         "typescript": "^5.0.0",
-        "vitest": "^2.0.0"
+        "vitest": "^2.1.9"
       },
       "engines": {
         "node": ">=18.0.0"
@@ -516,9 +518,9 @@
       "license": "BSD-3-Clause"
     },
     "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
-      "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
+      "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==",
       "cpu": [
         "arm"
       ],
@@ -530,9 +532,9 @@
       ]
     },
     "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
-      "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
+      "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==",
       "cpu": [
         "arm64"
       ],
@@ -544,9 +546,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
-      "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
+      "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==",
       "cpu": [
         "arm64"
       ],
@@ -558,9 +560,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
-      "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
+      "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
       "cpu": [
         "x64"
       ],
@@ -572,9 +574,9 @@
       ]
     },
     "node_modules/@rollup/rollup-freebsd-arm64": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
-      "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
+      "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==",
       "cpu": [
         "arm64"
       ],
@@ -586,9 +588,9 @@
       ]
     },
     "node_modules/@rollup/rollup-freebsd-x64": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
-      "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
+      "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==",
       "cpu": [
         "x64"
       ],
@@ -600,9 +602,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
-      "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
+      "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==",
       "cpu": [
         "arm"
       ],
@@ -614,9 +616,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-musleabihf": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
-      "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
+      "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==",
       "cpu": [
         "arm"
       ],
@@ -628,9 +630,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
-      "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
+      "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==",
       "cpu": [
         "arm64"
       ],
@@ -642,9 +644,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
-      "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
+      "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==",
       "cpu": [
         "arm64"
       ],
@@ -656,9 +658,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-loong64-gnu": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
-      "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
+      "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==",
       "cpu": [
         "loong64"
       ],
@@ -670,9 +672,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-loong64-musl": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
-      "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
+      "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==",
       "cpu": [
         "loong64"
       ],
@@ -684,9 +686,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-ppc64-gnu": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
-      "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
+      "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==",
       "cpu": [
         "ppc64"
       ],
@@ -698,9 +700,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-ppc64-musl": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
-      "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
+      "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==",
       "cpu": [
         "ppc64"
       ],
@@ -712,9 +714,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
-      "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
+      "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==",
       "cpu": [
         "riscv64"
       ],
@@ -726,9 +728,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-riscv64-musl": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
-      "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
+      "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==",
       "cpu": [
         "riscv64"
       ],
@@ -740,9 +742,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-s390x-gnu": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
-      "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
+      "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==",
       "cpu": [
         "s390x"
       ],
@@ -754,9 +756,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
-      "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
+      "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==",
       "cpu": [
         "x64"
       ],
@@ -768,9 +770,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
-      "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
+      "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==",
       "cpu": [
         "x64"
       ],
@@ -782,9 +784,9 @@
       ]
     },
     "node_modules/@rollup/rollup-openbsd-x64": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
-      "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
+      "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==",
       "cpu": [
         "x64"
       ],
@@ -796,9 +798,9 @@
       ]
     },
     "node_modules/@rollup/rollup-openharmony-arm64": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
-      "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
+      "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==",
       "cpu": [
         "arm64"
       ],
@@ -810,9 +812,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
-      "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
+      "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==",
       "cpu": [
         "arm64"
       ],
@@ -824,9 +826,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
-      "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
+      "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==",
       "cpu": [
         "ia32"
       ],
@@ -838,9 +840,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-x64-gnu": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
-      "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
+      "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==",
       "cpu": [
         "x64"
       ],
@@ -852,9 +854,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
-      "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
+      "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==",
       "cpu": [
         "x64"
       ],
@@ -865,6 +867,25 @@
         "win32"
       ]
     },
+    "node_modules/@sengac/tree-sitter-dart": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/@sengac/tree-sitter-dart/-/tree-sitter-dart-1.1.6.tgz",
+      "integrity": "sha512-lLsF6pVmsC8+JkCnSvRzqa1jJYs+129EOn93MZCsvNnmDrZ2gcEaiqhTj69ttsjQZ2sR+LNxumdphHsw/Ln0Ew==",
+      "hasInstallScript": true,
+      "license": "ISC",
+      "dependencies": {
+        "node-addon-api": "^7.1.0",
+        "node-gyp-build": "^4.8.0"
+      },
+      "peerDependencies": {
+        "@sengac/tree-sitter": "^0.25.10"
+      },
+      "peerDependenciesMeta": {
+        "tree_sitter": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@types/better-sqlite3": {
       "version": "7.6.13",
       "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
@@ -896,9 +917,9 @@
       "license": "MIT"
     },
     "node_modules/@types/node": {
-      "version": "20.19.30",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
-      "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
+      "version": "20.19.33",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
+      "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
       "license": "MIT",
       "dependencies": {
         "undici-types": "~6.21.0"
@@ -1070,9 +1091,9 @@
       }
     },
     "node_modules/bare-fs": {
-      "version": "4.5.2",
-      "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.2.tgz",
-      "integrity": "sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw==",
+      "version": "4.5.3",
+      "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.3.tgz",
+      "integrity": "sha512-9+kwVx8QYvt3hPWnmb19tPnh38c6Nihz8Lx3t0g9+4GoIf3/fTgYwM4Z6NxgI+B9elLQA7mLE9PpqcWtOMRDiQ==",
       "license": "Apache-2.0",
       "optional": true,
       "dependencies": {
@@ -1306,9 +1327,9 @@
       }
     },
     "node_modules/commander": {
-      "version": "14.0.2",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
-      "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
+      "version": "14.0.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+      "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
       "license": "MIT",
       "engines": {
         "node": ">=20"
@@ -1475,9 +1496,9 @@
       "license": "MIT"
     },
     "node_modules/figlet": {
-      "version": "1.9.4",
-      "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.9.4.tgz",
-      "integrity": "sha512-uN6QE+TrzTAHC1IWTyrc4FfGo2KH/82J8Jl1tyKB7+z5DBit/m3D++Iu5lg91qJMnQQ3vpJrj5gxcK/pk4R9tQ==",
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.10.0.tgz",
+      "integrity": "sha512-aktIwEZZ6Gp9AWdMXW4YCi0J2Ahuxo67fNJRUIWD81w8pQ0t9TS8FFpbl27ChlTLF06VkwjDesZSzEVzN75rzA==",
       "license": "MIT",
       "dependencies": {
         "commander": "^14.0.0"
@@ -1661,9 +1682,9 @@
       "license": "MIT"
     },
     "node_modules/node-abi": {
-      "version": "3.86.0",
-      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.86.0.tgz",
-      "integrity": "sha512-sn9Et4N3ynsetj3spsZR729DVlGH6iBG4RiDMV7HEp3guyOW6W3S0unGpLDxT50mXortGUMax/ykUNQXdqc/Xg==",
+      "version": "3.87.0",
+      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
+      "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
       "license": "MIT",
       "dependencies": {
         "semver": "^7.3.5"
@@ -1673,13 +1694,10 @@
       }
     },
     "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==",
-      "license": "MIT",
-      "engines": {
-        "node": "^18 || ^20 || >= 21"
-      }
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+      "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+      "license": "MIT"
     },
     "node_modules/node-gyp-build": {
       "version": "4.8.4",
@@ -1896,9 +1914,9 @@
       }
     },
     "node_modules/rollup": {
-      "version": "4.55.1",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
-      "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
+      "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -1912,31 +1930,31 @@
         "npm": ">=8.0.0"
       },
       "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.55.1",
-        "@rollup/rollup-android-arm64": "4.55.1",
-        "@rollup/rollup-darwin-arm64": "4.55.1",
-        "@rollup/rollup-darwin-x64": "4.55.1",
-        "@rollup/rollup-freebsd-arm64": "4.55.1",
-        "@rollup/rollup-freebsd-x64": "4.55.1",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
-        "@rollup/rollup-linux-arm-musleabihf": "4.55.1",
-        "@rollup/rollup-linux-arm64-gnu": "4.55.1",
-        "@rollup/rollup-linux-arm64-musl": "4.55.1",
-        "@rollup/rollup-linux-loong64-gnu": "4.55.1",
-        "@rollup/rollup-linux-loong64-musl": "4.55.1",
-        "@rollup/rollup-linux-ppc64-gnu": "4.55.1",
-        "@rollup/rollup-linux-ppc64-musl": "4.55.1",
-        "@rollup/rollup-linux-riscv64-gnu": "4.55.1",
-        "@rollup/rollup-linux-riscv64-musl": "4.55.1",
-        "@rollup/rollup-linux-s390x-gnu": "4.55.1",
-        "@rollup/rollup-linux-x64-gnu": "4.55.1",
-        "@rollup/rollup-linux-x64-musl": "4.55.1",
-        "@rollup/rollup-openbsd-x64": "4.55.1",
-        "@rollup/rollup-openharmony-arm64": "4.55.1",
-        "@rollup/rollup-win32-arm64-msvc": "4.55.1",
-        "@rollup/rollup-win32-ia32-msvc": "4.55.1",
-        "@rollup/rollup-win32-x64-gnu": "4.55.1",
-        "@rollup/rollup-win32-x64-msvc": "4.55.1",
+        "@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",
         "fsevents": "~2.3.2"
       }
     },
@@ -1961,9 +1979,9 @@
       "license": "MIT"
     },
     "node_modules/semver": {
-      "version": "7.7.3",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
-      "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+      "version": "7.7.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+      "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
       "license": "ISC",
       "bin": {
         "semver": "bin/semver.js"
@@ -2317,6 +2335,24 @@
         }
       }
     },
+    "node_modules/tree-sitter-c-sharp/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==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
+    "node_modules/tree-sitter-c/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==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
     "node_modules/tree-sitter-cli": {
       "version": "0.23.2",
       "resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.23.2.tgz",
@@ -2350,6 +2386,15 @@
         }
       }
     },
+    "node_modules/tree-sitter-cpp/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==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
     "node_modules/tree-sitter-go": {
       "version": "0.23.4",
       "resolved": "https://registry.npmjs.org/tree-sitter-go/-/tree-sitter-go-0.23.4.tgz",
@@ -2369,6 +2414,15 @@
         }
       }
     },
+    "node_modules/tree-sitter-go/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==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
     "node_modules/tree-sitter-java": {
       "version": "0.23.5",
       "resolved": "https://registry.npmjs.org/tree-sitter-java/-/tree-sitter-java-0.23.5.tgz",
@@ -2388,6 +2442,15 @@
         }
       }
     },
+    "node_modules/tree-sitter-java/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==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
     "node_modules/tree-sitter-javascript": {
       "version": "0.23.1",
       "resolved": "https://registry.npmjs.org/tree-sitter-javascript/-/tree-sitter-javascript-0.23.1.tgz",
@@ -2407,6 +2470,15 @@
         }
       }
     },
+    "node_modules/tree-sitter-javascript/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==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
     "node_modules/tree-sitter-kotlin": {
       "version": "0.3.8",
       "resolved": "https://registry.npmjs.org/tree-sitter-kotlin/-/tree-sitter-kotlin-0.3.8.tgz",
@@ -2426,12 +2498,6 @@
         }
       }
     },
-    "node_modules/tree-sitter-kotlin/node_modules/node-addon-api": {
-      "version": "7.1.1",
-      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
-      "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
-      "license": "MIT"
-    },
     "node_modules/tree-sitter-liquid": {
       "version": "0.1.0",
       "resolved": "git+ssh://git@github.com/hankthetank27/tree-sitter-liquid.git#d6ebde3974742cd1b61b55d1d94aab1dacb41056",
@@ -2450,6 +2516,15 @@
         }
       }
     },
+    "node_modules/tree-sitter-liquid/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==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
     "node_modules/tree-sitter-php": {
       "version": "0.23.12",
       "resolved": "https://registry.npmjs.org/tree-sitter-php/-/tree-sitter-php-0.23.12.tgz",
@@ -2469,6 +2544,15 @@
         }
       }
     },
+    "node_modules/tree-sitter-php/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==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
     "node_modules/tree-sitter-python": {
       "version": "0.23.6",
       "resolved": "https://registry.npmjs.org/tree-sitter-python/-/tree-sitter-python-0.23.6.tgz",
@@ -2488,6 +2572,15 @@
         }
       }
     },
+    "node_modules/tree-sitter-python/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==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
     "node_modules/tree-sitter-ruby": {
       "version": "0.23.1",
       "resolved": "https://registry.npmjs.org/tree-sitter-ruby/-/tree-sitter-ruby-0.23.1.tgz",
@@ -2507,6 +2600,15 @@
         }
       }
     },
+    "node_modules/tree-sitter-ruby/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==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
     "node_modules/tree-sitter-rust": {
       "version": "0.23.3",
       "resolved": "https://registry.npmjs.org/tree-sitter-rust/-/tree-sitter-rust-0.23.3.tgz",
@@ -2526,6 +2628,15 @@
         }
       }
     },
+    "node_modules/tree-sitter-rust/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==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
     "node_modules/tree-sitter-swift": {
       "version": "0.7.1",
       "resolved": "https://registry.npmjs.org/tree-sitter-swift/-/tree-sitter-swift-0.7.1.tgz",
@@ -2547,6 +2658,15 @@
         }
       }
     },
+    "node_modules/tree-sitter-swift/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==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
     "node_modules/tree-sitter-typescript": {
       "version": "0.23.2",
       "resolved": "https://registry.npmjs.org/tree-sitter-typescript/-/tree-sitter-typescript-0.23.2.tgz",
@@ -2567,6 +2687,24 @@
         }
       }
     },
+    "node_modules/tree-sitter-typescript/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==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
+    "node_modules/tree-sitter/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==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
     "node_modules/tunnel-agent": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",

+ 5 - 4
package.json

@@ -1,6 +1,6 @@
 {
   "name": "@colbymchenry/codegraph",
-  "version": "0.3.1",
+  "version": "0.3.2",
   "description": "Supercharge Claude Code with semantic code intelligence. 30% fewer tokens, 25% fewer tool calls, 100% local.",
   "main": "dist/index.js",
   "types": "dist/index.d.ts",
@@ -15,14 +15,14 @@
   "scripts": {
     "build": "tsc && npm run copy-assets",
     "postinstall": "node scripts/postinstall.js",
-    "copy-assets": "cp -r src/extraction/queries dist/extraction/ && cp src/db/schema.sql dist/db/",
+    "copy-assets": "node -e \"const fs=require('fs'),p=require('path');function cpR(s,d){if(!fs.existsSync(s))return;fs.mkdirSync(d,{recursive:true});for(const f of fs.readdirSync(s)){const sp=p.join(s,f),dp=p.join(d,f);fs.statSync(sp).isDirectory()?cpR(sp,dp):fs.copyFileSync(sp,dp)}}cpR('src/extraction/queries','dist/extraction/queries');fs.mkdirSync('dist/db',{recursive:true});fs.copyFileSync('src/db/schema.sql','dist/db/schema.sql')\"",
     "dev": "tsc --watch",
     "cli": "npm run build && node dist/bin/codegraph.js",
     "test": "vitest run",
     "test:watch": "vitest",
     "test:eval": "vitest run __tests__/evaluation/",
     "eval": "npm run build && npx tsx __tests__/evaluation/runner.ts",
-    "clean": "rm -rf dist"
+    "clean": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true})\""
   },
   "keywords": [
     "code-intelligence",
@@ -42,6 +42,7 @@
     "tree-sitter-c": "^0.23.4",
     "tree-sitter-c-sharp": "^0.23.1",
     "tree-sitter-cpp": "^0.23.4",
+    "@sengac/tree-sitter-dart": "^1.1.6",
     "tree-sitter-go": "^0.23.4",
     "tree-sitter-java": "^0.23.5",
     "tree-sitter-javascript": "^0.23.1",
@@ -59,7 +60,7 @@
     "@types/figlet": "^1.5.8",
     "@types/node": "^20.19.30",
     "typescript": "^5.0.0",
-    "vitest": "^2.0.0"
+    "vitest": "^2.1.9"
   },
   "engines": {
     "node": ">=18.0.0"

+ 112 - 0
scripts/patch-tree-sitter-dart.js

@@ -0,0 +1,112 @@
+#!/usr/bin/env node
+/**
+ * Patches tree-sitter-dart to use NAPI bindings compatible with tree-sitter 0.22+
+ *
+ * tree-sitter-dart v1.0.0 ships with NAN-style bindings that are incompatible
+ * with tree-sitter 0.22+ which expects NAPI-style bindings with type-tagged
+ * externals. This script rewrites the binding files and rebuilds.
+ */
+const { writeFileSync, existsSync } = require('fs');
+const { join } = require('path');
+const { execSync } = require('child_process');
+
+const DART_DIR = join(__dirname, '..', 'node_modules', 'tree-sitter-dart');
+
+if (!existsSync(DART_DIR)) {
+  // tree-sitter-dart not installed, skip
+  process.exit(0);
+}
+
+// Check if already patched (look for NAPI-style binding)
+const bindingPath = join(DART_DIR, 'bindings', 'node', 'binding.cc');
+const { readFileSync } = require('fs');
+try {
+  const existing = readFileSync(bindingPath, 'utf8');
+  if (existing.includes('napi.h')) {
+    // Already patched, check if build exists
+    const buildPath = join(DART_DIR, 'build', 'Release', 'tree_sitter_dart_binding.node');
+    if (existsSync(buildPath)) {
+      console.log('tree-sitter-dart: already patched and built.');
+      process.exit(0);
+    }
+    // Patched but not built, fall through to rebuild
+  }
+} catch {
+  // Can't read, continue with patch
+}
+
+console.log('Patching tree-sitter-dart for NAPI compatibility...');
+
+// Write NAPI-compatible binding.cc
+const bindingCC = `#include <napi.h>
+
+typedef struct TSLanguage TSLanguage;
+
+extern "C" TSLanguage *tree_sitter_dart();
+
+// "tree-sitter", "language" hashed with BLAKE2
+const napi_type_tag LANGUAGE_TYPE_TAG = {
+    0x8AF2E5212AD58ABF, 0xD5006CAD83ABBA16
+};
+
+Napi::Object Init(Napi::Env env, Napi::Object exports) {
+    exports["name"] = Napi::String::New(env, "dart");
+    auto language = Napi::External<TSLanguage>::New(env, tree_sitter_dart());
+    language.TypeTag(&LANGUAGE_TYPE_TAG);
+    exports["language"] = language;
+    return exports;
+}
+
+NODE_API_MODULE(tree_sitter_dart_binding, Init)
+`;
+writeFileSync(bindingPath, bindingCC);
+
+// Write NAPI-compatible binding.gyp
+const bindingGyp = `{
+  "targets": [
+    {
+      "target_name": "tree_sitter_dart_binding",
+      "dependencies": [
+        "<!(node -p \\"require('node-addon-api').targets\\"):node_addon_api_except"
+      ],
+      "include_dirs": [
+        "src"
+      ],
+      "sources": [
+        "src/parser.c",
+        "bindings/node/binding.cc",
+        "src/scanner.c"
+      ],
+      "conditions": [
+        ["OS!='win'", {
+          "cflags_c": [
+            "-std=c99"
+          ]
+        }, {
+          "cflags_c": [
+            "/std:c11",
+            "/utf-8"
+          ]
+        }]
+      ]
+    }
+  ]
+}
+`;
+writeFileSync(join(DART_DIR, 'binding.gyp'), bindingGyp);
+
+// Rebuild native module
+try {
+  execSync('npx node-gyp rebuild', {
+    cwd: DART_DIR,
+    stdio: 'pipe',
+    timeout: 120000,
+  });
+  console.log('tree-sitter-dart: patched and rebuilt successfully.');
+} catch (error) {
+  console.error('Warning: Failed to rebuild tree-sitter-dart native module.');
+  console.error('Dart language support may not work.');
+  if (process.env.DEBUG) {
+    console.error(error.stderr?.toString());
+  }
+}

+ 3 - 0
scripts/postinstall.js

@@ -62,6 +62,9 @@ async function downloadModel() {
   }
 }
 
+// @sengac/tree-sitter-dart ships with NAPI prebuilds for all platforms
+// No patching needed (replaced old tree-sitter-dart v1.0.0 which used NAN bindings)
+
 downloadModel().catch(() => {
   // Silent exit - don't break npm install
   process.exit(0);

+ 274 - 131
src/bin/codegraph.ts

@@ -12,9 +12,11 @@
  *   codegraph sync [path]        Sync changes since last index
  *   codegraph status [path]      Show index status
  *   codegraph query <search>     Search for symbols
+ *   codegraph files [options]    Show project file structure
  *   codegraph context <task>     Build context for a task
- *   codegraph hooks install      Install git hooks
- *   codegraph hooks remove       Remove git hooks
+ *
+ * Note: Git hooks have been removed. CodeGraph sync is triggered automatically
+ * through codegraph's Claude Code hooks integration.
  */
 
 import { Command } from 'commander';
@@ -23,10 +25,20 @@ import * as fs from 'fs';
 import CodeGraph from '../index';
 import type { IndexProgress } from '../index';
 import { runInstaller } from '../installer';
+import { initSentry, captureException } from '../sentry';
 
 // Check if running with no arguments - run installer
+// Read version for Sentry release tag
+const pkgVersion = (() => {
+  try {
+    return JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf-8')).version;
+  } catch { return undefined; }
+})();
+initSentry({ processName: 'codegraph-cli', version: pkgVersion });
+
 if (process.argv.length === 2) {
   runInstaller().catch((err) => {
+    captureException(err);
     console.error('Installation failed:', err.message);
     process.exit(1);
   });
@@ -35,6 +47,16 @@ if (process.argv.length === 2) {
   main();
 }
 
+process.on('uncaughtException', (error) => {
+  captureException(error);
+  console.error('[CodeGraph] Uncaught exception:', error);
+});
+
+process.on('unhandledRejection', (reason) => {
+  captureException(reason);
+  console.error('[CodeGraph] Unhandled rejection:', reason);
+});
+
 function main() {
 
 const program = new Command();
@@ -84,9 +106,34 @@ program
 
 /**
  * Resolve project path from argument or current directory
+ * Walks up parent directories to find nearest initialized CodeGraph project
+ * (must have .codegraph/codegraph.db, not just .codegraph/lessons.db)
  */
 function resolveProjectPath(pathArg?: string): string {
-  return path.resolve(pathArg || process.cwd());
+  const absolutePath = path.resolve(pathArg || process.cwd());
+
+  // If exact path is initialized (has codegraph.db), use it
+  if (CodeGraph.isInitialized(absolutePath)) {
+    return absolutePath;
+  }
+
+  // Walk up to find nearest parent with CodeGraph initialized
+  // Note: findNearestCodeGraphRoot finds any .codegraph folder, but we need one with codegraph.db
+  let current = absolutePath;
+  const root = path.parse(current).root;
+
+  while (current !== root) {
+    const parent = path.dirname(current);
+    if (parent === current) break;
+    current = parent;
+
+    if (CodeGraph.isInitialized(current)) {
+      return current;
+    }
+  }
+
+  // Not found - return original path (will fail later with helpful error)
+  return absolutePath;
 }
 
 /**
@@ -182,8 +229,7 @@ program
   .command('init [path]')
   .description('Initialize CodeGraph in a project directory')
   .option('-i, --index', 'Run initial indexing after initialization')
-  .option('--no-hooks', 'Skip git hooks installation')
-  .action(async (pathArg: string | undefined, options: { index?: boolean; hooks?: boolean }) => {
+  .action(async (pathArg: string | undefined, options: { index?: boolean }) => {
     const projectPath = resolveProjectPath(pathArg);
 
     console.log(chalk.bold('\nInitializing CodeGraph...\n'));
@@ -204,16 +250,6 @@ program
       success(`Initialized CodeGraph in ${projectPath}`);
       info(`Created .codegraph/ directory`);
 
-      // Install git hooks if requested (default: true)
-      if (options.hooks !== false && cg.isGitRepository()) {
-        const hookResult = cg.installGitHooks();
-        if (hookResult.success) {
-          success('Installed git post-commit hook for auto-sync');
-        } else {
-          warn(`Could not install git hooks: ${hookResult.message}`);
-        }
-      }
-
       // Run initial index if requested
       if (options.index) {
         console.log('\nIndexing project...\n');
@@ -238,6 +274,7 @@ program
 
       cg.destroy();
     } catch (err) {
+      captureException(err);
       error(`Failed to initialize: ${err instanceof Error ? err.message : String(err)}`);
       process.exit(1);
     }
@@ -305,6 +342,7 @@ program
 
       cg.destroy();
     } catch (err) {
+      captureException(err);
       error(`Failed to index: ${err instanceof Error ? err.message : String(err)}`);
       process.exit(1);
     }
@@ -361,6 +399,7 @@ program
 
       cg.destroy();
     } catch (err) {
+      captureException(err);
       if (!options.quiet) {
         error(`Failed to sync: ${err instanceof Error ? err.message : String(err)}`);
       }
@@ -374,11 +413,16 @@ program
 program
   .command('status [path]')
   .description('Show index status and statistics')
-  .action(async (pathArg: string | undefined) => {
+  .option('-j, --json', 'Output as JSON')
+  .action(async (pathArg: string | undefined, options: { json?: boolean }) => {
     const projectPath = resolveProjectPath(pathArg);
 
     try {
       if (!CodeGraph.isInitialized(projectPath)) {
+        if (options.json) {
+          console.log(JSON.stringify({ initialized: false, projectPath }));
+          return;
+        }
         console.log(chalk.bold('\nCodeGraph Status\n'));
         info(`Project: ${projectPath}`);
         warn('Not initialized');
@@ -390,6 +434,27 @@ program
       const stats = cg.getStats();
       const changes = cg.getChangedFiles();
 
+      // JSON output mode
+      if (options.json) {
+        console.log(JSON.stringify({
+          initialized: true,
+          projectPath,
+          fileCount: stats.fileCount,
+          nodeCount: stats.nodeCount,
+          edgeCount: stats.edgeCount,
+          dbSizeBytes: stats.dbSizeBytes,
+          nodesByKind: stats.nodesByKind,
+          languages: Object.entries(stats.filesByLanguage).filter(([, count]) => count > 0).map(([lang]) => lang),
+          pendingChanges: {
+            added: changes.added.length,
+            modified: changes.modified.length,
+            removed: changes.removed.length,
+          },
+        }));
+        cg.destroy();
+        return;
+      }
+
       console.log(chalk.bold('\nCodeGraph Status\n'));
 
       // Project info
@@ -443,19 +508,9 @@ program
       }
       console.log();
 
-      // Git hooks status
-      if (cg.isGitRepository()) {
-        const hookInstalled = cg.isGitHookInstalled();
-        if (hookInstalled) {
-          success('Git hooks: installed');
-        } else {
-          warn('Git hooks: not installed');
-          info('Run "codegraph hooks install" to enable auto-sync');
-        }
-      }
-
       cg.destroy();
     } catch (err) {
+      captureException(err);
       error(`Failed to get status: ${err instanceof Error ? err.message : String(err)}`);
       process.exit(1);
     }
@@ -517,28 +572,33 @@ program
 
       cg.destroy();
     } catch (err) {
+      captureException(err);
       error(`Search failed: ${err instanceof Error ? err.message : String(err)}`);
       process.exit(1);
     }
   });
 
 /**
- * codegraph context <task>
+ * codegraph files [path]
  */
 program
-  .command('context <task>')
-  .description('Build context for a task (outputs markdown)')
+  .command('files')
+  .description('Show project file structure from the index')
   .option('-p, --path <path>', 'Project path')
-  .option('-n, --max-nodes <number>', 'Maximum nodes to include', '50')
-  .option('-c, --max-code <number>', 'Maximum code blocks', '10')
-  .option('--no-code', 'Exclude code blocks')
-  .option('-f, --format <format>', 'Output format (markdown, json)', 'markdown')
-  .action(async (task: string, options: {
+  .option('--filter <dir>', 'Filter to files under this directory')
+  .option('--pattern <glob>', 'Filter files matching this glob pattern')
+  .option('--format <format>', 'Output format (tree, flat, grouped)', 'tree')
+  .option('--max-depth <number>', 'Maximum directory depth for tree format')
+  .option('--no-metadata', 'Hide file metadata (language, symbol count)')
+  .option('-j, --json', 'Output as JSON')
+  .action(async (options: {
     path?: string;
-    maxNodes?: string;
-    maxCode?: string;
-    code?: boolean;
+    filter?: string;
+    pattern?: string;
     format?: string;
+    maxDepth?: string;
+    metadata?: boolean;
+    json?: boolean;
   }) => {
     const projectPath = resolveProjectPath(options.path);
 
@@ -549,116 +609,199 @@ program
       }
 
       const cg = await CodeGraph.open(projectPath);
+      let files = cg.getFiles();
 
-      const context = await cg.buildContext(task, {
-        maxNodes: parseInt(options.maxNodes || '50', 10),
-        maxCodeBlocks: parseInt(options.maxCode || '10', 10),
-        includeCode: options.code !== false,
-        format: options.format as 'markdown' | 'json',
-      });
-
-      // Output the context
-      console.log(context);
-
-      cg.destroy();
-    } catch (err) {
-      error(`Failed to build context: ${err instanceof Error ? err.message : String(err)}`);
-      process.exit(1);
-    }
-  });
-
-/**
- * codegraph hooks <action>
- */
-const hooksCommand = program
-  .command('hooks')
-  .description('Manage git hooks');
+      if (files.length === 0) {
+        info('No files indexed. Run "codegraph index" first.');
+        cg.destroy();
+        return;
+      }
 
-hooksCommand
-  .command('install')
-  .description('Install git post-commit hook for auto-sync')
-  .option('-p, --path <path>', 'Project path')
-  .action(async (options: { path?: string }) => {
-    const projectPath = resolveProjectPath(options.path);
+      // Filter by path prefix
+      if (options.filter) {
+        const filter = options.filter;
+        files = files.filter(f => f.path.startsWith(filter) || f.path.startsWith('./' + filter));
+      }
 
-    try {
-      if (!CodeGraph.isInitialized(projectPath)) {
-        error(`CodeGraph not initialized in ${projectPath}`);
-        process.exit(1);
+      // Filter by glob pattern
+      if (options.pattern) {
+        const regex = globToRegex(options.pattern);
+        files = files.filter(f => regex.test(f.path));
       }
 
-      const cg = await CodeGraph.open(projectPath);
+      if (files.length === 0) {
+        info('No files found matching the criteria.');
+        cg.destroy();
+        return;
+      }
 
-      if (!cg.isGitRepository()) {
-        error('Not a git repository');
+      // JSON output
+      if (options.json) {
+        const output = files.map(f => ({
+          path: f.path,
+          language: f.language,
+          nodeCount: f.nodeCount,
+          size: f.size,
+        }));
+        console.log(JSON.stringify(output, null, 2));
         cg.destroy();
-        process.exit(1);
+        return;
       }
 
-      const result = cg.installGitHooks();
+      const includeMetadata = options.metadata !== false;
+      const format = options.format || 'tree';
+      const maxDepth = options.maxDepth ? parseInt(options.maxDepth, 10) : undefined;
+
+      // Format output
+      switch (format) {
+        case 'flat':
+          console.log(chalk.bold(`\nFiles (${files.length}):\n`));
+          for (const file of files.sort((a, b) => a.path.localeCompare(b.path))) {
+            if (includeMetadata) {
+              console.log(`  ${file.path} ${chalk.dim(`(${file.language}, ${file.nodeCount} symbols)`)}`);
+            } else {
+              console.log(`  ${file.path}`);
+            }
+          }
+          break;
+
+        case 'grouped':
+          console.log(chalk.bold(`\nFiles by Language (${files.length} total):\n`));
+          const byLang = new Map<string, typeof files>();
+          for (const file of files) {
+            const existing = byLang.get(file.language) || [];
+            existing.push(file);
+            byLang.set(file.language, existing);
+          }
+          const sortedLangs = [...byLang.entries()].sort((a, b) => b[1].length - a[1].length);
+          for (const [lang, langFiles] of sortedLangs) {
+            console.log(chalk.cyan(`${lang} (${langFiles.length}):`));
+            for (const file of langFiles.sort((a, b) => a.path.localeCompare(b.path))) {
+              if (includeMetadata) {
+                console.log(`  ${file.path} ${chalk.dim(`(${file.nodeCount} symbols)`)}`);
+              } else {
+                console.log(`  ${file.path}`);
+              }
+            }
+            console.log();
+          }
+          break;
 
-      if (result.success) {
-        success(result.message);
-        if (result.previousHookBackedUp) {
-          info('Previous hook backed up to post-commit.codegraph-backup');
-        }
-      } else {
-        error(result.message);
-        process.exit(1);
+        case 'tree':
+        default:
+          console.log(chalk.bold(`\nProject Structure (${files.length} files):\n`));
+          printFileTree(files, includeMetadata, maxDepth, chalk);
+          break;
       }
 
+      console.log();
       cg.destroy();
     } catch (err) {
-      error(`Failed to install hooks: ${err instanceof Error ? err.message : String(err)}`);
+      captureException(err);
+      error(`Failed to list files: ${err instanceof Error ? err.message : String(err)}`);
       process.exit(1);
     }
   });
 
-hooksCommand
-  .command('remove')
-  .description('Remove git post-commit hook')
-  .option('-p, --path <path>', 'Project path')
-  .action(async (options: { path?: string }) => {
-    const projectPath = resolveProjectPath(options.path);
+/**
+ * Convert glob pattern to regex
+ */
+function globToRegex(pattern: string): RegExp {
+  const escaped = pattern
+    .replace(/[.+^${}()|[\]\\]/g, '\\$&')
+    .replace(/\*\*/g, '{{GLOBSTAR}}')
+    .replace(/\*/g, '[^/]*')
+    .replace(/\?/g, '[^/]')
+    .replace(/\{\{GLOBSTAR\}\}/g, '.*');
+  return new RegExp(escaped);
+}
 
-    try {
-      if (!CodeGraph.isInitialized(projectPath)) {
-        error(`CodeGraph not initialized in ${projectPath}`);
-        process.exit(1);
-      }
+/**
+ * Print files as a tree
+ */
+function printFileTree(
+  files: { path: string; language: string; nodeCount: number }[],
+  includeMetadata: boolean,
+  maxDepth: number | undefined,
+  chalk: { dim: (s: string) => string; cyan: (s: string) => string }
+): void {
+  interface TreeNode {
+    name: string;
+    children: Map<string, TreeNode>;
+    file?: { language: string; nodeCount: number };
+  }
 
-      const cg = await CodeGraph.open(projectPath);
+  const root: TreeNode = { name: '', children: new Map() };
 
-      if (!cg.isGitRepository()) {
-        error('Not a git repository');
-        cg.destroy();
-        process.exit(1);
+  for (const file of files) {
+    const parts = file.path.split('/');
+    let current = root;
+
+    for (let i = 0; i < parts.length; i++) {
+      const part = parts[i];
+      if (!part) continue;
+
+      if (!current.children.has(part)) {
+        current.children.set(part, { name: part, children: new Map() });
       }
+      current = current.children.get(part)!;
 
-      const result = cg.removeGitHooks();
+      if (i === parts.length - 1) {
+        current.file = { language: file.language, nodeCount: file.nodeCount };
+      }
+    }
+  }
 
-      if (result.success) {
-        success(result.message);
-        if (result.restoredFromBackup) {
-          info('Restored previous hook from backup');
-        }
-      } else {
-        error(result.message);
-        process.exit(1);
+  const renderNode = (node: TreeNode, prefix: string, isLast: boolean, depth: number): void => {
+    if (maxDepth !== undefined && depth > maxDepth) return;
+
+    const connector = isLast ? '└── ' : '├── ';
+    const childPrefix = isLast ? '    ' : '│   ';
+
+    if (node.name) {
+      let line = prefix + connector + node.name;
+      if (node.file && includeMetadata) {
+        line += chalk.dim(` (${node.file.language}, ${node.file.nodeCount} symbols)`);
       }
+      console.log(line);
+    }
 
-      cg.destroy();
-    } catch (err) {
-      error(`Failed to remove hooks: ${err instanceof Error ? err.message : String(err)}`);
-      process.exit(1);
+    const children = [...node.children.values()];
+    children.sort((a, b) => {
+      const aIsDir = a.children.size > 0 && !a.file;
+      const bIsDir = b.children.size > 0 && !b.file;
+      if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
+      return a.name.localeCompare(b.name);
+    });
+
+    for (let i = 0; i < children.length; i++) {
+      const child = children[i]!;
+      const nextPrefix = node.name ? prefix + childPrefix : prefix;
+      renderNode(child, nextPrefix, i === children.length - 1, depth + 1);
     }
-  });
+  };
 
-hooksCommand
-  .command('status')
-  .description('Check git hooks status')
+  renderNode(root, '', true, 0);
+}
+
+/**
+ * codegraph context <task>
+ */
+program
+  .command('context <task>')
+  .description('Build context for a task (outputs markdown)')
   .option('-p, --path <path>', 'Project path')
-  .action(async (options: { path?: string }) => {
+  .option('-n, --max-nodes <number>', 'Maximum nodes to include', '50')
+  .option('-c, --max-code <number>', 'Maximum code blocks', '10')
+  .option('--no-code', 'Exclude code blocks')
+  .option('-f, --format <format>', 'Output format (markdown, json)', 'markdown')
+  .action(async (task: string, options: {
+    path?: string;
+    maxNodes?: string;
+    maxCode?: string;
+    code?: boolean;
+    format?: string;
+  }) => {
     const projectPath = resolveProjectPath(options.path);
 
     try {
@@ -669,22 +812,20 @@ hooksCommand
 
       const cg = await CodeGraph.open(projectPath);
 
-      if (!cg.isGitRepository()) {
-        info('Not a git repository');
-        cg.destroy();
-        return;
-      }
+      const context = await cg.buildContext(task, {
+        maxNodes: parseInt(options.maxNodes || '50', 10),
+        maxCodeBlocks: parseInt(options.maxCode || '10', 10),
+        includeCode: options.code !== false,
+        format: options.format as 'markdown' | 'json',
+      });
 
-      if (cg.isGitHookInstalled()) {
-        success('Git hook is installed');
-      } else {
-        warn('Git hook is not installed');
-        info('Run "codegraph hooks install" to enable auto-sync');
-      }
+      // Output the context
+      console.log(context);
 
       cg.destroy();
     } catch (err) {
-      error(`Failed to check hooks: ${err instanceof Error ? err.message : String(err)}`);
+      captureException(err);
+      error(`Failed to build context: ${err instanceof Error ? err.message : String(err)}`);
       process.exit(1);
     }
   });
@@ -729,9 +870,11 @@ program
         console.log(chalk.cyan('  codegraph_callees') + '   - Find what a symbol calls');
         console.log(chalk.cyan('  codegraph_impact') + '    - Analyze impact of changes');
         console.log(chalk.cyan('  codegraph_node') + '      - Get symbol details');
+        console.log(chalk.cyan('  codegraph_files') + '     - Get project file structure');
         console.log(chalk.cyan('  codegraph_status') + '    - Get index status');
       }
     } catch (err) {
+      captureException(err);
       error(`Failed to start server: ${err instanceof Error ? err.message : String(err)}`);
       process.exit(1);
     }

+ 203 - 12
src/context/index.ts

@@ -10,6 +10,8 @@ import * as path from 'path';
 import {
   Node,
   Edge,
+  NodeKind,
+  EdgeKind,
   Subgraph,
   CodeBlock,
   TaskContext,
@@ -24,6 +26,74 @@ import { VectorManager } from '../vectors';
 import { formatContextAsMarkdown, formatContextAsJson } from './formatter';
 import { logDebug, logWarn } from '../errors';
 
+/**
+ * Extract likely symbol names from a natural language query
+ *
+ * Identifies potential code symbols using patterns:
+ * - CamelCase: UserService, signInWithGoogle
+ * - snake_case: user_service, sign_in
+ * - SCREAMING_SNAKE: MAX_RETRIES
+ * - dot.notation: app.isPackaged (extracts both sides)
+ * - Single words that look like identifiers (no spaces, not common English words)
+ *
+ * @param query - Natural language query
+ * @returns Array of potential symbol names
+ */
+function extractSymbolsFromQuery(query: string): string[] {
+  const symbols = new Set<string>();
+
+  // Extract CamelCase identifiers (2+ chars, starts with letter)
+  const camelCasePattern = /\b([A-Z][a-z]+(?:[A-Z][a-z]*)*|[a-z]+(?:[A-Z][a-z]*)+)\b/g;
+  let match;
+  while ((match = camelCasePattern.exec(query)) !== null) {
+    if (match[1] && match[1].length >= 2) {
+      symbols.add(match[1]);
+    }
+  }
+
+  // Extract snake_case identifiers
+  const snakeCasePattern = /\b([a-z][a-z0-9]*(?:_[a-z0-9]+)+)\b/gi;
+  while ((match = snakeCasePattern.exec(query)) !== null) {
+    if (match[1] && match[1].length >= 3) {
+      symbols.add(match[1]);
+    }
+  }
+
+  // Extract SCREAMING_SNAKE_CASE
+  const screamingPattern = /\b([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+)\b/g;
+  while ((match = screamingPattern.exec(query)) !== null) {
+    if (match[1]) {
+      symbols.add(match[1]);
+    }
+  }
+
+  // Extract dot.notation and split into parts (e.g., "app.isPackaged" -> ["app", "isPackaged"])
+  const dotPattern = /\b([a-zA-Z][a-zA-Z0-9]*(?:\.[a-zA-Z][a-zA-Z0-9]*)+)\b/g;
+  while ((match = dotPattern.exec(query)) !== null) {
+    if (match[1]) {
+      // Add both the full path and individual parts
+      symbols.add(match[1]);
+      const parts = match[1].split('.');
+      for (const part of parts) {
+        if (part.length >= 2) {
+          symbols.add(part);
+        }
+      }
+    }
+  }
+
+  // Filter out common English words that might match patterns
+  const commonWords = new Set([
+    'the', 'and', 'for', 'with', 'from', 'this', 'that', 'have', 'been',
+    'will', 'would', 'could', 'should', 'does', 'done', 'make', 'made',
+    'use', 'used', 'using', 'work', 'works', 'find', 'found', 'show',
+    'call', 'called', 'calling', 'get', 'set', 'add', 'all', 'any',
+    'how', 'what', 'when', 'where', 'which', 'who', 'why'
+  ]);
+
+  return Array.from(symbols).filter(s => !commonWords.has(s.toLowerCase()));
+}
+
 /**
  * Default options for context building
  *
@@ -43,6 +113,16 @@ const DEFAULT_BUILD_OPTIONS: Required<BuildContextOptions> = {
   minScore: 0.3,
 };
 
+/**
+ * Node kinds that provide high information value in context results.
+ * Imports/exports are excluded because they have near-zero information density -
+ * they tell you something exists, not how it works.
+ */
+const HIGH_VALUE_NODE_KINDS: NodeKind[] = [
+  'function', 'method', 'class', 'interface', 'type_alias', 'struct', 'trait',
+  'component', 'route', 'variable', 'constant', 'enum', 'module', 'namespace',
+];
+
 /**
  * Default options for finding relevant context
  */
@@ -52,7 +132,7 @@ const DEFAULT_FIND_OPTIONS: Required<FindRelevantContextOptions> = {
   maxNodes: 20,          // Reduced from 50
   minScore: 0.3,
   edgeKinds: [],
-  nodeKinds: [],
+  nodeKinds: HIGH_VALUE_NODE_KINDS, // Filter out imports/exports by default
 };
 
 /**
@@ -156,10 +236,12 @@ export class ContextBuilder {
   /**
    * Find relevant subgraph for a query
    *
-   * Combines semantic search with graph traversal:
-   * 1. Use semantic search to find relevant entry points
-   * 2. Traverse graph from entry points
-   * 3. Merge results into a unified subgraph
+   * Uses hybrid search combining exact symbol lookup with semantic search:
+   * 1. Extract potential symbol names from query
+   * 2. Look up exact matches for those symbols (high confidence)
+   * 3. Use semantic search for concept matching
+   * 4. Merge results, prioritizing exact matches
+   * 5. Traverse graph from entry points
    *
    * @param query - Natural language query
    * @param options - Search and traversal options
@@ -181,35 +263,84 @@ export class ContextBuilder {
       return { nodes, edges, roots };
     }
 
-    // Try semantic search if vector manager is available
-    let searchResults: SearchResult[] = [];
+    // === HYBRID SEARCH ===
+
+    // Step 1: Extract potential symbol names from query
+    const symbolsFromQuery = extractSymbolsFromQuery(query);
+    logDebug('Extracted symbols from query', { query, symbols: symbolsFromQuery });
+
+    // Step 2: Look up exact matches for extracted symbols
+    let exactMatches: SearchResult[] = [];
+    if (symbolsFromQuery.length > 0) {
+      try {
+        exactMatches = this.queries.findNodesByExactName(symbolsFromQuery, {
+          limit: Math.ceil(opts.searchLimit * 2), // Get more since we'll merge
+          kinds: opts.nodeKinds && opts.nodeKinds.length > 0 ? opts.nodeKinds : undefined,
+        });
+        logDebug('Exact symbol matches', { count: exactMatches.length });
+      } catch (error) {
+        logDebug('Exact symbol lookup failed', { error: String(error) });
+      }
+    }
+
+    // Step 3: Try semantic search if vector manager is available
+    let semanticResults: SearchResult[] = [];
     if (this.vectorManager && this.vectorManager.isInitialized()) {
       try {
-        searchResults = await this.vectorManager.search(query, {
+        semanticResults = await this.vectorManager.search(query, {
           limit: opts.searchLimit,
           kinds: opts.nodeKinds && opts.nodeKinds.length > 0 ? opts.nodeKinds : undefined,
         });
+        logDebug('Semantic search results', { count: semanticResults.length });
       } catch (error) {
         logDebug('Semantic search failed, falling back to text search', { query, error: String(error) });
       }
     }
 
-    // Fall back to text search if no semantic results
-    if (searchResults.length === 0) {
+    // Step 4: Fall back to text search if no semantic results
+    if (semanticResults.length === 0 && exactMatches.length === 0) {
       try {
         const textResults = this.queries.searchNodes(query, {
           limit: opts.searchLimit,
           kinds: opts.nodeKinds && opts.nodeKinds.length > 0 ? opts.nodeKinds : undefined,
         });
-        searchResults = textResults;
+        semanticResults = textResults;
       } catch (error) {
         logWarn('Text search failed', { query, error: String(error) });
         // Return empty results
       }
     }
 
+    // Step 5: Merge results, prioritizing exact matches
+    const seenIds = new Set<string>();
+    let searchResults: SearchResult[] = [];
+
+    // Add exact matches first (highest priority)
+    for (const result of exactMatches) {
+      if (!seenIds.has(result.node.id)) {
+        seenIds.add(result.node.id);
+        searchResults.push(result);
+      }
+    }
+
+    // Add semantic/text results
+    for (const result of semanticResults) {
+      if (!seenIds.has(result.node.id)) {
+        seenIds.add(result.node.id);
+        searchResults.push(result);
+      }
+    }
+
+    // Limit total results
+    searchResults = searchResults.slice(0, opts.searchLimit * 2);
+
     // Filter by minimum score
-    const filteredResults = searchResults.filter((r) => r.score >= opts.minScore);
+    let filteredResults = searchResults.filter((r) => r.score >= opts.minScore);
+
+    // Resolve imports/exports to their actual definitions
+    // If someone searches "terminal" and finds `import { TerminalPanel }`,
+    // they want the TerminalPanel class, not the import statement
+    filteredResults = this.resolveImportsToDefinitions(filteredResults);
 
     // Add entry points to subgraph
     for (const result of filteredResults) {
@@ -431,6 +562,66 @@ export class ContextBuilder {
       `Key entry points: ${entryPointNames}${remaining}. ` +
       `${edgeCount} relationships identified.`;
   }
+
+  /**
+   * Resolve import/export nodes to their actual definitions
+   *
+   * When search returns `import { TerminalPanel }`, users want the TerminalPanel
+   * class definition, not the import statement. This follows the `imports` edge
+   * to find and return the actual definition instead.
+   *
+   * @param results - Search results that may include import/export nodes
+   * @returns Results with imports resolved to definitions where possible
+   */
+  private resolveImportsToDefinitions(results: SearchResult[]): SearchResult[] {
+    const resolved: SearchResult[] = [];
+    const seenIds = new Set<string>();
+
+    for (const result of results) {
+      const { node, score } = result;
+
+      // If it's not an import/export, keep it as-is
+      if (node.kind !== 'import' && node.kind !== 'export') {
+        if (!seenIds.has(node.id)) {
+          seenIds.add(node.id);
+          resolved.push(result);
+        }
+        continue;
+      }
+
+      // For imports/exports, try to find what they reference
+      // Imports have outgoing 'imports' edges to the definition
+      // Exports have outgoing 'exports' edges to the definition
+      const edgeKind = node.kind === 'import' ? 'imports' : 'exports';
+      const outgoingEdges = this.queries.getOutgoingEdges(node.id, [edgeKind as EdgeKind]);
+
+      let foundDefinition = false;
+      for (const edge of outgoingEdges) {
+        const targetNode = this.queries.getNodeById(edge.target);
+        if (targetNode && !seenIds.has(targetNode.id)) {
+          // Found the definition - use it instead of the import
+          seenIds.add(targetNode.id);
+          resolved.push({
+            node: targetNode,
+            score: score, // Preserve the original score
+          });
+          foundDefinition = true;
+          logDebug('Resolved import to definition', {
+            import: node.name,
+            definition: targetNode.name,
+            kind: targetNode.kind,
+          });
+        }
+      }
+
+      // If we couldn't resolve the import, skip it (it's low-value on its own)
+      if (!foundDefinition) {
+        logDebug('Skipping unresolved import', { name: node.name, file: node.filePath });
+      }
+    }
+
+    return resolved;
+  }
 }
 
 /**

+ 6 - 0
src/db/index.ts

@@ -38,6 +38,9 @@ export class DatabaseConnection {
     // Enable foreign keys and WAL mode for better performance
     db.pragma('foreign_keys = ON');
     db.pragma('journal_mode = WAL');
+    // Wait up to 2 minutes if database is locked by another process
+    // (indexing operations can hold locks for extended periods)
+    db.pragma('busy_timeout = 120000');
 
     // Run schema initialization
     const schemaPath = path.join(__dirname, 'schema.sql');
@@ -60,6 +63,9 @@ export class DatabaseConnection {
     // Enable foreign keys and WAL mode
     db.pragma('foreign_keys = ON');
     db.pragma('journal_mode = WAL');
+    // Wait up to 2 minutes if database is locked by another process
+    // (indexing operations can hold locks for extended periods)
+    db.pragma('busy_timeout = 120000');
 
     // Check and run migrations if needed
     const conn = new DatabaseConnection(db, dbPath);

+ 110 - 28
src/db/queries.ts

@@ -198,28 +198,54 @@ export class QueryBuilder {
       `);
     }
 
-    this.stmts.insertNode.run({
-      id: node.id,
-      kind: node.kind,
-      name: node.name,
-      qualifiedName: node.qualifiedName,
-      filePath: node.filePath,
-      language: node.language,
-      startLine: node.startLine,
-      endLine: node.endLine,
-      startColumn: node.startColumn,
-      endColumn: node.endColumn,
-      docstring: node.docstring ?? null,
-      signature: node.signature ?? null,
-      visibility: node.visibility ?? null,
-      isExported: node.isExported ? 1 : 0,
-      isAsync: node.isAsync ? 1 : 0,
-      isStatic: node.isStatic ? 1 : 0,
-      isAbstract: node.isAbstract ? 1 : 0,
-      decorators: node.decorators ? JSON.stringify(node.decorators) : null,
-      typeParameters: node.typeParameters ? JSON.stringify(node.typeParameters) : null,
-      updatedAt: node.updatedAt,
-    });
+    // Validate required fields to prevent SQLite bind errors
+    if (!node.id || !node.kind || !node.name || !node.filePath || !node.language) {
+      console.error('[CodeGraph] Skipping node with missing required fields:', {
+        id: node.id,
+        kind: node.kind,
+        name: node.name,
+        filePath: node.filePath,
+        language: node.language,
+      });
+      return;
+    }
+
+    try {
+      this.stmts.insertNode.run({
+        id: node.id,
+        kind: node.kind,
+        name: node.name,
+        qualifiedName: node.qualifiedName ?? node.name,
+        filePath: node.filePath,
+        language: node.language,
+        startLine: node.startLine ?? 0,
+        endLine: node.endLine ?? 0,
+        startColumn: node.startColumn ?? 0,
+        endColumn: node.endColumn ?? 0,
+        docstring: node.docstring ?? null,
+        signature: node.signature ?? null,
+        visibility: node.visibility ?? null,
+        isExported: node.isExported ? 1 : 0,
+        isAsync: node.isAsync ? 1 : 0,
+        isStatic: node.isStatic ? 1 : 0,
+        isAbstract: node.isAbstract ? 1 : 0,
+        decorators: node.decorators ? JSON.stringify(node.decorators) : null,
+        typeParameters: node.typeParameters ? JSON.stringify(node.typeParameters) : null,
+        updatedAt: node.updatedAt ?? Date.now(),
+      });
+    } catch (error) {
+      const { captureException } = require('../sentry');
+      captureException(error, {
+        operation: 'insertNode',
+        nodeId: node.id,
+        nodeKind: node.kind,
+        nodeName: node.name,
+        filePath: node.filePath,
+        language: node.language,
+        startLine: node.startLine,
+      });
+      throw error;
+    }
   }
 
   /**
@@ -266,17 +292,23 @@ export class QueryBuilder {
     // Invalidate cache before update
     this.nodeCache.delete(node.id);
 
+    // Validate required fields
+    if (!node.id || !node.kind || !node.name || !node.filePath || !node.language) {
+      console.error('[CodeGraph] Skipping node update with missing required fields:', node.id);
+      return;
+    }
+
     this.stmts.updateNode.run({
       id: node.id,
       kind: node.kind,
       name: node.name,
-      qualifiedName: node.qualifiedName,
+      qualifiedName: node.qualifiedName ?? node.name,
       filePath: node.filePath,
       language: node.language,
-      startLine: node.startLine,
-      endLine: node.endLine,
-      startColumn: node.startColumn,
-      endColumn: node.endColumn,
+      startLine: node.startLine ?? 0,
+      endLine: node.endLine ?? 0,
+      startColumn: node.startColumn ?? 0,
+      endColumn: node.endColumn ?? 0,
       docstring: node.docstring ?? null,
       signature: node.signature ?? null,
       visibility: node.visibility ?? null,
@@ -286,7 +318,7 @@ export class QueryBuilder {
       isAbstract: node.isAbstract ? 1 : 0,
       decorators: node.decorators ? JSON.stringify(node.decorators) : null,
       typeParameters: node.typeParameters ? JSON.stringify(node.typeParameters) : null,
-      updatedAt: node.updatedAt,
+      updatedAt: node.updatedAt ?? Date.now(),
     });
   }
 
@@ -524,6 +556,56 @@ export class QueryBuilder {
     }));
   }
 
+  /**
+   * Find nodes by exact name match
+   *
+   * Used for hybrid search - looks up symbols by exact name or case-insensitive match.
+   * Returns high-confidence matches for known symbol names extracted from query.
+   *
+   * @param names - Array of symbol names to look up
+   * @param options - Search options (kinds, languages, limit)
+   * @returns SearchResult array with exact matches scored at 1.0
+   */
+  findNodesByExactName(names: string[], options: SearchOptions = {}): SearchResult[] {
+    if (names.length === 0) return [];
+
+    const { kinds, languages, limit = 50 } = options;
+
+    // Build query with exact matches (case-insensitive)
+    let sql = `
+      SELECT nodes.*,
+        CASE
+          WHEN name COLLATE NOCASE IN (${names.map(() => '?').join(',')}) THEN 1.0
+          ELSE 0.9
+        END as score
+      FROM nodes
+      WHERE name COLLATE NOCASE IN (${names.map(() => '?').join(',')})
+    `;
+
+    // Duplicate names for both SELECT and WHERE clauses
+    const params: (string | number)[] = [...names, ...names];
+
+    if (kinds && kinds.length > 0) {
+      sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
+      params.push(...kinds);
+    }
+
+    if (languages && languages.length > 0) {
+      sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
+      params.push(...languages);
+    }
+
+    sql += ' ORDER BY score DESC, length(name) ASC LIMIT ?';
+    params.push(limit);
+
+    const rows = this.db.prepare(sql).all(...params) as (NodeRow & { score: number })[];
+
+    return rows.map((row) => ({
+      node: rowToNode(row),
+      score: row.score,
+    }));
+  }
+
   // ===========================================================================
   // Edge Operations
   // ===========================================================================

+ 10 - 9
src/db/schema.sql

@@ -89,32 +89,33 @@ CREATE INDEX IF NOT EXISTS idx_nodes_file_path ON nodes(file_path);
 CREATE INDEX IF NOT EXISTS idx_nodes_language ON nodes(language);
 CREATE INDEX IF NOT EXISTS idx_nodes_file_line ON nodes(file_path, start_line);
 
--- Full-text search index on node names and docstrings
+-- Full-text search index on node names, docstrings, and signatures
 CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
     id,
     name,
     qualified_name,
     docstring,
+    signature,
     content='nodes',
     content_rowid='rowid'
 );
 
 -- Triggers to keep FTS index in sync
 CREATE TRIGGER IF NOT EXISTS nodes_ai AFTER INSERT ON nodes BEGIN
-    INSERT INTO nodes_fts(rowid, id, name, qualified_name, docstring)
-    VALUES (NEW.rowid, NEW.id, NEW.name, NEW.qualified_name, NEW.docstring);
+    INSERT INTO nodes_fts(rowid, id, name, qualified_name, docstring, signature)
+    VALUES (NEW.rowid, NEW.id, NEW.name, NEW.qualified_name, NEW.docstring, NEW.signature);
 END;
 
 CREATE TRIGGER IF NOT EXISTS nodes_ad AFTER DELETE ON nodes BEGIN
-    INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualified_name, docstring)
-    VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.qualified_name, OLD.docstring);
+    INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualified_name, docstring, signature)
+    VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.qualified_name, OLD.docstring, OLD.signature);
 END;
 
 CREATE TRIGGER IF NOT EXISTS nodes_au AFTER UPDATE ON nodes BEGIN
-    INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualified_name, docstring)
-    VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.qualified_name, OLD.docstring);
-    INSERT INTO nodes_fts(rowid, id, name, qualified_name, docstring)
-    VALUES (NEW.rowid, NEW.id, NEW.name, NEW.qualified_name, NEW.docstring);
+    INSERT INTO nodes_fts(nodes_fts, rowid, id, name, qualified_name, docstring, signature)
+    VALUES ('delete', OLD.rowid, OLD.id, OLD.name, OLD.qualified_name, OLD.docstring, OLD.signature);
+    INSERT INTO nodes_fts(rowid, id, name, qualified_name, docstring, signature)
+    VALUES (NEW.rowid, NEW.id, NEW.name, NEW.qualified_name, NEW.docstring, NEW.signature);
 END;
 
 -- Edge indexes

+ 49 - 7
src/directory.ts

@@ -1,7 +1,7 @@
 /**
  * Directory Management
  *
- * Manages the .codegraph/ directory structure.
+ * Manages the .codegraph/ directory structure for CodeGraph data.
  */
 
 import * as fs from 'fs';
@@ -21,28 +21,69 @@ export function getCodeGraphDir(projectRoot: string): string {
 
 /**
  * Check if a project has been initialized with CodeGraph
+ * Requires both .codegraph/ directory AND codegraph.db to exist
  */
 export function isInitialized(projectRoot: string): boolean {
   const codegraphDir = getCodeGraphDir(projectRoot);
-  return fs.existsSync(codegraphDir) && fs.statSync(codegraphDir).isDirectory();
+  if (!fs.existsSync(codegraphDir) || !fs.statSync(codegraphDir).isDirectory()) {
+    return false;
+  }
+  // Must have codegraph.db, not just .codegraph folder
+  const dbPath = path.join(codegraphDir, 'codegraph.db');
+  return fs.existsSync(dbPath);
+}
+
+/**
+ * Find the nearest parent directory containing .codegraph/
+ *
+ * Walks up from the given path to find a CodeGraph-initialized project,
+ * similar to how git finds .git/ directories.
+ *
+ * @param startPath - Directory to start searching from
+ * @returns The project root containing .codegraph/, or null if not found
+ */
+export function findNearestCodeGraphRoot(startPath: string): string | null {
+  let current = path.resolve(startPath);
+  const root = path.parse(current).root;
+
+  while (current !== root) {
+    if (isInitialized(current)) {
+      return current;
+    }
+    const parent = path.dirname(current);
+    if (parent === current) break; // Reached filesystem root
+    current = parent;
+  }
+
+  // Check root as well
+  if (isInitialized(current)) {
+    return current;
+  }
+
+  return null;
 }
 
 /**
  * Create the .codegraph directory structure
+ * Note: Only throws if codegraph.db already exists, not just if .codegraph/ exists.
  */
 export function createDirectory(projectRoot: string): void {
   const codegraphDir = getCodeGraphDir(projectRoot);
+  const dbPath = path.join(codegraphDir, 'codegraph.db');
 
-  if (fs.existsSync(codegraphDir)) {
+  // Only throw if CodeGraph is actually initialized (db exists)
+  // .codegraph/ folder alone is fine
+  if (fs.existsSync(dbPath)) {
     throw new Error(`CodeGraph already initialized in ${projectRoot}`);
   }
 
-  // Create main directory
+  // Create main directory (if it doesn't exist)
   fs.mkdirSync(codegraphDir, { recursive: true });
 
-  // Create .gitignore inside .codegraph
+  // Create .gitignore inside .codegraph (if it doesn't exist)
   const gitignorePath = path.join(codegraphDir, '.gitignore');
-  const gitignoreContent = `# CodeGraph data files
+  if (!fs.existsSync(gitignorePath)) {
+    const gitignoreContent = `# CodeGraph data files
 # These are local to each machine and should not be committed
 
 # Database
@@ -57,7 +98,8 @@ cache/
 *.log
 `;
 
-  fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8');
+    fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8');
+  }
 }
 
 /**

+ 6 - 1
src/errors.ts

@@ -233,8 +233,13 @@ export function logWarn(message: string, context?: Record<string, unknown>): voi
 }
 
 /**
- * Log an error message
+ * Log an error message (also sends to Sentry if initialized)
  */
 export function logError(message: string, context?: Record<string, unknown>): void {
   currentLogger.error(message, context);
+  // Lazy import to avoid circular deps
+  try {
+    const { captureMessage } = require('./sentry');
+    captureMessage(message, context);
+  } catch {}
 }

+ 56 - 46
src/extraction/grammars.ts

@@ -7,59 +7,67 @@
 import Parser from 'tree-sitter';
 import { Language } from '../types';
 
-// Grammar module imports
+// Grammar module imports — wrapped in tryRequire so a missing native binding
+// (e.g. tree-sitter-kotlin on Windows) degrades gracefully instead of crashing.
 // eslint-disable-next-line @typescript-eslint/no-require-imports
-const TypeScript = require('tree-sitter-typescript').typescript;
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-const TSX = require('tree-sitter-typescript').tsx;
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-const JavaScript = require('tree-sitter-javascript');
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-const Python = require('tree-sitter-python');
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-const Go = require('tree-sitter-go');
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-const Rust = require('tree-sitter-rust');
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-const Java = require('tree-sitter-java');
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-const C = require('tree-sitter-c');
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-const Cpp = require('tree-sitter-cpp');
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-const CSharp = require('tree-sitter-c-sharp');
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-const PHP = require('tree-sitter-php').php;
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-const Ruby = require('tree-sitter-ruby');
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-const Swift = require('tree-sitter-swift');
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-const Kotlin = require('tree-sitter-kotlin');
+function tryRequire(id: string, prop?: string): unknown | null {
+  try {
+    // eslint-disable-next-line @typescript-eslint/no-require-imports
+    const mod = require(id);
+    return prop ? mod[prop] : mod;
+  } catch {
+    console.warn(`[CodeGraph] Failed to load ${id} — ${prop ?? id} parsing will be unavailable on this platform.`);
+    return null;
+  }
+}
+
+const TypeScript = tryRequire('tree-sitter-typescript', 'typescript');
+const TSX = tryRequire('tree-sitter-typescript', 'tsx');
+const JavaScript = tryRequire('tree-sitter-javascript');
+const Python = tryRequire('tree-sitter-python');
+const Go = tryRequire('tree-sitter-go');
+const Rust = tryRequire('tree-sitter-rust');
+const Java = tryRequire('tree-sitter-java');
+const C = tryRequire('tree-sitter-c');
+const Cpp = tryRequire('tree-sitter-cpp');
+const CSharp = tryRequire('tree-sitter-c-sharp');
+const PHP = tryRequire('tree-sitter-php', 'php');
+const Ruby = tryRequire('tree-sitter-ruby');
+const Swift = tryRequire('tree-sitter-swift');
+const Kotlin = tryRequire('tree-sitter-kotlin');
+const Dart = tryRequire('@sengac/tree-sitter-dart');
 // Note: tree-sitter-liquid has ABI compatibility issues with tree-sitter 0.22+
 // Liquid extraction is handled separately via regex in tree-sitter.ts
 
 /**
- * Mapping of Language to tree-sitter grammar
+ * Mapping of Language to tree-sitter grammar.
+ * Parsers that failed to load are excluded.
  */
-const GRAMMAR_MAP: Record<string, unknown> = {
-  typescript: TypeScript,
-  tsx: TSX,
-  javascript: JavaScript,
-  jsx: JavaScript, // JSX uses the JavaScript grammar
-  python: Python,
-  go: Go,
-  rust: Rust,
-  java: Java,
-  c: C,
-  cpp: Cpp,
-  csharp: CSharp,
-  php: PHP,
-  ruby: Ruby,
-  swift: Swift,
-  kotlin: Kotlin,
+const GRAMMAR_MAP: Record<string, unknown> = {};
+
+const grammarEntries: [string, unknown][] = [
+  ['typescript', TypeScript],
+  ['tsx', TSX],
+  ['javascript', JavaScript],
+  ['jsx', JavaScript], // JSX uses the JavaScript grammar
+  ['python', Python],
+  ['go', Go],
+  ['rust', Rust],
+  ['java', Java],
+  ['c', C],
+  ['cpp', Cpp],
+  ['csharp', CSharp],
+  ['php', PHP],
+  ['ruby', Ruby],
+  ['swift', Swift],
+  ['kotlin', Kotlin],
+  ['dart', Dart],
   // liquid: uses custom regex-based extraction, not tree-sitter
-};
+];
+
+for (const [lang, grammar] of grammarEntries) {
+  if (grammar) GRAMMAR_MAP[lang] = grammar;
+}
 
 /**
  * File extension to Language mapping
@@ -90,6 +98,7 @@ export const EXTENSION_MAP: Record<string, Language> = {
   '.swift': 'swift',
   '.kt': 'kotlin',
   '.kts': 'kotlin',
+  '.dart': 'dart',
   '.liquid': 'liquid',
 };
 
@@ -175,6 +184,7 @@ export function getLanguageDisplayName(language: Language): string {
     ruby: 'Ruby',
     swift: 'Swift',
     kotlin: 'Kotlin',
+    dart: 'Dart',
     liquid: 'Liquid',
     unknown: 'Unknown',
   };

+ 17 - 0
src/extraction/index.ts

@@ -18,6 +18,7 @@ import { QueryBuilder } from '../db/queries';
 import { extractFromSource } from './tree-sitter';
 import { detectLanguage, isLanguageSupported } from './grammars';
 import { logDebug } from '../errors';
+import { captureException } from '../sentry';
 
 /**
  * Progress callback for indexing operations
@@ -111,6 +112,11 @@ export function shouldIncludeFile(
   return false;
 }
 
+/**
+ * Marker file name that indicates a directory (and all children) should be skipped
+ */
+const CODEGRAPH_IGNORE_MARKER = '.codegraphignore';
+
 /**
  * Recursively scan directory for source files
  */
@@ -123,10 +129,18 @@ export function scanDirectory(
   let count = 0;
 
   function walk(dir: string): void {
+    // Check for .codegraphignore marker file - skip entire directory tree if present
+    const ignoreMarker = path.join(dir, CODEGRAPH_IGNORE_MARKER);
+    if (fs.existsSync(ignoreMarker)) {
+      logDebug('Skipping directory due to .codegraphignore marker', { dir });
+      return;
+    }
+
     let entries: fs.Dirent[];
     try {
       entries = fs.readdirSync(dir, { withFileTypes: true });
     } catch (error) {
+      captureException(error, { operation: 'walk-directory', dir });
       logDebug('Skipping unreadable directory', { dir, error: String(error) });
       return;
     }
@@ -330,6 +344,7 @@ export class ExtractionOrchestrator {
       stats = fs.statSync(fullPath);
       content = fs.readFileSync(fullPath, 'utf-8');
     } catch (error) {
+      captureException(error, { operation: 'extract-file', filePath: fullPath });
       return {
         nodes: [],
         edges: [],
@@ -476,6 +491,7 @@ export class ExtractionOrchestrator {
       try {
         content = fs.readFileSync(fullPath, 'utf-8');
       } catch (error) {
+        captureException(error, { operation: 'sync-read-file', filePath });
         logDebug('Skipping unreadable file during sync', { filePath, error: String(error) });
         continue;
       }
@@ -544,6 +560,7 @@ export class ExtractionOrchestrator {
       try {
         content = fs.readFileSync(fullPath, 'utf-8');
       } catch (error) {
+        captureException(error, { operation: 'detect-changes-read-file', filePath });
         logDebug('Skipping unreadable file while detecting changes', { filePath, error: String(error) });
         continue;
       }

+ 673 - 19
src/extraction/tree-sitter.ts

@@ -16,6 +16,7 @@ import {
   UnresolvedReference,
 } from '../types';
 import { getParser, detectLanguage, isLanguageSupported } from './grammars';
+import { captureException } from '../sentry';
 
 /**
  * Generate a unique node ID
@@ -107,6 +108,8 @@ interface LanguageExtractor {
   importTypes: string[];
   /** Node types that represent function calls */
   callTypes: string[];
+  /** Node types that represent variable declarations (const, let, var, etc.) */
+  variableTypes: string[];
   /** Field name for identifier/name */
   nameField: string;
   /** Field name for body */
@@ -125,6 +128,8 @@ interface LanguageExtractor {
   isAsync?: (node: SyntaxNode) => boolean;
   /** Check if node is static */
   isStatic?: (node: SyntaxNode) => boolean;
+  /** Check if variable declaration is a constant (const vs let/var) */
+  isConst?: (node: SyntaxNode) => boolean;
 }
 
 /**
@@ -140,6 +145,7 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
     enumTypes: ['enum_declaration'],
     importTypes: ['import_statement'],
     callTypes: ['call_expression'],
+    variableTypes: ['lexical_declaration', 'variable_declaration'],
     nameField: 'name',
     bodyField: 'body',
     paramsField: 'parameters',
@@ -187,6 +193,17 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
       }
       return false;
     },
+    isConst: (node) => {
+      // For lexical_declaration, check if it's 'const' or 'let'
+      // For variable_declaration, it's always 'var'
+      if (node.type === 'lexical_declaration') {
+        for (let i = 0; i < node.childCount; i++) {
+          const child = node.child(i);
+          if (child?.type === 'const') return true;
+        }
+      }
+      return false;
+    },
   },
   javascript: {
     functionTypes: ['function_declaration', 'arrow_function', 'function_expression'],
@@ -197,6 +214,7 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
     enumTypes: [],
     importTypes: ['import_statement'],
     callTypes: ['call_expression'],
+    variableTypes: ['lexical_declaration', 'variable_declaration'],
     nameField: 'name',
     bodyField: 'body',
     paramsField: 'parameters',
@@ -217,6 +235,15 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
       }
       return false;
     },
+    isConst: (node) => {
+      if (node.type === 'lexical_declaration') {
+        for (let i = 0; i < node.childCount; i++) {
+          const child = node.child(i);
+          if (child?.type === 'const') return true;
+        }
+      }
+      return false;
+    },
   },
   python: {
     functionTypes: ['function_definition'],
@@ -227,6 +254,7 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
     enumTypes: [],
     importTypes: ['import_statement', 'import_from_statement'],
     callTypes: ['call'],
+    variableTypes: ['assignment'], // Python uses assignment for variable declarations
     nameField: 'name',
     bodyField: 'body',
     paramsField: 'parameters',
@@ -264,6 +292,7 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
     enumTypes: [],
     importTypes: ['import_declaration'],
     callTypes: ['call_expression'],
+    variableTypes: ['var_declaration', 'short_var_declaration', 'const_declaration'],
     nameField: 'name',
     bodyField: 'body',
     paramsField: 'parameters',
@@ -288,6 +317,7 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
     enumTypes: ['enum_item'],
     importTypes: ['use_declaration'],
     callTypes: ['call_expression'],
+    variableTypes: ['let_declaration', 'const_item', 'static_item'],
     nameField: 'name',
     bodyField: 'body',
     paramsField: 'parameters',
@@ -328,6 +358,7 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
     enumTypes: ['enum_declaration'],
     importTypes: ['import_declaration'],
     callTypes: ['method_invocation'],
+    variableTypes: ['local_variable_declaration', 'field_declaration'],
     nameField: 'name',
     bodyField: 'body',
     paramsField: 'parameters',
@@ -370,6 +401,7 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
     enumTypes: ['enum_specifier'],
     importTypes: ['preproc_include'],
     callTypes: ['call_expression'],
+    variableTypes: ['declaration'],
     nameField: 'declarator',
     bodyField: 'body',
     paramsField: 'parameters',
@@ -383,6 +415,7 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
     enumTypes: ['enum_specifier'],
     importTypes: ['preproc_include'],
     callTypes: ['call_expression'],
+    variableTypes: ['declaration'],
     nameField: 'declarator',
     bodyField: 'body',
     paramsField: 'parameters',
@@ -412,6 +445,7 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
     enumTypes: ['enum_declaration'],
     importTypes: ['using_directive'],
     callTypes: ['invocation_expression'],
+    variableTypes: ['local_declaration_statement', 'field_declaration'],
     nameField: 'name',
     bodyField: 'body',
     paramsField: 'parameter_list',
@@ -456,6 +490,7 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
     enumTypes: ['enum_declaration'],
     importTypes: ['namespace_use_declaration'],
     callTypes: ['function_call_expression', 'member_call_expression', 'scoped_call_expression'],
+    variableTypes: ['property_declaration', 'const_declaration'],
     nameField: 'name',
     bodyField: 'body',
     paramsField: 'parameters',
@@ -489,6 +524,7 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
     enumTypes: [],
     importTypes: ['call'], // require/require_relative
     callTypes: ['call', 'method_call'],
+    variableTypes: ['assignment'], // Ruby uses assignment like Python
     nameField: 'name',
     bodyField: 'body',
     paramsField: 'parameters',
@@ -519,6 +555,7 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
     enumTypes: ['enum_declaration'],
     importTypes: ['import_declaration'],
     callTypes: ['call_expression'],
+    variableTypes: ['property_declaration', 'constant_declaration'],
     nameField: 'name',
     bodyField: 'body',
     paramsField: 'parameter',
@@ -578,6 +615,7 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
     enumTypes: ['class_declaration'], // Enums use class_declaration with 'enum' modifier
     importTypes: ['import_header'],
     callTypes: ['call_expression'],
+    variableTypes: ['property_declaration'],
     nameField: 'simple_identifier',
     bodyField: 'function_body',
     paramsField: 'function_value_parameters',
@@ -623,6 +661,76 @@ const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
       return false;
     },
   },
+  dart: {
+    functionTypes: ['function_signature'],
+    classTypes: ['class_definition'],
+    methodTypes: ['method_signature'],
+    interfaceTypes: [],
+    structTypes: [],
+    enumTypes: ['enum_declaration'],
+    importTypes: ['import_or_export'],
+    callTypes: [],  // Dart calls use identifier+selector, handled via function body traversal
+    variableTypes: [],
+    nameField: 'name',
+    bodyField: 'body', // class_definition uses 'body' field
+    paramsField: 'formal_parameter_list',
+    returnField: 'type',
+    getSignature: (node, source) => {
+      // For function_signature: extract params + return type
+      // For method_signature: delegate to inner function_signature
+      let sig = node;
+      if (node.type === 'method_signature') {
+        const inner = node.namedChildren.find((c: SyntaxNode) =>
+          c.type === 'function_signature' || c.type === 'getter_signature' || c.type === 'setter_signature'
+        );
+        if (inner) sig = inner;
+      }
+      const params = sig.namedChildren.find((c: SyntaxNode) => c.type === 'formal_parameter_list');
+      const retType = sig.namedChildren.find((c: SyntaxNode) =>
+        c.type === 'type_identifier' || c.type === 'void_type'
+      );
+      if (!params && !retType) return undefined;
+      let result = '';
+      if (retType) result += getNodeText(retType, source) + ' ';
+      if (params) result += getNodeText(params, source);
+      return result.trim() || undefined;
+    },
+    getVisibility: (node) => {
+      // Dart convention: _ prefix means private, otherwise public
+      let nameNode: SyntaxNode | null = null;
+      if (node.type === 'method_signature') {
+        const inner = node.namedChildren.find((c: SyntaxNode) =>
+          c.type === 'function_signature' || c.type === 'getter_signature' || c.type === 'setter_signature'
+        );
+        if (inner) nameNode = inner.namedChildren.find((c: SyntaxNode) => c.type === 'identifier') || null;
+      } else {
+        nameNode = node.childForFieldName('name');
+      }
+      if (nameNode && nameNode.text.startsWith('_')) return 'private';
+      return 'public';
+    },
+    isAsync: (node) => {
+      // In Dart, 'async' is on the function_body (next sibling), not the signature
+      const nextSibling = node.nextNamedSibling;
+      if (nextSibling?.type === 'function_body') {
+        for (let i = 0; i < nextSibling.childCount; i++) {
+          const child = nextSibling.child(i);
+          if (child?.type === 'async') return true;
+        }
+      }
+      return false;
+    },
+    isStatic: (node) => {
+      // For method_signature, check for 'static' child
+      if (node.type === 'method_signature') {
+        for (let i = 0; i < node.childCount; i++) {
+          const child = node.child(i);
+          if (child?.type === 'static') return true;
+        }
+      }
+      return false;
+    },
+  },
 };
 
 // TSX and JSX use the same extractors as their base languages
@@ -644,6 +752,28 @@ function extractName(node: SyntaxNode, source: string, extractor: LanguageExtrac
     return getNodeText(nameNode, source);
   }
 
+  // For Dart method_signature, look inside inner signature types
+  if (node.type === 'method_signature') {
+    for (let i = 0; i < node.namedChildCount; i++) {
+      const child = node.namedChild(i);
+      if (child && (
+        child.type === 'function_signature' ||
+        child.type === 'getter_signature' ||
+        child.type === 'setter_signature' ||
+        child.type === 'constructor_signature' ||
+        child.type === 'factory_constructor_signature'
+      )) {
+        // Find identifier inside the inner signature
+        for (let j = 0; j < child.namedChildCount; j++) {
+          const inner = child.namedChild(j);
+          if (inner?.type === 'identifier') {
+            return getNodeText(inner, source);
+          }
+        }
+      }
+    }
+  }
+
   // Fall back to first identifier child
   for (let i = 0; i < node.namedChildCount; i++) {
     const child = node.namedChild(i);
@@ -724,6 +854,7 @@ export class TreeSitterExtractor {
       this.tree = parser.parse(this.source);
       this.visitNode(this.tree.rootNode);
     } catch (error) {
+      captureException(error, { operation: 'tree-sitter-parse', filePath: this.filePath, language: this.language });
       this.errors.push({
         message: `Parse error: ${error instanceof Error ? error.message : String(error)}`,
         severity: 'error',
@@ -773,6 +904,11 @@ export class TreeSitterExtractor {
       }
       skipChildren = true; // extractClass visits body children
     }
+    // Dart-specific: mixin and extension declarations treated as classes
+    else if (this.language === 'dart' && (nodeType === 'mixin_declaration' || nodeType === 'extension_declaration')) {
+      this.extractClass(node);
+      skipChildren = true;
+    }
     // Check for method declarations (only if not already handled by functionTypes)
     else if (this.extractor.methodTypes.includes(nodeType)) {
       this.extractMethod(node);
@@ -793,6 +929,12 @@ export class TreeSitterExtractor {
       this.extractEnum(node);
       skipChildren = true; // extractEnum visits body children
     }
+    // Check for variable declarations (const, let, var, etc.)
+    // Only extract top-level variables (not inside functions/methods)
+    else if (this.extractor.variableTypes.includes(nodeType) && this.nodeStack.length === 0) {
+      this.extractVariable(node);
+      skipChildren = true; // extractVariable handles children
+    }
     // Check for imports
     else if (this.extractor.importTypes.includes(nodeType)) {
       this.extractImport(node);
@@ -912,7 +1054,10 @@ export class TreeSitterExtractor {
 
     // Push to stack and visit body
     this.nodeStack.push(funcNode.id);
-    const body = getChildByField(node, this.extractor.bodyField);
+    // Dart: function_body is a next sibling of function_signature, not a child
+    const body = this.language === 'dart'
+      ? node.nextNamedSibling?.type === 'function_body' ? node.nextNamedSibling : null
+      : getChildByField(node, this.extractor.bodyField);
     if (body) {
       this.visitFunctionBody(body, funcNode.id);
     }
@@ -941,7 +1086,14 @@ export class TreeSitterExtractor {
 
     // Push to stack and visit body
     this.nodeStack.push(classNode.id);
-    const body = getChildByField(node, this.extractor.bodyField) || node;
+    let body = getChildByField(node, this.extractor.bodyField);
+    // Dart: mixin_declaration uses class_body, extension uses extension_body
+    if (!body && this.language === 'dart') {
+      body = node.namedChildren.find((c: SyntaxNode) =>
+        c.type === 'class_body' || c.type === 'extension_body'
+      ) || null;
+    }
+    if (!body) body = node;
 
     // Visit all children for methods and properties
     for (let i = 0; i < body.namedChildCount; i++) {
@@ -984,7 +1136,10 @@ export class TreeSitterExtractor {
 
     // Push to stack and visit body
     this.nodeStack.push(methodNode.id);
-    const body = getChildByField(node, this.extractor.bodyField);
+    // Dart: function_body is a next sibling of method_signature, not a child
+    const body = this.language === 'dart'
+      ? node.nextNamedSibling?.type === 'function_body' ? node.nextNamedSibling : null
+      : getChildByField(node, this.extractor.bodyField);
     if (body) {
       this.visitFunctionBody(body, methodNode.id);
     }
@@ -1058,37 +1213,484 @@ export class TreeSitterExtractor {
     });
   }
 
+  /**
+   * Extract a variable declaration (const, let, var, etc.)
+   *
+   * Extracts top-level and module-level variable declarations.
+   * Captures the variable name and first 100 chars of initializer in signature for searchability.
+   */
+  private extractVariable(node: SyntaxNode): void {
+    if (!this.extractor) return;
+
+    // Different languages have different variable declaration structures
+    // TypeScript/JavaScript: lexical_declaration contains variable_declarator children
+    // Python: assignment has left (identifier) and right (value)
+    // Go: var_declaration, short_var_declaration, const_declaration
+
+    const isConst = this.extractor.isConst?.(node) ?? false;
+    const kind: NodeKind = isConst ? 'constant' : 'variable';
+    const docstring = getPrecedingDocstring(node, this.source);
+    const isExported = this.extractor.isExported?.(node, this.source) ?? false;
+
+    // Extract variable declarators based on language
+    if (this.language === 'typescript' || this.language === 'javascript' ||
+        this.language === 'tsx' || this.language === 'jsx') {
+      // Handle lexical_declaration and variable_declaration
+      // These contain one or more variable_declarator children
+      for (let i = 0; i < node.namedChildCount; i++) {
+        const child = node.namedChild(i);
+        if (child?.type === 'variable_declarator') {
+          const nameNode = getChildByField(child, 'name');
+          const valueNode = getChildByField(child, 'value');
+
+          if (nameNode) {
+            const name = getNodeText(nameNode, this.source);
+            // Skip if it looks like a function (arrow function or function expression)
+            if (valueNode && (valueNode.type === 'arrow_function' || valueNode.type === 'function_expression')) {
+              continue; // Already handled by function extraction
+            }
+
+            // Capture first 100 chars of initializer for context (stored in signature for searchability)
+            const initValue = valueNode ? getNodeText(valueNode, this.source).slice(0, 100) : undefined;
+            const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined;
+
+            this.createNode(kind, name, child, {
+              docstring,
+              signature: initSignature,
+              isExported,
+            });
+          }
+        }
+      }
+    } else if (this.language === 'python' || this.language === 'ruby') {
+      // Python/Ruby assignment: left = right
+      const left = getChildByField(node, 'left') || node.namedChild(0);
+      const right = getChildByField(node, 'right') || node.namedChild(1);
+
+      if (left && left.type === 'identifier') {
+        const name = getNodeText(left, this.source);
+        // Skip if name starts with lowercase and looks like a function call result
+        // Python constants are usually UPPER_CASE
+        const initValue = right ? getNodeText(right, this.source).slice(0, 100) : undefined;
+        const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined;
+
+        this.createNode(kind, name, node, {
+          docstring,
+          signature: initSignature,
+        });
+      }
+    } else if (this.language === 'go') {
+      // Go: var_declaration, short_var_declaration, const_declaration
+      // These can have multiple identifiers on the left
+      const specs = node.namedChildren.filter(c =>
+        c.type === 'var_spec' || c.type === 'const_spec'
+      );
+
+      for (const spec of specs) {
+        const nameNode = spec.namedChild(0);
+        if (nameNode && nameNode.type === 'identifier') {
+          const name = getNodeText(nameNode, this.source);
+          const valueNode = spec.namedChildCount > 1 ? spec.namedChild(spec.namedChildCount - 1) : null;
+          const initValue = valueNode ? getNodeText(valueNode, this.source).slice(0, 100) : undefined;
+          const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined;
+
+          this.createNode(node.type === 'const_declaration' ? 'constant' : 'variable', name, spec, {
+            docstring,
+            signature: initSignature,
+          });
+        }
+      }
+
+      // Handle short_var_declaration (:=)
+      if (node.type === 'short_var_declaration') {
+        const left = getChildByField(node, 'left');
+        const right = getChildByField(node, 'right');
+
+        if (left) {
+          // Can be expression_list with multiple identifiers
+          const identifiers = left.type === 'expression_list'
+            ? left.namedChildren.filter(c => c.type === 'identifier')
+            : [left];
+
+          for (const id of identifiers) {
+            const name = getNodeText(id, this.source);
+            const initValue = right ? getNodeText(right, this.source).slice(0, 100) : undefined;
+            const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined;
+
+            this.createNode('variable', name, node, {
+              docstring,
+              signature: initSignature,
+            });
+          }
+        }
+      }
+    } else {
+      // Generic fallback for other languages
+      // Try to find identifier children
+      for (let i = 0; i < node.namedChildCount; i++) {
+        const child = node.namedChild(i);
+        if (child?.type === 'identifier' || child?.type === 'variable_declarator') {
+          const name = child.type === 'identifier'
+            ? getNodeText(child, this.source)
+            : extractName(child, this.source, this.extractor);
+
+          if (name && name !== '<anonymous>') {
+            this.createNode(kind, name, child, {
+              docstring,
+              isExported,
+            });
+          }
+        }
+      }
+    }
+  }
+
   /**
    * Extract an import
+   *
+   * Creates an import node with the full import statement stored in signature for searchability.
+   * Also creates unresolved references for resolution purposes.
    */
   private extractImport(node: SyntaxNode): void {
-    // Create an edge to track the import
-    // For now, we'll create unresolved references
-    const importText = getNodeText(node, this.source);
+    const importText = getNodeText(node, this.source).trim();
 
     // Extract module/package name based on language
     let moduleName = '';
 
-    if (this.language === 'typescript' || this.language === 'javascript') {
+    if (this.language === 'typescript' || this.language === 'javascript' ||
+        this.language === 'tsx' || this.language === 'jsx') {
       const source = getChildByField(node, 'source');
       if (source) {
         moduleName = getNodeText(source, this.source).replace(/['"]/g, '');
       }
+
+      // Create import node with full statement as signature for searchability
+      if (moduleName) {
+        this.createNode('import', moduleName, node, {
+          signature: importText,
+        });
+      }
     } else if (this.language === 'python') {
-      const module = getChildByField(node, 'module_name') || node.namedChild(0);
-      if (module) {
-        moduleName = getNodeText(module, this.source);
+      // Python has two import forms:
+      // 1. import_statement: import os, sys
+      // 2. import_from_statement: from os import path
+      if (node.type === 'import_from_statement') {
+        const moduleNode = getChildByField(node, 'module_name');
+        if (moduleNode) {
+          moduleName = getNodeText(moduleNode, this.source);
+        }
+      } else {
+        // import_statement - may have multiple modules
+        // Can be dotted_name (import os) or aliased_import (import numpy as np)
+        for (let i = 0; i < node.namedChildCount; i++) {
+          const child = node.namedChild(i);
+          if (child?.type === 'dotted_name') {
+            const name = getNodeText(child, this.source);
+            this.createNode('import', name, node, {
+              signature: importText,
+            });
+          } else if (child?.type === 'aliased_import') {
+            // Extract the module name from inside aliased_import
+            const dottedName = child.namedChildren.find(c => c.type === 'dotted_name');
+            if (dottedName) {
+              const name = getNodeText(dottedName, this.source);
+              this.createNode('import', name, node, {
+                signature: importText,
+              });
+            }
+          }
+        }
+        // Skip creating another node below if we handled import_statement
+        if (node.type === 'import_statement') {
+          return;
+        }
+      }
+
+      if (moduleName) {
+        this.createNode('import', moduleName, node, {
+          signature: importText,
+        });
       }
     } else if (this.language === 'go') {
-      const path = node.namedChild(0);
-      if (path) {
-        moduleName = getNodeText(path, this.source).replace(/['"]/g, '');
+      // Go imports can be single or grouped
+      // Single: import "fmt" - uses import_spec directly as child
+      // Grouped: import ( "fmt" \n "os" ) - uses import_spec_list containing import_spec children
+
+      // Helper function to extract path from import_spec
+      const extractFromSpec = (spec: SyntaxNode): void => {
+        const stringLiteral = spec.namedChildren.find(c => c.type === 'interpreted_string_literal');
+        if (stringLiteral) {
+          const path = getNodeText(stringLiteral, this.source).replace(/['"]/g, '');
+          if (path) {
+            this.createNode('import', path, spec, {
+              signature: getNodeText(spec, this.source).trim(),
+            });
+          }
+        }
+      };
+
+      // Find import_spec_list for grouped imports
+      const importSpecList = node.namedChildren.find(c => c.type === 'import_spec_list');
+
+      if (importSpecList) {
+        // Grouped imports - iterate through import_spec children
+        const importSpecs = importSpecList.namedChildren.filter(c => c.type === 'import_spec');
+        for (const spec of importSpecs) {
+          extractFromSpec(spec);
+        }
+      } else {
+        // Single import: import "fmt" - import_spec is direct child
+        const importSpec = node.namedChildren.find(c => c.type === 'import_spec');
+        if (importSpec) {
+          extractFromSpec(importSpec);
+        }
+      }
+      return; // Go handled completely above
+    } else if (this.language === 'rust') {
+      // Rust use declarations
+      // use std::{ffi::OsStr, io}; -> scoped_use_list with identifier "std"
+      // use crate::error::Error;  -> scoped_identifier starting with "crate"
+      // use super::utils;         -> scoped_identifier starting with "super"
+
+      // Helper to get the root crate/module from a scoped path
+      const getRootModule = (scopedNode: SyntaxNode): string => {
+        // Recursively find the leftmost identifier/crate/super/self
+        const firstChild = scopedNode.namedChild(0);
+        if (!firstChild) return getNodeText(scopedNode, this.source);
+
+        if (firstChild.type === 'identifier' ||
+            firstChild.type === 'crate' ||
+            firstChild.type === 'super' ||
+            firstChild.type === 'self') {
+          return getNodeText(firstChild, this.source);
+        } else if (firstChild.type === 'scoped_identifier') {
+          return getRootModule(firstChild);
+        }
+        return getNodeText(firstChild, this.source);
+      };
+
+      // Find the use argument (scoped_use_list or scoped_identifier)
+      const useArg = node.namedChildren.find(c =>
+        c.type === 'scoped_use_list' ||
+        c.type === 'scoped_identifier' ||
+        c.type === 'use_list' ||
+        c.type === 'identifier'
+      );
+
+      if (useArg) {
+        moduleName = getRootModule(useArg);
+        this.createNode('import', moduleName, node, {
+          signature: importText,
+        });
+      }
+      return; // Rust handled completely above
+    } else if (this.language === 'swift') {
+      // Swift imports: import Foundation, @testable import Alamofire
+      // AST structure: import_declaration -> identifier -> simple_identifier
+      const identifier = node.namedChildren.find(c => c.type === 'identifier');
+      if (identifier) {
+        moduleName = getNodeText(identifier, this.source);
+        this.createNode('import', moduleName, node, {
+          signature: importText,
+        });
+      }
+      return; // Swift handled completely above
+    } else if (this.language === 'kotlin') {
+      // Kotlin imports: import java.io.IOException, import x.y.Z as Alias, import x.y.*
+      // AST structure: import_header -> identifier (dotted path)
+      const identifier = node.namedChildren.find(c => c.type === 'identifier');
+      if (identifier) {
+        moduleName = getNodeText(identifier, this.source);
+        this.createNode('import', moduleName, node, {
+          signature: importText,
+        });
+      }
+      return; // Kotlin handled completely above
+    } else if (this.language === 'java') {
+      // Java imports: import java.util.List, import static x.Y.method, import x.y.*
+      // AST structure: import_declaration -> scoped_identifier (dotted path)
+      const scopedId = node.namedChildren.find(c => c.type === 'scoped_identifier');
+      if (scopedId) {
+        moduleName = getNodeText(scopedId, this.source);
+        this.createNode('import', moduleName, node, {
+          signature: importText,
+        });
+      }
+      return; // Java handled completely above
+    } else if (this.language === 'csharp') {
+      // C# using directives: using System, using System.Collections.Generic, using static X, using Alias = X
+      // AST structure: using_directive -> qualified_name (dotted) or identifier (simple)
+      // For alias imports: identifier = qualified_name - we want the qualified_name
+      const qualifiedName = node.namedChildren.find(c => c.type === 'qualified_name');
+      if (qualifiedName) {
+        moduleName = getNodeText(qualifiedName, this.source);
+      } else {
+        // Simple namespace like "using System;" - get the first identifier
+        const identifier = node.namedChildren.find(c => c.type === 'identifier');
+        if (identifier) {
+          moduleName = getNodeText(identifier, this.source);
+        }
+      }
+      if (moduleName) {
+        this.createNode('import', moduleName, node, {
+          signature: importText,
+        });
+      }
+      return; // C# handled completely above
+    } else if (this.language === 'php') {
+      // PHP use declarations: use X\Y\Z, use X as Y, use function X\func, use X\{A, B}
+      // AST structure: namespace_use_declaration -> namespace_use_clause -> qualified_name or name
+
+      // Check for grouped imports first: use X\{A, B}
+      const namespacePrefix = node.namedChildren.find(c => c.type === 'namespace_name');
+      const useGroup = node.namedChildren.find(c => c.type === 'namespace_use_group');
+
+      if (namespacePrefix && useGroup) {
+        // Grouped import - create one import per item
+        const prefix = getNodeText(namespacePrefix, this.source);
+        const useClauses = useGroup.namedChildren.filter((c: SyntaxNode) => c.type === 'namespace_use_clause');
+        for (const clause of useClauses) {
+          const name = clause.namedChildren.find((c: SyntaxNode) => c.type === 'name');
+          if (name) {
+            const fullPath = `${prefix}\\${getNodeText(name, this.source)}`;
+            this.createNode('import', fullPath, node, {
+              signature: importText,
+            });
+          }
+        }
+        return;
+      }
+
+      // Single import - find namespace_use_clause
+      const useClause = node.namedChildren.find(c => c.type === 'namespace_use_clause');
+      if (useClause) {
+        // Look for qualified_name (full path) or name (simple)
+        const qualifiedName = useClause.namedChildren.find((c: SyntaxNode) => c.type === 'qualified_name');
+        if (qualifiedName) {
+          moduleName = getNodeText(qualifiedName, this.source);
+        } else {
+          const name = useClause.namedChildren.find((c: SyntaxNode) => c.type === 'name');
+          if (name) {
+            moduleName = getNodeText(name, this.source);
+          }
+        }
+      }
+
+      if (moduleName) {
+        this.createNode('import', moduleName, node, {
+          signature: importText,
+        });
+      }
+      return; // PHP handled completely above
+    } else if (this.language === 'ruby') {
+      // Ruby imports: require 'json', require_relative '../helper'
+      // AST structure: call -> identifier (require/require_relative) + argument_list -> string -> string_content
+
+      // Check if this is a require/require_relative call
+      const identifier = node.namedChildren.find(c => c.type === 'identifier');
+      if (!identifier) return;
+      const methodName = getNodeText(identifier, this.source);
+      if (methodName !== 'require' && methodName !== 'require_relative') {
+        return; // Not an import, skip
+      }
+
+      // Find the argument (string)
+      const argList = node.namedChildren.find(c => c.type === 'argument_list');
+      if (argList) {
+        const stringNode = argList.namedChildren.find((c: SyntaxNode) => c.type === 'string');
+        if (stringNode) {
+          // Get string_content (without quotes)
+          const stringContent = stringNode.namedChildren.find((c: SyntaxNode) => c.type === 'string_content');
+          if (stringContent) {
+            moduleName = getNodeText(stringContent, this.source);
+          }
+        }
+      }
+
+      if (moduleName) {
+        this.createNode('import', moduleName, node, {
+          signature: importText,
+        });
+      }
+      return; // Ruby handled completely above
+    } else if (this.language === 'dart') {
+      // Dart imports: import 'dart:async'; import 'package:foo/bar.dart' as bar;
+      // AST: import_or_export -> library_import -> import_specification -> configurable_uri -> uri -> string_literal
+      const libraryImport = node.namedChildren.find(c => c.type === 'library_import');
+      if (libraryImport) {
+        const importSpec = libraryImport.namedChildren.find((c: SyntaxNode) => c.type === 'import_specification');
+        if (importSpec) {
+          const configurableUri = importSpec.namedChildren.find((c: SyntaxNode) => c.type === 'configurable_uri');
+          if (configurableUri) {
+            const uri = configurableUri.namedChildren.find((c: SyntaxNode) => c.type === 'uri');
+            if (uri) {
+              const stringLiteral = uri.namedChildren.find((c: SyntaxNode) => c.type === 'string_literal');
+              if (stringLiteral) {
+                moduleName = getNodeText(stringLiteral, this.source).replace(/['"]/g, '');
+              }
+            }
+          }
+        }
+      }
+      // Also handle exports: export 'src/foo.dart';
+      const libraryExport = node.namedChildren.find(c => c.type === 'library_export');
+      if (libraryExport) {
+        const configurableUri = libraryExport.namedChildren.find((c: SyntaxNode) => c.type === 'configurable_uri');
+        if (configurableUri) {
+          const uri = configurableUri.namedChildren.find((c: SyntaxNode) => c.type === 'uri');
+          if (uri) {
+            const stringLiteral = uri.namedChildren.find((c: SyntaxNode) => c.type === 'string_literal');
+            if (stringLiteral) {
+              moduleName = getNodeText(stringLiteral, this.source).replace(/['"]/g, '');
+            }
+          }
+        }
+      }
+
+      if (moduleName) {
+        this.createNode('import', moduleName, node, {
+          signature: importText,
+        });
+      }
+      return; // Dart handled completely above
+    } else if (this.language === 'c' || this.language === 'cpp') {
+      // C/C++ includes: #include <iostream>, #include "myheader.h"
+      // AST: preproc_include -> system_lib_string (<...>) or string_literal ("...")
+
+      // Check for system include: <path>
+      const systemLib = node.namedChildren.find(c => c.type === 'system_lib_string');
+      if (systemLib) {
+        // Remove angle brackets: <iostream> -> iostream
+        moduleName = getNodeText(systemLib, this.source).replace(/^<|>$/g, '');
+      } else {
+        // Check for local include: "path"
+        const stringLiteral = node.namedChildren.find(c => c.type === 'string_literal');
+        if (stringLiteral) {
+          const stringContent = stringLiteral.namedChildren.find((c: SyntaxNode) => c.type === 'string_content');
+          if (stringContent) {
+            moduleName = getNodeText(stringContent, this.source);
+          }
+        }
       }
+
+      if (moduleName) {
+        this.createNode('import', moduleName, node, {
+          signature: importText,
+        });
+      }
+      return; // C/C++ handled completely above
     } else {
-      // Generic extraction
+      // Generic extraction for other languages
       moduleName = importText;
+      if (moduleName) {
+        this.createNode('import', moduleName, node, {
+          signature: importText,
+        });
+      }
     }
 
+    // Keep unresolved reference creation for resolution purposes
+    // This is used to resolve imports to their target files/modules
     if (moduleName && this.nodeStack.length > 0) {
       const parentId = this.nodeStack[this.nodeStack.length - 1];
       if (parentId) {
@@ -1195,7 +1797,8 @@ export class TreeSitterExtractor {
 
       if (
         child.type === 'implements_clause' ||
-        child.type === 'class_interface_clause'
+        child.type === 'class_interface_clause' ||
+        child.type === 'interfaces' // Dart
       ) {
         // Extract implemented interfaces
         for (let j = 0; j < child.namedChildCount; j++) {
@@ -1260,6 +1863,7 @@ export class LiquidExtractor {
       // Extract assign statements as variables
       this.extractAssignments(fileNode.id);
     } catch (error) {
+      captureException(error, { operation: 'liquid-extraction', filePath: this.filePath });
       this.errors.push({
         message: `Liquid extraction error: ${error instanceof Error ? error.message : String(error)}`,
         severity: 'error',
@@ -1309,9 +1913,34 @@ export class LiquidExtractor {
     let match;
 
     while ((match = renderRegex.exec(this.source)) !== null) {
-      const [, tagType, snippetName] = match;
+      const [fullMatch, tagType, snippetName] = match;
       const line = this.getLineNumber(match.index);
 
+      // Create an import node for searchability
+      const importNodeId = generateNodeId(this.filePath, 'import', snippetName!, line);
+      const importNode: Node = {
+        id: importNodeId,
+        kind: 'import',
+        name: snippetName!,
+        qualifiedName: `${this.filePath}::import:${snippetName}`,
+        filePath: this.filePath,
+        language: 'liquid',
+        signature: fullMatch,
+        startLine: line,
+        endLine: line,
+        startColumn: match.index - this.getLineStart(line),
+        endColumn: match.index - this.getLineStart(line) + fullMatch.length,
+        updatedAt: Date.now(),
+      };
+      this.nodes.push(importNode);
+
+      // Add containment edge from file to import
+      this.edges.push({
+        source: fileNodeId,
+        target: importNodeId,
+        kind: 'contains',
+      });
+
       // Create a component node for the snippet reference
       const nodeId = generateNodeId(this.filePath, 'component', `${tagType}:${snippetName}`, line);
 
@@ -1325,7 +1954,7 @@ export class LiquidExtractor {
         startLine: line,
         endLine: line,
         startColumn: match.index - this.getLineStart(line),
-        endColumn: match.index - this.getLineStart(line) + match[0].length,
+        endColumn: match.index - this.getLineStart(line) + fullMatch.length,
         updatedAt: Date.now(),
       };
 
@@ -1358,9 +1987,34 @@ export class LiquidExtractor {
     let match;
 
     while ((match = sectionRegex.exec(this.source)) !== null) {
-      const [, sectionName] = match;
+      const [fullMatch, sectionName] = match;
       const line = this.getLineNumber(match.index);
 
+      // Create an import node for searchability
+      const importNodeId = generateNodeId(this.filePath, 'import', sectionName!, line);
+      const importNode: Node = {
+        id: importNodeId,
+        kind: 'import',
+        name: sectionName!,
+        qualifiedName: `${this.filePath}::import:${sectionName}`,
+        filePath: this.filePath,
+        language: 'liquid',
+        signature: fullMatch,
+        startLine: line,
+        endLine: line,
+        startColumn: match.index - this.getLineStart(line),
+        endColumn: match.index - this.getLineStart(line) + fullMatch.length,
+        updatedAt: Date.now(),
+      };
+      this.nodes.push(importNode);
+
+      // Add containment edge from file to import
+      this.edges.push({
+        source: fileNodeId,
+        target: importNodeId,
+        kind: 'contains',
+      });
+
       // Create a component node for the section reference
       const nodeId = generateNodeId(this.filePath, 'component', `section:${sectionName}`, line);
 
@@ -1374,7 +2028,7 @@ export class LiquidExtractor {
         startLine: line,
         endLine: line,
         startColumn: match.index - this.getLineStart(line),
-        endColumn: match.index - this.getLineStart(line) + match[0].length,
+        endColumn: match.index - this.getLineStart(line) + fullMatch.length,
         updatedAt: Date.now(),
       };
 

+ 45 - 65
src/index.ts

@@ -47,19 +47,22 @@ import {
 import { GraphTraverser, GraphQueryManager } from './graph';
 import { VectorManager, createVectorManager, EmbeddingProgress } from './vectors';
 import { ContextBuilder, createContextBuilder } from './context';
-import { GitHooksManager, createGitHooksManager, HookInstallResult, HookRemoveResult } from './sync';
 import { Mutex } from './utils';
 
 // Re-export types for consumers
 export * from './types';
 export { getDatabasePath } from './db';
 export { getConfigPath } from './config';
-export { getCodeGraphDir, isInitialized } from './directory';
+export {
+  getCodeGraphDir,
+  isInitialized,
+  findNearestCodeGraphRoot,
+  CODEGRAPH_DIR,
+} from './directory';
 export { IndexProgress, IndexResult, SyncResult } from './extraction';
 export { detectLanguage, isLanguageSupported, getSupportedLanguages } from './extraction';
 export { ResolutionResult } from './resolution';
 export { EmbeddingProgress } from './vectors';
-export { HookInstallResult, HookRemoveResult } from './sync';
 export {
   CodeGraphError,
   FileError,
@@ -129,7 +132,6 @@ export class CodeGraph {
   private traverser: GraphTraverser;
   private vectorManager: VectorManager | null = null;
   private contextBuilder: ContextBuilder;
-  private gitHooksManager: GitHooksManager;
 
   // Mutex for preventing concurrent indexing operations
   private indexMutex = new Mutex();
@@ -149,12 +151,9 @@ export class CodeGraph {
     this.graphManager = new GraphQueryManager(queries);
     this.traverser = new GraphTraverser(queries);
     // Vector manager is created lazily when embeddings are enabled
+    // Uses global ~/.codegraph/models directory for shared embedding models
     if (config.enableEmbeddings) {
-      this.vectorManager = createVectorManager(db.getDb(), queries, {
-        embedder: {
-          cacheDir: path.join(projectRoot, '.codegraph', 'models'),
-        },
-      });
+      this.vectorManager = createVectorManager(db.getDb(), queries, {});
     }
     // Context builder (uses vector manager if available)
     this.contextBuilder = createContextBuilder(
@@ -163,8 +162,6 @@ export class CodeGraph {
       this.traverser,
       this.vectorManager
     );
-    // Git hooks manager
-    this.gitHooksManager = createGitHooksManager(projectRoot);
   }
 
   // ===========================================================================
@@ -174,7 +171,7 @@ export class CodeGraph {
   /**
    * Initialize a new CodeGraph project
    *
-   * Creates the .codegraph directory, database, and configuration.
+   * Creates the .CodeGraph directory, database, and configuration.
    *
    * @param projectRoot - Path to the project root directory
    * @param options - Initialization options
@@ -320,6 +317,11 @@ export class CodeGraph {
    * Close the CodeGraph instance and release resources
    */
   close(): void {
+    // Dispose vector manager first to release ONNX workers
+    if (this.vectorManager) {
+      this.vectorManager.dispose();
+      this.vectorManager = null;
+    }
     this.db.close();
   }
 
@@ -371,12 +373,22 @@ export class CodeGraph {
 
       // Resolve references to create call/import/extends edges
       if (result.success && result.filesIndexed > 0) {
+        // Get count of unresolved references for accurate progress
+        const unresolvedCount = this.queries.getUnresolvedReferences().length;
+
         options.onProgress?.({
           phase: 'resolving',
           current: 0,
-          total: 1,
+          total: unresolvedCount,
+        });
+
+        this.resolveReferences((current, total) => {
+          options.onProgress?.({
+            phase: 'resolving',
+            current,
+            total,
+          });
         });
-        this.resolveReferences();
       }
 
       return result;
@@ -405,7 +417,21 @@ export class CodeGraph {
 
       // Resolve references if files were updated
       if (result.filesAdded > 0 || result.filesModified > 0) {
-        this.resolveReferences();
+        const unresolvedCount = this.queries.getUnresolvedReferences().length;
+
+        options.onProgress?.({
+          phase: 'resolving',
+          current: 0,
+          total: unresolvedCount,
+        });
+
+        this.resolveReferences((current, total) => {
+          options.onProgress?.({
+            phase: 'resolving',
+            current,
+            total,
+          });
+        });
       }
 
       return result;
@@ -446,10 +472,10 @@ export class CodeGraph {
    * - Import-based resolution
    * - Name-based symbol matching
    */
-  resolveReferences(): ResolutionResult {
+  resolveReferences(onProgress?: (current: number, total: number) => void): ResolutionResult {
     // Get all unresolved references from the database
     const unresolvedRefs = this.queries.getUnresolvedReferences();
-    return this.resolver.resolveAndPersist(unresolvedRefs);
+    return this.resolver.resolveAndPersist(unresolvedRefs, onProgress);
   }
 
   /**
@@ -909,52 +935,6 @@ export class CodeGraph {
     return this.contextBuilder.buildContext(input, options);
   }
 
-  // ===========================================================================
-  // Git Integration
-  // ===========================================================================
-
-  /**
-   * Check if the project is a git repository
-   */
-  isGitRepository(): boolean {
-    return this.gitHooksManager.isGitRepository();
-  }
-
-  /**
-   * Check if the CodeGraph git hook is installed
-   */
-  isGitHookInstalled(): boolean {
-    return this.gitHooksManager.isHookInstalled();
-  }
-
-  /**
-   * Install git hooks for automatic incremental indexing
-   *
-   * Installs a post-commit hook that automatically runs `codegraph sync`
-   * after each commit to keep the graph up-to-date.
-   *
-   * If a post-commit hook already exists:
-   * - If it's a CodeGraph hook, it will be updated
-   * - If it's a user hook, it will be backed up before installing
-   *
-   * @returns Result indicating success/failure and any messages
-   */
-  installGitHooks(): HookInstallResult {
-    return this.gitHooksManager.installHook();
-  }
-
-  /**
-   * Remove CodeGraph git hooks
-   *
-   * Removes the CodeGraph post-commit hook. If a backup of a previous
-   * user hook exists, it will be restored.
-   *
-   * @returns Result indicating success/failure and any messages
-   */
-  removeGitHooks(): HookRemoveResult {
-    return this.gitHooksManager.removeHook();
-  }
-
   // ===========================================================================
   // Database Management
   // ===========================================================================
@@ -983,12 +963,12 @@ export class CodeGraph {
 
   /**
    * Completely remove CodeGraph from the project.
-   * This closes the database and deletes the .codegraph directory.
+   * This closes the database and deletes the .CodeGraph directory.
    *
    * WARNING: This permanently deletes all CodeGraph data for the project.
    */
   uninitialize(): void {
-    this.db.close();
+    this.close();
     removeDirectory(this.projectRoot);
   }
 }

+ 0 - 8
src/installer/index.ts

@@ -151,14 +151,6 @@ async function initializeLocalProject(): Promise<void> {
     success(`Indexed ${formatNumber(result.filesIndexed)} files with ${result.errors.length} warnings`);
   }
 
-  // Install git hooks if this is a git repository
-  if (cg.isGitRepository()) {
-    const hookResult = cg.installGitHooks();
-    if (hookResult.success) {
-      success('Installed git post-commit hook');
-    }
-  }
-
   cg.close();
 }
 

+ 20 - 4
src/mcp/index.ts

@@ -15,9 +15,12 @@
  * ```
  */
 
-import CodeGraph from '../index';
+import CodeGraph, { findNearestCodeGraphRoot } from '../index';
 import { StdioTransport, JsonRpcRequest, JsonRpcNotification, ErrorCodes } from './transport';
 import { tools, ToolHandler } from './tools';
+import { initSentry, captureException } from '../sentry';
+
+initSentry({ processName: 'codegraph-mcp' });
 
 /**
  * MCP Server Info
@@ -68,20 +71,28 @@ export class MCPServer {
 
   /**
    * Initialize CodeGraph for the project
+   *
+   * Walks up parent directories to find the nearest .codegraph/ folder,
+   * similar to how git finds .git/ directories.
    */
   private async initializeCodeGraph(projectPath: string): Promise<void> {
-    this.projectPath = projectPath;
+    // Walk up parent directories to find nearest .codegraph/
+    const resolvedRoot = findNearestCodeGraphRoot(projectPath);
 
-    if (!CodeGraph.isInitialized(projectPath)) {
+    if (!resolvedRoot) {
+      this.projectPath = projectPath;
       this.initError = `CodeGraph not initialized in ${projectPath}. Run 'codegraph init' first.`;
       return;
     }
 
+    this.projectPath = resolvedRoot;
+
     try {
-      this.cg = await CodeGraph.open(projectPath);
+      this.cg = await CodeGraph.open(resolvedRoot);
       this.toolHandler = new ToolHandler(this.cg);
       this.initError = null;
     } catch (err) {
+      captureException(err);
       this.initError = `Failed to open CodeGraph: ${err instanceof Error ? err.message : String(err)}`;
     }
   }
@@ -90,6 +101,11 @@ export class MCPServer {
    * Stop the server
    */
   stop(): void {
+    // Close all cached cross-project connections first
+    if (this.toolHandler) {
+      this.toolHandler.closeAll();
+    }
+    // Close the main CodeGraph instance
     if (this.cg) {
       this.cg.close();
       this.cg = null;

+ 347 - 15
src/mcp/tools.ts

@@ -4,8 +4,26 @@
  * Defines the tools exposed by the CodeGraph MCP server.
  */
 
-import CodeGraph from '../index';
+import CodeGraph, { findNearestCodeGraphRoot } from '../index';
 import type { Node, SearchResult, Subgraph, TaskContext, NodeKind } from '../types';
+import { createHash } from 'crypto';
+import { writeFileSync } from 'fs';
+import { tmpdir } from 'os';
+import { join } from 'path';
+
+/**
+ * Mark a Claude session as having consulted MCP tools.
+ * This enables Grep/Glob/Bash commands that would otherwise be blocked.
+ */
+function markSessionConsulted(sessionId: string): void {
+  try {
+    const hash = createHash('md5').update(sessionId).digest('hex').slice(0, 16);
+    const markerPath = join(tmpdir(), `codegraph-consulted-${hash}`);
+    writeFileSync(markerPath, new Date().toISOString(), 'utf8');
+  } catch {
+    // Silently fail - don't break MCP on marker write failure
+  }
+}
 
 /**
  * MCP Tool definition
@@ -38,11 +56,21 @@ export interface ToolResult {
   isError?: boolean;
 }
 
+/**
+ * Common projectPath property for cross-project queries
+ */
+const projectPathProperty: PropertySchema = {
+  type: 'string',
+  description: 'Path to a different project with .codegraph/ initialized. If omitted, uses current project. Use this to query other codebases.',
+};
+
 /**
  * All CodeGraph MCP tools
  *
  * Designed for minimal context usage - use codegraph_context as the primary tool,
  * and only use other tools for targeted follow-up queries.
+ *
+ * All tools support cross-project queries via the optional `projectPath` parameter.
  */
 export const tools: ToolDefinition[] = [
   {
@@ -65,6 +93,7 @@ export const tools: ToolDefinition[] = [
           description: 'Maximum results (default: 10)',
           default: 10,
         },
+        projectPath: projectPathProperty,
       },
       required: ['query'],
     },
@@ -89,6 +118,7 @@ export const tools: ToolDefinition[] = [
           description: 'Include code snippets for key symbols (default: true)',
           default: true,
         },
+        projectPath: projectPathProperty,
       },
       required: ['task'],
     },
@@ -108,6 +138,7 @@ export const tools: ToolDefinition[] = [
           description: 'Maximum number of callers to return (default: 20)',
           default: 20,
         },
+        projectPath: projectPathProperty,
       },
       required: ['symbol'],
     },
@@ -127,6 +158,7 @@ export const tools: ToolDefinition[] = [
           description: 'Maximum number of callees to return (default: 20)',
           default: 20,
         },
+        projectPath: projectPathProperty,
       },
       required: ['symbol'],
     },
@@ -146,6 +178,7 @@ export const tools: ToolDefinition[] = [
           description: 'How many levels of dependencies to traverse (default: 2)',
           default: 2,
         },
+        projectPath: projectPathProperty,
       },
       required: ['symbol'],
     },
@@ -165,6 +198,7 @@ export const tools: ToolDefinition[] = [
           description: 'Include full source code (default: false to minimize context)',
           default: false,
         },
+        projectPath: projectPathProperty,
       },
       required: ['symbol'],
     },
@@ -174,17 +208,111 @@ export const tools: ToolDefinition[] = [
     description: 'Get the status of the CodeGraph index, including statistics about indexed files, nodes, and edges.',
     inputSchema: {
       type: 'object',
-      properties: {},
+      properties: {
+        projectPath: projectPathProperty,
+      },
+    },
+  },
+  {
+    name: 'codegraph_files',
+    description: 'REQUIRED for file/folder exploration. Get the project file structure from the CodeGraph index. Returns a tree view of all indexed files with metadata (language, symbol count). Much faster than Glob/filesystem scanning. Use this FIRST when exploring project structure, finding files, or understanding codebase organization.',
+    inputSchema: {
+      type: 'object',
+      properties: {
+        path: {
+          type: 'string',
+          description: 'Filter to files under this directory path (e.g., "src/components"). Returns all files if not specified.',
+        },
+        pattern: {
+          type: 'string',
+          description: 'Filter files matching this glob pattern (e.g., "*.tsx", "**/*.test.ts")',
+        },
+        format: {
+          type: 'string',
+          description: 'Output format: "tree" (hierarchical, default), "flat" (simple list), "grouped" (by language)',
+          enum: ['tree', 'flat', 'grouped'],
+          default: 'tree',
+        },
+        includeMetadata: {
+          type: 'boolean',
+          description: 'Include file metadata like language and symbol count (default: true)',
+          default: true,
+        },
+        maxDepth: {
+          type: 'number',
+          description: 'Maximum directory depth to show (default: unlimited)',
+        },
+        projectPath: projectPathProperty,
+      },
     },
   },
 ];
 
 /**
  * Tool handler that executes tools against a CodeGraph instance
+ *
+ * Supports cross-project queries via the projectPath parameter.
+ * Other projects are opened on-demand and cached for performance.
  */
 export class ToolHandler {
+  // Cache of opened CodeGraph instances for cross-project queries
+  private projectCache: Map<string, CodeGraph> = new Map();
+
   constructor(private cg: CodeGraph) {}
 
+  /**
+   * Get CodeGraph instance for a project
+   *
+   * If projectPath is provided, opens that project's CodeGraph (cached).
+   * Otherwise returns the default CodeGraph instance.
+   *
+   * Walks up parent directories to find the nearest .codegraph/ folder,
+   * similar to how git finds .git/ directories.
+   */
+  private getCodeGraph(projectPath?: string): CodeGraph {
+    if (!projectPath) {
+      return this.cg;
+    }
+
+    // Check cache first (using original path as key)
+    if (this.projectCache.has(projectPath)) {
+      return this.projectCache.get(projectPath)!;
+    }
+
+    // Walk up parent directories to find nearest .codegraph/
+    const resolvedRoot = findNearestCodeGraphRoot(projectPath);
+
+    if (!resolvedRoot) {
+      throw new Error(`CodeGraph not initialized in ${projectPath}. Run 'codegraph init' in that project first.`);
+    }
+
+    // Check if we already have this resolved root cached (different path, same project)
+    if (this.projectCache.has(resolvedRoot)) {
+      const cg = this.projectCache.get(resolvedRoot)!;
+      // Cache under original path too for faster future lookups
+      this.projectCache.set(projectPath, cg);
+      return cg;
+    }
+
+    // Open and cache under both paths
+    const cg = CodeGraph.openSync(resolvedRoot);
+    this.projectCache.set(resolvedRoot, cg);
+    if (projectPath !== resolvedRoot) {
+      this.projectCache.set(projectPath, cg);
+    }
+    return cg;
+  }
+
+  /**
+   * Close all cached project connections
+   */
+  closeAll(): void {
+    for (const cg of this.projectCache.values()) {
+      cg.close();
+    }
+    this.projectCache.clear();
+  }
+
   /**
    * Execute a tool by name
    */
@@ -204,11 +332,14 @@ export class ToolHandler {
         case 'codegraph_node':
           return await this.handleNode(args);
         case 'codegraph_status':
-          return await this.handleStatus();
+          return await this.handleStatus(args);
+        case 'codegraph_files':
+          return await this.handleFiles(args);
         default:
           return this.errorResult(`Unknown tool: ${toolName}`);
       }
     } catch (err) {
+      try { const { captureException } = require('../sentry'); captureException(err, { tool: toolName }); } catch {}
       return this.errorResult(`Tool execution failed: ${err instanceof Error ? err.message : String(err)}`);
     }
   }
@@ -217,11 +348,12 @@ export class ToolHandler {
    * Handle codegraph_search
    */
   private async handleSearch(args: Record<string, unknown>): Promise<ToolResult> {
+    const cg = this.getCodeGraph(args.projectPath as string | undefined);
     const query = args.query as string;
     const kind = args.kind as string | undefined;
     const limit = (args.limit as number) || 10;
 
-    const results = this.cg.searchNodes(query, {
+    const results = cg.searchNodes(query, {
       limit,
       kinds: kind ? [kind as NodeKind] : undefined,
     });
@@ -238,11 +370,18 @@ export class ToolHandler {
    * Handle codegraph_context
    */
   private async handleContext(args: Record<string, unknown>): Promise<ToolResult> {
+    // Mark session as consulted (enables Grep/Glob/Bash)
+    const sessionId = process.env.CLAUDE_SESSION_ID;
+    if (sessionId) {
+      markSessionConsulted(sessionId);
+    }
+
+    const cg = this.getCodeGraph(args.projectPath as string | undefined);
     const task = args.task as string;
     const maxNodes = (args.maxNodes as number) || 20;
     const includeCode = args.includeCode !== false;
 
-    const context = await this.cg.buildContext(task, {
+    const context = await cg.buildContext(task, {
       maxNodes,
       includeCode,
       format: 'markdown',
@@ -295,17 +434,18 @@ export class ToolHandler {
    * Handle codegraph_callers
    */
   private async handleCallers(args: Record<string, unknown>): Promise<ToolResult> {
+    const cg = this.getCodeGraph(args.projectPath as string | undefined);
     const symbol = args.symbol as string;
     const limit = (args.limit as number) || 20;
 
     // First find the node by name
-    const results = this.cg.searchNodes(symbol, { limit: 1 });
+    const results = cg.searchNodes(symbol, { limit: 1 });
     if (results.length === 0 || !results[0]) {
       return this.textResult(`Symbol "${symbol}" not found in the codebase`);
     }
 
     const node = results[0].node;
-    const callers = this.cg.getCallers(node.id);
+    const callers = cg.getCallers(node.id);
 
     if (callers.length === 0) {
       return this.textResult(`No callers found for "${symbol}"`);
@@ -321,17 +461,18 @@ export class ToolHandler {
    * Handle codegraph_callees
    */
   private async handleCallees(args: Record<string, unknown>): Promise<ToolResult> {
+    const cg = this.getCodeGraph(args.projectPath as string | undefined);
     const symbol = args.symbol as string;
     const limit = (args.limit as number) || 20;
 
     // First find the node by name
-    const results = this.cg.searchNodes(symbol, { limit: 1 });
+    const results = cg.searchNodes(symbol, { limit: 1 });
     if (results.length === 0 || !results[0]) {
       return this.textResult(`Symbol "${symbol}" not found in the codebase`);
     }
 
     const node = results[0].node;
-    const callees = this.cg.getCallees(node.id);
+    const callees = cg.getCallees(node.id);
 
     if (callees.length === 0) {
       return this.textResult(`No callees found for "${symbol}"`);
@@ -347,17 +488,18 @@ export class ToolHandler {
    * Handle codegraph_impact
    */
   private async handleImpact(args: Record<string, unknown>): Promise<ToolResult> {
+    const cg = this.getCodeGraph(args.projectPath as string | undefined);
     const symbol = args.symbol as string;
     const depth = (args.depth as number) || 2;
 
     // First find the node by name
-    const results = this.cg.searchNodes(symbol, { limit: 1 });
+    const results = cg.searchNodes(symbol, { limit: 1 });
     if (results.length === 0 || !results[0]) {
       return this.textResult(`Symbol "${symbol}" not found in the codebase`);
     }
 
     const node = results[0].node;
-    const impact = this.cg.getImpactRadius(node.id, depth);
+    const impact = cg.getImpactRadius(node.id, depth);
 
     const formatted = this.formatImpact(symbol, impact);
     return this.textResult(formatted);
@@ -367,12 +509,13 @@ export class ToolHandler {
    * Handle codegraph_node
    */
   private async handleNode(args: Record<string, unknown>): Promise<ToolResult> {
+    const cg = this.getCodeGraph(args.projectPath as string | undefined);
     const symbol = args.symbol as string;
     // Default to false to minimize context usage
     const includeCode = args.includeCode === true;
 
     // Find the node by name
-    const results = this.cg.searchNodes(symbol, { limit: 1 });
+    const results = cg.searchNodes(symbol, { limit: 1 });
     if (results.length === 0 || !results[0]) {
       return this.textResult(`Symbol "${symbol}" not found in the codebase`);
     }
@@ -381,7 +524,7 @@ export class ToolHandler {
     let code: string | null = null;
 
     if (includeCode) {
-      code = await this.cg.getCode(node.id);
+      code = await cg.getCode(node.id);
     }
 
     const formatted = this.formatNodeDetails(node, code);
@@ -391,8 +534,9 @@ export class ToolHandler {
   /**
    * Handle codegraph_status
    */
-  private async handleStatus(): Promise<ToolResult> {
-    const stats = this.cg.getStats();
+  private async handleStatus(args: Record<string, unknown>): Promise<ToolResult> {
+    const cg = this.getCodeGraph(args.projectPath as string | undefined);
+    const stats = cg.getStats();
 
     const lines: string[] = [
       '## CodeGraph Status',
@@ -421,6 +565,194 @@ export class ToolHandler {
     return this.textResult(lines.join('\n'));
   }
 
+  /**
+   * Handle codegraph_files - get project file structure from the index
+   */
+  private async handleFiles(args: Record<string, unknown>): Promise<ToolResult> {
+    const cg = this.getCodeGraph(args.projectPath as string | undefined);
+    const pathFilter = args.path as string | undefined;
+    const pattern = args.pattern as string | undefined;
+    const format = (args.format as 'tree' | 'flat' | 'grouped') || 'tree';
+    const includeMetadata = args.includeMetadata !== false;
+    const maxDepth = args.maxDepth as number | undefined;
+
+    // Get all files from the index
+    const allFiles = cg.getFiles();
+
+    if (allFiles.length === 0) {
+      return this.textResult('No files indexed. Run `codegraph index` first.');
+    }
+
+    // Filter by path prefix
+    let files = pathFilter
+      ? allFiles.filter(f => f.path.startsWith(pathFilter) || f.path.startsWith('./' + pathFilter))
+      : allFiles;
+
+    // Filter by glob pattern
+    if (pattern) {
+      const regex = this.globToRegex(pattern);
+      files = files.filter(f => regex.test(f.path));
+    }
+
+    if (files.length === 0) {
+      return this.textResult(`No files found matching the criteria.`);
+    }
+
+    // Format output
+    let output: string;
+    switch (format) {
+      case 'flat':
+        output = this.formatFilesFlat(files, includeMetadata);
+        break;
+      case 'grouped':
+        output = this.formatFilesGrouped(files, includeMetadata);
+        break;
+      case 'tree':
+      default:
+        output = this.formatFilesTree(files, includeMetadata, maxDepth);
+        break;
+    }
+
+    return this.textResult(output);
+  }
+
+  /**
+   * Convert glob pattern to regex
+   */
+  private globToRegex(pattern: string): RegExp {
+    const escaped = pattern
+      .replace(/[.+^${}()|[\]\\]/g, '\\$&')  // Escape special regex chars except * and ?
+      .replace(/\*\*/g, '{{GLOBSTAR}}')       // Temp placeholder for **
+      .replace(/\*/g, '[^/]*')                // * matches anything except /
+      .replace(/\?/g, '[^/]')                 // ? matches single char except /
+      .replace(/\{\{GLOBSTAR\}\}/g, '.*');    // ** matches anything including /
+    return new RegExp(escaped);
+  }
+
+  /**
+   * Format files as a flat list
+   */
+  private formatFilesFlat(files: { path: string; language: string; nodeCount: number }[], includeMetadata: boolean): string {
+    const lines: string[] = [`## Files (${files.length})`, ''];
+
+    for (const file of files.sort((a, b) => a.path.localeCompare(b.path))) {
+      if (includeMetadata) {
+        lines.push(`- ${file.path} (${file.language}, ${file.nodeCount} symbols)`);
+      } else {
+        lines.push(`- ${file.path}`);
+      }
+    }
+
+    return lines.join('\n');
+  }
+
+  /**
+   * Format files grouped by language
+   */
+  private formatFilesGrouped(files: { path: string; language: string; nodeCount: number }[], includeMetadata: boolean): string {
+    const byLang = new Map<string, typeof files>();
+
+    for (const file of files) {
+      const existing = byLang.get(file.language) || [];
+      existing.push(file);
+      byLang.set(file.language, existing);
+    }
+
+    const lines: string[] = [`## Files by Language (${files.length} total)`, ''];
+
+    // Sort languages by file count (descending)
+    const sortedLangs = [...byLang.entries()].sort((a, b) => b[1].length - a[1].length);
+
+    for (const [lang, langFiles] of sortedLangs) {
+      lines.push(`### ${lang} (${langFiles.length})`);
+      for (const file of langFiles.sort((a, b) => a.path.localeCompare(b.path))) {
+        if (includeMetadata) {
+          lines.push(`- ${file.path} (${file.nodeCount} symbols)`);
+        } else {
+          lines.push(`- ${file.path}`);
+        }
+      }
+      lines.push('');
+    }
+
+    return lines.join('\n');
+  }
+
+  /**
+   * Format files as a tree structure
+   */
+  private formatFilesTree(
+    files: { path: string; language: string; nodeCount: number }[],
+    includeMetadata: boolean,
+    maxDepth?: number
+  ): string {
+    // Build tree structure
+    interface TreeNode {
+      name: string;
+      children: Map<string, TreeNode>;
+      file?: { language: string; nodeCount: number };
+    }
+
+    const root: TreeNode = { name: '', children: new Map() };
+
+    for (const file of files) {
+      const parts = file.path.split('/');
+      let current = root;
+
+      for (let i = 0; i < parts.length; i++) {
+        const part = parts[i];
+        if (!part) continue;
+
+        if (!current.children.has(part)) {
+          current.children.set(part, { name: part, children: new Map() });
+        }
+        current = current.children.get(part)!;
+
+        // If this is the last part, it's a file
+        if (i === parts.length - 1) {
+          current.file = { language: file.language, nodeCount: file.nodeCount };
+        }
+      }
+    }
+
+    // Render tree
+    const lines: string[] = [`## Project Structure (${files.length} files)`, ''];
+
+    const renderNode = (node: TreeNode, prefix: string, isLast: boolean, depth: number): void => {
+      if (maxDepth !== undefined && depth > maxDepth) return;
+
+      const connector = isLast ? '└── ' : '├── ';
+      const childPrefix = isLast ? '    ' : '│   ';
+
+      if (node.name) {
+        let line = prefix + connector + node.name;
+        if (node.file && includeMetadata) {
+          line += ` (${node.file.language}, ${node.file.nodeCount} symbols)`;
+        }
+        lines.push(line);
+      }
+
+      const children = [...node.children.values()];
+      // Sort: directories first, then files, both alphabetically
+      children.sort((a, b) => {
+        const aIsDir = a.children.size > 0 && !a.file;
+        const bIsDir = b.children.size > 0 && !b.file;
+        if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
+        return a.name.localeCompare(b.name);
+      });
+
+      for (let i = 0; i < children.length; i++) {
+        const child = children[i]!;
+        const nextPrefix = node.name ? prefix + childPrefix : prefix;
+        renderNode(child, nextPrefix, i === children.length - 1, depth + 1);
+      }
+    };
+
+    renderNode(root, '', true, 0);
+
+    return lines.join('\n');
+  }
+
   // =========================================================================
   // Formatting helpers (compact by default to reduce context usage)
   // =========================================================================

+ 2 - 0
src/mcp/transport.ts

@@ -5,6 +5,7 @@
  */
 
 import * as readline from 'readline';
+import { captureException } from '../sentry';
 
 /**
  * JSON-RPC 2.0 Request
@@ -162,6 +163,7 @@ export class StdioTransport {
       try {
         await this.messageHandler(parsed as JsonRpcRequest | JsonRpcNotification);
       } catch (err) {
+        captureException(err, { operation: 'mcp-message-handler' });
         const message = parsed as JsonRpcRequest;
         if ('id' in message) {
           this.sendError(

+ 31 - 4
src/resolution/index.ts

@@ -8,6 +8,7 @@ import * as fs from 'fs';
 import * as path from 'path';
 import { Node, UnresolvedReference, Edge } from '../types';
 import { QueryBuilder } from '../db/queries';
+import { captureException } from '../sentry';
 import {
   UnresolvedRef,
   ResolvedRef,
@@ -91,6 +92,7 @@ export class ReferenceResolver {
         try {
           return fs.existsSync(fullPath);
         } catch (error) {
+          captureException(error, { operation: 'resolution-file-exists', filePath });
           logDebug('Error checking file existence', { filePath, error: String(error) });
           return false;
         }
@@ -107,6 +109,7 @@ export class ReferenceResolver {
           this.fileCache.set(filePath, content);
           return content;
         } catch (error) {
+          captureException(error, { operation: 'resolution-read-file', filePath });
           logDebug('Failed to read file for resolution', { filePath, error: String(error) });
           this.fileCache.set(filePath, null);
           return null;
@@ -124,7 +127,10 @@ export class ReferenceResolver {
   /**
    * Resolve all unresolved references
    */
-  resolveAll(unresolvedRefs: UnresolvedReference[]): ResolutionResult {
+  resolveAll(
+    unresolvedRefs: UnresolvedReference[],
+    onProgress?: (current: number, total: number) => void
+  ): ResolutionResult {
     const resolved: ResolvedRef[] = [];
     const unresolved: UnresolvedRef[] = [];
     const byMethod: Record<string, number> = {};
@@ -140,7 +146,11 @@ export class ReferenceResolver {
       language: this.getLanguageFromNodeId(ref.fromNodeId),
     }));
 
-    for (const ref of refs) {
+    const total = refs.length;
+    let lastReportedPercent = -1;
+
+    for (let i = 0; i < refs.length; i++) {
+      const ref = refs[i]!; // Array index is guaranteed to be in bounds
       const result = this.resolveOne(ref);
 
       if (result) {
@@ -149,6 +159,20 @@ export class ReferenceResolver {
       } else {
         unresolved.push(ref);
       }
+
+      // Report progress every 1% to avoid too many updates
+      if (onProgress) {
+        const currentPercent = Math.floor((i / total) * 100);
+        if (currentPercent > lastReportedPercent) {
+          lastReportedPercent = currentPercent;
+          onProgress(i + 1, total);
+        }
+      }
+    }
+
+    // Final progress report
+    if (onProgress && total > 0) {
+      onProgress(total, total);
     }
 
     return {
@@ -215,8 +239,11 @@ export class ReferenceResolver {
   /**
    * Resolve and persist edges to database
    */
-  resolveAndPersist(unresolvedRefs: UnresolvedReference[]): ResolutionResult {
-    const result = this.resolveAll(unresolvedRefs);
+  resolveAndPersist(
+    unresolvedRefs: UnresolvedReference[],
+    onProgress?: (current: number, total: number) => void
+  ): ResolutionResult {
+    const result = this.resolveAll(unresolvedRefs, onProgress);
 
     // Create edges from resolved references
     const edges = this.createEdges(result.resolved);

+ 167 - 0
src/sentry.ts

@@ -0,0 +1,167 @@
+/**
+ * Lightweight Sentry client for CodeGraph — uses the HTTP envelope API directly.
+ * No @sentry/node dependency, works in any Node.js environment.
+ */
+
+import { createHash } from 'crypto';
+
+const DSN = 'https://9591f8aca69bcf98e9feb31544200b47@o1181972.ingest.us.sentry.io/4510840133713920';
+const DSN_PARTS = DSN.match(/^https:\/\/([^@]+)@([^/]+)\/(.+)$/);
+const PUBLIC_KEY = DSN_PARTS![1];
+const HOST = DSN_PARTS![2];
+const PROJECT_ID = DSN_PARTS![3];
+const STORE_URL = `https://${HOST}/api/${PROJECT_ID}/envelope/`;
+
+let _enabled = false;
+let _release = 'codegraph@unknown';
+let _tags: Record<string, string> = {};
+
+/**
+ * Initialize Sentry error reporting.
+ * Safe to call multiple times — subsequent calls update tags/release.
+ */
+export function initSentry({ processName, version }: { processName: string; version?: string }) {
+  // Skip in development/test environments
+  if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' || process.env.VITEST) {
+    return;
+  }
+  _enabled = true;
+  _release = `codegraph@${version ?? process.env.npm_package_version ?? 'unknown'}`;
+  _tags = { processName };
+}
+
+/**
+ * Send an error to Sentry with full stack trace and context.
+ * Fire-and-forget — never throws, never blocks.
+ */
+export function captureException(error: unknown, extra?: Record<string, unknown>) {
+  if (!_enabled) return;
+
+  try {
+    const err = error instanceof Error ? error : new Error(String(error));
+    const msg = err.message.toLowerCase();
+
+    // Filter non-actionable noise
+    if (msg.includes('pty') || msg.includes('terminal session')) return;
+    if ((msg.includes('econnrefused') || msg.includes('econnreset')) && msg.includes('127.0.0.1')) return;
+
+    const eventId = createHash('md5').update(`${Date.now()}-${Math.random()}`).digest('hex');
+    const timestamp = new Date().toISOString();
+
+    // Attach CodeGraphError context if available
+    const errorContext: Record<string, unknown> = { ...extra };
+    if ('code' in err && typeof (err as any).code === 'string') {
+      errorContext.errorCode = (err as any).code;
+    }
+    if ('context' in err && typeof (err as any).context === 'object') {
+      Object.assign(errorContext, (err as any).context);
+    }
+
+    const event: Record<string, unknown> = {
+      event_id: eventId,
+      timestamp,
+      platform: 'node',
+      level: 'error',
+      release: _release,
+      tags: _tags,
+      exception: {
+        values: [{
+          type: err.name,
+          value: err.message,
+          stacktrace: parseStack(err.stack),
+        }],
+      },
+    };
+
+    if (Object.keys(errorContext).length > 0) {
+      event.extra = errorContext;
+    }
+
+    const payload = JSON.stringify(event);
+    const envelope = [
+      JSON.stringify({ event_id: eventId, sent_at: timestamp, dsn: DSN }),
+      JSON.stringify({ type: 'event', length: payload.length }),
+      payload,
+    ].join('\n') + '\n';
+
+    fetch(STORE_URL, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/x-sentry-envelope',
+        'X-Sentry-Auth': `Sentry sentry_version=7, sentry_key=${PUBLIC_KEY}`,
+      },
+      body: envelope,
+    }).catch(() => {});
+  } catch {
+    // Never throw from error reporting
+  }
+}
+
+/**
+ * Send a message-level event to Sentry (for logged errors without Error objects).
+ */
+export function captureMessage(message: string, context?: Record<string, unknown>) {
+  if (!_enabled) return;
+
+  try {
+    const eventId = createHash('md5').update(`${Date.now()}-${Math.random()}`).digest('hex');
+    const timestamp = new Date().toISOString();
+
+    const event: Record<string, unknown> = {
+      event_id: eventId,
+      timestamp,
+      platform: 'node',
+      level: 'error',
+      release: _release,
+      tags: _tags,
+      message: { formatted: message },
+    };
+
+    if (context && Object.keys(context).length > 0) {
+      event.extra = context;
+    }
+
+    const payload = JSON.stringify(event);
+    const envelope = [
+      JSON.stringify({ event_id: eventId, sent_at: timestamp, dsn: DSN }),
+      JSON.stringify({ type: 'event', length: payload.length }),
+      payload,
+    ].join('\n') + '\n';
+
+    fetch(STORE_URL, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/x-sentry-envelope',
+        'X-Sentry-Auth': `Sentry sentry_version=7, sentry_key=${PUBLIC_KEY}`,
+      },
+      body: envelope,
+    }).catch(() => {});
+  } catch {
+    // Never throw from error reporting
+  }
+}
+
+/**
+ * Parse a Node.js Error.stack string into Sentry's stacktrace format.
+ */
+function parseStack(stack?: string): { frames: Array<{ filename: string; function: string; lineno?: number; colno?: number }> } | undefined {
+  if (!stack) return undefined;
+
+  const frames = stack
+    .split('\n')
+    .slice(1)
+    .map((line) => {
+      const match = line.match(/^\s+at\s+(?:(.+?)\s+\()?(.*?):(\d+):(\d+)\)?$/);
+      if (!match || !match[2] || !match[3] || !match[4]) return null;
+      return {
+        function: match[1] || '<anonymous>',
+        filename: match[2],
+        lineno: parseInt(match[3], 10),
+        colno: parseInt(match[4], 10),
+      };
+    })
+    .filter((f): f is NonNullable<typeof f> => f !== null)
+    .reverse();
+
+  return frames.length > 0 ? { frames } : undefined;
+}

+ 0 - 284
src/sync/git-hooks.ts

@@ -1,284 +0,0 @@
-/**
- * Git Hooks Management
- *
- * Installs and manages git hooks for automatic incremental indexing.
- */
-
-import * as fs from 'fs';
-import * as path from 'path';
-
-/**
- * Name of the post-commit hook file
- */
-const POST_COMMIT_HOOK = 'post-commit';
-
-/**
- * Marker comment to identify CodeGraph-managed hooks
- */
-const CODEGRAPH_MARKER = '# CodeGraph auto-sync hook';
-
-/**
- * The post-commit hook script content
- *
- * This script:
- * 1. Checks if codegraph CLI is available
- * 2. Falls back to npx if not
- * 3. Runs sync in the background to avoid blocking commits
- */
-const POST_COMMIT_SCRIPT = `#!/bin/sh
-${CODEGRAPH_MARKER}
-# This hook was installed by CodeGraph to enable automatic incremental indexing.
-# It runs after each commit to keep the code graph in sync.
-# To remove this hook, run: codegraph hooks --remove
-# Or delete this file manually.
-
-# Run sync in background to avoid blocking the commit
-(
-  # Check if we're in a CodeGraph project
-  if [ ! -d ".codegraph" ]; then
-    exit 0
-  fi
-
-  # Try to run codegraph sync
-  if command -v codegraph >/dev/null 2>&1; then
-    codegraph sync --quiet 2>/dev/null &
-  elif command -v npx >/dev/null 2>&1; then
-    npx codegraph sync --quiet 2>/dev/null &
-  fi
-) &
-
-exit 0
-`;
-
-/**
- * Result of hook installation
- */
-export interface HookInstallResult {
-  success: boolean;
-  hookPath: string;
-  message: string;
-  previousHookBackedUp?: boolean;
-  backupPath?: string;
-}
-
-/**
- * Result of hook removal
- */
-export interface HookRemoveResult {
-  success: boolean;
-  message: string;
-  restoredFromBackup?: boolean;
-}
-
-/**
- * Git hooks manager
- */
-export class GitHooksManager {
-  private gitDir: string;
-  private hooksDir: string;
-
-  constructor(projectRoot: string) {
-    this.gitDir = path.join(projectRoot, '.git');
-    this.hooksDir = path.join(this.gitDir, 'hooks');
-  }
-
-  /**
-   * Check if the project is a git repository
-   */
-  isGitRepository(): boolean {
-    return fs.existsSync(this.gitDir) && fs.statSync(this.gitDir).isDirectory();
-  }
-
-  /**
-   * Check if the post-commit hook is installed by CodeGraph
-   */
-  isHookInstalled(): boolean {
-    const hookPath = path.join(this.hooksDir, POST_COMMIT_HOOK);
-
-    if (!fs.existsSync(hookPath)) {
-      return false;
-    }
-
-    try {
-      const content = fs.readFileSync(hookPath, 'utf-8');
-      return content.includes(CODEGRAPH_MARKER);
-    } catch {
-      return false;
-    }
-  }
-
-  /**
-   * Install the post-commit hook
-   *
-   * If a hook already exists:
-   * - If it's a CodeGraph hook, update it
-   * - If it's a user hook, back it up and install ours
-   */
-  installHook(): HookInstallResult {
-    const hookPath = path.join(this.hooksDir, POST_COMMIT_HOOK);
-
-    // Check if this is a git repository
-    if (!this.isGitRepository()) {
-      return {
-        success: false,
-        hookPath,
-        message: 'Not a git repository. Initialize git first with: git init',
-      };
-    }
-
-    // Ensure hooks directory exists
-    if (!fs.existsSync(this.hooksDir)) {
-      try {
-        fs.mkdirSync(this.hooksDir, { recursive: true });
-      } catch (error) {
-        return {
-          success: false,
-          hookPath,
-          message: `Failed to create hooks directory: ${error}`,
-        };
-      }
-    }
-
-    // Check for existing hook
-    let previousHookBackedUp = false;
-    let backupPath: string | undefined;
-
-    if (fs.existsSync(hookPath)) {
-      try {
-        const existingContent = fs.readFileSync(hookPath, 'utf-8');
-
-        // If it's already our hook, just update it
-        if (existingContent.includes(CODEGRAPH_MARKER)) {
-          fs.writeFileSync(hookPath, POST_COMMIT_SCRIPT, { mode: 0o755 });
-          return {
-            success: true,
-            hookPath,
-            message: 'Post-commit hook updated.',
-          };
-        }
-
-        // It's a user hook - back it up
-        backupPath = `${hookPath}.codegraph-backup`;
-        fs.copyFileSync(hookPath, backupPath);
-        previousHookBackedUp = true;
-      } catch (error) {
-        return {
-          success: false,
-          hookPath,
-          message: `Failed to backup existing hook: ${error}`,
-        };
-      }
-    }
-
-    // Write the hook
-    try {
-      fs.writeFileSync(hookPath, POST_COMMIT_SCRIPT, { mode: 0o755 });
-    } catch (error) {
-      return {
-        success: false,
-        hookPath,
-        message: `Failed to write hook: ${error}`,
-      };
-    }
-
-    const message = previousHookBackedUp
-      ? `Post-commit hook installed. Previous hook backed up to: ${backupPath}`
-      : 'Post-commit hook installed.';
-
-    return {
-      success: true,
-      hookPath,
-      message,
-      previousHookBackedUp,
-      backupPath,
-    };
-  }
-
-  /**
-   * Remove the CodeGraph post-commit hook
-   *
-   * If a backup exists, restore it.
-   */
-  removeHook(): HookRemoveResult {
-    const hookPath = path.join(this.hooksDir, POST_COMMIT_HOOK);
-    const backupPath = `${hookPath}.codegraph-backup`;
-
-    // Check if hook exists
-    if (!fs.existsSync(hookPath)) {
-      return {
-        success: true,
-        message: 'No post-commit hook found.',
-      };
-    }
-
-    // Check if it's our hook
-    try {
-      const content = fs.readFileSync(hookPath, 'utf-8');
-      if (!content.includes(CODEGRAPH_MARKER)) {
-        return {
-          success: false,
-          message: 'Post-commit hook was not installed by CodeGraph. Not removing.',
-        };
-      }
-    } catch (error) {
-      return {
-        success: false,
-        message: `Failed to read hook: ${error}`,
-      };
-    }
-
-    // Remove the hook
-    try {
-      fs.unlinkSync(hookPath);
-    } catch (error) {
-      return {
-        success: false,
-        message: `Failed to remove hook: ${error}`,
-      };
-    }
-
-    // Restore backup if it exists
-    if (fs.existsSync(backupPath)) {
-      try {
-        fs.renameSync(backupPath, hookPath);
-        return {
-          success: true,
-          message: 'Post-commit hook removed. Previous hook restored from backup.',
-          restoredFromBackup: true,
-        };
-      } catch (error) {
-        return {
-          success: true,
-          message: `Post-commit hook removed. Warning: failed to restore backup: ${error}`,
-          restoredFromBackup: false,
-        };
-      }
-    }
-
-    return {
-      success: true,
-      message: 'Post-commit hook removed.',
-    };
-  }
-
-  /**
-   * Get the path to the hooks directory
-   */
-  getHooksDir(): string {
-    return this.hooksDir;
-  }
-
-  /**
-   * Get the path to the post-commit hook
-   */
-  getHookPath(): string {
-    return path.join(this.hooksDir, POST_COMMIT_HOOK);
-  }
-}
-
-/**
- * Create a git hooks manager for a project
- */
-export function createGitHooksManager(projectRoot: string): GitHooksManager {
-  return new GitHooksManager(projectRoot);
-}

+ 6 - 7
src/sync/index.ts

@@ -4,15 +4,14 @@
  * Provides synchronization functionality for keeping the code graph
  * up-to-date with file system changes.
  *
+ * Note: Git hooks functionality has been removed. CodeGraph sync is now
+ * triggered through codegraph's Claude Code hooks integration instead.
+ *
  * Components:
- * - Git hooks for automatic post-commit syncing
  * - Content hashing for change detection (in extraction module)
  * - Incremental reindexing (in extraction module)
  */
 
-export {
-  GitHooksManager,
-  createGitHooksManager,
-  HookInstallResult,
-  HookRemoveResult,
-} from './git-hooks';
+// This module is kept for potential future sync-related exports
+// Currently all sync functionality is in the extraction module
+export {};

+ 11 - 0
src/types.ts

@@ -71,6 +71,7 @@ export type Language =
   | 'ruby'
   | 'swift'
   | 'kotlin'
+  | 'dart'
   | 'liquid'
   | 'unknown';
 
@@ -497,6 +498,13 @@ export const DEFAULT_CONFIG: CodeGraphConfig = {
     '**/*.php',
     // Ruby
     '**/*.rb',
+    // Swift
+    '**/*.swift',
+    // Kotlin
+    '**/*.kt',
+    '**/*.kts',
+    // Dart
+    '**/*.dart',
     // Liquid (Shopify themes)
     '**/*.liquid',
   ],
@@ -570,6 +578,9 @@ export const DEFAULT_CONFIG: CodeGraphConfig = {
     '**/generated-sources/**',
     '**/.kotlin/**',
 
+    // Dart/Flutter
+    '**/.dart_tool/**',
+
     // C#/.NET
     '**/.vs/**',
     '**/.nuget/**',

+ 2 - 2
src/vectors/embedder.ts

@@ -9,7 +9,7 @@ import * as path from 'path';
 import * as fs from 'fs';
 import { homedir } from 'os';
 
-// Global model cache directory (shared across all projects)
+// Global model cache directory - uses codegraph's models directory for shared embedding models
 const GLOBAL_MODELS_DIR = path.join(homedir(), '.codegraph', 'models');
 
 // Dynamic import for @xenova/transformers (ESM-only package)
@@ -40,7 +40,7 @@ export interface EmbedderOptions {
   /** Model ID to use (default: nomic-ai/nomic-embed-text-v1.5) */
   modelId?: string;
 
-  /** Directory to cache the model (default: .codegraph/models) */
+  /** Directory to cache the model (default: ~/.codegraph/models) */
   cacheDir?: string;
 
   /** Whether to show progress during model download */