@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
@@ -0,0 +1,375 @@
1
+ // tests/government_leads.mjs — THE LEAD OF EACH BUILDING: who leads it, held as a
2
+ // position the government appoints and removes by vote (task 1004709, goal
3
+ // 1000111; ADR 0374).
4
+ //
5
+ // What this pins (the task's done-when lines, and the walls behind them):
6
+ // 1. A LEAD IS APPOINTED AND REMOVED BY VOTE. appoint_lead / remove_lead are
7
+ // People matter actions, validated strictly (a building of the nine, an
8
+ // active builder, a one-line charter or the building's default). Where
9
+ // People names a sitting the matter sits there; where People is decided as
10
+ // today it sits under the board IN FORCE (the government appoints), never
11
+ // refused; a vacant People refuses. A passed matter seats or unseats the
12
+ // Lead; a returned one does nothing; a departed appointee is not seated.
13
+ // 2. A LEAD SHOWS ON THEIR BUILDING. GET /government/leads reads the nine
14
+ // buildings, each with its Lead in office (who, charter, since, the sitting).
15
+ // 3. UNSEATED BUILDINGS ROUTE TO THE OWNER. answerFor (the one rule) and the
16
+ // read say who answers for each building: its Lead, else the owner seat
17
+ // (every active Archon, the founding one — the lowest id — first).
18
+ // 4. NOTHING ELSE WRITES A LEAD. One SQL writer (db.applyLeadAct), called from
19
+ // one place (decision-board.js), only for a passed People matter;
20
+ // government_029's trigger refuses every other INSERT/UPDATE and every
21
+ // DELETE; no Lead is seeded; every government route, driven for real,
22
+ // reaches no write.
23
+ // DB-free: db reads/writes are monkeypatched by property.
24
+ //
25
+ // Run: node --test tests/government_leads.mjs
26
+
27
+ import { strict as assert } from 'node:assert';
28
+ import { test, after } from 'node:test';
29
+ import http from 'node:http';
30
+ import fs from 'node:fs';
31
+ import path from 'node:path';
32
+ import { createRequire } from 'node:module';
33
+ import { fileURLToPath } from 'node:url';
34
+
35
+ const require = createRequire(import.meta.url);
36
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
37
+ const GOV = path.join(ROOT, 'modules', 'government');
38
+ const leads = require(path.join(GOV, 'leads.js'));
39
+ const decisionBoard = require(path.join(GOV, 'decision-board.js'));
40
+ const board = require(path.join(GOV, 'board.js'));
41
+ const dr = require(path.join(GOV, 'decision-rules.js'));
42
+ const db = require(path.join(GOV, 'db.js'));
43
+
44
+ const MIGRATION = fs.readFileSync(path.join(GOV, 'migrations', 'government_029_leads.sql'), 'utf8');
45
+ const MIGRATION_CODE = MIGRATION.replace(/--[^\n]*/g, '');
46
+
47
+ async function withDb(stubs, fn) {
48
+ const originals = {};
49
+ for (const [k, v] of Object.entries(stubs)) { originals[k] = db[k]; db[k] = v; }
50
+ try { return await fn(); } finally { for (const [k, v] of Object.entries(originals)) db[k] = v; }
51
+ }
52
+ const never = async () => { throw new Error('must not be reached'); };
53
+ const sittingRow = (area, decidedBy, rule) => ({ area, decided_by: decidedBy, rule, who: null, set_by_item_id: '1' });
54
+ const ARCHONS = [
55
+ { id: '9', github_login: 'late', display_name: 'Late Archon' },
56
+ { id: '3', github_login: 'ada', display_name: 'Ada' },
57
+ ];
58
+ const DEE = { id: '45', github_login: 'dee', display_name: 'Dee' };
59
+ const leadRow = (over = {}) => ({
60
+ id: '1', building: 'planning', builder_id: '45', charter: 'Goals, priorities and the order of work.', appointed_by_item_id: '40',
61
+ appointed_at: '2026-10-09T12:00:00.000Z', removed_by_item_id: null, removed_at: null, github_login: 'dee', display_name: 'Dee', ...over,
62
+ });
63
+
64
+ // ── 1. the shape of an appointment ─────────────────────────────────────────
65
+
66
+ test('appoint_lead and remove_lead are People matter actions, validated strictly; a charter is one line or the building\'s default', () => {
67
+ const v = (a) => decisionBoard.validateMatterAction('people', a);
68
+ assert.deepEqual(v({ type: 'appoint_lead', building: 'planning', builder_id: '45', charter: ' Goals first. ' }).action,
69
+ { type: 'appoint_lead', building: 'planning', builder_id: 45, charter: 'Goals first.' }, 'trimmed, the id a number');
70
+ assert.equal(v({ type: 'appoint_lead', building: 'treasury', builder_id: 45 }).action.charter, leads.DEFAULT_CHARTERS.treasury, 'no charter: the building\'s own');
71
+ assert.deepEqual(v({ type: 'remove_lead', building: 'review' }).action, { type: 'remove_lead', building: 'review' });
72
+ for (const k of dr.AREA_KEYS) assert.ok(leads.DEFAULT_CHARTERS[k] && leads.charterLine(leads.DEFAULT_CHARTERS[k], k), `${k} has a default charter that keeps the rule`);
73
+ const refused = [
74
+ [{ type: 'appoint_lead', building: 'kitchen', builder_id: 45 }, 'action.building'],
75
+ [{ type: 'appoint_lead', building: 'planning', builder_id: 0 }, 'action.builder_id'],
76
+ [{ type: 'appoint_lead', building: 'planning', builder_id: 45, charter: 'no' }, 'action.charter'],
77
+ [{ type: 'appoint_lead', building: 'planning', builder_id: 45, charter: 'x'.repeat(leads.CHARTER_MAX + 1) }, 'action.charter'],
78
+ [{ type: 'appoint_lead', building: 'planning', builder_id: 45, charter: 'Two\nlines' }, 'action.charter'],
79
+ [{ type: 'appoint_lead', building: 'planning', builder_id: 45, can_build_without_vote: true }, 'action.can_build_without_vote'],
80
+ [{ type: 'remove_lead' }, 'action.building'],
81
+ ];
82
+ for (const [action, field] of refused) {
83
+ const out = v(action);
84
+ assert.equal(out.ok, false, JSON.stringify(action));
85
+ assert.equal(out.field, field, JSON.stringify(out));
86
+ }
87
+ assert.equal(decisionBoard.validateMatterAction('rules', { type: 'appoint_lead', building: 'rules', builder_id: 45 }).ok, false, 'a Lead is a People matter, at no other building');
88
+ });
89
+
90
+ // ── 1. filed as a People matter, decided by the government ─────────────────
91
+
92
+ test('People decided as today: a Lead matter sits under the board IN FORCE, never refused as "decided by the gate"', async () => {
93
+ let made = null; let opened = null;
94
+ await withDb({
95
+ listDecisionRules: async () => [],
96
+ listLeads: async () => [],
97
+ getActiveBuilder: async (id) => (String(id) === '45' ? DEE : null),
98
+ createMatter: async (args) => { made = args; return { id: '8', ...args }; },
99
+ openBoardItem: async (args) => { opened = args; return { id: '40', subject_type: args.subjectType, subject_id: args.subjectId, constitution: args.constitution }; },
100
+ }, async () => {
101
+ const out = await board.openMatter({ area: 'people', title: 'Dee leads Planning', action: { type: 'appoint_lead', building: 'planning', builder_id: 45 }, proposedBy: '42' });
102
+ assert.equal(out.ok, true, JSON.stringify(out));
103
+ assert.equal(out.decided.sat_under, 'board');
104
+ // a rank assignment there is still decided on its own page, as before
105
+ const rank = await board.openMatter({ area: 'people', title: 'Make Dee a tester', action: { type: 'assign_rank', builder_id: 45, rank_key: 'tester' }, proposedBy: '42' });
106
+ assert.equal(rank.reason, 'decided_by_gate', 'only the Lead acts take the board');
107
+ });
108
+ assert.equal(made.area, 'people');
109
+ assert.deepEqual(made.action, { type: 'appoint_lead', building: 'planning', builder_id: 45, charter: leads.DEFAULT_CHARTERS.planning });
110
+ assert.equal(opened.subjectType, 'people_matter', 'a People matter, on the board');
111
+ assert.equal(opened.constitution.pass_rule, 'first_ratifier', 'decided under the board in force (this checkout: monarchy)');
112
+ assert.equal(opened.constitution.membership, 'rank:archon');
113
+ });
114
+
115
+ test('People handed to a sitting: the Lead matter sits under the ROW; a vacant People cannot appoint', async () => {
116
+ let opened = null;
117
+ await withDb({
118
+ listDecisionRules: async () => [sittingRow('people', 'rank:council', 'consent')],
119
+ listLeads: async () => [],
120
+ getActiveBuilder: async () => DEE,
121
+ createMatter: async (args) => ({ id: '8', ...args }),
122
+ openBoardItem: async (args) => { opened = args; return { id: '40', subject_type: args.subjectType, subject_id: args.subjectId, constitution: args.constitution }; },
123
+ }, async () => {
124
+ const out = await board.openMatter({ area: 'people', title: 'Dee leads Planning', action: { type: 'appoint_lead', building: 'planning', builder_id: 45 }, proposedBy: '42' });
125
+ assert.equal(out.ok, true, JSON.stringify(out));
126
+ });
127
+ assert.equal(opened.constitution.membership, 'rank:council');
128
+ assert.equal(opened.constitution.rule, 'consent');
129
+ await withDb({ listDecisionRules: async () => [sittingRow('people', 'nobody', 'vacant')], listLeads: async () => [], getActiveBuilder: async () => DEE, createMatter: never, openBoardItem: never }, async () => {
130
+ assert.equal((await board.openMatter({ area: 'people', title: 'Dee leads Planning', action: { type: 'appoint_lead', building: 'planning', builder_id: 45 }, proposedBy: '42' })).reason, 'no_one_holds_this');
131
+ });
132
+ });
133
+
134
+ test('refused before any write: an appointee who is not an active builder, a Lead already in office on that charter, removing nobody', async () => {
135
+ await withDb({
136
+ listDecisionRules: async () => [],
137
+ listLeads: async () => [leadRow()],
138
+ getActiveBuilder: async (id) => (String(id) === '45' ? DEE : null),
139
+ createMatter: never,
140
+ openBoardItem: never,
141
+ }, async () => {
142
+ const open = (action) => board.openMatter({ area: 'people', title: 'A Lead', action, proposedBy: '42' });
143
+ assert.equal((await open({ type: 'appoint_lead', building: 'planning', builder_id: 77 })).reason, 'unknown_builder');
144
+ assert.equal((await open({ type: 'appoint_lead', building: 'planning', builder_id: 45 })).reason, 'already_lead');
145
+ assert.equal((await open({ type: 'remove_lead', building: 'review' })).reason, 'no_lead');
146
+ });
147
+ // a new charter for the same Lead, or someone else, is a fresh appointment
148
+ let filed = 0;
149
+ await withDb({
150
+ listDecisionRules: async () => [],
151
+ listLeads: async () => [leadRow()],
152
+ getActiveBuilder: async () => DEE,
153
+ createMatter: async (a) => { filed += 1; return { id: String(8 + filed), ...a }; },
154
+ openBoardItem: async (a) => ({ id: '41', subject_type: a.subjectType, subject_id: a.subjectId, constitution: a.constitution }),
155
+ }, async () => {
156
+ assert.equal((await board.openMatter({ area: 'people', title: 'A new charter', action: { type: 'appoint_lead', building: 'planning', builder_id: 45, charter: 'Goals only.' }, proposedBy: '42' })).ok, true);
157
+ assert.equal((await board.openMatter({ area: 'people', title: 'Dee steps down', action: { type: 'remove_lead', building: 'planning' }, proposedBy: '42' })).ok, true);
158
+ });
159
+ assert.equal(filed, 2);
160
+ });
161
+
162
+ // ── 1. passed: carried out; returned: nothing ──────────────────────────────
163
+
164
+ function closeWith({ action, outcome = 'passed', active = true, stubs = {} }) {
165
+ const writes = [];
166
+ const run = withDb({
167
+ getVotesForItem: async () => [{ voter_id: '9', direction: outcome === 'passed' ? 'yes' : 'no' }],
168
+ closeBoardItem: async ({ outcome: o, afterClose }) => { assert.equal(afterClose, null, 'a matter books nothing in the close'); return { id: '40', subject_type: 'people_matter', subject_id: '8', opened_by: '42', closed_at: 'T', outcome: o }; },
169
+ getMatter: async () => ({ id: '8', area: 'people', title: 'A Lead', action, proposed_by: '42' }),
170
+ getActiveBuilder: async () => (active ? DEE : null),
171
+ applyLeadAct: async (args) => { writes.push(args); return { done: true, lead: null, ended: null }; },
172
+ insertAuthorRatifyKarma: never,
173
+ getBoardItemAuthor: async () => '42',
174
+ ...stubs,
175
+ }, async () => {
176
+ const item = { id: '40', subject_type: 'people_matter', subject_id: '8', opened_by: '42', closed_at: null, constitution: { membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true } };
177
+ return board.closeItem(item);
178
+ });
179
+ return run.then((out) => ({ out, writes }));
180
+ }
181
+
182
+ test('a passed appointment seats the Lead, through the one write, naming the sitting that passed it', async () => {
183
+ const action = { type: 'appoint_lead', building: 'planning', builder_id: 45, charter: 'Goals, priorities and the order of work.' };
184
+ const { out, writes } = await closeWith({ action });
185
+ assert.equal(out.outcome, 'passed');
186
+ assert.equal(out.author_karma, null, 'a matter pays nobody');
187
+ assert.deepEqual(out.acted, { area: 'people', matter_id: '8', action: 'appoint_lead', done: true, building: 'planning', builder_id: '45' });
188
+ assert.deepEqual(writes, [{ itemId: '40', action }]);
189
+ });
190
+
191
+ test('a passed removal unseats the Lead; a returned matter and a departed appointee seat nobody', async () => {
192
+ const removed = await closeWith({ action: { type: 'remove_lead', building: 'planning' } });
193
+ assert.equal(removed.out.acted.done, true);
194
+ assert.deepEqual(removed.writes, [{ itemId: '40', action: { type: 'remove_lead', building: 'planning' } }]);
195
+
196
+ const returned = await closeWith({ action: { type: 'appoint_lead', building: 'planning', builder_id: 45, charter: 'Goals first.' }, outcome: 'returned' });
197
+ assert.equal(returned.out.outcome, 'returned');
198
+ assert.deepEqual(returned.writes, [], 'a returned matter carries out nothing');
199
+
200
+ const gone = await closeWith({ action: { type: 'appoint_lead', building: 'planning', builder_id: 45, charter: 'Goals first.' }, active: false });
201
+ assert.equal(gone.out.outcome, 'passed', 'the decision stands');
202
+ assert.equal(gone.out.acted.done, false);
203
+ assert.equal(gone.out.acted.reason, 'builder_gone');
204
+ assert.deepEqual(gone.writes, [], 'a builder who left is not seated');
205
+
206
+ const nobody = await closeWith({ action: { type: 'remove_lead', building: 'planning' }, stubs: { applyLeadAct: async () => ({ done: false, reason: 'no_lead' }) } });
207
+ assert.equal(nobody.out.acted.done, false);
208
+ assert.equal(nobody.out.acted.reason, 'no_lead', 'someone else removed them first: said, not hidden');
209
+ });
210
+
211
+ // ── 2 + 3. who leads, who answers ──────────────────────────────────────────
212
+
213
+ test('answerFor is the one rule: a building\'s Lead answers for it; an unseated building falls to the owner', () => {
214
+ const owner = { seat: 'owner', builders: [{ id: '3' }, { id: '9' }] };
215
+ assert.deepEqual(leads.answerFor('planning', [leadRow()], owner), { seat: 'lead:planning', builder_ids: ['45'], fell_to_owner: false });
216
+ assert.deepEqual(leads.answerFor('review', [leadRow()], owner), { seat: 'owner', builder_ids: ['3', '9'], fell_to_owner: true });
217
+ assert.deepEqual(leads.answerFor('review', [], { seat: 'owner', builders: [] }).builder_ids, [], 'no active Archon: nobody, never a guess');
218
+ assert.deepEqual(leads.answerFor('planning', [leadRow({ removed_at: 'T' })], owner).seat, 'owner', 'a Lead who left office answers for nothing');
219
+ assert.equal(leads.answerFor('kitchen', [], owner), null);
220
+ });
221
+
222
+ test('the read: nine buildings in street order, each Lead with their charter and sitting, the rest answered by the owner seat', () => {
223
+ const v = leads.leadsView([leadRow()], ARCHONS);
224
+ assert.deepEqual(v.buildings.map((b) => b.building), dr.AREA_KEYS.slice());
225
+ assert.deepEqual(v.owner.builders.map((b) => b.id), ['3', '9'], 'every active Archon holds the owner seat, the founding one first');
226
+ assert.deepEqual(v.owner.builders[0], { id: '3', login: 'ada', name: 'Ada' });
227
+ assert.equal(v.seated, 1);
228
+ const planning = v.buildings.find((b) => b.building === 'planning');
229
+ assert.deepEqual(planning.lead.builder, { id: '45', login: 'dee', name: 'Dee' });
230
+ assert.equal(planning.lead.charter, 'Goals, priorities and the order of work.');
231
+ assert.equal(planning.lead.since, '2026-10-09T12:00:00.000Z');
232
+ assert.deepEqual(planning.lead.appointed_by, { item_id: '40', href: '/board-room?item=40' });
233
+ assert.equal(planning.answers.seat, 'lead:planning');
234
+ for (const b of v.buildings.filter((x) => x.building !== 'planning')) {
235
+ assert.equal(b.lead, null, `${b.building}: unseated`);
236
+ assert.deepEqual(b.answers, { seat: 'owner', builder_ids: ['3', '9'], fell_to_owner: true }, `${b.building}: the owner answers`);
237
+ }
238
+ assert.match(v.changed_by, /passed People matter only/);
239
+ // the record keeps who led before
240
+ const past = leads.leadsView([leadRow({ removed_at: 'T2', removed_by_item_id: '44' })], ARCHONS, { includePast: true });
241
+ assert.equal(past.seated, 0);
242
+ assert.deepEqual(past.past[0].removed_by, { item_id: '44', href: '/board-room?item=44' });
243
+ });
244
+
245
+ // ── 4. the walls ────────────────────────────────────────────────────────────
246
+
247
+ test('government_029: a Lead is written only by a passed People matter that says so; no delete; one in office per building', () => {
248
+ assert.match(MIGRATION_CODE, /CREATE TABLE IF NOT EXISTS government_leads/);
249
+ assert.match(MIGRATION_CODE, /CREATE UNIQUE INDEX IF NOT EXISTS government_leads_in_office_uniq\s+ON government_leads \(building\) WHERE removed_at IS NULL/, 'one Lead in office per building');
250
+ assert.match(MIGRATION_CODE, /i\.subject_type = 'people_matter'\s+AND i\.outcome = 'passed'\s+AND m\.area = 'people'/, 'only a passed People matter');
251
+ assert.match(MIGRATION_CODE, /act->>'type' IS DISTINCT FROM 'appoint_lead'\s+OR act->>'building' IS DISTINCT FROM NEW\.building\s+OR act->>'builder_id' IS DISTINCT FROM NEW\.builder_id::text\s+OR act->>'charter' IS DISTINCT FROM NEW\.charter THEN\s+RAISE EXCEPTION/, 'an appointment is exactly what the matter said');
252
+ assert.match(MIGRATION_CODE, /act->>'type' NOT IN \('remove_lead', 'appoint_lead'\)\s+OR act->>'building' IS DISTINCT FROM OLD\.building THEN\s+RAISE EXCEPTION/, 'leaving office names a matter for this building');
253
+ assert.match(MIGRATION_CODE, /IF TG_OP = 'DELETE' THEN\s+RAISE EXCEPTION/, 'no delete');
254
+ assert.match(MIGRATION_CODE, /IF OLD\.removed_at IS NOT NULL THEN\s+RAISE EXCEPTION/, 'an ended appointment never changes again');
255
+ assert.match(MIGRATION_CODE, /NEW\.charter IS DISTINCT FROM OLD\.charter/, 'a Lead in office cannot be edited');
256
+ assert.match(MIGRATION_CODE, /NEW\.appointed_at := now\(\)/, 'when is the database clock');
257
+ assert.match(MIGRATION_CODE, /BEFORE INSERT OR UPDATE OR DELETE ON government_leads/);
258
+ assert.doesNotMatch(MIGRATION_CODE, /INSERT INTO government_leads/, 'no Lead is seeded: every building starts with the owner');
259
+ });
260
+
261
+ test('one writer, one caller: db.applyLeadAct names the sitting; only decision-board calls it; no route does', () => {
262
+ const dbSrc = fs.readFileSync(path.join(GOV, 'db.js'), 'utf8');
263
+ assert.equal((dbSrc.match(/INSERT INTO government_leads/g) || []).length, 1, 'one INSERT');
264
+ assert.equal((dbSrc.match(/UPDATE government_leads/g) || []).length, 1, 'one UPDATE');
265
+ const callers = [];
266
+ const walk = (dir) => {
267
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
268
+ const p = path.join(dir, e.name);
269
+ if (e.isDirectory()) { if (e.name !== 'node_modules' && e.name !== 'migrations') walk(p); continue; }
270
+ if (e.name.endsWith('.js') && /applyLeadAct\(/.test(fs.readFileSync(p, 'utf8'))) callers.push(path.relative(ROOT, p).replace(/\\/g, '/'));
271
+ }
272
+ };
273
+ walk(path.join(ROOT, 'modules'));
274
+ walk(path.join(ROOT, 'src'));
275
+ assert.deepEqual(callers.sort(), ['modules/government/db.js', 'modules/government/decision-board.js']);
276
+ });
277
+
278
+ // ── the routes, through the real router ─────────────────────────────────────
279
+
280
+ const api = require(path.join(ROOT, 'src', 'module-api.js'));
281
+ const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
282
+ const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
283
+ const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
284
+ const express = require('express');
285
+
286
+ test('GET /government/leads is pinned where the room reads; the matters route files the appointment', () => {
287
+ const pins = rrc.MODULE_EXPECTED_RANKS.government;
288
+ assert.equal(pins['GET /government/leads'], 'perm:board.vote.cast');
289
+ assert.equal(pins['POST /government/board/matters'], 'perm:board.item.open');
290
+ });
291
+
292
+ const realRequireBuilder = api.requireBuilder;
293
+ api.requireBuilder = (req, res, next) => { req.builder = { id: 42, rank: 'archon' }; next(); };
294
+ seams._reset();
295
+ let held = ['government.manage', 'board.item.open', 'board.vote.cast'];
296
+ seams.registerProvider('government', { resolveBuilderPermissions: async () => held });
297
+ const buildGovernmentRouter = require(path.join(GOV, 'routes', 'government.js'));
298
+ const app = express();
299
+ app.use(express.json());
300
+ app.use(attachFail);
301
+ app.use(buildGovernmentRouter());
302
+ const server = http.createServer(app);
303
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
304
+ const BASE = `http://127.0.0.1:${server.address().port}`;
305
+ after(() => { server.close(); api.requireBuilder = realRequireBuilder; seams._reset(); });
306
+
307
+ async function call(method, p, body) {
308
+ const res = await fetch(`${BASE}${p}`, { method, headers: body === undefined ? {} : { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
309
+ let json = null; try { json = await res.json(); } catch { /* none */ }
310
+ return { status: res.status, json };
311
+ }
312
+
313
+ test('GET /government/leads: the Lead on their building, the owner answering for the rest; 403 without the atom', async () => {
314
+ let asked = null;
315
+ await withDb({ listLeads: async (o) => { asked = o; return [leadRow()]; }, listActiveArchons: async () => ARCHONS, listDecisionRules: async () => [], getActiveBuildersWithRank: async () => [] }, async () => {
316
+ const r = await call('GET', '/government/leads');
317
+ assert.equal(r.status, 200, JSON.stringify(r.json));
318
+ // what each holder may build without a vote rides the read (task 1004710; tests/government_build_rights.mjs)
319
+ assert.deepEqual(Object.keys(r.json.buildings.find((b) => b.building === 'planning').build_rights.capsules), ['committee', 'floor', 'annex', 'district']);
320
+ assert.deepEqual(asked, { includePast: false });
321
+ assert.equal(r.json.buildings.length, 9);
322
+ assert.equal(r.json.buildings.find((b) => b.building === 'planning').lead.builder.name, 'Dee');
323
+ assert.equal(r.json.buildings.find((b) => b.building === 'release').answers.seat, 'owner');
324
+ await call('GET', '/government/leads?include=past');
325
+ assert.deepEqual(asked, { includePast: true });
326
+ });
327
+ held = ['board.item.open'];
328
+ try { assert.equal((await call('GET', '/government/leads')).status, 403); } finally { held = ['government.manage', 'board.item.open', 'board.vote.cast']; }
329
+ });
330
+
331
+ test('POST /government/board/matters files an appointment (201) and refuses a removal of nobody (409)', async () => {
332
+ await withDb({
333
+ listDecisionRules: async () => [],
334
+ listLeads: async () => [],
335
+ getActiveBuilder: async () => DEE,
336
+ createMatter: async (a) => ({ id: '8', ...a }),
337
+ openBoardItem: async (a) => ({ id: '40', subject_type: a.subjectType, subject_id: a.subjectId, constitution: a.constitution }),
338
+ }, async () => {
339
+ const ok = await call('POST', '/government/board/matters', { area: 'people', title: 'Dee leads Planning', action: { type: 'appoint_lead', building: 'planning', builder_id: 45 } });
340
+ assert.equal(ok.status, 201, JSON.stringify(ok.json));
341
+ assert.equal(ok.json.item.subject_type, 'people_matter');
342
+ const none = await call('POST', '/government/board/matters', { area: 'people', title: 'Nobody leads Review', action: { type: 'remove_lead', building: 'review' } });
343
+ assert.equal(none.status, 409);
344
+ assert.equal(none.json.error.code, 'no_lead');
345
+ const bad = await call('POST', '/government/board/matters', { area: 'people', title: 'A Lead for the kitchen', action: { type: 'appoint_lead', building: 'kitchen', builder_id: 45 } });
346
+ assert.equal(bad.status, 400);
347
+ assert.equal(bad.json.error.code, 'invalid_matter');
348
+ });
349
+ });
350
+
351
+ test('every government route, driven for real, reaches no Lead write', async () => {
352
+ const router = buildGovernmentRouter();
353
+ const routes = [];
354
+ for (const layer of router.stack) {
355
+ if (!layer.route) continue;
356
+ for (const m of Object.keys(layer.route.methods)) routes.push([m.toUpperCase(), layer.route.path]);
357
+ }
358
+ assert.ok(routes.some(([m, p]) => m === 'GET' && p === '/government/leads'), 'the read is mounted');
359
+ const writes = [];
360
+ const stubs = {};
361
+ for (const k of Object.keys(db)) if (typeof db[k] === 'function') stubs[k] = async () => [];
362
+ stubs.applyLeadAct = async () => { writes.push('applyLeadAct'); return { done: true }; };
363
+ stubs.createMatter = async (a) => ({ id: '8', ...a });
364
+ stubs.openBoardItem = async (a) => ({ id: '40', subject_type: a.subjectType, subject_id: a.subjectId, constitution: a.constitution });
365
+ stubs.getActiveBuilder = async () => DEE;
366
+ const lead = { type: 'appoint_lead', building: 'planning', builder_id: 45 };
367
+ await withDb(stubs, async () => {
368
+ for (const [method, p] of routes) {
369
+ const url = p.replace(/:[A-Za-z]+/g, '1');
370
+ const bodies = method === 'GET' ? [undefined] : [{ area: 'people', title: 'Dee leads Planning', action: lead }, { action: lead }, lead, { direction: 'yes' }];
371
+ for (const body of bodies) await call(method, url, body);
372
+ }
373
+ });
374
+ assert.deepEqual(writes, [], 'opening a matter seats nobody; only a passed sitting does');
375
+ });
@@ -0,0 +1,127 @@
1
+ // tests/government_positions.mjs — GOVERNANCE IS A HELD POSITION (task 1004713,
2
+ // goal 1000111; ADR 0377).
3
+ //
4
+ // What it pins:
5
+ // 1. positions.js folds the real seats for one builder: the owner seat (every
6
+ // active Archon), a Lead in office, a seat on a building's own sitting, the
7
+ // board. A Lead is named by their seat and outranks the rest for the title;
8
+ // an unseated building is the owner's, never a Lead for anybody.
9
+ // 2. readPositions asks each read the server resolves (Leads, Archons, the
10
+ // board and each sitting's predicate) and answers null when any read fails —
11
+ // unknown, never "holds none". A craft never makes a seat.
12
+ // 3. GET /me carries the block through the government port, and the port asks
13
+ // the one rule each seat already has (routing.resolveSeat via leads.answerFor,
14
+ // board-membership.isBoardMember) — never a second one.
15
+ //
16
+ // Run: node --test --test-reporter=tap tests/government_positions.mjs
17
+
18
+ import { test } from 'node:test';
19
+ import assert from 'node:assert/strict';
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+ import { createRequire } from 'node:module';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ const require = createRequire(import.meta.url);
26
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
27
+ const positions = require(path.join(ROOT, 'modules', 'government', 'positions.js'));
28
+ const { ownerOf } = require(path.join(ROOT, 'modules', 'government', 'leads.js'));
29
+
30
+ const ARCHONS = [{ id: 3, github_login: 'founder' }, { id: 7, github_login: 'second' }];
31
+ const LEADS = [{ building: 'planning', builder_id: 42 }, { building: 'review', builder_id: 43, removed_at: '2026-10-01' }];
32
+ const kinds = (p) => p.held.map((s) => s.kind);
33
+
34
+ // ── 1. the fold ────────────────────────────────────────────────────────────────
35
+
36
+ test('a Lead in office holds their building, named by the seat', () => {
37
+ const p = positions.positionsOf('42', { leads: LEADS, owner: ownerOf(ARCHONS) });
38
+ assert.deepEqual(kinds(p), ['lead']);
39
+ assert.equal(p.held[0].seat, 'lead:planning');
40
+ assert.equal(p.title, 'Planning Lead');
41
+ assert.equal(p.has_position, true);
42
+ });
43
+
44
+ test('a builder with no seat holds nothing, whatever their id looks like', () => {
45
+ for (const id of ['424242', '43', null, '']) {
46
+ const p = positions.positionsOf(id, { leads: LEADS, owner: ownerOf(ARCHONS), board: false, sittings: [] });
47
+ assert.deepEqual(p.held, [], `${id}: a removed Lead and a stranger hold nothing`);
48
+ assert.equal(p.title, null);
49
+ assert.equal(p.has_position, false);
50
+ }
51
+ });
52
+
53
+ test('every active Archon holds the owner seat; an unseated building never makes them its Lead', () => {
54
+ const p = positions.positionsOf('7', { leads: LEADS, owner: ownerOf(ARCHONS) });
55
+ assert.deepEqual(kinds(p), ['owner'], 'review is unseated (its Lead was removed): it falls to the owner, not a Lead seat');
56
+ assert.equal(p.title, 'Owner');
57
+ });
58
+
59
+ test('a Lead names the Governing mode ahead of the owner seat, a building seat and the board', () => {
60
+ const p = positions.positionsOf('3', {
61
+ leads: [{ building: 'treasury', builder_id: 3 }], owner: ownerOf(ARCHONS), board: true, sittings: ['people'],
62
+ });
63
+ assert.deepEqual(kinds(p), ['lead', 'owner', 'seat', 'board']);
64
+ assert.deepEqual(p.held.map((s) => s.title), ['Treasury Lead', 'Owner', 'People seat', 'Board member']);
65
+ assert.equal(p.title, 'Treasury Lead');
66
+ });
67
+
68
+ // ── 2. the reads ───────────────────────────────────────────────────────────────
69
+
70
+ const reads = (over = {}) => ({
71
+ listLeads: async () => LEADS,
72
+ listActiveArchons: async () => ARCHONS,
73
+ board: () => ({ membership: 'rank:archon' }),
74
+ decisions: async () => [
75
+ { area: 'people', sitting: true, decided_by: 'rank:metic+' },
76
+ { area: 'rules', sitting: true, decided_by: 'board' },
77
+ { area: 'release', sitting: false, decided_by: 'rank:metic+' },
78
+ { area: 'review', sitting: true, decided_by: 'gate' },
79
+ ],
80
+ // the fake predicate: 42 is a metic, 3 and 7 archons
81
+ sits: async (id, membership) => (membership === 'rank:archon' ? ['3', '7'] : membership === 'rank:metic+' ? ['3', '7', '42'] : []).includes(String(id)),
82
+ ...over,
83
+ });
84
+
85
+ test('readPositions seats a builder on each building whose own sitting names them, and on the board', async () => {
86
+ const lead = await positions.readPositions('42', reads());
87
+ assert.deepEqual(kinds(lead), ['lead', 'seat'], 'the Planning Lead also sits on People (a metic+ sitting)');
88
+ assert.deepEqual(lead.held.map((s) => s.seat), ['lead:planning', 'board:people'],
89
+ 'a sitting the board holds (rules), one that is not a sitting (release) and a fixed gate (review) seat nobody');
90
+ const archon = await positions.readPositions('3', reads());
91
+ assert.deepEqual(kinds(archon), ['owner', 'seat', 'board']);
92
+ const stranger = await positions.readPositions('424242', reads());
93
+ assert.equal(stranger.has_position, false);
94
+ assert.deepEqual(stranger.held, []);
95
+ });
96
+
97
+ test('readPositions is unknown (null) when any read fails, or with no builder — never "holds none"', async () => {
98
+ for (const broken of ['listLeads', 'listActiveArchons', 'decisions', 'sits']) {
99
+ const r = await positions.readPositions('42', reads({ [broken]: async () => { throw new Error('db down'); } }));
100
+ assert.equal(r, null, `${broken} failed`);
101
+ }
102
+ assert.equal(await positions.readPositions('42', reads({ board: () => { throw new Error('config'); } })), null);
103
+ assert.equal(await positions.readPositions(null, reads()), null);
104
+ });
105
+
106
+ test('a craft never makes a seat: positions.js reads no discipline', () => {
107
+ const src = fs.readFileSync(path.join(ROOT, 'modules', 'government', 'positions.js'), 'utf8').replace(/^\s*\/\/.*$/gm, '');
108
+ assert.doesNotMatch(src, /disciplin|preferred_/i, 'no craft field is read');
109
+ });
110
+
111
+ // ── 3. the wiring ──────────────────────────────────────────────────────────────
112
+
113
+ test('GET /me carries positions through the government port', () => {
114
+ const me = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'routes', 'me.js'), 'utf8');
115
+ assert.match(me, /government\.positionsFor\(req\.builder\.id\)/, '/me asks the port for the requesting builder');
116
+ assert.match(me, /positions: govPositions,/, '/me answers a positions block');
117
+ });
118
+
119
+ test('the port asks the rules each seat already has, never a second one', () => {
120
+ const gov = fs.readFileSync(path.join(ROOT, 'modules', 'government', 'routes', 'government.js'), 'utf8');
121
+ assert.match(gov, /positionsFor: \(builderId\) => positions\.readPositions\(builderId, \{/);
122
+ assert.match(gov, /sits: \(id, membership\) => boardMembership\.isBoardMember\(id, \{ membership \}\)/);
123
+ const src = fs.readFileSync(path.join(ROOT, 'modules', 'government', 'positions.js'), 'utf8');
124
+ assert.match(src, /require\('\.\/routing'\)/, 'Lead and owner seats are routing.resolveSeat\'s answer');
125
+ assert.match(src, /resolveSeat\('owner'/);
126
+ assert.match(src, /resolveSeat\(`lead:\$\{b\}`/);
127
+ });