@bongos/core 1.23.19 → 1.23.21

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 (44) hide show
  1. package/.bongos-core.json +82 -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 +107 -101
  10. package/docs/copy-registry.json +189 -125
  11. package/docs/module-api-changelog.md +4 -0
  12. package/docs/page-inventory.json +3 -1
  13. package/docs/page-readings.json +73 -58
  14. package/modules/copy-desk/chrome-pages.js +178 -0
  15. package/modules/copy-desk/page-data.js +34 -2
  16. package/modules/copy-desk/tests/copy_chrome_pages.mjs +127 -0
  17. package/modules/copy-desk/tests/copy_no_cms.mjs +1 -1
  18. package/modules/platform-identity/builder-of-the-week.js +18 -9
  19. package/modules/platform-identity/community-leaderboard.js +3 -0
  20. package/modules/platform-identity/community-movers.js +152 -0
  21. package/modules/platform-identity/routes/community-leaderboard.js +22 -2
  22. package/modules/public-landing/public/community-movers-none.states.json +15 -0
  23. package/modules/public-landing/public/community.html +62 -4
  24. package/modules/public-landing/public/community.states.json +1 -1
  25. package/modules/ui-design/kit/fixtures/community-movers-none.json +6 -0
  26. package/modules/ui-design/kit/fixtures/community-movers.json +12 -0
  27. package/modules/ui-design/kit/serve.js +8 -0
  28. package/package-lock.json +2 -2
  29. package/package.json +1 -1
  30. package/release-notes.json +12 -0
  31. package/scripts/bongos/copy-apply.js +3 -1
  32. package/scripts/bongos/run-unit-tests.js +4 -0
  33. package/scripts/bongos/tweak-renders.js +9 -2
  34. package/src/module-api.js +1 -1
  35. package/tests/community_movers.mjs +280 -0
  36. package/tests/community_movers_db.mjs +147 -0
  37. package/tests/copy_desk_page_docx.mjs +3 -3
  38. package/tests/copy_desk_page_editor.mjs +6 -6
  39. package/tests/copy_desk_page_next.mjs +3 -2
  40. package/tests/copy_desk_page_reads.mjs +2 -1
  41. package/tests/copy_desk_page_submit.mjs +30 -2
  42. package/tests/hub_community_page.mjs +78 -9
  43. package/tests/privacy_matrix.mjs +48 -9
  44. package/tests/profile_role_stats.mjs +2 -1
@@ -3253,5 +3253,9 @@ 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)
3258
+ 1.23.21 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3259
+ landed since 1.23.20 with no explicit bump. run 40804017640. (task 1002620)
3256
3260
  ---------------------------------------------------------------------------
3257
3261
  ```
@@ -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
  {
@@ -0,0 +1,178 @@
1
+ // modules/copy-desk/chrome-pages.js — the shared page chrome as its own pages
2
+ // (task 1004904, ADR 0341 D3's "shared" placement).
3
+ //
4
+ // The reader (scripts/bongos/page-reader.js) records every visible line of a page,
5
+ // and the skip link, top bar, navigation and footer are on every page of a
6
+ // surface, so each page's reading carried the same few dozen lines. An artist
7
+ // tweaking one page was shown, and could rewrite, words that change on all of
8
+ // them. This is the read-side split:
9
+ //
10
+ // * A real page's reading loses its chrome lines. They are not shown, not
11
+ // editable, not counted, not in its drift.
12
+ // * Each surface gains one page per chrome section: "<surface>:top-bar",
13
+ // "<surface>:navigation", "<surface>:footer", "<surface>:skip-link". Each
14
+ // lists every DISTINCT line of that section once, in the order it first
15
+ // appears, and goes through the same claim, draft, submit and approve flow
16
+ // as any page.
17
+ //
18
+ // PURE, like pages.js: it derives from the two committed artifacts and writes
19
+ // nothing. The generators and the committed files are untouched, so the
20
+ // reader's --stale check and the registry are exactly as they were, and a
21
+ // surface with no chrome gains no page.
22
+
23
+ 'use strict';
24
+
25
+ const crypto = require('node:crypto');
26
+
27
+ // section name the reader writes -> the page it becomes
28
+ const CHROME = Object.freeze([
29
+ Object.freeze({ section: 'top bar', slug: 'top-bar', title: 'Top bar' }),
30
+ Object.freeze({ section: 'navigation', slug: 'navigation', title: 'Navigation' }),
31
+ Object.freeze({ section: 'footer', slug: 'footer', title: 'Footer' }),
32
+ Object.freeze({ section: 'skip link', slug: 'skip-link', title: 'Skip link' }),
33
+ Object.freeze({ section: 'sidebar', slug: 'sidebar', title: 'Sidebar' }),
34
+ ]);
35
+
36
+ const CHROME_SECTIONS = new Set(CHROME.map((c) => c.section));
37
+
38
+ function isChromeLine(line) {
39
+ return !!line && line.placement === 'shared' && CHROME_SECTIONS.has(line.section);
40
+ }
41
+
42
+ function hash(parts) {
43
+ return crypto.createHash('sha256').update(parts.join('\n')).digest('hex').slice(0, 16);
44
+ }
45
+
46
+ function key(n) { return `L${String(n).padStart(4, '0')}`; }
47
+
48
+ function countsOf(lines) {
49
+ return {
50
+ lines: lines.length,
51
+ placed: lines.filter((l) => l.placement === 'placed').length,
52
+ shared: lines.filter((l) => l.placement === 'shared').length,
53
+ unplaced: lines.filter((l) => l.placement === 'unplaced').length,
54
+ };
55
+ }
56
+
57
+ // The chrome page ids a surface can have, so a caller can tell one from a page.
58
+ // A reading names its surface; one that does not is read from its id.
59
+ function surfaceOf(page) { return page.surface || String(page.id || '').split(':')[0]; }
60
+
61
+ function chromePageId(surface, slug) { return `${surface}:${slug}`; }
62
+ function isChromePageId(id) {
63
+ const m = /^[a-z0-9-]+:([a-z0-9-]+)$/.exec(String(id || ''));
64
+ return !!m && CHROME.some((c) => c.slug === m[1]);
65
+ }
66
+
67
+ // readings -> readings, every real page without its chrome and the chrome pages
68
+ // after them. Returns { readings, chrome } where chrome maps a surface to the
69
+ // pages it gained (the inventory is built from it).
70
+ function splitReadings(readings) {
71
+ const pages = Array.isArray(readings && readings.pages) ? readings.pages : [];
72
+ const real = [];
73
+ // surface -> slug -> { lines: Map text -> line, from: page, states: Set, sources: Set of pages }
74
+ const gathered = new Map();
75
+ const order = [];
76
+ for (const p of pages) {
77
+ const lines = Array.isArray(p && p.lines) ? p.lines : [];
78
+ const kept = [];
79
+ for (const l of lines) {
80
+ if (!isChromeLine(l)) { kept.push(l); continue; }
81
+ const spec = CHROME.find((c) => c.section === l.section);
82
+ const surface = surfaceOf(p);
83
+ if (!gathered.has(surface)) { gathered.set(surface, new Map()); order.push(surface); }
84
+ const bySlug = gathered.get(surface);
85
+ if (!bySlug.has(spec.slug)) bySlug.set(spec.slug, { spec, lines: new Map(), from: p, states: new Set(), sources: new Set() });
86
+ const g = bySlug.get(spec.slug);
87
+ const text = String(l.text);
88
+ const seen = g.lines.get(text);
89
+ if (!seen) g.lines.set(text, { ...l });
90
+ else if (!seen.file && l.file) { seen.file = l.file; seen.line = l.line; seen.string_id = l.string_id; }
91
+ if (!g.sources.has(p)) {
92
+ g.sources.add(p);
93
+ for (const s of p.states_read || []) g.states.add(s);
94
+ }
95
+ }
96
+ if (kept.length === lines.length) { real.push(p); continue; }
97
+ real.push({
98
+ ...p,
99
+ ...(p.counts ? { counts: countsOf(kept) } : {}),
100
+ lines: kept,
101
+ });
102
+ }
103
+ const chrome = new Map();
104
+ const made = [];
105
+ for (const surface of order) {
106
+ const bySlug = gathered.get(surface);
107
+ const list = [];
108
+ for (const spec of CHROME) {
109
+ const g = bySlug.get(spec.slug);
110
+ if (!g) continue;
111
+ const lines = [...g.lines.values()].map((l, i) => ({ ...l, key: key(i + 1) }));
112
+ const id = chromePageId(surface, spec.slug);
113
+ made.push({
114
+ id,
115
+ surface,
116
+ title: spec.title,
117
+ states_read: [...g.states],
118
+ files_hash: hash([...g.sources].map((s) => String(s.files_hash || ''))),
119
+ reading_hash: hash(lines.map((l) => `${l.section}\t${l.text}`)),
120
+ counts: countsOf(lines),
121
+ omitted: {},
122
+ notes: [`The ${spec.title.toLowerCase()} shown on every ${surface} page, each line once. Read from ${g.sources.size} pages.`],
123
+ lines,
124
+ });
125
+ list.push({ id, spec, from: g.from, sources: g.sources, lines });
126
+ }
127
+ chrome.set(surface, list);
128
+ }
129
+ const out = { ...readings, pages: [...real, ...made] };
130
+ if (readings.counts) {
131
+ // Totals are counted from the lines, not from each page's own counts: a
132
+ // reading need not carry them.
133
+ const all = countsOf(out.pages.flatMap((p) => p.lines || []));
134
+ out.counts = { ...readings.counts, pages: out.pages.length, ...all };
135
+ }
136
+ return { readings: out, chrome };
137
+ }
138
+
139
+ // inventory -> inventory, with the chrome pages of each surface after that
140
+ // surface's own pages. A chrome page opens on the first page it was read from
141
+ // (the live page shows it), and its files are the ones its lines come from, so
142
+ // the claim's touches and the "changed since" read name the shell, not a page.
143
+ function addChromePages(inventory, chrome) {
144
+ const pages = Array.isArray(inventory && inventory.pages) ? inventory.pages : [];
145
+ const byId = new Map(pages.map((p) => [p.id, p]));
146
+ const added = [];
147
+ for (const [surface, list] of chrome) {
148
+ for (const c of list) {
149
+ const src = byId.get(c.from.id) || {};
150
+ const files = [...new Set(c.lines.map((l) => l.file).filter(Boolean))];
151
+ added.push({
152
+ id: c.id,
153
+ surface,
154
+ module: src.module || null,
155
+ page: c.spec.slug,
156
+ id_from: 'shared-chrome',
157
+ title: c.spec.title,
158
+ document_title: `${c.spec.title} — shown on every ${surface} page`,
159
+ path: src.path || null,
160
+ url: src.url || null,
161
+ files,
162
+ states_files: [],
163
+ states: [],
164
+ });
165
+ }
166
+ }
167
+ const all = [...pages, ...added];
168
+ const out = { ...inventory, pages: all };
169
+ if (inventory.counts) out.counts = { ...inventory.counts, pages: all.length };
170
+ if (Array.isArray(inventory.surfaces)) {
171
+ const tally = new Map();
172
+ for (const p of all) tally.set(p.surface, (tally.get(p.surface) || 0) + 1);
173
+ out.surfaces = inventory.surfaces.map((s) => ({ ...s, pages: tally.get(s.id) || 0 }));
174
+ }
175
+ return out;
176
+ }
177
+
178
+ module.exports = { isChromeLine, isChromePageId, splitReadings, addChromePages };
@@ -24,6 +24,7 @@
24
24
 
25
25
  const fs = require('node:fs');
26
26
  const path = require('node:path');
27
+ const chromePages = require('./chrome-pages');
27
28
 
28
29
  const ROOT = path.resolve(__dirname, '..', '..');
29
30
 
@@ -73,7 +74,38 @@ function load(which) {
73
74
  return value;
74
75
  }
75
76
 
76
- function loadInventory() { return load('inventory'); }
77
- function loadReadings() { return load('readings'); }
77
+ // The shared chrome (skip link, top bar, navigation, footer) is read out of
78
+ // every page and handed back as pages of its own (chrome-pages.js, task
79
+ // 1004904). Derived per raw load, so the committed files stay what the
80
+ // generators wrote; a WeakMap keeps one derivation per cached read.
81
+ const _derived = new WeakMap();
82
+ function derive() {
83
+ const rawReadings = load('readings');
84
+ if (!rawReadings.ok) return null;
85
+ const hit = _derived.get(rawReadings);
86
+ if (hit) return hit;
87
+ const split = chromePages.splitReadings(rawReadings.data);
88
+ const value = { readings: { ok: true, data: split.readings, at: rawReadings.at }, chrome: split.chrome };
89
+ _derived.set(rawReadings, value);
90
+ return value;
91
+ }
92
+
93
+ const _derivedInventory = new WeakMap();
94
+ function loadInventory() {
95
+ const raw = load('inventory');
96
+ if (!raw.ok) return raw;
97
+ const d = derive();
98
+ if (!d) return raw;
99
+ const key = d.readings;
100
+ const hit = _derivedInventory.get(raw);
101
+ if (hit && hit.key === key) return hit.value;
102
+ const value = { ok: true, data: chromePages.addChromePages(raw.data, d.chrome), at: raw.at };
103
+ _derivedInventory.set(raw, { key, value });
104
+ return value;
105
+ }
106
+ function loadReadings() {
107
+ const d = derive();
108
+ return d ? d.readings : load('readings');
109
+ }
78
110
 
79
111
  module.exports = { ARTIFACTS, loadInventory, loadReadings, _setRoot };