dsh-code-index 0.6.1 → 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 +9 -0
- package/README.md +31 -2
- package/README.zh.md +11 -2
- package/dist/client.js +2 -1
- package/dist/client.js.map +1 -1
- package/dist/index.js +475 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/release-smoke.mjs +67 -1
package/dist/index.js
CHANGED
|
@@ -1533,8 +1533,8 @@ function resolveCallTargetsFor(fileSet, file, call, symbolsByName, symbolsByFile
|
|
|
1533
1533
|
if (!qualified) {
|
|
1534
1534
|
const locals = named.filter((s) => s.file === file.path && lexicallyVisible(s, call));
|
|
1535
1535
|
if (locals.length > 0) {
|
|
1536
|
-
const
|
|
1537
|
-
const owned =
|
|
1536
|
+
const unique2 = locals.length === 1;
|
|
1537
|
+
const owned = unique2 && isStructuralOwner(call, locals[0]);
|
|
1538
1538
|
const provenance = owned && call.fromId !== null ? "structural-owner" : "local-unique";
|
|
1539
1539
|
return toEdges(locals, call, file.path, provenance, owned ? "exact" : "name-only", maxCandidates);
|
|
1540
1540
|
}
|
|
@@ -1618,10 +1618,10 @@ function resolveCallTargetsFor(fileSet, file, call, symbolsByName, symbolsByFile
|
|
|
1618
1618
|
return [];
|
|
1619
1619
|
}
|
|
1620
1620
|
function toEdges(candidates, call, filePath, provenance, resolution, maxCandidates) {
|
|
1621
|
-
const
|
|
1622
|
-
const count =
|
|
1621
|
+
const unique2 = dedupeSymbols(candidates);
|
|
1622
|
+
const count = unique2.length;
|
|
1623
1623
|
const cap = maxCandidates > 0 ? Math.floor(maxCandidates) : 0;
|
|
1624
|
-
return
|
|
1624
|
+
return unique2.slice(0, cap).map((target) => ({
|
|
1625
1625
|
sourceId: call.fromId,
|
|
1626
1626
|
targetId: target.id,
|
|
1627
1627
|
callSite: { file: filePath, line: call.line },
|
|
@@ -1701,7 +1701,7 @@ function isSubsequence(query, name2) {
|
|
|
1701
1701
|
}
|
|
1702
1702
|
return false;
|
|
1703
1703
|
}
|
|
1704
|
-
function searchSymbols(index, filter, limit = 50) {
|
|
1704
|
+
function searchSymbols(index, filter, limit = 50, refs = callerCounts(index)) {
|
|
1705
1705
|
const q = (filter.query ?? "").trim();
|
|
1706
1706
|
const filePat = filter.file?.trim().toLowerCase();
|
|
1707
1707
|
const hits = [];
|
|
@@ -1715,7 +1715,6 @@ function searchSymbols(index, filter, limit = 50) {
|
|
|
1715
1715
|
hits.push({ ...sym, score });
|
|
1716
1716
|
}
|
|
1717
1717
|
}
|
|
1718
|
-
const refs = callerCounts(index);
|
|
1719
1718
|
hits.sort((a, b) => {
|
|
1720
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);
|
|
1721
1720
|
return ab;
|
|
@@ -2715,6 +2714,409 @@ function renderChangeContext(result, maxChars = 12e3) {
|
|
|
2715
2714
|
return out;
|
|
2716
2715
|
}
|
|
2717
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
|
+
}
|
|
3119
|
+
|
|
2718
3120
|
// src/tools.ts
|
|
2719
3121
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2720
3122
|
import path8 from "path";
|
|
@@ -2726,7 +3128,8 @@ var DEFAULTS = {
|
|
|
2726
3128
|
mapMaxChars: 3200,
|
|
2727
3129
|
mapTtlMs: 6e4,
|
|
2728
3130
|
autoInject: true,
|
|
2729
|
-
codeHealth: false
|
|
3131
|
+
codeHealth: false,
|
|
3132
|
+
toolSurface: "full"
|
|
2730
3133
|
};
|
|
2731
3134
|
var state = { current: { ...DEFAULTS } };
|
|
2732
3135
|
function applyConfig(partial) {
|
|
@@ -2744,6 +3147,9 @@ function applyConfig(partial) {
|
|
|
2744
3147
|
if (!Number.isFinite(state.current.mapTtlMs) || state.current.mapTtlMs < 1e3) {
|
|
2745
3148
|
state.current.mapTtlMs = DEFAULTS.mapTtlMs;
|
|
2746
3149
|
}
|
|
3150
|
+
if (state.current.toolSurface !== "full" && state.current.toolSurface !== "compact") {
|
|
3151
|
+
state.current.toolSurface = DEFAULTS.toolSurface;
|
|
3152
|
+
}
|
|
2747
3153
|
}
|
|
2748
3154
|
function getConfig() {
|
|
2749
3155
|
return state.current;
|
|
@@ -3155,6 +3561,60 @@ var tools = [
|
|
|
3155
3561
|
}
|
|
3156
3562
|
}
|
|
3157
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
|
+
}),
|
|
3158
3618
|
defineTool({
|
|
3159
3619
|
name: "code_health",
|
|
3160
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).",
|
|
@@ -3204,7 +3664,8 @@ var settingsSchema = Schema.object({
|
|
|
3204
3664
|
mapMaxChars: Schema.number().default(3200),
|
|
3205
3665
|
mapTtlMs: Schema.number().default(6e4),
|
|
3206
3666
|
autoInject: Schema.boolean().default(true),
|
|
3207
|
-
codeHealth: Schema.boolean().default(false)
|
|
3667
|
+
codeHealth: Schema.boolean().default(false),
|
|
3668
|
+
toolSurface: Schema.string().default("full")
|
|
3208
3669
|
});
|
|
3209
3670
|
function registerSettings(getService, pluginConfig, onResolved) {
|
|
3210
3671
|
const service = getService("settings");
|
|
@@ -3236,7 +3697,8 @@ function apply(ctx, pluginConfig) {
|
|
|
3236
3697
|
);
|
|
3237
3698
|
});
|
|
3238
3699
|
}
|
|
3239
|
-
|
|
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) {
|
|
3240
3702
|
if (tool.name === "code_health" && !getConfig().codeHealth) continue;
|
|
3241
3703
|
disposers.push(ctx.tools.register(tool));
|
|
3242
3704
|
console.log(`[dsh-code-index] registered tool: ${tool.name}`);
|
|
@@ -3306,6 +3768,7 @@ export {
|
|
|
3306
3768
|
buildIndexWithCache,
|
|
3307
3769
|
buildModuleGraph,
|
|
3308
3770
|
buildSymbolTable,
|
|
3771
|
+
buildTaskContext,
|
|
3309
3772
|
callerCounts,
|
|
3310
3773
|
changeContextInputFromArgs,
|
|
3311
3774
|
defaultCachePath,
|
|
@@ -3326,6 +3789,8 @@ export {
|
|
|
3326
3789
|
renderChangeContext,
|
|
3327
3790
|
renderHit,
|
|
3328
3791
|
renderRepoMap,
|
|
3792
|
+
renderTaskContext,
|
|
3793
|
+
routeTask,
|
|
3329
3794
|
saveIndex,
|
|
3330
3795
|
scanRepo,
|
|
3331
3796
|
scoreFile,
|