@bongos/core 1.23.19 → 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 (32) hide show
  1. package/.bongos-core.json +62 -32
  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 +107 -101
  10. package/docs/copy-registry.json +189 -125
  11. package/docs/module-api-changelog.md +2 -0
  12. package/docs/page-inventory.json +3 -1
  13. package/docs/page-readings.json +73 -58
  14. package/modules/platform-identity/builder-of-the-week.js +18 -9
  15. package/modules/platform-identity/community-leaderboard.js +3 -0
  16. package/modules/platform-identity/community-movers.js +152 -0
  17. package/modules/platform-identity/routes/community-leaderboard.js +22 -2
  18. package/modules/public-landing/public/community-movers-none.states.json +15 -0
  19. package/modules/public-landing/public/community.html +62 -4
  20. package/modules/public-landing/public/community.states.json +1 -1
  21. package/modules/ui-design/kit/fixtures/community-movers-none.json +6 -0
  22. package/modules/ui-design/kit/fixtures/community-movers.json +12 -0
  23. package/modules/ui-design/kit/serve.js +8 -0
  24. package/package-lock.json +2 -2
  25. package/package.json +1 -1
  26. package/release-notes.json +6 -0
  27. package/scripts/bongos/run-unit-tests.js +4 -0
  28. package/src/module-api.js +1 -1
  29. package/tests/community_movers.mjs +280 -0
  30. package/tests/community_movers_db.mjs +147 -0
  31. package/tests/hub_community_page.mjs +78 -9
  32. package/tests/privacy_matrix.mjs +48 -9
@@ -3253,5 +3253,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3253
3253
  landed since 1.23.17 with no explicit bump. run 40611101597. (task 1002620)
3254
3254
  1.23.19 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3255
3255
  landed since 1.23.18 with no explicit bump. run 40613099968. (task 1002620)
3256
+ 1.23.20 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3257
+ landed since 1.23.19 with no explicit bump. run 40709473534. (task 1002620)
3256
3258
  ---------------------------------------------------------------------------
3257
3259
  ```
@@ -13,7 +13,7 @@
13
13
  "pages": 47,
14
14
  "surfaces": 3,
15
15
  "with_states": 40,
16
- "states": 352
16
+ "states": 353
17
17
  },
18
18
  "surfaces": [
19
19
  {
@@ -1456,6 +1456,7 @@
1456
1456
  "modules/public-landing/public/community-crowns-new-year.states.json",
1457
1457
  "modules/public-landing/public/community-crowns-none.states.json",
1458
1458
  "modules/public-landing/public/community-empty.states.json",
1459
+ "modules/public-landing/public/community-movers-none.states.json",
1459
1460
  "modules/public-landing/public/community-owner.states.json",
1460
1461
  "modules/public-landing/public/community-owner-two.states.json",
1461
1462
  "modules/public-landing/public/community-recruiter.states.json",
@@ -1482,6 +1483,7 @@
1482
1483
  "out-home-new-year",
1483
1484
  "out-home-none",
1484
1485
  "out-empty",
1486
+ "out-home-movers-none",
1485
1487
  "in-owner",
1486
1488
  "in-owner-find",
1487
1489
  "in-owner-two",
@@ -5,7 +5,7 @@
5
5
  "adr": "0341 D3",
6
6
  "reread": "node scripts/bongos/page-reader.js --stale",
7
7
  "how": ["Generated by node scripts/bongos/page-reader.js (BV2.TW04, task 1004315; ADR 0341 D3). Do not hand-edit.","One entry per page of docs/page-inventory.json. Lines are in page order: the default state first, and a line only another state shows sits where that state shows it, with its state named.","placement: placed = a docs/copy-registry.json row renders it (file, line, string_id); shared = shell text every page of the surface carries (top bar, sidebar, footer), with its row when the registry has one; unplaced = the registry cannot place it (a rewrite becomes an engineer task, ADR 0341 D4).","text keeps {…} where the registry row has a hole, or where a value the page read from its API was filled in. A string that is only such data is counted under omitted.data, not listed.","reading_hash is over the non-shared lines (key, section, text); files_hash is over the page's inventory files. --check compares files_hash only and never opens a browser.","Rendered through the hall-preview harness (hall) and the ui-design kit stub (landing, status), at 1280x800 with the clock pinned, so a reading is the fixture world's page."],
8
- "counts": {"pages":47,"lines":5271,"placed":1601,"shared":419,"unplaced":3251,"omitted_data":3563},
8
+ "counts": {"pages":47,"lines":5286,"placed":1607,"shared":419,"unplaced":3260,"omitted_data":3571},
9
9
  "pages": [
10
10
  {
11
11
  "id": "builders:atlas",
@@ -4490,42 +4490,42 @@
4490
4490
  "id": "landing:community",
4491
4491
  "surface": "landing",
4492
4492
  "title": "{{productName}} — Community",
4493
- "states_read": ["out","in","out-board","out-30d","out-artists","out-community-band","in-community-band","in-projects-band","out-projects-band","out-week","in-week","out-find","out-find-search","out-recruiters","in-recruiters","in-board","out-home-first","out-home-new-year","out-home-none","out-empty","in-owner","in-owner-find","in-owner-two","in-owner-two-picked","in-recruiter","out-week-none","out-board-no-crown"],
4494
- "files_hash": "9546be7928f86228",
4495
- "reading_hash": "a445d349aff849e4",
4496
- "counts": {"lines":117,"placed":46,"shared":12,"unplaced":59},
4497
- "omitted": {"data":59},
4493
+ "states_read": ["out","in","out-board","out-30d","out-artists","out-community-band","in-community-band","in-projects-band","out-projects-band","out-week","in-week","out-find","out-find-search","out-recruiters","in-recruiters","in-board","out-home-first","out-home-new-year","out-home-none","out-empty","out-home-movers-none","in-owner","in-owner-find","in-owner-two","in-owner-two-picked","in-recruiter","out-week-none","out-board-no-crown"],
4494
+ "files_hash": "9133cee62b885718",
4495
+ "reading_hash": "3cdb97deb6513c19",
4496
+ "counts": {"lines":132,"placed":52,"shared":12,"unplaced":68},
4497
+ "omitted": {"data":67},
4498
4498
  "notes": [],
4499
4499
  "lines": [
4500
4500
  {"key":"L0001","section":"top bar","text":"Community","placement":"shared"},
4501
4501
  {"key":"L0002","section":"top bar","text":"Projects","placement":"shared"},
4502
4502
  {"key":"L0003","section":"top bar","text":"Review","placement":"shared","state":"in"},
4503
4503
  {"key":"L0004","section":"page top","text":"Sky","placement":"unplaced","state":"in-projects-band"},
4504
- {"key":"L0005","section":"page top","text":"the sky of every project","placement":"placed","file":"modules/public-landing/public/community.html","line":188,"string_id":"8fa12f730019","state":"in-projects-band"},
4504
+ {"key":"L0005","section":"page top","text":"the sky of every project","placement":"placed","file":"modules/public-landing/public/community.html","line":200,"string_id":"8fa12f730019","state":"in-projects-band"},
4505
4505
  {"key":"L0006","section":"page top","text":"Table","placement":"unplaced","state":"in-projects-band"},
4506
- {"key":"L0007","section":"page top","text":"the same projects as rows","placement":"placed","file":"modules/public-landing/public/community.html","line":189,"string_id":"21453b3ba5c3","state":"in-projects-band"},
4507
- {"key":"L0008","section":"page top","text":"Your projects","placement":"placed","file":"modules/public-landing/public/community.html","line":190,"string_id":"4e25e6872321","state":"in-projects-band"},
4508
- {"key":"L0009","section":"page top","text":"the ones you own","placement":"placed","file":"modules/public-landing/public/community.html","line":190,"string_id":"a0fa148c60a6","state":"in-projects-band"},
4509
- {"key":"L0010","section":"page top","text":"New project","placement":"placed","file":"modules/public-landing/public/community.html","line":191,"string_id":"0a81a82d10c4","state":"in-projects-band"},
4510
- {"key":"L0011","section":"page top","text":"five questions and a home","placement":"placed","file":"modules/public-landing/public/community.html","line":191,"string_id":"94a3462373d2","state":"in-projects-band"},
4511
- {"key":"L0012","section":"top bar","text":"Sign in","placement":"shared","file":"modules/public-landing/public/community.html","line":169,"string_id":"ad80586f802a"},
4512
- {"key":"L0013","section":"Open to recruiters","text":"Open to recruiters","placement":"placed","file":"modules/public-landing/public/community.html","line":180,"string_id":"1e116d8fe95a","state":"out-recruiters"},
4513
- {"key":"L0014","section":"Open to recruiters","text":"Builders who keep their numbers hidden but are open to hearing from projects. Only project owners see who they are, and only by name.","placement":"placed","file":"modules/public-landing/public/community.html","line":321,"string_id":"932bb6ad495b","state":"out-recruiters"},
4506
+ {"key":"L0007","section":"page top","text":"the same projects as rows","placement":"placed","file":"modules/public-landing/public/community.html","line":201,"string_id":"21453b3ba5c3","state":"in-projects-band"},
4507
+ {"key":"L0008","section":"page top","text":"Your projects","placement":"placed","file":"modules/public-landing/public/community.html","line":202,"string_id":"4e25e6872321","state":"in-projects-band"},
4508
+ {"key":"L0009","section":"page top","text":"the ones you own","placement":"placed","file":"modules/public-landing/public/community.html","line":202,"string_id":"a0fa148c60a6","state":"in-projects-band"},
4509
+ {"key":"L0010","section":"page top","text":"New project","placement":"placed","file":"modules/public-landing/public/community.html","line":203,"string_id":"0a81a82d10c4","state":"in-projects-band"},
4510
+ {"key":"L0011","section":"page top","text":"five questions and a home","placement":"placed","file":"modules/public-landing/public/community.html","line":203,"string_id":"94a3462373d2","state":"in-projects-band"},
4511
+ {"key":"L0012","section":"top bar","text":"Sign in","placement":"shared","file":"modules/public-landing/public/community.html","line":181,"string_id":"ad80586f802a"},
4512
+ {"key":"L0013","section":"Open to recruiters","text":"Open to recruiters","placement":"placed","file":"modules/public-landing/public/community.html","line":192,"string_id":"1e116d8fe95a","state":"out-recruiters"},
4513
+ {"key":"L0014","section":"Open to recruiters","text":"Builders who keep their numbers hidden but are open to hearing from projects. Only project owners see who they are, and only by name.","placement":"placed","file":"modules/public-landing/public/community.html","line":341,"string_id":"932bb6ad495b","state":"out-recruiters"},
4514
4514
  {"key":"L0015","section":"Open to recruiters","text":"GitHub: {…}","placement":"unplaced","state":"in-recruiter","occurrences":3},
4515
- {"key":"L0016","section":"Open to recruiters","text":"Only project owners can see this list.","placement":"placed","file":"modules/public-landing/public/community.html","line":330,"string_id":"6bcd385b13d5","state":"in-recruiters"},
4516
- {"key":"L0017","section":"Open to recruiters","text":"Sign in to see whether this list is open to you.","placement":"placed","file":"modules/public-landing/public/community.html","line":324,"string_id":"f59de6034af4","state":"out-recruiters"},
4517
- {"key":"L0018","section":"Open to recruiters","text":"Sign in with GitHub","placement":"placed","file":"modules/public-landing/public/community.html","line":326,"string_id":"df38941b25a6","state":"out-recruiters"},
4518
- {"key":"L0019","section":"footer","text":"A non-profit, capture-resistant commons.","placement":"shared","file":"modules/public-landing/public/community.html","line":338,"string_id":"afe83bbbd0d4","state":"out-recruiters","occurrences":6},
4519
- {"key":"L0020","section":"footer","text":"Built by agents, on Cloud Bongos.","placement":"shared","file":"modules/public-landing/public/community.html","line":338,"string_id":"bd315dd71ea7","state":"out-recruiters","occurrences":6},
4520
- {"key":"L0021","section":"footer","text":"Open source","placement":"shared","file":"modules/public-landing/public/community.html","line":338,"string_id":"d6314ddcc1d0","state":"out-recruiters","occurrences":6},
4515
+ {"key":"L0016","section":"Open to recruiters","text":"Only project owners can see this list.","placement":"placed","file":"modules/public-landing/public/community.html","line":350,"string_id":"6bcd385b13d5","state":"in-recruiters"},
4516
+ {"key":"L0017","section":"Open to recruiters","text":"Sign in to see whether this list is open to you.","placement":"placed","file":"modules/public-landing/public/community.html","line":344,"string_id":"f59de6034af4","state":"out-recruiters"},
4517
+ {"key":"L0018","section":"Open to recruiters","text":"Sign in with GitHub","placement":"placed","file":"modules/public-landing/public/community.html","line":346,"string_id":"df38941b25a6","state":"out-recruiters"},
4518
+ {"key":"L0019","section":"footer","text":"A non-profit, capture-resistant commons.","placement":"shared","file":"modules/public-landing/public/community.html","line":358,"string_id":"afe83bbbd0d4","state":"out-recruiters","occurrences":6},
4519
+ {"key":"L0020","section":"footer","text":"Built by agents, on Cloud Bongos.","placement":"shared","file":"modules/public-landing/public/community.html","line":358,"string_id":"bd315dd71ea7","state":"out-recruiters","occurrences":6},
4520
+ {"key":"L0021","section":"footer","text":"Open source","placement":"shared","file":"modules/public-landing/public/community.html","line":358,"string_id":"d6314ddcc1d0","state":"out-recruiters","occurrences":6},
4521
4521
  {"key":"L0022","section":"footer","text":"AGPL-3.0","placement":"shared","state":"out-recruiters","occurrences":6},
4522
4522
  {"key":"L0023","section":"navigation","text":"Privacy","placement":"shared","state":"out-recruiters","occurrences":6},
4523
4523
  {"key":"L0024","section":"navigation","text":"Terms","placement":"shared","state":"out-recruiters","occurrences":6},
4524
4524
  {"key":"L0025","section":"navigation","text":"Contact","placement":"shared","state":"out-recruiters","occurrences":6},
4525
- {"key":"L0026","section":"navigation","text":"Builders' hall","placement":"shared","file":"modules/public-landing/public/community.html","line":338,"string_id":"90a128c97eae","state":"out-recruiters","occurrences":6},
4526
- {"key":"L0027","section":"Find a builder","text":"Find a builder","placement":"placed","file":"modules/public-landing/public/community.html","line":179,"string_id":"f1e93ba98ceb","state":"out-find"},
4527
- {"key":"L0028","section":"Find a builder","text":"Their name or handle","placement":"placed","file":"modules/public-landing/public/community.html","line":295,"string_id":"541d5a7612a9","attr":"placeholder","state":"out-find"},
4528
- {"key":"L0029","section":"Find a builder","text":"Any role","placement":"placed","file":"modules/public-landing/public/community.html","line":299,"string_id":"648862c67a23","state":"out-find"},
4525
+ {"key":"L0026","section":"navigation","text":"Builders' hall","placement":"shared","file":"modules/public-landing/public/community.html","line":358,"string_id":"90a128c97eae","state":"out-recruiters","occurrences":6},
4526
+ {"key":"L0027","section":"Find a builder","text":"Find a builder","placement":"placed","file":"modules/public-landing/public/community.html","line":191,"string_id":"f1e93ba98ceb","state":"out-find"},
4527
+ {"key":"L0028","section":"Find a builder","text":"Their name or handle","placement":"placed","file":"modules/public-landing/public/community.html","line":315,"string_id":"541d5a7612a9","attr":"placeholder","state":"out-find"},
4528
+ {"key":"L0029","section":"Find a builder","text":"Any role","placement":"placed","file":"modules/public-landing/public/community.html","line":319,"string_id":"648862c67a23","state":"out-find"},
4529
4529
  {"key":"L0030","section":"Find a builder","text":"Engineers","placement":"unplaced","state":"out-find"},
4530
4530
  {"key":"L0031","section":"Find a builder","text":"Artists","placement":"unplaced","state":"out-find"},
4531
4531
  {"key":"L0032","section":"Find a builder","text":"Ideators","placement":"unplaced","state":"out-find"},
@@ -4535,32 +4535,32 @@
4535
4535
  {"key":"L0036","section":"Find a builder","text":"2 builders found.","placement":"unplaced","state":"out-find-search"},
4536
4536
  {"key":"L0037","section":"Find a builder","text":"{…} · designer","placement":"unplaced","state":"out-find-search"},
4537
4537
  {"key":"L0038","section":"Find a builder","text":"Recruit","placement":"unplaced","state":"in-owner-find","occurrences":2},
4538
- {"key":"L0039","section":"page top","text":"Community home","placement":"placed","file":"modules/public-landing/public/community.html","line":176,"string_id":"d380e1ef9508","state":"out-community-band"},
4539
- {"key":"L0040","section":"page top","text":"the builders of the week, month and year","placement":"placed","file":"modules/public-landing/public/community.html","line":176,"string_id":"10dac85c9e17","state":"out-community-band"},
4538
+ {"key":"L0039","section":"page top","text":"Community home","placement":"placed","file":"modules/public-landing/public/community.html","line":188,"string_id":"d380e1ef9508","state":"out-community-band"},
4539
+ {"key":"L0040","section":"page top","text":"the builders of the week, month and year","placement":"placed","file":"modules/public-landing/public/community.html","line":188,"string_id":"10dac85c9e17","state":"out-community-band"},
4540
4540
  {"key":"L0041","section":"page top","text":"Leaderboard","placement":"unplaced","state":"out-community-band"},
4541
- {"key":"L0042","section":"page top","text":"who earned the most, across every project","placement":"placed","file":"modules/public-landing/public/community.html","line":177,"string_id":"776e80cf232d","state":"out-community-band"},
4542
- {"key":"L0043","section":"page top","text":"Builder of the week","placement":"placed","file":"modules/public-landing/public/community.html","line":178,"string_id":"39b61cc5172e","state":"out-community-band"},
4543
- {"key":"L0044","section":"page top","text":"the top earner of each week, and how it is chosen","placement":"placed","file":"modules/public-landing/public/community.html","line":178,"string_id":"15237c234a72","state":"out-community-band"},
4544
- {"key":"L0045","section":"page top","text":"Find a builder","placement":"placed","file":"modules/public-landing/public/community.html","line":179,"string_id":"f1e93ba98ceb","state":"out-community-band"},
4545
- {"key":"L0046","section":"page top","text":"anyone building here, by name or role","placement":"placed","file":"modules/public-landing/public/community.html","line":179,"string_id":"a4e84a1501c5","state":"out-community-band"},
4546
- {"key":"L0047","section":"page top","text":"Open to recruiters","placement":"placed","file":"modules/public-landing/public/community.html","line":180,"string_id":"1e116d8fe95a","state":"out-community-band"},
4547
- {"key":"L0048","section":"page top","text":"who is open to recruiting, for project owners","placement":"placed","file":"modules/public-landing/public/community.html","line":180,"string_id":"15ac45627fab","state":"out-community-band"},
4548
- {"key":"L0049","section":"page top","text":"Builders you may know","placement":"placed","file":"modules/public-landing/public/community.html","line":181,"string_id":"724de83c77c6","state":"in-community-band"},
4549
- {"key":"L0050","section":"page top","text":"connections or projects in common","placement":"placed","file":"modules/public-landing/public/community.html","line":181,"string_id":"171cee573b19","state":"in-community-band"},
4541
+ {"key":"L0042","section":"page top","text":"who earned the most, across every project","placement":"placed","file":"modules/public-landing/public/community.html","line":189,"string_id":"776e80cf232d","state":"out-community-band"},
4542
+ {"key":"L0043","section":"page top","text":"Builder of the week","placement":"placed","file":"modules/public-landing/public/community.html","line":190,"string_id":"39b61cc5172e","state":"out-community-band"},
4543
+ {"key":"L0044","section":"page top","text":"the top earner of each week, and how it is chosen","placement":"placed","file":"modules/public-landing/public/community.html","line":190,"string_id":"15237c234a72","state":"out-community-band"},
4544
+ {"key":"L0045","section":"page top","text":"Find a builder","placement":"placed","file":"modules/public-landing/public/community.html","line":191,"string_id":"f1e93ba98ceb","state":"out-community-band"},
4545
+ {"key":"L0046","section":"page top","text":"anyone building here, by name or role","placement":"placed","file":"modules/public-landing/public/community.html","line":191,"string_id":"a4e84a1501c5","state":"out-community-band"},
4546
+ {"key":"L0047","section":"page top","text":"Open to recruiters","placement":"placed","file":"modules/public-landing/public/community.html","line":192,"string_id":"1e116d8fe95a","state":"out-community-band"},
4547
+ {"key":"L0048","section":"page top","text":"who is open to recruiting, for project owners","placement":"placed","file":"modules/public-landing/public/community.html","line":192,"string_id":"15ac45627fab","state":"out-community-band"},
4548
+ {"key":"L0049","section":"page top","text":"Builders you may know","placement":"placed","file":"modules/public-landing/public/community.html","line":193,"string_id":"724de83c77c6","state":"in-community-band"},
4549
+ {"key":"L0050","section":"page top","text":"connections or projects in common","placement":"placed","file":"modules/public-landing/public/community.html","line":193,"string_id":"171cee573b19","state":"in-community-band"},
4550
4550
  {"key":"L0051","section":"Leaderboard","text":"Leaderboard","placement":"unplaced","state":"out-board"},
4551
- {"key":"L0052","section":"Leaderboard","text":"Builder of the week","placement":"placed","file":"modules/public-landing/public/community.html","line":178,"string_id":"39b61cc5172e","state":"out-board"},
4551
+ {"key":"L0052","section":"Leaderboard","text":"Builder of the week","placement":"placed","file":"modules/public-landing/public/community.html","line":190,"string_id":"39b61cc5172e","state":"out-board"},
4552
4552
  {"key":"L0053","section":"Leaderboard","text":", 1,240 {…}, 19–25 Oct","placement":"unplaced","state":"out-board"},
4553
4553
  {"key":"L0054","section":"Leaderboard","text":"Everyone","placement":"unplaced","state":"out-board"},
4554
4554
  {"key":"L0055","section":"Leaderboard","text":"Engineers","placement":"unplaced","state":"out-board"},
4555
4555
  {"key":"L0056","section":"Leaderboard","text":"Artists","placement":"unplaced","state":"out-board"},
4556
4556
  {"key":"L0057","section":"Leaderboard","text":"Ideators","placement":"unplaced","state":"out-board"},
4557
4557
  {"key":"L0058","section":"Leaderboard","text":"Designers","placement":"unplaced","state":"out-board"},
4558
- {"key":"L0059","section":"Leaderboard","text":"Last 7 days","placement":"placed","file":"modules/public-landing/public/community.html","line":256,"string_id":"fc3582ac098d","state":"out-board"},
4559
- {"key":"L0060","section":"Leaderboard","text":"Last 30 days","placement":"placed","file":"modules/public-landing/public/community.html","line":257,"string_id":"52f6b5974994","state":"out-board"},
4560
- {"key":"L0061","section":"Leaderboard","text":"Past year","placement":"placed","file":"modules/public-landing/public/community.html","line":258,"string_id":"42a9479b375c","state":"out-board"},
4561
- {"key":"L0062","section":"Leaderboard","text":"All time","placement":"placed","file":"modules/public-landing/public/community.html","line":259,"string_id":"23c761dcff62","state":"out-board"},
4558
+ {"key":"L0059","section":"Leaderboard","text":"Last 7 days","placement":"placed","file":"modules/public-landing/public/community.html","line":276,"string_id":"fc3582ac098d","state":"out-board"},
4559
+ {"key":"L0060","section":"Leaderboard","text":"Last 30 days","placement":"placed","file":"modules/public-landing/public/community.html","line":277,"string_id":"52f6b5974994","state":"out-board"},
4560
+ {"key":"L0061","section":"Leaderboard","text":"Past year","placement":"placed","file":"modules/public-landing/public/community.html","line":278,"string_id":"42a9479b375c","state":"out-board"},
4561
+ {"key":"L0062","section":"Leaderboard","text":"All time","placement":"placed","file":"modules/public-landing/public/community.html","line":279,"string_id":"23c761dcff62","state":"out-board"},
4562
4562
  {"key":"L0063","section":"Leaderboard","text":"Credits earned in the last 30 days, across every project.","placement":"unplaced","state":"out-30d"},
4563
- {"key":"L0064","section":"Leaderboard","text":"{{Currency}} earned across every project, since each builder started.","placement":"placed","file":"modules/public-landing/public/community.html","line":263,"string_id":"f81129627f7b","state":"out-board"},
4563
+ {"key":"L0064","section":"Leaderboard","text":"{{Currency}} earned across every project, since each builder started.","placement":"placed","file":"modules/public-landing/public/community.html","line":283,"string_id":"f81129627f7b","state":"out-board"},
4564
4564
  {"key":"L0065","section":"Leaderboard","text":"Recruit to","placement":"unplaced","state":"in-owner-two"},
4565
4565
  {"key":"L0066","section":"Leaderboard","text":"Recruit opens its hall with their name filled in, and you send the invite there.","placement":"unplaced","state":"in-owner-two"},
4566
4566
  {"key":"L0067","section":"Leaderboard","text":"Recruit opens {…}’s hall with their name filled in, and you send the invite there.","placement":"unplaced","state":"in-owner"},
@@ -4574,19 +4574,19 @@
4574
4574
  {"key":"L0075","section":"Leaderboard","text":"31 works shipped · 18 karma","placement":"unplaced","state":"out-board"},
4575
4575
  {"key":"L0076","section":"Leaderboard","text":"12 works shipped · 7 karma","placement":"unplaced","state":"out-board"},
4576
4576
  {"key":"L0077","section":"Leaderboard","text":"1 work shipped · 0 karma","placement":"unplaced","state":"out-board"},
4577
- {"key":"L0078","section":"Leaderboard","text":"A project reports a builder’s numbers when they sign in or ship there, so a window counts work when it was reported, not when it was done.","placement":"placed","file":"modules/public-landing/public/community.html","line":269,"string_id":"8d79db29b7d6","state":"out-board"},
4578
- {"key":"L0079","section":"Top builders","text":"Top builders","placement":"placed","file":"modules/public-landing/public/community.html","line":220,"string_id":"bb89654b6d4f"},
4579
- {"key":"L0080","section":"Builder of the week","text":"Builder of the week","placement":"placed","file":"modules/public-landing/public/community.html","line":178,"string_id":"39b61cc5172e"},
4577
+ {"key":"L0078","section":"Leaderboard","text":"A project reports a builder’s numbers when they sign in or ship there, so a window counts work when it was reported, not when it was done.","placement":"placed","file":"modules/public-landing/public/community.html","line":289,"string_id":"8d79db29b7d6","state":"out-board"},
4578
+ {"key":"L0079","section":"Top builders","text":"Top builders","placement":"placed","file":"modules/public-landing/public/community.html","line":232,"string_id":"bb89654b6d4f"},
4579
+ {"key":"L0080","section":"Builder of the week","text":"Builder of the week","placement":"placed","file":"modules/public-landing/public/community.html","line":190,"string_id":"39b61cc5172e"},
4580
4580
  {"key":"L0081","section":"Builder of the week","text":"No {…} of the week for 19–25 Oct 2026.","placement":"unplaced","state":"out-week-none"},
4581
4581
  {"key":"L0082","section":"Builder of the week","text":"4–10 Jan 2027","placement":"unplaced","state":"out-home-new-year"},
4582
4582
  {"key":"L0083","section":"Builder of the week","text":"19–25 Oct 2026","placement":"unplaced","state":"out-week"},
4583
4583
  {"key":"L0084","section":"Builder of the week","text":"26 Oct – 1 Nov 2026","placement":"unplaced"},
4584
4584
  {"key":"L0085","section":"Builder of the week","text":"No {…} of the week for 26 Oct – 1 Nov 2026.","placement":"unplaced","state":"out-home-none"},
4585
4585
  {"key":"L0086","section":"Builder of the week","text":"works shipped","placement":"unplaced"},
4586
- {"key":"L0087","section":"How it is chosen","text":"How it is chosen","placement":"placed","file":"modules/public-landing/public/community.html","line":282,"string_id":"104fbd1c98fd","state":"out-week"},
4587
- {"key":"L0088","section":"How it is chosen","text":"The builder who earned the most {{currency}} across every project in one week, Monday to Sunday (UTC), among the builders who shipped at least one work that week. A tie goes to whoever shipped more works, then to whoever earned more karma. Builders who hide their numbers or keep their account private are not counted.","placement":"placed","file":"modules/public-landing/public/community.html","line":283,"string_id":"5fe47a649523","state":"out-week"},
4588
- {"key":"L0089","section":"How it is chosen","text":"Each week is decided once, the first time a community page asks for it after it ends, and it does not change after that. A project reports a builder’s numbers when they sign in or ship there, so a week counts work when it was reported, not when it was done.","placement":"placed","file":"modules/public-landing/public/community.html","line":284,"string_id":"b451221d44da","state":"out-week"},
4589
- {"key":"L0090","section":"Past weeks","text":"Past weeks","placement":"placed","file":"modules/public-landing/public/community.html","line":285,"string_id":"5825a37975e0","state":"out-week"},
4586
+ {"key":"L0087","section":"How it is chosen","text":"How it is chosen","placement":"placed","file":"modules/public-landing/public/community.html","line":302,"string_id":"104fbd1c98fd","state":"out-week"},
4587
+ {"key":"L0088","section":"How it is chosen","text":"The builder who earned the most {{currency}} across every project in one week, Monday to Sunday (UTC), among the builders who shipped at least one work that week. A tie goes to whoever shipped more works, then to whoever earned more karma. Builders who hide their numbers or keep their account private are not counted.","placement":"placed","file":"modules/public-landing/public/community.html","line":303,"string_id":"5fe47a649523","state":"out-week"},
4588
+ {"key":"L0089","section":"How it is chosen","text":"Each week is decided once, the first time a community page asks for it after it ends, and it does not change after that. A project reports a builder’s numbers when they sign in or ship there, so a week counts work when it was reported, not when it was done.","placement":"placed","file":"modules/public-landing/public/community.html","line":304,"string_id":"b451221d44da","state":"out-week"},
4589
+ {"key":"L0090","section":"Past weeks","text":"Past weeks","placement":"placed","file":"modules/public-landing/public/community.html","line":305,"string_id":"5825a37975e0","state":"out-week"},
4590
4590
  {"key":"L0091","section":"Past weeks","text":"12–18 Oct","placement":"unplaced","state":"out-week"},
4591
4591
  {"key":"L0092","section":"Past weeks","text":"9 works shipped","placement":"unplaced","state":"out-week"},
4592
4592
  {"key":"L0093","section":"Past weeks","text":"5–11 Oct","placement":"unplaced","state":"out-week"},
@@ -4595,25 +4595,40 @@
4595
4595
  {"key":"L0096","section":"Past weeks","text":"6 works shipped","placement":"unplaced","state":"out-week"},
4596
4596
  {"key":"L0097","section":"Past weeks","text":"14–20 Sep","placement":"unplaced","state":"out-week"},
4597
4597
  {"key":"L0098","section":"Past weeks","text":"2 works shipped","placement":"unplaced","state":"out-week"},
4598
- {"key":"L0099","section":"Builder of the month","text":"Builder of the month","placement":"placed","file":"modules/public-landing/public/community.html","line":225,"string_id":"9e64afc8d912"},
4598
+ {"key":"L0099","section":"Builder of the month","text":"Builder of the month","placement":"placed","file":"modules/public-landing/public/community.html","line":237,"string_id":"9e64afc8d912"},
4599
4599
  {"key":"L0100","section":"Builder of the month","text":"December 2026","placement":"unplaced","state":"out-home-new-year"},
4600
4600
  {"key":"L0101","section":"Builder of the month","text":"September 2026","placement":"unplaced","state":"out-home-first"},
4601
4601
  {"key":"L0102","section":"Builder of the month","text":"The first {…} of the month is named on 1 Nov 2026.","placement":"unplaced","state":"out-home-first"},
4602
4602
  {"key":"L0103","section":"Builder of the month","text":"October 2026","placement":"unplaced"},
4603
4603
  {"key":"L0104","section":"Builder of the month","text":"No {…} of the month for October 2026.","placement":"unplaced","state":"out-home-none"},
4604
4604
  {"key":"L0105","section":"Builder of the month","text":"works shipped","placement":"unplaced"},
4605
- {"key":"L0106","section":"Builder of the year","text":"Builder of the year","placement":"placed","file":"modules/public-landing/public/community.html","line":226,"string_id":"bb31301c010e"},
4605
+ {"key":"L0106","section":"Builder of the year","text":"Builder of the year","placement":"placed","file":"modules/public-landing/public/community.html","line":238,"string_id":"bb31301c010e"},
4606
4606
  {"key":"L0107","section":"Builder of the year","text":"2027 so far","placement":"unplaced","state":"out-home-new-year"},
4607
4607
  {"key":"L0108","section":"Builder of the year","text":"2026 so far, since 27 Sep 2026","placement":"unplaced"},
4608
4608
  {"key":"L0109","section":"Builder of the year","text":"No {…} of 2026 yet.","placement":"unplaced","state":"out-home-none"},
4609
4609
  {"key":"L0110","section":"Builder of the year","text":"works shipped","placement":"unplaced"},
4610
- {"key":"L0111","section":"Builder of the year","text":"Each is the builder who earned the most {{currency}} across every project in that time, among the builders who shipped at least one work in it. A tie goes to whoever shipped more works, then to whoever earned more karma. Builders who hide their numbers or keep their account private are not counted. The week and the month are decided once, when they end; the year shows the leader so far, and is decided on 1 January.","placement":"placed","file":"modules/public-landing/public/community.html","line":228,"string_id":"544a3fb1602a"},
4611
- {"key":"L0112","section":"Community pages","text":"Community pages","placement":"placed","file":"modules/public-landing/public/community.html","line":229,"string_id":"f195f790ab49"},
4612
- {"key":"L0113","section":"Community pages","text":"Leaderboard","placement":"unplaced"},
4613
- {"key":"L0114","section":"Community pages","text":"Builder of the week","placement":"placed","file":"modules/public-landing/public/community.html","line":178,"string_id":"39b61cc5172e"},
4614
- {"key":"L0115","section":"Community pages","text":"Find a builder","placement":"placed","file":"modules/public-landing/public/community.html","line":179,"string_id":"f1e93ba98ceb"},
4615
- {"key":"L0116","section":"Community pages","text":"Open to recruiters","placement":"placed","file":"modules/public-landing/public/community.html","line":180,"string_id":"1e116d8fe95a"},
4616
- {"key":"L0117","section":"Community pages","text":"Builders you may know","placement":"placed","file":"modules/public-landing/public/community.html","line":181,"string_id":"724de83c77c6","state":"in"}
4610
+ {"key":"L0111","section":"Builder of the year","text":"Each is the builder who earned the most {{currency}} across every project in that time, among the builders who shipped at least one work in it. A tie goes to whoever shipped more works, then to whoever earned more karma. Builders who hide their numbers or keep their account private are not counted. The week and the month are decided once, when they end; the year shows the leader so far, and is decided on 1 January.","placement":"placed","file":"modules/public-landing/public/community.html","line":240,"string_id":"544a3fb1602a"},
4611
+ {"key":"L0112","section":"Rising builder","text":"Rising builder","placement":"placed","file":"modules/public-landing/public/community.html","line":245,"string_id":"a09a9f7fe86d"},
4612
+ {"key":"L0113","section":"Rising builder","text":"The last 30 days","placement":"placed","file":"modules/public-landing/public/community.html","line":245,"string_id":"e6ee07ea9204"},
4613
+ {"key":"L0114","section":"Rising builder","text":"No rising {…} in the last 30 days.","placement":"unplaced","state":"out-home-movers-none"},
4614
+ {"key":"L0115","section":"Rising builder","text":"works shipped","placement":"unplaced"},
4615
+ {"key":"L0116","section":"Projects on the move","text":"Projects on the move","placement":"placed","file":"modules/public-landing/public/community.html","line":246,"string_id":"c99aad168284"},
4616
+ {"key":"L0117","section":"Projects on the move","text":"The last 30 days","placement":"placed","file":"modules/public-landing/public/community.html","line":245,"string_id":"e6ee07ea9204"},
4617
+ {"key":"L0118","section":"Projects on the move","text":"No public project shipped a work in the last 30 days.","placement":"placed","file":"modules/public-landing/public/community.html","line":246,"string_id":"cf4c05abde4f","state":"out-home-movers-none"},
4618
+ {"key":"L0119","section":"Projects on the move","text":"1,840 {…} earned","placement":"unplaced"},
4619
+ {"key":"L0120","section":"Projects on the move","text":"works shipped","placement":"unplaced","occurrences":4},
4620
+ {"key":"L0121","section":"Projects on the move","text":"2,210 {…} earned","placement":"unplaced"},
4621
+ {"key":"L0122","section":"Projects on the move","text":"610 {…} earned","placement":"unplaced"},
4622
+ {"key":"L0123","section":"Projects on the move","text":"380 {…} earned","placement":"unplaced"},
4623
+ {"key":"L0124","section":"Projects on the move","text":"45 {…} earned","placement":"unplaced"},
4624
+ {"key":"L0125","section":"Projects on the move","text":"work shipped","placement":"unplaced"},
4625
+ {"key":"L0126","section":"Projects on the move","text":"The rising builder is the one who earned the most {{currency}} in the last 30 days, among the builders who had earned under 100 {{currency}} before them and shipped at least one work in them. Projects on the move are the public projects that shipped the most works in the last 30 days, then earned the most {{currency}}. Builders who hide their numbers or keep their account private are not counted in either.","placement":"placed","file":"modules/public-landing/public/community.html","line":248,"string_id":"3ff62ec88969"},
4626
+ {"key":"L0127","section":"Community pages","text":"Community pages","placement":"placed","file":"modules/public-landing/public/community.html","line":249,"string_id":"f195f790ab49"},
4627
+ {"key":"L0128","section":"Community pages","text":"Leaderboard","placement":"unplaced"},
4628
+ {"key":"L0129","section":"Community pages","text":"Builder of the week","placement":"placed","file":"modules/public-landing/public/community.html","line":190,"string_id":"39b61cc5172e"},
4629
+ {"key":"L0130","section":"Community pages","text":"Find a builder","placement":"placed","file":"modules/public-landing/public/community.html","line":191,"string_id":"f1e93ba98ceb"},
4630
+ {"key":"L0131","section":"Community pages","text":"Open to recruiters","placement":"placed","file":"modules/public-landing/public/community.html","line":192,"string_id":"1e116d8fe95a"},
4631
+ {"key":"L0132","section":"Community pages","text":"Builders you may know","placement":"placed","file":"modules/public-landing/public/community.html","line":193,"string_id":"724de83c77c6","state":"in"}
4617
4632
  ]
4618
4633
  },
4619
4634
  {
@@ -53,13 +53,15 @@ function crownedWeek(now = new Date()) {
53
53
 
54
54
  const isoDate = (d) => d.toISOString().slice(0, 10);
55
55
 
56
- // The winner of [start, end) among eligible builders, or null; the week's, and the month's
57
- // and year's (community-crowns.js). Arithmetic is bigint for the leaderboard's reason: the
56
+ // The per-(project, builder) deltas over [$1, $2] among eligible builders, as the CTEs
57
+ // `act` and `per_project`: the week's, month's and year's (computeWinner below) and the 30
58
+ // days' (community-movers.js). Arithmetic is bigint for the leaderboard's reason: the
58
59
  // columns are instance-reported int4. `fromFirstSnapshot` is the YEAR's rule (owner,
59
60
  // 2026-10-27): a row with no snapshot before the start, and not first seen inside, counts
60
61
  // from its first snapshot inside instead of not at all, since history began in late
61
62
  // September 2026 and the rule above would leave that year to new builders alone.
62
- async function computeWinner(start, end, pool, { fromFirstSnapshot = false } = {}) {
63
+ // `base_credits` is what a row had at the start by the same rule.
64
+ function periodRowsSql({ fromFirstSnapshot = false } = {}) {
63
65
  const base = (col) => (fromFirstSnapshot
64
66
  ? `COALESCE(b.${col}, CASE WHEN act.first_seen_at >= $1::timestamptz THEN 0 ELSE f.${col} END, 0)`
65
67
  : `COALESCE(b.${col}, 0)`);
@@ -74,17 +76,17 @@ async function computeWinner(start, end, pool, { fromFirstSnapshot = false } = {
74
76
  LIMIT 1
75
77
  ) f ON true`
76
78
  : '';
77
- const { rows } = await pool.query(
78
- `WITH act AS (
79
+ return `WITH act AS (
79
80
  SELECT a.client_id, a.github_id, a.first_seen_at, pa.handle, pa.github_login
80
81
  FROM platform_identity_builder_project_activity a
81
82
  JOIN platform_identity_accounts pa ON pa.github_id = a.github_id
82
83
  WHERE ${rollupVisibleSql('pa')}
83
84
  ), per_project AS (
84
- SELECT act.github_id, act.handle, act.github_login,
85
+ SELECT act.client_id, act.github_id, act.handle, act.github_login,
85
86
  COALESCE(e.credits, 0)::bigint - ${base('credits')} AS credits,
86
87
  COALESCE(e.tasks_shipped, 0)::bigint - ${base('tasks_shipped')} AS tasks_shipped,
87
- COALESCE(e.karma, 0)::bigint - ${base('karma')} AS karma
88
+ COALESCE(e.karma, 0)::bigint - ${base('karma')} AS karma,
89
+ ${base('credits')}::bigint AS base_credits
88
90
  FROM act
89
91
  LEFT JOIN LATERAL (
90
92
  SELECT s.credits, s.tasks_shipped, s.karma
@@ -103,7 +105,13 @@ async function computeWinner(start, end, pool, { fromFirstSnapshot = false } = {
103
105
  LIMIT 1
104
106
  ) b ON true${firstInside}
105
107
  WHERE b.credits IS NOT NULL OR act.first_seen_at >= $1::timestamptz${fromFirstSnapshot ? ' OR f.credits IS NOT NULL' : ''}
106
- )
108
+ )`;
109
+ }
110
+
111
+ // The winner of [start, end) among eligible builders, or null.
112
+ async function computeWinner(start, end, pool, { fromFirstSnapshot = false } = {}) {
113
+ const { rows } = await pool.query(
114
+ `${periodRowsSql({ fromFirstSnapshot })}
107
115
  SELECT github_id, sum(credits)::bigint AS credits, sum(tasks_shipped)::bigint AS tasks_shipped
108
116
  FROM per_project
109
117
  GROUP BY github_id, handle, github_login
@@ -201,8 +209,9 @@ module.exports = {
201
209
  crownedWeek,
202
210
  clearCrownMemo,
203
211
  ARCHIVE_WEEKS,
204
- // for community-crowns.js (the month and the year)
212
+ // for community-crowns.js (the month and the year) and community-movers.js (the 30 days)
205
213
  computeWinner,
214
+ periodRowsSql,
206
215
  crownOnce,
207
216
  isoDate,
208
217
  };
@@ -231,4 +231,7 @@ module.exports = {
231
231
  MAX_LIMIT,
232
232
  MAX_OFFSET,
233
233
  MAX_DISCIPLINE_CHARS,
234
+ // for community-movers.js (the rising builder), so who is shown and the tie order are one rule
235
+ eligibleIdentities,
236
+ compareRows,
234
237
  };
@@ -0,0 +1,152 @@
1
+ // modules/platform-identity/community-movers.js — the rising builder and the projects on the
2
+ // move, the read behind GET /community/movers for the row under the crowns on the hub's
3
+ // /community home (community spec D7 items 1–2; task 1002983, goal 1000110 CT7).
4
+ //
5
+ // THE 30 DAYS are rolling, up to now, and read from the snapshot history by the year's
6
+ // baseline rule (builder-of-the-week.js periodRowsSql, fromFirstSnapshot): a row first seen
7
+ // inside counts from zero, a row with no snapshot before them from its first one inside (an
8
+ // undercount, never a lifetime).
9
+ //
10
+ // THE RISING BUILDER (owner, 2026-10-28): the most credits earned in the 30 days among the
11
+ // builders who had under RISING_BASE_MAX before them, with at least one work shipped in
12
+ // them; ties as the crowns. "Before" is each row's baseline, and a row with no history in
13
+ // the 30 days at all counts its live total, so a veteran's quiet projects still count.
14
+ //
15
+ // THE PROJECTS ON THE MOVE (owner, 2026-10-28): the most works shipped in the 30 days, then
16
+ // credits, the top PROJECTS_MAX. Only the builders the leaderboard may show are counted
17
+ // (spec D8), and a project is named only where a profile may name it: on the map with the
18
+ // Public door (task 1003558); private and stealth projects are left off, not anonymised.
19
+ //
20
+ // TWO HALVES, as the leaderboard: the 30 days' rows (one snapshot probe per activity row)
21
+ // are memoised per pool for MOVERS_TTL_MS; who and which projects may be shown is asked on
22
+ // every read, so a hide, a private account or a closed door beats the memo. Viewer-
23
+ // independent; github_id and client_id never cross the projection.
24
+ 'use strict';
25
+
26
+ const { pool: defaultPool } = require('../../src/module-api');
27
+ const { periodRowsSql } = require('./builder-of-the-week');
28
+ const { eligibleIdentities, compareRows, windowStart } = require('./community-leaderboard');
29
+ const { onMapSql } = require('./projects-feed');
30
+
31
+ const WINDOW = '30d';
32
+ const RISING_BASE_MAX = 100;
33
+ const PROJECTS_MAX = 5;
34
+ const MOVERS_TTL_MS = 30 * 1000;
35
+
36
+ // Every eligible (project, builder) row's 30 days: [{ clientId, githubId, credits,
37
+ // tasks_shipped, karma, base }]. A row the period rule leaves out moved nowhere in them, so
38
+ // its deltas are zero and its baseline is its live total.
39
+ async function computeRows(start, end, pool) {
40
+ const { rows } = await pool.query(
41
+ `${periodRowsSql({ fromFirstSnapshot: true })}
42
+ SELECT a.client_id, a.github_id, pp.credits, pp.tasks_shipped, pp.karma,
43
+ COALESCE(pp.base_credits, a.credits::bigint) AS base_credits
44
+ FROM act
45
+ JOIN platform_identity_builder_project_activity a
46
+ ON a.client_id = act.client_id AND a.github_id = act.github_id
47
+ LEFT JOIN per_project pp
48
+ ON pp.client_id = act.client_id AND pp.github_id = act.github_id`,
49
+ [start, end],
50
+ );
51
+ return rows.map((r) => ({
52
+ clientId: String(r.client_id),
53
+ githubId: String(r.github_id),
54
+ credits: Number(r.credits || 0),
55
+ tasks_shipped: Number(r.tasks_shipped || 0),
56
+ karma: Number(r.karma || 0),
57
+ base: Number(r.base_credits || 0),
58
+ }));
59
+ }
60
+
61
+ // pool → { at, start, end, promise }. Concurrent reads share one in-flight computation; a
62
+ // failed one is dropped so the next read retries.
63
+ const rowsMemo = new Map();
64
+ function windowRows(now, pool) {
65
+ const m = rowsMemo.get(pool);
66
+ const at = now.getTime();
67
+ if (m && at >= m.at && at - m.at < MOVERS_TTL_MS) return m;
68
+ const start = windowStart(WINDOW, now);
69
+ const entry = { at, start, end: now, promise: computeRows(start, now, pool) };
70
+ entry.promise.catch(() => { if (rowsMemo.get(pool) === entry) rowsMemo.delete(pool); });
71
+ rowsMemo.set(pool, entry);
72
+ return entry;
73
+ }
74
+
75
+ // The projects among `clientIds` a profile may name, now: Map clientId → { name, origin }.
76
+ async function namedProjects(clientIds, pool) {
77
+ if (clientIds.length === 0) return new Map();
78
+ const { rows } = await pool.query(
79
+ `SELECT c.client_id, p.name, p.origin
80
+ FROM platform_identity_sso_clients c
81
+ JOIN platform_identity_projects p ON p.origin = c.origin
82
+ WHERE c.client_id = ANY($1::text[])
83
+ AND ${onMapSql('p')} AND p.visibility = 'public'`,
84
+ [clientIds],
85
+ );
86
+ return new Map(rows.map((r) => [String(r.client_id), { name: r.name, origin: r.origin }]));
87
+ }
88
+
89
+ // Sums `rows` per `key`, from zero.
90
+ function sumBy(rows, key) {
91
+ const out = new Map();
92
+ for (const r of rows) {
93
+ const k = r[key];
94
+ const s = out.get(k) || { credits: 0, tasks_shipped: 0, karma: 0, base: 0 };
95
+ s.credits += r.credits;
96
+ s.tasks_shipped += r.tasks_shipped;
97
+ s.karma += r.karma;
98
+ s.base += r.base;
99
+ out.set(k, s);
100
+ }
101
+ return out;
102
+ }
103
+
104
+ function risingBuilder(rows, identities) {
105
+ const ranked = [...sumBy(rows, 'githubId')]
106
+ .filter(([, s]) => s.base < RISING_BASE_MAX && s.tasks_shipped >= 1)
107
+ .map(([id, s]) => ({ ...identities.get(id), credits: s.credits, tasks_shipped: s.tasks_shipped, karma: s.karma }))
108
+ .sort(compareRows);
109
+ if (!ranked.length) return null;
110
+ const { handle, github_login, display_name, avatar_url, credits, tasks_shipped } = ranked[0];
111
+ return { handle, github_login, display_name, avatar_url, credits, tasks_shipped };
112
+ }
113
+
114
+ const byText = (x, y) => (x < y ? -1 : x > y ? 1 : 0);
115
+ function projectsOnTheMove(rows, named) {
116
+ return [...sumBy(rows.filter((r) => named.has(r.clientId)), 'clientId')]
117
+ .filter(([, s]) => s.tasks_shipped >= 1)
118
+ .map(([id, s]) => ({ ...named.get(id), tasks_shipped: s.tasks_shipped, credits: s.credits }))
119
+ .sort((a, b) => (b.tasks_shipped - a.tasks_shipped) || (b.credits - a.credits)
120
+ || byText(String(a.name), String(b.name)) || byText(String(a.origin), String(b.origin)))
121
+ .slice(0, PROJECTS_MAX);
122
+ }
123
+
124
+ // readMovers — { window: { start, end }, rising, projects }: the window as ISO instants,
125
+ // rising null when nobody qualifies or may be shown, projects best first (at most
126
+ // PROJECTS_MAX). `now` is injectable for tests.
127
+ async function readMovers({ now = new Date(), pool = defaultPool } = {}) {
128
+ const w = windowRows(now, pool);
129
+ const rows = await w.promise;
130
+ // Only a builder or project with a shipped work in the 30 days can be on either card.
131
+ const shipped = rows.filter((r) => r.tasks_shipped > 0);
132
+ const identities = await eligibleIdentities([...new Set(shipped.map((r) => r.githubId))], pool);
133
+ const shown = rows.filter((r) => identities.has(r.githubId));
134
+ const named = await namedProjects([...new Set(shown.filter((r) => r.tasks_shipped > 0).map((r) => r.clientId))], pool);
135
+ return {
136
+ window: { start: w.start.toISOString(), end: w.end.toISOString() },
137
+ rising: risingBuilder(shown, identities),
138
+ projects: projectsOnTheMove(shown, named),
139
+ };
140
+ }
141
+
142
+ function clearMoversMemo() {
143
+ rowsMemo.clear();
144
+ }
145
+
146
+ module.exports = {
147
+ readMovers,
148
+ clearMoversMemo,
149
+ RISING_BASE_MAX,
150
+ PROJECTS_MAX,
151
+ MOVERS_TTL_MS,
152
+ };
@@ -9,7 +9,8 @@
9
9
  // It also serves GET /community/builder-of-the-week (spec D5, task 1002981), the
10
10
  // board's weekly crown, from ../builder-of-the-week.js on the same terms, and GET
11
11
  // /community/crowns (task 1004902), the week, month and year for the hub's /community
12
- // page, from ../community-crowns.js.
12
+ // page, from ../community-crowns.js, and GET /community/movers (task 1002983), the rising
13
+ // builder and the projects on the move under them, from ../community-movers.js.
13
14
  //
14
15
  // Reaches core only through the doorway (ADR 0083).
15
16
  'use strict';
@@ -19,6 +20,7 @@ const api = require('../../../src/module-api');
19
20
  const leaderboard = require('../community-leaderboard');
20
21
  const builderOfTheWeek = require('../builder-of-the-week');
21
22
  const communityCrowns = require('../community-crowns');
23
+ const communityMovers = require('../community-movers');
22
24
  const { createReadRateLimit } = require('../read-rate-limit');
23
25
 
24
26
  const { pool, corsPublicGet } = api;
@@ -31,6 +33,8 @@ const WEEK_READ_RATE_SCOPE = 'builder-of-the-week-read';
31
33
  const weekReadRateLimit = createReadRateLimit({ scope: WEEK_READ_RATE_SCOPE, limit: READ_RATE_LIMIT });
32
34
  const CROWNS_READ_RATE_SCOPE = 'community-crowns-read';
33
35
  const crownsReadRateLimit = createReadRateLimit({ scope: CROWNS_READ_RATE_SCOPE, limit: READ_RATE_LIMIT });
36
+ const MOVERS_READ_RATE_SCOPE = 'community-movers-read';
37
+ const moversReadRateLimit = createReadRateLimit({ scope: MOVERS_READ_RATE_SCOPE, limit: READ_RATE_LIMIT });
34
38
 
35
39
  module.exports = function communityLeaderboardRoutes() {
36
40
  const router = express.Router();
@@ -109,7 +113,23 @@ module.exports = function communityLeaderboardRoutes() {
109
113
  }
110
114
  });
111
115
 
116
+ router.use('/community/movers', corsPublicGet('/community/movers'));
117
+
118
+ // rank: public — the rising builder and the projects on the move, the last 30 days (task
119
+ // 1002983). Takes no parameters and writes nothing. Viewer-independent; no-store so a
120
+ // builder who hides leaves it on the next read (ADR 0171 D5).
121
+ router.get('/community/movers', moversReadRateLimit, async (req, res) => {
122
+ try {
123
+ const out = await communityMovers.readMovers({ pool });
124
+ res.set('Cache-Control', 'no-store');
125
+ res.json(out);
126
+ } catch (err) {
127
+ api.logger('platform-identity').error({ err: err && err.message }, 'GET /community/movers failed');
128
+ res.fail('community_movers_failed', { status: 500, message: 'internal error' });
129
+ }
130
+ });
131
+
112
132
  return router;
113
133
  };
114
134
 
115
- module.exports._internals = { READ_RATE_LIMIT, READ_RATE_SCOPE, WEEK_READ_RATE_SCOPE, CROWNS_READ_RATE_SCOPE };
135
+ module.exports._internals = { READ_RATE_LIMIT, READ_RATE_SCOPE, WEEK_READ_RATE_SCOPE, CROWNS_READ_RATE_SCOPE, MOVERS_READ_RATE_SCOPE };