@bongos/core 1.19.1025 → 1.19.1027
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +88 -33
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +40 -0
- package/clients/bongos-client/index.cjs +40 -0
- package/clients/bongos-client/index.d.ts +63 -0
- package/clients/bongos-client/index.mjs +40 -0
- package/docs/adr/0213-platform-terms-acceptance-is-the-hubs-own-witness.md +4 -2
- package/docs/api/openapi.json +1361 -151
- package/docs/api-reference.md +32 -4
- package/docs/copy-inventory.md +6 -6
- package/docs/copy-registry.json +10 -20
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/platform-identity/community-leaderboard.js +235 -0
- package/modules/platform-identity/connections.js +1 -0
- package/modules/platform-identity/guilds.js +705 -0
- package/modules/platform-identity/migrations/platform_identity_025_guilds.sql +100 -0
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/read-rate-limit.js +62 -0
- package/modules/platform-identity/routes/community-leaderboard.js +69 -0
- package/modules/platform-identity/routes/community-search.js +4 -31
- package/modules/platform-identity/routes/guilds-public.js +57 -0
- package/modules/platform-identity/routes/guilds.js +393 -0
- package/modules/platform-identity/routes/projects.js +1 -1
- package/modules/platform-identity/terms-acceptance.js +14 -5
- package/modules/public-landing/public/terms.html +3 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/run-unit-tests.js +10 -0
- package/src/module-api.js +1 -1
- package/tests/community_leaderboard_db.mjs +202 -0
- package/tests/community_leaderboard_route.mjs +414 -0
- package/tests/goal_map_page.mjs +8 -0
- package/tests/guilds_api.mjs +521 -0
- package/tests/guilds_db.mjs +343 -0
- package/tests/own_scope_guard.mjs +5 -0
- package/tests/terms_acceptance.mjs +74 -28
- package/tests/terms_grandfather_db.mjs +2 -2
package/docs/api-reference.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Bongos API reference
|
|
4
4
|
|
|
5
|
-
> **Generated from the live route files** — the route file is authoritative.
|
|
5
|
+
> **Generated from the live route files** — the route file is authoritative. 437 endpoints across 74 route files.
|
|
6
6
|
> Machine-readable spec: [`docs/api/openapi.json`](api/openapi.json) (OpenAPI 3.1). Rendered docs site: **`/docs`** (e.g. `cloudbongos.com/docs`).
|
|
7
7
|
|
|
8
8
|
Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust-boundary-server-enforced-permissions.md)): `public` < `any-builder` < `metic+archon` < `archon`.
|
|
@@ -188,10 +188,11 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
188
188
|
| GET | `/api/bongos/closeness/weights` | `any-builder` | — | the MODEL is not private, and publishing it is the whole point of the weights being data. |
|
|
189
189
|
| GET | `/api/bongos/closeness/with/:builderId` | `any-builder` | — | One pair — "why am I close to THEM", asked directly rather than by paging the graph. |
|
|
190
190
|
|
|
191
|
-
## `community` (
|
|
191
|
+
## `community` (2)
|
|
192
192
|
|
|
193
193
|
| Method | Path | Rank | Body | Description |
|
|
194
194
|
|---|---|---|---|---|
|
|
195
|
+
| GET | `/api/bongos/community/leaderboard` | `public` | — | the cross-project leaderboard (?window=7d\|30d\|365d\|all, default all; ?discipline=; ?limit=&offset=). |
|
|
195
196
|
| GET | `/api/bongos/community/search` | `public` | — | a name/discipline search under EXACTLY the public-profile visibility rules (D4): a hide_stats account is findable by name but drops out o… |
|
|
196
197
|
|
|
197
198
|
## `connections` (8)
|
|
@@ -331,6 +332,31 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
331
332
|
|---|---|---|---|---|
|
|
332
333
|
| GET | `/api/bongos/grades/by-builder` | `metic+archon` | — | GET /grades/by-builder?days=30 — per-builder grade breakdown (task 726). |
|
|
333
334
|
|
|
335
|
+
## `guild-requests` (3)
|
|
336
|
+
|
|
337
|
+
| Method | Path | Rank | Body | Description |
|
|
338
|
+
|---|---|---|---|---|
|
|
339
|
+
| DELETE | `/api/bongos/guild-requests/:id` | `any-builder` | — | cancel a row I opened: my request, or (as owner) an invite my guild sent. |
|
|
340
|
+
| POST | `/api/bongos/guild-requests/:id/accept` | `any-builder` | `visibility`, `counted_in_totals` | accept an invite addressed to ME, or (as the guild's owner) a request to join it. |
|
|
341
|
+
| POST | `/api/bongos/guild-requests/:id/decline` | `any-builder` | — | decline an invite to ME, or (as owner) a request to my guild. |
|
|
342
|
+
|
|
343
|
+
## `guilds` (12)
|
|
344
|
+
|
|
345
|
+
| Method | Path | Rank | Body | Description |
|
|
346
|
+
|---|---|---|---|---|
|
|
347
|
+
| POST | `/api/bongos/guilds` | `any-builder` | `slug`, `name`, `description`, `visibility` | create a guild. |
|
|
348
|
+
| GET | `/api/bongos/guilds/:slug` | `public` | — | a public guild's page data. |
|
|
349
|
+
| PATCH | `/api/bongos/guilds/:slug` | `any-builder` | `name`, `description`, `visibility` | owner: edit name, description or visibility. |
|
|
350
|
+
| DELETE | `/api/bongos/guilds/:slug` | `any-builder` | — | owner: dissolve. |
|
|
351
|
+
| POST | `/api/bongos/guilds/:slug/invites` | `any-builder` | `handles` | owner: invite up to INVITE_BATCH_MAX builders by handle, each an ordinary individual invite (ADR 0336 D3). |
|
|
352
|
+
| GET | `/api/bongos/guilds/:slug/members` | `any-builder` | — | the members' roster, for members only (the own-scope door to an unlisted guild, ADR 0336 D4). |
|
|
353
|
+
| DELETE | `/api/bongos/guilds/:slug/members/:handle` | `any-builder` | — | owner: remove a member (never the owner). |
|
|
354
|
+
| PATCH | `/api/bongos/guilds/:slug/membership` | `any-builder` | `shown_publicly`, `counted_in_totals` | MY switches in one guild: shown on its public page (ADR 0336 D4) and counted in its totals (ADR 0337 D4.4). |
|
|
355
|
+
| DELETE | `/api/bongos/guilds/:slug/membership` | `any-builder` | — | leave. |
|
|
356
|
+
| GET | `/api/bongos/guilds/:slug/requests` | `any-builder` | — | owner: the join requests to decide and the invites still out. |
|
|
357
|
+
| POST | `/api/bongos/guilds/:slug/requests` | `any-builder` | `counted_in_totals` | ask to join. |
|
|
358
|
+
| POST | `/api/bongos/guilds/:slug/transfer` | `any-builder` | `handle` | owner: hand ownership to an existing member. |
|
|
359
|
+
|
|
334
360
|
## `healthz` (1)
|
|
335
361
|
|
|
336
362
|
| Method | Path | Rank | Body | Description |
|
|
@@ -405,7 +431,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
405
431
|
| POST | `/api/bongos/llm-cache/lookup` | `metic+archon` | `cache_key`, `tokens_saved_est` | POST /llm-cache/lookup — hit/miss + atomic hit_count bump. |
|
|
406
432
|
| POST | `/api/bongos/llm-cache/store` | `metic+archon` | `spec`, `result_stdout`, `result_cost_usd` | POST /llm-cache/store — upsert a freshly-produced result. |
|
|
407
433
|
|
|
408
|
-
## `me` (
|
|
434
|
+
## `me` (34)
|
|
409
435
|
|
|
410
436
|
| Method | Path | Rank | Body | Description |
|
|
411
437
|
|---|---|---|---|---|
|
|
@@ -420,6 +446,8 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
420
446
|
| PATCH | `/api/bongos/me/display-name` | `any-builder` | _undocumented_ | PATCH the display name. |
|
|
421
447
|
| GET | `/api/bongos/me/event-sound-prefs` | `any-builder` | — | Per-builder per-event sound variant selection (task 582, migration 164). |
|
|
422
448
|
| PATCH | `/api/bongos/me/event-sound-prefs` | `any-builder` | _undocumented_ | PATCH a partial set of event sound overrides. |
|
|
449
|
+
| GET | `/api/bongos/me/guild-requests` | `any-builder` | — | MY queue: invites addressed to me, each carrying the card (the guild's current visibility, which an accept must echo back), and the join … |
|
|
450
|
+
| GET | `/api/bongos/me/guilds` | `any-builder` | — | MY guilds, each with my own two switches. |
|
|
423
451
|
| PATCH | `/api/bongos/me/handle` | `any-builder` | `handle` | rename the caller's OWN handle (own data, keyed on the authenticated github_id). |
|
|
424
452
|
| GET | `/api/bongos/me/interaction` | `any-builder` | — | Per-builder INTERACTION PROFILE (task 2010, shares the behavior_prefs jsonb / migration 119). |
|
|
425
453
|
| PATCH | `/api/bongos/me/interaction` | `any-builder` | _undocumented_ | PATCH the interaction profile. |
|
|
@@ -516,7 +544,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
516
544
|
| PATCH | `/api/bongos/projects/:id/status` | `metic+archon` | `status` | THE TAKEDOWN LEVER (R08, task 1002327; ADR 0252 §5.1). |
|
|
517
545
|
| POST | `/api/bongos/projects/:projectId/specialities` | `any-builder` | `name`, `discipline`, `summary`, `contract_md`, `visibility`, `sellable` | POST /projects/:projectId/specialities — author one OWNED BY A PROJECT. |
|
|
518
546
|
| GET | `/api/bongos/projects/featured` | `public` | — | the public map of projects; no auth by design. |
|
|
519
|
-
| POST | `/api/bongos/projects/invite` | `metic+archon` | `client_id`, `github_login` | own-or-metic in effect: invite a builder into a project (ADR 0141 §5, task
|
|
547
|
+
| POST | `/api/bongos/projects/invite` | `metic+archon` | `client_id`, `github_login` | own-or-metic in effect: invite a builder into a project (ADR 0141 §5, task 1002115), called by a `project.curate` holder OR the project's… |
|
|
520
548
|
|
|
521
549
|
## `provisioning` (31)
|
|
522
550
|
|
package/docs/copy-inventory.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
> Deterministic on a given tree — regenerate and commit rather than hand-editing.
|
|
5
5
|
> Machine-readable twin: [`copy-registry.json`](copy-registry.json).
|
|
6
6
|
|
|
7
|
-
**2040 strings** across 3 surfaces ·
|
|
7
|
+
**2040 strings** across 3 surfaces · 1742 distinct pieces of copy · 2040 certain, 0 heuristic · 211 shown in more than one place.
|
|
8
8
|
|
|
9
9
|
## Surfaces
|
|
10
10
|
|
|
@@ -76,7 +76,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
76
76
|
| `064c72453555` | 3x | goals · | modules/hall-ui/public/roadmap.js:218, modules/hall-ui/public/sky-interaction.js:39, modules/hall-ui/public/sky-interaction.js:124 |
|
|
77
77
|
| `cc5106af4b35` | 3x | {…} of {…} criteria satisfied | modules/hall-ui/public/goals-lib.js:32, modules/hall-ui/public/goals-render.js:705, modules/hall-ui/public/goals.js:45 |
|
|
78
78
|
| `7926e30955d2` | 3x | ← Back to goal | modules/hall-ui/public/goals.html:199, modules/hall-ui/public/goals.html:210, modules/hall-ui/public/goals.html:222 |
|
|
79
|
-
| … | |
|
|
79
|
+
| … | | _171 more in the JSON registry_ | |
|
|
80
80
|
|
|
81
81
|
## `builders-hall` — 1547 strings
|
|
82
82
|
|
|
@@ -1062,7 +1062,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1062
1062
|
| `19b23f2cc45b` | certain | The grading panel could not run when this task was shipped, so no quality signal was recorded — an outage, not | `modules/hall-ui/public/task.js:413` |
|
|
1063
1063
|
| `027e9420daa2` | certain | The headline lands, but the sub-line reads like a brochure. | `modules/hall-ui/public/studio.js:162` |
|
|
1064
1064
|
| `9f45c63c9697` | certain | The kinds of work you prefer. Task suggestions lean toward your choices — pick one, several, or none. | `modules/hall-ui/public/settings.html:127` |
|
|
1065
|
-
| `
|
|
1065
|
+
| `814eb6889339` | certain | The ledger | `modules/hall-ui/public/goal-map.html:99` |
|
|
1066
1066
|
| `dbbcedba5f3c` | certain | The list | `modules/hall-ui/public/thinking.html:98` |
|
|
1067
1067
|
| `a02d386fbc13` | certain | The map | `modules/hall-ui/public/atlas.html:45` |
|
|
1068
1068
|
| `e1ce3766c6fe` | certain | The map has not been generated on this deploy yet. | `modules/hall-ui/public/atlas.js:416` |
|
|
@@ -1850,7 +1850,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1850
1850
|
| `485c565b5745` | certain | How people join | `modules/public-landing/public/projects.html:801` |
|
|
1851
1851
|
| `4b542b000afa` | certain | How this project is drawn: on the sky, in your list, at the top of this page and on its card. A planet is a | `modules/public-landing/public/projects.html:921` |
|
|
1852
1852
|
| `9f31eaed8252` | certain | If you grant repository access while creating a project, that permission is used to create or adopt your repos | `modules/public-landing/public/privacy.html:55` |
|
|
1853
|
-
| `
|
|
1853
|
+
| `3b1cbe9e33aa` | certain | If your account already existed when this version took effect, the platform accepted it on your behalf, rather | `modules/public-landing/public/terms.html:71` |
|
|
1854
1854
|
| `607551ae732b` | certain | Invite a builder | `modules/public-landing/public/projects.html:894` |
|
|
1855
1855
|
| `20786b02f47a` | certain | Invite builders | `modules/public-landing/public/projects.html:1305` |
|
|
1856
1856
|
| `a1256030d879` | certain | Invite only | `modules/public-landing/public/projects.html:805` |
|
|
@@ -1885,7 +1885,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1885
1885
|
| `837fdaa0936a` | certain | Nothing matches that. | `modules/public-landing/public/projects.html:649` |
|
|
1886
1886
|
| `1bc11853e893` | certain | On the map | `modules/public-landing/public/projects.html:820` |
|
|
1887
1887
|
| `b4ac8f44710c` | certain | On the public map | `modules/public-landing/public/projects.html:742` |
|
|
1888
|
-
| `
|
|
1888
|
+
| `7fe31ce20c09` | certain | Once you have signed in to a project with your platform account and accepted these terms, your profile and tha | `modules/public-landing/public/terms.html:68` |
|
|
1889
1889
|
| `8329bb1a3693` | certain | One tap files the request and the platform starts standing your project up — server, database, address. Here's | `modules/public-landing/public/projects.html:1277` |
|
|
1890
1890
|
| `86cde1930d84` | certain | Only mine | `modules/public-landing/public/projects.html:419` |
|
|
1891
1891
|
| `f82fe1982ab3` | certain | Open an issue on the repository | `modules/public-landing/public/contact.html:52` |
|
|
@@ -1981,7 +1981,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1981
1981
|
| `fb416bf1a26b` | certain | The handle names your project on the platform — and its address comes with it. Once it stands up, a few GitHub | `modules/public-landing/public/projects.html:1245` |
|
|
1982
1982
|
| `a954f0dce9a3` | certain | The platform | `modules/public-landing/public/contact.html:65` |
|
|
1983
1983
|
| `4fd8f5746cc5` | certain | The platform is run as a non-profit, capture-resistant commons. | `modules/public-landing/public/terms.html:50` |
|
|
1984
|
-
| `
|
|
1984
|
+
| `dede132846fe` | certain | The platform stores the date you accepted these terms, and which version, on your account. It is written when | `modules/public-landing/public/terms.html:70` |
|
|
1985
1985
|
| `20fcebaa85c4` | certain | The project hub needs JavaScript — the map, your projects, and starting a new one all live here. You can read | `modules/public-landing/public/projects.html:472` |
|
|
1986
1986
|
| `4a740b5afe73` | certain | The project map | `modules/public-landing/public/projects.html:482` |
|
|
1987
1987
|
| `345fec12e5d8` | certain | The repository | `modules/public-landing/public/contact.html:50` |
|
package/docs/copy-registry.json
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
"goal": 1000074,
|
|
5
5
|
"counts": {
|
|
6
6
|
"strings": 2040,
|
|
7
|
-
"unique_ids":
|
|
7
|
+
"unique_ids": 1742,
|
|
8
8
|
"certain": 2040,
|
|
9
9
|
"heuristic": 0,
|
|
10
|
-
"duplicate_ids":
|
|
10
|
+
"duplicate_ids": 211
|
|
11
11
|
},
|
|
12
12
|
"surfaces": {
|
|
13
13
|
"builders-hall": {
|
|
@@ -1665,16 +1665,6 @@
|
|
|
1665
1665
|
"modules/hall-ui/public/thinking.html:123"
|
|
1666
1666
|
]
|
|
1667
1667
|
},
|
|
1668
|
-
{
|
|
1669
|
-
"id": "dbbcedba5f3c",
|
|
1670
|
-
"surface": "builders-hall",
|
|
1671
|
-
"text": "The list",
|
|
1672
|
-
"occurrences": 2,
|
|
1673
|
-
"at": [
|
|
1674
|
-
"modules/hall-ui/public/goal-map.html:99",
|
|
1675
|
-
"modules/hall-ui/public/thinking.html:98"
|
|
1676
|
-
]
|
|
1677
|
-
},
|
|
1678
1668
|
{
|
|
1679
1669
|
"id": "7f03838faf0c",
|
|
1680
1670
|
"surface": "builders-hall",
|
|
@@ -11068,9 +11058,9 @@
|
|
|
11068
11058
|
"confidence": "certain"
|
|
11069
11059
|
},
|
|
11070
11060
|
{
|
|
11071
|
-
"id": "
|
|
11061
|
+
"id": "814eb6889339",
|
|
11072
11062
|
"surface": "builders-hall",
|
|
11073
|
-
"text": "The
|
|
11063
|
+
"text": "The ledger",
|
|
11074
11064
|
"file": "modules/hall-ui/public/goal-map.html",
|
|
11075
11065
|
"line": 99,
|
|
11076
11066
|
"origin": "html-attr:aria-label",
|
|
@@ -17170,9 +17160,9 @@
|
|
|
17170
17160
|
"confidence": "certain"
|
|
17171
17161
|
},
|
|
17172
17162
|
{
|
|
17173
|
-
"id": "
|
|
17163
|
+
"id": "3b1cbe9e33aa",
|
|
17174
17164
|
"surface": "landing",
|
|
17175
|
-
"text": "If your account already existed when this version took effect, the platform accepted it on your behalf, rather than waiting for you to sign in, and dated it the day that was done. The record marks that acceptance as grandfathered, so it is never mistaken for one you gave, and your own acceptance replaces it the next time you sign in.",
|
|
17165
|
+
"text": "If your account already existed when this version took effect, the platform accepted it on your behalf, rather than waiting for you to sign in, and dated it the day that was done. The record marks that acceptance as grandfathered, so it is never mistaken for one you gave, and your own acceptance replaces it the next time you sign in to a project.",
|
|
17176
17166
|
"file": "modules/public-landing/public/terms.html",
|
|
17177
17167
|
"line": 71,
|
|
17178
17168
|
"origin": "html-text",
|
|
@@ -17485,9 +17475,9 @@
|
|
|
17485
17475
|
"confidence": "certain"
|
|
17486
17476
|
},
|
|
17487
17477
|
{
|
|
17488
|
-
"id": "
|
|
17478
|
+
"id": "7fe31ce20c09",
|
|
17489
17479
|
"surface": "landing",
|
|
17490
|
-
"text": "Once you have signed in
|
|
17480
|
+
"text": "Once you have signed in to a project with your platform account and accepted these terms, your profile and that record are public by default: at your handle’s page, in the people directory, to project owners looking for people to work with, and to search engines. An account that has never signed in to a project that way stays provisional and is published nowhere.",
|
|
17491
17481
|
"file": "modules/public-landing/public/terms.html",
|
|
17492
17482
|
"line": 68,
|
|
17493
17483
|
"origin": "html-text",
|
|
@@ -18349,9 +18339,9 @@
|
|
|
18349
18339
|
"confidence": "certain"
|
|
18350
18340
|
},
|
|
18351
18341
|
{
|
|
18352
|
-
"id": "
|
|
18342
|
+
"id": "dede132846fe",
|
|
18353
18343
|
"surface": "landing",
|
|
18354
|
-
"text": "The platform stores the date you accepted these terms, and which version, on your account. It is written
|
|
18344
|
+
"text": "The platform stores the date you accepted these terms, and which version, on your account. It is written when you sign in to a project with your platform account, and only once — signing in again does not change the date you accepted. This is version",
|
|
18355
18345
|
"file": "modules/public-landing/public/terms.html",
|
|
18356
18346
|
"line": 70,
|
|
18357
18347
|
"origin": "html-text",
|
|
@@ -2529,5 +2529,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2529
2529
|
landed since 1.19.1023 with no explicit bump. run 36183325302. (task 1002620)
|
|
2530
2530
|
1.19.1025 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2531
2531
|
landed since 1.19.1024 with no explicit bump. run 36185386905. (task 1002620)
|
|
2532
|
+
1.19.1026 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2533
|
+
landed since 1.19.1025 with no explicit bump. run 36186644421. (task 1002620)
|
|
2534
|
+
1.19.1027 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2535
|
+
landed since 1.19.1026 with no explicit bump. run 36187742785. (task 1002620)
|
|
2532
2536
|
---------------------------------------------------------------------------
|
|
2533
2537
|
```
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
<div class="card glass" id="card" role="tooltip" aria-hidden="true"></div>
|
|
97
97
|
<div class="menu glass" id="menu" hidden role="menu" aria-label="Chart the routes"></div>
|
|
98
98
|
|
|
99
|
-
<aside class="room" id="room" hidden aria-label="The
|
|
99
|
+
<aside class="room" id="room" hidden aria-label="The ledger">
|
|
100
100
|
<div class="head"><h2 id="roomTitle">the ledger</h2><span class="lbl" id="roomCount"></span></div>
|
|
101
101
|
<div class="filters" id="filters" role="group" aria-label="Filter"></div>
|
|
102
102
|
<div class="rows" id="rows"></div>
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// modules/platform-identity/community-leaderboard.js — the PUBLIC windowed
|
|
2
|
+
// leaderboard (D3, docs/specs/bongos-v1-community-tab.md; task 1002978, goal
|
|
3
|
+
// 1000110 CT2): "who earned the most across every project, this week / month /
|
|
4
|
+
// year / ever", optionally narrowed to one discipline. The read behind
|
|
5
|
+
// GET /community/leaderboard, for the hub's /community page (task 1003363).
|
|
6
|
+
//
|
|
7
|
+
// WHO: rollupVisibleSql — active + public + NOT hide_stats, the privacy matrix's
|
|
8
|
+
// NUMBERS predicate, imported from account-visibility.js and never restated. A
|
|
9
|
+
// leaderboard row IS stats, so hide_stats excludes; private and provisional
|
|
10
|
+
// accounts never reach it. Deliberately NOT scoutingListableSql: the recruiting
|
|
11
|
+
// opt-out governs the recruiting directory only (spec D3). Viewer-independent —
|
|
12
|
+
// nothing here takes a viewer, so every caller sees the same board.
|
|
13
|
+
//
|
|
14
|
+
// WHAT (spec D2): all-time = the live rollup rows summed across projects. A
|
|
15
|
+
// window = per (project, builder), the live row minus the last snapshot
|
|
16
|
+
// at-or-before the window start (activity-snapshots.js), summed across projects;
|
|
17
|
+
// no such snapshot counts as zero. So until a builder's first changed report
|
|
18
|
+
// after snapshots began, their whole lifetime total counts inside every window —
|
|
19
|
+
// the spec's accepted rollout behaviour, self-correcting. Deltas attribute to
|
|
20
|
+
// REPORTING time, not earning time (the spec's stated V1 limitation).
|
|
21
|
+
//
|
|
22
|
+
// TWO HALVES, because the board is public and unauthenticated. The expensive
|
|
23
|
+
// half — every builder's window totals, one snapshot probe per activity row — is
|
|
24
|
+
// memoized per window for TOTALS_TTL_MS, so traffic cannot multiply it. The cheap
|
|
25
|
+
// half — who is eligible, and their name and avatar — is re-read on EVERY
|
|
26
|
+
// request, so a builder who hides or goes private leaves the board on the very
|
|
27
|
+
// next request (hide must beat the cache, ADR 0171 D5). The memo can only lag on
|
|
28
|
+
// numbers, never on who is shown.
|
|
29
|
+
//
|
|
30
|
+
// A builder with nothing to show in the window (credits, ships and karma all
|
|
31
|
+
// unchanged) is left off that window's board rather than listed at zero.
|
|
32
|
+
// github_id never crosses the projection; handle is the address.
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
const { pool: defaultPool } = require('../../src/module-api');
|
|
36
|
+
const { rollupVisibleSql } = require('./account-visibility');
|
|
37
|
+
// The sibling public community read's paging + input caps, shared rather than
|
|
38
|
+
// copied so the two routes cannot drift apart.
|
|
39
|
+
const { clampOffset, MAX_OFFSET, MAX_DISCIPLINE_CHARS } = require('./builder-search');
|
|
40
|
+
|
|
41
|
+
// The spec's four windows. null = all-time (no baseline subtracted).
|
|
42
|
+
const WINDOW_DAYS = Object.freeze({ '7d': 7, '30d': 30, '365d': 365, all: null });
|
|
43
|
+
const WINDOWS = Object.freeze(Object.keys(WINDOW_DAYS));
|
|
44
|
+
const DEFAULT_WINDOW = 'all';
|
|
45
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
46
|
+
|
|
47
|
+
const DEFAULT_LIMIT = 25;
|
|
48
|
+
const MAX_LIMIT = 100;
|
|
49
|
+
// How long one window's totals are reused. Numbers are reporting-time already
|
|
50
|
+
// (they move when an instance reports), so half a minute of lag is invisible;
|
|
51
|
+
// eligibility is never memoized.
|
|
52
|
+
const TOTALS_TTL_MS = 30 * 1000;
|
|
53
|
+
|
|
54
|
+
function isWindow(value) {
|
|
55
|
+
return typeof value === 'string' && Object.prototype.hasOwnProperty.call(WINDOW_DAYS, value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The instant a window opens, or null for all-time. `now` is injectable so a
|
|
59
|
+
// test can pin the boundary.
|
|
60
|
+
function windowStart(window, now = new Date()) {
|
|
61
|
+
const days = WINDOW_DAYS[window];
|
|
62
|
+
if (days == null) return null;
|
|
63
|
+
return new Date(now.getTime() - days * DAY_MS);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function clampLimit(limit) {
|
|
67
|
+
const n = parseInt(limit, 10);
|
|
68
|
+
if (!Number.isFinite(n) || n <= 0) return DEFAULT_LIMIT;
|
|
69
|
+
return Math.min(n, MAX_LIMIT);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Every eligible builder's totals for one window, unsorted and unpaged:
|
|
73
|
+
// [{ githubId, credits, tasks_shipped, karma, disciplines }]. The expensive
|
|
74
|
+
// half. Arithmetic is bigint: the columns are instance-reported int4, and one
|
|
75
|
+
// extreme report must not overflow the whole public board into a 500.
|
|
76
|
+
async function computeWindowTotals(since, pool) {
|
|
77
|
+
const { rows } = await pool.query(
|
|
78
|
+
`WITH act AS (
|
|
79
|
+
SELECT a.client_id, a.github_id, a.credits, a.tasks_shipped, a.karma, a.disciplines
|
|
80
|
+
FROM platform_identity_builder_project_activity a
|
|
81
|
+
JOIN platform_identity_accounts pa ON pa.github_id = a.github_id
|
|
82
|
+
WHERE ${rollupVisibleSql('pa')}
|
|
83
|
+
), per_project AS (
|
|
84
|
+
-- The baseline probe walks platform_identity_activity_snapshots_builder_time
|
|
85
|
+
-- in index order. All-time binds $1 NULL, so the probe never runs.
|
|
86
|
+
SELECT act.github_id,
|
|
87
|
+
act.credits::bigint - COALESCE(b.credits, 0) AS credits,
|
|
88
|
+
act.tasks_shipped::bigint - COALESCE(b.tasks_shipped, 0) AS tasks_shipped,
|
|
89
|
+
act.karma::bigint - COALESCE(b.karma, 0) AS karma
|
|
90
|
+
FROM act
|
|
91
|
+
LEFT JOIN LATERAL (
|
|
92
|
+
SELECT s.credits, s.tasks_shipped, s.karma
|
|
93
|
+
FROM platform_identity_activity_snapshots s
|
|
94
|
+
WHERE $1::timestamptz IS NOT NULL
|
|
95
|
+
AND s.client_id = act.client_id AND s.github_id = act.github_id
|
|
96
|
+
AND s.reported_at <= $1::timestamptz
|
|
97
|
+
ORDER BY s.reported_at DESC, s.id DESC
|
|
98
|
+
LIMIT 1
|
|
99
|
+
) b ON true
|
|
100
|
+
), board AS (
|
|
101
|
+
SELECT github_id,
|
|
102
|
+
sum(credits)::bigint AS credits,
|
|
103
|
+
sum(tasks_shipped)::bigint AS tasks_shipped,
|
|
104
|
+
sum(karma)::bigint AS karma
|
|
105
|
+
FROM per_project
|
|
106
|
+
GROUP BY github_id
|
|
107
|
+
), disc AS (
|
|
108
|
+
SELECT x.github_id, array_agg(DISTINCT x.d ORDER BY x.d) AS disciplines
|
|
109
|
+
FROM (SELECT act.github_id, unnest(act.disciplines) AS d FROM act) x
|
|
110
|
+
WHERE x.d IS NOT NULL
|
|
111
|
+
GROUP BY x.github_id
|
|
112
|
+
)
|
|
113
|
+
SELECT board.github_id, board.credits, board.tasks_shipped, board.karma, disc.disciplines
|
|
114
|
+
FROM board
|
|
115
|
+
LEFT JOIN disc ON disc.github_id = board.github_id
|
|
116
|
+
WHERE board.credits <> 0 OR board.tasks_shipped <> 0 OR board.karma <> 0`,
|
|
117
|
+
[since],
|
|
118
|
+
);
|
|
119
|
+
// int8 arrives from node-postgres as a string; any real total is far inside
|
|
120
|
+
// Number's exact-integer range.
|
|
121
|
+
return rows.map((r) => ({
|
|
122
|
+
githubId: String(r.github_id),
|
|
123
|
+
credits: Number(r.credits),
|
|
124
|
+
tasks_shipped: Number(r.tasks_shipped),
|
|
125
|
+
karma: Number(r.karma),
|
|
126
|
+
disciplines: Array.isArray(r.disciplines) ? r.disciplines : [],
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// window -> { expires, since, promise }. Concurrent requests share one
|
|
131
|
+
// in-flight computation; a failed one is dropped so the next request retries.
|
|
132
|
+
const totalsMemo = new Map();
|
|
133
|
+
|
|
134
|
+
function memoizedWindowTotals(window, pool) {
|
|
135
|
+
const hit = totalsMemo.get(window);
|
|
136
|
+
if (hit && hit.pool === pool && hit.expires > Date.now()) return hit;
|
|
137
|
+
const since = windowStart(window);
|
|
138
|
+
const entry = { pool, since, expires: Date.now() + TOTALS_TTL_MS, promise: computeWindowTotals(since, pool) };
|
|
139
|
+
entry.promise.catch(() => { if (totalsMemo.get(window) === entry) totalsMemo.delete(window); });
|
|
140
|
+
totalsMemo.set(window, entry);
|
|
141
|
+
return entry;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function clearTotalsMemo() {
|
|
145
|
+
totalsMemo.clear();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The cheap half, re-read on every request: which of these builders are
|
|
149
|
+
// eligible RIGHT NOW, and how they are shown. Map githubId -> identity.
|
|
150
|
+
async function eligibleIdentities(githubIds, pool) {
|
|
151
|
+
if (githubIds.length === 0) return new Map();
|
|
152
|
+
const { rows } = await pool.query(
|
|
153
|
+
`SELECT pa.github_id, pa.handle, pa.github_login, pa.display_name, pa.avatar_url
|
|
154
|
+
FROM platform_identity_accounts pa
|
|
155
|
+
WHERE pa.github_id = ANY($1::bigint[])
|
|
156
|
+
AND ${rollupVisibleSql('pa')}`,
|
|
157
|
+
[githubIds],
|
|
158
|
+
);
|
|
159
|
+
return new Map(rows.map((r) => [String(r.github_id), r]));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Credits, then ships, then karma, then handle/login so a tie is stable —
|
|
163
|
+
// compared by code unit, so the order never depends on the server's locale.
|
|
164
|
+
const byText = (x, y) => { const a = String(x ?? ''); const b = String(y ?? ''); return a < b ? -1 : a > b ? 1 : 0; };
|
|
165
|
+
function compareRows(a, b) {
|
|
166
|
+
return (b.credits - a.credits) || (b.tasks_shipped - a.tasks_shipped) || (b.karma - a.karma)
|
|
167
|
+
|| byText(a.handle, b.handle) || byText(a.github_login, b.github_login);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// listLeaderboard — one page of the board. Throws on an unknown window (the
|
|
171
|
+
// route validates and 400s first). Returns { window, since, results, limit,
|
|
172
|
+
// offset }: `since` is the window start the totals were computed from (null
|
|
173
|
+
// for all-time) and limit/offset are the CLAMPED values, so the route echoes
|
|
174
|
+
// rather than re-derives them. `place` is the 1-based position on the whole
|
|
175
|
+
// board — named so it can never be read as a builder's rank. Passing `now`
|
|
176
|
+
// pins the window start and bypasses the memo (tests of the boundary).
|
|
177
|
+
async function listLeaderboard({ window = DEFAULT_WINDOW, discipline, limit, offset, now, pool = defaultPool } = {}) {
|
|
178
|
+
if (!isWindow(window)) throw new Error(`listLeaderboard: unknown window ${JSON.stringify(window)}`);
|
|
179
|
+
const lim = clampLimit(limit);
|
|
180
|
+
const off = clampOffset(offset);
|
|
181
|
+
const disc = typeof discipline === 'string' ? discipline.trim().slice(0, MAX_DISCIPLINE_CHARS) : '';
|
|
182
|
+
|
|
183
|
+
let since;
|
|
184
|
+
let totals;
|
|
185
|
+
if (now !== undefined) {
|
|
186
|
+
since = windowStart(window, now);
|
|
187
|
+
totals = await computeWindowTotals(since, pool);
|
|
188
|
+
} else {
|
|
189
|
+
const entry = memoizedWindowTotals(window, pool);
|
|
190
|
+
since = entry.since;
|
|
191
|
+
totals = await entry.promise;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const candidates = disc ? totals.filter((t) => t.disciplines.includes(disc)) : totals;
|
|
195
|
+
const identities = await eligibleIdentities(candidates.map((t) => t.githubId), pool);
|
|
196
|
+
const board = candidates
|
|
197
|
+
.filter((t) => identities.has(t.githubId))
|
|
198
|
+
.map((t) => {
|
|
199
|
+
const who = identities.get(t.githubId);
|
|
200
|
+
return {
|
|
201
|
+
handle: who.handle,
|
|
202
|
+
github_login: who.github_login,
|
|
203
|
+
display_name: who.display_name,
|
|
204
|
+
avatar_url: who.avatar_url,
|
|
205
|
+
credits: t.credits,
|
|
206
|
+
tasks_shipped: t.tasks_shipped,
|
|
207
|
+
karma: t.karma,
|
|
208
|
+
disciplines: t.disciplines,
|
|
209
|
+
};
|
|
210
|
+
})
|
|
211
|
+
.sort(compareRows);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
window,
|
|
215
|
+
since: since ? since.toISOString() : null,
|
|
216
|
+
results: board.slice(off, off + lim).map((row, i) => ({ place: off + i + 1, ...row })),
|
|
217
|
+
limit: lim,
|
|
218
|
+
offset: off,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
listLeaderboard,
|
|
224
|
+
clearTotalsMemo,
|
|
225
|
+
windowStart,
|
|
226
|
+
isWindow,
|
|
227
|
+
clampLimit,
|
|
228
|
+
WINDOWS,
|
|
229
|
+
DEFAULT_WINDOW,
|
|
230
|
+
DEFAULT_LIMIT,
|
|
231
|
+
MAX_LIMIT,
|
|
232
|
+
MAX_OFFSET,
|
|
233
|
+
MAX_DISCIPLINE_CHARS,
|
|
234
|
+
TOTALS_TTL_MS,
|
|
235
|
+
};
|