@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
758 lines • 31.1 kB
JavaScript
/**
* Core Template Validator
*
* Validates orchestration templates against both OpenAPI schema requirements
* and orchestration-specific rules that extend beyond the base API.
*/
import { FIELDS } from '../../generated/fields.generated';
import { OrchestrationRulesLayer } from './OrchestrationRulesLayer';
import { ReferenceResolver } from './ReferenceResolver';
/**
* Main Template Validator Class
*/
export class TemplateValidator {
rules = new Map();
startTime = 0;
rulesChecked = 0;
orchestrationRules;
referenceResolver;
constructor() {
this.orchestrationRules = new OrchestrationRulesLayer();
this.referenceResolver = new ReferenceResolver();
this.initializeRules();
}
/**
* Initialize all validation rules
*/
initializeRules() {
// Rule 1: Metrics Scope Validation
this.addRule({
id: 'METRICS_SCOPE',
name: 'Metrics Scope Validation',
description: 'Validates correct metrics scope for campaigns vs experiments',
validate: (context) => this.validateMetricsScope(context)
});
// Rule 2: Audience Complexity Acknowledgment
this.addRule({
id: 'AUDIENCE_COMPLEXITY',
name: 'Audience Complexity Acknowledgment',
description: 'Complex audiences require _acknowledged_complexity flag',
validate: (context) => this.validateAudienceComplexity(context)
});
// Rule 3: Reference Format Validation
this.addRule({
id: 'REFERENCE_FORMAT',
name: 'Reference Format Validation',
description: 'Validates entity references use correct format',
validate: (context) => this.validateReferenceFormat(context)
});
// Rule 4: Platform Compatibility
this.addRule({
id: 'PLATFORM_COMPATIBILITY',
name: 'Platform Compatibility',
description: 'Ensures entities are valid for the target platform',
validate: (context) => this.validatePlatformCompatibility(context)
});
// Rule 5: Required Fields
this.addRule({
id: 'REQUIRED_FIELDS',
name: 'Required Fields Validation',
description: 'Ensures all required fields are present',
validate: (context) => this.validateRequiredFields(context)
});
// Rule 6: Field Type Validation
this.addRule({
id: 'FIELD_TYPES',
name: 'Field Type Validation',
description: 'Validates field values match expected types',
validate: (context) => this.validateFieldTypes(context)
});
// Rule 7: Orchestration Rules Validation
this.addRule({
id: 'ORCHESTRATION_RULES',
name: 'Orchestration Rules Validation',
description: 'Validates orchestration-specific rules from OrchestrationRulesLayer',
validate: (context) => this.validateOrchestrationRules(context)
});
// Rule 8: Traffic Allocation Validation
this.addRule({
id: 'TRAFFIC_ALLOCATION',
name: 'Traffic Allocation Validation',
description: 'Validates traffic allocation sums to 10000 for variations',
validate: (context) => this.validateTrafficAllocation(context)
});
}
/**
* Add a validation rule
*/
addRule(rule) {
this.rules.set(rule.id, rule);
}
/**
* Main validation entry point
*/
async validate(template) {
this.startTime = Date.now();
this.rulesChecked = 0;
const errors = [];
const warnings = [];
let validSteps = 0;
const context = {
platform: template.platform || 'web',
mode: template.mode || 'template',
template
};
// Validate template structure
const structureError = this.validateTemplateStructure(template);
if (structureError) {
errors.push(structureError);
return this.createResult(errors, warnings, template.steps?.length || 0, validSteps);
}
// Validate each step
for (const step of template.steps) {
context.currentStep = step;
let stepHasErrors = false;
// Run all rules for this step
for (const rule of this.rules.values()) {
this.rulesChecked++;
const error = await Promise.resolve(rule.validate(context));
if (error) {
if (error.severity === 'warning') {
warnings.push(error);
}
else {
errors.push(error);
stepHasErrors = true;
}
}
}
if (!stepHasErrors) {
validSteps++;
}
}
return this.createResult(errors, warnings, template.steps.length, validSteps);
}
/**
* Create validation result
*/
createResult(errors, warnings, totalSteps, validSteps) {
return {
valid: errors.length === 0,
errors,
warnings,
summary: {
totalSteps,
validSteps,
errorCount: errors.length,
warningCount: warnings.length
},
performance: {
duration: Date.now() - this.startTime,
rulesChecked: this.rulesChecked
}
};
}
/**
* Validate template structure
*/
validateTemplateStructure(template) {
if (!template.steps || !Array.isArray(template.steps)) {
return {
code: 'INVALID_STRUCTURE',
severity: 'fatal',
path: 'template.steps',
message: 'Template must have a "steps" array',
expected: 'array',
found: typeof template.steps
};
}
if (template.steps.length === 0) {
return {
code: 'EMPTY_TEMPLATE',
severity: 'error',
path: 'template.steps',
message: 'Template must have at least one step',
expected: 'steps.length > 0',
found: 0
};
}
return null;
}
/**
* Rule Implementation: Metrics Scope
*/
validateMetricsScope(context) {
const step = context.currentStep;
if (!step || step.type !== 'template')
return null;
const entityType = this.extractEntityType(step.template?.system_template_id);
if (!entityType || !['campaign', 'experiment'].includes(entityType))
return null;
const metrics = step.template?.inputs?.metrics;
if (!metrics || !Array.isArray(metrics))
return null;
const requiredScope = entityType === 'campaign' ? 'session' : 'visitor';
for (let i = 0; i < metrics.length; i++) {
const metric = metrics[i];
if (metric.scope && metric.scope !== requiredScope) {
return {
code: `${entityType.toUpperCase()}_METRICS_SCOPE`,
severity: 'error',
stepId: step.id,
path: `steps.${step.id}.template.inputs.metrics[${i}].scope`,
message: `${entityType === 'campaign' ? 'Campaigns' : 'Experiments'} MUST use metrics scope: "${requiredScope}", not "${metric.scope}"`,
found: metric.scope,
expected: requiredScope,
fix: {
description: `Change metrics scope to "${requiredScope}"`,
path: `steps.${step.id}.template.inputs.metrics[${i}].scope`,
value: requiredScope
}
};
}
}
return null;
}
/**
* Rule Implementation: Audience Complexity
*/
validateAudienceComplexity(context) {
const step = context.currentStep;
if (!step || step.type !== 'template')
return null;
const entityType = this.extractEntityType(step.template?.system_template_id);
if (entityType !== 'audience')
return null;
const inputs = step.template?.inputs;
if (!inputs)
return null;
// Check if audience has complex conditions
const hasConditions = inputs.conditions &&
(typeof inputs.conditions === 'string' ||
(typeof inputs.conditions === 'object' && Object.keys(inputs.conditions).length > 0));
if (hasConditions && inputs._acknowledged_complexity !== true) {
return {
code: 'AUDIENCE_COMPLEXITY_ACK',
severity: 'error',
stepId: step.id,
path: `steps.${step.id}.template.inputs._acknowledged_complexity`,
message: 'Complex audiences with conditions require _acknowledged_complexity: true',
found: inputs._acknowledged_complexity,
expected: true,
fix: {
description: 'Add _acknowledged_complexity: true to acknowledge complex audience creation',
path: `steps.${step.id}.template.inputs._acknowledged_complexity`,
value: true
}
};
}
return null;
}
/**
* Rule Implementation: Reference Format
*/
validateReferenceFormat(context) {
const step = context.currentStep;
if (!step || step.type !== 'template')
return null;
const inputs = step.template?.inputs;
if (!inputs)
return null;
// Check for reference fields
const referenceFields = ['audience', 'audiences', 'page_ids', 'event_id', 'campaign_id'];
for (const field of referenceFields) {
const value = inputs[field];
if (!value)
continue;
// In template mode, certain fields should use ref objects
if (context.mode === 'template' && field === 'audience') {
if (typeof value === 'string') {
return {
code: 'REFERENCE_FORMAT',
severity: 'warning',
stepId: step.id,
path: `steps.${step.id}.template.inputs.${field}`,
message: 'In template mode, use ref object format for audience field',
found: 'string',
expected: 'ref object',
fix: {
description: 'Convert to ref object format',
path: `steps.${step.id}.template.inputs.${field}`,
value: { ref: { name: value } }
}
};
}
}
// Validate step references
if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) {
const refPattern = /^\$\{([^.]+)\.([^}]+)\}$/;
const match = value.match(refPattern);
if (!match) {
return {
code: 'INVALID_STEP_REFERENCE',
severity: 'error',
stepId: step.id,
path: `steps.${step.id}.template.inputs.${field}`,
message: 'Invalid step reference format. Use ${step_id.field_name}',
found: value,
expected: '${step_id.entity_id}'
};
}
// Check if referenced step exists
const referencedStepId = match[1];
const stepExists = context.template?.steps?.some((s) => s.id === referencedStepId);
if (!stepExists) {
return {
code: 'UNKNOWN_STEP_REFERENCE',
severity: 'error',
stepId: step.id,
path: `steps.${step.id}.template.inputs.${field}`,
message: `Referenced step "${referencedStepId}" not found`,
found: value
};
}
}
}
return null;
}
/**
* Rule Implementation: Platform Compatibility
*/
validatePlatformCompatibility(context) {
const step = context.currentStep;
if (!step || step.type !== 'template')
return null;
const entityType = this.extractEntityType(step.template?.system_template_id);
if (!entityType)
return null;
const platform = context.platform;
// Define platform restrictions
const platformRestrictions = {
web: ['experiment', 'campaign', 'page', 'audience', 'event', 'project', 'environment', 'variation'],
feature: ['flag', 'ruleset', 'rule', 'audience', 'event', 'project', 'environment', 'variation']
};
const allowedEntities = platformRestrictions[platform] || [];
if (!allowedEntities.includes(entityType)) {
return {
code: 'PLATFORM_MISMATCH',
severity: 'error',
stepId: step.id,
path: `steps.${step.id}.template.system_template_id`,
message: `Entity type "${entityType}" is not supported on ${platform} platform`,
found: entityType,
expected: `one of: ${allowedEntities.join(', ')}`
};
}
return null;
}
/**
* Rule Implementation: Required Fields
*/
validateRequiredFields(context) {
const step = context.currentStep;
if (!step || step.type !== 'template')
return null;
const entityType = this.extractEntityType(step.template?.system_template_id);
if (!entityType)
return null;
const inputs = step.template?.inputs || {};
const fieldDef = FIELDS[entityType];
if (!fieldDef || !fieldDef.required)
return null;
// Check each required field
for (const requiredField of fieldDef.required) {
if (inputs[requiredField] === undefined || inputs[requiredField] === null) {
// Skip fields that might be provided by references
if (requiredField.endsWith('_id') && inputs[requiredField.replace('_id', '')] !== undefined) {
continue;
}
// Skip parameter references
const value = inputs[requiredField];
if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) {
continue;
}
return {
code: 'MISSING_REQUIRED_FIELD',
severity: 'error',
stepId: step.id,
path: `steps.${step.id}.template.inputs.${requiredField}`,
message: `Required field "${requiredField}" is missing`,
expected: fieldDef.fieldTypes?.[requiredField] || 'any'
};
}
}
return null;
}
/**
* Rule Implementation: Field Type Validation
*/
validateFieldTypes(context) {
const step = context.currentStep;
if (!step || step.type !== 'template')
return null;
const entityType = this.extractEntityType(step.template?.system_template_id);
if (!entityType)
return null;
const inputs = step.template?.inputs || {};
const fieldDef = FIELDS[entityType];
if (!fieldDef || !fieldDef.fieldTypes)
return null;
// Check each field's type
for (const [fieldName, value] of Object.entries(inputs)) {
if (value === undefined || value === null)
continue;
const expectedType = fieldDef.fieldTypes?.[fieldName];
if (!expectedType)
continue;
// Skip parameter references and step references for type checking
if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) {
continue;
}
const actualType = this.getActualType(value);
const isValidType = this.isTypeMatch(value, expectedType);
if (!isValidType) {
return {
code: 'INVALID_FIELD_TYPE',
severity: 'error',
stepId: step.id,
path: `steps.${step.id}.template.inputs.${fieldName}`,
message: `Field "${fieldName}" has invalid type`,
found: actualType,
expected: expectedType
};
}
// Check enum values
const enumValues = fieldDef.enums?.[fieldName];
if (enumValues && !enumValues.includes(value)) {
return {
code: 'INVALID_ENUM_VALUE',
severity: 'error',
stepId: step.id,
path: `steps.${step.id}.template.inputs.${fieldName}`,
message: `Field "${fieldName}" must be one of: ${enumValues.join(', ')}`,
found: value,
expected: enumValues
};
}
}
return null;
}
/**
* Rule Implementation: Orchestration Rules Validation
*/
async validateOrchestrationRules(context) {
const step = context.currentStep;
if (!step || step.type !== 'template')
return null;
const entityType = this.extractEntityType(step.template?.system_template_id);
if (!entityType)
return null;
const inputs = step.template?.inputs || {};
const platform = context.platform || 'web';
const mode = context.mode || 'template';
// Get all orchestration rules for this entity
const rules = this.orchestrationRules.getAllRulesForEntity(entityType, 'create', platform);
// Validate each field with orchestration rules
for (const [fieldName, value] of Object.entries(inputs)) {
// Skip parameter references for orchestration validation
if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) {
continue;
}
const rulesContext = {
platform,
mode,
operation: 'create',
entityType,
step,
template: context.template
};
const result = await this.orchestrationRules.validateField(entityType, fieldName, value, rulesContext);
if (!result.valid) {
return {
code: 'ORCHESTRATION_RULE_VIOLATION',
severity: 'error',
stepId: step.id,
path: `steps.${step.id}.template.inputs.${fieldName}`,
message: result.error || `Orchestration rule violation for field "${fieldName}"`,
found: value,
fix: result.fix ? {
description: result.fix.description || 'Fix orchestration rule violation',
path: `steps.${step.id}.template.inputs.${fieldName}`,
value: result.fix.value
} : undefined
};
}
}
return null;
}
/**
* Rule Implementation: Traffic Allocation Validation
*/
validateTrafficAllocation(context) {
const step = context.currentStep;
if (!step || step.type !== 'template')
return null;
const entityType = this.extractEntityType(step.template?.system_template_id);
// Only validate for experiments
if (entityType !== 'experiment')
return null;
const variations = step.template?.inputs?.variations;
if (!variations || !Array.isArray(variations))
return null;
// Calculate total weight
const totalWeight = variations.reduce((sum, v) => sum + (v.weight || 0), 0);
if (totalWeight !== 10000) {
return {
code: 'INVALID_TRAFFIC_ALLOCATION',
severity: 'error',
stepId: step.id,
path: `steps.${step.id}.template.inputs.variations`,
message: `Variation weights must sum to 10000 (100%), currently ${totalWeight}`,
found: totalWeight,
expected: 10000,
fix: {
description: 'Adjust variation weights to sum to 10000',
path: `steps.${step.id}.template.inputs.variations`,
value: this.redistributeWeights(variations)
}
};
}
// Check individual variation weights
for (let i = 0; i < variations.length; i++) {
const weight = variations[i].weight;
if (weight === undefined || weight < 0 || weight > 10000) {
return {
code: 'INVALID_VARIATION_WEIGHT',
severity: 'error',
stepId: step.id,
path: `steps.${step.id}.template.inputs.variations[${i}].weight`,
message: `Variation weight must be between 0 and 10000`,
found: weight,
expected: '0-10000'
};
}
}
return null;
}
/**
* Redistribute weights to sum to 10000
*/
redistributeWeights(variations) {
if (!variations || variations.length === 0)
return variations;
const totalWeight = variations.reduce((sum, v) => sum + (v.weight || 0), 0);
if (totalWeight === 0) {
// Equal distribution
const equalWeight = Math.floor(10000 / variations.length);
return variations.map((v, i) => ({
...v,
weight: i === variations.length - 1
? 10000 - (equalWeight * (variations.length - 1))
: equalWeight
}));
}
// Proportional distribution
return variations.map((v, i) => {
const proportionalWeight = Math.round((v.weight || 0) * 10000 / totalWeight);
return { ...v, weight: proportionalWeight };
});
}
/**
* Extract entity type from system template ID
*/
extractEntityType(systemTemplateId) {
if (!systemTemplateId)
return null;
const patterns = [
{ regex: /optimizely_experiment_/, type: 'experiment' },
{ regex: /optimizely_campaign_/, type: 'campaign' },
{ regex: /optimizely_audience_/, type: 'audience' },
{ regex: /optimizely_event_/, type: 'event' },
{ regex: /optimizely_flag_/, type: 'flag' },
{ regex: /optimizely_ruleset_/, type: 'ruleset' },
{ regex: /optimizely_rule_/, type: 'rule' },
{ regex: /optimizely_page_/, type: 'page' },
{ regex: /optimizely_project_/, type: 'project' },
{ regex: /optimizely_environment_/, type: 'environment' }
];
for (const pattern of patterns) {
if (pattern.regex.test(systemTemplateId)) {
return pattern.type;
}
}
return null;
}
/**
* Get actual type of a value
*/
getActualType(value) {
if (Array.isArray(value))
return 'array';
if (value === null)
return 'null';
return typeof value;
}
/**
* Check if value matches expected type
*/
isTypeMatch(value, expectedType) {
switch (expectedType) {
case 'string':
return typeof value === 'string';
case 'number':
case 'integer':
return typeof value === 'number' && !isNaN(value);
case 'boolean':
return typeof value === 'boolean';
case 'array':
return Array.isArray(value);
case 'object':
return typeof value === 'object' && !Array.isArray(value) && value !== null;
default:
return true;
}
}
/**
* Apply auto-fixes to a template
*/
async applyAutoFixes(template) {
const fixes = [];
const fixedTemplate = JSON.parse(JSON.stringify(template)); // Deep clone
// First validate to find all errors with fixes
const validationResult = await this.validate(template);
// Apply fixes from validation errors
for (const error of validationResult.errors) {
if (error.fix) {
const pathParts = error.path.split('.');
let target = fixedTemplate;
// Navigate to the parent of the field to fix
for (let i = 0; i < pathParts.length - 1; i++) {
const part = pathParts[i];
if (!target)
break;
if (part.includes('[') && part.includes(']')) {
// Handle array access
const [arrayName, indexStr] = part.split('[');
const index = parseInt(indexStr.replace(']', ''));
target = target[arrayName] ? target[arrayName][index] : undefined;
}
else if (part === 'steps' && i + 1 < pathParts.length) {
// Special handling for steps - find by ID
const stepId = pathParts[i + 1];
const step = target.steps?.find((s) => s.id === stepId);
if (step) {
target = step;
i++; // Skip the step ID part
}
else {
target = undefined;
}
}
else {
target = target[part];
}
}
if (!target)
continue; // Skip if path is invalid
// Apply the fix
const fieldName = pathParts[pathParts.length - 1];
let oldValue = undefined;
if (fieldName.includes('[') && fieldName.includes(']')) {
// Handle array element
const [arrayName, indexStr] = fieldName.split('[');
const index = parseInt(indexStr.replace(']', ''));
if (target[arrayName] && Array.isArray(target[arrayName])) {
oldValue = target[arrayName][index];
target[arrayName][index] = error.fix.value;
}
}
else {
oldValue = target[fieldName];
target[fieldName] = error.fix.value;
}
fixes.push({
path: error.path,
oldValue,
newValue: error.fix.value,
description: error.fix.description || error.message
});
}
}
// Apply orchestration layer auto-fixes
for (const step of fixedTemplate.steps || []) {
if (step.type !== 'template')
continue;
const entityType = this.extractEntityType(step.template?.system_template_id);
if (!entityType)
continue;
const platform = fixedTemplate.platform || 'web';
const mode = fixedTemplate.mode || 'template';
const { fixed, changes } = this.orchestrationRules.applyAutoFixes(step, entityType, platform, mode);
if (fixed) {
for (const change of changes) {
fixes.push({
path: `steps.${step.id}.template.inputs.${change.field}`,
oldValue: change.oldValue,
newValue: change.newValue,
description: `Auto-corrected ${change.field} for ${platform} ${entityType}`
});
}
}
}
return { fixedTemplate, fixes };
}
/**
* Format validation results for display
*/
formatResults(result) {
const lines = [];
lines.push('ORCHESTRATION TEMPLATE VALIDATION');
lines.push('=================================\n');
if (result.valid) {
lines.push('✅ Template is valid!');
lines.push(` ${result.summary.totalSteps} steps validated successfully`);
}
else {
if (result.errors.length > 0) {
lines.push(`❌ Found ${result.errors.length} error(s):\n`);
for (const error of result.errors) {
lines.push(`[${error.code}] ${error.stepId ? `Step "${error.stepId}"` : 'Template level'}`);
lines.push(` ${error.message}`);
if (error.found !== undefined) {
lines.push(` Found: ${JSON.stringify(error.found)}`);
}
if (error.expected !== undefined) {
lines.push(` Expected: ${JSON.stringify(error.expected)}`);
}
if (error.fix) {
lines.push(` ✨ Fix: ${error.fix.description}`);
}
lines.push('');
}
}
}
if (result.warnings.length > 0) {
lines.push(`⚠️ ${result.warnings.length} warning(s):\n`);
for (const warning of result.warnings) {
lines.push(`[${warning.code}] ${warning.stepId ? `Step "${warning.stepId}"` : 'Template level'}`);
lines.push(` ${warning.message}`);
if (warning.fix) {
lines.push(` 💡 Suggestion: ${warning.fix.description}`);
}
lines.push('');
}
}
lines.push('SUMMARY:');
lines.push(` Total steps: ${result.summary.totalSteps}`);
lines.push(` Valid steps: ${result.summary.validSteps}`);
lines.push(` Errors: ${result.summary.errorCount}`);
lines.push(` Warnings: ${result.summary.warningCount}`);
if (result.performance) {
lines.push(`\nPERFORMANCE:`);
lines.push(` Duration: ${result.performance.duration}ms`);
lines.push(` Rules checked: ${result.performance.rulesChecked}`);
}
return lines.join('\n');
}
}
//# sourceMappingURL=TemplateValidator.js.map