/* RFC-0883: Post-build-only validation command for fast iterative debugging. Runs SITES_CHECK_POSTBUILD_PIPELINE on an existing dist/ without a full rebuild. Do not run build.prepare, build.check, or Astro build — use mission.validate for the full pipeline. Do not replace mission.validate as the authoritative validation command. RFC-0883: initial implementation. RFC-1144: run read-only steps to completion in collect-errors pipeline mode Scheduler: collectErrors option on executeScheduledSteps — read-only steps (ScheduledStep.readOnly, executor-resolved from mutatesState === false) run after dependency failures with ranAfterFailedDependency annotation; ranTainted set propagates taint transitively so mutating dependents still skip. Executor: mergeCollectErrorsOption OR-merges PipelineDeclaration.collectErrors with the CLI flag at registry resolution; readOnly annotated post-buildSchedule; taint flag copied to step reports; tainted failures marked in pretty output. Contracts: PipelineDeclaration.collectErrors, KernelExecutionReport.ranAfterFailedDependency, pipelines maps store full declarations. Declarations: collectErrors on validation-gate registrations (build.post, check, build.check, compass, integrity.release in the workpiece template; packages.check in checks module). Tests: 10 new cases covering AC-1..AC-6 plus transitive taint. RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt RFC-1254: drop unused io-migration leftovers */ import { performance } from "node:perf_hooks"; import path from "node:path"; import type { DiscoveredSiteWorkspace, KernelCommandInput, KernelCommandResult, KernelExecutionReport, KernelPipelineReport, KernelRuntimeContext, } from "@warpgogol/werkstatt-engine/kernel"; import { executeKernelCommand, executeKernelPipeline, loadAppRuntime, resolveSiteWorkspace, } from "@warpgogol/werkstatt-engine/kernel"; import { readMissionManifest, resolveMissionDir } from "./mission-io.ts"; export interface ValidatePostbuildStepResult { name: string; status: "ok" | "fail" | "skip" | "warn"; durationMs: number; } export interface ValidatePostbuildData { command: "validate.postbuild"; status: "pass" | "fail"; steps: ValidatePostbuildStepResult[]; totalDurationMs: number; distPath: string; } const SLOW_STEPS = new Set([ "mobile.layout.check", "lighthouse.budget.check", "qa.independent.run", ]); function flagString(input: KernelCommandInput, key: string): string | undefined { const v = input.flags[key]; return typeof v === "string" ? v : undefined; } function stepStatusFromReport(report: KernelExecutionReport): "ok" | "fail" | "warn" { if (!report.ok) return "fail"; const logSummary = report.logSummary; if (logSummary && logSummary.warning > 0) return "warn"; return "ok"; } export async function runValidatePostbuild( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const { workspaceRoot, logger } = context; const missionId = flagString(input, "mission"); const siteId = flagString(input, "site"); const skipSlow = input.flags["skip-slow"] === true; if (!missionId && !siteId) { throw new Error("[validate.postbuild] --mission or --site is required"); } let siteWorkspace: DiscoveredSiteWorkspace; let siteName: string; if (missionId) { const manifest = await readMissionManifest(workspaceRoot, missionId, context.io); const missionDir = await resolveMissionDir(workspaceRoot, missionId, context.io); const workpieceDir = path.join(missionDir, "workpiece"); siteName = manifest.systemId; siteWorkspace = { name: manifest.systemId, directory: workpieceDir, toolsDirectory: path.join(workpieceDir, "tools"), configPath: path.join(workpieceDir, "tools", "kernel.config.ts"), }; } else { const sw = await resolveSiteWorkspace(workspaceRoot, siteId!); siteName = sw.name; siteWorkspace = sw; } const distPath = path.join(siteWorkspace.directory, "dist"); if (!(await context.io.exists(distPath))) { return { exitCode: 1, summary: "[validate.postbuild] No dist/ found — run mission.validate first to build the site.", nextSteps: [ { action: "Run: pnpm exec werkstatt run mission.validate --mission ", kind: "required", }, ], data: { command: "validate.postbuild", status: "fail", steps: [], totalDurationMs: 0, distPath, }, }; } logger.warn("dist/ may be stale — run mission.validate for a full check."); const startTime = performance.now(); if (!skipSlow) { const result = await executeKernelPipeline({ workspaceRoot, pipelineName: "sites-check.postbuild", siteName, outputFormat: "pretty", siteWorkspace, }); const report: KernelPipelineReport = Array.isArray(result) ? result[0]! : result; const steps: ValidatePostbuildStepResult[] = report.steps.map((step) => ({ name: step.commandName, status: stepStatusFromReport(step), durationMs: step.timing.durationMs, })); const totalDurationMs = Math.round(performance.now() - startTime); return { data: { command: "validate.postbuild", status: report.ok ? "pass" : "fail", steps, totalDurationMs, distPath, }, exitCode: report.ok ? 0 : 1, summary: report.ok ? "[validate.postbuild] All post-build validators passed." : `[validate.postbuild] Post-build validation failed: ${report.timing.failedStep ?? "unknown step"}`, nextSteps: report.ok ? undefined : [ { action: `Fix the failing post-build validator (${report.timing.failedStep ?? "unknown"}), then re-run: pnpm exec werkstatt run validate.postbuild --site ${siteName}`, kind: "required", }, ], }; } // Skip-slow mode: resolve steps from the registry and run individually. const { registry } = await loadAppRuntime(workspaceRoot, siteWorkspace); const pipelineDecl = registry.pipelines.get("sites-check.postbuild"); if (!pipelineDecl) { throw new Error(`Pipeline "sites-check.postbuild" is not registered for site "${siteName}".`); } const filteredSteps = pipelineDecl.steps.filter((step) => !SLOW_STEPS.has(step.command)); const stepResults: ValidatePostbuildStepResult[] = []; let allOk = true; for (const step of filteredSteps) { const stepStart = performance.now(); try { const result = await executeKernelCommand({ workspaceRoot, commandName: step.command, siteName, outputFormat: "pretty", argv: step.args ?? [], }); const report: KernelExecutionReport = Array.isArray(result) ? result[0]! : result; const durationMs = Math.round(performance.now() - stepStart); stepResults.push({ name: step.command, status: stepStatusFromReport(report), durationMs, }); if (!report.ok) { allOk = false; logger.info(` ${step.command} failed (exit ${report.exitCode})`); } } catch (err) { const durationMs = Math.round(performance.now() - stepStart); stepResults.push({ name: step.command, status: "fail", durationMs, }); allOk = false; logger.info(` ${step.command} failed: ${err instanceof Error ? err.message : String(err)}`); } } for (const step of pipelineDecl.steps) { if (SLOW_STEPS.has(step.command)) { stepResults.push({ name: step.command, status: "skip", durationMs: 0, }); } } const totalDurationMs = Math.round(performance.now() - startTime); return { data: { command: "validate.postbuild", status: allOk ? "pass" : "fail", steps: stepResults, totalDurationMs, distPath, }, exitCode: allOk ? 0 : 1, summary: allOk ? `[validate.postbuild] All post-build validators passed (slow steps skipped).` : `[validate.postbuild] Post-build validation failed.`, nextSteps: allOk ? undefined : [ { action: `Fix the failing post-build validators above, then re-run: pnpm exec werkstatt run validate.postbuild --site ${siteName}`, kind: "required", }, ], }; }