/*
defineStackChecks — the spec-driven check harness for stack plugins
(RFC-1100). A plugin declares one StackCheckSpec table; the harness derives kernel
command declarations, the checkGate hook body, and invariant table rows from it.
Validator files keep only a pure check() function — they never shape
KernelCommandResult or register commands themselves.
- Does not implement stack-specific rules — those live in the plugin's check() functions.
- Does not read files — check() functions consume the plugin's model/walker seams.
- RFC-1100: initial spec-driven check harness — StackCheckSpec, StackCheckViolation, StackCheckData, defineStackChecks returning commands + runCheckGate + invariantRows.
- RFC-1100: step 1 — shared tool/fs/check seams (walkFiles, runTool, defineStackChecks)
- RFC-1173: declare mutatesState on all kernel commands — collectDeclarationDiagnostics emits error-severity MUTATES-STATE-DECLARED, command.manifest.validate is the blocking consumer in packages.check, sweep declares the flag on every command definition (factories hardcode false for read-only check specs)
- RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt
*/
import type {
KernelCommandDefinition,
KernelCommandResult,
} from "@warpgogol/werkstatt-shared/kernel";
import type { HookResult, PluginHookContext } from "../plugin/plugin-contract.ts";
/** A single rule violation emitted by a stack check() function. */
export interface StackCheckViolation {
ruleId: string;
file: string;
message: string;
line?: number;
details?: Record;
}
/** Uniform data payload every stack check command returns. */
export interface StackCheckData {
command: string;
status: "pass" | "fail";
violations: StackCheckViolation[];
}
export interface StackCheckSpec {
/** Kernel command name, e.g. "phaser.scenes.validate". */
name: string;
/** Human-readable command description. */
description: string;
/** The single contract this check enforces (DNA-91). */
contract: string;
/** Rule IDs this check emits (DNA-91). */
rules: string[];
/** Workspace-relative path globs this check reads (RFC-0266). */
reads: string[];
/** Invariant ID for invariantRows — defaults to rules[0]. */
invariantId?: string;
/** Pure rule logic: project root in, violations out. */
check: (projectRoot: string) => Promise;
}
export interface StackCheckDeclarations {
/** Kernel command declarations, one per spec entry. */
commands: KernelCommandDefinition[];
/** The plugin's checkGate hook body — runs every spec in order. */
runCheckGate: (ctx: PluginHookContext) => Promise;
/** Invariant table rows derived from the spec — { id, command } pairs. */
invariantRows: Array<{ id: string; command: string }>;
}
function toCommand(spec: StackCheckSpec): KernelCommandDefinition {
return {
name: spec.name,
// RFC-1173: stack checks are structurally read-only — StackCheckSpec has
// no writes field and check() only returns violations.
mutatesState: false,
description: spec.description,
contract: spec.contract,
rules: spec.rules,
reads: spec.reads,
scope: "workspace",
cacheable: false,
async execute(_input, context): Promise> {
const violations = await spec.check(context.workspaceRoot);
const status = violations.length === 0 ? "pass" : "fail";
return {
data: { command: spec.name, status, violations },
exitCode: status === "pass" ? 0 : 1,
summary: `[stack.checks] ${spec.name}: ${status} (${violations.length} violations)`,
};
},
};
}
/** Short gate-log label: "phaser.scenes.validate" → "scenes". */
function gateLabel(commandName: string): string {
return commandName.split(".")[1] ?? commandName;
}
export function defineStackChecks(specs: StackCheckSpec[]): StackCheckDeclarations {
const commands = specs.map(toCommand);
async function runCheckGate(ctx: PluginHookContext): Promise {
const projectRoot = ctx.workpiecePath ?? ctx.workspaceRoot;
const errors: string[] = [];
const statuses: string[] = [];
for (const spec of specs) {
const violations = await spec.check(projectRoot);
const status = violations.length === 0 ? "pass" : "fail";
statuses.push(`${gateLabel(spec.name)}=${status}`);
if (violations.length > 0) {
errors.push(`${spec.name}: ${violations.length} violations`);
}
}
ctx.logger.info(`checkGate: ${statuses.join(", ")}`);
return {
success: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
};
}
const invariantRows = specs
.map((spec) => ({ id: spec.invariantId ?? spec.rules[0], command: spec.name }))
.filter((row): row is { id: string; command: string } => typeof row.id === "string");
return { commands, runCheckGate, invariantRows };
}