@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
@@ -48,6 +48,8 @@
48
48
 
49
49
  (function (root) {
50
50
  'use strict';
51
+ // the living model's one count rule (gov-living.js), so the city never counts for itself
52
+ const Living = root.GovLiving || (typeof require === 'function' ? require('./gov-living.js') : null);
51
53
 
52
54
  // ---------- the iso sub-palette (Street.dc.html theme(mode).iso) ----------
53
55
  function isoTheme(mode) {
@@ -230,7 +232,12 @@
230
232
  }
231
233
 
232
234
  // ---------- the city's fixed layout (Street.dc.html homeView) ----------
233
- const CIVIC_SLOTS = Object.freeze([['planning', 5, 5], ['rules', 7, 5], ['treasury', 9, 5], ['people', 5, 7], ['charter', 7, 7], ['release', 9, 7], ['disputes', 5, 9], ['automation', 7, 9], ['review', 9, 9]]);
235
+ // The back corner (5,5), the centre (7,7) and the front corner (9,9) stand in
236
+ // one screen column, so whatever is at the back sits behind the centre. The
237
+ // Charter, the tallest, takes the back corner and rises over the quarter;
238
+ // Planning takes the centre, where nothing in front is tall enough to hide its
239
+ // lit floor (task 1004718: behind the Charter, Planning's floor never showed).
240
+ const CIVIC_SLOTS = Object.freeze([['charter', 5, 5], ['rules', 7, 5], ['planning', 9, 5], ['people', 5, 7], ['treasury', 7, 7], ['release', 9, 7], ['disputes', 5, 9], ['automation', 7, 9], ['review', 9, 9]]);
234
241
  const ROOF = Object.freeze({ charter: 158, rules: 36, treasury: 66, planning: 51, people: 31, release: 72, disputes: 60, review: 53, automation: 86 });
235
242
 
236
243
  // The four prototype districts (route = the parcel road it feeds), then the
@@ -364,21 +371,536 @@
364
371
  // The home line, when the docket could not be read.
365
372
  const FALLBACK_LINE = 'the docket could not be read just now';
366
373
 
374
+ // ======================================================================
375
+ // THE LIVING CITY (task 1004685, the living-city prototype)
376
+ // ======================================================================
377
+ // When cityModel() is handed `living` (GovLiving.livingModel()'s output,
378
+ // gov-living.js) the city keeps its look and gains three levels, each with
379
+ // one meaning:
380
+ // ground the goal districts, hazed toward the void by depth band (a
381
+ // district with an agent at work stands one band clearer), and
382
+ // the spend conduit in the reserve blue, from the Treasury round
383
+ // the ring to them: the trunk's width is the real spend rate.
384
+ // civic the nine buildings, each now a core: the Lead's floor, then a
385
+ // capsule floor per committee room and built floor plug into a shaft on the
386
+ // roof, so structure is height. Ink windows say how busy it is;
387
+ // SALMON windows, on the floor whose seat the ask waits on, are
388
+ // the only salmon: questions waiting on the viewer. A build-out
389
+ // the form of government allows is a live site; a proposal waiting
390
+ // on a vote is a parked crane, a dashed-glass ghost annex and the
391
+ // council's tally arc; an open sitting is a light shaft.
392
+ // sky one chrome pod per active claim on a lane from the building its
393
+ // work answers to to its goal's district, at fixed spacing so the
394
+ // count reads; a dry pot's reserve-borrow beacon.
395
+ // Every mark is drawn from a real read (task 1004717: the prototype overlay
396
+ // is gone). Motion is the page's CSS: this file only names
397
+ // the parts that move (`cls`) and where their still frame stands (`style`).
398
+ const FLOOR_H = 13;
399
+ const BUILD_FLOORS = 3;
400
+ // [where the shaft starts, where the first capsule sits] on each recipe's roof
401
+ const CORE = Object.freeze({ charter: [144, 158], rules: [36, 36], treasury: [66, 66], planning: [38, 51], people: [0, 31], release: [56, 72], disputes: [48, 60], automation: [36, 44], review: [53, 53] });
402
+ // the main mass whose two seen faces carry the windows: x0, y0, x1, y1 on the slot, z0, z1
403
+ const BODY = Object.freeze({
404
+ charter: [0.3, 0.3, 1.7, 1.7, 6, 52], rules: [0.22, 0.62, 1.78, 1.38, 4, 32], treasury: [0.4, 0.4, 1.6, 1.6, 22, 52],
405
+ planning: [0.3, 0.3, 1.7, 1.7, 14, 38], people: [0.35, 1.1, 1.0, 1.7, 7, 14], release: [1.25, 0.75, 1.7, 1.25, 0, 66],
406
+ disputes: [0.75, 0.55, 1.25, 1.45, 14, 48], automation: [0.35, 0.4, 1.65, 1.5, 20, 36], review: [0.38, 0.38, 1.62, 1.62, 14, 50],
407
+ });
408
+ // where a crane's mast stands on the slot, and the z it stands on
409
+ const MAST = Object.freeze({ release: [1.5, 1.0, 66], treasury: [1.55, 1.0, 57] });
410
+ const LANE_PERIOD = 26; // seconds, the same on every lane: speed carries no data
411
+ const POD_GAP = 0.07; // the fixed spacing between pods on one lane, as a share of it
412
+ const MAX_LANE_PODS = 8; // a lane's convoy fits its spacing; past it the lane says "+N"
413
+ const MAX_PODS = 64; // the city's whole sky stays inside the moving-node budget (~100)
414
+ const AGE_FULL_DAYS = 14; // an age ring is full when the oldest ask on you is two weeks old
415
+ // The far, middle and near depth bands. Even the near band is hazed a little, so the civic
416
+ // quarter (never hazed) is the crispest thing in the city: the districts are its skyline.
417
+ const HAZE = Object.freeze([0.62, 0.48, 0.34]);
418
+ const ACTIVE_HAZE = 0.4; // a district with an agent at work keeps this share of its band's haze
419
+ const QUIET_HAZE = 0.3; // a civic building with no agent and no ask sits darker
420
+ // the haze the far bands mix toward: the void in dark, and on paper a cool sea-tinted
421
+ // paper, so distance reads as atmosphere (fainter, cooler, lower contrast) in both modes
422
+ const VOID = Object.freeze({ dark: '#000000', light: '#cbd3e4' }); // light: a cooler, deeper sea so the haze reads on paper (task 1004718)
423
+
424
+ const clamp01 = (n) => Math.max(0, Math.min(1, Number(n) || 0));
425
+ const r1 = (n) => Math.round(n * 10) / 10;
426
+
427
+ // ---------- haze: a tone mixed toward the void (drawing data, like isoTheme) ----------
428
+ function parseColor(c) {
429
+ let m = /^#([0-9a-f]{6})$/i.exec(String(c));
430
+ if (m) { const n = parseInt(m[1], 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255, 1]; }
431
+ m = /^rgba?\(([^)]+)\)$/i.exec(String(c));
432
+ if (m) { const p = m[1].split(',').map((x) => Number(x.trim())); return [p[0], p[1], p[2], p.length > 3 ? p[3] : 1]; }
433
+ return null;
434
+ }
435
+ const hex2 = (n) => ('0' + Math.max(0, Math.min(255, Math.round(n))).toString(16)).slice(-2);
436
+ // A solid face mixes toward the ground; a line or a glaze (alpha) thins out.
437
+ function mixToward(c, target, t) {
438
+ const a = parseColor(c), b = parseColor(target);
439
+ if (!a || !b || !(t > 0)) return c;
440
+ if (a[3] < 1) return 'rgba(' + a[0] + ',' + a[1] + ',' + a[2] + ',' + Math.round(a[3] * (1 - t) * 1000) / 1000 + ')';
441
+ return '#' + hex2(a[0] + (b[0] - a[0]) * t) + hex2(a[1] + (b[1] - a[1]) * t) + hex2(a[2] + (b[2] - a[2]) * t);
442
+ }
443
+ function hazeTheme(I, t, mode) {
444
+ if (!(t > 0)) return I;
445
+ const to = VOID[mode] || VOID.dark;
446
+ const out = {};
447
+ Object.keys(I).forEach((key) => {
448
+ const v = I[key];
449
+ if (v && typeof v === 'object') {
450
+ const o = {};
451
+ Object.keys(v).forEach((f) => { o[f] = f === 'dash' ? v[f] : mixToward(v[f], to, t); });
452
+ out[key] = o;
453
+ } else out[key] = typeof v === 'string' ? mixToward(v, to, t) : v;
454
+ });
455
+ return out;
456
+ }
457
+ // A district's band from its plot's centre: far (back corner), middle, near.
458
+ function hazeBand(lot) {
459
+ const d = (lot.x0 + lot.x1 + 1) / 2 + (lot.y0 + lot.y1 + 1) / 2;
460
+ return d < 8 ? 0 : d < 18 ? 1 : 2;
461
+ }
462
+
463
+ // ---------- who and what stands in a building ----------
464
+ // A building's floors, bottom up: the Lead's (GET /government/leads, task 1004709),
465
+ // then one per committee room and one per floor, annex or district that stands
466
+ // built (its `structure`, task 1004711).
467
+ function structureFloors(b) {
468
+ const out = [];
469
+ if (!b) return out;
470
+ if (b.lead) out.push({ kind: 'lead', seat: 'lead:' + b.key, name: String(b.lead.name || 'Lead') });
471
+ ((b.structure && b.structure.committees) || []).forEach((c) => out.push({ kind: 'committee', seat: 'committee:' + b.key + '.' + c.key, name: String(c.name || c.key) }));
472
+ ((b.structure && b.structure.built) || []).forEach((x) => out.push({ kind: String(x.capsule || 'floor'), seat: null, name: String(x.name || 'Floor') }));
473
+ return out;
474
+ }
475
+ // A building's asks, less the ones the viewer has answered on this page. On you
476
+ // and on others are gov-living.js's own predicates (countsOf counts with them).
477
+ const onYouRule = (a) => (Living ? Living.isOnYou(a) : a.state === 'needs' && !!a.needs_you);
478
+ const onOthersRule = (a) => (Living ? Living.isOnOthers(a) : a.state === 'needs' && !a.needs_you);
479
+ function asksAt(b, answered) {
480
+ const gone = answered || {};
481
+ const all = (b && Array.isArray(b.asks) ? b.asks : []).filter((a) => a && !gone[a.id]);
482
+ const open = all.filter((a) => a.state !== 'settled');
483
+ return { all: all, open: open, onYou: all.filter(onYouRule), others: all.filter(onOthersRule) };
484
+ }
485
+ function isQuiet(b, v) { return !!b && !(b.agents > 0) && v.open.length === 0; }
486
+
487
+ // ---------- windows ----------
488
+ // Window cells on the two faces of a box the viewer sees (y = y1, then x = x1), rows bottom up.
489
+ function windowCells(x0, y0, x1, y1, z0, z1) {
490
+ const cells = [];
491
+ const rows = Math.max(1, Math.floor((z1 - z0 - 3) / 9));
492
+ const nx = Math.max(2, Math.round((x1 - x0) / 0.28)), ny = Math.max(2, Math.round((y1 - y0) / 0.28));
493
+ for (let j = 0; j < rows; j++) {
494
+ const z = z0 + 3 + j * 9;
495
+ for (let i = 0; i < nx; i++) cells.push({ face: 'y', u: x0 + (x1 - x0) * (i + 0.5) / nx, v: y1, z: z });
496
+ for (let i = 0; i < ny; i++) cells.push({ face: 'x', u: y0 + (y1 - y0) * (i + 0.5) / ny, v: x1, z: z });
497
+ }
498
+ return cells;
499
+ }
500
+ function windowD(k, c) {
501
+ const P = k.P, hw = 0.045, h = 3.5;
502
+ return c.face === 'y'
503
+ ? k.poly([P(c.u - hw, c.v, c.z), P(c.u + hw, c.v, c.z), P(c.u + hw, c.v, c.z + h), P(c.u - hw, c.v, c.z + h)])
504
+ : k.poly([P(c.v, c.u - hw, c.z), P(c.v, c.u + hw, c.z), P(c.v, c.u + hw, c.z + h), P(c.v, c.u - hw, c.z + h)]);
505
+ }
506
+ // A capsule's round window: an octagon laid on the face (y = v, or x = v).
507
+ function porthole(k, face, u, v, z) {
508
+ const P = k.P, pts = [];
509
+ for (let i = 0; i < 8; i++) {
510
+ const a = Math.PI * 2 * i / 8, du = Math.cos(a) * 0.06, dz = Math.sin(a) * 2.6;
511
+ pts.push(face === 'y' ? P(u + du, v, z + dz) : P(v, u + du, z + dz));
512
+ }
513
+ return k.poly(pts);
514
+ }
515
+ function cls(out, d, c) { if (d) out.push({ d: d, f: 'none', s: 'none', dash: 'none', w: 1, cls: c }); }
516
+
517
+ // ---------- a crane ----------
518
+ // The mast, then the jib (with its ties, counterweight, hook and load) as its own list so a
519
+ // live site can swing it about the mast head. `parked` folds the jib along the other axis.
520
+ function crane(k, mx, my, z0, zTop, parked) {
521
+ const P = k.P, I = k.I, mast = [], jib = [];
522
+ let d = k.seg(P(mx, my, z0), P(mx, my, zTop + 6)) + k.seg(P(mx + 0.05, my + 0.05, z0), P(mx + 0.05, my + 0.05, zTop + 6));
523
+ for (let z = z0 + 6; z < zTop; z += 10) d += k.seg(P(mx, my, z), P(mx + 0.05, my + 0.05, z + 5));
524
+ push(mast, d, 'none', I.vacantLine);
525
+ const end = parked ? (dx) => P(mx, my - dx, zTop) : (dx) => P(mx - dx, my, zTop);
526
+ const at = parked ? (dx, z) => P(mx, my - dx, z) : (dx, z) => P(mx - dx, my, z);
527
+ const peak = P(mx, my, zTop + 12);
528
+ push(jib, k.seg(end(-0.3), end(1.0)) + k.seg(peak, end(1.0)) + k.seg(peak, end(-0.3)), 'none', I.vacantLine);
529
+ const hookAt = parked ? 0.35 : 0.7, drop = parked ? 6 : 18;
530
+ push(jib, k.seg(at(hookAt, zTop), at(hookAt, zTop - drop)), 'none', I.vacantLine);
531
+ const hx = parked ? mx : mx - hookAt, hy = parked ? my - hookAt : my;
532
+ isoBox(jib, k, hx - 0.06, hy - 0.06, hx + 0.06, hy + 0.06, zTop - drop - 6, 6, I.crate);
533
+ const cw = parked ? [mx - 0.06, my + 0.18, mx + 0.06, my + 0.3] : [mx + 0.18, my - 0.06, mx + 0.3, my + 0.06];
534
+ isoBox(jib, k, cw[0], cw[1], cw[2], cw[3], zTop - 5, 5, I.plinth);
535
+ const pivot = P(mx, my, zTop);
536
+ return { mast: mast, jib: jib, pivot: [r1(pivot[0]), r1(pivot[1])] };
537
+ }
538
+
539
+ // ---------- one civic building, alive ----------
540
+ // Draws the core, the capsule floors, the windows and the construction of one building into
541
+ // `polys` (static) and `parts` (ordered; a part with `cls` moves), and says where its top is.
542
+ function civicLiving(polys, k, slot, b, v, living) {
543
+ const key = slot[0], ox = slot[1], oy = slot[2], cx = ox + 1, cy = oy + 1, I = k.I, P = k.P;
544
+ const parts = [];
545
+ const floors = structureFloors(b);
546
+ const held = floors.map((f) => (f.seat ? v.onYou.filter((a) => a.waits_on && a.waits_on.seat === f.seat).length : 0));
547
+ const bodyYou = v.onYou.length - held.reduce((n, x) => n + x, 0);
548
+
549
+ // the windows of the main mass: salmon for the asks it holds, then ink for how busy it is
550
+ const bx = BODY[key];
551
+ const cells = windowCells(ox + bx[0], oy + bx[1], ox + bx[2], oy + bx[3], bx[4], bx[5])
552
+ .map((c, i) => ({ c: c, r: rnd(i * 7 + ox * 31 + oy * 17 + 3) }))
553
+ .sort((a, z) => a.r - z.r)
554
+ .map((x) => x.c);
555
+ const nYou = Math.min(cells.length, bodyYou * 2);
556
+ const nLit = Math.min(cells.length - nYou, Math.round(clamp01(b && b.lit) * cells.length * 0.6));
557
+ // what needs you lights the TOP rows first (still scattered within a row), so a
558
+ // neighbour in front never hides it (task 1004718); ink windows take the rest
559
+ const you = cells.slice().sort((a, z) => z.z - a.z).slice(0, nYou);
560
+ const rest = cells.filter((c) => you.indexOf(c) < 0);
561
+ cls(polys, you.map((c) => windowD(k, c)).join(''), 'city-win city-win--you');
562
+ cls(polys, rest.slice(0, nLit).map((c) => windowD(k, c)).join(''), 'city-win city-win--lit');
563
+
564
+ // the core and its capsule floors (the Lead's first), each plugging in on the world's arrive curve
565
+ const start = CORE[key][1];
566
+ let top = ROOF[key];
567
+ if (floors.length) {
568
+ const coreTop = start + floors.length * FLOOR_H + 3;
569
+ isoBox(polys, k, cx - 0.13, cy - 0.13, cx + 0.13, cy + 0.13, CORE[key][0], coreTop - CORE[key][0], I.roof, { fins: true, n: 2 });
570
+ floors.forEach((f, i) => {
571
+ const z0 = start + i * FLOOR_H + 1, h = FLOOR_H - 2, zc = z0 + h / 2;
572
+ const ySide = i % 2 === 0;
573
+ const box = ySide ? [cx - 0.25, cy + 0.13, cx + 0.25, cy + 0.47] : [cx + 0.13, cy - 0.25, cx + 0.47, cy + 0.25];
574
+ const shell = [];
575
+ isoBox(shell, k, box[0], box[1], box[2], box[3], z0, h, I.civic);
576
+ parts.push({ cls: 'city-plug', style: { '--i': String(i) }, polys: shell, floor: f.kind });
577
+ const win = ySide
578
+ ? porthole(k, 'y', cx - 0.12, box[3], zc) + porthole(k, 'y', cx + 0.12, box[3], zc) + porthole(k, 'x', cy + 0.3, box[2], zc)
579
+ : porthole(k, 'x', cy - 0.12, box[2], zc) + porthole(k, 'x', cy + 0.12, box[2], zc) + porthole(k, 'y', cx + 0.3, box[3], zc);
580
+ const lit = held[i] > 0 ? 'city-win city-win--you' : (b && b.lit > 0.3 ? 'city-win city-win--lit' : 'city-win');
581
+ const pane = [{ d: win, f: I.door, s: 'none', dash: 'none', w: 1, cls: lit }];
582
+ parts.push({ cls: 'city-plug city-plug--detail', style: { '--i': String(i) }, polys: pane, floor: f.kind });
583
+ });
584
+ const cap = [];
585
+ isoBox(cap, k, cx - 0.17, cy - 0.17, cx + 0.17, cy + 0.17, coreTop, 2, I.roof);
586
+ parts.push({ cls: 'city-plug', style: { '--i': String(floors.length) }, polys: cap });
587
+ top = coreTop + 2;
588
+ }
589
+
590
+ // construction: an allowed build-out is a live site; a proposal waits as a ghost (real from task 1004711)
591
+ const list = (b && b.construction) || [];
592
+ const live = list.find((c) => c.authorised);
593
+ const pending = live ? null : list.find((c) => !c.authorised);
594
+ const total = BUILD_FLOORS * FLOOR_H;
595
+ const fp = [cx - 0.3, cy - 0.3, cx + 0.3, cy + 0.3];
596
+ const m = MAST[key] || [1.55, 1.0, start];
597
+ let site = null, shaft = null, tally = null;
598
+ if (live) {
599
+ const progress = clamp01(live.progress);
600
+ const built = Math.min(BUILD_FLOORS, Math.floor(progress * BUILD_FLOORS + 1e-9));
601
+ const frac = Math.round((progress * BUILD_FLOORS - built) * 1000) / 1000;
602
+ const solid = [];
603
+ for (let i = 0; i < built; i++) isoBox(solid, k, fp[0], fp[1], fp[2], fp[3], top + i * FLOOR_H, FLOOR_H - 1, I.civic, { rows: true });
604
+ if (solid.length) parts.push({ cls: null, polys: solid });
605
+ if (built < BUILD_FLOORS) {
606
+ const rising = [];
607
+ isoBox(rising, k, fp[0], fp[1], fp[2], fp[3], top + built * FLOOR_H, FLOOR_H - 1, I.civic);
608
+ parts.push({ cls: 'city-rise', style: { '--rise': String(frac) }, polys: rising });
609
+ }
610
+ const frame = [];
611
+ isoBox(frame, k, fp[0] - 0.04, fp[1] - 0.04, fp[2] + 0.04, fp[3] + 0.04, top, total, I.scaffold, { fins: true, n: 3 });
612
+ parts.push({ cls: 'city-scaffold', polys: frame });
613
+ const cage = [];
614
+ isoBox(cage, k, fp[2] - 0.02, fp[3] - 0.16, fp[2] + 0.1, fp[3] - 0.02, top, 6, I.crate);
615
+ const lift = r1((total - 6) * k.S);
616
+ parts.push({ cls: 'city-climb', style: { '--climb': -lift + 'px', '--climb-at': -r1(progress * lift) + 'px' }, polys: cage });
617
+ const c = crane(k, ox + m[0], oy + m[1], m[2], top + total + 10, false);
618
+ parts.push({ cls: 'city-crane', polys: c.mast });
619
+ parts.push({ cls: 'city-jib', style: { '--pivot': c.pivot[0] + 'px ' + c.pivot[1] + 'px' }, polys: c.jib });
620
+ site = { kind: 'live', id: live.id, label: live.label, progress: progress, built: built, floors: BUILD_FLOORS };
621
+ top = top + total + 22;
622
+ } else if (pending) {
623
+ const ghost = [];
624
+ isoBox(ghost, k, fp[0], fp[1], fp[2], fp[3], top, total, Object.assign({}, I.glass, { dash: '4 3' }), { fins: true, n: 3 });
625
+ parts.push({ cls: 'city-ghost', polys: ghost });
626
+ const c = crane(k, ox + m[0], oy + m[1], m[2], top + total + 6, true);
627
+ parts.push({ cls: 'city-crane city-crane--parked', polys: c.mast.concat(c.jib) });
628
+ // the council's tally: one dot per seat on an arc over the ghost, filled as they vote
629
+ // (before its sitting's tally is read, one open dot per holder: nobody has voted)
630
+ const ask = v.all.find((a) => a.id === pending.id || a.id === pending.ask_id) || null;
631
+ const t = ask && ask.tally ? ask.tally : null;
632
+ const holders = (living.byKey.charter && living.byKey.charter.seats || []).filter((s) => s.role === 'holder').length;
633
+ const seats = t ? Math.max(1, (Number(t.for) || 0) + (Number(t.against) || 0) + (Number(t.open) || 0)) : Math.max(3, holders);
634
+ const yes = t ? Number(t.for) || 0 : 0, no = t ? Number(t.against) || 0 : 0;
635
+ const centre = P(cx, cy, top + total + 4);
636
+ const arc = { yes: '', no: '', open: '' };
637
+ for (let i = 0; i < seats; i++) {
638
+ const a = Math.PI * (1.12 + 0.76 * (seats === 1 ? 0.5 : i / (seats - 1)));
639
+ const d = ellipse(r1(centre[0] + Math.cos(a) * 22), r1(centre[1] + Math.sin(a) * 14), 2.4, 2.4);
640
+ arc[i < yes ? 'yes' : i < yes + no ? 'no' : 'open'] += d;
641
+ }
642
+ const tl = [];
643
+ cls(tl, arc.yes, 'city-tally city-tally--yes');
644
+ cls(tl, arc.no, 'city-tally city-tally--no');
645
+ cls(tl, arc.open, 'city-tally city-tally--open');
646
+ parts.push({ cls: null, polys: tl });
647
+ tally = { seats: seats, yes: yes, no: no };
648
+ site = { kind: 'pending', id: pending.id, label: pending.label, needs: pending.needs || 'a vote', ask_id: ask ? ask.id : null };
649
+ // an open sitting: a light shaft from the ghost's roof
650
+ if (!ask || ask.state !== 'settled') {
651
+ const s0 = P(cx, cy, top + total);
652
+ shaft = { key: 'shaft:' + key, building: key, x: r1(s0[0]), y: r1(s0[1]), h: 150 };
653
+ }
654
+ top = top + total + 26;
655
+ }
656
+ return { parts: parts, top: top, floors: floors, held: held, bodyYou: bodyYou, windows: { cells: cells.length, you: nYou, lit: nLit }, site: site, tally: tally, shaft: shaft };
657
+ }
658
+
659
+ // ---------- the sky: one pod per agent ----------
660
+ function skyLanes(k, traffic, districts, civicTop) {
661
+ const P = k.P, lanes = [], byKey = {}, lotOf = {};
662
+ districts.forEach((D) => { lotOf[D.goal_id] = D.lot; });
663
+ const slot = {};
664
+ CIVIC_SLOTS.forEach((s) => { slot[s[0]] = s; });
665
+ ((traffic && traffic.claims) || []).forEach((c) => {
666
+ if (!c || !slot[c.building]) return;
667
+ const goal = c.goal_id != null && lotOf[String(c.goal_id)] ? String(c.goal_id) : null;
668
+ const key = c.building + '>' + (goal || 'here');
669
+ if (!byKey[key]) { byKey[key] = { key: key, building: c.building, goal_id: goal, claims: [] }; lanes.push(byKey[key]); }
670
+ byKey[key].claims.push(c);
671
+ });
672
+ lanes.forEach((L) => {
673
+ const s = slot[L.building];
674
+ const a = P(s[1] + 1, s[2] + 1, civicTop[L.building] + 18);
675
+ const lot = L.goal_id ? lotOf[L.goal_id] : null;
676
+ if (lot) {
677
+ const b = P((lot.x0 + lot.x1 + 1) / 2, (lot.y0 + lot.y1 + 1) / 2, 96);
678
+ const c = [(a[0] + b[0]) / 2, Math.min(a[1], b[1]) - 70];
679
+ L.d = 'M' + r1(a[0]) + ' ' + r1(a[1]) + 'Q' + r1(c[0]) + ' ' + r1(c[1]) + ' ' + r1(b[0]) + ' ' + r1(b[1]);
680
+ L.orbit = false;
681
+ } else {
682
+ // work with no district circles over its own building
683
+ L.d = 'M' + r1(a[0] - 26) + ' ' + r1(a[1]) + 'a26 9 0 1 0 52 0a26 9 0 1 0 -52 0';
684
+ L.orbit = true;
685
+ }
686
+ L.pods = L.claims.slice(0, MAX_LANE_PODS).map((c, i) => ({
687
+ key: 'pod:' + String(c.claim_id || c.task_id || i), task_id: c.task_id != null ? String(c.task_id) : null,
688
+ at: r1(28 + i * POD_GAP * 100), delay: -r1(LANE_PERIOD * (0.18 + i * POD_GAP)), dur: LANE_PERIOD,
689
+ }));
690
+ L.extra = L.claims.length - L.pods.length;
691
+ });
692
+ // past the moving-node budget the lanes keep their first pods and say how many more
693
+ let budget = MAX_PODS;
694
+ lanes.forEach((L) => {
695
+ const keep = Math.max(0, Math.min(L.pods.length, budget));
696
+ budget -= keep;
697
+ L.extra += L.pods.length - keep;
698
+ L.pods = L.pods.slice(0, keep);
699
+ const end = L.orbit ? null : /([-\d.]+) ([-\d.]+)$/.exec(L.d);
700
+ L.more = L.extra > 0 ? { n: L.extra, x: end ? Number(end[1]) : null, y: end ? Number(end[2]) : null } : null;
701
+ });
702
+ return lanes;
703
+ }
704
+
705
+ // ---------- the ground: the spend conduit ----------
706
+ // The trunk runs from the Treasury onto the ring, round it; its width is the real spend rate
707
+ // (this month's spend over the days so far). A spur feeds each district its pot's share
708
+ // (GET /government/treasury, task 1004712); a dry pot's spur is
709
+ // starved and its district raises a beacon over its borrow from the reserve.
710
+ const RING = Object.freeze([4.2, 11.8]);
711
+ function spendRate(spend, now) {
712
+ if (!spend) return null;
713
+ const day = Number(now) > 0 ? new Date(Number(now)).getUTCDate() : null;
714
+ if (spend.month_usd != null && day) return Math.round(Number(spend.month_usd) / day * 100) / 100;
715
+ return spend.today_usd != null ? Number(spend.today_usd) : null;
716
+ }
717
+ function trunkWidth(rate) { return rate > 0 ? r1(Math.max(1.5, Math.min(8, Math.sqrt(rate) / 5))) : 0; }
718
+ function conduits(k, living, districts, now) {
719
+ const P = k.P, flat = [], beacons = [];
720
+ const spend = living.city && living.city.spend;
721
+ const rate = spendRate(spend, now);
722
+ const w = trunkWidth(rate);
723
+ const route = (pts) => pts.map((p, i) => { const q = P(p[0], p[1]); return (i ? 'L' : 'M') + r1(q[0]) + ' ' + r1(q[1]); }).join('');
724
+ if (w > 0) {
725
+ flat.push({ d: route([[RING[0], RING[0]], [RING[1], RING[0]], [RING[1], RING[1]], [RING[0], RING[1]]]) + 'Z', f: 'none', s: 'none', dash: 'none', w: w, cls: 'city-conduit', key: 'trunk' });
726
+ flat.push({ d: route([[10.85, 6], [RING[1], 6]]), f: 'none', s: 'none', dash: 'none', w: w, cls: 'city-conduit', key: 'treasury' });
727
+ }
728
+ const byGoal = {};
729
+ (living.districts || []).forEach((d) => { byGoal[d.goal_id] = d; });
730
+ const spent = districts.reduce((n, D) => { const p = byGoal[D.goal_id] && byGoal[D.goal_id].pot; return n + (p && p.spent_usd > 0 ? p.spent_usd : 0); }, 0);
731
+ const spurs = [];
732
+ districts.forEach((D) => {
733
+ const ld = byGoal[D.goal_id];
734
+ const pot = ld && ld.pot;
735
+ if (!pot || !(w > 0)) return;
736
+ const L = D.lot, cx = (L.x0 + L.x1 + 1) / 2, cy = (L.y0 + L.y1 + 1) / 2;
737
+ const ex = Math.max(RING[0], Math.min(RING[1], cx)), ey = Math.max(RING[0], Math.min(RING[1], cy));
738
+ const pts = ex !== cx && ey !== cy ? [[ex, ey], [ex, cy], [cx, cy]] : [[ex, ey], [cx, cy]];
739
+ const share = spent > 0 ? Math.max(0, pot.spent_usd) / spent : 0;
740
+ const sw = pot.dry ? 1.2 : r1(Math.max(0.75, w * Math.sqrt(share)));
741
+ flat.push({ d: route(pts), f: 'none', s: 'none', dash: pot.dry ? '2 3' : 'none', w: sw, cls: pot.dry ? 'city-conduit city-conduit--dry' : 'city-conduit', key: 'spur:' + D.goal_id });
742
+ spurs.push({ goal_id: D.goal_id, width: sw, share: Math.round(share * 1000) / 1000, dry: !!pot.dry });
743
+ if (pot.dry) {
744
+ // the reserve-borrow beacon, in the sky over the district
745
+ const foot = P(cx, cy, 70), head = P(cx, cy, 128);
746
+ const d = k.seg(foot, head);
747
+ const dia = 'M' + r1(head[0]) + ' ' + r1(head[1] - 5) + 'L' + r1(head[0] + 4) + ' ' + r1(head[1]) + 'L' + r1(head[0]) + ' ' + r1(head[1] + 5) + 'L' + r1(head[0] - 4) + ' ' + r1(head[1]) + 'Z';
748
+ beacons.push({ d: d, f: 'none', s: 'none', dash: '1 3', w: 1, cls: 'city-beacon city-beacon--stem' });
749
+ beacons.push({ d: dia, f: 'none', s: 'none', dash: 'none', w: 1, cls: 'city-beacon' });
750
+ beacons.push({ d: ellipse(r1(head[0]), r1(head[1]), 9, 4.5), f: 'none', s: 'none', dash: 'none', w: 1, cls: 'city-beacon city-beacon--ring' });
751
+ D.beacon = { goal_id: D.goal_id, ask_id: pot.borrow_ask_id || null, x: r1(head[0]), y: r1(head[1]) };
752
+ }
753
+ });
754
+ return { flat: flat, beacons: beacons, rate: rate, width: w, spurs: spurs };
755
+ }
756
+
757
+ // ---------- the pins, read from the living asks ----------
758
+ // Salmon (urgent, needs) only for asks that wait on the viewer's own seat; others deciding is
759
+ // ink (moving); a building whose only matter is settled is a grey dot. The age ring is the
760
+ // oldest ask on you, full at two weeks. Every pin is a real matter.
761
+ function livingPins(k, living, answered, civicTop) {
762
+ const P = k.P, pins = [];
763
+ let n = 0;
764
+ CIVIC_SLOTS.forEach((s) => {
765
+ const b = living.byKey[s[0]];
766
+ const v = asksAt(b, answered);
767
+ if (!v.all.length) return;
768
+ let tier, list;
769
+ if (v.onYou.length) { tier = v.onYou.some((a) => a.urgent) ? 'urgent' : 'needs'; list = v.onYou; } else if (v.open.length) { tier = 'moving'; list = v.open; } else { tier = 'settled'; list = v.all; }
770
+ const m = list[0];
771
+ const top = P(s[1] + 1, s[2] + 1, civicTop[s[0]] + 8);
772
+ const g = glyph(m.rule, 7, 7, 13);
773
+ const ages = v.onYou.map((a) => a.age_days).filter((x) => x != null);
774
+ const oldest = ages.length ? Math.max.apply(null, ages) : null;
775
+ const title = String((m.shown && m.shown.question) || (m.raw && m.raw.title) || '');
776
+ const name = (b && b.name) || s[0];
777
+ const aria = tier === 'settled' ? name + ': settled. ' + title
778
+ : v.onYou.length ? name + ': ' + v.onYou.length + ' ' + words('on_you', v.onYou.length) + (oldest != null ? ', the oldest ' + oldest + (oldest === 1 ? ' day' : ' days') : '') + '. ' + title
779
+ : v.others.length ? name + ': ' + v.others.length + ' ' + words('on_others', v.others.length) + '. ' + title
780
+ : name + ': ' + v.open.length + ' moving. ' + title;
781
+ pins.push({
782
+ key: String(m.id), building: s[0], state: tier, count: tier === 'settled' ? 0 : list.length, matters: list.map((a) => String(a.id)),
783
+ title: title, aria: aria, x: r1(top[0] - 15), y: r1(top[1] - 52), h: 52, gf: g.fill, gl: g.line, delay: -(n * 0.9),
784
+ age: tier === 'urgent' || tier === 'needs' ? (oldest != null ? Math.round(clamp01(oldest / AGE_FULL_DAYS) * 1000) / 1000 : 0) : null,
785
+ oldest_days: oldest,
786
+ });
787
+ n += 1;
788
+ });
789
+ return pins;
790
+ }
791
+
792
+ // ---------- the Lead plates, placed so none meets another or a pin ----------
793
+ // Most important first (asks on you, then a Lead, then the owner's): beside the top of the
794
+ // building, right then left, then a step lower; a plate with nowhere clear waits for the
795
+ // card, which always names who holds the building.
796
+ const PLATE_H = 18;
797
+ const plateWidth = (text) => Math.round(String(text).length * 6.1 + 16);
798
+ function placePlates(civic, pins, k) {
799
+ const boxes = pins.map((p) => ({ x0: p.x - 2, y0: p.y - 4, x1: p.x + 32, y1: p.y + 26, own: null }));
800
+ // another building's build site or ghost annex is never covered: it is the news on that roof
801
+ civic.forEach((c) => {
802
+ if (!c.site) return;
803
+ const h = (BUILD_FLOORS * FLOOR_H + 26) * k.S;
804
+ boxes.push({ x0: c.top[0] - 40, y0: c.top[1] + 6, x1: c.top[0] + 44, y1: c.top[1] + h, own: c.key });
805
+ });
806
+ const rank = (c) => (c.on_you > 0 ? 0 : c.plate.lead ? 1 : 2);
807
+ civic.map((c, i) => ({ c: c, i: i })).sort((a, b) => rank(a.c) - rank(b.c) || a.i - b.i).forEach((e) => {
808
+ const c = e.c, w = plateWidth(c.plate.text), tx = c.top[0], ty = c.top[1];
809
+ const hit = (b) => boxes.some((o) => o.own !== c.key && b.x0 < o.x1 && b.x1 > o.x0 && b.y0 < o.y1 && b.y1 > o.y0);
810
+ const tries = [[tx + 12, ty - 2], [tx - 12 - w, ty - 2], [tx + 12, ty - 20], [tx - 12 - w, ty - 20], [tx + 12, ty + 16], [tx - 12 - w, ty + 16]];
811
+ const at = tries.map((t) => ({ x0: t[0], y0: t[1] - PLATE_H / 2, x1: t[0] + w, y1: t[1] + PLATE_H / 2, own: null })).find((b) => !hit(b));
812
+ c.plate.w = w;
813
+ if (!at) { c.plate.hidden = true; return; }
814
+ boxes.push(at);
815
+ c.plate.x = r1(at.x0);
816
+ c.plate.y = r1((at.y0 + at.y1) / 2);
817
+ c.plate.hidden = false;
818
+ });
819
+ }
820
+
821
+ // ---------- the card a building opens in place ----------
822
+ const money =(n) => '$' + String(Math.round(Number(n) || 0)).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
823
+ function factFor(b, living) {
824
+ const r = ((b && b.readings) || []).find((x) => x && x.status === 'live' && x.figure);
825
+ if (r) return { text: String(r.figure), line: String(r.line || '') };
826
+ const fence = living.city && living.city.fence;
827
+ if (b && b.key === 'automation' && fence) return { text: fence.enabled ? 'running unattended' : 'unattended off', line: '' };
828
+ if (b && b.agents > 0) return { text: b.agents + ' ' + words('agents_at_work', b.agents), line: '' };
829
+ if (b && b.decision && b.decision.rule_words) return { text: String(b.decision.rule_words), line: '' };
830
+ return { text: 'quiet', line: '' };
831
+ }
832
+ // The card is a row and two doors: every ask is answered on the deck, whose taps
833
+ // take the matter's real route (task 1004717: the card's in-place answer, which
834
+ // sent nothing, is gone). `seated`: who leads each building is known (GET
835
+ // /government/leads, task 1004709); without that read the card names nobody as
836
+ // holding the building, with it the Lead, or the owner.
837
+ function cardFor(b, v, living, seated) {
838
+ const key = b.key;
839
+ return {
840
+ key: key, name: String(b.name || key), plate: !seated ? null : b.lead ? String(b.lead.name || 'Lead') : 'owner', lead: !!seated && !!b.lead,
841
+ fact: factFor(b, living), on_you: v.onYou.length, on_others: v.others.length, open: v.open.length, agents: Number(b.agents) || 0,
842
+ deck_href: STREET_HREF + '#deck=' + key, enter_href: STREET_HREF + '#building=' + key,
843
+ };
844
+ }
845
+
846
+ // ---------- the gate plaque, the door and the line ----------
847
+ // The words are gov-living.js's COUNT_WORDS, the labels every surface prints.
848
+ const words = (key, n) => (Living ? Living.countWords(key, n) : { on_you: 'to do', on_others: n === 1 ? 'waits on others' : 'wait on others', agents_at_work: n === 1 ? 'agent working' : 'agents working' }[key]);
849
+ function plaqueFor(living, constitution, onYou) {
850
+ const ra = constitution && constitution.charter && constitution.charter.reads_as;
851
+ const laws = (living.city && living.city.laws) || [];
852
+ const month = living.city && living.city.spend ? living.city.spend.month_usd : null;
853
+ return [
854
+ ra && ra.name ? { key: 'form', figure: String(ra.name), words: 'government' } : null,
855
+ laws.length ? { key: 'laws', figure: String(laws.length), words: laws.length === 1 ? 'law' : 'laws' } : null,
856
+ onYou > 0 ? { key: 'you', figure: String(onYou), words: words('on_you', onYou) } : null,
857
+ month != null ? { key: 'spend', figure: money(month), words: 'this month' } : null,
858
+ ].filter(Boolean);
859
+ }
860
+ function ctaFor(onYou) {
861
+ return onYou > 0
862
+ ? { label: 'decide ' + onYou + ' in a row', href: STREET_HREF + '#deck', aria: 'Decide the ' + onYou + ' questions on you, one card at a time' }
863
+ : { label: 'start governing', href: STREET_HREF, aria: 'Start governing: enter the street view' };
864
+ }
865
+ function livingLine(onYou, others, agents) {
866
+ const parts = [onYou ? onYou + ' ' + words('on_you', onYou) : 'nothing to do'];
867
+ if (others) parts.push(others + ' ' + words('on_others', others));
868
+ if (agents) parts.push(agents + ' ' + words('agents_at_work', agents));
869
+ return parts.join(' · ');
870
+ }
871
+
367
872
  // The whole home view as data.
368
873
  // docket the GET /government/docket body, or null when it failed
369
874
  // goals GET /goals goals[] (any status; only 'open' ones are drawn)
370
875
  // rollups GET /goals/rollups rollups[] (goal_id → task counts)
371
876
  // mode 'dark' | 'light'
372
877
  // now ms since the epoch (a deadline inside a day is urgent)
878
+ // and, for the living city (task 1004685):
879
+ // living GovLiving.livingModel() over the same reads (gov-living.js)
880
+ // constitution GET /government/constitution (the plaque's form of government)
881
+ // answered ask ids the viewer answered on this page (they leave the counts)
373
882
  function cityModel(input) {
374
883
  const opts = input || {};
375
884
  const docket = opts.docket && typeof opts.docket === 'object' ? opts.docket : null;
376
885
  const mode = opts.mode === 'light' ? 'light' : 'dark';
377
886
  const I = isoTheme(mode);
378
887
  const counts = docket && docket.counts ? docket.counts : { needs: 0, moving: 0, settled: 0 };
379
- const evening = !!docket && !(Number(counts.needs) > 0);
888
+ const living = opts.living && Array.isArray(opts.living.buildings) ? Object.assign({}, opts.living, { byKey: {} }) : null;
889
+ if (living) living.buildings.forEach((b) => { if (b && b.key) living.byKey[b.key] = b; });
890
+ const answered = {};
891
+ (Array.isArray(opts.answered) ? opts.answered : []).forEach((id) => { answered[String(id)] = true; });
892
+ const views = {};
893
+ if (living) CIVIC_SLOTS.forEach((s) => { views[s[0]] = asksAt(living.byKey[s[0]], answered); });
894
+ // the page's numbers are gov-living.js's countsOf (opts.counts when the page already
895
+ // holds them), the same numbers the street, the deck and the shell's tally print
896
+ const tally = living ? (opts.counts || (Living ? Living.countsOf(living, { answered: Object.keys(answered) }) : null)) : null;
897
+ const onYou = tally && tally.on_you != null ? tally.on_you : living ? CIVIC_SLOTS.reduce((n2, s) => n2 + views[s[0]].onYou.length, 0) : 0;
898
+ // the windows light for the evening when nothing waits on you (in the living city: on your seats)
899
+ const evening = !!docket && (living ? onYou === 0 : !(Number(counts.needs) > 0));
380
900
  const k = isoKit(60, 30, 720, 176, 30 / 28, I, evening);
381
- const P = k.P, r1 = (n) => Math.round(n * 10) / 10;
901
+ const P = k.P;
902
+ const kits = {};
903
+ const kitFor = (t) => (t > 0 ? (kits[t] = kits[t] || isoKit(60, 30, 720, 176, 30 / 28, hazeTheme(I, t, mode), evening)) : k);
382
904
 
383
905
  // ground, the ring road and its lanes, the plaza
384
906
  const flat = [];
@@ -406,15 +928,33 @@
406
928
 
407
929
  // the civic quarter, then the districts' work, painted back to front
408
930
  const blds = [];
931
+ const civicTop = {}, civicLive = {};
409
932
  CIVIC_SLOTS.forEach((s) => {
410
933
  const polys = [];
411
- isoInstitution(polys, k, s[0], s[1], s[2], { fixes: rulesAtWork(docket) });
412
- blds.push({ key: s[0], depth: s[1] + s[2] + 2, polys: polys });
934
+ const b = living ? living.byKey[s[0]] : null;
935
+ const quiet = !!living && isQuiet(b, views[s[0]]);
936
+ const kk = quiet ? kitFor(QUIET_HAZE) : k;
937
+ isoInstitution(polys, kk, s[0], s[1], s[2], { fixes: rulesAtWork(docket) });
938
+ const item = { key: s[0], depth: s[1] + s[2] + 2, polys: polys };
939
+ civicTop[s[0]] = ROOF[s[0]];
940
+ if (living && b) {
941
+ const civ = civicLiving(polys, kk, s, b, views[s[0]], living);
942
+ item.parts = civ.parts;
943
+ item.quiet = quiet;
944
+ civicTop[s[0]] = civ.top;
945
+ civicLive[s[0]] = civ;
946
+ }
947
+ blds.push(item);
413
948
  });
949
+ // the living city hazes the districts by depth band; one with an agent at work stands a band clearer
950
+ const agentsIn = {};
951
+ if (living) (living.districts || []).forEach((d) => { agentsIn[d.goal_id] = Number(d.agents) || 0; });
414
952
  districts.forEach((D) => {
953
+ D.haze = living ? Math.round(HAZE[hazeBand(D.lot)] * (agentsIn[D.goal_id] > 0 ? ACTIVE_HAZE : 1) * 100) / 100 : 0;
954
+ const kk = kitFor(D.haze);
415
955
  D.work.forEach((w) => {
416
956
  const polys = [];
417
- isoWork(polys, k, w.gx, w.gy, w.kind, w.h);
957
+ isoWork(polys, kk, w.gx, w.gy, w.kind, w.h);
418
958
  blds.push({ key: w.gx + ',' + w.gy, depth: w.gx + w.gy + 1, polys: polys });
419
959
  });
420
960
  });
@@ -429,9 +969,12 @@
429
969
  if (!m || !slotOf[m.building]) return;
430
970
  (atBuilding[m.building] = atBuilding[m.building] || []).push(m);
431
971
  });
432
- const pins = [];
972
+ let pins = [];
433
973
  let n = 0;
434
- CIVIC_SLOTS.forEach((s) => {
974
+ // who leads each building is known: GET /government/leads was read (task 1004709)
975
+ const seated = !!(living && living.seats_real);
976
+ if (living) pins = livingPins(k, living, answered, civicTop);
977
+ else CIVIC_SLOTS.forEach((s) => {
435
978
  const bp = buildingPin(atBuilding[s[0]], opts.now);
436
979
  if (!bp) return;
437
980
  const top = P(s[1] + 1, s[2] + 1, ROOF[s[0]] + 8);
@@ -458,14 +1001,58 @@
458
1001
  toReview.forEach((d, i) => { if (fed[i]) [0, 1].forEach((j) => parcels.push({ key: 'p' + i + j, d: d, dur: 12 + i, delay: -(i * 2.7 + j * 6.2), out: false })); });
459
1002
  if (districts.length) [0, 1].forEach((j) => parcels.push({ key: 'out' + j, d: route([[10.6, 9.6], [11.5, 9.6], [11.5, 8], [11.5, 0]]), dur: 10, delay: -(j * 5), out: true }));
460
1003
 
461
- const line = docket && docket.line ? String(docket.line) : FALLBACK_LINE;
1004
+ let line = docket && docket.line ? String(docket.line) : FALLBACK_LINE;
462
1005
  const pinCount = { needs: 0, moving: 0, settled: 0 };
463
1006
  pins.forEach((p) => { pinCount[p.state === 'urgent' ? 'needs' : p.state] += 1; });
464
- const label = 'The governor city: the nine civic buildings round the plaza and '
1007
+ let label = 'The governor city: the nine civic buildings round the plaza and '
465
1008
  + (districts.length === 1 ? 'one district' : districts.length + ' districts') + ', one for each open goal'
466
1009
  + (pins.length ? '. Pins: ' + pinCount.needs + ' need you, ' + pinCount.moving + ' moving, ' + pinCount.settled + ' settled.' : '.');
467
1010
 
468
- return { mode: mode, evening: evening, crate: { fill: I.crate.t, edge: I.crate.s }, flat: flat, blds: blds, pins: pins, parcels: parcels, districts: districts, unplaced: unplaced, line: line, label: label };
1011
+ const out = { mode: mode, evening: evening, crate: { fill: I.crate.t, edge: I.crate.s }, flat: flat, blds: blds, pins: pins, parcels: parcels, districts: districts, unplaced: unplaced, line: line, label: label };
1012
+ if (!living) return out;
1013
+
1014
+ // ---- the living city's three levels (task 1004685) ----
1015
+ const spend = conduits(k, living, districts, opts.now);
1016
+ // the conduit runs on the ground, over the plots and under every building
1017
+ out.flat = flat.concat(spend.flat);
1018
+ const lanes = skyLanes(k, living.city && living.city.traffic, districts, civicTop);
1019
+ const sky = [];
1020
+ lanes.forEach((L) => sky.push({ d: L.d, f: 'none', s: 'none', dash: 'none', w: 2, cls: 'city-lane', key: 'lane:' + L.key }));
1021
+ spend.beacons.forEach((b) => sky.push(b));
1022
+ const civic = CIVIC_SLOTS.map((s) => {
1023
+ const b = living.byKey[s[0]];
1024
+ if (!b) return null;
1025
+ const v = views[s[0]], civ = civicLive[s[0]];
1026
+ const top = P(s[1] + 1, s[2] + 1, civicTop[s[0]]);
1027
+ const foot = P(s[1] + 2, s[2] + 2, 0);
1028
+ return {
1029
+ key: s[0], name: String(b.name || s[0]), top: [r1(top[0]), r1(top[1])],
1030
+ hit: { x: r1(top[0] - 28), y: r1(top[1] - 6), w: 56, h: r1(foot[1] - top[1] + 6) },
1031
+ plate: { text: b.lead ? String(b.lead.name || 'Lead') : 'owner', lead: !!b.lead, x: r1(top[0] + 10), y: r1(top[1] - 4) },
1032
+ floors: civ.floors.map((f, i) => ({ kind: f.kind, seat: f.seat, name: f.name, on_you: civ.held[i] })),
1033
+ body_on_you: civ.bodyYou, windows: civ.windows, site: civ.site, tally: civ.tally, shaft: civ.shaft, quiet: !!civ && blds.some((x) => x.key === s[0] && x.quiet),
1034
+ on_you: v.onYou.length, on_others: v.others.length, open: v.open.length, card: cardFor(b, v, living, seated),
1035
+ };
1036
+ }).filter(Boolean);
1037
+ placePlates(civic, pins, k);
1038
+ // a plate names a Lead or the owner's seat: none where the real read does not say who
1039
+ if (!seated) civic.forEach((c) => { c.plate.hidden = true; });
1040
+ const others = tally && tally.on_others != null ? tally.on_others : CIVIC_SLOTS.reduce((n2, s) => n2 + views[s[0]].others.length, 0);
1041
+ const agents = tally && tally.agents_at_work != null ? tally.agents_at_work : living.city && living.city.traffic ? Number(living.city.traffic.agents) || 0 : 0;
1042
+ line = docket ? livingLine(onYou, others, agents) : FALLBACK_LINE;
1043
+ label = 'The governor city: the nine civic buildings round the plaza, ' + (districts.length === 1 ? 'one district' : districts.length + ' districts')
1044
+ + ' for the open goals, and ' + agents + (agents === 1 ? ' agent' : ' agents') + ' in the sky lanes. ' + line + '.';
1045
+ const pods = lanes.reduce((n2, L) => n2 + L.pods.length, 0);
1046
+ const shafts = civic.filter((c) => c.shaft).map((c) => c.shaft);
1047
+ return Object.assign(out, {
1048
+ line: line, label: label, living: true,
1049
+ sky: sky, lanes: lanes, shafts: shafts, civic: civic,
1050
+ spend: { rate_usd_per_day: spend.rate, width: spend.width, spurs: spend.spurs },
1051
+ beacons: districts.filter((D) => D.beacon).map((D) => D.beacon),
1052
+ plaque: plaqueFor(living, opts.constitution, onYou), cta: ctaFor(onYou),
1053
+ counts: { on_you: onYou, on_others: others, open: onYou + others, agents_at_work: agents, moving: tally ? tally.moving : null },
1054
+ motion: { pods: pods, sites: civic.filter((c) => c.site && c.site.kind === 'live').length * 3, shafts: shafts.length, parcels: parcels.length, pins: pins.length + pins.filter((p) => p.state === 'urgent').length },
1055
+ });
469
1056
  }
470
1057
 
471
1058
  // Where the start governing button goes: the street (task 1004525 builds it),
@@ -479,6 +1066,12 @@
479
1066
  matterTier: matterTier, buildingPin: buildingPin, pinAria: pinAria, TIERS: TIERS, URGENT_MS: URGENT_MS,
480
1067
  CIVIC_SLOTS: CIVIC_SLOTS, ROOF: ROOF, BASE_LOTS: BASE_LOTS, MAX_DISTRICTS: MAX_DISTRICTS,
481
1068
  FALLBACK_LINE: FALLBACK_LINE, STREET_HREF: STREET_HREF,
1069
+ // the living city (task 1004685)
1070
+ hazeTheme: hazeTheme, mixToward: mixToward, hazeBand: hazeBand, structureFloors: structureFloors, asksAt: asksAt,
1071
+ spendRate: spendRate, trunkWidth: trunkWidth, plaqueFor: plaqueFor, ctaFor: ctaFor, livingLine: livingLine,
1072
+ FLOOR_H: FLOOR_H, BUILD_FLOORS: BUILD_FLOORS, CORE: CORE, BODY: BODY, LANE_PERIOD: LANE_PERIOD, POD_GAP: POD_GAP,
1073
+ MAX_LANE_PODS: MAX_LANE_PODS, MAX_PODS: MAX_PODS, ACTIVE_HAZE: ACTIVE_HAZE,
1074
+ AGE_FULL_DAYS: AGE_FULL_DAYS, HAZE: HAZE, QUIET_HAZE: QUIET_HAZE,
482
1075
  };
483
1076
  root.GovCity = api;
484
1077
  if (typeof module !== 'undefined' && module.exports) module.exports = api;