@bongos/core 1.21.112 → 1.21.114

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 (265) hide show
  1. package/.bongos-core.json +616 -186
  2. package/.claude/skills/ask-for-help/SKILL.md +1 -1
  3. package/.claude/skills/bongos-feedback/SKILL.md +1 -1
  4. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  5. package/.claude/skills/builder-ship/SKILL.md +1 -0
  6. package/GOVERNMENT.md +128 -11
  7. package/clients/bongos-client/README.md +1 -1
  8. package/clients/bongos-client/bongos-client.global.js +28 -0
  9. package/clients/bongos-client/index.cjs +28 -0
  10. package/clients/bongos-client/index.d.ts +45 -2
  11. package/clients/bongos-client/index.mjs +28 -0
  12. package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
  13. package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
  14. package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
  15. package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
  16. package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
  17. package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
  18. package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
  19. package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
  20. package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
  21. package/docs/adr/README.md +10 -1
  22. package/docs/api/openapi.json +989 -87
  23. package/docs/api-reference.md +18 -4
  24. package/docs/copy-inventory.md +343 -262
  25. package/docs/copy-registry.json +1223 -426
  26. package/docs/file-map.md +2 -1
  27. package/docs/module-api-changelog.md +4 -0
  28. package/docs/packs/engineer.md +2 -1
  29. package/docs/page-inventory.json +91 -6
  30. package/docs/page-readings.json +5017 -4458
  31. package/modules/builder-settings/law-locks.js +202 -0
  32. package/modules/builder-settings/routes/settings.js +66 -6
  33. package/modules/builder-settings/settings.js +7 -0
  34. package/modules/discord/ship-broadcast.js +22 -13
  35. package/modules/government/asks-db.js +54 -0
  36. package/modules/government/asks.js +269 -0
  37. package/modules/government/board-idea-reads.js +68 -0
  38. package/modules/government/board.js +21 -57
  39. package/modules/government/build-rights.js +148 -0
  40. package/modules/government/buildouts.js +160 -0
  41. package/modules/government/counts.js +81 -0
  42. package/modules/government/db.js +242 -0
  43. package/modules/government/decision-board.js +234 -8
  44. package/modules/government/decision-rules.js +2 -0
  45. package/modules/government/docket.js +114 -6
  46. package/modules/government/law-board.js +131 -0
  47. package/modules/government/laws.js +241 -0
  48. package/modules/government/leads.js +170 -0
  49. package/modules/government/migrations/government_027_laws.sql +143 -0
  50. package/modules/government/migrations/government_028_asks.sql +57 -0
  51. package/modules/government/migrations/government_029_leads.sql +135 -0
  52. package/modules/government/migrations/government_030_buildouts.sql +111 -0
  53. package/modules/government/migrations/government_031_treasury.sql +107 -0
  54. package/modules/government/module.json +2 -1
  55. package/modules/government/pollers/treasury-borrow.js +62 -0
  56. package/modules/government/positions.js +107 -0
  57. package/modules/government/presets.js +149 -0
  58. package/modules/government/routes/government.js +425 -2
  59. package/modules/government/routing.js +154 -0
  60. package/modules/government/treasury-db.js +166 -0
  61. package/modules/government/treasury.js +241 -0
  62. package/modules/hall-ui/public/atlas.html +2 -2
  63. package/modules/hall-ui/public/blockers.html +2 -2
  64. package/modules/hall-ui/public/board-room.html +2 -2
  65. package/modules/hall-ui/public/board-room.js +2 -0
  66. package/modules/hall-ui/public/city-draw.js +604 -11
  67. package/modules/hall-ui/public/city.css +157 -1
  68. package/modules/hall-ui/public/city.html +37 -7
  69. package/modules/hall-ui/public/city.js +441 -21
  70. package/modules/hall-ui/public/city.states.json +110 -5
  71. package/modules/hall-ui/public/collab.html +2 -2
  72. package/modules/hall-ui/public/commands.html +2 -2
  73. package/modules/hall-ui/public/copy-desk.html +2 -2
  74. package/modules/hall-ui/public/craft-mode.js +112 -29
  75. package/modules/hall-ui/public/deploy.html +2 -2
  76. package/modules/hall-ui/public/diagram-viewer.css +12 -0
  77. package/modules/hall-ui/public/diagram-viewer.js +4 -0
  78. package/modules/hall-ui/public/diagrams.html +2 -2
  79. package/modules/hall-ui/public/drachmae.html +2 -2
  80. package/modules/hall-ui/public/fleet.html +2 -2
  81. package/modules/hall-ui/public/gate.html +2 -2
  82. package/modules/hall-ui/public/goal-map.html +2 -2
  83. package/modules/hall-ui/public/goals.html +2 -2
  84. package/modules/hall-ui/public/gov-feeds.js +141 -0
  85. package/modules/hall-ui/public/gov-living.js +1002 -0
  86. package/modules/hall-ui/public/gov-positions.js +97 -0
  87. package/modules/hall-ui/public/government.html +2 -2
  88. package/modules/hall-ui/public/hall-render.js +3 -0
  89. package/modules/hall-ui/public/idea.html +2 -2
  90. package/modules/hall-ui/public/ideas.html +2 -2
  91. package/modules/hall-ui/public/index.html +3 -3
  92. package/modules/hall-ui/public/modules.html +2 -2
  93. package/modules/hall-ui/public/my-agents.css +334 -0
  94. package/modules/hall-ui/public/my-agents.html +72 -0
  95. package/modules/hall-ui/public/my-agents.js +603 -0
  96. package/modules/hall-ui/public/my-agents.states.json +37 -0
  97. package/modules/hall-ui/public/primer.html +2 -2
  98. package/modules/hall-ui/public/profile-connections.js +40 -28
  99. package/modules/hall-ui/public/profile.html +2 -2
  100. package/modules/hall-ui/public/project-settings.html +2 -2
  101. package/modules/hall-ui/public/ranks.html +2 -2
  102. package/modules/hall-ui/public/roadmap.html +2 -2
  103. package/modules/hall-ui/public/room.css +8 -0
  104. package/modules/hall-ui/public/roster.html +2 -2
  105. package/modules/hall-ui/public/sessions.html +2 -2
  106. package/modules/hall-ui/public/settings.html +2 -2
  107. package/modules/hall-ui/public/shell.js +116 -16
  108. package/modules/hall-ui/public/street-card.js +18 -1
  109. package/modules/hall-ui/public/street-deck.css +299 -0
  110. package/modules/hall-ui/public/street-deck.js +1145 -0
  111. package/modules/hall-ui/public/street-depth.js +712 -0
  112. package/modules/hall-ui/public/street-desk.js +5 -2
  113. package/modules/hall-ui/public/street-draw.js +109 -16
  114. package/modules/hall-ui/public/street-inside.js +137 -22
  115. package/modules/hall-ui/public/street-office.js +705 -0
  116. package/modules/hall-ui/public/street.css +161 -11
  117. package/modules/hall-ui/public/street.html +41 -11
  118. package/modules/hall-ui/public/street.js +78 -66
  119. package/modules/hall-ui/public/street.states.json +129 -21
  120. package/modules/hall-ui/public/studio.html +2 -2
  121. package/modules/hall-ui/public/style.css +5 -1
  122. package/modules/hall-ui/public/task.html +2 -2
  123. package/modules/hall-ui/public/thinking.html +2 -2
  124. package/modules/hall-ui/public/tweak-editor.html +2 -2
  125. package/modules/hall-ui/public/watch.html +2 -2
  126. package/modules/hall-ui/public/watch.js +14 -1
  127. package/modules/hall-ui/public/work.html +2 -2
  128. package/modules/hall-ui/records/living-city.md +57 -0
  129. package/modules/hall-ui/records/my-agents.md +21 -0
  130. package/modules/hall-ui/records/oversight-family.md +1 -0
  131. package/modules/hall-ui/records/profile-connections.md +10 -0
  132. package/modules/hall-ui/records/reading-rooms.md +2 -0
  133. package/modules/ideas/routes/blockers.js +92 -3
  134. package/modules/lifecycle/module.json +2 -1
  135. package/modules/lifecycle/moving.js +17 -1
  136. package/modules/lifecycle/routes/moving.js +6 -0
  137. package/modules/lifecycle/routes/project-settings.js +23 -0
  138. package/modules/onboarding/approval-broadcast.js +8 -0
  139. package/modules/onboarding/routes/access-requests.js +16 -6
  140. package/modules/platform-identity/connections.js +13 -2
  141. package/modules/platform-identity/routes/connections.js +7 -1
  142. package/modules/public-landing/public/account-private.states.json +2 -2
  143. package/modules/public-landing/public/account.html +65 -5
  144. package/modules/public-landing/public/account.states.json +3 -3
  145. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  146. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  147. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
  148. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  149. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  150. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
  151. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  152. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  153. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
  154. package/modules/public-landing/public/assets/world.css +4 -4
  155. package/modules/public-landing/public/connections-empty.states.json +14 -0
  156. package/modules/public-landing/public/connections-inactive.states.json +14 -0
  157. package/modules/public-landing/public/connections.html +450 -0
  158. package/modules/public-landing/public/connections.probes.json +414 -0
  159. package/modules/public-landing/public/connections.states.json +17 -0
  160. package/modules/public-landing/public/contact.html +2 -2
  161. package/modules/public-landing/public/guild.html +2 -2
  162. package/modules/public-landing/public/index.html +2 -2
  163. package/modules/public-landing/public/invitations.html +2 -2
  164. package/modules/public-landing/public/privacy.html +2 -2
  165. package/modules/public-landing/public/projects.html +2 -2
  166. package/modules/public-landing/public/projects.probes.json +6 -2
  167. package/modules/public-landing/public/terms.html +2 -2
  168. package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
  169. package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
  170. package/modules/ui-design/kit/fixtures/connections.json +22 -0
  171. package/modules/ui-design/kit/serve.js +95 -0
  172. package/package-lock.json +2 -2
  173. package/package.json +1 -1
  174. package/release-notes.json +128 -0
  175. package/scripts/discord/README.md +8 -5
  176. package/scripts/discord/post.js +12 -1
  177. package/scripts/gds/artifact-audit.js +20 -3
  178. package/scripts/gds/ask.js +164 -0
  179. package/scripts/gds/autobongos-run.js +20 -5
  180. package/scripts/gds/autobongos-tweak-lane.js +262 -0
  181. package/scripts/gds/autobongos-verify.js +21 -2
  182. package/scripts/gds/claim.js +2 -0
  183. package/scripts/gds/government-walk.js +638 -4
  184. package/scripts/gds/pulse.js +9 -6
  185. package/scripts/gds/ship-escalation.js +22 -3
  186. package/scripts/gds/ship-finish.js +5 -4
  187. package/scripts/gds/ship-merge-gate.js +18 -6
  188. package/scripts/gds/tweak-halt.js +1 -0
  189. package/scripts/hall-preview/README.md +41 -1
  190. package/scripts/hall-preview/pref-writes.js +140 -0
  191. package/scripts/hall-preview/refresh-fixtures.js +101 -7
  192. package/scripts/hall-preview/scrub-identity.js +366 -7
  193. package/scripts/hall-preview/server.js +27 -1
  194. package/scripts/render-diagrams.sh +4 -2
  195. package/src/bongos/auth-idp.js +6 -1
  196. package/src/bongos/project-settings.js +41 -0
  197. package/src/bongos/route-rank-check.js +27 -0
  198. package/src/bongos/routes/me.js +68 -7
  199. package/src/bongos/serve-internal.js +5 -1
  200. package/src/module-api.js +7 -1
  201. package/src/platform-server.js +10 -0
  202. package/tests/application_lifecycle.mjs +47 -5
  203. package/tests/autobongos_tweak_lane.mjs +208 -0
  204. package/tests/conductor_main_e2e.mjs +21 -0
  205. package/tests/connections_api.mjs +18 -0
  206. package/tests/craft_mode_nav.mjs +5 -3
  207. package/tests/craft_mode_tallies.mjs +52 -16
  208. package/tests/discord_approvals.mjs +11 -0
  209. package/tests/discord_channels.mjs +20 -0
  210. package/tests/fixtures/gov-seated.mjs +64 -0
  211. package/tests/fixtures/government-ask-cards.json +1284 -0
  212. package/tests/government_act_alone_buildings.mjs +1 -0
  213. package/tests/government_agent_asks.mjs +322 -0
  214. package/tests/government_asks.mjs +408 -0
  215. package/tests/government_build_rights.mjs +253 -0
  216. package/tests/government_buildouts.mjs +382 -0
  217. package/tests/government_counts.mjs +143 -0
  218. package/tests/government_decision_rules.mjs +5 -1
  219. package/tests/government_laws.mjs +488 -0
  220. package/tests/government_laws_session.mjs +321 -0
  221. package/tests/government_leads.mjs +375 -0
  222. package/tests/government_positions.mjs +127 -0
  223. package/tests/government_presets.mjs +215 -0
  224. package/tests/government_routes.mjs +8 -1
  225. package/tests/government_routing.mjs +118 -0
  226. package/tests/government_seed.mjs +5 -0
  227. package/tests/government_treasury.mjs +394 -0
  228. package/tests/governor_docket.mjs +26 -2
  229. package/tests/hall_buildouts.mjs +208 -0
  230. package/tests/hall_city.mjs +14 -6
  231. package/tests/hall_city_living.mjs +617 -0
  232. package/tests/hall_gov_counts.mjs +336 -0
  233. package/tests/hall_gov_living.mjs +636 -0
  234. package/tests/hall_gov_real_site.mjs +655 -0
  235. package/tests/hall_matter_card.mjs +6 -1
  236. package/tests/hall_my_agents.mjs +481 -0
  237. package/tests/hall_nav.mjs +107 -4
  238. package/tests/hall_page_gate_map.mjs +4 -0
  239. package/tests/hall_preview_governor_fixtures.mjs +30 -3
  240. package/tests/hall_preview_scrub.mjs +59 -1
  241. package/tests/hall_preview_security.mjs +169 -0
  242. package/tests/hall_reading_world.mjs +44 -0
  243. package/tests/hall_roster_world.mjs +6 -2
  244. package/tests/hall_street.mjs +18 -2
  245. package/tests/hall_street_amend.mjs +1 -1
  246. package/tests/hall_street_deck.mjs +609 -0
  247. package/tests/hall_street_depth.mjs +559 -0
  248. package/tests/hall_street_desk.mjs +1 -1
  249. package/tests/hall_street_inside.mjs +29 -5
  250. package/tests/hall_tweak_editor.mjs +4 -1
  251. package/tests/hub_account_page.mjs +94 -3
  252. package/tests/hub_connections_page.mjs +507 -0
  253. package/tests/hub_invite_badge.mjs +1 -1
  254. package/tests/idp_device_exchange.mjs +18 -0
  255. package/tests/landing_legal_pages.mjs +8 -5
  256. package/tests/landing_page.mjs +23 -4
  257. package/tests/nav_permission_atoms.mjs +4 -1
  258. package/tests/profile_connections_ui.mjs +88 -25
  259. package/tests/projects_hub.mjs +15 -0
  260. package/tests/render_check.mjs +2 -2
  261. package/tests/ship_broadcast_routing.mjs +57 -16
  262. package/tests/ship_broadcast_server.mjs +40 -7
  263. package/tests/ship_merge_gate.mjs +19 -0
  264. package/tests/ui_design_kit.mjs +2 -2
  265. package/tests/watch_applications_queue.mjs +23 -0
@@ -73,6 +73,28 @@ const floorRow = (key, over = {}) => ({ key, building: key.split('.')[0], status
73
73
 
74
74
  // ── the drawing is the prototype's ───────────────────────────────────────────
75
75
 
76
+ // The one place the hall departs from the prototype's section (task 1004718): the
77
+ // chosen floor lit in the prototype's warm wash, salmon frame, salmon sign and
78
+ // salmon level number, which spends salmon on a floor that may need nothing.
79
+ // The hall lights it in ink. inked() turns the prototype's chosen floor into
80
+ // the hall's, colour for colour and nothing else, so every shape, path and
81
+ // position still has to match the prototype exactly.
82
+ const INKED = {
83
+ dark: { '#2b2219': '#202025', '#ffd9a8': '#e6e6ea', 'rgba(255,217,168,0.10)': 'rgba(255,255,255,0.07)', 'rgba(255,214,170,0.55)': 'rgba(220,224,240,0.40)', ink: '#ffffff' },
84
+ light: { '#fdf0e2': '#ebeaee', '#f0a86a': '#55565a', 'rgba(240,168,106,0.12)': 'rgba(23,24,26,0.06)', 'rgba(240,168,106,0.45)': 'rgba(23,24,26,0.22)', ink: '#17181a' },
85
+ };
86
+ function inked(view, mode) {
87
+ const M = INKED[mode], accentText = S.theme(mode).accentText;
88
+ const v = j(view);
89
+ v.shapes = v.shapes.map((s) => Object.assign(s, {
90
+ f: M[s.f] || s.f,
91
+ s: s.s === '#fb9569' && s.dash === 'none' && s.w === 1.5 ? M.ink : s.s,
92
+ }));
93
+ v.texts = v.texts.map((t) => (/^\d\d$/.test(t.v) && t.c === accentText ? Object.assign(t, { c: M.ink }) : t));
94
+ v.syms = v.syms.map((s) => (s.c === '#fb9569' ? Object.assign(s, { c: M.ink }) : s));
95
+ return v;
96
+ }
97
+
76
98
  const SECTION_KEYS = ['kicker', 'name', 'holds', 'whoText', 'howText', 'seats', 'fill', 'fillColor', 'growth', 'openKicker', 'sym', 'flipA', 'flipB', 'openTitle', 'openLines', 'drawerItems', 'onFile', 'shapes', 'texts', 'syms', 'lift', 'hasAnnex', 'annex'];
77
99
 
78
100
  test('every building in section is the prototype\'s, to the path, in both modes, before and after a pick', () => {
@@ -81,7 +103,7 @@ test('every building in section is the prototype\'s, to the path, in both modes,
81
103
  for (const st of [{}, { reviewHeld: true, floor: 1, drawer: 1, picks: 3 }, { floor: 99, drawer: 7, picks: 2 }]) {
82
104
  for (const key of S.CIVIC_KEYS) {
83
105
  const p = proto(mode, { view: key, ...st });
84
- const want = p.buildingView(p.theme(mode), key, p.matters());
106
+ const want = inked(p.buildingView(p.theme(mode), key, p.matters()), mode);
85
107
  const vacant = key === 'review' && !p.state.reviewHeld;
86
108
  const got = I.buildingView(S.theme(mode), key, p.civicInfo(key), { floor: p.state.floor, drawer: p.state.drawer, picks: p.state.picks, vacant, reviewHeld: p.state.reviewHeld, evening: false });
87
109
  for (const k of SECTION_KEYS) assert.deepEqual(j(got[k]), j(want[k]), `${mode} ${key} ${JSON.stringify(st)} ${k}`);
@@ -385,9 +407,11 @@ test('the lift runs to the chosen floor, and the chosen block flips on every pic
385
407
  assert.equal(b.selFloor, 7, 'a pick past the top is the top floor');
386
408
  assert.deepEqual([b.flipA, b.flipB], [true, false]);
387
409
  assert.ok(b.floors[7].selected && !b.floors[2].selected);
388
- assert.ok(a.shapes.some((s) => s.s === '#fb9569' && s.w === 1.5), 'the orange frame');
389
- assert.equal(a.syms[2].c, '#fb9569', 'and the orange sign on the chosen floor only');
390
- assert.ok(a.syms.filter((s, i) => i !== 2).every((s) => s.c !== '#fb9569'));
410
+ // the chosen floor is framed and signed in ink: salmon is only what needs you (task 1004718)
411
+ assert.ok(a.shapes.some((s) => s.s === '#ffffff' && s.w === 1.5 && s.dash === 'none'), 'the ink frame');
412
+ assert.ok(!a.shapes.some((s) => s.s === '#fb9569' && s.dash === 'none'), 'no solid salmon frame');
413
+ assert.equal(a.syms[2].c, '#ffffff', 'and the ink sign on the chosen floor only');
414
+ assert.ok(a.syms.filter((s, i) => i !== 2).every((s) => s.c !== '#ffffff' && s.c !== '#fb9569'));
391
415
  });
392
416
 
393
417
  // ── districts ──────────────────────────────────────────────────────────────
@@ -508,7 +532,7 @@ async function bootPage({ hash = '', feeds = {}, innerWidth = 1440 } = {}) {
508
532
  };
509
533
  const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date, console, encodeURIComponent, decodeURIComponent, isNaN, isFinite, Map });
510
534
  ctx.globalThis = ctx;
511
- for (const f of ['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-desk.js', 'street.js']) vm.runInContext(read(f), ctx);
535
+ for (const f of ['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-depth.js', 'street-desk.js', 'street.js']) vm.runInContext(read(f), ctx);
512
536
  await window.GovStreetPage.ready;
513
537
  await new Promise((r) => setTimeout(r, 0));
514
538
  const fireWin = (t, e) => (listeners[t] || []).forEach((fn) => fn(e));
@@ -651,7 +651,10 @@ try {
651
651
  assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
652
652
  assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
653
653
  assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
654
- assert.match(HTML, /shell\.js\?v=2026-10-08-glyph-fix/);
654
+ // the page carries the shell's current cache-bust, the one every hall page carries
655
+ // (read off the index, so a bump of the shell's stamp moves this pin with it)
656
+ const stamp = /shell\.js\?v=([\w.-]+)/.exec(fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/index.html'), 'utf8'))[1];
657
+ assert.ok(HTML.includes('shell.js?v=' + stamp), 'shell.js?v=' + stamp);
655
658
  assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
656
659
  for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
657
660
  assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
@@ -14,6 +14,9 @@
14
14
  // the builders/status hosts, and never on a child instance;
15
15
  // 5. the hall's side: the two privacy sections are gone, one link-out row
16
16
  // remains, and its hub origin is a bare https origin or nothing;
17
+ // 7. the guild notice (task 1004260, ADR 0337 D4.4): under Private and under
18
+ // Hide, for the choice as it stands, the sentence that a guild counts your
19
+ // numbers and a link to each of your guilds' own switch — never the switch;
17
20
  // 6. the bar (task 1004455): the hub's own bar and two bands rather than a
18
21
  // header of its own — the same markup as projects.html, cosmos.css's rules,
19
22
  // no map search, links into /projects, Account as the current place — and
@@ -81,7 +84,7 @@ test('the script builds no markup from strings, and the controls are real markup
81
84
  test('the door returns here, and the page calls only its own-scoped routes', () => {
82
85
  assert.match(PAGE, /href="\/api\/bongos\/auth\/web\/start\?return=\/account&amp;repos=1"/, 'sign-in lands back on /account, asking for repo access once (task 1004549)');
83
86
  const calls = [...SCRIPT.matchAll(/request\('(GET|PATCH|POST)', '([^']+)'/g)].map((m) => `${m[1]} ${m[2]}`);
84
- assert.deepEqual([...new Set(calls)].sort(), ['GET /me/cross-project', 'PATCH /me/cross-project', 'PATCH /me/handle', 'PATCH /me/profile', 'GET /my-projects', 'POST /my-projects/'].sort());
87
+ assert.deepEqual([...new Set(calls)].sort(), ['GET /me/cross-project', 'PATCH /me/cross-project', 'PATCH /me/handle', 'PATCH /me/profile', 'GET /my-projects', 'POST /my-projects/', 'GET /me/guilds'].sort());
85
88
  // the bar's two calls live in the shared asset: the identity read and sign out
86
89
  const barCalls = [...BAR.matchAll(/fetch\(API \+ '([^']+)', \{ method: '(GET|POST)'/g)].map((m) => `${m[2]} ${m[1]}`);
87
90
  assert.deepEqual(barCalls.sort(), ['GET /me', 'POST /auth/logout']);
@@ -134,7 +137,7 @@ test('the bands say what the hub’s say and lead into /projects; Account is the
134
137
  ['Your projects', '/projects?view=mine'], ['New project', '/projects?view=new'],
135
138
  ], 'this page has no views of its own, so every item names /projects in full');
136
139
  assert.deepEqual(bandLinks(bandOf(PAGE, 'menuProfile')), [
137
- ['My projects', '/projects?view=mine'], ['Invitations', '/invitations'], ['Account', '/account'], ['Sign out', '/api/bongos/auth/logout'],
140
+ ['My projects', '/projects?view=mine'], ['Invitations', '/invitations'], ['Connections', '/connections'], ['Account', '/account'], ['Sign out', '/api/bongos/auth/logout'],
138
141
  ]);
139
142
  assert.deepEqual(PAGE.match(/<a [^>]*aria-current="page"[^>]*>[^<]+/g), ['<a class="bandT" href="/account" aria-current="page">Account']);
140
143
  });
@@ -488,7 +491,7 @@ test('the section ships inside the signed-in body, last, as a list with an empty
488
491
  test('signed in: the list is read once, after the account, and each row names its project and host with its own Leave', async () => {
489
492
  const dom = boot(building(BUILDING));
490
493
  await settle();
491
- assert.deepEqual(dom.calls.filter((c) => c.method === 'GET' && c.path !== '/me').map((c) => c.path), ['/me/cross-project', '/my-projects']);
494
+ assert.deepEqual(dom.calls.filter((c) => c.method === 'GET' && c.path !== '/me').map((c) => c.path), ['/me/cross-project', '/my-projects', '/me/guilds']);
492
495
  assert.equal(dom.els.get('buildingLoading').hidden, true);
493
496
  assert.equal(dom.els.get('buildingEmpty').hidden, true);
494
497
  const rows = rowsOf(dom);
@@ -566,6 +569,94 @@ test('/projects keeps no copy: the mine view points here, and nothing there read
566
569
  assert.match(SCRIPT, /if \(target && typeof target\.scrollIntoView === 'function'\) target\.scrollIntoView\(\);/, 'and #aBuilding lands on the section once the body shows');
567
570
  });
568
571
 
572
+ // ── 3c. the guilds that count your numbers (task 1004260, ADR 0337 D4.4) ─────
573
+ // Every guild counts a member's numbers in its rounded totals unless they leave
574
+ // them out on that guild's page — a private account and a hidden record included.
575
+ // So Private and Hide each say so, for the choice as it stands, and list the
576
+ // builder's guilds with a link to each one's switch. The switch is the guild
577
+ // page's: this page reads GET /me/guilds and writes nothing to a guild.
578
+
579
+ const GUILDS = [
580
+ { slug: 'forge-crew', name: 'Forge Crew', visibility: 'public', membership_kind: 'member', shown_publicly: false, counted_in_totals: true },
581
+ { slug: 'night-shift', name: 'Night Shift', visibility: 'unlisted', membership_kind: 'owner', shown_publicly: false, counted_in_totals: false },
582
+ ];
583
+ const GUILD_PAGE = fs.readFileSync(path.join(LANDING, 'guild.html'), 'utf8').replace(/\r\n/g, '\n');
584
+ const withGuilds = (rows, acct = ACCOUNT) => (m, p) => {
585
+ if (m === 'GET' && p === '/me/cross-project') return { status: 200, data: acct() };
586
+ if (m === 'GET' && p === '/me/guilds') return rows ? { status: 200, data: { guilds: rows, count: rows.length } } : { status: 500, data: null };
587
+ return m === 'GET' ? { status: 500, data: null } : { status: 200, data: {} };
588
+ };
589
+ const guildLinks = (dom, key) => dom.els.get(key + 'GuildList').children.map((li) => ({
590
+ href: li.children[0].href, name: li.children[0].textContent, state: li.children[1].textContent,
591
+ }));
592
+
593
+ test('the notice ships hidden under each choice, as real markup, and the guild page is the only place a switch lives', () => {
594
+ for (const key of ['vis', 'record']) {
595
+ assert.match(PAGE, new RegExp(`<div class="aGuilds" id="${key}Guilds" hidden><p class="aWhat" id="${key}GuildsWhat"></p><ul class="aGuildList" id="${key}GuildList"></ul></div>`));
596
+ }
597
+ assert.ok(PAGE.indexOf('id="visGuilds"') < PAGE.indexOf('id="recLabel"'), 'under the profile-page choice, before recruiters');
598
+ assert.ok(PAGE.indexOf('id="recordGuilds"') > PAGE.indexOf('name="record" value="hide"'), 'under the show/hide choice');
599
+ assert.doesNotMatch(SCRIPT, /counted_in_totals:|\/membership|name="counted"/, 'no switch drawn or written here');
600
+ });
601
+
602
+ test('the notice speaks the guild page’s words: rounded to a band, counted weekly, never shown on its own', () => {
603
+ for (const words of ['They are never shown on their own: only inside totals rounded down to a band (10+, 20+, 50+ …) and counted once a week. ']) {
604
+ assert.ok(GUILD_PAGE.includes(words) && SCRIPT.includes(words), `"${words}" on both pages`);
605
+ }
606
+ });
607
+
608
+ test('public and shown: no notice; picking Private or Hide shows it under that choice alone, before any save', async () => {
609
+ const dom = boot(withGuilds(GUILDS));
610
+ await settle();
611
+ assert.equal(dom.els.get('visGuilds').hidden, true, 'a public page says nothing about guilds');
612
+ assert.equal(dom.els.get('recordGuilds').hidden, true);
613
+ dom.pick('visibility', 'private');
614
+ assert.equal(dom.els.get('visGuilds').hidden, false);
615
+ assert.equal(dom.els.get('recordGuilds').hidden, true, 'only the choice that applies');
616
+ assert.equal(dom.text('visGuildsWhat'), 'Every guild you belong to counts your numbers in its rounded totals unless you leave them out — even while your profile page is private. ' +
617
+ 'They are never shown on their own: only inside totals rounded down to a band (10+, 20+, 50+ …) and counted once a week. Each guild’s page has the switch:');
618
+ dom.pick('record', 'hide');
619
+ assert.equal(dom.els.get('recordGuilds').hidden, false);
620
+ assert.match(dom.text('recordGuildsWhat'), /unless you leave them out — even while your record is hidden\. /);
621
+ dom.pick('visibility', 'public');
622
+ assert.equal(dom.els.get('visGuilds').hidden, true, 'and goes again with the choice');
623
+ assert.equal(dom.calls.filter((c) => c.method !== 'GET').length, 0, 'picking sends nothing');
624
+ });
625
+
626
+ test('each guild links to its own page, with its current answer beside it', async () => {
627
+ const dom = boot(withGuilds([...GUILDS, { slug: 'a b', name: null, counted_in_totals: true }]));
628
+ await settle();
629
+ dom.pick('record', 'hide');
630
+ assert.deepEqual(guildLinks(dom, 'record'), [
631
+ { href: '/g/forge-crew', name: 'Forge Crew', state: ' · counted' },
632
+ { href: '/g/night-shift', name: 'Night Shift', state: ' · left out' },
633
+ { href: '/g/a%20b', name: 'a b', state: ' · counted' },
634
+ ], 'the slug is encoded, never spliced raw, and a missing name falls back to it');
635
+ });
636
+
637
+ test('a private, hidden account sees both notices on load; in no guild, neither', async () => {
638
+ const hiddenPrivate = () => ({ ...ACCOUNT(), account_visibility: 'private', hide_stats: true });
639
+ const dom = boot(withGuilds(GUILDS, hiddenPrivate));
640
+ await settle();
641
+ assert.equal(dom.els.get('visGuilds').hidden, false);
642
+ assert.equal(dom.els.get('recordGuilds').hidden, false);
643
+ assert.equal(guildLinks(dom, 'vis').length, 2);
644
+ const none = boot(withGuilds([], hiddenPrivate));
645
+ await settle();
646
+ assert.equal(none.els.get('visGuilds').hidden, true, 'no guild, nothing to say');
647
+ assert.equal(none.els.get('recordGuilds').hidden, true);
648
+ });
649
+
650
+ test('a failed guild read still says the sentence, with no list, and keeps the rest of the page', async () => {
651
+ const dom = boot(withGuilds(null));
652
+ await settle();
653
+ assert.equal(dom.els.get('acctBody').hidden, false);
654
+ dom.pick('visibility', 'private');
655
+ assert.equal(dom.els.get('visGuilds').hidden, false, 'not knowing your guilds is no reason to stay quiet');
656
+ assert.match(dom.text('visGuildsWhat'), /Each guild’s page has the switch\.$/);
657
+ assert.equal(dom.els.get('visGuildList').children.length, 0);
658
+ });
659
+
569
660
  // ── 3b. the bar, executed (task 1004455) ─────────────────────────────────────
570
661
 
571
662
  const ME = { builder: { id: '1', github_login: 'octocat', avatar_url: 'https://avatars.example/u/1' } };