dsh-code-index 0.7.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 +36 -0
- package/README.md +95 -52
- package/README.zh.md +97 -34
- package/dist/client.js +74 -19
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +634 -0
- package/dist/index.js +799 -253
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
- package/scripts/context-pack-demo.mjs +49 -0
- package/scripts/release-smoke.mjs +10 -0
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/buildIndex.ts
|
|
2
|
-
import { readFile as
|
|
2
|
+
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
3
3
|
import path4 from "path";
|
|
4
4
|
|
|
5
5
|
// src/extract.ts
|
|
@@ -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
|
{
|
|
@@ -1009,8 +1009,10 @@ async function parseFileToSymbols(filePath, repoRoot, code) {
|
|
|
1009
1009
|
}
|
|
1010
1010
|
|
|
1011
1011
|
// src/scan.ts
|
|
1012
|
-
import { readdir, stat } from "fs/promises";
|
|
1012
|
+
import { readFile as readFile2, readdir, stat } from "fs/promises";
|
|
1013
|
+
import { readFileSync, statSync } from "fs";
|
|
1013
1014
|
import path2 from "path";
|
|
1015
|
+
import ignore from "ignore";
|
|
1014
1016
|
var DEFAULT_EXCLUDED_DIRS = [
|
|
1015
1017
|
"node_modules",
|
|
1016
1018
|
".git",
|
|
@@ -1054,12 +1056,51 @@ var SUPPORTED_EXTS = /* @__PURE__ */ new Set([
|
|
|
1054
1056
|
".inl",
|
|
1055
1057
|
".c"
|
|
1056
1058
|
]);
|
|
1059
|
+
function isGitIgnoredPath(root, relativePath, cache = /* @__PURE__ */ new Map()) {
|
|
1060
|
+
const rel = relativePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1061
|
+
if (!rel) return false;
|
|
1062
|
+
const parentParts = rel.split("/").slice(0, -1);
|
|
1063
|
+
for (let depth = 0; depth <= parentParts.length; depth += 1) {
|
|
1064
|
+
const base = parentParts.slice(0, depth).join("/");
|
|
1065
|
+
const ignoreFile = path2.join(root, base, ".gitignore");
|
|
1066
|
+
let fileStat;
|
|
1067
|
+
try {
|
|
1068
|
+
fileStat = statSync(ignoreFile);
|
|
1069
|
+
} catch {
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
const previous = cache.get(ignoreFile);
|
|
1073
|
+
let matcher = previous?.matcher;
|
|
1074
|
+
if (!previous || previous.mtimeMs !== fileStat.mtimeMs || previous.size !== fileStat.size) {
|
|
1075
|
+
try {
|
|
1076
|
+
matcher = ignore().add(readFileSync(ignoreFile, "utf8"));
|
|
1077
|
+
cache.set(ignoreFile, { mtimeMs: fileStat.mtimeMs, size: fileStat.size, matcher });
|
|
1078
|
+
} catch {
|
|
1079
|
+
cache.delete(ignoreFile);
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const scoped = base ? path2.posix.relative(base, rel) : rel;
|
|
1084
|
+
if (matcher?.ignores(scoped)) return true;
|
|
1085
|
+
}
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1057
1088
|
async function scanRepo(root, options = {}) {
|
|
1058
1089
|
const excluded = /* @__PURE__ */ new Set([...DEFAULT_EXCLUDED_DIRS, ...options.excludeDirs ?? []]);
|
|
1059
1090
|
const results = [];
|
|
1060
|
-
const queue = [[root, ""]];
|
|
1091
|
+
const queue = [[root, "", []]];
|
|
1061
1092
|
while (queue.length) {
|
|
1062
|
-
const [absDir, relDir] = queue.pop();
|
|
1093
|
+
const [absDir, relDir, inherited] = queue.pop();
|
|
1094
|
+
let layers = inherited;
|
|
1095
|
+
try {
|
|
1096
|
+
const rules = await readFile2(path2.join(absDir, ".gitignore"), "utf8");
|
|
1097
|
+
layers = [...inherited, { base: relDir, matcher: ignore().add(rules) }];
|
|
1098
|
+
} catch {
|
|
1099
|
+
}
|
|
1100
|
+
const ignoredByGit = (relativePath) => layers.some(({ base, matcher }) => {
|
|
1101
|
+
const scoped = base ? path2.posix.relative(base, relativePath) : relativePath;
|
|
1102
|
+
return scoped !== ".." && !scoped.startsWith("../") && matcher.ignores(scoped);
|
|
1103
|
+
});
|
|
1063
1104
|
let entries;
|
|
1064
1105
|
try {
|
|
1065
1106
|
entries = await readdir(absDir, { withFileTypes: true });
|
|
@@ -1071,8 +1112,8 @@ async function scanRepo(root, options = {}) {
|
|
|
1071
1112
|
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
1072
1113
|
if (entry.isDirectory()) {
|
|
1073
1114
|
if (excluded.has(entry.name)) continue;
|
|
1074
|
-
queue.push([abs, rel]);
|
|
1075
|
-
} else if (entry.isFile() && SUPPORTED_EXTS.has(path2.extname(entry.name).toLowerCase())) {
|
|
1115
|
+
if (!ignoredByGit(rel)) queue.push([abs, rel, layers]);
|
|
1116
|
+
} else if (entry.isFile() && !ignoredByGit(rel) && SUPPORTED_EXTS.has(path2.extname(entry.name).toLowerCase())) {
|
|
1076
1117
|
try {
|
|
1077
1118
|
const st = await stat(abs);
|
|
1078
1119
|
results.push({ abs, rel, mtimeMs: st.mtimeMs });
|
|
@@ -1086,7 +1127,7 @@ async function scanRepo(root, options = {}) {
|
|
|
1086
1127
|
}
|
|
1087
1128
|
|
|
1088
1129
|
// src/store.ts
|
|
1089
|
-
import { mkdir, readFile as
|
|
1130
|
+
import { mkdir, readFile as readFile3, rename, rm, writeFile } from "fs/promises";
|
|
1090
1131
|
import path3 from "path";
|
|
1091
1132
|
import { createHash, randomUUID } from "crypto";
|
|
1092
1133
|
|
|
@@ -1114,7 +1155,7 @@ function legacyCachePath(root, cacheDir) {
|
|
|
1114
1155
|
}
|
|
1115
1156
|
async function loadIndex(cachePath) {
|
|
1116
1157
|
try {
|
|
1117
|
-
const raw = await
|
|
1158
|
+
const raw = await readFile3(cachePath, "utf8");
|
|
1118
1159
|
const parsed = JSON.parse(raw);
|
|
1119
1160
|
if (!parsed || typeof parsed.root !== "string" || !Array.isArray(parsed.files)) return null;
|
|
1120
1161
|
if (parsed.schemaVersion !== REPO_INDEX_SCHEMA_VERSION) return null;
|
|
@@ -1143,7 +1184,7 @@ async function saveIndex(cachePath, index) {
|
|
|
1143
1184
|
}
|
|
1144
1185
|
|
|
1145
1186
|
// src/buildIndex.ts
|
|
1146
|
-
async function buildIndex(root, options = {}, previous = null) {
|
|
1187
|
+
async function buildIndex(root, options = {}, previous = null, forcePaths = /* @__PURE__ */ new Set()) {
|
|
1147
1188
|
const scanned = await scanRepo(root, options);
|
|
1148
1189
|
const prevByPath = new Map((previous?.files ?? []).map((f) => [f.path, f]));
|
|
1149
1190
|
const prevMtime = new Map((previous?.files ?? []).map((f) => [f.path, f.mtimeMs]));
|
|
@@ -1155,7 +1196,7 @@ async function buildIndex(root, options = {}, previous = null) {
|
|
|
1155
1196
|
batch.map(async (f) => {
|
|
1156
1197
|
const lang = languageForFile(f.abs);
|
|
1157
1198
|
if (!lang) return null;
|
|
1158
|
-
if (prevMtime.get(f.rel) === f.mtimeMs) {
|
|
1199
|
+
if (prevMtime.get(f.rel) === f.mtimeMs && !forcePaths.has(f.rel) && !forcePaths.has("*")) {
|
|
1159
1200
|
const cached = prevByPath.get(f.rel);
|
|
1160
1201
|
return {
|
|
1161
1202
|
...cached,
|
|
@@ -1165,7 +1206,7 @@ async function buildIndex(root, options = {}, previous = null) {
|
|
|
1165
1206
|
}
|
|
1166
1207
|
let code;
|
|
1167
1208
|
try {
|
|
1168
|
-
code = await
|
|
1209
|
+
code = await readFile4(f.abs, "utf8");
|
|
1169
1210
|
} catch {
|
|
1170
1211
|
return null;
|
|
1171
1212
|
}
|
|
@@ -1193,7 +1234,7 @@ async function buildIndex(root, options = {}, previous = null) {
|
|
|
1193
1234
|
excludedDirs: [...DEFAULT_EXCLUDED_DIRS, ...options.excludeDirs ?? []]
|
|
1194
1235
|
};
|
|
1195
1236
|
}
|
|
1196
|
-
async function buildIndexWithCache(root, options = {}, cacheDir) {
|
|
1237
|
+
async function buildIndexWithCache(root, options = {}, cacheDir, forcePaths = /* @__PURE__ */ new Set()) {
|
|
1197
1238
|
const cachePath = defaultCachePath(root, cacheDir);
|
|
1198
1239
|
let prev = await loadIndex(cachePath);
|
|
1199
1240
|
let loadedLegacy = false;
|
|
@@ -1205,7 +1246,7 @@ async function buildIndexWithCache(root, options = {}, cacheDir) {
|
|
|
1205
1246
|
}
|
|
1206
1247
|
}
|
|
1207
1248
|
const reusable = prev && cacheKeyForRoot(prev.root) === cacheKeyForRoot(root) ? prev : null;
|
|
1208
|
-
const fresh = await buildIndex(root, options, reusable);
|
|
1249
|
+
const fresh = await buildIndex(root, options, reusable, forcePaths);
|
|
1209
1250
|
if (prev && indexesEqual(prev, fresh)) {
|
|
1210
1251
|
if (loadedLegacy) await saveIndex(cachePath, fresh);
|
|
1211
1252
|
return loadedLegacy ? fresh : prev;
|
|
@@ -1794,13 +1835,13 @@ function findOrphanModules(index) {
|
|
|
1794
1835
|
index.files.filter((file) => file.symbols.length > 0).map((file) => file.path)
|
|
1795
1836
|
);
|
|
1796
1837
|
return nodes.filter(
|
|
1797
|
-
(
|
|
1838
|
+
(path11) => withSymbols.has(path11) && (incoming.get(path11) ?? 0) === 0 && (edges.get(path11)?.size ?? 0) === 0 && !ENTRY_RE.test(path11) && !TEST_RE.test(path11)
|
|
1798
1839
|
);
|
|
1799
1840
|
}
|
|
1800
1841
|
|
|
1801
1842
|
// src/git-diff.ts
|
|
1802
1843
|
import { execFile } from "child_process";
|
|
1803
|
-
import { readFile as
|
|
1844
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1804
1845
|
import path6 from "path";
|
|
1805
1846
|
var HUNK_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
1806
1847
|
var HEADER_PATH_PREFIX_RE = /^[ab]\//;
|
|
@@ -1999,6 +2040,12 @@ function runGit(args, cwd) {
|
|
|
1999
2040
|
});
|
|
2000
2041
|
});
|
|
2001
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
|
+
}
|
|
2002
2049
|
async function readWorkingTreeDiff(root, baseRef) {
|
|
2003
2050
|
requireSafeRef(baseRef, "baseRef");
|
|
2004
2051
|
const tracked = await runGit(
|
|
@@ -2013,7 +2060,7 @@ async function readWorkingTreeDiff(root, baseRef) {
|
|
|
2013
2060
|
const normalized = rawPath.replace(/\\/g, "/");
|
|
2014
2061
|
if (normalized === ".dsh-code-index" || normalized.startsWith(".dsh-code-index/")) continue;
|
|
2015
2062
|
try {
|
|
2016
|
-
const content = await
|
|
2063
|
+
const content = await readFile5(path6.resolve(root, normalized), "utf8");
|
|
2017
2064
|
const lines = content.length === 0 ? 0 : content.split(/\r?\n/).length - (content.endsWith("\n") ? 1 : 0);
|
|
2018
2065
|
untrackedFiles.push({ path: normalized, content, lines });
|
|
2019
2066
|
} catch {
|
|
@@ -2149,9 +2196,15 @@ async function buildChangeContext(index, input, options = {}) {
|
|
|
2149
2196
|
`command root ${path7.resolve(commandRoot)} differs from indexed root ${index.root}; using the cached index`
|
|
2150
2197
|
);
|
|
2151
2198
|
}
|
|
2152
|
-
const addChanged = (symbol, change, side) => {
|
|
2199
|
+
const addChanged = (symbol, change, side, focusLine) => {
|
|
2153
2200
|
const key = `${side}:${symbol.id}`;
|
|
2154
|
-
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
|
+
});
|
|
2155
2208
|
};
|
|
2156
2209
|
const addUnmapped = (file, side, ranges, reason) => {
|
|
2157
2210
|
if (ranges.length > 0) unmapped.push({ file, side, ranges, reason });
|
|
@@ -2181,11 +2234,17 @@ async function buildChangeContext(index, input, options = {}) {
|
|
|
2181
2234
|
} else {
|
|
2182
2235
|
let text;
|
|
2183
2236
|
if (input.kind === "git") {
|
|
2184
|
-
baseRef = input.baseRef;
|
|
2185
|
-
text = await readWorkingTreeDiff(input.root,
|
|
2237
|
+
baseRef = await resolveGitRef(input.root, input.baseRef);
|
|
2238
|
+
text = await readWorkingTreeDiff(input.root, baseRef);
|
|
2186
2239
|
} else {
|
|
2187
2240
|
text = input.text;
|
|
2188
|
-
|
|
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
|
+
}
|
|
2189
2248
|
}
|
|
2190
2249
|
const changes = parseUnifiedDiff(text).map((change) => ({
|
|
2191
2250
|
...change,
|
|
@@ -2339,6 +2398,13 @@ async function mapDiffChanges(changes, symbolsByPath, addChanged, addUnmapped, c
|
|
|
2339
2398
|
const { oldPath, newPath, hunks } = change;
|
|
2340
2399
|
if (newPath) changedFiles.add(newPath);
|
|
2341
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;
|
|
2342
2408
|
const currentSymbols = newPath !== null && change.status !== "deleted" ? symbolsByPath.get(newPath) : void 0;
|
|
2343
2409
|
if (newPath !== null && change.status !== "deleted") {
|
|
2344
2410
|
const currentRanges = hunks.filter((hunk) => hunk.newLines > 0).map((hunk) => ({
|
|
@@ -2348,7 +2414,14 @@ async function mapDiffChanges(changes, symbolsByPath, addChanged, addUnmapped, c
|
|
|
2348
2414
|
if (currentRanges.length > 0 && currentSymbols) {
|
|
2349
2415
|
for (const range of currentRanges) {
|
|
2350
2416
|
for (const symbol of currentSymbols) {
|
|
2351
|
-
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));
|
|
2352
2425
|
}
|
|
2353
2426
|
}
|
|
2354
2427
|
const missed = currentRanges.filter(
|
|
@@ -2361,29 +2434,36 @@ async function mapDiffChanges(changes, symbolsByPath, addChanged, addUnmapped, c
|
|
|
2361
2434
|
if (change.status === "renamed" && hunks.length === 0) {
|
|
2362
2435
|
for (const symbol of currentSymbols ?? []) addChanged(symbol, "renamed", "current");
|
|
2363
2436
|
if (oldPath !== null) {
|
|
2364
|
-
const
|
|
2365
|
-
if (
|
|
2437
|
+
const oldSymbols2 = await readBaseSymbols(oldPath);
|
|
2438
|
+
if (oldSymbols2 === null) {
|
|
2366
2439
|
warnings.push(`rename baseline unavailable: ${oldPath}`);
|
|
2367
2440
|
} else {
|
|
2368
|
-
for (const symbol of
|
|
2441
|
+
for (const symbol of oldSymbols2) addChanged(symbol, "renamed", "base");
|
|
2369
2442
|
}
|
|
2370
2443
|
}
|
|
2371
2444
|
}
|
|
2372
2445
|
}
|
|
2373
|
-
const deletionHunks = hunks.filter((hunk) => hunk.
|
|
2446
|
+
const deletionHunks = hunks.filter((hunk) => hunk.oldLines > 0);
|
|
2374
2447
|
if (deletionHunks.length > 0 && oldPath !== null) {
|
|
2375
2448
|
const oldRanges = deletionHunks.map((hunk) => ({
|
|
2376
2449
|
startLine: hunk.oldStart,
|
|
2377
2450
|
endLine: hunk.oldStart + Math.max(hunk.oldLines, 1) - 1
|
|
2378
2451
|
}));
|
|
2379
|
-
const oldSymbols = await readBaseSymbols(oldPath);
|
|
2380
2452
|
if (oldSymbols === null) {
|
|
2381
2453
|
addUnmapped(oldPath, "base", oldRanges, "base-content-unavailable");
|
|
2382
2454
|
} else {
|
|
2383
|
-
const baseChange = change.status === "deleted" ? "deleted" : "modified";
|
|
2384
2455
|
for (const range of oldRanges) {
|
|
2385
2456
|
for (const symbol of oldSymbols) {
|
|
2386
|
-
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");
|
|
2387
2467
|
}
|
|
2388
2468
|
}
|
|
2389
2469
|
const missed = oldRanges.filter(
|
|
@@ -2541,6 +2621,15 @@ var TEST_RANK = {
|
|
|
2541
2621
|
"references-symbol-name": 3,
|
|
2542
2622
|
"path-convention": 4
|
|
2543
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
|
+
}
|
|
2544
2633
|
function collectAffectedTests(index, moduleGraph, changedFiles, changedSymbols, impactRows) {
|
|
2545
2634
|
const changedNames = new Set(changedSymbols.map((changed) => changed.symbol.name));
|
|
2546
2635
|
const impactFileDepth = /* @__PURE__ */ new Map();
|
|
@@ -2714,6 +2803,293 @@ function renderChangeContext(result, maxChars = 12e3) {
|
|
|
2714
2803
|
return out;
|
|
2715
2804
|
}
|
|
2716
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
|
+
|
|
2717
3093
|
// src/context.ts
|
|
2718
3094
|
var TEST_PATH_RE2 = /(^|\/)(tests?|__tests__)(\/|$)|\.(?:spec|test)\.[cm]?[jt]sx?$|(^|\/)(?:test_[^/]+\.py|[^/]+_test\.(?:py|go))$/i;
|
|
2719
3095
|
var FILE_TOKEN_RE = /[A-Za-z0-9_./\\-]+\.[A-Za-z0-9]+/g;
|
|
@@ -2743,7 +3119,20 @@ var STOP_WORDS = /* @__PURE__ */ new Set(
|
|
|
2743
3119
|
"what",
|
|
2744
3120
|
"where",
|
|
2745
3121
|
"why",
|
|
2746
|
-
"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"
|
|
2747
3136
|
]
|
|
2748
3137
|
);
|
|
2749
3138
|
var CHANGE_RE = /\b(fix|bug|broken|breaks?|change|changed|current|working\s+tree|regression|impact|modified|duplicate|fails?|failure)\b/i;
|
|
@@ -2768,9 +3157,6 @@ function clamp2(value, min, max, fallback) {
|
|
|
2768
3157
|
function wordBoundary(name2) {
|
|
2769
3158
|
return new RegExp(`(^|[^A-Za-z0-9_$])${name2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?=$|[^A-Za-z0-9_$])`);
|
|
2770
3159
|
}
|
|
2771
|
-
function symbolLabel2(symbol) {
|
|
2772
|
-
return `${symbol.kind} ${symbol.signature || symbol.name} \u2014 ${symbol.file}:${symbol.line}`;
|
|
2773
|
-
}
|
|
2774
3160
|
function isTestFile(file) {
|
|
2775
3161
|
return TEST_PATH_RE2.test(file);
|
|
2776
3162
|
}
|
|
@@ -2781,6 +3167,13 @@ function fileMentionMatches(file, mentions) {
|
|
|
2781
3167
|
return lower === normalized || lower.endsWith(`/${normalized}`) || lower.includes(normalized);
|
|
2782
3168
|
});
|
|
2783
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
|
+
}
|
|
2784
3177
|
function routeTask(task, index) {
|
|
2785
3178
|
const text = task.trim();
|
|
2786
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])));
|
|
@@ -2793,12 +3186,12 @@ function routeTask(task, index) {
|
|
|
2793
3186
|
(token) => token.toLowerCase()
|
|
2794
3187
|
);
|
|
2795
3188
|
const queries = unique(
|
|
2796
|
-
[...exactSymbolNames, ...codeTokens.filter((token) => !exactSymbolNames.includes(token))],
|
|
3189
|
+
[...exactSymbolNames, ...contextTerms(text), ...codeTokens.filter((token) => !exactSymbolNames.includes(token))],
|
|
2797
3190
|
(value) => value.toLowerCase()
|
|
2798
3191
|
).slice(0, 12);
|
|
2799
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";
|
|
2800
3193
|
const includeChangeContext = kind === "change" || kind === "test";
|
|
2801
|
-
const includeRelationships = kind
|
|
3194
|
+
const includeRelationships = kind !== "ambiguous";
|
|
2802
3195
|
const sources = /* @__PURE__ */ new Set();
|
|
2803
3196
|
if (kind === "change" || kind === "test") sources.add("change context");
|
|
2804
3197
|
if (kind === "architecture" || kind === "exploration" || kind === "ambiguous") {
|
|
@@ -2827,34 +3220,61 @@ function routeScore(route, hit) {
|
|
|
2827
3220
|
}
|
|
2828
3221
|
function gatherPrimarySymbols(index, route, maxSymbols, changedIds, changedFiles, refs) {
|
|
2829
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
|
+
}
|
|
2830
3231
|
for (const query of route.queries) {
|
|
2831
3232
|
hits.push(...searchSymbols(index, { query }, Math.max(maxSymbols * 3, 20), refs));
|
|
2832
3233
|
}
|
|
2833
3234
|
const byId = /* @__PURE__ */ new Map();
|
|
2834
3235
|
for (const hit of hits) {
|
|
2835
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;
|
|
2836
3239
|
let score = routeScore(route, hit);
|
|
2837
3240
|
if (changedIds.has(hit.id)) score += 80;
|
|
2838
3241
|
else if (changedFiles.has(hit.file)) score += 55;
|
|
2839
3242
|
if (route.kind === "test" && isTestFile(hit.file)) score += 35;
|
|
2840
|
-
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
|
+
};
|
|
2841
3249
|
const previous = byId.get(hit.id);
|
|
2842
3250
|
if (!previous || candidate.score > previous.score) byId.set(hit.id, candidate);
|
|
2843
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
|
+
}
|
|
2844
3259
|
if (route.includeChangeContext) {
|
|
3260
|
+
const matchedFiles = new Set([...byId.values()].map((candidate) => candidate.symbol.file));
|
|
2845
3261
|
for (const file of index.files) {
|
|
2846
|
-
if (!
|
|
3262
|
+
if (matchedFiles.size && !matchedFiles.has(file.path)) continue;
|
|
2847
3263
|
for (const symbol of file.symbols) {
|
|
3264
|
+
if (!changedIds.has(symbol.id)) continue;
|
|
2848
3265
|
if (byId.has(symbol.id)) continue;
|
|
2849
3266
|
byId.set(symbol.id, {
|
|
2850
3267
|
symbol,
|
|
2851
3268
|
score: 70,
|
|
2852
|
-
provenance: "lexical"
|
|
3269
|
+
provenance: "lexical",
|
|
3270
|
+
reason: "declaration intersects current change"
|
|
2853
3271
|
});
|
|
2854
3272
|
}
|
|
2855
3273
|
}
|
|
2856
3274
|
}
|
|
2857
|
-
|
|
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(
|
|
2858
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)
|
|
2859
3279
|
).slice(0, maxSymbols);
|
|
2860
3280
|
}
|
|
@@ -2877,20 +3297,22 @@ function collectRelationships(graph, primary) {
|
|
|
2877
3297
|
key,
|
|
2878
3298
|
text: edgeText(graph, edge, direction),
|
|
2879
3299
|
resolution: edge.resolution,
|
|
3300
|
+
provenance: edge.provenance,
|
|
2880
3301
|
priority
|
|
2881
3302
|
});
|
|
2882
3303
|
};
|
|
2883
3304
|
for (const candidate of primary) {
|
|
2884
|
-
|
|
2885
|
-
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);
|
|
2886
3308
|
}
|
|
2887
3309
|
return rows.sort(
|
|
2888
3310
|
(a, b) => b.priority - a.priority || a.resolution.localeCompare(b.resolution) || a.text.localeCompare(b.text)
|
|
2889
3311
|
);
|
|
2890
3312
|
}
|
|
2891
|
-
function addFile(files,
|
|
2892
|
-
const previous = files.get(
|
|
2893
|
-
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 });
|
|
2894
3316
|
}
|
|
2895
3317
|
function gatherFiles(index, route, primary, change, maxFiles) {
|
|
2896
3318
|
const files = /* @__PURE__ */ new Map();
|
|
@@ -2913,7 +3335,7 @@ function gatherFiles(index, route, primary, change, maxFiles) {
|
|
|
2913
3335
|
addFile(files, pathRow.changed.file, 90, "changed path");
|
|
2914
3336
|
}
|
|
2915
3337
|
}
|
|
2916
|
-
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 })) {
|
|
2917
3339
|
addFile(files, entry.path, Math.min(35, entry.score + 10), "repo map");
|
|
2918
3340
|
}
|
|
2919
3341
|
return [...files.values()].sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).slice(0, maxFiles);
|
|
@@ -2929,118 +3351,6 @@ function confidenceOf(primary, relationships, change) {
|
|
|
2929
3351
|
}
|
|
2930
3352
|
return out;
|
|
2931
3353
|
}
|
|
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
3354
|
async function buildTaskContext(index, task, options = {}) {
|
|
3045
3355
|
const budgetChars = clamp2(options.budgetChars, 300, 2e4, 5e3);
|
|
3046
3356
|
const maxFiles = clamp2(options.maxFiles, 1, 50, 12);
|
|
@@ -3070,6 +3380,19 @@ async function buildTaskContext(index, task, options = {}) {
|
|
|
3070
3380
|
changedFiles,
|
|
3071
3381
|
callerCounts(index)
|
|
3072
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
|
+
}
|
|
3073
3396
|
const graph = route.includeRelationships ? buildReferenceGraph(index) : null;
|
|
3074
3397
|
const relationships = graph ? collectRelationships(graph, primarySymbols) : [];
|
|
3075
3398
|
if (changeContext) {
|
|
@@ -3082,13 +3405,18 @@ async function buildTaskContext(index, task, options = {}) {
|
|
|
3082
3405
|
key,
|
|
3083
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}]`,
|
|
3084
3407
|
resolution: row.resolution,
|
|
3408
|
+
provenance: row.via.reason,
|
|
3085
3409
|
priority: 110
|
|
3086
3410
|
});
|
|
3087
3411
|
}
|
|
3088
3412
|
relationships.sort((a, b) => b.priority - a.priority || a.text.localeCompare(b.text));
|
|
3089
3413
|
}
|
|
3090
3414
|
const relevantFiles = gatherFiles(index, route, primarySymbols, changeContext, maxFiles);
|
|
3091
|
-
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);
|
|
3092
3420
|
const confidence = confidenceOf(primarySymbols, relationships, changeContext);
|
|
3093
3421
|
const partial = {
|
|
3094
3422
|
root: index.root,
|
|
@@ -3099,29 +3427,64 @@ async function buildTaskContext(index, task, options = {}) {
|
|
|
3099
3427
|
relationships,
|
|
3100
3428
|
changeContext,
|
|
3101
3429
|
tests,
|
|
3430
|
+
testEvidence,
|
|
3102
3431
|
confidence,
|
|
3103
3432
|
warnings
|
|
3104
3433
|
};
|
|
3105
|
-
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;
|
|
3106
3448
|
return {
|
|
3107
3449
|
...partial,
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
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 }
|
|
3113
3465
|
};
|
|
3114
3466
|
}
|
|
3115
3467
|
function renderTaskContext(result) {
|
|
3116
|
-
|
|
3117
|
-
return rendered.text;
|
|
3468
|
+
return renderContextPack(result.pack);
|
|
3118
3469
|
}
|
|
3119
3470
|
|
|
3120
3471
|
// src/tools.ts
|
|
3121
3472
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3122
|
-
import
|
|
3473
|
+
import path10 from "path";
|
|
3123
3474
|
|
|
3124
3475
|
// src/config.ts
|
|
3476
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
3477
|
+
var Config = Schema.object({
|
|
3478
|
+
excludeDirs: Schema.array(Schema.string()).default([]).volatile(),
|
|
3479
|
+
mapTopFiles: Schema.number().default(24).volatile(),
|
|
3480
|
+
mapMaxChars: Schema.number().default(3200).volatile(),
|
|
3481
|
+
mapTtlMs: Schema.number().default(6e4).volatile(),
|
|
3482
|
+
autoInject: Schema.boolean().default(true).volatile(),
|
|
3483
|
+
codeHealth: Schema.boolean().default(false).volatile(),
|
|
3484
|
+
toolSurface: Schema.string().default("full").volatile(),
|
|
3485
|
+
externalWatch: Schema.boolean().default(true).volatile(),
|
|
3486
|
+
watchDebounceMs: Schema.number().default(120).volatile()
|
|
3487
|
+
});
|
|
3125
3488
|
var DEFAULTS = {
|
|
3126
3489
|
excludeDirs: [],
|
|
3127
3490
|
mapTopFiles: 24,
|
|
@@ -3129,14 +3492,17 @@ var DEFAULTS = {
|
|
|
3129
3492
|
mapTtlMs: 6e4,
|
|
3130
3493
|
autoInject: true,
|
|
3131
3494
|
codeHealth: false,
|
|
3132
|
-
toolSurface: "full"
|
|
3495
|
+
toolSurface: "full",
|
|
3496
|
+
externalWatch: true,
|
|
3497
|
+
watchDebounceMs: 120
|
|
3133
3498
|
};
|
|
3134
3499
|
var state = { current: { ...DEFAULTS } };
|
|
3135
3500
|
function applyConfig(partial) {
|
|
3501
|
+
const resolved = partial ? Object.fromEntries(Object.entries(partial).map(([key, value]) => [key, unwrapVolatile(value)])) : void 0;
|
|
3136
3502
|
state.current = {
|
|
3137
3503
|
...DEFAULTS,
|
|
3138
|
-
...
|
|
3139
|
-
excludeDirs: [...DEFAULTS.excludeDirs, ...
|
|
3504
|
+
...resolved ?? {},
|
|
3505
|
+
excludeDirs: [...DEFAULTS.excludeDirs, ...resolved?.excludeDirs ?? []]
|
|
3140
3506
|
};
|
|
3141
3507
|
if (!Number.isFinite(state.current.mapTopFiles) || state.current.mapTopFiles < 1) {
|
|
3142
3508
|
state.current.mapTopFiles = DEFAULTS.mapTopFiles;
|
|
@@ -3150,6 +3516,16 @@ function applyConfig(partial) {
|
|
|
3150
3516
|
if (state.current.toolSurface !== "full" && state.current.toolSurface !== "compact") {
|
|
3151
3517
|
state.current.toolSurface = DEFAULTS.toolSurface;
|
|
3152
3518
|
}
|
|
3519
|
+
if (typeof state.current.externalWatch !== "boolean") state.current.externalWatch = DEFAULTS.externalWatch;
|
|
3520
|
+
if (!Number.isFinite(state.current.watchDebounceMs) || state.current.watchDebounceMs < 20) {
|
|
3521
|
+
state.current.watchDebounceMs = DEFAULTS.watchDebounceMs;
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
function unwrapVolatile(value) {
|
|
3525
|
+
if (typeof value !== "object" || value === null || !("get" in value) || typeof value.get !== "function") {
|
|
3526
|
+
return value;
|
|
3527
|
+
}
|
|
3528
|
+
return value.get();
|
|
3153
3529
|
}
|
|
3154
3530
|
function getConfig() {
|
|
3155
3531
|
return state.current;
|
|
@@ -3158,67 +3534,192 @@ function indexOptions() {
|
|
|
3158
3534
|
return { excludeDirs: state.current.excludeDirs };
|
|
3159
3535
|
}
|
|
3160
3536
|
|
|
3161
|
-
// src/
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3537
|
+
// src/repo-context.ts
|
|
3538
|
+
import { realpath as realpath2 } from "fs/promises";
|
|
3539
|
+
import path9 from "path";
|
|
3540
|
+
import chokidar from "chokidar";
|
|
3541
|
+
function ignoredRelativePath(rel, options) {
|
|
3542
|
+
const components = rel.split(/[\\/]+/).filter(Boolean);
|
|
3543
|
+
const excluded = /* @__PURE__ */ new Set([...DEFAULT_EXCLUDED_DIRS, ...options.excludeDirs ?? []]);
|
|
3544
|
+
return components.some((component) => excluded.has(component));
|
|
3545
|
+
}
|
|
3546
|
+
async function canonicalRepoRoot(root) {
|
|
3547
|
+
const resolved = await realpath2(path9.resolve(root));
|
|
3548
|
+
return path9.normalize(resolved);
|
|
3549
|
+
}
|
|
3550
|
+
var RepoContextManager = class {
|
|
3551
|
+
contexts = /* @__PURE__ */ new Map();
|
|
3552
|
+
options;
|
|
3553
|
+
disposed = false;
|
|
3554
|
+
constructor(options) {
|
|
3555
|
+
this.options = {
|
|
3556
|
+
indexOptions: options.indexOptions,
|
|
3557
|
+
watch: options.watch ?? true,
|
|
3558
|
+
debounceMs: Math.max(20, options.debounceMs ?? 120),
|
|
3559
|
+
maxContexts: Math.max(1, options.maxContexts ?? 4),
|
|
3560
|
+
onRefresh: options.onRefresh
|
|
3561
|
+
};
|
|
3562
|
+
}
|
|
3563
|
+
async get(root, force = false) {
|
|
3564
|
+
if (this.disposed) throw new Error("repo context manager is disposed");
|
|
3565
|
+
const canonical = await canonicalRepoRoot(root);
|
|
3566
|
+
const key = cacheKeyForRoot(canonical);
|
|
3567
|
+
let context = this.contexts.get(key);
|
|
3568
|
+
if (!context) {
|
|
3569
|
+
context = {
|
|
3570
|
+
root: canonical,
|
|
3571
|
+
index: null,
|
|
3572
|
+
lastRefreshAt: 0,
|
|
3573
|
+
dirtyFiles: /* @__PURE__ */ new Set(),
|
|
3574
|
+
ignoreMatcherCache: /* @__PURE__ */ new Map(),
|
|
3575
|
+
watcher: null,
|
|
3576
|
+
debounceTimer: null,
|
|
3577
|
+
refresh: null,
|
|
3578
|
+
disposed: false,
|
|
3579
|
+
generation: 0,
|
|
3580
|
+
lastUsedAt: Date.now()
|
|
3581
|
+
};
|
|
3582
|
+
this.contexts.set(key, context);
|
|
3583
|
+
this.attachWatcher(context);
|
|
3584
|
+
this.evictInactive();
|
|
3585
|
+
} else {
|
|
3586
|
+
this.contexts.delete(key);
|
|
3587
|
+
this.contexts.set(key, context);
|
|
3588
|
+
context.lastUsedAt = Date.now();
|
|
3589
|
+
}
|
|
3590
|
+
let index = await this.refresh(context, force);
|
|
3591
|
+
while (!context.disposed && context.dirtyFiles.size > 0) {
|
|
3592
|
+
if (context.refresh) await context.refresh;
|
|
3593
|
+
else index = await this.refresh(context);
|
|
3594
|
+
}
|
|
3595
|
+
return index;
|
|
3596
|
+
}
|
|
3597
|
+
getContext(root) {
|
|
3598
|
+
const key = cacheKeyForRoot(path9.resolve(root));
|
|
3599
|
+
return this.contexts.get(key);
|
|
3600
|
+
}
|
|
3601
|
+
async dispose() {
|
|
3602
|
+
if (this.disposed) return;
|
|
3603
|
+
this.disposed = true;
|
|
3604
|
+
const contexts = [...this.contexts.values()];
|
|
3605
|
+
this.contexts.clear();
|
|
3606
|
+
await Promise.all(contexts.map((context) => this.disposeContext(context)));
|
|
3607
|
+
}
|
|
3608
|
+
reconfigure(options) {
|
|
3609
|
+
if (options.debounceMs !== void 0) this.options.debounceMs = Math.max(20, options.debounceMs);
|
|
3610
|
+
if (options.watch === void 0 || options.watch === this.options.watch) return;
|
|
3611
|
+
this.options.watch = options.watch;
|
|
3612
|
+
for (const context of this.contexts.values()) {
|
|
3613
|
+
if (options.watch) this.attachWatcher(context);
|
|
3614
|
+
else {
|
|
3615
|
+
if (context.debounceTimer) clearTimeout(context.debounceTimer);
|
|
3616
|
+
context.debounceTimer = null;
|
|
3617
|
+
const watcher = context.watcher;
|
|
3618
|
+
context.watcher = null;
|
|
3619
|
+
void watcher?.close();
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
async refresh(context, force = false) {
|
|
3624
|
+
if (context.refresh) {
|
|
3625
|
+
const index = await context.refresh;
|
|
3626
|
+
return force ? this.refresh(context, true) : index;
|
|
3627
|
+
}
|
|
3628
|
+
const generation = context.generation;
|
|
3629
|
+
const dirty = force ? /* @__PURE__ */ new Set(["*"]) : new Set(context.dirtyFiles);
|
|
3630
|
+
context.dirtyFiles.clear();
|
|
3631
|
+
const promise = buildIndexWithCache(context.root, this.options.indexOptions(), void 0, dirty).then((index) => {
|
|
3632
|
+
if (!context.disposed && context.generation === generation) {
|
|
3633
|
+
context.index = index;
|
|
3634
|
+
context.lastRefreshAt = Date.now();
|
|
3635
|
+
this.options.onRefresh?.(context.root, index);
|
|
3174
3636
|
}
|
|
3175
3637
|
return index;
|
|
3176
3638
|
}).finally(() => {
|
|
3177
|
-
if (
|
|
3639
|
+
if (context.refresh === promise) context.refresh = null;
|
|
3640
|
+
if (!context.disposed && context.dirtyFiles.size > 0) this.scheduleRefresh(context);
|
|
3178
3641
|
});
|
|
3179
|
-
|
|
3642
|
+
context.refresh = promise;
|
|
3180
3643
|
return promise;
|
|
3181
3644
|
}
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
const
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3645
|
+
attachWatcher(context) {
|
|
3646
|
+
if (!this.options.watch) return;
|
|
3647
|
+
const options = this.options.indexOptions();
|
|
3648
|
+
const ignored = (absolute) => {
|
|
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);
|
|
3651
|
+
};
|
|
3652
|
+
const watcher = chokidar.watch(context.root, {
|
|
3653
|
+
ignored,
|
|
3654
|
+
ignoreInitial: true,
|
|
3655
|
+
persistent: true,
|
|
3656
|
+
awaitWriteFinish: { stabilityThreshold: 60, pollInterval: 20 },
|
|
3657
|
+
followSymlinks: false
|
|
3658
|
+
});
|
|
3659
|
+
context.watcher = watcher;
|
|
3660
|
+
watcher.on("all", (event, absolute) => {
|
|
3661
|
+
if (context.disposed || !["add", "change", "unlink"].includes(event)) return;
|
|
3662
|
+
const rel = path9.relative(context.root, absolute).split(path9.sep).join("/");
|
|
3663
|
+
if (!rel || ignoredRelativePath(rel, this.options.indexOptions()) || isGitIgnoredPath(context.root, rel, context.ignoreMatcherCache)) return;
|
|
3664
|
+
if (!SUPPORTED_EXTS.has(path9.extname(rel).toLowerCase())) return;
|
|
3665
|
+
context.dirtyFiles.add(rel);
|
|
3666
|
+
this.scheduleRefresh(context);
|
|
3667
|
+
});
|
|
3668
|
+
watcher.on("error", (error) => {
|
|
3669
|
+
if (!context.disposed) console.warn(`[dsh-code-index] watcher failed for ${context.root}: ${String(error)}`);
|
|
3670
|
+
});
|
|
3671
|
+
}
|
|
3672
|
+
scheduleRefresh(context) {
|
|
3673
|
+
if (context.disposed || this.disposed) return;
|
|
3674
|
+
if (context.debounceTimer) clearTimeout(context.debounceTimer);
|
|
3675
|
+
context.debounceTimer = setTimeout(() => {
|
|
3676
|
+
context.debounceTimer = null;
|
|
3677
|
+
void this.refresh(context).catch((error) => {
|
|
3678
|
+
if (!context.disposed) console.warn(`[dsh-code-index] refresh failed for ${context.root}: ${String(error)}`);
|
|
3679
|
+
});
|
|
3680
|
+
}, this.options.debounceMs);
|
|
3681
|
+
}
|
|
3682
|
+
evictInactive() {
|
|
3683
|
+
while (this.contexts.size > this.options.maxContexts) {
|
|
3684
|
+
const oldest = this.contexts.entries().next().value;
|
|
3685
|
+
if (!oldest) return;
|
|
3686
|
+
this.contexts.delete(oldest[0]);
|
|
3687
|
+
void this.disposeContext(oldest[1]);
|
|
3207
3688
|
}
|
|
3208
|
-
}
|
|
3689
|
+
}
|
|
3690
|
+
async disposeContext(context) {
|
|
3691
|
+
context.disposed = true;
|
|
3692
|
+
context.generation++;
|
|
3693
|
+
if (context.debounceTimer) clearTimeout(context.debounceTimer);
|
|
3694
|
+
context.debounceTimer = null;
|
|
3695
|
+
const watcher = context.watcher;
|
|
3696
|
+
context.watcher = null;
|
|
3697
|
+
if (watcher) await watcher.close();
|
|
3698
|
+
await context.refresh?.catch(() => void 0);
|
|
3699
|
+
context.dirtyFiles.clear();
|
|
3700
|
+
context.ignoreMatcherCache.clear();
|
|
3701
|
+
context.index = null;
|
|
3702
|
+
}
|
|
3703
|
+
};
|
|
3704
|
+
|
|
3705
|
+
// src/tools.ts
|
|
3706
|
+
var activeRepoContexts = null;
|
|
3707
|
+
function activateRepoContextManager(manager) {
|
|
3708
|
+
activeRepoContexts = manager;
|
|
3709
|
+
}
|
|
3710
|
+
function clearRepoContextManager(manager) {
|
|
3711
|
+
if (activeRepoContexts === manager) activeRepoContexts = null;
|
|
3209
3712
|
}
|
|
3210
|
-
var indexCache = createIndexCache((root) => buildIndexWithCache(root, indexOptions()));
|
|
3211
3713
|
function getIndex(root, force = false) {
|
|
3212
|
-
return
|
|
3714
|
+
return activeRepoContexts ? activeRepoContexts.get(root, force) : buildIndexWithCache(root, indexOptions(), void 0, force ? /* @__PURE__ */ new Set(["*"]) : void 0);
|
|
3213
3715
|
}
|
|
3214
3716
|
function invalidateIndexCache() {
|
|
3215
|
-
indexCache.invalidate();
|
|
3216
3717
|
}
|
|
3217
3718
|
async function resolveRoot(arg, exec) {
|
|
3218
|
-
const cwd = exec.agent?.session?.header?.cwd;
|
|
3719
|
+
const cwd = exec.agent?.session?.header?.cwd ?? exec.session?.header?.cwd ?? exec.cwd;
|
|
3219
3720
|
const base = arg ?? cwd ?? process.cwd();
|
|
3220
3721
|
const root = await findRepoRoot(base);
|
|
3221
|
-
if (!root) throw new Error(`no git repository found from ${
|
|
3722
|
+
if (!root) throw new Error(`no git repository found from ${path10.resolve(base)}`);
|
|
3222
3723
|
return root;
|
|
3223
3724
|
}
|
|
3224
3725
|
function hitsToSearchMeta(hits) {
|
|
@@ -3233,7 +3734,7 @@ function hitsToSearchMeta(hits) {
|
|
|
3233
3734
|
}
|
|
3234
3735
|
return {
|
|
3235
3736
|
shape: "matches",
|
|
3236
|
-
files: [...byFile.entries()].map(([
|
|
3737
|
+
files: [...byFile.entries()].map(([path11, matches]) => ({ path: path11, matches })),
|
|
3237
3738
|
truncated: false,
|
|
3238
3739
|
total: hits.length
|
|
3239
3740
|
};
|
|
@@ -3565,6 +4066,11 @@ var tools = [
|
|
|
3565
4066
|
name: "code_context",
|
|
3566
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.",
|
|
3567
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
|
+
},
|
|
3568
4074
|
task: {
|
|
3569
4075
|
type: "string",
|
|
3570
4076
|
required: true,
|
|
@@ -3588,8 +4094,8 @@ var tools = [
|
|
|
3588
4094
|
}
|
|
3589
4095
|
},
|
|
3590
4096
|
output: {
|
|
3591
|
-
schema: { type: "string" },
|
|
3592
|
-
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) }]
|
|
3593
4099
|
},
|
|
3594
4100
|
presentCall: (args) => ({
|
|
3595
4101
|
card: "generic",
|
|
@@ -3609,9 +4115,9 @@ var tools = [
|
|
|
3609
4115
|
maxFiles: args.maxFiles,
|
|
3610
4116
|
maxSymbols: args.maxSymbols
|
|
3611
4117
|
});
|
|
3612
|
-
return renderTaskContext(result);
|
|
4118
|
+
return args.outputFormat === "pack" ? JSON.parse(JSON.stringify(result.pack)) : renderTaskContext(result);
|
|
3613
4119
|
} catch (error) {
|
|
3614
|
-
return `code_context: ${error.message ?? String(error)}`;
|
|
4120
|
+
return `code_context: ${(error.message ?? String(error)).slice(0, 240)}`;
|
|
3615
4121
|
}
|
|
3616
4122
|
}
|
|
3617
4123
|
}),
|
|
@@ -3656,25 +4162,32 @@ var tools = [
|
|
|
3656
4162
|
];
|
|
3657
4163
|
|
|
3658
4164
|
// src/settings.ts
|
|
3659
|
-
import
|
|
4165
|
+
import Schema2 from "@deepseek-ai/schemastery";
|
|
3660
4166
|
var SETTINGS_NAMESPACE = "code-index";
|
|
3661
|
-
var settingsSchema =
|
|
3662
|
-
excludeDirs:
|
|
3663
|
-
mapTopFiles:
|
|
3664
|
-
mapMaxChars:
|
|
3665
|
-
mapTtlMs:
|
|
3666
|
-
autoInject:
|
|
3667
|
-
codeHealth:
|
|
3668
|
-
toolSurface:
|
|
4167
|
+
var settingsSchema = Schema2.object({
|
|
4168
|
+
excludeDirs: Schema2.array(Schema2.string()).default([]),
|
|
4169
|
+
mapTopFiles: Schema2.number().default(24),
|
|
4170
|
+
mapMaxChars: Schema2.number().default(3200),
|
|
4171
|
+
mapTtlMs: Schema2.number().default(6e4),
|
|
4172
|
+
autoInject: Schema2.boolean().default(true),
|
|
4173
|
+
codeHealth: Schema2.boolean().default(false),
|
|
4174
|
+
toolSurface: Schema2.string().default("full"),
|
|
4175
|
+
externalWatch: Schema2.boolean().default(true),
|
|
4176
|
+
watchDebounceMs: Schema2.number().default(120)
|
|
3669
4177
|
});
|
|
3670
|
-
function registerSettings(getService, pluginConfig, onResolved) {
|
|
4178
|
+
function registerSettings(getService, pluginConfig, onResolved, fiber) {
|
|
3671
4179
|
const service = getService("settings");
|
|
3672
|
-
if (!service) return;
|
|
4180
|
+
if (!service) return void 0;
|
|
4181
|
+
if (service.configure) {
|
|
4182
|
+
const dispose = service.configure({ auto: false }, fiber);
|
|
4183
|
+
onResolved(pluginConfig ?? {});
|
|
4184
|
+
return typeof dispose === "function" ? dispose : void 0;
|
|
4185
|
+
}
|
|
3673
4186
|
const scope = service.register(SETTINGS_NAMESPACE, settingsSchema, {
|
|
3674
4187
|
base: pluginConfig ?? {}
|
|
3675
4188
|
});
|
|
3676
4189
|
onResolved(scope.get());
|
|
3677
|
-
scope.watch((next) => {
|
|
4190
|
+
return scope.watch((next) => {
|
|
3678
4191
|
onResolved(next);
|
|
3679
4192
|
});
|
|
3680
4193
|
}
|
|
@@ -3687,30 +4200,51 @@ function apply(ctx, pluginConfig) {
|
|
|
3687
4200
|
invalidateIndexCache();
|
|
3688
4201
|
ctx.effect(() => {
|
|
3689
4202
|
const disposers = [];
|
|
4203
|
+
const repoContexts = new RepoContextManager({
|
|
4204
|
+
indexOptions,
|
|
4205
|
+
watch: getConfig().externalWatch,
|
|
4206
|
+
debounceMs: getConfig().watchDebounceMs
|
|
4207
|
+
});
|
|
4208
|
+
activateRepoContextManager(repoContexts);
|
|
4209
|
+
const updateConfig = (next) => {
|
|
4210
|
+
applyConfig(next);
|
|
4211
|
+
repoContexts.reconfigure({ watch: getConfig().externalWatch, debounceMs: getConfig().watchDebounceMs });
|
|
4212
|
+
};
|
|
3690
4213
|
console.log("[dsh-code-index] plugin loaded");
|
|
3691
4214
|
if (ctx.inject) {
|
|
3692
|
-
ctx.inject(["settings"], (settingsCtx) => {
|
|
3693
|
-
registerSettings(
|
|
4215
|
+
const injected = ctx.inject(["settings"], (settingsCtx) => {
|
|
4216
|
+
const dispose = registerSettings(
|
|
3694
4217
|
(name2) => settingsCtx.get(name2),
|
|
3695
4218
|
pluginConfig,
|
|
3696
|
-
(resolved) =>
|
|
4219
|
+
(resolved) => updateConfig(resolved),
|
|
4220
|
+
settingsCtx.fiber
|
|
3697
4221
|
);
|
|
4222
|
+
if (dispose) disposers.push(dispose);
|
|
3698
4223
|
});
|
|
4224
|
+
if (typeof injected === "function") disposers.push(injected);
|
|
3699
4225
|
}
|
|
4226
|
+
const removeVolatileListener = ctx.on?.("loader/volatile-update", () => updateConfig(pluginConfig));
|
|
4227
|
+
if (removeVolatileListener) disposers.push(removeVolatileListener);
|
|
3700
4228
|
const visibleTools = getConfig().toolSurface === "compact" ? tools.filter((tool) => ["code_index", "code_context", "code_health"].includes(tool.name)) : tools;
|
|
3701
4229
|
for (const tool of visibleTools) {
|
|
3702
4230
|
if (tool.name === "code_health" && !getConfig().codeHealth) continue;
|
|
3703
4231
|
disposers.push(ctx.tools.register(tool));
|
|
3704
4232
|
console.log(`[dsh-code-index] registered tool: ${tool.name}`);
|
|
3705
4233
|
}
|
|
3706
|
-
|
|
3707
|
-
|
|
4234
|
+
const cachedMaps = /* @__PURE__ */ new Map();
|
|
4235
|
+
const saveMap = (key, value) => {
|
|
4236
|
+
cachedMaps.delete(key);
|
|
4237
|
+
cachedMaps.set(key, value);
|
|
4238
|
+
while (cachedMaps.size > 4) cachedMaps.delete(cachedMaps.keys().next().value);
|
|
4239
|
+
};
|
|
4240
|
+
async function warmMap(base) {
|
|
3708
4241
|
const now = Date.now();
|
|
3709
4242
|
const cfg = getConfig();
|
|
4243
|
+
const key = cacheKeyForRoot(base);
|
|
3710
4244
|
try {
|
|
3711
|
-
const root = await findRepoRoot(
|
|
4245
|
+
const root = await findRepoRoot(base);
|
|
3712
4246
|
if (!root) {
|
|
3713
|
-
|
|
4247
|
+
saveMap(key, { root: "", at: now, text: "" });
|
|
3714
4248
|
return;
|
|
3715
4249
|
}
|
|
3716
4250
|
const index = await getIndex(root);
|
|
@@ -3719,27 +4253,34 @@ function apply(ctx, pluginConfig) {
|
|
|
3719
4253
|
{ maxChars: cfg.mapMaxChars }
|
|
3720
4254
|
);
|
|
3721
4255
|
const stats = symbolCount(index);
|
|
3722
|
-
|
|
4256
|
+
saveMap(key, {
|
|
3723
4257
|
root: index.root,
|
|
3724
4258
|
at: Date.now(),
|
|
3725
|
-
text: text ?
|
|
4259
|
+
text: text ? `Project: ${index.root}
|
|
4260
|
+
${text}
|
|
3726
4261
|
|
|
3727
4262
|
(summary: ${index.files.length} files, ${stats} symbols)` : ""
|
|
3728
|
-
};
|
|
4263
|
+
});
|
|
3729
4264
|
} catch {
|
|
3730
|
-
|
|
4265
|
+
saveMap(key, { root: "", at: now, text: "" });
|
|
3731
4266
|
}
|
|
3732
4267
|
}
|
|
3733
|
-
void warmMap();
|
|
3734
4268
|
if (getConfig().autoInject) {
|
|
3735
4269
|
disposers.push(ctx.systemPrompt.section({
|
|
3736
4270
|
name: "code-index:repo-map",
|
|
3737
4271
|
order: 60,
|
|
3738
4272
|
// before tool guidance (100–199), after persona (0)
|
|
3739
|
-
text: () => {
|
|
4273
|
+
text: (rawContext) => {
|
|
4274
|
+
const context = rawContext;
|
|
4275
|
+
const base = context.agent?.session?.header?.cwd ?? context.session?.header?.cwd ?? process.cwd();
|
|
4276
|
+
const key = cacheKeyForRoot(base);
|
|
3740
4277
|
const now = Date.now();
|
|
3741
|
-
|
|
3742
|
-
|
|
4278
|
+
const cached = cachedMaps.get(key);
|
|
4279
|
+
if (cached && now - cached.at < getConfig().mapTtlMs) {
|
|
4280
|
+
saveMap(key, cached);
|
|
4281
|
+
return cached.text;
|
|
4282
|
+
}
|
|
4283
|
+
void warmMap(base);
|
|
3743
4284
|
return cached?.text ?? "";
|
|
3744
4285
|
}
|
|
3745
4286
|
}));
|
|
@@ -3753,6 +4294,9 @@ function apply(ctx, pluginConfig) {
|
|
|
3753
4294
|
errors.push(error);
|
|
3754
4295
|
}
|
|
3755
4296
|
}
|
|
4297
|
+
clearRepoContextManager(repoContexts);
|
|
4298
|
+
void repoContexts.dispose();
|
|
4299
|
+
cachedMaps.clear();
|
|
3756
4300
|
console.log("[dsh-code-index] plugin unloaded");
|
|
3757
4301
|
if (errors.length > 0) throw new AggregateError(errors, "failed to unload dsh-code-index");
|
|
3758
4302
|
};
|
|
@@ -3760,6 +4304,7 @@ function apply(ctx, pluginConfig) {
|
|
|
3760
4304
|
}
|
|
3761
4305
|
export {
|
|
3762
4306
|
CACHE_DIR_NAME,
|
|
4307
|
+
Config,
|
|
3763
4308
|
DEFAULT_EXCLUDED_DIRS,
|
|
3764
4309
|
SUPPORTED_EXTS,
|
|
3765
4310
|
apply,
|
|
@@ -3787,6 +4332,7 @@ export {
|
|
|
3787
4332
|
readGitFileAtRef,
|
|
3788
4333
|
readWorkingTreeDiff,
|
|
3789
4334
|
renderChangeContext,
|
|
4335
|
+
renderContextPack,
|
|
3790
4336
|
renderHit,
|
|
3791
4337
|
renderRepoMap,
|
|
3792
4338
|
renderTaskContext,
|