Repository navigation
fix(meals): catalog meals reach Apple Health and use up Kitchen stock - #213
Conversation
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>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
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>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
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. |
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
alpha0907d91 (merged in), head58a44c6. 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.syncLoggedMealalways computed nutrition withNutritionService.macros(for: recipeId), the legacy ingredient math. A catalog meal lands on arecipesrow 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 becauseUserDataRepository.computeConsumedMacrosreads the frozen per-revision snapshot throughrecipes_cooking_catalog_refsfirst.Fix. The coordinator now reads
RecipeRepository.catalogNutrition(forHistoryID:)first (MealLogSyncCoordinator.swift:41). That is the same joincomputeConsumedMacrosuses, 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'sdailyMacroTotals(:316-380) each ran one SQL sum overrecipes⋈recipe_ingredients. A catalog meal's title-only row has norecipe_ingredients, so the inner join dropped it. Home's dashboard reads the sametodayMacros().Fix. Both now list the history rows in their window and add up
computeConsumedMacrosfor 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,PortionLoggingTestsandSwapLoggingTestsstill pin those totals.3. Cooking a catalog recipe takes its lines out of the Kitchen
Cause.
MealLogService.logMealalways ranInventoryRepository.applyConsumption, which readsrecipe_ingredientsfor 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,logMealruns the newInventoryRepository.applyCatalogConsumptioninside 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 byservingFactor(servings eaten × portion ÷ recipe servings), lots consumed soonest-expiry first with oneconsumeevent per lot, all committed once at completion. The celebration screen passes.cookedHere(CookingCelebrationView.swift:87).The outcome now reports one
CatalogLineConsumptionper required line (Inventory.swift:186), with its bridged ingredient ids, requested grams, grams taken per ingredient and a status:used,notInKitchen,noBridgeornoGramAmount.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 tableRecipeCatalogIngredientBridgebuilds and the household safety boundary reads.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.noBridge.noGramAmount.Swaps play no part in catalog deduction.
PersonalizationService.recordCookingalready 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 adeferreset as soon as the log finished (:1034). A second tap wrote a secondcooking_historyrow, synced it to Health again and, for a legacy pick, deducted again.Fix. A small
MealLogSubmissionvalue 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 beused(GoalPathTests.swift:297). It runs over the shipped catalog and real bridge data.Alternatives weighed
consumeLotsandservingFactor.recipe_ingredientsfor the meal's row, then reuseapplyConsumptionunchanged. 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.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]Progress and Apple Health read the same snapshot. Only a Home cook touches the Kitchen.
UI changes
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:
build-and-testfailed with 23 assertion failures across 6 of the 7 newCatalogMealEffectsTests(both exports recorded 0 kcal against 1,148 and 1,230 expected, no lot changed, the breakdown readnoIngredientList).GoalPathTestsfailed at its restored pantry assertion (940 g before and after). The meal-photo guard passed, as it should.build-and-testfailed, withtestProgressTodayAndWeekCountACatalogMealreading 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:testCatalogPickExportsTheNutritionProgressShowsandtestCatalogCookFromHomeExportsTheNutritionProgressShows: 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 carrycooking_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 reportsnotInKitchen.testLineWithNoBridgeTakesNothingAndSaysSo: saffron has no bridge rows and reportsnoBridge, 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 againsttodayMacros().testLogAMealSubmitsOncePerPhotoandtestAFailedLogCanBeRetried: after a success,begin()refuses and the button stays off, and after a failure it starts again.MealLogSubmissionis new, so these have no run against alpha. The evidence for the bug is the alpha code cited in section 7.GoalPathTestscovers the shipped-catalog cook, as described above.CatalogCookFlowTestsandMealLogPickerTestschange only their call sites.CI.
build-and-teston58a44c6: 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
MealFinalizationViewModel(the LeChef finalization sheet) still logs throughrecordCookingand legacyapplyConsumption, so a catalog meal finished there would take nothing. No app screen constructs that sheet today. Only its tests do.recipes_catalog_lineskeeps 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
🤖 Generated with Claude Code