Files
meal-agent d8aa87921e Sync: EGG LIMIT hard rule + weekly cuisine rotation + scripts
- SKILL.md: add egg-limit hard rule (max 4/meal, one egg-meal/day), cuisine-rotation section
- scripts/pick_cuisine.py: seeded weekly world-cuisine picker (stable per week, no recent repeats)
- scripts/verify_eggs.py: regression guard for the egg cap
- README.md: full project documentation
- .gitignore: exclude cuisine-rotation.json + python caches (live data stays local)
2026-08-27 19:07:53 +01:00

144 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""Pick this week's world cuisine for the weekly shop.
Design goals:
- RANDOM per week, but STABLE within a week (seeded by the week's Monday date)
so that re-runs / retries of the Monday job never flip the cuisine mid-week.
- VARIED over time: the last few chosen cuisines are excluded from the pool.
- PERSISTED to cuisine-rotation.json so the daily meal job can read the active
cuisine and cook on-theme meals from the same week's buys.
Usage:
python3 pick_cuisine.py # pick (or reuse) and persist
python3 pick_cuisine.py --dry-run # print only, do not write
Prints a JSON line:
{"week_monday": "...", "cuisine": "...", "theme_items": [...], "reused": bool}
"""
import argparse
import json
import os
from datetime import date, timedelta
HERE = os.path.dirname(os.path.abspath(__file__))
ROTATION_FILE = os.path.join(HERE, "..", "cuisine-rotation.json")
# Curated cuisines: all are achievable at Lidl on a ~£30 budget, high-protein,
# and contain NO red meat by design. theme_items are cheap, cuisine-appropriate
# buys (proteins/veg/staples/condiments) that add variety; some may already be
# in inventory — the weekly job still cross-checks budget + Lidl availability.
CUISINES = {
"Mexican / Tex-Mex": [
"wholemeal tortilla wraps", "tinned black beans", "tinned sweetcorn",
"tinned chopped tomatoes", "cheddar cheese", "lime", "cumin",
"smoked paprika", "avocado", "chicken breast", "Greek yogurt",
],
"Greek / Mediterranean": [
"feta", "Kalamata olives", "cucumber", "cherry tomatoes",
"chickpeas", "halloumi", "dried oregano", "lemon", "red onion",
"tinned tuna", "Greek yogurt",
],
"Indian / South Asian": [
"red lentils", "chickpeas", "tinned tomatoes", "fresh spinach",
"curry powder", "grated ginger", "onions", "Greek yogurt",
"firm tofu", "microwave rice", "chicken breast",
], "Thai": [
"jasmine rice", "coconut milk (tin)", "soy sauce", "lime",
"grated ginger", "red curry paste", "frozen prawns", "edamame",
"fish-free: tofu", "fresh coriander", "eggs",
],
"East Asian / Japanese": [
"egg noodles", "soy sauce", "edamame (frozen)", "eggs",
"firm tofu", "sesame seeds", "tinned tuna", "miso paste",
"frozen mixed veg", "grated ginger", "spring onions",
],
"Middle Eastern / Levantine": [
"chickpeas", "red lentils", "feta", "cucumber", "tomatoes",
"lemon", "cumin", "pitta breads", "halloumi", "Greek yogurt",
"fresh parsley", "chicken breast",
],
"Italian": [
"passata", "wholemeal pasta", "mozzarella", "tinned tuna",
"cherry tomatoes", "fresh basil", "black olives", "spinach",
"eggs", "chicken breast", "parmesan",
],
"Korean": [
"jasmine rice", "soy sauce", "sesame oil", "sesame seeds",
"gochujang (tube)", "eggs", "firm tofu", "kimchi (jar)",
"frozen mixed veg", "grated ginger", "spring onions",
],
}
# How many recent cuisines to exclude so weeks don't repeat back-to-back.
RECENT_WINDOW = 4
def week_monday(d=None):
d = d or date.today()
return d - timedelta(days=d.weekday()) # Monday=0
def load_rotation():
try:
with open(ROTATION_FILE) as f:
return json.load(f)
except FileNotFoundError:
return {"current": None, "history": []}
def save_rotation(data):
os.makedirs(os.path.dirname(ROTATION_FILE), exist_ok=True)
with open(ROTATION_FILE, "w") as f:
json.dump(data, f, indent=2)
def pick(dry_run=False):
import random
monday = week_monday()
monday_str = monday.isoformat()
rot = load_rotation()
# Same week -> reuse the locked-in cuisine (stable within the week).
if rot.get("current") and rot["current"].get("week_monday") == monday_str:
rot["current"]["reused"] = True
print(json.dumps(rot["current"]))
return
pool = list(CUISINES.keys())
recent = [h["cuisine"] for h in rot.get("history", [])[-RECENT_WINDOW:]]
candidates = [c for c in pool if c not in recent] or pool
# Seed by the week's Monday so the pick is deterministic for that week
# (re-runs in the same week always return the same cuisine).
rng = random.Random(monday_str)
cuisine = rng.choice(candidates)
chosen = {
"week_monday": monday_str,
"cuisine": cuisine,
"theme_items": CUISINES[cuisine],
"reused": False,
}
if dry_run:
print(json.dumps(chosen))
return
history = rot.get("history", [])
history.append({"week_monday": monday_str, "cuisine": cuisine})
# Keep history bounded.
history = history[-12:]
save_rotation({"current": chosen, "history": history})
print(json.dumps(chosen))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--dry-run", action="store_true",
help="print the pick without persisting")
args = ap.parse_args()
pick(dry_run=args.dry_run)
if __name__ == "__main__":
main()