universal-ast-mapper 1.27.0 → 1.28.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 +17 -0
- package/README.md +2 -1
- package/dist/cli.js +1 -1
- package/dist/index.js +2 -1
- package/dist/report.js +45 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,23 @@ since 1.0.0, guarantees a stable MCP tool / CLI surface across the 1.x line.
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [1.28.0] — 2026-06-11 · Test coverage in the dashboard
|
|
10
|
+
- The health dashboard (`ast-map report` / `get_codebase_report`) now surfaces
|
|
11
|
+
v1.27's structural test coverage:
|
|
12
|
+
- **Test coverage card** — coverage bar (tested/total sources, % colored
|
|
13
|
+
green ≥ 70 / amber ≥ 40 / red below) + the **untested sources ranked by
|
|
14
|
+
risk** (fan-in Ca, then symbol count), capped at 12 with a "+N more" note.
|
|
15
|
+
- **Test coverage stat tile** in the header grid.
|
|
16
|
+
- **Root fallback** — reporting on `src/` only (no test files in the scanned
|
|
17
|
+
dir)? Test files are pulled in from the project root automatically and the
|
|
18
|
+
card notes "(from project root)".
|
|
19
|
+
- **Health score** now includes a structural-coverage penalty (capped at 8
|
|
20
|
+
points, proportional to the untested share).
|
|
21
|
+
- `ReportData` gains `testCoverage` (testFiles, sourceFiles, testedSources,
|
|
22
|
+
coverageRatio, untestedCount, untested[], rootFallback) — additive.
|
|
23
|
+
- CLI `ast-map report` summary line now shows `tests N%`.
|
|
24
|
+
- Tests: +6 checks in `test/analysis.mjs` (159 total).
|
|
25
|
+
|
|
9
26
|
## [1.27.0] — 2026-06-11 · Test-coverage mapping
|
|
10
27
|
- **New MCP tool `get_test_coverage`** + **CLI `ast-map tests [dir]`** (alias
|
|
11
28
|
`coverage`) — structural test coverage with zero instrumentation: which source
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ 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
|
-
**30 MCP tools / 32 CLI commands / 5 MCP prompts** spanning skeletons, dependency graphs, and deep analysis — dead code, cycles, change-impact, complexity, duplicates, unused params, type-flow, decorators, test-coverage mapping — plus monorepo support, an interactive **graph explorer** with a **coupling overlay** (`ast-map explore`), **watch mode**, a one-page **health dashboard** (`ast-map report`), a **persistent parse cache + parallel parsing** (warm re-scans skip parsing entirely), and a **CI quality gate** (`ast-map check`, baseline ratchet).
|
|
7
|
+
**30 MCP tools / 32 CLI commands / 5 MCP prompts** spanning skeletons, dependency graphs, and deep analysis — dead code, cycles, change-impact, complexity, duplicates, unused params, type-flow, decorators, test-coverage mapping — plus monorepo support, an interactive **graph explorer** with a **coupling overlay** (`ast-map explore`), **watch mode**, a one-page **health dashboard** with test-coverage, coupling and SDP cards (`ast-map report`), a **persistent parse cache + parallel parsing** (warm re-scans skip parsing entirely), and a **CI quality gate** (`ast-map check`, baseline ratchet).
|
|
8
8
|
|
|
9
9
|
**Supported languages:** TypeScript · TSX · JavaScript (ESM/CJS) · Python · Go · Rust · Java · C# · C · C++ · Kotlin · Swift · Vue · Svelte (SFC `<script>`) · **PHP** · **Ruby**
|
|
10
10
|
|
|
@@ -835,6 +835,7 @@ Not part of the public API: the internal `src/` module layout and the generated
|
|
|
835
835
|
|
|
836
836
|
| Version | What changed |
|
|
837
837
|
|---------|--------------|
|
|
838
|
+
| **1.28.0** | **Test coverage in the dashboard** — `ast-map report` / `get_codebase_report` gain a **Test coverage** card (coverage bar + untested sources ranked by risk with Ca/symbols) and stat tile; structural coverage now factors into the health score (capped penalty). Reporting on `src/` only? Test files are **pulled in from the project root automatically**. |
|
|
838
839
|
| **1.27.0** | **Test-coverage mapping** — new MCP tool `get_test_coverage` + CLI `ast-map tests` (alias `coverage`): pairs test files with the sources they exercise (import edges + naming conventions) and lists **untested sources ranked by risk** (fan-in, then symbols). Fixture dirs excluded; orphan tests reported. File-level, zero instrumentation. (**30 tools / 32 commands**) |
|
|
839
840
|
| **1.26.0** | **Coupling overlay in the explorer** — `ast-map explore` gains a `color: coupling` mode: nodes shaded by **instability** I = Ce/(Ca+Ce) on a green (stable) → red (volatile) scale, with a legend, and Ca / Ce / I readouts in the hover tooltip and detail sidebar. Spot load-bearing files and volatile hotspots at a glance. |
|
|
840
841
|
| **1.25.0** | **Semantic symbol search** — new MCP tool `semantic_search` + CLI `ast-map find <query>`: find symbols by *meaning* ("remove expired sessions" → `clearDiskCache`). Identifier tokenization + 60-group programming thesaurus + stemming + fuzzy matching + BM25-style IDF ranking over names, docs, signatures and paths. No embeddings, no network. (**29 tools / 31 commands**) |
|
package/dist/cli.js
CHANGED
|
@@ -675,7 +675,7 @@ program
|
|
|
675
675
|
fs.writeFileSync(out, buildReportHtml(data), "utf8");
|
|
676
676
|
header(`Code Health \u2014 ${rel}/ ${dim(`(${data.fileCount} files)`)}`);
|
|
677
677
|
const gcolor = data.grade === "A" || data.grade === "B" ? green : data.grade === "C" || data.grade === "D" ? yellow : (x) => x;
|
|
678
|
-
console.log(indent(`Grade ${bold(gcolor(data.grade))} ${dim("(" + data.score + "/100)")} · ${data.dead.count} dead · ${data.cycles.count} cycles · max cx ${data.complexity.max}
|
|
678
|
+
console.log(indent(`Grade ${bold(gcolor(data.grade))} ${dim("(" + data.score + "/100)")} · ${data.dead.count} dead · ${data.cycles.count} cycles · max cx ${data.complexity.max} · tests ${Math.round(data.testCoverage.coverageRatio * 100)}%`));
|
|
679
679
|
console.log(indent(green("✓ wrote " + path.relative(process.cwd(), out))));
|
|
680
680
|
console.log();
|
|
681
681
|
});
|
package/dist/index.js
CHANGED
|
@@ -794,7 +794,8 @@ server.registerTool("get_codebase_report", {
|
|
|
794
794
|
title: "Codebase health report",
|
|
795
795
|
description: "Scan a directory and return a one-shot health summary: file/symbol counts, language " +
|
|
796
796
|
"breakdown, a health grade (A\u2013F) and score, complexity hotspots, god nodes (most-imported " +
|
|
797
|
-
"symbols), dead exports,
|
|
797
|
+
"symbols), dead exports, circular dependencies, module coupling, SDP violations, and structural " +
|
|
798
|
+
"test coverage (untested sources ranked by risk). The `ast-map report` CLI renders this as HTML.",
|
|
798
799
|
inputSchema: {
|
|
799
800
|
path: z.string().optional().describe("Directory to scan. Defaults to the project root."),
|
|
800
801
|
},
|
package/dist/report.js
CHANGED
|
@@ -6,6 +6,7 @@ import { buildSymbolGraph } from "./graph.js";
|
|
|
6
6
|
import { findDeadExports, findCircularDeps, getTopSymbols } from "./graph-analysis.js";
|
|
7
7
|
import { findLayerViolations } from "./layers.js";
|
|
8
8
|
import { computeModuleCoupling } from "./modulecoupling.js";
|
|
9
|
+
import { mapTestCoverage, isTestFile, isFixtureFile } from "./testmap.js";
|
|
9
10
|
function gradeFor(score) {
|
|
10
11
|
if (score >= 90)
|
|
11
12
|
return "A";
|
|
@@ -53,6 +54,27 @@ export async function buildReport(absDir, root) {
|
|
|
53
54
|
const god = getTopSymbols(graph, 8);
|
|
54
55
|
const layerViolations = findLayerViolations(graph);
|
|
55
56
|
const modules = computeModuleCoupling(graph).modules;
|
|
57
|
+
// Test coverage. If the scanned dir has no test files (common when reporting
|
|
58
|
+
// on src/ only), pull test files in from the project root so the map can
|
|
59
|
+
// still pair them with the scanned sources.
|
|
60
|
+
let covGraph = graph;
|
|
61
|
+
let rootFallback = false;
|
|
62
|
+
if (!skeletons.some((s) => isTestFile(s.file)) && path.resolve(absDir) !== path.resolve(root)) {
|
|
63
|
+
const have = new Set(items.map((i) => i.abs));
|
|
64
|
+
const testItems = collectSourceFiles(root, opts)
|
|
65
|
+
.filter((f) => !have.has(f))
|
|
66
|
+
.map((f) => ({ abs: f, rel: path.relative(root, f).split(path.sep).join("/") }))
|
|
67
|
+
.filter((i) => isTestFile(i.rel) && !isFixtureFile(i.rel));
|
|
68
|
+
if (testItems.length > 0) {
|
|
69
|
+
const builtTests = await buildSkeletonsBulk(testItems, opts);
|
|
70
|
+
const testSkels = builtTests.filter((r) => r !== null).map((r) => r.skel);
|
|
71
|
+
if (testSkels.length > 0) {
|
|
72
|
+
covGraph = buildSymbolGraph([...skeletons, ...testSkels], root);
|
|
73
|
+
rootFallback = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const cov = mapTestCoverage(covGraph);
|
|
56
78
|
hotspots.sort((a, b) => b.complexity - a.complexity);
|
|
57
79
|
const veryHigh = hotspots.filter((f) => f.complexity > 20).length;
|
|
58
80
|
const high = hotspots.filter((f) => f.complexity > 10 && f.complexity <= 20).length;
|
|
@@ -63,6 +85,7 @@ export async function buildReport(absDir, root) {
|
|
|
63
85
|
score -= Math.min(28, veryHigh * 4 + high * 1);
|
|
64
86
|
score -= Math.min(12, god.filter((g) => g.importCount >= 8).length * 4);
|
|
65
87
|
score -= Math.min(10, layerViolations.length);
|
|
88
|
+
score -= Math.min(8, Math.round((1 - cov.coverageRatio) * 8)); // structural test coverage
|
|
66
89
|
score = Math.max(0, Math.round(score));
|
|
67
90
|
const languages = [...langCount.entries()]
|
|
68
91
|
.map(([lang, f]) => ({ lang, files: f }))
|
|
@@ -82,6 +105,15 @@ export async function buildReport(absDir, root) {
|
|
|
82
105
|
complexity: { average: cxN ? Math.round((cxSum / cxN) * 10) / 10 : 0, max: cxMax, hotspots: hotspots.slice(0, 12) },
|
|
83
106
|
layerViolations: { count: layerViolations.length, items: layerViolations.slice(0, 12) },
|
|
84
107
|
modules: modules.slice(0, 10),
|
|
108
|
+
testCoverage: {
|
|
109
|
+
testFiles: cov.testFiles,
|
|
110
|
+
sourceFiles: cov.sourceFiles,
|
|
111
|
+
testedSources: cov.testedSources,
|
|
112
|
+
coverageRatio: cov.coverageRatio,
|
|
113
|
+
untestedCount: cov.untestedSources,
|
|
114
|
+
untested: cov.untested.slice(0, 12),
|
|
115
|
+
rootFallback,
|
|
116
|
+
},
|
|
85
117
|
};
|
|
86
118
|
}
|
|
87
119
|
/* ─── Premium HTML dashboard ───────────────────────────────────────────────── */
|
|
@@ -125,6 +157,17 @@ export function buildReportHtml(d) {
|
|
|
125
157
|
const modules = d.modules.length
|
|
126
158
|
? d.modules.map((m) => bar(`${m.module} · ${m.files} file(s)`, m.instability, 1, instColor(m.instability), `Ca ${m.afferent} · Ce ${m.efferent} · <b>I ${m.instability.toFixed(2)}</b>`)).join("")
|
|
127
159
|
: `<div class="empty">No cross-module imports.</div>`;
|
|
160
|
+
const covPct = Math.round(d.testCoverage.coverageRatio * 100);
|
|
161
|
+
const covC = d.testCoverage.coverageRatio >= 0.7 ? "#1d9e75" : d.testCoverage.coverageRatio >= 0.4 ? "#ba7517" : "#e24b4a";
|
|
162
|
+
const covHead = d.testCoverage.testFiles > 0
|
|
163
|
+
? bar(`${d.testCoverage.testedSources}/${d.testCoverage.sourceFiles} sources tested · ${d.testCoverage.testFiles} test file(s)${d.testCoverage.rootFallback ? " (from project root)" : ""}`, covPct, 100, covC, `<b>${covPct}%</b>`)
|
|
164
|
+
: "";
|
|
165
|
+
const covList = d.testCoverage.testFiles === 0
|
|
166
|
+
? `<div class="empty">No test files found in the scanned directory or project root.</div>`
|
|
167
|
+
: d.testCoverage.untested.length === 0
|
|
168
|
+
? `<div class="ok">✓ Every source file has at least one test</div>`
|
|
169
|
+
: d.testCoverage.untested.map((u) => `<div class="li"><span class="mono">${esc(u.file)}</span><span class="dim">${u.symbols} symbol(s)</span><span class="pill">Ca ${u.afferent}</span></div>`).join("")
|
|
170
|
+
+ (d.testCoverage.untestedCount > d.testCoverage.untested.length ? `<div class="more">+${d.testCoverage.untestedCount - d.testCoverage.untested.length} more…</div>` : "");
|
|
128
171
|
const sdp = d.layerViolations.count
|
|
129
172
|
? d.layerViolations.items.map((v) => `<div class="li"><span class="mono">${esc(v.from)}</span><span class="dim">→ ${esc(v.to)}</span><span class="pill" style="color:${instColor(0.9)}">+${v.severity.toFixed(2)}</span></div>`).join("")
|
|
130
173
|
+ (d.layerViolations.count > d.layerViolations.items.length ? `<div class="more">+${d.layerViolations.count - d.layerViolations.items.length} more…</div>` : "")
|
|
@@ -170,6 +213,7 @@ export function buildReportHtml(d) {
|
|
|
170
213
|
${statCard("Dead exports", d.dead.count, d.dead.count ? "#d85a30" : "#1d9e75")}
|
|
171
214
|
${statCard("Cycles", d.cycles.count, d.cycles.count ? "#e24b4a" : "#1d9e75")}
|
|
172
215
|
${statCard("SDP violations", d.layerViolations.count, d.layerViolations.count ? "#d85a30" : "#1d9e75")}
|
|
216
|
+
${statCard("Test coverage", covPct + "%", covC)}
|
|
173
217
|
</div>
|
|
174
218
|
<div class="card"><h2>Language breakdown</h2>${langs}</div>
|
|
175
219
|
<div class="card"><h2>Complexity hotspots</h2>${hotspots}</div>
|
|
@@ -181,6 +225,7 @@ export function buildReportHtml(d) {
|
|
|
181
225
|
<div class="card"><h2>Module coupling (instability)</h2>${modules}</div>
|
|
182
226
|
<div class="card"><h2>Layer violations (stable → volatile)</h2>${sdp}</div>
|
|
183
227
|
</div>
|
|
228
|
+
<div class="card"><h2>Test coverage (untested by risk)</h2>${covHead}${covList}</div>
|
|
184
229
|
<div class="card"><h2>Dead exports (high confidence)</h2>${dead}</div>
|
|
185
230
|
<div class="foot">Generated by AST-MCP · universal-ast-mapper</div>
|
|
186
231
|
</div></body></html>`;
|
package/package.json
CHANGED