Repository navigation
feat(spec): recipe bundle-v2, ingredient identity, and taste-event contracts - #148
Conversation
…t contracts Machine-checked contract layer for the recipe system: canonical ingredient identity over the 50 legacy data.json ids (13 resolved, 10 proposed, 27 unresolved against the bundled USDA catalog - nothing invented), a strict JSON Schema for shipped recipe bundles v2 (quantities fail closed, pinned thermometer checks, null-able diet claims, nutrition coverage bands with basis, rights provenance, stable catalog ids), and the taste-signal event envelope feeding on-device learning. Includes a 6-recipe sample covering every quantity kind and coverage level, generated fixtures, check.py (7/7 tamper cases caught), and SPEC.md documenting decisions and alternatives. Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Ingest the world-2026-10 (177/179) and everyday-2026-10 (185) packets into the corpus: packet ids renumbered into the fl-original-* band with the mapping recorded in data/recipes/evidence/ingest-2026-10-map.json, rights manifests converted to the main shape, two title-collision recipes excluded (never retitled). Vocabulary rebuilt with build_vocab (additive: per_100g/fdc_id unchanged for existing keys); bundle recompiled deterministically — 763 recipes rcp-000167..rcp-000934, 329 ingredients, validator PASS, 14/14 pipeline tests (corpus-size pin 401 -> 763, GENERATED_AT bumped per the tool's recompile instruction). Everyday categories stay packet-level metadata: the bundle-v2 collections enum does not cover them (spec-owned contract, PR #148). Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
|
Independent review (Obvious, Autobuild + Max) Verdict: Findings (by severity)1. High — schema and SPEC disagree on the rights enum. 2. Medium — 3. Medium — the sample violates its own 4. Low — event revisions aren't cross-checked. 5. Low — checker hygiene. 6. Low — body count nit. "Adversarial tamper tests, 7/7 caught" lists six mutations. I reproduced all six as caught; name the seventh or fix the count. What I verified (commands and results)
What I couldn't verify
Draft state is right — the contract shapes are worth reviewing now. Findings 1–3 are small, mechanical fixes; none of them questions the identity-resolution rules this review was waiting on. 🔗 Obvious Project · 🧵 Obvious Thread · 🔀 Obvious PR Artifact |
Review round 1 on PR #148 (findings 1-6): - taste-events-v1: allOf if/then branches now require integer rating 1-5 on recipe_rated, non-null dismiss_reason on suggestion_dismissed, and null on every other type (both directions pinned). - bundle-v2 sample: ginger per-100g populated from catalog FDC 169231 so rcp-000301's computed coverage is honest, and rcp-000301's per-serving row recomputed from its own per-100g rows (was 475 kcal / 34 g fat / 1720 mg sodium; strict sums give 354 / 22 / 982). - SPEC rights section amended to the shipped fail-closed two-state enum (original | quarantined); licensed/attribution_only are future statuses. New check.py assertion pins SPEC <-> schema enum agreement. - check.py: coverage-vs-weighed-lines cross-check for computed recipes; event recipe_revision bounded to shipped revisions and sample must exercise the rev-2 drift scenario; no-op 'or True' assertion removed; unused meta-schema validator removed; catalog-miss message now names the failing entry's own legacy_id. - tamper-tests.py committed: 15 named mutations; 8/15 caught at the reviewed head (all 7 new invariant cases slipped through), 15/15 now. Co-authored-by: obvious-autobuild[bot] <262744130+obvious-autobuild[bot]@users.noreply.github.com>
What changed and why
Machine-checked contract layer for the recipe system, under
docs/spec/recipes/— the data contracts the in-review recipe drafts (#113, #144, #62, #114, #127, #126) currently assume but nothing pins:SPEC.md— the design doc: identity rules, bundle semantics, taste-event boundary, decisions and alternatives, and the load-bearing assumption (canonical identity removes the recipe↔catalog id-mismatch class; what evidence would break that premise).contracts/recipe-bundle-v2.schema.json— strict JSON Schema for the shipped, versioned bundle: stablercp-NNNNNNcatalog ids +revision(existing recipe ids and cooking-history references survive migration), four quantity kinds (grams / count / household / unknown — "to taste" never gets a guessed gram value), steps referencing ingredients by index with optional timers and FSIS-pinned thermometer checks, diet claims that areboolean | null(null = unknown, treated as fail by allergen checks), per-serving nutrition with a requiredcoverageband (computed/partial/estimated) and a free-textbasisnaming which lines carry what, and rights provenance (original | quarantined— fail-closed;licensed/attribution_onlyare future statuses once a licence pipeline exists).contracts/ingredient-identity.schema.json+ generated map — maps the 50 legacydata.jsoningredient ids onto the canonical vocabulary against the bundled 800-row USDA catalog: 13 resolved (unique display-name/alias match), 10 proposed (unique prefix/stem candidate whose qualifiers don't change the food — pipeline confirms against full FDC before shipping), 27 unresolved (multi-variant foods like milk/cheese/mushroom, and misdirection traps: the only catalog hit foreggis salted-and-frozen, forbananait's Banana Pepper, fortofuit's Tofu Yogurt — all recorded as candidates, none auto-resolved). Nothing is invented; unresolved entries are resolved against full FoodData Central (CC0) with pinnedverification_source, mirroring the catalog's own verification columns.contracts/taste-events-v1.schema.json— the event envelope (six types, bound tocatalog_id+revision, opaque household/member ids, closed-set dismiss reasons, structured substitutions) that the recipe system emits to on-device taste learning. The profile and residual-ranker semantics stay owned by PR docs(spec): taste-residual-v1 taste profile and learning spec with machine-checked contracts #146; this fixes only the event vocabulary so bundle revisions can't drift from what the learner replays.contracts/examples/— a 6-recipe / 20-ingredient sample bundle covering every quantity kind, two thermometer checks (egg dishes, ground beef), all three nutrition coverage levels, a revision bump, optional/staple lines; the 50-entry identity map; six taste events covering all six types.contracts/check.py+requirements.txt— the machine check.How I checked it (commands and results)
python3 docs/spec/recipes/contracts/check.py→ all 7 checks pass (3 schemas valid JSON Schema 2020-12; 3 examples validate; cross-checks: bundle keys/ids unique, ingredient keys referenced,usesindexes in range,computedcoverage means every gram-weighed line has a per-100g row, identity map covers legacy ids 1–50 exactly with names matchingdata.json, every non-null fdc_id exists in the bundled catalog, events reference only bundle recipes with revisions within the shipped range and exercise the rev-2 drift scenario, SPEC rights statuses match the schema enum).contracts/tamper-tests.py: 15 named mutations, 15/15 caught at the current head —usesindex out of range; bundlefdc_idnot in catalog; identityfdc_idnot in catalog; identity map missing an entry; rating out of range; event referencing an unknown recipe;recipe_ratedwith null or missingrating;ratingon a non-rated event;suggestion_dismissedwith null or missingdismiss_reason;computedcoverage with a null per-100g weighed line; event revision beyond shipped;rights.statusoutside the enum; plus a failure-message assertion (a catalog miss names the failing entry's own legacy id). Each mutation makes the checker exit nonzero.partialwhile itsbasissaid all values were estimates — corrected toestimated; generator bugs found and fixed along the way (non-lowercased normalization, alias table'singredient_idbeing the catalog row id rather than the FDC id).What I couldn't check
Assumptions
alpha(where the other recipe drafts are reviewed), notmain.key(stable slug) is the join key going forward; integer ids (legacy 1–50, FDC) are evidence links only.rcp-000002/revision 2 are illustrative fixtures, not a migration commitment for specific rows.🚢 Draft — ready for review of the contract shapes; will mark ready once the identity-resolution rules get a look from someone who knows the FDC import plan.
Review round 1 (2026-10-15) — changes needed → fixed at e0eedd7
Independent review (pull/148#issuecomment-6199442480) ranked six findings; all six are addressed, none disputed. Failing-first evidence: the tamper suite run against the reviewed head
b4a19d7caught 8/15 — every new invariant case slipped through — and 15/15 ate0eedd7;check.pywent from 6 checks that pinned none of the flagged invariants to 7 that pin them.original | quarantinedtwo-state enum is the deliberate fail-closed contract (its own description: "Unknown rights means quarantine"), and it is what the fix(scraper): repair ingredient identity, gram estimates, and diet tags from measured import failures #43 pipeline actually produces — unproven rows ship quarantined. SPEC §2 and this body now sayoriginal | quarantinedwithlicensed/attribution_onlyas future statuses; a newcheck.pyassertion (SPEC rights statuses match the bundle schema enum) fails on the old SPEC text and passes now, so this drift class is machine-caught from here on.taste-events-v1.schema.jsongainedallOf/if/thenbranches requiringratingas integer 1–5 onrecipe_rated, a non-null enumdismiss_reasononsuggestion_dismissed, and null on every other type — both directions pinned. Five new tamper cases (null/missingrating,ratingon a non-rated event, null/missingdismiss_reason) were all uncaught atb4a19d7and are caught now.computedcoverage violated by its own sample (fixed, plus one adjacent find). ginger'sper_100gis now populated from its catalog row (FDC 169231, "Ginger Root (Raw)": 80 kcal, 1.82 P / 17.77 C / 0.75 F, 2.0 fiber, 0.013 g sodium → 13 mg, queried live fromusda_ingredient_catalog.sqlite), keeping rcp-000301computedhonestly. Adjacent, same error class: rcp-000301's per-serving row did not match strict sums from its own per-100g rows (claimed 475 kcal / 34 g fat / 1720 mg sodium; strict sums are 354 / 22 / 982) — recomputed with the basis naming the method. The new coverage-vs-weighed-lines cross-check (acomputedrecipe may not carry a gram-weighed line whose ingredient has no per-100g row) was uncaught atb4a19d7and is caught now.check.pynow bounds each event'srecipe_revisionto the referenced recipe's shipped revisions and requires the sample to exercise the rev-2 scenario: thecook_abandonedevent is bound to rcp-000002 revision 2. The "revision 9 on a rev-1 recipe" mutation was uncaught atb4a19d7and is caught now.... or Trueno-op assertion is gone; the unused meta-schema validator is gone; the catalog-miss message now usese["legacy_id"]instead of the loop-leakedlid(a tamper case asserts the message names the mutated entry's own legacy id).contracts/tamper-tests.py, 15 named cases), so the count is checkable instead of narrated.Generator provenance (review's "couldn't verify" note). The identity map's generator lived in scratch and is not in the repo. A from-scratch reconstruction does not reproduce the committed map byte-identically — it diverges on rule details (11/13/26 vs the committed 13/10/27 split, alias-match phrasing, candidate collection). Rather than overwrite a review-verified artifact with an approximation, the map stands as committed, bounded by
check.py's semantic validation (names matchdata.json, every non-null fdc_id exists in the bundled catalog, 50 entries, statuses complete) — all of which the independent review reproduced against the live catalog. Follow-up: rewrite the generator deliberately from SPEC §1 and regenerate the map as its own reviewed change.Commands run this round
python3 docs/spec/recipes/contracts/tamper-tests.pyatb4a19d7→ 8/15 caught (the 7 new invariant cases MISSED)python3 docs/spec/recipes/contracts/check.pyate0eedd7→ 7/7 checks passpython3 docs/spec/recipes/contracts/tamper-tests.pyate0eedd7→ 15/15 caughtgit diff b4a19d7..e0eedd7→ 6 files, all underdocs/spec/recipes/