dsh-code-index 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/LICENSE +0 -0
- package/README.md +46 -1
- package/README.zh.md +27 -2
- package/cordis.patch.yml +0 -0
- package/dist/client.js +12 -3
- package/dist/client.js.map +1 -1
- package/dist/index.js +2343 -194
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/scripts/fix-wsl-links.mjs +0 -0
- package/scripts/release-smoke.mjs +177 -0
package/dist/index.js
CHANGED
|
@@ -251,14 +251,20 @@ function getLanguage(id) {
|
|
|
251
251
|
}
|
|
252
252
|
return entry;
|
|
253
253
|
}
|
|
254
|
-
|
|
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 = "") {
|
|
255
261
|
const lang = await getLanguage(id);
|
|
256
262
|
const parser = await getParser();
|
|
257
263
|
parser.setLanguage(lang);
|
|
258
264
|
const tree = parser.parse(code);
|
|
259
265
|
if (!tree) throw new Error(`tree-sitter parse returned null for a ${id} source`);
|
|
260
266
|
try {
|
|
261
|
-
const
|
|
267
|
+
const drafts = [];
|
|
262
268
|
const symbolQuery = new Query(lang, QUERIES[id]);
|
|
263
269
|
try {
|
|
264
270
|
const captures = symbolQuery.captures(tree.rootNode);
|
|
@@ -269,61 +275,297 @@ async function extractAll(code, id) {
|
|
|
269
275
|
if (def.kind === "variable" && !isModuleLevelVariable(id, node)) continue;
|
|
270
276
|
const name2 = nameOf(id, node);
|
|
271
277
|
if (!name2) continue;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
281
288
|
});
|
|
282
289
|
}
|
|
283
290
|
} finally {
|
|
284
291
|
symbolQuery.delete();
|
|
285
292
|
}
|
|
286
|
-
const
|
|
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 = [];
|
|
287
298
|
const importQuery = new Query(lang, IMPORT_QUERIES[id]);
|
|
288
299
|
try {
|
|
289
300
|
for (const cap of importQuery.captures(tree.rootNode)) {
|
|
290
|
-
const
|
|
291
|
-
if (spec) imports.push(spec);
|
|
301
|
+
for (const info of importInfoFor(id, cap.node)) importDetails.push(info);
|
|
292
302
|
}
|
|
293
303
|
} finally {
|
|
294
304
|
importQuery.delete();
|
|
295
305
|
}
|
|
306
|
+
const imports = importDetails.map((info) => info.specifier);
|
|
296
307
|
const calls = [];
|
|
297
308
|
const callQuery = new Query(lang, CALL_QUERIES[id]);
|
|
298
309
|
try {
|
|
299
|
-
const
|
|
310
|
+
const seen = /* @__PURE__ */ new Set();
|
|
300
311
|
for (const cap of callQuery.captures(tree.rootNode)) {
|
|
301
312
|
const name2 = cap.node.text.trim();
|
|
302
313
|
if (!name2) continue;
|
|
303
314
|
const line = cap.node.startPosition.row + 1;
|
|
304
|
-
const
|
|
315
|
+
const callable = enclosingCallableNode(id, cap.node);
|
|
316
|
+
const from = callable ? callableNameOf(id, callable) : "";
|
|
317
|
+
const fromId = callable ? nearestDraftId(callable, symbolByNodeId) : null;
|
|
305
318
|
const key = `${from}|${name2}|${line}`;
|
|
306
|
-
if (
|
|
307
|
-
|
|
308
|
-
calls.push({ name: name2, line, from });
|
|
319
|
+
if (seen.has(key)) continue;
|
|
320
|
+
seen.add(key);
|
|
321
|
+
calls.push({ name: name2, line, from, fromId, qualifier: qualifierOf(cap.node) });
|
|
309
322
|
}
|
|
310
323
|
} finally {
|
|
311
324
|
callQuery.delete();
|
|
312
325
|
}
|
|
313
326
|
calls.sort((a, b) => a.line - b.line || a.name.localeCompare(b.name));
|
|
314
|
-
symbols.sort(
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (seen.has(key)) return false;
|
|
319
|
-
seen.add(key);
|
|
320
|
-
return true;
|
|
321
|
-
});
|
|
322
|
-
return { symbols: deduped, imports, calls };
|
|
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 };
|
|
323
331
|
} finally {
|
|
324
332
|
tree.delete();
|
|
325
333
|
}
|
|
326
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
|
+
}
|
|
327
569
|
async function extractSymbols(code, id) {
|
|
328
570
|
return (await extractAll(code, id)).symbols;
|
|
329
571
|
}
|
|
@@ -351,7 +593,7 @@ function cppNameOf(node) {
|
|
|
351
593
|
continue;
|
|
352
594
|
}
|
|
353
595
|
if (current.type === "qualified_identifier") {
|
|
354
|
-
return current
|
|
596
|
+
return cppQualifiedSegments(current).at(-1) ?? "";
|
|
355
597
|
}
|
|
356
598
|
if (current.type === "identifier" || current.type === "field_identifier" || current.type === "type_identifier") {
|
|
357
599
|
return current.text.trim();
|
|
@@ -361,42 +603,251 @@ function cppNameOf(node) {
|
|
|
361
603
|
}
|
|
362
604
|
return "";
|
|
363
605
|
}
|
|
364
|
-
function
|
|
606
|
+
function enclosingCallableNode(id, node) {
|
|
365
607
|
const callable = CALLABLE_NODES[id];
|
|
366
608
|
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
367
|
-
if (
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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() ?? "";
|
|
374
619
|
}
|
|
375
620
|
return "";
|
|
376
621
|
}
|
|
377
|
-
function
|
|
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;
|
|
378
650
|
switch (node.type) {
|
|
379
651
|
case "import_statement":
|
|
380
|
-
|
|
381
|
-
return stripQuotes(node.childForFieldName("source")?.text);
|
|
652
|
+
return id === "python" ? pythonModuleImports(node, line) : tsJsImports(node, line);
|
|
382
653
|
case "export_statement":
|
|
383
|
-
return
|
|
384
|
-
case "import_from_statement":
|
|
385
|
-
|
|
386
|
-
if (raw == null) return null;
|
|
387
|
-
return raw.startsWith(".") ? pythonRelative(raw) : dottedToPath(raw);
|
|
388
|
-
}
|
|
654
|
+
return tsJsReexport(node, line);
|
|
655
|
+
case "import_from_statement":
|
|
656
|
+
return pythonFromImport(node, line);
|
|
389
657
|
case "import_spec":
|
|
390
|
-
return
|
|
658
|
+
return goImport(node, line);
|
|
391
659
|
case "use_declaration":
|
|
392
|
-
return
|
|
660
|
+
return rustUse(node, line);
|
|
393
661
|
case "import_declaration":
|
|
394
|
-
return
|
|
395
|
-
case "preproc_include":
|
|
396
|
-
|
|
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
|
+
}
|
|
397
667
|
default:
|
|
398
|
-
return
|
|
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
|
+
}
|
|
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
|
+
];
|
|
399
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;
|
|
400
851
|
}
|
|
401
852
|
function rustUsePath(text) {
|
|
402
853
|
if (!text) return null;
|
|
@@ -552,9 +1003,9 @@ async function parseFileToSymbols(filePath, repoRoot, code) {
|
|
|
552
1003
|
const lang = languageForFile(filePath);
|
|
553
1004
|
if (!lang) return [];
|
|
554
1005
|
const text = code ?? await readFile(filePath, "utf8");
|
|
555
|
-
const rows = await extractSymbols(text, lang);
|
|
556
1006
|
const rel = path.relative(repoRoot, filePath).split(path.sep).join("/");
|
|
557
|
-
|
|
1007
|
+
const { symbols } = await extractAll(text, lang, rel);
|
|
1008
|
+
return symbols;
|
|
558
1009
|
}
|
|
559
1010
|
|
|
560
1011
|
// src/scan.ts
|
|
@@ -638,6 +1089,14 @@ async function scanRepo(root, options = {}) {
|
|
|
638
1089
|
import { mkdir, readFile as readFile2, rename, rm, writeFile } from "fs/promises";
|
|
639
1090
|
import path3 from "path";
|
|
640
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
|
|
641
1100
|
var CACHE_DIR_NAME = ".dsh-code-index";
|
|
642
1101
|
function cacheKeyForRoot(root) {
|
|
643
1102
|
const resolved = path3.resolve(root);
|
|
@@ -658,6 +1117,7 @@ async function loadIndex(cachePath) {
|
|
|
658
1117
|
const raw = await readFile2(cachePath, "utf8");
|
|
659
1118
|
const parsed = JSON.parse(raw);
|
|
660
1119
|
if (!parsed || typeof parsed.root !== "string" || !Array.isArray(parsed.files)) return null;
|
|
1120
|
+
if (parsed.schemaVersion !== REPO_INDEX_SCHEMA_VERSION) return null;
|
|
661
1121
|
return healSymbolFiles(parsed);
|
|
662
1122
|
} catch {
|
|
663
1123
|
return null;
|
|
@@ -709,13 +1169,14 @@ async function buildIndex(root, options = {}, previous = null) {
|
|
|
709
1169
|
} catch {
|
|
710
1170
|
return null;
|
|
711
1171
|
}
|
|
712
|
-
const { symbols, imports, calls } = await extractAll(code, lang);
|
|
1172
|
+
const { symbols, imports, importDetails, calls } = await extractAll(code, lang, f.rel);
|
|
713
1173
|
return {
|
|
714
1174
|
path: f.rel,
|
|
715
1175
|
lang,
|
|
716
1176
|
mtimeMs: f.mtimeMs,
|
|
717
1177
|
symbols: symbols.map((s) => ({ ...s, file: f.rel })),
|
|
718
1178
|
imports,
|
|
1179
|
+
importDetails,
|
|
719
1180
|
calls
|
|
720
1181
|
};
|
|
721
1182
|
})
|
|
@@ -725,6 +1186,7 @@ async function buildIndex(root, options = {}, previous = null) {
|
|
|
725
1186
|
}
|
|
726
1187
|
}
|
|
727
1188
|
return {
|
|
1189
|
+
schemaVersion: REPO_INDEX_SCHEMA_VERSION,
|
|
728
1190
|
root,
|
|
729
1191
|
generatedAt: Date.now(),
|
|
730
1192
|
files,
|
|
@@ -752,6 +1214,7 @@ async function buildIndexWithCache(root, options = {}, cacheDir) {
|
|
|
752
1214
|
return fresh;
|
|
753
1215
|
}
|
|
754
1216
|
function indexesEqual(left, right) {
|
|
1217
|
+
if (left.schemaVersion !== right.schemaVersion) return false;
|
|
755
1218
|
if (cacheKeyForRoot(left.root) !== cacheKeyForRoot(right.root)) return false;
|
|
756
1219
|
if (left.files.length !== right.files.length) return false;
|
|
757
1220
|
if (left.excludedDirs.length !== right.excludedDirs.length) return false;
|
|
@@ -763,9 +1226,32 @@ function indexesEqual(left, right) {
|
|
|
763
1226
|
}
|
|
764
1227
|
if (file.symbols.length !== other.symbols.length) return false;
|
|
765
1228
|
if (!callsEqual(file.calls, other.calls)) return false;
|
|
1229
|
+
if (!importsEqual(file.importDetails, other.importDetails)) return false;
|
|
766
1230
|
return file.symbols.every((symbol, symbolIndex) => {
|
|
767
1231
|
const candidate = other.symbols[symbolIndex];
|
|
768
|
-
return candidate !== void 0 && symbol.name === candidate.name && symbol.kind === candidate.kind && symbol.file === candidate.file && symbol.line === candidate.line && symbol.endLine === candidate.endLine && symbol.exported === candidate.exported && symbol.signature === candidate.signature;
|
|
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;
|
|
769
1255
|
});
|
|
770
1256
|
});
|
|
771
1257
|
}
|
|
@@ -775,7 +1261,7 @@ function callsEqual(left, right) {
|
|
|
775
1261
|
if (a.length !== b.length) return false;
|
|
776
1262
|
return a.every((call, index) => {
|
|
777
1263
|
const other = b[index];
|
|
778
|
-
return other !== void 0 && call.name === other.name && call.line === other.line && call.from === other.from;
|
|
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;
|
|
779
1265
|
});
|
|
780
1266
|
}
|
|
781
1267
|
async function findRepoRoot(startDir, maxLevels = 12) {
|
|
@@ -793,107 +1279,6 @@ async function findRepoRoot(startDir, maxLevels = 12) {
|
|
|
793
1279
|
return null;
|
|
794
1280
|
}
|
|
795
1281
|
|
|
796
|
-
// src/types.ts
|
|
797
|
-
function symbolCount(index) {
|
|
798
|
-
return index.files.reduce((n, f) => n + f.symbols.length, 0);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// src/refgraph.ts
|
|
802
|
-
function buildSymbolTable(index) {
|
|
803
|
-
const table = /* @__PURE__ */ new Map();
|
|
804
|
-
for (const file of index.files) {
|
|
805
|
-
for (const symbol of file.symbols) {
|
|
806
|
-
const list = table.get(symbol.name);
|
|
807
|
-
if (list) list.push(symbol);
|
|
808
|
-
else table.set(symbol.name, [symbol]);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
for (const list of table.values()) {
|
|
812
|
-
list.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
813
|
-
}
|
|
814
|
-
return table;
|
|
815
|
-
}
|
|
816
|
-
function callerCounts(index) {
|
|
817
|
-
const counts = /* @__PURE__ */ new Map();
|
|
818
|
-
for (const file of index.files) {
|
|
819
|
-
for (const call of file.calls ?? []) {
|
|
820
|
-
counts.set(call.name, (counts.get(call.name) ?? 0) + 1);
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
return counts;
|
|
824
|
-
}
|
|
825
|
-
function symbolRefs(index, symbol, limit = 200) {
|
|
826
|
-
const table = buildSymbolTable(index);
|
|
827
|
-
const defs = table.get(symbol) ?? [];
|
|
828
|
-
const callers = [];
|
|
829
|
-
const calleeFirstLine = /* @__PURE__ */ new Map();
|
|
830
|
-
for (const file of index.files) {
|
|
831
|
-
for (const call of file.calls ?? []) {
|
|
832
|
-
if (call.name === symbol) {
|
|
833
|
-
callers.push({ file: file.path, line: call.line, from: call.from });
|
|
834
|
-
}
|
|
835
|
-
if (call.from === symbol && !calleeFirstLine.has(call.name)) {
|
|
836
|
-
calleeFirstLine.set(call.name, call.line);
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
callers.sort(
|
|
841
|
-
(a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.from.localeCompare(b.from)
|
|
842
|
-
);
|
|
843
|
-
const callees = [...calleeFirstLine.entries()].map(([name2, line]) => ({ name: name2, line, targets: table.get(name2) ?? [] })).sort((a, b) => a.line - b.line || a.name.localeCompare(b.name));
|
|
844
|
-
return {
|
|
845
|
-
defs,
|
|
846
|
-
callers: callers.slice(0, limit),
|
|
847
|
-
callees: callees.slice(0, limit)
|
|
848
|
-
};
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
// src/search.ts
|
|
852
|
-
function matchScore(name2, query) {
|
|
853
|
-
const q = query.toLowerCase();
|
|
854
|
-
const n = name2.toLowerCase();
|
|
855
|
-
if (n === q) return 1;
|
|
856
|
-
if (n.startsWith(q)) return 0.8;
|
|
857
|
-
if (n.includes(q)) return 0.5;
|
|
858
|
-
if (q.length >= 3 && isSubsequence(q, n)) return 0.3;
|
|
859
|
-
return 0;
|
|
860
|
-
}
|
|
861
|
-
function isSubsequence(query, name2) {
|
|
862
|
-
let i = 0;
|
|
863
|
-
for (const ch of name2) {
|
|
864
|
-
if (ch === query[i]) i++;
|
|
865
|
-
if (i === query.length) return true;
|
|
866
|
-
}
|
|
867
|
-
return false;
|
|
868
|
-
}
|
|
869
|
-
function searchSymbols(index, filter, limit = 50) {
|
|
870
|
-
const q = (filter.query ?? "").trim();
|
|
871
|
-
const filePat = filter.file?.trim().toLowerCase();
|
|
872
|
-
const hits = [];
|
|
873
|
-
for (const file of index.files) {
|
|
874
|
-
if (filePat && !file.path.toLowerCase().includes(filePat)) continue;
|
|
875
|
-
for (const sym of file.symbols) {
|
|
876
|
-
if (filter.kind && sym.kind !== filter.kind) continue;
|
|
877
|
-
if (filter.exportedOnly && !sym.exported) continue;
|
|
878
|
-
const score = q ? matchScore(sym.name, q) : 0.4;
|
|
879
|
-
if (q && score === 0) continue;
|
|
880
|
-
hits.push({ ...sym, score });
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
const refs = callerCounts(index);
|
|
884
|
-
hits.sort((a, b) => {
|
|
885
|
-
const ab = Number(b.exported) - Number(a.exported) || b.score - a.score || (refs.get(b.name) ?? 0) - (refs.get(a.name) ?? 0) || a.name.localeCompare(b.name) || a.file.localeCompare(b.file);
|
|
886
|
-
return ab;
|
|
887
|
-
});
|
|
888
|
-
return hits.slice(0, limit);
|
|
889
|
-
}
|
|
890
|
-
function renderHit(hit) {
|
|
891
|
-
const exportMark = hit.exported ? "export " : "";
|
|
892
|
-
const sig = hit.signature || hit.name;
|
|
893
|
-
const score = hit.score < 1 ? ` [${hit.score.toFixed(2)}]` : "";
|
|
894
|
-
return `${exportMark}${hit.kind} ${sig}${score} \u2014 ${hit.file}:${hit.line}`;
|
|
895
|
-
}
|
|
896
|
-
|
|
897
1282
|
// src/repomap.ts
|
|
898
1283
|
import path5 from "path";
|
|
899
1284
|
var KIND_WEIGHT = {
|
|
@@ -998,30 +1383,45 @@ function referencePageRank(files, density) {
|
|
|
998
1383
|
for (let i = 0; i < n; i++) result.set(nodes[i], rank[i]);
|
|
999
1384
|
return result;
|
|
1000
1385
|
}
|
|
1001
|
-
function
|
|
1002
|
-
if (!spec) return null;
|
|
1386
|
+
function resolveImportDetailed(spec, fromPath, fileSet) {
|
|
1387
|
+
if (!spec) return { target: null, suffixSkipped: false };
|
|
1003
1388
|
if (spec.startsWith("./") || spec.startsWith("../")) {
|
|
1004
1389
|
const base = path5.posix.normalize(path5.posix.join(path5.posix.dirname(fromPath), spec));
|
|
1005
|
-
return candidateFor(base, fileSet);
|
|
1390
|
+
return { target: candidateFor(base, fileSet), suffixSkipped: false };
|
|
1006
1391
|
}
|
|
1007
1392
|
const segments = spec.split("/").filter(Boolean);
|
|
1008
1393
|
for (let skip = 0; skip < segments.length; skip++) {
|
|
1009
1394
|
const hit = candidateFor(segments.slice(skip).join("/"), fileSet);
|
|
1010
|
-
if (hit) return hit;
|
|
1395
|
+
if (hit) return { target: hit, suffixSkipped: skip > 0 };
|
|
1011
1396
|
}
|
|
1012
|
-
return null;
|
|
1397
|
+
return { target: null, suffixSkipped: false };
|
|
1398
|
+
}
|
|
1399
|
+
function resolveImport(spec, fromPath, fileSet) {
|
|
1400
|
+
return resolveImportDetailed(spec, fromPath, fileSet).target;
|
|
1013
1401
|
}
|
|
1014
1402
|
function candidateFor(base, fileSet) {
|
|
1015
1403
|
if (fileSet.has(base)) return base;
|
|
1016
1404
|
for (const ext of SUPPORTED_EXTS) {
|
|
1017
1405
|
if (fileSet.has(`${base}${ext}`)) return `${base}${ext}`;
|
|
1018
1406
|
}
|
|
1407
|
+
const aliased = tsSourceForJsSpecifier(base, fileSet);
|
|
1408
|
+
if (aliased) return aliased;
|
|
1019
1409
|
for (const ext of SUPPORTED_EXTS) {
|
|
1020
1410
|
if (fileSet.has(`${base}/index${ext}`)) return `${base}/index${ext}`;
|
|
1021
1411
|
if (fileSet.has(`${base}/__init__${ext}`)) return `${base}/__init__${ext}`;
|
|
1022
1412
|
}
|
|
1023
1413
|
return null;
|
|
1024
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
|
+
}
|
|
1025
1425
|
function renderRepoMap(entries, options = {}) {
|
|
1026
1426
|
const maxChars = options.maxChars ?? 3200;
|
|
1027
1427
|
const lines = ["# repo map"];
|
|
@@ -1042,32 +1442,318 @@ function renderRepoMap(entries, options = {}) {
|
|
|
1042
1442
|
return totalSymbols > 0 ? text : "";
|
|
1043
1443
|
}
|
|
1044
1444
|
|
|
1045
|
-
// src/
|
|
1046
|
-
function
|
|
1047
|
-
const
|
|
1048
|
-
const fileSet = new Set(nodes);
|
|
1049
|
-
const edges = /* @__PURE__ */ new Map();
|
|
1445
|
+
// src/refgraph.ts
|
|
1446
|
+
function buildSymbolTable(index) {
|
|
1447
|
+
const table = /* @__PURE__ */ new Map();
|
|
1050
1448
|
for (const file of index.files) {
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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]);
|
|
1055
1453
|
}
|
|
1056
|
-
edges.set(file.path, targets);
|
|
1057
1454
|
}
|
|
1058
|
-
|
|
1455
|
+
for (const list of table.values()) {
|
|
1456
|
+
list.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
1457
|
+
}
|
|
1458
|
+
return table;
|
|
1059
1459
|
}
|
|
1060
|
-
function
|
|
1061
|
-
const
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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;
|
|
1468
|
+
}
|
|
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 });
|
|
1478
|
+
}
|
|
1479
|
+
if (call.from === symbol && !calleeFirstLine.has(call.name)) {
|
|
1480
|
+
calleeFirstLine.set(call.name, call.line);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
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));
|
|
1488
|
+
return {
|
|
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 unique2 = locals.length === 1;
|
|
1537
|
+
const owned = unique2 && 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 unique2 = dedupeSymbols(candidates);
|
|
1622
|
+
const count = unique2.length;
|
|
1623
|
+
const cap = maxCandidates > 0 ? Math.floor(maxCandidates) : 0;
|
|
1624
|
+
return unique2.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, refs = callerCounts(index)) {
|
|
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
|
+
hits.sort((a, b) => {
|
|
1719
|
+
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);
|
|
1720
|
+
return ab;
|
|
1721
|
+
});
|
|
1722
|
+
return hits.slice(0, limit);
|
|
1723
|
+
}
|
|
1724
|
+
function renderHit(hit) {
|
|
1725
|
+
const exportMark = hit.exported ? "export " : "";
|
|
1726
|
+
const sig = hit.signature || hit.name;
|
|
1727
|
+
const score = hit.score < 1 ? ` [${hit.score.toFixed(2)}]` : "";
|
|
1728
|
+
return `${exportMark}${hit.kind} ${sig}${score} \u2014 ${hit.file}:${hit.line}`;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// src/health.ts
|
|
1732
|
+
function buildModuleGraph(index) {
|
|
1733
|
+
const nodes = index.files.map((file) => file.path).sort((a, b) => a.localeCompare(b));
|
|
1734
|
+
const fileSet = new Set(nodes);
|
|
1735
|
+
const edges = /* @__PURE__ */ new Map();
|
|
1736
|
+
for (const file of index.files) {
|
|
1737
|
+
const targets = /* @__PURE__ */ new Set();
|
|
1738
|
+
for (const spec of file.imports ?? []) {
|
|
1739
|
+
const target = resolveImport(spec, file.path, fileSet);
|
|
1740
|
+
if (target && target !== file.path) targets.add(target);
|
|
1741
|
+
}
|
|
1742
|
+
edges.set(file.path, targets);
|
|
1743
|
+
}
|
|
1744
|
+
return { nodes, edges };
|
|
1745
|
+
}
|
|
1746
|
+
function findCycles(index) {
|
|
1747
|
+
const { nodes, edges } = buildModuleGraph(index);
|
|
1748
|
+
const discovery = /* @__PURE__ */ new Map();
|
|
1749
|
+
const low = /* @__PURE__ */ new Map();
|
|
1750
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
1751
|
+
const stack = [];
|
|
1752
|
+
const components = [];
|
|
1753
|
+
let counter = 0;
|
|
1754
|
+
const visit = (node) => {
|
|
1755
|
+
discovery.set(node, counter);
|
|
1756
|
+
low.set(node, counter);
|
|
1071
1757
|
counter++;
|
|
1072
1758
|
stack.push(node);
|
|
1073
1759
|
onStack.add(node);
|
|
@@ -1108,13 +1794,1332 @@ function findOrphanModules(index) {
|
|
|
1108
1794
|
index.files.filter((file) => file.symbols.length > 0).map((file) => file.path)
|
|
1109
1795
|
);
|
|
1110
1796
|
return nodes.filter(
|
|
1111
|
-
(
|
|
1797
|
+
(path9) => withSymbols.has(path9) && (incoming.get(path9) ?? 0) === 0 && (edges.get(path9)?.size ?? 0) === 0 && !ENTRY_RE.test(path9) && !TEST_RE.test(path9)
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// src/git-diff.ts
|
|
1802
|
+
import { execFile } from "child_process";
|
|
1803
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1804
|
+
import path6 from "path";
|
|
1805
|
+
var HUNK_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
1806
|
+
var HEADER_PATH_PREFIX_RE = /^[ab]\//;
|
|
1807
|
+
var MAX_BUFFER = 64 * 1024 * 1024;
|
|
1808
|
+
function newDraft() {
|
|
1809
|
+
return {
|
|
1810
|
+
oldPath: null,
|
|
1811
|
+
newPath: null,
|
|
1812
|
+
gitOldPath: null,
|
|
1813
|
+
gitNewPath: null,
|
|
1814
|
+
sawOldHeader: false,
|
|
1815
|
+
sawNewHeader: false,
|
|
1816
|
+
renameFrom: null,
|
|
1817
|
+
renameTo: null,
|
|
1818
|
+
newFile: false,
|
|
1819
|
+
deletedFile: false,
|
|
1820
|
+
hunks: [],
|
|
1821
|
+
sawHunk: false
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
function parseUnifiedDiff(text) {
|
|
1825
|
+
const changes = [];
|
|
1826
|
+
let draft = null;
|
|
1827
|
+
for (const line of text.split("\n")) {
|
|
1828
|
+
if (line.startsWith("diff --git ")) {
|
|
1829
|
+
if (draft) changes.push(finalize(draft));
|
|
1830
|
+
draft = newDraft();
|
|
1831
|
+
const [rawOld, rawNew] = splitDiffGitPaths(line.slice("diff --git ".length));
|
|
1832
|
+
draft.gitOldPath = rawOld === null ? null : parseHeaderPath(rawOld);
|
|
1833
|
+
draft.gitNewPath = rawNew === null ? null : parseHeaderPath(rawNew);
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1836
|
+
if (!draft) {
|
|
1837
|
+
if (!isFileLine(line)) continue;
|
|
1838
|
+
draft = newDraft();
|
|
1839
|
+
}
|
|
1840
|
+
if (line.startsWith("new file mode ")) {
|
|
1841
|
+
draft.newFile = true;
|
|
1842
|
+
} else if (line.startsWith("deleted file mode ")) {
|
|
1843
|
+
draft.deletedFile = true;
|
|
1844
|
+
} else if (line.startsWith("rename from ")) {
|
|
1845
|
+
draft.renameFrom = parseRawPath(line.slice("rename from ".length));
|
|
1846
|
+
} else if (line.startsWith("rename to ")) {
|
|
1847
|
+
draft.renameTo = parseRawPath(line.slice("rename to ".length));
|
|
1848
|
+
} else if (!draft.sawHunk && line.startsWith("--- ")) {
|
|
1849
|
+
draft.oldPath = parseHeaderPath(line.slice(4));
|
|
1850
|
+
draft.sawOldHeader = true;
|
|
1851
|
+
} else if (!draft.sawHunk && line.startsWith("+++ ")) {
|
|
1852
|
+
draft.newPath = parseHeaderPath(line.slice(4));
|
|
1853
|
+
draft.sawNewHeader = true;
|
|
1854
|
+
} else if (line.startsWith("@@ ")) {
|
|
1855
|
+
const hunk = parseHunk(line);
|
|
1856
|
+
if (hunk) {
|
|
1857
|
+
draft.hunks.push(hunk);
|
|
1858
|
+
draft.sawHunk = true;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
if (draft) changes.push(finalize(draft));
|
|
1863
|
+
return changes;
|
|
1864
|
+
}
|
|
1865
|
+
function isFileLine(line) {
|
|
1866
|
+
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 ");
|
|
1867
|
+
}
|
|
1868
|
+
function finalize(draft) {
|
|
1869
|
+
let oldPath = draft.renameFrom ?? (draft.sawOldHeader ? draft.oldPath : draft.gitOldPath);
|
|
1870
|
+
let newPath = draft.renameTo ?? (draft.sawNewHeader ? draft.newPath : draft.gitNewPath);
|
|
1871
|
+
if (draft.newFile) oldPath = null;
|
|
1872
|
+
if (draft.deletedFile) newPath = null;
|
|
1873
|
+
let status = "modified";
|
|
1874
|
+
if (draft.newFile || oldPath === null && newPath !== null && draft.renameFrom === null) {
|
|
1875
|
+
status = "added";
|
|
1876
|
+
} else if (draft.deletedFile || newPath === null && oldPath !== null && draft.renameTo === null) {
|
|
1877
|
+
status = "deleted";
|
|
1878
|
+
} else if (draft.renameFrom !== null || draft.renameTo !== null) {
|
|
1879
|
+
status = "renamed";
|
|
1880
|
+
}
|
|
1881
|
+
return { oldPath, newPath, status, hunks: draft.hunks };
|
|
1882
|
+
}
|
|
1883
|
+
function splitDiffGitPaths(rest) {
|
|
1884
|
+
const tokens = [];
|
|
1885
|
+
let index = 0;
|
|
1886
|
+
while (index < rest.length && tokens.length < 2) {
|
|
1887
|
+
while (index < rest.length && rest[index] === " ") index++;
|
|
1888
|
+
if (index >= rest.length) break;
|
|
1889
|
+
const start = index;
|
|
1890
|
+
if (rest[index] === '"') {
|
|
1891
|
+
index++;
|
|
1892
|
+
while (index < rest.length) {
|
|
1893
|
+
if (rest[index] === "\\") index += 2;
|
|
1894
|
+
else if (rest[index] === '"') {
|
|
1895
|
+
index++;
|
|
1896
|
+
break;
|
|
1897
|
+
} else index++;
|
|
1898
|
+
}
|
|
1899
|
+
} else {
|
|
1900
|
+
while (index < rest.length && rest[index] !== " ") index++;
|
|
1901
|
+
}
|
|
1902
|
+
tokens.push(rest.slice(start, index));
|
|
1903
|
+
}
|
|
1904
|
+
return [tokens[0] ?? null, tokens[1] ?? null];
|
|
1905
|
+
}
|
|
1906
|
+
function parseHunk(line) {
|
|
1907
|
+
const match = HUNK_RE.exec(line);
|
|
1908
|
+
if (!match) return null;
|
|
1909
|
+
const oldCount = match[2];
|
|
1910
|
+
const newCount = match[4];
|
|
1911
|
+
return {
|
|
1912
|
+
oldStart: Number(match[1]),
|
|
1913
|
+
oldLines: oldCount === void 0 ? 1 : Number(oldCount),
|
|
1914
|
+
newStart: Number(match[3]),
|
|
1915
|
+
newLines: newCount === void 0 ? 1 : Number(newCount)
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
function parseHeaderPath(raw) {
|
|
1919
|
+
let value = raw;
|
|
1920
|
+
const tab = value.indexOf(" ");
|
|
1921
|
+
if (tab >= 0) value = value.slice(0, tab);
|
|
1922
|
+
value = value.trim();
|
|
1923
|
+
if (value === "/dev/null") return null;
|
|
1924
|
+
if (value.startsWith('"')) value = unquoteGitPath(value);
|
|
1925
|
+
value = value.replace(HEADER_PATH_PREFIX_RE, "");
|
|
1926
|
+
return value.length > 0 ? value : null;
|
|
1927
|
+
}
|
|
1928
|
+
function parseRawPath(raw) {
|
|
1929
|
+
let value = raw.trim();
|
|
1930
|
+
if (value.startsWith('"')) value = unquoteGitPath(value);
|
|
1931
|
+
return value.length > 0 ? value : null;
|
|
1932
|
+
}
|
|
1933
|
+
function unquoteGitPath(value) {
|
|
1934
|
+
const inner = value.slice(1, -1);
|
|
1935
|
+
const bytes = [];
|
|
1936
|
+
for (let i = 0; i < inner.length; i++) {
|
|
1937
|
+
const ch = inner[i];
|
|
1938
|
+
if (ch !== "\\") {
|
|
1939
|
+
bytes.push(...Buffer.from(ch, "utf8"));
|
|
1940
|
+
continue;
|
|
1941
|
+
}
|
|
1942
|
+
i++;
|
|
1943
|
+
const esc = inner[i];
|
|
1944
|
+
if (esc === void 0) break;
|
|
1945
|
+
if (esc >= "0" && esc <= "7") {
|
|
1946
|
+
let octal = esc;
|
|
1947
|
+
let next = inner[i + 1];
|
|
1948
|
+
while (octal.length < 3 && next !== void 0 && next >= "0" && next <= "7") {
|
|
1949
|
+
octal += next;
|
|
1950
|
+
i++;
|
|
1951
|
+
next = inner[i + 1];
|
|
1952
|
+
}
|
|
1953
|
+
bytes.push(parseInt(octal, 8));
|
|
1954
|
+
continue;
|
|
1955
|
+
}
|
|
1956
|
+
switch (esc) {
|
|
1957
|
+
case "n":
|
|
1958
|
+
bytes.push(10);
|
|
1959
|
+
break;
|
|
1960
|
+
case "t":
|
|
1961
|
+
bytes.push(9);
|
|
1962
|
+
break;
|
|
1963
|
+
case "r":
|
|
1964
|
+
bytes.push(13);
|
|
1965
|
+
break;
|
|
1966
|
+
case "f":
|
|
1967
|
+
bytes.push(12);
|
|
1968
|
+
break;
|
|
1969
|
+
case "v":
|
|
1970
|
+
bytes.push(11);
|
|
1971
|
+
break;
|
|
1972
|
+
case "b":
|
|
1973
|
+
bytes.push(8);
|
|
1974
|
+
break;
|
|
1975
|
+
case "a":
|
|
1976
|
+
bytes.push(7);
|
|
1977
|
+
break;
|
|
1978
|
+
case '"':
|
|
1979
|
+
bytes.push(34);
|
|
1980
|
+
break;
|
|
1981
|
+
case "\\":
|
|
1982
|
+
bytes.push(92);
|
|
1983
|
+
break;
|
|
1984
|
+
default:
|
|
1985
|
+
bytes.push(...Buffer.from(esc, "utf8"));
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
return Buffer.from(bytes).toString("utf8");
|
|
1989
|
+
}
|
|
1990
|
+
function requireSafeRef(ref, label) {
|
|
1991
|
+
if (ref.length === 0) throw new Error(`${label} must not be empty`);
|
|
1992
|
+
if (ref.startsWith("-")) throw new Error(`${label} must not start with '-': ${ref}`);
|
|
1993
|
+
}
|
|
1994
|
+
function runGit(args, cwd) {
|
|
1995
|
+
return new Promise((resolve, reject) => {
|
|
1996
|
+
execFile("git", args, { cwd, encoding: "utf8", maxBuffer: MAX_BUFFER }, (error, stdout) => {
|
|
1997
|
+
if (error) reject(error);
|
|
1998
|
+
else resolve(stdout);
|
|
1999
|
+
});
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
async function readWorkingTreeDiff(root, baseRef) {
|
|
2003
|
+
requireSafeRef(baseRef, "baseRef");
|
|
2004
|
+
const tracked = await runGit(
|
|
2005
|
+
["diff", "--no-ext-diff", "--find-renames", "--unified=0", baseRef, "--", "."],
|
|
2006
|
+
root
|
|
2007
|
+
);
|
|
2008
|
+
const untracked = await runGit(["ls-files", "--others", "--exclude-standard", "-z", "--"], root);
|
|
2009
|
+
const changes = parseUnifiedDiff(tracked);
|
|
2010
|
+
const untrackedFiles = [];
|
|
2011
|
+
for (const rawPath of untracked.split("\0")) {
|
|
2012
|
+
if (rawPath.length === 0) continue;
|
|
2013
|
+
const normalized = rawPath.replace(/\\/g, "/");
|
|
2014
|
+
if (normalized === ".dsh-code-index" || normalized.startsWith(".dsh-code-index/")) continue;
|
|
2015
|
+
try {
|
|
2016
|
+
const content = await readFile4(path6.resolve(root, normalized), "utf8");
|
|
2017
|
+
const lines = content.length === 0 ? 0 : content.split(/\r?\n/).length - (content.endsWith("\n") ? 1 : 0);
|
|
2018
|
+
untrackedFiles.push({ path: normalized, content, lines });
|
|
2019
|
+
} catch {
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
const paired = /* @__PURE__ */ new Set();
|
|
2023
|
+
for (const change of changes) {
|
|
2024
|
+
if (change.status !== "deleted" || change.oldPath === null) continue;
|
|
2025
|
+
let oldContent;
|
|
2026
|
+
try {
|
|
2027
|
+
oldContent = await runGit(["show", `${baseRef}:${change.oldPath}`], root);
|
|
2028
|
+
} catch {
|
|
2029
|
+
continue;
|
|
2030
|
+
}
|
|
2031
|
+
const match = untrackedFiles.find((file) => !paired.has(file.path) && file.content === oldContent);
|
|
2032
|
+
if (!match) continue;
|
|
2033
|
+
change.newPath = match.path;
|
|
2034
|
+
change.status = "renamed";
|
|
2035
|
+
change.hunks = [];
|
|
2036
|
+
paired.add(match.path);
|
|
2037
|
+
}
|
|
2038
|
+
for (const file of untrackedFiles) {
|
|
2039
|
+
if (paired.has(file.path)) continue;
|
|
2040
|
+
changes.push({
|
|
2041
|
+
oldPath: null,
|
|
2042
|
+
newPath: file.path,
|
|
2043
|
+
status: "added",
|
|
2044
|
+
hunks: file.lines > 0 ? [{ oldStart: 0, oldLines: 0, newStart: 1, newLines: file.lines }] : []
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
return renderNormalizedDiff(changes);
|
|
2048
|
+
}
|
|
2049
|
+
function renderNormalizedDiff(changes) {
|
|
2050
|
+
return changes.map((change) => {
|
|
2051
|
+
const oldPath = change.oldPath ?? change.newPath;
|
|
2052
|
+
const newPath = change.newPath ?? change.oldPath;
|
|
2053
|
+
const lines = [`diff --git ${diffToken(`a/${oldPath}`)} ${diffToken(`b/${newPath}`)}`];
|
|
2054
|
+
if (change.status === "added") lines.push("new file mode 100644");
|
|
2055
|
+
if (change.status === "deleted") lines.push("deleted file mode 100644");
|
|
2056
|
+
if (change.status === "renamed") {
|
|
2057
|
+
lines.push("similarity index 100%", `rename from ${diffToken(change.oldPath)}`, `rename to ${diffToken(change.newPath)}`);
|
|
2058
|
+
} else {
|
|
2059
|
+
lines.push(`--- ${change.oldPath === null ? "/dev/null" : diffToken(`a/${change.oldPath}`)}`);
|
|
2060
|
+
lines.push(`+++ ${change.newPath === null ? "/dev/null" : diffToken(`b/${change.newPath}`)}`);
|
|
2061
|
+
}
|
|
2062
|
+
for (const hunk of change.hunks) {
|
|
2063
|
+
const oldCount = hunk.oldLines === 1 ? "" : `,${hunk.oldLines}`;
|
|
2064
|
+
const newCount = hunk.newLines === 1 ? "" : `,${hunk.newLines}`;
|
|
2065
|
+
lines.push(`@@ -${hunk.oldStart}${oldCount} +${hunk.newStart}${newCount} @@`);
|
|
2066
|
+
}
|
|
2067
|
+
return `${lines.join("\n")}
|
|
2068
|
+
`;
|
|
2069
|
+
}).join("");
|
|
2070
|
+
}
|
|
2071
|
+
function diffToken(value) {
|
|
2072
|
+
return /[\s"]/.test(value) ? `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : value;
|
|
2073
|
+
}
|
|
2074
|
+
async function readGitFileAtRef(root, ref, repoRelativePath) {
|
|
2075
|
+
requireSafeRef(ref, "ref");
|
|
2076
|
+
try {
|
|
2077
|
+
return await runGit(["show", `${ref}:${repoRelativePath}`], root);
|
|
2078
|
+
} catch {
|
|
2079
|
+
return null;
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// src/change-context.ts
|
|
2084
|
+
import path7 from "path";
|
|
2085
|
+
var LABEL_STRENGTH = {
|
|
2086
|
+
exact: 3,
|
|
2087
|
+
"import-scoped": 2,
|
|
2088
|
+
"name-only": 1
|
|
2089
|
+
};
|
|
2090
|
+
var ENTRY_KINDS = /* @__PURE__ */ new Set(["function", "method", "class"]);
|
|
2091
|
+
var ENTRY_FILE_RE = /(^|\/)(main|index|cli|server|app)\.[^./]+$/;
|
|
2092
|
+
var TEST_RE2 = /(^|\/)(tests?|__tests__)(\/|$)|\.(?:spec|test)\.[cm]?[jt]sx?$|(^|\/)(?:test_[^/]+\.py|[^/]+_test\.(?:py|go))$/;
|
|
2093
|
+
function changeContextInputFromArgs(args, root) {
|
|
2094
|
+
const hasDiff = typeof args.diff === "string" && args.diff.trim().length > 0;
|
|
2095
|
+
const hasFiles = Array.isArray(args.files) && args.files.length > 0;
|
|
2096
|
+
const hasSymbols = Array.isArray(args.symbols) && args.symbols.length > 0;
|
|
2097
|
+
if ([hasDiff, hasFiles, hasSymbols].filter(Boolean).length > 1) {
|
|
2098
|
+
throw new Error("pass only one of diff, files, symbols");
|
|
2099
|
+
}
|
|
2100
|
+
const suppliedBaseRef = typeof args.baseRef === "string" && args.baseRef.length > 0 ? args.baseRef : void 0;
|
|
2101
|
+
if (hasDiff) {
|
|
2102
|
+
return suppliedBaseRef === void 0 ? { kind: "diff", root, text: args.diff } : { kind: "diff", root, text: args.diff, baseRef: suppliedBaseRef };
|
|
2103
|
+
}
|
|
2104
|
+
if (hasFiles) return { kind: "files", files: args.files.map(requireSafePath) };
|
|
2105
|
+
if (hasSymbols) return { kind: "symbols", symbolIds: [...args.symbols] };
|
|
2106
|
+
return { kind: "git", root, baseRef: suppliedBaseRef ?? "HEAD" };
|
|
2107
|
+
}
|
|
2108
|
+
function requireSafePath(raw) {
|
|
2109
|
+
const value = raw.replace(/\\/g, "/").trim();
|
|
2110
|
+
if (value.length === 0) throw new Error("file path must not be empty");
|
|
2111
|
+
if (value.startsWith("/") || /^[A-Za-z]:\//.test(value)) {
|
|
2112
|
+
throw new Error(`absolute file path not allowed: ${raw}`);
|
|
2113
|
+
}
|
|
2114
|
+
if (/(^|\/)\.\.(\/|$)/.test(value)) throw new Error(`path must stay inside the repo: ${raw}`);
|
|
2115
|
+
return value.replace(/^\.\//, "");
|
|
2116
|
+
}
|
|
2117
|
+
function clamp(value, lo, hi, fallback) {
|
|
2118
|
+
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
2119
|
+
return Math.min(hi, Math.max(lo, Math.floor(value)));
|
|
2120
|
+
}
|
|
2121
|
+
function weakest(labels) {
|
|
2122
|
+
let out = "exact";
|
|
2123
|
+
for (const label of labels) {
|
|
2124
|
+
if (LABEL_STRENGTH[label] < LABEL_STRENGTH[out]) out = label;
|
|
2125
|
+
}
|
|
2126
|
+
return out;
|
|
2127
|
+
}
|
|
2128
|
+
function symbolLabel(symbol) {
|
|
2129
|
+
return `${symbol.kind} ${symbol.signature || symbol.name}`;
|
|
2130
|
+
}
|
|
2131
|
+
async function buildChangeContext(index, input, options = {}) {
|
|
2132
|
+
const maxDepth = clamp(options.maxDepth, 1, 6, 3);
|
|
2133
|
+
const maxImpact = clamp(options.maxImpact, 1, 200, 40);
|
|
2134
|
+
const maxPaths = clamp(options.maxPaths, 1, 50, 10);
|
|
2135
|
+
const maxTests = clamp(options.maxTests, 1, 100, 20);
|
|
2136
|
+
const graph = buildReferenceGraph(index);
|
|
2137
|
+
const moduleGraph = buildModuleGraph(index);
|
|
2138
|
+
const filesByPath = new Map(index.files.map((file) => [file.path, file]));
|
|
2139
|
+
const fileSet = new Set(filesByPath.keys());
|
|
2140
|
+
const incomingCount = /* @__PURE__ */ new Map();
|
|
2141
|
+
for (const [id, edges] of graph.incoming) incomingCount.set(id, edges.length);
|
|
2142
|
+
const warnings = [];
|
|
2143
|
+
const changedByKey = /* @__PURE__ */ new Map();
|
|
2144
|
+
const unmapped = [];
|
|
2145
|
+
const changedFiles = /* @__PURE__ */ new Set();
|
|
2146
|
+
const commandRoot = input.kind === "git" || input.kind === "diff" ? input.root : index.root;
|
|
2147
|
+
if (path7.resolve(commandRoot) !== path7.resolve(index.root)) {
|
|
2148
|
+
warnings.push(
|
|
2149
|
+
`command root ${path7.resolve(commandRoot)} differs from indexed root ${index.root}; using the cached index`
|
|
2150
|
+
);
|
|
2151
|
+
}
|
|
2152
|
+
const addChanged = (symbol, change, side) => {
|
|
2153
|
+
const key = `${side}:${symbol.id}`;
|
|
2154
|
+
if (!changedByKey.has(key)) changedByKey.set(key, { symbol, change, side, resolution: "exact" });
|
|
2155
|
+
};
|
|
2156
|
+
const addUnmapped = (file, side, ranges, reason) => {
|
|
2157
|
+
if (ranges.length > 0) unmapped.push({ file, side, ranges, reason });
|
|
2158
|
+
};
|
|
2159
|
+
let baseRef;
|
|
2160
|
+
if (input.kind === "files") {
|
|
2161
|
+
for (const filePath of input.files) {
|
|
2162
|
+
const file = filesByPath.get(filePath);
|
|
2163
|
+
if (!file) {
|
|
2164
|
+
warnings.push(`file not in index: ${filePath}`);
|
|
2165
|
+
continue;
|
|
2166
|
+
}
|
|
2167
|
+
changedFiles.add(file.path);
|
|
2168
|
+
for (const symbol of file.symbols) addChanged(symbol, "modified", "current");
|
|
2169
|
+
if (file.symbols.length === 0) warnings.push(`no indexed symbols in changed file: ${filePath}`);
|
|
2170
|
+
}
|
|
2171
|
+
} else if (input.kind === "symbols") {
|
|
2172
|
+
for (const id of input.symbolIds) {
|
|
2173
|
+
const symbol = graph.symbolsById.get(id);
|
|
2174
|
+
if (!symbol) {
|
|
2175
|
+
warnings.push(`symbol not in index: ${id}`);
|
|
2176
|
+
continue;
|
|
2177
|
+
}
|
|
2178
|
+
changedFiles.add(symbol.file);
|
|
2179
|
+
addChanged(symbol, "modified", "current");
|
|
2180
|
+
}
|
|
2181
|
+
} else {
|
|
2182
|
+
let text;
|
|
2183
|
+
if (input.kind === "git") {
|
|
2184
|
+
baseRef = input.baseRef;
|
|
2185
|
+
text = await readWorkingTreeDiff(input.root, input.baseRef);
|
|
2186
|
+
} else {
|
|
2187
|
+
text = input.text;
|
|
2188
|
+
baseRef = input.baseRef;
|
|
2189
|
+
}
|
|
2190
|
+
const changes = parseUnifiedDiff(text).map((change) => ({
|
|
2191
|
+
...change,
|
|
2192
|
+
oldPath: change.oldPath === null ? null : requireSafePath(change.oldPath),
|
|
2193
|
+
newPath: change.newPath === null ? null : requireSafePath(change.newPath)
|
|
2194
|
+
}));
|
|
2195
|
+
if (changes.length === 0) warnings.push("no changes found in the diff");
|
|
2196
|
+
const baseSymbolsCache = /* @__PURE__ */ new Map();
|
|
2197
|
+
const readBaseSymbols = (filePath) => {
|
|
2198
|
+
const cached = baseSymbolsCache.get(filePath);
|
|
2199
|
+
if (cached) return cached;
|
|
2200
|
+
const pending = (async () => {
|
|
2201
|
+
if (baseRef === void 0) return null;
|
|
2202
|
+
const content = await readGitFileAtRef(commandRoot, baseRef, filePath);
|
|
2203
|
+
if (content === null) return null;
|
|
2204
|
+
const lang = languageForFile(filePath);
|
|
2205
|
+
if (!lang) return null;
|
|
2206
|
+
const extracted = await extractAll(content, lang, filePath);
|
|
2207
|
+
return extracted.symbols;
|
|
2208
|
+
})();
|
|
2209
|
+
baseSymbolsCache.set(filePath, pending);
|
|
2210
|
+
return pending;
|
|
2211
|
+
};
|
|
2212
|
+
const symbolsByPath = new Map(index.files.map((file) => [file.path, file.symbols]));
|
|
2213
|
+
await mapDiffChanges(
|
|
2214
|
+
changes,
|
|
2215
|
+
symbolsByPath,
|
|
2216
|
+
addChanged,
|
|
2217
|
+
addUnmapped,
|
|
2218
|
+
changedFiles,
|
|
2219
|
+
readBaseSymbols,
|
|
2220
|
+
warnings
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
const changedSymbols = [...changedByKey.values()].sort(compareChanged);
|
|
2224
|
+
const callerById = /* @__PURE__ */ new Map();
|
|
2225
|
+
for (const changed of changedSymbols) {
|
|
2226
|
+
for (const edge of graph.incoming.get(changed.symbol.id) ?? []) {
|
|
2227
|
+
if (edge.sourceId === null) continue;
|
|
2228
|
+
const caller = graph.symbolsById.get(edge.sourceId);
|
|
2229
|
+
if (!caller) continue;
|
|
2230
|
+
const row = {
|
|
2231
|
+
symbol: caller,
|
|
2232
|
+
depth: 1,
|
|
2233
|
+
via: hopOf(edge),
|
|
2234
|
+
resolution: edge.resolution
|
|
2235
|
+
};
|
|
2236
|
+
const existing = callerById.get(caller.id);
|
|
2237
|
+
if (!existing || LABEL_STRENGTH[row.resolution] > LABEL_STRENGTH[existing.resolution]) {
|
|
2238
|
+
callerById.set(caller.id, row);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
const allCallers = [...callerById.values()].sort(compareBySymbol);
|
|
2243
|
+
const changedIds = new Set(changedSymbols.map((changed) => changed.symbol.id));
|
|
2244
|
+
const impactById = /* @__PURE__ */ new Map();
|
|
2245
|
+
{
|
|
2246
|
+
const depthById = /* @__PURE__ */ new Map();
|
|
2247
|
+
const strengthById = /* @__PURE__ */ new Map();
|
|
2248
|
+
for (const id of changedIds) {
|
|
2249
|
+
depthById.set(id, 0);
|
|
2250
|
+
strengthById.set(id, "exact");
|
|
2251
|
+
}
|
|
2252
|
+
let frontier = [...changedIds];
|
|
2253
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
2254
|
+
const next = [];
|
|
2255
|
+
for (const id of frontier) {
|
|
2256
|
+
const pathStrength = strengthById.get(id) ?? "exact";
|
|
2257
|
+
for (const edge of graph.incoming.get(id) ?? []) {
|
|
2258
|
+
if (edge.sourceId === null || changedIds.has(edge.sourceId)) continue;
|
|
2259
|
+
const caller = graph.symbolsById.get(edge.sourceId);
|
|
2260
|
+
if (!caller) continue;
|
|
2261
|
+
const cumulative = weakest([pathStrength, edge.resolution]);
|
|
2262
|
+
const row = {
|
|
2263
|
+
symbol: caller,
|
|
2264
|
+
depth: depth + 1,
|
|
2265
|
+
via: hopOf(edge),
|
|
2266
|
+
resolution: cumulative
|
|
2267
|
+
};
|
|
2268
|
+
const seen = depthById.get(edge.sourceId);
|
|
2269
|
+
if (seen === void 0) {
|
|
2270
|
+
depthById.set(edge.sourceId, depth + 1);
|
|
2271
|
+
strengthById.set(edge.sourceId, cumulative);
|
|
2272
|
+
next.push(edge.sourceId);
|
|
2273
|
+
if (depth + 1 >= 2) impactById.set(edge.sourceId, row);
|
|
2274
|
+
} else if (seen === depth + 1) {
|
|
2275
|
+
const previous = strengthById.get(edge.sourceId) ?? "exact";
|
|
2276
|
+
if (LABEL_STRENGTH[cumulative] > LABEL_STRENGTH[previous]) {
|
|
2277
|
+
strengthById.set(edge.sourceId, cumulative);
|
|
2278
|
+
if (depth + 1 >= 2) impactById.set(edge.sourceId, row);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
frontier = next;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
const allImpact = [...impactById.values()].sort(compareByDepthThenSymbol);
|
|
2287
|
+
const allPaths = [];
|
|
2288
|
+
const pathKey = /* @__PURE__ */ new Set();
|
|
2289
|
+
for (const changed of changedSymbols) {
|
|
2290
|
+
for (const found of pathsToEntryPoints(changed.symbol, graph, incomingCount, maxDepth)) {
|
|
2291
|
+
const key = `${found.entry.id}\0${found.changed.id}`;
|
|
2292
|
+
if (pathKey.has(key)) continue;
|
|
2293
|
+
pathKey.add(key);
|
|
2294
|
+
allPaths.push(found);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
allPaths.sort(comparePaths);
|
|
2298
|
+
const importDependents = collectImportDependents(index, moduleGraph, fileSet, changedFiles);
|
|
2299
|
+
const allTests = collectAffectedTests(
|
|
2300
|
+
index,
|
|
2301
|
+
moduleGraph,
|
|
2302
|
+
changedFiles,
|
|
2303
|
+
changedSymbols,
|
|
2304
|
+
[...allCallers, ...allImpact]
|
|
2305
|
+
);
|
|
2306
|
+
const truncated = {
|
|
2307
|
+
callers: allCallers.length > maxImpact,
|
|
2308
|
+
dependents: importDependents.length > maxImpact,
|
|
2309
|
+
paths: allPaths.length > maxPaths,
|
|
2310
|
+
impact: allImpact.length > maxImpact,
|
|
2311
|
+
tests: allTests.length > maxTests
|
|
2312
|
+
};
|
|
2313
|
+
return {
|
|
2314
|
+
root: index.root,
|
|
2315
|
+
source: input.kind,
|
|
2316
|
+
...baseRef !== void 0 ? { baseRef } : {},
|
|
2317
|
+
changed: changedSymbols,
|
|
2318
|
+
unmapped: unmapped.sort(compareUnmapped),
|
|
2319
|
+
directCallers: allCallers.slice(0, maxImpact),
|
|
2320
|
+
importDependents: importDependents.slice(0, maxImpact),
|
|
2321
|
+
paths: allPaths.slice(0, maxPaths),
|
|
2322
|
+
impact: allImpact.slice(0, maxImpact),
|
|
2323
|
+
tests: allTests.slice(0, maxTests),
|
|
2324
|
+
warnings,
|
|
2325
|
+
truncated
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
function hopOf(edge) {
|
|
2329
|
+
return {
|
|
2330
|
+
fromId: edge.sourceId,
|
|
2331
|
+
toId: edge.targetId,
|
|
2332
|
+
callSite: edge.callSite,
|
|
2333
|
+
resolution: edge.resolution,
|
|
2334
|
+
reason: edge.provenance
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
async function mapDiffChanges(changes, symbolsByPath, addChanged, addUnmapped, changedFiles, readBaseSymbols, warnings) {
|
|
2338
|
+
for (const change of changes) {
|
|
2339
|
+
const { oldPath, newPath, hunks } = change;
|
|
2340
|
+
if (newPath) changedFiles.add(newPath);
|
|
2341
|
+
if (oldPath) changedFiles.add(oldPath);
|
|
2342
|
+
const currentSymbols = newPath !== null && change.status !== "deleted" ? symbolsByPath.get(newPath) : void 0;
|
|
2343
|
+
if (newPath !== null && change.status !== "deleted") {
|
|
2344
|
+
const currentRanges = hunks.filter((hunk) => hunk.newLines > 0).map((hunk) => ({
|
|
2345
|
+
startLine: hunk.newStart,
|
|
2346
|
+
endLine: hunk.newStart + Math.max(hunk.newLines, 1) - 1
|
|
2347
|
+
}));
|
|
2348
|
+
if (currentRanges.length > 0 && currentSymbols) {
|
|
2349
|
+
for (const range of currentRanges) {
|
|
2350
|
+
for (const symbol of currentSymbols) {
|
|
2351
|
+
if (intersects(symbol, range)) addChanged(symbol, change.status, "current");
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
const missed = currentRanges.filter(
|
|
2355
|
+
(range) => !currentSymbols.some((symbol) => intersects(symbol, range))
|
|
2356
|
+
);
|
|
2357
|
+
addUnmapped(newPath, "current", missed, "outside-symbol");
|
|
2358
|
+
} else if (currentRanges.length > 0) {
|
|
2359
|
+
addUnmapped(newPath, "current", currentRanges, "outside-symbol");
|
|
2360
|
+
}
|
|
2361
|
+
if (change.status === "renamed" && hunks.length === 0) {
|
|
2362
|
+
for (const symbol of currentSymbols ?? []) addChanged(symbol, "renamed", "current");
|
|
2363
|
+
if (oldPath !== null) {
|
|
2364
|
+
const oldSymbols = await readBaseSymbols(oldPath);
|
|
2365
|
+
if (oldSymbols === null) {
|
|
2366
|
+
warnings.push(`rename baseline unavailable: ${oldPath}`);
|
|
2367
|
+
} else {
|
|
2368
|
+
for (const symbol of oldSymbols) addChanged(symbol, "renamed", "base");
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
const deletionHunks = hunks.filter((hunk) => hunk.newLines === 0);
|
|
2374
|
+
if (deletionHunks.length > 0 && oldPath !== null) {
|
|
2375
|
+
const oldRanges = deletionHunks.map((hunk) => ({
|
|
2376
|
+
startLine: hunk.oldStart,
|
|
2377
|
+
endLine: hunk.oldStart + Math.max(hunk.oldLines, 1) - 1
|
|
2378
|
+
}));
|
|
2379
|
+
const oldSymbols = await readBaseSymbols(oldPath);
|
|
2380
|
+
if (oldSymbols === null) {
|
|
2381
|
+
addUnmapped(oldPath, "base", oldRanges, "base-content-unavailable");
|
|
2382
|
+
} else {
|
|
2383
|
+
const baseChange = change.status === "deleted" ? "deleted" : "modified";
|
|
2384
|
+
for (const range of oldRanges) {
|
|
2385
|
+
for (const symbol of oldSymbols) {
|
|
2386
|
+
if (intersects(symbol, range)) addChanged(symbol, baseChange, "base");
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
const missed = oldRanges.filter(
|
|
2390
|
+
(range) => !oldSymbols.some((symbol) => intersects(symbol, range))
|
|
2391
|
+
);
|
|
2392
|
+
addUnmapped(oldPath, "base", missed, "outside-symbol");
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
if (change.status === "renamed" && hunks.length === 0) {
|
|
2396
|
+
warnings.push(`renamed with no content change: ${oldPath} -> ${newPath}`);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
function intersects(symbol, range) {
|
|
2401
|
+
return symbol.line <= range.endLine && symbol.endLine >= range.startLine;
|
|
2402
|
+
}
|
|
2403
|
+
function pathsToEntryPoints(changed, graph, incomingCount, maxDepth) {
|
|
2404
|
+
const isEntry = (symbol) => {
|
|
2405
|
+
if (ENTRY_FILE_RE.test(symbol.file)) return true;
|
|
2406
|
+
if (!symbol.exported) return false;
|
|
2407
|
+
if (symbol.kind === "variable") {
|
|
2408
|
+
return symbol.scope.length === 0 || (incomingCount.get(symbol.id) ?? 0) === 0;
|
|
2409
|
+
}
|
|
2410
|
+
if (ENTRY_KINDS.has(symbol.kind)) return true;
|
|
2411
|
+
return (incomingCount.get(symbol.id) ?? 0) === 0;
|
|
2412
|
+
};
|
|
2413
|
+
const found = [];
|
|
2414
|
+
const depthById = /* @__PURE__ */ new Map([[changed.id, 0]]);
|
|
2415
|
+
const strengthById = /* @__PURE__ */ new Map([[changed.id, "exact"]]);
|
|
2416
|
+
const nextEdge = /* @__PURE__ */ new Map();
|
|
2417
|
+
const entryIds = /* @__PURE__ */ new Set();
|
|
2418
|
+
const queue = [changed.id];
|
|
2419
|
+
while (queue.length > 0) {
|
|
2420
|
+
const node = queue.shift();
|
|
2421
|
+
const nodeDepth = depthById.get(node) ?? 0;
|
|
2422
|
+
if (nodeDepth >= maxDepth) continue;
|
|
2423
|
+
const nodeStrength = strengthById.get(node) ?? "exact";
|
|
2424
|
+
for (const edge of graph.incoming.get(node) ?? []) {
|
|
2425
|
+
if (edge.sourceId === null) continue;
|
|
2426
|
+
const candidateDepth = nodeDepth + 1;
|
|
2427
|
+
const cumulative = weakest([nodeStrength, edge.resolution]);
|
|
2428
|
+
const existingDepth = depthById.get(edge.sourceId);
|
|
2429
|
+
if (existingDepth === void 0) {
|
|
2430
|
+
depthById.set(edge.sourceId, candidateDepth);
|
|
2431
|
+
strengthById.set(edge.sourceId, cumulative);
|
|
2432
|
+
nextEdge.set(edge.sourceId, hopOf(edge));
|
|
2433
|
+
queue.push(edge.sourceId);
|
|
2434
|
+
} else if (existingDepth === candidateDepth) {
|
|
2435
|
+
const existingStrength = strengthById.get(edge.sourceId) ?? "exact";
|
|
2436
|
+
if (LABEL_STRENGTH[cumulative] > LABEL_STRENGTH[existingStrength]) {
|
|
2437
|
+
strengthById.set(edge.sourceId, cumulative);
|
|
2438
|
+
nextEdge.set(edge.sourceId, hopOf(edge));
|
|
2439
|
+
queue.push(edge.sourceId);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
const entry = graph.symbolsById.get(edge.sourceId);
|
|
2443
|
+
if (entry && isEntry(entry)) entryIds.add(edge.sourceId);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
for (const entryId of entryIds) {
|
|
2447
|
+
const entry = graph.symbolsById.get(entryId);
|
|
2448
|
+
if (!entry) continue;
|
|
2449
|
+
const hops = [];
|
|
2450
|
+
let cursor = entryId;
|
|
2451
|
+
const guard = /* @__PURE__ */ new Set();
|
|
2452
|
+
while (cursor !== changed.id && !guard.has(cursor)) {
|
|
2453
|
+
guard.add(cursor);
|
|
2454
|
+
const hop = nextEdge.get(cursor);
|
|
2455
|
+
if (!hop) break;
|
|
2456
|
+
hops.push(hop);
|
|
2457
|
+
cursor = hop.toId;
|
|
2458
|
+
}
|
|
2459
|
+
if (cursor === changed.id && hops.length > 0) {
|
|
2460
|
+
found.push({
|
|
2461
|
+
entry,
|
|
2462
|
+
changed,
|
|
2463
|
+
hops,
|
|
2464
|
+
resolution: weakest(hops.map((hop) => hop.resolution))
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
return found;
|
|
2469
|
+
}
|
|
2470
|
+
function collectImportDependents(index, moduleGraph, fileSet, changedFiles) {
|
|
2471
|
+
const filesByPath = new Map(index.files.map((file) => [file.path, file]));
|
|
2472
|
+
const reverse = /* @__PURE__ */ new Map();
|
|
2473
|
+
for (const [from, targets] of moduleGraph.edges) {
|
|
2474
|
+
for (const target of targets) {
|
|
2475
|
+
const importers = reverse.get(target) ?? /* @__PURE__ */ new Set();
|
|
2476
|
+
importers.add(from);
|
|
2477
|
+
reverse.set(target, importers);
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
const augmented = /* @__PURE__ */ new Set([...fileSet, ...changedFiles]);
|
|
2481
|
+
let reverseImports = null;
|
|
2482
|
+
const reverseImportIndex = () => {
|
|
2483
|
+
if (reverseImports) return reverseImports;
|
|
2484
|
+
const map = /* @__PURE__ */ new Map();
|
|
2485
|
+
for (const file of index.files) {
|
|
2486
|
+
for (const info of file.importDetails ?? []) {
|
|
2487
|
+
const target = resolveImport(info.specifier, file.path, augmented);
|
|
2488
|
+
if (target === null) continue;
|
|
2489
|
+
const importers = map.get(target) ?? /* @__PURE__ */ new Set();
|
|
2490
|
+
importers.add(file.path);
|
|
2491
|
+
map.set(target, importers);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
reverseImports = map;
|
|
2495
|
+
return map;
|
|
2496
|
+
};
|
|
2497
|
+
const dependents = [];
|
|
2498
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2499
|
+
for (const changedFile of [...changedFiles].sort((a, b) => a.localeCompare(b))) {
|
|
2500
|
+
const importers = new Set(reverse.get(changedFile) ?? []);
|
|
2501
|
+
let resolutionSet = fileSet;
|
|
2502
|
+
if (!fileSet.has(changedFile)) {
|
|
2503
|
+
resolutionSet = augmented;
|
|
2504
|
+
for (const importerPath of reverseImportIndex().get(changedFile) ?? []) {
|
|
2505
|
+
importers.add(importerPath);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
for (const importerPath of [...importers].sort((a, b) => a.localeCompare(b))) {
|
|
2509
|
+
const importer = filesByPath.get(importerPath);
|
|
2510
|
+
if (!importer) continue;
|
|
2511
|
+
const specifier = specifierFor(importer, changedFile, resolutionSet);
|
|
2512
|
+
if (specifier === null) continue;
|
|
2513
|
+
const key = `${importerPath}\0${changedFile}\0${specifier}`;
|
|
2514
|
+
if (seen.has(key)) continue;
|
|
2515
|
+
seen.add(key);
|
|
2516
|
+
dependents.push({
|
|
2517
|
+
file: importerPath,
|
|
2518
|
+
changedFile,
|
|
2519
|
+
specifier,
|
|
2520
|
+
resolution: "import-scoped"
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
return dependents.sort(
|
|
2525
|
+
(a, b) => a.file.localeCompare(b.file) || a.changedFile.localeCompare(b.changedFile) || a.specifier.localeCompare(b.specifier)
|
|
2526
|
+
);
|
|
2527
|
+
}
|
|
2528
|
+
function specifierFor(file, target, fileSet) {
|
|
2529
|
+
for (const info of file.importDetails ?? []) {
|
|
2530
|
+
if (resolveImport(info.specifier, file.path, fileSet) === target) return info.specifier;
|
|
2531
|
+
}
|
|
2532
|
+
for (const spec of file.imports ?? []) {
|
|
2533
|
+
if (resolveImport(spec, file.path, fileSet) === target) return spec;
|
|
2534
|
+
}
|
|
2535
|
+
return null;
|
|
2536
|
+
}
|
|
2537
|
+
var TEST_RANK = {
|
|
2538
|
+
"changed-test": 0,
|
|
2539
|
+
"imports-changed-file": 1,
|
|
2540
|
+
"imports-impact-file": 2,
|
|
2541
|
+
"references-symbol-name": 3,
|
|
2542
|
+
"path-convention": 4
|
|
2543
|
+
};
|
|
2544
|
+
function collectAffectedTests(index, moduleGraph, changedFiles, changedSymbols, impactRows) {
|
|
2545
|
+
const changedNames = new Set(changedSymbols.map((changed) => changed.symbol.name));
|
|
2546
|
+
const impactFileDepth = /* @__PURE__ */ new Map();
|
|
2547
|
+
for (const row of impactRows) {
|
|
2548
|
+
const previous = impactFileDepth.get(row.symbol.file);
|
|
2549
|
+
if (previous === void 0 || row.depth < previous) {
|
|
2550
|
+
impactFileDepth.set(row.symbol.file, row.depth);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
const changedStems = new Set([...changedFiles].map(pathStem));
|
|
2554
|
+
const tests = [];
|
|
2555
|
+
for (const file of index.files) {
|
|
2556
|
+
if (!TEST_RE2.test(file.path)) continue;
|
|
2557
|
+
const imports = moduleGraph.edges.get(file.path) ?? /* @__PURE__ */ new Set();
|
|
2558
|
+
if (changedFiles.has(file.path)) {
|
|
2559
|
+
tests.push({ file: file.path, resolution: "exact", reason: "changed-test", distance: 0 });
|
|
2560
|
+
continue;
|
|
2561
|
+
}
|
|
2562
|
+
const changedImport = [...imports].find(
|
|
2563
|
+
(target) => changedFiles.has(target) && target !== file.path
|
|
2564
|
+
);
|
|
2565
|
+
if (changedImport !== void 0) {
|
|
2566
|
+
tests.push({
|
|
2567
|
+
file: file.path,
|
|
2568
|
+
resolution: "import-scoped",
|
|
2569
|
+
reason: "imports-changed-file",
|
|
2570
|
+
distance: 1
|
|
2571
|
+
});
|
|
2572
|
+
continue;
|
|
2573
|
+
}
|
|
2574
|
+
const impactImports = [...imports].filter((target) => impactFileDepth.has(target)).sort((a, b) => (impactFileDepth.get(a) ?? 0) - (impactFileDepth.get(b) ?? 0));
|
|
2575
|
+
if (impactImports.length > 0) {
|
|
2576
|
+
tests.push({
|
|
2577
|
+
file: file.path,
|
|
2578
|
+
resolution: "import-scoped",
|
|
2579
|
+
reason: "imports-impact-file",
|
|
2580
|
+
distance: impactFileDepth.get(impactImports[0]) ?? 2
|
|
2581
|
+
});
|
|
2582
|
+
continue;
|
|
2583
|
+
}
|
|
2584
|
+
if ((file.calls ?? []).some((call) => changedNames.has(call.name))) {
|
|
2585
|
+
tests.push({
|
|
2586
|
+
file: file.path,
|
|
2587
|
+
resolution: "name-only",
|
|
2588
|
+
reason: "references-symbol-name",
|
|
2589
|
+
distance: 1
|
|
2590
|
+
});
|
|
2591
|
+
continue;
|
|
2592
|
+
}
|
|
2593
|
+
if (changedStems.has(pathStem(file.path))) {
|
|
2594
|
+
tests.push({ file: file.path, resolution: "name-only", reason: "path-convention" });
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
return tests.sort(
|
|
2598
|
+
(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)
|
|
1112
2599
|
);
|
|
1113
2600
|
}
|
|
2601
|
+
function pathStem(filePath) {
|
|
2602
|
+
const base = filePath.split("/").pop() ?? filePath;
|
|
2603
|
+
return base.replace(/\.[A-Za-z0-9]+$/, "").replace(/\.(?:test|spec)$/, "").replace(/^test_/, "").replace(/_test$/, "");
|
|
2604
|
+
}
|
|
2605
|
+
function compareChanged(a, b) {
|
|
2606
|
+
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);
|
|
2607
|
+
}
|
|
2608
|
+
function compareBySymbol(a, b) {
|
|
2609
|
+
return a.symbol.file.localeCompare(b.symbol.file) || a.symbol.line - b.symbol.line || a.symbol.id.localeCompare(b.symbol.id);
|
|
2610
|
+
}
|
|
2611
|
+
function compareByDepthThenSymbol(a, b) {
|
|
2612
|
+
return a.depth - b.depth || LABEL_STRENGTH[b.resolution] - LABEL_STRENGTH[a.resolution] || compareBySymbol(a, b);
|
|
2613
|
+
}
|
|
2614
|
+
function comparePaths(a, b) {
|
|
2615
|
+
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);
|
|
2616
|
+
}
|
|
2617
|
+
function compareUnmapped(a, b) {
|
|
2618
|
+
return a.file.localeCompare(b.file) || a.side.localeCompare(b.side);
|
|
2619
|
+
}
|
|
2620
|
+
function renderChangeContext(result, maxChars = 12e3) {
|
|
2621
|
+
const sections = [
|
|
2622
|
+
{
|
|
2623
|
+
title: "CHANGED",
|
|
2624
|
+
rows: result.changed.map(
|
|
2625
|
+
(changed) => `${changed.change} ${changed.side === "base" ? "base " : ""}${symbolLabel(changed.symbol)} ${changed.symbol.file}:${changed.symbol.line} [${changed.resolution}]`
|
|
2626
|
+
)
|
|
2627
|
+
},
|
|
2628
|
+
{
|
|
2629
|
+
title: "DIRECT CALLERS",
|
|
2630
|
+
rows: result.directCallers.map(
|
|
2631
|
+
(row) => `${symbolLabel(row.symbol)} ${row.symbol.file}:${row.symbol.line} (called at ${row.via.callSite.file}:${row.via.callSite.line}) [${row.resolution}]`
|
|
2632
|
+
)
|
|
2633
|
+
},
|
|
2634
|
+
{
|
|
2635
|
+
title: "IMPORT DEPENDENTS",
|
|
2636
|
+
rows: result.importDependents.map(
|
|
2637
|
+
(dependent) => `${dependent.file} imports ${dependent.changedFile} ('${dependent.specifier}') [${dependent.resolution}]`
|
|
2638
|
+
)
|
|
2639
|
+
},
|
|
2640
|
+
{
|
|
2641
|
+
title: "SHORTEST PATHS TO ENTRY POINTS",
|
|
2642
|
+
rows: result.paths.map(
|
|
2643
|
+
(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}]`
|
|
2644
|
+
)
|
|
2645
|
+
},
|
|
2646
|
+
{
|
|
2647
|
+
title: "TRANSITIVE IMPACT",
|
|
2648
|
+
rows: result.impact.map(
|
|
2649
|
+
(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}]`
|
|
2650
|
+
)
|
|
2651
|
+
},
|
|
2652
|
+
{
|
|
2653
|
+
title: "LIKELY AFFECTED TESTS",
|
|
2654
|
+
rows: result.tests.map(
|
|
2655
|
+
(test) => `${test.file} (${test.reason}${test.distance !== void 0 ? `, distance ${test.distance}` : ""}) [${test.resolution}]`
|
|
2656
|
+
)
|
|
2657
|
+
},
|
|
2658
|
+
{
|
|
2659
|
+
title: "UNMAPPED",
|
|
2660
|
+
rows: result.unmapped.map(
|
|
2661
|
+
(entry) => `${entry.file} ${entry.side} lines ${entry.ranges.map((range) => `${range.startLine}-${range.endLine}`).join(", ")} (${entry.reason})`
|
|
2662
|
+
)
|
|
2663
|
+
}
|
|
2664
|
+
];
|
|
2665
|
+
const summary = `change context \u2014 source: ${result.source}${result.baseRef !== void 0 ? ` (base ${result.baseRef})` : ""}: ${result.changed.length} changed symbol(s)`;
|
|
2666
|
+
const warningLines = result.warnings.map((warning) => `- ${warning}`);
|
|
2667
|
+
const render = (kept) => {
|
|
2668
|
+
const lines = [summary];
|
|
2669
|
+
sections.forEach((section, index) => {
|
|
2670
|
+
if (section.rows.length === 0) {
|
|
2671
|
+
lines.push(`${section.title}: (none)`);
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
lines.push(`${section.title}:`);
|
|
2675
|
+
const take = kept[index] ?? 0;
|
|
2676
|
+
for (let row = 0; row < take; row++) lines.push(` ${section.rows[row]}`);
|
|
2677
|
+
if (take < section.rows.length) lines.push(` \u2026 (${section.rows.length - take} more)`);
|
|
2678
|
+
});
|
|
2679
|
+
lines.push("WARNINGS:");
|
|
2680
|
+
if (warningLines.length === 0) lines.push(" (none)");
|
|
2681
|
+
else for (const warning of warningLines) lines.push(` ${warning}`);
|
|
2682
|
+
return lines.join("\n");
|
|
2683
|
+
};
|
|
2684
|
+
const flattened = [];
|
|
2685
|
+
sections.forEach((section, index) => {
|
|
2686
|
+
section.rows.forEach((text) => flattened.push({ section: index, text }));
|
|
2687
|
+
});
|
|
2688
|
+
const countsFor = (count) => {
|
|
2689
|
+
const kept = sections.map(() => 0);
|
|
2690
|
+
for (let index = 0; index < count; index++) kept[flattened[index].section]++;
|
|
2691
|
+
return kept;
|
|
2692
|
+
};
|
|
2693
|
+
const total = flattened.length;
|
|
2694
|
+
const full = render(sections.map((section) => section.rows.length));
|
|
2695
|
+
if (full.length <= maxChars) return full;
|
|
2696
|
+
const truncationNote = "\n\u2026 truncated rows to fit the character cap (raise maxChars or narrow the change)";
|
|
2697
|
+
const headerOnly = render(countsFor(0));
|
|
2698
|
+
if (headerOnly.length > maxChars) {
|
|
2699
|
+
throw new Error(
|
|
2700
|
+
`maxChars ${maxChars} is below the renderer minimum ${headerOnly.length} (section headers and warnings cannot be kept)`
|
|
2701
|
+
);
|
|
2702
|
+
}
|
|
2703
|
+
if (headerOnly.length + truncationNote.length > maxChars) return headerOnly;
|
|
2704
|
+
const fits = (count) => render(countsFor(count)).length + truncationNote.length <= maxChars;
|
|
2705
|
+
let lo = 0;
|
|
2706
|
+
let hi = total;
|
|
2707
|
+
while (lo < hi) {
|
|
2708
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
2709
|
+
if (fits(mid)) lo = mid;
|
|
2710
|
+
else hi = mid - 1;
|
|
2711
|
+
}
|
|
2712
|
+
let out = render(countsFor(lo));
|
|
2713
|
+
if (lo < total) out += truncationNote;
|
|
2714
|
+
return out;
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
// src/context.ts
|
|
2718
|
+
var TEST_PATH_RE2 = /(^|\/)(tests?|__tests__)(\/|$)|\.(?:spec|test)\.[cm]?[jt]sx?$|(^|\/)(?:test_[^/]+\.py|[^/]+_test\.(?:py|go))$/i;
|
|
2719
|
+
var FILE_TOKEN_RE = /[A-Za-z0-9_./\\-]+\.[A-Za-z0-9]+/g;
|
|
2720
|
+
var CODE_TOKEN_RE = /[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)?/g;
|
|
2721
|
+
var STOP_WORDS = /* @__PURE__ */ new Set(
|
|
2722
|
+
[
|
|
2723
|
+
"a",
|
|
2724
|
+
"an",
|
|
2725
|
+
"and",
|
|
2726
|
+
"are",
|
|
2727
|
+
"be",
|
|
2728
|
+
"by",
|
|
2729
|
+
"does",
|
|
2730
|
+
"for",
|
|
2731
|
+
"from",
|
|
2732
|
+
"how",
|
|
2733
|
+
"in",
|
|
2734
|
+
"is",
|
|
2735
|
+
"it",
|
|
2736
|
+
"my",
|
|
2737
|
+
"of",
|
|
2738
|
+
"on",
|
|
2739
|
+
"or",
|
|
2740
|
+
"the",
|
|
2741
|
+
"this",
|
|
2742
|
+
"to",
|
|
2743
|
+
"what",
|
|
2744
|
+
"where",
|
|
2745
|
+
"why",
|
|
2746
|
+
"with"
|
|
2747
|
+
]
|
|
2748
|
+
);
|
|
2749
|
+
var CHANGE_RE = /\b(fix|bug|broken|breaks?|change|changed|current|working\s+tree|regression|impact|modified|duplicate|fails?|failure)\b/i;
|
|
2750
|
+
var TEST_RE3 = /\b(test|tests|spec|specs|failing|assert|coverage)\b/i;
|
|
2751
|
+
var ARCH_RE = /\b(architecture|architectural|flow|loading|authentication|authorization|plugin|startup|overview|pipeline|lifecycle|design|works?)\b/i;
|
|
2752
|
+
var EXPLORE_RE = /\b(where|implement|add|feature|introduce|entry\s+point|similar|explore|understand|locate)\b/i;
|
|
2753
|
+
function unique(values, key) {
|
|
2754
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2755
|
+
const out = [];
|
|
2756
|
+
for (const value of values) {
|
|
2757
|
+
const id = key(value);
|
|
2758
|
+
if (seen.has(id)) continue;
|
|
2759
|
+
seen.add(id);
|
|
2760
|
+
out.push(value);
|
|
2761
|
+
}
|
|
2762
|
+
return out;
|
|
2763
|
+
}
|
|
2764
|
+
function clamp2(value, min, max, fallback) {
|
|
2765
|
+
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
2766
|
+
return Math.min(max, Math.max(min, Math.floor(value)));
|
|
2767
|
+
}
|
|
2768
|
+
function wordBoundary(name2) {
|
|
2769
|
+
return new RegExp(`(^|[^A-Za-z0-9_$])${name2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?=$|[^A-Za-z0-9_$])`);
|
|
2770
|
+
}
|
|
2771
|
+
function symbolLabel2(symbol) {
|
|
2772
|
+
return `${symbol.kind} ${symbol.signature || symbol.name} \u2014 ${symbol.file}:${symbol.line}`;
|
|
2773
|
+
}
|
|
2774
|
+
function isTestFile(file) {
|
|
2775
|
+
return TEST_PATH_RE2.test(file);
|
|
2776
|
+
}
|
|
2777
|
+
function fileMentionMatches(file, mentions) {
|
|
2778
|
+
const lower = file.toLowerCase();
|
|
2779
|
+
return mentions.some((mention) => {
|
|
2780
|
+
const normalized = mention.replace(/\\/g, "/").toLowerCase();
|
|
2781
|
+
return lower === normalized || lower.endsWith(`/${normalized}`) || lower.includes(normalized);
|
|
2782
|
+
});
|
|
2783
|
+
}
|
|
2784
|
+
function routeTask(task, index) {
|
|
2785
|
+
const text = task.trim();
|
|
2786
|
+
const mentionedFiles = unique(text.match(FILE_TOKEN_RE) ?? [], (value) => value.toLowerCase()).map((value) => value.replace(/\\/g, "/")).filter((value) => !index || index.files.some((file) => fileMentionMatches(file.path, [value])));
|
|
2787
|
+
const exactSymbolNames = index ? unique(
|
|
2788
|
+
index.files.flatMap((file) => file.symbols.map((symbol) => symbol.name)),
|
|
2789
|
+
(name2) => name2
|
|
2790
|
+
).filter((name2) => wordBoundary(name2).test(text)).sort((a, b) => b.length - a.length || a.localeCompare(b)) : [];
|
|
2791
|
+
const codeTokens = unique(
|
|
2792
|
+
(text.match(CODE_TOKEN_RE) ?? []).map((token) => token.replace(/^[^A-Za-z_$]+|[^A-Za-z0-9_$]+$/g, "")).filter((token) => token.length >= 3 && !STOP_WORDS.has(token.toLowerCase())),
|
|
2793
|
+
(token) => token.toLowerCase()
|
|
2794
|
+
);
|
|
2795
|
+
const queries = unique(
|
|
2796
|
+
[...exactSymbolNames, ...codeTokens.filter((token) => !exactSymbolNames.includes(token))],
|
|
2797
|
+
(value) => value.toLowerCase()
|
|
2798
|
+
).slice(0, 12);
|
|
2799
|
+
const kind = TEST_RE3.test(text) || mentionedFiles.some(isTestFile) ? "test" : CHANGE_RE.test(text) ? "change" : exactSymbolNames.length > 0 ? "symbol" : EXPLORE_RE.test(text) ? "exploration" : ARCH_RE.test(text) ? "architecture" : "ambiguous";
|
|
2800
|
+
const includeChangeContext = kind === "change" || kind === "test";
|
|
2801
|
+
const includeRelationships = kind === "change" || kind === "test" || kind === "symbol";
|
|
2802
|
+
const sources = /* @__PURE__ */ new Set();
|
|
2803
|
+
if (kind === "change" || kind === "test") sources.add("change context");
|
|
2804
|
+
if (kind === "architecture" || kind === "exploration" || kind === "ambiguous") {
|
|
2805
|
+
sources.add("repo map");
|
|
2806
|
+
}
|
|
2807
|
+
if (queries.length > 0) sources.add("ranked search");
|
|
2808
|
+
if (includeRelationships) sources.add("call graph");
|
|
2809
|
+
if (mentionedFiles.length > 0) sources.add("file relevance");
|
|
2810
|
+
if (sources.size === 0) sources.add("repo map");
|
|
2811
|
+
return {
|
|
2812
|
+
kind,
|
|
2813
|
+
queries,
|
|
2814
|
+
mentionedFiles,
|
|
2815
|
+
exactSymbolNames,
|
|
2816
|
+
sources: [...sources],
|
|
2817
|
+
includeChangeContext,
|
|
2818
|
+
includeRelationships
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
function routeScore(route, hit) {
|
|
2822
|
+
let score = hit.score * 30;
|
|
2823
|
+
if (route.exactSymbolNames.includes(hit.name)) score += 100;
|
|
2824
|
+
if (fileMentionMatches(hit.file, route.mentionedFiles)) score += 30;
|
|
2825
|
+
if (isTestFile(hit.file) && route.kind !== "test") score -= 10;
|
|
2826
|
+
return score;
|
|
2827
|
+
}
|
|
2828
|
+
function gatherPrimarySymbols(index, route, maxSymbols, changedIds, changedFiles, refs) {
|
|
2829
|
+
const hits = [];
|
|
2830
|
+
for (const query of route.queries) {
|
|
2831
|
+
hits.push(...searchSymbols(index, { query }, Math.max(maxSymbols * 3, 20), refs));
|
|
2832
|
+
}
|
|
2833
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2834
|
+
for (const hit of hits) {
|
|
2835
|
+
const exact = route.exactSymbolNames.includes(hit.name);
|
|
2836
|
+
let score = routeScore(route, hit);
|
|
2837
|
+
if (changedIds.has(hit.id)) score += 80;
|
|
2838
|
+
else if (changedFiles.has(hit.file)) score += 55;
|
|
2839
|
+
if (route.kind === "test" && isTestFile(hit.file)) score += 35;
|
|
2840
|
+
const candidate = { symbol: hit, score, provenance: exact ? "exact" : "lexical" };
|
|
2841
|
+
const previous = byId.get(hit.id);
|
|
2842
|
+
if (!previous || candidate.score > previous.score) byId.set(hit.id, candidate);
|
|
2843
|
+
}
|
|
2844
|
+
if (route.includeChangeContext) {
|
|
2845
|
+
for (const file of index.files) {
|
|
2846
|
+
if (!changedFiles.has(file.path)) continue;
|
|
2847
|
+
for (const symbol of file.symbols) {
|
|
2848
|
+
if (byId.has(symbol.id)) continue;
|
|
2849
|
+
byId.set(symbol.id, {
|
|
2850
|
+
symbol,
|
|
2851
|
+
score: 70,
|
|
2852
|
+
provenance: "lexical"
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
return [...byId.values()].sort(
|
|
2858
|
+
(a, b) => b.score - a.score || Number(b.provenance === "exact") - Number(a.provenance === "exact") || a.symbol.file.localeCompare(b.symbol.file) || a.symbol.line - b.symbol.line || a.symbol.id.localeCompare(b.symbol.id)
|
|
2859
|
+
).slice(0, maxSymbols);
|
|
2860
|
+
}
|
|
2861
|
+
function edgeText(graph, edge, direction) {
|
|
2862
|
+
const source = edge.sourceId ? graph.symbolsById.get(edge.sourceId) : void 0;
|
|
2863
|
+
const target = graph.symbolsById.get(edge.targetId);
|
|
2864
|
+
const sourceText = source ? `${source.name} (${source.file}:${source.line})` : "module";
|
|
2865
|
+
const targetText = target ? `${target.name} (${target.file}:${target.line})` : edge.name;
|
|
2866
|
+
const arrow = direction === "caller" ? `${sourceText} \u2192 ${targetText}` : `${sourceText} \u2192 ${targetText}`;
|
|
2867
|
+
return `${arrow} at ${edge.callSite.file}:${edge.callSite.line} [${edge.resolution}; ${edge.provenance}]`;
|
|
2868
|
+
}
|
|
2869
|
+
function collectRelationships(graph, primary) {
|
|
2870
|
+
const rows = [];
|
|
2871
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2872
|
+
const add = (edge, direction, priority) => {
|
|
2873
|
+
const key = `${edge.sourceId ?? "module"}:${edge.targetId}:${edge.callSite.file}:${edge.callSite.line}`;
|
|
2874
|
+
if (seen.has(key)) return;
|
|
2875
|
+
seen.add(key);
|
|
2876
|
+
rows.push({
|
|
2877
|
+
key,
|
|
2878
|
+
text: edgeText(graph, edge, direction),
|
|
2879
|
+
resolution: edge.resolution,
|
|
2880
|
+
priority
|
|
2881
|
+
});
|
|
2882
|
+
};
|
|
2883
|
+
for (const candidate of primary) {
|
|
2884
|
+
for (const edge of graph.incoming.get(candidate.symbol.id) ?? []) add(edge, "caller", 100);
|
|
2885
|
+
for (const edge of graph.outgoing.get(candidate.symbol.id) ?? []) add(edge, "callee", 90);
|
|
2886
|
+
}
|
|
2887
|
+
return rows.sort(
|
|
2888
|
+
(a, b) => b.priority - a.priority || a.resolution.localeCompare(b.resolution) || a.text.localeCompare(b.text)
|
|
2889
|
+
);
|
|
2890
|
+
}
|
|
2891
|
+
function addFile(files, path9, score, reason) {
|
|
2892
|
+
const previous = files.get(path9);
|
|
2893
|
+
if (!previous || score > previous.score) files.set(path9, { path: path9, score, reason });
|
|
2894
|
+
}
|
|
2895
|
+
function gatherFiles(index, route, primary, change, maxFiles) {
|
|
2896
|
+
const files = /* @__PURE__ */ new Map();
|
|
2897
|
+
for (const file of index.files) {
|
|
2898
|
+
const base = Math.min(scoreFile(file), 8);
|
|
2899
|
+
if (route.kind === "architecture" || route.kind === "exploration" || route.kind === "ambiguous") {
|
|
2900
|
+
addFile(files, file.path, base, "structural importance");
|
|
2901
|
+
}
|
|
2902
|
+
if (fileMentionMatches(file.path, route.mentionedFiles)) addFile(files, file.path, 120, "task-mentioned file");
|
|
2903
|
+
}
|
|
2904
|
+
for (const candidate of primary) {
|
|
2905
|
+
addFile(files, candidate.symbol.file, candidate.score + 45, "primary symbol");
|
|
2906
|
+
}
|
|
2907
|
+
if (change) {
|
|
2908
|
+
for (const changed of change.changed) addFile(files, changed.symbol.file, 115, "current change");
|
|
2909
|
+
for (const dependent of change.importDependents) addFile(files, dependent.file, 65, "import dependent");
|
|
2910
|
+
for (const test of change.tests) addFile(files, test.file, 75, "likely affected test");
|
|
2911
|
+
for (const pathRow of change.paths) {
|
|
2912
|
+
addFile(files, pathRow.entry.file, 60, "entry path");
|
|
2913
|
+
addFile(files, pathRow.changed.file, 90, "changed path");
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
for (const entry of rankRepoMap(index, { topFiles: maxFiles * 2, symbolsPerFile: 8 })) {
|
|
2917
|
+
addFile(files, entry.path, Math.min(35, entry.score + 10), "repo map");
|
|
2918
|
+
}
|
|
2919
|
+
return [...files.values()].sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).slice(0, maxFiles);
|
|
2920
|
+
}
|
|
2921
|
+
function confidenceOf(primary, relationships, change) {
|
|
2922
|
+
const out = { exact: 0, "import-scoped": 0, "name-only": 0 };
|
|
2923
|
+
for (const symbol of primary) if (symbol.provenance === "exact") out.exact++;
|
|
2924
|
+
for (const relation of relationships) out[relation.resolution]++;
|
|
2925
|
+
if (change) {
|
|
2926
|
+
for (const row of [...change.directCallers, ...change.impact]) out[row.resolution]++;
|
|
2927
|
+
for (const dependent of change.importDependents) out[dependent.resolution]++;
|
|
2928
|
+
for (const test of change.tests) out[test.resolution]++;
|
|
2929
|
+
}
|
|
2930
|
+
return out;
|
|
2931
|
+
}
|
|
2932
|
+
function renderRows(result, budgetChars) {
|
|
2933
|
+
const lines = [
|
|
2934
|
+
`Task context \u2014 ${result.route.kind}`,
|
|
2935
|
+
`Task: ${result.task}`,
|
|
2936
|
+
`Sources: ${result.route.sources.join(", ")}`,
|
|
2937
|
+
"",
|
|
2938
|
+
"Primary symbols:"
|
|
2939
|
+
];
|
|
2940
|
+
if (result.primarySymbols.length === 0) lines.push("- (none found)");
|
|
2941
|
+
const optional = [];
|
|
2942
|
+
for (const candidate2 of result.primarySymbols) {
|
|
2943
|
+
optional.push({
|
|
2944
|
+
priority: 1e3 + candidate2.score,
|
|
2945
|
+
key: `symbol:${candidate2.symbol.id}`,
|
|
2946
|
+
section: "primary",
|
|
2947
|
+
line: `- [${candidate2.provenance}] ${symbolLabel2(candidate2.symbol)}`
|
|
2948
|
+
});
|
|
2949
|
+
}
|
|
2950
|
+
optional.push({ priority: 700, key: "files-header", section: "files", line: "\nRelevant files:" });
|
|
2951
|
+
for (const file of result.relevantFiles) {
|
|
2952
|
+
optional.push({ priority: 500 + file.score, key: `file:${file.path}`, section: "files", line: `- ${file.path} (${file.reason})` });
|
|
2953
|
+
}
|
|
2954
|
+
optional.push({ priority: 400, key: "relationships-header", section: "relationships", line: "\nRelationships:" });
|
|
2955
|
+
for (const relation of result.relationships) {
|
|
2956
|
+
optional.push({ priority: relation.priority, key: `relation:${relation.key}`, section: "relationships", line: `- ${relation.text}` });
|
|
2957
|
+
}
|
|
2958
|
+
if (result.changeContext) {
|
|
2959
|
+
optional.push({ priority: 850, key: "changes-header", section: "changes", line: "\nCurrent changes:" });
|
|
2960
|
+
for (const changed of result.changeContext.changed) {
|
|
2961
|
+
optional.push({
|
|
2962
|
+
priority: 840,
|
|
2963
|
+
key: `changed:${changed.side}:${changed.symbol.id}`,
|
|
2964
|
+
section: "changes",
|
|
2965
|
+
line: `- ${changed.change} ${symbolLabel2(changed.symbol)} [${changed.resolution}]`
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
if (result.changeContext.unmapped.length > 0) {
|
|
2969
|
+
optional.push({
|
|
2970
|
+
priority: 830,
|
|
2971
|
+
key: "changes-unmapped",
|
|
2972
|
+
section: "changes",
|
|
2973
|
+
line: `- unmapped: ${result.changeContext.unmapped.map((entry) => entry.file).join(", ")}`
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
if (result.tests.length > 0) {
|
|
2978
|
+
optional.push({ priority: 350, key: "tests-header", section: "tests", line: "\nLikely affected tests:" });
|
|
2979
|
+
for (const test of result.tests) optional.push({ priority: 340, key: `test:${test}`, section: "tests", line: `- ${test}` });
|
|
2980
|
+
}
|
|
2981
|
+
if (result.warnings.length > 0) {
|
|
2982
|
+
optional.push({ priority: 200, key: "warnings-header", section: "warnings", line: "\nWarnings:" });
|
|
2983
|
+
for (const warning of result.warnings) optional.push({ priority: 190, key: `warning:${warning}`, section: "warnings", line: `- ${warning}` });
|
|
2984
|
+
}
|
|
2985
|
+
const ordered = optional.map((item, index) => ({ ...item, index })).sort((a, b) => b.priority - a.priority || a.index - b.index || a.key.localeCompare(b.key));
|
|
2986
|
+
const footerReserve = 100;
|
|
2987
|
+
let truncated = false;
|
|
2988
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2989
|
+
for (const item of ordered) {
|
|
2990
|
+
if (seen.has(item.key)) continue;
|
|
2991
|
+
const next = lines.join("\n") + `
|
|
2992
|
+
${item.line}`;
|
|
2993
|
+
if (next.length + footerReserve > budgetChars) {
|
|
2994
|
+
truncated = true;
|
|
2995
|
+
continue;
|
|
2996
|
+
}
|
|
2997
|
+
seen.add(item.key);
|
|
2998
|
+
lines.push(item.line);
|
|
2999
|
+
}
|
|
3000
|
+
lines.push(
|
|
3001
|
+
"",
|
|
3002
|
+
`Confidence: exact ${result.confidence.exact}; import-scoped ${result.confidence["import-scoped"]}; name-only ${result.confidence["name-only"]}`
|
|
3003
|
+
);
|
|
3004
|
+
const body = lines.join("\n");
|
|
3005
|
+
const withFooter = (bodyText, wasTruncated) => {
|
|
3006
|
+
let usedChars = bodyText.length + 1;
|
|
3007
|
+
for (let iteration = 0; iteration < 10; iteration++) {
|
|
3008
|
+
const footer2 = `Budget: ${usedChars} / ${budgetChars} chars${wasTruncated ? " (trimmed low-priority context)" : ""}`;
|
|
3009
|
+
const text2 = `${bodyText}
|
|
3010
|
+
${footer2}`;
|
|
3011
|
+
const nextUsedChars = text2.length;
|
|
3012
|
+
if (nextUsedChars === usedChars) return { text: text2, usedChars: nextUsedChars };
|
|
3013
|
+
usedChars = nextUsedChars;
|
|
3014
|
+
}
|
|
3015
|
+
const footer = `Budget: ${usedChars} / ${budgetChars} chars${wasTruncated ? " (trimmed low-priority context)" : ""}`;
|
|
3016
|
+
const text = `${bodyText}
|
|
3017
|
+
${footer}`;
|
|
3018
|
+
return { text, usedChars: text.length };
|
|
3019
|
+
};
|
|
3020
|
+
const complete = withFooter(body, truncated);
|
|
3021
|
+
if (complete.usedChars <= budgetChars) {
|
|
3022
|
+
return { text: complete.text, usedChars: complete.usedChars, truncated };
|
|
3023
|
+
}
|
|
3024
|
+
truncated = true;
|
|
3025
|
+
const candidate = (prefixLength) => {
|
|
3026
|
+
const prefix = `${body.slice(0, prefixLength).trimEnd()}\u2026`;
|
|
3027
|
+
return withFooter(prefix, true);
|
|
3028
|
+
};
|
|
3029
|
+
let low = 0;
|
|
3030
|
+
let high = body.length;
|
|
3031
|
+
let best = candidate(0);
|
|
3032
|
+
while (low <= high) {
|
|
3033
|
+
const middle = Math.floor((low + high) / 2);
|
|
3034
|
+
const current = candidate(middle);
|
|
3035
|
+
if (current.usedChars <= budgetChars) {
|
|
3036
|
+
best = current;
|
|
3037
|
+
low = middle + 1;
|
|
3038
|
+
} else {
|
|
3039
|
+
high = middle - 1;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
return { text: best.text, usedChars: best.usedChars, truncated };
|
|
3043
|
+
}
|
|
3044
|
+
async function buildTaskContext(index, task, options = {}) {
|
|
3045
|
+
const budgetChars = clamp2(options.budgetChars, 300, 2e4, 5e3);
|
|
3046
|
+
const maxFiles = clamp2(options.maxFiles, 1, 50, 12);
|
|
3047
|
+
const maxSymbols = clamp2(options.maxSymbols, 1, 50, 10);
|
|
3048
|
+
const route = routeTask(task, index);
|
|
3049
|
+
let changeContext = null;
|
|
3050
|
+
const warnings = [];
|
|
3051
|
+
if (route.includeChangeContext) {
|
|
3052
|
+
try {
|
|
3053
|
+
changeContext = await buildChangeContext(index, {
|
|
3054
|
+
kind: "git",
|
|
3055
|
+
root: index.root,
|
|
3056
|
+
baseRef: "HEAD"
|
|
3057
|
+
});
|
|
3058
|
+
warnings.push(...changeContext.warnings);
|
|
3059
|
+
} catch (error) {
|
|
3060
|
+
warnings.push(`change context unavailable: ${error.message ?? String(error)}`);
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
const changedIds = new Set((changeContext?.changed ?? []).map((entry) => entry.symbol.id));
|
|
3064
|
+
const changedFiles = new Set((changeContext?.changed ?? []).map((entry) => entry.symbol.file));
|
|
3065
|
+
const primarySymbols = gatherPrimarySymbols(
|
|
3066
|
+
index,
|
|
3067
|
+
route,
|
|
3068
|
+
maxSymbols,
|
|
3069
|
+
changedIds,
|
|
3070
|
+
changedFiles,
|
|
3071
|
+
callerCounts(index)
|
|
3072
|
+
);
|
|
3073
|
+
const graph = route.includeRelationships ? buildReferenceGraph(index) : null;
|
|
3074
|
+
const relationships = graph ? collectRelationships(graph, primarySymbols) : [];
|
|
3075
|
+
if (changeContext) {
|
|
3076
|
+
const existing = new Set(relationships.map((row) => row.key));
|
|
3077
|
+
for (const row of changeContext.directCallers) {
|
|
3078
|
+
const key = `change:${row.symbol.id}:${row.via.callSite.file}:${row.via.callSite.line}`;
|
|
3079
|
+
if (existing.has(key)) continue;
|
|
3080
|
+
existing.add(key);
|
|
3081
|
+
relationships.push({
|
|
3082
|
+
key,
|
|
3083
|
+
text: `${row.symbol.name} (${row.symbol.file}:${row.symbol.line}) \u2192 changed symbol at ${row.via.callSite.file}:${row.via.callSite.line} [${row.resolution}; ${row.via.reason}]`,
|
|
3084
|
+
resolution: row.resolution,
|
|
3085
|
+
priority: 110
|
|
3086
|
+
});
|
|
3087
|
+
}
|
|
3088
|
+
relationships.sort((a, b) => b.priority - a.priority || a.text.localeCompare(b.text));
|
|
3089
|
+
}
|
|
3090
|
+
const relevantFiles = gatherFiles(index, route, primarySymbols, changeContext, maxFiles);
|
|
3091
|
+
const tests = unique(changeContext?.tests.map((test) => test.file) ?? [], (file) => file);
|
|
3092
|
+
const confidence = confidenceOf(primarySymbols, relationships, changeContext);
|
|
3093
|
+
const partial = {
|
|
3094
|
+
root: index.root,
|
|
3095
|
+
task,
|
|
3096
|
+
route,
|
|
3097
|
+
primarySymbols,
|
|
3098
|
+
relevantFiles,
|
|
3099
|
+
relationships,
|
|
3100
|
+
changeContext,
|
|
3101
|
+
tests,
|
|
3102
|
+
confidence,
|
|
3103
|
+
warnings
|
|
3104
|
+
};
|
|
3105
|
+
const rendered = renderRows(partial, budgetChars);
|
|
3106
|
+
return {
|
|
3107
|
+
...partial,
|
|
3108
|
+
budget: {
|
|
3109
|
+
usedChars: rendered.usedChars,
|
|
3110
|
+
budgetChars,
|
|
3111
|
+
truncated: rendered.truncated
|
|
3112
|
+
}
|
|
3113
|
+
};
|
|
3114
|
+
}
|
|
3115
|
+
function renderTaskContext(result) {
|
|
3116
|
+
const rendered = renderRows(result, result.budget.budgetChars);
|
|
3117
|
+
return rendered.text;
|
|
3118
|
+
}
|
|
1114
3119
|
|
|
1115
3120
|
// src/tools.ts
|
|
1116
3121
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
1117
|
-
import
|
|
3122
|
+
import path8 from "path";
|
|
1118
3123
|
|
|
1119
3124
|
// src/config.ts
|
|
1120
3125
|
var DEFAULTS = {
|
|
@@ -1123,7 +3128,8 @@ var DEFAULTS = {
|
|
|
1123
3128
|
mapMaxChars: 3200,
|
|
1124
3129
|
mapTtlMs: 6e4,
|
|
1125
3130
|
autoInject: true,
|
|
1126
|
-
codeHealth: false
|
|
3131
|
+
codeHealth: false,
|
|
3132
|
+
toolSurface: "full"
|
|
1127
3133
|
};
|
|
1128
3134
|
var state = { current: { ...DEFAULTS } };
|
|
1129
3135
|
function applyConfig(partial) {
|
|
@@ -1141,6 +3147,9 @@ function applyConfig(partial) {
|
|
|
1141
3147
|
if (!Number.isFinite(state.current.mapTtlMs) || state.current.mapTtlMs < 1e3) {
|
|
1142
3148
|
state.current.mapTtlMs = DEFAULTS.mapTtlMs;
|
|
1143
3149
|
}
|
|
3150
|
+
if (state.current.toolSurface !== "full" && state.current.toolSurface !== "compact") {
|
|
3151
|
+
state.current.toolSurface = DEFAULTS.toolSurface;
|
|
3152
|
+
}
|
|
1144
3153
|
}
|
|
1145
3154
|
function getConfig() {
|
|
1146
3155
|
return state.current;
|
|
@@ -1172,7 +3181,7 @@ function createIndexCache(load, ttlMs = 6e4, now = Date.now) {
|
|
|
1172
3181
|
}
|
|
1173
3182
|
return {
|
|
1174
3183
|
get(root, force = false) {
|
|
1175
|
-
const resolvedRoot =
|
|
3184
|
+
const resolvedRoot = path8.resolve(root);
|
|
1176
3185
|
const key = cacheKeyForRoot(resolvedRoot);
|
|
1177
3186
|
const running = inFlight.get(key);
|
|
1178
3187
|
if (running) {
|
|
@@ -1209,7 +3218,7 @@ async function resolveRoot(arg, exec) {
|
|
|
1209
3218
|
const cwd = exec.agent?.session?.header?.cwd;
|
|
1210
3219
|
const base = arg ?? cwd ?? process.cwd();
|
|
1211
3220
|
const root = await findRepoRoot(base);
|
|
1212
|
-
if (!root) throw new Error(`no git repository found from ${
|
|
3221
|
+
if (!root) throw new Error(`no git repository found from ${path8.resolve(base)}`);
|
|
1213
3222
|
return root;
|
|
1214
3223
|
}
|
|
1215
3224
|
function hitsToSearchMeta(hits) {
|
|
@@ -1224,7 +3233,7 @@ function hitsToSearchMeta(hits) {
|
|
|
1224
3233
|
}
|
|
1225
3234
|
return {
|
|
1226
3235
|
shape: "matches",
|
|
1227
|
-
files: [...byFile.entries()].map(([
|
|
3236
|
+
files: [...byFile.entries()].map(([path9, matches]) => ({ path: path9, matches })),
|
|
1228
3237
|
truncated: false,
|
|
1229
3238
|
total: hits.length
|
|
1230
3239
|
};
|
|
@@ -1247,7 +3256,7 @@ function searchCardFromMeta(meta) {
|
|
|
1247
3256
|
var tools = [
|
|
1248
3257
|
defineTool({
|
|
1249
3258
|
name: "code_index",
|
|
1250
|
-
description: "Manage the
|
|
3259
|
+
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.",
|
|
1251
3260
|
parameters: {
|
|
1252
3261
|
action: {
|
|
1253
3262
|
type: "string",
|
|
@@ -1477,6 +3486,135 @@ var tools = [
|
|
|
1477
3486
|
}
|
|
1478
3487
|
}
|
|
1479
3488
|
}),
|
|
3489
|
+
defineTool({
|
|
3490
|
+
name: "code_change_context",
|
|
3491
|
+
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.",
|
|
3492
|
+
parameters: {
|
|
3493
|
+
repoRoot: {
|
|
3494
|
+
type: "string",
|
|
3495
|
+
description: "Optional absolute repo path; defaults to the session workspace root."
|
|
3496
|
+
},
|
|
3497
|
+
diff: {
|
|
3498
|
+
type: "string",
|
|
3499
|
+
description: "Caller-supplied unified diff text; when present no git command runs."
|
|
3500
|
+
},
|
|
3501
|
+
baseRef: {
|
|
3502
|
+
type: "string",
|
|
3503
|
+
description: 'Git ref to diff against (default "HEAD"); also the baseline read for deletions.'
|
|
3504
|
+
},
|
|
3505
|
+
files: {
|
|
3506
|
+
type: "array",
|
|
3507
|
+
items: { type: "string" },
|
|
3508
|
+
description: "Repo-relative files to treat as wholly changed (mutually exclusive with diff/symbols)."
|
|
3509
|
+
},
|
|
3510
|
+
symbols: {
|
|
3511
|
+
type: "array",
|
|
3512
|
+
items: { type: "string" },
|
|
3513
|
+
description: "SymbolInfo ids to treat as changed (mutually exclusive with diff/files)."
|
|
3514
|
+
},
|
|
3515
|
+
maxDepth: {
|
|
3516
|
+
type: "number",
|
|
3517
|
+
description: "Transitive caller depth, 1..6 (default 3)."
|
|
3518
|
+
},
|
|
3519
|
+
maxImpact: {
|
|
3520
|
+
type: "number",
|
|
3521
|
+
description: "Max direct-caller / impact / dependent rows, 1..200 (default 40)."
|
|
3522
|
+
},
|
|
3523
|
+
maxPaths: {
|
|
3524
|
+
type: "number",
|
|
3525
|
+
description: "Max entry-point paths, 1..50 (default 10)."
|
|
3526
|
+
},
|
|
3527
|
+
maxTests: {
|
|
3528
|
+
type: "number",
|
|
3529
|
+
description: "Max affected-test rows, 1..100 (default 20)."
|
|
3530
|
+
}
|
|
3531
|
+
},
|
|
3532
|
+
output: {
|
|
3533
|
+
schema: { type: "string" },
|
|
3534
|
+
render: (_args, value) => [{ type: "text", text: value }]
|
|
3535
|
+
},
|
|
3536
|
+
presentCall: (args) => ({
|
|
3537
|
+
card: "generic",
|
|
3538
|
+
title: "Change context",
|
|
3539
|
+
kind: "search",
|
|
3540
|
+
rawInput: {
|
|
3541
|
+
baseRef: args.baseRef,
|
|
3542
|
+
files: args.files,
|
|
3543
|
+
symbols: args.symbols,
|
|
3544
|
+
diff: args.diff !== void 0 ? "(inline diff)" : void 0
|
|
3545
|
+
}
|
|
3546
|
+
}),
|
|
3547
|
+
async execute(args, exec) {
|
|
3548
|
+
try {
|
|
3549
|
+
const root = await resolveRoot(args.repoRoot, exec);
|
|
3550
|
+
const index = await getIndex(root);
|
|
3551
|
+
const input = changeContextInputFromArgs(args, root);
|
|
3552
|
+
const result = await buildChangeContext(index, input, {
|
|
3553
|
+
maxDepth: args.maxDepth,
|
|
3554
|
+
maxImpact: args.maxImpact,
|
|
3555
|
+
maxPaths: args.maxPaths,
|
|
3556
|
+
maxTests: args.maxTests
|
|
3557
|
+
});
|
|
3558
|
+
return renderChangeContext(result);
|
|
3559
|
+
} catch (error) {
|
|
3560
|
+
return `code_change_context: ${error.message ?? String(error)}`;
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
}),
|
|
3564
|
+
defineTool({
|
|
3565
|
+
name: "code_context",
|
|
3566
|
+
description: "Return the smallest useful structural context for a code task. Pass the task in plain language; the deterministic router combines ranked symbols, relevant files, call/import relationships, repository structure, current Git changes, affected tests, provenance, and a hard character budget. Existing specialized tools remain available for focused follow-up queries.",
|
|
3567
|
+
parameters: {
|
|
3568
|
+
task: {
|
|
3569
|
+
type: "string",
|
|
3570
|
+
required: true,
|
|
3571
|
+
description: "The code task or question the agent is currently trying to complete."
|
|
3572
|
+
},
|
|
3573
|
+
budgetChars: {
|
|
3574
|
+
type: "number",
|
|
3575
|
+
description: "Hard output budget in characters (default 5000)."
|
|
3576
|
+
},
|
|
3577
|
+
maxFiles: {
|
|
3578
|
+
type: "number",
|
|
3579
|
+
description: "Maximum relevant files to include (default 12)."
|
|
3580
|
+
},
|
|
3581
|
+
maxSymbols: {
|
|
3582
|
+
type: "number",
|
|
3583
|
+
description: "Maximum primary symbols to include (default 10)."
|
|
3584
|
+
},
|
|
3585
|
+
repoRoot: {
|
|
3586
|
+
type: "string",
|
|
3587
|
+
description: "Optional absolute repo path; defaults to the session workspace root."
|
|
3588
|
+
}
|
|
3589
|
+
},
|
|
3590
|
+
output: {
|
|
3591
|
+
schema: { type: "string" },
|
|
3592
|
+
render: (_args, value) => [{ type: "text", text: value }]
|
|
3593
|
+
},
|
|
3594
|
+
presentCall: (args) => ({
|
|
3595
|
+
card: "generic",
|
|
3596
|
+
title: "Task context",
|
|
3597
|
+
kind: "search",
|
|
3598
|
+
rawInput: { task: args.task, budgetChars: args.budgetChars }
|
|
3599
|
+
}),
|
|
3600
|
+
async execute(args, exec) {
|
|
3601
|
+
try {
|
|
3602
|
+
if (typeof args.task !== "string" || args.task.trim().length === 0) {
|
|
3603
|
+
throw new Error("task must be a non-empty string");
|
|
3604
|
+
}
|
|
3605
|
+
const root = await resolveRoot(args.repoRoot, exec);
|
|
3606
|
+
const index = await getIndex(root);
|
|
3607
|
+
const result = await buildTaskContext(index, args.task, {
|
|
3608
|
+
budgetChars: args.budgetChars,
|
|
3609
|
+
maxFiles: args.maxFiles,
|
|
3610
|
+
maxSymbols: args.maxSymbols
|
|
3611
|
+
});
|
|
3612
|
+
return renderTaskContext(result);
|
|
3613
|
+
} catch (error) {
|
|
3614
|
+
return `code_context: ${error.message ?? String(error)}`;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
}),
|
|
1480
3618
|
defineTool({
|
|
1481
3619
|
name: "code_health",
|
|
1482
3620
|
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).",
|
|
@@ -1526,7 +3664,8 @@ var settingsSchema = Schema.object({
|
|
|
1526
3664
|
mapMaxChars: Schema.number().default(3200),
|
|
1527
3665
|
mapTtlMs: Schema.number().default(6e4),
|
|
1528
3666
|
autoInject: Schema.boolean().default(true),
|
|
1529
|
-
codeHealth: Schema.boolean().default(false)
|
|
3667
|
+
codeHealth: Schema.boolean().default(false),
|
|
3668
|
+
toolSurface: Schema.string().default("full")
|
|
1530
3669
|
});
|
|
1531
3670
|
function registerSettings(getService, pluginConfig, onResolved) {
|
|
1532
3671
|
const service = getService("settings");
|
|
@@ -1558,7 +3697,8 @@ function apply(ctx, pluginConfig) {
|
|
|
1558
3697
|
);
|
|
1559
3698
|
});
|
|
1560
3699
|
}
|
|
1561
|
-
|
|
3700
|
+
const visibleTools = getConfig().toolSurface === "compact" ? tools.filter((tool) => ["code_index", "code_context", "code_health"].includes(tool.name)) : tools;
|
|
3701
|
+
for (const tool of visibleTools) {
|
|
1562
3702
|
if (tool.name === "code_health" && !getConfig().codeHealth) continue;
|
|
1563
3703
|
disposers.push(ctx.tools.register(tool));
|
|
1564
3704
|
console.log(`[dsh-code-index] registered tool: ${tool.name}`);
|
|
@@ -1623,11 +3763,14 @@ export {
|
|
|
1623
3763
|
DEFAULT_EXCLUDED_DIRS,
|
|
1624
3764
|
SUPPORTED_EXTS,
|
|
1625
3765
|
apply,
|
|
3766
|
+
buildChangeContext,
|
|
1626
3767
|
buildIndex,
|
|
1627
3768
|
buildIndexWithCache,
|
|
1628
3769
|
buildModuleGraph,
|
|
1629
3770
|
buildSymbolTable,
|
|
3771
|
+
buildTaskContext,
|
|
1630
3772
|
callerCounts,
|
|
3773
|
+
changeContextInputFromArgs,
|
|
1631
3774
|
defaultCachePath,
|
|
1632
3775
|
extractAll,
|
|
1633
3776
|
extractSymbols,
|
|
@@ -1639,9 +3782,15 @@ export {
|
|
|
1639
3782
|
loadIndex,
|
|
1640
3783
|
name,
|
|
1641
3784
|
parseFileToSymbols,
|
|
3785
|
+
parseUnifiedDiff,
|
|
1642
3786
|
rankRepoMap,
|
|
3787
|
+
readGitFileAtRef,
|
|
3788
|
+
readWorkingTreeDiff,
|
|
3789
|
+
renderChangeContext,
|
|
1643
3790
|
renderHit,
|
|
1644
3791
|
renderRepoMap,
|
|
3792
|
+
renderTaskContext,
|
|
3793
|
+
routeTask,
|
|
1645
3794
|
saveIndex,
|
|
1646
3795
|
scanRepo,
|
|
1647
3796
|
scoreFile,
|