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.
Files changed (52) hide show
  1. package/README.md +194 -194
  2. package/dist/cli/ai-context.js +87 -105
  3. package/dist/cli/analyze.js +0 -8
  4. package/dist/cli/index.js +25 -15
  5. package/dist/cli/setup.js +19 -17
  6. package/dist/core/augmentation/engine.js +20 -20
  7. package/dist/core/embeddings/embedding-pipeline.js +26 -26
  8. package/dist/core/ingestion/ast-cache.js +2 -3
  9. package/dist/core/ingestion/call-processor.js +5 -7
  10. package/dist/core/ingestion/cluster-enricher.js +16 -16
  11. package/dist/core/ingestion/pipeline.js +2 -23
  12. package/dist/core/ingestion/tree-sitter-queries.js +484 -484
  13. package/dist/core/ingestion/utils.js +5 -1
  14. package/dist/core/ingestion/workers/worker-pool.js +0 -8
  15. package/dist/core/kuzu/kuzu-adapter.js +19 -11
  16. package/dist/core/kuzu/schema.js +287 -287
  17. package/dist/core/search/bm25-index.js +6 -7
  18. package/dist/core/search/hybrid-search.js +3 -3
  19. package/dist/core/wiki/diagrams.d.ts +27 -0
  20. package/dist/core/wiki/diagrams.js +163 -0
  21. package/dist/core/wiki/generator.d.ts +50 -2
  22. package/dist/core/wiki/generator.js +548 -49
  23. package/dist/core/wiki/graph-queries.d.ts +42 -0
  24. package/dist/core/wiki/graph-queries.js +276 -97
  25. package/dist/core/wiki/html-viewer.js +192 -192
  26. package/dist/core/wiki/llm-client.js +73 -11
  27. package/dist/core/wiki/prompts.d.ts +52 -8
  28. package/dist/core/wiki/prompts.js +200 -86
  29. package/dist/mcp/core/kuzu-adapter.d.ts +3 -1
  30. package/dist/mcp/core/kuzu-adapter.js +44 -13
  31. package/dist/mcp/local/local-backend.js +128 -128
  32. package/dist/mcp/resources.js +42 -42
  33. package/dist/mcp/server.js +19 -18
  34. package/dist/mcp/tools.js +103 -93
  35. package/hooks/claude/gitnexus-hook.cjs +155 -238
  36. package/hooks/claude/pre-tool-use.sh +79 -79
  37. package/hooks/claude/session-start.sh +42 -42
  38. package/package.json +96 -96
  39. package/scripts/patch-tree-sitter-swift.cjs +74 -74
  40. package/skills/gitnexus-cli.md +82 -82
  41. package/skills/gitnexus-debugging.md +89 -89
  42. package/skills/gitnexus-exploring.md +78 -78
  43. package/skills/gitnexus-guide.md +64 -64
  44. package/skills/gitnexus-impact-analysis.md +97 -97
  45. package/skills/gitnexus-pr-review.md +163 -163
  46. package/skills/gitnexus-refactoring.md +121 -121
  47. package/vendor/leiden/index.cjs +355 -355
  48. package/vendor/leiden/utils.cjs +392 -392
  49. package/dist/cli/lazy-action.d.ts +0 -6
  50. package/dist/cli/lazy-action.js +0 -18
  51. package/dist/mcp/compatible-stdio-transport.d.ts +0 -25
  52. 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(createLazyAction(() => import('./setup.js'), 'setupCommand'));
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(createLazyAction(() => import('./analyze.js'), 'analyzeCommand'));
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(createLazyAction(() => import('./serve.js'), 'serveCommand'));
39
+ .action(serveCommand);
30
40
  program
31
41
  .command('mcp')
32
42
  .description('Start MCP server (stdio) — serves all indexed repos')
33
- .action(createLazyAction(() => import('./mcp.js'), 'mcpCommand'));
43
+ .action(mcpCommand);
34
44
  program
35
45
  .command('list')
36
46
  .description('List all indexed repositories')
37
- .action(createLazyAction(() => import('./list.js'), 'listCommand'));
47
+ .action(listCommand);
38
48
  program
39
49
  .command('status')
40
50
  .description('Show index status for current repo')
41
- .action(createLazyAction(() => import('./status.js'), 'statusCommand'));
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(createLazyAction(() => import('./clean.js'), 'cleanCommand'));
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(createLazyAction(() => import('./wiki.js'), 'wikiCommand'));
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(createLazyAction(() => import('./augment.js'), 'augmentCommand'));
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(createLazyAction(() => import('./tool.js'), 'queryCommand'));
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(createLazyAction(() => import('./tool.js'), 'contextCommand'));
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(createLazyAction(() => import('./tool.js'), 'impactCommand'));
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(createLazyAction(() => import('./tool.js'), 'cypherCommand'));
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(createLazyAction(() => import('./eval-server.js'), 'evalServerCommand'));
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
- const jsonCli = JSON.stringify(normalizedCli);
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 hookPath = path.join(destHooksDir, 'gitnexus-hook.cjs').replace(/\\/g, '/');
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
- function ensureHookEntry(eventName, matcher, timeout, statusMessage) {
164
- if (!existing.hooks[eventName])
165
- existing.hooks[eventName] = [];
166
- const hasHook = existing.hooks[eventName].some((h) => h.hooks?.some(hh => hh.command?.includes('gitnexus-hook')));
167
- if (!hasHook) {
168
- existing.hooks[eventName].push({
169
- matcher,
170
- hooks: [{ type: 'command', command: hookCmd, timeout, statusMessage }],
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, PostToolUse)');
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: effectiveMax,
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: effectiveMax
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
- return generateId('Constructor', `${filePath}:${funcName}`);
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 should match what parsing-processor generates: "Function:path/to/file:funcName"
118
- // Check if we already have a node with this ID in the symbol table to be safe
119
- const generatedId = generateId(label, `${filePath}:${funcName}`);
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
- let workerUrl = new URL('./workers/parse-worker.js', import.meta.url);
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
- if (isDev)
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)