@bongos/core 1.21.112 → 1.21.114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/.bongos-core.json +616 -186
  2. package/.claude/skills/ask-for-help/SKILL.md +1 -1
  3. package/.claude/skills/bongos-feedback/SKILL.md +1 -1
  4. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  5. package/.claude/skills/builder-ship/SKILL.md +1 -0
  6. package/GOVERNMENT.md +128 -11
  7. package/clients/bongos-client/README.md +1 -1
  8. package/clients/bongos-client/bongos-client.global.js +28 -0
  9. package/clients/bongos-client/index.cjs +28 -0
  10. package/clients/bongos-client/index.d.ts +45 -2
  11. package/clients/bongos-client/index.mjs +28 -0
  12. package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
  13. package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
  14. package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
  15. package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
  16. package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
  17. package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
  18. package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
  19. package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
  20. package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
  21. package/docs/adr/README.md +10 -1
  22. package/docs/api/openapi.json +989 -87
  23. package/docs/api-reference.md +18 -4
  24. package/docs/copy-inventory.md +343 -262
  25. package/docs/copy-registry.json +1223 -426
  26. package/docs/file-map.md +2 -1
  27. package/docs/module-api-changelog.md +4 -0
  28. package/docs/packs/engineer.md +2 -1
  29. package/docs/page-inventory.json +91 -6
  30. package/docs/page-readings.json +5017 -4458
  31. package/modules/builder-settings/law-locks.js +202 -0
  32. package/modules/builder-settings/routes/settings.js +66 -6
  33. package/modules/builder-settings/settings.js +7 -0
  34. package/modules/discord/ship-broadcast.js +22 -13
  35. package/modules/government/asks-db.js +54 -0
  36. package/modules/government/asks.js +269 -0
  37. package/modules/government/board-idea-reads.js +68 -0
  38. package/modules/government/board.js +21 -57
  39. package/modules/government/build-rights.js +148 -0
  40. package/modules/government/buildouts.js +160 -0
  41. package/modules/government/counts.js +81 -0
  42. package/modules/government/db.js +242 -0
  43. package/modules/government/decision-board.js +234 -8
  44. package/modules/government/decision-rules.js +2 -0
  45. package/modules/government/docket.js +114 -6
  46. package/modules/government/law-board.js +131 -0
  47. package/modules/government/laws.js +241 -0
  48. package/modules/government/leads.js +170 -0
  49. package/modules/government/migrations/government_027_laws.sql +143 -0
  50. package/modules/government/migrations/government_028_asks.sql +57 -0
  51. package/modules/government/migrations/government_029_leads.sql +135 -0
  52. package/modules/government/migrations/government_030_buildouts.sql +111 -0
  53. package/modules/government/migrations/government_031_treasury.sql +107 -0
  54. package/modules/government/module.json +2 -1
  55. package/modules/government/pollers/treasury-borrow.js +62 -0
  56. package/modules/government/positions.js +107 -0
  57. package/modules/government/presets.js +149 -0
  58. package/modules/government/routes/government.js +425 -2
  59. package/modules/government/routing.js +154 -0
  60. package/modules/government/treasury-db.js +166 -0
  61. package/modules/government/treasury.js +241 -0
  62. package/modules/hall-ui/public/atlas.html +2 -2
  63. package/modules/hall-ui/public/blockers.html +2 -2
  64. package/modules/hall-ui/public/board-room.html +2 -2
  65. package/modules/hall-ui/public/board-room.js +2 -0
  66. package/modules/hall-ui/public/city-draw.js +604 -11
  67. package/modules/hall-ui/public/city.css +157 -1
  68. package/modules/hall-ui/public/city.html +37 -7
  69. package/modules/hall-ui/public/city.js +441 -21
  70. package/modules/hall-ui/public/city.states.json +110 -5
  71. package/modules/hall-ui/public/collab.html +2 -2
  72. package/modules/hall-ui/public/commands.html +2 -2
  73. package/modules/hall-ui/public/copy-desk.html +2 -2
  74. package/modules/hall-ui/public/craft-mode.js +112 -29
  75. package/modules/hall-ui/public/deploy.html +2 -2
  76. package/modules/hall-ui/public/diagram-viewer.css +12 -0
  77. package/modules/hall-ui/public/diagram-viewer.js +4 -0
  78. package/modules/hall-ui/public/diagrams.html +2 -2
  79. package/modules/hall-ui/public/drachmae.html +2 -2
  80. package/modules/hall-ui/public/fleet.html +2 -2
  81. package/modules/hall-ui/public/gate.html +2 -2
  82. package/modules/hall-ui/public/goal-map.html +2 -2
  83. package/modules/hall-ui/public/goals.html +2 -2
  84. package/modules/hall-ui/public/gov-feeds.js +141 -0
  85. package/modules/hall-ui/public/gov-living.js +1002 -0
  86. package/modules/hall-ui/public/gov-positions.js +97 -0
  87. package/modules/hall-ui/public/government.html +2 -2
  88. package/modules/hall-ui/public/hall-render.js +3 -0
  89. package/modules/hall-ui/public/idea.html +2 -2
  90. package/modules/hall-ui/public/ideas.html +2 -2
  91. package/modules/hall-ui/public/index.html +3 -3
  92. package/modules/hall-ui/public/modules.html +2 -2
  93. package/modules/hall-ui/public/my-agents.css +334 -0
  94. package/modules/hall-ui/public/my-agents.html +72 -0
  95. package/modules/hall-ui/public/my-agents.js +603 -0
  96. package/modules/hall-ui/public/my-agents.states.json +37 -0
  97. package/modules/hall-ui/public/primer.html +2 -2
  98. package/modules/hall-ui/public/profile-connections.js +40 -28
  99. package/modules/hall-ui/public/profile.html +2 -2
  100. package/modules/hall-ui/public/project-settings.html +2 -2
  101. package/modules/hall-ui/public/ranks.html +2 -2
  102. package/modules/hall-ui/public/roadmap.html +2 -2
  103. package/modules/hall-ui/public/room.css +8 -0
  104. package/modules/hall-ui/public/roster.html +2 -2
  105. package/modules/hall-ui/public/sessions.html +2 -2
  106. package/modules/hall-ui/public/settings.html +2 -2
  107. package/modules/hall-ui/public/shell.js +116 -16
  108. package/modules/hall-ui/public/street-card.js +18 -1
  109. package/modules/hall-ui/public/street-deck.css +299 -0
  110. package/modules/hall-ui/public/street-deck.js +1145 -0
  111. package/modules/hall-ui/public/street-depth.js +712 -0
  112. package/modules/hall-ui/public/street-desk.js +5 -2
  113. package/modules/hall-ui/public/street-draw.js +109 -16
  114. package/modules/hall-ui/public/street-inside.js +137 -22
  115. package/modules/hall-ui/public/street-office.js +705 -0
  116. package/modules/hall-ui/public/street.css +161 -11
  117. package/modules/hall-ui/public/street.html +41 -11
  118. package/modules/hall-ui/public/street.js +78 -66
  119. package/modules/hall-ui/public/street.states.json +129 -21
  120. package/modules/hall-ui/public/studio.html +2 -2
  121. package/modules/hall-ui/public/style.css +5 -1
  122. package/modules/hall-ui/public/task.html +2 -2
  123. package/modules/hall-ui/public/thinking.html +2 -2
  124. package/modules/hall-ui/public/tweak-editor.html +2 -2
  125. package/modules/hall-ui/public/watch.html +2 -2
  126. package/modules/hall-ui/public/watch.js +14 -1
  127. package/modules/hall-ui/public/work.html +2 -2
  128. package/modules/hall-ui/records/living-city.md +57 -0
  129. package/modules/hall-ui/records/my-agents.md +21 -0
  130. package/modules/hall-ui/records/oversight-family.md +1 -0
  131. package/modules/hall-ui/records/profile-connections.md +10 -0
  132. package/modules/hall-ui/records/reading-rooms.md +2 -0
  133. package/modules/ideas/routes/blockers.js +92 -3
  134. package/modules/lifecycle/module.json +2 -1
  135. package/modules/lifecycle/moving.js +17 -1
  136. package/modules/lifecycle/routes/moving.js +6 -0
  137. package/modules/lifecycle/routes/project-settings.js +23 -0
  138. package/modules/onboarding/approval-broadcast.js +8 -0
  139. package/modules/onboarding/routes/access-requests.js +16 -6
  140. package/modules/platform-identity/connections.js +13 -2
  141. package/modules/platform-identity/routes/connections.js +7 -1
  142. package/modules/public-landing/public/account-private.states.json +2 -2
  143. package/modules/public-landing/public/account.html +65 -5
  144. package/modules/public-landing/public/account.states.json +3 -3
  145. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  146. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  147. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
  148. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  149. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  150. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
  151. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  152. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  153. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
  154. package/modules/public-landing/public/assets/world.css +4 -4
  155. package/modules/public-landing/public/connections-empty.states.json +14 -0
  156. package/modules/public-landing/public/connections-inactive.states.json +14 -0
  157. package/modules/public-landing/public/connections.html +450 -0
  158. package/modules/public-landing/public/connections.probes.json +414 -0
  159. package/modules/public-landing/public/connections.states.json +17 -0
  160. package/modules/public-landing/public/contact.html +2 -2
  161. package/modules/public-landing/public/guild.html +2 -2
  162. package/modules/public-landing/public/index.html +2 -2
  163. package/modules/public-landing/public/invitations.html +2 -2
  164. package/modules/public-landing/public/privacy.html +2 -2
  165. package/modules/public-landing/public/projects.html +2 -2
  166. package/modules/public-landing/public/projects.probes.json +6 -2
  167. package/modules/public-landing/public/terms.html +2 -2
  168. package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
  169. package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
  170. package/modules/ui-design/kit/fixtures/connections.json +22 -0
  171. package/modules/ui-design/kit/serve.js +95 -0
  172. package/package-lock.json +2 -2
  173. package/package.json +1 -1
  174. package/release-notes.json +128 -0
  175. package/scripts/discord/README.md +8 -5
  176. package/scripts/discord/post.js +12 -1
  177. package/scripts/gds/artifact-audit.js +20 -3
  178. package/scripts/gds/ask.js +164 -0
  179. package/scripts/gds/autobongos-run.js +20 -5
  180. package/scripts/gds/autobongos-tweak-lane.js +262 -0
  181. package/scripts/gds/autobongos-verify.js +21 -2
  182. package/scripts/gds/claim.js +2 -0
  183. package/scripts/gds/government-walk.js +638 -4
  184. package/scripts/gds/pulse.js +9 -6
  185. package/scripts/gds/ship-escalation.js +22 -3
  186. package/scripts/gds/ship-finish.js +5 -4
  187. package/scripts/gds/ship-merge-gate.js +18 -6
  188. package/scripts/gds/tweak-halt.js +1 -0
  189. package/scripts/hall-preview/README.md +41 -1
  190. package/scripts/hall-preview/pref-writes.js +140 -0
  191. package/scripts/hall-preview/refresh-fixtures.js +101 -7
  192. package/scripts/hall-preview/scrub-identity.js +366 -7
  193. package/scripts/hall-preview/server.js +27 -1
  194. package/scripts/render-diagrams.sh +4 -2
  195. package/src/bongos/auth-idp.js +6 -1
  196. package/src/bongos/project-settings.js +41 -0
  197. package/src/bongos/route-rank-check.js +27 -0
  198. package/src/bongos/routes/me.js +68 -7
  199. package/src/bongos/serve-internal.js +5 -1
  200. package/src/module-api.js +7 -1
  201. package/src/platform-server.js +10 -0
  202. package/tests/application_lifecycle.mjs +47 -5
  203. package/tests/autobongos_tweak_lane.mjs +208 -0
  204. package/tests/conductor_main_e2e.mjs +21 -0
  205. package/tests/connections_api.mjs +18 -0
  206. package/tests/craft_mode_nav.mjs +5 -3
  207. package/tests/craft_mode_tallies.mjs +52 -16
  208. package/tests/discord_approvals.mjs +11 -0
  209. package/tests/discord_channels.mjs +20 -0
  210. package/tests/fixtures/gov-seated.mjs +64 -0
  211. package/tests/fixtures/government-ask-cards.json +1284 -0
  212. package/tests/government_act_alone_buildings.mjs +1 -0
  213. package/tests/government_agent_asks.mjs +322 -0
  214. package/tests/government_asks.mjs +408 -0
  215. package/tests/government_build_rights.mjs +253 -0
  216. package/tests/government_buildouts.mjs +382 -0
  217. package/tests/government_counts.mjs +143 -0
  218. package/tests/government_decision_rules.mjs +5 -1
  219. package/tests/government_laws.mjs +488 -0
  220. package/tests/government_laws_session.mjs +321 -0
  221. package/tests/government_leads.mjs +375 -0
  222. package/tests/government_positions.mjs +127 -0
  223. package/tests/government_presets.mjs +215 -0
  224. package/tests/government_routes.mjs +8 -1
  225. package/tests/government_routing.mjs +118 -0
  226. package/tests/government_seed.mjs +5 -0
  227. package/tests/government_treasury.mjs +394 -0
  228. package/tests/governor_docket.mjs +26 -2
  229. package/tests/hall_buildouts.mjs +208 -0
  230. package/tests/hall_city.mjs +14 -6
  231. package/tests/hall_city_living.mjs +617 -0
  232. package/tests/hall_gov_counts.mjs +336 -0
  233. package/tests/hall_gov_living.mjs +636 -0
  234. package/tests/hall_gov_real_site.mjs +655 -0
  235. package/tests/hall_matter_card.mjs +6 -1
  236. package/tests/hall_my_agents.mjs +481 -0
  237. package/tests/hall_nav.mjs +107 -4
  238. package/tests/hall_page_gate_map.mjs +4 -0
  239. package/tests/hall_preview_governor_fixtures.mjs +30 -3
  240. package/tests/hall_preview_scrub.mjs +59 -1
  241. package/tests/hall_preview_security.mjs +169 -0
  242. package/tests/hall_reading_world.mjs +44 -0
  243. package/tests/hall_roster_world.mjs +6 -2
  244. package/tests/hall_street.mjs +18 -2
  245. package/tests/hall_street_amend.mjs +1 -1
  246. package/tests/hall_street_deck.mjs +609 -0
  247. package/tests/hall_street_depth.mjs +559 -0
  248. package/tests/hall_street_desk.mjs +1 -1
  249. package/tests/hall_street_inside.mjs +29 -5
  250. package/tests/hall_tweak_editor.mjs +4 -1
  251. package/tests/hub_account_page.mjs +94 -3
  252. package/tests/hub_connections_page.mjs +507 -0
  253. package/tests/hub_invite_badge.mjs +1 -1
  254. package/tests/idp_device_exchange.mjs +18 -0
  255. package/tests/landing_legal_pages.mjs +8 -5
  256. package/tests/landing_page.mjs +23 -4
  257. package/tests/nav_permission_atoms.mjs +4 -1
  258. package/tests/profile_connections_ui.mjs +88 -25
  259. package/tests/projects_hub.mjs +15 -0
  260. package/tests/render_check.mjs +2 -2
  261. package/tests/ship_broadcast_routing.mjs +57 -16
  262. package/tests/ship_broadcast_server.mjs +40 -7
  263. package/tests/ship_merge_gate.mjs +19 -0
  264. package/tests/ui_design_kit.mjs +2 -2
  265. package/tests/watch_applications_queue.mjs +23 -0
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ // modules/government/law-board.js — the board's side of the project's LAWS
4
+ // (task 1004706, goal 1000111). laws.js is the pure half (what a law is, strict
5
+ // validation, the guard); this file is where the law book meets the board:
6
+ // proposing a change as an amendment, writing a passed one inside its closing
7
+ // transaction, and reading the book with who passed each law and when. Split out
8
+ // of board.js the way decision-board.js is; board.js calls in here and never the
9
+ // other way round (no require cycle).
10
+ //
11
+ // The window-open nudge (board.item.opened) is emitted by board.js's wrapper
12
+ // around proposeLaws, beside every other open.
13
+
14
+ const db = require('./db');
15
+ const { loadGovernmentConfig } = require('./config');
16
+ const laws = require('./laws');
17
+ const charter = require('./charter');
18
+
19
+ // ── proposing: an amendment of kind 'laws' ──────────────────────────────────
20
+ //
21
+ // Validated strictly, checked against the laws in force, filed as an amendment
22
+ // of kind 'laws' and sat under the constitution IN FORCE — exactly like a
23
+ // constitution amendment. Nothing here writes a law; only the sitting passing
24
+ // does (applyPassedLaws). Returns { ok, amendment, item, proposal } or a named
25
+ // refusal: validateLawsProposal's reasons | checkAgainstInForce's reasons
26
+ // | 'missing_proposer'.
27
+ async function proposeLaws({ enact, repeal, rationaleMd = null, proposedBy } = {}) {
28
+ const v = laws.validateLawsProposal({ ...(enact !== undefined ? { enact } : {}), ...(repeal !== undefined ? { repeal } : {}) });
29
+ if (!v.ok) return v;
30
+ if (proposedBy == null || proposedBy === '') return { ok: false, reason: 'missing_proposer' };
31
+ const inForce = await db.listLaws();
32
+ const c = laws.checkAgainstInForce(v.proposal, inForce);
33
+ if (!c.ok) return c;
34
+
35
+ const { board } = loadGovernmentConfig();
36
+ const amendment = await db.createAmendment({
37
+ proposed: { laws: v.proposal },
38
+ rationaleMd: rationaleMd || laws.proposalWords(v.proposal, inForce),
39
+ proposedBy,
40
+ kind: 'laws',
41
+ });
42
+ const item = await db.openBoardItem({
43
+ subjectType: 'constitutional_amendment',
44
+ subjectId: amendment.id,
45
+ openedBy: proposedBy,
46
+ constitution: board,
47
+ windowMinutes: board.window_minutes,
48
+ });
49
+ return { ok: true, amendment, item, proposal: v.proposal };
50
+ }
51
+
52
+ // ── the one write ───────────────────────────────────────────────────────────
53
+ //
54
+ // Runs INSIDE the transaction that closes the sitting (`client`); a throw rolls
55
+ // the close back. Guarded here (laws.lawsWriteAllowed), in db.applyLaws's SQL,
56
+ // and by government_027's trigger.
57
+ async function applyPassedLaws(client, closedRow, amendment) {
58
+ if (!laws.lawsWriteAllowed(closedRow, amendment)) {
59
+ throw new Error(`[government] laws may only be written by a passed laws amendment (item ${closedRow && closedRow.id})`);
60
+ }
61
+ // Re-validated as a belt: the row was validated strictly at propose time and
62
+ // is immutable since, so a refusal here means the stored row was altered.
63
+ const v = laws.validateLawsProposal((amendment.proposed || {}).laws);
64
+ if (!v.ok) throw new Error(`[government] passed laws amendment ${amendment.id} no longer validates (${v.reason}) — NOT applied`);
65
+ const out = await db.applyLaws(client, {
66
+ itemId: closedRow.id,
67
+ enact: v.proposal.enact,
68
+ repeal: v.proposal.repeal,
69
+ proposedBy: amendment.proposed_by == null ? null : amendment.proposed_by,
70
+ });
71
+ return {
72
+ enacted: (out.enacted || []).map((r) => ({ id: String(r.id), text: r.text })),
73
+ repealed: (out.repealed || []).map((r) => String(r.id)),
74
+ };
75
+ }
76
+
77
+ // ── the law book ────────────────────────────────────────────────────────────
78
+ //
79
+ // Every law in force (or, with includeRepealed, every law ever passed), oldest
80
+ // first, each saying WHO passed it and WHEN: the government it was decided under
81
+ // (read as its form's name), who proposed it, who said yes, and the sitting.
82
+ const person = (id, login, name) => (id == null ? null : { id: String(id), login: login || null, name: name || login || null });
83
+
84
+ function projectLaw(row, yesByItem) {
85
+ const under = row.decided_under || null;
86
+ const form = under ? charter.readCharter(under) : null;
87
+ const itemId = String(row.passed_by_item_id);
88
+ return {
89
+ id: String(row.id),
90
+ text: row.text,
91
+ effect_on_ai: row.effect_on_ai || null,
92
+ locks: Array.isArray(row.locks) ? row.locks : [],
93
+ in_force: row.repealed_at == null,
94
+ passed_at: row.passed_at,
95
+ passed_by: {
96
+ item_id: itemId,
97
+ href: `/board-room?item=${encodeURIComponent(itemId)}`,
98
+ government: form ? form.name : null,
99
+ pass_rule: under && under.pass_rule ? under.pass_rule : null,
100
+ proposer: person(row.proposed_by, row.proposer_login, row.proposer_name),
101
+ yes: (yesByItem.get(itemId) || []).map((v) => person(v.voter_id, v.github_login, v.display_name)),
102
+ },
103
+ repealed_at: row.repealed_at || null,
104
+ repealed_by_item_id: row.repealed_by_item_id == null ? null : String(row.repealed_by_item_id),
105
+ };
106
+ }
107
+
108
+ async function lawsView({ includeRepealed = false } = {}) {
109
+ const rows = await db.listLaws({ includeRepealed });
110
+ const itemIds = [...new Set(rows.map((r) => String(r.passed_by_item_id)))];
111
+ const yes = itemIds.length ? await db.listYesVoters(itemIds) : [];
112
+ const yesByItem = new Map();
113
+ for (const v of yes) {
114
+ const k = String(v.item_id);
115
+ if (!yesByItem.has(k)) yesByItem.set(k, []);
116
+ yesByItem.get(k).push(v);
117
+ }
118
+ const book = rows.map((r) => projectLaw(r, yesByItem));
119
+ return {
120
+ laws: book,
121
+ in_force: book.filter((l) => l.in_force).length,
122
+ changed_by: 'a passed amendment only: POST /government/board/laws files one',
123
+ limits: { text_max: laws.LAW_TEXT_MAX, effect_max: laws.EFFECT_MAX, locks_max: laws.MAX_LOCKS },
124
+ };
125
+ }
126
+
127
+ module.exports = {
128
+ proposeLaws,
129
+ applyPassedLaws,
130
+ lawsView,
131
+ };
@@ -0,0 +1,241 @@
1
+ 'use strict';
2
+
3
+ // modules/government/laws.js — STANDING ORDERS: the project's laws for its AI
4
+ // agents (task 1004706, goal 1000111 "government is how a project steers its AI").
5
+ //
6
+ // WHAT A LAW IS. A short, one-line rule every AI agent on the project obeys —
7
+ // "Stuck? Note it, skip it, keep going", "Land only. Ask before going live",
8
+ // "Ask like a human". It carries an optional line saying what it means for an
9
+ // agent, and may LOCK personal settings it overrides (a lock names a setting and
10
+ // either fixes its value or caps it with a max). Before this, rules like these
11
+ // lived in one builder's private AI notes, so nobody else's agents ever saw them.
12
+ //
13
+ // CHANGED ONLY BY A PASSED AMENDMENT, like the constitution. There is no route
14
+ // that writes a law. A change — enacting new laws, repealing laws in force, or
15
+ // both at once — is filed as an amendment of kind 'laws' (law-board.js
16
+ // proposeLaws), decided by the board IN FORCE like every other amendment, and
17
+ // written INSIDE the transaction that closes that sitting as passed
18
+ // (board.closeItem → law-board.applyPassedLaws → db.applyLaws). The database
19
+ // refuses any other write (government_027's trigger), so a hand-written INSERT
20
+ // or UPDATE is refused too, and a law is never deleted: a repeal is a passed
21
+ // amendment, and the repealed row stays on the record.
22
+ //
23
+ // WHO PASSED IT, AND WHEN. Every law row names the board item that passed it,
24
+ // its proposer and when it passed; the read joins that item's constitution (the
25
+ // government it was decided under) and its yes votes. A repeal records the item
26
+ // that repealed it, and when.
27
+ //
28
+ // READ INTO EVERY AI SESSION (task 1004707). forSession below turns the laws in
29
+ // force into the one block every session reads: GET /me carries it, and the
30
+ // Conductor hook relays it verbatim beside the builder's interaction profile.
31
+ // The server writes the words; nothing a builder edits locally can change them.
32
+ // A lock is ENFORCED on the builder's personal settings by builder-settings
33
+ // (law-locks.js), on every read of the effective value and on every write. The
34
+ // first laws themselves are the owner's to ratify (task 1004708).
35
+ //
36
+ // PURE: no db, no requires. law-board.js and the routes hand rows in.
37
+
38
+ const LAW_TEXT_MIN = 3;
39
+ const LAW_TEXT_MAX = 100;
40
+ const EFFECT_MAX = 200;
41
+ const MAX_LOCKS = 8;
42
+ const MAX_ENACT = 10;
43
+ const MAX_REPEAL = 20;
44
+ const LOCK_VALUE_MAX = 64;
45
+ // A personal setting's key, as the hall and the interaction profile spell them
46
+ // (technical_level, wandering, model.main, on_stuck).
47
+ const SETTING_KEY = /^[a-z][a-z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)?$/;
48
+ const SETTING_KEY_MAX = 64;
49
+ // One line: no line breaks or other control characters.
50
+ // eslint-disable-next-line no-control-regex
51
+ const CONTROL = /[\u0000-\u001f\u007f]/;
52
+
53
+ function oneLine(raw, { min, max }) {
54
+ if (typeof raw !== 'string') return null;
55
+ if (CONTROL.test(raw)) return null;
56
+ const s = raw.trim().replace(/\s+/g, ' ');
57
+ if (s.length < min || s.length > max) return null;
58
+ return s;
59
+ }
60
+
61
+ // A lock: { setting, value } fixes the setting; { setting, max } caps it (a
62
+ // number, or the highest option of an ordered list such as wandering's).
63
+ function validateLock(raw, i) {
64
+ const at = `locks[${i}]`;
65
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, reason: 'bad_lock', field: at };
66
+ for (const k of Object.keys(raw)) {
67
+ if (k !== 'setting' && k !== 'value' && k !== 'max') return { ok: false, reason: 'unknown_field', field: `${at}.${k}` };
68
+ }
69
+ if (typeof raw.setting !== 'string' || raw.setting.length > SETTING_KEY_MAX || !SETTING_KEY.test(raw.setting)) {
70
+ return { ok: false, reason: 'bad_lock_setting', field: `${at}.setting` };
71
+ }
72
+ const hasValue = Object.prototype.hasOwnProperty.call(raw, 'value');
73
+ const hasMax = Object.prototype.hasOwnProperty.call(raw, 'max');
74
+ if (hasValue === hasMax) return { ok: false, reason: 'lock_needs_value_or_max', field: at, expected: 'exactly one of value, max' };
75
+ if (hasValue) {
76
+ const v = raw.value;
77
+ const okValue = typeof v === 'boolean'
78
+ || (typeof v === 'number' && Number.isFinite(v))
79
+ || (typeof v === 'string' && v.length >= 1 && v.length <= LOCK_VALUE_MAX && !CONTROL.test(v));
80
+ if (!okValue) return { ok: false, reason: 'bad_lock_value', field: `${at}.value` };
81
+ return { ok: true, lock: { setting: raw.setting, value: v } };
82
+ }
83
+ const m = raw.max;
84
+ const okMax = (typeof m === 'number' && Number.isFinite(m))
85
+ || (typeof m === 'string' && m.length >= 1 && m.length <= LOCK_VALUE_MAX && !CONTROL.test(m));
86
+ if (!okMax) return { ok: false, reason: 'bad_lock_max', field: `${at}.max` };
87
+ return { ok: true, lock: { setting: raw.setting, max: m } };
88
+ }
89
+
90
+ function validateLaw(raw, i) {
91
+ const at = `enact[${i}]`;
92
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, reason: 'bad_law', field: at };
93
+ for (const k of Object.keys(raw)) {
94
+ if (k !== 'text' && k !== 'effect_on_ai' && k !== 'locks') return { ok: false, reason: 'unknown_field', field: `${at}.${k}` };
95
+ }
96
+ const text = oneLine(raw.text, { min: LAW_TEXT_MIN, max: LAW_TEXT_MAX });
97
+ if (!text) return { ok: false, reason: 'bad_text', field: `${at}.text`, expected: `one line of ${LAW_TEXT_MIN}-${LAW_TEXT_MAX} characters` };
98
+ let effect = null;
99
+ if (raw.effect_on_ai != null) {
100
+ effect = oneLine(raw.effect_on_ai, { min: 1, max: EFFECT_MAX });
101
+ if (!effect) return { ok: false, reason: 'bad_effect', field: `${at}.effect_on_ai`, expected: `one line of at most ${EFFECT_MAX} characters` };
102
+ }
103
+ const locksIn = raw.locks == null ? [] : raw.locks;
104
+ if (!Array.isArray(locksIn) || locksIn.length > MAX_LOCKS) return { ok: false, reason: 'bad_locks', field: `${at}.locks`, expected: `a list of at most ${MAX_LOCKS}` };
105
+ const locks = [];
106
+ const seen = new Set();
107
+ for (let j = 0; j < locksIn.length; j++) {
108
+ const v = validateLock(locksIn[j], j);
109
+ if (!v.ok) return { ...v, field: `${at}.${v.field}` };
110
+ if (seen.has(v.lock.setting)) return { ok: false, reason: 'setting_locked_twice', field: `${at}.locks[${j}].setting`, setting: v.lock.setting };
111
+ seen.add(v.lock.setting);
112
+ locks.push(v.lock);
113
+ }
114
+ return { ok: true, law: { text, effect_on_ai: effect, locks } };
115
+ }
116
+
117
+ const sameText = (a, b) => String(a).trim().toLowerCase() === String(b).trim().toLowerCase();
118
+
119
+ // A proposal, validated STRICTLY — every field named, nothing corrected beyond
120
+ // trimming whitespace (the board must not ratify words the proposer never wrote).
121
+ // { enact?: [{ text, effect_on_ai?, locks? }], repeal?: [law id] }
122
+ // Returns { ok: true, proposal: { enact, repeal } } or { ok: false, reason, field?, expected? }.
123
+ function validateLawsProposal(raw) {
124
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, reason: 'bad_proposal', expected: '{ enact?: [law], repeal?: [law id] }' };
125
+ for (const k of Object.keys(raw)) {
126
+ if (k !== 'enact' && k !== 'repeal') return { ok: false, reason: 'unknown_field', field: k };
127
+ }
128
+ const enactIn = raw.enact == null ? [] : raw.enact;
129
+ const repealIn = raw.repeal == null ? [] : raw.repeal;
130
+ if (!Array.isArray(enactIn) || enactIn.length > MAX_ENACT) return { ok: false, reason: 'bad_enact', field: 'enact', expected: `a list of at most ${MAX_ENACT} laws` };
131
+ if (!Array.isArray(repealIn) || repealIn.length > MAX_REPEAL) return { ok: false, reason: 'bad_repeal', field: 'repeal', expected: `a list of at most ${MAX_REPEAL} law ids` };
132
+ if (!enactIn.length && !repealIn.length) return { ok: false, reason: 'nothing_to_change', expected: 'at least one law to enact or repeal' };
133
+ const enact = [];
134
+ for (let i = 0; i < enactIn.length; i++) {
135
+ const v = validateLaw(enactIn[i], i);
136
+ if (!v.ok) return v;
137
+ if (enact.some((l) => sameText(l.text, v.law.text))) return { ok: false, reason: 'duplicate_law', field: `enact[${i}].text` };
138
+ enact.push(v.law);
139
+ }
140
+ const repeal = [];
141
+ for (let i = 0; i < repealIn.length; i++) {
142
+ const r = repealIn[i];
143
+ const n = typeof r === 'string' && /^\d+$/.test(r) ? Number(r) : r;
144
+ if (typeof n !== 'number' || !Number.isSafeInteger(n) || n <= 0) return { ok: false, reason: 'bad_law_id', field: `repeal[${i}]` };
145
+ if (repeal.includes(String(n))) return { ok: false, reason: 'duplicate_law_id', field: `repeal[${i}]` };
146
+ repeal.push(String(n));
147
+ }
148
+ return { ok: true, proposal: { enact, repeal } };
149
+ }
150
+
151
+ // Against the laws in force: a repeal must name a law in force, an enacted law
152
+ // must not already be in force (unless the same proposal repeals it), and no
153
+ // setting may be locked by two laws at once. `inForce` rows: { id, text, locks }.
154
+ // Returns { ok: true } or a named refusal.
155
+ function checkAgainstInForce(proposal, inForce) {
156
+ const rows = Array.isArray(inForce) ? inForce : [];
157
+ const byId = new Map(rows.map((l) => [String(l.id), l]));
158
+ for (const id of proposal.repeal) {
159
+ if (!byId.has(id)) return { ok: false, reason: 'not_in_force', law_id: id };
160
+ }
161
+ const staying = rows.filter((l) => !proposal.repeal.includes(String(l.id)));
162
+ for (let i = 0; i < proposal.enact.length; i++) {
163
+ const law = proposal.enact[i];
164
+ const twin = staying.find((l) => sameText(l.text, law.text));
165
+ if (twin) return { ok: false, reason: 'already_in_force', field: `enact[${i}].text`, law_id: String(twin.id) };
166
+ for (const lock of law.locks) {
167
+ const holder = staying.find((l) => (Array.isArray(l.locks) ? l.locks : []).some((x) => x && x.setting === lock.setting))
168
+ || proposal.enact.slice(0, i).find((l) => l.locks.some((x) => x.setting === lock.setting));
169
+ if (holder) return { ok: false, reason: 'setting_already_locked', field: `enact[${i}].locks`, setting: lock.setting, ...(holder.id != null ? { law_id: String(holder.id) } : {}) };
170
+ }
171
+ }
172
+ return { ok: true };
173
+ }
174
+
175
+ // The record's own words for a proposal ("Law: Ask like a human. Repeal: ...").
176
+ function proposalWords(proposal, inForce) {
177
+ const byId = new Map((Array.isArray(inForce) ? inForce : []).map((l) => [String(l.id), l]));
178
+ const parts = [];
179
+ proposal.enact.forEach((l) => parts.push(`Law: ${l.text}`));
180
+ proposal.repeal.forEach((id) => parts.push(`Repeal: ${byId.has(id) ? byId.get(id).text : `law ${id}`}`));
181
+ return parts.join('. ').replace(/([.!?])\.(\s|$)/g, '$1$2').replace(/([^.!?])$/, '$1.');
182
+ }
183
+
184
+ // THE GUARD. A law may be written only when the row that just closed is a
185
+ // PASSED constitutional amendment whose amendment is of kind 'laws' and is the
186
+ // item's own subject. db.applyLaws carries the same condition in its SQL, and
187
+ // government_027's trigger refuses every other write — three layers.
188
+ function lawsWriteAllowed(closedRow, amendment) {
189
+ return !!closedRow && !!amendment
190
+ && closedRow.subject_type === 'constitutional_amendment'
191
+ && closedRow.outcome === 'passed'
192
+ && amendment.kind === 'laws'
193
+ && String(amendment.id) === String(closedRow.subject_id);
194
+ }
195
+
196
+ // ── the session read (task 1004707) ─────────────────────────────────────────
197
+ //
198
+ // What every AI session on the project reads: the laws in force, oldest first,
199
+ // numbered, each with what it means for an agent and the settings it locks.
200
+ // `rows`: db.listLaws() rows (in force only). Returns
201
+ // { laws: [{ id, no, text, effect_on_ai, locks }], contract, inject }
202
+ // `contract` is the block the Conductor relays verbatim; '' (and inject:false)
203
+ // when no law is in force, so a project with no laws spends no context. The
204
+ // text is one line per law by construction (government_027's CHECKs), so a law
205
+ // can never smuggle a second instruction onto a line of its own.
206
+ const lockWords = (lock) => (Object.prototype.hasOwnProperty.call(lock, 'value')
207
+ ? `${lock.setting} is fixed at ${lock.value}`
208
+ : `${lock.setting} is at most ${lock.max}`);
209
+
210
+ function forSession(rows) {
211
+ const list = (Array.isArray(rows) ? rows : [])
212
+ .filter((r) => r && r.id != null && r.repealed_at == null && typeof r.text === 'string');
213
+ const book = list.map((r, i) => ({
214
+ id: String(r.id),
215
+ no: i + 1,
216
+ text: r.text,
217
+ effect_on_ai: r.effect_on_ai || null,
218
+ locks: (Array.isArray(r.locks) ? r.locks : []).filter((l) => l && typeof l.setting === 'string'),
219
+ }));
220
+ if (!book.length) return { laws: [], contract: '', inject: false };
221
+ const lines = book.map((l) => {
222
+ let line = `${l.no}. ${l.text}`;
223
+ if (l.effect_on_ai) line += ` — ${l.effect_on_ai}`;
224
+ if (l.locks.length) line += ` (locks: ${l.locks.map(lockWords).join('; ')})`;
225
+ return line;
226
+ });
227
+ const contract = 'Project laws — passed by a vote of this project\'s government. Every AI agent on the project obeys them, and they come before this builder\'s own settings and preferences. A setting a law locks is enforced by the server; do not work around it:\n'
228
+ + lines.join('\n');
229
+ return { laws: book, contract, inject: true };
230
+ }
231
+
232
+ module.exports = {
233
+ forSession,
234
+ LAW_TEXT_MAX,
235
+ EFFECT_MAX,
236
+ MAX_LOCKS,
237
+ validateLawsProposal,
238
+ checkAgainstInForce,
239
+ proposalWords,
240
+ lawsWriteAllowed,
241
+ };
@@ -0,0 +1,170 @@
1
+ 'use strict';
2
+
3
+ // modules/government/leads.js — THE LEAD OF EACH BUILDING: who leads it, held as a
4
+ // position the government appoints and removes by vote (task 1004709, goal
5
+ // 1000111; ADR 0374). decision-board.js is the board side (opening the People
6
+ // matter, carrying out a passed one), board.js serves the read (GET
7
+ // /government/leads), and government_029 is the table and its guard.
8
+ //
9
+ // WHAT A LEAD IS. One builder in office at one of the nine buildings, carrying a
10
+ // short charter: one line saying what they answer for. A building has at most
11
+ // one Lead. A Lead is appointed and removed ONLY by a passed People matter — the
12
+ // actions appoint_lead { building, builder_id, charter } and remove_lead
13
+ // { building } (decision-board.js MATTER_ACTIONS) — and the table's trigger
14
+ // refuses every other write.
15
+ //
16
+ // WHO ANSWERS FOR A BUILDING. Its Lead; an UNSEATED building falls to the owner
17
+ // (owner ruling, 2026-10-09). The project has no owner field, so the owner seat
18
+ // is held by the founder's rank: every active Archon, the founding one (the
19
+ // lowest id, the Archon the project was bootstrapped with) first — never one
20
+ // Archon picked by a guess. answerFor() is that rule, written
21
+ // once, for the read here and for routing every ask to a seat (routing.js, task
22
+ // 1004705).
23
+ //
24
+ // WHAT IT DOES NOT DECIDE. Holding the Lead's office confers no authority of its
25
+ // own. Whether a building's holder may build it out without a vote comes from
26
+ // the FORM OF GOVERNMENT and the building's own decision rule (build-rights.js,
27
+ // task 1004710), never from the Lead or their charter; the read below carries
28
+ // that answer per building as `build_rights`. Positions on /me are task 1004713.
29
+ // What each building has grown under its holder rides the same read as
30
+ // `structure` (buildouts.js, task 1004711).
31
+ //
32
+ // PURE: no db, no requires beyond the buildings' vocabulary, the build rule and
33
+ // the build-outs' shape.
34
+
35
+ const { AREAS, AREA_KEYS } = require('./decision-rules');
36
+ const { buildRightsFor } = require('./build-rights');
37
+ const { structureOf } = require('./buildouts');
38
+
39
+ const CHARTER_MIN = 3;
40
+ const CHARTER_MAX = 140;
41
+
42
+ // The charter a Lead is appointed with when the matter names none: one line per
43
+ // building, saying what its Lead answers for.
44
+ const DEFAULT_CHARTERS = Object.freeze({
45
+ charter: 'The constitution: how the project is governed and how that changes.',
46
+ rules: 'The project settings and the standing rules of work.',
47
+ disputes: 'Security reports and the disputes over them.',
48
+ people: 'Who joins, and the ranks they hold.',
49
+ planning: 'Goals, priorities and the order of work.',
50
+ automation: 'Unattended building: when it runs and on what.',
51
+ review: 'Who checks finished work, and how.',
52
+ release: 'Releases and the path to going live.',
53
+ treasury: 'Spending limits and what the project pays for.',
54
+ });
55
+
56
+ const isBuilding = (b) => typeof b === 'string' && AREA_KEYS.includes(b);
57
+
58
+ // A charter line, normalised, or null when it breaks the rule (3-140 characters,
59
+ // one line). An absent charter takes the building's default.
60
+ function charterLine(raw, building) {
61
+ if (raw == null || raw === '') return DEFAULT_CHARTERS[building] || null;
62
+ if (typeof raw !== 'string' || /[\r\n]/.test(raw)) return null;
63
+ const s = raw.trim();
64
+ return s.length >= CHARTER_MIN && s.length <= CHARTER_MAX ? s : null;
65
+ }
66
+
67
+ // The two People matter actions that seat and unseat a Lead.
68
+ const LEAD_ACTS = Object.freeze(['appoint_lead', 'remove_lead']);
69
+ const isLeadAct = (type) => LEAD_ACTS.includes(type);
70
+
71
+ // The owner seat: every active Archon, the founding one (lowest id) first; nobody
72
+ // when the project has no active Archon.
73
+ function ownerOf(archons) {
74
+ const list = (archons || [])
75
+ .filter((b) => b && b.id != null)
76
+ .slice()
77
+ .sort((a, b) => Number(a.id) - Number(b.id))
78
+ .map((b) => ({ id: String(b.id), login: b.github_login || null, name: b.display_name || b.github_login || null }));
79
+ return { seat: 'owner', rank: 'archon', builders: list };
80
+ }
81
+
82
+ // Who answers for a building: its Lead in office, or the owner when unseated.
83
+ // `leadsInOffice` is a map building → lead ({ builder_id, ... }) or an array of rows.
84
+ function answerFor(building, leadsInOffice, owner) {
85
+ if (!isBuilding(building)) return null;
86
+ const rows = Array.isArray(leadsInOffice) ? leadsInOffice : Object.values(leadsInOffice || {});
87
+ const lead = rows.find((l) => l && l.building === building && !l.removed_at) || null;
88
+ if (lead) return { seat: `lead:${building}`, builder_ids: [String(lead.builder_id)], fell_to_owner: false };
89
+ const ids = ((owner && owner.builders) || []).map((b) => String(b.id));
90
+ return { seat: 'owner', builder_ids: ids, fell_to_owner: true };
91
+ }
92
+
93
+ // One Lead row as the read shows it.
94
+ function projectLead(r) {
95
+ return {
96
+ id: String(r.id),
97
+ building: r.building,
98
+ builder: { id: String(r.builder_id), login: r.github_login || null, name: r.display_name || r.github_login || null },
99
+ charter: r.charter,
100
+ since: r.appointed_at || null,
101
+ appointed_by: { item_id: String(r.appointed_by_item_id), href: `/board-room?item=${r.appointed_by_item_id}` },
102
+ ...(r.removed_at ? { removed_at: r.removed_at, removed_by: { item_id: String(r.removed_by_item_id), href: `/board-room?item=${r.removed_by_item_id}` } } : {}),
103
+ };
104
+ }
105
+
106
+ // Who may build a building out without a vote, from the context the server
107
+ // resolved (decision-board.leadsView): the board block in force, each building's
108
+ // resolved decision row, who sits on the board, who sits on each building's own
109
+ // sitting, and the goals hook. null when no context was read.
110
+ function rightsOf(building, answers, rights) {
111
+ if (!rights) return null;
112
+ const ids = ((answers && answers.builder_ids) || []).map(String);
113
+ const seatedIn = (list) => Array.isArray(list) && list.some((id) => ids.includes(String(id)));
114
+ const decision = (rights.decisions || []).find((d) => d && d.area === building) || null;
115
+ return buildRightsFor({
116
+ building,
117
+ board: rights.board,
118
+ decision,
119
+ holder: {
120
+ seat: answers ? answers.seat : null,
121
+ on_board: seatedIn(rights.boardMemberIds),
122
+ in_sitting: seatedIn((rights.sittingMemberIds || {})[building]),
123
+ },
124
+ goals: rights.goals || [],
125
+ });
126
+ }
127
+
128
+ // GET /government/leads: the nine buildings in street order, each with its Lead
129
+ // in office (or null), who answers for it, and what its holder may build without
130
+ // a vote (`build_rights`; null when the server could not read what it needs).
131
+ // `structure` ({ rows, pending, now }: the build-outs on the record and the
132
+ // open build_out sittings) gives each building what it has grown and what waits
133
+ // on a vote; without it each building's `structure` is null.
134
+ function leadsView(rows, archons, { includePast = false, rights = null, structure = null } = {}) {
135
+ const owner = ownerOf(archons);
136
+ const inOffice = (rows || []).filter((r) => r && !r.removed_at);
137
+ const buildings = AREAS.map((a) => {
138
+ const row = inOffice.find((r) => r.building === a.key) || null;
139
+ const answers = answerFor(a.key, inOffice, owner);
140
+ return {
141
+ building: a.key,
142
+ name: a.name,
143
+ lead: row ? projectLead(row) : null,
144
+ answers,
145
+ build_rights: rightsOf(a.key, answers, rights),
146
+ structure: structure ? structureOf(a.key, structure.rows, structure.pending, structure.now) : null,
147
+ };
148
+ });
149
+ return {
150
+ buildings,
151
+ owner,
152
+ seated: buildings.filter((b) => b.lead).length,
153
+ build_rights_from: 'the form of government and each building\'s own decision rule (goals may grant or limit it later), never a Lead\'s charter',
154
+ ...(includePast ? { past: (rows || []).filter((r) => r && r.removed_at).map(projectLead) } : {}),
155
+ changed_by: 'a passed People matter only: POST /government/board/matters with area people and an appoint_lead or remove_lead action',
156
+ grown_through: 'the building\'s holder: POST /government/buildouts builds at once where build_rights allows it, and otherwise files a build_out matter for a vote',
157
+ limits: { charter_min: CHARTER_MIN, charter_max: CHARTER_MAX },
158
+ };
159
+ }
160
+
161
+ module.exports = {
162
+ CHARTER_MAX,
163
+ DEFAULT_CHARTERS,
164
+ LEAD_ACTS,
165
+ isLeadAct,
166
+ charterLine,
167
+ answerFor,
168
+ ownerOf,
169
+ leadsView,
170
+ };