gitnexus 1.4.1 → 1.4.6

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 (169) hide show
  1. package/README.md +215 -194
  2. package/dist/cli/ai-context.d.ts +2 -1
  3. package/dist/cli/ai-context.js +117 -90
  4. package/dist/cli/analyze.d.ts +2 -0
  5. package/dist/cli/analyze.js +57 -30
  6. package/dist/cli/augment.js +1 -1
  7. package/dist/cli/eval-server.d.ts +1 -1
  8. package/dist/cli/eval-server.js +14 -6
  9. package/dist/cli/index.js +18 -25
  10. package/dist/cli/lazy-action.d.ts +6 -0
  11. package/dist/cli/lazy-action.js +18 -0
  12. package/dist/cli/mcp.js +1 -1
  13. package/dist/cli/setup.js +42 -32
  14. package/dist/cli/skill-gen.d.ts +26 -0
  15. package/dist/cli/skill-gen.js +549 -0
  16. package/dist/cli/status.js +13 -4
  17. package/dist/cli/tool.d.ts +3 -2
  18. package/dist/cli/tool.js +48 -13
  19. package/dist/cli/wiki.js +2 -2
  20. package/dist/config/ignore-service.d.ts +25 -0
  21. package/dist/config/ignore-service.js +76 -0
  22. package/dist/config/supported-languages.d.ts +1 -0
  23. package/dist/config/supported-languages.js +1 -1
  24. package/dist/core/augmentation/engine.js +99 -72
  25. package/dist/core/embeddings/embedder.d.ts +1 -1
  26. package/dist/core/embeddings/embedder.js +1 -1
  27. package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
  28. package/dist/core/embeddings/embedding-pipeline.js +74 -47
  29. package/dist/core/embeddings/types.d.ts +1 -1
  30. package/dist/core/graph/types.d.ts +5 -2
  31. package/dist/core/ingestion/ast-cache.js +3 -2
  32. package/dist/core/ingestion/call-processor.d.ts +5 -7
  33. package/dist/core/ingestion/call-processor.js +430 -283
  34. package/dist/core/ingestion/call-routing.d.ts +53 -0
  35. package/dist/core/ingestion/call-routing.js +108 -0
  36. package/dist/core/ingestion/cluster-enricher.js +16 -16
  37. package/dist/core/ingestion/constants.d.ts +16 -0
  38. package/dist/core/ingestion/constants.js +16 -0
  39. package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
  40. package/dist/core/ingestion/entry-point-scoring.js +94 -24
  41. package/dist/core/ingestion/export-detection.d.ts +18 -0
  42. package/dist/core/ingestion/export-detection.js +231 -0
  43. package/dist/core/ingestion/filesystem-walker.js +4 -3
  44. package/dist/core/ingestion/framework-detection.d.ts +5 -1
  45. package/dist/core/ingestion/framework-detection.js +48 -8
  46. package/dist/core/ingestion/heritage-processor.d.ts +13 -5
  47. package/dist/core/ingestion/heritage-processor.js +109 -55
  48. package/dist/core/ingestion/import-processor.d.ts +16 -20
  49. package/dist/core/ingestion/import-processor.js +202 -696
  50. package/dist/core/ingestion/language-config.d.ts +46 -0
  51. package/dist/core/ingestion/language-config.js +167 -0
  52. package/dist/core/ingestion/mro-processor.d.ts +45 -0
  53. package/dist/core/ingestion/mro-processor.js +369 -0
  54. package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
  55. package/dist/core/ingestion/named-binding-extraction.js +363 -0
  56. package/dist/core/ingestion/parsing-processor.d.ts +3 -11
  57. package/dist/core/ingestion/parsing-processor.js +85 -181
  58. package/dist/core/ingestion/pipeline.d.ts +5 -1
  59. package/dist/core/ingestion/pipeline.js +192 -116
  60. package/dist/core/ingestion/process-processor.js +2 -1
  61. package/dist/core/ingestion/resolution-context.d.ts +53 -0
  62. package/dist/core/ingestion/resolution-context.js +132 -0
  63. package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
  64. package/dist/core/ingestion/resolvers/csharp.js +109 -0
  65. package/dist/core/ingestion/resolvers/go.d.ts +19 -0
  66. package/dist/core/ingestion/resolvers/go.js +42 -0
  67. package/dist/core/ingestion/resolvers/index.d.ts +18 -0
  68. package/dist/core/ingestion/resolvers/index.js +13 -0
  69. package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
  70. package/dist/core/ingestion/resolvers/jvm.js +87 -0
  71. package/dist/core/ingestion/resolvers/php.d.ts +15 -0
  72. package/dist/core/ingestion/resolvers/php.js +35 -0
  73. package/dist/core/ingestion/resolvers/python.d.ts +19 -0
  74. package/dist/core/ingestion/resolvers/python.js +52 -0
  75. package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
  76. package/dist/core/ingestion/resolvers/ruby.js +15 -0
  77. package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
  78. package/dist/core/ingestion/resolvers/rust.js +73 -0
  79. package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
  80. package/dist/core/ingestion/resolvers/standard.js +123 -0
  81. package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
  82. package/dist/core/ingestion/resolvers/utils.js +122 -0
  83. package/dist/core/ingestion/symbol-table.d.ts +21 -1
  84. package/dist/core/ingestion/symbol-table.js +40 -12
  85. package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -11
  86. package/dist/core/ingestion/tree-sitter-queries.js +642 -485
  87. package/dist/core/ingestion/type-env.d.ts +49 -0
  88. package/dist/core/ingestion/type-env.js +611 -0
  89. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
  90. package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
  91. package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
  92. package/dist/core/ingestion/type-extractors/csharp.js +383 -0
  93. package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
  94. package/dist/core/ingestion/type-extractors/go.js +467 -0
  95. package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
  96. package/dist/core/ingestion/type-extractors/index.js +31 -0
  97. package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
  98. package/dist/core/ingestion/type-extractors/jvm.js +681 -0
  99. package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
  100. package/dist/core/ingestion/type-extractors/php.js +549 -0
  101. package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
  102. package/dist/core/ingestion/type-extractors/python.js +406 -0
  103. package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
  104. package/dist/core/ingestion/type-extractors/ruby.js +389 -0
  105. package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
  106. package/dist/core/ingestion/type-extractors/rust.js +449 -0
  107. package/dist/core/ingestion/type-extractors/shared.d.ts +133 -0
  108. package/dist/core/ingestion/type-extractors/shared.js +703 -0
  109. package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
  110. package/dist/core/ingestion/type-extractors/swift.js +137 -0
  111. package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
  112. package/dist/core/ingestion/type-extractors/types.js +1 -0
  113. package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
  114. package/dist/core/ingestion/type-extractors/typescript.js +494 -0
  115. package/dist/core/ingestion/utils.d.ts +98 -0
  116. package/dist/core/ingestion/utils.js +1064 -9
  117. package/dist/core/ingestion/workers/parse-worker.d.ts +38 -4
  118. package/dist/core/ingestion/workers/parse-worker.js +251 -359
  119. package/dist/core/ingestion/workers/worker-pool.js +8 -0
  120. package/dist/core/{kuzu → lbug}/csv-generator.d.ts +1 -1
  121. package/dist/core/{kuzu → lbug}/csv-generator.js +20 -4
  122. package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +19 -19
  123. package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +82 -82
  124. package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
  125. package/dist/core/{kuzu → lbug}/schema.js +304 -289
  126. package/dist/core/search/bm25-index.d.ts +4 -4
  127. package/dist/core/search/bm25-index.js +17 -16
  128. package/dist/core/search/hybrid-search.d.ts +2 -2
  129. package/dist/core/search/hybrid-search.js +9 -9
  130. package/dist/core/tree-sitter/parser-loader.js +9 -2
  131. package/dist/core/wiki/generator.d.ts +4 -52
  132. package/dist/core/wiki/generator.js +53 -552
  133. package/dist/core/wiki/graph-queries.d.ts +4 -46
  134. package/dist/core/wiki/graph-queries.js +103 -282
  135. package/dist/core/wiki/html-viewer.js +192 -192
  136. package/dist/core/wiki/llm-client.js +11 -73
  137. package/dist/core/wiki/prompts.d.ts +8 -52
  138. package/dist/core/wiki/prompts.js +86 -200
  139. package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
  140. package/dist/mcp/compatible-stdio-transport.js +200 -0
  141. package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +7 -9
  142. package/dist/mcp/core/{kuzu-adapter.js → lbug-adapter.js} +77 -79
  143. package/dist/mcp/local/local-backend.d.ts +7 -6
  144. package/dist/mcp/local/local-backend.js +176 -147
  145. package/dist/mcp/resources.js +42 -42
  146. package/dist/mcp/server.js +18 -19
  147. package/dist/mcp/tools.js +103 -104
  148. package/dist/server/api.js +12 -12
  149. package/dist/server/mcp-http.d.ts +1 -1
  150. package/dist/server/mcp-http.js +1 -1
  151. package/dist/storage/repo-manager.d.ts +20 -2
  152. package/dist/storage/repo-manager.js +55 -1
  153. package/dist/types/pipeline.d.ts +1 -1
  154. package/hooks/claude/gitnexus-hook.cjs +238 -155
  155. package/hooks/claude/pre-tool-use.sh +79 -79
  156. package/hooks/claude/session-start.sh +42 -42
  157. package/package.json +99 -96
  158. package/scripts/patch-tree-sitter-swift.cjs +74 -74
  159. package/skills/gitnexus-cli.md +82 -82
  160. package/skills/gitnexus-debugging.md +89 -89
  161. package/skills/gitnexus-exploring.md +78 -78
  162. package/skills/gitnexus-guide.md +64 -64
  163. package/skills/gitnexus-impact-analysis.md +97 -97
  164. package/skills/gitnexus-pr-review.md +163 -163
  165. package/skills/gitnexus-refactoring.md +121 -121
  166. package/vendor/leiden/index.cjs +355 -355
  167. package/vendor/leiden/utils.cjs +392 -392
  168. package/dist/core/wiki/diagrams.d.ts +0 -27
  169. package/dist/core/wiki/diagrams.js +0 -163
@@ -3,16 +3,16 @@
3
3
  *
4
4
  * Provides tool implementations using local .gitnexus/ indexes.
5
5
  * Supports multiple indexed repositories via a global registry.
6
- * KuzuDB connections are opened lazily per repo on first query.
6
+ * LadybugDB connections are opened lazily per repo on first query.
7
7
  */
8
8
  import fs from 'fs/promises';
9
9
  import path from 'path';
10
- import { initKuzu, executeQuery, executeParameterized, closeKuzu, isKuzuReady } from '../core/kuzu-adapter.js';
10
+ import { initLbug, executeQuery, executeParameterized, closeLbug, isLbugReady } from '../core/lbug-adapter.js';
11
11
  // Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
12
12
  // at MCP server startup — crashes on unsupported Node ABI versions (#89)
13
13
  // git utilities available if needed
14
14
  // import { isGitRepo, getCurrentCommit, getGitRoot } from '../../storage/git.js';
15
- import { listRegisteredRepos, } from '../../storage/repo-manager.js';
15
+ import { listRegisteredRepos, cleanupOldKuzuFiles, } from '../../storage/repo-manager.js';
16
16
  // AI context generation is CLI-only (gitnexus analyze)
17
17
  // import { generateAIContextFiles } from '../../cli/ai-context.js';
18
18
  /**
@@ -26,9 +26,10 @@ export function isTestFilePath(filePath) {
26
26
  p.includes('/test/') || p.includes('/tests/') ||
27
27
  p.includes('/testing/') || p.includes('/fixtures/') ||
28
28
  p.endsWith('_test.go') || p.endsWith('_test.py') ||
29
+ p.endsWith('_spec.rb') || p.endsWith('_test.rb') || p.includes('/spec/') ||
29
30
  p.includes('/test_') || p.includes('/conftest.'));
30
31
  }
31
- /** Valid KuzuDB node labels for safe Cypher query construction */
32
+ /** Valid LadybugDB node labels for safe Cypher query construction */
32
33
  export const VALID_NODE_LABELS = new Set([
33
34
  'File', 'Folder', 'Function', 'Class', 'Interface', 'Method', 'CodeElement',
34
35
  'Community', 'Process', 'Struct', 'Enum', 'Macro', 'Typedef', 'Union',
@@ -36,7 +37,7 @@ export const VALID_NODE_LABELS = new Set([
36
37
  'Record', 'Delegate', 'Annotation', 'Constructor', 'Template', 'Module',
37
38
  ]);
38
39
  /** Valid relation types for impact analysis filtering */
39
- export const VALID_RELATION_TYPES = new Set(['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']);
40
+ export const VALID_RELATION_TYPES = new Set(['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'OVERRIDES']);
40
41
  /** Regex to detect write operations in user-supplied Cypher queries */
41
42
  export const CYPHER_WRITE_RE = /\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH)\b/i;
42
43
  /** Check if a Cypher query contains write operations */
@@ -64,7 +65,7 @@ export class LocalBackend {
64
65
  /**
65
66
  * Re-read the global registry and update the in-memory repo map.
66
67
  * New repos are added, existing repos are updated, removed repos are pruned.
67
- * KuzuDB connections for removed repos are NOT closed (they idle-timeout naturally).
68
+ * LadybugDB connections for removed repos are NOT closed (they idle-timeout naturally).
68
69
  */
69
70
  async refreshRepos() {
70
71
  const entries = await listRegisteredRepos({ validate: true });
@@ -73,19 +74,25 @@ export class LocalBackend {
73
74
  const id = this.repoId(entry.name, entry.path);
74
75
  freshIds.add(id);
75
76
  const storagePath = entry.storagePath;
76
- const kuzuPath = path.join(storagePath, 'kuzu');
77
+ const lbugPath = path.join(storagePath, 'lbug');
78
+ // Clean up any leftover KuzuDB files from before the LadybugDB migration.
79
+ // If kuzu exists but lbug doesn't, warn so the user knows to re-analyze.
80
+ const kuzu = await cleanupOldKuzuFiles(storagePath);
81
+ if (kuzu.found && kuzu.needsReindex) {
82
+ console.error(`GitNexus: "${entry.name}" has a stale KuzuDB index. Run: gitnexus analyze ${entry.path}`);
83
+ }
77
84
  const handle = {
78
85
  id,
79
86
  name: entry.name,
80
87
  repoPath: entry.path,
81
88
  storagePath,
82
- kuzuPath,
89
+ lbugPath,
83
90
  indexedAt: entry.indexedAt,
84
91
  lastCommit: entry.lastCommit,
85
92
  stats: entry.stats,
86
93
  };
87
94
  this.repos.set(id, handle);
88
- // Build lightweight context (no KuzuDB needed)
95
+ // Build lightweight context (no LadybugDB needed)
89
96
  const s = entry.stats || {};
90
97
  this.contextCache.set(id, {
91
98
  projectName: entry.name,
@@ -186,16 +193,16 @@ export class LocalBackend {
186
193
  }
187
194
  return null; // Multiple repos, no param — ambiguous
188
195
  }
189
- // ─── Lazy KuzuDB Init ────────────────────────────────────────────
196
+ // ─── Lazy LadybugDB Init ────────────────────────────────────────────
190
197
  async ensureInitialized(repoId) {
191
198
  // Always check the actual pool — the idle timer may have evicted the connection
192
- if (this.initializedRepos.has(repoId) && isKuzuReady(repoId))
199
+ if (this.initializedRepos.has(repoId) && isLbugReady(repoId))
193
200
  return;
194
201
  const handle = this.repos.get(repoId);
195
202
  if (!handle)
196
203
  throw new Error(`Unknown repo: ${repoId}`);
197
204
  try {
198
- await initKuzu(repoId, handle.kuzuPath);
205
+ await initLbug(repoId, handle.lbugPath);
199
206
  this.initializedRepos.add(repoId);
200
207
  }
201
208
  catch (err) {
@@ -335,9 +342,9 @@ export class LocalBackend {
335
342
  // Find processes this symbol participates in
336
343
  let processRows = [];
337
344
  try {
338
- processRows = await executeParameterized(repo.id, `
339
- MATCH (n {id: $nodeId})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
340
- RETURN p.id AS pid, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
345
+ processRows = await executeParameterized(repo.id, `
346
+ MATCH (n {id: $nodeId})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
347
+ RETURN p.id AS pid, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
341
348
  `, { nodeId: sym.nodeId });
342
349
  }
343
350
  catch (e) {
@@ -347,10 +354,10 @@ export class LocalBackend {
347
354
  let cohesion = 0;
348
355
  let module;
349
356
  try {
350
- const cohesionRows = await executeParameterized(repo.id, `
351
- MATCH (n {id: $nodeId})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
352
- RETURN c.cohesion AS cohesion, c.heuristicLabel AS module
353
- LIMIT 1
357
+ const cohesionRows = await executeParameterized(repo.id, `
358
+ MATCH (n {id: $nodeId})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
359
+ RETURN c.cohesion AS cohesion, c.heuristicLabel AS module
360
+ LIMIT 1
354
361
  `, { nodeId: sym.nodeId });
355
362
  if (cohesionRows.length > 0) {
356
363
  cohesion = (cohesionRows[0].cohesion ?? cohesionRows[0][0]) || 0;
@@ -364,9 +371,9 @@ export class LocalBackend {
364
371
  let content;
365
372
  if (includeContent) {
366
373
  try {
367
- const contentRows = await executeParameterized(repo.id, `
368
- MATCH (n {id: $nodeId})
369
- RETURN n.content AS content
374
+ const contentRows = await executeParameterized(repo.id, `
375
+ MATCH (n {id: $nodeId})
376
+ RETURN n.content AS content
370
377
  `, { nodeId: sym.nodeId });
371
378
  if (contentRows.length > 0) {
372
379
  content = contentRows[0].content ?? contentRows[0][0];
@@ -458,13 +465,13 @@ export class LocalBackend {
458
465
  };
459
466
  }
460
467
  /**
461
- * BM25 keyword search helper - uses KuzuDB FTS for always-fresh results
468
+ * BM25 keyword search helper - uses LadybugDB FTS for always-fresh results
462
469
  */
463
470
  async bm25Search(repo, query, limit) {
464
- const { searchFTSFromKuzu } = await import('../../core/search/bm25-index.js');
471
+ const { searchFTSFromLbug } = await import('../../core/search/bm25-index.js');
465
472
  let bm25Results;
466
473
  try {
467
- bm25Results = await searchFTSFromKuzu(query, limit, repo.id);
474
+ bm25Results = await searchFTSFromLbug(query, limit, repo.id);
468
475
  }
469
476
  catch (err) {
470
477
  console.error('GitNexus: BM25/FTS search failed (FTS indexes may not exist) -', err.message);
@@ -474,11 +481,11 @@ export class LocalBackend {
474
481
  for (const bm25Result of bm25Results) {
475
482
  const fullPath = bm25Result.filePath;
476
483
  try {
477
- const symbols = await executeParameterized(repo.id, `
478
- MATCH (n)
479
- WHERE n.filePath = $filePath
480
- RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
481
- LIMIT 3
484
+ const symbols = await executeParameterized(repo.id, `
485
+ MATCH (n)
486
+ WHERE n.filePath = $filePath
487
+ RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
488
+ LIMIT 3
482
489
  `, { filePath: fullPath });
483
490
  if (symbols.length > 0) {
484
491
  for (const sym of symbols) {
@@ -528,14 +535,14 @@ export class LocalBackend {
528
535
  const queryVec = await embedQuery(query);
529
536
  const dims = getEmbeddingDims();
530
537
  const queryVecStr = `[${queryVec.join(',')}]`;
531
- const vectorQuery = `
532
- CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx',
533
- CAST(${queryVecStr} AS FLOAT[${dims}]), ${limit})
534
- YIELD node AS emb, distance
535
- WITH emb, distance
536
- WHERE distance < 0.6
537
- RETURN emb.nodeId AS nodeId, distance
538
- ORDER BY distance
538
+ const vectorQuery = `
539
+ CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx',
540
+ CAST(${queryVecStr} AS FLOAT[${dims}]), ${limit})
541
+ YIELD node AS emb, distance
542
+ WITH emb, distance
543
+ WHERE distance < 0.6
544
+ RETURN emb.nodeId AS nodeId, distance
545
+ ORDER BY distance
539
546
  `;
540
547
  const embResults = await executeQuery(repo.id, vectorQuery);
541
548
  if (embResults.length === 0)
@@ -582,8 +589,8 @@ export class LocalBackend {
582
589
  }
583
590
  async cypher(repo, params) {
584
591
  await this.ensureInitialized(repo.id);
585
- if (!isKuzuReady(repo.id)) {
586
- return { error: 'KuzuDB not ready. Index may be corrupted.' };
592
+ if (!isLbugReady(repo.id)) {
593
+ return { error: 'LadybugDB not ready. Index may be corrupted.' };
587
594
  }
588
595
  // Block write operations (defense-in-depth — DB is already read-only)
589
596
  if (CYPHER_WRITE_RE.test(params.query)) {
@@ -628,7 +635,7 @@ export class LocalBackend {
628
635
  /**
629
636
  * Aggregate same-named clusters: group by heuristicLabel, sum symbols,
630
637
  * weighted-average cohesion, filter out tiny clusters (<5 symbols).
631
- * Raw communities stay intact in KuzuDB for Cypher queries.
638
+ * Raw communities stay intact in LadybugDB for Cypher queries.
632
639
  */
633
640
  aggregateClusters(clusters) {
634
641
  const groups = new Map();
@@ -675,11 +682,11 @@ export class LocalBackend {
675
682
  try {
676
683
  // Fetch more raw communities than the display limit so aggregation has enough data
677
684
  const rawLimit = Math.max(limit * 5, 200);
678
- const clusters = await executeQuery(repo.id, `
679
- MATCH (c:Community)
680
- RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
681
- ORDER BY c.symbolCount DESC
682
- LIMIT ${rawLimit}
685
+ const clusters = await executeQuery(repo.id, `
686
+ MATCH (c:Community)
687
+ RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
688
+ ORDER BY c.symbolCount DESC
689
+ LIMIT ${rawLimit}
683
690
  `);
684
691
  const rawClusters = clusters.map((c) => ({
685
692
  id: c.id || c[0],
@@ -696,11 +703,11 @@ export class LocalBackend {
696
703
  }
697
704
  if (params.showProcesses !== false) {
698
705
  try {
699
- const processes = await executeQuery(repo.id, `
700
- MATCH (p:Process)
701
- RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
702
- ORDER BY p.stepCount DESC
703
- LIMIT ${limit}
706
+ const processes = await executeQuery(repo.id, `
707
+ MATCH (p:Process)
708
+ RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
709
+ ORDER BY p.stepCount DESC
710
+ LIMIT ${limit}
704
711
  `);
705
712
  result.processes = processes.map((p) => ({
706
713
  id: p.id || p[0],
@@ -730,10 +737,10 @@ export class LocalBackend {
730
737
  // Step 1: Find the symbol
731
738
  let symbols;
732
739
  if (uid) {
733
- symbols = await executeParameterized(repo.id, `
734
- MATCH (n {id: $uid})
735
- RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}
736
- LIMIT 1
740
+ symbols = await executeParameterized(repo.id, `
741
+ MATCH (n {id: $uid})
742
+ RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}
743
+ LIMIT 1
737
744
  `, { uid });
738
745
  }
739
746
  else {
@@ -752,10 +759,10 @@ export class LocalBackend {
752
759
  whereClause = `WHERE n.name = $symName`;
753
760
  queryParams = { symName: name };
754
761
  }
755
- symbols = await executeParameterized(repo.id, `
756
- MATCH (n) ${whereClause}
757
- RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}
758
- LIMIT 10
762
+ symbols = await executeParameterized(repo.id, `
763
+ MATCH (n) ${whereClause}
764
+ RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}
765
+ LIMIT 10
759
766
  `, queryParams);
760
767
  }
761
768
  if (symbols.length === 0) {
@@ -779,25 +786,25 @@ export class LocalBackend {
779
786
  const sym = symbols[0];
780
787
  const symId = sym.id || sym[0];
781
788
  // Categorized incoming refs
782
- const incomingRows = await executeParameterized(repo.id, `
783
- MATCH (caller)-[r:CodeRelation]->(n {id: $symId})
784
- WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
785
- RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
786
- LIMIT 30
789
+ const incomingRows = await executeParameterized(repo.id, `
790
+ MATCH (caller)-[r:CodeRelation]->(n {id: $symId})
791
+ WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
792
+ RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
793
+ LIMIT 30
787
794
  `, { symId });
788
795
  // Categorized outgoing refs
789
- const outgoingRows = await executeParameterized(repo.id, `
790
- MATCH (n {id: $symId})-[r:CodeRelation]->(target)
791
- WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
792
- RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
793
- LIMIT 30
796
+ const outgoingRows = await executeParameterized(repo.id, `
797
+ MATCH (n {id: $symId})-[r:CodeRelation]->(target)
798
+ WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
799
+ RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
800
+ LIMIT 30
794
801
  `, { symId });
795
802
  // Process participation
796
803
  let processRows = [];
797
804
  try {
798
- processRows = await executeParameterized(repo.id, `
799
- MATCH (n {id: $symId})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
800
- RETURN p.id AS pid, p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
805
+ processRows = await executeParameterized(repo.id, `
806
+ MATCH (n {id: $symId})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
807
+ RETURN p.id AS pid, p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
801
808
  `, { symId });
802
809
  }
803
810
  catch (e) {
@@ -852,10 +859,10 @@ export class LocalBackend {
852
859
  return this.context(repo, { name });
853
860
  }
854
861
  if (type === 'cluster') {
855
- const clusters = await executeParameterized(repo.id, `
856
- MATCH (c:Community)
857
- WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
858
- RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
862
+ const clusters = await executeParameterized(repo.id, `
863
+ MATCH (c:Community)
864
+ WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
865
+ RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
859
866
  `, { clusterName: name });
860
867
  if (clusters.length === 0)
861
868
  return { error: `Cluster '${name}' not found` };
@@ -869,11 +876,11 @@ export class LocalBackend {
869
876
  totalSymbols += s;
870
877
  weightedCohesion += (c.cohesion || 0) * s;
871
878
  }
872
- const members = await executeParameterized(repo.id, `
873
- MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
874
- WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
875
- RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
876
- LIMIT 30
879
+ const members = await executeParameterized(repo.id, `
880
+ MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
881
+ WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
882
+ RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
883
+ LIMIT 30
877
884
  `, { clusterName: name });
878
885
  return {
879
886
  cluster: {
@@ -890,20 +897,20 @@ export class LocalBackend {
890
897
  };
891
898
  }
892
899
  if (type === 'process') {
893
- const processes = await executeParameterized(repo.id, `
894
- MATCH (p:Process)
895
- WHERE p.label = $processName OR p.heuristicLabel = $processName
896
- RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
897
- LIMIT 1
900
+ const processes = await executeParameterized(repo.id, `
901
+ MATCH (p:Process)
902
+ WHERE p.label = $processName OR p.heuristicLabel = $processName
903
+ RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
904
+ LIMIT 1
898
905
  `, { processName: name });
899
906
  if (processes.length === 0)
900
907
  return { error: `Process '${name}' not found` };
901
908
  const proc = processes[0];
902
909
  const procId = proc.id || proc[0];
903
- const steps = await executeParameterized(repo.id, `
904
- MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: $procId})
905
- RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
906
- ORDER BY r.step
910
+ const steps = await executeParameterized(repo.id, `
911
+ MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: $procId})
912
+ RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
913
+ ORDER BY r.step
907
914
  `, { procId });
908
915
  return {
909
916
  process: {
@@ -964,10 +971,10 @@ export class LocalBackend {
964
971
  for (const file of changedFiles) {
965
972
  const normalizedFile = file.replace(/\\/g, '/');
966
973
  try {
967
- const symbols = await executeParameterized(repo.id, `
968
- MATCH (n) WHERE n.filePath CONTAINS $filePath
969
- RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
970
- LIMIT 20
974
+ const symbols = await executeParameterized(repo.id, `
975
+ MATCH (n) WHERE n.filePath CONTAINS $filePath
976
+ RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
977
+ LIMIT 20
971
978
  `, { filePath: normalizedFile });
972
979
  for (const sym of symbols) {
973
980
  changedSymbols.push({
@@ -987,9 +994,9 @@ export class LocalBackend {
987
994
  const affectedProcesses = new Map();
988
995
  for (const sym of changedSymbols) {
989
996
  try {
990
- const procs = await executeParameterized(repo.id, `
991
- MATCH (n {id: $nodeId})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
992
- RETURN p.id AS pid, p.heuristicLabel AS label, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
997
+ const procs = await executeParameterized(repo.id, `
998
+ MATCH (n {id: $nodeId})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
999
+ RETURN p.id AS pid, p.heuristicLabel AS label, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
993
1000
  `, { nodeId: sym.id });
994
1001
  for (const proc of procs) {
995
1002
  const pid = proc.pid || proc[0];
@@ -1182,6 +1189,22 @@ export class LocalBackend {
1182
1189
  };
1183
1190
  }
1184
1191
  async impact(repo, params) {
1192
+ try {
1193
+ return await this._impactImpl(repo, params);
1194
+ }
1195
+ catch (err) {
1196
+ // Return structured error instead of crashing (#321)
1197
+ return {
1198
+ error: (err instanceof Error ? err.message : String(err)) || 'Impact analysis failed',
1199
+ target: { name: params.target },
1200
+ direction: params.direction,
1201
+ impactedCount: 0,
1202
+ risk: 'UNKNOWN',
1203
+ suggestion: 'The graph query failed — try gitnexus context <symbol> as a fallback',
1204
+ };
1205
+ }
1206
+ }
1207
+ async _impactImpl(repo, params) {
1185
1208
  await this.ensureInitialized(repo.id);
1186
1209
  const { target, direction } = params;
1187
1210
  const maxDepth = params.maxDepth || 3;
@@ -1193,11 +1216,11 @@ export class LocalBackend {
1193
1216
  const minConfidence = params.minConfidence ?? 0;
1194
1217
  const relTypeFilter = relationTypes.map(t => `'${t}'`).join(', ');
1195
1218
  const confidenceFilter = minConfidence > 0 ? ` AND r.confidence >= ${minConfidence}` : '';
1196
- const targets = await executeParameterized(repo.id, `
1197
- MATCH (n)
1198
- WHERE n.name = $targetName
1199
- RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
1200
- LIMIT 1
1219
+ const targets = await executeParameterized(repo.id, `
1220
+ MATCH (n)
1221
+ WHERE n.name = $targetName
1222
+ RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
1223
+ LIMIT 1
1201
1224
  `, { targetName: target });
1202
1225
  if (targets.length === 0)
1203
1226
  return { error: `Target '${target}' not found` };
@@ -1206,6 +1229,7 @@ export class LocalBackend {
1206
1229
  const impacted = [];
1207
1230
  const visited = new Set([symId]);
1208
1231
  let frontier = [symId];
1232
+ let traversalComplete = true;
1209
1233
  for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
1210
1234
  const nextFrontier = [];
1211
1235
  // Batch frontier nodes into a single Cypher query per depth level
@@ -1237,6 +1261,10 @@ export class LocalBackend {
1237
1261
  }
1238
1262
  catch (e) {
1239
1263
  logQueryError('impact:depth-traversal', e);
1264
+ // Break out of depth loop on query failure but return partial results
1265
+ // collected so far, rather than silently swallowing the error (#321)
1266
+ traversalComplete = false;
1267
+ break;
1240
1268
  }
1241
1269
  frontier = nextFrontier;
1242
1270
  }
@@ -1255,24 +1283,24 @@ export class LocalBackend {
1255
1283
  const d1Ids = (grouped[1] || []).map((i) => `'${i.id.replace(/'/g, "''")}'`).join(', ');
1256
1284
  // Affected processes: which execution flows are broken and at which step
1257
1285
  const [processRows, moduleRows, directModuleRows] = await Promise.all([
1258
- executeQuery(repo.id, `
1259
- MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
1260
- WHERE s.id IN [${allIds}]
1261
- RETURN p.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits, MIN(r.step) AS minStep, p.stepCount AS stepCount
1262
- ORDER BY hits DESC
1263
- LIMIT 20
1286
+ executeQuery(repo.id, `
1287
+ MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
1288
+ WHERE s.id IN [${allIds}]
1289
+ RETURN p.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits, MIN(r.step) AS minStep, p.stepCount AS stepCount
1290
+ ORDER BY hits DESC
1291
+ LIMIT 20
1264
1292
  `).catch(() => []),
1265
- executeQuery(repo.id, `
1266
- MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
1267
- WHERE s.id IN [${allIds}]
1268
- RETURN c.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits
1269
- ORDER BY hits DESC
1270
- LIMIT 20
1293
+ executeQuery(repo.id, `
1294
+ MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
1295
+ WHERE s.id IN [${allIds}]
1296
+ RETURN c.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits
1297
+ ORDER BY hits DESC
1298
+ LIMIT 20
1271
1299
  `).catch(() => []),
1272
- d1Ids ? executeQuery(repo.id, `
1273
- MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
1274
- WHERE s.id IN [${d1Ids}]
1275
- RETURN DISTINCT c.heuristicLabel AS name
1300
+ d1Ids ? executeQuery(repo.id, `
1301
+ MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
1302
+ WHERE s.id IN [${d1Ids}]
1303
+ RETURN DISTINCT c.heuristicLabel AS name
1276
1304
  `).catch(() => []) : Promise.resolve([]),
1277
1305
  ]);
1278
1306
  affectedProcesses = processRows.map((r) => ({
@@ -1314,6 +1342,7 @@ export class LocalBackend {
1314
1342
  direction,
1315
1343
  impactedCount: impacted.length,
1316
1344
  risk,
1345
+ ...(!traversalComplete && { partial: true }),
1317
1346
  summary: {
1318
1347
  direct: directCount,
1319
1348
  processes_affected: processCount,
@@ -1334,11 +1363,11 @@ export class LocalBackend {
1334
1363
  await this.ensureInitialized(repo.id);
1335
1364
  try {
1336
1365
  const rawLimit = Math.max(limit * 5, 200);
1337
- const clusters = await executeQuery(repo.id, `
1338
- MATCH (c:Community)
1339
- RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
1340
- ORDER BY c.symbolCount DESC
1341
- LIMIT ${rawLimit}
1366
+ const clusters = await executeQuery(repo.id, `
1367
+ MATCH (c:Community)
1368
+ RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
1369
+ ORDER BY c.symbolCount DESC
1370
+ LIMIT ${rawLimit}
1342
1371
  `);
1343
1372
  const rawClusters = clusters.map((c) => ({
1344
1373
  id: c.id || c[0],
@@ -1361,11 +1390,11 @@ export class LocalBackend {
1361
1390
  const repo = await this.resolveRepo(repoName);
1362
1391
  await this.ensureInitialized(repo.id);
1363
1392
  try {
1364
- const processes = await executeQuery(repo.id, `
1365
- MATCH (p:Process)
1366
- RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
1367
- ORDER BY p.stepCount DESC
1368
- LIMIT ${limit}
1393
+ const processes = await executeQuery(repo.id, `
1394
+ MATCH (p:Process)
1395
+ RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
1396
+ ORDER BY p.stepCount DESC
1397
+ LIMIT ${limit}
1369
1398
  `);
1370
1399
  return {
1371
1400
  processes: processes.map((p) => ({
@@ -1388,10 +1417,10 @@ export class LocalBackend {
1388
1417
  async queryClusterDetail(name, repoName) {
1389
1418
  const repo = await this.resolveRepo(repoName);
1390
1419
  await this.ensureInitialized(repo.id);
1391
- const clusters = await executeParameterized(repo.id, `
1392
- MATCH (c:Community)
1393
- WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
1394
- RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
1420
+ const clusters = await executeParameterized(repo.id, `
1421
+ MATCH (c:Community)
1422
+ WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
1423
+ RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
1395
1424
  `, { clusterName: name });
1396
1425
  if (clusters.length === 0)
1397
1426
  return { error: `Cluster '${name}' not found` };
@@ -1405,11 +1434,11 @@ export class LocalBackend {
1405
1434
  totalSymbols += s;
1406
1435
  weightedCohesion += (c.cohesion || 0) * s;
1407
1436
  }
1408
- const members = await executeParameterized(repo.id, `
1409
- MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
1410
- WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
1411
- RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
1412
- LIMIT 30
1437
+ const members = await executeParameterized(repo.id, `
1438
+ MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
1439
+ WHERE c.label = $clusterName OR c.heuristicLabel = $clusterName
1440
+ RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
1441
+ LIMIT 30
1413
1442
  `, { clusterName: name });
1414
1443
  return {
1415
1444
  cluster: {
@@ -1432,20 +1461,20 @@ export class LocalBackend {
1432
1461
  async queryProcessDetail(name, repoName) {
1433
1462
  const repo = await this.resolveRepo(repoName);
1434
1463
  await this.ensureInitialized(repo.id);
1435
- const processes = await executeParameterized(repo.id, `
1436
- MATCH (p:Process)
1437
- WHERE p.label = $processName OR p.heuristicLabel = $processName
1438
- RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
1439
- LIMIT 1
1464
+ const processes = await executeParameterized(repo.id, `
1465
+ MATCH (p:Process)
1466
+ WHERE p.label = $processName OR p.heuristicLabel = $processName
1467
+ RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
1468
+ LIMIT 1
1440
1469
  `, { processName: name });
1441
1470
  if (processes.length === 0)
1442
1471
  return { error: `Process '${name}' not found` };
1443
1472
  const proc = processes[0];
1444
1473
  const procId = proc.id || proc[0];
1445
- const steps = await executeParameterized(repo.id, `
1446
- MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: $procId})
1447
- RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
1448
- ORDER BY r.step
1474
+ const steps = await executeParameterized(repo.id, `
1475
+ MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: $procId})
1476
+ RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
1477
+ ORDER BY r.step
1449
1478
  `, { procId });
1450
1479
  return {
1451
1480
  process: {
@@ -1458,7 +1487,7 @@ export class LocalBackend {
1458
1487
  };
1459
1488
  }
1460
1489
  async disconnect() {
1461
- await closeKuzu(); // close all connections
1490
+ await closeLbug(); // close all connections
1462
1491
  // Note: we intentionally do NOT call disposeEmbedder() here.
1463
1492
  // ONNX Runtime's native cleanup segfaults on macOS and some Linux configs,
1464
1493
  // and importing the embedder module on Node v24+ crashes if onnxruntime