import { createAamosClient } from "@app/ai-contracts"; import { SEED_INGREDIENTS } from "@app/database/seed"; import { createDatabase, closeDatabase, schema } from "@app/database"; import { runPipeline, type PipelineTarget, type PipelineIngredient } from "@app/recipe-generation"; import type { CanonicalIngredientLookup, SimilarityLookup, RecipeCandidate, } from "@app/recipe-generation"; import { BRAND } from "@app/shared-types"; import { randomUUID } from "node:crypto"; const VALID_CUISINES = new Set([ "swedish", "nordic", "italian", "french", "spanish", "greek", "thai", "chinese", "japanese", "korean", "vietnamese", "indian", "mexican", "american", "turkish", "lebanese", "moroccan", "middle_eastern", "international", ]); function normalizeCuisine(raw: string | undefined): string { const c = (raw ?? "swedish").toLowerCase().replace(/[^a-z]/g, "_"); if (VALID_CUISINES.has(c)) return c; if (c === "asian") return "international"; if (c.includes("swedish") || c.includes("nordic")) return "swedish"; if (c.includes("italian")) return "italian"; if (c.includes("asian")) return "international"; if (c.includes("japan")) return "japanese"; if (c.includes("china")) return "chinese"; if (c.includes("thai")) return "thai"; if (c.includes("indian")) return "indian"; if (c.includes("mexican")) return "mexican"; if (c.includes("american")) return "american"; if (c.includes("mediterranean")) return "greek"; return "international"; } const catalog: PipelineIngredient[] = SEED_INGREDIENTS.map((i) => ({ id: i.id, nameSv: i.nameSv, category: i.category, defaultUnit: i.defaultUnit, isVegan: i.isVegan, isVegetarian: i.isVegetarian, containsGluten: i.containsGluten, containsLactose: i.containsLactose, allergens: i.allergens, })); const ingredientLookup: CanonicalIngredientLookup = { getById(id: string) { const ing = SEED_INGREDIENTS.find((i) => i.id === id); if (!ing) return undefined; return { id: ing.id, nutritionPer100: ing.nutritionPer100, defaultUnit: ing.defaultUnit, densityGPerMl: ing.densityGPerMl ?? null, gramsPerPiece: ing.gramsPerPiece ?? null, allergens: ing.allergens, isVegan: ing.isVegan, isVegetarian: ing.isVegetarian, containsGluten: ing.containsGluten, containsLactose: ing.containsLactose, isPork: ing.isPork, isBeef: ing.isBeef, isAlcohol: ing.isAlcohol, }; }, }; const targets: PipelineTarget[] = [ { mealType: "dinner", mainIngredientId: "chicken_breast", dietVariant: "standard", count: 2 }, { mealType: "dinner", mainIngredientId: "minced_beef", dietVariant: "standard", count: 2 }, { mealType: "dinner", mainIngredientId: "tofu", dietVariant: "vegan", count: 1 }, ]; function inferDietTags(candidate: RecipeCandidate): string[] { const ings = candidate.ingredients .map((i) => ingredientLookup.getById(i.canonicalIngredientId)) .filter(Boolean); const tags: string[] = []; const allVegan = ings.every((i) => i?.isVegan); const allVegetarian = ings.every((i) => i?.isVegetarian); const noGluten = ings.every((i) => !i?.containsGluten); const noLactose = ings.every((i) => !i?.containsLactose); if (allVegan) tags.push("vegan"); else if (allVegetarian) tags.push("vegetarian"); if (noGluten) tags.push("gluten_free"); if (noLactose) tags.push("lactose_free"); return tags; } async function main() { const dbUrl = process.env.DATABASE_URL; if (!dbUrl) throw new Error("DATABASE_URL saknas"); const { db } = createDatabase(dbUrl); const client = createAamosClient(process.env); const existing = await db.query.recipes.findMany({ columns: { titleSv: true, slug: true } }); const knownTitles = new Set( existing.map((r) => r.titleSv.toLowerCase().replace(/[^a-z0-9åäö]/g, " ")), ); const knownSlugs = new Set(existing.map((r) => r.slug)); const similarityLookup: SimilarityLookup = { async hasSimilarity(title: string) { return knownTitles.has(title.toLowerCase().replace(/[^a-z0-9åäö]/g, " ")); }, }; console.error("[scale-smoke-test] starting"); const result = await runPipeline(client, targets, catalog, ingredientLookup, similarityLookup, { maxPrepTimeMinutes: 30, maxCookTimeMinutes: 45, portions: 4, }); console.error( `[scale-smoke-test] generated=${result.candidates.length} verified=${result.verifiedCount} unverified=${result.unverifiedCount} rejected=${result.rejectedCount}`, ); let seeded = 0; for (let i = 0; i < result.candidates.length; i++) { const candidate = result.candidates[i]!; const v = result.verificationResults[i]!; if (v.status !== "verified") continue; const slugBase = candidate.titleSv .toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); let slug = slugBase; if (knownSlugs.has(slug)) slug = `${slug}-${randomUUID().slice(0, 8)}`; knownSlugs.add(slug); const nutrition = v.nutritionPerPortion ?? { kcal: 0, proteinG: 0, carbsG: 0, fatG: 0, saturatedFatG: 0, fiberG: 0, sugarG: 0, saltG: 0, }; await db.insert(schema.recipes).values({ id: randomUUID(), slug, titleSv: candidate.titleSv, descriptionSv: candidate.descriptionSv, cuisine: normalizeCuisine(candidate.cuisine), mealTypes: candidate.mealTypes, tags: [], methods: [], equipment: [], prepTimeMinutes: candidate.prepTimeMinutes, cookTimeMinutes: candidate.cookTimeMinutes, totalTimeMinutes: candidate.totalTimeMinutes, portions: candidate.portions, nutritionPerPortion: nutrition, allergens: v.allergens, spiceLevel: candidate.spiceLevel, storageGuidanceSv: candidate.storageGuidanceSv ?? null, mealPrepFriendly: candidate.mealPrepFriendly, freezerFriendly: candidate.freezerFriendly, dna: { cuisine: normalizeCuisine(candidate.cuisine), protein: candidate.ingredients.find((i) => [ "chicken_breast", "chicken_thigh", "minced_beef", "minced_mixed", "salmon", "cod", "shrimp", "tofu", "red_lentils", ].includes(i.canonicalIngredientId), )?.canonicalIngredientId, carbohydrate: candidate.ingredients.find((i) => ["rice_white", "pasta_dry", "potato"].includes(i.canonicalIngredientId), )?.canonicalIngredientId, vegetables: candidate.ingredients .filter((i) => ["tomato", "zucchini", "paprika", "carrot", "spinach", "onion", "garlic"].includes( i.canonicalIngredientId, ), ) .map((i) => i.canonicalIngredientId), flavorProfile: inferDietTags(candidate), spiceLevel: candidate.spiceLevel, method: "stovetop", timeMinutes: candidate.totalTimeMinutes, calories: Math.round(nutrition.kcal), proteinGrams: Math.round(nutrition.proteinG), }, variantType: "standard", status: "draft", verificationStatus: "verified", sourceType: "ai_assisted_reviewed", creatorDisplayName: `${BRAND.name} AI Smoke`, }); seeded++; } console.error(`[scale-smoke-test] seeded=${seeded}`); await closeDatabase(); } main().catch((err) => { console.error(err); process.exit(1); });