Skip to content

Commit ed79359

Browse files
author
github-actions
committed
feat: introduce core code analysis capabilities including coupling, knowledge, hotspots, rot, compass, and churn, along with supporting date and file utilities.
1 parent 622f3c6 commit ed79359

10 files changed

Lines changed: 70 additions & 66 deletions

File tree

packages/core/src/analyzers/churn.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,14 @@
11
import type { RawCommit, AnalysisWindow, ChurnDataPoint } from "../types.js";
2+
import { getWindowCutoff } from "../utils/index.js";
23

34
/**
4-
* Calculates the cutoff date based on the analysis window.
5+
* Analyzes code churn (additions/deletions) over time.
56
*/
6-
function getWindowCutoffForChurn(window: AnalysisWindow): Date {
7-
if (window === "all") return new Date(0);
8-
const now = new Date();
9-
const days = { "7d": 7, "30d": 30, "90d": 90, "1y": 365 }[window];
10-
now.setDate(now.getDate() - days);
11-
return now;
12-
}
13-
14-
/**
15-
* Analyzes code churn (lines added and removed) over time.
16-
*/
17-
export function analyzeChurn(commits: RawCommit[], window: AnalysisWindow = "30d"): ChurnDataPoint[] {
18-
const cutoff = getWindowCutoffForChurn(window);
7+
export function analyzeChurn(
8+
commits: RawCommit[],
9+
window: AnalysisWindow = "30d"
10+
): ChurnDataPoint[] {
11+
const cutoff = getWindowCutoff(window);
1912
const filtered = commits.filter((c) => c.date >= cutoff);
2013
const churnMap = new Map<string, { added: number; removed: number; commits: number }>();
2114

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { RawCommit, CompassEntry } from "../types.js";
2+
import { extractFilesFromDiff } from "../utils/index.js";
23

34
/**
45
* Maps onboarding file priority based on centrality and developer touchpoints.
@@ -8,13 +9,11 @@ export function analyzeCompass(commits: RawCommit[]): CompassEntry[] {
89
const fileTouchpoints = new Map<string, Set<string>>();
910

1011
for (const commit of commits) {
11-
const diff = commit.diff as { files?: Array<{ file: string }> } | null;
12-
if (diff?.files) {
13-
for (const f of diff.files) {
14-
const authors = fileTouchpoints.get(f.file) ?? new Set<string>();
15-
authors.add(commit.author);
16-
fileTouchpoints.set(f.file, authors);
17-
}
12+
const files = extractFilesFromDiff(commit.diff);
13+
for (const file of files) {
14+
const authors = fileTouchpoints.get(file) ?? new Set<string>();
15+
authors.add(commit.author);
16+
fileTouchpoints.set(file, authors);
1817
}
1918
}
2019

@@ -24,11 +23,12 @@ export function analyzeCompass(commits: RawCommit[]): CompassEntry[] {
2423
priority: 1, // To be determined by deeper logic
2524
reason: `Touched by ${authors.size} unique contributors`,
2625
changeCount: commits.filter((c) => {
27-
const d = c.diff as { files?: Array<{ file: string }> } | null;
28-
return d?.files?.some((f) => f.file === path);
26+
const files = extractFilesFromDiff(c.diff);
27+
return files.includes(path);
2928
}).length,
3029
type: "core" as const,
3130
}))
3231
.sort((a, b) => b.changeCount - a.changeCount)
3332
.slice(0, 10);
3433
}
34+

packages/core/src/analyzers/coupling.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { RawCommit, CouplingLink } from "../types.js";
2+
import { extractFilesFromDiff } from "../utils/index.js";
3+
24

35
/**
46
* Identifies "Temporal Coupling": files that consistently change together.
@@ -53,8 +55,3 @@ export function analyzeCoupling(commits: RawCommit[]): CouplingLink[] {
5355
return results.sort((a, b) => b.coupling - a.coupling);
5456
}
5557

56-
function extractFilesFromDiff(diff: any): string[] {
57-
if (!diff || typeof diff !== "object") return [];
58-
const diffObj = diff as { files?: Array<{ file: string }> };
59-
return diffObj.files?.map((f) => f.file) ?? [];
60-
}

packages/core/src/analyzers/hotspot.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,7 @@
11
import type { RawCommit, HotspotFile, AnalysisWindow } from "../types.js";
22

3-
/**
4-
* Calculates the cutoff date based on the analysis window.
5-
*/
6-
function getWindowCutoff(window: AnalysisWindow): Date {
7-
if (window === "all") return new Date(0);
8-
const now = new Date();
9-
const days = { "7d": 7, "30d": 30, "90d": 90, "1y": 365 }[window];
10-
now.setDate(now.getDate() - days);
11-
return now;
12-
}
13-
14-
15-
/**
16-
* Extracts changed file paths from a commit's diff metadata.
17-
*/
18-
function extractFilesFromDiff(diff: any): string[] {
19-
if (!diff || typeof diff !== "object") return [];
20-
const diffObj = diff as { files?: Array<{ file: string }> };
21-
return diffObj.files?.map((f) => f.file) ?? [];
22-
}
3+
import { getWindowCutoff } from "../utils/index.js";
4+
import { extractFilesFromDiff } from "../utils/index.js";
235

246
/**
257
* Identifies hotspots in a repository based on change frequency and author diversity.
@@ -63,3 +45,4 @@ export function analyzeHotspots(
6345
}))
6446
.sort((a, b) => b.changeCount - a.changeCount);
6547
}
48+

packages/core/src/analyzers/impact.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { RawCommit, FileImpact } from "../types.js";
2+
import { extractFilesFromDiff } from "../utils/index.js";
23

34
/**
45
* Calculates "Blast Radius": the average number of files affected when a specific file is changed.
@@ -30,8 +31,3 @@ export function analyzeImpact(commits: RawCommit[]): FileImpact[] {
3031
.sort((a, b) => b.blastRadius - a.blastRadius);
3132
}
3233

33-
function extractFilesFromDiff(diff: any): string[] {
34-
if (!diff || typeof diff !== "object") return [];
35-
const diffObj = diff as { files?: Array<{ file: string }> };
36-
return diffObj.files?.map((f) => f.file) ?? [];
37-
}

packages/core/src/analyzers/knowledge.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { RawCommit, KnowledgeSilo } from "../types.js";
2+
import { extractFilesFromDiff } from "../utils/index.js";
3+
24

35
/**
46
* Identifies knowledge silos where a single person owns the vast majority of a file's history.
@@ -8,6 +10,7 @@ export function analyzeKnowledge(commits: RawCommit[]): KnowledgeSilo[] {
810

911
for (const commit of commits) {
1012
const files = extractFilesFromDiff(commit.diff);
13+
1114
for (const file of files) {
1215
const authorMap = fileAuthorMap.get(file) ?? new Map<string, number>();
1316
authorMap.set(commit.author, (authorMap.get(commit.author) || 0) + 1);
@@ -50,8 +53,3 @@ export function analyzeKnowledge(commits: RawCommit[]): KnowledgeSilo[] {
5053
return results.sort((a, b) => b.authorshipPercent - a.authorshipPercent);
5154
}
5255

53-
function extractFilesFromDiff(diff: any): string[] {
54-
if (!diff || typeof diff !== "object") return [];
55-
const diffObj = diff as { files?: Array<{ file: string }> };
56-
return diffObj.files?.map((f) => f.file) ?? [];
57-
}

packages/core/src/analyzers/rot.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { RawCommit, CompassEntry } from "../types.js";
1+
import type { RawCommit } from "../types.js";
2+
import { extractFilesFromDiff } from "../utils/index.js";
23

34
/**
45
* Identifies "Abandoned Code" or "Rot": files that haven't been touched in a long time.
@@ -27,8 +28,3 @@ export function analyzeRot(commits: RawCommit[]): string[] {
2728
.map(([path]) => path);
2829
}
2930

30-
function extractFilesFromDiff(diff: any): string[] {
31-
if (!diff || typeof diff !== "object") return [];
32-
const diffObj = diff as { files?: Array<{ file: string }> };
33-
return diffObj.files?.map((f) => f.file) ?? [];
34-
}

packages/core/src/utils/date.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { AnalysisWindow } from "../types.js";
2+
3+
/**
4+
* Calculates the cutoff date for a given analysis window.
5+
*/
6+
export function getWindowCutoff(window: AnalysisWindow): Date {
7+
if (window === "all") return new Date(0);
8+
9+
const now = new Date();
10+
const days = { "7d": 7, "30d": 30, "90d": 90, "1y": 365 };
11+
12+
// Type safety for dynamic lookup
13+
const dayCount = days[window as keyof typeof days] || 30;
14+
now.setDate(now.getDate() - dayCount);
15+
return now;
16+
}
17+
18+
/**
19+
* Calculates the difference in days between two dates.
20+
*/
21+
export function getDiffDays(d1: Date, d2: Date): number {
22+
return Math.abs(d1.getTime() - d2.getTime()) / (1000 * 60 * 60 * 24);
23+
}

packages/core/src/utils/file.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Extracts unique file paths from a simple-git diff object.
3+
*/
4+
export function extractFilesFromDiff(diff: unknown): string[] {
5+
if (!diff || typeof diff !== "object") return [];
6+
7+
const diffObj = diff as { files?: Array<{ file: string }> };
8+
if (!diffObj.files) return [];
9+
10+
return diffObj.files.map((f) => f.file);
11+
}
12+
13+
/**
14+
* Normalizes a file path.
15+
*/
16+
export function normalizePath(path: string): string {
17+
return path.replace(/\\/g, "/");
18+
}

packages/core/src/utils/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
// Utils module exports
2-
export {};
1+
export * from "./date.js";
2+
export * from "./file.js";

0 commit comments

Comments
 (0)