#!/usr/bin/env python3 """ Regression guard for RATIONAL PORTIONS. Checks TODAY's meals in meal-history.json (override with --all for the 7-day window) for: 1. CAP EXCEEDED — a protein item used in a single meal above its inventory `max_per_meal`, or above the fallback cap when no explicit cap exists. 2. OVERLOAD — a single protein at >=70% of its cap that ALSO supplies >=85% of the meal's protein (a "pile of one ingredient", e.g. 10 sausages). A balanced plate where the lead protein is capped but other proteins/veg contribute is NOT flagged. Units are normalised to grams so items logged in different units (cheese in g vs inventory's "small block") compare correctly. Exits non-zero (prints violations) if any rule is broken. """ import json, sys, os, argparse from datetime import datetime, timedelta SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) HISTORY = os.path.join(SKILL_DIR, "meal-history.json") INV = os.path.join(SKILL_DIR, "inventory.json") # protein (g) per unit, for the dominance estimate PROTEIN_G = { "egg": 6, "sausage": 11, "mince": 0.18, "lentil": 0.24, "tofu": 0.12, "tempeh": 0.19, "bean": 9, "whey": 0.8, "cheese": 0.25, "halloumi": 0.21, "tuna": 25, "salmon": 25, "mackerel": 20, "chicken": 0.31, "turkey": 0.29, "prawn": 0.24, "yogurt": 0.10, "yoghurt": 0.10, "fish": 25, } # grams per inventory unit, for converting a stored cap to grams GRAMS_PER_UNIT = { "g": 1, "ml": 1, "kg": 1000, "count": 1, "can": 1, "small block": 120, "block": 120, "serving": 1, "portion": 1, "pack": 1, "jar": 1, "tub": 1, "bulb": 1, "bottle": 1, "loaf": 1, } PROTEIN_CATS = {"protein", "dairy"} def cap_grams(name, unit, inv): it = next((i for i in inv["items"] if i["name"].lower() == name.lower()), None) if it and "max_per_meal" in it: gpu = GRAMS_PER_UNIT.get(it.get("unit", unit), 1) return it["max_per_meal"] * gpu rp = inv.get("diet", {}).get("rational_portions", {}) defaults = rp.get("default_caps", {}) for kw, cap in defaults.items(): if kw in name.lower(): gpu = GRAMS_PER_UNIT.get(rp.get("unit_fallback_unit", unit), 1) return cap * gpu fb = rp.get("unit_fallback", {}).get(unit, 6) return fb * GRAMS_PER_UNIT.get(unit, 1) def est_protein_g(name, qty, unit): per = PROTEIN_G.get(name.lower()) if per is None: for kw, v in PROTEIN_G.items(): if kw in name.lower(): per = v break if per is None: return 0.0 gpu = GRAMS_PER_UNIT.get(unit, 1) base = qty * gpu # grams of food # per is g protein per g of food for g/ml units; per-unit for count/can if unit in ("g", "ml", "kg"): return per * base return per * qty def main(): ap = argparse.ArgumentParser() ap.add_argument("--all", action="store_true", help="check full 7-day window") args = ap.parse_args() inv = json.load(open(INV)) hist = json.load(open(HISTORY)) entries = hist if isinstance(hist, list) else hist.get("entries", []) today = datetime.now().strftime("%Y-%m-%d") cutoff = datetime.now() - timedelta(days=7) violations = [] for e in entries: try: d = datetime.strptime(e["date"], "%Y-%m-%d") except Exception: continue if args.all: if d < cutoff: continue else: if e["date"] != today: continue meal_items = e.get("items", []) # 1) cap check for it in meal_items: cap = cap_grams(it["name"], it.get("unit", "count"), inv) used = est_protein_g(it["name"], it["qty"], it.get("unit", "count")) # compare the RAW quantity against the cap's raw quantity (both in same unit) cap_raw = cap / GRAMS_PER_UNIT.get(it.get("unit", "count"), 1) if it.get("unit") in ("g", "ml", "kg") else cap # simpler: compare grams of food used vs grams of cap food_g = it["qty"] * GRAMS_PER_UNIT.get(it.get("unit", "count"), 1) cap_food_g = cap if food_g > cap_food_g + 1e-6: violations.append( f"CAP EXCEEDED {e['date']} {e['meal']} '{e['name']}': " f"{it['name']} {it['qty']} {it.get('unit','')} exceeds cap " f"~{cap_food_g:.0f} g ({it.get('unit','')})") # 2) overload check (single protein >=70% cap AND >=85% of meal protein) prot = [(it, cap_grams(it["name"], it.get("unit", "count"), inv), est_protein_g(it["name"], it["qty"], it.get("unit", "count"))) for it in meal_items] total = sum(p for _, _, p in prot) or 1 for it, cap, pg in prot: food_g = it["qty"] * GRAMS_PER_UNIT.get(it.get("unit", "count"), 1) if cap and food_g > 0.7 * cap and pg > 0.85 * total: violations.append( f"OVERLOAD {e['date']} {e['meal']} '{e['name']}': " f"{it['name']} {it['qty']} is >=70% of its cap and supplies " f"{pg:.0f}/{total:.0f} g ({pg/total*100:.0f}%) of meal protein " f"— not a balanced plate") if violations: print("FAIL: rational-portion violations:") for v in violations: print(" -", v) sys.exit(1) scope = "all logged meals (7d)" if args.all else f"today ({today})" print(f"PASS: portions rational — no overload ({scope}).") if __name__ == "__main__": main()