ast-mcp-server 0.15.1 → 0.17.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 +48 -1
- package/README.md +11 -4
- package/cordis.patch.yml +1 -1
- package/dist/angular-template-typecheck-entry.js +19 -0
- package/dist/services/angular-decorators.js +218 -0
- package/dist/services/angular-package-resolution.js +62 -0
- package/dist/services/angular-template-bindings.js +111 -0
- package/dist/services/angular-template-rename.js +119 -0
- package/dist/services/angular-template-scope.js +163 -0
- package/dist/services/angular-template-typecheck-core.js +139 -0
- package/dist/services/angular-template-typecheck-protocol.js +75 -0
- package/dist/services/angular-template-typecheck.js +262 -0
- package/dist/services/angular-templates.js +273 -103
- package/dist/services/h03-timeout-fixture.js +2 -1
- package/dist/services/operation-block-policy.js +19 -0
- package/dist/services/operation-plan-file.js +38 -4
- package/dist/services/operations.js +109 -22
- package/dist/services/project-operation-scheduler.js +4 -0
- package/dist/tools/find_references.js +2 -1
- package/dist/tools/operation-schema.js +43 -0
- package/dist/tools/rename_symbol.js +9 -4
- package/package.json +9 -4
- package/skills/structural-code-editing/SKILL.md +11 -5
- package/skills/structural-code-editing/references/operations.md +4 -2
- package/skills/structural-code-editing/releases.json +113 -7
|
@@ -4,7 +4,11 @@ import { open, readFile, stat } from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createTwoFilesPatch } from "diff";
|
|
6
6
|
import { LanguageVariant, Node, ScriptTarget, SyntaxKind, ts } from "ts-morph";
|
|
7
|
+
import { isPipeClass } from "./angular-decorators.js";
|
|
8
|
+
import { planTemplateEdits } from "./angular-template-rename.js";
|
|
7
9
|
import { collectTemplateUsages } from "./angular-templates.js";
|
|
10
|
+
import { checkTemplateTypes } from "./angular-template-typecheck.js";
|
|
11
|
+
import { evaluateBlock } from "./operation-block-policy.js";
|
|
8
12
|
import { findNonDeclarationReferences } from "./references.js";
|
|
9
13
|
import { compareObservedDiagnostics, observeDiagnostic, } from "./diagnostics.js";
|
|
10
14
|
import { probePublicationCapability, publishAuthenticated, removeOwnedPublicationEntry, rollbackOwnedCommit, snapshotHeldFile, } from "./authenticated-publication.js";
|
|
@@ -45,6 +49,7 @@ function planHashFor(operation, version = 2) {
|
|
|
45
49
|
diagnostics: operation.diagnostics,
|
|
46
50
|
allow_new_errors: operation.allow_new_errors,
|
|
47
51
|
blocked: operation.blocked,
|
|
52
|
+
...(version === 3 ? templateHashFields(operation) : {}),
|
|
48
53
|
files: operation.files.map((file) => ({
|
|
49
54
|
file: file.file,
|
|
50
55
|
original_hash: file.originalHash,
|
|
@@ -53,6 +58,27 @@ function planHashFor(operation, version = 2) {
|
|
|
53
58
|
})),
|
|
54
59
|
}));
|
|
55
60
|
}
|
|
61
|
+
/** The version 3 additions, in a fixed key order so equal deltas always hash equal. */
|
|
62
|
+
function templateHashFields(operation) {
|
|
63
|
+
const delta = operation.template_diagnostics;
|
|
64
|
+
if (delta === undefined || operation.block_on_template_errors === undefined) {
|
|
65
|
+
throw new Error("A version 3 plan requires template_diagnostics and block_on_template_errors.");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
template_diagnostics: {
|
|
69
|
+
status: delta.status,
|
|
70
|
+
...(delta.status === "completed" ? {} : { reason: delta.reason }),
|
|
71
|
+
added: delta.added,
|
|
72
|
+
removed: delta.removed,
|
|
73
|
+
addedErrors: delta.addedErrors,
|
|
74
|
+
},
|
|
75
|
+
block_on_template_errors: operation.block_on_template_errors,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Plans without a template check keep hashing as version 2; only a check upgrades them. */
|
|
79
|
+
function planVersionOf(operation) {
|
|
80
|
+
return operation.template_diagnostics === undefined ? 2 : 3;
|
|
81
|
+
}
|
|
56
82
|
function pruneOperations() {
|
|
57
83
|
const now = operationNow();
|
|
58
84
|
for (const [id, operation] of operations) {
|
|
@@ -214,8 +240,22 @@ async function planAuxiliaryFiles(edits, projectRoot, snapshotFiles, requestCont
|
|
|
214
240
|
}
|
|
215
241
|
return planned;
|
|
216
242
|
}
|
|
243
|
+
/** Maps the planned files to the check input; paths are relative to the tsconfig directory. */
|
|
244
|
+
function templateCheckInput(tsConfigFilePath, files, auxiliaryFiles) {
|
|
245
|
+
const base = path.dirname(tsConfigFilePath);
|
|
246
|
+
return {
|
|
247
|
+
tsConfigFilePath,
|
|
248
|
+
templates: auxiliaryFiles.map((file) => file.absolutePath),
|
|
249
|
+
overlay: files.map((file) => ({ absolutePath: file.absolutePath, text: file.updatedText })),
|
|
250
|
+
changes: files.map((file) => ({
|
|
251
|
+
file: path.relative(base, file.absolutePath),
|
|
252
|
+
beforeText: isCreatedFile(file) ? null : file.originalText,
|
|
253
|
+
afterText: file.updatedText,
|
|
254
|
+
})),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
217
257
|
/** Internal seam shared by the prepare functions; exported so tests can drive `mutate` directly. */
|
|
218
|
-
export async function createPlan(projectRootInput, kind, allowNewErrors, mutate, requestContext) {
|
|
258
|
+
export async function createPlan(projectRootInput, kind, allowNewErrors, mutate, requestContext, planOptions = {}) {
|
|
219
259
|
requestContext.checkpoint();
|
|
220
260
|
let retainedOperationId;
|
|
221
261
|
return withProjectOperation(projectRootInput, async (operationContext) => {
|
|
@@ -314,13 +354,25 @@ export async function createPlan(projectRootInput, kind, allowNewErrors, mutate,
|
|
|
314
354
|
postWorkspaceFiles.set(file.absolutePath, file.updatedHash);
|
|
315
355
|
}
|
|
316
356
|
const postWorkspaceHash = hashWorkspaceFiles(postWorkspaceFiles);
|
|
357
|
+
// Only a rename that edits templates is checked; the check runs inside the project lock and
|
|
358
|
+
// before the digest check below, which proves the child's disk reads saw an unchanged tree.
|
|
359
|
+
const templateDiagnostics = kind === "rename_symbol" && auxiliaryFiles.length > 0
|
|
360
|
+
? await checkTemplateTypes(templateCheckInput(context.tsConfigFilePath, files, auxiliaryFiles), operationContext)
|
|
361
|
+
: undefined;
|
|
362
|
+
operationContext.checkpoint();
|
|
317
363
|
const workspaceAfter = createWorkspaceSnapshot(createFreshProject(context.tsConfigFilePath));
|
|
318
364
|
operationContext.checkpoint();
|
|
319
365
|
if (workspaceAfter.digest !== workspaceBefore.digest) {
|
|
320
366
|
throw new Error("Workspace changed while the operation was being prepared. Retry preparation.");
|
|
321
367
|
}
|
|
322
368
|
const fullPreview = files.map((file) => file.diff).join("\n");
|
|
323
|
-
const
|
|
369
|
+
const blockOnTemplateErrors = planOptions.blockOnTemplateErrors ?? false;
|
|
370
|
+
const verdict = evaluateBlock({
|
|
371
|
+
allowNewErrors,
|
|
372
|
+
typescriptErrors: diagnosticDelta.addedErrors.length,
|
|
373
|
+
template: templateDiagnostics,
|
|
374
|
+
blockOnTemplateErrors,
|
|
375
|
+
});
|
|
324
376
|
const now = operationNow();
|
|
325
377
|
const operationId = randomUUID();
|
|
326
378
|
const recordWithoutHash = {
|
|
@@ -342,17 +394,21 @@ export async function createPlan(projectRootInput, kind, allowNewErrors, mutate,
|
|
|
342
394
|
workspace_file_count: workspaceBefore.fileCount,
|
|
343
395
|
diagnostics: diagnosticDelta,
|
|
344
396
|
allow_new_errors: allowNewErrors,
|
|
345
|
-
blocked,
|
|
346
|
-
block_reason:
|
|
347
|
-
? `${diagnosticDelta.addedErrors.length} new TypeScript error(s) would be introduced. Prepare again with allow_new_errors=true only after explicit review.`
|
|
348
|
-
: null,
|
|
397
|
+
blocked: verdict.blocked,
|
|
398
|
+
block_reason: verdict.reason,
|
|
349
399
|
preview: fullPreview.length <= MAX_INLINE_PREVIEW_CHARS ? fullPreview : null,
|
|
350
400
|
preview_truncated: fullPreview.length > MAX_INLINE_PREVIEW_CHARS,
|
|
401
|
+
...(templateDiagnostics === undefined
|
|
402
|
+
? {}
|
|
403
|
+
: {
|
|
404
|
+
template_diagnostics: templateDiagnostics,
|
|
405
|
+
block_on_template_errors: blockOnTemplateErrors,
|
|
406
|
+
}),
|
|
351
407
|
files,
|
|
352
408
|
};
|
|
353
409
|
const record = {
|
|
354
410
|
...recordWithoutHash,
|
|
355
|
-
plan_hash: planHashFor(recordWithoutHash),
|
|
411
|
+
plan_hash: planHashFor(recordWithoutHash, planVersionOf(recordWithoutHash)),
|
|
356
412
|
};
|
|
357
413
|
operationContext.checkpoint();
|
|
358
414
|
operations.set(operationId, record);
|
|
@@ -386,12 +442,18 @@ function publicOperation(record) {
|
|
|
386
442
|
block_reason: record.block_reason,
|
|
387
443
|
preview: record.preview,
|
|
388
444
|
preview_truncated: record.preview_truncated,
|
|
445
|
+
...(record.template_diagnostics === undefined
|
|
446
|
+
? {}
|
|
447
|
+
: {
|
|
448
|
+
template_diagnostics: record.template_diagnostics,
|
|
449
|
+
block_on_template_errors: record.block_on_template_errors,
|
|
450
|
+
}),
|
|
389
451
|
};
|
|
390
452
|
}
|
|
391
453
|
const TEMPLATE_ANY_CAST = "$any";
|
|
392
454
|
/** Fails closed before any edit when template evidence is incomplete or the new name would be captured. */
|
|
393
|
-
function assertTemplateRenameAllowed(coverageStatus, usages, newName) {
|
|
394
|
-
if (coverageStatus === "unfinished" || coverageStatus === "unsupported") {
|
|
455
|
+
function assertTemplateRenameAllowed(coverageStatus, usages, newName, renameBlocked = false) {
|
|
456
|
+
if (renameBlocked || coverageStatus === "unfinished" || coverageStatus === "unsupported") {
|
|
395
457
|
throw new PublicOperationalError("INCOMPLETE_EVIDENCE", "Template coverage is incomplete, so the rename cannot be proven safe.");
|
|
396
458
|
}
|
|
397
459
|
if (usages.some((usage) => usage.container === "inline")) {
|
|
@@ -417,24 +479,36 @@ export async function prepareRename(args, requestContext = NO_REQUEST_CONTEXT) {
|
|
|
417
479
|
if (!Node.isRenameable(node)) {
|
|
418
480
|
throw new Error(`Symbol "${args.symbolPath}" (${node.getKindName()}) does not support structural rename.`);
|
|
419
481
|
}
|
|
482
|
+
// Templates bind a pipe by its `name` literal, never by class, so a class rename has no
|
|
483
|
+
// template edit and needs no template evidence.
|
|
484
|
+
const pipeClass = Node.isClassDeclaration(node) && isPipeClass(node);
|
|
420
485
|
// Template usages must be collected before the rename mutates the program.
|
|
421
|
-
const { coverage, usages } =
|
|
422
|
-
|
|
486
|
+
const { coverage, usages, renameBlocked } = pipeClass
|
|
487
|
+
? {
|
|
488
|
+
coverage: { status: "not_applicable" },
|
|
489
|
+
usages: [],
|
|
490
|
+
renameBlocked: false,
|
|
491
|
+
}
|
|
492
|
+
: await collectTemplateUsages(node, context, rc);
|
|
493
|
+
assertTemplateRenameAllowed(coverage.status, usages, args.newName, renameBlocked);
|
|
494
|
+
const edits = coverage.status === "completed" || usages.length > 0
|
|
495
|
+
? planTemplateEdits(node, usages, args.newName, context)
|
|
496
|
+
: [];
|
|
423
497
|
const tsReferenceCount = Node.isReferenceFindable(node)
|
|
424
498
|
? findNonDeclarationReferences(node).length
|
|
425
499
|
: 0;
|
|
426
500
|
node.rename(args.newName);
|
|
427
501
|
return {
|
|
428
|
-
referenceCount: tsReferenceCount +
|
|
429
|
-
auxiliaryEdits:
|
|
502
|
+
referenceCount: tsReferenceCount + edits.length,
|
|
503
|
+
auxiliaryEdits: edits.map(({ usage, newText }) => ({
|
|
430
504
|
absolutePath: usage.absolutePath,
|
|
431
505
|
start: usage.start,
|
|
432
506
|
end: usage.end,
|
|
433
|
-
newText
|
|
507
|
+
newText,
|
|
434
508
|
expectedHash: usage.fileHash,
|
|
435
509
|
})),
|
|
436
510
|
};
|
|
437
|
-
}, requestContext);
|
|
511
|
+
}, requestContext, { blockOnTemplateErrors: args.blockOnTemplateErrors });
|
|
438
512
|
}
|
|
439
513
|
export async function prepareReplaceBody(args, requestContext = NO_REQUEST_CONTEXT) {
|
|
440
514
|
return createPlan(args.projectRoot, "replace_symbol_body", args.allowNewErrors ?? false, async ({ project }) => {
|
|
@@ -968,7 +1042,7 @@ export function exportOperationRecord(operationId) {
|
|
|
968
1042
|
})),
|
|
969
1043
|
};
|
|
970
1044
|
}
|
|
971
|
-
export function importOperationRecord(persisted, expectedPlanHash, planVersion =
|
|
1045
|
+
export function importOperationRecord(persisted, expectedPlanHash, planVersion = planVersionOf(persisted)) {
|
|
972
1046
|
if (persisted.plan_hash !== expectedPlanHash) {
|
|
973
1047
|
throw new Error(`Plan hash mismatch for operation "${persisted.operation_id}". No files were written.`);
|
|
974
1048
|
}
|
|
@@ -1033,18 +1107,31 @@ export function importOperationRecord(persisted, expectedPlanHash, planVersion =
|
|
|
1033
1107
|
if (JSON.stringify(addedErrors) !== JSON.stringify(persisted.diagnostics.addedErrors)) {
|
|
1034
1108
|
throw new Error("Persisted diagnostic error delta is inconsistent.");
|
|
1035
1109
|
}
|
|
1036
|
-
const
|
|
1037
|
-
if (
|
|
1110
|
+
const templateDelta = persisted.template_diagnostics;
|
|
1111
|
+
if (templateDelta !== undefined) {
|
|
1112
|
+
const templateErrors = templateDelta.added.filter((diagnostic) => diagnostic.category === "Error");
|
|
1113
|
+
if (JSON.stringify(templateErrors) !== JSON.stringify(templateDelta.addedErrors)) {
|
|
1114
|
+
throw new Error("Persisted template diagnostic error delta is inconsistent.");
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
if ((planVersion === 3) !== (templateDelta !== undefined)) {
|
|
1118
|
+
throw new Error("Persisted template diagnostics are inconsistent with the plan version.");
|
|
1119
|
+
}
|
|
1120
|
+
const verdict = evaluateBlock({
|
|
1121
|
+
allowNewErrors: persisted.allow_new_errors,
|
|
1122
|
+
typescriptErrors: addedErrors.length,
|
|
1123
|
+
template: templateDelta,
|
|
1124
|
+
blockOnTemplateErrors: persisted.block_on_template_errors ?? false,
|
|
1125
|
+
});
|
|
1126
|
+
if (persisted.blocked !== verdict.blocked) {
|
|
1038
1127
|
throw new Error("Persisted blocked status is inconsistent with diagnostics policy.");
|
|
1039
1128
|
}
|
|
1040
1129
|
const fullPreview = files.map((file) => file.diff).join("\n");
|
|
1041
1130
|
const record = {
|
|
1042
1131
|
...persisted,
|
|
1043
1132
|
affected_files: affectedFiles,
|
|
1044
|
-
blocked,
|
|
1045
|
-
block_reason:
|
|
1046
|
-
? `${addedErrors.length} new TypeScript error(s) would be introduced. Prepare again with allow_new_errors=true only after explicit review.`
|
|
1047
|
-
: null,
|
|
1133
|
+
blocked: verdict.blocked,
|
|
1134
|
+
block_reason: verdict.reason,
|
|
1048
1135
|
preview: fullPreview.length <= MAX_INLINE_PREVIEW_CHARS ? fullPreview : null,
|
|
1049
1136
|
preview_truncated: fullPreview.length > MAX_INLINE_PREVIEW_CHARS,
|
|
1050
1137
|
tsConfigFilePath: context.tsConfigFilePath,
|
|
@@ -354,6 +354,10 @@ export class ProjectOperationScheduler {
|
|
|
354
354
|
checkpoint: () => {
|
|
355
355
|
this.#checkpoint(node);
|
|
356
356
|
},
|
|
357
|
+
remainingMs: () => {
|
|
358
|
+
const startedAt = node.executionStartedAt ?? this.#readNow();
|
|
359
|
+
return Math.max(0, this.#operationDeadlineMs - (this.#readNow() - startedAt));
|
|
360
|
+
},
|
|
357
361
|
markExecuting: () => {
|
|
358
362
|
this.#checkpoint(node);
|
|
359
363
|
if (node.phase !== "synchronizing") {
|
|
@@ -41,6 +41,7 @@ const TemplateCoverageSchema = z.object({
|
|
|
41
41
|
"size_exceeded",
|
|
42
42
|
"work_exhausted",
|
|
43
43
|
"inherited_member",
|
|
44
|
+
"cross_component",
|
|
44
45
|
])
|
|
45
46
|
.optional(),
|
|
46
47
|
templates: z.number().int().min(0).optional(),
|
|
@@ -68,7 +69,7 @@ const ReferenceOutputSchemas = {
|
|
|
68
69
|
export function registerFindReferences(server) {
|
|
69
70
|
server.registerTool(TOOL_NAME, {
|
|
70
71
|
title: "Find semantic symbol references",
|
|
71
|
-
description: "Finds type-resolved references and returns bounded project-relative locations, including declaration impact by default. When @angular/compiler resolves, it also reports Angular template usages tagged by origin with a template_coverage verdict; incomplete coverage does not prove absence
|
|
72
|
+
description: "Finds type-resolved references and returns bounded project-relative locations, including declaration impact by default. When @angular/compiler resolves, it also reports Angular template usages, including consumer templates, tagged by origin with a template_coverage verdict; completed covers templates inside the Project only, incomplete coverage does not prove absence. It reports usages only; template type errors are reported by ast_rename_symbol plans in template_diagnostics.",
|
|
72
73
|
inputSchema: AstFindReferencesInputSchema,
|
|
73
74
|
annotations: {
|
|
74
75
|
readOnlyHint: true,
|
|
@@ -34,6 +34,46 @@ export const PreparedOperationOutputSchema = z.object({
|
|
|
34
34
|
preview: z.string().nullable(),
|
|
35
35
|
preview_truncated: z.boolean(),
|
|
36
36
|
});
|
|
37
|
+
const TemplateDiagnosticsListSchema = z.array(NormalizedDiagnosticSchema);
|
|
38
|
+
const NonCompletedTemplateListSchema = z.array(NormalizedDiagnosticSchema).max(0);
|
|
39
|
+
/** Template type-check delta of a rename; present only when the check ran or tried to. */
|
|
40
|
+
export const TemplateDiagnosticsSchema = z.discriminatedUnion("status", [
|
|
41
|
+
z
|
|
42
|
+
.object({
|
|
43
|
+
status: z.literal("completed"),
|
|
44
|
+
added: TemplateDiagnosticsListSchema,
|
|
45
|
+
removed: TemplateDiagnosticsListSchema,
|
|
46
|
+
added_errors: TemplateDiagnosticsListSchema,
|
|
47
|
+
})
|
|
48
|
+
.strict(),
|
|
49
|
+
z
|
|
50
|
+
.object({
|
|
51
|
+
status: z.enum(["unavailable", "unfinished"]),
|
|
52
|
+
reason: z.enum([
|
|
53
|
+
"compiler_cli_missing",
|
|
54
|
+
"compiler_unsupported",
|
|
55
|
+
"typescript_incompatible",
|
|
56
|
+
"work_exhausted",
|
|
57
|
+
"deadline",
|
|
58
|
+
"check_failed",
|
|
59
|
+
]),
|
|
60
|
+
added: NonCompletedTemplateListSchema,
|
|
61
|
+
removed: NonCompletedTemplateListSchema,
|
|
62
|
+
added_errors: NonCompletedTemplateListSchema,
|
|
63
|
+
})
|
|
64
|
+
.strict(),
|
|
65
|
+
]);
|
|
66
|
+
/** Only `ast_rename_symbol` can carry template diagnostics, so only its contract changes. */
|
|
67
|
+
export const RenameOperationOutputSchema = PreparedOperationOutputSchema.extend({
|
|
68
|
+
template_diagnostics: TemplateDiagnosticsSchema.optional(),
|
|
69
|
+
});
|
|
70
|
+
/** The one place that turns the record shape (`addedErrors`) into the wire shape. */
|
|
71
|
+
export function toTemplateDiagnosticsOutput(delta) {
|
|
72
|
+
const lists = { added: delta.added, removed: delta.removed, added_errors: delta.addedErrors };
|
|
73
|
+
return delta.status === "completed"
|
|
74
|
+
? { status: "completed", ...lists }
|
|
75
|
+
: { status: delta.status, reason: delta.reason, ...lists };
|
|
76
|
+
}
|
|
37
77
|
export function serializePreparedOperation(operation) {
|
|
38
78
|
if (operation.status !== "prepared") {
|
|
39
79
|
throw new Error(`Operation ${operation.operation_id} is not in prepared state.`);
|
|
@@ -60,5 +100,8 @@ export function serializePreparedOperation(operation) {
|
|
|
60
100
|
block_reason: operation.block_reason,
|
|
61
101
|
preview: operation.preview,
|
|
62
102
|
preview_truncated: operation.preview_truncated,
|
|
103
|
+
...(operation.template_diagnostics === undefined
|
|
104
|
+
? {}
|
|
105
|
+
: { template_diagnostics: toTemplateDiagnosticsOutput(operation.template_diagnostics) }),
|
|
63
106
|
};
|
|
64
107
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { prepareRename } from "../services/operations.js";
|
|
3
3
|
import { createRequestContext } from "../services/request-context.js";
|
|
4
|
-
import {
|
|
4
|
+
import { RenameOperationOutputSchema, serializePreparedOperation } from "./operation-schema.js";
|
|
5
5
|
import { createToolErrorContext, errorResult, structuredResult } from "./result.js";
|
|
6
6
|
const TOOL_NAME = "ast_rename_symbol";
|
|
7
7
|
const AstRenameSymbolInputSchema = z.object({
|
|
@@ -17,20 +17,24 @@ const AstRenameSymbolInputSchema = z.object({
|
|
|
17
17
|
.boolean()
|
|
18
18
|
.default(false)
|
|
19
19
|
.describe("Permit a plan with new TypeScript errors after explicit review."),
|
|
20
|
+
block_on_template_errors: z
|
|
21
|
+
.boolean()
|
|
22
|
+
.default(false)
|
|
23
|
+
.describe("Block the plan when a completed template check adds template errors. allow_new_errors still waives it."),
|
|
20
24
|
});
|
|
21
25
|
export function registerRenameSymbol(server) {
|
|
22
26
|
server.registerTool(TOOL_NAME, {
|
|
23
27
|
title: "Prepare a project-wide structural rename",
|
|
24
|
-
description: "Prepares an exact hash-bound rename plan without writing. Review the preview and apply its operation_id with ast_apply_operation. The plan also rewrites external Angular templates hash-bound to the workspace snapshot
|
|
28
|
+
description: "Prepares an exact hash-bound rename plan without writing. Review the preview and apply its operation_id with ast_apply_operation. The plan also rewrites external Angular templates, including consumer templates when coverage is completed, hash-bound to the workspace snapshot. The diagnostics field stays TypeScript-only; when templates are edited, template_diagnostics reports the template type-check delta separately with a typed status, and blocks only with block_on_template_errors=true.",
|
|
25
29
|
inputSchema: AstRenameSymbolInputSchema,
|
|
26
|
-
outputSchema:
|
|
30
|
+
outputSchema: RenameOperationOutputSchema,
|
|
27
31
|
annotations: {
|
|
28
32
|
readOnlyHint: true,
|
|
29
33
|
destructiveHint: false,
|
|
30
34
|
idempotentHint: false,
|
|
31
35
|
openWorldHint: false,
|
|
32
36
|
},
|
|
33
|
-
}, async ({ project_root, file_path, symbol_path, new_name, dry_run, allow_new_errors }, extra) => {
|
|
37
|
+
}, async ({ project_root, file_path, symbol_path, new_name, dry_run, allow_new_errors, block_on_template_errors, }, extra) => {
|
|
34
38
|
const requestContext = createRequestContext(extra.signal);
|
|
35
39
|
try {
|
|
36
40
|
if (!dry_run) {
|
|
@@ -42,6 +46,7 @@ export function registerRenameSymbol(server) {
|
|
|
42
46
|
symbolPath: symbol_path,
|
|
43
47
|
newName: new_name,
|
|
44
48
|
allowNewErrors: allow_new_errors,
|
|
49
|
+
blockOnTemplateErrors: block_on_template_errors,
|
|
45
50
|
}, requestContext);
|
|
46
51
|
return structuredResult(serializePreparedOperation(operation));
|
|
47
52
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ast-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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",
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
},
|
|
27
27
|
"type": "module",
|
|
28
28
|
"packageManager": "yarn@4.15.0",
|
|
29
|
+
"workspaces": [
|
|
30
|
+
"test/angular-matrix/*"
|
|
31
|
+
],
|
|
29
32
|
"dsh": {
|
|
30
33
|
"bundle": {
|
|
31
34
|
"patch": "./cordis.patch.yml"
|
|
@@ -65,12 +68,14 @@
|
|
|
65
68
|
"hono": "4.13.13",
|
|
66
69
|
"minimatch": "11.0.3",
|
|
67
70
|
"nanoid": "3.3.18",
|
|
68
|
-
"qs": "6.16.1"
|
|
71
|
+
"qs": "6.16.1",
|
|
72
|
+
"@babel/core@npm:7.25.2": "npm:7.29.7",
|
|
73
|
+
"@babel/core@npm:7.26.9": "npm:7.29.7"
|
|
69
74
|
},
|
|
70
75
|
"scripts": {
|
|
71
76
|
"build": "tsc",
|
|
72
77
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
73
|
-
"test": "vitest run --exclude test/supervised-worker.integration.test.ts && yarn build && vitest run test/supervised-worker.integration.test.ts --no-file-parallelism",
|
|
78
|
+
"test": "vitest run --exclude test/supervised-worker.integration.test.ts --exclude test/angular-template-typecheck.integration.test.ts --exclude test/operations-template-typecheck.integration.test.ts && yarn build && vitest run test/supervised-worker.integration.test.ts test/angular-template-typecheck.integration.test.ts test/operations-template-typecheck.integration.test.ts --no-file-parallelism",
|
|
74
79
|
"test:watch": "vitest",
|
|
75
80
|
"test:mcp": "yarn build && node scripts/mcp-smoke.mjs",
|
|
76
81
|
"test:lifecycle": "yarn build && node scripts/mcp-lifecycle-smoke.mjs",
|
|
@@ -135,5 +140,5 @@
|
|
|
135
140
|
"vitest": "^5.0.5",
|
|
136
141
|
"yaml": "^2.9.0"
|
|
137
142
|
},
|
|
138
|
-
"gitHead": "
|
|
143
|
+
"gitHead": "e49987bd1fd36432aa76ecb27ca8359eedb613ad"
|
|
139
144
|
}
|
|
@@ -4,7 +4,7 @@ description: "Trigger: navegar, analizar o editar TypeScript/JavaScript con el c
|
|
|
4
4
|
license: "ISC"
|
|
5
5
|
metadata:
|
|
6
6
|
author: "yail"
|
|
7
|
-
version: "4.
|
|
7
|
+
version: "4.8.0"
|
|
8
8
|
hermes:
|
|
9
9
|
tags: ["typescript", "javascript", "ast", "refactoring", "mcp"]
|
|
10
10
|
homepage: "https://github.com/yailPeralta/ast-mcp-server"
|
|
@@ -45,16 +45,22 @@ For each dimension, `groups.length + omitted_group_count = total_group_count` an
|
|
|
45
45
|
|
|
46
46
|
## Angular templates
|
|
47
47
|
|
|
48
|
-
In an Angular project (`@angular/core` resolvable from the tsconfig directory), `ast_find_references` also reports usages of a component member inside its external `templateUrl` template and inline `template
|
|
48
|
+
In an Angular project (`@angular/core` resolvable from the tsconfig directory), `ast_find_references` also reports usages of a component member inside its external `templateUrl` template and inline `template`, and binding usages in consumer templates. Each item carries `origin` (`compiler`, `template_external` or `template_inline`), and the response adds `template_coverage` and `template_reference_count`. `reference_count` stays TypeScript-only; `total` counts both kinds.
|
|
49
49
|
|
|
50
|
-
Read `template_coverage` before inferring absence. Only `completed` proves every template
|
|
50
|
+
Read `template_coverage` before inferring absence. Only `completed` proves every template it covers was examined. `unfinished` (for example `compiler_missing`, `compiler_unsupported`, `template_unresolved`, `template_parse_error`, `size_exceeded`, `cross_component`, `work_exhausted`) and `unsupported` (for example an inherited member) never prove that no template usage exists. `not_applicable` means the feature is off and the response is unchanged.
|
|
51
51
|
|
|
52
|
-
`
|
|
52
|
+
Consumer templates: for a member bound through the selector (`@Input`, `@Output`, `input()`, `output()`, `model()`, a literal alias, or a literal entry of the `inputs` / `outputs` metadata), the binding keys `[x]`, `x="lit"`, `[(x)]`, `(y)` and `(xChange)` are listed with kinds `TemplateInputBinding` for `[x]`, `TemplateAttribute` for `x="lit"`, `TemplateTwoWayBinding` for `[(x)]` and `TemplateOutputBinding` for `(y)` and `(xChange)` in the templates of components whose resolved scope contains the declaring component (literal standalone `imports`, or NgModule `declarations` plus the `exports` of imported modules). For a `@Pipe` class with a literal `name`, and for its `transform` member, `| name` usages in in-scope templates are listed with kind `TemplatePipeName`. A same selector or pipe name outside that scope is ignored.
|
|
53
|
+
|
|
54
|
+
For these members, `completed` means template evidence inside the Project only: every component template of the Project (the tsconfig passed as `project_root`) was read and every match resolved. `hostDirectives`, `@Directive` inputs and outputs, and non-literal scopes, aliases, metadata or pipe names stay `unfinished` with `cross_component`. Runtime bindings (`setInput`, router input binding, `createComponent` and other dynamic component APIs) and consumers outside the Project (other tsconfigs, libraries, other applications) are never examined, so `completed` does not cover them.
|
|
55
|
+
|
|
56
|
+
`ast_rename_symbol` updates the declaring component's external templates and, for a selector-bound member with `completed` coverage, every external consumer template in the same reviewed plan; it rewrites them only through `ast_apply_operation`. Consumer rewrites keep `[`, `(`, `[(`, `bind-`, `on-`, `bindon-` and the values, a `model()` event key becomes `<name>Change`, and alias-keyed bindings stay. It fails closed: coverage `unfinished` or `unsupported`, `hostDirectives`, `@Directive` members, non-literal aliases and members listed in `inputs` / `outputs` metadata return `INCOMPLETE_EVIDENCE`. An inline template usage (own or consumer), `[(x)]` on a non-`model()` pair, a new name that collides with a template local, another input, output or alias of the directive (or its `Change` name) or a binding of another directive on the same element, `$any`, or a name in the directive's selector return `MUTATION_BLOCKED`. `#ref`/`let-` targets, host bindings and `styleUrls` are not renamed. Apply is sequential, not globally atomic; rollback restores the owned and consumer templates.
|
|
57
|
+
|
|
58
|
+
Pipes: renaming a `@Pipe` class applies with TypeScript edits only; the `name` literal and every `| name` usage stay unchanged. Renaming its `transform` member returns `INCOMPLETE_EVIDENCE`.
|
|
53
59
|
|
|
54
60
|
Limits to state when reporting:
|
|
55
61
|
|
|
56
62
|
- `ast_explore` is TypeScript-only: its references omit template usages. This is tracked in issue #380. Use `ast_find_references` for template evidence; rename still blocks independently.
|
|
57
|
-
-
|
|
63
|
+
- The `diagnostics` field stays TypeScript-only. When a rename edits a template, the plan also carries `template_diagnostics`, a best-effort template type-check delta (`status`, `reason`, `added`, `removed`, `added_errors`) from the project's own `@angular/compiler-cli`; it is absent for non-Angular projects and for renames that edit no template. `completed` is the only status with data; `unavailable` (`compiler_cli_missing`, `compiler_unsupported`, `typescript_incompatible`) and `unfinished` (`work_exhausted`, `deadline`, `check_failed`) mean unknown, so run the project's build or tests before applying. `block_on_template_errors` (default `false`) blocks only a `completed` delta with `added_errors`, and `allow_new_errors` waives it. `template_diagnostics[].file` is relative to the directory of the tsconfig, while `diagnostics[].file` is relative to `project_root`. `@angular/compiler-cli` is not a declared peer and must resolve from the project, otherwise the status is `unavailable/compiler_cli_missing`. Template-editing renames persist as plan schema v3, which is rejected on import after a downgrade.
|
|
58
64
|
- Detection uses the user's own `@angular/compiler`, never a bundled one. It is an optional peer (`>=18.0.0 <23.0.0`) that the project already installs with Angular. If it is missing or out of range, coverage is `unfinished` and rename is blocked. Yarn PnP projects without `node_modules` resolve nothing and report `unfinished`.
|
|
59
65
|
|
|
60
66
|
## Contributor contract
|
|
@@ -22,9 +22,11 @@ Preferir `file_path` relativo; un suffix ambiguo debe devolver candidatos. `ast_
|
|
|
22
22
|
|
|
23
23
|
### Angular templates
|
|
24
24
|
|
|
25
|
-
`ast_find_references` adds template usages (`origin: "template_external" | "template_inline"`, `template_coverage`, `template_reference_count`) only when `@angular/core` resolves; otherwise the response is unchanged. Treat `template_coverage.status` other than `completed` as not proving absence. `
|
|
25
|
+
`ast_find_references` adds template usages (`origin: "template_external" | "template_inline"`, `template_coverage`, `template_reference_count`) only when `@angular/core` resolves; otherwise the response is unchanged. Treat `template_coverage.status` other than `completed` as not proving absence. Selector-bound members (`@Input`, `@Output`, `input()`, `output()`, `model()`, literal aliases, literal `inputs` / `outputs` metadata) also list their bindings in consumer templates (kinds `TemplateInputBinding` for `[x]`, `TemplateAttribute` for `x="lit"`, `TemplateTwoWayBinding` for `[(x)]` and `TemplateOutputBinding` for `(y)` and `(xChange)`), and `@Pipe` classes and their `transform` list `| name` usages (kind `TemplatePipeName`), for components whose resolved standalone or NgModule scope contains the declaring class. For them, `completed` covers templates inside the Project only. `hostDirectives`, `@Directive` inputs and outputs, and non-literal scopes, aliases or names stay `unfinished` with `cross_component`; runtime bindings (`setInput`, router input binding, `createComponent`) and consumers outside the Project are never examined. `ast_explore` is TypeScript-only for references (tracked in issue #380).
|
|
26
26
|
|
|
27
|
-
`
|
|
27
|
+
The `diagnostics` field stays TypeScript-only. When a rename edits a template, the plan also carries `template_diagnostics`, a best-effort template type-check delta (`status`, `reason`, `added`, `removed`, `added_errors`) from the project's own `@angular/compiler-cli`; it is absent for non-Angular projects and for renames that edit no template. `completed` is the only status with data; `unavailable` (`compiler_cli_missing`, `compiler_unsupported`, `typescript_incompatible`) and `unfinished` (`work_exhausted`, `deadline`, `check_failed`) mean unknown, so run the project's build or tests before applying. `block_on_template_errors` (default `false`) blocks only a `completed` delta with `added_errors`, and `allow_new_errors` waives it. `template_diagnostics[].file` is relative to the directory of the tsconfig, while `diagnostics[].file` is relative to `project_root`. `@angular/compiler-cli` is not a declared peer and must resolve from the project, otherwise the status is `unavailable/compiler_cli_missing`. Template-editing renames persist as plan schema v3, which is rejected on import after a downgrade.
|
|
28
|
+
|
|
29
|
+
`ast_rename_symbol` edits external templates, including consumer templates of a selector-bound member with `completed` coverage, and returns `INCOMPLETE_EVIDENCE` (coverage `unfinished` or `unsupported`, `hostDirectives`, `@Directive` members, metadata-listed members, non-literal aliases) or `MUTATION_BLOCKED` (inline usage, template-local or binding-name collision, `[(x)]` on a non-`model()` pair, `$any`, name in the selector). Renaming a `@Pipe` class applies TypeScript edits only and keeps the `name` literal and `| name` usages; renaming `transform` returns `INCOMPLETE_EVIDENCE`. The user's `@angular/compiler` is an optional peer, never bundled; Yarn PnP projects without `node_modules` report `unfinished`. See the Angular templates section of `SKILL.md`.
|
|
28
30
|
|
|
29
31
|
## Trust, freshness y ausencia
|
|
30
32
|
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
"schema_version": 2,
|
|
3
3
|
"algorithm": "sha256",
|
|
4
4
|
"current": {
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.8.0",
|
|
6
6
|
"files": [
|
|
7
7
|
{
|
|
8
8
|
"path": "SKILL.md",
|
|
9
|
-
"sha256": "
|
|
9
|
+
"sha256": "15e5d25553a3ebce86365ff7b43ed61e7df11dd6ad658e83ec6e43cd84118762"
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"path": "references/operations.md",
|
|
13
|
-
"sha256": "
|
|
13
|
+
"sha256": "dd633a24e9aabeb1d9e9d1f93e261906e7b30eb721fadfb3d317d6acc60db2ca"
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
},
|
|
@@ -82,23 +82,129 @@
|
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
"version": "4.5.0",
|
|
85
|
-
"
|
|
85
|
+
"files": [
|
|
86
|
+
{
|
|
87
|
+
"path": "SKILL.md",
|
|
88
|
+
"sha256": "be757e8bd98733c7ee952d63a2805dcd9877becf651b4fcb6ab63d20f6546f21"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"path": "references/operations.md",
|
|
92
|
+
"sha256": "d52f01637e33e65ce523d19a810ce9488ab92228809768ec66a584120c24ad98"
|
|
93
|
+
}
|
|
94
|
+
],
|
|
86
95
|
"npm_versions": ["0.11.0"]
|
|
87
96
|
},
|
|
88
97
|
{
|
|
89
98
|
"version": "4.5.0",
|
|
90
|
-
"
|
|
99
|
+
"files": [
|
|
100
|
+
{
|
|
101
|
+
"path": "SKILL.md",
|
|
102
|
+
"sha256": "2729b30ff6b7344e6680c7ee02ab540bc6db9774ad7111f091cd356f8cc9d4ce"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"path": "references/operations.md",
|
|
106
|
+
"sha256": "d52f01637e33e65ce523d19a810ce9488ab92228809768ec66a584120c24ad98"
|
|
107
|
+
}
|
|
108
|
+
],
|
|
91
109
|
"npm_versions": ["0.11.1"]
|
|
92
110
|
},
|
|
93
111
|
{
|
|
94
112
|
"version": "4.5.0",
|
|
95
|
-
"
|
|
113
|
+
"files": [
|
|
114
|
+
{
|
|
115
|
+
"path": "SKILL.md",
|
|
116
|
+
"sha256": "18132b4b747d135e4ccfbca9e2190a9afb41e144cc06c31c5e6bc0f1571e11da"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"path": "references/operations.md",
|
|
120
|
+
"sha256": "d52f01637e33e65ce523d19a810ce9488ab92228809768ec66a584120c24ad98"
|
|
121
|
+
}
|
|
122
|
+
],
|
|
96
123
|
"npm_versions": ["0.11.2"]
|
|
97
124
|
},
|
|
98
125
|
{
|
|
99
126
|
"version": "4.6.0",
|
|
100
|
-
"
|
|
127
|
+
"files": [
|
|
128
|
+
{
|
|
129
|
+
"path": "SKILL.md",
|
|
130
|
+
"sha256": "d0048ffd9a585d00791540c6fa5e0bec91ff95b1448479678f85702e6b09be8a"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"path": "references/operations.md",
|
|
134
|
+
"sha256": "d52f01637e33e65ce523d19a810ce9488ab92228809768ec66a584120c24ad98"
|
|
135
|
+
}
|
|
136
|
+
],
|
|
101
137
|
"npm_versions": ["0.12.0"]
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"version": "4.6.0",
|
|
141
|
+
"files": [
|
|
142
|
+
{
|
|
143
|
+
"path": "SKILL.md",
|
|
144
|
+
"sha256": "df2ebc3f3903cf5abf4eb6612da527cad97a92f29f1a9a475ea6e8b4cf47abfb"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"path": "references/operations.md",
|
|
148
|
+
"sha256": "d52f01637e33e65ce523d19a810ce9488ab92228809768ec66a584120c24ad98"
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
"npm_versions": ["0.13.0", "0.13.1"]
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"version": "4.6.0",
|
|
155
|
+
"files": [
|
|
156
|
+
{
|
|
157
|
+
"path": "SKILL.md",
|
|
158
|
+
"sha256": "33d0c6520c1ed54d42a502251a08baed86153bf9b6c275aed4525b2968f1553b"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"path": "references/operations.md",
|
|
162
|
+
"sha256": "d52f01637e33e65ce523d19a810ce9488ab92228809768ec66a584120c24ad98"
|
|
163
|
+
}
|
|
164
|
+
],
|
|
165
|
+
"npm_versions": ["0.14.0", "0.14.1", "0.14.2", "0.14.3"]
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"version": "4.6.0",
|
|
169
|
+
"files": [
|
|
170
|
+
{
|
|
171
|
+
"path": "SKILL.md",
|
|
172
|
+
"sha256": "94ff4ce2a97d33cad56b7c1ed8057d0ff7ca22988aebd7923b21319326307c18"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"path": "references/operations.md",
|
|
176
|
+
"sha256": "ee5dd8138b4f4570ee256c820c69b94d650a3d454119fff80b1dc08a6297b380"
|
|
177
|
+
}
|
|
178
|
+
],
|
|
179
|
+
"npm_versions": ["0.15.0"]
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"version": "4.6.0",
|
|
183
|
+
"files": [
|
|
184
|
+
{
|
|
185
|
+
"path": "SKILL.md",
|
|
186
|
+
"sha256": "afd054a923d9a4f825066e5c3fd2dfa7ae2269a8464819b032a5fbf19a9b14fd"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"path": "references/operations.md",
|
|
190
|
+
"sha256": "ee5dd8138b4f4570ee256c820c69b94d650a3d454119fff80b1dc08a6297b380"
|
|
191
|
+
}
|
|
192
|
+
],
|
|
193
|
+
"npm_versions": ["0.15.1"]
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"version": "4.7.0",
|
|
197
|
+
"files": [
|
|
198
|
+
{
|
|
199
|
+
"path": "SKILL.md",
|
|
200
|
+
"sha256": "2606e5fc607a3063789c7c02c49aaf8b3129824fa5527b8654e50e95ed2a8556"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"path": "references/operations.md",
|
|
204
|
+
"sha256": "c744553832f55d0630d5ff64d3b5f201b8e87c323d6b7bfeb84eb1778e775b7d"
|
|
205
|
+
}
|
|
206
|
+
],
|
|
207
|
+
"npm_versions": ["0.16.0"]
|
|
102
208
|
}
|
|
103
209
|
]
|
|
104
210
|
}
|