ast-mcp-server 0.17.0 → 0.18.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 CHANGED
@@ -6,7 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- ## [0.17.0] — not yet published (2026-11-09)
9
+ ## [0.18.0] — not yet published (2026-11-10)
10
+
11
+ ### Added
12
+
13
+ - `ast_explore` reports Angular template evidence in `evidence[].references` whenever it expands references: each item carries `origin` (`compiler`, `template_external`, `template_inline`) and the block adds `template_coverage` and `template_reference_count`, with the verdict `ast_find_references` gives for the same symbol. The fields are additive and absent when coverage is `not_applicable`, so output for non-Angular projects is unchanged (#380).
14
+ - New `ast_explore` omission reason `template_coverage_incomplete` (category `incomplete`, component `references`). Any coverage status other than `completed` raises it once per symbol and sets `evidence_complete: false`; the reason code is in `template_coverage`, and the omission is still counted when `max_bytes` drops the references block. A failure of the template step alone keeps the TypeScript references and reports `unfinished` with `template_unresolved` (#380).
15
+ - `yarn benchmark:explore-templates` measures the added latency on demand. The recorded run adds 1059 ms at p95 for `detail: "full"`, `limit: 20` on 501 components (threshold 2 s), and every request on 2001 components returns a typed result with `work_exhausted`; see `docs/spikes/0380-explore-template-latency.md` (#380).
16
+
17
+ ### Changed
18
+
19
+ - In `ast_explore`, `reference_limit` now bounds the merged list of TypeScript and template items, with the same ordering as `ast_find_references`. Its input description still says "compiler references", and `reference_count` counts TypeScript items only. Page the remaining items through `ast_find_references` (#380).
20
+ - The `tools/list` digest changed because the `ast_explore` output schema gained the optional fields and the new omission reason. Tool descriptions and input schemas are unchanged. `ast_get_impact`, `ast_find_test_candidates` and the `ast_explore` call spines stay compiler-only (#380).
21
+ - The `structural-code-editing` skill is `4.9.0`: it and the README describe the `ast_explore` template evidence instead of the former TypeScript-only limit. The published npm `0.17.0` bytes (skill `4.8.0`) are recorded as a predecessor (#380).
22
+ - `ast_explore` reads and parses each Angular template at most once per request: the selected symbols share the component list, the template reads and the parses through a cache that lives for that request only. Items, order, per-symbol scan budgets and `template_coverage` verdicts are the same as before, and `ast_find_references` is unaffected. In the recorded run the added p50 for `detail: "full"`, `limit: 20` falls from 1040 ms to 179 ms on 501 components and from 4238 ms to 698 ms on 2001 components, where every symbol still ends with `work_exhausted`; see `docs/spikes/0380-explore-template-latency.md` (#454).
23
+
24
+ ## [0.17.0] — published to `next`, verification failed, not promoted (2026-11-10)
10
25
 
11
26
  ### Added
12
27
 
@@ -25,6 +40,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
25
40
  - With `@angular/compiler` 18 or 19, template usage scans walked every subtree twice per nesting level, so `ast_find_references` on a `@Pipe` class or a component member reported `unfinished` with `work_exhausted` on deeply nested templates, and `ast_rename_symbol` refused the rename. Each template is now walked once on every supported major (#441).
26
41
  - Upgrading the `structural-code-editing` skill over an unmodified install from npm `0.16.0` would have failed with a conflict: its content changed after `0.16.0` (#443) while the skill version stayed `4.7.0`, and the `0.16.0` byte set had no recorded predecessor. The published `0.16.0` bytes are now recorded, and the skill version is `4.8.0` (#447).
27
42
 
43
+ ### Release status
44
+
45
+ - npm published these immutable bytes under `next` with provenance and the expected Git commit (`e49987bd1fd36432aa76ecb27ca8359eedb613ad`, integrity `sha512-NmjdENfdfLCSqdMJyazxHfx1XuFHgQeJlAB2XIpHGklqCKkwVw43xKMlXmDfdT7m1XT/b4mokx1MefFdj/AIyw==`, publish run `42753818346`). Public-registry verification (run `42754346795`) then failed for the same reason as `0.14.3`–`0.16.0`: the consumer install resolves `minimatch@10.2.6` through `ts-morph@28.0.0`, flagged by `npm audit` for GHSA-g3hv-75wg-m249 (#361). `0.17.0` supersedes `0.16.0` on `next` and was not promoted to `latest`, tagged in Git, or released on GitHub.
46
+
28
47
  ## [0.16.0] — published to `next`, verification failed, not promoted (2026-11-07)
29
48
 
30
49
  ### Added
package/README.md CHANGED
@@ -96,7 +96,10 @@ Template evidence is explicit about what it proves:
96
96
  - For these members, `completed` means every component template inside the Project was read and every match resolved. `hostDirectives`, `@Directive` inputs and outputs, and non-literal scopes, aliases, metadata or pipe names stay `unfinished` with reason `cross_component`. Runtime bindings (`setInput`, router input binding, `createComponent`) and consumers outside the Project are never examined.
97
97
  - Rename rewrites consumer bindings of a selector-bound member when coverage is `completed`, keeping the binding syntax, values and alias-keyed bindings. It returns `INCOMPLETE_EVIDENCE` when coverage is `unfinished` or `unsupported` (including `hostDirectives`, `@Directive` members and metadata-listed members), and `MUTATION_BLOCKED` for inline templates, a name collision with a template local or another binding of the element, `[(x)]` on a non-`model()` pair, `$any`, or a name in the selector. Inline templates are never rewritten.
98
98
  - Renaming a `@Pipe` class applies TypeScript edits only and keeps the `name` literal and every `| name` usage; renaming `transform` returns `INCOMPLETE_EVIDENCE`.
99
- - `ast_explore` stays TypeScript-only for references; template evidence there is tracked in #380. Use `ast_find_references` for template usages.
99
+ - `ast_explore` reports the same template evidence in `evidence[].references` whenever it expands references (`detail: "full"` or `include_references: true`): each item carries `origin`, and the block adds `template_coverage` and `template_reference_count` with the verdict `ast_find_references` gives for the same symbol. None of these keys appear when coverage is `not_applicable`.
100
+ - In `ast_explore`, `reference_limit` bounds the merged list of TypeScript and template items, even though its input description still says "compiler references". `reference_count` counts TypeScript items only, and a list cut by the limit raises the usual `reference_limit` omission. `ast_explore` has no reference offset: page the remaining items through `ast_find_references`.
101
+ - Any `template_coverage.status` other than `completed` adds one `template_coverage_incomplete` omission (category `incomplete`, component `references`) per symbol and sets `evidence_complete: false`. The reason code (for example `compiler_missing`) is in `template_coverage`. `omissions.details` lists the omission only when the detail limit leaves room; `omissions.counts` and `omissions.total` always count it, also when `max_bytes` drops the references block.
102
+ - `ast_get_impact`, `ast_find_test_candidates` and the `ast_explore` call spines stay compiler-only: they never contain a template usage.
100
103
  - The `diagnostics` field stays TypeScript-only. When a rename edits a template, the plan also carries `template_diagnostics`: a best-effort Angular template type-check delta (`status`, `reason` when not `completed`, `added`, `removed`, `added_errors`) computed by the project's own `@angular/compiler-cli` in a separate child process that never writes to the workspace. It is absent for non-Angular projects and for renames that edit no template. `status` is `completed`, `unavailable` (`compiler_cli_missing`, `compiler_unsupported`, `typescript_incompatible`) or `unfinished` (`work_exhausted`, `deadline`, `check_failed`); the arrays are empty unless it is `completed`.
101
104
  - Treat `unavailable` and `unfinished` as unknown, never as clean: run the project's Angular build or tests before applying. `block_on_template_errors` (default `false`) blocks the plan only when the status is `completed` and `added_errors` is not empty; `allow_new_errors` waives it. Without the input the delta is reported but never blocks.
102
105
  - `template_diagnostics[].file` is relative to the directory of the tsconfig, while `diagnostics[].file` is relative to `project_root`; they differ when the tsconfig is not at the project root.
@@ -378,7 +381,7 @@ For a local candidate, pack and install its tarball instead:
378
381
 
379
382
  ```bash
380
383
  yarn pack --out ast-mcp-server-%v.tgz
381
- dsh plugin --profile web add ./ast-mcp-server-0.17.0.tgz
384
+ dsh plugin --profile web add ./ast-mcp-server-0.18.0.tgz
382
385
  ```
383
386
 
384
387
  The first supported surface is **reads + prepare + preview**. Every apply path is
package/cordis.patch.yml CHANGED
@@ -3,7 +3,7 @@
3
3
  # Mounts the packaged `ast-mcp-server` stdio command through the official
4
4
  # `@deepseek-ai/dsh-mcp-client` bridge. Harness does not resolve MCP binaries
5
5
  # from PATH, so the entrypoint is resolved relative to the profile directory
6
- # (`baseUrl`), where `dsh plugin --profile <name> add ./ast-mcp-server-0.17.0.tgz`
6
+ # (`baseUrl`), where `dsh plugin --profile <name> add ./ast-mcp-server-0.18.0.tgz`
7
7
  # installs a locally packed package as a dependency.
8
8
  #
9
9
  # First-slice surface: reads + prepare + preview. Every apply path is denied
@@ -175,10 +175,10 @@ export function memberOwner(node) {
175
175
  ? { cls: owner, name: nameNode.getText() }
176
176
  : undefined;
177
177
  }
178
- function hasDerivedComponent(context, cls) {
178
+ function hasDerivedComponent(cls, cache) {
179
179
  const identity = (node) => `${node.getSourceFile().getFilePath()}:${node.getPos()}`;
180
180
  const target = identity(cls);
181
- return componentClasses(context).some((candidate) => {
181
+ return cache.components().some((candidate) => {
182
182
  let base = candidate.getBaseClass();
183
183
  for (let depth = 0; base && depth < 32; depth += 1) {
184
184
  if (identity(base) === target)
@@ -406,6 +406,73 @@ function decodeTemplate(bytes) {
406
406
  return undefined;
407
407
  }
408
408
  }
409
+ /**
410
+ * Template work that does not depend on the queried symbol, shared by the collections of one
411
+ * request: the component list, each located template, each read and each parse. It is bound to the
412
+ * {@link ProjectContext} it was created for, and a collection over another context refuses it. It
413
+ * never outlives the request, so nothing is invalidated.
414
+ *
415
+ * Only whole results are kept. An entry is stored after its step returned, so a step that threw is
416
+ * run again by the next collection, and a scan that stopped early leaves complete entries for the
417
+ * templates it reached and none for the rest. Scan budgets are not part of it: every collection
418
+ * charges its own budget for each template, cached or not.
419
+ */
420
+ export class TemplateScanCache {
421
+ context;
422
+ #components;
423
+ #located = new Map();
424
+ /** External templates by path, so a file shared by two components is read once; inline by location. */
425
+ #reads = new Map();
426
+ #parsed = new WeakMap();
427
+ #lines = new Map();
428
+ constructor(context) {
429
+ this.context = context;
430
+ }
431
+ components() {
432
+ return (this.#components ??= componentClasses(this.context));
433
+ }
434
+ locate(cls) {
435
+ let located = this.#located.get(cls);
436
+ if (!located) {
437
+ located = locateTemplate(cls);
438
+ this.#located.set(cls, located);
439
+ }
440
+ return located;
441
+ }
442
+ async read(located) {
443
+ const key = located.container === "external" ? located.absolutePath : located;
444
+ let read = this.#reads.get(key);
445
+ if (!read) {
446
+ read = await readTemplate(this.context, located);
447
+ this.#reads.set(key, read);
448
+ }
449
+ return read;
450
+ }
451
+ /**
452
+ * The lines of the file that holds a usage, for context lines. An external template that the
453
+ * scan read is split from that text; any other file (a source file with an inline template) is
454
+ * read from disk once.
455
+ */
456
+ async lines(absolutePath) {
457
+ let lines = this.#lines.get(absolutePath);
458
+ if (!lines) {
459
+ const read = this.#reads.get(absolutePath);
460
+ const text = read && "text" in read ? read.text : await readFile(absolutePath, "utf8");
461
+ lines = text.split(/\r?\n/);
462
+ this.#lines.set(absolutePath, lines);
463
+ }
464
+ return lines;
465
+ }
466
+ /** The parse of a template returned by {@link read}, errors included. */
467
+ parse(compiler, read, absolutePath) {
468
+ let parsed = this.#parsed.get(read);
469
+ if (!parsed) {
470
+ parsed = compiler.parseTemplate(read.text, absolutePath, { preserveWhitespaces: true });
471
+ this.#parsed.set(read, parsed);
472
+ }
473
+ return parsed;
474
+ }
475
+ }
409
476
  /** Reads a located template within the per-template size cap. */
410
477
  async function readTemplate(context, located) {
411
478
  let text;
@@ -467,16 +534,11 @@ function bindingTarget(cls, node, name) {
467
534
  return undefined;
468
535
  return { selector, inputs: names.inputs, outputs: names.outputs };
469
536
  }
470
- /** Parses a consumer template, or returns undefined when the compiler reports errors. */
471
- function parseConsumer(compiler, text, absolutePath) {
472
- const parsed = compiler.parseTemplate(text, absolutePath, { preserveWhitespaces: true });
473
- return parsed.errors?.length ? undefined : parsed.nodes;
474
- }
475
537
  function selectorInspector(compiler, target) {
476
- return (text, absolutePath, tick) => {
538
+ return (text, parse, tick) => {
477
539
  if (!mayMatch(compiler, target.selector, text))
478
540
  return "skip";
479
- const nodes = parseConsumer(compiler, text, absolutePath);
541
+ const nodes = parse();
480
542
  if (!nodes)
481
543
  return "template_parse_error";
482
544
  const scan = extractConsumerUsages(compiler, nodes, text, target, tick);
@@ -489,10 +551,10 @@ function selectorInspector(compiler, target) {
489
551
  };
490
552
  }
491
553
  function pipeInspector(compiler, name, rc) {
492
- return (text, absolutePath, tick) => {
554
+ return (text, parse, tick) => {
493
555
  if (!text.includes(name))
494
556
  return "skip";
495
- const nodes = parseConsumer(compiler, text, absolutePath);
557
+ const nodes = parse();
496
558
  if (!nodes)
497
559
  return "template_parse_error";
498
560
  try {
@@ -511,7 +573,7 @@ function pipeInspector(compiler, name, rc) {
511
573
  * Scans every component template in the Project with `inspect`. Matching comes first; scope only
512
574
  * filters matches. Unreadable templates and unproven scopes are reported.
513
575
  */
514
- async function scanConsumers(context, cls, inspect, rc, limits) {
576
+ async function scanConsumers(context, cls, compiler, inspect, rc, limits, cache) {
515
577
  const remaining = { ...SCAN_LIMITS, ...limits };
516
578
  const usages = [];
517
579
  let failure;
@@ -528,25 +590,30 @@ async function scanConsumers(context, cls, inspect, rc, limits) {
528
590
  rc.checkpoint();
529
591
  };
530
592
  try {
531
- for (const consumer of componentClasses(context)) {
593
+ for (const consumer of cache.components()) {
532
594
  rc.checkpoint();
533
595
  if (--remaining.templates < 0)
534
596
  throw new WorkExhausted();
535
- const located = locateTemplate(consumer);
597
+ const located = cache.locate(consumer);
536
598
  if ("reason" in located) {
537
599
  failure ??= located.reason;
538
600
  continue;
539
601
  }
540
- const read = await readTemplate(context, located);
602
+ const read = await cache.read(located);
541
603
  if ("reason" in read) {
542
604
  failure ??= read.reason;
543
605
  continue;
544
606
  }
607
+ // The budget is charged for every template, whether or not the cache already held it.
545
608
  remaining.bytes -= read.bytes.length;
546
609
  if (remaining.bytes < 0)
547
610
  throw new WorkExhausted();
548
611
  examined += 1;
549
- const scan = inspect(read.text, located.absolutePath, tick);
612
+ const parse = () => {
613
+ const parsed = cache.parse(compiler, read, located.absolutePath);
614
+ return parsed.errors?.length ? undefined : parsed.nodes;
615
+ };
616
+ const scan = inspect(read.text, parse, tick);
550
617
  if (scan === "skip")
551
618
  continue;
552
619
  if (scan === "template_parse_error") {
@@ -583,7 +650,7 @@ function pipeName(cls) {
583
650
  return literalString(Node.isPropertyAssignment(property) ? property.getInitializer() : undefined);
584
651
  }
585
652
  /** `| name` usages of a pipe class in the templates of components that have the class in scope. */
586
- async function collectPipeUsages(cls, context, rc, limits) {
653
+ async function collectPipeUsages(cls, context, rc, limits, cache) {
587
654
  const unfinished = (reason, templates = 0) => ({
588
655
  coverage: { status: "unfinished", reason, templates },
589
656
  usages: [],
@@ -594,15 +661,23 @@ async function collectPipeUsages(cls, context, rc, limits) {
594
661
  const loaded = await loadCompiler(context.tsConfigFilePath);
595
662
  if ("reason" in loaded)
596
663
  return unfinished(loaded.reason);
597
- const scan = await scanConsumers(context, cls, pipeInspector(loaded.compiler, name, rc), rc, limits);
664
+ const scan = await scanConsumers(context, cls, loaded.compiler, pipeInspector(loaded.compiler, name, rc), rc, limits, cache);
598
665
  const coverage = scan.failure
599
666
  ? { status: "unfinished", reason: scan.failure, templates: scan.examined }
600
667
  : { status: "completed", templates: scan.examined };
601
668
  return { coverage, usages: scan.usages };
602
669
  }
603
- /** Collects usages of a component member inside its declaring component's template. */
604
- export async function collectTemplateUsages(node, context, rc, limits = {}) {
670
+ /**
671
+ * Collects usages of a component member inside its declaring component's template.
672
+ *
673
+ * Pass one `cache` to every collection of a request to share symbol-independent work between
674
+ * them; the result is the same with or without it. By default each call works alone.
675
+ */
676
+ export async function collectTemplateUsages(node, context, rc, limits = {}, cache = new TemplateScanCache(context)) {
605
677
  rc.checkpoint();
678
+ if (cache.context !== context) {
679
+ throw new Error("The template scan cache was created for another ProjectContext.");
680
+ }
606
681
  if (!hasAngularCore(context))
607
682
  return NOT_APPLICABLE;
608
683
  const fail = (status, reason) => ({
@@ -610,7 +685,7 @@ export async function collectTemplateUsages(node, context, rc, limits = {}) {
610
685
  usages: [],
611
686
  });
612
687
  if (Node.isClassDeclaration(node)) {
613
- return isPipeClass(node) ? collectPipeUsages(node, context, rc, limits) : NOT_APPLICABLE;
688
+ return isPipeClass(node) ? collectPipeUsages(node, context, rc, limits, cache) : NOT_APPLICABLE;
614
689
  }
615
690
  const owner = memberOwner(node);
616
691
  if (!owner)
@@ -620,14 +695,17 @@ export async function collectTemplateUsages(node, context, rc, limits = {}) {
620
695
  if (owner.name !== "transform" || componentDecorator(owner.cls)) {
621
696
  return fail("unfinished", "cross_component");
622
697
  }
623
- return { ...(await collectPipeUsages(owner.cls, context, rc, limits)), renameBlocked: true };
698
+ return {
699
+ ...(await collectPipeUsages(owner.cls, context, rc, limits, cache)),
700
+ renameBlocked: true,
701
+ };
624
702
  }
625
703
  const isComponent = componentDecorator(owner.cls) !== undefined;
626
704
  const selectorPublic = isSelectorPublic(node) || isMetadataPublic(owner.cls, owner.name);
627
705
  if (!isComponent && selectorPublic && isDirectiveClass(owner.cls)) {
628
706
  return fail("unfinished", "cross_component");
629
707
  }
630
- const derived = hasDerivedComponent(context, owner.cls);
708
+ const derived = hasDerivedComponent(owner.cls, cache);
631
709
  if (!isComponent && !derived)
632
710
  return NOT_APPLICABLE;
633
711
  if (derived)
@@ -635,16 +713,14 @@ export async function collectTemplateUsages(node, context, rc, limits = {}) {
635
713
  const loaded = await loadCompiler(context.tsConfigFilePath);
636
714
  if ("reason" in loaded)
637
715
  return fail("unfinished", loaded.reason);
638
- const located = locateTemplate(owner.cls);
716
+ const located = cache.locate(owner.cls);
639
717
  if ("reason" in located)
640
718
  return fail("unfinished", located.reason);
641
- const read = await readTemplate(context, located);
719
+ const read = await cache.read(located);
642
720
  if ("reason" in read)
643
721
  return fail("unfinished", read.reason);
644
722
  rc.checkpoint();
645
- const parsed = loaded.compiler.parseTemplate(read.text, located.absolutePath, {
646
- preserveWhitespaces: true,
647
- });
723
+ const parsed = cache.parse(loaded.compiler, read, located.absolutePath);
648
724
  if (parsed.errors?.length)
649
725
  return fail("unfinished", "template_parse_error");
650
726
  let raw;
@@ -669,7 +745,7 @@ export async function collectTemplateUsages(node, context, rc, limits = {}) {
669
745
  return unfinished("cross_component");
670
746
  if (!loaded.selectors)
671
747
  return unfinished("compiler_unsupported");
672
- const scan = await scanConsumers(context, owner.cls, selectorInspector(loaded.compiler, target), rc, limits);
748
+ const scan = await scanConsumers(context, owner.cls, loaded.compiler, selectorInspector(loaded.compiler, target), rc, limits, cache);
673
749
  const coverage = scan.failure
674
750
  ? { status: "unfinished", reason: scan.failure, templates: scan.examined }
675
751
  : { status: "completed", templates: scan.examined };
@@ -1,11 +1,12 @@
1
1
  import path from "node:path";
2
+ import { TemplateScanCache } from "./angular-templates.js";
2
3
  import { buildFileOutline, nodeSourceWithLocation } from "./outline.js";
3
4
  import { paginate } from "./pagination.js";
4
5
  import { resolveImpactRoot } from "./impact.js";
5
6
  import { planCallSpines } from "./call-spines.js";
6
7
  import { isCooperativeInterruption, NO_REQUEST_CONTEXT, } from "./request-context.js";
7
8
  import { findDeclarationByName, getSourceFileOrThrow, reportSymbolIndexFailure, } from "./project.js";
8
- import { collectSymbolReferences } from "./references.js";
9
+ import { collectSymbolReferencesWithTemplateVerdict, } from "./references.js";
9
10
  import { collectCompilerCallRelationships } from "./relationships.js";
10
11
  import { presentExploreClusters } from "./explore-presentation.js";
11
12
  import { searchProjectSymbolsPage, searchProjectSymbolsPageWithIndex, sourceFileSymbols, symbolMatchRank, } from "./symbols.js";
@@ -151,6 +152,8 @@ export async function buildExploreContext(context, request, requestContext = NO_
151
152
  const unresolved = [];
152
153
  const evidence = [];
153
154
  let callSpines;
155
+ // Lives for this request only: the selected symbols share template reads, never a scan budget.
156
+ const templateCache = new TemplateScanCache(effectiveContext);
154
157
  if (request.callSpines) {
155
158
  if (!request.filePath || !request.symbolPath || routed.route !== "symbol") {
156
159
  throw new Error("Call spines require an exact file_path and symbol_path.");
@@ -197,7 +200,13 @@ export async function buildExploreContext(context, request, requestContext = NO_
197
200
  }
198
201
  if (expansion.references) {
199
202
  try {
200
- references = collectSymbolReferences(node, effectiveContext.projectRoot, record.selector, true, request.referenceDetail, 0, request.referenceLimit, requestContext);
203
+ references = await collectSymbolReferencesWithTemplateVerdict(node, effectiveContext, {
204
+ symbolPath: record.selector,
205
+ includeDeclaration: true,
206
+ detail: request.referenceDetail,
207
+ offset: 0,
208
+ limit: request.referenceLimit,
209
+ }, requestContext, templateCache);
201
210
  }
202
211
  catch (error) {
203
212
  if (isCooperativeInterruption(error))
@@ -231,6 +240,16 @@ export async function buildExploreContext(context, request, requestContext = NO_
231
240
  reason: "reference_limit",
232
241
  });
233
242
  }
243
+ // Absent coverage means not applicable; anything short of completed leaves usages unknown.
244
+ const coverage = item.references?.template_coverage?.status;
245
+ if (coverage !== undefined && coverage !== "completed" && coverage !== "not_applicable") {
246
+ omissions.push({
247
+ subject: item.selector,
248
+ category: "incomplete",
249
+ component: "references",
250
+ reason: "template_coverage_incomplete",
251
+ });
252
+ }
234
253
  }
235
254
  if (callSpines?.authority_state === "untrusted") {
236
255
  omissions.push({
@@ -9,6 +9,7 @@ export const EXPLORE_OMISSION_REASONS = [
9
9
  "depth_limit",
10
10
  "node_limit",
11
11
  "edge_limit",
12
+ "template_coverage_incomplete",
12
13
  ];
13
14
  function jsonBytes(value) {
14
15
  return Buffer.byteLength(JSON.stringify(value), "utf8");
@@ -1,9 +1,8 @@
1
- import { readFile } from "node:fs/promises";
2
1
  import path from "node:path";
3
2
  import { Node } from "ts-morph";
4
- import { collectTemplateUsages, } from "./angular-templates.js";
3
+ import { collectTemplateUsages, TemplateScanCache, } from "./angular-templates.js";
5
4
  import { paginate } from "./pagination.js";
6
- import { NO_REQUEST_CONTEXT } from "./request-context.js";
5
+ import { isCooperativeInterruption, NO_REQUEST_CONTEXT, } from "./request-context.js";
7
6
  /**
8
7
  * Semantic references of `node` excluding the declaration itself.
9
8
  *
@@ -79,15 +78,11 @@ export function collectSymbolReferences(node, projectRoot, symbolPath, includeDe
79
78
  next_offset: page.next_offset,
80
79
  };
81
80
  }
82
- async function templateLocation(usage, detail, lineCache) {
81
+ async function templateLocation(usage, detail, templateCache) {
83
82
  let context;
84
83
  if (detail === "context") {
85
- let lines = lineCache.get(usage.absolutePath);
86
- if (!lines) {
87
- lines = readFile(usage.absolutePath, "utf8").then((text) => text.split(/\r?\n/));
88
- lineCache.set(usage.absolutePath, lines);
89
- }
90
- context = ((await lines)[usage.line - 1] ?? "").trim().slice(0, 500);
84
+ const lines = await templateCache.lines(usage.absolutePath);
85
+ context = (lines[usage.line - 1] ?? "").trim().slice(0, 500);
91
86
  }
92
87
  return {
93
88
  file: usage.file,
@@ -99,29 +94,28 @@ async function templateLocation(usage, detail, lineCache) {
99
94
  ...(context === undefined ? {} : { context }),
100
95
  };
101
96
  }
102
- /**
103
- * Collects TypeScript references and, for Angular component members, template usages.
104
- * Without Angular evidence the result is identical to {@link collectSymbolReferences}.
105
- */
106
- export async function collectSymbolReferencesWithTemplates(node, context, query, requestContext = NO_REQUEST_CONTEXT) {
107
- const { projectRoot } = context;
108
- const { coverage, usages } = await collectTemplateUsages(node, context, requestContext);
109
- if (coverage.status === "not_applicable") {
110
- return collectSymbolReferences(node, projectRoot, query.symbolPath, query.includeDeclaration, query.detail, query.offset, query.limit, requestContext);
111
- }
112
- const ts = collectTsLocations(node, projectRoot, query.symbolPath, query.includeDeclaration, query.detail, requestContext);
113
- const lineCache = new Map();
114
- const templateItems = [];
97
+ /** Template usages of `node` as reference items, with the coverage verdict that qualifies them. */
98
+ async function collectTemplateHalf(node, context, detail, requestContext, templateCache = new TemplateScanCache(context)) {
99
+ const { coverage, usages } = await collectTemplateUsages(node, context, requestContext, undefined, templateCache);
100
+ const items = [];
115
101
  for (const usage of usages) {
116
102
  requestContext.checkpoint();
117
- templateItems.push(await templateLocation(usage, query.detail, lineCache));
103
+ items.push(await templateLocation(usage, detail, templateCache));
104
+ }
105
+ return { coverage, items };
106
+ }
107
+ /** Merges the TypeScript references of `node` with an already collected template half. */
108
+ function mergeWithTemplateHalf(node, projectRoot, query, template, requestContext) {
109
+ if (template.coverage.status === "not_applicable") {
110
+ return collectSymbolReferences(node, projectRoot, query.symbolPath, query.includeDeclaration, query.detail, query.offset, query.limit, requestContext);
118
111
  }
112
+ const ts = collectTsLocations(node, projectRoot, query.symbolPath, query.includeDeclaration, query.detail, requestContext);
119
113
  const merged = [
120
114
  ...ts.locations.map((item) => ({ ...item, origin: "compiler" })),
121
- ...templateItems,
115
+ ...template.items,
122
116
  ].sort(compareLocations);
123
117
  const affectedFiles = [
124
- ...new Set([ts.declaration, ...ts.references, ...templateItems].map((item) => item.file)),
118
+ ...new Set([ts.declaration, ...ts.references, ...template.items].map((item) => item.file)),
125
119
  ].sort();
126
120
  const page = paginate(merged, query.offset, query.limit);
127
121
  requestContext.checkpoint();
@@ -137,7 +131,38 @@ export async function collectSymbolReferencesWithTemplates(node, context, query,
137
131
  total: page.total,
138
132
  has_more: page.has_more,
139
133
  next_offset: page.next_offset,
140
- template_coverage: coverage,
141
- template_reference_count: templateItems.length,
134
+ template_coverage: template.coverage,
135
+ template_reference_count: template.items.length,
142
136
  };
143
137
  }
138
+ /**
139
+ * Collects TypeScript references and, for Angular component members, template usages.
140
+ * Without Angular evidence the result is identical to {@link collectSymbolReferences}.
141
+ */
142
+ export async function collectSymbolReferencesWithTemplates(node, context, query, requestContext = NO_REQUEST_CONTEXT) {
143
+ const template = await collectTemplateHalf(node, context, query.detail, requestContext);
144
+ return mergeWithTemplateHalf(node, context.projectRoot, query, template, requestContext);
145
+ }
146
+ /**
147
+ * Same result as {@link collectSymbolReferencesWithTemplates}, but a failing template half is
148
+ * reported as an `unfinished` / `template_unresolved` verdict next to the TypeScript references
149
+ * instead of failing the call. TypeScript failures and cooperative interruptions still throw.
150
+ *
151
+ * A caller that collects several symbols in one request passes the same `templateCache` to each
152
+ * call; it shares template reads between them and does not change any result.
153
+ */
154
+ export async function collectSymbolReferencesWithTemplateVerdict(node, context, query, requestContext = NO_REQUEST_CONTEXT, templateCache) {
155
+ let template;
156
+ try {
157
+ template = await collectTemplateHalf(node, context, query.detail, requestContext, templateCache);
158
+ }
159
+ catch (error) {
160
+ if (isCooperativeInterruption(error))
161
+ throw error;
162
+ template = {
163
+ coverage: { status: "unfinished", reason: "template_unresolved", templates: 0 },
164
+ items: [],
165
+ };
166
+ }
167
+ return mergeWithTemplateHalf(node, context.projectRoot, query, template, requestContext);
168
+ }
@@ -8,6 +8,7 @@ import { createRequestContext } from "../services/request-context.js";
8
8
  import { FRESHNESS_CAUSES, SNAPSHOT_STATES, TRUNCATION_REASONS, } from "../services/read-contracts.js";
9
9
  import { createToolErrorContext, errorResult, structuredResult } from "./result.js";
10
10
  import { RelationshipEndpointSchema } from "./relationship-schema.js";
11
+ import { ReferenceOriginSchema, templateEvidenceFields } from "./template-evidence-schema.js";
11
12
  const TOOL_NAME = "ast_explore";
12
13
  const ExploreDetailSchema = z.enum(["selectors", "summary", "context", "full"]).default("summary");
13
14
  const ReferenceDetailSchema = z.enum(["locations", "context"]).default("locations");
@@ -100,6 +101,7 @@ const ExploreReferenceSchema = z.object({
100
101
  kind: z.string(),
101
102
  is_declaration: z.boolean(),
102
103
  context: z.string().optional(),
104
+ origin: ReferenceOriginSchema.optional(),
103
105
  });
104
106
  const ExploreEvidenceSchema = z.object({
105
107
  selector: z.string(),
@@ -111,6 +113,7 @@ const ExploreEvidenceSchema = z.object({
111
113
  declaration_count: z.number().int().min(0),
112
114
  reference_count: z.number().int().min(0),
113
115
  affected_files: z.array(z.string()),
116
+ ...templateEvidenceFields,
114
117
  references: z.array(ExploreReferenceSchema),
115
118
  offset: z.number().int().min(0),
116
119
  limit: z.number().int().positive(),
@@ -4,6 +4,7 @@ import { findDeclarationByName, getSourceFileOrThrow, withProject } from "../ser
4
4
  import { collectSymbolReferencesWithTemplates } from "../services/references.js";
5
5
  import { createRequestContext } from "../services/request-context.js";
6
6
  import { createToolErrorContext, errorResult, formattedResult, ToolOutputFormatInputSchema, } from "./result.js";
7
+ import { ReferenceOriginSchema, templateEvidenceFields } from "./template-evidence-schema.js";
7
8
  const TOOL_NAME = "ast_find_references";
8
9
  const ReferenceDetailSchema = z.enum(["locations", "context"]).default("locations");
9
10
  const AstFindReferencesInputSchema = z.object({
@@ -26,37 +27,20 @@ const ContextReferenceSchema = z.object({
26
27
  context: z.string(),
27
28
  });
28
29
  const ContextReferenceSchemaWithOrigin = ContextReferenceSchema.extend({
29
- origin: z.enum(["compiler", "template_external", "template_inline"]).optional(),
30
+ origin: ReferenceOriginSchema.optional(),
30
31
  });
31
32
  const LocationReferenceSchema = ContextReferenceSchemaWithOrigin.omit({ context: true });
32
- const TemplateCoverageSchema = z.object({
33
- status: z.enum(["completed", "unfinished", "unsupported"]),
34
- reason: z
35
- .enum([
36
- "compiler_missing",
37
- "compiler_unsupported",
38
- "template_unresolved",
39
- "template_unsafe",
40
- "template_parse_error",
41
- "size_exceeded",
42
- "work_exhausted",
43
- "inherited_member",
44
- "cross_component",
45
- ])
46
- .optional(),
47
- templates: z.number().int().min(0).optional(),
48
- });
49
33
  const ReferenceOutputBase = {
50
34
  symbol: z.string(),
51
35
  include_declaration: z.boolean(),
52
36
  declaration_count: z.number().int().min(0),
53
37
  reference_count: z.number().int().min(0),
54
38
  affected_files: z.array(z.string()),
55
- template_coverage: TemplateCoverageSchema.optional(),
56
- template_reference_count: z.number().int().min(0).optional(),
39
+ ...templateEvidenceFields,
57
40
  ...PaginationOutputSchema,
58
41
  };
59
- const ReferenceOutputSchemas = {
42
+ /** Exported so a golden test can pin the output JSON schema bytes; not a tool contract. */
43
+ export const ReferenceOutputSchemas = {
60
44
  locations: z.object({
61
45
  ...ReferenceOutputBase,
62
46
  references: z.array(LocationReferenceSchema),
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ /** Output schemas for Angular template evidence, shared by every tool that reports references. */
3
+ export const ReferenceOriginSchema = z.enum(["compiler", "template_external", "template_inline"]);
4
+ export const TemplateCoverageSchema = z.object({
5
+ status: z.enum(["completed", "unfinished", "unsupported"]),
6
+ reason: z
7
+ .enum([
8
+ "compiler_missing",
9
+ "compiler_unsupported",
10
+ "template_unresolved",
11
+ "template_unsafe",
12
+ "template_parse_error",
13
+ "size_exceeded",
14
+ "work_exhausted",
15
+ "inherited_member",
16
+ "cross_component",
17
+ ])
18
+ .optional(),
19
+ templates: z.number().int().min(0).optional(),
20
+ });
21
+ /** Present only when template coverage applies to the symbol. */
22
+ export const templateEvidenceFields = {
23
+ template_coverage: TemplateCoverageSchema.optional(),
24
+ template_reference_count: z.number().int().min(0).optional(),
25
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ast-mcp-server",
3
- "version": "0.17.0",
3
+ "version": "0.18.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",
@@ -91,6 +91,7 @@
91
91
  "benchmark:corpus": "yarn build && node scripts/benchmark-corpus.mjs",
92
92
  "benchmark:batch": "yarn build && node scripts/benchmark-batch.mjs",
93
93
  "benchmark:formats": "yarn build && node scripts/benchmark-formats.mjs",
94
+ "benchmark:explore-templates": "yarn build && node scripts/benchmark-explore-templates.mjs",
94
95
  "benchmark:shapes": "yarn build && node scripts/benchmark-result-shapes.mjs",
95
96
  "benchmark:agent-workflows": "yarn build && node scripts/benchmark-agent-workflows.mjs",
96
97
  "benchmark:index-storage": "yarn build && node scripts/benchmark-index-storage.mjs",
@@ -140,5 +141,5 @@
140
141
  "vitest": "^5.0.5",
141
142
  "yaml": "^2.9.0"
142
143
  },
143
- "gitHead": "e49987bd1fd36432aa76ecb27ca8359eedb613ad"
144
+ "gitHead": "9154dc7bd17f326e04c7b7e4dbfb9fcde9a66cdf"
144
145
  }
@@ -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.8.0"
7
+ version: "4.9.0"
8
8
  hermes:
9
9
  tags: ["typescript", "javascript", "ast", "refactoring", "mcp"]
10
10
  homepage: "https://github.com/yailPeralta/ast-mcp-server"
@@ -49,6 +49,11 @@ In an Angular project (`@angular/core` resolvable from the tsconfig directory),
49
49
 
50
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_explore` reports the same template evidence in `evidence[].references` whenever it expands references (`detail: "full"` or `include_references: true`): each item carries `origin`, and the block adds `template_coverage` and `template_reference_count` with the verdict `ast_find_references` gives for the same symbol. None of these keys appear when coverage is `not_applicable`. Read `template_coverage` before trusting explore references, and treat every status other than `completed` as unknown.
53
+
54
+ - In `ast_explore`, `reference_limit` bounds the merged list of TypeScript and template items, even though its input description still says "compiler references". `reference_count` counts TypeScript items only, and a list cut by the limit raises the usual `reference_limit` omission. `ast_explore` has no reference offset: page the remaining items through `ast_find_references`.
55
+ - Any `template_coverage.status` other than `completed` adds one `template_coverage_incomplete` omission (category `incomplete`, component `references`) per symbol and sets `evidence_complete: false`. The reason code (for example `compiler_missing`) is in `template_coverage`. `omissions.details` lists the omission only when the detail limit leaves room; `omissions.counts` and `omissions.total` always count it, also when `max_bytes` drops the references block.
56
+
52
57
  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
58
 
54
59
  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.
@@ -59,7 +64,7 @@ Pipes: renaming a `@Pipe` class applies with TypeScript edits only; the `name` l
59
64
 
60
65
  Limits to state when reporting:
61
66
 
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.
67
+ - `ast_get_impact`, `ast_find_test_candidates` and the `ast_explore` call spines stay compiler-only: they never contain a template usage. Rename blocks on its own evidence, independently of these tools.
63
68
  - 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.
64
69
  - 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`.
65
70
 
@@ -22,7 +22,9 @@ 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. 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).
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.
26
+
27
+ `ast_explore` reports the same template evidence in `evidence[].references` whenever it expands references (`detail: "full"` or `include_references: true`): each item carries `origin`, and the block adds `template_coverage` and `template_reference_count` with the verdict `ast_find_references` gives for the same symbol. None of these keys appear when coverage is `not_applicable`. Read `template_coverage` before trusting explore references. In `ast_explore`, `reference_limit` bounds the merged list of TypeScript and template items, even though its input description still says "compiler references". `reference_count` counts TypeScript items only, and a list cut by the limit raises the usual `reference_limit` omission. `ast_explore` has no reference offset: page the remaining items through `ast_find_references`. Any `template_coverage.status` other than `completed` adds one `template_coverage_incomplete` omission (category `incomplete`, component `references`) per symbol and sets `evidence_complete: false`. The reason code (for example `compiler_missing`) is in `template_coverage`. `omissions.details` lists the omission only when the detail limit leaves room; `omissions.counts` and `omissions.total` always count it, also when `max_bytes` drops the references block. `ast_get_impact`, `ast_find_test_candidates` and the `ast_explore` call spines stay compiler-only: they never contain a template usage.
26
28
 
27
29
  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
30
 
@@ -2,15 +2,15 @@
2
2
  "schema_version": 2,
3
3
  "algorithm": "sha256",
4
4
  "current": {
5
- "version": "4.8.0",
5
+ "version": "4.9.0",
6
6
  "files": [
7
7
  {
8
8
  "path": "SKILL.md",
9
- "sha256": "15e5d25553a3ebce86365ff7b43ed61e7df11dd6ad658e83ec6e43cd84118762"
9
+ "sha256": "451670dcaa0bbfc11273fe9bd507de61865dbaac80e1d586306bcd30962d90d4"
10
10
  },
11
11
  {
12
12
  "path": "references/operations.md",
13
- "sha256": "dd633a24e9aabeb1d9e9d1f93e261906e7b30eb721fadfb3d317d6acc60db2ca"
13
+ "sha256": "363ccb2332a42c68f21073a568a96d3000eff98954eaebff3d92e65d87c4f14c"
14
14
  }
15
15
  ]
16
16
  },
@@ -205,6 +205,20 @@
205
205
  }
206
206
  ],
207
207
  "npm_versions": ["0.16.0"]
208
+ },
209
+ {
210
+ "version": "4.8.0",
211
+ "files": [
212
+ {
213
+ "path": "SKILL.md",
214
+ "sha256": "15e5d25553a3ebce86365ff7b43ed61e7df11dd6ad658e83ec6e43cd84118762"
215
+ },
216
+ {
217
+ "path": "references/operations.md",
218
+ "sha256": "dd633a24e9aabeb1d9e9d1f93e261906e7b30eb721fadfb3d317d6acc60db2ca"
219
+ }
220
+ ],
221
+ "npm_versions": ["0.17.0"]
208
222
  }
209
223
  ]
210
224
  }