@bongos/core 1.24.64 → 1.24.65

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/bongos/package-core.js",
5
- "core_version": "1.24.64",
6
- "core_contract": "1.24.64",
7
- "source_commit": "3ec2f53ca824f8f5ae9954733449d50e6d771d78",
5
+ "core_version": "1.24.65",
6
+ "core_contract": "1.24.65",
7
+ "source_commit": "8f67a76402acd252e7d9997710389225189f1ff7",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-11-08T11:46:13.511Z",
9
+ "built_at": "2026-11-08T19:44:15.057Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 638,
13
13
  "agent_docs_stubbed": 32,
14
- "functional_verbatim": 3902,
14
+ "functional_verbatim": 3904,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 4573,
20
- "tree_sha256": "05be327dd13c886de8623f745d2aafc5a44e53b1474621c1d66528fb88bbc306",
19
+ "file_count": 4575,
20
+ "tree_sha256": "de673fb6edbef391285acd5a1ba81d227ff869398df36e3c4d06940dddae3ac8",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -3007,7 +3007,7 @@
3007
3007
  {
3008
3008
  "path": "docs/module-api-changelog.md",
3009
3009
  "mode": "0000644",
3010
- "sha256": "6f54bb50baf24667d580a0a1800893b4f274ec757b1766230ab2d99f80cf2e21"
3010
+ "sha256": "a661073ecdafcaa6ead7089b09758c4e3072ed1fba467670904b5ce73de426c9"
3011
3011
  },
3012
3012
  {
3013
3013
  "path": "docs/modules-contract.md",
@@ -11902,12 +11902,12 @@
11902
11902
  {
11903
11903
  "path": "package-lock.json",
11904
11904
  "mode": "0000644",
11905
- "sha256": "ebf93f1762d8c434f4be706a146234bc6047b37e2c1ebcca6c961da817a57b24"
11905
+ "sha256": "b7ffb5c80ef9e08ddbe54c1e125fdbd2e92c2b864c1b2d320fcf83ba964c9cc2"
11906
11906
  },
11907
11907
  {
11908
11908
  "path": "package.json",
11909
11909
  "mode": "0000644",
11910
- "sha256": "f878eabd1e4556e46155b760462e8f433a1472ecc771fed5638011a401844af6"
11910
+ "sha256": "482f515d87fb3ffc09c6971a26c5a7519551f6e0bd1816d0afd6c6cfb3e57448"
11911
11911
  },
11912
11912
  {
11913
11913
  "path": "public-docs/index.html",
@@ -11927,7 +11927,7 @@
11927
11927
  {
11928
11928
  "path": "release-notes.json",
11929
11929
  "mode": "0000644",
11930
- "sha256": "c56e5afc1883d40326c974209ebfe30d603375a991532ae4775c55f1cda428df"
11930
+ "sha256": "be73c388b331c7077a4564866c27f60912c513843ffebc4c94c31a19aeee954b"
11931
11931
  },
11932
11932
  {
11933
11933
  "path": "scripts/bongos/CLAUDE.md",
@@ -13177,7 +13177,7 @@
13177
13177
  {
13178
13178
  "path": "scripts/bongos/run-unit-tests.js",
13179
13179
  "mode": "0000644",
13180
- "sha256": "be2eb571965101b309308d73d2bea556ea7a9ff9550ec24542df61b904646d60"
13180
+ "sha256": "85fc4fb8e3940c71b4f2eac604374b583bca16e6e590847d597cf9e59fbb44f7"
13181
13181
  },
13182
13182
  {
13183
13183
  "path": "scripts/bongos/runner-drift.js",
@@ -15877,7 +15877,7 @@
15877
15877
  {
15878
15878
  "path": "src/module-api.js",
15879
15879
  "mode": "0000644",
15880
- "sha256": "6937e1a1767458c2da6342b38b7631c63d87752688584ca9b5fa59cd94b9a026"
15880
+ "sha256": "1f78ed60de37fa15b7f57e5c19acfc0fb9bcab021e6cbd3a2ecc2044f9d76518"
15881
15881
  },
15882
15882
  {
15883
15883
  "path": "src/module-loader/catalog.js",
@@ -16974,6 +16974,11 @@
16974
16974
  "mode": "0000644",
16975
16975
  "sha256": "386bea21404a5921a450adf251bb9cb54694684941a5b3d3b000a266cc9d09f7"
16976
16976
  },
16977
+ {
16978
+ "path": "tests/community_proof_db.mjs",
16979
+ "mode": "0000644",
16980
+ "sha256": "a68d8274db242141006e661f3a5879efd769d1facc25fc5e2c362abe80a4084e"
16981
+ },
16977
16982
  {
16978
16983
  "path": "tests/community_search_route.mjs",
16979
16984
  "mode": "0000644",
@@ -17769,6 +17774,11 @@
17769
17774
  "mode": "0000644",
17770
17775
  "sha256": "a86b984c420c260c5298b1fd15c48533a27a4ead3ad63543bdfa63ecd6b37949"
17771
17776
  },
17777
+ {
17778
+ "path": "tests/fixtures/privacy-matrix.mjs",
17779
+ "mode": "0000644",
17780
+ "sha256": "a540792ffd23206b4e37b9fca7b8498e55de853211f4714cad791af28029af9c"
17781
+ },
17772
17782
  {
17773
17783
  "path": "tests/fixtures/seed-tasks/bongos-v1-idea-routing.json",
17774
17784
  "mode": "0000644",
@@ -20697,7 +20707,7 @@
20697
20707
  {
20698
20708
  "path": "tests/privacy_matrix.mjs",
20699
20709
  "mode": "0000644",
20700
- "sha256": "a2592ce72e24d6bb5acee360594ae48b3b6d23ad93467953b4745d3eb552cfd9"
20710
+ "sha256": "7a9aea3dd6db1b8048b35cf64624553fa148a9cd03030fa336eb9453ba5068bb"
20701
20711
  },
20702
20712
  {
20703
20713
  "path": "tests/probe_cache.mjs",
@@ -3382,5 +3382,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3382
3382
  landed since 1.24.62 with no explicit bump. run 42392141827. (task 1002620)
3383
3383
  1.24.64 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3384
3384
  landed since 1.24.63 with no explicit bump. run 42477114353. (task 1002620)
3385
+ 1.24.65 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3386
+ landed since 1.24.64 with no explicit bump. run 42516347052. (task 1002620)
3385
3387
  ---------------------------------------------------------------------------
3386
3388
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.64",
3
+ "version": "1.24.65",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.64",
9
+ "version": "1.24.65",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.64",
3
+ "version": "1.24.65",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -11359,5 +11359,11 @@
11359
11359
  "id": "1005177",
11360
11360
  "text": "The next version is scoped. The builders' hall becomes a game you are inside rather than a website you navigate: three characters to play as, a city that grows as the project grows, and work claimed as quest chains instead of"
11361
11361
  }
11362
+ ],
11363
+ "1.24.65": [
11364
+ {
11365
+ "id": "1002985",
11366
+ "text": "The community page now has one test proving, on a real database, that hidden, private and new accounts never show on any leaderboard, search, crown or mover card, and that every week, month and year total is exact."
11367
+ }
11362
11368
  ]
11363
11369
  }
@@ -88,6 +88,10 @@ const INTEGRATION = new Set([
88
88
  // year's baseline over 30 days, a quiet row's live total, hidden and private left out.
89
89
  // The DB-free sibling is community_movers.
90
90
  'community_movers_db',
91
+ // task 1002985: the community proof — one fixture per privacy-matrix row and one snapshot
92
+ // history through every community route on real SQL. Its DB-free sibling is the
93
+ // privacy-matrix proof, which must stay unnamed here (it asserts that it is).
94
+ 'community_proof_db',
91
95
  // task 1004936: the Market Watch tables, hub seed and read queries against real SQL. The
92
96
  // CHECKs, partial unique indexes and ON CONFLICT targets are what make the seed
93
97
  // idempotent and a resumed run unable to record an item twice. DB-free sibling:
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.24.64'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.65'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,334 @@
1
+ // tests/community_proof_db.mjs — PROOF: the privacy matrix and the window math on every
2
+ // community surface, against real Postgres (task 1002985, goal 1000110 CT9;
3
+ // docs/specs/bongos-v1-community-tab.md D3, D4, D5, D7 and D8).
4
+ //
5
+ // WHAT THIS ADDS. tests/privacy_matrix.mjs proves every cell DB-free, through an
6
+ // interpreter that reads each statement's WHERE text, and each surface's own _db test
7
+ // proves its arithmetic on a fixture of its own. Neither runs ONE fixture through EVERY
8
+ // surface on real SQL. This does: one account per D2 row with the cells of
9
+ // tests/fixtures/privacy-matrix.mjs, one shared snapshot history, and the real routers
10
+ // over HTTP — the leaderboard in every window, search by name and by craft, the builder
11
+ // of the week, the crowns and the movers — each read signed out and signed in, and the
12
+ // two bodies compared byte for byte.
13
+ //
14
+ // THE HISTORY. PUB's credits step up one hour after every window edge, by 1, 2, 4 … 256,
15
+ // one work a step, so each window's total is a bitmask naming exactly the snapshots it
16
+ // counted (510 = every step after the 365-day edge, 192 = the week's two). Every account
17
+ // the matrix keeps off the boards gains 1000 a step: had any predicate let one through,
18
+ // it would top that board and take that crown, so its absence is never vacuous.
19
+ //
20
+ // THE CLOCK is frozen at NOW, a Wednesday in August 2097, because the routes take no
21
+ // `now`. Every read is global, and those windows hold no real builder's snapshot, so a
22
+ // shared database's own rows move nowhere in them (the crowns and movers _db precedent).
23
+ //
24
+ // Real-DB integration test: self-skips without a reachable Postgres; in the INTEGRATION
25
+ // set (scripts/bongos/run-unit-tests.js) so the DB-free lane never runs it.
26
+ //
27
+ // Run: DATABASE_URL=... node tests/community_proof_db.mjs
28
+
29
+ import { createRequire } from 'node:module';
30
+ import { readFileSync } from 'node:fs';
31
+ import { fileURLToPath } from 'node:url';
32
+ import { dirname, join } from 'node:path';
33
+ import http from 'node:http';
34
+ import { strict as assert } from 'node:assert';
35
+ import { KEYS, STATE, MATRIX } from './fixtures/privacy-matrix.mjs';
36
+
37
+ const require = createRequire(import.meta.url);
38
+ const { Pool } = require('pg');
39
+ const express = require('express');
40
+
41
+ const here = dirname(fileURLToPath(import.meta.url));
42
+ const MIG_DIR = join(here, '..', 'modules', 'platform-identity', 'migrations');
43
+ const migrationSql = (f) => readFileSync(join(MIG_DIR, f), 'utf8');
44
+
45
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
46
+ try {
47
+ await pool.query('SELECT 1');
48
+ } catch (e) {
49
+ console.log(`community_proof_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
50
+ await pool.end().catch(() => {});
51
+ process.exit(0);
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // The fixture
56
+ // ---------------------------------------------------------------------------
57
+
58
+ const PREFIX = 't1002985-';
59
+ const TAG = 't1002985-craft'; // every fixture row's only craft; the board reads filter on it
60
+ const ID = Object.fromEntries(KEYS.map((k, i) => [k, 90298501 + i]));
61
+ // '-acct' keeps one handle from being a substring of another (priv / privd / privdh)
62
+ const HANDLE = Object.fromEntries(KEYS.map((k) => [k, `${PREFIX}${k.toLowerCase()}-acct`]));
63
+ const NAME = Object.fromEntries(KEYS.map((k) => [k, `T1002985 ${k} Acct`]));
64
+ const IDS = Object.values(ID);
65
+ const CLIENT = 't1002985-p1';
66
+ const ORIGIN = 'https://t1002985-p1.example';
67
+ const PROJECT = 'T1002985 Alpha';
68
+
69
+ const NOW = '2097-08-14T12:00:00.000Z';
70
+ const HISTORY = '2095-06-01T00:00:00.000Z'; // the first snapshot written: when history began
71
+ // Every edge a surface reads, in time order. The routes' own answers are checked against
72
+ // these below, so a fixture that stopped straddling them would fail, not pass vacuously.
73
+ const EDGE = {
74
+ lastYear: '2096-01-01T00:00:00.000Z',
75
+ d365: '2096-08-14T12:00:00.000Z',
76
+ year: '2097-01-01T00:00:00.000Z',
77
+ month: '2097-07-01T00:00:00.000Z',
78
+ d30: '2097-07-15T12:00:00.000Z',
79
+ monthEnd: '2097-08-01T00:00:00.000Z',
80
+ week: '2097-08-05T00:00:00.000Z',
81
+ d7: '2097-08-07T12:00:00.000Z',
82
+ weekEnd: '2097-08-12T00:00:00.000Z',
83
+ };
84
+ const day = (iso) => iso.slice(0, 10);
85
+
86
+ // PUB's numbers per window, read off its steps: the sum of the steps after the edge.
87
+ const PUB_IN = {
88
+ all: [511, 9], // every step
89
+ '365d': [510, 8], // 2 … 256: the 365-day edge falls after the first step
90
+ '30d': [496, 5], // 16 … 256
91
+ '7d': [384, 2], // 128 + 256
92
+ week: [192, 2], // 64 + 128: the step after the week's end is the next week's
93
+ month: [24, 2], // 8 + 16: the step after the month's end is August's
94
+ year: [508, 7], // 4 … 256, the year so far
95
+ lastYear: [3, 2], // 1 + 2
96
+ };
97
+ // OPTOUT: 5 credits and one work at the 7-day edge plus two hours, so inside every rolling
98
+ // window, the week and the year so far, and in neither July nor last year.
99
+ const OPTOUT_IN = [5, 1];
100
+
101
+ // The shared rule this history is built for: the boards, the crowns and the movers show
102
+ // PUB and OPTOUT and nobody else (a crown or a card IS stats — the board's rule).
103
+ const SHOWN = KEYS.filter((k) => MATRIX[k].board === 'listed');
104
+ assert.deepEqual(SHOWN, ['PUB', 'OPTOUT'], 'the matrix changed — re-plan this history before trusting it');
105
+ for (const k of KEYS) {
106
+ for (const col of ['week', 'crowns', 'movers']) assert.equal(MATRIX[k][col], MATRIX[k].board, `${k}.${col} no longer follows the board`);
107
+ }
108
+ const KEPT_OFF = KEYS.filter((k) => !SHOWN.includes(k));
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // The frozen clock, the doorway and the real routers
112
+ // ---------------------------------------------------------------------------
113
+
114
+ // Every Date the modules make from here on is a FrozenDate. A Date made before the swap
115
+ // would fail pg's `instanceof Date`, so fixture times go to SQL as ISO strings.
116
+ const RealDate = Date;
117
+ const clock = { now: RealDate.parse(NOW) };
118
+ class FrozenDate extends RealDate {
119
+ constructor(...args) {
120
+ if (args.length) super(...args);
121
+ else super(clock.now);
122
+ }
123
+
124
+ static now() {
125
+ return clock.now;
126
+ }
127
+ }
128
+ globalThis.Date = FrozenDate;
129
+ const after = (iso, hours) => new RealDate(RealDate.parse(iso) + hours * 3600 * 1000).toISOString();
130
+
131
+ const moduleApiPath = require.resolve('../src/module-api.js');
132
+ require.cache[moduleApiPath] = {
133
+ id: moduleApiPath,
134
+ filename: moduleApiPath,
135
+ loaded: true,
136
+ exports: {
137
+ pool,
138
+ corsPublicGet: () => (_req, _res, next) => next(),
139
+ logger: () => ({ error: (o, m) => console.error(`route error: ${m}`, o), info: () => {}, warn: () => {} }),
140
+ },
141
+ };
142
+ const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
143
+ const app = express();
144
+ app.use(express.json());
145
+ app.use(attachFail);
146
+ for (const f of ['community-search', 'community-leaderboard']) {
147
+ app.use('/api/bongos', require(`../modules/platform-identity/routes/${f}.js`)());
148
+ }
149
+ const server = app.listen(0, '127.0.0.1');
150
+ await new Promise((r) => server.once('listening', r));
151
+ const port = server.address().port;
152
+
153
+ const SIGNED_IN = { Authorization: 'Bearer t1002985-viewer', Cookie: 'bongos_session=t1002985-viewer' };
154
+ const SEEN = []; // every body served, for the sweep at the end
155
+ function get(path, headers) {
156
+ return new Promise((resolve, reject) => {
157
+ const req = http.request({ host: '127.0.0.1', port, method: 'GET', path, headers, agent: false }, (res) => {
158
+ const chunks = [];
159
+ res.on('data', (c) => chunks.push(c));
160
+ res.on('end', () => resolve({ status: res.statusCode, text: Buffer.concat(chunks).toString('utf8') }));
161
+ });
162
+ req.on('error', reject);
163
+ req.end();
164
+ });
165
+ }
166
+ // A surface read signed out and signed in: the same bytes to both (spec D3, D8).
167
+ async function read(path) {
168
+ const anon = await get(path, {});
169
+ const signed = await get(path, SIGNED_IN);
170
+ assert.equal(anon.status, 200, `${path} → ${anon.status}: ${anon.text.slice(0, 300)}`);
171
+ assert.equal(signed.text, anon.text, `${path}: a signed-in viewer got a different body`);
172
+ SEEN.push({ path, text: anon.text });
173
+ return JSON.parse(anon.text);
174
+ }
175
+
176
+ const q = (sql, params) => pool.query(sql, params);
177
+ let pass = 0;
178
+ const same = (actual, expected, msg) => { assert.deepEqual(actual, expected, msg); pass++; console.log(` ok - ${msg}`); };
179
+ const who = (w) => w && [w.handle, w.credits, w.tasks_shipped];
180
+ const mine = (rows) => rows.filter((r) => r.handle.startsWith(PREFIX));
181
+
182
+ async function cleanup() {
183
+ await q('DELETE FROM platform_identity_activity_snapshots WHERE github_id = ANY($1::bigint[])', [IDS]).catch(() => {});
184
+ await q('DELETE FROM platform_identity_builder_project_activity WHERE github_id = ANY($1::bigint[])', [IDS]).catch(() => {});
185
+ await q('DELETE FROM platform_identity_accounts WHERE github_id = ANY($1::bigint[])', [IDS]).catch(() => {});
186
+ await q('DELETE FROM platform_identity_projects WHERE origin = $1', [ORIGIN]).catch(() => {});
187
+ await q('DELETE FROM platform_identity_sso_clients WHERE client_id = $1', [CLIENT]).catch(() => {});
188
+ await q('DELETE FROM platform_identity_weekly_features WHERE week_start = $1::date', [day(EDGE.week)]).catch(() => {});
189
+ await q(
190
+ `DELETE FROM platform_identity_period_features
191
+ WHERE (period = 'month' AND period_start = $1::date) OR (period = 'year' AND period_start = $2::date)`,
192
+ [day(EDGE.month), day(EDGE.lastYear)],
193
+ ).catch(() => {});
194
+ }
195
+
196
+ // One (project, builder) row, seen since history began, and its cumulative snapshots.
197
+ async function history(k, steps) {
198
+ const [, credits, ships] = steps[steps.length - 1];
199
+ await q(
200
+ `INSERT INTO platform_identity_builder_project_activity (client_id, github_id, credits, tasks_shipped, karma, disciplines, first_seen_at)
201
+ VALUES ($1, $2, $3, $4, 0, $5, $6)`,
202
+ [CLIENT, ID[k], credits, ships, [TAG], HISTORY],
203
+ );
204
+ for (const [at, c, s] of steps) {
205
+ await q(
206
+ `INSERT INTO platform_identity_activity_snapshots (client_id, github_id, credits, tasks_shipped, karma, reported_at)
207
+ VALUES ($1, $2, $3, $4, 0, $5)`,
208
+ [CLIENT, ID[k], c, s, at],
209
+ );
210
+ }
211
+ }
212
+ // From zero at HISTORY, one step `hours` after each edge, `size(i)` credits and one work each.
213
+ function stepsAfterEachEdge(hours, size) {
214
+ let credits = 0;
215
+ return [[HISTORY, 0, 0], ...Object.values(EDGE).map((edge, i) => {
216
+ credits += size(i);
217
+ return [after(edge, hours), credits, i + 1];
218
+ })];
219
+ }
220
+
221
+ try {
222
+ await q('CREATE EXTENSION IF NOT EXISTS pg_trgm');
223
+ for (const f of [
224
+ 'platform_identity_001_tables.sql', 'platform_identity_004_projects.sql', 'platform_identity_005_builder_activity.sql',
225
+ 'platform_identity_007_profile_consent.sql', 'platform_identity_008_handle.sql',
226
+ 'platform_identity_011_project_visibility.sql', 'platform_identity_012_account_privacy.sql',
227
+ 'platform_identity_019_terms_acceptance.sql', 'platform_identity_020_visibility_pull.sql',
228
+ 'platform_identity_022_activity_snapshots.sql', 'platform_identity_023_builder_search_trgm.sql',
229
+ 'platform_identity_032_weekly_features.sql', 'platform_identity_033_period_features.sql',
230
+ ]) await q(migrationSql(f));
231
+ await cleanup();
232
+
233
+ for (const k of KEYS) {
234
+ const s = STATE[k];
235
+ await q(
236
+ `INSERT INTO platform_identity_accounts
237
+ (github_id, github_login, display_name, handle, profile_state, account_visibility, hide_stats, recruiter_discoverable, terms_accepted_at)
238
+ VALUES ($1, $2, $3, $2, $4, $5, $6, $7, now())`,
239
+ [ID[k], HANDLE[k], NAME[k], s.profile_state, s.account_visibility, s.hide_stats, s.recruiter_discoverable],
240
+ );
241
+ }
242
+ await q(`INSERT INTO platform_identity_sso_clients (client_id, secret_hash, name, origin) VALUES ($1, 'x', $2, $3)`, [CLIENT, PROJECT, ORIGIN]);
243
+ await q(
244
+ `INSERT INTO platform_identity_projects (origin, name, description, featured, status, visibility, visibility_source)
245
+ VALUES ($1, $2, 'described', false, 'active', 'public', 'control-plane')`,
246
+ [ORIGIN, PROJECT],
247
+ );
248
+ await history('PUB', stepsAfterEachEdge(1, (i) => 2 ** i)); // first, so its HISTORY row is the first snapshot
249
+ await history('OPTOUT', [[HISTORY, 0, 0], [after(EDGE.d7, 2), ...OPTOUT_IN]]);
250
+ for (const k of KEPT_OFF) await history(k, stepsAfterEachEdge(2, () => 1000));
251
+
252
+ // 1 — The leaderboard in every window (D3): exactly the shown accounts, the numbers exact.
253
+ for (const [w, edge] of [['all', null], ['365d', EDGE.d365], ['30d', EDGE.d30], ['7d', EDGE.d7]]) {
254
+ const board = await read(`/api/bongos/community/leaderboard?window=${w}&discipline=${TAG}&limit=100`);
255
+ same(board.since, edge, `${w}: the window opens on the edge the history straddles`);
256
+ same(board.results.map((r) => [r.place, r.handle, r.credits, r.tasks_shipped]),
257
+ [[1, HANDLE.PUB, ...PUB_IN[w]], [2, HANDLE.OPTOUT, ...OPTOUT_IN]],
258
+ `${w} board: PUB ${PUB_IN[w][0]} and OPTOUT ${OPTOUT_IN[0]}; the five accounts kept off, each ahead of both, are absent`);
259
+ }
260
+
261
+ // 2 — Search (D4): findable by name per the matrix; a craft is stats, so the craft filter
262
+ // drops the hidden account the name search lists.
263
+ const byName = new Map(mine((await read(`/api/bongos/community/search?q=${PREFIX}&limit=50`)).results).map((r) => [r.handle, r]));
264
+ for (const k of KEYS) {
265
+ const r = byName.get(HANDLE[k]);
266
+ const got = !r ? 'absent' : r.disciplines === null ? 'name-only' : 'listed';
267
+ same(got, MATRIX[k].search, `search by name, ${STATE[k].label}: ${MATRIX[k].search}`);
268
+ if (got === 'listed') same(r.disciplines, [TAG], `${k}'s crafts are its own`);
269
+ }
270
+ const byCraft = mine((await read(`/api/bongos/community/search?q=${PREFIX}&discipline=${TAG}&limit=50`)).results);
271
+ same(byCraft.map((r) => r.handle).sort(), KEYS.filter((k) => MATRIX[k].search === 'listed').map((k) => HANDLE[k]).sort(),
272
+ 'search by craft: the hidden account found by name is absent');
273
+
274
+ // 3 — The builder of the week (D5): the last full Monday-to-Monday week, crowned at render.
275
+ const bow = await read('/api/bongos/community/builder-of-the-week');
276
+ same([bow.week.start, bow.week.end], [day(EDGE.week), day(EDGE.weekEnd)], 'the crowned week is the one the history straddles');
277
+ same(who(bow.winner), [HANDLE.PUB, ...PUB_IN.week], 'builder of the week: PUB’s 192 and two works');
278
+ same(mine(bow.past), [], 'no fixture account in the past weeks');
279
+
280
+ // 4 — The crowns: the month and last year crowned at render, the year so far live.
281
+ const crowns = await read('/api/bongos/community/crowns');
282
+ same(who(crowns.week.winner), [HANDLE.PUB, ...PUB_IN.week], 'the home’s week is the same crown');
283
+ same([crowns.month.start, crowns.month.end, crowns.month.first_crown], [day(EDGE.month), day(EDGE.monthEnd), null], 'the crowned month is July, the history straddling both ends');
284
+ same(who(crowns.month.winner), [HANDLE.PUB, ...PUB_IN.month], 'builder of the month: PUB’s 24 and two works');
285
+ same([crowns.year.start, crowns.year.since], [day(EDGE.year), day(EDGE.year)], 'the year so far, history having begun before it');
286
+ same(who(crowns.year.winner), [HANDLE.PUB, ...PUB_IN.year], 'builder of the year so far: PUB’s 508 and seven works');
287
+ same(crowns.year.last && [crowns.year.last.start, ...who(crowns.year.last.winner)], [day(EDGE.lastYear), HANDLE.PUB, ...PUB_IN.lastYear],
288
+ 'builder of last year: PUB’s 3 and two works');
289
+
290
+ // 5 — The movers (D7): the last 30 days, rolling.
291
+ const movers = await read('/api/bongos/community/movers');
292
+ same([movers.window.start, movers.window.end], [EDGE.d30, NOW], 'the 30 days end now and open on the 30-day edge');
293
+ same(who(movers.rising), [HANDLE.PUB, ...PUB_IN['30d']], 'rising builder: PUB, 15 before the 30 days (under 100) and 496 in them');
294
+ same(movers.projects.map((p) => [p.name, p.origin, p.tasks_shipped, p.credits]), [[PROJECT, ORIGIN, 6, 501]],
295
+ 'projects on the move: PUB’s five works and OPTOUT’s one; none of the kept-off accounts’ works count');
296
+
297
+ // 6 — A winner who hides (D8): gone from every surface on the next read, and no pinned
298
+ // period is handed to anyone else; only the live reads move on to OPTOUT.
299
+ await q('UPDATE platform_identity_accounts SET hide_stats = true WHERE github_id = $1', [ID.PUB]);
300
+ for (const w of ['all', '365d', '30d', '7d']) {
301
+ const board = await read(`/api/bongos/community/leaderboard?window=${w}&discipline=${TAG}&limit=100`);
302
+ same(board.results.map((r) => [r.place, r.handle, r.credits, r.tasks_shipped]), [[1, HANDLE.OPTOUT, ...OPTOUT_IN]], `${w} board after PUB hides: OPTOUT alone`);
303
+ }
304
+ const hidName = mine((await read(`/api/bongos/community/search?q=${PREFIX}&limit=50`)).results).find((r) => r.handle === HANDLE.PUB);
305
+ same(hidName && hidName.disciplines, null, 'search by name: PUB is still found, now name-only');
306
+ same(mine((await read(`/api/bongos/community/search?q=${PREFIX}&discipline=${TAG}&limit=50`)).results).map((r) => r.handle), [HANDLE.OPTOUT],
307
+ 'search by craft: PUB is gone');
308
+ same((await read('/api/bongos/community/builder-of-the-week')).winner, null, 'the week PUB won shows nobody, though OPTOUT earned in it');
309
+ const hidCrowns = await read('/api/bongos/community/crowns');
310
+ same([hidCrowns.week.winner, hidCrowns.month.winner, hidCrowns.year.last], [null, null, null], 'the week, July and last year stay PUB’s and show nobody');
311
+ same(hidCrowns.year.winner, null, 'the year so far shows nobody while the leader’s memo is warm');
312
+ const hidMovers = await read('/api/bongos/community/movers');
313
+ same(who(hidMovers.rising), [HANDLE.OPTOUT, ...OPTOUT_IN], 'rising builder: OPTOUT, without waiting for the memo');
314
+ same(hidMovers.projects.map((p) => [p.name, p.tasks_shipped, p.credits]), [[PROJECT, 1, 5]], 'PUB’s works leave the project’s totals');
315
+ clock.now += 31 * 1000; // past every 30s memo; no snapshot sits within 31s of an edge
316
+ same(who((await read('/api/bongos/community/crowns')).year.winner), [HANDLE.OPTOUT, ...OPTOUT_IN], 'once the memo lapses, the year so far is OPTOUT’s');
317
+
318
+ // 7 — The sweep: an account kept off the boards is named by no body served above, but
319
+ // the name search the matrix allows it.
320
+ for (const k of KEPT_OFF) {
321
+ for (const r of SEEN) {
322
+ if (r.path.startsWith('/api/bongos/community/search?') && !r.path.includes('discipline=') && MATRIX[k].search !== 'absent') continue;
323
+ assert.ok(!r.text.includes(HANDLE[k]) && !r.text.includes(NAME[k]), `${k} was named by GET ${r.path}`);
324
+ }
325
+ }
326
+ same(SEEN.length, 19, 'the sweep read all 19 bodies served, each identical signed out and signed in, and no kept-off account is named where the matrix says absent');
327
+
328
+ console.log(`\ncommunity_proof_db.mjs: ${pass} passed`);
329
+ } finally {
330
+ await cleanup();
331
+ globalThis.Date = RealDate;
332
+ await new Promise((r) => server.close(r));
333
+ await pool.end().catch(() => {});
334
+ }
@@ -0,0 +1,69 @@
1
+ // tests/fixtures/privacy-matrix.mjs — the privacy spec's D2 table, transcribed ONCE
2
+ // (docs/specs/bongos-v1-privacy-and-invites.md D2; task 1002973, shared since task 1002985).
3
+ //
4
+ // WHY A FIXTURE. Two proofs read these cells: tests/privacy_matrix.mjs walks every
5
+ // surface DB-free through an interpreter that reads each statement's WHERE text, and
6
+ // tests/community_proof_db.mjs runs the community surfaces against real Postgres. Two
7
+ // transcriptions of one spec table would drift; a cell lives here or nowhere.
8
+
9
+ // One fixture account per D2 row. The provisional account carries the MOST permissive
10
+ // flags on purpose (public, shown, discoverable): D2 says provisional dominates every
11
+ // flag, so nothing else may let it through.
12
+ export const KEYS = ['PROV', 'PUB', 'SHY', 'PRIV', 'PRIVD', 'PRIVDH', 'OPTOUT'];
13
+ export const STATE = {
14
+ PROV: { label: 'provisional (any flags)', profile_state: 'provisional', account_visibility: 'public', hide_stats: false, recruiter_discoverable: true },
15
+ PUB: { label: 'active · public · stats shown', profile_state: 'active', account_visibility: 'public', hide_stats: false, recruiter_discoverable: null },
16
+ SHY: { label: 'active · public · hide_stats', profile_state: 'active', account_visibility: 'public', hide_stats: true, recruiter_discoverable: null },
17
+ PRIV: { label: 'active · private (not discoverable)', profile_state: 'active', account_visibility: 'private', hide_stats: false, recruiter_discoverable: null },
18
+ PRIVD: { label: 'active · private · discoverable', profile_state: 'active', account_visibility: 'private', hide_stats: false, recruiter_discoverable: true },
19
+ PRIVDH: { label: 'active · private · discoverable · hide_stats', profile_state: 'active', account_visibility: 'private', hide_stats: true, recruiter_discoverable: true },
20
+ OPTOUT: { label: 'active · public · opted out of recruiting', profile_state: 'active', account_visibility: 'public', hide_stats: false, recruiter_discoverable: false },
21
+ };
22
+
23
+ // THE MATRIX — spec D2, cell by cell for the surfaces that exist
24
+ //
25
+ // profile GET /profiles/:handle Anon · Builder · Self's PUBLIC read (viewer-independent)
26
+ // 404 | 'full' (record shown) | 'identity' (renders, record null)
27
+ // search GET /community/search Anon · Builder — "findable by name/handle"
28
+ // 'listed' | 'name-only' (listed, disciplines null) | 'absent'
29
+ // board GET /community/leaderboard Anon · Builder — the cross-project leaderboard
30
+ // 'listed' | 'absent'
31
+ // week GET /community/builder-of-the-week Anon · Builder · Recruiter — the crown and
32
+ // the past weeks (task 1002981, spec D5): a row IS stats, so the board's rule.
33
+ // Every fixture account has won a recorded week, so the read's own predicate
34
+ // decides: 'listed' | 'absent'
35
+ // crowns GET /community/crowns Anon · Builder · Recruiter — the builders of the month,
36
+ // the year so far and last year (task 1004902): a crown IS stats, so the board's
37
+ // rule. Each account in turn is made the winner of all three, so the reads'
38
+ // own predicates decide: 'listed' | 'absent'
39
+ // movers GET /community/movers Anon · Builder · Recruiter — the rising builder and the
40
+ // projects on the move (task 1002983): a card IS stats, so the board's rule, and
41
+ // a project's totals are the sum of what may be shown. Each account in turn is
42
+ // the one who moved, so the reads' own predicates decide: 'listed' | 'absent'
43
+ // directory GET /scouting Recruiter — 'full' | 'name-only' | 'absent'
44
+ // sliver GET /scouting/:handle Recruiter — 'full' (D3) | 'name-only' (D3, hidden) | 404
45
+ // self GET /me/cross-project Self — 'own' (own numbers, any flags) | 'n/a' (no session exists)
46
+ // guild GET /guilds/:slug Anon · Builder · Recruiter · Self — the guild page's PUBLIC
47
+ // roster (task 1002301; ADR 0336 D4), viewer-independent: 'listed' | 'absent'.
48
+ // Every fixture account is a member with shown_publicly ON, so the account
49
+ // predicates alone decide; hide_stats never hides membership (D4 §2)
50
+ // members GET /guilds/:slug/members a fellow Member — the own-scope roster: 'listed' |
51
+ // 'absent'. Joining is consent to be seen BY THE GUILD (D4 §1), so a private
52
+ // member is listed here and nowhere public; a provisional account is never
53
+ // invite POST /projects/invite-guild a project's Inviter (task 1002303; ADR 0365 D3) — one
54
+ // hall link per member of a PUBLIC guild's public roster: 'listed' | 'absent'.
55
+ // It reads accounts across projects, so it may hand the inviter exactly the
56
+ // people the guild page already shows them — shown_publicly AND existence;
57
+ // hide_stats never hides membership, and private or provisional is never listed
58
+ //
59
+ // The week/crowns/movers wording above describes privacy_matrix.mjs's fixture; the real-DB
60
+ // proof seeds history instead and reads the same cells.
61
+ export const MATRIX = {
62
+ PROV: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'absent', sliver: 404, self: 'n/a', guild: 'absent', members: 'absent', invite: 'absent' },
63
+ PUB: { profile: 'full', search: 'listed', board: 'listed', week: 'listed', crowns: 'listed', movers: 'listed', directory: 'full', sliver: 'full', self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
64
+ SHY: { profile: 'identity', search: 'name-only', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'name-only', sliver: 'name-only', self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
65
+ PRIV: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'absent', sliver: 404, self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
66
+ PRIVD: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'absent', sliver: 'full', self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
67
+ PRIVDH: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'absent', sliver: 'name-only', self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
68
+ OPTOUT: { profile: 'full', search: 'listed', board: 'listed', week: 'listed', crowns: 'listed', movers: 'listed', directory: 'absent', sliver: 404, self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
69
+ };
@@ -11,7 +11,7 @@
11
11
  // them is, is THE TABLE. This file is: one fixture account per D2 row, every
12
12
  // viewer column, every cross-project surface that exists today, run through the
13
13
  // real routers in one process, and asserted against a literal transcription of
14
- // the spec's cells (MATRIX below). A cell that drifts — a surface added without
14
+ // the spec's cells (MATRIX, tests/fixtures/privacy-matrix.mjs). A cell that drifts — a surface added without
15
15
  // the predicate, a predicate loosened for one reader — reds here as the cell it
16
16
  // is, beside the six it should still agree with.
17
17
  //
@@ -69,6 +69,8 @@ import {
69
69
  scoutingListableSql,
70
70
  scoutingNameOnlySql,
71
71
  } from '../modules/platform-identity/account-visibility.js';
72
+ // The D2 rows and their cells, shared with tests/community_proof_db.mjs (task 1002985).
73
+ import { KEYS, STATE, MATRIX } from './fixtures/privacy-matrix.mjs';
72
74
 
73
75
  const require = createRequire(import.meta.url);
74
76
  const express = require('express');
@@ -79,20 +81,8 @@ const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
79
81
  // ===========================================================================
80
82
  //
81
83
  // Every value is an improbable per-account literal, so "absent from the body"
82
- // means absent. The provisional account carries the MOST permissive flags on
83
- // purpose (public, shown, discoverable, terms accepted): D2 says provisional
84
- // dominates every flag, so nothing else may let it through.
85
-
86
- const KEYS = ['PROV', 'PUB', 'SHY', 'PRIV', 'PRIVD', 'PRIVDH', 'OPTOUT'];
87
- const STATE = {
88
- PROV: { label: 'provisional (any flags)', profile_state: 'provisional', account_visibility: 'public', hide_stats: false, recruiter_discoverable: true },
89
- PUB: { label: 'active · public · stats shown', profile_state: 'active', account_visibility: 'public', hide_stats: false, recruiter_discoverable: null },
90
- SHY: { label: 'active · public · hide_stats', profile_state: 'active', account_visibility: 'public', hide_stats: true, recruiter_discoverable: null },
91
- PRIV: { label: 'active · private (not discoverable)', profile_state: 'active', account_visibility: 'private', hide_stats: false, recruiter_discoverable: null },
92
- PRIVD: { label: 'active · private · discoverable', profile_state: 'active', account_visibility: 'private', hide_stats: false, recruiter_discoverable: true },
93
- PRIVDH: { label: 'active · private · discoverable · hide_stats', profile_state: 'active', account_visibility: 'private', hide_stats: true, recruiter_discoverable: true },
94
- OPTOUT: { label: 'active · public · opted out of recruiting', profile_state: 'active', account_visibility: 'public', hide_stats: false, recruiter_discoverable: false },
95
- };
84
+ // means absent. KEYS and STATE (the flags, the provisional row the most
85
+ // permissive on purpose) are tests/fixtures/privacy-matrix.mjs's.
96
86
 
97
87
  const ACCOUNTS = KEYS.map((k, i) => {
98
88
  const id = String(3120001 + i);
@@ -144,48 +134,8 @@ function identitySentinels(acct) {
144
134
  // THE MATRIX — spec D2, transcribed cell by cell for the surfaces that exist
145
135
  // ===========================================================================
146
136
  //
147
- // profile GET /profiles/:handle Anon · Builder · Self's PUBLIC read (viewer-independent)
148
- // 404 | 'full' (record shown) | 'identity' (renders, record null)
149
- // search GET /community/search Anon · Builder — "findable by name/handle"
150
- // 'listed' | 'name-only' (listed, disciplines null) | 'absent'
151
- // board GET /community/leaderboard Anon · Builder — the cross-project leaderboard
152
- // 'listed' | 'absent'
153
- // week GET /community/builder-of-the-week Anon · Builder · Recruiter — the crown and
154
- // the past weeks (task 1002981, spec D5): a row IS stats, so the board's rule.
155
- // Every fixture account has won a recorded week, so the read's own predicate
156
- // decides: 'listed' | 'absent'
157
- // crowns GET /community/crowns Anon · Builder · Recruiter — the builders of the month,
158
- // the year so far and last year (task 1004902): a crown IS stats, so the board's
159
- // rule. Each account in turn is made the winner of all three, so the reads'
160
- // own predicates decide: 'listed' | 'absent'
161
- // movers GET /community/movers Anon · Builder · Recruiter — the rising builder and the
162
- // projects on the move (task 1002983): a card IS stats, so the board's rule, and
163
- // a project's totals are the sum of what may be shown. Each account in turn is
164
- // the one who moved, so the reads' own predicates decide: 'listed' | 'absent'
165
- // directory GET /scouting Recruiter — 'full' | 'name-only' | 'absent'
166
- // sliver GET /scouting/:handle Recruiter — 'full' (D3) | 'name-only' (D3, hidden) | 404
167
- // self GET /me/cross-project Self — 'own' (own numbers, any flags) | 'n/a' (no session exists)
168
- // guild GET /guilds/:slug Anon · Builder · Recruiter · Self — the guild page's PUBLIC
169
- // roster (task 1002301; ADR 0336 D4), viewer-independent: 'listed' | 'absent'.
170
- // Every fixture account is a member with shown_publicly ON, so the account
171
- // predicates alone decide; hide_stats never hides membership (D4 §2)
172
- // members GET /guilds/:slug/members a fellow Member — the own-scope roster: 'listed' |
173
- // 'absent'. Joining is consent to be seen BY THE GUILD (D4 §1), so a private
174
- // member is listed here and nowhere public; a provisional account is never
175
- // invite POST /projects/invite-guild a project's Inviter (task 1002303; ADR 0365 D3) — one
176
- // hall link per member of a PUBLIC guild's public roster: 'listed' | 'absent'.
177
- // It reads accounts across projects, so it may hand the inviter exactly the
178
- // people the guild page already shows them — shown_publicly AND existence;
179
- // hide_stats never hides membership, and private or provisional is never listed
180
- const MATRIX = {
181
- PROV: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'absent', sliver: 404, self: 'n/a', guild: 'absent', members: 'absent', invite: 'absent' },
182
- PUB: { profile: 'full', search: 'listed', board: 'listed', week: 'listed', crowns: 'listed', movers: 'listed', directory: 'full', sliver: 'full', self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
183
- SHY: { profile: 'identity', search: 'name-only', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'name-only', sliver: 'name-only', self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
184
- PRIV: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'absent', sliver: 404, self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
185
- PRIVD: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'absent', sliver: 'full', self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
186
- PRIVDH: { profile: 404, search: 'absent', board: 'absent', week: 'absent', crowns: 'absent', movers: 'absent', directory: 'absent', sliver: 'name-only', self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
187
- OPTOUT: { profile: 'full', search: 'listed', board: 'listed', week: 'listed', crowns: 'listed', movers: 'listed', directory: 'absent', sliver: 404, self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
188
- };
137
+ // MATRIX lives in tests/fixtures/privacy-matrix.mjs, with each column's viewers and
138
+ // values, so the real-DB community proof reads the same cells (task 1002985).
189
139
 
190
140
  // ===========================================================================
191
141
  // The in-memory tables + their statement interpreter