@bongos/core 1.21.112 → 1.21.114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/.bongos-core.json +616 -186
  2. package/.claude/skills/ask-for-help/SKILL.md +1 -1
  3. package/.claude/skills/bongos-feedback/SKILL.md +1 -1
  4. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  5. package/.claude/skills/builder-ship/SKILL.md +1 -0
  6. package/GOVERNMENT.md +128 -11
  7. package/clients/bongos-client/README.md +1 -1
  8. package/clients/bongos-client/bongos-client.global.js +28 -0
  9. package/clients/bongos-client/index.cjs +28 -0
  10. package/clients/bongos-client/index.d.ts +45 -2
  11. package/clients/bongos-client/index.mjs +28 -0
  12. package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
  13. package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
  14. package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
  15. package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
  16. package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
  17. package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
  18. package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
  19. package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
  20. package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
  21. package/docs/adr/README.md +10 -1
  22. package/docs/api/openapi.json +989 -87
  23. package/docs/api-reference.md +18 -4
  24. package/docs/copy-inventory.md +343 -262
  25. package/docs/copy-registry.json +1223 -426
  26. package/docs/file-map.md +2 -1
  27. package/docs/module-api-changelog.md +4 -0
  28. package/docs/packs/engineer.md +2 -1
  29. package/docs/page-inventory.json +91 -6
  30. package/docs/page-readings.json +5017 -4458
  31. package/modules/builder-settings/law-locks.js +202 -0
  32. package/modules/builder-settings/routes/settings.js +66 -6
  33. package/modules/builder-settings/settings.js +7 -0
  34. package/modules/discord/ship-broadcast.js +22 -13
  35. package/modules/government/asks-db.js +54 -0
  36. package/modules/government/asks.js +269 -0
  37. package/modules/government/board-idea-reads.js +68 -0
  38. package/modules/government/board.js +21 -57
  39. package/modules/government/build-rights.js +148 -0
  40. package/modules/government/buildouts.js +160 -0
  41. package/modules/government/counts.js +81 -0
  42. package/modules/government/db.js +242 -0
  43. package/modules/government/decision-board.js +234 -8
  44. package/modules/government/decision-rules.js +2 -0
  45. package/modules/government/docket.js +114 -6
  46. package/modules/government/law-board.js +131 -0
  47. package/modules/government/laws.js +241 -0
  48. package/modules/government/leads.js +170 -0
  49. package/modules/government/migrations/government_027_laws.sql +143 -0
  50. package/modules/government/migrations/government_028_asks.sql +57 -0
  51. package/modules/government/migrations/government_029_leads.sql +135 -0
  52. package/modules/government/migrations/government_030_buildouts.sql +111 -0
  53. package/modules/government/migrations/government_031_treasury.sql +107 -0
  54. package/modules/government/module.json +2 -1
  55. package/modules/government/pollers/treasury-borrow.js +62 -0
  56. package/modules/government/positions.js +107 -0
  57. package/modules/government/presets.js +149 -0
  58. package/modules/government/routes/government.js +425 -2
  59. package/modules/government/routing.js +154 -0
  60. package/modules/government/treasury-db.js +166 -0
  61. package/modules/government/treasury.js +241 -0
  62. package/modules/hall-ui/public/atlas.html +2 -2
  63. package/modules/hall-ui/public/blockers.html +2 -2
  64. package/modules/hall-ui/public/board-room.html +2 -2
  65. package/modules/hall-ui/public/board-room.js +2 -0
  66. package/modules/hall-ui/public/city-draw.js +604 -11
  67. package/modules/hall-ui/public/city.css +157 -1
  68. package/modules/hall-ui/public/city.html +37 -7
  69. package/modules/hall-ui/public/city.js +441 -21
  70. package/modules/hall-ui/public/city.states.json +110 -5
  71. package/modules/hall-ui/public/collab.html +2 -2
  72. package/modules/hall-ui/public/commands.html +2 -2
  73. package/modules/hall-ui/public/copy-desk.html +2 -2
  74. package/modules/hall-ui/public/craft-mode.js +112 -29
  75. package/modules/hall-ui/public/deploy.html +2 -2
  76. package/modules/hall-ui/public/diagram-viewer.css +12 -0
  77. package/modules/hall-ui/public/diagram-viewer.js +4 -0
  78. package/modules/hall-ui/public/diagrams.html +2 -2
  79. package/modules/hall-ui/public/drachmae.html +2 -2
  80. package/modules/hall-ui/public/fleet.html +2 -2
  81. package/modules/hall-ui/public/gate.html +2 -2
  82. package/modules/hall-ui/public/goal-map.html +2 -2
  83. package/modules/hall-ui/public/goals.html +2 -2
  84. package/modules/hall-ui/public/gov-feeds.js +141 -0
  85. package/modules/hall-ui/public/gov-living.js +1002 -0
  86. package/modules/hall-ui/public/gov-positions.js +97 -0
  87. package/modules/hall-ui/public/government.html +2 -2
  88. package/modules/hall-ui/public/hall-render.js +3 -0
  89. package/modules/hall-ui/public/idea.html +2 -2
  90. package/modules/hall-ui/public/ideas.html +2 -2
  91. package/modules/hall-ui/public/index.html +3 -3
  92. package/modules/hall-ui/public/modules.html +2 -2
  93. package/modules/hall-ui/public/my-agents.css +334 -0
  94. package/modules/hall-ui/public/my-agents.html +72 -0
  95. package/modules/hall-ui/public/my-agents.js +603 -0
  96. package/modules/hall-ui/public/my-agents.states.json +37 -0
  97. package/modules/hall-ui/public/primer.html +2 -2
  98. package/modules/hall-ui/public/profile-connections.js +40 -28
  99. package/modules/hall-ui/public/profile.html +2 -2
  100. package/modules/hall-ui/public/project-settings.html +2 -2
  101. package/modules/hall-ui/public/ranks.html +2 -2
  102. package/modules/hall-ui/public/roadmap.html +2 -2
  103. package/modules/hall-ui/public/room.css +8 -0
  104. package/modules/hall-ui/public/roster.html +2 -2
  105. package/modules/hall-ui/public/sessions.html +2 -2
  106. package/modules/hall-ui/public/settings.html +2 -2
  107. package/modules/hall-ui/public/shell.js +116 -16
  108. package/modules/hall-ui/public/street-card.js +18 -1
  109. package/modules/hall-ui/public/street-deck.css +299 -0
  110. package/modules/hall-ui/public/street-deck.js +1145 -0
  111. package/modules/hall-ui/public/street-depth.js +712 -0
  112. package/modules/hall-ui/public/street-desk.js +5 -2
  113. package/modules/hall-ui/public/street-draw.js +109 -16
  114. package/modules/hall-ui/public/street-inside.js +137 -22
  115. package/modules/hall-ui/public/street-office.js +705 -0
  116. package/modules/hall-ui/public/street.css +161 -11
  117. package/modules/hall-ui/public/street.html +41 -11
  118. package/modules/hall-ui/public/street.js +78 -66
  119. package/modules/hall-ui/public/street.states.json +129 -21
  120. package/modules/hall-ui/public/studio.html +2 -2
  121. package/modules/hall-ui/public/style.css +5 -1
  122. package/modules/hall-ui/public/task.html +2 -2
  123. package/modules/hall-ui/public/thinking.html +2 -2
  124. package/modules/hall-ui/public/tweak-editor.html +2 -2
  125. package/modules/hall-ui/public/watch.html +2 -2
  126. package/modules/hall-ui/public/watch.js +14 -1
  127. package/modules/hall-ui/public/work.html +2 -2
  128. package/modules/hall-ui/records/living-city.md +57 -0
  129. package/modules/hall-ui/records/my-agents.md +21 -0
  130. package/modules/hall-ui/records/oversight-family.md +1 -0
  131. package/modules/hall-ui/records/profile-connections.md +10 -0
  132. package/modules/hall-ui/records/reading-rooms.md +2 -0
  133. package/modules/ideas/routes/blockers.js +92 -3
  134. package/modules/lifecycle/module.json +2 -1
  135. package/modules/lifecycle/moving.js +17 -1
  136. package/modules/lifecycle/routes/moving.js +6 -0
  137. package/modules/lifecycle/routes/project-settings.js +23 -0
  138. package/modules/onboarding/approval-broadcast.js +8 -0
  139. package/modules/onboarding/routes/access-requests.js +16 -6
  140. package/modules/platform-identity/connections.js +13 -2
  141. package/modules/platform-identity/routes/connections.js +7 -1
  142. package/modules/public-landing/public/account-private.states.json +2 -2
  143. package/modules/public-landing/public/account.html +65 -5
  144. package/modules/public-landing/public/account.states.json +3 -3
  145. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  146. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  147. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
  148. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  149. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  150. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
  151. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  152. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  153. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
  154. package/modules/public-landing/public/assets/world.css +4 -4
  155. package/modules/public-landing/public/connections-empty.states.json +14 -0
  156. package/modules/public-landing/public/connections-inactive.states.json +14 -0
  157. package/modules/public-landing/public/connections.html +450 -0
  158. package/modules/public-landing/public/connections.probes.json +414 -0
  159. package/modules/public-landing/public/connections.states.json +17 -0
  160. package/modules/public-landing/public/contact.html +2 -2
  161. package/modules/public-landing/public/guild.html +2 -2
  162. package/modules/public-landing/public/index.html +2 -2
  163. package/modules/public-landing/public/invitations.html +2 -2
  164. package/modules/public-landing/public/privacy.html +2 -2
  165. package/modules/public-landing/public/projects.html +2 -2
  166. package/modules/public-landing/public/projects.probes.json +6 -2
  167. package/modules/public-landing/public/terms.html +2 -2
  168. package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
  169. package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
  170. package/modules/ui-design/kit/fixtures/connections.json +22 -0
  171. package/modules/ui-design/kit/serve.js +95 -0
  172. package/package-lock.json +2 -2
  173. package/package.json +1 -1
  174. package/release-notes.json +128 -0
  175. package/scripts/discord/README.md +8 -5
  176. package/scripts/discord/post.js +12 -1
  177. package/scripts/gds/artifact-audit.js +20 -3
  178. package/scripts/gds/ask.js +164 -0
  179. package/scripts/gds/autobongos-run.js +20 -5
  180. package/scripts/gds/autobongos-tweak-lane.js +262 -0
  181. package/scripts/gds/autobongos-verify.js +21 -2
  182. package/scripts/gds/claim.js +2 -0
  183. package/scripts/gds/government-walk.js +638 -4
  184. package/scripts/gds/pulse.js +9 -6
  185. package/scripts/gds/ship-escalation.js +22 -3
  186. package/scripts/gds/ship-finish.js +5 -4
  187. package/scripts/gds/ship-merge-gate.js +18 -6
  188. package/scripts/gds/tweak-halt.js +1 -0
  189. package/scripts/hall-preview/README.md +41 -1
  190. package/scripts/hall-preview/pref-writes.js +140 -0
  191. package/scripts/hall-preview/refresh-fixtures.js +101 -7
  192. package/scripts/hall-preview/scrub-identity.js +366 -7
  193. package/scripts/hall-preview/server.js +27 -1
  194. package/scripts/render-diagrams.sh +4 -2
  195. package/src/bongos/auth-idp.js +6 -1
  196. package/src/bongos/project-settings.js +41 -0
  197. package/src/bongos/route-rank-check.js +27 -0
  198. package/src/bongos/routes/me.js +68 -7
  199. package/src/bongos/serve-internal.js +5 -1
  200. package/src/module-api.js +7 -1
  201. package/src/platform-server.js +10 -0
  202. package/tests/application_lifecycle.mjs +47 -5
  203. package/tests/autobongos_tweak_lane.mjs +208 -0
  204. package/tests/conductor_main_e2e.mjs +21 -0
  205. package/tests/connections_api.mjs +18 -0
  206. package/tests/craft_mode_nav.mjs +5 -3
  207. package/tests/craft_mode_tallies.mjs +52 -16
  208. package/tests/discord_approvals.mjs +11 -0
  209. package/tests/discord_channels.mjs +20 -0
  210. package/tests/fixtures/gov-seated.mjs +64 -0
  211. package/tests/fixtures/government-ask-cards.json +1284 -0
  212. package/tests/government_act_alone_buildings.mjs +1 -0
  213. package/tests/government_agent_asks.mjs +322 -0
  214. package/tests/government_asks.mjs +408 -0
  215. package/tests/government_build_rights.mjs +253 -0
  216. package/tests/government_buildouts.mjs +382 -0
  217. package/tests/government_counts.mjs +143 -0
  218. package/tests/government_decision_rules.mjs +5 -1
  219. package/tests/government_laws.mjs +488 -0
  220. package/tests/government_laws_session.mjs +321 -0
  221. package/tests/government_leads.mjs +375 -0
  222. package/tests/government_positions.mjs +127 -0
  223. package/tests/government_presets.mjs +215 -0
  224. package/tests/government_routes.mjs +8 -1
  225. package/tests/government_routing.mjs +118 -0
  226. package/tests/government_seed.mjs +5 -0
  227. package/tests/government_treasury.mjs +394 -0
  228. package/tests/governor_docket.mjs +26 -2
  229. package/tests/hall_buildouts.mjs +208 -0
  230. package/tests/hall_city.mjs +14 -6
  231. package/tests/hall_city_living.mjs +617 -0
  232. package/tests/hall_gov_counts.mjs +336 -0
  233. package/tests/hall_gov_living.mjs +636 -0
  234. package/tests/hall_gov_real_site.mjs +655 -0
  235. package/tests/hall_matter_card.mjs +6 -1
  236. package/tests/hall_my_agents.mjs +481 -0
  237. package/tests/hall_nav.mjs +107 -4
  238. package/tests/hall_page_gate_map.mjs +4 -0
  239. package/tests/hall_preview_governor_fixtures.mjs +30 -3
  240. package/tests/hall_preview_scrub.mjs +59 -1
  241. package/tests/hall_preview_security.mjs +169 -0
  242. package/tests/hall_reading_world.mjs +44 -0
  243. package/tests/hall_roster_world.mjs +6 -2
  244. package/tests/hall_street.mjs +18 -2
  245. package/tests/hall_street_amend.mjs +1 -1
  246. package/tests/hall_street_deck.mjs +609 -0
  247. package/tests/hall_street_depth.mjs +559 -0
  248. package/tests/hall_street_desk.mjs +1 -1
  249. package/tests/hall_street_inside.mjs +29 -5
  250. package/tests/hall_tweak_editor.mjs +4 -1
  251. package/tests/hub_account_page.mjs +94 -3
  252. package/tests/hub_connections_page.mjs +507 -0
  253. package/tests/hub_invite_badge.mjs +1 -1
  254. package/tests/idp_device_exchange.mjs +18 -0
  255. package/tests/landing_legal_pages.mjs +8 -5
  256. package/tests/landing_page.mjs +23 -4
  257. package/tests/nav_permission_atoms.mjs +4 -1
  258. package/tests/profile_connections_ui.mjs +88 -25
  259. package/tests/projects_hub.mjs +15 -0
  260. package/tests/render_check.mjs +2 -2
  261. package/tests/ship_broadcast_routing.mjs +57 -16
  262. package/tests/ship_broadcast_server.mjs +40 -7
  263. package/tests/ship_merge_gate.mjs +19 -0
  264. package/tests/ui_design_kit.mjs +2 -2
  265. package/tests/watch_applications_queue.mjs +23 -0
@@ -0,0 +1,1002 @@
1
+ // modules/hall-ui/public/gov-living.js — THE LIVING CITY's model: the government
2
+ // that steers a project's AI, as data (task 1004685, the living-city prototype).
3
+ //
4
+ // WHAT THIS IS. city-draw.js draws the city from the docket and the goals; this
5
+ // file is the layer under the prototype's next drawings, which make the city
6
+ // "live and breathe". It reads every real governor feed (the harness snapshots
7
+ // them in scripts/hall-preview/fixtures) and returns one model the visual phases
8
+ // draw from. ALL OF IT IS REAL (task 1004717): the prototype overlay that once
9
+ // stood in for Leads, laws, pots, presets, build-outs and the human rewrite of
10
+ // each ask is deleted, so a real project's city is complete on its own reads. A
11
+ // concept no table holds (a Lead's deputies, a committee's members) is not drawn,
12
+ // and a matter with no stored ask keeps its own words.
13
+ //
14
+ // livingModel({ docket, constitution, goals, rollups, blockers, fence, claims,
15
+ // tasks, moving, sessions, learnings, cost, roster, me, laws, leads, presets,
16
+ // treasury, counts, now })
17
+ // → { viewer, owner, strictness, buildings[9], districts[], city, myAgents, _sources }
18
+ //
19
+ // countsOf(model, session) → { on_you, on_others, open, agents_at_work, moving }
20
+ // over the server's own counts (GET /government/counts, task 1004716) when read
21
+ // the ONE definition of every number the city, the street, the deck and the
22
+ // shell's Governing tally print (gov-feeds.js hands it to each of them);
23
+ // tests/hall_gov_counts.mjs renders each surface and holds them equal.
24
+ //
25
+ // Every input takes the route's own body or the array inside it, and every one
26
+ // may be missing: a missing feed leaves its part of the model empty, never
27
+ // invented. `now` is ms since the epoch, passed in (ages, today's spend).
28
+ //
29
+ // WHERE EACH FIELD COMES FROM. Each object carries `_sources`, mapping a field
30
+ // (or a dotted sub-field, `counts.building`) to:
31
+ // 'real' copied from a live read (an unread feed leaves it empty, never invented);
32
+ // 'derived' computed here from live reads only, by a rule written in this file;
33
+ // 'mixed' holds objects that carry their own `_sources` (read those).
34
+ // There is no other label. tests/hall_gov_living.mjs walks the whole model to
35
+ // hold it to that, and proves an `overlay` passed in changes nothing.
36
+ //
37
+ // ACTIVITY (0..1). An agent at work is an ACTIVE CLAIM (GET /tasks/claimable
38
+ // active_claims). A claim belongs to the building its work answers to:
39
+ // 1. its task's goal (GET /tasks?status=active gives task → goal), through
40
+ // goalBuilding(): the maintenance goal → Release; "Working area N" → the
41
+ // AREA_BUILDING table; an Autobongos goal → Automation; else the goal's
42
+ // category (CATEGORY_BUILDING);
43
+ // 2. failing that, the modules its claim touches (MODULE_BUILDING), by majority;
44
+ // 3. failing both, it is traffic with no building (`city.traffic.unplaced`).
45
+ // A building's activity is claims there / ACTIVITY_FULL (3 agents is a busy
46
+ // building), capped at 1. A district's is the larger of its claims and its
47
+ // rollup's `active` count over ACTIVITY_FULL, and at least RECENT_FLOOR when its
48
+ // rollup moved within the last day. `lit` is how lit the windows are: a base glow,
49
+ // plus activity, plus a lamp when something there needs a person.
50
+ //
51
+ // THE ASK FORMAT. askFormat(card) is the enforced shape of a question to a human
52
+ // (one question of at most 12 words, 2-3 tap options each with a one-line result,
53
+ // a recommendation and why, who it waits on, what happens if nobody answers);
54
+ // slopScore(text) measures the run-on text it replaces. A card that fails the
55
+ // format is REFUSED: the model keeps it, marks `format.ok: false`, and sets
56
+ // `shown: null` so a board shows the refusal, never the slop.
57
+ // THE SERVER HOLDS THE SAME RULES (task 1004703): modules/government/asks.js
58
+ // checkAsk refuses a slop ask before it is stored, and a matter that carries a
59
+ // stored ask hands it here as `ask`; that card is real (`ask_real`). A matter
60
+ // with no stored ask has no card: it shows in its own words (`shown: null`).
61
+ // tests/government_asks.mjs holds both copies to one verdict.
62
+ //
63
+ // THE TREASURY (task 1004712): GET /government/treasury says which goals have a
64
+ // pot (a budget the Treasury's holder gave it), what each spent this month
65
+ // (cost_log through the goal's tasks), what the reserve lent it, the reserve, and
66
+ // the spend cap (off by default: no limit). A dry pot's borrow is a real docket
67
+ // matter (treasury_spending:<id>) whose stored ask waits on the Treasury.
68
+ //
69
+ // THE LEADS (task 1004709): GET /government/leads says who leads each building
70
+ // (appointed by a passed People matter) and who holds the owner seat an unseated
71
+ // building falls to; every building's Lead, holder and charter come from it.
72
+ // The same read carries `build_rights` (task 1004710): what each building's
73
+ // holder may build out without a vote, decided on the server from the form of
74
+ // government; this file holds no rule of its own.
75
+ // A LEAD'S BUILD-OUTS (task 1004711): the same read carries each building's
76
+ // `structure` — committee rooms, floors, annexes and districts on the record
77
+ // (going up for a day, then built) and those waiting on a vote. A building's
78
+ // construction is drawn from it (a live site at its real progress, a ghost whose
79
+ // tally is its sitting's on the docket) and its committee rooms are its real ones.
80
+ // WHO AN ASK WAITS ON IS REAL (task 1004705, the direction desk): the server
81
+ // routes every docket matter to a seat (modules/government/routing.js — its
82
+ // stored ask's seat, else its building's Lead or its board; an unseated building
83
+ // falls to the owner) and the docket carries the answer as `route` with
84
+ // `routed: true`. Then `needs_you` is the route's `yours`, `waits_on` is the
85
+ // route's seat, and an ask nobody can answer (past its deadline, or on an empty
86
+ // seat) is settled with its `nobody` line: it does what its "if nobody answers"
87
+ // says and is on nobody's "to do". A docket that is not routed falls back to
88
+ // the seat the real Leads resolve, else the docket's own per-viewer word.
89
+ //
90
+ // PURE. No DOM, no fetch, no clock (the test reads this file to hold it to that).
91
+
92
+ (function (root) {
93
+ 'use strict';
94
+
95
+ const R = 'real';
96
+ const D = 'derived';
97
+ // a field holding objects that carry their own _sources (read those instead)
98
+ const M = 'mixed';
99
+
100
+ // ---------- the nine buildings (the docket's vocabulary order) ----------
101
+ const BUILDING_KEYS = Object.freeze(['charter', 'rules', 'disputes', 'people', 'planning', 'automation', 'review', 'release', 'treasury']);
102
+ const BUILDING_NAMES = Object.freeze({
103
+ charter: 'Charter', rules: 'Rules', disputes: 'Disputes', people: 'People', planning: 'Planning',
104
+ automation: 'Automation', review: 'Review', release: 'Release', treasury: 'Treasury',
105
+ });
106
+
107
+ // ---------- where work answers to (the activity mapping) ----------
108
+ // A working area (goals.working_area, or "Working area N" in its title) → the
109
+ // building whose Lead it reports to.
110
+ const AREA_BUILDING = Object.freeze({
111
+ 1: 'people', // project creation: founders and newcomers arrive
112
+ 2: 'people', // accounts, management & community
113
+ 3: 'planning', // human project management
114
+ 4: 'release', // core distribution
115
+ 5: 'release', // module distribution
116
+ 6: 'review', // the role experiences, checked by people
117
+ 7: 'charter', // government itself
118
+ 8: 'treasury', // platform credit economy
119
+ 9: 'planning', // analytics: the flow and promises readings
120
+ 10: 'disputes', // security reports
121
+ 11: 'rules',
122
+ 12: 'treasury', // cost optimisation
123
+ });
124
+ const CATEGORY_BUILDING = Object.freeze({
125
+ government: 'charter', economics: 'treasury', security: 'disputes', modules: 'release',
126
+ 'builders-hall': 'planning', 'user-experience': 'review',
127
+ });
128
+ const MODULE_BUILDING = Object.freeze({
129
+ government: 'charter', ideas: 'planning', lifecycle: 'planning', grading: 'review', 'copy-desk': 'review',
130
+ 'ui-design': 'review', 'hall-ui': 'review', 'npm-release': 'release', provisioning: 'release',
131
+ economy: 'treasury', autonomy: 'automation', 'platform-identity': 'people', onboarding: 'people',
132
+ 'builder-settings': 'people', security: 'disputes',
133
+ });
134
+ const ACTIVITY_FULL = 3;
135
+ const RECENT_FLOOR = 0.2;
136
+ const DAY_MS = 24 * 3600 * 1000;
137
+ const URGENT_MS = DAY_MS;
138
+
139
+ // ---------- small helpers ----------
140
+ const arr = (v, key) => (Array.isArray(v) ? v : v && Array.isArray(v[key]) ? v[key] : []);
141
+ const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : null);
142
+ const str = (v) => (v == null ? '' : String(v));
143
+ const clamp01 = (n) => Math.max(0, Math.min(1, Number(n) || 0));
144
+ const round2 = (n) => Math.round((Number(n) || 0) * 100) / 100;
145
+ const round3 = (n) => Math.round((Number(n) || 0) * 1000) / 1000;
146
+ const time = (iso) => { const t = iso ? Date.parse(iso) : NaN; return isFinite(t) ? t : null; };
147
+
148
+ // ---------- the ask format ----------
149
+ const ASK_RULES = Object.freeze({ question_words: 12, options_min: 2, options_max: 3, label_words: 4, line_words: 14, why_words: 20 });
150
+ const MARKUP = /\*\*|`|^#|\[[^\]]*\]\(|https?:\/\/|<\/?[a-z]/im;
151
+
152
+ function words(text) { return str(text).trim().split(/\s+/).filter(Boolean); }
153
+ // Sentences end at . ! ? followed by a space and a capital (or a digit, a quote,
154
+ // a bracket), or by the end; a version (1.20.82) or a decimal does not end one.
155
+ function sentences(text) {
156
+ const t = str(text).replace(/\s+/g, ' ').trim();
157
+ const out = [];
158
+ let start = 0;
159
+ for (let i = 0; i < t.length; i++) {
160
+ const ch = t[i];
161
+ if (ch !== '.' && ch !== '!' && ch !== '?') continue;
162
+ const next = t[i + 1];
163
+ if (next !== undefined && next !== ' ') continue;
164
+ const after = t[i + 2];
165
+ if (next === ' ' && after !== undefined && !/[A-Z0-9"'(*[]/.test(after)) continue;
166
+ out.push(t.slice(start, i + 1).trim());
167
+ start = i + 2;
168
+ }
169
+ if (start < t.length) out.push(t.slice(start).trim());
170
+ return out.filter((s) => /[A-Za-z0-9]/.test(s));
171
+ }
172
+
173
+ // askFormat(card) → { ok, errors: [{ field, rule, message }], question_words }
174
+ function askFormat(card) {
175
+ const errors = [];
176
+ const err = (field, rule, message) => errors.push({ field, rule, message });
177
+ const c = obj(card);
178
+ if (!c) {
179
+ err('card', 'missing', 'No human card: the ask is still in its raw form.');
180
+ return { ok: false, errors, question_words: 0 };
181
+ }
182
+ const q = str(c.question).trim();
183
+ const qWords = words(q).length;
184
+ if (!q) err('question', 'missing', 'Ask one question.');
185
+ else {
186
+ if (qWords > ASK_RULES.question_words) err('question', 'too_long', `The question is ${qWords} words; ${ASK_RULES.question_words} at most.`);
187
+ if (!/\?$/.test(q)) err('question', 'not_a_question', 'End the question with a question mark.');
188
+ if (sentences(q).length > 1 || /\n/.test(q)) err('question', 'more_than_one', 'Ask exactly one question.');
189
+ if (MARKUP.test(q)) err('question', 'markup', 'No markup or links in the question.');
190
+ }
191
+ const opts = Array.isArray(c.options) ? c.options : [];
192
+ if (opts.length < ASK_RULES.options_min || opts.length > ASK_RULES.options_max) {
193
+ err('options', 'count', `Offer ${ASK_RULES.options_min} or ${ASK_RULES.options_max} tap answers, not ${opts.length}.`);
194
+ }
195
+ const keys = new Set();
196
+ opts.forEach((o, i) => {
197
+ const f = `options[${i}]`;
198
+ if (!obj(o)) { err(f, 'missing', 'An answer needs a key, a label and a result.'); return; }
199
+ const k = str(o.key).trim();
200
+ if (!k) err(f, 'no_key', 'An answer needs a key.');
201
+ else if (keys.has(k)) err(f, 'duplicate', `Two answers share the key "${k}".`);
202
+ keys.add(k);
203
+ const lw = words(o.label).length;
204
+ if (!lw) err(f, 'no_label', 'An answer needs a label to tap.');
205
+ else if (lw > ASK_RULES.label_words) err(f, 'label_too_long', `"${str(o.label)}" is ${lw} words; a tap label is ${ASK_RULES.label_words} at most.`);
206
+ lineRule(f + '.result', o.result, 'Say in one line what this answer does.');
207
+ });
208
+ const rec = obj(c.recommend);
209
+ if (!rec) err('recommend', 'missing', 'Recommend an answer and say why.');
210
+ else {
211
+ if (!keys.has(str(rec.key))) err('recommend', 'unknown_key', 'Recommend one of the answers offered.');
212
+ const ww = words(rec.why).length;
213
+ if (!ww) err('recommend', 'no_why', 'Say why in one line.');
214
+ else if (ww > ASK_RULES.why_words || /\n/.test(str(rec.why))) err('recommend', 'why_too_long', `The why is ${ww} words; ${ASK_RULES.why_words} at most, one line.`);
215
+ }
216
+ if (!str(c.waits_on).trim()) err('waits_on', 'missing', 'Say who this waits on.');
217
+ lineRule('if_nobody_answers', c.if_nobody_answers, 'Say what happens if nobody answers.');
218
+ return { ok: errors.length === 0, errors, question_words: qWords };
219
+
220
+ function lineRule(field, text, missingMessage) {
221
+ const t = str(text).trim();
222
+ const n = words(t).length;
223
+ if (!n) err(field, 'missing', missingMessage);
224
+ else if (/\n/.test(t) || n > ASK_RULES.line_words) err(field, 'not_one_line', `${n} words; one line of ${ASK_RULES.line_words} at most.`);
225
+ else if (MARKUP.test(t)) err(field, 'markup', 'No markup or links.');
226
+ }
227
+ }
228
+
229
+ // slopScore(text) → how far a text is from something a person can answer at a
230
+ // glance. `slop` is true past any one threshold; `score` (0..1) ranks them.
231
+ const SLOP = Object.freeze({ words: 30, sentences: 2, longest: 20 });
232
+ function slopScore(text) {
233
+ const t = str(text);
234
+ const w = words(t).length;
235
+ const ss = sentences(t);
236
+ const longest = ss.reduce((m, s) => Math.max(m, words(s).length), 0);
237
+ const lines = t.trim() ? t.trim().split(/\n+/).length : 0;
238
+ const markup = (t.match(/\*\*|`+|^#+\s|\[[^\]]*\]\([^)]*\)|https?:\/\/\S+|^\s*[-*]\s/gm) || []).length;
239
+ const why = [];
240
+ if (w > SLOP.words) why.push(`${w} words`);
241
+ if (ss.length > SLOP.sentences) why.push(`${ss.length} sentences`);
242
+ if (longest > SLOP.longest) why.push(`a ${longest}-word sentence`);
243
+ if (markup) why.push(`${markup} bits of markup`);
244
+ const score = round3(Math.max(Math.min(1, w / 120), Math.min(1, Math.max(0, ss.length - 1) / 6), Math.min(1, longest / 50), Math.min(1, markup / 6)));
245
+ return { words: w, sentences: ss.length, longest_sentence: longest, lines, markup, score, slop: why.length > 0, why };
246
+ }
247
+
248
+ // ---------- people ----------
249
+ function peopleIndex(roster, docket) {
250
+ const byId = {};
251
+ arr(roster, 'builders').forEach((b) => {
252
+ if (b && b.id != null) byId[String(b.id)] = { id: String(b.id), login: str(b.github_login), name: str(b.display_name || b.github_login) };
253
+ });
254
+ arr(docket && docket.buildings).forEach((b) => arr(b && b.holders).forEach((h) => {
255
+ if (h && h.id != null && !byId[String(h.id)]) byId[String(h.id)] = { id: String(h.id), login: str(h.login), name: str(h.name || h.login) };
256
+ }));
257
+ return (id) => (id == null ? null : byId[String(id)] || { id: String(id), login: '', name: '' });
258
+ }
259
+
260
+ // ---------- where work answers to ----------
261
+ function goalBuilding(goal) {
262
+ const g = obj(goal);
263
+ if (!g) return null;
264
+ if (g.is_maintenance) return 'release';
265
+ const area = Number(g.working_area) || Number((/working area\s+(\d+)/i.exec(str(g.title)) || [])[1]) || null;
266
+ if (area && AREA_BUILDING[area]) return AREA_BUILDING[area];
267
+ if (/autobongos|unattended/i.test(str(g.title))) return 'automation';
268
+ const slug = g.category && g.category.slug;
269
+ return (slug && CATEGORY_BUILDING[slug]) || null;
270
+ }
271
+ function touchesBuilding(touches) {
272
+ const tally = {};
273
+ arr(touches).forEach((t) => {
274
+ const m = /^modules\/([^/]+)\//.exec(str(t));
275
+ const b = m && MODULE_BUILDING[m[1]];
276
+ if (b) tally[b] = (tally[b] || 0) + 1;
277
+ });
278
+ const best = Object.keys(tally).sort((a, b) => tally[b] - tally[a] || BUILDING_KEYS.indexOf(a) - BUILDING_KEYS.indexOf(b))[0];
279
+ return best || null;
280
+ }
281
+
282
+ // ---------- seats ----------
283
+ // A seat names who an ask waits on: 'owner', 'lead:<building>' (an unseated
284
+ // building falls to the owner), 'committee:<building>.<key>', 'board:<building>'.
285
+ function seatResolver(ctx) {
286
+ return function resolve(seat, building) {
287
+ const s = str(seat) || (building ? 'lead:' + building : 'owner');
288
+ let m;
289
+ // the owner seat's members: every one GET /government/leads names (none unread)
290
+ const ownerIds = ctx.ownerIds || [];
291
+ if (s === 'owner') return { seat: s, label: 'the owner', person: ctx.owner, members: ownerIds };
292
+ if ((m = /^lead:(\w+)$/.exec(s))) {
293
+ const lead = ctx.leads[m[1]];
294
+ if (lead && lead.person) return { seat: s, label: `the ${BUILDING_NAMES[m[1]] || m[1]} Lead`, person: lead.person, members: [lead.person.id] };
295
+ return { seat: s, label: 'the owner', person: ctx.owner, members: ownerIds, fell_to_owner: true };
296
+ }
297
+ if ((m = /^committee:(\w+)\.(\w+)$/.exec(s))) {
298
+ const lead = ctx.leads[m[1]];
299
+ const com = lead && lead.committees.find((c) => c.key === m[2]);
300
+ if (com) return { seat: s, label: `${com.name} (${BUILDING_NAMES[m[1]] || m[1]})`, person: lead.person, members: com.members.map((p) => p.id) };
301
+ return resolve('lead:' + m[1], m[1]);
302
+ }
303
+ if ((m = /^board:(\w+)$/.exec(s))) {
304
+ const holders = ctx.holders[m[1]] || [];
305
+ return { seat: s, label: 'the board', person: null, members: holders.map((p) => p.id) };
306
+ }
307
+ return { seat: s, label: s, person: null, members: [] };
308
+ };
309
+ }
310
+
311
+ // Who answers for a building, as GET /government/leads says: its Lead's seat, or
312
+ // the owner's when it is unseated (task 1004709). { seat, ids, fell_to_owner }
313
+ function answersOf(read, key) {
314
+ const b = arr(read.buildings).find((x) => x && x.building === key);
315
+ const a = b && obj(b.answers);
316
+ return a ? { seat: str(a.seat), ids: arr(a.builder_ids).map(str), fell_to_owner: !!a.fell_to_owner } : null;
317
+ }
318
+
319
+ // What a building's holder may build out without a vote (task 1004710). The
320
+ // SERVER decides it (modules/government/build-rights.js: the form of government
321
+ // and the building's own decision rule, never a Lead's charter) and GET
322
+ // /government/leads carries it per building; this only reads the answer, one
323
+ // capsule at a time. { form, form_name, holder, capsules: { key: { without_vote, why, by } } } or null.
324
+ const CAPSULE_KEYS = Object.freeze(['committee', 'floor', 'annex', 'district']);
325
+ function rightsShape(r) {
326
+ if (!obj(r) || !obj(r.capsules)) return null;
327
+ const capsules = {};
328
+ CAPSULE_KEYS.forEach((k) => {
329
+ const c = obj(r.capsules[k]) ? r.capsules[k] : null;
330
+ capsules[k] = { without_vote: !!(c && c.without_vote === true), why: str(c && c.why), ground: str(c && c.ground) };
331
+ });
332
+ return { form: str(r.form), form_name: str(r.form_name), holder: str(r.holder), capsules };
333
+ }
334
+ function rightsOf(read, key) {
335
+ const b = arr(read.buildings).find((x) => x && x.building === key);
336
+ return b ? rightsShape(b.build_rights) : null;
337
+ }
338
+
339
+ // ---------- one ask card ----------
340
+ const STATE_ORDER = Object.freeze({ needs: 1, moving: 2, settled: 3 });
341
+ function makeAsk(base, human, ctx, sources) {
342
+ const opened = time(base.opened_at);
343
+ const deadline = time(base.deadline);
344
+ const format = askFormat(human);
345
+ // the server's route (task 1004705), when the docket was routed
346
+ const served = ctx.routed ? obj(base.route) : null;
347
+ const route = served
348
+ ? { seat: str(served.seat), label: str(served.label), person: null, fell_to_owner: !!served.fell_to_owner, members: [] }
349
+ : ctx.resolveSeat(human && human.waits_on, base.building);
350
+ const nobody = served && obj(served.nobody) ? { why: str(served.nobody.why), outcome: served.nobody.outcome == null ? null : str(served.nobody.outcome) } : null;
351
+ // an ask nobody answers has done what its "if nobody answers" line says
352
+ const state = nobody ? 'settled' : STATE_ORDER[base.state] ? base.state : 'needs';
353
+ // Routed: the route's word. A matter the routed docket does not carry is
354
+ // nobody's here. Not routed: whether the viewer holds the seat the real Leads
355
+ // resolve (GET /government/leads read), else the docket's own per-viewer 'needs'.
356
+ const needsYou = state === 'needs' && !!ctx.viewerId && (served ? !!served.yours && ctx.routedFor === ctx.viewerId
357
+ : ctx.routed ? false
358
+ : (route.members.indexOf(ctx.viewerId) >= 0 || (!ctx.seated && !!base.from_docket)));
359
+ const rawText = str(base.raw_title) + (base.raw_detail ? '\n\n' + str(base.raw_detail) : '');
360
+ return {
361
+ id: str(base.id),
362
+ source: str(base.source),
363
+ building: base.building,
364
+ state,
365
+ urgent: state === 'needs' && deadline != null && ctx.now != null && deadline - ctx.now <= URGENT_MS,
366
+ opened_at: base.opened_at || null,
367
+ deadline: base.deadline || null,
368
+ age_days: opened != null && ctx.now != null ? Math.max(0, Math.floor((ctx.now - opened) / DAY_MS)) : null,
369
+ rule: base.rule || null,
370
+ ref: base.ref || null,
371
+ href: base.href || null,
372
+ acts: base.acts || [],
373
+ tally: base.tally || null,
374
+ raw: { title: str(base.raw_title), detail: str(base.raw_detail), slop: slopScore(rawText) },
375
+ human: human || null,
376
+ format,
377
+ shown: format.ok ? human : null,
378
+ waits_on: { seat: route.seat, label: route.label, person: route.person || null, fell_to_owner: !!route.fell_to_owner, real: !!served },
379
+ needs_you: needsYou,
380
+ nobody,
381
+ // the card's words are the agent's own stored ask (task 1004703)
382
+ ask_real: !!base.ask_real,
383
+ _sources: served ? Object.assign({}, sources, { state: D, waits_on: R, needs_you: D, nobody: R }) : Object.assign({}, sources, { nobody: D }),
384
+ };
385
+ }
386
+
387
+ // Needs-you first (needs that wait on the viewer's seat), then the rest that
388
+ // need a person, then moving, then settled; inside a tier the oldest first (an
389
+ // ask with no date after the dated ones), then by id.
390
+ function askTier(a) { return a.needs_you ? 0 : STATE_ORDER[a.state]; }
391
+ function compareAsks(a, b) {
392
+ const t = askTier(a) - askTier(b);
393
+ if (t) return t;
394
+ const ta = time(a.opened_at);
395
+ const tb = time(b.opened_at);
396
+ if (ta != null && tb != null && ta !== tb) return ta - tb;
397
+ if ((ta == null) !== (tb == null)) return ta == null ? 1 : -1;
398
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
399
+ }
400
+
401
+ // ---------- the counts every surface prints (one source) ----------
402
+ // The city's plaque, door and line, the street's header and readings, the
403
+ // decision deck and the shell's Governing tally all print these numbers, so
404
+ // each is defined once, here, with one plain label:
405
+ // on_you asks that need a person and wait on one of the viewer's seats
406
+ // on_others asks that need a person and wait on someone else's seat
407
+ // open on_you + on_others: what the deck holds
408
+ // agents_at_work active claims, one sky pod each ("5 agents working": a tile is
409
+ // one number and two words, the owner's line rule)
410
+ // moving tasks being built or waiting to land (GET /moving: active +
411
+ // confirmed, owner 2026-10-08); never below agents_at_work,
412
+ // since every active claim holds an active task
413
+ // `session` { answered: ids answered or passed on in this page view, extra: asks
414
+ // a building added this visit } is the page's own state, applied the same way
415
+ // wherever the numbers are printed. A figure that could not be read is null,
416
+ // never 0: zero says nothing waits on you.
417
+ // THE LABELS. The main count is "to do" (the owner's word, 2026-10-09; task
418
+ // 1004701): after a number "24 to do", and a heading that names the count (the
419
+ // street's header, the tally) is countLabel(): "to do 24". A desk's own floor
420
+ // line keeps "needs you": that is one matter, not the count.
421
+ const COUNT_WORDS = Object.freeze({
422
+ on_you: Object.freeze(['to do', 'to do']),
423
+ on_others: Object.freeze(['waits on others', 'wait on others']),
424
+ open: Object.freeze(['to decide', 'to decide']),
425
+ agents_at_work: Object.freeze(['agent working', 'agents working']),
426
+ moving: Object.freeze(['moving', 'moving']),
427
+ });
428
+ const COUNT_LABELS = Object.freeze({ on_you: 'to do', on_others: 'on others', open: 'to decide', agents_at_work: 'agents working', moving: 'moving' });
429
+ function countWords(key, n) { const w = COUNT_WORDS[key]; return w ? w[n === 1 ? 0 : 1] : ''; }
430
+ function countLabel(key) { return COUNT_LABELS[key] || ''; }
431
+ function isOnYou(a) { return !!a && a.state === 'needs' && !!a.needs_you; }
432
+ function isOnOthers(a) { return !!a && a.state === 'needs' && !a.needs_you; }
433
+ // THE SERVER'S COUNTS (task 1004716): GET /government/counts answers all five
434
+ // numbers in one read, over the docket's own routes (modules/government/
435
+ // counts.js), and names the matters on you and on others. Read, they ARE the
436
+ // real part of every count: this page takes away what it answered this visit
437
+ // and adds only what no server read holds yet (an ask a building added this
438
+ // visit). Not read, the counts are made here from the docket and the feeds, as
439
+ // before; tests/government_counts.mjs holds the two equal.
440
+ function servedCounts(c) {
441
+ if (!obj(c) || !Array.isArray(c.yours) || !Array.isArray(c.others)) return null;
442
+ const n = (v) => (typeof v === 'number' && isFinite(v) && v >= 0 ? v : null);
443
+ return { yours: c.yours.map(str), others: c.others.map(str), agents_at_work: n(c.agents_at_work), moving: n(c.moving) };
444
+ }
445
+ function countsOf(model, session) {
446
+ const city = model && obj(model.city) ? model.city : null;
447
+ const s = obj(session) || {};
448
+ const gone = {};
449
+ arr(s.answered).forEach((id) => { gone[str(id)] = true; });
450
+ const seen = {};
451
+ const extra = {};
452
+ arr(s.extra).forEach((a) => { if (a && a.id != null) extra[str(a.id)] = true; });
453
+ const asks = (city ? arr(city.asks) : []).concat(arr(s.extra)).filter((a) => {
454
+ if (!a || a.id == null || seen[a.id] || gone[str(a.id)]) return false;
455
+ seen[a.id] = true;
456
+ return true;
457
+ });
458
+ const served = city && obj(city.served) ? city.served : null;
459
+ const t = city && obj(city.traffic) ? city.traffic : null;
460
+ if (served) {
461
+ const held = {};
462
+ served.yours.concat(served.others).forEach((id) => { held[id] = true; });
463
+ const own = asks.filter((a) => !held[str(a.id)] && extra[str(a.id)]);
464
+ const onYou = served.yours.filter((id) => !gone[id]).length + own.filter(isOnYou).length;
465
+ const onOthers = served.others.filter((id) => !gone[id]).length + own.filter(isOnOthers).length;
466
+ const src = R;
467
+ return {
468
+ on_you: onYou,
469
+ on_others: onOthers,
470
+ open: onYou + onOthers,
471
+ agents_at_work: served.agents_at_work != null ? served.agents_at_work : t && t.read ? Number(t.agents) || 0 : null,
472
+ moving: served.moving != null ? served.moving : t && t.moving != null ? Number(t.moving) : null,
473
+ _sources: { on_you: src, on_others: src, open: src, agents_at_work: R, moving: R },
474
+ };
475
+ }
476
+ // the asks stand on the docket: without it there is nothing honest to count
477
+ const read = !!city && !!city.docket_read;
478
+ const onYou = read ? asks.filter(isOnYou).length : null;
479
+ const onOthers = read ? asks.filter(isOnOthers).length : null;
480
+ return {
481
+ on_you: onYou,
482
+ on_others: onOthers,
483
+ open: read ? onYou + onOthers : null,
484
+ agents_at_work: t && t.read ? Number(t.agents) || 0 : null,
485
+ moving: t && t.moving != null ? Number(t.moving) : null,
486
+ _sources: { on_you: D, on_others: D, open: D, agents_at_work: D, moving: R },
487
+ };
488
+ }
489
+
490
+ // ---------- my agents ----------
491
+ function agentSettings(me) {
492
+ const m = obj(me) || {};
493
+ const out = [];
494
+ const inter = obj(m.interaction);
495
+ if (inter) {
496
+ arr(inter.fields).forEach((f) => {
497
+ const eff = obj(inter.effective) || {};
498
+ out.push({
499
+ key: str(f.field), label: str(f.label), group: 'How it talks to you', source: 'me/interaction',
500
+ options: arr(f.values).map((v) => v.value), default: f.default_value,
501
+ value: eff[f.field] !== undefined ? eff[f.field] : f.default_value, real: true,
502
+ });
503
+ });
504
+ }
505
+ const wand = obj(m.wandering);
506
+ if (wand) {
507
+ out.push({
508
+ key: 'wandering', label: 'How far agents wander', group: 'How your agents work', source: 'me/wandering',
509
+ options: arr(wand.levels).filter((l) => l.allowed !== false).map((l) => l.key), default: wand.default_key,
510
+ value: wand.effective_key || wand.default_key, real: true,
511
+ });
512
+ }
513
+ const alloc = obj(m.model_allocation);
514
+ if (alloc) {
515
+ arr(alloc.slots).forEach((s) => {
516
+ const eff = obj(alloc.effective) || {};
517
+ out.push({
518
+ key: 'model.' + str(s.slot), label: str(s.label), group: 'Which models run', source: 'me/model-allocation',
519
+ options: arr(alloc.allowed_tiers).slice(), default: s.code_default,
520
+ value: eff[s.slot] !== undefined ? eff[s.slot] : s.code_default, real: true,
521
+ });
522
+ });
523
+ }
524
+ // THE LOCKS (task 1004707): GET /me's `laws.locks` — what each law in force
525
+ // did to THIS viewer's settings, enforced by the server. The slice's effective
526
+ // value already obeys it; `from` is the viewer's own. A lock the server could
527
+ // not enforce (a setting no table holds yet) locks nothing here: the row would
528
+ // claim an enforcement that is not happening.
529
+ const realLockFor = {};
530
+ arr(obj(m.laws) && m.laws.locks).forEach((l) => { if (l && l.enforced && l.setting && !realLockFor[l.setting]) realLockFor[l.setting] = l; });
531
+ return out.map((s) => {
532
+ const real = realLockFor[s.key] || null;
533
+ // A CAP (task 1004714): a law that only caps a setting ({ setting, max })
534
+ // leaves every value up to the cap the viewer's to pick. The server names
535
+ // them (`allowed`, law-locks.js); the row offers those and is not locked. A
536
+ // cap that leaves one value or fewer is a lock in all but name.
537
+ const capAllowed = real && real.max !== undefined && Array.isArray(real.allowed)
538
+ ? s.options.filter((v) => real.allowed.some((a) => String(a) === String(v))) : null;
539
+ if (capAllowed && capAllowed.length > 1) {
540
+ return {
541
+ key: s.key, label: s.label, group: s.group, options: capAllowed, default: s.default,
542
+ value: real.from != null ? real.from : s.value, effective: s.value, locked: false, lockedBy: null, advisedBy: null,
543
+ cappedBy: { law_id: 'law:' + str(real.law_id), no: Number(real.law_no) || null, text: str(real.law_text), max: real.max, real: true },
544
+ _sources: { key: R, label: R, options: R, default: R, value: R, effective: R, locked: R, lockedBy: R, advisedBy: D, cappedBy: R },
545
+ };
546
+ }
547
+ if (real) {
548
+ return {
549
+ key: s.key, label: s.label, group: s.group, options: s.options, default: s.default,
550
+ value: real.from != null ? real.from : s.value, effective: s.value, locked: true,
551
+ lockedBy: { law_id: 'law:' + str(real.law_id), no: Number(real.law_no) || null, text: str(real.law_text), real: true }, advisedBy: null,
552
+ _sources: { key: R, label: R, options: R, default: R, value: R, effective: R, locked: R, lockedBy: R, advisedBy: D },
553
+ };
554
+ }
555
+ // no law in force touches it: the viewer's own, free to change
556
+ return {
557
+ key: s.key, label: s.label, group: s.group, options: s.options, default: s.default,
558
+ value: s.value, effective: s.value, locked: false, lockedBy: null, advisedBy: null,
559
+ _sources: { key: R, label: R, options: R, default: R, value: R, effective: R, locked: D, lockedBy: D, advisedBy: D },
560
+ };
561
+ });
562
+ }
563
+
564
+ // ---------- the whole model ----------
565
+ function livingModel(input) {
566
+ const o = obj(input) || {};
567
+ const now = Number(o.now) > 0 ? Number(o.now) : null;
568
+ const docket = obj(o.docket) && Array.isArray(o.docket.matters) ? o.docket : null;
569
+ const goalsAll = arr(o.goals, 'goals');
570
+ const rollups = {};
571
+ arr(o.rollups, 'rollups').forEach((r) => { if (r && r.goal_id != null) rollups[String(r.goal_id)] = r; });
572
+ const claims = arr(o.claims, 'active_claims');
573
+ // whether the claims feed was read at all: no read is "unknown", never "no agent"
574
+ const claimsRead = Array.isArray(o.claims) || !!(obj(o.claims) && Array.isArray(o.claims.active_claims));
575
+ const tasks = arr(o.tasks, 'tasks');
576
+ const person = peopleIndex(o.roster, docket);
577
+ const me = obj(o.me) ? (obj(o.me.builder) ? o.me : { builder: o.me }) : null;
578
+
579
+ // the viewer
580
+ const vb = me && me.builder;
581
+ const viewerId = vb && vb.id != null ? String(vb.id) : null;
582
+
583
+ // real holders per building (who may decide there today)
584
+ const holders = {};
585
+ const dBuilding = {};
586
+ arr(docket && docket.buildings).forEach((b) => {
587
+ if (!b || !b.key) return;
588
+ dBuilding[b.key] = b;
589
+ holders[b.key] = arr(b.holders).map((h) => person(h.id));
590
+ });
591
+
592
+ // THE LEADS (task 1004709): GET /government/leads — who leads each building and
593
+ // who holds the owner seat. Unread, no building names a Lead and no owner.
594
+ const leadsRead = obj(o.leads) && Array.isArray(o.leads.buildings) ? o.leads : null;
595
+ const who = (p) => ({ id: str(p.id), login: str(p.login), name: str(p.name || p.login) });
596
+ const ownerRow = leadsRead && obj(leadsRead.owner) ? arr(leadsRead.owner.builders)[0] : null;
597
+ const owner = obj(ownerRow) && ownerRow.id != null ? who(ownerRow) : null;
598
+ const leads = {};
599
+ if (leadsRead) {
600
+ leadsRead.buildings.forEach((b) => {
601
+ const l = b && obj(b.lead);
602
+ if (!l || BUILDING_KEYS.indexOf(b.building) < 0 || !obj(l.builder) || l.builder.id == null) return;
603
+ leads[b.building] = { person: who(l.builder), since: l.since || null, charter: { scope: str(l.charter) }, committees: [], real: true };
604
+ });
605
+ }
606
+ // THE STRUCTURE (task 1004711): the same read carries what each building has
607
+ // grown under its holder (`structure`: build-outs standing, built or going up,
608
+ // and the ones waiting on a vote). A building's committee rooms are its real
609
+ // ones (no table seats members in them yet) and its build-outs are drawn from it.
610
+ const structRead = {};
611
+ if (leadsRead) {
612
+ leadsRead.buildings.forEach((b) => {
613
+ if (!b || BUILDING_KEYS.indexOf(b.building) < 0 || !obj(b.structure)) return;
614
+ structRead[b.building] = { standing: arr(b.structure.standing).filter(obj), proposed: arr(b.structure.proposed).filter(obj) };
615
+ });
616
+ }
617
+ const realRooms = (key) => structRead[key].standing.filter((x) => x.capsule === 'committee')
618
+ .map((x) => ({ key: 'b' + str(x.id), name: str(x.name), line: str(x.line), members: [], real: true, state: x.state === 'built' ? 'built' : 'building' }));
619
+ const realBuilt = (key) => structRead[key].standing.filter((x) => x.capsule !== 'committee' && x.state === 'built')
620
+ .map((x) => ({ id: 'buildout:' + str(x.id), capsule: str(x.capsule), name: str(x.name), line: str(x.line) }));
621
+ Object.keys(leads).forEach((k) => { if (structRead[k]) leads[k] = Object.assign({}, leads[k], { committees: realRooms(k) }); });
622
+ // THE PRESETS (task 1004715): GET /government/presets. The project's strictness
623
+ // is the preset its laws in force amount to (null: none), and each preset
624
+ // carries the locks a switch to it would leave in force. Unread: unknown (null).
625
+ const presetsRead = obj(o.presets) && Array.isArray(o.presets.presets) ? o.presets : null;
626
+ const realPresets = presetsRead ? presetsRead.presets.filter((p) => obj(p) && p.key).map((p) => ({
627
+ key: str(p.key), name: str(p.name), line: str(p.line), locks: arr(p.locks).filter((l) => obj(l) && l.setting).map((l) => Object.assign({}, l)),
628
+ })) : null;
629
+ const realCurrent = realPresets ? realPresets.find((p) => p.key === presetsRead.current) || null : null;
630
+ const ownerIds = leadsRead && obj(leadsRead.owner) ? arr(leadsRead.owner.builders).filter((p) => obj(p) && p.id != null).map((p) => str(p.id)) : null;
631
+ const resolveSeat = seatResolver({ owner, ownerIds, leads, holders });
632
+ // routed: the server routed every docket matter (task 1004705)
633
+ // (a docket is read per viewer: its `yours` is only ever its own viewer's)
634
+ const routed = !!(docket && docket.routed === true);
635
+ const routedFor = routed && obj(docket.viewer) && docket.viewer.id != null ? String(docket.viewer.id) : null;
636
+ const ctx = { now, viewerId, resolveSeat, routed, routedFor, seated: !!leadsRead };
637
+
638
+ // ---- asks: every docket matter, and every open blocker it does not already carry ----
639
+ const humanOf = (a) => (a ? {
640
+ question: a.question, options: a.options, recommend: a.recommend, waits_on: a.waits_on,
641
+ if_nobody_answers: a.if_nobody_answers, routing_note: a.routing_note || null,
642
+ } : null);
643
+ // THE STORED ASK (task 1004703): a matter whose agent wrote its card in the ask
644
+ // shape carries it as `ask` (GET /government/docket, GET /blockers), checked by
645
+ // the server's own rules (modules/government/asks.js). Its words are real; a
646
+ // matter without one has no card (`human: null`) and shows in its own words.
647
+ // Who its seat names is the server's route when the docket is routed (makeAsk
648
+ // relabels waits_on and needs_you), else the seat the real Leads resolve.
649
+ const ASK_SOURCES = { id: R, source: R, building: R, state: R, urgent: D, opened_at: R, deadline: R, age_days: D, rule: R, ref: R, href: R, acts: R, tally: R, raw: R, human: R, format: D, shown: D, waits_on: D, needs_you: D, ask_real: D };
650
+ const storedOf = (a) => (obj(a) && str(a.question) ? a : null);
651
+ const asks = [];
652
+ const seen = {};
653
+ arr(docket && docket.matters).forEach((m) => {
654
+ if (!m || m.id == null || seen[m.id]) return;
655
+ seen[m.id] = true;
656
+ const building = BUILDING_KEYS.indexOf(m.building) >= 0 ? m.building : 'planning';
657
+ const stored = storedOf(m.ask);
658
+ asks.push(makeAsk({
659
+ id: m.id, source: m.source, building, state: m.state, opened_at: m.opened_at, deadline: m.deadline,
660
+ rule: m.rule, ref: m.ref, href: m.href, acts: m.acts, tally: m.tally, raw_title: m.title, raw_detail: m.detail, from_docket: true, ask_real: !!stored, route: m.route,
661
+ }, humanOf(stored), ctx, ASK_SOURCES));
662
+ });
663
+ arr(o.blockers, 'blockers').forEach((b) => {
664
+ if (!b || b.id == null) return;
665
+ const id = 'blockers:' + b.id;
666
+ if (seen[id] || (b.status && b.status !== 'open')) return;
667
+ seen[id] = true;
668
+ const stored = storedOf(b.ask);
669
+ asks.push(makeAsk({
670
+ id, source: 'blockers', building: 'planning', state: 'needs', opened_at: b.created_at, deadline: null,
671
+ rule: 'decide', ref: { type: 'blocker', id: String(b.id) }, href: '/blockers#blocker-' + b.id, acts: [], raw_title: b.title, raw_detail: b.body_md, ask_real: !!stored,
672
+ }, humanOf(stored), ctx, ASK_SOURCES));
673
+ });
674
+ asks.sort(compareAsks);
675
+
676
+ // ---- traffic: where the agents are ----
677
+ const goalById = {};
678
+ goalsAll.forEach((g) => { if (g && g.id != null) goalById[String(g.id)] = g; });
679
+ const taskById = {};
680
+ tasks.forEach((t) => { if (t && t.id != null) taskById[String(t.id)] = t; });
681
+ const placed = claims.map((c) => {
682
+ const t = taskById[String(c.task_id)];
683
+ const goalId = t && t.goal_id != null ? String(t.goal_id) : null;
684
+ const building = (goalId && goalBuilding(goalById[goalId])) || touchesBuilding(c.touches) || null;
685
+ return { claim_id: str(c.claim_id), task_id: str(c.task_id), goal_id: goalId, building, builder: person(c.builder_id), since: c.claimed_at || null };
686
+ });
687
+ const claimsAt = {};
688
+ const claimsInGoal = {};
689
+ placed.forEach((p) => {
690
+ if (p.building) claimsAt[p.building] = (claimsAt[p.building] || 0) + 1;
691
+ if (p.goal_id) claimsInGoal[p.goal_id] = (claimsInGoal[p.goal_id] || 0) + 1;
692
+ });
693
+
694
+ // ---- construction ----
695
+ const construction = {};
696
+ BUILDING_KEYS.forEach((k) => { construction[k] = []; });
697
+ // A LEAD'S BUILD-OUTS (task 1004711), from GET /government/leads `structure`:
698
+ // one going up is a live site (scaffold, crane) at its real progress — a day
699
+ // from when it started; one waiting on a vote is a ghost whose tally is its
700
+ // sitting's on the docket (`ask_id` is that docket id). First in the list, so
701
+ // the drawing shows the build-out ahead of any other open sitting there.
702
+ const buildoutVotes = {};
703
+ BUILDING_KEYS.forEach((k) => {
704
+ const s = structRead[k];
705
+ if (!s) return;
706
+ s.standing.filter((x) => x.state === 'building').forEach((x) => {
707
+ const allowed = obj(x.allowed) ? x.allowed : {};
708
+ construction[k].push({
709
+ kind: 'buildout', capsule: str(x.capsule), id: 'buildout:' + str(x.id), label: str(x.name), line: str(x.line),
710
+ authorised: true, authorised_by: allowed.path === 'vote' ? 'a passed vote' : str(allowed.why) || null, needs: null,
711
+ progress: clamp01(x.progress), sitting_id: allowed.item_id != null ? String(allowed.item_id) : null,
712
+ illustrative: false, driven_by_task: null, task_status: null, ask_id: null,
713
+ _sources: { kind: D, capsule: R, id: R, label: R, line: R, authorised: D, authorised_by: R, needs: D, progress: R, sitting_id: R, illustrative: D, driven_by_task: D, task_status: D, ask_id: D },
714
+ });
715
+ });
716
+ s.proposed.forEach((x) => {
717
+ const id = str(x.docket_id);
718
+ buildoutVotes[id] = true;
719
+ construction[k].push({
720
+ kind: 'scaffold', capsule: str(x.capsule), id, label: str(x.name), line: str(x.line),
721
+ authorised: false, authorised_by: null, needs: 'a vote',
722
+ progress: null, sitting_id: x.item_id != null ? String(x.item_id) : null,
723
+ illustrative: false, driven_by_task: null, task_status: null, ask_id: id,
724
+ _sources: { kind: D, capsule: R, id: R, label: R, line: R, authorised: D, authorised_by: D, needs: D, progress: D, sitting_id: R, illustrative: D, driven_by_task: D, task_status: D, ask_id: R },
725
+ });
726
+ });
727
+ });
728
+ arr(docket && docket.matters).forEach((m) => {
729
+ // a sitting still open is a proposal drawn into the city: a real scaffold
730
+ // (a build-out's own sitting is drawn above, as its ghost)
731
+ if (!m || m.source !== 'board' || m.state === 'settled' || BUILDING_KEYS.indexOf(m.building) < 0 || buildoutVotes[str(m.id)]) return;
732
+ construction[m.building].push({
733
+ kind: 'scaffold', id: str(m.id), label: str(m.short || m.title), line: str(m.title), authorised: false,
734
+ progress: null, sitting_id: m.ref && m.ref.id != null ? String(m.ref.id) : null, illustrative: false, task_status: null,
735
+ _sources: { kind: D, id: R, label: R, line: R, authorised: D, progress: D, sitting_id: R, illustrative: D, task_status: D },
736
+ });
737
+ });
738
+ BUILDING_KEYS.forEach((k) => {
739
+ const d = dBuilding[k];
740
+ if (d && d.decision && d.decision.sitting) {
741
+ construction[k].push({
742
+ kind: 'scaffold', id: 'decision:' + k, label: 'A new way to decide', line: str(d.decision.rule_words), authorised: false,
743
+ progress: null, sitting_id: d.decision.set_by_item_id != null ? String(d.decision.set_by_item_id) : null, illustrative: false, task_status: null,
744
+ _sources: { kind: D, id: D, label: D, line: R, authorised: D, progress: D, sitting_id: R, illustrative: D, task_status: D },
745
+ });
746
+ }
747
+ });
748
+
749
+ // ---- the nine buildings ----
750
+ const vocab = {};
751
+ arr(docket && docket.vocabulary && docket.vocabulary.buildings).forEach((v) => { if (v && v.key) vocab[v.key] = v; });
752
+ const readings = arr(docket && docket.readings);
753
+ const floors = arr(docket && docket.floors);
754
+ const buildings = BUILDING_KEYS.map((key) => {
755
+ const d = dBuilding[key] || {};
756
+ const v = vocab[key] || {};
757
+ const lead = leads[key] || null;
758
+ const here = asks.filter((a) => a.building === key);
759
+ const needs = here.filter((a) => a.state === 'needs').length;
760
+ const movingAsks = here.filter((a) => a.state === 'moving').length;
761
+ const agents = claimsAt[key] || 0;
762
+ const activity = round3(clamp01(agents / ACTIVITY_FULL));
763
+ const lit = round3(clamp01(0.15 + 0.55 * activity + (needs > 0 ? 0.3 : 0)));
764
+ const seats = holders[key] ? holders[key].map((p) => ({ id: p.id, login: p.login, name: p.name, role: 'holder' })) : [];
765
+ if (lead && lead.person) seats.unshift({ id: lead.person.id, login: lead.person.login, name: lead.person.name, role: 'lead' });
766
+ const decision = d.decision ? {
767
+ rule: d.decision.rule || null, rule_words: d.decision.rule_words || null, who: d.decision.who || null,
768
+ decided_by: d.decision.decided_by || null, sitting: !!d.decision.sitting,
769
+ } : null;
770
+ return {
771
+ key,
772
+ name: str(d.name || v.name || BUILDING_NAMES[key]),
773
+ group: str(d.group || v.group),
774
+ lead: lead && lead.person ? Object.assign({ since: lead.since }, lead.person) : null,
775
+ held_by: lead && lead.person ? 'lead' : 'owner',
776
+ holder: lead && lead.person ? lead.person : owner,
777
+ seats,
778
+ charter: lead ? lead.charter : null,
779
+ // its committee rooms and the build-outs that stand built (GET
780
+ // /government/leads `structure`, task 1004711; empty unread)
781
+ structure: {
782
+ committees: structRead[key] ? realRooms(key) : [],
783
+ built: structRead[key] ? realBuilt(key) : [],
784
+ },
785
+ decision,
786
+ // who answers for it, as GET /government/leads says (null without that read)
787
+ answers: leadsRead ? answersOf(leadsRead, key) : null,
788
+ // what its holder may build out without a vote: the server's answer (null unread)
789
+ build_rights: leadsRead ? rightsOf(leadsRead, key) : null,
790
+ asks: here,
791
+ counts: { needs, moving: agents + movingAsks, building: construction[key].length, asks: here.length },
792
+ activity,
793
+ agents,
794
+ construction: construction[key],
795
+ lit,
796
+ readings: readings.filter((r) => r && r.building === key),
797
+ floors: floors.filter((f) => f && f.building === key).length,
798
+ _sources: {
799
+ key: D, name: R, group: R, decision: R, asks: M,
800
+ // the Lead, its holder and charter: GET /government/leads (task 1004709)
801
+ lead: R, held_by: D, holder: R, charter: R, seats: D, structure: R, answers: R, build_rights: R,
802
+ 'counts.needs': D, 'counts.moving': D, 'counts.building': D, 'counts.asks': D, activity: D, agents: D, construction: M, lit: D, readings: R, floors: D,
803
+ },
804
+ };
805
+ });
806
+
807
+ // ---- districts: one per open goal, in the order the goals come ----
808
+ // the treasury (task 1004712); unread, no district shows a pot
809
+ const treasuryRead = obj(o.treasury) && Array.isArray(o.treasury.pots) ? o.treasury : null;
810
+ const pots = {};
811
+ if (treasuryRead) treasuryRead.pots.forEach((p) => { if (p && p.goal_id != null) pots[String(p.goal_id)] = p; });
812
+ // a pot's borrow is the docket matter its server filed, when the docket carries it
813
+ const borrowOf = (p) => {
814
+ const b = obj(p.borrow);
815
+ return b && b.state === 'open' && b.matter_id && seen[b.matter_id] ? str(b.matter_id) : null;
816
+ };
817
+ const districts = goalsAll.filter((g) => g && g.status === 'open' && g.id != null).map((g) => {
818
+ const id = String(g.id);
819
+ const r = rollups[id] || {};
820
+ const inGoal = claimsInGoal[id] || 0;
821
+ const last = time(r.last_activity_at);
822
+ const recent = now != null && last != null && now - last <= DAY_MS;
823
+ let activity = clamp01(Math.max(inGoal, Number(r.active) || 0) / ACTIVITY_FULL);
824
+ if (recent) activity = Math.max(activity, RECENT_FLOOR);
825
+ const p = pots[id];
826
+ const pot = !p ? null : {
827
+ budget_usd: round2(p.budget_usd), spent_usd: round2(p.spent_usd), lent_usd: round2(p.lent_usd), left_usd: round2(p.left_usd),
828
+ dry: !!p.dry, share: p.share != null ? Number(p.share) : null, borrow_ask_id: borrowOf(p), note: null, real: true,
829
+ };
830
+ return {
831
+ goal_id: id,
832
+ title: str(g.title),
833
+ version_id: g.version_id || null,
834
+ working_area: g.working_area != null ? Number(g.working_area) : null,
835
+ building: goalBuilding(g),
836
+ counts: {
837
+ shipped: Number(r.done) || 0, active: Number(r.active) || 0, ready: Number(r.ready) || 0,
838
+ blocked: Number(r.blocked) || 0, backlog: Number(r.backlog) || 0, total: Number(r.total) || 0,
839
+ },
840
+ last_activity_at: r.last_activity_at || null,
841
+ agents: inGoal,
842
+ activity: round3(activity),
843
+ pot,
844
+ _sources: { goal_id: R, title: R, version_id: R, working_area: R, building: D, counts: R, last_activity_at: R, agents: D, activity: D, pot: R },
845
+ };
846
+ });
847
+
848
+ // ---- the city as a whole ----
849
+ const cost = obj(o.cost) || {};
850
+ const dayKey = now != null ? new Date(now).toISOString().slice(0, 10) : null;
851
+ const monthKey = dayKey ? dayKey.slice(0, 7) : null;
852
+ const daily = arr(cost.daily);
853
+ const byMonth = arr(cost.by_month);
854
+ const today = dayKey ? daily.find((x) => str(x.day).slice(0, 10) === dayKey) : null;
855
+ const month = monthKey ? byMonth.find((x) => x.month === monthKey) : null;
856
+ const monthIdx = month ? byMonth.indexOf(month) : -1;
857
+ const money = readings.find((r) => r && r.key === 'money');
858
+ const mv = obj(o.moving);
859
+ const sessionsList = arr(o.sessions, 'sessions');
860
+ const recentSessions = now != null ? sessionsList.filter((s) => { const t = time(s.uploaded_at); return t != null && now - t <= DAY_MS; }) : [];
861
+ const learningsList = arr(o.learnings, 'learnings');
862
+ const rulesReading = readings.find((r) => r && r.key === 'rules');
863
+ // THE LAW BOOK (task 1004706): GET /government/laws — every law in force,
864
+ // passed by a sitting of the government in force; a repealed law is not in the
865
+ // book. Its locks are enforced by the server (task 1004707): My agents reads
866
+ // what each lock did to the viewer off GET /me's `laws.locks`, never from this book.
867
+ const laws = arr(o.laws, 'laws').filter((l) => l && l.id != null && l.in_force !== false).map((l) => {
868
+ const pb = obj(l.passed_by) || {};
869
+ return {
870
+ id: 'law:' + str(l.id), text: str(l.text),
871
+ passed_by: pb.government ? { id: null, login: '', name: str(pb.government) } : null,
872
+ passed_at: l.passed_at || null, origin: 'vote', effect_on_ai: str(l.effect_on_ai), locks: arr(l.locks).slice(),
873
+ enforced_by: null, real: true,
874
+ _sources: { id: R, text: R, passed_by: R, passed_at: R, origin: D, effect_on_ai: R, locks: R, enforced_by: D, real: D },
875
+ };
876
+ });
877
+ if (rulesReading && rulesReading.status === 'live') {
878
+ // the one standing rule the project really runs (the docket's rules-at-work reading)
879
+ // its name is the reading's own words ("the landing rule settled 24 filings…")
880
+ const named = /^the ([a-z][a-z -]*? rule)\b/i.exec(str(rulesReading.line));
881
+ laws.unshift({
882
+ id: 'rules:' + str(rulesReading.key), text: named ? 'The ' + named[1] : str(rulesReading.figure), passed_by: null, passed_at: null, origin: 'docket',
883
+ effect_on_ai: str(rulesReading.line), locks: [], enforced_by: null, real: true,
884
+ _sources: { id: D, text: D, passed_by: D, passed_at: D, origin: D, effect_on_ai: R, locks: D, enforced_by: D, real: D },
885
+ });
886
+ }
887
+ const counts = { total: asks.length, needs_you: 0, needs: 0, moving: 0, settled: 0, refused: 0, by_seat: {} };
888
+ asks.forEach((a) => {
889
+ if (a.needs_you) counts.needs_you += 1;
890
+ counts[a.state] += 1;
891
+ if (!a.format.ok) counts.refused += 1;
892
+ counts.by_seat[a.waits_on.seat] = (counts.by_seat[a.waits_on.seat] || 0) + 1;
893
+ });
894
+ const city = {
895
+ line: docket && docket.line ? str(docket.line) : 'the docket could not be read just now',
896
+ matters: docket && docket.counts ? Object.assign({}, docket.counts) : null,
897
+ docket_read: !!docket,
898
+ // every docket matter was routed to a real seat on the server (task 1004705)
899
+ routed,
900
+ // the server's counts, when GET /government/counts was read (task 1004716)
901
+ served: servedCounts(o.counts),
902
+ laws,
903
+ asks,
904
+ ask_counts: counts,
905
+ traffic: {
906
+ agents: placed.length,
907
+ read: claimsRead,
908
+ moving: mv && mv.moving != null ? Number(mv.moving) : null,
909
+ by_building: claimsAt,
910
+ unplaced: placed.filter((p) => !p.building).length,
911
+ claims: placed,
912
+ },
913
+ spend: {
914
+ today_usd: today ? round2(today.amount_usd) : (dayKey && daily.length ? 0 : null),
915
+ month_usd: month ? round2(month.total_usd) : (monthKey && byMonth.length ? 0 : null),
916
+ last_month_usd: monthIdx > 0 ? round2(byMonth[monthIdx - 1].total_usd) : null,
917
+ total_usd: cost.total_usd != null ? round2(cost.total_usd) : null,
918
+ unattended: money && obj(money.value) ? { left_usd: money.value.left_usd, spent_usd: money.value.spent_usd, ceiling_usd: money.value.ceiling_usd, line: str(money.line) } : null,
919
+ reserve_usd: treasuryRead && obj(treasuryRead.reserve) && treasuryRead.reserve.usd != null ? round2(treasuryRead.reserve.usd) : null,
920
+ reserve_line: treasuryRead && obj(treasuryRead.reserve) ? str(treasuryRead.reserve.line) : null,
921
+ pots_basis: treasuryRead ? str(treasuryRead.basis) : null,
922
+ // no default spend limit (the owner's ruling): the treasury's cap setting, off
923
+ // unless the project set one; unknown without the read
924
+ limit: treasuryRead && obj(treasuryRead.cap) ? (treasuryRead.cap.on ? round2(treasuryRead.cap.usd) : 'none') : null,
925
+ cap: treasuryRead && obj(treasuryRead.cap) ? { on: !!treasuryRead.cap.on, usd: treasuryRead.cap.usd != null ? round2(treasuryRead.cap.usd) : null, line: str(treasuryRead.cap.line) } : null,
926
+ // who answers for the Treasury today, and whether it is the viewer
927
+ treasury_yours: treasuryRead && obj(treasuryRead.answers) ? !!treasuryRead.answers.yours : null,
928
+ },
929
+ sessions: { last_day: recentSessions.length, builders_last_day: Object.keys(recentSessions.reduce((m, s) => { m[str(s.builder_id)] = 1; return m; }, {})).length, total: (obj(o.sessions) && o.sessions.total != null) ? Number(o.sessions.total) : sessionsList.length },
930
+ learnings: { count: learningsList.length, latest_at: learningsList.reduce((m, l) => (str(l.created_at) > m ? str(l.created_at) : m), '') || null },
931
+ fence: obj(o.fence) ? { enabled: !!o.fence.enabled, paused_reason: o.fence.paused_reason || null, updated_at: o.fence.updated_at || null } : null,
932
+ _sources: {
933
+ line: R, matters: R, docket_read: D, routed: D, served: R, laws: M, asks: M, ask_counts: D,
934
+ 'traffic.agents': D, 'traffic.read': D, 'traffic.moving': R, 'traffic.by_building': D, 'traffic.unplaced': D, 'traffic.claims': D, counts: M,
935
+ 'spend.today_usd': R, 'spend.month_usd': R, 'spend.last_month_usd': R, 'spend.total_usd': R, 'spend.unattended': R,
936
+ 'spend.reserve_usd': R, 'spend.reserve_line': R, 'spend.pots_basis': R, 'spend.limit': R, 'spend.cap': R, 'spend.treasury_yours': R,
937
+ sessions: D, learnings: D, fence: R,
938
+ },
939
+ };
940
+ // the numbers every surface prints, as nobody has answered anything yet
941
+ city.counts = countsOf({ city });
942
+
943
+ // ---- the viewer and my agents ----
944
+ const holds = BUILDING_KEYS.filter((k) => viewerId && (holders[k] || []).some((p) => p.id === viewerId));
945
+ const seats = [];
946
+ holds.forEach((k) => seats.push({ building: k, role: 'holder' }));
947
+ Object.keys(leads).forEach((k) => {
948
+ const l = leads[k];
949
+ if (l.person && l.person.id === viewerId) seats.push({ building: k, role: 'lead' });
950
+ });
951
+ if (ownerIds && viewerId && ownerIds.indexOf(viewerId) >= 0) seats.push({ building: null, role: 'owner' });
952
+ // task 1004713: GET /me's `positions` block is the server's answer to "which seats
953
+ // do you hold" (modules/government/positions.js). When /me carries it, the seats,
954
+ // the seat flag and the governance tab are that block's; without it, the seats
955
+ // the docket's holders and GET /government/leads give above.
956
+ const sent = me && obj(me.positions) && Array.isArray(me.positions.held) ? me.positions : null;
957
+ const realSeats = sent ? arr(sent.held).filter((p) => p && p.kind).map((p) => ({ building: p.building || null, role: str(p.kind), title: str(p.title) })) : null;
958
+ const seatsNow = realSeats || seats;
959
+ const seatSrc = realSeats ? R : D;
960
+ const viewer = vb ? {
961
+ id: viewerId, login: str(vb.github_login), name: str(vb.display_name || vb.github_login), rank: vb.rank || null,
962
+ holds, seats: seatsNow, has_seat: seatsNow.length > 0,
963
+ tabs: seatsNow.length > 0 ? ['city', 'agents', 'governance'] : ['city', 'agents'],
964
+ _sources: { id: R, login: R, name: R, rank: R, holds: D, seats: seatSrc, has_seat: seatSrc, tabs: seatSrc },
965
+ } : null;
966
+ const settings = agentSettings(me);
967
+ const myAgents = {
968
+ strictness: realCurrent ? realCurrent.key : null,
969
+ settings,
970
+ locked: settings.filter((s) => s.locked).length,
971
+ _sources: { strictness: R, settings: M, locked: D },
972
+ };
973
+
974
+ return {
975
+ generated_at: now != null ? new Date(now).toISOString() : null,
976
+ viewer,
977
+ owner,
978
+ // the preset the laws in force amount to (GET /government/presets); null unread
979
+ strictness: realPresets
980
+ ? { key: realCurrent ? realCurrent.key : null, name: realCurrent ? realCurrent.name : 'No preset yet', line: realCurrent ? realCurrent.line : 'Laws are passed one at a time.', presets: realPresets, real: true }
981
+ : null,
982
+ buildings,
983
+ districts,
984
+ city,
985
+ myAgents,
986
+ // who leads each building is a real read (GET /government/leads, task 1004709)
987
+ seats_real: !!leadsRead,
988
+ // what needs whom is routed on the server (task 1004705)
989
+ routed,
990
+ _sources: { generated_at: D, viewer: M, owner: R, strictness: R, buildings: M, districts: M, city: M, myAgents: M, seats_real: D, routed: D },
991
+ };
992
+ }
993
+
994
+ const api = {
995
+ livingModel, askFormat, slopScore, goalBuilding, touchesBuilding, compareAsks,
996
+ countsOf, countWords, countLabel, isOnYou, isOnOthers, COUNT_WORDS, COUNT_LABELS,
997
+ BUILDING_KEYS, BUILDING_NAMES, AREA_BUILDING, CATEGORY_BUILDING, MODULE_BUILDING, ASK_RULES, SLOP,
998
+ ACTIVITY_FULL, RECENT_FLOOR, SOURCES: Object.freeze({ REAL: R, DERIVED: D, MIXED: M }),
999
+ };
1000
+ root.GovLiving = api;
1001
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
1002
+ }(typeof window !== 'undefined' ? window : globalThis));