@bongos/core 1.21.113 → 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 (260) hide show
  1. package/.bongos-core.json +603 -183
  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 +2 -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 +122 -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-verify.js +21 -2
  180. package/scripts/gds/government-walk.js +638 -4
  181. package/scripts/gds/pulse.js +9 -6
  182. package/scripts/gds/ship-escalation.js +22 -3
  183. package/scripts/gds/ship-finish.js +5 -4
  184. package/scripts/gds/ship-merge-gate.js +18 -6
  185. package/scripts/hall-preview/README.md +41 -1
  186. package/scripts/hall-preview/pref-writes.js +140 -0
  187. package/scripts/hall-preview/refresh-fixtures.js +101 -7
  188. package/scripts/hall-preview/scrub-identity.js +366 -7
  189. package/scripts/hall-preview/server.js +27 -1
  190. package/scripts/render-diagrams.sh +4 -2
  191. package/src/bongos/auth-idp.js +6 -1
  192. package/src/bongos/project-settings.js +41 -0
  193. package/src/bongos/route-rank-check.js +27 -0
  194. package/src/bongos/routes/me.js +68 -7
  195. package/src/bongos/serve-internal.js +5 -1
  196. package/src/module-api.js +7 -1
  197. package/src/platform-server.js +10 -0
  198. package/tests/application_lifecycle.mjs +47 -5
  199. package/tests/conductor_main_e2e.mjs +21 -0
  200. package/tests/connections_api.mjs +18 -0
  201. package/tests/craft_mode_nav.mjs +5 -3
  202. package/tests/craft_mode_tallies.mjs +52 -16
  203. package/tests/discord_approvals.mjs +11 -0
  204. package/tests/discord_channels.mjs +20 -0
  205. package/tests/fixtures/gov-seated.mjs +64 -0
  206. package/tests/fixtures/government-ask-cards.json +1284 -0
  207. package/tests/government_act_alone_buildings.mjs +1 -0
  208. package/tests/government_agent_asks.mjs +322 -0
  209. package/tests/government_asks.mjs +408 -0
  210. package/tests/government_build_rights.mjs +253 -0
  211. package/tests/government_buildouts.mjs +382 -0
  212. package/tests/government_counts.mjs +143 -0
  213. package/tests/government_decision_rules.mjs +5 -1
  214. package/tests/government_laws.mjs +488 -0
  215. package/tests/government_laws_session.mjs +321 -0
  216. package/tests/government_leads.mjs +375 -0
  217. package/tests/government_positions.mjs +127 -0
  218. package/tests/government_presets.mjs +215 -0
  219. package/tests/government_routes.mjs +8 -1
  220. package/tests/government_routing.mjs +118 -0
  221. package/tests/government_seed.mjs +5 -0
  222. package/tests/government_treasury.mjs +394 -0
  223. package/tests/governor_docket.mjs +26 -2
  224. package/tests/hall_buildouts.mjs +208 -0
  225. package/tests/hall_city.mjs +14 -6
  226. package/tests/hall_city_living.mjs +617 -0
  227. package/tests/hall_gov_counts.mjs +336 -0
  228. package/tests/hall_gov_living.mjs +636 -0
  229. package/tests/hall_gov_real_site.mjs +655 -0
  230. package/tests/hall_matter_card.mjs +6 -1
  231. package/tests/hall_my_agents.mjs +481 -0
  232. package/tests/hall_nav.mjs +107 -4
  233. package/tests/hall_page_gate_map.mjs +4 -0
  234. package/tests/hall_preview_governor_fixtures.mjs +30 -3
  235. package/tests/hall_preview_scrub.mjs +59 -1
  236. package/tests/hall_preview_security.mjs +169 -0
  237. package/tests/hall_reading_world.mjs +44 -0
  238. package/tests/hall_roster_world.mjs +6 -2
  239. package/tests/hall_street.mjs +18 -2
  240. package/tests/hall_street_amend.mjs +1 -1
  241. package/tests/hall_street_deck.mjs +609 -0
  242. package/tests/hall_street_depth.mjs +559 -0
  243. package/tests/hall_street_desk.mjs +1 -1
  244. package/tests/hall_street_inside.mjs +29 -5
  245. package/tests/hall_tweak_editor.mjs +4 -1
  246. package/tests/hub_account_page.mjs +94 -3
  247. package/tests/hub_connections_page.mjs +507 -0
  248. package/tests/hub_invite_badge.mjs +1 -1
  249. package/tests/idp_device_exchange.mjs +18 -0
  250. package/tests/landing_legal_pages.mjs +8 -5
  251. package/tests/landing_page.mjs +23 -4
  252. package/tests/nav_permission_atoms.mjs +4 -1
  253. package/tests/profile_connections_ui.mjs +88 -25
  254. package/tests/projects_hub.mjs +15 -0
  255. package/tests/render_check.mjs +2 -2
  256. package/tests/ship_broadcast_routing.mjs +57 -16
  257. package/tests/ship_broadcast_server.mjs +40 -7
  258. package/tests/ship_merge_gate.mjs +19 -0
  259. package/tests/ui_design_kit.mjs +2 -2
  260. package/tests/watch_applications_queue.mjs +23 -0
@@ -173,9 +173,11 @@
173
173
  recheck: 'check again',
174
174
  // task 1004580: a work category renamed, retired, brought back, or a new one added
175
175
  rename: 'rename', retire: 'retire', restore: 'bring back', add: 'add',
176
+ // task 1004712: a dry pot's borrow from the reserve: lend it, lend half, or say no
177
+ answer: 'answer',
176
178
  });
177
179
  // What a row's chips ask, per act (a reason or an option is one tap, never text).
178
- const ASKS = Object.freeze({ recheck: 'why', budget: 'monthly limit', unsign: 'why', decide: 'pick one', ratify_goal: 'which version', check: 'check', take_core: 'move to', rot: 'stale after', module_off: 'sure', invite: 'their GitHub name', object: 'why', force: 'why', switch_off: 'why', pause: 'why', deactivate: 'why', signoff: 'confirm', record: 'the recording', allow: 'which goal', tier: 'which tier', give: 'to whom', take: 'from whom', rename: 'new name', add: 'its name' });
180
+ const ASKS = Object.freeze({ recheck: 'why', budget: 'monthly limit', unsign: 'why', decide: 'pick one', ratify_goal: 'which version', check: 'check', take_core: 'move to', rot: 'stale after', module_off: 'sure', invite: 'their GitHub name', object: 'why', force: 'why', switch_off: 'why', pause: 'why', deactivate: 'why', signoff: 'confirm', record: 'the recording', allow: 'which goal', tier: 'which tier', give: 'to whom', take: 'from whom', rename: 'new name', add: 'its name', answer: 'lend from the reserve?' });
179
181
  // How a floor counts rows nobody is waiting on (desk rows), by its desk.
180
182
  const DESK_NOUNS = Object.freeze({
181
183
  'people.builders': 'builders', 'people.ranks': 'ranks', 'disputes.confirmed': 'to fix',
@@ -326,7 +328,8 @@
326
328
  });
327
329
  const first = actions.find((a) => a.ask);
328
330
  // deciding a blocker is picking one of its options: its chips stand open
329
- const show = first && first.key === 'decide' && choices.decide ? 'decide' : null;
331
+ // (so is answering a borrow: lend, half or no, task 1004712)
332
+ const show = first && (first.key === 'decide' || first.key === 'answer') && choices[first.key] ? first.key : null;
330
333
  return { actions: actions, chips: first ? choices[first.key] : null, choices: choices, show: show, wait: actions.length ? '' : 'not from here', card: card };
331
334
  }
332
335
 
@@ -107,9 +107,11 @@
107
107
  function rect(x, y, w, h) { return 'M' + x + ' ' + y + 'h' + w + 'v' + h + 'h' + (-w) + 'Z'; }
108
108
  function ellipse(cx, cy, rx, ry) { return 'M' + (cx - rx) + ' ' + cy + 'a' + rx + ' ' + ry + ' 0 1 0 ' + (2 * rx) + ' 0a' + rx + ' ' + ry + ' 0 1 0 ' + (-2 * rx) + ' 0Z'; }
109
109
 
110
+ // T.depth (the living street's deeper side face, task 1004685) is the side's
111
+ // depth when a box names none; the prototype's theme has none, so 7.
110
112
  function box(out, T, x, G, w, h, tone, o) {
111
113
  o = o || {};
112
- const d = o.d === undefined ? 7 : o.d, z = o.z || 0, base = G - z, y = base - h, dash = tone.dash || 'none';
114
+ const d = o.d === undefined ? (T && T.depth ? T.depth : 7) : o.d, z = o.z || 0, base = G - z, y = base - h, dash = tone.dash || 'none';
113
115
  push(out, 'M' + (x + w) + ' ' + y + 'L' + (x + w + d) + ' ' + (y - d) + 'L' + (x + w + d) + ' ' + (base - d) + 'L' + (x + w) + ' ' + base + 'Z', tone.r, tone.s, dash);
114
116
  push(out, 'M' + x + ' ' + y + 'L' + (x + d) + ' ' + (y - d) + 'L' + (x + w + d) + ' ' + (y - d) + 'L' + (x + w) + ' ' + y + 'Z', tone.t, tone.s, dash);
115
117
  push(out, rect(x, y, w, h), tone.f, tone.s, dash);
@@ -136,8 +138,8 @@
136
138
  if (o.brace) push(out, 'M' + x + ' ' + y + 'L' + (x + w) + ' ' + base + 'M' + (x + w) + ' ' + y + 'L' + x + ' ' + base, 'none', tone.s, '3 3');
137
139
  }
138
140
 
139
- function gable(out, x, yTop, w, hr, tone) {
140
- const d = 7;
141
+ function gable(out, x, yTop, w, hr, tone, depth) {
142
+ const d = depth || 7;
141
143
  push(out, 'M' + (x + w / 2) + ' ' + (yTop - hr) + 'L' + (x + w / 2 + d) + ' ' + (yTop - hr - d) + 'L' + (x + w + d) + ' ' + (yTop - d) + 'L' + (x + w) + ' ' + yTop + 'Z', tone.r, tone.s);
142
144
  push(out, 'M' + x + ' ' + yTop + 'L' + (x + w / 2) + ' ' + (yTop - hr) + 'L' + (x + w) + ' ' + yTop + 'Z', tone.f, tone.s);
143
145
  }
@@ -183,15 +185,15 @@
183
185
  if (kind === 'disputes') {
184
186
  box(out, T, x, G, 24, 40, C, { win: true, lit: lit });
185
187
  box(out, T, x + 24, G, 32, 84, C, { fins: 3, door: true });
186
- gable(out, x + 24, G - 84, 32, 16, R);
188
+ gable(out, x + 24, G - 84, 32, 16, R, T.depth);
187
189
  box(out, T, x + 56, G, 24, 40, C, { win: true, lit: lit });
188
190
  return 102;
189
191
  }
190
192
  if (kind === 'people') {
191
193
  box(out, T, x, G, 34, 28, C, { door: true });
192
- gable(out, x, G - 28, 34, 15, R);
194
+ gable(out, x, G - 28, 34, 15, R, T.depth);
193
195
  box(out, T, x + 40, G, 40, 36, C, { door: true, win: true, lit: lit });
194
- gable(out, x + 40, G - 36, 40, 18, R);
196
+ gable(out, x + 40, G - 36, 40, 18, R, T.depth);
195
197
  return 58;
196
198
  }
197
199
  if (kind === 'planning') {
@@ -398,6 +400,14 @@
398
400
  return 'nothing open';
399
401
  }
400
402
 
403
+ // A district's name under its plot: a working area by its number (the title is
404
+ // the door's label and the hover), anything else its first words.
405
+ function districtName(title) {
406
+ const t = String(title || '');
407
+ const m = /^working area\s+(\d+)/i.exec(t);
408
+ return m ? 'area ' + m[1] : t.split(/\s+[—–:-]\s+/)[0];
409
+ }
410
+
401
411
  function pinState(state) {
402
412
  return state === 'needs' || state === 'moving' || state === 'settled' ? state : 'moving';
403
413
  }
@@ -479,6 +489,14 @@
479
489
 
480
490
  const FALLBACK_LINE = 'the docket could not be read just now';
481
491
 
492
+ // ---------- THE DEPTH lives in street-depth.js (task 1004685) ----------
493
+ // The living street's three planes (the goals' skyline, the civic row's
494
+ // structure and construction, the skylanes, the road, the readings as tiles)
495
+ // are GovStreetDepth's, found on its global when a street is drawn deep: it
496
+ // requires this file, so this file never requires it (no require cycle), and
497
+ // without it a deep street is drawn flat.
498
+ const depthKit = () => root.GovStreetDepth || null;
499
+
482
500
  // The whole street view as data.
483
501
  // docket GET /government/docket body, or null when it failed
484
502
  // goals GET /goals goals[] (only 'open' ones are drawn)
@@ -487,6 +505,11 @@
487
505
  // mode 'dark' | 'light'
488
506
  // now ms since the epoch (for deadlines and the month)
489
507
  // here parseHash() of the address, or null
508
+ // depth true: the living street (task 1004685), in three planes; the
509
+ // model gains `depth` and the civic row its deeper side face
510
+ // living GovLiving.livingModel() over the same feeds, or null (no Leads,
511
+ // no agents, no construction: the deep street with real data only)
512
+ // roster GET /builders/roster body, or null (the people reading)
490
513
  function streetModel(input) {
491
514
  const opts = input || {};
492
515
  const docket = opts.docket && typeof opts.docket === 'object' && Array.isArray(opts.docket.matters) ? opts.docket : null;
@@ -495,8 +518,13 @@
495
518
  const now = Number(opts.now) || 0;
496
519
  const here = opts.here || null;
497
520
  const counts = docket && docket.counts ? docket.counts : { needs: 0, moving: 0, settled: 0 };
498
- const needs = Math.max(0, Number(counts.needs) || 0);
499
- const evening = !!docket && needs === 0;
521
+ // THE COUNTS (task 1004685): `counts` is gov-living.js countsOf, the numbers the
522
+ // city, the deck and the shell print; the header reads it when it is given
523
+ // (null: still reading, so "—"), and the docket's own counts only without it.
524
+ const shared = opts.counts !== undefined;
525
+ const sc = opts.counts || null;
526
+ const needs = shared ? Math.max(0, Number(sc && sc.on_you) || 0) : Math.max(0, Number(counts.needs) || 0);
527
+ const evening = !!docket && (shared ? !!sc && sc.on_you === 0 : needs === 0);
500
528
 
501
529
  const goals = (Array.isArray(opts.goals) ? opts.goals : []).filter((g) => g && g.status === 'open' && g.id != null);
502
530
  const byGoal = {};
@@ -512,6 +540,32 @@
512
540
  const reviewHeld = reviewHeldOf(docket);
513
541
  const queue = matters.filter((m) => m.building === 'review' && pinState(m.state) !== 'settled');
514
542
 
543
+ // the living street (task 1004685): the deeper civic row and its structure
544
+ const Dp = opts.depth ? depthKit() : null;
545
+ const deep = !!Dp;
546
+ const TD = deep ? Object.assign({}, T, { depth: Dp.DEPTH.side }) : T;
547
+ const living = deep && opts.living && Array.isArray(opts.living.buildings) ? opts.living : null;
548
+ const lb = {};
549
+ if (living) living.buildings.forEach((b) => { if (b && b.key) lb[b.key] = b; });
550
+ const struct = {};
551
+ const askById = {};
552
+ if (living && living.city && Array.isArray(living.city.asks)) living.city.asks.forEach((a) => { if (a && a.id) askById[a.id] = a; });
553
+ // SALMON ONLY FOR WHAT NEEDS YOU: on the living street a matter the government
554
+ // routes to another seat waits on someone else, so its pin and its windows stand
555
+ // in ink (the docket's own 'needs' is the viewer's before anyone was seated)
556
+ const seen = living ? matters.map((m) => (m.state === 'needs' && askById[m.id] && !askById[m.id].needs_you ? Object.assign({}, m, { state: 'moving' }) : m)) : matters;
557
+ const boardSeats = Math.max(1, ((bInfo.charter && Array.isArray(bInfo.charter.holders)) ? bInfo.charter.holders.length : 0) || 5);
558
+ const depthCtx = {
559
+ // a vote's tally: its matter's (real) or its ask's, over the seats that decide it
560
+ tallyOf: (c) => {
561
+ const m = c.sitting_id != null ? matters.find((x) => x.source === 'board' && x.ref && String(x.ref.id) === String(c.sitting_id)) : null;
562
+ const t = (m && m.tally) || (c.ask_id && askById[c.ask_id] && askById[c.ask_id].tally) || null;
563
+ const f = t ? Math.max(0, Number(t.for) || 0) : 0, ag = t ? Math.max(0, Number(t.against) || 0) : 0, op = t ? Math.max(0, Number(t.open) || 0) : 0;
564
+ const seats = Math.max(boardSeats, f + ag + op, 1);
565
+ return { for: f, against: ag, seats: Math.min(seats, 12) };
566
+ },
567
+ };
568
+
515
569
  // the road
516
570
  const flat = [];
517
571
  push(flat, rect(32, G, W - 64, 30), T.road, 'none');
@@ -525,7 +579,16 @@
525
579
  if (T.shadow !== 'none') push(flat, ellipse(s.x + s.w / 2 + 4, G + 2, s.w * 0.56, 5), T.shadow, 'none');
526
580
  if (s.k) {
527
581
  const polys = [];
528
- roofs[s.k] = civic(polys, T, s.k, s.x, G, s.w, { evening: evening, reviewHeld: reviewHeld, fixes: City.rulesAtWork(docket) });
582
+ const st = { evening: evening, reviewHeld: reviewHeld, fixes: City.rulesAtWork(docket) };
583
+ if (deep) {
584
+ // the structure stands ON the roof, in front of its deeper top face,
585
+ // so it is drawn after the recipe (its core's foot plugs into the roof)
586
+ const r = civic(polys, TD, s.k, s.x, G, s.w, st) + (Dp.DEPTH.side - 7);
587
+ const sp = Dp.structureOf(TD, s.k, s.x, lb[s.k] || null, depthCtx);
588
+ sp.polys.forEach((p) => polys.push(p));
589
+ roofs[s.k] = Math.max(r, sp.top);
590
+ struct[s.k] = sp;
591
+ } else roofs[s.k] = civic(polys, T, s.k, s.x, G, s.w, st);
529
592
  blds.push({ key: s.k, polys: polys });
530
593
  } else {
531
594
  let wx = s.x + 2;
@@ -557,12 +620,21 @@
557
620
  if (k === 'review') return !reviewHeld ? 'empty seat' : queue.length ? queue.length + ' waiting' : 'nothing waiting';
558
621
  if (k === 'planning') return goals.length + (goals.length === 1 ? ' goal open' : ' goals open');
559
622
  if (readingFor[k]) return readingFor[k].figure;
623
+ // on the living street a building's line counts as the header does (needs you
624
+ // is what waits on your seats; what the government routed elsewhere waits on others)
625
+ const b = living ? lb[k] : null;
626
+ if (b && Array.isArray(b.asks)) {
627
+ const you = b.asks.filter((a) => a && a.state === 'needs' && a.needs_you).length;
628
+ const others = b.asks.filter((a) => a && a.state === 'needs' && !a.needs_you).length;
629
+ if (you) return you + ' to do';
630
+ if (others) return others + (others === 1 ? ' waits on others' : ' wait on others');
631
+ }
560
632
  return countsLine(bInfo[k] ? bInfo[k].counts : null);
561
633
  }
562
634
  const cols = segs.map((s) => s.k
563
635
  ? { key: s.k, x: s.x - 10, w: s.w + 20, name: s.k, title: s.k, state: civicState(s.k), strong: s.k === 'review' && !reviewHeld }
564
636
  // districts stand 10 apart, so their names keep to their own plot (and ellipsis there)
565
- : { key: 'd:' + s.d, x: s.x - 4, w: s.w + 8, name: s.title, title: s.title, state: s.more ? 'on the goals page' : districtLine(byGoal[s.d]), strong: false });
637
+ : { key: 'd:' + s.d, x: s.x - 4, w: s.w + 8, name: districtName(s.title), title: s.title, state: s.more ? 'on the goals page' : districtLine(byGoal[s.d]), strong: false });
566
638
  const groups = GROUP_NAMES.map((n, gi) => {
567
639
  const mem = segs.filter((s) => s.g === gi);
568
640
  const x0 = mem[0].x, x1 = mem[mem.length - 1].x + mem[mem.length - 1].w;
@@ -572,7 +644,7 @@
572
644
  // one pin per building, in the tier of its most important matter (task 1004569)
573
645
  const pins = [];
574
646
  const byB = {};
575
- matters.forEach((m) => { if (m.building !== 'review') (byB[m.building] = byB[m.building] || []).push(m); });
647
+ seen.forEach((m) => { if (m.building !== 'review') (byB[m.building] = byB[m.building] || []).push(m); });
576
648
  CIVIC_KEYS.forEach((k) => {
577
649
  const bp = City.buildingPin(byB[k], now);
578
650
  if (!bp) return;
@@ -616,20 +688,40 @@
616
688
  return { key: k, label: label, figure: figure, line: line, building: b, href: buildingHref(b), hot: !!docket && (r.hot === true || bNeeds), here: !!here && here.kind === 'building' && here.key === b, aria: label + ': ' + figure + '. Go to ' + b };
617
689
  });
618
690
 
619
- const moving = Math.max(0, Number(counts.moving) || 0), settled = Math.max(0, Number(counts.settled) || 0);
691
+ const moving = shared ? (sc && sc.moving != null ? Math.max(0, Number(sc.moving) || 0) : null) : Math.max(0, Number(counts.moving) || 0);
692
+ const settled = Math.max(0, Number(counts.settled) || 0);
620
693
  const share = monthShare(opts.earnings, now);
621
694
  const pinCount = { needs: 0, moving: 0, settled: 0 };
622
695
  pins.forEach((p) => { const t = p.state === 'urgent' ? 'needs' : p.state; if (pinCount[t] != null) pinCount[t] += 1; });
623
- return {
696
+ const hudNeeds = !docket || (shared && (!sc || sc.on_you == null)) ? '—' : String(needs);
697
+ const out = {
624
698
  mode: mode, width: W, evening: evening, reviewHeld: reviewHeld,
625
699
  flat: flat, blds: blds, crates: crates, cols: cols, groups: groups, pins: pins, doors: doors, signs: signs,
626
- hud: { needs: docket ? String(needs) : '—', moving: docket ? String(moving) : '—', share: money(share), hot: needs > 0 },
700
+ hud: { needs: hudNeeds, moving: !docket || moving == null ? '—' : String(moving), share: money(share), hot: hudNeeds !== '—' && needs > 0, shared: shared },
627
701
  line: docket ? String(docket.line || '') : FALLBACK_LINE,
628
- eveningLine: 'this month · ' + moving + ' moving · ' + settled + ' settled · ' + money(share) + ' to your share',
702
+ eveningLine: 'this month · ' + (moving == null ? '—' : moving) + ' moving · ' + settled + ' settled · ' + money(share) + ' to your share',
629
703
  label: 'The street: the nine civic buildings in the order work moves, and '
630
704
  + (goals.length === 1 ? 'one district' : goals.length + ' districts') + ', one for each open goal'
631
705
  + (docket ? '. Pins: ' + pinCount.needs + ' need you, ' + pinCount.moving + ' moving, ' + pinCount.settled + ' settled.' : '.'),
632
706
  };
707
+ if (!deep) return out;
708
+ // the living street: the depth layers, the deck behind "need you", and a
709
+ // door that says who holds its building
710
+ out.depth = Dp.depthOf({ segs: segs, at: at, roofs: roofs, struct: struct, living: living, lb: lb, docket: docket, goals: goals, byGoal: byGoal, roster: opts.roster || null, W: W, now: now, matters: living ? seen : null });
711
+ out.hud.deck = { href: Dp.DECK_HREF, aria: hudNeeds !== '—' ? needs + ' to do. Decide them in a row.' : 'Decide what is yours to do, in a row.' };
712
+ out.label += ' ' + out.depth.label;
713
+ // who holds it: a Lead, or the owner's seat — the real read (GET /government/leads,
714
+ // task 1004709); without it, nobody is named
715
+ if (living && living.seats_real) {
716
+ const viewerId = living.viewer ? String(living.viewer.id) : null;
717
+ out.doors.forEach((d) => {
718
+ const b = lb[d.key];
719
+ if (!b) return;
720
+ const who = b.lead ? (viewerId && String(b.lead.id) === viewerId ? 'you' : String(b.lead.name || 'its Lead')) : 'the owner';
721
+ d.aria += b.lead ? ', led by ' + who : ', held by ' + who;
722
+ });
723
+ }
724
+ return out;
633
725
  }
634
726
 
635
727
  // The short card a pin opens (task 1004528 makes it act). null for an unknown key.
@@ -657,7 +749,8 @@
657
749
  theme: theme, push: push, rect: rect, ellipse: ellipse, box: box, gable: gable, sawtooth: sawtooth, tree: tree, light: light, civic: civic, work: work,
658
750
  layout: layout, districtWidth: districtWidth, districtItems: districtItems, districtLine: districtLine, countsLine: countsLine,
659
751
  whenWord: whenWord, reviewHeldOf: reviewHeldOf, monthShare: monthShare, money: money, parseHash: parseHash, buildingHref: buildingHref, districtHref: districtHref,
660
- streetModel: streetModel, cardFor: cardFor, placeLabels: placeLabels, labelBox: labelBox, shownLabels: shownLabels, pinLabel: pinLabel,
752
+ streetModel: streetModel, cardFor: cardFor, districtName: districtName, placeLabels: placeLabels, labelBox: labelBox, shownLabels: shownLabels, pinLabel: pinLabel,
753
+ count: count, MORE_KEY: MORE_KEY,
661
754
  G: G, SEGS: SEGS, GROUP_NAMES: GROUP_NAMES, CIVIC_KEYS: CIVIC_KEYS, READING_KEYS: READING_KEYS, ARTBOARD_W: ARTBOARD_W,
662
755
  PIN_W: PIN_W, PIN_LABEL: PIN_LABEL, LABEL_MAX_W: LABEL_MAX_W, PIN_STACK: PIN_STACK, MAX_CRATES: MAX_CRATES, MAX_DISTRICTS: MAX_DISTRICTS, GOALS_HREF: GOALS_HREF, FALLBACK_LINE: FALLBACK_LINE, HOME_HREF: '/builders/city',
663
756
  };
@@ -183,13 +183,25 @@
183
183
  const vacant = !!st.vacant;
184
184
  const dark = T.bg === '#000000';
185
185
  const X = 130, W = 460, FH = 52, GY = 676;
186
- const occ = b.floors.length, cap = Math.max(b.cap, occ), top = GY - cap * FH;
186
+ // THE LEAD'S OFFICE AND THE BUILD-OUTS (task 1004685, the living-city
187
+ // prototype). Opt-in: with no st.office and no st.works the drawing is the
188
+ // prototype's to the pixel (tests/hall_street_inside.mjs). The office is one
189
+ // storey on top of the building's floors; a floor being built (a live site)
190
+ // or proposed (a vote is open) takes the next empty storey; an annex or a
191
+ // district stands on the annex lot. officeDraw() and worksDraw() below.
192
+ const office = st.office || null;
193
+ const works = Array.isArray(st.works) ? st.works : [];
194
+ const sites = works.filter((w) => w && w.kind === 'floor');
195
+ const capFloors = Math.max(b.cap, b.floors.length + sites.length);
196
+ const occ = b.floors.length, cap = office || works.length ? capFloors + (office ? 1 : 0) : Math.max(b.cap, occ), top = GY - cap * FH;
197
+ const officeIdx = office ? cap - 1 : -1;
198
+ const officeSel = !!office && st.floor === 'office';
187
199
  const firstPending = b.floors.findIndex((f) => f.status === 'pending' || f.status === 'proposed');
188
- const pick = st.floor === null || st.floor === undefined ? null : Number(st.floor);
200
+ const pick = officeSel || st.floor === null || st.floor === undefined ? null : Number(st.floor);
189
201
  const selFloor = pick === null || !isFinite(pick) ? (firstPending >= 0 ? firstPending : occ - 1) : Math.max(0, Math.min(pick, occ - 1));
190
202
  const M2 = dark
191
- ? { slab: '#4a4a52', wall: '#2c2c32', room: '#121215', roomLit: '#2b2219', lamp: '#55565a', lampLit: '#ffd9a8', glow: 'rgba(255,217,168,0.10)', pane: 'rgba(150,170,255,0.22)', paneLit: 'rgba(255,214,170,0.55)', kit: 'rgba(255,255,255,0.34)', kitFill: 'rgba(255,255,255,0.05)', name: '#ffffff', line: '#c0c0c0', faint: '#8e8e93' }
192
- : { slab: '#b9b8be', wall: '#e2e1e5', room: '#f8f7f9', roomLit: '#fdf0e2', lamp: '#a9a8ad', lampLit: '#f0a86a', glow: 'rgba(240,168,106,0.12)', pane: 'rgba(91,111,208,0.20)', paneLit: 'rgba(240,168,106,0.45)', kit: 'rgba(23,24,26,0.40)', kitFill: 'rgba(23,24,26,0.05)', name: '#17181a', line: '#55565a', faint: '#65656a' };
203
+ ? { slab: '#4a4a52', wall: '#2c2c32', room: '#121215', roomSel: '#202025', lampSel: '#e6e6ea', glowSel: 'rgba(255,255,255,0.07)', paneSel: 'rgba(220,224,240,0.40)', roomLit: '#2b2219', lamp: '#55565a', lampLit: '#ffd9a8', glow: 'rgba(255,217,168,0.10)', pane: 'rgba(150,170,255,0.22)', paneLit: 'rgba(255,214,170,0.55)', kit: 'rgba(255,255,255,0.34)', kitFill: 'rgba(255,255,255,0.05)', name: '#ffffff', line: '#c0c0c0', faint: '#8e8e93' }
204
+ : { slab: '#b9b8be', wall: '#e2e1e5', room: '#f8f7f9', roomSel: '#ebeaee', lampSel: '#55565a', glowSel: 'rgba(23,24,26,0.06)', paneSel: 'rgba(23,24,26,0.22)', roomLit: '#fdf0e2', lamp: '#a9a8ad', lampLit: '#f0a86a', glow: 'rgba(240,168,106,0.12)', pane: 'rgba(91,111,208,0.20)', paneLit: 'rgba(240,168,106,0.45)', kit: 'rgba(23,24,26,0.40)', kitFill: 'rgba(23,24,26,0.05)', name: '#17181a', line: '#55565a', faint: '#65656a' };
193
205
  if (key === 'review' && st.reviewHeld) { M2.room = dark ? '#121722' : '#f1f3fb'; M2.pane = dark ? 'rgba(150,170,255,0.35)' : 'rgba(91,111,208,0.30)'; }
194
206
  const shapes = [], texts = [], syms = [];
195
207
  const sans = "Manrope, 'Segoe UI', system-ui, sans-serif", mono = "'JetBrains Mono', ui-monospace, Consolas, monospace";
@@ -205,20 +217,26 @@
205
217
  }
206
218
  for (let i = 0; i < cap; i++) {
207
219
  const y0 = GY - (i + 1) * FH, y1 = y0 + FH, fl = b.floors[i];
208
- const used = !!fl, sel = used && i === selFloor;
220
+ if (i === officeIdx) { officeDraw(y0, y1, i); continue; }
221
+ if (!fl && i >= occ && sites[i - occ]) { siteDraw(sites[i - occ], y0, y1, i); continue; }
222
+ const used = !!fl, sel = used && i === selFloor && !officeSel;
209
223
  const pend = used && fl.status === 'pending';
210
224
  const prop = used && (fl.status === 'proposed' || QUIET[fl.status] !== undefined);
211
225
  // a floor you cleared this visit lights its windows and shows a tick (task 1004558)
212
226
  const clr = used && Array.isArray(st.cleared) && st.cleared.indexOf(i) >= 0;
213
- const lit = sel || clr || (used && st.evening);
227
+ // the chosen floor lights in ink (a cool, neutral light), never the warm lit wash:
228
+ // salmon and its warmth mean "needs you" only (task 1004718). A floor you cleared,
229
+ // or every floor in the evening, still lights warm.
230
+ const lit = clr || (used && st.evening), on = lit || sel;
231
+ const room = lit ? M2.roomLit : sel ? M2.roomSel : M2.room;
214
232
  push(shapes, rect(X - 6, y1 - 5, W + 12, 5), fillOr(M2.slab), line, dash);
215
- push(shapes, rect(X + 9, y0, W - 18, FH - 5), fillOr(lit ? M2.roomLit : M2.room), line, dash);
233
+ push(shapes, rect(X + 9, y0, W - 18, FH - 5), fillOr(room), line, dash);
216
234
  [X, X + W - 9].forEach((wx) => {
217
235
  push(shapes, rect(wx, y0, 9, FH - 5), fillOr(M2.wall), line, dash);
218
- if (!ghost) push(shapes, rect(wx + 3, y0 + 9, 3, FH - 23), lit ? M2.paneLit : M2.pane, 'none');
236
+ if (!ghost) push(shapes, rect(wx + 3, y0 + 9, 3, FH - 23), lit ? M2.paneLit : sel ? M2.paneSel : M2.pane, 'none');
219
237
  });
220
- if (i === 0 && !ghost) push(shapes, rect(X, y1 - 33, 9, 28), lit ? M2.roomLit : M2.room, 'none');
221
- txt(X - 50, y0 + 30, String(i + 1).padStart(2, '0'), sel ? T.accentText : M2.faint, 11, 400, 'end', mono, '0.08em');
238
+ if (i === 0 && !ghost) push(shapes, rect(X, y1 - 33, 9, 28), room, 'none');
239
+ txt(X - 50, y0 + 30, String(i + 1).padStart(2, '0'), sel ? M2.name : M2.faint, 11, 400, 'end', mono, '0.08em');
222
240
  if (!used) {
223
241
  if (!ghost) txt(X + 24, y0 + 30, 'empty floor', M2.faint, 12.5, 400);
224
242
  continue;
@@ -228,14 +246,14 @@
228
246
  [0.2, 0.45, 0.8].forEach((f) => {
229
247
  const lx = Math.round(X + W * f);
230
248
  push(shapes, 'M' + lx + ' ' + y0 + 'V' + (y0 + 7), 'none', ghost ? T.ghost.s : M2.kit);
231
- push(shapes, ellipse(lx, y0 + 9, 2.4, 2.4), lit ? M2.lampLit : M2.lamp, 'none');
232
- if (lit) push(shapes, ellipse(lx, y0 + 12, 12, 7), M2.glow, 'none');
249
+ push(shapes, ellipse(lx, y0 + 9, 2.4, 2.4), lit ? M2.lampLit : sel ? M2.lampSel : M2.lamp, 'none');
250
+ if (on) push(shapes, ellipse(lx, y0 + 12, 12, 7), lit ? M2.glow : M2.glowSel, 'none');
233
251
  });
234
252
  // the floor's sign: a plate on the wall carrying the one symbol that says what this floor is
235
253
  const sx0 = px + 16, sy0 = y0 + 5;
236
254
  push(shapes, rect(sx0 - 4, sy0 - 1, 44, 40), ghost ? 'none' : M2.wall, ghost ? T.ghost.s : M2.kit, dash);
237
255
  const sp = symbolPath(floorSymbol(fl.name));
238
- syms.push({ key: 's' + i, x: sx0 + 2, y: sy0 + 2, k: 1.333, d: sp.d, f: sp.f, c: sel ? '#fb9569' : pend ? T.accentText : (ghost || prop ? M2.faint : M2.line), w: sel ? 1.9 : 1.5 });
256
+ syms.push({ key: 's' + i, x: sx0 + 2, y: sy0 + 2, k: 1.333, d: sp.d, f: sp.f, c: sel ? M2.name : pend ? T.accentText : (ghost || prop ? M2.faint : M2.line), w: sel ? 1.9 : 1.5 });
239
257
  if (pend) push(shapes, rect(X + 11, y0 + 2, W - 22, FH - 9), 'none', '#fb9569', '4 4');
240
258
  txt(X + 24, y0 + 24, cut(fl.name, 34), ghost || prop ? M2.faint : M2.name, 14.5, 500);
241
259
  txt(X + 24, y0 + 40, cut(fl.line, 44), pend ? T.accentText : M2.line, 12, 400);
@@ -244,13 +262,96 @@
244
262
  else if (pend) txt(X + W - 22, y0 + 32, 'being decided', T.accentText, 11.5, 500, 'end');
245
263
  else if (QUIET[fl.status]) txt(X + W - 22, y0 + 32, QUIET[fl.status], M2.faint, 11.5, 400, 'end');
246
264
  }
247
- // the chosen floor gets a frame, and a lift runs in its shaft beside the building to meet it
248
- if (b.floors[selFloor]) push(shapes, rect(X - 3, GY - (selFloor + 1) * FH - 2, W + 6, FH - 1), 'none', '#fb9569', 'none', 1.5);
265
+ // the chosen floor gets an ink frame, and a lift runs in its shaft beside the building to meet it
266
+ if (officeSel) push(shapes, rect(X - 3, GY - (officeIdx + 1) * FH - 2, W + 6, FH - 1), 'none', M2.name, 'none', 1.5);
267
+ else if (b.floors[selFloor]) push(shapes, rect(X - 3, GY - (selFloor + 1) * FH - 2, W + 6, FH - 1), 'none', M2.name, 'none', 1.5);
249
268
  push(shapes, rect(X - 40, top, 22, cap * FH), 'none', ghost ? T.ghost.s : M2.kit, ghost ? '4 4' : 'none');
250
269
  let rungs = '';
251
270
  for (let i = 1; i < cap; i++) rungs += 'M' + (X - 40) + ' ' + (GY - i * FH) + 'h22';
252
271
  push(shapes, rungs, 'none', M2.kit, '2 3');
253
- const floors = b.floors.map((f, i) => ({ key: f.name, index: i, x: X, y: GY - (i + 1) * FH, w: W, h: FH - 5, aria: 'Open ' + f.name, selected: i === selFloor }));
272
+ works.forEach((w) => { if (w && (w.kind === 'annex' || w.kind === 'district')) lotDraw(w); });
273
+ const floors = b.floors.map((f, i) => ({ key: f.name, index: i, x: X, y: GY - (i + 1) * FH, w: W, h: FH - 5, aria: 'Open ' + f.name, selected: i === selFloor && !officeSel }));
274
+ if (office) floors.push({ key: 'office', index: 'office', x: X, y: GY - (officeIdx + 1) * FH, w: W, h: FH - 5, aria: 'Open the Lead\'s office', selected: officeSel });
275
+ sites.forEach((s, j) => floors.push({ key: 'site:' + j, index: 'office', x: X, y: GY - (occ + j + 1) * FH, w: W, h: FH - 5, aria: 'Open the build: ' + s.label, selected: false }));
276
+
277
+ // ---- the office, a floor going up, and the annex lot (tasks 1004685, 1004711) ----
278
+ function officeDraw(y0, y1, i) {
279
+ // THE LEAD'S OFFICE: its lamps lit while a Lead sits in it, a dashed vacancy
280
+ // while the owner holds the building; its committees are rooms on its back
281
+ // wall (a room the Lead has asked for, dashed); the sign is the hall.
282
+ const seated = !!office.seated, oDash = seated ? 'none' : '4 4';
283
+ push(shapes, rect(X - 6, y1 - 5, W + 12, 5), M2.slab, 'none');
284
+ push(shapes, rect(X + 9, y0, W - 18, FH - 5), officeSel ? M2.roomSel : seated ? M2.room : 'none', seated ? 'none' : T.ghost.s, oDash);
285
+ [X, X + W - 9].forEach((wx) => {
286
+ push(shapes, rect(wx, y0, 9, FH - 5), M2.wall, 'none');
287
+ push(shapes, rect(wx + 3, y0 + 9, 3, FH - 23), seated ? M2.paneLit : M2.pane, 'none');
288
+ });
289
+ txt(X - 50, y0 + 30, String(i + 1).padStart(2, '0'), officeSel ? M2.name : M2.faint, 11, 400, 'end', mono, '0.08em');
290
+ const px = Math.round(X + W * 0.62);
291
+ push(shapes, 'M' + px + ' ' + y0 + 'V' + (y1 - 25), 'none', seated ? M2.kit : T.ghost.s, oDash);
292
+ [0.2, 0.45].forEach((f) => {
293
+ const lx = Math.round(X + W * f);
294
+ push(shapes, 'M' + lx + ' ' + y0 + 'V' + (y0 + 7), 'none', M2.kit);
295
+ push(shapes, ellipse(lx, y0 + 9, 2.4, 2.4), seated ? M2.lampLit : M2.lamp, 'none');
296
+ if (seated) push(shapes, ellipse(lx, y0 + 12, 12, 7), M2.glow, 'none');
297
+ });
298
+ const sx0 = px + 16, sy0 = y0 + 5;
299
+ push(shapes, rect(sx0 - 4, sy0 - 1, 44, 40), seated ? M2.wall : 'none', seated ? M2.kit : T.ghost.s, oDash);
300
+ const sp = symbolPath('hall');
301
+ syms.push({ key: 'so', x: sx0 + 2, y: sy0 + 2, k: 1.333, d: sp.d, f: sp.f, c: officeSel ? M2.name : seated ? M2.line : M2.faint, w: officeSel ? 1.9 : 1.5 });
302
+ txt(X + 24, y0 + 24, 'Lead\'s office', seated ? M2.name : M2.faint, 14.5, 500);
303
+ txt(X + 24, y0 + 40, cut(String(office.line || ''), 44), M2.line, 12, 400);
304
+ const rooms = Array.isArray(office.rooms) ? office.rooms.slice(0, 2) : [];
305
+ rooms.forEach((r, j) => {
306
+ const rx = sx0 + 48 + j * 48;
307
+ push(shapes, rect(rx, y0 + 6, 44, FH - 17), r.proposed ? 'none' : M2.kitFill, r.proposed ? T.ghost.s : M2.kit, r.proposed ? '3 3' : 'none');
308
+ txt(rx + 22, y0 + 29, cut(String(r.name || ''), 7), r.proposed ? M2.faint : M2.line, 9.5, 400, 'middle', mono);
309
+ });
310
+ if (!rooms.length && office.ini) txt(X + W - 22, y0 + 34, office.ini, seated ? M2.name : M2.faint, 20, 300, 'end', sans, '-0.02em');
311
+ }
312
+ function siteDraw(site, y0, y1, i) {
313
+ // A FLOOR GOING UP: a live site where the form of government lets it build (the
314
+ // floor fills from its slab as the work goes), a dashed ghost while a vote
315
+ // is open on it. The crane over a live site is the page's (CSS motion).
316
+ const live = site.state === 'site';
317
+ const p = live && site.progress != null ? Math.max(0, Math.min(1, Number(site.progress) || 0)) : null;
318
+ push(shapes, rect(X - 6, y1 - 5, W + 12, 5), live ? M2.slab : 'none', live ? 'none' : T.ghost.s, live ? 'none' : '4 4');
319
+ if (p) push(shapes, rect(X + 9, y1 - 5 - Math.round((FH - 5) * p), W - 18, Math.round((FH - 5) * p)), M2.kitFill, 'none');
320
+ push(shapes, rect(X + 9, y0, W - 18, FH - 5), 'none', live ? M2.kit : T.ghost.s, '4 3');
321
+ const px = Math.round(X + W * 0.62);
322
+ let br = '';
323
+ for (let bx = px; bx + 30 <= X + W - 70; bx += 30) br += 'M' + bx + ' ' + (y1 - 5) + 'L' + (bx + 30) + ' ' + y0 + 'M' + bx + ' ' + y0 + 'L' + (bx + 30) + ' ' + (y1 - 5);
324
+ push(shapes, br, 'none', live ? M2.kit : T.ghost.s, live ? 'none' : '3 4');
325
+ txt(X - 50, y0 + 30, String(i + 1).padStart(2, '0'), M2.faint, 11, 400, 'end', mono, '0.08em');
326
+ txt(X + 24, y0 + 24, cut(String(site.label || 'A new floor'), 30), live ? M2.name : M2.faint, 14.5, 500);
327
+ txt(X + 24, y0 + 40, cut(String(site.line || (live ? 'being built' : 'a vote is open')), 34), M2.line, 12, 400);
328
+ if (p != null) txt(X + W - 22, y0 + 34, Math.round(p * 100) + '%', M2.name, 20, 300, 'end', sans, '-0.02em');
329
+ else txt(X + W - 22, y0 + 32, live ? 'going up' : 'vote open', M2.faint, 11.5, 400, 'end');
330
+ }
331
+ function lotDraw(w) {
332
+ // THE ANNEX LOT: a proposed annex (or district) is a dashed-glass ghost with
333
+ // a parked crane, joined by a dashed bridge; once built it stands solid.
334
+ const built = w.state === 'built';
335
+ const ax = 628, aw = 160;
336
+ const tone = built ? T.civic : T.glass;
337
+ const dashLot = built ? 'none' : '5 4';
338
+ const blocks = w.kind === 'district' ? [[ax + 6, 44, 96], [ax + 58, 44, 136], [ax + 110, 44, 72]] : [[ax, aw, 156]];
339
+ blocks.forEach((bk) => {
340
+ push(shapes, rect(bk[0], GY - bk[2], bk[1], bk[2]), tone.f, tone.s, dashLot);
341
+ let panes = '';
342
+ for (let yy = GY - bk[2] + 14; yy < GY - 14; yy += 22) for (let xx = bk[0] + 10; xx + 10 <= bk[0] + bk[1] - 8; xx += 18) panes += 'M' + xx + ' ' + yy + 'h8v10h-8Z';
343
+ push(shapes, panes, built ? M2.paneLit : M2.pane, built ? 'none' : tone.s, built ? 'none' : '2 2');
344
+ });
345
+ push(shapes, rect(X + W + 16, GY - 64, ax - (X + W + 16), 10), built ? T.civic.f : 'none', built ? T.civic.s : T.glass.s, dashLot);
346
+ const topY = GY - (w.kind === 'district' ? 136 : 156);
347
+ if (!built) {
348
+ // the parked crane: a mast, its jib and a hook, standing still
349
+ const mx = ax + aw - 14, jy = topY - 64;
350
+ push(shapes, 'M' + mx + ' ' + GY + 'V' + jy + 'M' + (ax + 8) + ' ' + jy + 'H' + (mx + 14) + 'M' + (ax + 40) + ' ' + jy + 'V' + (topY - 28) + 'M' + mx + ' ' + (jy - 14) + 'L' + (ax + 8) + ' ' + jy + 'M' + mx + ' ' + (jy - 14) + 'L' + (mx + 14) + ' ' + jy, 'none', M2.kit);
351
+ }
352
+ txt(ax + aw / 2, topY - (built ? 26 : 92), cut(String(w.label || 'An annex'), 22), built ? M2.name : M2.faint, 12.5, 500, 'middle');
353
+ txt(ax + aw / 2, topY - (built ? 10 : 76), built ? 'built' : String(w.line || 'proposed · a vote is open'), M2.faint, 11, 400, 'middle');
354
+ }
254
355
  const record = Array.isArray(b.record) ? b.record : [];
255
356
  const selDrawer = record.length ? Math.max(0, Math.min(st.drawer === null || st.drawer === undefined ? 0 : Number(st.drawer) || 0, record.length - 1)) : -1;
256
357
  const seatOf = (s) => s === 'you'
@@ -258,14 +359,16 @@
258
359
  : s === '—' ? { key: s, init: '', bg: 'transparent', fg: T.faint, border: '1px dashed ' + T.seatOpen }
259
360
  : { key: s, init: s, bg: T.seatFor.bg, fg: T.seatFor.fg, border: 'none' };
260
361
  const g = City.glyph(b.rule, 9, 9, 16);
261
- const pct = Math.round(occ / cap * 100);
362
+ // the floors in use, out of the floors there are (the office is not one)
363
+ const capOf = office || works.length ? capFloors : cap;
364
+ const pct = Math.round(occ / capOf * 100);
262
365
  const f = b.floors[selFloor];
263
366
  const quietWord = f && QUIET[f.status] !== undefined ? ' · ' + (f.status === 'proposed' ? 'proposed' : QUIET[f.status]) : '';
264
367
  return {
265
368
  kicker: b.group + ' · ' + b.name.toLowerCase(), name: b.name, holds: b.holds,
266
369
  seats: b.seats.map(seatOf), whoText: b.who, gf: g.fill, gl: g.line, howText: b.how,
267
370
  fill: pct, fillColor: pct >= 80 ? '#fb9569' : T.line,
268
- growth: (vacant ? 'Not built yet. ' : occ + ' of ' + cap + ' floors in use. ') + b.growth,
371
+ growth: (vacant ? 'Not built yet. ' : occ + ' of ' + capOf + ' floors in use. ') + b.growth,
269
372
  openKicker: 'floor ' + (selFloor + 1) + (f && f.status === 'pending' ? ' · being decided' : quietWord),
270
373
  sym: f ? Object.assign(symbolPath(floorSymbol(f.name)), { c: '#fb9569' }) : { d: '', f: '', c: 'none' },
271
374
  flipA: ((st.picks || 0) % 2) === 0, flipB: ((st.picks || 0) % 2) === 1,
@@ -273,9 +376,12 @@
273
376
  drawers: record.map((r, i) => ({ key: r.label, index: i, label: r.label, count: r.count == null ? r.items.length : r.count, aria: 'Open the ' + r.label + ' drawer', selected: i === selDrawer, bg: i === selDrawer ? T.wash : 'transparent', border: i === selDrawer ? T.ink : T.ruleStrong, handle: i === selDrawer ? T.ink : T.ruleStrong, color: T.ink })),
274
377
  drawerItems: selDrawer >= 0 ? record[selDrawer].items.map((it, i) => ({ key: 'r' + i, text: it[0], meta: it[1] })) : [],
275
378
  onFile: b.onFile, shapes: shapes, texts: texts, syms: syms, floors: floors,
276
- lift: { x: X - 38, y: GY - FH + 6, ty: -(selFloor * FH), h: FH - 16 },
277
- hasAnnex: !!b.annex, annex: b.annex ? { x: 628, y: GY - 156, w: 160, h: 156, text: b.annex } : { x: 0, y: 0, w: 0, h: 0, text: '' },
379
+ lift: { x: X - 38, y: GY - FH + 6, ty: -((officeSel ? officeIdx : selFloor) * FH), h: FH - 16 },
380
+ hasAnnex: !!b.annex && !works.some((w) => w && (w.kind === 'annex' || w.kind === 'district')), annex: b.annex ? { x: 628, y: GY - 156, w: 160, h: 156, text: b.annex } : { x: 0, y: 0, w: 0, h: 0, text: '' },
278
381
  selFloor: selFloor, selDrawer: selDrawer, cap: cap, occ: occ,
382
+ officeSel: officeSel, officeIdx: officeIdx, capFloors: capOf, builtSel: !officeSel && !!(f && f.built),
383
+ // where a live site stands, for the page's crane (CSS motion over the drawing)
384
+ sites: sites.map((s, j) => ({ label: s.label, state: s.state, x: X, y: GY - (occ + j + 1) * FH, w: W, h: FH - 5 })),
279
385
  };
280
386
  }
281
387
 
@@ -539,6 +645,11 @@
539
645
  // The building interior as data: civicInfo() drawn by buildingView().
540
646
  // input { key, docket, goals, mode, now, floor, drawer, picks, cleared }
541
647
  // cleared: the floor indexes the viewer cleared at the desk this visit (street-desk.js)
648
+ // The living-city prototype (task 1004685, street-office.js) may add:
649
+ // office { seated, line, ini, rooms[{ name, proposed }] } — the Lead's office on top
650
+ // works [{ kind: floor|annex|district, state: site|proposed|built, label, line, progress }]
651
+ // extraFloors [{ name, line }] — floors a build-out put up (GET /government/leads structure)
652
+ // floor may be 'office'
542
653
  function buildingModel(input) {
543
654
  const o = input || {};
544
655
  if (S.CIVIC_KEYS.indexOf(o.key) < 0) return null;
@@ -546,11 +657,15 @@
546
657
  const T = S.theme(mode);
547
658
  const goals = (Array.isArray(o.goals) ? o.goals : []).filter((g) => g && g.status === 'open');
548
659
  const b = civicInfo(o.key, { docket: o.docket, goals: goals, now: o.now });
660
+ if (Array.isArray(o.extraFloors) && o.extraFloors.length) {
661
+ o.extraFloors.forEach((x) => { const fl = F(String(x.name || 'A new floor'), String(x.line || 'built by a build-out'), '', 'ok', [[String(x.line || 'built by a build-out'), 'now']]); fl.built = true; b.floors.push(fl); });
662
+ b.annex = !b.vacant && b.floors.length >= PLAN[o.key].cap ? PLAN[o.key].annex : null;
663
+ }
549
664
  const docket = o.docket && Array.isArray(o.docket.matters) ? o.docket : null;
550
665
  const evening = !!docket && !(docket.counts && Number(docket.counts.needs) > 0);
551
- const bv = buildingView(T, o.key, b, { floor: o.floor, drawer: o.drawer, picks: o.picks, vacant: b.vacant, reviewHeld: !b.vacant, evening: evening, cleared: o.cleared });
666
+ const bv = buildingView(T, o.key, b, { floor: o.floor, drawer: o.drawer, picks: o.picks, vacant: b.vacant, reviewHeld: !b.vacant, evening: evening, cleared: o.cleared, office: o.office || null, works: o.works || null });
552
667
  bv.key = o.key; bv.mode = mode; bv.vacant = b.vacant;
553
- bv.label = b.name + ' in section: ' + b.floors.length + ' of ' + bv.cap + ' floors in use.';
668
+ bv.label = b.name + ' in section: ' + b.floors.length + ' of ' + bv.capFloors + ' floors in use' + (o.office ? ', the Lead\'s office on top' : '') + '.';
554
669
  return bv;
555
670
  }
556
671