@bongos/core 1.23.14 → 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 (43) hide show
  1. package/.bongos-core.json +77 -42
  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 +924 -878
  10. package/docs/copy-registry.json +1796 -1325
  11. package/docs/module-api-changelog.md +2 -0
  12. package/docs/page-inventory.json +8 -3
  13. package/docs/page-readings.json +575 -555
  14. package/modules/platform-identity/builder-of-the-week.js +180 -0
  15. package/modules/platform-identity/migrations/platform_identity_032_weekly_features.sql +53 -0
  16. package/modules/platform-identity/routes/community-leaderboard.js +25 -1
  17. package/modules/public-landing/public/account.html +2 -1
  18. package/modules/public-landing/public/community-week-none.states.json +16 -0
  19. package/modules/public-landing/public/community.html +142 -13
  20. package/modules/public-landing/public/community.states.json +6 -4
  21. package/modules/public-landing/public/connections.html +2 -1
  22. package/modules/public-landing/public/contact.html +2 -1
  23. package/modules/public-landing/public/guild.html +2 -1
  24. package/modules/public-landing/public/index.html +2 -1
  25. package/modules/public-landing/public/invitations.html +2 -1
  26. package/modules/public-landing/public/privacy.html +2 -1
  27. package/modules/public-landing/public/projects.html +2 -1
  28. package/modules/public-landing/public/terms.html +2 -1
  29. package/modules/ui-design/kit/fixtures/community-week-none.json +6 -0
  30. package/modules/ui-design/kit/fixtures/community-week.json +11 -0
  31. package/modules/ui-design/kit/serve.js +8 -0
  32. package/package-lock.json +2 -2
  33. package/package.json +1 -1
  34. package/release-notes.json +6 -0
  35. package/scripts/bongos/run-unit-tests.js +4 -0
  36. package/src/module-api.js +1 -1
  37. package/src/platform-server.js +5 -5
  38. package/tests/builder_of_the_week.mjs +266 -0
  39. package/tests/builder_of_the_week_db.mjs +166 -0
  40. package/tests/hub_account_page.mjs +2 -2
  41. package/tests/hub_community_page.mjs +136 -25
  42. package/tests/landing_legal_pages.mjs +2 -2
  43. package/tests/privacy_matrix.mjs +53 -8
@@ -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
+ }
@@ -135,8 +135,8 @@ test('the bands say what the hub’s say and lead into /projects; Account is the
135
135
  assert.deepEqual(bandLinks(bandOf(PAGE, 'menuProjects')), [
136
136
  ['Sky', '/projects?view=map'], ['Table', '/projects?view=map&sub=table'],
137
137
  ['Your projects', '/projects?view=mine'], ['New project', '/projects?view=new'],
138
- // the Community menu's three sub-pages, folded into the band under 480px (task 1003363)
139
- ['Leaderboard', '/community'], ['Find a builder', '/community/find'], ['Open to recruiters', '/community/recruiters'],
138
+ // the Community menu's four sub-pages, folded into the band under 480px (tasks 1003363, 1002981)
139
+ ['Leaderboard', '/community'], ['Builder of the week', '/community/week'], ['Find a builder', '/community/find'], ['Open to recruiters', '/community/recruiters'],
140
140
  ['Builders you may know', '/connections#connSuggest'],
141
141
  ], 'this page has no views of its own, so every item names /projects in full');
142
142
  assert.deepEqual(bandLinks(bandOf(PAGE, 'menuProfile')), [
@@ -1,10 +1,11 @@
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 three sub-pages of the bar's Community menu (owner, 2026-10-27):
5
- // /community the leaderboard, /community/find the builder search, /community/recruiters
6
- // the name-only list of hidden builders open to recruiters. They render three existing
7
- // platform-identity reads and add no rule of their own. What this file pins:
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:
8
9
  // • the markup: the hub's bar with the Community menu as the current section, no top
9
10
  // block, noindex, dark only, the routes' own windows and query limits
10
11
  // • viewing is ungated: the leaderboard and the search read the same routes for every
@@ -13,11 +14,12 @@
13
14
  // item marked current; the board in the server's order with its `place`, the window
14
15
  // dropdown and the crafts, the empty, failed and rate-limited board; the search and
15
16
  // its refusals; the recruiter list as a door signed out, a refusal on a 403, and rows
16
- // only from a 200 — name, face and login, never the ranked half's numbers
17
+ // 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
17
19
  // • the Community menu on every page that wears the hub's bar (apex-only where the page
18
20
  // also serves a child), folded into the Projects band on phones
19
- // • the routes: the three paths served on the hub's apex only
20
- // • the kit: the states files, and the stub answering as the three reads do
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
21
23
  //
22
24
  // DB-free. Run: node --test tests/hub_community_page.mjs
23
25
 
@@ -44,7 +46,8 @@ const STYLE = PAGE.split('<style>')[1].split('</style>')[0].replace(/\/\*[\s\S]*
44
46
  const MAIN = PAGE.split('<main class="acct" id="comm">')[1].split('</main>')[0];
45
47
  const BOARD_JS = fs.readFileSync(path.join(PI, 'community-leaderboard.js'), 'utf8');
46
48
  const SEARCH_JS = fs.readFileSync(path.join(PI, 'builder-search.js'), 'utf8');
47
- const SUBPAGES = [['/community', 'Leaderboard'], ['/community/find', 'Find a builder'], ['/community/recruiters', 'Open to recruiters']];
49
+ const SUBPAGES = [['/community', 'Leaderboard'], ['/community/week', 'Builder of the week'], ['/community/find', 'Find a builder'], ['/community/recruiters', 'Open to recruiters']];
50
+ const WEEK_JS = fs.readFileSync(path.join(PI, 'builder-of-the-week.js'), 'utf8');
48
51
 
49
52
  // ── 1. the markup ────────────────────────────────────────────────────────────
50
53
 
@@ -72,7 +75,7 @@ test('the bar and bands are /connections’ but for the sign-in return and the c
72
75
 
73
76
  test('no top block: one visually hidden h1, then one section per sub-page, only the leaderboard shown in the markup', () => {
74
77
  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)');
75
- const order = ['<section id="board" data-view="leaderboard" aria-labelledby="boardH">', '<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));
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));
76
79
  assert.ok(order.every((i) => i > 0), 'every sub-page is real markup the script only shows');
77
80
  assert.deepEqual([...order].sort((a, b) => a - b), order);
78
81
  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');
@@ -82,6 +85,23 @@ test('no top block: one visually hidden h1, then one section per sub-page, only
82
85
  assert.match(MAIN, /<h2 id="recruitH">Open to recruiters<\/h2>/, 'the hide flag is "hidden", never "private" (ADR 0337)');
83
86
  assert.match(MAIN, /<a class="btn" id="recruitSignin" href="\/api\/bongos\/auth\/web\/start\?return=\/community\/recruiters&amp;repos=1">/);
84
87
  for (const id of ['recruitDoor', 'recruitClosed', 'recruitFailed', 'recruitEmpty']) assert.match(MAIN, new RegExp(` id="${id}"[^>]*hidden`), `${id} waits for the route’s answer`);
88
+ // the builder of the week (task 1002981): its line on the board and its page wait for the read
89
+ assert.match(MAIN, /<h2 id="boardH">Leaderboard<\/h2>\n\s*<p class="commCrownLine" id="boardCrown" hidden><\/p>/, 'the crown line sits under the board’s heading, above the rows');
90
+ for (const id of ['weekCrown', 'weekNone', 'weekFailed', 'pastH']) assert.match(MAIN, new RegExp(` id="${id}"[^>]*hidden`), `${id} waits for the route’s answer`);
91
+ assert.match(MAIN, /<ol class="commRows weekRows" id="weekPast" aria-labelledby="pastH"><\/ol>/);
92
+ });
93
+
94
+ test('the builder of the week states its rule in full, and the rule is the module’s (spec D5: the formula is public)', () => {
95
+ const rule = MAIN.split('<h3 class="commSub" id="weekRuleH">How it is chosen</h3>')[1].split('<h3 ')[0];
96
+ assert.match(rule, /the most \{\{currency\}\} across every project in one week, Monday to Sunday \(UTC\)/, 'the instance’s own unit, the week the module counts');
97
+ assert.match(rule, /shipped at least one work that week/);
98
+ assert.match(WEEK_JS, /HAVING sum\(tasks_shipped\) >= 1/, 'the floor the page states is the one the query applies');
99
+ assert.match(rule, /A tie goes to whoever shipped more works, then to whoever earned more karma\./);
100
+ 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
+ assert.match(rule, /Builders who hide their numbers or keep their account private are not counted\./);
102
+ 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\./);
104
+ assert.match(WEEK_JS, /ON CONFLICT \(week_start\) DO NOTHING/);
85
105
  });
86
106
 
87
107
  test('the window dropdown and the query limits are the routes’ own (boundary-forced copies)', () => {
@@ -101,12 +121,12 @@ test('the window dropdown and the query limits are the routes’ own (boundary-f
101
121
  assert.match(MAIN, new RegExp(`<input id="findQ" type="search"[^>]* maxlength="${max}"`));
102
122
  });
103
123
 
104
- test('the script builds no markup from strings, reads only the four routes, and never re-ranks or reads the ranked tier', () => {
124
+ test('the script builds no markup from strings, reads only the five routes, and never re-ranks or reads the ranked tier', () => {
105
125
  assert.doesNotMatch(SCRIPT, /innerHTML|insertAdjacentHTML|outerHTML|document\.write/);
106
- // every path literal in the code: the API base, the four reads, the three sub-pages, the
126
+ // every path literal in the code: the API base, the five reads, the four sub-pages, the
107
127
  // sign-in door and the hall's invite box the Recruit door opens (task 1002292)
108
128
  const paths = [...new Set([...CODE.matchAll(/'(\/[^'?]*)/g)].map((m) => m[1]))];
109
- assert.deepEqual(paths.sort(), ['/api/bongos', '/api/bongos/auth/web/start', '/builders/watch', '/community', '/community/find', '/community/leaderboard', '/community/recruiters', '/community/search', '/my-projects', '/scouting']);
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']);
110
130
  assert.doesNotMatch(CODE, /method:/, 'it only reads');
111
131
  assert.doesNotMatch(CODE, /'\/me\b/, 'who is signed in is the bar’s question');
112
132
  assert.doesNotMatch(CODE, /\.sort\(|\.count\b|_count\b/, 'the server ranks; every count is a list’s length');
@@ -147,7 +167,13 @@ const find = (n, cls) => {
147
167
  };
148
168
 
149
169
  const ROW = (place, handle, credits, extra = {}) => ({ place, handle, github_login: handle, display_name: handle.toUpperCase(), avatar_url: `https://avatars.example/${handle}`, credits, tasks_shipped: 2, karma: 1, disciplines: ['engineer'], ...extra });
170
+ const WEEK = () => ({ week: { start: '2026-10-19', end: '2026-10-26' },
171
+ winner: { week_start: '2026-10-19', handle: 'ana', github_login: 'ana', display_name: 'Ana <i>L</i>', avatar_url: 'https://avatars.example/ana', credits: 1240, tasks_shipped: 1 },
172
+ past: [{ week_start: '2026-10-12', handle: 'bo', github_login: 'bo', display_name: null, avatar_url: 'data:x', credits: 300, tasks_shipped: 2 },
173
+ { week_start: '2026-09-28', handle: 'cy', github_login: 'cy', display_name: 'Cy', avatar_url: null, credits: 90, tasks_shipped: 1 },
174
+ { week_start: '2025-12-29', handle: 'di', github_login: 'di', display_name: 'Di', avatar_url: null, credits: 10, tasks_shipped: 1 }] });
150
175
  const READS = () => ({
176
+ '/community/builder-of-the-week': { status: 200, body: WEEK() },
151
177
  '/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 })] } },
152
178
  '/community/leaderboard?window=30d': { status: 200, body: { window: '30d', results: [ROW(1, 'cy', 40)] } },
153
179
  '/community/leaderboard?window=all&discipline=artist': { status: 200, body: { window: 'all', results: [] } },
@@ -199,9 +225,9 @@ async function boot({ pathname = '/community', me = null, reads = READS() } = {}
199
225
  }
200
226
 
201
227
  test('each path shows its one section, marks its menu items current and names itself; any other shape is the leaderboard', async () => {
202
- for (const [pathname, title, section] of [['/community', 'Leaderboard', 'board'], ['/community/', 'Leaderboard', 'board'], ['/community/find', 'Find a builder', 'find'], ['/community/recruiters/', 'Open to recruiters', 'recruit']]) {
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']]) {
203
229
  const ui = await boot({ pathname });
204
- assert.deepEqual(['board', 'find', 'recruit'].filter((s) => !ui.$(s).hidden), [section], `${pathname}: one section`);
230
+ assert.deepEqual(['board', 'week', 'find', 'recruit'].filter((s) => !ui.$(s).hidden), [section], `${pathname}: one section`);
205
231
  const href = SUBPAGES.find(([, t]) => t === title)[0];
206
232
  assert.deepEqual(ui.current(), [href, href], `${pathname}: its Community item and its folded twin in the Projects band`);
207
233
  assert.equal(ui.document.title, `Cloud Bongos — Community · ${title}`);
@@ -211,7 +237,7 @@ test('each path shows its one section, marks its menu items current and names it
211
237
 
212
238
  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 () => {
213
239
  const ui = await boot();
214
- assert.deepEqual(ui.gets(), ['/api/bongos/community/leaderboard?window=all']);
240
+ assert.deepEqual(ui.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/community/builder-of-the-week']);
215
241
  assert.ok(ui.requests.every((r) => r.credentials === 'same-origin'));
216
242
  assert.equal(ui.$('boardLoading').hidden, true);
217
243
  assert.equal(ui.$('boardFailed').hidden, true);
@@ -229,7 +255,7 @@ test('the leaderboard, signed out: the all-time board in the server’s order wi
229
255
  assert.equal(find(bo, 'mineRow__hint').textContent, '1 work shipped · 0 karma');
230
256
  assert.equal(ui.$('boardNote').textContent, 'Credits earned across every project, since each builder started.');
231
257
  const signedIn = await boot({ me: { github_login: 'someone' } });
232
- assert.deepEqual(signedIn.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/my-projects'], 'signed in, the same board read: viewing is ungated, and the one more read is the Recruit door’s');
258
+ assert.deepEqual(signedIn.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/community/builder-of-the-week', '/api/bongos/my-projects'], 'signed in, the same board read: viewing is ungated, and the one more read is the Recruit door’s');
233
259
  assert.equal(signedIn.$('boardList').textContent, ui.$('boardList').textContent, 'the same rows for every viewer');
234
260
  });
235
261
 
@@ -325,6 +351,76 @@ test('recruiters: a door signed out, a refusal on a 403, rows only from a 200 as
325
351
  assert.equal(failed.$('recruitFailed').textContent, 'This list could not be read. Reload the page to try again.');
326
352
  });
327
353
 
354
+ // ── 2a. the builder of the week (task 1002981; spec D5) ──────────────────────
355
+ // The server decides each week and records it; the page draws what it sent, the same for
356
+ // every viewer: one line on the board, and its own page with the crown and the weeks before.
357
+
358
+ test('the board’s crown line: the winner, their credits and the week, linking to its page; nothing on no winner or a failed read', async () => {
359
+ const ui = await boot();
360
+ const line = ui.$('boardCrown');
361
+ assert.equal(line.hidden, false);
362
+ assert.equal(line.textContent, 'Builder of the week: Ana <i>L</i>, ' + (1240).toLocaleString() + ' credits, 19–25 Oct', 'a name is text, never markup');
363
+ const link = kids(line)[0];
364
+ assert.equal(link.tag, 'a');
365
+ assert.equal(link.attrs.href, '/community/week');
366
+ assert.equal(kids(line)[2].tag, 'strong', 'the name stands out of the line');
367
+ const signedIn = await boot({ me: { github_login: 'someone' } });
368
+ assert.equal(signedIn.$('boardCrown').textContent, line.textContent, 'the same crown for every viewer');
369
+ for (const answer of [{ status: 200, body: { ...WEEK(), winner: null } }, { status: 500, body: {} }, { status: 429, body: {} }, { reject: true }]) {
370
+ const reads = READS();
371
+ reads['/community/builder-of-the-week'] = answer;
372
+ const quiet = await boot({ reads });
373
+ assert.equal(quiet.$('boardCrown').hidden, true, 'no winner to show, or no answer: no line');
374
+ assert.equal(quiet.$('boardCrown').textContent, '');
375
+ assert.equal(quiet.$('boardFailed').hidden, true, 'and never the board’s failure');
376
+ assert.equal(kids(quiet.$('boardList')).length, 2);
377
+ }
378
+ });
379
+
380
+ test('the week page: the crown, then the past weeks in the server’s order with their dates; nothing about projects asked', async () => {
381
+ const ui = await boot({ pathname: '/community/week', me: { github_login: 'owner' } });
382
+ assert.deepEqual(ui.gets(), ['/api/bongos/community/builder-of-the-week'], 'one read: no board, and no Recruit door here');
383
+ assert.equal(ui.$('weekLoading').hidden, true);
384
+ assert.equal(ui.$('weekFailed').hidden, true);
385
+ assert.equal(ui.$('weekNone').hidden, true);
386
+ const crown = ui.$('weekCrown');
387
+ assert.equal(crown.hidden, false);
388
+ assert.equal(find(crown, 'crownWhen').textContent, '19–25 Oct 2026');
389
+ assert.equal(find(crown, 'crownName').textContent, 'Ana <i>L</i>');
390
+ assert.equal(find(crown, 'mineRow__host').textContent, '@ana');
391
+ assert.equal(find(crown, 'connAv').src, 'https://avatars.example/ana');
392
+ assert.equal(find(crown, 'crownNums').textContent, (1240).toLocaleString() + 'credits1work shipped', 'one work, singular');
393
+ assert.equal(ui.$('pastH').hidden, false);
394
+ const rows = kids(ui.$('weekPast'));
395
+ assert.deepEqual(rows.map((r) => find(r, 'commPlace').textContent), ['12–18 Oct', '28 Sep – 4 Oct', '29 Dec – 4 Jan'], 'newest first, as sent; a week across a month says both');
396
+ assert.equal(find(rows[0], 'mineRow__name').textContent, 'bo', 'no display name: the handle stands in');
397
+ assert.equal(find(rows[0], 'connAv').tag, 'span', 'an avatar that is not http(s) is never loaded');
398
+ assert.equal(find(rows[0], 'mineRow__hint').textContent, '2 works shipped');
399
+ assert.equal(find(rows[0], 'commNum').textContent, '300');
400
+ assert.equal(find(rows[0], 'commUnit').textContent, 'credits');
401
+ const out = await boot({ pathname: '/community/week' });
402
+ assert.equal(out.$('weekCrown').textContent, crown.textContent, 'the same page signed out');
403
+ });
404
+
405
+ test('the week page with no one to show, with no past weeks, and when the read fails', async () => {
406
+ const reads = READS();
407
+ reads['/community/builder-of-the-week'] = { status: 200, body: { week: { start: '2026-12-28', end: '2027-01-04' }, winner: null, past: [] } };
408
+ const none = await boot({ pathname: '/community/week', reads });
409
+ assert.equal(none.$('weekCrown').hidden, true);
410
+ assert.equal(none.$('weekNone').hidden, false);
411
+ assert.equal(none.$('weekNone').textContent, 'No builder of the week for 28 Dec 2026 – 3 Jan 2027.', 'a hidden winner and an empty week read the same (spec D8)');
412
+ assert.equal(none.$('pastH').hidden, true, 'no past weeks: no heading over nothing');
413
+ reads['/community/builder-of-the-week'] = { status: 200, body: { week: { start: 'soon' }, winner: null, past: [] } };
414
+ assert.equal((await boot({ pathname: '/community/week', reads })).$('weekNone').textContent, 'No builder of the week yet.', 'a date it cannot read is never drawn');
415
+ reads['/community/builder-of-the-week'] = { status: 500, body: {} };
416
+ const failed = await boot({ pathname: '/community/week', reads });
417
+ assert.equal(failed.$('weekFailed').hidden, false);
418
+ assert.equal(failed.$('weekFailed').textContent, 'The builder of the week could not be read. Reload the page to try again.');
419
+ assert.equal(failed.$('weekNone').hidden, true, 'a failure never reads as nobody this week');
420
+ reads['/community/builder-of-the-week'] = { status: 429, body: {} };
421
+ assert.match((await boot({ pathname: '/community/week', reads })).$('weekFailed').textContent, /^Too many requests/);
422
+ });
423
+
328
424
  // ── 2b. the Recruit door (task 1002292; spec D6, owner 2026-10-27) ───────────
329
425
  // A project OWNER gets a Recruit link on each row: the hub cannot read a project's
330
426
  // `project.recruit` (ADR 0335 D1), so GET /my-projects' `owned` stands in. The link is the
@@ -342,7 +438,7 @@ test('recruit: an owner gets a Recruit link on every row but their own, opening
342
438
  const reads = READS();
343
439
  reads['/my-projects'] = OWNED(['mercury', 'https://mercury.example/x?y=1', 'Mercury']);
344
440
  const ui = await boot({ me: { github_login: 'BO' }, reads });
345
- assert.deepEqual(ui.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/my-projects']);
441
+ assert.deepEqual(ui.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/community/builder-of-the-week', '/api/bongos/my-projects']);
346
442
  const [ana, bo] = kids(ui.$('boardList'));
347
443
  const a = recruitOf(ana);
348
444
  assert.equal(a.tag, 'a');
@@ -380,7 +476,7 @@ test('recruit: none for a builder who owns no project, signed out, on a failed r
380
476
  assert.equal(member.$('boardFor').hidden, true);
381
477
  assert.doesNotMatch(member.$('boardList').className, /canRecruit/);
382
478
  const out = await boot({ reads });
383
- assert.deepEqual(out.gets(), ['/api/bongos/community/leaderboard?window=all'], 'signed out, nobody is asked about projects');
479
+ assert.deepEqual(out.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/community/builder-of-the-week'], 'signed out, nobody is asked about projects');
384
480
  for (const answer of [{ status: 500, body: {} }, { status: 401, body: {} }, { reject: true }, OWNED(['evil', 'javascript:alert(1)', 'Evil'], ['none', null, 'None'])]) {
385
481
  reads['/my-projects'] = answer;
386
482
  const ui = await boot({ me: { github_login: 'someone' }, reads });
@@ -440,6 +536,7 @@ const HUB_ONLY = ['account.html', 'connections.html', 'invitations.html', 'guild
440
536
  const wrap = (apex, s) => (apex ? `<!--apex-only:start-->${s}<!--apex-only:end-->` : s);
441
537
  const ITEMS = [
442
538
  ['/community', 'Leaderboard', 'who earned the most, across every project'],
539
+ ['/community/week', 'Builder of the week', 'the top earner of each week, and how it is chosen'],
443
540
  ['/community/find', 'Find a builder', 'anyone building here, by name or role'],
444
541
  ['/community/recruiters', 'Open to recruiters', 'who is open to recruiting, for project owners'],
445
542
  ];
@@ -460,9 +557,9 @@ test('every page that wears the hub’s bar carries the Community menu before Pr
460
557
  const at = html.indexOf(apex ? `<!--apex-only:start-->${trig}` : trig);
461
558
  assert.ok(at > 0, `${f}: the Community trigger`);
462
559
  assert.ok(html.indexOf('id="projTrig"') > at, `${f}: Community comes before Projects`);
463
- assert.ok(html.includes(wrap(apex, band)), `${f}: the Community band, its three sub-pages`);
560
+ assert.ok(html.includes(wrap(apex, band)), `${f}: the Community band, its four sub-pages`);
464
561
  assert.ok(html.indexOf('id="menuCommunity"') < html.indexOf('id="menuProjects"'), `${f}: the bands in the bar’s order`);
465
- assert.ok(html.includes(wrap(apex, fold)), `${f}: the three folded into the Projects band for phones`);
562
+ assert.ok(html.includes(wrap(apex, fold)), `${f}: the four folded into the Projects band for phones`);
466
563
  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`);
467
564
  }
468
565
  });
@@ -520,7 +617,7 @@ function get(pathname, host) {
520
617
  });
521
618
  }
522
619
 
523
- test('GET /community, /community/find and /community/recruiters: one stamped page on the hub’s apex; never on a child or another host', async () => {
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 () => {
524
621
  modules.setModuleOverrides({ 'platform-identity': false });
525
622
  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`);
526
623
  modules.setModuleOverrides({ 'platform-identity': true });
@@ -533,7 +630,7 @@ test('GET /community, /community/find and /community/recruiters: one stamped pag
533
630
  bodies.push(hub.body);
534
631
  }
535
632
  assert.ok(bodies.every((b) => b === bodies[0]), 'byte-identical for every sub-page: the script picks the view');
536
- assert.doesNotMatch((await get('/community/everyone', 'game.test.local')).body, /— Community<\/title>/, 'only the three sub-pages');
633
+ assert.doesNotMatch((await get('/community/everyone', 'game.test.local')).body, /— Community<\/title>/, 'only the four sub-pages');
537
634
  for (const host of ['builders.test.local', 'status.test.local']) {
538
635
  assert.doesNotMatch((await get('/community', host)).body, /— Community<\/title>/, `${host} must not serve /community`);
539
636
  }
@@ -546,15 +643,18 @@ test('the kit renders each sub-page for everyone, the empty board, and a project
546
643
  const states = json('community.states.json');
547
644
  assert.equal(states.page, '/community');
548
645
  assert.deepEqual(states.modes, ['dark']);
549
- for (const s of ['out', 'out-30d', 'out-artists', 'out-find', 'out-find-search', 'out-recruiters', 'in-recruiters', 'in']) assert.ok(states.states[s], `kit state ${s}`);
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}`);
550
647
  assert.equal(states.states['out-find'].url, '/community/find');
648
+ 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');
551
650
  assert.equal(states.states['in-recruiters'].url, '/community/recruiters');
552
651
  assert.ok(states.states['in-recruiters'].expect.visible.includes('#recruitClosed'), 'signed in but not admitted: told, shown no one');
553
- 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']]) {
652
+ 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']]) {
554
653
  assert.equal(json(file).stub.fixtures[name], fixture);
555
654
  assert.ok(fs.existsSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', fixture + '.json')), `fixture ${fixture}`);
556
655
  }
557
656
  assert.equal(json('community-recruiter.states.json').states['in-recruiter'].url, '/community/recruiters');
657
+ assert.equal(json('community-week-none.states.json').states['out-week-none'].url, '/community/week');
558
658
  // the Recruit door (task 1002292): an owner of one project on the board and the search, and of two with the choice
559
659
  assert.ok(json('community-owner.states.json').states['in-owner'].expect.count.some(([sel, n]) => sel === '#boardList a.commRecruit' && n === 6));
560
660
  assert.equal(json('community-owner.states.json').states['in-owner-find'].url, '/community/find');
@@ -585,9 +685,14 @@ async function call(origin, p) {
585
685
  return { status: res.status, body: await res.json() };
586
686
  }
587
687
 
588
- test('the kit stub answers the board, the search and the recruiting door the way the routes do', async () => {
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 () => {
589
689
  const s = await stub();
590
690
  try {
691
+ const week = await call(s.origin, '/community/builder-of-the-week');
692
+ assert.deepEqual(Object.keys(week.body).sort(), ['past', 'week', 'winner'], 'the route’s three keys');
693
+ assert.ok(week.body.winner && week.body.past.length, 'a crown and the weeks before it');
694
+ assert.ok([week.body.winner, ...week.body.past].every((r) => !('github_id' in r) && Object.keys(r).sort().join() === 'avatar_url,credits,display_name,github_login,handle,tasks_shipped,week_start'), 'the module’s projection, no github_id');
695
+ assert.deepEqual(week.body.past.map((p) => p.week_start), [...week.body.past.map((p) => p.week_start)].sort().reverse(), 'newest first');
591
696
  const all = await call(s.origin, '/community/leaderboard');
592
697
  assert.equal(all.body.window, 'all');
593
698
  assert.deepEqual(all.body.results.map((r) => r.place), all.body.results.map((_, i) => i + 1));
@@ -606,6 +711,12 @@ test('the kit stub answers the board, the search and the recruiting door the way
606
711
  try {
607
712
  assert.equal((await call(member.origin, '/scouting')).status, 403, 'signed in, most builders are refused');
608
713
  } finally { member.stop(); }
714
+ const none = await stub({ FIXTURE_ALIAS: JSON.stringify({ 'community-week': 'community-week-none' }) });
715
+ try {
716
+ const r = await call(none.origin, '/community/builder-of-the-week');
717
+ assert.equal(r.body.winner, null);
718
+ assert.deepEqual(r.body.past, []);
719
+ } finally { none.stop(); }
609
720
  const owner = await stub({ AUTH: '1', FIXTURE_ALIAS: JSON.stringify({ scouting: 'scouting-recruiter' }) });
610
721
  try {
611
722
  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 three sub-pages, folded into the band under 480px (task 1003363); apex-only
197
- ['Leaderboard', '/community'], ['Find a builder', '/community/find'], ['Open to recruiters', '/community/recruiters'],
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'],
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');