Fas 2 steg 3: Household Trust Score (admin + i18n status i appen)

This commit is contained in:
Sven (AAMOS AI)
2026-08-06 23:45:28 +07:00
parent bcf932d2ff
commit 4385fe5125
21 changed files with 396 additions and 23 deletions
+98
View File
@@ -19,6 +19,7 @@ export interface InventoryItemLike {
lastVerifiedAt: Date | string | null;
quantity: number;
updatedAt: Date | string;
depletedAt?: Date | string | null;
}
export interface TrustResult {
@@ -118,6 +119,103 @@ export function itemTrustState(
return computeTrust(item, now, profile).state;
}
export interface HouseholdTrustInput {
items: InventoryItemLike[];
/** Antal justeringstransaktioner senaste 30 dagarna (korrigeringsfrekvens). */
correctionCount30d: number;
/** Totalt antal transaktioner senaste 30 dagarna (används för korrigeringsfrekvens). */
transactionCount30d: number;
}
export interface HouseholdTrustResult {
score: number; // 0100
status: "up_to_date" | "needs_check" | "uncertain";
}
export const TRUST_STATUS_THRESHOLDS = {
upToDate: 80,
needsCheck: 50,
} as const;
/**
* Deterministisk household trust score 0100.
*
* Viktade komponenter (Fas 2 §5.3):
* - 25% verifierade poster (verifiedByUser)
* - 25% ålder på senaste kontroll (nyare = bättre)
* - 20% andel uppskattade mängder (lägre confidence ger mer osäkerhet)
* - 15% poster som borde vara slut (låg quantity utan depletedAt straffar)
* - 15% korrigeringsfrekvens (fler korrigeringar = lägre förtroende)
*
* Decay påverkar FORTROENDE, inte ätbarhet.
*/
export function householdTrustScore(
input: HouseholdTrustInput,
now: Date = new Date(),
): HouseholdTrustResult {
const items = input.items.filter((i) => i.quantity > 0 && !i.depletedAt);
if (items.length === 0) {
return { score: 100, status: "up_to_date" };
}
const nowMs = now.getTime();
// 1. Andel verifierade (0100)
const verifiedRatio = items.filter((i) => i.verifiedByUser).length / items.length;
const verifiedScore = verifiedRatio * 100;
// 2. Ålder på senaste kontroll (0100, 100 = alla kontrollerade inom 7 dagar)
const ageScore =
items.reduce((sum, i) => {
const verifiedMs = i.lastVerifiedAt ? new Date(i.lastVerifiedAt).getTime() : null;
const referenceMs = verifiedMs ?? new Date(i.updatedAt).getTime();
const days = Math.max(0, (nowMs - referenceMs) / 86_400_000);
return sum + Math.max(0, 100 - (days / 30) * 100);
}, 0) / items.length;
// 3. Andel uppskattade mängder (0100, 100 = alla har confidence 1)
const confidenceScore =
items.reduce((sum, i) => sum + Math.min(1, Math.max(0, i.confidence)), 0) / items.length * 100;
// 4. Poster som borde vara slut (0100, 100 = inga låga kvantiteter)
const depletionScore =
items.reduce((sum, i) => {
const qty = i.quantity;
if (qty <= 0) return sum + 0;
if (qty < 0.2) return sum + 40;
if (qty < 0.5) return sum + 70;
return sum + 100;
}, 0) / items.length;
// 5. Korrigeringsfrekvens (0100, 100 = inga korrigeringar)
const correctionRatio =
input.transactionCount30d > 0
? input.correctionCount30d / input.transactionCount30d
: 0;
const correctionScore = Math.max(0, 100 - correctionRatio * 200);
const score = Math.round(
verifiedScore * 0.25 +
ageScore * 0.25 +
confidenceScore * 0.20 +
depletionScore * 0.15 +
correctionScore * 0.15,
);
const clamped = Math.max(0, Math.min(100, score));
let status: HouseholdTrustResult["status"];
if (clamped >= TRUST_STATUS_THRESHOLDS.upToDate) {
status = "up_to_date";
} else if (clamped >= TRUST_STATUS_THRESHOLDS.needsCheck) {
status = "needs_check";
} else {
status = "uncertain";
}
return { score: clamped, status };
}
/** 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"];