diff --git a/gitnexus-mcp/package.json b/gitnexus-mcp/package.json index 4203ff69f..037571f93 100644 --- a/gitnexus-mcp/package.json +++ b/gitnexus-mcp/package.json @@ -1,6 +1,6 @@ { "name": "gitnexus-mcp", - "version": "0.1.1", + "version": "0.2.0", "description": "MCP server for GitNexus code intelligence - connect Cursor, Claude, and other AI agents to your codebase", "author": "Abhigyan Patwari", "license": "MIT", @@ -44,4 +44,4 @@ "engines": { "node": ">=18.0.0" } -} +} \ No newline at end of file diff --git a/gitnexus-mcp/src/mcp/server.ts b/gitnexus-mcp/src/mcp/server.ts index 81b663613..a5c4ef7e7 100644 --- a/gitnexus-mcp/src/mcp/server.ts +++ b/gitnexus-mcp/src/mcp/server.ts @@ -74,22 +74,28 @@ function formatContextAsMarkdown(context: CodebaseContext): string { // Usage hints lines.push('## 🛠️ Available Tools'); lines.push(''); - lines.push('- **search**: Semantic search across the codebase'); - lines.push('- **cypher**: Execute Cypher queries on the knowledge graph'); - lines.push('- **blastRadius**: Analyze impact of changes to a node'); - lines.push('- **highlight**: Visualize nodes in the graph'); + lines.push('- **search**: Semantic + keyword search across codebase'); + lines.push('- **cypher**: Execute Cypher queries on knowledge graph'); + lines.push('- **grep**: Regex pattern search in files'); + lines.push('- **read**: Read file contents'); + lines.push('- **explore**: Deep dive on symbol, cluster, or process'); + lines.push('- **overview**: Codebase map (all clusters + processes)'); + lines.push('- **impact**: Analyze change impact (upstream/downstream)'); + lines.push('- **highlight**: Visualize nodes in graph'); lines.push(''); lines.push('## 📝 Graph Schema'); lines.push(''); - lines.push('**Node Types**: File, Folder, Function, Class, Interface, Method'); + lines.push('**Node Types**: File, Folder, Function, Class, Interface, Method, Community, Process'); lines.push(''); lines.push('**Relation**: `CodeRelation` with `type` property:'); - lines.push('- CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS'); + lines.push('- CALLS, IMPORTS, EXTENDS, IMPLEMENTS, CONTAINS, DEFINES'); + lines.push('- MEMBER_OF (symbol → community), STEP_IN_PROCESS (symbol → process)'); lines.push(''); lines.push('**Example Cypher Queries**:'); lines.push('```cypher'); lines.push('MATCH (f:Function) RETURN f.name LIMIT 10'); lines.push("MATCH (f:File)-[:CodeRelation {type: 'IMPORTS'}]->(g:File) RETURN f.name, g.name"); + lines.push("MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) RETURN c.label, count(s)"); lines.push('```'); return lines.join('\n'); diff --git a/gitnexus-mcp/src/mcp/tools.ts b/gitnexus-mcp/src/mcp/tools.ts index 9ca2d27d2..84ab18d72 100644 --- a/gitnexus-mcp/src/mcp/tools.ts +++ b/gitnexus-mcp/src/mcp/tools.ts @@ -41,19 +41,20 @@ ALWAYS call this first to understand the codebase before searching or querying.` { name: 'search', description: `Hybrid search (keyword + semantic) across the codebase. -Returns code nodes with their graph connections. +Returns code nodes with their graph connections, grouped by process. WHEN TO USE: - Finding implementations ("where is auth handled?") - Understanding code flow ("what calls UserService?") - Locating patterns ("find all API endpoints") -RETURNS: Array of {name, type, filePath, code, connections[]}`, +RETURNS: Array of {name, type, filePath, code, connections[], cluster, processes[]}`, inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Natural language or keyword search query' }, limit: { type: 'number', description: 'Max results to return', default: 10 }, + groupByProcess: { type: 'boolean', description: 'Group results by process', default: true }, }, required: ['query'], }, @@ -63,23 +64,23 @@ RETURNS: Array of {name, type, filePath, code, connections[]}`, description: `Execute Cypher query against the code knowledge graph. SCHEMA: -- Nodes: File, Function, Class, Interface, Method -- Edges: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, CONTAINS +- Nodes: File, Folder, Function, Class, Interface, Method, Community, Process +- Edges via CodeRelation.type: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, CONTAINS, DEFINES, MEMBER_OF, STEP_IN_PROCESS EXAMPLES: • Find callers of a function: - MATCH (a)-[:CALLS]->(b:Function {name: "validateUser"}) RETURN a.name, a.filePath + MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b:Function {name: "validateUser"}) RETURN a.name, a.filePath -• Find class hierarchy: - MATCH (c:Class)-[:EXTENDS*]->(base) WHERE c.name = "AdminUser" RETURN base.name +• Find all functions in a community: + MATCH (f:Function)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community {label: "Auth"}) RETURN f.name -• Impact analysis (what depends on X): - MATCH (target:Function {name: $name})<-[:CALLS*1..3]-(caller) RETURN DISTINCT caller +• Find steps in a process: + MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process {label: "UserLogin"}) RETURN s.name, r.step ORDER BY r.step TIPS: -- Relationship types are UPPERCASE: CALLS, IMPORTS, EXTENDS -- Node labels are PascalCase: Function, Class, Interface -- Properties: name, filePath, code, startLine, endLine`, +- All relationships use CodeRelation table with 'type' property +- Community = functional cluster detected by Leiden algorithm +- Process = execution flow trace from entry point to terminal`, inputSchema: { type: 'object', properties: { @@ -133,17 +134,60 @@ RETURNS: {filePath, content, language, lines}`, }, }, { - name: 'blastRadius', + name: 'explore', + description: `Deep dive on a symbol, cluster, or process. + +TYPE: symbol | cluster | process + +For SYMBOL: Shows cluster membership, process participation, callers/callees +For CLUSTER: Shows members, cohesion score, processes touching it +For PROCESS: Shows step-by-step trace, clusters traversed, entry/terminal points + +Use after search to understand context of a specific node.`, + inputSchema: { + type: 'object', + properties: { + name: { type: 'string', description: 'Name of symbol, cluster, or process to explore' }, + type: { type: 'string', description: 'Type: symbol, cluster, or process' }, + }, + required: ['name', 'type'], + }, + }, + { + name: 'overview', + description: `Get codebase map showing all clusters and processes. + +Returns: +- All communities (clusters) with member counts and cohesion scores +- All processes with step counts and types (intra/cross-community) +- High-level architectural view + +Use to understand overall codebase structure before diving deep.`, + inputSchema: { + type: 'object', + properties: { + showProcesses: { type: 'boolean', description: 'Include process list', default: true }, + showClusters: { type: 'boolean', description: 'Include cluster list', default: true }, + limit: { type: 'number', description: 'Max items per category', default: 20 }, + }, + required: [], + }, + }, + { + name: 'impact', description: `Analyze the impact of changing a code element. Returns all nodes affected by modifying the target, with distance, edge type, and confidence. USE BEFORE making changes to understand ripple effects. -Output format (compact tabular): - Type|Name|File:Line|EdgeType|Confidence% +Output includes: +- Affected processes (with step positions) +- Affected clusters (direct/indirect) +- Risk assessment (critical/high/medium/low) +- Callers/dependents grouped by depth EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS -Confidence: 100% = certain, <80% = fuzzy match [fuzzy] +Confidence: 100% = certain, <80% = fuzzy match Depth groups: - d=1: WILL BREAK (direct callers/importers) @@ -155,7 +199,7 @@ Depth groups: target: { type: 'string', description: 'Name of function, class, or file to analyze' }, direction: { type: 'string', description: 'upstream (what depends on this) or downstream (what this depends on)' }, maxDepth: { type: 'number', description: 'Max relationship depth (default: 3)', default: 3 }, - relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, CONTAINS, DEFINES (default: usage-based)' }, + relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS (default: usage-based)' }, includeTests: { type: 'boolean', description: 'Include test files (default: false)' }, minConfidence: { type: 'number', description: 'Minimum confidence 0-1 (default: 0.7)' }, }, diff --git a/gitnexus/docs/FRAMEWORK_SUPPORT.md b/gitnexus/docs/FRAMEWORK_SUPPORT.md new file mode 100644 index 000000000..71cca03e4 --- /dev/null +++ b/gitnexus/docs/FRAMEWORK_SUPPORT.md @@ -0,0 +1,74 @@ +# Framework Support for Entry Point Detection + +GitNexus automatically detects frameworks and boosts entry point scores for known patterns. + +## Status Legend +- ✅ Supported (path-based detection) +- ❌ Not yet supported + +--- + +## JavaScript / TypeScript + +| Framework | Status | Detection Pattern | Multiplier | +|-----------|--------|-------------------|------------| +| Next.js (Pages) | ✅ | `/pages/*.tsx` | 3.0x | +| Next.js (App) | ✅ | `/app/*/page.tsx` | 3.0x | +| Next.js API | ✅ | `/pages/api/*`, `/app/*/route.ts` | 3.0x | +| Express.js | ✅ | `/routes/*` | 2.5x | +| React | ✅ | `/components/*.tsx` (PascalCase) | 1.5x | +| NestJS | ❌ | TODO: `@Controller` decorator | - | + +## Python + +| Framework | Status | Detection Pattern | Multiplier | +|-----------|--------|-------------------|------------| +| Django | ✅ | `views.py`, `urls.py` | 3.0x | +| FastAPI | ✅ | `/routers/*`, `/endpoints/*` | 2.5x | +| Flask | ✅ | `/routes/*` | 2.5x | + +## Java + +| Framework | Status | Detection Pattern | Multiplier | +|-----------|--------|-------------------|------------| +| Spring Boot | ✅ | `/controller/*`, `*Controller.java` | 3.0x | +| JAX-RS | ❌ | TODO: `@Path` annotation | - | + +## C# + +| Framework | Status | Detection Pattern | Multiplier | +|-----------|--------|-------------------|------------| +| ASP.NET Core | ✅ | `/Controllers/*`, `*Controller.cs` | 3.0x | +| Blazor | ✅ | `/Pages/*.razor` | 2.5x | + +## Go + +| Framework | Status | Detection Pattern | Multiplier | +|-----------|--------|-------------------|------------| +| net/http | ✅ | `/handlers/*`, `main.go` | 2.5-3.0x | +| Gin/Echo | ✅ | `/handlers/*`, `/routes/*` | 2.5x | + +## Rust + +| Framework | Status | Detection Pattern | Multiplier | +|-----------|--------|-------------------|------------| +| Actix/Axum/Rocket | ✅ | `/handlers/*`, `main.rs` | 2.5-3.0x | + +## C / C++ + +| Framework | Status | Detection Pattern | Multiplier | +|-----------|--------|-------------------|------------| +| Generic | ✅ | `main.c`, `main.cpp` | 3.0x | + +--- + +## Adding New Framework Support + +1. Edit `framework-detection.ts` → `detectFrameworkFromPath()` +2. Add path pattern with appropriate multiplier +3. Update this documentation +4. Test with a sample project + +## Graceful Fallback + +Unknown frameworks return `null`, resulting in a **1.0x multiplier** (no bonus, no penalty). diff --git a/gitnexus/src/core/graph/types.ts b/gitnexus/src/core/graph/types.ts index 17a12388e..7bc9a5a95 100644 --- a/gitnexus/src/core/graph/types.ts +++ b/gitnexus/src/core/graph/types.ts @@ -38,6 +38,9 @@ export type NodeProperties = { communities?: string[], entryPointId?: string, terminalId?: string, + // Entry point scoring (computed by process detection) + entryPointScore?: number, + entryPointReason?: string, } export type RelationshipType = diff --git a/gitnexus/src/core/ingestion/entry-point-scoring.ts b/gitnexus/src/core/ingestion/entry-point-scoring.ts new file mode 100644 index 000000000..1ef3d3ddc --- /dev/null +++ b/gitnexus/src/core/ingestion/entry-point-scoring.ts @@ -0,0 +1,281 @@ +/** + * Entry Point Scoring + * + * Calculates entry point scores for process detection based on: + * 1. Call ratio (existing algorithm - callees / (callers + 1)) + * 2. Export status (exported functions get higher priority) + * 3. Name patterns (functions matching entry point patterns like handle*, on*, *Controller) + * 4. Framework detection (path-based detection for Next.js, Express, Django, etc.) + * + * This module is language-agnostic - language-specific patterns are defined per language. + */ + +import { detectFrameworkFromPath } from './framework-detection'; + +// ============================================================================ +// NAME PATTERNS - All 9 supported languages +// ============================================================================ + +/** + * Common entry point naming patterns by language + * These patterns indicate functions that are likely feature entry points + */ +const ENTRY_POINT_PATTERNS: Record = { + // Universal patterns (apply to all languages) + '*': [ + /^(main|init|bootstrap|start|run|setup|configure)$/i, + /^handle[A-Z]/, // handleLogin, handleSubmit + /^on[A-Z]/, // onClick, onSubmit + /Handler$/, // RequestHandler + /Controller$/, // UserController + /^process[A-Z]/, // processPayment + /^execute[A-Z]/, // executeQuery + /^perform[A-Z]/, // performAction + /^dispatch[A-Z]/, // dispatchEvent + /^trigger[A-Z]/, // triggerAction + /^fire[A-Z]/, // fireEvent + /^emit[A-Z]/, // emitEvent + ], + + // JavaScript/TypeScript + 'javascript': [ + /^use[A-Z]/, // React hooks (useEffect, etc.) + ], + 'typescript': [ + /^use[A-Z]/, // React hooks + ], + + // Python + 'python': [ + /^app$/, // Flask/FastAPI app + /^(get|post|put|delete|patch)_/i, // REST conventions + /^api_/, // API functions + /^view_/, // Django views + ], + + // Java + 'java': [ + /^do[A-Z]/, // doGet, doPost (Servlets) + /^create[A-Z]/, // Factory patterns + /^build[A-Z]/, // Builder patterns + /Service$/, // UserService + ], + + // C# + 'csharp': [ + /^(Get|Post|Put|Delete)/, // ASP.NET conventions + /Action$/, // MVC actions + /^On[A-Z]/, // Event handlers + /Async$/, // Async entry points + ], + + // Go + 'go': [ + /Handler$/, // http.Handler pattern + /^Serve/, // ServeHTTP + /^New[A-Z]/, // Constructor pattern (returns new instance) + /^Make[A-Z]/, // Make functions + ], + + // Rust + 'rust': [ + /^(get|post|put|delete)_handler$/i, + /^handle_/, // handle_request + /^new$/, // Constructor pattern + /^run$/, // run entry point + /^spawn/, // Async spawn + ], + + // C - explicit main() boost (critical for C programs) + 'c': [ + /^main$/, // THE entry point + /^init_/, // Initialization functions + /^start_/, // Start functions + /^run_/, // Run functions + ], + + // C++ - same as C plus class patterns + 'cpp': [ + /^main$/, // THE entry point + /^init_/, + /^Create[A-Z]/, // Factory patterns + /^Run$/, // Run methods + /^Start$/, // Start methods + ], +}; + +// ============================================================================ +// UTILITY PATTERNS - Functions that should be penalized +// ============================================================================ + +/** + * Patterns that indicate utility/helper functions (NOT entry points) + * These get penalized in scoring + */ +const UTILITY_PATTERNS: RegExp[] = [ + /^(get|set|is|has|can|should|will|did)[A-Z]/, // Accessors/predicates + /^_/, // Private by convention + /^(format|parse|validate|convert|transform)/i, // Transformation utilities + /^(log|debug|error|warn|info)$/i, // Logging + /^(to|from)[A-Z]/, // Conversions + /^(encode|decode)/i, // Encoding utilities + /^(serialize|deserialize)/i, // Serialization + /^(clone|copy|deep)/i, // Cloning utilities + /^(merge|extend|assign)/i, // Object utilities + /^(filter|map|reduce|sort|find)/i, // Collection utilities (standalone) + /Helper$/, + /Util$/, + /Utils$/, + /^utils?$/i, + /^helpers?$/i, +]; + +// ============================================================================ +// TYPES +// ============================================================================ + +export interface EntryPointScoreResult { + score: number; + reasons: string[]; +} + +// ============================================================================ +// MAIN SCORING FUNCTION +// ============================================================================ + +/** + * Calculate an entry point score for a function/method + * + * Higher scores indicate better entry point candidates. + * Score = baseScore × exportMultiplier × nameMultiplier + * + * @param name - Function/method name + * @param language - Programming language + * @param isExported - Whether the function is exported/public + * @param callerCount - Number of functions that call this function + * @param calleeCount - Number of functions this function calls + * @returns Score and array of reasons explaining the score + */ +export function calculateEntryPointScore( + name: string, + language: string, + isExported: boolean, + callerCount: number, + calleeCount: number, + filePath: string = '' // Optional for backwards compatibility +): EntryPointScoreResult { + const reasons: string[] = []; + + // Must have outgoing calls to be an entry point (we need to trace forward) + if (calleeCount === 0) { + return { score: 0, reasons: ['no-outgoing-calls'] }; + } + + // Base score: call ratio (existing algorithm) + // High ratio = calls many, called by few = likely entry point + const baseScore = calleeCount / (callerCount + 1); + reasons.push(`base:${baseScore.toFixed(2)}`); + + // Export bonus: exported/public functions are more likely entry points + const exportMultiplier = isExported ? 2.0 : 1.0; + if (isExported) { + reasons.push('exported'); + } + + // Name pattern scoring + let nameMultiplier = 1.0; + + // Check negative patterns first (utilities get penalized) + if (UTILITY_PATTERNS.some(p => p.test(name))) { + nameMultiplier = 0.3; // Significant penalty + reasons.push('utility-pattern'); + } else { + // Check positive patterns + const universalPatterns = ENTRY_POINT_PATTERNS['*'] || []; + const langPatterns = ENTRY_POINT_PATTERNS[language] || []; + const allPatterns = [...universalPatterns, ...langPatterns]; + + if (allPatterns.some(p => p.test(name))) { + nameMultiplier = 1.5; // Bonus for matching entry point pattern + reasons.push('entry-pattern'); + } + } + + // Framework detection bonus (Phase 2) + let frameworkMultiplier = 1.0; + if (filePath) { + const frameworkHint = detectFrameworkFromPath(filePath); + if (frameworkHint) { + frameworkMultiplier = frameworkHint.entryPointMultiplier; + reasons.push(`framework:${frameworkHint.reason}`); + } + } + + // Calculate final score + const finalScore = baseScore * exportMultiplier * nameMultiplier * frameworkMultiplier; + + return { + score: finalScore, + reasons, + }; +} + +// ============================================================================ +// HELPER FUNCTIONS +// ============================================================================ + +/** + * Check if a file path is a test file (should be excluded from entry points) + * Covers common test file patterns across all supported languages + */ +export function isTestFile(filePath: string): boolean { + const p = filePath.toLowerCase().replace(/\\/g, '/'); + + return ( + // JavaScript/TypeScript test patterns + p.includes('.test.') || + p.includes('.spec.') || + p.includes('__tests__/') || + p.includes('__mocks__/') || + // Generic test folders + p.includes('/test/') || + p.includes('/tests/') || + p.includes('/testing/') || + // Python test patterns + p.endsWith('_test.py') || + p.includes('/test_') || + // Go test patterns + p.endsWith('_test.go') || + // Java test patterns + p.includes('/src/test/') || + // Rust test patterns (inline tests are different, but test files) + p.includes('/tests/') || + // C# test patterns + p.includes('.tests/') || + p.includes('tests.cs') + ); +} + +/** + * Check if a file path is likely a utility/helper file + * These might still have entry points but should be lower priority + */ +export function isUtilityFile(filePath: string): boolean { + const p = filePath.toLowerCase().replace(/\\/g, '/'); + + return ( + p.includes('/utils/') || + p.includes('/util/') || + p.includes('/helpers/') || + p.includes('/helper/') || + p.includes('/common/') || + p.includes('/shared/') || + p.includes('/lib/') || + p.endsWith('/utils.ts') || + p.endsWith('/utils.js') || + p.endsWith('/helpers.ts') || + p.endsWith('/helpers.js') || + p.endsWith('_utils.py') || + p.endsWith('_helpers.py') + ); +} diff --git a/gitnexus/src/core/ingestion/framework-detection.ts b/gitnexus/src/core/ingestion/framework-detection.ts new file mode 100644 index 000000000..d3c75ab87 --- /dev/null +++ b/gitnexus/src/core/ingestion/framework-detection.ts @@ -0,0 +1,243 @@ +/** + * Framework Detection + * + * Detects frameworks from file path patterns and provides entry point multipliers. + * This enables framework-aware entry point scoring. + * + * DESIGN: Returns null for unknown frameworks, which causes a 1.0 multiplier + * (no bonus, no penalty) - same behavior as before this feature. + */ + +// ============================================================================ +// TYPES +// ============================================================================ + +export interface FrameworkHint { + framework: string; + entryPointMultiplier: number; + reason: string; +} + +// ============================================================================ +// PATH-BASED FRAMEWORK DETECTION +// ============================================================================ + +/** + * Detect framework from file path patterns + * + * This provides entry point multipliers based on well-known framework conventions. + * Returns null if no framework pattern is detected (falls back to 1.0 multiplier). + */ +export function detectFrameworkFromPath(filePath: string): FrameworkHint | null { + // Normalize path separators and ensure leading slash for consistent matching + let p = filePath.toLowerCase().replace(/\\/g, '/'); + if (!p.startsWith('/')) { + p = '/' + p; // Add leading slash so patterns like '/app/' match 'app/...' + } + + // ========== JAVASCRIPT / TYPESCRIPT FRAMEWORKS ========== + + // Next.js - Pages Router (high confidence) + if (p.includes('/pages/') && !p.includes('/_') && !p.includes('/api/')) { + if (p.endsWith('.tsx') || p.endsWith('.ts') || p.endsWith('.jsx') || p.endsWith('.js')) { + return { framework: 'nextjs-pages', entryPointMultiplier: 3.0, reason: 'nextjs-page' }; + } + } + + // Next.js - App Router (page.tsx files) + if (p.includes('/app/') && ( + p.endsWith('page.tsx') || p.endsWith('page.ts') || + p.endsWith('page.jsx') || p.endsWith('page.js') + )) { + return { framework: 'nextjs-app', entryPointMultiplier: 3.0, reason: 'nextjs-app-page' }; + } + + // Next.js - API Routes + if (p.includes('/pages/api/') || (p.includes('/app/') && p.includes('/api/') && p.endsWith('route.ts'))) { + return { framework: 'nextjs-api', entryPointMultiplier: 3.0, reason: 'nextjs-api-route' }; + } + + // Next.js - Layout files (moderate - they're entry-ish but not the main entry) + if (p.includes('/app/') && (p.endsWith('layout.tsx') || p.endsWith('layout.ts'))) { + return { framework: 'nextjs-app', entryPointMultiplier: 2.0, reason: 'nextjs-layout' }; + } + + // Express / Node.js routes + if (p.includes('/routes/') && (p.endsWith('.ts') || p.endsWith('.js'))) { + return { framework: 'express', entryPointMultiplier: 2.5, reason: 'routes-folder' }; + } + + // Generic controllers (MVC pattern) + if (p.includes('/controllers/') && (p.endsWith('.ts') || p.endsWith('.js'))) { + return { framework: 'mvc', entryPointMultiplier: 2.5, reason: 'controllers-folder' }; + } + + // Generic handlers + if (p.includes('/handlers/') && (p.endsWith('.ts') || p.endsWith('.js'))) { + return { framework: 'handlers', entryPointMultiplier: 2.5, reason: 'handlers-folder' }; + } + + // React components (lower priority - not all are entry points) + if ((p.includes('/components/') || p.includes('/views/')) && + (p.endsWith('.tsx') || p.endsWith('.jsx'))) { + // Only boost if PascalCase filename (likely a component, not util) + const fileName = p.split('/').pop() || ''; + if (/^[A-Z]/.test(fileName)) { + return { framework: 'react', entryPointMultiplier: 1.5, reason: 'react-component' }; + } + } + + // ========== PYTHON FRAMEWORKS ========== + + // Django views (high confidence) + if (p.endsWith('views.py')) { + return { framework: 'django', entryPointMultiplier: 3.0, reason: 'django-views' }; + } + + // Django URL configs + if (p.endsWith('urls.py')) { + return { framework: 'django', entryPointMultiplier: 2.0, reason: 'django-urls' }; + } + + // FastAPI / Flask routers + if ((p.includes('/routers/') || p.includes('/endpoints/') || p.includes('/routes/')) && + p.endsWith('.py')) { + return { framework: 'fastapi', entryPointMultiplier: 2.5, reason: 'api-routers' }; + } + + // Python API folder + if (p.includes('/api/') && p.endsWith('.py') && !p.endsWith('__init__.py')) { + return { framework: 'python-api', entryPointMultiplier: 2.0, reason: 'api-folder' }; + } + + // ========== JAVA FRAMEWORKS ========== + + // Spring Boot controllers + if ((p.includes('/controller/') || p.includes('/controllers/')) && p.endsWith('.java')) { + return { framework: 'spring', entryPointMultiplier: 3.0, reason: 'spring-controller' }; + } + + // Spring Boot - files ending in Controller.java + if (p.endsWith('controller.java')) { + return { framework: 'spring', entryPointMultiplier: 3.0, reason: 'spring-controller-file' }; + } + + // Java service layer (often entry points for business logic) + if ((p.includes('/service/') || p.includes('/services/')) && p.endsWith('.java')) { + return { framework: 'java-service', entryPointMultiplier: 1.8, reason: 'java-service' }; + } + + // ========== C# / .NET FRAMEWORKS ========== + + // ASP.NET Controllers + if (p.includes('/controllers/') && p.endsWith('.cs')) { + return { framework: 'aspnet', entryPointMultiplier: 3.0, reason: 'aspnet-controller' }; + } + + // ASP.NET - files ending in Controller.cs + if (p.endsWith('controller.cs')) { + return { framework: 'aspnet', entryPointMultiplier: 3.0, reason: 'aspnet-controller-file' }; + } + + // Blazor pages + if (p.includes('/pages/') && p.endsWith('.razor')) { + return { framework: 'blazor', entryPointMultiplier: 2.5, reason: 'blazor-page' }; + } + + // ========== GO FRAMEWORKS ========== + + // Go handlers + if ((p.includes('/handlers/') || p.includes('/handler/')) && p.endsWith('.go')) { + return { framework: 'go-http', entryPointMultiplier: 2.5, reason: 'go-handlers' }; + } + + // Go routes + if (p.includes('/routes/') && p.endsWith('.go')) { + return { framework: 'go-http', entryPointMultiplier: 2.5, reason: 'go-routes' }; + } + + // Go controllers + if (p.includes('/controllers/') && p.endsWith('.go')) { + return { framework: 'go-mvc', entryPointMultiplier: 2.5, reason: 'go-controller' }; + } + + // Go main.go files (THE entry point) + if (p.endsWith('/main.go') || p.endsWith('/cmd/') && p.endsWith('.go')) { + return { framework: 'go', entryPointMultiplier: 3.0, reason: 'go-main' }; + } + + // ========== RUST FRAMEWORKS ========== + + // Rust handlers/routes + if ((p.includes('/handlers/') || p.includes('/routes/')) && p.endsWith('.rs')) { + return { framework: 'rust-web', entryPointMultiplier: 2.5, reason: 'rust-handlers' }; + } + + // Rust main.rs (THE entry point) + if (p.endsWith('/main.rs')) { + return { framework: 'rust', entryPointMultiplier: 3.0, reason: 'rust-main' }; + } + + // Rust bin folder (executables) + if (p.includes('/bin/') && p.endsWith('.rs')) { + return { framework: 'rust', entryPointMultiplier: 2.5, reason: 'rust-bin' }; + } + + // ========== C / C++ ========== + + // C/C++ main files + if (p.endsWith('/main.c') || p.endsWith('/main.cpp') || p.endsWith('/main.cc')) { + return { framework: 'c-cpp', entryPointMultiplier: 3.0, reason: 'c-main' }; + } + + // C/C++ src folder entry points (if named specifically) + if ((p.includes('/src/') && (p.endsWith('/app.c') || p.endsWith('/app.cpp')))) { + return { framework: 'c-cpp', entryPointMultiplier: 2.5, reason: 'c-app' }; + } + + // ========== GENERIC PATTERNS ========== + + // Any language: index files in API folders + if (p.includes('/api/') && ( + p.endsWith('/index.ts') || p.endsWith('/index.js') || + p.endsWith('/__init__.py') + )) { + return { framework: 'api', entryPointMultiplier: 1.8, reason: 'api-index' }; + } + + // No framework detected - return null for graceful fallback (1.0 multiplier) + return null; +} + +// ============================================================================ +// FUTURE: AST-BASED PATTERNS (for Phase 3) +// ============================================================================ + +/** + * Patterns that indicate entry points within code (for future AST-based detection) + * These would require parsing decorators/annotations in the code itself. + */ +export const FRAMEWORK_AST_PATTERNS = { + // JavaScript/TypeScript decorators + 'nestjs': ['@Controller', '@Get', '@Post', '@Put', '@Delete', '@Patch'], + 'express': ['app.get', 'app.post', 'app.put', 'app.delete', 'router.get', 'router.post'], + + // Python decorators + 'fastapi': ['@app.get', '@app.post', '@app.put', '@app.delete', '@router.get'], + 'flask': ['@app.route', '@blueprint.route'], + + // Java annotations + 'spring': ['@RestController', '@Controller', '@GetMapping', '@PostMapping', '@RequestMapping'], + 'jaxrs': ['@Path', '@GET', '@POST', '@PUT', '@DELETE'], + + // C# attributes + 'aspnet': ['[ApiController]', '[HttpGet]', '[HttpPost]', '[Route]'], + + // Go patterns (function signatures) + 'go-http': ['http.Handler', 'http.HandlerFunc', 'ServeHTTP'], + + // Rust macros + 'actix': ['#[get', '#[post', '#[put', '#[delete'], + 'axum': ['Router::new'], + 'rocket': ['#[get', '#[post'], +}; diff --git a/gitnexus/src/core/ingestion/import-processor.ts b/gitnexus/src/core/ingestion/import-processor.ts index 0fffaf733..c0cb6bd68 100644 --- a/gitnexus/src/core/ingestion/import-processor.ts +++ b/gitnexus/src/core/ingestion/import-processor.ts @@ -93,18 +93,13 @@ const resolveImportPath = ( ); if (matchIdx !== -1) { const match = allFileList[matchIdx]; - if (import.meta.env.DEV) { - console.log(`📦 Import resolved: ${importPath} → ${match}`); - } resolveCache.set(cacheKey, match); return match; } } } - if (import.meta.env.DEV && pathParts.length > 0) { - console.log(`⚠️ Import unresolved: ${importPath} (tried suffixes from ${pathParts.join('/')})`); - } + // Unresolved imports (external packages, SDK imports) are expected - don't log resolveCache.set(cacheKey, null); return null; }; @@ -156,12 +151,7 @@ export const processImports = async ( query = parser.getLanguage().query(queryStr); matches = query.matches(tree.rootNode); - if (import.meta.env.DEV && language === 'java') { - const importMatches = matches.filter(m => m.captures.some(c => c.name === 'import')); - if (importMatches.length > 0) { - console.log(`📋 Java file ${file.path}: ${importMatches.length} import matches found`); - } - } + // Removed verbose Java import logging } catch (queryError: any) { // Detailed debug logging for query failures console.group(`🔴 Query Error: ${file.path}`); @@ -194,9 +184,7 @@ export const processImports = async ( const rawImportPath = sourceNode.text.replace(/['"]/g, ''); totalImportsFound++; - if (import.meta.env.DEV && language === 'java') { - console.log(`🔍 Java import found in ${file.path}: ${rawImportPath}`); - } + // Removed verbose per-import logging // Resolve to actual file in the system const resolvedPath = resolveImportPath( diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index 79d53216e..807bcf581 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -8,6 +8,108 @@ import { getLanguageFromFilename } from './utils'; export type FileProgressCallback = (current: number, total: number, filePath: string) => void; +// ============================================================================ +// EXPORT DETECTION - Language-specific visibility detection +// ============================================================================ + +/** + * Check if a symbol (function, class, etc.) is exported/public + * Handles all 9 supported languages with explicit logic + * + * @param node - The AST node for the symbol name + * @param name - The symbol name + * @param language - The programming language + * @returns true if the symbol is exported/public + */ +const isNodeExported = (node: any, name: string, language: string): boolean => { + let current = node; + + switch (language) { + // JavaScript/TypeScript: Check for export keyword in ancestors + case 'javascript': + case 'typescript': + while (current) { + const type = current.type; + if (type === 'export_statement' || + type === 'export_specifier' || + type === 'lexical_declaration' && current.parent?.type === 'export_statement') { + return true; + } + // Also check if text starts with 'export ' + if (current.text?.startsWith('export ')) { + return true; + } + current = current.parent; + } + return false; + + // Python: Public if no leading underscore (convention) + case 'python': + return !name.startsWith('_'); + + // Java: Check for 'public' modifier + // In tree-sitter Java, modifiers are siblings of the name node, not parents + case 'java': + while (current) { + // Check if this node or any sibling is a 'modifiers' node containing 'public' + if (current.parent) { + const parent = current.parent; + // Check all children of the parent for modifiers + for (let i = 0; i < parent.childCount; i++) { + const child = parent.child(i); + if (child?.type === 'modifiers' && child.text?.includes('public')) { + return true; + } + } + // Also check if the parent's text starts with 'public' (fallback) + if (parent.type === 'method_declaration' || parent.type === 'constructor_declaration') { + if (parent.text?.trimStart().startsWith('public')) { + return true; + } + } + } + current = current.parent; + } + return false; + + // C#: Check for 'public' modifier in ancestors + case 'csharp': + while (current) { + if (current.type === 'modifier' || current.type === 'modifiers') { + if (current.text?.includes('public')) return true; + } + current = current.parent; + } + return false; + + // Go: Uppercase first letter = exported + case 'go': + if (name.length === 0) return false; + const first = name[0]; + // Must be uppercase letter (not a number or symbol) + return first === first.toUpperCase() && first !== first.toLowerCase(); + + // Rust: Check for 'pub' visibility modifier + case 'rust': + while (current) { + if (current.type === 'visibility_modifier') { + if (current.text?.includes('pub')) return true; + } + current = current.parent; + } + return false; + + // C/C++: No native export concept at language level + // Entry points will be detected via name patterns (main, etc.) + case 'c': + case 'cpp': + return false; + + default: + return false; + } +}; + export const processParsing = async ( graph: KnowledgeGraph, files: { path: string; content: string }[], @@ -123,7 +225,8 @@ export const processParsing = async ( filePath: file.path, startLine: nameNode.startPosition.row, endLine: nameNode.endPosition.row, - language: language + language: language, + isExported: isNodeExported(nameNode, nodeName, language), } }; diff --git a/gitnexus/src/core/ingestion/process-processor.ts b/gitnexus/src/core/ingestion/process-processor.ts index 654eae992..cf983d2e6 100644 --- a/gitnexus/src/core/ingestion/process-processor.ts +++ b/gitnexus/src/core/ingestion/process-processor.ts @@ -12,6 +12,7 @@ import { KnowledgeGraph, GraphNode, GraphRelationship, NodeLabel } from '../graph/types'; import { CommunityMembership } from './community-processor'; +import { calculateEntryPointScore, isTestFile } from './entry-point-scoring'; // ============================================================================ // CONFIGURATION @@ -236,11 +237,12 @@ const buildReverseCallsGraph = (graph: KnowledgeGraph): AdjacencyList => { /** * Find functions/methods that are good entry points for tracing. * - * An entry point is a function that: - * 1. Has outgoing CALLS (so we can trace forward) - * 2. Ranked by having high outgoing/incoming call ratio + * Entry points are scored based on: + * 1. Call ratio (calls many, called by few) + * 2. Export status (exported/public functions rank higher) + * 3. Name patterns (handle*, on*, *Controller, etc.) * - * We prioritize functions that call many others but are called by few. + * Test files are excluded entirely. */ const findEntryPoints = ( graph: KnowledgeGraph, @@ -248,39 +250,53 @@ const findEntryPoints = ( callsEdges: AdjacencyList ): string[] => { const symbolTypes = new Set(['Function', 'Method']); - const entryPointCandidates: { id: string; score: number; callers: number; callees: number }[] = []; + const entryPointCandidates: { + id: string; + score: number; + reasons: string[]; + }[] = []; graph.nodes.forEach(node => { - if (symbolTypes.has(node.label)) { - const callers = reverseCallsEdges.get(node.id) || []; - const callees = callsEdges.get(node.id) || []; - - // Must have at least 1 outgoing call to trace forward - if (callees.length > 0) { - // Score: ratio of outgoing to incoming calls - // Higher ratio = better entry point (calls many, called by few) - // Add 1 to denominators to avoid division by zero - const score = callees.length / (callers.length + 1); - - entryPointCandidates.push({ - id: node.id, - score, - callers: callers.length, - callees: callees.length - }); - } + if (!symbolTypes.has(node.label)) return; + + const filePath = node.properties.filePath || ''; + + // Skip test files entirely + if (isTestFile(filePath)) return; + + const callers = reverseCallsEdges.get(node.id) || []; + const callees = callsEdges.get(node.id) || []; + + // Must have at least 1 outgoing call to trace forward + if (callees.length === 0) return; + + // Calculate entry point score using new scoring system + const { score, reasons } = calculateEntryPointScore( + node.properties.name, + node.properties.language || 'javascript', + node.properties.isExported ?? false, + callers.length, + callees.length, + filePath // Pass filePath for framework detection + ); + + if (score > 0) { + entryPointCandidates.push({ id: node.id, score, reasons }); } }); // Sort by score descending and return top candidates const sorted = entryPointCandidates.sort((a, b) => b.score - a.score); - // DEBUG: Log top candidates - if (sorted.length > 0) { - console.log(`[Process Debug] Top 5 entry point candidates:`); - sorted.slice(0, 5).forEach((c, i) => { + // DEBUG: Log top candidates with new scoring details + if (sorted.length > 0 && typeof import.meta !== 'undefined' && import.meta.env?.DEV) { + console.log(`[Process] Top 10 entry point candidates (new scoring):`); + sorted.slice(0, 10).forEach((c, i) => { const node = graph.nodes.find(n => n.id === c.id); - console.log(` ${i+1}. ${node?.properties.name} - calls: ${c.callees}, callers: ${c.callers}, score: ${c.score.toFixed(2)}`); + const exported = node?.properties.isExported ? '✓' : '✗'; + const shortPath = node?.properties.filePath?.split('/').slice(-2).join('/') || ''; + console.log(` ${i+1}. ${node?.properties.name} [exported:${exported}] (${shortPath})`); + console.log(` score: ${c.score.toFixed(2)} = [${c.reasons.join(' × ')}]`); }); } diff --git a/gitnexus/src/core/llm/agent.ts b/gitnexus/src/core/llm/agent.ts index 3769fc13d..8ec2cc9d0 100644 --- a/gitnexus/src/core/llm/agent.ts +++ b/gitnexus/src/core/llm/agent.ts @@ -81,6 +81,17 @@ You are an investigator. For each question: Nodes: File, Folder, Function, Class, Interface, Method, Community, Process Relations: \`CodeRelation\` with \`type\` property: CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS, MEMBER_OF, STEP_IN_PROCESS +## 📐 GRAPH SEMANTICS (Important!) +**Edge Types:** +- \`CALLS\`: Method invocation OR constructor injection. If A receives B as parameter and uses it, A→B is CALLS. This is intentional simplification. +- \`IMPORTS\`: File-level import/include statement. +- \`EXTENDS/IMPLEMENTS\`: Class inheritance. + +**Process Nodes:** +- Process labels use format: "EntryPoint → Terminal" (e.g., "onCreate → showToast") +- These are heuristic names from tracing execution flow, NOT application-defined names +- Entry points are detected via export status, naming patterns, and framework conventions + Cypher examples: - \`MATCH (f:Function) RETURN f.name LIMIT 10\` - \`MATCH (f:File)-[:CodeRelation {type: 'IMPORTS'}]->(g:File) RETURN f.name, g.name\` diff --git a/gitnexus/src/core/llm/tools.ts b/gitnexus/src/core/llm/tools.ts index 872914417..879e451fc 100644 --- a/gitnexus/src/core/llm/tools.ts +++ b/gitnexus/src/core/llm/tools.ts @@ -908,12 +908,23 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`, : 'Dependencies this RELIES ON'; // Try to find the target node first - const findTargetQuery = ` - MATCH (n) - WHERE n.name = '${target.replace(/'/g, "''")}' - RETURN n.id AS id, label(n) AS nodeType, n.filePath AS filePath - LIMIT 5 - `; + // If target contains '/', search by filePath; otherwise by name + const isPathQuery = target.includes('/'); + const escapedTarget = target.replace(/'/g, "''"); + + const findTargetQuery = isPathQuery + ? ` + MATCH (n) + WHERE n.filePath IS NOT NULL AND n.filePath CONTAINS '${escapedTarget}' + RETURN n.id AS id, label(n) AS nodeType, n.filePath AS filePath + LIMIT 10 + ` + : ` + MATCH (n) + WHERE n.name = '${escapedTarget}' + RETURN n.id AS id, label(n) AS nodeType, n.filePath AS filePath + LIMIT 10 + `; let targetResults; try {