import { createDatabase, closeDatabase } from "@app/database"; import { SEED_INGREDIENTS } from "@app/database/seed"; import { BRAND } from "@app/shared-types"; import * as fs from "node:fs/promises"; import * as path from "node:path"; const publicDir = "/mnt/c/Users/Public"; interface RecipeWithIngredients { id: string; slug: string; titleSv: string; titleEn: string | null; cuisine: string | null; mealType: string | null; dietVariant: string | null; sourceType: string; verificationStatus: string; } function normalizeDietVariant(raw: string | null): string { return raw ?? "standard"; } async function main() { const dbUrl = process.env.DATABASE_URL; if (!dbUrl) throw new Error("DATABASE_URL saknas"); const { db } = createDatabase(dbUrl); const recipes = (await db.query.recipes.findMany({ where: (t, { inArray }) => inArray(t.verificationStatus, ["verified", "editorial"]), })) as RecipeWithIngredients[]; const enriched = []; for (const r of recipes) { const ingredients = await db.query.recipeIngredients.findMany({ where: (t, { eq }) => eq(t.recipeId, r.id), }); const steps = await db.query.recipeSteps.findMany({ where: (t, { eq }) => eq(t.recipeId, r.id), }); enriched.push({ ...r, ingredients, steps }); } // ── Coverage matrix ────────────────────────────────────────────────────── const mainIds = Array.from(new Set(SEED_INGREDIENTS.map((i) => i.id))); const matrix = new Map>(); for (const r of recipes) { const ingredients = await db.query.recipeIngredients.findMany({ where: (t, { eq }) => eq(t.recipeId, r.id), }); const mainIngs = ingredients.filter((ing) => mainIds.includes(ing.canonicalIngredientId)); const diet = normalizeDietVariant(r.dietVariant); for (const ing of mainIngs) { if (!matrix.has(ing.canonicalIngredientId)) matrix.set(ing.canonicalIngredientId, new Map()); const cell = matrix.get(ing.canonicalIngredientId)!; cell.set(diet, (cell.get(diet) ?? 0) + 1); } } const coverageMatrix = mainIds .map((id) => { const ing = SEED_INGREDIENTS.find((i) => i.id === id); const cell = matrix.get(id) ?? new Map(); return { ingredientId: id, nameSv: ing?.nameSv ?? id, category: ing?.category ?? "unknown", total: Array.from(cell.values()).reduce((s, v) => s + v, 0), byDiet: Object.fromEntries(cell), }; }) .sort((a, b) => a.total - b.total); const thinCells = coverageMatrix.filter((c) => c.total <= 2); // ── Dedup / similarity stats ───────────────────────────────────────────── const slugs = recipes.map((r) => r.slug); const uniqueSlugs = new Set(slugs); const titleWords = recipes.map((r) => r.titleSv .toLowerCase() .replace(/[^\w\s]/g, "") .split(/\s+/) .filter((w) => w.length > 3), ); const titleSignatures = titleWords.map((words) => words.slice(0, 4).sort().join(" ")); const sigCounts = new Map(); for (const sig of titleSignatures) sigCounts.set(sig, (sigCounts.get(sig) ?? 0) + 1); const suspectedDupes = Array.from(sigCounts.entries()).filter(([, n]) => n > 1); // ── Cost summary ───────────────────────────────────────────────────────── // Round 1 from earlier log summary; round 2 from the combined logs below. const round1CostUsd = 0.0612; const round2CostUsd = 0.0546; // g1 0.0040 + g2 0.0169 + g3 0.0337 const totalCostUsd = round1CostUsd + round2CostUsd; const summary = { exportedAt: new Date().toISOString(), totalVerified: recipes.length, editorialRecipes: recipes.filter((r) => r.sourceType === "own_editorial").length, aiAssistedRecipes: recipes.filter((r) => r.sourceType === "ai_assisted_reviewed").length, editorialRecipesByStatus: recipes.filter((r) => r.verificationStatus === "editorial").length, verifiedRecipesByStatus: recipes.filter((r) => r.verificationStatus === "verified").length, dedupStats: { totalSlugs: slugs.length, uniqueSlugs: uniqueSlugs.size, duplicateSlugs: slugs.length - uniqueSlugs.size, suspectedTitleSignatureDuplicates: suspectedDupes.length, suspectedTitleSignatures: suspectedDupes.map(([sig, n]) => ({ signature: sig, count: n })), }, coverageStats: { totalCanonicalIngredients: mainIds.length, ingredientsWithVerifiedRecipes: coverageMatrix.filter((c) => c.total > 0).length, thinCellsCount: thinCells.length, thinnestCells: thinCells.slice(0, 10), }, costUsd: { round1: round1CostUsd, round2: round2CostUsd, total: totalCostUsd, }, }; await fs.writeFile( path.join(publicDir, `${BRAND.slug}-verified-final-result.json`), JSON.stringify( { exportedAt: new Date().toISOString(), count: enriched.length, recipes: enriched }, null, 2, ), "utf-8", ); await fs.writeFile( path.join(publicDir, `${BRAND.slug}-verified-final-coverage-matrix.json`), JSON.stringify({ exportedAt: new Date().toISOString(), coverageMatrix }, null, 2), "utf-8", ); await fs.writeFile( path.join(publicDir, `${BRAND.slug}-verified-final-dedup-stats.json`), JSON.stringify({ exportedAt: new Date().toISOString(), ...summary.dedupStats }, null, 2), "utf-8", ); await fs.writeFile( path.join(publicDir, `${BRAND.slug}-verified-final-summary.md`), `# ${BRAND.name} Receptkatalog – Final Verified Set - **Exporterad:** ${summary.exportedAt} - **Totalt godkända recept (verified + editorial):** ${summary.totalVerified} - Editorial: ${summary.editorialRecipes} (status=editorial) - AI-assisted & reviewed: ${summary.aiAssistedRecipes} (status=verified) - **Kostnad:** $${summary.costUsd.total.toFixed(4)} (runda 1: $${summary.costUsd.round1.toFixed(4)}, runda 2: $${summary.costUsd.round2.toFixed(4)}) ## Dedup-statistik - Unika slugs: ${summary.dedupStats.uniqueSlugs} / ${summary.dedupStats.totalSlugs} - Dublett-slugs: ${summary.dedupStats.duplicateSlugs} - Misstänkta titel-signatur-dubbletter: ${summary.dedupStats.suspectedTitleSignatureDuplicates} ## Täckningsmatris (tunnaste cellerna) | Huvudingrediens | Kategori | Totalt | Fördelning | |---|---|---:|---| ${thinCells .slice(0, 15) .map((c) => `| ${c.nameSv} | ${c.category} | ${c.total} | ${JSON.stringify(c.byDiet)} |`) .join("\n")} ## Granskning Setet är redo för allergen- + närings-sanity av granskare. `, "utf-8", ); console.error(`[export-verified] Exported ${recipes.length} verified recipes`); console.error( ` result.json -> ${path.join(publicDir, `${BRAND.slug}-verified-final-result.json`)}`, ); console.error( ` coverage-matrix -> ${path.join(publicDir, `${BRAND.slug}-verified-final-coverage-matrix.json`)}`, ); console.error( ` dedup-stats -> ${path.join(publicDir, `${BRAND.slug}-verified-final-dedup-stats.json`)}`, ); console.error( ` summary.md -> ${path.join(publicDir, `${BRAND.slug}-verified-final-summary.md`)}`, ); await closeDatabase(); } main().catch((err) => { console.error(err); process.exit(1); });