universal-ast-mapper 2.0.0 → 2.0.1

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 (75) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +261 -12
  3. package/dist/ai-refactor.js +185 -0
  4. package/dist/ai-testgen.js +105 -0
  5. package/dist/analysis.js +134 -0
  6. package/dist/arch-rules.js +82 -0
  7. package/dist/callgraph.js +467 -0
  8. package/dist/check.js +112 -0
  9. package/dist/cli.js +2284 -0
  10. package/dist/complexity.js +98 -0
  11. package/dist/config.js +53 -0
  12. package/dist/contextpack.js +79 -0
  13. package/dist/coupling.js +35 -0
  14. package/dist/covmerge.js +176 -0
  15. package/dist/crosslang.js +425 -0
  16. package/dist/dashboard.js +259 -0
  17. package/dist/diagram.js +264 -0
  18. package/dist/diskcache.js +97 -0
  19. package/dist/docgen.js +156 -0
  20. package/dist/embeddings.js +136 -0
  21. package/dist/explain.js +123 -0
  22. package/dist/explorer.js +123 -0
  23. package/dist/extractors/c.js +204 -0
  24. package/dist/extractors/common.js +56 -0
  25. package/dist/extractors/cpp.js +272 -0
  26. package/dist/extractors/csharp.js +209 -0
  27. package/dist/extractors/go.js +212 -0
  28. package/dist/extractors/java.js +152 -0
  29. package/dist/extractors/kotlin.js +159 -0
  30. package/dist/extractors/php.js +208 -0
  31. package/dist/extractors/python.js +153 -0
  32. package/dist/extractors/ruby.js +146 -0
  33. package/dist/extractors/rust.js +249 -0
  34. package/dist/extractors/swift.js +192 -0
  35. package/dist/extractors/typescript.js +577 -0
  36. package/dist/fix.js +92 -0
  37. package/dist/gitdiff.js +178 -0
  38. package/dist/graph-analysis.js +279 -0
  39. package/dist/graph.js +165 -0
  40. package/dist/history.js +36 -0
  41. package/dist/html.js +658 -0
  42. package/dist/incremental.js +122 -0
  43. package/dist/index.js +1945 -0
  44. package/dist/indexstore.js +105 -0
  45. package/dist/layers.js +36 -0
  46. package/dist/lsp.js +238 -0
  47. package/dist/modulecoupling.js +0 -0
  48. package/dist/parser.js +84 -0
  49. package/dist/patch.js +199 -0
  50. package/dist/plugins.js +88 -0
  51. package/dist/pool.js +114 -0
  52. package/dist/prompts.js +67 -0
  53. package/dist/registry.js +87 -0
  54. package/dist/report.js +441 -0
  55. package/dist/resolver.js +222 -0
  56. package/dist/roots.js +47 -0
  57. package/dist/search.js +68 -0
  58. package/dist/security.js +178 -0
  59. package/dist/semantic.js +365 -0
  60. package/dist/serve.js +185 -0
  61. package/dist/sfc.js +27 -0
  62. package/dist/similar.js +98 -0
  63. package/dist/skeleton.js +132 -0
  64. package/dist/smells.js +285 -0
  65. package/dist/sourcemap.js +60 -0
  66. package/dist/testgen.js +280 -0
  67. package/dist/testmap.js +167 -0
  68. package/dist/tsconfig.js +212 -0
  69. package/dist/typeflow.js +124 -0
  70. package/dist/types.js +5 -0
  71. package/dist/unused-params.js +127 -0
  72. package/dist/webapp.js +341 -0
  73. package/dist/worker.js +27 -0
  74. package/dist/workspace.js +330 -0
  75. package/package.json +2 -1
@@ -0,0 +1,88 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ // ─── Loader ───────────────────────────────────────────────────────────────────
4
+ const PLUGINS_DIR = ".ast-map/plugins";
5
+ /** Load all `.mjs` / `.js` plugin files from `<root>/.ast-map/plugins/`. */
6
+ export async function loadPlugins(root) {
7
+ const dir = path.join(root, PLUGINS_DIR);
8
+ if (!fs.existsSync(dir))
9
+ return [];
10
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith(".mjs") || f.endsWith(".js"));
11
+ const plugins = [];
12
+ for (const file of files) {
13
+ const abs = path.resolve(dir, file);
14
+ try {
15
+ const mod = await import(abs);
16
+ const plugin = "default" in mod && isPlugin(mod.default) ? mod.default
17
+ : "plugin" in mod && isPlugin(mod.plugin) ? mod.plugin
18
+ : isPlugin(mod) ? mod
19
+ : undefined;
20
+ if (plugin)
21
+ plugins.push(plugin);
22
+ else
23
+ process.stderr.write(`[ast-map plugins] ${file}: no valid default/plugin export\n`);
24
+ }
25
+ catch (e) {
26
+ process.stderr.write(`[ast-map plugins] failed to load ${file}: ${e instanceof Error ? e.message : String(e)}\n`);
27
+ }
28
+ }
29
+ return plugins;
30
+ }
31
+ function isPlugin(v) {
32
+ return typeof v === "object" && v !== null && "id" in v && "run" in v && typeof v.run === "function";
33
+ }
34
+ /** Run all loaded plugins against the given skeletons. */
35
+ export async function runPlugins(plugins, ctx) {
36
+ const results = [];
37
+ for (const plugin of plugins) {
38
+ try {
39
+ const violations = await plugin.run(ctx);
40
+ results.push({ pluginId: plugin.id, description: plugin.description, violations });
41
+ }
42
+ catch (e) {
43
+ results.push({
44
+ pluginId: plugin.id,
45
+ description: plugin.description,
46
+ violations: [],
47
+ error: e instanceof Error ? e.message : String(e),
48
+ });
49
+ }
50
+ }
51
+ return results;
52
+ }
53
+ // ─── Example plugin scaffolding (written to disk by ast-map init) ─────────────
54
+ export const EXAMPLE_PLUGIN = `/**
55
+ * Example ast-map plugin: no-console-in-lib
56
+ *
57
+ * Reports any "console.log/warn/error" usage in library source files.
58
+ * Adapt the logic to define your own custom rules.
59
+ *
60
+ * Export your plugin as the default export.
61
+ * @type {import('universal-ast-mapper').AstMapPlugin}
62
+ */
63
+ export default {
64
+ id: "no-console-in-lib",
65
+ description: "Disallow console.* calls in library code",
66
+
67
+ run({ skeletons }) {
68
+ const violations = [];
69
+ for (const skel of skeletons) {
70
+ // Only apply to files under src/ (adjust as needed)
71
+ if (!skel.file.startsWith("src/")) continue;
72
+ for (const sym of skel.symbols) {
73
+ if (sym.name.startsWith("console")) {
74
+ violations.push({
75
+ rule: "no-console-in-lib",
76
+ file: skel.file,
77
+ line: sym.range?.startLine,
78
+ symbol: sym.name,
79
+ severity: "warning",
80
+ message: \`console.\${sym.name} should not be used in library code\`,
81
+ });
82
+ }
83
+ }
84
+ }
85
+ return violations;
86
+ },
87
+ };
88
+ `;
package/dist/pool.js ADDED
@@ -0,0 +1,114 @@
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
+ }
@@ -0,0 +1,67 @@
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
+ }
@@ -0,0 +1,87 @@
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
+ }