@optave/codegraph 3.1.4 → 3.2.0

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 (210) hide show
  1. package/README.md +29 -72
  2. package/package.json +10 -8
  3. package/src/ast-analysis/engine.js +260 -246
  4. package/src/ast-analysis/shared.js +2 -14
  5. package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
  6. package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
  7. package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
  8. package/src/cli/commands/ast.js +4 -7
  9. package/src/cli/commands/audit.js +11 -11
  10. package/src/cli/commands/batch.js +6 -5
  11. package/src/cli/commands/branch-compare.js +1 -1
  12. package/src/cli/commands/brief.js +12 -0
  13. package/src/cli/commands/build.js +1 -1
  14. package/src/cli/commands/cfg.js +5 -8
  15. package/src/cli/commands/check.js +28 -36
  16. package/src/cli/commands/children.js +9 -7
  17. package/src/cli/commands/co-change.js +5 -3
  18. package/src/cli/commands/communities.js +2 -6
  19. package/src/cli/commands/complexity.js +5 -3
  20. package/src/cli/commands/context.js +9 -8
  21. package/src/cli/commands/cycles.js +12 -8
  22. package/src/cli/commands/dataflow.js +5 -8
  23. package/src/cli/commands/deps.js +9 -8
  24. package/src/cli/commands/diff-impact.js +2 -6
  25. package/src/cli/commands/embed.js +1 -1
  26. package/src/cli/commands/export.js +34 -31
  27. package/src/cli/commands/exports.js +2 -6
  28. package/src/cli/commands/flow.js +5 -8
  29. package/src/cli/commands/fn-impact.js +9 -8
  30. package/src/cli/commands/impact.js +2 -6
  31. package/src/cli/commands/info.js +2 -2
  32. package/src/cli/commands/map.js +1 -1
  33. package/src/cli/commands/mcp.js +1 -1
  34. package/src/cli/commands/models.js +1 -1
  35. package/src/cli/commands/owners.js +5 -3
  36. package/src/cli/commands/path.js +2 -2
  37. package/src/cli/commands/plot.js +40 -31
  38. package/src/cli/commands/query.js +9 -8
  39. package/src/cli/commands/registry.js +2 -2
  40. package/src/cli/commands/roles.js +5 -8
  41. package/src/cli/commands/search.js +9 -3
  42. package/src/cli/commands/sequence.js +5 -8
  43. package/src/cli/commands/snapshot.js +6 -1
  44. package/src/cli/commands/stats.js +1 -1
  45. package/src/cli/commands/structure.js +5 -4
  46. package/src/cli/commands/triage.js +41 -30
  47. package/src/cli/commands/watch.js +1 -1
  48. package/src/cli/commands/where.js +2 -6
  49. package/src/cli/index.js +11 -5
  50. package/src/cli/shared/open-graph.js +13 -0
  51. package/src/cli/shared/options.js +22 -2
  52. package/src/cli.js +1 -1
  53. package/src/db/connection.js +140 -11
  54. package/src/{db.js → db/index.js} +12 -5
  55. package/src/db/migrations.js +42 -65
  56. package/src/db/query-builder.js +72 -9
  57. package/src/db/repository/base.js +1 -1
  58. package/src/db/repository/graph-read.js +3 -3
  59. package/src/db/repository/in-memory-repository.js +30 -28
  60. package/src/db/repository/nodes.js +10 -17
  61. package/src/domain/analysis/brief.js +155 -0
  62. package/src/domain/analysis/context.js +392 -0
  63. package/src/domain/analysis/dependencies.js +395 -0
  64. package/src/{analysis → domain/analysis}/exports.js +11 -6
  65. package/src/domain/analysis/impact.js +581 -0
  66. package/src/domain/analysis/module-map.js +348 -0
  67. package/src/{analysis → domain/analysis}/roles.js +12 -9
  68. package/src/{analysis → domain/analysis}/symbol-lookup.js +19 -11
  69. package/src/{builder → domain/graph/builder}/helpers.js +4 -4
  70. package/src/{builder → domain/graph/builder}/incremental.js +119 -93
  71. package/src/domain/graph/builder/pipeline.js +156 -0
  72. package/src/domain/graph/builder/stages/build-edges.js +376 -0
  73. package/src/{builder → domain/graph/builder}/stages/build-structure.js +4 -4
  74. package/src/{builder → domain/graph/builder}/stages/collect-files.js +2 -2
  75. package/src/{builder → domain/graph/builder}/stages/detect-changes.js +204 -183
  76. package/src/{builder → domain/graph/builder}/stages/finalize.js +4 -4
  77. package/src/domain/graph/builder/stages/insert-nodes.js +203 -0
  78. package/src/{builder → domain/graph/builder}/stages/parse-files.js +2 -2
  79. package/src/{builder → domain/graph/builder}/stages/resolve-imports.js +1 -1
  80. package/src/{builder → domain/graph/builder}/stages/run-analyses.js +2 -2
  81. package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
  82. package/src/{cycles.js → domain/graph/cycles.js} +4 -4
  83. package/src/{journal.js → domain/graph/journal.js} +1 -1
  84. package/src/{resolve.js → domain/graph/resolve.js} +2 -2
  85. package/src/{watcher.js → domain/graph/watcher.js} +7 -7
  86. package/src/{parser.js → domain/parser.js} +24 -15
  87. package/src/{queries.js → domain/queries.js} +17 -16
  88. package/src/{embeddings → domain/search}/generator.js +3 -3
  89. package/src/{embeddings → domain/search}/models.js +2 -2
  90. package/src/{embeddings → domain/search}/search/cli-formatter.js +1 -1
  91. package/src/{embeddings → domain/search}/search/filters.js +9 -5
  92. package/src/{embeddings → domain/search}/search/hybrid.js +1 -1
  93. package/src/{embeddings → domain/search}/search/keyword.js +13 -6
  94. package/src/{embeddings → domain/search}/search/prepare.js +15 -7
  95. package/src/{embeddings → domain/search}/search/semantic.js +1 -1
  96. package/src/{embeddings → domain/search}/strategies/structured.js +1 -1
  97. package/src/extractors/csharp.js +224 -207
  98. package/src/extractors/go.js +176 -172
  99. package/src/extractors/hcl.js +94 -78
  100. package/src/extractors/java.js +213 -207
  101. package/src/extractors/javascript.js +275 -305
  102. package/src/extractors/php.js +234 -221
  103. package/src/extractors/python.js +252 -250
  104. package/src/extractors/ruby.js +192 -185
  105. package/src/extractors/rust.js +182 -167
  106. package/src/{ast.js → features/ast.js} +13 -11
  107. package/src/{audit.js → features/audit.js} +20 -46
  108. package/src/{batch.js → features/batch.js} +5 -5
  109. package/src/{boundaries.js → features/boundaries.js} +100 -85
  110. package/src/{branch-compare.js → features/branch-compare.js} +3 -3
  111. package/src/{cfg.js → features/cfg.js} +141 -150
  112. package/src/{check.js → features/check.js} +13 -30
  113. package/src/{cochange.js → features/cochange.js} +5 -5
  114. package/src/{communities.js → features/communities.js} +72 -57
  115. package/src/{complexity.js → features/complexity.js} +154 -143
  116. package/src/{dataflow.js → features/dataflow.js} +155 -158
  117. package/src/{export.js → features/export.js} +6 -6
  118. package/src/{flow.js → features/flow.js} +4 -4
  119. package/src/{viewer.js → features/graph-enrichment.js} +8 -8
  120. package/src/{manifesto.js → features/manifesto.js} +15 -12
  121. package/src/{owners.js → features/owners.js} +6 -5
  122. package/src/features/sequence.js +300 -0
  123. package/src/features/shared/find-nodes.js +31 -0
  124. package/src/{snapshot.js → features/snapshot.js} +3 -3
  125. package/src/{structure.js → features/structure.js} +139 -108
  126. package/src/features/triage.js +141 -0
  127. package/src/graph/builders/dependency.js +33 -14
  128. package/src/graph/classifiers/risk.js +3 -2
  129. package/src/graph/classifiers/roles.js +6 -3
  130. package/src/index.cjs +16 -0
  131. package/src/index.js +40 -39
  132. package/src/{native.js → infrastructure/native.js} +1 -1
  133. package/src/mcp/middleware.js +1 -1
  134. package/src/mcp/server.js +68 -59
  135. package/src/mcp/tool-registry.js +15 -2
  136. package/src/mcp/tools/ast-query.js +1 -1
  137. package/src/mcp/tools/audit.js +1 -1
  138. package/src/mcp/tools/batch-query.js +1 -1
  139. package/src/mcp/tools/branch-compare.js +3 -1
  140. package/src/mcp/tools/brief.js +8 -0
  141. package/src/mcp/tools/cfg.js +1 -1
  142. package/src/mcp/tools/check.js +3 -3
  143. package/src/mcp/tools/co-changes.js +1 -1
  144. package/src/mcp/tools/code-owners.js +1 -1
  145. package/src/mcp/tools/communities.js +1 -1
  146. package/src/mcp/tools/complexity.js +1 -1
  147. package/src/mcp/tools/dataflow.js +2 -2
  148. package/src/mcp/tools/execution-flow.js +2 -2
  149. package/src/mcp/tools/export-graph.js +2 -2
  150. package/src/mcp/tools/find-cycles.js +2 -2
  151. package/src/mcp/tools/index.js +2 -0
  152. package/src/mcp/tools/list-repos.js +1 -1
  153. package/src/mcp/tools/sequence.js +1 -1
  154. package/src/mcp/tools/structure.js +1 -1
  155. package/src/mcp/tools/triage.js +2 -2
  156. package/src/{commands → presentation}/audit.js +2 -2
  157. package/src/{commands → presentation}/batch.js +1 -1
  158. package/src/{commands → presentation}/branch-compare.js +2 -2
  159. package/src/presentation/brief.js +51 -0
  160. package/src/{commands → presentation}/cfg.js +1 -1
  161. package/src/{commands → presentation}/check.js +2 -2
  162. package/src/{commands → presentation}/communities.js +1 -1
  163. package/src/{commands → presentation}/complexity.js +1 -1
  164. package/src/{commands → presentation}/dataflow.js +1 -1
  165. package/src/{commands → presentation}/flow.js +2 -2
  166. package/src/{commands → presentation}/manifesto.js +1 -1
  167. package/src/{commands → presentation}/owners.js +1 -1
  168. package/src/presentation/queries-cli/exports.js +53 -0
  169. package/src/presentation/queries-cli/impact.js +214 -0
  170. package/src/presentation/queries-cli/index.js +5 -0
  171. package/src/presentation/queries-cli/inspect.js +329 -0
  172. package/src/presentation/queries-cli/overview.js +196 -0
  173. package/src/presentation/queries-cli/path.js +65 -0
  174. package/src/presentation/queries-cli.js +27 -0
  175. package/src/{commands → presentation}/query.js +1 -1
  176. package/src/presentation/result-formatter.js +126 -3
  177. package/src/{commands → presentation}/sequence.js +2 -2
  178. package/src/{commands → presentation}/structure.js +1 -1
  179. package/src/presentation/table.js +0 -8
  180. package/src/{commands → presentation}/triage.js +1 -1
  181. package/src/{constants.js → shared/constants.js} +1 -1
  182. package/src/shared/file-utils.js +2 -2
  183. package/src/shared/generators.js +9 -5
  184. package/src/shared/hierarchy.js +1 -1
  185. package/src/{kinds.js → shared/kinds.js} +1 -1
  186. package/src/analysis/context.js +0 -408
  187. package/src/analysis/dependencies.js +0 -341
  188. package/src/analysis/impact.js +0 -463
  189. package/src/analysis/module-map.js +0 -322
  190. package/src/builder/pipeline.js +0 -130
  191. package/src/builder/stages/build-edges.js +0 -297
  192. package/src/builder/stages/insert-nodes.js +0 -195
  193. package/src/mcp.js +0 -2
  194. package/src/queries-cli.js +0 -866
  195. package/src/sequence.js +0 -289
  196. package/src/triage.js +0 -126
  197. /package/src/{builder → domain/graph/builder}/context.js +0 -0
  198. /package/src/{builder.js → domain/graph/builder.js} +0 -0
  199. /package/src/{embeddings → domain/search}/index.js +0 -0
  200. /package/src/{embeddings → domain/search}/stores/fts5.js +0 -0
  201. /package/src/{embeddings → domain/search}/stores/sqlite-blob.js +0 -0
  202. /package/src/{embeddings → domain/search}/strategies/source.js +0 -0
  203. /package/src/{embeddings → domain/search}/strategies/text-utils.js +0 -0
  204. /package/src/{config.js → infrastructure/config.js} +0 -0
  205. /package/src/{logger.js → infrastructure/logger.js} +0 -0
  206. /package/src/{registry.js → infrastructure/registry.js} +0 -0
  207. /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
  208. /package/src/{commands → presentation}/cochange.js +0 -0
  209. /package/src/{errors.js → shared/errors.js} +0 -0
  210. /package/src/{paginate.js → shared/paginate.js} +0 -0
@@ -0,0 +1,155 @@
1
+ import {
2
+ findDistinctCallers,
3
+ findFileNodes,
4
+ findImportDependents,
5
+ findImportSources,
6
+ findImportTargets,
7
+ findNodesByFile,
8
+ openReadonlyOrFail,
9
+ } from '../../db/index.js';
10
+ import { isTestFile } from '../../infrastructure/test-filter.js';
11
+
12
+ /** Symbol kinds meaningful for a file brief — excludes parameters, properties, constants. */
13
+ const BRIEF_KINDS = new Set([
14
+ 'function',
15
+ 'method',
16
+ 'class',
17
+ 'interface',
18
+ 'type',
19
+ 'struct',
20
+ 'enum',
21
+ 'trait',
22
+ 'record',
23
+ 'module',
24
+ ]);
25
+
26
+ /**
27
+ * Compute file risk tier from symbol roles and max fan-in.
28
+ * @param {{ role: string|null, callerCount: number }[]} symbols
29
+ * @returns {'high'|'medium'|'low'}
30
+ */
31
+ function computeRiskTier(symbols) {
32
+ let maxCallers = 0;
33
+ let hasCoreRole = false;
34
+ for (const s of symbols) {
35
+ if (s.callerCount > maxCallers) maxCallers = s.callerCount;
36
+ if (s.role === 'core') hasCoreRole = true;
37
+ }
38
+ if (maxCallers >= 10 || hasCoreRole) return 'high';
39
+ if (maxCallers >= 3) return 'medium';
40
+ return 'low';
41
+ }
42
+
43
+ /**
44
+ * BFS to count transitive callers for a single node.
45
+ * Lightweight variant — only counts, does not collect details.
46
+ */
47
+ function countTransitiveCallers(db, startId, noTests, maxDepth = 5) {
48
+ const visited = new Set([startId]);
49
+ let frontier = [startId];
50
+
51
+ for (let d = 1; d <= maxDepth; d++) {
52
+ const nextFrontier = [];
53
+ for (const fid of frontier) {
54
+ const callers = findDistinctCallers(db, fid);
55
+ for (const c of callers) {
56
+ if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
57
+ visited.add(c.id);
58
+ nextFrontier.push(c.id);
59
+ }
60
+ }
61
+ }
62
+ frontier = nextFrontier;
63
+ if (frontier.length === 0) break;
64
+ }
65
+
66
+ return visited.size - 1;
67
+ }
68
+
69
+ /**
70
+ * Count transitive file-level import dependents via BFS.
71
+ * Depth-bounded to match countTransitiveCallers and keep hook latency predictable.
72
+ */
73
+ function countTransitiveImporters(db, fileNodeIds, noTests, maxDepth = 5) {
74
+ const visited = new Set(fileNodeIds);
75
+ let frontier = [...fileNodeIds];
76
+
77
+ for (let d = 1; d <= maxDepth; d++) {
78
+ const nextFrontier = [];
79
+ for (const current of frontier) {
80
+ const dependents = findImportDependents(db, current);
81
+ for (const dep of dependents) {
82
+ if (!visited.has(dep.id) && (!noTests || !isTestFile(dep.file))) {
83
+ visited.add(dep.id);
84
+ nextFrontier.push(dep.id);
85
+ }
86
+ }
87
+ }
88
+ frontier = nextFrontier;
89
+ if (frontier.length === 0) break;
90
+ }
91
+
92
+ return visited.size - fileNodeIds.length;
93
+ }
94
+
95
+ /**
96
+ * Produce a token-efficient file brief: symbols with roles and caller counts,
97
+ * importer info with transitive count, and file risk tier.
98
+ *
99
+ * @param {string} file - File path (partial match)
100
+ * @param {string} customDbPath - Path to graph.db
101
+ * @param {{ noTests?: boolean }} opts
102
+ * @returns {{ file: string, results: object[] }}
103
+ */
104
+ export function briefData(file, customDbPath, opts = {}) {
105
+ const db = openReadonlyOrFail(customDbPath);
106
+ try {
107
+ const noTests = opts.noTests || false;
108
+ const fileNodes = findFileNodes(db, `%${file}%`);
109
+ if (fileNodes.length === 0) {
110
+ return { file, results: [] };
111
+ }
112
+
113
+ const results = fileNodes.map((fn) => {
114
+ // Direct importers
115
+ let importedBy = findImportSources(db, fn.id);
116
+ if (noTests) importedBy = importedBy.filter((i) => !isTestFile(i.file));
117
+ const directImporters = [...new Set(importedBy.map((i) => i.file))];
118
+
119
+ // Transitive importer count
120
+ const totalImporterCount = countTransitiveImporters(db, [fn.id], noTests);
121
+
122
+ // Direct imports
123
+ let importsTo = findImportTargets(db, fn.id);
124
+ if (noTests) importsTo = importsTo.filter((i) => !isTestFile(i.file));
125
+
126
+ // Symbol definitions with roles and caller counts
127
+ const defs = findNodesByFile(db, fn.file).filter((d) => BRIEF_KINDS.has(d.kind));
128
+ const symbols = defs.map((d) => {
129
+ const callerCount = countTransitiveCallers(db, d.id, noTests);
130
+ return {
131
+ name: d.name,
132
+ kind: d.kind,
133
+ line: d.line,
134
+ role: d.role || null,
135
+ callerCount,
136
+ };
137
+ });
138
+
139
+ const riskTier = computeRiskTier(symbols);
140
+
141
+ return {
142
+ file: fn.file,
143
+ risk: riskTier,
144
+ imports: importsTo.map((i) => i.file),
145
+ importedBy: directImporters,
146
+ totalImporterCount,
147
+ symbols,
148
+ };
149
+ });
150
+
151
+ return { file, results };
152
+ } finally {
153
+ db.close();
154
+ }
155
+ }
@@ -0,0 +1,392 @@
1
+ import path from 'node:path';
2
+ import {
3
+ findCallees,
4
+ findCallers,
5
+ findCrossFileCallTargets,
6
+ findDbPath,
7
+ findFileNodes,
8
+ findImportSources,
9
+ findImportTargets,
10
+ findIntraFileCallEdges,
11
+ findNodeChildren,
12
+ findNodesByFile,
13
+ getComplexityForNode,
14
+ openReadonlyOrFail,
15
+ } from '../../db/index.js';
16
+ import { debug } from '../../infrastructure/logger.js';
17
+ import { isTestFile } from '../../infrastructure/test-filter.js';
18
+ import {
19
+ createFileLinesReader,
20
+ extractSignature,
21
+ extractSummary,
22
+ isFileLikeTarget,
23
+ readSourceRange,
24
+ } from '../../shared/file-utils.js';
25
+ import { resolveMethodViaHierarchy } from '../../shared/hierarchy.js';
26
+ import { normalizeSymbol } from '../../shared/normalize.js';
27
+ import { paginateResult } from '../../shared/paginate.js';
28
+ import { findMatchingNodes } from './symbol-lookup.js';
29
+
30
+ function buildCallees(db, node, repoRoot, getFileLines, opts) {
31
+ const { noTests, depth } = opts;
32
+ const calleeRows = findCallees(db, node.id);
33
+ const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;
34
+
35
+ const callees = filteredCallees.map((c) => {
36
+ const cLines = getFileLines(c.file);
37
+ const summary = cLines ? extractSummary(cLines, c.line) : null;
38
+ let calleeSource = null;
39
+ if (depth >= 1) {
40
+ calleeSource = readSourceRange(repoRoot, c.file, c.line, c.end_line);
41
+ }
42
+ return {
43
+ name: c.name,
44
+ kind: c.kind,
45
+ file: c.file,
46
+ line: c.line,
47
+ endLine: c.end_line || null,
48
+ summary,
49
+ source: calleeSource,
50
+ };
51
+ });
52
+
53
+ if (depth > 1) {
54
+ const visited = new Set(filteredCallees.map((c) => c.id));
55
+ visited.add(node.id);
56
+ let frontier = filteredCallees.map((c) => c.id);
57
+ const maxDepth = Math.min(depth, 5);
58
+ for (let d = 2; d <= maxDepth; d++) {
59
+ const nextFrontier = [];
60
+ for (const fid of frontier) {
61
+ const deeper = findCallees(db, fid);
62
+ for (const c of deeper) {
63
+ if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
64
+ visited.add(c.id);
65
+ nextFrontier.push(c.id);
66
+ const cLines = getFileLines(c.file);
67
+ callees.push({
68
+ name: c.name,
69
+ kind: c.kind,
70
+ file: c.file,
71
+ line: c.line,
72
+ endLine: c.end_line || null,
73
+ summary: cLines ? extractSummary(cLines, c.line) : null,
74
+ source: readSourceRange(repoRoot, c.file, c.line, c.end_line),
75
+ });
76
+ }
77
+ }
78
+ }
79
+ frontier = nextFrontier;
80
+ if (frontier.length === 0) break;
81
+ }
82
+ }
83
+
84
+ return callees;
85
+ }
86
+
87
+ function buildCallers(db, node, noTests) {
88
+ let callerRows = findCallers(db, node.id);
89
+
90
+ if (node.kind === 'method' && node.name.includes('.')) {
91
+ const methodName = node.name.split('.').pop();
92
+ const relatedMethods = resolveMethodViaHierarchy(db, methodName);
93
+ for (const rm of relatedMethods) {
94
+ if (rm.id === node.id) continue;
95
+ const extraCallers = findCallers(db, rm.id);
96
+ callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
97
+ }
98
+ }
99
+ if (noTests) callerRows = callerRows.filter((c) => !isTestFile(c.file));
100
+
101
+ return callerRows.map((c) => ({
102
+ name: c.name,
103
+ kind: c.kind,
104
+ file: c.file,
105
+ line: c.line,
106
+ viaHierarchy: c.viaHierarchy || undefined,
107
+ }));
108
+ }
109
+
110
+ function buildRelatedTests(db, node, getFileLines, includeTests) {
111
+ const testCallerRows = findCallers(db, node.id);
112
+ const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
113
+
114
+ const testsByFile = new Map();
115
+ for (const tc of testCallers) {
116
+ if (!testsByFile.has(tc.file)) testsByFile.set(tc.file, []);
117
+ testsByFile.get(tc.file).push(tc);
118
+ }
119
+
120
+ const relatedTests = [];
121
+ for (const [file] of testsByFile) {
122
+ const tLines = getFileLines(file);
123
+ const testNames = [];
124
+ if (tLines) {
125
+ for (const tl of tLines) {
126
+ const tm = tl.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/);
127
+ if (tm) testNames.push(tm[1]);
128
+ }
129
+ }
130
+ const testSource = includeTests && tLines ? tLines.join('\n') : undefined;
131
+ relatedTests.push({
132
+ file,
133
+ testCount: testNames.length,
134
+ testNames,
135
+ source: testSource,
136
+ });
137
+ }
138
+
139
+ return relatedTests;
140
+ }
141
+
142
+ function getComplexityMetrics(db, nodeId) {
143
+ try {
144
+ const cRow = getComplexityForNode(db, nodeId);
145
+ if (!cRow) return null;
146
+ return {
147
+ cognitive: cRow.cognitive,
148
+ cyclomatic: cRow.cyclomatic,
149
+ maxNesting: cRow.max_nesting,
150
+ maintainabilityIndex: cRow.maintainability_index || 0,
151
+ halsteadVolume: cRow.halstead_volume || 0,
152
+ };
153
+ } catch (e) {
154
+ debug(`complexity lookup failed for node ${nodeId}: ${e.message}`);
155
+ return null;
156
+ }
157
+ }
158
+
159
+ function getNodeChildrenSafe(db, nodeId) {
160
+ try {
161
+ return findNodeChildren(db, nodeId).map((c) => ({
162
+ name: c.name,
163
+ kind: c.kind,
164
+ line: c.line,
165
+ endLine: c.end_line || null,
166
+ }));
167
+ } catch (e) {
168
+ debug(`findNodeChildren failed for node ${nodeId}: ${e.message}`);
169
+ return [];
170
+ }
171
+ }
172
+
173
+ function explainFileImpl(db, target, getFileLines) {
174
+ const fileNodes = findFileNodes(db, `%${target}%`);
175
+ if (fileNodes.length === 0) return [];
176
+
177
+ return fileNodes.map((fn) => {
178
+ const symbols = findNodesByFile(db, fn.file);
179
+
180
+ // IDs of symbols that have incoming calls from other files (public)
181
+ const publicIds = findCrossFileCallTargets(db, fn.file);
182
+
183
+ const fileLines = getFileLines(fn.file);
184
+ const mapSymbol = (s) => ({
185
+ name: s.name,
186
+ kind: s.kind,
187
+ line: s.line,
188
+ role: s.role || null,
189
+ summary: fileLines ? extractSummary(fileLines, s.line) : null,
190
+ signature: fileLines ? extractSignature(fileLines, s.line) : null,
191
+ });
192
+
193
+ const publicApi = symbols.filter((s) => publicIds.has(s.id)).map(mapSymbol);
194
+ const internal = symbols.filter((s) => !publicIds.has(s.id)).map(mapSymbol);
195
+
196
+ const imports = findImportTargets(db, fn.id).map((r) => ({ file: r.file }));
197
+ const importedBy = findImportSources(db, fn.id).map((r) => ({ file: r.file }));
198
+
199
+ const intraEdges = findIntraFileCallEdges(db, fn.file);
200
+ const dataFlowMap = new Map();
201
+ for (const edge of intraEdges) {
202
+ if (!dataFlowMap.has(edge.caller_name)) dataFlowMap.set(edge.caller_name, []);
203
+ dataFlowMap.get(edge.caller_name).push(edge.callee_name);
204
+ }
205
+ const dataFlow = [...dataFlowMap.entries()].map(([caller, callees]) => ({
206
+ caller,
207
+ callees,
208
+ }));
209
+
210
+ const metric = db
211
+ .prepare(`SELECT nm.line_count FROM node_metrics nm WHERE nm.node_id = ?`)
212
+ .get(fn.id);
213
+ let lineCount = metric?.line_count || null;
214
+ if (!lineCount) {
215
+ const maxLine = db
216
+ .prepare(`SELECT MAX(end_line) as max_end FROM nodes WHERE file = ?`)
217
+ .get(fn.file);
218
+ lineCount = maxLine?.max_end || null;
219
+ }
220
+
221
+ return {
222
+ file: fn.file,
223
+ lineCount,
224
+ symbolCount: symbols.length,
225
+ publicApi,
226
+ internal,
227
+ imports,
228
+ importedBy,
229
+ dataFlow,
230
+ };
231
+ });
232
+ }
233
+
234
+ function explainFunctionImpl(db, target, noTests, getFileLines) {
235
+ let nodes = db
236
+ .prepare(
237
+ `SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function','method','class','interface','type','struct','enum','trait','record','module','constant') ORDER BY file, line`,
238
+ )
239
+ .all(`%${target}%`);
240
+ if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
241
+ if (nodes.length === 0) return [];
242
+
243
+ const hc = new Map();
244
+ return nodes.slice(0, 10).map((node) => {
245
+ const fileLines = getFileLines(node.file);
246
+ const lineCount = node.end_line ? node.end_line - node.line + 1 : null;
247
+ const summary = fileLines ? extractSummary(fileLines, node.line) : null;
248
+ const signature = fileLines ? extractSignature(fileLines, node.line) : null;
249
+
250
+ const callees = findCallees(db, node.id).map((c) => ({
251
+ name: c.name,
252
+ kind: c.kind,
253
+ file: c.file,
254
+ line: c.line,
255
+ }));
256
+
257
+ let callers = findCallers(db, node.id).map((c) => ({
258
+ name: c.name,
259
+ kind: c.kind,
260
+ file: c.file,
261
+ line: c.line,
262
+ }));
263
+ if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
264
+
265
+ const testCallerRows = findCallers(db, node.id);
266
+ const seenFiles = new Set();
267
+ const relatedTests = testCallerRows
268
+ .filter((r) => isTestFile(r.file) && !seenFiles.has(r.file) && seenFiles.add(r.file))
269
+ .map((r) => ({ file: r.file }));
270
+
271
+ return {
272
+ ...normalizeSymbol(node, db, hc),
273
+ lineCount,
274
+ summary,
275
+ signature,
276
+ complexity: getComplexityMetrics(db, node.id),
277
+ callees,
278
+ callers,
279
+ relatedTests,
280
+ };
281
+ });
282
+ }
283
+
284
+ function explainCallees(parentResults, currentDepth, visited, db, noTests, getFileLines) {
285
+ if (currentDepth <= 0) return;
286
+ for (const r of parentResults) {
287
+ const newCallees = [];
288
+ for (const callee of r.callees) {
289
+ const key = `${callee.name}:${callee.file}:${callee.line}`;
290
+ if (visited.has(key)) continue;
291
+ visited.add(key);
292
+ const calleeResults = explainFunctionImpl(db, callee.name, noTests, getFileLines);
293
+ const exact = calleeResults.find((cr) => cr.file === callee.file && cr.line === callee.line);
294
+ if (exact) {
295
+ exact._depth = (r._depth || 0) + 1;
296
+ newCallees.push(exact);
297
+ }
298
+ }
299
+ if (newCallees.length > 0) {
300
+ r.depDetails = newCallees;
301
+ explainCallees(newCallees, currentDepth - 1, visited, db, noTests, getFileLines);
302
+ }
303
+ }
304
+ }
305
+
306
+ // ─── Exported functions ──────────────────────────────────────────────────
307
+
308
+ export function contextData(name, customDbPath, opts = {}) {
309
+ const db = openReadonlyOrFail(customDbPath);
310
+ try {
311
+ const depth = opts.depth || 0;
312
+ const noSource = opts.noSource || false;
313
+ const noTests = opts.noTests || false;
314
+ const includeTests = opts.includeTests || false;
315
+
316
+ const dbPath = findDbPath(customDbPath);
317
+ const repoRoot = path.resolve(path.dirname(dbPath), '..');
318
+
319
+ const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
320
+ if (nodes.length === 0) {
321
+ return { name, results: [] };
322
+ }
323
+
324
+ const getFileLines = createFileLinesReader(repoRoot);
325
+
326
+ const results = nodes.map((node) => {
327
+ const fileLines = getFileLines(node.file);
328
+
329
+ const source = noSource
330
+ ? null
331
+ : readSourceRange(repoRoot, node.file, node.line, node.end_line);
332
+
333
+ const signature = fileLines ? extractSignature(fileLines, node.line) : null;
334
+
335
+ const callees = buildCallees(db, node, repoRoot, getFileLines, { noTests, depth });
336
+ const callers = buildCallers(db, node, noTests);
337
+ const relatedTests = buildRelatedTests(db, node, getFileLines, includeTests);
338
+ const complexityMetrics = getComplexityMetrics(db, node.id);
339
+ const nodeChildren = getNodeChildrenSafe(db, node.id);
340
+
341
+ return {
342
+ name: node.name,
343
+ kind: node.kind,
344
+ file: node.file,
345
+ line: node.line,
346
+ role: node.role || null,
347
+ endLine: node.end_line || null,
348
+ source,
349
+ signature,
350
+ complexity: complexityMetrics,
351
+ children: nodeChildren.length > 0 ? nodeChildren : undefined,
352
+ callees,
353
+ callers,
354
+ relatedTests,
355
+ };
356
+ });
357
+
358
+ const base = { name, results };
359
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
360
+ } finally {
361
+ db.close();
362
+ }
363
+ }
364
+
365
+ export function explainData(target, customDbPath, opts = {}) {
366
+ const db = openReadonlyOrFail(customDbPath);
367
+ try {
368
+ const noTests = opts.noTests || false;
369
+ const depth = opts.depth || 0;
370
+ const kind = isFileLikeTarget(target) ? 'file' : 'function';
371
+
372
+ const dbPath = findDbPath(customDbPath);
373
+ const repoRoot = path.resolve(path.dirname(dbPath), '..');
374
+
375
+ const getFileLines = createFileLinesReader(repoRoot);
376
+
377
+ const results =
378
+ kind === 'file'
379
+ ? explainFileImpl(db, target, getFileLines)
380
+ : explainFunctionImpl(db, target, noTests, getFileLines);
381
+
382
+ if (kind === 'function' && depth > 0 && results.length > 0) {
383
+ const visited = new Set(results.map((r) => `${r.name}:${r.file}:${r.line}`));
384
+ explainCallees(results, depth, visited, db, noTests, getFileLines);
385
+ }
386
+
387
+ const base = { target, kind, results };
388
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
389
+ } finally {
390
+ db.close();
391
+ }
392
+ }