universal-ast-mapper 1.7.2 → 1.10.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 ADDED
@@ -0,0 +1,127 @@
1
+ # Changelog
2
+
3
+ All notable changes to **universal-ast-mapper** (AST-MCP). Format based on
4
+ [Keep a Changelog](https://keepachangelog.com/); this project follows semver and,
5
+ since 1.0.0, guarantees a stable MCP tool / CLI surface across the 1.x line.
6
+
7
+ ---
8
+
9
+ ## [1.10.0] — 2026-06-01 · Source maps
10
+ - **`read_source_map`** MCP tool + **`ast-map sourcemap <file>`** CLI: trace a
11
+ compiled JS/CSS file (inline `data:` or external `.map`) back to its original
12
+ sources; honors `sourceRoot` and reports embedded `sourcesContent`.
13
+ - Ruby re-investigated and confirmed blocked (external-scanner grammar needs
14
+ web-tree-sitter ≥0.22; engine upgrade would risk the 12 working languages).
15
+
16
+ ## [1.9.0] — 2026-06-01 · Watch mode
17
+ - **`ast-map watch [dir]`** — debounced, coalesced rebuild of the dependency
18
+ analysis (files · dead exports · cycles) on every source change; `-o file.html`
19
+ regenerates the live explorer too.
20
+ - Explorer debug readout hidden by default (toggle with `d`).
21
+
22
+ ## [1.8.0] — 2026-06-01 · Explorer detail sidebar
23
+ - Click a file in `ast-map explore` for a side panel: language, symbol count,
24
+ symbols, **Imports** and **Imported by** (each clickable to navigate the graph).
25
+ - **1.8.1–1.8.3 (fixes):** explorer now reliably centers/fills the viewport —
26
+ separated orphan files into a tidy grid, clamped the force layout to stop nodes
27
+ being flung to huge coordinates, and sized the canvas from `innerWidth/innerHeight`.
28
+
29
+ ## [1.7.0] — 2026-06-01 · Web UI graph explorer
30
+ - **`ast-map explore [dir]`** writes a self-contained, dependency-free interactive
31
+ HTML: a force-directed file dependency graph (drag / zoom / pan / click-to-
32
+ highlight / name filter). Opens in any browser, no build step.
33
+ - **1.7.1–1.7.3 (fixes):** auto-fit and layout tuning.
34
+
35
+ ## [1.6.0] — 2026-06-01 · MCP resource endpoints
36
+ - Browseable resources: **`ast://languages`**, **`ast://skeleton/{path}`**
37
+ (templated, one per file via `resources/list`), **`ast://graph`**. Agents can
38
+ list/read codebase structure as resources, not just call tools.
39
+
40
+ ## [1.5.0] — 2026-06-01 · `.d.ts` / ambient declarations
41
+ - Extract `declare function/const/class`, `declare module "x"`, and
42
+ `declare namespace` (plus plain `namespace`); a `.d.ts` used to yield 0 symbols.
43
+ - New `namespace` symbol kind.
44
+
45
+ ## [1.4.0] — 2026-06-01 · Dynamic import tracking
46
+ - Capture dynamic `import("...")` and CommonJS `require("...")` with an
47
+ `isDynamic` flag; relative ones resolve and draw graph edges like static imports.
48
+
49
+ ## [1.3.0] — 2026-06-01 · TS/JS decorators
50
+ - Class and method symbols carry a `decorators` field (`@Component`, `@Get(...)`),
51
+ in skeletons and `get_call_graph`. Extends the Python decorator support to TS/JS.
52
+
53
+ ## [1.2.0] — 2026-06-01 · File-level cross-package resolution
54
+ - In a monorepo, bare imports of a workspace package (`@org/utils`, `@org/utils/sub`)
55
+ resolve to the real source file (prefers `src/` over `dist/`), so `resolve_imports`
56
+ marks them in-project and `build_symbol_graph` draws cross-package edges.
57
+
58
+ ## [1.1.0] — 2026-06-01 · Monorepo support
59
+ - **`analyze_workspace`** tool + **`ast-map workspace`** CLI: discover packages
60
+ (npm/yarn `workspaces`, `pnpm-workspace.yaml`, `lerna.json`), map internal
61
+ package dependencies, and detect circular package deps.
62
+
63
+ ## [1.0.0] — 2026-06-01 · Stable release 🎉
64
+ - Locked public API (MCP tool names + schemas, CLI surface) for the 1.x line.
65
+ - Bundled **GitHub Action** (`action.yml`) running `ast-map validate` as a CI gate,
66
+ plus a project CI workflow.
67
+ - 12 languages · 18 MCP tools / 17 CLI commands at release.
68
+
69
+ ## [0.9.0] — 2026-05-31 · Scoped type-flow tracing
70
+ - **`trace_type`** tool + **`ast-map trace-type`** CLI: follow a named type through
71
+ function params, return types, typed variables, and class fields. Completes the
72
+ deeper-analysis suite.
73
+
74
+ ## [0.8.7] — 2026-05-31 · Python decorators
75
+ - `decorators` field on Python symbols + `get_call_graph`; traces
76
+ `@router.get(...)` → handler and stacked decorators.
77
+
78
+ ## [0.8.6] — 2026-05-31 · Unused parameter detection
79
+ - **`find_unused_params`** tool + **`ast-map unused-params`** CLI: named functions
80
+ whose params are never referenced (low false-positive; counts object shorthand).
81
+
82
+ ## [0.8.5] — 2026-05-31 · Cyclomatic complexity
83
+ - **`get_complexity`** tool + **`ast-map complexity`** CLI: per-function score with
84
+ low/moderate/high/very-high ratings and directory hotspots.
85
+
86
+ ## [0.8.4] — 2026-05-31 · Duplicate symbol detection
87
+ - **`find_duplicate_symbols`** tool + **`ast-map duplicates`** CLI: exported names
88
+ declared in 2+ files.
89
+
90
+ ## [0.8.3] — 2026-05-31 · TSX/React component props
91
+ - Component symbols carry `propsType` + `props[]`; detects `React.FC<P>` and
92
+ JSX-returning PascalCase functions. MCP server version now read from package.json.
93
+
94
+ ## [0.8.2] — 2026-05-30 · Swift cross-file wiring
95
+ - `import <Module>` → that module's files (`Sources/<Module>/`). Completes
96
+ cross-file graph/resolver support for all four v0.8.0 languages.
97
+
98
+ ## [0.8.1] — 2026-05-30 · Kotlin + C/C++ cross-file wiring
99
+ - Kotlin FQCN/package index; C/C++ `#include` resolution with header↔impl pairing.
100
+ - Fixes: parse-cache rel-path leak; Kotlin call-graph extraction.
101
+
102
+ ---
103
+
104
+ ## Earlier (pre-session history)
105
+
106
+ - **0.8.0** — +4 languages: C · C++ · Kotlin · Swift (symbol extraction + imports).
107
+ - **0.7.0** — Go full module resolution; C# reverse `calledBy`; 4-suite test harness.
108
+ - **0.6.0** — +3 languages: Rust · Java · C#; cross-language resolver.
109
+ - **0.5.x** — `/ast-map` skill auto-install; iterative DFS; barrel re-exports; parse cache; call-graph aliases; `.ast-map.config.json`.
110
+ - **0.4.0** — `search_symbol`, `get_file_deps`, `get_top_symbols`, dead-code tiers.
111
+ - **0.3.0** — CLI; `find_dead_code`, `find_circular_deps`, `get_change_impact`, `get_call_graph`.
112
+ - **0.2.0** — import extraction; `resolve_imports`; `build_symbol_graph`.
113
+ - **0.1.0** — `get_skeleton_json`, `generate_skeleton`, `get_symbol_context`, `validate_architecture`.
114
+
115
+ [1.10.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.10.0
116
+ [1.9.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.9.0
117
+ [1.8.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.8.0
118
+ [1.7.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.7.0
119
+ [1.6.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.6.0
120
+ [1.5.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.5.0
121
+ [1.4.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.4.0
122
+ [1.3.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.3.0
123
+ [1.2.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.2.0
124
+ [1.1.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.1.0
125
+ [1.0.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.0.0
126
+ [0.9.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v0.9.0
127
+ [0.8.1]: https://github.com/6ixthxense/AST-MCP/releases/tag/v0.8.1
package/README.md CHANGED
@@ -99,6 +99,8 @@ ast-map unused-params <path> [alias: unused]
99
99
  ast-map trace-type <type> [dir] [alias: flow]
100
100
  ast-map workspace [dir] [alias: ws]
101
101
  ast-map explore [dir] [-o out.html]
102
+ ast-map watch [dir] [-o out.html]
103
+ ast-map sourcemap <file>
102
104
  ast-map search <pattern> [dir] [-m contains|exact|regex] [-k kind] [-e]
103
105
  ast-map deps <file> [--scan <dir>]
104
106
  ast-map top <dir> [-n 10]
@@ -618,6 +620,12 @@ Not part of the public API: the internal `src/` module layout and the generated
618
620
 
619
621
  | Version | What changed |
620
622
  |---------|--------------|
623
+ | **1.10.0** | **Source-map support** — new `read_source_map` MCP tool + `ast-map sourcemap <file>` CLI: given a compiled JS/CSS file with an inline (`data:`) or external `sourceMappingURL`, returns the original source files it maps back to (honors `sourceRoot`). Traces `dist/` output back to source. |
624
+ | **1.9.0** | **Watch mode** — `ast-map watch [dir]` recomputes the dependency analysis (file count · dead exports · cycles) on every source-file change, debounced; `-o file.html` also regenerates the live explorer each time. Plus: the explorer debug readout is now hidden (toggle with `d`). |
625
+ | **1.8.2** | **Explorer stability fix** — clamp the force layout (distance floor + velocity cap) so nodes that initialize close together can't be flung to huge coordinates, which was blowing up the bounding box and shrinking the whole graph into a corner. Now reliably centers and fills. |
626
+ | **1.8.1** | **Explorer self-heal sizing** — the explorer now re-checks canvas size every frame and re-fits, so it always centers/fills even if the canvas reports zero size at load (and survives container resizes). |
627
+ | **1.8.0** | **Explorer detail sidebar** — click a file in `ast-map explore` to open a side panel: language, symbol count, its symbols, the files it imports, and the files that import it — each clickable to jump to that file. |
628
+ | **1.7.3** | **Explorer layout overhaul** — only connected files are force-laid (centered + filling the viewport); orphan files with no in-scope deps are parked in a tidy grid below instead of sprawling. Verified centered at any window size. |
621
629
  | **1.7.2** | **Explorer fit, really this time** — continuous auto-fit until you interact, robust canvas sizing, and centered node init, so the graph fills the viewport instead of bunching in a corner. |
622
630
  | **1.7.1** | **Explorer fit fix** — the `ast-map explore` graph now auto-fits the viewport (spreads to fill the screen instead of clustering in the centre); double-click re-fits. |
623
631
  | **1.7.0** | **Web UI graph explorer** — `ast-map explore [dir]` writes a self-contained, dependency-free interactive HTML: a force-directed file dependency graph (drag, zoom, click-to-highlight neighbours, filter by name). No build step, no external scripts — just open it in a browser. |
package/dist/cli.js CHANGED
@@ -15,6 +15,7 @@ import { findUnusedParams } from "./unused-params.js";
15
15
  import { traceTypeInFile } from "./typeflow.js";
16
16
  import { discoverWorkspace, findPackageCycles } from "./workspace.js";
17
17
  import { buildExplorerHtml } from "./explorer.js";
18
+ import { readSourceMap } from "./sourcemap.js";
18
19
  import { buildCallGraph } from "./callgraph.js";
19
20
  import { searchSymbols } from "./search.js";
20
21
  const ROOT = path.resolve(process.env.AST_MAP_ROOT ?? process.cwd());
@@ -371,6 +372,80 @@ program
371
372
  }
372
373
  console.log();
373
374
  });
375
+ // ─── Command: watch ───────────────────────────────────────────────────────────
376
+ program
377
+ .command("watch [dir]")
378
+ .description("Rebuild analysis (and optionally the explorer) when files change")
379
+ .option("-o, --out <file>", "Also regenerate the explorer HTML on each change")
380
+ .action(async (dir, opts) => {
381
+ const { abs, rel } = resolveArg(dir ?? ".");
382
+ if (!fs.statSync(abs).isDirectory())
383
+ die(`"${rel}" is not a directory`);
384
+ let building = false;
385
+ let queued = false;
386
+ async function rebuild(reason) {
387
+ if (building) {
388
+ queued = true;
389
+ return;
390
+ }
391
+ building = true;
392
+ try {
393
+ const skels = await gatherSkeletons(abs);
394
+ const graph = buildSymbolGraph(skels, ROOT);
395
+ const dead = findDeadExports(graph).filter((d) => d.confidence === "high").length;
396
+ const cycles = findCircularDeps(graph).length;
397
+ let line = `${dim(new Date().toLocaleTimeString())} ${bold(String(skels.length))} files · ${dead} dead · ${cycles} cycle(s)`;
398
+ if (opts.out) {
399
+ fs.writeFileSync(path.resolve(process.cwd(), opts.out), buildExplorerHtml(graph, abs), "utf8");
400
+ line += ` · ${green("explorer updated")}`;
401
+ }
402
+ line += ` ${dim(reason)}`;
403
+ console.log(line);
404
+ }
405
+ finally {
406
+ building = false;
407
+ if (queued) {
408
+ queued = false;
409
+ rebuild("(coalesced)");
410
+ }
411
+ }
412
+ }
413
+ header(`Watching ${rel}/ ${dim("(Ctrl+C to stop)")}`);
414
+ await rebuild("initial");
415
+ let timer = null;
416
+ const exts = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", ".cs", ".c", ".cpp", ".h", ".hpp", ".kt", ".swift"]);
417
+ fs.watch(abs, { recursive: true }, (_evt, file) => {
418
+ if (!file)
419
+ return;
420
+ const f = String(file).split(path.sep).join("/");
421
+ if (/(^|\/)(node_modules|\.git|dist|\.ast-map)(\/|$)/.test(f))
422
+ return;
423
+ if (!exts.has(path.extname(f).toLowerCase()))
424
+ return;
425
+ if (timer)
426
+ clearTimeout(timer);
427
+ timer = setTimeout(() => rebuild(`(${f.split("/").pop()} changed)`), 300);
428
+ });
429
+ await new Promise(() => { }); // keep the process alive
430
+ });
431
+ // ─── Command: sourcemap ───────────────────────────────────────────────────────
432
+ program
433
+ .command("sourcemap <file>")
434
+ .description("Show the original sources a compiled file maps back to")
435
+ .option("--json", "Output as JSON")
436
+ .action(async (inputPath, opts) => {
437
+ const { abs, rel } = resolveArg(inputPath);
438
+ const info = readSourceMap(abs, rel);
439
+ if (!info)
440
+ die(`No source map found for "${rel}"`);
441
+ if (opts.json)
442
+ return jsonOut(info);
443
+ header(`Source Map — ${rel} ${dim("(" + info.mapKind + ")")}`);
444
+ for (const sourceFile of info.sources)
445
+ console.log(indent(green("←") + " " + sourceFile));
446
+ console.log(`\n ${info.sources.length} original source(s)` + (info.hasContent ? dim(" · embeds sourcesContent") : ""));
447
+ console.log();
448
+ });
374
449
  // ─── Command: explore ─────────────────────────────────────────────────────────
375
450
  program
376
451
  .command("explore [dir]")
package/dist/explorer.js CHANGED
@@ -1,13 +1,26 @@
1
1
  /** Derive a file-level dependency graph (nodes = files, edges = imports). */
2
2
  function deriveFileGraph(graph) {
3
3
  const nodeMap = new Map(graph.nodes.map((n) => [n.id, n]));
4
+ // top-level symbol names per file (for the detail panel).
5
+ const fileSyms = new Map();
6
+ for (const n of graph.nodes) {
7
+ if (n.nodeType !== "symbol")
8
+ continue;
9
+ const s = n;
10
+ if (s.id.indexOf("::") !== s.id.lastIndexOf("::"))
11
+ continue; // skip nested (one :: only)
12
+ const arr = fileSyms.get(s.file) ?? [];
13
+ if (arr.length < 60)
14
+ arr.push(s.kind + " " + s.symbol);
15
+ fileSyms.set(s.file, arr);
16
+ }
4
17
  const nodes = [];
5
18
  for (const n of graph.nodes) {
6
19
  if (n.nodeType !== "file")
7
20
  continue;
8
21
  const f = n;
9
22
  const parts = f.id.split("/");
10
- nodes.push({ id: f.id, symbols: f.symbolCount, group: parts.length > 1 ? parts[0] : "(root)", lang: f.language });
23
+ nodes.push({ id: f.id, symbols: f.symbolCount, group: parts.length > 1 ? parts[0] : "(root)", lang: f.language, syms: fileSyms.get(f.id) ?? [] });
11
24
  }
12
25
  const seen = new Set();
13
26
  const links = [];
@@ -27,34 +40,58 @@ function deriveFileGraph(graph) {
27
40
  return { nodes, links };
28
41
  }
29
42
  const STYLE = "body{margin:0;font-family:system-ui,sans-serif;color:#222;background:#fafafa}" +
30
- "#bar{position:fixed;top:0;left:0;right:0;height:48px;display:flex;align-items:center;gap:12px;padding:0 14px;background:#fff;border-bottom:1px solid #e5e5e5;z-index:2;box-sizing:border-box}" +
43
+ "#bar{position:fixed;top:0;left:0;right:0;height:48px;display:flex;align-items:center;gap:12px;padding:0 14px;background:#fff;border-bottom:1px solid #e5e5e5;z-index:4;box-sizing:border-box}" +
31
44
  "#bar h1{font-size:14px;margin:0;font-weight:600}#bar .muted{color:#888;font-size:12px}" +
32
- "#q{flex:0 0 220px;padding:6px 10px;border:1px solid #ddd;border-radius:6px;font-size:13px}" +
45
+ "#q{flex:0 0 200px;padding:6px 10px;border:1px solid #ddd;border-radius:6px;font-size:13px}" +
33
46
  "#cv{position:fixed;top:48px;left:0;right:0;bottom:0;display:block;cursor:grab}" +
34
- "#tip{position:fixed;pointer-events:none;background:#222;color:#fff;font-size:12px;padding:4px 8px;border-radius:5px;display:none;z-index:3}" +
35
- "@media(prefers-color-scheme:dark){body{color:#ddd;background:#161616}#bar{background:#1e1e1e;border-color:#333}#q{background:#2a2a2a;border-color:#444;color:#ddd}}";
36
- const CLIENT = "var c=document.getElementById('cv'),ctx=c.getContext('2d'),tip=document.getElementById('tip');" +
37
- "var W,H;function resize(){var r=devicePixelRatio||1;W=c.clientWidth||innerWidth;H=c.clientHeight||(innerHeight-48);c.width=W*r;c.height=H*r;ctx.setTransform(r,0,0,r,0,0);}addEventListener('resize',resize);resize();" +
38
- "var nodes=DATA.nodes,links=DATA.links,byId={};nodes.forEach(function(n){n.x=W/2+(Math.random()-0.5)*Math.min(W||800,600);n.y=H/2+(Math.random()-0.5)*Math.min(H||600,500);n.vx=0;n.vy=0;byId[n.id]=n;});" +
47
+ "#tip{position:fixed;pointer-events:none;background:#222;color:#fff;font-size:12px;padding:4px 8px;border-radius:5px;display:none;z-index:5}" +
48
+ "#panel{position:fixed;top:48px;right:0;bottom:0;width:300px;background:#fff;border-left:1px solid #e5e5e5;z-index:3;overflow-y:auto;padding:14px 16px;box-sizing:border-box;display:none;font-size:13px}" +
49
+ "#panel h2{font-size:14px;margin:0 0 2px;word-break:break-all}#panel .path{color:#888;font-size:11px;margin-bottom:10px;word-break:break-all}" +
50
+ "#panel .meta{color:#555;margin-bottom:12px}#panel h3{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:#999;margin:14px 0 6px}" +
51
+ "#panel .row{padding:3px 6px;border-radius:5px;cursor:pointer;word-break:break-all;line-height:1.5}#panel .row:hover{background:#f0f0f0}" +
52
+ "#panel .sym{color:#444;padding:2px 6px;word-break:break-all}#panel .k{color:#999;font-size:11px}" +
53
+ "#close{position:absolute;top:10px;right:12px;cursor:pointer;color:#999;font-size:18px;line-height:1;border:none;background:none}" +
54
+ "@media(prefers-color-scheme:dark){body{color:#ddd;background:#161616}#bar,#panel{background:#1e1e1e;border-color:#333}#q{background:#2a2a2a;border-color:#444;color:#ddd}#panel .row:hover{background:#2a2a2a}#panel .sym{color:#bbb}}";
55
+ const CLIENT = "var c=document.getElementById('cv'),ctx=c.getContext('2d'),tip=document.getElementById('tip'),panel=document.getElementById('panel');" +
56
+ "var PANELW=300,panelOpen=false;" +
57
+ "var W,H;function resize(){var r=devicePixelRatio||1;W=innerWidth||c.clientWidth||800;H=(innerHeight-48)||c.clientHeight||600;c.width=W*r;c.height=H*r;ctx.setTransform(r,0,0,r,0,0);}addEventListener('resize',function(){resize();});resize();" +
58
+ "function availW(){return W-(panelOpen?PANELW:0);}" +
59
+ "var nodes=DATA.nodes,links=DATA.links,byId={};nodes.forEach(function(n){byId[n.id]=n;n.vx=0;n.vy=0;});" +
60
+ "var deg={},out={},inn={};links.forEach(function(l){deg[l.source]=(deg[l.source]||0)+1;deg[l.target]=(deg[l.target]||0)+1;(out[l.source]=out[l.source]||[]).push(l.target);(inn[l.target]=inn[l.target]||[]).push(l.source);});" +
61
+ "var sim=nodes.filter(function(n){return deg[n.id];}),orphans=nodes.filter(function(n){return !deg[n.id];});" +
62
+ "sim.forEach(function(n){n.x=W/2+(Math.random()-0.5)*240;n.y=H/2+(Math.random()-0.5)*240;});" +
39
63
  "var groups={},gi=0;function color(g){if(groups[g]==null)groups[g]=gi++;return 'hsl('+((groups[g]*67)%360)+',58%,55%)';}" +
40
64
  "var adj={};links.forEach(function(l){(adj[l.source]=adj[l.source]||[]).push(l.target);(adj[l.target]=adj[l.target]||[]).push(l.source);});" +
41
- "var view={x:0,y:0,k:1},sel=null,hover=null,drag=null,pan=null,q='',autofit=true,frame=0;" +
65
+ "var view={x:0,y:0,k:1},sel=null,hover=null,drag=null,pan=null,q='',autofit=true;" +
42
66
  "function radius(n){return 4+Math.sqrt(n.symbols||0)*1.7;}" +
43
- "function tick(){var k=0.0006;for(var i=0;i<nodes.length;i++){var a=nodes[i];a.vx+=(W/2-a.x)*k;a.vy+=(H/2-a.y)*k;for(var j=i+1;j<nodes.length;j++){var b=nodes[j];var dx=a.x-b.x,dy=a.y-b.y,d2=dx*dx+dy*dy+0.01,d=Math.sqrt(d2),f=2600/d2,fx=f*dx/d,fy=f*dy/d;a.vx+=fx;a.vy+=fy;b.vx-=fx;b.vy-=fy;}}" +
44
- "links.forEach(function(l){var a=byId[l.source],b=byId[l.target];if(!a||!b)return;var dx=b.x-a.x,dy=b.y-a.y,d=Math.sqrt(dx*dx+dy*dy)+0.01,f=(d-115)*0.012,fx=f*dx/d,fy=f*dy/d;a.vx+=fx;a.vy+=fy;b.vx-=fx;b.vy-=fy;});" +
45
- "for(var i=0;i<nodes.length;i++){var n=nodes[i];if(n===drag)continue;n.vx*=0.86;n.vy*=0.86;n.x+=n.vx;n.y+=n.vy;}}" +
46
- "function draw(){ctx.clearRect(0,0,W,H);ctx.save();ctx.translate(view.x,view.y);ctx.scale(view.k,view.k);ctx.lineWidth=0.7;" +
47
- "links.forEach(function(l){var a=byId[l.source],b=byId[l.target];if(!a||!b)return;var on=sel&&(l.source===sel.id||l.target===sel.id);ctx.strokeStyle=on?'rgba(110,110,240,0.85)':'rgba(140,140,140,0.16)';ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke();});" +
48
- "nodes.forEach(function(n){var dim=(sel&&n!==sel&&(adj[sel.id]||[]).indexOf(n.id)<0)||(q&&n.id.toLowerCase().indexOf(q)<0);ctx.globalAlpha=dim?0.16:1;ctx.beginPath();ctx.arc(n.x,n.y,radius(n),0,6.2832);ctx.fillStyle=color(n.group);ctx.fill();if(n===sel||n===hover){ctx.lineWidth=2;ctx.strokeStyle='#111';ctx.stroke();ctx.lineWidth=0.7;}});" +
49
- "ctx.globalAlpha=1;ctx.fillStyle=getComputedStyle(document.body).color;ctx.font='11px system-ui';nodes.forEach(function(n){if(n===sel||n===hover||n.symbols>=14){ctx.fillText(n.id.split('/').pop(),n.x+radius(n)+3,n.y+3);}});ctx.restore();}" +
50
- "function fitView(){if(!nodes.length)return;var a=1e9,b=1e9,c2=-1e9,d2=-1e9;for(var i=0;i<nodes.length;i++){var n=nodes[i];if(n.x<a)a=n.x;if(n.y<b)b=n.y;if(n.x>c2)c2=n.x;if(n.y>d2)d2=n.y;}var bw=Math.max(c2-a,1),bh=Math.max(d2-b,1),k=Math.min(W/(bw+90),H/(bh+90));k=Math.max(0.12,Math.min(k,2.5));view.k=k;view.x=W/2-((a+c2)/2)*k;view.y=H/2-((b+d2)/2)*k;}function loop(){tick();tick();frame++;if(autofit)fitView();draw();requestAnimationFrame(loop);}" +
67
+ "function tick(){if(!sim.length)return;var k=0.0016;for(var i=0;i<sim.length;i++){var a=sim[i];a.vx+=(W/2-a.x)*k;a.vy+=(H/2-a.y)*k;for(var j=i+1;j<sim.length;j++){var b=sim[j];var dx=a.x-b.x,dy=a.y-b.y,d2=dx*dx+dy*dy;if(d2<100)d2=100;var d=Math.sqrt(d2),f=2200/d2,fx=f*dx/d,fy=f*dy/d;a.vx+=fx;a.vy+=fy;b.vx-=fx;b.vy-=fy;}}" +
68
+ "links.forEach(function(l){var a=byId[l.source],b=byId[l.target];if(!a||!b)return;var dx=b.x-a.x,dy=b.y-a.y,d=Math.sqrt(dx*dx+dy*dy)+0.01,f=(d-90)*0.02,fx=f*dx/d,fy=f*dy/d;a.vx+=fx;a.vy+=fy;b.vx-=fx;b.vy-=fy;});" +
69
+ "for(var i=0;i<sim.length;i++){var n=sim[i];if(n===drag)continue;n.vx*=0.85;n.vy*=0.85;if(n.vx>30)n.vx=30;if(n.vx<-30)n.vx=-30;if(n.vy>30)n.vy=30;if(n.vy<-30)n.vy=-30;n.x+=n.vx;n.y+=n.vy;}}" +
70
+ "function bb4(arr){var a=1e9,b=1e9,c2=-1e9,d2=-1e9;for(var i=0;i<arr.length;i++){var n=arr[i];if(n.x<a)a=n.x;if(n.y<b)b=n.y;if(n.x>c2)c2=n.x;if(n.y>d2)d2=n.y;}return[a,b,c2,d2];}" +
71
+ "function layoutOrphans(){if(!orphans.length)return;var bb=sim.length?bb4(sim):[W*0.3,H*0.3,W*0.7,H*0.5];var left=bb[0],bottom=bb[3]+46,wide=Math.max(bb[2]-bb[0],260);var cols=Math.max(1,Math.ceil(Math.sqrt(orphans.length*1.8)));var gap=Math.max(22,wide/cols);for(var i=0;i<orphans.length;i++){orphans[i].x=left+(i%cols)*gap;orphans[i].y=bottom+Math.floor(i/cols)*22;}}" +
72
+ "function fitView(){var bb=bb4(nodes);var aw=availW();var bw=Math.max(bb[2]-bb[0],1),bh=Math.max(bb[3]-bb[1],1),k=Math.min(aw/(bw+70),H/(bh+70));k=Math.max(0.12,Math.min(k,2.2));view.k=k;view.x=aw/2-((bb[0]+bb[2])/2)*k;view.y=H/2-((bb[1]+bb[3])/2)*k;}" +
73
+ "function center(n){view.k=Math.max(view.k,0.7);view.x=availW()/2-n.x*view.k;view.y=H/2-n.y*view.k;}" +
74
+ "function esc(t){return String(t).replace(/&/g,'&amp;').replace(/</g,'&lt;');}" +
75
+ "function rowList(ids){if(!ids||!ids.length)return '<div class=\"sym\" style=\"color:#aaa\">none</div>';return ids.slice().sort().map(function(id){return '<div class=\"row\" data-id=\"'+esc(id)+'\">'+esc(id)+'</div>';}).join('');}" +
76
+ "function showPanel(n){sel=n;panelOpen=true;var imp=out[n.id]||[],impBy=inn[n.id]||[];var syms=(n.syms||[]).map(function(s){var i=s.indexOf(' ');return '<div class=\"sym\"><span class=\"k\">'+esc(s.slice(0,i))+'</span> '+esc(s.slice(i+1))+'</div>';}).join('')||'<div class=\"sym\" style=\"color:#aaa\">none</div>';" +
77
+ "panel.innerHTML='<button id=\"close\">&times;</button>'+'<h2>'+esc(n.id.split('/').pop())+'</h2><div class=\"path\">'+esc(n.id)+'</div>'+'<div class=\"meta\">'+esc(n.lang)+' &middot; '+(n.symbols||0)+' symbols'+(deg[n.id]?'':' &middot; no in-scope deps')+'</div>'+'<h3>Imports ('+imp.length+')</h3>'+rowList(imp)+'<h3>Imported by ('+impBy.length+')</h3>'+rowList(impBy)+'<h3>Symbols</h3>'+syms;" +
78
+ "panel.style.display='block';}" +
79
+ "panel.addEventListener('click',function(e){if(e.target.id==='close'){panelOpen=false;sel=null;panel.style.display='none';autofit=true;return;}var id=e.target.getAttribute('data-id');if(id&&byId[id]){showPanel(byId[id]);center(byId[id]);}});" +
80
+ "function draw(){ctx.clearRect(0,0,W,H);ctx.save();ctx.translate(view.x,view.y);ctx.scale(view.k,view.k);ctx.lineWidth=0.8;" +
81
+ "links.forEach(function(l){var a=byId[l.source],b=byId[l.target];if(!a||!b)return;var on=sel&&(l.source===sel.id||l.target===sel.id);ctx.strokeStyle=on?'rgba(110,110,240,0.9)':'rgba(150,150,150,0.18)';ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke();});" +
82
+ "function dot(n,orphan){var dim=(sel&&n!==sel&&(adj[sel.id]||[]).indexOf(n.id)<0)||(q&&n.id.toLowerCase().indexOf(q)<0);ctx.globalAlpha=dim?0.14:(orphan?0.55:1);ctx.beginPath();ctx.arc(n.x,n.y,orphan?3.2:radius(n),0,6.2832);ctx.fillStyle=color(n.group);ctx.fill();if(n===sel||n===hover){ctx.lineWidth=2;ctx.strokeStyle='#fff';ctx.stroke();ctx.lineWidth=0.8;}}" +
83
+ "orphans.forEach(function(n){dot(n,true);});sim.forEach(function(n){dot(n,false);});" +
84
+ "ctx.globalAlpha=1;ctx.fillStyle=getComputedStyle(document.body).color;ctx.font='11px system-ui';sim.forEach(function(n){if(n===sel||n===hover||n.symbols>=14){ctx.fillText(n.id.split('/').pop(),n.x+radius(n)+3,n.y+3);}});ctx.restore();}" +
85
+ "function loop(){var w=innerWidth,hh=innerHeight-48;if(w&&hh&&(w!==W||hh!==H))resize();tick();tick();layoutOrphans();if(autofit)fitView();draw();var bx=bb4(nodes);document.getElementById(\"dbg\").textContent=\"W=\"+W+\" H=\"+H+\" iw=\"+innerWidth+\"x\"+innerHeight+\" dpr=\"+(devicePixelRatio||1)+\" k=\"+view.k.toFixed(2)+\" vx=\"+Math.round(view.x)+\" vy=\"+Math.round(view.y)+\" fit=\"+autofit+\" sim=\"+sim.length+\" orph=\"+orphans.length+\" worldBox=\"+Math.round(bx[0])+\",\"+Math.round(bx[1])+\"..\"+Math.round(bx[2])+\",\"+Math.round(bx[3]);requestAnimationFrame(loop);}" +
51
86
  "function world(e){return{x:(e.clientX-view.x)/view.k,y:(e.clientY-48-view.y)/view.k};}" +
52
- "function pick(p){for(var i=nodes.length-1;i>=0;i--){var n=nodes[i];if((p.x-n.x)*(p.x-n.x)+(p.y-n.y)*(p.y-n.y)<=radius(n)*radius(n)+12)return n;}return null;}" +
53
- "c.addEventListener('mousedown',function(e){autofit=false;var n=pick(world(e));if(n){drag=n;sel=n;}else{pan={x:e.clientX-view.x,y:e.clientY-view.y};sel=null;}});c.addEventListener('dblclick',function(){fitView();});" +
54
- "addEventListener('mousemove',function(e){var p=world(e);if(drag){drag.x=p.x;drag.y=p.y;drag.vx=0;drag.vy=0;}else if(pan){view.x=e.clientX-pan.x;view.y=e.clientY-pan.y;}else{hover=pick(p);if(hover){tip.style.display='block';tip.style.left=(e.clientX+12)+'px';tip.style.top=(e.clientY+12)+'px';tip.textContent=hover.id+' · '+hover.symbols+' symbols · '+hover.lang;}else tip.style.display='none';}});" +
87
+ "function pick(p){var all=sim.concat(orphans);for(var i=all.length-1;i>=0;i--){var n=all[i];var r=(deg[n.id]?radius(n):3.2)+5;if((p.x-n.x)*(p.x-n.x)+(p.y-n.y)*(p.y-n.y)<=r*r)return n;}return null;}" +
88
+ "c.addEventListener('mousedown',function(e){autofit=false;var n=pick(world(e));if(n){drag=n;showPanel(n);}else{pan={x:e.clientX-view.x,y:e.clientY-view.y};}});" +
89
+ "c.addEventListener('dblclick',function(){panelOpen=false;sel=null;panel.style.display='none';autofit=true;});" +
90
+ "addEventListener('mousemove',function(e){var p=world(e);if(drag){drag.x=p.x;drag.y=p.y;drag.vx=0;drag.vy=0;}else if(pan){view.x=e.clientX-pan.x;view.y=e.clientY-pan.y;}else{hover=pick(p);if(hover){tip.style.display='block';tip.style.left=(e.clientX+12)+'px';tip.style.top=(e.clientY+12)+'px';tip.textContent=hover.id+' · '+(hover.symbols||0)+' symbols · '+hover.lang;}else tip.style.display='none';}});" +
55
91
  "addEventListener('mouseup',function(){drag=null;pan=null;});" +
56
92
  "c.addEventListener('wheel',function(e){e.preventDefault();autofit=false;var s=e.deltaY<0?1.1:0.9;var mx=e.clientX,my=e.clientY-48;view.x=mx-(mx-view.x)*s;view.y=my-(my-view.y)*s;view.k*=s;},{passive:false});" +
57
- "document.getElementById('q').addEventListener('input',function(e){q=e.target.value.toLowerCase();});loop();";
93
+ "document.getElementById('q').addEventListener('input',function(e){q=e.target.value.toLowerCase();});" +
94
+ "addEventListener('keydown',function(e){if(e.key==='d'&&e.target.tagName!=='INPUT'){var x=document.getElementById('dbg');x.style.display=x.style.display==='none'?'block':'none';}});loop();";
58
95
  /** Build a self-contained, dependency-free HTML graph explorer. */
59
96
  export function buildExplorerHtml(graph, root) {
60
97
  const data = deriveFileGraph(graph);
@@ -64,6 +101,6 @@ export function buildExplorerHtml(graph, root) {
64
101
  "<title>AST-MCP — " + title + " graph</title><style>" + STYLE + "</style></head><body>" +
65
102
  "<div id=\"bar\"><h1>AST-MCP graph</h1><span class=\"muted\">" + data.nodes.length + " files · " + data.links.length + " edges · drag / scroll / click</span>" +
66
103
  "<input id=\"q\" placeholder=\"filter files…\" /></div>" +
67
- "<canvas id=\"cv\"></canvas><div id=\"tip\"></div>" +
104
+ "<canvas id=\"cv\"></canvas><div id=\"tip\"></div><div id=\"panel\"></div><div id=\"dbg\" style=\"position:fixed;left:8px;bottom:8px;font:11px monospace;color:#e07;z-index:6;pointer-events:none;white-space:pre;display:none\"></div>" +
68
105
  "<script>var DATA=" + dataJson + ";</script><script>" + CLIENT + "</script></body></html>");
69
106
  }
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ import { computeFileComplexity } from "./complexity.js";
19
19
  import { findUnusedParams } from "./unused-params.js";
20
20
  import { traceTypeInFile } from "./typeflow.js";
21
21
  import { discoverWorkspace, findPackageCycles } from "./workspace.js";
22
+ import { readSourceMap } from "./sourcemap.js";
22
23
  /** Files may only be read inside this root (override with AST_MAP_ROOT). */
23
24
  const ROOT = path.resolve(process.env.AST_MAP_ROOT ?? process.cwd());
24
25
  function resolveInRoot(input) {
@@ -745,6 +746,27 @@ server.registerTool("analyze_workspace", {
745
746
  return errorText(describeError(err));
746
747
  }
747
748
  });
749
+ /* ─────────────────── tool: read_source_map ─────────────────────────────── */
750
+ server.registerTool("read_source_map", {
751
+ title: "Read a compiled file's source map",
752
+ description: "Given a compiled JS/CSS file with an inline (`data:`) or external `sourceMappingURL`, " +
753
+ "return the original source paths it maps back to. Useful for tracing built output in " +
754
+ "dist/ back to the real source files.",
755
+ inputSchema: {
756
+ path: z.string().describe("Compiled file path, relative to project root or absolute within it."),
757
+ },
758
+ }, async ({ path: input }) => {
759
+ try {
760
+ const { abs, rel } = resolveInRoot(input);
761
+ const info = readSourceMap(abs, rel.split(path.sep).join("/"));
762
+ if (!info)
763
+ return errorText(`No source map found for "${input}".`);
764
+ return jsonText(info);
765
+ }
766
+ catch (err) {
767
+ return errorText(describeError(err));
768
+ }
769
+ });
748
770
  /* ─────────────────── tool: get_change_impact ───────────────────────────── */
749
771
  server.registerTool("get_change_impact", {
750
772
  title: "Get change impact (blast radius)",
@@ -0,0 +1,60 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ function decodeInline(url) {
4
+ const b64 = url.match(/base64,(.+)$/);
5
+ try {
6
+ if (b64)
7
+ return JSON.parse(Buffer.from(b64[1], "base64").toString("utf8"));
8
+ const comma = url.indexOf(",");
9
+ if (comma >= 0)
10
+ return JSON.parse(decodeURIComponent(url.slice(comma + 1)));
11
+ }
12
+ catch { /* malformed */ }
13
+ return null;
14
+ }
15
+ /**
16
+ * Read the source map for a compiled JS/CSS file: handles an inline
17
+ * `//# sourceMappingURL=data:...` comment or an external `.map` file, and
18
+ * returns the original source paths it maps back to.
19
+ */
20
+ export function readSourceMap(absPath, relPath) {
21
+ let src;
22
+ try {
23
+ src = fs.readFileSync(absPath, "utf8");
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ const matches = [...src.matchAll(/[#@]\s*sourceMappingURL=([^\s'"]+)/g)];
29
+ if (matches.length === 0)
30
+ return null;
31
+ const url = matches[matches.length - 1][1];
32
+ let map = null;
33
+ let mapKind;
34
+ let mapFile;
35
+ if (url.startsWith("data:")) {
36
+ mapKind = "inline";
37
+ map = decodeInline(url);
38
+ }
39
+ else {
40
+ mapKind = "external";
41
+ mapFile = url;
42
+ try {
43
+ map = JSON.parse(fs.readFileSync(path.resolve(path.dirname(absPath), url), "utf8"));
44
+ }
45
+ catch {
46
+ map = null;
47
+ }
48
+ }
49
+ if (!map || !Array.isArray(map.sources))
50
+ return null;
51
+ const root = typeof map.sourceRoot === "string" ? map.sourceRoot.replace(/\/$/, "") : "";
52
+ const sources = map.sources.map((s) => root && !s.startsWith("/") ? root + "/" + s : s);
53
+ return {
54
+ file: relPath,
55
+ mapKind,
56
+ ...(mapFile ? { mapFile } : {}),
57
+ sources,
58
+ hasContent: Array.isArray(map.sourcesContent) && map.sourcesContent.length > 0,
59
+ };
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-ast-mapper",
3
- "version": "1.7.2",
3
+ "version": "1.10.0",
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",
@@ -12,6 +12,7 @@
12
12
  "dist",
13
13
  "scripts",
14
14
  "README.md",
15
+ "CHANGELOG.md",
15
16
  "BLUEPRINT.md"
16
17
  ],
17
18
  "scripts": {