Initial commit: Inner OS v1
Full-stack inner development operating system built on Next.js 16 + Prisma 7 + PostgreSQL + OpenAI + Clerk. Generates personalized daily mental training tasks, collects reflection, updates user state, and provides internal reward feedback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
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<number, { sessions: number; consistency: number }> = {
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user