dsh-code-index 0.10.2 → 0.11.1
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 +21 -1
- package/README.md +62 -10
- package/README.zh.md +64 -10
- package/dist/client.js +641 -46
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +89 -1
- package/dist/index.js +242 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- 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 +24 -2
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,
|