feat(recipe-generation): Cibello Receptkatalog Fas A – STEG 1

- Nytt paket @app/recipe-generation med pipeline, verifieringsgrind,
  gaprapport och pilotbatch-script.
- Ny AAMOS-task GENERATE_RECIPE_CANDIDATES i @app/ai-contracts.
- GeminiAamosClient utökat med receptgenereringsstöd.
- MockAamosClient uppdaterad med deterministiska kandidater.
- Verifieringspipeline: ingrediensmappning (hård spärr), härledda allergener,
  beräknad näring via nutrition-engine, språk-/kvalitetskontroll, dedup.
- Hermetiska tester: 10/10 gröna.
- typecheck + test + build: gröna för hela workspace.

Refs: docs/32-receptkatalog-buildout.md
This commit is contained in:
Sven (AAMOS AI)
2026-08-09 19:47:03 +07:00
parent 9197d93a92
commit 4b7dec2759
15 changed files with 1554 additions and 0 deletions
@@ -0,0 +1,69 @@
import { describe, it, expect } from "vitest";
import { MockAamosClient } from "@app/ai-contracts";
import { runPipeline, type PipelineTarget, type PipelineIngredient } from "../src/pipeline.js";
import type { CanonicalIngredientLookup, SimilarityLookup } from "../src/verification.js";
const mockClient = new MockAamosClient();
const mockIngredients: CanonicalIngredientLookup = {
getById(id: string) {
const db: Record<string, ReturnType<CanonicalIngredientLookup["getById"]>> = {
kycklingfile: {
id: "kycklingfile",
nutritionPer100: {
basis: "per_100_g",
values: {
kcal: 165, proteinG: 31, carbsG: 0, fatG: 3.6,
saturatedFatG: 1, fiberG: 0, sugarG: 0, saltG: 0.1,
},
},
defaultUnit: "GRAM",
densityGPerMl: null,
gramsPerPiece: null,
allergens: [],
isVegan: false,
isVegetarian: false,
containsGluten: false,
containsLactose: false,
isPork: false,
isBeef: false,
isAlcohol: false,
},
};
return db[id] ?? undefined;
},
};
const noSimilarity: SimilarityLookup = {
async hasSimilarity() {
return false;
},
};
const catalog: PipelineIngredient[] = [
{
id: "kycklingfile",
nameSv: "kycklingfilé",
category: "kott_fagel",
defaultUnit: "GRAM",
isVegan: false,
isVegetarian: false,
containsGluten: false,
containsLactose: false,
allergens: [],
},
];
const targets: PipelineTarget[] = [
{ mealType: "dinner", mainIngredientId: "kycklingfile", dietVariant: "standard", count: 2 },
];
describe("runPipeline", () => {
it("kör med mockad klient och returnerar resultat", async () => {
const result = await runPipeline(mockClient, targets, catalog, mockIngredients, noSimilarity);
expect(result.geminiResult.status).toBe("ok");
// MockAamosClient returnerar nu kandidater för GENERATE_RECIPE_CANDIDATES
expect(result.candidates.length).toBeGreaterThan(0);
expect(result.verifiedCount + result.unverifiedCount + result.rejectedCount).toBe(result.candidates.length);
});
});