ast-mcp-server 0.6.0 → 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 +27 -0
- package/README.md +47 -15
- package/SECURITY.md +31 -0
- package/dist/cli.js +0 -0
- package/dist/index.js +129 -10
- package/dist/server.js +21 -2
- package/dist/services/agent-setup.js +36 -117
- package/dist/services/agent-targets.js +122 -0
- package/dist/services/context-builder.js +52 -24
- package/dist/services/diagnostics.js +10 -5
- package/dist/services/file-fingerprints.js +104 -0
- package/dist/services/file-snapshot.js +8 -1
- 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 +7 -1
- 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 +403 -79
- 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 +8 -6
- package/dist/tools/find_references.js +12 -13
- package/dist/tools/get_diagnostics.js +10 -7
- package/dist/tools/get_file.js +23 -11
- 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 -14
- package/docs/support.md +80 -0
- package/package.json +16 -5
- package/skills/structural-code-editing/SKILL.md +37 -10
|
@@ -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
|
}
|
package/dist/tools/explore.js
CHANGED
|
@@ -2,8 +2,9 @@ import { z } from "zod";
|
|
|
2
2
|
import { buildExploreContext, EXPLORE_DEFAULT_LIMIT, EXPLORE_DEFAULT_MAX_BYTES, EXPLORE_DEFAULT_REFERENCE_LIMIT, EXPLORE_MAX_BYTES, } from "../services/context-builder.js";
|
|
3
3
|
import { createPaginationInputSchema } from "../services/pagination.js";
|
|
4
4
|
import { withProject } from "../services/project.js";
|
|
5
|
+
import { createRequestContext } from "../services/request-context.js";
|
|
5
6
|
import { FRESHNESS_CAUSES, SNAPSHOT_STATES, TRUNCATION_REASONS, } from "../services/read-contracts.js";
|
|
6
|
-
import { errorResult, structuredResult } from "./result.js";
|
|
7
|
+
import { createToolErrorContext, errorResult, structuredResult } from "./result.js";
|
|
7
8
|
const ExploreDetailSchema = z.enum(["selectors", "summary", "context", "full"]).default("summary");
|
|
8
9
|
const ReferenceDetailSchema = z.enum(["locations", "context"]).default("locations");
|
|
9
10
|
const ExploreInputSchema = z
|
|
@@ -139,7 +140,7 @@ const ExploreOutputSchema = z.object({
|
|
|
139
140
|
export function registerExplore(server) {
|
|
140
141
|
server.registerTool("ast_explore", {
|
|
141
142
|
title: "Explore project context",
|
|
142
|
-
description: "Composes bounded structural search, exact selectors, source evidence and compiler references without weakening the primitive AST tools.",
|
|
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.",
|
|
143
144
|
inputSchema: ExploreInputSchema,
|
|
144
145
|
outputSchema: ExploreOutputSchema,
|
|
145
146
|
annotations: {
|
|
@@ -148,9 +149,10 @@ export function registerExplore(server) {
|
|
|
148
149
|
idempotentHint: true,
|
|
149
150
|
openWorldHint: false,
|
|
150
151
|
},
|
|
151
|
-
}, async ({ project_root, query, file_path, symbol_path, kinds, file_filter, detail, include_source, include_references, reference_detail, reference_limit, offset, limit, max_bytes, }) => {
|
|
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);
|
|
152
154
|
try {
|
|
153
|
-
const output = await withProject(project_root, (context) => buildExploreContext(context, {
|
|
155
|
+
const output = await withProject(project_root, (context, operationContext) => buildExploreContext(context, {
|
|
154
156
|
query,
|
|
155
157
|
filePath: file_path,
|
|
156
158
|
symbolPath: symbol_path,
|
|
@@ -164,11 +166,11 @@ export function registerExplore(server) {
|
|
|
164
166
|
limit,
|
|
165
167
|
referenceLimit: reference_limit,
|
|
166
168
|
maxBytes: max_bytes,
|
|
167
|
-
}));
|
|
169
|
+
}, operationContext), requestContext);
|
|
168
170
|
return structuredResult(ExploreOutputSchema.parse(output));
|
|
169
171
|
}
|
|
170
172
|
catch (error) {
|
|
171
|
-
return errorResult(error);
|
|
173
|
+
return errorResult(error, createToolErrorContext("ast_explore", project_root));
|
|
172
174
|
}
|
|
173
175
|
});
|
|
174
176
|
}
|
|
@@ -2,7 +2,8 @@ import { z } from "zod";
|
|
|
2
2
|
import { PaginationInputSchema, PaginationOutputSchema } from "../services/pagination.js";
|
|
3
3
|
import { findDeclarationByName, getSourceFileOrThrow, withProject } from "../services/project.js";
|
|
4
4
|
import { collectSymbolReferences } from "../services/references.js";
|
|
5
|
-
import {
|
|
5
|
+
import { createRequestContext } from "../services/request-context.js";
|
|
6
|
+
import { createToolErrorContext, errorResult, formattedResult, ToolOutputFormatInputSchema, } from "./result.js";
|
|
6
7
|
const ReferenceDetailSchema = z.enum(["locations", "context"]).default("locations");
|
|
7
8
|
const AstFindReferencesInputSchema = z.object({
|
|
8
9
|
project_root: z
|
|
@@ -53,31 +54,29 @@ export function registerFindReferences(server) {
|
|
|
53
54
|
idempotentHint: true,
|
|
54
55
|
openWorldHint: false,
|
|
55
56
|
},
|
|
56
|
-
}, 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);
|
|
57
59
|
try {
|
|
58
|
-
const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
|
|
60
|
+
const structuredContent = await withProject(project_root, ({ project, projectRoot }, operationContext) => {
|
|
59
61
|
const sourceFile = getSourceFileOrThrow(project, file_path);
|
|
60
62
|
const node = findDeclarationByName(sourceFile, symbol_path);
|
|
61
|
-
const collected = collectSymbolReferences(node, projectRoot, symbol_path, include_declaration, detail, offset, limit);
|
|
63
|
+
const collected = collectSymbolReferences(node, projectRoot, symbol_path, include_declaration, detail, offset, limit, operationContext);
|
|
62
64
|
const { references, ...metadata } = collected;
|
|
63
65
|
return {
|
|
64
66
|
...metadata,
|
|
65
67
|
references: detail === "context"
|
|
66
68
|
? references
|
|
67
|
-
: references.map(({ file, line, column, kind, is_declaration }) =>
|
|
68
|
-
|
|
69
|
-
line,
|
|
70
|
-
|
|
71
|
-
kind,
|
|
72
|
-
is_declaration,
|
|
73
|
-
})),
|
|
69
|
+
: references.map(({ file, line, column, kind, is_declaration }) => {
|
|
70
|
+
operationContext.checkpoint();
|
|
71
|
+
return { file, line, column, kind, is_declaration };
|
|
72
|
+
}),
|
|
74
73
|
};
|
|
75
|
-
});
|
|
74
|
+
}, requestContext);
|
|
76
75
|
const outputSchema = ReferenceOutputSchemas[detail];
|
|
77
76
|
return formattedResult(outputSchema, structuredContent, output_format);
|
|
78
77
|
}
|
|
79
78
|
catch (error) {
|
|
80
|
-
return errorResult(error);
|
|
79
|
+
return errorResult(error, createToolErrorContext("ast_find_references", project_root));
|
|
81
80
|
}
|
|
82
81
|
});
|
|
83
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
|
}
|
package/dist/tools/get_file.js
CHANGED
|
@@ -2,7 +2,9 @@ import { z } from "zod";
|
|
|
2
2
|
import { DEFAULT_FILE_LINE_LIMIT, MAX_FILE_LINE_LIMIT, readFileSnapshot, } from "../services/file-snapshot.js";
|
|
3
3
|
import { buildFileOutline } from "../services/outline.js";
|
|
4
4
|
import { getSourceFileOrThrow, withProject } from "../services/project.js";
|
|
5
|
-
import {
|
|
5
|
+
import { createRequestContext } from "../services/request-context.js";
|
|
6
|
+
import { FRESHNESS_CAUSES, SNAPSHOT_STATES } from "../services/read-contracts.js";
|
|
7
|
+
import { createToolErrorContext, errorResult, structuredResult } from "./result.js";
|
|
6
8
|
const AstGetFileInputSchema = z.object({
|
|
7
9
|
project_root: z
|
|
8
10
|
.string()
|
|
@@ -44,6 +46,11 @@ const FileSnapshotBaseSchema = {
|
|
|
44
46
|
file: z.string(),
|
|
45
47
|
file_hash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
46
48
|
snapshot_state: z.enum(["fresh", "stale"]),
|
|
49
|
+
freshness: z.object({
|
|
50
|
+
state: z.enum(SNAPSHOT_STATES),
|
|
51
|
+
causes: z.array(z.enum(FRESHNESS_CAUSES)),
|
|
52
|
+
checked_at: z.string().nullable(),
|
|
53
|
+
}),
|
|
47
54
|
};
|
|
48
55
|
const AstGetFileOutputSchema = z.object({
|
|
49
56
|
mode: z.enum(["source", "symbols_only"]),
|
|
@@ -55,7 +62,7 @@ const AstGetFileOutputSchema = z.object({
|
|
|
55
62
|
export function registerGetFile(server) {
|
|
56
63
|
server.registerTool("ast_get_file", {
|
|
57
64
|
title: "Get bounded source file",
|
|
58
|
-
description: "Returns exact bounded source lines for one TypeScript/JavaScript file, or body-free symbols when symbols_only is true. Read-only; freshness
|
|
65
|
+
description: "Returns exact bounded source lines for one TypeScript/JavaScript file, or body-free symbols when symbols_only is true. Read-only; file snapshot freshness and project freshness are reported separately from compiler diagnostics.",
|
|
59
66
|
inputSchema: AstGetFileInputSchema,
|
|
60
67
|
outputSchema: AstGetFileOutputSchema,
|
|
61
68
|
annotations: {
|
|
@@ -64,17 +71,21 @@ export function registerGetFile(server) {
|
|
|
64
71
|
idempotentHint: true,
|
|
65
72
|
openWorldHint: false,
|
|
66
73
|
},
|
|
67
|
-
}, async ({ project_root, file_path, offset, limit, symbols_only }) => {
|
|
74
|
+
}, async ({ project_root, file_path, offset, limit, symbols_only }, extra) => {
|
|
75
|
+
const requestContext = createRequestContext(extra.signal);
|
|
68
76
|
try {
|
|
69
|
-
const structuredContent = await withProject(project_root, async ({ project, projectRoot }) => {
|
|
70
|
-
const snapshot = await readFileSnapshot(project, projectRoot, file_path, {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
const structuredContent = await withProject(project_root, async ({ project, projectRoot, status }, operationContext) => {
|
|
78
|
+
const snapshot = await readFileSnapshot(project, projectRoot, file_path, { offset, limit }, operationContext);
|
|
79
|
+
const freshness = {
|
|
80
|
+
state: status.state,
|
|
81
|
+
causes: status.causes,
|
|
82
|
+
checked_at: status.lastSuccessfulSyncAt,
|
|
83
|
+
};
|
|
74
84
|
if (!symbols_only) {
|
|
75
85
|
return {
|
|
76
86
|
mode: "source",
|
|
77
87
|
...snapshot,
|
|
88
|
+
freshness,
|
|
78
89
|
};
|
|
79
90
|
}
|
|
80
91
|
const sourceFile = getSourceFileOrThrow(project, file_path);
|
|
@@ -83,13 +94,14 @@ export function registerGetFile(server) {
|
|
|
83
94
|
file: snapshot.file,
|
|
84
95
|
file_hash: snapshot.file_hash,
|
|
85
96
|
snapshot_state: snapshot.snapshot_state,
|
|
86
|
-
|
|
97
|
+
freshness,
|
|
98
|
+
symbols: buildFileOutline(sourceFile, operationContext).symbols,
|
|
87
99
|
};
|
|
88
|
-
});
|
|
100
|
+
}, requestContext);
|
|
89
101
|
return structuredResult(structuredContent);
|
|
90
102
|
}
|
|
91
103
|
catch (error) {
|
|
92
|
-
return errorResult(error);
|
|
104
|
+
return errorResult(error, createToolErrorContext("ast_get_file", project_root));
|
|
93
105
|
}
|
|
94
106
|
});
|
|
95
107
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
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 { FRESHNESS_CAUSES, SNAPSHOT_STATES, TRUNCATION_REASONS, } from "../services/read-contracts.js";
|
|
4
|
+
import { withProject } from "../services/project.js";
|
|
5
|
+
import { createRequestContext } from "../services/request-context.js";
|
|
6
|
+
import { RELATIONSHIP_CONFIDENCES, RELATIONSHIP_EDGE_KINDS, RELATIONSHIP_PROVENANCES, RELATIONSHIP_RESOLUTIONS, } from "../services/relationships.js";
|
|
7
|
+
import { createToolErrorContext, errorResult, formattedResult, ToolOutputFormatInputSchema, } from "./result.js";
|
|
8
|
+
const AstGetImpactInputSchema = z.object({
|
|
9
|
+
project_root: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe("Absolute project directory containing tsconfig.json, or the config path."),
|
|
12
|
+
file_path: z.string().describe("Project-relative file containing the exact impact root."),
|
|
13
|
+
symbol_path: z.string().describe("Exact symbol path returned by an outline or symbol search."),
|
|
14
|
+
direction: z
|
|
15
|
+
.enum(IMPACT_DIRECTIONS)
|
|
16
|
+
.default("both")
|
|
17
|
+
.describe("Traversal direction relative to the root symbol."),
|
|
18
|
+
max_depth: z
|
|
19
|
+
.number()
|
|
20
|
+
.int()
|
|
21
|
+
.min(0)
|
|
22
|
+
.max(MAX_IMPACT_DEPTH)
|
|
23
|
+
.default(DEFAULT_IMPACT_MAX_DEPTH)
|
|
24
|
+
.describe("Maximum relationship depth, including zero for the root only."),
|
|
25
|
+
max_nodes: z
|
|
26
|
+
.number()
|
|
27
|
+
.int()
|
|
28
|
+
.min(1)
|
|
29
|
+
.max(MAX_IMPACT_NODES)
|
|
30
|
+
.default(DEFAULT_IMPACT_MAX_NODES)
|
|
31
|
+
.describe("Maximum returned impact nodes, including the root."),
|
|
32
|
+
max_edges: z
|
|
33
|
+
.number()
|
|
34
|
+
.int()
|
|
35
|
+
.min(1)
|
|
36
|
+
.max(MAX_IMPACT_EDGES)
|
|
37
|
+
.default(DEFAULT_IMPACT_MAX_EDGES)
|
|
38
|
+
.describe("Maximum returned relationship edges."),
|
|
39
|
+
relationship_kinds: z
|
|
40
|
+
.array(z.enum(RELATIONSHIP_EDGE_KINDS))
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Optional relationship kinds to traverse; defaults to all normalized kinds."),
|
|
43
|
+
...ToolOutputFormatInputSchema,
|
|
44
|
+
});
|
|
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
|
+
const ImpactOutputSchema = z.object({
|
|
74
|
+
root: RelationshipEndpointSchema,
|
|
75
|
+
direction: z.enum(IMPACT_DIRECTIONS),
|
|
76
|
+
relationship_kinds: z.array(z.enum(RELATIONSHIP_EDGE_KINDS)),
|
|
77
|
+
nodes: z.array(z.object({
|
|
78
|
+
endpoint: RelationshipEndpointSchema,
|
|
79
|
+
depth: z.number().int().min(0),
|
|
80
|
+
direct: z.boolean(),
|
|
81
|
+
})),
|
|
82
|
+
edges: z.array(RelationshipEdgeSchema),
|
|
83
|
+
visited_nodes: z.number().int().min(0),
|
|
84
|
+
visited_edges: z.number().int().min(0),
|
|
85
|
+
max_depth_reached: z.number().int().min(0),
|
|
86
|
+
max_depth: z.number().int().min(0),
|
|
87
|
+
max_nodes: z.number().int().positive(),
|
|
88
|
+
max_edges: z.number().int().positive(),
|
|
89
|
+
incomplete: z.boolean(),
|
|
90
|
+
truncation: z.object({
|
|
91
|
+
truncated: z.boolean(),
|
|
92
|
+
reason: z.enum(TRUNCATION_REASONS).nullable(),
|
|
93
|
+
}),
|
|
94
|
+
truncation_reasons: z.array(z.enum(TRUNCATION_REASONS)),
|
|
95
|
+
freshness: FreshnessSchema,
|
|
96
|
+
});
|
|
97
|
+
export function registerGetImpact(server) {
|
|
98
|
+
server.registerTool("ast_get_impact", {
|
|
99
|
+
title: "Get bounded compiler-backed impact",
|
|
100
|
+
description: "Traverses exact compiler-backed relationships around a symbol with explicit direction, depth, node and edge budgets. Returns bounded session freshness metadata and fails closed when exact relationships are not fresh. Read-only evidence only; it never prepares or applies mutations.",
|
|
101
|
+
inputSchema: AstGetImpactInputSchema,
|
|
102
|
+
outputSchema: ImpactOutputSchema,
|
|
103
|
+
annotations: {
|
|
104
|
+
readOnlyHint: true,
|
|
105
|
+
destructiveHint: false,
|
|
106
|
+
idempotentHint: true,
|
|
107
|
+
openWorldHint: false,
|
|
108
|
+
},
|
|
109
|
+
}, async ({ project_root, file_path, symbol_path, direction, max_depth, max_nodes, max_edges, relationship_kinds, output_format, }, extra) => {
|
|
110
|
+
const requestContext = createRequestContext(extra.signal);
|
|
111
|
+
try {
|
|
112
|
+
const structuredContent = await withProject(project_root, (context, operationContext) => {
|
|
113
|
+
if (context.status.state !== "fresh") {
|
|
114
|
+
throw new Error("Compiler-backed impact evidence is not fresh. Retry the read.");
|
|
115
|
+
}
|
|
116
|
+
const freshness = {
|
|
117
|
+
state: context.status.state,
|
|
118
|
+
causes: context.status.causes,
|
|
119
|
+
checked_at: context.status.lastSuccessfulSyncAt,
|
|
120
|
+
};
|
|
121
|
+
const root = resolveImpactRoot(context.project, context.projectRoot, { file_path, symbol_path }, operationContext);
|
|
122
|
+
const impact = traverseCompilerImpact(context.project, context.projectRoot, root, freshness, { direction, max_depth, max_nodes, max_edges, relationship_kinds }, operationContext);
|
|
123
|
+
return {
|
|
124
|
+
...impact,
|
|
125
|
+
freshness,
|
|
126
|
+
};
|
|
127
|
+
}, requestContext);
|
|
128
|
+
return formattedResult(ImpactOutputSchema, structuredContent, output_format);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
return errorResult(error, createToolErrorContext("ast_get_impact", project_root));
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|