mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-22 06:54:26 +00:00
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
This commit is contained in:
parent
e2cba003a8
commit
4ef32c27cb
2 changed files with 90 additions and 2 deletions
|
|
@ -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<string> {
|
||||
const firstSessionIds = new Set<string>()
|
||||
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -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<ProvidersModule>()
|
||||
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', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue