Initial commit (unpacked platform)

This commit is contained in:
Sven (AAMOS AI)
2026-08-05 19:21:11 +07:00
commit ac5340195a
314 changed files with 57584 additions and 0 deletions
+101
View File
@@ -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));
}