openvibers/openvibe.food AGENTS.md

AGENTS.md

main 11.8 KB · 158 lines · markdown

AGENTS.md — OpenVibe.Food

Project Overview

OpenVibe.Food answers "what should we eat?" for a household. It finds food banks, soup kitchens, community fridges,
free pantry boxes and budget grocery stores near a place (OpenStreetMap), plans a shop within a budget, carries a food
database with nutrition and typical prices, keeps a pantry with a deterministic "what can I make", and adds barcode,
photo/receipt and AI recipe features. It also follows FDA food recalls from OpenVibe.Sources, compares delivery apps
by opening their own searches, keeps a shared meal plan with a household Chat room, and runs a reviewed recipe library.

  • Domain: openvibe.food · Port: 4970 · Service id: food · Env prefix: FOOD
  • Runtime: Node 22, Express 4, PostgreSQL 18 through openvibe-sdk/db (PGlite in development/tests), Valkey 9 for
    shared limits.
  • Read first: [README.md](README.md), [STATUS.json](STATUS.json), [.env.example](.env.example).

Network context: identity/accounts are OpenVibe.Network (OAuth client food, PKCE S256). Place data is
OpenStreetMap (Nominatim/Overpass, ODbL, attributed); barcode products are Open Food Facts (ODbL, only the barcode is
sent, answers cached a day); FDA recalls, BLS average prices, USDA ERS prices and Open Food Facts product rows are
read from OpenVibe.Sources. AI recipe ideas and pantry-photo recognition call OpenVibe.AI
(food.suggest_recipes, food.read_pantry_photo). Household chat creates a room through OpenVibe.Chat and reads it
through food.household.read. Checked recipes are OpenVibe.Search documents. Event delivery is OpenVibe.Events. Food
stores no accounts, no money and no third-party data beyond the cached public rows.

Commands

npm ci                         # install (contracts/sdk/shared/publishing pinned by tag)
npm run dev                    # http://localhost:4970, PGlite under data/pglite
npm start                      # node server/index.js
npm test                       # every test/*.test.js on temp PGlite, mock Network/AI/Sources, no internet
npm run test:pg                # the same through PostgreSQL + PgBouncer
npm test -- pantry             # only test files whose name contains the word
node --check <file.js>         # syntax check (no linter configured)

No build step: pages are server-rendered; assets [public/css/](public/css/) and [public/js/](public/js/)
(barcode-scan.js, pantry-photo.js). Every page works without JavaScript; the barcode/photo pages are the only
camera-assisted paths.

Architecture at a Glance

  • Entry: [server/index.js](server/index.js) — listener, telemetry/retention, the Events subscriptions, the recipe
    library, Search index, recall/price/Open-Food-Facts followers and the "food near" warm-up; graceful stop. FOOD_DRILL
    keeps boot-time work off.
  • App factory: [server/app.js](server/app.js) — CSP, SSO, /auth, /internal/events, /internal/households/:id/chat-context,
    static//shared, the share API, /api/v1, agent layer, pages, 404/error handler.
  • Config: [server/config.js](server/config.js) — load(env) is pure; production /etc/openvibe/food.env.
  • Product logic: [server/food/](server/food/) — foods.js, planner.js, pantry-match.js, recipes.js,
    library.js, recalls.js, official-prices.js, usda-prices.js, off.js, barcode.js, places.js,
    upstream.js (the only OSM client), delivery.js, household-chat.js, box-links.js, search-index.js,
    sharing.js, store.js, near-warm.js.
  • HTTP: [server/http/](server/http/) — api.js, pages.js, library-routes.js, recall-pages.js,
    box-pages.js, principal.js, caller-limits.js, router.js, discovery.js.
  • Auth/account data: [server/auth/sso.js](server/auth/sso.js), [server/auth/keys.js](server/auth/keys.js),
    [server/identity/account-data.js](server/identity/account-data.js).
  • Rendering: [server/render/](server/render/) (layout.js, html.js, markdown.js, short.js), agent twins
    [server/agent-pages.js](server/agent-pages.js), [server/agent-descriptor.js](server/agent-descriptor.js).
  • Database: [server/db.js](server/db.js) and [migrations/](migrations/): food_geo_cache, food_plans,
    food_pantry, account_data_events, food_recipe_suggestions, telemetry_samples, food_library_recipes,
    food_library_runs, food_recalls, food_ingest_cursor, food_off_products, food_official_prices,
    food_usda_prices, sharing tables and the household/plan share rows.
  • Deploy: [deploy/systemd/openvibe-food.service](deploy/systemd/openvibe-food.service) (127.0.0.1:4970,
    /etc/openvibe/food.env, /opt/openvibe.food, database ov_food) and
    [deploy/nginx/openvibe.food.conf](deploy/nginx/openvibe.food.conf) (TLS, www→apex, auth/API/forms zones, 4 MB on the
    photo route, /internal and /metrics closed). sudo ovhost deploy food; rollback on a failed /api/ready.

Ownership and Boundaries

Food owns its plans, pantry, recipe suggestions/library, recall rows and cursor, cached OSM/price/Open-Food-Facts data,
share rows and event receipts. It is not the authority for identity (Network), place/store data (OSM), products
(Open Food Facts), recall/price source records (Sources), AI output (AI), the wallet (Network), Chat rooms (Chat), the
search index (Search) or event delivery (Events). The food list, prices and nutrition are carried from Tools' grocery
source, not authored here. Food never calls the FDA, never fetches a delivery app, and never returns raw rows.

Auth and Capabilities

  • A person is the session cookie or a Network user token; a cookie-authenticated write must come from
    openvibe.food itself. Per-caller limits are in [server/http/caller-limits.js](server/http/caller-limits.js)
    (FOOD_LIMITS_MINUTE/HOUR); the place search has a tighter budget, recipe suggestions 10/person/day.
  • Capabilities (Contracts): food.plan.read|write, food.pantry.read|write, food.recipes.suggest (proposed), and
    the internal food.household.read granted to Chat. Public reads (/places, /foods) need none. A service token
    needs the route's capability; a person acting for themself needs none.
  • Food calls no capability on another service for its public reads. Outbound: OSM, and the named OpenVibe
    services (Network, Events, AI, Sources, Chat, Search, Coupons/Reviews for a food box's codes/score).
  • Errors are RFC 9457 problem+json with stable food.* codes. With OV_AI_INTERNAL_URL unset, recipe ideas answer
    503 food.ai_off; a photo outside the AI/pantry limits is refused before upload.

Pages and API

Pages: /, /near, /plan, /plans/p/:id, /foods, /foods/:slug, /pantry, /pantry/barcode, /pantry/photo,
/recipes, /recipes/library, /recalls, /recalls/:number, /delivery, /updates, plus discovery (/llms.txt,
sitemap, robots, Markdown twins) and /openapi.json.
API /api/v1: GET /ping, GET /places, GET /foods[/:slug], POST /plans, GET /plans[/:id], GET /pantry,
PUT /pantry, POST /recipes/suggest, GET /recipes; the one Share sheet's API
(/api/v1/plans/:id/share, requests/transfer, /share/people, /shared-with-me). /internal/events is loopback;
/internal/households/:id/chat-context is loopback + food.household.read; /api/health, /api/ready,
/release.json, /metrics are machine endpoints.

Conventions

  • CommonJS, 4-space indent, single quotes, semicolons; no linter/formatter. Async handlers through
    [server/http/router.js](server/http/router.js); never log request bodies or a person's photo/pantry content.
  • New schema = a new migrations/NNNN_name.sql; never edit one that has run. Keep food_ prefixes (plus the shared
    account_data_events/telemetry_samples/share tables).
  • The only outbound clients are [server/food/upstream.js](server/food/upstream.js) (Nominatim + Overpass — paced
    once a second per host, identified with FOOD_OSM_USER_AGENT, cached FOOD_GEO_CACHE_TTL_MS, bounded, stale
    fallback, ODbL attribution on every answer) and the named OpenVibe service clients. Never fetch a user URL (a
    delivery link, a listing, an image).
  • AI output is validated against the food list and priced with Food's own typical prices; it is labelled
    AI-suggested. A photo sends only the picture to AI; AI keeps the photo's hash, and a receipt's prices/card/store
    details are never read out.
  • Barcode: only the barcode is sent to Open Food Facts with Food's User-Agent; the picture never leaves the device;
    answers cached a day and capped. A person confirms the match before anything is added.
  • Recalls are shown exactly as Sources holds the report, with openFDA's note (unvalidated, not for medical decisions,
    no FDA endorsement); /recalls/:number is noindex because the source's registry entry asks.
  • Delivery pages open each app's own search in a new tab — Food fetches none of them and shows no invented prices or
    fees. The meal planner is deterministic and a budget is a hard cap.
  • CSP: add origins to helmet in [server/app.js](server/app.js). Phone first: every page fits 390 px; no Font
    Awesome (shared glyphs).
  • "Calm reduces, only Off removes": a household/share preference reduces visibility; only an explicit delete removes.
  • Empty states use the shared kit; plans/about pages must show real content first (no placeholders).

Key Pitfalls

  • OSM policy: one request a second per host, a contact User-Agent, cached answers, an around-query bound, and
    attribution. A test points the client at a stand-in; a real call in a test is a bug
    ([test/places.test.js](test/places.test.js), [test/helpers/osm.js](test/helpers/osm.js)).
  • A household plan's Chat room is created as the owner through Chat's room API, with members added by subject and
    the AI member linked to the plan; Chat reads the plan through food.household.read (loopback, service token). Never
    read a Chat room directly or post as the AI yourself.
  • Recipe library: one theme every six hours; every ingredient must be a food slug or the recipe is dropped; only
    staff-checked recipes are indexable; /recipes/library.json?since= (oldest first, ≤100) is Blog's feed.
  • Search documents are written through openvibe-publishing/search-relay; a hide/unpublish writes a tombstone.
    The library's AI text is labelled and noindex until checked.
  • Photo route allows 4 MB at nginx and resizes to 1280 px on the device first; ten photos a person a day. Do not
    raise either without the owner's decision.
  • Account export/deletion runs through /internal/events only, is signature-verified and idempotent; the export
    carries the person's plans/pantry/suggestions and the deletion erases them (shared plans stay for the others).
  • Drill mode (FOOD_DRILL) keeps the OSM warm-up, followers, library, search sync and timers off.
  • A share is revoked by the owner immediately; canView is checked on every read path, not only the share route.

Testing

npm test runs each file in its own process on temp PGlite with a mock Network and fixture OSM/AI/Sources: places and
attribution, planner arithmetic and budget cap, food pages/sitemap, pantry and deterministic matches, recipes/AI
validation/limits, barcode, Open Food Facts, official/USDA prices, recalls, recipe library and Search, household chat,
sharing, boxes, delivery links, account data, SSO/JWKS, limits, secret-leak/session/redirect, layout, cache, nginx
zones and perf budgets. npm run test:pg for PostgreSQL. Run the narrowest file first, then the suite; always
node --check changed files.

Documentation

  • [README.md](README.md) — purpose, what it does, owns/does-not-own, dependencies, capabilities, API, data sources and
    their terms, configuration, development, acceptance, deploy, account data, security.
  • [STATUS.json](STATUS.json) — stage, deployed flag, features and not-yet list.
  • [.env.example](.env.example) — variable names only.
  • [CHANGELOG.md](CHANGELOG.md), [SECURITY.md](SECURITY.md).