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 +133 -0
- package/README.md +10 -0
- package/dist/cli.js +98 -0
- package/dist/explorer.js +59 -22
- package/dist/index.js +45 -0
- package/dist/report.js +162 -0
- package/dist/sourcemap.js +60 -0
- package/package.json +2 -1
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:
|
|
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
|
|
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:
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
"
|
|
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
|
|
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.
|
|
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-
|
|
45
|
-
"for(var i=0;i<
|
|
46
|
-
"function
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"function
|
|
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,'&').replace(/</g,'<');}" +
|
|
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\">×</button>'+'<h2>'+esc(n.id.split('/').pop())+'</h2><div class=\"path\">'+esc(n.id)+'</div>'+'<div class=\"meta\">'+esc(n.lang)+' · '+(n.symbols||0)+' symbols'+(deg[n.id]?'':' · 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=
|
|
53
|
-
"c.addEventListener('mousedown',function(e){autofit=false;var n=pick(world(e));if(n){drag=n;
|
|
54
|
-
"addEventListener('
|
|
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();});
|
|
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, "&").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
|
+
}
|
|
@@ -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.
|
|
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": {
|