Files
Imhotep/packages/imhotep-reporter/src/human.ts
T

121 lines
3.1 KiB
TypeScript
Raw Normal View History

/**
* Human-readable reporter output.
*
* Turns diagnostics, traces, and shrink results into plain text
* suitable for terminal reading.
*/
import type { Diagnostic } from './diagnostics.js';
import type { TraceEvent } from './traces.js';
import type { ShrinkResult } from './shrink.js';
/**
* Options for human formatting.
* Injected so callers control colors, verbosity, etc.
*/
export interface HumanFormatOptions {
// show trace events after each diagnostic
showTraces?: boolean;
// show shrink summary when available
showShrink?: boolean;
// max related facts to print
maxRelated?: number;
}
/**
* Render a list of diagnostics into a human-readable string.
*/
export function renderHumanReport(
diagnostics: Diagnostic[],
traces: readonly TraceEvent[],
shrinkResults: Map<string, ShrinkResult>,
opts: HumanFormatOptions = {},
): string {
const lines: string[] = [];
for (const d of diagnostics) {
lines.push(renderDiagnostic(d, opts));
if (opts.showShrink && d.clauseId && shrinkResults.has(d.clauseId)) {
const shrink = shrinkResults.get(d.clauseId)!;
lines.push(renderShrink(shrink));
}
if (opts.showTraces && d.traceRef) {
const relevant = traces.filter(
(t) => t.traceEventId === d.traceRef || t.refs.diagnosticId === d.diagnosticId,
);
if (relevant.length > 0) {
lines.push(' trace:');
for (const t of relevant) {
lines.push(` ${t.phase} at ${t.at}`);
}
}
}
}
return lines.join('\n');
}
/**
* Render a single diagnostic in human form.
*/
export function renderDiagnostic(
d: Diagnostic,
opts: HumanFormatOptions = {},
): string {
const lines: string[] = [];
const prefix = d.severity === 'error' ? '✖' : d.severity === 'warning' ? '⚠' : '';
lines.push(`${prefix} ${d.message}`);
lines.push(` ${d.code}`);
if (d.position) {
lines.push(
` at line ${d.position.start.line}, column ${d.position.start.column}`,
);
}
const maxRelated = opts.maxRelated ?? 5;
if (d.related.length > 0) {
lines.push(' related:');
for (const r of d.related.slice(0, maxRelated)) {
lines.push(`${r.message}`);
}
if (d.related.length > maxRelated) {
lines.push(` … and ${d.related.length - maxRelated} more`);
}
}
if (d.fixHints.length > 0) {
lines.push(' hints:');
for (const h of d.fixHints) {
lines.push(`${h}`);
}
}
if (d.suggestedFix) {
lines.push(' suggested fix:');
lines.push(` action: ${d.suggestedFix.action}`);
lines.push(` target: ${d.suggestedFix.target}`);
lines.push(` value: ${d.suggestedFix.value}`);
lines.push(` rationale: ${d.suggestedFix.rationale}`);
}
return lines.join('\n');
}
/**
* Render a shrink result summary.
*/
export function renderShrink(result: ShrinkResult): string {
const lines: string[] = [];
lines.push(' shrink:');
if (result.reduced) {
lines.push(` reduced across: ${result.axes.join(', ')}`);
lines.push(` steps: ${result.steps}`);
} else {
lines.push(' no reduction possible');
}
return lines.join('\n');
}