Skip to content

feat(substitutions): canonical-key substitution table over the catalog vocabulary with real allergen rechecks - #177

Draft
obvious-autobuild[bot] wants to merge 2 commits into
obv/burst6-fridgeluck-catalog-recommendfrom
obv/subs-canonical-keys-v2
Draft

obvious-autobuild[bot] wants to merge 2 commits into
obv/burst6-fridgeluck-catalog-recommendfrom
obv/subs-canonical-keys-v2

Conversation

@obvious-autobuild

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

Copy link
Copy Markdown

Bridges PR #114's reviewed substitution table onto the base branch's canonical ingredient vocabulary (329 entries) instead of the legacy app ingredient-id space, and re-arms the cook-mode allergen recheck with real composition facts.

Why

The shipped substitution resource indexes pairs by legacy app ingredient IDs and offers only 35 of 279 reviewed pairs. The reviewed table no longer depends on the app's id space; the catalog's canonical keys are the stable join. The legacy wiring also made cook mode report every substitute as unknown composition ({ _ in nil }), so proposal-driven swaps always failed closed — correctly, but uselessly.

What changed

  • scripts/substitutions/convert.py maps pair endpoints to canonical keys via exact evidence only (canonical names, recorded aliases, fdc-id equality, recorded USDA source names), failing closed on ambiguous/conflicting matches. Self-swaps created by canonicalization and duplicate (original, substitute, role) offers are dropped (first pairId wins). Emits a per-key composition map with the compound allergen/diet closure (vocabulary curated classes ∪ mapped facts store); keys nothing covers fail closed to compositionKnown: false with empty class lists. Regeneration is deterministic (byte-identical from the pinned inputs).
  • substitutions-v2.json ships 70/279 reviewed pairs covering 126/317 endpoints (legacy: 35 pairs / 40 ingredients), 50 swaps with known substitute composition, 209 dropped pairs recorded with named unmapped-key audit.
  • SubstitutionTable (Swift) decodes schema 2 canonical-key resources and rejects non-v2; lookup by canonical original key; screening refuses naming the forbidden class, fails unknown composition to ask-the-user under any active restriction, and unknown restriction ids add no forbids.
  • SubstitutionService loads v2, translates app ingredient ids ↔ canonical keys through the catalog identity bridge (legacy id → canonical keys → ingredient rows, min id for determinism), assembles deterministic offers, and answers the cook-mode substituteIsAllergen recheck from real composition facts. CookingGuideView is wired to it.
  • substitution-check tool + XCTest cover the semantics; README documents the v2 regen flow.

How I checked

  • python3 -m pytest scripts/substitutions/ -q → 32 passed (canonical evidence, ambiguity/conflict fail-closed, self-swap/dup rejection, composition merge + screening, CLI, generated resource).
  • Regen determinism: reconverted from the pinned pairs + vocabulary → cmp byte-identical.
  • bash apps/ios/Tools/substitution-check/run.sh (Linux swiftc, compiles the production table + checker against the real resource) → OK — 70 swaps, 126 mapped endpoints, 329 composition keys, sweep allowed=597 askUser=260 refused=53. Includes: no self-swaps/dups, composition coverage for every endpoint, recheck↔compositions agreement, vegan refusal names dairy (yogurt), soy-free refusal + gluten-free allowance (tofu), egg→banana resolves, schema gate rejects v1, determinism.
  • swiftc -parse clean on all five changed Swift files.

Not checked here

  • No Xcode in this environment: SubstitutionService.swift, CookingGuideView.swift, and SubstitutionTableV2Tests.swift were syntax-checked (-parse) but not type-checked or run — only SubstitutionTable.swift was fully compiled (with the checker) on Linux. macOS CI is the real gate for the app target and the XCTest target.
  • Base PR [obvious-fleet] feat(ios): recommend from the 763-recipe catalog through the real engine #172's CI was still failing (pre-existing ReverseScanService.swift/ReverseScanMealView.swift errors on its head) when this branch was cut from 4ba2043; this PR's diff doesn't touch those files.

Assumptions

  • One canonical key can bridge to several app ingredient rows (source aliases); offers pick the min id deterministically, and substituteKey rides along in the legacy Substitution model for display.
  • A swap screens askUser even when the only active restrictions are ids the table doesn't know (an active dietary context exists; unknown blocks). Known-composition swaps stay allowed under such ids.
  • v2 ships no butter substitute; the reviewer dairy example uses yogurt (6 shipped swaps), which carries the same compound closure.
  • Legacy Substitution.substituteId semantics preserved: the resolved app row id, not a canonical key string.

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

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

CI on head 63f8301

  • build-and-test run 38816535943: fail — inherited from the base stack. The only error is RecipeCatalogRecommendTests.swift:179:7: cannot find 'CookThermometer' in scope, the pre-existing failure on base PR [obvious-fleet] feat(ios): recommend from the 763-recipe catalog through the real engine #172 (its current head fails the same check). No error names any file this PR changes: the log shows SubstitutionTable.swift compiling in FLFeatureLogic, and SubstitutionService.swift, CookingGuideView.swift, and the substitutions-v2.json bundle copy all completing in the FridgeLuck app target. Because the test-target batch aborts on the base error, SubstitutionTableV2Tests.swift was in the compile job but its full macOS type-check is not confirmed by this run; the Linux checker's full compile of the table and 13 XCTest cases remain the portable evidence.
  • dead-code-advisory: skipped (upstream job gate). CodeRabbit: pass (bot not eligible).

Packet ref: this work is also published as the fleet packet obv-pr/burst6-fridgeluck-subs-canonical (code branch obv/burst6-fridgeluck-subs-canonical, same head 63f8301). If the platform opens a PR from that packet, prefer it and close this one.

ObviousApp and others added 2 commits October 15, 2026 22:20
…able

Map PR #114's reviewed substitution pairs onto the base branch's canonical
ingredient vocabulary instead of the legacy app ingredient-id space:
exact name/alias/fdc-id/USDA-reference evidence only, fail-closed on
ambiguous or conflicting matches, self-swaps and duplicate key/role
offers dropped, per-key composition map with the compound allergen/diet
closure, unknown composition fail-closed to known=false.

Regenerated resource: substitutions-v2.json ships 70/279 reviewed pairs
covering 126/317 endpoints (the legacy app-id table shipped 35/40) with
50 swaps carrying known substitute composition; 209 pairs drop with a
named unmapped-key audit. 32 converter tests; regeneration from the
pinned pairs+vocabulary is byte-identical.

Co-authored-by: Sam Gu <sgu07966@gmail.com>
…llergen rechecks

SubstitutionTable decodes schema-2 canonical-key resources (rejects v1),
looks swaps up by canonical original key, and screens the substitute's
composition per eater: forbidden classes refuse naming the class,
unknown composition fails closed to ask-the-user under any active
restriction, unknown restriction ids add no forbids.

SubstitutionService loads the v2 resource, translates between app
ingredient ids and canonical keys through the catalog identity bridge
(legacy id -> canonical keys -> ingredient rows), assembles deterministic
offers, and answers the cook-mode recheck from real composition facts —
the cooking guide no longer reports unknown for every substitute.

substitution-check v2 pins audit counts, canonical-key integrity,
recheck/compositions agreement, the all-pairs x all-restrictions sweep,
and the reviewer compound-closure examples (yogurt carries dairy, tofu
carries soy); 13 XCTest cases cover table semantics on an inline
miniature document.

Co-authored-by: Sam Gu <sgu07966@gmail.com>
@coderabbitai

coderabbitai Bot commented Oct 15, 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: e9b19c48-2247-4b42-b8d9-cbdf306fbe83

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.

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