dsh-code-index 0.8.0 → 0.9.0

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