From 73590b286246fdbf778713a61b5742f39ebfac00 Mon Sep 17 00:00:00 2001 From: Tim Strazzere Date: Tue, 24 Feb 2026 13:42:29 -0800 Subject: [PATCH 1/9] fix: ensure exec usage does not allow poisoning Previous usage was vulnerable to "poisoned" tags which could enduce commands to be run when a `detectChanges` command was hit. This was primarily fixed in `local-backend.ts` however I changes the `execSync` usages where any injection was potentially able to be performed (e.g. staleness). Skipped touching wiki and generator as those use static input, though these should potentially be changed over in the future. --- gitnexus/src/cli/wiki.ts | 11 +++++---- gitnexus/src/core/wiki/generator.ts | 6 ++--- gitnexus/src/mcp/local/local-backend.ts | 32 +++++++++++++++---------- gitnexus/src/mcp/staleness.ts | 6 ++--- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/gitnexus/src/cli/wiki.ts b/gitnexus/src/cli/wiki.ts index ac626fe80..70ab00785 100644 --- a/gitnexus/src/cli/wiki.ts +++ b/gitnexus/src/cli/wiki.ts @@ -7,7 +7,7 @@ import path from 'path'; import readline from 'readline'; -import { execSync } from 'child_process'; +import { execSync, execFileSync } from 'child_process'; import cliProgress from 'cli-progress'; import { getGitRoot, isGitRepo } from '../storage/git.js'; import { getStoragePaths, loadMeta, loadCLIConfig, saveCLIConfig } from '../storage/repo-manager.js'; @@ -343,10 +343,11 @@ function hasGhCLI(): boolean { function publishGist(htmlPath: string): { url: string; rawUrl: string } | null { try { - const output = execSync( - `gh gist create "${htmlPath}" --desc "Repository Wiki — generated by GitNexus" --public`, - { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }, - ).trim(); + const output = execFileSync('gh', [ + 'gist', 'create', htmlPath, + '--desc', 'Repository Wiki — generated by GitNexus', + '--public', + ], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); // gh gist create prints the gist URL as the last line const lines = output.split('\n'); diff --git a/gitnexus/src/core/wiki/generator.ts b/gitnexus/src/core/wiki/generator.ts index 29a16a541..666dc6e46 100644 --- a/gitnexus/src/core/wiki/generator.ts +++ b/gitnexus/src/core/wiki/generator.ts @@ -12,7 +12,7 @@ import fs from 'fs/promises'; import path from 'path'; -import { execSync } from 'child_process'; +import { execSync, execFileSync } from 'child_process'; import { initWikiDb, @@ -712,8 +712,8 @@ export class WikiGenerator { private getChangedFiles(fromCommit: string, toCommit: string): string[] { try { - const output = execSync( - `git diff ${fromCommit}..${toCommit} --name-only`, + const output = execFileSync( + 'git', ['diff', `${fromCommit}..${toCommit}`, '--name-only'], { cwd: this.repoPath }, ).toString().trim(); return output ? output.split('\n').filter(Boolean) : []; diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index 9ae45ee6a..4f7bbfd8c 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -988,30 +988,30 @@ export class LocalBackend { await this.ensureInitialized(repo.id); const scope = params.scope || 'unstaged'; - const { execSync } = await import('child_process'); - - // Build git diff command based on scope - let diffCmd: string; + const { execFileSync } = await import('child_process'); + + // Build git diff args based on scope (using execFileSync to avoid shell injection) + let diffArgs: string[]; switch (scope) { case 'staged': - diffCmd = 'git diff --staged --name-only'; + diffArgs = ['diff', '--staged', '--name-only']; break; case 'all': - diffCmd = 'git diff HEAD --name-only'; + diffArgs = ['diff', 'HEAD', '--name-only']; break; case 'compare': if (!params.base_ref) return { error: 'base_ref is required for "compare" scope' }; - diffCmd = `git diff ${params.base_ref} --name-only`; + diffArgs = ['diff', params.base_ref, '--name-only']; break; case 'unstaged': default: - diffCmd = 'git diff --name-only'; + diffArgs = ['diff', '--name-only']; break; } - + let changedFiles: string[]; try { - const output = execSync(diffCmd, { cwd: repo.repoPath, encoding: 'utf-8' }); + const output = execFileSync('git', diffArgs, { cwd: repo.repoPath, encoding: 'utf-8' }); changedFiles = output.trim().split('\n').filter(f => f.length > 0); } catch (err: any) { return { error: `Git diff failed: ${err.message}` }; @@ -1185,9 +1185,15 @@ export class LocalBackend { // Simple text search across the repo for the old name (in files not already covered by graph) try { - const { execSync } = await import('child_process'); - const rgCmd = `rg -l --type-add "code:*.{ts,tsx,js,jsx,py,go,rs,java}" -t code "\\b${oldName}\\b" .`; - const output = execSync(rgCmd, { cwd: repo.repoPath, encoding: 'utf-8', timeout: 5000 }); + const { execFileSync } = await import('child_process'); + const rgArgs = [ + '-l', + '--type-add', 'code:*.{ts,tsx,js,jsx,py,go,rs,java}', + '-t', 'code', + `\\b${oldName}\\b`, + '.', + ]; + const output = execFileSync('rg', rgArgs, { cwd: repo.repoPath, encoding: 'utf-8', timeout: 5000 }); const files = output.trim().split('\n').filter(f => f.length > 0); for (const file of files) { diff --git a/gitnexus/src/mcp/staleness.ts b/gitnexus/src/mcp/staleness.ts index 0b7cf2a15..8c044e61d 100644 --- a/gitnexus/src/mcp/staleness.ts +++ b/gitnexus/src/mcp/staleness.ts @@ -5,7 +5,7 @@ * Returns a hint for the LLM to call analyze if stale. */ -import { execSync } from 'child_process'; +import { execFileSync } from 'child_process'; import path from 'path'; export interface StalenessInfo { @@ -20,8 +20,8 @@ export interface StalenessInfo { export function checkStaleness(repoPath: string, lastCommit: string): StalenessInfo { try { // Get count of commits between lastCommit and HEAD - const result = execSync( - `git rev-list --count ${lastCommit}..HEAD`, + const result = execFileSync( + 'git', ['rev-list', '--count', `${lastCommit}..HEAD`], { cwd: repoPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] } ).trim(); From 50fc8df2a1f412f335dd56f04b27963050f39924 Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Fri, 27 Feb 2026 05:08:45 +0530 Subject: [PATCH 2/9] fix(web): replace stale isBackendMode ref with serverBaseUrl PR 66 refactored isBackendMode to serverBaseUrl in useAppState but missed updating EmbeddingStatus.tsx, causing TypeScript build failure on Vercel. Co-Authored-By: Claude Opus 4.6 --- gitnexus-web/src/components/EmbeddingStatus.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gitnexus-web/src/components/EmbeddingStatus.tsx b/gitnexus-web/src/components/EmbeddingStatus.tsx index ab706b99a..e5a0e5418 100644 --- a/gitnexus-web/src/components/EmbeddingStatus.tsx +++ b/gitnexus-web/src/components/EmbeddingStatus.tsx @@ -14,7 +14,7 @@ export const EmbeddingStatus = () => { startEmbeddings, graph, viewMode, - isBackendMode, + serverBaseUrl, testArrayParams, } = useAppState(); @@ -22,7 +22,7 @@ export const EmbeddingStatus = () => { const [showFallbackDialog, setShowFallbackDialog] = useState(false); // Only show when exploring a loaded graph; hide in backend mode (no WASM DB) - if (viewMode !== 'exploring' || !graph || isBackendMode) return null; + if (viewMode !== 'exploring' || !graph || serverBaseUrl) return null; const nodeCount = graph.nodes.length; From a8b3c6b23fb2b88ae6b78c2a7cd1c8470f25cf7f Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Fri, 27 Feb 2026 10:36:35 +0530 Subject: [PATCH 3/9] fix(mcp): don't crash server when no repos are indexed (#91) The MCP server called process.exit(1) at startup when no repositories were found in the registry. This prevented users from configuring the MCP integration before running `gitnexus analyze`. The server now starts gracefully with 0 repos and discovers newly indexed repos lazily via refreshRepos() on each tool call. Closes #91 Co-Authored-By: Claude Opus 4.6 --- gitnexus/src/cli/mcp.ts | 35 ++++++++++------------------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/gitnexus/src/cli/mcp.ts b/gitnexus/src/cli/mcp.ts index 90c55b9bd..933356ff4 100644 --- a/gitnexus/src/cli/mcp.ts +++ b/gitnexus/src/cli/mcp.ts @@ -8,7 +8,6 @@ import { startMCPServer } from '../mcp/server.js'; import { LocalBackend } from '../mcp/local/local-backend.js'; -import { listRegisteredRepos } from '../storage/repo-manager.js'; export const mcpCommand = async () => { // Prevent unhandled errors from crashing the MCP server process. @@ -21,33 +20,19 @@ export const mcpCommand = async () => { console.error(`GitNexus MCP: unhandled rejection — ${msg}`); }); - // Load all registered repos - const entries = await listRegisteredRepos({ validate: true }); - - if (entries.length === 0) { - console.error(''); - console.error(' GitNexus: No indexed repositories found.'); - console.error(''); - console.error(' To get started:'); - console.error(' 1. cd into a git repository'); - console.error(' 2. Run: gitnexus analyze'); - console.error(' 3. Restart your editor'); - console.error(''); - process.exit(1); - } - - // Initialize multi-repo backend from registry + // Initialize multi-repo backend from registry. + // The server starts even with 0 repos — tools call refreshRepos() lazily, + // so repos indexed after the server starts are discovered automatically. const backend = new LocalBackend(); - const ok = await backend.init(); + await backend.init(); - if (!ok) { - console.error('GitNexus: Failed to initialize backend from registry.'); - process.exit(1); + const repos = await backend.listRepos(); + if (repos.length === 0) { + console.error('GitNexus: No indexed repos yet. Run `gitnexus analyze` in a git repo — the server will pick it up automatically.'); + } else { + console.error(`GitNexus: MCP server starting with ${repos.length} repo(s): ${repos.map(r => r.name).join(', ')}`); } - const repoNames = (await backend.listRepos()).map(r => r.name); - console.error(`GitNexus: MCP server starting with ${repoNames.length} repo(s): ${repoNames.join(', ')}`); - - // Start MCP server (serves all repos) + // Start MCP server (serves all repos, discovers new ones lazily) await startMCPServer(backend); }; From 5c3a32d0c69636a235cc13697efc4d98ea9040a9 Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Fri, 27 Feb 2026 11:31:07 +0530 Subject: [PATCH 4/9] fix(kuzu): remove duplicate ftsLoaded declaration that broke typecheck The module-level `let ftsLoaded` was declared twice (line 19 and 679), causing TS2451. Removed the duplicate and cleaned up redundant assignments in loadFTSExtension. Co-Authored-By: Claude Opus 4.6 --- gitnexus/src/core/kuzu/kuzu-adapter.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gitnexus/src/core/kuzu/kuzu-adapter.ts b/gitnexus/src/core/kuzu/kuzu-adapter.ts index 1ba30d15b..3f20f9084 100644 --- a/gitnexus/src/core/kuzu/kuzu-adapter.ts +++ b/gitnexus/src/core/kuzu/kuzu-adapter.ts @@ -674,22 +674,18 @@ export const getEmbeddingTableName = (): string => EMBEDDING_TABLE_NAME; /** * Load the FTS extension (required before using FTS functions). - * Safe to call multiple times — tracks loaded state. + * Safe to call multiple times — tracks loaded state via module-level ftsLoaded. */ -let ftsLoaded = false; export const loadFTSExtension = async (): Promise => { if (ftsLoaded) return; if (!conn) { throw new Error('KuzuDB not initialized. Call initKuzu first.'); } - if (ftsLoaded) return; try { await conn.query('INSTALL fts'); await conn.query('LOAD EXTENSION fts'); - ftsLoaded = true; } catch { // Extension may already be loaded - ftsLoaded = true; } ftsLoaded = true; }; From 989673a624314e7c0c25251ad00e7d3b3f74eae6 Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Fri, 27 Feb 2026 12:09:17 +0530 Subject: [PATCH 5/9] fix: lazy-import embeddings to avoid onnxruntime crash on unsupported Node versions Convert static imports of @huggingface/transformers (which triggers onnxruntime-node native binary loading) to dynamic import() calls. This prevents crashes on Node versions whose ABI isn't supported by the prebuilt onnxruntime binaries (e.g. Node v24). Affected entry points: - cli/analyze.ts: embedding pipeline only loaded when --embeddings is passed - mcp/local/local-backend.ts: embedder only loaded on first semantic search - server/api.ts: embedder only loaded when search endpoint needs embeddings Fixes #89 Co-Authored-By: Claude Opus 4.6 --- gitnexus/src/cli/analyze.ts | 5 ++++- gitnexus/src/mcp/local/local-backend.ts | 5 ++++- gitnexus/src/server/api.ts | 6 ++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/gitnexus/src/cli/analyze.ts b/gitnexus/src/cli/analyze.ts index 6ed6c05ef..f0c1ee320 100644 --- a/gitnexus/src/cli/analyze.ts +++ b/gitnexus/src/cli/analyze.ts @@ -10,7 +10,9 @@ import v8 from 'v8'; import cliProgress from 'cli-progress'; import { runPipelineFromRepo } from '../core/ingestion/pipeline.js'; import { initKuzu, loadGraphToKuzu, getKuzuStats, executeQuery, executeWithReusedStatement, closeKuzu, createFTSIndex, loadCachedEmbeddings } from '../core/kuzu/kuzu-adapter.js'; -import { runEmbeddingPipeline } from '../core/embeddings/embedding-pipeline.js'; +// Embedding imports are lazy (dynamic import) so onnxruntime-node is never +// loaded when embeddings are not requested. This avoids crashes on Node +// versions whose ABI is not yet supported by the native binary (#89). // disposeEmbedder intentionally not called — ONNX Runtime segfaults on cleanup (see #38) import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath } from '../storage/repo-manager.js'; import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js'; @@ -256,6 +258,7 @@ export const analyzeCommand = async ( if (!embeddingSkipped) { updateBar(90, 'Loading embedding model...'); const t0Emb = Date.now(); + const { runEmbeddingPipeline } = await import('../core/embeddings/embedding-pipeline.js'); await runEmbeddingPipeline( executeQuery, executeWithReusedStatement, diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index c47560d2b..99ab22c83 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -9,7 +9,8 @@ import fs from 'fs/promises'; import path from 'path'; import { initKuzu, executeQuery, closeKuzu, isKuzuReady } from '../core/kuzu-adapter.js'; -import { embedQuery, getEmbeddingDims, disposeEmbedder } from '../core/embedder.js'; +// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node +// at MCP server startup — crashes on unsupported Node ABI versions (#89) // git utilities available if needed // import { isGitRepo, getCurrentCommit, getGitRoot } from '../../storage/git.js'; import { @@ -586,6 +587,7 @@ export class LocalBackend { const tableCheck = await executeQuery(repo.id, `MATCH (e:CodeEmbedding) RETURN COUNT(*) AS cnt LIMIT 1`); if (!tableCheck.length || (tableCheck[0].cnt ?? tableCheck[0][0]) === 0) return []; + const { embedQuery, getEmbeddingDims } = await import('../core/embedder.js'); const queryVec = await embedQuery(query); const dims = getEmbeddingDims(); const queryVecStr = `[${queryVec.join(',')}]`; @@ -1590,6 +1592,7 @@ export class LocalBackend { async disconnect(): Promise { await closeKuzu(); // close all connections + const { disposeEmbedder } = await import('../core/embedder.js'); await disposeEmbedder(); this.repos.clear(); this.contextCache.clear(); diff --git a/gitnexus/src/server/api.ts b/gitnexus/src/server/api.ts index b0c9e9845..d587eff9a 100644 --- a/gitnexus/src/server/api.ts +++ b/gitnexus/src/server/api.ts @@ -18,8 +18,8 @@ import { NODE_TABLES } from '../core/kuzu/schema.js'; import { GraphNode, GraphRelationship } from '../core/graph/types.js'; import { searchFTSFromKuzu } from '../core/search/bm25-index.js'; import { hybridSearch } from '../core/search/hybrid-search.js'; -import { semanticSearch } from '../core/embeddings/embedding-pipeline.js'; -import { isEmbedderReady } from '../core/embeddings/embedder.js'; +// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node +// at server startup — crashes on unsupported Node ABI versions (#89) import { LocalBackend } from '../mcp/local/local-backend.js'; import { mountMCPEndpoints } from './mcp-http.js'; @@ -230,7 +230,9 @@ export const createServer = async (port: number, host: string = '127.0.0.1') => : 10; const results = await withKuzuDb(kuzuPath, async () => { + const { isEmbedderReady } = await import('../core/embeddings/embedder.js'); if (isEmbedderReady()) { + const { semanticSearch } = await import('../core/embeddings/embedding-pipeline.js'); return hybridSearch(query, limit, executeQuery, semanticSearch); } // FTS-only fallback when embeddings aren't loaded From de935a4f4c9acb08d4b2ef5588fd3f91af6cbec0 Mon Sep 17 00:00:00 2001 From: PurpleNewNew Date: Fri, 27 Feb 2026 15:40:42 +0800 Subject: [PATCH 6/9] feat(ingestion): add AST decorator-based entrypoint hints --- gitnexus/src/core/graph/types.ts | 5 +- .../src/core/ingestion/framework-detection.ts | 72 +++++++++++++++++-- .../src/core/ingestion/parsing-processor.ts | 48 ++++++++++++- .../src/core/ingestion/process-processor.ts | 9 ++- .../core/ingestion/workers/parse-worker.ts | 44 ++++++++++++ 5 files changed, 169 insertions(+), 9 deletions(-) diff --git a/gitnexus/src/core/graph/types.ts b/gitnexus/src/core/graph/types.ts index a5b32e9c6..c675bdf1d 100644 --- a/gitnexus/src/core/graph/types.ts +++ b/gitnexus/src/core/graph/types.ts @@ -42,6 +42,9 @@ export type NodeProperties = { endLine?: number, language?: string, isExported?: boolean, + // Optional AST-derived framework hint (e.g. @Controller, @GetMapping) + astFrameworkMultiplier?: number, + astFrameworkReason?: string, // Community-specific properties heuristicLabel?: string, cohesion?: number, @@ -113,4 +116,4 @@ export interface KnowledgeGraph { addRelationship: (relationship: GraphRelationship) => void, removeNode: (nodeId: string) => boolean, removeNodesByFile: (filePath: string) => number, -} \ No newline at end of file +} diff --git a/gitnexus/src/core/ingestion/framework-detection.ts b/gitnexus/src/core/ingestion/framework-detection.ts index 4aec27f7c..299966c7e 100644 --- a/gitnexus/src/core/ingestion/framework-detection.ts +++ b/gitnexus/src/core/ingestion/framework-detection.ts @@ -1,8 +1,10 @@ /** * Framework Detection * - * Detects frameworks from file path patterns and provides entry point multipliers. - * This enables framework-aware entry point scoring. + * Detects frameworks from: + * 1) file path patterns + * 2) AST definition text (decorators/annotations/attributes) + * and provides entry point multipliers for process scoring. * * DESIGN: Returns null for unknown frameworks, which causes a 1.0 multiplier * (no bonus, no penalty) - same behavior as before this feature. @@ -272,12 +274,12 @@ export function detectFrameworkFromPath(filePath: string): FrameworkHint | null } // ============================================================================ -// FUTURE: AST-BASED PATTERNS (for Phase 3) +// AST-BASED FRAMEWORK DETECTION // ============================================================================ /** - * Patterns that indicate entry points within code (for future AST-based detection) - * These would require parsing decorators/annotations in the code itself. + * Patterns that indicate framework entry points within code definitions. + * These are matched against AST node text (class/method/function declaration text). */ export const FRAMEWORK_AST_PATTERNS = { // JavaScript/TypeScript decorators @@ -307,3 +309,63 @@ export const FRAMEWORK_AST_PATTERNS = { 'axum': ['Router::new'], 'rocket': ['#[get', '#[post'], }; + +interface AstFrameworkPatternConfig { + framework: string; + entryPointMultiplier: number; + reason: string; + patterns: string[]; +} + +const AST_FRAMEWORK_PATTERNS_BY_LANGUAGE: Record = { + javascript: [ + { framework: 'nestjs', entryPointMultiplier: 3.2, reason: 'nestjs-decorator', patterns: FRAMEWORK_AST_PATTERNS.nestjs }, + ], + typescript: [ + { framework: 'nestjs', entryPointMultiplier: 3.2, reason: 'nestjs-decorator', patterns: FRAMEWORK_AST_PATTERNS.nestjs }, + ], + python: [ + { framework: 'fastapi', entryPointMultiplier: 3.0, reason: 'fastapi-decorator', patterns: FRAMEWORK_AST_PATTERNS.fastapi }, + { framework: 'flask', entryPointMultiplier: 2.8, reason: 'flask-decorator', patterns: FRAMEWORK_AST_PATTERNS.flask }, + ], + java: [ + { framework: 'spring', entryPointMultiplier: 3.2, reason: 'spring-annotation', patterns: FRAMEWORK_AST_PATTERNS.spring }, + { framework: 'jaxrs', entryPointMultiplier: 3.0, reason: 'jaxrs-annotation', patterns: FRAMEWORK_AST_PATTERNS.jaxrs }, + ], + csharp: [ + { framework: 'aspnet', entryPointMultiplier: 3.2, reason: 'aspnet-attribute', patterns: FRAMEWORK_AST_PATTERNS.aspnet }, + ], + php: [ + { framework: 'laravel', entryPointMultiplier: 3.0, reason: 'php-route-attribute', patterns: FRAMEWORK_AST_PATTERNS.laravel }, + ], +}; + +/** + * Detect framework entry points from AST definition text (decorators/annotations/attributes). + * Returns null if no known pattern is found. + */ +export function detectFrameworkFromAST( + language: string, + definitionText: string +): FrameworkHint | null { + if (!language || !definitionText) return null; + + const configs = AST_FRAMEWORK_PATTERNS_BY_LANGUAGE[language.toLowerCase()]; + if (!configs || configs.length === 0) return null; + + const normalized = definitionText.toLowerCase(); + + for (const cfg of configs) { + for (const pattern of cfg.patterns) { + if (normalized.includes(pattern.toLowerCase())) { + return { + framework: cfg.framework, + entryPointMultiplier: cfg.entryPointMultiplier, + reason: cfg.reason, + }; + } + } + } + + return null; +} diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index c15d39a17..7647f0c37 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -6,6 +6,7 @@ import { generateId } from '../../lib/utils.js'; import { SymbolTable } from './symbol-table.js'; import { ASTCache } from './ast-cache.js'; import { getLanguageFromFilename, yieldToEventLoop } from './utils.js'; +import { detectFrameworkFromAST } from './framework-detection.js'; import { WorkerPool } from './workers/worker-pool.js'; import type { ParseWorkerResult, ParseWorkerInput, ExtractedImport, ExtractedCall, ExtractedHeritage } from './workers/parse-worker.js'; @@ -17,6 +18,38 @@ export interface WorkerExtractedData { heritage: ExtractedHeritage[]; } +const getDefinitionNodeFromCaptures = (captureMap: Record): any | null => { + const definitionKeys = [ + 'definition.function', + 'definition.class', + 'definition.interface', + 'definition.method', + 'definition.struct', + 'definition.enum', + 'definition.namespace', + 'definition.module', + 'definition.trait', + 'definition.impl', + 'definition.type', + 'definition.const', + 'definition.static', + 'definition.typedef', + 'definition.macro', + 'definition.union', + 'definition.property', + 'definition.record', + 'definition.delegate', + 'definition.annotation', + 'definition.constructor', + 'definition.template', + ]; + + for (const key of definitionKeys) { + if (captureMap[key]) return captureMap[key]; + } + return null; +}; + // ============================================================================ // EXPORT DETECTION - Language-specific visibility detection // ============================================================================ @@ -287,14 +320,25 @@ const processParsingSequential = async ( const node: GraphNode = { id: nodeId, label: nodeLabel as any, - properties: { + properties: (() => { + const definitionNode = getDefinitionNodeFromCaptures(captureMap); + const frameworkHint = definitionNode + ? detectFrameworkFromAST(language, definitionNode.text || '') + : null; + + return { name: nodeName, filePath: file.path, startLine: nameNode.startPosition.row, endLine: nameNode.endPosition.row, language: language, isExported: isNodeExported(nameNode, nodeName, language), - } + ...(frameworkHint ? { + astFrameworkMultiplier: frameworkHint.entryPointMultiplier, + astFrameworkReason: frameworkHint.reason, + } : {}), + }; + })() }; graph.addNode(node); diff --git a/gitnexus/src/core/ingestion/process-processor.ts b/gitnexus/src/core/ingestion/process-processor.ts index 587aa3359..1c54001cb 100644 --- a/gitnexus/src/core/ingestion/process-processor.ts +++ b/gitnexus/src/core/ingestion/process-processor.ts @@ -285,7 +285,7 @@ const findEntryPoints = ( if (callees.length === 0) continue; // Calculate entry point score using new scoring system - const { score, reasons } = calculateEntryPointScore( + const { score: baseScore, reasons } = calculateEntryPointScore( node.properties.name, node.properties.language || 'javascript', node.properties.isExported ?? false, @@ -294,6 +294,13 @@ const findEntryPoints = ( filePath // Pass filePath for framework detection ); + let score = baseScore; + const astFrameworkMultiplier = node.properties.astFrameworkMultiplier ?? 1.0; + if (astFrameworkMultiplier > 1.0) { + score *= astFrameworkMultiplier; + reasons.push(`framework-ast:${node.properties.astFrameworkReason || 'decorator'}`); + } + if (score > 0) { entryPointCandidates.push({ id: node.id, score, reasons }); } diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index ff985ad4c..4b31fd9c3 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -13,6 +13,7 @@ import PHP from 'tree-sitter-php'; import { SupportedLanguages } from '../../../config/supported-languages.js'; import { LANGUAGE_QUERIES } from '../tree-sitter-queries.js'; import { getLanguageFromFilename } from '../utils.js'; +import { detectFrameworkFromAST } from '../framework-detection.js'; import { generateId } from '../../../lib/utils.js'; // ============================================================================ @@ -29,6 +30,8 @@ interface ParsedNode { endLine: number; language: string; isExported: boolean; + astFrameworkMultiplier?: number; + astFrameworkReason?: string; description?: string; }; } @@ -372,6 +375,38 @@ const getLabelFromCaptures = (captureMap: Record): string | null => return 'CodeElement'; }; +const getDefinitionNodeFromCaptures = (captureMap: Record): any | null => { + const definitionKeys = [ + 'definition.function', + 'definition.class', + 'definition.interface', + 'definition.method', + 'definition.struct', + 'definition.enum', + 'definition.namespace', + 'definition.module', + 'definition.trait', + 'definition.impl', + 'definition.type', + 'definition.const', + 'definition.static', + 'definition.typedef', + 'definition.macro', + 'definition.union', + 'definition.property', + 'definition.record', + 'definition.delegate', + 'definition.annotation', + 'definition.constructor', + 'definition.template', + ]; + + for (const key of definitionKeys) { + if (captureMap[key]) return captureMap[key]; + } + return null; +}; + // ============================================================================ // Process a batch of files // ============================================================================ @@ -666,6 +701,11 @@ const processFileGroup = ( } } + const definitionNode = getDefinitionNodeFromCaptures(captureMap); + const frameworkHint = definitionNode + ? detectFrameworkFromAST(language, definitionNode.text || '') + : null; + result.nodes.push({ id: nodeId, label: nodeLabel, @@ -676,6 +716,10 @@ const processFileGroup = ( endLine: nameNode.endPosition.row, language: language, isExported: isNodeExported(nameNode, nodeName, language), + ...(frameworkHint ? { + astFrameworkMultiplier: frameworkHint.entryPointMultiplier, + astFrameworkReason: frameworkHint.reason, + } : {}), ...(description !== undefined ? { description } : {}), }, }); From 0074fd71ff72d4e043b566764796bc9207af94ab Mon Sep 17 00:00:00 2001 From: christopher Date: Fri, 27 Feb 2026 14:26:36 +0800 Subject: [PATCH 7/9] fix(web): map API `path` field to `repoPath` in fetchRepoInfo The backend `/api/repo` endpoint returns `path` but `ServerRepoInfo` expects `repoPath`, causing `undefined.split('/')` crash in App.tsx when connecting to a local gitnexus serve instance. Fixes #92 --- gitnexus-web/src/services/server-connection.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gitnexus-web/src/services/server-connection.ts b/gitnexus-web/src/services/server-connection.ts index 7687a1fef..8c262000b 100644 --- a/gitnexus-web/src/services/server-connection.ts +++ b/gitnexus-web/src/services/server-connection.ts @@ -69,7 +69,9 @@ export async function fetchRepoInfo(baseUrl: string, repoName?: string): Promise if (!response.ok) { throw new Error(`Server returned ${response.status}: ${response.statusText}`); } - return response.json(); + const data = await response.json(); + // npm gitnexus@1.3.3 returns "path"; git HEAD returns "repoPath" + return { ...data, repoPath: data.repoPath ?? data.path }; } export async function fetchGraph( From 6b4f10cae1743723bf753e3240e59c063f2531cb Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Fri, 27 Feb 2026 17:41:06 +0530 Subject: [PATCH 8/9] fix: remove unconditional embedder import from disconnect() to prevent crash on Node v24+ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The disconnect() method was unconditionally importing embedder.js on every graceful shutdown, which loads @huggingface/transformers and onnxruntime-node — triggering the exact crash this branch fixes. Since process.exit(0) follows immediately, the OS reclaims all resources without needing disposeEmbedder(). Matches the pattern already established in analyze.ts (lines 318-320). Fixes #89 Co-Authored-By: Claude Opus 4.6 --- gitnexus/src/mcp/local/local-backend.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index 99ab22c83..8c721b646 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -1592,8 +1592,11 @@ export class LocalBackend { async disconnect(): Promise { await closeKuzu(); // close all connections - const { disposeEmbedder } = await import('../core/embedder.js'); - await disposeEmbedder(); + // Note: we intentionally do NOT call disposeEmbedder() here. + // ONNX Runtime's native cleanup segfaults on macOS and some Linux configs, + // and importing the embedder module on Node v24+ crashes if onnxruntime + // was never loaded during the session. Since process.exit(0) follows + // immediately after disconnect(), the OS reclaims everything. See #38, #89. this.repos.clear(); this.contextCache.clear(); this.initializedRepos.clear(); From c758f4eaf0fbfc8ee31baea859bb46fc5ea1f9d5 Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Sat, 28 Feb 2026 07:56:08 +0530 Subject: [PATCH 9/9] chore: bump version to 1.3.4 Co-Authored-By: Claude Opus 4.6 --- gitnexus/package-lock.json | 4 ++-- gitnexus/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index b2f14dea1..7361d8fca 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -1,12 +1,12 @@ { "name": "gitnexus", - "version": "1.3.3", + "version": "1.3.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gitnexus", - "version": "1.3.3", + "version": "1.3.4", "license": "PolyForm-Noncommercial-1.0.0", "dependencies": { "@huggingface/transformers": "^3.0.0", diff --git a/gitnexus/package.json b/gitnexus/package.json index 04b20e88a..44a78b131 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -1,6 +1,6 @@ { "name": "gitnexus", - "version": "1.3.3", + "version": "1.3.4", "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.", "author": "Abhigyan Patwari", "license": "PolyForm-Noncommercial-1.0.0",