Skip to content

Commit d9351c5

Browse files
msoginclaude
andcommitted
feat(timeline): P95 filter on Avg Lines Changed / PR (GLOOK-10)
Mirrors the existing P95 filter on the Lines Changed chart, but at the PR level: compute each PR's total lines across all weeks, derive the 95th-percentile threshold across distinct PRs, and exclude PRs above the threshold from the per-week avgLinesPerPr calculation. The PRs / Week count chart is unaffected — it still shows the raw PR count, so the user can see all activity. Only the size-signal chart gets smoothed. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent bbec619 commit d9351c5

2 files changed

Lines changed: 36 additions & 5 deletions

File tree

src/lib/__tests__/unit/timeline.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,16 @@ describe('aggregateWeekly — avgLinesPerPr field', () => {
109109
]);
110110
expect(out[0].avgLinesPerPr).toBe(10);
111111
});
112+
113+
it('excludes outlier PRs above P95 from the average', () => {
114+
const rows: any[] = [];
115+
for (let i = 1; i <= 20; i++) {
116+
rows.push(row({ pr_number: i, lines_added: 50, lines_removed: 0 }));
117+
}
118+
// One huge PR — above P95 of the 21-PR population, should be excluded from the avg.
119+
rows.push(row({ pr_number: 999, lines_added: 10000, lines_removed: 0 }));
120+
const out = aggregateWeekly(rows);
121+
expect(out[0].prs).toBe(21);
122+
expect(out[0].avgLinesPerPr).toBe(50);
123+
});
112124
});

src/lib/report/timeline.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@ export function aggregateWeekly(commits: any[], opts?: { trackDevs?: boolean }):
5050
? commitLineTotals[Math.floor(commitLineTotals.length * 0.95)]
5151
: Infinity;
5252

53+
// Total lines per PR across all weeks — used to apply a P95 outlier filter to the
54+
// per-week avgLinesPerPr so one giant refactor PR can't dominate the chart.
55+
const prLineTotals = new Map<string, number>();
56+
for (const c of commits) {
57+
if (c.pr_number == null || !c.committed_at) continue;
58+
const key = String(c.pr_number);
59+
prLineTotals.set(key, (prLineTotals.get(key) ?? 0) + (Number(c.lines_added) || 0) + (Number(c.lines_removed) || 0));
60+
}
61+
const sortedPrTotals = [...prLineTotals.values()].sort((a, b) => a - b);
62+
const prP95Threshold = sortedPrTotals.length > 0
63+
? sortedPrTotals[Math.floor(sortedPrTotals.length * 0.95)]
64+
: Infinity;
65+
5366
const weeklyMap = new Map<string, {
5467
week: string;
5568
commits: number;
@@ -63,7 +76,8 @@ export function aggregateWeekly(commits: any[], opts?: { trackDevs?: boolean }):
6376
types: Record<string, number>;
6477
activeDevs: Set<string>;
6578
prNumbers: Set<string>;
66-
prLines: number;
79+
prNumbersP95: Set<string>;
80+
prLinesP95: number;
6781
}>();
6882

6983
for (const c of commits) {
@@ -78,7 +92,7 @@ export function aggregateWeekly(commits: any[], opts?: { trackDevs?: boolean }):
7892
linesP95Added: 0, linesP95Removed: 0,
7993
totalComplexity: 0, complexityCount: 0, aiCount: 0,
8094
types: {}, activeDevs: new Set(), prNumbers: new Set(),
81-
prLines: 0,
95+
prNumbersP95: new Set(), prLinesP95: 0,
8296
});
8397
}
8498
const w = weeklyMap.get(weekKey)!;
@@ -100,8 +114,13 @@ export function aggregateWeekly(commits: any[], opts?: { trackDevs?: boolean }):
100114
if (c.type) w.types[c.type] = (w.types[c.type] || 0) + 1;
101115
if (c.github_login) w.activeDevs.add(c.github_login);
102116
if (c.pr_number != null) {
103-
w.prNumbers.add(String(c.pr_number));
104-
w.prLines += la + lr;
117+
const key = String(c.pr_number);
118+
w.prNumbers.add(key);
119+
// Exclude PRs whose total size exceeds P95 from the avgLinesPerPr signal.
120+
if ((prLineTotals.get(key) ?? 0) <= prP95Threshold) {
121+
w.prNumbersP95.add(key);
122+
w.prLinesP95 += la + lr;
123+
}
105124
}
106125
}
107126

@@ -112,7 +131,7 @@ export function aggregateWeekly(commits: any[], opts?: { trackDevs?: boolean }):
112131
week: w.week,
113132
commits: w.commits,
114133
prs: w.prNumbers.size,
115-
avgLinesPerPr: w.prNumbers.size > 0 ? Math.round(w.prLines / w.prNumbers.size) : 0,
134+
avgLinesPerPr: w.prNumbersP95.size > 0 ? Math.round(w.prLinesP95 / w.prNumbersP95.size) : 0,
116135
linesAdded: w.linesAdded,
117136
linesRemoved: w.linesRemoved,
118137
linesP95Added: w.linesP95Added,

0 commit comments

Comments
 (0)