dsh-code-index 0.11.1 → 0.12.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
@@ -2304,6 +2304,102 @@ async function readGitFileAtRef(root, ref, repoRelativePath) {
2304
2304
 
2305
2305
  // src/change-context.ts
2306
2306
  import path7 from "path";
2307
+
2308
+ // src/evidence-summary.ts
2309
+ function summarizeEvidence(index, options) {
2310
+ const status = { parsed: 0, partial: 0, failed: 0, unknown: 0 };
2311
+ const diagnostics = [];
2312
+ for (const file of index.files) {
2313
+ const diagnostic = file.diagnostic;
2314
+ status[diagnostic?.status ?? "unknown"]++;
2315
+ if (diagnostic && diagnostic.status !== "parsed") diagnostics.push({
2316
+ file: file.path,
2317
+ status: diagnostic.status,
2318
+ ...diagnostic.issues[0] ? { line: diagnostic.issues[0].line } : {},
2319
+ ...diagnostic.reason ? { reason: diagnostic.reason } : {}
2320
+ });
2321
+ }
2322
+ diagnostics.sort((a, b) => a.file.localeCompare(b.file));
2323
+ const relationships = { exact: 0, "import-scoped": 0, "name-only": 0 };
2324
+ for (const resolution of options.resolutions) relationships[resolution]++;
2325
+ const gaps = [...options.gaps ?? []];
2326
+ if (status.partial || status.failed) gaps.push({ kind: "index-incomplete", code: "parse-diagnostics", source: "indexed-file.diagnostic" });
2327
+ if (status.unknown) gaps.push({ kind: "unavailable", code: "parse-status-unknown", source: "indexed-file.diagnostic" });
2328
+ const result = {
2329
+ scope: "indexed-static-only",
2330
+ basis: options.basis ?? "context-pack",
2331
+ index: {
2332
+ ...status,
2333
+ ...diagnostics.length ? { diagnostics: diagnostics.slice(0, 3) } : {},
2334
+ ...diagnostics.length > 3 ? { diagnosticsOmitted: diagnostics.length - 3 } : {}
2335
+ },
2336
+ relationships,
2337
+ tests: { candidates: new Set(options.testFiles).size, verification: "not-run" }
2338
+ };
2339
+ if (options.graph && options.seeds?.length) {
2340
+ const graph = options.graph;
2341
+ const seeds = [...new Map(options.seeds.map((seed) => [seed.id, seed])).values()];
2342
+ const zero = seeds.filter((seed) => !graph.incoming.get(seed.id)?.length).map((seed) => ({ file: seed.file, line: seed.line, name: seed.name }));
2343
+ result.callers = {
2344
+ checkedSymbols: seeds.length,
2345
+ zero: zero.slice(0, 3),
2346
+ ...zero.length > 3 ? { zeroOmitted: zero.length - 3 } : {}
2347
+ };
2348
+ const unresolved = [];
2349
+ for (const seed of seeds) {
2350
+ const edges = graph.outgoing.get(seed.id) ?? [];
2351
+ for (const call of index.files.find((file) => file.path === seed.file)?.calls ?? []) {
2352
+ if (call.fromId !== seed.id) continue;
2353
+ if (!edges.some((edge) => edge.callSite.file === seed.file && edge.callSite.line === call.line && edge.name === call.name)) {
2354
+ unresolved.push({ file: seed.file, line: call.line, name: call.name });
2355
+ }
2356
+ }
2357
+ }
2358
+ if (unresolved.length) {
2359
+ result.unresolved = {
2360
+ count: unresolved.length,
2361
+ sites: unresolved.slice(0, 3),
2362
+ ...unresolved.length > 3 ? { sitesOmitted: unresolved.length - 3 } : {}
2363
+ };
2364
+ gaps.push({ kind: "not-found", code: "unresolved-call-targets", source: "indexed-call-sites" });
2365
+ }
2366
+ }
2367
+ if (gaps.length) result.gaps = [...new Map(gaps.map((gap) => [JSON.stringify(gap), gap])).values()];
2368
+ return result;
2369
+ }
2370
+ function renderEvidenceSummary(summary) {
2371
+ const i = summary.index;
2372
+ const r = summary.relationships;
2373
+ const lines = [
2374
+ `Evidence (indexed static scope; unscanned files not assessed):`,
2375
+ `Index: ${i.parsed} parsed, ${i.partial} partial, ${i.failed} failed, ${i.unknown} unknown.`,
2376
+ `Selected relationship items: exact ${r.exact}, import-scoped ${r["import-scoped"]}, name-only ${r["name-only"]}.`,
2377
+ `Tests: ${summary.tests.candidates} candidate(s), not run; not verified coverage.`
2378
+ ];
2379
+ for (const d of i.diagnostics ?? []) lines.push(`Parse: ${d.file}${d.line ? `:${d.line}` : ""} ${d.status}${d.reason ? ` (${d.reason})` : ""}.`);
2380
+ if (i.diagnosticsOmitted) lines.push(`More parse diagnostics: ${i.diagnosticsOmitted}; inspect code_index.`);
2381
+ if (summary.callers?.zero.length) lines.push(`0 callers for ${summary.callers.zero.map((s) => `${s.name} (${s.file}:${s.line})`).join(", ")}: none found in current index/static resolution; not proof of no callers.`);
2382
+ if (summary.callers?.zeroOmitted) lines.push(`0 callers for ${summary.callers.zeroOmitted} other checked declaration(s) in indexed static scope only; not proof of no callers. Locations omitted for budget.`);
2383
+ if (summary.unresolved) lines.push(`Unresolved call sites: ${summary.unresolved.count}; target unknown, external not established.`);
2384
+ if (summary.gaps?.length) lines.push(`Evidence gaps: ${[...new Set(summary.gaps.map((g) => `${g.kind}/${g.code}`))].join(", ")}.`);
2385
+ return lines.join("\n");
2386
+ }
2387
+ function compactEvidence(summary) {
2388
+ const { diagnostics, diagnosticsOmitted, ...counts } = summary.index;
2389
+ const omitted = (diagnostics?.length ?? 0) + (diagnosticsOmitted ?? 0);
2390
+ return {
2391
+ ...summary,
2392
+ index: { ...counts, ...omitted ? { diagnosticsOmitted: omitted } : {} },
2393
+ ...summary.callers ? { callers: {
2394
+ checkedSymbols: summary.callers.checkedSymbols,
2395
+ zero: [],
2396
+ ...summary.callers.zero.length || summary.callers.zeroOmitted ? { zeroOmitted: summary.callers.zero.length + (summary.callers.zeroOmitted ?? 0) } : {}
2397
+ } } : {},
2398
+ ...summary.unresolved ? { unresolved: { count: summary.unresolved.count, sites: [], sitesOmitted: summary.unresolved.count } } : {}
2399
+ };
2400
+ }
2401
+
2402
+ // src/change-context.ts
2307
2403
  var LABEL_STRENGTH = {
2308
2404
  exact: 3,
2309
2405
  "import-scoped": 2,
@@ -2518,7 +2614,19 @@ async function buildChangeContext(index, input, options = {}) {
2518
2614
  frontier = next;
2519
2615
  }
2520
2616
  }
2521
- const allImpact = [...impactById.values()].sort(compareByDepthThenSymbol);
2617
+ const focusFiles = new Set(options.focusFiles ?? []);
2618
+ const focusIds = /* @__PURE__ */ new Set();
2619
+ if (focusFiles.size) {
2620
+ const impactAncestors = new Map([...allCallers, ...impactById.values()].map((row) => [row.symbol.id, row]));
2621
+ for (const row of impactById.values()) if (focusFiles.has(row.symbol.file)) {
2622
+ let cursor = row;
2623
+ for (let depth = 0; cursor && depth < maxDepth; depth++) {
2624
+ focusIds.add(cursor.symbol.id);
2625
+ cursor = impactAncestors.get(cursor.via.toId);
2626
+ }
2627
+ }
2628
+ }
2629
+ const allImpact = [...impactById.values()].sort((a, b) => Number(focusIds.has(b.symbol.id)) - Number(focusIds.has(a.symbol.id)) || compareByDepthThenSymbol(a, b));
2522
2630
  const allPaths = [];
2523
2631
  const pathKey = /* @__PURE__ */ new Set();
2524
2632
  for (const changed of changedSymbols) {
@@ -2545,7 +2653,7 @@ async function buildChangeContext(index, input, options = {}) {
2545
2653
  impact: allImpact.length > maxImpact,
2546
2654
  tests: allTests.length > maxTests
2547
2655
  };
2548
- return {
2656
+ const result = {
2549
2657
  root: index.root,
2550
2658
  source: input.kind,
2551
2659
  ...baseRef !== void 0 ? { baseRef } : {},
@@ -2559,6 +2667,26 @@ async function buildChangeContext(index, input, options = {}) {
2559
2667
  warnings,
2560
2668
  truncated
2561
2669
  };
2670
+ const gaps = [];
2671
+ if (!changedSymbols.length) gaps.push({ kind: "not-found", code: "no-changed-symbols", source: "diff-or-selection" });
2672
+ if (!allTests.length) gaps.push({ kind: "not-found", code: "no-test-candidates", source: "indexed-test-discovery" });
2673
+ if (Object.values(truncated).some(Boolean)) gaps.push({ kind: "selection-limit", code: "analysis-row-cap", source: "change-analysis" });
2674
+ for (const entry of result.unmapped.slice(0, 3)) gaps.push({
2675
+ kind: entry.reason === "base-content-unavailable" ? "unavailable" : "not-found",
2676
+ code: entry.reason,
2677
+ source: entry.reason === "base-content-unavailable" ? "baseline-content" : "diff-symbol-mapping",
2678
+ file: entry.file,
2679
+ side: entry.side
2680
+ });
2681
+ result.evidence = summarizeEvidence(index, {
2682
+ graph,
2683
+ basis: "change-analysis",
2684
+ seeds: changedSymbols.filter((row) => row.side === "current").map((row) => row.symbol),
2685
+ resolutions: [...result.directCallers, ...result.impact, ...result.importDependents, ...result.paths].map((row) => row.resolution),
2686
+ testFiles: result.tests.map((row) => row.file),
2687
+ gaps
2688
+ });
2689
+ return result;
2562
2690
  }
2563
2691
  function hopOf(edge) {
2564
2692
  return {
@@ -2917,7 +3045,7 @@ function renderChangeContext(result, maxChars = 12e3) {
2917
3045
  {
2918
3046
  title: "LIKELY AFFECTED TESTS",
2919
3047
  rows: result.tests.map(
2920
- (test) => `${test.file} (${test.reason}${test.distance !== void 0 ? `, distance ${test.distance}` : ""}) [${test.resolution}]`
3048
+ (test) => `${test.file} (candidate; not run; coverage unverified; ${test.reason}${test.distance !== void 0 ? `, distance ${test.distance}` : ""}) [${test.resolution}]`
2921
3049
  )
2922
3050
  },
2923
3051
  {
@@ -2929,11 +3057,12 @@ function renderChangeContext(result, maxChars = 12e3) {
2929
3057
  ];
2930
3058
  const summary = `change context \u2014 source: ${result.source}${result.baseRef !== void 0 ? ` (base ${result.baseRef})` : ""}: ${result.changed.length} changed symbol(s)`;
2931
3059
  const warningLines = result.warnings.map((warning) => `- ${warning}`);
3060
+ let evidenceText = result.evidence ? renderEvidenceSummary(result.evidence) : "";
2932
3061
  const render = (kept) => {
2933
3062
  const lines = [summary];
2934
3063
  sections.forEach((section, index) => {
2935
3064
  if (section.rows.length === 0) {
2936
- lines.push(`${section.title}: (none)`);
3065
+ lines.push(`${section.title}: ${section.title === "DIRECT CALLERS" ? "(none found in current indexed static scope)" : "(none)"}`);
2937
3066
  return;
2938
3067
  }
2939
3068
  lines.push(`${section.title}:`);
@@ -2941,6 +3070,7 @@ function renderChangeContext(result, maxChars = 12e3) {
2941
3070
  for (let row = 0; row < take; row++) lines.push(` ${section.rows[row]}`);
2942
3071
  if (take < section.rows.length) lines.push(` \u2026 (${section.rows.length - take} more)`);
2943
3072
  });
3073
+ if (evidenceText) lines.push(evidenceText);
2944
3074
  lines.push("WARNINGS:");
2945
3075
  if (warningLines.length === 0) lines.push(" (none)");
2946
3076
  else for (const warning of warningLines) lines.push(` ${warning}`);
@@ -2956,6 +3086,7 @@ function renderChangeContext(result, maxChars = 12e3) {
2956
3086
  return kept;
2957
3087
  };
2958
3088
  const total = flattened.length;
3089
+ if (render(countsFor(0)).length > maxChars) evidenceText = "";
2959
3090
  const full = render(sections.map((section) => section.rows.length));
2960
3091
  if (full.length <= maxChars) return full;
2961
3092
  const truncationNote = "\n\u2026 truncated rows to fit the character cap (raise maxChars or narrow the change)";
@@ -2979,6 +3110,75 @@ function renderChangeContext(result, maxChars = 12e3) {
2979
3110
  return out;
2980
3111
  }
2981
3112
 
3113
+ // src/context-terms.ts
3114
+ var STOP_WORDS = /* @__PURE__ */ new Set(
3115
+ [
3116
+ "a",
3117
+ "an",
3118
+ "and",
3119
+ "are",
3120
+ "be",
3121
+ "by",
3122
+ "does",
3123
+ "for",
3124
+ "from",
3125
+ "how",
3126
+ "in",
3127
+ "is",
3128
+ "it",
3129
+ "my",
3130
+ "of",
3131
+ "on",
3132
+ "or",
3133
+ "the",
3134
+ "this",
3135
+ "to",
3136
+ "what",
3137
+ "where",
3138
+ "why",
3139
+ "with",
3140
+ "src",
3141
+ "lib",
3142
+ "test",
3143
+ "tests",
3144
+ "spec",
3145
+ "fix",
3146
+ "explain",
3147
+ "please",
3148
+ "help",
3149
+ "current",
3150
+ "change",
3151
+ "changes",
3152
+ "code",
3153
+ "repo",
3154
+ "repository",
3155
+ "function",
3156
+ "symbol",
3157
+ "git"
3158
+ ]
3159
+ );
3160
+ var CONCEPT_TERMS = [
3161
+ [/忽略|白名单|嵌套规则|gitignore/iu, "ignore git"],
3162
+ [/扫描/u, "scan"],
3163
+ [/索引/u, "index scan"],
3164
+ [/监听|watcher/iu, "watch"],
3165
+ [/刷新/u, "refresh"],
3166
+ [/工具/u, "tool"],
3167
+ [/配置/u, "config"],
3168
+ [/加载/u, "load"],
3169
+ [/空白|空格/u, "whitespace"],
3170
+ [/启动/u, "startup"]
3171
+ ];
3172
+ function contextTerms(text) {
3173
+ const concepts = CONCEPT_TERMS.filter(([pattern]) => pattern.test(text)).map(([, terms]) => terms).join(" ");
3174
+ return [...new Set(`${concepts} ${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) => [
3175
+ term,
3176
+ ...term.length > 5 && term.endsWith("ing") ? [term.slice(0, -3)] : [],
3177
+ ...term.length > 5 && term.endsWith("ed") ? [term.slice(0, -1), term.slice(0, -2)] : [],
3178
+ ...term.length > 4 && term.endsWith("s") ? [term.slice(0, -1)] : []
3179
+ ]))];
3180
+ }
3181
+
2982
3182
  // src/source-excerpts.ts
2983
3183
  import { readFile as readFile6, realpath, stat as stat3 } from "fs/promises";
2984
3184
  import path8 from "path";
@@ -3085,9 +3285,11 @@ ${item.kind}: ${item.name ?? item.text ?? ""}${location}${item.side ? ` [${item.
3085
3285
  ${item.code}`);
3086
3286
  } else if (item.mode === "signature") rows.push(`Signature only: ${item.signature || "(source unavailable within budget)"}`);
3087
3287
  }
3288
+ if (pack.evidence) rows.push(`
3289
+ ${renderEvidenceSummary(pack.evidence)}`);
3088
3290
  if (pack.gaps.length) rows.push(`
3089
3291
  Gaps: ${pack.gaps.join("; ")}`);
3090
- rows.push(`Budget: ${pack.budget.usedChars} / ${pack.budget.budgetChars} chars${pack.budget.truncated ? " (trimmed low-priority context)" : ""}`);
3292
+ rows.push(`Budget: ${pack.budget.usedChars} / ${pack.budget.budgetChars} chars${pack.budget.truncated ? " (bounded output; see evidence/gaps)" : ""}`);
3091
3293
  return rows.join("\n");
3092
3294
  }
3093
3295
  function account(pack) {
@@ -3099,32 +3301,48 @@ function account(pack) {
3099
3301
  pack.budget.packChars = packChars;
3100
3302
  }
3101
3303
  }
3102
- async function selectContextPack(index, context, budgetChars, maxFiles = 12) {
3304
+ async function selectContextPack(index, context, budgetChars, maxFiles = 12, graph = buildReferenceGraph(index)) {
3103
3305
  const read = sourceReader(index);
3104
- const graph = buildReferenceGraph(index);
3105
3306
  const candidates = [];
3307
+ const selection = /* @__PURE__ */ new Map();
3106
3308
  const seen = /* @__PURE__ */ new Set();
3107
- const gaps = [...context.warnings];
3309
+ const gaps = ["test candidates are static leads, not verified coverage", ...context.warnings];
3108
3310
  const unavailable = /* @__PURE__ */ new Set();
3109
- const source = async (symbol, kind, reason, extra = {}, focus) => {
3311
+ const queryTerms = new Set(context.route.queries.flatMap(contextTerms));
3312
+ const primaryFiles = new Set(context.primarySymbols.map((seed) => seed.symbol.file));
3313
+ const fileSet = new Set(index.files.map((file) => file.path));
3314
+ const source = async (symbol, kind, reason, extra = {}, focus, priority = 50) => {
3110
3315
  const side = extra.side ?? "current";
3111
3316
  const key = `${side}:${extra.ref ?? ""}:${kind}:${symbol.file}:${symbol.line}:${symbol.name}`;
3112
3317
  if (seen.has(key)) return;
3113
3318
  seen.add(key);
3114
3319
  const lines = await read(symbol.file, side, extra.ref);
3115
3320
  if (!lines) unavailable.add(`${side}:${symbol.file}`);
3116
- candidates.push(excerptVariants(lines, symbol, side, extra.ref, focus).map((excerpt) => ({
3117
- ...excerpt,
3118
- ...symbol.id === "" && excerpt.mode === "complete" ? { mode: "window" } : {},
3119
- kind,
3120
- name: symbol.name,
3121
- reason,
3122
- ...extra
3123
- })));
3321
+ if (side === "current" && (focus === void 0 || focus <= symbol.line + 5) && lines && (symbol.kind === "function" || symbol.kind === "method")) {
3322
+ const calls = index.files.find((file) => file.path === symbol.file)?.calls ?? [];
3323
+ const relevant = calls.filter((call) => call.line > symbol.line && call.line <= symbol.endLine).map((call) => {
3324
+ const terms = contextTerms(call.name).filter((term) => queryTerms.has(term)).length;
3325
+ return { line: call.line, terms, score: terms * 4 + (context.primarySymbols.some((seed) => seed.symbol.name === call.name) ? 3 : 0) + (/\b(?:if|return|while)\b/.test(lines[call.line - 1]) ? 2 : 0) };
3326
+ }).filter((call) => call.terms > 0).sort((a, b) => b.score - a.score || a.line - b.line);
3327
+ focus = relevant[0]?.line ?? focus;
3328
+ }
3329
+ candidates.push({
3330
+ priority,
3331
+ range: { start: symbol.line, end: symbol.endLine },
3332
+ protected: kind === "primary" && (symbol.kind === "function" || symbol.kind === "method" || context.route.exactSymbolNames.includes(symbol.name)) || kind === "change" || (kind === "caller" || kind === "callee") && (priority >= 75 || context.route.mentionedFiles.some((file) => symbol.file.toLowerCase() === file.toLowerCase() || symbol.file.toLowerCase().endsWith(`/${file.toLowerCase()}`))) || kind === "test" && priority >= 60,
3333
+ variants: excerptVariants(lines, symbol, side, extra.ref, focus).map((excerpt) => ({
3334
+ ...excerpt,
3335
+ ...symbol.id === "" && excerpt.mode === "complete" ? { mode: "window" } : {},
3336
+ kind,
3337
+ name: symbol.name,
3338
+ reason,
3339
+ ...extra
3340
+ }))
3341
+ });
3124
3342
  };
3125
- for (const primary of context.primarySymbols) {
3343
+ for (const [rank, primary] of context.primarySymbols.entries()) {
3126
3344
  const changed = context.changeContext?.changed.find((row) => row.side === "current" && row.symbol.id === primary.symbol.id);
3127
- await source(primary.symbol, "primary", primary.reason, changed ? { change: changed.change } : {}, changed?.focusLine);
3345
+ await source(primary.symbol, "primary", primary.reason, changed ? { change: changed.change } : {}, changed?.focusLine, rank < 4 ? 100 : 45);
3128
3346
  }
3129
3347
  for (const changed of context.changeContext?.changed ?? []) {
3130
3348
  if (changed.side === "current" && context.primarySymbols.some((seed) => seed.symbol.id === changed.symbol.id)) continue;
@@ -3132,14 +3350,22 @@ async function selectContextPack(index, context, budgetChars, maxFiles = 12) {
3132
3350
  side: changed.side,
3133
3351
  ...changed.side === "base" ? { ref: context.changeContext.baseRef } : {},
3134
3352
  change: changed.change
3135
- }, changed.focusLine);
3353
+ }, changed.focusLine, changed.side === "current" ? 80 : 40);
3136
3354
  }
3137
- for (const test of context.testEvidence) {
3355
+ const stem = (file) => file.split("/").at(-1).replace(/\.(?:spec|test)(?=\.)/i, "").replace(/\.[^.]+$/, "");
3356
+ const seedStems = new Set([...primaryFiles].map(stem));
3357
+ const sortedTests = [...context.testEvidence].sort((a, b) => Number(seedStems.has(stem(b.file))) - Number(seedStems.has(stem(a.file))));
3358
+ for (const [rank, test] of sortedTests.entries()) {
3138
3359
  const file = index.files.find((row) => row.path === test.file);
3139
3360
  const seedNames = new Set(context.primarySymbols.map((seed) => seed.symbol.name));
3361
+ for (const imported of file.importDetails ?? []) if (primaryFiles.has(resolveImport(imported.specifier, file.path, fileSet) ?? "")) {
3362
+ for (const binding of imported.bindings) seedNames.add(binding.local);
3363
+ }
3140
3364
  const call = file.calls?.find((call2) => seedNames.has(call2.name));
3141
3365
  const lines = await read(file.path, "current");
3142
3366
  const symbol = file.symbols.find((symbol2) => call && symbol2.line <= call.line && symbol2.endLine >= call.line);
3367
+ const changedTest = context.changeContext?.changed.some((row) => row.side === "current" && row.symbol.file === test.file);
3368
+ const reason = test.reason === "imports-changed-file" ? "test imports primary seed file" : test.reason === "changed-test" ? changedTest ? "test declaration intersects Git change" : "primary seed is a test file" : test.reason;
3143
3369
  await source(
3144
3370
  symbol ?? {
3145
3371
  id: "",
@@ -3154,9 +3380,10 @@ async function selectContextPack(index, context, budgetChars, maxFiles = 12) {
3154
3380
  exported: false
3155
3381
  },
3156
3382
  "test",
3157
- test.reason === "imports-changed-file" ? "test imports primary seed file" : test.reason,
3158
- { resolution: test.resolution, provenance: test.reason },
3159
- call?.line
3383
+ `candidate test (not verified coverage): ${reason}`,
3384
+ { resolution: test.resolution, provenance: test.reason === "changed-test" && !changedTest ? "primary seed file; not Git change evidence" : test.reason },
3385
+ call?.line,
3386
+ rank < 2 ? 60 : 30
3160
3387
  );
3161
3388
  }
3162
3389
  for (const primary of context.primarySymbols.slice(0, 4)) {
@@ -3171,7 +3398,8 @@ async function selectContextPack(index, context, budgetChars, maxFiles = 12) {
3171
3398
  direction,
3172
3399
  `direct ${direction} of ${primary.symbol.name}`,
3173
3400
  { resolution: edge.resolution, provenance: edge.provenance },
3174
- direction === "caller" ? edge.callSite.line : symbol.line
3401
+ direction === "caller" ? edge.callSite.line : symbol.line,
3402
+ 75
3175
3403
  );
3176
3404
  }
3177
3405
  }
@@ -3197,30 +3425,47 @@ async function selectContextPack(index, context, budgetChars, maxFiles = 12) {
3197
3425
  },
3198
3426
  "import",
3199
3427
  `import binding referenced in primary source ${primary.symbol.file}`,
3200
- { resolution: "import-scoped", provenance: "import statement" }
3428
+ { resolution: "import-scoped", provenance: "import statement" },
3429
+ void 0,
3430
+ 35
3201
3431
  );
3202
3432
  }
3203
3433
  }
3204
- for (const relation of context.relationships) candidates.push([{
3434
+ const mentioned = (file) => context.route.mentionedFiles.some((path11) => file.toLowerCase() === path11.toLowerCase() || file.toLowerCase().endsWith(`/${path11.toLowerCase()}`));
3435
+ const isTest = (file) => /(^|\/)(tests?|__tests__)(\/|$)|\.(?:spec|test)\./i.test(file);
3436
+ const impactSources = (context.changeContext?.impact ?? []).filter((row) => row.via.resolution !== "name-only").sort((a, b) => Number(mentioned(b.symbol.file)) - Number(mentioned(a.symbol.file)) || Number(isTest(a.symbol.file)) - Number(isTest(b.symbol.file)) || a.depth - b.depth);
3437
+ for (const row of impactSources.slice(0, 4)) {
3438
+ if (!context.primarySymbols.some((seed) => seed.symbol.id === row.symbol.id)) {
3439
+ await source(row.symbol, "caller", `candidate transitive caller at depth ${row.depth}; weakest path: ${row.resolution}`, {
3440
+ resolution: row.resolution,
3441
+ provenance: row.via.reason
3442
+ }, row.via.callSite.line, 70);
3443
+ }
3444
+ }
3445
+ for (const relation of context.relationships) candidates.push({ priority: 20, variants: [{
3205
3446
  kind: "relationship",
3206
3447
  text: relation.text,
3207
3448
  reason: "bounded primary graph neighborhood",
3208
3449
  resolution: relation.resolution,
3209
3450
  provenance: relation.provenance
3210
- }]);
3451
+ }] });
3211
3452
  if (!context.primarySymbols.length) for (const file of context.relevantFiles.slice(0, 4)) {
3212
- candidates.push([{ kind: "file", file: file.path, reason: file.reason }]);
3453
+ candidates.push({ priority: 10, variants: [{ kind: "file", file: file.path, reason: file.reason }] });
3213
3454
  }
3214
3455
  if (!context.primarySymbols.length) gaps.push("no strong primary symbol; specify a symbol, path, or failing behavior");
3215
3456
  if (!context.testEvidence.length) gaps.push("no related indexed test found");
3216
3457
  gaps.push(...context.warnings);
3458
+ const initialGaps = [];
3459
+ if (context.warnings.some((warning) => warning.startsWith("index incomplete:"))) initialGaps.push("index incomplete; inspect code_index");
3460
+ if (context.route.includeChangeContext && !context.changeContext) initialGaps.push("change context unavailable; source: change-analysis");
3461
+ if (!initialGaps.length) initialGaps.push("indexed static only");
3217
3462
  const pack = {
3218
3463
  version: 1,
3219
3464
  root: index.root,
3220
3465
  task: context.task.slice(0, 120),
3221
3466
  route: context.route.kind,
3222
3467
  items: [],
3223
- gaps: context.warnings.some((warning) => warning.startsWith("index incomplete:")) ? ["index incomplete; inspect code_index"] : ["budget omitted some evidence"],
3468
+ gaps: initialGaps,
3224
3469
  budget: { budgetChars, usedChars: 0, packChars: 0, truncated: context.task.length > 120 }
3225
3470
  };
3226
3471
  const fits = () => {
@@ -3236,25 +3481,35 @@ async function selectContextPack(index, context, budgetChars, maxFiles = 12) {
3236
3481
  fits();
3237
3482
  }
3238
3483
  if (!fits()) throw new Error("budget too small for repository identity");
3239
- for (const variants of candidates) {
3484
+ let budgetLimited = false;
3485
+ let fileLimited = false;
3486
+ for (const entry of candidates.sort((a, b) => b.priority - a.priority)) {
3487
+ const { variants } = entry;
3240
3488
  let added = false;
3241
3489
  for (const candidate of variants) {
3490
+ if (context.primarySymbols.length > 4 && candidate.code !== void 0 && JSON.stringify(candidate).length > Math.max(600, Math.floor(budgetChars / 8))) {
3491
+ budgetLimited = true;
3492
+ continue;
3493
+ }
3242
3494
  const fragments = candidate.code !== void 0 ? subtractExcerpt(candidate, pack.items.filter((item) => item.code !== void 0)).map((fragment) => ({ ...candidate, ...fragment })) : [candidate];
3243
3495
  if (!fragments.length) continue;
3244
3496
  pack.items.push(...fragments);
3245
3497
  const fileCount = new Set(pack.items.flatMap((item) => item.file ? [item.file] : [])).size;
3246
- if (fits() && fileCount <= maxFiles) {
3498
+ const withinBudget = fits();
3499
+ if (withinBudget && fileCount <= maxFiles) {
3500
+ for (const item of fragments) selection.set(item, entry);
3247
3501
  added = true;
3248
3502
  if (candidate.mode === "signature") pack.budget.truncated = true;
3249
3503
  break;
3250
3504
  }
3505
+ if (!withinBudget) budgetLimited = true;
3506
+ if (fileCount > maxFiles) fileLimited = true;
3251
3507
  pack.items.splice(pack.items.length - fragments.length);
3252
3508
  }
3253
3509
  if (!added) pack.budget.truncated = true;
3254
3510
  }
3255
- if (!pack.budget.truncated) pack.gaps = pack.gaps.filter((gap) => gap.startsWith("index incomplete"));
3256
3511
  for (const item of pack.items) if (item.file && unavailable.has(`${item.side}:${item.file}`)) {
3257
- gaps.push(`source unavailable or changed after indexing: ${item.file} (${item.side})`);
3512
+ gaps.push(`source unavailable (reader rejected): ${item.file} (${item.side})`);
3258
3513
  }
3259
3514
  for (const gap of [...new Set(gaps)].slice(0, 4)) {
3260
3515
  pack.gaps.push(gap.slice(0, 140));
@@ -3263,9 +3518,76 @@ async function selectContextPack(index, context, budgetChars, maxFiles = 12) {
3263
3518
  account(pack);
3264
3519
  while (Math.max(pack.budget.usedChars, pack.budget.packChars) > budgetChars && pack.items.length) {
3265
3520
  pack.items.pop();
3521
+ budgetLimited = true;
3266
3522
  pack.budget.truncated = true;
3267
3523
  account(pack);
3268
3524
  }
3525
+ const summary = () => {
3526
+ const detected = [];
3527
+ if (!context.primarySymbols.length) detected.push({ kind: "not-found", code: "no-primary-symbol", source: "task-selection" });
3528
+ if (!context.testEvidence.length) detected.push({ kind: "not-found", code: "no-test-candidates", source: "indexed-test-discovery" });
3529
+ if (context.route.includeChangeContext && !context.changeContext) detected.push({ kind: "unavailable", code: "change-context-unavailable", source: "change-analysis" });
3530
+ if (context.task.length > 120) detected.push({ kind: "selection-limit", code: "task-summary-shortened", source: "task-summary" });
3531
+ if (fileLimited) detected.push({ kind: "selection-limit", code: "max-files", source: "pack-selection" });
3532
+ if (budgetLimited) detected.push({ kind: "budget", code: "pack-selection-trimmed", source: "text-and-json-budget" });
3533
+ const missingWindowLines = pack.items.some((item) => {
3534
+ const range = selection.get(item)?.range;
3535
+ if (item.mode !== "window" || !range) return false;
3536
+ let next = range.start;
3537
+ const covered = pack.items.filter((other) => other.code !== void 0 && other.file === item.file && other.side === item.side && other.ref === item.ref).sort((a, b) => a.startLine - b.startLine);
3538
+ for (const other of covered) {
3539
+ if (other.startLine > next) break;
3540
+ next = Math.max(next, other.endLine + 1);
3541
+ }
3542
+ return next <= range.end;
3543
+ });
3544
+ if (missingWindowLines) detected.push({ kind: "selection-limit", code: "bounded-source-window", source: "source-excerpts" });
3545
+ for (const item of pack.items) if (item.file && unavailable.has(`${item.side}:${item.file}`)) detected.push({
3546
+ kind: "unavailable",
3547
+ code: "source-reader-rejected",
3548
+ source: "source-reader",
3549
+ file: item.file,
3550
+ side: item.side
3551
+ });
3552
+ const seeds = context.primarySymbols.filter((seed) => pack.items.some((item) => item.kind === "primary" && item.file === seed.symbol.file && item.name === seed.symbol.name && item.side === "current")).map((seed) => seed.symbol);
3553
+ return summarizeEvidence(index, {
3554
+ graph: context.route.includeRelationships ? graph : void 0,
3555
+ seeds,
3556
+ resolutions: pack.items.flatMap((item) => item.resolution && item.kind !== "test" ? [item.resolution] : []),
3557
+ testFiles: pack.items.flatMap((item) => item.kind === "test" && item.file ? [item.file] : []),
3558
+ gaps: detected
3559
+ });
3560
+ };
3561
+ const removable = [...pack.items].filter((item) => !selection.get(item)?.protected).sort((a, b) => (selection.get(a)?.priority ?? 0) - (selection.get(b)?.priority ?? 0));
3562
+ const originalItems = [...pack.items];
3563
+ const originalTruncated = pack.budget.truncated;
3564
+ const originalBudgetLimited = budgetLimited;
3565
+ pack.evidence = summary();
3566
+ if (!fits()) pack.evidence = compactEvidence(summary());
3567
+ while (!fits() && removable.length) {
3568
+ const item = removable.shift();
3569
+ pack.items.splice(pack.items.indexOf(item), 1);
3570
+ budgetLimited = true;
3571
+ pack.budget.truncated = true;
3572
+ pack.evidence = compactEvidence(summary());
3573
+ }
3574
+ if (!fits()) {
3575
+ delete pack.evidence;
3576
+ pack.items = originalItems;
3577
+ pack.budget.truncated = originalTruncated;
3578
+ budgetLimited = originalBudgetLimited;
3579
+ if (pack.items.some((item) => item.kind === "test" && item.reason.includes("not verified coverage"))) {
3580
+ pack.gaps = pack.gaps.filter((gap) => gap !== "test candidates are static leads, not verified coverage");
3581
+ }
3582
+ const note = "budget: summary omitted";
3583
+ pack.gaps.push(note);
3584
+ if (!fits()) pack.gaps.pop();
3585
+ } else {
3586
+ const compact = pack.evidence;
3587
+ pack.evidence = summary();
3588
+ if (!fits()) pack.evidence = compact;
3589
+ }
3590
+ account(pack);
3269
3591
  return pack;
3270
3592
  }
3271
3593
 
@@ -3273,47 +3595,30 @@ async function selectContextPack(index, context, budgetChars, maxFiles = 12) {
3273
3595
  var TEST_PATH_RE2 = /(^|\/)(tests?|__tests__)(\/|$)|\.(?:spec|test)\.(?:[cm]?[jt]sx?|ets)$|(^|\/)(?:test_[^/]+\.py|[^/]+_test\.(?:py|go))$/i;
3274
3596
  var FILE_TOKEN_RE = /[A-Za-z0-9_./\\-]+\.[A-Za-z0-9]+/g;
3275
3597
  var CODE_TOKEN_RE = /[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)?/g;
3276
- var STOP_WORDS = /* @__PURE__ */ new Set(
3277
- [
3278
- "a",
3279
- "an",
3280
- "and",
3281
- "are",
3282
- "be",
3283
- "by",
3284
- "does",
3285
- "for",
3286
- "from",
3287
- "how",
3288
- "in",
3289
- "is",
3290
- "it",
3291
- "my",
3292
- "of",
3293
- "on",
3294
- "or",
3295
- "the",
3296
- "this",
3297
- "to",
3298
- "what",
3299
- "where",
3300
- "why",
3301
- "with",
3302
- "src",
3303
- "lib",
3304
- "test",
3305
- "tests",
3306
- "spec",
3307
- "fix",
3308
- "explain",
3309
- "please",
3310
- "help",
3311
- "current",
3312
- "change",
3313
- "changes",
3314
- "code"
3315
- ]
3316
- );
3598
+ var PROSE_SYMBOL_WORDS = /* @__PURE__ */ new Set([
3599
+ "config",
3600
+ "configuration",
3601
+ "loading",
3602
+ "startup",
3603
+ "file",
3604
+ "files",
3605
+ "path",
3606
+ "directory",
3607
+ "index",
3608
+ "scan",
3609
+ "tool",
3610
+ "flow",
3611
+ "coverage",
3612
+ "error",
3613
+ "errors",
3614
+ "impact",
3615
+ "source",
3616
+ "keep",
3617
+ "other",
3618
+ "behavior",
3619
+ "scope",
3620
+ "root"
3621
+ ]);
3317
3622
  var CHANGE_RE = /\b(fix|bug|broken|breaks?|change|changed|current|working\s+tree|regression|impact|modified|duplicate|fails?|failure)\b/i;
3318
3623
  var TEST_RE3 = /\b(test|tests|spec|specs|failing|assert|coverage)\b/i;
3319
3624
  var ARCH_RE = /\b(architecture|architectural|flow|loading|authentication|authorization|plugin|startup|overview|pipeline|lifecycle|design|works?)\b/i;
@@ -3343,32 +3648,40 @@ function fileMentionMatches(file, mentions) {
3343
3648
  const lower = file.toLowerCase();
3344
3649
  return mentions.some((mention) => {
3345
3650
  const normalized = mention.replace(/\\/g, "/").toLowerCase();
3346
- return lower === normalized || lower.endsWith(`/${normalized}`) || lower.includes(normalized);
3651
+ return lower === normalized || lower.endsWith(`/${normalized}`);
3347
3652
  });
3348
3653
  }
3349
- function contextTerms(text) {
3350
- 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) => [
3351
- term,
3352
- ...term.length > 5 && term.endsWith("ing") ? [term.slice(0, -3)] : [],
3353
- ...term.length > 4 && term.endsWith("s") ? [term.slice(0, -1)] : []
3354
- ]), (term) => term);
3355
- }
3356
3654
  function routeTask(task, index) {
3357
3655
  const text = task.trim();
3358
3656
  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])));
3657
+ const identifiers = text.replace(FILE_TOKEN_RE, (value) => /[\\/]/.test(value) || /\.(?:[cm]?[jt]sx?|ets|pyi?|go|rs|java|c(?:pp|c|xx)?|h(?:pp|xx)?|json|md|ya?ml|toml)$/i.test(value) || index?.files.some((file) => fileMentionMatches(file.path, [value])) ? " " : value).replace(/(?:[A-Za-z]:)?(?:[A-Za-z0-9_.-]+[\\/])+[A-Za-z0-9_.*?\\/-]*/g, " ");
3658
+ const explicitlyNamed = (name2) => {
3659
+ if (!wordBoundary(name2).test(identifiers)) return false;
3660
+ const escaped = name2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3661
+ const syntax = new RegExp("`" + escaped + "`|[\"\u201C\u2018']" + escaped + `["\u201D\u2019']|\\b${escaped}\\s*\\(|(?:\\b(?:function|symbol|method)\\s+|(?:\u51FD\u6570|\u7B26\u53F7|\u65B9\u6CD5)\\s*)${escaped}(?=$|[^A-Za-z0-9_$])|(?:\\b(?:find|locate|lookup)\\s+|(?:\u67E5\u627E|\u5BFB\u627E|\u5B9A\u4F4D)\\s*)${escaped}\\s*(?:[?!.\u3002\uFF1F]|$)|(^|[^A-Za-z0-9_$])${escaped}\\s*(?:\u51FD\u6570|\u7B26\u53F7|\u65B9\u6CD5|function\\b|symbol\\b|method\\b)`, "i").test(identifiers);
3662
+ if (syntax) return true;
3663
+ if (STOP_WORDS.has(name2.toLowerCase())) return false;
3664
+ const words = (identifiers.match(CODE_TOKEN_RE) ?? []).filter((word) => !STOP_WORDS.has(word.toLowerCase()));
3665
+ return !PROSE_SYMBOL_WORDS.has(name2.toLowerCase()) || words.length <= 1;
3666
+ };
3359
3667
  const exactSymbolNames = index ? unique(
3360
3668
  index.files.flatMap((file) => file.symbols.map((symbol) => symbol.name)),
3361
3669
  (name2) => name2
3362
- ).filter((name2) => wordBoundary(name2).test(text)).sort((a, b) => b.length - a.length || a.localeCompare(b)) : [];
3670
+ ).filter(explicitlyNamed).sort((a, b) => b.length - a.length || a.localeCompare(b)) : [];
3363
3671
  const codeTokens = unique(
3364
- (text.match(CODE_TOKEN_RE) ?? []).map((token) => token.replace(/^[^A-Za-z_$]+|[^A-Za-z0-9_$]+$/g, "")).filter((token) => token.length >= 3 && !STOP_WORDS.has(token.toLowerCase())),
3672
+ (identifiers.match(CODE_TOKEN_RE) ?? []).map((token) => token.replace(/^[^A-Za-z_$]+|[^A-Za-z0-9_$]+$/g, "")).filter((token) => token.length >= 3 && !STOP_WORDS.has(token.toLowerCase())),
3365
3673
  (token) => token.toLowerCase()
3366
3674
  );
3367
3675
  const queries = unique(
3368
- [...exactSymbolNames, ...contextTerms(text), ...codeTokens.filter((token) => !exactSymbolNames.includes(token))],
3676
+ [...exactSymbolNames, ...contextTerms(identifiers), ...codeTokens.filter((token) => !exactSymbolNames.includes(token))],
3369
3677
  (value) => value.toLowerCase()
3370
3678
  ).slice(0, 12);
3371
- 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";
3679
+ const intent = identifiers.replace(/(?:不要|无需|不必|禁止)(?:再|实际)?(?:修改代码|修改|编辑|修复|运行测试|测试|提交|发布)/gu, "");
3680
+ const chineseChange = /修复|排查(?:错误|异常|问题)|修改(?:代码|实现|逻辑)|变更|影响(?:范围|分析)?|回归|报错|异常/u.test(intent);
3681
+ const chineseTest = /(?:查找|寻找|定位|补充|分析|运行|验证)(?:相关|候选|单元|集成)?测试|测试覆盖/u.test(intent);
3682
+ const chineseArchitecture = /架构|调用流程|调用链|工作原理|生命周期/u.test(intent);
3683
+ const chineseExplore = /查找|定位|实现|理解|追踪|分析原因|解释原因/u.test(intent);
3684
+ const kind = chineseChange ? "change" : chineseTest ? "test" : TEST_RE3.test(intent) || mentionedFiles.some(isTestFile) ? "test" : CHANGE_RE.test(intent) ? "change" : exactSymbolNames.length > 0 ? "symbol" : chineseArchitecture ? "architecture" : EXPLORE_RE.test(intent) || chineseExplore ? "exploration" : ARCH_RE.test(intent) ? "architecture" : "ambiguous";
3372
3685
  const includeChangeContext = kind === "change" || kind === "test";
3373
3686
  const includeRelationships = kind !== "ambiguous";
3374
3687
  const sources = /* @__PURE__ */ new Set();
@@ -3391,10 +3704,15 @@ function routeTask(task, index) {
3391
3704
  };
3392
3705
  }
3393
3706
  function routeScore(route, hit) {
3394
- let score = hit.score * 30;
3395
- if (route.exactSymbolNames.includes(hit.name)) score += 100;
3707
+ const queryTerms = new Set(route.queries.flatMap(contextTerms));
3708
+ const names = contextTerms(hit.name).filter((term) => queryTerms.has(term));
3709
+ const signature = contextTerms(hit.signature).filter((term) => queryTerms.has(term) && !names.includes(term));
3710
+ const path11 = contextTerms(hit.file).filter((term) => queryTerms.has(term) && !names.includes(term) && !signature.includes(term));
3711
+ let score = names.length * 24 + signature.length * 4 + path11.length * 2 + hit.score * 5;
3712
+ if (route.exactSymbolNames.includes(hit.name)) score += 200;
3396
3713
  if (fileMentionMatches(hit.file, route.mentionedFiles)) score += 30;
3397
- if (isTestFile(hit.file) && route.kind !== "test") score -= 10;
3714
+ if (hit.kind === "function" || hit.kind === "method") score += 30;
3715
+ if (isTestFile(hit.file) && !fileMentionMatches(hit.file, route.mentionedFiles) && !route.exactSymbolNames.includes(hit.name)) score -= 45;
3398
3716
  return score;
3399
3717
  }
3400
3718
  function gatherPrimarySymbols(index, route, maxSymbols, changedIds, changedFiles, refs) {
@@ -3416,9 +3734,10 @@ function gatherPrimarySymbols(index, route, maxSymbols, changedIds, changedFiles
3416
3734
  const overlap = contextTerms(`${hit.name} ${hit.signature} ${hit.file}`).filter((term) => queryTerms.has(term)).length;
3417
3735
  if (!exact && route.kind === "ambiguous" && queryTerms.size > 1 && overlap < 2) continue;
3418
3736
  let score = routeScore(route, hit);
3419
- if (changedIds.has(hit.id)) score += 80;
3420
- else if (changedFiles.has(hit.file)) score += 55;
3421
- if (route.kind === "test" && isTestFile(hit.file)) score += 35;
3737
+ const changeRelevant = !isTestFile(hit.file) || route.kind === "test" || exact || fileMentionMatches(hit.file, route.mentionedFiles);
3738
+ if (changeRelevant && changedIds.has(hit.id)) score += 60;
3739
+ else if (changeRelevant && changedFiles.has(hit.file)) score += 8;
3740
+ if (route.kind === "test" && isTestFile(hit.file)) score += 15;
3422
3741
  const candidate = {
3423
3742
  symbol: hit,
3424
3743
  score,
@@ -3444,7 +3763,7 @@ function gatherPrimarySymbols(index, route, maxSymbols, changedIds, changedFiles
3444
3763
  if (byId.has(symbol.id)) continue;
3445
3764
  byId.set(symbol.id, {
3446
3765
  symbol,
3447
- score: 70,
3766
+ score: 72,
3448
3767
  provenance: "lexical",
3449
3768
  reason: "declaration intersects current change"
3450
3769
  });
@@ -3453,7 +3772,7 @@ function gatherPrimarySymbols(index, route, maxSymbols, changedIds, changedFiles
3453
3772
  }
3454
3773
  const definitionMatch = (candidate) => contextTerms(`${candidate.symbol.name} ${candidate.symbol.signature}`).some((term) => queryTerms.has(term));
3455
3774
  const hasDefinitionMatch = [...byId.values()].some(definitionMatch);
3456
- return [...byId.values()].filter((candidate) => !hasDefinitionMatch || definitionMatch(candidate) || candidate.reason === "declaration intersects current change" || fileMentionMatches(candidate.symbol.file, route.mentionedFiles)).sort(
3775
+ return [...byId.values()].filter((candidate) => candidate.provenance === "exact" || candidate.score >= 20).filter((candidate) => !hasDefinitionMatch || definitionMatch(candidate) || candidate.reason === "declaration intersects current change" || fileMentionMatches(candidate.symbol.file, route.mentionedFiles)).sort(
3457
3776
  (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)
3458
3777
  ).slice(0, maxSymbols);
3459
3778
  }
@@ -3543,7 +3862,7 @@ async function buildTaskContext(index, task, options = {}) {
3543
3862
  kind: "git",
3544
3863
  root: index.root,
3545
3864
  baseRef: "HEAD"
3546
- });
3865
+ }, { focusFiles: index.files.filter((file) => fileMentionMatches(file.path, route.mentionedFiles)).map((file) => file.path) });
3547
3866
  warnings.push(...changeContext.warnings);
3548
3867
  } catch (error) {
3549
3868
  warnings.push(`change context unavailable: ${error.message ?? String(error)}`);
@@ -3563,10 +3882,18 @@ async function buildTaskContext(index, task, options = {}) {
3563
3882
  if (changeContext && taskFiles.size) {
3564
3883
  const changed = changeContext.changed.filter((row) => taskFiles.has(row.symbol.file));
3565
3884
  const changedIds2 = new Set(changed.map((row) => row.symbol.id));
3885
+ const directCallers = changeContext.directCallers.filter((row) => changedIds2.has(row.via.toId));
3886
+ const reachable = /* @__PURE__ */ new Set([...changedIds2, ...directCallers.map((row) => row.symbol.id)]);
3887
+ const impact = [...changeContext.impact].sort((a, b) => a.depth - b.depth).filter((row) => {
3888
+ if (!reachable.has(row.via.toId)) return false;
3889
+ reachable.add(row.symbol.id);
3890
+ return true;
3891
+ });
3566
3892
  changeContext = {
3567
3893
  ...changeContext,
3568
3894
  changed,
3569
- directCallers: changeContext.directCallers.filter((row) => changedIds2.has(row.via.toId)),
3895
+ directCallers,
3896
+ impact,
3570
3897
  tests: collectSeedTests(index, changed.map((row) => row.symbol)),
3571
3898
  importDependents: changeContext.importDependents.filter((row) => taskFiles.has(row.changedFile)),
3572
3899
  paths: changeContext.paths.filter((row) => changedIds2.has(row.changed.id))
@@ -3610,11 +3937,12 @@ async function buildTaskContext(index, task, options = {}) {
3610
3937
  confidence,
3611
3938
  warnings
3612
3939
  };
3613
- const pack = await selectContextPack(index, partial, budgetChars, maxFiles);
3940
+ const pack = await selectContextPack(index, partial, budgetChars, maxFiles, graph ?? void 0);
3614
3941
  const selected = (file, name2, kind) => pack.items.some((item) => item.file === file && (!name2 || item.name === name2) && (!kind || item.kind === kind));
3615
3942
  const selectedRelationships = relationships.filter((row) => pack.items.some((item) => item.text === row.text));
3616
3943
  const selectedChange = changeContext ? {
3617
3944
  ...changeContext,
3945
+ evidence: pack.evidence,
3618
3946
  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)),
3619
3947
  unmapped: [],
3620
3948
  directCallers: [],
@@ -3910,6 +4238,10 @@ async function buildImpactXray(index, result) {
3910
4238
  } }
3911
4239
  };
3912
4240
  const read = sourceReader(index);
4241
+ if (result.evidence) {
4242
+ view.evidence = result.evidence;
4243
+ view.gaps.push(...renderEvidenceSummary(result.evidence).split("\n"));
4244
+ }
3913
4245
  const files = new Map(index.files.map((f) => [f.path, f]));
3914
4246
  const symbols = new Map(index.files.flatMap((f) => f.symbols.map((s) => [s.id, s])));
3915
4247
  const changedFiles = new Set(result.changed.map((r) => r.symbol.file).concat(result.unmapped.map((r) => r.file)));
@@ -4369,7 +4701,7 @@ var tools = [
4369
4701
  }
4370
4702
  if (direction === "callers" || direction === "both") {
4371
4703
  lines.push("", `callers (${refs.callers.length}):`);
4372
- if (refs.callers.length === 0) lines.push(" (none)");
4704
+ if (refs.callers.length === 0) lines.push(" (none found in current index/static resolution; not proof of no callers)");
4373
4705
  for (const caller of refs.callers) {
4374
4706
  lines.push(` ${caller.file}:${caller.line}${caller.from ? ` in ${caller.from}` : ""}`);
4375
4707
  }
@@ -4380,7 +4712,7 @@ var tools = [
4380
4712
  for (const callee of refs.callees) {
4381
4713
  const target = callee.targets[0];
4382
4714
  lines.push(
4383
- ` ${callee.name} :${callee.line}${target ? ` \u2192 ${target.file}:${target.line}` : " (external)"}`
4715
+ ` ${callee.name} :${callee.line}${target ? ` \u2192 ${target.file}:${target.line}` : " (unresolved in current index; external not established)"}`
4384
4716
  );
4385
4717
  }
4386
4718
  }
@@ -4754,6 +5086,7 @@ export {
4754
5086
  readWorkingTreeDiff,
4755
5087
  renderChangeContext,
4756
5088
  renderContextPack,
5089
+ renderEvidenceSummary,
4757
5090
  renderHit,
4758
5091
  renderRepoMap,
4759
5092
  renderTaskContext,