universal-ast-mapper 1.7.2 → 1.11.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,133 @@
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.11.0] — 2026-06-01 · Code-health dashboard
10
+ - **`ast-map report`** writes a premium self-contained HTML dashboard: health
11
+ grade (A–F), stats, language breakdown, complexity hotspots, god nodes, dead
12
+ code, and cycles. **`get_codebase_report`** MCP tool returns the same as JSON.
13
+
14
+ ## [1.10.0] — 2026-06-01 · Source maps
15
+ - **`read_source_map`** MCP tool + **`ast-map sourcemap <file>`** CLI: trace a
16
+ compiled JS/CSS file (inline `data:` or external `.map`) back to its original
17
+ sources; honors `sourceRoot` and reports embedded `sourcesContent`.
18
+ - Ruby re-investigated and confirmed blocked (external-scanner grammar needs
19
+ web-tree-sitter ≥0.22; engine upgrade would risk the 12 working languages).
20
+
21
+ ## [1.9.0] — 2026-06-01 · Watch mode
22
+ - **`ast-map watch [dir]`** — debounced, coalesced rebuild of the dependency
23
+ analysis (files · dead exports · cycles) on every source change; `-o file.html`
24
+ regenerates the live explorer too.
25
+ - Explorer debug readout hidden by default (toggle with `d`).
26
+
27
+ ## [1.8.0] — 2026-06-01 · Explorer detail sidebar
28
+ - Click a file in `ast-map explore` for a side panel: language, symbol count,
29
+ symbols, **Imports** and **Imported by** (each clickable to navigate the graph).
30
+ - **1.8.1–1.8.3 (fixes):** explorer now reliably centers/fills the viewport —
31
+ separated orphan files into a tidy grid, clamped the force layout to stop nodes
32
+ being flung to huge coordinates, and sized the canvas from `innerWidth/innerHeight`.
33
+
34
+ ## [1.7.0] — 2026-06-01 · Web UI graph explorer
35
+ - **`ast-map explore [dir]`** writes a self-contained, dependency-free interactive
36
+ HTML: a force-directed file dependency graph (drag / zoom / pan / click-to-
37
+ highlight / name filter). Opens in any browser, no build step.
38
+ - **1.7.1–1.7.3 (fixes):** auto-fit and layout tuning.
39
+
40
+ ## [1.6.0] — 2026-06-01 · MCP resource endpoints
41
+ - Browseable resources: **`ast://languages`**, **`ast://skeleton/{path}`**
42
+ (templated, one per file via `resources/list`), **`ast://graph`**. Agents can
43
+ list/read codebase structure as resources, not just call tools.
44
+
45
+ ## [1.5.0] — 2026-06-01 · `.d.ts` / ambient declarations
46
+ - Extract `declare function/const/class`, `declare module "x"`, and
47
+ `declare namespace` (plus plain `namespace`); a `.d.ts` used to yield 0 symbols.
48
+ - New `namespace` symbol kind.
49
+
50
+ ## [1.4.0] — 2026-06-01 · Dynamic import tracking
51
+ - Capture dynamic `import("...")` and CommonJS `require("...")` with an
52
+ `isDynamic` flag; relative ones resolve and draw graph edges like static imports.
53
+
54
+ ## [1.3.0] — 2026-06-01 · TS/JS decorators
55
+ - Class and method symbols carry a `decorators` field (`@Component`, `@Get(...)`),
56
+ in skeletons and `get_call_graph`. Extends the Python decorator support to TS/JS.
57
+
58
+ ## [1.2.0] — 2026-06-01 · File-level cross-package resolution
59
+ - In a monorepo, bare imports of a workspace package (`@org/utils`, `@org/utils/sub`)
60
+ resolve to the real source file (prefers `src/` over `dist/`), so `resolve_imports`
61
+ marks them in-project and `build_symbol_graph` draws cross-package edges.
62
+
63
+ ## [1.1.0] — 2026-06-01 · Monorepo support
64
+ - **`analyze_workspace`** tool + **`ast-map workspace`** CLI: discover packages
65
+ (npm/yarn `workspaces`, `pnpm-workspace.yaml`, `lerna.json`), map internal
66
+ package dependencies, and detect circular package deps.
67
+
68
+ ## [1.0.0] — 2026-06-01 · Stable release 🎉
69
+ - Locked public API (MCP tool names + schemas, CLI surface) for the 1.x line.
70
+ - Bundled **GitHub Action** (`action.yml`) running `ast-map validate` as a CI gate,
71
+ plus a project CI workflow.
72
+ - 12 languages · 18 MCP tools / 17 CLI commands at release.
73
+
74
+ ## [0.9.0] — 2026-05-31 · Scoped type-flow tracing
75
+ - **`trace_type`** tool + **`ast-map trace-type`** CLI: follow a named type through
76
+ function params, return types, typed variables, and class fields. Completes the
77
+ deeper-analysis suite.
78
+
79
+ ## [0.8.7] — 2026-05-31 · Python decorators
80
+ - `decorators` field on Python symbols + `get_call_graph`; traces
81
+ `@router.get(...)` → handler and stacked decorators.
82
+
83
+ ## [0.8.6] — 2026-05-31 · Unused parameter detection
84
+ - **`find_unused_params`** tool + **`ast-map unused-params`** CLI: named functions
85
+ whose params are never referenced (low false-positive; counts object shorthand).
86
+
87
+ ## [0.8.5] — 2026-05-31 · Cyclomatic complexity
88
+ - **`get_complexity`** tool + **`ast-map complexity`** CLI: per-function score with
89
+ low/moderate/high/very-high ratings and directory hotspots.
90
+
91
+ ## [0.8.4] — 2026-05-31 · Duplicate symbol detection
92
+ - **`find_duplicate_symbols`** tool + **`ast-map duplicates`** CLI: exported names
93
+ declared in 2+ files.
94
+
95
+ ## [0.8.3] — 2026-05-31 · TSX/React component props
96
+ - Component symbols carry `propsType` + `props[]`; detects `React.FC<P>` and
97
+ JSX-returning PascalCase functions. MCP server version now read from package.json.
98
+
99
+ ## [0.8.2] — 2026-05-30 · Swift cross-file wiring
100
+ - `import <Module>` → that module's files (`Sources/<Module>/`). Completes
101
+ cross-file graph/resolver support for all four v0.8.0 languages.
102
+
103
+ ## [0.8.1] — 2026-05-30 · Kotlin + C/C++ cross-file wiring
104
+ - Kotlin FQCN/package index; C/C++ `#include` resolution with header↔impl pairing.
105
+ - Fixes: parse-cache rel-path leak; Kotlin call-graph extraction.
106
+
107
+ ---
108
+
109
+ ## Earlier (pre-session history)
110
+
111
+ - **0.8.0** — +4 languages: C · C++ · Kotlin · Swift (symbol extraction + imports).
112
+ - **0.7.0** — Go full module resolution; C# reverse `calledBy`; 4-suite test harness.
113
+ - **0.6.0** — +3 languages: Rust · Java · C#; cross-language resolver.
114
+ - **0.5.x** — `/ast-map` skill auto-install; iterative DFS; barrel re-exports; parse cache; call-graph aliases; `.ast-map.config.json`.
115
+ - **0.4.0** — `search_symbol`, `get_file_deps`, `get_top_symbols`, dead-code tiers.
116
+ - **0.3.0** — CLI; `find_dead_code`, `find_circular_deps`, `get_change_impact`, `get_call_graph`.
117
+ - **0.2.0** — import extraction; `resolve_imports`; `build_symbol_graph`.
118
+ - **0.1.0** — `get_skeleton_json`, `generate_skeleton`, `get_symbol_context`, `validate_architecture`.
119
+
120
+ [1.11.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.11.0
121
+ [1.10.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.10.0
122
+ [1.9.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.9.0
123
+ [1.8.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.8.0
124
+ [1.7.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.7.0
125
+ [1.6.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.6.0
126
+ [1.5.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.5.0
127
+ [1.4.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.4.0
128
+ [1.3.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.3.0
129
+ [1.2.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.2.0
130
+ [1.1.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.1.0
131
+ [1.0.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.0.0
132
+ [0.9.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v0.9.0
133
+ [0.8.1]: https://github.com/6ixthxense/AST-MCP/releases/tag/v0.8.1
package/README.md CHANGED
@@ -99,6 +99,9 @@ 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>
104
+ ast-map report [dir] [-o report.html]
102
105
  ast-map search <pattern> [dir] [-m contains|exact|regex] [-k kind] [-e]
103
106
  ast-map deps <file> [--scan <dir>]
104
107
  ast-map top <dir> [-n 10]
@@ -618,6 +621,13 @@ Not part of the public API: the internal `src/` module layout and the generated
618
621
 
619
622
  | Version | What changed |
620
623
  |---------|--------------|
624
+ | **1.11.0** | **Code-health dashboard** — new `ast-map report` CLI writes a premium self-contained HTML overview (grade A–F, stats, language breakdown, complexity hotspots, god nodes, dead code, cycles) + `get_codebase_report` MCP tool for the same as JSON. |
625
+ | **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. |
626
+ | **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`). |
627
+ | **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. |
628
+ | **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). |
629
+ | **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. |
630
+ | **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
631
  | **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
632
  | **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
633
  | **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,8 @@ 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";
19
+ import { buildReport, buildReportHtml } from "./report.js";
18
20
  import { buildCallGraph } from "./callgraph.js";
19
21
  import { searchSymbols } from "./search.js";
20
22
  const ROOT = path.resolve(process.env.AST_MAP_ROOT ?? process.cwd());
@@ -371,6 +373,102 @@ program
371
373
  }
372
374
  console.log();
373
375
  });
376
+ // ─── Command: watch ───────────────────────────────────────────────────────────
377
+ program
378
+ .command("watch [dir]")
379
+ .description("Rebuild analysis (and optionally the explorer) when files change")
380
+ .option("-o, --out <file>", "Also regenerate the explorer HTML on each change")
381
+ .action(async (dir, opts) => {
382
+ const { abs, rel } = resolveArg(dir ?? ".");
383
+ if (!fs.statSync(abs).isDirectory())
384
+ die(`"${rel}" is not a directory`);
385
+ let building = false;
386
+ let queued = false;
387
+ async function rebuild(reason) {
388
+ if (building) {
389
+ queued = true;
390
+ return;
391
+ }
392
+ building = true;
393
+ try {
394
+ const skels = await gatherSkeletons(abs);
395
+ const graph = buildSymbolGraph(skels, ROOT);
396
+ const dead = findDeadExports(graph).filter((d) => d.confidence === "high").length;
397
+ const cycles = findCircularDeps(graph).length;
398
+ let line = `${dim(new Date().toLocaleTimeString())} ${bold(String(skels.length))} files · ${dead} dead · ${cycles} cycle(s)`;
399
+ if (opts.out) {
400
+ fs.writeFileSync(path.resolve(process.cwd(), opts.out), buildExplorerHtml(graph, abs), "utf8");
401
+ line += ` · ${green("explorer updated")}`;
402
+ }
403
+ line += ` ${dim(reason)}`;
404
+ console.log(line);
405
+ }
406
+ finally {
407
+ building = false;
408
+ if (queued) {
409
+ queued = false;
410
+ rebuild("(coalesced)");
411
+ }
412
+ }
413
+ }
414
+ header(`Watching ${rel}/ ${dim("(Ctrl+C to stop)")}`);
415
+ await rebuild("initial");
416
+ let timer = null;
417
+ const exts = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", ".cs", ".c", ".cpp", ".h", ".hpp", ".kt", ".swift"]);
418
+ fs.watch(abs, { recursive: true }, (_evt, file) => {
419
+ if (!file)
420
+ return;
421
+ const f = String(file).split(path.sep).join("/");
422
+ if (/(^|\/)(node_modules|\.git|dist|\.ast-map)(\/|$)/.test(f))
423
+ return;
424
+ if (!exts.has(path.extname(f).toLowerCase()))
425
+ return;
426
+ if (timer)
427
+ clearTimeout(timer);
428
+ timer = setTimeout(() => rebuild(`(${f.split("/").pop()} changed)`), 300);
429
+ });
430
+ await new Promise(() => { }); // keep the process alive
431
+ });
432
+ // ─── Command: sourcemap ───────────────────────────────────────────────────────
433
+ program
434
+ .command("sourcemap <file>")
435
+ .description("Show the original sources a compiled file maps back to")
436
+ .option("--json", "Output as JSON")
437
+ .action(async (inputPath, opts) => {
438
+ const { abs, rel } = resolveArg(inputPath);
439
+ const info = readSourceMap(abs, rel);
440
+ if (!info)
441
+ die(`No source map found for "${rel}"`);
442
+ if (opts.json)
443
+ return jsonOut(info);
444
+ header(`Source Map — ${rel} ${dim("(" + info.mapKind + ")")}`);
445
+ for (const sourceFile of info.sources)
446
+ console.log(indent(green("←") + " " + sourceFile));
447
+ console.log(`\n ${info.sources.length} original source(s)` + (info.hasContent ? dim(" · embeds sourcesContent") : ""));
448
+ console.log();
449
+ });
450
+ // ─── Command: report ──────────────────────────────────────────────────────────
451
+ program
452
+ .command("report [dir]")
453
+ .description("Generate a code-health dashboard (HTML)")
454
+ .option("-o, --out <file>", "Output HTML path", "ast-report.html")
455
+ .option("--json", "Print the report data as JSON")
456
+ .action(async (dir, opts) => {
457
+ const { abs, rel } = resolveArg(dir ?? ".");
458
+ if (!fs.statSync(abs).isDirectory())
459
+ die(`"${rel}" is not a directory`);
460
+ const data = await buildReport(abs, ROOT);
461
+ if (opts.json)
462
+ return jsonOut(data);
463
+ const out = path.resolve(process.cwd(), opts.out);
464
+ fs.mkdirSync(path.dirname(out), { recursive: true });
465
+ fs.writeFileSync(out, buildReportHtml(data), "utf8");
466
+ header(`Code Health \u2014 ${rel}/ ${dim(`(${data.fileCount} files)`)}`);
467
+ const gcolor = data.grade === "A" || data.grade === "B" ? green : data.grade === "C" || data.grade === "D" ? yellow : (x) => x;
468
+ console.log(indent(`Grade ${bold(gcolor(data.grade))} ${dim("(" + data.score + "/100)")} · ${data.dead.count} dead · ${data.cycles.count} cycles · max cx ${data.complexity.max}`));
469
+ console.log(indent(green("✓ wrote " + path.relative(process.cwd(), out))));
470
+ console.log();
471
+ });
374
472
  // ─── Command: explore ─────────────────────────────────────────────────────────
375
473
  program
376
474
  .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,8 @@ 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";
23
+ import { buildReport } from "./report.js";
22
24
  /** Files may only be read inside this root (override with AST_MAP_ROOT). */
23
25
  const ROOT = path.resolve(process.env.AST_MAP_ROOT ?? process.cwd());
24
26
  function resolveInRoot(input) {
@@ -745,6 +747,49 @@ server.registerTool("analyze_workspace", {
745
747
  return errorText(describeError(err));
746
748
  }
747
749
  });
750
+ /* ─────────────────── tool: read_source_map ─────────────────────────────── */
751
+ server.registerTool("read_source_map", {
752
+ title: "Read a compiled file's source map",
753
+ description: "Given a compiled JS/CSS file with an inline (`data:`) or external `sourceMappingURL`, " +
754
+ "return the original source paths it maps back to. Useful for tracing built output in " +
755
+ "dist/ back to the real source files.",
756
+ inputSchema: {
757
+ path: z.string().describe("Compiled file path, relative to project root or absolute within it."),
758
+ },
759
+ }, async ({ path: input }) => {
760
+ try {
761
+ const { abs, rel } = resolveInRoot(input);
762
+ const info = readSourceMap(abs, rel.split(path.sep).join("/"));
763
+ if (!info)
764
+ return errorText(`No source map found for "${input}".`);
765
+ return jsonText(info);
766
+ }
767
+ catch (err) {
768
+ return errorText(describeError(err));
769
+ }
770
+ });
771
+ /* ─────────────────── tool: get_codebase_report ─────────────────────────── */
772
+ server.registerTool("get_codebase_report", {
773
+ title: "Codebase health report",
774
+ description: "Scan a directory and return a one-shot health summary: file/symbol counts, language " +
775
+ "breakdown, a health grade (A\u2013F) and score, complexity hotspots, god nodes (most-imported " +
776
+ "symbols), dead exports, and circular dependencies. The `ast-map report` CLI renders this as HTML.",
777
+ inputSchema: {
778
+ path: z.string().optional().describe("Directory to scan. Defaults to the project root."),
779
+ },
780
+ }, async ({ path: input }) => {
781
+ try {
782
+ const { abs, rel } = resolveInRoot(input ?? ".");
783
+ if (!fs.statSync(abs).isDirectory()) {
784
+ return errorText(`"${input}" is not a directory. get_codebase_report requires a directory.`);
785
+ }
786
+ const data = await buildReport(abs, ROOT);
787
+ return jsonText({ directory: rel.split(path.sep).join("/") || ".", ...data });
788
+ }
789
+ catch (err) {
790
+ return errorText(describeError(err));
791
+ }
792
+ });
748
793
  /* ─────────────────── tool: get_change_impact ───────────────────────────── */
749
794
  server.registerTool("get_change_impact", {
750
795
  title: "Get change impact (blast radius)",
package/dist/report.js ADDED
@@ -0,0 +1,162 @@
1
+ import path from "node:path";
2
+ import { collectSourceFiles, buildSkeleton } from "./skeleton.js";
3
+ import { resolveOptions } from "./config.js";
4
+ import { buildSymbolGraph } from "./graph.js";
5
+ import { findDeadExports, findCircularDeps, getTopSymbols } from "./graph-analysis.js";
6
+ import { computeFileComplexity } from "./complexity.js";
7
+ function gradeFor(score) {
8
+ if (score >= 90)
9
+ return "A";
10
+ if (score >= 80)
11
+ return "B";
12
+ if (score >= 70)
13
+ return "C";
14
+ if (score >= 60)
15
+ return "D";
16
+ return "F";
17
+ }
18
+ export async function buildReport(absDir, root) {
19
+ const opts = resolveOptions({ detail: "outline", emitHtml: false });
20
+ const files = collectSourceFiles(absDir, opts);
21
+ const skeletons = [];
22
+ const langCount = new Map();
23
+ let symbolCount = 0;
24
+ const hotspots = [];
25
+ let cxSum = 0, cxN = 0, cxMax = 0;
26
+ for (const file of files) {
27
+ const rel = path.relative(root, file).split(path.sep).join("/");
28
+ try {
29
+ const skel = await buildSkeleton(file, rel, opts);
30
+ skeletons.push(skel);
31
+ symbolCount += skel.symbolCount;
32
+ langCount.set(skel.language, (langCount.get(skel.language) ?? 0) + 1);
33
+ const fc = await computeFileComplexity(file, rel);
34
+ if (fc) {
35
+ for (const f of fc.functions) {
36
+ hotspots.push({ ...f, file: rel });
37
+ cxSum += f.complexity;
38
+ cxN++;
39
+ cxMax = Math.max(cxMax, f.complexity);
40
+ }
41
+ }
42
+ }
43
+ catch { /* skip unparsable */ }
44
+ }
45
+ const graph = buildSymbolGraph(skeletons, root);
46
+ const dead = findDeadExports(graph).filter((d) => d.confidence === "high");
47
+ const cycles = findCircularDeps(graph);
48
+ const god = getTopSymbols(graph, 8);
49
+ hotspots.sort((a, b) => b.complexity - a.complexity);
50
+ const veryHigh = hotspots.filter((f) => f.complexity > 20).length;
51
+ const high = hotspots.filter((f) => f.complexity > 10 && f.complexity <= 20).length;
52
+ // Health score: start at 100, subtract weighted penalties.
53
+ let score = 100;
54
+ score -= Math.min(20, dead.length * 1.5);
55
+ score -= Math.min(22, cycles.length * 6);
56
+ score -= Math.min(28, veryHigh * 4 + high * 1);
57
+ score -= Math.min(12, god.filter((g) => g.importCount >= 8).length * 4);
58
+ score = Math.max(0, Math.round(score));
59
+ const languages = [...langCount.entries()]
60
+ .map(([lang, f]) => ({ lang, files: f }))
61
+ .sort((a, b) => b.files - a.files);
62
+ return {
63
+ project: absDir.split(/[\\/]/).filter(Boolean).pop() || "project",
64
+ generatedAt: new Date().toISOString(),
65
+ fileCount: skeletons.length,
66
+ symbolCount,
67
+ edgeCount: graph.edges.filter((e) => e.edgeType === "imports").length,
68
+ languages,
69
+ score,
70
+ grade: gradeFor(score),
71
+ dead: { count: dead.length, items: dead.slice(0, 25).map((d) => ({ file: d.file, symbol: d.symbol, kind: d.kind })) },
72
+ cycles: { count: cycles.length, items: cycles.slice(0, 12).map((c) => c.cycle) },
73
+ godNodes: god.map((g) => ({ symbol: g.symbol, file: g.file, importCount: g.importCount })),
74
+ complexity: { average: cxN ? Math.round((cxSum / cxN) * 10) / 10 : 0, max: cxMax, hotspots: hotspots.slice(0, 12) },
75
+ };
76
+ }
77
+ /* ─── Premium HTML dashboard ───────────────────────────────────────────────── */
78
+ const GRADE_COLOR = {
79
+ A: "#1d9e75", B: "#1d9e75", C: "#ba7517", D: "#d85a30", F: "#e24b4a",
80
+ };
81
+ function esc(s) {
82
+ return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
83
+ }
84
+ function ratingColor(r) {
85
+ return r === "very-high" ? "#e24b4a" : r === "high" ? "#d85a30" : r === "moderate" ? "#ba7517" : "#1d9e75";
86
+ }
87
+ function statCard(label, value, accent) {
88
+ return `<div class="stat"><div class="sv"${accent ? ` style="color:${accent}"` : ""}>${value}</div><div class="sl">${label}</div></div>`;
89
+ }
90
+ function bar(label, value, max, color, right) {
91
+ const pct = max > 0 ? Math.round((value / max) * 100) : 0;
92
+ return `<div class="row"><div class="rl">${esc(label)}</div><div class="track"><div class="fill" style="width:${pct}%;background:${color}"></div></div><div class="rr">${right}</div></div>`;
93
+ }
94
+ export function buildReportHtml(d) {
95
+ const gc = GRADE_COLOR[d.grade] ?? "#888";
96
+ const maxLang = d.languages[0]?.files ?? 1;
97
+ const langs = d.languages.map((l) => bar(l.lang, l.files, maxLang, "#534ab7", `${l.files}`)).join("");
98
+ const maxCx = d.complexity.hotspots[0]?.complexity ?? 1;
99
+ const hotspots = d.complexity.hotspots.length
100
+ ? d.complexity.hotspots.map((h) => bar(`${h.name} · ${h.file}`, h.complexity, maxCx, ratingColor(h.rating), `<b>${h.complexity}</b>`)).join("")
101
+ : `<div class="empty">No functions found.</div>`;
102
+ const god = d.godNodes.length
103
+ ? d.godNodes.map((g) => `<div class="li"><span class="mono">${esc(g.symbol)}</span><span class="dim">${esc(g.file)}</span><span class="pill">${g.importCount} importers</span></div>`).join("")
104
+ : `<div class="empty">None.</div>`;
105
+ const dead = d.dead.count
106
+ ? d.dead.items.map((x) => `<div class="li"><span class="kbadge">${esc(x.kind)}</span><span class="mono">${esc(x.symbol)}</span><span class="dim">${esc(x.file)}</span></div>`).join("")
107
+ + (d.dead.count > d.dead.items.length ? `<div class="more">+${d.dead.count - d.dead.items.length} more…</div>` : "")
108
+ : `<div class="ok">✓ No high-confidence dead exports</div>`;
109
+ const cycles = d.cycles.count
110
+ ? d.cycles.items.map((c) => `<div class="li"><span class="mono">${esc(c.join(" → "))}</span></div>`).join("")
111
+ : `<div class="ok">✓ No circular dependencies</div>`;
112
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
113
+ <title>${esc(d.project)} — code health</title><style>
114
+ :root{--bg:#fafaf8;--card:#fff;--bd:#e7e5df;--tx:#2b2b28;--dim:#8a8880;--soft:#f1efe9}
115
+ @media(prefers-color-scheme:dark){:root{--bg:#161613;--card:#1e1e1b;--bd:#33332e;--tx:#e6e4dd;--dim:#9a988f;--soft:#26261f}}
116
+ *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--tx);font-family:system-ui,-apple-system,sans-serif;line-height:1.5}
117
+ .wrap{max-width:980px;margin:0 auto;padding:32px 24px 60px}
118
+ .hero{display:flex;align-items:center;gap:24px;margin-bottom:28px}
119
+ .badge{width:104px;height:104px;border-radius:24px;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;flex:0 0 auto}
120
+ .badge .g{font-size:46px;font-weight:700;line-height:1}.badge .s{font-size:12px;opacity:.9}
121
+ .h1{font-size:26px;font-weight:650;margin:0}.sub{color:var(--dim);font-size:13px;margin-top:4px}
122
+ .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(110px,1fr));gap:12px;margin-bottom:30px}
123
+ .stat{background:var(--card);border:1px solid var(--bd);border-radius:14px;padding:14px 16px}
124
+ .sv{font-size:24px;font-weight:650}.sl{font-size:12px;color:var(--dim);margin-top:2px}
125
+ .card{background:var(--card);border:1px solid var(--bd);border-radius:16px;padding:18px 20px;margin-bottom:18px}
126
+ .card h2{font-size:14px;font-weight:600;margin:0 0 14px;letter-spacing:.02em;text-transform:uppercase;color:var(--dim)}
127
+ .row{display:flex;align-items:center;gap:12px;margin:7px 0;font-size:13px}
128
+ .rl{flex:0 0 46%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
129
+ .track{flex:1;height:8px;background:var(--soft);border-radius:5px;overflow:hidden}.fill{height:100%;border-radius:5px}
130
+ .rr{flex:0 0 auto;color:var(--dim);min-width:32px;text-align:right}
131
+ .li{display:flex;align-items:center;gap:10px;padding:5px 0;font-size:13px;border-top:1px solid var(--bd)}.li:first-child{border-top:none}
132
+ .mono{font-family:ui-monospace,monospace;font-weight:550}.dim{color:var(--dim);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
133
+ .pill{margin-left:auto;background:var(--soft);border-radius:20px;padding:2px 10px;font-size:11px;color:var(--dim);flex:0 0 auto}
134
+ .kbadge{font-size:11px;color:var(--dim);background:var(--soft);border-radius:5px;padding:1px 7px;flex:0 0 auto}
135
+ .ok{color:#1d9e75;font-size:13px}.empty{color:var(--dim);font-size:13px}.more{color:var(--dim);font-size:12px;padding-top:6px}
136
+ .two{display:grid;grid-template-columns:1fr 1fr;gap:18px}@media(max-width:720px){.two{grid-template-columns:1fr}.rl{flex-basis:42%}}
137
+ .foot{color:var(--dim);font-size:11px;text-align:center;margin-top:24px}
138
+ </style></head><body><div class="wrap">
139
+ <div class="hero">
140
+ <div class="badge" style="background:${gc}"><div class="g">${d.grade}</div><div class="s">${d.score}/100</div></div>
141
+ <div><h1 class="h1">${esc(d.project)} — code health</h1>
142
+ <div class="sub">${d.fileCount} files · ${d.symbolCount} symbols · ${d.languages.length} language(s) · ${esc(d.generatedAt.slice(0, 10))}</div></div>
143
+ </div>
144
+ <div class="grid">
145
+ ${statCard("Files", d.fileCount)}
146
+ ${statCard("Symbols", d.symbolCount)}
147
+ ${statCard("Import edges", d.edgeCount)}
148
+ ${statCard("Avg complexity", d.complexity.average)}
149
+ ${statCard("Max complexity", d.complexity.max, ratingColor(d.complexity.max > 20 ? "very-high" : d.complexity.max > 10 ? "high" : "low"))}
150
+ ${statCard("Dead exports", d.dead.count, d.dead.count ? "#d85a30" : "#1d9e75")}
151
+ ${statCard("Cycles", d.cycles.count, d.cycles.count ? "#e24b4a" : "#1d9e75")}
152
+ </div>
153
+ <div class="card"><h2>Language breakdown</h2>${langs}</div>
154
+ <div class="card"><h2>Complexity hotspots</h2>${hotspots}</div>
155
+ <div class="two">
156
+ <div class="card"><h2>God nodes (most imported)</h2>${god}</div>
157
+ <div class="card"><h2>Circular dependencies</h2>${cycles}</div>
158
+ </div>
159
+ <div class="card"><h2>Dead exports (high confidence)</h2>${dead}</div>
160
+ <div class="foot">Generated by AST-MCP · universal-ast-mapper</div>
161
+ </div></body></html>`;
162
+ }
@@ -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.11.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": {