gitnexus 1.2.8 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -186
- package/dist/cli/ai-context.js +71 -71
- package/dist/cli/analyze.js +69 -28
- package/dist/cli/index.js +20 -0
- package/dist/cli/setup.js +8 -1
- package/dist/cli/view.d.ts +13 -0
- package/dist/cli/view.js +59 -0
- package/dist/core/augmentation/engine.js +20 -20
- package/dist/core/embeddings/embedding-pipeline.js +26 -26
- package/dist/core/graph/graph.js +5 -0
- package/dist/core/graph/html-graph-viewer.d.ts +15 -0
- package/dist/core/graph/html-graph-viewer.js +542 -0
- package/dist/core/graph/html-graph-viewer.test.d.ts +1 -0
- package/dist/core/graph/html-graph-viewer.test.js +67 -0
- package/dist/core/graph/types.d.ts +12 -1
- package/dist/core/ingestion/call-processor.js +52 -32
- package/dist/core/ingestion/cluster-enricher.js +16 -16
- package/dist/core/ingestion/community-processor.js +75 -40
- package/dist/core/ingestion/filesystem-walker.d.ts +23 -0
- package/dist/core/ingestion/filesystem-walker.js +38 -3
- package/dist/core/ingestion/import-processor.d.ts +11 -3
- package/dist/core/ingestion/import-processor.js +27 -11
- package/dist/core/ingestion/parsing-processor.js +2 -4
- package/dist/core/ingestion/pipeline.js +142 -135
- package/dist/core/ingestion/process-processor.js +12 -11
- package/dist/core/ingestion/workers/parse-worker.js +67 -6
- package/dist/core/ingestion/workers/worker-pool.d.ts +3 -9
- package/dist/core/ingestion/workers/worker-pool.js +39 -18
- package/dist/core/kuzu/csv-generator.d.ts +15 -8
- package/dist/core/kuzu/csv-generator.js +258 -196
- package/dist/core/kuzu/kuzu-adapter.d.ts +1 -4
- package/dist/core/kuzu/kuzu-adapter.js +84 -72
- package/dist/core/kuzu/schema.d.ts +1 -1
- package/dist/core/kuzu/schema.js +266 -256
- package/dist/core/search/bm25-index.js +5 -5
- package/dist/core/search/hybrid-search.js +3 -3
- package/dist/core/wiki/graph-queries.js +52 -52
- package/dist/core/wiki/html-viewer.js +192 -192
- package/dist/core/wiki/prompts.js +82 -82
- package/dist/mcp/core/embedder.js +8 -4
- package/dist/mcp/local/local-backend.d.ts +6 -0
- package/dist/mcp/local/local-backend.js +224 -117
- package/dist/mcp/resources.js +42 -42
- package/dist/mcp/server.js +16 -16
- package/dist/mcp/tools.js +86 -77
- package/dist/server/api.d.ts +4 -2
- package/dist/server/api.js +253 -83
- package/dist/types/pipeline.d.ts +6 -2
- package/dist/types/pipeline.js +6 -4
- package/hooks/claude/gitnexus-hook.cjs +135 -135
- package/hooks/claude/pre-tool-use.sh +78 -78
- package/hooks/claude/session-start.sh +42 -42
- package/package.json +82 -82
- package/skills/debugging.md +85 -85
- package/skills/exploring.md +75 -75
- package/skills/impact-analysis.md +94 -94
- package/skills/refactoring.md +113 -113
- package/vendor/leiden/index.cjs +355 -355
- package/vendor/leiden/utils.cjs +392 -392
package/dist/cli/analyze.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Indexes a repository and stores the knowledge graph in .gitnexus/
|
|
5
5
|
*/
|
|
6
6
|
import path from 'path';
|
|
7
|
+
import { execFileSync } from 'child_process';
|
|
8
|
+
import v8 from 'v8';
|
|
7
9
|
import cliProgress from 'cli-progress';
|
|
8
10
|
import { runPipelineFromRepo } from '../core/ingestion/pipeline.js';
|
|
9
11
|
import { initKuzu, loadGraphToKuzu, getKuzuStats, executeQuery, executeWithReusedStatement, closeKuzu, createFTSIndex, loadCachedEmbeddings } from '../core/kuzu/kuzu-adapter.js';
|
|
@@ -14,6 +16,27 @@ import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
|
|
|
14
16
|
import { generateAIContextFiles } from './ai-context.js';
|
|
15
17
|
import fs from 'fs/promises';
|
|
16
18
|
import { registerClaudeHook } from './claude-hooks.js';
|
|
19
|
+
const HEAP_MB = 8192;
|
|
20
|
+
const HEAP_FLAG = `--max-old-space-size=${HEAP_MB}`;
|
|
21
|
+
/** Re-exec the process with an 8GB heap if we're currently below that. */
|
|
22
|
+
function ensureHeap() {
|
|
23
|
+
const nodeOpts = process.env.NODE_OPTIONS || '';
|
|
24
|
+
if (nodeOpts.includes('--max-old-space-size'))
|
|
25
|
+
return false;
|
|
26
|
+
const v8Heap = v8.getHeapStatistics().heap_size_limit;
|
|
27
|
+
if (v8Heap >= HEAP_MB * 1024 * 1024 * 0.9)
|
|
28
|
+
return false;
|
|
29
|
+
try {
|
|
30
|
+
execFileSync(process.execPath, [HEAP_FLAG, ...process.argv.slice(1)], {
|
|
31
|
+
stdio: 'inherit',
|
|
32
|
+
env: { ...process.env, NODE_OPTIONS: `${nodeOpts} ${HEAP_FLAG}`.trim() },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
process.exitCode = e.status ?? 1;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
17
40
|
/** Threshold: auto-skip embeddings for repos with more nodes than this */
|
|
18
41
|
const EMBEDDING_NODE_LIMIT = 50_000;
|
|
19
42
|
const PHASE_LABELS = {
|
|
@@ -32,6 +55,8 @@ const PHASE_LABELS = {
|
|
|
32
55
|
done: 'Done',
|
|
33
56
|
};
|
|
34
57
|
export const analyzeCommand = async (inputPath, options) => {
|
|
58
|
+
if (ensureHeap())
|
|
59
|
+
return;
|
|
35
60
|
console.log('\n GitNexus Analyzer\n');
|
|
36
61
|
let repoPath;
|
|
37
62
|
if (inputPath) {
|
|
@@ -70,18 +95,47 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
70
95
|
stopOnComplete: false,
|
|
71
96
|
}, cliProgress.Presets.shades_grey);
|
|
72
97
|
bar.start(100, 0, { phase: 'Initializing...' });
|
|
98
|
+
// Graceful SIGINT handling — clean up resources and exit
|
|
99
|
+
let aborted = false;
|
|
100
|
+
const sigintHandler = () => {
|
|
101
|
+
if (aborted)
|
|
102
|
+
process.exit(1); // Second Ctrl-C: force exit
|
|
103
|
+
aborted = true;
|
|
104
|
+
bar.stop();
|
|
105
|
+
console.log('\n Interrupted — cleaning up...');
|
|
106
|
+
closeKuzu().catch(() => { }).finally(() => process.exit(130));
|
|
107
|
+
};
|
|
108
|
+
process.on('SIGINT', sigintHandler);
|
|
73
109
|
// Route all console output through bar.log() so the bar doesn't stamp itself
|
|
74
110
|
// multiple times when other code writes to stdout/stderr mid-render.
|
|
75
111
|
const origLog = console.log.bind(console);
|
|
76
112
|
const origWarn = console.warn.bind(console);
|
|
77
113
|
const origError = console.error.bind(console);
|
|
78
|
-
const barLog = (...args) =>
|
|
114
|
+
const barLog = (...args) => {
|
|
115
|
+
// Clear the bar line, print the message, then let the next bar.update redraw
|
|
116
|
+
process.stdout.write('\x1b[2K\r');
|
|
117
|
+
origLog(args.map(a => (typeof a === 'string' ? a : String(a))).join(' '));
|
|
118
|
+
};
|
|
79
119
|
console.log = barLog;
|
|
80
120
|
console.warn = barLog;
|
|
81
121
|
console.error = barLog;
|
|
82
|
-
//
|
|
122
|
+
// Track elapsed time per phase — both updateBar and the interval use the
|
|
123
|
+
// same format so they don't flicker against each other.
|
|
83
124
|
let lastPhaseLabel = 'Initializing...';
|
|
84
125
|
let phaseStart = Date.now();
|
|
126
|
+
/** Update bar with phase label + elapsed seconds (shown after 3s). */
|
|
127
|
+
const updateBar = (value, phaseLabel) => {
|
|
128
|
+
if (phaseLabel !== lastPhaseLabel) {
|
|
129
|
+
lastPhaseLabel = phaseLabel;
|
|
130
|
+
phaseStart = Date.now();
|
|
131
|
+
}
|
|
132
|
+
const elapsed = Math.round((Date.now() - phaseStart) / 1000);
|
|
133
|
+
const display = elapsed >= 3 ? `${phaseLabel} (${elapsed}s)` : phaseLabel;
|
|
134
|
+
bar.update(value, { phase: display });
|
|
135
|
+
};
|
|
136
|
+
// Tick elapsed seconds for phases with infrequent progress callbacks
|
|
137
|
+
// (e.g. CSV streaming, FTS indexing). Uses the same display format as
|
|
138
|
+
// updateBar so there's no flickering.
|
|
85
139
|
const elapsedTimer = setInterval(() => {
|
|
86
140
|
const elapsed = Math.round((Date.now() - phaseStart) / 1000);
|
|
87
141
|
if (elapsed >= 3) {
|
|
@@ -94,7 +148,7 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
94
148
|
let cachedEmbeddings = [];
|
|
95
149
|
if (options?.embeddings && existingMeta && !options?.force) {
|
|
96
150
|
try {
|
|
97
|
-
|
|
151
|
+
updateBar(0, 'Caching embeddings...');
|
|
98
152
|
await initKuzu(kuzuPath);
|
|
99
153
|
const cached = await loadCachedEmbeddings();
|
|
100
154
|
cachedEmbeddingNodeIds = cached.embeddingNodeIds;
|
|
@@ -112,16 +166,10 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
112
166
|
const pipelineResult = await runPipelineFromRepo(repoPath, (progress) => {
|
|
113
167
|
const phaseLabel = PHASE_LABELS[progress.phase] || progress.phase;
|
|
114
168
|
const scaled = Math.round(progress.percent * 0.6);
|
|
115
|
-
|
|
116
|
-
lastPhaseLabel = phaseLabel;
|
|
117
|
-
phaseStart = Date.now();
|
|
118
|
-
}
|
|
119
|
-
bar.update(scaled, { phase: phaseLabel });
|
|
169
|
+
updateBar(scaled, phaseLabel);
|
|
120
170
|
});
|
|
121
171
|
// ── Phase 2: KuzuDB (60–85%) ──────────────────────────────────────
|
|
122
|
-
|
|
123
|
-
phaseStart = Date.now();
|
|
124
|
-
bar.update(60, { phase: lastPhaseLabel });
|
|
172
|
+
updateBar(60, 'Loading into KuzuDB...');
|
|
125
173
|
await closeKuzu();
|
|
126
174
|
const kuzuFiles = [kuzuPath, `${kuzuPath}.wal`, `${kuzuPath}.lock`];
|
|
127
175
|
for (const f of kuzuFiles) {
|
|
@@ -133,17 +181,15 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
133
181
|
const t0Kuzu = Date.now();
|
|
134
182
|
await initKuzu(kuzuPath);
|
|
135
183
|
let kuzuMsgCount = 0;
|
|
136
|
-
const kuzuResult = await loadGraphToKuzu(pipelineResult.graph, pipelineResult.
|
|
184
|
+
const kuzuResult = await loadGraphToKuzu(pipelineResult.graph, pipelineResult.repoPath, storagePath, (msg) => {
|
|
137
185
|
kuzuMsgCount++;
|
|
138
186
|
const progress = Math.min(84, 60 + Math.round((kuzuMsgCount / (kuzuMsgCount + 10)) * 24));
|
|
139
|
-
|
|
187
|
+
updateBar(progress, msg);
|
|
140
188
|
});
|
|
141
189
|
const kuzuTime = ((Date.now() - t0Kuzu) / 1000).toFixed(1);
|
|
142
190
|
const kuzuWarnings = kuzuResult.warnings;
|
|
143
191
|
// ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
|
|
144
|
-
|
|
145
|
-
phaseStart = Date.now();
|
|
146
|
-
bar.update(85, { phase: lastPhaseLabel });
|
|
192
|
+
updateBar(85, 'Creating search indexes...');
|
|
147
193
|
const t0Fts = Date.now();
|
|
148
194
|
try {
|
|
149
195
|
await createFTSIndex('File', 'file_fts', ['name', 'content']);
|
|
@@ -158,7 +204,7 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
158
204
|
const ftsTime = ((Date.now() - t0Fts) / 1000).toFixed(1);
|
|
159
205
|
// ── Phase 3.5: Re-insert cached embeddings ────────────────────────
|
|
160
206
|
if (cachedEmbeddings.length > 0) {
|
|
161
|
-
|
|
207
|
+
updateBar(88, `Restoring ${cachedEmbeddings.length} cached embeddings...`);
|
|
162
208
|
const EMBED_BATCH = 200;
|
|
163
209
|
for (let i = 0; i < cachedEmbeddings.length; i += EMBED_BATCH) {
|
|
164
210
|
const batch = cachedEmbeddings.slice(i, i + EMBED_BATCH);
|
|
@@ -183,29 +229,23 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
183
229
|
}
|
|
184
230
|
}
|
|
185
231
|
if (!embeddingSkipped) {
|
|
186
|
-
|
|
187
|
-
phaseStart = Date.now();
|
|
188
|
-
bar.update(90, { phase: lastPhaseLabel });
|
|
232
|
+
updateBar(90, 'Loading embedding model...');
|
|
189
233
|
const t0Emb = Date.now();
|
|
190
234
|
await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (progress) => {
|
|
191
235
|
const scaled = 90 + Math.round((progress.percent / 100) * 8);
|
|
192
236
|
const label = progress.phase === 'loading-model' ? 'Loading embedding model...' : `Embedding ${progress.nodesProcessed || 0}/${progress.totalNodes || '?'}`;
|
|
193
|
-
|
|
194
|
-
lastPhaseLabel = label;
|
|
195
|
-
phaseStart = Date.now();
|
|
196
|
-
}
|
|
197
|
-
bar.update(scaled, { phase: label });
|
|
237
|
+
updateBar(scaled, label);
|
|
198
238
|
}, {}, cachedEmbeddingNodeIds.size > 0 ? cachedEmbeddingNodeIds : undefined);
|
|
199
239
|
embeddingTime = ((Date.now() - t0Emb) / 1000).toFixed(1);
|
|
200
240
|
}
|
|
201
241
|
// ── Phase 5: Finalize (98–100%) ───────────────────────────────────
|
|
202
|
-
|
|
242
|
+
updateBar(98, 'Saving metadata...');
|
|
203
243
|
const meta = {
|
|
204
244
|
repoPath,
|
|
205
245
|
lastCommit: currentCommit,
|
|
206
246
|
indexedAt: new Date().toISOString(),
|
|
207
247
|
stats: {
|
|
208
|
-
files: pipelineResult.
|
|
248
|
+
files: pipelineResult.totalFileCount,
|
|
209
249
|
nodes: stats.nodes,
|
|
210
250
|
edges: stats.edges,
|
|
211
251
|
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
|
@@ -227,7 +267,7 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
227
267
|
aggregatedClusterCount = Array.from(groups.values()).filter(count => count >= 5).length;
|
|
228
268
|
}
|
|
229
269
|
const aiContext = await generateAIContextFiles(repoPath, storagePath, projectName, {
|
|
230
|
-
files: pipelineResult.
|
|
270
|
+
files: pipelineResult.totalFileCount,
|
|
231
271
|
nodes: stats.nodes,
|
|
232
272
|
edges: stats.edges,
|
|
233
273
|
communities: pipelineResult.communityResult?.stats.totalCommunities,
|
|
@@ -240,6 +280,7 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
240
280
|
// Since the process exits immediately after, Node.js reclaims everything.
|
|
241
281
|
const totalTime = ((Date.now() - t0Global) / 1000).toFixed(1);
|
|
242
282
|
clearInterval(elapsedTimer);
|
|
283
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
243
284
|
console.log = origLog;
|
|
244
285
|
console.warn = origWarn;
|
|
245
286
|
console.error = origError;
|
package/dist/cli/index.js
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// Raise Node heap limit for large repos (e.g. Linux kernel).
|
|
3
|
+
// Must run before any heavy allocation. If already set by the user, respect it.
|
|
4
|
+
if (!process.env.NODE_OPTIONS?.includes('--max-old-space-size')) {
|
|
5
|
+
const execArgv = process.execArgv.join(' ');
|
|
6
|
+
if (!execArgv.includes('--max-old-space-size')) {
|
|
7
|
+
// Re-spawn with a larger heap (8 GB)
|
|
8
|
+
const { execFileSync } = await import('node:child_process');
|
|
9
|
+
try {
|
|
10
|
+
execFileSync(process.execPath, ['--max-old-space-size=8192', ...process.argv.slice(1)], {
|
|
11
|
+
stdio: 'inherit',
|
|
12
|
+
env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --max-old-space-size=8192`.trim() },
|
|
13
|
+
});
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
// If the child exited with an error code, propagate it
|
|
18
|
+
process.exit(e.status ?? 1);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
2
22
|
import { Command } from 'commander';
|
|
3
23
|
import { analyzeCommand } from './analyze.js';
|
|
4
24
|
import { serveCommand } from './serve.js';
|
package/dist/cli/setup.js
CHANGED
|
@@ -13,9 +13,16 @@ import { getGlobalDir } from '../storage/repo-manager.js';
|
|
|
13
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
14
|
const __dirname = path.dirname(__filename);
|
|
15
15
|
/**
|
|
16
|
-
* The MCP server entry for all editors
|
|
16
|
+
* The MCP server entry for all editors.
|
|
17
|
+
* On Windows, npx must be invoked via cmd /c since it's a .cmd script.
|
|
17
18
|
*/
|
|
18
19
|
function getMcpEntry() {
|
|
20
|
+
if (process.platform === 'win32') {
|
|
21
|
+
return {
|
|
22
|
+
command: 'cmd',
|
|
23
|
+
args: ['/c', 'npx', '-y', 'gitnexus@latest', 'mcp'],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
19
26
|
return {
|
|
20
27
|
command: 'npx',
|
|
21
28
|
args: ['-y', 'gitnexus@latest', 'mcp'],
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View Command
|
|
3
|
+
*
|
|
4
|
+
* Generates a self-contained graph.html from the KuzuDB index and
|
|
5
|
+
* opens it in the default browser.
|
|
6
|
+
*
|
|
7
|
+
* Usage: gitnexus view [path] [--no-open]
|
|
8
|
+
*/
|
|
9
|
+
export interface ViewCommandOptions {
|
|
10
|
+
noOpen?: boolean;
|
|
11
|
+
output?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const viewCommand: (inputPath?: string, options?: ViewCommandOptions) => Promise<void>;
|
package/dist/cli/view.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View Command
|
|
3
|
+
*
|
|
4
|
+
* Generates a self-contained graph.html from the KuzuDB index and
|
|
5
|
+
* opens it in the default browser.
|
|
6
|
+
*
|
|
7
|
+
* Usage: gitnexus view [path] [--no-open]
|
|
8
|
+
*/
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import { exec } from 'child_process';
|
|
12
|
+
import { findRepo } from '../storage/repo-manager.js';
|
|
13
|
+
import { initKuzu } from '../core/kuzu/kuzu-adapter.js';
|
|
14
|
+
import { buildGraph } from '../server/api.js';
|
|
15
|
+
import { generateHTMLGraphViewer } from '../core/graph/html-graph-viewer.js';
|
|
16
|
+
import { getCurrentCommit } from '../storage/git.js';
|
|
17
|
+
function openInBrowser(filePath) {
|
|
18
|
+
const url = `file://${filePath}`;
|
|
19
|
+
let cmd;
|
|
20
|
+
if (process.platform === 'darwin') {
|
|
21
|
+
cmd = `open "${url}"`;
|
|
22
|
+
}
|
|
23
|
+
else if (process.platform === 'win32') {
|
|
24
|
+
cmd = `start "" "${url}"`;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
cmd = `xdg-open "${url}"`;
|
|
28
|
+
}
|
|
29
|
+
exec(cmd, (err) => {
|
|
30
|
+
if (err)
|
|
31
|
+
console.error('Failed to open browser:', err.message);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export const viewCommand = async (inputPath, options) => {
|
|
35
|
+
console.log('⚠ Experimental: gitnexus view is under active development.\n');
|
|
36
|
+
const repoPath = inputPath ? path.resolve(inputPath) : process.cwd();
|
|
37
|
+
const repo = await findRepo(repoPath);
|
|
38
|
+
if (!repo) {
|
|
39
|
+
console.error('No index found. Run: gitnexus analyze');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const currentCommit = getCurrentCommit(repo.repoPath);
|
|
43
|
+
if (currentCommit !== repo.meta.lastCommit) {
|
|
44
|
+
console.warn('Index is stale — showing last indexed state. Run: gitnexus analyze\n');
|
|
45
|
+
}
|
|
46
|
+
await initKuzu(repo.kuzuPath);
|
|
47
|
+
const { nodes, relationships } = await buildGraph();
|
|
48
|
+
const projectName = path.basename(repo.repoPath);
|
|
49
|
+
const outputPath = options?.output
|
|
50
|
+
? path.resolve(options.output)
|
|
51
|
+
: path.join(repo.storagePath, 'graph.html');
|
|
52
|
+
const html = generateHTMLGraphViewer(nodes, relationships, projectName);
|
|
53
|
+
await fs.writeFile(outputPath, html, 'utf-8');
|
|
54
|
+
console.log(`Graph written to: ${outputPath}`);
|
|
55
|
+
console.log(`Nodes: ${nodes.length} Edges: ${relationships.length}`);
|
|
56
|
+
if (!options?.noOpen) {
|
|
57
|
+
openInBrowser(outputPath);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
@@ -98,11 +98,11 @@ export async function augment(pattern, cwd) {
|
|
|
98
98
|
for (const result of bm25Results.slice(0, 5)) {
|
|
99
99
|
const escaped = result.filePath.replace(/'/g, "''");
|
|
100
100
|
try {
|
|
101
|
-
const symbols = await executeQuery(repoId, `
|
|
102
|
-
MATCH (n) WHERE n.filePath = '${escaped}'
|
|
103
|
-
AND n.name CONTAINS '${pattern.replace(/'/g, "''").split(/\s+/)[0]}'
|
|
104
|
-
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
105
|
-
LIMIT 3
|
|
101
|
+
const symbols = await executeQuery(repoId, `
|
|
102
|
+
MATCH (n) WHERE n.filePath = '${escaped}'
|
|
103
|
+
AND n.name CONTAINS '${pattern.replace(/'/g, "''").split(/\s+/)[0]}'
|
|
104
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
105
|
+
LIMIT 3
|
|
106
106
|
`);
|
|
107
107
|
for (const sym of symbols) {
|
|
108
108
|
symbolMatches.push({
|
|
@@ -130,10 +130,10 @@ export async function augment(pattern, cwd) {
|
|
|
130
130
|
// Callers
|
|
131
131
|
let callers = [];
|
|
132
132
|
try {
|
|
133
|
-
const rows = await executeQuery(repoId, `
|
|
134
|
-
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(n {id: '${escaped}'})
|
|
135
|
-
RETURN caller.name AS name
|
|
136
|
-
LIMIT 3
|
|
133
|
+
const rows = await executeQuery(repoId, `
|
|
134
|
+
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(n {id: '${escaped}'})
|
|
135
|
+
RETURN caller.name AS name
|
|
136
|
+
LIMIT 3
|
|
137
137
|
`);
|
|
138
138
|
callers = rows.map((r) => r.name || r[0]).filter(Boolean);
|
|
139
139
|
}
|
|
@@ -141,10 +141,10 @@ export async function augment(pattern, cwd) {
|
|
|
141
141
|
// Callees
|
|
142
142
|
let callees = [];
|
|
143
143
|
try {
|
|
144
|
-
const rows = await executeQuery(repoId, `
|
|
145
|
-
MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'CALLS'}]->(callee)
|
|
146
|
-
RETURN callee.name AS name
|
|
147
|
-
LIMIT 3
|
|
144
|
+
const rows = await executeQuery(repoId, `
|
|
145
|
+
MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'CALLS'}]->(callee)
|
|
146
|
+
RETURN callee.name AS name
|
|
147
|
+
LIMIT 3
|
|
148
148
|
`);
|
|
149
149
|
callees = rows.map((r) => r.name || r[0]).filter(Boolean);
|
|
150
150
|
}
|
|
@@ -152,9 +152,9 @@ export async function augment(pattern, cwd) {
|
|
|
152
152
|
// Processes
|
|
153
153
|
let processes = [];
|
|
154
154
|
try {
|
|
155
|
-
const rows = await executeQuery(repoId, `
|
|
156
|
-
MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
157
|
-
RETURN p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
|
|
155
|
+
const rows = await executeQuery(repoId, `
|
|
156
|
+
MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
157
|
+
RETURN p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
|
|
158
158
|
`);
|
|
159
159
|
processes = rows.map((r) => {
|
|
160
160
|
const label = r.label || r[0];
|
|
@@ -167,10 +167,10 @@ export async function augment(pattern, cwd) {
|
|
|
167
167
|
// Cluster cohesion (internal ranking signal)
|
|
168
168
|
let cohesion = 0;
|
|
169
169
|
try {
|
|
170
|
-
const rows = await executeQuery(repoId, `
|
|
171
|
-
MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
172
|
-
RETURN c.cohesion AS cohesion
|
|
173
|
-
LIMIT 1
|
|
170
|
+
const rows = await executeQuery(repoId, `
|
|
171
|
+
MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
172
|
+
RETURN c.cohesion AS cohesion
|
|
173
|
+
LIMIT 1
|
|
174
174
|
`);
|
|
175
175
|
if (rows.length > 0) {
|
|
176
176
|
cohesion = (rows[0].cohesion ?? rows[0][0]) || 0;
|
|
@@ -24,19 +24,19 @@ const queryEmbeddableNodes = async (executeQuery) => {
|
|
|
24
24
|
let query;
|
|
25
25
|
if (label === 'File') {
|
|
26
26
|
// File nodes don't have startLine/endLine
|
|
27
|
-
query = `
|
|
28
|
-
MATCH (n:File)
|
|
29
|
-
RETURN n.id AS id, n.name AS name, 'File' AS label,
|
|
30
|
-
n.filePath AS filePath, n.content AS content
|
|
27
|
+
query = `
|
|
28
|
+
MATCH (n:File)
|
|
29
|
+
RETURN n.id AS id, n.name AS name, 'File' AS label,
|
|
30
|
+
n.filePath AS filePath, n.content AS content
|
|
31
31
|
`;
|
|
32
32
|
}
|
|
33
33
|
else {
|
|
34
34
|
// Code elements have startLine/endLine
|
|
35
|
-
query = `
|
|
36
|
-
MATCH (n:${label})
|
|
37
|
-
RETURN n.id AS id, n.name AS name, '${label}' AS label,
|
|
38
|
-
n.filePath AS filePath, n.content AS content,
|
|
39
|
-
n.startLine AS startLine, n.endLine AS endLine
|
|
35
|
+
query = `
|
|
36
|
+
MATCH (n:${label})
|
|
37
|
+
RETURN n.id AS id, n.name AS name, '${label}' AS label,
|
|
38
|
+
n.filePath AS filePath, n.content AS content,
|
|
39
|
+
n.startLine AS startLine, n.endLine AS endLine
|
|
40
40
|
`;
|
|
41
41
|
}
|
|
42
42
|
const rows = await executeQuery(query);
|
|
@@ -77,8 +77,8 @@ const batchInsertEmbeddings = async (executeWithReusedStatement, updates) => {
|
|
|
77
77
|
* Now indexes the separate CodeEmbedding table
|
|
78
78
|
*/
|
|
79
79
|
const createVectorIndex = async (executeQuery) => {
|
|
80
|
-
const cypher = `
|
|
81
|
-
CALL CREATE_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx', 'embedding', metric := 'cosine')
|
|
80
|
+
const cypher = `
|
|
81
|
+
CALL CREATE_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx', 'embedding', metric := 'cosine')
|
|
82
82
|
`;
|
|
83
83
|
try {
|
|
84
84
|
await executeQuery(cypher);
|
|
@@ -240,14 +240,14 @@ export const semanticSearch = async (executeQuery, query, k = 10, maxDistance =
|
|
|
240
240
|
const queryVec = embeddingToArray(queryEmbedding);
|
|
241
241
|
const queryVecStr = `[${queryVec.join(',')}]`;
|
|
242
242
|
// Query the vector index on CodeEmbedding to get nodeIds and distances
|
|
243
|
-
const vectorQuery = `
|
|
244
|
-
CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx',
|
|
245
|
-
CAST(${queryVecStr} AS FLOAT[384]), ${k})
|
|
246
|
-
YIELD node AS emb, distance
|
|
247
|
-
WITH emb, distance
|
|
248
|
-
WHERE distance < ${maxDistance}
|
|
249
|
-
RETURN emb.nodeId AS nodeId, distance
|
|
250
|
-
ORDER BY distance
|
|
243
|
+
const vectorQuery = `
|
|
244
|
+
CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx',
|
|
245
|
+
CAST(${queryVecStr} AS FLOAT[384]), ${k})
|
|
246
|
+
YIELD node AS emb, distance
|
|
247
|
+
WITH emb, distance
|
|
248
|
+
WHERE distance < ${maxDistance}
|
|
249
|
+
RETURN emb.nodeId AS nodeId, distance
|
|
250
|
+
ORDER BY distance
|
|
251
251
|
`;
|
|
252
252
|
const embResults = await executeQuery(vectorQuery);
|
|
253
253
|
if (embResults.length === 0) {
|
|
@@ -266,16 +266,16 @@ export const semanticSearch = async (executeQuery, query, k = 10, maxDistance =
|
|
|
266
266
|
try {
|
|
267
267
|
let nodeQuery;
|
|
268
268
|
if (label === 'File') {
|
|
269
|
-
nodeQuery = `
|
|
270
|
-
MATCH (n:File {id: '${nodeId.replace(/'/g, "''")}'})
|
|
271
|
-
RETURN n.name AS name, n.filePath AS filePath
|
|
269
|
+
nodeQuery = `
|
|
270
|
+
MATCH (n:File {id: '${nodeId.replace(/'/g, "''")}'})
|
|
271
|
+
RETURN n.name AS name, n.filePath AS filePath
|
|
272
272
|
`;
|
|
273
273
|
}
|
|
274
274
|
else {
|
|
275
|
-
nodeQuery = `
|
|
276
|
-
MATCH (n:${label} {id: '${nodeId.replace(/'/g, "''")}'})
|
|
277
|
-
RETURN n.name AS name, n.filePath AS filePath,
|
|
278
|
-
n.startLine AS startLine, n.endLine AS endLine
|
|
275
|
+
nodeQuery = `
|
|
276
|
+
MATCH (n:${label} {id: '${nodeId.replace(/'/g, "''")}'})
|
|
277
|
+
RETURN n.name AS name, n.filePath AS filePath,
|
|
278
|
+
n.startLine AS startLine, n.endLine AS endLine
|
|
279
279
|
`;
|
|
280
280
|
}
|
|
281
281
|
const nodeRows = await executeQuery(nodeQuery);
|
package/dist/core/graph/graph.js
CHANGED
|
@@ -46,6 +46,11 @@ export const createKnowledgeGraph = () => {
|
|
|
46
46
|
get relationships() {
|
|
47
47
|
return Array.from(relationshipMap.values());
|
|
48
48
|
},
|
|
49
|
+
iterNodes: () => nodeMap.values(),
|
|
50
|
+
iterRelationships: () => relationshipMap.values(),
|
|
51
|
+
forEachNode(fn) { nodeMap.forEach(fn); },
|
|
52
|
+
forEachRelationship(fn) { relationshipMap.forEach(fn); },
|
|
53
|
+
getNode: (id) => nodeMap.get(id),
|
|
49
54
|
// O(1) count getters - avoid creating arrays just for length
|
|
50
55
|
get nodeCount() {
|
|
51
56
|
return nodeMap.size;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Graph Viewer Generator
|
|
3
|
+
*
|
|
4
|
+
* Produces a self-contained graph.html that renders the knowledge graph
|
|
5
|
+
* using Sigma.js v2 + graphology (both from CDN).
|
|
6
|
+
*
|
|
7
|
+
* Critical: node `content` fields are stripped before embedding to prevent
|
|
8
|
+
* </script> injection from source code breaking the HTML parser.
|
|
9
|
+
*/
|
|
10
|
+
import { GraphNode, GraphRelationship } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Generate a self-contained HTML file that renders the knowledge graph.
|
|
13
|
+
* Strips large/unsafe fields from nodes before embedding.
|
|
14
|
+
*/
|
|
15
|
+
export declare function generateHTMLGraphViewer(nodes: GraphNode[], relationships: GraphRelationship[], projectName: string): string;
|