// --------------------------------------------------------------------------- // Render the eval run as a PR comment (markdown) or a console summary // (aligned plain text). Both formats are driven by: // // - MultiRunEvaluation — pass rates, build counts, per-trial reasoning // - ComparisonOutcome (optional) — tagged result of the baseline // comparison: `ok` (ran, has scenarios), `no_baseline` (skipped), or // `fetch_failed` / `self_baseline` (skipped for cause). Each kind // drives a distinct top-of-comment alert so a LangSmith outage doesn't // get dressed up as "no baseline configured". // // When no comparison is available (no baseline yet, LangSmith offline) // the renderers still produce a useful per-test-case summary. When a // comparison is available, sections render in priority order: // regressions, likely regressions, worth watching, improvements, // failure-category drift. Only sections with content are emitted. // --------------------------------------------------------------------------- import { expectationUnitKey, hardRegressions, improvements, scenarioUnitKey, softRegressions, unitKeyOf, watchList, type ComparisonOutcome, type ComparisonResult, type EvaluationUnitComparison, type FailureCategoryComparison, type UnitRef, } from './compare'; import type { GateCriterion, GateResult, GateUnit } from './gate'; import { aggregateWorkflowChecks } from '../binaryChecks/aggregate'; import { CHECK_DIMENSIONS } from '../binaryChecks/types'; import { countAggregatedUnitTrials, getAggregatedCaseUnits, getCaseRunStatus, getCaseRunStatusLabel, getCheckedRunCount, } from '../summary'; import type { MultiRunEvaluation, TestCaseAggregation, WorkflowTestCase } from '../types'; import { caseDisplayPrompt } from '../utils/conversation-text'; /** How to re-run this eval against a PR's latest commit (PR-open runs go stale * on new pushes); drives the comment's re-run instructions. */ export interface RerunHint { /** PR number to dispatch against (`-f pr=`). */ prNumber: string; /** GitHub Actions "Run workflow" page for the eval dispatch workflow. */ dispatchUrl: string; } interface FormatOptions { /** Optional commit SHA for the terminal heading. Truncated to 8 chars. */ commitSha?: string; /** When set, the comment shows how to re-run against the PR's latest commit. */ rerun?: RerunHint; /** Maps each test-case reference to its file slug. When provided, the * per-scenario failure breakdown looks up failed runs by * `${fileSlug}/${scenarioName}` — deterministic across collisions like * multiple `happy-path` scenarios. When omitted, the breakdown is * skipped (no name-only fallback — that lookup was wrong on real data). */ slugByTestCase?: Map; /** Absolute green-gate verdict for curated tiers. When set, the comment renders * the gate verdict in place of the baseline comparison. */ gate?: GateResult; /** Run-level pass@k / pass^k (terminal k = totalRuns) averaged over measured * units (scenarios + build expectations) — same numbers as * `summary.passAtK`/`summary.passHatK` in eval-results.json. */ passMetrics?: { passAtK: number; passHatK: number }; /** LangSmith experiment URL, when the run recorded one. */ experimentUrl?: string; } /** `_pass@k … · pass^k … · [LangSmith experiment](…)_` — everything optional. */ function formatRunMetaLine(totalRuns: number, options: FormatOptions): string | undefined { const parts: string[] = []; if (options.passMetrics) { const { passAtK, passHatK } = options.passMetrics; parts.push( `pass@${String(totalRuns)} ${(passAtK * 100).toFixed(1)}% · pass^${String(totalRuns)} ${(passHatK * 100).toFixed(1)}%`, ); } if (options.experimentUrl) { parts.push(`[LangSmith experiment](${options.experimentUrl})`); } return parts.length > 0 ? `_${parts.join(' · ')}_` : undefined; } function evaluatedBuildExpectations(tc: TestCaseAggregation) { return tc.buildExpectations.filter((ea) => ea.evaluatedCount > 0); } function aggregateMeasuredUnits(testCases: TestCaseAggregation[]) { let passed = 0; let total = 0; let scenarios = 0; let expectations = 0; for (const tc of testCases) { // Only evaluated runs count — verifier-incomplete runs carry no verdict. const evaluatedScenarios = tc.executionScenarios.filter((sa) => sa.evaluatedCount > 0); scenarios += evaluatedScenarios.length; for (const sa of evaluatedScenarios) { passed += sa.passCount; total += sa.evaluatedCount; } const buildExpectations = evaluatedBuildExpectations(tc); expectations += buildExpectations.length; for (const ea of buildExpectations) { passed += ea.passCount; total += ea.evaluatedCount; } } return { passed, total, scenarios, expectations }; } function unitCountLabel(summary: { scenarios: number; expectations: number }, totalRuns: number) { const scenarioLabel = `${summary.scenarios} scenario${summary.scenarios === 1 ? '' : 's'}`; const expectationLabel = summary.expectations > 0 ? ` + ${summary.expectations} expectation${summary.expectations === 1 ? '' : 's'}` : ''; return `${scenarioLabel}${expectationLabel}, N=${totalRuns}`; } /** Display label for a comparison unit — `file/scenario`, or the * `file :: expectation-text…` style used by the Failures section. */ function unitLabel(unit: UnitRef): string { return unit.kind === 'scenario' ? `${unit.testCaseFile}/${unit.name}` : `${unit.testCaseFile} :: ${unit.name.slice(0, 60)}`; } /** `${n} units (X scenarios + Y expectations)` — collapses to the legacy * `${n} scenarios` copy when no expectation units are present. */ function unitMixLabel(units: Array<{ kind: EvaluationUnitComparison['kind'] }>): string { const scenarios = units.filter((u) => u.kind === 'scenario').length; const expectations = units.length - scenarios; if (expectations === 0) return `${scenarios} scenario${scenarios === 1 ? '' : 's'}`; return `${units.length} units (${scenarios} scenario${scenarios === 1 ? '' : 's'} + ${expectations} expectation${expectations === 1 ? '' : 's'})`; } /** Sentence fragments describing units missing on one side of the comparison. * Expectations missing from the baseline get their own clause — the usual * cause is a baseline captured before expectation persistence, not case drift. */ function describePartialCoverage(comparison: ComparisonResult): string[] { const parts: string[] = []; if (comparison.baselineOnly.length > 0) { const label = comparison.baselineOnly.every((u) => u.kind === 'scenario') ? 'baseline scenarios' : 'baseline units'; parts.push(`${comparison.baselineOnly.length} ${label} not run by PR`); } const prOnlyScenarios = comparison.prOnly.filter((u) => u.kind === 'scenario').length; const prOnlyExpectations = comparison.prOnly.length - prOnlyScenarios; if (prOnlyScenarios > 0) { parts.push( `${prOnlyScenarios} PR scenarios have no baseline data (added since baseline captured)`, ); } if (prOnlyExpectations > 0) { parts.push( `${prOnlyExpectations} PR expectations have no baseline data (baseline predates expectation persistence)`, ); } return parts; } // --------------------------------------------------------------------------- // Markdown PR comment // --------------------------------------------------------------------------- export function formatComparisonMarkdown( evaluation: MultiRunEvaluation, outcome?: ComparisonOutcome, options: FormatOptions = {}, ): string { const lines: string[] = []; const gate = options.gate; const comparison = !gate && outcome?.kind === 'ok' ? outcome.result : undefined; lines.push(formatHeading()); lines.push(''); lines.push(renderRerunCallout(options.rerun)); lines.push(''); const runMetaLine = formatRunMetaLine(evaluation.totalRuns, options); if (gate) { lines.push(formatGateAlertMarkdown(gate)); lines.push(''); lines.push(...renderGateSummaryMarkdown(gate)); if (runMetaLine) { lines.push(runMetaLine); lines.push(''); } // Failures (full judge text) go right under the verdict in gate mode — the point // is to see what failed at a glance, not hunt for it at the bottom of the comment. lines.push(...renderFailureDetails(evaluation, options.slugByTestCase)); } else { lines.push(formatTopAlert(outcome)); lines.push(''); lines.push(formatAggregateBlock(evaluation, comparison)); if (runMetaLine) lines.push(runMetaLine); lines.push(''); } if (comparison) { const hard = hardRegressions(comparison); const soft = softRegressions(comparison); const watch = watchList(comparison); const imps = improvements(comparison); const renderedAnyTable = hard.length > 0 || soft.length > 0 || imps.length > 0; // Built once and reused across the regression-tier sections so each // scenario row can carry a collapsible breakdown of its failed PR runs. // Improvements skip the breakdown — they passed. Skipped entirely when // the caller didn't pass a slug map (lookup would be ambiguous). const failedIndex = options.slugByTestCase ? buildFailedRunsIndex(evaluation, options.slugByTestCase) : undefined; if (hard.length > 0) { lines.push(...renderUnitSection('Regressions', '— high-confidence', hard, true, failedIndex)); } if (soft.length > 0) { lines.push( ...renderUnitSection( 'Likely regressions', '— looser statistical flag, investigate if related to your changes', soft, true, failedIndex, ), ); } if (watch.length > 0) { lines.push( ...renderUnitSection( 'Worth watching', '— large change, not flagged as a regression', watch, false, failedIndex, ), ); } if (imps.length > 0) { lines.push(...renderUnitSection('Improvements', '', imps, true)); } if (renderedAnyTable) { lines.push( "_p = Fisher's exact one-sided p-value. Lower = stronger evidence of a real change._", ); lines.push(''); } // Always render the breakdown when comparison data is available — the // renderer drops 0/0 rows itself, so empty categories don't pollute // the output but the reader still sees the full taxonomy of what's // tracked. lines.push(...renderFailureCategorySection(comparison.failureCategories)); } lines.push(...renderPerTestCaseDetails(evaluation, options.slugByTestCase)); const workflowChecksSection = renderWorkflowChecksSection(evaluation); if (workflowChecksSection.length > 0) lines.push(...workflowChecksSection); if (comparison) { const otherFindings = renderOtherFindings(comparison); if (otherFindings.length > 0) lines.push(...otherFindings); } // Gate mode already rendered the Failures section under the verdict (above). const failureDetails = gate ? [] : renderFailureDetails(evaluation, options.slugByTestCase); if (failureDetails.length > 0) lines.push(...failureDetails); return lines.join('\n'); } // --------------------------------------------------------------------------- // Absolute green-gate verdict (curated tiers, no baseline) // --------------------------------------------------------------------------- function gateCriterionLabel(criterion: GateCriterion): string { switch (criterion.kind) { case 'passAtK': return 'pass@k = 100% (every unit passes at least once across k runs)'; case 'minAggregatePassRate': return `aggregate pass rate ≥ ${Math.round(criterion.minRate * 100)}%`; } } function gateFailuresCell(unit: GateUnit): string { if (!unit.failureCategories) return ''; return Object.entries(unit.failureCategories) .sort((a, b) => b[1] - a[1]) .map(([cat, n]) => `${n}× ${cat}`) .join(', '); } // A green unit that failed ≥1 run (e.g. 2/3) — passes the gate, still worth listing. function degradedUnits(gate: GateResult): GateUnit[] { return gate.units.filter((u) => u.green && u.passCount < u.total); } // A green unit that failed the *majority* of its runs (passed <50%, e.g. 1/3) — it // barely cleared pass@k, one bad run from red. Pass-rate based so it scales across k // (3 on PRs, 10 on baselines); a single flaky miss (2/3) is not "barely passed". function barelyPassedUnits(gate: GateResult): GateUnit[] { return gate.units.filter((u) => u.green && u.total > 0 && u.passCount / u.total < 0.5); } function pluralUnits(c: number): string { return `${c} unit${c === 1 ? '' : 's'}`; } function formatGateAlertMarkdown(gate: GateResult): string { const n = gate.units.length; const over = `over ${gate.totalRuns} run${gate.totalRuns === 1 ? '' : 's'}`; // Key off failing, not gate.green: under minAggregatePassRate gate.green can be // true via the pooled rate while individual units fail. if (gate.failing.length > 0) { return [ '> [!CAUTION]', `> 🔴 ${gate.failing.length} of ${pluralUnits(n)} not green ${over}.`, ].join('\n'); } // Zero measured units (e.g. verifier outage excluded everything): red, not clean. if (!gate.green) { return [ '> [!CAUTION]', `> 🔴 Gate has no measured units — ${gate.excluded.length} excluded with no verdict. Nothing was actually gated.`, ].join('\n'); } // All units pass@k. Only warn when a unit *barely* passed (failed most of its runs); // a single flaky miss stays green but is still listed in Failures below. const barely = barelyPassedUnits(gate).length; if (barely > 0) { return [ '> [!WARNING]', `> 🟡 All ${pluralUnits(n)} green ${over}, but ${barely} barely passed (failed most runs) — see Failures below.`, ].join('\n'); } const flaky = degradedUnits(gate).length; if (flaky > 0) { return [ '> [!TIP]', `> 🟢 All ${pluralUnits(n)} green ${over}; ${flaky} flaky — see Failures below.`, ].join('\n'); } return ['> [!TIP]', `> 🟢 All ${pluralUnits(n)} green ${over} (clean).`].join('\n'); } function renderGateSummaryMarkdown(gate: GateResult): string[] { const lines: string[] = []; const { passed, total, rate } = gate.aggregate; lines.push( `**Gate**: ${gateCriterionLabel(gate.criterion)} — ${(rate * 100).toFixed(1)}% pass (${passed}/${total} trials over ${gate.units.length} units · k=${gate.totalRuns})`, ); if (gate.excluded.length > 0) { lines.push(`_${gate.excluded.length} unit(s) not gated (no judge verdict)._`); } lines.push(''); // Failing / flaky units (with their full judge text) are rendered by the Failures // section below — not a per-unit table here. Build expectations carry no failure // category, which left the old table's Failures column blank. return lines; } function formatTerminalGateLine(gate: GateResult): string { const n = gate.units.length; const over = `over ${gate.totalRuns} run${gate.totalRuns === 1 ? '' : 's'}`; if (gate.failing.length > 0) { return `▶ GATE: ${gate.failing.length} of ${pluralUnits(n)} NOT green ${over}`; } if (!gate.green) { return `▶ GATE: NO measured units (${gate.excluded.length} excluded, no verdicts) — not green`; } const barely = barelyPassedUnits(gate).length; if (barely > 0) { return `▶ GATE: all ${pluralUnits(n)} green ${over}, ${barely} barely passed`; } const flaky = degradedUnits(gate).length; if (flaky > 0) { return `▶ GATE: all ${pluralUnits(n)} green ${over}, ${flaky} flaky`; } return `▶ GATE: all ${pluralUnits(n)} green ${over} (clean)`; } function formatTerminalGateSummary(gate: GateResult): string[] { const lines: string[] = []; const { passed, total, rate } = gate.aggregate; lines.push(TERMINAL_INDENT + `gate: ${gateCriterionLabel(gate.criterion)}`); lines.push( TERMINAL_INDENT + `aggregate: ${(rate * 100).toFixed(1)}% (${passed}/${total} trials, ${gate.units.length} units, k=${gate.totalRuns})`, ); if (gate.excluded.length > 0) { lines.push(TERMINAL_INDENT + `${gate.excluded.length} not gated (no judge verdict)`); } for (const u of gate.failing) { const cats = u.failureCategories ? ` [${gateFailuresCell(u)}]` : ''; lines.push(TERMINAL_INDENT + ` NOT GREEN ${u.slug} ${u.passCount}/${u.total}${cats}`); } for (const u of degradedUnits(gate)) { const cats = u.failureCategories ? ` [${gateFailuresCell(u)}]` : ''; const label = u.total > 0 && u.passCount / u.total < 0.5 ? 'BARELY ' : 'flaky '; lines.push(TERMINAL_INDENT + ` ${label} ${u.slug} ${u.passCount}/${u.total}${cats}`); } return lines; } // Evals fire on PR open/ready, not on push. The re-run is a self-seeded // dispatch keyed by PR number — it resolves the PR head at run time, so it // always tests the latest push (unlike GitHub's "Re-run jobs", which replays // the stale PR-open commit). function renderRerunCallout(rerun?: RerunHint): string { if (!rerun) { return [ '> [!IMPORTANT]', "> **This eval does not re-run on new commits.** Re-run it against the PR's latest commit by dispatching the **Instance AI Evals: PR Gate** workflow with your PR number.", ].join('\n'); } return [ '> [!IMPORTANT]', '> **This eval does not re-run on new commits.** To test your latest push, re-run it against the PR head:', '>', '> ```bash', `> gh workflow run ci-instance-ai-evals.yml -f pr=${rerun.prNumber}`, '> ```', `> …or use the [Run workflow button](${rerun.dispatchUrl}) and set **pr** = \`${rerun.prNumber}\`.`, ].join('\n'); } function renderWorkflowChecksSection(evaluation: MultiRunEvaluation): string[] { const aggregate = aggregateWorkflowChecks(evaluation); if (!aggregate) return []; const names = Object.keys(aggregate.perCheck).sort(); const rowByName: Record = {}; let failingChecks = 0; for (const name of names) { const entry = aggregate.perCheck[name]; const scored = entry.passes + entry.fails; const rate = scored > 0 ? `${String(Math.round((entry.passes / scored) * 100))}%` : '—'; if (entry.fails > 0) failingChecks++; rowByName[name] = { dimension: entry.dimension, failed: entry.fails > 0, row: `| \`${entry.dimension}\` | \`${name}\` | ${entry.kind} | ${String(entry.passes)} | ${String(entry.fails)} | ${String(entry.nA)} | ${String(entry.errors)} | ${rate} |`, }; } const header = [ '| Dimension | Check | Kind | Pass | Fail | N/A | Error | Pass rate |', '|---|---|---|---|---|---|---|---|', ]; const rowsWhere = (keep: (name: string) => boolean): string[] => { const byDimension: Record = {}; for (const name of names) { if (keep(name)) (byDimension[rowByName[name].dimension] ??= []).push(rowByName[name].row); } return CHECK_DIMENSIONS.flatMap((dim) => byDimension[dim] ?? []); }; const lines: string[] = [ '#### Workflow checks', '', `_Scored over ${String(aggregate.scoredBuilds)} successful build(s). N/A = check did not apply to that workflow. Error = check could not be measured (e.g. judge timeout)._`, '', ]; // Failing checks render inline (the only rows that usually carry signal); the // full, mostly-100% table collapses into
so an all-green run is a // one-line summary instead of a ~30-row wall. const failingRows = rowsWhere((name) => rowByName[name].failed); if (failingRows.length > 0) lines.push(...header, ...failingRows, ''); lines.push( `
All workflow checks (${String(failingChecks)} failing of ${String(names.length)} checks)`, '', ...header, ...rowsWhere(() => true), '', '
', '', ); return lines; } function formatHeading(): string { return '### Instance AI Workflow Eval'; } function formatTopAlert(outcome?: ComparisonOutcome): string { if (!outcome) { return ['> [!NOTE]', '> No baseline comparison ran (LangSmith disabled for this run).'].join( '\n', ); } if (outcome.kind === 'no_baseline') { return [ '> [!NOTE]', '> No baseline configured — comparison skipped. Run the eval with `--experiment-name instance-ai-baseline` on master to create one.', ].join('\n'); } if (outcome.kind === 'self_baseline') { return [ '> [!NOTE]', `> This run is the baseline (\`${outcome.experimentName}\`) — nothing to compare against.`, ].join('\n'); } if (outcome.kind === 'fetch_failed') { return [ '> [!WARNING]', `> Regression detection did not run — baseline fetch failed: ${outcome.error}`, ].join('\n'); } const comparison = outcome.result; const hard = hardRegressions(comparison).length; const soft = softRegressions(comparison).length; const watch = watchList(comparison).length; const imps = improvements(comparison).length; const stable = countByVerdict(comparison, 'stable'); const aggDelta = comparison.aggregate.delta * 100; const aggDeltaText = `${aggDelta >= 0 ? '+' : ''}${aggDelta.toFixed(1)}pp`; const passRateText = `pass rate ${aggDeltaText} vs baseline`; // Two-line summary: regression-tier counts on top, positives/neutrals on the // bottom. The pass-rate delta tails whichever line matches its sign so the // per-line story stays coherent (negative delta lives with the concerns). const concernsParts = [ hard > 0 ? `**${hard} regression${hard === 1 ? '' : 's'}**` : '0 regressions', `${soft} likely regression${soft === 1 ? '' : 's'}`, `${watch} worth watching`, ]; const winsParts = [`${imps} improvement${imps === 1 ? '' : 's'}`, `${stable} stable`]; if (aggDelta < 0) { concernsParts.push(passRateText); } else { winsParts.push(passRateText); } const concerns = concernsParts.join(' · '); const wins = winsParts.join(' · '); let icon: string; let alertKind: 'CAUTION' | 'WARNING' | 'NOTE' | 'TIP'; if (hard > 0) { icon = '🔴'; alertKind = 'CAUTION'; } else if (soft > 0) { icon = '🟡'; alertKind = 'WARNING'; } else if (watch > 0) { icon = '🔵'; alertKind = 'NOTE'; } else { icon = '🟢'; alertKind = 'TIP'; } return `> [!${alertKind}]\n> ${icon} ${concerns}\n> ${wins}`; } function formatAggregateBlock( evaluation: MultiRunEvaluation, comparison?: ComparisonResult, ): string { if (!comparison) { const summary = aggregateMeasuredUnits(evaluation.testCases); const rate = summary.total > 0 ? (summary.passed / summary.total) * 100 : 0; return `**Aggregate**: ${rate.toFixed(1)}% pass (${summary.passed}/${summary.total} trials, ${unitCountLabel(summary, evaluation.totalRuns)})`; } const { aggregate } = comparison; const delta = aggregate.delta * 100; const sign = delta >= 0 ? '+' : ''; const arrow = delta > 0 ? ' ↑' : delta < 0 ? ' ↓' : ''; const baselineN = inferBaselineN(comparison); const mixLabel = unitMixLabel(comparison.evaluationUnits); const sampleLine = baselineN ? `_${mixLabel} · N=${evaluation.totalRuns} (PR) vs N=${baselineN} (baseline) · baseline: \`${comparison.baseline.experimentName}\`_` : `_${mixLabel} · N=${evaluation.totalRuns} (PR) · baseline: \`${comparison.baseline.experimentName}\`_`; const partialParts = describePartialCoverage(comparison); const partialNote = partialParts.length > 0 ? `\n_Partial: ${partialParts.join(', ')}._` : ''; return [ `**Aggregate**: ${pct(aggregate.prAggregatePassRate)}% PR vs ${pct(aggregate.baselineAggregatePassRate)}% baseline — **${sign}${delta.toFixed(1)}pp${arrow}**`, sampleLine + partialNote, ].join('\n'); } function renderUnitSection( heading: string, subtitle: string, units: EvaluationUnitComparison[], withPValue: boolean, failedIndex?: FailedRunsBySlug, ): string[] { const lines: string[] = []; const headingLine = subtitle ? `#### ${heading} (${units.length}) ${subtitle}` : `#### ${heading} (${units.length})`; lines.push(headingLine); lines.push(''); if (withPValue) { lines.push('| Unit | PR | Baseline | Δ | p |'); lines.push('|---|---|---|---|---|'); } else { lines.push('| Unit | PR | Baseline | Δ |'); lines.push('|---|---|---|---|'); } for (const s of units) { const cells = [ `\`${unitLabel(s)}\``, formatRateCell(s.prPasses, s.prTotal), formatRateCell(s.baselinePasses, s.baselineTotal), formatDeltaCell(s.delta), ]; if (withPValue) { const p = s.verdict === 'improvement' ? s.pValueRight : s.pValueLeft; cells.push(p.toFixed(3)); } lines.push(`| ${cells.join(' | ')} |`); } lines.push(''); // Per-unit failure breakdown — one collapsible per row that had failed // PR runs. Lets the reader drill into each flagged unit without // hunting through a separate "Failure details" section. if (failedIndex) { for (const s of units) { const failedRuns = failedIndex.get(unitKeyOf(s)) ?? []; if (failedRuns.length === 0) continue; lines.push(...renderUnitFailureBreakdown(s, failedRuns)); } } return lines; } function renderUnitFailureBreakdown( s: EvaluationUnitComparison, failedRuns: FailedRunDetail[], ): string[] { const slug = unitLabel(s); const categoryMix = summarizeCategories(failedRuns); const summaryParts = [`${failedRuns.length} of ${s.prTotal} failed`]; if (categoryMix) summaryParts.push(categoryMix); const lines: string[] = []; lines.push(`
${slug} — ${summaryParts.join(' · ')}`); lines.push(''); for (const fr of failedRuns) { const tag = fr.category ? ` [${fr.category}]` : ''; lines.push(`> Run ${fr.runIndex}${tag}: ${fr.reasoning.slice(0, 300)}`); lines.push('>'); } // Drop the trailing empty quote line. if (lines[lines.length - 1] === '>') lines.pop(); lines.push(''); lines.push('
'); lines.push(''); return lines; } function renderFailureCategorySection(categories: FailureCategoryComparison[]): string[] { // Drop rows that are 0/0 on both sides — they carry no signal for the // reader. Categories with non-zero count on either side are kept so the // reader sees the full picture even if not "notable". const rows = categories.filter((c) => c.prCount > 0 || c.baselineCount > 0); if (rows.length === 0) return []; const lines: string[] = []; lines.push('#### Failure breakdown'); lines.push(''); lines.push('| Category | PR | Baseline | Δ | |'); lines.push('|---|---|---|---|---|'); for (const c of rows) { const isNew = c.baselineCount === 0 && c.prCount > 0; const label = isNew ? `\`${c.category}\` 🆕` : `\`${c.category}\``; const delta = c.delta * 100; const sign = delta >= 0 ? '+' : ''; const arrow = delta > 0 ? ' ↑' : delta < 0 ? ' ↓' : ''; const notableMarker = c.notable ? '**notable**' : ''; lines.push( `| ${label} | ${c.prCount} (${pct(c.prRate)}%) | ${c.baselineCount} (${pct(c.baselineRate)}%) | ${sign}${delta.toFixed(1)}pp${arrow} | ${notableMarker} |`, ); } lines.push(''); return lines; } function renderPerTestCaseDetails( evaluation: MultiRunEvaluation, slugByTestCase?: Map, ): string[] { const { totalRuns, testCases } = evaluation; if (testCases.length === 0) return []; const lines: string[] = []; lines.push(`
Per-test-case results (${testCases.length})`); lines.push(''); const renderName = (tc: TestCaseAggregation): string => { const slug = slugByTestCase?.get(tc.testCase); return slug ? `\`${slug}\`` : `\`${caseDisplayPrompt(tc.testCase).slice(0, 70)}\``; }; if (totalRuns > 1) { lines.push(`| Workflow | Status | pass@${totalRuns} | pass^${totalRuns} |`); lines.push('|---|---|---|---|'); for (const tc of testCases) { const units = [ ...tc.executionScenarios.filter((sa) => sa.evaluatedCount > 0), ...evaluatedBuildExpectations(tc), ]; const meanPassAtK = units.length ? Math.round( (units.reduce((sum, unit) => sum + (unit.passAtK[unit.passAtK.length - 1] ?? 0), 0) / units.length) * 100, ) : 0; const meanPassHatK = units.length ? Math.round( (units.reduce((sum, unit) => sum + (unit.passHatK[unit.passHatK.length - 1] ?? 0), 0) / units.length) * 100, ) : 0; lines.push( `| ${renderName(tc)} | ${getCheckedRunCount(tc)}/${totalRuns} | ${meanPassAtK}% | ${meanPassHatK}% |`, ); } } else { lines.push('| Workflow | Status | Pass rate |'); lines.push('|---|---|---|'); for (const tc of testCases) { const run = tc.runs[0]; const status = run ? getCaseRunStatusLabel(run) : 'NO RUN'; const { passCount, totalCount } = countAggregatedUnitTrials(getAggregatedCaseUnits(tc)); lines.push(`| ${renderName(tc)} | ${status} | ${passCount}/${totalCount} |`); } } lines.push(''); lines.push('
'); lines.push(''); return lines; } function renderOtherFindings(comparison: ComparisonResult): string[] { const stable = countByVerdict(comparison, 'stable'); const flaky = countByVerdict(comparison, 'unreliable_baseline'); const noData = countByVerdict(comparison, 'insufficient_data'); if (stable === 0 && flaky === 0 && noData === 0) return []; const summaryParts: string[] = []; if (flaky > 0) summaryParts.push(`${flaky} on flaky baseline`); if (noData > 0) summaryParts.push(`${noData} no data`); if (stable > 0) summaryParts.push(`${stable} stable`); const summary = summaryParts.join(' · '); const lines: string[] = []; lines.push(`
Other findings: ${summary}`); lines.push(''); const stableUnits = comparison.evaluationUnits.filter((s) => s.verdict === 'stable'); const flakyUnits = comparison.evaluationUnits.filter((s) => s.verdict === 'unreliable_baseline'); const noDataUnits = comparison.evaluationUnits.filter((s) => s.verdict === 'insufficient_data'); if (flakyUnits.length > 0) { lines.push('**Confident drop on a flaky baseline (surfaced for visibility, not flagged):**'); lines.push(''); lines.push('| Unit | PR | Baseline | Δ |'); lines.push('|---|---|---|---|'); for (const s of flakyUnits) { lines.push( `| \`${unitLabel(s)}\` | ${formatRateCell(s.prPasses, s.prTotal)} | ${formatRateCell(s.baselinePasses, s.baselineTotal)} | ${formatDeltaCell(s.delta)} |`, ); } lines.push(''); } if (noDataUnits.length > 0) { lines.push(`**No data:** ${noDataUnits.map((s) => `\`${unitLabel(s)}\``).join(', ')}`); lines.push(''); } if (stableUnits.length > 0) { lines.push(`**Stable (${stableUnits.length}):**`); lines.push(stableUnits.map((s) => `\`${unitLabel(s)}\``).join(', ') + '.'); lines.push(''); } lines.push('
'); lines.push(''); return lines; } function renderFailureDetails( evaluation: MultiRunEvaluation, slugByTestCase?: Map, ): string[] { interface FailedUnit { slug: string; passCount: number; total: number; runs: Array<{ category?: string; text: string }>; } const units: FailedUnit[] = []; for (const tc of evaluation.testCases) { const prefix = slugByTestCase?.get(tc.testCase) ?? caseDisplayPrompt(tc.testCase).slice(0, 50).trim(); for (const sa of tc.executionScenarios) { // Verifier-incomplete runs are shown for visibility but tagged as // excluded — they're outside the passCount/total denominator. const failedRuns = sa.runs.filter((r) => !r.success && !r.incomplete); const excludedRuns = sa.runs.filter((r) => r.incomplete); if (failedRuns.length > 0 || excludedRuns.length > 0) { units.push({ slug: `${prefix}/${sa.scenario.name}`, passCount: sa.passCount, total: sa.evaluatedCount, runs: [ ...failedRuns.map((r) => ({ category: r.failureCategory, text: r.reasoning })), ...excludedRuns.map((r) => ({ category: 'excluded from scoring — verifier returned no verdict', text: r.reasoning, })), ], }); } } for (const ea of tc.buildExpectations) { // Only evaluated verdicts count; `incomplete` runs have no judge text to show. const failedRuns = ea.runs.filter((r) => !r.incomplete && !r.pass); if (failedRuns.length > 0) { units.push({ slug: `${prefix} :: ${ea.expectation.slice(0, 60)}`, passCount: ea.passCount, total: ea.evaluatedCount, runs: failedRuns.map((r) => ({ text: r.reason })), }); } } } if (units.length === 0) return []; // Full judge text for every failed run — scenarios and build expectations alike — // so it's easy to see exactly what failed. Open by default but collapsible. const lines: string[] = [`
Failures (${units.length})`, '']; for (const u of units) { lines.push(`**\`${u.slug}\`** — passed ${u.passCount}/${u.total}`, ''); // Collapse identical reasonings — a unit usually fails the same way each run. const byText = new Map(); for (const r of u.runs) { const key = `${r.category ?? ''}::${r.text}`; const existing = byText.get(key); if (existing) existing.count++; else byText.set(key, { ...r, count: 1 }); } for (const r of byText.values()) { const tag = r.category ? `_[${r.category}]_ ` : ''; const mult = r.count > 1 ? ` _(×${r.count})_` : ''; const text = (r.text.trim() || '_(judge returned no text)_').replace(/\n+/g, '\n> '); lines.push(`> ${tag}${text}${mult}`, ''); } } lines.push('
', ''); return lines; } // --------------------------------------------------------------------------- // Per-scenario failure lookup // --------------------------------------------------------------------------- // // The comparison carries per-scenario counts (passed / total) but not the // underlying reasoning text. The evaluation has the reasoning, but keys // testCases by reference identity — not by the `testCaseFile` slug used in // the comparison. The slug map (built in cli/index.ts where the file slugs // are first known) bridges the two so the lookup is deterministic. Without // it we'd have to disambiguate by scenarioName alone, which collides on // reused names (`happy-path` shows up across most workflows). interface FailedRunDetail { category?: string; reasoning: string; runIndex: number; // 1-based for display } type FailedRunsBySlug = Map; function buildFailedRunsIndex( evaluation: MultiRunEvaluation, slugByTestCase: Map, ): FailedRunsBySlug { const map: FailedRunsBySlug = new Map(); for (const tc of evaluation.testCases) { const fileSlug = slugByTestCase.get(tc.testCase); if (!fileSlug) continue; // testCase not in the slug map — skip rather than misattribute for (const sa of tc.executionScenarios) { const failedRuns: FailedRunDetail[] = []; sa.runs.forEach((r, i) => { // Incomplete runs are outside the comparison denominators — skip. if (!r.success && !r.incomplete) { failedRuns.push({ category: r.failureCategory, reasoning: r.reasoning, runIndex: i + 1, }); } }); if (failedRuns.length > 0) { map.set(scenarioUnitKey(fileSlug, sa.scenario.name), failedRuns); } } for (const ea of tc.buildExpectations) { const failedRuns: FailedRunDetail[] = []; ea.runs.forEach((r, i) => { // Judge-incomplete verdicts are outside the comparison denominators — skip. if (!r.incomplete && !r.pass) { failedRuns.push({ reasoning: r.reason, runIndex: i + 1 }); } }); if (failedRuns.length > 0) { map.set(expectationUnitKey(fileSlug, ea.expectation), failedRuns); } } } return map; } function summarizeCategories(failedRuns: FailedRunDetail[]): string | undefined { const counts = new Map(); for (const fr of failedRuns) { if (fr.category) counts.set(fr.category, (counts.get(fr.category) ?? 0) + 1); } if (counts.size === 0) return undefined; return [...counts.entries()] .sort((a, b) => b[1] - a[1]) .map(([cat, n]) => `${n}× ${cat}`) .join(', '); } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function pct(rate: number): string { return (rate * 100).toFixed(1); } function formatRateCell(passes: number, total: number): string { const rate = total > 0 ? Math.round((passes / total) * 100) : 0; return `${passes}/${total} (${rate}%)`; } function formatDeltaCell(delta: number): string { const pp = delta * 100; const sign = pp >= 0 ? '+' : ''; const arrow = pp > 0 ? ' ↑' : pp < 0 ? ' ↓' : ''; return `${sign}${pp.toFixed(0)}pp${arrow}`; } function countByVerdict( comparison: ComparisonResult, verdict: EvaluationUnitComparison['verdict'], ): number { return comparison.evaluationUnits.filter((s) => s.verdict === verdict).length; } /** Best-effort N=baseline iteration count, inferred from the most-common * scenario trial total (the baseline runs every scenario the same number of * times). Scenario units only — expectation denominators exclude * judge-incomplete verdicts, so they'd distort the inferred N. */ function inferBaselineN(comparison: ComparisonResult): number | undefined { const totals = comparison.evaluationUnits .filter((s) => s.kind === 'scenario' && s.baselineTotal > 0) .map((s) => s.baselineTotal); if (totals.length === 0) return undefined; const counts = new Map(); for (const t of totals) counts.set(t, (counts.get(t) ?? 0) + 1); let best = totals[0]; let bestCount = 0; for (const [n, c] of counts) { if (c > bestCount) { best = n; bestCount = c; } } return best; } // --------------------------------------------------------------------------- // Terminal renderer: aligned plain text for the eval CLI's end-of-run print. // --------------------------------------------------------------------------- const TERMINAL_INDENT = ' '; const TERMINAL_TABLE_INDENT = ' '; export function formatComparisonTerminal( evaluation: MultiRunEvaluation, outcome?: ComparisonOutcome, options: FormatOptions = {}, ): string { const lines: string[] = []; const gate = options.gate; const comparison = !gate && outcome?.kind === 'ok' ? outcome.result : undefined; const titleSuffix = options.commitSha ? ` — ${options.commitSha.slice(0, 8)}` : ''; const title = `Instance AI Workflow Eval${titleSuffix}`; lines.push(title); lines.push('═'.repeat(title.length)); if (gate) { lines.push(TERMINAL_INDENT + formatTerminalGateLine(gate)); lines.push(''); lines.push(...formatTerminalGateSummary(gate)); lines.push(''); } else { lines.push(TERMINAL_INDENT + formatTerminalVerdictLine(outcome)); lines.push(''); lines.push(...formatTerminalAggregate(evaluation, comparison)); lines.push(''); } lines.push(...formatTerminalPerTestCase(evaluation, options.slugByTestCase)); if (comparison) { const hard = hardRegressions(comparison); const soft = softRegressions(comparison); const watch = watchList(comparison); const imps = improvements(comparison); if (hard.length > 0) { lines.push( TERMINAL_INDENT + 'REGRESSIONS (high-confidence: large drop on a reliable scenario, unlikely noise)', ); lines.push(formatTerminalUnitTable(hard, true)); lines.push(''); } if (soft.length > 0) { lines.push( TERMINAL_INDENT + 'LIKELY REGRESSIONS (looser statistical flag — investigate if related to your changes)', ); lines.push(formatTerminalUnitTable(soft, true)); lines.push(''); } if (watch.length > 0) { lines.push(TERMINAL_INDENT + 'WORTH WATCHING (large change, not flagged as a regression)'); lines.push(formatTerminalUnitTable(watch, false)); lines.push(''); } if (imps.length > 0) { lines.push(TERMINAL_INDENT + 'IMPROVEMENTS'); lines.push(formatTerminalUnitTable(imps, true)); lines.push(''); } // Always render the breakdown when comparison data is available — same // rationale as the markdown side. The terminal table drops 0/0 rows // itself. const breakdownRows = comparison.failureCategories.filter( (c) => c.prCount > 0 || c.baselineCount > 0, ); if (breakdownRows.length > 0) { lines.push(TERMINAL_INDENT + 'failure breakdown'); lines.push(formatTerminalCategoryTable(breakdownRows)); lines.push(''); } // Stable count is already in the verdict line; surface only the rarer // outcomes here. const flaky = countByVerdict(comparison, 'unreliable_baseline'); const noData = countByVerdict(comparison, 'insufficient_data'); const otherParts: string[] = []; if (flaky > 0) otherParts.push(`${flaky} on flaky baseline`); if (noData > 0) otherParts.push(`${noData} no data`); if (otherParts.length > 0) { lines.push(TERMINAL_INDENT + 'other: ' + otherParts.join(' · ')); } } return lines.join('\n'); } function formatTerminalVerdictLine(outcome?: ComparisonOutcome): string { if (!outcome) return '▶ No baseline comparison ran (LangSmith disabled).'; if (outcome.kind === 'no_baseline') { return '▶ No baseline configured — comparison skipped.'; } if (outcome.kind === 'self_baseline') { return `▶ This run is the baseline (${outcome.experimentName}) — nothing to compare.`; } if (outcome.kind === 'fetch_failed') { return `▶ Regression detection did not run — baseline fetch failed: ${outcome.error}`; } const comparison = outcome.result; const hard = hardRegressions(comparison).length; const soft = softRegressions(comparison).length; const watch = watchList(comparison).length; const imps = improvements(comparison).length; const stable = countByVerdict(comparison, 'stable'); const aggDelta = comparison.aggregate.delta * 100; const aggDeltaText = `${aggDelta >= 0 ? '+' : ''}${aggDelta.toFixed(1)}pp`; const passRateText = `pass rate ${aggDeltaText} vs baseline`; const concernsParts = [ `${hard} regression${hard === 1 ? '' : 's'}`, `${soft} likely regression${soft === 1 ? '' : 's'}`, `${watch} worth watching`, ]; const winsParts = [`${imps} improvement${imps === 1 ? '' : 's'}`, `${stable} stable`]; if (aggDelta < 0) { concernsParts.push(passRateText); } else { winsParts.push(passRateText); } // The caller prepends TERMINAL_INDENT to the start of this string. Embed an // extra TERMINAL_INDENT after the line break so the wins line aligns under // the concerns text (past the `▶ ` arrow). return `▶ ${concernsParts.join(' · ')}\n${TERMINAL_INDENT} ${winsParts.join(' · ')}`; } function formatTerminalAggregate( evaluation: MultiRunEvaluation, comparison?: ComparisonResult, ): string[] { const lines: string[] = []; if (!comparison) { const summary = aggregateMeasuredUnits(evaluation.testCases); const rate = summary.total > 0 ? (summary.passed / summary.total) * 100 : 0; lines.push( TERMINAL_INDENT + `Aggregate: ${rate.toFixed(1)}% pass (${summary.passed}/${summary.total} trials, ${unitCountLabel(summary, evaluation.totalRuns)})`, ); return lines; } const { aggregate } = comparison; const baselineN = inferBaselineN(comparison); const aggDelta = aggregate.delta * 100; const sign = aggDelta >= 0 ? '+' : ''; const arrow = aggDelta > 0 ? ' ↑' : aggDelta < 0 ? ' ↓' : ''; lines.push(TERMINAL_INDENT + `Aggregate (${unitMixLabel(comparison.evaluationUnits)})`); lines.push( TERMINAL_INDENT + ` PR ${pct(aggregate.prAggregatePassRate)}% (N=${evaluation.totalRuns})`, ); if (baselineN !== undefined) { lines.push( TERMINAL_INDENT + ` baseline ${pct(aggregate.baselineAggregatePassRate)}% (N=${baselineN})`, ); } else { lines.push(TERMINAL_INDENT + ` baseline ${pct(aggregate.baselineAggregatePassRate)}%`); } lines.push(TERMINAL_INDENT + ` Δ ${sign}${aggDelta.toFixed(1)}pp${arrow}`); const partialParts = describePartialCoverage(comparison); if (partialParts.length > 0) { lines.push(TERMINAL_INDENT + ` partial: ${partialParts.join(', ')}`); } return lines; } function formatTerminalPerTestCase( evaluation: MultiRunEvaluation, slugByTestCase?: Map, ): string[] { const { totalRuns, testCases } = evaluation; if (testCases.length === 0) return []; const lines: string[] = []; const heading = `Per-test-case results (${testCases.length})`; lines.push(TERMINAL_INDENT + heading); const nameOf = (tc: TestCaseAggregation, max: number): string => { const slug = slugByTestCase?.get(tc.testCase); return slug ?? caseDisplayPrompt(tc.testCase).slice(0, max); }; if (totalRuns > 1) { const rows = testCases.map((tc) => { const units = getAggregatedCaseUnits(tc); const meanPassAtK = units.length > 0 ? Math.round( (units.reduce((sum, u) => sum + (u.passAtK[u.passAtK.length - 1] ?? 0), 0) / units.length) * 100, ) : 0; const meanPassHatK = units.length > 0 ? Math.round( (units.reduce((sum, u) => sum + (u.passHatK[u.passHatK.length - 1] ?? 0), 0) / units.length) * 100, ) : 0; return { name: nameOf(tc, 60), status: `${getCheckedRunCount(tc)}/${totalRuns}`, passAtK: `${meanPassAtK}%`, passHatK: `${meanPassHatK}%`, }; }); const statusHeader = 'status'; const nameW = maxWidth( rows.map((r) => r.name), 'workflow', ); const buildsW = maxWidth( rows.map((r) => r.status), statusHeader, ); const atKHeader = `pass@${totalRuns}`; const hatKHeader = `pass^${totalRuns}`; const atKW = maxWidth( rows.map((r) => r.passAtK), atKHeader, ); const hatKW = maxWidth( rows.map((r) => r.passHatK), hatKHeader, ); lines.push( TERMINAL_TABLE_INDENT + `${'workflow'.padEnd(nameW)} ${statusHeader.padEnd(buildsW)} ${atKHeader.padStart(atKW)} ${hatKHeader.padStart(hatKW)}`, ); lines.push( TERMINAL_TABLE_INDENT + `${'─'.repeat(nameW)} ${'─'.repeat(buildsW)} ${'─'.repeat(atKW)} ${'─'.repeat(hatKW)}`, ); for (const r of rows) { lines.push( TERMINAL_TABLE_INDENT + `${r.name.padEnd(nameW)} ${r.status.padEnd(buildsW)} ${r.passAtK.padStart(atKW)} ${r.passHatK.padStart(hatKW)}`, ); } } else { for (const tc of testCases) { const r = tc.runs[0]; const buildStatus = getCaseRunStatusLabel(r); lines.push(''); lines.push(TERMINAL_INDENT + `${nameOf(tc, 70)}…`); lines.push(TERMINAL_INDENT + ` ${buildStatus}${r.workflowId ? ` (${r.workflowId})` : ''}`); if (getCaseRunStatus(r) !== 'checked' && r.buildError) { lines.push(TERMINAL_INDENT + ` error: ${r.buildError.slice(0, 200)}`); } for (const sa of tc.executionScenarios) { const sr = sa.runs[0]; const status = sr.incomplete ? 'SKIP (no verdict)' : sr.success ? 'PASS' : 'FAIL'; const category = sr.failureCategory ? ` [${sr.failureCategory}]` : ''; lines.push(TERMINAL_INDENT + ` ${status} ${sr.scenario.name}${category}`); if (!sr.success) { const errs = sr.evalResult?.errors ?? []; if (errs.length > 0) { lines.push(TERMINAL_INDENT + ` error: ${errs.join('; ').slice(0, 200)}`); } lines.push(TERMINAL_INDENT + ` diagnosis: ${sr.reasoning.slice(0, 200)}`); } } for (const ea of tc.buildExpectations) { const er = ea.runs[0]; if (!er) continue; const status = er.incomplete ? 'SKIP' : er.pass ? 'PASS' : 'FAIL'; lines.push(TERMINAL_INDENT + ` ${status} expectation: ${ea.expectation.slice(0, 80)}`); if (status === 'FAIL') { lines.push(TERMINAL_INDENT + ` ${er.reason.slice(0, 200)}`); } } } } lines.push(''); return lines; } function formatTerminalUnitTable(units: EvaluationUnitComparison[], withPValue: boolean): string { const names = units.map((s) => unitLabel(s)); const prCells = units.map((s) => `${s.prPasses}/${s.prTotal}`); const baseCells = units.map((s) => `${s.baselinePasses}/${s.baselineTotal}`); const deltaCells = units.map((s) => { const d = s.delta * 100; const sign = d >= 0 ? '+' : ''; const arrow = d > 0 ? ' ↑' : d < 0 ? ' ↓' : ''; return `${sign}${d.toFixed(0)}pp${arrow}`; }); const pCells = withPValue ? units.map((s) => (s.verdict === 'improvement' ? s.pValueRight : s.pValueLeft).toFixed(3)) : []; const nameW = maxWidth(names, 'unit'); const prW = maxWidth(prCells, 'PR'); const baseW = maxWidth(baseCells, 'baseline'); const deltaW = maxWidth(deltaCells, 'Δ'); const pW = withPValue ? maxWidth(pCells, 'p') : 0; const headers = [ 'unit'.padEnd(nameW), 'PR'.padEnd(prW), 'baseline'.padEnd(baseW), 'Δ'.padEnd(deltaW), ]; if (withPValue) headers.push('p'.padEnd(pW)); const widths = withPValue ? [nameW, prW, baseW, deltaW, pW] : [nameW, prW, baseW, deltaW]; const sep = widths.map((w) => '─'.repeat(w)).join(' '); const rows = units.map((_, i) => { const cells = [ names[i].padEnd(nameW), prCells[i].padEnd(prW), baseCells[i].padEnd(baseW), deltaCells[i].padEnd(deltaW), ]; if (withPValue) cells.push(pCells[i].padEnd(pW)); return TERMINAL_TABLE_INDENT + cells.join(' '); }); return [TERMINAL_TABLE_INDENT + headers.join(' '), TERMINAL_TABLE_INDENT + sep, ...rows].join( '\n', ); } function formatTerminalCategoryTable(cats: FailureCategoryComparison[]): string { const names = cats.map((c) => { const isNew = c.baselineCount === 0 && c.prCount > 0; return c.category + (isNew ? ' 🆕' : ''); }); const prCells = cats.map((c) => `${c.prCount} (${pct(c.prRate)}%)`); const baseCells = cats.map((c) => `${c.baselineCount} (${pct(c.baselineRate)}%)`); const deltaCells = cats.map((c) => { const d = c.delta * 100; const sign = d >= 0 ? '+' : ''; return `${sign}${d.toFixed(1)}pp`; }); const nameW = maxWidth(names, 'category'); const prW = maxWidth(prCells, 'PR'); const baseW = maxWidth(baseCells, 'baseline'); const headers = ['category'.padEnd(nameW), 'PR'.padEnd(prW), 'baseline'.padEnd(baseW), 'Δ']; const sep = [nameW, prW, baseW, maxWidth(deltaCells, 'Δ')].map((w) => '─'.repeat(w)).join(' '); const rows = cats.map( (_, i) => TERMINAL_TABLE_INDENT + [ names[i].padEnd(nameW), prCells[i].padEnd(prW), baseCells[i].padEnd(baseW), deltaCells[i], ].join(' '), ); return [TERMINAL_TABLE_INDENT + headers.join(' '), TERMINAL_TABLE_INDENT + sep, ...rows].join( '\n', ); } function maxWidth(values: string[], header: string): number { return values.reduce((m, v) => Math.max(m, v.length), header.length); }