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