openvibers/openvibe.deals AGENTS.md

AGENTS.md

main 9.8 KB · 164 lines · markdown

1# AGENTS.md — OpenVibe.Deals
2
3## Project overview
4
5Deals submitted and voted on by the community, with source, price and freshness always shown:
6normalised deal submissions and imports, product/store records, offers with timestamped observations,
7server-authoritative votes and hotness, duplicate resolution, watches and saved searches. Discussion
8is OpenVibe.Community's; imports come from OpenVibe.Sources; notifications are events for Network.
9Node.js/Express, PostgreSQL (`ov_deals`), server-rendered, CommonJS, **no build step**.
10
11- Service id `deals`, port `4840`, origin `https://openvibe.deals`, unit `openvibe-deals.service`,
12 release root `/opt/openvibe.deals`.
13- **The one rule: a price is an observation.** It has a source and an `observed_at`, it is shown
14 "as of" that time, and an offer is never presented as current without a fresh observation. A price
15 nobody stated stays "not stated" — in the page, JSON, structured data, comparison, feeds and Search.
16
17## What it owns
18
19The nine charter tables in [migrations/](migrations/):
20
21| Table | What |
22|---|---|
23| `deal_products` | product records (name, brand, category), `/p/:slug` |
24| `deal_product_aliases` | `gtin` (14 digits), `mpn`, `sku`, exact `name`, `url` → one product; an alias never silently moves |
25| `deal_offers` | title, link (tracking stripped except per-source policy), store, product, status `active\|expired\|disabled`, **stated** expiry, review state, `merged_into` |
26| `deal_offer_sources` | the submission, a person's observations, or a Sources item reference `{sources, item, itm_…, revision}` — the item is never copied |
27| `deal_votes` | one per (offer, subject): `+1`/`-1`/`0` (kept as history), weight and IP hash |
28| `deal_hotness_snapshots` | every hotness computation with all inputs (`hot@1`) |
29| `deal_watches` | keyword / product / price-below watches and saved searches |
30| `deal_price_observations` | the only place a price lives: price/currency, shipping, condition, availability, `observed_at`, source |
31| `deal_flags` | reports by people and system flags (vote rings) |
32
33Companion tables include `deal_stores`, `watch_notifications` (unique watch+observation),
34`moderation_log`, `rate_events`, `import_state`, `subject_projections`, `deal_campaigns`,
35`event_outbox`/`idempotency_receipts`, and publishing's `deal_offer_discussion_refs`/
36`deal_index_revisions`. [docs/](docs/) records the deviations from the charter list.
37
38Not owned: comment content (Community), the source items (Sources), notification delivery and
39membership (Network/VIP), payments (Billing), file bytes (Media).
40
41## Who calls it, and whom it calls
42
43- Callers: people in the browser; the browser extension/Embed for offers; services with a Network
44 service token audience `openvibe.deals` and the route's capability; OpenVibe.Sources' pull loop and
45 signed event consumer.
46- Depends on: **Network** (SSO client `deals`, JWKS, `identity.subject.resolve`, future notification
47 consumer); **Sources** (`sources.item.read`; the seeded `dealnews-daily` is disabled until a person
48 re-verifies its terms; its terms are encoded in `server/domain/source-policy.js`); **Community**
49 (`community.comment.write`, optionally moderate); **Events** (outbox, and `events.subscription.manage`
50 once for `scripts/subscribe.js`); **Search** (consumes `deals.index_document.*`); PostgreSQL and
51 Valkey; `openvibe-publishing` (seo/JSON-LD/feeds/sitemaps/index hooks, ingest, SSR).
52
53## Repository map
54
55- **Entry:** [server/index.js](server/index.js) — app, worker (Sources pull, hotness, expiry),
56 outbox; graceful stop.
57- **App:** [server/app.js](server/app.js) — helmet/CSP, `/auth`, `/shared`, `/internal/config`,
58 `/internal/events`, `/api/v1`, pages, discovery, health/ready.
59- **Config/db:** [server/config.js](server/config.js), [server/db.js](server/db.js),
60 [server/settings.js](server/settings.js).
61- **Domain:** [server/domain/](server/domain/) — `reads.js`, `catalog.js`, `publication.js`,
62 `indexing.js`, `hotness.js`, `watches.js`, `limits.js`, `access.js`, `offers.js`, `flags.js`,
63 `votes.js`, `listings.js`, `source-items.js`, `source-policy.js`, `account-data.js`.
64- **Clients:** [server/clients/network.js](server/clients/network.js), `community.js`, and publishing's
65 `openvibe-publishing/ingest`.
66- **HTTP:** [server/http/api.js](server/http/api.js), `pages.js`, `discovery.js`, `events.js`,
67 `actor-limits.js`; views [server/render/views.js](server/render/views.js).
68- **Auth:** [server/auth/viewer.js](server/auth/viewer.js), [server/sharing.js](server/sharing.js).
69- **Worker:** [server/worker.js](server/worker.js).
70- **Deploy:** [deploy/systemd/openvibe-deals.service](deploy/systemd/openvibe-deals.service),
71 [deploy/nginx/openvibe.deals.conf](deploy/nginx/openvibe.deals.conf).
72
73## Commands
74
75```bash
76npm ci
77npm run dev # NODE_ENV=development node server/index.js
78npm start # node server/index.js, http://127.0.0.1:4840
79npm test # test/run.js, PGlite, mock Network/Sources/Community
80npm run test:pg # same on PostgreSQL (DEALS_TEST_STORE=pg)
81npm test -- freshness # filter by file name; --strict makes a skip fail
82ov test # the harness's sharded run of what the change reaches
83```
84
85## Database and migrations
86
87`ov_deals` on the host's data role (ADR-035); PGlite in development/tests. Async through
88`openvibe-sdk/db`; migrations `migrations/NNNN_*.sql` applied at boot; never edit an applied one, add a
89new number. The price observation is the only price storage; do not add a price column to
90`deal_offers`. Votes keep history (`0` means removed); merges keep `merged_into` and never delete the
91loser. Outbox rows are written in the same transaction as the change.
92
93## Auth, votes and moderation
94
95- People sign in through Network SSO (OAuth client `deals`); a person's subject is the identity, never a
96 body/query integer. Service callers need a service token audience `openvibe.deals` and the route's
97 capability; `X-OV-Subject` scopes a first-party service call.
98- **Votes are server-authoritative.** The server computes the weight and hotness; a client-supplied
99 score is ignored. Vote rings raise a system flag for moderators.
100- Watches and saved searches are per person; notifications are events (`deals.watch.matched`) — no
101 consumer exists yet, so nothing is "sent" by Deals.
102- Moderation actions are audited; a flagged vote ring can be removed without erasing history.
103
104## Frontend loading model
105
106Every page is server-rendered and **useful without JavaScript**; there is no SPA, bundler or
107transpiler. `openvibe-shared` supplies the Frame, legal/SEO and `/shared/*` from the pinned
108node_modules; `openvibe-publishing` supplies JSON-LD, feeds, sitemaps and SSR. Discovery is generated
109from the same data. CSP and cache policy are in [server/app.js](server/app.js) and
110`openvibe-shared/cache-policy`.
111
112## Deploy
113
114`sudo ovhost deploy deals` (git-checkout: fetch, fast-forward `/opt/openvibe.deals`, install on a
115lockfile change, restart, wait for `/api/ready`). Unit `openvibe-deals.service` on
116`127.0.0.1:4840`, env `/etc/openvibe/deals.env`; the release migrates `ov_deals` at boot; vhost
117[deploy/nginx/openvibe.deals.conf](deploy/nginx/openvibe.deals.conf). Rollback
118`sudo ovhost rollback deals --to <sha>`; migrations only add tables and columns.
119
120## Tests
121
122`npm test` runs each `test/*.test.js` in its own process with PGlite and in-process Network/Sources/
123Community mocks. The README's acceptance table maps each guarantee to its test: price observations and
124freshness/as-of rendering, unknown stays unknown, votes/hotness/abuse, duplicate merge, watches and
125saved searches, Sources import and per-source link policy, deletion/export, privacy and discovery.
126`ov test` runs the same.
127
128## Conventions
129
130- CommonJS, 4-space indent, single quotes, semicolons; domain in `server/domain/`, HTTP in
131 `server/http/`, no SQL in routes.
132- Money is integer minor units with an ISO 4217 currency; a price is never a float and never a bare
133 number without its currency and `observed_at`.
134- IDs/errors/capabilities from `openvibe-contracts`; errors are problem+json with stable codes.
135- Time is UTC and rendered "as of" the observation; a page may say "not stated", never invent a price.
136
137## Pitfalls
138
139- **Never present a price as current.** Render the observation with `observed_at` and the source, or
140 say it is not stated. A cached/stale page must keep its as-of date (and Search/JSON must agree).
141- **Do not copy a Sources item into Deals.** Store the typed reference `{sources, item, itm_…,
142 revision}` and read through the source; a copy drifts and breaks attribution.
143- **An alias never silently moves** to another product; a wrong mapping needs a correction and an audit
144 trail, not an update in place.
145- **Track-only links are stripped**, except where a source policy keeps a link verbatim; the policy
146 lives in `server/domain/source-policy.js`, not in the renderer.
147- **A merge keeps both records**: the loser gets `merged_into`, its votes/observations stay for history,
148 and the API redirects/canonicalises; do not hard-delete.
149- **Only one notification per (watch, observation)** — the unique row is the guard; do not "notify
150 again" on a retry.
151- **Votes are not client-computed** and `0` is a removal kept as history; a vote ring raises a flag.
152- **No delivery pretence:** `deals.watch.matched` is emitted but no consumer exists yet; the UI must not
153 say a notification was sent.
154- **The charter list is nine tables; the deviations are documented** (`deal_stores`, etc.) — adding a
155 tenth charter table needs the doc updated, not just the migration.
156
157## Documentation
158
159- [README.md](README.md) — purpose, tables, routes, API, events, prices/freshness, duplicates, votes,
160 watches, imports, discovery, acceptance, security and deploy.
161- [docs/](docs/) — the charter deviations and source policy notes.
162- OpenVibe.Contracts holds `deals.*` shapes/capabilities; Sources, Community and publishing own their
163 contracts.
164