(c: ComparisonResult)
| 321 | * Format a ComparisonResult as a readable string. |
| 322 | */ |
| 323 | export function formatComparison(c: ComparisonResult): string { |
| 324 | const lines: string[] = []; |
| 325 | const ts = c.before_timestamp ? c.before_timestamp.replace('T', ' ').slice(0, 16) : 'unknown'; |
| 326 | lines.push(`\nvs previous: ${c.before_branch}/${c.deltas.length ? 'eval' : ''} (${ts})`); |
| 327 | lines.push('─'.repeat(70)); |
| 328 | |
| 329 | // Per-test deltas |
| 330 | for (const d of c.deltas) { |
| 331 | const arrow = d.status_change === 'improved' ? '↑' : d.status_change === 'regressed' ? '↓' : '='; |
| 332 | const beforeStatus = d.before.passed ? 'PASS' : 'FAIL'; |
| 333 | const afterStatus = d.after.passed ? 'PASS' : 'FAIL'; |
| 334 | |
| 335 | // Turns delta |
| 336 | let turnsDelta = ''; |
| 337 | if (d.before.turns_used !== undefined && d.after.turns_used !== undefined) { |
| 338 | const td = d.after.turns_used - d.before.turns_used; |
| 339 | turnsDelta = ` ${d.before.turns_used}→${d.after.turns_used}t`; |
| 340 | if (td !== 0) turnsDelta += `(${td > 0 ? '+' : ''}${td})`; |
| 341 | } else if (d.after.turns_used !== undefined) { |
| 342 | turnsDelta = ` ${d.after.turns_used}t`; |
| 343 | } |
| 344 | |
| 345 | // Duration delta |
| 346 | let durDelta = ''; |
| 347 | if (d.before.duration_ms !== undefined && d.after.duration_ms !== undefined) { |
| 348 | const bs = Math.round(d.before.duration_ms / 1000); |
| 349 | const as = Math.round(d.after.duration_ms / 1000); |
| 350 | const dd = as - bs; |
| 351 | durDelta = ` ${bs}→${as}s`; |
| 352 | if (dd !== 0) durDelta += `(${dd > 0 ? '+' : ''}${dd})`; |
| 353 | } else if (d.after.duration_ms !== undefined) { |
| 354 | durDelta = ` ${Math.round(d.after.duration_ms / 1000)}s`; |
| 355 | } |
| 356 | |
| 357 | let detail = ''; |
| 358 | if (d.before.detection_rate !== undefined || d.after.detection_rate !== undefined) { |
| 359 | detail = ` ${d.before.detection_rate ?? '?'}→${d.after.detection_rate ?? '?'} det`; |
| 360 | } else { |
| 361 | const costBefore = d.before.cost_usd.toFixed(2); |
| 362 | const costAfter = d.after.cost_usd.toFixed(2); |
| 363 | detail = ` $${costBefore}→$${costAfter}`; |
| 364 | } |
| 365 | |
| 366 | const name = d.name.length > 30 ? d.name.slice(0, 27) + '...' : d.name.padEnd(30); |
| 367 | lines.push(` ${name} ${beforeStatus.padEnd(5)} → ${afterStatus.padEnd(5)} ${arrow}${detail}${turnsDelta}${durDelta}`); |
| 368 | } |
| 369 | |
| 370 | lines.push('─'.repeat(70)); |
| 371 | |
| 372 | // Totals |
| 373 | const parts: string[] = []; |
| 374 | if (c.improved > 0) parts.push(`${c.improved} improved`); |
| 375 | if (c.regressed > 0) parts.push(`${c.regressed} regressed`); |
| 376 | if (c.unchanged > 0) parts.push(`${c.unchanged} unchanged`); |
| 377 | lines.push(` Status: ${parts.join(', ')}`); |
| 378 | |
| 379 | const costSign = c.total_cost_delta >= 0 ? '+' : ''; |
| 380 | lines.push(` Cost: ${costSign}$${c.total_cost_delta.toFixed(2)}`); |
no test coverage detected