docs: refresh HANDOFF + ORIENTATION for fresh-agent pickup

Bring docs in line with the post-AM-6 state so a fresh agent can pick
up cleanly without first reconciling stale numbers:

- pytest count: 88/59/31 → 92 across all references
- live scrape: 9,960 rows / 36s → 9,980 rows / 44s (latest run, 2026-05-06)
- migrations applied: 0001-0005 → 0001-0007 in both docs
- verification gate updated with auto-minted JWT detail and the 29,779
  ingredient_grocery_match rows produced post-scrape
- "What is real" / Backend: added swiftly_auth.py bullet describing
  get_token() / mint_anonymous_token() / cache semantics + the
  10,928-item live verification
- file map: added services/swiftly_auth.py, services/matcher.py,
  services/planner/, mentioned scraper_service runs matcher post-scrape
- file map: alembic versions 0001 → 0007, tests/ count = 92, marked
  config.py as no-longer-carrying SWIFTLY_BEARER_TOKEN
- spec map line for swiftly-token-auto-mint: "next-up" → "Implemented"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 15:50:14 -07:00
co-authored by Claude Opus 4.7
parent 95b8e0c1b5
commit dc01a0fefb
2 changed files with 20 additions and 16 deletions
+17 -13
View File
@@ -10,7 +10,7 @@ Date of handoff: 2026-05-06. Last commit before handoff: Swiftly token auto-mint
The project completed a **recovery pass** (R1+R2+R3-0) from 2026-05-04 to 2026-05-05 (12 defects from a prior agent fixed), then shipped **thin Phase 4** (recipe engine + ingredient↔grocery match layer + 30-recipe seed) and **Phase 9** (meal-planner generation algorithm: filter → score → top-K=20 set enumeration with diversity penalty → persist MealPlan + items via `POST /api/admin/meal-plans/generate`). The project completed a **recovery pass** (R1+R2+R3-0) from 2026-05-04 to 2026-05-05 (12 defects from a prior agent fixed), then shipped **thin Phase 4** (recipe engine + ingredient↔grocery match layer + 30-recipe seed) and **Phase 9** (meal-planner generation algorithm: filter → score → top-K=20 set enumeration with diversity penalty → persist MealPlan + items via `POST /api/admin/meal-plans/generate`).
The project's reason to exist is now real and verified end-to-end. 88/88 pytest tests pass. The project's reason to exist is now real and verified end-to-end. 92/92 pytest tests pass.
**Operator toil eliminated.** Swiftly bearer JWTs are now auto-minted via Firebase REST anon-signUp (`backend/app/services/swiftly_auth.py`); the `SWIFTLY_BEARER_TOKEN` env var is gone. Cache hit ratio in steady state is ~99% (one mint per ~hour). See `docs/specs/2026-05-06-swiftly-token-auto-mint.md` (status: Implemented). **Operator toil eliminated.** Swiftly bearer JWTs are now auto-minted via Firebase REST anon-signUp (`backend/app/services/swiftly_auth.py`); the `SWIFTLY_BEARER_TOKEN` env var is gone. Cache hit ratio in steady state is ~99% (one mint per ~hour). See `docs/specs/2026-05-06-swiftly-token-auto-mint.md` (status: Implemented).
@@ -29,10 +29,11 @@ The project's reason to exist is now real and verified end-to-end. 88/88 pytest
- Email backend (`backend/app/services/email.py`): Protocol + `ConsoleEmailBackend` (writes JSONL to `backend/var/email_outbox.jsonl`) + `SendGridEmailBackend` stub that raises `NotImplementedError`. Selected via `EMAIL_BACKEND` env (default `console`). - Email backend (`backend/app/services/email.py`): Protocol + `ConsoleEmailBackend` (writes JSONL to `backend/var/email_outbox.jsonl`) + `SendGridEmailBackend` stub that raises `NotImplementedError`. Selected via `EMAIL_BACKEND` env (default `console`).
- 401 from Swiftly is now rare (we always send a freshly minted JWT). When it does happen, `SwiftlyAuthError` carries a message pointing at the auto-mint spec; mint failures upstream surface as `SwiftlyAuthMintError`. Either lands verbatim in `ScrapeLog.error_message` via the bg runner. - 401 from Swiftly is now rare (we always send a freshly minted JWT). When it does happen, `SwiftlyAuthError` carries a message pointing at the auto-mint spec; mint failures upstream surface as `SwiftlyAuthMintError`. Either lands verbatim in `ScrapeLog.error_message` via the bg runner.
- Thin Phase 4: ingredient + recipe CRUD endpoints with admin gating; NeverSuggest CRUD (covers both ingredient blocklist and recipe blocklist via the existing schema); ingredient↔grocery_item match layer (rapidfuzz top-3 ranking with confidence threshold 0.75, manual override via /api/admin/ingredients/{id}/matches and /api/admin/ingredient-matches/{id}); 50 canonical ingredients seeded with aliases enriching pre-existing rows from migration 0002; 30 starter recipes spanning chicken/beef/turkey/pork/fish/vegetarian with varied cuisines, all under 45 min for 28/30. Match job runs after each successful scrape; matcher failures don't flip the scrape to FAILED. - Thin Phase 4: ingredient + recipe CRUD endpoints with admin gating; NeverSuggest CRUD (covers both ingredient blocklist and recipe blocklist via the existing schema); ingredient↔grocery_item match layer (rapidfuzz top-3 ranking with confidence threshold 0.75, manual override via /api/admin/ingredients/{id}/matches and /api/admin/ingredient-matches/{id}); 50 canonical ingredients seeded with aliases enriching pre-existing rows from migration 0002; 30 starter recipes spanning chicken/beef/turkey/pork/fish/vegetarian with varied cuisines, all under 45 min for 28/30. Match job runs after each successful scrape; matcher failures don't flip the scrape to FAILED.
- Phase 9: meal-plan generation. POST /api/admin/meal-plans/generate runs the full filter→score→set-select pipeline against seeded recipes and produces a persisted MealPlan with up to 3 MealPlanItem dinners. Regenerate endpoint accepts relaxed constraint overrides (`relax_time_max_minutes`, `relax_calorie_pct`, `relax_max_meal_cost`) and deletes any prior plan for the same `(family, week_start_date)` before re-running. Per-meal cost matched against ingredient_grocery_match using the top-confidence grocery row. 88 tests green. - Phase 9: meal-plan generation. POST /api/admin/meal-plans/generate runs the full filter→score→set-select pipeline against seeded recipes and produces a persisted MealPlan with up to 3 MealPlanItem dinners. Regenerate endpoint accepts relaxed constraint overrides (`relax_time_max_minutes`, `relax_calorie_pct`, `relax_max_meal_cost`) and deletes any prior plan for the same `(family, week_start_date)` before re-running. Per-meal cost matched against ingredient_grocery_match using the top-confidence grocery row.
- **Swiftly auto-mint** (`backend/app/services/swiftly_auth.py`): `get_token()` returns a Firebase anon-signUp JWT, cached in process memory until exp 5min. `mint_anonymous_token()` fetches `firebaseApiKey` from `luckysupermarkets.com/config.json`, posts to `identitytoolkit.googleapis.com/v1/accounts:signUp` with `Origin`/`Referer` set to `https://luckysupermarkets.com`, validates `iss`/`exp` on the returned JWT. `LuckyCaliforniaScraper.fetch_category()` calls it; mint failures surface as `SwiftlyAuthMintError`. Live-verified 2026-05-06: 10,928 items scraped in 44s, 29,779 ingredient_grocery_match rows produced.
### Database ### Database
- Postgres 15. Seven migrations: `0001_initial_migration`, `0002_seed_data`, `0003_grocery_item_description`, `0004_family_profile_calorie_target`, `0005_grocery_item_external_id`, `0006_thin_phase4` (ingredient.aliases, recipe.calories_per_serving, ingredient_grocery_match), `0007_seed_canonical_ingredients` (50 ingredients + 30 recipes). - Postgres 15. Seven migrations head-at: `0001_initial_migration`, `0002_seed_data`, `0003_grocery_item_description`, `0004_family_profile_calorie_target`, `0005_grocery_item_external_id`, `0006_thin_phase4` (ingredient.aliases, recipe.calories_per_serving, ingredient_grocery_match), `0007_seed_canonical_ingredients` (50 ingredients + 30 recipes).
- `0001` downgrade now does a `DO $$ … DROP TABLE … DROP TYPE … END $$;` block that preserves `alembic_version`. Round-trip works. - `0001` downgrade now does a `DO $$ … DROP TABLE … DROP TYPE … END $$;` block that preserves `alembic_version`. Round-trip works.
- `0002` seed is idempotent (`ON CONFLICT (name_lower) DO NOTHING`). - `0002` seed is idempotent (`ON CONFLICT (name_lower) DO NOTHING`).
- Every `SQLEnum(...)` column carries `values_callable=lambda obj: [e.value for e in obj]` — without this, name-mode breaks reads against the lowercase Postgres enum values. - Every `SQLEnum(...)` column carries `values_callable=lambda obj: [e.value for e in obj]` — without this, name-mode breaks reads against the lowercase Postgres enum values.
@@ -45,8 +46,8 @@ The project's reason to exist is now real and verified end-to-end. 88/88 pytest
- **No login UI yet.** No feedback page. No tests. - **No login UI yet.** No feedback page. No tests.
### Tests ### Tests
- 59 tests under `backend/tests/` (31 prior + Phase 4 additions: ingredient/recipe schema + API, matcher, match-hook, never-suggest, resolve-ingredient, thin-phase-4 smoke). All green when `TEST_DATABASE_URL` is set; `requires_postgres` marker auto-skips locally without it. - **92 tests** under `backend/tests/` — 88 from Phase 4 + Phase 9 work, plus 4 new in `test_swiftly_auth.py` (mint, cache hit, near-expiry re-mint, Firebase non-200 → SwiftlyAuthMintError). All green when `TEST_DATABASE_URL` is set; `requires_postgres` marker auto-skips locally without it.
- Live spike scripts in `scripts/`: `spike_lucky_scrape.py` (R2-A archived path), `send_test_approval.py` (email round-trip prover, supports `--simulate-click {approve|deny}`), `spike_swiftly_ingest.py` (R3-0 live ingestion prover; requires `--confirm-live`). - Live spike scripts in `scripts/`: `spike_lucky_scrape.py` (R2-A archived path), `send_test_approval.py` (email round-trip prover, supports `--simulate-click {approve|deny}`), `spike_swiftly_ingest.py` (live Swiftly ingestion prover; requires `--confirm-live`; uses auto-minted JWT — no env var needed).
### CI ### CI
- `.github/workflows/ci.yml`: backend job (postgres:15 service, alembic + pytest) + frontend job (npm ci + build). Triggers on push and pull_request. - `.github/workflows/ci.yml`: backend job (postgres:15 service, alembic + pytest) + frontend job (npm ci + build). Triggers on push and pull_request.
@@ -56,7 +57,7 @@ The project's reason to exist is now real and verified end-to-end. 88/88 pytest
## What is stubbed or missing ## What is stubbed or missing
### Phase 4 — Recipe Engine (thin slice complete) ### Phase 4 — Recipe Engine (thin slice complete)
- Thin slice landed: ingredient + recipe CRUD, NeverSuggest CRUD, ingredient↔grocery match layer with rapidfuzz + manual override, 50 canonical ingredients + 30 starter recipes seeded. 59/59 tests green. - Thin slice landed: ingredient + recipe CRUD, NeverSuggest CRUD, ingredient↔grocery match layer with rapidfuzz + manual override, 50 canonical ingredients + 30 starter recipes seeded.
- Phase 4 ingestion source (Spoonacular / TheMealDB / manual-only) — pros/cons table in `docs/specs/2026-05-05-meal-planner-algorithm-design.md` §6; decision deferred until Phase 9 lands. - Phase 4 ingestion source (Spoonacular / TheMealDB / manual-only) — pros/cons table in `docs/specs/2026-05-05-meal-planner-algorithm-design.md` §6; decision deferred until Phase 9 lands.
- Still thin: full-text recipe search, advanced tag filtering, bulk import endpoints — punted until the engine demonstrates which surfaces it actually needs. - Still thin: full-text recipe search, advanced tag filtering, bulk import endpoints — punted until the engine demonstrates which surfaces it actually needs.
@@ -125,7 +126,7 @@ docker compose --env-file .env.test exec backend alembic upgrade head
# Tests # Tests
docker compose --env-file .env.test exec \ docker compose --env-file .env.test exec \
-e TEST_DATABASE_URL=postgresql://mealplanner:${POSTGRES_PASSWORD}@db:5432/mealplanner \ -e TEST_DATABASE_URL=postgresql://mealplanner:${POSTGRES_PASSWORD}@db:5432/mealplanner \
backend pytest -q tests/ # → 59 passed backend pytest -q tests/ # → 92 passed
# Frontend # Frontend
cd frontend && npm ci && npm run build cd frontend && npm ci && npm run build
@@ -200,18 +201,21 @@ backend/app/
│ └── shopping_list.py │ └── shopping_list.py
├── scraper/ ├── scraper/
│ ├── base.py rate-limited HTTP base │ ├── base.py rate-limited HTTP base
│ └── lucky_ca_scraper.py Swiftly JSON API client (R3-0) │ └── lucky_ca_scraper.py Swiftly JSON API client; mints via swiftly_auth.get_token()
├── services/ ├── services/
│ ├── approval.py per-voter token issue/verify/consume │ ├── approval.py per-voter token issue/verify/consume
│ ├── email.py Console + SendGrid stub │ ├── email.py Console + SendGrid stub
── scraper_service.py enqueue + bg runner ── matcher.py ingredient ↔ grocery rapidfuzz scorer
│ ├── planner/ Phase 9 filter / score / select / orchestrator
│ ├── scraper_service.py enqueue + bg runner; runs matcher post-scrape
│ └── swiftly_auth.py Firebase REST anon-signUp; process-local JWT cache
├── security.py require_admin / require_session ├── security.py require_admin / require_session
├── config.py pydantic Settings ├── config.py pydantic Settings (no SWIFTLY_BEARER_TOKEN — auto-minted)
├── database.py engine / SessionLocal / get_db ├── database.py engine / SessionLocal / get_db
└── models/__init__.py all SQLAlchemy models └── models/__init__.py all SQLAlchemy models
backend/alembic/versions/ 0001 → 0005 backend/alembic/versions/ 0001 → 0007
backend/tests/ 31 tests backend/tests/ 92 tests (incl. test_swiftly_auth.py)
backend/tests/fixtures/lucky_ca/ categories.html, category_meat_seafood.json, weekly_ad.html (R2-A archive) backend/tests/fixtures/lucky_ca/ categories.html, category_meat_seafood.json, weekly_ad.html (R2-A archive)
scripts/ scripts/
@@ -221,7 +225,7 @@ scripts/
docs/specs/ docs/specs/
├── 2026-05-05-meal-planner-algorithm-design.md Phase 9 + thin Phase 4 design ├── 2026-05-05-meal-planner-algorithm-design.md Phase 9 + thin Phase 4 design
└── 2026-05-06-swiftly-token-auto-mint.md next-up: replace SWIFTLY_BEARER_TOKEN env var └── 2026-05-06-swiftly-token-auto-mint.md Implemented; auto-minted JWT replaces SWIFTLY_BEARER_TOKEN
.github/workflows/ci.yml backend (postgres + pytest) + frontend (npm build) .github/workflows/ci.yml backend (postgres + pytest) + frontend (npm build)
``` ```
+3 -3
View File
@@ -47,7 +47,7 @@ web ─► nginx :80/:443 ─► React/Vite ────┼─► FastAPI ─
| 10 | Image strategy (scraped + AI fallback) | Not started | | 10 | Image strategy (scraped + AI fallback) | Not started |
| 11 | Polish (variety analysis, budget tracking, APScheduler) | Not started | | 11 | Polish (variety analysis, budget tracking, APScheduler) | Not started |
Verification gate (R1+R2 + R3-0): 31/31 pytest green; alembic upgrade→downgrade→upgrade clean; frontend `npm run build` clean; live scrape persists 9,960 grocery_item rows in 36 s; email approval round-trip (approve/deny/single-use) verified end-to-end. Verification gate (current, 2026-05-06): **92/92 pytest green**; alembic upgrade→downgrade→upgrade clean; frontend `npm run build` clean; live scrape persists **9,980 grocery_item rows in 44 s** with auto-minted JWT (no env var); 29,779 ingredient_grocery_match rows produced; email approval round-trip (approve/deny/single-use) verified end-to-end.
--- ---
@@ -69,7 +69,7 @@ Conventions: UUID PKs everywhere, `TIMESTAMPTZ`, Postgres ENUMs (with `values_ca
Key relationships: `family_profile``family_member`; `family_member``meal_plan_vote` (per-voter); `recipe.ingredients` JSONB (no recipe-ingredient join table); `grocery_item.(source, external_id)` is the upsert key for scrape ingestion. Key relationships: `family_profile``family_member`; `family_member``meal_plan_vote` (per-voter); `recipe.ingredients` JSONB (no recipe-ingredient join table); `grocery_item.(source, external_id)` is the upsert key for scrape ingestion.
Migrations applied: 0001 initial, 0002 seed (idempotent via `ON CONFLICT DO NOTHING`), 0003 grocery_item.description, 0004 family_profile.calorie_target, 0005 grocery_item.external_id + source + composite index. Migrations applied: 0001 initial, 0002 seed (idempotent via `ON CONFLICT DO NOTHING`), 0003 grocery_item.description, 0004 family_profile.calorie_target, 0005 grocery_item.external_id + source + composite index, 0006 thin Phase 4 (ingredient.aliases, recipe.calories_per_serving, ingredient_grocery_match), 0007 seed canonical ingredients (50) + 30 starter recipes.
Full schema: `docs/database-schema.md`. Full schema: `docs/database-schema.md`.
@@ -113,7 +113,7 @@ The Swiftly bearer JWT is auto-minted at request time via Firebase REST anon-sig
# Full local stack # Full local stack
docker compose --env-file .env.test up -d db backend docker compose --env-file .env.test up -d db backend
docker compose --env-file .env.test exec backend alembic upgrade head docker compose --env-file .env.test exec backend alembic upgrade head
docker compose --env-file .env.test exec -e TEST_DATABASE_URL=postgresql://mealplanner:${POSTGRES_PASSWORD}@db:5432/mealplanner backend pytest -q tests/ docker compose --env-file .env.test exec -e TEST_DATABASE_URL=postgresql://mealplanner:${POSTGRES_PASSWORD}@db:5432/mealplanner backend pytest -q tests/ # → 92 passed
# Frontend # Frontend
cd frontend && npm ci && npm run build cd frontend && npm ci && npm run build