Skip to content

feat(evaluation): re-gate taste residual on the app recommendation pool - #184

Open
obvious-autobuild[bot] wants to merge 2 commits into
obv/burst8-fridgeluck-goal-pathfrom
obv/burst9-fridgeluck-taste-gate
Open

obvious-autobuild[bot] wants to merge 2 commits into
obv/burst8-fridgeluck-goal-pathfrom
obv/burst9-fridgeluck-taste-gate

Conversation

@obvious-autobuild

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

Copy link
Copy Markdown

What changed and why

The committed taste gate ran over a synthetic setup: pantries drawing 160–260 of 299 non-staple keys, ranked over the exact-feasible tier only. The pool the app actually ranks is different — RecipeRepositoryCatalogRecommend ranks the exact tier plus recipes missing at most two blocking required lines over a product-shaped pantry. This burst re-runs the gate on that pool before porting anything to Swift.

  • Pool modes. The taste-lab simulator now has app mode (pantry = 15–40 non-staple keys + assumed staples; candidate pool = exact-feasible ∪ {≤ 2 missing blocking lines}; assumed-staple lines never block feasibility and count like optional matches, +2.5, when present — the app's sharedRankingScore behavior) and legacy mode (the committed gate's setup, retained for reproducibility). App-mode pool sizes land at 75–181 recipes (median 108), none skipped.
  • Two lab-only variants. churn_bounded confines learned reorders to runs of consecutive pool entries whose rule scores lie within a tuned band; shrunk scales contributions by n/(n+k) on exposure count. Both are fixture-pinnable and CLI-selectable. No Swift changed.
  • Tuning-and-gate command. python3 -m taste_lab gate tunes each variant on seeds 7/11 only (grid: band ∈ {1,2,3,6}, k ∈ {2,4,8,16}), then evaluates on seeds 7/11/13/17/19 with the pre-registered pass rule: on each held-out seed (13/17/19), paired delta-vs-rule CI lower bound > 0 and top-10 churn ≤ 5%. ~45 s per run.
  • Fixtures regenerated in app mode (golden-hh00/hh01/hh23, seeds 7/11/13) so replay covers the event model the gate measured.
  • Verdict: the residual-ranker default stays OFF (TasteResidualSettings.builtInDefault = false). No arm passes; per the pre-registered rule, nothing was ported. Full report: evaluation/taste-lab/results/app-pool-gate.md.

Gate results

seed residual Δ NDCG@10 95% CI churn shrunk Δ shrunk CI shrunk churn
7 (tune) +0.0012 [-0.0014, +0.0041] 0.025 +0.0009 [-0.0017, +0.0037] 0.030
11 (tune) +0.0007 [-0.0005, +0.0021] 0.040 +0.0008 [-0.0005, +0.0022] 0.040
13 (held out) +0.0004 [-0.0010, +0.0017] 0.030 +0.0003 [-0.0006, +0.0011] 0.020
17 (held out) +0.0005 [-0.0010, +0.0022] 0.030 -0.0000 [-0.0016, +0.0013] 0.025
19 (held out) +0.0006 [-0.0018, +0.0030] 0.040 +0.0009 [-0.0016, +0.0034] 0.025

Every arm fails (CI lower bound ≤ 0 on held-out seeds); churn passes everywhere at 2.5–4% vs the 5% limit. Notable findings:

  • The committed gate's numbers were a wide-pantry artifact. Over the exact-only 160–260-key setup the residual delta read +0.0022 / +0.0111 / +0.0059 with churn 13.5–22.5%; on the app pool the same layer reads ~10× smaller with no seed clearing zero, and churn drops 5–8×. The wide setup manufactured rule-near-ties for the learned layer to exploit.
  • churn_bounded never binds: every learned reorder sits inside ≤ 1.0-point rule-score runs, so all four band values produce metrics identical to the shipped residual.
  • shrunk trades delta for churn and still fails the CI rule — the shipped bounds were never the binding problem.

How I checked it

  • python3 -m pytest tests/ — 17 passed (was 10; added pool-mode, pantry-shape, band-binding, shrunk-scaling, variant-validation, and fixture-pinning tests).
  • docs/spec/taste/contracts/check.py — all examples valid and self-consistent.
  • python3 -m taste_lab validate-fixtures --catalog apps/ios/Resources/RecipeCatalog/recipes-v2.json --fixtures evaluation/taste-lab/fixtures — all fixtures replay exactly.
  • python3 -m taste_lab gate --catalog apps/ios/Resources/RecipeCatalog/recipes-v2.json --households 40 --days 30 --out-prefix results/app-pool — produced results/app-pool-seed{7,11,13,17,19}.json + app-pool-gate.json and the fail verdicts above; reports reproducible end-to-end.
  • White-box probes: pool sizes min 75 / p50 108 / max 181 over 80 household draws (2 seeds × 40); band=2.0 changes the order for 0 of 40 households vs the unbounded residual.

What I couldn't check

  • No iOS build, XCTest, or device run — no Swift changed (the gate failed, so the port was not attempted per the pre-registered rule). CI on this PR exercises the Python lab via the repo's hosted jobs; Swift behavior is untouched and untested here.

Assumptions

  • The 15–40 non-staple pantry draw approximates the product's intake flow; the documented intake paths don't pin a pantry size. If real pantries are materially denser, the wide-pool setup — and the committed gate's numbers — become relevant again.
  • Exposure share floor 0.15 and personal-score = 0 for catalog entries carried over from the committed setup unchanged.
  • Pass rule held exactly as pre-registered in the task (held-out CI > 0, churn ≤ 5%); no post-hoc criterion changes.

Human author

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

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

CI status note (at 9096d45)

The first iOS CI run (38842276172) failed in build-and-test at a Swift compile error this PR does not touch: apps/ios/Feature/CookMode/CookModeViewModel.swift:540:27 — CookReceiptLog(store:) called without the sessionID/sessionRevision its initializer requires. That mismatch exists verbatim at this branch's base commit (3e152dd), at this PR's head, and at the base branch tip e398c475 (CookMode/ has zero commits since the branch point — git log 3e152dd6..e398c475 -- apps/ios/Feature/CookMode/ is empty). This PR's diff contains zero files under apps/ios/. The base branch's own PR runs were red before and after this push (runs 38839878965 / 38840455779 / 38840752062 / 38841007670, GoalPath test compile errors at 02:20 and runtime test failures — including the known GRDB non-reentrant fatal — at 02:33). A re-run of the failed job (02:53) reproduces the identical error, so the compile failure is deterministic on this head. One unexplained discrepancy is left on record: the base branch's own run 38841007670, on app-target content byte-identical to this head (the trees differ only in apps/ios/Tests/GoalPath/GoalPathTests.swift), executed 33 test-suite runs with no missing-arguments error - an outcome that could not be reconciled with the source content from the available logs. The failed job belongs to the goal-path burst's in-flight repair; fixing Swift here would collide with that work and is out of scope for a Python-only re-gate.

Update: base CI repair merged forward (2cbd7cc)

The goal-path burst repaired the inherited break on obv/burst8-fridgeluck-goal-path and its own iOS CI went green at e87ee7b2 (run 38851718150, build-and-test success). The call site now reads CookReceiptLog(store: sessionID: sessionRevision:) and the repair is confirmed in the merged tree. The green base tip was merged forward into this branch as merge commit 2cbd7ccf (zero conflicts - this PR's diff is evaluation/taste-lab only); the taste-lab suite was re-run post-merge (17 passed in 1.02s). CI run 38854123101 on 2cbd7ccf completed success in 12m07s, so the PR's iOS CI now judges this PR's own content rather than the inherited break: build-and-test and dead-code-advisory are green at the current head. The earlier red runs on 9096d456 (compile error at CookModeViewModel.swift:540, reproduced deterministically) are retained below as the historical record.

Re-run the learned-ranking gate over the pool RecipeRepositoryCatalogRecommend
actually ranks instead of the wide-pantry synthetic setup: app mode draws
15-40 non-staple pantry keys plus assumed staples and ranks the exact tier
plus the at-most-two-missing near-match tier (legacy mode retains the
committed gate's 160-260-key, exact-only setup for reproducibility).

- simulator: pool modes with product-shaped pantries and candidate pools;
  assumed-staple lines never block feasibility and count like optional
  matches (+2.5) when present, mirroring the app's sharedRankingScore
- two lab-only ranking variants: churn_bounded (reorders confined to
  consecutive rule-score runs within a tuned band) and shrunk (exposure-count
  shrinkage n/(n+k)); fixture variant pinning and CLI flags for both
- tuning-and-gate command: tune on seeds 7/11, evaluate on all five seeds
  (7/11/13/17/19), pre-registered pass rule (held-out delta CI lower bound
  > 0 and top-10 churn <= 5% on each held-out seed)
- verdict: no arm passes - residual delta +0.0004..+0.0012 NDCG@10 with CI
  spanning zero on every held-out seed; churn 2.5-4% everywhere; the band
  never binds. TasteResidualSettings.builtInDefault stays false; no Swift
  ported. Report: evaluation/taste-lab/results/app-pool-gate.md
- fixtures regenerated in app mode; 17 lab tests pass; contracts check green

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

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

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

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6359752f-1b51-4e66-931f-114756b3e024

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.

@codemetrics-ai

Copy link
Copy Markdown

Note

Review skipped

Codemetrics is installed on this repository, but it hasn't been added to your team yet.

To enable AI code reviews for this repo, go to your dashboard and add FridgeLuck to your team's repositories.

@greptile-apps

greptile-apps Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] Adds ranking variants to the recipe recommendation evaluation lab.

Correct the app-mode pantry and regenerate the gate evidence before merging.

Fix All in Claude CodeFindings

  1. P1 Assumed staples are not guaranteed ▶
  2. P2 Chained scores escape the band ▶
Fix with agent prompt
### Issue 1
evaluation/taste-lab/taste_lab/sim.py:114-115
`app` mode does not add the assumed staples promised by the gate setup. `pantry_for_day` gets its stable set from `Catalog.staple_ids()`, which counts only blocking required ingredients. The v2 loader excludes assumed staples from that list, so those keys compete for the random 15–40-key draw instead of being added first.

This makes the new +2.5 staple bonuses depend on that draw, changing the rule order and measured churn. Build the app-mode staple set from the catalog's assumed-staple lines, leave `legacy` unchanged, and regenerate the results and fixtures.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

### Issue 2
evaluation/taste-lab/taste_lab/engine.py:448-449
`_churn_bounded_reorder` checks each gap against the previous entry, so a chain can defeat the promised bound. With rule scores 60, 58.5, and 57 and `band=2`, all three entries enter one run. A learned contribution above 3 can then move the last entry ahead of the first, despite their rule gap exceeding the band.

The new test forbids that inversion, but its sampled history does not exercise it. Bound each run's full score spread and add a three-entry chain test so future experiments do not report these reorders as bounded.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds app and legacy pool modes, two lab-only ranking variants, a tuning-and-gate command, and regenerated fixtures and reports. Swift is unchanged and the residual default remains off.

  • The taste lab ranks recipes from the app-shaped recommendation pool.
  • The taste lab can compare bounded and evidence-shrunk rankings.
  • The taste lab tunes ranking variants and reports a held-out gate result.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[App-mode pantry] --> B[Catalog candidate pool]
  B --> C[Shared simulated history]
  C --> D[Tune variants on seeds 7 and 11]
  D --> E[Evaluate chosen variants on all five seeds]
  E --> F[Check held-out confidence bounds and churn]
  F --> G[Write seed reports and gate verdicts]
Loading

Reviews (1) · Last reviewed commit: "feat(evaluation): re-gate taste residual..." · Reviewed by Greptile

Comment on lines +114 to +115
if catalog.is_v2:
lo, hi = (15, 40) if self.pool_mode == "app" else (160, 260)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Assumed staples are not guaranteed

app mode does not add the assumed staples promised by the gate setup. pantry_for_day gets its stable set from Catalog.staple_ids(), which counts only blocking required ingredients. The v2 loader excludes assumed staples from that list, so those keys compete for the random 15–40-key draw instead of being added first.

This makes the new +2.5 staple bonuses depend on that draw, changing the rule order and measured churn. Build the app-mode staple set from the catalog's assumed-staple lines, leave legacy unchanged, and regenerate the results and fixtures.

Prompt To Fix With AI
This is a comment left during a code review.
Path: evaluation/taste-lab/taste_lab/sim.py
Line: 114-115

Comment:
**Assumed staples are not guaranteed**

`app` mode does not add the assumed staples promised by the gate setup. `pantry_for_day` gets its stable set from `Catalog.staple_ids()`, which counts only blocking required ingredients. The v2 loader excludes assumed staples from that list, so those keys compete for the random 15–40-key draw instead of being added first.

This makes the new +2.5 staple bonuses depend on that draw, changing the rule order and measured churn. Build the app-mode staple set from the catalog's assumed-staple lines, leave `legacy` unchanged, and regenerate the results and fixtures.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex

Comment on lines +448 to +449
if run and (run[-1].rule_score - entry.rule_score) > band:
ordered.extend(sorted(run, key=lambda x: (-x.total_score, x.recipe_id)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Chained scores escape the band

_churn_bounded_reorder checks each gap against the previous entry, so a chain can defeat the promised bound. With rule scores 60, 58.5, and 57 and band=2, all three entries enter one run. A learned contribution above 3 can then move the last entry ahead of the first, despite their rule gap exceeding the band.

The new test forbids that inversion, but its sampled history does not exercise it. Bound each run's full score spread and add a three-entry chain test so future experiments do not report these reorders as bounded.

Prompt To Fix With AI
This is a comment left during a code review.
Path: evaluation/taste-lab/taste_lab/engine.py
Line: 448-449

Comment:
**Chained scores escape the band**

`_churn_bounded_reorder` checks each gap against the previous entry, so a chain can defeat the promised bound. With rule scores 60, 58.5, and 57 and `band=2`, all three entries enter one run. A learned contribution above 3 can then move the last entry ahead of the first, despite their rule gap exceeding the band.

The new test forbids that inversion, but its sampled history does not exercise it. Bound each run's full score spread and add a three-entry chain test so future experiments do not report these reorders as bounded.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

obvious-autobuild Bot added a commit that referenced this pull request Oct 16, 2026
…etup

Re-runs the five-seed gate (seeds 7/11/13/17/19, 40 households x 30
days) and regenerates the golden fixtures after guaranteeing assumed
staples in app-mode pantries and bounding churn_bounded runs by full
rule-score spread. app-pool-gate.md gains a paired table of #184's
numbers beside the re-gate's, per arm and seed.

Verdict is unchanged: no arm passes the pre-registered rule (held-out
paired delta-vs-rule CI lower bound > 0 and top-10 churn <= 5%), so
TasteResidualSettings.builtInDefault stays false and nothing is ported.
The band fix makes churn_bounded bind for the first time (chosen band
moves 1.0 -> 6.0); shrunk clears seed 19 for the first time but still
fails seeds 13 and 17.

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