Files
devclaw-gitea/lib/config/schema.ts
Lauren ten Hoor 371e760d94 feat: enhance workflow and testing infrastructure
- Introduced ExecutionMode type for project execution modes (parallel, sequential).
- Updated SetupOpts to use ExecutionMode instead of string literals.
- Enhanced workflow states to include a new "In Review" state with appropriate transitions.
- Implemented TestHarness for end-to-end testing, including command interception and workspace setup.
- Created TestProvider for in-memory issue tracking during tests.
- Refactored project registration and setup tools to utilize ExecutionMode.
- Updated various tools to ensure compatibility with new workflow and execution modes.
- Added new dependencies: cockatiel for resilience and zod for schema validation.
2026-02-16 13:27:14 +08:00

115 lines
3.6 KiB
TypeScript

/**
* config/schema.ts — Zod validation for DevClaw workflow config.
*
* Validates workflow YAML at load time with clear error messages.
* Enforces: transition targets exist, queue states have roles,
* terminal states have no outgoing transitions.
*/
import { z } from "zod";
import { StateType } from "../workflow.js";
const STATE_TYPES = Object.values(StateType) as [string, ...string[]];
const TransitionTargetSchema = z.union([
z.string(),
z.object({
target: z.string(),
actions: z.array(z.string()).optional(),
description: z.string().optional(),
}),
]);
const StateConfigSchema = z.object({
type: z.enum(STATE_TYPES),
role: z.string().optional(),
label: z.string(),
color: z.string(),
priority: z.number().optional(),
description: z.string().optional(),
check: z.string().optional(),
on: z.record(z.string(), TransitionTargetSchema).optional(),
});
const WorkflowConfigSchema = z.object({
initial: z.string(),
states: z.record(z.string(), StateConfigSchema),
});
const RoleOverrideSchema = z.union([
z.literal(false),
z.object({
levels: z.array(z.string()).optional(),
defaultLevel: z.string().optional(),
models: z.record(z.string(), z.string()).optional(),
emoji: z.record(z.string(), z.string()).optional(),
completionResults: z.array(z.string()).optional(),
}),
]);
const TimeoutConfigSchema = z.object({
gitPullMs: z.number().positive().optional(),
gatewayMs: z.number().positive().optional(),
sessionPatchMs: z.number().positive().optional(),
dispatchMs: z.number().positive().optional(),
staleWorkerHours: z.number().positive().optional(),
}).optional();
export const DevClawConfigSchema = z.object({
roles: z.record(z.string(), RoleOverrideSchema).optional(),
workflow: WorkflowConfigSchema.partial().optional(),
timeouts: TimeoutConfigSchema,
});
/**
* Validate a raw parsed config object.
* Returns the validated config or throws with a descriptive error.
*/
export function validateConfig(raw: unknown): void {
DevClawConfigSchema.parse(raw);
}
/**
* Validate structural integrity of a fully-resolved workflow config.
* Checks cross-references that Zod schema alone can't enforce:
* - All transition targets point to existing states
* - Queue states have a role assigned
* - Terminal states have no outgoing transitions
*/
export function validateWorkflowIntegrity(
workflow: { initial: string; states: Record<string, { type: string; role?: string; on?: Record<string, unknown> }> },
): string[] {
const errors: string[] = [];
const stateKeys = new Set(Object.keys(workflow.states));
if (!stateKeys.has(workflow.initial)) {
errors.push(`Initial state "${workflow.initial}" does not exist in states`);
}
for (const [key, state] of Object.entries(workflow.states)) {
if (state.type === StateType.QUEUE && !state.role) {
errors.push(`Queue state "${key}" must have a role assigned`);
}
if (state.type === StateType.ACTIVE && !state.role) {
errors.push(`Active state "${key}" must have a role assigned`);
}
if (state.type === StateType.TERMINAL && state.on && Object.keys(state.on).length > 0) {
errors.push(`Terminal state "${key}" should not have outgoing transitions`);
}
if (state.on) {
for (const [event, transition] of Object.entries(state.on)) {
const target = typeof transition === "string"
? transition
: (transition as { target: string }).target;
if (!stateKeys.has(target)) {
errors.push(`State "${key}" transition "${event}" targets non-existent state "${target}"`);
}
}
}
}
return errors;
}