mission-control/lib/services/cap-gates.service.ts

578 lines
16 KiB
TypeScript

/**
* 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<CapGate[]> {
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<CapGate | null> {
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<boolean> {
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<CapGateCheckResult> {
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<CapGateCheck> {
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<CapGateCheckResult> {
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<boolean> {
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<CapGate, 'id' | 'created_at' | 'updated_at'>[] = [
{
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<boolean> {
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;
}