From c181bad94e3f0ad3dcdedf0d94dc25f0844aeebd Mon Sep 17 00:00:00 2001 From: Shakker Date: Fri, 1 May 2026 10:45:03 +0100 Subject: [PATCH] feat: add markdown failure cards --- src/report.mjs | 44 +++++++++++++++++++++++++++++++++ src/selfcheck.mjs | 62 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/src/report.mjs b/src/report.mjs index cabb262..27d757d 100644 --- a/src/report.mjs +++ b/src/report.mjs @@ -33,6 +33,9 @@ export function renderMarkdownReport(report) { ...Object.entries(report.summary.statuses).map(([status, count]) => `- ${status}: ${count}`), "" ); + if (!report.gate) { + lines.push(...formatRecordFailureCards(report.records)); + } if (report.gate) { lines.push(...formatGateSection(report.gate)); } @@ -464,6 +467,47 @@ function formatMetrics(metrics) { return lines.length > 0 ? lines : ["- unavailable"]; } +function formatRecordFailureCards(records = []) { + const cards = records + .filter((record) => !["PASS", "DRY-RUN"].includes(record.status)) + .map(recordFailureCard); + if (cards.length === 0) { + return []; + } + + const lines = ["## Failure Cards", ""]; + for (const card of cards.slice(0, 8)) { + lines.push(`- ${card.status} ${card.scenario}${card.state ? `/${card.state}` : ""}: ${card.summary}`); + lines.push(` - likely owner: ${card.likelyOwner}`); + if (card.command) { + lines.push(` - command: \`${card.command}\``); + } + for (const item of card.evidence.slice(0, 4)) { + lines.push(` - evidence: ${item}`); + } + } + if (cards.length > 8) { + lines.push(`- ${cards.length - 8} additional failure card(s) omitted from Markdown. See JSON report for full records.`); + } + lines.push(""); + return lines; +} + +function recordFailureCard(record) { + const failed = firstFailedCommand(record); + const violationMessages = (record.violations ?? []).map((violation) => violation.message); + const summary = violationMessages[0] ?? summarizeFailureReason(failed) ?? `${record.status} ${record.scenario}`; + return { + status: record.status, + scenario: record.scenario, + state: record.state?.id ?? null, + summary, + likelyOwner: record.likelyOwner ?? "OpenClaw", + command: failed?.command ? shortCommand(failed.command) : null, + evidence: briefEvidence(record.measurements ?? {}, violationMessages) + }; +} + function indentFence(value) { return [" ```text", ...value.trim().split("\n").slice(0, 80).map((line) => ` ${line}`), " ```"].join("\n"); } diff --git a/src/selfcheck.mjs b/src/selfcheck.mjs index a6c2566..f7475ee 100644 --- a/src/selfcheck.mjs +++ b/src/selfcheck.mjs @@ -206,6 +206,7 @@ export async function runSelfCheck(flags = {}) { checks.push(runtimeDepsLogParserCheck()); checks.push(runtimeDepsWarmReuseEvaluationCheck()); checks.push(await performanceBaselineCheck(tmp)); + checks.push(markdownFailureCardsCheck()); checks.push(readinessClassificationCheck()); checks.push(await resourceRoleAttributionCheck(tmp)); checks.push(await processSnapshotCheck(tmp)); @@ -3034,6 +3035,67 @@ function thresholdPolicyCalibrationCheck() { } } +function markdownFailureCardsCheck() { + try { + const rendered = renderMarkdownReport({ + generatedAt: "2026-05-01T00:00:00.000Z", + runId: "self-check-failure-cards", + mode: "execution", + target: "runtime:stable", + platform: { os: "test", release: "test", arch: "test", node: "test" }, + summary: { total: 1, statuses: { FAIL: 1 } }, + records: [{ + scenario: "gateway-performance", + title: "Gateway Performance", + status: "FAIL", + target: "runtime:stable", + envName: "kova-self-check", + likelyOwner: "gateway-runtime", + objective: "Synthetic failure card check", + phases: [{ + id: "start", + title: "Start", + intent: "Start gateway", + commands: ["ocm start kova-self-check --runtime stable --json"], + evidence: [], + results: [{ + command: "ocm start kova-self-check --runtime stable --json", + status: 1, + timedOut: false, + durationMs: 45000, + stdout: "", + stderr: "gateway did not become healthy" + }] + }], + measurements: { + timeToHealthReadyMs: 45000, + peakRssMb: 1100, + resourceTopRolesByRss: [{ role: "gateway", peakRssMb: 1100, maxCpuPercent: 220 }] + }, + violations: [{ message: "gateway readiness exceeded threshold" }] + }] + }); + assertEqual(rendered.includes("## Failure Cards"), true, "markdown failure cards section"); + assertEqual(rendered.includes("FAIL gateway-performance: gateway readiness exceeded threshold"), true, "failure card summary"); + assertEqual(rendered.includes("likely owner: gateway-runtime"), true, "failure card owner"); + assertEqual(rendered.includes("evidence: timeToHealthReadyMs: 45000"), true, "failure card evidence"); + return { + id: "markdown-failure-cards", + status: "PASS", + command: "render synthetic failure Markdown", + durationMs: 0 + }; + } catch (error) { + return { + id: "markdown-failure-cards", + status: "FAIL", + command: "render synthetic failure Markdown", + durationMs: 0, + message: error.message + }; + } +} + function stateRegistryValidationCheck() { try { let rejectedTrait = false;