@bongos/core 1.23.13 → 1.23.15

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.
Files changed (56) hide show
  1. package/.bongos-core.json +90 -55
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +2 -0
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/api/openapi.json +22 -1
  8. package/docs/api-reference.md +3 -2
  9. package/docs/copy-inventory.md +1000 -946
  10. package/docs/copy-registry.json +2009 -1465
  11. package/docs/module-api-changelog.md +4 -2
  12. package/docs/page-inventory.json +10 -4
  13. package/docs/page-readings.json +593 -573
  14. package/modules/copy-desk/page-status.js +2 -2
  15. package/modules/copy-desk/tests/copy_page_status.mjs +1 -1
  16. package/modules/hall-ui/public/approval-queue-lib.js +1 -1
  17. package/modules/hall-ui/public/approval-queue.css +26 -12
  18. package/modules/hall-ui/public/approval-queue.js +40 -16
  19. package/modules/hall-ui/public/studio-lib.js +5 -3
  20. package/modules/hall-ui/public/studio.html +8 -8
  21. package/modules/hall-ui/public/studio.js +25 -5
  22. package/modules/hall-ui/public/studio.states.json +54 -4
  23. package/modules/hall-ui/public/welcome-boot.js +62 -12
  24. package/modules/platform-identity/builder-of-the-week.js +180 -0
  25. package/modules/platform-identity/migrations/platform_identity_032_weekly_features.sql +53 -0
  26. package/modules/platform-identity/routes/community-leaderboard.js +25 -1
  27. package/modules/public-landing/public/account.html +2 -1
  28. package/modules/public-landing/public/community-week-none.states.json +16 -0
  29. package/modules/public-landing/public/community.html +142 -13
  30. package/modules/public-landing/public/community.states.json +6 -4
  31. package/modules/public-landing/public/connections.html +2 -1
  32. package/modules/public-landing/public/contact.html +2 -1
  33. package/modules/public-landing/public/guild.html +2 -1
  34. package/modules/public-landing/public/index.html +2 -1
  35. package/modules/public-landing/public/invitations.html +2 -1
  36. package/modules/public-landing/public/privacy.html +2 -1
  37. package/modules/public-landing/public/projects.html +2 -1
  38. package/modules/public-landing/public/terms.html +2 -1
  39. package/modules/ui-design/kit/fixtures/community-week-none.json +6 -0
  40. package/modules/ui-design/kit/fixtures/community-week.json +11 -0
  41. package/modules/ui-design/kit/serve.js +8 -0
  42. package/package-lock.json +2 -2
  43. package/package.json +1 -1
  44. package/release-notes.json +15 -1
  45. package/scripts/bongos/run-unit-tests.js +4 -0
  46. package/src/module-api.js +1 -1
  47. package/src/platform-server.js +5 -5
  48. package/tests/builder_of_the_week.mjs +266 -0
  49. package/tests/builder_of_the_week_db.mjs +166 -0
  50. package/tests/hall_approval_queue.mjs +33 -9
  51. package/tests/hall_studio_home.mjs +74 -7
  52. package/tests/hub_account_page.mjs +2 -2
  53. package/tests/hub_community_page.mjs +136 -25
  54. package/tests/landing_legal_pages.mjs +2 -2
  55. package/tests/privacy_matrix.mjs +53 -8
  56. package/tests/welcome_boot_browser.mjs +45 -3
@@ -338,11 +338,11 @@ function buildPlatformApp() {
338
338
  res.type('html').send(html);
339
339
  });
340
340
  // THE COMMUNITY PAGES (task 1003363, ADR 0217 D1, ADR 0337 D1/D3): who is building
341
- // across every project — the leaderboard, the builder search (the same for every
342
- // viewer) and the recruiter list GET /scouting's own door admits. One file serves all
343
- // three; its script shows the one the path names. Hub-only for the /account reason:
344
- // every read it makes is a platform-identity route.
345
- app.get(['/community', '/community/find', '/community/recruiters'], (req, res, next) => {
341
+ // across every project — the leaderboard, the builder of the week (task 1002981), the
342
+ // builder search (the same for every viewer) and the recruiter list GET /scouting's own
343
+ // door admits. One file serves all four; its script shows the one the path names.
344
+ // Hub-only for the /account reason: every read it makes is a platform-identity route.
345
+ app.get(['/community', '/community/week', '/community/find', '/community/recruiters'], (req, res, next) => {
346
346
  if (!isApexHost(req) || !isModuleEnabled('platform-identity')) return next();
347
347
  const html = landingPage('community.html');
348
348
  if (!html) return next();
@@ -0,0 +1,266 @@
1
+ // tests/builder_of_the_week.mjs — Builder of the Week (community spec D5; task 1002981).
2
+ //
3
+ // DB-free. A fake pool answers the module's four statements: the existence probe on
4
+ // platform_identity_weekly_features, the winner computation, the INSERT that records
5
+ // the week, and the read of the crown and the weeks before it. Which snapshot becomes
6
+ // the week's start and end, and that the predicate really filters, need a real
7
+ // Postgres: tests/builder_of_the_week_db.mjs.
8
+ //
9
+ // What this pins:
10
+ // 1. the week: the last FULL Monday-to-Monday week (UTC) before now, at every
11
+ // boundary of the week.
12
+ // 2. the statements: both account reads carry the leaderboard's NUMBERS predicate
13
+ // (rollupVisibleSql) and not the recruiting one; the winner needs a shipped work
14
+ // and ties go to works, then karma, then handle; the write never overwrites.
15
+ // 3. crown at render: a recorded week is never recomputed; an unrecorded one is
16
+ // computed once and written once, however many reads arrive together; a week
17
+ // nobody won is written with no winner; a failed crowning is retried next read.
18
+ // 4. the read: the crown only for this week's row, the past weeks newest first, at
19
+ // most ARCHIVE_WEEKS of them, and no github_id anywhere.
20
+ // 5. the route: no-store, the same body with or without credentials, the 500
21
+ // envelope, and its own rate-limit scope.
22
+ //
23
+ // Run: node tests/builder_of_the_week.mjs
24
+
25
+ import { strict as assert } from 'node:assert';
26
+ import { createRequire } from 'node:module';
27
+ import http from 'node:http';
28
+
29
+ const require = createRequire(import.meta.url);
30
+ const express = require('express');
31
+
32
+ let passed = 0;
33
+ let failed = 0;
34
+ async function t(name, fn) {
35
+ try { await fn(); passed++; console.log(` ok ${name}`); }
36
+ catch (e) { failed++; console.error(` FAIL ${name}\n ${e.message}`); }
37
+ }
38
+
39
+ const { rollupVisibleSql, scoutingListableSql } = require('../modules/platform-identity/account-visibility.js');
40
+
41
+ // ---- fake engine ------------------------------------------------------------
42
+
43
+ const PROBE = /^SELECT 1 FROM platform_identity_weekly_features WHERE week_start = \$1::date$/;
44
+ const isWinner = (sql) => /platform_identity_activity_snapshots/.test(sql);
45
+ const isInsert = (sql) => /^INSERT INTO platform_identity_weekly_features/.test(sql);
46
+ const isRead = (sql) => /FROM platform_identity_weekly_features f\s+JOIN platform_identity_accounts pa/.test(sql);
47
+
48
+ const engine = {
49
+ calls: [],
50
+ recorded: new Set(), // weeks the probe finds
51
+ winner: null, // what the winner statement returns
52
+ shown: [], // what the read returns (already filtered, newest first)
53
+ throwOn: null, // a predicate over the SQL that makes a call fail once
54
+ slowWinner: false,
55
+ query: async (text, params = []) => {
56
+ const sql = String(text).replace(/\s+/g, ' ').trim();
57
+ engine.calls.push({ sql, params });
58
+ if (engine.throwOn && engine.throwOn(sql)) { engine.throwOn = null; throw new Error('boom'); }
59
+ if (PROBE.test(sql)) return { rows: engine.recorded.has(params[0]) ? [{ '?column?': 1 }] : [] };
60
+ if (isWinner(sql)) {
61
+ if (engine.slowWinner) await new Promise((r) => setTimeout(r, 10));
62
+ return { rows: engine.winner ? [engine.winner] : [] };
63
+ }
64
+ if (isInsert(sql)) { engine.recorded.add(params[0]); return { rows: [], rowCount: 1 }; }
65
+ if (isRead(sql)) return { rows: engine.shown.slice(0, params[1]) };
66
+ throw new Error(`fake pool: unexpected statement ${sql.slice(0, 80)}`);
67
+ },
68
+ };
69
+ function reset() {
70
+ engine.calls = []; engine.recorded = new Set(); engine.winner = null; engine.shown = [];
71
+ engine.throwOn = null; engine.slowWinner = false;
72
+ botw.clearCrownMemo();
73
+ }
74
+ const callsOf = (pred) => engine.calls.filter((c) => pred(c.sql));
75
+
76
+ // Pin the module doorway (ADR 0083) BEFORE the modules load.
77
+ const moduleApiPath = require.resolve('../src/module-api.js');
78
+ require.cache[moduleApiPath] = {
79
+ id: moduleApiPath, filename: moduleApiPath, loaded: true,
80
+ exports: {
81
+ pool: engine,
82
+ corsPublicGet: () => (_req, res, next) => { res.set('Access-Control-Allow-Origin', '*'); next(); },
83
+ logger: () => ({ error: () => {}, info: () => {}, warn: () => {} }),
84
+ },
85
+ };
86
+
87
+ const botw = require('../modules/platform-identity/builder-of-the-week.js');
88
+ const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
89
+ const buildRoutes = require('../modules/platform-identity/routes/community-leaderboard.js');
90
+
91
+ const NOW = new Date('2026-10-28T09:30:00.000Z'); // a Wednesday
92
+ const shownRow = (week_start, handle, credits, ships) => ({
93
+ week_start, handle, github_login: handle, display_name: handle.toUpperCase(), avatar_url: null,
94
+ credits_delta: String(credits), ships_delta: String(ships),
95
+ });
96
+
97
+ // ---- 1. the week ------------------------------------------------------------
98
+
99
+ await t('the crowned week is the last full Monday-to-Monday week (UTC) before now, at every edge', () => {
100
+ const week = (iso) => { const w = botw.crownedWeek(new Date(iso)); return [w.start.toISOString(), w.end.toISOString()]; };
101
+ const last = ['2026-10-19T00:00:00.000Z', '2026-10-26T00:00:00.000Z'];
102
+ assert.deepEqual(week('2026-10-28T09:30:00.000Z'), last, 'a Wednesday: the week before this one');
103
+ assert.deepEqual(week('2026-10-26T00:00:00.000Z'), last, 'the very first instant of Monday: the week that just ended');
104
+ assert.deepEqual(week('2026-11-01T23:59:59.999Z'), last, 'the last instant of Sunday: still the same week');
105
+ assert.deepEqual(week('2026-10-25T23:59:59.999Z'), ['2026-10-12T00:00:00.000Z', '2026-10-19T00:00:00.000Z'], 'Sunday night: the week ending tonight is not over yet');
106
+ assert.deepEqual(week('2027-01-01T12:00:00.000Z'), ['2026-12-21T00:00:00.000Z', '2026-12-28T00:00:00.000Z'], 'across a year');
107
+ });
108
+
109
+ // ---- 2. the statements --------------------------------------------------------
110
+
111
+ await t('the winner: eligible builders only, a shipped work required, ties by works then karma then handle', async () => {
112
+ reset();
113
+ await botw.readBuilderOfTheWeek({ now: NOW, pool: engine });
114
+ const [w] = callsOf(isWinner);
115
+ assert.ok(w.sql.includes(rollupVisibleSql('pa').replace(/\s+/g, ' ').trim()), 'the leaderboard’s numbers predicate (hide, private and provisional are out)');
116
+ assert.ok(!w.sql.includes(scoutingListableSql('pa').replace(/\s+/g, ' ').trim()), 'the recruiting opt-out does not apply (spec D3)');
117
+ assert.match(w.sql, /HAVING sum\(tasks_shipped\) >= 1/, 'at least one work shipped in the week');
118
+ assert.match(w.sql, /ORDER BY sum\(credits\) DESC, sum\(tasks_shipped\) DESC, sum\(karma\) DESC, handle COLLATE "C", github_login COLLATE "C" LIMIT 1/);
119
+ assert.match(w.sql, /s\.reported_at <= \$2::timestamptz/, 'the week’s end: the last snapshot at-or-before it');
120
+ assert.match(w.sql, /s\.reported_at <= \$1::timestamptz/, 'the week’s start: the last snapshot at-or-before it');
121
+ assert.match(w.sql, /WITH act AS \( SELECT a\.client_id, a\.github_id, a\.first_seen_at, pa\.handle, pa\.github_login FROM/, 'never the live totals: a past week is read from history');
122
+ assert.match(w.sql, /\) b ON true WHERE b\.credits IS NOT NULL OR act\.first_seen_at >= \$1::timestamptz \)/, 'no history before the week: counted only if first seen in it (no lifetime in one week)');
123
+ assert.deepEqual(w.params.map((d) => d.toISOString()), ['2026-10-19T00:00:00.000Z', '2026-10-26T00:00:00.000Z']);
124
+ });
125
+
126
+ await t('the record: written once, never overwritten', async () => {
127
+ reset();
128
+ engine.winner = { github_id: '42', credits: '900', tasks_shipped: '3' };
129
+ await botw.readBuilderOfTheWeek({ now: NOW, pool: engine });
130
+ const [ins] = callsOf(isInsert);
131
+ assert.match(ins.sql, /ON CONFLICT \(week_start\) DO NOTHING$/, 'the first decision stands');
132
+ assert.doesNotMatch(ins.sql, /DO UPDATE/);
133
+ assert.deepEqual(ins.params, ['2026-10-19', '42', '900', '3']);
134
+ });
135
+
136
+ await t('the read: the same predicate, at read time, and no github_id in the projection', async () => {
137
+ reset();
138
+ engine.recorded.add('2026-10-19');
139
+ await botw.readBuilderOfTheWeek({ now: NOW, pool: engine });
140
+ const [r] = callsOf(isRead);
141
+ assert.ok(r.sql.includes(rollupVisibleSql('pa').replace(/\s+/g, ' ').trim()), 'a winner who hid since is not drawn (spec D8)');
142
+ const cols = r.sql.match(/^SELECT (.+?) FROM /)[1];
143
+ assert.doesNotMatch(cols, /github_id/, 'github_id never crosses');
144
+ assert.deepEqual(r.params, ['2026-10-19', botw.ARCHIVE_WEEKS + 1]);
145
+ });
146
+
147
+ // ---- 3. crown at render ---------------------------------------------------------
148
+
149
+ await t('a recorded week is never recomputed', async () => {
150
+ reset();
151
+ engine.recorded.add('2026-10-19');
152
+ await botw.readBuilderOfTheWeek({ now: NOW, pool: engine });
153
+ assert.equal(callsOf(isWinner).length, 0);
154
+ assert.equal(callsOf(isInsert).length, 0);
155
+ await botw.readBuilderOfTheWeek({ now: NOW, pool: engine });
156
+ assert.equal(callsOf((s) => PROBE.test(s)).length, 1, 'and once known, not even asked about again in this process');
157
+ });
158
+
159
+ await t('an unrecorded week is computed and written once, however many reads arrive together', async () => {
160
+ reset();
161
+ engine.slowWinner = true;
162
+ engine.winner = { github_id: '7', credits: '50', tasks_shipped: '1' };
163
+ await Promise.all([1, 2, 3, 4].map(() => botw.readBuilderOfTheWeek({ now: NOW, pool: engine })));
164
+ assert.equal(callsOf(isWinner).length, 1);
165
+ assert.equal(callsOf(isInsert).length, 1);
166
+ assert.equal(callsOf(isRead).length, 4, 'every read still reads the crown');
167
+ });
168
+
169
+ await t('a week nobody won is written with no winner, so it stays empty', async () => {
170
+ reset();
171
+ const out = await botw.readBuilderOfTheWeek({ now: NOW, pool: engine });
172
+ const [ins] = callsOf(isInsert);
173
+ assert.deepEqual(ins.params, ['2026-10-19', null, 0, 0]);
174
+ assert.equal(out.winner, null);
175
+ });
176
+
177
+ await t('a failed crowning is not remembered: the next read tries again', async () => {
178
+ reset();
179
+ engine.throwOn = isWinner;
180
+ await assert.rejects(() => botw.readBuilderOfTheWeek({ now: NOW, pool: engine }), /boom/);
181
+ assert.equal(callsOf(isInsert).length, 0, 'nothing written on a failure');
182
+ await botw.readBuilderOfTheWeek({ now: NOW, pool: engine });
183
+ assert.equal(callsOf(isInsert).length, 1);
184
+ });
185
+
186
+ await t('a new week is crowned on its own first read', async () => {
187
+ reset();
188
+ engine.recorded.add('2026-10-19');
189
+ await botw.readBuilderOfTheWeek({ now: NOW, pool: engine });
190
+ await botw.readBuilderOfTheWeek({ now: new Date('2026-11-02T00:00:01.000Z'), pool: engine });
191
+ assert.deepEqual(callsOf(isInsert).map((c) => c.params[0]), ['2026-10-26']);
192
+ });
193
+
194
+ // ---- 4. the read ----------------------------------------------------------------
195
+
196
+ await t('the crown is this week’s row only; the past weeks are newest first, capped', async () => {
197
+ reset();
198
+ engine.recorded.add('2026-10-19');
199
+ engine.shown = [shownRow('2026-10-19', 'ana', 1240, 4), shownRow('2026-10-12', 'bo', 300, 1)];
200
+ const out = await botw.readBuilderOfTheWeek({ now: NOW, pool: engine });
201
+ assert.deepEqual(out.week, { start: '2026-10-19', end: '2026-10-26' });
202
+ assert.deepEqual(out.winner, { week_start: '2026-10-19', handle: 'ana', github_login: 'ana', display_name: 'ANA', avatar_url: null, credits: 1240, tasks_shipped: 4 });
203
+ assert.deepEqual(out.past.map((p) => [p.week_start, p.handle, p.credits]), [['2026-10-12', 'bo', 300]]);
204
+ // this week's winner hid since (or nobody won): no crown, and last week does not move up into it
205
+ engine.shown = [shownRow('2026-10-12', 'bo', 300, 1)];
206
+ const hidden = await botw.readBuilderOfTheWeek({ now: NOW, pool: engine });
207
+ assert.equal(hidden.winner, null);
208
+ assert.deepEqual(hidden.past.map((p) => p.week_start), ['2026-10-12']);
209
+ engine.shown = Array.from({ length: 20 }, (_, i) => shownRow(`2026-0${(i % 9) + 1}-01`, `b${i}`, 10, 1));
210
+ assert.equal((await botw.readBuilderOfTheWeek({ now: NOW, pool: engine })).past.length, botw.ARCHIVE_WEEKS);
211
+ });
212
+
213
+ // ---- 5. the route -----------------------------------------------------------------
214
+
215
+ function serve() {
216
+ const app = express();
217
+ app.use(attachFail);
218
+ app.use('/api/bongos', buildRoutes());
219
+ const server = app.listen(0, '127.0.0.1');
220
+ return new Promise((resolve) => server.once('listening', () => resolve(server)));
221
+ }
222
+ function get(server, path_, headers = {}) {
223
+ return new Promise((resolve, reject) => {
224
+ http.get({ host: '127.0.0.1', port: server.address().port, path: path_, headers }, (res) => {
225
+ let body = '';
226
+ res.on('data', (c) => (body += c));
227
+ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body, json: body ? JSON.parse(body) : null }));
228
+ }).on('error', reject);
229
+ });
230
+ }
231
+
232
+ const server = await serve();
233
+ const URL_ = '/api/bongos/community/builder-of-the-week';
234
+
235
+ await t('GET /community/builder-of-the-week: the week, the crown and the past weeks; no-store; the same for every viewer', async () => {
236
+ reset();
237
+ engine.shown = [shownRow(botw.crownedWeek().start.toISOString().slice(0, 10), 'ana', 1240, 4)];
238
+ const anon = await get(server, URL_);
239
+ assert.equal(anon.status, 200);
240
+ assert.equal(anon.headers['cache-control'], 'no-store');
241
+ assert.equal(anon.headers['access-control-allow-origin'], '*');
242
+ assert.deepEqual(Object.keys(anon.json).sort(), ['past', 'week', 'winner']);
243
+ assert.equal(anon.json.winner.handle, 'ana');
244
+ assert.doesNotMatch(anon.body, /github_id/);
245
+ const signedIn = await get(server, URL_, { Cookie: 'bongos_session=x', Authorization: 'Bearer y' });
246
+ assert.equal(signedIn.body, anon.body, 'the route reads no session');
247
+ });
248
+
249
+ await t('a failure is the 500 envelope, never a stack', async () => {
250
+ reset();
251
+ engine.throwOn = (sql) => PROBE.test(sql);
252
+ const r = await get(server, URL_);
253
+ assert.equal(r.status, 500);
254
+ assert.equal(r.json.error.code, 'builder_of_the_week_failed');
255
+ assert.doesNotMatch(r.body, /boom/);
256
+ });
257
+
258
+ await t('its own rate-limit budget, not the leaderboard’s', () => {
259
+ const { _internals } = buildRoutes;
260
+ assert.equal(_internals.WEEK_READ_RATE_SCOPE, 'builder-of-the-week-read');
261
+ assert.notEqual(_internals.WEEK_READ_RATE_SCOPE, _internals.READ_RATE_SCOPE);
262
+ });
263
+
264
+ server.close();
265
+ console.log(`\n${passed} passed, ${failed} failed`);
266
+ if (failed) process.exit(1);
@@ -0,0 +1,166 @@
1
+ // tests/builder_of_the_week_db.mjs — task 1002981 (community spec D5; real DB).
2
+ //
3
+ // Proves Builder of the Week against real SQL: which snapshots open and close the
4
+ // week, the shipped-work floor, the tie order, the leaderboard's eligibility, that
5
+ // the first decision is pinned (a later change, a hide, a race) and that a week
6
+ // nobody won is recorded empty. A fake pool can only prove the statements' shape
7
+ // (tests/builder_of_the_week.mjs).
8
+ //
9
+ // The crown is global (every eligible builder on the hub), so every fixture week is
10
+ // in 2099: a shared database's real builders have no snapshot there, their week's
11
+ // delta is zero, and the shipped-work floor keeps them out.
12
+ //
13
+ // Real-DB integration test: self-skips without a reachable Postgres; in the
14
+ // INTEGRATION set (scripts/bongos/run-unit-tests.js) so the DB-free lane never runs it.
15
+ //
16
+ // Run: DATABASE_URL=... node tests/builder_of_the_week_db.mjs
17
+
18
+ import { createRequire } from 'node:module';
19
+ import { readFileSync } from 'node:fs';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { dirname, join } from 'node:path';
22
+ import { strict as assert } from 'node:assert';
23
+ const require = createRequire(import.meta.url);
24
+ const { Pool } = require('pg');
25
+ const { readBuilderOfTheWeek, crownedWeek, clearCrownMemo } = require('../modules/platform-identity/builder-of-the-week.js');
26
+
27
+ const here = dirname(fileURLToPath(import.meta.url));
28
+ const MIG_DIR = join(here, '..', 'modules', 'platform-identity', 'migrations');
29
+ const migrationSql = (f) => readFileSync(join(MIG_DIR, f), 'utf8');
30
+
31
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
32
+ try {
33
+ await pool.query('SELECT 1');
34
+ } catch (e) {
35
+ console.log(`builder_of_the_week_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
36
+ await pool.end().catch(() => {});
37
+ process.exit(0);
38
+ }
39
+
40
+ const A = 90298101, B = 90298102, C = 90298103, D = 90298104;
41
+ const HIDDEN = 90298105, PRIVATE = 90298106, E = 90298107;
42
+ const IDS = [A, B, C, D, E, HIDDEN, PRIVATE];
43
+ const P1 = 't1002981-p1', P2 = 't1002981-p2';
44
+ const DAY = 24 * 60 * 60 * 1000;
45
+ const NOW = new Date('2099-03-12T10:00:00.000Z');
46
+ const { start, end } = crownedWeek(NOW);
47
+ const at = (base, ms) => new Date(base.getTime() + ms);
48
+ const q = (sql, params) => pool.query(sql, params);
49
+
50
+ async function cleanup() {
51
+ await q(`DELETE FROM platform_identity_weekly_features WHERE week_start >= '2099-01-01'`).catch(() => {});
52
+ await q(`DELETE FROM platform_identity_activity_snapshots WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
53
+ await q(`DELETE FROM platform_identity_builder_project_activity WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
54
+ await q(`DELETE FROM platform_identity_accounts WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
55
+ }
56
+ async function account(id, handle, { visibility = 'public', hide = false } = {}) {
57
+ await q(
58
+ `INSERT INTO platform_identity_accounts
59
+ (github_id, github_login, display_name, handle, profile_state, account_visibility, hide_stats, terms_accepted_at)
60
+ VALUES ($1, $2, $2, $2, 'active', $3, $4, now())`,
61
+ [id, handle, visibility, hide],
62
+ );
63
+ }
64
+ // The live row (its numbers do not matter to a past week; first seen long before the
65
+ // week unless said) and a history of snapshots.
66
+ async function builder(id, client, history, firstSeen = at(start, -365 * DAY)) {
67
+ await q(`INSERT INTO platform_identity_builder_project_activity (client_id, github_id, credits, tasks_shipped, karma, first_seen_at) VALUES ($1, $2, 0, 0, 0, $3)`, [client, id, firstSeen]);
68
+ for (const [when, credits, ships, karma] of history) {
69
+ await q(
70
+ `INSERT INTO platform_identity_activity_snapshots (client_id, github_id, credits, tasks_shipped, karma, reported_at) VALUES ($1, $2, $3, $4, $5, $6)`,
71
+ [client, id, credits, ships, karma, when],
72
+ );
73
+ }
74
+ }
75
+ const recorded = async (week) => (await q(
76
+ `SELECT github_id::text AS github_id, credits_delta::int AS credits, ships_delta::int AS ships FROM platform_identity_weekly_features WHERE week_start = $1::date`,
77
+ [week],
78
+ )).rows;
79
+ const read = (now = NOW) => { clearCrownMemo(); return readBuilderOfTheWeek({ now, pool }); };
80
+
81
+ let pass = 0;
82
+ const ok = (cond, msg) => { assert.ok(cond, msg); pass++; console.log(` ok - ${msg}`); };
83
+ const same = (actual, expected, msg) => { assert.deepEqual(actual, expected, msg); pass++; console.log(` ok - ${msg}`); };
84
+
85
+ try {
86
+ for (const f of [
87
+ 'platform_identity_001_tables.sql', 'platform_identity_005_builder_activity.sql',
88
+ 'platform_identity_007_profile_consent.sql', 'platform_identity_008_handle.sql',
89
+ 'platform_identity_012_account_privacy.sql', 'platform_identity_019_terms_acceptance.sql',
90
+ 'platform_identity_022_activity_snapshots.sql', 'platform_identity_032_weekly_features.sql',
91
+ ]) await q(migrationSql(f));
92
+ await cleanup();
93
+
94
+ await account(A, 't1002981-a');
95
+ await account(B, 't1002981-b');
96
+ await account(C, 't1002981-c');
97
+ await account(D, 't1002981-d');
98
+ await account(E, 't1002981-e');
99
+ await account(HIDDEN, 't1002981-hidden', { hide: true });
100
+ await account(PRIVATE, 't1002981-private', { visibility: 'private' });
101
+
102
+ // A: 350 credits, 3 works, no karma — across two projects. P1 has a snapshot before
103
+ // the week (the baseline); P2 has none but was first seen in the week (counts from
104
+ // zero). A snapshot a second AFTER the week closes is not the week's.
105
+ await builder(A, P1, [[at(start, -3 * DAY), 100, 5, 4], [at(start, 2 * DAY), 400, 7, 4], [at(end, 1000), 99999, 99, 4]]);
106
+ await builder(A, P2, [[at(start, 3 * DAY), 50, 1, 0]], at(start, 3 * DAY));
107
+ // E: a lifetime brought into the week — no history before it, and seen before it (or
108
+ // before first_seen_at existed, NULL). Counting from zero would crown E; neither counts.
109
+ await builder(E, P1, [[at(start, DAY), 90000, 40, 0]], null);
110
+ await builder(E, P2, [[at(start, DAY), 90000, 40, 0]]);
111
+ // C: the same credits and works as A, two more karma — C beats A on karma.
112
+ await builder(C, P1, [[at(start, -DAY), 0, 0, 0], [at(start, DAY), 360, 3, 2]]);
113
+ // D: the same credits, the most karma, but one work fewer — works come before karma.
114
+ await builder(D, P1, [[at(start, -DAY), 0, 0, 0], [at(start, DAY), 350, 2, 9]]);
115
+ // B: the most credits, but no work shipped in the week (the floor).
116
+ await builder(B, P1, [[at(start, -DAY), 0, 4, 0], [at(start, DAY), 900, 4, 0]]);
117
+ // HIDDEN and PRIVATE out-earn everyone, and the leaderboard may not show them.
118
+ await builder(HIDDEN, P1, [[at(start, -DAY), 0, 0, 0], [at(start, DAY), 5000, 5, 0]]);
119
+ await builder(PRIVATE, P1, [[at(start, -DAY), 0, 0, 0], [at(start, DAY), 5000, 5, 0]]);
120
+ // A snapshot exactly at the start belongs to the baseline: C's week is 360 - 10, not 360.
121
+ await q(`INSERT INTO platform_identity_activity_snapshots (client_id, github_id, credits, tasks_shipped, karma, reported_at) VALUES ($1, $2, 10, 0, 0, $3)`, [P1, C, start]);
122
+
123
+ // 1 — the crown, and the row that records it
124
+ const first = await read();
125
+ same(first.week, { start: start.toISOString().slice(0, 10), end: end.toISOString().slice(0, 10) }, 'the week is the last full Monday-to-Monday week');
126
+ same(first.winner && [first.winner.handle, first.winner.credits, first.winner.tasks_shipped], ['t1002981-c', 350, 3], 'ties go to works shipped, then karma; no work, hidden, private and a lifetime brought into the week never win');
127
+ ok(!('github_id' in first.winner), 'github_id never crosses');
128
+ same(await recorded(first.week.start), [{ github_id: String(C), credits: 350, ships: 3 }], 'the week is recorded on the first read');
129
+
130
+ // 2 — pinned: a later report into the past week, or a builder who goes public, changes nothing
131
+ await q(`INSERT INTO platform_identity_activity_snapshots (client_id, github_id, credits, tasks_shipped, karma, reported_at) VALUES ($1, $2, 99999, 50, 0, $3)`, [P1, A, at(start, 4 * DAY)]);
132
+ await q(`UPDATE platform_identity_accounts SET hide_stats = false WHERE github_id = $1`, [HIDDEN]);
133
+ same((await read()).winner.handle, 't1002981-c', 'the first decision stands');
134
+ await q(`UPDATE platform_identity_accounts SET hide_stats = true WHERE github_id = $1`, [HIDDEN]);
135
+
136
+ // 3 — a winner who hides is not shown, and their week goes to nobody else
137
+ await q(`UPDATE platform_identity_accounts SET hide_stats = true WHERE github_id = $1`, [C]);
138
+ same((await read()).winner, null, 'a hidden winner is not drawn');
139
+ same(await recorded(first.week.start), [{ github_id: String(C), credits: 350, ships: 3 }], 'and the row is never re-crowned');
140
+
141
+ // 4 — a later week nobody shipped in (two on, past A's late snapshot) is recorded empty;
142
+ // the crowned week is in its past list once its winner is shown again
143
+ const next = at(NOW, 14 * DAY);
144
+ const empty = await read(next);
145
+ same(empty.winner, null, 'a week nobody won has no crown');
146
+ same(await recorded(empty.week.start), [{ github_id: null, credits: 0, ships: 0 }], 'and is recorded empty, so it stays empty');
147
+ same(empty.past, [], 'the week before is left out while its winner hides');
148
+ await q(`UPDATE platform_identity_accounts SET hide_stats = false WHERE github_id = $1`, [C]);
149
+ same((await read(next)).past.map((p) => [p.week_start, p.handle]), [[first.week.start, 't1002981-c']], 'shown again, it is back in the past weeks');
150
+
151
+ // 5 — two hub processes crowning the same week at once: one row, no error
152
+ const third = at(NOW, 21 * DAY);
153
+ const other = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
154
+ try {
155
+ clearCrownMemo();
156
+ const both = await Promise.all([readBuilderOfTheWeek({ now: third, pool }), readBuilderOfTheWeek({ now: third, pool: other })]);
157
+ same(both.map((b) => b.winner), [null, null], 'both answer');
158
+ same((await recorded(both[0].week.start)).length, 1, 'one row: ON CONFLICT DO NOTHING');
159
+ } finally { await other.end().catch(() => {}); }
160
+
161
+ console.log(`\nbuilder_of_the_week_db.mjs: ${pass} passed`);
162
+ } finally {
163
+ clearCrownMemo();
164
+ await cleanup();
165
+ await pool.end().catch(() => {});
166
+ }
@@ -188,14 +188,19 @@ await test('open: the queue\'s head, the applied page\'s render, and the words b
188
188
  assert.ok(b.classes.has('aq-open'), 'the page behind stops scrolling');
189
189
  assert.equal(text(b.q('.aq-eyebrow')), 'approval queue · 1 of 2');
190
190
  assert.equal(text(b.q('.aq-title')), 'Home · landing · 1 line rewritten, applied · 1 not applied');
191
+ assert.equal(b.q('img'), null, 'the words lead: no picture until the artist asks for it');
192
+ assert.equal(b.requests.filter((r) => r.url.endsWith('/visuals')).length, 0, 'and none is fetched until then');
193
+ await b.click(b.q('[data-peek]'));
191
194
  const img = b.q('img');
192
195
  assert.equal(img.attrs.src, url(41, 'after-desktop-light'), 'the AFTER render, desktop, in the hall\'s own mode');
193
196
  assert.equal(img.attrs.alt, 'Home, after, desktop, light');
194
197
  assert.deepEqual(b.qa('[data-phase]').map((x) => [text(x), x.attrs['aria-pressed']]), [['before', 'false'], ['after', 'true']]);
198
+ await b.click(b.q('[data-peek]'));
199
+ assert.equal(b.q('img'), null, 'and it folds away again');
195
200
  assert.equal(b.qa('mark').length, 1, 'one applied line carries the mark');
196
201
  assert.equal(text(b.q('mark')), 'Agents write the code.');
197
202
  assert.match(text(b.q('.aq-line--refused')), /not applied: the page no longer shows it/);
198
- assert.equal(b.body.querySelectorAll('img').length, 1, 'a hostile line is text, never markup');
203
+ assert.equal(b.body.querySelectorAll('img').length, 0, 'a hostile line is text, never markup');
199
204
  assert.equal(b.win.pwned, undefined);
200
205
  assert.equal(text(b.q('.aq-hint')), 'highlighted: the words you changed');
201
206
  assert.deepEqual(b.counts, [2], 'the door hears how many wait');
@@ -205,9 +210,10 @@ await test('open: the queue\'s head, the applied page\'s render, and the words b
205
210
  await test('the switches: before and after, desktop and phone, light and dark, each a render of the applied branch', async () => {
206
211
  const b = bootPanel({ answers: { [APPROVALS]: queueFor(), [vis(41)]: slotsFor(41) } });
207
212
  await b.panel.open(); await settle(); await settle();
213
+ await b.click(b.q('[data-peek]'));
208
214
  await b.click(b.q('[data-phase="before"]'));
209
215
  assert.equal(b.q('img').attrs.src, url(41, 'before-desktop-light'));
210
- assert.equal(b.qa('mark').length, 0, 'before is the page as it was: nothing marked');
216
+ assert.equal(b.qa('mark').length, 1, 'the words beside the picture do not change with it');
211
217
  await b.click(b.q('[data-device="phone"]'));
212
218
  assert.equal(b.q('img').attrs.src, url(41, 'before-phone-light'));
213
219
  assert.match(b.q('img').attrs.class, /aq-render--phone/);
@@ -216,17 +222,19 @@ await test('the switches: before and after, desktop and phone, light and dark, e
216
222
  assert.equal(b.requests.filter((r) => r.url.endsWith('/visuals')).length, 1, 'the renders are read once per round');
217
223
  });
218
224
 
219
- await test('a missing render says what it shows instead; no renders at all means nothing to approve from', async () => {
225
+ await test('a missing render says what it shows instead; no renders at all never blocks the decision', async () => {
220
226
  const b = bootPanel({ answers: { [APPROVALS]: queueFor(), [vis(41)]: slotsFor(41, ['after-desktop-dark']) } });
221
227
  await b.panel.open(); await settle(); await settle();
228
+ await b.click(b.q('[data-peek]'));
222
229
  assert.equal(b.q('img').attrs.src, url(41, 'after-desktop-dark'));
223
230
  assert.equal(text(b.q('.aq-frame__swap')), 'No desktop light render; showing desktop dark.');
224
231
  const none = bootPanel({ answers: { [APPROVALS]: queueFor(), [vis(41)]: slotsFor(41, []) } });
225
232
  await none.panel.open(); await settle(); await settle();
233
+ assert.ok(!none.q('[data-approve]').disabled, 'the words are what is approved: no picture is needed');
234
+ await none.click(none.q('[data-peek]'));
226
235
  assert.equal(none.q('img'), null);
227
236
  assert.match(text(none.q('.aq-frame__note')), /No render is attached/);
228
- assert.ok(none.q('[data-approve]').disabled, 'approve waits for a picture of the page');
229
- assert.ok(!none.q('[data-open-note]').disabled, 'sending it back does not');
237
+ assert.ok(!none.q('[data-open-note]').disabled, 'sending it back is open too');
230
238
  });
231
239
 
232
240
  await test('approve and ship: one POST through the host\'s write, then the artboard\'s line and the next page', async () => {
@@ -297,6 +305,24 @@ await test('a builder who may not decide reads the queue with a sentence, not tw
297
305
  assert.match(text(b.q('.aq-sealed')), /Only the artist who wrote this page, or another artist/);
298
306
  });
299
307
 
308
+ await test('open(pageId) lands on that page card, says so when it is not in the queue, and can skip the second read', async () => {
309
+ const b = bootPanel({ answers: { [APPROVALS]: queueFor(), [vis(42)]: slotsFor(42) } });
310
+ await b.panel.open('landing:projects');
311
+ await settle(); await settle();
312
+ assert.equal(text(b.q('.aq-eyebrow')), 'approval queue · 2 of 2', 'the Projects card, not the head');
313
+ const miss = bootPanel({ answers: { [APPROVALS]: queueFor() } });
314
+ await miss.panel.open('landing:nowhere');
315
+ await settle(); await settle();
316
+ assert.equal(text(miss.q('.aq-eyebrow')), 'approval queue · 1 of 2');
317
+ assert.equal(text(miss.q('.aq-error')), 'That page is not waiting for your sign-off right now.');
318
+ const once = bootPanel({ answers: { [APPROVALS]: queueFor() } });
319
+ await once.panel.refresh();
320
+ await once.panel.open(undefined, { reload: false });
321
+ await settle(); await settle();
322
+ assert.equal(once.requests.filter((r) => r.url.endsWith('/copy-desk/approvals')).length, 1, 'refreshed once, opened without re-reading');
323
+ assert.equal(once.panel.notAppliedCount(), 0);
324
+ });
325
+
300
326
  await test('empty, closed with Escape, and focus goes back where it was', async () => {
301
327
  const b = bootPanel({ answers: { [APPROVALS]: queueFor({ tasks: [] }) } });
302
328
  const opener = { focus() { boot.focused = opener; } };
@@ -391,9 +417,6 @@ await test('a line holding {{productName}} reads as the product name in a chip,
391
417
  assert.ok(chips.every((c) => c.attrs['data-hole'] === 'productName' && text(c) === 'Cloud Bongos'));
392
418
  assert.ok(b.q('mark').querySelector('.aq-hole'), 'the chip sits inside the changed words\' mark');
393
419
  assert.ok(!/\{\{/.test(b.scrim.innerHTML || '') && !/\{\{/.test(text(b.scrim)), 'no raw {{token}} anywhere in the panel');
394
- b.scrim.fire('click', { target: b.q('[data-phase="before"]') }); await settle();
395
- assert.equal(lineWords(), 'Both are how Cloud Bongos itself works.');
396
- assert.equal(asideWords(), 'becomes: This project, Cloud Bongos, has two doors.');
397
420
  });
398
421
 
399
422
  await test('the dialog fits the viewport at every width: capped by the screen, the page shrinks first, rows wrap', () => {
@@ -411,7 +434,8 @@ await test('the dialog fits the viewport at every width: capped by the screen, t
411
434
  assert.match(rule('.aq-foot'), /flex-wrap: wrap/);
412
435
  assert.match(rule('.aq-body'), /min-width: 0/);
413
436
  assert.match(rule('.aq-frame'), /flex-grow: 1;[\s\S]*min-width: 0/, 'the rendered page is what shrinks first');
414
- assert.match(rule('.aq-lines'), /width: clamp\(240px, 32%, 300px\)/, 'the words keep the artboard\'s 300 until the page has shrunk');
437
+ assert.match(rule('.aq-lines'), /min-width: 0/, 'the words take the full width and can shrink');
438
+ assert.match(rule('.aq-line'), /grid-template-columns: minmax\(0, 1fr\) minmax\(0, 1fr\)/, 'was and now sit side by side');
415
439
  assert.match(rule('.aq-line__text'), /overflow-wrap: anywhere/, 'a long word breaks inside its column');
416
440
  });
417
441