Sprint 15 round 2 (commit 97b84a7) added seed_recipes_round2.py
which seeded 18 more Spoonacular recipes today. Library went
49 -> 67 total. LLM test (Sprint 13 endpoint, week 2026-07-20):
{picked_count: 0, filled_count: 21, failed_count: 0}
The library now covers all 21 slots of a week (was 19/21 + 2
failed in round 1).
This commit updates the 6 running docs:
- .agent/plan.md — Sprint 15 Round 2 section (S15R2.1-S15R2.4
+ Done when + Out of scope) added after the round 1 sections.
50-query list documented by category.
- .agent/context.md — Round 2 decisions (D7-D10) added: list
focuses on cuisines round 1 didn't cover, round 2 is a
separate script, library at 67 with 21/21 LLM coverage.
- Review/sprint15-verification.md — Round 2 section appended
with: 50-query list, 18-imported result, 12 no-hits, 1 402,
updated DB state (37 Spoonacular + 30 manual = 67), LLM
test result, round 3 follow-up ticket.
- Review/ui-nielsen-audit.md — Round 2 paragraph added to the
Sprint 15 status block.
- fix-ui-audit.md — T8.6 added to the Sprint 15 section.
- Review/handoff-ui-audit.md — TL;DR and Sprint 15 section
updated with round 2.
- docs/HANDOFF.md — Round 2 paragraph added to the Sprint 15
section, Last-updated footer updated.
All 6 docs now reflect Sprint 15 round 2. Library has 67 unique
recipes — 1.25x rotation across 4 weeks of planning. Round 3
can add 30+ more by re-running scripts/seed_recipes.py on the
next day (its query list has round 1 cap-blocked American +
Mediterranean cuisines still to import).
10 KiB
Sprint 15 Verification — Seed 50 family-friendly recipes for 4-week planning (content op) + Sprint 12 latent-bug fix
Date: 2026-06-06. Owner: this agent. Status: code complete (1 file modified + 1 script added + 1 latent-bug fix in main.py), 18 recipes imported (Spoonacular 50-pt cap hit), 49 total recipes in DB (19 Spoonacular + 30 manual), LLM picked_count=0 / filled_count=19 / failed_count=2 for a future-week test. Awaiting commit + push.
What Sprint 15 does
Two parts:
-
Sprint 12 latent-bug fix: moves
recipe_search_api.routermount inbackend/app/main.pyto before the pre-existing WIPrecipes_api.public_routermount. Without this fix, the WIP'sGET /api/recipes/{recipe_id}(UUID-typed) catches/searchand returns 422, breaking Sprint 12's "Search the web" feature in production. The fix is a one-line reorder; no schema, no logic changes. -
Sprint 15 content op: adds
scripts/seed_recipes.py(NEW) — a one-shot Python script that seeds family-friendly recipes from Spoonacular into the local library. The script bypasses the (then-broken)/api/recipes/searchroute by calling Spoonacular'scomplexSearchAPI directly, then POSTs each top hit to the (working)/api/recipes/importendpoint for the local insert.
What Sprint 15 produced
Spoonacular recipes added (18, plus 1 from earlier manual test = 19 total):
| Cuisine | Recipes |
|---|---|
| Italian (8) | Best Chicken Parmesan, Easy Gift Lasagna, Minestrone Soup, Fresh Pesto Shrimp Pasta, Easy Chicken Piccata, Mushroom Risotto, Orange Caprese Salad, Eggplant Parmesan |
| Mexican (7) | Smoky Chicken Tacos, Chicken and Black Bean Burritos, Cheesy Cowboy Quesadillas, Healthy & Spicy Fish Taco Salad, Pork Carnitas Tacos, Hearty Chicken Tortilla Soup, Huevos Rancheros |
| Asian (3) | Chinese Style Chicken and Noodle Stir Fry, Mango Fried Rice, Chicken Spring Rolls |
| American (0) | — (cap hit before American queries) |
| Mediterranean / Middle Eastern (0) | — (cap hit) |
Plus 1 from my earlier manual curl test (Pasta with Garlic, Scallions, Cauliflower & Breadcrumbs, ext=716429).
Final DB state: 49 total recipes, 19 from Spoonacular, 30 from prior manual entry.
Spoonacular cap reason: the free tier is 50 points/day, not 150. My prior cost math was wrong (I assumed 150). At 28 queries, the script hit the cap. Re-running tomorrow would yield ~30 more (after the 18 already imported count toward 50). I did not have time to wait; the user can re-run the script on a later day for more.
LLM plan test (Sprint 13 endpoint, future week 2026-07-06):
{"plan_id": "28b82b80-...", "picked_count": 0, "filled_count": 19, "failed_count": 2, "reasoning": null}
- 19 of 21 slots filled from the library. The LLM (kimi-k2.6:cloud) returned 0 picks; the library fill took over (Sprint 13 tolerance works as designed).
- 2 failed slots — could be repeats, dietary filters, or simply the LLM + library can't cover every slot for that week. Acceptable for a first pass.
Files added
scripts/seed_recipes.py(NEW, ~150 lines) — the one-shot import script. Idempotent (409 on duplicate). 1.5 sec sleep between queries. Logs per-query result. Exits cleanly on Spoonacular 402.
Files modified
backend/app/main.py— movedrecipe_search_api.routerimport up + moved itsinclude_routercall from line 64 to beforerecipes_api.public_routermount. One-line reorder plus a 3-line comment explaining the why..agent/plan.md— Sprint 15 section (S15.1-S15.4 + Done when + Out of scope) added after the Sprint 14 section..agent/context.md— Sprint 15 decisions (D1-D6), open Q1-Q2, file:line references added.Review/sprint15-verification.md(NEW) — this file.
Verification commands
ssh docker-willester
cd /home/peter/MealPlanner
# 1. Confirm search route is reachable (Sprint 12 latent-bug fix)
curl -s 'http://localhost:8082/api/recipes/search?q=chicken+parmesan&limit=2' | head -c 200
# → 200, returns hits
# 2. Confirm 49 recipes in DB
docker exec mealplanner-db-1 psql -U mealplanner -d mealplanner -c \
"SELECT count(*) FILTER (WHERE external_source='spoonacular') AS spoonacular,
count(*) AS total FROM recipe;"
# → spoonacular=19, total=49
# 3. Confirm LLM endpoint can use the new library
curl -s -X POST 'http://localhost:8082/api/llm/plan' \
-H 'Content-Type: application/json' \
-d '{"prompt": "Italian-inspired vegetarian, 30 min max", "week_start": "2026-07-13"}'
# → {plan_id, picked_count, filled_count, failed_count}
# 4. Re-run the seed script (next day) for more recipes
set -a && source .env && set +a
python3 scripts/seed_recipes.py
# → continues from where it left off; 409s for already-imported, 201s for new
Cost math (corrected)
Free tier is 50 pts/day, not 150. Sprint 12's backend cap is 140 (with 10-pt safety margin) — that cap is now incorrectly calibrated to 150, but the real ceiling from Spoonacular is 50. Future fix: lower _DAILY_LIMIT in recipe_search.py:48 from 140 to 45 (leaves 5-pt safety margin). Filed as a follow-up.
Per-query cost:
complexSearch: 1 pt base + 0.01 ×number= 1.10 pts (withnumber=1)./information(called by/api/recipes/import): 1 pt.
50 queries = 50 × 1.10 + 50 × 1 = 50 × 2.10 = 105 pts. So 50 queries needs 3 days on free tier. To get to 50 recipes in 1 day, the user needs a paid Spoonacular plan (which is what the docs assumed).
Sprint 15 net contribution today: 19 Spoonacular recipes (18 from script + 1 from manual test). The remaining 32 can be imported over the next 2 days by re-running the script.
Deploy
ssh docker-willester
cd /home/peter/MealPlanner
git pull
docker compose up -d --build backend # picks up main.py mount order fix
cd frontend && npm test && cd .. # confirm 7/7 (Sprint 14)
docker compose up -d --build frontend
Sprint 15 does not require a migration or new runtime deps. The 18 newly-imported recipes are already in the DB; the deploy is just the code + script + (optionally) the seed script for future runs.
Risk table
| Risk | Mitigation | Status |
|---|---|---|
main.py reorder breaks some other route |
Verified: /api/recipes/search 200; /api/recipes/import 201; /api/recipes GET (WIP) still works; no other route regression. Manual smoke: recipes_api.public_router is mounted AFTER recipe_search_api.router now, but its /{recipe_id} still matches because /search is a literal path, not a UUID. |
Resolved |
_DAILY_LIMIT=140 doesn't match the 50-pt free tier |
Lower to 45 in a follow-up. Doesn't block Sprint 15. | Open (follow-up) |
| Spoonacular 50-pt cap hit at 18/50 | User can re-run the script over the next 2 days. 19 recipes is enough for 1 week; the family can run again for the next 4 weeks. | Acceptable |
LLM picked_count=0 in the test |
The library fill (Sprint 13 tolerance) covered 19/21 slots. The LLM may have been slow to respond or returned 0 picks. The user can re-prompt with a different prompt to exercise the LLM path. | Acceptable |
| Re-running the script double-counts | Idempotent: 409 from /api/recipes/import for already-imported IDs is logged and skipped. |
Resolved |
scripts/seed_recipes.py is in the host's scripts/ but not in git |
The file is now rsynced. The next commit will include it. | Resolved (next commit) |
What Sprint 15 does NOT do
- No new feature work, no schema changes, no UI changes. This is a content op.
- No tuning of the quota counter or the inference logic.
_DAILY_LIMIT=140is wrong (should be 45); follow-up ticket. - No re-running of previous sprints' verification flows. Sprint 15 is additive.
- F9-full (local Ollama model pull). Still opt-in based on cloud-billing feedback.
Open question for follow-up
Q1 — Re-run scripts/seed_recipes.py on a later day to seed the remaining 32 recipes? The script is idempotent; re-running will skip the 19 already imported and import the rest. The user can do this from the host with one command.
Sprint 15 — Round 2 (2026-06-07): +18 recipes, library at 67 total
User direction (2026-06-07): "please add more meals to the potential list" / "Pull in more recipes so we have a larger sample to generate from."
What changed
scripts/seed_recipes_round2.py(NEW, ~120 lines) — second-round seed script. Same idempotent behavior as round 1 (409 on duplicate). Different query list focused on cuisines and meal types the round 1 list didn't cover: Indian (8) + Thai (6) + Chinese regional (6) + Soups & stews (6) + Salads (6) + Sandwiches/wraps (5) + Breakfast (5) + German/European (4) + French (4) = 50 queries.- Result: 18 recipes imported (queries 1, 2, 3, 5, 6, 7, 8, 10, 17, 19, 21, 22, 23, 24, 25, 27, 28, 29). 12 queries returned no hits (Spoonacular's free-tier index doesn't include all titles, e.g. "chana masala", "thai basil chicken", "dan dan noodles"). Query 30 ("wedge salad") hit 402 mid-import, ending the run.
Updated DB state
spoonacular | total
------------+-------
37 | 67
- 19 from round 1 + 18 from round 2 = 37 Spoonacular recipes.
- 30 manual recipes from before.
- 67 unique recipes total. 4 weeks × 21 meals = 84 picks needed; 67 unique = 1.25× coverage (some rotation, but no full uniqueness across 4 weeks — some meals will repeat).
LLM test (Sprint 13 endpoint, week 2026-07-20, prompt: "variety, mix of cuisines, family-friendly, no repeats")
{"plan_id": "04fe4860-...", "picked_count": 0, "filled_count": 21, "failed_count": 0, "reasoning": null}
- 21/21 slots filled, 0 failed. Round 1 returned 19/21 + 2 failed; round 2's expanded library now covers all 21 slots. The LLM still returned 0 picks (Sprint 13 tolerance took over).
- The library fill uses the Sprint 6+
fillEmptySlotspattern: prefers un-used recipes, falls back to any. With 67 unique recipes, a 21-slot week has plenty of un-used options.
Round 2 follow-up
- Q2 — Run a third round? Quota resets every 24h. The next day, the user can re-run
scripts/seed_recipes.py(round 1, will pick up where round 2 left off) or design a round 3 with even more variety. The 67 → 80-100 trajectory is realistic. - Q3 — Lower
_DAILY_LIMIT=140to 45 to match the real 50-pt free-tier cap. Doesn't block; surface in the next sprint that touches the recipe_search.py file.
Cost (corrected, both rounds)
Free tier is 50 pts/day. Each query = 1.10 (search) + 1 (information) = 2.10 pts. 50 queries = 105 pts ≈ 2 days. Round 1 imported 18, round 2 imported 18 = 36 new recipes from ~60 queries. Round 3 could add 30 more.