universal-ast-mapper 2.0.0 → 2.0.2

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 +15 -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 +328 -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 +646 -0
  73. package/dist/worker.js +27 -0
  74. package/dist/workspace.js +330 -0
  75. package/package.json +2 -1
package/dist/worker.js ADDED
@@ -0,0 +1,27 @@
1
+ // Worker-thread entry: builds skeletons (and optionally per-file complexity)
2
+ // off the main thread. Spawned by pool.ts with { cacheDir } as workerData.
3
+ import { parentPort, workerData } from "node:worker_threads";
4
+ import { buildSkeleton } from "./skeleton.js";
5
+ import { computeFileComplexity } from "./complexity.js";
6
+ import { initDiskCache } from "./diskcache.js";
7
+ const data = (workerData ?? {});
8
+ if (data.cacheDir)
9
+ initDiskCache(data.cacheDir);
10
+ parentPort.on("message", (msg) => {
11
+ void (async () => {
12
+ try {
13
+ const skel = await buildSkeleton(msg.abs, msg.rel, msg.opts);
14
+ const complexity = msg.withComplexity
15
+ ? await computeFileComplexity(msg.abs, msg.rel)
16
+ : undefined;
17
+ parentPort.postMessage({ id: msg.id, ok: true, skel, complexity });
18
+ }
19
+ catch (e) {
20
+ parentPort.postMessage({
21
+ id: msg.id,
22
+ ok: false,
23
+ error: e instanceof Error ? e.message : String(e),
24
+ });
25
+ }
26
+ })();
27
+ });
@@ -0,0 +1,330 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const IGNORE_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "coverage"]);
4
+ function readJson(file) {
5
+ try {
6
+ return JSON.parse(fs.readFileSync(file, "utf8"));
7
+ }
8
+ catch {
9
+ return null;
10
+ }
11
+ }
12
+ /** Minimal `packages:` list reader for pnpm-workspace.yaml (no YAML dep). */
13
+ function readPnpmPatterns(file) {
14
+ let text;
15
+ try {
16
+ text = fs.readFileSync(file, "utf8");
17
+ }
18
+ catch {
19
+ return [];
20
+ }
21
+ const out = [];
22
+ let inPackages = false;
23
+ for (const raw of text.split(/\r?\n/)) {
24
+ const line = raw.replace(/#.*$/, "");
25
+ if (/^packages\s*:/.test(line)) {
26
+ inPackages = true;
27
+ continue;
28
+ }
29
+ if (inPackages) {
30
+ const m = line.match(/^\s*-\s*['"]?([^'"]+?)['"]?\s*$/);
31
+ if (m)
32
+ out.push(m[1].trim());
33
+ else if (/^\S/.test(line))
34
+ break; // next top-level key
35
+ }
36
+ }
37
+ return out;
38
+ }
39
+ /** Expand a workspace glob pattern to package directories containing package.json. */
40
+ function expandPattern(rootAbs, pattern) {
41
+ const clean = pattern.replace(/\/+$/, "");
42
+ const dirs = [];
43
+ const hasPkg = (dir) => fs.existsSync(path.join(dir, "package.json"));
44
+ if (!clean.includes("*")) {
45
+ const abs = path.resolve(rootAbs, clean);
46
+ if (hasPkg(abs))
47
+ dirs.push(abs);
48
+ return dirs;
49
+ }
50
+ if (clean.endsWith("/**")) {
51
+ const base = path.resolve(rootAbs, clean.slice(0, -3));
52
+ walkForPackages(base, dirs, 6);
53
+ return dirs;
54
+ }
55
+ // `prefix/*` — immediate subdirectories.
56
+ if (clean.endsWith("/*")) {
57
+ const base = path.resolve(rootAbs, clean.slice(0, -2));
58
+ let entries = [];
59
+ try {
60
+ entries = fs.readdirSync(base, { withFileTypes: true });
61
+ }
62
+ catch {
63
+ return dirs;
64
+ }
65
+ for (const e of entries) {
66
+ if (e.isDirectory() && !IGNORE_DIRS.has(e.name) && hasPkg(path.join(base, e.name))) {
67
+ dirs.push(path.join(base, e.name));
68
+ }
69
+ }
70
+ return dirs;
71
+ }
72
+ // Fallback: bounded recursive scan under the non-glob prefix.
73
+ const prefix = clean.split("*")[0];
74
+ walkForPackages(path.resolve(rootAbs, prefix), dirs, 6);
75
+ return dirs;
76
+ }
77
+ function walkForPackages(base, out, depth) {
78
+ if (depth < 0)
79
+ return;
80
+ let entries;
81
+ try {
82
+ entries = fs.readdirSync(base, { withFileTypes: true });
83
+ }
84
+ catch {
85
+ return;
86
+ }
87
+ if (fs.existsSync(path.join(base, "package.json")))
88
+ out.push(base);
89
+ for (const e of entries) {
90
+ if (e.isDirectory() && !IGNORE_DIRS.has(e.name)) {
91
+ walkForPackages(path.join(base, e.name), out, depth - 1);
92
+ }
93
+ }
94
+ }
95
+ function depNames(pkg) {
96
+ const out = new Set();
97
+ for (const key of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
98
+ const d = pkg?.[key];
99
+ if (d && typeof d === "object")
100
+ for (const k of Object.keys(d))
101
+ out.add(k);
102
+ }
103
+ return [...out];
104
+ }
105
+ /**
106
+ * Discover the packages in a JS/TS monorepo and the dependency edges between
107
+ * them. Supports npm/yarn `workspaces`, pnpm-workspace.yaml, and lerna.json.
108
+ */
109
+ export function discoverWorkspace(rootAbs) {
110
+ const rootPkg = readJson(path.join(rootAbs, "package.json"));
111
+ let tool = "none";
112
+ const patterns = [];
113
+ // npm / yarn workspaces
114
+ const ws = rootPkg?.workspaces;
115
+ if (Array.isArray(ws)) {
116
+ patterns.push(...ws);
117
+ tool = "npm";
118
+ }
119
+ else if (ws && Array.isArray(ws.packages)) {
120
+ patterns.push(...ws.packages);
121
+ tool = "npm";
122
+ }
123
+ // pnpm
124
+ const pnpmFile = path.join(rootAbs, "pnpm-workspace.yaml");
125
+ if (fs.existsSync(pnpmFile)) {
126
+ patterns.push(...readPnpmPatterns(pnpmFile));
127
+ tool = "pnpm";
128
+ }
129
+ // lerna
130
+ const lerna = readJson(path.join(rootAbs, "lerna.json"));
131
+ if (lerna && Array.isArray(lerna.packages)) {
132
+ patterns.push(...lerna.packages);
133
+ if (tool === "none")
134
+ tool = "lerna";
135
+ }
136
+ // Dedupe patterns, expand to package dirs.
137
+ const dirSet = new Set();
138
+ for (const p of [...new Set(patterns)]) {
139
+ for (const d of expandPattern(rootAbs, p))
140
+ dirSet.add(d);
141
+ }
142
+ const packages = [];
143
+ const nameToPkg = new Map();
144
+ const pending = [];
145
+ for (const dirAbs of [...dirSet].sort()) {
146
+ const pj = readJson(path.join(dirAbs, "package.json"));
147
+ if (!pj || !pj.name)
148
+ continue;
149
+ const rel = path.relative(rootAbs, dirAbs).split(path.sep).join("/");
150
+ const allDeps = depNames(pj);
151
+ const wp = { name: pj.name, dir: rel || ".", internalDeps: [], allDeps };
152
+ packages.push(wp);
153
+ nameToPkg.set(pj.name, wp);
154
+ pending.push({ pkg: wp, deps: allDeps });
155
+ }
156
+ // Resolve internal deps now that all package names are known.
157
+ const edges = [];
158
+ for (const { pkg, deps } of pending) {
159
+ for (const d of deps) {
160
+ if (nameToPkg.has(d) && d !== pkg.name) {
161
+ pkg.internalDeps.push(d);
162
+ edges.push({ from: pkg.name, to: d });
163
+ }
164
+ }
165
+ pkg.internalDeps.sort();
166
+ }
167
+ return { root: rootAbs, tool, packages, edges };
168
+ }
169
+ /** Detect circular dependencies among workspace packages (package-level). */
170
+ export function findPackageCycles(info) {
171
+ const adj = new Map();
172
+ for (const p of info.packages)
173
+ adj.set(p.name, p.internalDeps.slice());
174
+ const color = new Map();
175
+ for (const k of adj.keys())
176
+ color.set(k, "white");
177
+ const cycles = [];
178
+ const seen = new Set();
179
+ const path = [];
180
+ const dfs = (node) => {
181
+ color.set(node, "gray");
182
+ path.push(node);
183
+ for (const next of adj.get(node) ?? []) {
184
+ const c = color.get(next);
185
+ if (c === "gray") {
186
+ const start = path.indexOf(next);
187
+ const raw = path.slice(start);
188
+ const min = raw.reduce((b, n, i) => (n < raw[b] ? i : b), 0);
189
+ const canon = [...raw.slice(min), ...raw.slice(0, min)];
190
+ const key = canon.join(">");
191
+ if (!seen.has(key)) {
192
+ seen.add(key);
193
+ cycles.push([...canon, canon[0]]);
194
+ }
195
+ }
196
+ else if (c === "white") {
197
+ dfs(next);
198
+ }
199
+ }
200
+ path.pop();
201
+ color.set(node, "black");
202
+ };
203
+ for (const k of adj.keys())
204
+ if (color.get(k) === "white")
205
+ dfs(k);
206
+ return cycles;
207
+ }
208
+ /* ─── File-level cross-package import resolution ───────────────────────────── */
209
+ const SRC_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
210
+ const JS_TO_SRC = {
211
+ ".js": [".ts", ".tsx", ".js"],
212
+ ".jsx": [".tsx", ".jsx"],
213
+ ".mjs": [".mts", ".mjs"],
214
+ ".cjs": [".cts", ".cjs"],
215
+ };
216
+ function existsFileSync(p) {
217
+ try {
218
+ return fs.statSync(p).isFile();
219
+ }
220
+ catch {
221
+ return false;
222
+ }
223
+ }
224
+ /**
225
+ * Resolve a candidate path (file, extensionless, or directory) to a real source
226
+ * file, preferring TS sources over the declared `.js` build output.
227
+ */
228
+ function resolveSourceFile(candidate) {
229
+ const ext = path.extname(candidate).toLowerCase();
230
+ if (ext && JS_TO_SRC[ext]) {
231
+ const base = candidate.slice(0, candidate.length - ext.length);
232
+ for (const e of JS_TO_SRC[ext]) {
233
+ if (existsFileSync(base + e))
234
+ return base + e;
235
+ }
236
+ }
237
+ if (existsFileSync(candidate))
238
+ return candidate;
239
+ for (const e of SRC_EXTS)
240
+ if (existsFileSync(candidate + e))
241
+ return candidate + e;
242
+ for (const e of SRC_EXTS) {
243
+ const idx = path.join(candidate, `index${e}`);
244
+ if (existsFileSync(idx))
245
+ return idx;
246
+ }
247
+ return null;
248
+ }
249
+ /** Pick the source entry file of a package, preferring real source over dist. */
250
+ function packageEntry(pkgDir, pkg) {
251
+ const candidates = [];
252
+ // explicit source-ish fields first
253
+ for (const f of [pkg?.source, pkg?.module, pkg?.types, pkg?.typings, pkg?.main]) {
254
+ if (typeof f === "string")
255
+ candidates.push(f);
256
+ }
257
+ // exports["."] as a string or { source/import/default/types }
258
+ const dot = pkg?.exports?.["."] ?? pkg?.exports;
259
+ if (typeof dot === "string")
260
+ candidates.push(dot);
261
+ else if (dot && typeof dot === "object") {
262
+ for (const k of ["source", "import", "default", "types"]) {
263
+ if (typeof dot[k] === "string")
264
+ candidates.push(dot[k]);
265
+ }
266
+ }
267
+ // conventional source roots
268
+ candidates.push("src/index.ts", "src/index.tsx", "src/index.js", "index.ts", "index.tsx", "index.js");
269
+ for (const c of candidates) {
270
+ const resolved = resolveSourceFile(path.resolve(pkgDir, c));
271
+ if (resolved)
272
+ return resolved;
273
+ }
274
+ return null;
275
+ }
276
+ /**
277
+ * Resolve a bare import specifier to an in-monorepo source file when it targets
278
+ * a workspace package. Handles exact package imports (`@org/utils`) and subpaths
279
+ * (`@org/utils/helpers`). Returns null for non-workspace (external) specifiers.
280
+ */
281
+ export function resolveWorkspaceImport(importFrom, rootAbs, info) {
282
+ if (importFrom.startsWith("."))
283
+ return null; // relative, not a package import
284
+ let match = null;
285
+ let subpath = "";
286
+ for (const p of info.packages) {
287
+ if (importFrom === p.name) {
288
+ match = p;
289
+ subpath = "";
290
+ break;
291
+ }
292
+ if (importFrom.startsWith(p.name + "/")) {
293
+ match = p;
294
+ subpath = importFrom.slice(p.name.length + 1);
295
+ break;
296
+ }
297
+ }
298
+ if (!match)
299
+ return null;
300
+ const pkgDir = path.resolve(rootAbs, match.dir);
301
+ const pj = readJson(path.join(pkgDir, "package.json"));
302
+ const entry = packageEntry(pkgDir, pj);
303
+ if (!subpath)
304
+ return entry;
305
+ // Subpath import (`@org/utils/helpers`): resolve against the source root
306
+ // (the entry file's directory, typically `src/`) first, then the package dir.
307
+ const srcRoot = entry ? path.dirname(entry) : pkgDir;
308
+ return (resolveSourceFile(path.resolve(srcRoot, subpath)) ??
309
+ resolveSourceFile(path.resolve(pkgDir, subpath)));
310
+ }
311
+ /* ─── Cached per-root workspace index (for resolvers) ──────────────────────── */
312
+ const workspaceCache = new Map();
313
+ /** Discover (and cache) the workspace for a root, then resolve a package import. */
314
+ export function resolveWorkspaceImportCached(importFrom, rootAbs) {
315
+ if (importFrom.startsWith("."))
316
+ return null;
317
+ const key = path.resolve(rootAbs);
318
+ let info = workspaceCache.get(key);
319
+ if (!info) {
320
+ info = discoverWorkspace(key);
321
+ workspaceCache.set(key, info);
322
+ }
323
+ if (info.packages.length === 0)
324
+ return null;
325
+ return resolveWorkspaceImport(importFrom, key, info);
326
+ }
327
+ /** Test/debug hook: drop the cached workspace index. */
328
+ export function clearWorkspaceCache() {
329
+ workspaceCache.clear();
330
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-ast-mapper",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
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",
@@ -17,6 +17,7 @@
17
17
  "BLUEPRINT.md"
18
18
  ],
19
19
  "scripts": {
20
+ "prepare": "npm run build",
20
21
  "build": "tsc",
21
22
  "start": "node dist/index.js",
22
23
  "smoke": "node test/smoke.mjs",