UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

190 lines 8.38 kB
/** * Business Logic Preserver * @description Identifies and preserves user-specified business logic during auto-correction * * Purpose: Prevent auto-correction from changing business-critical values like * traffic allocation percentages, weights, and other values that affect experiment behavior. */ import { getLogger } from '../logging/Logger.js'; export class BusinessLogicPreserver { logger = getLogger(); // Fields that represent business logic, not just technical formatting BUSINESS_CRITICAL_FIELDS = new Set([ 'traffic_allocation', 'holdback', 'percentage_included', 'weight', 'allocation_policy', 'enabled', 'status', 'archived', 'variations', // Array of variations affects experiment behavior 'default_variation_key', 'whitelist' // User whitelist is business logic ]); // Fields where any non-zero value was likely intentional PERCENTAGE_FIELDS = new Set([ 'traffic_allocation', 'holdback', 'percentage_included', 'weight' ]); /** * Check if a field change affects business logic */ isBusinessCriticalChange(field, originalValue, newValue) { // Always critical if in the critical fields list if (this.BUSINESS_CRITICAL_FIELDS.has(field)) { // For percentage fields, changing from a high value to a low value is critical if (this.PERCENTAGE_FIELDS.has(field)) { const original = Number(originalValue); const suggested = Number(newValue); // If user specified 100% (10000) and we're suggesting 1% (100), that's critical if (original > 1000 && suggested < 200) { return true; } // Any change > 10% is business critical if (Math.abs(original - suggested) > 1000) { return true; } } // For boolean fields, any change is critical if (typeof originalValue === 'boolean' && originalValue !== newValue) { return true; } // For status fields, certain changes are critical if (field === 'status' || field === 'archived' || field === 'enabled') { return originalValue !== newValue; } } return false; } /** * Analyze proposed changes and identify business logic modifications */ analyzeChanges(original, suggested, context) { const changes = []; const warnings = []; let hasBusinessCriticalChanges = false; // Check each field in the suggested payload for (const [field, suggestedValue] of Object.entries(suggested)) { const originalValue = original[field]; // Skip if values are the same if (originalValue === suggestedValue) { continue; } // Skip if original had no value (not user-specified) if (originalValue === undefined || originalValue === null) { continue; } const isCritical = this.isBusinessCriticalChange(field, originalValue, suggestedValue); if (isCritical) { hasBusinessCriticalChanges = true; const change = { field, originalValue, suggestedValue, reason: this.getChangeReason(field, originalValue, suggestedValue), isBusinessCritical: true, requiresConfirmation: true }; changes.push(change); // Add warning for percentage changes if (this.PERCENTAGE_FIELDS.has(field)) { const originalPercent = (Number(originalValue) / 100).toFixed(1); const suggestedPercent = (Number(suggestedValue) / 100).toFixed(1); warnings.push(`⚠️ ${field} would change from ${originalPercent}% to ${suggestedPercent}%. ` + `This significantly affects experiment behavior. Add "_preserve_business_values": true to keep original value.`); } } } this.logger.info({ hasBusinessCriticalChanges, changeCount: changes.length, fields: changes.map(c => c.field) }, 'Business logic preservation analysis complete'); return { preserved: !hasBusinessCriticalChanges, changes, warnings }; } /** * Get human-readable reason for the change */ getChangeReason(field, originalValue, suggestedValue) { if (field === 'traffic_allocation') { if (Number(originalValue) === 10000 && Number(suggestedValue) === 100) { return 'Reducing from 100% to 1% (safe default for new experiments)'; } return `Changing traffic allocation from ${(Number(originalValue) / 100).toFixed(1)}% to ${(Number(suggestedValue) / 100).toFixed(1)}%`; } if (field === 'holdback') { return `Changing holdback from ${(Number(originalValue) / 100).toFixed(1)}% to ${(Number(suggestedValue) / 100).toFixed(1)}%`; } if (field === 'weight') { return `Changing variation weight from ${(Number(originalValue) / 100).toFixed(1)}% to ${(Number(suggestedValue) / 100).toFixed(1)}%`; } if (field === 'enabled' || field === 'archived' || field === 'status') { return `Changing ${field} from ${originalValue} to ${suggestedValue}`; } return `Changing ${field} value`; } /** * Apply preservation logic to maintain user-specified values */ preserveBusinessLogic(original, suggested, options) { const preserved = { ...suggested }; const analysis = this.analyzeChanges(original, suggested); if (options?.preserveAll || original._preserve_business_values) { // Preserve all business-critical values for (const change of analysis.changes) { if (change.isBusinessCritical) { preserved[change.field] = change.originalValue; this.logger.info({ field: change.field, preservedValue: change.originalValue, rejectedValue: change.suggestedValue }, 'Preserved user-specified business value'); } } } else if (options?.preserveFields) { // Preserve specific fields for (const field of options.preserveFields) { if (original[field] !== undefined) { preserved[field] = original[field]; } } } else { // Default: Preserve percentage fields that would change significantly for (const change of analysis.changes) { if (this.PERCENTAGE_FIELDS.has(change.field)) { const originalNum = Number(change.originalValue); const suggestedNum = Number(change.suggestedValue); // Preserve if changing by more than 90% (e.g., 10000 to 100) if (originalNum > 0 && (suggestedNum / originalNum) < 0.1) { preserved[change.field] = change.originalValue; preserved._business_logic_preserved = true; preserved._preserved_fields = preserved._preserved_fields || []; preserved._preserved_fields.push({ field: change.field, reason: change.reason, original: change.originalValue, suggested: change.suggestedValue }); } } } } // Add warnings if we preserved values if (preserved._business_logic_preserved) { preserved._preservation_warnings = analysis.warnings; } return preserved; } } // Export singleton instance export const businessLogicPreserver = new BusinessLogicPreserver(); //# sourceMappingURL=BusinessLogicPreserver.js.map