universal-ast-mapper 1.11.0 → 1.18.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,65 @@ since 1.0.0, guarantees a stable MCP tool / CLI surface across the 1.x line.
6
6
 
7
7
  ---
8
8
 
9
+ ## [1.18.0] — 2026-06-09 · Vue & Svelte SFC support
10
+ - `.vue` and `.svelte` **single-file components** are now first-class inputs. The
11
+ `<script>` / `<script setup>` block is lifted out and parsed with the TS/JS extractor
12
+ (grammar chosen from `lang="ts"`), so component symbols and imports are extracted and
13
+ wired into the dependency graph — including edges from a component into a plain `.ts`
14
+ module, and into other components.
15
+ - Offsets are preserved: everything outside the script is blank-padded, so every symbol
16
+ range still points at the exact line/column in the original SFC.
17
+ - New languages `vue` and `svelte` (extensions `.vue`, `.svelte`); resolver now resolves
18
+ imports of `.vue` / `.svelte` files. **14 languages.**
19
+ - Tests: 8 new assertions (127 total) + Vue/Svelte fixtures — symbol extraction, import
20
+ capture, and cross-file graph edges for both.
21
+
22
+ ## [1.17.0] — 2026-06-09 · MCP prompts
23
+ - The server now registers **MCP prompts** — named, parameterized workflows a client
24
+ can invoke from its prompt/slash menu, each returning a ready-to-run instruction that
25
+ chains the right tools: `architecture_audit` (dir?), `safe_refactor` (file, symbol),
26
+ `dead_code_cleanup` (dir?), `health_check` (dir?), `onboard_codebase` (dir?).
27
+ - The Cookbook recipes become first-class, discoverable, and one call away — no pasting.
28
+ - New `test/prompts-smoke.mjs` (12 checks): `prompts/list` returns all 5, argument
29
+ interpolation works, and rendered prompts reference real tools. Wired into CI.
30
+
31
+ ## [1.16.0] — 2026-06-09 · Module coupling
32
+ - **`get_module_coupling`** + **`ast-map modules`** (alias `mods`): aggregates the
33
+ file-level import graph up to the **directory/module level** — per-module afferent
34
+ (Ca) / efferent (Ce) coupling and instability, plus the weighted inter-module edges.
35
+ Intra-module imports (files importing siblings in the same directory) are ignored;
36
+ only cross-module dependencies count. The architectural view above per-file coupling.
37
+ - Tests: 5 new assertions (119 total) — a three-module ui→api→core gradient with the
38
+ expected stability ordering and edge count.
39
+
40
+ ## [1.15.0] — 2026-06-09 · Layer-violation detection
41
+ - **`get_layer_violations`** + **`ast-map layers`** (alias `sdp`): detect violations of
42
+ Robert C. Martin's **Stable Dependencies Principle** — a stable file (low instability)
43
+ that imports a more volatile one (high instability). Such dependencies point "uphill"
44
+ on the stability gradient and drag stable code along every time the volatile file churns.
45
+ Sorted by severity (the instability gap crossed). `minGap` filters small gaps.
46
+ - Builds directly on the v1.14.0 coupling metrics.
47
+ - Tests: 5 new assertions (114 total) — clean fixture yields none, a synthetic
48
+ stable→volatile graph yields exactly one with the correct severity.
49
+
50
+ ## [1.14.0] — 2026-06-09 · Coupling metrics
51
+ - **`get_coupling`** + **`ast-map coupling [dir]`**: Robert C. Martin's per-file
52
+ coupling metrics — afferent coupling (Ca, fan-in), efferent coupling (Ce,
53
+ fan-out), and instability I = Ce/(Ca+Ce). High-Ca files are load-bearing (break
54
+ carefully); high-instability files change freely. Derived from the import graph.
55
+ - Tests: 4 new assertions (109 total) verifying stable/unstable/middle files and
56
+ the [0,1] instability bound.
57
+
58
+ ## [1.13.0] — 2026-06-08 · Context-pack
59
+ - **`pack_context`** + **`ast-map pack <file> [symbol]`**: the minimal context to
60
+ work on a symbol — its source, the signatures it depends on, and its dependents
61
+ — with a token estimate, instead of reading whole files.
62
+
63
+ ## [1.12.0] — 2026-06-08 · Git-aware analysis
64
+ - **`ast-map diff [base]`** + **`get_diff`**: changed symbols since a git ref,
65
+ breaking changes (removed / signature-changed exports), and blast radius.
66
+ - **`ast-map risk`** + **`get_risk_map`**: rank files by churn × complexity.
67
+
9
68
  ## [1.11.0] — 2026-06-01 · Code-health dashboard
10
69
  - **`ast-map report`** writes a premium self-contained HTML dashboard: health
11
70
  grade (A–F), stats, language breakdown, complexity hotspots, god nodes, dead
@@ -93,41 +152,4 @@ since 1.0.0, guarantees a stable MCP tool / CLI surface across the 1.x line.
93
152
  declared in 2+ files.
94
153
 
95
154
  ## [0.8.3] — 2026-05-31 · TSX/React component props
96
- - Component symbols carry `propsType` + `props[]`; detects `React.FC<P>` and
97
- JSX-returning PascalCase functions. MCP server version now read from package.json.
98
-
99
- ## [0.8.2] — 2026-05-30 · Swift cross-file wiring
100
- - `import <Module>` → that module's files (`Sources/<Module>/`). Completes
101
- cross-file graph/resolver support for all four v0.8.0 languages.
102
-
103
- ## [0.8.1] — 2026-05-30 · Kotlin + C/C++ cross-file wiring
104
- - Kotlin FQCN/package index; C/C++ `#include` resolution with header↔impl pairing.
105
- - Fixes: parse-cache rel-path leak; Kotlin call-graph extraction.
106
-
107
- ---
108
-
109
- ## Earlier (pre-session history)
110
-
111
- - **0.8.0** — +4 languages: C · C++ · Kotlin · Swift (symbol extraction + imports).
112
- - **0.7.0** — Go full module resolution; C# reverse `calledBy`; 4-suite test harness.
113
- - **0.6.0** — +3 languages: Rust · Java · C#; cross-language resolver.
114
- - **0.5.x** — `/ast-map` skill auto-install; iterative DFS; barrel re-exports; parse cache; call-graph aliases; `.ast-map.config.json`.
115
- - **0.4.0** — `search_symbol`, `get_file_deps`, `get_top_symbols`, dead-code tiers.
116
- - **0.3.0** — CLI; `find_dead_code`, `find_circular_deps`, `get_change_impact`, `get_call_graph`.
117
- - **0.2.0** — import extraction; `resolve_imports`; `build_symbol_graph`.
118
- - **0.1.0** — `get_skeleton_json`, `generate_skeleton`, `get_symbol_context`, `validate_architecture`.
119
-
120
- [1.11.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.11.0
121
- [1.10.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.10.0
122
- [1.9.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.9.0
123
- [1.8.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.8.0
124
- [1.7.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.7.0
125
- [1.6.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.6.0
126
- [1.5.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.5.0
127
- [1.4.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.4.0
128
- [1.3.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.3.0
129
- [1.2.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.2.0
130
- [1.1.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.1.0
131
- [1.0.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.0.0
132
- [0.9.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v0.9.0
133
- [0.8.1]: https://github.com/6ixthxense/AST-MCP/releases/tag/v0.8.1
155
+ - Component s
package/README.md CHANGED
@@ -4,7 +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
- **Supported languages:** TypeScript · TSX · JavaScript (ESM/CJS) · Python · Go · Rust · Java · C# · C · C++ · Kotlin · Swift
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
+
9
+ **Supported languages:** TypeScript · TSX · JavaScript (ESM/CJS) · Python · Go · Rust · Java · C# · C · C++ · Kotlin · Swift · **Vue** · **Svelte** (SFC `<script>`)
8
10
 
9
11
  | Capability | TS/JS | Python | Go | Rust | Java | C# | C | C++ | Kt | Swift |
10
12
  |--------------------------|:-----:|:------:|:---:|:----:|:----:|:---:|:---:|:---:|:---:|:-----:|
@@ -102,6 +104,12 @@ ast-map explore [dir] [-o out.html]
102
104
  ast-map watch [dir] [-o out.html]
103
105
  ast-map sourcemap <file>
104
106
  ast-map report [dir] [-o report.html]
107
+ ast-map diff [base] [--dir <d>] # git-aware changed symbols + impact
108
+ ast-map risk [dir] [-n N] # churn × complexity
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
105
113
  ast-map search <pattern> [dir] [-m contains|exact|regex] [-k kind] [-e]
106
114
  ast-map deps <file> [--scan <dir>]
107
115
  ast-map top <dir> [-n 10]
@@ -348,6 +356,103 @@ Scan a file or directory for **named functions/methods with parameters that are
348
356
 
349
357
  ---
350
358
 
359
+ ### `read_source_map`
360
+ Given a compiled JS/CSS file with an inline (`data:`) or external `sourceMappingURL`, return the **original source files** it maps back to (honors `sourceRoot`; reports embedded `sourcesContent`).
361
+
362
+ ```json
363
+ { "file": "dist/bundle.js", "mapKind": "inline", "sources": ["../src/app.ts", "../src/util.ts"], "hasContent": true }
364
+ ```
365
+
366
+ **Params:** `path`
367
+
368
+ ---
369
+
370
+ ### `get_codebase_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, and circular dependencies. Rendered as a premium HTML dashboard by `ast-map report`.
372
+
373
+ ```json
374
+ { "grade": "B", "score": 82, "fileCount": 120, "symbolCount": 1400,
375
+ "complexity": { "average": 4.1, "max": 22, "hotspots": [ … ] },
376
+ "godNodes": [ … ], "dead": { "count": 3, "items": [ … ] }, "cycles": { "count": 0, "items": [] } }
377
+ ```
378
+
379
+ **Params:** `path` (optional, defaults to root)
380
+
381
+ ---
382
+
383
+ ### `get_diff`
384
+ **Git-aware.** Compare the working tree to a git ref (default `HEAD`) and return which symbols were added/removed/modified per file, which changes are potentially **breaking** (removed or signature-changed exports), and the **blast radius** — files that depend on those breaking changes. Untracked new files count as additions.
385
+
386
+ ```json
387
+ { "summary": { "filesChanged": 2, "added": 1, "removed": 1, "modified": 1, "breaking": 2, "impactedFiles": 1 },
388
+ "breaking": [ { "file": "src/a.ts", "symbol": "foo", "reason": "signature changed" } ],
389
+ "impactedFiles": ["src/b.ts"] }
390
+ ```
391
+
392
+ **Params:** `base` (optional), `path` (optional)
393
+
394
+ ---
395
+
396
+ ### `pack_context`
397
+ **Token-efficient.** Assemble the *minimal* context to understand or change a symbol — its own source, the **signatures** of what it depends on (resolved imports), and the files that depend on it — instead of reading whole files. Returns a token estimate so you can see the savings.
398
+
399
+ ```json
400
+ { "primary": { "symbol": "login", "startLine": 8, "endLine": 12, "source": "…" },
401
+ "dependencies": [ { "file": "utils.ts", "symbols": [ { "name": "hashPassword", "signature": "…" } ] } ],
402
+ "dependents": [ { "file": "router.ts" } ], "tokenEstimate": 56 }
403
+ ```
404
+
405
+ **Params:** `path`, `symbol` (optional), `scan` (optional)
406
+
407
+ ---
408
+
409
+ ### `get_risk_map`
410
+ Rank files by **refactor risk = git churn × max complexity** — the files that are both frequently changed and complex (the best refactor / test targets).
411
+
412
+ ```json
413
+ { "files": [ { "file": "src/callgraph.ts", "churn": 7, "maxComplexity": 69, "risk": 483 } ] }
414
+ ```
415
+
416
+ **Params:** `path` (optional)
417
+
418
+ ---
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
+
351
456
  ### `get_change_impact`
352
457
  Given a file + symbol, reverse-traverse the import graph to compute **blast radius**.
353
458
 
@@ -578,6 +683,20 @@ Beyond tools, the server exposes the codebase as **browseable MCP resources**, s
578
683
 
579
684
  ---
580
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
+
581
700
  ## GitHub Action — architecture gate in CI
582
701
 
583
702
  Use AST-MCP as a CI check with the bundled composite action (`action.yml`):
@@ -621,6 +740,13 @@ Not part of the public API: the internal `src/` module layout and the generated
621
740
 
622
741
  | Version | What changed |
623
742
  |---------|--------------|
743
+ | **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**. |
744
+ | **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. |
745
+ | **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**. |
746
+ | **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**. |
747
+ | **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**. |
748
+ | **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**. |
749
+ | **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**. |
624
750
  | **1.11.0** | **Code-health dashboard** — new `ast-map report` CLI writes a premium self-contained HTML overview (grade A–F, stats, language breakdown, complexity hotspots, god nodes, dead code, cycles) + `get_codebase_report` MCP tool for the same as JSON. |
625
751
  | **1.10.0** | **Source-map support** — new `read_source_map` MCP tool + `ast-map sourcemap <file>` CLI: given a compiled JS/CSS file with an inline (`data:`) or external `sourceMappingURL`, returns the original source files it maps back to (honors `sourceRoot`). Traces `dist/` output back to source. |
626
752
  | **1.9.0** | **Watch mode** — `ast-map watch [dir]` recomputes the dependency analysis (file count · dead exports · cycles) on every source-file change, debounced; `-o file.html` also regenerates the live explorer each time. Plus: the explorer debug readout is now hidden (toggle with `d`). |
@@ -641,19 +767,4 @@ Not part of the public API: the internal `src/` module layout and the generated
641
767
  | **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**. |
642
768
  | **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. |
643
769
  | **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. |
644
- | **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. |
645
- | **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. |
646
- | **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`). |
647
- | **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. |
648
- | **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`). |
649
- | **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. |
650
- | **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) |
651
- | **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 |
652
- | **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) |
653
- | **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`) |
654
- | **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 |
655
- | **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) |
656
- | **0.4.0** | `search_symbol` · `get_file_deps` · `get_top_symbols` · dead code confidence tiers · 3 new CLI commands |
657
- | **0.3.0** | `ast-map` CLI · `find_dead_code` · `find_circular_deps` · `get_change_impact` · `get_call_graph` |
658
- | **0.2.0** | Import extraction · `resolve_imports` · `build_symbol_graph` |
659
- | **0.1.0** | `get_skeleton_json` · `generate_skeleton` · `get_symbol_context` · `validate_architecture` |
770
+ | **0.8.5** | **Cyclomatic complexity** — new `get_complexity` MCP tool + `ast-map complex
package/dist/cli.js CHANGED
@@ -17,6 +17,11 @@ import { discoverWorkspace, findPackageCycles } from "./workspace.js";
17
17
  import { buildExplorerHtml } from "./explorer.js";
18
18
  import { readSourceMap } from "./sourcemap.js";
19
19
  import { buildReport, buildReportHtml } from "./report.js";
20
+ import { computeDiff, computeRisk, isGitRepo } from "./gitdiff.js";
21
+ import { packContext } from "./contextpack.js";
22
+ import { computeCoupling } from "./coupling.js";
23
+ import { findLayerViolations } from "./layers.js";
24
+ import { computeModuleCoupling } from "./modulecoupling.js";
20
25
  import { buildCallGraph } from "./callgraph.js";
21
26
  import { searchSymbols } from "./search.js";
22
27
  const ROOT = path.resolve(process.env.AST_MAP_ROOT ?? process.cwd());
@@ -447,6 +452,183 @@ program
447
452
  console.log(`\n ${info.sources.length} original source(s)` + (info.hasContent ? dim(" · embeds sourcesContent") : ""));
448
453
  console.log();
449
454
  });
455
+ // ─── Command: pack ────────────────────────────────────────────────────────────
456
+ program
457
+ .command("pack <file> [symbol]")
458
+ .description("Minimal context pack for a symbol (source + dep signatures + dependents)")
459
+ .option("--scan <dir>", "Directory to scan for dependents", ".")
460
+ .option("--json", "Output as JSON")
461
+ .action(async (file, symbol, opts) => {
462
+ const { abs, rel } = resolveArg(file);
463
+ if (fs.statSync(abs).isDirectory())
464
+ die(`"${rel}" is a directory; pass a file`);
465
+ const scanAbs = resolveArg(opts.scan).abs;
466
+ const pack = await packContext(abs, rel, ROOT, symbol, scanAbs);
467
+ if (opts.json)
468
+ return jsonOut(pack);
469
+ header(`Context Pack \u2014 ${rel}${symbol ? "::" + symbol : ""} ${dim("(~" + pack.tokenEstimate + " tokens)")}`);
470
+ console.log(indent(bold("Primary") + dim(` lines ${pack.primary.startLine}-${pack.primary.endLine}`)));
471
+ console.log();
472
+ console.log(indent(bold("Depends on:")));
473
+ if (pack.dependencies.length === 0)
474
+ console.log(indent(dim("(none in-project)"), 4));
475
+ for (const d of pack.dependencies) {
476
+ console.log(indent(green(d.file), 4));
477
+ for (const sym of d.symbols)
478
+ console.log(indent(dim((sym.signature || sym.name)), 6));
479
+ }
480
+ console.log();
481
+ console.log(indent(bold("Depended on by:")));
482
+ if (pack.dependents.length === 0)
483
+ console.log(indent(dim("(none found in scan)"), 4));
484
+ for (const dep of pack.dependents)
485
+ console.log(indent(yellow(dep.file), 4));
486
+ console.log();
487
+ });
488
+ // ─── Command: diff ────────────────────────────────────────────────────────────
489
+ program
490
+ .command("diff [base]")
491
+ .description("Symbols changed since a git ref + breaking changes + blast radius")
492
+ .option("--dir <dir>", "Limit to a subdirectory", ".")
493
+ .option("--json", "Output as JSON")
494
+ .action(async (base, opts) => {
495
+ if (!isGitRepo(ROOT))
496
+ die("not a git repository (or git is unavailable)");
497
+ const { abs, rel } = resolveArg(opts.dir);
498
+ const ref = base ?? "HEAD";
499
+ const d = await computeDiff(abs, ROOT, ref);
500
+ if (opts.json)
501
+ return jsonOut(d);
502
+ header(`Diff since ${bold(ref)} ${dim(`(${d.summary.filesChanged} file(s) · +${d.summary.added} ~${d.summary.modified} -${d.summary.removed})`)}`);
503
+ if (d.files.length === 0) {
504
+ console.log(indent(dim("No source-symbol changes.")));
505
+ console.log();
506
+ return;
507
+ }
508
+ for (const f of d.files) {
509
+ console.log(indent(`${bold(f.file)} ${dim("[" + f.status + "]")}`));
510
+ for (const a of f.added)
511
+ console.log(indent(green("+ ") + a.symbol + dim(a.exported ? " (exported)" : ""), 4));
512
+ for (const m of f.modified)
513
+ console.log(indent(yellow("~ ") + m.symbol + dim(m.exported ? " (exported)" : ""), 4));
514
+ for (const r of f.removed)
515
+ console.log(indent(red("- ") + r.symbol + dim(r.exported ? " (exported)" : ""), 4));
516
+ }
517
+ if (d.breaking.length > 0) {
518
+ console.log(`\n${indent(bold(red("\u26a0 Breaking changes (" + d.breaking.length + ")")))}`);
519
+ for (const b of d.breaking)
520
+ console.log(indent(`${red(b.symbol)} ${dim(b.reason)} ${dim(b.file)}`, 4));
521
+ console.log(`\n${indent(yellow(d.impactedFiles.length + " file(s) impacted") + dim(" by breaking changes"))}`);
522
+ for (const f of d.impactedFiles.slice(0, 20))
523
+ console.log(indent(dim(f), 4));
524
+ }
525
+ console.log();
526
+ });
527
+ // ─── Command: risk ────────────────────────────────────────────────────────────
528
+ program
529
+ .command("risk [dir]")
530
+ .description("Rank files by refactor risk (git churn × complexity)")
531
+ .option("--json", "Output as JSON")
532
+ .option("-n, --top <n>", "Show top N", (v) => parseInt(v, 10), 15)
533
+ .action(async (dir, opts) => {
534
+ if (!isGitRepo(ROOT))
535
+ die("not a git repository (or git is unavailable)");
536
+ const { abs, rel } = resolveArg(dir ?? ".");
537
+ const files = await computeRisk(abs, ROOT);
538
+ if (opts.json)
539
+ return jsonOut({ count: files.length, files });
540
+ header(`Refactor Risk \u2014 ${rel}/ ${dim("(churn × max complexity)")}`);
541
+ if (files.length === 0) {
542
+ console.log(indent(green("✓ nothing risky (no churn × complexity)")));
543
+ console.log();
544
+ return;
545
+ }
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]]);
547
+ console.log();
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
+ });
450
632
  // ─── Command: report ──────────────────────────────────────────────────────────
451
633
  program
452
634
  .command("report [dir]")
@@ -0,0 +1,79 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { buildSkeleton, collectSourceFiles } from "./skeleton.js";
4
+ import { resolveOptions } from "./config.js";
5
+ import { resolveFileImports } from "./resolver.js";
6
+ import { buildCallGraph } from "./callgraph.js";
7
+ function findSym(syms, name) {
8
+ for (const s of syms) {
9
+ if (s.name === name)
10
+ return s;
11
+ const n = findSym(s.children, name);
12
+ if (n)
13
+ return n;
14
+ }
15
+ return null;
16
+ }
17
+ const tok = (s) => Math.round(s.length / 4);
18
+ /**
19
+ * Assemble the minimal context an agent needs to understand or change a symbol:
20
+ * the symbol's own source, the signatures of what it depends on (resolved
21
+ * imports), and the files that depend on it — instead of reading whole files.
22
+ */
23
+ export async function packContext(absFile, relFile, root, symbolName, scanDir) {
24
+ const opts = resolveOptions({ detail: "full", emitHtml: false });
25
+ const skel = await buildSkeleton(absFile, relFile, opts);
26
+ const lines = fs.readFileSync(absFile, "utf8").split(/\r?\n/);
27
+ let startLine = 1, endLine = lines.length;
28
+ if (symbolName) {
29
+ const sym = findSym(skel.symbols, symbolName);
30
+ if (sym) {
31
+ startLine = sym.range.startLine;
32
+ endLine = sym.range.endLine;
33
+ }
34
+ }
35
+ const source = lines.slice(startLine - 1, endLine).join("\n");
36
+ // Dependencies: resolved in-project imports + the target symbol signatures.
37
+ const refs = await resolveFileImports(skel, absFile, root);
38
+ const byFile = new Map();
39
+ for (const r of refs) {
40
+ if (!r.found || !r.resolvedRel)
41
+ continue;
42
+ const arr = byFile.get(r.resolvedRel) ?? [];
43
+ if (!arr.some((x) => x.name === r.symbol))
44
+ arr.push({ name: r.symbol, signature: r.signature ?? null });
45
+ byFile.set(r.resolvedRel, arr);
46
+ }
47
+ const dependencies = [...byFile.entries()].map(([file, symbols]) => ({ file, symbols }));
48
+ // Dependents: who calls the seed symbol (needs a directory scan).
49
+ let dependents = [];
50
+ if (symbolName && scanDir) {
51
+ const sopts = resolveOptions({ detail: "outline", emitHtml: false });
52
+ const skels = [];
53
+ for (const f of collectSourceFiles(scanDir, sopts)) {
54
+ const rr = path.relative(root, f).split(path.sep).join("/");
55
+ try {
56
+ skels.push(await buildSkeleton(f, rr, sopts));
57
+ }
58
+ catch { /* skip */ }
59
+ }
60
+ const cg = await buildCallGraph(absFile, symbolName, root, skels);
61
+ if (cg) {
62
+ const seen = new Set();
63
+ for (const c of cg.calledBy)
64
+ if (!seen.has(c.file)) {
65
+ seen.add(c.file);
66
+ dependents.push({ file: c.file });
67
+ }
68
+ }
69
+ }
70
+ const depTok = dependencies.reduce((a, d) => a + d.symbols.reduce((b, s) => b + tok(s.signature || s.name), 0), 0);
71
+ return {
72
+ seed: { file: relFile, ...(symbolName ? { symbol: symbolName } : {}) },
73
+ primary: { file: relFile, ...(symbolName ? { symbol: symbolName } : {}), startLine, endLine, source },
74
+ dependencies,
75
+ dependents,
76
+ tokenEstimate: tok(source) + depTok,
77
+ note: "Read primary.source in full; for dependencies you usually only need the listed signatures, not the whole files.",
78
+ };
79
+ }
@@ -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
+ }
@@ -0,0 +1,178 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { buildSkeleton, collectSourceFiles } from "./skeleton.js";
6
+ import { resolveOptions } from "./config.js";
7
+ import { buildSymbolGraph } from "./graph.js";
8
+ import { getChangeImpact } from "./graph-analysis.js";
9
+ import { detectLanguage } from "./registry.js";
10
+ import { computeFileComplexity } from "./complexity.js";
11
+ function git(args, cwd) {
12
+ return execFileSync("git", args, { cwd, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
13
+ }
14
+ export function isGitRepo(root) {
15
+ try {
16
+ git(["rev-parse", "--is-inside-work-tree"], root);
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ function changedFiles(root, base) {
24
+ let out;
25
+ try {
26
+ out = git(["diff", "--name-status", base, "--"], root);
27
+ }
28
+ catch {
29
+ return [];
30
+ }
31
+ const res = [];
32
+ for (const line of out.split(/\r?\n/)) {
33
+ if (!line.trim())
34
+ continue;
35
+ const m = line.match(/^([AMD])\t(.+)$/);
36
+ if (m) {
37
+ res.push({ status: m[1], file: m[2] });
38
+ continue;
39
+ }
40
+ const r = line.match(/^R\d+\t\S+\t(.+)$/); // rename → treat new path as modified
41
+ if (r)
42
+ res.push({ status: "M", file: r[1] });
43
+ }
44
+ // Untracked files are new since any ref — treat them as added.
45
+ try {
46
+ const untracked = git(["ls-files", "--others", "--exclude-standard"], root);
47
+ for (const f of untracked.split(/\r?\n/)) {
48
+ if (f.trim() && !res.some((x) => x.file === f))
49
+ res.push({ status: "A", file: f });
50
+ }
51
+ }
52
+ catch { /* ignore */ }
53
+ return res.filter((f) => detectLanguage(f.file));
54
+ }
55
+ function oldContent(root, base, rel) {
56
+ try {
57
+ return git(["show", `${base}:${rel}`], root);
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ async function skeletonFromSource(source, rel) {
64
+ const ext = path.extname(rel);
65
+ const tmp = path.join(os.tmpdir(), `astdiff-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`);
66
+ try {
67
+ fs.writeFileSync(tmp, source);
68
+ return await buildSkeleton(tmp, rel, resolveOptions({ detail: "full", emitHtml: false }));
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ finally {
74
+ try {
75
+ fs.unlinkSync(tmp);
76
+ }
77
+ catch { /* ignore */ }
78
+ }
79
+ }
80
+ function flatten(syms, prefix, acc) {
81
+ for (const s of syms) {
82
+ const q = prefix ? prefix + "." + s.name : s.name;
83
+ acc.set(q, s);
84
+ flatten(s.children, q, acc);
85
+ }
86
+ return acc;
87
+ }
88
+ const sc = (s) => ({ symbol: s.name, kind: s.kind, exported: s.exported ?? false });
89
+ export async function computeDiff(absDir, root, base) {
90
+ const absDirNorm = path.resolve(absDir);
91
+ const changed = changedFiles(root, base).filter((f) => path.resolve(root, f.file).startsWith(absDirNorm));
92
+ const files = [];
93
+ const breaking = [];
94
+ for (const cf of changed) {
95
+ const rel = cf.file;
96
+ const newSkel = cf.status === "D" ? null : await safeBuildFromDisk(path.resolve(root, rel), rel);
97
+ const oldSrc = cf.status === "A" ? null : oldContent(root, base, rel);
98
+ const oldSkel = oldSrc != null ? await skeletonFromSource(oldSrc, rel) : null;
99
+ const oldMap = oldSkel ? flatten(oldSkel.symbols, "", new Map()) : new Map();
100
+ const newMap = newSkel ? flatten(newSkel.symbols, "", new Map()) : new Map();
101
+ const added = [], removed = [], modified = [];
102
+ for (const [q, s] of newMap)
103
+ if (!oldMap.has(q))
104
+ added.push(sc(s));
105
+ for (const [q, s] of oldMap)
106
+ if (!newMap.has(q))
107
+ removed.push(sc(s));
108
+ for (const [q, s] of newMap) {
109
+ const o = oldMap.get(q);
110
+ if (o && (o.signature ?? "") !== (s.signature ?? ""))
111
+ modified.push(sc(s));
112
+ }
113
+ const status = cf.status === "A" ? "added" : cf.status === "D" ? "deleted" : "modified";
114
+ files.push({ file: rel, status, added, removed, modified });
115
+ for (const r of removed)
116
+ if (r.exported && !r.symbol.includes(" ")) {
117
+ breaking.push({ file: rel, symbol: r.symbol, reason: cf.status === "D" ? "file deleted" : "export removed" });
118
+ }
119
+ for (const m of modified)
120
+ if (m.exported)
121
+ breaking.push({ file: rel, symbol: m.symbol, reason: "signature changed" });
122
+ }
123
+ // Blast radius of breaking changes (top-level symbols only).
124
+ const impacted = new Set();
125
+ if (breaking.length > 0) {
126
+ const opts = resolveOptions({ detail: "outline", emitHtml: false });
127
+ const skels = [];
128
+ for (const f of collectSourceFiles(absDirNorm, opts)) {
129
+ const r = path.relative(root, f).split(path.sep).join("/");
130
+ try {
131
+ skels.push(await buildSkeleton(f, r, opts));
132
+ }
133
+ catch { /* skip */ }
134
+ }
135
+ const graph = buildSymbolGraph(skels, root);
136
+ for (const b of breaking) {
137
+ const imp = getChangeImpact(graph, `${b.file}::${b.symbol}`);
138
+ if (imp)
139
+ for (const d of [...imp.direct, ...imp.transitive])
140
+ if (d.file !== b.file)
141
+ impacted.add(d.file);
142
+ }
143
+ }
144
+ const sum = files.reduce((a, f) => ({ added: a.added + f.added.length, removed: a.removed + f.removed.length, modified: a.modified + f.modified.length }), { added: 0, removed: 0, modified: 0 });
145
+ return {
146
+ base,
147
+ files,
148
+ breaking,
149
+ impactedFiles: [...impacted].sort(),
150
+ summary: { filesChanged: files.length, ...sum, breaking: breaking.length, impactedFiles: impacted.size },
151
+ };
152
+ }
153
+ async function safeBuildFromDisk(abs, rel) {
154
+ try {
155
+ return await buildSkeleton(abs, rel, resolveOptions({ detail: "full", emitHtml: false }));
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ }
161
+ export async function computeRisk(absDir, root) {
162
+ const opts = resolveOptions({ detail: "outline", emitHtml: false });
163
+ const out = [];
164
+ for (const f of collectSourceFiles(absDir, opts)) {
165
+ const rel = path.relative(root, f).split(path.sep).join("/");
166
+ let churn = 0;
167
+ try {
168
+ churn = parseInt(git(["rev-list", "--count", "HEAD", "--", rel], root).trim(), 10) || 0;
169
+ }
170
+ catch {
171
+ churn = 0;
172
+ }
173
+ const fc = await computeFileComplexity(f, rel);
174
+ const maxC = fc ? fc.maxComplexity : 0;
175
+ out.push({ file: rel, churn, maxComplexity: maxC, risk: churn * maxC });
176
+ }
177
+ return out.filter((r) => r.risk > 0).sort((a, b) => b.risk - a.risk);
178
+ }
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
@@ -21,6 +21,12 @@ import { traceTypeInFile } from "./typeflow.js";
21
21
  import { discoverWorkspace, findPackageCycles } from "./workspace.js";
22
22
  import { readSourceMap } from "./sourcemap.js";
23
23
  import { buildReport } from "./report.js";
24
+ import { computeDiff, computeRisk, isGitRepo } from "./gitdiff.js";
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";
24
30
  /** Files may only be read inside this root (override with AST_MAP_ROOT). */
25
31
  const ROOT = path.resolve(process.env.AST_MAP_ROOT ?? process.cwd());
26
32
  function resolveInRoot(input) {
@@ -65,6 +71,7 @@ const server = new McpServer({
65
71
  name: "universal-ast-mapper",
66
72
  version: PKG_VERSION,
67
73
  });
74
+ registerPrompts(server);
68
75
  /* ----------------------- tool: list_supported_languages ----------------------- */
69
76
  server.registerTool("list_supported_languages", {
70
77
  title: "List supported languages",
@@ -790,6 +797,172 @@ server.registerTool("get_codebase_report", {
790
797
  return errorText(describeError(err));
791
798
  }
792
799
  });
800
+ /* ─────────────────── tool: get_diff ────────────────────────────────────── */
801
+ server.registerTool("get_diff", {
802
+ title: "Git-aware change diff + blast radius",
803
+ description: "Compare the working tree against a git ref (default HEAD) and return which symbols were " +
804
+ "added/removed/modified per file, which changes are potentially **breaking** (removed or " +
805
+ "signature-changed exports), and the **blast radius** \u2014 files that depend on those breaking changes.",
806
+ inputSchema: {
807
+ base: z.string().optional().describe("Git ref to compare against. Default HEAD."),
808
+ path: z.string().optional().describe("Limit to a subdirectory. Default project root."),
809
+ },
810
+ }, async ({ base, path: input }) => {
811
+ try {
812
+ if (!isGitRepo(ROOT))
813
+ return errorText("Not a git repository (or git is unavailable).");
814
+ const { abs, rel } = resolveInRoot(input ?? ".");
815
+ const data = await computeDiff(abs, ROOT, base ?? "HEAD");
816
+ return jsonText({ directory: rel.split(path.sep).join("/") || ".", ...data });
817
+ }
818
+ catch (err) {
819
+ return errorText(describeError(err));
820
+ }
821
+ });
822
+ /* ─────────────────── tool: get_risk_map ────────────────────────────────── */
823
+ server.registerTool("get_risk_map", {
824
+ title: "Refactor risk map (churn \u00d7 complexity)",
825
+ description: "Rank files by refactor risk = git churn (number of commits touching the file) \u00d7 the file's " +
826
+ "max function complexity. Surfaces the files that are both frequently changed and complex \u2014 " +
827
+ "the most valuable refactor / test targets.",
828
+ inputSchema: {
829
+ path: z.string().optional().describe("Directory to scan. Default project root."),
830
+ },
831
+ }, async ({ path: input }) => {
832
+ try {
833
+ if (!isGitRepo(ROOT))
834
+ return errorText("Not a git repository (or git is unavailable).");
835
+ const { abs, rel } = resolveInRoot(input ?? ".");
836
+ const files = await computeRisk(abs, ROOT);
837
+ return jsonText({ directory: rel.split(path.sep).join("/") || ".", count: files.length, files: files.slice(0, 50) });
838
+ }
839
+ catch (err) {
840
+ return errorText(describeError(err));
841
+ }
842
+ });
843
+ /* ─────────────────── tool: pack_context ────────────────────────────────── */
844
+ server.registerTool("pack_context", {
845
+ title: "Minimal context pack for a symbol",
846
+ description: "Assemble the *minimal* context needed to understand or change a symbol \u2014 the symbol's own " +
847
+ "source, the signatures of what it depends on (resolved imports), and the files that depend on " +
848
+ "it \u2014 instead of reading whole files. Returns a token estimate so you can see the savings.",
849
+ inputSchema: {
850
+ path: z.string().describe("File containing the symbol (relative to root or absolute within it)."),
851
+ symbol: z.string().optional().describe("Symbol name to centre the pack on. Omit for the whole file."),
852
+ scan: z.string().optional().describe("Directory to scan for dependents. Default: project root."),
853
+ },
854
+ }, async ({ path: input, symbol, scan }) => {
855
+ try {
856
+ const { abs, rel } = resolveInRoot(input);
857
+ if (fs.statSync(abs).isDirectory())
858
+ return errorText(`"${input}" is a directory; pass a file.`);
859
+ const scanAbs = scan ? resolveInRoot(scan).abs : ROOT;
860
+ const pack = await packContext(abs, rel.split(path.sep).join("/"), ROOT, symbol, scanAbs);
861
+ return jsonText(pack);
862
+ }
863
+ catch (err) {
864
+ return errorText(describeError(err));
865
+ }
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
+ });
793
966
  /* ─────────────────── tool: get_change_impact ───────────────────────────── */
794
967
  server.registerTool("get_change_impact", {
795
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/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.11.0",
3
+ "version": "1.18.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",