@bongos/core 1.23.18 → 1.23.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.
Files changed (60) hide show
  1. package/.bongos-core.json +134 -49
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +4 -0
  4. package/clients/bongos-client/index.cjs +4 -0
  5. package/clients/bongos-client/index.d.ts +4 -0
  6. package/clients/bongos-client/index.mjs +4 -0
  7. package/docs/api/openapi.json +43 -1
  8. package/docs/api-reference.md +4 -2
  9. package/docs/copy-inventory.md +992 -935
  10. package/docs/copy-registry.json +1897 -1313
  11. package/docs/module-api-changelog.md +4 -0
  12. package/docs/page-inventory.json +12 -2
  13. package/docs/page-readings.json +616 -576
  14. package/modules/platform-identity/builder-of-the-week.js +61 -24
  15. package/modules/platform-identity/community-crowns.js +176 -0
  16. package/modules/platform-identity/community-leaderboard.js +4 -1
  17. package/modules/platform-identity/community-movers.js +152 -0
  18. package/modules/platform-identity/migrations/platform_identity_033_period_features.sql +43 -0
  19. package/modules/platform-identity/routes/community-leaderboard.js +45 -3
  20. package/modules/public-landing/public/account.html +3 -2
  21. package/modules/public-landing/public/community-crowns-first.states.json +15 -0
  22. package/modules/public-landing/public/community-crowns-new-year.states.json +15 -0
  23. package/modules/public-landing/public/community-crowns-none.states.json +15 -0
  24. package/modules/public-landing/public/community-empty.states.json +1 -1
  25. package/modules/public-landing/public/community-movers-none.states.json +15 -0
  26. package/modules/public-landing/public/community-owner-two.states.json +2 -2
  27. package/modules/public-landing/public/community-owner.states.json +1 -1
  28. package/modules/public-landing/public/community-week-none.states.json +1 -1
  29. package/modules/public-landing/public/community.html +188 -22
  30. package/modules/public-landing/public/community.probes.json +20 -3
  31. package/modules/public-landing/public/community.states.json +8 -6
  32. package/modules/public-landing/public/connections.html +3 -2
  33. package/modules/public-landing/public/contact.html +3 -2
  34. package/modules/public-landing/public/guild.html +3 -2
  35. package/modules/public-landing/public/index.html +3 -2
  36. package/modules/public-landing/public/invitations.html +3 -2
  37. package/modules/public-landing/public/privacy.html +3 -2
  38. package/modules/public-landing/public/projects.html +3 -2
  39. package/modules/public-landing/public/terms.html +3 -2
  40. package/modules/ui-design/kit/fixtures/community-crowns-first.json +6 -0
  41. package/modules/ui-design/kit/fixtures/community-crowns-new-year.json +6 -0
  42. package/modules/ui-design/kit/fixtures/community-crowns-none.json +6 -0
  43. package/modules/ui-design/kit/fixtures/community-crowns.json +6 -0
  44. package/modules/ui-design/kit/fixtures/community-movers-none.json +6 -0
  45. package/modules/ui-design/kit/fixtures/community-movers.json +12 -0
  46. package/modules/ui-design/kit/serve.js +22 -0
  47. package/package-lock.json +2 -2
  48. package/package.json +1 -1
  49. package/release-notes.json +12 -0
  50. package/scripts/bongos/run-unit-tests.js +8 -0
  51. package/src/module-api.js +1 -1
  52. package/src/platform-server.js +4 -3
  53. package/tests/community_crowns.mjs +369 -0
  54. package/tests/community_crowns_db.mjs +164 -0
  55. package/tests/community_movers.mjs +280 -0
  56. package/tests/community_movers_db.mjs +147 -0
  57. package/tests/hub_account_page.mjs +2 -2
  58. package/tests/hub_community_page.mjs +213 -29
  59. package/tests/landing_legal_pages.mjs +2 -2
  60. package/tests/privacy_matrix.mjs +107 -9
@@ -1,11 +1,12 @@
1
1
  // tests/hub_community_page.mjs — the hub's community pages (task 1003363; ADR 0217 D1,
2
2
  // ADR 0337 D1/D3/D4.1, docs/specs/bongos-v1-community-tab.md D1/D3/D4/D8).
3
3
  //
4
- // One file served as four sub-pages of the bar's Community menu (owner, 2026-10-27):
5
- // /community the leaderboard, /community/week the builder of the week (task 1002981),
6
- // /community/find the builder search, /community/recruiters the name-only list of hidden
7
- // builders open to recruiters. They render existing platform-identity reads and add no
8
- // rule of their own. What this file pins:
4
+ // One file served as five sub-pages of the bar's Community menu (owner, 2026-10-27):
5
+ // /community the community home (task 1004902: the builders of the week, month and year,
6
+ // and a door to each page), /community/leaderboard the leaderboard, /community/week the
7
+ // builder of the week (task 1002981), /community/find the builder search,
8
+ // /community/recruiters the name-only list of hidden builders open to recruiters. They
9
+ // render existing platform-identity reads and add no rule of their own. What this file pins:
9
10
  // • the markup: the hub's bar with the Community menu as the current section, no top
10
11
  // block, noindex, dark only, the routes' own windows and query limits
11
12
  // • viewing is ungated: the leaderboard and the search read the same routes for every
@@ -15,11 +16,13 @@
15
16
  // dropdown and the crafts, the empty, failed and rate-limited board; the search and
16
17
  // its refusals; the recruiter list as a door signed out, a refusal on a 403, and rows
17
18
  // only from a 200 — name, face and login, never the ranked half's numbers; the builder
18
- // of the week as one line on the board and as its own page, drawn as the server sent it
19
+ // of the week as one line on the board and as its own page, drawn as the server sent it;
20
+ // the home's three crowns, its empty and failed states, and its doors; the row under
21
+ // them (task 1002983), the rising builder and the projects on the move, from its own read
19
22
  // • the Community menu on every page that wears the hub's bar (apex-only where the page
20
23
  // also serves a child), folded into the Projects band on phones
21
- // • the routes: the four paths served on the hub's apex only
22
- // • the kit: the states files, and the stub answering as the four reads do
24
+ // • the routes: the five paths served on the hub's apex only
25
+ // • the kit: the states files, and the stub answering as the six reads do
23
26
  //
24
27
  // DB-free. Run: node --test tests/hub_community_page.mjs
25
28
 
@@ -46,8 +49,10 @@ const STYLE = PAGE.split('<style>')[1].split('</style>')[0].replace(/\/\*[\s\S]*
46
49
  const MAIN = PAGE.split('<main class="acct" id="comm">')[1].split('</main>')[0];
47
50
  const BOARD_JS = fs.readFileSync(path.join(PI, 'community-leaderboard.js'), 'utf8');
48
51
  const SEARCH_JS = fs.readFileSync(path.join(PI, 'builder-search.js'), 'utf8');
49
- const SUBPAGES = [['/community', 'Leaderboard'], ['/community/week', 'Builder of the week'], ['/community/find', 'Find a builder'], ['/community/recruiters', 'Open to recruiters']];
52
+ const SUBPAGES = [['/community', 'Community home'], ['/community/leaderboard', 'Leaderboard'], ['/community/week', 'Builder of the week'], ['/community/find', 'Find a builder'], ['/community/recruiters', 'Open to recruiters']];
50
53
  const WEEK_JS = fs.readFileSync(path.join(PI, 'builder-of-the-week.js'), 'utf8');
54
+ const CROWNS_JS = fs.readFileSync(path.join(PI, 'community-crowns.js'), 'utf8');
55
+ const MOVERS_JS = fs.readFileSync(path.join(PI, 'community-movers.js'), 'utf8');
51
56
 
52
57
  // ── 1. the markup ────────────────────────────────────────────────────────────
53
58
 
@@ -73,9 +78,9 @@ test('the bar and bands are /connections’ but for the sign-in return and the c
73
78
  assert.equal(bar(PAGE), bar(read('connections.html')));
74
79
  });
75
80
 
76
- test('no top block: one visually hidden h1, then one section per sub-page, only the leaderboard shown in the markup', () => {
81
+ test('no top block: one visually hidden h1, then one section per sub-page, only the home shown in the markup', () => {
77
82
  assert.match(MAIN, /^\s*<h1 class="vh">Community<\/h1>\s*(<!--[\s\S]*?-->\s*)?<div class="mineExtra">/, 'nothing is drawn above the first section (the /design floor)');
78
- const order = ['<section id="board" data-view="leaderboard" aria-labelledby="boardH">', '<section id="week" data-view="week" aria-labelledby="weekH" hidden>', '<section id="find" data-view="find" aria-labelledby="findH" hidden>', '<section id="recruit" data-view="recruiters" aria-labelledby="recruitH" hidden>'].map((s) => MAIN.indexOf(s));
83
+ const order = ['<section id="home" data-view="home" aria-labelledby="homeH">', '<section id="board" data-view="leaderboard" aria-labelledby="boardH" hidden>', '<section id="week" data-view="week" aria-labelledby="weekH" hidden>', '<section id="find" data-view="find" aria-labelledby="findH" hidden>', '<section id="recruit" data-view="recruiters" aria-labelledby="recruitH" hidden>'].map((s) => MAIN.indexOf(s));
79
84
  assert.ok(order.every((i) => i > 0), 'every sub-page is real markup the script only shows');
80
85
  assert.deepEqual([...order].sort((a, b) => a - b), order);
81
86
  assert.match(MAIN, /<ol class="commRows" id="boardList" data-unit="\{\{currency\}\}" aria-labelledby="boardH"><\/ol>/, 'the unit is the instance’s own word, stamped by the server');
@@ -100,10 +105,44 @@ test('the builder of the week states its rule in full, and the rule is the modul
100
105
  assert.match(WEEK_JS, /ORDER BY sum\(credits\) DESC, sum\(tasks_shipped\) DESC, sum\(karma\) DESC,/, 'the tie order the page states is the query’s');
101
106
  assert.match(rule, /Builders who hide their numbers or keep their account private are not counted\./);
102
107
  assert.match(WEEK_JS, /rollupVisibleSql\('pa'\)/);
103
- assert.match(rule, /Each week is decided once, the first time anyone opens this page after it ends, and it does not change after that\./);
108
+ assert.match(rule, /Each week is decided once, the first time a community page asks for it after it ends, and it does not change after that\./);
104
109
  assert.match(WEEK_JS, /ON CONFLICT \(week_start\) DO NOTHING/);
105
110
  });
106
111
 
112
+ test('the home states the month’s and year’s rule, and it is the module’s; its doors are the other pages (task 1004902)', () => {
113
+ const home = MAIN.split('<section id="home"')[1].split('</section>')[0];
114
+ const rule = home.split('<p class="commNote" id="homeRule">')[1].split('</p>')[0];
115
+ assert.match(rule, /the most \{\{currency\}\} across every project in that time, among the builders who shipped at least one work in it\./, 'the week’s formula, which the month and the year reuse');
116
+ assert.match(CROWNS_JS, /require\('\.\/builder-of-the-week'\)/);
117
+ assert.match(CROWNS_JS, /computeWinner\(start, end, pool, opts\)/, 'the month and the crowned year are the week’s statement');
118
+ assert.match(rule, /A tie goes to whoever shipped more works, then to whoever earned more karma\./);
119
+ assert.match(rule, /Builders who hide their numbers or keep their account private are not counted\./);
120
+ assert.match(CROWNS_JS, /rollupVisibleSql\('pa'\)/);
121
+ assert.match(rule, /The week and the month are decided once, when they end; the year shows the leader so far, and is decided on 1 January\./);
122
+ assert.match(CROWNS_JS, /ON CONFLICT \(period, period_start\) DO NOTHING/);
123
+ assert.match(CROWNS_JS, /computeWinner\(start, now, pool, \{ fromFirstSnapshot: true \}\)/, 'the year so far is live');
124
+ for (const id of ['homeFailed', 'homeCrowns', 'crownYearLast', 'homeMovers', 'moversRule', 'moveNone']) assert.match(home, new RegExp(` id="${id}"[^>]*hidden`), `${id} waits for the route’s answer`);
125
+ assert.ok(home.indexOf('id="homeMovers"') > home.indexOf('id="homeRule"') && home.indexOf('id="homeMovers"') < home.indexOf('id="doorsH"'), 'the row under the crowns and their rule, above the doors (owner decision 5, task 1004902)');
126
+ const doors = [...home.split('<ul class="homeDoors" aria-labelledby="doorsH">')[1].split('</ul>')[0].matchAll(/<li( class="knowCell" hidden)?><a class="btn ghost" href="([^"]+)">([^<]+)<\/a><\/li>/g)];
127
+ assert.deepEqual(doors.map((m) => [m[2], m[3], !!m[1]]), [
128
+ ['/community/leaderboard', 'Leaderboard', false], ['/community/week', 'Builder of the week', false], ['/community/find', 'Find a builder', false],
129
+ ['/community/recruiters', 'Open to recruiters', false], ['/connections#connSuggest', 'Builders you may know', true],
130
+ ], 'one door per page, the menu’s order; Builders you may know signed in only, like its menu item');
131
+ });
132
+
133
+ test('the movers’ rule is the module’s: the base cap, the shipped-work floor, the projects’ order and who is counted (task 1002983)', () => {
134
+ const rule = MAIN.split('<p class="commNote" id="moversRule" hidden>')[1].split('</p>')[0];
135
+ const cap = MOVERS_JS.match(/const RISING_BASE_MAX = (\d+);/)[1];
136
+ assert.match(rule, new RegExp(`the most \\{\\{currency\\}\\} in the last 30 days, among the builders who had earned under ${cap} \\{\\{currency\\}\\} before them and shipped at least one work in them\\.`));
137
+ assert.match(MOVERS_JS, /const WINDOW = '30d';/);
138
+ assert.match(MOVERS_JS, /s\.base < RISING_BASE_MAX && s\.tasks_shipped >= 1/, 'the cap and the floor the page states are the ones applied');
139
+ assert.match(rule, /Projects on the move are the public projects that shipped the most works in the last 30 days, then earned the most \{\{currency\}\}\./);
140
+ assert.match(MOVERS_JS, /\(b\.tasks_shipped - a\.tasks_shipped\) \|\| \(b\.credits - a\.credits\)/);
141
+ assert.match(MOVERS_JS, /onMapSql\('p'\)\} AND p\.visibility = 'public'/, '“public projects”: on the map with the Public door');
142
+ assert.match(rule, /Builders who hide their numbers or keep their account private are not counted in either\./);
143
+ assert.match(MOVERS_JS, /eligibleIdentities/);
144
+ });
145
+
107
146
  test('the window dropdown and the query limits are the routes’ own (boundary-forced copies)', () => {
108
147
  const days = BOARD_JS.match(/const WINDOW_DAYS = Object\.freeze\(\{([^}]*)\}\);/);
109
148
  const windows = [...days[1].matchAll(/'?(\w+)'?:/g)].map((m) => m[1]);
@@ -121,12 +160,12 @@ test('the window dropdown and the query limits are the routes’ own (boundary-f
121
160
  assert.match(MAIN, new RegExp(`<input id="findQ" type="search"[^>]* maxlength="${max}"`));
122
161
  });
123
162
 
124
- test('the script builds no markup from strings, reads only the five routes, and never re-ranks or reads the ranked tier', () => {
163
+ test('the script builds no markup from strings, reads only the seven routes, and never re-ranks or reads the ranked tier', () => {
125
164
  assert.doesNotMatch(SCRIPT, /innerHTML|insertAdjacentHTML|outerHTML|document\.write/);
126
- // every path literal in the code: the API base, the five reads, the four sub-pages, the
165
+ // every path literal in the code: the API base, the seven reads, the five sub-pages, the
127
166
  // sign-in door and the hall's invite box the Recruit door opens (task 1002292)
128
167
  const paths = [...new Set([...CODE.matchAll(/'(\/[^'?]*)/g)].map((m) => m[1]))];
129
- assert.deepEqual(paths.sort(), ['/api/bongos', '/api/bongos/auth/web/start', '/builders/watch', '/community', '/community/builder-of-the-week', '/community/find', '/community/leaderboard', '/community/recruiters', '/community/search', '/community/week', '/my-projects', '/scouting']);
168
+ assert.deepEqual(paths.sort(), ['/api/bongos', '/api/bongos/auth/web/start', '/builders/watch', '/community', '/community/builder-of-the-week', '/community/crowns', '/community/find', '/community/leaderboard', '/community/movers', '/community/recruiters', '/community/search', '/community/week', '/my-projects', '/scouting']);
130
169
  assert.doesNotMatch(CODE, /method:/, 'it only reads');
131
170
  assert.doesNotMatch(CODE, /'\/me\b/, 'who is signed in is the bar’s question');
132
171
  assert.doesNotMatch(CODE, /\.sort\(|\.count\b|_count\b/, 'the server ranks; every count is a list’s length');
@@ -172,7 +211,20 @@ const WEEK = () => ({ week: { start: '2026-10-19', end: '2026-10-26' },
172
211
  past: [{ week_start: '2026-10-12', handle: 'bo', github_login: 'bo', display_name: null, avatar_url: 'data:x', credits: 300, tasks_shipped: 2 },
173
212
  { week_start: '2026-09-28', handle: 'cy', github_login: 'cy', display_name: 'Cy', avatar_url: null, credits: 90, tasks_shipped: 1 },
174
213
  { week_start: '2025-12-29', handle: 'di', github_login: 'di', display_name: 'Di', avatar_url: null, credits: 10, tasks_shipped: 1 }] });
214
+ const WINNER = (handle, credits, ships, extra = {}) => ({ handle, github_login: handle, display_name: handle.toUpperCase(), avatar_url: `https://avatars.example/${handle}`, credits, tasks_shipped: ships, ...extra });
215
+ const CROWNS = () => ({
216
+ week: { start: '2026-10-26', end: '2026-11-02', winner: WINNER('ana', 1240, 4, { display_name: 'Ana <i>L</i>' }) },
217
+ month: { start: '2026-10-01', end: '2026-11-01', first_crown: null, winner: WINNER('bo', 6120, 1, { display_name: null, avatar_url: 'javascript:alert(1)' }) },
218
+ year: { start: '2026-01-01', since: '2026-09-27', last: null, winner: WINNER('cy', 9870, 31) },
219
+ });
220
+ const MOVERS = () => ({
221
+ window: { start: '2026-09-28T09:30:00.000Z', end: '2026-10-28T09:30:00.000Z' },
222
+ rising: WINNER('kofi', 640, 5, { display_name: 'Kofi <b>M</b>' }),
223
+ projects: [{ name: 'Tide <i>pool</i>', origin: 'https://tidepool.example', tasks_shipped: 23, credits: 1840 }, { name: 'Quill', origin: 'javascript:alert(1)', tasks_shipped: 1, credits: 45 }],
224
+ });
175
225
  const READS = () => ({
226
+ '/community/crowns': { status: 200, body: CROWNS() },
227
+ '/community/movers': { status: 200, body: MOVERS() },
176
228
  '/community/builder-of-the-week': { status: 200, body: WEEK() },
177
229
  '/community/leaderboard?window=all': { status: 200, body: { window: 'all', count: 99, results: [ROW(26, 'ana', 1200), ROW(27, 'bo', 1500, { tasks_shipped: 1, karma: 0, avatar_url: 'javascript:alert(1)', display_name: null })] } },
178
230
  '/community/leaderboard?window=30d': { status: 200, body: { window: '30d', results: [ROW(1, 'cy', 40)] } },
@@ -182,7 +234,7 @@ const READS = () => ({
182
234
  '/scouting': { status: 403, body: { error: { code: 'forbidden' } } },
183
235
  });
184
236
 
185
- async function boot({ pathname = '/community', me = null, reads = READS() } = {}) {
237
+ async function boot({ pathname = '/community/leaderboard', me = null, reads = READS() } = {}) {
186
238
  const ids = new Map();
187
239
  for (const m of PAGE.matchAll(/<(\w+)([^>]* id="([^"]+)"[^>]*)>/g)) {
188
240
  const attrs = attrsOf(m[2]);
@@ -224,17 +276,123 @@ async function boot({ pathname = '/community', me = null, reads = READS() } = {}
224
276
  return { ids, requests, document, gets: () => requests.map((r) => r.url), tab, craft, pick, submit, current, $: (id) => ids.get(id) };
225
277
  }
226
278
 
227
- test('each path shows its one section, marks its menu items current and names itself; any other shape is the leaderboard', async () => {
228
- for (const [pathname, title, section] of [['/community', 'Leaderboard', 'board'], ['/community/', 'Leaderboard', 'board'], ['/community/week', 'Builder of the week', 'week'], ['/community/find', 'Find a builder', 'find'], ['/community/recruiters/', 'Open to recruiters', 'recruit']]) {
279
+ test('each path shows its one section, marks its menu items current and names itself; any other shape is the home', async () => {
280
+ for (const [pathname, item, section] of [['/community', 'Community home', 'home'], ['/community/', 'Community home', 'home'], ['/community/everyone', 'Community home', 'home'], ['/community/leaderboard/', 'Leaderboard', 'board'], ['/community/week', 'Builder of the week', 'week'], ['/community/find', 'Find a builder', 'find'], ['/community/recruiters/', 'Open to recruiters', 'recruit']]) {
229
281
  const ui = await boot({ pathname });
230
- assert.deepEqual(['board', 'week', 'find', 'recruit'].filter((s) => !ui.$(s).hidden), [section], `${pathname}: one section`);
231
- const href = SUBPAGES.find(([, t]) => t === title)[0];
282
+ assert.deepEqual(['home', 'board', 'week', 'find', 'recruit'].filter((s) => !ui.$(s).hidden), [section], `${pathname}: one section`);
283
+ const href = SUBPAGES.find(([, t]) => t === item)[0];
232
284
  assert.deepEqual(ui.current(), [href, href], `${pathname}: its Community item and its folded twin in the Projects band`);
233
- assert.equal(ui.document.title, `Cloud Bongos — Community · ${title}`);
285
+ assert.equal(ui.document.title, section === 'home' ? 'Cloud Bongos — Community' : `Cloud Bongos — Community · ${item}`, 'the home is the Community page itself');
234
286
  assert.equal(ui.$('signinTop').attrs.href, `/api/bongos/auth/web/start?return=${href}&repos=1`, 'signing in comes back to this sub-page');
235
287
  }
236
288
  });
237
289
 
290
+ // ── 2c. the community home (task 1004902) ───────────────────────────────────────
291
+ // community-crowns.js decides and records each period; the home draws what it sent, the
292
+ // same for every viewer.
293
+
294
+ test('the home: one read, the three crowns with their periods, names as text, and nothing asked about projects', async () => {
295
+ const ui = await boot({ pathname: '/community' });
296
+ assert.deepEqual(ui.gets(), ['/api/bongos/community/crowns', '/api/bongos/community/movers']);
297
+ assert.equal(ui.$('homeLoading').hidden, true);
298
+ assert.equal(ui.$('homeFailed').hidden, true);
299
+ assert.equal(ui.$('homeCrowns').hidden, false);
300
+ assert.equal(ui.$('crownWeekWhen').textContent, '26 Oct – 1 Nov 2026');
301
+ assert.equal(find(ui.$('crownWeek'), 'crownName').textContent, 'Ana <i>L</i>', 'a name is text, never markup');
302
+ assert.equal(find(ui.$('crownWeek'), 'crownNums').textContent, (1240).toLocaleString() + 'credits4works shipped');
303
+ assert.equal(ui.$('crownMonthWhen').textContent, 'October 2026');
304
+ assert.equal(find(ui.$('crownMonth'), 'crownName').textContent, 'bo', 'no display name: the handle stands in');
305
+ assert.equal(find(ui.$('crownMonth'), 'connAv').tag, 'span', 'an avatar that is not http(s) is never loaded');
306
+ assert.equal(find(ui.$('crownMonth'), 'crownNums').textContent, (6120).toLocaleString() + 'credits1work shipped', 'one work, singular');
307
+ assert.equal(ui.$('crownYearWhen').textContent, '2026 so far, since 27 Sep 2026', 'the year says when history began while that is inside it');
308
+ assert.equal(find(ui.$('crownYear'), 'mineRow__host').textContent, '@cy');
309
+ assert.equal(ui.$('crownYearLast').hidden, true, 'no year crowned yet');
310
+ const signedIn = await boot({ pathname: '/community', me: { github_login: 'owner' } });
311
+ assert.deepEqual(signedIn.gets(), ['/api/bongos/community/crowns', '/api/bongos/community/movers'], 'signed in, the same two reads: no Recruit door here');
312
+ assert.equal(signedIn.$('homeCrowns').textContent, ui.$('homeCrowns').textContent, 'the same crowns for every viewer');
313
+ });
314
+
315
+ test('the home: last year under the year so far, the first month’s date, and no one to show', async () => {
316
+ const reads = READS();
317
+ reads['/community/crowns'] = { status: 200, body: { ...CROWNS(),
318
+ year: { start: '2027-01-01', since: '2027-01-01', winner: WINNER('di', 310, 2), last: { start: '2026-01-01', winner: WINNER('cy', 11250, 38, { display_name: 'Cy <b>!</b>' }) } } } };
319
+ const next = await boot({ pathname: '/community', reads });
320
+ assert.equal(next.$('crownYearWhen').textContent, '2027 so far', 'history began before the year: no “since”');
321
+ assert.equal(next.$('crownYearLast').hidden, false);
322
+ assert.equal(next.$('crownYearLast').textContent, '2026: Cy <b>!</b>, ' + (11250).toLocaleString() + ' credits');
323
+ assert.equal(kids(next.$('crownYearLast'))[1].tag, 'strong');
324
+ reads['/community/crowns'] = { status: 200, body: {
325
+ week: { start: '2026-10-19', end: '2026-10-26', winner: null },
326
+ month: { start: '2026-09-01', end: '2026-10-01', first_crown: '2026-11-01', winner: null },
327
+ year: { start: '2026-01-01', since: '2026-09-27', winner: null, last: null } } };
328
+ const first = await boot({ pathname: '/community', reads });
329
+ const line = (id) => find(first.$(id), 'commMsg').textContent;
330
+ assert.equal(line('crownWeek'), 'No builder of the week for 19–25 Oct 2026.');
331
+ assert.equal(first.$('crownMonthWhen').textContent, 'September 2026');
332
+ assert.equal(line('crownMonth'), 'The first builder of the month is named on 1 Nov 2026.', 'a month before history is not “nobody”');
333
+ assert.equal(line('crownYear'), 'No builder of 2026 yet.');
334
+ assert.equal(find(first.$('homeCrowns'), 'crownName'), null);
335
+ reads['/community/crowns'] = { status: 200, body: { ...CROWNS(), month: { start: '2026-10-01', end: '2026-11-01', first_crown: null, winner: null } } };
336
+ assert.equal(find((await boot({ pathname: '/community', reads })).$('crownMonth'), 'commMsg').textContent, 'No builder of the month for October 2026.', 'a hidden winner and an empty month read the same (spec D8)');
337
+ reads['/community/crowns'] = { status: 200, body: { week: { start: 'soon' }, month: {}, year: { last: { start: 'x', winner: WINNER('ed', 1, 1) } } } };
338
+ const odd = await boot({ pathname: '/community', reads });
339
+ assert.deepEqual(['crownWeek', 'crownMonth', 'crownYear'].map((id) => find(odd.$(id), 'commMsg').textContent), ['No builder of the week yet.', 'No builder of the month yet.', 'No builder of the year yet.'], 'a date it cannot read is never drawn');
340
+ assert.equal(odd.$('crownYearLast').hidden, true);
341
+ });
342
+
343
+ test('the home: a failed read is an alert, never three empty crowns; a rate limit says to wait', async () => {
344
+ const reads = READS();
345
+ for (const answer of [{ status: 500, body: {} }, { reject: true }]) {
346
+ reads['/community/crowns'] = answer;
347
+ const failed = await boot({ pathname: '/community', reads });
348
+ assert.equal(failed.$('homeFailed').hidden, false);
349
+ assert.equal(failed.$('homeFailed').textContent, 'The top builders could not be read. Reload the page to try again.');
350
+ assert.equal(failed.$('homeCrowns').hidden, true);
351
+ assert.equal(failed.$('homeLoading').hidden, true);
352
+ }
353
+ reads['/community/crowns'] = { status: 429, body: {} };
354
+ assert.match((await boot({ pathname: '/community', reads })).$('homeFailed').textContent, /^Too many requests/);
355
+ });
356
+
357
+ test('the row under the crowns: the rising builder as a crown, the projects in the server’s order, names as text, a link only to a web address (task 1002983)', async () => {
358
+ const ui = await boot({ pathname: '/community' });
359
+ assert.equal(ui.$('homeMovers').hidden, false);
360
+ assert.equal(ui.$('moversRule').hidden, false);
361
+ assert.equal(find(ui.$('moveRising'), 'crownName').textContent, 'Kofi <b>M</b>', 'a name is text, never markup');
362
+ assert.equal(find(ui.$('moveRising'), 'crownNums').textContent, '640credits5works shipped');
363
+ const rows = kids(ui.$('moveProjects'));
364
+ assert.deepEqual(rows.map((r) => find(r, 'commPlace').textContent), ['1', '2'], 'positions in the order sent');
365
+ const first = find(rows[0], 'mineRow__name');
366
+ assert.equal(first.tag, 'a');
367
+ assert.equal(first.attrs.href, 'https://tidepool.example/');
368
+ assert.equal(first.textContent, 'Tide <i>pool</i>');
369
+ assert.equal(find(rows[0], 'mineRow__hint').textContent, (1840).toLocaleString() + ' credits earned');
370
+ assert.equal(find(rows[0], 'commCredits').textContent, '23works shipped');
371
+ assert.equal(find(rows[1], 'mineRow__name').tag, 'div', 'an address that is not http(s) is never a link');
372
+ assert.equal(find(rows[1], 'commCredits').textContent, '1work shipped', 'one work, singular');
373
+ assert.equal(ui.$('moveNone').hidden, true);
374
+ const signedIn = await boot({ pathname: '/community', me: { github_login: 'owner' } });
375
+ assert.equal(signedIn.$('homeMovers').textContent, ui.$('homeMovers').textContent, 'the same row for every viewer');
376
+ });
377
+
378
+ test('the row under the crowns: nobody to show is one line in each card; a failed read drops only the row', async () => {
379
+ const reads = READS();
380
+ reads['/community/movers'] = { status: 200, body: { window: {}, rising: null, projects: [] } };
381
+ const quiet = await boot({ pathname: '/community', reads });
382
+ assert.equal(quiet.$('homeMovers').hidden, false);
383
+ assert.equal(find(quiet.$('moveRising'), 'commMsg').textContent, 'No rising builder in the last 30 days.');
384
+ assert.equal(kids(quiet.$('moveProjects')).length, 0);
385
+ assert.equal(quiet.$('moveNone').hidden, false);
386
+ for (const answer of [{ status: 500, body: {} }, { status: 429, body: {} }, { reject: true }]) {
387
+ reads['/community/movers'] = answer;
388
+ const failed = await boot({ pathname: '/community', reads });
389
+ assert.equal(failed.$('homeMovers').hidden, true);
390
+ assert.equal(failed.$('moversRule').hidden, true, 'no rule for cards that are not there');
391
+ assert.equal(failed.$('homeCrowns').hidden, false, 'the crowns stand on their own');
392
+ assert.equal(failed.$('homeFailed').hidden, true, 'and no alert: the row is the home’s extra');
393
+ }
394
+ });
395
+
238
396
  test('the leaderboard, signed out: the all-time board in the server’s order with the server’s places, and nothing asked about the viewer', async () => {
239
397
  const ui = await boot();
240
398
  assert.deepEqual(ui.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/community/builder-of-the-week']);
@@ -535,7 +693,8 @@ const SERVES_A_CHILD = ['privacy.html', 'terms.html', 'contact.html', 'index.htm
535
693
  const HUB_ONLY = ['account.html', 'connections.html', 'invitations.html', 'guild.html', 'community.html'];
536
694
  const wrap = (apex, s) => (apex ? `<!--apex-only:start-->${s}<!--apex-only:end-->` : s);
537
695
  const ITEMS = [
538
- ['/community', 'Leaderboard', 'who earned the most, across every project'],
696
+ ['/community', 'Community home', 'the builders of the week, month and year'],
697
+ ['/community/leaderboard', 'Leaderboard', 'who earned the most, across every project'],
539
698
  ['/community/week', 'Builder of the week', 'the top earner of each week, and how it is chosen'],
540
699
  ['/community/find', 'Find a builder', 'anyone building here, by name or role'],
541
700
  ['/community/recruiters', 'Open to recruiters', 'who is open to recruiting, for project owners'],
@@ -557,9 +716,9 @@ test('every page that wears the hub’s bar carries the Community menu before Pr
557
716
  const at = html.indexOf(apex ? `<!--apex-only:start-->${trig}` : trig);
558
717
  assert.ok(at > 0, `${f}: the Community trigger`);
559
718
  assert.ok(html.indexOf('id="projTrig"') > at, `${f}: Community comes before Projects`);
560
- assert.ok(html.includes(wrap(apex, band)), `${f}: the Community band, its four sub-pages`);
719
+ assert.ok(html.includes(wrap(apex, band)), `${f}: the Community band, its five sub-pages`);
561
720
  assert.ok(html.indexOf('id="menuCommunity"') < html.indexOf('id="menuProjects"'), `${f}: the bands in the bar’s order`);
562
- assert.ok(html.includes(wrap(apex, fold)), `${f}: the four folded into the Projects band for phones`);
721
+ assert.ok(html.includes(wrap(apex, fold)), `${f}: the five folded into the Projects band for phones`);
563
722
  if (apex) assert.doesNotMatch(stripApexOnly(html, false).split('<body>')[1], /\/community|commTrig|menuCommunity|connSuggest/, `${f}: a child has no community pages, so no trace of the menu`);
564
723
  }
565
724
  });
@@ -617,7 +776,7 @@ function get(pathname, host) {
617
776
  });
618
777
  }
619
778
 
620
- test('GET /community, /community/week, /community/find and /community/recruiters: one stamped page on the hub’s apex; never on a child or another host', async () => {
779
+ test('GET /community, /community/leaderboard, /community/week, /community/find and /community/recruiters: one stamped page on the hub’s apex; never on a child or another host', async () => {
621
780
  modules.setModuleOverrides({ 'platform-identity': false });
622
781
  for (const [p] of SUBPAGES) assert.equal((await get(p, 'game.test.local')).status, 404, `${p}: a child instance has none of the routes these pages read`);
623
782
  modules.setModuleOverrides({ 'platform-identity': true });
@@ -630,7 +789,7 @@ test('GET /community, /community/week, /community/find and /community/recruiters
630
789
  bodies.push(hub.body);
631
790
  }
632
791
  assert.ok(bodies.every((b) => b === bodies[0]), 'byte-identical for every sub-page: the script picks the view');
633
- assert.doesNotMatch((await get('/community/everyone', 'game.test.local')).body, /— Community<\/title>/, 'only the four sub-pages');
792
+ assert.doesNotMatch((await get('/community/everyone', 'game.test.local')).body, /— Community<\/title>/, 'only the five sub-pages');
634
793
  for (const host of ['builders.test.local', 'status.test.local']) {
635
794
  assert.doesNotMatch((await get('/community', host)).body, /— Community<\/title>/, `${host} must not serve /community`);
636
795
  }
@@ -643,10 +802,19 @@ test('the kit renders each sub-page for everyone, the empty board, and a project
643
802
  const states = json('community.states.json');
644
803
  assert.equal(states.page, '/community');
645
804
  assert.deepEqual(states.modes, ['dark']);
646
- for (const s of ['out', 'out-30d', 'out-artists', 'out-week', 'in-week', 'out-find', 'out-find-search', 'out-recruiters', 'in-recruiters', 'in']) assert.ok(states.states[s], `kit state ${s}`);
805
+ for (const s of ['out', 'in', 'out-board', 'out-30d', 'out-artists', 'out-week', 'in-week', 'out-find', 'out-find-search', 'out-recruiters', 'in-recruiters', 'in-board']) assert.ok(states.states[s], `kit state ${s}`);
806
+ assert.equal(states.states.out.url, undefined, 'the home is /community itself');
807
+ for (const s of ['out-board', 'out-30d', 'out-artists', 'in-board']) assert.equal(states.states[s].url, '/community/leaderboard', `${s}: the board’s own address`);
647
808
  assert.equal(states.states['out-find'].url, '/community/find');
648
809
  assert.equal(states.states['out-week'].url, '/community/week');
649
- assert.ok(states.states.out.expect.visible.includes('#boardCrown'), 'the board carries the crown line');
810
+ assert.ok(states.states['out-board'].expect.visible.includes('#boardCrown'), 'the board carries the crown line');
811
+ assert.ok(states.states.out.expect.visible.includes('#homeMovers'), 'the home carries the row under the crowns (task 1002983)');
812
+ assert.equal(json('community-movers-none.states.json').stub.fixtures['community-movers'], 'community-movers-none');
813
+ // the home's other answers (task 1004902): before the first month, nobody, and a new year
814
+ for (const [file, fixture] of [['community-crowns-first.states.json', 'community-crowns-first'], ['community-crowns-none.states.json', 'community-crowns-none'], ['community-crowns-new-year.states.json', 'community-crowns-new-year']]) {
815
+ assert.equal(json(file).stub.fixtures['community-crowns'], fixture);
816
+ assert.ok(fs.existsSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', fixture + '.json')), `fixture ${fixture}`);
817
+ }
650
818
  assert.equal(states.states['in-recruiters'].url, '/community/recruiters');
651
819
  assert.ok(states.states['in-recruiters'].expect.visible.includes('#recruitClosed'), 'signed in but not admitted: told, shown no one');
652
820
  for (const [file, name, fixture] of [['community-empty.states.json', 'community-leaderboard', 'community-leaderboard-empty'], ['community-recruiter.states.json', 'scouting', 'scouting-recruiter'], ['community-owner.states.json', 'my-projects', 'my-projects-owner'], ['community-owner-two.states.json', 'my-projects', 'my-projects-owner-two'], ['community-week-none.states.json', 'community-week', 'community-week-none']]) {
@@ -685,9 +853,20 @@ async function call(origin, p) {
685
853
  return { status: res.status, body: await res.json() };
686
854
  }
687
855
 
688
- test('the kit stub answers the board, the builder of the week, the search and the recruiting door the way the routes do', async () => {
856
+ test('the kit stub answers the crowns, the movers, the board, the builder of the week, the search and the recruiting door the way the routes do', async () => {
689
857
  const s = await stub();
690
858
  try {
859
+ const crowns = await call(s.origin, '/community/crowns');
860
+ assert.deepEqual(Object.keys(crowns.body), ['week', 'month', 'year'], 'the route’s three periods');
861
+ const { week: w, month: m, year: y } = crowns.body;
862
+ assert.deepEqual([Object.keys(w).sort(), Object.keys(m).sort(), Object.keys(y).sort()], [['end', 'start', 'winner'], ['end', 'first_crown', 'start', 'winner'], ['last', 'since', 'start', 'winner']], 'community-crowns.js’s shape');
863
+ assert.ok([w.winner, m.winner, y.winner].every((r) => r && Object.keys(r).sort().join() === 'avatar_url,credits,display_name,github_login,handle,tasks_shipped'), 'the module’s projection, no github_id');
864
+ const movers = await call(s.origin, '/community/movers');
865
+ assert.deepEqual(Object.keys(movers.body), ['window', 'rising', 'projects'], 'community-movers.js’s shape');
866
+ assert.equal(Object.keys(movers.body.rising).sort().join(), 'avatar_url,credits,display_name,github_login,handle,tasks_shipped', 'the crown’s projection, no github_id');
867
+ assert.ok(movers.body.projects.length && movers.body.projects.every((p) => Object.keys(p).sort().join() === 'credits,name,origin,tasks_shipped'), 'a project’s name, address and two numbers, no client_id');
868
+ const ships = movers.body.projects.map((p) => p.tasks_shipped);
869
+ assert.deepEqual(ships, [...ships].sort((a, b) => b - a), 'most works first');
691
870
  const week = await call(s.origin, '/community/builder-of-the-week');
692
871
  assert.deepEqual(Object.keys(week.body).sort(), ['past', 'week', 'winner'], 'the route’s three keys');
693
872
  assert.ok(week.body.winner && week.body.past.length, 'a crown and the weeks before it');
@@ -717,6 +896,11 @@ test('the kit stub answers the board, the builder of the week, the search and th
717
896
  assert.equal(r.body.winner, null);
718
897
  assert.deepEqual(r.body.past, []);
719
898
  } finally { none.stop(); }
899
+ const nobody = await stub({ FIXTURE_ALIAS: JSON.stringify({ 'community-crowns': 'community-crowns-none' }) });
900
+ try {
901
+ const r = await call(nobody.origin, '/community/crowns');
902
+ assert.deepEqual([r.body.week.winner, r.body.month.winner, r.body.year.winner, r.body.year.last], [null, null, null, null]);
903
+ } finally { nobody.stop(); }
720
904
  const owner = await stub({ AUTH: '1', FIXTURE_ALIAS: JSON.stringify({ scouting: 'scouting-recruiter' }) });
721
905
  try {
722
906
  const r = await call(owner.origin, '/scouting');
@@ -193,8 +193,8 @@ for (const p of PAGES) {
193
193
  assert.deepEqual(bandLinks(bandOf(html, 'menuProjects')), [
194
194
  ['Sky', '/projects?view=map'], ['Table', '/projects?view=map&amp;sub=table'],
195
195
  ['Your projects', '/projects?view=mine'], ['New project', '/projects?view=new'],
196
- // the Community menu's four sub-pages, folded into the band under 480px (tasks 1003363, 1002981); apex-only
197
- ['Leaderboard', '/community'], ['Builder of the week', '/community/week'], ['Find a builder', '/community/find'], ['Open to recruiters', '/community/recruiters'],
196
+ // the Community menu's five sub-pages, folded into the band under 480px (tasks 1003363, 1002981, 1004902); apex-only
197
+ ['Community home', '/community'], ['Leaderboard', '/community/leaderboard'], ['Builder of the week', '/community/week'], ['Find a builder', '/community/find'], ['Open to recruiters', '/community/recruiters'],
198
198
  ['Builders you may know', '/connections#connSuggest'],
199
199
  ], 'this page has no views of its own, so every item names /projects in full');
200
200
  assert.deepEqual(bandLinks(bandOf(stripApexOnly(html, false), 'menuProjects')).map((l) => l[0]), ['Sky', 'Table', 'Your projects', 'New project'], 'a child has no /community');
@@ -154,7 +154,15 @@ function identitySentinels(acct) {
154
154
  // the past weeks (task 1002981, spec D5): a row IS stats, so the board's rule.
155
155
  // Every fixture account has won a recorded week, so the read's own predicate
156
156
  // decides: 'listed' | 'absent'
157
- // directory GET /scouting Recruiter — 'full' | 'name-only' | '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'
158
166
  // sliver GET /scouting/:handle Recruiter — 'full' (D3) | 'name-only' (D3, hidden) | 404
159
167
  // self GET /me/cross-project Self — 'own' (own numbers, any flags) | 'n/a' (no session exists)
160
168
  // guild GET /guilds/:slug Anon · Builder · Recruiter · Self — the guild page's PUBLIC
@@ -170,13 +178,13 @@ function identitySentinels(acct) {
170
178
  // people the guild page already shows them — shown_publicly AND existence;
171
179
  // hide_stats never hides membership, and private or provisional is never listed
172
180
  const MATRIX = {
173
- PROV: { profile: 404, search: 'absent', board: 'absent', week: 'absent', directory: 'absent', sliver: 404, self: 'n/a', guild: 'absent', members: 'absent', invite: 'absent' },
174
- PUB: { profile: 'full', search: 'listed', board: 'listed', week: 'listed', directory: 'full', sliver: 'full', self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
175
- SHY: { profile: 'identity', search: 'name-only', board: 'absent', week: 'absent', directory: 'name-only', sliver: 'name-only', self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
176
- PRIV: { profile: 404, search: 'absent', board: 'absent', week: 'absent', directory: 'absent', sliver: 404, self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
177
- PRIVD: { profile: 404, search: 'absent', board: 'absent', week: 'absent', directory: 'absent', sliver: 'full', self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
178
- PRIVDH: { profile: 404, search: 'absent', board: 'absent', week: 'absent', directory: 'absent', sliver: 'name-only', self: 'own', guild: 'absent', members: 'listed', invite: 'absent' },
179
- OPTOUT: { profile: 'full', search: 'listed', board: 'listed', week: 'listed', directory: 'absent', sliver: 404, self: 'own', guild: 'listed', members: 'listed', invite: 'listed' },
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' },
180
188
  };
181
189
 
182
190
  // ===========================================================================
@@ -296,6 +304,12 @@ function weekOf(key) {
296
304
  }
297
305
  return weeksWon[key];
298
306
  }
307
+ // THE ACCOUNT that won the month, the year so far and last year (task 1004902); the
308
+ // crowns test makes each fixture account the winner in turn.
309
+ let CROWNED = 'PUB';
310
+ // THE ACCOUNT that moved in the 30 days, on MX_CLIENT's project (task 1002983); the movers
311
+ // test makes each fixture account the one in turn.
312
+ let MOVED = 'PUB';
299
313
 
300
314
  const pool = {
301
315
  query: async (text, params = []) => {
@@ -370,6 +384,53 @@ const pool = {
370
384
  return { rows, rowCount: rows.length };
371
385
  }
372
386
 
387
+ // THE CROWNS (task 1004902): history began long ago, every month and year is already
388
+ // recorded, and CROWNED names the account that won all three — so each read's own
389
+ // predicate decides who is shown. The projections are pinned: a new column is a new
390
+ // disclosure.
391
+ if (/^SELECT reported_at FROM platform_identity_activity_snapshots ORDER BY id ASC LIMIT 1$/.test(sql)) return { rows: [{ reported_at: '2020-01-01T00:00:00.000Z' }], rowCount: 1 };
392
+ if (/^SELECT 1 FROM platform_identity_period_features WHERE /.test(sql)) return { rows: [{ '?column?': 1 }], rowCount: 1 };
393
+ if (/FROM platform_identity_period_features f JOIN platform_identity_accounts pa/.test(sql)) {
394
+ const cols = (/^SELECT (.+?) FROM /.exec(sql) || [, ''])[1];
395
+ if (cols !== 'f.credits_delta, f.ships_delta, pa.handle, pa.github_login, pa.display_name, pa.avatar_url') throw new Error(`fake pool: the crowned-period read selects something new — ${cols}`);
396
+ const where = lift(sql, /AND f\.period_start = \$2::date AND (.+)$/, 'crowned period');
397
+ const rows = ACCOUNTS.filter((a) => a.key === CROWNED).filter(accountFilter(where, [])).map((a) => ({
398
+ credits_delta: String(ROLLUP[a.github_id].credits), ships_delta: String(ROLLUP[a.github_id].works_shipped),
399
+ handle: a.handle, github_login: a.github_login, display_name: a.display_name, avatar_url: a.avatar_url,
400
+ }));
401
+ return { rows, rowCount: rows.length };
402
+ }
403
+ // THE MOVERS (task 1002983): the 30 days' rows carry the floor on the act CTE, as the
404
+ // year's; MOVED had nothing before them. The project names are MX_CLIENT's.
405
+ if (/^WITH act AS/.test(sql) && /LEFT JOIN per_project pp/.test(sql)) {
406
+ const where = lift(sql, /JOIN platform_identity_accounts pa ON pa\.github_id = a\.github_id WHERE (.+?) \), per_project AS/, 'movers');
407
+ const rows = ACCOUNTS.filter((a) => a.key === MOVED).filter(accountFilter(where, [])).map((a) => ({
408
+ client_id: MX_CLIENT.client_id, github_id: a.github_id, credits: String(ROLLUP[a.github_id].credits),
409
+ tasks_shipped: String(ROLLUP[a.github_id].works_shipped), karma: String(ROLLUP[a.github_id].karma), base_credits: '0',
410
+ }));
411
+ return { rows, rowCount: rows.length };
412
+ }
413
+ if (/FROM platform_identity_sso_clients c JOIN platform_identity_projects p ON p\.origin = c\.origin WHERE c\.client_id = ANY\(\$1::text\[\]\)/.test(sql)) {
414
+ const rows = (params[0] || []).includes(MX_CLIENT.client_id) ? [{ client_id: MX_CLIENT.client_id, name: MX_CLIENT.name, origin: MX_CLIENT.origin }] : [];
415
+ return { rows, rowCount: rows.length };
416
+ }
417
+ // ... the year so far: computeWinner's act CTE carries the floor, then the leader's re-read
418
+ if (/^WITH act AS/.test(sql) && /\) f ON true/.test(sql)) {
419
+ const where = lift(sql, /JOIN platform_identity_accounts pa ON pa\.github_id = a\.github_id WHERE (.+?) \), per_project AS/, 'year so far');
420
+ const rows = ACCOUNTS.filter((a) => a.key === CROWNED).filter(accountFilter(where, [])).map((a) => ({
421
+ github_id: a.github_id, credits: String(ROLLUP[a.github_id].credits), tasks_shipped: String(ROLLUP[a.github_id].works_shipped),
422
+ }));
423
+ return { rows, rowCount: rows.length };
424
+ }
425
+ if (/FROM platform_identity_accounts pa WHERE pa\.github_id = \$1::bigint AND /.test(sql)) {
426
+ const cols = (/^SELECT (.+?) FROM /.exec(sql) || [, ''])[1];
427
+ if (cols !== 'pa.handle, pa.github_login, pa.display_name, pa.avatar_url') throw new Error(`fake pool: the year leader read selects something new — ${cols}`);
428
+ const where = lift(sql, /WHERE pa\.github_id = \$1::bigint AND (.+)$/, 'year leader');
429
+ const rows = ACCOUNTS.filter((a) => a.github_id === String(params[0])).filter(accountFilter(where, []))
430
+ .map((a) => ({ handle: a.handle, github_login: a.github_login, display_name: a.display_name, avatar_url: a.avatar_url }));
431
+ return { rows, rowCount: rows.length };
432
+ }
433
+
373
434
  // THE LEADERBOARD's totals: the rollup floor sits on the act CTE's join.
374
435
  if (/platform_identity_activity_snapshots/.test(sql)) {
375
436
  const where = lift(sql, /JOIN platform_identity_accounts pa ON pa\.github_id = a\.github_id WHERE (.+?) \), per_project AS/, 'leaderboard totals');
@@ -809,6 +870,43 @@ test('the builder of the week — only shown, public, active winners, on the cro
809
870
  assert.doesNotMatch(seen[0].text, /github_id/, 'github_id never crosses');
810
871
  });
811
872
 
873
+ test('the crowns — the month, the year so far and last year only for shown, public, active winners, the same for every viewer', async () => {
874
+ const { clearCrownsMemo } = require('../modules/platform-identity/community-crowns.js');
875
+ try {
876
+ for (const k of KEYS) {
877
+ CROWNED = k;
878
+ clearCrownsMemo(); // the year's leader is memoised: a new winner is a new computation
879
+ const seen = [];
880
+ for (const who of [ANON, STRANGER, RECRUITER]) seen.push(await get(who, '/api/bongos/community/crowns'));
881
+ for (const r of seen.slice(1)) assert.equal(r.text, seen[0].text, `${r.who} got different crowns with ${k} crowned`);
882
+ const { month, year } = seen[0].json;
883
+ const shown = { month: month.winner, year: year.winner, last: year.last && year.last.winner };
884
+ for (const [which, w] of Object.entries(shown)) {
885
+ assert.equal(!!w && w.handle === ACCT[k].handle, MATRIX[k].crowns === 'listed', `${k} as ${which}: expected ${MATRIX[k].crowns}`);
886
+ }
887
+ assert.doesNotMatch(seen[0].text, /github_id/, 'github_id never crosses');
888
+ }
889
+ } finally { CROWNED = 'PUB'; }
890
+ });
891
+
892
+ test('the movers — the rising builder and a project’s totals only from shown, public, active accounts, the same for every viewer', async () => {
893
+ const { clearMoversMemo } = require('../modules/platform-identity/community-movers.js');
894
+ try {
895
+ for (const k of KEYS) {
896
+ MOVED = k;
897
+ clearMoversMemo(); // the 30 days are memoised: a new mover is a new computation
898
+ const seen = [];
899
+ for (const who of [ANON, STRANGER, RECRUITER]) seen.push(await get(who, '/api/bongos/community/movers'));
900
+ for (const r of seen.slice(1)) assert.equal(r.text, seen[0].text, `${r.who} got different movers with ${k} moving`);
901
+ const { rising, projects } = seen[0].json;
902
+ const listed = MATRIX[k].movers === 'listed';
903
+ assert.equal(!!rising && rising.handle === ACCT[k].handle, listed, `${k} as the rising builder: expected ${MATRIX[k].movers}`);
904
+ assert.deepEqual(projects.map((p) => p.tasks_shipped), listed ? [ROLLUP[ACCT[k].github_id].works_shipped] : [], `${k}'s works in a project's totals: expected ${MATRIX[k].movers}`);
905
+ assert.doesNotMatch(seen[0].text, /github_id|client_id/, 'github_id and client_id never cross');
906
+ }
907
+ } finally { MOVED = 'PUB'; }
908
+ });
909
+
812
910
  test('the recruiting directory — the recruiter sees the full and name-only tiers; Anon and Builder are refused', async () => {
813
911
  const r = await get(RECRUITER, '/api/bongos/scouting');
814
912
  assert.equal(r.status, 200);
@@ -851,7 +949,7 @@ function disclosed(r) {
851
949
  }
852
950
 
853
951
  test('the walk covered every surface for every row (so the claims below are about something)', () => {
854
- for (const p of ['/profiles/', '/me/cross-project', '/community/search', '/community/leaderboard', '/community/builder-of-the-week', '/scouting', '/guilds/mx-guild', '/guilds/mx-guild/members', '/projects/invite-guild']) {
952
+ for (const p of ['/profiles/', '/me/cross-project', '/community/search', '/community/leaderboard', '/community/builder-of-the-week', '/community/crowns', '/community/movers', '/scouting', '/guilds/mx-guild', '/guilds/mx-guild/members', '/projects/invite-guild']) {
855
953
  assert.ok(SEEN.some((r) => r.path.includes(p) && r.status === 200), `no 200 from ${p} — the sweep below would be vacuous for it`);
856
954
  }
857
955
  assert.ok(SEEN.length >= KEYS.length * 6, `only ${SEEN.length} responses recorded`);