Commit graph

2 commits

Author SHA1 Message Date
36aba73f66 v0.3 step 3+4: AI plan generator + /list shopping aggregation
- migrations 012 + 013: cauldron_meal_plan_slots + cauldron_pick_points
- db: list_plan_slots, save_plan_slots, delete_plan_slots, mark_plan_generated,
  clear_plan_generated, award_pick_points, enrich_plan_with_slots; scoreboard
  extended with points (sum from pick_points) and weeks_locked alias
- forge.generate_plan: sonnet prompt builds 7-day plan respecting picks,
  validates slot count + day uniqueness + slug-in-pool, fills picker_subs
  from ground-truth picks (model output is advisory)
- POST /api/plan/generate: race-safe (existing slots → 409 with plan),
  lock-aware (locked → 409), idempotent
- POST /api/plan/regenerate: re-roll for the original generator, gated by
  ownership + lock; wipes slots + pick_points then re-runs generate
- plan.html: generate CTA + 7 day cards with picker chips + AI reason +
  re-roll button (generator-only, pre-lock); scoreboard now shows points + wins
- /list: pulls plan slots, queries Mealie for ingredients, runs aggregator,
  renders 48px-tall checkbox shopping list with localStorage state per plan_id
- tests: 13 new tests across forge.generate_plan + /api/plan/generate routes
  + /list view + scoreboard SQL inspection. conftest+_testenv stub
  pymysql/oidc/foods at import time so tests run against module-level app
  without a live DB. Both pytest and `unittest discover` paths green (27/27).

Defers: bulk sterilizer admin (A1), foods dedupe (A2), Mealie shopping-list-
export (button rendered but disabled). 7-slot count is fixed at the
endpoint (no UI for slot-count selection yet).

Spec: memory/spec-cauldron-v0.3.md
2026-04-29 06:26:54 -07:00
cc6222139d v0.3 step 2: density-table aggregator engine — the killer math
Pure-Python module + 14 unit tests proving the centerpiece works:

  test_rice_mixed:
    in:  [(2 cup, rice), (1.25 lb, rice)]
    out: 2.25 lb rice  (one line, properly mass+volume combined via density)

  test_butter_mixed:
    in:  [(0.5 cup, butter), (4 oz, butter)]
    out: ~227g butter (~8oz / 0.5 lb)

  test_three_recipes:
    feeds 9 ingredients across 3 recipes through the aggregator;
    rice (cup + lb) collapses, garlic (cloves) sums, eggs count, salt as 'pinch'
    bucketed as to-taste. All on one shopping list.

Algorithm in cauldron/aggregator.py:
  1. Bucket ingredients by canonical food (foods_lookup callable injected — no DB coupling)
  2. Within each food, classify each unit (mass / volume / count / vague / unknown)
  3. CASE 1: only one unit class present → simple sum, display in canonical store-friendly unit
  4. CASE 2: mass + volume (the killer) → use density_g_per_ml to combine to grams
  5. CASE 3: count + (mass | volume) → use common_size_g to convert count to grams
  6. CASE 4: anything that can't reconcile (no density, mixed unknown) → split into 1 line per class with is_split=True
  7. vague (pinch, dash, to taste) → annotate as 'plus to-taste'
  8. unknown units → emit verbatim with the original text

Display: store-friendly unit picker:
  <30g  → grams
  <500g → ounces (nearest 0.5)
  <2kg  → pounds (nearest 0.25)
  >2kg  → big pounds

The aggregator is dependency-injection-friendly — foods_lookup(name) is
the only external call. Tests pass a stub dict; production will pass
foods.search_food(db, name). Decouples math from data quality.

Tests run via:
  python3 -m unittest discover -s tests -v
2026-04-28 22:14:01 -07:00