@optave/codegraph 2.2.3-dev.44e8146 → 2.3.1-dev.1aeea34

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.
@@ -2,8 +2,224 @@ import { findChild, nodeEndLine } from './helpers.js';
2
2
 
3
3
  /**
4
4
  * Extract symbols from a JS/TS parsed AST.
5
+ * When a compiled tree-sitter Query is provided (from parser.js),
6
+ * uses the fast query-based path. Falls back to manual tree walk otherwise.
5
7
  */
6
- export function extractSymbols(tree, _filePath) {
8
+ export function extractSymbols(tree, _filePath, query) {
9
+ if (query) return extractSymbolsQuery(tree, query);
10
+ return extractSymbolsWalk(tree);
11
+ }
12
+
13
+ // ── Query-based extraction (fast path) ──────────────────────────────────────
14
+
15
+ function extractSymbolsQuery(tree, query) {
16
+ const definitions = [];
17
+ const calls = [];
18
+ const imports = [];
19
+ const classes = [];
20
+ const exps = [];
21
+
22
+ const matches = query.matches(tree.rootNode);
23
+
24
+ for (const match of matches) {
25
+ // Build capture lookup for this match (1-3 captures each, very fast)
26
+ const c = Object.create(null);
27
+ for (const cap of match.captures) c[cap.name] = cap.node;
28
+
29
+ if (c.fn_node) {
30
+ // function_declaration
31
+ definitions.push({
32
+ name: c.fn_name.text,
33
+ kind: 'function',
34
+ line: c.fn_node.startPosition.row + 1,
35
+ endLine: nodeEndLine(c.fn_node),
36
+ });
37
+ } else if (c.varfn_name) {
38
+ // variable_declarator with arrow_function / function_expression
39
+ const declNode = c.varfn_name.parent?.parent;
40
+ const line = declNode ? declNode.startPosition.row + 1 : c.varfn_name.startPosition.row + 1;
41
+ definitions.push({
42
+ name: c.varfn_name.text,
43
+ kind: 'function',
44
+ line,
45
+ endLine: nodeEndLine(c.varfn_value),
46
+ });
47
+ } else if (c.cls_node) {
48
+ // class_declaration
49
+ const className = c.cls_name.text;
50
+ const startLine = c.cls_node.startPosition.row + 1;
51
+ definitions.push({
52
+ name: className,
53
+ kind: 'class',
54
+ line: startLine,
55
+ endLine: nodeEndLine(c.cls_node),
56
+ });
57
+ const heritage =
58
+ c.cls_node.childForFieldName('heritage') || findChild(c.cls_node, 'class_heritage');
59
+ if (heritage) {
60
+ const superName = extractSuperclass(heritage);
61
+ if (superName) classes.push({ name: className, extends: superName, line: startLine });
62
+ const implementsList = extractImplements(heritage);
63
+ for (const iface of implementsList) {
64
+ classes.push({ name: className, implements: iface, line: startLine });
65
+ }
66
+ }
67
+ } else if (c.meth_node) {
68
+ // method_definition
69
+ const methName = c.meth_name.text;
70
+ const parentClass = findParentClass(c.meth_node);
71
+ const fullName = parentClass ? `${parentClass}.${methName}` : methName;
72
+ definitions.push({
73
+ name: fullName,
74
+ kind: 'method',
75
+ line: c.meth_node.startPosition.row + 1,
76
+ endLine: nodeEndLine(c.meth_node),
77
+ });
78
+ } else if (c.iface_node) {
79
+ // interface_declaration (TS/TSX only)
80
+ const ifaceName = c.iface_name.text;
81
+ definitions.push({
82
+ name: ifaceName,
83
+ kind: 'interface',
84
+ line: c.iface_node.startPosition.row + 1,
85
+ endLine: nodeEndLine(c.iface_node),
86
+ });
87
+ const body =
88
+ c.iface_node.childForFieldName('body') ||
89
+ findChild(c.iface_node, 'interface_body') ||
90
+ findChild(c.iface_node, 'object_type');
91
+ if (body) extractInterfaceMethods(body, ifaceName, definitions);
92
+ } else if (c.type_node) {
93
+ // type_alias_declaration (TS/TSX only)
94
+ definitions.push({
95
+ name: c.type_name.text,
96
+ kind: 'type',
97
+ line: c.type_node.startPosition.row + 1,
98
+ endLine: nodeEndLine(c.type_node),
99
+ });
100
+ } else if (c.imp_node) {
101
+ // import_statement
102
+ const isTypeOnly = c.imp_node.text.startsWith('import type');
103
+ const modPath = c.imp_source.text.replace(/['"]/g, '');
104
+ const names = extractImportNames(c.imp_node);
105
+ imports.push({
106
+ source: modPath,
107
+ names,
108
+ line: c.imp_node.startPosition.row + 1,
109
+ typeOnly: isTypeOnly,
110
+ });
111
+ } else if (c.exp_node) {
112
+ // export_statement
113
+ const exportLine = c.exp_node.startPosition.row + 1;
114
+ const decl = c.exp_node.childForFieldName('declaration');
115
+ if (decl) {
116
+ const declType = decl.type;
117
+ const kindMap = {
118
+ function_declaration: 'function',
119
+ class_declaration: 'class',
120
+ interface_declaration: 'interface',
121
+ type_alias_declaration: 'type',
122
+ };
123
+ const kind = kindMap[declType];
124
+ if (kind) {
125
+ const n = decl.childForFieldName('name');
126
+ if (n) exps.push({ name: n.text, kind, line: exportLine });
127
+ }
128
+ }
129
+ const source = c.exp_node.childForFieldName('source') || findChild(c.exp_node, 'string');
130
+ if (source && !decl) {
131
+ const modPath = source.text.replace(/['"]/g, '');
132
+ const reexportNames = extractImportNames(c.exp_node);
133
+ const nodeText = c.exp_node.text;
134
+ const isWildcard = nodeText.includes('export *') || nodeText.includes('export*');
135
+ imports.push({
136
+ source: modPath,
137
+ names: reexportNames,
138
+ line: exportLine,
139
+ reexport: true,
140
+ wildcardReexport: isWildcard && reexportNames.length === 0,
141
+ });
142
+ }
143
+ } else if (c.callfn_node) {
144
+ // call_expression with identifier function
145
+ calls.push({
146
+ name: c.callfn_name.text,
147
+ line: c.callfn_node.startPosition.row + 1,
148
+ });
149
+ } else if (c.callmem_node) {
150
+ // call_expression with member_expression function
151
+ const callInfo = extractCallInfo(c.callmem_fn, c.callmem_node);
152
+ if (callInfo) calls.push(callInfo);
153
+ const cbDef = extractCallbackDefinition(c.callmem_node, c.callmem_fn);
154
+ if (cbDef) definitions.push(cbDef);
155
+ } else if (c.callsub_node) {
156
+ // call_expression with subscript_expression function
157
+ const callInfo = extractCallInfo(c.callsub_fn, c.callsub_node);
158
+ if (callInfo) calls.push(callInfo);
159
+ } else if (c.assign_node) {
160
+ // CommonJS: module.exports = require(...) / module.exports = { ...require(...) }
161
+ handleCommonJSAssignment(c.assign_left, c.assign_right, c.assign_node, imports);
162
+ }
163
+ }
164
+
165
+ return { definitions, calls, imports, classes, exports: exps };
166
+ }
167
+
168
+ function handleCommonJSAssignment(left, right, node, imports) {
169
+ if (!left || !right) return;
170
+ const leftText = left.text;
171
+ if (!leftText.startsWith('module.exports') && leftText !== 'exports') return;
172
+
173
+ const rightType = right.type;
174
+ const assignLine = node.startPosition.row + 1;
175
+
176
+ if (rightType === 'call_expression') {
177
+ const fn = right.childForFieldName('function');
178
+ const args = right.childForFieldName('arguments') || findChild(right, 'arguments');
179
+ if (fn && fn.text === 'require' && args) {
180
+ const strArg = findChild(args, 'string');
181
+ if (strArg) {
182
+ imports.push({
183
+ source: strArg.text.replace(/['"]/g, ''),
184
+ names: [],
185
+ line: assignLine,
186
+ reexport: true,
187
+ wildcardReexport: true,
188
+ });
189
+ }
190
+ }
191
+ }
192
+
193
+ if (rightType === 'object') {
194
+ for (let ci = 0; ci < right.childCount; ci++) {
195
+ const child = right.child(ci);
196
+ if (child && child.type === 'spread_element') {
197
+ const spreadExpr = child.child(1) || child.childForFieldName('value');
198
+ if (spreadExpr && spreadExpr.type === 'call_expression') {
199
+ const fn2 = spreadExpr.childForFieldName('function');
200
+ const args2 =
201
+ spreadExpr.childForFieldName('arguments') || findChild(spreadExpr, 'arguments');
202
+ if (fn2 && fn2.text === 'require' && args2) {
203
+ const strArg2 = findChild(args2, 'string');
204
+ if (strArg2) {
205
+ imports.push({
206
+ source: strArg2.text.replace(/['"]/g, ''),
207
+ names: [],
208
+ line: assignLine,
209
+ reexport: true,
210
+ wildcardReexport: true,
211
+ });
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ // ── Manual tree walk (fallback when Query not available) ────────────────────
221
+
222
+ function extractSymbolsWalk(tree) {
7
223
  const definitions = [];
8
224
  const calls = [];
9
225
  const imports = [];
@@ -28,30 +244,23 @@ export function extractSymbols(tree, _filePath) {
28
244
  case 'class_declaration': {
29
245
  const nameNode = node.childForFieldName('name');
30
246
  if (nameNode) {
31
- const cls = {
32
- name: nameNode.text,
247
+ const className = nameNode.text;
248
+ const startLine = node.startPosition.row + 1;
249
+ definitions.push({
250
+ name: className,
33
251
  kind: 'class',
34
- line: node.startPosition.row + 1,
252
+ line: startLine,
35
253
  endLine: nodeEndLine(node),
36
- };
37
- definitions.push(cls);
254
+ });
38
255
  const heritage = node.childForFieldName('heritage') || findChild(node, 'class_heritage');
39
256
  if (heritage) {
40
257
  const superName = extractSuperclass(heritage);
41
258
  if (superName) {
42
- classes.push({
43
- name: nameNode.text,
44
- extends: superName,
45
- line: node.startPosition.row + 1,
46
- });
259
+ classes.push({ name: className, extends: superName, line: startLine });
47
260
  }
48
261
  const implementsList = extractImplements(heritage);
49
262
  for (const iface of implementsList) {
50
- classes.push({
51
- name: nameNode.text,
52
- implements: iface,
53
- line: node.startPosition.row + 1,
54
- });
263
+ classes.push({ name: className, implements: iface, line: startLine });
55
264
  }
56
265
  }
57
266
  }
@@ -113,19 +322,20 @@ export function extractSymbols(tree, _filePath) {
113
322
  if (declarator && declarator.type === 'variable_declarator') {
114
323
  const nameN = declarator.childForFieldName('name');
115
324
  const valueN = declarator.childForFieldName('value');
116
- if (
117
- nameN &&
118
- valueN &&
119
- (valueN.type === 'arrow_function' ||
120
- valueN.type === 'function_expression' ||
121
- valueN.type === 'function')
122
- ) {
123
- definitions.push({
124
- name: nameN.text,
125
- kind: 'function',
126
- line: node.startPosition.row + 1,
127
- endLine: nodeEndLine(valueN),
128
- });
325
+ if (nameN && valueN) {
326
+ const valType = valueN.type;
327
+ if (
328
+ valType === 'arrow_function' ||
329
+ valType === 'function_expression' ||
330
+ valType === 'function'
331
+ ) {
332
+ definitions.push({
333
+ name: nameN.text,
334
+ kind: 'function',
335
+ line: node.startPosition.row + 1,
336
+ endLine: nodeEndLine(valueN),
337
+ });
338
+ }
129
339
  }
130
340
  }
131
341
  }
@@ -136,9 +346,7 @@ export function extractSymbols(tree, _filePath) {
136
346
  const fn = node.childForFieldName('function');
137
347
  if (fn) {
138
348
  const callInfo = extractCallInfo(fn, node);
139
- if (callInfo) {
140
- calls.push(callInfo);
141
- }
349
+ if (callInfo) calls.push(callInfo);
142
350
  if (fn.type === 'member_expression') {
143
351
  const cbDef = extractCallbackDefinition(node, fn);
144
352
  if (cbDef) definitions.push(cbDef);
@@ -164,33 +372,32 @@ export function extractSymbols(tree, _filePath) {
164
372
  }
165
373
 
166
374
  case 'export_statement': {
375
+ const exportLine = node.startPosition.row + 1;
167
376
  const decl = node.childForFieldName('declaration');
168
377
  if (decl) {
169
- if (decl.type === 'function_declaration') {
170
- const n = decl.childForFieldName('name');
171
- if (n)
172
- exports.push({ name: n.text, kind: 'function', line: node.startPosition.row + 1 });
173
- } else if (decl.type === 'class_declaration') {
174
- const n = decl.childForFieldName('name');
175
- if (n) exports.push({ name: n.text, kind: 'class', line: node.startPosition.row + 1 });
176
- } else if (decl.type === 'interface_declaration') {
177
- const n = decl.childForFieldName('name');
178
- if (n)
179
- exports.push({ name: n.text, kind: 'interface', line: node.startPosition.row + 1 });
180
- } else if (decl.type === 'type_alias_declaration') {
378
+ const declType = decl.type;
379
+ const kindMap = {
380
+ function_declaration: 'function',
381
+ class_declaration: 'class',
382
+ interface_declaration: 'interface',
383
+ type_alias_declaration: 'type',
384
+ };
385
+ const kind = kindMap[declType];
386
+ if (kind) {
181
387
  const n = decl.childForFieldName('name');
182
- if (n) exports.push({ name: n.text, kind: 'type', line: node.startPosition.row + 1 });
388
+ if (n) exports.push({ name: n.text, kind, line: exportLine });
183
389
  }
184
390
  }
185
391
  const source = node.childForFieldName('source') || findChild(node, 'string');
186
392
  if (source && !decl) {
187
393
  const modPath = source.text.replace(/['"]/g, '');
188
394
  const reexportNames = extractImportNames(node);
189
- const isWildcard = node.text.includes('export *') || node.text.includes('export*');
395
+ const nodeText = node.text;
396
+ const isWildcard = nodeText.includes('export *') || nodeText.includes('export*');
190
397
  imports.push({
191
398
  source: modPath,
192
399
  names: reexportNames,
193
- line: node.startPosition.row + 1,
400
+ line: exportLine,
194
401
  reexport: true,
195
402
  wildcardReexport: isWildcard && reexportNames.length === 0,
196
403
  });
@@ -212,9 +419,8 @@ export function extractSymbols(tree, _filePath) {
212
419
  if (fn && fn.text === 'require' && args) {
213
420
  const strArg = findChild(args, 'string');
214
421
  if (strArg) {
215
- const modPath = strArg.text.replace(/['"]/g, '');
216
422
  imports.push({
217
- source: modPath,
423
+ source: strArg.text.replace(/['"]/g, ''),
218
424
  names: [],
219
425
  line: node.startPosition.row + 1,
220
426
  reexport: true,
@@ -236,9 +442,8 @@ export function extractSymbols(tree, _filePath) {
236
442
  if (fn2 && fn2.text === 'require' && args2) {
237
443
  const strArg2 = findChild(args2, 'string');
238
444
  if (strArg2) {
239
- const modPath2 = strArg2.text.replace(/['"]/g, '');
240
445
  imports.push({
241
- source: modPath2,
446
+ source: strArg2.text.replace(/['"]/g, ''),
242
447
  names: [],
243
448
  line: node.startPosition.row + 1,
244
449
  reexport: true,
@@ -266,6 +471,8 @@ export function extractSymbols(tree, _filePath) {
266
471
  return { definitions, calls, imports, classes, exports };
267
472
  }
268
473
 
474
+ // ── Shared helpers ──────────────────────────────────────────────────────────
475
+
269
476
  function extractInterfaceMethods(bodyNode, interfaceName, definitions) {
270
477
  for (let i = 0; i < bodyNode.childCount; i++) {
271
478
  const child = bodyNode.child(i);
@@ -319,52 +526,63 @@ function extractImplementsFromNode(node) {
319
526
 
320
527
  function extractReceiverName(objNode) {
321
528
  if (!objNode) return undefined;
322
- if (objNode.type === 'identifier') return objNode.text;
323
- if (objNode.type === 'this') return 'this';
324
- if (objNode.type === 'super') return 'super';
529
+ const t = objNode.type;
530
+ if (t === 'identifier' || t === 'this' || t === 'super') return objNode.text;
325
531
  return objNode.text;
326
532
  }
327
533
 
328
534
  function extractCallInfo(fn, callNode) {
329
- if (fn.type === 'identifier') {
535
+ const fnType = fn.type;
536
+ if (fnType === 'identifier') {
330
537
  return { name: fn.text, line: callNode.startPosition.row + 1 };
331
538
  }
332
539
 
333
- if (fn.type === 'member_expression') {
540
+ if (fnType === 'member_expression') {
334
541
  const obj = fn.childForFieldName('object');
335
542
  const prop = fn.childForFieldName('property');
336
543
  if (!prop) return null;
337
544
 
338
- if (prop.text === 'call' || prop.text === 'apply' || prop.text === 'bind') {
545
+ const callLine = callNode.startPosition.row + 1;
546
+ const propText = prop.text;
547
+
548
+ if (propText === 'call' || propText === 'apply' || propText === 'bind') {
339
549
  if (obj && obj.type === 'identifier')
340
- return { name: obj.text, line: callNode.startPosition.row + 1, dynamic: true };
550
+ return { name: obj.text, line: callLine, dynamic: true };
341
551
  if (obj && obj.type === 'member_expression') {
342
552
  const innerProp = obj.childForFieldName('property');
343
- if (innerProp)
344
- return { name: innerProp.text, line: callNode.startPosition.row + 1, dynamic: true };
553
+ if (innerProp) return { name: innerProp.text, line: callLine, dynamic: true };
345
554
  }
346
555
  }
347
556
 
348
- if (prop.type === 'string' || prop.type === 'string_fragment') {
349
- const methodName = prop.text.replace(/['"]/g, '');
557
+ const propType = prop.type;
558
+ if (propType === 'string' || propType === 'string_fragment') {
559
+ const methodName = propText.replace(/['"]/g, '');
350
560
  if (methodName) {
351
561
  const receiver = extractReceiverName(obj);
352
- return { name: methodName, line: callNode.startPosition.row + 1, dynamic: true, receiver };
562
+ return { name: methodName, line: callLine, dynamic: true, receiver };
353
563
  }
354
564
  }
355
565
 
356
566
  const receiver = extractReceiverName(obj);
357
- return { name: prop.text, line: callNode.startPosition.row + 1, receiver };
567
+ return { name: propText, line: callLine, receiver };
358
568
  }
359
569
 
360
- if (fn.type === 'subscript_expression') {
570
+ if (fnType === 'subscript_expression') {
361
571
  const obj = fn.childForFieldName('object');
362
572
  const index = fn.childForFieldName('index');
363
- if (index && (index.type === 'string' || index.type === 'template_string')) {
364
- const methodName = index.text.replace(/['"`]/g, '');
365
- if (methodName && !methodName.includes('$')) {
366
- const receiver = extractReceiverName(obj);
367
- return { name: methodName, line: callNode.startPosition.row + 1, dynamic: true, receiver };
573
+ if (index) {
574
+ const indexType = index.type;
575
+ if (indexType === 'string' || indexType === 'template_string') {
576
+ const methodName = index.text.replace(/['"`]/g, '');
577
+ if (methodName && !methodName.includes('$')) {
578
+ const receiver = extractReceiverName(obj);
579
+ return {
580
+ name: methodName,
581
+ line: callNode.startPosition.row + 1,
582
+ dynamic: true,
583
+ receiver,
584
+ };
585
+ }
368
586
  }
369
587
  }
370
588
  }
@@ -395,7 +613,8 @@ function findFirstStringArg(argsNode) {
395
613
  function walkCallChain(startNode, methodName) {
396
614
  let current = startNode;
397
615
  while (current) {
398
- if (current.type === 'call_expression') {
616
+ const curType = current.type;
617
+ if (curType === 'call_expression') {
399
618
  const fn = current.childForFieldName('function');
400
619
  if (fn && fn.type === 'member_expression') {
401
620
  const prop = fn.childForFieldName('property');
@@ -403,13 +622,9 @@ function walkCallChain(startNode, methodName) {
403
622
  return current;
404
623
  }
405
624
  }
406
- }
407
- if (current.type === 'member_expression') {
408
- const obj = current.childForFieldName('object');
409
- current = obj;
410
- } else if (current.type === 'call_expression') {
411
- const fn = current.childForFieldName('function');
412
625
  current = fn;
626
+ } else if (curType === 'member_expression') {
627
+ current = current.childForFieldName('object');
413
628
  } else {
414
629
  break;
415
630
  }
@@ -506,7 +721,8 @@ function extractSuperclass(heritage) {
506
721
  function findParentClass(node) {
507
722
  let current = node.parent;
508
723
  while (current) {
509
- if (current.type === 'class_declaration' || current.type === 'class') {
724
+ const t = current.type;
725
+ if (t === 'class_declaration' || t === 'class') {
510
726
  const nameNode = current.childForFieldName('name');
511
727
  return nameNode ? nameNode.text : null;
512
728
  }
package/src/index.js CHANGED
@@ -7,6 +7,15 @@
7
7
 
8
8
  // Graph building
9
9
  export { buildGraph, collectFiles, loadPathAliases, resolveImportPath } from './builder.js';
10
+ // Co-change analysis
11
+ export {
12
+ analyzeCoChanges,
13
+ coChangeData,
14
+ coChangeForFiles,
15
+ coChangeTopData,
16
+ computeCoChanges,
17
+ scanGitHistory,
18
+ } from './cochange.js';
10
19
  // Configuration
11
20
  export { loadConfig } from './config.js';
12
21
  // Shared constants
@@ -21,6 +30,7 @@ export {
21
30
  buildEmbeddings,
22
31
  cosineSim,
23
32
  DEFAULT_MODEL,
33
+ disposeModel,
24
34
  EMBEDDING_STRATEGIES,
25
35
  embed,
26
36
  estimateTokens,
@@ -53,7 +63,9 @@ export {
53
63
  impactAnalysisData,
54
64
  moduleMapData,
55
65
  queryNameData,
66
+ rolesData,
56
67
  statsData,
68
+ VALID_ROLES,
57
69
  whereData,
58
70
  } from './queries.js';
59
71
  // Registry (multi-repo)
@@ -70,6 +82,7 @@ export {
70
82
  // Structure analysis
71
83
  export {
72
84
  buildStructure,
85
+ classifyNodeRoles,
73
86
  formatHotspots,
74
87
  formatModuleBoundaries,
75
88
  formatStructure,
package/src/mcp.js CHANGED
@@ -8,7 +8,7 @@
8
8
  import { createRequire } from 'node:module';
9
9
  import { findCycles } from './cycles.js';
10
10
  import { findDbPath } from './db.js';
11
- import { ALL_SYMBOL_KINDS, diffImpactMermaid } from './queries.js';
11
+ import { ALL_SYMBOL_KINDS, diffImpactMermaid, VALID_ROLES } from './queries.js';
12
12
 
13
13
  const REPO_PROP = {
14
14
  repo: {
@@ -273,6 +273,23 @@ const BASE_TOOLS = [
273
273
  },
274
274
  },
275
275
  },
276
+ {
277
+ name: 'node_roles',
278
+ description:
279
+ 'Show node role classification (entry, core, utility, adapter, dead, leaf) based on connectivity patterns',
280
+ inputSchema: {
281
+ type: 'object',
282
+ properties: {
283
+ role: {
284
+ type: 'string',
285
+ enum: VALID_ROLES,
286
+ description: 'Filter to a specific role',
287
+ },
288
+ file: { type: 'string', description: 'Scope to a specific file (partial match)' },
289
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
290
+ },
291
+ },
292
+ },
276
293
  {
277
294
  name: 'hotspots',
278
295
  description:
@@ -295,6 +312,27 @@ const BASE_TOOLS = [
295
312
  },
296
313
  },
297
314
  },
315
+ {
316
+ name: 'co_changes',
317
+ description:
318
+ 'Find files that historically change together based on git commit history. Requires prior `codegraph co-change --analyze`.',
319
+ inputSchema: {
320
+ type: 'object',
321
+ properties: {
322
+ file: {
323
+ type: 'string',
324
+ description: 'File path (partial match). Omit for top global pairs.',
325
+ },
326
+ limit: { type: 'number', description: 'Max results', default: 20 },
327
+ min_jaccard: {
328
+ type: 'number',
329
+ description: 'Minimum Jaccard similarity (0-1)',
330
+ default: 0.3,
331
+ },
332
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
333
+ },
334
+ },
335
+ },
298
336
  ];
299
337
 
300
338
  const LIST_REPOS_TOOL = {
@@ -372,6 +410,7 @@ export async function startMCPServer(customDbPath, options = {}) {
372
410
  whereData,
373
411
  diffImpactData,
374
412
  listFunctionsData,
413
+ rolesData,
375
414
  } = await import('./queries.js');
376
415
 
377
416
  const require = createRequire(import.meta.url);
@@ -540,6 +579,13 @@ export async function startMCPServer(customDbPath, options = {}) {
540
579
  noTests: args.no_tests,
541
580
  });
542
581
  break;
582
+ case 'node_roles':
583
+ result = rolesData(dbPath, {
584
+ role: args.role,
585
+ file: args.file,
586
+ noTests: args.no_tests,
587
+ });
588
+ break;
543
589
  case 'structure': {
544
590
  const { structureData } = await import('./structure.js');
545
591
  result = structureData(dbPath, {
@@ -559,6 +605,21 @@ export async function startMCPServer(customDbPath, options = {}) {
559
605
  });
560
606
  break;
561
607
  }
608
+ case 'co_changes': {
609
+ const { coChangeData, coChangeTopData } = await import('./cochange.js');
610
+ result = args.file
611
+ ? coChangeData(args.file, dbPath, {
612
+ limit: args.limit,
613
+ minJaccard: args.min_jaccard,
614
+ noTests: args.no_tests,
615
+ })
616
+ : coChangeTopData(dbPath, {
617
+ limit: args.limit,
618
+ minJaccard: args.min_jaccard,
619
+ noTests: args.no_tests,
620
+ });
621
+ break;
622
+ }
562
623
  case 'list_repos': {
563
624
  const { listRepos, pruneRegistry } = await import('./registry.js');
564
625
  pruneRegistry();