@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
@@ -0,0 +1,712 @@
1
+ // modules/hall-ui/public/street-depth.js — THE LIVING STREET's exterior: its
2
+ // depth, as data, and the painters that put it on the page (task 1004685, the
3
+ // living-city prototype; carved out of street-draw.js and street.js).
4
+ //
5
+ // WHY ITS OWN FILE. The living street made street.js cross the 1,500-line
6
+ // ratchet and gave street-draw.js a require cycle with street-inside.js (the
7
+ // floor plan the needs-you windows light). So the living street's exterior is
8
+ // here, in the shape street-deck.js has: two halves.
9
+ //
10
+ // MODEL (pure: no DOM, no fetch, no clock, node-require-safe). The three
11
+ // planes over a street street-draw.js has already laid out: structureOf (a
12
+ // civic building's capsule floors and its construction), skylineOf (every
13
+ // open goal as a cluster on a hazed skyline), readingTiles (the six readings
14
+ // as one number and two words) and depthOf (the skylanes, the road, the
15
+ // sittings, the windows that need you, the plates, the key). street-draw.js
16
+ // finds this file on its global when a street is drawn deep (it never
17
+ // requires it, so there is no cycle); this file requires street-draw.js for
18
+ // the front-on kit and street-inside.js for the floor plan.
19
+ //
20
+ // PAGE (`page`, browser only). street.js hands it the street's data, its card
21
+ // opener and a redraw: pin(p) and sign(sg, tile) are the exterior's two
22
+ // controls, depth(dm, W) paints the planes, and loadLiving / livingModel read
23
+ // the living model's other feeds once through gov-feeds.js (the same reads the
24
+ // city, the deck and the shell's tally stand on). No innerHTML anywhere.
25
+ //
26
+ // Motion lives in street.css, and only says "this is live": no number here is
27
+ // a speed or a blink.
28
+
29
+ (function (root) {
30
+ 'use strict';
31
+ const S = root.GovStreet || (typeof require === 'function' ? require('./street-draw.js') : null);
32
+ const City = root.GovCity || (typeof require === 'function' ? require('./city-draw.js') : null);
33
+ const push = S.push, rect = S.rect, ellipse = S.ellipse, box = S.box, money = S.money, count = S.count;
34
+ const G = S.G, CIVIC_KEYS = S.CIVIC_KEYS, MORE_KEY = S.MORE_KEY;
35
+
36
+ // ---------- THE DEPTH (task 1004685, the living-city prototype) ----------
37
+ // The street was one flat row; it now stands in three planes, front-on, in
38
+ // the street's own drawing style:
39
+ // BACK a skyline of EVERY open goal, one cluster each, its height from
40
+ // the goal's real rollup (its size, sqrt-scaled to the largest goal,
41
+ // times how much of it is built), in two rows the page hazes toward
42
+ // the void (the faces mixed 25% and 50% into the ground; the civic
43
+ // row is 0%). No detail: no windows, no names.
44
+ // MIDDLE the nine civic buildings with a deeper side face (DEPTH.side) and
45
+ // a height that is structure: one capsule floor plugs into the core
46
+ // for a seated Lead and each committee room (gov-living.js, over GET
47
+ // /government/leads), so a Lead's added floor makes the
48
+ // building taller. Under it, at street level, the Lead's name plate,
49
+ // or "owner" where the building is unseated.
50
+ // FRONT the road: an agent whose claim answers to a building but to no
51
+ // goal on the street works on the road in front of it, and the
52
+ // parcels (the crates at Review) stay where they were.
53
+ // SKY the skylanes: an agent whose claim belongs to a goal travels a
54
+ // lane between that goal's plot (or the plot that stands for the
55
+ // other goals) and the building it answers to. One pod per active
56
+ // claim, wherever it rides, at a fixed spacing, so they count.
57
+ // Construction is the city's: an authorised build-out is a live site (its
58
+ // finished floors, the scaffold at its % complete, a crane whose jib swings);
59
+ // a proposal waiting on a vote is a parked crane, a dashed-glass ghost annex
60
+ // and a tally arc of one dot per seat; an open sitting is a light shaft from
61
+ // the roof; and a matter that needs you lights SALMON windows on the floor
62
+ // that holds it (street-inside.js PLAN), with a ring that fills as it ages.
63
+ // Motion lives in street.css, and only says "this is live": no number here is
64
+ // a speed or a blink.
65
+ const DEPTH = Object.freeze({
66
+ side: 12, // the civic row's side face (the prototype's is 7)
67
+ cap: 13, // one capsule floor: an 11 px pod and its 2 px joint
68
+ siteFloors: 3, // an authorised build-out, finished
69
+ ghostFloors: 3, // a proposal's ghost annex, as tall as the build-out it would be
70
+ siteW: 40, // a build-out's footprint: wider than the core, so it reads as a site
71
+ craneRise: 30, // the crane's mast stands this far over the finished height
72
+ jib: 58, // the jib's reach from the mast
73
+ tallyR: 18, // the council's tally arc
74
+ skyTop: 236, // the first skylane's crest (just under the band's top, street.js BAND_TOP)
75
+ laneStep: 16, // each next lane crests this much lower
76
+ laneDrop: 30, // a lane's way back runs this much under its crest
77
+ podGap: 7, // pods on a skylane, in % of its loop
78
+ trainGap: 13, // pods on the road, in px
79
+ shaftTop: 200, // a sitting's light shaft reaches up to here
80
+ ageFull: 30, // days: an age ring is full at a month
81
+ period: 28, // s: one lap of a skylane (street.css street-depth-travel)
82
+ });
83
+ // Where a building's capsule floors plug in: the core's centre from the
84
+ // building's left edge, and the height its first floor sits at, on the
85
+ // recipe's own roof (a gable's apex, the sawtooth's teeth, Release's bridge).
86
+ const PLUG = Object.freeze({
87
+ charter: [42, 212], rules: [48, 58], disputes: [40, 100], people: [60, 54], planning: [40, 90],
88
+ automation: [62, 72], review: [43, 86], release: [46, 122], treasury: [42, 114],
89
+ });
90
+ // The face whose storeys stand for a building's floors (street-inside.js
91
+ // PLAN, bottom first): [x0, x1] from its left edge, [y0, y1] above the street.
92
+ const FACE = Object.freeze({
93
+ charter: [14, 70, 10, 136], rules: [8, 88, 8, 50], disputes: [25, 55, 4, 82], people: [41, 79, 6, 34],
94
+ planning: [3, 77, 6, 70], automation: [3, 77, 10, 52], review: [7, 79, 4, 80], release: [2, 24, 6, 146], treasury: [10, 74, 12, 90],
95
+ });
96
+ const CAP_W = Object.freeze({ lead: 30, committee: 24 });
97
+ const DECK_HREF = '/builders/street#deck';
98
+
99
+ // The street-inside.js floor plan, read when a street is drawn (it requires
100
+ // this file, so it is found on its global, or required late in node).
101
+ function floorPlan() {
102
+ const I = root.GovInside || (typeof require === 'function' ? require('./street-inside.js') : null);
103
+ return I && I.PLAN ? I.PLAN : {};
104
+ }
105
+ function floorOfSource(kind, source) {
106
+ const plan = floorPlan()[kind];
107
+ const floors = plan && Array.isArray(plan.floors) ? plan.floors : [];
108
+ for (let i = 0; i < floors.length; i++) if (floors[i][2] && floors[i][2].source === source) return { index: i, of: floors.length, name: floors[i][0] };
109
+ return null;
110
+ }
111
+
112
+ // A building's structure, standing on its recipe's roof: the core, one
113
+ // capsule floor per seat, then what is being built on top. Returns the
114
+ // polys, the site and proposal parts the page draws in front (the crane,
115
+ // the tally), and the heights above the street.
116
+ function structureOf(T, kind, x, b, ctx) {
117
+ const polys = [];
118
+ const plug = PLUG[kind];
119
+ const cx = x + plug[0];
120
+ const floors = [];
121
+ if (b) {
122
+ if (b.lead) floors.push({ role: 'lead' });
123
+ ((b.structure && b.structure.committees) || []).forEach(() => floors.push({ role: 'committee' }));
124
+ }
125
+ let h = plug[1];
126
+ if (floors.length) {
127
+ const coreTop = h + floors.length * DEPTH.cap + 4;
128
+ push(polys, rect(cx - 3, G - coreTop, 6, coreTop - h + 6), T.civic.r, T.civic.s);
129
+ floors.forEach((f, i) => {
130
+ const cw = CAP_W[f.role];
131
+ const off = i % 2 ? 3 : -3;
132
+ const z = h + i * DEPTH.cap + 1;
133
+ const x0 = cx - cw / 2 + off;
134
+ box(polys, T, x0, G, cw, 11, T.civic, { z: z, d: 5 });
135
+ // its window: a Lead's office is a strip, a committee two portholes
136
+ const wy = G - z - 5.5;
137
+ if (f.role === 'lead') push(polys, rect(x0 + 5, wy - 1.5, cw - 10, 3), T.win, 'none');
138
+ else push(polys, ellipse(cx + off - 5, wy, 2.2, 2.2) + ellipse(cx + off + 5, wy, 2.2, 2.2), T.win, 'none');
139
+ });
140
+ h += floors.length * DEPTH.cap;
141
+ }
142
+ const stack = h;
143
+ // the top capsule's own top face (d 5) is part of the building's height
144
+ let top = floors.length ? h + 5 : h;
145
+ const cons = (b && Array.isArray(b.construction)) ? b.construction : [];
146
+ const siteOf = cons.find((c) => c && c.kind === 'buildout' && c.authorised);
147
+ const propOf = cons.find((c) => c && !c.authorised);
148
+ let site = null;
149
+ let proposal = null;
150
+ // Construction reads at a glance and is the city's family: the same parts at the
151
+ // same proportions (a footprint wider than the core, a lattice mast standing
152
+ // clear of it, a jib with its ties and counterweight, a hook and its load).
153
+ const fw = DEPTH.siteW;
154
+ if (siteOf) {
155
+ // a live site: the finished floors, the scaffold up to its share built, a crane past the finished height
156
+ const p = siteOf.progress == null ? 0 : Math.max(0, Math.min(1, Number(siteOf.progress) || 0));
157
+ const target = DEPTH.siteFloors * DEPTH.cap;
158
+ const built = Math.floor(p * DEPTH.siteFloors + 1e-9);
159
+ for (let i = 0; i < built; i++) box(polys, T, cx - fw / 2, G, fw, 11, T.civic, { z: h + i * DEPTH.cap + 1, d: 5, fins: 3 });
160
+ const sh = Math.max(4, Math.round(p * target));
161
+ box(polys, T, cx - fw / 2 - 3, G, fw + 6, sh, T.scaffold, { z: h, d: 5, brace: true });
162
+ const mx = cx + fw / 2 + 10, base = G - h, mastTop = base - target - DEPTH.craneRise, hookY = base - sh - 12;
163
+ let mast = 'M' + (mx - 2.5) + ' ' + base + 'V' + mastTop + 'M' + (mx + 2.5) + ' ' + base + 'V' + mastTop;
164
+ for (let y = base - 8, k = 0; y > mastTop + 6; y -= 8, k++) mast += 'M' + (mx - 2.5) + ' ' + y + 'L' + (mx + 2.5) + ' ' + (k % 2 ? y + 8 : y - 8);
165
+ mast += rect(mx - 4, mastTop + 1, 8, 6);
166
+ const tip = mx - DEPTH.jib, tail = mx + 18;
167
+ site = {
168
+ key: 'site:' + kind, building: kind, label: String(siteOf.label || ''), progress: p,
169
+ mast: mast,
170
+ jib: 'M' + tip + ' ' + mastTop + 'H' + tail + 'M' + tip + ' ' + mastTop + 'L' + mx + ' ' + (mastTop - 12) + 'L' + tail + ' ' + mastTop
171
+ + 'M' + (tip + 14) + ' ' + mastTop + 'L' + (tip + 14) + ' ' + (mastTop + 3) + 'M' + (tip + 28) + ' ' + mastTop + 'L' + (tip + 28) + ' ' + (mastTop + 3),
172
+ weight: rect(tail - 9, mastTop, 9, 7),
173
+ line: 'M' + (tip + 8) + ' ' + mastTop + 'V' + hookY,
174
+ slab: rect(tip + 1, hookY, 16, 4),
175
+ ox: mx, oy: mastTop, hx: tip + 8, lift: Math.round((hookY - mastTop) / 2),
176
+ platform: { x: cx - fw / 2 - 2, y: base - sh, w: fw + 4, rise: Math.max(0, sh - 3) },
177
+ };
178
+ h += target;
179
+ top = Math.max(top, h + DEPTH.craneRise + 14);
180
+ }
181
+ if (propOf) {
182
+ // a proposal waiting on a vote: a dashed-glass ghost annex, a parked crane, a tally arc
183
+ const gh = DEPTH.ghostFloors * DEPTH.cap;
184
+ const ghost = { f: T.glass.f, r: T.glass.r, t: T.glass.t, s: T.glass.s, dash: '3 3' };
185
+ box(polys, T, cx - fw / 2, G, fw, gh, ghost, { z: h, d: 5, grid: 3 });
186
+ const base = G - h, mx = cx + fw / 2 + 10, mastTop = base - gh - 20;
187
+ const t = ctx.tallyOf(propOf);
188
+ const r = DEPTH.tallyR, acx = cx, acy = base - gh - 14;
189
+ const dots = [];
190
+ for (let i = 0; i < t.seats; i++) {
191
+ const a = Math.PI * (1 - (t.seats === 1 ? 0.5 : i / (t.seats - 1)));
192
+ dots.push({ cx: Math.round((acx + r * Math.cos(a)) * 10) / 10, cy: Math.round((acy - r * Math.sin(a)) * 10) / 10, vote: i < t.for ? 'for' : i < t.for + t.against ? 'against' : 'open' });
193
+ }
194
+ let mast = 'M' + (mx - 2.5) + ' ' + base + 'V' + mastTop + 'M' + (mx + 2.5) + ' ' + base + 'V' + mastTop + 'M' + (mx - 2.5) + ' ' + mastTop + 'H' + (mx + 2.5);
195
+ for (let y = base - 8, k = 0; y > mastTop + 6; y -= 8, k++) mast += 'M' + (mx - 2.5) + ' ' + y + 'L' + (mx + 2.5) + ' ' + (k % 2 ? y + 8 : y - 8);
196
+ proposal = {
197
+ key: 'proposal:' + kind, building: kind, label: String(propOf.label || ''),
198
+ // parked: the jib folded down along the mast, no hook, no load
199
+ crane: mast + 'M' + mx + ' ' + mastTop + 'L' + (mx - 9) + ' ' + (mastTop + 30) + 'M' + mx + ' ' + mastTop + 'L' + (mx + 7) + ' ' + (mastTop - 7),
200
+ arc: 'M' + (acx - r) + ' ' + acy + 'A' + r + ' ' + r + ' 0 0 1 ' + (acx + r) + ' ' + acy,
201
+ dots: dots, tally: t,
202
+ };
203
+ h += gh;
204
+ top = Math.max(top, h + 14 + r + 4);
205
+ }
206
+ return { polys: polys, cx: cx, floors: floors, stack: stack, top: top, site: site, proposal: proposal };
207
+ }
208
+
209
+ // Every open goal as a cluster on the skyline, in two rows (near, far): the
210
+ // tallest tower of the largest, most-built goal stands SKY_MAX high.
211
+ const SKY_MIN = 24, SKY_MAX = 196;
212
+ function skylineOf(goals, byGoal, W) {
213
+ const total = (g) => {
214
+ const r = byGoal[String(g.id)];
215
+ return count(r, 'total') || (count(r, 'done') + count(r, 'active') + count(r, 'ready') + count(r, 'blocked') + count(r, 'backlog'));
216
+ };
217
+ const max = Math.max(1, ...goals.map(total), 1);
218
+ const rows = { near: { face: '', side: '' }, far: { face: '', side: '' } };
219
+ const clusters = [];
220
+ const lists = [['near', goals.filter((_, i) => i % 2 === 0), 0.5], ['far', goals.filter((_, i) => i % 2 === 1), 1]];
221
+ lists.forEach((l) => {
222
+ const row = l[0], list = l[1], shift = l[2];
223
+ const slot = W / Math.max(1, list.length);
224
+ list.forEach((g, j) => {
225
+ const tot = total(g), done = count(byGoal[String(g.id)], 'done');
226
+ const seed = String(g.id).split('').reduce((a, c) => (a * 31 + c.charCodeAt(0)) % 9973, 7);
227
+ // never above the Charter's own height, so the civic row stays the subject
228
+ const H = Math.round(SKY_MIN + (SKY_MAX - SKY_MIN) * Math.sqrt(tot / max) * (0.45 + 0.55 * (tot ? Math.min(1, done / tot) : 0)));
229
+ const n = 2 + Math.min(2, Math.floor(tot / 60));
230
+ const tw = [];
231
+ for (let t = 0; t < n; t++) tw.push(16 + Math.round(City.rnd(seed + t * 7) * 14));
232
+ const width = tw.reduce((a, b) => a + b, 0) + (n - 1) * 3;
233
+ const cx = (((j + shift) * slot) % W + W) % W;
234
+ let x = Math.round(cx - width / 2);
235
+ const x0 = x;
236
+ const tallest = Math.floor(City.rnd(seed + 3) * n) % n;
237
+ tw.forEach((w, t) => {
238
+ const h = t === tallest ? H : Math.max(14, Math.round(H * (0.45 + 0.4 * City.rnd(seed + t * 11 + 5))));
239
+ const y = G - h, d = 6;
240
+ rows[row].face += rect(x, y, w, h) + 'M' + x + ' ' + y + 'L' + (x + d) + ' ' + (y - d) + 'L' + (x + w + d) + ' ' + (y - d) + 'L' + (x + w) + ' ' + y + 'Z';
241
+ rows[row].side += 'M' + (x + w) + ' ' + y + 'L' + (x + w + d) + ' ' + (y - d) + 'L' + (x + w + d) + ' ' + (G - d) + 'L' + (x + w) + ' ' + G + 'Z';
242
+ x += w + 3;
243
+ });
244
+ clusters.push({ goal: String(g.id), row: row, x0: x0, x1: x, h: H, total: tot, done: done });
245
+ });
246
+ });
247
+ const paths = (r) => (rows[r].face ? [{ d: rows[r].side, c: 'street-depth-sil__side' }, { d: rows[r].face, c: 'street-depth-sil__face' }] : []);
248
+ return { near: paths('near'), far: paths('far'), clusters: clusters };
249
+ }
250
+
251
+ // The six readings as the desk's tiles: one number and two words, or null
252
+ // (hidden) at zero or when nothing real holds the number. The living model
253
+ // drives the four it can read (flow: agents working; money: spend this
254
+ // month; people: the roster; rules: the laws in force); promises and
255
+ // fairness are the docket's own readings.
256
+ function readingTiles(docket, living, roster) {
257
+ const reading = {};
258
+ (docket && Array.isArray(docket.readings) ? docket.readings : []).forEach((r) => { if (r && r.key) reading[r.key] = r; });
259
+ const live = (k) => (reading[k] && reading[k].status === 'live' ? reading[k] : null);
260
+ const num = (v) => (typeof v === 'number' && isFinite(v) ? v : null);
261
+ const tile = (n, one, many, title) => (n == null || n === 0 || n === '$0' ? null : { n: typeof n === 'number' ? n.toLocaleString('en-US') : String(n), words: n === 1 ? one : many, title: String(title || '') });
262
+ // a live reading's own figure, split into its number and the rest
263
+ const fromFigure = (k) => {
264
+ const r = live(k);
265
+ const m = r && /^\s*(\$?[\d,.]+)\s+(.+)$/.exec(String(r.figure || ''));
266
+ // a figure that is not one number and at most two words is no tile
267
+ return m && !/^\$?0$/.test(m[1]) && m[2].trim().split(/\s+/).length <= 2 && !/\d/.test(m[2]) ? { n: m[1], words: m[2].trim(), title: String(r.line || '') } : null;
268
+ };
269
+ const city = living && living.city ? living.city : null;
270
+ const out = {};
271
+ // flow: the agents working right now (the count every surface prints, gov-living.js)
272
+ const atWork = city && city.counts ? num(city.counts.agents_at_work) : city && city.traffic ? num(city.traffic.agents) : null;
273
+ out.flow = city && (city.counts || city.traffic) ? tile(atWork, 'agent working', 'agents working', 'active claims right now') : fromFigure('flow');
274
+ // promises: the criteria the goals say they deliver, kept
275
+ const pv = live('promises') && live('promises').value;
276
+ out.promises = pv && typeof pv === 'object' ? tile(num(pv.kept), 'promise kept', 'promises kept', live('promises').figure) : fromFigure('promises');
277
+ // money: the AI spend this month
278
+ const spend = city && city.spend ? num(city.spend.month_usd) : null;
279
+ out.money = spend != null ? tile(spend > 0 ? money(spend) : 0, 'this month', 'this month', 'spent on AI this month') : fromFigure('money');
280
+ // people: who builds here
281
+ const list = roster && Array.isArray(roster.builders) ? roster.builders : null;
282
+ const active = list ? list.filter((b) => b && (b.status == null || b.status === 'active')).length : null;
283
+ out.people = active != null ? tile(active, 'builder here', 'builders here', 'active builders on the roster') : fromFigure('people');
284
+ // fairness: the disputes still open
285
+ const fv = live('fairness') && num(live('fairness').value);
286
+ const fd = docket && Array.isArray(docket.matters) ? docket.matters.filter((m) => m && m.building === 'disputes' && m.state !== 'settled').length : null;
287
+ out.fairness = fv != null ? tile(fv, 'dispute open', 'disputes open', live('fairness').line) : fd != null ? tile(fd, 'dispute open', 'disputes open', 'open at Disputes') : null;
288
+ // rules: the laws in force (GET /government/laws, and the rule the project runs)
289
+ const laws = city && Array.isArray(city.laws) ? city.laws : null;
290
+ out.rules = laws ? tile(laws.length, 'standing law', 'standing laws', laws.map((l) => l.text).join(' · ')) : fromFigure('rules');
291
+ return out;
292
+ }
293
+
294
+ // The depth layers over a street already laid out (see the header above).
295
+ function depthOf(c) {
296
+ const segs = c.segs, at = c.at, roofs = c.roofs, struct = c.struct, living = c.living, docket = c.docket;
297
+ const openGoal = {};
298
+ c.goals.forEach((g) => { openGoal[String(g.id)] = true; });
299
+ const W = c.W;
300
+ const sky = skylineOf(c.goals, c.byGoal, W);
301
+
302
+ // the plates: the Lead's name, or the owner where nobody holds it
303
+ const plates = [];
304
+ const viewerId = living && living.viewer ? String(living.viewer.id) : null;
305
+ if (living) {
306
+ segs.forEach((s) => {
307
+ if (!s.k) return;
308
+ const b = c.lb[s.k];
309
+ if (!b) return;
310
+ const lead = b.lead;
311
+ const holder = lead || b.holder;
312
+ if (!holder) return;
313
+ const you = viewerId != null && String(holder.id) === viewerId;
314
+ const text = lead ? (you ? 'you' : String(lead.name || lead.login || 'a Lead')) : (you ? 'you · owner' : 'owner');
315
+ plates.push({ key: 'plate:' + s.k, building: s.k, x: s.x - 3, y: G + 3, w: s.w + 6, text: text, seat: lead ? 'lead' : 'owner', you: you });
316
+ });
317
+ }
318
+
319
+ // the agents: one pod per active claim
320
+ const claims = living && living.city && living.city.traffic && Array.isArray(living.city.traffic.claims) ? living.city.traffic.claims : [];
321
+ const lanes = [];
322
+ const laneBy = {};
323
+ const trains = {};
324
+ claims.forEach((cl) => {
325
+ const b = cl && cl.building && at[cl.building] ? cl.building : null;
326
+ // its goal's own plot, or the plot that stands for the open goals past the eighth
327
+ const plot = cl && cl.goal_id != null && openGoal[String(cl.goal_id)] ? (at['d:' + cl.goal_id] || at['d:' + MORE_KEY] || null) : null;
328
+ if (b && plot) {
329
+ const k = plot.d + '>' + b;
330
+ if (!laneBy[k]) { laneBy[k] = { key: 'lane:' + k, from: plot.d, building: b, n: 0 }; lanes.push(laneBy[k]); }
331
+ laneBy[k].n += 1;
332
+ } else {
333
+ const k = b || '*';
334
+ trains[k] = (trains[k] || 0) + 1;
335
+ }
336
+ });
337
+ lanes.sort((a, b) => at[a.building].x - at[b.building].x || a.key.localeCompare(b.key));
338
+ const pods = [];
339
+ lanes.forEach((ln, i) => {
340
+ const from = at['d:' + ln.from], to = at[ln.building];
341
+ const sx = Math.round(from.x + from.w / 2), sy = G - (roofs['d:' + ln.from] || 0) - 4;
342
+ const ex = struct[ln.building] ? Math.round(struct[ln.building].cx) : Math.round(to.x + to.w / 2), ey = G - (struct[ln.building] ? struct[ln.building].stack : roofs[ln.building]) - 3;
343
+ const yA = DEPTH.skyTop + i * DEPTH.laneStep, yB = yA + DEPTH.laneDrop;
344
+ ln.d = 'M' + sx + ' ' + sy + 'C' + sx + ' ' + yA + ' ' + ex + ' ' + yA + ' ' + ex + ' ' + ey + 'C' + ex + ' ' + yB + ' ' + sx + ' ' + yB + ' ' + sx + ' ' + sy;
345
+ const gap = Math.min(DEPTH.podGap, 100 / ln.n);
346
+ for (let j = 0; j < ln.n; j++) {
347
+ const pct = Math.round((25 + (j - (ln.n - 1) / 2) * gap) * 100) / 100;
348
+ const atPct = ((pct % 100) + 100) % 100;
349
+ pods.push({ key: ln.key + ':' + j, lane: ln.key, path: ln.d, at: atPct, delay: -Math.round(atPct / 100 * DEPTH.period * 100) / 100 });
350
+ }
351
+ });
352
+ const road = Object.keys(trains).map((k) => {
353
+ const s = k === '*' ? null : at[k];
354
+ const n = trains[k];
355
+ const len = (n - 1) * DEPTH.trainGap + 9;
356
+ const x0 = s ? s.x + 4 : 43, x1 = s ? s.x + s.w - 4 : W - 43;
357
+ const half = len / 2;
358
+ const a = Math.min(x0 + half, (x0 + x1) / 2), b = Math.max(x1 - half, (x0 + x1) / 2);
359
+ return { key: 'road:' + k, building: s ? k : null, n: n, path: 'M' + a + ' ' + (G + 22) + 'H' + b, w: len, gap: DEPTH.trainGap };
360
+ });
361
+
362
+ // an open sitting: a light shaft from the roof (a board sitting still open,
363
+ // or a building whose way of deciding is being voted)
364
+ const sitting = {};
365
+ (docket && Array.isArray(docket.matters) ? docket.matters : []).forEach((m) => {
366
+ if (m && m.source === 'board' && m.state !== 'settled' && at[m.building]) sitting[m.building] = true;
367
+ });
368
+ (docket && Array.isArray(docket.buildings) ? docket.buildings : []).forEach((b) => {
369
+ if (b && b.decision && b.decision.sitting && at[b.key]) sitting[b.key] = true;
370
+ });
371
+ const shafts = CIVIC_KEYS.filter((k) => sitting[k]).map((k) => {
372
+ const cx = struct[k] ? struct[k].cx : at[k].x + at[k].w / 2;
373
+ const y1 = G - (struct[k] ? struct[k].stack : roofs[k]) + 4;
374
+ return { key: 'shaft:' + k, building: k, x: Math.round(cx - 10), y: DEPTH.shaftTop, w: 20, h: Math.max(40, y1 - DEPTH.shaftTop) };
375
+ });
376
+
377
+ // what needs you lights the windows of the floor that holds it (c.matters: the
378
+ // matters as the living street sees them, a seat's asks for someone else in ink)
379
+ const windows = [];
380
+ const byFloor = {};
381
+ (Array.isArray(c.matters) ? c.matters : docket && Array.isArray(docket.matters) ? docket.matters : []).forEach((m) => {
382
+ if (!m || m.state !== 'needs' || !at[m.building] || !FACE[m.building]) return;
383
+ const f = floorOfSource(m.building, m.source);
384
+ if (!f) return;
385
+ const k = m.building + ':' + f.index;
386
+ (byFloor[k] = byFloor[k] || { building: m.building, floor: f, matters: [] }).matters.push(m);
387
+ });
388
+ Object.keys(byFloor).sort().forEach((k) => {
389
+ const g = byFloor[k];
390
+ const s = at[g.building], face = FACE[g.building];
391
+ const y = Math.round(G - (face[2] + (g.floor.index + 0.5) * (face[3] - face[2]) / g.floor.of));
392
+ const ringX = s.x + face[0] + 4;
393
+ const room = Math.max(1, Math.floor((face[1] - face[0] - 10) / 7));
394
+ const n = Math.min(g.matters.length, room);
395
+ let d = '';
396
+ for (let i = 0; i < n; i++) d += rect(s.x + face[0] + 10 + i * 7, y - 1.5, 4, 3);
397
+ const opened = g.matters.map((m) => (m.opened_at ? Date.parse(m.opened_at) : NaN)).filter((t) => isFinite(t));
398
+ const age = opened.length && c.now ? Math.max(0, Math.floor((c.now - Math.min(...opened)) / 86400000)) : null;
399
+ const frac = age == null ? 0 : Math.min(1, age / DEPTH.ageFull);
400
+ const r = 3.6;
401
+ let arc = '';
402
+ if (frac >= 1) arc = ellipse(ringX, y, r, r);
403
+ else if (frac > 0) {
404
+ const a = -Math.PI / 2 + frac * 2 * Math.PI;
405
+ arc = 'M' + ringX + ' ' + Math.round((y - r) * 100) / 100 + 'A' + r + ' ' + r + ' 0 ' + (frac > 0.5 ? 1 : 0) + ' 1 ' + Math.round((ringX + r * Math.cos(a)) * 100) / 100 + ' ' + Math.round((y + r * Math.sin(a)) * 100) / 100;
406
+ }
407
+ windows.push({ key: 'needs:' + k, building: g.building, floor: g.floor.name, count: g.matters.length, lit: n, d: d, ring: ellipse(ringX, y, r, r), arc: arc, age: age, frac: Math.round(frac * 1000) / 1000 });
408
+ });
409
+
410
+ // where agents are at work the road is lit: a pool under the building or the
411
+ // district, as strong as it is busy (an ink wash on paper: paper cannot glow)
412
+ const pools = [];
413
+ if (living) {
414
+ const byDistrict = {};
415
+ (Array.isArray(living.districts) ? living.districts : []).forEach((d) => { if (d && d.goal_id != null) byDistrict[String(d.goal_id)] = d; });
416
+ segs.forEach((s) => {
417
+ const b = s.k ? c.lb[s.k] : byDistrict[String(s.d)];
418
+ const a = b ? Math.max(0, Math.min(1, Number(b.activity) || 0)) : 0;
419
+ if (a > 0) pools.push({ key: 'pool:' + (s.k || 'd:' + s.d), building: s.k || null, x: Math.round(s.x - 14), y: G - 10, w: Math.round(s.w + 28), a: Math.round(a * 100) / 100 });
420
+ });
421
+ }
422
+ const sites = CIVIC_KEYS.map((k) => struct[k] && struct[k].site).filter(Boolean);
423
+ const proposals = CIVIC_KEYS.map((k) => struct[k] && struct[k].proposal).filter(Boolean);
424
+ const capsules = CIVIC_KEYS.filter((k) => struct[k] && struct[k].floors.length).map((k) => ({ building: k, floors: struct[k].floors.length, roles: struct[k].floors.map((f) => f.role) }));
425
+ const tiles = readingTiles(docket, living, c.roster);
426
+ const roadN = road.reduce((a, t) => a + t.n, 0);
427
+ const label = (sky.clusters.length ? 'Behind the street, ' + (sky.clusters.length === 1 ? 'one goal stands' : sky.clusters.length + ' goals stand') + ' as a skyline. ' : '')
428
+ + (claims.length ? (claims.length === 1 ? 'One agent is' : claims.length + ' agents are') + ' at work: ' + pods.length + ' on the skylanes, ' + roadN + ' on the road.' : 'No agent is at work.');
429
+ return {
430
+ far: sky.far, near: sky.near, clusters: sky.clusters,
431
+ lanes: lanes.map((l) => ({ key: l.key, from: l.from, building: l.building, n: l.n, d: l.d })), pods: pods, road: road,
432
+ shafts: shafts, windows: windows, sites: sites, proposals: proposals, capsules: capsules, plates: plates, pools: pools,
433
+ tiles: tiles, agents: claims.length,
434
+ label: label, period: DEPTH.period,
435
+ };
436
+ }
437
+
438
+ // =====================================================================
439
+ // THE PAGE HALF. street.js calls these with its own data and callbacks;
440
+ // nothing here runs until it does, so node can require the model above.
441
+ // =====================================================================
442
+ const page = (function () {
443
+ const SVG_NS = 'http://www.w3.org/2000/svg';
444
+ // the page's document (a global in the browser and in the tests' vm alike)
445
+ const doc = () => (typeof document !== 'undefined' ? document : root.document);
446
+ const $ = (id) => doc().getElementById(id);
447
+ function clear(el) { while (el && el.firstChild) el.removeChild(el.firstChild); }
448
+ function node(tag, cls, text) {
449
+ const el = doc().createElement(tag);
450
+ if (cls) el.className = cls;
451
+ if (text != null) el.textContent = String(text);
452
+ return el;
453
+ }
454
+ function svgEl(tag) { return doc().createElementNS(SVG_NS, tag); }
455
+ function at(el, x, y, w, h) {
456
+ el.style.left = x + 'px';
457
+ el.style.top = y + 'px';
458
+ if (w != null) el.style.width = w + 'px';
459
+ if (h != null) el.style.height = h + 'px';
460
+ return el;
461
+ }
462
+ // a CSS offset path from path data this file computed; anything but path data is refused
463
+ function offsetPathOf(d) { return /^[MLHVCQAZmlhvcqaz0-9 .,-]+$/.test(String(d)) ? "path('" + d + "')" : "none"; }
464
+ function pathEl(p) {
465
+ const el = svgEl('path');
466
+ el.setAttribute('d', p.d);
467
+ el.setAttribute('fill', p.f);
468
+ el.setAttribute('stroke', p.s);
469
+ if (p.dash && p.dash !== 'none') el.setAttribute('stroke-dasharray', p.dash);
470
+ el.setAttribute('stroke-width', String(p.w || 1));
471
+ return el;
472
+ }
473
+
474
+ // One pin per building (task 1004569): its tier is its class, a label shows
475
+ // only when the collision pass placed it (street-pin--shown; the rest show
476
+ // on hover or focus), and a count badge says how many matters are open there.
477
+ // env { open: the matter whose card is open, openCard(key, el, keys) }
478
+ function pin(p, env) {
479
+ const keys = p.matters && p.matters.length ? p.matters : [p.key];
480
+ const el = node('button', 'street-pin street-pin--' + p.state + (p.shown ? ' street-pin--shown' : ''));
481
+ el.setAttribute('type', 'button');
482
+ el.setAttribute('aria-label', p.aria);
483
+ el.setAttribute('aria-expanded', keys.indexOf(env.open) >= 0 ? 'true' : 'false');
484
+ el.setAttribute('aria-controls', 'streetCard');
485
+ el.setAttribute('data-matter', p.key);
486
+ el.setAttribute('data-matters', keys.join(' '));
487
+ el.setAttribute('data-building', p.building);
488
+ at(el, p.x, p.y, S.PIN_W, p.h);
489
+ const label = node('span', 'street-pin__label', p.label);
490
+ if (p.label) label.setAttribute('title', p.label);
491
+ el.appendChild(label);
492
+ const head = node('span', 'street-pin__head');
493
+ if (p.state === 'urgent') head.appendChild(node('span', 'street-pin__ring'));
494
+ if (p.isCount) head.appendChild(node('span', 'street-pin__count', p.count));
495
+ else if (p.state !== 'settled') {
496
+ const svg = svgEl('svg');
497
+ svg.setAttribute('width', '18');
498
+ svg.setAttribute('height', '18');
499
+ svg.setAttribute('viewBox', '0 0 18 18');
500
+ svg.setAttribute('aria-hidden', 'true');
501
+ const fill = svgEl('path');
502
+ fill.setAttribute('d', p.gf || '');
503
+ const line = svgEl('path');
504
+ line.setAttribute('d', p.gl || '');
505
+ svg.appendChild(fill);
506
+ svg.appendChild(line);
507
+ head.appendChild(svg);
508
+ }
509
+ if (!p.isCount && Number(p.count) > 1) head.appendChild(node('span', 'street-pin__badge', p.count));
510
+ el.appendChild(head);
511
+ el.appendChild(node('span', 'street-pin__stem'));
512
+ el.addEventListener('click', function () { env.openCard(keys[0], el, keys); });
513
+ return el;
514
+ }
515
+
516
+ // A reading is a tile (task 1004685): one number and two words, hidden at
517
+ // zero or when nothing real holds it (the living model's tiles); without a
518
+ // tile it is the docket's own figure and line, as before.
519
+ function sign(sg, tile) {
520
+ const a = node('a', 'street-sign' + (sg.hot ? ' street-sign--hot' : '') + (sg.here ? ' street-sign--here' : '') + (tile !== undefined ? ' street-depth-tile' : ''));
521
+ a.setAttribute('href', sg.href);
522
+ a.setAttribute('aria-label', tile ? sg.label + ': ' + tile.n + ' ' + tile.words + '. Go to ' + sg.building : sg.aria);
523
+ a.setAttribute('data-reading', sg.key);
524
+ if (sg.here) a.setAttribute('aria-current', 'location');
525
+ const top = node('span', 'street-sign__top');
526
+ top.appendChild(node('span', 'street-sign__dot'));
527
+ top.appendChild(node('span', 'street-label', sg.label));
528
+ a.appendChild(top);
529
+ if (tile === undefined) {
530
+ a.appendChild(node('span', 'street-sign__figure', sg.figure));
531
+ a.appendChild(node('span', 'street-sign__line', sg.line));
532
+ return a;
533
+ }
534
+ if (!tile) { a.hidden = true; return a; }
535
+ const fig = node('span', 'street-sign__figure street-depth-tile__fig');
536
+ const n = node('span', 'street-depth-tile__n', tile.n);
537
+ fig.appendChild(n);
538
+ fig.appendChild(node('span', 'street-depth-tile__words', tile.words));
539
+ a.appendChild(fig);
540
+ if (tile.title) a.setAttribute('title', tile.title);
541
+ return a;
542
+ }
543
+
544
+ // ---------- the living model's reads ----------
545
+ // The street's own three feeds are street.js's load(); these are the living
546
+ // model's others, read once through gov-feeds.js and again only after an act
547
+ // refreshed one (the memo hands back the same bodies until then, so a redraw
548
+ // never re-reads and never loops). GovLiving or GovFeeds missing (an old cached
549
+ // page) draws the deep street from real data alone. The model is data.living,
550
+ // which the deck and the offices draw from; its feeds are data.livingFeeds.
551
+ // laws: the project's law book (task 1004706)
552
+ // counts: the server's one read of the counts every surface prints (task 1004716)
553
+ const LIVING_KEYS = Object.freeze(['counts', 'claims', 'tasks', 'cost', 'roster', 'me', 'blockers', 'moving', 'fence', 'constitution', 'laws', 'leads', 'treasury', 'presets']);
554
+ const living = { asking: false, feeds: null };
555
+ function loadLiving(data, redraw) {
556
+ const F = root.GovFeeds;
557
+ if (living.asking || !root.GovLiving || !F || !data.loaded) return null;
558
+ living.asking = true;
559
+ return F.read(LIVING_KEYS.slice()).then(function (f) {
560
+ living.asking = false;
561
+ const same = living.feeds && LIVING_KEYS.every(function (k) { return living.feeds[k] === f[k]; });
562
+ if (same) return;
563
+ living.feeds = f;
564
+ redraw();
565
+ });
566
+ }
567
+ function livingModel(data) {
568
+ if (!living.feeds || !root.GovFeeds) return null;
569
+ data.livingFeeds = living.feeds;
570
+ const raw = Object.assign({ docket: data.docket, goals: data.goals, rollups: data.rollups }, living.feeds);
571
+ return root.GovFeeds.model(raw, Date.now());
572
+ }
573
+
574
+ // ---------- the planes ----------
575
+ // A skyline row's svg is a 1 px box that paints its whole row as overflow:
576
+ // a deep, scaled-up layer's own box would otherwise widen the street's
577
+ // sideways scroll (a 3D transform counts toward it; an svg's painted
578
+ // overflow does not), and clipping it would flatten the parallax.
579
+ function depthSvg(host, W, point) {
580
+ const svg = svgEl('svg');
581
+ svg.setAttribute('class', 'street-depth-svg');
582
+ svg.setAttribute('width', point ? '1' : String(W));
583
+ svg.setAttribute('height', point ? '1' : '900');
584
+ if (!point) svg.setAttribute('viewBox', '0 0 ' + W + ' 900');
585
+ svg.setAttribute('aria-hidden', 'true');
586
+ svg.setAttribute('focusable', 'false');
587
+ host.appendChild(svg);
588
+ return svg;
589
+ }
590
+ // a path the page colours by class (the hall's tokens), or one in the drawing's own palette
591
+ function depthPath(p, cls) {
592
+ const el = p.c || cls ? svgEl('path') : pathEl(p);
593
+ if (p.c || cls) { el.setAttribute('d', p.d); el.setAttribute('class', p.c || cls); }
594
+ return el;
595
+ }
596
+ function depth(dm, W) {
597
+ const far = $('streetDepthFar'), near = $('streetDepthNear'), sky = $('streetDepthSky'), front = $('streetDepthFront');
598
+ [far, near, sky, front].forEach(clear);
599
+ if (!dm) return;
600
+ // BACK: the goals' skyline, two rows hazed toward the void
601
+ const board = $('streetBoard');
602
+ if (board) board.style.setProperty('--street-depth-ox', (W / 2) + 'px');
603
+ if (far) { const fs = depthSvg(far, W, true); dm.far.forEach(function (p) { fs.appendChild(depthPath(p)); }); }
604
+ if (near) { const ns = depthSvg(near, W, true); dm.near.forEach(function (p) { ns.appendChild(depthPath(p)); }); }
605
+ // SKY: the sittings' light shafts and the skylanes, behind the civic row
606
+ if (sky) {
607
+ dm.shafts.forEach(function (sh) {
608
+ const el = at(node('span', 'street-depth-shaft'), sh.x, sh.y, sh.w, sh.h);
609
+ el.setAttribute('data-building', sh.building);
610
+ el.style.setProperty('--street-depth-climb', sh.h + 'px');
611
+ // the ring rises inside a track that fades at both ends, so it never pops
612
+ const track = node('span', 'street-depth-shaft__track');
613
+ track.appendChild(node('span', 'street-depth-shaft__ring'));
614
+ el.appendChild(track);
615
+ sky.appendChild(el);
616
+ });
617
+ const ls = depthSvg(sky, W);
618
+ dm.lanes.forEach(function (ln) { const p = depthPath({ d: ln.d }, 'street-depth-lane'); p.setAttribute('data-lane', ln.key); ls.appendChild(p); });
619
+ dm.pods.forEach(function (pd) {
620
+ const el = node('span', 'street-depth-pod');
621
+ el.style.offsetPath = offsetPathOf(pd.path);
622
+ el.style.offsetDistance = pd.at + '%';
623
+ el.style.animationDelay = pd.delay + 's';
624
+ el.setAttribute('data-lane', pd.lane);
625
+ sky.appendChild(el);
626
+ });
627
+ }
628
+ if (!front) return;
629
+ // FRONT: what needs you, construction, the plates and the road
630
+ const fsv = depthSvg(front, W);
631
+ dm.windows.forEach(function (w) {
632
+ const g = svgEl('g');
633
+ g.setAttribute('data-building', w.building);
634
+ g.appendChild(depthPath({ d: w.d }, 'street-depth-needs'));
635
+ g.appendChild(depthPath({ d: w.ring }, 'street-depth-age'));
636
+ if (w.arc) g.appendChild(depthPath({ d: w.arc }, 'street-depth-age__arc'));
637
+ fsv.appendChild(g);
638
+ });
639
+ dm.sites.forEach(function (st) {
640
+ const g = svgEl('g');
641
+ g.setAttribute('class', 'street-depth-site');
642
+ g.setAttribute('data-building', st.building);
643
+ const pf = svgEl('rect');
644
+ pf.setAttribute('class', 'street-depth-platform');
645
+ pf.setAttribute('x', String(st.platform.x)); pf.setAttribute('y', String(st.platform.y - 1));
646
+ pf.setAttribute('width', String(st.platform.w)); pf.setAttribute('height', '2');
647
+ pf.style.setProperty('--street-depth-rise', st.platform.rise + 'px');
648
+ g.appendChild(pf);
649
+ g.appendChild(depthPath({ d: st.mast }, 'street-depth-crane'));
650
+ const jib = svgEl('g');
651
+ jib.setAttribute('class', 'street-depth-jib');
652
+ jib.style.transformOrigin = st.ox + 'px ' + st.oy + 'px';
653
+ jib.appendChild(depthPath({ d: st.jib }, 'street-depth-crane'));
654
+ jib.appendChild(depthPath({ d: st.weight }, 'street-depth-weight'));
655
+ // the hook: its line reels in from the jib while the slab rises with it
656
+ const line = depthPath({ d: st.line }, 'street-depth-crane street-depth-line');
657
+ line.style.transformOrigin = st.hx + 'px ' + st.oy + 'px';
658
+ const slab = depthPath({ d: st.slab }, 'street-depth-slab');
659
+ slab.style.setProperty('--street-depth-lift', (-st.lift) + 'px');
660
+ jib.appendChild(line);
661
+ jib.appendChild(slab);
662
+ g.appendChild(jib);
663
+ fsv.appendChild(g);
664
+ });
665
+ dm.proposals.forEach(function (pr) {
666
+ const g = svgEl('g');
667
+ g.setAttribute('class', 'street-depth-proposal');
668
+ g.setAttribute('data-building', pr.building);
669
+ g.appendChild(depthPath({ d: pr.crane }, 'street-depth-crane'));
670
+ g.appendChild(depthPath({ d: pr.arc }, 'street-depth-tally'));
671
+ pr.dots.forEach(function (dt) {
672
+ const c = svgEl('circle');
673
+ c.setAttribute('class', 'street-depth-seat street-depth-seat--' + dt.vote);
674
+ c.setAttribute('cx', String(dt.cx)); c.setAttribute('cy', String(dt.cy)); c.setAttribute('r', '2.4');
675
+ g.appendChild(c);
676
+ });
677
+ fsv.appendChild(g);
678
+ });
679
+ // the light pools sit on the road, under the plates
680
+ (dm.pools || []).forEach(function (pl) {
681
+ const el = at(node('span', 'street-depth-pool'), pl.x, pl.y, pl.w);
682
+ el.style.setProperty('--street-depth-pool-a', String(pl.a));
683
+ if (pl.building) el.setAttribute('data-building', pl.building);
684
+ front.appendChild(el);
685
+ });
686
+ dm.plates.forEach(function (pl) {
687
+ const el = at(node('span', 'street-depth-plate street-depth-plate--' + pl.seat + (pl.you ? ' street-depth-plate--you' : '')), pl.x, pl.y, pl.w);
688
+ el.setAttribute('data-building', pl.building);
689
+ el.appendChild(node('span', 'street-depth-plate__t', pl.text));
690
+ front.appendChild(el);
691
+ });
692
+ dm.road.forEach(function (rd) {
693
+ const tr = node('span', 'street-depth-train');
694
+ tr.style.offsetPath = offsetPathOf(rd.path);
695
+ tr.style.width = rd.w + 'px';
696
+ if (rd.building) tr.setAttribute('data-building', rd.building);
697
+ for (let i = 0; i < rd.n; i++) tr.appendChild(at(node('span', 'street-depth-pod street-depth-pod--road'), i * rd.gap, 0));
698
+ front.appendChild(tr);
699
+ });
700
+ }
701
+
702
+ return { pin: pin, sign: sign, depth: depth, loadLiving: loadLiving, livingModel: livingModel, living: living, LIVING_KEYS: LIVING_KEYS };
703
+ }());
704
+
705
+ const api = {
706
+ structureOf: structureOf, skylineOf: skylineOf, readingTiles: readingTiles, floorOfSource: floorOfSource, depthOf: depthOf,
707
+ DEPTH: DEPTH, PLUG: PLUG, FACE: FACE, CAP_W: CAP_W, DECK_HREF: DECK_HREF, SKY_MIN: SKY_MIN, SKY_MAX: SKY_MAX,
708
+ page: page,
709
+ };
710
+ root.GovStreetDepth = api;
711
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
712
+ }(typeof window !== 'undefined' ? window : globalThis));