/*
RFC-0203 canonical result builders. Every builder returns a CheckResult
(`{ command, status, diagnostics, summary }`) so non-zero exits propagate to CI
and the central failure printer renders findings uniformly. The string builders
(passResult/failResult/resultFromViolations) key each violation to a coarse
ruleId = command; diagnosticsResult takes rich Diagnostic[] with registered ids.
- Do not interpret violation severity — string builders emit error-severity diagnostics; failResult always exits 1.
- RFC-0029 review: Initial creation to fix systemic exit-code bug across check files.
- RFC-1027: Auto-populate Diagnostic.remediation from REMEDIATION_CATALOG in diagnosticsResult().
*/
import type {
CheckResult,
Diagnostic,
KernelCommandResult,
KernelNextStep,
} from "@warpgogol/werkstatt-shared/kernel";
import { lookupRemediation } from "../remediation/remediation-catalog.ts";
function defaultFailNextSteps(command: string): KernelNextStep[] {
return [
{
action: `Fix the error diagnostics reported by ${command} above, then re-run the pipeline`,
kind: "required",
},
];
}
/**
* Format error/warning counts with proper pluralization and zero-omission.
*
* - `0, 0` → `""` (empty string — caller should omit)
* - `1, 0` → `"1 error"`
* - `0, 1` → `"1 warning"`
* - `2, 0` → `"2 errors"`
* - `2, 1` → `"2 errors, 1 warning"`
*/
export function formatCounts(errors: number, warnings: number): string {
const parts: string[] = [];
if (errors > 0) {
parts.push(`${errors} error${errors === 1 ? "" : "s"}`);
}
if (warnings > 0) {
parts.push(`${warnings} warning${warnings === 1 ? "" : "s"}`);
}
return parts.join(", ");
}
/**
* RFC-0203: canonical result builder. Migrated checks emit `Diagnostic[]` with
* registered rule ids instead of bare strings. Exit code is 1 when any
* diagnostic is an error; warnings and info do not fail the pipeline.
*
* When `nextSteps` are not provided and the result has errors, a default
* agent-actionable nextStep is emitted: "Fix the error diagnostics … then
* re-run the pipeline".
*/
export function diagnosticsResult(
command: string,
diagnostics: Diagnostic[],
nextSteps?: KernelNextStep[],
): KernelCommandResult {
const enrichedDiagnostics = diagnostics.map((d) => {
if (d.remediation) return d;
const pattern = lookupRemediation(d.ruleId);
if (!pattern) return d;
return {
...d,
remediation: {
ruleId: pattern.ruleId,
action: pattern.action,
...(pattern.template !== undefined && { template: pattern.template }),
...(pattern.docRef !== undefined && { docRef: pattern.docRef }),
...(pattern.targetFiles !== undefined && { targetFiles: pattern.targetFiles }),
},
};
});
const summary = {
error: enrichedDiagnostics.filter((d) => d.severity === "error").length,
warning: enrichedDiagnostics.filter((d) => d.severity === "warning").length,
info: enrichedDiagnostics.filter((d) => d.severity === "info").length,
};
const status: CheckResult["status"] =
summary.error > 0 ? "fail" : summary.warning > 0 ? "warn" : "pass";
const resolvedNextSteps =
nextSteps ?? (summary.error > 0 ? defaultFailNextSteps(command) : undefined);
const counts = formatCounts(summary.error, summary.warning);
return {
data: { command, status, diagnostics: enrichedDiagnostics, summary },
exitCode: summary.error > 0 ? 1 : 0,
summary: counts ? `[${command}] ${counts}` : `[${command}]`,
nextSteps: resolvedNextSteps,
};
}
/**
* RFC-0203: map bare violation strings into canonical error Diagnostics. The
* command name is the coarse, stable ruleId for these single-rule checks; checks
* that need finer ids or fixHints use {@link diagnosticsResult} directly.
*/
function violationsToDiagnostics(command: string, violations: string[]): Diagnostic[] {
return violations.map((message) => ({ ruleId: command, severity: "error" as const, message }));
}
function emptySummary(): CheckResult["summary"] {
return { error: 0, warning: 0, info: 0 };
}
/**
* Canonical success result. RFC-0203: emits an empty `CheckResult` (no
* diagnostics) rather than the legacy `{ command, status, violations }` shape.
*/
export function passResult(
command: string,
summary?: string,
nextSteps?: KernelNextStep[],
): KernelCommandResult {
return {
data: { command, status: "pass", diagnostics: [], summary: emptySummary() },
exitCode: 0,
summary: summary ?? `[${command}] OK`,
nextSteps,
};
}
/**
* Canonical failure result — always exits 1 (preserving the historical
* failResult contract). RFC-0203: violation strings become error Diagnostics
* keyed by the command name.
*
* When `nextSteps` are not provided, a default agent-actionable nextStep is
* emitted: "Fix the error diagnostics … then re-run the pipeline".
*/
export function failResult(
command: string,
violations: string[],
nextSteps?: KernelNextStep[],
): KernelCommandResult {
const diagnostics = violationsToDiagnostics(command, violations);
return {
data: {
command,
status: "fail",
diagnostics,
summary: { error: diagnostics.length, warning: 0, info: 0 },
},
exitCode: 1,
summary: `[${command}] ${violations.length} violation${violations.length === 1 ? "" : "s"}`,
nextSteps: nextSteps ?? defaultFailNextSteps(command),
};
}
/**
* Build a canonical result from a violations array — pass if empty, fail
* otherwise. RFC-0203: no longer a bare-string shim; it emits canonical
* Diagnostics (ruleId = command). Checks that need registered fine-grained
* ruleIds, locators, or fixHints use {@link diagnosticsResult} instead.
*/
export function resultFromViolations(
command: string,
violations: string[],
nextSteps?: KernelNextStep[],
): KernelCommandResult {
return violations.length > 0 ? failResult(command, violations, nextSteps) : passResult(command);
}