@bongos/core 1.20.18 → 1.20.20
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 +72 -27
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0352-a-rollup-report-carries-its-as-of-in-a-header.md +38 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +6 -4
- package/docs/module-api-changelog.md +4 -0
- package/modules/platform-identity/guild-totals.js +217 -0
- package/modules/platform-identity/guilds.js +6 -2
- package/modules/platform-identity/migrations/platform_identity_026_activity_reported_as_of.sql +27 -0
- package/modules/platform-identity/migrations/platform_identity_027_guild_totals.sql +45 -0
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/platform-identity.js +11 -6
- package/modules/platform-identity/pollers/guild-totals.js +76 -0
- package/modules/platform-identity/routes/guilds-public.js +11 -9
- package/modules/platform-identity/routes/sso.js +18 -0
- package/modules/platform-identity/tests/platform-identity.mjs +3 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/run-unit-tests.js +9 -0
- package/src/bongos/auth-admission.js +8 -1
- package/src/module-api.js +1 -1
- package/tests/activity_rollup_as_of.mjs +172 -0
- package/tests/activity_rollup_order_db.mjs +131 -0
- package/tests/activity_snapshots_db.mjs +2 -0
- package/tests/guild_totals.mjs +183 -0
- package/tests/guild_totals_db.mjs +292 -0
- package/tests/guilds_api.mjs +6 -1
- package/tests/profile_rollup_consent.mjs +2 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.20.
|
|
6
|
-
"core_contract": "1.20.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.20.20",
|
|
6
|
+
"core_contract": "1.20.20",
|
|
7
|
+
"source_commit": "81de3611c8c98f0a428be1780f94435846cd7bf2",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-30T14:55:04.182Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 557,
|
|
13
13
|
"agent_docs_stubbed": 25,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2636,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3219,
|
|
20
|
+
"tree_sha256": "9d08157c7a131c0010616436de1c1b4b0ecd19030def84afd3ed402813e39499",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -347,7 +347,7 @@
|
|
|
347
347
|
{
|
|
348
348
|
"path": "clients/bongos-client/index.d.ts",
|
|
349
349
|
"mode": "0000644",
|
|
350
|
-
"sha256": "
|
|
350
|
+
"sha256": "d9b3edd04125960791f6282252f5f31b2ace6abb90beeb19c483af1a422fbf53"
|
|
351
351
|
},
|
|
352
352
|
{
|
|
353
353
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -2229,10 +2229,15 @@
|
|
|
2229
2229
|
"mode": "0000644",
|
|
2230
2230
|
"sha256": "4ca4eae961c994cae0dd9d306ea0a5a6f9ce660de161a62c5ccd125d910f70ec"
|
|
2231
2231
|
},
|
|
2232
|
+
{
|
|
2233
|
+
"path": "docs/adr/0352-a-rollup-report-carries-its-as-of-in-a-header.md",
|
|
2234
|
+
"mode": "0000644",
|
|
2235
|
+
"sha256": "d74b4f6262d4f23fd67f73771bbe8187e9cb92fa321b83ffb33ef03cf96d1236"
|
|
2236
|
+
},
|
|
2232
2237
|
{
|
|
2233
2238
|
"path": "docs/adr/README.md",
|
|
2234
2239
|
"mode": "0000644",
|
|
2235
|
-
"sha256": "
|
|
2240
|
+
"sha256": "943ac0f8c9c0b2dc5ef2a05241b0bac329c5fa986b09425841e12e62f4f9b936"
|
|
2236
2241
|
},
|
|
2237
2242
|
{
|
|
2238
2243
|
"path": "docs/api-reference.md",
|
|
@@ -2242,7 +2247,7 @@
|
|
|
2242
2247
|
{
|
|
2243
2248
|
"path": "docs/api/openapi.json",
|
|
2244
2249
|
"mode": "0000644",
|
|
2245
|
-
"sha256": "
|
|
2250
|
+
"sha256": "4167b07fddd64f47f901082e130a12f307e4e2fab5ac9bc821e24a0c4f6f3d72"
|
|
2246
2251
|
},
|
|
2247
2252
|
{
|
|
2248
2253
|
"path": "docs/architecture.md",
|
|
@@ -2772,7 +2777,7 @@
|
|
|
2772
2777
|
{
|
|
2773
2778
|
"path": "docs/module-api-changelog.md",
|
|
2774
2779
|
"mode": "0000644",
|
|
2775
|
-
"sha256": "
|
|
2780
|
+
"sha256": "2b82bf65da0987ffc862ad6369e0cbcf8e6adef0a70915cf7a7a2c2289a69c85"
|
|
2776
2781
|
},
|
|
2777
2782
|
{
|
|
2778
2783
|
"path": "docs/modules-contract.md",
|
|
@@ -6859,10 +6864,15 @@
|
|
|
6859
6864
|
"mode": "0000644",
|
|
6860
6865
|
"sha256": "793e96feacef39ec52166dd7372c894a25679c7137c9c3c5e9df38ec323f1c4a"
|
|
6861
6866
|
},
|
|
6867
|
+
{
|
|
6868
|
+
"path": "modules/platform-identity/guild-totals.js",
|
|
6869
|
+
"mode": "0000644",
|
|
6870
|
+
"sha256": "8e07aed97641d68d826ffea3055d209b8fd9eb172e1d000f17e335f7296cc6d6"
|
|
6871
|
+
},
|
|
6862
6872
|
{
|
|
6863
6873
|
"path": "modules/platform-identity/guilds.js",
|
|
6864
6874
|
"mode": "0000644",
|
|
6865
|
-
"sha256": "
|
|
6875
|
+
"sha256": "e048e674b087eae9035da5b3449d8f4029ec329a42333980eb21ac3e6c13a297"
|
|
6866
6876
|
},
|
|
6867
6877
|
{
|
|
6868
6878
|
"path": "modules/platform-identity/hub-device.js",
|
|
@@ -7009,10 +7019,20 @@
|
|
|
7009
7019
|
"mode": "0000644",
|
|
7010
7020
|
"sha256": "72be4f7e3aa3afa47dd78de7a57dcdf57b877675b44fbdf02f6607a8632e1787"
|
|
7011
7021
|
},
|
|
7022
|
+
{
|
|
7023
|
+
"path": "modules/platform-identity/migrations/platform_identity_026_activity_reported_as_of.sql",
|
|
7024
|
+
"mode": "0000644",
|
|
7025
|
+
"sha256": "9f8d582d626dec82b55b96272f9d6d7f6c1488ac28bd9a98ab673404afde0c8e"
|
|
7026
|
+
},
|
|
7027
|
+
{
|
|
7028
|
+
"path": "modules/platform-identity/migrations/platform_identity_027_guild_totals.sql",
|
|
7029
|
+
"mode": "0000644",
|
|
7030
|
+
"sha256": "9b263f91ce6df19640ef8ae413f3eb40cc17589924fdf21256da7647b55631c1"
|
|
7031
|
+
},
|
|
7012
7032
|
{
|
|
7013
7033
|
"path": "modules/platform-identity/module.json",
|
|
7014
7034
|
"mode": "0000644",
|
|
7015
|
-
"sha256": "
|
|
7035
|
+
"sha256": "48c9d82d9ec647430a97ba19f403b22e7eba13e16ff553b6bbd08691a8918a7b"
|
|
7016
7036
|
},
|
|
7017
7037
|
{
|
|
7018
7038
|
"path": "modules/platform-identity/origin-containment.js",
|
|
@@ -7022,13 +7042,18 @@
|
|
|
7022
7042
|
{
|
|
7023
7043
|
"path": "modules/platform-identity/platform-identity.js",
|
|
7024
7044
|
"mode": "0000644",
|
|
7025
|
-
"sha256": "
|
|
7045
|
+
"sha256": "a823bd5e79795f93aa011470088b5840bf2ada6fb696fbf6b9b1c6c2ea85e5ef"
|
|
7026
7046
|
},
|
|
7027
7047
|
{
|
|
7028
7048
|
"path": "modules/platform-identity/pollers/code-sweep.js",
|
|
7029
7049
|
"mode": "0000644",
|
|
7030
7050
|
"sha256": "b6eea95dbe66a3126fc2c73d93237cf38dd648a803e910eb88fbd5c780ccef62"
|
|
7031
7051
|
},
|
|
7052
|
+
{
|
|
7053
|
+
"path": "modules/platform-identity/pollers/guild-totals.js",
|
|
7054
|
+
"mode": "0000644",
|
|
7055
|
+
"sha256": "cfd421477fdcee4a8a92ed30fa1bb987e9f8a055862c8564de9a9bfe8eb5e089"
|
|
7056
|
+
},
|
|
7032
7057
|
{
|
|
7033
7058
|
"path": "modules/platform-identity/pollers/visibility-pull.js",
|
|
7034
7059
|
"mode": "0000644",
|
|
@@ -7082,7 +7107,7 @@
|
|
|
7082
7107
|
{
|
|
7083
7108
|
"path": "modules/platform-identity/routes/guilds-public.js",
|
|
7084
7109
|
"mode": "0000644",
|
|
7085
|
-
"sha256": "
|
|
7110
|
+
"sha256": "b6029f6f9d40fec18e42c0eaba75779ad7baa0c29df47bb7fe248a83cb783396"
|
|
7086
7111
|
},
|
|
7087
7112
|
{
|
|
7088
7113
|
"path": "modules/platform-identity/routes/guilds.js",
|
|
@@ -7117,7 +7142,7 @@
|
|
|
7117
7142
|
{
|
|
7118
7143
|
"path": "modules/platform-identity/routes/sso.js",
|
|
7119
7144
|
"mode": "0000644",
|
|
7120
|
-
"sha256": "
|
|
7145
|
+
"sha256": "2906d873fa46a2745fbbecefdeb81194f6f433e97deed65ee5fbee3e9a7fa140"
|
|
7121
7146
|
},
|
|
7122
7147
|
{
|
|
7123
7148
|
"path": "modules/platform-identity/scouting-authz.js",
|
|
@@ -7142,7 +7167,7 @@
|
|
|
7142
7167
|
{
|
|
7143
7168
|
"path": "modules/platform-identity/tests/platform-identity.mjs",
|
|
7144
7169
|
"mode": "0000644",
|
|
7145
|
-
"sha256": "
|
|
7170
|
+
"sha256": "93d74c10d601eab840da83b5d33d7bbf51907830626401cc27fadb138441fcc3"
|
|
7146
7171
|
},
|
|
7147
7172
|
{
|
|
7148
7173
|
"path": "modules/platform-identity/ui/.gitkeep",
|
|
@@ -8862,12 +8887,12 @@
|
|
|
8862
8887
|
{
|
|
8863
8888
|
"path": "package-lock.json",
|
|
8864
8889
|
"mode": "0000644",
|
|
8865
|
-
"sha256": "
|
|
8890
|
+
"sha256": "830a8f9950a12b57c520a79198562f423818f1f1a98095d7122ddf4a6100f052"
|
|
8866
8891
|
},
|
|
8867
8892
|
{
|
|
8868
8893
|
"path": "package.json",
|
|
8869
8894
|
"mode": "0000644",
|
|
8870
|
-
"sha256": "
|
|
8895
|
+
"sha256": "3b0d575917863d61bca84ddc2ce68886c33bc0d4fae9825a7abb8e2a5da7eec8"
|
|
8871
8896
|
},
|
|
8872
8897
|
{
|
|
8873
8898
|
"path": "public-docs/index.html",
|
|
@@ -8887,7 +8912,7 @@
|
|
|
8887
8912
|
{
|
|
8888
8913
|
"path": "release-notes.json",
|
|
8889
8914
|
"mode": "0000644",
|
|
8890
|
-
"sha256": "
|
|
8915
|
+
"sha256": "47fee7ce9947c485d183f574d1564c43da3329822ffd470897609631fe95effc"
|
|
8891
8916
|
},
|
|
8892
8917
|
{
|
|
8893
8918
|
"path": "scripts/bongos-mcp.js",
|
|
@@ -10047,7 +10072,7 @@
|
|
|
10047
10072
|
{
|
|
10048
10073
|
"path": "scripts/gds/run-unit-tests.js",
|
|
10049
10074
|
"mode": "0000644",
|
|
10050
|
-
"sha256": "
|
|
10075
|
+
"sha256": "b4b2358efdc538a35472258f5c0812bc3990e179612f663810cdcde6f1e0b143"
|
|
10051
10076
|
},
|
|
10052
10077
|
{
|
|
10053
10078
|
"path": "scripts/gds/runner-drift.js",
|
|
@@ -10667,7 +10692,7 @@
|
|
|
10667
10692
|
{
|
|
10668
10693
|
"path": "src/bongos/auth-admission.js",
|
|
10669
10694
|
"mode": "0000644",
|
|
10670
|
-
"sha256": "
|
|
10695
|
+
"sha256": "9fd9311df751a418985073db3aed97825e52d2a45ca7501b93f93cf5019c1fe6"
|
|
10671
10696
|
},
|
|
10672
10697
|
{
|
|
10673
10698
|
"path": "src/bongos/auth-config.js",
|
|
@@ -11002,7 +11027,7 @@
|
|
|
11002
11027
|
{
|
|
11003
11028
|
"path": "src/module-api.js",
|
|
11004
11029
|
"mode": "0000644",
|
|
11005
|
-
"sha256": "
|
|
11030
|
+
"sha256": "091a4bde841eaad09639727d4691f0c8a5555926e33cbffc49c7acc6b95870a6"
|
|
11006
11031
|
},
|
|
11007
11032
|
{
|
|
11008
11033
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11104,10 +11129,20 @@
|
|
|
11104
11129
|
"mode": "0000644",
|
|
11105
11130
|
"sha256": "f7434d510d656e5081f735d1f9356ecb11e8c3630a86a86137c3290134541c8d"
|
|
11106
11131
|
},
|
|
11132
|
+
{
|
|
11133
|
+
"path": "tests/activity_rollup_as_of.mjs",
|
|
11134
|
+
"mode": "0000644",
|
|
11135
|
+
"sha256": "d5903404f0fed48e599b68f259e0d79a44d11c249bb69bc4b59643e7dac62cbb"
|
|
11136
|
+
},
|
|
11137
|
+
{
|
|
11138
|
+
"path": "tests/activity_rollup_order_db.mjs",
|
|
11139
|
+
"mode": "0000644",
|
|
11140
|
+
"sha256": "497b453aa441ba06276d5146fc71f54da111a12c7c0c2e192da199cc1f2812ec"
|
|
11141
|
+
},
|
|
11107
11142
|
{
|
|
11108
11143
|
"path": "tests/activity_snapshots_db.mjs",
|
|
11109
11144
|
"mode": "0000644",
|
|
11110
|
-
"sha256": "
|
|
11145
|
+
"sha256": "73cb5a43e3f3ce61dd7f57720e523a310390ed60ed491d39fdaf8b8e9c78053a"
|
|
11111
11146
|
},
|
|
11112
11147
|
{
|
|
11113
11148
|
"path": "tests/admission_ceiling_gate.mjs",
|
|
@@ -13044,10 +13079,20 @@
|
|
|
13044
13079
|
"mode": "0000644",
|
|
13045
13080
|
"sha256": "8298006ae35010d726bcac8a2d351a5aa932d2ee6d5b53ed7b8a5c095ca5e54f"
|
|
13046
13081
|
},
|
|
13082
|
+
{
|
|
13083
|
+
"path": "tests/guild_totals.mjs",
|
|
13084
|
+
"mode": "0000644",
|
|
13085
|
+
"sha256": "c488d1231e2d7b9233e7b857cf584c4c6df9c596b34c7e4a74c8d976f38d1ada"
|
|
13086
|
+
},
|
|
13087
|
+
{
|
|
13088
|
+
"path": "tests/guild_totals_db.mjs",
|
|
13089
|
+
"mode": "0000644",
|
|
13090
|
+
"sha256": "095f66255fa4755ff57db9aef07a5334f12a2f1720a71e9523eeb5d28356029a"
|
|
13091
|
+
},
|
|
13047
13092
|
{
|
|
13048
13093
|
"path": "tests/guilds_api.mjs",
|
|
13049
13094
|
"mode": "0000644",
|
|
13050
|
-
"sha256": "
|
|
13095
|
+
"sha256": "2476ef4a332fa6d82d9d40ca798012c5aef40a74b08c5ca8d8d40e1975223cbb"
|
|
13051
13096
|
},
|
|
13052
13097
|
{
|
|
13053
13098
|
"path": "tests/guilds_db.mjs",
|
|
@@ -14347,7 +14392,7 @@
|
|
|
14347
14392
|
{
|
|
14348
14393
|
"path": "tests/profile_rollup_consent.mjs",
|
|
14349
14394
|
"mode": "0000644",
|
|
14350
|
-
"sha256": "
|
|
14395
|
+
"sha256": "093f078f965454e535d6026c1852a0e0668695809f270f84e5d26dba4025abbd"
|
|
14351
14396
|
},
|
|
14352
14397
|
{
|
|
14353
14398
|
"path": "tests/profile_route.mjs",
|
|
@@ -74,7 +74,7 @@ export interface GetGovernmentRanksResponse { ranks: unknown }
|
|
|
74
74
|
export interface GetGradesByBuilderResponse { window_days: unknown; builders: unknown }
|
|
75
75
|
export interface GetGuildsSlugMembersResponse { guild: unknown; you: unknown; members: unknown; count: unknown }
|
|
76
76
|
export interface GetGuildsSlugRequestsResponse { incoming: unknown; outgoing: unknown; incoming_count: unknown; outgoing_count: unknown }
|
|
77
|
-
export interface GetGuildsSlugResponse { guild: unknown; members: unknown; size: unknown }
|
|
77
|
+
export interface GetGuildsSlugResponse { guild: unknown; members: unknown; size: unknown; totals: unknown }
|
|
78
78
|
export interface GetHealthzResponse { ok: boolean; auth_configured: unknown }
|
|
79
79
|
export interface GetHelpRequestsArchiveResponse { asked_of_you: unknown; asked_of_others: unknown; count: unknown; crafts: unknown }
|
|
80
80
|
export interface GetHelpRequestsForMeResponse { help_requests: unknown; count: unknown; crafts: unknown }
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# ADR 0352 — A rollup report carries its "as of" in a header, and the hub keeps the newest
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-09-30
|
|
5
|
+
- **Task:** [task 1004268](https://cloudbongos.com/builders#/task/1004268) (goal 1000110), found in [task 1004244](https://cloudbongos.com/builders#/task/1004244)'s code review
|
|
6
|
+
- **Interacts with:** [ADR 0141](<redacted>.md) §4 (the activity rollup), [ADR 0171](0171-profile-rollup-join-and-consent.md) D7 (a report is third-party self-report), [ADR 0083](0083-modular-architecture-strangler-build-plan.md) §5 (module-namespaced migrations)
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
`POST /sso/activity/rollup` upserted the live activity row last-writer-wins, and a report carried no timestamp. An instance pushes one on every federated sign-in and one on every ship, so a sign-in report read earlier and a ship report read later can reach the hub together, and the older one can commit last. The live row and, since task 1004244, the newest snapshot then show the older totals. The leaderboard reads a regression that never happened until the builder's next report. It is low-urgency, since the next report corrects it, but it is wrong in exactly the place people compare numbers.
|
|
11
|
+
|
|
12
|
+
The fix the task names is for the instance to send when it read the totals, and for the hub to refuse an older report. What is not obvious is where that stamp travels.
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
**D1 — The stamp is a request header, `Bongos-Report-As-Of`, not a body field.** The hub validates the rollup body strictly (`validateOrRespond`), so an unknown field is a 400. The hub and its instances upgrade independently, and an instance can run a newer core than the hub (today the hub's pin trails main). A body field sent by an upgraded instance would be refused by a hub that predates it. The sender never reads the response, so every report from that instance would be lost silently until the hub caught up. An unknown header is ignored, so the header is safe in both directions: an old hub ignores it, and a new hub treats its absence as "unstamped".
|
|
17
|
+
|
|
18
|
+
**D2 — Unstamped means the old behaviour, never a refusal.** A missing or unparseable header binds NULL. A NULL on either side of the comparison (an older instance's report, or a row written before migration 026) applies last-writer-wins, as before. An unstamped report also clears the stored stamp, so a stamp never outlives the numbers it described. A malformed header is not a 400, for D1's reason: a refusal drops the report, when the only thing lost should be its ordering.
|
|
19
|
+
|
|
20
|
+
**D3 — The guard is a WHERE on the upsert's conflict branch.** `ON CONFLICT (client_id, github_id) DO UPDATE … WHERE EXCLUDED.reported_as_of IS NULL OR <row>.reported_as_of IS NULL OR EXCLUDED.reported_as_of >= <row>.reported_as_of`. A skipped update still takes the row lock, so racing reports stay queued exactly as task 1004244 arranged, and the snapshot beside it copies an unchanged live row, which records nothing. Ties apply (the later arrival wins, as before).
|
|
21
|
+
|
|
22
|
+
**D4 — The stamp is the instance's clock, compared only with itself, and clamped to the hub's.** A row's stored stamp only ever comes from the same client for the same builder, so skew between an instance and the hub does not matter. A stamp later than the hub's `now()` is stored as `now()`, so an instance whose clock runs ahead cannot freeze the row until its clock is reached. The sender stamps before its first read, so a report is never ordered ahead of totals read after it.
|
|
23
|
+
|
|
24
|
+
## Consequences
|
|
25
|
+
|
|
26
|
+
- Migration `platform_identity_026_activity_reported_as_of.sql` adds one nullable column, with no backfill and no default. An invented stamp on an existing row could refuse an honest report from an instance whose clock reads earlier than the hub's.
|
|
27
|
+
- A mixed fleet behaves as before until each instance upgrades. An upgraded instance talking to an old hub is also unaffected.
|
|
28
|
+
- **Honest limit:** an instance clock that steps backwards (an NTP correction after running fast) has its reports refused until it passes the last stamp it sent. The skew bounds this, the clamp bounds the skew by the hub's own time, and the next report after that applies. A report refused this way still answers `{ ok: true }`, because the instance ignores the body.
|
|
29
|
+
- **The pattern generalises:** a field added to a hub↔instance wire that the older side validates strictly must be able to ride where the older side ignores it, or the change must land on the hub first and be released before any instance sends it.
|
|
30
|
+
|
|
31
|
+
Proof: `tests/activity_rollup_order_db.mjs` (real Postgres: the out-of-order race, ties, NULLs on each side, the future clamp, and ten rounds of five racing writers ending on the latest stamp with the snapshot agreeing) and `tests/activity_rollup_as_of.mjs` (DB-free: the sender stamps a header before its reads and never the body; the route binds it and degrades a bad one to unstamped).
|
|
32
|
+
|
|
33
|
+
## Alternatives considered
|
|
34
|
+
|
|
35
|
+
- **A body field `as_of`.** This is what the task sketched. It is lost against any older hub (D1).
|
|
36
|
+
- **`GREATEST` on each total.** Credits and karma can legitimately fall, so this would pin a clawback forever.
|
|
37
|
+
- **The standard `Date` header.** It has one-second granularity, and a sign-in push and a ship push inside the same second are exactly the case being fixed.
|
|
38
|
+
- **A 400 on a malformed stamp.** It turns a sender bug into silent data loss (D2).
|
package/docs/adr/README.md
CHANGED
|
@@ -470,3 +470,4 @@ These 20 numbers are each shared by exactly two files. They are **accepted histo
|
|
|
470
470
|
| 0349 | [**A version preview is a sandboxed child the web tier launches, reached by a cookie** ([task 1004300](https://cloudbongos.com/builders#/task/1004300), goal 1000090 — BONGOS-V2, owner Lars, approved 2026-09-29). Amends ADR 0293 D1 for this feature only. **D1 (owner):** route by a `bongos_preview` cookie through an optional `request.divert` seam, not a path; forward only with the cookie, off the sign-in and preview paths, with no Bearer, and with `core.pin.move` re-checked on every request. **D2 (owner):** the npm-release module launches the preview itself, with a deny-by-default environment, a dump-and-restore copy of the database (never a template copy), one at a time, 20-minute idle stop, published versions within 30 of the running one. **D3:** the same database role and no egress firewall are accepted for now.](0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md) | modules / npm-release / deploy |
|
|
471
471
|
| 0350 | [**The hub holds a per-project WRITE deploy key, so a hosted project's upgraded pin always reaches its GitHub repo** ([task 1004291](https://cloudbongos.com/builders#/task/1004291), follow-up to task 1004065). Amends ADR 0176. The owner's token lasts one hour and the checkout's own key is read-only, so most hosted upgrades ended "GitHub was not updated". **D1:** a second deploy key per repo, registered `read_only: false` as `cloudbongos-pin-<slug>`, the pull key untouched. **D2:** kept outside every checkout in the runner's own directory (0700 dir, 0600 key, modes verified), unreadable by the project's account. **D3:** minted at standup and whenever a fresh owner token finds none, so reconnecting GitHub once is enough. **D4:** `pushUpgradePin` tries write key, then owner token, then origin, never forced. **D5:** revoked on teardown and disconnect. **D6:** a project on the shared app user is refused a key: "needs its own account first". **D7:** /deploy says reconnecting sets up lasting access.](0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md) | provisioning / security |
|
|
472
472
|
| 0351 | [**A criterion closes on a UAT, not on its linked tasks shipping** ([task 1004392](https://cloudbongos.com/builders#/task/1004392), goal 1000089 — working area 3). Supersedes ADR 0183's close rule. `wa7-government` had closed on six Board Room navigation tasks while nothing it describes was on the live site. **D1:** three checks — Code (linked tasks done, ≥1 shipped; automatic), Live, UAT — and the sweep closes a criterion only with a CURRENT sign-off (no older than the newest linked ship). **D2 (owner):** "Awaiting UAT" is a visible state. **D3 (owner):** one step; the signer is refused if they shipped linked work, the project owner excepted. **D4 (owner):** an mp4/webm up to 100 MB on the project's own server. **D5 (owner):** backend-only is set while a criterion is open and takes a recording-free sign-off. **D6 (owner):** Live is read via the `deploy.taskWhere` port where the site can tell, attested otherwise. **D7 (owner):** already-closed criteria stay closed ("Closed before UAT"). **D8:** `/satisfy` is a recorded override that needs a reason. **D9:** `/goal-uat` + `scripts/gds/uat.js`; the hall is task 1004402.](0351-a-criterion-closes-on-a-uat.md) | lifecycle / criteria / review |
|
|
473
|
+
| 0352 | [**A rollup report carries its "as of" in a header, and the hub keeps the newest** ([task 1004268](https://cloudbongos.com/builders#/task/1004268), goal 1000110). `POST /sso/activity/rollup` was last-writer-wins with no report timestamp, so a sign-in push read earlier and a ship push read later could land the older totals last. **D1:** the instance's read time rides a `Bongos-Report-As-Of` HEADER, not a body field, because the hub validates the body strictly and hub and instances upgrade independently: a body field from a newer instance would 400 on an older hub and lose the report, while an unknown header is ignored. **D2:** missing or unparseable = unstamped = last-writer-wins, never a 400. **D3:** the guard is a WHERE on the upsert's conflict branch (migration 026, `reported_as_of`, no backfill); a skipped update keeps the row lock, so the task 1004244 snapshot stays in step. **D4:** the stamp is compared only with the same client's, and clamped to the hub's `now()`.](0352-a-rollup-report-carries-its-as-of-in-a-header.md) | platform identity / federation |
|
package/docs/api/openapi.json
CHANGED
|
@@ -8645,7 +8645,7 @@
|
|
|
8645
8645
|
"guilds"
|
|
8646
8646
|
],
|
|
8647
8647
|
"summary": "GET /guilds/:slug",
|
|
8648
|
-
"description": "a public guild's page data. The roster is the members who agreed to be shown AND whose own accounts are publicly visible (ADR 0336 D4); size is that roster's length. no-store: a hide or a visibility flip must beat every cache (ADR 0171 D5).\n\n**Rank:** `public` — No authentication — any caller.",
|
|
8648
|
+
"description": "a public guild's page data. The roster is the members who agreed to be shown AND whose own accounts are publicly visible (ADR 0336 D4); size is that roster's length; totals are the weekly band floors (ADR 0337 D4.4), null before the first refresh. no-store: a hide or a visibility flip must beat every cache (ADR 0171 D5).\n\n**Rank:** `public` — No authentication — any caller.",
|
|
8649
8649
|
"x-rank": "public",
|
|
8650
8650
|
"x-source": "modules/platform-identity/routes/guilds-public.js",
|
|
8651
8651
|
"parameters": [
|
|
@@ -18502,7 +18502,7 @@
|
|
|
18502
18502
|
"sso"
|
|
18503
18503
|
],
|
|
18504
18504
|
"summary": "POST /sso/activity/rollup",
|
|
18505
|
-
"description": "POST /sso/activity/rollup — a federated instance reports a builder's activity snapshot (ADR 0141 §4). Server-to-server, client_id+client_secret auth. Keyed to the AUTHENTICATED client_id. Descriptive analytics for the Archon-only scouting view — never authority. rank: public — authenticated by client_id + client_secret, not a builder rank.\n\n**Rank:** `public` — No authentication — any caller.",
|
|
18505
|
+
"description": "POST /sso/activity/rollup — a federated instance reports a builder's activity snapshot (ADR 0141 §4). Server-to-server, client_id+client_secret auth. Keyed to the AUTHENTICATED client_id. Descriptive analytics for the Archon-only scouting view — never authority. Optional `Bongos-Report-As-Of` header (task 1004268): when the instance read these totals, so an older report landing last cannot overwrite a newer one. A HEADER, not a body field, because the body is validated strictly and the hub and its instances upgrade independently: a new instance's body field would 400 on an older hub and the report would be lost, while an unknown header is ignored. Absent or unparseable reads as unstamped (last-writer-wins, as before) rather than a 400 — the sender never reads the response, so a refusal would silently drop the report instead of only its ordering. rank: public — authenticated by client_id + client_secret, not a builder rank.\n\n**Rank:** `public` — No authentication — any caller.",
|
|
18506
18506
|
"x-rank": "public",
|
|
18507
18507
|
"x-source": "modules/platform-identity/routes/sso.js",
|
|
18508
18508
|
"requestBody": {
|
|
@@ -22586,12 +22586,14 @@
|
|
|
22586
22586
|
"properties": {
|
|
22587
22587
|
"guild": {},
|
|
22588
22588
|
"members": {},
|
|
22589
|
-
"size": {}
|
|
22589
|
+
"size": {},
|
|
22590
|
+
"totals": {}
|
|
22590
22591
|
},
|
|
22591
22592
|
"required": [
|
|
22592
22593
|
"guild",
|
|
22593
22594
|
"members",
|
|
22594
|
-
"size"
|
|
22595
|
+
"size",
|
|
22596
|
+
"totals"
|
|
22595
22597
|
]
|
|
22596
22598
|
},
|
|
22597
22599
|
"GetHealthzResponse": {
|
|
@@ -2693,5 +2693,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2693
2693
|
landed since 1.20.16 with no explicit bump. run 36722139698. (task 1002620)
|
|
2694
2694
|
1.20.18 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2695
2695
|
landed since 1.20.17 with no explicit bump. run 36724081934. (task 1002620)
|
|
2696
|
+
1.20.19 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2697
|
+
landed since 1.20.18 with no explicit bump. run 36727774141. (task 1002620)
|
|
2698
|
+
1.20.20 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2699
|
+
landed since 1.20.19 with no explicit bump. run 36732445320. (task 1002620)
|
|
2696
2700
|
---------------------------------------------------------------------------
|
|
2697
2701
|
```
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// modules/platform-identity/guild-totals.js — a guild's collective record: the
|
|
2
|
+
// rounded totals its public page publishes (task 1002302, R26; ADR 0337 D4.4,
|
|
3
|
+
// which supersedes ADR 0336 D5's read-time sum).
|
|
4
|
+
//
|
|
5
|
+
// THE RULE, next to the data so no reader can forget a half of it:
|
|
6
|
+
//
|
|
7
|
+
// * EVERY MEMBER COUNTS UNLESS THEY SWITCHED IT OFF. counted_in_totals is the
|
|
8
|
+
// per-membership switch R24 stores (task 1002300); the notice on the join
|
|
9
|
+
// cards is the consent. An account that has not consented at all (not
|
|
10
|
+
// accountActiveSql) is never counted, whatever its switch says.
|
|
11
|
+
// * A PUBLIC MEMBER is one whose numbers are already public on the guild's page:
|
|
12
|
+
// shown_publicly AND rollupVisibleSql. Everyone else counted is NON-PUBLIC — a
|
|
13
|
+
// private account, hidden numbers, or not agreeing to be shown.
|
|
14
|
+
// * A MINIMUM OF 3. Non-public members' numbers enter only while the guild has
|
|
15
|
+
// at least NON_PUBLIC_MIN_COUNTED counted non-public members. Below that the
|
|
16
|
+
// totals add up the public members alone.
|
|
17
|
+
// * ROUNDED, NEVER EXACT. Each total is published as its band floor on the
|
|
18
|
+
// 1-2-5 scale ("100+"), and only the band floor is ever stored. The exact sum
|
|
19
|
+
// lives in memory for as long as it takes to round it.
|
|
20
|
+
// * WEEKLY, NEVER ON A READ. A poller (pollers/guild-totals.js) refreshes a row
|
|
21
|
+
// at most once every TOTALS_REFRESH_DAYS, and the upsert re-checks that age in
|
|
22
|
+
// its own statement. readGuildTotals only SELECTs, so no read, however many,
|
|
23
|
+
// can move a number an observer is watching.
|
|
24
|
+
// * PROJECTS ARE THE PUBLIC MEMBERS' NAMED PUBLIC PROJECTS, never a non-public
|
|
25
|
+
// member's (ADR 0336 D5's union rule, which D4.4 keeps). Even a hidden
|
|
26
|
+
// member's public projects are withheld on their own profile, so they stay
|
|
27
|
+
// out of the count.
|
|
28
|
+
//
|
|
29
|
+
// WHAT IS DELIBERATELY ABSENT. No count of counted members, and nothing that says
|
|
30
|
+
// whether non-public numbers are in the total: either would reveal how many
|
|
31
|
+
// non-public members a guild has, which its public size (the roster's length,
|
|
32
|
+
// ADR 0336 D4) is built never to show.
|
|
33
|
+
//
|
|
34
|
+
// THE HONEST LIMIT, accepted by the owner (ADR 0337 D4.4, 2026-09-25). This stops
|
|
35
|
+
// a casual reader, not a determined guild owner: padding the guild with two
|
|
36
|
+
// accounts whose numbers the owner already knows makes a real non-public member
|
|
37
|
+
// the third, and comparing totals across refreshes then yields a range for that
|
|
38
|
+
// member. The notice and the per-guild switch answer it, not this file.
|
|
39
|
+
// tests/guild_totals_db.mjs documents the padding case; it does not "fix" it.
|
|
40
|
+
//
|
|
41
|
+
// NOTHING HERE IS AUTHORITY (ADR 0016 / ADR 0141). Descriptive only.
|
|
42
|
+
//
|
|
43
|
+
// Reaches core only through the doorway (ADR 0083).
|
|
44
|
+
'use strict';
|
|
45
|
+
|
|
46
|
+
const api = require('../../src/module-api');
|
|
47
|
+
const { accountActiveSql, rollupVisibleSql } = require('./account-visibility');
|
|
48
|
+
const { onMapSql } = require('./projects-feed');
|
|
49
|
+
|
|
50
|
+
const { pool: defaultPool } = api;
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// The constants ADR 0337 D4.4 names. Exported so a route, a test and a doc read
|
|
54
|
+
// the same number.
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
// The 1-2-5 band scale: 10, 20, 50, 100, 200, 500, ... Below the first band a
|
|
58
|
+
// total publishes as 0, which a page renders as "under 10".
|
|
59
|
+
const TOTALS_BAND_MANTISSAS = [1, 2, 5];
|
|
60
|
+
const TOTALS_FIRST_BAND = 10;
|
|
61
|
+
|
|
62
|
+
// k: non-public members' numbers enter only while at least this many of them
|
|
63
|
+
// are counted.
|
|
64
|
+
const NON_PUBLIC_MIN_COUNTED = 3;
|
|
65
|
+
|
|
66
|
+
// A row is refreshed at most this often. Each before/after comparison costs an
|
|
67
|
+
// observer this long.
|
|
68
|
+
const TOTALS_REFRESH_DAYS = 7;
|
|
69
|
+
|
|
70
|
+
// The columns are integer; a band floor never exceeds what they can hold.
|
|
71
|
+
const INT4_MAX = 2147483647;
|
|
72
|
+
|
|
73
|
+
// bandFloor(n) → the largest band on the scale that is <= n, or 0 below the
|
|
74
|
+
// first band (and for a negative or non-numeric sum).
|
|
75
|
+
function bandFloor(value) {
|
|
76
|
+
const raw = Math.floor(Number(value));
|
|
77
|
+
if (!Number.isFinite(raw) || raw < TOTALS_FIRST_BAND) return 0;
|
|
78
|
+
const n = Math.min(raw, INT4_MAX);
|
|
79
|
+
let decade = TOTALS_FIRST_BAND;
|
|
80
|
+
while (decade * 10 <= n) decade *= 10;
|
|
81
|
+
let floor = decade;
|
|
82
|
+
for (const m of TOTALS_BAND_MANTISSAS) if (m * decade <= n) floor = m * decade;
|
|
83
|
+
return floor;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// The computation
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
// The exact sums for a batch of guilds, one row per id asked for (a guild with
|
|
91
|
+
// nobody counted sums to zero). Internal: its output is never stored or sent —
|
|
92
|
+
// only its band floors are.
|
|
93
|
+
async function computeGuildSums(guildIds, { pool = defaultPool } = {}) {
|
|
94
|
+
if (!guildIds.length) return [];
|
|
95
|
+
const { rows } = await pool.query(
|
|
96
|
+
`WITH m AS (
|
|
97
|
+
SELECT gm.guild_id, gm.github_id,
|
|
98
|
+
(gm.shown_publicly AND ${rollupVisibleSql('pa')}) AS is_public
|
|
99
|
+
FROM platform_identity_guild_members gm
|
|
100
|
+
JOIN platform_identity_accounts pa ON pa.github_id = gm.github_id
|
|
101
|
+
WHERE gm.guild_id = ANY($1::bigint[])
|
|
102
|
+
AND gm.counted_in_totals
|
|
103
|
+
AND ${accountActiveSql('pa')}
|
|
104
|
+
),
|
|
105
|
+
k AS (
|
|
106
|
+
SELECT guild_id, count(*) FILTER (WHERE NOT is_public) AS non_public
|
|
107
|
+
FROM m GROUP BY guild_id
|
|
108
|
+
),
|
|
109
|
+
counted AS (
|
|
110
|
+
SELECT m.guild_id, m.github_id, m.is_public
|
|
111
|
+
FROM m JOIN k ON k.guild_id = m.guild_id
|
|
112
|
+
WHERE m.is_public OR k.non_public >= $2
|
|
113
|
+
),
|
|
114
|
+
sums AS (
|
|
115
|
+
SELECT c.guild_id,
|
|
116
|
+
COALESCE(sum(a.credits), 0)::bigint AS credits,
|
|
117
|
+
COALESCE(sum(a.tasks_shipped), 0)::bigint AS works_shipped,
|
|
118
|
+
COALESCE(sum(a.karma), 0)::bigint AS karma
|
|
119
|
+
FROM counted c
|
|
120
|
+
LEFT JOIN platform_identity_builder_project_activity a ON a.github_id = c.github_id
|
|
121
|
+
GROUP BY c.guild_id
|
|
122
|
+
),
|
|
123
|
+
projs AS (
|
|
124
|
+
SELECT c.guild_id, count(DISTINCT p.id)::bigint AS projects
|
|
125
|
+
FROM counted c
|
|
126
|
+
JOIN platform_identity_builder_project_activity a ON a.github_id = c.github_id
|
|
127
|
+
JOIN platform_identity_sso_clients sc ON sc.client_id = a.client_id
|
|
128
|
+
JOIN platform_identity_projects p ON p.origin = sc.origin
|
|
129
|
+
WHERE c.is_public AND ${onMapSql('p')} AND p.visibility = 'public'
|
|
130
|
+
GROUP BY c.guild_id
|
|
131
|
+
)
|
|
132
|
+
SELECT g.id::text AS guild_id,
|
|
133
|
+
COALESCE(s.credits, 0)::text AS credits,
|
|
134
|
+
COALESCE(s.works_shipped, 0)::text AS works_shipped,
|
|
135
|
+
COALESCE(s.karma, 0)::text AS karma,
|
|
136
|
+
COALESCE(pr.projects, 0)::text AS projects
|
|
137
|
+
FROM unnest($1::bigint[]) AS g(id)
|
|
138
|
+
LEFT JOIN sums s ON s.guild_id = g.id
|
|
139
|
+
LEFT JOIN projs pr ON pr.guild_id = g.id`,
|
|
140
|
+
[guildIds, NON_PUBLIC_MIN_COUNTED],
|
|
141
|
+
);
|
|
142
|
+
return rows;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Refresh every guild whose record is missing or at least TOTALS_REFRESH_DAYS
|
|
146
|
+
// old, stalest first, at most `limit` per call. The upsert re-asserts the age in
|
|
147
|
+
// its own WHERE, so a racing sweep cannot refresh a guild twice in a week, and
|
|
148
|
+
// it re-checks each guild still exists so one dissolved mid-sweep is skipped
|
|
149
|
+
// rather than failing the batch. Returns { scanned, refreshed }.
|
|
150
|
+
async function refreshStaleGuildTotals({ limit = 200 } = {}, { pool = defaultPool } = {}) {
|
|
151
|
+
const { rows: stale } = await pool.query(
|
|
152
|
+
`SELECT g.id::text AS id
|
|
153
|
+
FROM platform_identity_guilds g
|
|
154
|
+
LEFT JOIN platform_identity_guild_totals t ON t.guild_id = g.id
|
|
155
|
+
WHERE t.guild_id IS NULL OR t.computed_at <= now() - make_interval(days => $1::int)
|
|
156
|
+
ORDER BY t.computed_at ASC NULLS FIRST, g.id
|
|
157
|
+
LIMIT $2`,
|
|
158
|
+
[TOTALS_REFRESH_DAYS, Math.max(1, Number(limit) || 1)],
|
|
159
|
+
);
|
|
160
|
+
if (!stale.length) return { scanned: 0, refreshed: 0 };
|
|
161
|
+
const sums = await computeGuildSums(stale.map((r) => r.id), { pool });
|
|
162
|
+
const col = (k) => sums.map((r) => bandFloor(r[k]));
|
|
163
|
+
const { rowCount } = await pool.query(
|
|
164
|
+
`INSERT INTO platform_identity_guild_totals AS t
|
|
165
|
+
(guild_id, credits, works_shipped, karma, projects, computed_at)
|
|
166
|
+
SELECT u.guild_id, u.credits, u.works_shipped, u.karma, u.projects, now()
|
|
167
|
+
FROM unnest($1::bigint[], $2::int[], $3::int[], $4::int[], $5::int[])
|
|
168
|
+
AS u(guild_id, credits, works_shipped, karma, projects)
|
|
169
|
+
WHERE EXISTS (SELECT 1 FROM platform_identity_guilds g WHERE g.id = u.guild_id)
|
|
170
|
+
ON CONFLICT (guild_id) DO UPDATE
|
|
171
|
+
SET credits = EXCLUDED.credits,
|
|
172
|
+
works_shipped = EXCLUDED.works_shipped,
|
|
173
|
+
karma = EXCLUDED.karma,
|
|
174
|
+
projects = EXCLUDED.projects,
|
|
175
|
+
computed_at = EXCLUDED.computed_at
|
|
176
|
+
WHERE t.computed_at <= now() - make_interval(days => $6::int)`,
|
|
177
|
+
[sums.map((r) => r.guild_id), col('credits'), col('works_shipped'), col('karma'), col('projects'),
|
|
178
|
+
TOTALS_REFRESH_DAYS],
|
|
179
|
+
);
|
|
180
|
+
return { scanned: stale.length, refreshed: rowCount };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// The read
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
// A guild's published record, or null before its first refresh. A SELECT and
|
|
188
|
+
// nothing else: the record moves only on the poller's clock, never on a read.
|
|
189
|
+
// The caller decides whether the guild may be read at all (getPublicGuild).
|
|
190
|
+
async function readGuildTotals(guildId, { pool = defaultPool } = {}) {
|
|
191
|
+
const { rows } = await pool.query(
|
|
192
|
+
`SELECT credits, works_shipped, karma, projects, computed_at
|
|
193
|
+
FROM platform_identity_guild_totals
|
|
194
|
+
WHERE guild_id = $1`,
|
|
195
|
+
[guildId],
|
|
196
|
+
);
|
|
197
|
+
const r = rows[0];
|
|
198
|
+
if (!r) return null;
|
|
199
|
+
return {
|
|
200
|
+
credits: r.credits,
|
|
201
|
+
works_shipped: r.works_shipped,
|
|
202
|
+
karma: r.karma,
|
|
203
|
+
projects: r.projects,
|
|
204
|
+
as_of: r.computed_at,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = {
|
|
209
|
+
TOTALS_BAND_MANTISSAS,
|
|
210
|
+
TOTALS_FIRST_BAND,
|
|
211
|
+
NON_PUBLIC_MIN_COUNTED,
|
|
212
|
+
TOTALS_REFRESH_DAYS,
|
|
213
|
+
bandFloor,
|
|
214
|
+
computeGuildSums,
|
|
215
|
+
refreshStaleGuildTotals,
|
|
216
|
+
readGuildTotals,
|
|
217
|
+
};
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// be seen by the guild, never by the world). Every count is the length of a
|
|
27
27
|
// list the reader may see.
|
|
28
28
|
// * counted_in_totals (ADR 0337 D4.4) is stored and switchable here; the
|
|
29
|
-
// totals that read it
|
|
29
|
+
// totals that read it live in guild-totals.js (task 1002302).
|
|
30
30
|
//
|
|
31
31
|
// NOTHING HERE IS AUTHORITY (ADR 0016 / ADR 0141). A guild grants no rank,
|
|
32
32
|
// credits, admission or access to any project.
|
|
@@ -40,6 +40,7 @@ const {
|
|
|
40
40
|
} = require('./account-visibility');
|
|
41
41
|
const conn = require('./connections');
|
|
42
42
|
const { normalizeHandle } = require('./platform-identity');
|
|
43
|
+
const { readGuildTotals } = require('./guild-totals');
|
|
43
44
|
|
|
44
45
|
const { pool: defaultPool } = api;
|
|
45
46
|
|
|
@@ -654,6 +655,8 @@ async function listGuildRequests({ guildId, ownerGithubId }, { pool = defaultPoo
|
|
|
654
655
|
// vary by one. The roster is shown_publicly AND accountVisibleSql, ordered only
|
|
655
656
|
// by what it publishes (ADR 0255), capped at LIST_MAX; `size` is the length of
|
|
656
657
|
// the list returned, never a total beside it (D4: there is no "+N more").
|
|
658
|
+
// `totals` is the weekly rounded record (ADR 0337 D4.4), or null before the
|
|
659
|
+
// first refresh — read, never computed, here.
|
|
657
660
|
async function getPublicGuild(slug, { pool = defaultPool } = {}) {
|
|
658
661
|
const g = await pool.query(
|
|
659
662
|
`SELECT id, slug, name, description, created_at
|
|
@@ -672,8 +675,9 @@ async function getPublicGuild(slug, { pool = defaultPool } = {}) {
|
|
|
672
675
|
LIMIT $2`,
|
|
673
676
|
[guild.id, LIST_MAX],
|
|
674
677
|
);
|
|
678
|
+
const totals = await readGuildTotals(guild.id, { pool });
|
|
675
679
|
const { id, ...shown } = guild;
|
|
676
|
-
return { guild: shown, members: rows, size: rows.length };
|
|
680
|
+
return { guild: shown, members: rows, size: rows.length, totals };
|
|
677
681
|
}
|
|
678
682
|
|
|
679
683
|
module.exports = {
|