Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ FileMetrics {
betweenness: number
fanIn: number
fanOut: number
coupling: number // fanOut / (fanIn + fanOut)
coupling: number // fanOut / (max(fanIn, 1) + fanOut)
tension: number // Entropy of multi-module pulls
isBridge: boolean // betweenness > 0.1

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

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

## Module Metrics

Expand Down
1 change: 1 addition & 0 deletions specs/history.log
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
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
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.
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.
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.
301 changes: 301 additions & 0 deletions specs/shipped/2026-03-11-fix-metrics-test-files.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/analyzer/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ describe("analyzeGraph", () => {
expect(metricsD?.fanOut).toBe(0);
});

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

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

// b.ts: fanIn=1, fanOut=0 → coupling = 0/(1+0) = 0
expect(result.fileMetrics.get("b.ts")?.coupling).toBe(0);
Expand Down
3 changes: 2 additions & 1 deletion src/analyzer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function analyzeGraph(built: BuiltGraph, parsedFiles?: ParsedFile[]): Cod
for (const node of fileNodes) {
const fanIn = graph.inDegree(node.id);
const fanOut = graph.outDegree(node.id);
const coupling = fanOut === 0 && fanIn === 0 ? 0 : fanOut / (fanIn + fanOut);
const coupling = fanOut === 0 && fanIn === 0 ? 0 : fanOut / (Math.max(fanIn, 1) + fanOut);
const pr = pageRanks.get(node.id) ?? 0;
const btwn = betweennessScores.get(node.id) ?? 0;

Expand Down Expand Up @@ -94,6 +94,7 @@ export function analyzeGraph(built: BuiltGraph, parsedFiles?: ParsedFile[]): Cod
deadExports,
hasTests: parsed?.testFile !== undefined,
testFile: parsed?.testFile ?? "",
isTestFile: parsed?.isTestFile ?? false,
});
}

Expand Down
3 changes: 3 additions & 0 deletions src/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,10 @@ export function registerTools(server: McpServer, graph: CodebaseGraph): void {
});
}
} else {
const filterTestFiles = metric === "coverage" || metric === "coupling";
for (const [filePath, metrics] of graph.fileMetrics) {
if (filterTestFiles && metrics.isTestFile) continue;

let score: number;
let reason: string;

Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export interface FileMetrics {
deadExports: string[];
hasTests: boolean;
testFile: string;
isTestFile: boolean;
}

export interface ModuleMetrics {
Expand Down
9 changes: 9 additions & 0 deletions tests/fixture-codebase/src/users/user-service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { describe, it, expect } from "vitest";
import { UserService } from "./user-service.js";

describe("UserService", () => {
it("should create an instance", () => {
const service = new UserService();
expect(service).toBeDefined();
});
});
Loading
Loading