UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

1,103 lines (1,100 loc) • 149 kB
/** * Comprehensive Auto-Correction Engine * * This engine aggressively auto-corrects user input using: * 1. Real swagger defaults from the API * 2. Safe operational fallbacks * 3. URL protocol fixing * 4. Field name corrections * 5. Enum fuzzy matching * 6. Format validation and auto-repair * * Philosophy: Be smart about fixing obvious mistakes, provide clear feedback */ import { FIELDS } from '../generated/fields.generated.js'; import { getLogger } from '../logging/Logger.js'; import { TrafficAllocationCalculator } from '../utils/TrafficAllocationCalculator.js'; import { UNIFIED_FIELD_SYNONYMS } from '../parsers/UnifiedFieldMappings.js'; /** * Real swagger defaults extracted from actual API specs */ const SWAGGER_DEFAULTS = { // Experiments - REAL swagger defaults experiment: { audience_conditions: "everyone", is_classic: false, metrics: [], type: "a/b" }, // Projects - REAL swagger defaults project: { // Note: platform should NOT have a default since it depends on is_flags_enabled // platform: "web" (removed to prevent overriding user input) status: "active" }, // Audiences - REAL swagger defaults audience: { archived: false, segmentation: false }, // Pages - REAL swagger defaults page: { archived: false, category: "other" }, // Events - REAL swagger defaults event: { category: "other", is_classic: false }, // Attributes - REAL swagger defaults attribute: { archived: false, condition_type: "custom_attribute" }, // Campaigns - REAL swagger defaults campaign: { type: "personalization" }, // Features - REAL swagger defaults feature: { archived: false }, // Extensions - REAL swagger defaults extension: { fields: [] } }; /** * Safety fallbacks for fields with no swagger defaults * These are operationally safe values when we can't determine user intent */ const SAFETY_FALLBACKS = { // Page safety defaults page: { activation_type: "immediate", // Safest - triggers immediately page_type: "single_url" // Most common // conditions will be auto-generated from edit_url in validateAndFixFormats }, // Audience safety defaults audience: { // DO NOT set conditions as safety default - it overrides valid conditions // conditions: "[]" // Removed - was overriding actual conditions }, // Experiment safety defaults experiment: { status: "not_started", // Safe - won't go live percentage_included: 1.0 // Modern default (100% traffic) }, // Event safety defaults event: { event_type: "custom" // Default to custom events when not specified }, // Flag safety defaults (Feature Experimentation) flag: { archived: false, outlier_filtering_enabled: false // REGRESSION FIX: Do NOT add variable_definitions by default - this causes basic flags // to be treated as complex and routed through orchestration // variable_definitions: {} // Only add if the flag actually has variables }, // Variation safety defaults variation: { weight: null, // Will be calculated by TrafficAllocationCalculator actions: [] // Will be filled by orchestrator } }; /** * Entity safety rules - determines auto-correction behavior */ const ENTITY_SAFETY_RULES = { page: { safeDefaults: true, autoCorrectUrls: true, fallbackStrategy: "immediate_safe" }, event: { safeDefaults: false, // STRICT MODE autoCorrectUrls: false, fallbackStrategy: "fail_fast" // Don't guess event types }, audience: { safeDefaults: true, autoCorrectUrls: false, fallbackStrategy: "everyone_targeting" }, experiment: { safeDefaults: true, autoCorrectUrls: true, // Enable URL fixing for experiment templates fallbackStrategy: "not_started_safe" }, flag: { safeDefaults: true, autoCorrectUrls: false, fallbackStrategy: "disabled_safe" }, campaign: { safeDefaults: true, autoCorrectUrls: false, fallbackStrategy: "not_started_safe" }, project: { safeDefaults: true, autoCorrectUrls: true, fallbackStrategy: "modern_defaults" } }; /** * Field name corrections - common mistakes agents make */ const FIELD_NAME_CORRECTIONS = { activation_mode: 'activation_type', activation_code: 'activation_code', event_type_name: 'event_type', audience_targeting: 'audience_conditions', targeting_conditions: 'audience_conditions', page_targeting: 'page_ids', metric_events: 'metrics', traffic_split: 'traffic_allocation', experiment_type: 'type', flag_status: 'status', match: 'match_type', // Common mistake in conditions url_match: 'match_type', matching_type: 'match_type', conditional_activation: 'activation_type', // Should be corrected at field level too // CRITICAL: Flag entity field mappings flag_key: 'key', // Users often provide flag_key but schema expects key flag_name: 'name', // Users often provide flag_name but schema expects name // Entity key mappings for different entity types experiment_key: 'key', // For experiments audience_key: 'key', // For audiences page_key: 'key', // For pages // event_key: 'key', // REMOVED: event_key is a separate field in metrics, not a synonym for key campaign_key: 'key', // For campaigns rule_key: 'key', // For rules attribute_key: 'key' // For attributes }; /** * Enum fuzzy matching - correct common enum mistakes */ const ENUM_FUZZY_CORRECTIONS = { activation_type: { // Case variations (all keys lowercase for lookup) 'instant': 'immediate', 'immedate': 'immediate', // Common typo 'immidiate': 'immediate', // Common typo // Manual variations 'manual_mode': 'manual', 'manual-mode': 'manual', 'manualmode': 'manual', 'manuel': 'manual', // Common typo // DOM changed variations 'dom_change': 'dom_changed', 'dom_changes': 'dom_changed', 'dom-change': 'dom_changed', 'dom-changed': 'dom_changed', 'dom-changes': 'dom_changed', 'domchange': 'dom_changed', 'domchanged': 'dom_changed', 'domchanges': 'dom_changed', 'dom_changed': 'dom_changed', 'dom_modified': 'dom_changed', 'dom_updated': 'dom_changed', 'on_dom_change': 'dom_changed', 'on_dom_changed': 'dom_changed', 'when_dom_changes': 'dom_changed', 'conditional_activation': 'dom_changed', 'conditional': 'dom_changed', // URL changed variations 'url_change': 'url_changed', 'url_changes': 'url_changed', 'url-change': 'url_changed', 'url-changed': 'url_changed', 'url-changes': 'url_changed', 'urlchange': 'url_changed', 'urlchanged': 'url_changed', 'urlchanges': 'url_changed', 'url_changed': 'url_changed', 'url_modified': 'url_changed', 'url_updated': 'url_changed', 'on_url_change': 'url_changed', 'on_url_changed': 'url_changed', 'when_url_changes': 'url_changed', // Polling variations 'polling_mode': 'polling', 'polling-mode': 'polling', 'pollingmode': 'polling', 'poll': 'polling', 'poling': 'polling', // Common typo 'pull': 'polling', // Common confusion // Callback variations 'callback_mode': 'callback', 'callback-mode': 'callback', 'callbackmode': 'callback', 'call_back': 'callback', 'call-back': 'callback', 'callback': 'callback', 'event_callback': 'callback', 'event-callback': 'callback', 'eventcallback': 'callback' }, event_type: { // Click variations (all keys lowercase) 'click_event': 'click', 'click-event': 'click', 'clickevent': 'click', 'mouse_click': 'click', 'mouse-click': 'click', 'mouseclick': 'click', 'button_click': 'click', 'link_click': 'click', // Conversion variations - ALL map to 'custom' since API only accepts 'custom' 'conversion': 'custom', // CRITICAL FIX: API only accepts 'custom' for user events 'conversion_event': 'custom', 'conversion-event': 'custom', 'conversionevent': 'custom', 'goal': 'custom', 'target': 'custom', 'objective': 'custom', 'convert': 'custom', 'convertion': 'custom', // Common typo // Pageview variations 'page_view': 'pageview', 'page-view': 'pageview', 'pageview': 'pageview', 'page_load': 'pageview', 'page-load': 'pageview', 'pageload': 'pageview', 'view': 'pageview', 'visit': 'pageview', // Custom variations 'custom_event': 'custom', 'custom-event': 'custom', 'customevent': 'custom', 'user_defined': 'custom', 'user-defined': 'custom', 'userdefined': 'custom', 'custom_metric': 'custom', 'custom-metric': 'custom' }, experiment_type: { // A/B test variations (all keys lowercase) 'ab_test': 'a/b', 'ab-test': 'a/b', 'abtest': 'a/b', 'ab': 'a/b', 'a_b': 'a/b', 'a-b': 'a/b', 'a/b': 'a/b', 'split_test': 'a/b', 'split-test': 'a/b', 'splitest': 'a/b', 'variant_test': 'a/b', 'variation_test': 'a/b', // Multivariate variations 'multivariate_test': 'multivariate', 'multivariate-test': 'multivariate', 'multivariatetest': 'multivariate', 'mvt': 'multivariate', 'multi_variate': 'multivariate', 'multi-variate': 'multivariate', 'multivariate': 'multivariate', 'multivariate_experiment': 'multivariate', 'multi_variable': 'multivariate', 'multi-variable': 'multivariate', // Feature test variations 'feature_test': 'feature', 'feature-test': 'feature', 'featuretest': 'feature', 'feature_flag': 'feature', 'feature-flag': 'feature', 'featureflag': 'feature', 'feature_experiment': 'feature', 'feature-experiment': 'feature', 'featureexperiment': 'feature' }, status: { // Running variations (all keys lowercase) 'active': 'running', 'live': 'running', 'started': 'running', 'in_progress': 'running', 'in-progress': 'running', 'inprogress': 'running', 'ongoing': 'running', 'executing': 'running', // Paused variations 'inactive': 'paused', 'stopped': 'paused', 'halted': 'paused', 'suspended': 'paused', 'on_hold': 'paused', 'on-hold': 'paused', 'onhold': 'paused', 'hold': 'paused', 'disabled': 'paused', // Not started variations 'draft': 'not_started', 'pending': 'not_started', 'scheduled': 'not_started', 'queued': 'not_started', 'waiting': 'not_started', 'created': 'not_started', 'new': 'not_started', 'ready': 'not_started', 'prepared': 'not_started', 'not_running': 'not_started', 'not-running': 'not_started', 'notrunning': 'not_started', 'not_active': 'not_started', 'not-active': 'not_started', 'notactive': 'not_started' }, // Web Experimentation change types change_type: { // CSS variations 'css': 'custom_css', 'style': 'custom_css', 'styles': 'custom_css', 'stylesheet': 'custom_css', 'custom-css': 'custom_css', 'customcss': 'custom_css', // JavaScript variations 'js': 'custom_code', 'javascript': 'custom_code', 'script': 'custom_code', 'code': 'custom_code', 'custom-code': 'custom_code', 'customcode': 'custom_code', // HTML variations 'html': 'insert_html', 'markup': 'insert_html', 'element': 'insert_html', 'insert-html': 'insert_html', 'inserthtml': 'insert_html', // Image variations 'img': 'insert_image', 'image': 'insert_image', 'picture': 'insert_image', 'photo': 'insert_image', 'insert-image': 'insert_image', 'insertimage': 'insert_image', // Redirect variations 'url': 'redirect', 'link': 'redirect', 'navigation': 'redirect', 'route': 'redirect', 'goto': 'redirect', 'navigate': 'redirect' } }; export class ComprehensiveAutoCorrector { static logger = getLogger(); /** * Main auto-correction entry point */ static autoCorrect(entityType, userInput, context) { // Handle undefined/null userInput (e.g., from enable_flag tool) if (!userInput || typeof userInput !== 'object') { return { correctedData: userInput || {}, corrections: [], userMessage: 'No data to correct', wasCorrected: false }; } this.logger.debug({ method: 'autoCorrect ENTRY', entityType, isTemplateMode: context?.isTemplateMode, inputKeys: Object.keys(userInput), hasFlagKey: 'flag_key' in userInput, flagKeyValue: userInput.flag_key, hasArchived: 'archived' in userInput, hasOutlierFiltering: 'outlier_filtering_enabled' in userInput }, 'AUTO-CORRECTOR ENTRY POINT'); // DEBUG: Log all flag input to see what we receive if (entityType === 'flag') { this.logger.error({ entityType, inputKeys: Object.keys(userInput), hasAbTest: !!userInput.ab_test, abTestKeys: userInput.ab_test ? Object.keys(userInput.ab_test) : [], hasVariations: !!(userInput.ab_test && userInput.ab_test.variations), variationsValue: userInput.ab_test?.variations, variationsType: typeof userInput.ab_test?.variations, fullInput: JSON.stringify(userInput, null, 2) }, 'ComprehensiveAutoCorrector: FLAG INPUT ANALYSIS (ALL FLAGS) - ERROR LEVEL'); } // DEBUG: Log what we receive before any processing for page conditions if (entityType === 'page' && userInput.conditions) { getLogger().debug({ entityType, conditionsType: typeof userInput.conditions, conditionsIsArray: Array.isArray(userInput.conditions), conditionsValue: userInput.conditions, conditionsStringified: JSON.stringify(userInput.conditions, null, 2) }, 'DEBUG AutoCorrector ENTRY: Page conditions structure'); if (Array.isArray(userInput.conditions)) { userInput.conditions.forEach((item, index) => { getLogger().debug({ index, itemType: typeof item, itemIsArray: Array.isArray(item), itemValue: item, itemStringified: JSON.stringify(item, null, 2) }, `DEBUG AutoCorrector ENTRY: conditions[${index}] analysis`); }); } } const corrections = []; let correctedData = { ...userInput }; // Get entity rules const rules = ENTITY_SAFETY_RULES[entityType] || { safeDefaults: true, autoCorrectUrls: false, fallbackStrategy: 'modern_defaults' }; // Step 1: Fix obvious field name mistakes (entity-aware) correctedData = this.correctFieldNames(entityType, correctedData, corrections); // Step 2: Fix URL protocols if (rules.autoCorrectUrls) { correctedData = this.fixUrlProtocols(correctedData, corrections); } // Step 3: Apply enum fuzzy matching correctedData = this.correctEnumValues(entityType, correctedData, corrections, context); // Step 3.5: Apply platform-specific corrections correctedData = this.applyPlatformSpecificCorrections(entityType, correctedData, corrections, context); // Step 3.6: Apply template-specific field stripping for UPDATE templates correctedData = this.stripInvalidTemplateFields(entityType, correctedData, corrections, context); // Step 3.7: Create default A/B test variations when missing if (entityType === 'flag' && correctedData.ab_test) { this.logger.error({ entityType, hasAbTest: !!correctedData.ab_test, hasVariations: !!correctedData.ab_test.variations, variationsValue: correctedData.ab_test.variations, variationsType: typeof correctedData.ab_test.variations, shouldCreateVariations: !correctedData.ab_test.variations }, 'ComprehensiveAutoCorrector: A/B test variation check - ERROR LEVEL'); if (!correctedData.ab_test.variations) { correctedData = this.createDefaultABTestVariations(correctedData, corrections); } } // Step 3.8: Auto-detect and add missing template markers for UPDATE operations if (context?.isTemplateMode && !correctedData._template_type) { correctedData = this.addMissingTemplateMarker(entityType, correctedData, corrections); } // Step 4: Apply swagger defaults for missing fields // CRITICAL: Skip defaults for template mode to avoid interfering with template detection // CRITICAL: Skip defaults for UPDATE operations to preserve existing values if (!context?.isTemplateMode && context?.operation !== 'update') { correctedData = this.applySwaggerDefaults(entityType, correctedData, corrections); } // Step 5: Apply safety fallbacks if allowed // CRITICAL: Skip safety fallbacks for template mode and UPDATE operations if (rules.safeDefaults && !context?.isTemplateMode && context?.operation !== 'update') { correctedData = this.applySafetyFallbacks(entityType, correctedData, corrections, context); } else if (!context?.isTemplateMode && this.hasRequiredFieldsWithInvalidValues(entityType, correctedData, context)) { // Strict mode - fail if critical fields are invalid throw new Error(`Entity type '${entityType}' requires precise data. Cannot auto-correct invalid values. šŸ”§ SOLUTION: Call get_entity_templates FIRST! Use: get_entity_templates with entity_type="${entityType}" to see the exact structure and required fields. The template will show you: - All required fields with correct names - Proper data structure and nesting - Valid values and options - Working examples šŸ’” LAW OF TEMPLATES: Always call get_entity_templates before ANY entity creation or update.`); } // Step 6: Validate format and fix common mistakes correctedData = this.validateAndFixFormats(entityType, correctedData, corrections, context); // Step 7: Apply personalization-specific corrections if (entityType === 'experiment' && correctedData.type === 'personalization') { correctedData = this.applyPersonalizationCorrections(correctedData, corrections); } // Step 8: Apply experiment update-specific corrections if (entityType === 'experiment' && context?.isTemplateMode) { correctedData = this.applyExperimentUpdateCorrections(correctedData, corrections); } // Generate user-friendly message const userMessage = this.generateUserMessage(entityType, corrections); this.logger.debug({ method: 'autoCorrect EXIT', entityType, isTemplateMode: context?.isTemplateMode, outputKeys: Object.keys(correctedData), hasFlagKey: 'flag_key' in correctedData, flagKeyValue: correctedData.flag_key, hasArchived: 'archived' in correctedData, hasOutlierFiltering: 'outlier_filtering_enabled' in correctedData, correctionsCount: corrections.length }, 'AUTO-CORRECTOR EXIT POINT'); return { correctedData, corrections, userMessage, wasCorrected: corrections.length > 0 }; } /** * Fix common field name mistakes (entity-aware) */ static correctFieldNames(entityType, data, corrections) { const corrected = { ...data }; // CRITICAL FIX: Don't apply flag_key -> key correction for variations and rulesets // Variations have their own 'key' field that should not be overwritten // Rulesets use flag_key as a URL parameter, not in the payload if ((entityType === 'variation' || entityType === 'ruleset') && corrected.flag_key !== undefined) { // For variations and rulesets, flag_key should be removed (it's in the URL path) // but for variations, the 'key' field should remain untouched delete corrected.flag_key; getLogger().debug({ entityType, action: `Removed flag_key from ${entityType} payload - it belongs in URL path, not request body`, variationKey: entityType === 'variation' ? corrected.key : undefined, removedFlagKey: data.flag_key }, `Entity-aware field correction for ${entityType}`); // Don't record this as a correction since it's expected behavior return corrected; } // CRITICAL FIX: For flag metrics updates, preserve rule_key as separate parameter const isMetricsUpdate = entityType === 'flag' && !!corrected.metrics && !!corrected.rule_key; for (const [wrongName, correctName] of Object.entries(FIELD_NAME_CORRECTIONS)) { if (corrected[wrongName] !== undefined) { // Skip rule_key -> key mapping for metrics updates only if (isMetricsUpdate && wrongName === 'rule_key' && correctName === 'key') { getLogger().debug({ entityType, action: 'Preserving rule_key for flag_update_metrics template', flag_key: corrected.flag_key, rule_key: corrected.rule_key }, 'Skipping rule_key -> key mapping for metrics update'); continue; } corrected[correctName] = corrected[wrongName]; delete corrected[wrongName]; corrections.push({ field: correctName, original: wrongName, corrected: correctName, reason: `Corrected field name from '${wrongName}' to '${correctName}'`, type: 'field_rename' }); } } return corrected; } /** * Fix URL protocols - add https:// if missing (recursive) */ static fixUrlProtocols(data, corrections, path = '') { const corrected = { ...data }; // URL field patterns to match const urlFieldPatterns = [ 'edit_url', 'url', 'webhook_url', 'callback_url', 'target_url', 'redirect_url' // NOTE: 'value' removed - it's used for many non-URL fields like custom_code ]; // URL value patterns to detect const urlValuePattern = /^(?:www\.|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)[\w\-._~:/?#[\]@!$&'()*+,;=.]+$/; for (const [key, value] of Object.entries(corrected)) { const currentPath = path ? `${path}.${key}` : key; if (value && typeof value === 'string') { let shouldFix = false; // Check if field name suggests it's a URL if (urlFieldPatterns.some(pattern => key.toLowerCase().includes(pattern.toLowerCase()))) { shouldFix = true; } // Special handling for 'value' field - only fix if parent is url_targeting else if (key === 'value' && path.includes('url_targeting')) { if (!value.match(/^https?:\/\//) && value.includes('.')) { shouldFix = true; } } // Check if value looks like a URL missing protocol (but not for 'value' fields) // Also exclude numeric values (e.g., "0.9", "123.45") else if (key !== 'value' && urlValuePattern.test(value) && !value.match(/^https?:\/\//) && isNaN(Number(value))) { shouldFix = true; } if (shouldFix) { const original = value; let fixed = original; // Add https:// if no protocol if (!fixed.match(/^https?:\/\//)) { fixed = `https://${fixed}`; } // Upgrade http to https if (fixed.startsWith('http://')) { fixed = fixed.replace('http://', 'https://'); } if (fixed !== original) { corrected[key] = fixed; corrections.push({ field: currentPath, original, corrected: fixed, reason: 'Added/upgraded to HTTPS protocol', type: 'url_fix' }); } } } // Recursively check nested objects else if (value && typeof value === 'object' && !Array.isArray(value)) { corrected[key] = this.fixUrlProtocols(value, corrections, currentPath); } // Recursively check arrays else if (Array.isArray(value)) { corrected[key] = value.map((item, index) => { if (item && typeof item === 'object') { return this.fixUrlProtocols(item, corrections, `${currentPath}[${index}]`); } return item; }); } } return corrected; } /** * Apply enum fuzzy matching */ static correctEnumValues(entityType, data, corrections, context) { const corrected = { ...data }; const entityFields = FIELDS[entityType]; const entityEnums = entityFields?.enums || {}; // First apply standard enum corrections for top-level fields for (const [fieldName, validValues] of Object.entries(entityEnums)) { if (corrected[fieldName] && typeof corrected[fieldName] === 'string') { const original = corrected[fieldName]; const userValueLower = original.toLowerCase(); // Check if it's already valid (case-sensitive first) if (Array.isArray(validValues) && validValues.includes(original)) continue; // Check if it's valid with case-insensitive matching if (Array.isArray(validValues)) { const exactCaseInsensitiveMatch = validValues.find((valid) => String(valid).toLowerCase() === userValueLower); if (exactCaseInsensitiveMatch) { corrected[fieldName] = exactCaseInsensitiveMatch; corrections.push({ field: fieldName, original, corrected: exactCaseInsensitiveMatch, reason: `Fixed case for '${original}' to valid enum value '${exactCaseInsensitiveMatch}'`, type: 'enum_correction' }); continue; } } // Try fuzzy corrections (using lowercase key for lookup) const fuzzyCorrections = ENUM_FUZZY_CORRECTIONS[fieldName] || {}; if (fuzzyCorrections[userValueLower]) { const correctedValue = fuzzyCorrections[userValueLower]; corrected[fieldName] = correctedValue; corrections.push({ field: fieldName, original, corrected: correctedValue, reason: `Fuzzy matched '${original}' to valid enum value '${correctedValue}'`, type: 'enum_correction' }); continue; } // Try exact match on original value (case sensitive) in fuzzy corrections if (fuzzyCorrections[original]) { const correctedValue = fuzzyCorrections[original]; corrected[fieldName] = correctedValue; corrections.push({ field: fieldName, original, corrected: correctedValue, reason: `Exact fuzzy matched '${original}' to valid enum value '${correctedValue}'`, type: 'enum_correction' }); continue; } // Try partial matching as last resort if (Array.isArray(validValues)) { const partialMatch = validValues.find((valid) => String(valid).toLowerCase().includes(userValueLower) || userValueLower.includes(String(valid).toLowerCase())); if (partialMatch) { corrected[fieldName] = partialMatch; corrections.push({ field: fieldName, original, corrected: partialMatch, reason: `Partial matched '${original}' to valid enum value '${partialMatch}'`, type: 'enum_correction' }); } } } } // Apply nested enum corrections for Web Experimentation contexts only if (entityType === 'experiment' && context?.platform !== 'feature') { this.correctNestedWebExperimentationEnums(corrected, corrections); } return corrected; } /** * Apply nested enum corrections specifically for Web Experimentation * CRITICAL: Only applies to Web Experimentation, NOT Feature Flags */ static correctNestedWebExperimentationEnums(data, corrections) { // Handle variation.actions[].change_type for Web Experimentation if (data.variation?.actions && Array.isArray(data.variation.actions)) { data.variation.actions.forEach((action, index) => { if (action.change_type && typeof action.change_type === 'string') { const fuzzyCorrections = ENUM_FUZZY_CORRECTIONS.change_type || {}; const original = action.change_type; const userValueLower = original.toLowerCase(); // Try fuzzy corrections first if (fuzzyCorrections[userValueLower]) { const correctedValue = fuzzyCorrections[userValueLower]; action.change_type = correctedValue; corrections.push({ field: `variation.actions[${index}].change_type`, original, corrected: correctedValue, reason: `Web Experimentation: Fuzzy matched '${original}' to valid change_type '${correctedValue}'`, type: 'enum_correction' }); } // Try exact match else if (fuzzyCorrections[original]) { const correctedValue = fuzzyCorrections[original]; action.change_type = correctedValue; corrections.push({ field: `variation.actions[${index}].change_type`, original, corrected: correctedValue, reason: `Web Experimentation: Exact matched '${original}' to valid change_type '${correctedValue}'`, type: 'enum_correction' }); } } }); } // Handle variations[].actions[].change_type for experiment updates if (data.variations && Array.isArray(data.variations)) { data.variations.forEach((variation, variationIndex) => { if (variation.actions && Array.isArray(variation.actions)) { variation.actions.forEach((action, actionIndex) => { if (action.change_type && typeof action.change_type === 'string') { const fuzzyCorrections = ENUM_FUZZY_CORRECTIONS.change_type || {}; const original = action.change_type; const userValueLower = original.toLowerCase(); // Try fuzzy corrections first if (fuzzyCorrections[userValueLower]) { const correctedValue = fuzzyCorrections[userValueLower]; action.change_type = correctedValue; corrections.push({ field: `variations[${variationIndex}].actions[${actionIndex}].change_type`, original, corrected: correctedValue, reason: `Web Experimentation: Fuzzy matched '${original}' to valid change_type '${correctedValue}'`, type: 'enum_correction' }); } // Try exact match else if (fuzzyCorrections[original]) { const correctedValue = fuzzyCorrections[original]; action.change_type = correctedValue; corrections.push({ field: `variations[${variationIndex}].actions[${actionIndex}].change_type`, original, corrected: correctedValue, reason: `Web Experimentation: Exact matched '${original}' to valid change_type '${correctedValue}'`, type: 'enum_correction' }); } } }); } }); } } /** * Apply real swagger defaults */ static applySwaggerDefaults(entityType, data, corrections) { const corrected = { ...data }; const swaggerDefaults = SWAGGER_DEFAULTS[entityType] || {}; // Check which fields were explicitly removed by previous corrections const removedFields = new Set(corrections .filter(c => c.corrected === undefined || c.corrected === 'REMOVED') .map(c => c.field)); for (const [field, defaultValue] of Object.entries(swaggerDefaults)) { // Don't add back fields that were explicitly removed if (removedFields.has(field)) { continue; } if (corrected[field] === undefined || corrected[field] === null || corrected[field] === '') { corrected[field] = defaultValue; corrections.push({ field, original: corrected[field], corrected: defaultValue, reason: `Applied swagger default value`, type: 'swagger_default' }); } } return corrected; } /** * Apply safety fallbacks for missing critical fields */ static applySafetyFallbacks(entityType, data, corrections, context) { const corrected = { ...data }; const safetyDefaults = SAFETY_FALLBACKS[entityType] || {}; for (const [field, safeValue] of Object.entries(safetyDefaults)) { // CRITICAL: Skip fields for Feature Experimentation variations if (entityType === 'variation' && context?.platform === 'feature') { if (field === 'weight' || field === 'actions') { continue; // Don't add weight or actions fields for Feature Experimentation } } if (corrected[field] === undefined || corrected[field] === null || corrected[field] === '') { corrected[field] = safeValue; corrections.push({ field, original: corrected[field], corrected: safeValue, reason: `Applied safe fallback value for operational safety`, type: 'safety_fallback' }); } } return corrected; } /** * Apply platform-specific corrections */ static applyPlatformSpecificCorrections(entityType, data, corrections, context) { const corrected = { ...data }; // CRITICAL FIX: Correct common platform value mistakes for projects if (entityType === 'project' && corrected.platform) { const platformMapping = { 'feature_experimentation': 'custom', 'Feature Experimentation': 'custom', 'feature': 'custom', 'Feature': 'custom', 'web_experimentation': 'web', 'Web Experimentation': 'web', 'Web': 'web', 'Custom': 'custom' }; if (platformMapping[corrected.platform]) { const oldValue = corrected.platform; corrected.platform = platformMapping[corrected.platform]; corrections.push({ field: 'platform', original: oldValue, corrected: corrected.platform, reason: `Corrected invalid platform value "${oldValue}" to "${corrected.platform}". Use "custom" for Feature Experimentation and "web" for Web Experimentation.`, type: 'enum_correction' }); this.logger.info({ entityType, oldPlatform: oldValue, newPlatform: corrected.platform }, 'ComprehensiveAutoCorrector: Fixed invalid platform value for project'); } } // CRITICAL FIX: Handle project platform vs is_flags_enabled conflict if (entityType === 'project') { // Platform-based project type detection: // - platform: "web" → Web Experimentation (is_flags_enabled: false/undefined) // - platform: "custom" → Feature Experimentation (is_flags_enabled: true, keep platform: "custom") // - platform: "ios"/"android" → Feature Experimentation (convert to platform: "custom", is_flags_enabled: true) // Handle legacy iOS/Android platforms → convert to Feature Experimentation if (corrected.platform === 'ios' || corrected.platform === 'android') { corrected.platform = 'custom'; corrected.is_flags_enabled = true; corrections.push({ field: 'platform', original: data.platform, corrected: 'custom', reason: `Converted legacy platform "${data.platform}" to "custom" for Feature Experimentation`, type: 'format_fix' }); corrections.push({ field: 'is_flags_enabled', original: undefined, corrected: true, reason: `Set is_flags_enabled=true for Feature Experimentation project`, type: 'format_fix' }); } // Handle platform: "custom" → Feature Experimentation else if (corrected.platform === 'custom' && corrected.is_flags_enabled !== true) { corrected.is_flags_enabled = true; corrections.push({ field: 'is_flags_enabled', original: corrected.is_flags_enabled, corrected: true, reason: `Set is_flags_enabled=true for platform "custom" (Feature Experimentation)`, type: 'format_fix' }); } // Handle explicit is_flags_enabled: true → ensure platform is "custom" else if (corrected.is_flags_enabled === true && corrected.platform !== 'custom') { const original = corrected.platform; corrected.platform = 'custom'; corrections.push({ field: 'platform', original, corrected: 'custom', reason: 'Set platform="custom" for Feature Experimentation project (is_flags_enabled=true)', type: 'format_fix' }); } // Handle missing platform field else if (!corrected.platform) { // Infer platform from is_flags_enabled if (corrected.is_flags_enabled === true) { corrected.platform = 'custom'; corrections.push({ field: 'platform', original: undefined, corrected: 'custom', reason: 'Set platform="custom" for Feature Experimentation project (is_flags_enabled=true)', type: 'format_fix' }); } else { // Default to web for projects without explicit flags enabled corrected.platform = 'web'; corrections.push({ field: 'platform', original: undefined, corrected: 'web', reason: 'Set platform="web" as default for Web Experimentation project', type: 'format_fix' }); } } // Web Experimentation projects should have platform field explicitly else if (corrected.is_flags_enabled === false && corrected.platform !== 'web') { const originalPlatform = corrected.platform; corrected.platform = 'web'; corrections.push({ field: 'platform', original: originalPlatform, corrected: 'web', reason: 'Set platform="web" for Web Experimentation project (is_flags_enabled=false)', type: 'format_fix' }); } } // CRITICAL FIX: Handle variation fields if (entityType === 'variation') { // CRITICAL: Ensure variation has a key field if (!corrected.key) { // Generate a key from name if available, otherwise use a default if (corrected.name) { // Convert name to key format: lowercase, replace spaces with underscores corrected.key = corrected.name.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_-]/g, ''); } else { // Generate a unique key with timestamp corrected.key = `variation_${Date.now()}`; } corrections.push({ field: 'key', original: undefined, corrected: corrected.key, reason: `Generated variation key from ${corrected.name ? 'name' : 'timestamp'}`, type: 'safety_fallback' }); } // Handle weight field based on platform if (context?.platform === 'feature') { // For Feature Experimentation, remove weight if provided (handled at rule level) if (corrected.weight !== undefined) { const original = corrected.weight; delete corrected.weight; corrections.push({ field: 'weight', original, corrected: undefined, reason: 'Removed weight field for Feature Experimentation variation (weight is handled at rule level)', type: 'format_fix' }); } } else { // STEP 1: Check for weight synonyms and convert them to 'weight' field const weightSynonyms = UNIFIED_FIELD_SYNONYMS.weight || []; for (const synonym of weightSynonyms) { if (synonym !== 'weight' && corrected[synonym] !== undefined) { // Found a weight synonym, move its value to the weight field if (corrected.weight === undefined) { const originalValue = corrected[synonym]; corrected.weight = originalValue; delete corrected[synonym]; corrections.push({ field: 'weight', original: `${synonym}: ${originalValue}`, corrected: corrected.weight, reason: `Converted weight synonym '${synonym}' to 'weight' field`, type: 'field_rename' }); } } } // STEP 2: For Web Experimentation, ensure weight is provided and in correct format (basis points) if (corrected.weight === undefined || corrected.weight === null) { corrected.weight = 5000; // Default equal weight for web variations corrections.push({ field: 'weight', original: undefined, corrected: 5000, reason: 'Added default weight for Web Experimentation variation (required by API)', type: 'swagger_default' }); } else { // Convert percentage values to basis points using TrafficAllocationCalculator try { const originalWeight = corrected.weight; const basisPointsWeight = TrafficAllocationCalculator.percentageToBasisPoints(corrected.weight); // Only add correction if conversion was needed if (basisPointsWeight !== originalWeight) { corrected.weight = basisPointsWeight; corrections.push({ field: 'weight', original: originalWeight, corrected: basisPointsWeight, reason: `Converted weight from percentage (${originalWeight}) to basis points (${basisPointsWeight}) - Optimizely requires basis points where 10000 = 100%`, type: 'format_fix' }); } } catch (error) { // If conversion fails, replace with safe default and log correction const originalWeight = corrected.weight; corrected.weight = 5000; // Safe default for equal weight distribution corrections.push({ field: 'weight', original: originalWeight, corrected: 5000, reason: `Invalid weight value '${originalWeight}' replaced with safe default (5000 basis points = 50%) - ${error instanceof Error ? error.message : 'Unknown error'}`, type: 'safety_fallback' }); getLogger().warn({ originalWeight, error: error instanceof Error ? error.message : 'Unknown error', correctedWeight: 5000 }, 'Invalid weight value corrected to safe default'); } } } } return corrected; } /** * Check if entity has required fields with invalid values */ static hasRequiredFieldsWithInvalidValues(entityType, data, context) { // For events - check if event_type is invalid or missing if (entityType === 'event') { const validEventTypes