Public Access
docs: thin phase 4 complete; refresh ORIENTATION + HANDOFF
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
"""End-to-end: ingredient + recipe + match all wired together."""
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.requires_postgres
|
||||
|
||||
|
||||
def _admin() -> dict:
|
||||
return {"Authorization": "Bearer test-admin-token"}
|
||||
|
||||
|
||||
def test_seed_data_present_and_resolvable(client):
|
||||
r = client.get("/api/recipes")
|
||||
assert r.status_code == 200
|
||||
recipes = r.json()
|
||||
assert len(recipes) >= 30, f"expected >=30 seeded recipes, got {len(recipes)}"
|
||||
|
||||
|
||||
def test_resolve_against_seeded_ingredients(client):
|
||||
r = client.post(
|
||||
"/api/admin/recipes/resolve-ingredient",
|
||||
json={"text": "1 lb chicken thighs"},
|
||||
headers=_admin(),
|
||||
)
|
||||
assert r.status_code == 200
|
||||
candidates = r.json()["candidates"]
|
||||
assert candidates, "expected at least one candidate"
|
||||
assert "chicken" in candidates[0]["name"].lower()
|
||||
|
||||
|
||||
def test_match_job_runs_against_seeded_data(db_session):
|
||||
from app.services.matcher import run_match_job
|
||||
|
||||
written = run_match_job(db_session)
|
||||
assert written >= 0
|
||||
+11
-10
@@ -2,7 +2,7 @@
|
||||
|
||||
You are taking over a project in mid-flight. Read `docs/ORIENTATION.md` first for the high-level. This file is the deep dive: what's real, what's stubbed, where the bodies are buried, and what to do next.
|
||||
|
||||
Date of handoff: 2026-05-05. Last commit before handoff: `8e89f79`.
|
||||
Date of handoff: 2026-05-06. Last commit before handoff: thin Phase 4 (P4-15).
|
||||
|
||||
---
|
||||
|
||||
@@ -28,9 +28,10 @@ The project's reason to exist — the meal-planner generation algorithm — is *
|
||||
- Approval flow (`backend/app/services/approval.py` + meals router): per-voter `URLSafeTimedSerializer` tokens, TTL, single-use enforced in `consume_token`, GET renders an HTMLResponse vote page, POST records the vote and applies the rule (any deny → item denied; all approve → item approved; otherwise pending).
|
||||
- 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 raises `SwiftlyAuthError` carrying the verbatim message `"SWIFTLY_BEARER_TOKEN expired — request a fresh token from the user (capture from luckysupermarkets.com network tab on a /search/api/v1 request)"`. The bg runner catches it and writes `ScrapeLog.error_message` so it surfaces via the admin logs endpoint.
|
||||
- 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.
|
||||
|
||||
### Database
|
||||
- Postgres 15. Five migrations: `0001_initial_migration`, `0002_seed_data`, `0003_grocery_item_description`, `0004_family_profile_calorie_target`, `0005_grocery_item_external_id`.
|
||||
- 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).
|
||||
- `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`).
|
||||
- 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.
|
||||
@@ -43,7 +44,7 @@ The project's reason to exist — the meal-planner generation algorithm — is *
|
||||
- **No login UI yet.** No feedback page. No tests.
|
||||
|
||||
### Tests
|
||||
- 31 tests under `backend/tests/`: `test_smoke`, `test_alembic`, `test_config`, `test_auth`, `test_scrape_endpoint`, `test_approval`, `test_swiftly_api`. All green when `TEST_DATABASE_URL` is set; `requires_postgres` marker auto-skips locally without it.
|
||||
- 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.
|
||||
- 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`).
|
||||
|
||||
### CI
|
||||
@@ -53,10 +54,10 @@ The project's reason to exist — the meal-planner generation algorithm — is *
|
||||
|
||||
## What is stubbed or missing
|
||||
|
||||
### Phase 4 — Recipe Engine (not started)
|
||||
- `/api/recipes` accepts CRUD but doesn't filter by `never_suggest`, doesn't search, doesn't tag.
|
||||
- No recipe ingestion source. Decision needed: manual entry only? scrape from public recipe sites (NYT Cooking / Serious Eats / Smitten Kitchen)? AI-generated? user CSV import?
|
||||
- The schema is ready: `recipe.cuisine_tags`, `recipe.dietary_tags`, `recipe.protein_type`, `recipe.spice_level`, JSONB `ingredients` array.
|
||||
### 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.
|
||||
- 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.
|
||||
|
||||
### Phase 5 — Meal-planner orchestration (not started)
|
||||
- The weekly cycle: scrape Sunday → generate Monday → email Monday-evening → deadline Thursday → finalize Friday.
|
||||
@@ -105,7 +106,7 @@ The project's reason to exist — the meal-planner generation algorithm — is *
|
||||
|
||||
8. **Routes use `@router.get("")` (no trailing slash).** FastAPI's `redirect_slashes=True` (the default) will 307-redirect `/api/profile/` to `/api/profile`. Tests assert canonical paths (no slash). The frontend client matches.
|
||||
|
||||
9. **No `recipe` data exists.** Phase 4 needs an ingestion strategy before Phase 9 can do anything useful.
|
||||
9. **30 starter recipes seeded.** Migration 0007 loads them; enough to exercise Phase 9 against real data. Bulk ingestion source still deferred.
|
||||
|
||||
10. **Frontend doesn't have a login UI.** Until you build one, the family-facing flows can't actually be exercised by a real user — only by tests. The Dashboard/Pantry/etc. pages assume the cookie is already set.
|
||||
|
||||
@@ -123,7 +124,7 @@ docker compose --env-file .env.test exec backend alembic upgrade head
|
||||
# Tests
|
||||
docker compose --env-file .env.test exec \
|
||||
-e TEST_DATABASE_URL=postgresql://mealplanner:${POSTGRES_PASSWORD}@db:5432/mealplanner \
|
||||
backend pytest -q tests/ # → 31 passed
|
||||
backend pytest -q tests/ # → 59 passed
|
||||
|
||||
# Frontend
|
||||
cd frontend && npm ci && npm run build
|
||||
@@ -224,4 +225,4 @@ scripts/
|
||||
|
||||
Trust the tests. Trust the live runs. Don't trust prose claims that something is "complete" without running the verification gate yourself. The recovery happened because the prior agent did the latter without the former.
|
||||
|
||||
Last updated: 2026-05-05.
|
||||
Last updated: 2026-05-06.
|
||||
|
||||
+3
-3
@@ -31,14 +31,14 @@ web ─► nginx :80/:443 ─► React/Vite ────┼─► FastAPI ─
|
||||
|
||||
---
|
||||
|
||||
## Phase status (2026-05-05)
|
||||
## Phase status (2026-05-06)
|
||||
|
||||
| # | Phase | Status |
|
||||
|---|---|---|
|
||||
| 1 | Infra (Docker, FastAPI, React, nginx, Postgres) | **Complete** |
|
||||
| 2 | DB & models (Alembic, Pydantic schemas, API endpoints) | **Complete** (real, verified) |
|
||||
| 3 | Lucky California ingestion (Swiftly JSON API) | **Complete** — 17 categories, ~10k products live |
|
||||
| 4 | Recipe engine (CRUD, search, tagging, never-suggest filter) | Not started |
|
||||
| 4 | Recipe engine (CRUD, search, tagging, never-suggest filter) | **Thin slice complete** — recipe + ingredient CRUD, ingredient↔grocery match layer (rapidfuzz, manual override), NeverSuggest CRUD, 30-recipe seed. Ingestion source decision deferred (see spec). |
|
||||
| 5 | Meal planner orchestration (generate → email → vote → finalize) | Not started |
|
||||
| 6 | SendGrid email integration (proposal/reminder/confirmation) | Stub only — `app/services/email.py::SendGridEmailBackend` raises NotImplementedError |
|
||||
| 7 | Web UI core (Dashboard / Meal Detail / Pantry / Shopping List) | **Complete** (no auth UI yet) |
|
||||
@@ -149,4 +149,4 @@ A `.env.test` template lives in the repo root (gitignored) for local stack runs.
|
||||
|
||||
---
|
||||
|
||||
Last updated: 2026-05-05 — after R1+R2 stabilization + R3-0 Swiftly ingestion (commit `8e89f79`).
|
||||
Last updated: 2026-05-06 — after thin Phase 4 (ingredient + recipe CRUD, match layer, 50 ingredient + 30 recipe seed). 59/59 pytest green.
|
||||
|
||||
Reference in New Issue
Block a user