mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-21 06:24:32 +00:00
optimize: per-group subtotals in every finding render
Each class header now carries its own token/dollar subtotal and finding count, so the apply-able slice is never mistaken for the whole board; the headline savings line names that slice explicitly. CLI and TUI share one classHeaderLine helper, the desktop app reads the same numbers from the new summary.byClass in --format json (add-only; the three subtotals sum to findingCount and potentialSavingsTokens). Also scopes the SHELL_PROFILE_SCOPE comment to what is actually true: the MCP deferral plans refuse to rewrite a shell profile, but bash-output-cap appends its own marker block to one.
This commit is contained in:
parent
6267c49c25
commit
5660909801
7 changed files with 82 additions and 12 deletions
|
|
@ -139,6 +139,11 @@ function installDefaultMocks() {
|
|||
healthScore: 100, healthGrade: 'A', findingCount: 0, periodCostUSD: 0,
|
||||
sessions: 0, calls: 0, potentialSavingsTokens: 0, potentialSavingsCostUSD: 0,
|
||||
potentialSavingsPercent: 0, costRateUSD: 0, measuredSavingsUSD: 0,
|
||||
byClass: {
|
||||
fix: { tokensSaved: 0, savingsUSD: 0, count: 0 },
|
||||
nudge: { tokensSaved: 0, savingsUSD: 0, count: 0 },
|
||||
keep: { tokensSaved: 0, savingsUSD: 0, count: 0 },
|
||||
},
|
||||
},
|
||||
findings: [],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -423,6 +423,7 @@ export type OptimizeJsonReport = {
|
|||
potentialSavingsPercent: number | null
|
||||
costRateUSD: number
|
||||
measuredSavingsUSD: number
|
||||
byClass: Record<FindingClass, { tokensSaved: number; savingsUSD: number; count: number }>
|
||||
}
|
||||
findings: Array<{
|
||||
id: string
|
||||
|
|
|
|||
|
|
@ -49,6 +49,11 @@ function makeOptimizeReport(): OptimizeJsonReport {
|
|||
sessions: 88, calls: 1220, potentialSavingsTokens: 184_000,
|
||||
potentialSavingsCostUSD: 94.4, potentialSavingsPercent: 15.4, costRateUSD: 0.0005,
|
||||
measuredSavingsUSD: 27.8,
|
||||
byClass: {
|
||||
fix: { tokensSaved: 18_200, savingsUSD: 9.1, count: 1 },
|
||||
nudge: { tokensSaved: 17_400, savingsUSD: 8.7, count: 1 },
|
||||
keep: { tokensSaved: 4_800, savingsUSD: 2.4, count: 1 },
|
||||
},
|
||||
},
|
||||
findings: [
|
||||
{
|
||||
|
|
@ -129,7 +134,11 @@ describe('Optimize', () => {
|
|||
|
||||
await screen.findByText('Opus is doing your small talk')
|
||||
const groups = document.querySelectorAll('.opt-group')
|
||||
expect([...groups].map(g => g.textContent)).toEqual(['Fix now (apply-able)', 'Habits', 'FYI'])
|
||||
expect([...groups].map(g => g.textContent)).toEqual([
|
||||
'Fix now (apply-able) · 18.2K tokens · $9.10 · 1 finding',
|
||||
'Habits · 17.4K tokens · $8.70 · 1 finding',
|
||||
'FYI · 4.8K tokens · $2.40 · 1 finding',
|
||||
])
|
||||
})
|
||||
|
||||
it('renders tabs and actionable Waste findings with impact, savings, explanation, and copy-paste fix', async () => {
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ function WasteRows({ report }: { report: Polled<OptimizeJsonReport> }) {
|
|||
<div className="opt-summary">
|
||||
{report.data.summary.findingCount.toLocaleString('en-US')} findings · {formatUsd(report.data.summary.potentialSavingsCostUSD)} potential · health {report.data.summary.healthScore}/100
|
||||
</div>
|
||||
<ActionableFindingRows findings={report.data.findings} />
|
||||
<ActionableFindingRows findings={report.data.findings} byClass={report.data.summary.byClass} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -124,7 +124,7 @@ function actionText(fix: WasteAction): string {
|
|||
return fix.type === 'file-content' ? fix.content : fix.text
|
||||
}
|
||||
|
||||
function ActionableFindingRows({ findings }: { findings: OptimizeFinding[] }) {
|
||||
function ActionableFindingRows({ findings, byClass }: { findings: OptimizeFinding[]; byClass: OptimizeJsonReport['summary']['byClass'] }) {
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null)
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null)
|
||||
|
||||
|
|
@ -145,7 +145,11 @@ function ActionableFindingRows({ findings }: { findings: OptimizeFinding[] }) {
|
|||
const showHeader = finding.class !== findings[i - 1]?.class
|
||||
return (
|
||||
<Fragment key={finding.id}>
|
||||
{showHeader && <div className="opt-group">{CLASS_HEADERS[finding.class]}</div>}
|
||||
{showHeader && (
|
||||
<div className="opt-group">
|
||||
{CLASS_HEADERS[finding.class]} · {formatCompact(byClass[finding.class].tokensSaved)} tokens · {formatUsd(byClass[finding.class].savingsUSD)} · {byClass[finding.class].count} {byClass[finding.class].count === 1 ? 'finding' : 'findings'}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className="opt-finding opt-finding-toggle"
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { findUnpricedModels, isExpectedFreeModel, loadPricing } from './models.j
|
|||
import { aggregateModelTotals } from './model-breakdown.js'
|
||||
import { buildDurablePeriod } from './usage-aggregator.js'
|
||||
import { getAllProviders } from './providers/index.js'
|
||||
import { CLASS_HEADERS, findingBasis, findingClass, scanAndDetect, type FindingClass, type WasteFinding, type WasteAction, type OptimizeResult } from './optimize.js'
|
||||
import { classHeaderLine, classTotals, findingBasis, findingClass, scanAndDetect, type FindingClass, type WasteFinding, type WasteAction, type OptimizeResult } from './optimize.js'
|
||||
import { aggregateFileChurn, buildCoachingNotes, computePricingCoverage, medianTimeToFirstEditMs, scanUserCorrections, worstOneShotCategory, type ReworkedFile } from './workflow-insights.js'
|
||||
import { estimateContextBudget, type ContextBudget } from './context-budget.js'
|
||||
import { dateKey } from './day-aggregator.js'
|
||||
|
|
@ -1105,6 +1105,7 @@ function OptimizeView({ findings, costRate, projects, label, width, healthScore,
|
|||
const start = total === 0 ? 0 : Math.min(cursor, Math.max(0, total - FINDINGS_WINDOW_SIZE))
|
||||
const end = Math.min(start + FINDINGS_WINDOW_SIZE, total)
|
||||
const visible = findings.slice(start, end)
|
||||
const totals = classTotals(findings, costRate)
|
||||
return (
|
||||
<Box flexDirection="column" width={width}>
|
||||
<Box flexDirection="column" borderStyle="round" borderColor={ORANGE} paddingX={1} width={width}>
|
||||
|
|
@ -1126,7 +1127,7 @@ function OptimizeView({ findings, costRate, projects, label, width, healthScore,
|
|||
const previous: FindingClass | null = i > 0 ? findingClass(visible[i - 1]!) : null
|
||||
return (
|
||||
<Fragment key={start + i}>
|
||||
{cls !== previous && <Box paddingX={1} width={width}><Text bold color={ORANGE}>{CLASS_HEADERS[cls]}</Text></Box>}
|
||||
{cls !== previous && <Box paddingX={1} width={width}><Text bold color={ORANGE} wrap="truncate-end">{classHeaderLine(cls, totals[cls], costRate)}</Text></Box>}
|
||||
<FindingPanel index={start + i + 1} finding={f} costRate={costRate} width={width} />
|
||||
</Fragment>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -341,8 +341,10 @@ export const FINDING_BASIS: Record<FindingId, FindingBasis> = {
|
|||
'unused-commands': 'estimated', // count x TOKENS_PER_COMMAND_DEF
|
||||
}
|
||||
|
||||
/// Scope label for a setting that lives in ~/.zshrc / ~/.bashrc. Plans never
|
||||
/// rewrite shell profiles, they only report them.
|
||||
/// Scope label for a setting that lives in ~/.zshrc / ~/.bashrc. The MCP
|
||||
/// deferral plans (defer-enable, defer-threshold) refuse to rewrite an
|
||||
/// override found there and report it instead; bash-output-cap does append
|
||||
/// its own marker block to the shell rc.
|
||||
export const SHELL_PROFILE_SCOPE = 'shell profile'
|
||||
|
||||
export function findingClass(f: WasteFinding): FindingClass {
|
||||
|
|
@ -366,11 +368,36 @@ export function findingBasis(f: WasteFinding): FindingBasis {
|
|||
const CLASS_ORDER: Record<FindingClass, number> = { fix: 0, nudge: 1, keep: 2 }
|
||||
|
||||
export const CLASS_HEADERS: Record<FindingClass, string> = {
|
||||
fix: 'Fix now (apply-able) — codeburn optimize --apply',
|
||||
fix: 'Fix now (apply-able)',
|
||||
nudge: 'Habits',
|
||||
keep: 'FYI',
|
||||
}
|
||||
|
||||
export type ClassTotals = { tokensSaved: number; savingsUSD: number; count: number }
|
||||
|
||||
export function classTotals(findings: WasteFinding[], costRate: number): Record<FindingClass, ClassTotals> {
|
||||
const totals: Record<FindingClass, ClassTotals> = {
|
||||
fix: { tokensSaved: 0, savingsUSD: 0, count: 0 },
|
||||
nudge: { tokensSaved: 0, savingsUSD: 0, count: 0 },
|
||||
keep: { tokensSaved: 0, savingsUSD: 0, count: 0 },
|
||||
}
|
||||
for (const f of findings) {
|
||||
const t = totals[findingClass(f)]
|
||||
t.tokensSaved += f.tokensSaved
|
||||
t.savingsUSD += f.tokensSaved * costRate
|
||||
t.count++
|
||||
}
|
||||
return totals
|
||||
}
|
||||
|
||||
/// Group header with its own subtotal, shared by the CLI and the TUI so the
|
||||
/// two never drift apart.
|
||||
export function classHeaderLine(cls: FindingClass, totals: ClassTotals, costRate: number): string {
|
||||
const cost = costRate > 0 ? ` (~${formatCost(totals.savingsUSD)})` : ''
|
||||
const suffix = cls === 'fix' ? ' — codeburn optimize --apply' : ''
|
||||
return `${CLASS_HEADERS[cls]} · ~${formatTokens(totals.tokensSaved)} tokens${cost} · ${totals.count} finding${totals.count === 1 ? '' : 's'}${suffix}`
|
||||
}
|
||||
|
||||
// Cause taxonomy for defer-enable plans (mcp-deferral-off findings).
|
||||
// 'proxy-verified' is never produced by the detector today: it is reserved
|
||||
// for the #614 part-3 proxy verifier, which upgrades 'proxy-unknown' once a
|
||||
|
|
@ -443,6 +470,9 @@ export type OptimizeJsonReport = {
|
|||
/// Portion of `potentialSavingsCostUSD` coming from `measured`-basis
|
||||
/// findings. The total keeps its old meaning: measured plus estimated.
|
||||
measuredSavingsUSD: number
|
||||
/// Per-class subtotals; the three counts and token sums add up to
|
||||
/// `findingCount` and `potentialSavingsTokens`.
|
||||
byClass: Record<FindingClass, ClassTotals>
|
||||
}
|
||||
findings: Array<{
|
||||
id: FindingId
|
||||
|
|
@ -3385,8 +3415,12 @@ export function renderOptimize(
|
|||
const pctRaw = periodCost > 0 ? (totalCost / periodCost) * 100 : 0
|
||||
const pct = pctRaw >= 1 ? pctRaw.toFixed(0) : pctRaw.toFixed(1)
|
||||
|
||||
const totals = classTotals(findings, costRate)
|
||||
const costText = costRate > 0 ? ` (~${formatCost(totalCost)}, ~${pct}% of spend)` : ''
|
||||
lines.push(chalk.hex(GREEN)(` Potential savings: ~${formatTokens(totalTokens)} tokens${costText}`))
|
||||
// The headline is the whole board; name the apply-able slice separately so
|
||||
// it never reads as "what CodeBurn can fix for you".
|
||||
const applyable = costRate > 0 && totals.fix.count > 0 ? ` — apply-able: ~${formatCost(totals.fix.savingsUSD)}` : ''
|
||||
lines.push(chalk.hex(GREEN)(` Potential savings: ~${formatTokens(totalTokens)} tokens${costText}${applyable}`))
|
||||
lines.push('')
|
||||
|
||||
// One block per class, in fix -> nudge -> keep order; numbering runs
|
||||
|
|
@ -3395,7 +3429,7 @@ export function renderOptimize(
|
|||
for (const cls of ['fix', 'nudge', 'keep'] as const) {
|
||||
const group = findings.filter(f => findingClass(f) === cls)
|
||||
if (group.length === 0) continue
|
||||
lines.push(chalk.bold.hex(ORANGE)(` ${CLASS_HEADERS[cls]}`))
|
||||
lines.push(chalk.bold.hex(ORANGE)(` ${classHeaderLine(cls, totals[cls], costRate)}`))
|
||||
lines.push('')
|
||||
for (const f of group) {
|
||||
const appliedOn = previouslyApplied?.[f.id]
|
||||
|
|
@ -3507,6 +3541,7 @@ export function buildOptimizeJsonReport(
|
|||
measuredSavingsUSD: result.findings
|
||||
.filter(f => findingBasis(f) === 'measured')
|
||||
.reduce((s, f) => s + f.tokensSaved * result.costRate, 0),
|
||||
byClass: classTotals(result.findings, result.costRate),
|
||||
},
|
||||
findings: result.findings.map(f => ({
|
||||
id: f.id,
|
||||
|
|
|
|||
|
|
@ -1311,6 +1311,15 @@ describe('buildOptimizeJsonReport', () => {
|
|||
costRateUSD: 0.00002,
|
||||
})
|
||||
expect(report.summary.measuredSavingsUSD).toBe(0)
|
||||
expect(report.summary.byClass).toEqual({
|
||||
fix: { tokensSaved: 0, savingsUSD: 0, count: 0 },
|
||||
nudge: { tokensSaved: 50_000, savingsUSD: 1, count: 1 },
|
||||
keep: { tokensSaved: 0, savingsUSD: 0, count: 0 },
|
||||
})
|
||||
const classes = Object.values(report.summary.byClass)
|
||||
expect(classes.reduce((s, c) => s + c.tokensSaved, 0)).toBe(report.summary.potentialSavingsTokens)
|
||||
expect(classes.reduce((s, c) => s + c.savingsUSD, 0)).toBeCloseTo(report.summary.potentialSavingsCostUSD, 10)
|
||||
expect(classes.reduce((s, c) => s + c.count, 0)).toBe(report.summary.findingCount)
|
||||
expect(report.findings[0]).toMatchObject({
|
||||
title: 'Trim stale context',
|
||||
severity: 'medium',
|
||||
|
|
@ -1349,9 +1358,15 @@ describe('renderOptimize grouping', () => {
|
|||
]
|
||||
const out = plain(renderOptimize(findings, 0.00001, '7 Days', 10, 5, 100, 80, 'B', [], []))
|
||||
|
||||
const headers = ['Fix now (apply-able)', 'Habits', 'FYI'].map(h => out.indexOf(h))
|
||||
const headers = [
|
||||
'Fix now (apply-able) · ~1.0K tokens (~$0.010) · 1 finding — codeburn optimize --apply',
|
||||
'Habits · ~1.0K tokens (~$0.010) · 1 finding',
|
||||
'FYI · ~1.0K tokens (~$0.010) · 1 finding',
|
||||
].map(h => out.indexOf(h))
|
||||
expect(headers.every(i => i >= 0)).toBe(true)
|
||||
expect(headers).toEqual([...headers].sort((a, b) => a - b))
|
||||
// Headline is the whole board; the apply-able slice is named separately.
|
||||
expect(out).toContain('Potential savings: ~3.0K tokens (~$0.030, ~0.3% of spend) — apply-able: ~$0.010')
|
||||
expect(out).toContain('1. Cap bash output')
|
||||
expect(out).toContain('2. Trim CLAUDE.md')
|
||||
expect(out).toContain('3. Context-heavy sessions')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue