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/CHANGELOG.md +22 -2
- package/README.md +65 -11
- package/README.zh.md +65 -11
- package/dist/client.js +641 -46
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +91 -3
- package/dist/index.js +245 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/impact-xray-benchmark.mjs +30 -0
- package/scripts/impact-xray-demo.mjs +23 -0
- package/scripts/impact-xray-poc.mjs +27 -0
- package/scripts/impact-xray-rc-benchmark.mjs +77 -0
- package/scripts/process-impact-xray-demo.mjs +29 -0
- package/scripts/release-smoke.mjs +62 -1
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 =
|
|
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,
|