dsh-code-index 0.3.1 → 0.5.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.
package/dist/index.js CHANGED
@@ -15,7 +15,9 @@ var GRAMMAR_NAMES = {
15
15
  python: "tree-sitter-python",
16
16
  go: "tree-sitter-go",
17
17
  rust: "tree-sitter-rust",
18
- java: "tree-sitter-java"
18
+ java: "tree-sitter-java",
19
+ cpp: "tree-sitter-cpp",
20
+ c: "tree-sitter-c"
19
21
  };
20
22
  var EXT_TO_LANG = {
21
23
  ".ts": "typescript",
@@ -30,7 +32,19 @@ var EXT_TO_LANG = {
30
32
  ".pyi": "python",
31
33
  ".go": "go",
32
34
  ".rs": "rust",
33
- ".java": "java"
35
+ ".java": "java",
36
+ ".cpp": "cpp",
37
+ ".cc": "cpp",
38
+ ".cxx": "cpp",
39
+ ".c++": "cpp",
40
+ ".hpp": "cpp",
41
+ ".hxx": "cpp",
42
+ ".hh": "cpp",
43
+ ".h": "cpp",
44
+ ".ipp": "cpp",
45
+ ".tpp": "cpp",
46
+ ".inl": "cpp",
47
+ ".c": "c"
34
48
  };
35
49
  function languageForFile(filePath) {
36
50
  const ext = path.extname(filePath).toLowerCase();
@@ -79,6 +93,31 @@ var QUERIES = {
79
93
  (enum_declaration) @enum
80
94
  (method_declaration) @method
81
95
  (constructor_declaration) @method
96
+ `,
97
+ // C++ grammar (superset of C — also serves .h headers). function names
98
+ // live inside the declarator chain, so patterns stay broad and names,
99
+ // kinds and signatures are resolved in code (cppNameOf / kindFor /
100
+ // signatureFor). Top-level `declaration` covers prototypes, constructors
101
+ // and globals; field_declaration covers class-body members.
102
+ cpp: `
103
+ (function_definition) @function
104
+ (class_specifier body: (_)) @class
105
+ (struct_specifier body: (_)) @class
106
+ (enum_specifier body: (_)) @enum
107
+ (type_definition) @type
108
+ (alias_declaration) @type
109
+ (namespace_definition) @module
110
+ (field_declaration) @field
111
+ (declaration) @variable
112
+ `,
113
+ // Plain C: same core nodes minus C++-only namespace/alias forms.
114
+ c: `
115
+ (function_definition) @function
116
+ (struct_specifier body: (_)) @class
117
+ (enum_specifier body: (_)) @enum
118
+ (type_definition) @type
119
+ (field_declaration) @field
120
+ (declaration) @variable
82
121
  `
83
122
  };
84
123
  var CAPTURE_KINDS = {
@@ -89,7 +128,8 @@ var CAPTURE_KINDS = {
89
128
  type: { kind: "type" },
90
129
  enum: { kind: "enum" },
91
130
  variable: { kind: "variable" },
92
- field: { kind: "field" }
131
+ field: { kind: "field" },
132
+ module: { kind: "module" }
93
133
  };
94
134
  var IMPORT_QUERIES = {
95
135
  typescript: `
@@ -112,8 +152,76 @@ var IMPORT_QUERIES = {
112
152
  `,
113
153
  java: `
114
154
  (import_declaration) @import
155
+ `,
156
+ // C/C++: #include "local.hpp" — quoted form resolves in-repo; <system>
157
+ // captures too but yields null and is dropped by the extractor.
158
+ cpp: `
159
+ (preproc_include) @import
160
+ `,
161
+ c: `
162
+ (preproc_include) @import
163
+ `
164
+ };
165
+ var CALL_QUERIES = {
166
+ typescript: `
167
+ (call_expression function: (identifier) @call)
168
+ (call_expression function: (member_expression property: (property_identifier) @call))
169
+ (new_expression constructor: (identifier) @call)
170
+ `,
171
+ javascript: `
172
+ (call_expression function: (identifier) @call)
173
+ (call_expression function: (member_expression property: (property_identifier) @call))
174
+ (new_expression constructor: (identifier) @call)
175
+ `,
176
+ python: `
177
+ (call function: (identifier) @call)
178
+ (call function: (attribute attribute: (identifier) @call))
179
+ `,
180
+ go: `
181
+ (call_expression function: (identifier) @call)
182
+ (call_expression function: (selector_expression field: (field_identifier) @call))
183
+ `,
184
+ rust: `
185
+ (call_expression function: (identifier) @call)
186
+ (call_expression function: (field_expression field: (field_identifier) @call))
187
+ (call_expression function: (scoped_identifier name: (identifier) @call))
188
+ `,
189
+ java: `
190
+ (method_invocation name: (identifier) @call)
191
+ (object_creation_expression type: (type_identifier) @call)
192
+ `,
193
+ cpp: `
194
+ (call_expression function: (identifier) @call)
195
+ (call_expression function: (field_expression field: (field_identifier) @call))
196
+ (call_expression function: (qualified_identifier name: (identifier) @call))
197
+ `,
198
+ c: `
199
+ (call_expression function: (identifier) @call)
200
+ (call_expression function: (field_expression field: (field_identifier) @call))
115
201
  `
116
202
  };
203
+ var CALLABLE_NODES = {
204
+ typescript: /* @__PURE__ */ new Set([
205
+ "function_declaration",
206
+ "generator_function_declaration",
207
+ "method_definition",
208
+ "function_expression",
209
+ "arrow_function"
210
+ ]),
211
+ javascript: /* @__PURE__ */ new Set([
212
+ "function_declaration",
213
+ "generator_function_declaration",
214
+ "method_definition",
215
+ "function_expression",
216
+ "arrow_function"
217
+ ]),
218
+ python: /* @__PURE__ */ new Set(["function_definition"]),
219
+ go: /* @__PURE__ */ new Set(["function_declaration", "method_declaration", "func_literal"]),
220
+ rust: /* @__PURE__ */ new Set(["function_item", "function_signature_item"]),
221
+ java: /* @__PURE__ */ new Set(["method_declaration", "constructor_declaration"]),
222
+ cpp: /* @__PURE__ */ new Set(["function_definition", "lambda_expression"]),
223
+ c: /* @__PURE__ */ new Set(["function_definition"])
224
+ };
117
225
  var parserPromise = null;
118
226
  async function getParser() {
119
227
  if (!parserPromise) {
@@ -136,6 +244,7 @@ function getLanguage(id) {
136
244
  const lang = await Language.load(bytes);
137
245
  new Query(lang, QUERIES[id]).delete();
138
246
  new Query(lang, IMPORT_QUERIES[id]).delete();
247
+ new Query(lang, CALL_QUERIES[id]).delete();
139
248
  return lang;
140
249
  });
141
250
  languageCache.set(id, entry);
@@ -157,8 +266,8 @@ async function extractAll(code, id) {
157
266
  const def = CAPTURE_KINDS[cap.name];
158
267
  if (!def) continue;
159
268
  const node = cap.node;
160
- if (def.kind === "variable" && !isModuleLevelVariable(node)) continue;
161
- const name2 = nameOf(node);
269
+ if (def.kind === "variable" && !isModuleLevelVariable(id, node)) continue;
270
+ const name2 = nameOf(id, node);
162
271
  if (!name2) continue;
163
272
  symbols.push({
164
273
  name: name2,
@@ -168,7 +277,7 @@ async function extractAll(code, id) {
168
277
  line: node.startPosition.row + 1,
169
278
  endLine: node.endPosition.row + 1,
170
279
  exported: isExported(id, node),
171
- signature: signatureFor(node)
280
+ signature: signatureFor(id, node)
172
281
  });
173
282
  }
174
283
  } finally {
@@ -184,8 +293,33 @@ async function extractAll(code, id) {
184
293
  } finally {
185
294
  importQuery.delete();
186
295
  }
296
+ const calls = [];
297
+ const callQuery = new Query(lang, CALL_QUERIES[id]);
298
+ try {
299
+ const seen2 = /* @__PURE__ */ new Set();
300
+ for (const cap of callQuery.captures(tree.rootNode)) {
301
+ const name2 = cap.node.text.trim();
302
+ if (!name2) continue;
303
+ const line = cap.node.startPosition.row + 1;
304
+ const from = enclosingCallable(id, cap.node);
305
+ const key = `${from}|${name2}|${line}`;
306
+ if (seen2.has(key)) continue;
307
+ seen2.add(key);
308
+ calls.push({ name: name2, line, from });
309
+ }
310
+ } finally {
311
+ callQuery.delete();
312
+ }
313
+ calls.sort((a, b) => a.line - b.line || a.name.localeCompare(b.name));
187
314
  symbols.sort((a, b) => a.line - b.line);
188
- return { symbols, imports };
315
+ const seen = /* @__PURE__ */ new Set();
316
+ const deduped = symbols.filter((s) => {
317
+ const key = `${s.kind}|${s.name}|${s.line}`;
318
+ if (seen.has(key)) return false;
319
+ seen.add(key);
320
+ return true;
321
+ });
322
+ return { symbols: deduped, imports, calls };
189
323
  } finally {
190
324
  tree.delete();
191
325
  }
@@ -193,11 +327,53 @@ async function extractAll(code, id) {
193
327
  async function extractSymbols(code, id) {
194
328
  return (await extractAll(code, id)).symbols;
195
329
  }
196
- function nameOf(node) {
330
+ function nameOf(id, node) {
331
+ if (id === "cpp" || id === "c") return cppNameOf(node);
197
332
  const field = node.childForFieldName?.("name");
198
333
  if (field) return field.text.trim();
199
334
  return "";
200
335
  }
336
+ function cppNameOf(node) {
337
+ const nameField = node.childForFieldName?.("name");
338
+ if (nameField) return nameField.text.trim();
339
+ if (node.type === "type_definition") {
340
+ return node.childForFieldName?.("declarator")?.text.trim() ?? "";
341
+ }
342
+ let declarator = node.childForFieldName?.("declarator");
343
+ if (declarator?.type === "init_declarator") {
344
+ declarator = declarator.childForFieldName?.("declarator");
345
+ }
346
+ if (declarator) {
347
+ let current = declarator;
348
+ while (current) {
349
+ if (current.type === "function_declarator" || current.type === "pointer_declarator" || current.type === "array_declarator" || current.type === "reference_declarator" || current.type === "parenthesized_declarator" || current.type === "init_declarator") {
350
+ current = current.childForFieldName?.("declarator") ?? current.namedChildren[0] ?? null;
351
+ continue;
352
+ }
353
+ if (current.type === "qualified_identifier") {
354
+ return current.childForFieldName?.("name")?.text.trim() ?? current.text.trim();
355
+ }
356
+ if (current.type === "identifier" || current.type === "field_identifier" || current.type === "type_identifier") {
357
+ return current.text.trim();
358
+ }
359
+ break;
360
+ }
361
+ }
362
+ return "";
363
+ }
364
+ function enclosingCallable(id, node) {
365
+ const callable = CALLABLE_NODES[id];
366
+ for (let parent = node.parent; parent; parent = parent.parent) {
367
+ if (!callable.has(parent.type)) continue;
368
+ const named = nameOf(id, parent);
369
+ if (named) return named;
370
+ const declaration = parent.parent;
371
+ const field = declaration?.type === "variable_declarator" ? "name" : null;
372
+ if (field) return declaration?.childForFieldName?.(field)?.text.trim() ?? "";
373
+ return "";
374
+ }
375
+ return "";
376
+ }
201
377
  function specifierOf(id, node) {
202
378
  switch (node.type) {
203
379
  case "import_statement":
@@ -216,6 +392,8 @@ function specifierOf(id, node) {
216
392
  return rustUsePath(node.childForFieldName("argument")?.text);
217
393
  case "import_declaration":
218
394
  return dottedToPath(node.namedChildren[0]?.text);
395
+ case "preproc_include":
396
+ return stripQuotes(node.childForFieldName("path")?.text);
219
397
  default:
220
398
  return null;
221
399
  }
@@ -247,21 +425,43 @@ function pythonRelative(raw) {
247
425
  const prefix = dots === 1 ? "./" : "../".repeat(dots - 1);
248
426
  return prefix + rest;
249
427
  }
250
- function signatureFor(node) {
251
- const name2 = nameOf(node);
428
+ function signatureFor(id, node) {
429
+ const sig = rawSignature(id, node);
430
+ return sig.length > 80 ? `${sig.slice(0, 77)}...` : sig;
431
+ }
432
+ function rawSignature(id, node) {
433
+ const name2 = nameOf(id, node);
252
434
  const params = node.childForFieldName?.("parameters") ?? node.namedChildren.find(
253
435
  (c) => c?.type === "formal_parameters" || c?.type === "method_parameters" || c?.type === "parameters" || c?.type === "parameter_list"
254
436
  );
255
437
  if (params) {
256
438
  return `${name2}${collapseSpace(params.text)}`;
257
439
  }
440
+ if (id === "cpp" || id === "c") {
441
+ let current = node.childForFieldName?.("declarator");
442
+ while (current) {
443
+ if (current.type === "function_declarator") {
444
+ const list = current.childForFieldName?.("parameters");
445
+ return list ? `${name2}${collapseSpace(list.text)}` : name2;
446
+ }
447
+ current = current.childForFieldName?.("declarator") ?? current.namedChildren[0] ?? null;
448
+ if (current && !current.type.includes("declarator") && current.type !== "identifier" && current.type !== "qualified_identifier" && current.type !== "field_identifier") break;
449
+ }
450
+ }
258
451
  const first = node.namedChildren[0];
259
452
  return first ? collapseSpace(first.text) : name2;
260
453
  }
261
454
  function collapseSpace(text) {
262
455
  return text.replace(/\s+/g, " ").replace(/\( /g, "(").replace(/ \)/g, ")").replace(/ ,/g, ",").replace(/,\)/g, ")").trim();
263
456
  }
264
- function isModuleLevelVariable(node) {
457
+ function isModuleLevelVariable(id, node) {
458
+ if (id === "cpp" || id === "c") {
459
+ for (let parent = node.parent; parent; parent = parent.parent) {
460
+ if (parent.type === "function_definition" || parent.type === "compound_statement") return false;
461
+ if (parent.type === "translation_unit" || parent.type === "declaration_list" || parent.type === "field_declaration_list") return true;
462
+ }
463
+ return false;
464
+ }
265
465
  const declaration = node.parent;
266
466
  const container = declaration?.parent;
267
467
  return container?.type === "program" || container?.type === "export_statement";
@@ -271,7 +471,7 @@ function isExported(id, node) {
271
471
  return node.parent?.type === "module";
272
472
  }
273
473
  if (id === "go") {
274
- const name2 = nameOf(node);
474
+ const name2 = nameOf(id, node);
275
475
  return !!name2 && /^[A-Z]/.test(name2);
276
476
  }
277
477
  if (id === "rust") {
@@ -283,6 +483,9 @@ function isExported(id, node) {
283
483
  if (node.parent?.type === "interface_body") return true;
284
484
  return node.namedChildren.some((c) => c?.type === "modifiers" && /\bpublic\b/.test(c?.text ?? ""));
285
485
  }
486
+ if (id === "cpp" || id === "c") {
487
+ return cppIsExported(node);
488
+ }
286
489
  for (let parent = node.parent; parent; parent = parent.parent) {
287
490
  if (parent.type === "export_statement") return true;
288
491
  if (parent.type === "statement_block" || parent.type === "class_body") return false;
@@ -290,6 +493,31 @@ function isExported(id, node) {
290
493
  }
291
494
  return false;
292
495
  }
496
+ function cppIsExported(node) {
497
+ const isStatic = node.namedChildren.some(
498
+ (c) => c?.type === "storage_class_specifier" && c.text === "static"
499
+ );
500
+ for (let parent = node.parent; parent; parent = parent.parent) {
501
+ if (parent.type === "field_declaration_list") {
502
+ const body = parent;
503
+ const structKind = body.parent?.type === "struct_specifier" ? "struct" : "class";
504
+ let current = structKind === "struct" ? "public" : "private";
505
+ for (let up = node; up && up.id !== body.id; up = up.parent) {
506
+ const next = up.parent;
507
+ if (next?.type === "field_declaration_list") {
508
+ const siblingIndex = next.namedChildren.findIndex((c) => c?.id === up.id);
509
+ const accessBefore = next.namedChildren.slice(0, siblingIndex).filter((c) => c?.type === "access_specifier");
510
+ if (accessBefore.length > 0) {
511
+ current = accessBefore.at(-1).text.replace(":", "").trim();
512
+ }
513
+ }
514
+ }
515
+ return current === "public" && !isStatic;
516
+ }
517
+ if (parent.type === "translation_unit") return !isStatic;
518
+ }
519
+ return !isStatic;
520
+ }
293
521
  function kindFor(id, node, kind) {
294
522
  if (id === "python" && kind === "function") {
295
523
  const inClassBody = node.parent?.type === "block" && node.parent?.parent?.type === "class_definition";
@@ -305,6 +533,19 @@ function kindFor(id, node, kind) {
305
533
  const inImpl = node.parent?.type === "declaration_list" && node.parent?.parent?.type === "impl_item";
306
534
  if (inImpl) return "method";
307
535
  }
536
+ if ((id === "cpp" || id === "c") && (kind === "variable" || kind === "field")) {
537
+ let current = node.childForFieldName?.("declarator");
538
+ while (current) {
539
+ if (current.type === "function_declarator") {
540
+ return node.parent?.type === "field_declaration_list" ? "method" : "function";
541
+ }
542
+ current = current.childForFieldName?.("declarator") ?? current.namedChildren[0] ?? null;
543
+ }
544
+ return node.type === "field_declaration" ? "field" : "variable";
545
+ }
546
+ if ((id === "cpp" || id === "c") && node.type === "function_definition") {
547
+ if (node.parent?.type === "field_declaration_list") return "method";
548
+ }
308
549
  return kind;
309
550
  }
310
551
  async function parseFileToSymbols(filePath, repoRoot, code) {
@@ -348,7 +589,19 @@ var SUPPORTED_EXTS = /* @__PURE__ */ new Set([
348
589
  ".pyi",
349
590
  ".go",
350
591
  ".rs",
351
- ".java"
592
+ ".java",
593
+ ".cpp",
594
+ ".cc",
595
+ ".cxx",
596
+ ".c++",
597
+ ".hpp",
598
+ ".hxx",
599
+ ".hh",
600
+ ".h",
601
+ ".ipp",
602
+ ".tpp",
603
+ ".inl",
604
+ ".c"
352
605
  ]);
353
606
  async function scanRepo(root, options = {}) {
354
607
  const excluded = /* @__PURE__ */ new Set([...DEFAULT_EXCLUDED_DIRS, ...options.excludeDirs ?? []]);
@@ -456,13 +709,14 @@ async function buildIndex(root, options = {}, previous = null) {
456
709
  } catch {
457
710
  return null;
458
711
  }
459
- const { symbols, imports } = await extractAll(code, lang);
712
+ const { symbols, imports, calls } = await extractAll(code, lang);
460
713
  return {
461
714
  path: f.rel,
462
715
  lang,
463
716
  mtimeMs: f.mtimeMs,
464
717
  symbols: symbols.map((s) => ({ ...s, file: f.rel })),
465
- imports
718
+ imports,
719
+ calls
466
720
  };
467
721
  })
468
722
  );
@@ -508,12 +762,22 @@ function indexesEqual(left, right) {
508
762
  return false;
509
763
  }
510
764
  if (file.symbols.length !== other.symbols.length) return false;
765
+ if (!callsEqual(file.calls, other.calls)) return false;
511
766
  return file.symbols.every((symbol, symbolIndex) => {
512
767
  const candidate = other.symbols[symbolIndex];
513
768
  return candidate !== void 0 && symbol.name === candidate.name && symbol.kind === candidate.kind && symbol.file === candidate.file && symbol.line === candidate.line && symbol.endLine === candidate.endLine && symbol.exported === candidate.exported && symbol.signature === candidate.signature;
514
769
  });
515
770
  });
516
771
  }
772
+ function callsEqual(left, right) {
773
+ const a = left ?? [];
774
+ const b = right ?? [];
775
+ if (a.length !== b.length) return false;
776
+ return a.every((call, index) => {
777
+ const other = b[index];
778
+ return other !== void 0 && call.name === other.name && call.line === other.line && call.from === other.from;
779
+ });
780
+ }
517
781
  async function findRepoRoot(startDir, maxLevels = 12) {
518
782
  let dir = path4.resolve(startDir);
519
783
  for (let level = 0; level < maxLevels; level++) {
@@ -534,6 +798,56 @@ function symbolCount(index) {
534
798
  return index.files.reduce((n, f) => n + f.symbols.length, 0);
535
799
  }
536
800
 
801
+ // src/refgraph.ts
802
+ function buildSymbolTable(index) {
803
+ const table = /* @__PURE__ */ new Map();
804
+ for (const file of index.files) {
805
+ for (const symbol of file.symbols) {
806
+ const list = table.get(symbol.name);
807
+ if (list) list.push(symbol);
808
+ else table.set(symbol.name, [symbol]);
809
+ }
810
+ }
811
+ for (const list of table.values()) {
812
+ list.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
813
+ }
814
+ return table;
815
+ }
816
+ function callerCounts(index) {
817
+ const counts = /* @__PURE__ */ new Map();
818
+ for (const file of index.files) {
819
+ for (const call of file.calls ?? []) {
820
+ counts.set(call.name, (counts.get(call.name) ?? 0) + 1);
821
+ }
822
+ }
823
+ return counts;
824
+ }
825
+ function symbolRefs(index, symbol, limit = 200) {
826
+ const table = buildSymbolTable(index);
827
+ const defs = table.get(symbol) ?? [];
828
+ const callers = [];
829
+ const calleeFirstLine = /* @__PURE__ */ new Map();
830
+ for (const file of index.files) {
831
+ for (const call of file.calls ?? []) {
832
+ if (call.name === symbol) {
833
+ callers.push({ file: file.path, line: call.line, from: call.from });
834
+ }
835
+ if (call.from === symbol && !calleeFirstLine.has(call.name)) {
836
+ calleeFirstLine.set(call.name, call.line);
837
+ }
838
+ }
839
+ }
840
+ callers.sort(
841
+ (a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.from.localeCompare(b.from)
842
+ );
843
+ const callees = [...calleeFirstLine.entries()].map(([name2, line]) => ({ name: name2, line, targets: table.get(name2) ?? [] })).sort((a, b) => a.line - b.line || a.name.localeCompare(b.name));
844
+ return {
845
+ defs,
846
+ callers: callers.slice(0, limit),
847
+ callees: callees.slice(0, limit)
848
+ };
849
+ }
850
+
537
851
  // src/search.ts
538
852
  function matchScore(name2, query) {
539
853
  const q = query.toLowerCase();
@@ -566,8 +880,9 @@ function searchSymbols(index, filter, limit = 50) {
566
880
  hits.push({ ...sym, score });
567
881
  }
568
882
  }
883
+ const refs = callerCounts(index);
569
884
  hits.sort((a, b) => {
570
- const ab = Number(b.exported) - Number(a.exported) || b.score - a.score || a.name.localeCompare(b.name) || a.file.localeCompare(b.file);
885
+ const ab = Number(b.exported) - Number(a.exported) || b.score - a.score || (refs.get(b.name) ?? 0) - (refs.get(a.name) ?? 0) || a.name.localeCompare(b.name) || a.file.localeCompare(b.file);
571
886
  return ab;
572
887
  });
573
888
  return hits.slice(0, limit);
@@ -697,6 +1012,7 @@ function resolveImport(spec, fromPath, fileSet) {
697
1012
  return null;
698
1013
  }
699
1014
  function candidateFor(base, fileSet) {
1015
+ if (fileSet.has(base)) return base;
700
1016
  for (const ext of SUPPORTED_EXTS) {
701
1017
  if (fileSet.has(`${base}${ext}`)) return `${base}${ext}`;
702
1018
  }
@@ -726,6 +1042,76 @@ function renderRepoMap(entries, options = {}) {
726
1042
  return totalSymbols > 0 ? text : "";
727
1043
  }
728
1044
 
1045
+ // src/health.ts
1046
+ function buildModuleGraph(index) {
1047
+ const nodes = index.files.map((file) => file.path).sort((a, b) => a.localeCompare(b));
1048
+ const fileSet = new Set(nodes);
1049
+ const edges = /* @__PURE__ */ new Map();
1050
+ for (const file of index.files) {
1051
+ const targets = /* @__PURE__ */ new Set();
1052
+ for (const spec of file.imports ?? []) {
1053
+ const target = resolveImport(spec, file.path, fileSet);
1054
+ if (target && target !== file.path) targets.add(target);
1055
+ }
1056
+ edges.set(file.path, targets);
1057
+ }
1058
+ return { nodes, edges };
1059
+ }
1060
+ function findCycles(index) {
1061
+ const { nodes, edges } = buildModuleGraph(index);
1062
+ const discovery = /* @__PURE__ */ new Map();
1063
+ const low = /* @__PURE__ */ new Map();
1064
+ const onStack = /* @__PURE__ */ new Set();
1065
+ const stack = [];
1066
+ const components = [];
1067
+ let counter = 0;
1068
+ const visit = (node) => {
1069
+ discovery.set(node, counter);
1070
+ low.set(node, counter);
1071
+ counter++;
1072
+ stack.push(node);
1073
+ onStack.add(node);
1074
+ for (const next of edges.get(node) ?? []) {
1075
+ if (!discovery.has(next)) {
1076
+ visit(next);
1077
+ low.set(node, Math.min(low.get(node), low.get(next)));
1078
+ } else if (onStack.has(next)) {
1079
+ low.set(node, Math.min(low.get(node), discovery.get(next)));
1080
+ }
1081
+ }
1082
+ if (low.get(node) === discovery.get(node)) {
1083
+ const component = [];
1084
+ for (; ; ) {
1085
+ const member = stack.pop();
1086
+ onStack.delete(member);
1087
+ component.push(member);
1088
+ if (member === node) break;
1089
+ }
1090
+ if (component.length > 1) components.push(component.sort());
1091
+ }
1092
+ };
1093
+ for (const node of nodes) {
1094
+ if (!discovery.has(node)) visit(node);
1095
+ }
1096
+ components.sort((a, b) => a[0].localeCompare(b[0]));
1097
+ return components;
1098
+ }
1099
+ var ENTRY_RE = /(^|\/)(index|main|cli|app|server|mod|lib|__init__)\.[A-Za-z0-9]+$|(^|\/)(bin|cmd|scripts)\/|\.config\.[A-Za-z0-9]+$/;
1100
+ var TEST_RE = /(^|\/)(tests?|__tests__)(\/|$)|\.(?:spec|test)\.[cm]?[jt]sx?$|(^|\/)(?:test_[^/]+\.py|[^/]+_test\.(?:py|go))$/;
1101
+ function findOrphanModules(index) {
1102
+ const { nodes, edges } = buildModuleGraph(index);
1103
+ const incoming = /* @__PURE__ */ new Map();
1104
+ for (const targets of edges.values()) {
1105
+ for (const target of targets) incoming.set(target, (incoming.get(target) ?? 0) + 1);
1106
+ }
1107
+ const withSymbols = new Set(
1108
+ index.files.filter((file) => file.symbols.length > 0).map((file) => file.path)
1109
+ );
1110
+ return nodes.filter(
1111
+ (path7) => withSymbols.has(path7) && (incoming.get(path7) ?? 0) === 0 && (edges.get(path7)?.size ?? 0) === 0 && !ENTRY_RE.test(path7) && !TEST_RE.test(path7)
1112
+ );
1113
+ }
1114
+
729
1115
  // src/tools.ts
730
1116
  import { defineTool } from "@deepseek-ai/dsh-tools";
731
1117
  import path6 from "path";
@@ -736,7 +1122,8 @@ var DEFAULTS = {
736
1122
  mapTopFiles: 24,
737
1123
  mapMaxChars: 3200,
738
1124
  mapTtlMs: 6e4,
739
- autoInject: true
1125
+ autoInject: true,
1126
+ codeHealth: false
740
1127
  };
741
1128
  var state = { current: { ...DEFAULTS } };
742
1129
  function applyConfig(partial) {
@@ -825,6 +1212,38 @@ async function resolveRoot(arg, exec) {
825
1212
  if (!root) throw new Error(`no git repository found from ${path6.resolve(base)}`);
826
1213
  return root;
827
1214
  }
1215
+ function hitsToSearchMeta(hits) {
1216
+ const byFile = /* @__PURE__ */ new Map();
1217
+ for (const raw of hits) {
1218
+ const hit = raw;
1219
+ if (!hit.file || typeof hit.line !== "number") continue;
1220
+ const label = `${hit.kind ?? ""} ${hit.signature || hit.name || ""}`.trim();
1221
+ const list = byFile.get(hit.file) ?? [];
1222
+ list.push({ lineNumber: hit.line, line: label });
1223
+ byFile.set(hit.file, list);
1224
+ }
1225
+ return {
1226
+ shape: "matches",
1227
+ files: [...byFile.entries()].map(([path7, matches]) => ({ path: path7, matches })),
1228
+ truncated: false,
1229
+ total: hits.length
1230
+ };
1231
+ }
1232
+ function searchCardFromMeta(meta) {
1233
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0;
1234
+ const record = meta;
1235
+ if (record.shape !== "matches" || !Array.isArray(record.files)) return void 0;
1236
+ const files = record.files.filter(
1237
+ (entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry)
1238
+ );
1239
+ return {
1240
+ card: "search",
1241
+ shape: "matches",
1242
+ files,
1243
+ truncated: record.truncated === true,
1244
+ total: typeof record.total === "number" ? record.total : 0
1245
+ };
1246
+ }
828
1247
  var tools = [
829
1248
  defineTool({
830
1249
  name: "code_index",
@@ -897,8 +1316,16 @@ var tools = [
897
1316
  render: (_args, value) => value.map((v) => ({
898
1317
  type: "text",
899
1318
  text: renderHit(v)
900
- }))
1319
+ })),
1320
+ presentationMeta: (_args, value) => hitsToSearchMeta(value)
901
1321
  },
1322
+ presentCall: (args) => ({
1323
+ card: "generic",
1324
+ title: args.kind ? `List ${args.kind} symbols` : "List symbols",
1325
+ kind: "search",
1326
+ rawInput: { query: args.query, file: args.file, kind: args.kind }
1327
+ }),
1328
+ presentResult: (_args, result) => searchCardFromMeta(result.meta),
902
1329
  async execute(args, exec) {
903
1330
  const root = await resolveRoot(args.repoRoot, exec);
904
1331
  const index = await getIndex(root);
@@ -933,8 +1360,16 @@ var tools = [
933
1360
  render: (_args, value) => value.map((v) => ({
934
1361
  type: "text",
935
1362
  text: renderHit(v)
936
- }))
1363
+ })),
1364
+ presentationMeta: (_args, value) => hitsToSearchMeta(value)
937
1365
  },
1366
+ presentCall: (args) => ({
1367
+ card: "generic",
1368
+ title: `Search "${args.query}"`,
1369
+ kind: "search",
1370
+ rawInput: { query: args.query }
1371
+ }),
1372
+ presentResult: (_args, result) => searchCardFromMeta(result.meta),
938
1373
  async execute(args, exec) {
939
1374
  const root = await resolveRoot(args.repoRoot, exec);
940
1375
  const index = await getIndex(root);
@@ -970,9 +1405,141 @@ var tools = [
970
1405
  return `code_index: ${error.message ?? String(error)}`;
971
1406
  }
972
1407
  }
1408
+ }),
1409
+ defineTool({
1410
+ name: "code_refs",
1411
+ description: "Trace a symbol through the call graph: who calls it (callers) and what it calls (callees), resolved to in-repo definitions with file:line. Complements code_search \u2014 search finds a definition, code_refs shows how it is used. Resolution is name-based, so like-named symbols are reported as candidate definitions.",
1412
+ parameters: {
1413
+ symbol: {
1414
+ type: "string",
1415
+ required: true,
1416
+ description: "Symbol name to trace (case-sensitive)."
1417
+ },
1418
+ direction: {
1419
+ type: "string",
1420
+ enum: ["callers", "callees", "both"],
1421
+ description: 'Which side of the call graph to return (default "both").'
1422
+ },
1423
+ limit: {
1424
+ type: "number",
1425
+ description: "Max rows per side (default 50)."
1426
+ },
1427
+ repoRoot: {
1428
+ type: "string",
1429
+ description: "Optional absolute repo path; defaults to the session workspace root."
1430
+ }
1431
+ },
1432
+ output: {
1433
+ schema: { type: "string" },
1434
+ render: (_args, value) => [{ type: "text", text: value }]
1435
+ },
1436
+ presentCall: (args) => ({
1437
+ card: "generic",
1438
+ title: `Refs ${args.symbol}`,
1439
+ kind: "search",
1440
+ rawInput: { symbol: args.symbol, direction: args.direction }
1441
+ }),
1442
+ async execute(args, exec) {
1443
+ try {
1444
+ const root = await resolveRoot(args.repoRoot, exec);
1445
+ const index = await getIndex(root);
1446
+ const refs = symbolRefs(index, args.symbol, args.limit ?? 50);
1447
+ const direction = args.direction ?? "both";
1448
+ const lines = [];
1449
+ if (refs.defs.length > 0) {
1450
+ lines.push("definitions:");
1451
+ for (const def of refs.defs) {
1452
+ lines.push(` ${def.kind} ${def.signature || def.name} \u2014 ${def.file}:${def.line}`);
1453
+ }
1454
+ } else {
1455
+ lines.push(`no definition of "${args.symbol}" in this repo`);
1456
+ }
1457
+ if (direction === "callers" || direction === "both") {
1458
+ lines.push("", `callers (${refs.callers.length}):`);
1459
+ if (refs.callers.length === 0) lines.push(" (none)");
1460
+ for (const caller of refs.callers) {
1461
+ lines.push(` ${caller.file}:${caller.line}${caller.from ? ` in ${caller.from}` : ""}`);
1462
+ }
1463
+ }
1464
+ if (direction === "callees" || direction === "both") {
1465
+ lines.push("", `callees (${refs.callees.length}):`);
1466
+ if (refs.callees.length === 0) lines.push(" (none)");
1467
+ for (const callee of refs.callees) {
1468
+ const target = callee.targets[0];
1469
+ lines.push(
1470
+ ` ${callee.name} :${callee.line}${target ? ` \u2192 ${target.file}:${target.line}` : " (external)"}`
1471
+ );
1472
+ }
1473
+ }
1474
+ return lines.join("\n");
1475
+ } catch (error) {
1476
+ return `code_refs: ${error.message ?? String(error)}`;
1477
+ }
1478
+ }
1479
+ }),
1480
+ defineTool({
1481
+ name: "code_health",
1482
+ description: "Report structural health from the import graph: circular dependencies (import cycles) and orphan modules (symbol-bearing files that nothing imports and that import nothing, excluding entry points and tests).",
1483
+ parameters: {
1484
+ limit: {
1485
+ type: "number",
1486
+ description: "Max cycles and orphan rows to list (default 50)."
1487
+ },
1488
+ repoRoot: {
1489
+ type: "string",
1490
+ description: "Optional absolute repo path; defaults to the session workspace root."
1491
+ }
1492
+ },
1493
+ output: {
1494
+ schema: { type: "string" },
1495
+ render: (_args, value) => [{ type: "text", text: value }]
1496
+ },
1497
+ presentCall: () => ({ card: "generic", title: "Code health", kind: "read" }),
1498
+ async execute(args, exec) {
1499
+ try {
1500
+ const root = await resolveRoot(args.repoRoot, exec);
1501
+ const index = await getIndex(root);
1502
+ const limit = args.limit ?? 50;
1503
+ const cycles = findCycles(index);
1504
+ const orphans = findOrphanModules(index);
1505
+ const lines = [`repo health \u2014 ${index.files.length} files`];
1506
+ lines.push("", `circular dependencies (${cycles.length}):`);
1507
+ if (cycles.length === 0) lines.push(" (none)");
1508
+ for (const cycle of cycles.slice(0, limit)) lines.push(` ${cycle.join(" \u2192 ")}`);
1509
+ lines.push("", `orphan modules (${orphans.length}):`);
1510
+ if (orphans.length === 0) lines.push(" (none)");
1511
+ for (const orphan of orphans.slice(0, limit)) lines.push(` ${orphan}`);
1512
+ return lines.join("\n");
1513
+ } catch (error) {
1514
+ return `code_health: ${error.message ?? String(error)}`;
1515
+ }
1516
+ }
973
1517
  })
974
1518
  ];
975
1519
 
1520
+ // src/settings.ts
1521
+ import Schema from "@deepseek-ai/schemastery";
1522
+ var SETTINGS_NAMESPACE = "code-index";
1523
+ var settingsSchema = Schema.object({
1524
+ excludeDirs: Schema.array(Schema.string()).default([]),
1525
+ mapTopFiles: Schema.number().default(24),
1526
+ mapMaxChars: Schema.number().default(3200),
1527
+ mapTtlMs: Schema.number().default(6e4),
1528
+ autoInject: Schema.boolean().default(true),
1529
+ codeHealth: Schema.boolean().default(false)
1530
+ });
1531
+ function registerSettings(getService, pluginConfig, onResolved) {
1532
+ const service = getService("settings");
1533
+ if (!service) return;
1534
+ const scope = service.register(SETTINGS_NAMESPACE, settingsSchema, {
1535
+ base: pluginConfig ?? {}
1536
+ });
1537
+ onResolved(scope.get());
1538
+ scope.watch((next) => {
1539
+ onResolved(next);
1540
+ });
1541
+ }
1542
+
976
1543
  // src/index.ts
977
1544
  var name = "dsh-code-index";
978
1545
  var inject = ["tools", "systemPrompt"];
@@ -982,7 +1549,17 @@ function apply(ctx, pluginConfig) {
982
1549
  ctx.effect(() => {
983
1550
  const disposers = [];
984
1551
  console.log("[dsh-code-index] plugin loaded");
1552
+ if (ctx.inject) {
1553
+ ctx.inject(["settings"], (settingsCtx) => {
1554
+ registerSettings(
1555
+ (name2) => settingsCtx.get(name2),
1556
+ pluginConfig,
1557
+ (resolved) => applyConfig(resolved)
1558
+ );
1559
+ });
1560
+ }
985
1561
  for (const tool of tools) {
1562
+ if (tool.name === "code_health" && !getConfig().codeHealth) continue;
986
1563
  disposers.push(ctx.tools.register(tool));
987
1564
  console.log(`[dsh-code-index] registered tool: ${tool.name}`);
988
1565
  }
@@ -1048,9 +1625,14 @@ export {
1048
1625
  apply,
1049
1626
  buildIndex,
1050
1627
  buildIndexWithCache,
1628
+ buildModuleGraph,
1629
+ buildSymbolTable,
1630
+ callerCounts,
1051
1631
  defaultCachePath,
1052
1632
  extractAll,
1053
1633
  extractSymbols,
1634
+ findCycles,
1635
+ findOrphanModules,
1054
1636
  findRepoRoot,
1055
1637
  inject,
1056
1638
  languageForFile,
@@ -1065,6 +1647,7 @@ export {
1065
1647
  scoreFile,
1066
1648
  searchSymbols,
1067
1649
  symbolCount,
1650
+ symbolRefs,
1068
1651
  tools
1069
1652
  };
1070
1653
  //# sourceMappingURL=index.js.map