Skip to content

feat(ios): one allergen and diet screen behind every offering surface - #187

Draft
obvious-autobuild[bot] wants to merge 24 commits into
obv/burst8-fridgeluck-goal-pathfrom
obv/burst9-fridgeluck-safety-gates
Draft

obvious-autobuild[bot] wants to merge 24 commits into
obv/burst8-fridgeluck-goal-pathfrom
obv/burst9-fridgeluck-safety-gates

Conversation

@obvious-autobuild

@obvious-autobuild obvious-autobuild Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

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.

  • Bundle contract — every ingredient in recipes-v2.json carries composition classes (dairy, meat, fish, shellfish, egg, plant, grain, ...). RecipeBundleV2 decodes 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.
  • Gate — HouseholdSafetyGate (pure, in FeatureLogic/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).
  • Resolver — HouseholdSafetyProfileResolver derives the household at query time from stored allergen ingredient IDs via AllergenSupport keyword groups, and screens catalog lines by canonical-key classes, so lines the bridge cannot resolve are screened instead of passed.
  • Fail-closed reads — a failed profile or vocabulary read is not treated as "no restrictions"; the offering surface blocks. Legacy candidates keep their existing ID-based checks; the new class screen covers catalog-backed paths.

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 to unknown removes a recipe from the surfaces, and a dropped group→class mapping would let a violating recipe through.

How I checked it

Check Command Result
Linux taste-store suite (compiles the edited RecipeBundleV2.swift) bash apps/ios/Tools/taste-store-check/Scripts/test.sh 50/50 pass after re-pin
Community-check mirror hashes bash apps/ios/Tools/community-check/verify.sh all OK
Syntax check of every changed Swift file swiftc -parse per file all parse (caught a real guard trailing-closure parse error in SubstitutionService.swift, fixed)
Bundle reproduction python3 data/recipes/tools/compile_bundle.py SHA-256 3401c7ae9d0a854284ed6cd04a7f88172cea92bda93960e84d2b67dd66f31eed, matches manifest.json
Whole-bundle class-screen reach (preliminary, bundle-level) offline count pescatarian 475/762 recipes; allergen-only 448 (wheat/gluten) – 755 (mustard); keto threshold candidates 160/200/294 at 15/20/30 g carbs

Hosted macOS CI status (head bd8af1b)

  • build-and-test: pass — the app target and the full test suite (including SafetyGatesSweepTests) compile and run green on hosted macOS CI. Runs: 38857388989 (pull_request) and 38857395785 (dispatch), both success at bd8af1b5a2dfbfb9b7a49e5818fbdeba172269ee.
  • Earlier red rounds on this branch (0a815db and earlier) were sweep-fixture and merge-contract defects, all fixed on this head: the sweep now seeds the swept household into health_profile before driving loadSnapshot (the weekly-plan pool was previously screened unrestricted), the weekly-plan pool screens rcp-* rows through the composition class screen while legacy rows keep their documented required-flagged-line filter, substituteIsAllergen is 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.
  • The base tip e87ee7b2 (test-expectation alignment for the shipped bundles) is merged; its GoalPath/Weekly failures do not reproduce here.
  • PR state: MERGEABLE / CLEAN against obv/burst8-fridgeluck-goal-path.

What I could not check

  • No device run — CI verifies the simulator suite only.
  • SwiftLint was not run locally (not installed in the Linux sandbox); repo lint runs in CI.
  • substitution-check compiles only SubstitutionTable.swift, so it does not exercise the edited service; the service compiles first on hosted CI.
  • Keto reach counts are bundle-level, not app-query results — with the threshold constant still unset, the keto pool is empty by design (fail closed).

Assumptions

  • Base is 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 of obv/burst9-fridgeluck-catalog-data, obv/burst6-fridgeluck-catalog-recommend, and obv/burst6-fridgeluck-weekly-plan.
  • The keto carb-threshold constant stays unset in this PR; setting it is a one-line follow-up once a threshold is chosen (candidates above).
  • Nothing merged; draft for review.

Human author: Sam Gu (sgu07966@gmail.com)

🔗 Obvious Project · 🧵 Obvious Thread · 🔀 Obvious PR Artifact

ObviousApp and others added 14 commits October 16, 2026 01:33
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>
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7c9c9b59-c6dc-4f6f-af74-a5c5fb17daef

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.

…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.
ObviousApp and others added 5 commits October 16, 2026 04:22
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).
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