ast-mcp-server 0.9.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/CHANGELOG.md +33 -0
- package/README.md +46 -14
- package/SECURITY.md +1 -1
- package/dist/batch/schema.js +2 -0
- package/dist/cli.js +67 -1
- package/dist/server.js +2 -0
- package/dist/services/agent-setup.js +11 -5
- package/dist/services/agent-targets.js +1 -0
- package/dist/services/call-spines.js +101 -0
- package/dist/services/cli-project-discovery.js +97 -0
- package/dist/services/context-builder.js +83 -24
- package/dist/services/doctor-continuation.js +37 -0
- package/dist/services/doctor-model.js +77 -0
- package/dist/services/doctor.js +111 -0
- package/dist/services/explore-presentation.js +160 -0
- package/dist/services/managed-bundle.js +145 -0
- package/dist/services/managed-file.js +37 -0
- package/dist/services/project.js +103 -24
- package/dist/services/public-errors.js +7 -1
- package/dist/services/relationships.js +104 -0
- package/dist/services/skill-installer.js +74 -56
- package/dist/services/test-candidates.js +15 -8
- package/dist/services/upgrade-command.js +77 -0
- package/dist/services/upgrade-reconcile.js +41 -0
- package/dist/services/upgrade-runtime.js +157 -0
- package/dist/services/upgrade.js +232 -0
- package/dist/tools/explore.js +73 -2
- package/dist/tools/find_test_candidates.js +124 -0
- package/dist/tools/get_impact.js +3 -30
- package/dist/tools/relationship-schema.js +31 -0
- package/docs/support.md +13 -13
- package/package.json +2 -2
- package/skills/structural-code-editing/SKILL.md +30 -158
- package/skills/structural-code-editing/references/operations.md +53 -0
- package/skills/structural-code-editing/releases.json +22 -3
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
const PACKAGE = "ast-mcp-server";
|
|
3
|
+
const CONTINUATION = "After reinstalling through its owning toolchain, run: ast-tool upgrade --check";
|
|
4
|
+
export class UpgradeError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
continuation;
|
|
7
|
+
constructor(code, message, continuation = CONTINUATION) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.continuation = continuation;
|
|
11
|
+
this.name = "UpgradeError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function fail(code, message) {
|
|
15
|
+
throw new UpgradeError(code, message);
|
|
16
|
+
}
|
|
17
|
+
function paths(platform) {
|
|
18
|
+
return platform === "win32" ? path.win32 : path.posix;
|
|
19
|
+
}
|
|
20
|
+
function same(a, b, platform) {
|
|
21
|
+
return platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
22
|
+
}
|
|
23
|
+
function inside(root, target, platform) {
|
|
24
|
+
const p = paths(platform);
|
|
25
|
+
const relative = p.relative(root, target);
|
|
26
|
+
return relative !== ".." && !relative.startsWith(`..${p.sep}`) && !p.isAbsolute(relative);
|
|
27
|
+
}
|
|
28
|
+
async function canonical(runtime, value) {
|
|
29
|
+
return runtime
|
|
30
|
+
.realpath(value)
|
|
31
|
+
.catch(() => fail("UPGRADE_PROVENANCE_UNSUPPORTED", "Package ownership could not be proven."));
|
|
32
|
+
}
|
|
33
|
+
function packageFields(record, platform) {
|
|
34
|
+
const bins = record.bin;
|
|
35
|
+
if (record.name !== PACKAGE ||
|
|
36
|
+
typeof record.version !== "string" ||
|
|
37
|
+
!record.version ||
|
|
38
|
+
!bins ||
|
|
39
|
+
typeof bins["ast-tool"] !== "string" ||
|
|
40
|
+
typeof bins["ast-mcp-server"] !== "string") {
|
|
41
|
+
fail("UPGRADE_PROVENANCE_UNSUPPORTED", "Package identity or bins are unsupported.");
|
|
42
|
+
}
|
|
43
|
+
const p = paths(platform);
|
|
44
|
+
for (const value of [bins["ast-tool"], bins["ast-mcp-server"]]) {
|
|
45
|
+
if (p.isAbsolute(value) || p.normalize(value).startsWith(`..${p.sep}`)) {
|
|
46
|
+
fail("UPGRADE_PROVENANCE_UNSUPPORTED", "Package executable location is unsafe.");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { version: record.version, cli: bins["ast-tool"], server: bins["ast-mcp-server"] };
|
|
50
|
+
}
|
|
51
|
+
async function readPackage(runtime, root) {
|
|
52
|
+
try {
|
|
53
|
+
return packageFields(await runtime.readPackage(root), runtime.platform);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error instanceof UpgradeError)
|
|
57
|
+
throw error;
|
|
58
|
+
fail("UPGRADE_PROVENANCE_UNSUPPORTED", "Package metadata could not be verified.");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function proveNpm(runtime, packageRoot) {
|
|
62
|
+
if (!runtime.npmCliExecutable)
|
|
63
|
+
return;
|
|
64
|
+
const p = paths(runtime.platform);
|
|
65
|
+
try {
|
|
66
|
+
const node = await canonical(runtime, runtime.nodeExecutable);
|
|
67
|
+
const npmCli = await canonical(runtime, runtime.npmCliExecutable);
|
|
68
|
+
const toolchainRoot = runtime.platform === "win32" ? p.dirname(node) : p.resolve(p.dirname(node), "..");
|
|
69
|
+
if (p.basename(npmCli).toLowerCase() !== "npm-cli.js" ||
|
|
70
|
+
!inside(toolchainRoot, npmCli, runtime.platform))
|
|
71
|
+
return;
|
|
72
|
+
const [rootText, prefixText] = await Promise.all([
|
|
73
|
+
runtime.run(runtime.nodeExecutable, [npmCli, "root", "--global"]),
|
|
74
|
+
runtime.run(runtime.nodeExecutable, [npmCli, "prefix", "--global"]),
|
|
75
|
+
]);
|
|
76
|
+
const lexicalRoot = p.resolve(rootText.trim());
|
|
77
|
+
const lexicalPackage = p.join(lexicalRoot, PACKAGE);
|
|
78
|
+
if (!p.isAbsolute(rootText.trim()) || !(await runtime.directDirectory(lexicalPackage)))
|
|
79
|
+
return;
|
|
80
|
+
const root = await canonical(runtime, lexicalRoot);
|
|
81
|
+
const prefix = await canonical(runtime, prefixText.trim());
|
|
82
|
+
const expectedRoot = await canonical(runtime, runtime.platform === "win32"
|
|
83
|
+
? p.join(prefix, "node_modules")
|
|
84
|
+
: p.join(prefix, "lib", "node_modules"));
|
|
85
|
+
const expectedPackage = await canonical(runtime, lexicalPackage);
|
|
86
|
+
if (!same(root, expectedRoot, runtime.platform) ||
|
|
87
|
+
!same(expectedPackage, packageRoot, runtime.platform))
|
|
88
|
+
return;
|
|
89
|
+
return {
|
|
90
|
+
kind: "npm",
|
|
91
|
+
executable: runtime.nodeExecutable,
|
|
92
|
+
args: [npmCli, "install", "--global", `${PACKAGE}@latest`, "--ignore-scripts"],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function proveVolta(runtime) {
|
|
100
|
+
if (!runtime.voltaExecutable)
|
|
101
|
+
return;
|
|
102
|
+
try {
|
|
103
|
+
const [tool, node] = await Promise.all([
|
|
104
|
+
runtime.run(runtime.voltaExecutable, ["which", "ast-tool"]),
|
|
105
|
+
runtime.run(runtime.voltaExecutable, ["which", "node"]),
|
|
106
|
+
]);
|
|
107
|
+
if (!same(await canonical(runtime, tool.trim()), await canonical(runtime, runtime.cliExecutable), runtime.platform) ||
|
|
108
|
+
!same(await canonical(runtime, node.trim()), await canonical(runtime, runtime.nodeExecutable), runtime.platform))
|
|
109
|
+
return;
|
|
110
|
+
return {
|
|
111
|
+
kind: "volta",
|
|
112
|
+
executable: runtime.voltaExecutable,
|
|
113
|
+
args: ["install", `${PACKAGE}@latest`],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function versionParts(value) {
|
|
121
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
|
|
122
|
+
return match ? [+match[1], +match[2], +match[3], match[4] ?? ""] : undefined;
|
|
123
|
+
}
|
|
124
|
+
function updateAvailable(current, latest) {
|
|
125
|
+
const left = versionParts(current);
|
|
126
|
+
const right = versionParts(latest);
|
|
127
|
+
if (!left || !right)
|
|
128
|
+
fail("UPGRADE_INSPECTION_FAILED", "Package version is invalid.");
|
|
129
|
+
for (let index = 0; index < 3; index++) {
|
|
130
|
+
if (left[index] !== right[index])
|
|
131
|
+
return left[index] < right[index];
|
|
132
|
+
}
|
|
133
|
+
if (!left[3] || !right[3])
|
|
134
|
+
return Boolean(left[3]);
|
|
135
|
+
return left[3].localeCompare(right[3], "en", { numeric: true }) < 0;
|
|
136
|
+
}
|
|
137
|
+
export async function planUpgrade(runtime) {
|
|
138
|
+
const p = paths(runtime.platform);
|
|
139
|
+
const root = await canonical(runtime, runtime.packageRoot);
|
|
140
|
+
const current = await readPackage(runtime, root);
|
|
141
|
+
const cli = await canonical(runtime, p.resolve(root, current.cli));
|
|
142
|
+
const server = await canonical(runtime, p.resolve(root, current.server));
|
|
143
|
+
if (!inside(root, cli, runtime.platform) ||
|
|
144
|
+
!inside(root, server, runtime.platform) ||
|
|
145
|
+
!same(cli, await canonical(runtime, runtime.cliExecutable), runtime.platform)) {
|
|
146
|
+
fail("UPGRADE_PROVENANCE_UNSUPPORTED", "The running CLI does not belong to this package.");
|
|
147
|
+
}
|
|
148
|
+
const proofs = (await Promise.all([proveNpm(runtime, root), proveVolta(runtime)])).filter((proof) => proof !== undefined);
|
|
149
|
+
if (proofs.length !== 1)
|
|
150
|
+
fail("UPGRADE_PROVENANCE_UNSUPPORTED", "Package ownership is ambiguous or unsupported.");
|
|
151
|
+
const proof = proofs[0];
|
|
152
|
+
let latest;
|
|
153
|
+
try {
|
|
154
|
+
latest = await runtime.latestVersion(proof.kind);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
fail("UPGRADE_INSPECTION_FAILED", "Latest package version could not be verified.");
|
|
158
|
+
}
|
|
159
|
+
const update = updateAvailable(current.version, latest);
|
|
160
|
+
return {
|
|
161
|
+
version: 1,
|
|
162
|
+
status: "ok",
|
|
163
|
+
current_version: current.version,
|
|
164
|
+
latest_version: latest,
|
|
165
|
+
provenance: proof.kind,
|
|
166
|
+
update_available: update,
|
|
167
|
+
action: "ast-tool upgrade",
|
|
168
|
+
restart_required: update,
|
|
169
|
+
package_root: root,
|
|
170
|
+
cli_executable: cli,
|
|
171
|
+
install_executable: proof.executable,
|
|
172
|
+
install_args: proof.args,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export async function inspectUpgrade(runtime) {
|
|
176
|
+
const plan = await planUpgrade(runtime);
|
|
177
|
+
return {
|
|
178
|
+
version: plan.version,
|
|
179
|
+
status: plan.status,
|
|
180
|
+
current_version: plan.current_version,
|
|
181
|
+
latest_version: plan.latest_version,
|
|
182
|
+
provenance: plan.provenance,
|
|
183
|
+
update_available: plan.update_available,
|
|
184
|
+
action: plan.action,
|
|
185
|
+
restart_required: plan.restart_required,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
export async function performUpgrade(runtime) {
|
|
189
|
+
const planned = await planUpgrade(runtime);
|
|
190
|
+
if (!planned.update_available) {
|
|
191
|
+
return {
|
|
192
|
+
...inspectionFrom(planned),
|
|
193
|
+
status: "current",
|
|
194
|
+
installed_version: planned.current_version,
|
|
195
|
+
new_cli: planned.cli_executable,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
await runtime.run(planned.install_executable, planned.install_args);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
fail("UPGRADE_VERIFICATION_FAILED", "Package update command failed.");
|
|
203
|
+
}
|
|
204
|
+
let installed;
|
|
205
|
+
try {
|
|
206
|
+
installed = await planUpgrade(runtime);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
fail("UPGRADE_VERIFICATION_FAILED", "Updated package verification failed.");
|
|
210
|
+
}
|
|
211
|
+
if (installed.current_version !== installed.latest_version || installed.update_available) {
|
|
212
|
+
fail("UPGRADE_VERIFICATION_FAILED", "Updated package does not match refreshed latest.");
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
...inspectionFrom(installed),
|
|
216
|
+
status: "updated",
|
|
217
|
+
restart_required: true,
|
|
218
|
+
installed_version: installed.current_version,
|
|
219
|
+
new_cli: installed.cli_executable,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function inspectionFrom(plan) {
|
|
223
|
+
return {
|
|
224
|
+
version: plan.version,
|
|
225
|
+
current_version: plan.current_version,
|
|
226
|
+
latest_version: plan.latest_version,
|
|
227
|
+
provenance: plan.provenance,
|
|
228
|
+
update_available: plan.update_available,
|
|
229
|
+
action: plan.action,
|
|
230
|
+
restart_required: plan.restart_required,
|
|
231
|
+
};
|
|
232
|
+
}
|
package/dist/tools/explore.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { buildExploreContext, EXPLORE_DEFAULT_LIMIT, EXPLORE_DEFAULT_MAX_BYTES, EXPLORE_DEFAULT_REFERENCE_LIMIT, EXPLORE_MAX_BYTES, } from "../services/context-builder.js";
|
|
2
|
+
import { buildExploreContext, EXPLORE_DEFAULT_LIMIT, EXPLORE_DEFAULT_MAX_BYTES, EXPLORE_DEFAULT_OMISSION_DETAIL_LIMIT, EXPLORE_DEFAULT_REFERENCE_LIMIT, EXPLORE_MAX_BYTES, EXPLORE_MAX_OMISSION_DETAIL_LIMIT, } from "../services/context-builder.js";
|
|
3
|
+
import { DEFAULT_IMPACT_MAX_DEPTH, DEFAULT_IMPACT_MAX_EDGES, DEFAULT_IMPACT_MAX_NODES, MAX_IMPACT_DEPTH, MAX_IMPACT_EDGES, MAX_IMPACT_NODES, } from "../services/impact.js";
|
|
4
|
+
import { EXPLORE_OMISSION_REASONS } from "../services/explore-presentation.js";
|
|
3
5
|
import { createPaginationInputSchema } from "../services/pagination.js";
|
|
4
6
|
import { withProject } from "../services/project.js";
|
|
5
7
|
import { createRequestContext } from "../services/request-context.js";
|
|
6
8
|
import { FRESHNESS_CAUSES, SNAPSHOT_STATES, TRUNCATION_REASONS, } from "../services/read-contracts.js";
|
|
7
9
|
import { createToolErrorContext, errorResult, structuredResult } from "./result.js";
|
|
10
|
+
import { RelationshipEndpointSchema } from "./relationship-schema.js";
|
|
8
11
|
const ExploreDetailSchema = z.enum(["selectors", "summary", "context", "full"]).default("summary");
|
|
9
12
|
const ReferenceDetailSchema = z.enum(["locations", "context"]).default("locations");
|
|
13
|
+
const CallSpinesInputSchema = z.object({
|
|
14
|
+
direction: z.enum(["incoming", "outgoing"]).default("outgoing"),
|
|
15
|
+
max_depth: z.number().int().min(0).max(MAX_IMPACT_DEPTH).default(DEFAULT_IMPACT_MAX_DEPTH),
|
|
16
|
+
max_nodes: z.number().int().min(1).max(MAX_IMPACT_NODES).default(DEFAULT_IMPACT_MAX_NODES),
|
|
17
|
+
max_edges: z.number().int().min(1).max(MAX_IMPACT_EDGES).default(DEFAULT_IMPACT_MAX_EDGES),
|
|
18
|
+
});
|
|
10
19
|
const ExploreInputSchema = z
|
|
11
20
|
.object({
|
|
12
21
|
project_root: z
|
|
@@ -46,6 +55,14 @@ const ExploreInputSchema = z
|
|
|
46
55
|
.max(EXPLORE_MAX_BYTES)
|
|
47
56
|
.default(EXPLORE_DEFAULT_MAX_BYTES)
|
|
48
57
|
.describe("Maximum serialized logical result size in UTF-8 bytes."),
|
|
58
|
+
call_spines: CallSpinesInputSchema.optional().describe("Optional compiler-authoritative static call paths for an exact file_path and symbol_path."),
|
|
59
|
+
omission_detail_limit: z
|
|
60
|
+
.number()
|
|
61
|
+
.int()
|
|
62
|
+
.min(0)
|
|
63
|
+
.max(EXPLORE_MAX_OMISSION_DETAIL_LIMIT)
|
|
64
|
+
.default(EXPLORE_DEFAULT_OMISSION_DETAIL_LIMIT)
|
|
65
|
+
.describe("Maximum deterministic omission detail records; exact counts are always retained."),
|
|
49
66
|
})
|
|
50
67
|
.refine((input) => Boolean(input.query || input.file_path), {
|
|
51
68
|
message: "Provide query, file_path, or both file_path and symbol_path.",
|
|
@@ -54,6 +71,10 @@ const ExploreInputSchema = z
|
|
|
54
71
|
.refine((input) => !input.symbol_path || Boolean(input.file_path), {
|
|
55
72
|
message: "symbol_path requires file_path so the exact declaration can be resolved.",
|
|
56
73
|
path: ["symbol_path"],
|
|
74
|
+
})
|
|
75
|
+
.refine((input) => !input.call_spines || Boolean(input.file_path && input.symbol_path), {
|
|
76
|
+
message: "call_spines requires exact file_path and symbol_path inputs.",
|
|
77
|
+
path: ["call_spines"],
|
|
57
78
|
});
|
|
58
79
|
const ExploreSymbolSchema = z.object({
|
|
59
80
|
file: z.string(),
|
|
@@ -97,6 +118,35 @@ const ExploreEvidenceSchema = z.object({
|
|
|
97
118
|
})
|
|
98
119
|
.optional(),
|
|
99
120
|
});
|
|
121
|
+
const ExploreOmissionSchema = z.object({
|
|
122
|
+
subject: z.string(),
|
|
123
|
+
category: z.enum(["budget", "incomplete", "untrusted"]),
|
|
124
|
+
component: z.enum(["signature", "source", "references", "call_spine"]),
|
|
125
|
+
reason: z.enum(EXPLORE_OMISSION_REASONS),
|
|
126
|
+
});
|
|
127
|
+
const CallSpinesOutputSchema = z.object({
|
|
128
|
+
root: RelationshipEndpointSchema,
|
|
129
|
+
direction: z.enum(["incoming", "outgoing"]),
|
|
130
|
+
paths: z.array(z.object({
|
|
131
|
+
endpoint: RelationshipEndpointSchema,
|
|
132
|
+
endpoints: z.array(RelationshipEndpointSchema),
|
|
133
|
+
relationship_ids: z.array(z.string()),
|
|
134
|
+
})),
|
|
135
|
+
visited: z.object({
|
|
136
|
+
nodes: z.number().int().min(0),
|
|
137
|
+
edges: z.number().int().min(0),
|
|
138
|
+
max_depth: z.number().int().min(0),
|
|
139
|
+
}),
|
|
140
|
+
budget: z.object({
|
|
141
|
+
max_depth: z.number().int().min(0),
|
|
142
|
+
max_nodes: z.number().int().positive(),
|
|
143
|
+
max_edges: z.number().int().positive(),
|
|
144
|
+
}),
|
|
145
|
+
incomplete: z.boolean(),
|
|
146
|
+
truncation_reasons: z.array(z.enum(["depth_limit", "node_limit", "edge_limit"])),
|
|
147
|
+
authority_state: z.enum(["authoritative", "incomplete", "untrusted"]),
|
|
148
|
+
empty_proven: z.boolean(),
|
|
149
|
+
});
|
|
100
150
|
const ExploreOutputSchema = z.object({
|
|
101
151
|
route: z.enum(["query", "file", "symbol"]),
|
|
102
152
|
query: z.string().nullable(),
|
|
@@ -110,6 +160,17 @@ const ExploreOutputSchema = z.object({
|
|
|
110
160
|
total: z.number().int().min(0),
|
|
111
161
|
has_more: z.boolean(),
|
|
112
162
|
next_offset: z.number().int().min(0).nullable(),
|
|
163
|
+
omissions: z.object({
|
|
164
|
+
counts: z.array(z.object({
|
|
165
|
+
category: z.enum(["budget", "incomplete", "untrusted"]),
|
|
166
|
+
component: z.enum(["signature", "source", "references", "call_spine"]),
|
|
167
|
+
count: z.number().int().positive(),
|
|
168
|
+
})),
|
|
169
|
+
details: z.array(ExploreOmissionSchema),
|
|
170
|
+
total: z.number().int().min(0),
|
|
171
|
+
has_more: z.boolean(),
|
|
172
|
+
}),
|
|
173
|
+
call_spines: CallSpinesOutputSchema.optional(),
|
|
113
174
|
freshness: z.object({
|
|
114
175
|
state: z.enum(SNAPSHOT_STATES),
|
|
115
176
|
causes: z.array(z.enum(FRESHNESS_CAUSES)),
|
|
@@ -119,6 +180,7 @@ const ExploreOutputSchema = z.object({
|
|
|
119
180
|
complete: z.boolean(),
|
|
120
181
|
symbols_complete: z.boolean(),
|
|
121
182
|
evidence_complete: z.boolean(),
|
|
183
|
+
spines_complete: z.boolean().optional(),
|
|
122
184
|
unresolved: z.array(z.object({
|
|
123
185
|
selector: z.string(),
|
|
124
186
|
reason: z.enum(["source_unresolved", "references_unresolved"]),
|
|
@@ -149,7 +211,7 @@ export function registerExplore(server) {
|
|
|
149
211
|
idempotentHint: true,
|
|
150
212
|
openWorldHint: false,
|
|
151
213
|
},
|
|
152
|
-
}, async ({ project_root, query, file_path, symbol_path, kinds, file_filter, detail, include_source, include_references, reference_detail, reference_limit, offset, limit, max_bytes, }, extra) => {
|
|
214
|
+
}, async ({ project_root, query, file_path, symbol_path, kinds, file_filter, detail, include_source, include_references, reference_detail, reference_limit, offset, limit, max_bytes, call_spines, omission_detail_limit, }, extra) => {
|
|
153
215
|
const requestContext = createRequestContext(extra.signal);
|
|
154
216
|
try {
|
|
155
217
|
const output = await withProject(project_root, (context, operationContext) => buildExploreContext(context, {
|
|
@@ -166,6 +228,15 @@ export function registerExplore(server) {
|
|
|
166
228
|
limit,
|
|
167
229
|
referenceLimit: reference_limit,
|
|
168
230
|
maxBytes: max_bytes,
|
|
231
|
+
callSpines: call_spines
|
|
232
|
+
? {
|
|
233
|
+
direction: call_spines.direction,
|
|
234
|
+
maxDepth: call_spines.max_depth,
|
|
235
|
+
maxNodes: call_spines.max_nodes,
|
|
236
|
+
maxEdges: call_spines.max_edges,
|
|
237
|
+
}
|
|
238
|
+
: undefined,
|
|
239
|
+
omissionDetailLimit: omission_detail_limit,
|
|
169
240
|
}, operationContext), requestContext);
|
|
170
241
|
return structuredResult(ExploreOutputSchema.parse(output));
|
|
171
242
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { DEFAULT_IMPACT_MAX_DEPTH, DEFAULT_IMPACT_MAX_EDGES, DEFAULT_IMPACT_MAX_NODES, MAX_IMPACT_DEPTH, MAX_IMPACT_EDGES, MAX_IMPACT_NODES, resolveImpactRoot, traverseCompilerImpact, } from "../services/impact.js";
|
|
3
|
+
import { paginate, PaginationInputSchema, PaginationOutputSchema } from "../services/pagination.js";
|
|
4
|
+
import { withProject } from "../services/project.js";
|
|
5
|
+
import { PublicOperationalError } from "../services/public-errors.js";
|
|
6
|
+
import { createRequestContext } from "../services/request-context.js";
|
|
7
|
+
import { findTestCandidates, MAX_TEST_CANDIDATE_CONVENTION_ITEMS, MAX_TEST_CANDIDATE_CONVENTION_LENGTH, TEST_CANDIDATE_CONFIDENCES, TEST_CANDIDATE_REASONS, } from "../services/test-candidates.js";
|
|
8
|
+
import { FreshnessSchema, RelationshipEdgeSchema, RelationshipEndpointSchema, } from "./relationship-schema.js";
|
|
9
|
+
import { createToolErrorContext, errorResult, structuredResult } from "./result.js";
|
|
10
|
+
const ConventionSchema = z
|
|
11
|
+
.array(z.string().min(1).max(MAX_TEST_CANDIDATE_CONVENTION_LENGTH))
|
|
12
|
+
.max(MAX_TEST_CANDIDATE_CONVENTION_ITEMS)
|
|
13
|
+
.optional();
|
|
14
|
+
const AstFindTestCandidatesInputSchema = z.object({
|
|
15
|
+
project_root: z.string(),
|
|
16
|
+
file_path: z.string(),
|
|
17
|
+
symbol_path: z.string(),
|
|
18
|
+
max_depth: z.number().int().min(0).max(MAX_IMPACT_DEPTH).default(DEFAULT_IMPACT_MAX_DEPTH),
|
|
19
|
+
max_nodes: z.number().int().min(1).max(MAX_IMPACT_NODES).default(DEFAULT_IMPACT_MAX_NODES),
|
|
20
|
+
max_edges: z.number().int().min(1).max(MAX_IMPACT_EDGES).default(DEFAULT_IMPACT_MAX_EDGES),
|
|
21
|
+
test_file_patterns: ConventionSchema,
|
|
22
|
+
test_directories: ConventionSchema,
|
|
23
|
+
...PaginationInputSchema,
|
|
24
|
+
});
|
|
25
|
+
const TestCandidateSchema = z.object({
|
|
26
|
+
file: z.string(),
|
|
27
|
+
reason: z.enum(TEST_CANDIDATE_REASONS),
|
|
28
|
+
confidence: z.enum(TEST_CANDIDATE_CONFIDENCES),
|
|
29
|
+
evidence: z.object({
|
|
30
|
+
depth: z.number().int().min(0),
|
|
31
|
+
direct: z.boolean(),
|
|
32
|
+
compiler_authoritative: z.literal(true),
|
|
33
|
+
relationship_ids: z.array(z.string()),
|
|
34
|
+
relationships: z.array(RelationshipEdgeSchema),
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
const FindTestCandidatesOutputSchema = z.object({
|
|
38
|
+
backend: z.literal("typescript_compiler"),
|
|
39
|
+
compiler_authoritative: z.literal(true),
|
|
40
|
+
root: RelationshipEndpointSchema,
|
|
41
|
+
direction: z.literal("incoming"),
|
|
42
|
+
candidates: z.array(TestCandidateSchema),
|
|
43
|
+
...PaginationOutputSchema,
|
|
44
|
+
visited_nodes: z.number().int().min(0),
|
|
45
|
+
visited_edges: z.number().int().min(0),
|
|
46
|
+
max_depth_reached: z.number().int().min(0),
|
|
47
|
+
max_depth: z.number().int().min(0),
|
|
48
|
+
max_nodes: z.number().int().positive(),
|
|
49
|
+
max_edges: z.number().int().positive(),
|
|
50
|
+
incomplete: z.literal(false),
|
|
51
|
+
truncation: z.object({
|
|
52
|
+
truncated: z.literal(false),
|
|
53
|
+
reason: z.null(),
|
|
54
|
+
}),
|
|
55
|
+
freshness: FreshnessSchema,
|
|
56
|
+
completeness: z.object({
|
|
57
|
+
complete: z.literal(true),
|
|
58
|
+
proven_empty: z.boolean(),
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
export function registerFindTestCandidates(server) {
|
|
62
|
+
server.registerTool("ast_find_test_candidates", {
|
|
63
|
+
title: "Find compiler-backed affected test candidates",
|
|
64
|
+
description: "Finds deterministic affected test candidates from fresh, complete incoming compiler relationships. Returns bounded whole-candidate proofs and never executes tests or prepares mutations.",
|
|
65
|
+
inputSchema: AstFindTestCandidatesInputSchema,
|
|
66
|
+
outputSchema: FindTestCandidatesOutputSchema,
|
|
67
|
+
annotations: {
|
|
68
|
+
readOnlyHint: true,
|
|
69
|
+
destructiveHint: false,
|
|
70
|
+
idempotentHint: true,
|
|
71
|
+
openWorldHint: false,
|
|
72
|
+
},
|
|
73
|
+
}, async ({ project_root, file_path, symbol_path, max_depth, max_nodes, max_edges, test_file_patterns, test_directories, offset, limit, }, extra) => {
|
|
74
|
+
const requestContext = createRequestContext(extra.signal);
|
|
75
|
+
try {
|
|
76
|
+
const result = await withProject(project_root, (context, operationContext) => {
|
|
77
|
+
if (context.status.state !== "fresh") {
|
|
78
|
+
throw new PublicOperationalError("STALE_WORKSPACE", "Retry the workspace read.");
|
|
79
|
+
}
|
|
80
|
+
const freshness = {
|
|
81
|
+
state: context.status.state,
|
|
82
|
+
causes: context.status.causes,
|
|
83
|
+
checked_at: context.status.lastSuccessfulSyncAt,
|
|
84
|
+
};
|
|
85
|
+
const root = resolveImpactRoot(context.project, context.projectRoot, { file_path, symbol_path }, operationContext);
|
|
86
|
+
const impact = traverseCompilerImpact(context.project, context.projectRoot, root, freshness, { direction: "incoming", max_depth, max_nodes, max_edges }, operationContext);
|
|
87
|
+
if (impact.incomplete || impact.truncation.truncated) {
|
|
88
|
+
throw new PublicOperationalError("INCOMPLETE_EVIDENCE", "Evidence is incomplete.");
|
|
89
|
+
}
|
|
90
|
+
const candidates = findTestCandidates(impact, {
|
|
91
|
+
test_file_patterns,
|
|
92
|
+
test_directories,
|
|
93
|
+
});
|
|
94
|
+
const page = paginate(candidates, offset, limit);
|
|
95
|
+
return {
|
|
96
|
+
backend: "typescript_compiler",
|
|
97
|
+
compiler_authoritative: true,
|
|
98
|
+
root: impact.root,
|
|
99
|
+
direction: "incoming",
|
|
100
|
+
candidates: page.items,
|
|
101
|
+
offset: page.offset,
|
|
102
|
+
limit: page.limit,
|
|
103
|
+
total: page.total,
|
|
104
|
+
has_more: page.has_more,
|
|
105
|
+
next_offset: page.next_offset,
|
|
106
|
+
visited_nodes: impact.visited_nodes,
|
|
107
|
+
visited_edges: impact.visited_edges,
|
|
108
|
+
max_depth_reached: impact.max_depth_reached,
|
|
109
|
+
max_depth: impact.max_depth,
|
|
110
|
+
max_nodes: impact.max_nodes,
|
|
111
|
+
max_edges: impact.max_edges,
|
|
112
|
+
incomplete: false,
|
|
113
|
+
truncation: { truncated: false, reason: null },
|
|
114
|
+
freshness,
|
|
115
|
+
completeness: { complete: true, proven_empty: candidates.length === 0 },
|
|
116
|
+
};
|
|
117
|
+
}, requestContext);
|
|
118
|
+
return structuredResult(FindTestCandidatesOutputSchema.parse(result));
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return errorResult(error, createToolErrorContext("ast_find_test_candidates", project_root));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
package/dist/tools/get_impact.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { DEFAULT_IMPACT_MAX_DEPTH, DEFAULT_IMPACT_MAX_EDGES, DEFAULT_IMPACT_MAX_NODES, IMPACT_DIRECTIONS, MAX_IMPACT_DEPTH, MAX_IMPACT_EDGES, MAX_IMPACT_NODES, resolveImpactRoot, traverseCompilerImpact, } from "../services/impact.js";
|
|
3
|
-
import {
|
|
3
|
+
import { TRUNCATION_REASONS } from "../services/read-contracts.js";
|
|
4
4
|
import { withProject } from "../services/project.js";
|
|
5
5
|
import { createRequestContext } from "../services/request-context.js";
|
|
6
|
-
import {
|
|
6
|
+
import { RELATIONSHIP_EDGE_KINDS } from "../services/relationships.js";
|
|
7
|
+
import { FreshnessSchema, RelationshipEdgeSchema, RelationshipEndpointSchema, } from "./relationship-schema.js";
|
|
7
8
|
import { createToolErrorContext, errorResult, formattedResult, ToolOutputFormatInputSchema, } from "./result.js";
|
|
8
9
|
const AstGetImpactInputSchema = z.object({
|
|
9
10
|
project_root: z
|
|
@@ -42,34 +43,6 @@ const AstGetImpactInputSchema = z.object({
|
|
|
42
43
|
.describe("Optional relationship kinds to traverse; defaults to all normalized kinds."),
|
|
43
44
|
...ToolOutputFormatInputSchema,
|
|
44
45
|
});
|
|
45
|
-
const SourceRangeSchema = z.object({
|
|
46
|
-
start_line: z.number().int().positive(),
|
|
47
|
-
start_column: z.number().int().positive().optional(),
|
|
48
|
-
end_line: z.number().int().positive().optional(),
|
|
49
|
-
end_column: z.number().int().positive().optional(),
|
|
50
|
-
});
|
|
51
|
-
const RelationshipEndpointSchema = z.object({
|
|
52
|
-
file: z.string(),
|
|
53
|
-
symbol_path: z.string(),
|
|
54
|
-
selector: z.string(),
|
|
55
|
-
range: SourceRangeSchema.optional(),
|
|
56
|
-
});
|
|
57
|
-
const FreshnessSchema = z.object({
|
|
58
|
-
state: z.enum(SNAPSHOT_STATES),
|
|
59
|
-
causes: z.array(z.enum(FRESHNESS_CAUSES)),
|
|
60
|
-
checked_at: z.string().nullable(),
|
|
61
|
-
});
|
|
62
|
-
const RelationshipEdgeSchema = z.object({
|
|
63
|
-
relationship_id: z.string(),
|
|
64
|
-
source: RelationshipEndpointSchema,
|
|
65
|
-
target: RelationshipEndpointSchema,
|
|
66
|
-
kind: z.enum(RELATIONSHIP_EDGE_KINDS),
|
|
67
|
-
provenance: z.enum(RELATIONSHIP_PROVENANCES),
|
|
68
|
-
confidence: z.enum(RELATIONSHIP_CONFIDENCES),
|
|
69
|
-
resolution: z.enum(RELATIONSHIP_RESOLUTIONS),
|
|
70
|
-
compiler_authoritative: z.boolean(),
|
|
71
|
-
freshness: FreshnessSchema,
|
|
72
|
-
});
|
|
73
46
|
const ImpactOutputSchema = z.object({
|
|
74
47
|
root: RelationshipEndpointSchema,
|
|
75
48
|
direction: z.enum(IMPACT_DIRECTIONS),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { FRESHNESS_CAUSES, SNAPSHOT_STATES } from "../services/read-contracts.js";
|
|
3
|
+
import { RELATIONSHIP_CONFIDENCES, RELATIONSHIP_EDGE_KINDS, RELATIONSHIP_PROVENANCES, RELATIONSHIP_RESOLUTIONS, } from "../services/relationships.js";
|
|
4
|
+
export const SourceRangeSchema = z.object({
|
|
5
|
+
start_line: z.number().int().positive(),
|
|
6
|
+
start_column: z.number().int().positive().optional(),
|
|
7
|
+
end_line: z.number().int().positive().optional(),
|
|
8
|
+
end_column: z.number().int().positive().optional(),
|
|
9
|
+
});
|
|
10
|
+
export const RelationshipEndpointSchema = z.object({
|
|
11
|
+
file: z.string(),
|
|
12
|
+
symbol_path: z.string(),
|
|
13
|
+
selector: z.string(),
|
|
14
|
+
range: SourceRangeSchema.optional(),
|
|
15
|
+
});
|
|
16
|
+
export const FreshnessSchema = z.object({
|
|
17
|
+
state: z.enum(SNAPSHOT_STATES),
|
|
18
|
+
causes: z.array(z.enum(FRESHNESS_CAUSES)),
|
|
19
|
+
checked_at: z.string().nullable(),
|
|
20
|
+
});
|
|
21
|
+
export const RelationshipEdgeSchema = z.object({
|
|
22
|
+
relationship_id: z.string(),
|
|
23
|
+
source: RelationshipEndpointSchema,
|
|
24
|
+
target: RelationshipEndpointSchema,
|
|
25
|
+
kind: z.enum(RELATIONSHIP_EDGE_KINDS),
|
|
26
|
+
provenance: z.enum(RELATIONSHIP_PROVENANCES),
|
|
27
|
+
confidence: z.enum(RELATIONSHIP_CONFIDENCES),
|
|
28
|
+
resolution: z.enum(RELATIONSHIP_RESOLUTIONS),
|
|
29
|
+
compiler_authoritative: z.boolean(),
|
|
30
|
+
freshness: FreshnessSchema,
|
|
31
|
+
});
|
package/docs/support.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# Support policy
|
|
2
2
|
|
|
3
|
-
This document defines the supported platform, runtime, persistence, and operational boundary for published `ast-mcp-server` v0.
|
|
3
|
+
This document defines the supported platform, runtime, persistence, and operational boundary for published `ast-mcp-server` v0.10.0 and the local `0.11.0` release candidate.
|
|
4
4
|
|
|
5
5
|
## Supported release target
|
|
6
6
|
|
|
7
|
-
| Environment | Status | Contract
|
|
8
|
-
| ----------------------------------------------------------------------------- | ----------- |
|
|
9
|
-
| Linux x64 with required GNU coreutils and procfs, Node.js 22.5.0 |
|
|
10
|
-
| Linux x64 with required GNU coreutils and procfs, exact Node.js 22.13.0 | Supported |
|
|
11
|
-
| Linux x64 with required GNU coreutils and procfs, current Node.js 24 line | Supported | Published v0.
|
|
12
|
-
| Other Linux architectures or systems without the required filesystem features | Unverified | Not supported until equivalent architecture, mutation, and filesystem-publication gates pass.
|
|
13
|
-
| macOS | Unverified | Not supported until equivalent filesystem, process, package, mutation, and canary gates pass.
|
|
14
|
-
| Windows | Unverified | Not supported until equivalent filesystem, process, package, mutation, and canary gates pass.
|
|
15
|
-
| Remote, untrusted, or multi-tenant service use | Unsupported | No network authentication, sandbox, or tenant-isolation boundary is provided.
|
|
7
|
+
| Environment | Status | Contract |
|
|
8
|
+
| ----------------------------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------- |
|
|
9
|
+
| Linux x64 with required GNU coreutils and procfs, Node.js 22.5.0 | Historical | Immutable v0.8.1 release evidence; not the runtime floor of the published or candidate lines. |
|
|
10
|
+
| Linux x64 with required GNU coreutils and procfs, exact Node.js 22.13.0 | Supported | Published v0.10.0 and local 0.11.0 matrices, each bound to its own package and tree bytes. |
|
|
11
|
+
| Linux x64 with required GNU coreutils and procfs, current Node.js 24 line | Supported | Published v0.10.0 and local 0.11.0 matrices, each bound to its own package and tree bytes. |
|
|
12
|
+
| Other Linux architectures or systems without the required filesystem features | Unverified | Not supported until equivalent architecture, mutation, and filesystem-publication gates pass. |
|
|
13
|
+
| macOS | Unverified | Not supported until equivalent filesystem, process, package, mutation, and canary gates pass. |
|
|
14
|
+
| Windows | Unverified | Not supported until equivalent filesystem, process, package, mutation, and canary gates pass. |
|
|
15
|
+
| Remote, untrusted, or multi-tenant service use | Unsupported | No network authentication, sandbox, or tenant-isolation boundary is provided. |
|
|
16
16
|
|
|
17
|
-
The local `0.
|
|
17
|
+
The local `0.11.0` candidate requires Node.js `>=22.13.0`; its matrix targets exact Node.js 22.13.0 and the governed Node.js 24 major. Published v0.10.0 retains its immutable `>=22.13.0` metadata and exact Node.js 22.13.0/24 release evidence. The earlier v0.8.1 Node.js 22.5.0/24 matrix remains historical evidence for those bytes only. A future runtime satisfying an engine range is not automatically a verified release target.
|
|
18
18
|
|
|
19
19
|
The checked-evidence freezer requires GNU coreutils 9.7 `mv` with `--update=none-fail`, `--no-copy`, and `--no-target-directory`. Managed setup-file mutation additionally requires that same `mv` with `--exchange`, GNU coreutils `ln -L -T`, procfs descriptor paths at `/proc/self/fd`, `O_DIRECTORY`, and `O_NOFOLLOW`. It fails closed when any verified Linux primitive is unavailable. Equivalent publication, exchange, descriptor-link, and descriptor-relative mutation semantics have not been verified on other operating systems or architectures.
|
|
20
20
|
|
|
@@ -34,7 +34,7 @@ Operation locks coordinate cooperating same-user processes. They do not stop edi
|
|
|
34
34
|
|
|
35
35
|
## Symbol-index persistence
|
|
36
36
|
|
|
37
|
-
Published v0.
|
|
37
|
+
Published v0.10.0 and the local `0.11.0` candidate select native SQLite when the persistence setting is absent or explicitly `enabled`; operators requiring no persistent index state must set `disabled` explicitly:
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
AST_SYMBOL_INDEX_PERSISTENCE=disabled \
|
|
@@ -64,7 +64,7 @@ Both commands are bounded and path-free. Clear preflights the tree, refuses unsa
|
|
|
64
64
|
|
|
65
65
|
## Production-readiness acceptance
|
|
66
66
|
|
|
67
|
-
The four retained Linux x64 production-readiness reports cover `ast-mcp-server` and `x-scraper` under Node.js 22.5.0 and Node.js 24 for package version 0.6.0, commit `2d0b21bbb80fae1acfca6a85d5891d87e68b59c1`, and tree `af931d49769fabdf06f623965a6cfe1f9afb8a81`. They are historical integration evidence only: they are not reattributed to v0.8.1, published-next v0.9.0 or v0.9.1,
|
|
67
|
+
The four retained Linux x64 production-readiness reports cover `ast-mcp-server` and `x-scraper` under Node.js 22.5.0 and Node.js 24 for package version 0.6.0, commit `2d0b21bbb80fae1acfca6a85d5891d87e68b59c1`, and tree `af931d49769fabdf06f623965a6cfe1f9afb8a81`. They are historical integration evidence only: they are not reattributed to v0.8.1, published-next v0.9.0 or v0.9.1, the local 0.9.2 candidate, published v0.10.0, or the local v0.11.0 candidate. The exact 0.9.0 and 0.9.1 release matrices remain historical evidence for their immutable published-next bytes, the exact 0.9.2 matrix remains historical local-candidate evidence, and the exact 0.10.0 matrix remains historical published-release evidence for its own bytes. A separate exact 0.11.0 release matrix is the candidate-specific gate for current repository bytes under both supported runtime lines, including managed setup; none of these matrices rebinds the older retained reports. MCP-PROD-404 remains normative for admission:
|
|
68
68
|
|
|
69
69
|
- zero semantic mismatches across disabled, cold, warm, restart, and rollback reads;
|
|
70
70
|
- byte-identical real-repository status and worktree trees;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ast-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Structural code MCP server and batch CLI for compact TypeScript/JavaScript reads and hash-bound AST edits.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -97,5 +97,5 @@
|
|
|
97
97
|
"typescript-eslint": "^8.65.0",
|
|
98
98
|
"vitest": "^4.1.10"
|
|
99
99
|
},
|
|
100
|
-
"gitHead": "
|
|
100
|
+
"gitHead": "f0c8a66d72db71ffdf8ef0014f4b7b28f1d8852e"
|
|
101
101
|
}
|