openvibers/openvibe.reviews AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Reviews |
| 2 | |
| 3 | ## Project Overview |
| 4 | |
| 5 | OpenVibe.Reviews gathers review signals from sources with provenance, resolves them to entities, and publishes honest |
| 6 | aggregates and editor-written summaries. It also carries members' own reviews, the ratings other sites show (saved by |
| 7 | members), and official records (inspections, complaints, violations). "Every review site, one search" is the reader |
| 8 | promise; nothing is invented and no review text from a source is stored. |
| 9 | |
| 10 | - **Domain:** `openvibe.reviews` · **Port:** 4830 · **Service id:** `reviews` |
| 11 | - **Runtime:** Node 22, Express 4, PostgreSQL 18 through `openvibe-sdk/db` (PGlite in development/tests), Valkey 9 |
| 12 | optional for shared per-actor limits. |
| 13 | - **Read first:** [README.md](README.md), [STATUS.json](STATUS.json), [docs/threat-review.md](docs/threat-review.md), |
| 14 | [.env.example](.env.example). |
| 15 | |
| 16 | **Network context:** identity, sessions and roles are OpenVibe.Network (`reviews` OAuth client, PKCE; service |
| 17 | principal `svc:reviews`). Source items and terms are OpenVibe.Sources (`sources.item.read`, `sources.source.read`) — |
| 18 | Reviews never fetches a site. Discussion threads are OpenVibe.Community (`community.comment.write`). AI summaries are |
| 19 | proposals from OpenVibe.AI (`reviews.summary.propose`); Reviews only accepts them as drafts. Search consumes the index |
| 20 | events. Event delivery is OpenVibe.Events. |
| 21 | |
| 22 | ## Commands |
| 23 | |
| 24 | ```bash |
| 25 | npm ci # install (contracts/sdk/shared/publishing pinned by tag) |
| 26 | npm run dev # http://localhost:4830, PGlite under data/ |
| 27 | npm start # node server/index.js |
| 28 | npm test # every test/*.test.js, temp PGlite, mock Network/Sources/AI |
| 29 | npm run test:pg # the same through PostgreSQL + PgBouncer |
| 30 | npm run subscribe # create the sources.item.* subscription (first install) |
| 31 | npm test -- aggregate # only test files whose name contains the word |
| 32 | node --check <file.js> # syntax check (no linter configured) |
| 33 | ``` |
| 34 | |
| 35 | No build step: pages are server-rendered; CSS only under [public/css/](public/css/) (`reviews`, `everywhere`, |
| 36 | `outcomes`). Every page and form works without JavaScript. |
| 37 | |
| 38 | ## Architecture at a Glance |
| 39 | |
| 40 | - **Entry:** [server/index.js](server/index.js) — `start()` builds the app and (when asked) the Sources pull, import |
| 41 | queue and Events outbox relay; tests call `start({ workers: false })`. Graceful stop. `REVIEWS_DRILL` keeps |
| 42 | boot-time work off. |
| 43 | - **App factory:** [server/app.js](server/app.js) — CSP, session, `/auth`, `/internal/events`, `/shared`+static, |
| 44 | pages, `/api/v1`, discovery, 404/error handler. |
| 45 | - **Config:** [server/config.js](server/config.js) — `load(env)` is pure; production `/etc/openvibe/reviews.env`. |
| 46 | - **Domain:** [server/reviews/](server/reviews/) — `service.js`, `source-items.js` (Sources ingest/resolution), |
| 47 | `aggregate.js` (the honest aggregate), `extract.js` (signals and values), `home.js`, `everywhere.js` (the |
| 48 | every-review-site search), `access.js`, `account-data.js`. |
| 49 | - **HTTP:** [server/http/api.js](server/http/api.js), [server/http/pages.js](server/http/pages.js), |
| 50 | [server/http/events.js](server/http/events.js), [server/http/machine.js](server/http/machine.js) (robots, |
| 51 | sitemaps, feeds, llms), [server/http/actor-limits.js](server/http/actor-limits.js), [server/http/common.js](server/http/common.js). |
| 52 | - **Auth/capabilities:** [server/auth/session.js](server/auth/session.js), [server/auth/viewer.js](server/auth/viewer.js), |
| 53 | [server/auth/capabilities.js](server/auth/capabilities.js) (the Contracts grant rule), [server/auth/keys.js](server/auth/keys.js). |
| 54 | - **Clients/integrations:** [server/integrations/platform.js](server/integrations/platform.js), Sources through |
| 55 | `openvibe-publishing/ingest`, Community and Events through the SDK. |
| 56 | - **Rendering:** [server/render/](server/render/) (`layout.js`, `views.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/). Authority tables: `review_entities`, |
| 59 | `review_entity_aliases`, `review_sources`, `review_source_items`, `review_signals`, `review_summaries`, |
| 60 | `review_summary_revisions`, `review_trust_metadata`, `review_entity_links`. Helpers: `review_aggregates`, |
| 61 | `review_member_reviews`, `review_seen_ratings`, `review_records`, `review_summary_citations`, `review_corrections`, |
| 62 | `review_audit`, `review_import_queue`, `review_sync_state`, `review_summary_reviews`, `review_summary_drafts`, |
| 63 | `review_entity_redirects`, `review_discussion_refs`, `review_index_revisions`, `review_event_outbox`, |
| 64 | `review_event_inbox`, `account_data_events`, `telemetry_samples`. |
| 65 | - **Deploy:** [deploy/systemd/openvibe-reviews.service](deploy/systemd/openvibe-reviews.service) (127.0.0.1:4830, |
| 66 | `/etc/openvibe/reviews.env`, `/opt/openvibe.reviews`) and [deploy/nginx/openvibe.reviews.conf](deploy/nginx/openvibe.reviews.conf). |
| 67 | `sudo ovhost deploy reviews` (git-checkout, database `ov_reviews`, migrations at boot add tables/columns only); |
| 68 | rollback on a failed `/api/ready`, then `sudo ovhost rollback reviews --to <sha>`. |
| 69 | |
| 70 | ## Ownership and Boundaries |
| 71 | |
| 72 | Reviews owns entities/aliases, the Sources items it read (signal fields only — **never review text**), typed signals |
| 73 | with provenance, summaries and immutable revisions, trust decisions with history, typed links/merges, aggregates, |
| 74 | member reviews, seen ratings, official records, corrections and the append-only editorial audit. It does **not** own |
| 75 | discussion (Community), source adapter policy or ingestion (Sources), AI generation (AI), the search index (Search), |
| 76 | accounts/roles (Network) or event delivery (Events). A merge is a `merged_into` link with who/when/why and can be |
| 77 | reversed by a split; never delete an entity to hide a merge. |
| 78 | |
| 79 | ## Auth and Capabilities |
| 80 | |
| 81 | One capability per route, audience `openvibe.reviews`; a service acting for a person sends `X-OV-Subject`. Editorial |
| 82 | writes need a person who is staff or in `REVIEWS_EDITORS`. A cookie write started by another site is 403 |
| 83 | `request.cross_site` (Bearer tokens are unaffected). Errors are RFC 9457 problem+json. |
| 84 | |
| 85 | | Capability | Covers | |
| 86 | |---|---| |
| 87 | | `reviews.entity.resolve` | `POST /resolve`, reads, history/aggregate/summary revision, items and item resolution | |
| 88 | | `reviews.entity.manage` | entity CRUD, aliases, links, trust, corrections queue/resolve | |
| 89 | | `reviews.entity.merge` / `.split` | merge into another entity, split a merge | |
| 90 | | `reviews.signal.import` | signal import, source sync | |
| 91 | | `reviews.summary.propose` | AI proposal (the only AI write) | |
| 92 | | `reviews.summary.publish` | create/review/publish/unpublish a summary revision | |
| 93 | | `reviews.correction.submit` | a person's correction | |
| 94 | |
| 95 | Outbound: `sources.item.read`, `sources.source.read`, `events.event.publish` + `events.subscription.manage`, |
| 96 | `community.comment.write`. Per-actor limits ([server/http/actor-limits.js](server/http/actor-limits.js)): reads |
| 97 | 120/min–3000/h, entity writes 30/300, merge/split 10/100, item resolution and import 60/1200, sync now 2/20, summary |
| 98 | writes 30/300, proposals 60/1200, correction submit 5/20, correction resolve 30/300, discussion comment 20/300. A |
| 99 | form and its API route share one budget; forms count only a signed-in person. Never limit health/readiness/release/ |
| 100 | metrics, sign-in, public reads (per-address only) or signed `/internal/events`. |
| 101 | |
| 102 | ## API and Pages |
| 103 | |
| 104 | Pages: `/`, `/about`, `/search`, `/e/:slug` (+ `.json`, `/history`, `/summary/:n`), `GET|POST /e/:slug/correct`, |
| 105 | `POST /e/:slug/discuss`, the editor desk (`/editor/...`, `/e/:slug/edit`), and discovery |
| 106 | (`/robots.txt`, `/sitemap.xml`, `/sitemaps/*.xml`, `/feed.atom`, `/feed.json`, `/llms.txt`). |
| 107 | API `/api/v1`: `POST /resolve`, `GET /entities`, `/entities/:ref`, `/history`, `/aggregate`, |
| 108 | `/summary/revisions/:n`, `GET /items`, `/items/:id`, `POST /items/:id/resolution`, entity/alias/link/trust writes, |
| 109 | `POST /signals/import`, `POST /sources/sync`, summary proposals/revisions/review/publish/unpublish, correction |
| 110 | submit/resolve, merge/split, `GET /corrections`. `/internal/events` is loopback-only and HMAC-signed; machine |
| 111 | endpoints are loopback for `/metrics`. |
| 112 | |
| 113 | ## Conventions |
| 114 | |
| 115 | - CommonJS, 4-space indent, single quotes, semicolons; no linter/formatter. |
| 116 | - **Provenance is mandatory** on every signal/aggregate: source item, revision, observed/retrieved date, the source's |
| 117 | terms. The same item revision's signal values are immutable (database triggers); a changed value is a new signal and |
| 118 | the old one is `superseded`/`withdrawn` with a reason. |
| 119 | - The aggregate never silently drops a signal: a trust exclusion needs a visible reason, limitation and verification |
| 120 | notes (`review_trust_metadata`; history kept). Official records are labelled as records, separate from ratings. |
| 121 | - New schema = a new `migrations/NNNN_name.sql`; never edit one that has run. Keep `review_` prefixes. |
| 122 | - Source text is never copied: store signal fields and quotes only where the licence allows. A source removal drops |
| 123 | its signals and recalculates, never leaves a stale number. |
| 124 | - Summaries: editor-written or AI proposal, immutable revisions, authorship, citations to the signals; an AI draft |
| 125 | needs a person's review before publish. A correction is published at once with `correction_note`/`correction_id`. |
| 126 | - CSP: add origins to `helmet` in [server/app.js](server/app.js); Community comments are rendered from its API, never |
| 127 | copied into Reviews' HTML storage. No Font Awesome; shared glyphs. |
| 128 | - Phone first: entity, search, editor and home pages fit 390 px; forms work without JS. |
| 129 | - Empty states use the shared kit. "Calm reduces, only Off removes": excluding a source from an aggregate is visible |
| 130 | and reversible; only a takedown/delete removes data. |
| 131 | - Hostile text stays text in every page, JSON, JSON-LD, feed, sitemap and Search document. |
| 132 | |
| 133 | ## Key Pitfalls |
| 134 | |
| 135 | - **Never fetch a source or a user-supplied URL.** Ingest only through Sources; an alias/URL is stored and rendered, |
| 136 | never requested. [docs/threat-review.md](docs/threat-review.md) is the threat model. |
| 137 | - **Strong identifiers are unique** (GTIN/SKU/MPN, source binding, external id); a conflicting resolution is a review |
| 138 | action, not a silent overwrite. Merges/splits are audited and reversible; old slugs redirect. |
| 139 | - **Signals are immutable:** a trigger rejects an UPDATE of the value fields. A re-import of the same revision is a |
| 140 | no-op; a new revision supersedes the old with `replaces`. |
| 141 | - **Corrections:** anyone signed in may submit; only staff/editor resolves. Accepting with a correction also needs |
| 142 | `reviews.summary.publish`, and the resulting revision carries the correction note. |
| 143 | - **Per-actor budgets** must be checked after the actor is resolved and the capability passed, before any work; a form |
| 144 | budget is not a second allowance. |
| 145 | - **AI summaries** are drafts only; the stub provider's output is never public, and no AI call happens without the |
| 146 | optional AI URL. |
| 147 | - **Drill mode** keeps the Sources pull, import queue, outbox relay and timers off. |
| 148 | - **`review_event_outbox`** needs its rejected partial index (migration 0009); keep it. |
| 149 | - Account deletion removes a person's member reviews/seen ratings and receipts through `/internal/events`; a |
| 150 | redelivery changes nothing. |
| 151 | |
| 152 | ## Testing |
| 153 | |
| 154 | `npm test` runs each file in its own process on temp PGlite with mock Network/Sources/AI: resolution/aliases, |
| 155 | signals/aggregates, summaries/proposals/publish, corrections, merges/splits, member reviews/seen ratings, official |
| 156 | records, home/sitemap/link-header/IndexNow, no-JS/pages, capabilities/delegation, actor limits, account data, CSP, |
| 157 | Ink/contrast, threat, service kit and perf budgets. `npm run test:pg` for PostgreSQL. Run the narrowest file first, |
| 158 | then the suite; always `node --check` changed files. |
| 159 | |
| 160 | ## Documentation |
| 161 | |
| 162 | - [README.md](README.md) — purpose, every-review-site search, member reviews, outside ratings, official records, |
| 163 | owns/does-not-own, what works, routes, API, limits, events, dependencies, capabilities, acceptance, security, deploy. |
| 164 | - [docs/threat-review.md](docs/threat-review.md) — the pre-launch threat review. |
| 165 | - [docs/capabilities-proposal/](docs/capabilities-proposal/) and [docs/service-manifest-proposal.json](docs/service-manifest-proposal.json). |
| 166 | - [STATUS.json](STATUS.json), [.env.example](.env.example), [CHANGELOG.md](CHANGELOG.md), [SECURITY.md](SECURITY.md). |
| 167 |