import type { TrustState } from "@app/shared-types"; export interface DecayProfile { halfLifeDays: number; staleAfterDays: number; } export interface TrustInput { confidence: number; verifiedByUser: boolean; lastVerifiedAt: Date | string | null; quantity: number; updatedAt: Date | string; } export interface InventoryItemLike { confidence: number; verifiedByUser: boolean; lastVerifiedAt: Date | string | null; quantity: number; updatedAt: Date | string; } export interface TrustResult { state: TrustState; score: number; // 0–100 } const DEFAULT_DECAY: DecayProfile = { halfLifeDays: 7, staleAfterDays: 30 }; /** * Deterministic exponential decay of confidence. * Decay affects TRUST only, never edibility (mjölkprincipen D-035). */ export function applyDecay( confidence: number, elapsedDays: number, profile: DecayProfile = DEFAULT_DECAY, ): number { if (elapsedDays <= 0) return confidence; if (elapsedDays >= profile.staleAfterDays) return 0; const factor = Math.pow(0.5, elapsedDays / profile.halfLifeDays); return Math.max(0, confidence * factor); } /** * Compute the trust state of an inventory item. * * - unverified: quantity is present but never verified. * - trusted: user verified or high confidence and recently touched. * - decaying: confidence is dropping / not verified for a while. * - stale: very old, unverified, or confidence critically low. * * Decay affects TRUST, not edibility (mjölkprincipen D-035). */ export function computeTrust( input: TrustInput, now: Date = new Date(), profile: DecayProfile = DEFAULT_DECAY, ): TrustResult { const nowMs = now.getTime(); const lastVerified = input.lastVerifiedAt ? new Date(input.lastVerifiedAt).getTime() : null; const updated = new Date(input.updatedAt).getTime(); const referenceMs = lastVerified ?? updated; const elapsedDays = (nowMs - referenceMs) / 86_400_000; const daysSinceUpdate = (nowMs - updated) / 86_400_000; // Decay confidence deterministically from the profile. const effectiveConfidence = applyDecay(input.confidence, elapsedDays, profile); // Base score from effective confidence (0–80 points) let score = Math.round(effectiveConfidence * 80); // Verification bonus (0–20 points) – verification age is already reflected by decay, // but a known user-verified item retains a small pedigree bonus. if (input.verifiedByUser && elapsedDays <= 7) { score += 20; } else if (input.verifiedByUser && elapsedDays <= 30) { score += 10; } else if (input.verifiedByUser) { score += 5; } // Additional staleness penalty when update is old. if (daysSinceUpdate > profile.staleAfterDays) { score -= 20; } else if (daysSinceUpdate > 14) { score -= Math.min(15, Math.floor((daysSinceUpdate - 14) / 7) * 5); } if (input.quantity <= 0) { score -= 10; // depleted items are less interesting for trust } score = Math.max(0, Math.min(100, score)); let state: TrustState; if (score >= 80) { state = "trusted"; } else if (score >= 60) { state = "trusted"; } else if (score >= 30) { state = "decaying"; } else if (lastVerified === null && daysSinceUpdate <= 1) { state = "unverified"; } else { state = "stale"; } return { state, score }; } /** Convenience wrapper that returns just the state for an inventory row. */ export function itemTrustState( item: InventoryItemLike, now?: Date, profile?: DecayProfile, ): TrustState { return computeTrust(item, now, profile).state; } /** Pick the most urgent trust state from a list (used for household summary). */ export function worstTrustState(states: TrustState[]): TrustState { const order: TrustState[] = ["stale", "decaying", "unverified", "trusted"]; for (const s of order) { if (states.includes(s)) return s; } return "unverified"; }