UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

330 lines 15 kB
/** * Direct Template Validator * * Validates orchestration templates using direct ModelFriendlyTemplate format */ import { MODEL_FRIENDLY_TEMPLATES } from '../../templates/ModelFriendlyTemplates.js'; import { getLogger } from '../../logging/Logger.js'; export class DirectTemplateValidator { logger = getLogger(); /** * Validate an orchestration step using direct template format */ validateStep(step, context) { const result = { valid: true, errors: [], warnings: [], suggestions: [] }; // 1. Validate required fields if (!step.entity_type) { result.errors.push('Missing required field: entity_type'); result.valid = false; } if (!step.operation) { result.errors.push('Missing required field: operation'); result.suggestions.push('Add "operation": "create" for entity creation'); result.valid = false; } if (!step.entity_data && !step.template_data) { result.errors.push('Step must include either entity_data or template_data'); result.suggestions.push('Add "template_data" field with the entity configuration'); result.valid = false; } // 3. Validate entity type const validEntityTypes = Object.keys(MODEL_FRIENDLY_TEMPLATES); if (step.entity_type && !validEntityTypes.includes(step.entity_type)) { result.errors.push(`Invalid entity_type: ${step.entity_type}`); result.suggestions.push(`Valid types: ${validEntityTypes.join(', ')}`); result.valid = false; } // 4. Validate ModelFriendlyTemplate compliance if (step.entity_type && (step.entity_data || step.template_data)) { const templateValidation = this.validateAgainstModelFriendlyTemplate(step.entity_type, step.entity_data || step.template_data, context); result.errors.push(...templateValidation.errors); result.warnings.push(...templateValidation.warnings); result.suggestions.push(...templateValidation.suggestions); if (templateValidation.errors.length > 0) { result.valid = false; } } // 5. Platform compatibility check if (context?.platform && step.entity_type) { const platformCheck = this.checkPlatformCompatibility(step.entity_type, context.platform); if (!platformCheck.compatible) { result.errors.push(platformCheck.error); result.valid = false; } } return result; } /** * Validate data against ModelFriendlyTemplate requirements */ validateAgainstModelFriendlyTemplate(entityType, data, context) { const errors = []; const warnings = []; const suggestions = []; const template = MODEL_FRIENDLY_TEMPLATES[entityType]; if (!template) { warnings.push(`No ModelFriendlyTemplate found for ${entityType}`); return { errors, warnings, suggestions }; } // Check required fields if (template.fields) { for (const [fieldName, fieldConfig] of Object.entries(template.fields)) { if (fieldConfig.required && !(fieldName in data)) { errors.push(`Missing required field: ${fieldName}`); // Provide helpful suggestions if (fieldName === 'key' && data.name) { const suggestedKey = this.generateKey(data.name); suggestions.push(`Generate key from name: "key": "${suggestedKey}"`); } else if (fieldName === '_acknowledged_complexity' && template.complexity_score >= 2) { suggestions.push(`This entity has complexity score ${template.complexity_score}, add "_acknowledged_complexity": true`); } else if (fieldConfig.examples?.sample) { suggestions.push(`Add ${fieldName}: ${JSON.stringify(fieldConfig.examples.sample)}`); } else if (fieldConfig.default !== undefined) { suggestions.push(`Add ${fieldName}: ${JSON.stringify(fieldConfig.default)}`); } } } } // Check complexity acknowledgment for complex entities if (template.complexity_score >= 2 && !data._acknowledged_complexity) { errors.push('Missing _acknowledged_complexity: true for complex entity'); suggestions.push(`This entity has complexity score ${template.complexity_score}, add "_acknowledged_complexity": true`); } // Validate field types and values for (const [fieldName, value] of Object.entries(data)) { const fieldConfig = template.fields?.[fieldName]; if (!fieldConfig) { // Check if it's a known field from example_filled if (template.example_filled && !(fieldName in template.example_filled)) { warnings.push(`Unknown field: ${fieldName}`); suggestions.push(`Consider removing unknown field "${fieldName}" or check documentation`); } continue; } // Type validation const typeValidation = this.validateFieldType(fieldName, value, fieldConfig); if (typeValidation.error) { errors.push(typeValidation.error); } if (typeValidation.suggestion) { suggestions.push(typeValidation.suggestion); } // Enum validation if (fieldConfig.enum && !fieldConfig.enum.includes(String(value))) { errors.push(`Invalid value for ${fieldName}: "${value}". Valid values: ${fieldConfig.enum.join(', ')}`); // Suggest closest match const closestMatch = this.findClosestMatch(String(value), fieldConfig.enum); if (closestMatch) { suggestions.push(`Did you mean "${closestMatch}" for field ${fieldName}?`); } } } // Entity-specific validation this.validateEntitySpecificRules(entityType, data, errors, warnings, suggestions); return { errors, warnings, suggestions }; } /** * Validate field type and format */ validateFieldType(fieldName, value, fieldConfig) { if (!fieldConfig.type) return {}; switch (fieldConfig.type) { case 'string': if (typeof value !== 'string') { return { error: `Field ${fieldName} must be a string, got ${typeof value}`, suggestion: `Convert ${fieldName} to string: "${String(value)}"` }; } break; case 'number': case 'integer': if (typeof value !== 'number') { return { error: `Field ${fieldName} must be a number, got ${typeof value}`, suggestion: `Convert ${fieldName} to number: ${Number(value)}` }; } break; case 'boolean': if (typeof value !== 'boolean') { return { error: `Field ${fieldName} must be a boolean, got ${typeof value}`, suggestion: `Convert ${fieldName} to boolean: ${!!value}` }; } break; case 'array': if (!Array.isArray(value)) { return { error: `Field ${fieldName} must be an array, got ${typeof value}`, suggestion: `Convert ${fieldName} to array: [${value}]` }; } // Validate array constraints if (fieldConfig.min !== undefined && value.length < fieldConfig.min) { return { error: `Field ${fieldName} must have at least ${fieldConfig.min} items, got ${value.length}` }; } if (fieldConfig.max !== undefined && value.length > fieldConfig.max) { return { error: `Field ${fieldName} must have at most ${fieldConfig.max} items, got ${value.length}` }; } break; case 'object': if (typeof value !== 'object' || value === null || Array.isArray(value)) { return { error: `Field ${fieldName} must be an object, got ${typeof value}` }; } break; } return {}; } /** * Entity-specific validation rules */ validateEntitySpecificRules(entityType, data, errors, warnings, suggestions) { switch (entityType) { case 'flag': // Flag must have at least 2 variations if (data.variations && Array.isArray(data.variations) && data.variations.length < 2) { errors.push('Flag must have at least 2 variations'); suggestions.push('Add at least one more variation to the variations array'); } // Validate key format if (data.key && !/^[a-z0-9_]+$/.test(data.key)) { errors.push('Flag key must use snake_case format (lowercase letters, numbers, underscores only)'); suggestions.push(`Fix key format: "${this.generateKey(data.key)}"`); } break; case 'audience': // Audience conditions validation if (data.conditions && typeof data.conditions === 'string') { try { const parsed = JSON.parse(data.conditions); if (!Array.isArray(parsed) || parsed.length === 0) { errors.push('Audience conditions must be a non-empty array'); suggestions.push('Use ["or"] for basic targeting or ["and", {...}] for specific conditions'); } } catch (e) { errors.push('Audience conditions must be valid JSON'); suggestions.push('Check JSON syntax in conditions field'); } } break; case 'experiment': case 'campaign': // Traffic allocation validation if (data.traffic_allocation && Array.isArray(data.traffic_allocation)) { const totalWeight = data.traffic_allocation.reduce((sum, alloc) => sum + (alloc.weight || 0), 0); if (totalWeight !== 10000) { errors.push(`Traffic allocation weights must sum to 10000, got ${totalWeight}`); suggestions.push('Adjust traffic allocation weights to sum to exactly 10000'); } } break; case 'project': // Platform validation for project creation if (data.platform === 'feature_experimentation') { errors.push('Invalid platform value "feature_experimentation"'); suggestions.push('Use platform: "custom" for Feature Experimentation projects'); } break; } } /** * Check platform compatibility */ checkPlatformCompatibility(entityType, platform) { const webOnlyEntities = ['experiment', 'campaign', 'page', 'extension']; const featureOnlyEntities = ['flag', 'rule', 'ruleset', 'variable_definition']; if (webOnlyEntities.includes(entityType) && platform === 'feature') { return { compatible: false, error: `Entity type '${entityType}' is only available in Web Experimentation` }; } if (featureOnlyEntities.includes(entityType) && platform === 'web') { return { compatible: false, error: `Entity type '${entityType}' is only available in Feature Experimentation` }; } return { compatible: true }; } /** * Generate key from name */ generateKey(name) { return name .toLowerCase() .replace(/[^a-z0-9]/g, '_') .replace(/_{2,}/g, '_') .replace(/^_|_$/g, ''); } /** * Find closest match for typos using simple string similarity */ findClosestMatch(value, validValues) { const lowerValue = value.toLowerCase(); // Exact case-insensitive match const exactMatch = validValues.find(v => v.toLowerCase() === lowerValue); if (exactMatch) return exactMatch; // Partial match const partialMatch = validValues.find(v => v.toLowerCase().includes(lowerValue) || lowerValue.includes(v.toLowerCase())); if (partialMatch) return partialMatch; // Simple Levenshtein distance for closest match let closestMatch = validValues[0]; let minDistance = this.levenshteinDistance(lowerValue, validValues[0].toLowerCase()); for (const validValue of validValues.slice(1)) { const distance = this.levenshteinDistance(lowerValue, validValue.toLowerCase()); if (distance < minDistance) { minDistance = distance; closestMatch = validValue; } } // Only suggest if distance is reasonable (less than half the length) if (minDistance <= Math.max(2, Math.floor(value.length / 2))) { return closestMatch; } return null; } /** * Calculate Levenshtein distance between two strings */ levenshteinDistance(str1, str2) { const matrix = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(null)); for (let i = 0; i <= str1.length; i++) { matrix[0][i] = i; } for (let j = 0; j <= str2.length; j++) { matrix[j][0] = j; } for (let j = 1; j <= str2.length; j++) { for (let i = 1; i <= str1.length; i++) { const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1; matrix[j][i] = Math.min(matrix[j][i - 1] + 1, // deletion matrix[j - 1][i] + 1, // insertion matrix[j - 1][i - 1] + indicator // substitution ); } } return matrix[str2.length][str1.length]; } } //# sourceMappingURL=DirectTemplateValidator.js.map