@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
330 lines • 15 kB
JavaScript
/**
* 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