dsh-code-index 0.8.0 → 0.9.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 +13 -0
- package/README.md +85 -62
- package/README.zh.md +87 -44
- package/dist/index.d.ts +634 -0
- package/dist/index.js +488 -172
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/scripts/context-pack-demo.mjs +49 -0
- package/scripts/release-smoke.mjs +10 -0
package/dist/index.js
CHANGED
|
@@ -800,9 +800,9 @@ function rustBindings(node) {
|
|
|
800
800
|
return name2 ? [{ imported: name2, local: name2, kind: "named" }] : [];
|
|
801
801
|
}
|
|
802
802
|
case "use_as_clause": {
|
|
803
|
-
const
|
|
803
|
+
const path11 = node.childForFieldName("path");
|
|
804
804
|
const alias = node.childForFieldName("alias")?.text.trim();
|
|
805
|
-
const imported =
|
|
805
|
+
const imported = path11 ? rustLeafName(path11) : null;
|
|
806
806
|
return imported && alias ? [{ imported, local: alias, kind: "named" }] : [];
|
|
807
807
|
}
|
|
808
808
|
case "scoped_use_list": {
|
|
@@ -823,8 +823,8 @@ function rustLeafName(node) {
|
|
|
823
823
|
return node.childForFieldName("name")?.text.trim() ?? null;
|
|
824
824
|
}
|
|
825
825
|
function javaImport(node, line) {
|
|
826
|
-
const
|
|
827
|
-
const specifier = dottedToPath(
|
|
826
|
+
const path11 = node.namedChildren.find((child) => child?.type === "scoped_identifier");
|
|
827
|
+
const specifier = dottedToPath(path11?.text);
|
|
828
828
|
if (!specifier) return [];
|
|
829
829
|
const wildcard = node.namedChildren.some((child) => child?.type === "asterisk");
|
|
830
830
|
if (wildcard) {
|
|
@@ -832,7 +832,7 @@ function javaImport(node, line) {
|
|
|
832
832
|
{ specifier, line, kind: "import", bindings: [{ imported: "*", local: "*", kind: "glob" }] }
|
|
833
833
|
];
|
|
834
834
|
}
|
|
835
|
-
const imported =
|
|
835
|
+
const imported = path11?.childForFieldName("name")?.text.trim() ?? lastSlashSegment(specifier);
|
|
836
836
|
const isStatic = /^import\s+static\b/.test(node.text);
|
|
837
837
|
return [
|
|
838
838
|
{
|
|
@@ -1835,7 +1835,7 @@ function findOrphanModules(index) {
|
|
|
1835
1835
|
index.files.filter((file) => file.symbols.length > 0).map((file) => file.path)
|
|
1836
1836
|
);
|
|
1837
1837
|
return nodes.filter(
|
|
1838
|
-
(
|
|
1838
|
+
(path11) => withSymbols.has(path11) && (incoming.get(path11) ?? 0) === 0 && (edges.get(path11)?.size ?? 0) === 0 && !ENTRY_RE.test(path11) && !TEST_RE.test(path11)
|
|
1839
1839
|
);
|
|
1840
1840
|
}
|
|
1841
1841
|
|
|
@@ -2040,6 +2040,12 @@ function runGit(args, cwd) {
|
|
|
2040
2040
|
});
|
|
2041
2041
|
});
|
|
2042
2042
|
}
|
|
2043
|
+
async function resolveGitRef(root, ref) {
|
|
2044
|
+
requireSafeRef(ref, "baseRef");
|
|
2045
|
+
const commit = (await runGit(["rev-parse", "--verify", `${ref}^{commit}`], root)).trim();
|
|
2046
|
+
if (!/^[a-f0-9]{40,64}$/i.test(commit)) throw new Error("invalid baseline commit");
|
|
2047
|
+
return commit;
|
|
2048
|
+
}
|
|
2043
2049
|
async function readWorkingTreeDiff(root, baseRef) {
|
|
2044
2050
|
requireSafeRef(baseRef, "baseRef");
|
|
2045
2051
|
const tracked = await runGit(
|
|
@@ -2190,9 +2196,15 @@ async function buildChangeContext(index, input, options = {}) {
|
|
|
2190
2196
|
`command root ${path7.resolve(commandRoot)} differs from indexed root ${index.root}; using the cached index`
|
|
2191
2197
|
);
|
|
2192
2198
|
}
|
|
2193
|
-
const addChanged = (symbol, change, side) => {
|
|
2199
|
+
const addChanged = (symbol, change, side, focusLine) => {
|
|
2194
2200
|
const key = `${side}:${symbol.id}`;
|
|
2195
|
-
if (!changedByKey.has(key)) changedByKey.set(key, {
|
|
2201
|
+
if (!changedByKey.has(key)) changedByKey.set(key, {
|
|
2202
|
+
symbol,
|
|
2203
|
+
change,
|
|
2204
|
+
side,
|
|
2205
|
+
resolution: "exact",
|
|
2206
|
+
...focusLine === void 0 ? {} : { focusLine }
|
|
2207
|
+
});
|
|
2196
2208
|
};
|
|
2197
2209
|
const addUnmapped = (file, side, ranges, reason) => {
|
|
2198
2210
|
if (ranges.length > 0) unmapped.push({ file, side, ranges, reason });
|
|
@@ -2222,11 +2234,17 @@ async function buildChangeContext(index, input, options = {}) {
|
|
|
2222
2234
|
} else {
|
|
2223
2235
|
let text;
|
|
2224
2236
|
if (input.kind === "git") {
|
|
2225
|
-
baseRef = input.baseRef;
|
|
2226
|
-
text = await readWorkingTreeDiff(input.root,
|
|
2237
|
+
baseRef = await resolveGitRef(input.root, input.baseRef);
|
|
2238
|
+
text = await readWorkingTreeDiff(input.root, baseRef);
|
|
2227
2239
|
} else {
|
|
2228
2240
|
text = input.text;
|
|
2229
|
-
|
|
2241
|
+
if (input.baseRef !== void 0) {
|
|
2242
|
+
try {
|
|
2243
|
+
baseRef = await resolveGitRef(input.root, input.baseRef);
|
|
2244
|
+
} catch {
|
|
2245
|
+
warnings.push(`baseline unavailable: ${input.baseRef}`);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2230
2248
|
}
|
|
2231
2249
|
const changes = parseUnifiedDiff(text).map((change) => ({
|
|
2232
2250
|
...change,
|
|
@@ -2380,6 +2398,13 @@ async function mapDiffChanges(changes, symbolsByPath, addChanged, addUnmapped, c
|
|
|
2380
2398
|
const { oldPath, newPath, hunks } = change;
|
|
2381
2399
|
if (newPath) changedFiles.add(newPath);
|
|
2382
2400
|
if (oldPath) changedFiles.add(oldPath);
|
|
2401
|
+
const identity = (symbol) => JSON.stringify([
|
|
2402
|
+
symbol.kind,
|
|
2403
|
+
symbol.name,
|
|
2404
|
+
symbol.scope.map((owner) => [owner.kind, owner.name])
|
|
2405
|
+
]);
|
|
2406
|
+
const oldSymbols = oldPath === null ? [] : await readBaseSymbols(oldPath);
|
|
2407
|
+
const count = (symbols, symbol) => symbols.filter((candidate) => identity(candidate) === identity(symbol)).length;
|
|
2383
2408
|
const currentSymbols = newPath !== null && change.status !== "deleted" ? symbolsByPath.get(newPath) : void 0;
|
|
2384
2409
|
if (newPath !== null && change.status !== "deleted") {
|
|
2385
2410
|
const currentRanges = hunks.filter((hunk) => hunk.newLines > 0).map((hunk) => ({
|
|
@@ -2389,7 +2414,14 @@ async function mapDiffChanges(changes, symbolsByPath, addChanged, addUnmapped, c
|
|
|
2389
2414
|
if (currentRanges.length > 0 && currentSymbols) {
|
|
2390
2415
|
for (const range of currentRanges) {
|
|
2391
2416
|
for (const symbol of currentSymbols) {
|
|
2392
|
-
if (intersects(symbol, range))
|
|
2417
|
+
if (!intersects(symbol, range)) continue;
|
|
2418
|
+
let classification = "unclassified";
|
|
2419
|
+
if (change.status === "added") classification = "added";
|
|
2420
|
+
else if (oldSymbols !== null && count(currentSymbols, symbol) === 1) {
|
|
2421
|
+
const matches = count(oldSymbols, symbol);
|
|
2422
|
+
classification = matches === 0 ? "added" : matches === 1 ? "modified" : "unclassified";
|
|
2423
|
+
}
|
|
2424
|
+
addChanged(symbol, classification, "current", Math.max(symbol.line, range.startLine));
|
|
2393
2425
|
}
|
|
2394
2426
|
}
|
|
2395
2427
|
const missed = currentRanges.filter(
|
|
@@ -2402,29 +2434,36 @@ async function mapDiffChanges(changes, symbolsByPath, addChanged, addUnmapped, c
|
|
|
2402
2434
|
if (change.status === "renamed" && hunks.length === 0) {
|
|
2403
2435
|
for (const symbol of currentSymbols ?? []) addChanged(symbol, "renamed", "current");
|
|
2404
2436
|
if (oldPath !== null) {
|
|
2405
|
-
const
|
|
2406
|
-
if (
|
|
2437
|
+
const oldSymbols2 = await readBaseSymbols(oldPath);
|
|
2438
|
+
if (oldSymbols2 === null) {
|
|
2407
2439
|
warnings.push(`rename baseline unavailable: ${oldPath}`);
|
|
2408
2440
|
} else {
|
|
2409
|
-
for (const symbol of
|
|
2441
|
+
for (const symbol of oldSymbols2) addChanged(symbol, "renamed", "base");
|
|
2410
2442
|
}
|
|
2411
2443
|
}
|
|
2412
2444
|
}
|
|
2413
2445
|
}
|
|
2414
|
-
const deletionHunks = hunks.filter((hunk) => hunk.
|
|
2446
|
+
const deletionHunks = hunks.filter((hunk) => hunk.oldLines > 0);
|
|
2415
2447
|
if (deletionHunks.length > 0 && oldPath !== null) {
|
|
2416
2448
|
const oldRanges = deletionHunks.map((hunk) => ({
|
|
2417
2449
|
startLine: hunk.oldStart,
|
|
2418
2450
|
endLine: hunk.oldStart + Math.max(hunk.oldLines, 1) - 1
|
|
2419
2451
|
}));
|
|
2420
|
-
const oldSymbols = await readBaseSymbols(oldPath);
|
|
2421
2452
|
if (oldSymbols === null) {
|
|
2422
2453
|
addUnmapped(oldPath, "base", oldRanges, "base-content-unavailable");
|
|
2423
2454
|
} else {
|
|
2424
|
-
const baseChange = change.status === "deleted" ? "deleted" : "modified";
|
|
2425
2455
|
for (const range of oldRanges) {
|
|
2426
2456
|
for (const symbol of oldSymbols) {
|
|
2427
|
-
if (intersects(symbol, range))
|
|
2457
|
+
if (!intersects(symbol, range)) continue;
|
|
2458
|
+
const matches = count(currentSymbols ?? [], symbol);
|
|
2459
|
+
if (change.status === "deleted" || matches === 0 && count(oldSymbols, symbol) === 1) {
|
|
2460
|
+
addChanged(symbol, "deleted", "base", Math.max(symbol.line, range.startLine));
|
|
2461
|
+
} else if (matches > 1 || count(oldSymbols, symbol) > 1) {
|
|
2462
|
+
addChanged(symbol, "unclassified", "base");
|
|
2463
|
+
} else if (hunks.some((hunk) => hunk.newLines === 0 && intersects(symbol, {
|
|
2464
|
+
startLine: hunk.oldStart,
|
|
2465
|
+
endLine: hunk.oldStart + hunk.oldLines - 1
|
|
2466
|
+
}))) addChanged(symbol, "modified", "base");
|
|
2428
2467
|
}
|
|
2429
2468
|
}
|
|
2430
2469
|
const missed = oldRanges.filter(
|
|
@@ -2582,6 +2621,15 @@ var TEST_RANK = {
|
|
|
2582
2621
|
"references-symbol-name": 3,
|
|
2583
2622
|
"path-convention": 4
|
|
2584
2623
|
};
|
|
2624
|
+
function collectSeedTests(index, seeds) {
|
|
2625
|
+
return collectAffectedTests(
|
|
2626
|
+
index,
|
|
2627
|
+
buildModuleGraph(index),
|
|
2628
|
+
new Set(seeds.map((seed) => seed.file)),
|
|
2629
|
+
seeds.map((symbol) => ({ symbol, side: "current", change: "unclassified", resolution: "exact" })),
|
|
2630
|
+
[]
|
|
2631
|
+
);
|
|
2632
|
+
}
|
|
2585
2633
|
function collectAffectedTests(index, moduleGraph, changedFiles, changedSymbols, impactRows) {
|
|
2586
2634
|
const changedNames = new Set(changedSymbols.map((changed) => changed.symbol.name));
|
|
2587
2635
|
const impactFileDepth = /* @__PURE__ */ new Map();
|
|
@@ -2755,6 +2803,293 @@ function renderChangeContext(result, maxChars = 12e3) {
|
|
|
2755
2803
|
return out;
|
|
2756
2804
|
}
|
|
2757
2805
|
|
|
2806
|
+
// src/source-excerpts.ts
|
|
2807
|
+
import { readFile as readFile6, realpath, stat as stat3 } from "fs/promises";
|
|
2808
|
+
import path8 from "path";
|
|
2809
|
+
function sourceReader(index) {
|
|
2810
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2811
|
+
return async (file, side, ref) => {
|
|
2812
|
+
const key = `${side}:${ref ?? ""}:${file}`;
|
|
2813
|
+
if (!cache.has(key)) cache.set(key, (async () => {
|
|
2814
|
+
if (side === "base") {
|
|
2815
|
+
if (!ref) return null;
|
|
2816
|
+
const code = await readGitFileAtRef(index.root, ref, file);
|
|
2817
|
+
return code === null ? null : code.split(/\r?\n/);
|
|
2818
|
+
}
|
|
2819
|
+
const indexed = index.files.find((entry) => entry.path === file);
|
|
2820
|
+
if (!indexed) return null;
|
|
2821
|
+
try {
|
|
2822
|
+
const root = await realpath(index.root);
|
|
2823
|
+
const target = await realpath(path8.join(root, file));
|
|
2824
|
+
const relative = path8.relative(root, target);
|
|
2825
|
+
if (relative.startsWith("..") || path8.isAbsolute(relative)) return null;
|
|
2826
|
+
const before = await stat3(target);
|
|
2827
|
+
if (before.size > 1e6 || before.mtimeMs !== indexed.mtimeMs) return null;
|
|
2828
|
+
const code = await readFile6(target, "utf8");
|
|
2829
|
+
const after = await stat3(target);
|
|
2830
|
+
if (after.mtimeMs !== before.mtimeMs || after.size !== before.size) return null;
|
|
2831
|
+
return code.split(/\r?\n/);
|
|
2832
|
+
} catch {
|
|
2833
|
+
return null;
|
|
2834
|
+
}
|
|
2835
|
+
})());
|
|
2836
|
+
return cache.get(key);
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
function excerptVariants(lines, symbol, side = "current", ref, focusLine = symbol.line) {
|
|
2840
|
+
const base = { file: symbol.file, side, ...ref ? { ref } : {} };
|
|
2841
|
+
const signature = {
|
|
2842
|
+
...base,
|
|
2843
|
+
startLine: symbol.line,
|
|
2844
|
+
endLine: symbol.line,
|
|
2845
|
+
mode: "signature",
|
|
2846
|
+
signature: symbol.signature
|
|
2847
|
+
};
|
|
2848
|
+
if (!lines || symbol.line < 1 || symbol.endLine > lines.length) return [signature];
|
|
2849
|
+
const ranges = [
|
|
2850
|
+
{ start: symbol.line, end: symbol.endLine, mode: "complete" },
|
|
2851
|
+
{ start: Math.max(symbol.line, focusLine - 3), end: Math.min(symbol.endLine, focusLine + 5), mode: "window" },
|
|
2852
|
+
{ start: symbol.line, end: Math.min(symbol.endLine, symbol.line + 2), mode: "window" }
|
|
2853
|
+
];
|
|
2854
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2855
|
+
const variants = [];
|
|
2856
|
+
for (const range of ranges) {
|
|
2857
|
+
const key = `${range.start}:${range.end}`;
|
|
2858
|
+
if (seen.has(key) || range.end < range.start) continue;
|
|
2859
|
+
seen.add(key);
|
|
2860
|
+
const code = lines.slice(range.start - 1, range.end).join("\n");
|
|
2861
|
+
if (code.length > 1600 || range.end - range.start >= 80) continue;
|
|
2862
|
+
variants.push({
|
|
2863
|
+
...base,
|
|
2864
|
+
startLine: range.start,
|
|
2865
|
+
endLine: range.end,
|
|
2866
|
+
mode: range.mode,
|
|
2867
|
+
code,
|
|
2868
|
+
...range.mode === "window" ? { signature: symbol.signature } : {}
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
return [...variants, signature];
|
|
2872
|
+
}
|
|
2873
|
+
function subtractExcerpt(excerpt, selected) {
|
|
2874
|
+
if (excerpt.code === void 0) return [excerpt];
|
|
2875
|
+
const covered = selected.filter((item) => item.code !== void 0 && item.file === excerpt.file && item.side === excerpt.side && item.ref === excerpt.ref);
|
|
2876
|
+
const lines = excerpt.code.split("\n");
|
|
2877
|
+
const fragments = [];
|
|
2878
|
+
let start = null;
|
|
2879
|
+
for (let line = excerpt.startLine; line <= excerpt.endLine + 1; line++) {
|
|
2880
|
+
const include = line <= excerpt.endLine && !covered.some((item) => line >= item.startLine && line <= item.endLine);
|
|
2881
|
+
if (include && start === null) start = line;
|
|
2882
|
+
if (!include && start !== null) {
|
|
2883
|
+
fragments.push({
|
|
2884
|
+
...excerpt,
|
|
2885
|
+
startLine: start,
|
|
2886
|
+
endLine: line - 1,
|
|
2887
|
+
mode: start === excerpt.startLine && line - 1 === excerpt.endLine ? excerpt.mode : "window",
|
|
2888
|
+
code: lines.slice(start - excerpt.startLine, line - excerpt.startLine).join("\n")
|
|
2889
|
+
});
|
|
2890
|
+
start = null;
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
return fragments;
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
// src/context-pack.ts
|
|
2897
|
+
function renderContextPack(pack) {
|
|
2898
|
+
const rows = [`Task context \u2014 ${pack.route}`, `Repo/worktree: ${pack.root}`, `Task: ${pack.task}`];
|
|
2899
|
+
for (const item of pack.items) {
|
|
2900
|
+
const location = item.file ? ` ${item.file}:${item.startLine ?? 1}-${item.endLine ?? item.startLine ?? 1}` : "";
|
|
2901
|
+
rows.push(`
|
|
2902
|
+
${item.kind}: ${item.name ?? item.text ?? ""}${location}${item.side ? ` [${item.side}${item.ref ? ` @${item.ref}` : ""}]` : ""}`);
|
|
2903
|
+
rows.push(`Reason: ${item.reason}`);
|
|
2904
|
+
if (item.change) rows.push(`Change: ${item.change}`);
|
|
2905
|
+
if (item.resolution) rows.push(`Relationship provenance: ${item.resolution}${item.provenance ? `; ${item.provenance}` : ""}`);
|
|
2906
|
+
if (item.code !== void 0) {
|
|
2907
|
+
if (item.signature) rows.push(`Declaration: ${item.signature}`);
|
|
2908
|
+
rows.push(`Source (${item.mode}):
|
|
2909
|
+
${item.code}`);
|
|
2910
|
+
} else if (item.mode === "signature") rows.push(`Signature only: ${item.signature || "(source unavailable within budget)"}`);
|
|
2911
|
+
}
|
|
2912
|
+
if (pack.gaps.length) rows.push(`
|
|
2913
|
+
Gaps: ${pack.gaps.join("; ")}`);
|
|
2914
|
+
rows.push(`Budget: ${pack.budget.usedChars} / ${pack.budget.budgetChars} chars${pack.budget.truncated ? " (trimmed low-priority context)" : ""}`);
|
|
2915
|
+
return rows.join("\n");
|
|
2916
|
+
}
|
|
2917
|
+
function account(pack) {
|
|
2918
|
+
for (let i = 0; i < 8; i++) {
|
|
2919
|
+
const textChars = renderContextPack(pack).length;
|
|
2920
|
+
const packChars = JSON.stringify(pack).length;
|
|
2921
|
+
if (pack.budget.usedChars === textChars && pack.budget.packChars === packChars) break;
|
|
2922
|
+
pack.budget.usedChars = textChars;
|
|
2923
|
+
pack.budget.packChars = packChars;
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
async function selectContextPack(index, context, budgetChars, maxFiles = 12) {
|
|
2927
|
+
const read = sourceReader(index);
|
|
2928
|
+
const graph = buildReferenceGraph(index);
|
|
2929
|
+
const candidates = [];
|
|
2930
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2931
|
+
const gaps = [];
|
|
2932
|
+
const unavailable = /* @__PURE__ */ new Set();
|
|
2933
|
+
const source = async (symbol, kind, reason, extra = {}, focus) => {
|
|
2934
|
+
const side = extra.side ?? "current";
|
|
2935
|
+
const key = `${side}:${extra.ref ?? ""}:${kind}:${symbol.file}:${symbol.line}:${symbol.name}`;
|
|
2936
|
+
if (seen.has(key)) return;
|
|
2937
|
+
seen.add(key);
|
|
2938
|
+
const lines = await read(symbol.file, side, extra.ref);
|
|
2939
|
+
if (!lines) unavailable.add(`${side}:${symbol.file}`);
|
|
2940
|
+
candidates.push(excerptVariants(lines, symbol, side, extra.ref, focus).map((excerpt) => ({
|
|
2941
|
+
...excerpt,
|
|
2942
|
+
...symbol.id === "" && excerpt.mode === "complete" ? { mode: "window" } : {},
|
|
2943
|
+
kind,
|
|
2944
|
+
name: symbol.name,
|
|
2945
|
+
reason,
|
|
2946
|
+
...extra
|
|
2947
|
+
})));
|
|
2948
|
+
};
|
|
2949
|
+
for (const primary of context.primarySymbols) {
|
|
2950
|
+
const changed = context.changeContext?.changed.find((row) => row.side === "current" && row.symbol.id === primary.symbol.id);
|
|
2951
|
+
await source(primary.symbol, "primary", primary.reason, changed ? { change: changed.change } : {}, changed?.focusLine);
|
|
2952
|
+
}
|
|
2953
|
+
for (const changed of context.changeContext?.changed ?? []) {
|
|
2954
|
+
if (changed.side === "current" && context.primarySymbols.some((seed) => seed.symbol.id === changed.symbol.id)) continue;
|
|
2955
|
+
await source(changed.symbol, "change", "declaration intersects Git change", {
|
|
2956
|
+
side: changed.side,
|
|
2957
|
+
...changed.side === "base" ? { ref: context.changeContext.baseRef } : {},
|
|
2958
|
+
change: changed.change
|
|
2959
|
+
}, changed.focusLine);
|
|
2960
|
+
}
|
|
2961
|
+
for (const test of context.testEvidence) {
|
|
2962
|
+
const file = index.files.find((row) => row.path === test.file);
|
|
2963
|
+
const seedNames = new Set(context.primarySymbols.map((seed) => seed.symbol.name));
|
|
2964
|
+
const call = file.calls?.find((call2) => seedNames.has(call2.name));
|
|
2965
|
+
const lines = await read(file.path, "current");
|
|
2966
|
+
const symbol = file.symbols.find((symbol2) => call && symbol2.line <= call.line && symbol2.endLine >= call.line);
|
|
2967
|
+
await source(
|
|
2968
|
+
symbol ?? {
|
|
2969
|
+
id: "",
|
|
2970
|
+
name: test.file,
|
|
2971
|
+
file: test.file,
|
|
2972
|
+
line: Math.max(1, (call?.line ?? 1) - 3),
|
|
2973
|
+
endLine: Math.min(lines?.length ?? 1, (call?.line ?? 1) + 5),
|
|
2974
|
+
signature: "",
|
|
2975
|
+
scope: [],
|
|
2976
|
+
ordinal: 1,
|
|
2977
|
+
kind: "function",
|
|
2978
|
+
exported: false
|
|
2979
|
+
},
|
|
2980
|
+
"test",
|
|
2981
|
+
test.reason === "imports-changed-file" ? "test imports primary seed file" : test.reason,
|
|
2982
|
+
{ resolution: test.resolution, provenance: test.reason },
|
|
2983
|
+
call?.line
|
|
2984
|
+
);
|
|
2985
|
+
}
|
|
2986
|
+
for (const primary of context.primarySymbols.slice(0, 4)) {
|
|
2987
|
+
for (const direction of ["caller", "callee"]) {
|
|
2988
|
+
const edges = direction === "caller" ? graph.incoming.get(primary.symbol.id) : graph.outgoing.get(primary.symbol.id);
|
|
2989
|
+
const strongEdges = (edges ?? []).filter((edge) => edge.resolution !== "name-only").sort((a, b) => a.resolution.localeCompare(b.resolution) || a.callSite.file.localeCompare(b.callSite.file) || a.callSite.line - b.callSite.line);
|
|
2990
|
+
for (const edge of strongEdges.slice(0, 4)) {
|
|
2991
|
+
const symbol = graph.symbolsById.get(direction === "caller" ? edge.sourceId ?? "" : edge.targetId);
|
|
2992
|
+
if (symbol && !context.primarySymbols.some((seed) => seed.symbol.id === symbol.id)) {
|
|
2993
|
+
await source(
|
|
2994
|
+
symbol,
|
|
2995
|
+
direction,
|
|
2996
|
+
`direct ${direction} of ${primary.symbol.name}`,
|
|
2997
|
+
{ resolution: edge.resolution, provenance: edge.provenance },
|
|
2998
|
+
direction === "caller" ? edge.callSite.line : symbol.line
|
|
2999
|
+
);
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
const file = index.files.find((file2) => file2.path === primary.symbol.file);
|
|
3004
|
+
const lines = await read(file.path, "current");
|
|
3005
|
+
const body = lines?.slice(primary.symbol.line - 1, primary.symbol.endLine).join("\n") ?? primary.symbol.signature;
|
|
3006
|
+
const relevantImports = (file.importDetails ?? []).filter((imported) => imported.bindings.some((binding) => {
|
|
3007
|
+
const escaped = binding.local.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3008
|
+
return new RegExp(`(^|[^A-Za-z0-9_$])${escaped}(?=$|[^A-Za-z0-9_$])`).test(body);
|
|
3009
|
+
}));
|
|
3010
|
+
for (const imported of relevantImports.slice(0, 3)) {
|
|
3011
|
+
const offset = lines?.slice(imported.line - 1, imported.line + 11).findIndex((line) => line.includes(imported.specifier)) ?? -1;
|
|
3012
|
+
const endLine = offset < 0 ? imported.line : imported.line + offset;
|
|
3013
|
+
await source(
|
|
3014
|
+
{
|
|
3015
|
+
...primary.symbol,
|
|
3016
|
+
...offset < 0 ? { id: "" } : {},
|
|
3017
|
+
name: imported.specifier,
|
|
3018
|
+
line: imported.line,
|
|
3019
|
+
endLine,
|
|
3020
|
+
signature: ""
|
|
3021
|
+
},
|
|
3022
|
+
"import",
|
|
3023
|
+
`import binding referenced in primary source ${primary.symbol.file}`,
|
|
3024
|
+
{ resolution: "import-scoped", provenance: "import statement" }
|
|
3025
|
+
);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
for (const relation of context.relationships) candidates.push([{
|
|
3029
|
+
kind: "relationship",
|
|
3030
|
+
text: relation.text,
|
|
3031
|
+
reason: "bounded primary graph neighborhood",
|
|
3032
|
+
resolution: relation.resolution,
|
|
3033
|
+
provenance: relation.provenance
|
|
3034
|
+
}]);
|
|
3035
|
+
if (!context.primarySymbols.length) for (const file of context.relevantFiles.slice(0, 4)) {
|
|
3036
|
+
candidates.push([{ kind: "file", file: file.path, reason: file.reason }]);
|
|
3037
|
+
}
|
|
3038
|
+
if (!context.primarySymbols.length) gaps.push("no strong primary symbol; specify a symbol, path, or failing behavior");
|
|
3039
|
+
if (!context.testEvidence.length) gaps.push("no related indexed test found");
|
|
3040
|
+
gaps.push(...context.warnings);
|
|
3041
|
+
const pack = {
|
|
3042
|
+
version: 1,
|
|
3043
|
+
root: index.root,
|
|
3044
|
+
task: context.task.slice(0, 120),
|
|
3045
|
+
route: context.route.kind,
|
|
3046
|
+
items: [],
|
|
3047
|
+
gaps: ["budget omitted some evidence"],
|
|
3048
|
+
budget: { budgetChars, usedChars: 0, packChars: 0, truncated: context.task.length > 120 }
|
|
3049
|
+
};
|
|
3050
|
+
const fits = () => {
|
|
3051
|
+
account(pack);
|
|
3052
|
+
return Math.max(pack.budget.usedChars, pack.budget.packChars) <= budgetChars;
|
|
3053
|
+
};
|
|
3054
|
+
if (!fits()) {
|
|
3055
|
+
pack.task = "";
|
|
3056
|
+
pack.gaps = [];
|
|
3057
|
+
fits();
|
|
3058
|
+
}
|
|
3059
|
+
if (!fits()) throw new Error("budget too small for repository identity");
|
|
3060
|
+
for (const variants of candidates) {
|
|
3061
|
+
let added = false;
|
|
3062
|
+
for (const candidate of variants) {
|
|
3063
|
+
const fragments = candidate.code !== void 0 ? subtractExcerpt(candidate, pack.items.filter((item) => item.code !== void 0)).map((fragment) => ({ ...candidate, ...fragment })) : [candidate];
|
|
3064
|
+
if (!fragments.length) continue;
|
|
3065
|
+
pack.items.push(...fragments);
|
|
3066
|
+
const fileCount = new Set(pack.items.flatMap((item) => item.file ? [item.file] : [])).size;
|
|
3067
|
+
if (fits() && fileCount <= maxFiles) {
|
|
3068
|
+
added = true;
|
|
3069
|
+
if (candidate.mode === "signature") pack.budget.truncated = true;
|
|
3070
|
+
break;
|
|
3071
|
+
}
|
|
3072
|
+
pack.items.splice(pack.items.length - fragments.length);
|
|
3073
|
+
}
|
|
3074
|
+
if (!added) pack.budget.truncated = true;
|
|
3075
|
+
}
|
|
3076
|
+
if (!pack.budget.truncated) pack.gaps = [];
|
|
3077
|
+
for (const item of pack.items) if (item.file && unavailable.has(`${item.side}:${item.file}`)) {
|
|
3078
|
+
gaps.push(`source unavailable or changed after indexing: ${item.file} (${item.side})`);
|
|
3079
|
+
}
|
|
3080
|
+
for (const gap of [...new Set(gaps)].slice(0, 4)) {
|
|
3081
|
+
pack.gaps.push(gap.slice(0, 140));
|
|
3082
|
+
if (!fits()) pack.gaps.pop();
|
|
3083
|
+
}
|
|
3084
|
+
account(pack);
|
|
3085
|
+
while (Math.max(pack.budget.usedChars, pack.budget.packChars) > budgetChars && pack.items.length) {
|
|
3086
|
+
pack.items.pop();
|
|
3087
|
+
pack.budget.truncated = true;
|
|
3088
|
+
account(pack);
|
|
3089
|
+
}
|
|
3090
|
+
return pack;
|
|
3091
|
+
}
|
|
3092
|
+
|
|
2758
3093
|
// src/context.ts
|
|
2759
3094
|
var TEST_PATH_RE2 = /(^|\/)(tests?|__tests__)(\/|$)|\.(?:spec|test)\.[cm]?[jt]sx?$|(^|\/)(?:test_[^/]+\.py|[^/]+_test\.(?:py|go))$/i;
|
|
2760
3095
|
var FILE_TOKEN_RE = /[A-Za-z0-9_./\\-]+\.[A-Za-z0-9]+/g;
|
|
@@ -2784,7 +3119,20 @@ var STOP_WORDS = /* @__PURE__ */ new Set(
|
|
|
2784
3119
|
"what",
|
|
2785
3120
|
"where",
|
|
2786
3121
|
"why",
|
|
2787
|
-
"with"
|
|
3122
|
+
"with",
|
|
3123
|
+
"src",
|
|
3124
|
+
"lib",
|
|
3125
|
+
"test",
|
|
3126
|
+
"tests",
|
|
3127
|
+
"spec",
|
|
3128
|
+
"fix",
|
|
3129
|
+
"explain",
|
|
3130
|
+
"please",
|
|
3131
|
+
"help",
|
|
3132
|
+
"current",
|
|
3133
|
+
"change",
|
|
3134
|
+
"changes",
|
|
3135
|
+
"code"
|
|
2788
3136
|
]
|
|
2789
3137
|
);
|
|
2790
3138
|
var CHANGE_RE = /\b(fix|bug|broken|breaks?|change|changed|current|working\s+tree|regression|impact|modified|duplicate|fails?|failure)\b/i;
|
|
@@ -2809,9 +3157,6 @@ function clamp2(value, min, max, fallback) {
|
|
|
2809
3157
|
function wordBoundary(name2) {
|
|
2810
3158
|
return new RegExp(`(^|[^A-Za-z0-9_$])${name2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?=$|[^A-Za-z0-9_$])`);
|
|
2811
3159
|
}
|
|
2812
|
-
function symbolLabel2(symbol) {
|
|
2813
|
-
return `${symbol.kind} ${symbol.signature || symbol.name} \u2014 ${symbol.file}:${symbol.line}`;
|
|
2814
|
-
}
|
|
2815
3160
|
function isTestFile(file) {
|
|
2816
3161
|
return TEST_PATH_RE2.test(file);
|
|
2817
3162
|
}
|
|
@@ -2822,6 +3167,13 @@ function fileMentionMatches(file, mentions) {
|
|
|
2822
3167
|
return lower === normalized || lower.endsWith(`/${normalized}`) || lower.includes(normalized);
|
|
2823
3168
|
});
|
|
2824
3169
|
}
|
|
3170
|
+
function contextTerms(text) {
|
|
3171
|
+
return unique(text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z])([A-Z][a-z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length >= 3 && !STOP_WORDS.has(term)).flatMap((term) => [
|
|
3172
|
+
term,
|
|
3173
|
+
...term.length > 5 && term.endsWith("ing") ? [term.slice(0, -3)] : [],
|
|
3174
|
+
...term.length > 4 && term.endsWith("s") ? [term.slice(0, -1)] : []
|
|
3175
|
+
]), (term) => term);
|
|
3176
|
+
}
|
|
2825
3177
|
function routeTask(task, index) {
|
|
2826
3178
|
const text = task.trim();
|
|
2827
3179
|
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])));
|
|
@@ -2834,12 +3186,12 @@ function routeTask(task, index) {
|
|
|
2834
3186
|
(token) => token.toLowerCase()
|
|
2835
3187
|
);
|
|
2836
3188
|
const queries = unique(
|
|
2837
|
-
[...exactSymbolNames, ...codeTokens.filter((token) => !exactSymbolNames.includes(token))],
|
|
3189
|
+
[...exactSymbolNames, ...contextTerms(text), ...codeTokens.filter((token) => !exactSymbolNames.includes(token))],
|
|
2838
3190
|
(value) => value.toLowerCase()
|
|
2839
3191
|
).slice(0, 12);
|
|
2840
3192
|
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";
|
|
2841
3193
|
const includeChangeContext = kind === "change" || kind === "test";
|
|
2842
|
-
const includeRelationships = kind
|
|
3194
|
+
const includeRelationships = kind !== "ambiguous";
|
|
2843
3195
|
const sources = /* @__PURE__ */ new Set();
|
|
2844
3196
|
if (kind === "change" || kind === "test") sources.add("change context");
|
|
2845
3197
|
if (kind === "architecture" || kind === "exploration" || kind === "ambiguous") {
|
|
@@ -2868,34 +3220,61 @@ function routeScore(route, hit) {
|
|
|
2868
3220
|
}
|
|
2869
3221
|
function gatherPrimarySymbols(index, route, maxSymbols, changedIds, changedFiles, refs) {
|
|
2870
3222
|
const hits = [];
|
|
3223
|
+
const queryTerms = new Set(route.queries.flatMap(contextTerms));
|
|
3224
|
+
for (const file of index.files) for (const symbol of file.symbols) {
|
|
3225
|
+
const terms = contextTerms(`${symbol.name} ${symbol.signature} ${file.path}`);
|
|
3226
|
+
const matched = terms.filter((term) => queryTerms.has(term)).length;
|
|
3227
|
+
if (route.exactSymbolNames.includes(symbol.name) || matched > 0 || fileMentionMatches(file.path, route.mentionedFiles)) {
|
|
3228
|
+
hits.push({ ...symbol, score: route.exactSymbolNames.includes(symbol.name) ? 1 : Math.min(0.8, matched * 0.2) });
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
2871
3231
|
for (const query of route.queries) {
|
|
2872
3232
|
hits.push(...searchSymbols(index, { query }, Math.max(maxSymbols * 3, 20), refs));
|
|
2873
3233
|
}
|
|
2874
3234
|
const byId = /* @__PURE__ */ new Map();
|
|
2875
3235
|
for (const hit of hits) {
|
|
2876
3236
|
const exact = route.exactSymbolNames.includes(hit.name);
|
|
3237
|
+
const overlap = contextTerms(`${hit.name} ${hit.signature} ${hit.file}`).filter((term) => queryTerms.has(term)).length;
|
|
3238
|
+
if (!exact && route.kind === "ambiguous" && queryTerms.size > 1 && overlap < 2) continue;
|
|
2877
3239
|
let score = routeScore(route, hit);
|
|
2878
3240
|
if (changedIds.has(hit.id)) score += 80;
|
|
2879
3241
|
else if (changedFiles.has(hit.file)) score += 55;
|
|
2880
3242
|
if (route.kind === "test" && isTestFile(hit.file)) score += 35;
|
|
2881
|
-
const candidate = {
|
|
3243
|
+
const candidate = {
|
|
3244
|
+
symbol: hit,
|
|
3245
|
+
score,
|
|
3246
|
+
provenance: exact ? "exact" : "lexical",
|
|
3247
|
+
reason: exact ? "task names this symbol" : "matches task terms"
|
|
3248
|
+
};
|
|
2882
3249
|
const previous = byId.get(hit.id);
|
|
2883
3250
|
if (!previous || candidate.score > previous.score) byId.set(hit.id, candidate);
|
|
2884
3251
|
}
|
|
3252
|
+
if (!byId.size && (route.kind === "architecture" || route.kind === "exploration")) {
|
|
3253
|
+
for (const entry of rankRepoMap(index, { topFiles: 2, symbolsPerFile: 2 })) {
|
|
3254
|
+
for (const symbol of index.files.find((file) => file.path === entry.path)?.symbols.slice(0, 2) ?? []) {
|
|
3255
|
+
byId.set(symbol.id, { symbol, score: 5, provenance: "lexical", reason: "bounded architecture entry lead" });
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
2885
3259
|
if (route.includeChangeContext) {
|
|
3260
|
+
const matchedFiles = new Set([...byId.values()].map((candidate) => candidate.symbol.file));
|
|
2886
3261
|
for (const file of index.files) {
|
|
2887
|
-
if (!
|
|
3262
|
+
if (matchedFiles.size && !matchedFiles.has(file.path)) continue;
|
|
2888
3263
|
for (const symbol of file.symbols) {
|
|
3264
|
+
if (!changedIds.has(symbol.id)) continue;
|
|
2889
3265
|
if (byId.has(symbol.id)) continue;
|
|
2890
3266
|
byId.set(symbol.id, {
|
|
2891
3267
|
symbol,
|
|
2892
3268
|
score: 70,
|
|
2893
|
-
provenance: "lexical"
|
|
3269
|
+
provenance: "lexical",
|
|
3270
|
+
reason: "declaration intersects current change"
|
|
2894
3271
|
});
|
|
2895
3272
|
}
|
|
2896
3273
|
}
|
|
2897
3274
|
}
|
|
2898
|
-
|
|
3275
|
+
const definitionMatch = (candidate) => contextTerms(`${candidate.symbol.name} ${candidate.symbol.signature}`).some((term) => queryTerms.has(term));
|
|
3276
|
+
const hasDefinitionMatch = [...byId.values()].some(definitionMatch);
|
|
3277
|
+
return [...byId.values()].filter((candidate) => !hasDefinitionMatch || definitionMatch(candidate) || candidate.reason === "declaration intersects current change" || fileMentionMatches(candidate.symbol.file, route.mentionedFiles)).sort(
|
|
2899
3278
|
(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)
|
|
2900
3279
|
).slice(0, maxSymbols);
|
|
2901
3280
|
}
|
|
@@ -2918,20 +3297,22 @@ function collectRelationships(graph, primary) {
|
|
|
2918
3297
|
key,
|
|
2919
3298
|
text: edgeText(graph, edge, direction),
|
|
2920
3299
|
resolution: edge.resolution,
|
|
3300
|
+
provenance: edge.provenance,
|
|
2921
3301
|
priority
|
|
2922
3302
|
});
|
|
2923
3303
|
};
|
|
2924
3304
|
for (const candidate of primary) {
|
|
2925
|
-
|
|
2926
|
-
for (const edge of graph.
|
|
3305
|
+
const bounded = (edges) => [...edges].sort((a, b) => a.resolution.localeCompare(b.resolution) || a.callSite.file.localeCompare(b.callSite.file) || a.callSite.line - b.callSite.line).slice(0, 8);
|
|
3306
|
+
for (const edge of bounded(graph.incoming.get(candidate.symbol.id) ?? [])) add(edge, "caller", 100);
|
|
3307
|
+
for (const edge of bounded(graph.outgoing.get(candidate.symbol.id) ?? [])) add(edge, "callee", 90);
|
|
2927
3308
|
}
|
|
2928
3309
|
return rows.sort(
|
|
2929
3310
|
(a, b) => b.priority - a.priority || a.resolution.localeCompare(b.resolution) || a.text.localeCompare(b.text)
|
|
2930
3311
|
);
|
|
2931
3312
|
}
|
|
2932
|
-
function addFile(files,
|
|
2933
|
-
const previous = files.get(
|
|
2934
|
-
if (!previous || score > previous.score) files.set(
|
|
3313
|
+
function addFile(files, path11, score, reason) {
|
|
3314
|
+
const previous = files.get(path11);
|
|
3315
|
+
if (!previous || score > previous.score) files.set(path11, { path: path11, score, reason });
|
|
2935
3316
|
}
|
|
2936
3317
|
function gatherFiles(index, route, primary, change, maxFiles) {
|
|
2937
3318
|
const files = /* @__PURE__ */ new Map();
|
|
@@ -2954,7 +3335,7 @@ function gatherFiles(index, route, primary, change, maxFiles) {
|
|
|
2954
3335
|
addFile(files, pathRow.changed.file, 90, "changed path");
|
|
2955
3336
|
}
|
|
2956
3337
|
}
|
|
2957
|
-
for (const entry of rankRepoMap(index, { topFiles: maxFiles * 2, symbolsPerFile: 8 })) {
|
|
3338
|
+
if (!primary.length) for (const entry of rankRepoMap(index, { topFiles: maxFiles * 2, symbolsPerFile: 8 })) {
|
|
2958
3339
|
addFile(files, entry.path, Math.min(35, entry.score + 10), "repo map");
|
|
2959
3340
|
}
|
|
2960
3341
|
return [...files.values()].sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).slice(0, maxFiles);
|
|
@@ -2970,118 +3351,6 @@ function confidenceOf(primary, relationships, change) {
|
|
|
2970
3351
|
}
|
|
2971
3352
|
return out;
|
|
2972
3353
|
}
|
|
2973
|
-
function renderRows(result, budgetChars) {
|
|
2974
|
-
const lines = [
|
|
2975
|
-
`Task context \u2014 ${result.route.kind}`,
|
|
2976
|
-
`Task: ${result.task}`,
|
|
2977
|
-
`Sources: ${result.route.sources.join(", ")}`,
|
|
2978
|
-
"",
|
|
2979
|
-
"Primary symbols:"
|
|
2980
|
-
];
|
|
2981
|
-
if (result.primarySymbols.length === 0) lines.push("- (none found)");
|
|
2982
|
-
const optional = [];
|
|
2983
|
-
for (const candidate2 of result.primarySymbols) {
|
|
2984
|
-
optional.push({
|
|
2985
|
-
priority: 1e3 + candidate2.score,
|
|
2986
|
-
key: `symbol:${candidate2.symbol.id}`,
|
|
2987
|
-
section: "primary",
|
|
2988
|
-
line: `- [${candidate2.provenance}] ${symbolLabel2(candidate2.symbol)}`
|
|
2989
|
-
});
|
|
2990
|
-
}
|
|
2991
|
-
optional.push({ priority: 700, key: "files-header", section: "files", line: "\nRelevant files:" });
|
|
2992
|
-
for (const file of result.relevantFiles) {
|
|
2993
|
-
optional.push({ priority: 500 + file.score, key: `file:${file.path}`, section: "files", line: `- ${file.path} (${file.reason})` });
|
|
2994
|
-
}
|
|
2995
|
-
optional.push({ priority: 400, key: "relationships-header", section: "relationships", line: "\nRelationships:" });
|
|
2996
|
-
for (const relation of result.relationships) {
|
|
2997
|
-
optional.push({ priority: relation.priority, key: `relation:${relation.key}`, section: "relationships", line: `- ${relation.text}` });
|
|
2998
|
-
}
|
|
2999
|
-
if (result.changeContext) {
|
|
3000
|
-
optional.push({ priority: 850, key: "changes-header", section: "changes", line: "\nCurrent changes:" });
|
|
3001
|
-
for (const changed of result.changeContext.changed) {
|
|
3002
|
-
optional.push({
|
|
3003
|
-
priority: 840,
|
|
3004
|
-
key: `changed:${changed.side}:${changed.symbol.id}`,
|
|
3005
|
-
section: "changes",
|
|
3006
|
-
line: `- ${changed.change} ${symbolLabel2(changed.symbol)} [${changed.resolution}]`
|
|
3007
|
-
});
|
|
3008
|
-
}
|
|
3009
|
-
if (result.changeContext.unmapped.length > 0) {
|
|
3010
|
-
optional.push({
|
|
3011
|
-
priority: 830,
|
|
3012
|
-
key: "changes-unmapped",
|
|
3013
|
-
section: "changes",
|
|
3014
|
-
line: `- unmapped: ${result.changeContext.unmapped.map((entry) => entry.file).join(", ")}`
|
|
3015
|
-
});
|
|
3016
|
-
}
|
|
3017
|
-
}
|
|
3018
|
-
if (result.tests.length > 0) {
|
|
3019
|
-
optional.push({ priority: 350, key: "tests-header", section: "tests", line: "\nLikely affected tests:" });
|
|
3020
|
-
for (const test of result.tests) optional.push({ priority: 340, key: `test:${test}`, section: "tests", line: `- ${test}` });
|
|
3021
|
-
}
|
|
3022
|
-
if (result.warnings.length > 0) {
|
|
3023
|
-
optional.push({ priority: 200, key: "warnings-header", section: "warnings", line: "\nWarnings:" });
|
|
3024
|
-
for (const warning of result.warnings) optional.push({ priority: 190, key: `warning:${warning}`, section: "warnings", line: `- ${warning}` });
|
|
3025
|
-
}
|
|
3026
|
-
const ordered = optional.map((item, index) => ({ ...item, index })).sort((a, b) => b.priority - a.priority || a.index - b.index || a.key.localeCompare(b.key));
|
|
3027
|
-
const footerReserve = 100;
|
|
3028
|
-
let truncated = false;
|
|
3029
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3030
|
-
for (const item of ordered) {
|
|
3031
|
-
if (seen.has(item.key)) continue;
|
|
3032
|
-
const next = lines.join("\n") + `
|
|
3033
|
-
${item.line}`;
|
|
3034
|
-
if (next.length + footerReserve > budgetChars) {
|
|
3035
|
-
truncated = true;
|
|
3036
|
-
continue;
|
|
3037
|
-
}
|
|
3038
|
-
seen.add(item.key);
|
|
3039
|
-
lines.push(item.line);
|
|
3040
|
-
}
|
|
3041
|
-
lines.push(
|
|
3042
|
-
"",
|
|
3043
|
-
`Confidence: exact ${result.confidence.exact}; import-scoped ${result.confidence["import-scoped"]}; name-only ${result.confidence["name-only"]}`
|
|
3044
|
-
);
|
|
3045
|
-
const body = lines.join("\n");
|
|
3046
|
-
const withFooter = (bodyText, wasTruncated) => {
|
|
3047
|
-
let usedChars = bodyText.length + 1;
|
|
3048
|
-
for (let iteration = 0; iteration < 10; iteration++) {
|
|
3049
|
-
const footer2 = `Budget: ${usedChars} / ${budgetChars} chars${wasTruncated ? " (trimmed low-priority context)" : ""}`;
|
|
3050
|
-
const text2 = `${bodyText}
|
|
3051
|
-
${footer2}`;
|
|
3052
|
-
const nextUsedChars = text2.length;
|
|
3053
|
-
if (nextUsedChars === usedChars) return { text: text2, usedChars: nextUsedChars };
|
|
3054
|
-
usedChars = nextUsedChars;
|
|
3055
|
-
}
|
|
3056
|
-
const footer = `Budget: ${usedChars} / ${budgetChars} chars${wasTruncated ? " (trimmed low-priority context)" : ""}`;
|
|
3057
|
-
const text = `${bodyText}
|
|
3058
|
-
${footer}`;
|
|
3059
|
-
return { text, usedChars: text.length };
|
|
3060
|
-
};
|
|
3061
|
-
const complete = withFooter(body, truncated);
|
|
3062
|
-
if (complete.usedChars <= budgetChars) {
|
|
3063
|
-
return { text: complete.text, usedChars: complete.usedChars, truncated };
|
|
3064
|
-
}
|
|
3065
|
-
truncated = true;
|
|
3066
|
-
const candidate = (prefixLength) => {
|
|
3067
|
-
const prefix = `${body.slice(0, prefixLength).trimEnd()}\u2026`;
|
|
3068
|
-
return withFooter(prefix, true);
|
|
3069
|
-
};
|
|
3070
|
-
let low = 0;
|
|
3071
|
-
let high = body.length;
|
|
3072
|
-
let best = candidate(0);
|
|
3073
|
-
while (low <= high) {
|
|
3074
|
-
const middle = Math.floor((low + high) / 2);
|
|
3075
|
-
const current = candidate(middle);
|
|
3076
|
-
if (current.usedChars <= budgetChars) {
|
|
3077
|
-
best = current;
|
|
3078
|
-
low = middle + 1;
|
|
3079
|
-
} else {
|
|
3080
|
-
high = middle - 1;
|
|
3081
|
-
}
|
|
3082
|
-
}
|
|
3083
|
-
return { text: best.text, usedChars: best.usedChars, truncated };
|
|
3084
|
-
}
|
|
3085
3354
|
async function buildTaskContext(index, task, options = {}) {
|
|
3086
3355
|
const budgetChars = clamp2(options.budgetChars, 300, 2e4, 5e3);
|
|
3087
3356
|
const maxFiles = clamp2(options.maxFiles, 1, 50, 12);
|
|
@@ -3111,6 +3380,19 @@ async function buildTaskContext(index, task, options = {}) {
|
|
|
3111
3380
|
changedFiles,
|
|
3112
3381
|
callerCounts(index)
|
|
3113
3382
|
);
|
|
3383
|
+
const taskFiles = new Set(primarySymbols.filter((seed) => seed.reason !== "declaration intersects current change").map((seed) => seed.symbol.file));
|
|
3384
|
+
if (changeContext && taskFiles.size) {
|
|
3385
|
+
const changed = changeContext.changed.filter((row) => taskFiles.has(row.symbol.file));
|
|
3386
|
+
const changedIds2 = new Set(changed.map((row) => row.symbol.id));
|
|
3387
|
+
changeContext = {
|
|
3388
|
+
...changeContext,
|
|
3389
|
+
changed,
|
|
3390
|
+
directCallers: changeContext.directCallers.filter((row) => changedIds2.has(row.via.toId)),
|
|
3391
|
+
tests: collectSeedTests(index, changed.map((row) => row.symbol)),
|
|
3392
|
+
importDependents: changeContext.importDependents.filter((row) => taskFiles.has(row.changedFile)),
|
|
3393
|
+
paths: changeContext.paths.filter((row) => changedIds2.has(row.changed.id))
|
|
3394
|
+
};
|
|
3395
|
+
}
|
|
3114
3396
|
const graph = route.includeRelationships ? buildReferenceGraph(index) : null;
|
|
3115
3397
|
const relationships = graph ? collectRelationships(graph, primarySymbols) : [];
|
|
3116
3398
|
if (changeContext) {
|
|
@@ -3123,13 +3405,18 @@ async function buildTaskContext(index, task, options = {}) {
|
|
|
3123
3405
|
key,
|
|
3124
3406
|
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}]`,
|
|
3125
3407
|
resolution: row.resolution,
|
|
3408
|
+
provenance: row.via.reason,
|
|
3126
3409
|
priority: 110
|
|
3127
3410
|
});
|
|
3128
3411
|
}
|
|
3129
3412
|
relationships.sort((a, b) => b.priority - a.priority || a.text.localeCompare(b.text));
|
|
3130
3413
|
}
|
|
3131
3414
|
const relevantFiles = gatherFiles(index, route, primarySymbols, changeContext, maxFiles);
|
|
3132
|
-
const
|
|
3415
|
+
const testEvidence = unique([
|
|
3416
|
+
...collectSeedTests(index, primarySymbols.filter((seed) => seed.provenance === "exact" || seed.score >= 20).map((seed) => seed.symbol)),
|
|
3417
|
+
...changeContext?.tests ?? []
|
|
3418
|
+
], (test) => test.file).slice(0, 8);
|
|
3419
|
+
const tests = testEvidence.map((test) => test.file);
|
|
3133
3420
|
const confidence = confidenceOf(primarySymbols, relationships, changeContext);
|
|
3134
3421
|
const partial = {
|
|
3135
3422
|
root: index.root,
|
|
@@ -3140,27 +3427,50 @@ async function buildTaskContext(index, task, options = {}) {
|
|
|
3140
3427
|
relationships,
|
|
3141
3428
|
changeContext,
|
|
3142
3429
|
tests,
|
|
3430
|
+
testEvidence,
|
|
3143
3431
|
confidence,
|
|
3144
3432
|
warnings
|
|
3145
3433
|
};
|
|
3146
|
-
const
|
|
3434
|
+
const pack = await selectContextPack(index, partial, budgetChars, maxFiles);
|
|
3435
|
+
const selected = (file, name2, kind) => pack.items.some((item) => item.file === file && (!name2 || item.name === name2) && (!kind || item.kind === kind));
|
|
3436
|
+
const selectedRelationships = relationships.filter((row) => pack.items.some((item) => item.text === row.text));
|
|
3437
|
+
const selectedChange = changeContext ? {
|
|
3438
|
+
...changeContext,
|
|
3439
|
+
changed: changeContext.changed.filter((row) => pack.items.some((item) => item.file === row.symbol.file && item.name === row.symbol.name && item.side === row.side && item.change === row.change)),
|
|
3440
|
+
unmapped: [],
|
|
3441
|
+
directCallers: [],
|
|
3442
|
+
importDependents: [],
|
|
3443
|
+
paths: [],
|
|
3444
|
+
impact: [],
|
|
3445
|
+
tests: changeContext.tests.filter((test) => selected(test.file, void 0, "test")),
|
|
3446
|
+
warnings: pack.gaps
|
|
3447
|
+
} : null;
|
|
3147
3448
|
return {
|
|
3148
3449
|
...partial,
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3450
|
+
pack,
|
|
3451
|
+
route: {
|
|
3452
|
+
...route,
|
|
3453
|
+
exactSymbolNames: route.exactSymbolNames.filter((name2) => pack.items.some((item) => item.name === name2)),
|
|
3454
|
+
mentionedFiles: route.mentionedFiles.filter((file) => pack.items.some((item) => item.file && fileMentionMatches(item.file, [file])))
|
|
3455
|
+
},
|
|
3456
|
+
primarySymbols: primarySymbols.filter((seed) => selected(seed.symbol.file, seed.symbol.name, "primary")),
|
|
3457
|
+
relevantFiles: relevantFiles.filter((file) => selected(file.path)),
|
|
3458
|
+
relationships: selectedRelationships,
|
|
3459
|
+
changeContext: selectedChange,
|
|
3460
|
+
tests: tests.filter((file) => selected(file, void 0, "test")),
|
|
3461
|
+
testEvidence: testEvidence.filter((test) => selected(test.file, void 0, "test")),
|
|
3462
|
+
confidence: confidenceOf([], selectedRelationships, null),
|
|
3463
|
+
warnings: pack.gaps,
|
|
3464
|
+
budget: { usedChars: pack.budget.usedChars, budgetChars, truncated: pack.budget.truncated }
|
|
3154
3465
|
};
|
|
3155
3466
|
}
|
|
3156
3467
|
function renderTaskContext(result) {
|
|
3157
|
-
|
|
3158
|
-
return rendered.text;
|
|
3468
|
+
return renderContextPack(result.pack);
|
|
3159
3469
|
}
|
|
3160
3470
|
|
|
3161
3471
|
// src/tools.ts
|
|
3162
3472
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3163
|
-
import
|
|
3473
|
+
import path10 from "path";
|
|
3164
3474
|
|
|
3165
3475
|
// src/config.ts
|
|
3166
3476
|
import Schema from "@deepseek-ai/schemastery";
|
|
@@ -3225,8 +3535,8 @@ function indexOptions() {
|
|
|
3225
3535
|
}
|
|
3226
3536
|
|
|
3227
3537
|
// src/repo-context.ts
|
|
3228
|
-
import { realpath } from "fs/promises";
|
|
3229
|
-
import
|
|
3538
|
+
import { realpath as realpath2 } from "fs/promises";
|
|
3539
|
+
import path9 from "path";
|
|
3230
3540
|
import chokidar from "chokidar";
|
|
3231
3541
|
function ignoredRelativePath(rel, options) {
|
|
3232
3542
|
const components = rel.split(/[\\/]+/).filter(Boolean);
|
|
@@ -3234,8 +3544,8 @@ function ignoredRelativePath(rel, options) {
|
|
|
3234
3544
|
return components.some((component) => excluded.has(component));
|
|
3235
3545
|
}
|
|
3236
3546
|
async function canonicalRepoRoot(root) {
|
|
3237
|
-
const resolved = await
|
|
3238
|
-
return
|
|
3547
|
+
const resolved = await realpath2(path9.resolve(root));
|
|
3548
|
+
return path9.normalize(resolved);
|
|
3239
3549
|
}
|
|
3240
3550
|
var RepoContextManager = class {
|
|
3241
3551
|
contexts = /* @__PURE__ */ new Map();
|
|
@@ -3285,7 +3595,7 @@ var RepoContextManager = class {
|
|
|
3285
3595
|
return index;
|
|
3286
3596
|
}
|
|
3287
3597
|
getContext(root) {
|
|
3288
|
-
const key = cacheKeyForRoot(
|
|
3598
|
+
const key = cacheKeyForRoot(path9.resolve(root));
|
|
3289
3599
|
return this.contexts.get(key);
|
|
3290
3600
|
}
|
|
3291
3601
|
async dispose() {
|
|
@@ -3336,8 +3646,8 @@ var RepoContextManager = class {
|
|
|
3336
3646
|
if (!this.options.watch) return;
|
|
3337
3647
|
const options = this.options.indexOptions();
|
|
3338
3648
|
const ignored = (absolute) => {
|
|
3339
|
-
const rel =
|
|
3340
|
-
return rel === ".." || rel.startsWith(`..${
|
|
3649
|
+
const rel = path9.relative(context.root, absolute);
|
|
3650
|
+
return rel === ".." || rel.startsWith(`..${path9.sep}`) || path9.isAbsolute(rel) || ignoredRelativePath(rel, options) || isGitIgnoredPath(context.root, rel, context.ignoreMatcherCache);
|
|
3341
3651
|
};
|
|
3342
3652
|
const watcher = chokidar.watch(context.root, {
|
|
3343
3653
|
ignored,
|
|
@@ -3349,9 +3659,9 @@ var RepoContextManager = class {
|
|
|
3349
3659
|
context.watcher = watcher;
|
|
3350
3660
|
watcher.on("all", (event, absolute) => {
|
|
3351
3661
|
if (context.disposed || !["add", "change", "unlink"].includes(event)) return;
|
|
3352
|
-
const rel =
|
|
3662
|
+
const rel = path9.relative(context.root, absolute).split(path9.sep).join("/");
|
|
3353
3663
|
if (!rel || ignoredRelativePath(rel, this.options.indexOptions()) || isGitIgnoredPath(context.root, rel, context.ignoreMatcherCache)) return;
|
|
3354
|
-
if (!SUPPORTED_EXTS.has(
|
|
3664
|
+
if (!SUPPORTED_EXTS.has(path9.extname(rel).toLowerCase())) return;
|
|
3355
3665
|
context.dirtyFiles.add(rel);
|
|
3356
3666
|
this.scheduleRefresh(context);
|
|
3357
3667
|
});
|
|
@@ -3409,7 +3719,7 @@ async function resolveRoot(arg, exec) {
|
|
|
3409
3719
|
const cwd = exec.agent?.session?.header?.cwd ?? exec.session?.header?.cwd ?? exec.cwd;
|
|
3410
3720
|
const base = arg ?? cwd ?? process.cwd();
|
|
3411
3721
|
const root = await findRepoRoot(base);
|
|
3412
|
-
if (!root) throw new Error(`no git repository found from ${
|
|
3722
|
+
if (!root) throw new Error(`no git repository found from ${path10.resolve(base)}`);
|
|
3413
3723
|
return root;
|
|
3414
3724
|
}
|
|
3415
3725
|
function hitsToSearchMeta(hits) {
|
|
@@ -3424,7 +3734,7 @@ function hitsToSearchMeta(hits) {
|
|
|
3424
3734
|
}
|
|
3425
3735
|
return {
|
|
3426
3736
|
shape: "matches",
|
|
3427
|
-
files: [...byFile.entries()].map(([
|
|
3737
|
+
files: [...byFile.entries()].map(([path11, matches]) => ({ path: path11, matches })),
|
|
3428
3738
|
truncated: false,
|
|
3429
3739
|
total: hits.length
|
|
3430
3740
|
};
|
|
@@ -3756,6 +4066,11 @@ var tools = [
|
|
|
3756
4066
|
name: "code_context",
|
|
3757
4067
|
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.",
|
|
3758
4068
|
parameters: {
|
|
4069
|
+
outputFormat: {
|
|
4070
|
+
type: "string",
|
|
4071
|
+
enum: ["text", "pack"],
|
|
4072
|
+
description: "Default text preserves the canonical string return; pack explicitly requests the selected ContextPack DTO."
|
|
4073
|
+
},
|
|
3759
4074
|
task: {
|
|
3760
4075
|
type: "string",
|
|
3761
4076
|
required: true,
|
|
@@ -3779,8 +4094,8 @@ var tools = [
|
|
|
3779
4094
|
}
|
|
3780
4095
|
},
|
|
3781
4096
|
output: {
|
|
3782
|
-
schema: { type: "string" },
|
|
3783
|
-
render: (_args, value) => [{ type: "text", text: value }]
|
|
4097
|
+
schema: { oneOf: [{ type: "string" }, { type: "object", additionalProperties: true }] },
|
|
4098
|
+
render: (_args, value) => [{ type: "text", text: typeof value === "string" ? value : renderContextPack(value) }]
|
|
3784
4099
|
},
|
|
3785
4100
|
presentCall: (args) => ({
|
|
3786
4101
|
card: "generic",
|
|
@@ -3800,9 +4115,9 @@ var tools = [
|
|
|
3800
4115
|
maxFiles: args.maxFiles,
|
|
3801
4116
|
maxSymbols: args.maxSymbols
|
|
3802
4117
|
});
|
|
3803
|
-
return renderTaskContext(result);
|
|
4118
|
+
return args.outputFormat === "pack" ? JSON.parse(JSON.stringify(result.pack)) : renderTaskContext(result);
|
|
3804
4119
|
} catch (error) {
|
|
3805
|
-
return `code_context: ${error.message ?? String(error)}`;
|
|
4120
|
+
return `code_context: ${(error.message ?? String(error)).slice(0, 240)}`;
|
|
3806
4121
|
}
|
|
3807
4122
|
}
|
|
3808
4123
|
}),
|
|
@@ -4017,6 +4332,7 @@ export {
|
|
|
4017
4332
|
readGitFileAtRef,
|
|
4018
4333
|
readWorkingTreeDiff,
|
|
4019
4334
|
renderChangeContext,
|
|
4335
|
+
renderContextPack,
|
|
4020
4336
|
renderHit,
|
|
4021
4337
|
renderRepoMap,
|
|
4022
4338
|
renderTaskContext,
|