openvibers/openvibe.quest AGENTS.md

AGENTS.md

main 11.1 KB · 155 lines · markdown

1# AGENTS.md — OpenVibe.Quest
2
3## Project Overview
4
5OpenVibe.Quest is one quest log across every OpenVibe site: it counts something a person really did (went live,
6started a thread, said hello in a public chat, edited a wiki page, played a game) and pays a badge. When OpenCoins are
7on it credits loyalty points through OpenVibe.Network's wallet (never money, never Vibes), and it can grant one of
8Live's items through OpenVibe.Inventory. The home page leads with one shared daily quest; `/me` and `/me/daily` show a
9private checklist, streak and level.
10
11- **Domain:** `openvibe.quest` · **Port:** 4980 · **Service id:** `quest` · **Env prefix:** `QUEST`
12- **Runtime:** Node 22, Express 4, PostgreSQL 18 through `openvibe-sdk/db` (PGlite in development/tests), Valkey for
13 shared limits when configured.
14- **Read first:** [README.md](README.md), [STATUS.json](STATUS.json), [.env.example](.env.example).
15
16**Network context:** identity/sign-in and the wallet are OpenVibe.Network (OAuth client `quest`, PKCE S256; staff
17roles). Events and their delivery are OpenVibe.Events. Item instances are OpenVibe.Inventory (Live names Quest a
18grantor of its items, ADR-054 §3). Platform experience/levels are `openvibe-sdk/levels` calling Network. Daily quest
19glyphs may request a draft from OpenVibe.Design (`design.glyph.draft`; with a bundled fallback always visible). Quest
20stores no identity, no event, no item row and no balance.
21
22## Commands
23
24```bash
25npm ci # install (contracts/sdk/shared pinned by tag)
26npm run dev # http://localhost:4980, PGlite under data/
27npm start # node server/index.js
28npm test # every test/*.test.js on temp PGlite, mock Network, no network
29npm run test:pg # the same through PostgreSQL + PgBouncer
30npm test -- daily # only test files whose name contains the word
31node --check <file.js> # syntax check (no linter configured)
32```
33
34No build step: pages are server-rendered; CSS/JS under [public/css/](public/css/) (`app.css`, `quest-visual.css`) and
35[public/js/quest-visual.js](public/js/quest-visual.js).
36
37## Architecture at a Glance
38
39- **Entry:** [server/index.js](server/index.js) — listener, telemetry, Events subscriptions, the OpenCoins/item/level
40 retry timers and graceful stop. `QUEST_DRILL` keeps boot-time work off.
41- **App factory:** [server/app.js](server/app.js) — CSP, SSO, `/auth`, `/internal/events`, `/shared`+static, `/api/v1`,
42 discovery/agent twins, pages, 404/error handler.
43- **Config:** [server/config.js](server/config.js) — `load(env)` is pure; production `/etc/openvibe/quest.env`.
44- **Catalog:** [server/quests/catalog.js](server/quests/catalog.js) — 36 first-party quests and every event Quest
45 listens to; a quest naming an undocumented event fails at load. Never add an event without the catalog entry.
46- **Progress and rewards:** [server/quests/apply.js](server/quests/apply.js) (which event field names the person),
47 [server/quests/store.js](server/quests/store.js), [server/quests/daily.js](server/quests/daily.js),
48 [server/quests/coins.js](server/quests/coins.js), [server/quests/items.js](server/quests/items.js),
49 [server/quests/levels.js](server/quests/levels.js), [server/quests/app-quests.js](server/quests/app-quests.js),
50 [server/quests/glyph-drafts.js](server/quests/glyph-drafts.js).
51- **Events:** [server/events-consumer.js](server/events-consumer.js) — raw body, v2 signature required, forwarded
52 headers refused, SDK inbox for once-only application, 503 without `QUEST_EVENTS_SECRET`.
53- **HTTP:** [server/http/api.js](server/http/api.js), [server/http/pages.js](server/http/pages.js),
54 [server/http/principal.js](server/http/principal.js), [server/http/discovery.js](server/http/discovery.js),
55 [server/http/router.js](server/http/router.js), [server/http/caller-limits.js](server/http/caller-limits.js).
56- **Auth/account data:** [server/auth/sso.js](server/auth/sso.js), [server/auth/keys.js](server/auth/keys.js),
57 [server/identity/account-data.js](server/identity/account-data.js), [server/identity/test-subjects.js](server/identity/test-subjects.js).
58- **Rendering:** [server/render/](server/render/) — `layout.js`, `quests.js`, `quest-icons.js`, `badge-art.js`,
59 `html.js`, `short.js`, `markdown.js`; agent twins [server/agent-pages.js](server/agent-pages.js),
60 [server/agent-descriptor.js](server/agent-descriptor.js). Search documents: [server/search-index.js](server/search-index.js).
61- **Database:** [server/db.js](server/db.js) and [migrations/](migrations/): `quest_progress`, `quest_completions`,
62 `quest_badges`, `quest_event_inbox`, `account_data_events`, `quest_index_revisions`, `app_quests`,
63 `quest_subject_merges`, `daily_quests`, `daily_step_types`, `daily_events`, `daily_personal_quests`,
64 `daily_completions`, `daily_preferences`, `channel_daily_quests`, `daily_ai_choices`, a `test_subject` fixture
65 table and `telemetry_samples`.
66- **Deploy:** [deploy/systemd/openvibe-quest.service](deploy/systemd/openvibe-quest.service) (127.0.0.1:4980,
67 `/etc/openvibe/quest.env`, `/opt/openvibe.quest`) and [deploy/nginx/openvibe.quest.conf](deploy/nginx/openvibe.quest.conf)
68 (`/internal/` is 404 from outside). `sudo ovhost deploy quest`; database `ov_quest`.
69
70## Ownership and Boundaries
71
72Quest is the authority for a person's progress, completions, badges, daily quests/choices/preferences and app quests,
73and for the `quest.*` namespace. It does **not** own identity, the wallet/OpenCoins ledger, Events, item definitions
74or instances, or the sites' content (threads, posts, comments, messages, revisions, streams). Quest counts that an
75event happened and stores none of the content itself. A `gst_` guest, a service/app/mod principal, or an event with
76no person field is ignored — nothing is written.
77
78## Auth and Capabilities
79
80- A person is the Network token or this site's session (cookie writes must be same-origin). The only capabilities
81 Quest itself uses are `quest.app_quest.manage` (a developer app defines its own quests),
82 `quest.steps.contribute` (an app proposes a step type) and `quest.steps.review` (staff). `CAPABILITIES` in
83 [server/http/principal.js](server/http/principal.js) is exactly that list — add to it and Contracts together.
84- Outbound with Quest's own client-credentials token: `identity.subject.resolve`, `network.coins.credit`,
85 `events.subscription.manage`, `inventory.item.grant`, and `network.levels.award` for platform experience.
86 `design.glyph.draft` only when the Design internal URL is set; `QUEST_DESIGN_DRAFTS=off` disables requests.
87- OpenCoins: `QUEST_COINS=on` credits with `idempotency_key = <completion id>`; per-person and global daily caps put
88 over-cap completions in `coins_state=skipped` with a reason. Off records `skipped` and the pages say rewards start
89 soon.
90- Item rewards: six quests grant one of Live's items as `earned`, keyed by the completion id; `QUEST_ITEMS=off` turns
91 it off. A refusal is recorded and retried by a timer, never silently dropped.
92- Developer app quests cannot mint platform experience from arbitrary app events; their experience state is
93 `skipped`.
94- Errors are RFC 9457 problem+json with stable `quest.*` codes; limits return 429 with `Retry-After` before the route
95 does work.
96
97## API (`/api/v1`)
98
99Public: `GET /ping`, `/quests`, `/quests/:id`, `/daily`, `/daily/:date`, `/step-types`,
100`/profiles/:subject/badges` (id, name, date only), `/profiles/:subject/streak`, `/channels/:subject/quest`.
101Signed in: `GET /me/quests`, `GET /me/daily`, `GET /quests/today`, `GET /quests/today/steps/:id`,
102`POST /quests/today/steps/:id/done`, `PUT /me/daily/ai-text`, `PUT /me/daily/time-zone`, `PUT /me/channel-quest`.
103App: `GET|PUT|DELETE /app-quests[/:slug]`, `POST /step-types`; staff `POST /step-types/:id/review`.
104Machine endpoints `/api/health`, `/api/ready`, `/release.json`, `/metrics` are loopback only.
105
106## Conventions
107
108- CommonJS, 4-space indent, single quotes, semicolons; no linter/formatter. Async handlers through the router.
109- **The person is not the envelope actor.** Take it from the field the event's schema defines (`channel.subject`,
110 `follower`, `author`, `user_subject`, `player.subject`, `owner`), and only use the actor where the schema says the
111 actor is that person.
112- Two deliveries of one event change nothing; a replay of the whole request changes nothing (SDK inbox + unique
113 completion).
114- New schema = a new `migrations/NNNN_name.sql`; never edit one that has run. Keep `quest_`/`daily_` prefixes.
115- Daily quests: one per UTC date, progress uses the person's chosen IANA time zone; a due-based job picks three
116 verified step types across at least two sites, asks AI for recent network changes, and falls back to a hand-written
117 set when AI is unavailable.
118- Glyphs: the bundled glyph is always the fallback; only a Design-approved SVG may replace it, and pending drafts
119 never leave Design.
120- CSP: add origins to `helmet` in [server/app.js](server/app.js); do not widen `frameAncestors`/`objectSrc`.
121- Phone first: home, a quest, `/me`, `/me/daily` fit 390 px; `<progress>` bars and forms work without JS.
122- Empty states use the shared kit; no Font Awesome (shared glyphs only); "Calm reduces, only Off removes".
123
124## Key Pitfalls
125
126- **Event signatures:** forwarded requests and a missing `QUEST_EVENTS_SECRET` are refusals (503), not a bypass. A
127 bad signature writes nothing. Never log an event's content.
128- **Catalog load:** a quest that counts an undocumented event fails at load — add the event to the catalog and the
129 subscription list, then Contracts if needed.
130- **Badges are public and minimal:** `GET /profiles/:subject/badges` returns id, name and date and nothing else.
131- **OpenCoins are loyalty points, not money or Vibes.** Never read or move money; never credit without the idempotency
132 key, and never exceed either daily cap.
133- **Levels:** only catalog/daily completions may award platform experience; a developer app quest sets
134 `experience_state=skipped`. A 409 `levels.duplicate` is success, 404 `levels.person_not_found` is a skip.
135- **Drill mode** (`QUEST_DRILL`) keeps subscriptions and every retry timer off; add new timers there.
136- **Account export/deletion** runs through `/internal/events` only, is idempotent, and carries only the person's rows;
137 a deletion erases them whole.
138- **Migration 0009** adds the `event_outbox` rejected partial index; keep it.
139
140## Testing
141
142`npm test` runs each file in its own process on temp PGlite with a mock Network: events consumer (signature, replay,
143per-event person field), quests API/pages, daily and streaks, levels, items, coins off/on and caps, app quests, step
144types, account data, discovery, SSO/JWKS, caller limits, secret-leak/session/redirect, service kit, layout and perf
145budgets. `npm run test:pg` for PostgreSQL. Run the narrowest file first, then the suite; always `node --check`
146changed files.
147
148## Documentation
149
150- [README.md](README.md) — what it is, owns/does-not-own, capabilities/grants, what works, API, how progress happens,
151 data terms, configuration, acceptance, deploy, account data, security.
152- [docs/screenshots/](docs/screenshots/) — recorded UI states.
153- [STATUS.json](STATUS.json) — stage, deployed flag, features and not-yet list.
154- [.env.example](.env.example), [CHANGELOG.md](CHANGELOG.md), [SECURITY.md](SECURITY.md).
155