openvibers/openvibe.food AGENTS.md

AGENTS.md

main 11.8 KB · 158 lines · markdown

1# AGENTS.md — OpenVibe.Food
2
3## Project Overview
4
5OpenVibe.Food answers "what should we eat?" for a household. It finds food banks, soup kitchens, community fridges,
6free pantry boxes and budget grocery stores near a place (OpenStreetMap), plans a shop within a budget, carries a food
7database with nutrition and typical prices, keeps a pantry with a deterministic "what can I make", and adds barcode,
8photo/receipt and AI recipe features. It also follows FDA food recalls from OpenVibe.Sources, compares delivery apps
9by 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
17OpenStreetMap (Nominatim/Overpass, ODbL, attributed); barcode products are Open Food Facts (ODbL, only the barcode is
18sent, answers cached a day); FDA recalls, BLS average prices, USDA ERS prices and Open Food Facts product rows are
19read 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
21through `food.household.read`. Checked recipes are OpenVibe.Search documents. Event delivery is OpenVibe.Events. Food
22stores no accounts, no money and no third-party data beyond the cached public rows.
23
24## Commands
25
26```bash
27npm ci # install (contracts/sdk/shared/publishing pinned by tag)
28npm run dev # http://localhost:4970, PGlite under data/pglite
29npm start # node server/index.js
30npm test # every test/*.test.js on temp PGlite, mock Network/AI/Sources, no internet
31npm run test:pg # the same through PostgreSQL + PgBouncer
32npm test -- pantry # only test files whose name contains the word
33node --check <file.js> # syntax check (no linter configured)
34```
35
36No 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
38camera-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
69Food owns its plans, pantry, recipe suggestions/library, recall rows and cursor, cached OSM/price/Open-Food-Facts data,
70share 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
72search index (Search) or event delivery (Events). The food list, prices and nutrition are carried from Tools' grocery
73source, 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
90Pages: `/`, `/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`,
92sitemap, robots, Markdown twins) and `/openapi.json`.
93API `/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
145attribution, planner arithmetic and budget cap, food pages/sitemap, pantry and deterministic matches, recipes/AI
146validation/limits, barcode, Open Food Facts, official/USDA prices, recalls, recipe library and Search, household chat,
147sharing, boxes, delivery links, account data, SSO/JWKS, limits, secret-leak/session/redirect, layout, cache, nginx
148zones 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