dsh-code-index 0.4.0 → 0.6.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.
package/dist/index.js CHANGED
@@ -162,6 +162,66 @@ var IMPORT_QUERIES = {
162
162
  (preproc_include) @import
163
163
  `
164
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))
201
+ `
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
+ };
165
225
  var parserPromise = null;
166
226
  async function getParser() {
167
227
  if (!parserPromise) {
@@ -184,20 +244,27 @@ function getLanguage(id) {
184
244
  const lang = await Language.load(bytes);
185
245
  new Query(lang, QUERIES[id]).delete();
186
246
  new Query(lang, IMPORT_QUERIES[id]).delete();
247
+ new Query(lang, CALL_QUERIES[id]).delete();
187
248
  return lang;
188
249
  });
189
250
  languageCache.set(id, entry);
190
251
  }
191
252
  return entry;
192
253
  }
193
- async function extractAll(code, id) {
254
+ function encodeSymbolComponent(value) {
255
+ return encodeURIComponent(value).replace(
256
+ /[!'()*~]/g,
257
+ (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`
258
+ );
259
+ }
260
+ async function extractAll(code, id, file = "") {
194
261
  const lang = await getLanguage(id);
195
262
  const parser = await getParser();
196
263
  parser.setLanguage(lang);
197
264
  const tree = parser.parse(code);
198
265
  if (!tree) throw new Error(`tree-sitter parse returned null for a ${id} source`);
199
266
  try {
200
- const symbols = [];
267
+ const drafts = [];
201
268
  const symbolQuery = new Query(lang, QUERIES[id]);
202
269
  try {
203
270
  const captures = symbolQuery.captures(tree.rootNode);
@@ -208,43 +275,297 @@ async function extractAll(code, id) {
208
275
  if (def.kind === "variable" && !isModuleLevelVariable(id, node)) continue;
209
276
  const name2 = nameOf(id, node);
210
277
  if (!name2) continue;
211
- symbols.push({
212
- name: name2,
213
- kind: kindFor(id, node, def.kind),
214
- file: "",
215
- // set by the caller (extractor is file-agnostic)
216
- line: node.startPosition.row + 1,
217
- endLine: node.endPosition.row + 1,
218
- exported: isExported(id, node),
219
- signature: signatureFor(id, node)
278
+ drafts.push({
279
+ symbol: {
280
+ name: name2,
281
+ kind: kindFor(id, node, def.kind),
282
+ line: node.startPosition.row + 1,
283
+ endLine: node.endPosition.row + 1,
284
+ exported: isExported(id, node),
285
+ signature: signatureFor(id, node)
286
+ },
287
+ node
220
288
  });
221
289
  }
222
290
  } finally {
223
291
  symbolQuery.delete();
224
292
  }
225
- const imports = [];
293
+ const dedupedDrafts = dedupeDrafts(drafts);
294
+ const symbols = assignSymbolIds(file, dedupedDrafts, id);
295
+ const symbolByNodeId = /* @__PURE__ */ new Map();
296
+ dedupedDrafts.forEach((draft, index) => symbolByNodeId.set(draft.node.id, symbols[index]));
297
+ const importDetails = [];
226
298
  const importQuery = new Query(lang, IMPORT_QUERIES[id]);
227
299
  try {
228
300
  for (const cap of importQuery.captures(tree.rootNode)) {
229
- const spec = specifierOf(id, cap.node);
230
- if (spec) imports.push(spec);
301
+ for (const info of importInfoFor(id, cap.node)) importDetails.push(info);
231
302
  }
232
303
  } finally {
233
304
  importQuery.delete();
234
305
  }
235
- symbols.sort((a, b) => a.line - b.line);
236
- const seen = /* @__PURE__ */ new Set();
237
- const deduped = symbols.filter((s) => {
238
- const key = `${s.kind}|${s.name}|${s.line}`;
239
- if (seen.has(key)) return false;
240
- seen.add(key);
241
- return true;
242
- });
243
- return { symbols: deduped, imports };
306
+ const imports = importDetails.map((info) => info.specifier);
307
+ const calls = [];
308
+ const callQuery = new Query(lang, CALL_QUERIES[id]);
309
+ try {
310
+ const seen = /* @__PURE__ */ new Set();
311
+ for (const cap of callQuery.captures(tree.rootNode)) {
312
+ const name2 = cap.node.text.trim();
313
+ if (!name2) continue;
314
+ const line = cap.node.startPosition.row + 1;
315
+ const callable = enclosingCallableNode(id, cap.node);
316
+ const from = callable ? callableNameOf(id, callable) : "";
317
+ const fromId = callable ? nearestDraftId(callable, symbolByNodeId) : null;
318
+ const key = `${from}|${name2}|${line}`;
319
+ if (seen.has(key)) continue;
320
+ seen.add(key);
321
+ calls.push({ name: name2, line, from, fromId, qualifier: qualifierOf(cap.node) });
322
+ }
323
+ } finally {
324
+ callQuery.delete();
325
+ }
326
+ calls.sort((a, b) => a.line - b.line || a.name.localeCompare(b.name));
327
+ symbols.sort(
328
+ (a, b) => a.line - b.line || a.endLine - b.endLine || a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name)
329
+ );
330
+ return { symbols, imports, importDetails, calls };
244
331
  } finally {
245
332
  tree.delete();
246
333
  }
247
334
  }
335
+ function dedupeDrafts(drafts) {
336
+ const sorted = [...drafts].sort(
337
+ (a, b) => a.symbol.line - b.symbol.line || a.node.startIndex - b.node.startIndex
338
+ );
339
+ const seen = /* @__PURE__ */ new Set();
340
+ const out = [];
341
+ for (const draft of sorted) {
342
+ if (seen.has(draft.node.id)) continue;
343
+ seen.add(draft.node.id);
344
+ out.push(draft);
345
+ }
346
+ return out;
347
+ }
348
+ var MATCHABLE_OWNER_KINDS = /* @__PURE__ */ new Set([
349
+ "class",
350
+ "interface",
351
+ "trait",
352
+ "module",
353
+ "namespace",
354
+ "impl"
355
+ ]);
356
+ function scopeKindOf(language, kind) {
357
+ switch (kind) {
358
+ case "class":
359
+ return "class";
360
+ case "interface":
361
+ return language === "rust" ? "trait" : "interface";
362
+ case "function":
363
+ return "function";
364
+ case "method":
365
+ return "method";
366
+ case "module":
367
+ return language === "cpp" ? "namespace" : "module";
368
+ default:
369
+ return "owner";
370
+ }
371
+ }
372
+ function segmentOf(entry) {
373
+ return `${entry.kind}:${encodeSymbolComponent(entry.name)}@${entry.ordinal}`;
374
+ }
375
+ function assignSymbolIds(file, drafts, language) {
376
+ const entries = drafts.map((draft, index) => ({
377
+ kind: scopeKindOf(language, draft.symbol.kind),
378
+ name: draft.symbol.name,
379
+ startIndex: draft.node.startIndex,
380
+ endIndex: draft.node.endIndex,
381
+ parent: -1,
382
+ ordinal: 1,
383
+ scopeKey: "",
384
+ draftIndex: index
385
+ }));
386
+ const entryByNodeId = /* @__PURE__ */ new Map();
387
+ drafts.forEach((draft, index) => entryByNodeId.set(draft.node.id, index));
388
+ const entriesByName = /* @__PURE__ */ new Map();
389
+ entries.forEach((entry, index) => {
390
+ const list = entriesByName.get(entry.name);
391
+ if (list) list.push(index);
392
+ else entriesByName.set(entry.name, [index]);
393
+ });
394
+ const syntheticByKey = /* @__PURE__ */ new Map();
395
+ const resolveOwner = (desc, parent, selfIndex) => {
396
+ const matches = entriesByName.get(desc.name);
397
+ const typed = matches?.find((index2) => {
398
+ if (index2 === selfIndex) return false;
399
+ const entry = entries[index2];
400
+ if (!MATCHABLE_OWNER_KINDS.has(entry.kind)) return false;
401
+ if (entry.parent !== parent) return false;
402
+ return desc.kind === "owner" || entry.kind === desc.kind;
403
+ });
404
+ if (typed !== void 0) return typed;
405
+ const key = `${parent}\0${desc.kind}\0${desc.name}`;
406
+ const existing = syntheticByKey.get(key);
407
+ if (existing !== void 0) return existing;
408
+ const index = entries.length;
409
+ entries.push({
410
+ kind: desc.kind,
411
+ name: desc.name,
412
+ startIndex: Number.MAX_SAFE_INTEGER,
413
+ endIndex: Number.MAX_SAFE_INTEGER,
414
+ parent,
415
+ ordinal: 1,
416
+ scopeKey: "",
417
+ draftIndex: -1
418
+ });
419
+ syntheticByKey.set(key, index);
420
+ return index;
421
+ };
422
+ drafts.forEach((draft, index) => {
423
+ let parent = -1;
424
+ for (let node = draft.node.parent; node; node = node.parent) {
425
+ const owner = entryByNodeId.get(node.id);
426
+ if (owner !== void 0) {
427
+ parent = owner;
428
+ break;
429
+ }
430
+ }
431
+ for (const desc of ownerScopeOf(language, draft.node)) {
432
+ parent = resolveOwner(desc, parent, index);
433
+ }
434
+ entries[index].parent = parent;
435
+ });
436
+ assignOrdinals(entries);
437
+ return drafts.map((draft, index) => {
438
+ const entry = entries[index];
439
+ const chain = [];
440
+ for (let node = entries[entry.parent]; node; node = entries[node.parent]) {
441
+ chain.push(node);
442
+ }
443
+ chain.reverse();
444
+ const scope = chain.map((node) => ({
445
+ kind: node.kind,
446
+ name: node.name,
447
+ ordinal: node.ordinal
448
+ }));
449
+ const segments = [...chain.map(segmentOf), segmentOf(entry)];
450
+ return {
451
+ ...draft.symbol,
452
+ file,
453
+ id: `sym:v1:${encodeSymbolComponent(file)}#${segments.join("/")}`,
454
+ scope,
455
+ ordinal: entry.ordinal
456
+ };
457
+ });
458
+ }
459
+ function assignOrdinals(entries) {
460
+ const depthCache = /* @__PURE__ */ new Map();
461
+ const depthOf = (index) => {
462
+ const cached = depthCache.get(index);
463
+ if (cached !== void 0) return cached;
464
+ const parent = entries[index].parent;
465
+ const value = parent >= 0 ? depthOf(parent) + 1 : 0;
466
+ depthCache.set(index, value);
467
+ return value;
468
+ };
469
+ const byDepth = /* @__PURE__ */ new Map();
470
+ let maxDepth = 0;
471
+ for (let index = 0; index < entries.length; index++) {
472
+ const depth = depthOf(index);
473
+ maxDepth = Math.max(maxDepth, depth);
474
+ const list = byDepth.get(depth);
475
+ if (list) list.push(index);
476
+ else byDepth.set(depth, [index]);
477
+ }
478
+ for (let depth = 0; depth <= maxDepth; depth++) {
479
+ const groups = /* @__PURE__ */ new Map();
480
+ for (const index of byDepth.get(depth) ?? []) {
481
+ const entry = entries[index];
482
+ const parentKey = entry.parent >= 0 ? entries[entry.parent].scopeKey : "";
483
+ const key = `${parentKey}\0${entry.kind}\0${entry.name}`;
484
+ const list = groups.get(key);
485
+ if (list) list.push(index);
486
+ else groups.set(key, [index]);
487
+ }
488
+ for (const group of groups.values()) {
489
+ group.sort((a, b) => {
490
+ const left = entries[a];
491
+ const right = entries[b];
492
+ return left.startIndex - right.startIndex || left.endIndex - right.endIndex || left.kind.localeCompare(right.kind) || left.name.localeCompare(right.name);
493
+ });
494
+ group.forEach((index, position) => {
495
+ const entry = entries[index];
496
+ entry.ordinal = position + 1;
497
+ const segment = segmentOf(entry);
498
+ entry.scopeKey = entry.parent >= 0 ? `${entries[entry.parent].scopeKey}/${segment}` : segment;
499
+ });
500
+ }
501
+ }
502
+ }
503
+ function ownerScopeOf(language, node) {
504
+ if (language === "go") return goReceiverOwners(node);
505
+ if (language === "rust") return rustImplOwners(node);
506
+ if (language === "cpp" || language === "c") return cppQualifiedOwners(node);
507
+ return [];
508
+ }
509
+ function goReceiverOwners(node) {
510
+ if (node.type !== "method_declaration") return [];
511
+ const receiver = node.childForFieldName("receiver");
512
+ const type = receiver ? findDescendant(receiver, "type_identifier") : null;
513
+ return type ? [{ kind: "class", name: type.text.trim() }] : [];
514
+ }
515
+ function rustImplOwners(node) {
516
+ if (node.type !== "function_item" && node.type !== "function_signature_item") return [];
517
+ let impl = null;
518
+ for (let parent = node.parent; parent; parent = parent.parent) {
519
+ if (parent.type === "impl_item") {
520
+ impl = parent;
521
+ break;
522
+ }
523
+ if (parent.type === "trait_item" || CALLABLE_NODES.rust.has(parent.type)) return [];
524
+ }
525
+ if (!impl) return [];
526
+ const type = impl.childForFieldName("type");
527
+ const targetName = type ? findDescendant(type, "type_identifier")?.text.trim() : void 0;
528
+ const traitName = impl.childForFieldName("trait")?.text.trim();
529
+ const name2 = targetName && traitName ? `${traitName} for ${targetName}` : targetName ?? traitName;
530
+ return name2 ? [{ kind: "impl", name: name2 }] : [];
531
+ }
532
+ function cppQualifiedOwners(node) {
533
+ const declarator = cppDeclaratorQid(node);
534
+ if (!declarator) return [];
535
+ return cppQualifiedSegments(declarator).slice(0, -1).map((name2) => ({ kind: "owner", name: name2 }));
536
+ }
537
+ function cppDeclaratorQid(node) {
538
+ let current = node.childForFieldName?.("declarator");
539
+ while (current) {
540
+ if (current.type === "qualified_identifier") return current;
541
+ if (!current.type.includes("declarator")) return null;
542
+ current = current.childForFieldName?.("declarator") ?? current.namedChildren[0] ?? null;
543
+ }
544
+ return null;
545
+ }
546
+ function cppQualifiedSegments(qualified) {
547
+ const scope = qualified.childForFieldName("scope");
548
+ const name2 = qualified.childForFieldName("name");
549
+ const head = scope ? [scope.text.trim()] : [];
550
+ const tail = name2?.type === "qualified_identifier" ? cppQualifiedSegments(name2) : [name2?.text.trim() ?? ""];
551
+ return [...head, ...tail].filter(Boolean);
552
+ }
553
+ function findDescendant(node, type) {
554
+ if (node.type === type) return node;
555
+ for (const child of node.namedChildren) {
556
+ if (!child) continue;
557
+ const nested = findDescendant(child, type);
558
+ if (nested) return nested;
559
+ }
560
+ return null;
561
+ }
562
+ function nearestDraftId(node, symbols) {
563
+ for (let current = node; current; current = current.parent) {
564
+ const symbol = symbols.get(current.id);
565
+ if (symbol) return symbol.id;
566
+ }
567
+ return null;
568
+ }
248
569
  async function extractSymbols(code, id) {
249
570
  return (await extractAll(code, id)).symbols;
250
571
  }
@@ -272,7 +593,7 @@ function cppNameOf(node) {
272
593
  continue;
273
594
  }
274
595
  if (current.type === "qualified_identifier") {
275
- return current.childForFieldName?.("name")?.text.trim() ?? current.text.trim();
596
+ return cppQualifiedSegments(current).at(-1) ?? "";
276
597
  }
277
598
  if (current.type === "identifier" || current.type === "field_identifier" || current.type === "type_identifier") {
278
599
  return current.text.trim();
@@ -282,29 +603,251 @@ function cppNameOf(node) {
282
603
  }
283
604
  return "";
284
605
  }
285
- function specifierOf(id, node) {
606
+ function enclosingCallableNode(id, node) {
607
+ const callable = CALLABLE_NODES[id];
608
+ for (let parent = node.parent; parent; parent = parent.parent) {
609
+ if (callable.has(parent.type)) return parent;
610
+ }
611
+ return null;
612
+ }
613
+ function callableNameOf(id, callable) {
614
+ const named = nameOf(id, callable);
615
+ if (named) return named;
616
+ const declaration = callable.parent;
617
+ if (declaration?.type === "variable_declarator") {
618
+ return declaration.childForFieldName?.("name")?.text.trim() ?? "";
619
+ }
620
+ return "";
621
+ }
622
+ function qualifierOf(node) {
623
+ const parent = node.parent;
624
+ if (!parent) return null;
625
+ let receiver;
626
+ switch (parent.type) {
627
+ case "member_expression":
628
+ case "method_invocation":
629
+ case "attribute":
630
+ receiver = parent.childForFieldName("object");
631
+ break;
632
+ case "selector_expression":
633
+ receiver = parent.childForFieldName("operand");
634
+ break;
635
+ case "field_expression":
636
+ receiver = parent.childForFieldName("value") ?? parent.childForFieldName("argument");
637
+ break;
638
+ case "qualified_identifier":
639
+ case "scoped_identifier":
640
+ receiver = parent.childForFieldName("scope") ?? parent.childForFieldName("path");
641
+ break;
642
+ default:
643
+ return null;
644
+ }
645
+ const text = receiver?.text.replace(/\s+/g, " ").trim();
646
+ return text ? text : null;
647
+ }
648
+ function importInfoFor(id, node) {
649
+ const line = node.startPosition.row + 1;
286
650
  switch (node.type) {
287
651
  case "import_statement":
288
- if (id === "python") return dottedToPath(node.childForFieldName("name")?.text);
289
- return stripQuotes(node.childForFieldName("source")?.text);
652
+ return id === "python" ? pythonModuleImports(node, line) : tsJsImports(node, line);
290
653
  case "export_statement":
291
- return stripQuotes(node.childForFieldName("source")?.text);
292
- case "import_from_statement": {
293
- const raw = node.childForFieldName("module_name")?.text;
294
- if (raw == null) return null;
295
- return raw.startsWith(".") ? pythonRelative(raw) : dottedToPath(raw);
296
- }
654
+ return tsJsReexport(node, line);
655
+ case "import_from_statement":
656
+ return pythonFromImport(node, line);
297
657
  case "import_spec":
298
- return stripQuotes(node.childForFieldName("path")?.text);
658
+ return goImport(node, line);
299
659
  case "use_declaration":
300
- return rustUsePath(node.childForFieldName("argument")?.text);
660
+ return rustUse(node, line);
301
661
  case "import_declaration":
302
- return dottedToPath(node.namedChildren[0]?.text);
303
- case "preproc_include":
304
- return stripQuotes(node.childForFieldName("path")?.text);
662
+ return javaImport(node, line);
663
+ case "preproc_include": {
664
+ const specifier = stripQuotes(node.childForFieldName("path")?.text);
665
+ return specifier ? [{ specifier, line, kind: "include", bindings: [] }] : [];
666
+ }
305
667
  default:
306
- return null;
668
+ return [];
669
+ }
670
+ }
671
+ function tsJsImports(node, line) {
672
+ const specifier = stripQuotes(node.childForFieldName("source")?.text);
673
+ if (!specifier) return [];
674
+ return [{ specifier, line, kind: "import", bindings: importClauseBindings(node) }];
675
+ }
676
+ function importClauseBindings(node) {
677
+ const bindings = [];
678
+ const clause = node.namedChildren.find((child) => child?.type === "import_clause");
679
+ if (!clause) return bindings;
680
+ for (const child of clause.namedChildren) {
681
+ if (!child) continue;
682
+ if (child.type === "identifier") {
683
+ bindings.push({ imported: "default", local: child.text.trim(), kind: "default" });
684
+ } else if (child.type === "namespace_import") {
685
+ const local = child.namedChildren.find((c) => c?.type === "identifier")?.text.trim();
686
+ if (local) bindings.push({ imported: "*", local, kind: "namespace" });
687
+ } else if (child.type === "named_imports") {
688
+ for (const spec of child.namedChildren) {
689
+ if (spec?.type !== "import_specifier") continue;
690
+ const imported = spec.childForFieldName("name")?.text.trim();
691
+ if (!imported) continue;
692
+ const alias = spec.childForFieldName("alias")?.text.trim();
693
+ bindings.push({ imported, local: alias ?? imported, kind: "named" });
694
+ }
695
+ }
307
696
  }
697
+ return bindings;
698
+ }
699
+ function tsJsReexport(node, line) {
700
+ const specifier = stripQuotes(node.childForFieldName("source")?.text);
701
+ if (!specifier) return [];
702
+ const bindings = [];
703
+ const clause = node.namedChildren.find((child) => child?.type === "export_clause");
704
+ for (const spec of clause?.namedChildren ?? []) {
705
+ if (spec?.type !== "export_specifier") continue;
706
+ const imported = spec.childForFieldName("name")?.text.trim();
707
+ if (!imported) continue;
708
+ const alias = spec.childForFieldName("alias")?.text.trim();
709
+ bindings.push({ imported, local: alias ?? imported, kind: "named" });
710
+ }
711
+ const namespaceExport = node.namedChildren.find((child) => child?.type === "namespace_export");
712
+ const local = namespaceExport?.namedChildren.find((c) => c?.type === "identifier")?.text.trim();
713
+ if (local) bindings.push({ imported: "*", local, kind: "namespace" });
714
+ return [{ specifier, line, kind: "reexport", bindings }];
715
+ }
716
+ function pythonModuleImports(node, line) {
717
+ const out = [];
718
+ for (const nameNode of node.childrenForFieldName("name")) {
719
+ if (!nameNode) continue;
720
+ if (nameNode.type === "aliased_import") {
721
+ const moduleName = nameNode.childForFieldName("name")?.text.trim();
722
+ const specifier = dottedToPath(moduleName);
723
+ if (!specifier) continue;
724
+ const alias = nameNode.childForFieldName("alias")?.text.trim();
725
+ out.push({
726
+ specifier,
727
+ line,
728
+ kind: "import",
729
+ bindings: [
730
+ { imported: moduleName ?? null, local: alias ?? moduleName ?? "", kind: "namespace" }
731
+ ]
732
+ });
733
+ } else if (nameNode.type === "dotted_name") {
734
+ const moduleName = nameNode.text.trim();
735
+ const specifier = dottedToPath(moduleName);
736
+ if (!specifier) continue;
737
+ out.push({
738
+ specifier,
739
+ line,
740
+ kind: "import",
741
+ bindings: [{ imported: null, local: moduleName.split(".")[0] ?? moduleName, kind: "package" }]
742
+ });
743
+ }
744
+ }
745
+ return out;
746
+ }
747
+ function pythonFromImport(node, line) {
748
+ const raw = node.childForFieldName("module_name")?.text;
749
+ if (raw == null) return [];
750
+ const specifier = raw.startsWith(".") ? pythonRelative(raw) : dottedToPath(raw);
751
+ if (!specifier) return [];
752
+ const moduleNode = node.childForFieldName("module_name");
753
+ const bindings = [];
754
+ for (const nameNode of node.namedChildren) {
755
+ if (!nameNode || nameNode.id === moduleNode?.id) continue;
756
+ if (nameNode.type === "aliased_import") {
757
+ const imported = nameNode.childForFieldName("name")?.text.trim();
758
+ if (!imported) continue;
759
+ const alias = nameNode.childForFieldName("alias")?.text.trim();
760
+ bindings.push({ imported, local: alias ?? lastDotSegment(imported), kind: "named" });
761
+ } else if (nameNode.type === "dotted_name") {
762
+ const imported = nameNode.text.trim();
763
+ bindings.push({ imported, local: lastDotSegment(imported), kind: "named" });
764
+ } else if (nameNode.type === "wildcard_import") {
765
+ bindings.push({ imported: "*", local: "*", kind: "glob" });
766
+ }
767
+ }
768
+ return [{ specifier, line, kind: "import", bindings }];
769
+ }
770
+ function goImport(node, line) {
771
+ const specifier = stripQuotes(node.childForFieldName("path")?.text);
772
+ if (!specifier) return [];
773
+ const nameNode = node.childForFieldName("name");
774
+ let binding;
775
+ if (!nameNode) {
776
+ binding = { imported: null, local: lastSlashSegment(specifier), kind: "package" };
777
+ } else if (nameNode.type === "dot") {
778
+ binding = { imported: "*", local: ".", kind: "glob" };
779
+ } else {
780
+ binding = { imported: null, local: nameNode.text.trim(), kind: "package" };
781
+ }
782
+ return [{ specifier, line, kind: "import", bindings: [binding] }];
783
+ }
784
+ function rustUse(node, line) {
785
+ const argument = node.childForFieldName("argument");
786
+ if (!argument) return [];
787
+ const base = argument.type === "use_as_clause" ? argument.childForFieldName("path") : argument;
788
+ const specifier = rustUsePath(base?.text ?? argument.text);
789
+ if (!specifier) return [];
790
+ return [{ specifier, line, kind: "use", bindings: rustBindings(argument) }];
791
+ }
792
+ function rustBindings(node) {
793
+ switch (node.type) {
794
+ case "scoped_identifier": {
795
+ const imported = node.childForFieldName("name")?.text.trim();
796
+ return imported ? [{ imported, local: imported, kind: "named" }] : [];
797
+ }
798
+ case "identifier": {
799
+ const name2 = node.text.trim();
800
+ return name2 ? [{ imported: name2, local: name2, kind: "named" }] : [];
801
+ }
802
+ case "use_as_clause": {
803
+ const path9 = node.childForFieldName("path");
804
+ const alias = node.childForFieldName("alias")?.text.trim();
805
+ const imported = path9 ? rustLeafName(path9) : null;
806
+ return imported && alias ? [{ imported, local: alias, kind: "named" }] : [];
807
+ }
808
+ case "scoped_use_list": {
809
+ const bindings = [];
810
+ for (const item of node.childForFieldName("list")?.namedChildren ?? []) {
811
+ if (item) bindings.push(...rustBindings(item));
812
+ }
813
+ return bindings;
814
+ }
815
+ case "use_wildcard":
816
+ return [{ imported: "*", local: "*", kind: "glob" }];
817
+ default:
818
+ return [];
819
+ }
820
+ }
821
+ function rustLeafName(node) {
822
+ if (node.type === "identifier") return node.text.trim();
823
+ return node.childForFieldName("name")?.text.trim() ?? null;
824
+ }
825
+ function javaImport(node, line) {
826
+ const path9 = node.namedChildren.find((child) => child?.type === "scoped_identifier");
827
+ const specifier = dottedToPath(path9?.text);
828
+ if (!specifier) return [];
829
+ const wildcard = node.namedChildren.some((child) => child?.type === "asterisk");
830
+ if (wildcard) {
831
+ return [
832
+ { specifier, line, kind: "import", bindings: [{ imported: "*", local: "*", kind: "glob" }] }
833
+ ];
834
+ }
835
+ const imported = path9?.childForFieldName("name")?.text.trim() ?? lastSlashSegment(specifier);
836
+ const isStatic = /^import\s+static\b/.test(node.text);
837
+ return [
838
+ {
839
+ specifier,
840
+ line,
841
+ kind: "import",
842
+ bindings: [{ imported, local: imported, kind: isStatic ? "static" : "named" }]
843
+ }
844
+ ];
845
+ }
846
+ function lastDotSegment(value) {
847
+ return value.split(".").pop() ?? value;
848
+ }
849
+ function lastSlashSegment(value) {
850
+ return value.split("/").pop() ?? value;
308
851
  }
309
852
  function rustUsePath(text) {
310
853
  if (!text) return null;
@@ -460,9 +1003,9 @@ async function parseFileToSymbols(filePath, repoRoot, code) {
460
1003
  const lang = languageForFile(filePath);
461
1004
  if (!lang) return [];
462
1005
  const text = code ?? await readFile(filePath, "utf8");
463
- const rows = await extractSymbols(text, lang);
464
1006
  const rel = path.relative(repoRoot, filePath).split(path.sep).join("/");
465
- return rows.map((r) => ({ ...r, file: rel }));
1007
+ const { symbols } = await extractAll(text, lang, rel);
1008
+ return symbols;
466
1009
  }
467
1010
 
468
1011
  // src/scan.ts
@@ -546,6 +1089,14 @@ async function scanRepo(root, options = {}) {
546
1089
  import { mkdir, readFile as readFile2, rename, rm, writeFile } from "fs/promises";
547
1090
  import path3 from "path";
548
1091
  import { createHash, randomUUID } from "crypto";
1092
+
1093
+ // src/types.ts
1094
+ var REPO_INDEX_SCHEMA_VERSION = 2;
1095
+ function symbolCount(index) {
1096
+ return index.files.reduce((n, f) => n + f.symbols.length, 0);
1097
+ }
1098
+
1099
+ // src/store.ts
549
1100
  var CACHE_DIR_NAME = ".dsh-code-index";
550
1101
  function cacheKeyForRoot(root) {
551
1102
  const resolved = path3.resolve(root);
@@ -566,6 +1117,7 @@ async function loadIndex(cachePath) {
566
1117
  const raw = await readFile2(cachePath, "utf8");
567
1118
  const parsed = JSON.parse(raw);
568
1119
  if (!parsed || typeof parsed.root !== "string" || !Array.isArray(parsed.files)) return null;
1120
+ if (parsed.schemaVersion !== REPO_INDEX_SCHEMA_VERSION) return null;
569
1121
  return healSymbolFiles(parsed);
570
1122
  } catch {
571
1123
  return null;
@@ -617,13 +1169,15 @@ async function buildIndex(root, options = {}, previous = null) {
617
1169
  } catch {
618
1170
  return null;
619
1171
  }
620
- const { symbols, imports } = await extractAll(code, lang);
1172
+ const { symbols, imports, importDetails, calls } = await extractAll(code, lang, f.rel);
621
1173
  return {
622
1174
  path: f.rel,
623
1175
  lang,
624
1176
  mtimeMs: f.mtimeMs,
625
1177
  symbols: symbols.map((s) => ({ ...s, file: f.rel })),
626
- imports
1178
+ imports,
1179
+ importDetails,
1180
+ calls
627
1181
  };
628
1182
  })
629
1183
  );
@@ -632,6 +1186,7 @@ async function buildIndex(root, options = {}, previous = null) {
632
1186
  }
633
1187
  }
634
1188
  return {
1189
+ schemaVersion: REPO_INDEX_SCHEMA_VERSION,
635
1190
  root,
636
1191
  generatedAt: Date.now(),
637
1192
  files,
@@ -659,6 +1214,7 @@ async function buildIndexWithCache(root, options = {}, cacheDir) {
659
1214
  return fresh;
660
1215
  }
661
1216
  function indexesEqual(left, right) {
1217
+ if (left.schemaVersion !== right.schemaVersion) return false;
662
1218
  if (cacheKeyForRoot(left.root) !== cacheKeyForRoot(right.root)) return false;
663
1219
  if (left.files.length !== right.files.length) return false;
664
1220
  if (left.excludedDirs.length !== right.excludedDirs.length) return false;
@@ -669,12 +1225,45 @@ function indexesEqual(left, right) {
669
1225
  return false;
670
1226
  }
671
1227
  if (file.symbols.length !== other.symbols.length) return false;
1228
+ if (!callsEqual(file.calls, other.calls)) return false;
1229
+ if (!importsEqual(file.importDetails, other.importDetails)) return false;
672
1230
  return file.symbols.every((symbol, symbolIndex) => {
673
1231
  const candidate = other.symbols[symbolIndex];
674
- 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;
1232
+ return candidate !== void 0 && symbol.id === candidate.id && 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 && symbol.ordinal === candidate.ordinal && scopesEqual(symbol.scope, candidate.scope);
1233
+ });
1234
+ });
1235
+ }
1236
+ function scopesEqual(left, right) {
1237
+ if (left.length !== right.length) return false;
1238
+ return left.every((part, index) => {
1239
+ const other = right[index];
1240
+ return other !== void 0 && part.kind === other.kind && part.name === other.name && part.ordinal === other.ordinal;
1241
+ });
1242
+ }
1243
+ function importsEqual(left, right) {
1244
+ const a = left ?? [];
1245
+ const b = right ?? [];
1246
+ if (a.length !== b.length) return false;
1247
+ return a.every((info, index) => {
1248
+ const other = b[index];
1249
+ if (!other || info.specifier !== other.specifier || info.line !== other.line || info.kind !== other.kind || info.bindings.length !== other.bindings.length) {
1250
+ return false;
1251
+ }
1252
+ return info.bindings.every((binding, bindingIndex) => {
1253
+ const candidate = other.bindings[bindingIndex];
1254
+ return candidate !== void 0 && binding.imported === candidate.imported && binding.local === candidate.local && binding.kind === candidate.kind;
675
1255
  });
676
1256
  });
677
1257
  }
1258
+ function callsEqual(left, right) {
1259
+ const a = left ?? [];
1260
+ const b = right ?? [];
1261
+ if (a.length !== b.length) return false;
1262
+ return a.every((call, index) => {
1263
+ const other = b[index];
1264
+ return other !== void 0 && call.name === other.name && call.line === other.line && call.from === other.from && call.fromId === other.fromId && call.qualifier === other.qualifier;
1265
+ });
1266
+ }
678
1267
  async function findRepoRoot(startDir, maxLevels = 12) {
679
1268
  let dir = path4.resolve(startDir);
680
1269
  for (let level = 0; level < maxLevels; level++) {
@@ -690,56 +1279,6 @@ async function findRepoRoot(startDir, maxLevels = 12) {
690
1279
  return null;
691
1280
  }
692
1281
 
693
- // src/types.ts
694
- function symbolCount(index) {
695
- return index.files.reduce((n, f) => n + f.symbols.length, 0);
696
- }
697
-
698
- // src/search.ts
699
- function matchScore(name2, query) {
700
- const q = query.toLowerCase();
701
- const n = name2.toLowerCase();
702
- if (n === q) return 1;
703
- if (n.startsWith(q)) return 0.8;
704
- if (n.includes(q)) return 0.5;
705
- if (q.length >= 3 && isSubsequence(q, n)) return 0.3;
706
- return 0;
707
- }
708
- function isSubsequence(query, name2) {
709
- let i = 0;
710
- for (const ch of name2) {
711
- if (ch === query[i]) i++;
712
- if (i === query.length) return true;
713
- }
714
- return false;
715
- }
716
- function searchSymbols(index, filter, limit = 50) {
717
- const q = (filter.query ?? "").trim();
718
- const filePat = filter.file?.trim().toLowerCase();
719
- const hits = [];
720
- for (const file of index.files) {
721
- if (filePat && !file.path.toLowerCase().includes(filePat)) continue;
722
- for (const sym of file.symbols) {
723
- if (filter.kind && sym.kind !== filter.kind) continue;
724
- if (filter.exportedOnly && !sym.exported) continue;
725
- const score = q ? matchScore(sym.name, q) : 0.4;
726
- if (q && score === 0) continue;
727
- hits.push({ ...sym, score });
728
- }
729
- }
730
- hits.sort((a, b) => {
731
- const ab = Number(b.exported) - Number(a.exported) || b.score - a.score || a.name.localeCompare(b.name) || a.file.localeCompare(b.file);
732
- return ab;
733
- });
734
- return hits.slice(0, limit);
735
- }
736
- function renderHit(hit) {
737
- const exportMark = hit.exported ? "export " : "";
738
- const sig = hit.signature || hit.name;
739
- const score = hit.score < 1 ? ` [${hit.score.toFixed(2)}]` : "";
740
- return `${exportMark}${hit.kind} ${sig}${score} \u2014 ${hit.file}:${hit.line}`;
741
- }
742
-
743
1282
  // src/repomap.ts
744
1283
  import path5 from "path";
745
1284
  var KIND_WEIGHT = {
@@ -844,30 +1383,45 @@ function referencePageRank(files, density) {
844
1383
  for (let i = 0; i < n; i++) result.set(nodes[i], rank[i]);
845
1384
  return result;
846
1385
  }
847
- function resolveImport(spec, fromPath, fileSet) {
848
- if (!spec) return null;
1386
+ function resolveImportDetailed(spec, fromPath, fileSet) {
1387
+ if (!spec) return { target: null, suffixSkipped: false };
849
1388
  if (spec.startsWith("./") || spec.startsWith("../")) {
850
1389
  const base = path5.posix.normalize(path5.posix.join(path5.posix.dirname(fromPath), spec));
851
- return candidateFor(base, fileSet);
1390
+ return { target: candidateFor(base, fileSet), suffixSkipped: false };
852
1391
  }
853
1392
  const segments = spec.split("/").filter(Boolean);
854
1393
  for (let skip = 0; skip < segments.length; skip++) {
855
1394
  const hit = candidateFor(segments.slice(skip).join("/"), fileSet);
856
- if (hit) return hit;
1395
+ if (hit) return { target: hit, suffixSkipped: skip > 0 };
857
1396
  }
858
- return null;
1397
+ return { target: null, suffixSkipped: false };
1398
+ }
1399
+ function resolveImport(spec, fromPath, fileSet) {
1400
+ return resolveImportDetailed(spec, fromPath, fileSet).target;
859
1401
  }
860
1402
  function candidateFor(base, fileSet) {
861
1403
  if (fileSet.has(base)) return base;
862
1404
  for (const ext of SUPPORTED_EXTS) {
863
1405
  if (fileSet.has(`${base}${ext}`)) return `${base}${ext}`;
864
1406
  }
1407
+ const aliased = tsSourceForJsSpecifier(base, fileSet);
1408
+ if (aliased) return aliased;
865
1409
  for (const ext of SUPPORTED_EXTS) {
866
1410
  if (fileSet.has(`${base}/index${ext}`)) return `${base}/index${ext}`;
867
1411
  if (fileSet.has(`${base}/__init__${ext}`)) return `${base}/__init__${ext}`;
868
1412
  }
869
1413
  return null;
870
1414
  }
1415
+ function tsSourceForJsSpecifier(base, fileSet) {
1416
+ const match = /\.(js|jsx|mjs|cjs)$/.exec(base);
1417
+ if (!match) return null;
1418
+ const stem = base.slice(0, base.length - match[0].length);
1419
+ const candidates = match[1] === "jsx" ? [".tsx", ".ts"] : match[1] === "mjs" ? [".mts", ".ts"] : match[1] === "cjs" ? [".cts", ".ts"] : [".ts", ".tsx"];
1420
+ for (const ext of candidates) {
1421
+ if (fileSet.has(`${stem}${ext}`)) return `${stem}${ext}`;
1422
+ }
1423
+ return null;
1424
+ }
871
1425
  function renderRepoMap(entries, options = {}) {
872
1426
  const maxChars = options.maxChars ?? 3200;
873
1427
  const lines = ["# repo map"];
@@ -888,67 +1442,1341 @@ function renderRepoMap(entries, options = {}) {
888
1442
  return totalSymbols > 0 ? text : "";
889
1443
  }
890
1444
 
891
- // src/tools.ts
892
- import { defineTool } from "@deepseek-ai/dsh-tools";
893
- import path6 from "path";
894
-
895
- // src/config.ts
896
- var DEFAULTS = {
897
- excludeDirs: [],
898
- mapTopFiles: 24,
899
- mapMaxChars: 3200,
900
- mapTtlMs: 6e4,
901
- autoInject: true
902
- };
903
- var state = { current: { ...DEFAULTS } };
904
- function applyConfig(partial) {
905
- state.current = {
906
- ...DEFAULTS,
907
- ...partial ?? {},
908
- excludeDirs: [...DEFAULTS.excludeDirs, ...partial?.excludeDirs ?? []]
909
- };
910
- if (!Number.isFinite(state.current.mapTopFiles) || state.current.mapTopFiles < 1) {
911
- state.current.mapTopFiles = DEFAULTS.mapTopFiles;
912
- }
913
- if (!Number.isFinite(state.current.mapMaxChars) || state.current.mapMaxChars < 200) {
914
- state.current.mapMaxChars = DEFAULTS.mapMaxChars;
1445
+ // src/refgraph.ts
1446
+ function buildSymbolTable(index) {
1447
+ const table = /* @__PURE__ */ new Map();
1448
+ for (const file of index.files) {
1449
+ for (const symbol of file.symbols) {
1450
+ const list = table.get(symbol.name);
1451
+ if (list) list.push(symbol);
1452
+ else table.set(symbol.name, [symbol]);
1453
+ }
915
1454
  }
916
- if (!Number.isFinite(state.current.mapTtlMs) || state.current.mapTtlMs < 1e3) {
917
- state.current.mapTtlMs = DEFAULTS.mapTtlMs;
1455
+ for (const list of table.values()) {
1456
+ list.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
918
1457
  }
1458
+ return table;
919
1459
  }
920
- function getConfig() {
921
- return state.current;
922
- }
923
- function indexOptions() {
924
- return { excludeDirs: state.current.excludeDirs };
1460
+ function callerCounts(index) {
1461
+ const counts = /* @__PURE__ */ new Map();
1462
+ for (const file of index.files) {
1463
+ for (const call of file.calls ?? []) {
1464
+ counts.set(call.name, (counts.get(call.name) ?? 0) + 1);
1465
+ }
1466
+ }
1467
+ return counts;
925
1468
  }
926
-
927
- // src/tools.ts
928
- function createIndexCache(load, ttlMs = 6e4, now = Date.now) {
929
- const completed = /* @__PURE__ */ new Map();
930
- const inFlight = /* @__PURE__ */ new Map();
931
- let epoch = 0;
932
- function startLoad(key, root, previous) {
933
- const loadEpoch = epoch;
934
- const begin = previous ? previous.catch(() => void 0).then(() => load(root)) : load(root);
935
- const promise = begin.then((index) => {
936
- if (loadEpoch === epoch) {
937
- completed.delete(key);
938
- completed.set(key, { index, at: now(), epoch: loadEpoch });
939
- while (completed.size > 16) completed.delete(completed.keys().next().value);
1469
+ function symbolRefs(index, symbol, limit = 200) {
1470
+ const table = buildSymbolTable(index);
1471
+ const defs = table.get(symbol) ?? [];
1472
+ const callers = [];
1473
+ const calleeFirstLine = /* @__PURE__ */ new Map();
1474
+ for (const file of index.files) {
1475
+ for (const call of file.calls ?? []) {
1476
+ if (call.name === symbol) {
1477
+ callers.push({ file: file.path, line: call.line, from: call.from });
940
1478
  }
941
- return index;
942
- }).finally(() => {
943
- if (inFlight.get(key)?.promise === promise) inFlight.delete(key);
944
- });
945
- inFlight.set(key, { promise, epoch: loadEpoch });
946
- return promise;
1479
+ if (call.from === symbol && !calleeFirstLine.has(call.name)) {
1480
+ calleeFirstLine.set(call.name, call.line);
1481
+ }
1482
+ }
947
1483
  }
1484
+ callers.sort(
1485
+ (a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.from.localeCompare(b.from)
1486
+ );
1487
+ 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));
948
1488
  return {
949
- get(root, force = false) {
950
- const resolvedRoot = path6.resolve(root);
951
- const key = cacheKeyForRoot(resolvedRoot);
1489
+ defs,
1490
+ callers: callers.slice(0, limit),
1491
+ callees: callees.slice(0, limit)
1492
+ };
1493
+ }
1494
+ var DEFAULT_MAX_CANDIDATES = 8;
1495
+ function buildReferenceGraph(index, options = {}) {
1496
+ const maxCandidates = options.maxCandidatesPerCall ?? DEFAULT_MAX_CANDIDATES;
1497
+ const symbolsById = /* @__PURE__ */ new Map();
1498
+ const symbolsByFile = /* @__PURE__ */ new Map();
1499
+ for (const file of index.files) {
1500
+ for (const symbol of file.symbols) symbolsById.set(symbol.id, symbol);
1501
+ symbolsByFile.set(file.path, file.symbols);
1502
+ }
1503
+ const symbolsByName = buildSymbolTable(index);
1504
+ const fileSet = new Set(index.files.map((file) => file.path));
1505
+ const outgoing = /* @__PURE__ */ new Map();
1506
+ const incoming = /* @__PURE__ */ new Map();
1507
+ const moduleLevelEdges = [];
1508
+ for (const file of index.files) {
1509
+ for (const call of file.calls ?? []) {
1510
+ const edges = resolveCallTargetsFor(
1511
+ fileSet,
1512
+ file,
1513
+ call,
1514
+ symbolsByName,
1515
+ symbolsByFile,
1516
+ maxCandidates
1517
+ );
1518
+ if (edges.length === 0) continue;
1519
+ if (call.fromId) {
1520
+ appendEdges(outgoing, call.fromId, edges);
1521
+ } else {
1522
+ moduleLevelEdges.push(...edges);
1523
+ }
1524
+ for (const edge of edges) appendEdges(incoming, edge.targetId, [edge]);
1525
+ }
1526
+ }
1527
+ return { symbolsById, outgoing, incoming, moduleLevelEdges };
1528
+ }
1529
+ function resolveCallTargetsFor(fileSet, file, call, symbolsByName, symbolsByFile, maxCandidates) {
1530
+ const named = symbolsByName.get(call.name) ?? [];
1531
+ const qualified = call.qualifier !== null;
1532
+ const imports = (file.importDetails ?? []).filter((info) => info.kind !== "reexport");
1533
+ if (!qualified) {
1534
+ const locals = named.filter((s) => s.file === file.path && lexicallyVisible(s, call));
1535
+ if (locals.length > 0) {
1536
+ const unique = locals.length === 1;
1537
+ const owned = unique && isStructuralOwner(call, locals[0]);
1538
+ const provenance = owned && call.fromId !== null ? "structural-owner" : "local-unique";
1539
+ return toEdges(locals, call, file.path, provenance, owned ? "exact" : "name-only", maxCandidates);
1540
+ }
1541
+ const bound = [];
1542
+ let directBinding = true;
1543
+ let defaultBinding = false;
1544
+ for (const info of imports) {
1545
+ for (const binding of info.bindings) {
1546
+ if (binding.local !== call.name) continue;
1547
+ const isDefault = binding.kind === "default" || binding.imported === "default";
1548
+ if (!isDefault && !isExplicitBinding(binding)) continue;
1549
+ const resolved = resolveImportDetailed(info.specifier, file.path, fileSet);
1550
+ const target = resolved.target;
1551
+ if (!target) continue;
1552
+ if (resolved.suffixSkipped) directBinding = false;
1553
+ if (isDefault) {
1554
+ defaultBinding = true;
1555
+ const moduleSymbols = symbolsByFile.get(target) ?? [];
1556
+ const nameMatches = moduleSymbols.filter((s) => s.name === binding.local);
1557
+ bound.push(
1558
+ ...nameMatches.length > 0 ? nameMatches : moduleSymbols.filter((s) => s.exported)
1559
+ );
1560
+ continue;
1561
+ }
1562
+ if (!isExplicitBinding(binding)) continue;
1563
+ for (const def of symbolsByName.get(binding.imported) ?? []) {
1564
+ if (def.file === target) bound.push(def);
1565
+ }
1566
+ }
1567
+ }
1568
+ if (bound.length > 0) {
1569
+ const label = bound.length === 1 && directBinding && !defaultBinding ? "exact" : "import-scoped";
1570
+ return toEdges(bound, call, file.path, "explicit-import-binding", label, maxCandidates);
1571
+ }
1572
+ const resolvedModules = /* @__PURE__ */ new Set();
1573
+ for (const info of imports) {
1574
+ const target = resolveImport(info.specifier, file.path, fileSet);
1575
+ if (target && target !== file.path) resolvedModules.add(target);
1576
+ }
1577
+ const imported = named.filter((s) => resolvedModules.has(s.file));
1578
+ if (imported.length > 0) {
1579
+ return toEdges(
1580
+ imported,
1581
+ call,
1582
+ file.path,
1583
+ "resolved-import-module",
1584
+ "import-scoped",
1585
+ maxCandidates
1586
+ );
1587
+ }
1588
+ }
1589
+ if (qualified) {
1590
+ const qualifier = call.qualifier;
1591
+ const modules = /* @__PURE__ */ new Set();
1592
+ for (const info of imports) {
1593
+ const namespaceBound = info.bindings.some(
1594
+ (b) => (b.kind === "namespace" || b.kind === "package") && b.local === qualifier
1595
+ );
1596
+ if (!namespaceBound) {
1597
+ if (info.bindings.length > 0) continue;
1598
+ if (specifierLastSegment(info.specifier) !== qualifier) continue;
1599
+ }
1600
+ const target = resolveImport(info.specifier, file.path, fileSet);
1601
+ if (target && target !== file.path) modules.add(target);
1602
+ }
1603
+ const qualifiedTargets = named.filter((s) => modules.has(s.file));
1604
+ if (qualifiedTargets.length > 0) {
1605
+ return toEdges(
1606
+ qualifiedTargets,
1607
+ call,
1608
+ file.path,
1609
+ "namespace-import",
1610
+ "import-scoped",
1611
+ maxCandidates
1612
+ );
1613
+ }
1614
+ }
1615
+ if (named.length > 0) {
1616
+ return toEdges(named, call, file.path, "global-name", "name-only", maxCandidates);
1617
+ }
1618
+ return [];
1619
+ }
1620
+ function toEdges(candidates, call, filePath, provenance, resolution, maxCandidates) {
1621
+ const unique = dedupeSymbols(candidates);
1622
+ const count = unique.length;
1623
+ const cap = maxCandidates > 0 ? Math.floor(maxCandidates) : 0;
1624
+ return unique.slice(0, cap).map((target) => ({
1625
+ sourceId: call.fromId,
1626
+ targetId: target.id,
1627
+ callSite: { file: filePath, line: call.line },
1628
+ name: call.name,
1629
+ provenance,
1630
+ resolution,
1631
+ candidateCount: count
1632
+ }));
1633
+ }
1634
+ function appendEdges(map, key, edges) {
1635
+ const list = map.get(key);
1636
+ if (list) list.push(...edges);
1637
+ else map.set(key, [...edges]);
1638
+ }
1639
+ function dedupeSymbols(candidates) {
1640
+ const sorted = [...candidates].sort(compareSymbols);
1641
+ const out = [];
1642
+ const seen = /* @__PURE__ */ new Set();
1643
+ for (const symbol of sorted) {
1644
+ if (seen.has(symbol.id)) continue;
1645
+ seen.add(symbol.id);
1646
+ out.push(symbol);
1647
+ }
1648
+ return out;
1649
+ }
1650
+ function compareSymbols(a, b) {
1651
+ return a.file.localeCompare(b.file) || a.line - b.line || a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name) || a.id.localeCompare(b.id);
1652
+ }
1653
+ function lexicallyVisible(candidate, call) {
1654
+ if (call.fromId) {
1655
+ const ownerPath = pathAfterHash(call.fromId);
1656
+ const scopePath = containerPath(candidate.id);
1657
+ if (scopePath === "") return true;
1658
+ return ownerPath === scopePath || ownerPath.startsWith(`${scopePath}/`);
1659
+ }
1660
+ if (candidate.scope.length === 0) return true;
1661
+ return candidate.line <= call.line && call.line <= candidate.endLine;
1662
+ }
1663
+ function isStructuralOwner(call, target) {
1664
+ if (!call.fromId) return containerPath(target.id) === "";
1665
+ if (target.id === call.fromId) return true;
1666
+ return containerPath(target.id) === pathAfterHash(call.fromId);
1667
+ }
1668
+ function pathAfterHash(id) {
1669
+ const hash = id.indexOf("#");
1670
+ return hash >= 0 ? id.slice(hash + 1) : id;
1671
+ }
1672
+ function containerPath(id) {
1673
+ const full = pathAfterHash(id);
1674
+ const slash = full.lastIndexOf("/");
1675
+ return slash >= 0 ? full.slice(0, slash) : "";
1676
+ }
1677
+ function isExplicitBinding(binding) {
1678
+ return binding.imported != null && binding.imported !== "*" && binding.imported !== "default";
1679
+ }
1680
+ function specifierLastSegment(specifier) {
1681
+ const clean = specifier.split(/[?#]/)[0] ?? specifier;
1682
+ const segment = clean.split("/").filter(Boolean).pop() ?? clean;
1683
+ return segment.replace(/\.[A-Za-z0-9]+$/, "");
1684
+ }
1685
+
1686
+ // src/search.ts
1687
+ function matchScore(name2, query) {
1688
+ const q = query.toLowerCase();
1689
+ const n = name2.toLowerCase();
1690
+ if (n === q) return 1;
1691
+ if (n.startsWith(q)) return 0.8;
1692
+ if (n.includes(q)) return 0.5;
1693
+ if (q.length >= 3 && isSubsequence(q, n)) return 0.3;
1694
+ return 0;
1695
+ }
1696
+ function isSubsequence(query, name2) {
1697
+ let i = 0;
1698
+ for (const ch of name2) {
1699
+ if (ch === query[i]) i++;
1700
+ if (i === query.length) return true;
1701
+ }
1702
+ return false;
1703
+ }
1704
+ function searchSymbols(index, filter, limit = 50) {
1705
+ const q = (filter.query ?? "").trim();
1706
+ const filePat = filter.file?.trim().toLowerCase();
1707
+ const hits = [];
1708
+ for (const file of index.files) {
1709
+ if (filePat && !file.path.toLowerCase().includes(filePat)) continue;
1710
+ for (const sym of file.symbols) {
1711
+ if (filter.kind && sym.kind !== filter.kind) continue;
1712
+ if (filter.exportedOnly && !sym.exported) continue;
1713
+ const score = q ? matchScore(sym.name, q) : 0.4;
1714
+ if (q && score === 0) continue;
1715
+ hits.push({ ...sym, score });
1716
+ }
1717
+ }
1718
+ const refs = callerCounts(index);
1719
+ hits.sort((a, b) => {
1720
+ 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);
1721
+ return ab;
1722
+ });
1723
+ return hits.slice(0, limit);
1724
+ }
1725
+ function renderHit(hit) {
1726
+ const exportMark = hit.exported ? "export " : "";
1727
+ const sig = hit.signature || hit.name;
1728
+ const score = hit.score < 1 ? ` [${hit.score.toFixed(2)}]` : "";
1729
+ return `${exportMark}${hit.kind} ${sig}${score} \u2014 ${hit.file}:${hit.line}`;
1730
+ }
1731
+
1732
+ // src/health.ts
1733
+ function buildModuleGraph(index) {
1734
+ const nodes = index.files.map((file) => file.path).sort((a, b) => a.localeCompare(b));
1735
+ const fileSet = new Set(nodes);
1736
+ const edges = /* @__PURE__ */ new Map();
1737
+ for (const file of index.files) {
1738
+ const targets = /* @__PURE__ */ new Set();
1739
+ for (const spec of file.imports ?? []) {
1740
+ const target = resolveImport(spec, file.path, fileSet);
1741
+ if (target && target !== file.path) targets.add(target);
1742
+ }
1743
+ edges.set(file.path, targets);
1744
+ }
1745
+ return { nodes, edges };
1746
+ }
1747
+ function findCycles(index) {
1748
+ const { nodes, edges } = buildModuleGraph(index);
1749
+ const discovery = /* @__PURE__ */ new Map();
1750
+ const low = /* @__PURE__ */ new Map();
1751
+ const onStack = /* @__PURE__ */ new Set();
1752
+ const stack = [];
1753
+ const components = [];
1754
+ let counter = 0;
1755
+ const visit = (node) => {
1756
+ discovery.set(node, counter);
1757
+ low.set(node, counter);
1758
+ counter++;
1759
+ stack.push(node);
1760
+ onStack.add(node);
1761
+ for (const next of edges.get(node) ?? []) {
1762
+ if (!discovery.has(next)) {
1763
+ visit(next);
1764
+ low.set(node, Math.min(low.get(node), low.get(next)));
1765
+ } else if (onStack.has(next)) {
1766
+ low.set(node, Math.min(low.get(node), discovery.get(next)));
1767
+ }
1768
+ }
1769
+ if (low.get(node) === discovery.get(node)) {
1770
+ const component = [];
1771
+ for (; ; ) {
1772
+ const member = stack.pop();
1773
+ onStack.delete(member);
1774
+ component.push(member);
1775
+ if (member === node) break;
1776
+ }
1777
+ if (component.length > 1) components.push(component.sort());
1778
+ }
1779
+ };
1780
+ for (const node of nodes) {
1781
+ if (!discovery.has(node)) visit(node);
1782
+ }
1783
+ components.sort((a, b) => a[0].localeCompare(b[0]));
1784
+ return components;
1785
+ }
1786
+ var ENTRY_RE = /(^|\/)(index|main|cli|app|server|mod|lib|__init__)\.[A-Za-z0-9]+$|(^|\/)(bin|cmd|scripts)\/|\.config\.[A-Za-z0-9]+$/;
1787
+ var TEST_RE = /(^|\/)(tests?|__tests__)(\/|$)|\.(?:spec|test)\.[cm]?[jt]sx?$|(^|\/)(?:test_[^/]+\.py|[^/]+_test\.(?:py|go))$/;
1788
+ function findOrphanModules(index) {
1789
+ const { nodes, edges } = buildModuleGraph(index);
1790
+ const incoming = /* @__PURE__ */ new Map();
1791
+ for (const targets of edges.values()) {
1792
+ for (const target of targets) incoming.set(target, (incoming.get(target) ?? 0) + 1);
1793
+ }
1794
+ const withSymbols = new Set(
1795
+ index.files.filter((file) => file.symbols.length > 0).map((file) => file.path)
1796
+ );
1797
+ return nodes.filter(
1798
+ (path9) => withSymbols.has(path9) && (incoming.get(path9) ?? 0) === 0 && (edges.get(path9)?.size ?? 0) === 0 && !ENTRY_RE.test(path9) && !TEST_RE.test(path9)
1799
+ );
1800
+ }
1801
+
1802
+ // src/git-diff.ts
1803
+ import { execFile } from "child_process";
1804
+ import { readFile as readFile4 } from "fs/promises";
1805
+ import path6 from "path";
1806
+ var HUNK_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
1807
+ var HEADER_PATH_PREFIX_RE = /^[ab]\//;
1808
+ var MAX_BUFFER = 64 * 1024 * 1024;
1809
+ function newDraft() {
1810
+ return {
1811
+ oldPath: null,
1812
+ newPath: null,
1813
+ gitOldPath: null,
1814
+ gitNewPath: null,
1815
+ sawOldHeader: false,
1816
+ sawNewHeader: false,
1817
+ renameFrom: null,
1818
+ renameTo: null,
1819
+ newFile: false,
1820
+ deletedFile: false,
1821
+ hunks: [],
1822
+ sawHunk: false
1823
+ };
1824
+ }
1825
+ function parseUnifiedDiff(text) {
1826
+ const changes = [];
1827
+ let draft = null;
1828
+ for (const line of text.split("\n")) {
1829
+ if (line.startsWith("diff --git ")) {
1830
+ if (draft) changes.push(finalize(draft));
1831
+ draft = newDraft();
1832
+ const [rawOld, rawNew] = splitDiffGitPaths(line.slice("diff --git ".length));
1833
+ draft.gitOldPath = rawOld === null ? null : parseHeaderPath(rawOld);
1834
+ draft.gitNewPath = rawNew === null ? null : parseHeaderPath(rawNew);
1835
+ continue;
1836
+ }
1837
+ if (!draft) {
1838
+ if (!isFileLine(line)) continue;
1839
+ draft = newDraft();
1840
+ }
1841
+ if (line.startsWith("new file mode ")) {
1842
+ draft.newFile = true;
1843
+ } else if (line.startsWith("deleted file mode ")) {
1844
+ draft.deletedFile = true;
1845
+ } else if (line.startsWith("rename from ")) {
1846
+ draft.renameFrom = parseRawPath(line.slice("rename from ".length));
1847
+ } else if (line.startsWith("rename to ")) {
1848
+ draft.renameTo = parseRawPath(line.slice("rename to ".length));
1849
+ } else if (!draft.sawHunk && line.startsWith("--- ")) {
1850
+ draft.oldPath = parseHeaderPath(line.slice(4));
1851
+ draft.sawOldHeader = true;
1852
+ } else if (!draft.sawHunk && line.startsWith("+++ ")) {
1853
+ draft.newPath = parseHeaderPath(line.slice(4));
1854
+ draft.sawNewHeader = true;
1855
+ } else if (line.startsWith("@@ ")) {
1856
+ const hunk = parseHunk(line);
1857
+ if (hunk) {
1858
+ draft.hunks.push(hunk);
1859
+ draft.sawHunk = true;
1860
+ }
1861
+ }
1862
+ }
1863
+ if (draft) changes.push(finalize(draft));
1864
+ return changes;
1865
+ }
1866
+ function isFileLine(line) {
1867
+ return line.startsWith("--- ") || line.startsWith("+++ ") || line.startsWith("@@ ") || line.startsWith("new file mode ") || line.startsWith("deleted file mode ") || line.startsWith("rename from ") || line.startsWith("rename to ");
1868
+ }
1869
+ function finalize(draft) {
1870
+ let oldPath = draft.renameFrom ?? (draft.sawOldHeader ? draft.oldPath : draft.gitOldPath);
1871
+ let newPath = draft.renameTo ?? (draft.sawNewHeader ? draft.newPath : draft.gitNewPath);
1872
+ if (draft.newFile) oldPath = null;
1873
+ if (draft.deletedFile) newPath = null;
1874
+ let status = "modified";
1875
+ if (draft.newFile || oldPath === null && newPath !== null && draft.renameFrom === null) {
1876
+ status = "added";
1877
+ } else if (draft.deletedFile || newPath === null && oldPath !== null && draft.renameTo === null) {
1878
+ status = "deleted";
1879
+ } else if (draft.renameFrom !== null || draft.renameTo !== null) {
1880
+ status = "renamed";
1881
+ }
1882
+ return { oldPath, newPath, status, hunks: draft.hunks };
1883
+ }
1884
+ function splitDiffGitPaths(rest) {
1885
+ const tokens = [];
1886
+ let index = 0;
1887
+ while (index < rest.length && tokens.length < 2) {
1888
+ while (index < rest.length && rest[index] === " ") index++;
1889
+ if (index >= rest.length) break;
1890
+ const start = index;
1891
+ if (rest[index] === '"') {
1892
+ index++;
1893
+ while (index < rest.length) {
1894
+ if (rest[index] === "\\") index += 2;
1895
+ else if (rest[index] === '"') {
1896
+ index++;
1897
+ break;
1898
+ } else index++;
1899
+ }
1900
+ } else {
1901
+ while (index < rest.length && rest[index] !== " ") index++;
1902
+ }
1903
+ tokens.push(rest.slice(start, index));
1904
+ }
1905
+ return [tokens[0] ?? null, tokens[1] ?? null];
1906
+ }
1907
+ function parseHunk(line) {
1908
+ const match = HUNK_RE.exec(line);
1909
+ if (!match) return null;
1910
+ const oldCount = match[2];
1911
+ const newCount = match[4];
1912
+ return {
1913
+ oldStart: Number(match[1]),
1914
+ oldLines: oldCount === void 0 ? 1 : Number(oldCount),
1915
+ newStart: Number(match[3]),
1916
+ newLines: newCount === void 0 ? 1 : Number(newCount)
1917
+ };
1918
+ }
1919
+ function parseHeaderPath(raw) {
1920
+ let value = raw;
1921
+ const tab = value.indexOf(" ");
1922
+ if (tab >= 0) value = value.slice(0, tab);
1923
+ value = value.trim();
1924
+ if (value === "/dev/null") return null;
1925
+ if (value.startsWith('"')) value = unquoteGitPath(value);
1926
+ value = value.replace(HEADER_PATH_PREFIX_RE, "");
1927
+ return value.length > 0 ? value : null;
1928
+ }
1929
+ function parseRawPath(raw) {
1930
+ let value = raw.trim();
1931
+ if (value.startsWith('"')) value = unquoteGitPath(value);
1932
+ return value.length > 0 ? value : null;
1933
+ }
1934
+ function unquoteGitPath(value) {
1935
+ const inner = value.slice(1, -1);
1936
+ const bytes = [];
1937
+ for (let i = 0; i < inner.length; i++) {
1938
+ const ch = inner[i];
1939
+ if (ch !== "\\") {
1940
+ bytes.push(...Buffer.from(ch, "utf8"));
1941
+ continue;
1942
+ }
1943
+ i++;
1944
+ const esc = inner[i];
1945
+ if (esc === void 0) break;
1946
+ if (esc >= "0" && esc <= "7") {
1947
+ let octal = esc;
1948
+ let next = inner[i + 1];
1949
+ while (octal.length < 3 && next !== void 0 && next >= "0" && next <= "7") {
1950
+ octal += next;
1951
+ i++;
1952
+ next = inner[i + 1];
1953
+ }
1954
+ bytes.push(parseInt(octal, 8));
1955
+ continue;
1956
+ }
1957
+ switch (esc) {
1958
+ case "n":
1959
+ bytes.push(10);
1960
+ break;
1961
+ case "t":
1962
+ bytes.push(9);
1963
+ break;
1964
+ case "r":
1965
+ bytes.push(13);
1966
+ break;
1967
+ case "f":
1968
+ bytes.push(12);
1969
+ break;
1970
+ case "v":
1971
+ bytes.push(11);
1972
+ break;
1973
+ case "b":
1974
+ bytes.push(8);
1975
+ break;
1976
+ case "a":
1977
+ bytes.push(7);
1978
+ break;
1979
+ case '"':
1980
+ bytes.push(34);
1981
+ break;
1982
+ case "\\":
1983
+ bytes.push(92);
1984
+ break;
1985
+ default:
1986
+ bytes.push(...Buffer.from(esc, "utf8"));
1987
+ }
1988
+ }
1989
+ return Buffer.from(bytes).toString("utf8");
1990
+ }
1991
+ function requireSafeRef(ref, label) {
1992
+ if (ref.length === 0) throw new Error(`${label} must not be empty`);
1993
+ if (ref.startsWith("-")) throw new Error(`${label} must not start with '-': ${ref}`);
1994
+ }
1995
+ function runGit(args, cwd) {
1996
+ return new Promise((resolve, reject) => {
1997
+ execFile("git", args, { cwd, encoding: "utf8", maxBuffer: MAX_BUFFER }, (error, stdout) => {
1998
+ if (error) reject(error);
1999
+ else resolve(stdout);
2000
+ });
2001
+ });
2002
+ }
2003
+ async function readWorkingTreeDiff(root, baseRef) {
2004
+ requireSafeRef(baseRef, "baseRef");
2005
+ const tracked = await runGit(
2006
+ ["diff", "--no-ext-diff", "--find-renames", "--unified=0", baseRef, "--", "."],
2007
+ root
2008
+ );
2009
+ const untracked = await runGit(["ls-files", "--others", "--exclude-standard", "-z", "--"], root);
2010
+ const changes = parseUnifiedDiff(tracked);
2011
+ const untrackedFiles = [];
2012
+ for (const rawPath of untracked.split("\0")) {
2013
+ if (rawPath.length === 0) continue;
2014
+ const normalized = rawPath.replace(/\\/g, "/");
2015
+ if (normalized === ".dsh-code-index" || normalized.startsWith(".dsh-code-index/")) continue;
2016
+ try {
2017
+ const content = await readFile4(path6.resolve(root, normalized), "utf8");
2018
+ const lines = content.length === 0 ? 0 : content.split(/\r?\n/).length - (content.endsWith("\n") ? 1 : 0);
2019
+ untrackedFiles.push({ path: normalized, content, lines });
2020
+ } catch {
2021
+ }
2022
+ }
2023
+ const paired = /* @__PURE__ */ new Set();
2024
+ for (const change of changes) {
2025
+ if (change.status !== "deleted" || change.oldPath === null) continue;
2026
+ let oldContent;
2027
+ try {
2028
+ oldContent = await runGit(["show", `${baseRef}:${change.oldPath}`], root);
2029
+ } catch {
2030
+ continue;
2031
+ }
2032
+ const match = untrackedFiles.find((file) => !paired.has(file.path) && file.content === oldContent);
2033
+ if (!match) continue;
2034
+ change.newPath = match.path;
2035
+ change.status = "renamed";
2036
+ change.hunks = [];
2037
+ paired.add(match.path);
2038
+ }
2039
+ for (const file of untrackedFiles) {
2040
+ if (paired.has(file.path)) continue;
2041
+ changes.push({
2042
+ oldPath: null,
2043
+ newPath: file.path,
2044
+ status: "added",
2045
+ hunks: file.lines > 0 ? [{ oldStart: 0, oldLines: 0, newStart: 1, newLines: file.lines }] : []
2046
+ });
2047
+ }
2048
+ return renderNormalizedDiff(changes);
2049
+ }
2050
+ function renderNormalizedDiff(changes) {
2051
+ return changes.map((change) => {
2052
+ const oldPath = change.oldPath ?? change.newPath;
2053
+ const newPath = change.newPath ?? change.oldPath;
2054
+ const lines = [`diff --git ${diffToken(`a/${oldPath}`)} ${diffToken(`b/${newPath}`)}`];
2055
+ if (change.status === "added") lines.push("new file mode 100644");
2056
+ if (change.status === "deleted") lines.push("deleted file mode 100644");
2057
+ if (change.status === "renamed") {
2058
+ lines.push("similarity index 100%", `rename from ${diffToken(change.oldPath)}`, `rename to ${diffToken(change.newPath)}`);
2059
+ } else {
2060
+ lines.push(`--- ${change.oldPath === null ? "/dev/null" : diffToken(`a/${change.oldPath}`)}`);
2061
+ lines.push(`+++ ${change.newPath === null ? "/dev/null" : diffToken(`b/${change.newPath}`)}`);
2062
+ }
2063
+ for (const hunk of change.hunks) {
2064
+ const oldCount = hunk.oldLines === 1 ? "" : `,${hunk.oldLines}`;
2065
+ const newCount = hunk.newLines === 1 ? "" : `,${hunk.newLines}`;
2066
+ lines.push(`@@ -${hunk.oldStart}${oldCount} +${hunk.newStart}${newCount} @@`);
2067
+ }
2068
+ return `${lines.join("\n")}
2069
+ `;
2070
+ }).join("");
2071
+ }
2072
+ function diffToken(value) {
2073
+ return /[\s"]/.test(value) ? `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : value;
2074
+ }
2075
+ async function readGitFileAtRef(root, ref, repoRelativePath) {
2076
+ requireSafeRef(ref, "ref");
2077
+ try {
2078
+ return await runGit(["show", `${ref}:${repoRelativePath}`], root);
2079
+ } catch {
2080
+ return null;
2081
+ }
2082
+ }
2083
+
2084
+ // src/change-context.ts
2085
+ import path7 from "path";
2086
+ var LABEL_STRENGTH = {
2087
+ exact: 3,
2088
+ "import-scoped": 2,
2089
+ "name-only": 1
2090
+ };
2091
+ var ENTRY_KINDS = /* @__PURE__ */ new Set(["function", "method", "class"]);
2092
+ var ENTRY_FILE_RE = /(^|\/)(main|index|cli|server|app)\.[^./]+$/;
2093
+ var TEST_RE2 = /(^|\/)(tests?|__tests__)(\/|$)|\.(?:spec|test)\.[cm]?[jt]sx?$|(^|\/)(?:test_[^/]+\.py|[^/]+_test\.(?:py|go))$/;
2094
+ function changeContextInputFromArgs(args, root) {
2095
+ const hasDiff = typeof args.diff === "string" && args.diff.trim().length > 0;
2096
+ const hasFiles = Array.isArray(args.files) && args.files.length > 0;
2097
+ const hasSymbols = Array.isArray(args.symbols) && args.symbols.length > 0;
2098
+ if ([hasDiff, hasFiles, hasSymbols].filter(Boolean).length > 1) {
2099
+ throw new Error("pass only one of diff, files, symbols");
2100
+ }
2101
+ const suppliedBaseRef = typeof args.baseRef === "string" && args.baseRef.length > 0 ? args.baseRef : void 0;
2102
+ if (hasDiff) {
2103
+ return suppliedBaseRef === void 0 ? { kind: "diff", root, text: args.diff } : { kind: "diff", root, text: args.diff, baseRef: suppliedBaseRef };
2104
+ }
2105
+ if (hasFiles) return { kind: "files", files: args.files.map(requireSafePath) };
2106
+ if (hasSymbols) return { kind: "symbols", symbolIds: [...args.symbols] };
2107
+ return { kind: "git", root, baseRef: suppliedBaseRef ?? "HEAD" };
2108
+ }
2109
+ function requireSafePath(raw) {
2110
+ const value = raw.replace(/\\/g, "/").trim();
2111
+ if (value.length === 0) throw new Error("file path must not be empty");
2112
+ if (value.startsWith("/") || /^[A-Za-z]:\//.test(value)) {
2113
+ throw new Error(`absolute file path not allowed: ${raw}`);
2114
+ }
2115
+ if (/(^|\/)\.\.(\/|$)/.test(value)) throw new Error(`path must stay inside the repo: ${raw}`);
2116
+ return value.replace(/^\.\//, "");
2117
+ }
2118
+ function clamp(value, lo, hi, fallback) {
2119
+ if (value === void 0 || !Number.isFinite(value)) return fallback;
2120
+ return Math.min(hi, Math.max(lo, Math.floor(value)));
2121
+ }
2122
+ function weakest(labels) {
2123
+ let out = "exact";
2124
+ for (const label of labels) {
2125
+ if (LABEL_STRENGTH[label] < LABEL_STRENGTH[out]) out = label;
2126
+ }
2127
+ return out;
2128
+ }
2129
+ function symbolLabel(symbol) {
2130
+ return `${symbol.kind} ${symbol.signature || symbol.name}`;
2131
+ }
2132
+ async function buildChangeContext(index, input, options = {}) {
2133
+ const maxDepth = clamp(options.maxDepth, 1, 6, 3);
2134
+ const maxImpact = clamp(options.maxImpact, 1, 200, 40);
2135
+ const maxPaths = clamp(options.maxPaths, 1, 50, 10);
2136
+ const maxTests = clamp(options.maxTests, 1, 100, 20);
2137
+ const graph = buildReferenceGraph(index);
2138
+ const moduleGraph = buildModuleGraph(index);
2139
+ const filesByPath = new Map(index.files.map((file) => [file.path, file]));
2140
+ const fileSet = new Set(filesByPath.keys());
2141
+ const incomingCount = /* @__PURE__ */ new Map();
2142
+ for (const [id, edges] of graph.incoming) incomingCount.set(id, edges.length);
2143
+ const warnings = [];
2144
+ const changedByKey = /* @__PURE__ */ new Map();
2145
+ const unmapped = [];
2146
+ const changedFiles = /* @__PURE__ */ new Set();
2147
+ const commandRoot = input.kind === "git" || input.kind === "diff" ? input.root : index.root;
2148
+ if (path7.resolve(commandRoot) !== path7.resolve(index.root)) {
2149
+ warnings.push(
2150
+ `command root ${path7.resolve(commandRoot)} differs from indexed root ${index.root}; using the cached index`
2151
+ );
2152
+ }
2153
+ const addChanged = (symbol, change, side) => {
2154
+ const key = `${side}:${symbol.id}`;
2155
+ if (!changedByKey.has(key)) changedByKey.set(key, { symbol, change, side, resolution: "exact" });
2156
+ };
2157
+ const addUnmapped = (file, side, ranges, reason) => {
2158
+ if (ranges.length > 0) unmapped.push({ file, side, ranges, reason });
2159
+ };
2160
+ let baseRef;
2161
+ if (input.kind === "files") {
2162
+ for (const filePath of input.files) {
2163
+ const file = filesByPath.get(filePath);
2164
+ if (!file) {
2165
+ warnings.push(`file not in index: ${filePath}`);
2166
+ continue;
2167
+ }
2168
+ changedFiles.add(file.path);
2169
+ for (const symbol of file.symbols) addChanged(symbol, "modified", "current");
2170
+ if (file.symbols.length === 0) warnings.push(`no indexed symbols in changed file: ${filePath}`);
2171
+ }
2172
+ } else if (input.kind === "symbols") {
2173
+ for (const id of input.symbolIds) {
2174
+ const symbol = graph.symbolsById.get(id);
2175
+ if (!symbol) {
2176
+ warnings.push(`symbol not in index: ${id}`);
2177
+ continue;
2178
+ }
2179
+ changedFiles.add(symbol.file);
2180
+ addChanged(symbol, "modified", "current");
2181
+ }
2182
+ } else {
2183
+ let text;
2184
+ if (input.kind === "git") {
2185
+ baseRef = input.baseRef;
2186
+ text = await readWorkingTreeDiff(input.root, input.baseRef);
2187
+ } else {
2188
+ text = input.text;
2189
+ baseRef = input.baseRef;
2190
+ }
2191
+ const changes = parseUnifiedDiff(text).map((change) => ({
2192
+ ...change,
2193
+ oldPath: change.oldPath === null ? null : requireSafePath(change.oldPath),
2194
+ newPath: change.newPath === null ? null : requireSafePath(change.newPath)
2195
+ }));
2196
+ if (changes.length === 0) warnings.push("no changes found in the diff");
2197
+ const baseSymbolsCache = /* @__PURE__ */ new Map();
2198
+ const readBaseSymbols = (filePath) => {
2199
+ const cached = baseSymbolsCache.get(filePath);
2200
+ if (cached) return cached;
2201
+ const pending = (async () => {
2202
+ if (baseRef === void 0) return null;
2203
+ const content = await readGitFileAtRef(commandRoot, baseRef, filePath);
2204
+ if (content === null) return null;
2205
+ const lang = languageForFile(filePath);
2206
+ if (!lang) return null;
2207
+ const extracted = await extractAll(content, lang, filePath);
2208
+ return extracted.symbols;
2209
+ })();
2210
+ baseSymbolsCache.set(filePath, pending);
2211
+ return pending;
2212
+ };
2213
+ const symbolsByPath = new Map(index.files.map((file) => [file.path, file.symbols]));
2214
+ await mapDiffChanges(
2215
+ changes,
2216
+ symbolsByPath,
2217
+ addChanged,
2218
+ addUnmapped,
2219
+ changedFiles,
2220
+ readBaseSymbols,
2221
+ warnings
2222
+ );
2223
+ }
2224
+ const changedSymbols = [...changedByKey.values()].sort(compareChanged);
2225
+ const callerById = /* @__PURE__ */ new Map();
2226
+ for (const changed of changedSymbols) {
2227
+ for (const edge of graph.incoming.get(changed.symbol.id) ?? []) {
2228
+ if (edge.sourceId === null) continue;
2229
+ const caller = graph.symbolsById.get(edge.sourceId);
2230
+ if (!caller) continue;
2231
+ const row = {
2232
+ symbol: caller,
2233
+ depth: 1,
2234
+ via: hopOf(edge),
2235
+ resolution: edge.resolution
2236
+ };
2237
+ const existing = callerById.get(caller.id);
2238
+ if (!existing || LABEL_STRENGTH[row.resolution] > LABEL_STRENGTH[existing.resolution]) {
2239
+ callerById.set(caller.id, row);
2240
+ }
2241
+ }
2242
+ }
2243
+ const allCallers = [...callerById.values()].sort(compareBySymbol);
2244
+ const changedIds = new Set(changedSymbols.map((changed) => changed.symbol.id));
2245
+ const impactById = /* @__PURE__ */ new Map();
2246
+ {
2247
+ const depthById = /* @__PURE__ */ new Map();
2248
+ const strengthById = /* @__PURE__ */ new Map();
2249
+ for (const id of changedIds) {
2250
+ depthById.set(id, 0);
2251
+ strengthById.set(id, "exact");
2252
+ }
2253
+ let frontier = [...changedIds];
2254
+ for (let depth = 0; depth < maxDepth; depth++) {
2255
+ const next = [];
2256
+ for (const id of frontier) {
2257
+ const pathStrength = strengthById.get(id) ?? "exact";
2258
+ for (const edge of graph.incoming.get(id) ?? []) {
2259
+ if (edge.sourceId === null || changedIds.has(edge.sourceId)) continue;
2260
+ const caller = graph.symbolsById.get(edge.sourceId);
2261
+ if (!caller) continue;
2262
+ const cumulative = weakest([pathStrength, edge.resolution]);
2263
+ const row = {
2264
+ symbol: caller,
2265
+ depth: depth + 1,
2266
+ via: hopOf(edge),
2267
+ resolution: cumulative
2268
+ };
2269
+ const seen = depthById.get(edge.sourceId);
2270
+ if (seen === void 0) {
2271
+ depthById.set(edge.sourceId, depth + 1);
2272
+ strengthById.set(edge.sourceId, cumulative);
2273
+ next.push(edge.sourceId);
2274
+ if (depth + 1 >= 2) impactById.set(edge.sourceId, row);
2275
+ } else if (seen === depth + 1) {
2276
+ const previous = strengthById.get(edge.sourceId) ?? "exact";
2277
+ if (LABEL_STRENGTH[cumulative] > LABEL_STRENGTH[previous]) {
2278
+ strengthById.set(edge.sourceId, cumulative);
2279
+ if (depth + 1 >= 2) impactById.set(edge.sourceId, row);
2280
+ }
2281
+ }
2282
+ }
2283
+ }
2284
+ frontier = next;
2285
+ }
2286
+ }
2287
+ const allImpact = [...impactById.values()].sort(compareByDepthThenSymbol);
2288
+ const allPaths = [];
2289
+ const pathKey = /* @__PURE__ */ new Set();
2290
+ for (const changed of changedSymbols) {
2291
+ for (const found of pathsToEntryPoints(changed.symbol, graph, incomingCount, maxDepth)) {
2292
+ const key = `${found.entry.id}\0${found.changed.id}`;
2293
+ if (pathKey.has(key)) continue;
2294
+ pathKey.add(key);
2295
+ allPaths.push(found);
2296
+ }
2297
+ }
2298
+ allPaths.sort(comparePaths);
2299
+ const importDependents = collectImportDependents(index, moduleGraph, fileSet, changedFiles);
2300
+ const allTests = collectAffectedTests(
2301
+ index,
2302
+ moduleGraph,
2303
+ changedFiles,
2304
+ changedSymbols,
2305
+ [...allCallers, ...allImpact]
2306
+ );
2307
+ const truncated = {
2308
+ callers: allCallers.length > maxImpact,
2309
+ dependents: importDependents.length > maxImpact,
2310
+ paths: allPaths.length > maxPaths,
2311
+ impact: allImpact.length > maxImpact,
2312
+ tests: allTests.length > maxTests
2313
+ };
2314
+ return {
2315
+ root: index.root,
2316
+ source: input.kind,
2317
+ ...baseRef !== void 0 ? { baseRef } : {},
2318
+ changed: changedSymbols,
2319
+ unmapped: unmapped.sort(compareUnmapped),
2320
+ directCallers: allCallers.slice(0, maxImpact),
2321
+ importDependents: importDependents.slice(0, maxImpact),
2322
+ paths: allPaths.slice(0, maxPaths),
2323
+ impact: allImpact.slice(0, maxImpact),
2324
+ tests: allTests.slice(0, maxTests),
2325
+ warnings,
2326
+ truncated
2327
+ };
2328
+ }
2329
+ function hopOf(edge) {
2330
+ return {
2331
+ fromId: edge.sourceId,
2332
+ toId: edge.targetId,
2333
+ callSite: edge.callSite,
2334
+ resolution: edge.resolution,
2335
+ reason: edge.provenance
2336
+ };
2337
+ }
2338
+ async function mapDiffChanges(changes, symbolsByPath, addChanged, addUnmapped, changedFiles, readBaseSymbols, warnings) {
2339
+ for (const change of changes) {
2340
+ const { oldPath, newPath, hunks } = change;
2341
+ if (newPath) changedFiles.add(newPath);
2342
+ if (oldPath) changedFiles.add(oldPath);
2343
+ const currentSymbols = newPath !== null && change.status !== "deleted" ? symbolsByPath.get(newPath) : void 0;
2344
+ if (newPath !== null && change.status !== "deleted") {
2345
+ const currentRanges = hunks.filter((hunk) => hunk.newLines > 0).map((hunk) => ({
2346
+ startLine: hunk.newStart,
2347
+ endLine: hunk.newStart + Math.max(hunk.newLines, 1) - 1
2348
+ }));
2349
+ if (currentRanges.length > 0 && currentSymbols) {
2350
+ for (const range of currentRanges) {
2351
+ for (const symbol of currentSymbols) {
2352
+ if (intersects(symbol, range)) addChanged(symbol, change.status, "current");
2353
+ }
2354
+ }
2355
+ const missed = currentRanges.filter(
2356
+ (range) => !currentSymbols.some((symbol) => intersects(symbol, range))
2357
+ );
2358
+ addUnmapped(newPath, "current", missed, "outside-symbol");
2359
+ } else if (currentRanges.length > 0) {
2360
+ addUnmapped(newPath, "current", currentRanges, "outside-symbol");
2361
+ }
2362
+ if (change.status === "renamed" && hunks.length === 0) {
2363
+ for (const symbol of currentSymbols ?? []) addChanged(symbol, "renamed", "current");
2364
+ if (oldPath !== null) {
2365
+ const oldSymbols = await readBaseSymbols(oldPath);
2366
+ if (oldSymbols === null) {
2367
+ warnings.push(`rename baseline unavailable: ${oldPath}`);
2368
+ } else {
2369
+ for (const symbol of oldSymbols) addChanged(symbol, "renamed", "base");
2370
+ }
2371
+ }
2372
+ }
2373
+ }
2374
+ const deletionHunks = hunks.filter((hunk) => hunk.newLines === 0);
2375
+ if (deletionHunks.length > 0 && oldPath !== null) {
2376
+ const oldRanges = deletionHunks.map((hunk) => ({
2377
+ startLine: hunk.oldStart,
2378
+ endLine: hunk.oldStart + Math.max(hunk.oldLines, 1) - 1
2379
+ }));
2380
+ const oldSymbols = await readBaseSymbols(oldPath);
2381
+ if (oldSymbols === null) {
2382
+ addUnmapped(oldPath, "base", oldRanges, "base-content-unavailable");
2383
+ } else {
2384
+ const baseChange = change.status === "deleted" ? "deleted" : "modified";
2385
+ for (const range of oldRanges) {
2386
+ for (const symbol of oldSymbols) {
2387
+ if (intersects(symbol, range)) addChanged(symbol, baseChange, "base");
2388
+ }
2389
+ }
2390
+ const missed = oldRanges.filter(
2391
+ (range) => !oldSymbols.some((symbol) => intersects(symbol, range))
2392
+ );
2393
+ addUnmapped(oldPath, "base", missed, "outside-symbol");
2394
+ }
2395
+ }
2396
+ if (change.status === "renamed" && hunks.length === 0) {
2397
+ warnings.push(`renamed with no content change: ${oldPath} -> ${newPath}`);
2398
+ }
2399
+ }
2400
+ }
2401
+ function intersects(symbol, range) {
2402
+ return symbol.line <= range.endLine && symbol.endLine >= range.startLine;
2403
+ }
2404
+ function pathsToEntryPoints(changed, graph, incomingCount, maxDepth) {
2405
+ const isEntry = (symbol) => {
2406
+ if (ENTRY_FILE_RE.test(symbol.file)) return true;
2407
+ if (!symbol.exported) return false;
2408
+ if (symbol.kind === "variable") {
2409
+ return symbol.scope.length === 0 || (incomingCount.get(symbol.id) ?? 0) === 0;
2410
+ }
2411
+ if (ENTRY_KINDS.has(symbol.kind)) return true;
2412
+ return (incomingCount.get(symbol.id) ?? 0) === 0;
2413
+ };
2414
+ const found = [];
2415
+ const depthById = /* @__PURE__ */ new Map([[changed.id, 0]]);
2416
+ const strengthById = /* @__PURE__ */ new Map([[changed.id, "exact"]]);
2417
+ const nextEdge = /* @__PURE__ */ new Map();
2418
+ const entryIds = /* @__PURE__ */ new Set();
2419
+ const queue = [changed.id];
2420
+ while (queue.length > 0) {
2421
+ const node = queue.shift();
2422
+ const nodeDepth = depthById.get(node) ?? 0;
2423
+ if (nodeDepth >= maxDepth) continue;
2424
+ const nodeStrength = strengthById.get(node) ?? "exact";
2425
+ for (const edge of graph.incoming.get(node) ?? []) {
2426
+ if (edge.sourceId === null) continue;
2427
+ const candidateDepth = nodeDepth + 1;
2428
+ const cumulative = weakest([nodeStrength, edge.resolution]);
2429
+ const existingDepth = depthById.get(edge.sourceId);
2430
+ if (existingDepth === void 0) {
2431
+ depthById.set(edge.sourceId, candidateDepth);
2432
+ strengthById.set(edge.sourceId, cumulative);
2433
+ nextEdge.set(edge.sourceId, hopOf(edge));
2434
+ queue.push(edge.sourceId);
2435
+ } else if (existingDepth === candidateDepth) {
2436
+ const existingStrength = strengthById.get(edge.sourceId) ?? "exact";
2437
+ if (LABEL_STRENGTH[cumulative] > LABEL_STRENGTH[existingStrength]) {
2438
+ strengthById.set(edge.sourceId, cumulative);
2439
+ nextEdge.set(edge.sourceId, hopOf(edge));
2440
+ queue.push(edge.sourceId);
2441
+ }
2442
+ }
2443
+ const entry = graph.symbolsById.get(edge.sourceId);
2444
+ if (entry && isEntry(entry)) entryIds.add(edge.sourceId);
2445
+ }
2446
+ }
2447
+ for (const entryId of entryIds) {
2448
+ const entry = graph.symbolsById.get(entryId);
2449
+ if (!entry) continue;
2450
+ const hops = [];
2451
+ let cursor = entryId;
2452
+ const guard = /* @__PURE__ */ new Set();
2453
+ while (cursor !== changed.id && !guard.has(cursor)) {
2454
+ guard.add(cursor);
2455
+ const hop = nextEdge.get(cursor);
2456
+ if (!hop) break;
2457
+ hops.push(hop);
2458
+ cursor = hop.toId;
2459
+ }
2460
+ if (cursor === changed.id && hops.length > 0) {
2461
+ found.push({
2462
+ entry,
2463
+ changed,
2464
+ hops,
2465
+ resolution: weakest(hops.map((hop) => hop.resolution))
2466
+ });
2467
+ }
2468
+ }
2469
+ return found;
2470
+ }
2471
+ function collectImportDependents(index, moduleGraph, fileSet, changedFiles) {
2472
+ const filesByPath = new Map(index.files.map((file) => [file.path, file]));
2473
+ const reverse = /* @__PURE__ */ new Map();
2474
+ for (const [from, targets] of moduleGraph.edges) {
2475
+ for (const target of targets) {
2476
+ const importers = reverse.get(target) ?? /* @__PURE__ */ new Set();
2477
+ importers.add(from);
2478
+ reverse.set(target, importers);
2479
+ }
2480
+ }
2481
+ const augmented = /* @__PURE__ */ new Set([...fileSet, ...changedFiles]);
2482
+ let reverseImports = null;
2483
+ const reverseImportIndex = () => {
2484
+ if (reverseImports) return reverseImports;
2485
+ const map = /* @__PURE__ */ new Map();
2486
+ for (const file of index.files) {
2487
+ for (const info of file.importDetails ?? []) {
2488
+ const target = resolveImport(info.specifier, file.path, augmented);
2489
+ if (target === null) continue;
2490
+ const importers = map.get(target) ?? /* @__PURE__ */ new Set();
2491
+ importers.add(file.path);
2492
+ map.set(target, importers);
2493
+ }
2494
+ }
2495
+ reverseImports = map;
2496
+ return map;
2497
+ };
2498
+ const dependents = [];
2499
+ const seen = /* @__PURE__ */ new Set();
2500
+ for (const changedFile of [...changedFiles].sort((a, b) => a.localeCompare(b))) {
2501
+ const importers = new Set(reverse.get(changedFile) ?? []);
2502
+ let resolutionSet = fileSet;
2503
+ if (!fileSet.has(changedFile)) {
2504
+ resolutionSet = augmented;
2505
+ for (const importerPath of reverseImportIndex().get(changedFile) ?? []) {
2506
+ importers.add(importerPath);
2507
+ }
2508
+ }
2509
+ for (const importerPath of [...importers].sort((a, b) => a.localeCompare(b))) {
2510
+ const importer = filesByPath.get(importerPath);
2511
+ if (!importer) continue;
2512
+ const specifier = specifierFor(importer, changedFile, resolutionSet);
2513
+ if (specifier === null) continue;
2514
+ const key = `${importerPath}\0${changedFile}\0${specifier}`;
2515
+ if (seen.has(key)) continue;
2516
+ seen.add(key);
2517
+ dependents.push({
2518
+ file: importerPath,
2519
+ changedFile,
2520
+ specifier,
2521
+ resolution: "import-scoped"
2522
+ });
2523
+ }
2524
+ }
2525
+ return dependents.sort(
2526
+ (a, b) => a.file.localeCompare(b.file) || a.changedFile.localeCompare(b.changedFile) || a.specifier.localeCompare(b.specifier)
2527
+ );
2528
+ }
2529
+ function specifierFor(file, target, fileSet) {
2530
+ for (const info of file.importDetails ?? []) {
2531
+ if (resolveImport(info.specifier, file.path, fileSet) === target) return info.specifier;
2532
+ }
2533
+ for (const spec of file.imports ?? []) {
2534
+ if (resolveImport(spec, file.path, fileSet) === target) return spec;
2535
+ }
2536
+ return null;
2537
+ }
2538
+ var TEST_RANK = {
2539
+ "changed-test": 0,
2540
+ "imports-changed-file": 1,
2541
+ "imports-impact-file": 2,
2542
+ "references-symbol-name": 3,
2543
+ "path-convention": 4
2544
+ };
2545
+ function collectAffectedTests(index, moduleGraph, changedFiles, changedSymbols, impactRows) {
2546
+ const changedNames = new Set(changedSymbols.map((changed) => changed.symbol.name));
2547
+ const impactFileDepth = /* @__PURE__ */ new Map();
2548
+ for (const row of impactRows) {
2549
+ const previous = impactFileDepth.get(row.symbol.file);
2550
+ if (previous === void 0 || row.depth < previous) {
2551
+ impactFileDepth.set(row.symbol.file, row.depth);
2552
+ }
2553
+ }
2554
+ const changedStems = new Set([...changedFiles].map(pathStem));
2555
+ const tests = [];
2556
+ for (const file of index.files) {
2557
+ if (!TEST_RE2.test(file.path)) continue;
2558
+ const imports = moduleGraph.edges.get(file.path) ?? /* @__PURE__ */ new Set();
2559
+ if (changedFiles.has(file.path)) {
2560
+ tests.push({ file: file.path, resolution: "exact", reason: "changed-test", distance: 0 });
2561
+ continue;
2562
+ }
2563
+ const changedImport = [...imports].find(
2564
+ (target) => changedFiles.has(target) && target !== file.path
2565
+ );
2566
+ if (changedImport !== void 0) {
2567
+ tests.push({
2568
+ file: file.path,
2569
+ resolution: "import-scoped",
2570
+ reason: "imports-changed-file",
2571
+ distance: 1
2572
+ });
2573
+ continue;
2574
+ }
2575
+ const impactImports = [...imports].filter((target) => impactFileDepth.has(target)).sort((a, b) => (impactFileDepth.get(a) ?? 0) - (impactFileDepth.get(b) ?? 0));
2576
+ if (impactImports.length > 0) {
2577
+ tests.push({
2578
+ file: file.path,
2579
+ resolution: "import-scoped",
2580
+ reason: "imports-impact-file",
2581
+ distance: impactFileDepth.get(impactImports[0]) ?? 2
2582
+ });
2583
+ continue;
2584
+ }
2585
+ if ((file.calls ?? []).some((call) => changedNames.has(call.name))) {
2586
+ tests.push({
2587
+ file: file.path,
2588
+ resolution: "name-only",
2589
+ reason: "references-symbol-name",
2590
+ distance: 1
2591
+ });
2592
+ continue;
2593
+ }
2594
+ if (changedStems.has(pathStem(file.path))) {
2595
+ tests.push({ file: file.path, resolution: "name-only", reason: "path-convention" });
2596
+ }
2597
+ }
2598
+ return tests.sort(
2599
+ (a, b) => TEST_RANK[a.reason] - TEST_RANK[b.reason] || (a.distance ?? Number.MAX_SAFE_INTEGER) - (b.distance ?? Number.MAX_SAFE_INTEGER) || a.file.localeCompare(b.file)
2600
+ );
2601
+ }
2602
+ function pathStem(filePath) {
2603
+ const base = filePath.split("/").pop() ?? filePath;
2604
+ return base.replace(/\.[A-Za-z0-9]+$/, "").replace(/\.(?:test|spec)$/, "").replace(/^test_/, "").replace(/_test$/, "");
2605
+ }
2606
+ function compareChanged(a, b) {
2607
+ return a.side.localeCompare(b.side) || a.symbol.file.localeCompare(b.symbol.file) || a.symbol.line - b.symbol.line || a.symbol.id.localeCompare(b.symbol.id);
2608
+ }
2609
+ function compareBySymbol(a, b) {
2610
+ return a.symbol.file.localeCompare(b.symbol.file) || a.symbol.line - b.symbol.line || a.symbol.id.localeCompare(b.symbol.id);
2611
+ }
2612
+ function compareByDepthThenSymbol(a, b) {
2613
+ return a.depth - b.depth || LABEL_STRENGTH[b.resolution] - LABEL_STRENGTH[a.resolution] || compareBySymbol(a, b);
2614
+ }
2615
+ function comparePaths(a, b) {
2616
+ return a.entry.file.localeCompare(b.entry.file) || a.entry.line - b.entry.line || a.changed.file.localeCompare(b.changed.file) || a.changed.line - b.changed.line || a.entry.id.localeCompare(b.entry.id);
2617
+ }
2618
+ function compareUnmapped(a, b) {
2619
+ return a.file.localeCompare(b.file) || a.side.localeCompare(b.side);
2620
+ }
2621
+ function renderChangeContext(result, maxChars = 12e3) {
2622
+ const sections = [
2623
+ {
2624
+ title: "CHANGED",
2625
+ rows: result.changed.map(
2626
+ (changed) => `${changed.change} ${changed.side === "base" ? "base " : ""}${symbolLabel(changed.symbol)} ${changed.symbol.file}:${changed.symbol.line} [${changed.resolution}]`
2627
+ )
2628
+ },
2629
+ {
2630
+ title: "DIRECT CALLERS",
2631
+ rows: result.directCallers.map(
2632
+ (row) => `${symbolLabel(row.symbol)} ${row.symbol.file}:${row.symbol.line} (called at ${row.via.callSite.file}:${row.via.callSite.line}) [${row.resolution}]`
2633
+ )
2634
+ },
2635
+ {
2636
+ title: "IMPORT DEPENDENTS",
2637
+ rows: result.importDependents.map(
2638
+ (dependent) => `${dependent.file} imports ${dependent.changedFile} ('${dependent.specifier}') [${dependent.resolution}]`
2639
+ )
2640
+ },
2641
+ {
2642
+ title: "SHORTEST PATHS TO ENTRY POINTS",
2643
+ rows: result.paths.map(
2644
+ (symbolPath) => `${symbolPath.entry.file}:${symbolPath.entry.line} ${symbolLabel(symbolPath.entry)} \u2192 ${symbolPath.changed.file}:${symbolPath.changed.line} ${symbolLabel(symbolPath.changed)} (${symbolPath.hops.length} hop(s)) [${symbolPath.resolution}]`
2645
+ )
2646
+ },
2647
+ {
2648
+ title: "TRANSITIVE IMPACT",
2649
+ rows: result.impact.map(
2650
+ (row) => `depth ${row.depth}: ${symbolLabel(row.symbol)} ${row.symbol.file}:${row.symbol.line} (reached via ${row.via.callSite.file}:${row.via.callSite.line}) [${row.resolution}]`
2651
+ )
2652
+ },
2653
+ {
2654
+ title: "LIKELY AFFECTED TESTS",
2655
+ rows: result.tests.map(
2656
+ (test) => `${test.file} (${test.reason}${test.distance !== void 0 ? `, distance ${test.distance}` : ""}) [${test.resolution}]`
2657
+ )
2658
+ },
2659
+ {
2660
+ title: "UNMAPPED",
2661
+ rows: result.unmapped.map(
2662
+ (entry) => `${entry.file} ${entry.side} lines ${entry.ranges.map((range) => `${range.startLine}-${range.endLine}`).join(", ")} (${entry.reason})`
2663
+ )
2664
+ }
2665
+ ];
2666
+ const summary = `change context \u2014 source: ${result.source}${result.baseRef !== void 0 ? ` (base ${result.baseRef})` : ""}: ${result.changed.length} changed symbol(s)`;
2667
+ const warningLines = result.warnings.map((warning) => `- ${warning}`);
2668
+ const render = (kept) => {
2669
+ const lines = [summary];
2670
+ sections.forEach((section, index) => {
2671
+ if (section.rows.length === 0) {
2672
+ lines.push(`${section.title}: (none)`);
2673
+ return;
2674
+ }
2675
+ lines.push(`${section.title}:`);
2676
+ const take = kept[index] ?? 0;
2677
+ for (let row = 0; row < take; row++) lines.push(` ${section.rows[row]}`);
2678
+ if (take < section.rows.length) lines.push(` \u2026 (${section.rows.length - take} more)`);
2679
+ });
2680
+ lines.push("WARNINGS:");
2681
+ if (warningLines.length === 0) lines.push(" (none)");
2682
+ else for (const warning of warningLines) lines.push(` ${warning}`);
2683
+ return lines.join("\n");
2684
+ };
2685
+ const flattened = [];
2686
+ sections.forEach((section, index) => {
2687
+ section.rows.forEach((text) => flattened.push({ section: index, text }));
2688
+ });
2689
+ const countsFor = (count) => {
2690
+ const kept = sections.map(() => 0);
2691
+ for (let index = 0; index < count; index++) kept[flattened[index].section]++;
2692
+ return kept;
2693
+ };
2694
+ const total = flattened.length;
2695
+ const full = render(sections.map((section) => section.rows.length));
2696
+ if (full.length <= maxChars) return full;
2697
+ const truncationNote = "\n\u2026 truncated rows to fit the character cap (raise maxChars or narrow the change)";
2698
+ const headerOnly = render(countsFor(0));
2699
+ if (headerOnly.length > maxChars) {
2700
+ throw new Error(
2701
+ `maxChars ${maxChars} is below the renderer minimum ${headerOnly.length} (section headers and warnings cannot be kept)`
2702
+ );
2703
+ }
2704
+ if (headerOnly.length + truncationNote.length > maxChars) return headerOnly;
2705
+ const fits = (count) => render(countsFor(count)).length + truncationNote.length <= maxChars;
2706
+ let lo = 0;
2707
+ let hi = total;
2708
+ while (lo < hi) {
2709
+ const mid = Math.ceil((lo + hi) / 2);
2710
+ if (fits(mid)) lo = mid;
2711
+ else hi = mid - 1;
2712
+ }
2713
+ let out = render(countsFor(lo));
2714
+ if (lo < total) out += truncationNote;
2715
+ return out;
2716
+ }
2717
+
2718
+ // src/tools.ts
2719
+ import { defineTool } from "@deepseek-ai/dsh-tools";
2720
+ import path8 from "path";
2721
+
2722
+ // src/config.ts
2723
+ var DEFAULTS = {
2724
+ excludeDirs: [],
2725
+ mapTopFiles: 24,
2726
+ mapMaxChars: 3200,
2727
+ mapTtlMs: 6e4,
2728
+ autoInject: true,
2729
+ codeHealth: false
2730
+ };
2731
+ var state = { current: { ...DEFAULTS } };
2732
+ function applyConfig(partial) {
2733
+ state.current = {
2734
+ ...DEFAULTS,
2735
+ ...partial ?? {},
2736
+ excludeDirs: [...DEFAULTS.excludeDirs, ...partial?.excludeDirs ?? []]
2737
+ };
2738
+ if (!Number.isFinite(state.current.mapTopFiles) || state.current.mapTopFiles < 1) {
2739
+ state.current.mapTopFiles = DEFAULTS.mapTopFiles;
2740
+ }
2741
+ if (!Number.isFinite(state.current.mapMaxChars) || state.current.mapMaxChars < 200) {
2742
+ state.current.mapMaxChars = DEFAULTS.mapMaxChars;
2743
+ }
2744
+ if (!Number.isFinite(state.current.mapTtlMs) || state.current.mapTtlMs < 1e3) {
2745
+ state.current.mapTtlMs = DEFAULTS.mapTtlMs;
2746
+ }
2747
+ }
2748
+ function getConfig() {
2749
+ return state.current;
2750
+ }
2751
+ function indexOptions() {
2752
+ return { excludeDirs: state.current.excludeDirs };
2753
+ }
2754
+
2755
+ // src/tools.ts
2756
+ function createIndexCache(load, ttlMs = 6e4, now = Date.now) {
2757
+ const completed = /* @__PURE__ */ new Map();
2758
+ const inFlight = /* @__PURE__ */ new Map();
2759
+ let epoch = 0;
2760
+ function startLoad(key, root, previous) {
2761
+ const loadEpoch = epoch;
2762
+ const begin = previous ? previous.catch(() => void 0).then(() => load(root)) : load(root);
2763
+ const promise = begin.then((index) => {
2764
+ if (loadEpoch === epoch) {
2765
+ completed.delete(key);
2766
+ completed.set(key, { index, at: now(), epoch: loadEpoch });
2767
+ while (completed.size > 16) completed.delete(completed.keys().next().value);
2768
+ }
2769
+ return index;
2770
+ }).finally(() => {
2771
+ if (inFlight.get(key)?.promise === promise) inFlight.delete(key);
2772
+ });
2773
+ inFlight.set(key, { promise, epoch: loadEpoch });
2774
+ return promise;
2775
+ }
2776
+ return {
2777
+ get(root, force = false) {
2778
+ const resolvedRoot = path8.resolve(root);
2779
+ const key = cacheKeyForRoot(resolvedRoot);
952
2780
  const running = inFlight.get(key);
953
2781
  if (running) {
954
2782
  if (running.epoch === epoch && !force) return running.promise;
@@ -984,13 +2812,45 @@ async function resolveRoot(arg, exec) {
984
2812
  const cwd = exec.agent?.session?.header?.cwd;
985
2813
  const base = arg ?? cwd ?? process.cwd();
986
2814
  const root = await findRepoRoot(base);
987
- if (!root) throw new Error(`no git repository found from ${path6.resolve(base)}`);
2815
+ if (!root) throw new Error(`no git repository found from ${path8.resolve(base)}`);
988
2816
  return root;
989
2817
  }
2818
+ function hitsToSearchMeta(hits) {
2819
+ const byFile = /* @__PURE__ */ new Map();
2820
+ for (const raw of hits) {
2821
+ const hit = raw;
2822
+ if (!hit.file || typeof hit.line !== "number") continue;
2823
+ const label = `${hit.kind ?? ""} ${hit.signature || hit.name || ""}`.trim();
2824
+ const list = byFile.get(hit.file) ?? [];
2825
+ list.push({ lineNumber: hit.line, line: label });
2826
+ byFile.set(hit.file, list);
2827
+ }
2828
+ return {
2829
+ shape: "matches",
2830
+ files: [...byFile.entries()].map(([path9, matches]) => ({ path: path9, matches })),
2831
+ truncated: false,
2832
+ total: hits.length
2833
+ };
2834
+ }
2835
+ function searchCardFromMeta(meta) {
2836
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0;
2837
+ const record = meta;
2838
+ if (record.shape !== "matches" || !Array.isArray(record.files)) return void 0;
2839
+ const files = record.files.filter(
2840
+ (entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry)
2841
+ );
2842
+ return {
2843
+ card: "search",
2844
+ shape: "matches",
2845
+ files,
2846
+ truncated: record.truncated === true,
2847
+ total: typeof record.total === "number" ? record.total : 0
2848
+ };
2849
+ }
990
2850
  var tools = [
991
2851
  defineTool({
992
2852
  name: "code_index",
993
- description: "Manage the semantic repo index: status or (re)build it for the current workspace. Auto-builds the first time it is queried. Returns file/symbol counts and the index location.",
2853
+ description: "Manage the code index: status or (re)build it for the current workspace. Auto-builds the first time it is queried. Returns file/symbol counts and the index location.",
994
2854
  parameters: {
995
2855
  action: {
996
2856
  type: "string",
@@ -1059,8 +2919,16 @@ var tools = [
1059
2919
  render: (_args, value) => value.map((v) => ({
1060
2920
  type: "text",
1061
2921
  text: renderHit(v)
1062
- }))
2922
+ })),
2923
+ presentationMeta: (_args, value) => hitsToSearchMeta(value)
1063
2924
  },
2925
+ presentCall: (args) => ({
2926
+ card: "generic",
2927
+ title: args.kind ? `List ${args.kind} symbols` : "List symbols",
2928
+ kind: "search",
2929
+ rawInput: { query: args.query, file: args.file, kind: args.kind }
2930
+ }),
2931
+ presentResult: (_args, result) => searchCardFromMeta(result.meta),
1064
2932
  async execute(args, exec) {
1065
2933
  const root = await resolveRoot(args.repoRoot, exec);
1066
2934
  const index = await getIndex(root);
@@ -1095,8 +2963,16 @@ var tools = [
1095
2963
  render: (_args, value) => value.map((v) => ({
1096
2964
  type: "text",
1097
2965
  text: renderHit(v)
1098
- }))
2966
+ })),
2967
+ presentationMeta: (_args, value) => hitsToSearchMeta(value)
1099
2968
  },
2969
+ presentCall: (args) => ({
2970
+ card: "generic",
2971
+ title: `Search "${args.query}"`,
2972
+ kind: "search",
2973
+ rawInput: { query: args.query }
2974
+ }),
2975
+ presentResult: (_args, result) => searchCardFromMeta(result.meta),
1100
2976
  async execute(args, exec) {
1101
2977
  const root = await resolveRoot(args.repoRoot, exec);
1102
2978
  const index = await getIndex(root);
@@ -1132,9 +3008,216 @@ var tools = [
1132
3008
  return `code_index: ${error.message ?? String(error)}`;
1133
3009
  }
1134
3010
  }
3011
+ }),
3012
+ defineTool({
3013
+ name: "code_refs",
3014
+ 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.",
3015
+ parameters: {
3016
+ symbol: {
3017
+ type: "string",
3018
+ required: true,
3019
+ description: "Symbol name to trace (case-sensitive)."
3020
+ },
3021
+ direction: {
3022
+ type: "string",
3023
+ enum: ["callers", "callees", "both"],
3024
+ description: 'Which side of the call graph to return (default "both").'
3025
+ },
3026
+ limit: {
3027
+ type: "number",
3028
+ description: "Max rows per side (default 50)."
3029
+ },
3030
+ repoRoot: {
3031
+ type: "string",
3032
+ description: "Optional absolute repo path; defaults to the session workspace root."
3033
+ }
3034
+ },
3035
+ output: {
3036
+ schema: { type: "string" },
3037
+ render: (_args, value) => [{ type: "text", text: value }]
3038
+ },
3039
+ presentCall: (args) => ({
3040
+ card: "generic",
3041
+ title: `Refs ${args.symbol}`,
3042
+ kind: "search",
3043
+ rawInput: { symbol: args.symbol, direction: args.direction }
3044
+ }),
3045
+ async execute(args, exec) {
3046
+ try {
3047
+ const root = await resolveRoot(args.repoRoot, exec);
3048
+ const index = await getIndex(root);
3049
+ const refs = symbolRefs(index, args.symbol, args.limit ?? 50);
3050
+ const direction = args.direction ?? "both";
3051
+ const lines = [];
3052
+ if (refs.defs.length > 0) {
3053
+ lines.push("definitions:");
3054
+ for (const def of refs.defs) {
3055
+ lines.push(` ${def.kind} ${def.signature || def.name} \u2014 ${def.file}:${def.line}`);
3056
+ }
3057
+ } else {
3058
+ lines.push(`no definition of "${args.symbol}" in this repo`);
3059
+ }
3060
+ if (direction === "callers" || direction === "both") {
3061
+ lines.push("", `callers (${refs.callers.length}):`);
3062
+ if (refs.callers.length === 0) lines.push(" (none)");
3063
+ for (const caller of refs.callers) {
3064
+ lines.push(` ${caller.file}:${caller.line}${caller.from ? ` in ${caller.from}` : ""}`);
3065
+ }
3066
+ }
3067
+ if (direction === "callees" || direction === "both") {
3068
+ lines.push("", `callees (${refs.callees.length}):`);
3069
+ if (refs.callees.length === 0) lines.push(" (none)");
3070
+ for (const callee of refs.callees) {
3071
+ const target = callee.targets[0];
3072
+ lines.push(
3073
+ ` ${callee.name} :${callee.line}${target ? ` \u2192 ${target.file}:${target.line}` : " (external)"}`
3074
+ );
3075
+ }
3076
+ }
3077
+ return lines.join("\n");
3078
+ } catch (error) {
3079
+ return `code_refs: ${error.message ?? String(error)}`;
3080
+ }
3081
+ }
3082
+ }),
3083
+ defineTool({
3084
+ name: "code_change_context",
3085
+ description: "Return the smallest change-aware context for the current diff: changed symbols, direct callers, import dependents, shortest paths to entry points, transitive impact, and likely affected tests. Every relation is labeled exact / import-scoped / name-only, so confidence is explicit. Select the change with an explicit diff, a list of files, a list of symbol ids, or (default) the working tree against baseRef.",
3086
+ parameters: {
3087
+ repoRoot: {
3088
+ type: "string",
3089
+ description: "Optional absolute repo path; defaults to the session workspace root."
3090
+ },
3091
+ diff: {
3092
+ type: "string",
3093
+ description: "Caller-supplied unified diff text; when present no git command runs."
3094
+ },
3095
+ baseRef: {
3096
+ type: "string",
3097
+ description: 'Git ref to diff against (default "HEAD"); also the baseline read for deletions.'
3098
+ },
3099
+ files: {
3100
+ type: "array",
3101
+ items: { type: "string" },
3102
+ description: "Repo-relative files to treat as wholly changed (mutually exclusive with diff/symbols)."
3103
+ },
3104
+ symbols: {
3105
+ type: "array",
3106
+ items: { type: "string" },
3107
+ description: "SymbolInfo ids to treat as changed (mutually exclusive with diff/files)."
3108
+ },
3109
+ maxDepth: {
3110
+ type: "number",
3111
+ description: "Transitive caller depth, 1..6 (default 3)."
3112
+ },
3113
+ maxImpact: {
3114
+ type: "number",
3115
+ description: "Max direct-caller / impact / dependent rows, 1..200 (default 40)."
3116
+ },
3117
+ maxPaths: {
3118
+ type: "number",
3119
+ description: "Max entry-point paths, 1..50 (default 10)."
3120
+ },
3121
+ maxTests: {
3122
+ type: "number",
3123
+ description: "Max affected-test rows, 1..100 (default 20)."
3124
+ }
3125
+ },
3126
+ output: {
3127
+ schema: { type: "string" },
3128
+ render: (_args, value) => [{ type: "text", text: value }]
3129
+ },
3130
+ presentCall: (args) => ({
3131
+ card: "generic",
3132
+ title: "Change context",
3133
+ kind: "search",
3134
+ rawInput: {
3135
+ baseRef: args.baseRef,
3136
+ files: args.files,
3137
+ symbols: args.symbols,
3138
+ diff: args.diff !== void 0 ? "(inline diff)" : void 0
3139
+ }
3140
+ }),
3141
+ async execute(args, exec) {
3142
+ try {
3143
+ const root = await resolveRoot(args.repoRoot, exec);
3144
+ const index = await getIndex(root);
3145
+ const input = changeContextInputFromArgs(args, root);
3146
+ const result = await buildChangeContext(index, input, {
3147
+ maxDepth: args.maxDepth,
3148
+ maxImpact: args.maxImpact,
3149
+ maxPaths: args.maxPaths,
3150
+ maxTests: args.maxTests
3151
+ });
3152
+ return renderChangeContext(result);
3153
+ } catch (error) {
3154
+ return `code_change_context: ${error.message ?? String(error)}`;
3155
+ }
3156
+ }
3157
+ }),
3158
+ defineTool({
3159
+ name: "code_health",
3160
+ 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).",
3161
+ parameters: {
3162
+ limit: {
3163
+ type: "number",
3164
+ description: "Max cycles and orphan rows to list (default 50)."
3165
+ },
3166
+ repoRoot: {
3167
+ type: "string",
3168
+ description: "Optional absolute repo path; defaults to the session workspace root."
3169
+ }
3170
+ },
3171
+ output: {
3172
+ schema: { type: "string" },
3173
+ render: (_args, value) => [{ type: "text", text: value }]
3174
+ },
3175
+ presentCall: () => ({ card: "generic", title: "Code health", kind: "read" }),
3176
+ async execute(args, exec) {
3177
+ try {
3178
+ const root = await resolveRoot(args.repoRoot, exec);
3179
+ const index = await getIndex(root);
3180
+ const limit = args.limit ?? 50;
3181
+ const cycles = findCycles(index);
3182
+ const orphans = findOrphanModules(index);
3183
+ const lines = [`repo health \u2014 ${index.files.length} files`];
3184
+ lines.push("", `circular dependencies (${cycles.length}):`);
3185
+ if (cycles.length === 0) lines.push(" (none)");
3186
+ for (const cycle of cycles.slice(0, limit)) lines.push(` ${cycle.join(" \u2192 ")}`);
3187
+ lines.push("", `orphan modules (${orphans.length}):`);
3188
+ if (orphans.length === 0) lines.push(" (none)");
3189
+ for (const orphan of orphans.slice(0, limit)) lines.push(` ${orphan}`);
3190
+ return lines.join("\n");
3191
+ } catch (error) {
3192
+ return `code_health: ${error.message ?? String(error)}`;
3193
+ }
3194
+ }
1135
3195
  })
1136
3196
  ];
1137
3197
 
3198
+ // src/settings.ts
3199
+ import Schema from "@deepseek-ai/schemastery";
3200
+ var SETTINGS_NAMESPACE = "code-index";
3201
+ var settingsSchema = Schema.object({
3202
+ excludeDirs: Schema.array(Schema.string()).default([]),
3203
+ mapTopFiles: Schema.number().default(24),
3204
+ mapMaxChars: Schema.number().default(3200),
3205
+ mapTtlMs: Schema.number().default(6e4),
3206
+ autoInject: Schema.boolean().default(true),
3207
+ codeHealth: Schema.boolean().default(false)
3208
+ });
3209
+ function registerSettings(getService, pluginConfig, onResolved) {
3210
+ const service = getService("settings");
3211
+ if (!service) return;
3212
+ const scope = service.register(SETTINGS_NAMESPACE, settingsSchema, {
3213
+ base: pluginConfig ?? {}
3214
+ });
3215
+ onResolved(scope.get());
3216
+ scope.watch((next) => {
3217
+ onResolved(next);
3218
+ });
3219
+ }
3220
+
1138
3221
  // src/index.ts
1139
3222
  var name = "dsh-code-index";
1140
3223
  var inject = ["tools", "systemPrompt"];
@@ -1144,7 +3227,17 @@ function apply(ctx, pluginConfig) {
1144
3227
  ctx.effect(() => {
1145
3228
  const disposers = [];
1146
3229
  console.log("[dsh-code-index] plugin loaded");
3230
+ if (ctx.inject) {
3231
+ ctx.inject(["settings"], (settingsCtx) => {
3232
+ registerSettings(
3233
+ (name2) => settingsCtx.get(name2),
3234
+ pluginConfig,
3235
+ (resolved) => applyConfig(resolved)
3236
+ );
3237
+ });
3238
+ }
1147
3239
  for (const tool of tools) {
3240
+ if (tool.name === "code_health" && !getConfig().codeHealth) continue;
1148
3241
  disposers.push(ctx.tools.register(tool));
1149
3242
  console.log(`[dsh-code-index] registered tool: ${tool.name}`);
1150
3243
  }
@@ -1208,18 +3301,29 @@ export {
1208
3301
  DEFAULT_EXCLUDED_DIRS,
1209
3302
  SUPPORTED_EXTS,
1210
3303
  apply,
3304
+ buildChangeContext,
1211
3305
  buildIndex,
1212
3306
  buildIndexWithCache,
3307
+ buildModuleGraph,
3308
+ buildSymbolTable,
3309
+ callerCounts,
3310
+ changeContextInputFromArgs,
1213
3311
  defaultCachePath,
1214
3312
  extractAll,
1215
3313
  extractSymbols,
3314
+ findCycles,
3315
+ findOrphanModules,
1216
3316
  findRepoRoot,
1217
3317
  inject,
1218
3318
  languageForFile,
1219
3319
  loadIndex,
1220
3320
  name,
1221
3321
  parseFileToSymbols,
3322
+ parseUnifiedDiff,
1222
3323
  rankRepoMap,
3324
+ readGitFileAtRef,
3325
+ readWorkingTreeDiff,
3326
+ renderChangeContext,
1223
3327
  renderHit,
1224
3328
  renderRepoMap,
1225
3329
  saveIndex,
@@ -1227,6 +3331,7 @@ export {
1227
3331
  scoreFile,
1228
3332
  searchSymbols,
1229
3333
  symbolCount,
3334
+ symbolRefs,
1230
3335
  tools
1231
3336
  };
1232
3337
  //# sourceMappingURL=index.js.map