@optave/codegraph 3.8.1 → 3.9.1

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 (132) hide show
  1. package/README.md +12 -7
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +121 -48
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  6. package/dist/ast-analysis/visitors/ast-store-visitor.js +15 -18
  7. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  8. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  9. package/dist/ast-analysis/visitors/complexity-visitor.js +50 -1
  10. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  11. package/dist/cli/commands/branch-compare.d.ts.map +1 -1
  12. package/dist/cli/commands/branch-compare.js +4 -0
  13. package/dist/cli/commands/branch-compare.js.map +1 -1
  14. package/dist/cli/commands/diff-impact.d.ts.map +1 -1
  15. package/dist/cli/commands/diff-impact.js +2 -1
  16. package/dist/cli/commands/diff-impact.js.map +1 -1
  17. package/dist/cli/commands/info.d.ts.map +1 -1
  18. package/dist/cli/commands/info.js +3 -2
  19. package/dist/cli/commands/info.js.map +1 -1
  20. package/dist/db/connection.d.ts +1 -0
  21. package/dist/db/connection.d.ts.map +1 -1
  22. package/dist/db/connection.js +22 -4
  23. package/dist/db/connection.js.map +1 -1
  24. package/dist/db/repository/base.d.ts +41 -0
  25. package/dist/db/repository/base.d.ts.map +1 -1
  26. package/dist/db/repository/base.js +22 -0
  27. package/dist/db/repository/base.js.map +1 -1
  28. package/dist/db/repository/index.d.ts +1 -0
  29. package/dist/db/repository/index.d.ts.map +1 -1
  30. package/dist/db/repository/index.js.map +1 -1
  31. package/dist/db/repository/native-repository.d.ts +8 -1
  32. package/dist/db/repository/native-repository.d.ts.map +1 -1
  33. package/dist/db/repository/native-repository.js +69 -1
  34. package/dist/db/repository/native-repository.js.map +1 -1
  35. package/dist/db/repository/sqlite-repository.d.ts +1 -0
  36. package/dist/db/repository/sqlite-repository.d.ts.map +1 -1
  37. package/dist/db/repository/sqlite-repository.js +25 -0
  38. package/dist/db/repository/sqlite-repository.js.map +1 -1
  39. package/dist/domain/analysis/dependencies.d.ts +1 -28
  40. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  41. package/dist/domain/analysis/dependencies.js +24 -8
  42. package/dist/domain/analysis/dependencies.js.map +1 -1
  43. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  44. package/dist/domain/graph/builder/incremental.js +18 -0
  45. package/dist/domain/graph/builder/incremental.js.map +1 -1
  46. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  47. package/dist/domain/graph/builder/pipeline.js +298 -206
  48. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  49. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  50. package/dist/domain/graph/builder/stages/build-edges.js +56 -3
  51. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  52. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/stages/resolve-imports.js +19 -23
  54. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  55. package/dist/domain/graph/watcher.d.ts.map +1 -1
  56. package/dist/domain/graph/watcher.js +99 -95
  57. package/dist/domain/graph/watcher.js.map +1 -1
  58. package/dist/domain/parser.d.ts +4 -0
  59. package/dist/domain/parser.d.ts.map +1 -1
  60. package/dist/domain/parser.js +130 -61
  61. package/dist/domain/parser.js.map +1 -1
  62. package/dist/domain/search/models.d.ts.map +1 -1
  63. package/dist/domain/search/models.js +7 -5
  64. package/dist/domain/search/models.js.map +1 -1
  65. package/dist/extractors/go.js +53 -35
  66. package/dist/extractors/go.js.map +1 -1
  67. package/dist/extractors/javascript.js +85 -36
  68. package/dist/extractors/javascript.js.map +1 -1
  69. package/dist/features/complexity.d.ts.map +1 -1
  70. package/dist/features/complexity.js +78 -58
  71. package/dist/features/complexity.js.map +1 -1
  72. package/dist/features/dataflow.d.ts.map +1 -1
  73. package/dist/features/dataflow.js +109 -118
  74. package/dist/features/dataflow.js.map +1 -1
  75. package/dist/features/structure.d.ts.map +1 -1
  76. package/dist/features/structure.js +147 -97
  77. package/dist/features/structure.js.map +1 -1
  78. package/dist/graph/algorithms/louvain.d.ts.map +1 -1
  79. package/dist/graph/algorithms/louvain.js +4 -2
  80. package/dist/graph/algorithms/louvain.js.map +1 -1
  81. package/dist/graph/classifiers/roles.d.ts +2 -0
  82. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  83. package/dist/graph/classifiers/roles.js +13 -5
  84. package/dist/graph/classifiers/roles.js.map +1 -1
  85. package/dist/presentation/communities.d.ts.map +1 -1
  86. package/dist/presentation/communities.js +38 -34
  87. package/dist/presentation/communities.js.map +1 -1
  88. package/dist/presentation/manifesto.d.ts.map +1 -1
  89. package/dist/presentation/manifesto.js +31 -33
  90. package/dist/presentation/manifesto.js.map +1 -1
  91. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  92. package/dist/presentation/queries-cli/inspect.js +47 -46
  93. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  94. package/dist/shared/file-utils.d.ts.map +1 -1
  95. package/dist/shared/file-utils.js +94 -72
  96. package/dist/shared/file-utils.js.map +1 -1
  97. package/dist/types.d.ts +83 -2
  98. package/dist/types.d.ts.map +1 -1
  99. package/grammars/tree-sitter-erlang.wasm +0 -0
  100. package/grammars/tree-sitter-gleam.wasm +0 -0
  101. package/package.json +9 -9
  102. package/src/ast-analysis/engine.ts +150 -55
  103. package/src/ast-analysis/visitors/ast-store-visitor.ts +19 -21
  104. package/src/ast-analysis/visitors/complexity-visitor.ts +55 -1
  105. package/src/cli/commands/branch-compare.ts +4 -0
  106. package/src/cli/commands/diff-impact.ts +2 -1
  107. package/src/cli/commands/info.ts +3 -2
  108. package/src/db/connection.ts +24 -5
  109. package/src/db/repository/base.ts +57 -0
  110. package/src/db/repository/index.ts +1 -0
  111. package/src/db/repository/native-repository.ts +92 -1
  112. package/src/db/repository/sqlite-repository.ts +26 -0
  113. package/src/domain/analysis/dependencies.ts +24 -6
  114. package/src/domain/graph/builder/incremental.ts +21 -0
  115. package/src/domain/graph/builder/pipeline.ts +396 -245
  116. package/src/domain/graph/builder/stages/build-edges.ts +53 -2
  117. package/src/domain/graph/builder/stages/resolve-imports.ts +20 -20
  118. package/src/domain/graph/watcher.ts +118 -98
  119. package/src/domain/parser.ts +131 -63
  120. package/src/domain/search/models.ts +11 -5
  121. package/src/extractors/go.ts +57 -32
  122. package/src/extractors/javascript.ts +88 -35
  123. package/src/features/complexity.ts +94 -58
  124. package/src/features/dataflow.ts +153 -132
  125. package/src/features/structure.ts +167 -95
  126. package/src/graph/algorithms/louvain.ts +5 -2
  127. package/src/graph/classifiers/roles.ts +14 -5
  128. package/src/presentation/communities.ts +44 -39
  129. package/src/presentation/manifesto.ts +35 -38
  130. package/src/presentation/queries-cli/inspect.ts +48 -46
  131. package/src/shared/file-utils.ts +116 -77
  132. package/src/types.ts +87 -1
@@ -182,6 +182,39 @@ interface InterfacesData {
182
182
  results: InterfacesResult[];
183
183
  }
184
184
 
185
+ function renderWhereSymbolResults(results: WhereSymbolResult[]): void {
186
+ for (const r of results) {
187
+ const roleTag = r.role ? ` [${r.role}]` : '';
188
+ const tag = r.exported ? ' (exported)' : '';
189
+ console.log(`\n${kindIcon(r.kind)} ${r.name}${roleTag} ${r.file}:${r.line}${tag}`);
190
+ if (r.uses.length > 0) {
191
+ const useStrs = r.uses.map((u) => `${u.file}:${u.line}`);
192
+ console.log(` Used in: ${useStrs.join(', ')}`);
193
+ } else {
194
+ console.log(' No uses found');
195
+ }
196
+ }
197
+ }
198
+
199
+ function renderWhereFileResults(results: WhereFileResult[]): void {
200
+ for (const r of results) {
201
+ console.log(`\n# ${r.file}`);
202
+ if (r.symbols.length > 0) {
203
+ const symStrs = r.symbols.map((s) => `${s.name}:${s.line}`);
204
+ console.log(` Symbols: ${symStrs.join(', ')}`);
205
+ }
206
+ if (r.imports.length > 0) {
207
+ console.log(` Imports: ${r.imports.join(', ')}`);
208
+ }
209
+ if (r.importedBy.length > 0) {
210
+ console.log(` Imported by: ${r.importedBy.join(', ')}`);
211
+ }
212
+ if (r.exported.length > 0) {
213
+ console.log(` Exported: ${r.exported.join(', ')}`);
214
+ }
215
+ }
216
+ }
217
+
185
218
  export function where(target: string, customDbPath: string, opts: OutputOpts = {}): void {
186
219
  const data = whereData(target, customDbPath, opts as Record<string, unknown>) as WhereData;
187
220
  if (outputResult(data as unknown as Record<string, unknown>, 'results', opts)) return;
@@ -196,34 +229,9 @@ export function where(target: string, customDbPath: string, opts: OutputOpts = {
196
229
  }
197
230
 
198
231
  if (data.mode === 'symbol') {
199
- for (const r of data.results as WhereSymbolResult[]) {
200
- const roleTag = r.role ? ` [${r.role}]` : '';
201
- const tag = r.exported ? ' (exported)' : '';
202
- console.log(`\n${kindIcon(r.kind)} ${r.name}${roleTag} ${r.file}:${r.line}${tag}`);
203
- if (r.uses.length > 0) {
204
- const useStrs = r.uses.map((u) => `${u.file}:${u.line}`);
205
- console.log(` Used in: ${useStrs.join(', ')}`);
206
- } else {
207
- console.log(' No uses found');
208
- }
209
- }
232
+ renderWhereSymbolResults(data.results as WhereSymbolResult[]);
210
233
  } else {
211
- for (const r of data.results as WhereFileResult[]) {
212
- console.log(`\n# ${r.file}`);
213
- if (r.symbols.length > 0) {
214
- const symStrs = r.symbols.map((s) => `${s.name}:${s.line}`);
215
- console.log(` Symbols: ${symStrs.join(', ')}`);
216
- }
217
- if (r.imports.length > 0) {
218
- console.log(` Imports: ${r.imports.join(', ')}`);
219
- }
220
- if (r.importedBy.length > 0) {
221
- console.log(` Imported by: ${r.importedBy.join(', ')}`);
222
- }
223
- if (r.exported.length > 0) {
224
- console.log(` Exported: ${r.exported.join(', ')}`);
225
- }
226
- }
234
+ renderWhereFileResults(data.results as WhereFileResult[]);
227
235
  }
228
236
  console.log();
229
237
  }
@@ -402,6 +410,17 @@ function renderContextResult(r: ContextResult): void {
402
410
  }
403
411
  }
404
412
 
413
+ function renderExplainSymbolList(label: string, symbols: ExplainSymbol[]): void {
414
+ if (symbols.length === 0) return;
415
+ console.log(`\n## ${label}`);
416
+ for (const s of symbols) {
417
+ const sig = s.signature?.params != null ? `(${s.signature.params})` : '';
418
+ const roleTag = s.role ? ` [${s.role}]` : '';
419
+ const summary = s.summary ? ` -- ${s.summary}` : '';
420
+ console.log(` ${kindIcon(s.kind)} ${s.name}${sig}${roleTag} :${s.line}${summary}`);
421
+ }
422
+ }
423
+
405
424
  function renderFileExplain(r: FileExplainResult): void {
406
425
  const publicCount = r.publicApi.length;
407
426
  const internalCount = r.internal.length;
@@ -418,25 +437,8 @@ function renderFileExplain(r: FileExplainResult): void {
418
437
  console.log(` Imported by: ${r.importedBy.map((i) => i.file).join(', ')}`);
419
438
  }
420
439
 
421
- if (r.publicApi.length > 0) {
422
- console.log(`\n## Exported`);
423
- for (const s of r.publicApi) {
424
- const sig = s.signature?.params != null ? `(${s.signature.params})` : '';
425
- const roleTag = s.role ? ` [${s.role}]` : '';
426
- const summary = s.summary ? ` -- ${s.summary}` : '';
427
- console.log(` ${kindIcon(s.kind)} ${s.name}${sig}${roleTag} :${s.line}${summary}`);
428
- }
429
- }
430
-
431
- if (r.internal.length > 0) {
432
- console.log(`\n## Internal`);
433
- for (const s of r.internal) {
434
- const sig = s.signature?.params != null ? `(${s.signature.params})` : '';
435
- const roleTag = s.role ? ` [${s.role}]` : '';
436
- const summary = s.summary ? ` -- ${s.summary}` : '';
437
- console.log(` ${kindIcon(s.kind)} ${s.name}${sig}${roleTag} :${s.line}${summary}`);
438
- }
439
- }
440
+ renderExplainSymbolList('Exported', r.publicApi);
441
+ renderExplainSymbolList('Internal', r.internal);
440
442
 
441
443
  if (r.dataFlow.length > 0) {
442
444
  console.log(`\n## Data Flow`);
@@ -45,56 +45,97 @@ interface ExtractSummaryOpts {
45
45
  summaryMaxChars?: number;
46
46
  }
47
47
 
48
- export function extractSummary(
49
- fileLines: string[] | null,
50
- line: number | undefined,
51
- opts: ExtractSummaryOpts = {},
48
+ /** Truncate text to maxChars, appending "..." if truncated. */
49
+ function truncate(text: string, maxChars: number): string {
50
+ return text.length > maxChars ? `${text.slice(0, maxChars)}...` : text;
51
+ }
52
+
53
+ /** Try to extract a single-line comment (// or #) above the definition. */
54
+ function extractSingleLineComment(
55
+ fileLines: string[],
56
+ idx: number,
57
+ scanLines: number,
58
+ maxChars: number,
52
59
  ): string | null {
53
- if (!fileLines || !line || line <= 1) return null;
54
- const idx = line - 2; // line above the definition (0-indexed)
55
- const jsdocEndScanLines = opts.jsdocEndScanLines ?? 10;
56
- const jsdocOpenScanLines = opts.jsdocOpenScanLines ?? 20;
57
- const summaryMaxChars = opts.summaryMaxChars ?? 100;
58
- // Scan up for JSDoc or comment
59
- let jsdocEnd = -1;
60
- for (let i = idx; i >= Math.max(0, idx - jsdocEndScanLines); i--) {
60
+ for (let i = idx; i >= Math.max(0, idx - scanLines); i--) {
61
61
  const trimmed = fileLines[i]!.trim();
62
- if (trimmed.endsWith('*/')) {
63
- jsdocEnd = i;
64
- break;
65
- }
62
+ if (trimmed.endsWith('*/')) return null; // hit a block comment — defer to JSDoc extractor
66
63
  if (trimmed.startsWith('//') || trimmed.startsWith('#')) {
67
- // Single-line comment immediately above
68
64
  const text = trimmed
69
65
  .replace(/^\/\/\s*/, '')
70
66
  .replace(/^#\s*/, '')
71
67
  .trim();
72
- return text.length > summaryMaxChars ? `${text.slice(0, summaryMaxChars)}...` : text;
68
+ return truncate(text, maxChars);
73
69
  }
74
- if (trimmed !== '' && !trimmed.startsWith('*') && !trimmed.startsWith('/*')) break;
70
+ if (trimmed !== '' && !trimmed.startsWith('*') && !trimmed.startsWith('/*')) return null;
75
71
  }
76
- if (jsdocEnd >= 0) {
77
- // Find opening /**
78
- for (let i = jsdocEnd; i >= Math.max(0, jsdocEnd - jsdocOpenScanLines); i--) {
79
- if (fileLines[i]!.trim().startsWith('/**')) {
80
- // Extract first non-tag, non-empty line
81
- for (let j = i + 1; j <= jsdocEnd; j++) {
82
- const docLine = fileLines[j]!.trim()
83
- .replace(/^\*\s?/, '')
84
- .trim();
85
- if (docLine && !docLine.startsWith('@') && docLine !== '/' && docLine !== '*/') {
86
- return docLine.length > summaryMaxChars
87
- ? `${docLine.slice(0, summaryMaxChars)}...`
88
- : docLine;
89
- }
90
- }
91
- break;
72
+ return null;
73
+ }
74
+
75
+ /** Find the line index where a block comment (*​/) ends, scanning upward from idx. */
76
+ function findJsdocEndLine(fileLines: string[], idx: number, scanLines: number): number {
77
+ for (let i = idx; i >= Math.max(0, idx - scanLines); i--) {
78
+ const trimmed = fileLines[i]!.trim();
79
+ if (trimmed.endsWith('*/')) return i;
80
+ if (
81
+ trimmed !== '' &&
82
+ !trimmed.startsWith('*') &&
83
+ !trimmed.startsWith('/*') &&
84
+ !trimmed.startsWith('//') &&
85
+ !trimmed.startsWith('#')
86
+ ) {
87
+ break;
88
+ }
89
+ }
90
+ return -1;
91
+ }
92
+
93
+ /** Extract the first description line from a JSDoc block ending at jsdocEnd. */
94
+ function extractJsdocDescription(
95
+ fileLines: string[],
96
+ jsdocEnd: number,
97
+ openScanLines: number,
98
+ maxChars: number,
99
+ ): string | null {
100
+ for (let i = jsdocEnd; i >= Math.max(0, jsdocEnd - openScanLines); i--) {
101
+ if (!fileLines[i]!.trim().startsWith('/**')) continue;
102
+ for (let j = i + 1; j <= jsdocEnd; j++) {
103
+ const docLine = fileLines[j]!.trim()
104
+ .replace(/^\*\s?/, '')
105
+ .trim();
106
+ if (docLine && !docLine.startsWith('@') && docLine !== '/' && docLine !== '*/') {
107
+ return truncate(docLine, maxChars);
92
108
  }
93
109
  }
110
+ break;
94
111
  }
95
112
  return null;
96
113
  }
97
114
 
115
+ export function extractSummary(
116
+ fileLines: string[] | null,
117
+ line: number | undefined,
118
+ opts: ExtractSummaryOpts = {},
119
+ ): string | null {
120
+ if (!fileLines || !line || line <= 1) return null;
121
+ const idx = line - 2; // line above the definition (0-indexed)
122
+ const jsdocEndScanLines = opts.jsdocEndScanLines ?? 10;
123
+ const jsdocOpenScanLines = opts.jsdocOpenScanLines ?? 20;
124
+ const summaryMaxChars = opts.summaryMaxChars ?? 100;
125
+
126
+ // Try single-line comment first
127
+ const singleLine = extractSingleLineComment(fileLines, idx, jsdocEndScanLines, summaryMaxChars);
128
+ if (singleLine) return singleLine;
129
+
130
+ // Try JSDoc block comment
131
+ const jsdocEnd = findJsdocEndLine(fileLines, idx, jsdocEndScanLines);
132
+ if (jsdocEnd >= 0) {
133
+ return extractJsdocDescription(fileLines, jsdocEnd, jsdocOpenScanLines, summaryMaxChars);
134
+ }
135
+
136
+ return null;
137
+ }
138
+
98
139
  interface ExtractSignatureOpts {
99
140
  signatureGatherLines?: number;
100
141
  }
@@ -104,6 +145,38 @@ export interface Signature {
104
145
  returnType: string | null;
105
146
  }
106
147
 
148
+ /** Per-language signature patterns. Each entry has a regex and an extractor for return type. */
149
+ const SIGNATURE_PATTERNS: Array<{
150
+ regex: RegExp;
151
+ returnType: (m: RegExpMatchArray) => string | null;
152
+ }> = [
153
+ // JS/TS: function name(params) or async function
154
+ {
155
+ regex: /(?:export\s+)?(?:async\s+)?function\s*\*?\s*\w*\s*\(([^)]*)\)\s*(?::\s*([^\n{]+))?/,
156
+ returnType: (m) => (m[2] ? m[2].trim().replace(/\s*\{$/, '') : null),
157
+ },
158
+ // Arrow: const name = (params) => or (params):ReturnType =>
159
+ {
160
+ regex: /=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*([^=>\n{]+))?\s*=>/,
161
+ returnType: (m) => (m[2] ? m[2].trim() : null),
162
+ },
163
+ // Python: def name(params) -> return:
164
+ {
165
+ regex: /def\s+\w+\s*\(([^)]*)\)\s*(?:->\s*([^:\n]+))?/,
166
+ returnType: (m) => (m[2] ? m[2].trim() : null),
167
+ },
168
+ // Go: func (recv) name(params) (returns)
169
+ {
170
+ regex: /func\s+(?:\([^)]*\)\s+)?\w+\s*\(([^)]*)\)\s*(?:\(([^)]+)\)|(\w[^\n{]*))?/,
171
+ returnType: (m) => (m[2] || m[3] || '').trim() || null,
172
+ },
173
+ // Rust: fn name(params) -> ReturnType
174
+ {
175
+ regex: /fn\s+\w+\s*\(([^)]*)\)\s*(?:->\s*([^\n{]+))?/,
176
+ returnType: (m) => (m[2] ? m[2].trim() : null),
177
+ },
178
+ ];
179
+
107
180
  export function extractSignature(
108
181
  fileLines: string[] | null,
109
182
  line: number | undefined,
@@ -112,52 +185,18 @@ export function extractSignature(
112
185
  if (!fileLines || !line) return null;
113
186
  const idx = line - 1;
114
187
  const signatureGatherLines = opts.signatureGatherLines ?? 5;
115
- // Gather lines to handle multi-line params
116
188
  const chunk = fileLines
117
189
  .slice(idx, Math.min(fileLines.length, idx + signatureGatherLines))
118
190
  .join('\n');
119
191
 
120
- // JS/TS: function name(params) or (params) => or async function
121
- let m = chunk.match(
122
- /(?:export\s+)?(?:async\s+)?function\s*\*?\s*\w*\s*\(([^)]*)\)\s*(?::\s*([^\n{]+))?/,
123
- );
124
- if (m) {
125
- return {
126
- params: m[1]!.trim() || null,
127
- returnType: m[2] ? m[2].trim().replace(/\s*\{$/, '') : null,
128
- };
129
- }
130
- // Arrow: const name = (params) => or (params):ReturnType =>
131
- m = chunk.match(/=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*([^=>\n{]+))?\s*=>/);
132
- if (m) {
133
- return {
134
- params: m[1]!.trim() || null,
135
- returnType: m[2] ? m[2].trim() : null,
136
- };
137
- }
138
- // Python: def name(params) -> return:
139
- m = chunk.match(/def\s+\w+\s*\(([^)]*)\)\s*(?:->\s*([^:\n]+))?/);
140
- if (m) {
141
- return {
142
- params: m[1]!.trim() || null,
143
- returnType: m[2] ? m[2].trim() : null,
144
- };
145
- }
146
- // Go: func (recv) name(params) (returns)
147
- m = chunk.match(/func\s+(?:\([^)]*\)\s+)?\w+\s*\(([^)]*)\)\s*(?:\(([^)]+)\)|(\w[^\n{]*))?/);
148
- if (m) {
149
- return {
150
- params: m[1]!.trim() || null,
151
- returnType: (m[2] || m[3] || '').trim() || null,
152
- };
153
- }
154
- // Rust: fn name(params) -> ReturnType
155
- m = chunk.match(/fn\s+\w+\s*\(([^)]*)\)\s*(?:->\s*([^\n{]+))?/);
156
- if (m) {
157
- return {
158
- params: m[1]!.trim() || null,
159
- returnType: m[2] ? m[2].trim() : null,
160
- };
192
+ for (const pattern of SIGNATURE_PATTERNS) {
193
+ const m = chunk.match(pattern.regex);
194
+ if (m) {
195
+ return {
196
+ params: m[1]!.trim() || null,
197
+ returnType: pattern.returnType(m),
198
+ };
199
+ }
161
200
  }
162
201
  return null;
163
202
  }
package/src/types.ts CHANGED
@@ -302,6 +302,7 @@ export interface Repository {
302
302
  // ── Edge queries ──────────────────────────────────────────────────
303
303
  findCallees(nodeId: number): RelatedNodeRow[];
304
304
  findCallers(nodeId: number): RelatedNodeRow[];
305
+ findCallersBatch(nodeIds: number[]): Map<number, RelatedNodeRow[]>;
305
306
  findDistinctCallers(nodeId: number): RelatedNodeRow[];
306
307
  findAllOutgoingEdges(nodeId: number): AdjacentEdgeRow[];
307
308
  findAllIncomingEdges(nodeId: number): AdjacentEdgeRow[];
@@ -333,6 +334,35 @@ export interface Repository {
333
334
  getFileHash(file: string): string | null;
334
335
  hasImplementsEdges(): boolean;
335
336
  hasCoChangesTable(): boolean;
337
+
338
+ // ── Composite queries ──────────────────────────────────────────────
339
+ fnDeps(
340
+ name: string,
341
+ opts?: { depth?: number; noTests?: boolean; file?: string; kind?: string },
342
+ ): {
343
+ name: string;
344
+ results: Array<{
345
+ name: string;
346
+ kind: string;
347
+ file: string;
348
+ line: number | null;
349
+ endLine: number | null;
350
+ role: string | null;
351
+ fileHash: string | null;
352
+ callees: Array<{ name: string; kind: string; file: string; line: number | null }>;
353
+ callers: Array<{
354
+ name: string;
355
+ kind: string;
356
+ file: string;
357
+ line: number | null;
358
+ viaHierarchy?: string;
359
+ }>;
360
+ transitiveCallers: Record<
361
+ number,
362
+ Array<{ name: string; kind: string; file: string; line: number | null }>
363
+ >;
364
+ }>;
365
+ } | null;
336
366
  }
337
367
 
338
368
  /**
@@ -408,7 +438,7 @@ export interface DefinitionComplexity {
408
438
  cognitive: number;
409
439
  cyclomatic: number;
410
440
  maxNesting: number;
411
- halstead?: HalsteadMetrics;
441
+ halstead?: HalsteadDerivedMetrics | HalsteadMetrics;
412
442
  loc?: LOCMetrics;
413
443
  maintainabilityIndex?: number;
414
444
  }
@@ -665,6 +695,12 @@ export interface AnalysisTiming {
665
695
  complexityMs: number;
666
696
  cfgMs: number;
667
697
  dataflowMs: number;
698
+ /**
699
+ * Diagnostic: total wall-clock time for the unified walk loop (includes
700
+ * setupVisitors overhead). Walk time is already distributed equally into
701
+ * the per-phase timers above, so this overlaps — it is not an additive
702
+ * bucket. Useful for cross-checking that Σ phase timers ≈ this value.
703
+ */
668
704
  _unifiedWalkMs?: number;
669
705
  }
670
706
 
@@ -1877,6 +1913,7 @@ export interface NativeAddon {
1877
1913
  fileNodeIds: unknown[],
1878
1914
  barrelFiles: string[],
1879
1915
  rootDir: string,
1916
+ symbolNodes?: Array<{ name: string; file: string; nodeId: number }>,
1880
1917
  ): unknown[];
1881
1918
  engineVersion(): string;
1882
1919
  analyzeComplexity(
@@ -2048,6 +2085,46 @@ export interface NativeComplexityMetrics {
2048
2085
  halsteadVolume: number | null;
2049
2086
  }
2050
2087
 
2088
+ // ── Native composite query types (fnDeps) ──────────────────────────────
2089
+
2090
+ export interface NativeFnDepsNode {
2091
+ name: string;
2092
+ kind: string;
2093
+ file: string;
2094
+ line: number | null;
2095
+ }
2096
+
2097
+ export interface NativeFnDepsCallerNode {
2098
+ name: string;
2099
+ kind: string;
2100
+ file: string;
2101
+ line: number | null;
2102
+ viaHierarchy: string | null;
2103
+ }
2104
+
2105
+ export interface NativeFnDepsTransitiveGroup {
2106
+ depth: number;
2107
+ callers: NativeFnDepsNode[];
2108
+ }
2109
+
2110
+ export interface NativeFnDepsEntry {
2111
+ name: string;
2112
+ kind: string;
2113
+ file: string;
2114
+ line: number | null;
2115
+ endLine: number | null;
2116
+ role: string | null;
2117
+ fileHash: string | null;
2118
+ callees: NativeFnDepsNode[];
2119
+ callers: NativeFnDepsCallerNode[];
2120
+ transitiveCallers: NativeFnDepsTransitiveGroup[];
2121
+ }
2122
+
2123
+ export interface NativeFnDepsResult {
2124
+ name: string;
2125
+ results: NativeFnDepsEntry[];
2126
+ }
2127
+
2051
2128
  /** Native rusqlite database wrapper instance (Phase 6.13 + 6.14 + 6.15). */
2052
2129
  export interface NativeDatabase {
2053
2130
  // ── Lifecycle (6.13) ────────────────────────────────────────────────
@@ -2132,6 +2209,15 @@ export interface NativeDatabase {
2132
2209
  getComplexityForNode(nodeId: number): NativeComplexityMetrics | null;
2133
2210
  getFileHash(file: string): string | null;
2134
2211
 
2212
+ // ── Composite queries ──────────────────────────────────────────────
2213
+ fnDeps(
2214
+ name: string,
2215
+ depth: number | null | undefined,
2216
+ noTests: boolean | null | undefined,
2217
+ file: string | null | undefined,
2218
+ kind: string | null | undefined,
2219
+ ): NativeFnDepsResult;
2220
+
2135
2221
  // ── Build pipeline writes (6.15) ───────────────────────────────────
2136
2222
  bulkInsertNodes(
2137
2223
  batches: Array<{