Files
Cibello-app/packages/inventory-engine/src/cooking-profiles.ts
T
Sven (AAMOS AI) c32a7e33c7
CI / Typecheck, test & build (push) Failing after 2s
ci: trigga på master + formatfix inför Gitea Actions
2026-08-13 17:25:14 +07:00

115 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Antagandeprofiler för matlagning (Fas 3 §6.2).
* Rensa, deterministiska funktioner för att uppdatera per-hushålls-per-ingrediens-profiler
* utifrån faktiska svar efter en cooking session.
*/
export interface ProfileUpdateInput {
householdId: string;
canonicalIngredientId: string;
plannedPortions: number;
actualPortionsEaten: number;
leftoverEstimatePortions: number;
sessionId: string;
date: string;
}
export interface ExistingProfile {
averageEatenPortions: number | null;
averageLeftoverPortions: number | null;
observationCount: number;
lastSessionAnswers?: Array<{
sessionId: string;
eaten: number;
leftovers: number;
date: string;
}> | null;
}
/**
* Beräkna nya profilvärden med exponentiellt glidande medelvärde.
* alpha = 1 / (observationCount + 1) ger jämn viktning över tid.
*/
export function updateCookingAssumptionProfile(
input: ProfileUpdateInput,
existing: ExistingProfile,
): {
averageEatenPortions: number;
averageLeftoverPortions: number;
observationCount: number;
lastSessionAnswers: Array<{ sessionId: string; eaten: number; leftovers: number; date: string }>;
} {
const eaten = clamp(input.actualPortionsEaten, 0, input.plannedPortions);
const leftovers = clamp(input.leftoverEstimatePortions, 0, input.plannedPortions - eaten);
const prevEaten = existing.averageEatenPortions ?? eaten;
const prevLeftovers = existing.averageLeftoverPortions ?? leftovers;
const prevCount = existing.observationCount ?? 0;
const alpha = 1 / (prevCount + 1);
const averageEatenPortions = round2(prevEaten + alpha * (eaten - prevEaten));
const averageLeftoverPortions = round2(prevLeftovers + alpha * (leftovers - prevLeftovers));
const observationCount = prevCount + 1;
const history = existing.lastSessionAnswers ?? [];
const lastSessionAnswers = [
{ sessionId: input.sessionId, eaten, leftovers, date: input.date },
...history,
].slice(0, 10);
return { averageEatenPortions, averageLeftoverPortions, observationCount, lastSessionAnswers };
}
/**
* Återställ en antagandeprofil genom att ta bort ett specifikt sessions-id
* från historiken och räkna om medelvärdena deterministiskt från resterande
* observationer. Ren funktion alla beroenden är explicita argument.
*/
export function rollbackCookingAssumptionProfile(
sessionId: string,
existing: ExistingProfile,
): {
averageEatenPortions: number | null;
averageLeftoverPortions: number | null;
observationCount: number;
lastSessionAnswers: Array<{ sessionId: string; eaten: number; leftovers: number; date: string }>;
} {
const history = (existing.lastSessionAnswers ?? []).filter((a) => a.sessionId !== sessionId);
if (history.length === 0) {
return {
averageEatenPortions: null,
averageLeftoverPortions: null,
observationCount: 0,
lastSessionAnswers: [],
};
}
// Räkna om EMA från scratch i kronologisk ordning (äldst först).
const chronological = [...history].reverse();
let avgEaten = chronological[0]!.eaten;
let avgLeftovers = chronological[0]!.leftovers;
for (let i = 1; i < chronological.length; i++) {
const alpha = 1 / (i + 1);
const a = chronological[i]!;
avgEaten = round2(avgEaten + alpha * (a.eaten - avgEaten));
avgLeftovers = round2(avgLeftovers + alpha * (a.leftovers - avgLeftovers));
}
return {
averageEatenPortions: avgEaten,
averageLeftoverPortions: avgLeftovers,
observationCount: history.length,
lastSessionAnswers: history,
};
}
function clamp(n: number, min: number, max: number): number {
return Math.max(min, Math.min(max, n));
}
function round2(n: number): number {
return Math.round(n * 100) / 100;
}