From 91289404c2f51492ced8529eaab2195ba1a9bb7d Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Wed, 25 Feb 2026 17:10:51 +0530 Subject: [PATCH] =?UTF-8?q?feat(gitnexus):=20v1.2.9=20=E2=80=94=20impact?= =?UTF-8?q?=20enrichment,=20cypher=20markdown,=20Windows=20setup=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Impact tool now returns risk score, affected processes/modules, and summary - Cypher tool formats results as markdown tables for LLM readability - Context tool includes module (functional area) field - Semantic search skips model init when embeddings are disabled - Setup: wrap npx in cmd /c on Windows for .cmd script compatibility - Embedder: silence stderr during ONNX model load to protect MCP stdio - API: use executeCypher directly to avoid double formatting - Add community integrations section to READMEs Co-Authored-By: Claude Opus 4.6 --- AGENTS.md | 2 +- CLAUDE.md | 2 +- README.md | 6 ++ gitnexus/README.md | 6 ++ gitnexus/package-lock.json | 4 +- gitnexus/package.json | 2 +- gitnexus/src/cli/setup.ts | 9 +- gitnexus/src/mcp/core/embedder.ts | 12 ++- gitnexus/src/mcp/local/local-backend.ts | 127 ++++++++++++++++++++++-- gitnexus/src/mcp/tools.ts | 15 ++- gitnexus/src/server/api.ts | 6 +- 11 files changed, 166 insertions(+), 25 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 322c9439e..d5b788b41 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ # GitNexus MCP -This project is indexed by GitNexus as **GitnexusV2** (1309 symbols, 3350 relationships, 101 execution flows). +This project is indexed by GitNexus as **GitnexusV2** (1348 symbols, 3469 relationships, 104 execution flows). GitNexus provides a knowledge graph over this codebase — call chains, blast radius, execution flows, and semantic search. diff --git a/CLAUDE.md b/CLAUDE.md index b4f97d4c0..3fbcde034 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # GitNexus MCP -This project is indexed by GitNexus as **GitnexusV2** (1309 symbols, 3350 relationships, 101 execution flows). +This project is indexed by GitNexus as **GitnexusV2** (1348 symbols, 3469 relationships, 104 execution flows). GitNexus provides a knowledge graph over this codebase — call chains, blast radius, execution flows, and semantic search. diff --git a/README.md b/README.md index b36cd313b..2cf84826f 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,12 @@ To configure MCP for your editor, run `npx gitnexus setup` once — or set it up > **Claude Code** gets the deepest integration: MCP tools + agent skills + PreToolUse hooks that automatically enrich grep/glob/bash calls with knowledge graph context. +### Community Integrations + +| Agent | Install | Source | +|-------|---------|--------| +| [pi](https://pi.dev) | `pi install npm:pi-gitnexus` | [pi-gitnexus](https://github.com/tintinweb/pi-gitnexus) | + If you prefer manual configuration: **Claude Code** (full support — MCP + skills + hooks): diff --git a/gitnexus/README.md b/gitnexus/README.md index b69cb1a10..e6aa62940 100644 --- a/gitnexus/README.md +++ b/gitnexus/README.md @@ -39,6 +39,12 @@ To configure MCP for your editor, run `npx gitnexus setup` once — or set it up > **Claude Code** gets the deepest integration: MCP tools + agent skills + PreToolUse hooks that automatically enrich grep/glob/bash calls with knowledge graph context. +### Community Integrations + +| Agent | Install | Source | +|-------|---------|--------| +| [pi](https://pi.dev) | `pi install npm:pi-gitnexus` | [pi-gitnexus](https://github.com/tintinweb/pi-gitnexus) | + ## MCP Setup (manual) If you prefer to configure manually instead of using `gitnexus setup`: diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index 86415b4d9..b937d11b9 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -1,12 +1,12 @@ { "name": "gitnexus", - "version": "1.1.9", + "version": "1.2.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gitnexus", - "version": "1.1.9", + "version": "1.2.9", "license": "PolyForm-Noncommercial-1.0.0", "dependencies": { "@huggingface/transformers": "^3.0.0", diff --git a/gitnexus/package.json b/gitnexus/package.json index a4c3ea1ce..261be4bfc 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -1,6 +1,6 @@ { "name": "gitnexus", - "version": "1.2.8", + "version": "1.2.9", "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", diff --git a/gitnexus/src/cli/setup.ts b/gitnexus/src/cli/setup.ts index 77515a49a..a68b3ac0a 100644 --- a/gitnexus/src/cli/setup.ts +++ b/gitnexus/src/cli/setup.ts @@ -22,9 +22,16 @@ interface SetupResult { } /** - * The MCP server entry for all editors + * The MCP server entry for all editors. + * On Windows, npx must be invoked via cmd /c since it's a .cmd script. */ function getMcpEntry() { + if (process.platform === 'win32') { + return { + command: 'cmd', + args: ['/c', 'npx', '-y', 'gitnexus@latest', 'mcp'], + }; + } return { command: 'npx', args: ['-y', 'gitnexus@latest', 'mcp'], diff --git a/gitnexus/src/mcp/core/embedder.ts b/gitnexus/src/mcp/core/embedder.ts index 097b13fd7..ee480a6a9 100644 --- a/gitnexus/src/mcp/core/embedder.ts +++ b/gitnexus/src/mcp/core/embedder.ts @@ -43,10 +43,13 @@ export const initEmbedder = async (): Promise => { for (const device of devicesToTry) { try { - // Silence stdout during model load — ONNX Runtime and transformers.js - // may write progress/init messages to stdout which corrupts MCP stdio protocol. - const origWrite = process.stdout.write; + // Silence stdout and stderr during model load — ONNX Runtime and transformers.js + // may write progress/init messages that corrupt MCP stdio protocol or produce + // noisy warnings (e.g. node assignment to execution providers). + const origStdout = process.stdout.write; + const origStderr = process.stderr.write; process.stdout.write = (() => true) as any; + process.stderr.write = (() => true) as any; try { embedderInstance = await (pipeline as any)( 'feature-extraction', @@ -57,7 +60,8 @@ export const initEmbedder = async (): Promise => { } ); } finally { - process.stdout.write = origWrite; + process.stdout.write = origStdout; + process.stderr.write = origStderr; } console.error(`GitNexus: Embedding model loaded (${device})`); return embedderInstance!; diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index 9ae45ee6a..c47560d2b 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -279,8 +279,10 @@ export class LocalBackend { switch (method) { case 'query': return this.query(repo, params); - case 'cypher': - return this.cypher(repo, params); + case 'cypher': { + const raw = await this.cypher(repo, params); + return this.formatCypherAsMarkdown(raw); + } case 'context': return this.context(repo, params); case 'impact': @@ -395,16 +397,18 @@ export class LocalBackend { `); } catch { /* symbol might not be in any process */ } - // Get cluster cohesion as internal ranking signal (never exposed) + // Get cluster membership + cohesion (cohesion used as internal ranking signal) let cohesion = 0; + let module: string | undefined; try { const cohesionRows = await executeQuery(repo.id, ` MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) - RETURN c.cohesion AS cohesion + RETURN c.cohesion AS cohesion, c.heuristicLabel AS module LIMIT 1 `); if (cohesionRows.length > 0) { cohesion = (cohesionRows[0].cohesion ?? cohesionRows[0][0]) || 0; + module = cohesionRows[0].module ?? cohesionRows[0][1]; } } catch { /* no cluster info */ } @@ -429,6 +433,7 @@ export class LocalBackend { filePath: sym.filePath, startLine: sym.startLine, endLine: sym.endLine, + ...(module ? { module } : {}), ...(includeContent && content ? { content } : {}), }; @@ -577,6 +582,10 @@ export class LocalBackend { */ private async semanticSearch(repo: RepoHandle, query: string, limit: number): Promise { try { + // Check if embedding table exists before loading the model (avoids heavy model init when embeddings are off) + 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 queryVec = await embedQuery(query); const dims = getEmbeddingDims(); const queryVecStr = `[${queryVec.join(',')}]`; @@ -630,8 +639,8 @@ export class LocalBackend { } return results; - } catch (err: any) { - console.error('GitNexus: Semantic search unavailable -', err.message); + } catch { + // Expected when embeddings are disabled — silently fall back to BM25-only return []; } } @@ -643,11 +652,11 @@ export class LocalBackend { private async cypher(repo: RepoHandle, params: { query: string }): Promise { await this.ensureInitialized(repo.id); - + if (!isKuzuReady(repo.id)) { return { error: 'KuzuDB not ready. Index may be corrupted.' }; } - + try { const result = await executeQuery(repo.id, params.query); return result; @@ -656,6 +665,36 @@ export class LocalBackend { } } + /** + * Format raw Cypher result rows as a markdown table for LLM readability. + * Falls back to raw result if rows aren't tabular objects. + */ + private formatCypherAsMarkdown(result: any): any { + if (!Array.isArray(result) || result.length === 0) return result; + + const firstRow = result[0]; + if (typeof firstRow !== 'object' || firstRow === null) return result; + + const keys = Object.keys(firstRow); + if (keys.length === 0) return result; + + const header = '| ' + keys.join(' | ') + ' |'; + const separator = '| ' + keys.map(() => '---').join(' | ') + ' |'; + const dataRows = result.map((row: any) => + '| ' + keys.map(k => { + const v = row[k]; + if (v === null || v === undefined) return ''; + if (typeof v === 'object') return JSON.stringify(v); + return String(v); + }).join(' | ') + ' |' + ); + + return { + markdown: [header, separator, ...dataRows].join('\n'), + row_count: result.length, + }; + } + /** * Aggregate same-named clusters: group by heuristicLabel, sum symbols, * weighted-average cohesion, filter out tiny clusters (<5 symbols). @@ -1318,7 +1357,69 @@ export class LocalBackend { if (!grouped[item.depth]) grouped[item.depth] = []; grouped[item.depth].push(item); } - + + // ── Enrichment: affected processes, modules, risk ────────────── + const directCount = (grouped[1] || []).length; + let affectedProcesses: any[] = []; + let affectedModules: any[] = []; + + if (impacted.length > 0) { + const allIds = impacted.map(i => `'${i.id.replace(/'/g, "''")}'`).join(', '); + const d1Ids = (grouped[1] || []).map((i: any) => `'${i.id.replace(/'/g, "''")}'`).join(', '); + + // Affected processes: which execution flows are broken and at which step + const [processRows, moduleRows, directModuleRows] = await Promise.all([ + executeQuery(repo.id, ` + MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process) + WHERE s.id IN [${allIds}] + RETURN p.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits, MIN(r.step) AS minStep, p.stepCount AS stepCount + ORDER BY hits DESC + LIMIT 20 + `).catch(() => []), + executeQuery(repo.id, ` + MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) + WHERE s.id IN [${allIds}] + RETURN c.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits + ORDER BY hits DESC + LIMIT 20 + `).catch(() => []), + d1Ids ? executeQuery(repo.id, ` + MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) + WHERE s.id IN [${d1Ids}] + RETURN DISTINCT c.heuristicLabel AS name + `).catch(() => []) : Promise.resolve([]), + ]); + + affectedProcesses = processRows.map((r: any) => ({ + name: r.name || r[0], + hits: r.hits || r[1], + broken_at_step: r.minStep ?? r[2], + step_count: r.stepCount ?? r[3], + })); + + const directModuleSet = new Set(directModuleRows.map((r: any) => r.name || r[0])); + affectedModules = moduleRows.map((r: any) => { + const name = r.name || r[0]; + return { + name, + hits: r.hits || r[1], + impact: directModuleSet.has(name) ? 'direct' : 'indirect', + }; + }); + } + + // Risk scoring + const processCount = affectedProcesses.length; + const moduleCount = affectedModules.length; + let risk = 'LOW'; + if (directCount >= 30 || processCount >= 5 || moduleCount >= 5 || impacted.length >= 200) { + risk = 'CRITICAL'; + } else if (directCount >= 15 || processCount >= 3 || moduleCount >= 3 || impacted.length >= 100) { + risk = 'HIGH'; + } else if (directCount >= 5 || impacted.length >= 30) { + risk = 'MEDIUM'; + } + return { target: { id: symId, @@ -1328,6 +1429,14 @@ export class LocalBackend { }, direction, impactedCount: impacted.length, + risk, + summary: { + direct: directCount, + processes_affected: processCount, + modules_affected: moduleCount, + }, + affected_processes: affectedProcesses, + affected_modules: affectedModules, byDepth: grouped, }; } diff --git a/gitnexus/src/mcp/tools.ts b/gitnexus/src/mcp/tools.ts index 20be63278..9e2fe7eed 100644 --- a/gitnexus/src/mcp/tools.ts +++ b/gitnexus/src/mcp/tools.ts @@ -49,7 +49,7 @@ AFTER THIS: Use context() on a specific symbol for 360-degree view (callers, cal Returns results grouped by process (execution flow): - processes: ranked execution flows with relevance priority -- process_symbols: all symbols in those flows with file locations +- process_symbols: all symbols in those flows with file locations and module (functional area) - definitions: standalone types/interfaces not in any process Hybrid ranking: BM25 keyword + semantic vector search, ranked by Reciprocal Rank Fusion.`, @@ -91,6 +91,8 @@ EXAMPLES: • Trace a process: MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process) WHERE p.heuristicLabel = "UserLogin" RETURN s.name, r.step ORDER BY r.step +OUTPUT: Returns { markdown, row_count } — results formatted as a Markdown table for easy reading. + TIPS: - All relationships use single CodeRelation table — filter with {type: 'CALLS'} etc. - Community = auto-detected functional area (Leiden algorithm) @@ -172,10 +174,17 @@ Each edit is tagged with confidence: { name: 'impact', description: `Analyze the blast radius of changing a code symbol. -Returns all symbols affected by modifying the target, grouped by depth with edge types and confidence. +Returns affected symbols grouped by depth, plus risk assessment, affected execution flows, and affected modules. WHEN TO USE: Before making code changes — especially refactoring, renaming, or modifying shared code. Shows what would break. -AFTER THIS: Review d=1 items (WILL BREAK). READ gitnexus://repo/{name}/processes to check affected execution flows. +AFTER THIS: Review d=1 items (WILL BREAK). Use context() on high-risk symbols. + +Output includes: +- risk: LOW / MEDIUM / HIGH / CRITICAL +- summary: direct callers, processes affected, modules affected +- affected_processes: which execution flows break and at which step +- affected_modules: which functional areas are hit (direct vs indirect) +- byDepth: all affected symbols grouped by traversal depth Depth groups: - d=1: WILL BREAK (direct callers/importers) diff --git a/gitnexus/src/server/api.ts b/gitnexus/src/server/api.ts index fbe0ebcd6..eb690a4d9 100644 --- a/gitnexus/src/server/api.ts +++ b/gitnexus/src/server/api.ts @@ -191,9 +191,9 @@ export const createServer = async (port: number) => { return; } - const result = await backend.callTool('cypher', { repo: repoName, query: cypher }); - if (result && !Array.isArray(result) && result.error) { - res.status(500).json({ error: result.error }); + const result = await backend.executeCypher(repoName, cypher); + if (result && !Array.isArray(result) && (result as any).error) { + res.status(500).json({ error: (result as any).error }); return; } res.json({ result });