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