@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,19 +1,19 @@
1
1
  import { checkData } from '../check.js';
2
+ import { AnalysisError } from '../errors.js';
2
3
  import { outputResult } from '../infrastructure/result-formatter.js';
3
4
 
4
5
  /**
5
- * CLI formatter — prints check results and exits with code 1 on failure.
6
+ * CLI formatter — prints check results and sets exitCode 1 on failure.
6
7
  */
7
8
  export function check(customDbPath, opts = {}) {
8
9
  const data = checkData(customDbPath, opts);
9
10
 
10
11
  if (data.error) {
11
- console.error(data.error);
12
- process.exit(1);
12
+ throw new AnalysisError(data.error);
13
13
  }
14
14
 
15
15
  if (outputResult(data, null, opts)) {
16
- if (!data.passed) process.exit(1);
16
+ if (!data.passed) process.exitCode = 1;
17
17
  return;
18
18
  }
19
19
 
@@ -77,6 +77,6 @@ export function check(customDbPath, opts = {}) {
77
77
  console.log(`\n ${s.total} predicates | ${s.passed} passed | ${s.failed} failed\n`);
78
78
 
79
79
  if (!data.passed) {
80
- process.exit(1);
80
+ process.exitCode = 1;
81
81
  }
82
82
  }
@@ -2,13 +2,13 @@ import { outputResult } from '../infrastructure/result-formatter.js';
2
2
  import { manifestoData } from '../manifesto.js';
3
3
 
4
4
  /**
5
- * CLI formatter — prints manifesto results and exits with code 1 on failure.
5
+ * CLI formatter — prints manifesto results and sets exitCode 1 on failure.
6
6
  */
7
7
  export function manifesto(customDbPath, opts = {}) {
8
8
  const data = manifestoData(customDbPath, opts);
9
9
 
10
10
  if (outputResult(data, 'violations', opts)) {
11
- if (!data.passed) process.exit(1);
11
+ if (!data.passed) process.exitCode = 1;
12
12
  return;
13
13
  }
14
14
 
@@ -72,6 +72,6 @@ export function manifesto(customDbPath, opts = {}) {
72
72
  console.log();
73
73
 
74
74
  if (!data.passed) {
75
- process.exit(1);
75
+ process.exitCode = 1;
76
76
  }
77
77
  }
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { hotspotsData, moduleBoundariesData, structureData } from '../structure.js';
3
3
 
4
- export { structureData, hotspotsData, moduleBoundariesData };
4
+ export { hotspotsData, moduleBoundariesData, structureData };
5
5
 
6
6
  export function formatStructure(data) {
7
7
  if (data.count === 0) return 'No directory structure found. Run "codegraph build" first.';
@@ -1,79 +1,9 @@
1
1
  import path from 'node:path';
2
- import Graph from 'graphology';
3
- import louvain from 'graphology-communities-louvain';
4
- import {
5
- getCallableNodes,
6
- getCallEdges,
7
- getFileNodesAll,
8
- getImportEdges,
9
- openReadonlyOrFail,
10
- } from './db.js';
11
- import { isTestFile } from './infrastructure/test-filter.js';
2
+ import { openReadonlyOrFail } from './db.js';
3
+ import { louvainCommunities } from './graph/algorithms/louvain.js';
4
+ import { buildDependencyGraph } from './graph/builders/dependency.js';
12
5
  import { paginateResult } from './paginate.js';
13
6
 
14
- // ─── Graph Construction ───────────────────────────────────────────────
15
-
16
- /**
17
- * Build a graphology graph from the codegraph SQLite database.
18
- *
19
- * @param {object} db - open better-sqlite3 database (readonly)
20
- * @param {object} opts
21
- * @param {boolean} [opts.functions] - Function-level instead of file-level
22
- * @param {boolean} [opts.noTests] - Exclude test files
23
- * @returns {Graph}
24
- */
25
- function buildGraphologyGraph(db, opts = {}) {
26
- const graph = new Graph({ type: 'undirected' });
27
-
28
- if (opts.functions) {
29
- // Function-level: nodes = function/method/class symbols, edges = calls
30
- let nodes = getCallableNodes(db);
31
- if (opts.noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
32
-
33
- const nodeIds = new Set();
34
- for (const n of nodes) {
35
- const key = String(n.id);
36
- graph.addNode(key, { label: n.name, file: n.file, kind: n.kind });
37
- nodeIds.add(n.id);
38
- }
39
-
40
- const edges = getCallEdges(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);
48
- }
49
- }
50
- } else {
51
- // File-level: nodes = files, edges = imports + imports-type (deduplicated, cross-file)
52
- let nodes = getFileNodesAll(db);
53
- if (opts.noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
54
-
55
- const nodeIds = new Set();
56
- for (const n of nodes) {
57
- const key = String(n.id);
58
- graph.addNode(key, { label: n.file, file: n.file });
59
- nodeIds.add(n.id);
60
- }
61
-
62
- const edges = getImportEdges(db);
63
- for (const e of edges) {
64
- if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
65
- const src = String(e.source_id);
66
- const tgt = String(e.target_id);
67
- if (src === tgt) continue;
68
- if (!graph.hasEdge(src, tgt)) {
69
- graph.addEdge(src, tgt);
70
- }
71
- }
72
- }
73
-
74
- return graph;
75
- }
76
-
77
7
  // ─── Directory Helpers ────────────────────────────────────────────────
78
8
 
79
9
  function getDirectory(filePath) {
@@ -97,11 +27,10 @@ function getDirectory(filePath) {
97
27
  */
98
28
  export function communitiesData(customDbPath, opts = {}) {
99
29
  const db = openReadonlyOrFail(customDbPath);
100
- const resolution = opts.resolution ?? 1.0;
101
30
  let graph;
102
31
  try {
103
- graph = buildGraphologyGraph(db, {
104
- functions: opts.functions,
32
+ graph = buildDependencyGraph(db, {
33
+ fileLevel: !opts.functions,
105
34
  noTests: opts.noTests,
106
35
  });
107
36
  } finally {
@@ -109,27 +38,27 @@ export function communitiesData(customDbPath, opts = {}) {
109
38
  }
110
39
 
111
40
  // Handle empty or trivial graphs
112
- if (graph.order === 0 || graph.size === 0) {
41
+ if (graph.nodeCount === 0 || graph.edgeCount === 0) {
113
42
  return {
114
43
  communities: [],
115
44
  modularity: 0,
116
45
  drift: { splitCandidates: [], mergeCandidates: [] },
117
- summary: { communityCount: 0, modularity: 0, nodeCount: graph.order, driftScore: 0 },
46
+ summary: { communityCount: 0, modularity: 0, nodeCount: graph.nodeCount, driftScore: 0 },
118
47
  };
119
48
  }
120
49
 
121
50
  // Run Louvain
122
- const details = louvain.detailed(graph, { resolution });
123
- const assignments = details.communities; // node community id
124
- const modularity = details.modularity;
51
+ const resolution = opts.resolution ?? 1.0;
52
+ const { assignments, modularity } = louvainCommunities(graph, { resolution });
125
53
 
126
54
  // Group nodes by community
127
55
  const communityMap = new Map(); // community id → node keys[]
128
- graph.forEachNode((key) => {
129
- const cid = assignments[key];
56
+ for (const [key] of graph.nodes()) {
57
+ const cid = assignments.get(key);
58
+ if (cid == null) continue;
130
59
  if (!communityMap.has(cid)) communityMap.set(cid, []);
131
60
  communityMap.get(cid).push(key);
132
- });
61
+ }
133
62
 
134
63
  // Build community objects
135
64
  const communities = [];
@@ -139,7 +68,7 @@ export function communitiesData(customDbPath, opts = {}) {
139
68
  const dirCounts = {};
140
69
  const memberData = [];
141
70
  for (const key of members) {
142
- const attrs = graph.getNodeAttributes(key);
71
+ const attrs = graph.getNodeAttrs(key);
143
72
  const dir = getDirectory(attrs.file);
144
73
  dirCounts[dir] = (dirCounts[dir] || 0) + 1;
145
74
  memberData.push({
@@ -196,7 +125,6 @@ export function communitiesData(customDbPath, opts = {}) {
196
125
  mergeCandidates.sort((a, b) => b.directoryCount - a.directoryCount);
197
126
 
198
127
  // Drift score: 0-100 based on how much directory structure diverges from communities
199
- // Higher = more drift (directories don't match communities)
200
128
  const totalDirs = dirToCommunities.size;
201
129
  const splitDirs = splitCandidates.length;
202
130
  const splitRatio = totalDirs > 0 ? splitDirs / totalDirs : 0;
@@ -214,7 +142,7 @@ export function communitiesData(customDbPath, opts = {}) {
214
142
  summary: {
215
143
  communityCount: communities.length,
216
144
  modularity: +modularity.toFixed(4),
217
- nodeCount: graph.order,
145
+ nodeCount: graph.nodeCount,
218
146
  driftScore,
219
147
  },
220
148
  };
package/src/cycles.js CHANGED
@@ -1,4 +1,6 @@
1
- import { isTestFile } from './infrastructure/test-filter.js';
1
+ import { tarjan } from './graph/algorithms/tarjan.js';
2
+ import { buildDependencyGraph } from './graph/builders/dependency.js';
3
+ import { CodeGraph } from './graph/model.js';
2
4
  import { loadNative } from './native.js';
3
5
 
4
6
  /**
@@ -12,107 +14,50 @@ export function findCycles(db, opts = {}) {
12
14
  const fileLevel = opts.fileLevel !== false;
13
15
  const noTests = opts.noTests || false;
14
16
 
15
- // Build adjacency list from SQLite (stays in JS only the algorithm can move to Rust)
16
- let edges;
17
- if (fileLevel) {
18
- edges = db
19
- .prepare(`
20
- SELECT DISTINCT n1.file AS source, n2.file AS target
21
- FROM edges e
22
- JOIN nodes n1 ON e.source_id = n1.id
23
- JOIN nodes n2 ON e.target_id = n2.id
24
- WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type')
25
- `)
26
- .all();
27
- if (noTests) {
28
- edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
29
- }
30
- } else {
31
- edges = db
32
- .prepare(`
33
- SELECT DISTINCT
34
- (n1.name || '|' || n1.file) AS source,
35
- (n2.name || '|' || n2.file) AS target
36
- FROM edges e
37
- JOIN nodes n1 ON e.source_id = n1.id
38
- JOIN nodes n2 ON e.target_id = n2.id
39
- WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
40
- AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
41
- AND e.kind = 'calls'
42
- AND n1.id != n2.id
43
- `)
44
- .all();
45
- if (noTests) {
46
- edges = edges.filter((e) => {
47
- const sourceFile = e.source.split('|').pop();
48
- const targetFile = e.target.split('|').pop();
49
- return !isTestFile(sourceFile) && !isTestFile(targetFile);
50
- });
17
+ const graph = buildDependencyGraph(db, { fileLevel, noTests });
18
+
19
+ // Build a label map: DB string ID → human-readable key
20
+ // File-level: file path; Function-level: name|file composite (for native Rust compat)
21
+ const idToLabel = new Map();
22
+ for (const [id, attrs] of graph.nodes()) {
23
+ if (fileLevel) {
24
+ idToLabel.set(id, attrs.file);
25
+ } else {
26
+ idToLabel.set(id, `${attrs.label}|${attrs.file}`);
51
27
  }
52
28
  }
53
29
 
30
+ // Build edge array with human-readable keys (for native engine)
31
+ const edges = graph.toEdgeArray().map((e) => ({
32
+ source: idToLabel.get(e.source),
33
+ target: idToLabel.get(e.target),
34
+ }));
35
+
54
36
  // Try native Rust implementation
55
37
  const native = loadNative();
56
38
  if (native) {
57
39
  return native.detectCycles(edges);
58
40
  }
59
41
 
60
- // Fallback: JS Tarjan
61
- return findCyclesJS(edges);
42
+ // Fallback: JS Tarjan via graph subsystem
43
+ // Re-key graph with human-readable labels for consistent output
44
+ const labelGraph = new CodeGraph();
45
+ for (const { source, target } of edges) {
46
+ labelGraph.addEdge(source, target);
47
+ }
48
+ return tarjan(labelGraph);
62
49
  }
63
50
 
64
51
  /**
65
52
  * Pure-JS Tarjan's SCC implementation.
53
+ * Kept for backward compatibility — accepts raw {source, target}[] edges.
66
54
  */
67
55
  export function findCyclesJS(edges) {
68
- const graph = new Map();
56
+ const graph = new CodeGraph();
69
57
  for (const { source, target } of edges) {
70
- if (!graph.has(source)) graph.set(source, []);
71
- graph.get(source).push(target);
72
- if (!graph.has(target)) graph.set(target, []);
58
+ graph.addEdge(source, target);
73
59
  }
74
-
75
- // Tarjan's strongly connected components algorithm
76
- let index = 0;
77
- const stack = [];
78
- const onStack = new Set();
79
- const indices = new Map();
80
- const lowlinks = new Map();
81
- const sccs = [];
82
-
83
- function strongconnect(v) {
84
- indices.set(v, index);
85
- lowlinks.set(v, index);
86
- index++;
87
- stack.push(v);
88
- onStack.add(v);
89
-
90
- for (const w of graph.get(v) || []) {
91
- if (!indices.has(w)) {
92
- strongconnect(w);
93
- lowlinks.set(v, Math.min(lowlinks.get(v), lowlinks.get(w)));
94
- } else if (onStack.has(w)) {
95
- lowlinks.set(v, Math.min(lowlinks.get(v), indices.get(w)));
96
- }
97
- }
98
-
99
- if (lowlinks.get(v) === indices.get(v)) {
100
- const scc = [];
101
- let w;
102
- do {
103
- w = stack.pop();
104
- onStack.delete(w);
105
- scc.push(w);
106
- } while (w !== v);
107
- if (scc.length > 1) sccs.push(scc);
108
- }
109
- }
110
-
111
- for (const node of graph.keys()) {
112
- if (!indices.has(node)) strongconnect(node);
113
- }
114
-
115
- return sccs;
60
+ return tarjan(graph);
116
61
  }
117
62
 
118
63
  /**
package/src/dataflow.js CHANGED
@@ -26,8 +26,7 @@ import { paginateResult } from './paginate.js';
26
26
  import { ALL_SYMBOL_KINDS, normalizeSymbol } from './queries.js';
27
27
 
28
28
  // Re-export for backward compatibility
29
- export { DATAFLOW_RULES };
30
- export { _makeDataflowRules as makeDataflowRules };
29
+ export { _makeDataflowRules as makeDataflowRules, DATAFLOW_RULES };
31
30
 
32
31
  export const DATAFLOW_EXTENSIONS = buildExtensionSet(DATAFLOW_RULES);
33
32
 
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import Database from 'better-sqlite3';
4
+ import { DbError } from '../errors.js';
4
5
  import { warn } from '../logger.js';
5
6
 
6
7
  function isProcessAlive(pid) {
@@ -78,11 +79,10 @@ export function findDbPath(customPath) {
78
79
  export function openReadonlyOrFail(customPath) {
79
80
  const dbPath = findDbPath(customPath);
80
81
  if (!fs.existsSync(dbPath)) {
81
- console.error(
82
- `No codegraph database found at ${dbPath}.\n` +
83
- `Run "codegraph build" first to analyze your codebase.`,
82
+ throw new DbError(
83
+ `No codegraph database found at ${dbPath}.\nRun "codegraph build" first to analyze your codebase.`,
84
+ { file: dbPath },
84
85
  );
85
- process.exit(1);
86
86
  }
87
87
  return new Database(dbPath, { readonly: true });
88
88
  }
@@ -229,6 +229,17 @@ export const MIGRATIONS = [
229
229
  CREATE INDEX IF NOT EXISTS idx_nodes_exported ON nodes(exported);
230
230
  `,
231
231
  },
232
+ {
233
+ version: 15,
234
+ up: `
235
+ ALTER TABLE nodes ADD COLUMN qualified_name TEXT;
236
+ ALTER TABLE nodes ADD COLUMN scope TEXT;
237
+ ALTER TABLE nodes ADD COLUMN visibility TEXT;
238
+ UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL;
239
+ CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name);
240
+ CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope);
241
+ `,
242
+ },
232
243
  ];
233
244
 
234
245
  export function getBuildMeta(db, key) {
@@ -309,4 +320,34 @@ export function initSchema(db) {
309
320
  } catch {
310
321
  /* already exists */
311
322
  }
323
+ try {
324
+ db.exec('ALTER TABLE nodes ADD COLUMN qualified_name TEXT');
325
+ } catch {
326
+ /* already exists */
327
+ }
328
+ try {
329
+ db.exec('ALTER TABLE nodes ADD COLUMN scope TEXT');
330
+ } catch {
331
+ /* already exists */
332
+ }
333
+ try {
334
+ db.exec('ALTER TABLE nodes ADD COLUMN visibility TEXT');
335
+ } catch {
336
+ /* already exists */
337
+ }
338
+ try {
339
+ db.exec('UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL');
340
+ } catch {
341
+ /* nodes table may not exist yet */
342
+ }
343
+ try {
344
+ db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name)');
345
+ } catch {
346
+ /* already exists */
347
+ }
348
+ try {
349
+ db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope)');
350
+ } catch {
351
+ /* already exists */
352
+ }
312
353
  }
@@ -1,3 +1,4 @@
1
+ import { DbError } from '../errors.js';
1
2
  import { EVERY_EDGE_KIND } from '../kinds.js';
2
3
 
3
4
  // ─── Validation Helpers ─────────────────────────────────────────────
@@ -12,13 +13,13 @@ const SAFE_SELECT_TOKEN_RE =
12
13
 
13
14
  function validateAlias(alias) {
14
15
  if (!SAFE_ALIAS_RE.test(alias)) {
15
- throw new Error(`Invalid SQL alias: ${alias}`);
16
+ throw new DbError(`Invalid SQL alias: ${alias}`);
16
17
  }
17
18
  }
18
19
 
19
20
  function validateColumn(column) {
20
21
  if (!SAFE_COLUMN_RE.test(column)) {
21
- throw new Error(`Invalid SQL column: ${column}`);
22
+ throw new DbError(`Invalid SQL column: ${column}`);
22
23
  }
23
24
  }
24
25
 
@@ -26,7 +27,7 @@ function validateOrderBy(clause) {
26
27
  const terms = clause.split(',').map((t) => t.trim());
27
28
  for (const term of terms) {
28
29
  if (!SAFE_ORDER_TERM_RE.test(term)) {
29
- throw new Error(`Invalid ORDER BY term: ${term}`);
30
+ throw new DbError(`Invalid ORDER BY term: ${term}`);
30
31
  }
31
32
  }
32
33
  }
@@ -51,14 +52,14 @@ function validateSelectCols(cols) {
51
52
  const tokens = splitTopLevelCommas(cols);
52
53
  for (const token of tokens) {
53
54
  if (!SAFE_SELECT_TOKEN_RE.test(token)) {
54
- throw new Error(`Invalid SELECT expression: ${token}`);
55
+ throw new DbError(`Invalid SELECT expression: ${token}`);
55
56
  }
56
57
  }
57
58
  }
58
59
 
59
60
  function validateEdgeKind(edgeKind) {
60
61
  if (!EVERY_EDGE_KIND.includes(edgeKind)) {
61
- throw new Error(
62
+ throw new DbError(
62
63
  `Invalid edge kind: ${edgeKind} (expected one of ${EVERY_EDGE_KIND.join(', ')})`,
63
64
  );
64
65
  }