fix(recipe-generation): BRAND.name-interpolation för creatorDisplayName + rubriker/filnamn

feat(recommendation-engine,api): S4 Smak/Hälsa/Lager-vyer för 'Vad ska vi äta?'

- Ersätter hårdkodat 'Cibello' i scale-batch, scale-smoke-test och export-verified.
- DB-backfill: 224 recept hade 'Cibello AI' i creator_display_name (värdena
  motsvarar nuvarande BRAND.name, ingen rad ändrades men kontrollen är gjord).
- Lägger till view-query-param (default|taste|health|pantry) med fördefinierade
  ScoringWeights och samtyckesgrind.
- Unit-tester för vyer; integrationstester för vy-param, validering och
  fallback utan personalization-samtycke.
- brand-guard grön; pnpm typecheck 19/19; pnpm test --force x2 grönt (34 tasks,
  275 tester).
This commit is contained in:
Sven (AAMOS AI)
2026-08-10 05:17:43 +07:00
parent 881c5bb1e3
commit f4a603e977
8 changed files with 605 additions and 103 deletions
+24 -15
View File
@@ -17,6 +17,7 @@ import {
rankAll,
seasonForDate,
summarizeContext,
viewWeights,
type CookingAssumption,
type RecommendationCandidate,
type RecommendationContext,
@@ -381,11 +382,17 @@ export async function recommendationRoutes(app: FastifyInstance) {
? Math.floor((today.getTime() - Date.parse(lastDate)) / 86_400_000)
: null,
householdRating: householdRatingMap.get(recipe.id) ?? null,
ingredientIds: recipeIngredients.map((i) => i.canonicalIngredientId).filter((id): id is string => id != null),
ingredientIds: recipeIngredients
.map((i) => i.canonicalIngredientId)
.filter((id): id is string => id != null),
});
}
const weights = personalizationEnabled ? undefined : NON_PERSONALIZED_WEIGHTS;
const weights = personalizationEnabled
? q.view === "default"
? undefined
: viewWeights(q.view)
: NON_PERSONALIZED_WEIGHTS;
let recommendations = rankAll(scoredCandidates, ctx, weights, q.limit);
// --- 9. AAMOS-omrankning bakom feature flag (aldrig obligatorisk) ---
@@ -416,19 +423,20 @@ export async function recommendationRoutes(app: FastifyInstance) {
}
// --- 10. Matlådor först när rimligt (spec §24) ---
const mealBoxes = q.includeLeftovers && householdId
? await app.db
.select()
.from(schema.mealBoxes)
.where(
and(
eq(schema.mealBoxes.householdId, householdId),
eq(schema.mealBoxes.status, "available"),
),
)
.orderBy(schema.mealBoxes.recommendedUseBy)
.limit(5)
: [];
const mealBoxes =
q.includeLeftovers && householdId
? await app.db
.select()
.from(schema.mealBoxes)
.where(
and(
eq(schema.mealBoxes.householdId, householdId),
eq(schema.mealBoxes.status, "available"),
),
)
.orderBy(schema.mealBoxes.recommendedUseBy)
.limit(5)
: [];
const mealBoxSuggestions = mealBoxes.map((box) => ({
mealBoxId: box.id,
titleSv: box.titleSv,
@@ -449,6 +457,7 @@ export async function recommendationRoutes(app: FastifyInstance) {
remainingKcal: ctx.remainingKcal,
remainingProteinG: ctx.remainingProteinG,
craving: craving ?? null,
view: personalizationEnabled ? q.view : "default",
},
mealBoxSuggestions,
recommendations,