UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

329 lines • 12.5 kB
/** * Dry Run Executor * * Simulates template execution without making actual API calls. * Shows what entities would be created and in what order. */ import { ReferenceResolver } from './ReferenceResolver'; import { OrchestrationRulesLayer } from './OrchestrationRulesLayer'; export class DryRunExecutor { referenceResolver; orchestrationRules; simulatedEntities; constructor() { this.referenceResolver = new ReferenceResolver(); this.orchestrationRules = new OrchestrationRulesLayer(); this.simulatedEntities = new Map(); } /** * Execute a dry run of a template */ async execute(template, options) { const steps = []; const errors = []; const warnings = []; let totalEstimatedTime = 0; // Reset simulated entities this.simulatedEntities.clear(); // Process parameters const resolvedParams = this.resolveParameters(template.parameters || {}, options.parameters || {}); // Process each step for (const step of template.steps || []) { try { const dryRunStep = await this.processStep(step, { ...options, template, resolvedParams, previousSteps: steps }); steps.push(dryRunStep); totalEstimatedTime += dryRunStep.estimatedTime; // Stop on error if configured if (options.stopOnError && dryRunStep.action === 'skip' && dryRunStep.reason?.includes('Error')) { errors.push(`Step ${step.id} failed: ${dryRunStep.reason}`); break; } } catch (error) { errors.push(`Error processing step ${step.id}: ${error instanceof Error ? error.message : String(error)}`); if (options.stopOnError) break; } } // Generate summary const summary = { totalSteps: steps.length, createActions: steps.filter(s => s.action === 'create').length, updateActions: steps.filter(s => s.action === 'update').length, lookupActions: steps.filter(s => s.action === 'lookup').length, skippedSteps: steps.filter(s => s.action === 'skip').length }; return { success: errors.length === 0, steps, totalEstimatedTime, errors, warnings, summary }; } /** * Process a single step */ async processStep(step, context) { const entityType = this.extractEntityType(step.template?.system_template_id); if (!entityType) { return { stepId: step.id, action: 'skip', entityType: 'unknown', entityName: 'Unknown', reason: 'Could not determine entity type', inputs: {}, outputs: {}, dependencies: [], warnings: ['Invalid system_template_id'], estimatedTime: 0 }; } // Resolve inputs const resolvedInputs = await this.resolveInputs(step.template?.inputs || {}, context); // Check dependencies const dependencies = this.extractDependencies(step.template?.inputs || {}); // Determine action let action = 'create'; let reason = ''; // Check if entity would be looked up if (resolvedInputs.ref) { action = 'lookup'; reason = 'Entity reference provided'; } // Simulate entity creation const simulatedEntity = { id: `simulated_${step.id}_${Date.now()}`, type: entityType, name: resolvedInputs.name || step.id, ...resolvedInputs }; // Store for future reference resolution this.simulatedEntities.set(step.id, simulatedEntity); // Extract warnings from orchestration rules const warnings = await this.checkOrchestrationWarnings(entityType, resolvedInputs, context.template.platform || 'web'); return { stepId: step.id, action, entityType, entityName: resolvedInputs.name || step.id, reason, inputs: resolvedInputs, outputs: { entity_id: simulatedEntity.id, entity_type: entityType, entity_name: simulatedEntity.name }, dependencies, warnings, estimatedTime: this.estimateExecutionTime(action, entityType) }; } /** * Resolve template parameters */ resolveParameters(templateParams, providedParams) { const resolved = {}; for (const [key, config] of Object.entries(templateParams)) { if (providedParams[key] !== undefined) { resolved[key] = providedParams[key]; } else if (config.default !== undefined) { resolved[key] = config.default; } else if (config.required) { resolved[key] = `<MISSING_REQUIRED_PARAM:${key}>`; } } return { ...providedParams, ...resolved }; } /** * Resolve step inputs (replace parameter references) */ async resolveInputs(inputs, context) { const resolved = {}; for (const [key, value] of Object.entries(inputs)) { if (typeof value === 'string') { // Parameter reference if (value.startsWith('${') && value.endsWith('}') && !value.includes('.')) { const paramName = value.slice(2, -1); resolved[key] = context.resolvedParams[paramName] || value; } // Step reference else if (value.startsWith('${') && value.endsWith('}')) { const [stepId, field] = value.slice(2, -1).split('.'); const previousStep = context.previousSteps.find((s) => s.stepId === stepId); if (previousStep) { resolved[key] = previousStep.outputs[field] || `<REF:${stepId}.${field}>`; } else { resolved[key] = `<UNRESOLVED_REF:${value}>`; } } else { resolved[key] = value; } } else if (Array.isArray(value)) { resolved[key] = await Promise.all(value.map(item => this.resolveInputs(typeof item === 'object' ? item : { value: item }, context))); } else if (typeof value === 'object' && value !== null) { resolved[key] = await this.resolveInputs(value, context); } else { resolved[key] = value; } } return resolved; } /** * Extract dependencies from inputs */ extractDependencies(inputs) { const deps = new Set(); const extract = (obj) => { if (typeof obj === 'string' && obj.startsWith('${') && obj.endsWith('}')) { const [stepId] = obj.slice(2, -1).split('.'); if (stepId) deps.add(stepId); } else if (Array.isArray(obj)) { obj.forEach(extract); } else if (typeof obj === 'object' && obj !== null) { Object.values(obj).forEach(extract); } }; extract(inputs); return Array.from(deps); } /** * Check for orchestration warnings */ async checkOrchestrationWarnings(entityType, inputs, platform) { const warnings = []; // Platform compatibility warnings const webOnly = ['experiment', 'campaign', 'page']; const featureOnly = ['flag', 'ruleset', 'rule']; if (webOnly.includes(entityType) && platform === 'feature') { warnings.push(`${entityType} is not typically used on feature platform`); } if (featureOnly.includes(entityType) && platform === 'web') { warnings.push(`${entityType} is not supported on web platform`); } // Check for common issues if (entityType === 'experiment' && !inputs.variations) { warnings.push('Experiment missing variations'); } if (entityType === 'campaign' && !inputs.metrics) { warnings.push('Campaign missing metrics'); } return warnings; } /** * Estimate execution time for an action */ estimateExecutionTime(action, entityType) { const baseTime = { create: 2000, update: 1500, lookup: 500, skip: 0 }; const entityComplexity = { experiment: 1.5, campaign: 1.5, audience: 1.2, flag: 1.0, ruleset: 1.3, page: 0.8, event: 0.7 }; const base = baseTime[action] || 1000; const complexity = entityComplexity[entityType] || 1.0; return Math.round(base * complexity); } /** * Extract entity type from system template ID */ extractEntityType(systemTemplateId) { if (!systemTemplateId) return null; const patterns = [ { regex: /optimizely_experiment_/, type: 'experiment' }, { regex: /optimizely_campaign_/, type: 'campaign' }, { regex: /optimizely_audience_/, type: 'audience' }, { regex: /optimizely_event_/, type: 'event' }, { regex: /optimizely_flag_/, type: 'flag' }, { regex: /optimizely_ruleset_/, type: 'ruleset' }, { regex: /optimizely_rule_/, type: 'rule' }, { regex: /optimizely_page_/, type: 'page' }, { regex: /optimizely_environment_/, type: 'environment' }, { regex: /optimizely_project_/, type: 'project' } ]; for (const pattern of patterns) { if (pattern.regex.test(systemTemplateId)) { return pattern.type; } } return null; } /** * Format dry run results for display */ formatResults(result) { const lines = []; lines.push('TEMPLATE DRY RUN PREVIEW'); lines.push('========================\n'); if (!result.success) { lines.push('❌ Dry run failed with errors:'); result.errors.forEach(err => lines.push(` • ${err}`)); lines.push(''); } lines.push('EXECUTION PLAN:'); lines.push('---------------\n'); for (const step of result.steps) { const icon = { create: '✨', update: '📝', lookup: '🔍', skip: '⏭️' }[step.action]; lines.push(`${icon} Step ${step.stepId}: ${step.action.toUpperCase()} ${step.entityType}`); lines.push(` Name: ${step.entityName}`); if (step.reason) { lines.push(` Reason: ${step.reason}`); } if (step.dependencies.length > 0) { lines.push(` Dependencies: ${step.dependencies.join(', ')}`); } if (step.warnings.length > 0) { lines.push(` ⚠️ Warnings:`); step.warnings.forEach(w => lines.push(` • ${w}`)); } lines.push(` Estimated time: ${step.estimatedTime}ms`); lines.push(''); } lines.push('SUMMARY:'); lines.push('--------'); lines.push(`Total steps: ${result.summary.totalSteps}`); lines.push(` • Create: ${result.summary.createActions}`); lines.push(` • Update: ${result.summary.updateActions}`); lines.push(` • Lookup: ${result.summary.lookupActions}`); lines.push(` • Skip: ${result.summary.skippedSteps}`); lines.push(`\nEstimated total time: ${(result.totalEstimatedTime / 1000).toFixed(1)}s`); if (result.warnings.length > 0) { lines.push('\n⚠️ WARNINGS:'); result.warnings.forEach(w => lines.push(` • ${w}`)); } return lines.join('\n'); } } //# sourceMappingURL=DryRunExecutor.js.map