Skip to content

Commit b6546d3

Browse files
authored
fix: exclude test files from coverage/coupling metrics (#12)
## Summary Fixes #7 - Added `isTestFile` detection to FileMetrics (matches `.test.ts`, `.spec.tsx`, `__tests__/`, etc.) - Filtered test files from coverage and coupling hotspot results - Adjusted coupling formula: `fanOut / (max(fanIn, 1) + fanOut)` — leaf consumers no longer auto-rank highest - Updated existing tests for new coupling formula ## Test plan - [x] Regression tests for all ACs (19 new tests) - [x] All existing tests pass (205 total) - [x] Quality gates: lint, typecheck, build, test
1 parent 364e649 commit b6546d3

File tree

10 files changed

+741
-6
lines changed

10 files changed

+741
-6
lines changed

docs/data-model.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ FileMetrics {
6363
betweenness: number
6464
fanIn: number
6565
fanOut: number
66-
coupling: number // fanOut / (fanIn + fanOut)
66+
coupling: number // fanOut / (max(fanIn, 1) + fanOut)
6767
tension: number // Entropy of multi-module pulls
6868
isBridge: boolean // betweenness > 0.1
6969

@@ -76,6 +76,7 @@ FileMetrics {
7676
cyclomaticComplexity: number // Avg complexity of exports
7777
blastRadius: number // Transitive dependent count
7878
deadExports: string[] // Unused export names
79+
isTestFile: boolean // Whether this file is a test file
7980
}
8081

8182
ModuleMetrics {

docs/metrics.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ All metrics are computed per-file and stored in `FileMetrics`. Module-level aggr
1010
| **betweenness** | number | 0-1 | graphology-metrics | How often file bridges shortest paths between others. Normalized. |
1111
| **fanIn** | number | 0-N | graph in-degree | Count of files that import this file. |
1212
| **fanOut** | number | 0-N | graph out-degree | Count of files this file imports. |
13-
| **coupling** | number | 0-1 | derived | `fanOut / (fanIn + fanOut)`. 0=pure dependency, 1=pure dependent. |
13+
| **coupling** | number | 0-1 | derived | `fanOut / (max(fanIn, 1) + fanOut)`. 0=pure dependency. Leaf consumers (fan_in=0) score < 1.0. |
1414
| **tension** | number | 0-1 | entropy | Evenness of pulls from multiple modules. >0.3 = tension. |
1515
| **isBridge** | boolean | - | derived | `betweenness > 0.1`. Bridges separate clusters. |
1616
| **churn** | number | 0-N | git log | Number of commits touching this file. 0 if not a git repo. |
@@ -19,6 +19,7 @@ All metrics are computed per-file and stored in `FileMetrics`. Module-level aggr
1919
| **deadExports** | string[] | - | cross-ref | Export names not consumed by any edge in the graph. |
2020
| **hasTests** | boolean | - | filename match | Whether a matching `.test.ts`/`.spec.ts`/`__tests__/` file exists. |
2121
| **testFile** | string | - | filename match | Relative path to the test file, if found. |
22+
| **isTestFile** | boolean | - | parser | Whether this file IS a test file (`.test.`/`.spec.`/`__tests__/`). Used to filter test files from coverage and coupling hotspots. |
2223

2324
## Module Metrics
2425

specs/history.log

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
2026-03-02 | shipped | mcp-parity-readme-sync | 3h→2h | 1d | 100% MCP-REST parity: +2 tools, enhanced 3 tools, 15 tool descriptions, README sync, 21 new tests
33
2026-03-11 | shipped | fix-dead-export-false-positives | 2h→1.5h | 1d | Fix 33% false positive rate: merge duplicate imports, include same-file calls, call graph consumption. 8 regression tests.
44
2026-03-11 | shipped | fix-error-handling | 1h→0.5h | 1d | Consistent impact_analysis error handling, LOC off-by-one fix, empty file guard. 17 regression tests.
5+
2026-03-11 | shipped | fix-metrics-test-files | 2h→1.5h | 1d | Exclude test files from coverage/coupling metrics, isTestFile detection, coupling formula fix. 19 regression tests.

specs/shipped/2026-03-11-fix-metrics-test-files.md

Lines changed: 301 additions & 0 deletions
Large diffs are not rendered by default.

src/analyzer/index.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ describe("analyzeGraph", () => {
9090
expect(metricsD?.fanOut).toBe(0);
9191
});
9292

93-
it("computes coupling = fanOut / (fanIn + fanOut)", () => {
93+
it("computes coupling = fanOut / (max(fanIn, 1) + fanOut)", () => {
9494
const files = [
9595
makeFile("a.ts", { imports: [imp("b.ts"), imp("c.ts")] }),
9696
makeFile("b.ts"),
@@ -99,8 +99,8 @@ describe("analyzeGraph", () => {
9999
const built = buildGraph(files);
100100
const result = analyzeGraph(built);
101101

102-
// a.ts: fanIn=0, fanOut=2 → coupling = 2/(0+2) = 1.0
103-
expect(result.fileMetrics.get("a.ts")?.coupling).toBe(1);
102+
// a.ts: fanIn=0, fanOut=2 → coupling = 2/(max(0,1)+2) = 2/3 ≈ 0.667
103+
expect(result.fileMetrics.get("a.ts")?.coupling).toBeCloseTo(2 / 3, 5);
104104

105105
// b.ts: fanIn=1, fanOut=0 → coupling = 0/(1+0) = 0
106106
expect(result.fileMetrics.get("b.ts")?.coupling).toBe(0);

src/analyzer/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function analyzeGraph(built: BuiltGraph, parsedFiles?: ParsedFile[]): Cod
6363
for (const node of fileNodes) {
6464
const fanIn = graph.inDegree(node.id);
6565
const fanOut = graph.outDegree(node.id);
66-
const coupling = fanOut === 0 && fanIn === 0 ? 0 : fanOut / (fanIn + fanOut);
66+
const coupling = fanOut === 0 && fanIn === 0 ? 0 : fanOut / (Math.max(fanIn, 1) + fanOut);
6767
const pr = pageRanks.get(node.id) ?? 0;
6868
const btwn = betweennessScores.get(node.id) ?? 0;
6969

@@ -94,6 +94,7 @@ export function analyzeGraph(built: BuiltGraph, parsedFiles?: ParsedFile[]): Cod
9494
deadExports,
9595
hasTests: parsed?.testFile !== undefined,
9696
testFile: parsed?.testFile ?? "",
97+
isTestFile: parsed?.isTestFile ?? false,
9798
});
9899
}
99100

src/mcp/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,10 @@ export function registerTools(server: McpServer, graph: CodebaseGraph): void {
204204
});
205205
}
206206
} else {
207+
const filterTestFiles = metric === "coverage" || metric === "coupling";
207208
for (const [filePath, metrics] of graph.fileMetrics) {
209+
if (filterTestFiles && metrics.isTestFile) continue;
210+
208211
let score: number;
209212
let reason: string;
210213

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export interface FileMetrics {
9595
deadExports: string[];
9696
hasTests: boolean;
9797
testFile: string;
98+
isTestFile: boolean;
9899
}
99100

100101
export interface ModuleMetrics {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { describe, it, expect } from "vitest";
2+
import { UserService } from "./user-service.js";
3+
4+
describe("UserService", () => {
5+
it("should create an instance", () => {
6+
const service = new UserService();
7+
expect(service).toBeDefined();
8+
});
9+
});

0 commit comments

Comments
 (0)