Integrates cf-core reranker into the L1/L2 recipe engine. Paid+ tier
gets a BGE cross-encoder pass over the top-20 FTS candidates, scoring
each recipe against the user's full context: pantry state, dietary
constraints, allergies, expiry urgency, style preference, and effort
preference. Free tier keeps the existing overlap sort unchanged.
- New app/services/recipe/reranker.py: build_query, build_candidate_string,
rerank_suggestions with tier gate (_RERANKER_TIERS) and graceful fallback
- rerank_score field added to RecipeSuggestion (None on free tier, float on paid+)
- recipe_engine.py: single call after candidate assembly, before final sort;
hard_day_mode tier grouping preserved as primary sort when reranker active
- Fix pre-existing circular import in app/services/__init__.py (eager import
of ReceiptService triggered store.py → services → receipt_service → store)
- 27 unit tests (mock backend, no model weights) + 2 engine-level tier tests;
325 tests passing, no regressions
- Add max_total_min to RecipeRequest schema and TypeScript interface
- Add _within_time() helper to recipe_engine using parse_time_effort()
with graceful degradation (empty directions or no signals -> pass)
- Wire max_total_min filter into suggest() loop after max_time_min
- Add time_first_layout to allowed settings keys
- Add timeFirstLayout ref to settings store (preserves sensoryPreferences)
- Add maxTotalMin ref to recipes store, wired into _buildRequest()
- Add time bucket selector UI (15/30/45/60/90 min) in RecipesView
Find tab, gated by timeFirstLayout != 'normal'
- Add time-first layout selector section in SettingsView
- Add 5 _within_time unit tests and 2 settings key tests
Add GroceryLink schema model and grocery_links field to RecipeResult.
Introduce GroceryLinkBuilder service (Amazon Fresh, Walmart, Instacart)
using env-var affiliate tags; no links emitted when tags are absent.
Wire link builder into RecipeEngine.suggest() for levels 1-2.
Add test_grocery_links_free_tier to verify structure contract.
35 tests passing.