@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
@@ -25,11 +25,21 @@ const db = require('../db');
25
25
  const sessionScopes = require('../session-scopes');
26
26
  const boardMembership = require('../board-membership');
27
27
  const board = require('../board');
28
+ const laws = require('../laws');
29
+ const presets = require('../presets');
28
30
  const boardWithdraw = require('../board-withdraw');
29
31
  const recommendations = require('../recommendations');
30
32
  const docket = require('../docket');
33
+ const counts = require('../counts');
34
+ const asks = require('../asks');
35
+ const asksDb = require('../asks-db');
36
+ const routing = require('../routing');
31
37
  const governmentConfig = require('../config');
32
38
  const membershipPredicate = require('../membership-predicate');
39
+ const positions = require('../positions');
40
+ const decisionBoard = require('../decision-board');
41
+ const treasury = require('../treasury');
42
+ const treasuryDb = require('../treasury-db');
33
43
  // task 1003208: structured logging (pino via the doorway) — was console.*.
34
44
  const log = api.logger('government');
35
45
 
@@ -130,8 +140,51 @@ const governmentPort = {
130
140
  // building's own direct route runs before letting one builder act alone.
131
141
  decisionRulesView: board.decisionRulesView,
132
142
  checkActAlone: board.checkActAlone,
143
+ // task 1004707 — the laws every AI session reads. GET /me carries this block
144
+ // and the Conductor relays its contract verbatim; builder-settings enforces
145
+ // each law's locks on the builder's personal settings. Read per request,
146
+ // uncached (a law passed a moment ago is in the next read). Read-only: a law
147
+ // is written only by a passed amendment (law-board.js), never through here.
148
+ lawsForSession: async () => laws.forSession(await db.listLaws()),
149
+ // task 1004713 — the POSITIONS a builder holds (positions.js): owner, Lead,
150
+ // a building's sitting, the board. GET /me carries it so the hall shows the
151
+ // governing surfaces only to a seat holder. Read per request, uncached; null
152
+ // when a read failed (unknown, never "none"). Grants nothing.
153
+ positionsFor: (builderId) => positions.readPositions(builderId, {
154
+ listLeads: () => db.listLeads(),
155
+ listActiveArchons: () => db.listActiveArchons(),
156
+ board: () => governmentConfig.loadGovernmentConfig().board || {},
157
+ decisions: (b) => decisionBoard.decisionRulesFor(b),
158
+ sits: (id, membership) => boardMembership.isBoardMember(id, { membership }),
159
+ }),
160
+ // task 1004703 — THE ASK. The one shape every request from an agent to a person
161
+ // takes (asks.js). A module that files such a request (ideas' POST /blockers)
162
+ // checks it here, refuses slop with refusalLine's words, stores it per docket
163
+ // matter, and reads it back for its own list. Authority stays with the filing
164
+ // route: these only check, store and read the shape.
165
+ checkAsk: asks.checkAsk,
166
+ refusalLine: asks.refusalLine,
167
+ // task 1004704: what a hand filing from an AI session must carry, in plain words.
168
+ askRequiredMessage: asks.ASK_REQUIRED_MESSAGE,
169
+ saveAsk: (...a) => asksDb.saveAsk(...a),
170
+ asksFor: (...a) => asksDb.asksFor(...a),
133
171
  };
134
172
 
173
+ // An ask arrives bare, or wrapped as { ask } (the shape POST /blockers takes it in).
174
+ function askBody(req) {
175
+ const b = req.body;
176
+ return b && typeof b === 'object' && b.ask !== undefined ? b.ask : b;
177
+ }
178
+ // The one refusal every ask route answers with: 400 `ask_refused`, the fixes in
179
+ // plain words as the message, and every broken rule as details.
180
+ function refuseAsk(res, verdict) {
181
+ return res.fail('ask_refused', {
182
+ status: 400,
183
+ message: asks.refusalLine(verdict.errors),
184
+ details: { errors: verdict.errors, rules: asks.ASK_RULES },
185
+ });
186
+ }
187
+
135
188
  function registerSeams() {
136
189
  if (!api.hasProvider('government')) {
137
190
  api.registerProvider('government', governmentPort);
@@ -198,8 +251,37 @@ function contributeRankDesk() {
198
251
  });
199
252
  }
200
253
 
254
+ // THE TREASURY'S SPENDING MATTERS (task 1004712, ADR 0379): every open borrow a
255
+ // dry pot filed, as a docket matter at the Treasury, under the treasury read's own
256
+ // atom. Its acts go only to a viewer who sits in the seat its stored ask waits on
257
+ // (routing.resolveSeat over the real seats, read per request); the answer route
258
+ // checks the same again, so the docket only hides what would be refused.
259
+ function contributeTreasury() {
260
+ api.contribute('governor.docket', {
261
+ source: treasury.MATTER_SOURCE,
262
+ building: 'treasury',
263
+ gate: { permission: 'board.vote.cast' },
264
+ read: async (ctx) => {
265
+ const open = await treasuryDb.openBorrowRows();
266
+ if (!open.length) return [];
267
+ const seats = await docket.loadSeats({ holders: {} });
268
+ return open.map(({ row, ask }) => {
269
+ const r = seats ? routing.resolveSeat((ask && ask.waits_on) || treasury.SEAT, seats) : null;
270
+ return treasury.borrowMatter(row, { title: row.title, canAnswer: !!(r && r.builder_ids.includes(String(ctx.builderId))) });
271
+ });
272
+ },
273
+ });
274
+ }
275
+
276
+ // Who answers for the Treasury today: its Lead, or the owner seat while unseated.
277
+ async function treasuryHolder() {
278
+ const seats = await docket.loadSeats({ holders: {} });
279
+ return seats ? routing.resolveSeat(treasury.SEAT, seats) : null;
280
+ }
281
+
201
282
  module.exports = function buildGovernmentRouter() {
202
283
  contributeRankDesk();
284
+ contributeTreasury();
203
285
  registerSeams();
204
286
  const router = express.Router();
205
287
 
@@ -663,7 +745,9 @@ module.exports = function buildGovernmentRouter() {
663
745
  // matters: [{ id: '<source>:<id>', source, building, rule, state,
664
746
  // title, short, where, opened_at, deadline,
665
747
  // proposer: { id, login, name } | null,
666
- // tally: { for, against, open, needs } | null, href, ref }],
748
+ // tally: { for, against, open, needs } | null, href, ref,
749
+ // ask?: { question, options, recommend, waits_on, // task 1004703:
750
+ // if_nobody_answers, detail, written_at } }], // the stored ask
667
751
  // buildings: [{ key, name, group, counts, matter_ids }], // street order
668
752
  // sources: [{ source, building, label, status, count, reason }],
669
753
  // // status: live | not_yours | coming | off | unavailable
@@ -682,6 +766,96 @@ module.exports = function buildGovernmentRouter() {
682
766
  }
683
767
  });
684
768
 
769
+ // rank: perm board.vote.cast — the government's COUNTS in one read (task 1004716):
770
+ // what is on the viewer ("to do"), what waits on others, what is to decide, how
771
+ // many agents are working and how much is moving. The docket's own atom and its
772
+ // own per-source gates (counts.countsFor reads the docket this viewer would), so
773
+ // a count never includes a matter the viewer could not see.
774
+ // Response: { viewer: { id }, generated_at, on_you, on_others, open,
775
+ // agents_at_work, moving, routed, yours: [matter id], others: [matter id] }
776
+ // (a figure that could not be read is null, never 0).
777
+ router.get('/government/counts', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
778
+ try {
779
+ res.json(await counts.countsFor(req.builder));
780
+ } catch (err) {
781
+ log.error('[gds] GET /government/counts', err);
782
+ res.fail('counts_read_failed', { status: 500, message: 'internal error' });
783
+ }
784
+ });
785
+
786
+ // rank: perm board.vote.cast — CHECK an ask without storing it (task 1004703):
787
+ // the agent's dry run before it files. Answers the same verdict a write would:
788
+ // { ok: true, ask } (the stored form), or 400 `ask_refused` whose message says
789
+ // in plain words what to fix and whose details list every broken rule. Writes
790
+ // nothing and reads nothing; the docket's own atom (xenos floor), like the write.
791
+ //
792
+ // Body: the ask — { question, options: [{ key, label, result, act? }],
793
+ // recommend: { key, why }, waits_on, if_nobody_answers, detail? } — bare, or as { ask }.
794
+ router.post('/government/asks/check', api.requireBuilder, api.requirePermission('board.vote.cast'), (req, res) => {
795
+ const verdict = asks.checkAsk(askBody(req));
796
+ if (!verdict.ok) return refuseAsk(res, verdict);
797
+ res.json({ ok: true, ask: verdict.ask, question_words: verdict.question_words, rules: asks.ASK_RULES });
798
+ });
799
+
800
+ // rank: perm board.vote.cast — put an ASK on a docket matter (task 1004703):
801
+ // the one shape every request from an agent to a person takes. The matter is
802
+ // named by its docket id (`blockers:12`, `board:34`); the writer must hold the
803
+ // docket's atom AND see the matter, through the same per-source gate the
804
+ // docket applies (docket.findMatter), so an ask is never written onto a matter
805
+ // its writer cannot see. A slop ask is refused (400 `ask_refused`, every fix
806
+ // named) and nothing is stored. A second PUT replaces the ask whole.
807
+ //
808
+ // Body: the ask (as POST /government/asks/check). → 200 { ok, matter_id, ask }
809
+ router.put('/government/asks/:matterId', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
810
+ const matterId = String(req.params.matterId || '');
811
+ if (!asks.isMatterId(matterId)) return res.fail('bad_matter_id', { status: 400, message: 'Name the matter by its docket id, like blockers:12.' });
812
+ const verdict = asks.checkAsk(askBody(req));
813
+ if (!verdict.ok) return refuseAsk(res, verdict);
814
+ try {
815
+ const found = await docket.findMatterFor(req.builder, matterId);
816
+ if (!found.matter) {
817
+ if (found.reason === 'unavailable') return res.fail('matter_unavailable', { status: 503, message: 'That matter could not be read just now, so nothing was saved. Try again.' });
818
+ // a matter you cannot see answers exactly as one that does not exist
819
+ return res.fail('matter_not_found', { status: 404, message: 'No open matter by that id that you can see.' });
820
+ }
821
+ const ask = await asksDb.saveAsk({ matterId, ask: verdict.ask, builderId: req.builder.id });
822
+ res.json({ ok: true, matter_id: matterId, ask });
823
+ } catch (err) {
824
+ log.error('[gds] PUT /government/asks/:matterId', err);
825
+ res.fail('ask_save_failed', { status: 500, message: 'internal error' });
826
+ }
827
+ });
828
+
829
+ // rank: perm board.vote.cast — PASS AN ASK ON to another seat (task 1004705,
830
+ // the direction desk). Only whoever the ask waits on today may hand it on
831
+ // (routing.passRefusal, from real seats: its Lead, the owner seat, a board's
832
+ // holders), and only an ask that is stored; a board sitting is its voters' and
833
+ // is never passed. The ask is kept word for word; only its seat changes, so it
834
+ // leaves the passer's "to do" and lands on the new seat's.
835
+ //
836
+ // Body: { seat } → 200 { ok, matter_id, ask, route }
837
+ router.post('/government/asks/:matterId/pass', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
838
+ const matterId = String(req.params.matterId || '');
839
+ if (!asks.isMatterId(matterId)) return res.fail('bad_matter_id', { status: 400, message: 'Name the matter by its docket id, like blockers:12.' });
840
+ if (validateOrRespond(req, res, { seat: { required: true, type: 'string', minLength: 1, maxLength: 80 } })) return;
841
+ const seat = req.body.seat.trim();
842
+ try {
843
+ const found = await docket.matterWithSeats(req.builder, matterId, seat);
844
+ if (!found.matter) {
845
+ if (found.reason === 'unavailable') return res.fail('matter_unavailable', { status: 503, message: 'That matter could not be read just now, so nothing was passed on. Try again.' });
846
+ return res.fail('matter_not_found', { status: 404, message: 'No open matter by that id that you can see.' });
847
+ }
848
+ const refused = routing.passRefusal(found.matter, req.builder.id, seat, found.seats);
849
+ if (refused) return res.fail(refused.code, { status: refused.status, message: refused.message });
850
+ const ask = await asksDb.saveAsk({ matterId, ask: { ...found.matter.ask, waits_on: seat }, builderId: req.builder.id });
851
+ const route = routing.routeMatter({ ...found.matter, ask }, req.builder.id, found.seats);
852
+ res.json({ ok: true, matter_id: matterId, ask, route });
853
+ } catch (err) {
854
+ log.error('[gds] POST /government/asks/:matterId/pass', err);
855
+ res.fail('ask_pass_failed', { status: 500, message: 'internal error' });
856
+ }
857
+ });
858
+
685
859
  // rank: perm board.item.open — put an AMENDMENT to the board (R14 of goal
686
860
  // 1000069, ADR 0175 §7). The board's other subject never comes through here:
687
861
  // a Full Idea's window opens AUTOMATICALLY on grade pass (R07), so requesting
@@ -841,6 +1015,125 @@ module.exports = function buildGovernmentRouter() {
841
1015
  }
842
1016
  });
843
1017
 
1018
+ // rank: perm board.item.open — propose a change to the project's LAWS (task
1019
+ // 1004706): laws to enact, laws in force to repeal, or both, filed as an
1020
+ // amendment of kind 'laws' and sat under the constitution IN FORCE, like every
1021
+ // amendment. Nothing here writes a law — only the sitting passing does, inside
1022
+ // its closing transaction. The same atom as proposing any amendment.
1023
+ //
1024
+ // Body: { enact?: [{ text, effect_on_ai?, locks? }], repeal?: [law id], rationale_md? }
1025
+ // text one line, 3-100 characters: what every agent obeys
1026
+ // effect_on_ai one line, at most 200: what it means for an agent
1027
+ // locks at most 8 of { setting, value } (fixes it) or { setting, max } (caps it)
1028
+ router.post('/government/board/laws', api.requireBuilder, api.requirePermission('board.item.open'), async (req, res) => {
1029
+ if (validateOrRespond(req, res, {
1030
+ enact: { type: 'array' },
1031
+ repeal: { type: 'array' },
1032
+ rationale_md: { type: 'string', maxLength: 5000 },
1033
+ })) return;
1034
+ try {
1035
+ const out = await board.proposeLaws({
1036
+ enact: req.body.enact,
1037
+ repeal: req.body.repeal,
1038
+ rationaleMd: req.body.rationale_md ?? null,
1039
+ proposedBy: req.builder.id,
1040
+ });
1041
+ if (!out.ok) {
1042
+ if (out.reason === 'already_in_force' || out.reason === 'setting_already_locked' || out.reason === 'not_in_force') {
1043
+ return res.fail(out.reason, 409, {
1044
+ ...(out.law_id ? { law_id: out.law_id } : {}),
1045
+ ...(out.setting ? { setting: out.setting } : {}),
1046
+ ...(out.field ? { field: out.field } : {}),
1047
+ });
1048
+ }
1049
+ return res.fail('invalid_laws', 400, {
1050
+ reason: out.reason,
1051
+ ...(out.field ? { field: out.field } : {}),
1052
+ ...(out.expected ? { expected: out.expected } : {}),
1053
+ ...(out.setting ? { setting: out.setting } : {}),
1054
+ });
1055
+ }
1056
+ res.status(201).json({ ok: true, amendment: out.amendment, item: out.item, proposal: out.proposal });
1057
+ } catch (err) {
1058
+ log.error('[gds] POST /government/board/laws', err);
1059
+ res.fail('propose_failed', { status: 500, message: 'internal error' });
1060
+ }
1061
+ });
1062
+
1063
+ // rank: perm board.vote.cast — the law book (task 1004706): every law in force,
1064
+ // oldest first, each with who passed it (the government it was decided under,
1065
+ // its proposer, who said yes, the sitting) and when. ?include=repealed adds the
1066
+ // laws repealed since, with when and by which sitting. Every builder's agents
1067
+ // obey these, so the read floors where the room's does. A read; nothing here
1068
+ // writes a law.
1069
+ router.get('/government/laws', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
1070
+ try {
1071
+ res.json(await board.lawsView({ includeRepealed: req.query.include === 'repealed' }));
1072
+ } catch (err) {
1073
+ log.error('[gds] GET /government/laws', err);
1074
+ res.fail('laws_read_failed', { status: 500, message: 'internal error' });
1075
+ }
1076
+ });
1077
+
1078
+ // rank: perm board.vote.cast — HOW STRICT the project runs its agents (task
1079
+ // 1004715): the three strictness presets (Open workshop, Guild, Factory), which
1080
+ // one the laws in force amount to (read, never stored: null when none), and for
1081
+ // each its laws and the locks a switch to it would leave in force. A read.
1082
+ router.get('/government/presets', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
1083
+ try {
1084
+ res.json(presets.presetsView(await db.listLaws()));
1085
+ } catch (err) {
1086
+ log.error('[gds] GET /government/presets', err);
1087
+ res.fail('presets_read_failed', { status: 500, message: 'internal error' });
1088
+ }
1089
+ });
1090
+
1091
+ // rank: perm board.item.open — SWITCH the project's strictness preset (task
1092
+ // 1004715). A switch is a vote: ONE laws amendment (the same path as POST
1093
+ // /government/board/laws) that enacts the preset's laws not in force and repeals
1094
+ // the other presets' laws that are. Nothing here writes a law; only the sitting
1095
+ // passing does. The same atom as proposing any law.
1096
+ //
1097
+ // Body: { preset: 'open' | 'guild' | 'factory' }
1098
+ router.post('/government/board/presets', api.requireBuilder, api.requirePermission('board.item.open'), async (req, res) => {
1099
+ if (validateOrRespond(req, res, { preset: { type: 'string', required: true, maxLength: 32 } })) return;
1100
+ try {
1101
+ const sw = presets.switchProposal(req.body.preset, await db.listLaws());
1102
+ if (!sw.ok) {
1103
+ if (sw.reason === 'preset_in_force') return res.fail('preset_in_force', 409, { preset: req.body.preset });
1104
+ return res.fail('invalid_preset', 400, { reason: sw.reason, ...(sw.expected ? { expected: sw.expected } : {}) });
1105
+ }
1106
+ const out = await board.proposeLaws({ ...sw.proposal, rationaleMd: sw.rationale, proposedBy: req.builder.id });
1107
+ if (!out.ok) {
1108
+ if (out.reason === 'already_in_force' || out.reason === 'setting_already_locked' || out.reason === 'not_in_force') {
1109
+ return res.fail(out.reason, 409, { ...(out.law_id ? { law_id: out.law_id } : {}), ...(out.setting ? { setting: out.setting } : {}) });
1110
+ }
1111
+ return res.fail('invalid_preset', 400, { reason: out.reason });
1112
+ }
1113
+ res.status(201).json({ ok: true, preset: req.body.preset, amendment: out.amendment, item: out.item, proposal: out.proposal });
1114
+ } catch (err) {
1115
+ log.error('[gds] POST /government/board/presets', err);
1116
+ res.fail('propose_failed', { status: 500, message: 'internal error' });
1117
+ }
1118
+ });
1119
+
1120
+ // rank: perm board.vote.cast — the Lead of each building (task 1004709): the
1121
+ // nine buildings in street order, each with its Lead in office (who, their
1122
+ // charter, since when, the sitting that appointed them) or null, and who answers
1123
+ // for it — its Lead, or the owner (every active Archon, the founding one first)
1124
+ // when it is unseated.
1125
+ // ?include=past adds the appointments that have ended. A read; a Lead is
1126
+ // appointed and removed only by a passed People matter (POST
1127
+ // /government/board/matters, action appoint_lead or remove_lead).
1128
+ router.get('/government/leads', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
1129
+ try {
1130
+ res.json(await board.leadsView({ includePast: req.query.include === 'past' }));
1131
+ } catch (err) {
1132
+ log.error('[gds] GET /government/leads', err);
1133
+ res.fail('leads_read_failed', { status: 500, message: 'internal error' });
1134
+ }
1135
+ });
1136
+
844
1137
  // rank: perm board.item.open — put a MATTER to a building's sitting (task
845
1138
  // 1004529): the board's new subjects, one per building other than the
846
1139
  // charter. Refused unless the building's row names a sitting — under the
@@ -849,7 +1142,12 @@ module.exports = function buildGovernmentRouter() {
849
1142
  //
850
1143
  // Body: { area, title, detail_md?, action? }
851
1144
  // action (People only): { type: 'assign_rank' | 'unassign_rank', builder_id, rank_key }
852
- // — carried out when the sitting passes, under the same walls as the direct route.
1145
+ // — carried out when the sitting passes, under the same walls as the direct route;
1146
+ // or { type: 'appoint_lead', building, builder_id, charter? } | { type: 'remove_lead', building }
1147
+ // (task 1004709) — the Lead of a building. Where People is decided as today
1148
+ // this one matter sits under the board in force instead of refusing.
1149
+ // Or, at any building, { type: 'build_out', building, capsule, name?, line? } (task
1150
+ // 1004711) — a build-out put to a vote; POST /government/buildouts files it for the holder.
853
1151
  router.post('/government/board/matters', api.requireBuilder, api.requirePermission('board.item.open'), async (req, res) => {
854
1152
  if (validateOrRespond(req, res, {
855
1153
  area: { required: true, type: 'string', maxLength: 32 },
@@ -869,11 +1167,17 @@ module.exports = function buildGovernmentRouter() {
869
1167
  if (out.reason === 'decided_by_gate' || out.reason === 'no_one_holds_this' || out.reason === 'action_not_wired') {
870
1168
  return res.fail(out.reason, 409, { area: out.area, ...(out.gate ? { gate: out.gate } : {}), what_this_means: out.what_this_means });
871
1169
  }
1170
+ if (out.reason === 'no_lead' || out.reason === 'already_lead') {
1171
+ return res.fail(out.reason, 409, { area: out.area, building: out.building, what_this_means: out.what_this_means });
1172
+ }
1173
+ // task 1004711: a build-out is the building holder's to ask for, by this route too
1174
+ if (out.reason === 'not_the_holder') return res.fail(out.reason, 403, { building: out.building, what_this_means: out.what_this_means });
872
1175
  return res.fail('invalid_matter', 400, {
873
1176
  reason: out.reason,
874
1177
  ...(out.field ? { field: out.field } : {}),
875
1178
  ...(out.expected ? { expected: out.expected } : {}),
876
1179
  ...(out.rank_key ? { rank_key: out.rank_key } : {}),
1180
+ ...(out.builder_id ? { builder_id: out.builder_id } : {}),
877
1181
  });
878
1182
  }
879
1183
  res.status(201).json({ ok: true, matter: out.matter, item: out.item, decided: out.decided });
@@ -883,6 +1187,125 @@ module.exports = function buildGovernmentRouter() {
883
1187
  }
884
1188
  });
885
1189
 
1190
+ // rank: perm board.vote.cast — a Lead's BUILD-OUT (task 1004711, ADR 0378): a
1191
+ // committee room, a new floor, an annex, or growing the building into a
1192
+ // district. The coarse atom is the room's (every builder who reads the Leads);
1193
+ // the wall is server-side and per request: only the building's HOLDER — its
1194
+ // Lead in office, or the owner seat (every active Archon) when it is unseated
1195
+ // — may ask (403 not_the_holder). build_rights (build-rights.js, the one rule)
1196
+ // then decides: at once → 201 { path: 'at_once', buildout }, on the record now
1197
+ // and drawn as a live site; otherwise → 201 { path: 'vote', matter, item },
1198
+ // filed as a build_out matter through the same openMatter the matters route
1199
+ // runs, drawn as a ghost with the sitting's tally until it closes.
1200
+ //
1201
+ // Body: { building, capsule: committee|floor|annex|district, name?, line? }
1202
+ router.post('/government/buildouts', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
1203
+ if (validateOrRespond(req, res, {
1204
+ building: { required: true, type: 'string', maxLength: 32 },
1205
+ capsule: { required: true, type: 'string', maxLength: 32 },
1206
+ name: { type: 'string', maxLength: 80 },
1207
+ line: { type: 'string', maxLength: 280 },
1208
+ })) return;
1209
+ try {
1210
+ const out = await board.buildOut({
1211
+ buildout: { building: req.body.building, capsule: req.body.capsule, name: req.body.name, line: req.body.line },
1212
+ builderId: req.builder.id,
1213
+ });
1214
+ if (!out.ok) {
1215
+ if (out.reason === 'not_the_holder') return res.fail(out.reason, 403, { building: out.building, what_this_means: out.what_this_means });
1216
+ if (out.reason === 'decided_by_gate' || out.reason === 'no_one_holds_this') {
1217
+ return res.fail(out.reason, 409, { area: out.area, what_this_means: out.what_this_means });
1218
+ }
1219
+ return res.fail('invalid_buildout', 400, { reason: out.reason, ...(out.field ? { field: out.field } : {}) });
1220
+ }
1221
+ if (out.path === 'at_once') return res.status(201).json({ ok: true, path: out.path, buildout: out.buildout, rights: out.rights });
1222
+ res.status(201).json({ ok: true, path: out.path, matter: out.matter, item: out.item, decided: out.decided, rights: out.rights });
1223
+ } catch (err) {
1224
+ log.error('[gds] POST /government/buildouts', err);
1225
+ res.fail('buildout_failed', { status: 500, message: 'internal error' });
1226
+ }
1227
+ });
1228
+
1229
+ // rank: perm board.vote.cast — THE TREASURY (task 1004712, ADR 0379): this
1230
+ // month's real spend and last month's, the spend cap (a project setting, off
1231
+ // by default: no limit), the reserve, and every open goal's pot — its budget,
1232
+ // what the reserve lent it this month, its real spend this month (cost_log
1233
+ // through the goal's tasks) and its borrow, if it asked. Plus who answers for
1234
+ // the Treasury today. A read; the room's atom, as GET /government/leads.
1235
+ router.get('/government/treasury', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
1236
+ try {
1237
+ const [view, holder] = await Promise.all([treasuryDb.readTreasury(), treasuryHolder()]);
1238
+ res.json({ ...view, answers: holder ? { seat: holder.seat, label: holder.label, fell_to_owner: holder.fell_to_owner, yours: holder.builder_ids.includes(String(req.builder.id)) } : null });
1239
+ } catch (err) {
1240
+ log.error('[gds] GET /government/treasury', err);
1241
+ res.fail('treasury_read_failed', { status: 500, message: 'internal error' });
1242
+ }
1243
+ });
1244
+
1245
+ // rank: perm board.vote.cast — GIVE A GOAL A POT, change its budget, or take it
1246
+ // away (budget_usd: null). The room's atom; the wall is server-side and per
1247
+ // request: only whoever answers for the Treasury today — its Lead, or the owner
1248
+ // seat while it is unseated — may (403 not_the_holder). Only an open goal.
1249
+ //
1250
+ // Body: { budget_usd: number | null } → 200 { ok, pot | removed }
1251
+ router.put('/government/treasury/pots/:goalId', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
1252
+ const goalId = parseId(req, res, { param: 'goalId', code: 'bad_goal_id', positiveInt: true });
1253
+ if (goalId === null) return;
1254
+ if (validateOrRespond(req, res, { budget_usd: {} })) return; // a number of dollars, or null to take the pot away; parsed below
1255
+ const parsed = treasury.parseBudget(req.body ? req.body.budget_usd : undefined);
1256
+ if (parsed.reason) return res.fail('bad_budget', { status: 400, message: parsed.reason });
1257
+ try {
1258
+ const holder = await treasuryHolder();
1259
+ if (!holder) return res.fail('seats_unavailable', { status: 503, message: 'Who answers for the Treasury could not be read just now, so nothing was set. Try again.' });
1260
+ if (!holder.builder_ids.includes(String(req.builder.id))) {
1261
+ return res.fail('not_the_holder', 403, { building: 'treasury', what_this_means: `Only ${holder.label} sets a goal's pot.` });
1262
+ }
1263
+ const goal = await treasuryDb.goalById(goalId);
1264
+ if (!goal) return res.fail('goal_not_found', { status: 404, message: 'No goal by that id.' });
1265
+ if (goal.status !== 'open' && parsed.budget != null) return res.fail('goal_not_open', { status: 409, message: 'Only an open goal has a pot.' });
1266
+ const out = await treasuryDb.setPot({ goalId, budget: parsed.budget, builderId: req.builder.id });
1267
+ res.json({ ok: true, goal_id: String(goalId), ...(out.pot ? { pot: { goal_id: String(out.pot.goal_id), budget_usd: Number(out.pot.budget_usd), set_at: out.pot.set_at } } : { removed: out.removed }) });
1268
+ } catch (err) {
1269
+ log.error('[gds] PUT /government/treasury/pots/:goalId', err);
1270
+ res.fail('pot_set_failed', { status: 500, message: 'internal error' });
1271
+ }
1272
+ });
1273
+
1274
+ // rank: perm board.vote.cast — ANSWER A BORROW: lend it all, lend half, or say
1275
+ // no. The room's atom; the wall is the direction desk's: only whoever the
1276
+ // borrow's ask waits on today may answer it (routing.routeMatter over real
1277
+ // seats: the Treasury's Lead, or the owner seat while it is unseated, or the
1278
+ // seat it was passed to). Answered once; a second answer is 409. Saying no
1279
+ // stops nothing: with no spend limit, work runs on and the overspend is debt.
1280
+ //
1281
+ // Body: { key: 'lend' | 'half' | 'no' } → 200 { ok, borrow }
1282
+ router.post('/government/treasury/borrows/:borrowId/answer', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
1283
+ const borrowId = parseId(req, res, { param: 'borrowId', code: 'bad_borrow_id', positiveInt: true });
1284
+ if (borrowId === null) return;
1285
+ if (validateOrRespond(req, res, { key: { required: true, type: 'string', maxLength: 16 } })) return;
1286
+ const key = req.body && typeof req.body.key === 'string' ? req.body.key.trim() : '';
1287
+ if (!treasury.ANSWERS.includes(key)) return res.fail('bad_answer', { status: 400, message: 'Answer lend, half or no.' });
1288
+ try {
1289
+ const borrow = await treasuryDb.getBorrow(borrowId);
1290
+ if (!borrow) return res.fail('borrow_not_found', { status: 404, message: 'No borrow by that id.' });
1291
+ if (borrow.state !== 'open') return res.fail('already_answered', { status: 409, message: 'This borrow was already answered.' });
1292
+ const found = await docket.matterWithSeats(req.builder, treasury.matterId(borrowId));
1293
+ if (!found.matter) {
1294
+ if (found.reason === 'unavailable') return res.fail('matter_unavailable', { status: 503, message: 'That borrow could not be read just now, so nothing was answered. Try again.' });
1295
+ return res.fail('borrow_not_found', { status: 404, message: 'No open borrow by that id that you can see.' });
1296
+ }
1297
+ const route = routing.routeMatter(found.matter, req.builder.id, found.seats);
1298
+ if (!route.yours) return res.fail('not_yours', { status: 403, message: `Only ${route.label} answers this borrow.` });
1299
+ const written = await treasuryDb.answerBorrow({ id: borrowId, answer: treasury.answerOf(borrow, key), builderId: req.builder.id });
1300
+ if (!written) return res.fail('already_answered', { status: 409, message: 'This borrow was already answered.' });
1301
+ log.info(`[gds] treasury borrow ${borrowId} (goal ${borrow.goal_id}) answered ${key} by builder ${req.builder.id}`);
1302
+ res.json({ ok: true, borrow: treasury.borrowView(written) });
1303
+ } catch (err) {
1304
+ log.error('[gds] POST /government/treasury/borrows/:borrowId/answer', err);
1305
+ res.fail('borrow_answer_failed', { status: 500, message: 'internal error' });
1306
+ }
1307
+ });
1308
+
886
1309
  // rank: perm board.item.open — withdraw your own open sitting (task 1004564).
887
1310
  // The same atom as putting one up: taking it back is the proposer's half of
888
1311
  // the same act. The atom is only the coarse gate; the walls are in
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+
3
+ // modules/government/routing.js — THE DIRECTION DESK: every ask routed to a
4
+ // real seat (task 1004705, goal 1000111 "government is how a project steers its
5
+ // AI"; ADR 0376).
6
+ //
7
+ // WHAT IT DECIDES. Which seat an open docket matter waits on, who sits in that
8
+ // seat today, and so whether it is THIS viewer's to answer ("to do") or someone
9
+ // else's ("waits on others"). The hall used to answer that from the prototype
10
+ // overlay; the server answers it now, from real seats, and the docket carries the
11
+ // answer on every matter as `route`.
12
+ //
13
+ // THE SEAT A MATTER WAITS ON.
14
+ // 1. Its stored ask's `waits_on` (asks.js): the seat its agent named, or the
15
+ // one somebody it waited on passed it to (POST /government/asks/:matter/pass).
16
+ // 2. With no stored ask, the seat that answers for its building: a sitting of
17
+ // the board (source 'board') waits on `board:<building>`, its voters;
18
+ // anything else on `lead:<building>`.
19
+ //
20
+ // WHO SITS IN IT (the seat as resolved; never a guess):
21
+ // owner every active Archon (leads.js ownerOf)
22
+ // lead:<b> its Lead in office; an UNSEATED building falls to the
23
+ // owner (owner ruling) — leads.answerFor, written once
24
+ // board:<b> the members its decision-rules row seats today
25
+ // (docket.loadGovernment holders); nobody seated →
26
+ // falls to the owner. A board sitting with no ask of
27
+ // its own is its voters': the docket's per-voter
28
+ // `needs` (board.awaitsVoter) already says whose it is.
29
+ // committee:<b>.<name> no committee is held in a table yet, so it falls to
30
+ // the building's Lead (and so, unseated, to the owner).
31
+ //
32
+ // IF NOBODY ANSWERS. An ask past its matter's deadline, or one whose seat nobody
33
+ // holds (no Archon, nobody seated), is on nobody's "to do": it does what its own
34
+ // `if_nobody_answers` line says, and the route carries that line as
35
+ // `nobody.outcome`, so every surface prints the same words.
36
+ //
37
+ // PURE: no db. docket.js reads the seats (leads, Archons, holders) once per
38
+ // request, uncached (ADR 0016), and routes every matter with routeAll.
39
+
40
+ const { answerFor } = require('./leads');
41
+ const asks = require('./asks');
42
+
43
+ const BUILDING_NAMES = Object.freeze({
44
+ charter: 'Charter', rules: 'Rules', disputes: 'Disputes', people: 'People', planning: 'Planning',
45
+ automation: 'Automation', review: 'Review', release: 'Release', treasury: 'Treasury',
46
+ });
47
+ const isBuilding = (b) => Object.prototype.hasOwnProperty.call(BUILDING_NAMES, b);
48
+
49
+ // The seat a matter waits on before anything falls: its stored ask's, else its building's.
50
+ function seatOf(matter) {
51
+ const m = matter || {};
52
+ const stored = m.ask && typeof m.ask.waits_on === 'string' && asks.SEAT.test(m.ask.waits_on) ? m.ask.waits_on : null;
53
+ if (stored) return stored;
54
+ const b = isBuilding(m.building) ? m.building : 'planning';
55
+ return m.source === 'board' ? `board:${b}` : `lead:${b}`;
56
+ }
57
+
58
+ // How a seat reads, after "waits on".
59
+ function seatLabel(seat) {
60
+ const s = String(seat || '');
61
+ if (s === 'owner') return 'the owner';
62
+ const m = /^(lead|board):([a-z]+)$/.exec(s);
63
+ if (m) return `the ${BUILDING_NAMES[m[2]] || m[2]} ${m[1] === 'lead' ? 'Lead' : 'board'}`;
64
+ return asks.seatWords(s);
65
+ }
66
+
67
+ const idsOf = (list) => (Array.isArray(list) ? list : [])
68
+ .map((p) => (p && typeof p === 'object' ? (p.builder_id ?? p.id) : p))
69
+ .filter((id) => id != null)
70
+ .map(String);
71
+
72
+ // resolveSeat(asked, ctx) → { asked, seat, label, builder_ids, fell_to_owner }
73
+ // asked the seat the matter names
74
+ // seat the seat that answers it, after any fall
75
+ // builder_ids who sits there now ([] when nobody does)
76
+ // ctx { leads: Lead rows in office, owner: leads ownerOf(archons) shape
77
+ // { builders: [{ id }] }, holders: { building: [people] | null } }
78
+ function resolveSeat(asked, ctx) {
79
+ const c = ctx || {};
80
+ const owner = c.owner || { builders: [] };
81
+ const ownerIds = idsOf(owner.builders);
82
+ const seated = (seat, ids) => ({ asked, seat, label: seatLabel(seat), builder_ids: ids, fell_to_owner: false });
83
+ const toOwner = () => ({ asked, seat: 'owner', label: seatLabel('owner'), builder_ids: ownerIds, fell_to_owner: true });
84
+ if (asked === 'owner') return seated('owner', ownerIds);
85
+ let m = /^lead:([a-z]+)$/.exec(String(asked || ''));
86
+ if (m) {
87
+ const a = answerFor(m[1], c.leads || [], owner);
88
+ return !a || a.fell_to_owner ? toOwner() : seated(a.seat, a.builder_ids.map(String));
89
+ }
90
+ if ((m = /^board:([a-z]+)$/.exec(String(asked || '')))) {
91
+ const held = idsOf((c.holders || {})[m[1]]);
92
+ return held.length ? seated(asked, held) : toOwner();
93
+ }
94
+ if ((m = /^committee:([a-z]+)\.[a-z0-9_]+$/.exec(String(asked || '')))) {
95
+ return { ...resolveSeat(`lead:${m[1]}`, c), asked };
96
+ }
97
+ return toOwner();
98
+ }
99
+
100
+ // routeMatter(matter, viewerId, ctx, now) → the route the docket carries:
101
+ // { seat, label, asked, fell_to_owner, yours, nobody: null | { why, outcome } }
102
+ // `yours` is the one answer to "is this on me": an open matter that needs a
103
+ // person, on a seat the viewer sits in, that somebody can still answer.
104
+ function routeMatter(matter, viewerId, ctx, now = Date.now()) {
105
+ const m = matter || {};
106
+ const needs = m.state === 'needs';
107
+ // a sitting's voters are the item's own (its snapshot), never a fall to the owner
108
+ const ownVote = m.source === 'board' && !m.ask;
109
+ const asked = seatOf(m);
110
+ const r = ownVote ? { asked, seat: asked, label: seatLabel(asked), builder_ids: [], fell_to_owner: false } : resolveSeat(asked, ctx);
111
+ const deadline = m.deadline ? Date.parse(m.deadline) : NaN;
112
+ const outcome = m.ask && m.ask.if_nobody_answers ? String(m.ask.if_nobody_answers) : null;
113
+ let nobody = null;
114
+ if (needs && Number.isFinite(deadline) && deadline <= now) nobody = { why: 'lapsed', outcome };
115
+ else if (needs && !ownVote && !r.builder_ids.length) nobody = { why: 'empty_seat', outcome };
116
+ const viewer = viewerId == null ? null : String(viewerId);
117
+ const sits = ownVote ? needs : r.builder_ids.includes(viewer);
118
+ return {
119
+ seat: r.seat,
120
+ label: r.label,
121
+ asked: r.asked,
122
+ fell_to_owner: r.fell_to_owner,
123
+ yours: needs && !nobody && !!viewer && sits,
124
+ nobody,
125
+ };
126
+ }
127
+
128
+ // Route every matter in place; → how many are the viewer's.
129
+ function routeAll(matters, viewerId, ctx, now = Date.now()) {
130
+ let yours = 0;
131
+ for (const m of matters || []) {
132
+ m.route = routeMatter(m, viewerId, ctx, now);
133
+ if (m.route.yours) yours += 1;
134
+ }
135
+ return yours;
136
+ }
137
+
138
+ // May this viewer pass this matter's ask on to `toSeat`? → null (yes) or a
139
+ // refusal { code, status, message }. Only whoever the ask waits on may hand it
140
+ // to another seat; a board sitting is its voters' and is never passed; the new
141
+ // seat must be a seat, and a different one.
142
+ function passRefusal(matter, viewerId, toSeat, ctx, now = Date.now()) {
143
+ const m = matter || {};
144
+ if (!m.ask) return { code: 'no_ask', status: 409, message: 'This matter has no ask to pass on yet.' };
145
+ if (m.source === 'board') return { code: 'vote_not_passable', status: 409, message: 'A vote is its voters\' to answer; it cannot be passed on.' };
146
+ const to = String(toSeat || '').trim();
147
+ if (!asks.SEAT.test(to)) return { code: 'unknown_seat', status: 400, message: 'Name a seat: owner, lead:<building>, committee:<building>.<name> or board:<building>.' };
148
+ const route = routeMatter(m, viewerId, ctx, now);
149
+ if (!route.yours) return { code: 'not_yours', status: 403, message: 'Only whoever this waits on can pass it on.' };
150
+ if (to === m.ask.waits_on || resolveSeat(to, ctx).seat === route.seat) return { code: 'same_seat', status: 409, message: 'It already waits on that seat.' };
151
+ return null;
152
+ }
153
+
154
+ module.exports = { seatOf, seatLabel, resolveSeat, routeMatter, routeAll, passRefusal, BUILDING_NAMES };