/** * earth-now.co * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * File: apps/web/lib/smoothing.ts * Purpose: Pure 60 s linear blend between an old and a new CounterModel — no visible jump when SSE delivers a refit (time is a parameter) */ import { counterValue, type CounterModel } from "@earth-now/counter"; /** Guardrail duration: a model swap is smoothed over 60 s (CLAUDE.md production rule). */ export const SMOOTHING_DURATION_MS = 60_000; /** Blend weight for the new model at time t: 0 before swapStart, 1 after swapStart+duration. */ export function smoothingAlpha( tMs: number, swapStartMs: number, durationMs: number = SMOOTHING_DURATION_MS, ): number { if (durationMs <= 0 || tMs >= swapStartMs + durationMs) return 1; if (tMs <= swapStartMs) return 0; return (tMs - swapStartMs) / durationMs; } /** Linear blend of two already-evaluated values across the smoothing window. */ export function blendValues( oldValue: number, newValue: number, tMs: number, swapStartMs: number, durationMs: number = SMOOTHING_DURATION_MS, ): number { const alpha = smoothingAlpha(tMs, swapStartMs, durationMs); if (alpha === 0) return oldValue; if (alpha === 1) return newValue; return oldValue * (1 - alpha) + newValue * alpha; } /** * Evaluator that linearly cross-fades counterValue(oldModel) → counterValue(newModel) * over [swapStartMs, swapStartMs + durationMs]. Exactly the old value at swapStart, * exactly the new value from swapStart+duration onwards. Pure — never reads the clock. */ export function makeSmoothedEvaluator( oldModel: CounterModel, newModel: CounterModel, swapStartMs: number, durationMs: number = SMOOTHING_DURATION_MS, ): (tMs: number) => number { return (tMs: number): number => { const alpha = smoothingAlpha(tMs, swapStartMs, durationMs); if (alpha === 1) return counterValue(newModel, tMs); const oldValue = counterValue(oldModel, tMs); if (alpha === 0) return oldValue; return oldValue * (1 - alpha) + counterValue(newModel, tMs) * alpha; }; }