/* RFC-0086 text-mode diagnostic printing: format a failing command's data payload (canonical RFC-0203 Diagnostic array, or the legacy violations/findings/details shapes) into indented console-ready body lines, deterministically ordered. Do not introduce app-specific runtime composition or deployment behavior into this reusable package source file. Diagnostics print the command's data payload — the payload is the canonical error surface. RFC-0303: split out of runtime.ts (Phase 3 file-size split, hot-path file 8/8). RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. */ const DIAGNOSTIC_LINE_CAP = 50; /** * RFC-0086: inspect a failing command's `data` payload for actionable items * and return a list of indented body strings ready to feed through * `logger.error()`. The logger prefixes each line with `[ERROR] `, so the * returned strings start with 2 spaces to produce the documented * `[ERROR] · · ` shape. * * Precedence (first match wins, never concatenated): * 1. data.diagnostics: string[] | object[] — plain text or {message, …} * 2. data.violations: string[] | object[] — {ruleId/rule, file, message} * 3. data.findings: object[] — RFC-0074 audit findings * 4. data.errors: string[] | object[] — generic error strings or {message, …} * 5. data.details: object[] — generic {file, message} * * Returns an empty array when no array matches. Caps the printable body at * 50 lines and appends ` … and N more (run with --json for the full list)` * when truncated. */ export function formatFailureDiagnostics(data: unknown): string[] { if (!data || typeof data !== "object") return []; const record = data as Record; const arrays: Array<{ key: string; value: unknown }> = [ { key: "diagnostics", value: record.diagnostics }, { key: "violations", value: record.violations }, { key: "findings", value: record.findings }, { key: "errors", value: record.errors }, { key: "details", value: record.details }, ]; const source = arrays.find((entry) => Array.isArray(entry.value)); if (!source) return []; const items = source.value as unknown[]; if (items.length === 0) return []; // RFC-0203: deterministic ordering for canonical Diagnostic items so console // output is stable run-to-run (severity → file → line → ruleId). String items // (the resultFromViolations shim) keep their authored order. const ordered = items.every((item) => item !== null && typeof item === "object") ? [...items].sort(compareDiagnostics) : items; const limit = Math.min(ordered.length, DIAGNOSTIC_LINE_CAP); const lines: string[] = []; for (let index = 0; index < limit; index += 1) { const body = formatDiagnosticItem(ordered[index]); for (const segment of body.split(/\r?\n/)) { lines.push(` ${segment}`); } } if (ordered.length > DIAGNOSTIC_LINE_CAP) { const remainder = ordered.length - DIAGNOSTIC_LINE_CAP; lines.push(` … and ${remainder} more (run with --json for the full list)`); } return lines; } const SEVERITY_RANK: Record = { error: 0, warning: 1, info: 2 }; function diagnosticTarget(record: Record): string { return ( asNonEmptyString(record.file) ?? asNonEmptyString(record.path) ?? asNonEmptyString(record.target) ?? "" ); } /** Stable comparator for canonical Diagnostic-shaped records (RFC-0203). */ function compareDiagnostics(a: unknown, b: unknown): number { const ra = a as Record; const rb = b as Record; const sevA = SEVERITY_RANK[String(ra.severity)] ?? 3; const sevB = SEVERITY_RANK[String(rb.severity)] ?? 3; if (sevA !== sevB) return sevA - sevB; const fileA = diagnosticTarget(ra); const fileB = diagnosticTarget(rb); if (fileA !== fileB) return fileA < fileB ? -1 : 1; const lineA = typeof ra.line === "number" ? ra.line : 0; const lineB = typeof rb.line === "number" ? rb.line : 0; if (lineA !== lineB) return lineA - lineB; const ruleA = asNonEmptyString(ra.ruleId) ?? asNonEmptyString(ra.rule) ?? ""; const ruleB = asNonEmptyString(rb.ruleId) ?? asNonEmptyString(rb.rule) ?? ""; return ruleA < ruleB ? -1 : ruleA > ruleB ? 1 : 0; } function formatDiagnosticItem(item: unknown): string { if (typeof item === "string") return item; if (!item || typeof item !== "object") return JSON.stringify(item); const record = item as Record; const parts: string[] = []; const ruleOrSeverity = asNonEmptyString(record.ruleId) ?? asNonEmptyString(record.rule) ?? asNonEmptyString(record.severity); if (ruleOrSeverity) parts.push(ruleOrSeverity); // RFC-0203: locator in the canonical file:line:column form so editors and // agents can jump straight to the violation. const target = asNonEmptyString(record.file) ?? asNonEmptyString(record.path) ?? asNonEmptyString(record.target); if (target) { let locator = target; if (typeof record.line === "number") { locator += `:${record.line}`; if (typeof record.column === "number") locator += `:${record.column}`; } parts.push(locator); } const message = asNonEmptyString(record.message); if (message) parts.push(message); const head = parts.length > 0 ? parts.join(" · ") : JSON.stringify(item); // RFC-0203: surface the remediation on its own line so an agent reading the // console knows not just what is wrong but how to fix it. const fix = asNonEmptyString(record.fixHint) ?? asNonEmptyString(record.suggestion); return fix ? `${head}\nfix: ${fix}` : head; } function asNonEmptyString(value: unknown): string | null { return typeof value === "string" && value.length > 0 ? value : null; }