/** * Voxyz Cap Gates Service * Queue management and capacity control system * Phase 10: Autonomous Architecture */ import { CapGate, CapGateCheck, CapGateCheckResult, CapGateViolation, CapGateConfig, MissionProposal, DailyMission } from '@/lib/supabase/voxyz.types'; import { createClient } from '@/lib/supabase/client'; const DEFAULT_CONFIG: CapGateConfig = { dailyMissionLimit: 3, concurrentHighPriorityLimit: 2, maxDailyWorkloadMinutes: 480, minFocusTimeMinutes: 120, minCalendarBufferMinutes: 15, reactionRatio: 0.30, }; /** * Get all active cap gates from the database */ export async function getActiveCapGates(): Promise { const supabase = createClient(); const { data, error } = await supabase .from('ops_cap_gates') .select('*') .eq('is_active', true) .order('gate_type', { ascending: true }); if (error) { console.error('Error fetching cap gates:', error); return []; } return data || []; } /** * Get a specific cap gate by type */ export async function getCapGateByType(gateType: CapGate['gate_type']): Promise { const supabase = createClient(); const { data, error } = await supabase .from('ops_cap_gates') .select('*') .eq('gate_type', gateType) .eq('is_active', true) .single(); if (error) { console.error('Error fetching cap gate:', error); return null; } return data; } /** * Update cap gate current value */ export async function updateCapGateValue( gateId: string, currentValue: number ): Promise { const supabase = createClient(); const { error } = await supabase .from('ops_cap_gates') .update({ current_value: currentValue, last_evaluated_at: new Date().toISOString(), updated_at: new Date().toISOString(), }) .eq('id', gateId); if (error) { console.error('Error updating cap gate:', error); return false; } return true; } /** * Check all cap gates for a potential new proposal */ export async function checkCapGates( proposedMissionMinutes: number, existingProposals: MissionProposal[], config: CapGateConfig = DEFAULT_CONFIG ): Promise { const gates = await getActiveCapGates(); const checks: CapGateCheck[] = []; const violations: CapGateViolation[] = []; for (const gate of gates) { const check = await evaluateCapGate(gate, proposedMissionMinutes, existingProposals, config); checks.push(check); if (!check.passed && check.violation) { violations.push(check.violation); } } // Always check these essential gates even if not in DB const essentialChecks = await checkEssentialCapGates( proposedMissionMinutes, existingProposals, config ); checks.push(...essentialChecks.checks); violations.push(...essentialChecks.violations); return { allPassed: violations.length === 0, checks, violations, summary: violations.length === 0 ? 'All cap gates passed' : `${violations.length} cap gate violation${violations.length !== 1 ? 's' : ''}`, }; } /** * Evaluate a single cap gate */ async function evaluateCapGate( gate: CapGate, proposedMissionMinutes: number, existingProposals: MissionProposal[], config: CapGateConfig ): Promise { let currentValue = gate.current_value; let passed = true; let violation: CapGateViolation | undefined; switch (gate.gate_type) { case 'daily_mission_limit': currentValue = existingProposals.filter(p => p.status !== 'rejected' && p.status !== 'expired' ).length; passed = currentValue < (gate.max_value || config.dailyMissionLimit); if (!passed) { violation = { gate_name: gate.name, gate_type: gate.gate_type, current_value: currentValue, max_value: gate.max_value, violation_message: `Daily mission limit of ${gate.max_value} reached (${currentValue} existing)`, }; } break; case 'workload_capacity': const totalMinutes = existingProposals.reduce((sum, p) => sum + (p.estimated_duration_minutes || 120), 0 ); currentValue = totalMinutes + proposedMissionMinutes; passed = currentValue <= (gate.max_value || config.maxDailyWorkloadMinutes); if (!passed) { violation = { gate_name: gate.name, gate_type: gate.gate_type, current_value: currentValue, max_value: gate.max_value, violation_message: `Workload would exceed ${gate.max_value} minutes (${currentValue} proposed)`, }; } break; case 'concurrent_high_priority': const highPriorityCount = existingProposals.filter(p => p.priority_score >= 75 && p.status !== 'rejected' ).length; currentValue = highPriorityCount; passed = currentValue < (gate.max_value || config.concurrentHighPriorityLimit); if (!passed) { violation = { gate_name: gate.name, gate_type: gate.gate_type, current_value: currentValue, max_value: gate.max_value, violation_message: `Too many concurrent high-priority missions (${currentValue} of ${gate.max_value} max)`, }; } break; case 'focus_time_minimum': // Calculate if we have enough focus time allocated const totalAllocatedMinutes = existingProposals.reduce((sum, p) => sum + (p.estimated_duration_minutes || 120), 0 ); currentValue = totalAllocatedMinutes; // This is a minimum gate, so we check if we still have capacity passed = true; // Focus time is a soft constraint break; case 'reaction_ratio': // Track reaction time allocation (30% of workday) const workdayMinutes = 480; // 8 hours const reactionMinutes = Math.round(workdayMinutes * (gate.max_value / 100 || config.reactionRatio)); const plannedMinutes = existingProposals.reduce((sum, p) => sum + (p.estimated_duration_minutes || 120), 0 ); currentValue = Math.round((plannedMinutes / workdayMinutes) * 100); // Reaction ratio is tracked but not blocking passed = true; break; default: passed = true; } // Update gate value in database await updateCapGateValue(gate.id, currentValue); return { gate, passed, currentValue, violation, }; } /** * Check essential cap gates that should always be evaluated */ async function checkEssentialCapGates( proposedMissionMinutes: number, existingProposals: MissionProposal[], config: CapGateConfig ): Promise { const checks: CapGateCheck[] = []; const violations: CapGateViolation[] = []; const now = new Date(); // Gate 1: Daily mission limit const activeProposals = existingProposals.filter(p => p.status !== 'rejected' && p.status !== 'expired' ); const dailyLimitCheck: CapGateCheck = { gate: { id: 'essential-daily-limit', name: 'Daily Mission Limit', description: 'Maximum missions per day', gate_type: 'daily_mission_limit', max_value: config.dailyMissionLimit, min_value: 0, unit: 'count', current_value: activeProposals.length, last_evaluated_at: now.toISOString(), policy_id: null, is_active: true, is_blocking: false, evaluation_window_hours: 24, created_at: now.toISOString(), updated_at: now.toISOString(), created_by: null, updated_by: null, }, passed: activeProposals.length < config.dailyMissionLimit, currentValue: activeProposals.length, }; if (!dailyLimitCheck.passed) { dailyLimitCheck.gate.is_blocking = true; violations.push({ gate_name: dailyLimitCheck.gate.name, gate_type: dailyLimitCheck.gate.gate_type, current_value: dailyLimitCheck.currentValue, max_value: config.dailyMissionLimit, violation_message: `Daily mission limit of ${config.dailyMissionLimit} reached`, }); } checks.push(dailyLimitCheck); // Gate 2: Workload capacity const totalWorkload = activeProposals.reduce((sum, p) => sum + (p.estimated_duration_minutes || 120), 0 ); const proposedWorkload = totalWorkload + proposedMissionMinutes; const workloadCheck: CapGateCheck = { gate: { id: 'essential-workload', name: 'Workload Capacity', description: 'Daily workload capacity', gate_type: 'workload_capacity', max_value: config.maxDailyWorkloadMinutes, min_value: 0, unit: 'minutes', current_value: proposedWorkload, last_evaluated_at: now.toISOString(), policy_id: null, is_active: true, is_blocking: false, evaluation_window_hours: 24, created_at: now.toISOString(), updated_at: now.toISOString(), created_by: null, updated_by: null, }, passed: proposedWorkload <= config.maxDailyWorkloadMinutes, currentValue: proposedWorkload, }; if (!workloadCheck.passed) { workloadCheck.gate.is_blocking = true; violations.push({ gate_name: workloadCheck.gate.name, gate_type: workloadCheck.gate.gate_type, current_value: proposedWorkload, max_value: config.maxDailyWorkloadMinutes, violation_message: `Workload would exceed ${config.maxDailyWorkloadMinutes} minutes (${proposedWorkload} proposed)`, }); } checks.push(workloadCheck); // Gate 3: Concurrent high priority const highPriorityCount = activeProposals.filter(p => p.priority_score >= 75).length; const highPriorityCheck: CapGateCheck = { gate: { id: 'essential-high-priority', name: 'Concurrent High Priority', description: 'Limit on concurrent high-priority work', gate_type: 'concurrent_high_priority', max_value: config.concurrentHighPriorityLimit, min_value: 0, unit: 'count', current_value: highPriorityCount, last_evaluated_at: now.toISOString(), policy_id: null, is_active: true, is_blocking: false, evaluation_window_hours: 24, created_at: now.toISOString(), updated_at: now.toISOString(), created_by: null, updated_by: null, }, passed: highPriorityCount < config.concurrentHighPriorityLimit, currentValue: highPriorityCount, }; if (!highPriorityCheck.passed) { highPriorityCheck.gate.is_blocking = true; violations.push({ gate_name: highPriorityCheck.gate.name, gate_type: highPriorityCheck.gate.gate_type, current_value: highPriorityCount, max_value: config.concurrentHighPriorityLimit, violation_message: `Too many concurrent high-priority missions`, }); } checks.push(highPriorityCheck); return { allPassed: violations.length === 0, checks, violations, summary: violations.length === 0 ? 'All essential cap gates passed' : `${violations.length} essential cap gate violation${violations.length !== 1 ? 's' : ''}`, }; } /** * Get current capacity status */ export async function getCapacityStatus(config: CapGateConfig = DEFAULT_CONFIG): Promise<{ dailyMissionsUsed: number; dailyMissionsRemaining: number; workloadUsedMinutes: number; workloadRemainingMinutes: number; highPriorityUsed: number; highPriorityRemaining: number; focusTimeAllocatedMinutes: number; reactionTimeReservedMinutes: number; }> { const supabase = createClient(); const today = new Date().toISOString().split('T')[0]; // Get today's proposals const { data: proposals } = await supabase .from('ops_mission_proposals') .select('*') .eq('proposal_date', today) .neq('status', 'rejected') .neq('status', 'expired'); const activeProposals = proposals || []; const dailyMissionsUsed = activeProposals.length; const workloadUsedMinutes = activeProposals.reduce((sum, p) => sum + (p.estimated_duration_minutes || 120), 0 ); const highPriorityUsed = activeProposals.filter(p => p.priority_score >= 75).length; // Calculate focus time (non-reaction time) const workdayMinutes = 480; const reactionMinutes = Math.round(workdayMinutes * config.reactionRatio); const focusTimeAllocatedMinutes = Math.min(workloadUsedMinutes, workdayMinutes - reactionMinutes); return { dailyMissionsUsed, dailyMissionsRemaining: Math.max(0, config.dailyMissionLimit - dailyMissionsUsed), workloadUsedMinutes, workloadRemainingMinutes: Math.max(0, config.maxDailyWorkloadMinutes - workloadUsedMinutes), highPriorityUsed, highPriorityRemaining: Math.max(0, config.concurrentHighPriorityLimit - highPriorityUsed), focusTimeAllocatedMinutes, reactionTimeReservedMinutes: reactionMinutes, }; } /** * Check if a new proposal can be added */ export async function canAddProposal( estimatedMinutes: number = 120 ): Promise<{ allowed: boolean; reason?: string }> { const status = await getCapacityStatus(); if (status.dailyMissionsRemaining <= 0) { return { allowed: false, reason: 'Daily mission limit reached' }; } if (status.workloadRemainingMinutes < estimatedMinutes) { return { allowed: false, reason: 'Insufficient workload capacity' }; } return { allowed: true }; } /** * Create default cap gates in database */ export async function initializeCapGates(config: CapGateConfig = DEFAULT_CONFIG): Promise { const supabase = createClient(); // Check if cap gates already exist const { count } = await supabase .from('ops_cap_gates') .select('*', { count: 'exact', head: true }); if (count && count > 0) { return true; // Already initialized } // Create default cap gates const defaultGates: Omit[] = [ { name: 'Daily Mission Limit', description: 'Maximum number of missions per day', gate_type: 'daily_mission_limit', max_value: config.dailyMissionLimit, min_value: 1, unit: 'count', current_value: 0, last_evaluated_at: null, policy_id: null, is_active: true, is_blocking: true, evaluation_window_hours: 24, created_by: null, updated_by: null, }, { name: 'Workload Capacity', description: 'Maximum daily workload in minutes', gate_type: 'workload_capacity', max_value: config.maxDailyWorkloadMinutes, min_value: 60, unit: 'minutes', current_value: 0, last_evaluated_at: null, policy_id: null, is_active: true, is_blocking: true, evaluation_window_hours: 24, created_by: null, updated_by: null, }, { name: 'Concurrent High Priority', description: 'Maximum concurrent high-priority missions', gate_type: 'concurrent_high_priority', max_value: config.concurrentHighPriorityLimit, min_value: 0, unit: 'count', current_value: 0, last_evaluated_at: null, policy_id: null, is_active: true, is_blocking: true, evaluation_window_hours: 24, created_by: null, updated_by: null, }, { name: 'Focus Time Minimum', description: 'Minimum focus time required per day', gate_type: 'focus_time_minimum', max_value: 480, min_value: config.minFocusTimeMinutes, unit: 'minutes', current_value: 0, last_evaluated_at: null, policy_id: null, is_active: true, is_blocking: false, evaluation_window_hours: 24, created_by: null, updated_by: null, }, { name: 'Reaction Time Reserve', description: 'Time reserved for spontaneous reactions', gate_type: 'reaction_ratio', max_value: Math.round(config.reactionRatio * 100), min_value: 10, unit: 'percentage', current_value: 0, last_evaluated_at: null, policy_id: null, is_active: true, is_blocking: false, evaluation_window_hours: 24, created_by: null, updated_by: null, }, ]; const { error } = await supabase .from('ops_cap_gates') .insert(defaultGates); if (error) { console.error('Error initializing cap gates:', error); return false; } return true; } /** * Reset cap gate values for a new day */ export async function resetCapGatesForNewDay(): Promise { const supabase = createClient(); const { error } = await supabase .from('ops_cap_gates') .update({ current_value: 0, last_evaluated_at: new Date().toISOString(), is_blocking: false, }) .eq('is_active', true); if (error) { console.error('Error resetting cap gates:', error); return false; } return true; }