From 4ef32c27cb972fe21bdf71119a7213cd24f2e2e5 Mon Sep 17 00:00:00 2001 From: ozymandiashh <234437643+ozymandiashh@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:03:06 +0300 Subject: [PATCH] fix(optimize): exclude a young project's founding session from cost-outlier detection (#666) A project's first/bootstrap session is a different kind of work (one-off scaffolding), not an expensive instance of routine work. While a project is still young its peer sample is thin, so the leave-one-out average is dominated by a few routine sessions and the founding session's legitimately high one-off cost trips the >2x outlier multiplier. Exclude each young project's earliest costed session from the outlier finding only; mature projects are unaffected so genuine outliers still surface. Young = fewer than 2x MIN_SESSIONS_FOR_OUTLIER costed sessions. Fixes #664 --- src/optimize.ts | 30 +++++++++++++++++++- tests/optimize.test.ts | 62 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/optimize.ts b/src/optimize.ts index 8c59e0ec..86edce0b 100644 --- a/src/optimize.ts +++ b/src/optimize.ts @@ -82,6 +82,10 @@ const MCP_NEW_CONFIG_GRACE_MS = 24 * 60 * 60 * 1000 const BASH_DEFAULT_LIMIT = 30000 const BASH_RECOMMENDED_LIMIT = 15000 const MIN_SESSIONS_FOR_OUTLIER = 3 +// A project is still bootstrapping until it has twice the minimum sessions +// needed to evaluate outliers; below that, its peer average is too thin to +// distinguish founding work from waste. +const YOUNG_PROJECT_SESSION_LIMIT = 2 * MIN_SESSIONS_FOR_OUTLIER const SESSION_OUTLIER_MULTIPLIER = 2 const MIN_SESSION_OUTLIER_COST_USD = 1 const SESSION_OUTLIER_PREVIEW = 5 @@ -2267,6 +2271,29 @@ export function detectSessionOutliers(projects: ProjectSummary[], excludedSessio } } +function findYoungProjectFirstSessionIds(projects: ProjectSummary[]): Set { + const firstSessionIds = new Set() + + for (const project of projects) { + const costed = project.sessions.filter(s => s.totalCostUSD > 0) + if (costed.length >= YOUNG_PROJECT_SESSION_LIMIT) continue + + let firstSession: ProjectSummary['sessions'][number] | null = null + for (const session of costed) { + if ( + firstSession === null + || new Date(session.firstTimestamp).getTime() < new Date(firstSession.firstTimestamp).getTime() + ) { + firstSession = session + } + } + + if (firstSession) firstSessionIds.add(firstSession.sessionId) + } + + return firstSessionIds +} + // ============================================================================ // Scoring // ============================================================================ @@ -2391,7 +2418,8 @@ export async function scanAndDetect( .filter(c => !lowWorthSessionIds.has(c.sessionId)) .map(c => c.sessionId), ) - const outlierExclusions = new Set([...lowWorthSessionIds, ...contextBloatVisibleIds]) + const firstSessionIds = findYoungProjectFirstSessionIds(projects) + const outlierExclusions = new Set([...lowWorthSessionIds, ...contextBloatVisibleIds, ...firstSessionIds]) const syncDetectors: Array<() => WasteFinding | null> = [ () => detectCacheBloat(apiCalls, projects, dateRange), () => detectLowReadEditRatio(toolCalls), diff --git a/tests/optimize.test.ts b/tests/optimize.test.ts index 352cf3c9..d01c13a7 100644 --- a/tests/optimize.test.ts +++ b/tests/optimize.test.ts @@ -1,4 +1,15 @@ -import { describe, it, expect } from 'vitest' +import { describe, it, expect, vi } from 'vitest' + +vi.mock('../src/providers/index.js', async (importOriginal) => { + type ProvidersModule = typeof import('../src/providers/index.js') + const actual = await importOriginal() + return { + ...actual, + async discoverAllSessions() { + return [] + }, + } +}) import { detectJunkReads, @@ -10,6 +21,7 @@ import { detectCapabilityReliability, detectLowWorthSessions, detectSessionOutliers, + scanAndDetect, computeHealth, computeTrend, buildOptimizeJsonReport, @@ -61,6 +73,22 @@ function projectWithSessions(costs: number[], project = 'app'): ProjectSummary { } } +function projectWithDeliveredSessions(costs: number[], project = 'app'): ProjectSummary { + const summary = projectWithSessions(costs, project) + for (const session of summary.sessions) { + session.bashBreakdown = { 'git commit -m test': { calls: 1 } } + } + return summary +} + +function optimizeDateRange(day: number) { + const padded = String(day).padStart(2, '0') + return { + start: new Date(`2026-06-${padded}T00:00:00Z`), + end: new Date(`2026-06-${padded}T23:59:59Z`), + } +} + type TestSession = ProjectSummary['sessions'][number] function contextSession( @@ -978,6 +1006,38 @@ describe('detectSessionOutliers', () => { expect(finding).not.toBeNull() expect(finding!.explanation).toContain('app/s4') }) + + it('scanAndDetect excludes the earliest high-cost session while the project is young', async () => { + const result = await scanAndDetect( + [projectWithDeliveredSessions([20, 1, 1, 1])], + optimizeDateRange(1), + ) + + const finding = result.findings.find(f => f.id === 'cost-outliers') + expect(finding).toBeUndefined() + }) + + it('scanAndDetect still flags a later high-cost session while the project is young', async () => { + const result = await scanAndDetect( + [projectWithDeliveredSessions([1, 20, 1, 1])], + optimizeDateRange(2), + ) + + const finding = result.findings.find(f => f.id === 'cost-outliers') + expect(finding).toBeDefined() + expect(finding!.explanation).toContain('app/s2') + }) + + it('scanAndDetect still flags the earliest high-cost session once the project is mature', async () => { + const result = await scanAndDetect( + [projectWithDeliveredSessions([20, 1, 1, 1, 1, 1])], + optimizeDateRange(3), + ) + + const finding = result.findings.find(f => f.id === 'cost-outliers') + expect(finding).toBeDefined() + expect(finding!.explanation).toContain('app/s1') + }) }) describe('computeHealth', () => {