- upsert_slot: raise 422 immediately if meal_type not in VALID_MEAL_TYPES
- update_prep_task: assert whitelist safety contract after dict comprehension
- CreatePlanRequest: week_start typed as date with must_be_monday validator; str() cast at call site
- PrepTask: frozen=True; build_prep_tasks rewired to use (priority, kwargs) tuples so frozen instances are built with correct sequence_order in one pass (no post-construction mutation)
- Move deferred import json to file-level in meal_plans.py
- Fix test dates: "2026-04-14" was a Tuesday; updated request bodies to "2026-04-13" (Monday)
Hard Day Mode now prioritises results by effort tier before match_count:
Tier 0 (premade): frozen/instant title keywords, or ≤2 ingredients with
heat/microwave-only steps (frozen dinner, heat-and-eat, microwave meal)
Tier 1 (super simple): ≤3 ingredients + any easy method (quesadilla,
cheese toast, scrambled eggs)
Tier 2 (easy/moderate): everything else that passed the 'involved' filter
Assembly templates default to tier 1 (inherently simple). Normal mode sort
is unchanged — match_count only.
- Add migration 021: recipe_browser_fts FTS5 table on category + keywords
columns, eliminating LIKE '%keyword%' full sequential scans on 3.1M rows
- _count_recipes_for_keywords now uses FTS5 MATCH (O(log N) vs O(N))
- browse_recipes reuses cached count, eliminating the second COUNT(*) scan
per page request; ORDER BY r.id replaces the unindexed ORDER BY title sort
- Module-level _COUNT_CACHE keyed by (db_path, keywords) means domain-switch
category counts are computed once per process lifetime
feat(find): dietary preset grid, Big 9 allergen pills, Hard Day Mode surface
- Dietary constraints replaced with toggle-button preset grid (8 options)
+ free-text "Other" field; removes dense freeform text input
- Allergies replaced with Big 9 pill picker (peanuts, tree nuts, shellfish,
fish, milk, eggs, wheat, soy, sesame) + "Other" for custom entries
- Hard Day Mode surfaced as a standalone aria-pressed button above the
dietary collapsible; no longer buried inside a collapsed section
- Active-state dot indicators on both collapsibles show filter engagement
at a glance without expanding
fix(a11y): aria-describedby wiring for wildcard checkbox and tag inputs (#40)
- Persistent hint spans replace placeholder-only instructions for constraint
and allergy fields (WCAG 3.3.2)
fix(browse): auto-select highest-count category on domain switch (#41)
- Eliminates the 3-decision cold start (domain → category → content)
- Surprise Me button added for zero-decision random navigation
- "Use What I Have" / "Allow Swaps" / "Get Creative" / "Surprise Me 🎲"
- Active level shows a one-liner description beneath the selector
- Hover tooltips repeat the description for mouse users
- Label changed from "Creativity Level" to "How far should we stretch?"
- Wildcard confirmation copy updated to match new framing
Replace hand-rolled feedback.py with make_feedback_router() from
circuitforge_core.api.feedback. Update tests to mount the shared
router on a minimal FastAPI app and mock at the core module level.
Patch _user_db_path tests to monkeypatch CLOUD_DATA_ROOT onto a
tmp_path so they never touch /devl or any real filesystem path.
Remove duplicate X-Real-IP comment block in cloud_session.get_session.
Add household_id and is_household_owner fields to CloudUser dataclass.
Update _user_db_path to route household members to a shared DB path.
Update _fetch_cloud_tier to return a 3-tuple and cache a dict.
Update get_session to unpack and propagate household fields.