@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
996 lines • 47.3 kB
JavaScript
/**
* Field Mapper
* @description Maps AI-generated field names to template field names using fuzzy matching
*
* Purpose: Bridge the gap between creative AI field naming and standardized template fields
* using intelligent fuzzy matching with Fuse.js.
*
* Key Features:
* - Fuzzy matching with configurable thresholds
* - Entity-specific field mappings
* - Confidence scoring for mappings
* - Common transformation patterns
*
* @author Optimizely MCP Server
* @version 1.0.0
*/
import Fuse from 'fuse.js';
import { getLogger } from '../logging/Logger.js';
/**
* Field Mapper for intelligent field name matching
*/
export class FieldMapper {
logger = getLogger();
templateFieldCache = new Map();
commonTransformations = [];
constructor() {
this.logger.debug('FieldMapper initialized');
this.initializeTransformations();
}
// Generate all case permutations for a term
generateCasePermutations(term) {
const permutations = [];
const parts = term.split(/[_\s-]+/);
// All lowercase variations
permutations.push(parts.join('')); // lowercase
permutations.push(parts.join('_')); // snake_case
permutations.push(parts.join('-')); // kebab-case
permutations.push(parts.join(' ')); // space separated
permutations.push(parts.join('.')); // dot.separated
// Camel case variations
if (parts.length > 1) {
const camelCase = parts[0].toLowerCase() + parts.slice(1).map(p => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()).join('');
permutations.push(camelCase); // camelCase
}
// Pascal case
const pascalCase = parts.map(p => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()).join('');
permutations.push(pascalCase); // PascalCase
// All uppercase variations
permutations.push(parts.join('').toUpperCase()); // UPPERCASE
permutations.push(parts.join('_').toUpperCase()); // SNAKE_CASE_UPPER
permutations.push(parts.join('-').toUpperCase()); // KEBAB-CASE-UPPER
// Mixed case variations
permutations.push(parts.map(p => p.toUpperCase()).join('_')); // SCREAMING_SNAKE_CASE
return [...new Set(permutations)]; // Remove duplicates
}
// Build comprehensive regex patterns from base terms
buildComprehensivePattern(baseTerms) {
const allPatterns = [];
for (const term of baseTerms) {
const permutations = this.generateCasePermutations(term);
allPatterns.push(...permutations);
}
// Create regex that matches any of these patterns
const pattern = allPatterns.map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
return new RegExp(`^(${pattern})$`, 'i');
}
// Initialize all transformations
initializeTransformations() {
this.commonTransformations = [
// --- CORE IDENTIFIERS & METADATA (Highest Confidence) ---
{
from: this.buildComprehensivePattern([
'key', 'id', 'identifier', 'unique key', 'unique id', 'slug', 'handle', 'ref', 'reference', 'token',
'lookup_key', 'natural_key', 'api key', 'external key', 'external id', 'shortcode',
'flag key', 'feature key', 'feature flag key', 'ff key', 'experiment key', 'test key',
'event key', 'event id', 'page key', 'variation key', 'variant key', 'rule key',
'metric key', 'audience key'
]),
to: 'key',
confidence: 0.98
},
{
from: this.buildComprehensivePattern([
'name', 'title', 'label', 'display name', 'display title', 'heading', 'caption',
'friendly_name', 'human_name', 'ui_name', 'title_text', 'experiment name', 'test name',
'ab test name', 'split test name', 'flag name', 'feature name', 'feature flag name',
'ff name', 'event name', 'metric name', 'audience name', 'segment name', 'page name',
'variation name', 'variant name', 'version name', 'campaign name', 'rule name',
'ruleset name', 'group name', 'nombre'
]),
to: 'name',
confidence: 0.9
},
{
from: this.buildComprehensivePattern([
'description', 'desc', 'descr', 'desciption', 'details', 'info', 'information',
'summary', 'about', 'purpose', 'notes', 'comments', 'blurb', 'overview', 'info_text',
'markdown', 'body', 'copy', 'synopsis', 'experiment description', 'test description',
'flag description', 'hypothesis', 'goal', 'objective', 'test hypothesis',
'the why', 'what is this for'
]),
to: 'description',
confidence: 0.88
},
{
from: this.buildComprehensivePattern([
'project', 'project id', 'projectid', 'proj id', 'project identifier', 'project key',
'project ref', 'parent project', 'parent id', 'workspace', 'workspace id', 'app id',
'container id', 'namespace', 'namespace_id'
]),
to: 'project_id',
confidence: 0.95
},
{
from: this.buildComprehensivePattern([
'archived', 'is archived', 'deleted', 'is deleted', 'removed', 'is removed',
'inactive', 'is inactive', 'deprecated', 'is deprecated', 'disabled', 'is disabled',
'trashed', 'is trashed', 'soft deleted', 'marked for deletion', 'retired',
'withdrawn', 'hidden'
]),
to: 'archived',
confidence: 0.9
},
{
from: this.buildComprehensivePattern([
'type', 'kind', 'category', 'classification', 'entity type', 'object type',
'resource type', 'experiment type', 'test type', 'variation type', 'event type',
'metric type', 'rule type', 'page type'
]),
to: 'type',
confidence: 0.85
},
// --- EXPERIMENT & FLAG LIFECYCLE (High-collision-risk fields first) ---
{
from: this.buildComprehensivePattern([
'enabled', 'is enabled', 'enable', 'active', 'is active', 'activated', 'is activated',
'on', 'is on', 'turned on', 'is running', 'running', 'live', 'is live', 'published',
'is published', 'deployed', 'is deployed', 'status enabled', 'active status',
'activation status', 'toggle', 'switch', 'flag enabled', 'feature enabled',
'launched', 'rolled out', 'isrunning', 'toggle on'
]),
to: 'enabled',
confidence: 0.92 // High confidence, but must come before 'status' rule
},
{
from: this.buildComprehensivePattern([
'status', 'state', 'phase', 'stage', 'lifecycle', 'run_state', 'campaign_status',
'test_state', 'current status', 'lifecycle status', 'workflow status', 'run status',
'experiment status', 'test status', 'draft', 'paused', 'completed', 'finished'
]),
to: 'status',
confidence: 0.8 // Lower confidence as 'status' can be a substring of 'status enabled'
},
{
from: this.buildComprehensivePattern([
'traffic allocation', 'traffic_allocation', 'experiment allocation', 'total traffic',
'experiment traffic', 'test traffic', 'overall allocation', 'global allocation',
'audience size', 'sample size', 'coverage', 'total coverage', 'global percent',
'allocation total'
]),
to: 'traffic_allocation',
confidence: 0.95
},
{
from: this.buildComprehensivePattern([
'percentage', 'percent', 'pct', 'weight', 'allocation', 'traffic', 'traffic percentage',
'traffic allocation', 'traffic weight', 'traffic split', 'split percentage',
'percentage included', 'percent included', 'inclusion percentage', 'rollout percentage',
'rollout percent', 'exposure', 'exposure percentage', 'distribution',
'distribution percentage', 'serving percentage', 'serving percent', 'variant weight',
'variation weight', 'variation percentage', 'bucket percentage', 'sample rate',
'ramp', 'rollout pct', 'weight total'
]),
to: 'percentage_included',
confidence: 0.88
},
{
from: this.buildComprehensivePattern([
'variations', 'variants', 'versions', 'treatments', 'arms', 'cells', 'buckets',
'experiences', 'options', 'choices', 'test variations', 'test variants',
'ab variations', 'experiment variations', 'variation list', 'variant list',
'variation set', 'variant array', 'alternatives'
]),
to: 'variations',
confidence: 0.95
},
{
from: this.buildComprehensivePattern([
'default variation', 'default variant', 'default version', 'fallback variation',
'fallback variant', 'fallback', 'control variation', 'control variant', 'control',
'baseline variation', 'baseline variant', 'baseline', 'default treatment',
'default experience', 'default value', 'else variation', 'else condition',
'catch all', 'holdout', 'original', 'variant a', 'the a version'
]),
to: 'default_variation_key',
confidence: 0.92
},
{
from: this.buildComprehensivePattern([
'metrics', 'metric list', 'goals', 'goal list', 'kpis', 'kpi list', 'success metrics',
'tracking metrics', 'events', 'event list', 'conversion events', 'tracked events',
'success events', 'outcome definitions', 'outcomes', 'what to track'
]),
to: 'metrics',
confidence: 0.9
},
// --- AUDIENCE, TARGETING & CONDITIONS ---
{
from: this.buildComprehensivePattern([
'audience', 'audiences', 'audience conditions', 'audience ids', 'targeting',
'targeting conditions', 'target audience', 'segments', 'segment conditions',
'user segments', 'eligibility', 'eligibility criteria', 'who sees this',
'visitor conditions', 'user conditions', 'traffic conditions', 'qualification',
'eligibility rule', 'segment logic', 'filter expression', 'targeting logic',
'who to target'
]),
to: 'audience_conditions',
confidence: 0.9
},
{
from: this.buildComprehensivePattern([
'conditions', 'condition', 'criteria', 'rules', 'targeting conditions',
'page conditions', 'url conditions', 'match conditions', 'trigger conditions',
'when to show', 'matcher', 'predicate', 'rule_json', 'segment_tree',
'where_clause', 'url rules', 'triggers', 'path conditions', 'route filter'
]),
to: 'conditions',
confidence: 0.85
},
// --- ENTITY-SPECIFIC ATTRIBUTES ---
{
from: this.buildComprehensivePattern([
'feature enabled', 'feature on', 'feature active', 'flag enabled', 'flag on',
'show feature', 'enable feature', 'activate feature', 'is feature enabled',
'variables enabled'
]),
to: 'feature_enabled',
confidence: 0.95
},
{
from: this.buildComprehensivePattern([
'event type', 'event category', 'tracking type', 'conversion type', 'goal type',
'metric type', 'action type', 'interaction type', 'goal kind', 'metric mode',
'measurement type', 'click vs view', 'count vs revenue'
]),
to: 'event_type',
confidence: 0.9
},
{
from: this.buildComprehensivePattern([
'url', 'edit url', 'page url', 'target url', 'destination', 'destination url',
'link', 'href', 'address', 'web address', 'location', 'endpoint', 'permalink',
'canonical url', 'preview url'
]),
to: 'edit_url',
confidence: 0.85
},
// --- ADDITIONAL PATTERNS FROM ORIGINAL (that weren't in the new list) ---
// RULES FIELDS
{
from: this.buildComprehensivePattern([
'rules', 'rule list', 'rule set', 'ruleset',
'targeting rules', 'delivery rules', 'rollout rules',
'rule conditions'
]),
to: 'rules',
confidence: 0.9
},
// CRITICAL: Flag key for variations (MUST come before generic key mapping!)
{
from: this.buildComprehensivePattern([
'flag_key', 'flagkey', 'flag key', 'feature flag key',
'parent flag', 'parent_flag', 'parent flag key',
'flag id', 'flag_id', 'feature_key', 'feature key',
'ff key', 'ff_key', 'feature_flag_key'
]),
to: 'flag_key',
confidence: 0.95
},
// Experiment ID for variations
{
from: this.buildComprehensivePattern([
'experiment_id', 'experimentid', 'experiment id',
'parent experiment', 'parent_experiment', 'exp id',
'exp_id', 'test id', 'test_id', 'ab test id'
]),
to: 'experiment_id',
confidence: 0.95
},
// ENTITY/DATA FIELDS - Common wrapper patterns
{
from: this.buildComprehensivePattern([
'entity', 'entity data', 'entity_data', 'data',
'payload', 'request', 'request data', 'request_data',
'body', 'request body', 'request_body',
'input', 'input data', 'input_data',
'params', 'parameters', 'request params',
'config', 'configuration', 'settings'
]),
to: '_wrapper_', // Special marker for wrapper objects
confidence: 0.7
},
// WEIGHT FIELD (specific for variations)
{
from: this.buildComprehensivePattern([
'weight', 'variation weight', 'variant weight',
'percentage', 'variation percentage', 'split',
'traffic split', 'allocation', 'variation allocation'
]),
to: 'weight',
confidence: 0.9
},
// ACTIONS FIELD (for variations - Web specific)
{
from: this.buildComprehensivePattern([
'action', 'actions', 'changes', 'change',
'modifications', 'modification', 'updates', 'update',
'variation actions', 'variation changes',
'page changes', 'dom changes', 'css changes'
]),
to: 'actions',
confidence: 0.85
},
// VARIABLE VALUES (for feature flags)
{
from: this.buildComprehensivePattern([
'variable values', 'variable_values', 'variables',
'variable value', 'variable_value',
'feature variables', 'feature_variables',
'flag variables', 'flag_variables',
'values', 'variable settings', 'var values'
]),
to: 'variable_values',
confidence: 0.9
},
// PAGE IDS (plural handling)
{
from: this.buildComprehensivePattern([
'page ids', 'page_ids', 'pageids', 'page id',
'pages', 'page list', 'page_list',
'target pages', 'target_pages', 'page array'
]),
to: 'page_ids',
confidence: 0.9
},
// ENVIRONMENT KEY
{
from: this.buildComprehensivePattern([
'environment', 'environment key', 'environment_key',
'env', 'env key', 'env_key',
'environment name', 'env name'
]),
to: 'environment_key',
confidence: 0.9
},
// RULE PRIORITIES (for rulesets)
{
from: this.buildComprehensivePattern([
'rule priorities', 'rule_priorities', 'priorities',
'rule priority', 'rule_priority', 'priority',
'rule order', 'rule_order', 'ordering',
'priority list', 'priority_list'
]),
to: 'rule_priorities',
confidence: 0.9
},
// ========================================
// CRITICAL MISSING MAPPINGS FROM AUDIT
// ========================================
// Campaign ID - Essential for personalization experiments
{
from: this.buildComprehensivePattern([
'campaign', 'campaign_id', 'campaignid', 'campaign id',
'parent_campaign', 'parent campaign', 'campaign_ref',
'campaign_key', 'personalization_campaign', 'parent_id'
]),
to: 'campaign_id',
confidence: 0.95
},
// Feature ID - Essential for feature tests
{
from: this.buildComprehensivePattern([
'feature', 'feature_id', 'featureid', 'feature id',
'feature_ref', 'flag_feature', 'flag_feature_id',
'attached_feature', 'linked_feature', 'parent_feature'
]),
to: 'feature_id',
confidence: 0.95
},
// Page ID - Essential for page events
{
from: this.buildComprehensivePattern([
'page', 'page_id', 'pageid', 'page id',
'page_ref', 'parent_page', 'target_page',
'associated_page', 'linked_page'
]),
to: 'page_id',
confidence: 0.95
},
// Account ID
{
from: this.buildComprehensivePattern([
'account', 'account_id', 'accountid', 'account id',
'account_ref', 'parent_account', 'org_id',
'organization_id', 'tenant_id'
]),
to: 'account_id',
confidence: 0.95
},
// Layer ID (for rules)
{
from: this.buildComprehensivePattern([
'layer', 'layer_id', 'layerid', 'layer id',
'layer_ref', 'parent_layer', 'decision_layer'
]),
to: 'layer_id',
confidence: 0.95
},
// Layer Experiment ID
{
from: this.buildComprehensivePattern([
'layer_experiment', 'layer_experiment_id', 'layer experiment id',
'experiment_layer_id', 'linked_experiment', 'associated_experiment'
]),
to: 'layer_experiment_id',
confidence: 0.95
},
// Created timestamp
{
from: this.buildComprehensivePattern([
'created', 'created_at', 'created_time', 'createdat',
'creation_date', 'created_date', 'date_created',
'timestamp_created', 'created_timestamp', 'created_on',
'createdAt', 'createdTime', 'creation_time'
]),
to: 'created',
confidence: 0.9
},
// Modified timestamp
{
from: this.buildComprehensivePattern([
'modified', 'last_modified', 'updated', 'updated_at',
'updated_time', 'modified_at', 'modified_time',
'last_updated', 'updatedat', 'updatedAt', 'modifiedAt',
'modification_date', 'last_change', 'changed_at'
]),
to: 'last_modified',
confidence: 0.9
},
// Numeric ID (generic - lower priority than specific IDs)
{
from: this.buildComprehensivePattern([
'id', 'numeric_id', 'entity_id', 'resource_id',
'object_id', 'record_id', 'db_id', 'database_id',
'internal_id', 'system_id'
]),
to: 'id',
confidence: 0.85
},
// Variation ID (specific)
{
from: this.buildComprehensivePattern([
'variation_id', 'variationid', 'variation id',
'variant_id', 'variantid', 'version_id'
]),
to: 'variation_id',
confidence: 0.95
},
// Group ID
{
from: this.buildComprehensivePattern([
'group', 'group_id', 'groupid', 'group id',
'group_ref', 'mutex_group', 'exclusion_group'
]),
to: 'group_id',
confidence: 0.95
},
// Confidence Threshold
{
from: this.buildComprehensivePattern([
'confidence', 'confidence_threshold', 'confidence threshold',
'significance_level', 'statistical_significance',
'confidence_level', 'conf_threshold', 'stats_confidence'
]),
to: 'confidence_threshold',
confidence: 0.9
},
// Holdback
{
from: this.buildComprehensivePattern([
'holdback', 'hold_back', 'holdback_percentage',
'holdback_percent', 'control_percentage',
'excluded_traffic', 'reserve_percentage'
]),
to: 'holdback',
confidence: 0.9
},
// Whitelist/Allow List
{
from: this.buildComprehensivePattern([
'whitelist', 'white_list', 'allowlist', 'allow_list',
'allowed_users', 'forced_users', 'user_overrides',
'forced_variations', 'user_whitelist'
]),
to: 'allow_list',
confidence: 0.9
},
// Experiment Priorities (for campaigns)
{
from: this.buildComprehensivePattern([
'experiment_priorities', 'experiment priorities',
'exp_priorities', 'priority_order', 'experiment_order',
'test_priorities', 'campaign_priorities'
]),
to: 'experiment_priorities',
confidence: 0.9
},
// Variable Definitions (complex object)
{
from: this.buildComprehensivePattern([
'variable_definitions', 'variable definitions',
'var_definitions', 'variables_def', 'flag_variables',
'variable_schema', 'variables_definition'
]),
to: 'variable_definitions',
confidence: 0.9
},
// Environments (complex object)
{
from: this.buildComprehensivePattern([
'environments', 'environment_config', 'env_config',
'environment_settings', 'env_settings',
'environment_map', 'env_map'
]),
to: 'environments',
confidence: 0.9
},
// Outlier Filtering
{
from: this.buildComprehensivePattern([
'outlier_filtering', 'outlier_filtering_enabled',
'filter_outliers', 'outlier_filter', 'remove_outliers',
'outlier_detection', 'statistical_filtering'
]),
to: 'outlier_filtering_enabled',
confidence: 0.9
},
// Feature Name
{
from: this.buildComprehensivePattern([
'feature_name', 'feature name', 'flag_name',
'feature_display_name', 'feature_title'
]),
to: 'feature_name',
confidence: 0.9
},
// DCP Service ID
{
from: this.buildComprehensivePattern([
'dcp_service', 'dcp_service_id', 'dcp service id',
'dynamic_customer_profile', 'dcp_id'
]),
to: 'dcp_service_id',
confidence: 0.95
}
];
}
// Entity-specific template fields (extracted from schemas)
entityTemplateFields = {
flag: ['key', 'name', 'description', 'archived', 'environments'],
experiment: ['name', 'key', 'description', 'variations', 'metrics', 'audience_conditions', 'traffic_allocation', 'status'],
event: ['key', 'name', 'description', 'event_type', 'category', 'archived'],
audience: ['name', 'conditions', 'description', 'archived'],
page: ['name', 'key', 'conditions', 'activation_code', 'page_type', 'archived'],
variation: ['key', 'name', 'actions', 'weight', 'feature_enabled', 'variable_values'],
ruleset: ['enabled', 'rules', 'default_variation_key', 'rollout_id', 'archived'],
rule: ['key', 'enabled', 'audience_conditions', 'variations', 'percentage_included']
};
/**
* Map AI field names to template field names
*/
async mapFields(payload, options) {
this.logger.debug(`Starting field mapping for ${options.entityType}`);
const aiFields = this.extractFieldNames(payload);
const templateFields = this.getTemplateFields(options.entityType);
if (templateFields.length === 0) {
this.logger.debug(`No template fields found for entity type: ${options.entityType}`);
return [];
}
const mappings = [];
// Apply common transformations first
const transformationMappings = this.applyCommonTransformations(aiFields, templateFields);
mappings.push(...transformationMappings);
// Apply fuzzy matching for remaining fields
const unmappedFields = aiFields.filter(field => !mappings.some(mapping => mapping.aiFieldName === field));
if (unmappedFields.length > 0 && !options.strictMode) {
const fuzzyMappings = await this.applyFuzzyMatching(unmappedFields, templateFields, options.threshold || 0.6);
mappings.push(...fuzzyMappings);
}
// Sort by confidence descending
mappings.sort((a, b) => b.confidence - a.confidence);
this.logger.debug(`Field mapping completed: ${mappings.length} mappings found`);
return mappings;
}
/**
* Extract field names from payload
*/
extractFieldNames(payload) {
const fields = [];
const extract = (obj, prefix = '') => {
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
Object.keys(obj).forEach(key => {
const fullKey = prefix ? `${prefix}.${key}` : key;
fields.push(fullKey);
// Don't recurse too deeply for field mapping
if (!prefix.includes('.') && obj[key] && typeof obj[key] === 'object') {
extract(obj[key], fullKey);
}
});
}
};
extract(payload);
return fields;
}
/**
* Get template fields for entity type
*/
getTemplateFields(entityType) {
// Check cache first
if (this.templateFieldCache.has(entityType)) {
return this.templateFieldCache.get(entityType);
}
const fields = this.entityTemplateFields[entityType.toLowerCase()] || [];
this.templateFieldCache.set(entityType, fields);
return fields;
}
/**
* Apply common transformation patterns
*/
applyCommonTransformations(aiFields, templateFields) {
const mappings = [];
for (const aiField of aiFields) {
for (const transformation of this.commonTransformations) {
if (transformation.from.test(aiField)) {
// Check if the target field exists in template
if (templateFields.includes(transformation.to)) {
mappings.push({
aiFieldName: aiField,
templateFieldName: transformation.to,
confidence: transformation.confidence,
transformationType: 'custom'
});
break; // Only apply first matching transformation
}
}
}
}
return mappings;
}
/**
* Apply fuzzy matching using Fuse.js
*/
async applyFuzzyMatching(aiFields, templateFields, threshold) {
const mappings = [];
// Configure Fuse.js for field name matching
const fuse = new Fuse(templateFields, {
includeScore: true,
threshold: threshold,
ignoreLocation: true,
ignoreFieldNorm: true,
keys: [''] // We're searching the strings directly
});
for (const aiField of aiFields) {
// Clean field name for better matching
const cleanField = this.cleanFieldName(aiField);
const results = fuse.search(cleanField);
if (results.length > 0) {
const bestMatch = results[0];
const confidence = 1 - (bestMatch.score || 0); // Convert Fuse score to confidence
if (confidence >= 0.4) { // Minimum confidence threshold
mappings.push({
aiFieldName: aiField,
templateFieldName: bestMatch.item,
confidence,
transformationType: this.detectTransformationType(aiField, bestMatch.item)
});
}
}
}
return mappings;
}
/**
* Clean field name for better matching
*/
cleanFieldName(fieldName) {
return fieldName
.toLowerCase()
.replace(/[_-]/g, ' ') // Convert snake_case and kebab-case to spaces
.replace(/([a-z])([A-Z])/g, '$1 $2') // Convert camelCase to spaces
.trim();
}
/**
* Detect transformation type needed between fields
*/
detectTransformationType(aiField, templateField) {
// Exact match
if (aiField === templateField)
return 'direct';
// Case conversion needed
if (aiField.toLowerCase() === templateField.toLowerCase()) {
if (templateField.includes('_'))
return 'snake_case';
if (/[A-Z]/.test(templateField))
return 'camelCase';
}
return 'custom';
}
/**
* Apply field mappings to transform a payload
*/
async applyMappings(payload, mappings) {
const transformed = { ...payload };
// CRITICAL: Preserve complete array structures before field mappings
// ⚠️ REGRESSION FIX #2 - DO NOT REMOVE THIS PRESERVATION LOGIC ⚠️
//
// HISTORY: This bug has occurred TWICE:
// 1. First fix: Unknown date - variations were being stripped
// 2. Second fix: June 29, 2025 - FieldMapper was stripping ab_test.variations during UPDATE operations
//
// ROOT CAUSE: Field mapping transformations were deleting nested arrays when cleaning up old field names
// IMPACT:
// - Feature Experimentation: Test #7 failures - UPDATE operations lost user-provided variation keys
// - Web Experimentation: Experiment variations, campaign structures, page conditions lost
//
// This preservation logic MUST remain to prevent regression. The arrays listed below are CRITICAL
// for BOTH Feature Experimentation AND Web Experimentation platforms. All entities from both
// platforms pass through this FieldMapper, so we must preserve arrays for both.
//
// IF YOU ARE MODIFYING THIS CODE:
// 1. DO NOT remove any arrays from these lists without understanding the impact
// 2. DO NOT skip the preservation/restoration logic
// 3. DO ADD new critical arrays if you discover them being stripped
// 4. TEST with UPDATE operations for BOTH platforms
// 5. TEST complex nested structures (campaigns with experiments with variations)
//
// See: PROJECT_ORION_C358_TEST_REGISTRY.md and REGRESSION_PREVENTION_GUIDE.md
const preserveArrays = ['variations', 'rules', 'metrics', 'audience_conditions', 'actions', 'experiments', 'conditions', 'options'];
const preserveNestedArrays = [
// Feature Experimentation nested arrays
'ab_test.variations', // CRITICAL: Flag UPDATE operations with A/B tests
'ab_test.metrics', // CRITICAL: Metrics configuration for experiments
'ab_test.audience_conditions', // CRITICAL: Audience targeting
'ruleset.rules', // CRITICAL: Feature flag rules
// Web Experimentation nested arrays
'experiment.variations', // CRITICAL: Web experiment variations
'experiments.variations', // CRITICAL: Nested variations in campaign experiments
'url_targeting.conditions', // CRITICAL: URL targeting rules for experiments
'campaigns.experiments', // CRITICAL: Experiments array within campaigns
'extensions.options', // CRITICAL: Extension configuration options
'page.conditions', // CRITICAL: Page activation conditions
'schedule.time_zone', // Schedule data for experiments
'experiment.metrics', // Metrics array in experiments
'experiments.metrics', // Metrics in nested experiments
'multivariate_traffic_policy.variations' // Multivariate test variations
];
const preservedArrays = {};
const preservedNestedArrays = {};
// CRITICAL: Complete objects that must be preserved during field mapping
// These objects contain complex structures that should not be modified or stripped
const preserveCompleteObjects = ['ab_test', 'variables', 'variable_definitions', 'template_data'];
const preservedObjects = {};
// Save complete arrays before processing
for (const arrayField of preserveArrays) {
if (payload[arrayField] && Array.isArray(payload[arrayField])) {
preservedArrays[arrayField] = this.deepCloneArray(payload[arrayField]);
}
}
// Save complete objects before processing
for (const objectField of preserveCompleteObjects) {
if (payload[objectField] && typeof payload[objectField] === 'object') {
preservedObjects[objectField] = JSON.parse(JSON.stringify(payload[objectField])); // Deep clone
this.logger.debug({
objectField,
objectKeys: Object.keys(payload[objectField]),
hasVariations: !!payload[objectField].variations,
variationCount: payload[objectField].variations?.length || 0
}, 'FieldMapper: Preserving complete object before field mapping');
}
}
// Save complete nested arrays before processing
for (const nestedPath of preserveNestedArrays) {
const parts = nestedPath.split('.');
if (parts.length === 2) {
const [parentKey, arrayKey] = parts;
if (payload[parentKey] && payload[parentKey][arrayKey] && Array.isArray(payload[parentKey][arrayKey])) {
preservedNestedArrays[nestedPath] = this.deepCloneArray(payload[parentKey][arrayKey]);
this.logger.debug({
nestedPath,
arrayLength: payload[parentKey][arrayKey].length,
firstItem: payload[parentKey][arrayKey][0]
}, 'FieldMapper: Preserving nested array before field mapping');
}
}
}
for (const mapping of mappings) {
const { aiFieldName, templateFieldName, transformer } = mapping;
// Skip array fields - we'll restore them completely later
if (preserveArrays.includes(templateFieldName)) {
continue;
}
// CRITICAL FIX: Do not map ab_test.environment to other fields
// The ab_test.environment field must stay within ab_test for orchestration
if (aiFieldName === 'ab_test.environment') {
getLogger().info({
skippedMapping: aiFieldName,
wouldMapTo: templateFieldName,
reason: 'ab_test.environment must remain within ab_test structure'
}, 'FieldMapper: Skipping mapping of critical ab_test field');
continue;
}
// Handle nested field names
if (aiFieldName.includes('.')) {
const parts = aiFieldName.split('.');
let current = payload;
// CRITICAL FIX: Prevent nested arrays from being mapped to other fields
// This prevents ab_test.variations from being mapped to description
if (preserveNestedArrays.includes(aiFieldName)) {
getLogger().info({
skippedMapping: aiFieldName,
wouldMapTo: templateFieldName,
reason: 'Nested arrays must be preserved in their original structure'
}, 'FieldMapper: Skipping mapping of preserved nested array');
continue;
}
// Navigate to the nested value
for (let i = 0; i < parts.length - 1; i++) {
if (current[parts[i]]) {
current = current[parts[i]];
}
else {
break;
}
}
const finalKey = parts[parts.length - 1];
if (current[finalKey] !== undefined) {
const value = transformer ? transformer(current[finalKey]) : current[finalKey];
// CRITICAL FIX: Don't map array values to non-array fields
if (Array.isArray(value) && templateFieldName === 'description') {
getLogger().info({
skippedMapping: aiFieldName,
templateField: templateFieldName,
valueType: 'array',
reason: 'Preventing array from being mapped to description field'
}, 'FieldMapper: Skipped mapping array to description');
continue;
}
transformed[templateFieldName] = value;
// Remove the original nested field if it's at root level
if (parts.length === 2 && transformed[parts[0]]) {
delete transformed[parts[0]][finalKey];
// Remove empty parent object
if (Object.keys(transformed[parts[0]]).length === 0) {
delete transformed[parts[0]];
}
}
}
}
else {
// Simple field mapping
if (payload[aiFieldName] !== undefined) {
const value = transformer ? transformer(payload[aiFieldName]) : payload[aiFieldName];
// CRITICAL FIX: Don't overwrite description with variations
// If the template field is 'description' and already has a value, and the AI field contains 'variations',
// skip this mapping to prevent ab_test.variations from overwriting the description
if (templateFieldName === 'description' && transformed[templateFieldName] !== undefined &&
(aiFieldName.includes('variations') || aiFieldName.includes('ab_test'))) {
this.logger.debug({
skippedMapping: aiFieldName,
templateField: templateFieldName,
reason: 'Preventing variations from overwriting existing description field'
}, 'FieldMapper: Skipped mapping to preserve description');
}
else {
transformed[templateFieldName] = value;
}
// Remove original field if name changed
if (aiFieldName !== templateFieldName) {
delete transformed[aiFieldName];
}
}
}
}
// CRITICAL: Restore preserved arrays with all their original fields intact
for (const [arrayField, arrayValue] of Object.entries(preservedArrays)) {
transformed[arrayField] = arrayValue;
}
// CRITICAL: Restore preserved nested arrays
// ⚠️ REGRESSION FIX #2 - THIS RESTORATION IS MANDATORY ⚠️
//
// This restoration step MUST execute to prevent data loss during field mapping.
// Without this, UPDATE operations will lose critical nested arrays like ab_test.variations
//
// NEVER skip or modify this restoration logic without understanding the full impact
// on UPDATE operations, especially for flags with A/B tests.
for (const [nestedPath, arrayValue] of Object.entries(preservedNestedArrays)) {
const parts = nestedPath.split('.');
if (parts.length === 2) {
const [parentKey, arrayKey] = parts;
if (!transformed[parentKey]) {
transformed[parentKey] = {};
}
transformed[parentKey][arrayKey] = arrayValue;
this.logger.debug({
nestedPath,
restoredArrayLength: arrayValue.length,
firstRestoredItem: arrayValue[0]
}, 'FieldMapper: CRITICAL - Restored nested array after field mapping');
}
}
// CRITICAL: Restore preserved complete objects
// ⚠️ REGRESSION FIX #3 - THIS OBJECT RESTORATION IS MANDATORY ⚠️
//
// This restoration step ensures that complete objects like ab_test are preserved
// during field mapping. Without this, complex flags with ab_test data will be
// treated as basic flags, causing TEST #6 regression.
//
// NEVER skip this restoration logic - it prevents ab_test data from being stripped
for (const [objectField, objectValue] of Object.entries(preservedObjects)) {
transformed[objectField] = objectValue;
this.logger.debug({
objectField,
restoredObjectKeys: Object.keys(objectValue),
hasVariations: !!objectValue.variations,
variationCount: objectValue.variations?.length || 0
}, 'FieldMapper: CRITICAL - Restored complete object after field mapping');
}
return transformed;
}
/**
* Deep clone array preserving all nested structure
*/
deepCloneArray(arr) {
return arr.map(item => {
if (item && typeof item === 'object') {
if (Array.isArray(item)) {
return this.deepCloneArray(item);
}
else {
// Deep clone object
const cloned = {};
Object.entries(item).forEach(([key, value]) => {
if (Array.isArray(value)) {
cloned[key] = this.deepCloneArray(value);
}
else if (value && typeof value === 'object') {
cloned[key] = { ...value };
}
else {
cloned[key] = value;
}
});
return cloned;
}
}
return item;
});
}
/**
* Get mapping suggestions for debugging
*/
async getMappingSuggestions(aiFields, entityType) {
const templateFields = this.getTemplateFields(entityType);
const suggestions = [];
const fuse = new Fuse(templateFields, {
includeScore: true,
threshold: 0.8,
ignoreLocation: true,
keys: ['']
});
for (const field of aiFields) {
const cleanField = this.cleanFieldName(field);
const results = fuse.search(cleanField).slice(0, 3); // Top 3 suggestions
suggestions.push({
field,
suggestions: results.map(r => r.item),
confidence: results.map(r => 1 - (r.score || 0))
});
}
return suggestions;
}
}
/**
* Singleton instance for global use
*/
export const fieldMapper = new FieldMapper();
//# sourceMappingURL=FieldMapper.js.map