feat: AI context builder

This commit is contained in:
2026-05-20 17:46:49 +02:00
parent 0a69ccabfb
commit 278ac112cf
2 changed files with 107 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
import { getDb, resetDb } from '../../db';
import { insertSession } from '../../db/sessions';
import { insertScheduleEntry, updateScheduleStatus } from '../../db/schedule';
import { setSetting } from '../../db/settings';
import { buildContext } from '../../ai/context';
beforeEach(() => { resetDb(); });
describe('buildContext', () => {
it('returns empty history when no data exists', () => {
const db = getDb();
const ctx = buildContext(db, '2026-05-20');
expect(ctx.recentSessions).toHaveLength(0);
expect(ctx.upcomingSchedule).toHaveLength(0);
expect(ctx.currentStreak).toBe(0);
});
it('includes recent sessions', () => {
const db = getDb();
insertSession(db, '2026-05-19', 60, 'good session');
const ctx = buildContext(db, '2026-05-20');
expect(ctx.recentSessions).toHaveLength(1);
expect(ctx.recentSessions[0]!.duration_min).toBe(60);
});
it('includes upcoming schedule', () => {
const db = getDb();
insertScheduleEntry(db, '2026-05-21', 60, null);
const ctx = buildContext(db, '2026-05-20');
expect(ctx.upcomingSchedule).toHaveLength(1);
});
it('reads training goal from settings', () => {
const db = getDb();
setSetting(db, 'goal', 'wear 2 hours comfortably');
const ctx = buildContext(db, '2026-05-20');
expect(ctx.goal).toBe('wear 2 hours comfortably');
});
it('calculates current streak', () => {
const db = getDb();
insertSession(db, '2026-05-18', 60, null);
insertSession(db, '2026-05-19', 60, null);
insertScheduleEntry(db, '2026-05-18', 60, null);
updateScheduleStatus(db, '2026-05-18', 'completed', null);
insertScheduleEntry(db, '2026-05-19', 60, null);
updateScheduleStatus(db, '2026-05-19', 'completed', null);
const ctx = buildContext(db, '2026-05-20');
expect(ctx.currentStreak).toBe(2);
});
});
+56
View File
@@ -0,0 +1,56 @@
import type { Database } from 'better-sqlite3';
import { getRecentSessions, type Session } from '../db/sessions';
import { getUpcomingSchedule, type ScheduleEntry } from '../db/schedule';
import { getSetting } from '../db/settings';
export interface AIContext {
today: string;
goal: string;
recentSessions: Session[];
upcomingSchedule: ScheduleEntry[];
currentStreak: number;
recentSkips: Array<{ date: string; reason: string | null }>;
}
function calculateStreak(db: Database): number {
const rows = db.prepare(
"SELECT date FROM schedule WHERE status = 'completed' ORDER BY date DESC"
).all() as Array<{ date: string }>;
if (rows.length === 0) return 0;
let streak = 0;
let prev: string | null = null;
for (const { date } of rows) {
if (prev === null) {
streak = 1;
prev = date;
continue;
}
const prevDate = new Date(prev + 'T12:00:00');
const currDate = new Date(date + 'T12:00:00');
const diffDays = Math.round((prevDate.getTime() - currDate.getTime()) / 86400000);
if (diffDays <= 2) {
streak++;
prev = date;
} else {
break;
}
}
return streak;
}
export function buildContext(db: Database, today: string): AIContext {
const recentSessions = getRecentSessions(db, 30);
const upcomingSchedule = getUpcomingSchedule(db, today);
const goal = getSetting(db, 'goal') ?? 'Not set';
const currentStreak = calculateStreak(db);
const recentSkips = (db.prepare(
"SELECT date, skip_reason as reason FROM schedule WHERE status = 'skipped' ORDER BY date DESC LIMIT 10"
).all() as Array<{ date: string; reason: string | null }>);
return { today, goal, recentSessions, upcomingSchedule, currentStreak, recentSkips };
}