@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
@@ -1,11 +1,128 @@
1
- import { printNdjson } from '../paginate.js';
1
+ import { printNdjson } from '../shared/paginate.js';
2
+ import { formatTable, truncEnd } from './table.js';
2
3
 
3
4
  /**
4
- * Shared JSON / NDJSON output dispatch for CLI wrappers.
5
+ * Flatten a nested object into dot-notation keys.
6
+ * Arrays are JSON-stringified; nested objects are recursed.
7
+ *
8
+ * Note: this assumes input objects do not contain literal dot-notation keys
9
+ * (e.g. `{ "a.b": 1 }`). If they do, flattened keys will silently collide
10
+ * with nested paths (e.g. `{ a: { b: 2 } }` also produces `"a.b"`).
11
+ */
12
+ function flattenObject(obj, prefix = '') {
13
+ const result = {};
14
+ for (const [key, value] of Object.entries(obj)) {
15
+ const fullKey = prefix ? `${prefix}.${key}` : key;
16
+ if (
17
+ value !== null &&
18
+ typeof value === 'object' &&
19
+ !Array.isArray(value) &&
20
+ Object.getPrototypeOf(value) === Object.prototype
21
+ ) {
22
+ Object.assign(result, flattenObject(value, fullKey));
23
+ } else if (Array.isArray(value)) {
24
+ result[fullKey] = JSON.stringify(value);
25
+ } else {
26
+ result[fullKey] = value;
27
+ }
28
+ }
29
+ return result;
30
+ }
31
+
32
+ /**
33
+ * Flatten items array and derive column names.
34
+ * Shared by printCsv and printAutoTable.
35
+ * @returns {{ flatItems: object[], columns: string[] } | null}
36
+ */
37
+ function prepareFlatItems(data, field) {
38
+ const items = field ? data[field] : data;
39
+ if (!Array.isArray(items)) return null;
40
+
41
+ const flatItems = items.map((item) =>
42
+ typeof item === 'object' && item !== null && !Array.isArray(item)
43
+ ? flattenObject(item)
44
+ : { value: item },
45
+ );
46
+ const columns = (() => {
47
+ const keys = new Set();
48
+ for (const item of flatItems) for (const key of Object.keys(item)) keys.add(key);
49
+ return [...keys];
50
+ })();
51
+ if (columns.length === 0) columns.push('value');
52
+
53
+ return { flatItems, columns };
54
+ }
55
+
56
+ /** Escape a value for CSV output (LF line endings). */
57
+ function escapeCsv(val) {
58
+ const str = val == null ? '' : String(val);
59
+ if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
60
+ return `"${str.replace(/"/g, '""')}"`;
61
+ }
62
+ return str;
63
+ }
64
+
65
+ /**
66
+ * Print data as CSV to stdout.
67
+ * @param {object} data - Result object from a *Data() function
68
+ * @param {string} field - Array field name (e.g. 'results')
69
+ */
70
+ function printCsv(data, field) {
71
+ const prepared = prepareFlatItems(data, field);
72
+ if (!prepared) return false;
73
+ const { flatItems, columns } = prepared;
74
+
75
+ console.log(columns.map(escapeCsv).join(','));
76
+ for (const row of flatItems) {
77
+ console.log(columns.map((col) => escapeCsv(row[col])).join(','));
78
+ }
79
+ return true;
80
+ }
81
+
82
+ const MAX_COL_WIDTH = 40;
83
+
84
+ /**
85
+ * Print data as an aligned table to stdout.
86
+ * @param {object} data - Result object from a *Data() function
87
+ * @param {string} field - Array field name (e.g. 'results')
88
+ */
89
+ function printAutoTable(data, field) {
90
+ const prepared = prepareFlatItems(data, field);
91
+ if (!prepared) return false;
92
+ const { flatItems, columns } = prepared;
93
+
94
+ const colDefs = columns.map((col) => {
95
+ const maxLen = flatItems.reduce(
96
+ (max, item) => Math.max(max, String(item[col] ?? '').length),
97
+ col.length,
98
+ );
99
+ const isNumeric =
100
+ flatItems.length > 0 &&
101
+ flatItems.every((item) => {
102
+ const v = item[col];
103
+ return v == null || v === '' || (typeof v !== 'boolean' && Number.isFinite(Number(v)));
104
+ });
105
+ return {
106
+ header: col,
107
+ width: Math.min(maxLen, MAX_COL_WIDTH),
108
+ align: isNumeric ? 'right' : 'left',
109
+ };
110
+ });
111
+
112
+ const rows = flatItems.map((item) =>
113
+ columns.map((col) => truncEnd(String(item[col] ?? ''), MAX_COL_WIDTH)),
114
+ );
115
+
116
+ console.log(formatTable({ columns: colDefs, rows }));
117
+ return true;
118
+ }
119
+
120
+ /**
121
+ * Shared JSON / NDJSON / table / CSV output dispatch for CLI wrappers.
5
122
  *
6
123
  * @param {object} data - Result object from a *Data() function
7
124
  * @param {string} field - Array field name for NDJSON streaming (e.g. 'results')
8
- * @param {object} opts - CLI options ({ json?, ndjson? })
125
+ * @param {object} opts - CLI options ({ json?, ndjson?, table?, csv? })
9
126
  * @returns {boolean} true if output was handled (caller should return early)
10
127
  */
11
128
  export function outputResult(data, field, opts) {
@@ -17,5 +134,11 @@ export function outputResult(data, field, opts) {
17
134
  console.log(JSON.stringify(data, null, 2));
18
135
  return true;
19
136
  }
137
+ if (opts.csv) {
138
+ return printCsv(data, field) !== false;
139
+ }
140
+ if (opts.table) {
141
+ return printAutoTable(data, field) !== false;
142
+ }
20
143
  return false;
21
144
  }
@@ -1,6 +1,6 @@
1
+ import { kindIcon } from '../domain/queries.js';
2
+ import { sequenceData, sequenceToMermaid } from '../features/sequence.js';
1
3
  import { outputResult } from '../infrastructure/result-formatter.js';
2
- import { kindIcon } from '../queries.js';
3
- import { sequenceData, sequenceToMermaid } from '../sequence.js';
4
4
 
5
5
  /**
6
6
  * CLI entry point — format sequence data as mermaid, JSON, or ndjson.
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { hotspotsData, moduleBoundariesData, structureData } from '../structure.js';
2
+ import { hotspotsData, moduleBoundariesData, structureData } from '../features/structure.js';
3
3
 
4
4
  export { hotspotsData, moduleBoundariesData, structureData };
5
5
 
@@ -37,11 +37,3 @@ export function truncEnd(str, maxLen) {
37
37
  if (str.length <= maxLen) return str;
38
38
  return `${str.slice(0, maxLen - 1)}\u2026`;
39
39
  }
40
-
41
- /**
42
- * Truncate a string from the start, prepending '\u2026' if truncated.
43
- */
44
- export function truncStart(str, maxLen) {
45
- if (str.length <= maxLen) return str;
46
- return `\u2026${str.slice(-(maxLen - 1))}`;
47
- }
@@ -1,5 +1,5 @@
1
+ import { triageData } from '../features/triage.js';
1
2
  import { outputResult } from '../infrastructure/result-formatter.js';
2
- import { triageData } from '../triage.js';
3
3
 
4
4
  /**
5
5
  * Print triage results to console.
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { SUPPORTED_EXTENSIONS } from './parser.js';
2
+ import { SUPPORTED_EXTENSIONS } from '../domain/parser.js';
3
3
 
4
4
  export const IGNORE_DIRS = new Set([
5
5
  'node_modules',
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { debug } from '../logger.js';
4
- import { LANGUAGE_REGISTRY } from '../parser.js';
3
+ import { LANGUAGE_REGISTRY } from '../domain/parser.js';
4
+ import { debug } from '../infrastructure/logger.js';
5
5
 
6
6
  /**
7
7
  * Resolve a file path relative to repoRoot, rejecting traversal outside the repo.
@@ -1,6 +1,7 @@
1
- import { iterateFunctionNodes, openReadonlyOrFail } from '../db.js';
1
+ import { iterateFunctionNodes, openReadonlyOrFail } from '../db/index.js';
2
+ import { buildFileConditionSQL } from '../db/query-builder.js';
2
3
  import { isTestFile } from '../infrastructure/test-filter.js';
3
- import { ALL_SYMBOL_KINDS } from '../kinds.js';
4
+ import { ALL_SYMBOL_KINDS } from './kinds.js';
4
5
 
5
6
  /**
6
7
  * Generator: stream functions one-by-one using .iterate() for memory efficiency.
@@ -52,9 +53,12 @@ export function* iterRoles(customDbPath, opts = {}) {
52
53
  conditions.push('role = ?');
53
54
  params.push(opts.role);
54
55
  }
55
- if (opts.file) {
56
- conditions.push('file LIKE ?');
57
- params.push(`%${opts.file}%`);
56
+ {
57
+ const fc = buildFileConditionSQL(opts.file, 'file');
58
+ if (fc.sql) {
59
+ conditions.push(fc.sql.replace(/^ AND /, ''));
60
+ params.push(...fc.params);
61
+ }
58
62
  }
59
63
 
60
64
  const stmt = db.prepare(
@@ -1,4 +1,4 @@
1
- import { getClassHierarchy } from '../db.js';
1
+ import { getClassHierarchy } from '../db/index.js';
2
2
 
3
3
  export function resolveMethodViaHierarchy(db, methodName) {
4
4
  const methods = db
@@ -47,4 +47,4 @@ export const STRUCTURAL_EDGE_KINDS = ['parameter_of', 'receiver'];
47
47
  // Full set for MCP enum and validation
48
48
  export const EVERY_EDGE_KIND = [...CORE_EDGE_KINDS, ...STRUCTURAL_EDGE_KINDS];
49
49
 
50
- export const VALID_ROLES = ['entry', 'core', 'utility', 'adapter', 'dead', 'leaf'];
50
+ export const VALID_ROLES = ['entry', 'core', 'utility', 'adapter', 'dead', 'test-only', 'leaf'];
@@ -1,408 +0,0 @@
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.js';
16
- import { isTestFile } from '../infrastructure/test-filter.js';
17
- import { paginateResult } from '../paginate.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 { findMatchingNodes } from './symbol-lookup.js';
28
-
29
- function explainFileImpl(db, target, getFileLines) {
30
- const fileNodes = findFileNodes(db, `%${target}%`);
31
- if (fileNodes.length === 0) return [];
32
-
33
- return fileNodes.map((fn) => {
34
- const symbols = findNodesByFile(db, fn.file);
35
-
36
- // IDs of symbols that have incoming calls from other files (public)
37
- const publicIds = findCrossFileCallTargets(db, fn.file);
38
-
39
- const fileLines = getFileLines(fn.file);
40
- const mapSymbol = (s) => ({
41
- name: s.name,
42
- kind: s.kind,
43
- line: s.line,
44
- role: s.role || null,
45
- summary: fileLines ? extractSummary(fileLines, s.line) : null,
46
- signature: fileLines ? extractSignature(fileLines, s.line) : null,
47
- });
48
-
49
- const publicApi = symbols.filter((s) => publicIds.has(s.id)).map(mapSymbol);
50
- const internal = symbols.filter((s) => !publicIds.has(s.id)).map(mapSymbol);
51
-
52
- // Imports / importedBy
53
- const imports = findImportTargets(db, fn.id).map((r) => ({ file: r.file }));
54
-
55
- const importedBy = findImportSources(db, fn.id).map((r) => ({ file: r.file }));
56
-
57
- // Intra-file data flow
58
- const intraEdges = findIntraFileCallEdges(db, fn.file);
59
-
60
- const dataFlowMap = new Map();
61
- for (const edge of intraEdges) {
62
- if (!dataFlowMap.has(edge.caller_name)) dataFlowMap.set(edge.caller_name, []);
63
- dataFlowMap.get(edge.caller_name).push(edge.callee_name);
64
- }
65
- const dataFlow = [...dataFlowMap.entries()].map(([caller, callees]) => ({
66
- caller,
67
- callees,
68
- }));
69
-
70
- // Line count: prefer node_metrics (actual), fall back to MAX(end_line)
71
- const metric = db
72
- .prepare(`SELECT nm.line_count FROM node_metrics nm WHERE nm.node_id = ?`)
73
- .get(fn.id);
74
- let lineCount = metric?.line_count || null;
75
- if (!lineCount) {
76
- const maxLine = db
77
- .prepare(`SELECT MAX(end_line) as max_end FROM nodes WHERE file = ?`)
78
- .get(fn.file);
79
- lineCount = maxLine?.max_end || null;
80
- }
81
-
82
- return {
83
- file: fn.file,
84
- lineCount,
85
- symbolCount: symbols.length,
86
- publicApi,
87
- internal,
88
- imports,
89
- importedBy,
90
- dataFlow,
91
- };
92
- });
93
- }
94
-
95
- function explainFunctionImpl(db, target, noTests, getFileLines) {
96
- let nodes = db
97
- .prepare(
98
- `SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function','method','class','interface','type','struct','enum','trait','record','module') ORDER BY file, line`,
99
- )
100
- .all(`%${target}%`);
101
- if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
102
- if (nodes.length === 0) return [];
103
-
104
- const hc = new Map();
105
- return nodes.slice(0, 10).map((node) => {
106
- const fileLines = getFileLines(node.file);
107
- const lineCount = node.end_line ? node.end_line - node.line + 1 : null;
108
- const summary = fileLines ? extractSummary(fileLines, node.line) : null;
109
- const signature = fileLines ? extractSignature(fileLines, node.line) : null;
110
-
111
- const callees = findCallees(db, node.id).map((c) => ({
112
- name: c.name,
113
- kind: c.kind,
114
- file: c.file,
115
- line: c.line,
116
- }));
117
-
118
- let callers = findCallers(db, node.id).map((c) => ({
119
- name: c.name,
120
- kind: c.kind,
121
- file: c.file,
122
- line: c.line,
123
- }));
124
- if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
125
-
126
- const testCallerRows = findCallers(db, node.id);
127
- const seenFiles = new Set();
128
- const relatedTests = testCallerRows
129
- .filter((r) => isTestFile(r.file) && !seenFiles.has(r.file) && seenFiles.add(r.file))
130
- .map((r) => ({ file: r.file }));
131
-
132
- // Complexity metrics
133
- let complexityMetrics = null;
134
- try {
135
- const cRow = getComplexityForNode(db, node.id);
136
- if (cRow) {
137
- complexityMetrics = {
138
- cognitive: cRow.cognitive,
139
- cyclomatic: cRow.cyclomatic,
140
- maxNesting: cRow.max_nesting,
141
- maintainabilityIndex: cRow.maintainability_index || 0,
142
- halsteadVolume: cRow.halstead_volume || 0,
143
- };
144
- }
145
- } catch {
146
- /* table may not exist */
147
- }
148
-
149
- return {
150
- ...normalizeSymbol(node, db, hc),
151
- lineCount,
152
- summary,
153
- signature,
154
- complexity: complexityMetrics,
155
- callees,
156
- callers,
157
- relatedTests,
158
- };
159
- });
160
- }
161
-
162
- // ─── Exported functions ──────────────────────────────────────────────────
163
-
164
- export function contextData(name, customDbPath, opts = {}) {
165
- const db = openReadonlyOrFail(customDbPath);
166
- try {
167
- const depth = opts.depth || 0;
168
- const noSource = opts.noSource || false;
169
- const noTests = opts.noTests || false;
170
- const includeTests = opts.includeTests || false;
171
-
172
- const dbPath = findDbPath(customDbPath);
173
- const repoRoot = path.resolve(path.dirname(dbPath), '..');
174
-
175
- const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
176
- if (nodes.length === 0) {
177
- return { name, results: [] };
178
- }
179
-
180
- // No hardcoded slice — pagination handles bounding via limit/offset
181
-
182
- const getFileLines = createFileLinesReader(repoRoot);
183
-
184
- const results = nodes.map((node) => {
185
- const fileLines = getFileLines(node.file);
186
-
187
- // Source
188
- const source = noSource
189
- ? null
190
- : readSourceRange(repoRoot, node.file, node.line, node.end_line);
191
-
192
- // Signature
193
- const signature = fileLines ? extractSignature(fileLines, node.line) : null;
194
-
195
- // Callees
196
- const calleeRows = findCallees(db, node.id);
197
- const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;
198
-
199
- const callees = filteredCallees.map((c) => {
200
- const cLines = getFileLines(c.file);
201
- const summary = cLines ? extractSummary(cLines, c.line) : null;
202
- let calleeSource = null;
203
- if (depth >= 1) {
204
- calleeSource = readSourceRange(repoRoot, c.file, c.line, c.end_line);
205
- }
206
- return {
207
- name: c.name,
208
- kind: c.kind,
209
- file: c.file,
210
- line: c.line,
211
- endLine: c.end_line || null,
212
- summary,
213
- source: calleeSource,
214
- };
215
- });
216
-
217
- // Deep callee expansion via BFS (depth > 1, capped at 5)
218
- if (depth > 1) {
219
- const visited = new Set(filteredCallees.map((c) => c.id));
220
- visited.add(node.id);
221
- let frontier = filteredCallees.map((c) => c.id);
222
- const maxDepth = Math.min(depth, 5);
223
- for (let d = 2; d <= maxDepth; d++) {
224
- const nextFrontier = [];
225
- for (const fid of frontier) {
226
- const deeper = findCallees(db, fid);
227
- for (const c of deeper) {
228
- if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
229
- visited.add(c.id);
230
- nextFrontier.push(c.id);
231
- const cLines = getFileLines(c.file);
232
- callees.push({
233
- name: c.name,
234
- kind: c.kind,
235
- file: c.file,
236
- line: c.line,
237
- endLine: c.end_line || null,
238
- summary: cLines ? extractSummary(cLines, c.line) : null,
239
- source: readSourceRange(repoRoot, c.file, c.line, c.end_line),
240
- });
241
- }
242
- }
243
- }
244
- frontier = nextFrontier;
245
- if (frontier.length === 0) break;
246
- }
247
- }
248
-
249
- // Callers
250
- let callerRows = findCallers(db, node.id);
251
-
252
- // Method hierarchy resolution
253
- if (node.kind === 'method' && node.name.includes('.')) {
254
- const methodName = node.name.split('.').pop();
255
- const relatedMethods = resolveMethodViaHierarchy(db, methodName);
256
- for (const rm of relatedMethods) {
257
- if (rm.id === node.id) continue;
258
- const extraCallers = findCallers(db, rm.id);
259
- callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
260
- }
261
- }
262
- if (noTests) callerRows = callerRows.filter((c) => !isTestFile(c.file));
263
-
264
- const callers = callerRows.map((c) => ({
265
- name: c.name,
266
- kind: c.kind,
267
- file: c.file,
268
- line: c.line,
269
- viaHierarchy: c.viaHierarchy || undefined,
270
- }));
271
-
272
- // Related tests: callers that live in test files
273
- const testCallerRows = findCallers(db, node.id);
274
- const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
275
-
276
- const testsByFile = new Map();
277
- for (const tc of testCallers) {
278
- if (!testsByFile.has(tc.file)) testsByFile.set(tc.file, []);
279
- testsByFile.get(tc.file).push(tc);
280
- }
281
-
282
- const relatedTests = [];
283
- for (const [file] of testsByFile) {
284
- const tLines = getFileLines(file);
285
- const testNames = [];
286
- if (tLines) {
287
- for (const tl of tLines) {
288
- const tm = tl.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/);
289
- if (tm) testNames.push(tm[1]);
290
- }
291
- }
292
- const testSource = includeTests && tLines ? tLines.join('\n') : undefined;
293
- relatedTests.push({
294
- file,
295
- testCount: testNames.length,
296
- testNames,
297
- source: testSource,
298
- });
299
- }
300
-
301
- // Complexity metrics
302
- let complexityMetrics = null;
303
- try {
304
- const cRow = getComplexityForNode(db, node.id);
305
- if (cRow) {
306
- complexityMetrics = {
307
- cognitive: cRow.cognitive,
308
- cyclomatic: cRow.cyclomatic,
309
- maxNesting: cRow.max_nesting,
310
- maintainabilityIndex: cRow.maintainability_index || 0,
311
- halsteadVolume: cRow.halstead_volume || 0,
312
- };
313
- }
314
- } catch {
315
- /* table may not exist */
316
- }
317
-
318
- // Children (parameters, properties, constants)
319
- let nodeChildren = [];
320
- try {
321
- nodeChildren = findNodeChildren(db, node.id).map((c) => ({
322
- name: c.name,
323
- kind: c.kind,
324
- line: c.line,
325
- endLine: c.end_line || null,
326
- }));
327
- } catch {
328
- /* parent_id column may not exist */
329
- }
330
-
331
- return {
332
- name: node.name,
333
- kind: node.kind,
334
- file: node.file,
335
- line: node.line,
336
- role: node.role || null,
337
- endLine: node.end_line || null,
338
- source,
339
- signature,
340
- complexity: complexityMetrics,
341
- children: nodeChildren.length > 0 ? nodeChildren : undefined,
342
- callees,
343
- callers,
344
- relatedTests,
345
- };
346
- });
347
-
348
- const base = { name, results };
349
- return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
350
- } finally {
351
- db.close();
352
- }
353
- }
354
-
355
- export function explainData(target, customDbPath, opts = {}) {
356
- const db = openReadonlyOrFail(customDbPath);
357
- try {
358
- const noTests = opts.noTests || false;
359
- const depth = opts.depth || 0;
360
- const kind = isFileLikeTarget(target) ? 'file' : 'function';
361
-
362
- const dbPath = findDbPath(customDbPath);
363
- const repoRoot = path.resolve(path.dirname(dbPath), '..');
364
-
365
- const getFileLines = createFileLinesReader(repoRoot);
366
-
367
- const results =
368
- kind === 'file'
369
- ? explainFileImpl(db, target, getFileLines)
370
- : explainFunctionImpl(db, target, noTests, getFileLines);
371
-
372
- // Recursive dependency explanation for function targets
373
- if (kind === 'function' && depth > 0 && results.length > 0) {
374
- const visited = new Set(results.map((r) => `${r.name}:${r.file}:${r.line}`));
375
-
376
- function explainCallees(parentResults, currentDepth) {
377
- if (currentDepth <= 0) return;
378
- for (const r of parentResults) {
379
- const newCallees = [];
380
- for (const callee of r.callees) {
381
- const key = `${callee.name}:${callee.file}:${callee.line}`;
382
- if (visited.has(key)) continue;
383
- visited.add(key);
384
- const calleeResults = explainFunctionImpl(db, callee.name, noTests, getFileLines);
385
- const exact = calleeResults.find(
386
- (cr) => cr.file === callee.file && cr.line === callee.line,
387
- );
388
- if (exact) {
389
- exact._depth = (r._depth || 0) + 1;
390
- newCallees.push(exact);
391
- }
392
- }
393
- if (newCallees.length > 0) {
394
- r.depDetails = newCallees;
395
- explainCallees(newCallees, currentDepth - 1);
396
- }
397
- }
398
- }
399
-
400
- explainCallees(results, depth);
401
- }
402
-
403
- const base = { target, kind, results };
404
- return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
405
- } finally {
406
- db.close();
407
- }
408
- }