Skip to content

Overhaul: reviewed substitution table (data milestone 1) - #114

Draft
SamGu-NRX wants to merge 27 commits into
alphafrom
overhaul/substitution-table
Draft

SamGu-NRX wants to merge 27 commits into
alphafrom
overhaul/substitution-table

Conversation

@SamGu-NRX

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

Copy link
Copy Markdown
Owner

Why

SPEC overhaul-20261011 §3.5 requires substitutions to come from a reviewed, context-specific table — ingredient pair, recipe context, quality note, macro delta, allergen check — with LeChef reasoning beyond it only when empty and saying so, and §3.1 requires every substitution to recheck all eaters' allergens including compound ingredients (soy sauce contains wheat, pesto contains nuts), describing the difference honestly and never promising "no change". Current production behaviour is a hardcoded 27-pair map in SubstitutionService.buildMap(). This PR ships the reviewed table, the package that serves it safely, and an auditable coverage report.

This round resolves the BLOCK review at 0dc3bdf (both blockers, all lesser findings) and implements the committed fix list (docs/audits/ingredient-facts/PR-114.md on overhaul/ingredient-facts): the canonical ingredient-facts source is integrated fail-closed.

What changed

  • data/substitutions/pairs.json — the reviewed table: 279 cited pairs across all 11 functional roles, a 381-ingredient registry with USDA SR Legacy identities, 23 registry sources, 13 restriction definitions. Per pair: functional role, ratio with evidence status and basis, preparation changes, one plain-language difference sentence (never "no change"), allergen/diet classes for both sides with compound expansion, and the roles where the swap fails. All 27 production pairs from PR feat(evaluation): substitution evidence harness — 27 pairs, 227 recipe contexts, Swift replay #49's frozen snapshot are represented; conflicts are recorded, not silently resolved.

Review-fix round (BLOCK review + fix list)

  • Canonical composition source adopted (fix list §1). Commit 7b85bec is a history-preserving merge of overhaul/ingredient-facts (choosing the merge over vendoring so the package and dataset history stays joined and attributable). packages/FLIngredientFacts + data/ingredient-facts are now the canonical composition referee; the table's class lists are evidence labels kept in step with it.
  • Rechecks are store-backed and fail closed (fix list §2). Every ingredient record carries an explicit facts_key — exact-name mapping against the store's identities, never fuzzy: 123 of 381 records map; 258 map to no canonical record and fail closed. store.evaluate(ingredient:restrictions:) returns allowed / violated(reason) (the reason names the offending component) / unknown. Unknown composition is ask-the-user and never offered — a missing record or an unreviewed one can no longer pass every restriction check (the old fail-open ingredients[key]?.classes ?? [] pattern is gone). RestrictionSet keeps its name validation; its forbids are now the shared-vocabulary FLRestrictions; the both-sides recheck stays (a swap is offered only when the substitute screens clean).
  • Blocker (a) — taxonomy reconciled with the store. lard carries meat_whole_cut_pork, fish_sauce and worcestershire carry flesh_fish, oyster_sauce carries flesh_shellfish — the classes vegetarian/vegan forbids actually screen. The fix list's second branch was chosen over adding animal_product to the forbids lists because the canonical diet facts mark butter, eggs and honey vegetarian-allowed; the marker stays an evidence label, not a forbid.
  • Blocker (b) — tamari separated from product evidence. Generic tamari is soy-certain / gluten-unknown: its unevidenced "wheat-free soy sauce" alias is dropped, the note says labels vary, and rechecks return ask-the-user for gluten-free eaters — a note to read the bottle cannot substitute for the recheck. A certified gluten-free tamari is a distinct product-form entry (tamari-certified-gluten-free in data/ingredient-facts) carrying label evidence, and it evaluates allowed.
  • Finding (c) — non-circular tests. ReviewerExamplesTests pins the review's examples as first-class named cases (see Proof). The sweep gained an independent store-outcome sweep: wherever the store settles a violation for a substitute, the candidate must be refused across all pairs × restrictions — it does not derive expectations from the same class lists the implementation consumes. Household unions screen both axes in one FLRestrictions.
  • Finding (d) — mass arithmetic only on mass ratios. Macro deltas exist exclusively on weight/weight-cooked rows: 148 deltas were nulled with explicit macro_delta_null_reason (dimensionally invalid multiplication; partial butter-to-applesauce also omits the retained-fat component), 34 mass-basis deltas remain and are recomputed by the tests. Sodium stays in mg inside the grams-internally contract; no boundary conversion was invented. Alternative roles get role-aware scaling: a supported_also role exposes only its own recorded ratio (nil today — no such row has an evidenced ratio), and scaling refuses to run off the primary ratio.
  • Finding (e) — conditional rows are separate from functional failures. Five failure rows are kind: "conditional" with named restrictions (e.g. seitan as a protein: refused for gluten-free eaters, offered otherwise); a sourced ratio can no longer override a role failure, and a conditional refusal does not degrade the swap's own verdict.
  • Finding (f) — the report's any-role association limit is preserved (coverage still counts a line when any connected role verifies), and the generator loads the canonical facts dataset (sibling data/ingredient-facts by default, --facts to override) with a new composition-status section: the formerly invisible 211 empty-class-list records now count as their own class — 124 records resolve in the canonical store, 257 are composition-unknown — so the unknown bulk shrinks visibly as records gain reviewed facts.
  • Label alignment found by the store sweeps: coconut-milk records drop the stale tree_nut label per the store's composition verdict (closed composition of coconut and water — tree-nut free; raw coconut stays unknown in the store and fails closed).
  • SCHEMA.md documents schema 1.1.0 (facts_key, fl mappings, fails[].kind/restrictions, role-specific supported_also ratios, the mass-statement scaling contract, canonical-recheck semantics, the tamari product-form rule, the taxonomy alignments).

Boundaries: only data/substitutions/ and packages/FLSubstitutions/ were touched this round, plus the merge commit. overhaul/ingredient-facts was consumed read-only (merged, never pushed to). No workflows, app code, settings, or other PRs touched. iOS-native behaviour is UNVERIFIED-NATIVE (no Xcode/macOS in this environment).

Tradeoffs

  • Restricted coverage honestly collapsed (see Proof): dairy-free covered lines went 265 → 90, gluten-free 299 → 24, soy-free 278 → 0. The old numbers were an artifact of rechecks that never screened compositions. The new numbers say what is actually true: most substitutes have no canonical composition record, so restricted suggestions fail closed until the 257 shrink. Unrestricted coverage is unchanged (299/781, 38.3%).
  • Fail-closed costs convenience by design: an unknown composition is a question to the user, not a silent pass. Restoring coverage requires reviewed facts (upstream task's dataset), not code leniency.
  • 112 of 279 pairs are verified (sourced function and sourced ratio); the rest rank accordingly — quality over count, unknown stays unknown.
  • Suite C contract over a richer local vocabulary: wheat rides on gluten; wheat-specific medical notes live in allergen_note. Diet and FSIS meat/flesh classes are documented extensions that never enter the shared nine.
  • Linux-verified only. swift build/swift test pass on Linux; anything iOS-native is unverified here and labelled UNVERIFIED-NATIVE.

Proof

Environment: Linux sandbox, SwiftPM, no Xcode. No CI claims — CI run ids are referenced, not asserted. Fix-1 commands ran at the pushed head c7784c0; fix-2 commands at a6cc83e.

Review round 2, fix 2 — alternative-role nutrition is unknown without per-role evidence (packages/FLSubstitutions):

At the fix-2 head a6cc83e (append-only on top of c7784c0):

$ cd packages/FLSubstitutions && swift build && swift test
Build complete!
	 Executed 54 tests, with 0 failures (0 unexpected) in 15.569 (15.569) seconds

54 = the fix-1 49 (all kept green, unmodified) + 5 new AlternativeRoleNutritionTests.

Red-first proof at c7784c0 (the tests alone, before the source fix):

$ cd packages/FLSubstitutions && swift test --filter AlternativeRoleNutritionTests
	 Executed 5 tests, with 15 failures (0 unexpected) in 0.358 (0.358) seconds

4 of the 5 fail with the leak reproduced on real data: the egg (binder) → mashed-banana candidate requested as LIQUID returns the binder's recorded delta (kcal -18, protein -5.6, carbs 13.3, fat -4.6, fiber 1.6, sugar 0.0, sodium -70.4), as do its leavening and sweetener requests, and the synthetic evidenced-liquid case returns the primary's delta (kcal -40, protein -3.6, …) instead of computing from the liquid ratio.

The fix: SwapCandidate.macroDelta is role-aware. For the primary role it returns the pair's recorded delta unchanged; for an alternative role it computes the SCHEMA.md macro-delta method from THAT role's own evidenced ratio — mass statements only (review finding d: a volume or quarter-cup-per-egg ratio is never a mass multiplier), unknown/unsupported amounts are not computation inputs — and reports unknown (nil) until the role's amount has evidence. The reviewer's pin — egg as a binder to mashed banana, requested as a liquid — reads as unknown nutrition, never the binder delta reskinned. No result-type change was needed: Macros? nil is the unknown representation.

The new cases (asserting the RETURNED delta on the nutrition path, not merely a stored ratio):

  • testEggBinderToMashedBananaRequestedAsLiquidHasUnknownNutrition — the reviewer's exact case: liquid, leavening (functionally failed), and sweetener requests are all unknown on the shipped table.
  • testAlternativeRoleWithEvidencedRatioComputesFromThatRatio — the shipped table has no evidenced alternative-role ratio (every supported_also row is ratio-less), so a synthetic schema-1.1.0 fixture carries a sourced liquid mass ratio (0.5 per gram, 50 g typical): the returned delta is computed from THAT ratio (kcal -75, protein -4.4, carbs 2.5, fat -4.7, fiber 0.0, sugar 2.5, sodium -42.5), distinct from the primary's recorded -40, -3.6, 9.5, -4.3, 0.7, 7.4, -32; the math is shown in the test.
  • testPrimaryRoleRequestsKeepTheRecordedDeltaAcrossTheWholeTable — primary-role requests return pair.macroDelta unchanged for every pair in the shipped table.
  • testAlternativeRoleWithNonMassRatioHasUnknownNutrition — review finding d on the alternative path: a quarter-cup-per-egg ratio never becomes nutrition arithmetic.
  • testAlternativeRoleWithUnsupportedAmountHasUnknownNutrition — an amount recorded unsupported is not evidence to compute from.

No legacy test encoded the primary-delta leak: the pre-existing macro-delta tests exercised Pair.macroDelta directly, and no test asserted a candidate's alternative-role delta — so all 49 stayed green unmodified; nothing needed re-pointing to the reviewer's contract.

Coverage report unchanged — regenerated at this head through the tool and diffed byte-identical against the committed data/substitutions/REPORT.md; per the only-if-output-changes rule nothing was regenerated into the tree. Environment note: one in-tree release build died on an llbuild assertion (BuildEngine.cpp:281) mid-swift run -c release; rebuilt in a clean build path and the tool then ran to exit 0 with identical output (deterministic by construction).

Review round 2, fix 1 — needs-confirmation never counts as usable (packages/FLSubstitutions):

$ cd packages/FLSubstitutions && swift build && swift test
Build complete!
	 Executed 49 tests, with 0 failures (0 unexpected) in 12.884 (12.884) seconds

49 = the prior 43 (all kept green) + 6 new NeedsConfirmationSeparationTests.

The reviewer's exact case, first-class — testButtermilkToWholeMilkUnderVegetarianNeedsConfirmationAndIsNeverUsable: buttermilk-liquid-to-whole-milk under vegetarian has needsConfirmation == true (original side askUser: buttermilk has no canonical facts record), verdict verified, substitute outcome allowed — and is demonstrably no longer classified as verified/usable: absent from swaps(...) and from classifiedSwaps(...).usable, landing in .needsConfirmation.

The preserved property, first-class — testKnownViolationOriginalWithKnownSafeSubstituteStaysUsable: butter-fat-to-olive-oil under vegan has original violated ("'Butter' is not compatible with the vegan diet"), substitute allowed, removes dairy + animal_product, and stays usable/removing; the needs-confirmation bucket does not swallow it.

The mechanics (same commit): Lookup.classifiedSwaps separates every candidate into usable / needs-confirmation / refused — disjoint, complete, each bucket in the candidates' rank order (asserted across the deterministic lookup battery × restriction batteries × all roles, together with swaps == usable); swaps returns exactly the usable bucket; verifiedSwaps excludes needs-confirmation so coverage reporting cannot count unconfirmed composition as verified evidence.

Coverage report regenerated by the tool, not hand-edited (the tool's output changed at this head, per the fix-list rule):

$ swift run -c release --package-path packages/FLSubstitutions flsub-report \
    --table data/substitutions/pairs.json \
    --recipes apps/ios/Resources/data.json \
    > data/substitutions/REPORT.md

The restricted-coverage drop is the honest post-fix number, not a regression: the store fails closed for reviewed ingredients whose records lack a fact for the axis — e.g. evaluate(Butter, gluten-free) = unknown ("diet 'glutenFree' fact for 'butter' is missing or unknown"), same for olive oil/milk under egg-free — so those swaps are needs-confirmation questions, never verified coverage. Diet facts for dairy-free/vegan/vegetarian are broadly recorded, so those rows stay meaningful. Unrestricted coverage is unchanged.

Eater scope Before fix 1 (00c36b4) After fix 1 (c7784c0)
No restrictions 299 (38.3%) unchanged
dairy-free 90 58
vegan 90 90
vegetarian 113 113
gluten-free 24 0
egg-free / fish-free / nut-free / peanut-free / sesame-free / shellfish-free / tree-nut-free 21 each 0 each
soy-free 0 0

Round 1 proof (head 00c36b4) — kept for the record:

$ cd packages/FLSubstitutions && swift build && swift test
Test Suite 'AllergySweepTests' passed ... Executed 4 tests, with 0 failures (0 unexpected)
Test Suite 'CompoundAllergenTests' passed ... Executed 7 tests, with 0 failures (0 unexpected)
Test Suite 'DeterminismTests' passed ... Executed 2 tests, with 0 failures (0 unexpected)
Test Suite 'RatioScalingTests' passed ... Executed 5 tests, with 0 failures (0 unexpected)
Test Suite 'ReviewerExamplesTests' passed ... Executed 8 tests, with 0 failures (0 unexpected)
Test Suite 'RoleMismatchTests' passed ... Executed 5 tests, with 0 failures (0 unexpected)
Test Suite 'SchemaValidationTests' passed ... Executed 8 tests, with 0 failures (0 unexpected)
Test Suite 'SuiteCContractTests' passed ... Executed 4 tests, with 0 failures (0 unexpected)
Test Suite 'All tests' passed at 2026-10-12 02:56:02.455
	 Executed 43 tests, with 0 failures (0 unexpected) in 12.557 (12.557) seconds
$ cd packages/FLIngredientFacts && swift build && swift test
Test Suite 'All tests' passed at 2026-10-12 02:56:21.387
	 Executed 37 tests, with 0 failures (0 unexpected) in 12.122 (12.122) seconds

The review's round-1 examples as named, passing tests (from ReviewerExamplesTests):

  • testLardIsRefusedForVegetarianAndVeganEaters — store-settled violation, lard never offered, unrestricted verdict untouched.
  • testWorcestershireIsRefusedForVegetarianAndVeganEaters — same, via the flesh_fish reconciliation.
  • testGenericTamariIsAskTheUserForGlutenFreeEaters — askUser reasons name tamari; never offered; the composition-unknown note stays.
  • testCertifiedGlutenFreeTamariIsADistinctProductFormWithLabelEvidence — generic tamari unknown, product form allowed on label evidence.
  • testSubstituteMissingFromTheFactsRegistryYieldsAskTheUserAndIsNeverOffered — exercised against the 257 unmapped records.
  • testTwoEatersRestrictionsUnionScreenBothAxes — the milk + peanut union folds into one FLRestrictions (diet + allergen axes); a dairy/gluten union refuses both axes in one lookup.
  • testSeitanAsProteinIsRefusedOnlyForGlutenFreeEaters — conditional refusal, verdict not degraded.
  • testAlternativeRolesNeverScaleWithThePrimaryRatio — role-aware ratios, no primary-ratio leak (57 supported_also pairs exercised).

Coverage report regenerated byte-identical (run twice, cmp) at that head, with the composition-status section: 124 ingredient records resolve in the canonical store; 257 composition-unknown; verified-swap coverage 299/781 lines (38.3%) unrestricted; restricted coverage as of THAT head (gluten-free 24, dairy-free 90, soy-free 0) was still inflated by the fix-1 leak — see the fix-1 table above for the honest numbers.
Attribution: every commit a5402d9, 61551fb, e4a7aa3, 0dc3bdf, 7b85bec (merge), e12a032, 00c36b4, c7784c0, a6cc83e is authored by Sam Gu <127461594+SamGu-NRX@users.noreply.github.com> with the Co-authored-by: obvious-autobuild[bot] trailer; authorship verified on the pushed remote head before each push (git ls-remote verified at each push; current remote head a6cc83e5fb7fc3ee71b34412ca362b750807fa0c). Append-only: no rebase, amend, or force-push.

Wrap-up — top-30 composition-unknown list for PR #127 (head 4b58954):

Per the operator ruling — the 257 composition-unknown entries need no reviewed facts before merge; fail-closed is the contract (an unknown entry is never offered as a usable swap and shows as a check to the user), and reviewing them is coverage work owned by FLIngredientFacts (task fl-root-ingredient-facts-f1, PR #127) — flsub-report gained an optional --originals flag and REPORT.md a new generated section, "Composition-unknown entries seen most often": the top 30 unknown table entries ranked by recipe-line frequency across (1) the bundled recipes (apps/ios/Resources/data.json) and (2) the originals corpus (data/recipes/originals/originals.jsonl on origin/overhaul/original-recipes, materialized read-only with git show — the branch is never checked out or pushed).

Matching rule (stated in the report itself): a corpus ingredient counts toward a table entry when the table's own resolution matches it — an exact, case-insensitive match on the entry's key, display name, or a recorded alias, plus the app data's underscore-for-space fallback for snake_case ids; occurrences are recipe ingredient lines (required + optional), so every count is a lower bound. Corpus names that match no table entry, or a composition-reviewed entry, are not listed. All 50 bundled app ingredients resolve to reviewed facts, so every Bundled count in the section is 0 and the ranking is driven by the originals corpus (401 recipes, 2829 ingredient lines, 2165 resolved to a table entry); the head of the list: salt 91, crushed tomatoes 79, vegetable oil 43.

Replay — the header's documented command reproduces the committed REPORT.md byte-for-byte at the pushed tree 4b58954:

$ git fetch origin overhaul/original-recipes
$ git show origin/overhaul/original-recipes:data/recipes/originals/originals.jsonl > /tmp/originals.jsonl
$ swift run -c release --package-path packages/FLSubstitutions flsub-report \
    --table data/substitutions/pairs.json \
    --recipes apps/ios/Resources/data.json \
    --originals /tmp/originals.jsonl \
    > /tmp/report-replay.md
$ cmp /tmp/report-replay.md data/substitutions/REPORT.md && echo REPLAY-BYTE-IDENTICAL
REPLAY-BYTE-IDENTICAL

Determinism and suite at 4b58954:

$ swift test --package-path packages/FLSubstitutions
	 Executed 54 tests, with 0 failures (0 unexpected) in 9.593 (9.593) seconds
$ sha256sum data/substitutions/REPORT.md
9da221c9e90c361318d7e0ea39dc6584fc6b5da085c757f88dc5c8fc0108e253  data/substitutions/REPORT.md

Two independent flsub-report runs were byte-identical (cmp); all pre-existing report lines are byte-stable against the a6cc83e output except the header's regenerate command, which gains the fetch/show and --originals steps. Attribution: commit 4b58954 authored by Sam Gu <127461594+SamGu-NRX@users.noreply.github.com> with the Co-authored-by: obvious-autobuild[bot] trailer (verified before push; remote head verified via git ls-remote).

Handoff

Done

  • Round-2 fixes landed append-only and parent-verified. Fix 1 at c7784c0: Lookup.classifiedSwaps partitions every candidate into usable / needs-confirmation / refused — disjoint, complete, each bucket in rank order (asserted across the deterministic lookup battery × restriction batteries × all roles, with swaps == usable); swaps returns exactly the usable bucket and verifiedSwaps excludes needs-confirmation so coverage cannot count unconfirmed composition as verified; buttermilk-liquid-to-whole-milk under vegetarian is pinned needs-confirmation; the preserved property — butter-fat-to-olive-oil under vegan (original violated, substitute allowed) stays usable and still removes dairy + animal_product. Fix 2 at a6cc83e: SwapCandidate.macroDelta is role-aware — primary roles return the recorded delta unchanged; alternative roles compute from that role's own evidenced mass ratio (volume/quarter-cup-per-egg ratios and unsupported amounts are never computation inputs) or read unknown; the reviewer's pin (egg binder → mashed banana requested as liquid) reads unknown, never the binder delta reskinned; 5 new nutrition-path tests, red-first at c7784c0 (5 tests, 15 failures before the source fix).
  • This wrap-up at 4b58954: REPORT.md's new tool-generated section "Composition-unknown entries seen most often" — the top 30 composition-unknown table entries by recipe-line frequency across the bundled recipes and the originals corpus, matching rule stated in the report; replay command and output digest in Proof. The list is handed to FLIngredientFacts (PR FLIngredientFacts: canonical ingredient facts with fail-closed restriction evaluation #127) as the review order for the 257 unknowns.
  • Operator ruling honored: the 257 composition-unknown entries do not block merge (fail-closed is the contract; an unknown entry is never offered as a usable swap and shows as a check to the user); no second fact base was built on the substitution table.
  • Suite at 4b58954: swift test --package-path packages/FLSubstitutions → Executed 54 tests, with 0 failures (Linux sandbox, no Xcode; no CI claims).

Not done / limits

  • Sam's re-review at final head 4b58954 is the remaining gate. The independent reviewer's last published read was FIX 5/7 at 00c36b4; c7784c0, a6cc83e and 4b58954 are newer and await that re-read.
  • Composition-unknown coverage is owned by FLIngredientFacts (task fl-root-ingredient-facts-f1, PR FLIngredientFacts: canonical ingredient facts with fail-closed restriction evaluation #127); this PR contributes the top-30 list only.
  • iOS-native verification: UNVERIFIED-NATIVE (no Xcode). App integration (SubstitutionService → FLSubstitutions) is out of this task's file scope and unstarted.
  • 34 mass-basis macro deltas remain; volume/unit-row deltas stay unknown with explicit reasons rather than invented densities. No CI assertions (sandbox runs only).

How to continue

  1. Re-review requested at head 4b58954 — the round-2 point-by-point is in the re-review comment; the named round-1 cases remain Tests/FLSubstitutionsTests/ReviewerExamplesTests.swift.
  2. For PR FLIngredientFacts: canonical ingredient facts with fail-closed restriction evaluation #127: review facts for the top-30 list first (salt, crushed tomatoes, vegetable oil, …); when facts land, regenerate REPORT.md with the Proof command — the ranking only shrinks as records resolve.
  3. If further fixes are needed: work happens in packages/FLSubstitutions/ + data/substitutions/; the invariants to hold are SCHEMA.md §"Canonical composition rechecks" (fail closed, both-sides recheck, conditional≠functional, role-specific ratios, mass-basis deltas).
  4. After approval: app-integration PR routes SubstitutionService through the table (LeChef reasons beyond it only when empty, saying so, per §3.5).

Final head SHA: 4b5895409cb1c990a5e8251f490fe80ccd35c295

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

First milestone of the substitution-table overhaul: pairs.json, the
reviewed, context-specific substitution table per SPEC §3.1/§3.5.

- 279 pairs across all 11 functional roles (acid, fat, binder, leavening,
  thickener, liquid, aromatic, umami, sweetener, protein, garnish)
- every pair carries: ratio with evidence status (sourced/convention/
  conflict/unsupported/unknown), preparation changes, a plain-language
  flavour/texture difference, macro delta computed from USDA SR Legacy
  per-100 g values, allergen classes for BOTH sides including compound
  ingredients (Suite C vocabulary), and the roles where the swap fails
- every pair is cited (23-entry source registry); conflicting shipped
  ratios are recorded as conflicts, never silently resolved
  (egg->mashed banana 0.6 vs ~1.2; honey->banana sugar delivery ~1/5)
- reuses PR #49's evidence verdicts verbatim; all 27 production pairs
  from its snapshot are covered consistently (21 re-recorded with honest
  statuses, 6 already consistent)

Swift package, tests, SCHEMA.md and REPORT.md follow in the next
milestones.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Oct 11, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • 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 3 commits October 11, 2026 15:17
Foundation-only SwiftPM package over the reviewed substitution table:
restriction-aware ranked lookup that refuses allergen-introducing swaps
(compound expansion included), context verdicts (verified/plausible/
unsupported/unknown, unknown never upgraded), deterministic ranking,
strict schema validation, and 33 Linux tests including the all-pairs x
all-restrictions allergen sweep and byte-identical determinism.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
The bundled recipe data references "mushroom"; the registry carried the
mushrooms identity without the singular alias, so app lookups failed
closed. Alias fix only; no pair changes.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
flsub-report resolves every bundled recipe ingredient line through the
table's alias map, counts verified-swap coverage per restriction, ranks
gaps, and prints an evenly spaced 40-pair audited sample with verdicts
recomputed by ContextRules. Two runs are byte-identical. SCHEMA.md
documents the file format, shared Suite C allergen contract, compound
expansion, verdict rules, macro-delta method, source policy, and the
regeneration command. REPORT.md is the committed generated output.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>

@SamGu-NRX SamGu-NRX left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: BLOCK

Reviewed 0dc3bdf against SPEC section 3.5, the recipe-platform substitution requirements and the PR body. No exact task-row match found.

Blockers:

  • data/substitutions/pairs.json:30-50 and Lookup.swift:46-47,85-86. Vegan/vegetarian restrictions omit classes used by their own ingredient registry. shortening-fat-to-lard is sourced/verified and lard carries only animal_product, so it is offered to vegan and vegetarian eaters. anchovy-umami-to-worcestershire likewise passes with fish + animal_product because the diet rules forbid flesh_fish instead. Reconcile diet classification with the actual registry and add independent expected-refusal cases.
  • data/substitutions/pairs.json:6764-6783. Generic tamari is declared soy-only and aliased with wheat-free soy sauce, while its note admits labels vary. soy-sauce-umami-to-tamari is consequently verified and offered for gluten-free eaters without product evidence. Separate confirmed wheat-free products from composition-unknown tamari and refuse the latter for affected eaters. A note to read the bottle cannot substitute for the recheck.

Lesser findings:

  • Tests/FLSubstitutionsTests/AllergySweepTests.swift:13-23 derives expected safety from the same classes and forbidden sets as the implementation. It cannot detect either taxonomy error above. Add independently specified compound, label-unknown and diet cases, including household unions.
  • Tests/FLSubstitutionsTests/RatioScalingTests.swift:58-69 and the table's macro_delta values multiply grams by every ratio, including volume and quarter-cup-per-egg ratios. A volume ratio is not a mass ratio; partial butter-to-applesauce also omits retained fat. Use evidenced densities/component amounts or leave the delta unknown. Macros.sodiumMg is also contrary to the spec's grams-internally contract unless an explicit boundary conversion is established.
  • ContextRules.swift:41-47 ignores any failure row when the ratio is sourced, without inspecting why it fails. Encode conditional restrictions separately from actual functional failures so sourced ratio evidence cannot override a role failure.
  • The coverage report clearly discloses any-role association rather than actual recipe-role verification. Preserve that limit in consumers. Lookup.swift:17 exposes the primary ratio even for an alternative requested role; store each supported role's actual ratio before offering scaling.

Criteria: 250+ cited pairs met; role/evidence statuses partly met; safe all-eater refusal missing; truthful ratio/nutrition partly met; regenerable coverage and 40-pair sample met; regression tests partly met; scope/no workflow edits met. 3/7 met. Existing iOS CI run 38151147654 passed; the PR reports 33 Linux tests, but these new package checks are not shown in that CI. No local tests, Xcode or simulator run. Citations were inspected as recorded evidence, not blanket clearance of every upstream source.

Pins the FLIngredientFacts API contract and semantics (fail-closed
restriction evaluation), the dataset layout with sourced evidence rules,
and the audit index for per-PR integration fix lists.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
SamGu-NRX and others added 2 commits October 11, 2026 23:18
Fail-open audits at each PR head: per-module ingredient lists, absent
restriction evaluation, and missing unknown states, with exact
FLIngredientFacts / data-conformance integration steps per file.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
Fail-open audits at each PR head: claims-based screening with defaults,
sim catalogs built without facts, substitution class divergence
(tamari/lard/worcestershire), name-only pantry feasibility, and
scenario keyword expansion, with exact FLIngredientFacts / data
conformance integration steps per file.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
@SamGu-NRX

Copy link
Copy Markdown
Owner Author

FLIngredientFacts integration fix list — restriction checks fail open

Audited at PR head 0dc3bdf22cb47a4fd7387d15a941c7e8c26be392. This fix list is committed under docs/audits/ingredient-facts/PR-114.md on branch overhaul/ingredient-facts.


PR #114 — Integration fix list: restriction checks fail open

PR head at audit: 0dc3bdf22cb47a4fd7387d15a941c7e8c26be392 (branch overhaul/substitution-table).
Canonical facts source to adopt: packages/FLIngredientFacts + data/ingredient-facts on overhaul/ingredient-facts — the substitution table's class lists and this dataset are two divergent lists describing the same ingredients, and they disagree today.

1. Where the restriction code sits at this head

File Symbols Role
packages/FLSubstitutions/Sources/FLSubstitutions/Lookup.swift swapCandidates (lines 34–83: ingredients[key]?.classes ?? [] at 42, ingredients[pair.substitute]?.classes ?? [] at 46; verifiedSwaps lines ~99–130, same fallbacks at 99, 107) The recheck: substitute classes ∩ restriction forbids ⇒ refuse
packages/FLSubstitutions/Sources/FLSubstitutions/Restrictions.swift RestrictionSet (init resolves names against table.restrictions[name] and unions forbids), AllergenRecheck Restriction vocabulary comes from the table's own JSON
packages/FLSubstitutions/Sources/FLSubstitutions/Model.swift IngredientRecord.classes (line 24), AllergenClass (Vocabulary.swift, 18 cases) Per-ingredient class lists live in data/substitutions/pairs.json
data/substitutions/pairs.json ingredients (381 records, 211 with empty classes), restrictions (vegan/vegetarian/pescatarian/gluten-free/dairy-free/nut-free/…) The table's own facts + restriction registry
packages/FLSubstitutions/Tests/FLSubstitutionsTests/AllergySweepTests.swift, CompoundAllergenTests.swift sweep tests Sweep runs only over the table's own vocabulary

2. Fail-open behavior found

  1. Missing record or empty classes ⇒ allowed. Lookup.swift line 42/46: ingredients[key]?.classes ?? [] — a substitute absent from the ingredient registry, or one of the 211 of 381 records whose classes array is empty, is evaluated as "carries nothing" and passes every restriction check. Absence of data is treated as evidence of safety, the exact inverse of fail-closed.
  2. tamari is classed gluten-free without evidence — divergent from soy sauce. In pairs.json: tamari → ["soy"] while soy_sauce → ["gluten","soy"]. Generic tamari carries soy with wheat/gluten unknown unless product evidence exists; the table silently encodes it as gluten-free. A gluten-free swap to tamari passes the recheck with no label evidence behind it. The same divergence pattern must be audited for other variable-composition products (stock cubes, bouillon).
  3. lard and worcestershire pass the vegetarian recheck. The table's vegetarian restriction forbids only the flesh/meat classes (meat_poultry, meat_ground, meat_whole_cut_pork, meat_whole_cut_red, flesh_fish, flesh_shellfish). But lard → ["animal_product"] and worcestershire → ["animal_product","fish"] — neither animal_product nor the fish allergen class is in vegetarian's forbids. swaps(for:role:restrictions:["vegetarian"]) happily offers lard and Worcestershire to vegetarians. (Vegan's list adds dairy/egg/honey but not animal_product either — bacon fat passes vegan the same way.)
  4. Two vocabularies for one reality. The table's AllergenClass (18 cases, incl. flesh/meat classes and markers) and restriction registry are separate from FLRestrictions' FLAllergen (9) + FLDiet (5). Every other module reads its own lists (see the sibling fix lists); nothing forces these to agree. RestrictionSet throwing on unknown restriction names is good, but the vocabulary itself drifts.
  5. No unknown state. AllergenRecheck has no "composition unknown" verdict — an unreviewed ingredient is indistinguishable from a verified-safe one, and EvidenceStatus.unknown covers only ratio/function claims, not composition.

3. Exact changes needed (file by file)

Goal: composition facts come from one dataset; unknown ⇒ ask the user; restriction semantics shared; the reviewer examples become conformance cases.

3.1 data/substitutions/pairs.json (or a migration away from it)

  • Fix the divergent entries now: tamari must carry ["soy"] plus gluten unknown — in the shared facts vocabulary that is soy-certain, gluten-unknown (a certified GF tamari product is a distinct preparation/product-form entry with a label evidence claim). lard, bacon_fat, worcestershire must carry classes that vegetarian/vegan actually forbid (animal_product must be added to both forbids lists, or the entries must carry the flesh classes where true — Worcestershire carries fish).
  • Add vegetarian/vegan forbids for animal_product and reconcile fish vs flesh_fish so the class an ingredient carries is always covered by the restrictions that should exclude it.
  • Drive the 211 empty-class records to reviewed facts or explicit unknowns — the shared dataset's facts.json already tracks certainty levels (certain/typical/variable/unknown); empty is not a state an ingredient may be in.

3.2 packages/FLSubstitutions/Package.swift

targets: [
  .target(name: "FLSubstitutions", dependencies: ["FLIngredientFacts"]),
]

3.3 packages/FLSubstitutions/Sources/FLSubstitutions/Model.swift + Vocabulary.swift

Map AllergenClass → FLAllergen/FLDiet (or replace it): the flesh/meat classes and animal_product/honey markers are diet facts and belong in the shared diet vocabulary; the nine contract classes map to FLAllergen. Keep the marker classes only as evidence labels.

3.4 packages/FLSubstitutions/Sources/FLSubstitutions/Lookup.swift

Replace the class-list intersection with store evaluation:

// before
let substituteClasses = sortedClasses(ingredients[pair.substitute]?.classes ?? [])   // FAIL-OPEN: missing ⇒ []
let violated = sortedClasses(substituteClasses.filter { restrictionSet.forbidden.contains($0) })

// after
let meal = restrictionSet.flRestrictions                  // shared vocabulary
switch store.evaluate(ingredient: pair.substitute, restrictions: meal) {
case .allowed:       /* keep candidate */
case .violated(let v): /* refuse: reason names the offending component */
case .unknown:       /* fail closed: not offered; surface as "ask the user / confirm" */
}

RestrictionSet (Restrictions.swift) keeps name validation but its forbids become FLRestrictions; the both-sides recheck stays (the original side matters too when it is still on the plate).

3.5 packages/FLSubstitutions/Sources/flsub-report/main.swift + data/substitutions/REPORT.md

The coverage report should report unknown-composition ingredients as their own class (not silently safe), so the 211 shrink visibly.

3.6 Tests (packages/FLSubstitutions/Tests/FLSubstitutionsTests/)

  • AllergySweepTests/CompoundAllergenTests: add the reviewer examples as first-class cases — tamari swap refused for gluten-free (no label evidence), lard/worcestershire refused for vegetarian and vegan, a substitute missing from the registry yields ask-the-user (never offered), and two eaters' restrictions union (milk + peanut ⇒ both screened).

Conformance kit (Swift)

cd packages/FLSubstitutions && swift build && swift test
cd packages/FLIngredientFacts && swift build && swift test

SamGu-NRX and others added 15 commits October 11, 2026 23:46
Generator produces identities.json (canonical keys, display names,
synonyms, USDA FDC ids from the bundled catalog where matched, app local
ids for the 50 core ingredients) and facts.json (components with
certainty, sourced allergen facts, gluten as a separate property, diet
facts, certified-gluten-free tamari product form). Validation enforces
evidence on every non-unknown fact, resolvable component keys, unique
match names and a path-based cycle check. Corpus coverage enumerates the
bundled app data and the seven originals branches programmatically.

Draft checkpoint: corpus compounds from the originals compound-allergens
tables still to be expanded.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
… labels

Reviews 5485500522 (BLOCK) and 5485457120 (FIX): a gluten-free label
supports the gluten property only — FDA permits processed wheat-derived
ingredients in gluten-free-labeled foods, so GF evidence never
establishes wheat absence (Q&A General question 6). Certified-GF tamari
now keeps wheat unknown; new gluten-free-bread identity models the FDA
case (GF label + declared wheat starch: glutenFree allowed, wheat
violated); spelt and farro are wheat-species violations; certified-GF
oats form carries wheat-free (standard) plus gluten-free (label)
evidence.

Both READMEs now pin the identities/facts JSON schemas, evidence payload
and per-result evidence rules — absence facts (free/allowed) may not use
usda/recipe evidence, enforced by the generator validator — and mark
deliverables not yet in the tree as PLANNED. Verification commands are
all repository-root relative. Forms overrides are validated through the
same fact checks.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
- PR-93: recipe-level reject/hold verdict in RecipeRepository (lines
  130-198) with a rescue counterexample; safe throwing store loader.
- PR-94, PR-101: dispatch on the typed FLEvaluation kind discriminant,
  exhaustive with never-guard, reason/missing preserved; wrapper-level
  rejection tests for violated and unknown.
- PR-119: re-audited at the real head eb303be (packages/ScenarioJoin,
  SwapGate, FLDomain Restriction defaults) replacing the invented
  citations; URL-based store loading.
- PR-99/103/105/114/117: conformance commands rooted at the repository
  root.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
Adds the compound ingredients enumerated by the originals-branch
compound-allergens tables (teriyaki, hoisin, doenjang, ssamjang,
doubanjiang, black-bean-sauce, shrimp-paste, thai-curry-paste, satay,
peanut-sauce, mole, aioli, hollandaise, bechamel, fresh-pasta,
wonton-wrappers, gyoza-skins, gnocchi, orzo, tempeh, soy-milk,
chocolate) and the corpus simple identities (nut, fish, meat, dairy and
stock entries), each with sourced facts or honest fail-closed gaps.
Ghee, coconut and pine-nuts are promoted to named identities; tahini
gains the tahina alias; a generic vinegar part distinguishes distilled
from malt. Compound diet facts now propagate from component classes with
certainty (variable components demote allowed to unknown); compound
allergen facts derive from certain/typical component carriers, explicit
unknowns for variable carriers, and free facts only for complete
compositions. 193 identities, 261 fact entries, 85/85 corpus names
covered; absence facts may not use usda/recipe evidence (validator-
enforced).

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
usdaFdcId and appLocalIds are integers, usdaRef/usdaNote name the
matched catalog row, and forms are {key, display, synonyms} records
matching the README pin. Fact entries now carry their class. Generator
validator and coverage name-index follow the new form shape.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
FLIngredientFactsStore resolves keys, display names, synonyms and
preparation-form match names; evaluation gives violated > unknown >
allowed, propagates compound components, treats variable components as
unknown-when-violating, and keeps wheat and gluten evidence independent.
The loader rejects malformed data, unresolved references and absence
facts resting on catalog/recipe evidence. All 43 conformance cases pass
through the real store (flfacts-conformance).

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
Both runners re-implement the pinned evaluation semantics from scratch
(no shared code with the Swift package), load and strictly validate the
dataset, run all 43 shared cases and exit nonzero on any failure. All
43 cases pass in both, matching the Swift store result for result.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
33 XCTests: the real dataset loads; every pinned malformed-data class is
rejected (bad JSON, unknown vocab, unresolved component or form, missing
evidence fields, absence facts backed by USDA or recipe evidence,
duplicate synonyms); all 43 shared cases run through the real store;
fail-closed, unknown propagation, certainty levels, union restrictions
and the independent wheat/gluten axes are unit-tested, including a
synthetic compound that violates purely via a certain component.
README status tables now reflect what exists.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
Replaces the PLANNED markers on cases.json and the two runners with
present-tense status; all three are in the tree and green.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
The TypeScript and Python runners validated evidence ref/note only on
fact-level objects and accepted empty strings; a dataset copy with a
component evidence 'ref' stripped loaded fine and all 43 cases passed.
Both runners now run the same completeness check the Swift loader
enforces — known kind, non-blank ref and note on every evidence object,
fact-level and component-level. The Swift loader additionally rejects
blank (whitespace) ref/note, which typed decoding alone let through.
Adds negative_checks.sh, which replays six malformed-data mutations
against all three implementations (bun runner, Python runner and the
flfacts-conformance binary) and exits nonzero if any of them accepts a
mutation, plus four new Swift loader tests (37 total).

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
Merges overhaul/ingredient-facts (b0deecc) into overhaul/substitution-table
per the PR #114 integration fix list: the substitution table's class lists and
the shared ingredient-facts dataset are two divergent lists describing the same
ingredients. Composition facts now come from one canonical dataset; the next
commits rewire the restriction recheck through store evaluation so unknown
composition fails closed (ask the user) instead of passing as safe.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>

@SamGu-NRX SamGu-NRX left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: BLOCK

Reviewed 7b85bec against SPEC 3.5, the substitution requirements and the PR body. No exact task-row match, so task_id remains null. Substitution source, table, report and tests are unchanged since 0dc3bdf. The inherited ingredient-facts files match the already reviewed #127 head b0deecc; merging them does not connect them to FLSubstitutions.

Blockers:

  • data/substitutions/pairs.json:30-50 and packages/FLSubstitutions/Sources/FLSubstitutions/Lookup.swift:46-47,85-86 still use incompatible diet classes. Lard carries only animal_product, which neither vegan nor vegetarian forbids, so shortening-fat-to-lard is offered as verified. Worcestershire carries fish and animal_product, while those diets forbid flesh_fish, so anchovy-umami-to-worcestershire is offered as plausible. Reconcile the diet rules with the actual ingredient vocabulary or use sourced canonical facts, and add independently specified refusal cases.
  • data/substitutions/pairs.json:6764-6783 still gives generic tamari only the soy class and aliases it with wheat-free soy sauce, while its own note says labels vary. The lookup consequently offers soy-sauce-umami-to-tamari to gluten-free eaters without product evidence. Separate confirmed products from generic composition-unknown tamari and refuse unresolved checks. A read-the-bottle note does not enforce the recheck. packages/FLSubstitutions/Package.swift:10 has no ingredient-facts dependency, and Lookup still reads only the old class arrays.

Lesser findings:

  • AllergySweepTests.swift:13-23 derives expected refusals from the same class arrays and forbidden sets used by lookup. It cannot catch either taxonomy/evidence defect above. Add household-union, compound and unknown-product cases with independent expectations.
  • RatioScalingTests.swift:58-69 and table macro deltas still apply ratio values to grams for volume and kitchen-unit ratios. Use evidenced densities/component amounts or leave the delta unknown. Supported alternative roles also expose the primary ratio through Lookup.swift:17.
  • ContextRules.swift:41-47 still disregards a fails row whenever its ratio is sourced, without distinguishing an eater condition from a functional failure. Source evidence for a ratio cannot erase a failed role.
  • The inherited #127 dataset remains BLOCK for incomplete composition clearance and unsupported absence facts. It needs its own fixes before adoption; this review does not clear that dataset.

Task criteria, 3/7 met:

  • 250+ cited pairs: met. The 279-pair table is unchanged.
  • Role/evidence statuses: partly met. Functional failure handling remains incorrect.
  • Safe all-eater refusal: missing. Known diet and unknown-product bypasses remain.
  • Ratio/nutrition: partly met. Unit and alternative-role problems remain.
  • Regenerable coverage and 40-pair sample: met in the unchanged artifacts, with any-role coverage limits preserved.
  • Regression tests: partly met. The sweep repeats the implementation's assumptions.
  • Scope/no workflow edits: met for substitution commits. This update inherits the facts and PR-writing assets.

Current-head app build-and-test and dead-code-advisory checks passed in run 38193326657. They do not run this standalone package. The body still names 0dc3bdf and its 33-test Linux proof; no current-head package execution is shown. No local tests ran.

SamGu-NRX and others added 2 commits October 12, 2026 02:48
Integrates the canonical FLIngredientFacts store as the composition
referee for substitution rechecks (fix list, docs/audits/ingredient-facts/PR-114.md):

- every ingredient record carries an explicit facts_key to the canonical
  identity (exact-name mapping: 123 matched, 258 fail closed as
  composition-unknown - never fuzzy);
- rechecks evaluate the store: allowed / violated(reason names the
  offending component) / unknown - unknown fails closed as ask-the-user
  and is never offered; RestrictionSet keeps name validation, its
  forbids now live in the shared FLRestrictions vocabulary, and the
  both-sides recheck stays;
- the both-sides recheck also runs when no restrictions are supplied
  (it settles allowed), so determinism serializations are unchanged;
- macro deltas exist only on mass-ratio rows (a volume or
  quarter-cup-per-egg ratio is not a mass ratio); 148 rows were nulled
  with explicit reasons, 34 mass-basis rows keep their deltas;
- conditional failure rows (5) are separate from functional failures:
  they refuse for named restrictions without degrading the verdict;
- supported_also roles expose only their own recorded ratio; scaling
  never leaks the primary ratio into an alternative role;
- taxonomy reconciled with the store: lard += pork flesh,
  fish_sauce/worcestershire += fish flesh, oyster_sauce += shellfish
  flesh; tamari loses the unevidenced "wheat-free" alias (soy-certain,
  gluten-unknown; the certified gluten-free form is a distinct
  product-form entry with label evidence); coconut milk labels align
  with the store's treeNuts: free composition verdict;
- tests: reviewer examples as first-class cases (lard and worcestershire
  refused for vegetarian/vegan; generic tamari asks the user for
  gluten-free; missing registry entries ask the user, never offered;
  restriction unions screen both axes; conditional seitan refusal), a
  store-violated refusal sweep, role-ratio no-leak, and mass-basis
  delta reproduction (43 tests, 0 failures).

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
… docs

The coverage report now loads the canonical facts dataset (sibling
data/ingredient-facts by default, --facts to override) and reports a
composition-status section: 124 ingredient records resolve in the
canonical store, 257 are composition-unknown (no canonical record;
rechecks ask the user under restrictions). The formerly invisible 211
empty-class-list records now count as their own class and shrink
visibly as records gain reviewed facts.

Restricted coverage in REPORT.md honestly collapses where substitutes
lack canonical composition records (dairy-free 265 -> 90 covered lines,
gluten-free 299 -> 24, soy-free 278 -> 0): the previous numbers were an
artifact of rechecks never screening compositions. Unrestricted
coverage is unchanged at 299/781.

SCHEMA.md documents schema 1.1.0: facts_key (exact-name canonical
identity, null fails closed), restriction fl mappings, fails[] kind
(functional vs conditional with named restrictions), role-specific
supported_also ratios (an alternative role never inherits the primary
ratio), free-form ratio bases with the mass-statement scaling contract,
the canonical-recheck semantics (allowed / violated / unknown ->
ask-the-user), the taxonomy alignments (lard, fish sauce, worcestershire,
oyster sauce, coconut milk labels), the tamari product-form rule, and
the mass-basis-only macro-delta method.

The report runs are byte-identical (verified twice).

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
@SamGu-NRX

Copy link
Copy Markdown
Owner Author

@SamGu-NRX the two BLOCK review blockers and all lesser findings are addressed at 00c36b4 (43 + 37 tests green in-sandbox, reviewer examples as first-class cases in Tests/FLSubstitutionsTests/ReviewerExamplesTests.swift, composition-status report showing 124 reviewed / 257 composition-unknown). Proof and Handoff are updated in the body. Re-review requested.

@SamGu-NRX SamGu-NRX left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: FIX

Reviewed 00c36b4 against SPEC 3.1/3.5 and the substitution requirements. No exact task row matches. All 14 changed files were inspected, including the table changes. Canonical rechecks now refuse lard and Worcestershire for vegetarian/vegan eaters and generic tamari for gluten-free eaters. Named regression tests cover those cases. Functional failure rows now override sourced ratios, and non-mass ratios no longer carry nutrition deltas.

Blockers:

  • packages/FLSubstitutions/Sources/FLSubstitutions/Lookup.swift:20,31-42 still exposes the primary role's macro delta for an alternative role whose ratio is unknown. egg-binder-to-mashed-banana requested as liquid has no alternative ratio and refuses gram scaling, but its candidate still returns the binder's nutrition delta. Return unknown nutrition until the requested role's amount is evidenced, or recompute from that amount. The new alternative-role tests check ratios, not this nutrition path.
  • packages/FLSubstitutions/Sources/FLSubstitutions/Restrictions.swift:142-149 and Lookup.swift:105-108 allow a needs-confirmation candidate into the usable swaps result when only the original composition is unknown. For example, buttermilk-to-whole-milk under vegetarian returns a verified swap with unknown original facts. The API promises safe, usable results and the schema says unknown rechecks are never offered. Require an explicit confirmation path before treating these candidates as usable, while allowing a known original violation to be removed by a safe replacement.

Lesser findings:

  • packages/FLSubstitutions/Sources/flsub-report/main.swift:196-204 labels any resolved identity as composition reviewed. A resolved generic tamari or incomplete chicken stock is counted there despite unresolved composition. Separate identity resolution from complete composition and restriction-specific outcomes. SCHEMA also says 123 matched while REPORT says 124, because display-name fallback resolves another entry.
  • ContextRules.swift:43,50 checks only the first failure row for a role. A conditional row before a functional row hides the latter; the loader permits both. Search for any functional failure and validate duplicate/conflicting role rows.
  • The inherited ingredient-facts dataset remains subject to #127's unsupported absence findings. This review clears the named substitution counterexamples, not those upstream facts.
  • The PR body still reports 33 tests, old restricted coverage and final head 0dc3bdf. REPORT now shows substantially lower restricted coverage, including zero soy-free coverage. Update Proof/Handoff to this revision.

Task criteria, 5/7 met:

  • 250+ cited pairs: met, 279 pairs and 23 registered sources.
  • Role/evidence statuses: met for the shipped functional/conditional rows; malformed duplicate rows need validation.
  • Safe all-eater refusal: partly met, canonical substitute checks and restriction unions work, but confirmation is not enforced for unknown originals.
  • Ratio/nutrition: partly met, non-mass deltas and alternative scaling are corrected; alternative-role nutrition still leaks.
  • Regenerable coverage and 40-pair sample: met, generator and updated report inspected; composition label needs correction.
  • Regression tests: met, named blocker cases, union checks and non-mass/alternative-ratio checks added.
  • Scope/no workflow edits: met, this delta stays within substitution data/package paths.

REST build-and-test is in progress in run 38197653995 at this head. The app workflow does not run this standalone package. No current-head package proof appears in the body. Source and CI review only; no local tests ran.

SamGu-NRX and others added 2 commits October 12, 2026 03:58
PR #114 review round 2, fix 1. A candidate whose recheck needs
confirmation — composition unknown on either side, per the canonical
facts store's fail-closed evaluate — used to land in the usable swaps
result when only the ORIGINAL ingredient's side was unknown: buttermilk
to whole milk under vegetarian came back as a verified swap on the
strength of the reviewed replacement alone.

The lookup now separates verdicts into three buckets: usable swaps
(no confirmation requirement, no refusal, evidence verdict),
needs-confirmation questions (surfaced, never usable), and the refused
remainder (settled violations, conditional refusals, non-evidence
verdicts). swaps(for:role:restrictions:) returns exactly the usable
bucket, and verifiedSwaps excludes needs-confirmation candidates so the
coverage report cannot count an unconfirmed composition as verified
evidence — REPORT.md is regenerated by flsub-report accordingly.

The preserved property holds: a swap whose ORIGINAL is a settled
violation and whose replacement is known-safe stays usable — removing
the violation is the point of the swap (butter to olive oil under
vegan, now pinned first-class).

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
…-role evidence

SwapCandidate.macroDelta returned the primary role's recorded delta for
every requested role. A swap requested FOR an alternative role now
computes its delta from THAT role's own evidenced mass ratio (the
SCHEMA.md macro-delta method, mass statements only - review finding d),
or reports unknown (nil) until the role's amount has evidence. The
reviewer's pin - egg as a binder to mashed banana, requested as a
liquid - reads as unknown nutrition, never the binder delta reskinned.
Primary-role requests keep the recorded delta unchanged.

Tests on the nutrition path (asserting the returned delta, not the
stored ratio): the reviewer's liquid case is unknown on the shipped
table; an alternative role with an evidenced per-role mass ratio
computes from that ratio on a synthetic table (distinct from the
primary's, math shown); primary-role deltas are unchanged across the
whole table; non-mass and unsupported alternative ratios stay unknown.
All 49 pre-existing tests pass unmodified.

PR #114 review round 2, fix 2.

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>

@SamGu-NRX SamGu-NRX left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: FIX

Reviewed a6cc83e5fb7fc3ee71b34412ca362b750807fa0c against SPEC 3.1/3.5 and the established substitution scope, task_id null. The head moved before posting the c7784c0 draft, so this review includes both commits and all six changed files. Unknown composition is now separated from usable swaps and verified coverage. Alternative-role nutrition now uses that role's mass ratio or returns nil; the banana-as-liquid counterexample is fixed, with independent numeric fixture assertions.

Blockers:

  • packages/FLSubstitutions/Sources/FLSubstitutions/ContextRules.swift:37-38,54-56 uses the alternative role's function status as its ratio evidence. The shipped egg-binder-to-mashed-banana liquid row has status: sourced but no ratio, so it still gets verified and appears in unrestricted verified coverage, despite unknown amount and the now-correct nil nutrition. The same holds for applesauce-as-liquid and seven other shipped alternative rows. Derive ratio status from also.ratio, using unknown when absent, and assess function status separately. A sourced role association cannot establish an amount.
  • ContextRules.swift:43,50 takes only the first failure row matching a role. A conditional row followed by a functional failure for that role hides the functional refusal and can return verified. SubstitutionTable.swift checks IDs and conditional restriction names, but accepts those duplicate role rows. Search all applicable functional failures or reject the conflict at load time.

Lesser findings:

  • Sources/flsub-report/main.swift:196-204 still labels resolved identities composition reviewed, including incomplete facts. Distinguish identity resolution from restriction/composition evidence.
  • The inherited ingredient-facts dataset retains #127's unsupported-absence findings. The confirmation repair enforces unknown results that reach it; it does not correct false allowed results upstream.
  • The current body has Proof for c7784c0's 49 tests but still ends with the older 00c36b4 Handoff SHA. This new nutrition commit needs current Proof/Handoff.

Task criteria, 6/7 met: 279 cited pairs met; role/evidence verdicts partly met; all-eater confirmation filtering met within the canonical store's verdicts; role-aware ratio/nutrition output met; regenerable report and sample met; regression coverage met; ownership met. No recipe text or workflow changes in these commits.

Exact-head CI run 38202908480 is still building. App CI does not run this package. Static source and data review only; no local tests, Xcode or simulator.

…quency

Operator ruling (PR #114 review round 2): the 257 composition-unknown
entries need no reviewed facts before merge (fail-closed is the contract),
and reviewing them is coverage work owned by FLIngredientFacts (PR #127).
This gives that task its starting order: flsub-report gains an optional
--originals flag and ranks the unknown entries by recipe-line frequency
across the bundled recipes and the originals corpus, read from
origin/overhaul/original-recipes via git show (the branch is never
checked out or pushed).

Matching rule (stated in the report): a corpus ingredient counts toward a
table entry when the table's own resolution matches it exactly,
case-insensitively, on key, display, or a recorded alias, plus the app
data's underscore-for-space fallback; occurrences are recipe ingredient
lines (required + optional), so every count is a lower bound.

All 50 bundled app ingredients resolve to reviewed facts, so the ranking
is driven by the originals corpus (401 recipes, 2829 ingredient lines,
2165 resolved to a table entry); the head of the list is salt (91),
crushed tomatoes (79), vegetable oil (43). Existing report sections stay
byte-stable; the header regenerate command gains the fetch/show and
--originals steps. Suite 54/54; two runs byte-identical (cmp).

Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
@SamGu-NRX

Copy link
Copy Markdown
Owner Author

@SamGu-NRX round 2 is code-complete at 4b58954 (append-only: c7784c0 → a6cc83e → 4b58954 on top of the reviewed 00c36b4). Point by point:

Fix 1 — needs-confirmation never counts as usable (c7784c0). Lookup.classifiedSwaps partitions every candidate into usable / needs-confirmation / refused — disjoint, complete, each bucket in the candidates' rank order (asserted across the deterministic lookup battery × restriction batteries × all roles, together with swaps == usable). swaps returns exactly the usable bucket; verifiedSwaps excludes needs-confirmation so coverage reporting cannot count unconfirmed composition as verified evidence. Your buttermilk pin, first-class: testButtermilkToWholeMilkUnderVegetarianNeedsConfirmationAndIsNeverUsable — buttermilk-liquid-to-whole-milk under vegetarian has needsConfirmation == true (original side askUser: buttermilk has no canonical facts record), verdict verified, substitute outcome allowed — and it is demonstrably no longer usable: absent from swaps(...) and from classifiedSwaps(...).usable, landing in .needsConfirmation. The preserved removal property, first-class: testKnownViolationOriginalWithKnownSafeSubstituteStaysUsable — butter-fat-to-olive-oil under vegan (original violated, substitute allowed) still removes dairy + animal_product and stays usable; the needs-confirmation bucket does not swallow it. 6 new NeedsConfirmationSeparationTests, 49 green at that head (Executed 49 tests, with 0 failures).

Fix 2 — alternative-role nutrition is unknown without per-role evidence (a6cc83e). SwapCandidate.macroDelta is role-aware: primary roles return the pair's recorded delta unchanged; an alternative role computes the SCHEMA.md macro-delta method from THAT role's own evidenced mass ratio (a volume or quarter-cup-per-egg ratio is never a mass multiplier; unknown/unsupported amounts are not computation inputs) and reports unknown (nil) until the role's amount has evidence. Your liquid pin, first-class: testEggBinderToMashedBananaRequestedAsLiquidHasUnknownNutrition — egg (binder) → mashed banana requested as liquid (and as leavening, functionally failed, and as sweetener) reads unknown on the shipped table, never the binder delta reskinned. Red-first at c7784c0: the 5 new AlternativeRoleNutritionTests ran before the source fix — Executed 5 tests, with 15 failures — the leak reproduced on real data (the liquid request returned the binder's recorded delta, kcal -18 / protein -5.6 / carbs 13.3 / fat -4.6 / fiber 1.6 / sugar 0.0 / sodium -70.4). The evidenced-ratio case: testAlternativeRoleWithEvidencedRatioComputesFromThatRatio (synthetic schema-1.1.0 fixture with a sourced liquid mass ratio, 0.5 per gram, 50 g typical) — the returned delta is computed from THAT ratio (kcal -75 / protein -4.4 / carbs 2.5 / fat -4.7 / fiber 0.0 / sugar 2.5 / sodium -42.5), distinct from the primary's recorded (kcal -40 / protein -3.6 / …). 54 = 49 + 5 at that head; all 49 fix-1 tests kept green unmodified.

Product ruling honored. The 257 composition-unknown entries are not blocked on reviewed facts: fail-closed stays the contract (an unknown entry is never offered as a usable swap and shows as a check to the user), and no second fact base was built on the substitution table. For FLIngredientFacts (fl-root-ingredient-facts-f1, PR #127) this head delivers the review order: REPORT.md's new tool-generated section "Composition-unknown entries seen most often" — the top 30 unknown entries by recipe-line frequency across the bundled recipes (apps/ios/Resources/data.json) and the originals corpus (data/recipes/originals/originals.jsonl on origin/overhaul/original-recipes, materialized read-only with git show; that branch was never checked out or pushed). Matching rule, stated in the report itself: a corpus ingredient counts toward a table entry on an exact, case-insensitive match of the table's own resolution — key, display name, or recorded alias, plus the app data's underscore-for-space fallback; occurrences are recipe ingredient lines, so every count is a lower bound. All 50 bundled app ingredients resolve to reviewed facts (bundled counts are 0), so the ranking is driven by the originals corpus: 401 recipes, 2829 ingredient lines, 2165 resolved to a table entry. Head of the list: salt 91, crushed tomatoes 79, vegetable oil 43; 50 of the 257 unknown entries have occurrences, 207 have none in either corpus.

Sandbox proof at 4b58954 (Linux, SwiftPM, no Xcode; no CI claims):

$ swift test --package-path packages/FLSubstitutions
	 Executed 54 tests, with 0 failures (0 unexpected) in 9.593 (9.593) seconds
$ swift run -c release --package-path packages/FLSubstitutions flsub-report \
    --table data/substitutions/pairs.json --recipes apps/ios/Resources/data.json \
    --originals /tmp/originals.jsonl > /tmp/report-replay.md
$ cmp /tmp/report-replay.md data/substitutions/REPORT.md && echo REPLAY-BYTE-IDENTICAL
REPLAY-BYTE-IDENTICAL
$ sha256sum data/substitutions/REPORT.md
9da221c9e90c361318d7e0ea39dc6584fc6b5da085c757f88dc5c8fc0108e253  data/substitutions/REPORT.md

Two independent flsub-report runs byte-identical; existing report sections byte-stable except the header's regenerate command (it gains the fetch/show and --originals steps). Commit 4b58954 authored by Sam Gu 127461594+SamGu-NRX@users.noreply.github.com with the Co-authored-by trailer.

On the score: the independent reviewer's last published read was FIX 5/7 at 00c36b4 — nothing is claimed at this head; c7784c0, a6cc83e and 4b58954 are newer and await its re-read. Re-review requested at 4b58954. The body's Handoff cites the same head; Proof records the replay command and digest.

@SamGu-NRX SamGu-NRX left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: FIX

Reviewed 4b5895409cb1c990a5e8251f490fe80ccd35c295 against SPEC 3.1/3.5 and the established substitution scope, task_id null. The handoff explicitly requests re-review at this head. Since a6cc83e, only REPORT.md and the report generator changed; verdict, lookup and nutrition source and tests are unchanged.

Blockers:

  • packages/FLSubstitutions/Sources/FLSubstitutions/ContextRules.swift:37-38,54-56 still uses an alternative role's function status as ratio evidence. The shipped egg-binder-to-mashed-banana liquid row is sourced but has no ratio, yet receives verified. Nine shipped sourced alternative-role rows lack a ratio. The nutrition fix correctly returns nil, but verified lookup and coverage still overstate the evidence. Derive ratio status from that role's ratio, using unknown when absent, and assess function evidence separately.
  • ContextRules.swift:43,50 still checks only the first failure row for a role. A conditional row before a functional failure hides the functional refusal. SubstitutionTable.swift:145-162 accepts that conflict. Check every applicable functional failure or reject duplicate conflicting role rows when loading.

Lesser findings:

  • packages/FLSubstitutions/Sources/flsub-report/main.swift:229-247,314,363-380 calls identity resolution composition review. FLIngredientFactsStore.resolve resolves names without evaluating restriction evidence. Existing identities with incomplete composition therefore disappear from the new unknown-composition ranking. Label this list unresolved identities, or derive unknown composition from facts and the relevant restrictions.
  • main.swift:366-368 and data/substitutions/REPORT.md:137-139 say unknown composition is never offered as a verified swap and always shows a check. Restrictions.swift:199-202 explicitly skips composition checks when restrictions is empty; Lookup.swift:270-287 can return verified in that case. Qualify the report's statement for restricted eaters rather than presenting it as an unconditional guarantee.
  • REPORT.md:7-8 regenerates against a moving originals branch without recording its input SHA or digest. Pin that input in the report so later corpus updates do not silently change this evidence.
  • The inherited canonical dataset's unsupported-absence findings remain tracked in #127. This report update does not correct upstream false allowed results.

The new ranking's counts match static parsing of originals at 607ef996e685ed250c0c0036690736587fddf728: 401 recipes, 2829 named lines, 2165 table matches, 257 unresolved table entries, 50 with occurrences, and all 30 displayed rows. The committed report SHA-256 matches Proof. This verifies the ranking for those inputs, not the composition-review label or executable determinism claim. Current Proof/Handoff now identify 4b58954; the previous stale-head finding is resolved.

Task criteria, 6/7 met: cited 279-pair table met; role/evidence verdicts partly met; all-eater confirmation filtering met within the canonical store's verdicts; role-aware ratio/nutrition output met; generated report and sample met with the qualifications above; regression coverage met; ownership met. No workflow changes or recipe expression added in this delta.

REST CI run 38203749537 reports build-and-test success and dead-code-advisory in progress. App CI does not run FLSubstitutions. The 54-test Linux run is producer evidence. Static source/data inspection only; no local tests, Xcode or simulator.

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