gitnexus 1.3.11 → 1.4.1
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 -194
- package/dist/cli/ai-context.js +87 -105
- package/dist/cli/analyze.js +0 -8
- package/dist/cli/index.js +25 -15
- package/dist/cli/setup.js +19 -17
- package/dist/core/augmentation/engine.js +20 -20
- package/dist/core/embeddings/embedding-pipeline.js +26 -26
- package/dist/core/ingestion/ast-cache.js +2 -3
- package/dist/core/ingestion/call-processor.js +5 -7
- package/dist/core/ingestion/cluster-enricher.js +16 -16
- package/dist/core/ingestion/pipeline.js +2 -23
- package/dist/core/ingestion/tree-sitter-queries.js +484 -484
- package/dist/core/ingestion/utils.js +5 -1
- package/dist/core/ingestion/workers/worker-pool.js +0 -8
- package/dist/core/kuzu/kuzu-adapter.js +19 -11
- package/dist/core/kuzu/schema.js +287 -287
- package/dist/core/search/bm25-index.js +6 -7
- package/dist/core/search/hybrid-search.js +3 -3
- package/dist/core/wiki/diagrams.d.ts +27 -0
- package/dist/core/wiki/diagrams.js +163 -0
- package/dist/core/wiki/generator.d.ts +50 -2
- package/dist/core/wiki/generator.js +548 -49
- package/dist/core/wiki/graph-queries.d.ts +42 -0
- package/dist/core/wiki/graph-queries.js +276 -97
- package/dist/core/wiki/html-viewer.js +192 -192
- package/dist/core/wiki/llm-client.js +73 -11
- package/dist/core/wiki/prompts.d.ts +52 -8
- package/dist/core/wiki/prompts.js +200 -86
- package/dist/mcp/core/kuzu-adapter.d.ts +3 -1
- package/dist/mcp/core/kuzu-adapter.js +44 -13
- package/dist/mcp/local/local-backend.js +128 -128
- package/dist/mcp/resources.js +42 -42
- package/dist/mcp/server.js +19 -18
- package/dist/mcp/tools.js +103 -93
- package/hooks/claude/gitnexus-hook.cjs +155 -238
- package/hooks/claude/pre-tool-use.sh +79 -79
- package/hooks/claude/session-start.sh +42 -42
- package/package.json +96 -96
- package/scripts/patch-tree-sitter-swift.cjs +74 -74
- package/skills/gitnexus-cli.md +82 -82
- package/skills/gitnexus-debugging.md +89 -89
- package/skills/gitnexus-exploring.md +78 -78
- package/skills/gitnexus-guide.md +64 -64
- package/skills/gitnexus-impact-analysis.md +97 -97
- package/skills/gitnexus-pr-review.md +163 -163
- package/skills/gitnexus-refactoring.md +121 -121
- package/vendor/leiden/index.cjs +355 -355
- package/vendor/leiden/utils.cjs +392 -392
- package/dist/cli/lazy-action.d.ts +0 -6
- package/dist/cli/lazy-action.js +0 -18
- package/dist/mcp/compatible-stdio-transport.d.ts +0 -25
- package/dist/mcp/compatible-stdio-transport.js +0 -200
package/dist/cli/index.js
CHANGED
|
@@ -2,8 +2,18 @@
|
|
|
2
2
|
// Heap re-spawn removed — only analyze.ts needs the 8GB heap (via its own ensureHeap()).
|
|
3
3
|
// Removing it from here improves MCP server startup time significantly.
|
|
4
4
|
import { Command } from 'commander';
|
|
5
|
+
import { analyzeCommand } from './analyze.js';
|
|
6
|
+
import { serveCommand } from './serve.js';
|
|
7
|
+
import { listCommand } from './list.js';
|
|
8
|
+
import { statusCommand } from './status.js';
|
|
9
|
+
import { mcpCommand } from './mcp.js';
|
|
10
|
+
import { cleanCommand } from './clean.js';
|
|
11
|
+
import { setupCommand } from './setup.js';
|
|
12
|
+
import { augmentCommand } from './augment.js';
|
|
13
|
+
import { wikiCommand } from './wiki.js';
|
|
14
|
+
import { queryCommand, contextCommand, impactCommand, cypherCommand } from './tool.js';
|
|
15
|
+
import { evalServerCommand } from './eval-server.js';
|
|
5
16
|
import { createRequire } from 'node:module';
|
|
6
|
-
import { createLazyAction } from './lazy-action.js';
|
|
7
17
|
const _require = createRequire(import.meta.url);
|
|
8
18
|
const pkg = _require('../../package.json');
|
|
9
19
|
const program = new Command();
|
|
@@ -14,37 +24,37 @@ program
|
|
|
14
24
|
program
|
|
15
25
|
.command('setup')
|
|
16
26
|
.description('One-time setup: configure MCP for Cursor, Claude Code, OpenCode')
|
|
17
|
-
.action(
|
|
27
|
+
.action(setupCommand);
|
|
18
28
|
program
|
|
19
29
|
.command('analyze [path]')
|
|
20
30
|
.description('Index a repository (full analysis)')
|
|
21
31
|
.option('-f, --force', 'Force full re-index even if up to date')
|
|
22
32
|
.option('--embeddings', 'Enable embedding generation for semantic search (off by default)')
|
|
23
|
-
.action(
|
|
33
|
+
.action(analyzeCommand);
|
|
24
34
|
program
|
|
25
35
|
.command('serve')
|
|
26
36
|
.description('Start local HTTP server for web UI connection')
|
|
27
37
|
.option('-p, --port <port>', 'Port number', '4747')
|
|
28
38
|
.option('--host <host>', 'Bind address (default: 127.0.0.1, use 0.0.0.0 for remote access)')
|
|
29
|
-
.action(
|
|
39
|
+
.action(serveCommand);
|
|
30
40
|
program
|
|
31
41
|
.command('mcp')
|
|
32
42
|
.description('Start MCP server (stdio) — serves all indexed repos')
|
|
33
|
-
.action(
|
|
43
|
+
.action(mcpCommand);
|
|
34
44
|
program
|
|
35
45
|
.command('list')
|
|
36
46
|
.description('List all indexed repositories')
|
|
37
|
-
.action(
|
|
47
|
+
.action(listCommand);
|
|
38
48
|
program
|
|
39
49
|
.command('status')
|
|
40
50
|
.description('Show index status for current repo')
|
|
41
|
-
.action(
|
|
51
|
+
.action(statusCommand);
|
|
42
52
|
program
|
|
43
53
|
.command('clean')
|
|
44
54
|
.description('Delete GitNexus index for current repo')
|
|
45
55
|
.option('-f, --force', 'Skip confirmation prompt')
|
|
46
56
|
.option('--all', 'Clean all indexed repos')
|
|
47
|
-
.action(
|
|
57
|
+
.action(cleanCommand);
|
|
48
58
|
program
|
|
49
59
|
.command('wiki [path]')
|
|
50
60
|
.description('Generate repository wiki from knowledge graph')
|
|
@@ -54,11 +64,11 @@ program
|
|
|
54
64
|
.option('--api-key <key>', 'LLM API key (saved to ~/.gitnexus/config.json)')
|
|
55
65
|
.option('--concurrency <n>', 'Parallel LLM calls (default: 3)', '3')
|
|
56
66
|
.option('--gist', 'Publish wiki as a public GitHub Gist after generation')
|
|
57
|
-
.action(
|
|
67
|
+
.action(wikiCommand);
|
|
58
68
|
program
|
|
59
69
|
.command('augment <pattern>')
|
|
60
70
|
.description('Augment a search pattern with knowledge graph context (used by hooks)')
|
|
61
|
-
.action(
|
|
71
|
+
.action(augmentCommand);
|
|
62
72
|
// ─── Direct Tool Commands (no MCP overhead) ────────────────────────
|
|
63
73
|
// These invoke LocalBackend directly for use in eval, scripts, and CI.
|
|
64
74
|
program
|
|
@@ -69,7 +79,7 @@ program
|
|
|
69
79
|
.option('-g, --goal <text>', 'What you want to find')
|
|
70
80
|
.option('-l, --limit <n>', 'Max processes to return (default: 5)')
|
|
71
81
|
.option('--content', 'Include full symbol source code')
|
|
72
|
-
.action(
|
|
82
|
+
.action(queryCommand);
|
|
73
83
|
program
|
|
74
84
|
.command('context [name]')
|
|
75
85
|
.description('360-degree view of a code symbol: callers, callees, processes')
|
|
@@ -77,7 +87,7 @@ program
|
|
|
77
87
|
.option('-u, --uid <uid>', 'Direct symbol UID (zero-ambiguity lookup)')
|
|
78
88
|
.option('-f, --file <path>', 'File path to disambiguate common names')
|
|
79
89
|
.option('--content', 'Include full symbol source code')
|
|
80
|
-
.action(
|
|
90
|
+
.action(contextCommand);
|
|
81
91
|
program
|
|
82
92
|
.command('impact <target>')
|
|
83
93
|
.description('Blast radius analysis: what breaks if you change a symbol')
|
|
@@ -85,17 +95,17 @@ program
|
|
|
85
95
|
.option('-r, --repo <name>', 'Target repository')
|
|
86
96
|
.option('--depth <n>', 'Max relationship depth (default: 3)')
|
|
87
97
|
.option('--include-tests', 'Include test files in results')
|
|
88
|
-
.action(
|
|
98
|
+
.action(impactCommand);
|
|
89
99
|
program
|
|
90
100
|
.command('cypher <query>')
|
|
91
101
|
.description('Execute raw Cypher query against the knowledge graph')
|
|
92
102
|
.option('-r, --repo <name>', 'Target repository')
|
|
93
|
-
.action(
|
|
103
|
+
.action(cypherCommand);
|
|
94
104
|
// ─── Eval Server (persistent daemon for SWE-bench) ─────────────────
|
|
95
105
|
program
|
|
96
106
|
.command('eval-server')
|
|
97
107
|
.description('Start lightweight HTTP server for fast tool calls during evaluation')
|
|
98
108
|
.option('-p, --port <port>', 'Port number', '4848')
|
|
99
109
|
.option('--idle-timeout <seconds>', 'Auto-shutdown after N seconds idle (0 = disabled)', '0')
|
|
100
|
-
.action(
|
|
110
|
+
.action(evalServerCommand);
|
|
101
111
|
program.parse(process.argv);
|
package/dist/cli/setup.js
CHANGED
|
@@ -147,34 +147,36 @@ async function installClaudeCodeHooks(result) {
|
|
|
147
147
|
// even when it's no longer inside the npm package tree
|
|
148
148
|
const resolvedCli = path.join(__dirname, '..', 'cli', 'index.js');
|
|
149
149
|
const normalizedCli = path.resolve(resolvedCli).replace(/\\/g, '/');
|
|
150
|
-
|
|
151
|
-
content = content.replace("let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');", `let cliPath = ${jsonCli};`);
|
|
150
|
+
content = content.replace("let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');", `let cliPath = '${normalizedCli}';`);
|
|
152
151
|
await fs.writeFile(dest, content, 'utf-8');
|
|
153
152
|
}
|
|
154
153
|
catch {
|
|
155
154
|
// Script not found in source — skip
|
|
156
155
|
}
|
|
157
|
-
const
|
|
158
|
-
const hookCmd = `node "${hookPath.replace(/"/g, '\\"')}"`;
|
|
156
|
+
const hookCmd = `node "${path.join(destHooksDir, 'gitnexus-hook.cjs').replace(/\\/g, '/')}"`;
|
|
159
157
|
// Merge hook config into ~/.claude/settings.json
|
|
160
158
|
const existing = await readJsonFile(settingsPath) || {};
|
|
161
159
|
if (!existing.hooks)
|
|
162
160
|
existing.hooks = {};
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
161
|
+
// NOTE: SessionStart hooks are broken on Windows (Claude Code bug #23576).
|
|
162
|
+
// Session context is delivered via CLAUDE.md / skills instead.
|
|
163
|
+
// Add PreToolUse hook if not already present
|
|
164
|
+
if (!existing.hooks.PreToolUse)
|
|
165
|
+
existing.hooks.PreToolUse = [];
|
|
166
|
+
const hasPreToolHook = existing.hooks.PreToolUse.some((h) => h.hooks?.some((hh) => hh.command?.includes('gitnexus')));
|
|
167
|
+
if (!hasPreToolHook) {
|
|
168
|
+
existing.hooks.PreToolUse.push({
|
|
169
|
+
matcher: 'Grep|Glob|Bash',
|
|
170
|
+
hooks: [{
|
|
171
|
+
type: 'command',
|
|
172
|
+
command: hookCmd,
|
|
173
|
+
timeout: 8000,
|
|
174
|
+
statusMessage: 'Enriching with GitNexus graph context...',
|
|
175
|
+
}],
|
|
176
|
+
});
|
|
173
177
|
}
|
|
174
|
-
ensureHookEntry('PreToolUse', 'Grep|Glob|Bash', 10, 'Enriching with GitNexus graph context...');
|
|
175
|
-
ensureHookEntry('PostToolUse', 'Bash', 10, 'Checking GitNexus index freshness...');
|
|
176
178
|
await writeJsonFile(settingsPath, existing);
|
|
177
|
-
result.configured.push('Claude Code hooks (PreToolUse
|
|
179
|
+
result.configured.push('Claude Code hooks (PreToolUse)');
|
|
178
180
|
}
|
|
179
181
|
catch (err) {
|
|
180
182
|
result.errors.push(`Claude Code hooks: ${err.message}`);
|
|
@@ -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);
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { LRUCache } from 'lru-cache';
|
|
2
2
|
export const createASTCache = (maxSize = 50) => {
|
|
3
|
-
const effectiveMax = Math.max(maxSize, 1);
|
|
4
3
|
// Initialize the cache with a 'dispose' handler
|
|
5
4
|
// This is the magic: When an item is evicted (dropped), this runs automatically.
|
|
6
5
|
const cache = new LRUCache({
|
|
7
|
-
max:
|
|
6
|
+
max: maxSize,
|
|
8
7
|
dispose: (tree) => {
|
|
9
8
|
try {
|
|
10
9
|
// NOTE: web-tree-sitter has tree.delete(); native tree-sitter trees are GC-managed.
|
|
@@ -29,7 +28,7 @@ export const createASTCache = (maxSize = 50) => {
|
|
|
29
28
|
},
|
|
30
29
|
stats: () => ({
|
|
31
30
|
size: cache.size,
|
|
32
|
-
maxSize:
|
|
31
|
+
maxSize: maxSize
|
|
33
32
|
})
|
|
34
33
|
};
|
|
35
34
|
};
|
|
@@ -54,7 +54,8 @@ const findEnclosingFunction = (node, filePath, symbolTable) => {
|
|
|
54
54
|
// Swift init/deinit — handle before generic cases (more specific)
|
|
55
55
|
if (current.type === 'init_declaration' || current.type === 'deinit_declaration') {
|
|
56
56
|
const funcName = current.type === 'init_declaration' ? 'init' : 'deinit';
|
|
57
|
-
|
|
57
|
+
const startLine = current.startPosition?.row ?? 0;
|
|
58
|
+
return generateId('Constructor', `${filePath}:${funcName}:${startLine}`);
|
|
58
59
|
}
|
|
59
60
|
if (current.type === 'function_declaration' ||
|
|
60
61
|
current.type === 'function_definition' ||
|
|
@@ -114,12 +115,9 @@ const findEnclosingFunction = (node, filePath, symbolTable) => {
|
|
|
114
115
|
if (nodeId)
|
|
115
116
|
return nodeId;
|
|
116
117
|
// Try construct ID manually if lookup fails (common for non-exported internal functions)
|
|
117
|
-
// Format
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// Ideally we should verify this ID exists, but strictly speaking if we are inside it,
|
|
121
|
-
// it SHOULD exist. Returning it is better than falling back to File.
|
|
122
|
-
return generatedId;
|
|
118
|
+
// Format must match parsing-processor: "Label:path/to/file:funcName:startLine"
|
|
119
|
+
const startLine = current.startPosition?.row ?? 0;
|
|
120
|
+
return generateId(label, `${filePath}:${funcName}:${startLine}`);
|
|
123
121
|
}
|
|
124
122
|
// Couldn't determine function name - try parent (might be nested)
|
|
125
123
|
}
|
|
@@ -13,12 +13,12 @@ const buildEnrichmentPrompt = (members, heuristicLabel) => {
|
|
|
13
13
|
const memberList = limitedMembers
|
|
14
14
|
.map(m => `${m.name} (${m.type})`)
|
|
15
15
|
.join(', ');
|
|
16
|
-
return `Analyze this code cluster and provide a semantic name and short description.
|
|
17
|
-
|
|
18
|
-
Heuristic: "${heuristicLabel}"
|
|
19
|
-
Members: ${memberList}${members.length > 20 ? ` (+${members.length - 20} more)` : ''}
|
|
20
|
-
|
|
21
|
-
Reply with JSON only:
|
|
16
|
+
return `Analyze this code cluster and provide a semantic name and short description.
|
|
17
|
+
|
|
18
|
+
Heuristic: "${heuristicLabel}"
|
|
19
|
+
Members: ${memberList}${members.length > 20 ? ` (+${members.length - 20} more)` : ''}
|
|
20
|
+
|
|
21
|
+
Reply with JSON only:
|
|
22
22
|
{"name": "2-4 word semantic name", "description": "One sentence describing purpose"}`;
|
|
23
23
|
};
|
|
24
24
|
// ============================================================================
|
|
@@ -115,18 +115,18 @@ export const enrichClustersBatch = async (communities, memberMap, llmClient, bat
|
|
|
115
115
|
const memberList = limitedMembers
|
|
116
116
|
.map(m => `${m.name} (${m.type})`)
|
|
117
117
|
.join(', ');
|
|
118
|
-
return `Cluster ${idx + 1} (id: ${community.id}):
|
|
119
|
-
Heuristic: "${community.heuristicLabel}"
|
|
118
|
+
return `Cluster ${idx + 1} (id: ${community.id}):
|
|
119
|
+
Heuristic: "${community.heuristicLabel}"
|
|
120
120
|
Members: ${memberList}`;
|
|
121
121
|
}).join('\n\n');
|
|
122
|
-
const prompt = `Analyze these code clusters and generate semantic names, keywords, and descriptions.
|
|
123
|
-
|
|
124
|
-
${batchPrompt}
|
|
125
|
-
|
|
126
|
-
Output JSON array:
|
|
127
|
-
[
|
|
128
|
-
{"id": "comm_X", "name": "...", "keywords": [...], "description": "..."},
|
|
129
|
-
...
|
|
122
|
+
const prompt = `Analyze these code clusters and generate semantic names, keywords, and descriptions.
|
|
123
|
+
|
|
124
|
+
${batchPrompt}
|
|
125
|
+
|
|
126
|
+
Output JSON array:
|
|
127
|
+
[
|
|
128
|
+
{"id": "comm_X", "name": "...", "keywords": [...], "description": "..."},
|
|
129
|
+
...
|
|
130
130
|
]`;
|
|
131
131
|
try {
|
|
132
132
|
const response = await llmClient.generate(prompt);
|
|
@@ -12,9 +12,6 @@ import { walkRepositoryPaths, readFileContents } from './filesystem-walker.js';
|
|
|
12
12
|
import { getLanguageFromFilename } from './utils.js';
|
|
13
13
|
import { isLanguageAvailable } from '../tree-sitter/parser-loader.js';
|
|
14
14
|
import { createWorkerPool } from './workers/worker-pool.js';
|
|
15
|
-
import fs from 'node:fs';
|
|
16
|
-
import path from 'node:path';
|
|
17
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
18
15
|
const isDev = process.env.NODE_ENV === 'development';
|
|
19
16
|
/** Max bytes of source content to load per parse chunk. Each chunk's source +
|
|
20
17
|
* parsed ASTs + extracted records + worker serialization overhead all live in
|
|
@@ -90,14 +87,6 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
|
|
|
90
87
|
console.warn(`Skipping ${count} ${lang} file(s) — ${lang} parser not available (native binding may not have built). Try: npm rebuild tree-sitter-${lang}`);
|
|
91
88
|
}
|
|
92
89
|
const totalParseable = parseableScanned.length;
|
|
93
|
-
if (totalParseable === 0) {
|
|
94
|
-
onProgress({
|
|
95
|
-
phase: 'parsing',
|
|
96
|
-
percent: 82,
|
|
97
|
-
message: 'No parseable files found — skipping parsing phase',
|
|
98
|
-
stats: { filesProcessed: 0, totalFiles: 0, nodesCreated: graph.nodeCount },
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
90
|
// Build byte-budget chunks
|
|
102
91
|
const chunks = [];
|
|
103
92
|
let currentChunk = [];
|
|
@@ -127,21 +116,11 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
|
|
|
127
116
|
// Create worker pool once, reuse across chunks
|
|
128
117
|
let workerPool;
|
|
129
118
|
try {
|
|
130
|
-
|
|
131
|
-
// When running under vitest, import.meta.url points to src/ where no .js exists.
|
|
132
|
-
// Fall back to the compiled dist/ worker so the pool can spawn real worker threads.
|
|
133
|
-
const thisDir = fileURLToPath(new URL('.', import.meta.url));
|
|
134
|
-
if (!fs.existsSync(fileURLToPath(workerUrl))) {
|
|
135
|
-
const distWorker = path.resolve(thisDir, '..', '..', '..', 'dist', 'core', 'ingestion', 'workers', 'parse-worker.js');
|
|
136
|
-
if (fs.existsSync(distWorker)) {
|
|
137
|
-
workerUrl = pathToFileURL(distWorker);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
119
|
+
const workerUrl = new URL('./workers/parse-worker.js', import.meta.url);
|
|
140
120
|
workerPool = createWorkerPool(workerUrl);
|
|
141
121
|
}
|
|
142
122
|
catch (err) {
|
|
143
|
-
|
|
144
|
-
console.warn('Worker pool creation failed, using sequential fallback:', err.message);
|
|
123
|
+
// Worker pool creation failed — sequential fallback
|
|
145
124
|
}
|
|
146
125
|
let filesParsedSoFar = 0;
|
|
147
126
|
// AST cache sized for one chunk (sequential fallback uses it for import/call/heritage)
|