UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

520 lines 25 kB
/** * Orchestration Rules Layer * * This layer adds orchestration-specific validation and transformation rules * on top of the base OpenAPI fields from fields.generated.ts */ import { FIELDS } from '../../generated/fields.generated'; export class OrchestrationRulesLayer { baseFields = FIELDS; orchestrationRules; constructor() { // Define all orchestration-specific rules this.orchestrationRules = { // Web platform rules web: { campaign: { create: { metrics: { scope: { override: 'session', validation: (value) => value === 'session', error: 'Campaigns MUST use metrics scope: "session", not "visitor"', autoFix: () => 'session' } }, // Holdback validation holdback: { validation: (value) => { if (value === undefined) return true; return typeof value === 'number' && value >= 0 && value <= 10000; }, error: 'Holdback must be between 0 and 10000 (basis points)', autoFix: (value) => Math.max(0, Math.min(10000, parseInt(value) || 0)) } } }, experiment: { create: { metrics: { scope: { override: 'visitor', validation: (value) => value === 'visitor', error: 'Experiments MUST use metrics scope: "visitor", not "session"', autoFix: () => 'visitor' } }, // Mode-specific field naming audience: { modes: { template: { fieldName: 'audience', format: 'ref_object' }, direct: { fieldName: 'audience_conditions', format: 'json_string' } }, validation: (value, context) => { if (context.mode === 'template') { // In template mode, should be ref object return typeof value === 'object' && value.ref !== undefined; } return true; }, error: 'In template mode, use ref object format for audience field' }, // Traffic allocation validation traffic_allocation: { validation: (value) => { if (value === undefined) return true; return typeof value === 'number' && value >= 0 && value <= 10000; }, error: 'Traffic allocation must be between 0 and 10000 (basis points)' }, // Variations validation variations: { validation: (value) => { if (!Array.isArray(value)) return false; if (value.length < 2) return false; // Check weights sum to 10000 const totalWeight = value.reduce((sum, v) => sum + (v.weight || 0), 0); if (totalWeight !== 10000) return false; return true; }, error: 'Experiments must have at least 2 variations with weights summing to 10000', autoFix: (value) => { if (!Array.isArray(value) || value.length < 2) { return [ { name: 'Control', weight: 5000 }, { name: 'Variation', weight: 5000 } ]; } // Adjust weights to sum to 10000 const totalWeight = value.reduce((sum, v) => sum + (v.weight || 0), 0); if (totalWeight === 0) { const equalWeight = Math.floor(10000 / value.length); return value.map((v, i) => ({ ...v, weight: i === value.length - 1 ? 10000 - (equalWeight * (value.length - 1)) : equalWeight })); } // Scale weights proportionally return value.map(v => ({ ...v, weight: Math.round((v.weight || 0) * 10000 / totalWeight) })); } } } }, page: { create: { // URL validation for pages edit_url: { validation: (value) => { try { new URL(value); return true; } catch { return false; } }, error: 'edit_url must be a valid URL', autoFix: (value) => { if (!value) return 'https://example.com'; if (!value.startsWith('http')) return `https://${value}`; return value; } }, // Activation type rules activation_code: { conditional: (context) => { const activationType = context.step?.template?.inputs?.activation_type; return activationType === 'polling' || activationType === 'callback'; }, validation: (value, context) => { const activationType = context.step?.template?.inputs?.activation_type; if (activationType === 'polling' || activationType === 'callback') { return typeof value === 'string' && value.length > 0; } return true; }, error: 'activation_code is required when activation_type is "polling" or "callback"' } } } }, // Feature platform rules feature: { flag: { create: { // Default value type matching default_value: { customValidation: (value, context) => { const varType = context.variables?.[0]?.type; if (!varType) return true; return this.validateTypeMatch(value, varType); }, error: 'default_value must match variable type', autoFix: (value, context) => { const varType = context.variables?.[0]?.type; if (!varType) return value; switch (varType) { case 'boolean': return false; case 'string': return ''; case 'integer': return 0; case 'double': return 0.0; case 'json': return '{}'; default: return value; } } }, // Key format validation key: { validation: (value) => { return /^[a-zA-Z0-9_\-]+$/.test(value); }, error: 'Flag key must contain only alphanumeric characters, hyphens, and underscores', autoFix: (value) => { return value.replace(/[^a-zA-Z0-9_\-]/g, '_').toLowerCase(); } } } }, ruleset: { create: { // Rule priorities validation rule_priorities: { validation: (value, context) => { if (!Array.isArray(value)) return false; // Check all rules exist const rules = context.rules || {}; return value.every(ruleKey => rules[ruleKey] !== undefined); }, error: 'All rules in rule_priorities must exist in rules object' }, // Default variation validation default_variation_key: { validation: (value, context) => { const flag = context.flag; if (!flag || !flag.variations) return true; return flag.variations.some((v) => v.key === value); }, error: 'default_variation_key must match a variation key in the flag' } } }, rule: { create: { // Percentage included validation percentage_included: { validation: (value) => { return typeof value === 'number' && value >= 0 && value <= 10000; }, error: 'percentage_included must be between 0 and 10000 (basis points)', autoFix: (value) => Math.max(0, Math.min(10000, parseInt(value) || 0)) }, // Distribution mode specific rules variations: { conditional: (context) => { return context.step?.template?.inputs?.type === 'a/b'; }, validation: (value, context) => { if (context.step?.template?.inputs?.type !== 'a/b') return true; if (!value || typeof value !== 'object') return false; // For A/B tests, need at least 2 variations return Object.keys(value).length >= 2; }, error: 'A/B test rules must have at least 2 variations' } } } }, // Rules that apply to both platforms both: { audience: { create: { _acknowledged_complexity: { validation: (value, context) => { // Required when conditions are complex const conditions = context.step?.template?.inputs?.conditions; if (conditions && typeof conditions === 'string' && conditions.length > 50) { return value === true; } return true; }, error: 'Complex audiences require _acknowledged_complexity: true', autoFix: (value, context) => { const conditions = context.step?.template?.inputs?.conditions; if (conditions && typeof conditions === 'string' && conditions.length > 50) { return true; } return undefined; } }, // Conditions format validation conditions: { validation: (value) => { if (!value) return true; if (typeof value !== 'string') return false; try { const parsed = JSON.parse(value); // Basic structure validation return Array.isArray(parsed) || typeof parsed === 'object'; } catch { return false; } }, error: 'Audience conditions must be a valid JSON string', autoFix: (value) => { if (typeof value === 'object') { return JSON.stringify(value); } return value; } } } }, event: { create: { // Key format validation key: { validation: (value) => { return /^[a-zA-Z0-9_\-]+$/.test(value) && value.length <= 64; }, error: 'Event key must contain only alphanumeric characters, hyphens, underscores (max 64 chars)', autoFix: (value) => { return value.replace(/[^a-zA-Z0-9_\-]/g, '_').substring(0, 64).toLowerCase(); } } } }, variation: { create: { // Weight validation weight: { validation: (value, context) => { if (typeof value !== 'number') return false; if (value < 0 || value > 10000) return false; // If part of a set, will be validated at parent level return true; }, error: 'Variation weight must be between 0 and 10000 (basis points)' } } } } }; } /** * Get fields with orchestration rules applied */ getFieldsWithRules(entityType, operation = 'create', platform = 'web', mode = 'direct') { // Start with base fields from generated file const baseFields = this.baseFields[entityType]; if (!baseFields) { throw new Error(`Unknown entity type: ${entityType}`); } // Apply orchestration-specific overrides const platformRules = this.orchestrationRules[platform]?.[entityType]?.[operation] || {}; const sharedRules = this.orchestrationRules.both?.[entityType]?.[operation] || {}; const rules = { ...sharedRules, ...platformRules }; return { required: [...(baseFields.required || [])], optional: [...(baseFields.optional || [])], defaults: { ...(baseFields.defaults || {}) }, enums: { ...(baseFields.enums || {}) }, fieldTypes: { ...(baseFields.fieldTypes || {}) }, fieldDescriptions: { ...(baseFields.fieldDescriptions || {}) }, fieldExamples: { ...(baseFields.fieldExamples || {}) }, orchestrationRules: rules, getField: (fieldName) => { const base = baseFields.fieldDescriptions?.[fieldName]; const rule = rules?.[fieldName]; return { name: fieldName, type: baseFields.fieldTypes?.[fieldName], required: baseFields.required?.includes(fieldName), description: base, enum: baseFields.enums?.[fieldName], example: baseFields.fieldExamples?.[fieldName], default: baseFields.defaults?.[fieldName], // Orchestration-specific additions orchestrationRule: rule, platformOverride: rule?.override, customValidation: rule?.validation || rule?.customValidation, modeVariations: rule?.modes }; } }; } /** * Validate a value against both OpenAPI and orchestration rules */ async validateField(entityType, fieldName, value, context) { const baseFields = this.baseFields[entityType]; if (!baseFields) { return { valid: false, error: `Unknown entity type: ${entityType}` }; } // First validate against OpenAPI schema const fieldType = baseFields.fieldTypes?.[fieldName]; if (fieldType && !this.matchesType(value, fieldType)) { return { valid: false, error: `Field ${fieldName} expects type ${fieldType}, got ${typeof value}` }; } // Check enum values from OpenAPI const enumValues = baseFields.enums?.[fieldName]; if (enumValues && !enumValues.includes(value)) { return { valid: false, error: `Field ${fieldName} must be one of: ${enumValues.join(', ')}` }; } // Apply orchestration-specific rules const rules = this.getOrchestrationRules(entityType, context.operation, context.platform); const fieldRule = rules?.[fieldName]; if (fieldRule) { // Check conditional if (fieldRule.conditional && !fieldRule.conditional(context)) { return { valid: true }; // Rule doesn't apply } // Check override if (fieldRule.override !== undefined && value !== fieldRule.override) { return { valid: false, error: fieldRule.error || `Field ${fieldName} must be "${fieldRule.override}" for ${context.platform} ${entityType}`, fix: { value: fieldRule.override, description: `Set to required value for ${context.platform} platform` } }; } // Check custom validation if (fieldRule.validation && !fieldRule.validation(value, context)) { const fix = fieldRule.autoFix ? { value: fieldRule.autoFix(value, context), description: `Auto-corrected to valid value` } : undefined; return { valid: false, error: fieldRule.error || `Field ${fieldName} validation failed`, fix }; } if (fieldRule.customValidation && !fieldRule.customValidation(value, context)) { return { valid: false, error: fieldRule.error || `Field ${fieldName} custom validation failed` }; } // Check mode-specific naming if (fieldRule.modes && context.mode) { const modeConfig = fieldRule.modes[context.mode]; if (modeConfig && context.actualFieldName !== modeConfig.fieldName) { return { valid: false, error: `In ${context.mode} mode, use field name "${modeConfig.fieldName}" not "${context.actualFieldName}"` }; } } } return { valid: true }; } /** * Get all orchestration rules for an entity */ getAllRulesForEntity(entityType, operation = 'create', platform = 'web') { const platformRules = this.orchestrationRules[platform]?.[entityType]?.[operation] || {}; const sharedRules = this.orchestrationRules.both?.[entityType]?.[operation] || {}; return { ...sharedRules, ...platformRules }; } /** * Apply auto-fixes to a template step */ applyAutoFixes(step, entityType, platform = 'web', mode = 'template') { const changes = []; const rules = this.getAllRulesForEntity(entityType, 'create', platform); if (!step.template?.inputs) return { fixed: false, changes }; const inputs = step.template.inputs; const context = { platform: platform, mode: mode, operation: 'create', entityType, step, template: step.template }; for (const [fieldName, rule] of Object.entries(rules)) { if (!rule.autoFix) continue; const currentValue = inputs[fieldName]; const shouldApply = !rule.conditional || rule.conditional(context); if (shouldApply) { const newValue = rule.autoFix(currentValue, context); if (newValue !== currentValue) { inputs[fieldName] = newValue; changes.push({ field: fieldName, oldValue: currentValue, newValue }); } } } return { fixed: changes.length > 0, changes }; } matchesType(value, expectedType) { switch (expectedType) { case 'string': return typeof value === 'string'; case 'number': case 'integer': return typeof value === 'number'; case 'boolean': return typeof value === 'boolean'; case 'array': return Array.isArray(value); case 'object': return typeof value === 'object' && !Array.isArray(value); case 'any': return true; default: return true; } } validateTypeMatch(value, varType) { switch (varType) { case 'boolean': return typeof value === 'boolean' || value === 'true' || value === 'false'; case 'string': return typeof value === 'string'; case 'integer': return Number.isInteger(Number(value)); case 'double': return !isNaN(Number(value)); case 'json': { try { if (typeof value === 'string') JSON.parse(value); return true; } catch { return false; } } default: return true; } } getOrchestrationRules(entityType, operation, platform) { return this.orchestrationRules[platform]?.[entityType]?.[operation] || this.orchestrationRules.both?.[entityType]?.[operation] || {}; } } //# sourceMappingURL=OrchestrationRulesLayer.js.map