universal-ast-mapper 1.27.0 → 2.0.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.
Files changed (55) hide show
  1. package/BLUEPRINT.md +230 -230
  2. package/CHANGELOG.md +466 -321
  3. package/README.md +878 -877
  4. package/package.json +48 -47
  5. package/scripts/install-skill.mjs +187 -187
  6. package/dist/analysis.js +0 -134
  7. package/dist/callgraph.js +0 -467
  8. package/dist/check.js +0 -112
  9. package/dist/cli.js +0 -1275
  10. package/dist/complexity.js +0 -98
  11. package/dist/config.js +0 -53
  12. package/dist/contextpack.js +0 -79
  13. package/dist/coupling.js +0 -35
  14. package/dist/crosslang.js +0 -425
  15. package/dist/diskcache.js +0 -97
  16. package/dist/explorer.js +0 -123
  17. package/dist/extractors/c.js +0 -204
  18. package/dist/extractors/common.js +0 -56
  19. package/dist/extractors/cpp.js +0 -272
  20. package/dist/extractors/csharp.js +0 -209
  21. package/dist/extractors/go.js +0 -212
  22. package/dist/extractors/java.js +0 -152
  23. package/dist/extractors/kotlin.js +0 -159
  24. package/dist/extractors/php.js +0 -208
  25. package/dist/extractors/python.js +0 -153
  26. package/dist/extractors/ruby.js +0 -146
  27. package/dist/extractors/rust.js +0 -249
  28. package/dist/extractors/swift.js +0 -192
  29. package/dist/extractors/typescript.js +0 -577
  30. package/dist/gitdiff.js +0 -178
  31. package/dist/graph-analysis.js +0 -279
  32. package/dist/graph.js +0 -165
  33. package/dist/html.js +0 -326
  34. package/dist/index.js +0 -1407
  35. package/dist/layers.js +0 -36
  36. package/dist/modulecoupling.js +0 -0
  37. package/dist/parser.js +0 -84
  38. package/dist/pool.js +0 -114
  39. package/dist/prompts.js +0 -67
  40. package/dist/registry.js +0 -87
  41. package/dist/report.js +0 -187
  42. package/dist/resolver.js +0 -222
  43. package/dist/roots.js +0 -47
  44. package/dist/search.js +0 -68
  45. package/dist/semantic.js +0 -365
  46. package/dist/sfc.js +0 -27
  47. package/dist/skeleton.js +0 -132
  48. package/dist/sourcemap.js +0 -60
  49. package/dist/testmap.js +0 -167
  50. package/dist/tsconfig.js +0 -212
  51. package/dist/typeflow.js +0 -124
  52. package/dist/types.js +0 -5
  53. package/dist/unused-params.js +0 -127
  54. package/dist/worker.js +0 -27
  55. package/dist/workspace.js +0 -330
package/dist/layers.js DELETED
@@ -1,36 +0,0 @@
1
- import { computeCoupling } from "./coupling.js";
2
- /**
3
- * Detect violations of Robert C. Martin's Stable Dependencies Principle (SDP):
4
- * a module should depend only on modules at least as stable as itself. A "stable"
5
- * file (low instability) that imports a "volatile" file (high instability) is a
6
- * violation — volatile code changes often and will keep dragging the stable code
7
- * with it. Severity is the instability gap the dependency crosses.
8
- */
9
- export function findLayerViolations(graph, minGap = 0) {
10
- const inst = new Map(computeCoupling(graph).map((m) => [m.file, m.instability]));
11
- const nodeMap = new Map(graph.nodes.map((n) => [n.id, n]));
12
- const seen = new Set();
13
- const violations = [];
14
- for (const e of graph.edges) {
15
- if (e.edgeType !== "imports")
16
- continue;
17
- const to = nodeMap.get(e.to);
18
- const toFile = to ? (to.nodeType === "file" ? to.id : to.file) : null;
19
- const fromFile = e.from;
20
- if (!toFile || fromFile === toFile)
21
- continue;
22
- const fi = inst.get(fromFile);
23
- const ti = inst.get(toFile);
24
- if (fi === undefined || ti === undefined)
25
- continue;
26
- const severity = Math.round((ti - fi) * 100) / 100;
27
- if (severity <= minGap)
28
- continue; // only "uphill" dependencies (stable -> volatile)
29
- const key = fromFile + " " + toFile;
30
- if (seen.has(key))
31
- continue;
32
- seen.add(key);
33
- violations.push({ from: fromFile, to: toFile, fromInstability: fi, toInstability: ti, severity });
34
- }
35
- return violations.sort((a, b) => b.severity - a.severity || a.from.localeCompare(b.from));
36
- }
Binary file
package/dist/parser.js DELETED
@@ -1,84 +0,0 @@
1
- import Parser from "web-tree-sitter";
2
- import { createRequire } from "node:module";
3
- import path from "node:path";
4
- const require = createRequire(import.meta.url);
5
- // `web-tree-sitter` 0.20.x ships a CommonJS class with static members that are
6
- // only populated after `init()`. We treat the static surface loosely.
7
- const P = Parser;
8
- let initPromise = null;
9
- const languageCache = new Map();
10
- const parserCache = new Map();
11
- function pkgDir(spec) {
12
- return path.dirname(require.resolve(spec));
13
- }
14
- function grammarWasmPath(grammar) {
15
- return path.join(pkgDir("tree-sitter-wasms/package.json"), "out", `tree-sitter-${grammar}.wasm`);
16
- }
17
- export async function initParser() {
18
- if (!initPromise) {
19
- const coreDir = pkgDir("web-tree-sitter/package.json");
20
- initPromise = P.init({
21
- locateFile(name) {
22
- // The runtime requests "tree-sitter.wasm"; serve it from the package dir.
23
- return path.join(coreDir, name);
24
- },
25
- });
26
- }
27
- return initPromise;
28
- }
29
- async function loadLanguage(grammar) {
30
- await initParser();
31
- const cached = languageCache.get(grammar);
32
- if (cached)
33
- return cached;
34
- const lang = await P.Language.load(grammarWasmPath(grammar));
35
- languageCache.set(grammar, lang);
36
- return lang;
37
- }
38
- /** Parse source code with the given grammar and return the root node. */
39
- export async function parseSource(grammar, source) {
40
- const lang = await loadLanguage(grammar);
41
- let parser = parserCache.get(grammar);
42
- if (!parser) {
43
- parser = new P();
44
- parser.setLanguage(lang);
45
- parserCache.set(grammar, parser);
46
- }
47
- return parser.parse(source).rootNode;
48
- }
49
- /* ----------------------------- node helpers ----------------------------- */
50
- export function namedChildren(node) {
51
- const out = [];
52
- for (let i = 0; i < node.namedChildCount; i++) {
53
- const c = node.namedChild(i);
54
- if (c)
55
- out.push(c);
56
- }
57
- return out;
58
- }
59
- export function nameOf(node) {
60
- const n = node.childForFieldName("name");
61
- return n ? n.text : null;
62
- }
63
- /**
64
- * Build a one-line "header" signature: text from the node start up to the
65
- * start of its body (or the whole node if there is no body), whitespace
66
- * collapsed. Works uniformly across languages.
67
- */
68
- export function headerSignature(node, body) {
69
- const src = node.text;
70
- const slice = body ? src.slice(0, body.startIndex - node.startIndex) : src;
71
- return slice.replace(/\s+/g, " ").trim();
72
- }
73
- /** Collect consecutive leading line/block comments immediately above a node. */
74
- export function leadingComment(node) {
75
- const lines = [];
76
- let prev = node.previousNamedSibling;
77
- while (prev && prev.type === "comment") {
78
- lines.unshift(prev.text);
79
- prev = prev.previousNamedSibling;
80
- }
81
- if (lines.length === 0)
82
- return null;
83
- return lines.join("\n").slice(0, 500);
84
- }
package/dist/pool.js DELETED
@@ -1,114 +0,0 @@
1
- import os from "node:os";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { Worker } from "node:worker_threads";
6
- import { buildSkeleton } from "./skeleton.js";
7
- import { computeFileComplexity } from "./complexity.js";
8
- import { diskCacheDir } from "./diskcache.js";
9
- /** Batches smaller than this are parsed sequentially (worker startup costs more). */
10
- const MIN_BATCH = 64;
11
- const MAX_WORKERS = 8;
12
- function envWorkers() {
13
- const env = process.env.AST_MAP_WORKERS;
14
- if (env === undefined || env === "")
15
- return null;
16
- const v = Number.parseInt(env, 10);
17
- return Number.isNaN(v) ? null : Math.max(0, Math.min(v, MAX_WORKERS));
18
- }
19
- function plannedWorkers(n) {
20
- const forced = envWorkers();
21
- if (forced !== null)
22
- return forced;
23
- const cpus = os.cpus().length;
24
- return Math.min(Math.max(cpus - 1, 0), MAX_WORKERS, Math.ceil(n / MIN_BATCH));
25
- }
26
- async function buildSequential(items, opts, withComplexity, out) {
27
- for (let i = 0; i < items.length; i++) {
28
- if (out[i] !== undefined)
29
- continue; // already produced by a worker
30
- try {
31
- const skel = await buildSkeleton(items[i].abs, items[i].rel, opts);
32
- const complexity = withComplexity
33
- ? await computeFileComplexity(items[i].abs, items[i].rel)
34
- : undefined;
35
- out[i] = { skel, complexity };
36
- }
37
- catch {
38
- out[i] = null; // unparsable / unsupported — callers skip nulls
39
- }
40
- }
41
- }
42
- /**
43
- * Build skeletons for many files, in parallel when it pays off.
44
- * Returns one entry per input item (null = failed/unsupported file).
45
- */
46
- export async function buildSkeletonsBulk(items, opts, withComplexity = false) {
47
- const out = new Array(items.length);
48
- const workers = plannedWorkers(items.length);
49
- const workerFile = path.join(path.dirname(fileURLToPath(import.meta.url)), "worker.js");
50
- // An explicit AST_MAP_WORKERS >= 2 bypasses the batch-size gate.
51
- const smallBatch = items.length < MIN_BATCH && envWorkers() === null;
52
- if (smallBatch || workers <= 1 || !fs.existsSync(workerFile)) {
53
- await buildSequential(items, opts, withComplexity, out);
54
- return out;
55
- }
56
- let failed = false;
57
- await new Promise((resolve) => {
58
- let next = 0;
59
- let done = 0;
60
- let open = 0;
61
- const pool = [];
62
- const finish = () => {
63
- for (const w of pool)
64
- void w.terminate();
65
- resolve();
66
- };
67
- const dispatch = (w) => {
68
- if (failed || next >= items.length) {
69
- return;
70
- }
71
- const id = next++;
72
- w.postMessage({ id, abs: items[id].abs, rel: items[id].rel, opts, withComplexity });
73
- };
74
- for (let i = 0; i < workers; i++) {
75
- let w;
76
- try {
77
- w = new Worker(workerFile, { workerData: { cacheDir: diskCacheDir() } });
78
- }
79
- catch {
80
- failed = true;
81
- break;
82
- }
83
- open++;
84
- pool.push(w);
85
- w.on("message", (msg) => {
86
- out[msg.id] = msg.ok && msg.skel ? { skel: msg.skel, complexity: msg.complexity } : null;
87
- done++;
88
- if (done >= items.length || failed)
89
- finish();
90
- else
91
- dispatch(w);
92
- });
93
- w.on("error", () => {
94
- failed = true;
95
- finish();
96
- });
97
- dispatch(w);
98
- // prime a second task per worker to hide round-trip latency
99
- dispatch(w);
100
- }
101
- if (pool.length === 0)
102
- resolve();
103
- else if (open === 0)
104
- resolve();
105
- });
106
- // Fill any gaps (worker failure, early termination) sequentially.
107
- for (let i = 0; i < items.length; i++) {
108
- if (out[i] === undefined) {
109
- await buildSequential(items, opts, withComplexity, out);
110
- break;
111
- }
112
- }
113
- return out;
114
- }
package/dist/prompts.js DELETED
@@ -1,67 +0,0 @@
1
- import { z } from "zod";
2
- /** GetPromptResult helper — a single user-role text message. */
3
- function userPrompt(text) {
4
- return { messages: [{ role: "user", content: { type: "text", text } }] };
5
- }
6
- const dirArg = { dir: z.string().optional().describe("Directory to analyze, relative to the project root. Default 'src'.") };
7
- const d = (dir) => (dir && dir.trim() ? dir.trim() : "src");
8
- /**
9
- * Register the Cookbook recipes as MCP prompts so clients can invoke a whole
10
- * AST-MCP workflow (a chain of tool calls) by name, instead of the user pasting
11
- * the recipe text. Each prompt returns a ready-to-run instruction that references
12
- * the server's own tools.
13
- */
14
- export function registerPrompts(server) {
15
- server.registerPrompt("architecture_audit", {
16
- title: "Architecture audit",
17
- description: "Full structural audit of a directory: God Nodes, cycles, rule violations, and module coupling.",
18
- argsSchema: dirArg,
19
- }, ({ dir }) => userPrompt(`Run a full architecture audit of \`${d(dir)}\` using the ast-mapper tools, in this order:\n\n` +
20
- `1. \`build_symbol_graph\` on \`${d(dir)}\` to load the dependency graph.\n` +
21
- `2. \`get_top_symbols\` — identify the 5 most-imported symbols (the "God Nodes").\n` +
22
- `3. \`find_circular_deps\` — report any circular import chains.\n` +
23
- `4. \`validate_architecture\` — list structural rule violations (large files, too many imports, god exports).\n` +
24
- `5. \`get_module_coupling\` — which directories are load-bearing (high Ca) vs. volatile (high I)?\n` +
25
- `6. \`get_layer_violations\` — any stable code depending on volatile code (SDP breaks)?\n\n` +
26
- `Then write a short prioritized summary: the top 3 architectural risks and a concrete first step for each.`));
27
- server.registerPrompt("safe_refactor", {
28
- title: "Safe refactor checklist",
29
- description: "Everything you need to know before changing a specific symbol: blast radius, callees, and minimal context.",
30
- argsSchema: {
31
- file: z.string().describe("File containing the symbol, relative to the project root."),
32
- symbol: z.string().describe("Name of the function/class/symbol you intend to change."),
33
- },
34
- }, ({ file, symbol }) => userPrompt(`Before refactoring \`${symbol}\` in \`${file}\`, gather the impact using the ast-mapper tools:\n\n` +
35
- `1. \`get_change_impact\` for \`${file}\` / \`${symbol}\` — who depends on it (the blast radius)?\n` +
36
- `2. \`get_call_graph\` — what does \`${symbol}\` call, and what calls it?\n` +
37
- `3. \`pack_context\` for \`${file}\` / \`${symbol}\` — the minimal context (its source + the signatures it depends on).\n\n` +
38
- `Then summarize: what will break if the signature changes, which call sites need updating, and a safe step-by-step refactor order.`));
39
- server.registerPrompt("dead_code_cleanup", {
40
- title: "Dead-code cleanup",
41
- description: "Find unused exports and verify each is safe to delete before removing it.",
42
- argsSchema: dirArg,
43
- }, ({ dir }) => userPrompt(`Help me remove dead code from \`${d(dir)}\` using the ast-mapper tools:\n\n` +
44
- `1. \`find_dead_code\` on \`${d(dir)}\` — list exported symbols nobody imports.\n` +
45
- `2. For each HIGH-confidence result, double-check with \`get_change_impact\` (should be empty).\n` +
46
- `3. Before suggesting deletion, show the symbol's source with \`get_symbol_context\`.\n\n` +
47
- `Produce a deletion checklist: only symbols that are high-confidence AND have zero impact. Flag anything dynamic (string-referenced, re-exported) as "verify manually".`));
48
- server.registerPrompt("health_check", {
49
- title: "Codebase health check",
50
- description: "A one-pass health report: overall grade, riskiest files, and stability inversions.",
51
- argsSchema: dirArg,
52
- }, ({ dir }) => userPrompt(`Give me a health check of \`${d(dir)}\` using the ast-mapper tools:\n\n` +
53
- `1. \`get_codebase_report\` on \`${d(dir)}\` — overall grade A–F, hotspots, god nodes, dead code, cycles.\n` +
54
- `2. \`get_risk_map\` — the files with the highest churn × complexity (best refactor / test targets).\n` +
55
- `3. \`get_layer_violations\` — stable files depending on volatile ones.\n\n` +
56
- `Summarize as: the grade, the single biggest problem, and the 3 files I should touch first to improve it.`));
57
- server.registerPrompt("onboard_codebase", {
58
- title: "Onboard to a codebase",
59
- description: "Get oriented in an unfamiliar directory: languages, structure, the core symbols, and how modules connect.",
60
- argsSchema: dirArg,
61
- }, ({ dir }) => userPrompt(`I'm new to this codebase. Walk me through \`${d(dir)}\` using the ast-mapper tools:\n\n` +
62
- `1. \`list_supported_languages\` then \`generate_skeleton\` on \`${d(dir)}\` — what languages and overall shape?\n` +
63
- `2. \`get_top_symbols\` — the most-depended-on symbols are the concepts to learn first.\n` +
64
- `3. \`get_module_coupling\` — how do the directories relate; what's the stable core?\n` +
65
- `4. For the top God Node, \`get_symbol_context\` with related symbols to see how it's used.\n\n` +
66
- `Then write a 5-bullet "start here" orientation: what this code does, its core abstractions, and where to begin reading.`));
67
- }
package/dist/registry.js DELETED
@@ -1,87 +0,0 @@
1
- import path from "node:path";
2
- import { extractTypeScript, extractDirectivesTS, extractImportsTS } from "./extractors/typescript.js";
3
- import { extractPython, extractImportsPython } from "./extractors/python.js";
4
- import { extractGo, extractImportsGo } from "./extractors/go.js";
5
- import { extractRust, extractImportsRust } from "./extractors/rust.js";
6
- import { extractJava, extractDirectivesJava, extractImportsJava } from "./extractors/java.js";
7
- import { extractCSharp, extractDirectivesCSharp, extractImportsCSharp } from "./extractors/csharp.js";
8
- import { extractC, extractImportsC } from "./extractors/c.js";
9
- import { extractCpp, extractImportsCpp } from "./extractors/cpp.js";
10
- import { extractKotlin, extractDirectivesKotlin, extractImportsKotlin } from "./extractors/kotlin.js";
11
- import { extractSwift, extractImportsSwift } from "./extractors/swift.js";
12
- import { extractPhp, extractImportsPhp } from "./extractors/php.js";
13
- import { extractRuby, extractImportsRuby } from "./extractors/ruby.js";
14
- const TS_ENTRY = (language, grammar) => ({
15
- language,
16
- grammar,
17
- extract: extractTypeScript,
18
- extractDirectives: extractDirectivesTS,
19
- extractImports: extractImportsTS,
20
- });
21
- const SFC_ENTRY = (language) => ({
22
- language,
23
- grammar: "typescript", // overridden per-file after detecting lang="ts" vs js
24
- extract: extractTypeScript,
25
- extractDirectives: extractDirectivesTS,
26
- extractImports: extractImportsTS,
27
- sfc: true,
28
- });
29
- const BY_EXT = {
30
- ".ts": TS_ENTRY("typescript", "typescript"),
31
- ".mts": TS_ENTRY("typescript", "typescript"),
32
- ".cts": TS_ENTRY("typescript", "typescript"),
33
- ".tsx": TS_ENTRY("tsx", "tsx"),
34
- ".js": TS_ENTRY("javascript", "javascript"),
35
- ".mjs": TS_ENTRY("javascript", "javascript"),
36
- ".cjs": TS_ENTRY("javascript", "javascript"),
37
- ".jsx": TS_ENTRY("javascript", "tsx"),
38
- ".py": { language: "python", grammar: "python", extract: extractPython, extractImports: extractImportsPython },
39
- ".pyi": { language: "python", grammar: "python", extract: extractPython, extractImports: extractImportsPython },
40
- ".go": { language: "go", grammar: "go", extract: extractGo, extractImports: extractImportsGo },
41
- ".rs": { language: "rust", grammar: "rust", extract: extractRust, extractImports: extractImportsRust },
42
- ".java": {
43
- language: "java", grammar: "java",
44
- extract: extractJava, extractDirectives: extractDirectivesJava, extractImports: extractImportsJava,
45
- },
46
- ".cs": {
47
- language: "csharp", grammar: "c_sharp",
48
- extract: extractCSharp, extractDirectives: extractDirectivesCSharp, extractImports: extractImportsCSharp,
49
- },
50
- ".c": { language: "c", grammar: "c", extract: extractC, extractImports: extractImportsC },
51
- ".h": { language: "c", grammar: "c", extract: extractC, extractImports: extractImportsC },
52
- ".cpp": { language: "cpp", grammar: "cpp", extract: extractCpp, extractImports: extractImportsCpp },
53
- ".cxx": { language: "cpp", grammar: "cpp", extract: extractCpp, extractImports: extractImportsCpp },
54
- ".cc": { language: "cpp", grammar: "cpp", extract: extractCpp, extractImports: extractImportsCpp },
55
- ".hpp": { language: "cpp", grammar: "cpp", extract: extractCpp, extractImports: extractImportsCpp },
56
- ".hxx": { language: "cpp", grammar: "cpp", extract: extractCpp, extractImports: extractImportsCpp },
57
- ".hh": { language: "cpp", grammar: "cpp", extract: extractCpp, extractImports: extractImportsCpp },
58
- ".kt": {
59
- language: "kotlin", grammar: "kotlin",
60
- extract: extractKotlin, extractDirectives: extractDirectivesKotlin, extractImports: extractImportsKotlin,
61
- },
62
- ".kts": {
63
- language: "kotlin", grammar: "kotlin",
64
- extract: extractKotlin, extractDirectives: extractDirectivesKotlin, extractImports: extractImportsKotlin,
65
- },
66
- ".swift": { language: "swift", grammar: "swift", extract: extractSwift, extractImports: extractImportsSwift },
67
- ".php": { language: "php", grammar: "php", extract: extractPhp, extractImports: extractImportsPhp },
68
- ".rb": { language: "ruby", grammar: "ruby", extract: extractRuby, extractImports: extractImportsRuby },
69
- ".rake": { language: "ruby", grammar: "ruby", extract: extractRuby, extractImports: extractImportsRuby },
70
- ".vue": SFC_ENTRY("vue"),
71
- ".svelte": SFC_ENTRY("svelte"),
72
- };
73
- export function detectLanguage(filePath) {
74
- return BY_EXT[path.extname(filePath).toLowerCase()] ?? null;
75
- }
76
- export function supportedExtensions() {
77
- return Object.keys(BY_EXT);
78
- }
79
- export function supportedLanguages() {
80
- const map = new Map();
81
- for (const [ext, entry] of Object.entries(BY_EXT)) {
82
- const arr = map.get(entry.language) ?? [];
83
- arr.push(ext);
84
- map.set(entry.language, arr);
85
- }
86
- return [...map.entries()].map(([language, extensions]) => ({ language, extensions }));
87
- }
package/dist/report.js DELETED
@@ -1,187 +0,0 @@
1
- import path from "node:path";
2
- import { collectSourceFiles } from "./skeleton.js";
3
- import { buildSkeletonsBulk } from "./pool.js";
4
- import { resolveOptions } from "./config.js";
5
- import { buildSymbolGraph } from "./graph.js";
6
- import { findDeadExports, findCircularDeps, getTopSymbols } from "./graph-analysis.js";
7
- import { findLayerViolations } from "./layers.js";
8
- import { computeModuleCoupling } from "./modulecoupling.js";
9
- function gradeFor(score) {
10
- if (score >= 90)
11
- return "A";
12
- if (score >= 80)
13
- return "B";
14
- if (score >= 70)
15
- return "C";
16
- if (score >= 60)
17
- return "D";
18
- return "F";
19
- }
20
- export async function buildReport(absDir, root) {
21
- const opts = resolveOptions({ detail: "outline", emitHtml: false });
22
- const files = collectSourceFiles(absDir, opts);
23
- const skeletons = [];
24
- const langCount = new Map();
25
- let symbolCount = 0;
26
- const hotspots = [];
27
- let cxSum = 0, cxN = 0, cxMax = 0;
28
- const items = files.map((file) => ({
29
- abs: file,
30
- rel: path.relative(root, file).split(path.sep).join("/"),
31
- }));
32
- const built = await buildSkeletonsBulk(items, opts, true);
33
- for (let i = 0; i < built.length; i++) {
34
- const r = built[i];
35
- if (!r)
36
- continue; // skip unparsable
37
- const rel = items[i].rel;
38
- skeletons.push(r.skel);
39
- symbolCount += r.skel.symbolCount;
40
- langCount.set(r.skel.language, (langCount.get(r.skel.language) ?? 0) + 1);
41
- if (r.complexity) {
42
- for (const f of r.complexity.functions) {
43
- hotspots.push({ ...f, file: rel });
44
- cxSum += f.complexity;
45
- cxN++;
46
- cxMax = Math.max(cxMax, f.complexity);
47
- }
48
- }
49
- }
50
- const graph = buildSymbolGraph(skeletons, root);
51
- const dead = findDeadExports(graph).filter((d) => d.confidence === "high");
52
- const cycles = findCircularDeps(graph);
53
- const god = getTopSymbols(graph, 8);
54
- const layerViolations = findLayerViolations(graph);
55
- const modules = computeModuleCoupling(graph).modules;
56
- hotspots.sort((a, b) => b.complexity - a.complexity);
57
- const veryHigh = hotspots.filter((f) => f.complexity > 20).length;
58
- const high = hotspots.filter((f) => f.complexity > 10 && f.complexity <= 20).length;
59
- // Health score: start at 100, subtract weighted penalties.
60
- let score = 100;
61
- score -= Math.min(20, dead.length * 1.5);
62
- score -= Math.min(22, cycles.length * 6);
63
- score -= Math.min(28, veryHigh * 4 + high * 1);
64
- score -= Math.min(12, god.filter((g) => g.importCount >= 8).length * 4);
65
- score -= Math.min(10, layerViolations.length);
66
- score = Math.max(0, Math.round(score));
67
- const languages = [...langCount.entries()]
68
- .map(([lang, f]) => ({ lang, files: f }))
69
- .sort((a, b) => b.files - a.files);
70
- return {
71
- project: absDir.split(/[\\/]/).filter(Boolean).pop() || "project",
72
- generatedAt: new Date().toISOString(),
73
- fileCount: skeletons.length,
74
- symbolCount,
75
- edgeCount: graph.edges.filter((e) => e.edgeType === "imports").length,
76
- languages,
77
- score,
78
- grade: gradeFor(score),
79
- dead: { count: dead.length, items: dead.slice(0, 25).map((d) => ({ file: d.file, symbol: d.symbol, kind: d.kind })) },
80
- cycles: { count: cycles.length, items: cycles.slice(0, 12).map((c) => c.cycle) },
81
- godNodes: god.map((g) => ({ symbol: g.symbol, file: g.file, importCount: g.importCount })),
82
- complexity: { average: cxN ? Math.round((cxSum / cxN) * 10) / 10 : 0, max: cxMax, hotspots: hotspots.slice(0, 12) },
83
- layerViolations: { count: layerViolations.length, items: layerViolations.slice(0, 12) },
84
- modules: modules.slice(0, 10),
85
- };
86
- }
87
- /* ─── Premium HTML dashboard ───────────────────────────────────────────────── */
88
- const GRADE_COLOR = {
89
- A: "#1d9e75", B: "#1d9e75", C: "#ba7517", D: "#d85a30", F: "#e24b4a",
90
- };
91
- function esc(s) {
92
- return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
93
- }
94
- function ratingColor(r) {
95
- return r === "very-high" ? "#e24b4a" : r === "high" ? "#d85a30" : r === "moderate" ? "#ba7517" : "#1d9e75";
96
- }
97
- function instColor(i) {
98
- return i >= 0.8 ? "#e24b4a" : i <= 0.2 ? "#1d9e75" : "#ba7517";
99
- }
100
- function statCard(label, value, accent) {
101
- return `<div class="stat"><div class="sv"${accent ? ` style="color:${accent}"` : ""}>${value}</div><div class="sl">${label}</div></div>`;
102
- }
103
- function bar(label, value, max, color, right) {
104
- const pct = max > 0 ? Math.round((value / max) * 100) : 0;
105
- 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>`;
106
- }
107
- export function buildReportHtml(d) {
108
- const gc = GRADE_COLOR[d.grade] ?? "#888";
109
- const maxLang = d.languages[0]?.files ?? 1;
110
- const langs = d.languages.map((l) => bar(l.lang, l.files, maxLang, "#534ab7", `${l.files}`)).join("");
111
- const maxCx = d.complexity.hotspots[0]?.complexity ?? 1;
112
- const hotspots = d.complexity.hotspots.length
113
- ? d.complexity.hotspots.map((h) => bar(`${h.name} · ${h.file}`, h.complexity, maxCx, ratingColor(h.rating), `<b>${h.complexity}</b>`)).join("")
114
- : `<div class="empty">No functions found.</div>`;
115
- const god = d.godNodes.length
116
- ? 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("")
117
- : `<div class="empty">None.</div>`;
118
- const dead = d.dead.count
119
- ? 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("")
120
- + (d.dead.count > d.dead.items.length ? `<div class="more">+${d.dead.count - d.dead.items.length} more…</div>` : "")
121
- : `<div class="ok">✓ No high-confidence dead exports</div>`;
122
- const cycles = d.cycles.count
123
- ? d.cycles.items.map((c) => `<div class="li"><span class="mono">${esc(c.join(" → "))}</span></div>`).join("")
124
- : `<div class="ok">✓ No circular dependencies</div>`;
125
- const modules = d.modules.length
126
- ? d.modules.map((m) => bar(`${m.module} · ${m.files} file(s)`, m.instability, 1, instColor(m.instability), `Ca ${m.afferent} · Ce ${m.efferent} · <b>I ${m.instability.toFixed(2)}</b>`)).join("")
127
- : `<div class="empty">No cross-module imports.</div>`;
128
- const sdp = d.layerViolations.count
129
- ? d.layerViolations.items.map((v) => `<div class="li"><span class="mono">${esc(v.from)}</span><span class="dim">→ ${esc(v.to)}</span><span class="pill" style="color:${instColor(0.9)}">+${v.severity.toFixed(2)}</span></div>`).join("")
130
- + (d.layerViolations.count > d.layerViolations.items.length ? `<div class="more">+${d.layerViolations.count - d.layerViolations.items.length} more…</div>` : "")
131
- : `<div class="ok">✓ No stability inversions (SDP)</div>`;
132
- return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
133
- <title>${esc(d.project)} — code health</title><style>
134
- :root{--bg:#fafaf8;--card:#fff;--bd:#e7e5df;--tx:#2b2b28;--dim:#8a8880;--soft:#f1efe9}
135
- @media(prefers-color-scheme:dark){:root{--bg:#161613;--card:#1e1e1b;--bd:#33332e;--tx:#e6e4dd;--dim:#9a988f;--soft:#26261f}}
136
- *{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}
137
- .wrap{max-width:980px;margin:0 auto;padding:32px 24px 60px}
138
- .hero{display:flex;align-items:center;gap:24px;margin-bottom:28px}
139
- .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}
140
- .badge .g{font-size:46px;font-weight:700;line-height:1}.badge .s{font-size:12px;opacity:.9}
141
- .h1{font-size:26px;font-weight:650;margin:0}.sub{color:var(--dim);font-size:13px;margin-top:4px}
142
- .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(110px,1fr));gap:12px;margin-bottom:30px}
143
- .stat{background:var(--card);border:1px solid var(--bd);border-radius:14px;padding:14px 16px}
144
- .sv{font-size:24px;font-weight:650}.sl{font-size:12px;color:var(--dim);margin-top:2px}
145
- .card{background:var(--card);border:1px solid var(--bd);border-radius:16px;padding:18px 20px;margin-bottom:18px}
146
- .card h2{font-size:14px;font-weight:600;margin:0 0 14px;letter-spacing:.02em;text-transform:uppercase;color:var(--dim)}
147
- .row{display:flex;align-items:center;gap:12px;margin:7px 0;font-size:13px}
148
- .rl{flex:0 0 46%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
149
- .track{flex:1;height:8px;background:var(--soft);border-radius:5px;overflow:hidden}.fill{height:100%;border-radius:5px}
150
- .rr{flex:0 0 auto;color:var(--dim);min-width:32px;text-align:right}
151
- .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}
152
- .mono{font-family:ui-monospace,monospace;font-weight:550}.dim{color:var(--dim);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
153
- .pill{margin-left:auto;background:var(--soft);border-radius:20px;padding:2px 10px;font-size:11px;color:var(--dim);flex:0 0 auto}
154
- .kbadge{font-size:11px;color:var(--dim);background:var(--soft);border-radius:5px;padding:1px 7px;flex:0 0 auto}
155
- .ok{color:#1d9e75;font-size:13px}.empty{color:var(--dim);font-size:13px}.more{color:var(--dim);font-size:12px;padding-top:6px}
156
- .two{display:grid;grid-template-columns:1fr 1fr;gap:18px}@media(max-width:720px){.two{grid-template-columns:1fr}.rl{flex-basis:42%}}
157
- .foot{color:var(--dim);font-size:11px;text-align:center;margin-top:24px}
158
- </style></head><body><div class="wrap">
159
- <div class="hero">
160
- <div class="badge" style="background:${gc}"><div class="g">${d.grade}</div><div class="s">${d.score}/100</div></div>
161
- <div><h1 class="h1">${esc(d.project)} — code health</h1>
162
- <div class="sub">${d.fileCount} files · ${d.symbolCount} symbols · ${d.languages.length} language(s) · ${esc(d.generatedAt.slice(0, 10))}</div></div>
163
- </div>
164
- <div class="grid">
165
- ${statCard("Files", d.fileCount)}
166
- ${statCard("Symbols", d.symbolCount)}
167
- ${statCard("Import edges", d.edgeCount)}
168
- ${statCard("Avg complexity", d.complexity.average)}
169
- ${statCard("Max complexity", d.complexity.max, ratingColor(d.complexity.max > 20 ? "very-high" : d.complexity.max > 10 ? "high" : "low"))}
170
- ${statCard("Dead exports", d.dead.count, d.dead.count ? "#d85a30" : "#1d9e75")}
171
- ${statCard("Cycles", d.cycles.count, d.cycles.count ? "#e24b4a" : "#1d9e75")}
172
- ${statCard("SDP violations", d.layerViolations.count, d.layerViolations.count ? "#d85a30" : "#1d9e75")}
173
- </div>
174
- <div class="card"><h2>Language breakdown</h2>${langs}</div>
175
- <div class="card"><h2>Complexity hotspots</h2>${hotspots}</div>
176
- <div class="two">
177
- <div class="card"><h2>God nodes (most imported)</h2>${god}</div>
178
- <div class="card"><h2>Circular dependencies</h2>${cycles}</div>
179
- </div>
180
- <div class="two">
181
- <div class="card"><h2>Module coupling (instability)</h2>${modules}</div>
182
- <div class="card"><h2>Layer violations (stable → volatile)</h2>${sdp}</div>
183
- </div>
184
- <div class="card"><h2>Dead exports (high confidence)</h2>${dead}</div>
185
- <div class="foot">Generated by AST-MCP · universal-ast-mapper</div>
186
- </div></body></html>`;
187
- }