@bongos/core 1.21.113 → 1.21.115

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 +608 -188
  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 +709 -626
  25. package/docs/copy-registry.json +1673 -858
  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 +4875 -4748
  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-hall-live.states.json +0 -5
  166. package/modules/public-landing/public/projects.html +169 -6
  167. package/modules/public-landing/public/projects.probes.json +10 -6
  168. package/modules/public-landing/public/projects.states.json +13 -13
  169. package/modules/public-landing/public/terms.html +2 -2
  170. package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
  171. package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
  172. package/modules/ui-design/kit/fixtures/connections.json +22 -0
  173. package/modules/ui-design/kit/serve.js +95 -0
  174. package/package-lock.json +2 -2
  175. package/package.json +1 -1
  176. package/release-notes.json +128 -0
  177. package/scripts/discord/README.md +8 -5
  178. package/scripts/discord/post.js +12 -1
  179. package/scripts/gds/artifact-audit.js +20 -3
  180. package/scripts/gds/ask.js +164 -0
  181. package/scripts/gds/autobongos-verify.js +21 -2
  182. package/scripts/gds/government-walk.js +638 -4
  183. package/scripts/gds/pulse.js +9 -6
  184. package/scripts/gds/ship-escalation.js +22 -3
  185. package/scripts/gds/ship-finish.js +5 -4
  186. package/scripts/gds/ship-merge-gate.js +18 -6
  187. package/scripts/hall-preview/README.md +41 -1
  188. package/scripts/hall-preview/pref-writes.js +140 -0
  189. package/scripts/hall-preview/refresh-fixtures.js +101 -7
  190. package/scripts/hall-preview/scrub-identity.js +366 -7
  191. package/scripts/hall-preview/server.js +27 -1
  192. package/scripts/render-diagrams.sh +4 -2
  193. package/src/bongos/auth-idp.js +6 -1
  194. package/src/bongos/project-settings.js +41 -0
  195. package/src/bongos/route-rank-check.js +27 -0
  196. package/src/bongos/routes/me.js +68 -7
  197. package/src/bongos/serve-internal.js +5 -1
  198. package/src/module-api.js +7 -1
  199. package/src/platform-server.js +10 -0
  200. package/tests/application_lifecycle.mjs +47 -5
  201. package/tests/conductor_main_e2e.mjs +21 -0
  202. package/tests/connections_api.mjs +18 -0
  203. package/tests/craft_mode_nav.mjs +5 -3
  204. package/tests/craft_mode_tallies.mjs +52 -16
  205. package/tests/discord_approvals.mjs +11 -0
  206. package/tests/discord_channels.mjs +20 -0
  207. package/tests/fixtures/gov-seated.mjs +64 -0
  208. package/tests/fixtures/government-ask-cards.json +1284 -0
  209. package/tests/government_act_alone_buildings.mjs +1 -0
  210. package/tests/government_agent_asks.mjs +322 -0
  211. package/tests/government_asks.mjs +408 -0
  212. package/tests/government_build_rights.mjs +253 -0
  213. package/tests/government_buildouts.mjs +382 -0
  214. package/tests/government_counts.mjs +143 -0
  215. package/tests/government_decision_rules.mjs +5 -1
  216. package/tests/government_laws.mjs +488 -0
  217. package/tests/government_laws_session.mjs +321 -0
  218. package/tests/government_leads.mjs +375 -0
  219. package/tests/government_positions.mjs +127 -0
  220. package/tests/government_presets.mjs +215 -0
  221. package/tests/government_routes.mjs +8 -1
  222. package/tests/government_routing.mjs +118 -0
  223. package/tests/government_seed.mjs +5 -0
  224. package/tests/government_treasury.mjs +394 -0
  225. package/tests/governor_docket.mjs +26 -2
  226. package/tests/hall_buildouts.mjs +208 -0
  227. package/tests/hall_city.mjs +14 -6
  228. package/tests/hall_city_living.mjs +617 -0
  229. package/tests/hall_gov_counts.mjs +336 -0
  230. package/tests/hall_gov_living.mjs +636 -0
  231. package/tests/hall_gov_real_site.mjs +655 -0
  232. package/tests/hall_matter_card.mjs +6 -1
  233. package/tests/hall_my_agents.mjs +481 -0
  234. package/tests/hall_nav.mjs +107 -4
  235. package/tests/hall_page_gate_map.mjs +4 -0
  236. package/tests/hall_preview_governor_fixtures.mjs +30 -3
  237. package/tests/hall_preview_scrub.mjs +59 -1
  238. package/tests/hall_preview_security.mjs +169 -0
  239. package/tests/hall_reading_world.mjs +44 -0
  240. package/tests/hall_roster_world.mjs +6 -2
  241. package/tests/hall_street.mjs +18 -2
  242. package/tests/hall_street_amend.mjs +1 -1
  243. package/tests/hall_street_deck.mjs +609 -0
  244. package/tests/hall_street_depth.mjs +559 -0
  245. package/tests/hall_street_desk.mjs +1 -1
  246. package/tests/hall_street_inside.mjs +29 -5
  247. package/tests/hall_tweak_editor.mjs +4 -1
  248. package/tests/hub_account_page.mjs +94 -3
  249. package/tests/hub_connections_page.mjs +507 -0
  250. package/tests/hub_invite_badge.mjs +1 -1
  251. package/tests/idp_device_exchange.mjs +18 -0
  252. package/tests/landing_legal_pages.mjs +8 -5
  253. package/tests/landing_page.mjs +23 -4
  254. package/tests/nav_permission_atoms.mjs +4 -1
  255. package/tests/profile_connections_ui.mjs +88 -25
  256. package/tests/projects_hub.mjs +15 -0
  257. package/tests/projects_hub_pre_uat.mjs +4 -2
  258. package/tests/render_check.mjs +2 -2
  259. package/tests/ship_broadcast_routing.mjs +57 -16
  260. package/tests/ship_broadcast_server.mjs +40 -7
  261. package/tests/ship_merge_gate.mjs +19 -0
  262. package/tests/ui_design_kit.mjs +2 -2
  263. package/tests/watch_applications_queue.mjs +23 -0
  264. package/tests/wizard_physics.mjs +1 -1
  265. package/tests/wizard_physics_more.mjs +133 -0
@@ -15,8 +15,10 @@
15
15
  const api = require('../../src/module-api');
16
16
  const db = require('./db');
17
17
  const { loadGovernmentConfig, isClockSafe } = require('./config');
18
- const { isBoardMember } = require('./board-membership');
18
+ const { isBoardMember, resolveBoardMembers } = require('./board-membership');
19
19
  const decisionRules = require('./decision-rules');
20
+ const { charterLine, isLeadAct, answerFor, ownerOf, leadsView: leadsBook } = require('./leads');
21
+ const buildouts = require('./buildouts');
20
22
 
21
23
  const log = api.logger('government');
22
24
 
@@ -60,7 +62,15 @@ async function actOnPassedMatter(closed) {
60
62
  try {
61
63
  matter = await db.getMatter(closed.subject_id);
62
64
  const action = matter && matter.action;
63
- if (area === 'people' && action && (action.type === 'assign_rank' || action.type === 'unassign_rank')) {
65
+ if (action && action.type === 'build_out') {
66
+ // A build-out the vote allowed (task 1004711): put on the record naming
67
+ // this very item, which government_030's trigger checks.
68
+ result = { ...result, ...(await actOnBuildOutMatter(closed, matter, area)) };
69
+ } else if (area === 'people' && action && isLeadAct(action.type)) {
70
+ // A Lead appointed or removed (task 1004709): carried out here, in one
71
+ // transaction the table's trigger checks against this very item.
72
+ result = { ...result, ...(await actOnLeadMatter(closed, action)) };
73
+ } else if (area === 'people' && action && (action.type === 'assign_rank' || action.type === 'unassign_rank')) {
64
74
  // The same walls as the direct route: a custom rank that still exists,
65
75
  // never a seeded one (a seeded rank follows the builder's rank — R95b).
66
76
  const rank = await db.getRankByKey(action.rank_key);
@@ -111,6 +121,45 @@ async function actOnPassedMatter(closed) {
111
121
  };
112
122
  }
113
123
 
124
+ // A passed appoint_lead / remove_lead (task 1004709). Re-validated first (the
125
+ // stored row was checked when opened and is immutable since); the appointee must
126
+ // still be an active builder of the project. Never throws past the caller's catch.
127
+ async function actOnLeadMatter(closed, action) {
128
+ const v = validateMatterAction('people', action);
129
+ if (!v.ok) return { action: action.type || null, done: false, reason: 'bad_action' };
130
+ const a = v.action;
131
+ if (a.type === 'appoint_lead' && !(await db.getActiveBuilder(a.builder_id))) {
132
+ return { action: a.type, done: false, reason: 'builder_gone', building: a.building };
133
+ }
134
+ const out = await db.applyLeadAct({ itemId: closed.id, action: a });
135
+ return {
136
+ action: a.type,
137
+ done: out.done === true,
138
+ building: a.building,
139
+ ...(a.type === 'appoint_lead' ? { builder_id: String(a.builder_id) } : {}),
140
+ ...(out.reason ? { reason: out.reason } : {}),
141
+ };
142
+ }
143
+
144
+ // A passed build_out (task 1004711). Re-validated first (the stored row was
145
+ // checked when opened and is immutable since). Never throws past the caller's catch.
146
+ async function actOnBuildOutMatter(closed, matter, area) {
147
+ const v = validateMatterAction(area, matter.action);
148
+ if (!v.ok) return { action: 'build_out', done: false, reason: 'bad_action' };
149
+ const a = v.action;
150
+ const out = await db.insertBuildout({
151
+ building: a.building, capsule: a.capsule, name: a.name, line: a.line,
152
+ builtBy: matter.proposed_by == null ? null : matter.proposed_by, itemId: closed.id,
153
+ });
154
+ return {
155
+ action: 'build_out',
156
+ done: out.done === true,
157
+ building: a.building,
158
+ capsule: a.capsule,
159
+ ...(out.reason ? { reason: out.reason } : {}),
160
+ };
161
+ }
162
+
114
163
  // ── who decides what: proposing a change (task 1004529) ─────────────────────
115
164
  //
116
165
  // A change to who decides what at a building is an AMENDMENT — of kind
@@ -180,11 +229,15 @@ const MATTER_DETAIL_MAX = 5000;
180
229
  // contributed to 'governor.matterAct' as { area, type, act }. This table is only
181
230
  // the SHAPE of each act (field → kind), checked when the matter is opened and
182
231
  // again before it acts. 'id' is a positive integer, 'rank' a custom rank key,
183
- // 'usd' a non-negative amount or null (no limit).
232
+ // 'usd' a non-negative amount or null (no limit), 'building' one of the nine,
233
+ // 'charter' a Lead's one-line charter (absent: the building's default).
184
234
  const MATTER_ACTIONS = Object.freeze({
185
235
  people: Object.freeze({
186
236
  assign_rank: Object.freeze({ builder_id: 'id', rank_key: 'rank' }),
187
237
  unassign_rank: Object.freeze({ builder_id: 'id', rank_key: 'rank' }),
238
+ // task 1004709: the Lead of a building, appointed and removed by vote
239
+ appoint_lead: Object.freeze({ building: 'building', builder_id: 'id', charter: 'charter' }),
240
+ remove_lead: Object.freeze({ building: 'building' }),
188
241
  }),
189
242
  automation: Object.freeze({
190
243
  switch_on: Object.freeze({}),
@@ -205,7 +258,12 @@ const MATTER_ACTIONS = Object.freeze({
205
258
  const MATTER_ACT_POINT = 'governor.matterAct';
206
259
  const MAX_BUDGET_USD = 100000;
207
260
 
208
- function fieldValue(kind, raw) {
261
+ function fieldValue(kind, raw, action) {
262
+ if (kind === 'building') return decisionRules.AREA_KEYS.includes(raw) ? { ok: true, v: raw } : { ok: false };
263
+ if (kind === 'charter') {
264
+ const line = charterLine(raw, action && action.building);
265
+ return line ? { ok: true, v: line } : { ok: false };
266
+ }
209
267
  if (kind === 'id') {
210
268
  const n = Number(raw);
211
269
  return Number.isSafeInteger(n) && n > 0 && raw !== null && raw !== '' && typeof raw !== 'boolean' ? { ok: true, v: n } : { ok: false };
@@ -220,6 +278,17 @@ function fieldValue(kind, raw) {
220
278
 
221
279
  function validateMatterAction(area, action) {
222
280
  if (action == null) return { ok: true, action: null };
281
+ // A build-out (task 1004711) is a matter at the building it adds to — the
282
+ // Charter's at People (buildouts.matterAreaFor) — carried out here, at every
283
+ // building, whichever modules are on.
284
+ if (action && typeof action === 'object' && action.type === 'build_out') {
285
+ const b = buildouts.checkBuildOut(action);
286
+ if (!b.ok) return { ok: false, reason: b.reason === 'unknown_field' ? 'unknown_field' : 'bad_action', field: b.field ? `action.${b.field}` : undefined };
287
+ if (buildouts.matterAreaFor(b.buildout.building) !== area) {
288
+ return { ok: false, reason: 'bad_action', field: 'action.building', expected: `a build-out is a matter at the building it adds to (${buildouts.matterAreaFor(b.buildout.building)})` };
289
+ }
290
+ return { ok: true, action: b.buildout };
291
+ }
223
292
  const acts = MATTER_ACTIONS[area];
224
293
  if (!acts) return { ok: false, reason: 'action_not_supported_here', expected: `a matter acts by itself only at ${Object.keys(MATTER_ACTIONS).join(', ')}` };
225
294
  if (typeof action !== 'object' || Array.isArray(action)) return { ok: false, reason: 'bad_action' };
@@ -230,8 +299,9 @@ function validateMatterAction(area, action) {
230
299
  }
231
300
  const out = { type: action.type };
232
301
  for (const [k, kind] of Object.entries(shape)) {
233
- if (!Object.prototype.hasOwnProperty.call(action, k)) return { ok: false, reason: 'bad_action', field: `action.${k}` };
234
- const f = fieldValue(kind, action[k]);
302
+ // a Lead's charter may be left out: the building's default is filed in its place
303
+ if (!Object.prototype.hasOwnProperty.call(action, k) && kind !== 'charter') return { ok: false, reason: 'bad_action', field: `action.${k}` };
304
+ const f = fieldValue(kind, action[k], action);
235
305
  if (!f.ok) return { ok: false, reason: 'bad_action', field: `action.${k}` };
236
306
  out[k] = f.v;
237
307
  }
@@ -256,6 +326,36 @@ async function openMatter({ area, title, detailMd = null, action = null, propose
256
326
  const { board } = loadGovernmentConfig();
257
327
  const rows = await db.listDecisionRules();
258
328
  const resolved = decisionRules.resolveAll(rows, { board }).find((r) => r.area === area);
329
+ // A Lead is the government's to appoint (task 1004709, ADR 0374): where People
330
+ // has no sitting of its own (decided as today), the matter sits under the board
331
+ // in force, like an amendment; where People names a sitting, it sits there.
332
+ const leadAct = !!(a.action && area === 'people' && isLeadAct(a.action.type));
333
+ // A build-out (task 1004711, ADR 0378) is the government's to allow, like a
334
+ // Lead: it sits under the board in force where its building has no sitting of
335
+ // its own, and ALWAYS when it is a district (a new part of the city) or the
336
+ // Charter's own (the board's building); otherwise at the building's sitting —
337
+ // the same places build-rights.js says the vote goes.
338
+ const isBuildOut = !!(a.action && a.action.type === 'build_out');
339
+ // Only the building's holder may ask, by this path too (ADR 0378 D2): a
340
+ // build-out filed straight at the matters route meets the same wall.
341
+ if (isBuildOut) {
342
+ const notYours = await buildOutHolderRefusal(a.action.building, proposedBy);
343
+ if (notYours) return notYours;
344
+ }
345
+ const boardBuilds = isBuildOut && (resolved.as_today || a.action.capsule === 'district' || a.action.building === 'charter');
346
+ if ((leadAct && resolved.as_today) || boardBuilds) {
347
+ const refused = leadAct ? await leadMatterRefusal(a.action) : null;
348
+ if (refused) return refused;
349
+ const matter = await db.createMatter({ area, title: title.trim(), detailMd, action: a.action, proposedBy });
350
+ const item = await db.openBoardItem({
351
+ subjectType: decisionRules.MATTER_SUBJECT_TYPES[area],
352
+ subjectId: matter.id,
353
+ openedBy: proposedBy,
354
+ constitution: board,
355
+ windowMinutes: board.window_minutes,
356
+ });
357
+ return { ok: true, matter, item, decided: { ...resolved, sat_under: 'board' } };
358
+ }
259
359
  if (resolved.as_today) {
260
360
  return {
261
361
  ok: false, reason: 'decided_by_gate', area, gate: resolved.gate,
@@ -263,10 +363,13 @@ async function openMatter({ area, title, detailMd = null, action = null, propose
263
363
  };
264
364
  }
265
365
  if (!resolved.sitting) return { ok: false, reason: 'no_one_holds_this', area, what_this_means: `No one holds ${resolved.name} yet, so nothing there can be decided.` };
266
- if (a.action && area !== 'people' && !matterActFor(area, a.action.type)) {
366
+ if (a.action && area !== 'people' && !isBuildOut && !matterActFor(area, a.action.type)) {
267
367
  return { ok: false, reason: 'action_not_wired', area, what_this_means: `Nothing at ${resolved.name} can carry that out right now, so it would pass and do nothing. Turn on the module that owns ${resolved.name}, or file the matter without an act.` };
268
368
  }
269
- if (a.action && area === 'people') {
369
+ if (leadAct) {
370
+ const refused = await leadMatterRefusal(a.action);
371
+ if (refused) return refused;
372
+ } else if (a.action && area === 'people' && !isBuildOut) {
270
373
  const rank = await db.getRankByKey(a.action.rank_key);
271
374
  if (!rank) return { ok: false, reason: 'unknown_rank', rank_key: a.action.rank_key };
272
375
  if (rank.is_system) return { ok: false, reason: 'seeded_rank', rank_key: a.action.rank_key, expected: 'a custom rank — a seeded rank follows the builder\'s rank' };
@@ -285,6 +388,21 @@ async function openMatter({ area, title, detailMd = null, action = null, propose
285
388
  return { ok: true, matter, item, decided: resolved };
286
389
  }
287
390
 
391
+ // Why a Lead matter cannot be filed, or null when it can (task 1004709): the
392
+ // appointee must be an active builder and not already lead the building on the
393
+ // same charter; a removal needs a Lead in office.
394
+ async function leadMatterRefusal(action) {
395
+ const inOffice = (await db.listLeads()).find((l) => l.building === action.building) || null;
396
+ if (action.type === 'remove_lead') {
397
+ return inOffice ? null : { ok: false, reason: 'no_lead', area: 'people', building: action.building, what_this_means: `${decisionRules.areaByKey(action.building).name} has no Lead to remove.` };
398
+ }
399
+ if (!(await db.getActiveBuilder(action.builder_id))) return { ok: false, reason: 'unknown_builder', builder_id: String(action.builder_id) };
400
+ if (inOffice && String(inOffice.builder_id) === String(action.builder_id) && inOffice.charter === action.charter) {
401
+ return { ok: false, reason: 'already_lead', area: 'people', building: action.building, what_this_means: 'They already lead it, on that charter.' };
402
+ }
403
+ return null;
404
+ }
405
+
288
406
  // May this builder act ALONE at this building — the check a building's own
289
407
  // direct route runs before acting (People's rank assignments here; every other
290
408
  // building's routes through the doorway's requireActAlone, task 1004657 — the
@@ -332,6 +450,112 @@ async function announcePassedMatter(acted) {
332
450
 
333
451
  // ── the reads board.js shows ────────────────────────────────────────────────
334
452
 
453
+ // GET /government/leads (task 1004709): the nine buildings, each with its Lead in
454
+ // office or null, and who answers for it (its Lead, or the owner when unseated).
455
+ // Read per request, uncached.
456
+ //
457
+ // Each building also carries `build_rights` (task 1004710): what its holder may
458
+ // build out without a vote, from the FORM OF GOVERNMENT in force and the
459
+ // building's own decision row (build-rights.js is the rule). The context is
460
+ // resolved here, server-side and uncached (ADR 0016): the board block, the
461
+ // resolved decision table, who the board seats, and who each 'decide' / 'first'
462
+ // sitting seats. Every part FAILS CLOSED: an unreadable part reads as "a vote",
463
+ // never as "at once". No goal stores build terms yet, so the goals hook is [].
464
+ async function buildRightsContext() {
465
+ let board = {};
466
+ // read through the module object (not the destructured name) so a test can
467
+ // hand it a constitution
468
+ try { board = require('./config').loadGovernmentConfig().board || {}; } catch (_) { board = {}; }
469
+ const decisions = (await decisionRulesFor(board)) || [];
470
+ const idsOf = async (membership) => {
471
+ try { return (await resolveBoardMembers({ membership })).map((m) => String(m.builder_id)); } catch (_) { return []; }
472
+ };
473
+ const boardMemberIds = await idsOf(board.membership);
474
+ const sittingMemberIds = {};
475
+ // only a sitting whose rule lets a seated member act alone can let its holder
476
+ // build at once, so only those are asked who they seat
477
+ const actsAlone = decisionRules.DECISION_RULES.filter((r) => r.actsAlone).map((r) => r.key);
478
+ for (const d of decisions) {
479
+ if (d.sitting && actsAlone.includes(d.rule)) sittingMemberIds[d.area] = await idsOf(d.decided_by);
480
+ }
481
+ return { board, decisions, boardMemberIds, sittingMemberIds, goals: [] };
482
+ }
483
+
484
+ // Each building also carries its `structure` (task 1004711): the build-outs on
485
+ // the record (built, or going up) and the ones waiting on a vote. A failed read
486
+ // of either leaves the building's structure out (null), never invented.
487
+ // `structure: false` skips the build-out reads (a caller that needs only who
488
+ // answers for a building and its build_rights).
489
+ async function leadsView({ includePast = false, now = Date.now(), structure: withStructure = true } = {}) {
490
+ const [rows, archons, rights, standing, pending] = await Promise.all([
491
+ db.listLeads({ includePast }), db.listActiveArchons(), buildRightsContext(),
492
+ withStructure ? db.listBuildouts().catch(() => null) : null, withStructure ? db.listPendingBuildouts().catch(() => null) : null,
493
+ ]);
494
+ const structure = standing && pending ? { rows: standing, pending, now } : null;
495
+ return leadsBook(rows, archons, { includePast, rights, structure });
496
+ }
497
+
498
+ // ── a Lead's build-outs (task 1004711, ADR 0378) ────────────────────────────
499
+
500
+ // The refusal when someone other than the building's holder asks, with where to go instead.
501
+ function notTheHolder(building, leadName) {
502
+ const area = decisionRules.areaByKey(building);
503
+ const name = area ? area.name : 'This building';
504
+ return {
505
+ ok: false, reason: 'not_the_holder', building,
506
+ what_this_means: leadName
507
+ ? `${name} is led by ${leadName}, and only they grow it. Ask them to put it up.`
508
+ : `${name} has no Lead, so only the owner grows it. Ask the owner to put it up.`,
509
+ };
510
+ }
511
+
512
+ // Only the building's holder may ask for a build-out, whichever path files it
513
+ // (answerFor, the one rule): null when `builderId` holds it, else the refusal.
514
+ async function buildOutHolderRefusal(building, builderId) {
515
+ const [rows, archons] = await Promise.all([db.listLeads(), db.listActiveArchons()]);
516
+ const answers = answerFor(building, rows, ownerOf(archons));
517
+ if (answers && answers.builder_ids.map(String).includes(String(builderId))) return null;
518
+ const lead = (rows || []).find((l) => l && l.building === building && !l.removed_at) || null;
519
+ return notTheHolder(building, lead ? lead.display_name || lead.github_login || 'its Lead' : null);
520
+ }
521
+ //
522
+ // POST /government/buildouts. The building's HOLDER (its Lead, or the owner
523
+ // seat when unseated — leads.answerFor) asks for a build-out; build-rights.js,
524
+ // read off the same server-side view GET /government/leads serves, says whether
525
+ // it goes up at once or goes to a vote. At once: put on the record now, with the
526
+ // rule's ground and why. A vote: filed as a build_out matter through openMatter,
527
+ // the same path POST /government/board/matters runs (the caller, board.js, adds
528
+ // the window-open nudge). Fails closed: rights that cannot be read are a vote.
529
+ //
530
+ // Returns { ok: true, path: 'at_once', buildout, rights } |
531
+ // { ok: true, path: 'vote', matter, item, decided, rights } |
532
+ // { ok: false, reason, … } ('bad_buildout', 'unknown_field', 'not_the_holder', openMatter's).
533
+ async function buildOut({ buildout, builderId } = {}) {
534
+ const b = buildouts.checkBuildOut({ type: 'build_out', ...(buildout || {}) });
535
+ if (!b.ok) return { ok: false, reason: b.reason, field: b.field };
536
+ if (builderId == null || builderId === '') return { ok: false, reason: 'missing_proposer' };
537
+ const a = b.buildout;
538
+ const view = await leadsView({ structure: false });
539
+ const here = view.buildings.find((x) => x.building === a.building) || null;
540
+ const holders = ((here && here.answers && here.answers.builder_ids) || []).map(String);
541
+ if (!holders.includes(String(builderId))) return notTheHolder(a.building, here && here.lead ? here.lead.builder.name : null);
542
+ const rights = here.build_rights && here.build_rights.capsules ? here.build_rights.capsules[a.capsule] || null : null;
543
+ if (rights && rights.without_vote === true && a.capsule !== 'district') {
544
+ const out = await db.insertBuildout({
545
+ building: a.building, capsule: a.capsule, name: a.name, line: a.line,
546
+ builtBy: builderId, ground: rights.ground, why: rights.why,
547
+ });
548
+ return { ok: true, path: 'at_once', buildout: out.buildout || null, rights };
549
+ }
550
+ const what = { committee: 'a committee room', floor: 'a new floor', annex: 'an annex', district: 'a district of its own' }[a.capsule];
551
+ const title = a.capsule === 'district' ? `Grow ${here.name} into a district: ${a.name}` : `Add ${what} to ${here.name}: ${a.name}`;
552
+ const filed = await openMatter({
553
+ area: buildouts.matterAreaFor(a.building), title: title.slice(0, MATTER_TITLE_MAX), detailMd: a.line || null, action: a, proposedBy: builderId,
554
+ });
555
+ if (!filed.ok) return { ...filed, rights };
556
+ return { ok: true, path: 'vote', matter: filed.matter, item: filed.item, decided: filed.decided, rights };
557
+ }
558
+
335
559
  // The Board Room's matter subjects, keyed by id. No query when no matter is on
336
560
  // the board, so a project that never amended itself reads exactly what it did.
337
561
  async function matterSubjectsById(items) {
@@ -386,4 +610,6 @@ module.exports = {
386
610
  MATTER_ACT_POINT,
387
611
  openMatter,
388
612
  checkActAlone,
613
+ leadsView,
614
+ buildOut,
389
615
  };
@@ -115,6 +115,8 @@ const AREAS = Object.freeze([
115
115
  Object.freeze({ key: 'treasury', name: 'Treasury', gate: 'builder.budget.set',
116
116
  today: 'whoever may set spending limits', acts: Object.freeze([
117
117
  'PATCH /builders/:id/budget',
118
+ // task 1004712: the project's optional spend cap (off by default: no limit)
119
+ 'PATCH /project-settings/spend-cap',
118
120
  ]) }),
119
121
  ]);
120
122
  const AREA_KEYS = Object.freeze(AREAS.map((a) => a.key));
@@ -83,6 +83,9 @@ const resolver = require('./resolver');
83
83
  const decisionRules = require('./decision-rules');
84
84
  const docketFloors = require('./docket-floors');
85
85
  const governmentConfig = require('./config');
86
+ const asksDb = require('./asks-db');
87
+ const routing = require('./routing');
88
+ const { ownerOf } = require('./leads');
86
89
 
87
90
  const log = api.logger('government');
88
91
 
@@ -108,6 +111,8 @@ const EXPECTED_SOURCES = Object.freeze([
108
111
  Object.freeze({ source: 'gate_approvals', building: 'release', label: 'CI gate approvals' }),
109
112
  Object.freeze({ source: 'autobongos_fence', building: 'automation', label: 'the Autobongos fence' }),
110
113
  Object.freeze({ source: 'module_rechecks', building: 'release', label: 'module re-checks' }),
114
+ // task 1004712: a dry pot's borrow from the reserve, a treasury matter (treasury.js)
115
+ Object.freeze({ source: 'treasury_spending', building: 'treasury', label: 'spending matters' }),
111
116
  ]);
112
117
 
113
118
  // Named by the design, not yet recorded anywhere. Each says why, in words.
@@ -116,8 +121,6 @@ const COMING_SOURCES = Object.freeze([
116
121
  reason: 'There is no way to appeal a grade yet.' }),
117
122
  Object.freeze({ source: 'standing_rules', building: 'rules', label: 'standing rules',
118
123
  reason: 'A new standing rule has nowhere to be proposed yet. The one rule in force, idea routing, is counted in the rules-at-work reading.' }),
119
- Object.freeze({ source: 'treasury_spending', building: 'treasury', label: 'spending matters',
120
- reason: 'The project has no budget or spending votes recorded yet.' }),
121
124
  ]);
122
125
 
123
126
  // Floors the design names whose engine does not exist yet. Drawn, and say why.
@@ -339,7 +342,9 @@ function countStates(matters) {
339
342
  return c;
340
343
  }
341
344
 
342
- // The home line under "start governing", in the design's words.
345
+ // The home line under "start governing", in the design's words. `counts.needs`
346
+ // is what waits on the viewer: on a routed docket (task 1004705) the matters on
347
+ // the viewer's own seats, not every matter that needs somebody.
343
348
  function homeLine(counts) {
344
349
  if (!counts.needs && !counts.moving) return 'nothing waits on you.';
345
350
  const parts = [];
@@ -519,6 +524,28 @@ function registeredContributions() {
519
524
  return { sources, readings, floors, histories, desks };
520
525
  }
521
526
 
527
+ // ── the ask each matter carries (task 1004703) ───────────────────────────────
528
+
529
+ // A matter an agent put to a person carries its ASK (asks.js): the question, two
530
+ // or three tap answers, the pick and why, the seat it waits on and what happens
531
+ // if nobody answers, stored per matter id in government_asks. Attached only to
532
+ // the matters this viewer already sees, so an ask is never more visible than its
533
+ // matter. A read that fails or is slow attaches none: the city still stands, and
534
+ // each matter keeps its own words.
535
+ async function attachAsks(matters, readAsks, timeoutMs) {
536
+ if (typeof readAsks !== 'function' || !matters.length) return;
537
+ try {
538
+ const found = await withTimeout(Promise.resolve(readAsks(matters.map((m) => m.id))), timeoutMs);
539
+ if (!found || typeof found.get !== 'function') return;
540
+ for (const m of matters) {
541
+ const ask = found.get(m.id);
542
+ if (ask) m.ask = ask;
543
+ }
544
+ } catch (err) {
545
+ log.warn({ err: err && err.message }, 'governor docket: asks unreadable');
546
+ }
547
+ }
548
+
522
549
  // ── the collector ────────────────────────────────────────────────────────────
523
550
 
524
551
  // Build the docket for one viewer. `permissions` is the viewer's resolved set;
@@ -529,7 +556,12 @@ function registeredContributions() {
529
556
  // rather than a guess.
530
557
  // `government` is what docketFor() read once for this module's own floors and
531
558
  // record (loadGovernment below); null leaves those floors `unavailable`.
532
- async function buildDocket({ viewer, permissions, contributions = registeredContributions(), now = Date.now(), timeoutMs = SOURCE_TIMEOUT_MS, decisions = null, government = null } = {}) {
559
+ // `readAsks(matterIds)` → Map(matter id → ask) reads the stored asks (task
560
+ // 1004703, asks-db.js asksFor); null attaches none.
561
+ // `seats` is who sits in each seat (loadSeats below: Leads in office, the owner
562
+ // seat, each building's holders); given, every matter carries `route` (routing.js,
563
+ // task 1004705) and the docket says `routed: true`. null routes nothing.
564
+ async function buildDocket({ viewer, permissions, contributions = registeredContributions(), now = Date.now(), timeoutMs = SOURCE_TIMEOUT_MS, decisions = null, government = null, readAsks = null, seats = null } = {}) {
533
565
  const held = new Set(permissions || []);
534
566
  const ctx = Object.freeze({
535
567
  builderId: String(viewer.id),
@@ -581,6 +613,9 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
581
613
  const order = declared.map((d) => d.source);
582
614
  sourceStatus.sort((a, b) => order.indexOf(a.source) - order.indexOf(b.source));
583
615
  matters.sort(compareMatters);
616
+ await attachAsks(matters, readAsks, timeoutMs);
617
+ // THE DIRECTION DESK (task 1004705): who each matter waits on, from real seats.
618
+ const yours = seats ? routing.routeAll(matters, ctx.builderId, seats, now) : null;
584
619
 
585
620
  // Readings: contributed + this module's own + the ones derived from matters.
586
621
  const readingImpls = new Map(contributions.readings);
@@ -689,6 +724,7 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
689
724
  historySources.sort((a, b) => (a.source < b.source ? -1 : 1));
690
725
 
691
726
  const counts = countStates(matters);
727
+ if (yours != null) counts.yours = yours;
692
728
  const decisionBy = new Map((Array.isArray(decisions) ? decisions : []).filter(Boolean).map((d) => [d.area, d]));
693
729
  const holdersBy = govAllowed && government && government.holders ? government.holders : {};
694
730
  const buildings = vocab.BUILDINGS.map((b) => {
@@ -712,7 +748,8 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
712
748
  viewer: { id: ctx.builderId },
713
749
  generated_at: new Date(now).toISOString(),
714
750
  counts,
715
- line: homeLine(counts),
751
+ line: homeLine(yours != null ? { needs: yours, moving: counts.moving } : counts),
752
+ routed: yours != null,
716
753
  matters,
717
754
  buildings,
718
755
  sources: sourceStatus,
@@ -729,6 +766,41 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
729
766
  };
730
767
  }
731
768
 
769
+ // ONE matter, as this viewer's docket would show it (task 1004703): the write
770
+ // side of an ask may only reach a matter its writer can see. Reads that one
771
+ // source under the gate it declared — the same gate the docket applies — and
772
+ // finds the matter in it. → { matter } | { reason: 'no_such_source' |
773
+ // 'not_yours' | 'not_found' | 'unavailable' }. Pure over its inputs, like
774
+ // buildDocket.
775
+ async function findMatter({ viewer, permissions, matterId, contributions = registeredContributions(), now = Date.now(), timeoutMs = SOURCE_TIMEOUT_MS } = {}) {
776
+ const id = String(matterId || '');
777
+ const cut = id.indexOf(':');
778
+ const source = cut > 0 ? id.slice(0, cut) : '';
779
+ const decl = EXPECTED_SOURCES.find((d) => d.source === source);
780
+ const c = source === 'board' ? BOARD_SOURCE : new Map(contributions.sources).get(source);
781
+ if (!decl || !c) return { reason: 'no_such_source' };
782
+ const held = new Set(permissions || []);
783
+ const ctx = Object.freeze({ builderId: String(viewer.id), rank: viewer.rank || null, has: (p) => held.has(p) });
784
+ if (!gateAllows(c.gate, ctx)) return { reason: 'not_yours' };
785
+ const memo = new Map();
786
+ const once = (key, fn) => {
787
+ if (!memo.has(key)) memo.set(key, Promise.resolve().then(fn));
788
+ return memo.get(key);
789
+ };
790
+ try {
791
+ const raw = await withTimeout(Promise.resolve(c.read(ctx, { now, once })), timeoutMs);
792
+ const matter = (Array.isArray(raw) ? raw : []).map((m) => normalizeMatter(m, { source, building: c.building || decl.building })).find((m) => m && m.id === id);
793
+ return matter ? { matter } : { reason: 'not_found' };
794
+ } catch (err) {
795
+ log.warn({ source, err: err && err.message }, 'governor docket: a matter could not be read');
796
+ return { reason: 'unavailable' };
797
+ }
798
+ }
799
+ async function findMatterFor(builder, matterId) {
800
+ const permissions = await resolver.resolveBuilderPermissions(builder.id);
801
+ return findMatter({ viewer: builder, permissions, matterId });
802
+ }
803
+
732
804
  // The route's entry point: resolve the viewer's permissions server-side (fresh,
733
805
  // uncached — ADR 0016), then build.
734
806
  async function docketFor(builder) {
@@ -740,7 +812,38 @@ async function docketFor(builder) {
740
812
  return null;
741
813
  });
742
814
  const government = permissions.includes(GOVERNMENT_GATE.permission) ? await loadGovernment({ decisions }) : null;
743
- return buildDocket({ viewer: builder, permissions, decisions, government });
815
+ const seats = government ? await loadSeats(government) : null;
816
+ return buildDocket({ viewer: builder, permissions, decisions, government, readAsks: asksDb.asksFor, seats });
817
+ }
818
+
819
+ // Who sits in each seat now (task 1004705), read per request, uncached (ADR
820
+ // 0016): the Leads in office, the owner seat (every active Archon) and each
821
+ // building's holders as loadGovernment resolved them. A read that fails routes
822
+ // nothing — the docket then says `routed: false` rather than guess whose it is.
823
+ async function loadSeats(government) {
824
+ try {
825
+ const [leads, archons] = await Promise.all([db.listLeads(), db.listActiveArchons()]);
826
+ return { leads, owner: ownerOf(archons), holders: (government && government.holders) || {} };
827
+ } catch (err) {
828
+ log.warn({ err: err && err.message }, 'governor docket: seats unreadable, nothing routed');
829
+ return null;
830
+ }
831
+ }
832
+
833
+ // One matter with its stored ask and the seats, for passing it on to `toSeat`
834
+ // (task 1004705): → { matter, seats } | { reason } as findMatter answers. A
835
+ // board's holders are read only when a board seat is involved.
836
+ async function matterWithSeats(builder, matterId, toSeat = '') {
837
+ const found = await findMatterFor(builder, matterId);
838
+ if (!found.matter) return found;
839
+ const ask = (await asksDb.asksFor([found.matter.id])).get(found.matter.id);
840
+ if (ask) found.matter.ask = ask;
841
+ const boardSeat = [toSeat, ask && ask.waits_on].some((s) => /^board:/.test(String(s || '')));
842
+ const government = boardSeat
843
+ ? await loadGovernment({ decisions: await board.decisionRulesView().catch(() => null) })
844
+ : { holders: {} };
845
+ const seats = await loadSeats(government);
846
+ return seats ? { matter: found.matter, seats } : { reason: 'unavailable' };
744
847
  }
745
848
 
746
849
  // This module's own floors and record, read ONCE per request (task 1004527),
@@ -809,4 +912,9 @@ module.exports = {
809
912
  registeredContributions,
810
913
  buildDocket,
811
914
  docketFor,
915
+ attachAsks,
916
+ findMatter,
917
+ findMatterFor,
918
+ loadSeats,
919
+ matterWithSeats,
812
920
  };