Initial commit
This commit is contained in:
305
commands/_feature-flag-evaluator.md
Normal file
305
commands/_feature-flag-evaluator.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# 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
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```javascript
|
||||
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`:
|
||||
|
||||
```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:
|
||||
|
||||
```markdown
|
||||
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:
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
/ccpm:config feature-flags optimized_workflow_commands false
|
||||
```
|
||||
|
||||
### Emergency Rollback (Team-Initiated)
|
||||
|
||||
Team can trigger full rollback:
|
||||
|
||||
```bash
|
||||
/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
|
||||
Reference in New Issue
Block a user