Skip to content

update_custom_food: set the default serving's quantity - #66

Merged
rwestergren merged 4 commits into
rwestergren:mainfrom
ATECHPCS:custom-food-serving-quantity
Oct 8, 2026
Merged

rwestergren merged 4 commits into
rwestergren:mainfrom
ATECHPCS:custom-food-serving-quantity

Conversation

@ATECHPCS

@ATECHPCS ATECHPCS commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A measure is amount units of name weighing value grams in total, and the app renders a serving as <amount> <name>. update_custom_food only writes name and value, so it cannot fix a food whose default measure has a quantity other than 1.

Example from a real account: a custom food whose default measure had quantity 400 and type "Weight". After update_custom_food(food_id, serving_name="1 serving", serving_grams=250) the stored measure was

{"id": 310790759, "name": "1 serving", "amount": 400, "value": 250, "type": "Weight"}

get_food_details reads that back as 1 serving = 250 g, but the iOS editor showed the serving as "400 1 serving" with no serving weight.

Change

  • serving_grams now also resets the measure's quantity to 1 and its type to "Atomic", the same shape create_custom_food writes.
  • New optional serving_quantity sets the quantity explicitly (alone, or together with serving_grams).
  • A plain number leading serving_name ("1 serving", "2 cookies") is moved into the quantity, unless serving_quantity is given. The server already does this for a new measure: add_custom_food(serving_name="10 pieces") sends amount: 1.0 and reads back as amount: 10, name: "pieces". It does not do it when an existing measure is re-sent, so an update with "1 serving" would render as "1 1 serving".
  • Any other serving_name alone still only renames.
  • serving_grams and serving_quantity must be positive finite numbers.
  • get_food_details reports each measure's quantity and type, so the result can be checked.

Verification

  • Against the live API: re-sending the food through /api/v2/add_food with amount: 1, type: "Atomic" on the existing measure id returned SUCCESS, and get_food read back the same measure id with the new quantity, type, name and weight. The other measures and the per-100g nutrients were unchanged.
  • One existing assertion changes: serving_name="1 cup" now stores name cup with quantity 1.
  • New tests in tests/test_custom_food_edit.py; ruff check and the full suite pass (162 tests).

ATECHPCS and others added 4 commits October 7, 2026 13:32
A measure is `amount` units of `name` weighing `value` grams, and the
app renders the serving as "<amount> <name>". update_custom_food only
wrote name and value, so a food first saved as "400 g" kept amount=400
and type "Weight" and showed as "400 1 serving" with no weight.

Setting serving_grams now resets the quantity to 1 and makes the measure
a named serving; serving_quantity sets it explicitly. get_food_details
reports each measure's quantity and type.
The server stores a new measure named "10 pieces" as amount=10,
name="pieces", but keeps the name verbatim when an existing measure is
re-sent, so "1 serving" on an update rendered as "1 1 serving". Do the
same split on update; an explicit serving_quantity wins.
Report quantity, unit, and total gram weight per portion while keeping servings as the number of portions consumed.
A quantity parsed from serving_name is now checked like an explicit serving_quantity, so an overflowing number can't be saved as inf. The docs now state that serving_quantity without serving_grams keeps the serving's total weight.
@rwestergren
rwestergren merged commit 1c6776b into rwestergren:main Oct 8, 2026
1 check passed
ColinToft added a commit to ColinToft/cronometer-api-mcp that referenced this pull request Oct 12, 2026
- _split_quantity_led_name follows the server's rule for a leading
  quantity: integer, decimal or n/d before a space, so "1/2 cup" is sent
  as amount 0.5, name "cup" (verified live: stored that way with no
  re-send). The verbatim re-send now only fires for mid-name fractions.
- _apply_serving: a bare rename keeps the quantity ("2 cookies" ->
  "2 biscuits"), except on a unit-count measure such as "400 g", which
  resets to 1. update_custom_food keeps resetting to 1 when the weight is
  set (upstream rwestergren#66).
- update_recipe gains serving_quantity, e.g. to repair a mangled serving.
- create_recipe checks the name, and a failure after the recipe exists
  raises a CronometerError naming its food_id and pointing to
  update_recipe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ColinToft added a commit to ColinToft/cronometer-api-mcp that referenced this pull request Oct 12, 2026
* Add update_recipe and delete_recipe tools

Recipes are foods with an ingredients array, saved through the same
/api/v2/add_food upsert as custom foods. Verified against the live API on
throwaway recipes: re-sending a recipe with its id edits it in place (food
id, measure ids and untouched fields survive), a re-sent ingredient list
replaces the old one wholesale, and the server re-derives nutrients and the
"full recipe" weight from the ingredients on every save.

- update_recipe(food_id, ingredients, name, serving_name, serving_grams,
  comments): partial in-place edit. Refuses database foods, plain custom
  foods, serving-based recipes, and serving edits on the g/oz/full recipe
  measures.
- delete_recipe(food_id): retires the recipe, like delete_custom_food. No
  hard-delete endpoint exists among 19 v2 names and 9 v3 paths probed.
- get_food_details now returns a recipe's ingredients (in update_recipe's
  input shape), comments, and retired flag.
- create_recipe's ingredient parsing/aggregation is shared with
  update_recipe; non-finite grams are now rejected. The recipe tools reject
  unknown ingredient keys instead of ignoring them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Trim comments and docstrings in recipe edit change

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Address review: fold recipe delete into delete_custom_food, server-side derivation

- Drop client-side recipe nutrient aggregation and the "full recipe"
  rewrite: verified live that the server derives both on create and on
  update even when sent `nutrients: []`.
- Reject serving names that collide with the built-in g/oz/full recipe
  measures (case-insensitive, after quantity splitting).
- Resolve a recipe's serving as its non-fixed measure rather than
  defaultMeasureId, which Cronometer moves to the last-logged measure.
- One _split_quantity_led_name helper for custom foods and recipes; a zero
  leading number is now an error in both.
- One _get_user_food(food_id, kind) guard. delete_custom_food now retires
  recipes too; the separate delete_recipe tool and retire_recipe are gone.
- Diary-propagation claims in the update tools' docs verified live.
- Round-trip test: get_food_details ingredients back into update_recipe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep serving quantity on resize; simplify user-food guard

- update_recipe(serving_grams=...) alone no longer resets the serving's
  quantity ("2 bowls" stayed "1 bowls"); only serving_name sets it.
- _get_user_food(food_id) does just the source check; the edit paths
  check for ingredients inline.
- Round-trip fixture documents why ingredient value == grams: an app-made
  recipe stores 33 g of milk on a 100 g "cup" measure as value 33.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep recipe serving names verbatim on create; one serving rule

- create_recipe reads the recipe back and re-sends the serving verbatim
  when the server re-parsed its name. Verified live: for a new measure
  with amount 1 the server splits off a leading number or an n/d fraction
  (d 2-5) followed by a space, e.g. "serving (1/2 batch)" -> amount 0.5,
  name "batch)"; "serving (1/6 batch)" is kept. Existing measures are
  stored verbatim. update_recipe verifies its read-back the same way.
- Batch weight is read back from the server's "full recipe" measure.
- Shared _apply_serving: a bare serving_name means quantity 1, weight
  unchanged, for update_custom_food and update_recipe alike.
- A non-positive leading number ("0 calorie syrup") stays part of the
  name again; blank names and overflowing numbers still raise.
- _get_user_food(food_id, recipe=...), shared _rename and checks.
- Tests run against a stateful FakeFoodServer that mimics the observed
  upsert, re-parse and derivation behaviour.
- Drop the probe-output .gitignore entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Split leading fractions; bare renames keep the portion count

- _split_quantity_led_name follows the server's rule for a leading
  quantity: integer, decimal or n/d before a space, so "1/2 cup" is sent
  as amount 0.5, name "cup" (verified live: stored that way with no
  re-send). The verbatim re-send now only fires for mid-name fractions.
- _apply_serving: a bare rename keeps the quantity ("2 cookies" ->
  "2 biscuits"), except on a unit-count measure such as "400 g", which
  resets to 1. update_custom_food keeps resetting to 1 when the weight is
  set (upstream rwestergren#66).
- update_recipe gains serving_quantity, e.g. to repair a mangled serving.
- create_recipe checks the name, and a failure after the recipe exists
  raises a CronometerError naming its food_id and pointing to
  update_recipe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Stop parsing serving names client-side; revert update_custom_food

- Recipe serving names are sent exactly as given (stripped) with an
  explicit serving_quantity (default 1). After every save the serving is
  read back; if the server re-parsed a new measure's name it is re-sent
  verbatim once, compared with == on the name and math.isclose on the
  amount. Verified live for "serving (1/6 batch)", "serving (1/2 batch)",
  "1 1/2 cups", "cups" x1.5 and "serving (1/3 batch)".
- A bare rename in update_recipe keeps the quantity. Removes
  _split_quantity_led_name, _apply_serving and _UNIT_COUNT_MEASURES.
- update_custom_food is back to origin/main's behaviour; only the shared
  _get_user_food guard and _rename touch it.
- _get_user_food refuses retired foods unless allow_retired (retire).
- add_recipe gains serving_quantity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Refuse quantity-led recipe serving names; verify grams and nutrients

- Recipe serving names starting with a quantity ("2 bowls", "1/2 cup",
  "1 1/2 cups") are refused with a pointer to serving_quantity; names with
  a fraction inside ("serving (1/8 batch)") stay valid.
- The serving read-back also compares grams (math.isclose), and every
  create/update checks that the server derived energy (208 > 0) for a
  non-empty batch, raising with the food_id otherwise. create_recipe and
  add_recipe return serving_grams.
- serving_grams heavier than the batch is refused before saving, on create
  and update (including ingredient edits that shrink the batch).
- update_custom_food uses the shared _check_positive / _check_name, so a
  blank rename now fails.
- FakeFoodServer derives per-100g nutrients, with a switch to simulate a
  save that derived none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Accept zero-calorie recipes; refuse serving-based ones everywhere

- The derived-nutrient check now means "energy is present": a water-only
  recipe reads back with energy 0 (verified live), which is valid. Uses
  NUTRIENT_IDS["energy"]; drops the dead zero-grams early return.
- A derivation failure after create raises RecipeNotDerivedError and says
  to check the ingredients in the app; other post-create failures still
  point to update_recipe for that id.
- Serving-based recipes ("Recipe"-type measures) are refused in
  _get_user_food, so retire_custom_food refuses them too.
- update_recipe returns servings_per_batch before/after, and the tool docs
  say a "serving (1/N batch)" needs serving_grams recomputed when the
  ingredients change.
- _full_recipe_grams shares _is_fixed_recipe_measure's normalisation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Send empty nutrients on update; servings_per_batch; simpler guards

- update_recipe sends nutrients [] like create_recipe, so the read-back
  energy check proves the server derived them (verified live: an update
  with [] read back 143 kcal/100 g for an egg-only batch). FakeFoodServer's
  derive_nutrients=False now keeps whatever was sent.
- Serving names may not start with a digit, "." or a vulgar fraction
  (catches "½ cup", ".5 cup", "2x bowls", "1,5 cups", "2", ...).
- servings_per_batch on create_recipe / update_recipe (and tools), mutually
  exclusive with serving_grams: serving = ingredient total / N, so a
  "serving (1/N batch)" stays 1/N when the ingredients change.
- One batch-weight model: the ingredient-gram sum. Drops the full-recipe
  read-back and OddWeightServer.
- The post-create wrapper catches only CronometerError and says the recipe
  exists (id N); don't re-create it, without suggesting update_recipe.
- delete_custom_food documents why serving-based recipes are refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Name the recipe id on any post-create failure; refuse ambiguous serving edits

- create_recipe wraps every exception after the recipe exists in one
  CronometerError ("The recipe exists (id N); don't re-create it. ...")
  chained from the cause; RecipeNotDerivedError is gone.
- update_recipe refuses serving edits when the recipe has more than one
  non-fixed measure, whatever the default is.
- Fix the stale per-100g docstring line and drop the total_grams alias.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Resolve the recipe serving lazily; refuse fraction drift; round serving grams

- update_recipe only resolves the serving when a serving field is passed or
  new ingredients change the batch weight, so renames and notes work on a
  recipe with a drifted default or no named serving. The result only
  reports the serving when it was resolved.
- Changing the batch weight without servings_per_batch or serving_grams is
  refused, naming both ways to keep the serving; an ambiguous serving is
  refused too.
- _recipe_serving is simply "the one non-fixed measure"; the editing flag
  is gone.
- servings_per_batch rounds to 0.01 g, and read-back grams compare with a
  0.01 g absolute tolerance (live: 151 g / 6 stored as 25.17).
- Pure argument checks move to _check_serving_args.
- get_food_details: note why comments uses .get (absent when unset, live).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Document how diary entries follow a recipe serving resize; simplify quantity check

- Live: an entry logged by a recipe's serving measure follows a resize
  (50 g became 100 g, still 1 serving). update_recipe's docstrings now say
  so precisely (nutrients per gram update; serving-measure entries rescale;
  g/oz entries untested), and the result carries a `note` whenever the
  serving's grams change.
- Leading-quantity check is now `isnumeric() or "."`, replacing the
  hand-listed _QUANTITY_START set; "⅟2 cup" and "↉" are rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* State verified diary behaviour for g-logged entries after a recipe resize

Live, side by side on one throwaway recipe: after a serving resize the
entry logged by the serving measure went from 50 to 100 g, while the entry
logged by "g" stayed 50 g. The update_recipe docstrings and result note now
say exactly that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Let a rounded-up whole-batch serving fit; document multi-serving refusal

- _check_serving_fits allows a serving up to _GRAMS_TOL over the batch, so
  servings_per_batch=1 on a batch like 523.3868 g (serving 523.39 g) is no
  longer refused, on create or update.
- update_recipe docstrings: on a recipe with several named servings, any
  batch-weight-changing ingredient edit is refused and must be done in the
  app. Fix a stray line break in the tool docstring.
- Index food["measures"] directly in _recipe_servings and _get_user_food.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Restore the _GRAMS_TOL serving-fits check

The previous commit accidentally shipped a mutation-test variant of
_check_serving_fits (the old relative isclose), so its own N=1 tests failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Prove derivation on the settle retry; guard app-set batch weights; wrap update read-back

- _settle_recipe_serving's re-send clears nutrients again, so the second
  read-back also proves the server derived them.
- update_recipe refuses, before any write, a recipe whose stored "full
  recipe" weight differs from its ingredient total by more than 0.01 g
  (set in the app); the server's id-0 placeholder is skipped.
- Any failure after update_recipe's save is re-raised as "Recipe N was
  saved; verify with get_food_details. ..." chained from the cause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants