import type { ValidationResult, WizardState, WizardTrackConfig, WizardStageConfig } from '@/types/pipeline-wizard' function ok(): ValidationResult { return { valid: true, errors: [], warnings: [] } } function fail(errors: string[], warnings: string[] = []): ValidationResult { return { valid: false, errors, warnings } } export function validateBasics(state: WizardState): ValidationResult { const errors: string[] = [] if (!state.name.trim()) errors.push('Pipeline name is required') if (!state.slug.trim()) errors.push('Pipeline slug is required') else if (!/^[a-z0-9-]+$/.test(state.slug)) errors.push('Slug must be lowercase alphanumeric with hyphens only') if (!state.programId) errors.push('Program must be selected') return errors.length ? fail(errors) : ok() } export function validateStage(stage: WizardStageConfig): ValidationResult { const errors: string[] = [] if (!stage.name.trim()) errors.push(`Stage name is required`) if (!stage.slug.trim()) errors.push(`Stage slug is required`) else if (!/^[a-z0-9-]+$/.test(stage.slug)) errors.push(`Stage slug "${stage.slug}" is invalid`) return errors.length ? fail(errors) : ok() } export function validateTrack(track: WizardTrackConfig): ValidationResult { const errors: string[] = [] const warnings: string[] = [] if (!track.name.trim()) errors.push('Track name is required') if (!track.slug.trim()) errors.push('Track slug is required') if (track.stages.length === 0) errors.push(`Track "${track.name}" must have at least one stage`) // Check for duplicate slugs within track const slugs = new Set() for (const stage of track.stages) { if (slugs.has(stage.slug)) { errors.push(`Duplicate stage slug "${stage.slug}" in track "${track.name}"`) } slugs.add(stage.slug) const stageResult = validateStage(stage) errors.push(...stageResult.errors) } // MAIN track should ideally have at least INTAKE and one other stage if (track.kind === 'MAIN' && track.stages.length < 2) { warnings.push('Main track should have at least 2 stages') } // AWARD tracks need awardConfig if (track.kind === 'AWARD' && !track.awardConfig?.name) { errors.push(`Award track "${track.name}" requires an award name`) } return errors.length ? fail(errors, warnings) : { valid: true, errors: [], warnings } } export function validateTracks(tracks: WizardTrackConfig[]): ValidationResult { const errors: string[] = [] const warnings: string[] = [] if (tracks.length === 0) { errors.push('At least one track is required') return fail(errors) } const mainTracks = tracks.filter((t) => t.kind === 'MAIN') if (mainTracks.length === 0) { errors.push('At least one MAIN track is required') } else if (mainTracks.length > 1) { warnings.push('Multiple MAIN tracks detected — typically only one is needed') } // Check for duplicate track slugs const trackSlugs = new Set() for (const track of tracks) { if (trackSlugs.has(track.slug)) { errors.push(`Duplicate track slug "${track.slug}"`) } trackSlugs.add(track.slug) const trackResult = validateTrack(track) errors.push(...trackResult.errors) warnings.push(...trackResult.warnings) } return errors.length ? fail(errors, warnings) : { valid: true, errors: [], warnings } } export function validateNotifications(config: Record): ValidationResult { // Notifications are optional — just validate structure return ok() } export function validateAll(state: WizardState): { valid: boolean sections: { basics: ValidationResult tracks: ValidationResult notifications: ValidationResult } } { const basics = validateBasics(state) const tracks = validateTracks(state.tracks) const notifications = validateNotifications(state.notificationConfig) return { valid: basics.valid && tracks.valid && notifications.valid, sections: { basics, tracks, notifications }, } }