/*
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",
},
],
};
}