Rewrite variety score and suggestions with configurable scoring

- Add VarietyScoreConfig entity, repository, and V020 migration for
  per-household scoring weights and configurable tag types
- Rewrite getVarietyScore: tag-type repeats on consecutive days,
  non-staple ingredient overlaps, cooking log history, plan duplicates
- Rewrite getSuggestions: simulate variety score for each candidate,
  add tag filter (AND, case-insensitive) and configurable topN param
- Update SuggestionResponse to return simulatedScore instead of
  fitReasons/warnings, update VarietyScoreResponse to new shape
- Seed default VarietyScoreConfig on household creation
- Extend test suite across all domains (+270 tests, all passing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 10:33:11 +02:00
parent 9ec703abcd
commit 8221a1fd41
21 changed files with 3225 additions and 110 deletions

View File

@@ -0,0 +1,76 @@
package com.recipeapp.planning.entity;
import com.recipeapp.household.entity.Household;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
@Entity
@Table(name = "variety_score_config")
public class VarietyScoreConfig {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "household_id", nullable = false, unique = true)
private Household household;
@Column(name = "repeat_tag_types", nullable = false, columnDefinition = "text[]")
private String[] repeatTagTypes;
@Column(name = "w_tag_repeat", nullable = false, precision = 3, scale = 1)
private BigDecimal wTagRepeat;
@Column(name = "w_ingredient_overlap", nullable = false, precision = 3, scale = 1)
private BigDecimal wIngredientOverlap;
@Column(name = "w_recent_repeat", nullable = false, precision = 3, scale = 1)
private BigDecimal wRecentRepeat;
@Column(name = "w_plan_duplicate", nullable = false, precision = 3, scale = 1)
private BigDecimal wPlanDuplicate;
@Column(name = "history_days", nullable = false)
private int historyDays;
protected VarietyScoreConfig() {}
public VarietyScoreConfig(Household household, String[] repeatTagTypes,
BigDecimal wTagRepeat, BigDecimal wIngredientOverlap,
BigDecimal wRecentRepeat, BigDecimal wPlanDuplicate,
int historyDays) {
this.household = household;
this.repeatTagTypes = repeatTagTypes;
this.wTagRepeat = wTagRepeat;
this.wIngredientOverlap = wIngredientOverlap;
this.wRecentRepeat = wRecentRepeat;
this.wPlanDuplicate = wPlanDuplicate;
this.historyDays = historyDays;
}
public static VarietyScoreConfig defaults(Household household) {
return new VarietyScoreConfig(
household,
new String[]{"protein", "cuisine"},
new BigDecimal("1.5"),
new BigDecimal("0.3"),
new BigDecimal("1.0"),
new BigDecimal("2.0"),
14
);
}
public UUID getId() { return id; }
public Household getHousehold() { return household; }
public List<String> getRepeatTagTypes() { return Arrays.asList(repeatTagTypes); }
public BigDecimal getWTagRepeat() { return wTagRepeat; }
public BigDecimal getWIngredientOverlap() { return wIngredientOverlap; }
public BigDecimal getWRecentRepeat() { return wRecentRepeat; }
public BigDecimal getWPlanDuplicate() { return wPlanDuplicate; }
public int getHistoryDays() { return historyDays; }
}