universal-ast-mapper 1.10.0 → 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 CHANGED
@@ -6,6 +6,11 @@ since 1.0.0, guarantees a stable MCP tool / CLI surface across the 1.x line.
6
6
 
7
7
  ---
8
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
+
9
14
  ## [1.10.0] — 2026-06-01 · Source maps
10
15
  - **`read_source_map`** MCP tool + **`ast-map sourcemap <file>`** CLI: trace a
11
16
  compiled JS/CSS file (inline `data:` or external `.map`) back to its original
@@ -112,6 +117,7 @@ since 1.0.0, guarantees a stable MCP tool / CLI surface across the 1.x line.
112
117
  - **0.2.0** — import extraction; `resolve_imports`; `build_symbol_graph`.
113
118
  - **0.1.0** — `get_skeleton_json`, `generate_skeleton`, `get_symbol_context`, `validate_architecture`.
114
119
 
120
+ [1.11.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.11.0
115
121
  [1.10.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.10.0
116
122
  [1.9.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.9.0
117
123
  [1.8.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.8.0
package/README.md CHANGED
@@ -101,6 +101,7 @@ ast-map workspace [dir] [alias: ws]
101
101
  ast-map explore [dir] [-o out.html]
102
102
  ast-map watch [dir] [-o out.html]
103
103
  ast-map sourcemap <file>
104
+ ast-map report [dir] [-o report.html]
104
105
  ast-map search <pattern> [dir] [-m contains|exact|regex] [-k kind] [-e]
105
106
  ast-map deps <file> [--scan <dir>]
106
107
  ast-map top <dir> [-n 10]
@@ -620,6 +621,7 @@ Not part of the public API: the internal `src/` module layout and the generated
620
621
 
621
622
  | Version | What changed |
622
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. |
623
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. |
624
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`). |
625
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. |
package/dist/cli.js CHANGED
@@ -16,6 +16,7 @@ import { traceTypeInFile } from "./typeflow.js";
16
16
  import { discoverWorkspace, findPackageCycles } from "./workspace.js";
17
17
  import { buildExplorerHtml } from "./explorer.js";
18
18
  import { readSourceMap } from "./sourcemap.js";
19
+ import { buildReport, buildReportHtml } from "./report.js";
19
20
  import { buildCallGraph } from "./callgraph.js";
20
21
  import { searchSymbols } from "./search.js";
21
22
  const ROOT = path.resolve(process.env.AST_MAP_ROOT ?? process.cwd());
@@ -446,6 +447,28 @@ program
446
447
  console.log(`\n ${info.sources.length} original source(s)` + (info.hasContent ? dim(" · embeds sourcesContent") : ""));
447
448
  console.log();
448
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
+ });
449
472
  // ─── Command: explore ─────────────────────────────────────────────────────────
450
473
  program
451
474
  .command("explore [dir]")
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ import { findUnusedParams } from "./unused-params.js";
20
20
  import { traceTypeInFile } from "./typeflow.js";
21
21
  import { discoverWorkspace, findPackageCycles } from "./workspace.js";
22
22
  import { readSourceMap } from "./sourcemap.js";
23
+ import { buildReport } from "./report.js";
23
24
  /** Files may only be read inside this root (override with AST_MAP_ROOT). */
24
25
  const ROOT = path.resolve(process.env.AST_MAP_ROOT ?? process.cwd());
25
26
  function resolveInRoot(input) {
@@ -767,6 +768,28 @@ server.registerTool("read_source_map", {
767
768
  return errorText(describeError(err));
768
769
  }
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
+ });
770
793
  /* ─────────────────── tool: get_change_impact ───────────────────────────── */
771
794
  server.registerTool("get_change_impact", {
772
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-ast-mapper",
3
- "version": "1.10.0",
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",