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.
@@ -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 blocked = !allowNewErrors && diagnosticDelta.addedErrors.length > 0;
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: blocked
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 } = await collectTemplateUsages(node, context, rc);
422
- assertTemplateRenameAllowed(coverage.status, usages, args.newName);
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 + usages.length,
429
- auxiliaryEdits: usages.map((usage) => ({
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: args.newName,
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 = 2) {
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 blocked = !persisted.allow_new_errors && addedErrors.length > 0;
1037
- if (persisted.blocked !== blocked) {
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: blocked
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, and template errors are not diagnosed.",
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 { PreparedOperationOutputSchema, serializePreparedOperation } from "./operation-schema.js";
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; diagnostics stay TypeScript-only.",
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: PreparedOperationOutputSchema,
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.15.1",
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": "20548017b7c9266160963a0272ce8757b8f42a77"
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.6.0"
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`. 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.
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 of the declaring component was examined. `unfinished` (for example `compiler_missing`, `compiler_unsupported`, `template_unresolved`, `template_parse_error`, `size_exceeded`) 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.
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
- `ast_rename_symbol` updates external templates in the same reviewed plan and rewrites them only through `ast_apply_operation`. It fails closed: coverage `unfinished` or `unsupported` returns `INCOMPLETE_EVIDENCE`, and an inline template usage, a new name that collides with a template local, or `$any` returns `MUTATION_BLOCKED`. Pipe names, `#ref`/`let-` targets, child inputs and outputs, host bindings and `styleUrls` are not renamed. Apply is sequential, not globally atomic; rollback restores the owned templates.
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
- - Diagnostics stay TypeScript-only: `ast_get_diagnostics` and the post-apply delta do not detect template errors. A template-only breakage produces no diagnostic, so run the project's Angular build or tests after a rename.
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. `ast_explore` is TypeScript-only for references (tracked in issue #380), and diagnostics stay TypeScript-only, so template errors are never detected.
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
- `ast_rename_symbol` edits external templates and returns `INCOMPLETE_EVIDENCE` (coverage `unfinished` or `unsupported`) or `MUTATION_BLOCKED` (inline usage, template-local collision, `$any`). 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`.
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.6.0",
5
+ "version": "4.8.0",
6
6
  "files": [
7
7
  {
8
8
  "path": "SKILL.md",
9
- "sha256": "afd054a923d9a4f825066e5c3fd2dfa7ae2269a8464819b032a5fbf19a9b14fd"
9
+ "sha256": "15e5d25553a3ebce86365ff7b43ed61e7df11dd6ad658e83ec6e43cd84118762"
10
10
  },
11
11
  {
12
12
  "path": "references/operations.md",
13
- "sha256": "ee5dd8138b4f4570ee256c820c69b94d650a3d454119fff80b1dc08a6297b380"
13
+ "sha256": "dd633a24e9aabeb1d9e9d1f93e261906e7b30eb721fadfb3d317d6acc60db2ca"
14
14
  }
15
15
  ]
16
16
  },
@@ -82,23 +82,129 @@
82
82
  },
83
83
  {
84
84
  "version": "4.5.0",
85
- "sha256": "be757e8bd98733c7ee952d63a2805dcd9877becf651b4fcb6ab63d20f6546f21",
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
- "sha256": "2729b30ff6b7344e6680c7ee02ab540bc6db9774ad7111f091cd356f8cc9d4ce",
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
- "sha256": "18132b4b747d135e4ccfbca9e2190a9afb41e144cc06c31c5e6bc0f1571e11da",
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
- "sha256": "d0048ffd9a585d00791540c6fa5e0bec91ff95b1448479678f85702e6b09be8a",
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
  }