ast-mcp-server 0.5.1 → 0.7.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 +36 -0
- package/README.md +63 -14
- package/SECURITY.md +31 -0
- package/dist/cli.js +0 -0
- package/dist/index.js +129 -10
- package/dist/server.js +25 -2
- package/dist/services/agent-setup.js +36 -117
- package/dist/services/agent-targets.js +122 -0
- package/dist/services/context-builder.js +262 -0
- package/dist/services/diagnostics.js +10 -5
- package/dist/services/file-fingerprints.js +104 -0
- package/dist/services/file-snapshot.js +103 -0
- package/dist/services/impact.js +334 -0
- package/dist/services/operations.js +343 -254
- package/dist/services/outline.js +94 -21
- package/dist/services/pagination.js +10 -4
- package/dist/services/project-operation-scheduler.js +475 -0
- package/dist/services/project-status.js +134 -52
- package/dist/services/project-watcher.js +230 -0
- package/dist/services/project.js +646 -66
- package/dist/services/public-errors.js +255 -0
- package/dist/services/references.js +54 -0
- package/dist/services/relationships.js +1198 -0
- package/dist/services/request-context.js +42 -0
- package/dist/services/runtime-activity.js +65 -0
- package/dist/services/runtime-logger.js +50 -0
- package/dist/services/runtime-policy.js +47 -0
- package/dist/services/setup-wizard.js +4 -7
- package/dist/services/shutdown.js +93 -0
- package/dist/services/symbol-index-limits.js +5 -0
- package/dist/services/symbol-index-order.js +13 -0
- package/dist/services/symbol-index-policy.js +91 -0
- package/dist/services/symbol-index-sqlite.js +850 -0
- package/dist/services/symbol-index.js +447 -0
- package/dist/services/symbols.js +430 -47
- package/dist/services/test-candidates.js +209 -0
- package/dist/services/workspace.js +18 -7
- package/dist/tools/apply_operation.js +8 -4
- package/dist/tools/explore.js +176 -0
- package/dist/tools/find_references.js +17 -55
- package/dist/tools/get_diagnostics.js +10 -7
- package/dist/tools/get_file.js +107 -0
- package/dist/tools/get_impact.js +134 -0
- package/dist/tools/get_operation_preview.js +7 -4
- package/dist/tools/get_outline.js +8 -6
- package/dist/tools/get_project_status.js +77 -8
- package/dist/tools/get_symbol_source.js +8 -6
- package/dist/tools/list_files.js +11 -6
- package/dist/tools/rename_symbol.js +6 -4
- package/dist/tools/replace_symbol_body.js +6 -4
- package/dist/tools/result.js +27 -17
- package/dist/tools/scaffold_class.js +6 -4
- package/dist/tools/search_symbols.js +15 -63
- package/docs/support.md +80 -0
- package/package.json +18 -6
- package/skills/structural-code-editing/SKILL.md +42 -8
- package/benchmark/README.md +0 -129
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { assertExactImpactEvidence } from "./impact.js";
|
|
2
|
+
export const DEFAULT_TEST_FILE_PATTERNS = Object.freeze(["**/*.test.*", "**/*.spec.*"]);
|
|
3
|
+
export const DEFAULT_TEST_DIRECTORIES = Object.freeze(["test", "tests", "__tests__"]);
|
|
4
|
+
export const TEST_CANDIDATE_REASONS = Object.freeze([
|
|
5
|
+
"direct_compiler_reference",
|
|
6
|
+
"transitive_compiler_reference",
|
|
7
|
+
]);
|
|
8
|
+
export const TEST_CANDIDATE_CONFIDENCES = Object.freeze(["exact", "high"]);
|
|
9
|
+
const MAX_CONVENTION_ITEMS = 32;
|
|
10
|
+
const MAX_CONVENTION_LENGTH = 256;
|
|
11
|
+
function normalizeConventionValues(values, defaults, label) {
|
|
12
|
+
const selected = values ?? defaults;
|
|
13
|
+
if (!Array.isArray(selected) || selected.length > MAX_CONVENTION_ITEMS) {
|
|
14
|
+
throw new Error(`${label} must contain at most ${MAX_CONVENTION_ITEMS} entries.`);
|
|
15
|
+
}
|
|
16
|
+
const normalized = selected.map((value) => {
|
|
17
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
18
|
+
throw new Error(`${label} entries must be non-empty strings.`);
|
|
19
|
+
}
|
|
20
|
+
if (value.length > MAX_CONVENTION_LENGTH) {
|
|
21
|
+
throw new Error(`${label} entries must not exceed ${MAX_CONVENTION_LENGTH} characters.`);
|
|
22
|
+
}
|
|
23
|
+
return value.trim().replaceAll("\\", "/");
|
|
24
|
+
});
|
|
25
|
+
return [...new Set(normalized)];
|
|
26
|
+
}
|
|
27
|
+
function assertSafePattern(pattern) {
|
|
28
|
+
if (pattern.startsWith("/") ||
|
|
29
|
+
/^[A-Za-z]:\//u.test(pattern) ||
|
|
30
|
+
pattern.split("/").some((segment) => segment === "..")) {
|
|
31
|
+
throw new Error("Test file patterns must be project-relative.");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function assertSafeDirectory(directory) {
|
|
35
|
+
if (directory.startsWith("/") ||
|
|
36
|
+
/^[A-Za-z]:\//u.test(directory) ||
|
|
37
|
+
directory.split("/").some((segment) => segment === "..")) {
|
|
38
|
+
throw new Error("Test directories must be project-relative.");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function normalizeConventions(conventions = {}) {
|
|
42
|
+
if (typeof conventions !== "object" || conventions === null) {
|
|
43
|
+
throw new Error("Test candidate conventions are invalid.");
|
|
44
|
+
}
|
|
45
|
+
const patterns = normalizeConventionValues(conventions.test_file_patterns, DEFAULT_TEST_FILE_PATTERNS, "test_file_patterns");
|
|
46
|
+
const directories = normalizeConventionValues(conventions.test_directories, DEFAULT_TEST_DIRECTORIES, "test_directories");
|
|
47
|
+
patterns.forEach(assertSafePattern);
|
|
48
|
+
directories.forEach(assertSafeDirectory);
|
|
49
|
+
return { patterns, directories };
|
|
50
|
+
}
|
|
51
|
+
function escapeRegexCharacter(character) {
|
|
52
|
+
return /[\\^$.*+?()[\]{}|]/u.test(character) ? `\\${character}` : character;
|
|
53
|
+
}
|
|
54
|
+
function globToRegExp(pattern) {
|
|
55
|
+
let source = "^";
|
|
56
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
57
|
+
const character = pattern[index];
|
|
58
|
+
if (character === "*" && pattern[index + 1] === "*") {
|
|
59
|
+
index += 1;
|
|
60
|
+
if (pattern[index + 1] === "/") {
|
|
61
|
+
index += 1;
|
|
62
|
+
source += "(?:[^/]+/)*";
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
source += ".*";
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (character === "*") {
|
|
70
|
+
source += "[^/]*";
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (character === "?") {
|
|
74
|
+
source += "[^/]";
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
source += escapeRegexCharacter(character);
|
|
78
|
+
}
|
|
79
|
+
return new RegExp(`${source}$`, "u");
|
|
80
|
+
}
|
|
81
|
+
function matchesFilePattern(file, pattern) {
|
|
82
|
+
const matcher = globToRegExp(pattern);
|
|
83
|
+
const basename = file.slice(file.lastIndexOf("/") + 1);
|
|
84
|
+
return matcher.test(file) || matcher.test(basename);
|
|
85
|
+
}
|
|
86
|
+
function pathContainsDirectory(file, directory) {
|
|
87
|
+
const fileSegments = file.split("/");
|
|
88
|
+
const directorySegments = directory.split("/").filter(Boolean);
|
|
89
|
+
if (directorySegments.length === 0)
|
|
90
|
+
return false;
|
|
91
|
+
return fileSegments.some((_, index) => directorySegments.every((segment, offset) => fileSegments[index + offset] === segment));
|
|
92
|
+
}
|
|
93
|
+
function isProjectRelativeFile(file) {
|
|
94
|
+
return (file.length > 0 &&
|
|
95
|
+
!file.startsWith("/") &&
|
|
96
|
+
!/^[A-Za-z]:[\\/]/u.test(file) &&
|
|
97
|
+
!file.split("/").some((segment) => segment === ".." || segment.length === 0));
|
|
98
|
+
}
|
|
99
|
+
function isTestFile(file, conventions) {
|
|
100
|
+
if (!isProjectRelativeFile(file))
|
|
101
|
+
return false;
|
|
102
|
+
return (conventions.patterns.some((pattern) => matchesFilePattern(file, pattern)) ||
|
|
103
|
+
conventions.directories.some((directory) => pathContainsDirectory(file, directory)));
|
|
104
|
+
}
|
|
105
|
+
function endpointKey(endpoint) {
|
|
106
|
+
return `${endpoint.file}\u0000${endpoint.symbol_path}\u0000${endpoint.selector}`;
|
|
107
|
+
}
|
|
108
|
+
function edgeOrder(left, right) {
|
|
109
|
+
return (left.relationship_id.localeCompare(right.relationship_id) ||
|
|
110
|
+
left.source.file.localeCompare(right.source.file) ||
|
|
111
|
+
left.source.selector.localeCompare(right.source.selector) ||
|
|
112
|
+
left.target.file.localeCompare(right.target.file) ||
|
|
113
|
+
left.target.selector.localeCompare(right.target.selector));
|
|
114
|
+
}
|
|
115
|
+
function candidateReason(depth) {
|
|
116
|
+
return depth === 1 ? "direct_compiler_reference" : "transitive_compiler_reference";
|
|
117
|
+
}
|
|
118
|
+
function candidateConfidence(depth) {
|
|
119
|
+
return depth === 1 ? "exact" : "high";
|
|
120
|
+
}
|
|
121
|
+
function findPathToRoot(startKeys, rootKey, edges) {
|
|
122
|
+
const queue = [
|
|
123
|
+
...startKeys,
|
|
124
|
+
]
|
|
125
|
+
.sort()
|
|
126
|
+
.map((key) => ({ key, path: [] }));
|
|
127
|
+
const visited = new Set(startKeys);
|
|
128
|
+
const orderedEdges = [...edges].sort(edgeOrder);
|
|
129
|
+
while (queue.length > 0) {
|
|
130
|
+
const current = queue.shift();
|
|
131
|
+
if (current.key === rootKey && current.path.length > 0)
|
|
132
|
+
return current.path;
|
|
133
|
+
const steps = [];
|
|
134
|
+
for (const edge of orderedEdges) {
|
|
135
|
+
const sourceKey = endpointKey(edge.source);
|
|
136
|
+
const targetKey = endpointKey(edge.target);
|
|
137
|
+
if (sourceKey === current.key)
|
|
138
|
+
steps.push({ edge, nextKey: targetKey });
|
|
139
|
+
if (targetKey === current.key)
|
|
140
|
+
steps.push({ edge, nextKey: sourceKey });
|
|
141
|
+
}
|
|
142
|
+
steps.sort((left, right) => edgeOrder(left.edge, right.edge) || left.nextKey.localeCompare(right.nextKey));
|
|
143
|
+
for (const step of steps) {
|
|
144
|
+
if (visited.has(step.nextKey))
|
|
145
|
+
continue;
|
|
146
|
+
visited.add(step.nextKey);
|
|
147
|
+
queue.push({ key: step.nextKey, path: [...current.path, step.edge] });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
export function findTestCandidates(impact, conventions = {}) {
|
|
153
|
+
assertExactImpactEvidence(impact);
|
|
154
|
+
if (typeof impact.root !== "object" ||
|
|
155
|
+
impact.root === null ||
|
|
156
|
+
!Array.isArray(impact.nodes) ||
|
|
157
|
+
!impact.nodes.every((node) => typeof node === "object" && node !== null)) {
|
|
158
|
+
throw new Error("Impact nodes are invalid.");
|
|
159
|
+
}
|
|
160
|
+
const normalized = normalizeConventions(conventions);
|
|
161
|
+
const nodesByEndpoint = new Map(impact.nodes.map((node) => [endpointKey(node.endpoint), node]));
|
|
162
|
+
const rootFile = impact.root.file;
|
|
163
|
+
const rootKey = endpointKey(impact.root);
|
|
164
|
+
const candidatesByFile = new Map();
|
|
165
|
+
for (const node of impact.nodes) {
|
|
166
|
+
const file = node.endpoint.file;
|
|
167
|
+
if (file === rootFile || !isTestFile(file, normalized))
|
|
168
|
+
continue;
|
|
169
|
+
const candidate = candidatesByFile.get(file);
|
|
170
|
+
if (candidate) {
|
|
171
|
+
candidate.depth = Math.min(candidate.depth, node.depth);
|
|
172
|
+
candidate.endpointKeys.add(endpointKey(node.endpoint));
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
candidatesByFile.set(file, {
|
|
176
|
+
depth: node.depth,
|
|
177
|
+
endpointKeys: new Set([endpointKey(node.endpoint)]),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const edge of impact.edges) {
|
|
182
|
+
const sourceNode = nodesByEndpoint.get(endpointKey(edge.source));
|
|
183
|
+
const targetNode = nodesByEndpoint.get(endpointKey(edge.target));
|
|
184
|
+
if (!sourceNode || !targetNode) {
|
|
185
|
+
throw new Error("Impact edge does not have matching bounded nodes.");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return [...candidatesByFile.entries()]
|
|
189
|
+
.map(([file, candidate]) => {
|
|
190
|
+
const relationships = findPathToRoot([...candidate.endpointKeys], rootKey, impact.edges);
|
|
191
|
+
if (!relationships) {
|
|
192
|
+
throw new Error(`Test candidate "${file}" has no exact impact path to the root.`);
|
|
193
|
+
}
|
|
194
|
+
const depth = candidate.depth;
|
|
195
|
+
return {
|
|
196
|
+
file,
|
|
197
|
+
reason: candidateReason(depth),
|
|
198
|
+
confidence: candidateConfidence(depth),
|
|
199
|
+
evidence: {
|
|
200
|
+
depth,
|
|
201
|
+
direct: depth === 1,
|
|
202
|
+
compiler_authoritative: true,
|
|
203
|
+
relationship_ids: relationships.map((edge) => edge.relationship_id),
|
|
204
|
+
relationships,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
})
|
|
208
|
+
.sort((left, right) => left.evidence.depth - right.evidence.depth || left.file.localeCompare(right.file));
|
|
209
|
+
}
|
|
@@ -2,9 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { ts } from "ts-morph";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
5
|
+
import { collectFileFingerprints, compareFileFingerprints, hashBytes, } from "./file-fingerprints.js";
|
|
6
|
+
export { hashBytes } from "./file-fingerprints.js";
|
|
8
7
|
export function hashWorkspaceFiles(files) {
|
|
9
8
|
const digest = createHash("sha256");
|
|
10
9
|
for (const [filePath, fileHash] of [...files].sort(([left], [right]) => left.localeCompare(right))) {
|
|
@@ -62,19 +61,29 @@ export function createConfigSnapshot(rootConfigPath) {
|
|
|
62
61
|
}
|
|
63
62
|
return { digest: digest.digest("hex"), files };
|
|
64
63
|
}
|
|
65
|
-
export function createWorkspaceSnapshot(context) {
|
|
64
|
+
export function createWorkspaceSnapshot(context, options = {}) {
|
|
66
65
|
const sourcePaths = context.project
|
|
67
66
|
.getSourceFiles()
|
|
68
67
|
.map((sourceFile) => canonicalPath(sourceFile.getFilePath()));
|
|
69
68
|
const configSnapshot = createConfigSnapshot(context.tsConfigFilePath);
|
|
69
|
+
const allPaths = [...new Set([...sourcePaths, ...configSnapshot.files])];
|
|
70
|
+
const fingerprintCollection = collectFileFingerprints(allPaths, options.previousFingerprints, {
|
|
71
|
+
verifyContentHash: options.verifyContentHash,
|
|
72
|
+
});
|
|
73
|
+
const fingerprints = fingerprintCollection.files;
|
|
70
74
|
const sourceFiles = new Map();
|
|
71
75
|
for (const filePath of [...new Set(sourcePaths)].sort()) {
|
|
72
|
-
const
|
|
73
|
-
|
|
76
|
+
const fingerprint = fingerprints.get(filePath);
|
|
77
|
+
if (fingerprint)
|
|
78
|
+
sourceFiles.set(filePath, fingerprint.content_hash);
|
|
74
79
|
}
|
|
75
80
|
const files = new Map(sourceFiles);
|
|
76
81
|
for (const filePath of configSnapshot.files) {
|
|
77
|
-
const
|
|
82
|
+
const fingerprint = fingerprints.get(filePath);
|
|
83
|
+
if (!fingerprint) {
|
|
84
|
+
throw new Error(`Configuration file disappeared while creating a workspace snapshot: ${filePath}`);
|
|
85
|
+
}
|
|
86
|
+
const digest = fingerprint.content_hash;
|
|
78
87
|
const previous = files.get(filePath);
|
|
79
88
|
if (previous && previous !== digest) {
|
|
80
89
|
throw new Error(`Workspace identity collision for ${filePath}.`);
|
|
@@ -88,5 +97,7 @@ export function createWorkspaceSnapshot(context) {
|
|
|
88
97
|
sourceFileCount: sourceFiles.size,
|
|
89
98
|
fileCount: files.size,
|
|
90
99
|
files,
|
|
100
|
+
fingerprints,
|
|
101
|
+
fingerprintChanges: compareFileFingerprints(options.previousFingerprints ?? new Map(), fingerprints),
|
|
91
102
|
};
|
|
92
103
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { applyOperation } from "../services/operations.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createRequestContext } from "../services/request-context.js";
|
|
4
|
+
import { createToolErrorContext, errorResult, structuredResult } from "./result.js";
|
|
4
5
|
const AstApplyOperationInputSchema = z.object({
|
|
5
6
|
operation_id: z.string().uuid().describe("Identifier returned by a prepare operation."),
|
|
6
7
|
plan_hash: z.string().length(64).describe("Plan hash returned by the same prepare operation."),
|
|
@@ -25,12 +26,15 @@ export function registerApplyOperation(server) {
|
|
|
25
26
|
idempotentHint: true,
|
|
26
27
|
openWorldHint: false,
|
|
27
28
|
},
|
|
28
|
-
}, async ({ operation_id, plan_hash }) => {
|
|
29
|
+
}, async ({ operation_id, plan_hash }, extra) => {
|
|
30
|
+
const requestContext = createRequestContext(extra.signal);
|
|
29
31
|
try {
|
|
30
|
-
return structuredResult({
|
|
32
|
+
return structuredResult({
|
|
33
|
+
...(await applyOperation(operation_id, plan_hash, requestContext)),
|
|
34
|
+
});
|
|
31
35
|
}
|
|
32
36
|
catch (error) {
|
|
33
|
-
return errorResult(error);
|
|
37
|
+
return errorResult(error, createToolErrorContext("ast_apply_operation"));
|
|
34
38
|
}
|
|
35
39
|
});
|
|
36
40
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
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";
|
|
3
|
+
import { createPaginationInputSchema } from "../services/pagination.js";
|
|
4
|
+
import { withProject } from "../services/project.js";
|
|
5
|
+
import { createRequestContext } from "../services/request-context.js";
|
|
6
|
+
import { FRESHNESS_CAUSES, SNAPSHOT_STATES, TRUNCATION_REASONS, } from "../services/read-contracts.js";
|
|
7
|
+
import { createToolErrorContext, errorResult, structuredResult } from "./result.js";
|
|
8
|
+
const ExploreDetailSchema = z.enum(["selectors", "summary", "context", "full"]).default("summary");
|
|
9
|
+
const ReferenceDetailSchema = z.enum(["locations", "context"]).default("locations");
|
|
10
|
+
const ExploreInputSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
project_root: z
|
|
13
|
+
.string()
|
|
14
|
+
.describe("Absolute project directory containing tsconfig.json, or the config path."),
|
|
15
|
+
query: z.string().min(1).optional().describe("Bounded case-insensitive symbol query."),
|
|
16
|
+
file_path: z.string().optional().describe("Known project-relative or unambiguous source file."),
|
|
17
|
+
symbol_path: z
|
|
18
|
+
.string()
|
|
19
|
+
.min(1)
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Exact symbol path or selector, valid only with file_path."),
|
|
22
|
+
kinds: z.array(z.string()).max(32).optional().describe("Optional exact syntax-kind filters."),
|
|
23
|
+
file_filter: z.string().optional().describe("Optional query-route file path substring filter."),
|
|
24
|
+
detail: ExploreDetailSchema.describe("selectors returns routing coordinates; summary adds signatures; context adds source; full adds source and references."),
|
|
25
|
+
include_source: z
|
|
26
|
+
.boolean()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Override detail-based source expansion for selected symbols."),
|
|
29
|
+
include_references: z
|
|
30
|
+
.boolean()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Override detail-based compiler reference expansion for selected symbols."),
|
|
33
|
+
reference_detail: ReferenceDetailSchema.describe("Reference locations or bounded source-line context."),
|
|
34
|
+
reference_limit: z
|
|
35
|
+
.number()
|
|
36
|
+
.int()
|
|
37
|
+
.min(1)
|
|
38
|
+
.max(100)
|
|
39
|
+
.default(EXPLORE_DEFAULT_REFERENCE_LIMIT)
|
|
40
|
+
.describe("Maximum compiler references per selected symbol."),
|
|
41
|
+
...createPaginationInputSchema(EXPLORE_DEFAULT_LIMIT),
|
|
42
|
+
max_bytes: z
|
|
43
|
+
.number()
|
|
44
|
+
.int()
|
|
45
|
+
.min(1024)
|
|
46
|
+
.max(EXPLORE_MAX_BYTES)
|
|
47
|
+
.default(EXPLORE_DEFAULT_MAX_BYTES)
|
|
48
|
+
.describe("Maximum serialized logical result size in UTF-8 bytes."),
|
|
49
|
+
})
|
|
50
|
+
.refine((input) => Boolean(input.query || input.file_path), {
|
|
51
|
+
message: "Provide query, file_path, or both file_path and symbol_path.",
|
|
52
|
+
path: ["query"],
|
|
53
|
+
})
|
|
54
|
+
.refine((input) => !input.symbol_path || Boolean(input.file_path), {
|
|
55
|
+
message: "symbol_path requires file_path so the exact declaration can be resolved.",
|
|
56
|
+
path: ["symbol_path"],
|
|
57
|
+
});
|
|
58
|
+
const ExploreSymbolSchema = z.object({
|
|
59
|
+
file: z.string(),
|
|
60
|
+
selector: z.string(),
|
|
61
|
+
kind: z.string(),
|
|
62
|
+
symbol_path: z.string().optional(),
|
|
63
|
+
name: z.string().optional(),
|
|
64
|
+
line: z.number().int().positive().optional(),
|
|
65
|
+
signature: z.string().optional(),
|
|
66
|
+
});
|
|
67
|
+
const ExploreSourceSchema = z.object({
|
|
68
|
+
file: z.string(),
|
|
69
|
+
startLine: z.number().int().positive(),
|
|
70
|
+
endLine: z.number().int().positive(),
|
|
71
|
+
text: z.string(),
|
|
72
|
+
});
|
|
73
|
+
const ExploreReferenceSchema = z.object({
|
|
74
|
+
file: z.string(),
|
|
75
|
+
line: z.number().int().positive(),
|
|
76
|
+
column: z.number().int().positive(),
|
|
77
|
+
kind: z.string(),
|
|
78
|
+
is_declaration: z.boolean(),
|
|
79
|
+
context: z.string().optional(),
|
|
80
|
+
});
|
|
81
|
+
const ExploreEvidenceSchema = z.object({
|
|
82
|
+
selector: z.string(),
|
|
83
|
+
source: ExploreSourceSchema.optional(),
|
|
84
|
+
references: z
|
|
85
|
+
.object({
|
|
86
|
+
symbol: z.string(),
|
|
87
|
+
include_declaration: z.boolean(),
|
|
88
|
+
declaration_count: z.number().int().min(0),
|
|
89
|
+
reference_count: z.number().int().min(0),
|
|
90
|
+
affected_files: z.array(z.string()),
|
|
91
|
+
references: z.array(ExploreReferenceSchema),
|
|
92
|
+
offset: z.number().int().min(0),
|
|
93
|
+
limit: z.number().int().positive(),
|
|
94
|
+
total: z.number().int().min(0),
|
|
95
|
+
has_more: z.boolean(),
|
|
96
|
+
next_offset: z.number().int().min(0).nullable(),
|
|
97
|
+
})
|
|
98
|
+
.optional(),
|
|
99
|
+
});
|
|
100
|
+
const ExploreOutputSchema = z.object({
|
|
101
|
+
route: z.enum(["query", "file", "symbol"]),
|
|
102
|
+
query: z.string().nullable(),
|
|
103
|
+
file: z.string().nullable(),
|
|
104
|
+
symbol: z.string().nullable(),
|
|
105
|
+
detail: ExploreDetailSchema,
|
|
106
|
+
symbols: z.array(ExploreSymbolSchema),
|
|
107
|
+
evidence: z.array(ExploreEvidenceSchema),
|
|
108
|
+
offset: z.number().int().min(0),
|
|
109
|
+
limit: z.number().int().positive(),
|
|
110
|
+
total: z.number().int().min(0),
|
|
111
|
+
has_more: z.boolean(),
|
|
112
|
+
next_offset: z.number().int().min(0).nullable(),
|
|
113
|
+
freshness: z.object({
|
|
114
|
+
state: z.enum(SNAPSHOT_STATES),
|
|
115
|
+
causes: z.array(z.enum(FRESHNESS_CAUSES)),
|
|
116
|
+
checked_at: z.string().nullable(),
|
|
117
|
+
}),
|
|
118
|
+
completeness: z.object({
|
|
119
|
+
complete: z.boolean(),
|
|
120
|
+
symbols_complete: z.boolean(),
|
|
121
|
+
evidence_complete: z.boolean(),
|
|
122
|
+
unresolved: z.array(z.object({
|
|
123
|
+
selector: z.string(),
|
|
124
|
+
reason: z.enum(["source_unresolved", "references_unresolved"]),
|
|
125
|
+
})),
|
|
126
|
+
}),
|
|
127
|
+
budget: z.object({
|
|
128
|
+
max_records: z.number().int().positive(),
|
|
129
|
+
max_bytes: z.number().int().positive(),
|
|
130
|
+
max_depth: z.null(),
|
|
131
|
+
max_edges: z.null(),
|
|
132
|
+
max_invocations: z.number().int().positive(),
|
|
133
|
+
used_bytes: z.number().int().nonnegative(),
|
|
134
|
+
}),
|
|
135
|
+
truncation: z.object({
|
|
136
|
+
truncated: z.boolean(),
|
|
137
|
+
reason: z.enum(TRUNCATION_REASONS).nullable(),
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
export function registerExplore(server) {
|
|
141
|
+
server.registerTool("ast_explore", {
|
|
142
|
+
title: "Explore project context",
|
|
143
|
+
description: "Composes bounded structural search, exact selectors, source evidence and compiler references without weakening the primitive AST tools. Returns bounded session freshness metadata for fresh, pending, stale, rebuilding or degraded state.",
|
|
144
|
+
inputSchema: ExploreInputSchema,
|
|
145
|
+
outputSchema: ExploreOutputSchema,
|
|
146
|
+
annotations: {
|
|
147
|
+
readOnlyHint: true,
|
|
148
|
+
destructiveHint: false,
|
|
149
|
+
idempotentHint: true,
|
|
150
|
+
openWorldHint: false,
|
|
151
|
+
},
|
|
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) => {
|
|
153
|
+
const requestContext = createRequestContext(extra.signal);
|
|
154
|
+
try {
|
|
155
|
+
const output = await withProject(project_root, (context, operationContext) => buildExploreContext(context, {
|
|
156
|
+
query,
|
|
157
|
+
filePath: file_path,
|
|
158
|
+
symbolPath: symbol_path,
|
|
159
|
+
kinds,
|
|
160
|
+
fileFilter: file_filter,
|
|
161
|
+
detail,
|
|
162
|
+
includeSource: include_source,
|
|
163
|
+
includeReferences: include_references,
|
|
164
|
+
referenceDetail: reference_detail,
|
|
165
|
+
offset,
|
|
166
|
+
limit,
|
|
167
|
+
referenceLimit: reference_limit,
|
|
168
|
+
maxBytes: max_bytes,
|
|
169
|
+
}, operationContext), requestContext);
|
|
170
|
+
return structuredResult(ExploreOutputSchema.parse(output));
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
return errorResult(error, createToolErrorContext("ast_explore", project_root));
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { Node } from "ts-morph";
|
|
3
1
|
import { z } from "zod";
|
|
4
|
-
import { PaginationInputSchema, PaginationOutputSchema
|
|
2
|
+
import { PaginationInputSchema, PaginationOutputSchema } from "../services/pagination.js";
|
|
5
3
|
import { findDeclarationByName, getSourceFileOrThrow, withProject } from "../services/project.js";
|
|
6
|
-
import {
|
|
4
|
+
import { collectSymbolReferences } from "../services/references.js";
|
|
5
|
+
import { createRequestContext } from "../services/request-context.js";
|
|
6
|
+
import { createToolErrorContext, errorResult, formattedResult, ToolOutputFormatInputSchema, } from "./result.js";
|
|
7
7
|
const ReferenceDetailSchema = z.enum(["locations", "context"]).default("locations");
|
|
8
8
|
const AstFindReferencesInputSchema = z.object({
|
|
9
9
|
project_root: z
|
|
@@ -54,67 +54,29 @@ export function registerFindReferences(server) {
|
|
|
54
54
|
idempotentHint: true,
|
|
55
55
|
openWorldHint: false,
|
|
56
56
|
},
|
|
57
|
-
}, async ({ project_root, file_path, symbol_path, include_declaration, detail, output_format, offset, limit, }) => {
|
|
57
|
+
}, async ({ project_root, file_path, symbol_path, include_declaration, detail, output_format, offset, limit, }, extra) => {
|
|
58
|
+
const requestContext = createRequestContext(extra.signal);
|
|
58
59
|
try {
|
|
59
|
-
const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
|
|
60
|
+
const structuredContent = await withProject(project_root, ({ project, projectRoot }, operationContext) => {
|
|
60
61
|
const sourceFile = getSourceFileOrThrow(project, file_path);
|
|
61
62
|
const node = findDeclarationByName(sourceFile, symbol_path);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
const lineCache = new Map();
|
|
66
|
-
const location = (referenceNode, isDeclaration) => {
|
|
67
|
-
const referenceSource = referenceNode.getSourceFile();
|
|
68
|
-
const absoluteFile = referenceSource.getFilePath();
|
|
69
|
-
let lines = lineCache.get(absoluteFile);
|
|
70
|
-
if (!lines) {
|
|
71
|
-
lines = referenceSource.getFullText().split(/\r?\n/);
|
|
72
|
-
lineCache.set(absoluteFile, lines);
|
|
73
|
-
}
|
|
74
|
-
const position = referenceSource.getLineAndColumnAtPos(referenceNode.getStart());
|
|
75
|
-
return {
|
|
76
|
-
file: path.relative(projectRoot, absoluteFile),
|
|
77
|
-
line: position.line,
|
|
78
|
-
column: position.column,
|
|
79
|
-
kind: referenceNode.getParent()?.getKindName() ?? referenceNode.getKindName(),
|
|
80
|
-
is_declaration: isDeclaration,
|
|
81
|
-
context: (lines[position.line - 1] ?? "").trim().slice(0, 500),
|
|
82
|
-
};
|
|
83
|
-
};
|
|
84
|
-
const references = node
|
|
85
|
-
.findReferencesAsNodes()
|
|
86
|
-
.map((reference) => location(reference, false));
|
|
87
|
-
const declaration = location(node, true);
|
|
88
|
-
const allLocations = include_declaration ? [declaration, ...references] : references;
|
|
89
|
-
allLocations.sort((left, right) => `${left.file}:${left.line}:${left.column}:${left.is_declaration ? 0 : 1}`.localeCompare(`${right.file}:${right.line}:${right.column}:${right.is_declaration ? 0 : 1}`));
|
|
90
|
-
const affectedFiles = [
|
|
91
|
-
...new Set([declaration, ...references].map((item) => item.file)),
|
|
92
|
-
].sort();
|
|
93
|
-
const page = paginate(allLocations, offset, limit);
|
|
94
|
-
const { items, ...metadata } = page;
|
|
63
|
+
const collected = collectSymbolReferences(node, projectRoot, symbol_path, include_declaration, detail, offset, limit, operationContext);
|
|
64
|
+
const { references, ...metadata } = collected;
|
|
95
65
|
return {
|
|
96
|
-
symbol: symbol_path,
|
|
97
|
-
include_declaration,
|
|
98
|
-
declaration_count: 1,
|
|
99
|
-
reference_count: references.length,
|
|
100
|
-
affected_files: affectedFiles,
|
|
101
|
-
references: detail === "context"
|
|
102
|
-
? items
|
|
103
|
-
: items.map(({ file, line, column, kind, is_declaration }) => ({
|
|
104
|
-
file,
|
|
105
|
-
line,
|
|
106
|
-
column,
|
|
107
|
-
kind,
|
|
108
|
-
is_declaration,
|
|
109
|
-
})),
|
|
110
66
|
...metadata,
|
|
67
|
+
references: detail === "context"
|
|
68
|
+
? references
|
|
69
|
+
: references.map(({ file, line, column, kind, is_declaration }) => {
|
|
70
|
+
operationContext.checkpoint();
|
|
71
|
+
return { file, line, column, kind, is_declaration };
|
|
72
|
+
}),
|
|
111
73
|
};
|
|
112
|
-
});
|
|
74
|
+
}, requestContext);
|
|
113
75
|
const outputSchema = ReferenceOutputSchemas[detail];
|
|
114
76
|
return formattedResult(outputSchema, structuredContent, output_format);
|
|
115
77
|
}
|
|
116
78
|
catch (error) {
|
|
117
|
-
return errorResult(error);
|
|
79
|
+
return errorResult(error, createToolErrorContext("ast_find_references", project_root));
|
|
118
80
|
}
|
|
119
81
|
});
|
|
120
82
|
}
|
|
@@ -2,7 +2,8 @@ import { z } from "zod";
|
|
|
2
2
|
import { normalizeDiagnostic } from "../services/diagnostics.js";
|
|
3
3
|
import { PaginationInputSchema, PaginationOutputSchema, paginate } from "../services/pagination.js";
|
|
4
4
|
import { getSourceFileOrThrow, withProject } from "../services/project.js";
|
|
5
|
-
import {
|
|
5
|
+
import { createRequestContext } from "../services/request-context.js";
|
|
6
|
+
import { createToolErrorContext, errorResult, formattedResult, ToolOutputFormatInputSchema, } from "./result.js";
|
|
6
7
|
const DiagnosticSchema = z.object({
|
|
7
8
|
code: z.number().int(),
|
|
8
9
|
category: z.string(),
|
|
@@ -40,31 +41,33 @@ export function registerGetDiagnostics(server) {
|
|
|
40
41
|
idempotentHint: true,
|
|
41
42
|
openWorldHint: false,
|
|
42
43
|
},
|
|
43
|
-
}, async ({ project_root, file_path, output_format, offset, limit }) => {
|
|
44
|
+
}, async ({ project_root, file_path, output_format, offset, limit }, extra) => {
|
|
45
|
+
const requestContext = createRequestContext(extra.signal);
|
|
44
46
|
try {
|
|
45
|
-
const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
|
|
47
|
+
const structuredContent = await withProject(project_root, ({ project, projectRoot }, operationContext) => {
|
|
46
48
|
const startedAt = performance.now();
|
|
47
49
|
const diagnostics = file_path
|
|
48
50
|
? getSourceFileOrThrow(project, file_path).getPreEmitDiagnostics()
|
|
49
51
|
: [...project.getConfigFileParsingDiagnostics(), ...project.getPreEmitDiagnostics()];
|
|
50
52
|
const normalized = diagnostics
|
|
51
|
-
.map((diagnostic) => normalizeDiagnostic(diagnostic, projectRoot))
|
|
53
|
+
.map((diagnostic) => normalizeDiagnostic(diagnostic, projectRoot, operationContext))
|
|
52
54
|
.sort((left, right) => `${left.file ?? ""}:${left.line ?? 0}:${left.column ?? 0}:${left.code}`.localeCompare(`${right.file ?? ""}:${right.line ?? 0}:${right.column ?? 0}:${right.code}`));
|
|
53
55
|
const page = paginate(normalized, offset, limit);
|
|
54
56
|
const { items, ...metadata } = page;
|
|
55
57
|
return {
|
|
56
58
|
diagnostics: items,
|
|
57
|
-
error_count: normalized.filter((diagnostic) => diagnostic.category === "Error")
|
|
59
|
+
error_count: normalized.filter((diagnostic) => diagnostic.category === "Error")
|
|
60
|
+
.length,
|
|
58
61
|
warning_count: normalized.filter((diagnostic) => diagnostic.category === "Warning")
|
|
59
62
|
.length,
|
|
60
63
|
duration_ms: performance.now() - startedAt,
|
|
61
64
|
...metadata,
|
|
62
65
|
};
|
|
63
|
-
});
|
|
66
|
+
}, requestContext);
|
|
64
67
|
return formattedResult(AstGetDiagnosticsOutputSchema, structuredContent, output_format);
|
|
65
68
|
}
|
|
66
69
|
catch (error) {
|
|
67
|
-
return errorResult(error);
|
|
70
|
+
return errorResult(error, createToolErrorContext("ast_get_diagnostics", project_root));
|
|
68
71
|
}
|
|
69
72
|
});
|
|
70
73
|
}
|