@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
1,103 lines (1,100 loc) ⢠149 kB
JavaScript
/**
* 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