/*
RFC-0971: mission.preflight — runs ownership.sync.validate and
generated.stale.validate against the cache clone (systems-cache//)
before a mission is opened. Read-only per K-0004 (evaluation must not
mutate its subject). Dispatches to existing check commands via
executeRegisteredCommand with a custom KernelRuntimeContext where
site.directory points to the cache clone path.
- Do not sync the cache clone — syncing is a mutating operation (sternsystem.sync); preflight is read-only per K-0004.
- Do not create a generates.coverage.validate command — RFC-0970 explicitly did not create one; ownership.sync.validate already performs the coverage check.
- Do not replace mission.validate — preflight is a subset check that runs before materialization.
- Do not run build pipelines — preflight is metadata-only.
- Do not check content validity — that is the domain of author pipeline checks.
- RFC-0971: initial implementation.
- RFC-0972: removed handler-local isJson exit code workaround — CLI layer now handles --json exit code globally.
- RFC-1157: resolve check commands from site-module ActualStates; merge computeOwnershipMap output deduped by (command, artifact.path).
- RFC-1254: drop unused io-migration leftovers
*/
import { join } from "node:path";
import type {
CheckResult,
KernelCommandInput,
KernelCommandResult,
KernelExecutionReport,
KernelNextStep,
KernelRuntimeContext,
DiscoveredSiteWorkspace,
} from "@warpgogol/werkstatt-engine/kernel";
import { executeRegisteredCommand, siteModuleLoaders } from "@warpgogol/werkstatt-engine/kernel";
import { computeOwnershipMap } from "../kernel/runtime/execute-command.ts";
import { getOrBuildSiteModuleStates } from "../kernel/runtime/registry-cache.ts";
import { resolveCacheClonePath } from "../sternsystem/registry-io.ts";
const PREFLIGHT_CHECKS = ["ownership.sync.validate", "generated.stale.validate"] as const;
export interface MissionPreflightInput {
system: string;
}
export interface PreflightCheckResult {
command: string;
status: "pass" | "fail";
violations: number;
}
export interface MissionPreflightData {
command: "mission.preflight";
system: string;
status: "pass" | "fail";
checks: PreflightCheckResult[];
totalViolations: number;
}
function countViolations(report: KernelExecutionReport): number {
const data = report.data as CheckResult | undefined;
if (!data || !data.diagnostics) return 0;
return data.diagnostics.filter((d) => d.severity === "error").length;
}
function buildNextSteps(checks: PreflightCheckResult[]): KernelNextStep[] {
const failed = checks.filter((c) => c.status === "fail");
if (failed.length === 0) return [];
return [
{
action: `Fix ownership map violations reported by: ${failed.map((c) => c.command).join(", ")}`,
kind: "required",
},
];
}
export async function runMissionPreflight(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const systemId = input.flags.system as string | undefined;
if (!systemId) {
return {
data: {
command: "mission.preflight",
system: "",
status: "fail",
checks: [],
totalViolations: 0,
},
exitCode: 1,
summary: "[mission.preflight] missing required --system flag",
nextSteps: [{ action: "Provide --system flag", kind: "required" }],
};
}
const cacheClonePath = resolveCacheClonePath(context.workspaceRoot, systemId);
if (!(await context.io.exists(cacheClonePath))) {
return {
data: {
command: "mission.preflight",
system: systemId,
status: "fail",
checks: [],
totalViolations: 0,
},
exitCode: 1,
summary: `[mission.preflight] Cache clone not found at ${cacheClonePath}. Run sternsystem.register first.`,
nextSteps: [
{
action: `Run: pnpm exec werkstatt run sternsystem.register --id ${systemId}`,
kind: "required",
},
],
};
}
const cacheCloneSite: DiscoveredSiteWorkspace = {
name: systemId,
directory: cacheClonePath,
toolsDirectory: join(cacheClonePath, "tools"),
};
// RFC-1157: after RFC-1147 the check commands AND their `generates:`
// declarations live in site-module ActualStates, not the workspace one.
// Materialize the canonical site module set once (memoized per
// workspaceRoot) — it resolves the commands AND feeds buildGeneratorOwnership
// so context.ownershipMap covers site generators, not just workspace ones.
let siteStates: Awaited> = new Map();
try {
siteStates = await getOrBuildSiteModuleStates(context.workspaceRoot, siteModuleLoaders());
} catch (err) {
context.logger.warn(
`[mission.preflight] site-module states unavailable: ${err instanceof Error ? err.message : String(err)}`,
);
}
const ownershipMap = [...(context.ownershipMap ?? [])];
// RFC-1157: same-execute commands may be registered by several site modules
// (RFC-0816) — dedup merged entries so each (command, artifact path) pair
// counts once instead of tripping OWN-DUP-01.
const seenOwnership = new Set(ownershipMap.map((e) => `${e.command}:${e.artifact.path}`));
for (const state of siteStates.values()) {
const siteMap = await computeOwnershipMap(state);
if (!siteMap) continue;
for (const entry of siteMap) {
const key = `${entry.command}:${entry.artifact.path}`;
if (seenOwnership.has(key)) continue;
seenOwnership.add(key);
ownershipMap.push(entry);
}
}
const cacheCloneContext: KernelRuntimeContext = {
...context,
site: cacheCloneSite,
ownershipMap,
};
const checks: PreflightCheckResult[] = [];
let totalViolations = 0;
for (const checkName of PREFLIGHT_CHECKS) {
let command = context.actualState.commands.get(checkName);
if (!command) {
for (const state of siteStates.values()) {
command = state.commands.get(checkName);
if (command) break;
}
}
if (!command) {
checks.push({ command: checkName, status: "fail", violations: 0 });
totalViolations += 1;
context.logger.warn(`[mission.preflight] Command ${checkName} is not registered`);
continue;
}
const report = await executeRegisteredCommand(command, cacheCloneContext, []);
const violations = countViolations(report);
const status = violations > 0 ? "fail" : "pass";
checks.push({ command: checkName, status, violations });
totalViolations += violations;
}
const overallStatus = totalViolations > 0 ? "fail" : "pass";
const exitCode = totalViolations > 0 ? 1 : 0;
const data: MissionPreflightData = {
command: "mission.preflight",
system: systemId,
status: overallStatus,
checks,
totalViolations,
};
const summary =
overallStatus === "pass"
? `[mission.preflight] ${systemId}: all ${checks.length} checks passed`
: `[mission.preflight] ${systemId}: ${totalViolations} violation(s) across ${checks.filter((c) => c.status === "fail").length} check(s)`;
return {
data,
exitCode,
summary,
nextSteps: buildNextSteps(checks),
};
}