@optave/codegraph 3.1.3 → 3.1.4

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 (185) hide show
  1. package/README.md +17 -19
  2. package/package.json +10 -7
  3. package/src/analysis/context.js +408 -0
  4. package/src/analysis/dependencies.js +341 -0
  5. package/src/analysis/exports.js +130 -0
  6. package/src/analysis/impact.js +463 -0
  7. package/src/analysis/module-map.js +322 -0
  8. package/src/analysis/roles.js +45 -0
  9. package/src/analysis/symbol-lookup.js +232 -0
  10. package/src/ast-analysis/shared.js +5 -4
  11. package/src/batch.js +2 -1
  12. package/src/builder/context.js +85 -0
  13. package/src/builder/helpers.js +218 -0
  14. package/src/builder/incremental.js +178 -0
  15. package/src/builder/pipeline.js +130 -0
  16. package/src/builder/stages/build-edges.js +297 -0
  17. package/src/builder/stages/build-structure.js +113 -0
  18. package/src/builder/stages/collect-files.js +44 -0
  19. package/src/builder/stages/detect-changes.js +413 -0
  20. package/src/builder/stages/finalize.js +139 -0
  21. package/src/builder/stages/insert-nodes.js +195 -0
  22. package/src/builder/stages/parse-files.js +28 -0
  23. package/src/builder/stages/resolve-imports.js +143 -0
  24. package/src/builder/stages/run-analyses.js +44 -0
  25. package/src/builder.js +10 -1485
  26. package/src/cfg.js +1 -2
  27. package/src/cli/commands/ast.js +26 -0
  28. package/src/cli/commands/audit.js +46 -0
  29. package/src/cli/commands/batch.js +68 -0
  30. package/src/cli/commands/branch-compare.js +21 -0
  31. package/src/cli/commands/build.js +26 -0
  32. package/src/cli/commands/cfg.js +30 -0
  33. package/src/cli/commands/check.js +79 -0
  34. package/src/cli/commands/children.js +31 -0
  35. package/src/cli/commands/co-change.js +65 -0
  36. package/src/cli/commands/communities.js +23 -0
  37. package/src/cli/commands/complexity.js +45 -0
  38. package/src/cli/commands/context.js +34 -0
  39. package/src/cli/commands/cycles.js +28 -0
  40. package/src/cli/commands/dataflow.js +32 -0
  41. package/src/cli/commands/deps.js +16 -0
  42. package/src/cli/commands/diff-impact.js +30 -0
  43. package/src/cli/commands/embed.js +30 -0
  44. package/src/cli/commands/export.js +75 -0
  45. package/src/cli/commands/exports.js +18 -0
  46. package/src/cli/commands/flow.js +36 -0
  47. package/src/cli/commands/fn-impact.js +30 -0
  48. package/src/cli/commands/impact.js +16 -0
  49. package/src/cli/commands/info.js +76 -0
  50. package/src/cli/commands/map.js +19 -0
  51. package/src/cli/commands/mcp.js +18 -0
  52. package/src/cli/commands/models.js +19 -0
  53. package/src/cli/commands/owners.js +25 -0
  54. package/src/cli/commands/path.js +36 -0
  55. package/src/cli/commands/plot.js +80 -0
  56. package/src/cli/commands/query.js +49 -0
  57. package/src/cli/commands/registry.js +100 -0
  58. package/src/cli/commands/roles.js +34 -0
  59. package/src/cli/commands/search.js +42 -0
  60. package/src/cli/commands/sequence.js +32 -0
  61. package/src/cli/commands/snapshot.js +61 -0
  62. package/src/cli/commands/stats.js +15 -0
  63. package/src/cli/commands/structure.js +32 -0
  64. package/src/cli/commands/triage.js +78 -0
  65. package/src/cli/commands/watch.js +12 -0
  66. package/src/cli/commands/where.js +24 -0
  67. package/src/cli/index.js +118 -0
  68. package/src/cli/shared/options.js +39 -0
  69. package/src/cli/shared/output.js +1 -0
  70. package/src/cli.js +11 -1522
  71. package/src/commands/check.js +5 -5
  72. package/src/commands/manifesto.js +3 -3
  73. package/src/commands/structure.js +1 -1
  74. package/src/communities.js +15 -87
  75. package/src/cycles.js +30 -85
  76. package/src/dataflow.js +1 -2
  77. package/src/db/connection.js +4 -4
  78. package/src/db/migrations.js +41 -0
  79. package/src/db/query-builder.js +6 -5
  80. package/src/db/repository/base.js +201 -0
  81. package/src/db/repository/graph-read.js +5 -2
  82. package/src/db/repository/in-memory-repository.js +584 -0
  83. package/src/db/repository/index.js +5 -1
  84. package/src/db/repository/nodes.js +63 -4
  85. package/src/db/repository/sqlite-repository.js +219 -0
  86. package/src/db.js +5 -0
  87. package/src/embeddings/generator.js +163 -0
  88. package/src/embeddings/index.js +13 -0
  89. package/src/embeddings/models.js +218 -0
  90. package/src/embeddings/search/cli-formatter.js +151 -0
  91. package/src/embeddings/search/filters.js +46 -0
  92. package/src/embeddings/search/hybrid.js +121 -0
  93. package/src/embeddings/search/keyword.js +68 -0
  94. package/src/embeddings/search/prepare.js +66 -0
  95. package/src/embeddings/search/semantic.js +145 -0
  96. package/src/embeddings/stores/fts5.js +27 -0
  97. package/src/embeddings/stores/sqlite-blob.js +24 -0
  98. package/src/embeddings/strategies/source.js +14 -0
  99. package/src/embeddings/strategies/structured.js +43 -0
  100. package/src/embeddings/strategies/text-utils.js +43 -0
  101. package/src/errors.js +78 -0
  102. package/src/export.js +217 -520
  103. package/src/extractors/csharp.js +10 -2
  104. package/src/extractors/go.js +3 -1
  105. package/src/extractors/helpers.js +71 -0
  106. package/src/extractors/java.js +9 -2
  107. package/src/extractors/javascript.js +38 -1
  108. package/src/extractors/php.js +3 -1
  109. package/src/extractors/python.js +14 -3
  110. package/src/extractors/rust.js +3 -1
  111. package/src/graph/algorithms/bfs.js +49 -0
  112. package/src/graph/algorithms/centrality.js +16 -0
  113. package/src/graph/algorithms/index.js +5 -0
  114. package/src/graph/algorithms/louvain.js +26 -0
  115. package/src/graph/algorithms/shortest-path.js +41 -0
  116. package/src/graph/algorithms/tarjan.js +49 -0
  117. package/src/graph/builders/dependency.js +91 -0
  118. package/src/graph/builders/index.js +3 -0
  119. package/src/graph/builders/structure.js +40 -0
  120. package/src/graph/builders/temporal.js +33 -0
  121. package/src/graph/classifiers/index.js +2 -0
  122. package/src/graph/classifiers/risk.js +85 -0
  123. package/src/graph/classifiers/roles.js +64 -0
  124. package/src/graph/index.js +13 -0
  125. package/src/graph/model.js +230 -0
  126. package/src/index.js +33 -210
  127. package/src/infrastructure/result-formatter.js +2 -21
  128. package/src/mcp/index.js +2 -0
  129. package/src/mcp/middleware.js +26 -0
  130. package/src/mcp/server.js +128 -0
  131. package/src/mcp/tool-registry.js +801 -0
  132. package/src/mcp/tools/ast-query.js +14 -0
  133. package/src/mcp/tools/audit.js +21 -0
  134. package/src/mcp/tools/batch-query.js +11 -0
  135. package/src/mcp/tools/branch-compare.js +10 -0
  136. package/src/mcp/tools/cfg.js +21 -0
  137. package/src/mcp/tools/check.js +43 -0
  138. package/src/mcp/tools/co-changes.js +20 -0
  139. package/src/mcp/tools/code-owners.js +12 -0
  140. package/src/mcp/tools/communities.js +15 -0
  141. package/src/mcp/tools/complexity.js +18 -0
  142. package/src/mcp/tools/context.js +17 -0
  143. package/src/mcp/tools/dataflow.js +26 -0
  144. package/src/mcp/tools/diff-impact.js +24 -0
  145. package/src/mcp/tools/execution-flow.js +26 -0
  146. package/src/mcp/tools/export-graph.js +57 -0
  147. package/src/mcp/tools/file-deps.js +12 -0
  148. package/src/mcp/tools/file-exports.js +13 -0
  149. package/src/mcp/tools/find-cycles.js +15 -0
  150. package/src/mcp/tools/fn-impact.js +15 -0
  151. package/src/mcp/tools/impact-analysis.js +12 -0
  152. package/src/mcp/tools/index.js +71 -0
  153. package/src/mcp/tools/list-functions.js +14 -0
  154. package/src/mcp/tools/list-repos.js +11 -0
  155. package/src/mcp/tools/module-map.js +6 -0
  156. package/src/mcp/tools/node-roles.js +14 -0
  157. package/src/mcp/tools/path.js +12 -0
  158. package/src/mcp/tools/query.js +30 -0
  159. package/src/mcp/tools/semantic-search.js +65 -0
  160. package/src/mcp/tools/sequence.js +17 -0
  161. package/src/mcp/tools/structure.js +15 -0
  162. package/src/mcp/tools/symbol-children.js +14 -0
  163. package/src/mcp/tools/triage.js +35 -0
  164. package/src/mcp/tools/where.js +13 -0
  165. package/src/mcp.js +2 -1470
  166. package/src/native.js +3 -1
  167. package/src/presentation/colors.js +44 -0
  168. package/src/presentation/export.js +444 -0
  169. package/src/presentation/result-formatter.js +21 -0
  170. package/src/presentation/sequence-renderer.js +43 -0
  171. package/src/presentation/table.js +47 -0
  172. package/src/presentation/viewer.js +634 -0
  173. package/src/queries.js +35 -2276
  174. package/src/resolve.js +1 -1
  175. package/src/sequence.js +2 -38
  176. package/src/shared/file-utils.js +153 -0
  177. package/src/shared/generators.js +125 -0
  178. package/src/shared/hierarchy.js +27 -0
  179. package/src/shared/normalize.js +59 -0
  180. package/src/snapshot.js +6 -5
  181. package/src/structure.js +15 -40
  182. package/src/triage.js +20 -72
  183. package/src/viewer.js +35 -656
  184. package/src/watcher.js +8 -148
  185. package/src/embedder.js +0 -1097
@@ -1,4 +1,4 @@
1
- import { findChild, nodeEndLine } from './helpers.js';
1
+ import { extractModifierVisibility, findChild, nodeEndLine } from './helpers.js';
2
2
 
3
3
  /**
4
4
  * Extract symbols from C# files.
@@ -133,6 +133,7 @@ export function extractCSharpSymbols(tree, _filePath) {
133
133
  line: node.startPosition.row + 1,
134
134
  endLine: nodeEndLine(node),
135
135
  children: params.length > 0 ? params : undefined,
136
+ visibility: extractModifierVisibility(node),
136
137
  });
137
138
  }
138
139
  break;
@@ -150,6 +151,7 @@ export function extractCSharpSymbols(tree, _filePath) {
150
151
  line: node.startPosition.row + 1,
151
152
  endLine: nodeEndLine(node),
152
153
  children: params.length > 0 ? params : undefined,
154
+ visibility: extractModifierVisibility(node),
153
155
  });
154
156
  }
155
157
  break;
@@ -165,6 +167,7 @@ export function extractCSharpSymbols(tree, _filePath) {
165
167
  kind: 'property',
166
168
  line: node.startPosition.row + 1,
167
169
  endLine: nodeEndLine(node),
170
+ visibility: extractModifierVisibility(node),
168
171
  });
169
172
  }
170
173
  break;
@@ -260,7 +263,12 @@ function extractCSharpClassFields(classNode) {
260
263
  if (!child || child.type !== 'variable_declarator') continue;
261
264
  const nameNode = child.childForFieldName('name');
262
265
  if (nameNode) {
263
- fields.push({ name: nameNode.text, kind: 'property', line: member.startPosition.row + 1 });
266
+ fields.push({
267
+ name: nameNode.text,
268
+ kind: 'property',
269
+ line: member.startPosition.row + 1,
270
+ visibility: extractModifierVisibility(member),
271
+ });
264
272
  }
265
273
  }
266
274
  }
@@ -1,4 +1,4 @@
1
- import { findChild, nodeEndLine } from './helpers.js';
1
+ import { findChild, goVisibility, nodeEndLine } from './helpers.js';
2
2
 
3
3
  /**
4
4
  * Extract symbols from Go files.
@@ -22,6 +22,7 @@ export function extractGoSymbols(tree, _filePath) {
22
22
  line: node.startPosition.row + 1,
23
23
  endLine: nodeEndLine(node),
24
24
  children: params.length > 0 ? params : undefined,
25
+ visibility: goVisibility(nameNode.text),
25
26
  });
26
27
  }
27
28
  break;
@@ -55,6 +56,7 @@ export function extractGoSymbols(tree, _filePath) {
55
56
  line: node.startPosition.row + 1,
56
57
  endLine: nodeEndLine(node),
57
58
  children: params.length > 0 ? params : undefined,
59
+ visibility: goVisibility(nameNode.text),
58
60
  });
59
61
  }
60
62
  break;
@@ -9,3 +9,74 @@ export function findChild(node, type) {
9
9
  }
10
10
  return null;
11
11
  }
12
+
13
+ /**
14
+ * Extract visibility from a node by scanning its children for modifier keywords.
15
+ * Works for Java, C#, PHP, and similar languages where modifiers are child nodes.
16
+ * @param {object} node - tree-sitter node
17
+ * @param {Set<string>} [modifierTypes] - node types that indicate modifiers
18
+ * @returns {'public'|'private'|'protected'|undefined}
19
+ */
20
+ const DEFAULT_MODIFIER_TYPES = new Set([
21
+ 'modifiers',
22
+ 'modifier',
23
+ 'visibility_modifier',
24
+ 'accessibility_modifier',
25
+ ]);
26
+ const VISIBILITY_KEYWORDS = new Set(['public', 'private', 'protected']);
27
+
28
+ /**
29
+ * Python convention: __name → private, _name → protected, else undefined.
30
+ */
31
+ export function pythonVisibility(name) {
32
+ if (name.startsWith('__') && name.endsWith('__')) return undefined; // dunder — public
33
+ if (name.startsWith('__')) return 'private';
34
+ if (name.startsWith('_')) return 'protected';
35
+ return undefined;
36
+ }
37
+
38
+ /**
39
+ * Go convention: uppercase first letter → public, lowercase → private.
40
+ */
41
+ export function goVisibility(name) {
42
+ if (!name) return undefined;
43
+ // Strip receiver prefix (e.g., "Receiver.Method" → check "Method")
44
+ const bare = name.includes('.') ? name.split('.').pop() : name;
45
+ if (!bare) return undefined;
46
+ return bare[0] === bare[0].toUpperCase() && bare[0] !== bare[0].toLowerCase()
47
+ ? 'public'
48
+ : 'private';
49
+ }
50
+
51
+ /**
52
+ * Rust: check for `visibility_modifier` child (pub, pub(crate), etc.).
53
+ */
54
+ export function rustVisibility(node) {
55
+ for (let i = 0; i < node.childCount; i++) {
56
+ const child = node.child(i);
57
+ if (!child) continue;
58
+ if (child.type === 'visibility_modifier') {
59
+ return 'public'; // pub, pub(crate), pub(super) all mean "visible"
60
+ }
61
+ }
62
+ return 'private';
63
+ }
64
+
65
+ export function extractModifierVisibility(node, modifierTypes = DEFAULT_MODIFIER_TYPES) {
66
+ for (let i = 0; i < node.childCount; i++) {
67
+ const child = node.child(i);
68
+ if (!child) continue;
69
+ // Direct keyword match (e.g., PHP visibility_modifier = "public")
70
+ if (modifierTypes.has(child.type)) {
71
+ const text = child.text;
72
+ if (VISIBILITY_KEYWORDS.has(text)) return text;
73
+ // C# 'private protected' — accessible to derived types in same assembly → protected
74
+ if (text === 'private protected') return 'protected';
75
+ // Compound modifiers node (Java: "public static") — scan its text for a keyword
76
+ for (const kw of VISIBILITY_KEYWORDS) {
77
+ if (text.includes(kw)) return kw;
78
+ }
79
+ }
80
+ }
81
+ return undefined;
82
+ }
@@ -1,4 +1,4 @@
1
- import { findChild, nodeEndLine } from './helpers.js';
1
+ import { extractModifierVisibility, findChild, nodeEndLine } from './helpers.js';
2
2
 
3
3
  /**
4
4
  * Extract symbols from Java files.
@@ -165,6 +165,7 @@ export function extractJavaSymbols(tree, _filePath) {
165
165
  line: node.startPosition.row + 1,
166
166
  endLine: nodeEndLine(node),
167
167
  children: params.length > 0 ? params : undefined,
168
+ visibility: extractModifierVisibility(node),
168
169
  });
169
170
  }
170
171
  break;
@@ -182,6 +183,7 @@ export function extractJavaSymbols(tree, _filePath) {
182
183
  line: node.startPosition.row + 1,
183
184
  endLine: nodeEndLine(node),
184
185
  children: params.length > 0 ? params : undefined,
186
+ visibility: extractModifierVisibility(node),
185
187
  });
186
188
  }
187
189
  break;
@@ -267,7 +269,12 @@ function extractClassFields(classNode) {
267
269
  if (!child || child.type !== 'variable_declarator') continue;
268
270
  const nameNode = child.childForFieldName('name');
269
271
  if (nameNode) {
270
- fields.push({ name: nameNode.text, kind: 'property', line: member.startPosition.row + 1 });
272
+ fields.push({
273
+ name: nameNode.text,
274
+ kind: 'property',
275
+ line: member.startPosition.row + 1,
276
+ visibility: extractModifierVisibility(member),
277
+ });
271
278
  }
272
279
  }
273
280
  }
@@ -77,12 +77,14 @@ function extractSymbolsQuery(tree, query) {
77
77
  const parentClass = findParentClass(c.meth_node);
78
78
  const fullName = parentClass ? `${parentClass}.${methName}` : methName;
79
79
  const methChildren = extractParameters(c.meth_node);
80
+ const methVis = extractVisibility(c.meth_node);
80
81
  definitions.push({
81
82
  name: fullName,
82
83
  kind: 'method',
83
84
  line: c.meth_node.startPosition.row + 1,
84
85
  endLine: nodeEndLine(c.meth_node),
85
86
  children: methChildren.length > 0 ? methChildren : undefined,
87
+ visibility: methVis,
86
88
  });
87
89
  } else if (c.iface_node) {
88
90
  // interface_declaration (TS/TSX only)
@@ -375,12 +377,14 @@ function extractSymbolsWalk(tree) {
375
377
  const parentClass = findParentClass(node);
376
378
  const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text;
377
379
  const methChildren = extractParameters(node);
380
+ const methVis = extractVisibility(node);
378
381
  definitions.push({
379
382
  name: fullName,
380
383
  kind: 'method',
381
384
  line: node.startPosition.row + 1,
382
385
  endLine: nodeEndLine(node),
383
386
  children: methChildren.length > 0 ? methChildren : undefined,
387
+ visibility: methVis,
384
388
  });
385
389
  }
386
390
  break;
@@ -701,13 +705,46 @@ function extractClassProperties(classNode) {
701
705
  nameNode.type === 'identifier' ||
702
706
  nameNode.type === 'private_property_identifier')
703
707
  ) {
704
- props.push({ name: nameNode.text, kind: 'property', line: child.startPosition.row + 1 });
708
+ // Private # fields: nameNode.type is 'private_property_identifier'
709
+ // TS modifiers: accessibility_modifier child on the field_definition
710
+ const vis =
711
+ nameNode.type === 'private_property_identifier' ? 'private' : extractVisibility(child);
712
+ props.push({
713
+ name: nameNode.text,
714
+ kind: 'property',
715
+ line: child.startPosition.row + 1,
716
+ visibility: vis,
717
+ });
705
718
  }
706
719
  }
707
720
  }
708
721
  return props;
709
722
  }
710
723
 
724
+ /**
725
+ * Extract visibility modifier from a class member node.
726
+ * Checks for TS access modifiers (public/private/protected) and JS private (#) fields.
727
+ * Returns 'public' | 'private' | 'protected' | undefined.
728
+ */
729
+ function extractVisibility(node) {
730
+ // Check for TS accessibility modifiers (accessibility_modifier child)
731
+ for (let i = 0; i < node.childCount; i++) {
732
+ const child = node.child(i);
733
+ if (!child) continue;
734
+ if (child.type === 'accessibility_modifier') {
735
+ const text = child.text;
736
+ if (text === 'private' || text === 'protected' || text === 'public') return text;
737
+ }
738
+ }
739
+ // Check for JS private name (# prefix) — try multiple field names
740
+ const nameNode =
741
+ node.childForFieldName('name') || node.childForFieldName('property') || node.child(0);
742
+ if (nameNode && nameNode.type === 'private_property_identifier') {
743
+ return 'private';
744
+ }
745
+ return undefined;
746
+ }
747
+
711
748
  function isConstantValue(valueNode) {
712
749
  if (!valueNode) return false;
713
750
  const t = valueNode.type;
@@ -1,4 +1,4 @@
1
- import { findChild, nodeEndLine } from './helpers.js';
1
+ import { extractModifierVisibility, findChild, nodeEndLine } from './helpers.js';
2
2
 
3
3
  function extractPhpParameters(fnNode) {
4
4
  const params = [];
@@ -35,6 +35,7 @@ function extractPhpClassChildren(classNode) {
35
35
  name: varNode.text,
36
36
  kind: 'property',
37
37
  line: member.startPosition.row + 1,
38
+ visibility: extractModifierVisibility(member),
38
39
  });
39
40
  }
40
41
  }
@@ -231,6 +232,7 @@ export function extractPHPSymbols(tree, _filePath) {
231
232
  line: node.startPosition.row + 1,
232
233
  endLine: nodeEndLine(node),
233
234
  children: params.length > 0 ? params : undefined,
235
+ visibility: extractModifierVisibility(node),
234
236
  });
235
237
  }
236
238
  break;
@@ -1,4 +1,4 @@
1
- import { findChild, nodeEndLine } from './helpers.js';
1
+ import { findChild, nodeEndLine, pythonVisibility } from './helpers.js';
2
2
 
3
3
  /**
4
4
  * Extract symbols from Python files.
@@ -30,6 +30,7 @@ export function extractPythonSymbols(tree, _filePath) {
30
30
  endLine: nodeEndLine(node),
31
31
  decorators,
32
32
  children: fnChildren.length > 0 ? fnChildren : undefined,
33
+ visibility: pythonVisibility(nameNode.text),
33
34
  });
34
35
  }
35
36
  break;
@@ -209,7 +210,12 @@ export function extractPythonSymbols(tree, _filePath) {
209
210
  const left = assignment.childForFieldName('left');
210
211
  if (left && left.type === 'identifier' && !seen.has(left.text)) {
211
212
  seen.add(left.text);
212
- props.push({ name: left.text, kind: 'property', line: child.startPosition.row + 1 });
213
+ props.push({
214
+ name: left.text,
215
+ kind: 'property',
216
+ line: child.startPosition.row + 1,
217
+ visibility: pythonVisibility(left.text),
218
+ });
213
219
  }
214
220
  }
215
221
  }
@@ -262,7 +268,12 @@ export function extractPythonSymbols(tree, _filePath) {
262
268
  !seen.has(attr.text)
263
269
  ) {
264
270
  seen.add(attr.text);
265
- props.push({ name: attr.text, kind: 'property', line: stmt.startPosition.row + 1 });
271
+ props.push({
272
+ name: attr.text,
273
+ kind: 'property',
274
+ line: stmt.startPosition.row + 1,
275
+ visibility: pythonVisibility(attr.text),
276
+ });
266
277
  }
267
278
  }
268
279
  }
@@ -1,4 +1,4 @@
1
- import { findChild, nodeEndLine } from './helpers.js';
1
+ import { findChild, nodeEndLine, rustVisibility } from './helpers.js';
2
2
 
3
3
  /**
4
4
  * Extract symbols from Rust files.
@@ -37,6 +37,7 @@ export function extractRustSymbols(tree, _filePath) {
37
37
  line: node.startPosition.row + 1,
38
38
  endLine: nodeEndLine(node),
39
39
  children: params.length > 0 ? params : undefined,
40
+ visibility: rustVisibility(node),
40
41
  });
41
42
  }
42
43
  break;
@@ -52,6 +53,7 @@ export function extractRustSymbols(tree, _filePath) {
52
53
  line: node.startPosition.row + 1,
53
54
  endLine: nodeEndLine(node),
54
55
  children: fields.length > 0 ? fields : undefined,
56
+ visibility: rustVisibility(node),
55
57
  });
56
58
  }
57
59
  break;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Breadth-first traversal on a CodeGraph.
3
+ *
4
+ * @param {import('../model.js').CodeGraph} graph
5
+ * @param {string|string[]} startIds - One or more starting node IDs
6
+ * @param {{ maxDepth?: number, direction?: 'forward'|'backward'|'both' }} [opts]
7
+ * @returns {Map<string, number>} nodeId → depth from nearest start node
8
+ */
9
+ export function bfs(graph, startIds, opts = {}) {
10
+ const maxDepth = opts.maxDepth ?? Infinity;
11
+ const direction = opts.direction ?? 'forward';
12
+ const starts = Array.isArray(startIds) ? startIds : [startIds];
13
+
14
+ const depths = new Map();
15
+ const queue = [];
16
+
17
+ for (const id of starts) {
18
+ const key = String(id);
19
+ if (graph.hasNode(key)) {
20
+ depths.set(key, 0);
21
+ queue.push(key);
22
+ }
23
+ }
24
+
25
+ let head = 0;
26
+ while (head < queue.length) {
27
+ const current = queue[head++];
28
+ const depth = depths.get(current);
29
+ if (depth >= maxDepth) continue;
30
+
31
+ let neighbors;
32
+ if (direction === 'forward') {
33
+ neighbors = graph.successors(current);
34
+ } else if (direction === 'backward') {
35
+ neighbors = graph.predecessors(current);
36
+ } else {
37
+ neighbors = graph.neighbors(current);
38
+ }
39
+
40
+ for (const n of neighbors) {
41
+ if (!depths.has(n)) {
42
+ depths.set(n, depth + 1);
43
+ queue.push(n);
44
+ }
45
+ }
46
+ }
47
+
48
+ return depths;
49
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Fan-in / fan-out centrality for all nodes in a CodeGraph.
3
+ *
4
+ * @param {import('../model.js').CodeGraph} graph
5
+ * @returns {Map<string, { fanIn: number, fanOut: number }>}
6
+ */
7
+ export function fanInOut(graph) {
8
+ const result = new Map();
9
+ for (const id of graph.nodeIds()) {
10
+ result.set(id, {
11
+ fanIn: graph.inDegree(id),
12
+ fanOut: graph.outDegree(id),
13
+ });
14
+ }
15
+ return result;
16
+ }
@@ -0,0 +1,5 @@
1
+ export { bfs } from './bfs.js';
2
+ export { fanInOut } from './centrality.js';
3
+ export { louvainCommunities } from './louvain.js';
4
+ export { shortestPath } from './shortest-path.js';
5
+ export { tarjan } from './tarjan.js';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Louvain community detection via graphology.
3
+ *
4
+ * @param {import('../model.js').CodeGraph} graph
5
+ * @param {{ resolution?: number }} [opts]
6
+ * @returns {{ assignments: Map<string, number>, modularity: number }}
7
+ */
8
+ import graphologyLouvain from 'graphology-communities-louvain';
9
+
10
+ export function louvainCommunities(graph, opts = {}) {
11
+ const gy = graph.toGraphology({ type: 'undirected' });
12
+
13
+ if (gy.order === 0 || gy.size === 0) {
14
+ return { assignments: new Map(), modularity: 0 };
15
+ }
16
+
17
+ const resolution = opts.resolution ?? 1.0;
18
+ const details = graphologyLouvain.detailed(gy, { resolution });
19
+
20
+ const assignments = new Map();
21
+ for (const [nodeId, communityId] of Object.entries(details.communities)) {
22
+ assignments.set(nodeId, communityId);
23
+ }
24
+
25
+ return { assignments, modularity: details.modularity };
26
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * BFS-based shortest path on a CodeGraph.
3
+ *
4
+ * @param {import('../model.js').CodeGraph} graph
5
+ * @param {string} fromId
6
+ * @param {string} toId
7
+ * @returns {string[]|null} Path from fromId to toId (inclusive), or null if unreachable
8
+ */
9
+ export function shortestPath(graph, fromId, toId) {
10
+ const from = String(fromId);
11
+ const to = String(toId);
12
+
13
+ if (!graph.hasNode(from) || !graph.hasNode(to)) return null;
14
+ if (from === to) return [from];
15
+
16
+ const parent = new Map();
17
+ parent.set(from, null);
18
+ const queue = [from];
19
+ let head = 0;
20
+
21
+ while (head < queue.length) {
22
+ const current = queue[head++];
23
+ for (const neighbor of graph.successors(current)) {
24
+ if (parent.has(neighbor)) continue;
25
+ parent.set(neighbor, current);
26
+ if (neighbor === to) {
27
+ // Reconstruct path
28
+ const path = [];
29
+ let node = to;
30
+ while (node !== null) {
31
+ path.push(node);
32
+ node = parent.get(node);
33
+ }
34
+ return path.reverse();
35
+ }
36
+ queue.push(neighbor);
37
+ }
38
+ }
39
+
40
+ return null;
41
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Tarjan's strongly connected components algorithm.
3
+ * Operates on a CodeGraph instance.
4
+ *
5
+ * @param {import('../model.js').CodeGraph} graph
6
+ * @returns {string[][]} SCCs with length > 1 (cycles)
7
+ */
8
+ export function tarjan(graph) {
9
+ let index = 0;
10
+ const stack = [];
11
+ const onStack = new Set();
12
+ const indices = new Map();
13
+ const lowlinks = new Map();
14
+ const sccs = [];
15
+
16
+ function strongconnect(v) {
17
+ indices.set(v, index);
18
+ lowlinks.set(v, index);
19
+ index++;
20
+ stack.push(v);
21
+ onStack.add(v);
22
+
23
+ for (const w of graph.successors(v)) {
24
+ if (!indices.has(w)) {
25
+ strongconnect(w);
26
+ lowlinks.set(v, Math.min(lowlinks.get(v), lowlinks.get(w)));
27
+ } else if (onStack.has(w)) {
28
+ lowlinks.set(v, Math.min(lowlinks.get(v), indices.get(w)));
29
+ }
30
+ }
31
+
32
+ if (lowlinks.get(v) === indices.get(v)) {
33
+ const scc = [];
34
+ let w;
35
+ do {
36
+ w = stack.pop();
37
+ onStack.delete(w);
38
+ scc.push(w);
39
+ } while (w !== v);
40
+ if (scc.length > 1) sccs.push(scc);
41
+ }
42
+ }
43
+
44
+ for (const id of graph.nodeIds()) {
45
+ if (!indices.has(id)) strongconnect(id);
46
+ }
47
+
48
+ return sccs;
49
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Build a CodeGraph from the SQLite database.
3
+ * Replaces inline graph construction in cycles.js, communities.js, viewer.js, export.js.
4
+ */
5
+
6
+ import { getCallableNodes, getCallEdges, getFileNodesAll, getImportEdges } from '../../db.js';
7
+ import { isTestFile } from '../../infrastructure/test-filter.js';
8
+ import { CodeGraph } from '../model.js';
9
+
10
+ /**
11
+ * @param {object} db - Open better-sqlite3 database (readonly)
12
+ * @param {object} [opts]
13
+ * @param {boolean} [opts.fileLevel=true] - File-level (imports) or function-level (calls)
14
+ * @param {boolean} [opts.noTests=false] - Exclude test files
15
+ * @param {number} [opts.minConfidence] - Minimum edge confidence (function-level only)
16
+ * @returns {CodeGraph}
17
+ */
18
+ export function buildDependencyGraph(db, opts = {}) {
19
+ const fileLevel = opts.fileLevel !== false;
20
+ const noTests = opts.noTests || false;
21
+
22
+ if (fileLevel) {
23
+ return buildFileLevelGraph(db, noTests);
24
+ }
25
+ return buildFunctionLevelGraph(db, noTests, opts.minConfidence);
26
+ }
27
+
28
+ function buildFileLevelGraph(db, noTests) {
29
+ const graph = new CodeGraph();
30
+
31
+ let nodes = getFileNodesAll(db);
32
+ if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
33
+
34
+ const nodeIds = new Set();
35
+ for (const n of nodes) {
36
+ graph.addNode(String(n.id), { label: n.file, file: n.file, dbId: n.id });
37
+ nodeIds.add(n.id);
38
+ }
39
+
40
+ const edges = getImportEdges(db);
41
+ for (const e of edges) {
42
+ if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
43
+ const src = String(e.source_id);
44
+ const tgt = String(e.target_id);
45
+ if (src === tgt) continue;
46
+ if (!graph.hasEdge(src, tgt)) {
47
+ graph.addEdge(src, tgt, { kind: 'imports' });
48
+ }
49
+ }
50
+
51
+ return graph;
52
+ }
53
+
54
+ function buildFunctionLevelGraph(db, noTests, minConfidence) {
55
+ const graph = new CodeGraph();
56
+
57
+ let nodes = getCallableNodes(db);
58
+ if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
59
+
60
+ const nodeIds = new Set();
61
+ for (const n of nodes) {
62
+ graph.addNode(String(n.id), {
63
+ label: n.name,
64
+ file: n.file,
65
+ kind: n.kind,
66
+ dbId: n.id,
67
+ });
68
+ nodeIds.add(n.id);
69
+ }
70
+
71
+ let edges;
72
+ if (minConfidence != null) {
73
+ edges = db
74
+ .prepare("SELECT source_id, target_id FROM edges WHERE kind = 'calls' AND confidence >= ?")
75
+ .all(minConfidence);
76
+ } else {
77
+ edges = getCallEdges(db);
78
+ }
79
+
80
+ for (const e of edges) {
81
+ if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
82
+ const src = String(e.source_id);
83
+ const tgt = String(e.target_id);
84
+ if (src === tgt) continue;
85
+ if (!graph.hasEdge(src, tgt)) {
86
+ graph.addEdge(src, tgt, { kind: 'calls' });
87
+ }
88
+ }
89
+
90
+ return graph;
91
+ }
@@ -0,0 +1,3 @@
1
+ export { buildDependencyGraph } from './dependency.js';
2
+ export { buildStructureGraph } from './structure.js';
3
+ export { buildTemporalGraph } from './temporal.js';
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Build a containment graph (directory → file) from the SQLite database.
3
+ */
4
+
5
+ import { CodeGraph } from '../model.js';
6
+
7
+ /**
8
+ * @param {object} db - Open better-sqlite3 database (readonly)
9
+ * @returns {CodeGraph} Directed graph with directory→file containment edges
10
+ */
11
+ export function buildStructureGraph(db) {
12
+ const graph = new CodeGraph();
13
+
14
+ const dirs = db.prepare("SELECT id, name FROM nodes WHERE kind = 'directory'").all();
15
+
16
+ for (const d of dirs) {
17
+ graph.addNode(String(d.id), { label: d.name, kind: 'directory' });
18
+ }
19
+
20
+ const files = db.prepare("SELECT id, name, file FROM nodes WHERE kind = 'file'").all();
21
+
22
+ for (const f of files) {
23
+ graph.addNode(String(f.id), { label: f.name, kind: 'file', file: f.file });
24
+ }
25
+
26
+ const containsEdges = db
27
+ .prepare(`
28
+ SELECT e.source_id, e.target_id
29
+ FROM edges e
30
+ JOIN nodes n ON e.source_id = n.id
31
+ WHERE e.kind = 'contains' AND n.kind = 'directory'
32
+ `)
33
+ .all();
34
+
35
+ for (const e of containsEdges) {
36
+ graph.addEdge(String(e.source_id), String(e.target_id), { kind: 'contains' });
37
+ }
38
+
39
+ return graph;
40
+ }