diff --git a/README.md b/README.md index 4957f17..b5a8ca8 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,10 @@ every save, so the client sends none, and reads both back to check them. The ser ingredient's grams toward the batch only if all of the food's measures are type `Weight` (what the web client saves for a serving with a gram weight); with an `Atomic` one, its nutrients are added but its grams aren't, so the batch weight comes out short. Custom foods are therefore saved with -`Weight` servings, and recipes refuse other ingredients. No hard-delete endpoint for foods is known, so +`Weight` servings, and recipes refuse other ingredients. Don't fix an older food by switching an +`Atomic` measure to `Weight`: the server stores a diary entry as a count of its measure, with a `Weight` unit +weighing its value in grams and an `Atomic` one 100 g, so the switch rescales every entry logged by +it (180 g logged by an `Atomic` "g" read 1.8 g after switching it to `Weight`). No hard-delete endpoint for foods is known, so `delete_custom_food` sets `retired` instead (the food leaves search after a few seconds but stays readable by id, and diary entries that use it are kept). Retired foods can't be edited. Serving names are sent exactly as given with an explicit quantity, diff --git a/src/cronometer_api_mcp/client.py b/src/cronometer_api_mcp/client.py index b9d7169..870d70a 100644 --- a/src/cronometer_api_mcp/client.py +++ b/src/cronometer_api_mcp/client.py @@ -88,6 +88,16 @@ _RECIPE_FIXED_MEASURES = _WEIGHT_UNIT_MEASURES | {_FULL_RECIPE_MEASURE} # The web client saves a serving with a gram weight as "Weight"; "Atomic" is a # count unit (a supplement "tablet"). See _check_recipe_ingredient. +# +# The server stores a diary entry as a count of its measure, and reports its +# grams as count x the measure's value for "Weight", but count x 100 for +# "Atomic" (whatever its value). Logging by grams, as this client does, is +# right either way: the count is derived by the same rule. Editing a measure later re-reads every +# entry logged by it (verified live, Oct 13, 2026): resizing a Weight measure +# rescales those entries, resizing an Atomic one doesn't, and switching a +# measure's type multiplies them by value/100 (Atomic to Weight) or 100/value +# (Weight to Atomic): 180 g logged by an Atomic "g" became 1.8 g. Never change +# a logged measure's type. _WEIGHED_MEASURE = "Weight" # Recipe serving weights are sent rounded to this many decimals, and read back # within _GRAMS_TOL: a batch that doesn't divide evenly (4561 g / 6) would @@ -893,9 +903,10 @@ def update_custom_food( alone leaves the stored per-100g values as they are, so the food's per-serving numbers shift with the new weight. - Resizing a measure rescales every diary entry logged by it (seen - live), so the measure is picked by _custom_food_serving, never guessed, - and a "g" or "oz" measure is never renamed or resized. + Resizing a "Weight" measure rescales every diary entry logged by it + (see _WEIGHED_MEASURE), so the measure is picked by + _custom_food_serving, never guessed, and a "g" or "oz" measure is never + renamed or resized. Measure types are re-sent unchanged. A measure is `amount` units of `name` weighing `value` grams in total, and the app renders the serving as " ". Setting the @@ -2469,7 +2480,10 @@ def _check_recipe_ingredient(food: dict) -> None: f"Food {food['id']} ({food['name']!r}) can't be a recipe ingredient: " f"its measures {[(m['name'], m['type']) for m in others]} aren't type " f"'Weight', and Cronometer leaves such a food's grams out of a " - f"recipe's batch weight, which would inflate its per-100 g nutrients" + f"recipe's batch weight, which would inflate its per-100 g nutrients. " + f"If it's your own food, don't switch an 'Atomic' measure to 'Weight' " + f"to fix this: Cronometer re-reads every diary entry logged by it at " + f"value/100 of its grams. Make a new custom food for recipes instead" ) diff --git a/tests/test_recipe_edit.py b/tests/test_recipe_edit.py index d5d7b45..d845727 100644 --- a/tests/test_recipe_edit.py +++ b/tests/test_recipe_edit.py @@ -1847,6 +1847,7 @@ def test_create_recipe_refuses_unweighable_ingredient_before_saving(tmp_path, fo f"Food {LEGACY_ID} ('ZZ custom') can't be a recipe ingredient: its measures" ) assert "leaves such a food's grams out of a recipe's batch weight" in msg + assert "don't switch an 'Atomic' measure to 'Weight'" in msg assert state["endpoints"] == ["get_foods"] # nothing created assert srv.foods == {}