@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
520 lines • 25 kB
JavaScript
/**
* Orchestration Rules Layer
*
* This layer adds orchestration-specific validation and transformation rules
* on top of the base OpenAPI fields from fields.generated.ts
*/
import { FIELDS } from '../../generated/fields.generated';
export class OrchestrationRulesLayer {
baseFields = FIELDS;
orchestrationRules;
constructor() {
// Define all orchestration-specific rules
this.orchestrationRules = {
// Web platform rules
web: {
campaign: {
create: {
metrics: {
scope: {
override: 'session',
validation: (value) => value === 'session',
error: 'Campaigns MUST use metrics scope: "session", not "visitor"',
autoFix: () => 'session'
}
},
// Holdback validation
holdback: {
validation: (value) => {
if (value === undefined)
return true;
return typeof value === 'number' && value >= 0 && value <= 10000;
},
error: 'Holdback must be between 0 and 10000 (basis points)',
autoFix: (value) => Math.max(0, Math.min(10000, parseInt(value) || 0))
}
}
},
experiment: {
create: {
metrics: {
scope: {
override: 'visitor',
validation: (value) => value === 'visitor',
error: 'Experiments MUST use metrics scope: "visitor", not "session"',
autoFix: () => 'visitor'
}
},
// Mode-specific field naming
audience: {
modes: {
template: { fieldName: 'audience', format: 'ref_object' },
direct: { fieldName: 'audience_conditions', format: 'json_string' }
},
validation: (value, context) => {
if (context.mode === 'template') {
// In template mode, should be ref object
return typeof value === 'object' && value.ref !== undefined;
}
return true;
},
error: 'In template mode, use ref object format for audience field'
},
// Traffic allocation validation
traffic_allocation: {
validation: (value) => {
if (value === undefined)
return true;
return typeof value === 'number' && value >= 0 && value <= 10000;
},
error: 'Traffic allocation must be between 0 and 10000 (basis points)'
},
// Variations validation
variations: {
validation: (value) => {
if (!Array.isArray(value))
return false;
if (value.length < 2)
return false;
// Check weights sum to 10000
const totalWeight = value.reduce((sum, v) => sum + (v.weight || 0), 0);
if (totalWeight !== 10000)
return false;
return true;
},
error: 'Experiments must have at least 2 variations with weights summing to 10000',
autoFix: (value) => {
if (!Array.isArray(value) || value.length < 2) {
return [
{ name: 'Control', weight: 5000 },
{ name: 'Variation', weight: 5000 }
];
}
// Adjust weights to sum to 10000
const totalWeight = value.reduce((sum, v) => sum + (v.weight || 0), 0);
if (totalWeight === 0) {
const equalWeight = Math.floor(10000 / value.length);
return value.map((v, i) => ({
...v,
weight: i === value.length - 1 ? 10000 - (equalWeight * (value.length - 1)) : equalWeight
}));
}
// Scale weights proportionally
return value.map(v => ({
...v,
weight: Math.round((v.weight || 0) * 10000 / totalWeight)
}));
}
}
}
},
page: {
create: {
// URL validation for pages
edit_url: {
validation: (value) => {
try {
new URL(value);
return true;
}
catch {
return false;
}
},
error: 'edit_url must be a valid URL',
autoFix: (value) => {
if (!value)
return 'https://example.com';
if (!value.startsWith('http'))
return `https://${value}`;
return value;
}
},
// Activation type rules
activation_code: {
conditional: (context) => {
const activationType = context.step?.template?.inputs?.activation_type;
return activationType === 'polling' || activationType === 'callback';
},
validation: (value, context) => {
const activationType = context.step?.template?.inputs?.activation_type;
if (activationType === 'polling' || activationType === 'callback') {
return typeof value === 'string' && value.length > 0;
}
return true;
},
error: 'activation_code is required when activation_type is "polling" or "callback"'
}
}
}
},
// Feature platform rules
feature: {
flag: {
create: {
// Default value type matching
default_value: {
customValidation: (value, context) => {
const varType = context.variables?.[0]?.type;
if (!varType)
return true;
return this.validateTypeMatch(value, varType);
},
error: 'default_value must match variable type',
autoFix: (value, context) => {
const varType = context.variables?.[0]?.type;
if (!varType)
return value;
switch (varType) {
case 'boolean': return false;
case 'string': return '';
case 'integer': return 0;
case 'double': return 0.0;
case 'json': return '{}';
default: return value;
}
}
},
// Key format validation
key: {
validation: (value) => {
return /^[a-zA-Z0-9_\-]+$/.test(value);
},
error: 'Flag key must contain only alphanumeric characters, hyphens, and underscores',
autoFix: (value) => {
return value.replace(/[^a-zA-Z0-9_\-]/g, '_').toLowerCase();
}
}
}
},
ruleset: {
create: {
// Rule priorities validation
rule_priorities: {
validation: (value, context) => {
if (!Array.isArray(value))
return false;
// Check all rules exist
const rules = context.rules || {};
return value.every(ruleKey => rules[ruleKey] !== undefined);
},
error: 'All rules in rule_priorities must exist in rules object'
},
// Default variation validation
default_variation_key: {
validation: (value, context) => {
const flag = context.flag;
if (!flag || !flag.variations)
return true;
return flag.variations.some((v) => v.key === value);
},
error: 'default_variation_key must match a variation key in the flag'
}
}
},
rule: {
create: {
// Percentage included validation
percentage_included: {
validation: (value) => {
return typeof value === 'number' && value >= 0 && value <= 10000;
},
error: 'percentage_included must be between 0 and 10000 (basis points)',
autoFix: (value) => Math.max(0, Math.min(10000, parseInt(value) || 0))
},
// Distribution mode specific rules
variations: {
conditional: (context) => {
return context.step?.template?.inputs?.type === 'a/b';
},
validation: (value, context) => {
if (context.step?.template?.inputs?.type !== 'a/b')
return true;
if (!value || typeof value !== 'object')
return false;
// For A/B tests, need at least 2 variations
return Object.keys(value).length >= 2;
},
error: 'A/B test rules must have at least 2 variations'
}
}
}
},
// Rules that apply to both platforms
both: {
audience: {
create: {
_acknowledged_complexity: {
validation: (value, context) => {
// Required when conditions are complex
const conditions = context.step?.template?.inputs?.conditions;
if (conditions && typeof conditions === 'string' && conditions.length > 50) {
return value === true;
}
return true;
},
error: 'Complex audiences require _acknowledged_complexity: true',
autoFix: (value, context) => {
const conditions = context.step?.template?.inputs?.conditions;
if (conditions && typeof conditions === 'string' && conditions.length > 50) {
return true;
}
return undefined;
}
},
// Conditions format validation
conditions: {
validation: (value) => {
if (!value)
return true;
if (typeof value !== 'string')
return false;
try {
const parsed = JSON.parse(value);
// Basic structure validation
return Array.isArray(parsed) || typeof parsed === 'object';
}
catch {
return false;
}
},
error: 'Audience conditions must be a valid JSON string',
autoFix: (value) => {
if (typeof value === 'object') {
return JSON.stringify(value);
}
return value;
}
}
}
},
event: {
create: {
// Key format validation
key: {
validation: (value) => {
return /^[a-zA-Z0-9_\-]+$/.test(value) && value.length <= 64;
},
error: 'Event key must contain only alphanumeric characters, hyphens, underscores (max 64 chars)',
autoFix: (value) => {
return value.replace(/[^a-zA-Z0-9_\-]/g, '_').substring(0, 64).toLowerCase();
}
}
}
},
variation: {
create: {
// Weight validation
weight: {
validation: (value, context) => {
if (typeof value !== 'number')
return false;
if (value < 0 || value > 10000)
return false;
// If part of a set, will be validated at parent level
return true;
},
error: 'Variation weight must be between 0 and 10000 (basis points)'
}
}
}
}
};
}
/**
* Get fields with orchestration rules applied
*/
getFieldsWithRules(entityType, operation = 'create', platform = 'web', mode = 'direct') {
// Start with base fields from generated file
const baseFields = this.baseFields[entityType];
if (!baseFields) {
throw new Error(`Unknown entity type: ${entityType}`);
}
// Apply orchestration-specific overrides
const platformRules = this.orchestrationRules[platform]?.[entityType]?.[operation] || {};
const sharedRules = this.orchestrationRules.both?.[entityType]?.[operation] || {};
const rules = { ...sharedRules, ...platformRules };
return {
required: [...(baseFields.required || [])],
optional: [...(baseFields.optional || [])],
defaults: { ...(baseFields.defaults || {}) },
enums: { ...(baseFields.enums || {}) },
fieldTypes: { ...(baseFields.fieldTypes || {}) },
fieldDescriptions: { ...(baseFields.fieldDescriptions || {}) },
fieldExamples: { ...(baseFields.fieldExamples || {}) },
orchestrationRules: rules,
getField: (fieldName) => {
const base = baseFields.fieldDescriptions?.[fieldName];
const rule = rules?.[fieldName];
return {
name: fieldName,
type: baseFields.fieldTypes?.[fieldName],
required: baseFields.required?.includes(fieldName),
description: base,
enum: baseFields.enums?.[fieldName],
example: baseFields.fieldExamples?.[fieldName],
default: baseFields.defaults?.[fieldName],
// Orchestration-specific additions
orchestrationRule: rule,
platformOverride: rule?.override,
customValidation: rule?.validation || rule?.customValidation,
modeVariations: rule?.modes
};
}
};
}
/**
* Validate a value against both OpenAPI and orchestration rules
*/
async validateField(entityType, fieldName, value, context) {
const baseFields = this.baseFields[entityType];
if (!baseFields) {
return { valid: false, error: `Unknown entity type: ${entityType}` };
}
// First validate against OpenAPI schema
const fieldType = baseFields.fieldTypes?.[fieldName];
if (fieldType && !this.matchesType(value, fieldType)) {
return {
valid: false,
error: `Field ${fieldName} expects type ${fieldType}, got ${typeof value}`
};
}
// Check enum values from OpenAPI
const enumValues = baseFields.enums?.[fieldName];
if (enumValues && !enumValues.includes(value)) {
return {
valid: false,
error: `Field ${fieldName} must be one of: ${enumValues.join(', ')}`
};
}
// Apply orchestration-specific rules
const rules = this.getOrchestrationRules(entityType, context.operation, context.platform);
const fieldRule = rules?.[fieldName];
if (fieldRule) {
// Check conditional
if (fieldRule.conditional && !fieldRule.conditional(context)) {
return { valid: true }; // Rule doesn't apply
}
// Check override
if (fieldRule.override !== undefined && value !== fieldRule.override) {
return {
valid: false,
error: fieldRule.error || `Field ${fieldName} must be "${fieldRule.override}" for ${context.platform} ${entityType}`,
fix: {
value: fieldRule.override,
description: `Set to required value for ${context.platform} platform`
}
};
}
// Check custom validation
if (fieldRule.validation && !fieldRule.validation(value, context)) {
const fix = fieldRule.autoFix ? {
value: fieldRule.autoFix(value, context),
description: `Auto-corrected to valid value`
} : undefined;
return {
valid: false,
error: fieldRule.error || `Field ${fieldName} validation failed`,
fix
};
}
if (fieldRule.customValidation && !fieldRule.customValidation(value, context)) {
return {
valid: false,
error: fieldRule.error || `Field ${fieldName} custom validation failed`
};
}
// Check mode-specific naming
if (fieldRule.modes && context.mode) {
const modeConfig = fieldRule.modes[context.mode];
if (modeConfig && context.actualFieldName !== modeConfig.fieldName) {
return {
valid: false,
error: `In ${context.mode} mode, use field name "${modeConfig.fieldName}" not "${context.actualFieldName}"`
};
}
}
}
return { valid: true };
}
/**
* Get all orchestration rules for an entity
*/
getAllRulesForEntity(entityType, operation = 'create', platform = 'web') {
const platformRules = this.orchestrationRules[platform]?.[entityType]?.[operation] || {};
const sharedRules = this.orchestrationRules.both?.[entityType]?.[operation] || {};
return { ...sharedRules, ...platformRules };
}
/**
* Apply auto-fixes to a template step
*/
applyAutoFixes(step, entityType, platform = 'web', mode = 'template') {
const changes = [];
const rules = this.getAllRulesForEntity(entityType, 'create', platform);
if (!step.template?.inputs)
return { fixed: false, changes };
const inputs = step.template.inputs;
const context = {
platform: platform,
mode: mode,
operation: 'create',
entityType,
step,
template: step.template
};
for (const [fieldName, rule] of Object.entries(rules)) {
if (!rule.autoFix)
continue;
const currentValue = inputs[fieldName];
const shouldApply = !rule.conditional || rule.conditional(context);
if (shouldApply) {
const newValue = rule.autoFix(currentValue, context);
if (newValue !== currentValue) {
inputs[fieldName] = newValue;
changes.push({
field: fieldName,
oldValue: currentValue,
newValue
});
}
}
}
return { fixed: changes.length > 0, changes };
}
matchesType(value, expectedType) {
switch (expectedType) {
case 'string': return typeof value === 'string';
case 'number':
case 'integer': return typeof value === 'number';
case 'boolean': return typeof value === 'boolean';
case 'array': return Array.isArray(value);
case 'object': return typeof value === 'object' && !Array.isArray(value);
case 'any': return true;
default: return true;
}
}
validateTypeMatch(value, varType) {
switch (varType) {
case 'boolean': return typeof value === 'boolean' || value === 'true' || value === 'false';
case 'string': return typeof value === 'string';
case 'integer': return Number.isInteger(Number(value));
case 'double': return !isNaN(Number(value));
case 'json': {
try {
if (typeof value === 'string')
JSON.parse(value);
return true;
}
catch {
return false;
}
}
default: return true;
}
}
getOrchestrationRules(entityType, operation, platform) {
return this.orchestrationRules[platform]?.[entityType]?.[operation] ||
this.orchestrationRules.both?.[entityType]?.[operation] || {};
}
}
//# sourceMappingURL=OrchestrationRulesLayer.js.map