Files
Cibello-app/packages/inventory-engine/src/trust.ts
T

129 lines
3.8 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.
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; // 0100
}
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 (080 points)
let score = Math.round(effectiveConfidence * 80);
// Verification bonus (020 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";
}