dsh-code-index 0.10.2 → 0.11.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
@@ -3881,6 +3881,229 @@ var RepoContextManager = class {
3881
3881
  }
3882
3882
  };
3883
3883
 
3884
+ // src/impact-xray-model.ts
3885
+ var XRAY_LIMITS = { nodes: 32, edges: 48, paths: 12, evidenceChars: 900, metadataBytes: 96e3 };
3886
+
3887
+ // src/impact-xray.ts
3888
+ async function buildImpactXray(index, result) {
3889
+ const view = {
3890
+ card: "impact-xray",
3891
+ version: 2,
3892
+ root: result.root,
3893
+ source: result.source,
3894
+ nodes: [],
3895
+ edges: [],
3896
+ tests: [],
3897
+ imports: [],
3898
+ paths: [],
3899
+ unmapped: [],
3900
+ gaps: result.warnings.slice(0, 12),
3901
+ truncated: false,
3902
+ bounds: { analysis: { ...result.truncated }, projection: false, totals: {
3903
+ changed: result.changed.length,
3904
+ callers: result.directCallers.length,
3905
+ impact: result.impact.length,
3906
+ imports: result.importDependents.length,
3907
+ tests: result.tests.length,
3908
+ paths: result.paths.length,
3909
+ unmapped: result.unmapped.length
3910
+ } }
3911
+ };
3912
+ const read = sourceReader(index);
3913
+ const files = new Map(index.files.map((f) => [f.path, f]));
3914
+ const symbols = new Map(index.files.flatMap((f) => f.symbols.map((s) => [s.id, s])));
3915
+ const changedFiles = new Set(result.changed.map((r) => r.symbol.file).concat(result.unmapped.map((r) => r.file)));
3916
+ const impactFiles = new Set([...result.directCallers, ...result.impact].map((r) => r.symbol.file));
3917
+ const fileSet = /* @__PURE__ */ new Set([...files.keys(), ...changedFiles]);
3918
+ const testFiles = new Set(result.tests.map((t) => t.file));
3919
+ const changed = new Set(result.changed.filter((r) => r.side === "current").map((r) => r.symbol.id));
3920
+ const depths = new Map([...result.directCallers, ...result.impact].map((r) => [r.symbol.id, r.depth]));
3921
+ const nodes = /* @__PURE__ */ new Map();
3922
+ const clip = () => {
3923
+ view.bounds.projection = true;
3924
+ };
3925
+ const evidence = async (file, line, type, side = "current") => {
3926
+ const ref = side === "base" ? result.baseRef : void 0;
3927
+ const entry = { file, line, startLine: Math.max(1, line - 2), side, type, ...ref ? { ref } : {} };
3928
+ const lines = await read(file, side, ref);
3929
+ if (!lines) return { ...entry, gap: "source-unavailable" };
3930
+ if (line < 1 || line > lines.length) return { ...entry, gap: "line-unavailable" };
3931
+ const code = lines.slice(entry.startLine - 1, line + 3).join("\n");
3932
+ return code.length <= XRAY_LIMITS.evidenceChars ? { ...entry, code } : { ...entry, gap: "source-budget" };
3933
+ };
3934
+ const addNode = async (s, reason, side = "current", extra = {}) => {
3935
+ const id = `${side}:${s.id}`;
3936
+ if (nodes.has(id)) return id;
3937
+ if (view.nodes.length >= XRAY_LIMITS.nodes) {
3938
+ clip();
3939
+ return null;
3940
+ }
3941
+ const isChanged = side === "base" || changed.has(s.id);
3942
+ const node = {
3943
+ id,
3944
+ name: s.name,
3945
+ kind: s.kind,
3946
+ file: s.file,
3947
+ line: s.line,
3948
+ changed: isChanged,
3949
+ category: isChanged ? "change" : testFiles.has(s.file) ? "test" : (depths.get(s.id) ?? 1) > 1 ? "impact" : "caller",
3950
+ depth: isChanged ? 0 : depths.get(s.id) ?? 1,
3951
+ reason,
3952
+ evidence: await evidence(s.file, s.line, "declaration", side),
3953
+ ...extra
3954
+ };
3955
+ nodes.set(id, node);
3956
+ view.nodes.push(node);
3957
+ return id;
3958
+ };
3959
+ for (const row of result.changed.slice(0, 12)) await addNode(
3960
+ row.symbol,
3961
+ result.source === "files" || result.source === "symbols" ? "explicit-selection" : "diff-mapping",
3962
+ row.side,
3963
+ { change: row.change }
3964
+ );
3965
+ if (result.changed.length > 12) clip();
3966
+ const hopKeys = /* @__PURE__ */ new Map();
3967
+ const keyOf = (hop) => JSON.stringify([hop.fromId, hop.toId, hop.callSite, hop.resolution, hop.reason]);
3968
+ const hops = [...result.directCallers, ...result.impact].map((r) => r.via).concat(result.paths.flatMap((r) => r.hops));
3969
+ for (const hop of hops) {
3970
+ const key = keyOf(hop);
3971
+ if (hopKeys.has(key)) continue;
3972
+ if (view.edges.length >= 32) {
3973
+ clip();
3974
+ continue;
3975
+ }
3976
+ const from = hop.fromId === null ? void 0 : symbols.get(hop.fromId), to = symbols.get(hop.toId);
3977
+ if (!from || !to) {
3978
+ view.gaps.push("endpoint-unavailable");
3979
+ continue;
3980
+ }
3981
+ const missing = new Set([from.id, to.id].filter((id) => !nodes.has(`current:${id}`))).size;
3982
+ if (view.nodes.length + missing > 24) {
3983
+ clip();
3984
+ continue;
3985
+ }
3986
+ const fromId = await addNode(from, hop.reason), toId = await addNode(to, hop.reason);
3987
+ if (fromId && toId) {
3988
+ const id = `call:${view.edges.length}`;
3989
+ view.edges.push({
3990
+ id,
3991
+ from: fromId,
3992
+ to: toId,
3993
+ kind: "call",
3994
+ resolution: hop.resolution,
3995
+ reason: hop.reason,
3996
+ evidence: await evidence(hop.callSite.file, hop.callSite.line, "call-site")
3997
+ });
3998
+ hopKeys.set(key, id);
3999
+ }
4000
+ }
4001
+ for (const p of result.paths.slice(0, XRAY_LIMITS.paths)) {
4002
+ const edgeIds = p.hops.map((h) => hopKeys.get(keyOf(h))).filter((id) => id !== void 0);
4003
+ view.paths.push({
4004
+ id: `path:${view.paths.length}`,
4005
+ from: p.entry.name,
4006
+ to: p.changed.name,
4007
+ edgeIds,
4008
+ resolution: p.resolution,
4009
+ omitted: edgeIds.length !== p.hops.length
4010
+ });
4011
+ }
4012
+ if (result.paths.length > XRAY_LIMITS.paths) clip();
4013
+ const fileNode = async (file, line = 1, reason = "module", supplied) => {
4014
+ const side = files.has(file) ? "current" : "base";
4015
+ const id = `file:${side}:${file}`;
4016
+ if (nodes.has(id)) return id;
4017
+ if (view.nodes.length >= XRAY_LIMITS.nodes) {
4018
+ clip();
4019
+ return null;
4020
+ }
4021
+ const n = {
4022
+ id,
4023
+ name: file,
4024
+ kind: "module",
4025
+ file,
4026
+ line,
4027
+ changed: changedFiles.has(file),
4028
+ category: testFiles.has(file) ? "test" : "module",
4029
+ reason,
4030
+ evidence: supplied ?? await evidence(file, line, "declaration", side)
4031
+ };
4032
+ nodes.set(id, n);
4033
+ view.nodes.push(n);
4034
+ return id;
4035
+ };
4036
+ for (const row of result.importDependents.slice(0, 8)) {
4037
+ const site = files.get(row.file)?.importDetails?.find((i) => i.specifier === row.specifier);
4038
+ const from = await fileNode(row.file, site?.line ?? 1, "import-dependent");
4039
+ const to = await fileNode(row.changedFile, 1, "changed-module");
4040
+ let edgeId;
4041
+ if (site && from && to && view.edges.length < XRAY_LIMITS.edges) {
4042
+ edgeId = `import:${view.edges.length}`;
4043
+ view.edges.push({
4044
+ id: edgeId,
4045
+ from,
4046
+ to,
4047
+ kind: "import",
4048
+ resolution: row.resolution,
4049
+ reason: "imports-changed-file",
4050
+ evidence: await evidence(row.file, site.line, "import-site")
4051
+ });
4052
+ } else view.gaps.push("import-site-unavailable");
4053
+ view.imports.push({ ...row, ...edgeId ? { edgeId } : {} });
4054
+ }
4055
+ for (const row of result.tests.slice(0, 8)) {
4056
+ const file = files.get(row.file);
4057
+ const targets = row.reason === "imports-changed-file" ? changedFiles : row.reason === "imports-impact-file" ? impactFiles : /* @__PURE__ */ new Set();
4058
+ const site = file?.importDetails?.find((i) => {
4059
+ const target = resolveImport(i.specifier, row.file, fileSet);
4060
+ return target !== null && targets.has(target);
4061
+ });
4062
+ const call = row.reason === "references-symbol-name" ? file?.calls?.find((c) => result.changed.some((s) => s.symbol.name === c.name)) : void 0;
4063
+ const line = site?.line ?? call?.line ?? 1;
4064
+ const e = await evidence(row.file, line, site ? "import-site" : "test-lead");
4065
+ const nodeId = await fileNode(row.file, line, row.reason, e);
4066
+ if (nodeId) {
4067
+ const n = nodes.get(nodeId);
4068
+ n.reason = row.reason;
4069
+ n.resolution = row.resolution;
4070
+ n.evidence = e;
4071
+ n.line = line;
4072
+ }
4073
+ view.tests.push({ ...row, ...nodeId ? { nodeId } : {} });
4074
+ }
4075
+ if (result.importDependents.length > 8 || result.tests.length > 8) clip();
4076
+ for (const row of result.unmapped.slice(0, 8)) view.unmapped.push({
4077
+ ...row,
4078
+ ranges: row.ranges.slice(0, 16),
4079
+ evidence: await evidence(row.file, row.ranges[0]?.startLine ?? 1, "unmapped-change", row.side)
4080
+ });
4081
+ if (result.unmapped.length > 8 || result.unmapped.some((r) => r.ranges.length > 16)) clip();
4082
+ view.gaps = [...new Set(view.gaps)].slice(0, 20);
4083
+ view.truncated = view.bounds.projection || Object.values(result.truncated).some(Boolean);
4084
+ if (Buffer.byteLength(JSON.stringify(view)) > XRAY_LIMITS.metadataBytes) {
4085
+ for (const e of [...view.nodes.map((n) => n.evidence), ...view.edges.map((e2) => e2.evidence), ...view.unmapped.map((u) => u.evidence)]) {
4086
+ delete e.code;
4087
+ e.gap = "source-budget";
4088
+ }
4089
+ clip();
4090
+ view.truncated = true;
4091
+ if (Buffer.byteLength(JSON.stringify(view)) > XRAY_LIMITS.metadataBytes) return {
4092
+ card: "impact-xray",
4093
+ version: 2,
4094
+ root: "",
4095
+ source: result.source,
4096
+ nodes: [],
4097
+ edges: [],
4098
+ tests: [],
4099
+ imports: [],
4100
+ gaps: ["metadata-budget"],
4101
+ truncated: true
4102
+ };
4103
+ }
4104
+ return view;
4105
+ }
4106
+
3884
4107
  // src/tools.ts
3885
4108
  var activeRepoContexts = null;
3886
4109
  function activateRepoContextManager(manager) {
@@ -4171,6 +4394,11 @@ var tools = [
4171
4394
  name: "code_change_context",
4172
4395
  description: "Return the smallest change-aware context for the current diff: changed symbols, direct callers, import dependents, shortest paths to entry points, transitive impact, and likely affected tests. Every relation is labeled exact / import-scoped / name-only, so confidence is explicit. Select the change with an explicit diff, a list of files, a list of symbol ids, or (default) the working tree against baseRef.",
4173
4396
  parameters: {
4397
+ outputFormat: {
4398
+ type: "string",
4399
+ enum: ["text", "xray"],
4400
+ description: "Default text preserves the canonical string. xray requests the experimental bounded Impact X-Ray evidence snapshot."
4401
+ },
4174
4402
  repoRoot: {
4175
4403
  type: "string",
4176
4404
  description: "Optional absolute repo path; defaults to the session workspace root."
@@ -4211,8 +4439,9 @@ var tools = [
4211
4439
  }
4212
4440
  },
4213
4441
  output: {
4214
- schema: { type: "string" },
4215
- render: (_args, value) => [{ type: "text", text: value }]
4442
+ schema: { oneOf: [{ type: "string" }, { type: "object", additionalProperties: true }] },
4443
+ render: (_args, value) => [{ type: "text", text: typeof value === "string" ? value : String(value.text) }],
4444
+ presentationMeta: (_args, value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value.xray ?? null : null
4216
4445
  },
4217
4446
  presentCall: (args) => ({
4218
4447
  card: "generic",
@@ -4236,6 +4465,15 @@ var tools = [
4236
4465
  maxPaths: args.maxPaths,
4237
4466
  maxTests: args.maxTests
4238
4467
  });
4468
+ if (args.outputFormat === "xray") {
4469
+ const xray = await buildImpactXray(index, result);
4470
+ let text = renderChangeContext(result);
4471
+ if (text.length > 24e3) {
4472
+ text = text.slice(0, 23800).split("\n").slice(0, -1).join("\n") + "\n[X-Ray text budget reached; default text output remains available.]";
4473
+ if (xray.bounds) xray.bounds.text = true;
4474
+ }
4475
+ return { text, xray: JSON.parse(JSON.stringify(xray)) };
4476
+ }
4239
4477
  return renderChangeContext(result);
4240
4478
  } catch (error) {
4241
4479
  return `code_change_context: ${error.message ?? String(error)}`;
@@ -4487,8 +4725,10 @@ export {
4487
4725
  Config,
4488
4726
  DEFAULT_EXCLUDED_DIRS,
4489
4727
  SUPPORTED_EXTS,
4728
+ XRAY_LIMITS,
4490
4729
  apply,
4491
4730
  buildChangeContext,
4731
+ buildImpactXray,
4492
4732
  buildIndex,
4493
4733
  buildIndexWithCache,
4494
4734
  buildModuleGraph,