@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
390 lines • 17.8 kB
JavaScript
/**
* Ruleset Payload Transformer
* @description Specialized transformer for converting AI-generated ruleset payloads to API-compatible format
*
* Purpose: Handle the specific case that triggered this implementation - malformed ruleset payloads
* where AI agents send nested objects instead of the complete ruleset structure required by PATCH operations.
*
* Key Features:
* - Handles nested ruleset_data extraction
* - Converts A/B test structures to proper rulesets
* - Validates ruleset completeness
* - Provides detailed transformation logging
*
* @author Optimizely MCP Server
* @version 1.0.0
*/
import { getLogger } from '../logging/Logger.js';
/**
* Specialized transformer for ruleset payloads
*/
export class RulesetPayloadTransformer {
logger = getLogger();
// Required fields for a complete ruleset according to API spec
requiredRulesetFields = [
'enabled',
'rules',
'default_variation_key'
];
// Optional but common ruleset fields
optionalRulesetFields = [
'rollout_id',
'archived'
];
constructor() {
this.logger.debug('RulesetPayloadTransformer initialized');
}
/**
* Transform malformed ruleset payload to API-compatible format
*/
async transformRulesetPayload(payload) {
this.logger.debug(`Transforming ruleset payload with ${Object.keys(payload || {}).length} root fields`);
const transformationsApplied = [];
let transformed = { ...payload };
const errors = [];
const warnings = [];
try {
// Step 1: Extract nested ruleset_data if present
if (payload.ruleset_data) {
const extractionResult = await this.extractNestedRulesetData(transformed);
transformed = extractionResult.transformed;
transformationsApplied.push(...extractionResult.transformations);
warnings.push(...extractionResult.warnings);
}
// Step 2: Handle A/B test structure conversion
if (this.isABTestStructure(transformed)) {
const abTestResult = await this.convertABTestToRuleset(transformed);
transformed = abTestResult.transformed;
transformationsApplied.push(...abTestResult.transformations);
warnings.push(...abTestResult.warnings);
}
// Step 3: Normalize field names and values
const normalizationResult = await this.normalizeRulesetFields(transformed);
transformed = normalizationResult.transformed;
transformationsApplied.push(...normalizationResult.transformations);
// Step 4: Add missing required fields with defaults
const completionResult = await this.completeRulesetStructure(transformed);
transformed = completionResult.transformed;
transformationsApplied.push(...completionResult.transformations);
warnings.push(...completionResult.warnings);
// Step 5: Validate final structure
const validation = await this.validateRulesetStructure(transformed);
errors.push(...validation.errors);
warnings.push(...validation.warnings);
// Calculate confidence based on validation and transformations
const confidence = this.calculateTransformationConfidence(validation, transformationsApplied, payload, transformed);
const result = {
success: validation.isValid && errors.length === 0,
transformedRuleset: validation.isValid ? transformed : undefined,
errors: errors.length > 0 ? errors : undefined,
warnings: warnings.length > 0 ? warnings : undefined,
transformationsApplied,
confidence
};
this.logger.debug(`Ruleset transformation completed: success=${result.success}, transformations=${transformationsApplied.length}, confidence=${confidence.toFixed(3)}`);
return result;
}
catch (error) {
this.logger.error(`Ruleset transformation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
return {
success: false,
errors: [`Transformation failed: ${error instanceof Error ? error.message : 'Unknown error'}`],
transformationsApplied,
confidence: 0
};
}
}
/**
* Extract nested ruleset_data to root level
*/
async extractNestedRulesetData(payload) {
const transformed = { ...payload };
const transformations = [];
const warnings = [];
if (payload.ruleset_data && typeof payload.ruleset_data === 'object') {
const rulesetData = payload.ruleset_data;
// Extract each field to root level
if (rulesetData.enabled !== undefined) {
transformed.enabled = rulesetData.enabled;
transformations.push('Extracted enabled from ruleset_data');
}
if (rulesetData.rules) {
transformed.rules = rulesetData.rules;
transformations.push('Extracted rules from ruleset_data');
}
if (rulesetData.default_variation_key) {
transformed.default_variation_key = rulesetData.default_variation_key;
transformations.push('Extracted default_variation_key from ruleset_data');
}
if (rulesetData.rollout_id) {
transformed.rollout_id = rulesetData.rollout_id;
transformations.push('Extracted rollout_id from ruleset_data');
}
if (rulesetData.archived !== undefined) {
transformed.archived = rulesetData.archived;
transformations.push('Extracted archived from ruleset_data');
}
// Remove the nested structure
delete transformed.ruleset_data;
transformations.push('Removed nested ruleset_data structure');
// Check for any remaining fields in original ruleset_data
const remainingFields = Object.keys(rulesetData).filter(key => !['enabled', 'rules', 'default_variation_key', 'rollout_id', 'archived'].includes(key));
if (remainingFields.length > 0) {
warnings.push(`Unhandled fields in ruleset_data: ${remainingFields.join(', ')}`);
}
}
return { transformed, transformations, warnings };
}
/**
* Check if payload has A/B test structure
*/
isABTestStructure(payload) {
// Look for common A/B test indicators
return !!(payload.variations && Array.isArray(payload.variations) ||
payload.test_variations ||
payload.ab_test ||
payload.split_test ||
(payload.control && payload.treatment));
}
/**
* Convert A/B test structure to proper ruleset
*/
async convertABTestToRuleset(payload) {
const transformed = { ...payload };
const transformations = [];
const warnings = [];
// Handle variations array
if (payload.variations && Array.isArray(payload.variations)) {
// Create a rule from variations
const rule = {
key: 'default_rule',
enabled: true,
audience_conditions: ['everyone'],
variations: payload.variations.map((variation, index) => ({
key: variation.key || `variation_${index}`,
name: variation.name || `Variation ${index + 1}`,
percentage_included: variation.percentage_included || variation.weight ||
(index === 0 ? 5000 : 5000) // Default 50/50 split
}))
};
transformed.rules = [rule];
transformations.push('Converted variations array to rules structure');
// Set default variation from first variation
if (payload.variations[0]) {
transformed.default_variation_key = payload.variations[0].key || 'variation_0';
transformations.push('Set default_variation_key from first variation');
}
// Remove original variations field
delete transformed.variations;
}
// Handle control/treatment structure
if (payload.control && payload.treatment) {
const rule = {
key: 'ab_test_rule',
enabled: true,
audience_conditions: ['everyone'],
variations: [
{
key: payload.control.key || 'control',
name: payload.control.name || 'Control',
percentage_included: 5000
},
{
key: payload.treatment.key || 'treatment',
name: payload.treatment.name || 'Treatment',
percentage_included: 5000
}
]
};
transformed.rules = [rule];
transformed.default_variation_key = payload.control.key || 'control';
transformations.push('Converted control/treatment to rules structure');
delete transformed.control;
delete transformed.treatment;
}
return { transformed, transformations, warnings };
}
/**
* Normalize field names and values
*/
async normalizeRulesetFields(payload) {
const transformed = { ...payload };
const transformations = [];
// Normalize enabled field
if (payload.is_active !== undefined && payload.enabled === undefined) {
transformed.enabled = payload.is_active;
delete transformed.is_active;
transformations.push('Converted is_active to enabled');
}
if (payload.active !== undefined && payload.enabled === undefined) {
transformed.enabled = payload.active;
delete transformed.active;
transformations.push('Converted active to enabled');
}
// Normalize boolean values
if (typeof transformed.enabled === 'string') {
const normalizedEnabled = transformed.enabled.toLowerCase();
if (['true', '1', 'yes', 'on'].includes(normalizedEnabled)) {
transformed.enabled = true;
transformations.push('Normalized enabled string to boolean true');
}
else if (['false', '0', 'no', 'off'].includes(normalizedEnabled)) {
transformed.enabled = false;
transformations.push('Normalized enabled string to boolean false');
}
}
// Normalize archived field
if (typeof transformed.archived === 'string') {
const normalizedArchived = transformed.archived.toLowerCase();
if (['true', '1', 'yes'].includes(normalizedArchived)) {
transformed.archived = true;
transformations.push('Normalized archived string to boolean true');
}
else if (['false', '0', 'no'].includes(normalizedArchived)) {
transformed.archived = false;
transformations.push('Normalized archived string to boolean false');
}
}
// Ensure rules is an array
if (transformed.rules && !Array.isArray(transformed.rules)) {
if (typeof transformed.rules === 'object') {
transformed.rules = [transformed.rules];
transformations.push('Converted single rule object to array');
}
}
return { transformed, transformations };
}
/**
* Complete ruleset structure with required defaults
*/
async completeRulesetStructure(payload) {
const transformed = { ...payload };
const transformations = [];
const warnings = [];
// Ensure enabled field exists
if (transformed.enabled === undefined) {
transformed.enabled = true;
transformations.push('Added default enabled=true');
}
// Ensure rules array exists
if (!transformed.rules) {
transformed.rules = [];
transformations.push('Added empty rules array');
warnings.push('No rules provided - ruleset will not serve any variations');
}
// Ensure default_variation_key exists
if (!transformed.default_variation_key) {
// Try to infer from rules
if (transformed.rules && Array.isArray(transformed.rules) && transformed.rules.length > 0) {
const firstRule = transformed.rules[0];
if (firstRule.variations && firstRule.variations.length > 0) {
transformed.default_variation_key = firstRule.variations[0].key;
transformations.push('Inferred default_variation_key from first rule variation');
}
}
// If still no default_variation_key, this is an error that will be caught in validation
if (!transformed.default_variation_key) {
warnings.push('Could not determine default_variation_key - this is required for rulesets');
}
}
// Set archived default if not specified
if (transformed.archived === undefined) {
transformed.archived = false;
transformations.push('Added default archived=false');
}
return { transformed, transformations, warnings };
}
/**
* Validate final ruleset structure
*/
async validateRulesetStructure(ruleset) {
const errors = [];
const warnings = [];
const missingFields = [];
const suggestions = [];
// Check required fields
for (const field of this.requiredRulesetFields) {
if (ruleset[field] === undefined || ruleset[field] === null) {
errors.push(`Required field '${field}' is missing or null`);
missingFields.push(field);
}
}
// Validate field types and values
if (ruleset.enabled !== undefined && typeof ruleset.enabled !== 'boolean') {
errors.push('Field "enabled" must be a boolean');
}
if (ruleset.rules !== undefined && !Array.isArray(ruleset.rules)) {
errors.push('Field "rules" must be an array');
}
if (ruleset.default_variation_key !== undefined && typeof ruleset.default_variation_key !== 'string') {
errors.push('Field "default_variation_key" must be a string');
}
if (ruleset.archived !== undefined && typeof ruleset.archived !== 'boolean') {
errors.push('Field "archived" must be a boolean');
}
// Validate rules structure
if (ruleset.rules && Array.isArray(ruleset.rules)) {
if (ruleset.rules.length === 0) {
warnings.push('Ruleset has no rules - it will not serve any variations to users');
}
ruleset.rules.forEach((rule, index) => {
if (!rule.key) {
warnings.push(`Rule ${index} missing key field`);
}
if (!rule.variations || !Array.isArray(rule.variations) || rule.variations.length === 0) {
warnings.push(`Rule ${index} has no variations`);
}
});
}
// Generate helpful suggestions
if (missingFields.length > 0) {
suggestions.push(`Add missing required fields: ${missingFields.join(', ')}`);
}
if (errors.length === 0 && warnings.length > 0) {
suggestions.push('Consider reviewing warnings to ensure optimal ruleset configuration');
}
return {
isValid: errors.length === 0,
errors,
warnings,
missingFields,
suggestions
};
}
/**
* Calculate transformation confidence score
*/
calculateTransformationConfidence(validation, transformationsApplied, originalPayload, transformedPayload) {
let confidence = 0;
// Base confidence on validation success
if (validation.isValid) {
confidence += 0.4; // 40% for valid structure
}
else {
confidence -= 0.2; // Penalty for invalid structure
}
// Factor in successful transformations
const transformationScore = Math.min(transformationsApplied.length / 5, 1) * 0.3; // Up to 30%
confidence += transformationScore;
// Bonus for having all required fields after transformation
const requiredFieldsPresent = this.requiredRulesetFields.every(field => transformedPayload[field] !== undefined);
if (requiredFieldsPresent) {
confidence += 0.2; // 20% bonus
}
// Small bonus for preserving original data
const originalFieldsCount = Object.keys(originalPayload).length;
const preservedFieldsCount = Object.keys(originalPayload).filter(key => transformedPayload[key] !== undefined || key === 'ruleset_data' // ruleset_data gets extracted
).length;
if (originalFieldsCount > 0) {
const preservationRatio = preservedFieldsCount / originalFieldsCount;
confidence += preservationRatio * 0.1; // Up to 10% bonus
}
// Ensure confidence is between 0 and 1
return Math.max(0, Math.min(1, confidence));
}
}
/**
* Singleton instance for global use
*/
export const rulesetPayloadTransformer = new RulesetPayloadTransformer();
//# sourceMappingURL=RulesetPayloadTransformer.js.map