dsh-code-index 0.10.1 → 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
@@ -992,6 +992,7 @@ function signatureFor(id, node) {
992
992
  }
993
993
  function rawSignature(id, node) {
994
994
  const name2 = nameOf(id, node);
995
+ if (id === "rust" && ["struct_item", "enum_item", "trait_item"].includes(node.type)) return name2;
995
996
  if (id === "arkts" && node.type === "struct_declaration") return `struct ${name2}`;
996
997
  if (id === "arkts" && node.type === "public_field_definition") {
997
998
  return `${name2}${collapseSpace(node.childForFieldName("type")?.text ?? "")}`;
@@ -1249,7 +1250,7 @@ import path3 from "path";
1249
1250
  import { createHash, randomUUID } from "crypto";
1250
1251
 
1251
1252
  // src/types.ts
1252
- var REPO_INDEX_SCHEMA_VERSION = 3;
1253
+ var REPO_INDEX_SCHEMA_VERSION = 4;
1253
1254
  function symbolCount(index) {
1254
1255
  return index.files.reduce((n, f) => n + f.symbols.length, 0);
1255
1256
  }
@@ -1302,6 +1303,7 @@ async function saveIndex(cachePath, index) {
1302
1303
 
1303
1304
  // src/buildIndex.ts
1304
1305
  async function buildIndex(root, options = {}, previous = null, forcePaths = /* @__PURE__ */ new Set()) {
1306
+ if (previous?.schemaVersion !== REPO_INDEX_SCHEMA_VERSION) previous = null;
1305
1307
  const scanned = await scanRepo(root, options);
1306
1308
  const prevByPath = new Map((previous?.files ?? []).map((f) => [f.path, f]));
1307
1309
  const prevMtime = new Map((previous?.files ?? []).map((f) => [f.path, f.mtimeMs]));
@@ -3879,6 +3881,229 @@ var RepoContextManager = class {
3879
3881
  }
3880
3882
  };
3881
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
+
3882
4107
  // src/tools.ts
3883
4108
  var activeRepoContexts = null;
3884
4109
  function activateRepoContextManager(manager) {
@@ -4169,6 +4394,11 @@ var tools = [
4169
4394
  name: "code_change_context",
4170
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.",
4171
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
+ },
4172
4402
  repoRoot: {
4173
4403
  type: "string",
4174
4404
  description: "Optional absolute repo path; defaults to the session workspace root."
@@ -4209,8 +4439,9 @@ var tools = [
4209
4439
  }
4210
4440
  },
4211
4441
  output: {
4212
- schema: { type: "string" },
4213
- 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
4214
4445
  },
4215
4446
  presentCall: (args) => ({
4216
4447
  card: "generic",
@@ -4234,6 +4465,15 @@ var tools = [
4234
4465
  maxPaths: args.maxPaths,
4235
4466
  maxTests: args.maxTests
4236
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
+ }
4237
4477
  return renderChangeContext(result);
4238
4478
  } catch (error) {
4239
4479
  return `code_change_context: ${error.message ?? String(error)}`;
@@ -4485,8 +4725,10 @@ export {
4485
4725
  Config,
4486
4726
  DEFAULT_EXCLUDED_DIRS,
4487
4727
  SUPPORTED_EXTS,
4728
+ XRAY_LIMITS,
4488
4729
  apply,
4489
4730
  buildChangeContext,
4731
+ buildImpactXray,
4490
4732
  buildIndex,
4491
4733
  buildIndexWithCache,
4492
4734
  buildModuleGraph,