diff --git a/.gitignore b/.gitignore index 2d84f1f..bdc9b2e 100644 --- a/.gitignore +++ b/.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/ diff --git a/CLAUDE.md b/CLAUDE.md index aa32f31..3a7f73a 100644 --- a/CLAUDE.md +++ b/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 diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 2de1d45..65d99d8 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/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 │ diff --git a/__tests__/evaluation/evaluation.test.ts b/__tests__/evaluation/evaluation.test.ts deleted file mode 100644 index efdbfcf..0000000 --- a/__tests__/evaluation/evaluation.test.ts +++ /dev/null @@ -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 { - 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): 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 { - 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); - }); - } - }); -}); diff --git a/__tests__/evaluation/fixtures/python-project/auth.py b/__tests__/evaluation/fixtures/python-project/auth.py deleted file mode 100644 index 9e88d29..0000000 --- a/__tests__/evaluation/fixtures/python-project/auth.py +++ /dev/null @@ -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() diff --git a/__tests__/evaluation/fixtures/python-project/database.py b/__tests__/evaluation/fixtures/python-project/database.py deleted file mode 100644 index be7f5e4..0000000 --- a/__tests__/evaluation/fixtures/python-project/database.py +++ /dev/null @@ -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() diff --git a/__tests__/evaluation/fixtures/python-project/ground-truth.ts b/__tests__/evaluation/fixtures/python-project/ground-truth.ts deleted file mode 100644 index 11dd8e0..0000000 --- a/__tests__/evaluation/fixtures/python-project/ground-truth.ts +++ /dev/null @@ -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' }, - ], -}; diff --git a/__tests__/evaluation/fixtures/python-project/models.py b/__tests__/evaluation/fixtures/python-project/models.py deleted file mode 100644 index 9137a9e..0000000 --- a/__tests__/evaluation/fixtures/python-project/models.py +++ /dev/null @@ -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 diff --git a/__tests__/evaluation/fixtures/python-project/tasks.py b/__tests__/evaluation/fixtures/python-project/tasks.py deleted file mode 100644 index 93ae0a4..0000000 --- a/__tests__/evaluation/fixtures/python-project/tasks.py +++ /dev/null @@ -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() diff --git a/__tests__/evaluation/fixtures/python-project/validation.py b/__tests__/evaluation/fixtures/python-project/validation.py deleted file mode 100644 index e61278c..0000000 --- a/__tests__/evaluation/fixtures/python-project/validation.py +++ /dev/null @@ -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) diff --git a/__tests__/evaluation/fixtures/typescript-project/ground-truth.ts b/__tests__/evaluation/fixtures/typescript-project/ground-truth.ts deleted file mode 100644 index bdbeebb..0000000 --- a/__tests__/evaluation/fixtures/typescript-project/ground-truth.ts +++ /dev/null @@ -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' }, - ], -}; diff --git a/__tests__/evaluation/fixtures/typescript-project/src/auth.ts b/__tests__/evaluation/fixtures/typescript-project/src/auth.ts deleted file mode 100644 index 216d957..0000000 --- a/__tests__/evaluation/fixtures/typescript-project/src/auth.ts +++ /dev/null @@ -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 = new Map(); - - async register(email: string, password: string, name: string): Promise { - 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 { - 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 { - this.tokens.delete(token); - } - - async validateToken(token: string): Promise { - 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 { - 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(); diff --git a/__tests__/evaluation/fixtures/typescript-project/src/database.ts b/__tests__/evaluation/fixtures/typescript-project/src/database.ts deleted file mode 100644 index 4aee79d..0000000 --- a/__tests__/evaluation/fixtures/typescript-project/src/database.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Database abstraction layer - */ - -import { User, Product, Order } from './types'; - -export class Database { - private users: Map = new Map(); - private products: Map = new Map(); - private orders: Map = new Map(); - - async findUserById(id: string): Promise { - return this.users.get(id) || null; - } - - async findUserByEmail(email: string): Promise { - for (const user of this.users.values()) { - if (user.email === email) { - return user; - } - } - return null; - } - - async createUser(user: User): Promise { - this.users.set(user.id, user); - } - - async updateUser(id: string, updates: Partial): Promise { - const user = this.users.get(id); - if (user) { - this.users.set(id, { ...user, ...updates }); - } - } - - async findProductById(id: string): Promise { - return this.products.get(id) || null; - } - - async updateProductStock(id: string, quantity: number): Promise { - const product = this.products.get(id); - if (product) { - product.stock -= quantity; - this.products.set(id, product); - } - } - - async createOrder(order: Order): Promise { - this.orders.set(order.id, order); - } - - async findOrderById(id: string): Promise { - return this.orders.get(id) || null; - } - - async findOrdersByUserId(userId: string): Promise { - 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 { - const order = this.orders.get(id); - if (order) { - order.status = status; - this.orders.set(id, order); - } - } -} - -export const db = new Database(); diff --git a/__tests__/evaluation/fixtures/typescript-project/src/index.ts b/__tests__/evaluation/fixtures/typescript-project/src/index.ts deleted file mode 100644 index a369264..0000000 --- a/__tests__/evaluation/fixtures/typescript-project/src/index.ts +++ /dev/null @@ -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'; diff --git a/__tests__/evaluation/fixtures/typescript-project/src/order.ts b/__tests__/evaluation/fixtures/typescript-project/src/order.ts deleted file mode 100644 index b774cf5..0000000 --- a/__tests__/evaluation/fixtures/typescript-project/src/order.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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(); diff --git a/__tests__/evaluation/fixtures/typescript-project/src/payment.ts b/__tests__/evaluation/fixtures/typescript-project/src/payment.ts deleted file mode 100644 index 64e8fdb..0000000 --- a/__tests__/evaluation/fixtures/typescript-project/src/payment.ts +++ /dev/null @@ -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 { - 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 { - 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(); diff --git a/__tests__/evaluation/fixtures/typescript-project/src/types.ts b/__tests__/evaluation/fixtures/typescript-project/src/types.ts deleted file mode 100644 index 99bd4d9..0000000 --- a/__tests__/evaluation/fixtures/typescript-project/src/types.ts +++ /dev/null @@ -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; -} diff --git a/__tests__/evaluation/fixtures/typescript-project/src/user.ts b/__tests__/evaluation/fixtures/typescript-project/src/user.ts deleted file mode 100644 index d46e9b3..0000000 --- a/__tests__/evaluation/fixtures/typescript-project/src/user.ts +++ /dev/null @@ -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 { - return db.findUserById(id); - } - - async getUserByEmail(email: string): Promise { - return db.findUserByEmail(email); - } - - async updateProfile(userId: string, updates: { name?: string; email?: string }): Promise { - 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 { - 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(); diff --git a/__tests__/evaluation/fixtures/typescript-project/src/utils/crypto.ts b/__tests__/evaluation/fixtures/typescript-project/src/utils/crypto.ts deleted file mode 100644 index 77dc85e..0000000 --- a/__tests__/evaluation/fixtures/typescript-project/src/utils/crypto.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Cryptographic utilities - */ - -export async function hashPassword(password: string): Promise { - // Simulated password hashing - return `hashed_${password}_${Date.now()}`; -} - -export async function verifyPassword(password: string, hash: string): Promise { - // 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)}`; -} diff --git a/__tests__/evaluation/fixtures/typescript-project/src/utils/validation.ts b/__tests__/evaluation/fixtures/typescript-project/src/utils/validation.ts deleted file mode 100644 index a48aade..0000000 --- a/__tests__/evaluation/fixtures/typescript-project/src/utils/validation.ts +++ /dev/null @@ -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; -} diff --git a/__tests__/evaluation/runner.ts b/__tests__/evaluation/runner.ts deleted file mode 100644 index 92c00a0..0000000 --- a/__tests__/evaluation/runner.ts +++ /dev/null @@ -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 { - const names = new Set(); - 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): 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 { - 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 { - 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 { - 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); - }); -} diff --git a/__tests__/evaluation/types.ts b/__tests__/evaluation/types.ts deleted file mode 100644 index f24dc35..0000000 --- a/__tests__/evaluation/types.ts +++ /dev/null @@ -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; -} diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index a1ac499..c1c0c90 100644 --- a/__tests__/extraction.test.ts +++ b/__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 { }); }); +describe('Dart Extraction', () => { + it('should extract class declarations', () => { + const code = ` +class UserService { + final Database _db; + + Future 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 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;`; + 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'); + 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 = ` n.kind === 'import'); + expect(importNode).toBeDefined(); + expect(importNode?.name).toBe('PHPUnit\\Framework\\TestCase'); + }); + + it('should extract aliased use', () => { + const code = ` n.kind === 'import'); + expect(importNode).toBeDefined(); + expect(importNode?.name).toBe('Mockery'); + expect(importNode?.signature).toContain('as m'); + }); + + it('should extract function use', () => { + const code = ` 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 = ` 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 = ` 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 `; + 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 '); + }); + + it('should extract system include with path', () => { + const code = `#include `; + 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 `; + 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 +#include +#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; diff --git a/__tests__/foundation.test.ts b/__tests__/foundation.test.ts index a1790af..f7729cd 100644 --- a/__tests__/foundation.test.ts +++ b/__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 diff --git a/__tests__/sync.test.ts b/__tests__/sync.test.ts index 013014a..909c079 100644 --- a/__tests__/sync.test.ts +++ b/__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; diff --git a/package-lock.json b/package-lock.json index f3d6ab0..4b990c2 100644 --- a/package-lock.json +++ b/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", diff --git a/package.json b/package.json index 62a0dfd..1290802 100644 --- a/package.json +++ b/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" diff --git a/scripts/patch-tree-sitter-dart.js b/scripts/patch-tree-sitter-dart.js new file mode 100644 index 0000000..c7de1a8 --- /dev/null +++ b/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 + +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::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": [ + " { // Silent exit - don't break npm install process.exit(0); diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index 5a19461..05884c9 100644 --- a/src/bin/codegraph.ts +++ b/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 for symbols + * codegraph files [options] Show project file structure * codegraph context 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,11 +572,218 @@ program cg.destroy(); } catch (err) { + captureException(err); error(`Search failed: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } }); +/** + * codegraph files [path] + */ +program + .command('files') + .description('Show project file structure from the index') + .option('-p, --path ', 'Project path') + .option('--filter ', 'Filter to files under this directory') + .option('--pattern ', 'Filter files matching this glob pattern') + .option('--format ', 'Output format (tree, flat, grouped)', 'tree') + .option('--max-depth ', '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; + filter?: string; + pattern?: string; + format?: string; + maxDepth?: string; + metadata?: boolean; + json?: boolean; + }) => { + const projectPath = resolveProjectPath(options.path); + + try { + if (!CodeGraph.isInitialized(projectPath)) { + error(`CodeGraph not initialized in ${projectPath}`); + process.exit(1); + } + + const cg = await CodeGraph.open(projectPath); + let files = cg.getFiles(); + + if (files.length === 0) { + info('No files indexed. Run "codegraph index" first.'); + cg.destroy(); + return; + } + + // Filter by path prefix + if (options.filter) { + const filter = options.filter; + files = files.filter(f => f.path.startsWith(filter) || f.path.startsWith('./' + filter)); + } + + // Filter by glob pattern + if (options.pattern) { + const regex = globToRegex(options.pattern); + files = files.filter(f => regex.test(f.path)); + } + + if (files.length === 0) { + info('No files found matching the criteria.'); + cg.destroy(); + return; + } + + // 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(); + return; + } + + 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(); + 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; + + 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) { + captureException(err); + error(`Failed to list files: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); + } + }); + +/** + * 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); +} + +/** + * 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; + 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 (i === parts.length - 1) { + current.file = { language: file.language, nodeCount: file.nodeCount }; + } + } + } + + 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); + } + + 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); + } + }; + + renderNode(root, '', true, 0); +} + /** * codegraph context */ @@ -562,133 +824,12 @@ program cg.destroy(); } catch (err) { + captureException(err); error(`Failed to build context: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } }); -/** - * codegraph hooks - */ -const hooksCommand = program - .command('hooks') - .description('Manage git hooks'); - -hooksCommand - .command('install') - .description('Install git post-commit hook for auto-sync') - .option('-p, --path ', 'Project path') - .action(async (options: { path?: string }) => { - const projectPath = resolveProjectPath(options.path); - - try { - if (!CodeGraph.isInitialized(projectPath)) { - error(`CodeGraph not initialized in ${projectPath}`); - process.exit(1); - } - - const cg = await CodeGraph.open(projectPath); - - if (!cg.isGitRepository()) { - error('Not a git repository'); - cg.destroy(); - process.exit(1); - } - - const result = cg.installGitHooks(); - - 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); - } - - cg.destroy(); - } catch (err) { - error(`Failed to install hooks: ${err instanceof Error ? err.message : String(err)}`); - process.exit(1); - } - }); - -hooksCommand - .command('remove') - .description('Remove git post-commit hook') - .option('-p, --path ', 'Project path') - .action(async (options: { path?: string }) => { - const projectPath = resolveProjectPath(options.path); - - try { - if (!CodeGraph.isInitialized(projectPath)) { - error(`CodeGraph not initialized in ${projectPath}`); - process.exit(1); - } - - const cg = await CodeGraph.open(projectPath); - - if (!cg.isGitRepository()) { - error('Not a git repository'); - cg.destroy(); - process.exit(1); - } - - const result = cg.removeGitHooks(); - - if (result.success) { - success(result.message); - if (result.restoredFromBackup) { - info('Restored previous hook from backup'); - } - } else { - error(result.message); - process.exit(1); - } - - cg.destroy(); - } catch (err) { - error(`Failed to remove hooks: ${err instanceof Error ? err.message : String(err)}`); - process.exit(1); - } - }); - -hooksCommand - .command('status') - .description('Check git hooks status') - .option('-p, --path ', 'Project path') - .action(async (options: { path?: string }) => { - const projectPath = resolveProjectPath(options.path); - - try { - if (!CodeGraph.isInitialized(projectPath)) { - error(`CodeGraph not initialized in ${projectPath}`); - process.exit(1); - } - - const cg = await CodeGraph.open(projectPath); - - if (!cg.isGitRepository()) { - info('Not a git repository'); - cg.destroy(); - return; - } - - if (cg.isGitHookInstalled()) { - success('Git hook is installed'); - } else { - warn('Git hook is not installed'); - info('Run "codegraph hooks install" to enable auto-sync'); - } - - cg.destroy(); - } catch (err) { - error(`Failed to check hooks: ${err instanceof Error ? err.message : String(err)}`); - process.exit(1); - } - }); - /** * codegraph serve */ @@ -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); } diff --git a/src/context/index.ts b/src/context/index.ts index 3c327cb..99bb890 100644 --- a/src/context/index.ts +++ b/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(); + + // 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 = { 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 = { 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(); + 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(); + + 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; + } } /** diff --git a/src/db/index.ts b/src/db/index.ts index 917f040..b282c18 100644 --- a/src/db/index.ts +++ b/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); diff --git a/src/db/queries.ts b/src/db/queries.ts index de93347..3a40c47 100644 --- a/src/db/queries.ts +++ b/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 // =========================================================================== diff --git a/src/db/schema.sql b/src/db/schema.sql index e23f8f7..a68c7b7 100644 --- a/src/db/schema.sql +++ b/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 diff --git a/src/directory.ts b/src/directory.ts index c7b520c..645c1d9 100644 --- a/src/directory.ts +++ b/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'); + } } /** diff --git a/src/errors.ts b/src/errors.ts index acd440c..1a9a811 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -233,8 +233,13 @@ export function logWarn(message: string, context?: Record): voi } /** - * Log an error message + * Log an error message (also sends to Sentry if initialized) */ export function logError(message: string, context?: Record): void { currentLogger.error(message, context); + // Lazy import to avoid circular deps + try { + const { captureMessage } = require('./sentry'); + captureMessage(message, context); + } catch {} } diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index f896833..5715a70 100644 --- a/src/extraction/grammars.ts +++ b/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 = { - 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 = {}; + +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 = { '.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', }; diff --git a/src/extraction/index.ts b/src/extraction/index.ts index 49f8601..1ac0332 100644 --- a/src/extraction/index.ts +++ b/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; } diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 7e91ecb..a86736a 100644 --- a/src/extraction/tree-sitter.ts +++ b/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> = { 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> = { } 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> = { 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> = { } 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> = { 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> = { 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> = { 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> = { 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> = { enumTypes: ['enum_specifier'], importTypes: ['preproc_include'], callTypes: ['call_expression'], + variableTypes: ['declaration'], nameField: 'declarator', bodyField: 'body', paramsField: 'parameters', @@ -383,6 +415,7 @@ const EXTRACTORS: Partial> = { enumTypes: ['enum_specifier'], importTypes: ['preproc_include'], callTypes: ['call_expression'], + variableTypes: ['declaration'], nameField: 'declarator', bodyField: 'body', paramsField: 'parameters', @@ -412,6 +445,7 @@ const EXTRACTORS: Partial> = { 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> = { 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> = { 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> = { 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> = { 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> = { 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 !== '') { + 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 , #include "myheader.h" + // AST: preproc_include -> system_lib_string (<...>) or string_literal ("...") + + // Check for system include: + const systemLib = node.namedChildren.find(c => c.type === 'system_lib_string'); + if (systemLib) { + // Remove angle brackets: -> 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(), }; diff --git a/src/index.ts b/src/index.ts index 6905daa..d0638cc 100644 --- a/src/index.ts +++ b/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); } } diff --git a/src/installer/index.ts b/src/installer/index.ts index eb385ec..f54770c 100644 --- a/src/installer/index.ts +++ b/src/installer/index.ts @@ -151,14 +151,6 @@ async function initializeLocalProject(): Promise { 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(); } diff --git a/src/mcp/index.ts b/src/mcp/index.ts index bdcd655..e35b61a 100644 --- a/src/mcp/index.ts +++ b/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 { - 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; diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index 8084fe3..a892405 100644 --- a/src/mcp/tools.ts +++ b/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 = 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): Promise { + 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): Promise { + // 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): Promise { + 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): Promise { + 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): Promise { + 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): Promise { + 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 { - const stats = this.cg.getStats(); + private async handleStatus(args: Record): Promise { + 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): Promise { + 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(); + + 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; + 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) // ========================================================================= diff --git a/src/mcp/transport.ts b/src/mcp/transport.ts index 4403891..6c21257 100644 --- a/src/mcp/transport.ts +++ b/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( diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 351dbf3..5d0b836 100644 --- a/src/resolution/index.ts +++ b/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 = {}; @@ -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); diff --git a/src/sentry.ts b/src/sentry.ts new file mode 100644 index 0000000..76fe496 --- /dev/null +++ b/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 = {}; + +/** + * 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) { + 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 = { ...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 = { + 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) { + if (!_enabled) return; + + try { + const eventId = createHash('md5').update(`${Date.now()}-${Math.random()}`).digest('hex'); + const timestamp = new Date().toISOString(); + + const event: Record = { + 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] || '', + filename: match[2], + lineno: parseInt(match[3], 10), + colno: parseInt(match[4], 10), + }; + }) + .filter((f): f is NonNullable => f !== null) + .reverse(); + + return frames.length > 0 ? { frames } : undefined; +} diff --git a/src/sync/git-hooks.ts b/src/sync/git-hooks.ts deleted file mode 100644 index 621c158..0000000 --- a/src/sync/git-hooks.ts +++ /dev/null @@ -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); -} diff --git a/src/sync/index.ts b/src/sync/index.ts index f56e8c4..b63d94b 100644 --- a/src/sync/index.ts +++ b/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 {}; diff --git a/src/types.ts b/src/types.ts index 3cb4d1a..1dea1bb 100644 --- a/src/types.ts +++ b/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/**', diff --git a/src/vectors/embedder.ts b/src/vectors/embedder.ts index 06ec613..8084ff7 100644 --- a/src/vectors/embedder.ts +++ b/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 */