Public Access
fix: address adversarial review blockers
All §1 consensus blockers and §2 high-risk gaps resolved: Schema fixes: - Remove RecipeIngredient join table, use JSONB for ingredients - Add family_member table for per-voter approval tracking - Add all ENUMs for status fields (no loose VARCHAR) - Add CHECK constraints (household_size, rating 1-5, day_of_week) - Add name_lower for case-insensitive ingredient matching - Add grocery_item → ingredient FK - Fix day_of_week to ISO-8601 (1=Monday, 7=Sunday) - Remove calorie_target (nutrition is non-goal) Approval flow redesign: - Email link → confirmation page (GET), not auto-approve - Actual vote is POST from confirmation page - Per-voter tokens (single-use, 72h TTL) - Record which member voted Auth model: - VPN-only for admin endpoints - Session-based for family web UI Docker hardening: - Remove direct port exposure for backend/frontend - nginx is sole entrypoint - Add docker-compose.dev.yml for local dev Skeleton fixes: - Add missing Pantry.tsx page - Add missing index.html (Vite entrypoint) - Add package-lock.json - Fix SQLAlchemy 2 text() for raw SQL - Remove create_all from startup (use migrations) - Configure Alembic properly Docs updates: - Update Lucky URL to luckysupermarkets.com - Add WCAG 2.1 AA accessibility target - Update family profile with correct mushroom preferences - Add external dependencies list to SPEC Verification: - docker compose config: PASS - docker compose build backend: PASS - docker compose build frontend: PASS - backend import: PASS - alembic context: PASS
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
# Adversarial Documentation Review — Claude
|
||||
|
||||
Scope: `docs/SPEC.md`, `docs/ARCHITECTURE.md`, `docs/database-schema.md`, `docs/implementation-plan.md`, `docs/ORIENTATION.md`, `docs/RUNNING.md`, `README.md`, `meal-planner-plan.md`, `.env.example`, `docker-compose.yml`. Reviewed independently of the GPT-5.5 review.
|
||||
|
||||
Tone: adversarial. Findings ranked Blocking / Major / Minor. Cite file:line where useful.
|
||||
|
||||
---
|
||||
|
||||
## Verdict
|
||||
|
||||
The docs read as a coherent first draft, but the spec, architecture, and schema do not agree with each other on three load-bearing surfaces: **how recipes link to ingredients, how approvals identify a voter, and what the authentication model is** (there isn't one). The mushroom/family-profile section is mathematically self-contradictory. Several "self-hosted" claims are undermined by hard external dependencies (SendGrid, AI image APIs, scraping third-party sites). Phase 1 was committed before these contradictions were resolved, so the migration written next will encode whichever side of the contradiction the implementer happens to read first.
|
||||
|
||||
Recommendation: **do not start Phase 2 (DB & Models) until SPEC §6, ARCHITECTURE §2.2/§3, and `database-schema.md` §2.3 are reconciled** — the migration is the wrong place to make these decisions.
|
||||
|
||||
---
|
||||
|
||||
## Blocking
|
||||
|
||||
### B1. Family profile is internally inconsistent — the math doesn't close
|
||||
|
||||
`docs/SPEC.md:79-88` states household = 2 adults + 2 children. Then:
|
||||
- "One adult likes mushrooms" → 1 adult
|
||||
- "One child is OK with mushrooms" → 1 child
|
||||
- "Two adults and one child do NOT like mushrooms" → 2 adults + 1 child
|
||||
|
||||
Adult count: 1 + 2 = **3 adults claimed in a 2-adult household**. Either the spec is wrong about the household composition, the preference list is wrong, or this is meant to illustrate a structure (per-person preferences) that the schema does not actually model — see B2.
|
||||
|
||||
This is the single concrete worked example in the entire spec. If it doesn't typecheck, neither does anything downstream that consumes it (planner constraints, denial reasons, "never suggest" semantics).
|
||||
|
||||
### B2. There is no `family_member` (or `user`) table — but the spec is per-person
|
||||
|
||||
SPEC §6 talks about individual preferences ("one adult likes…"). SPEC §7 sends the proposal to "both adults" and treats their votes asymmetrically ("if either denies → swap"). Yet `database-schema.md` has only `family_profile` (a household row) and `feedback` keyed by `meal_plan_item_id` with `UNIQUE(meal_plan_item_id)` (`database-schema.md:147`).
|
||||
|
||||
Consequences:
|
||||
- Cannot record *which* family member likes/dislikes mushrooms.
|
||||
- Cannot record *which* adult denied a meal — both share one `approval_token` per `meal_plan_item` (`database-schema.md:104`). Whoever clicks first wins; the other can silently overwrite, and the system can't tell them apart.
|
||||
- The `UNIQUE` on feedback means a household gets exactly one rating per meal — a regression from per-member learning that the spec implies.
|
||||
- "Send to both adult email addresses" (`implementation-plan.md:234`) has no schema support — addresses live only in env vars (`FAMILY_EMAIL_1`, `FAMILY_EMAIL_2`), which means you cannot extend to in-laws/guests/kid-with-phone without a redeploy.
|
||||
|
||||
Fix the model before writing the migration. Either add `family_member(id, family_profile_id, name, role, email)` and key tokens/feedback off it, or explicitly downgrade the spec to "one shared mailbox, one shared opinion."
|
||||
|
||||
### B3. Recipe↔ingredient relationship is documented two contradictory ways
|
||||
|
||||
- `ARCHITECTURE.md:201, 241` — N:N via `recipe_ingredient` join table, drawn in the ASCII diagram.
|
||||
- `database-schema.md:66` — `recipe.ingredients JSONB NOT NULL`, no join table defined.
|
||||
|
||||
These are not equivalent. JSONB precludes the FK indexes shown later (`idx_…` in `database-schema.md` §3.2) for ingredient-driven planner queries ("find recipes that use this on-sale item"), which is the **core meal-planner query**. The planner inputs at `ARCHITECTURE.md:94-108` cannot be served efficiently by a JSONB column without a GIN index that no one has specified, and even then ingredient-level operations (substitution, pantry subtraction, sale lookup) become awkward.
|
||||
|
||||
Pick one. If JSONB is genuinely the choice, the schema doc must justify it and add the GIN index; if the join table is the choice, the JSONB column must come out of the recipe row. Until then, `Recipe` SQLAlchemy model is being asked to be both shapes simultaneously.
|
||||
|
||||
### B4. There is no authentication anywhere in the system
|
||||
|
||||
Search across all docs for `auth`, `login`, `password`, `session`, `user` returns: nothing actionable. Yet:
|
||||
- The web UI has a "Pantry Manager", "Approval Center", and "Admin/Settings" page (`ARCHITECTURE.md:159-165`).
|
||||
- "Remote access via reverse proxy" is a stated requirement (`SPEC.md:128-129`, `ARCHITECTURE.md:337`).
|
||||
- `docker-compose.yml` exposes port 80/443 publicly.
|
||||
- `POST /api/admin/scrape` and `GET /api/admin/logs` (`ARCHITECTURE.md:267-268`) are unauthenticated.
|
||||
|
||||
"Family of 4 inside the house" is not a security boundary once `RUNNING.md` describes Let's Encrypt + a public domain. Anyone on the internet who guesses the URL can trigger scrapes, dump logs (potentially containing emails and SendGrid IDs from `email_log`), edit the pantry, or change the family profile. The `SECRET_KEY=change-me-to-a-random-secret-key` placeholder in `.env.example` is referenced by no doc — what does it sign? If JWTs, where? If session cookies, against what user table?
|
||||
|
||||
The "VPN option" and "IP whitelist capability" mentioned at `ARCHITECTURE.md:339-340` are not a substitute for auth on a system that explicitly accepts approval/denial state mutations from email links.
|
||||
|
||||
### B5. Approval-by-email is fragile and probably exploitable
|
||||
|
||||
`ARCHITECTURE.md:271-273`:
|
||||
```
|
||||
GET /api/approve/{token} → Mark meal approved
|
||||
GET /api/deny/{token} → Show denial reason form
|
||||
```
|
||||
|
||||
Two distinct problems:
|
||||
1. **GET mutates state.** Many corporate/AV email scanners (Microsoft Defender SafeLinks, Proofpoint URL Defense, GMail link prefetch) and some clients pre-fetch every link in an email to scan it. Every meal will be silently auto-approved before the human sees the message. This is a known, common, well-documented failure mode for "click the link to approve" patterns. Fix: the email link must lead to a confirmation page that POSTs.
|
||||
2. **Token model gives no per-voter accountability.** See B2. Combined with B4 (no login on the confirmation page), once a token leaks (forwarded email, screenshot in a chat, archived in a mailbox) anyone can vote. `approval_token_expires` is nullable (`database-schema.md:105`), so absent application logic the token is permanent.
|
||||
|
||||
Also: `denial_reason VARCHAR(50)` with the values listed in `database-schema.md:106` should be a CHECK or enum; otherwise denial-reason analytics in the learning step will be polluted by typos.
|
||||
|
||||
### B6. "Self-hosted, no external cloud services *except SendGrid*" is undermined
|
||||
|
||||
`SPEC.md:130-131` makes a clean claim, then immediately exempts SendGrid, then `ARCHITECTURE.md` adds:
|
||||
- "AI Image Service (DALL-E, Anthropic, etc.)" (`ARCHITECTURE.md:351-356`)
|
||||
- "Centralized logging optional (Papertrail, Datadog)" (`ARCHITECTURE.md:397`)
|
||||
- "Caddy or Nginx with Let's Encrypt" (an external CA) (`ARCHITECTURE.md:338`)
|
||||
- Recipe site scraping (third-party, see B7)
|
||||
- Hotlinked image URLs from third-party recipe sites (`ARCHITECTURE.md:347-350`)
|
||||
|
||||
If the user genuinely cares about self-hosting (the README leads with it), at minimum SPEC §8 needs to enumerate every external dependency, not just SendGrid. If they don't, drop the "self-hosted, no external services" framing — it's marketing copy at this point.
|
||||
|
||||
---
|
||||
|
||||
## Major
|
||||
|
||||
### M1. Scraping legality / ToS unaddressed
|
||||
|
||||
`SPEC.md:132-136` and `ARCHITECTURE.md §2.1` describe scraping Lucky California's weekly ad, product catalog, *and* "public recipe sites". "Respect robots.txt and rate limiting" is the entire policy. There is no:
|
||||
- Review of Lucky California's ToS (most US grocery retailers prohibit scraping).
|
||||
- Plan for Cloudflare/bot-protection bypass (Playwright is mentioned, suggesting the author already knows static fetch will be blocked — `RUNNING.md:225` even has a manual Playwright retry incantation).
|
||||
- Recipe-site copyright analysis (image hotlinking + caching scraped images is copying protected content — `ARCHITECTURE.md:347` calls it out as "stored as URLs, not downloaded" but `ARCHITECTURE.md:354` says generated images are "cached in database", and `recipe.image_source = 'scraped'` (`database-schema.md:56`) implies persistence).
|
||||
|
||||
A homelab project for one family is unlikely to attract a takedown, but a doc that says "this is the architecture" should at least name the risk.
|
||||
|
||||
Also worth verifying: `LUCKY_CA_URL=https://www.luckyncal.com` (`.env.example:14`, `RUNNING.md:39`). The actual chain's site is `luckysupermarkets.com`. `luckyncal.com` *might* be the franchise's California-specific site, but no doc cites the source. If the URL is wrong, every Phase 4 task is built on sand.
|
||||
|
||||
### M2. Phase ordering buries the hard problems behind weeks of plumbing
|
||||
|
||||
`implementation-plan.md` puts the actually risky work — scraping (Phase 4), planner algorithm (Phase 6), email/approval (Phase 7) — *after* full backend skeleton, full schema, full API surface, and most of the React UI. Each of those phases is the one most likely to fail or force schema changes; deferring them maximizes rework cost.
|
||||
|
||||
A more honest plan would build a minimal "can I scrape one ad page" and "can I email + receive an approval click" spike before committing to a 12-table schema and 6 React pages. Otherwise you'll do Phase 2 twice.
|
||||
|
||||
### M3. APScheduler + multiple workers will misfire
|
||||
|
||||
`implementation-plan.md:154` picks APScheduler. `docker-compose.yml` and the Dockerfile docs don't mention worker count, but `uvicorn --workers N` is the default FastAPI deployment. APScheduler in-process will fire the weekly scrape *N times* if N>1. No doc mentions a leader-election scheme, a separate `scheduler` service, or `--workers 1`. Either pin to one worker (and document it) or move scheduling to a dedicated container / cron.
|
||||
|
||||
Same hazard: per-worker state for retry counters, image caches, etc.
|
||||
|
||||
### M4. Schema has avoidable defects
|
||||
|
||||
- `meal_plan_item.day_of_week` 0=Monday (`database-schema.md:101`). Postgres `EXTRACT(DOW)` is 0=Sunday; ISO-8601 is 1=Monday. Pick ISO and document it; "0=Monday" is the worst of three options because it silently disagrees with both common conventions.
|
||||
- `recipe.total_time_minutes` is documented as "Computed: prep + cook" (`database-schema.md:59`) but stored as a plain INTEGER. Make it a Postgres `GENERATED ALWAYS AS … STORED` column or remove it. As written, it will drift the first time someone backfills.
|
||||
- `family_profile.household_size` with separate `adult_count`/`child_count` and no CHECK that they sum (`database-schema.md:21-23`). Trivial CHECK; add it.
|
||||
- `feedback.meal_plan_item_id UNIQUE` (`database-schema.md:147`) — see B2.
|
||||
- `home_pantry.unit` and `ingredient.unit` (`database-schema.md:41, 125`) are duplicate truth. The pantry-vs-recipe unit-mismatch problem (cans vs oz vs cups) is real but not addressed — `implementation-plan.md:184` waves at "handle unit conversions" with no design.
|
||||
- `grocery_item` has no FK to `ingredient` (`database-schema.md §2.9`). The "map products to existing ingredients or create new ones" step (`implementation-plan.md:147`) is the entire bridge between scraping and planning, and the schema gives it no place to live. A `grocery_item.ingredient_id` (nullable until matched) is the obvious missing column.
|
||||
- `ingredient.name UNIQUE` (`database-schema.md:37`) is case-sensitive. "Carrots" ≠ "carrots" ≠ "Carrot". Either lower-case-on-write or use `CITEXT`.
|
||||
- `denial_reason`, `image_source`, `meal_plan.status`, `feedback.denial_reason`, `email_log.status`, `scrape_log.status` — all are `VARCHAR(N)` with values enumerated in prose. Use Postgres ENUMs or CHECK constraints; otherwise "approved " (with a trailing space) is a valid status.
|
||||
|
||||
### M5. Migration story is muddled
|
||||
|
||||
`database-schema.md:251-257`: "Use SQLAlchemy or Alembic for schema management." These aren't alternatives — Alembic *is* SQLAlchemy's migration tool. Pick the wording. Also: `implementation-plan.md` Phase 1.1 commits to Alembic + initial migration *before* Phase 2 defines the SQLAlchemy models. Either Phase 1 ships an empty migration and Phase 2 generates the real one, or the order is wrong. As written, the doc says do them simultaneously.
|
||||
|
||||
If the Phase 1 commit (`1328ec3`) already created models or a migration, that drift is already in the repo and the docs need to catch up.
|
||||
|
||||
### M6. "If both approve OR no response → meal confirmed" is silence-as-consent
|
||||
|
||||
`SPEC.md:114`. Combined with B5, this means the default state of an unread email is **approved**. For a system whose pitch is "stop forcing me to log into apps", that's coherent — but the spec should *say* that explicitly, and the success metric "Approval rate >80% of proposed meals approved without changes" (`SPEC.md:166`) is meaningless if silence counts as approval. You'll hit 100% the first week the email lands in spam.
|
||||
|
||||
### M7. "Calorie target" appears in the schema but is a non-goal
|
||||
|
||||
`database-schema.md:26` defines `calorie_target INTEGER`. `SPEC.md:55` lists "Nutrition tracking beyond high-level calorie awareness" as a non-goal. Either the column has no consumer (dead schema), or §4 is wrong. Same tension between `database-schema.md:65` (`spice_level`) and the spec's silence on heat preferences.
|
||||
|
||||
### M8. Image strategy contradicts itself
|
||||
|
||||
`SPEC.md:54`: "AI image generation as primary source (scraped images first)" is listed as a *non-goal*, suggesting AI-as-primary is rejected. `ARCHITECTURE.md §6.2` and `implementation-plan.md §11.2` then build AI image generation as a fallback. "Non-goal: X as primary" and "build X as fallback" are not in conflict, but the wording in SPEC §4 reads as if AI generation is out entirely. Tighten the language so an implementer doesn't shave the AI-fallback task on a literal reading.
|
||||
|
||||
Also: the email image strategy says "Embed images via CDN URL or inline base64" (`ARCHITECTURE.md:359`). Inline base64 inflates emails by ~33% and is filtered by GMail above 102KB; "or" hides a real decision.
|
||||
|
||||
---
|
||||
|
||||
## Minor
|
||||
|
||||
### m1. Doc hygiene
|
||||
|
||||
- `implementation-plan.md:431` references `docs/implemenation-plan.md` (typo: missing `t`). Self-referential broken link.
|
||||
- `README.md` "Quick Start" section is empty per the indexer (just a header) — the file references `docs/RUNNING.md` for setup but offers no `docker compose up` one-liner up front.
|
||||
- `meal-planner-plan.md` is described as "Original planning file (may be superseded)" in `ORIENTATION.md`. Either delete it or freeze it with a banner; leaving "may be superseded" in a Key Files table guarantees someone reads it as canonical.
|
||||
- ORIENTATION says "Phase 1 complete" but `implementation-plan.md` checkboxes are all unticked. The plan is the canonical task list per the user's CLAUDE.md memory rules; update it.
|
||||
|
||||
### m2. Operational gaps not in any doc
|
||||
|
||||
- No backup/restore procedure (postgres volume, recipe images if ever downloaded, scraped catalog).
|
||||
- No CI plan. `pytest` is in `requirements.txt`-equivalent but no test phase, no test layout, no CI yaml. Phase verification commands are all `curl localhost:…` — fine for a smoke test, useless as regression coverage.
|
||||
- No accessibility commitment. The user's global CLAUDE.md mandates WCAG 2.1 AA for UI work; the React UI plan never mentions it.
|
||||
- No log redaction. `email_log` stores recipient emails; `scrape_log` may capture HTML; both are retained 30-90 days (`database-schema.md §5`). PII handling not addressed.
|
||||
- No rate-limiting design beyond "Rate limiting on approval endpoints" (`ARCHITECTURE.md:336`). What limit? Per IP? Per token? Per family?
|
||||
|
||||
### m3. Docker-compose exposure
|
||||
|
||||
`docker-compose.yml` (per the indexer) publishes ports for backend (8000), frontend (3000), *and* nginx (80/443). On a remote-accessible host, 8000 and 3000 should be `expose:` (intra-compose only), not `ports:` (host-published). Otherwise nginx routing is bypassable by hitting :8000 directly, and the rate limits on /api/* don't apply. Worth a one-line fix in the next iteration but doc-impacting because `RUNNING.md` and `ARCHITECTURE.md` both describe nginx as "the" entrypoint.
|
||||
|
||||
### m4. Success metrics aren't measurable
|
||||
|
||||
`SPEC.md §10`:
|
||||
- "Family consistently uses the system weekly" — undefined ("consistent" = ?).
|
||||
- ">80% approved without changes" — broken by silence-as-consent (M6).
|
||||
- "After 4 weeks, system should not propose previously denied meals" — that's a correctness invariant, not a metric. Should hold week 1.
|
||||
- "Within 150% of equivalent grocery-store meal" — equivalent how? No baseline source.
|
||||
|
||||
Either drop the metrics section or make them queryable from the schema.
|
||||
|
||||
### m5. Future features are mixed into the implementation plan
|
||||
|
||||
`implementation-plan.md §12.3` (WhatsApp) and `§12.4` (recipe scraping) are explicitly out-of-scope per `SPEC.md §4`/`§11`. Putting them in the implementation plan creates pressure to do them. Move to a separate "future" doc or remove the checkboxes.
|
||||
|
||||
### m6. `SECRET_KEY` ghost variable
|
||||
|
||||
`.env.example` defines `SECRET_KEY=change-me-to-a-random-secret-key`. No doc says what consumes it. If it's for FastAPI session/JWT signing, the auth model (B4) needs to exist first. If it's vestigial, drop it — placeholder secrets in `.env.example` get copied into real `.env` files unchanged more often than anyone admits.
|
||||
|
||||
---
|
||||
|
||||
## Cross-doc contradictions, summarized
|
||||
|
||||
| Topic | Doc A says… | Doc B says… |
|
||||
|---|---|---|
|
||||
| Recipe ingredients | join table `recipe_ingredient` (ARCHITECTURE.md:201, 241) | `recipe.ingredients JSONB` column (database-schema.md:66) |
|
||||
| Calorie tracking | non-goal (SPEC.md:55) | `calorie_target` column (database-schema.md:26) |
|
||||
| Self-hosting | "no external services except SendGrid" (SPEC.md:130) | DALL-E, Let's Encrypt, recipe scraping, hotlinked images, optional Datadog (ARCHITECTURE.md §6, §8.2) |
|
||||
| Reverse proxy | nginx (ARCHITECTURE.md §5.2) | "Caddy or Nginx" (ARCHITECTURE.md:338, implementation-plan.md:64) |
|
||||
| AI image generation | non-goal as primary (SPEC.md:54) | full Phase 11.2 fallback (implementation-plan.md §11.2) |
|
||||
| Meal types | "dinner only for MVP" (implementation-plan.md:198) | breakfast/lunch/dinner enum (database-schema.md:102) |
|
||||
| Migration tool | "SQLAlchemy or Alembic" (database-schema.md:251) | Alembic, Phase 1.1 (implementation-plan.md) |
|
||||
| Phase 1 status | complete (ORIENTATION.md) | all unchecked (implementation-plan.md) |
|
||||
| Voter identity | sent to "both adults", denial swaps (SPEC.md §7) | one shared `approval_token` per meal_plan_item, no member table (database-schema.md §2.5) |
|
||||
|
||||
---
|
||||
|
||||
## Suggested order of operations before any more code
|
||||
|
||||
1. **Resolve B1 + B2.** Decide whether the system models the household as one entity or as members. This is a 1-line decision that propagates everywhere.
|
||||
2. **Resolve B3.** Pick join-table or JSONB. Then update both docs.
|
||||
3. **Resolve B4.** Decide auth model: VPN-only (drop the public-internet framing), basic-auth at nginx, or app-level sessions. Pick one and put it in SPEC §8.
|
||||
4. **Resolve B5.** Switch email approval to a confirmation-page POST; document token TTL and per-voter scoping (which depends on B2).
|
||||
5. **Tighten SPEC §6 and §10** so the worked example typechecks and the metrics are queryable.
|
||||
6. *Then* regenerate the migration and proceed to Phase 2.
|
||||
|
||||
Phase 2 written today against the current docs will produce a schema that contradicts itself in three places and will be revised within two phases. The cheapest review is the one done before the migration ships.
|
||||
Reference in New Issue
Block a user