Fas 2 steg 2: Inventory Trust Engine - decay-profiler och cache-jobb

This commit is contained in:
Sven (AAMOS AI)
2026-08-06 23:25:32 +07:00
parent 8ae4bf6ec3
commit bcf932d2ff
17 changed files with 18296 additions and 61 deletions
+12
View File
@@ -16,6 +16,7 @@ import {
processStoreNotification,
processSubscriptionSweep,
processTrainingExport,
processTrustDecay,
} from "./processors/maintenance.js";
import {
@@ -110,6 +111,12 @@ const worker = new Worker(
return;
}
case "UPDATE_TRUST_STATES": {
const updated = await processTrustDecay(ctx);
if (updated > 0) log(`Trust decay: ${updated} items uppdaterade`);
return;
}
// Deterministiska/planerade jobb som inte kräver egen processor ännu
case "NORMALIZE_PRODUCTS":
case "DEDUPLICATE_INVENTORY":
@@ -201,6 +208,11 @@ async function registerRepeatableJobs() {
{ every: 30_000 },
{ name: "PUBLISH_OUTBOX", data: { jobType: "PUBLISH_OUTBOX" }, opts: baseOpts },
);
await queue.upsertJobScheduler(
"scheduler-trust",
{ every: 300_000 },
{ name: "UPDATE_TRUST_STATES", data: { jobType: "UPDATE_TRUST_STATES" }, opts: baseOpts },
);
await queue.upsertJobScheduler(
"scheduler-expiry",
{ pattern: "0 7 * * *", tz: "Europe/Stockholm" },
+53 -1
View File
@@ -1,6 +1,6 @@
import { and, eq, gt, inArray, isNull, lte, sql } from "drizzle-orm";
import { schema } from "@app/database";
import { classifyExpiry } from "@app/inventory-engine";
import { classifyExpiry, computeTrust } from "@app/inventory-engine";
import { deriveMemoryUpdates } from "@app/memory-client";
import { getLocaleContext } from "../locale.js";
import type { WorkerContext } from "../context.js";
@@ -31,6 +31,58 @@ export async function processOutbox(ctx: WorkerContext): Promise<number> {
return pending.length;
}
/**
* UPDATE_TRUST_STATES (Fas 2 §5.2): uppdatera cache-kolumnen inventory_items.trust_state
* utifrån aktiv decay-profil. Körs periodiskt. Decay påverkar endast förtroende,
* aldrig ätbarhet (mjölkprincipen D-035).
*/
export async function processTrustDecay(ctx: WorkerContext): Promise<number> {
const [profile] = await ctx.db
.select({
halfLifeDays: schema.inventoryDecayProfiles.halfLifeDays,
staleAfterDays: schema.inventoryDecayProfiles.staleAfterDays,
})
.from(schema.inventoryDecayProfiles)
.where(eq(schema.inventoryDecayProfiles.active, true))
.orderBy(schema.inventoryDecayProfiles.createdAt)
.limit(1);
const decayProfile = {
halfLifeDays: profile?.halfLifeDays ?? 7,
staleAfterDays: profile?.staleAfterDays ?? 30,
};
const now = new Date();
const rows = await ctx.db
.select()
.from(schema.inventoryItems)
.where(and(isNull(schema.inventoryItems.depletedAt), gt(schema.inventoryItems.quantity, 0)))
.limit(500);
let updated = 0;
for (const item of rows) {
const { state } = computeTrust(
{
confidence: item.confidence,
verifiedByUser: item.verifiedByUser,
lastVerifiedAt: item.lastVerifiedAt,
quantity: item.quantity,
updatedAt: item.updatedAt,
},
now,
decayProfile,
);
if (state !== item.trustState) {
await ctx.db
.update(schema.inventoryItems)
.set({ trustState: state, updatedAt: now })
.where(eq(schema.inventoryItems.id, item.id));
updated++;
}
}
return updated;
}
/** SEND_EXPIRY_NOTIFICATION (spec §54): skapa notiser för varor som snart går ut. */
export async function processExpiryNotifications(ctx: WorkerContext): Promise<number> {
const households = await ctx.db.select({ id: schema.households.id }).from(schema.households);