/** * Voxyz Proposal Service * Core service for mission proposal creation, validation, and management * Phase 10: Autonomous Architecture */ import { MissionProposal, MissionProposalInsert, CapGateCheckResult, CapGateViolation, AIAnalysisSummary, ProposalStatus, ProposalServiceConfig } from '@/lib/supabase/voxyz.types'; import { getServiceSupabase } from '@/lib/supabase/client'; import { Task } from '@/lib/data/tasks'; import { Project } from '@/lib/data/projects'; const DEFAULT_CONFIG: ProposalServiceConfig = { maxProposalsPerDay: 3, minProposalsPerDay: 1, defaultProposalExpiryHours: 24, priorityScoreThreshold: 50, autoApproveThreshold: 85, }; /** * Priority scoring factors and their weights */ const PRIORITY_FACTORS = { deadlineUrgency: 0.30, taskPriority: 0.25, projectImportance: 0.20, blockedStatus: 0.15, staleTask: 0.10, }; /** * Calculate priority score for a proposal */ export function calculatePriorityScore( task: Task, project: Project | undefined, context: ProposalContext ): number { let score = 50; // Base score const factors: { factor: string; weight: number; contribution: number }[] = []; // 1. Deadline urgency (30%) const deadlineScore = calculateDeadlineScore(task, context); const deadlineContribution = deadlineScore * PRIORITY_FACTORS.deadlineUrgency; score += deadlineContribution; factors.push({ factor: 'deadline_urgency', weight: PRIORITY_FACTORS.deadlineUrgency, contribution: deadlineContribution }); // 2. Task priority (25%) const priorityScore = calculateTaskPriorityScore(task); const priorityContribution = priorityScore * PRIORITY_FACTORS.taskPriority; score += priorityContribution; factors.push({ factor: 'task_priority', weight: PRIORITY_FACTORS.taskPriority, contribution: priorityContribution }); // 3. Project importance (20%) const projectScore = calculateProjectImportanceScore(project); const projectContribution = projectScore * PRIORITY_FACTORS.projectImportance; score += projectContribution; factors.push({ factor: 'project_importance', weight: PRIORITY_FACTORS.projectImportance, contribution: projectContribution }); // 4. Blocked status (15%) const blockedScore = calculateBlockedScore(task, context); const blockedContribution = blockedScore * PRIORITY_FACTORS.blockedStatus; score += blockedContribution; factors.push({ factor: 'blocked_status', weight: PRIORITY_FACTORS.blockedStatus, contribution: blockedContribution }); // 5. Stale task (10%) const staleScore = calculateStaleScore(task, context); const staleContribution = staleScore * PRIORITY_FACTORS.staleTask; score += staleContribution; factors.push({ factor: 'stale_task', weight: PRIORITY_FACTORS.staleTask, contribution: staleContribution }); return Math.min(100, Math.max(0, Math.round(score))); } /** * Calculate deadline urgency score (0-100) */ function calculateDeadlineScore(task: Task, context: ProposalContext): number { if (!task.dueDate) return 0; const dueDate = new Date(task.dueDate); const now = context.currentTime; const hoursUntilDue = (dueDate.getTime() - now.getTime()) / (1000 * 60 * 60); if (hoursUntilDue < 0) return 100; // Overdue if (hoursUntilDue < 24) return 90; // Due within 24 hours if (hoursUntilDue < 72) return 70; // Due within 3 days if (hoursUntilDue < 168) return 50; // Due within a week if (hoursUntilDue < 336) return 30; // Due within 2 weeks return 10; // Due later } /** * Calculate task priority score (0-100) */ function calculateTaskPriorityScore(task: Task): number { const priorityMap: Record = { 'urgent': 100, 'high': 75, 'medium': 50, 'low': 25, }; return priorityMap[task.priority] || 50; } /** * Calculate project importance score (0-100) */ function calculateProjectImportanceScore(project: Project | undefined): number { if (!project) return 50; // Check for high-priority keywords in project const highPriorityKeywords = ['ios', 'revenue', 'critical', 'milestone']; const projectNameLower = project.name.toLowerCase(); for (const keyword of highPriorityKeywords) { if (projectNameLower.includes(keyword)) { return 80; } } return 50; } /** * Calculate blocked status score (0-100) */ function calculateBlockedScore(task: Task, context: ProposalContext): number { if (task.status === 'blocked') { // Check how long it's been blocked const blockedHours = task.comments?.reduce((hours, comment) => { const item = comment as { text?: unknown; createdAt?: unknown }; if (typeof item.text === 'string' && item.text.toLowerCase().includes('blocked') && typeof item.createdAt === 'string') { const commentTime = new Date(item.createdAt); const blockedHours = (context.currentTime.getTime() - commentTime.getTime()) / (1000 * 60 * 60); return Math.max(hours, blockedHours); } return hours; }, 0) || 0; if (blockedHours > 48) return 100; if (blockedHours > 24) return 80; return 60; } return 0; } /** * Calculate stale task score (0-100) */ function calculateStaleScore(task: Task, context: ProposalContext): number { const lastUpdate = new Date(task.updatedAt); const daysSinceUpdate = (context.currentTime.getTime() - lastUpdate.getTime()) / (1000 * 60 * 60 * 24); if (task.status === 'in-progress' || task.status === 'todo') { if (daysSinceUpdate > 14) return 100; if (daysSinceUpdate > 7) return 75; if (daysSinceUpdate > 3) return 50; } return 0; } /** * Generate AI analysis summary for a proposal */ export function generateAIAnalysis( task: Task, project: Project | undefined, priorityScore: number, factors: { factor: string; weight: number; contribution: number }[] ): AIAnalysisSummary { const urgencyScore = calculateDeadlineScore(task, { currentTime: new Date() }); const impactScore = Math.round((priorityScore + urgencyScore) / 2); // Determine effort estimate based on task complexity const effortEstimate = estimateEffort(task); // Generate reasoning const reasoning = generateReasoning(task, project, factors); // Identify risks const risks = identifyRisks(task, project); // Identify opportunities const opportunities = identifyOpportunities(task, project); return { priority_factors: factors, urgency_score: urgencyScore, impact_score: impactScore, effort_estimate: effortEstimate, confidence: calculateConfidence(task, factors), reasoning, risks, opportunities, }; } /** * Estimate effort required for a task */ function estimateEffort(task: Task): 'low' | 'medium' | 'high' { const description = (task.description || '').toLowerCase(); const title = task.title.toLowerCase(); // High effort indicators const highEffortKeywords = ['refactor', 'architecture', 'redesign', 'migration', 'integrate', 'implement']; for (const keyword of highEffortKeywords) { if (description.includes(keyword) || title.includes(keyword)) { return 'high'; } } // Medium effort indicators const mediumEffortKeywords = ['update', 'modify', 'enhance', 'improve', 'fix', 'add']; for (const keyword of mediumEffortKeywords) { if (description.includes(keyword) || title.includes(keyword)) { return 'medium'; } } return 'low'; } /** * Generate reasoning for the proposal */ function generateReasoning( task: Task, project: Project | undefined, factors: { factor: string; weight: number; contribution: number }[] ): string { const parts: string[] = []; // Sort factors by contribution const sortedFactors = [...factors].sort((a, b) => b.contribution - a.contribution); // Add top factor const topFactor = sortedFactors[0]; if (topFactor) { parts.push(`Primary driver: ${topFactor.factor.replace('_', ' ')} (+${Math.round(topFactor.contribution)} points)`); } // Add deadline context if (task.dueDate) { const dueDate = new Date(task.dueDate); const daysUntil = Math.ceil((dueDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)); if (daysUntil <= 3) { parts.push(`Urgent: Due in ${daysUntil} day${daysUntil !== 1 ? 's' : ''}`); } } // Add project context if (project) { parts.push(`Part of ${project.name}`); } return parts.join('. '); } /** * Identify risks for the task */ function identifyRisks(task: Task, project: Project | undefined): string[] { const risks: string[] = []; if (task.status === 'blocked') { risks.push('Task is currently blocked'); } if (task.dueDate) { const dueDate = new Date(task.dueDate); const daysUntil = Math.ceil((dueDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)); if (daysUntil < 0) { risks.push('Task is overdue'); } else if (daysUntil <= 2) { risks.push('Very tight deadline'); } } if (task.priority === 'urgent') { risks.push('High priority task may have dependencies'); } return risks; } /** * Identify opportunities for the task */ function identifyOpportunities(task: Task, project: Project | undefined): string[] { const opportunities: string[] = []; if (project?.name.toLowerCase().includes('ios')) { opportunities.push('Contributes to iOS portfolio growth'); } if (task.tags.some(tag => tag.toLowerCase().includes('revenue'))) { opportunities.push('May impact revenue directly'); } if (task.tags.some(tag => tag.toLowerCase().includes('milestone'))) { opportunities.push('Advances key milestone'); } return opportunities; } /** * Calculate confidence in the proposal */ function calculateConfidence(task: Task, factors: { factor: string; weight: number; contribution: number }[]): number { let confidence = 70; // Base confidence // Boost confidence if we have clear signals const hasDeadline = !!task.dueDate; const hasClearPriority = task.priority === 'urgent' || task.priority === 'high'; const hasDescription = (task.description || '').length > 50; if (hasDeadline) confidence += 10; if (hasClearPriority) confidence += 10; if (hasDescription) confidence += 5; // Reduce confidence if factors are contradictory const factorValues = factors.map(f => f.contribution); const variance = calculateVariance(factorValues); if (variance > 400) confidence -= 10; // High variance = conflicting signals return Math.min(95, Math.max(50, confidence)); } /** * Calculate variance of an array */ function calculateVariance(values: number[]): number { if (values.length === 0) return 0; const mean = values.reduce((a, b) => a + b, 0) / values.length; const squaredDiffs = values.map(v => Math.pow(v - mean, 2)); return squaredDiffs.reduce((a, b) => a + b, 0) / values.length; } /** * Create a mission proposal from a task */ export function createProposalFromTask( task: Task, project: Project | undefined, context: ProposalContext, config: ProposalServiceConfig = DEFAULT_CONFIG ): MissionProposalInsert { const priorityScore = calculatePriorityScore(task, project, context); const factors = getPriorityFactors(task, project, context); const aiAnalysis = generateAIAnalysis(task, project, priorityScore, factors); // Estimate duration based on effort const durationMap: Record = { 'low': 60, 'medium': 180, 'high': 360, }; const estimatedDuration = durationMap[aiAnalysis.effort_estimate || 'medium'] || 120; // Calculate suggested time slot const suggestedStartTime = calculateSuggestedStartTime(context, estimatedDuration); const suggestedEndTime = suggestedStartTime ? new Date(suggestedStartTime.getTime() + estimatedDuration * 60000) : null; // Generate rationale const rationale = aiAnalysis.reasoning || `High priority task: ${task.title}`; return { proposal_date: context.currentTime.toISOString().split('T')[0], status: 'pending', priority_score: priorityScore, passes_cap_gates: false, // Will be validated later cap_gate_violations: [], source_task_ids: [task.id], source_project_ids: project ? [project.id] : [], title: task.title, description: task.description ?? null, rationale, ai_analysis: aiAnalysis, suggested_start_time: suggestedStartTime?.toISOString() || null, suggested_end_time: suggestedEndTime?.toISOString() || null, estimated_duration_minutes: estimatedDuration, approved_at: null, approved_by: null, rejected_at: null, rejection_reason: null, calendar_event_id: null, calendar_synced_at: null, executed_at: null, execution_notes: null, expires_at: new Date(context.currentTime.getTime() + config.defaultProposalExpiryHours * 60 * 60 * 1000).toISOString(), }; } /** * Get priority factors for a task */ function getPriorityFactors( task: Task, project: Project | undefined, context: ProposalContext ): { factor: string; weight: number; contribution: number }[] { const factors: { factor: string; weight: number; contribution: number }[] = []; const deadlineScore = calculateDeadlineScore(task, context); factors.push({ factor: 'deadline_urgency', weight: PRIORITY_FACTORS.deadlineUrgency, contribution: deadlineScore * PRIORITY_FACTORS.deadlineUrgency }); const priorityScore = calculateTaskPriorityScore(task); factors.push({ factor: 'task_priority', weight: PRIORITY_FACTORS.taskPriority, contribution: priorityScore * PRIORITY_FACTORS.taskPriority }); const projectScore = calculateProjectImportanceScore(project); factors.push({ factor: 'project_importance', weight: PRIORITY_FACTORS.projectImportance, contribution: projectScore * PRIORITY_FACTORS.projectImportance }); const blockedScore = calculateBlockedScore(task, context); factors.push({ factor: 'blocked_status', weight: PRIORITY_FACTORS.blockedStatus, contribution: blockedScore * PRIORITY_FACTORS.blockedStatus }); const staleScore = calculateStaleScore(task, context); factors.push({ factor: 'stale_task', weight: PRIORITY_FACTORS.staleTask, contribution: staleScore * PRIORITY_FACTORS.staleTask }); return factors; } /** * Calculate suggested start time for a proposal */ function calculateSuggestedStartTime(context: ProposalContext, durationMinutes: number): Date | null { // Start with tomorrow at 9 AM const tomorrow = new Date(context.currentTime); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(9, 0, 0, 0); // TODO: Consider calendar availability from context return tomorrow; } /** * Proposal context for calculations */ export interface ProposalContext { currentTime: Date; activeTasks?: Task[]; recentProposals?: MissionProposal[]; calendarAvailability?: unknown[]; } /** * Validate a proposal against cap gates */ export async function validateProposalAgainstCapGates( proposal: MissionProposalInsert, existingProposals: MissionProposal[], config: ProposalServiceConfig = DEFAULT_CONFIG ): Promise { const violations: CapGateViolation[] = []; const checks: CapGateCheckResult['checks'] = []; // Check 1: Daily mission limit const todayProposals = existingProposals.filter(p => p.proposal_date === proposal.proposal_date && p.status !== 'rejected' && p.status !== 'expired' ); const dailyLimitGate = { gate: { id: 'daily_limit', name: 'Daily Mission Limit', description: 'Maximum number of missions per day', gate_type: 'daily_mission_limit' as const, max_value: config.maxProposalsPerDay, min_value: config.minProposalsPerDay, unit: 'count', current_value: todayProposals.length, last_evaluated_at: new Date().toISOString(), policy_id: null, is_active: true, is_blocking: false, evaluation_window_hours: 24, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), created_by: null, updated_by: null, }, passed: todayProposals.length < config.maxProposalsPerDay, currentValue: todayProposals.length, }; if (!dailyLimitGate.passed) { dailyLimitGate.gate.is_blocking = true; violations.push({ gate_name: dailyLimitGate.gate.name, gate_type: dailyLimitGate.gate.gate_type, current_value: dailyLimitGate.currentValue, max_value: config.maxProposalsPerDay, violation_message: `Daily mission limit of ${config.maxProposalsPerDay} reached`, }); } checks.push(dailyLimitGate); // Check 2: Workload capacity const totalWorkload = todayProposals.reduce((sum, p) => sum + (p.estimated_duration_minutes || 120), 0 ); const proposedWorkload = totalWorkload + (proposal.estimated_duration_minutes || 120); const maxWorkloadMinutes = 480; // 8 hours const workloadGate = { gate: { id: 'workload_capacity', name: 'Workload Capacity', description: 'Daily workload capacity in minutes', gate_type: 'workload_capacity' as const, max_value: maxWorkloadMinutes, min_value: 0, unit: 'minutes', current_value: proposedWorkload, last_evaluated_at: new Date().toISOString(), policy_id: null, is_active: true, is_blocking: false, evaluation_window_hours: 24, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), created_by: null, updated_by: null, }, passed: proposedWorkload <= maxWorkloadMinutes, currentValue: proposedWorkload, }; if (!workloadGate.passed) { workloadGate.gate.is_blocking = true; violations.push({ gate_name: workloadGate.gate.name, gate_type: workloadGate.gate.gate_type, current_value: proposedWorkload, max_value: maxWorkloadMinutes, violation_message: `Workload would exceed ${maxWorkloadMinutes} minutes (${Math.round(proposedWorkload)} proposed)`, }); } checks.push(workloadGate); // Check 3: Priority threshold if (proposal.priority_score < config.priorityScoreThreshold) { violations.push({ gate_name: 'Priority Threshold', gate_type: 'prioritization', current_value: proposal.priority_score, max_value: config.priorityScoreThreshold, violation_message: `Priority score ${proposal.priority_score} below threshold ${config.priorityScoreThreshold}`, }); } return { allPassed: violations.length === 0, checks, violations, summary: violations.length === 0 ? 'All cap gates passed' : `${violations.length} cap gate violation${violations.length !== 1 ? 's' : ''}`, }; } /** * Save a proposal to the database */ export async function saveProposal( proposal: MissionProposalInsert, capGateResult: CapGateCheckResult ): Promise { const supabase = getServiceSupabase(); const proposalWithCapGates: MissionProposalInsert = { ...proposal, passes_cap_gates: capGateResult.allPassed, cap_gate_violations: capGateResult.violations, }; const { data, error } = await supabase .from('ops_mission_proposals') .insert(proposalWithCapGates) .select() .single(); if (error) { console.error('Error saving proposal:', error); return null; } return data as MissionProposal; } /** * Get proposals for a specific date */ export async function getProposalsForDate(date: Date): Promise { const supabase = getServiceSupabase(); const dateString = date.toISOString().split('T')[0]; const { data, error } = await supabase .from('ops_mission_proposals') .select('*') .eq('proposal_date', dateString) .order('priority_score', { ascending: false }); if (error) { console.error('Error fetching proposals:', error); return []; } return (data || []) as MissionProposal[]; } /** * Approve a proposal */ export async function approveProposal( proposalId: string, userId: string ): Promise { const supabase = getServiceSupabase(); const { error } = await supabase .from('ops_mission_proposals') .update({ status: 'approved', approved_at: new Date().toISOString(), approved_by: userId, }) .eq('id', proposalId); if (error) { console.error('Error approving proposal:', error); return false; } return true; } /** * Reject a proposal */ export async function rejectProposal( proposalId: string, reason: string ): Promise { const supabase = getServiceSupabase(); const { error } = await supabase .from('ops_mission_proposals') .update({ status: 'rejected', rejected_at: new Date().toISOString(), rejection_reason: reason, }) .eq('id', proposalId); if (error) { console.error('Error rejecting proposal:', error); return false; } return true; } /** * Mark proposal as completed */ export async function completeProposal( proposalId: string, notes?: string ): Promise { const supabase = getServiceSupabase(); const { error } = await supabase .from('ops_mission_proposals') .update({ status: 'completed', executed_at: new Date().toISOString(), execution_notes: notes || null, }) .eq('id', proposalId); if (error) { console.error('Error completing proposal:', error); return false; } return true; } /** * Get top proposals for today */ export async function getTopProposalsForToday(limit: number = 3): Promise { const today = new Date(); const proposals = await getProposalsForDate(today); // Filter to pending and approved proposals const activeProposals = proposals.filter(p => p.status === 'pending' || p.status === 'approved' ); // Sort by priority score activeProposals.sort((a, b) => b.priority_score - a.priority_score); return activeProposals.slice(0, limit); } /** * Expire old proposals */ export async function expireOldProposals(): Promise { const supabase = getServiceSupabase(); const now = new Date().toISOString(); const { data, error } = await supabase .from('ops_mission_proposals') .update({ status: 'expired' }) .lt('expires_at', now) .eq('status', 'pending') .select('id'); if (error) { console.error('Error expiring proposals:', error); return 0; } return data?.length || 0; }