UNPKG

supa-seed

Version:

A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support

624 lines 26.7 kB
"use strict"; /** * User Choice Validator System * Phase 4, Checkpoint D2 - Advanced validation for interactive configuration choices */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.UserChoiceValidator = void 0; const logger_1 = require("../core/utils/logger"); const fs = __importStar(require("fs")); class UserChoiceValidator { constructor(userPreferences) { this.validationRules = new Map(); this.systemConstraints = []; this.userPreferences = { riskTolerance: 'balanced', autoConfirmLowRisk: false, requireExplicitHighRisk: true, preferredBackupStrategy: 'high_risk_only', maxConfigurationTime: 30, notificationLevel: 'standard', ...userPreferences }; this.initializeBuiltInRules(); this.initializeSystemConstraints(); } /** * Validate a single user choice */ async validateChoice(choice, prompt, context) { logger_1.Logger.debug(`🔍 Validating choice: ${choice.optionId} for prompt ${prompt.id}`); const errors = []; const warnings = []; const suggestions = []; let totalExecutionTime = 0; let canAutoFix = false; // Run all validation rules for (const rule of this.validationRules.values()) { const startTime = Date.now(); try { const result = rule.validator(choice, prompt, context); totalExecutionTime += Date.now() - startTime; errors.push(...result.errors); warnings.push(...result.warnings); suggestions.push(...result.suggestions); if (result.metadata.canAutoFix) { canAutoFix = true; } } catch (error) { logger_1.Logger.warn(`Validation rule ${rule.id} failed: ${error.message}`); warnings.push({ code: 'RULE_EXECUTION_FAILED', message: `Validation rule '${rule.name}' failed to execute`, impact: 'low', recommendation: 'Manual verification recommended' }); } } const isValid = errors.filter(e => e.severity === 'error' || e.severity === 'critical').length === 0; return { isValid, errors, warnings, suggestions, metadata: { ruleId: 'composite', executionTime: totalExecutionTime, confidence: isValid ? 95 : 60, canAutoFix } }; } /** * Validate multiple choices for consistency and dependencies */ async validateChoiceSequence(choices, prompts, baseContext) { logger_1.Logger.info(`🔍 Validating choice sequence: ${choices.length} choices`); let totalErrors = 0; let totalWarnings = 0; let totalSuggestions = 0; let autoFixesApplied = 0; let highestRisk = 'LOW'; const recommendations = []; // Build validation context const context = { previousChoices: [], availablePrompts: prompts, schemaChanges: baseContext.schemaChanges || [], systemConstraints: this.systemConstraints, userPreferences: this.userPreferences, environment: baseContext.environment || { nodeEnv: process.env.NODE_ENV || 'development', projectPath: process.cwd(), configFiles: [], dependencies: {} } }; // Validate each choice in sequence for (let i = 0; i < choices.length; i++) { const choice = choices[i]; const prompt = prompts.find(p => p.id === choice.promptId); if (!prompt) { totalErrors++; continue; } // Update context with previous choices context.previousChoices = choices.slice(0, i); const validation = await this.validateChoice(choice, prompt, context); totalErrors += validation.errors.length; totalWarnings += validation.warnings.length; totalSuggestions += validation.suggestions.length; // Update risk level const optionRisk = this.getOptionRiskLevel(prompt, choice.optionId); if (this.isHigherRisk(optionRisk, highestRisk)) { highestRisk = optionRisk; } // Collect recommendations validation.suggestions.forEach(suggestion => { if (suggestion.confidence > 70) { recommendations.push(suggestion.description); } }); // Attempt auto-fix if needed and allowed if (!validation.isValid && validation.metadata.canAutoFix && this.userPreferences.autoConfirmLowRisk) { const autoFixedChoice = await this.autoFixChoice(choice, prompt, context); if (autoFixedChoice) { choices[i] = autoFixedChoice; autoFixesApplied++; } } } // Generate overall assessment const impactAssessment = this.assessOverallImpact(choices, prompts, context); return { totalChoices: choices.length, validChoices: choices.length - totalErrors, errorsFound: totalErrors, warningsFound: totalWarnings, suggestionsGenerated: totalSuggestions, autoFixesApplied, overallRisk: highestRisk, estimatedImpact: impactAssessment, recommendations: [...new Set(recommendations)].slice(0, 5) // Top 5 unique recommendations }; } /** * Attempt to automatically fix a choice */ async autoFixChoice(choice, prompt, context) { logger_1.Logger.debug(`🔧 Attempting auto-fix for choice: ${choice.optionId}`); for (const rule of this.validationRules.values()) { if (rule.autoFix) { try { const fixedChoice = rule.autoFix(choice, prompt, context); if (fixedChoice) { logger_1.Logger.info(`✅ Auto-fixed choice using rule: ${rule.name}`); return fixedChoice; } } catch (error) { logger_1.Logger.warn(`Auto-fix failed for rule ${rule.id}: ${error.message}`); } } } return null; } /** * Validate choice against system constraints */ validateSystemConstraints(context) { const satisfied = []; const violated = []; for (const constraint of this.systemConstraints) { try { if (constraint.validator(context)) { satisfied.push(constraint); } else { violated.push(constraint); } } catch (error) { logger_1.Logger.warn(`System constraint validation failed: ${constraint.id} - ${error.message}`); violated.push(constraint); } } return { satisfied, violated }; } /** * Add custom validation rule */ addValidationRule(rule) { this.validationRules.set(rule.id, rule); logger_1.Logger.debug(`Added validation rule: ${rule.name}`); } /** * Remove validation rule */ removeValidationRule(ruleId) { const removed = this.validationRules.delete(ruleId); if (removed) { logger_1.Logger.debug(`Removed validation rule: ${ruleId}`); } return removed; } /** * Get all validation rules */ getValidationRules() { return Array.from(this.validationRules.values()); } /** * Update user preferences */ updateUserPreferences(preferences) { this.userPreferences = { ...this.userPreferences, ...preferences }; logger_1.Logger.info('Updated user preferences'); } /** * Generate validation report */ generateValidationReport(summary) { const report = [ '📊 Configuration Choice Validation Report', '='.repeat(50), '', `Total Choices: ${summary.totalChoices}`, `Valid Choices: ${summary.validChoices}`, `Errors Found: ${summary.errorsFound}`, `Warnings Found: ${summary.warningsFound}`, `Suggestions Generated: ${summary.suggestionsGenerated}`, `Auto-fixes Applied: ${summary.autoFixesApplied}`, '', `Overall Risk Level: ${summary.overallRisk}`, `Estimated Impact: ${summary.estimatedImpact}`, '' ]; if (summary.recommendations.length > 0) { report.push('💡 Top Recommendations:'); summary.recommendations.forEach((rec, index) => { report.push(` ${index + 1}. ${rec}`); }); report.push(''); } const successRate = summary.totalChoices > 0 ? Math.round((summary.validChoices / summary.totalChoices) * 100) : 100; report.push(`✅ Validation Success Rate: ${successRate}%`); return report.join('\n'); } /** * Private: Initialize built-in validation rules */ initializeBuiltInRules() { // Option existence rule this.addValidationRule({ id: 'option-exists', name: 'Option Existence Validator', type: 'constraint', priority: 'CRITICAL', description: 'Validates that the selected option exists in the prompt', validator: (choice, prompt) => { const optionExists = prompt.options.some(opt => opt.id === choice.optionId); return { isValid: optionExists, errors: optionExists ? [] : [{ code: 'OPTION_NOT_FOUND', message: `Option '${choice.optionId}' does not exist in prompt '${prompt.id}'`, severity: 'critical', actualValue: choice.optionId, expectedValue: prompt.options.map(o => o.id) }], warnings: [], suggestions: optionExists ? [] : [{ type: 'alternative_option', description: `Consider selecting one of: ${prompt.options.map(o => o.label).join(', ')}`, rationale: 'Valid options are available', confidence: 90 }], metadata: { ruleId: 'option-exists', executionTime: 1, confidence: 100, canAutoFix: true } }; }, autoFix: (choice, prompt) => { // Auto-fix by selecting the default option const defaultOption = prompt.options.find(o => o.id === prompt.defaultOption) || prompt.options[0]; if (defaultOption) { return { ...choice, optionId: defaultOption.id, confidence: 60, // Lower confidence for auto-fix userNotes: `Auto-fixed: Selected ${defaultOption.label} (was invalid option)` }; } return null; } }); // Dependencies rule this.addValidationRule({ id: 'dependency-check', name: 'Dependency Validator', type: 'dependency', priority: 'HIGH', description: 'Validates that prompt dependencies are satisfied', validator: (choice, prompt, context) => { const unmetDependencies = prompt.dependencies.filter(depId => !context.previousChoices.some(prevChoice => prevChoice.promptId === depId)); return { isValid: unmetDependencies.length === 0, errors: unmetDependencies.map(depId => ({ code: 'UNMET_DEPENDENCY', message: `Prompt '${prompt.id}' depends on '${depId}' which has not been completed`, severity: 'error', actualValue: depId, resolution: 'Complete the dependent prompt first' })), warnings: [], suggestions: unmetDependencies.length > 0 ? [{ type: 'dependency_resolution', description: 'Complete dependent prompts before proceeding', rationale: 'Dependencies must be resolved for consistent configuration', confidence: 95 }] : [], metadata: { ruleId: 'dependency-check', executionTime: 2, confidence: 100, canAutoFix: false } }; } }); // Risk tolerance rule this.addValidationRule({ id: 'risk-tolerance', name: 'Risk Tolerance Validator', type: 'safety', priority: 'HIGH', description: 'Validates choices against user risk tolerance', validator: (choice, prompt, context) => { const selectedOption = prompt.options.find(o => o.id === choice.optionId); if (!selectedOption) return { isValid: true, errors: [], warnings: [], suggestions: [], metadata: { ruleId: 'risk-tolerance', executionTime: 0, confidence: 0, canAutoFix: false } }; const warnings = []; const suggestions = []; // Check if high-risk choice conflicts with conservative preference if (context.userPreferences.riskTolerance === 'conservative' && selectedOption.riskLevel === 'HIGH') { warnings.push({ code: 'HIGH_RISK_CONSERVATIVE', message: 'High-risk option selected with conservative risk tolerance', impact: 'medium', recommendation: 'Consider a lower-risk alternative' }); // Suggest lower-risk alternatives const lowerRiskOptions = prompt.options.filter(o => o.riskLevel === 'LOW' || o.riskLevel === 'MEDIUM'); if (lowerRiskOptions.length > 0) { suggestions.push({ type: 'alternative_option', description: `Consider lower-risk options: ${lowerRiskOptions.map(o => o.label).join(', ')}`, rationale: 'Aligns better with conservative risk tolerance', confidence: 80 }); } } // Check confidence level for high-risk choices if (selectedOption.riskLevel === 'HIGH' && choice.confidence < 70) { warnings.push({ code: 'LOW_CONFIDENCE_HIGH_RISK', message: 'High-risk option selected with low confidence', impact: 'high', recommendation: 'Increase confidence level or select a safer option' }); } return { isValid: true, // Warnings don't invalidate the choice errors: [], warnings, suggestions, metadata: { ruleId: 'risk-tolerance', executionTime: 3, confidence: 90, canAutoFix: false } }; } }); // Prerequisite validation rule this.addValidationRule({ id: 'prerequisite-check', name: 'Prerequisite Validator', type: 'constraint', priority: 'HIGH', description: 'Validates that option prerequisites are met', validator: (choice, prompt, context) => { const selectedOption = prompt.options.find(o => o.id === choice.optionId); if (!selectedOption) return { isValid: true, errors: [], warnings: [], suggestions: [], metadata: { ruleId: 'prerequisite-check', executionTime: 0, confidence: 0, canAutoFix: false } }; const errors = []; const warnings = []; // Check each prerequisite for (const prereq of selectedOption.prerequisites) { // This is a simplified check - in a real implementation, // you'd have more sophisticated prerequisite validation if (prereq.includes('file') && prereq.includes('not found')) { errors.push({ code: 'PREREQUISITE_NOT_MET', message: `Prerequisite not met: ${prereq}`, severity: 'error', resolution: 'Ensure all prerequisites are satisfied before proceeding' }); } else if (prereq.includes('manual') || prereq.includes('review')) { warnings.push({ code: 'MANUAL_PREREQUISITE', message: `Manual prerequisite: ${prereq}`, impact: 'medium', recommendation: 'Ensure manual steps are completed' }); } } return { isValid: errors.length === 0, errors, warnings, suggestions: [], metadata: { ruleId: 'prerequisite-check', executionTime: 2, confidence: 95, canAutoFix: false } }; } }); // Time budget rule this.addValidationRule({ id: 'time-budget', name: 'Time Budget Validator', type: 'business_logic', priority: 'MEDIUM', description: 'Validates choices against available time budget', validator: (choice, prompt, context) => { const selectedOption = prompt.options.find(o => o.id === choice.optionId); if (!selectedOption) return { isValid: true, errors: [], warnings: [], suggestions: [], metadata: { ruleId: 'time-budget', executionTime: 0, confidence: 0, canAutoFix: false } }; const warnings = []; const suggestions = []; // Calculate total estimated time from previous choices and current choice const previousTime = context.previousChoices.reduce((sum, prevChoice) => { const prevPrompt = context.availablePrompts.find(p => p.id === prevChoice.promptId); return sum + (prevPrompt?.metadata.estimatedTime || 0); }, 0); const currentTime = prompt.metadata.estimatedTime; const totalTime = previousTime + currentTime; if (totalTime > context.userPreferences.maxConfigurationTime) { warnings.push({ code: 'TIME_BUDGET_EXCEEDED', message: `Total configuration time (${totalTime}min) exceeds budget (${context.userPreferences.maxConfigurationTime}min)`, impact: 'medium', recommendation: 'Consider deferring non-critical changes' }); // Suggest defer or skip options const deferOptions = prompt.options.filter(o => o.type === 'defer' || o.type === 'skip'); if (deferOptions.length > 0) { suggestions.push({ type: 'alternative_option', description: `Consider deferring: ${deferOptions.map(o => o.label).join(', ')}`, rationale: 'Helps stay within time budget', confidence: 70 }); } } return { isValid: true, errors: [], warnings, suggestions, metadata: { ruleId: 'time-budget', executionTime: 1, confidence: 85, canAutoFix: false } }; } }); } /** * Private: Initialize system constraints */ initializeSystemConstraints() { // File system permissions this.systemConstraints.push({ id: 'file-permissions', type: 'permission', description: 'Configuration files must be writable', validator: (context) => { return context.environment.configFiles.every(file => { try { if (fs.existsSync(file)) { fs.accessSync(file, fs.constants.W_OK); } return true; } catch { return false; } }); }, errorMessage: 'One or more configuration files are not writable' }); // Node.js version compatibility this.systemConstraints.push({ id: 'node-version', type: 'version', description: 'Node.js version must be 14.0.0 or higher', validator: (context) => { const nodeVersion = process.version; const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]); return majorVersion >= 14; }, errorMessage: 'Node.js version 14.0.0 or higher is required' }); // Disk space availability this.systemConstraints.push({ id: 'disk-space', type: 'resource', description: 'Sufficient disk space must be available', validator: (context) => { try { const stats = fs.statSync(context.environment.projectPath); // This is a simplified check - real implementation would check available space return true; } catch { return false; } }, errorMessage: 'Insufficient disk space for configuration changes' }); } /** * Private: Get risk level for a specific option */ getOptionRiskLevel(prompt, optionId) { const option = prompt.options.find(o => o.id === optionId); return option?.riskLevel === 'HIGH' ? 'HIGH' : option?.riskLevel === 'MEDIUM' ? 'MEDIUM' : 'LOW'; } /** * Private: Check if one risk level is higher than another */ isHigherRisk(risk1, risk2) { const riskLevels = { LOW: 1, MEDIUM: 2, HIGH: 3, CRITICAL: 4 }; return riskLevels[risk1] > riskLevels[risk2]; } /** * Private: Assess overall impact of choices */ assessOverallImpact(choices, prompts, context) { const impacts = []; // Analyze configuration file changes const affectedFiles = new Set(); prompts.forEach(prompt => { prompt.context.affectedFiles.forEach(file => affectedFiles.add(file)); }); if (affectedFiles.size > 0) { impacts.push(`${affectedFiles.size} configuration files will be modified`); } // Analyze schema changes const schemaChanges = context.schemaChanges.length; if (schemaChanges > 0) { impacts.push(`${schemaChanges} schema changes will be processed`); } // Analyze time impact const totalTime = choices.reduce((sum, choice) => { const prompt = prompts.find(p => p.id === choice.promptId); return sum + (prompt?.metadata.estimatedTime || 0); }, 0); if (totalTime > 0) { impacts.push(`Estimated completion time: ${totalTime} minutes`); } return impacts.join('; ') || 'No significant impact detected'; } } exports.UserChoiceValidator = UserChoiceValidator; //# sourceMappingURL=choice-validator.js.map