Files
gh-duongdev-ccpm/commands/_feature-flag-evaluator.md
2025-11-29 18:24:24 +08:00

7.7 KiB

Feature Flag Evaluator

Evaluates feature flags for Phase 6 rollout with deterministic rollout control and variant assignment.

Purpose

This shared utility handles:

  • Feature flag configuration loading
  • Deterministic rollout control (consistent user assignment)
  • Variant assignment for A/B testing
  • Stage transitions (beta → early access → GA)
  • User preference overrides

Algorithm

function evaluateFlag(userId, flagName, config) {
  // Step 1: Check if flag is enabled globally
  const flagDef = config.flags[flagName];
  if (!flagDef || !flagDef.enabled) {
    return { enabled: false, variant: 'disabled', reason: 'flag_disabled' };
  }

  // Step 2: Check minimum version requirement
  const currentVersion = getPluginVersion(); // e.g., "2.3.0-beta.1"
  if (!meetsMinVersion(currentVersion, flagDef.min_version)) {
    return {
      enabled: false,
      variant: 'old_version',
      reason: 'version_requirement_not_met',
      required_version: flagDef.min_version,
      current_version: currentVersion
    };
  }

  // Step 3: Check user overrides (highest priority)
  const userConfig = getUserConfig();
  if (flagDef.user_override && userConfig.feature_flags?.[flagName]?.override !== null) {
    const override = userConfig.feature_flags[flagName].override;
    return {
      enabled: override,
      variant: override ? 'user_enabled' : 'user_disabled',
      reason: 'user_override'
    };
  }

  // Step 4: Deterministic rollout assignment
  // Use hash(userId + flagName + salt) % 100 for consistent assignment
  const hash = deterministicHash(userId, flagName);
  const rolloutPercentage = flagDef.rollout_percentage || 0;

  if (hash >= rolloutPercentage) {
    return {
      enabled: false,
      variant: 'not_in_rollout',
      reason: 'rollout_percentage_not_reached',
      rollout_percentage: rolloutPercentage,
      user_hash: hash
    };
  }

  // Step 5: Assign variant based on rollout
  const variant = assignVariant(flagDef.variants, hash);
  return {
    enabled: true,
    variant: variant,
    reason: 'flag_enabled',
    rollout_percentage: rolloutPercentage
  };
}

function assignVariant(variants, hash) {
  // Assign control or treatment variant
  if (!variants) return 'default';

  let cumulativePercentage = 0;
  for (const [variantName, variantDef] of Object.entries(variants)) {
    const variantPercentage = variantDef.percentage || 50;
    if (hash < cumulativePercentage + variantPercentage) {
      return variantName;
    }
    cumulativePercentage += variantPercentage;
  }

  return 'default';
}

function deterministicHash(userId, flagName) {
  // Simple deterministic hash: consistent across calls
  const combined = `${userId}:${flagName}`;
  let hash = 0;
  for (let i = 0; i < combined.length; i++) {
    const char = combined.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;
    hash = hash & hash; // Convert to 32bit integer
  }
  return Math.abs(hash % 100);
}

function meetsMinVersion(current, required) {
  // Semantic version comparison
  const currentParts = current.split(/[.-]/);
  const requiredParts = required.split(/[.-]/);

  for (let i = 0; i < Math.max(currentParts.length, requiredParts.length); i++) {
    const curr = parseInt(currentParts[i]) || 0;
    const req = parseInt(requiredParts[i]) || 0;

    if (curr > req) return true;
    if (curr < req) return false;
  }
  return true;
}

Feature Flag Caching

Feature flags are loaded and cached at startup with 60-second refresh interval:

class FeatureFlagCache {
  constructor() {
    this.cache = new Map();
    this.lastRefresh = 0;
    this.refreshInterval = 60000; // 60 seconds
  }

  async getFlag(userId, flagName) {
    // Return cached result if fresh
    const cacheKey = `${userId}:${flagName}`;
    if (this.cache.has(cacheKey)) {
      const cached = this.cache.get(cacheKey);
      if (Date.now() - cached.timestamp < this.refreshInterval) {
        return cached.value;
      }
    }

    // Load and cache flag configuration
    const config = await loadFeatureFlagsConfig();
    const result = evaluateFlag(userId, flagName, config);

    this.cache.set(cacheKey, {
      value: result,
      timestamp: Date.now()
    });

    return result;
  }

  invalidate(userId, flagName) {
    const cacheKey = `${userId}:${flagName}`;
    this.cache.delete(cacheKey);
  }

  clear() {
    this.cache.clear();
  }
}

User Configuration

Users can override feature flags in ~/.claude/ccpm-config.json:

{
  "feature_flags": {
    "optimized_workflow_commands": {
      "override": true,
      "enabled": true
    },
    "linear_subagent_enabled": {
      "override": false,
      "enabled": false
    }
  }
}

Stage Transitions

Feature flags automatically transition through stages based on configuration:

Beta (Dec 9) → 10% rollout
   ↓
Early Access (Dec 21) → 30% rollout
   ↓
General Availability (Jan 6) → 100% rollout

Stages are managed automatically by checking current date against rollout_schedule in feature-flags.json.

Integration with Commands

All commands should check feature flags before execution:

Task(feature-flag-evaluator): `
userId: {{currentUserId}}
flag: "optimized_workflow_commands"
`

If result.enabled is true:
  - Use optimized implementation
Else:
  - Use legacy implementation
  - Show migration hint after completion

Monitoring

Feature flag evaluator tracks:

  • Number of users in each rollout stage
  • Variant distribution
  • Override usage
  • Cache hit rates
  • Evaluation latency

Metrics are stored in .ccpm/metrics.json and used for dashboards.

Admin Commands

Admins can manage feature flags via commands:

# View current feature flag status
/ccpm:admin:flags --show

# Manually set rollout percentage
/ccpm:admin:flags --set optimized_workflow_commands --percentage 50

# Override flag for specific user
/ccpm:admin:flags --set optimized_workflow_commands --user-override true

# Trigger automatic rollback
/ccpm:admin:flags --rollback optimized_workflow_commands

# View metrics
/ccpm:admin:flags --metrics

Testing

Feature flags can be tested in isolated environments:

# Test with specific rollout percentage
CCPM_FEATURE_FLAG_OVERRIDE=optimized_workflow_commands:50 claude --code

# Test with specific variant
CCPM_FEATURE_FLAG_VARIANT=optimized_workflow_commands:treatment claude --code

# Disable all new features
CCPM_FEATURE_FLAGS_DISABLED=true claude --code

Rollback Procedures

Automatic Rollback (Monitoring)

When critical error threshold reached (5%+ error rate):

  1. Monitoring detects error rate spike
  2. Automatically disables flag via feature-flags.json update
  3. Routes traffic to control variant
  4. Sends alert to on-call engineer
  5. No user action required (automatic recovery)

Manual Rollback (User-Initiated)

User can disable feature flag:

/ccpm:config feature-flags optimized_workflow_commands false

Emergency Rollback (Team-Initiated)

Team can trigger full rollback:

/ccpm:admin:flags --emergency-rollback optimized_workflow_commands

This immediately sets rollout_percentage to 0 and notifies all affected users.

Success Metrics

Feature flag evaluator success is measured by:

  • Consistent user assignment (same user gets same variant every time)
  • Accurate rollout percentages (±2% deviation allowed)
  • Sub-100ms evaluation latency
  • >90% cache hit rate
  • Zero cache invalidation bugs
  • Deterministic, reproducible results

References

  • .ccpm/feature-flags.json - Feature flag configuration
  • ~/.claude/ccpm-config.json - User overrides
  • .ccpm/metrics.json - Evaluation metrics
  • docs/guides/feature-flag-configuration.md - User guide
  • docs/architecture/feature-flag-system.md - Design document