@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,160 @@
1
+ 'use strict';
2
+
3
+ // modules/government/buildouts.js — A LEAD'S BUILD-OUTS: the structure a
4
+ // building grows under its holder, as data (task 1004711, goal 1000111; ADR
5
+ // 0378). db.js holds the reads and the one write, decision-board.js decides a
6
+ // build-out (at once, or filed as a vote) and carries out a passed one,
7
+ // routes/government.js serves POST /government/buildouts, and government_030
8
+ // is the table and its guard.
9
+ //
10
+ // WHAT A BUILD-OUT IS. One of the four capsules build-rights.js names, added to
11
+ // one of the nine buildings: a committee room, a new floor, an annex, or growing
12
+ // the building into a district. It carries a short name and one line saying
13
+ // what it is for. A build-out is never edited or taken down: each row is one
14
+ // change to the city, on the record.
15
+ //
16
+ // THE TWO PATHS, decided by the ONE rule (build-rights.js, task 1004710):
17
+ // * AT ONCE — the building's holder (its Lead, or the owner seat when it is
18
+ // unseated: leads.answerFor) asks, and build_rights for that capsule says
19
+ // `without_vote`: the row is written now, carrying the rule's `ground` and
20
+ // `why`. The city shows it being built (a live site, scaffold and crane)
21
+ // for BUILD_HOURS from when it started; then it stands as built.
22
+ // * BY VOTE — otherwise the ask is filed as a matter (action build_out)
23
+ // through the board's own matters path (decision-board openMatter, the same
24
+ // code POST /government/board/matters runs). While the sitting is open the
25
+ // city draws it as a dashed ghost with the sitting's tally arc; when it
26
+ // passes, the row is written naming the passed item and it goes up.
27
+ //
28
+ // WHO MAY ASK. Only the building's holder: a Lead grows the structure under
29
+ // them. Whether they may do it without a vote is never theirs to say.
30
+ //
31
+ // PURE: no db.
32
+
33
+ const { AREA_KEYS } = require('./decision-rules');
34
+ const { CAPSULES } = require('./build-rights');
35
+
36
+ // How long a build-out stands as a live site before it reads as built. Progress
37
+ // is the share of this window since it started: a real clock, not a number
38
+ // anyone sets.
39
+ const BUILD_HOURS = 24;
40
+ const BUILD_MS = BUILD_HOURS * 3600 * 1000;
41
+ const NAME_MIN = 2;
42
+ const NAME_MAX = 40;
43
+ const LINE_MAX = 140;
44
+
45
+ // What each capsule is called when its holder names nothing.
46
+ const DEFAULT_NAMES = Object.freeze({
47
+ committee: 'Committee room',
48
+ floor: 'New floor',
49
+ annex: 'Annex',
50
+ district: 'District',
51
+ });
52
+
53
+ const isBuilding = (b) => typeof b === 'string' && AREA_KEYS.includes(b);
54
+ const isCapsule = (c) => typeof c === 'string' && CAPSULES.includes(c);
55
+
56
+ // A build-out's name, normalised, or null when it breaks the rule (2-40
57
+ // characters, one line). An absent name takes the capsule's default.
58
+ function nameOf(raw, capsule) {
59
+ if (raw == null || raw === '') return DEFAULT_NAMES[capsule] || null;
60
+ if (typeof raw !== 'string' || /[\r\n]/.test(raw)) return null;
61
+ const s = raw.trim();
62
+ return s.length >= NAME_MIN && s.length <= NAME_MAX ? s : null;
63
+ }
64
+ // Its line: one line of at most 140 characters, or '' when none is given.
65
+ function lineOf(raw) {
66
+ if (raw == null || raw === '') return '';
67
+ if (typeof raw !== 'string' || /[\r\n]/.test(raw)) return null;
68
+ const s = raw.trim();
69
+ return s.length <= LINE_MAX ? s : null;
70
+ }
71
+
72
+ // A build-out ask, checked strictly: { ok, buildout } or { ok: false, reason, field }.
73
+ // The same shape is the matter action (type 'build_out') a vote carries.
74
+ function checkBuildOut(raw) {
75
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, reason: 'bad_buildout', field: null };
76
+ for (const k of Object.keys(raw)) {
77
+ if (!['type', 'building', 'capsule', 'name', 'line'].includes(k)) return { ok: false, reason: 'unknown_field', field: k };
78
+ }
79
+ if (raw.type != null && raw.type !== 'build_out') return { ok: false, reason: 'bad_buildout', field: 'type' };
80
+ if (!isBuilding(raw.building)) return { ok: false, reason: 'bad_buildout', field: 'building' };
81
+ if (!isCapsule(raw.capsule)) return { ok: false, reason: 'bad_buildout', field: 'capsule' };
82
+ const name = nameOf(raw.name, raw.capsule);
83
+ if (!name) return { ok: false, reason: 'bad_buildout', field: 'name' };
84
+ const line = lineOf(raw.line);
85
+ if (line == null) return { ok: false, reason: 'bad_buildout', field: 'line' };
86
+ return { ok: true, buildout: { type: 'build_out', building: raw.building, capsule: raw.capsule, name, line } };
87
+ }
88
+
89
+ // Where a build-out vote is filed: a matter at the building itself. The Charter
90
+ // takes no matters (its changes are amendments), so its build-outs go to the
91
+ // board as a People matter, the building that answers for positions.
92
+ const matterAreaFor = (building) => (building === 'charter' ? 'people' : building);
93
+
94
+ // How far up a build-out stands at `now` (ms): 0..1 over BUILD_HOURS from its start.
95
+ function progressOf(row, now) {
96
+ const t = Date.parse(row && row.started_at);
97
+ if (!Number.isFinite(t) || !Number.isFinite(now)) return 1;
98
+ return Math.max(0, Math.min(1, Math.round(((now - t) / BUILD_MS) * 1000) / 1000));
99
+ }
100
+
101
+ // One standing build-out as the read shows it.
102
+ function projectBuildOut(r, now) {
103
+ const progress = progressOf(r, now);
104
+ const started = r.started_at instanceof Date ? r.started_at.toISOString() : r.started_at || null;
105
+ return {
106
+ id: String(r.id),
107
+ building: r.building,
108
+ capsule: r.capsule,
109
+ name: r.name,
110
+ line: r.line || '',
111
+ state: progress >= 1 ? 'built' : 'building',
112
+ progress,
113
+ started_at: started,
114
+ builder: r.built_by == null ? null : { id: String(r.built_by), login: r.github_login || null, name: r.display_name || r.github_login || null },
115
+ // how it was allowed: a passed vote, or the rule that let its holder build at once
116
+ allowed: r.authorised_by_item_id != null
117
+ ? { path: 'vote', item_id: String(r.authorised_by_item_id), href: `/board-room?item=${r.authorised_by_item_id}` }
118
+ : { path: 'at_once', ground: r.ground, why: r.why || null },
119
+ };
120
+ }
121
+
122
+ // One proposal waiting on its sitting: an open board item whose matter is a build_out.
123
+ function projectProposal(p) {
124
+ const a = (p && p.action) || {};
125
+ return {
126
+ item_id: String(p.item_id),
127
+ matter_id: String(p.matter_id),
128
+ // the docket's id for this sitting: its tally rides there
129
+ docket_id: `board:${p.item_id}`,
130
+ building: a.building,
131
+ capsule: a.capsule,
132
+ name: a.name,
133
+ line: a.line || '',
134
+ opened_at: p.opened_at || null,
135
+ closes_at: p.closes_at || null,
136
+ href: `/board-room?item=${p.item_id}`,
137
+ };
138
+ }
139
+
140
+ // Per building: what stands (built or going up) and what waits on a vote.
141
+ // `rows` are government_buildouts rows, `pending` the open build_out sittings
142
+ // ({ item_id, matter_id, action, opened_at, closes_at }).
143
+ function structureOf(building, rows, pending, now) {
144
+ return {
145
+ standing: (rows || []).filter((r) => r && r.building === building).map((r) => projectBuildOut(r, now)),
146
+ proposed: (pending || []).filter((p) => p && p.action && p.action.building === building).map(projectProposal),
147
+ };
148
+ }
149
+
150
+ module.exports = {
151
+ BUILD_HOURS,
152
+ BUILD_MS,
153
+ DEFAULT_NAMES,
154
+ NAME_MAX,
155
+ LINE_MAX,
156
+ checkBuildOut,
157
+ matterAreaFor,
158
+ progressOf,
159
+ structureOf,
160
+ };
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ // modules/government/counts.js — the government's counts, in ONE server read
4
+ // (task 1004716, goal 1000111): GET /government/counts.
5
+ //
6
+ // WHY. The city's plaque and door, the street's header, the decision deck and the
7
+ // top bar's Governing tally all print the same five numbers ("24 to do", "3 wait
8
+ // on others", "38 to decide", "5 agents working", "7 moving"). The hall counted
9
+ // them itself over about five reads (the docket, the blockers, the seats, the
10
+ // claims, GET /moving), and a page that only wanted the tally paid for all of
11
+ // them. Now the server answers the counts once, and the hall's one count rule
12
+ // (modules/hall-ui/public/gov-living.js countsOf) reads them from here.
13
+ //
14
+ // THE RULE IS THE DOCKET'S, not a second one. Whether a matter is on you is the
15
+ // route the docket already carries (routing.routeMatter `yours`, task 1004705):
16
+ // on_you docket matters whose route is yours (the docket's counts.yours)
17
+ // on_others matters that need a person, are not yours, and somebody can
18
+ // still answer (an overdue or unheld ask has settled: route.nobody)
19
+ // open on_you + on_others: what the deck holds
20
+ // agents_at_work open claims, one per agent (GET /tasks/claimable active_claims)
21
+ // moving tasks being built or waiting to land (GET /moving: active +
22
+ // confirmed); never below agents_at_work
23
+ // A docket that could not be routed (the seats were unreadable) says every matter
24
+ // that needs a person is on you, as the hall did before routing existed.
25
+ // A count that could not be read is null, never 0: zero says nothing waits on you.
26
+ //
27
+ // `yours` and `others` name the matters, so a page can take away the ones it has
28
+ // answered this visit without reading the docket again.
29
+
30
+ const api = require('../../src/module-api');
31
+
32
+ // → the counts over a docket (docket.buildDocket's shape) and the traffic counts.
33
+ // Pure; the route and the hall's parity test (tests/government_counts.mjs) call it.
34
+ function countsOf(docket, traffic) {
35
+ const d = docket && typeof docket === 'object' ? docket : null;
36
+ const t = traffic && typeof traffic === 'object' ? traffic : {};
37
+ const yours = [];
38
+ const others = [];
39
+ if (d) {
40
+ const routed = d.routed === true;
41
+ for (const m of Array.isArray(d.matters) ? d.matters : []) {
42
+ if (!m || m.id == null || m.state !== 'needs') continue;
43
+ const r = routed && m.route && typeof m.route === 'object' ? m.route : null;
44
+ if (r && r.nobody) continue;
45
+ (routed ? (r && r.yours) : true) ? yours.push(String(m.id)) : others.push(String(m.id));
46
+ }
47
+ }
48
+ const n = (v) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null);
49
+ return {
50
+ on_you: d ? yours.length : null,
51
+ on_others: d ? others.length : null,
52
+ open: d ? yours.length + others.length : null,
53
+ agents_at_work: n(t.agents_at_work),
54
+ moving: n(t.moving),
55
+ routed: !!(d && d.routed === true),
56
+ yours: d ? yours : null,
57
+ others: d ? others : null,
58
+ };
59
+ }
60
+
61
+ // The two traffic counts through the `lifecycle.traffic` port (modules/lifecycle/
62
+ // moving.js); a module that is off, or a count that fails, leaves its figure null.
63
+ async function readTraffic() {
64
+ const port = typeof api.resolveOptional === 'function' ? api.resolveOptional('lifecycle.traffic', null) : null;
65
+ if (!port) return {};
66
+ const [moving, agents] = await Promise.all([
67
+ Promise.resolve().then(() => port.countMoving()).catch(() => null),
68
+ Promise.resolve().then(() => port.countAgentsAtWork()).catch(() => null),
69
+ ]);
70
+ return { moving: moving && typeof moving.moving === 'number' ? moving.moving : null, agents_at_work: agents };
71
+ }
72
+
73
+ // GET /government/counts for this viewer: the docket they would read (the same
74
+ // per-source gates, the same seats) and the traffic, counted once.
75
+ async function countsFor(builder, deps = {}) {
76
+ const docket = deps.docket || require('./docket');
77
+ const [d, traffic] = await Promise.all([docket.docketFor(builder), (deps.readTraffic || readTraffic)()]);
78
+ return Object.assign({ viewer: { id: d && d.viewer ? d.viewer.id : null }, generated_at: d && d.generated_at ? d.generated_at : new Date().toISOString() }, countsOf(d, traffic));
79
+ }
80
+
81
+ module.exports = { countsOf, countsFor };
@@ -687,6 +687,238 @@ async function applyDecisionRules(client, { itemId, rules }) {
687
687
  return written;
688
688
  }
689
689
 
690
+ // --- standing orders: the project's laws (task 1004706, government_027) -----
691
+
692
+ // The law book. In force only by default, in the order they passed; with
693
+ // includeRepealed, every law ever passed. Each row carries the government it was
694
+ // decided under (the passing item's snapshotted constitution) and its proposer.
695
+ // Read per request, uncached — a law passed a moment ago reads back at once.
696
+ async function listLaws({ includeRepealed = false } = {}) {
697
+ const { rows } = await pool.query(
698
+ `SELECT l.id, l.text, l.effect_on_ai, l.locks, l.proposed_by, l.passed_by_item_id, l.passed_at,
699
+ l.repealed_by_item_id, l.repealed_at, i.constitution AS decided_under,
700
+ b.github_login AS proposer_login, b.display_name AS proposer_name
701
+ FROM government_laws l
702
+ JOIN government_board_items i ON i.id = l.passed_by_item_id
703
+ LEFT JOIN builders b ON b.id = l.proposed_by
704
+ WHERE $1::boolean OR l.repealed_at IS NULL
705
+ ORDER BY l.passed_at, l.id`,
706
+ [includeRepealed === true],
707
+ );
708
+ return rows;
709
+ }
710
+
711
+ // Who said yes on these sittings — the "who passed it" half a law row points at.
712
+ // Open ballot (ADR 0175 §9), so nothing here is private. Oldest vote first.
713
+ async function listYesVoters(itemIds) {
714
+ if (!itemIds || !itemIds.length) return [];
715
+ const { rows } = await pool.query(
716
+ `SELECT v.item_id, v.voter_id, v.cast_at, b.github_login, b.display_name
717
+ FROM government_board_votes v
718
+ LEFT JOIN builders b ON b.id = v.voter_id
719
+ WHERE v.item_id = ANY($1::bigint[]) AND v.direction = 'yes'
720
+ ORDER BY v.cast_at, v.id`,
721
+ [itemIds],
722
+ );
723
+ return rows;
724
+ }
725
+
726
+ // THE ONE WRITE, and it runs only INSIDE the transaction that closes a laws
727
+ // amendment as passed (board.closeItem's afterClose — `client` is that
728
+ // transaction). Each statement writes nothing unless the named item is a passed
729
+ // laws amendment (the WHERE EXISTS), and government_027's trigger refuses any
730
+ // write that is not — so a law that skipped the board cannot land even through
731
+ // hand-written SQL. An enacted law that did not land throws, which rolls the
732
+ // close back (the sitting is retried, never passed-but-unapplied). A repeal of a
733
+ // law another sitting already repealed is not an error: it is already out of
734
+ // force, and the count says so.
735
+ async function applyLaws(client, { itemId, enact = [], repeal = [], proposedBy = null }) {
736
+ const enacted = [];
737
+ for (const law of enact) {
738
+ const { rows } = await client.query(
739
+ `INSERT INTO government_laws (text, effect_on_ai, locks, proposed_by, passed_by_item_id)
740
+ SELECT $1::text, $2::text, $3::jsonb, $4::bigint, $5::bigint
741
+ WHERE EXISTS (
742
+ SELECT 1 FROM government_board_items i
743
+ JOIN government_amendments a ON a.id = i.subject_id
744
+ WHERE i.id = $5::bigint AND i.subject_type = 'constitutional_amendment'
745
+ AND i.outcome = 'passed' AND a.kind = 'laws')
746
+ RETURNING id, text, effect_on_ai, locks, proposed_by, passed_by_item_id, passed_at`,
747
+ [law.text, law.effect_on_ai ?? null, JSON.stringify(law.locks || []), proposedBy, itemId],
748
+ );
749
+ if (rows.length !== 1) throw new Error(`law "${law.text}" not written (item ${itemId} is not a passed laws amendment)`);
750
+ enacted.push(rows[0]);
751
+ }
752
+ let repealed = [];
753
+ if (repeal.length) {
754
+ const { rows } = await client.query(
755
+ `UPDATE government_laws
756
+ SET repealed_by_item_id = $1::bigint, repealed_at = now()
757
+ WHERE id = ANY($2::bigint[]) AND repealed_at IS NULL
758
+ AND EXISTS (
759
+ SELECT 1 FROM government_board_items i
760
+ JOIN government_amendments a ON a.id = i.subject_id
761
+ WHERE i.id = $1::bigint AND i.subject_type = 'constitutional_amendment'
762
+ AND i.outcome = 'passed' AND a.kind = 'laws')
763
+ RETURNING id, repealed_at`,
764
+ [itemId, repeal],
765
+ );
766
+ repealed = rows;
767
+ }
768
+ return { enacted, repealed };
769
+ }
770
+
771
+ // --- the Lead of each building (task 1004709, government_029) -------------
772
+
773
+ // Every appointment, in office first by default. Each row carries the Lead's
774
+ // login and name. Read per request, uncached — an appointment carried out a
775
+ // moment ago reads back at once.
776
+ async function listLeads({ includePast = false } = {}) {
777
+ const { rows } = await pool.query(
778
+ `SELECT l.id, l.building, l.builder_id, l.charter, l.appointed_by_item_id, l.appointed_at,
779
+ l.removed_by_item_id, l.removed_at, b.github_login, b.display_name
780
+ FROM government_leads l
781
+ LEFT JOIN builders b ON b.id = l.builder_id
782
+ WHERE $1::boolean OR l.removed_at IS NULL
783
+ ORDER BY l.appointed_at, l.id`,
784
+ [includePast === true],
785
+ );
786
+ return rows;
787
+ }
788
+
789
+ // Every active Archon (the founder's rank), oldest id first: the owner seat
790
+ // (leads.js ownerOf), the founding Archon first.
791
+ async function listActiveArchons() {
792
+ const { rows } = await pool.query(
793
+ `SELECT id, github_login, display_name
794
+ FROM builders
795
+ WHERE rank = 'archon' AND status = 'active' AND system_role IS NULL
796
+ ORDER BY id`,
797
+ );
798
+ return rows;
799
+ }
800
+
801
+ // One builder of this project, or null: who a Lead appointment names. Only an
802
+ // active person (no machine identity) can be appointed.
803
+ async function getActiveBuilder(builderId) {
804
+ const { rows } = await pool.query(
805
+ `SELECT id, github_login, display_name FROM builders
806
+ WHERE id = $1 AND status = 'active' AND system_role IS NULL`,
807
+ [builderId],
808
+ );
809
+ return rows[0] || null;
810
+ }
811
+
812
+ // THE ONE WRITE: carry out a PASSED People matter's Lead act, in one
813
+ // transaction. appoint_lead ends the building's Lead in office (if any) and
814
+ // seats the new one; remove_lead ends the Lead in office. Every statement names
815
+ // the passed item, and government_029's trigger refuses any write that item
816
+ // does not say — so a Lead that skipped the vote cannot land even through
817
+ // hand-written SQL. Carried out once: a second run of the same item seats
818
+ // nobody twice (the unique index on appointed_by_item_id) and says so.
819
+ // Returns { done, reason?, lead?, ended? }.
820
+ async function applyLeadAct({ itemId, action }) {
821
+ const client = await pool.connect();
822
+ try {
823
+ await client.query('BEGIN');
824
+ // the building's row of office, locked so two passed matters land one at a time
825
+ const { rows: current } = await client.query(
826
+ `SELECT id, builder_id, appointed_by_item_id FROM government_leads
827
+ WHERE building = $1 AND removed_at IS NULL
828
+ FOR UPDATE`,
829
+ [action.building],
830
+ );
831
+ const sitting = current[0] || null;
832
+ if (action.type === 'appoint_lead' && sitting && String(sitting.appointed_by_item_id) === String(itemId)) {
833
+ await client.query('ROLLBACK');
834
+ return { done: true, reason: 'already_carried_out' };
835
+ }
836
+ if (action.type === 'remove_lead' && !sitting) {
837
+ await client.query('ROLLBACK');
838
+ return { done: false, reason: 'no_lead' };
839
+ }
840
+ let ended = null;
841
+ if (sitting) {
842
+ const { rows } = await client.query(
843
+ `UPDATE government_leads SET removed_by_item_id = $1::bigint, removed_at = now()
844
+ WHERE id = $2 AND removed_at IS NULL
845
+ RETURNING id, builder_id, removed_at`,
846
+ [itemId, sitting.id],
847
+ );
848
+ ended = rows[0] || null;
849
+ }
850
+ let lead = null;
851
+ if (action.type === 'appoint_lead') {
852
+ const { rows } = await client.query(
853
+ `INSERT INTO government_leads (building, builder_id, charter, appointed_by_item_id)
854
+ VALUES ($1, $2::bigint, $3, $4::bigint)
855
+ ON CONFLICT (appointed_by_item_id) DO NOTHING
856
+ RETURNING id, building, builder_id, charter, appointed_by_item_id, appointed_at`,
857
+ [action.building, action.builder_id, action.charter, itemId],
858
+ );
859
+ if (!rows.length) {
860
+ await client.query('ROLLBACK');
861
+ return { done: true, reason: 'already_carried_out' };
862
+ }
863
+ lead = rows[0];
864
+ }
865
+ await client.query('COMMIT');
866
+ return { done: true, lead, ended };
867
+ } catch (err) {
868
+ await client.query('ROLLBACK').catch(() => {});
869
+ throw err;
870
+ } finally {
871
+ client.release();
872
+ }
873
+ }
874
+
875
+ // --- a Lead's build-outs (task 1004711, government_030) ---------------------
876
+
877
+ // Every build-out on the record, oldest first, each with who asked. Read per
878
+ // request, uncached: one built a moment ago reads back at once.
879
+ async function listBuildouts() {
880
+ const { rows } = await pool.query(
881
+ `SELECT o.id, o.building, o.capsule, o.name, o.line, o.built_by, o.authorised_by_item_id,
882
+ o.ground, o.why, o.started_at, b.github_login, b.display_name
883
+ FROM government_buildouts o
884
+ LEFT JOIN builders b ON b.id = o.built_by
885
+ ORDER BY o.started_at, o.id`,
886
+ );
887
+ return rows;
888
+ }
889
+
890
+ // The build-outs waiting on a vote: every OPEN sitting whose matter's action is
891
+ // a build_out, oldest first.
892
+ async function listPendingBuildouts() {
893
+ const { rows } = await pool.query(
894
+ `SELECT i.id AS item_id, m.id AS matter_id, m.action, i.opened_at, i.closes_at
895
+ FROM government_board_items i
896
+ JOIN government_matters m ON m.id = i.subject_id AND i.subject_type = m.area || '_matter'
897
+ WHERE i.closed_at IS NULL
898
+ AND m.action->>'type' = 'build_out'
899
+ ORDER BY i.opened_at, i.id`,
900
+ );
901
+ return rows;
902
+ }
903
+
904
+ // THE ONE WRITE: put a build-out on the record. Exactly one of `itemId` (a
905
+ // passed build_out matter: the vote path) or `ground` + `why` (the build-rights
906
+ // answer that let its holder build at once). government_030's trigger refuses a
907
+ // vote its matter did not say, an at-once district, and any later edit. A
908
+ // passed item builds once: a second run returns { done: true, reason:
909
+ // 'already_carried_out' }.
910
+ async function insertBuildout({ building, capsule, name, line = '', builtBy = null, itemId = null, ground = null, why = null }) {
911
+ const { rows } = await pool.query(
912
+ `INSERT INTO government_buildouts (building, capsule, name, line, built_by, authorised_by_item_id, ground, why)
913
+ VALUES ($1, $2, $3, $4, $5::bigint, $6::bigint, $7, $8)
914
+ ON CONFLICT (authorised_by_item_id) WHERE authorised_by_item_id IS NOT NULL DO NOTHING
915
+ RETURNING id, building, capsule, name, line, built_by, authorised_by_item_id, ground, why, started_at`,
916
+ [building, capsule, name, line, builtBy, itemId, ground, why],
917
+ );
918
+ if (!rows.length) return { done: true, reason: 'already_carried_out' };
919
+ return { done: true, buildout: rows[0] };
920
+ }
921
+
690
922
  // A matter put to a building's sitting. Immutable once filed, like an amendment.
691
923
  async function createMatter({ area, title, detailMd = null, action = null, proposedBy }) {
692
924
  const { rows } = await pool.query(
@@ -955,6 +1187,16 @@ module.exports = {
955
1187
  getAmendment,
956
1188
  listDecisionRules,
957
1189
  applyDecisionRules,
1190
+ listLaws,
1191
+ listYesVoters,
1192
+ applyLaws,
1193
+ listLeads,
1194
+ listActiveArchons,
1195
+ getActiveBuilder,
1196
+ applyLeadAct,
1197
+ listBuildouts,
1198
+ listPendingBuildouts,
1199
+ insertBuildout,
958
1200
  createMatter,
959
1201
  getMatter,
960
1202
  getMatterSubjects,