/**
*
* RFC-1296: summarize per-step diagnostics from command result data
* into counts consumable by validation-report.json — warnings must
* not be dropped when a step passes.
* Kernel / shared
* mission-materialization-commands.ts, validation-report.json consumers
*
* - Recognized shapes are fixed by the RFC-1296 contract — extend only
* additively (new shape branches appended after existing ones).
*
*
- Not a contract source of truth — callers depend on the module's exported API, not this file's internals.
- RFC-1311: Compass header sweep — repairs pre-existing contract debt surfaced by the test-file scaffold rollout.
*/
export interface DiagnosticsSummary {
errors: number;
warnings: number;
rules: Record;
}
interface DiagnosticLike {
ruleId?: string;
severity?: string;
}
function isDiagnosticArray(value: unknown): value is DiagnosticLike[] {
return (
Array.isArray(value) &&
value.every((d) => typeof d === "object" && d !== null && !Array.isArray(d) && "severity" in d)
);
}
function countInto(
summary: DiagnosticsSummary,
diagnostics: readonly DiagnosticLike[],
): DiagnosticsSummary {
for (const d of diagnostics) {
if (d.severity !== "error" && d.severity !== "warning") continue; // info/notice not counted
const ruleId = typeof d.ruleId === "string" && d.ruleId ? d.ruleId : "(no-rule)";
const bucket = (summary.rules[ruleId] ??= { error: 0, warning: 0 });
if (d.severity === "error") {
summary.errors += 1;
bucket.error += 1;
} else {
summary.warnings += 1;
bucket.warning += 1;
}
}
return summary;
}
function countStrings(summary: DiagnosticsSummary, diagnostics: readonly string[]): void {
for (const _ of diagnostics) {
summary.errors += 1;
const bucket = (summary.rules["(untyped)"] ??= { error: 0, warning: 0 });
bucket.error += 1;
}
}
/**
* Extract `{ errors, warnings, rules }` from any command result `data` shape
* used in the workshop. Recognized shapes, first match wins:
*
* 1. `data.findings: DiagnosticLike[]` (audit results, `buildAuditResult`)
* 2. `data.diagnostics: DiagnosticLike[]` (`diagnosticsResult`)
* 3. `data.result?.diagnostics: DiagnosticLike[]` (nested)
* 4. `data.diagnostics: string[]` — counted as errors only when `exitCode != 0`,
* under ruleId `"(untyped)"`
*
* Any other shape returns zeros.
*/
export function summarizeStepDiagnostics(data: unknown, exitCode?: number): DiagnosticsSummary {
const summary: DiagnosticsSummary = { errors: 0, warnings: 0, rules: {} };
if (typeof data !== "object" || data === null) return summary;
const record = data as Record;
// Shape 1: audit findings
if (isDiagnosticArray(record.findings)) {
return countInto(summary, record.findings);
}
// Shape 2: flat diagnostics
if (isDiagnosticArray(record.diagnostics)) {
return countInto(summary, record.diagnostics);
}
// Shape 3: nested result.diagnostics
const nested = record.result;
if (
typeof nested === "object" &&
nested !== null &&
isDiagnosticArray((nested as Record).diagnostics)
) {
return countInto(summary, (nested as Record).diagnostics as DiagnosticLike[]);
}
// Shape 4: untyped string diagnostics — errors only on non-zero exit
if (Array.isArray(record.diagnostics) && record.diagnostics.every((d) => typeof d === "string")) {
if (exitCode !== undefined && exitCode !== 0) {
countStrings(summary, record.diagnostics as string[]);
}
return summary;
}
return summary;
}