Initial commit (unpacked platform)
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import { and, eq, ne, sql } from "drizzle-orm";
|
||||
import { schema } from "@app/database";
|
||||
import type { WorkerContext } from "../context.js";
|
||||
|
||||
/**
|
||||
* Publiceringsflöde för användarrecept (spec §35):
|
||||
* submitted → AI-kontroll (AAMOS MODERATE_RECIPE) → dubblettkontroll (spec §36)
|
||||
* → in_moderation (mänsklig granskning i admin) eller direkt reject.
|
||||
*/
|
||||
export async function processModerateRecipe(ctx: WorkerContext, recipeId: string): Promise<void> {
|
||||
const [recipe] = await ctx.db
|
||||
.select()
|
||||
.from(schema.recipes)
|
||||
.where(eq(schema.recipes.id, recipeId))
|
||||
.limit(1);
|
||||
if (!recipe || recipe.status !== "submitted") return;
|
||||
|
||||
const ingredients = await ctx.db
|
||||
.select()
|
||||
.from(schema.recipeIngredients)
|
||||
.where(eq(schema.recipeIngredients.recipeId, recipeId));
|
||||
const steps = await ctx.db
|
||||
.select()
|
||||
.from(schema.recipeSteps)
|
||||
.where(eq(schema.recipeSteps.recipeId, recipeId))
|
||||
.orderBy(schema.recipeSteps.stepNumber);
|
||||
|
||||
// 1. AI-kontroll (spec §35 steg 2)
|
||||
const result = await ctx.aamos.runTask("MODERATE_RECIPE", {
|
||||
titleSv: recipe.titleSv,
|
||||
descriptionSv: recipe.descriptionSv,
|
||||
ingredients: ingredients.map((i) => `${i.quantity} ${i.unit} ${i.displayNameSv}`),
|
||||
steps: steps.map((s) => s.instructionSv),
|
||||
});
|
||||
|
||||
if (result.status === "ok" && result.output?.recommendation === "reject") {
|
||||
await ctx.db
|
||||
.update(schema.recipes)
|
||||
.set({
|
||||
status: "rejected",
|
||||
moderationNote: result.output.flags.map((f) => f.messageSv).join(" "),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.recipes.id, recipeId));
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Dubblettkontroll (spec §36): jämför ingrediensuppsättning + DNA
|
||||
const candidates = await ctx.db
|
||||
.select({ id: schema.recipes.id, dna: schema.recipes.dna, titleSv: schema.recipes.titleSv })
|
||||
.from(schema.recipes)
|
||||
.where(and(eq(schema.recipes.status, "published"), ne(schema.recipes.id, recipeId)))
|
||||
.limit(500);
|
||||
|
||||
const mySet = new Set(ingredients.map((i) => i.canonicalIngredientId));
|
||||
for (const candidate of candidates) {
|
||||
const otherIngredients = await ctx.db
|
||||
.select({ canonicalIngredientId: schema.recipeIngredients.canonicalIngredientId })
|
||||
.from(schema.recipeIngredients)
|
||||
.where(eq(schema.recipeIngredients.recipeId, candidate.id));
|
||||
const otherSet = new Set(otherIngredients.map((i) => i.canonicalIngredientId));
|
||||
const intersection = [...mySet].filter((id) => otherSet.has(id)).length;
|
||||
const union = new Set([...mySet, ...otherSet]).size;
|
||||
const jaccard = union > 0 ? intersection / union : 0;
|
||||
|
||||
if (jaccard >= 0.6) {
|
||||
const classification =
|
||||
jaccard >= 0.9 ? "duplicate" : jaccard >= 0.75 ? "variant" : "inspired";
|
||||
await ctx.db
|
||||
.insert(schema.recipeSimilarities)
|
||||
.values({
|
||||
recipeAId: recipeId,
|
||||
recipeBId: candidate.id,
|
||||
similarityScore: Math.round(jaccard * 100) / 100,
|
||||
classification,
|
||||
details: { method: "ingredient_jaccard", intersection, union },
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Till mänsklig moderering (spec §35 steg 4). Vanliga rätter får ha
|
||||
// legitima varianter – dubbletter avgörs av människa, inte automatik.
|
||||
const flagsNote =
|
||||
result.status === "ok" && result.output
|
||||
? result.output.flags.map((f) => `[${f.severity}] ${f.messageSv}`).join(" ")
|
||||
: "AI-kontrollen kunde inte köras – manuell granskning krävs.";
|
||||
const dupCount = await ctx.db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(schema.recipeSimilarities)
|
||||
.where(eq(schema.recipeSimilarities.recipeAId, recipeId));
|
||||
|
||||
await ctx.db
|
||||
.update(schema.recipes)
|
||||
.set({
|
||||
status: "in_moderation",
|
||||
moderationNote: `${flagsNote} Dubblettkandidater: ${Number(dupCount[0]?.count ?? 0)}.`.trim(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.recipes.id, recipeId));
|
||||
}
|
||||
Reference in New Issue
Block a user