openvibers/openvibe.food AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Food |
| 2 | |
| 3 | ## Project Overview |
| 4 | |
| 5 | OpenVibe.Food answers "what should we eat?" for a household. It finds food banks, soup kitchens, community fridges, |
| 6 | free pantry boxes and budget grocery stores near a place (OpenStreetMap), plans a shop within a budget, carries a food |
| 7 | database with nutrition and typical prices, keeps a pantry with a deterministic "what can I make", and adds barcode, |
| 8 | photo/receipt and AI recipe features. It also follows FDA food recalls from OpenVibe.Sources, compares delivery apps |
| 9 | by opening their own searches, keeps a shared meal plan with a household Chat room, and runs a reviewed recipe library. |
| 10 | |
| 11 | - **Domain:** `openvibe.food` · **Port:** 4970 · **Service id:** `food` · **Env prefix:** `FOOD` |
| 12 | - **Runtime:** Node 22, Express 4, PostgreSQL 18 through `openvibe-sdk/db` (PGlite in development/tests), Valkey 9 for |
| 13 | shared limits. |
| 14 | - **Read first:** [README.md](README.md), [STATUS.json](STATUS.json), [.env.example](.env.example). |
| 15 | |
| 16 | **Network context:** identity/accounts are OpenVibe.Network (OAuth client `food`, PKCE S256). Place data is |
| 17 | OpenStreetMap (Nominatim/Overpass, ODbL, attributed); barcode products are Open Food Facts (ODbL, only the barcode is |
| 18 | sent, answers cached a day); FDA recalls, BLS average prices, USDA ERS prices and Open Food Facts product rows are |
| 19 | read from OpenVibe.Sources. AI recipe ideas and pantry-photo recognition call OpenVibe.AI |
| 20 | (`food.suggest_recipes`, `food.read_pantry_photo`). Household chat creates a room through OpenVibe.Chat and reads it |
| 21 | through `food.household.read`. Checked recipes are OpenVibe.Search documents. Event delivery is OpenVibe.Events. Food |
| 22 | stores no accounts, no money and no third-party data beyond the cached public rows. |
| 23 | |
| 24 | ## Commands |
| 25 | |
| 26 | ```bash |
| 27 | npm ci # install (contracts/sdk/shared/publishing pinned by tag) |
| 28 | npm run dev # http://localhost:4970, PGlite under data/pglite |
| 29 | npm start # node server/index.js |
| 30 | npm test # every test/*.test.js on temp PGlite, mock Network/AI/Sources, no internet |
| 31 | npm run test:pg # the same through PostgreSQL + PgBouncer |
| 32 | npm test -- pantry # only test files whose name contains the word |
| 33 | node --check <file.js> # syntax check (no linter configured) |
| 34 | ``` |
| 35 | |
| 36 | No build step: pages are server-rendered; assets [public/css/](public/css/) and [public/js/](public/js/) |
| 37 | (`barcode-scan.js`, `pantry-photo.js`). Every page works without JavaScript; the barcode/photo pages are the only |
| 38 | camera-assisted paths. |
| 39 | |
| 40 | ## Architecture at a Glance |
| 41 | |
| 42 | - **Entry:** [server/index.js](server/index.js) — listener, telemetry/retention, the Events subscriptions, the recipe |
| 43 | library, Search index, recall/price/Open-Food-Facts followers and the "food near" warm-up; graceful stop. `FOOD_DRILL` |
| 44 | keeps boot-time work off. |
| 45 | - **App factory:** [server/app.js](server/app.js) — CSP, SSO, `/auth`, `/internal/events`, `/internal/households/:id/chat-context`, |
| 46 | static/`/shared`, the share API, `/api/v1`, agent layer, pages, 404/error handler. |
| 47 | - **Config:** [server/config.js](server/config.js) — `load(env)` is pure; production `/etc/openvibe/food.env`. |
| 48 | - **Product logic:** [server/food/](server/food/) — `foods.js`, `planner.js`, `pantry-match.js`, `recipes.js`, |
| 49 | `library.js`, `recalls.js`, `official-prices.js`, `usda-prices.js`, `off.js`, `barcode.js`, `places.js`, |
| 50 | `upstream.js` (the only OSM client), `delivery.js`, `household-chat.js`, `box-links.js`, `search-index.js`, |
| 51 | `sharing.js`, `store.js`, `near-warm.js`. |
| 52 | - **HTTP:** [server/http/](server/http/) — `api.js`, `pages.js`, `library-routes.js`, `recall-pages.js`, |
| 53 | `box-pages.js`, `principal.js`, `caller-limits.js`, `router.js`, `discovery.js`. |
| 54 | - **Auth/account data:** [server/auth/sso.js](server/auth/sso.js), [server/auth/keys.js](server/auth/keys.js), |
| 55 | [server/identity/account-data.js](server/identity/account-data.js). |
| 56 | - **Rendering:** [server/render/](server/render/) (`layout.js`, `html.js`, `markdown.js`, `short.js`), agent twins |
| 57 | [server/agent-pages.js](server/agent-pages.js), [server/agent-descriptor.js](server/agent-descriptor.js). |
| 58 | - **Database:** [server/db.js](server/db.js) and [migrations/](migrations/): `food_geo_cache`, `food_plans`, |
| 59 | `food_pantry`, `account_data_events`, `food_recipe_suggestions`, `telemetry_samples`, `food_library_recipes`, |
| 60 | `food_library_runs`, `food_recalls`, `food_ingest_cursor`, `food_off_products`, `food_official_prices`, |
| 61 | `food_usda_prices`, sharing tables and the household/plan share rows. |
| 62 | - **Deploy:** [deploy/systemd/openvibe-food.service](deploy/systemd/openvibe-food.service) (127.0.0.1:4970, |
| 63 | `/etc/openvibe/food.env`, `/opt/openvibe.food`, database `ov_food`) and |
| 64 | [deploy/nginx/openvibe.food.conf](deploy/nginx/openvibe.food.conf) (TLS, www→apex, auth/API/forms zones, 4 MB on the |
| 65 | photo route, `/internal` and `/metrics` closed). `sudo ovhost deploy food`; rollback on a failed `/api/ready`. |
| 66 | |
| 67 | ## Ownership and Boundaries |
| 68 | |
| 69 | Food owns its plans, pantry, recipe suggestions/library, recall rows and cursor, cached OSM/price/Open-Food-Facts data, |
| 70 | share rows and event receipts. It is **not** the authority for identity (Network), place/store data (OSM), products |
| 71 | (Open Food Facts), recall/price source records (Sources), AI output (AI), the wallet (Network), Chat rooms (Chat), the |
| 72 | search index (Search) or event delivery (Events). The food list, prices and nutrition are carried from Tools' grocery |
| 73 | source, not authored here. Food never calls the FDA, never fetches a delivery app, and never returns raw rows. |
| 74 | |
| 75 | ## Auth and Capabilities |
| 76 | |
| 77 | - A person is the session cookie or a Network user token; a cookie-authenticated write must come from |
| 78 | `openvibe.food` itself. Per-caller limits are in [server/http/caller-limits.js](server/http/caller-limits.js) |
| 79 | (`FOOD_LIMITS_MINUTE`/`HOUR`); the place search has a tighter budget, recipe suggestions 10/person/day. |
| 80 | - Capabilities (Contracts): `food.plan.read|write`, `food.pantry.read|write`, `food.recipes.suggest` (proposed), and |
| 81 | the internal `food.household.read` granted to Chat. Public reads (`/places`, `/foods`) need none. A service token |
| 82 | needs the route's capability; a person acting for themself needs none. |
| 83 | - Food calls **no** capability on another service for its public reads. Outbound: OSM, and the named OpenVibe |
| 84 | services (Network, Events, AI, Sources, Chat, Search, Coupons/Reviews for a food box's codes/score). |
| 85 | - Errors are RFC 9457 problem+json with stable `food.*` codes. With `OV_AI_INTERNAL_URL` unset, recipe ideas answer |
| 86 | 503 `food.ai_off`; a photo outside the AI/pantry limits is refused before upload. |
| 87 | |
| 88 | ## Pages and API |
| 89 | |
| 90 | Pages: `/`, `/near`, `/plan`, `/plans/p/:id`, `/foods`, `/foods/:slug`, `/pantry`, `/pantry/barcode`, `/pantry/photo`, |
| 91 | `/recipes`, `/recipes/library`, `/recalls`, `/recalls/:number`, `/delivery`, `/updates`, plus discovery (`/llms.txt`, |
| 92 | sitemap, robots, Markdown twins) and `/openapi.json`. |
| 93 | API `/api/v1`: `GET /ping`, `GET /places`, `GET /foods[/:slug]`, `POST /plans`, `GET /plans[/:id]`, `GET /pantry`, |
| 94 | `PUT /pantry`, `POST /recipes/suggest`, `GET /recipes`; the one Share sheet's API |
| 95 | (`/api/v1/plans/:id/share`, requests/transfer, `/share/people`, `/shared-with-me`). `/internal/events` is loopback; |
| 96 | `/internal/households/:id/chat-context` is loopback + `food.household.read`; `/api/health`, `/api/ready`, |
| 97 | `/release.json`, `/metrics` are machine endpoints. |
| 98 | |
| 99 | ## Conventions |
| 100 | |
| 101 | - CommonJS, 4-space indent, single quotes, semicolons; no linter/formatter. Async handlers through |
| 102 | [server/http/router.js](server/http/router.js); never log request bodies or a person's photo/pantry content. |
| 103 | - New schema = a new `migrations/NNNN_name.sql`; never edit one that has run. Keep `food_` prefixes (plus the shared |
| 104 | `account_data_events`/`telemetry_samples`/share tables). |
| 105 | - **The only outbound clients** are [server/food/upstream.js](server/food/upstream.js) (Nominatim + Overpass — paced |
| 106 | once a second per host, identified with `FOOD_OSM_USER_AGENT`, cached `FOOD_GEO_CACHE_TTL_MS`, bounded, stale |
| 107 | fallback, ODbL attribution on every answer) and the named OpenVibe service clients. Never fetch a user URL (a |
| 108 | delivery link, a listing, an image). |
| 109 | - AI output is validated against the food list and priced with Food's own typical prices; it is labelled |
| 110 | AI-suggested. A photo sends only the picture to AI; AI keeps the photo's hash, and a receipt's prices/card/store |
| 111 | details are never read out. |
| 112 | - Barcode: only the barcode is sent to Open Food Facts with Food's User-Agent; the picture never leaves the device; |
| 113 | answers cached a day and capped. A person confirms the match before anything is added. |
| 114 | - Recalls are shown exactly as Sources holds the report, with openFDA's note (unvalidated, not for medical decisions, |
| 115 | no FDA endorsement); `/recalls/:number` is noindex because the source's registry entry asks. |
| 116 | - Delivery pages open each app's own search in a new tab — Food fetches none of them and shows no invented prices or |
| 117 | fees. The meal planner is deterministic and a budget is a hard cap. |
| 118 | - CSP: add origins to `helmet` in [server/app.js](server/app.js). Phone first: every page fits 390 px; no Font |
| 119 | Awesome (shared glyphs). |
| 120 | - "Calm reduces, only Off removes": a household/share preference reduces visibility; only an explicit delete removes. |
| 121 | - Empty states use the shared kit; plans/about pages must show real content first (no placeholders). |
| 122 | |
| 123 | ## Key Pitfalls |
| 124 | |
| 125 | - **OSM policy:** one request a second per host, a contact User-Agent, cached answers, an around-query bound, and |
| 126 | attribution. A test points the client at a stand-in; a real call in a test is a bug |
| 127 | ([test/places.test.js](test/places.test.js), [test/helpers/osm.js](test/helpers/osm.js)). |
| 128 | - **A household plan's Chat room** is created as the owner through Chat's room API, with members added by subject and |
| 129 | the AI member linked to the plan; Chat reads the plan through `food.household.read` (loopback, service token). Never |
| 130 | read a Chat room directly or post as the AI yourself. |
| 131 | - **Recipe library:** one theme every six hours; every ingredient must be a food slug or the recipe is dropped; only |
| 132 | staff-checked recipes are indexable; `/recipes/library.json?since=` (oldest first, ≤100) is Blog's feed. |
| 133 | - **Search documents** are written through `openvibe-publishing/search-relay`; a hide/unpublish writes a tombstone. |
| 134 | The library's AI text is labelled and noindex until checked. |
| 135 | - **Photo route** allows 4 MB at nginx and resizes to 1280 px on the device first; ten photos a person a day. Do not |
| 136 | raise either without the owner's decision. |
| 137 | - **Account export/deletion** runs through `/internal/events` only, is signature-verified and idempotent; the export |
| 138 | carries the person's plans/pantry/suggestions and the deletion erases them (shared plans stay for the others). |
| 139 | - **Drill mode** (`FOOD_DRILL`) keeps the OSM warm-up, followers, library, search sync and timers off. |
| 140 | - A **share** is revoked by the owner immediately; `canView` is checked on every read path, not only the share route. |
| 141 | |
| 142 | ## Testing |
| 143 | |
| 144 | `npm test` runs each file in its own process on temp PGlite with a mock Network and fixture OSM/AI/Sources: places and |
| 145 | attribution, planner arithmetic and budget cap, food pages/sitemap, pantry and deterministic matches, recipes/AI |
| 146 | validation/limits, barcode, Open Food Facts, official/USDA prices, recalls, recipe library and Search, household chat, |
| 147 | sharing, boxes, delivery links, account data, SSO/JWKS, limits, secret-leak/session/redirect, layout, cache, nginx |
| 148 | zones and perf budgets. `npm run test:pg` for PostgreSQL. Run the narrowest file first, then the suite; always |
| 149 | `node --check` changed files. |
| 150 | |
| 151 | ## Documentation |
| 152 | |
| 153 | - [README.md](README.md) — purpose, what it does, owns/does-not-own, dependencies, capabilities, API, data sources and |
| 154 | their terms, configuration, development, acceptance, deploy, account data, security. |
| 155 | - [STATUS.json](STATUS.json) — stage, deployed flag, features and not-yet list. |
| 156 | - [.env.example](.env.example) — variable names only. |
| 157 | - [CHANGELOG.md](CHANGELOG.md), [SECURITY.md](SECURITY.md). |
| 158 |