/** * Mandu Diagnose — aggregator. * * Runs all registered checks in parallel (they are pure I/O, no shared * mutable state) and builds a unified `DiagnoseReport`. */ import type { DiagnoseCheckResult, DiagnoseReport } from "./types"; import { checkManifestFreshness, checkPrerenderPollution, checkCloneElementWarnings, checkDevArtifactsInProd, checkPackageExportGaps, checkNestedInternalCore, checkA11yHints, checkClientBoundaryManifests, } from "./checks"; /** * Registered extended checks (Issue #215 + Phase 18.χ). * * Order matters for display purposes only — result aggregation is * order-independent. The a11y smoke is last because it's the most * expensive check by an order of magnitude (dynamic import + DOM * parse + axe rules) and we prefer failing fast on cheaper structural * checks first. */ export const EXTENDED_CHECKS = [ { name: "manifest_freshness", run: checkManifestFreshness }, { name: "prerender_pollution", run: checkPrerenderPollution }, { name: "cloneelement_warnings", run: checkCloneElementWarnings }, { name: "dev_artifacts_in_prod", run: checkDevArtifactsInProd }, { name: "package_export_gaps", run: checkPackageExportGaps }, { name: "nested_internal_core", run: checkNestedInternalCore }, { name: "client_boundary_manifests", run: checkClientBoundaryManifests }, { name: "a11y_hints", run: checkA11yHints }, ] as const; /** * Run every extended check in parallel and return the aggregate report. * * Legacy checks (kitchen_errors, guard_check, contract_validation, * manifest_validation) are NOT run here — they live in the MCP tool * surface and have different dependency requirements. Instead, the MCP * `mandu.diagnose` composite combines the extended checks with the * legacy ones and normalizes both into a single unified report. */ export async function runExtendedDiagnose(rootDir: string): Promise { const checks = await Promise.all( EXTENDED_CHECKS.map(async ({ run }) => { try { return await run(rootDir); } catch (err) { // Defensive fallback — individual checks shouldn't throw, but if // one does, we surface it as an error rather than crashing the // whole diagnose run. return { ok: false, rule: "diagnose_internal_error", severity: "error" as const, message: `Check threw an error: ${err instanceof Error ? err.message : String(err)}`, }; } }) ); return buildReport(checks); } /** * Aggregate a pre-computed list of check results into a `DiagnoseReport`. * Exported so the MCP composite can pass in both extended + legacy checks * at once. */ export function buildReport(checks: DiagnoseCheckResult[]): DiagnoseReport { let errorCount = 0; let warningCount = 0; for (const c of checks) { if (c.ok) continue; if (c.severity === "error") errorCount += 1; else if (c.severity === "warning") warningCount += 1; } return { healthy: errorCount === 0, errorCount, warningCount, checks, summary: { total: checks.length, passed: checks.filter((c) => c.ok).length, failed: checks.filter((c) => !c.ok).length, }, }; }