universal-ast-mapper 0.8.2 → 0.8.4

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/README.md CHANGED
@@ -93,6 +93,7 @@ ast-map graph <dir> [-o graph.json]
93
93
  ast-map validate <path> [--max-lines N] [--max-imports N] [--max-exports N]
94
94
  ast-map dead <dir>
95
95
  ast-map cycles <dir>
96
+ ast-map duplicates <dir> [alias: dupes]
96
97
  ast-map search <pattern> [dir] [-m contains|exact|regex] [-k kind] [-e]
97
98
  ast-map deps <file> [--scan <dir>]
98
99
  ast-map top <dir> [-n 10]
@@ -260,6 +261,24 @@ Each cycle is canonicalised to avoid duplicates.
260
261
 
261
262
  ---
262
263
 
264
+ ### `find_duplicate_symbols`
265
+ Scan a directory → find symbol names exported from **more than one file** (accidental collisions / parallel implementations). Each result lists every file + kind that declares the name.
266
+
267
+ ```json
268
+ {
269
+ "duplicates": [
270
+ { "symbol": "validate", "count": 2, "locations": [
271
+ { "file": "src/a.ts", "kind": "function" },
272
+ { "file": "src/b.ts", "kind": "function" }
273
+ ]}
274
+ ]
275
+ }
276
+ ```
277
+
278
+ **Params:** `path`
279
+
280
+ ---
281
+
263
282
  ### `get_change_impact`
264
283
  Given a file + symbol, reverse-traverse the import graph to compute **blast radius**.
265
284
 
@@ -482,6 +501,8 @@ src/
482
501
 
483
502
  | Version | What changed |
484
503
  |---------|--------------|
504
+ | **0.8.4** | **Duplicate symbol detection** — new `find_duplicate_symbols` MCP tool + `ast-map duplicates` (alias `dupes`) CLI command: finds symbol names exported from more than one file, with every file/kind that declares each name. |
505
+ | **0.8.3** | **TSX/React component props** — component symbols now carry extracted prop fields. PascalCase functions/arrows that return JSX or are typed `React.FC<P>`/`FC<P>` get `propsType` (named props type) + `props[]` (name, type, optional), resolved from same-file `interface`/`type` declarations or inline object types. Plus: MCP server now reports its real version from `package.json` (was hardcoded `0.5.3`). |
485
506
  | **0.8.2** | **Swift cross-file wiring** — `import <Module>` resolves to that module's files (module = the `Sources/<Module>/` directory, else parent dir), wired into `build_symbol_graph` + `resolve_imports`. System modules (Foundation, UIKit, …) stay external. Completes cross-file graph/resolver support for all four v0.8.0 languages. |
486
507
  | **0.8.1** | **Cross-file graph wiring for Kotlin & C/C++** — Kotlin FQCN/package index + C/C++ `#include` resolution (with header↔impl pairing) wired into `build_symbol_graph`, `resolve_imports`, and `get_call_graph`. Fixes a parse-cache rel-path leak (stale `.file` poisoned the cross-lang index → doubled paths) and Kotlin call-graph extraction (`function_declaration` name + field-less `call_expression`). |
487
508
  | **0.8.0** | **4 new languages: C · C++ · Kotlin · Swift** — symbol extraction + imports parsing. C++ tracks access_specifier through class bodies. Kotlin handles `package`/`object`/`data class`. Swift handles `class`/`struct`/`enum` (all under `class_declaration`) and `protocol_declaration`. Ruby grammar in tree-sitter-wasms@0.1.13 is unstable — skipped. |
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { supportedLanguages } from "./registry.js";
9
9
  import { findSymbol, findRelatedSymbols, findServerImports, isApiRoute, findMissingTryCatch, checkGeneralRules, GENERAL_RULE_DEFAULTS } from "./analysis.js";
10
10
  import { resolveFileImports } from "./resolver.js";
11
11
  import { buildSymbolGraph } from "./graph.js";
12
- import { findDeadExports, findCircularDeps, getChangeImpact, getFileDeps, getTopSymbols } from "./graph-analysis.js";
12
+ import { findDeadExports, findCircularDeps, getChangeImpact, getFileDeps, getTopSymbols, findDuplicateSymbols } from "./graph-analysis.js";
13
13
  import { buildCallGraph } from "./callgraph.js";
14
14
  import { searchSymbols } from "./search.js";
15
15
  const ROOT = path.resolve(process.env.AST_MAP_ROOT ?? process.cwd());
@@ -366,6 +366,36 @@ program
366
366
  }
367
367
  console.log();
368
368
  });
369
+ // ─── Command: duplicates ──────────────────────────────────────────────────────
370
+ program
371
+ .command("duplicates <dir>")
372
+ .alias("dupes")
373
+ .description("Find symbol names exported from more than one file")
374
+ .option("--json", "Output as JSON")
375
+ .action(async (inputPath, opts) => {
376
+ const { abs, rel } = resolveArg(inputPath);
377
+ if (!fs.statSync(abs).isDirectory())
378
+ die(`"${rel}" is not a directory`);
379
+ const skeletons = await gatherSkeletons(abs);
380
+ const graph = buildSymbolGraph(skeletons, ROOT);
381
+ const duplicates = findDuplicateSymbols(graph);
382
+ if (opts.json)
383
+ return jsonOut({ directory: rel, scanned: skeletons.length, duplicateCount: duplicates.length, duplicates });
384
+ header(`Duplicate Symbols — ${rel}/ ${dim(`(${skeletons.length} files scanned)`)}`);
385
+ if (duplicates.length === 0) {
386
+ console.log(indent(green("✓ No duplicate exported symbols found.")));
387
+ }
388
+ else {
389
+ for (const d of duplicates) {
390
+ console.log(indent(`${yellow(d.symbol)} ${dim(`— exported from ${d.count} files`)}`));
391
+ for (const loc of d.locations) {
392
+ console.log(indent(`${dim(col(loc.kind, 10))} ${loc.file}`, 5));
393
+ }
394
+ }
395
+ console.log(`\n ${yellow(`${duplicates.length} duplicated name(s)`)}`);
396
+ }
397
+ console.log();
398
+ });
369
399
  // ─── Command: cycles ──────────────────────────────────────────────────────────
370
400
  program
371
401
  .command("cycles <dir>")
@@ -592,24 +622,24 @@ program
592
622
  .name("ast-map")
593
623
  .description("CLI for universal-ast-mapper — structural code analysis tools")
594
624
  .version("0.5.3")
595
- .addHelpText("after", `
596
- ${bold("Examples:")}
597
- ast-map langs
598
- ast-map skeleton src/
599
- ast-map symbol src/utils.ts sanitize --related
600
- ast-map imports src/pages/login.tsx
601
- ast-map graph src/ -o graph.json
602
- ast-map validate src/
603
- ast-map dead src/
604
- ast-map cycles src/
605
- ast-map search validateSession src/ --exported
606
- ast-map deps src/lib/auth.ts --scan src/
607
- ast-map top src/ -n 15
608
- ast-map impact src/utils.ts sanitize --scan src/
609
- ast-map calls src/utils.ts buildCallGraph --scan src/
610
-
611
- ${bold("Root:")}
612
- Defaults to cwd. Override with AST_MAP_ROOT=<path> or run from your project root.
625
+ .addHelpText("after", `
626
+ ${bold("Examples:")}
627
+ ast-map langs
628
+ ast-map skeleton src/
629
+ ast-map symbol src/utils.ts sanitize --related
630
+ ast-map imports src/pages/login.tsx
631
+ ast-map graph src/ -o graph.json
632
+ ast-map validate src/
633
+ ast-map dead src/
634
+ ast-map cycles src/
635
+ ast-map search validateSession src/ --exported
636
+ ast-map deps src/lib/auth.ts --scan src/
637
+ ast-map top src/ -n 15
638
+ ast-map impact src/utils.ts sanitize --scan src/
639
+ ast-map calls src/utils.ts buildCallGraph --scan src/
640
+
641
+ ${bold("Root:")}
642
+ Defaults to cwd. Override with AST_MAP_ROOT=<path> or run from your project root.
613
643
  `);
614
644
  program.parseAsync(process.argv).catch(err => {
615
645
  console.error(red("Fatal: ") + (err instanceof Error ? err.message : String(err)));
@@ -22,17 +22,14 @@ export function extractDirectivesTS(root, _source) {
22
22
  }
23
23
  return directives;
24
24
  }
25
- /**
26
- * Extractor shared by TypeScript, TSX and JavaScript.
27
- * TS-only node types (interface/type/enum) simply never appear in JS sources.
28
- */
29
25
  export function extractTypeScript(root, _source) {
30
- return collect(namedChildren(root), false);
26
+ const typeIndex = buildTypeIndex(root);
27
+ return collect(namedChildren(root), false, typeIndex);
31
28
  }
32
- function collect(nodes, exported) {
29
+ function collect(nodes, exported, typeIndex) {
33
30
  const out = [];
34
31
  for (const n of nodes) {
35
- const res = handle(n, exported);
32
+ const res = handle(n, exported, typeIndex);
36
33
  if (Array.isArray(res))
37
34
  out.push(...res);
38
35
  else if (res)
@@ -40,16 +37,16 @@ function collect(nodes, exported) {
40
37
  }
41
38
  return out;
42
39
  }
43
- function handle(node, exported) {
40
+ function handle(node, exported, typeIndex) {
44
41
  switch (node.type) {
45
42
  case "export_statement":
46
43
  // `export <decl>` / `export default <decl>` — mark the inner declarations exported.
47
- return collect(namedChildren(node), true);
44
+ return collect(namedChildren(node), true, typeIndex);
48
45
  case "class_declaration":
49
46
  case "abstract_class_declaration": {
50
47
  const name = nameOf(node) ?? "(anonymous class)";
51
48
  const body = node.childForFieldName("body");
52
- const children = body ? collect(namedChildren(body), false) : [];
49
+ const children = body ? collect(namedChildren(body), false, typeIndex) : [];
53
50
  return makeSymbol({
54
51
  name,
55
52
  kind: "class",
@@ -63,7 +60,7 @@ function handle(node, exported) {
63
60
  case "interface_declaration": {
64
61
  const name = nameOf(node) ?? "(anonymous interface)";
65
62
  const body = node.childForFieldName("body");
66
- const children = body ? collect(namedChildren(body), false) : [];
63
+ const children = body ? collect(namedChildren(body), false, typeIndex) : [];
67
64
  return makeSymbol({
68
65
  name,
69
66
  kind: "interface",
@@ -78,7 +75,7 @@ function handle(node, exported) {
78
75
  case "generator_function_declaration": {
79
76
  const name = nameOf(node) ?? "(anonymous function)";
80
77
  const body = node.childForFieldName("body");
81
- return makeSymbol({
78
+ const fnSym = makeSymbol({
82
79
  name,
83
80
  kind: "function",
84
81
  node,
@@ -87,6 +84,8 @@ function handle(node, exported) {
87
84
  exported,
88
85
  doc: leadingComment(node),
89
86
  });
87
+ attachComponentInfo(fnSym, node, null, name, typeIndex);
88
+ return fnSym;
90
89
  }
91
90
  case "type_alias_declaration":
92
91
  return makeSymbol({
@@ -109,7 +108,7 @@ function handle(node, exported) {
109
108
  });
110
109
  case "lexical_declaration":
111
110
  case "variable_declaration":
112
- return fromVariableDeclaration(node, exported);
111
+ return fromVariableDeclaration(node, exported, typeIndex);
113
112
  case "method_definition":
114
113
  case "method_signature":
115
114
  case "abstract_method_signature": {
@@ -148,7 +147,7 @@ function handle(node, exported) {
148
147
  return null;
149
148
  }
150
149
  }
151
- function fromVariableDeclaration(node, exported) {
150
+ function fromVariableDeclaration(node, exported, typeIndex) {
152
151
  const out = [];
153
152
  for (const decl of namedChildren(node)) {
154
153
  if (decl.type !== "variable_declarator")
@@ -159,7 +158,7 @@ function fromVariableDeclaration(node, exported) {
159
158
  continue;
160
159
  if (value && (value.type === "arrow_function" || value.type === "function" || value.type === "function_expression")) {
161
160
  const body = value.childForFieldName("body");
162
- out.push(makeSymbol({
161
+ const arrowSym = makeSymbol({
163
162
  name,
164
163
  kind: "function",
165
164
  node: decl,
@@ -167,12 +166,14 @@ function fromVariableDeclaration(node, exported) {
167
166
  signature: headerSignature(value, body),
168
167
  exported,
169
168
  doc: leadingComment(node),
170
- }));
169
+ });
170
+ attachComponentInfo(arrowSym, value, decl, name, typeIndex);
171
+ out.push(arrowSym);
171
172
  }
172
173
  else if (value && (value.type === "class_expression" || value.type === "class")) {
173
174
  // const MyClass = class { ... }
174
175
  const body = value.childForFieldName("body");
175
- const children = body ? collect(namedChildren(body), false) : [];
176
+ const children = body ? collect(namedChildren(body), false, typeIndex) : [];
176
177
  out.push(makeSymbol({
177
178
  name,
178
179
  kind: "class",
@@ -318,3 +319,138 @@ function memberVisibility(node) {
318
319
  return "private";
319
320
  return "public";
320
321
  }
322
+ // ─── React/TSX component prop extraction ──────────────────────────────────────
323
+ const JSX_NODES = new Set(["jsx_element", "jsx_self_closing_element", "jsx_fragment"]);
324
+ function firstNamed(node) {
325
+ return node.namedChildCount > 0 ? node.namedChild(0) : null;
326
+ }
327
+ /**
328
+ * Index every top-level (and exported) interface / object-type alias by name,
329
+ * mapping it to its prop fields. Used to resolve a component's named props type
330
+ * (e.g. `ButtonProps`) back to its individual props.
331
+ */
332
+ function buildTypeIndex(root) {
333
+ const idx = new Map();
334
+ const visit = (nodes) => {
335
+ for (const n of nodes) {
336
+ if (n.type === "export_statement") {
337
+ visit(namedChildren(n));
338
+ continue;
339
+ }
340
+ if (n.type === "interface_declaration") {
341
+ const name = nameOf(n);
342
+ const body = n.childForFieldName("body");
343
+ if (name && body)
344
+ idx.set(name, propsFromMembers(body));
345
+ }
346
+ else if (n.type === "type_alias_declaration") {
347
+ const name = nameOf(n);
348
+ const val = n.childForFieldName("value");
349
+ if (name && val && val.type === "object_type")
350
+ idx.set(name, propsFromMembers(val));
351
+ }
352
+ }
353
+ };
354
+ visit(namedChildren(root));
355
+ return idx;
356
+ }
357
+ /** Read `property_signature` members out of an interface_body / object_type. */
358
+ function propsFromMembers(container) {
359
+ const props = [];
360
+ for (const m of namedChildren(container)) {
361
+ if (m.type !== "property_signature")
362
+ continue;
363
+ const nameNode = m.childForFieldName("name");
364
+ if (!nameNode)
365
+ continue;
366
+ const info = { name: nameNode.text };
367
+ const typeAnn = m.childForFieldName("type");
368
+ const typeNode = typeAnn ? firstNamed(typeAnn) : null;
369
+ if (typeNode)
370
+ info.type = typeNode.text.replace(/\s+/g, " ").trim();
371
+ const colon = m.text.indexOf(":");
372
+ const head = colon >= 0 ? m.text.slice(0, colon) : m.text;
373
+ if (head.includes("?"))
374
+ info.optional = true;
375
+ props.push(info);
376
+ }
377
+ return props;
378
+ }
379
+ /** Walk a function body looking for any JSX node (marks it a React component). */
380
+ function returnsJSX(node) {
381
+ if (!node)
382
+ return false;
383
+ let found = false;
384
+ const walk = (n) => {
385
+ if (found)
386
+ return;
387
+ if (JSX_NODES.has(n.type)) {
388
+ found = true;
389
+ return;
390
+ }
391
+ for (let i = 0; i < n.namedChildCount; i++) {
392
+ const c = n.namedChild(i);
393
+ if (c)
394
+ walk(c);
395
+ }
396
+ };
397
+ walk(node);
398
+ return found;
399
+ }
400
+ /**
401
+ * If `typeNode` is `FC<P>` / `React.FC<P>` / `FunctionComponent<P>` (or the
402
+ * React-qualified form), return the first type argument node (the props type).
403
+ */
404
+ function fcTypeArgument(typeNode) {
405
+ if (!typeNode || typeNode.type !== "generic_type")
406
+ return null;
407
+ const base = typeNode.childForFieldName("name");
408
+ const baseText = base ? base.text : "";
409
+ if (!/(^|\.)(FC|FunctionComponent)$/.test(baseText))
410
+ return null;
411
+ for (let i = 0; i < typeNode.namedChildCount; i++) {
412
+ const c = typeNode.namedChild(i);
413
+ if (c && c.type === "type_arguments")
414
+ return firstNamed(c);
415
+ }
416
+ return null;
417
+ }
418
+ /**
419
+ * Detect a React component (PascalCase + returns JSX, or typed as FC) and
420
+ * attach its props. `funcNode` is the function/arrow; `declNode` is the
421
+ * variable_declarator when the component is `const X: React.FC<P> = ...`.
422
+ */
423
+ function attachComponentInfo(sym, funcNode, declNode, name, idx) {
424
+ if (!/^[A-Z]/.test(name))
425
+ return; // components are PascalCase
426
+ let propsTypeNode = null;
427
+ let fc = false;
428
+ if (declNode) {
429
+ const ta = declNode.childForFieldName("type");
430
+ const arg = ta ? fcTypeArgument(firstNamed(ta)) : null;
431
+ if (arg) {
432
+ propsTypeNode = arg;
433
+ fc = true;
434
+ }
435
+ }
436
+ if (!fc && !returnsJSX(funcNode.childForFieldName("body")))
437
+ return; // not a component
438
+ if (!propsTypeNode) {
439
+ const params = funcNode.childForFieldName("parameters");
440
+ const first = params ? firstNamed(params) : null; // required/optional_parameter
441
+ const ta = first ? first.childForFieldName("type") : null;
442
+ if (ta)
443
+ propsTypeNode = firstNamed(ta);
444
+ }
445
+ if (!propsTypeNode)
446
+ return; // component, but untyped props — nothing to extract
447
+ if (propsTypeNode.type === "object_type") {
448
+ sym.props = propsFromMembers(propsTypeNode);
449
+ return;
450
+ }
451
+ const typeName = propsTypeNode.text.replace(/\s+/g, " ").trim();
452
+ sym.propsType = typeName;
453
+ const resolved = idx.get(typeName);
454
+ if (resolved)
455
+ sym.props = resolved;
456
+ }
@@ -241,3 +241,39 @@ export function getTopSymbols(graph, limit = 10) {
241
241
  }
242
242
  return results.sort((a, b) => b.importCount - a.importCount).slice(0, limit);
243
243
  }
244
+ /**
245
+ * Find symbol names that are exported from more than one file. These are often
246
+ * accidental collisions (copy-paste, parallel implementations) that make a
247
+ * codebase harder to navigate and can cause the wrong import to be auto-suggested.
248
+ *
249
+ * Only exported symbols are considered, and a name must appear in at least two
250
+ * distinct files to count as a duplicate.
251
+ */
252
+ export function findDuplicateSymbols(graph) {
253
+ const byName = new Map();
254
+ for (const node of graph.nodes) {
255
+ if (node.nodeType !== "symbol")
256
+ continue;
257
+ const sym = node;
258
+ if (!sym.exported)
259
+ continue;
260
+ const arr = byName.get(sym.symbol) ?? [];
261
+ arr.push(sym);
262
+ byName.set(sym.symbol, arr);
263
+ }
264
+ const out = [];
265
+ for (const [name, syms] of byName) {
266
+ // Collapse to one location per file (a file may declare the name once).
267
+ const perFile = new Map();
268
+ for (const s of syms)
269
+ if (!perFile.has(s.file))
270
+ perFile.set(s.file, s);
271
+ if (perFile.size < 2)
272
+ continue;
273
+ const locations = [...perFile.values()]
274
+ .map((s) => ({ file: s.file, kind: s.kind, nodeId: s.id }))
275
+ .sort((a, b) => a.file.localeCompare(b.file));
276
+ out.push({ symbol: name, count: perFile.size, locations });
277
+ }
278
+ return out.sort((a, b) => b.count - a.count || a.symbol.localeCompare(b.symbol));
279
+ }
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { z } from "zod";
7
+ import { fileURLToPath } from "node:url";
7
8
  import { resolveOptions, loadProjectConfig } from "./config.js";
8
9
  import { buildSkeleton, collectSourceFiles, UnsupportedLanguageError, } from "./skeleton.js";
9
10
  import { renderHtml, renderCombinedHtml } from "./html.js";
@@ -11,7 +12,7 @@ import { supportedLanguages } from "./registry.js";
11
12
  import { findSymbol, findRelatedSymbols, findServerImports, isApiRoute, findMissingTryCatch, checkGeneralRules, GENERAL_RULE_DEFAULTS, } from "./analysis.js";
12
13
  import { resolveFileImports } from "./resolver.js";
13
14
  import { buildSymbolGraph } from "./graph.js";
14
- import { findDeadExports, findCircularDeps, getChangeImpact, getFileDeps, getTopSymbols } from "./graph-analysis.js";
15
+ import { findDeadExports, findCircularDeps, getChangeImpact, getFileDeps, getTopSymbols, findDuplicateSymbols } from "./graph-analysis.js";
15
16
  import { buildCallGraph } from "./callgraph.js";
16
17
  import { searchSymbols } from "./search.js";
17
18
  /** Files may only be read inside this root (override with AST_MAP_ROOT). */
@@ -44,9 +45,19 @@ function errorText(message) {
44
45
  content: [{ type: "text", text: message }],
45
46
  };
46
47
  }
48
+ /** Read the package version at runtime so it never drifts from package.json. */
49
+ const PKG_VERSION = (() => {
50
+ try {
51
+ const dir = path.dirname(fileURLToPath(import.meta.url));
52
+ return JSON.parse(fs.readFileSync(path.join(dir, "..", "package.json"), "utf8")).version;
53
+ }
54
+ catch {
55
+ return "0.0.0";
56
+ }
57
+ })();
47
58
  const server = new McpServer({
48
59
  name: "universal-ast-mapper",
49
- version: "0.5.3",
60
+ version: PKG_VERSION,
50
61
  });
51
62
  /* ----------------------- tool: list_supported_languages ----------------------- */
52
63
  server.registerTool("list_supported_languages", {
@@ -512,6 +523,50 @@ server.registerTool("find_circular_deps", {
512
523
  return errorText(describeError(err));
513
524
  }
514
525
  });
526
+ /* ─────────────────── tool: find_duplicate_symbols ──────────────────────── */
527
+ server.registerTool("find_duplicate_symbols", {
528
+ title: "Find duplicate exported symbols",
529
+ description: "Scan a directory and return symbol names that are exported from more than one file. " +
530
+ "These are often accidental collisions (copy-paste, parallel implementations) that make " +
531
+ "a codebase harder to navigate. Each result lists every file/kind that declares the name.",
532
+ inputSchema: {
533
+ path: z
534
+ .string()
535
+ .describe("Directory to scan, relative to project root or absolute within it."),
536
+ },
537
+ }, async ({ path: input }) => {
538
+ try {
539
+ const { abs, rel } = resolveInRoot(input);
540
+ if (!fs.statSync(abs).isDirectory()) {
541
+ return errorText(`"${input}" is not a directory. find_duplicate_symbols requires a directory.`);
542
+ }
543
+ const opts = resolveOptions({ detail: "outline", emitHtml: false });
544
+ const files = collectSourceFiles(abs, opts);
545
+ const skeletons = [];
546
+ const errors = [];
547
+ for (const file of files) {
548
+ const fileRel = path.relative(ROOT, file).split(path.sep).join("/");
549
+ try {
550
+ skeletons.push(await buildSkeleton(file, fileRel, opts));
551
+ }
552
+ catch (err) {
553
+ errors.push({ file: fileRel, error: describeError(err) });
554
+ }
555
+ }
556
+ const graph = buildSymbolGraph(skeletons, ROOT);
557
+ const duplicates = findDuplicateSymbols(graph);
558
+ return jsonText({
559
+ directory: rel.split(path.sep).join("/"),
560
+ scanned: files.length,
561
+ duplicateCount: duplicates.length,
562
+ ...(errors.length > 0 ? { errors } : {}),
563
+ duplicates,
564
+ });
565
+ }
566
+ catch (err) {
567
+ return errorText(describeError(err));
568
+ }
569
+ });
515
570
  /* ─────────────────── tool: get_change_impact ───────────────────────────── */
516
571
  server.registerTool("get_change_impact", {
517
572
  title: "Get change impact (blast radius)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-ast-mapper",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "MCP server that maps source files into a normalized code skeleton (JSON + HTML) using tree-sitter.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",