Skip to content

fix(meals): catalog meals reach Apple Health and use up Kitchen stock - #213

Merged
SamGu-NRX merged 5 commits into
alphafrom
lead/fridgeluck-catalog-meal-effects
Oct 17, 2026
Merged

SamGu-NRX merged 5 commits into
alphafrom
lead/fridgeluck-catalog-meal-effects

Conversation

@SamGu-NRX

@SamGu-NRX SamGu-NRX commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

What changed

A meal made from one of the 822 catalog recipes now has the same effects as a legacy meal. Its nutrition reaches Apple Health and Progress's daily totals, and cooking it from Home takes its ingredients out of the Kitchen. Log a Meal also stops logging the same plate twice. Base is alpha 0907d91 (merged in), head 58a44c6. The fix is commit 9d5e1cc. Cited lines without a link are at af85600, the alpha this branch started from.

1. Apple Health gets the numbers Progress shows

Cause. MealLogSyncCoordinator.syncLoggedMeal always computed nutrition with NutritionService.macros(for: recipeId), the legacy ingredient math. A catalog meal lands on a recipes row that only carries the title and has no ingredient rows, so that math returned zero for every field. Both callers hit it: Log a Meal (ReverseScanMealView.swift:1051) and the Home cook's celebration screen (CookingCelebrationView.swift:360). The Recent Meals list was right because UserDataRepository.computeConsumedMacros reads the frozen per-revision snapshot through recipes_cooking_catalog_refs first.

Fix. The coordinator now reads RecipeRepository.catalogNutrition(forHistoryID:) first (MealLogSyncCoordinator.swift:41). That is the same join computeConsumedMacros uses, and the coordinator scales it by servings eaten times portion, as that function does. Meals without a catalog link keep the legacy math. The snapshot has no sugar field, so sugar stays 0 and the HealthKit writer skips that sample instead of recording an invented value. The coordinator still returns early unless the user has authorized Apple Health, and both callers still sync only after the meal log commits.

2. Progress counts catalog meals in today's total and the weekly chart

Cause. A simulator walk at af85600 logged a 944-calorie catalog meal. Recent Meals listed it, and Progress read "0 / 2,000 cal". todayMacros() (UserDataRepository.swift:385-414) and the chart's dailyMacroTotals (:316-380) each ran one SQL sum over recipes ⋈ recipe_ingredients. A catalog meal's title-only row has no recipe_ingredients, so the inner join dropped it. Home's dashboard reads the same todayMacros().

Fix. Both now list the history rows in their window and add up computeConsumedMacros for each (UserDataRepository.swift:362). That is the function behind the meal list, and the Health export reads the same snapshot, so all three agree. For legacy meals the per-meal sum is the same arithmetic as the old SQL. NutritionReportingTests, PortionLoggingTests and SwapLoggingTests still pin those totals.

3. Cooking a catalog recipe takes its lines out of the Kitchen

Cause. MealLogService.logMeal always ran InventoryRepository.applyConsumption, which reads recipe_ingredients for the resolved legacy row. For a catalog meal that row has no ingredients, so nothing came out.

Fix. A catalog meal now passes a CatalogMealLog (id, revision, and what the meal does to stock) instead of two loose optionals (MealLogService.swift:18). When the stock choice is .cookedHere, logMeal runs the new InventoryRepository.applyCatalogConsumption inside the same write transaction as the history row, the catalog link and the taste events (MealLogService.swift:161, InventoryRepository.swift:325). This mirrors the legacy cook: required lines only, grams scaled by servingFactor (servings eaten × portion ÷ recipe servings), lots consumed soonest-expiry first with one consume event per lot, all committed once at completion. The celebration screen passes .cookedHere (CookingCelebrationView.swift:87).

The outcome now reports one CatalogLineConsumption per required line (Inventory.swift:186), with its bridged ingredient ids, requested grams, grams taken per ingredient and a status: used, notInKitchen, noBridge or noGramAmount.

4. The deduction rule for lines that bridge to several ingredients, or to none

Catalog lines name vocabulary keys. They reach stock only through recipes_catalog_ingredient_bridge, the table RecipeCatalogIngredientBridge builds and the household safety boundary reads.

  • Several bridged ingredients. The line is one need that any of them can meet. The recipe search already reads it that way when it sums stock across the bridged ids (RecipeRepositoryCatalogRecommend.swift:126). The cook takes the line's grams from the lots the Kitchen actually holds for any of those ingredients, soonest expiry first across all of them (InventoryRepository.swift:775). An ingredient the Kitchen doesn't hold is never touched.
  • No bridge rows. No lot can stand for the line, so it takes nothing and reports noBridge.
  • No weight ("to taste", a count with no grams). It takes nothing and reports noGramAmount.
  • Never invent stock. No line takes more than its lots hold. A short Kitchen reports the shortfall, and no lot is created or driven negative.

Swaps play no part in catalog deduction. PersonalizationService.recordCooking already rejects a swap on a catalog meal, because the title-only row has no required ingredients to swap, and the cook guide builds no swaps for catalog recipes.

5. Log a Meal still leaves the Kitchen alone for a catalog pick

A photographed plate may not have come from this Kitchen, so a catalog pick on Log a Meal passes .leaveKitchen (ReverseScanMealView.swift:54). Nothing changes for legacy picks on that screen. They still take what the "Log Meal · Deduct N items" preview shows (601d827, e47360a, MealDeductionPreviewTests). The two pick types now behave differently on that screen, and only the catalog side defaults to no deduction. Making legacy picks match, or adding an opt-in "I cooked this" choice, is a product decision this PR doesn't make.

6. The meal screen's ingredient breakdown lists a catalog pick's lines

The breakdown card read only recipe_ingredients, so a catalog pick always showed "No ingredient amounts to show for this recipe." It now reads the catalog cook packet and lists required lines that state a weight, named by the catalog vocabulary and scaled over the catalog recipe's servings (ReverseScanResultsSections.swift:99). Rows now use their position as identity, since an unbridged line has no ingredient id.

7. Log a Meal logs a plate once

Cause. After "Meal logged", "Confirm & Log Meal" was enabled again. Its only guard was isLoggingMeal (ReverseScanMealView.swift:1024, :736), which a defer reset as soon as the log finished (:1034). A second tap wrote a second cooking_history row, synced it to Health again and, for a legacy pick, deducted again.

Fix. A small MealLogSubmission value tracks ready, logging and logged (ReverseScanResultsSections.swift:244). The log starts only from ready (ReverseScanMealView.swift:1049). Success leaves the button off and titled "Meal Logged" until a new photo is analyzed (:996), and a failed log returns to ready so the user can retry.

Why GoalPathTests:303 changes

The assertion "the catalog-cooked meal leaves pantry stock untouched today" came from e87ee7b (2026-10-16), in the goal-path walk that landed through #182 and the release-candidate PR #188. The walk originally asserted pantryAfter < pantryBefore ("logging the meal consumed pantry stock"). That assertion could not pass, because catalog cooks deducted nothing. The commit message calls this "the inventory-deduction gap tracked as a feature-sized follow-up" and pins the behavior the code had, so the walk could go green. It didn't record a decision that catalog cooks should leave stock alone. This PR is that follow-up, so the test goes back to its original intent, more strictly. The pantry total must fall by exactly the grams the line results report, and at least one line must be used (GoalPathTests.swift:297). It runs over the shipped catalog and real bridge data.

Alternatives weighed

  1. Chosen: deduct through the bridge from catalog lines. Stock leaves in the same transaction, units and lot order as a legacy cook, and the rule for multi-bridge lines matches how the search already judges supply. The cost is a second consumption path beside the legacy one. Both share consumeLots and servingFactor.
  2. Rejected: copy catalog lines into recipe_ingredients for the meal's row, then reuse applyConsumption unchanged. One bridged id would have to stand for each line before the Kitchen is consulted, so a line bridged to scallion and green-onion tops would ask for whichever id was picked and miss the one the user holds. It would also write the legacy rows the bundle apply promises never to write, and Progress would start reading that row's ingredient math alongside the snapshot.
  3. Rejected for Health: compute export nutrition from the bridged ingredients' per-100 g rows. That disagrees with Progress whenever a line is unbridged or unweighed, which is exactly the coverage the frozen snapshot already accounts for.

How it works

flowchart LR
  C[Catalog meal logged] --> L{Cooked here?}
  L -- "Home cook" --> B[Each required line<br/>through the bridge]
  B --> K[Held lots,<br/>soonest expiry first]
  L -- "Log a Meal pick" --> N[Kitchen unchanged]
  C --> S[Frozen per-serving snapshot]
  S --> P[Progress]
  S --> H[Apple Health]
Loading

Progress and Apple Health read the same snapshot. Only a Home cook touches the Kitchen.

UI changes

  • Log a Meal, Ingredient Breakdown card, catalog pick: "No ingredient amounts to show for this recipe" becomes a list of the recipe's weighed required lines, such as "Egg 70 g" for one large serving.
  • Log a Meal, after a successful log: the primary button reads "Meal Logged" and is disabled. It used to return to "Confirm & Log Meal".
  • Progress and Home: today's calories and macros and the weekly chart include catalog meals.

The before state for Progress is in the af85600 walk captures (alpha-af85600-18, -19), which are local files and not attached. Nobody ran a simulator walk on this branch, so there are no after captures. Layout and motion are unchanged.

Verification

Tests, and evidence they failed before the fix. Two commits add tests alone, written against alpha's API, and CI ran each before the fix landed:

  • 504744a, run 38941266108: build-and-test failed with 23 assertion failures across 6 of the 7 new CatalogMealEffectsTests (both exports recorded 0 kcal against 1,148 and 1,230 expected, no lot changed, the breakdown read noIngredientList). GoalPathTests failed at its restored pantry assertion (940 g before and after). The meal-photo guard passed, as it should.
  • 1d78fb2, run 39039316655: build-and-test failed, with testProgressTodayAndWeekCountACatalogMeal reading 0 kcal for today and for the chart's last bar against 1,148 expected.

The fix commit then moves the tests to the new API and adds what the old API couldn't express.

New in Tests/CatalogMealEffectsTests.swift, over a synthetic bundle hydrated through the production loader:

  • testCatalogPickExportsTheNutritionProgressShows and testCatalogCookFromHomeExportsTheNutritionProgressShows: each logs the way its screen does and syncs to a recording Apple Health fake. Calories, protein, carbs and fat must equal the meal's Progress entry, and fiber and sodium must equal the snapshot × servings × portion (2 × 1.4 and 3). Before the fix both recorded 0 kcal.
  • testCookingACatalogRecipeTakesTheScaledGramsFromTheHeldLots: 3 servings of a 2-serving recipe take 150 g egg (60 g from the sooner lot, 90 g from the later), 450 g rice and 30 g green onion. The optional butter stays. Consume events carry cooking_celebration:<recipeId>.
  • testMultiBridgeLineDrawsFromTheIngredientTheKitchenHolds: green_onion bridges to two ingredients and only the second is held. 20 g comes from that lot, and no lot appears for the other.
  • testShortLineTakesOnlyWhatIsHeld: a 300 g rice line with 100 g held takes 100 g and reports a 200 g shortfall. An egg line with no egg held reports notInKitchen.
  • testLineWithNoBridgeTakesNothingAndSaysSo: saffron has no bridge rows and reports noBridge, with nothing taken. The Kitchen falls only by the egg line. Before the fix the outcome had no per-line report at all, so the test version on 504744a couldn't express "says so" and leaves this test out.
  • testMealPhotoCatalogPickLeavesTheKitchenAlone: the screen's choice is .leaveKitchen, and logging a pick with it leaves lots and consume events untouched. This one guards existing behavior, so it passed before the fix too.
  • testMealScreenBreakdownListsACatalogPicksWeighedRequiredLines: one large serving lists Egg 70 g, Green onion 14 g, Cooked rice 210 g and Saffron 0.7 g.
  • testProgressTodayAndWeekCountACatalogMeal: two large servings of a 410 kcal snapshot give 1,148 kcal today and on the chart's last bar, with protein, carbs and fat to match. The other six days stay 0. The two export tests also check the Health record against todayMacros().
  • testLogAMealSubmitsOncePerPhoto and testAFailedLogCanBeRetried: after a success, begin() refuses and the button stays off, and after a failure it starts again. MealLogSubmission is new, so these have no run against alpha. The evidence for the bug is the alpha code cited in section 7.

GoalPathTests covers the shipped-catalog cook, as described above. CatalogCookFlowTests and MealLogPickerTests change only their call sites.

CI. build-and-test on 58a44c6: run 39040760175 passed. That covers the full unit suite and the UI journey, which cooks a catalog recipe from Home and now deducts during it. Nothing was built locally.

Not checked. Nobody has written to HealthKit on a device. The tests use a recording fake, so they don't show that the Health app displays the samples.

Risk

  • Log a Meal now treats catalog and legacy picks differently, as section 5 describes.
  • MealFinalizationViewModel (the LeChef finalization sheet) still logs through recordCooking and legacy applyConsumption, so a catalog meal finished there would take nothing. No app screen constructs that sheet today. Only its tests do.
  • Progress now computes nutrition per meal in Swift instead of one SQL sum. That is one nutrition query per meal in the chart's date range. Nobody has timed it on a device with a long history.
  • recipes_catalog_lines keeps only the installed revision, so deduction reads today's lines even for a meal linked to an older revision. That only matters if a bundle update lands between opening the cook guide and finishing it.

Checklist

  • The scope is focused; related changes belong together. Every item is a catalog meal (or a meal log) failing to have the effect a legacy meal has.
  • I explained what changed and why
  • I included before/after images for visible changes. Not included: no simulator walk ran on this branch. The three visible changes are described under UI changes.
  • N/A: video for motion or interaction changes. The PR adds no motion.

🤖 Generated with Claude Code

Written against alpha's API, before the fix, so CI shows each one failing:
a catalog meal logged from the picker or cooked from Home exports zero
nutrition, cooking one takes nothing out of the Kitchen, and the meal
screen's ingredient breakdown is empty for a catalog pick. The meal-photo
test is a guard and passes. GoalPathTests stops pinning an unchanged
pantry after a catalog cook.

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

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 779c7551-ff69-4b7d-8e2b-20c641ecf530

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

SamGu-NRX and others added 4 commits October 17, 2026 09:06
Against alpha's code, so CI shows it failing: todayMacros and
dailyMacroTotals sum only the legacy recipe_ingredients join, and a
catalog meal's title-only recipes row adds nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Apple Health export reads the meal's frozen catalog snapshot, the same
  per-serving numbers Progress uses, scaled by servings and portion.
  It used the legacy ingredient math, which is zero for the title-only
  recipes row a catalog meal lands on.
- Progress today and the weekly chart sum each meal's consumed macros
  instead of one legacy recipe_ingredients join, so catalog meals count.
- Cooking a catalog recipe from Home takes its required lines out of the
  Kitchen through the ingredient bridge, in the meal log's transaction.
  A line bridged to several ingredients draws from the held lots of any
  of them, soonest expiry first. A line with no bridge or no weight takes
  nothing and reports why. No line takes more than its lots hold.
- A catalog pick on Log a Meal leaves the Kitchen alone, and the screen's
  ingredient breakdown lists the catalog lines.
- Log a Meal logs once per photo. A second tap after "Meal logged" wrote
  a second history row.
- GoalPathTests asserts the catalog cook's pantry drop again; e87ee7b
  pinned the missing deduction as a known follow-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@SamGu-NRX
SamGu-NRX marked this pull request as ready for review October 17, 2026 14:44
@SamGu-NRX
SamGu-NRX merged commit 6bcbfc9 into alpha Oct 17, 2026
3 checks passed
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T14:46:06.085769Z 58a44c6 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@codemetrics-ai

Copy link
Copy Markdown

Note

Review skipped

Codemetrics is installed on this repository, but it hasn't been added to your team yet.

To enable AI code reviews for this repo, go to your dashboard and add FridgeLuck to your team's repositories.

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.

1 participant