Repository navigation
feat(ios): one allergen and diet screen behind every offering surface - #187
Draft
obvious-autobuild[bot] wants to merge 24 commits into
Draft
obvious-autobuild[bot] wants to merge 24 commits into
obvious-autobuild[bot] wants to merge 24 commits into
Conversation
The claims schema has no low-carb field, so no catalog recipe can prove keto compliance, while the legacy pool enforces the .lowCarb tag mask — without this arm a keto user loses every legacy result yet keeps every catalog result. Fails-first: testKetoDietExcludesCatalogPoolFailingClosed reproduces the ungated catalog entry before the gate arm lands. Co-authored-by: Sam Gu <sgu07966@gmail.com>
Catalog entries materialize as nil-id legacy stand-ins, so Start Cooking rendered an empty guide, loadIngredients returned nothing, and Le Chef received zero ingredient context. Add catalogCookPacket(for:) and cookPacketIngredients(_:) on the repository — the single structured path — and route the preview drawer, the cooking guide, and the assistant context through it. Fails-first: the packet path had no repository API before this commit and testCatalogRecommendationYieldsCookPacketAndBridgedIngredients pins non-empty steps and bridged ingredient lines for a catalog result. Co-authored-by: Sam Gu <sgu07966@gmail.com>
TasteEventStoreTests declared cookedEvent -> TasteEvent while constructing a TasteEventRecord, which Linux Swift 6.4 rejects with 16 conversion errors (reproduced in the portable harness; fails-first). Ports trunk commit 9de2985 so the Linux portable checks compile on this lineage too. macOS CI compiles both forms; semantics are unchanged. Co-authored-by: Sam Gu <sgu07966@gmail.com>
… staples Review round on PR #178 (five findings), each pinned by a failing-first regression test in LeChefPlanWeeklyTests: - Blocked substitutes are never offered or applied: the snapshot drops swaps whose substitute is a household allergen, and the allocator refuses any swap whose substitute an eater blocks (previously only recipe lines were screened). - Stock is claimed within a dinner: a line's swap sees the pool minus what earlier lines of the same dinner already claimed, so the substitute line reports its real remainder instead of hiding it. - Leftover surplus is tracked per cooked dinner: a leftovers night re-serves one dish and must be covered by that dish's own surplus, never by portions pooled across different dishes. - Saved catalog dinners reload their numeric identity (the negative digit tail of the rcp- id) instead of 0, keeping replacement and exclusion working after a plan reload. - Catalog assumed_staple defaults yield to an explicit household pantry tier: an ingredient marked only_if_confirmed is never re-assumed. Verified: Linux scratch harness 48/48 (the six new/updated tests fail before the fixes and pass after). Hosted macOS CI runs the app-target suite. Co-authored-by: Sam Gu <sgu07966@gmail.com>
…y entries Of 215 vocabulary keys carrying only 'unknown', 210 now carry decisive composition classes with a recorded class_source: 47 from the suite allergen table, decisive USDA FoodData Central catalog categories for a further set, and per-key curation where catalog categories were unreliable (steak listed as vegetables, cinnamon as baked products). Ambiguous cases keep the 'unknown' fail-closed marker; five keys remain fully unknown (broth, dashi, green_curry_paste, massaman_curry_paste, stock). Mustard gains its allergen class. Co-authored-by: Sam Gu <sgu07966@gmail.com> Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com> Co-authored-by: Sam Gu <sgu07966@gmail.com> Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
…ims from evidence The bundle now carries what the app's diet and safety gates need instead of coarse proxies: - ingredients ship their vocabulary composition classes; the schema requires them. - thermometer checks cite their FSIS class and the validator checks the cited row's endpoint minimum instead of a coarse class floor, which falsely rejected valid ground-meat and leftovers checks. - authored timer_min metadata wins during compilation; text parsing remains the fallback (940 authored steps ship their exact seconds). - the compiler auto-places a charted FSIS check when a recipe's required classes are not covered by any authored safety block (none needed on the committed corpus). - vegetarian, vegan, gluten_free, and dairy_free claims are derived from line classes fail-closed: an authored true claim contradicted by a known blocking class fails validation; unknowns keep the authored value and warn. Recompiled bundle: 762 recipes, 328 ingredients, 468 thermometer checks, 1170 timered steps, derived true claims vegetarian 327 / vegan 121 / gluten_free 336 / dairy_free 306. Validator and contract checker pass; portable suite 37 passed, 1 skipped. Co-authored-by: Sam Gu <sgu07966@gmail.com> Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com> Co-authored-by: Sam Gu <sgu07966@gmail.com> Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
reach_report_v2.py resolves ten written household pantries (pantries-v2.json) against the app ingredient space by exact match only (snake_case and plural-fold candidates; every resolution rule recorded, misses disclosed) and counts, per pantry, recipes cookable with 0 and at most 2 missing required lines, split by the app's fail-closed diet gate. Shipped bundle vs this branch's build, medians across pantries (near tier, at most 2 missing lines): omnivore 129.5 -> 129.5, vegetarian 3 -> 83.5, vegan 0 -> 23.5, gluten_free 5.5 -> 60, dairy_free 3 -> 59.5. Identity reach is unchanged; the diet-gated reach gain comes from the composition classes carrying the claims. Co-authored-by: Sam Gu <sgu07966@gmail.com> Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
…-gates Resolved PlanDataAdapter.swift by keeping the weekly-plan catalog-id reconstruction (catalogDatabaseID for rcp- refs) and the goal-path dinner title fetch.
Every bundle ingredient now ships composition classes (dairy, meat, fish, plant, ...), pinned as the vocabulary's safety contract. The loader persists bundle-authoritative classes into the catalog vocabulary, a migration adds the classes column and best-effort backfills existing rows from the shipped bundle, and decoding falls back to [unknown] so older bundles fail closed. Co-authored-by: Sam Gu <sgu07966@gmail.com>
… gate HouseholdSafetyGate is the single screen every surface that offers a recipe or a swap applies: allergen groups map to composition classes, a restricted household is blocked by a restricted class, unresolved (unknown) composition blocks, and keto fails closed while no class expresses a carb threshold. Pescatarian screens land-animal classes — it has no diet claim to lean on. The resolver derives the household from the stored profile at query time (AllergenSupport keyword groups over seeded ingredient rows) and screens catalog recipe lines by canonical key classes, so lines the bridge cannot resolve are screened, not passed. Wired through: catalog exact and near-match recommendations, substitution offers and availability, cook-time swap rechecks, and weekly-plan catalog candidates (swaps reuse the screened service). Legacy candidates keep their existing ID-based checks; a failed profile or vocabulary read is not 'no restrictions' — it blocks. Co-authored-by: Sam Gu <sgu07966@gmail.com>
The sweep hydrates the shipped bundle on a fresh database and, for every allergen group, every diet, and a combined household, drives the recommendation, weekly-plan, and substitution surfaces and re-screens each returned recipe and offered substitute through the bundle's composition classes. Fail-closed unit tests cover unknown composition, keto, and pescatarian. Mutation checks prove the sweep can fail: an unknown-classed line removes a recipe from the surfaces, and a dropped group-to-class mapping would let a violating recipe through. The migration backfill is tested by inserting pre-classes vocabulary and migrating forward. Recommend and weekly-plan fixtures gain representative class data; the bridge-rebuild fixture deliberately stays classless to exercise the backfill. Co-authored-by: Sam Gu <sgu07966@gmail.com>
Co-authored-by: Sam Gu <sgu07966@gmail.com>
|
Important Review skippedBot user detected. To trigger a single review, invoke 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 |
…rst9 safety-gates # Conflicts: # apps/ios/Platform/Persistence/KitchenPlan/PlanDataAdapter.swift # apps/ios/Platform/Persistence/Services/SubstitutionService.swift
…e shipped bundle RecipeCatalogMigrations.swift used Bundle, Data, and JSONSerialization with only GRDB imported, which compiles on Linux but fails on the macOS app target with 'cannot find Bundle in scope'. Import Foundation in the app source and mirror the same import into the community-check copy, then refresh the pinned hashes.
… into obv/burst9-fridgeluck-safety-gates # Conflicts: # apps/ios/Platform/Persistence/KitchenPlan/PlanDataAdapter.swift # apps/ios/Platform/Persistence/Services/SubstitutionService.swift
loadCatalogCandidates requires the household profile and composition classes, but loadSnapshot still called it with only the database handle — a missing-arguments error parse checks cannot surface. Resolve the profile and classes on the snapshot's held connection (no repository re-entry) and share HealthProfile's canonical diet selection with the adapter instead of duplicating its priority order.
The stash pop onto the merged tree re-added the promoted static helper without removing the original private declaration, so the app target failed to compile with an invalid redeclaration. Keep the shared static.
The sweep test was written without a compilable host: the recommendation surfaces take Set<Int64> pantries, HealthProfile.dietaryRestrictions is a JSON array string rather than an array, and database reads/writes inside async test bodies resolve to GRDB's async overloads and need await; the unknown-class mutation check hoists its fetch out of XCTUnwrap's autoclosure, which cannot carry async calls.
loadSnapshot's read closure drives substitutions(for:in: db), whose household context and offer screening still re-entered the database queue (householdScreen's profile and vocabulary reads, verdict-time bridge lookups) — a held-transaction re-entry GRDB aborts with "Database methods are not reentrant". Add handle-based householdScreen and bridged screening variants that run every read on the caller's connection, and complete the persistence fixture's catalog-ingredient schema with the classes column the snapshot read now selects.
The screenedSubstitutions path still called substituteScreenVerdict without the bridged parameter; queue-safe callers read the bridge per query, so bridge() is correct here.
obvious-autobuild Bot
added a commit
that referenced
this pull request
Oct 16, 2026
…n land green Merge ab6c9f1 brought the safety-gates feature whose sweep suite has never run green anywhere: CI run 38856495680 fails 934 sweep assertions plus one substitution-screen case, and the rejoin line's repaired head bc155f2 (run 38855840833) fails the same signature — the breakage is base-borne on obv/burst9-fridgeluck-safety-gates, not introduced by this branch's merge. PRs #187/#188 own that repair; this branch is the LeChef socket E2E work. Reverting merge ab6c9f1 restores the aea3b8f state (alpha + goal-path tip merged), where GoalPathTests.testFridgeToMealToTasteGoalPath is green — the original blocker of this PR, fixed by carrying e87ee7b's pin of actual pantry/evidence behavior. Co-authored-by: Sam Gu <sgu07966@gmail.com> Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
The sweep drove PlanDataAdapter.loadSnapshot — which reads the household from the health_profile table — without ever seeding that table, so the weekly-plan pool screened unrestricted while the test-side re-screen used the restricted profile: blocked recipes were offered and keto candidates leaked. Seed the swept profile before loadSnapshot, screen rcp-* pool rows through the composition class screen and legacy rows (numeric ids from the weekly-plan merge) through the documented required-flagged-line filter, and guard the unrestricted pool reach instead of an exact-tier count the FDC bridge coverage cannot support in a fresh-install database. Restore the base's canonical substituteIsAllergen: the merge resolution had rewritten it into a composition recheck whose empty-bridge nil broke the explicit-id/no-diet contract (an unflagged substitute must stay allowed when no diet is active). The composition screen stays on the offering surfaces; the id/diet gate keeps the base's semantics. Flip the dropped-group-mapping mutation check to its achievable contract: the gate's group-to-class fallback keeps refusing a dropped mapping, so the mutation asserts both refusals survive. Also merge base tip e87ee7b (test-expectation alignment for the shipped bundles).
This was referenced Oct 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed and why
One household safety screen now sits behind every surface that offers a recipe or a swap: exact and near-match recommendations, substitution offers and availability, cook-time swap rechecks, and weekly-plan catalog candidates and swaps.
Why: the surfaces previously screened independently — keto failed closed in recommendations, but pescatarian had no class gate, allergens were only checked where the app-ingredient bridge resolved a line, substitutes and plan swaps carried no household screen, and catalog recipe lines the bridge could not resolve passed silently.
recipes-v2.jsoncarries composition classes (dairy,meat,fish,shellfish,egg,plant,grain, ...).RecipeBundleV2decodes them with an[unknown]fallback so older bundles fail closed; the loader persists bundle-authoritative classes; a migration adds the column and best-effort backfills pre-existing vocabulary from the shipped bundle.HouseholdSafetyGate(pure, inFeatureLogic/Safety) maps allergen groups to classes and screens lines: a restricted group's class blocks, unresolved (unknown) composition blocks a restricted household, keto fails closed while no carb threshold is selected, pescatarian blocks land-animal classes (fish and shellfish allowed — it has no diet claim to lean on).HouseholdSafetyProfileResolverderives the household at query time from stored allergen ingredient IDs viaAllergenSupportkeyword groups, and screens catalog lines by canonical-key classes, so lines the bridge cannot resolve are screened instead of passed.A test sweep (
Tests/SafetyGates/SafetyGatesSweepTests.swift) drives every surface for each allergen group, each diet, and a combined household, and re-screens each returned recipe and offered substitute through the bundle's classes. Mutation checks prove the sweep can fail: forcing a line's classes tounknownremoves a recipe from the surfaces, and a dropped group→class mapping would let a violating recipe through.How I checked it
RecipeBundleV2.swift)bash apps/ios/Tools/taste-store-check/Scripts/test.shbash apps/ios/Tools/community-check/verify.shswiftc -parseper fileSubstitutionService.swift, fixed)python3 data/recipes/tools/compile_bundle.py3401c7ae9d0a854284ed6cd04a7f88172cea92bda93960e84d2b67dd66f31eed, matchesmanifest.jsonHosted macOS CI status (head bd8af1b)
SafetyGatesSweepTests) compile and run green on hosted macOS CI. Runs:38857388989(pull_request) and38857395785(dispatch), both success atbd8af1b5a2dfbfb9b7a49e5818fbdeba172269ee.health_profilebefore drivingloadSnapshot(the weekly-plan pool was previously screened unrestricted), the weekly-plan pool screensrcp-*rows through the composition class screen while legacy rows keep their documented required-flagged-line filter,substituteIsAllergenis restored to the base's canonical id/diet contract (the composition screen stays on the offering surfaces), and the dropped-group-mapping mutation check asserts the gate's two surviving refusals.e87ee7b2(test-expectation alignment for the shipped bundles) is merged; its GoalPath/Weekly failures do not reproduce here.MERGEABLE/CLEANagainstobv/burst8-fridgeluck-goal-path.What I could not check
substitution-checkcompiles onlySubstitutionTable.swift, so it does not exercise the edited service; the service compiles first on hosted CI.Assumptions
obv/burst8-fridgeluck-goal-path(stacked, like PR [obvious-fleet] real-WebSocket cook-session E2E for phone and gateway, goal-path green #186); this branch carries the prerequisite merges ofobv/burst9-fridgeluck-catalog-data,obv/burst6-fridgeluck-catalog-recommend, andobv/burst6-fridgeluck-weekly-plan.Human author: Sam Gu (sgu07966@gmail.com)
🔗 Obvious Project · 🧵 Obvious Thread · 🔀 Obvious PR Artifact