/*
Small helpers shared between runtime/execute-command.ts and runtime/execute-pipeline.ts:
log-severity summarization for an execution report, the option-object shape guard used
by both execute* entry points, and the skipped-execution report builder.
- Do not introduce app-specific runtime composition or deployment behavior into this reusable package source file.
- Shared helpers stay pure — no I/O, no state, just transformations.
- ADR-0087: add skippedExecutionReport — shared zero-duration Skipped-report builder used by the pipeline step loop and the executor-level closed-workpiece guard.
- RFC-0303: split out of runtime.ts (Phase 3 file-size split, hot-path file 8/8).
- RFC-1097: step 6 — compass.migrate codemod run
Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order.
- RFC-1097: sweep — werkstatt-engine clean
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.
- RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt
*/
import type {
KernelCommandDefinition,
KernelExecutionReport,
KernelRuntimeContext,
} from "@warpgogol/werkstatt-shared/kernel";
export function summarizeLogs(
logs: KernelExecutionReport["logs"],
): KernelExecutionReport["logSummary"] {
return {
error: logs.filter((event) => event.severity === "error" || event.level === "error").length,
warning: logs.filter((event) => event.severity === "warning" || event.level === "warn").length,
notice: logs.filter((event) => event.severity === "notice").length,
expectedFallback: logs.filter((event) => event.kind === "expected-fallback").length,
suppressedDebug: logs.filter((event) => event.severity === "debug").length,
};
}
// ---------------------------------------------------------------------------
// RFC-0260: strict shape validation for the execute* options objects.
// ---------------------------------------------------------------------------
// Regression guard for commit 8b3e62ab, where `args:` was passed instead of
// `argv:` and an `as any` cast let it through silently. These options objects
// are internal call-site contracts (not user CLI input), so a first-party
// Levenshtein nearest-key hint is enough — no schema library needed.
function levenshteinDistance(a: string, b: string): number {
const rows = a.length + 1;
const cols = b.length + 1;
const dp: number[][] = Array.from({ length: rows }, () => new Array(cols).fill(0));
for (let i = 0; i < rows; i += 1) dp[i]![0] = i;
for (let j = 0; j < cols; j += 1) dp[0]![j] = j;
for (let i = 1; i < rows; i += 1) {
for (let j = 1; j < cols; j += 1) {
dp[i]![j] =
a[i - 1] === b[j - 1]
? dp[i - 1]![j - 1]!
: 1 + Math.min(dp[i - 1]![j]!, dp[i]![j - 1]!, dp[i - 1]![j - 1]!);
}
}
return dp[rows - 1]![cols - 1]!;
}
function nearestKeyHint(key: string, allowedKeys: string[]): string | undefined {
let best: string | undefined;
let bestDistance = Infinity;
for (const candidate of allowedKeys) {
const distance = levenshteinDistance(key, candidate);
if (distance < bestDistance) {
bestDistance = distance;
best = candidate;
}
}
return bestDistance <= 3 ? best : undefined;
}
export function assertKnownOptionKeys(options: object, allowedKeys: string[], label: string): void {
const unknown = Object.keys(options).filter((key) => !allowedKeys.includes(key));
if (unknown.length === 0) return;
const described = unknown.map((key) => {
const hint = nearestKeyHint(key, allowedKeys);
return hint ? `"${key}" (did you mean "${hint}"?)` : `"${key}"`;
});
throw new Error(
`${label} received unknown key(s): ${described.join(", ")}. Valid keys: ${allowedKeys.join(", ")}.`,
);
}
/**
* Build a zero-duration `Skipped: ` report for a command that was not
* executed. Shared by the pipeline step loop (transitive-cache / closed-mission
* skips) and the executor-level closed-workpiece guard (ADR-0087).
*/
export function skippedExecutionReport(
command: KernelCommandDefinition,
context: KernelRuntimeContext,
reason?: string,
): KernelExecutionReport {
// `Skipped: ` is a pipeline protocol prefix — execute-pipeline
// matches `startsWith("Skipped: closed-mission")`; not user-facing summary.
const report: KernelExecutionReport = {
siteName: context.site?.name,
commandName: command.name,
exitCode: 0,
ok: true,
summary: `Skipped: ${reason ?? "pipeline step marked skip"}`,
metadata: command,
logs: context.logger.getEvents(),
logSummary: summarizeLogs(context.logger.getEvents()),
timing: {
durationMs: 0,
exceededTimeout: false,
},
filesModified: [],
};
return report;
}