ast-mcp-server 0.9.1 → 0.10.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 +28 -2
- package/README.md +21 -14
- package/SECURITY.md +1 -1
- package/dist/batch/schema.js +2 -0
- package/dist/server.js +2 -0
- package/dist/services/agent-targets.js +1 -0
- package/dist/services/call-spines.js +101 -0
- package/dist/services/context-builder.js +83 -24
- package/dist/services/explore-presentation.js +160 -0
- package/dist/services/project.js +5 -2
- package/dist/services/public-errors.js +7 -1
- package/dist/services/relationships.js +104 -0
- package/dist/services/test-candidates.js +15 -8
- 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 +6 -4
- package/skills/structural-code-editing/releases.json +12 -2
|
@@ -13,6 +13,7 @@ export const PUBLIC_ERROR_CODES = Object.freeze([
|
|
|
13
13
|
"NOT_FOUND",
|
|
14
14
|
"AMBIGUOUS_TARGET",
|
|
15
15
|
"STALE_WORKSPACE",
|
|
16
|
+
"INCOMPLETE_EVIDENCE",
|
|
16
17
|
"MUTATION_BLOCKED",
|
|
17
18
|
"CONFLICT",
|
|
18
19
|
"INTERNAL_ERROR",
|
|
@@ -35,6 +36,7 @@ const FIXED_PUBLIC_MESSAGES = Object.freeze({
|
|
|
35
36
|
NOT_FOUND: "The requested target was not found.",
|
|
36
37
|
AMBIGUOUS_TARGET: "The requested target is ambiguous.",
|
|
37
38
|
STALE_WORKSPACE: "The workspace changed. Retry the operation.",
|
|
39
|
+
INCOMPLETE_EVIDENCE: "The required compiler evidence is incomplete.",
|
|
38
40
|
MUTATION_BLOCKED: "The mutation is blocked.",
|
|
39
41
|
CONFLICT: "The operation conflicts with current state.",
|
|
40
42
|
INTERNAL_ERROR: INTERNAL_ERROR_MESSAGE,
|
|
@@ -70,6 +72,10 @@ const LEGACY_MESSAGE_RULES = Object.freeze([
|
|
|
70
72
|
code: "STALE_WORKSPACE",
|
|
71
73
|
pattern: /^(?:Workspace changed\b|Project (?:configuration|source) changed during synchronization\.)/,
|
|
72
74
|
},
|
|
75
|
+
{
|
|
76
|
+
code: "INCOMPLETE_EVIDENCE",
|
|
77
|
+
pattern: /^Compiler-backed impact evidence is incomplete\.$/,
|
|
78
|
+
},
|
|
73
79
|
{
|
|
74
80
|
code: "CONFLICT",
|
|
75
81
|
pattern: /^(?:Conflict:|Plan hash mismatch\b|Persisted plan integrity mismatch\b|Operation id collision\b|Applied receipt conflict\b|Postimage recovery conflict\b|Operation target already exists\b|Another AST apply holds\b)/,
|
|
@@ -80,7 +86,7 @@ const LEGACY_MESSAGE_RULES = Object.freeze([
|
|
|
80
86
|
},
|
|
81
87
|
{
|
|
82
88
|
code: "INVALID_INPUT",
|
|
83
|
-
pattern: /^(?:File path traversal is not allowed\.|Windows absolute paths are not valid on this host\.|Source file resolves outside the configured project root\.|Unsupported (?:UTF-16|non-UTF-8) source encoding\b|Persisted (?:operation|applied operation|prepared operation|project_root|original hash|updated hash|affected_files|diagnostic error delta|blocked status)\b|Scaffold file_path must\b|.{0,1024} is not a valid TypeScript identifier\.|.{0,1024} must (?:be|not)\b|.{0,1024} requires\b|.{0,1024} exceeds \d+ characters\.)/,
|
|
89
|
+
pattern: /^(?:File path traversal is not allowed\.|Windows absolute paths are not valid on this host\.|Source file resolves outside the configured project root\.|Unsupported (?:UTF-16|non-UTF-8) source encoding\b|Persisted (?:operation|applied operation|prepared operation|project_root|original hash|updated hash|affected_files|diagnostic error delta|blocked status)\b|Scaffold file_path must\b|.{0,1024} is not a valid TypeScript identifier\.|.{0,1024} must (?:be|not|contain)\b|.{0,1024} requires\b|.{0,1024} exceeds \d+ characters\.)/,
|
|
84
90
|
},
|
|
85
91
|
]);
|
|
86
92
|
export class PublicOperationalError extends Error {
|
|
@@ -1159,6 +1159,110 @@ export function createCompilerRelationshipResolver(project, projectRoot, freshne
|
|
|
1159
1159
|
},
|
|
1160
1160
|
};
|
|
1161
1161
|
}
|
|
1162
|
+
function callLikeExpression(node) {
|
|
1163
|
+
if (Node.isCallExpression(node) || Node.isNewExpression(node))
|
|
1164
|
+
return node.getExpression();
|
|
1165
|
+
if (Node.isTaggedTemplateExpression(node))
|
|
1166
|
+
return node.getTag();
|
|
1167
|
+
return undefined;
|
|
1168
|
+
}
|
|
1169
|
+
function unwrapInvocationExpression(node) {
|
|
1170
|
+
let current = node;
|
|
1171
|
+
while (Node.isParenthesizedExpression(current) ||
|
|
1172
|
+
Node.isNonNullExpression(current) ||
|
|
1173
|
+
Node.isAsExpression(current) ||
|
|
1174
|
+
Node.isTypeAssertion(current) ||
|
|
1175
|
+
Node.isSatisfiesExpression(current)) {
|
|
1176
|
+
current = current.getExpression();
|
|
1177
|
+
}
|
|
1178
|
+
return current;
|
|
1179
|
+
}
|
|
1180
|
+
function locatedCallTarget(projectRoot, declaration) {
|
|
1181
|
+
if (Node.isParameterDeclaration(declaration) || Node.isMethodSignature(declaration))
|
|
1182
|
+
return undefined;
|
|
1183
|
+
const sourceFile = declaration.getSourceFile();
|
|
1184
|
+
if (!isProjectScopedFile(projectRoot, sourceFile.getFilePath()))
|
|
1185
|
+
return undefined;
|
|
1186
|
+
const targetNode = Node.isConstructorDeclaration(declaration)
|
|
1187
|
+
? declaration.getFirstAncestorByKind(SyntaxKind.ClassDeclaration)
|
|
1188
|
+
: declaration;
|
|
1189
|
+
if (!targetNode)
|
|
1190
|
+
return undefined;
|
|
1191
|
+
return (collectSymbols(sourceFile).find((symbol) => symbol.node === targetNode) ??
|
|
1192
|
+
containingSymbol(sourceFile, targetNode));
|
|
1193
|
+
}
|
|
1194
|
+
export function collectCompilerCallRelationships(project, projectRoot, freshness, options = {}, requestContext = NO_REQUEST_CONTEXT) {
|
|
1195
|
+
const maxEdges = options.max_edges ?? 5_000;
|
|
1196
|
+
const maxWorkItems = options.max_work_items ?? 100_000;
|
|
1197
|
+
if (!Number.isSafeInteger(maxEdges) || maxEdges < 1) {
|
|
1198
|
+
throw new Error("Compiler call max_edges must be a positive safe integer.");
|
|
1199
|
+
}
|
|
1200
|
+
if (!Number.isSafeInteger(maxWorkItems) || maxWorkItems < 1) {
|
|
1201
|
+
throw new Error("Compiler call max_work_items must be a positive safe integer.");
|
|
1202
|
+
}
|
|
1203
|
+
const normalizedFreshness = normalizeFreshness(freshness);
|
|
1204
|
+
const checker = project.getTypeChecker();
|
|
1205
|
+
const edges = new Map();
|
|
1206
|
+
let workItems = 0;
|
|
1207
|
+
let workLimitReached = false;
|
|
1208
|
+
for (const sourceFile of project
|
|
1209
|
+
.getSourceFiles()
|
|
1210
|
+
.sort((left, right) => left.getFilePath().localeCompare(right.getFilePath()))) {
|
|
1211
|
+
sourceFile.forEachDescendant((node, traversal) => {
|
|
1212
|
+
requestContext.checkpoint();
|
|
1213
|
+
if (++workItems > maxWorkItems) {
|
|
1214
|
+
workLimitReached = true;
|
|
1215
|
+
traversal.stop();
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const expression = callLikeExpression(node);
|
|
1219
|
+
if (!expression)
|
|
1220
|
+
return;
|
|
1221
|
+
const invoked = unwrapInvocationExpression(expression);
|
|
1222
|
+
const caller = containingSymbol(sourceFile, node);
|
|
1223
|
+
if (!caller)
|
|
1224
|
+
return;
|
|
1225
|
+
const signature = checker.getResolvedSignature(node);
|
|
1226
|
+
const implicitConstructSignatures = Node.isNewExpression(node)
|
|
1227
|
+
? invoked.getType().getConstructSignatures()
|
|
1228
|
+
: [];
|
|
1229
|
+
if (!signature && implicitConstructSignatures.length !== 1)
|
|
1230
|
+
return;
|
|
1231
|
+
const signatureDeclaration = signature?.getDeclaration();
|
|
1232
|
+
const declarations = declarationsForSymbol(invoked.getSymbol());
|
|
1233
|
+
const targets = new Map();
|
|
1234
|
+
for (const declaration of declarations.length > 0
|
|
1235
|
+
? declarations
|
|
1236
|
+
: signatureDeclaration
|
|
1237
|
+
? [signatureDeclaration]
|
|
1238
|
+
: []) {
|
|
1239
|
+
const target = locatedCallTarget(projectRoot, declaration);
|
|
1240
|
+
if (target)
|
|
1241
|
+
targets.set(symbolEndpoint(target, projectRoot).selector, target);
|
|
1242
|
+
}
|
|
1243
|
+
if (targets.size !== 1)
|
|
1244
|
+
return;
|
|
1245
|
+
const target = [...targets.values()][0];
|
|
1246
|
+
const edge = createRelationshipEdge({
|
|
1247
|
+
source: symbolEndpoint(caller, projectRoot),
|
|
1248
|
+
target: symbolEndpoint(target, projectRoot),
|
|
1249
|
+
kind: "call",
|
|
1250
|
+
provenance: "compiler",
|
|
1251
|
+
confidence: "exact",
|
|
1252
|
+
resolution: "resolved",
|
|
1253
|
+
freshness: normalizedFreshness,
|
|
1254
|
+
});
|
|
1255
|
+
edges.set(edge.relationship_id, edge);
|
|
1256
|
+
});
|
|
1257
|
+
if (workLimitReached)
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
const ordered = [...edges.values()].sort((left, right) => left.relationship_id.localeCompare(right.relationship_id));
|
|
1261
|
+
return Object.freeze({
|
|
1262
|
+
edges: Object.freeze(ordered.slice(0, maxEdges)),
|
|
1263
|
+
incomplete: workLimitReached || ordered.length > maxEdges,
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1162
1266
|
export function collectCompilerRelationships(project, projectRoot, freshness, requestContext = NO_REQUEST_CONTEXT) {
|
|
1163
1267
|
requestContext.checkpoint();
|
|
1164
1268
|
const sourceFiles = project
|
|
@@ -4,21 +4,22 @@ export const DEFAULT_TEST_DIRECTORIES = Object.freeze(["test", "tests", "__tests
|
|
|
4
4
|
export const TEST_CANDIDATE_REASONS = Object.freeze([
|
|
5
5
|
"direct_compiler_reference",
|
|
6
6
|
"transitive_compiler_reference",
|
|
7
|
+
"convention_match",
|
|
7
8
|
]);
|
|
8
9
|
export const TEST_CANDIDATE_CONFIDENCES = Object.freeze(["exact", "high"]);
|
|
9
|
-
const
|
|
10
|
-
const
|
|
10
|
+
export const MAX_TEST_CANDIDATE_CONVENTION_ITEMS = 32;
|
|
11
|
+
export const MAX_TEST_CANDIDATE_CONVENTION_LENGTH = 256;
|
|
11
12
|
function normalizeConventionValues(values, defaults, label) {
|
|
12
13
|
const selected = values ?? defaults;
|
|
13
|
-
if (!Array.isArray(selected) || selected.length >
|
|
14
|
-
throw new Error(`${label} must contain at most ${
|
|
14
|
+
if (!Array.isArray(selected) || selected.length > MAX_TEST_CANDIDATE_CONVENTION_ITEMS) {
|
|
15
|
+
throw new Error(`${label} must contain at most ${MAX_TEST_CANDIDATE_CONVENTION_ITEMS} entries.`);
|
|
15
16
|
}
|
|
16
17
|
const normalized = selected.map((value) => {
|
|
17
18
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
18
19
|
throw new Error(`${label} entries must be non-empty strings.`);
|
|
19
20
|
}
|
|
20
|
-
if (value.length >
|
|
21
|
-
throw new Error(`${label} entries must not exceed ${
|
|
21
|
+
if (value.length > MAX_TEST_CANDIDATE_CONVENTION_LENGTH) {
|
|
22
|
+
throw new Error(`${label} entries must not exceed ${MAX_TEST_CANDIDATE_CONVENTION_LENGTH} characters.`);
|
|
22
23
|
}
|
|
23
24
|
return value.trim().replaceAll("\\", "/");
|
|
24
25
|
});
|
|
@@ -112,7 +113,13 @@ function edgeOrder(left, right) {
|
|
|
112
113
|
left.target.file.localeCompare(right.target.file) ||
|
|
113
114
|
left.target.selector.localeCompare(right.target.selector));
|
|
114
115
|
}
|
|
115
|
-
function candidateReason(depth) {
|
|
116
|
+
function candidateReason(file, depth) {
|
|
117
|
+
if (!isTestFile(file, {
|
|
118
|
+
patterns: DEFAULT_TEST_FILE_PATTERNS,
|
|
119
|
+
directories: DEFAULT_TEST_DIRECTORIES,
|
|
120
|
+
})) {
|
|
121
|
+
return "convention_match";
|
|
122
|
+
}
|
|
116
123
|
return depth === 1 ? "direct_compiler_reference" : "transitive_compiler_reference";
|
|
117
124
|
}
|
|
118
125
|
function candidateConfidence(depth) {
|
|
@@ -194,7 +201,7 @@ export function findTestCandidates(impact, conventions = {}) {
|
|
|
194
201
|
const depth = candidate.depth;
|
|
195
202
|
return {
|
|
196
203
|
file,
|
|
197
|
-
reason: candidateReason(depth),
|
|
204
|
+
reason: candidateReason(file, depth),
|
|
198
205
|
confidence: candidateConfidence(depth),
|
|
199
206
|
evidence: {
|
|
200
207
|
depth,
|
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.8.1 and the local `0.
|
|
3
|
+
This document defines the supported platform, runtime, persistence, and operational boundary for published `ast-mcp-server` v0.8.1 and the local `0.10.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 | Published | Immutable v0.8.1 release matrix; not the runtime floor of the current development tree.
|
|
10
|
-
| Linux x64 with required GNU coreutils and procfs, exact Node.js 22.13.0 | Supported | Local 0.
|
|
11
|
-
| Linux x64 with required GNU coreutils and procfs, current Node.js 24 line | Supported | Published v0.8.1 and local 0.
|
|
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 | Published | Immutable v0.8.1 release matrix; not the runtime floor of the current development tree. |
|
|
10
|
+
| Linux x64 with required GNU coreutils and procfs, exact Node.js 22.13.0 | Supported | Local 0.10.0 candidate package, persistence, MCP/CLI/package, lifecycle and mutation matrix. |
|
|
11
|
+
| Linux x64 with required GNU coreutils and procfs, current Node.js 24 line | Supported | Published v0.8.1 and local 0.10.0 candidate matrices, each bound to its own package/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.10.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.8.1 retains its immutable `>=22.5.0` metadata and exact Node.js 22.5.0/24 release evidence. 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.8.1 remains memory-default. In the local `0.
|
|
37
|
+
Published v0.8.1 remains memory-default. In the local `0.10.0` candidate, absence and explicit `enabled` select native SQLite; 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 the local 0.9.
|
|
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, or the local 0.9.2 candidate. The exact 0.9.0 and 0.9.1 release matrices remain historical evidence for their immutable published-next bytes, and the exact 0.9.2 release matrix remains historical local-candidate evidence for its own bytes. A separate exact 0.10.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.10.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": "7f97830d82d086e1a7ceafb84a8bc795be57039a"
|
|
101
101
|
}
|