import type { DoctorCheck, DoctorCheckId, DoctorReport, DoctorResult } from "./doctor.js"; interface DoctorSection { readonly checkIds: readonly DoctorCheckId[]; readonly title: string; } function section(title: string, checkIds: readonly DoctorCheckId[]): DoctorSection { return Object.freeze({ checkIds: Object.freeze([...checkIds]), title }); } const sections: readonly DoctorSection[] = Object.freeze([ section("Platform", ["platform.runtime"]), section("Modules", [ "modules.discovery", "modules.dependencies", "modules.permissions", "modules.lifecycle", "modules.configuration", ]), section("Data definitions", ["schemas.discovery", "migrations.discovery"]), section("PostgreSQL", ["database.transport", "postgresql.connection", "postgresql.plan"]), section("Identity", ["identity.configuration", "identity.client-address"]), section("Object storage", ["object-storage.integration", "object-storage.bindings"]), section("Outbound email", ["outbound-email.configuration"]), ]); /** * Every check id, exactly once. * * A check absent from `sections` is still computed and still counted toward the overall result, but * is never shown — so a configuration failure would flip an operator's report to NOT READY with * nothing on screen explaining why. `outbound-email.configuration` was in precisely that state, and * `doctor-report.test.ts` now compares this list against the ids of a real report, so a new check * cannot be added without being rendered. */ export const RENDERED_DOCTOR_CHECK_IDS: readonly DoctorCheckId[] = Object.freeze( sections.flatMap((entry) => [...entry.checkIds]), ); const statusSymbols: Readonly> = Object.freeze({ failure: "✗", pass: "✓", skipped: "○", warning: "!", }); const resultLabels: Readonly> = Object.freeze({ "not-ready": "NOT READY", ready: "READY", "ready-with-warnings": "READY WITH WARNINGS", }); export function formatDoctorReport(report: DoctorReport): readonly string[] { const checks = new Map(report.checks.map((check) => [check.id, check])); const lines = ["Ekklesia Doctor", ""]; for (const [sectionIndex, section] of sections.entries()) { lines.push(section.title); for (const checkId of section.checkIds) { const check = checks.get(checkId); if (!check) { continue; } lines.push(` ${statusSymbols[check.status]} ${check.summary}`); for (const detail of check.details) { lines.push(` ${detail}`); } } if (sectionIndex < sections.length - 1) { lines.push(""); } } lines.push("", `Result: ${resultLabels[report.result]}`); return Object.freeze(lines); }