universal-ast-mapper 1.13.0 → 1.19.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 CHANGED
@@ -6,6 +6,66 @@ since 1.0.0, guarantees a stable MCP tool / CLI surface across the 1.x line.
6
6
 
7
7
  ---
8
8
 
9
+ ## [1.19.0] — 2026-06-09 · Dashboard: coupling + SDP
10
+ - The health dashboard (`ast-map report` / `get_codebase_report`) now surfaces the
11
+ v1.14–1.16 architecture metrics: a **Module coupling** card (per-directory instability
12
+ bars with Ca/Ce) and a **Layer violations** card (stable→volatile SDP inversions),
13
+ plus an **SDP violations** stat tile.
14
+ - SDP violations now factor into the health score (small capped penalty), so a codebase
15
+ that systematically depends "uphill" on the stability gradient scores lower.
16
+ - `ReportData` gains `layerViolations` and `modules`; purely additive.
17
+ - Tests: 4 new assertions (131 total) — report carries the new data and the HTML renders
18
+ both cards.
19
+
20
+ ## [1.18.0] — 2026-06-09 · Vue & Svelte SFC support
21
+ - `.vue` and `.svelte` **single-file components** are now first-class inputs. The
22
+ `<script>` / `<script setup>` block is lifted out and parsed with the TS/JS extractor
23
+ (grammar chosen from `lang="ts"`), so component symbols and imports are extracted and
24
+ wired into the dependency graph — including edges from a component into a plain `.ts`
25
+ module, and into other components.
26
+ - Offsets are preserved: everything outside the script is blank-padded, so every symbol
27
+ range still points at the exact line/column in the original SFC.
28
+ - New languages `vue` and `svelte` (extensions `.vue`, `.svelte`); resolver now resolves
29
+ imports of `.vue` / `.svelte` files. **14 languages.**
30
+ - Tests: 8 new assertions (127 total) + Vue/Svelte fixtures — symbol extraction, import
31
+ capture, and cross-file graph edges for both.
32
+
33
+ ## [1.17.0] — 2026-06-09 · MCP prompts
34
+ - The server now registers **MCP prompts** — named, parameterized workflows a client
35
+ can invoke from its prompt/slash menu, each returning a ready-to-run instruction that
36
+ chains the right tools: `architecture_audit` (dir?), `safe_refactor` (file, symbol),
37
+ `dead_code_cleanup` (dir?), `health_check` (dir?), `onboard_codebase` (dir?).
38
+ - The Cookbook recipes become first-class, discoverable, and one call away — no pasting.
39
+ - New `test/prompts-smoke.mjs` (12 checks): `prompts/list` returns all 5, argument
40
+ interpolation works, and rendered prompts reference real tools. Wired into CI.
41
+
42
+ ## [1.16.0] — 2026-06-09 · Module coupling
43
+ - **`get_module_coupling`** + **`ast-map modules`** (alias `mods`): aggregates the
44
+ file-level import graph up to the **directory/module level** — per-module afferent
45
+ (Ca) / efferent (Ce) coupling and instability, plus the weighted inter-module edges.
46
+ Intra-module imports (files importing siblings in the same directory) are ignored;
47
+ only cross-module dependencies count. The architectural view above per-file coupling.
48
+ - Tests: 5 new assertions (119 total) — a three-module ui→api→core gradient with the
49
+ expected stability ordering and edge count.
50
+
51
+ ## [1.15.0] — 2026-06-09 · Layer-violation detection
52
+ - **`get_layer_violations`** + **`ast-map layers`** (alias `sdp`): detect violations of
53
+ Robert C. Martin's **Stable Dependencies Principle** — a stable file (low instability)
54
+ that imports a more volatile one (high instability). Such dependencies point "uphill"
55
+ on the stability gradient and drag stable code along every time the volatile file churns.
56
+ Sorted by severity (the instability gap crossed). `minGap` filters small gaps.
57
+ - Builds directly on the v1.14.0 coupling metrics.
58
+ - Tests: 5 new assertions (114 total) — clean fixture yields none, a synthetic
59
+ stable→volatile graph yields exactly one with the correct severity.
60
+
61
+ ## [1.14.0] — 2026-06-09 · Coupling metrics
62
+ - **`get_coupling`** + **`ast-map coupling [dir]`**: Robert C. Martin's per-file
63
+ coupling metrics — afferent coupling (Ca, fan-in), efferent coupling (Ce,
64
+ fan-out), and instability I = Ce/(Ca+Ce). High-Ca files are load-bearing (break
65
+ carefully); high-instability files change freely. Derived from the import graph.
66
+ - Tests: 4 new assertions (109 total) verifying stable/unstable/middle files and
67
+ the [0,1] instability bound.
68
+
9
69
  ## [1.13.0] — 2026-06-08 · Context-pack
10
70
  - **`pack_context`** + **`ast-map pack <file> [symbol]`**: the minimal context to
11
71
  work on a symbol — its source, the signatures it depends on, and its dependents
@@ -103,43 +163,4 @@ since 1.0.0, guarantees a stable MCP tool / CLI surface across the 1.x line.
103
163
  declared in 2+ files.
104
164
 
105
165
  ## [0.8.3] — 2026-05-31 · TSX/React component props
106
- - Component symbols carry `propsType` + `props[]`; detects `React.FC<P>` and
107
- JSX-returning PascalCase functions. MCP server version now read from package.json.
108
-
109
- ## [0.8.2] — 2026-05-30 · Swift cross-file wiring
110
- - `import <Module>` → that module's files (`Sources/<Module>/`). Completes
111
- cross-file graph/resolver support for all four v0.8.0 languages.
112
-
113
- ## [0.8.1] — 2026-05-30 · Kotlin + C/C++ cross-file wiring
114
- - Kotlin FQCN/package index; C/C++ `#include` resolution with header↔impl pairing.
115
- - Fixes: parse-cache rel-path leak; Kotlin call-graph extraction.
116
-
117
- ---
118
-
119
- ## Earlier (pre-session history)
120
-
121
- - **0.8.0** — +4 languages: C · C++ · Kotlin · Swift (symbol extraction + imports).
122
- - **0.7.0** — Go full module resolution; C# reverse `calledBy`; 4-suite test harness.
123
- - **0.6.0** — +3 languages: Rust · Java · C#; cross-language resolver.
124
- - **0.5.x** — `/ast-map` skill auto-install; iterative DFS; barrel re-exports; parse cache; call-graph aliases; `.ast-map.config.json`.
125
- - **0.4.0** — `search_symbol`, `get_file_deps`, `get_top_symbols`, dead-code tiers.
126
- - **0.3.0** — CLI; `find_dead_code`, `find_circular_deps`, `get_change_impact`, `get_call_graph`.
127
- - **0.2.0** — import extraction; `resolve_imports`; `build_symbol_graph`.
128
- - **0.1.0** — `get_skeleton_json`, `generate_skeleton`, `get_symbol_context`, `validate_architecture`.
129
-
130
- [1.13.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.13.0
131
- [1.12.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.12.0
132
- [1.11.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.11.0
133
- [1.10.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.10.0
134
- [1.9.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.9.0
135
- [1.8.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.8.0
136
- [1.7.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.7.0
137
- [1.6.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.6.0
138
- [1.5.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.5.0
139
- [1.4.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.4.0
140
- [1.3.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.3.0
141
- [1.2.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.2.0
142
- [1.1.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.1.0
143
- [1.0.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.0.0
144
- [0.9.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v0.9.0
145
- [0.8.1]: https://github.com/6ixthxense/AST-MCP/releases/tag/v0.8.1
166
+ - Component s
package/README.md CHANGED
@@ -4,9 +4,9 @@ An **MCP server + CLI tool** that turns source code into structured, machine-rea
4
4
 
5
5
  Built on [tree-sitter](https://tree-sitter.github.io/) WASM grammars. Zero regex guessing — real AST parsing.
6
6
 
7
- **24 MCP tools / 25 CLI commands** spanning skeletons, dependency graphs, and deep analysis — dead code, cycles, change-impact, complexity, duplicates, unused params, type-flow, decorators — plus monorepo support, an interactive **graph explorer** (`ast-map explore`), **watch mode**, and a one-page **health dashboard** (`ast-map report`).
7
+ **27 MCP tools / 28 CLI commands / 5 MCP prompts** spanning skeletons, dependency graphs, and deep analysis — dead code, cycles, change-impact, complexity, duplicates, unused params, type-flow, decorators — plus monorepo support, an interactive **graph explorer** (`ast-map explore`), **watch mode**, and a one-page **health dashboard** (`ast-map report`).
8
8
 
9
- **Supported languages:** TypeScript · TSX · JavaScript (ESM/CJS) · Python · Go · Rust · Java · C# · C · C++ · Kotlin · Swift
9
+ **Supported languages:** TypeScript · TSX · JavaScript (ESM/CJS) · Python · Go · Rust · Java · C# · C · C++ · Kotlin · Swift · **Vue** · **Svelte** (SFC `<script>`)
10
10
 
11
11
  | Capability | TS/JS | Python | Go | Rust | Java | C# | C | C++ | Kt | Swift |
12
12
  |--------------------------|:-----:|:------:|:---:|:----:|:----:|:---:|:---:|:---:|:---:|:-----:|
@@ -107,6 +107,9 @@ ast-map report [dir] [-o report.html]
107
107
  ast-map diff [base] [--dir <d>] # git-aware changed symbols + impact
108
108
  ast-map risk [dir] [-n N] # churn × complexity
109
109
  ast-map pack <file> [symbol] [--scan <d>] # minimal context pack
110
+ ast-map coupling [dir] [-n N] # Ca / Ce / instability per file
111
+ ast-map layers [dir] [-g gap] # SDP: stable→volatile violations
112
+ ast-map modules [dir] # directory-level coupling + edges
110
113
  ast-map search <pattern> [dir] [-m contains|exact|regex] [-k kind] [-e]
111
114
  ast-map deps <file> [--scan <dir>]
112
115
  ast-map top <dir> [-n 10]
@@ -365,7 +368,7 @@ Given a compiled JS/CSS file with an inline (`data:`) or external `sourceMapping
365
368
  ---
366
369
 
367
370
  ### `get_codebase_report`
368
- A one-shot **codebase health summary**: file/symbol counts, language breakdown, a health **grade (A–F)** + score, complexity hotspots, god nodes, dead exports, and circular dependencies. Rendered as a premium HTML dashboard by `ast-map report`.
371
+ A one-shot **codebase health summary**: file/symbol counts, language breakdown, a health **grade (A–F)** + score, complexity hotspots, god nodes, dead exports, circular dependencies, **module coupling** (per-directory instability), and **layer violations** (SDP). Rendered as a premium HTML dashboard by `ast-map report`.
369
372
 
370
373
  ```json
371
374
  { "grade": "B", "score": 82, "fileCount": 120, "symbolCount": 1400,
@@ -414,6 +417,42 @@ Rank files by **refactor risk = git churn × max complexity** — the files that
414
417
 
415
418
  ---
416
419
 
420
+ ### `get_coupling`
421
+ Per-file **coupling metrics** (Robert C. Martin): afferent coupling **Ca** (fan-in), efferent coupling **Ce** (fan-out), and **instability** I = Ce/(Ca+Ce) (0 = stable / load-bearing, 1 = unstable / volatile).
422
+
423
+ ```json
424
+ { "files": [ { "file": "src/types.ts", "afferent": 27, "efferent": 0, "instability": 0 } ] }
425
+ ```
426
+
427
+ **Params:** `path` (optional)
428
+
429
+ ---
430
+
431
+ ### `get_layer_violations`
432
+ Find dependencies that point **the wrong way on the stability gradient** — a stable file (low instability) importing a more volatile one (high instability), violating Martin's **Stable Dependencies Principle**. Sorted by severity (the instability gap crossed).
433
+
434
+ ```json
435
+ { "violations": [ { "from": "src/skeleton.ts", "to": "src/registry.ts", "fromInstability": 0.36, "toInstability": 0.6, "severity": 0.24 } ] }
436
+ ```
437
+
438
+ **Params:** `path` (optional), `minGap` (optional, 0–1)
439
+
440
+ ---
441
+
442
+ ### `get_module_coupling`
443
+ Aggregate the import graph up to the **directory/module level** — per-module Ca / Ce / instability plus the weighted inter-module edges. Intra-module imports are ignored, so this is the architectural bird's-eye view above per-file coupling.
444
+
445
+ ```json
446
+ {
447
+ "modules": [ { "module": "src/extractors", "files": 11, "afferent": 1, "efferent": 1, "instability": 0.5 } ],
448
+ "edges": [ { "from": "src/extractors", "to": "src", "weight": 75 } ]
449
+ }
450
+ ```
451
+
452
+ **Params:** `path` (optional)
453
+
454
+ ---
455
+
417
456
  ### `get_change_impact`
418
457
  Given a file + symbol, reverse-traverse the import graph to compute **blast radius**.
419
458
 
@@ -644,6 +683,20 @@ Beyond tools, the server exposes the codebase as **browseable MCP resources**, s
644
683
 
645
684
  ---
646
685
 
686
+ ## MCP prompts — one-call recipes
687
+
688
+ The server also registers **prompts**: named, parameterized workflows an MCP client can invoke directly (they show up in the client's prompt/slash menu). Each returns a ready-to-run instruction that chains the right tools, so you don't paste the recipe by hand.
689
+
690
+ | Prompt | Args | What it does |
691
+ |--------|------|--------------|
692
+ | `architecture_audit` | `dir?` | God Nodes → cycles → rule violations → module coupling → SDP breaks, then a prioritized summary |
693
+ | `safe_refactor` | `file`, `symbol` | blast radius → call graph → minimal context before changing a symbol |
694
+ | `dead_code_cleanup` | `dir?` | unused exports, each verified zero-impact before deletion |
695
+ | `health_check` | `dir?` | grade A–F → risk map → layer violations, with the 3 files to fix first |
696
+ | `onboard_codebase` | `dir?` | languages → structure → core symbols → module map, as a "start here" guide |
697
+
698
+ ---
699
+
647
700
  ## GitHub Action — architecture gate in CI
648
701
 
649
702
  Use AST-MCP as a CI check with the bundled composite action (`action.yml`):
@@ -687,6 +740,12 @@ Not part of the public API: the internal `src/` module layout and the generated
687
740
 
688
741
  | Version | What changed |
689
742
  |---------|--------------|
743
+ | **1.19.0** | **Dashboard: coupling + SDP** — `ast-map report` / `get_codebase_report` now include **module coupling** (per-directory instability bars) and **layer violations** (stable→volatile, SDP) cards, plus an SDP stat; SDP inversions also factor into the health score. The v1.14–1.16 metrics are now visual. |
744
+ | **1.18.0** | **Vue & Svelte SFC support** — `.vue` and `.svelte` single-file components are now parsed: the `<script>` / `<script setup>` block is lifted out (TS or JS) and its symbols + imports extracted, with cross-file graph edges into plain modules. Blank-padding keeps every symbol's line numbers pointing at the original SFC. **14 languages**. |
745
+ | **1.17.0** | **MCP prompts** — the server now registers 5 parameterized **prompts** (`architecture_audit`, `safe_refactor`, `dead_code_cleanup`, `health_check`, `onboard_codebase`): named workflows a client can invoke from its prompt menu, each chaining the right tools. The Cookbook recipes, one call away. |
746
+ | **1.16.0** | **Module coupling** — new `get_module_coupling` MCP tool + `ast-map modules` (alias `mods`) CLI: aggregates the import graph to the **directory/module level** — per-module Ca / Ce / instability plus weighted inter-module edges (intra-module imports ignored). The bird's-eye view above per-file coupling. **27 MCP tools**. |
747
+ | **1.15.0** | **Layer-violation detection** — new `get_layer_violations` MCP tool + `ast-map layers` (alias `sdp`) CLI: flags dependencies that break Martin's **Stable Dependencies Principle** — a stable file (low instability) importing a more volatile one — sorted by the instability gap. Builds directly on the coupling metrics. **26 MCP tools**. |
748
+ | **1.14.0** | **Coupling metrics** — new `get_coupling` MCP tool + `ast-map coupling [dir]` CLI: per-file afferent (Ca) / efferent (Ce) coupling and **instability** I = Ce/(Ca+Ce), the way to spot load-bearing files (high Ca) vs. volatile ones (high I). **25 MCP tools**. |
690
749
  | **1.13.0** | **Context-pack** — new `pack_context` MCP tool + `ast-map pack <file> [symbol]` CLI: the minimal context to work on a symbol (its source + the signatures it depends on + its dependents) with a token estimate, instead of reading whole files. **24 MCP tools**. |
691
750
  | **1.12.0** | **Git-aware analysis** — `ast-map diff [base]` + `get_diff` tool: changed symbols since a ref, **breaking changes** (removed / signature-changed exports), and blast radius. `ast-map risk` + `get_risk_map` tool: rank files by churn × complexity. Brings a time/history dimension. **23 MCP tools**. |
692
751
  | **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. |
@@ -709,19 +768,4 @@ Not part of the public API: the internal `src/` module layout and the generated
709
768
  | **0.9.0** | **Scoped type-flow tracing** — new `trace_type` MCP tool + `ast-map trace-type` (alias `flow`) CLI: follow a named type through function params, return types, typed variables, and class fields across a directory. Completes the deeper-analysis suite (dead code · cycles · impact · complexity · duplicates · unused params · type flow). **18 MCP tools**. |
710
769
  | **0.8.7** | **Python decorators in the call graph** — function/method symbols now carry a `decorators` field (`@router.get("/x")` → `router.get("/x")`), surfaced in skeletons (outline + full) and in `get_call_graph`. Traces framework wiring like FastAPI/Flask routes and `@staticmethod`/`@property` stacks to their handler. |
711
770
  | **0.8.6** | **Unused parameter detection** — new `find_unused_params` MCP tool + `ast-map unused-params` (alias `unused`) CLI: named functions whose params are never referenced. Skips `_`-prefixed/destructured/anonymous and treats object-shorthand as a use (low false-positive). Server now 17 tools. |
712
- | **0.8.5** | **Cyclomatic complexity** — new `get_complexity` MCP tool + `ast-map complexity` (alias `cx`, `--min N`) CLI: per-function AST-based complexity score (`1 + if/for/while/case/catch/ternary/&&/\|\|`) with low/moderate/high/very-high ratings and directory hotspots. Server now 16 tools. |
713
- | **0.8.4** | **Duplicate symbol detection** — new `find_duplicate_symbols` MCP tool + `ast-map duplicates` (alias `dupes`) CLI command: finds symbol names exported from more than one file, with every file/kind that declares each name. |
714
- | **0.8.3** | **TSX/React component props** — component symbols now carry extracted prop fields. PascalCase functions/arrows that return JSX or are typed `React.FC<P>`/`FC<P>` get `propsType` (named props type) + `props[]` (name, type, optional), resolved from same-file `interface`/`type` declarations or inline object types. Plus: MCP server now reports its real version from `package.json` (was hardcoded `0.5.3`). |
715
- | **0.8.2** | **Swift cross-file wiring** — `import <Module>` resolves to that module's files (module = the `Sources/<Module>/` directory, else parent dir), wired into `build_symbol_graph` + `resolve_imports`. System modules (Foundation, UIKit, …) stay external. Completes cross-file graph/resolver support for all four v0.8.0 languages. |
716
- | **0.8.1** | **Cross-file graph wiring for Kotlin & C/C++** — Kotlin FQCN/package index + C/C++ `#include` resolution (with header↔impl pairing) wired into `build_symbol_graph`, `resolve_imports`, and `get_call_graph`. Fixes a parse-cache rel-path leak (stale `.file` poisoned the cross-lang index → doubled paths) and Kotlin call-graph extraction (`function_declaration` name + field-less `call_expression`). |
717
- | **0.8.0** | **4 new languages: C · C++ · Kotlin · Swift** — symbol extraction + imports parsing. C++ tracks access_specifier through class bodies. Kotlin handles `package`/`object`/`data class`. Swift handles `class`/`struct`/`enum` (all under `class_declaration`) and `protocol_declaration`. Ruby grammar in tree-sitter-wasms@0.1.13 is unstable — skipped. |
718
- | **0.7.0** | Go full resolution (reads `go.mod`, resolves package-as-directory) · C# reverse `calledBy` via call-site scanning · `csharpTypes` index lets `using` directives resolve to specific types · 4-suite test harness (smoke + graph-smoke + resolver-smoke + callgraph-smoke) |
719
- | **0.6.0** | **3 new languages: Rust · Java · C#** (extractors + import parsing) · cross-language resolver in `crosslang.ts` (Java FQCN index, C# namespace index, Rust `crate::` module walk) · symbol-graph `imports` edges + `resolveFileImports` enrichment + `get_call_graph` callee resolution rewired through it · Java `package` and C# `namespace` captured as directives |
720
- | **0.5.3** | Auto-install `/ast-map` Claude Code skill on `npm install` · `postinstall` writes `~/.claude/skills/ast-map/SKILL.md` + registers trigger in `CLAUDE.md` (idempotent, CI-safe) |
721
- | **0.5.2** | Iterative DFS in `findCircularDeps` (eliminates stack overflow on large codebases) · `build_symbol_graph` inline size guard (>2000 nodes → stats + warning) · integration test suite (`test/analysis.mjs`) |
722
- | **0.5.1** | Re-export tracking (`export { X } from './foo'`, barrel files) · `export const` surfaced as symbols · `const X = class {}` support · Python relative import fix · parser instance cache |
723
- | **0.5.0** | Call graph destructuring aliases · in-process parse cache · `.ast-map.config.json` · general validation rules (large-file, too-many-imports, god-export) |
724
- | **0.4.0** | `search_symbol` · `get_file_deps` · `get_top_symbols` · dead code confidence tiers · 3 new CLI commands |
725
- | **0.3.0** | `ast-map` CLI · `find_dead_code` · `find_circular_deps` · `get_change_impact` · `get_call_graph` |
726
- | **0.2.0** | Import extraction · `resolve_imports` · `build_symbol_graph` |
727
- | **0.1.0** | `get_skeleton_json` · `generate_skeleton` · `get_symbol_context` · `validate_architecture` |
771
+ | **0.8.5** | **Cyclomatic complexity** — new `get_complexity` MCP tool + `ast-map complex
package/dist/cli.js CHANGED
@@ -19,6 +19,9 @@ import { readSourceMap } from "./sourcemap.js";
19
19
  import { buildReport, buildReportHtml } from "./report.js";
20
20
  import { computeDiff, computeRisk, isGitRepo } from "./gitdiff.js";
21
21
  import { packContext } from "./contextpack.js";
22
+ import { computeCoupling } from "./coupling.js";
23
+ import { findLayerViolations } from "./layers.js";
24
+ import { computeModuleCoupling } from "./modulecoupling.js";
22
25
  import { buildCallGraph } from "./callgraph.js";
23
26
  import { searchSymbols } from "./search.js";
24
27
  const ROOT = path.resolve(process.env.AST_MAP_ROOT ?? process.cwd());
@@ -543,6 +546,89 @@ program
543
546
  table(files.slice(0, opts.top).map((f) => [String(f.risk), `${f.churn} × ${f.maxComplexity}`, f.file]), [["Risk", 7], ["churn×cx", 12], ["File", 44]]);
544
547
  console.log();
545
548
  });
549
+ // ─── Command: coupling ────────────────────────────────────────────────────────
550
+ program
551
+ .command("coupling [dir]")
552
+ .description("Per-file coupling metrics: afferent (Ca), efferent (Ce), instability")
553
+ .option("--json", "Output as JSON")
554
+ .option("-n, --top <n>", "Show top N by total coupling", (v) => parseInt(v, 10), 25)
555
+ .action(async (dir, opts) => {
556
+ const { abs, rel } = resolveArg(dir ?? ".");
557
+ if (!fs.statSync(abs).isDirectory())
558
+ die(`"${rel}" is not a directory`);
559
+ const skeletons = await gatherSkeletons(abs);
560
+ const metrics = computeCoupling(buildSymbolGraph(skeletons, ROOT));
561
+ if (opts.json)
562
+ return jsonOut({ count: metrics.length, files: metrics });
563
+ header(`Coupling \u2014 ${rel}/ ${dim("(Ca = fan-in, Ce = fan-out, I = instability)")}`);
564
+ if (metrics.length === 0) {
565
+ console.log(indent(dim("No import edges found.")));
566
+ console.log();
567
+ return;
568
+ }
569
+ const icolor = (i) => (i >= 0.8 ? red : i <= 0.2 ? green : yellow);
570
+ table(metrics.slice(0, opts.top).map((m) => [String(m.afferent), String(m.efferent), icolor(m.instability)(m.instability.toFixed(2)), m.file]), [["Ca", 4], ["Ce", 4], ["I", 6], ["File", 46]]);
571
+ console.log(indent(dim("high Ca = load-bearing (break carefully) · high I = volatile")));
572
+ console.log();
573
+ });
574
+ // ─── Command: layers ──────────────────────────────────────────────────────────
575
+ program
576
+ .command("layers [dir]")
577
+ .alias("sdp")
578
+ .description("Stable Dependencies Principle: stable files that depend on volatile ones")
579
+ .option("--json", "Output as JSON")
580
+ .option("-g, --min-gap <n>", "Only show violations with instability gap > n", (v) => parseFloat(v), 0)
581
+ .action(async (dir, opts) => {
582
+ const { abs, rel } = resolveArg(dir ?? ".");
583
+ if (!fs.statSync(abs).isDirectory())
584
+ die(`"${rel}" is not a directory`);
585
+ const skeletons = await gatherSkeletons(abs);
586
+ const violations = findLayerViolations(buildSymbolGraph(skeletons, ROOT), opts.minGap);
587
+ if (opts.json)
588
+ return jsonOut({ count: violations.length, violations });
589
+ header(`Layer Violations \u2014 ${rel}/ ${dim("(stable \u2192 volatile dependencies, SDP)")}`);
590
+ if (violations.length === 0) {
591
+ console.log(indent(green("\u2713 No SDP violations \u2014 dependencies flow toward stability.")));
592
+ console.log();
593
+ return;
594
+ }
595
+ for (const v of violations) {
596
+ const sev = v.severity >= 0.4 ? red : v.severity >= 0.2 ? yellow : dim;
597
+ console.log(indent(`${sev(v.severity.toFixed(2))} ${bold(v.from)} ${dim(`(I=${v.fromInstability})`)} ${red("\u2192")} ${v.to} ${dim(`(I=${v.toInstability})`)}`));
598
+ }
599
+ console.log();
600
+ console.log(indent(dim(`${violations.length} stable file(s) depend on more volatile ones \u2014 they churn when those do`)));
601
+ console.log();
602
+ });
603
+ // ─── Command: modules ─────────────────────────────────────────────────────────
604
+ program
605
+ .command("modules [dir]")
606
+ .alias("mods")
607
+ .description("Directory/module-level coupling: per-module Ca / Ce / instability + edges")
608
+ .option("--json", "Output as JSON")
609
+ .action(async (dir, opts) => {
610
+ const { abs, rel } = resolveArg(dir ?? ".");
611
+ if (!fs.statSync(abs).isDirectory())
612
+ die(`"${rel}" is not a directory`);
613
+ const skeletons = await gatherSkeletons(abs);
614
+ const mc = computeModuleCoupling(buildSymbolGraph(skeletons, ROOT));
615
+ if (opts.json)
616
+ return jsonOut(mc);
617
+ header(`Module Coupling \u2014 ${rel}/ ${dim("(directory-level Ca / Ce / instability)")}`);
618
+ if (mc.modules.length === 0) {
619
+ console.log(indent(dim("No cross-module imports found.")));
620
+ console.log();
621
+ return;
622
+ }
623
+ const icolor = (i) => (i >= 0.8 ? red : i <= 0.2 ? green : yellow);
624
+ table(mc.modules.map((m) => [String(m.files), String(m.afferent), String(m.efferent), icolor(m.instability)(m.instability.toFixed(2)), m.module]), [["Files", 6], ["Ca", 4], ["Ce", 4], ["I", 6], ["Module", 40]]);
625
+ if (mc.edges.length) {
626
+ console.log(indent(bold("Inter-module dependencies:")));
627
+ for (const e of mc.edges.slice(0, 20))
628
+ console.log(indent(` ${e.from} ${dim("\u2192")} ${e.to} ${dim(`(${e.weight})`)}`));
629
+ }
630
+ console.log();
631
+ });
546
632
  // ─── Command: report ──────────────────────────────────────────────────────────
547
633
  program
548
634
  .command("report [dir]")
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Compute Robert C. Martin's coupling metrics per file from the symbol graph's
3
+ * file-level import edges: afferent (fan-in), efferent (fan-out), and instability.
4
+ */
5
+ export function computeCoupling(graph) {
6
+ const nodeMap = new Map(graph.nodes.map((n) => [n.id, n]));
7
+ const out = new Map(); // file -> set of files it imports
8
+ const inc = new Map(); // file -> set of files that import it
9
+ const files = new Set();
10
+ for (const n of graph.nodes)
11
+ if (n.nodeType === "file")
12
+ files.add(n.id);
13
+ for (const e of graph.edges) {
14
+ if (e.edgeType !== "imports")
15
+ continue;
16
+ const to = nodeMap.get(e.to);
17
+ const toFile = to ? (to.nodeType === "file" ? to.id : to.file) : null;
18
+ const fromFile = e.from;
19
+ if (!toFile || fromFile === toFile)
20
+ continue;
21
+ files.add(fromFile);
22
+ files.add(toFile);
23
+ (out.get(fromFile) ?? out.set(fromFile, new Set()).get(fromFile)).add(toFile);
24
+ (inc.get(toFile) ?? inc.set(toFile, new Set()).get(toFile)).add(fromFile);
25
+ }
26
+ const metrics = [];
27
+ for (const f of files) {
28
+ const ce = out.get(f)?.size ?? 0;
29
+ const ca = inc.get(f)?.size ?? 0;
30
+ const instability = ca + ce === 0 ? 0 : Math.round((ce / (ca + ce)) * 100) / 100;
31
+ metrics.push({ file: f, afferent: ca, efferent: ce, instability });
32
+ }
33
+ // Sort by total coupling desc (most-connected first).
34
+ return metrics.sort((a, b) => (b.afferent + b.efferent) - (a.afferent + a.efferent) || a.file.localeCompare(b.file));
35
+ }
package/dist/graph.js CHANGED
@@ -27,7 +27,9 @@ function isPathBasedLanguage(language) {
27
27
  return (language === "typescript" ||
28
28
  language === "tsx" ||
29
29
  language === "javascript" ||
30
- language === "python");
30
+ language === "python" ||
31
+ language === "vue" ||
32
+ language === "svelte");
31
33
  }
32
34
  // Wire one TS/JS/Python-style relative import.
33
35
  function wirePathImport(skel, imp, fromFileAbs, root, exportedSymbolMap, edges) {
package/dist/index.js CHANGED
@@ -23,6 +23,10 @@ import { readSourceMap } from "./sourcemap.js";
23
23
  import { buildReport } from "./report.js";
24
24
  import { computeDiff, computeRisk, isGitRepo } from "./gitdiff.js";
25
25
  import { packContext } from "./contextpack.js";
26
+ import { computeCoupling } from "./coupling.js";
27
+ import { findLayerViolations } from "./layers.js";
28
+ import { computeModuleCoupling } from "./modulecoupling.js";
29
+ import { registerPrompts } from "./prompts.js";
26
30
  /** Files may only be read inside this root (override with AST_MAP_ROOT). */
27
31
  const ROOT = path.resolve(process.env.AST_MAP_ROOT ?? process.cwd());
28
32
  function resolveInRoot(input) {
@@ -67,6 +71,7 @@ const server = new McpServer({
67
71
  name: "universal-ast-mapper",
68
72
  version: PKG_VERSION,
69
73
  });
74
+ registerPrompts(server);
70
75
  /* ----------------------- tool: list_supported_languages ----------------------- */
71
76
  server.registerTool("list_supported_languages", {
72
77
  title: "List supported languages",
@@ -859,6 +864,105 @@ server.registerTool("pack_context", {
859
864
  return errorText(describeError(err));
860
865
  }
861
866
  });
867
+ /* ─────────────────── tool: get_coupling ────────────────────────────────── */
868
+ server.registerTool("get_coupling", {
869
+ title: "Coupling metrics (afferent / efferent / instability)",
870
+ description: "Compute Robert C. Martin's coupling metrics per file from the import graph: afferent coupling " +
871
+ "(Ca, fan-in), efferent coupling (Ce, fan-out), and instability I = Ce/(Ca+Ce) (0 = stable, " +
872
+ "1 = unstable). High-Ca files are load-bearing; high-instability files change freely.",
873
+ inputSchema: {
874
+ path: z.string().optional().describe("Directory to scan. Default project root."),
875
+ },
876
+ }, async ({ path: input }) => {
877
+ try {
878
+ const { abs, rel } = resolveInRoot(input ?? ".");
879
+ if (!fs.statSync(abs).isDirectory()) {
880
+ return errorText(`"${input}" is not a directory. get_coupling requires a directory.`);
881
+ }
882
+ const opts = resolveOptions({ detail: "outline", emitHtml: false });
883
+ const files = collectSourceFiles(abs, opts);
884
+ const skels = [];
885
+ for (const f of files) {
886
+ const r = path.relative(ROOT, f).split(path.sep).join("/");
887
+ try {
888
+ skels.push(await buildSkeleton(f, r, opts));
889
+ }
890
+ catch { /* skip */ }
891
+ }
892
+ const metrics = computeCoupling(buildSymbolGraph(skels, ROOT));
893
+ return jsonText({ directory: rel.split(path.sep).join("/") || ".", count: metrics.length, files: metrics });
894
+ }
895
+ catch (err) {
896
+ return errorText(describeError(err));
897
+ }
898
+ });
899
+ /* ─────────────────── tool: get_layer_violations ────────────────────────── */
900
+ server.registerTool("get_layer_violations", {
901
+ title: "Layer violations (Stable Dependencies Principle)",
902
+ description: "Find dependencies that point the wrong way on the stability gradient: a stable file " +
903
+ "(low instability) that imports a more volatile file (high instability). Per Robert C. Martin's " +
904
+ "Stable Dependencies Principle, stable code should not depend on volatile code — it gets dragged " +
905
+ "along every time the volatile file churns. Results are sorted by severity (the instability gap).",
906
+ inputSchema: {
907
+ path: z.string().optional().describe("Directory to scan. Default project root."),
908
+ minGap: z.number().optional().describe("Only report violations whose instability gap exceeds this (0-1). Default 0."),
909
+ },
910
+ }, async ({ path: input, minGap }) => {
911
+ try {
912
+ const { abs, rel } = resolveInRoot(input ?? ".");
913
+ if (!fs.statSync(abs).isDirectory()) {
914
+ return errorText(`"${input}" is not a directory. get_layer_violations requires a directory.`);
915
+ }
916
+ const opts = resolveOptions({ detail: "outline", emitHtml: false });
917
+ const files = collectSourceFiles(abs, opts);
918
+ const skels = [];
919
+ for (const f of files) {
920
+ const r = path.relative(ROOT, f).split(path.sep).join("/");
921
+ try {
922
+ skels.push(await buildSkeleton(f, r, opts));
923
+ }
924
+ catch { /* skip */ }
925
+ }
926
+ const violations = findLayerViolations(buildSymbolGraph(skels, ROOT), minGap ?? 0);
927
+ return jsonText({ directory: rel.split(path.sep).join("/") || ".", count: violations.length, violations });
928
+ }
929
+ catch (err) {
930
+ return errorText(describeError(err));
931
+ }
932
+ });
933
+ /* ─────────────────── tool: get_module_coupling ─────────────────────────── */
934
+ server.registerTool("get_module_coupling", {
935
+ title: "Module coupling (directory-level Ca / Ce / instability)",
936
+ description: "Aggregate the import graph up to the directory/module level: per-module afferent (Ca) / " +
937
+ "efferent (Ce) coupling and instability, plus the weighted inter-module edges. Intra-module " +
938
+ "imports (files importing siblings in the same directory) are ignored — only cross-module " +
939
+ "dependencies count. The architectural bird's-eye view above per-file coupling.",
940
+ inputSchema: {
941
+ path: z.string().optional().describe("Directory to scan. Default project root."),
942
+ },
943
+ }, async ({ path: input }) => {
944
+ try {
945
+ const { abs, rel } = resolveInRoot(input ?? ".");
946
+ if (!fs.statSync(abs).isDirectory()) {
947
+ return errorText(`"${input}" is not a directory. get_module_coupling requires a directory.`);
948
+ }
949
+ const opts = resolveOptions({ detail: "outline", emitHtml: false });
950
+ const files = collectSourceFiles(abs, opts);
951
+ const skels = [];
952
+ for (const f of files) {
953
+ const r = path.relative(ROOT, f).split(path.sep).join("/");
954
+ try {
955
+ skels.push(await buildSkeleton(f, r, opts));
956
+ }
957
+ catch { /* skip */ }
958
+ }
959
+ const mc = computeModuleCoupling(buildSymbolGraph(skels, ROOT));
960
+ return jsonText({ directory: rel.split(path.sep).join("/") || ".", moduleCount: mc.modules.length, ...mc });
961
+ }
962
+ catch (err) {
963
+ return errorText(describeError(err));
964
+ }
965
+ });
862
966
  /* ─────────────────── tool: get_change_impact ───────────────────────────── */
863
967
  server.registerTool("get_change_impact", {
864
968
  title: "Get change impact (blast radius)",
package/dist/layers.js ADDED
@@ -0,0 +1,36 @@
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
@@ -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
+ }
package/dist/registry.js CHANGED
@@ -16,6 +16,14 @@ const TS_ENTRY = (language, grammar) => ({
16
16
  extractDirectives: extractDirectivesTS,
17
17
  extractImports: extractImportsTS,
18
18
  });
19
+ const SFC_ENTRY = (language) => ({
20
+ language,
21
+ grammar: "typescript", // overridden per-file after detecting lang="ts" vs js
22
+ extract: extractTypeScript,
23
+ extractDirectives: extractDirectivesTS,
24
+ extractImports: extractImportsTS,
25
+ sfc: true,
26
+ });
19
27
  const BY_EXT = {
20
28
  ".ts": TS_ENTRY("typescript", "typescript"),
21
29
  ".mts": TS_ENTRY("typescript", "typescript"),
@@ -54,6 +62,8 @@ const BY_EXT = {
54
62
  extract: extractKotlin, extractDirectives: extractDirectivesKotlin, extractImports: extractImportsKotlin,
55
63
  },
56
64
  ".swift": { language: "swift", grammar: "swift", extract: extractSwift, extractImports: extractImportsSwift },
65
+ ".vue": SFC_ENTRY("vue"),
66
+ ".svelte": SFC_ENTRY("svelte"),
57
67
  };
58
68
  export function detectLanguage(filePath) {
59
69
  return BY_EXT[path.extname(filePath).toLowerCase()] ?? null;
package/dist/report.js CHANGED
@@ -4,6 +4,8 @@ import { resolveOptions } from "./config.js";
4
4
  import { buildSymbolGraph } from "./graph.js";
5
5
  import { findDeadExports, findCircularDeps, getTopSymbols } from "./graph-analysis.js";
6
6
  import { computeFileComplexity } from "./complexity.js";
7
+ import { findLayerViolations } from "./layers.js";
8
+ import { computeModuleCoupling } from "./modulecoupling.js";
7
9
  function gradeFor(score) {
8
10
  if (score >= 90)
9
11
  return "A";
@@ -46,6 +48,8 @@ export async function buildReport(absDir, root) {
46
48
  const dead = findDeadExports(graph).filter((d) => d.confidence === "high");
47
49
  const cycles = findCircularDeps(graph);
48
50
  const god = getTopSymbols(graph, 8);
51
+ const layerViolations = findLayerViolations(graph);
52
+ const modules = computeModuleCoupling(graph).modules;
49
53
  hotspots.sort((a, b) => b.complexity - a.complexity);
50
54
  const veryHigh = hotspots.filter((f) => f.complexity > 20).length;
51
55
  const high = hotspots.filter((f) => f.complexity > 10 && f.complexity <= 20).length;
@@ -55,6 +59,7 @@ export async function buildReport(absDir, root) {
55
59
  score -= Math.min(22, cycles.length * 6);
56
60
  score -= Math.min(28, veryHigh * 4 + high * 1);
57
61
  score -= Math.min(12, god.filter((g) => g.importCount >= 8).length * 4);
62
+ score -= Math.min(10, layerViolations.length);
58
63
  score = Math.max(0, Math.round(score));
59
64
  const languages = [...langCount.entries()]
60
65
  .map(([lang, f]) => ({ lang, files: f }))
@@ -72,6 +77,8 @@ export async function buildReport(absDir, root) {
72
77
  cycles: { count: cycles.length, items: cycles.slice(0, 12).map((c) => c.cycle) },
73
78
  godNodes: god.map((g) => ({ symbol: g.symbol, file: g.file, importCount: g.importCount })),
74
79
  complexity: { average: cxN ? Math.round((cxSum / cxN) * 10) / 10 : 0, max: cxMax, hotspots: hotspots.slice(0, 12) },
80
+ layerViolations: { count: layerViolations.length, items: layerViolations.slice(0, 12) },
81
+ modules: modules.slice(0, 10),
75
82
  };
76
83
  }
77
84
  /* ─── Premium HTML dashboard ───────────────────────────────────────────────── */
@@ -84,6 +91,9 @@ function esc(s) {
84
91
  function ratingColor(r) {
85
92
  return r === "very-high" ? "#e24b4a" : r === "high" ? "#d85a30" : r === "moderate" ? "#ba7517" : "#1d9e75";
86
93
  }
94
+ function instColor(i) {
95
+ return i >= 0.8 ? "#e24b4a" : i <= 0.2 ? "#1d9e75" : "#ba7517";
96
+ }
87
97
  function statCard(label, value, accent) {
88
98
  return `<div class="stat"><div class="sv"${accent ? ` style="color:${accent}"` : ""}>${value}</div><div class="sl">${label}</div></div>`;
89
99
  }
@@ -109,6 +119,13 @@ export function buildReportHtml(d) {
109
119
  const cycles = d.cycles.count
110
120
  ? d.cycles.items.map((c) => `<div class="li"><span class="mono">${esc(c.join(" → "))}</span></div>`).join("")
111
121
  : `<div class="ok">✓ No circular dependencies</div>`;
122
+ const modules = d.modules.length
123
+ ? 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("")
124
+ : `<div class="empty">No cross-module imports.</div>`;
125
+ const sdp = d.layerViolations.count
126
+ ? 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("")
127
+ + (d.layerViolations.count > d.layerViolations.items.length ? `<div class="more">+${d.layerViolations.count - d.layerViolations.items.length} more…</div>` : "")
128
+ : `<div class="ok">✓ No stability inversions (SDP)</div>`;
112
129
  return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
113
130
  <title>${esc(d.project)} — code health</title><style>
114
131
  :root{--bg:#fafaf8;--card:#fff;--bd:#e7e5df;--tx:#2b2b28;--dim:#8a8880;--soft:#f1efe9}
@@ -149,6 +166,7 @@ export function buildReportHtml(d) {
149
166
  ${statCard("Max complexity", d.complexity.max, ratingColor(d.complexity.max > 20 ? "very-high" : d.complexity.max > 10 ? "high" : "low"))}
150
167
  ${statCard("Dead exports", d.dead.count, d.dead.count ? "#d85a30" : "#1d9e75")}
151
168
  ${statCard("Cycles", d.cycles.count, d.cycles.count ? "#e24b4a" : "#1d9e75")}
169
+ ${statCard("SDP violations", d.layerViolations.count, d.layerViolations.count ? "#d85a30" : "#1d9e75")}
152
170
  </div>
153
171
  <div class="card"><h2>Language breakdown</h2>${langs}</div>
154
172
  <div class="card"><h2>Complexity hotspots</h2>${hotspots}</div>
@@ -156,6 +174,10 @@ export function buildReportHtml(d) {
156
174
  <div class="card"><h2>God nodes (most imported)</h2>${god}</div>
157
175
  <div class="card"><h2>Circular dependencies</h2>${cycles}</div>
158
176
  </div>
177
+ <div class="two">
178
+ <div class="card"><h2>Module coupling (instability)</h2>${modules}</div>
179
+ <div class="card"><h2>Layer violations (stable → volatile)</h2>${sdp}</div>
180
+ </div>
159
181
  <div class="card"><h2>Dead exports (high confidence)</h2>${dead}</div>
160
182
  <div class="foot">Generated by AST-MCP · universal-ast-mapper</div>
161
183
  </div></body></html>`;
package/dist/resolver.js CHANGED
@@ -5,7 +5,7 @@ import { resolveOptions } from "./config.js";
5
5
  import { findSymbol } from "./analysis.js";
6
6
  import { buildCrossLangIndex, resolveCrossLangTarget, } from "./crosslang.js";
7
7
  import { resolveWorkspaceImportCached } from "./workspace.js";
8
- const SRC_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"];
8
+ const SRC_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs", ".vue", ".svelte"];
9
9
  function extractParams(sig) {
10
10
  const start = sig.indexOf("(");
11
11
  if (start === -1)
package/dist/sfc.js ADDED
@@ -0,0 +1,27 @@
1
+ const SCRIPT_RE = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
2
+ export function isSfcExt(ext) {
3
+ return ext === ".vue" || ext === ".svelte";
4
+ }
5
+ export function extractScript(source) {
6
+ // Start from an all-blank canvas the same shape as the source (newlines kept,
7
+ // every other character turned into a space) to preserve line/column offsets.
8
+ let out = source.replace(/[^\n]/g, " ");
9
+ let hasScript = false;
10
+ let lang = "js";
11
+ let m;
12
+ SCRIPT_RE.lastIndex = 0;
13
+ while ((m = SCRIPT_RE.exec(source)) !== null) {
14
+ hasScript = true;
15
+ const attrs = m[1] ?? "";
16
+ if (/lang\s*=\s*["'](ts|typescript)["']/i.test(attrs))
17
+ lang = "ts";
18
+ const inner = m[2] ?? "";
19
+ const innerStart = m.index + m[0].indexOf(inner, m[1] ? m[1].length : 0);
20
+ out = out.slice(0, innerStart) + inner + out.slice(innerStart + inner.length);
21
+ }
22
+ return {
23
+ code: hasScript ? out : "",
24
+ grammar: lang === "ts" ? "typescript" : "javascript",
25
+ hasScript,
26
+ };
27
+ }
package/dist/skeleton.js CHANGED
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { detectLanguage, supportedExtensions } from "./registry.js";
4
4
  import { parseSource } from "./parser.js";
5
5
  import { countSymbols, toOutline } from "./extractors/common.js";
6
+ import { extractScript } from "./sfc.js";
6
7
  export const SCHEMA_VERSION = "1.1";
7
8
  export const GRAMMAR_SOURCE = "tree-sitter-wasms@0.1.13";
8
9
  const parseCache = new Map();
@@ -57,8 +58,14 @@ export async function buildSkeleton(absPath, relPath, opts) {
57
58
  const wantFile = relPath.split(path.sep).join("/");
58
59
  return cached.file === wantFile ? cached : { ...cached, file: wantFile };
59
60
  }
60
- const source = fs.readFileSync(absPath, "utf8");
61
- const root = await parseSource(entry.grammar, source);
61
+ let source = fs.readFileSync(absPath, "utf8");
62
+ let grammar = entry.grammar;
63
+ if (entry.sfc) {
64
+ const script = extractScript(source);
65
+ source = script.code; // blank-padded script-only source (offsets preserved)
66
+ grammar = script.grammar;
67
+ }
68
+ const root = await parseSource(grammar, source);
62
69
  let symbols = entry.extract(root, source);
63
70
  if (opts.detail === "outline")
64
71
  symbols = toOutline(symbols);
@@ -69,7 +76,7 @@ export async function buildSkeleton(absPath, relPath, opts) {
69
76
  file: relPath.split(path.sep).join("/"),
70
77
  language: entry.language,
71
78
  generatedAt: new Date().toISOString(),
72
- parser: { engine: "tree-sitter", grammar: `${entry.grammar} (${GRAMMAR_SOURCE})` },
79
+ parser: { engine: "tree-sitter", grammar: `${grammar} (${GRAMMAR_SOURCE})` },
73
80
  symbolCount: countSymbols(symbols),
74
81
  ...(directives.length > 0 ? { directives } : {}),
75
82
  ...(imports.length > 0 ? { imports } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-ast-mapper",
3
- "version": "1.13.0",
3
+ "version": "1.19.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",