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 +6 -0
- package/README.md +2 -0
- package/dist/cli.js +23 -0
- package/dist/index.js +23 -0
- package/dist/report.js +162 -0
- package/package.json +1 -1
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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