/** * AgentGuard(TM) Spend: local Advisor forecast skeleton. * * Reads local decision logs only. No network calls are made. * * Patent notice: Protected by U.S. patent-pending technology * (App. Nos. 63/983,615; 63/983,621; 63/983,843; 63/984,626; * 64/071,781; 64/071,789). */ import type { AdvisorSpendPoint } from './anomaly'; export interface AdvisorForecast { daysObserved: number; monthEndCents: number; capCents: number | null; overCap: boolean; message: string; } export function forecastMonthEnd(points: AdvisorSpendPoint[], capCents: number | null = null, now = new Date()): AdvisorForecast { const daily = lastThirtyDaily(points, now); if (daily.length === 0) { return { daysObserved: 0, monthEndCents: 0, capCents, overCap: false, message: 'No local spend history found.' }; } const slope = linearSlope(daily.map((row, index) => ({ x: index + 1, y: row.cents }))); const monthDays = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate(); const elapsed = now.getDate(); const observedTotal = daily.reduce((sum, row) => sum + row.cents, 0); const avg = observedTotal / Math.max(1, daily.length); const projectedRemaining = Math.max(0, monthDays - elapsed) * Math.max(0, avg + slope); const monthEndCents = Math.round(observedTotal + projectedRemaining); const overCap = capCents !== null && monthEndCents > capCents; return { daysObserved: daily.length, monthEndCents, capCents, overCap, message: overCap ? 'Projected spend is above cap. Consider a lower fallback model or a tighter per_day cap.' : 'Projected spend is within the current cap.', }; } function lastThirtyDaily(points: AdvisorSpendPoint[], now: Date): Array<{ day: string; cents: number }> { const cutoff = now.getTime() - 30 * 24 * 60 * 60 * 1000; const buckets = new Map(); for (const point of points) { const t = Date.parse(point.ts); if (!Number.isFinite(t) || t < cutoff) continue; const day = new Date(t).toISOString().slice(0, 10); buckets.set(day, (buckets.get(day) ?? 0) + point.cents); } return [...buckets.entries()].map(([day, cents]) => ({ day, cents })).sort((a, b) => a.day.localeCompare(b.day)); } function linearSlope(points: Array<{ x: number; y: number }>): number { if (points.length < 2) return 0; const n = points.length; const sx = points.reduce((sum, point) => sum + point.x, 0); const sy = points.reduce((sum, point) => sum + point.y, 0); const sxx = points.reduce((sum, point) => sum + point.x * point.x, 0); const sxy = points.reduce((sum, point) => sum + point.x * point.y, 0); const denom = n * sxx - sx * sx; return denom === 0 ? 0 : (n * sxy - sx * sy) / denom; }