Fas 2 steg 1: Inventory Trust Engine - trust states pa inventory_items

This commit is contained in:
Sven (AAMOS AI)
2026-08-06 23:01:43 +07:00
parent 3d1d823583
commit 8ae4bf6ec3
10 changed files with 9090 additions and 4 deletions
+1
View File
@@ -3,3 +3,4 @@ export * from "./balance.js";
export * from "./fefo.js";
export * from "./dedup.js";
export * from "./forecast.js";
export * from "./trust.js";
+94
View File
@@ -0,0 +1,94 @@
import type { TrustState } from "@app/shared-types";
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
}
/**
* 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 spec D-035).
*/
export function computeTrust(input: TrustInput): TrustResult {
const now = Date.now();
const lastVerified = input.lastVerifiedAt ? new Date(input.lastVerifiedAt).getTime() : null;
const updated = new Date(input.updatedAt).getTime();
const daysSinceVerification = lastVerified ? (now - lastVerified) / 86_400_000 : null;
const daysSinceUpdate = (now - updated) / 86_400_000;
// Base score from confidence (080 points)
let score = Math.round(input.confidence * 80);
// Verification bonus (020 points)
if (input.verifiedByUser && daysSinceVerification !== null && daysSinceVerification <= 7) {
score += 20;
} else if (input.verifiedByUser && daysSinceVerification !== null && daysSinceVerification <= 30) {
score += 10;
} else if (input.verifiedByUser) {
score += 5;
}
// Decay penalties (only affect trust score)
if (daysSinceVerification !== null && daysSinceVerification > 7) {
score -= Math.min(20, Math.floor((daysSinceVerification - 7) / 7) * 5);
}
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 (input.verifiedByUser && 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): TrustState {
return computeTrust(item).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";
}
@@ -0,0 +1,62 @@
import { describe, expect, it } from "vitest";
import { computeTrust, worstTrustState } from "../src/trust.js";
describe("computeTrust", () => {
it("returns trusted for a fresh high-confidence item even without user verification", () => {
const result = computeTrust({
confidence: 0.8,
verifiedByUser: false,
lastVerifiedAt: null,
quantity: 2,
updatedAt: new Date(),
});
expect(result.state).toBe("trusted");
expect(result.score).toBeGreaterThanOrEqual(60);
});
it("decays when last verification is old", () => {
const thirtyDaysAgo = new Date(Date.now() - 30 * 86_400_000);
const result = computeTrust({
confidence: 0.6,
verifiedByUser: true,
lastVerifiedAt: thirtyDaysAgo,
quantity: 1,
updatedAt: thirtyDaysAgo,
});
expect(result.state).toBe("decaying");
});
it("is stale for very old unverified low-confidence items", () => {
const sixtyDaysAgo = new Date(Date.now() - 60 * 86_400_000);
const result = computeTrust({
confidence: 0.2,
verifiedByUser: false,
lastVerifiedAt: null,
quantity: 1,
updatedAt: sixtyDaysAgo,
});
expect(result.state).toBe("stale");
});
it("is trusted when user verified recently", () => {
const result = computeTrust({
confidence: 0.7,
verifiedByUser: true,
lastVerifiedAt: new Date(),
quantity: 1,
updatedAt: new Date(),
});
expect(result.state).toBe("trusted");
expect(result.score).toBeGreaterThanOrEqual(70);
});
});
describe("worstTrustState", () => {
it("returns stale if present", () => {
expect(worstTrustState(["trusted", "stale", "decaying"])).toBe("stale");
});
it("falls back to unverified", () => {
expect(worstTrustState(["trusted", "unverified"])).toBe("unverified");
});
});