import type { UserState } from "@/generated/prisma/client"; // --------------------------------------------------------------------------- // T3: Stage advancement rules // --------------------------------------------------------------------------- /** * Practice stages: * 0 — Beginner (just started, < 5 sessions) * 1 — Foundation (5-14 sessions, building habit) * 2 — Growing (15-29 sessions, consistent practice) * 3 — Established (30-59 sessions, deepening) * 4 — Advanced (60+ sessions, integration focus) */ const STAGE_THRESHOLDS: Record = { 0: { sessions: 0, consistency: 0 }, 1: { sessions: 5, consistency: 20 }, 2: { sessions: 15, consistency: 35 }, 3: { sessions: 30, consistency: 50 }, 4: { sessions: 60, consistency: 65 }, }; export function computeStage( totalSessions: number, consistencyScore: number ): { stage: number; advanced: boolean } { let stage = 0; for (const [s, threshold] of Object.entries(STAGE_THRESHOLDS)) { const stageNum = parseInt(s); if ( totalSessions >= threshold.sessions && consistencyScore >= threshold.consistency ) { stage = Math.max(stage, stageNum); } } return { stage, advanced: stage > 0 }; } // --------------------------------------------------------------------------- // Streak computation // --------------------------------------------------------------------------- export interface StreakResult { streakDays: number; streakBroken: boolean; lastSessionDate: Date | null; } export function computeStreak( lastSessionDate: Date | null, previousStreak: number, now: Date = new Date() ): StreakResult { if (!lastSessionDate) { return { streakDays: 0, streakBroken: false, lastSessionDate: null }; } const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const lastDate = new Date( lastSessionDate.getFullYear(), lastSessionDate.getMonth(), lastSessionDate.getDate() ); const diffDays = Math.floor( (today.getTime() - lastDate.getTime()) / (1000 * 60 * 60 * 24) ); if (diffDays === 0) { // Already completed today — maintain current streak return { streakDays: previousStreak, streakBroken: false, lastSessionDate }; } if (diffDays === 1) { // Yesterday — streak continues return { streakDays: previousStreak + 1, streakBroken: false, lastSessionDate }; } // More than 1 day gap — streak broken return { streakDays: 1, streakBroken: true, lastSessionDate }; } // --------------------------------------------------------------------------- // Consistency score // --------------------------------------------------------------------------- /** * Consistency score (0-100) reflects how regularly the user practices. * - Each day with a session in the last 14 days contributes ~7.14 points * - Capped at 100 */ export function computeConsistency( sessionDates: Date[], lookbackDays: number = 14 ): number { const now = new Date(); const cutoff = new Date(now.getTime() - lookbackDays * 24 * 60 * 60 * 1000); const recentDates = new Set( sessionDates .filter((d) => d >= cutoff) .map((d) => { const local = new Date(d); return `${local.getFullYear()}-${local.getMonth()}-${local.getDate()}`; }) ); return Math.min(Math.round((recentDates.size / lookbackDays) * 100), 100); } // --------------------------------------------------------------------------- // Full state update after session completion // --------------------------------------------------------------------------- export interface StateUpdate { attentionLevel: number; emotionStability: number; stressLevel: number; practiceStage: number; consistencyScore: number; streakDays: number; totalSessionsCompleted: number; stageAdvanced: boolean; streakMilestone: boolean; } export function computeStateUpdate( currentState: UserState, deltas: { attentionDelta: number; emotionDelta: number; stressDelta: number; }, recentSessionDates: Date[], now: Date = new Date() ): StateUpdate { // Apply deltas with clamping const attentionLevel = Math.max( 0, Math.min(100, currentState.attentionLevel + deltas.attentionDelta) ); const emotionStability = Math.max( 0, Math.min(100, currentState.emotionStability + deltas.emotionDelta) ); const stressLevel = Math.max( 0, Math.min(100, currentState.stressLevel + deltas.stressDelta) ); // Compute streak const streak = computeStreak( currentState.lastSessionDate, currentState.streakDays, now ); // Compute consistency const consistencyScore = computeConsistency(recentSessionDates); // Compute stage const totalSessionsCompleted = currentState.totalSessionsCompleted + 1; const newStage = computeStage(totalSessionsCompleted, consistencyScore); const stageAdvanced = newStage.stage > currentState.practiceStage; // Check streak milestones (3, 7, 14, 30) const streakMilestone = streak.streakDays > 0 && [3, 7, 14, 30].includes(streak.streakDays) && streak.streakDays > currentState.streakDays; return { attentionLevel, emotionStability, stressLevel, practiceStage: newStage.stage, consistencyScore, streakDays: streak.streakDays, totalSessionsCompleted, stageAdvanced, streakMilestone, }; }