578 lines
16 KiB
TypeScript
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;
|
|
}
|