From 9f4537d0cb5013afdcfcc434b713aeb2e773b9c5 Mon Sep 17 00:00:00 2001 From: dev_agent Date: Sat, 3 Oct 2026 00:26:15 -0400 Subject: [PATCH] Feature 01 task 6: AGENTS.md, CLAUDE.md, README and architecture docs (#7) Co-authored-by: dev_agent Co-committed-by: dev_agent --- AGENTS.md | 143 ++++++++++++++++++++++++++ CLAUDE.md | 1 + README.md | 56 +++++++++++ docs/ARCHITECTURE.md | 235 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 435 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 docs/ARCHITECTURE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d306043 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,143 @@ +# AGENTS.md — MealQuest agent guide + +This is the authoritative guide for every agent (and human) working in this +repository. `CLAUDE.md` only imports this file. Where this guide and a general +habit disagree, this guide wins. + +MealQuest is a single Go service with an embedded React PWA for one +household. The architecture of record is [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md); +product requirements are in [design/MealQuest Requirements.md](design/MealQuest%20Requirements.md). + +## House rules + +1. **Fail fast.** Invalid configuration, input or state is an error reported + immediately and clearly — the service refuses to start on bad config. +2. **No silent fallbacks.** Never substitute a default, retry quietly, or + swallow an error to "keep going". If something is missing, say so. +3. **One way to do a thing.** No parallel code paths, alternative settings or + "just in case" options. Pick one approach and use it everywhere. +4. **No new third-party dependency without owner approval.** Use only the + approved list below; anything else needs the owner's explicit yes first. +5. **Never use `tea`.** The Gitea CLI on this host is authenticated as the + human owner. Agents act only through their own Gitea identities. + +## Approved dependencies (from CTXD) + +Backend: + +- Go 1.25, built with `CGO_ENABLED=0`. +- `github.com/go-chi/chi/v5` — HTTP router. +- `modernc.org/sqlite` — CGO-free SQLite driver. +- `sqlc` — query code generation (dev tool only). +- `github.com/coreos/go-oidc/v3` — OIDC (Authentik). +- `golang.org/x/oauth2` and `golang.org/x/oauth2/google`. +- `github.com/SherClockHolmes/webpush-go` — Web Push. +- `golang.org/x/image` — photo processing. +- `github.com/oklog/ulid/v2` — IDs. +- Standard library `log/slog` and `net/http`. +- **Not allowed:** LLM vendor SDKs, Google API SDKs (call the HTTP APIs + directly), goose, golang-migrate. + +Frontend: + +- React 19, TypeScript, Vite. +- React Router 7, TanStack Query 5. +- `vite-plugin-pwa`. +- `lucide-react` icons at stroke width 2.75. +- CSS Modules over the design tokens in + [tokens.css](design/MealQuest%20UI%20Designs/design_handoff_mealquest/tokens.css). + **No Tailwind, no Framer Motion, no hard-coded hex colours.** +- Self-hosted fonts: Bagel Fat One and Figtree. +- Tests: Vitest, Testing Library, jsdom. + +## Quality floor + +Every change must pass all of these before review (CI in +[.gitea/workflows/ci.yml](.gitea/workflows/ci.yml) enforces them): + +```sh +gofmt -l . # must print nothing +go vet ./... +go test -race ./... +cd web && npm run typecheck && npm test && npm run build +``` + +And the build entry points in the [Makefile](Makefile) and [Dockerfile](Dockerfile): + +```sh +make test +make build +docker build . +``` + +Lint proves code parses, not that it works: where a change has runtime +behaviour, exercise it (hit the endpoint, render the page). + +## Repository layout + +| Path | Purpose | +| --- | --- | +| `cmd/server` | Entry point. Wiring only — no business logic. | +| `internal/config` | `MEALQUEST_*` environment config; secrets from `*_FILE`. | +| `internal/server` | HTTP server, routes, `/health`. | +| `internal/frontend` | `go:embed` of the built PWA with SPA fallback. | +| `internal/*` (planned) | `auth`, `db`, `store`, `secretbox`, `household`, `dinners`, `recipes`, `ai/llm`, `ai/harness`, `ai/skills`, `suggest`, `plan`, `shopping`, `ideas`, `gcal`, `notify`, `push`, `chat`, `jobs`, `scheduler`. | +| `migrations/` | SQL migrations (planned). | +| `web/` | React PWA source. | +| `deploy/` | Deployment files (planned). | +| `docs/` | Project documentation, including [ARCHITECTURE.md](docs/ARCHITECTURE.md). | +| `design/` | Requirements and UI handoff. **Read-only reference** — do not edit. | +| `Dockerfile` | Multi-stage image build, at the repository root. | +| `Makefile` | `dev`, `test`, `build`, `docker`. | +| `.gitea/workflows/ci.yml` | CI quality floor. | + +Design references: + +- [design/MealQuest Requirements.md](design/MealQuest%20Requirements.md) +- [design/MealQuest UI Designs/design_handoff_mealquest/README.md](design/MealQuest%20UI%20Designs/design_handoff_mealquest/README.md) +- [design/MealQuest UI Designs/design_handoff_mealquest/tokens.css](design/MealQuest%20UI%20Designs/design_handoff_mealquest/tokens.css) + +## Branches, pull requests and merges + +- Integration branch is `dev`. `main` is the release branch. +- **Feature branches:** `feature/-`, branched from `dev`. +- **Task branches:** `task/_`, branched from the feature branch. +- Task pull requests **squash-merge into the feature branch**. +- The feature pull request merges into `dev`. +- **Only `overseer` merges `dev` → `main`** (a release). +- Never push directly to `dev` or `main`. +- Authors never review, approve, merge or close their own work. +- Never store credentials in the repository; environment or ignored files only. + +## Where context lives and where work is tracked + +- **Context:** CTXD project `mealquest` — client guide at + . Project documents: `CONTEXT.MD` + (architecture), `DECISIONS.MD` (decision log, e.g. D-021), `RUNBOOKS.MD`, + `PROMPTS.MD`, `GLOSSARY.MD`. +- **Work tracking:** Maestro at , + workspace **Maniacs-Mansion** → project **MealQuest**. Every change has a + task with a verifiable done-condition. + +## Vocabulary (GLOSSARY) + +Use these product terms in code names, UI copy, docs and commit messages. Do +not invent synonyms. + +| Term | Meaning | +| --- | --- | +| **Regulars** | Dinners the household makes routinely. | +| **Side quest** | An AI-suggested new dinner to try. | +| **Quest log** | The to-try pool of accepted Side quests, scheduled later. | +| **Try it / Pass** | Accept (swipe right) / decline (swipe left) a suggestion. | +| **Rotation check** / **"Keep it in rotation?"** | The prompt after a Quest-log dinner is tried, asking whether it becomes a Regular. Promotion is always the household's choice, never automatic. | +| **Supply run** | The current week's shopping list, grouped by dinner. | +| **Supply archive** | The blog-style history of past shopping lists (by week, month, year). | +| **Draft board** | The mobile weekly plan layout the AI drafts for the household to adjust. | +| **Rebuild week** | Ask the AI to redraft the week's plan. | +| **Push to Calendar** | Push the plan one-way to Google Calendar. | +| **Quick dinner** | Cooked in a crock pot **or** 30 minutes or less from prep to table. | +| **Cook from what I have** | Ideas from a list of ingredients on hand. | +| **Kitchen reading mode** | Tablet recipe view: large text, ingredient checkboxes, step-by-step. | +| **"Why this?"** | The reason shown for a suggestion, referencing the household's Regulars. | +| **Our kitchen / Shared household** | The single shared household identity; no per-person profiles. | diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/README.md b/README.md index e69de29..c764516 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,56 @@ +# MealQuest + +MealQuest is a mobile-first Progressive Web App for weekly dinner planning for +one household. The household lists the dinners it makes regularly, an AI +assistant suggests new dinners to try (swipe "Try it" or "Pass"), and the app +builds a weekly plan, pushes it to Google Calendar, and generates a shopping +list grouped by dinner, kept as a browsable weekly history. It ships as a +single Go service with the React PWA embedded in the binary. + +> Status: this repository is the Feature 01 scaffold (Go service with +> `/health`, embedded PWA skeleton, Makefile, Dockerfile, CI). The planned +> application is described in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). + +## Quick start + +Requirements: Go 1.25 and Node.js `^22.22.2 || ^24.15.0 || >=26.0.0` — that +is, 22.22.2 or later on the 22 line, 24.15.0 or later on the 24 line, or 26.0.0 +or later. Node.js 23, 24.0–24.14 and 25 are not supported: this is the +`engines` range of jsdom in the committed lockfile, the strictest of all the +web dependencies. + +```sh +(cd web && npm ci) +mkdir -p ~/.mealquest-dev # outside the repository: it holds a secret +chmod 700 ~/.mealquest-dev +head -c 32 /dev/urandom | base64 > ~/.mealquest-dev/secret_key +chmod 600 ~/.mealquest-dev/secret_key +export MEALQUEST_HTTP_ADDR=:8080 +export MEALQUEST_PUBLIC_URL=http://localhost:5173 +export MEALQUEST_DATA_DIR="$HOME/.mealquest-dev" +export MEALQUEST_SHUTDOWN_TIMEOUT=10s +export MEALQUEST_SECRET_KEY_FILE="$HOME/.mealquest-dev/secret_key" +make dev +``` + +`make dev` runs the Go server on `:8080` and the Vite dev server on `:5173` +(which proxies `/api`, `/auth` and `/media` to `:8080`). Open +http://localhost:5173. Configuration is environment-only and fails fast: +every variable above is required, and secrets are read only from files named +by `*_FILE` variables. + +## Common commands + +| Command | What it does | +| -------------- | --------------------------------------------------------------- | +| `make dev` | Go server plus Vite dev server. | +| `make test` | `gofmt` check, `go vet`, `go test -race`, web typecheck and tests. | +| `make build` | Build the PWA, embed it, build the static binary `bin/mealquest`. | +| `make docker` | Build the container image from the root `Dockerfile`. | + +## Where to read next + +- [AGENTS.md](AGENTS.md) — house rules, approved dependencies, conventions. +- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — architecture of record. +- [design/MealQuest Requirements.md](design/MealQuest%20Requirements.md) — product requirements. +- [design/MealQuest UI Designs/design_handoff_mealquest/README.md](design/MealQuest%20UI%20Designs/design_handoff_mealquest/README.md) — UI handoff. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..4beae8c --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,235 @@ +# MealQuest architecture + +This mirrors the architecture section of `CONTEXT.MD` in the CTXD project +`mealquest`, which is the source of record; if the two disagree, CTXD wins and +this file should be updated. It describes the **planned** application. The +repository today is the Feature 01 scaffold: `cmd/server`, `internal/config`, +`internal/server` (`/health`), `internal/frontend` (embedded PWA with SPA +fallback), the `web/` skeleton, `Makefile`, `Dockerfile` and CI. + +Decision numbers (D-NNN) refer to `DECISIONS.MD` in CTXD. + +## Topology + +``` +PWA (phone / tablet / desktop) + │ HTTPS + ▼ +Traefik v3.6 — 10.60.1.33 + file route: mealquest-ccl.yml + │ + ▼ +docker compose project "mealquest" — 10.60.1.14 + ├── mealquest Go service + embedded PWA, host 8090 → container 8080 + ├── searxng sidecar for recipe search; internal compose network + │ only, no published port + └── Litestream SQLite replication → Synology NAS 10.60.1.50 +``` + +One Go service serves the API, the auth flow, media and the PWA. Its SQLite +database is replicated by Litestream to the NAS. + +## Repository layout + +| Path | Purpose | +| --- | --- | +| `cmd/server` | Entry point; wiring only. | +| `internal/config` | Env-only `MEALQUEST_*` config; secrets from `*_FILE`. | +| `internal/server` | HTTP server, router, middleware. | +| `internal/frontend` | Embedded PWA, SPA fallback. | +| `internal/auth` | Authentik OIDC, sessions, CSRF. | +| `internal/db`, `internal/store` | SQLite connections, sqlc queries. | +| `internal/secretbox` | AES-256-GCM encryption for `_ct` columns. | +| `internal/household`, `internal/dinners`, `internal/recipes` | Domain. | +| `internal/ai/llm`, `internal/ai/harness`, `internal/ai/skills` | AI provider adapters, skill harness, skills. | +| `internal/suggest`, `internal/plan`, `internal/shopping`, `internal/ideas` | Feature services. | +| `internal/gcal`, `internal/notify`, `internal/push`, `internal/chat` | Integrations. | +| `internal/jobs`, `internal/scheduler` | Background jobs and schedules. | +| `migrations/` | SQL migrations (no goose or golang-migrate). | +| `web/` | React PWA. | +| `deploy/` | Deployment files. | +| `docs/` | Documentation. | +| `design/` | Requirements and UI handoff (read-only reference). | + +## Backend stack and conventions + +- Go 1.25, `CGO_ENABLED=0`, a single static binary. +- Router: `go-chi/chi/v5`. Logging: `log/slog` JSON. +- SQLite via `modernc.org/sqlite`, with `journal_mode=WAL`, + `foreign_keys=ON` and `busy_timeout`. **One writer connection plus a read + pool.** Queries generated by `sqlc`. +- IDs are ULIDs (`oklog/ulid/v2`). +- Timestamps are stored in UTC; local calendar dates are `YYYY-MM-DD` strings + in the household timezone. +- Configuration is environment-only (`MEALQUEST_*`). Secrets come only from + files named by `*_FILE` variables. Missing or invalid config stops startup. +- Error responses always have the body + `{"error":{"code":"…","message":"…","retryable":false}}`. + +## Authentication + +- Authentik OIDC; the backend is a **confidential client** + (`coreos/go-oidc/v3`, `golang.org/x/oauth2`). +- Session cookie `__Host-mealquest_session` (HttpOnly, Secure, + SameSite=Lax). Sessions expire after 30 days idle or 180 days absolute. +- Non-GET requests require the CSRF header `X-MealQuest-CSRF`. +- Access requires membership of the Authentik group `mealquest-admin`. + There is a **single role**: one shared household, no per-person profiles. + +## Data model + +Columns ending in `_ct` hold AES-256-GCM ciphertext (via `secretbox`). + +| Table | Holds | +| --- | --- | +| `household_settings` | One row: timezone, dinner_time (17:30), reminder_time (16:30), review_time (09:00), plan_draft day/time (Fri 09:00), suggestions day/time (Sun 08:00), chat_autosend day/time (Sat 08:00), onboarding_completed_at, updates_seen_at. | +| `recipes` | A real recipe page: source_url (unique), site_name, title, total/prep/cook minutes, yield, raw_jsonld, fetched_at. | +| `recipe_ingredients` | recipe_id, position, raw_text, name, qty, unit, note. | +| `recipe_steps` | recipe_id, position, text. | +| `photos` | source_url, local_path, width, height, sha256, fetched_at. | +| `dinners` | The meal record and its lifecycle: name, normalized_name, status (`suggested` / `quest_log` / `tried` / `regular` / `archived`), origin, method, minutes_total, difficulty, cuisine, is_favorite, recipe_id, photo_id, grounding_status, updated_by. | +| `suggestion_batches` | trigger, status, ai_run_id. | +| `suggestions` | dinner_id, batch_id, reason, cited_regular_ids (JSON), status, decided_at. | +| `week_plans` | week_start (unique), status (`draft` / `synced` / `changed`), ai_note, ai_run_id, calendar_synced_at. | +| `plan_days` | plan_id, date (unique per plan), dinner_id, is_new, calendar_event_id. | +| `rotation_reviews` | dinner_id + plan_date (unique), status, decision (`keep` / `drop` / `not_made`), decided_at, updated_by. | +| `shopping_lists` | week_start (unique), plan_id, title, status (`current` / `archived`), sent_to_chat_at, frozen_at. | +| `shopping_groups` / `shopping_items` | One group per dinner/day; item name, qty, needed (bool), position. | +| `ai_settings` | One row: provider, base_url, model, temperature, system_prompt, api_key_ct, api_key_last4, key_verified_at. | +| `recipe_search_settings` | One row: provider (`searxng` / `brave`), base_url, api_key_ct. | +| `integrations_google` | One row: refresh_token_ct, calendar_id, calendar_name, connected_at, last_sync_error. | +| `chat_settings` | One row: webhook_url_ct, space_label, last_test_at, last_test_status. | +| `notification_prefs` | event (`plan_ready` / `list_ready` / `day_of_reminder` / `new_suggestions` / `rotation_review`), push, chat. | +| `push_subscriptions` | endpoint (unique), p256dh, auth, user_agent, failure_count, last_success_at. | +| `notification_outbox` | event_key + channel (unique), payload, status, attempts, next_attempt_at, last_error. | +| `jobs` | kind, status, payload, result (progressive), error, timestamps. | +| `schedule_runs` | schedule, period_key (unique). | +| `ai_runs` | skill, skill_version, provider, model, temperature, status, latency_ms, tokens_in/out, error, redacted request/response (30-day retention). | +| `sessions` | id_hash, subject, email, name, groups, csrf_token, created/last_seen/expires. | + +`recipes` holds a fetched recipe page; `dinners` holds the household's meal +record and its lifecycle, optionally linked to a recipe and a photo. + +## Dinner lifecycle + +``` +suggested (deck) ──Try it──▶ quest_log ──planned date passes──▶ tried + │ ▲ │ + │ └──────── explicit decision ─────┤ + │ ├──▶ regular + │ └──▶ archived + └──Pass──▶ archived (name kept for dedupe) +``` + +From `tried`, an explicit decision moves the dinner to `regular`, to +`archived`, or back to `quest_log`. Promotion to `regular` is **never +automatic** (D-021). + +## API surface + +- JSON API under `/api/v1`, using the error body above for every failure. + It requires the session cookie and, for non-GET requests, the + `X-MealQuest-CSRF` header. +- `/auth/*` — the OIDC flow. +- `/media/*` — cached photos. +- `/health` — liveness. +- Everything else serves the embedded PWA with SPA fallback. + +## AI layer + +- **Provider interface** (`ai/llm`) with adapters for OpenAI-compatible, + Anthropic and Ollama endpoints, called over plain HTTP — no vendor SDKs. +- **Skill harness** (`ai/harness`): prompt layering, validation of the + structured output, and **one repair retry**; if the repair also fails the + run fails — no silent fallback. Every call is recorded in `ai_runs`. + +## Recipe grounding + +- Dinners are grounded in real recipe pages found through recipe search + (`searxng` or `brave`, per `recipe_search_settings`). +- Fetching is **SSRF-safe**. +- Recipe data is parsed from schema.org `Recipe` JSON-LD. +- Photos are cached locally (`photos`) and processed with + `golang.org/x/image`. + +## Scheduler + +Times are in the household timezone. The times of `day_of_reminder` +(`reminder_time`) and `rotation_check` (`review_time`), and the day and time +of `suggestions_weekly`, `plan_draft` and `shopping_chat_autosend`, are +household settings in `household_settings` (`dinner_time` is also stored +there); the table shows their defaults. `week_rollover` and the `ai_runs` +purge are fixed and not configurable. + +| Schedule | When | Does | +| --- | --- | --- | +| `suggestions_weekly` | Sunday 08:00 | New batch of Side quests. | +| `plan_draft` | Friday 09:00 | AI drafts next week's plan. | +| `shopping_chat_autosend` | Saturday 08:00 | Sends the Supply run to Google Chat. | +| `day_of_reminder` | Daily 16:30 | Tonight's dinner reminder. | +| `rotation_check` | Daily 09:00 | Asks "Keep it in rotation?" for tried dinners. | +| `week_rollover` | Monday 00:05 (fixed) | Freezes last week's Supply run into the Supply archive. | +| `ai_runs` purge | Daily (fixed) | Deletes `ai_runs` rows past 30-day retention. | + +Runs are recorded in `schedule_runs` and executed through `jobs`. + +## Notifications + +- Events: `plan_ready`, `list_ready`, `day_of_reminder`, `new_suggestions`, + `rotation_review`; each can go to Web Push and/or Google Chat per + `notification_prefs`. +- Delivery goes through `notification_outbox`. +- Web Push uses VAPID (`webpush-go`). Google Chat uses the stored incoming + webhook. + +## Google Calendar + +- One-way push: the app is the source of truth; edits in Google Calendar do + not come back. +- Connected once with Google OAuth in Settings; one event per planned + dinner. +- **Deterministic event ids**; each plan day records its + `calendar_event_id`. + +## Frontend + +- React 19, TypeScript, Vite, React Router 7, TanStack Query 5, + `vite-plugin-pwa`, `lucide-react` (stroke 2.75). +- CSS Modules over `tokens.css`; no Tailwind, no Framer Motion, no + hard-coded hex. Self-hosted Bagel Fat One and Figtree. +- Tests: Vitest, Testing Library, jsdom. +- Offline cache for the current plan, shopping list and recipes. + +| Route | Screen | +| --- | --- | +| `/` | Home | +| `/onboarding` | Add regular dinners | +| `/plan` | Weekly plan (Draft board) | +| `/plan/shopping` | Shopping list (Supply run) | +| `/plan/shopping/archive` | Supply archive | +| `/discover` | Swipe suggestions (Side quests) | +| `/quick` | Quick dinners | +| `/cook-from-pantry` | Cook from what I have | +| `/dinner/:id` | Recipe / dinner detail (Kitchen reading mode on tablet) | +| `/settings` | Settings home | +| `/settings/household` | Household settings | +| `/settings/regulars` | Regular dinners | +| `/settings/calendar` | Google Calendar connection | +| `/settings/notifications` | Notifications and Google Chat | +| `/settings/ai` | Admin · AI assistant | + +`/settings/household`, `/settings/regulars` and `/settings/calendar` come from +CTXD; the other routes are defined in the UI handoff. + +## Boundaries and gotchas + +- **Secrets never reach the client** — API keys, OAuth tokens and the Chat + webhook stay server-side, encrypted at rest. +- **The app is the source of truth for the plan**; Google Calendar is a + one-way mirror. +- **Never auto-promote to Regular** (D-021). +- **Quick** means cooked in a crock pot **or** ≤ 30 minutes prep to table. +- **Weeks run Monday–Sunday** in the household timezone. +- **No silent fallbacks** — fail fast and report. +- **iOS Web Push requires the PWA to be installed to the Home Screen**; show + an install hint.