@bongos/core 1.21.113 → 1.21.114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/.bongos-core.json +603 -183
  2. package/.claude/skills/ask-for-help/SKILL.md +1 -1
  3. package/.claude/skills/bongos-feedback/SKILL.md +1 -1
  4. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  5. package/.claude/skills/builder-ship/SKILL.md +1 -0
  6. package/GOVERNMENT.md +128 -11
  7. package/clients/bongos-client/README.md +1 -1
  8. package/clients/bongos-client/bongos-client.global.js +28 -0
  9. package/clients/bongos-client/index.cjs +28 -0
  10. package/clients/bongos-client/index.d.ts +45 -2
  11. package/clients/bongos-client/index.mjs +28 -0
  12. package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
  13. package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
  14. package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
  15. package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
  16. package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
  17. package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
  18. package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
  19. package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
  20. package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
  21. package/docs/adr/README.md +10 -1
  22. package/docs/api/openapi.json +989 -87
  23. package/docs/api-reference.md +18 -4
  24. package/docs/copy-inventory.md +343 -262
  25. package/docs/copy-registry.json +1223 -426
  26. package/docs/file-map.md +2 -1
  27. package/docs/module-api-changelog.md +2 -0
  28. package/docs/packs/engineer.md +2 -1
  29. package/docs/page-inventory.json +91 -6
  30. package/docs/page-readings.json +5017 -4458
  31. package/modules/builder-settings/law-locks.js +202 -0
  32. package/modules/builder-settings/routes/settings.js +66 -6
  33. package/modules/builder-settings/settings.js +7 -0
  34. package/modules/discord/ship-broadcast.js +22 -13
  35. package/modules/government/asks-db.js +54 -0
  36. package/modules/government/asks.js +269 -0
  37. package/modules/government/board-idea-reads.js +68 -0
  38. package/modules/government/board.js +21 -57
  39. package/modules/government/build-rights.js +148 -0
  40. package/modules/government/buildouts.js +160 -0
  41. package/modules/government/counts.js +81 -0
  42. package/modules/government/db.js +242 -0
  43. package/modules/government/decision-board.js +234 -8
  44. package/modules/government/decision-rules.js +2 -0
  45. package/modules/government/docket.js +114 -6
  46. package/modules/government/law-board.js +131 -0
  47. package/modules/government/laws.js +241 -0
  48. package/modules/government/leads.js +170 -0
  49. package/modules/government/migrations/government_027_laws.sql +143 -0
  50. package/modules/government/migrations/government_028_asks.sql +57 -0
  51. package/modules/government/migrations/government_029_leads.sql +135 -0
  52. package/modules/government/migrations/government_030_buildouts.sql +111 -0
  53. package/modules/government/migrations/government_031_treasury.sql +107 -0
  54. package/modules/government/module.json +2 -1
  55. package/modules/government/pollers/treasury-borrow.js +62 -0
  56. package/modules/government/positions.js +107 -0
  57. package/modules/government/presets.js +149 -0
  58. package/modules/government/routes/government.js +425 -2
  59. package/modules/government/routing.js +154 -0
  60. package/modules/government/treasury-db.js +166 -0
  61. package/modules/government/treasury.js +241 -0
  62. package/modules/hall-ui/public/atlas.html +2 -2
  63. package/modules/hall-ui/public/blockers.html +2 -2
  64. package/modules/hall-ui/public/board-room.html +2 -2
  65. package/modules/hall-ui/public/board-room.js +2 -0
  66. package/modules/hall-ui/public/city-draw.js +604 -11
  67. package/modules/hall-ui/public/city.css +157 -1
  68. package/modules/hall-ui/public/city.html +37 -7
  69. package/modules/hall-ui/public/city.js +441 -21
  70. package/modules/hall-ui/public/city.states.json +110 -5
  71. package/modules/hall-ui/public/collab.html +2 -2
  72. package/modules/hall-ui/public/commands.html +2 -2
  73. package/modules/hall-ui/public/copy-desk.html +2 -2
  74. package/modules/hall-ui/public/craft-mode.js +112 -29
  75. package/modules/hall-ui/public/deploy.html +2 -2
  76. package/modules/hall-ui/public/diagram-viewer.css +12 -0
  77. package/modules/hall-ui/public/diagram-viewer.js +4 -0
  78. package/modules/hall-ui/public/diagrams.html +2 -2
  79. package/modules/hall-ui/public/drachmae.html +2 -2
  80. package/modules/hall-ui/public/fleet.html +2 -2
  81. package/modules/hall-ui/public/gate.html +2 -2
  82. package/modules/hall-ui/public/goal-map.html +2 -2
  83. package/modules/hall-ui/public/goals.html +2 -2
  84. package/modules/hall-ui/public/gov-feeds.js +141 -0
  85. package/modules/hall-ui/public/gov-living.js +1002 -0
  86. package/modules/hall-ui/public/gov-positions.js +97 -0
  87. package/modules/hall-ui/public/government.html +2 -2
  88. package/modules/hall-ui/public/hall-render.js +3 -0
  89. package/modules/hall-ui/public/idea.html +2 -2
  90. package/modules/hall-ui/public/ideas.html +2 -2
  91. package/modules/hall-ui/public/index.html +3 -3
  92. package/modules/hall-ui/public/modules.html +2 -2
  93. package/modules/hall-ui/public/my-agents.css +334 -0
  94. package/modules/hall-ui/public/my-agents.html +72 -0
  95. package/modules/hall-ui/public/my-agents.js +603 -0
  96. package/modules/hall-ui/public/my-agents.states.json +37 -0
  97. package/modules/hall-ui/public/primer.html +2 -2
  98. package/modules/hall-ui/public/profile-connections.js +40 -28
  99. package/modules/hall-ui/public/profile.html +2 -2
  100. package/modules/hall-ui/public/project-settings.html +2 -2
  101. package/modules/hall-ui/public/ranks.html +2 -2
  102. package/modules/hall-ui/public/roadmap.html +2 -2
  103. package/modules/hall-ui/public/room.css +8 -0
  104. package/modules/hall-ui/public/roster.html +2 -2
  105. package/modules/hall-ui/public/sessions.html +2 -2
  106. package/modules/hall-ui/public/settings.html +2 -2
  107. package/modules/hall-ui/public/shell.js +116 -16
  108. package/modules/hall-ui/public/street-card.js +18 -1
  109. package/modules/hall-ui/public/street-deck.css +299 -0
  110. package/modules/hall-ui/public/street-deck.js +1145 -0
  111. package/modules/hall-ui/public/street-depth.js +712 -0
  112. package/modules/hall-ui/public/street-desk.js +5 -2
  113. package/modules/hall-ui/public/street-draw.js +109 -16
  114. package/modules/hall-ui/public/street-inside.js +137 -22
  115. package/modules/hall-ui/public/street-office.js +705 -0
  116. package/modules/hall-ui/public/street.css +161 -11
  117. package/modules/hall-ui/public/street.html +41 -11
  118. package/modules/hall-ui/public/street.js +78 -66
  119. package/modules/hall-ui/public/street.states.json +129 -21
  120. package/modules/hall-ui/public/studio.html +2 -2
  121. package/modules/hall-ui/public/style.css +5 -1
  122. package/modules/hall-ui/public/task.html +2 -2
  123. package/modules/hall-ui/public/thinking.html +2 -2
  124. package/modules/hall-ui/public/tweak-editor.html +2 -2
  125. package/modules/hall-ui/public/watch.html +2 -2
  126. package/modules/hall-ui/public/watch.js +14 -1
  127. package/modules/hall-ui/public/work.html +2 -2
  128. package/modules/hall-ui/records/living-city.md +57 -0
  129. package/modules/hall-ui/records/my-agents.md +21 -0
  130. package/modules/hall-ui/records/oversight-family.md +1 -0
  131. package/modules/hall-ui/records/profile-connections.md +10 -0
  132. package/modules/hall-ui/records/reading-rooms.md +2 -0
  133. package/modules/ideas/routes/blockers.js +92 -3
  134. package/modules/lifecycle/module.json +2 -1
  135. package/modules/lifecycle/moving.js +17 -1
  136. package/modules/lifecycle/routes/moving.js +6 -0
  137. package/modules/lifecycle/routes/project-settings.js +23 -0
  138. package/modules/onboarding/approval-broadcast.js +8 -0
  139. package/modules/onboarding/routes/access-requests.js +16 -6
  140. package/modules/platform-identity/connections.js +13 -2
  141. package/modules/platform-identity/routes/connections.js +7 -1
  142. package/modules/public-landing/public/account-private.states.json +2 -2
  143. package/modules/public-landing/public/account.html +65 -5
  144. package/modules/public-landing/public/account.states.json +3 -3
  145. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  146. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  147. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
  148. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  149. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  150. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
  151. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  152. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  153. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
  154. package/modules/public-landing/public/assets/world.css +4 -4
  155. package/modules/public-landing/public/connections-empty.states.json +14 -0
  156. package/modules/public-landing/public/connections-inactive.states.json +14 -0
  157. package/modules/public-landing/public/connections.html +450 -0
  158. package/modules/public-landing/public/connections.probes.json +414 -0
  159. package/modules/public-landing/public/connections.states.json +17 -0
  160. package/modules/public-landing/public/contact.html +2 -2
  161. package/modules/public-landing/public/guild.html +2 -2
  162. package/modules/public-landing/public/index.html +2 -2
  163. package/modules/public-landing/public/invitations.html +2 -2
  164. package/modules/public-landing/public/privacy.html +2 -2
  165. package/modules/public-landing/public/projects.html +2 -2
  166. package/modules/public-landing/public/projects.probes.json +6 -2
  167. package/modules/public-landing/public/terms.html +2 -2
  168. package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
  169. package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
  170. package/modules/ui-design/kit/fixtures/connections.json +22 -0
  171. package/modules/ui-design/kit/serve.js +95 -0
  172. package/package-lock.json +2 -2
  173. package/package.json +1 -1
  174. package/release-notes.json +122 -0
  175. package/scripts/discord/README.md +8 -5
  176. package/scripts/discord/post.js +12 -1
  177. package/scripts/gds/artifact-audit.js +20 -3
  178. package/scripts/gds/ask.js +164 -0
  179. package/scripts/gds/autobongos-verify.js +21 -2
  180. package/scripts/gds/government-walk.js +638 -4
  181. package/scripts/gds/pulse.js +9 -6
  182. package/scripts/gds/ship-escalation.js +22 -3
  183. package/scripts/gds/ship-finish.js +5 -4
  184. package/scripts/gds/ship-merge-gate.js +18 -6
  185. package/scripts/hall-preview/README.md +41 -1
  186. package/scripts/hall-preview/pref-writes.js +140 -0
  187. package/scripts/hall-preview/refresh-fixtures.js +101 -7
  188. package/scripts/hall-preview/scrub-identity.js +366 -7
  189. package/scripts/hall-preview/server.js +27 -1
  190. package/scripts/render-diagrams.sh +4 -2
  191. package/src/bongos/auth-idp.js +6 -1
  192. package/src/bongos/project-settings.js +41 -0
  193. package/src/bongos/route-rank-check.js +27 -0
  194. package/src/bongos/routes/me.js +68 -7
  195. package/src/bongos/serve-internal.js +5 -1
  196. package/src/module-api.js +7 -1
  197. package/src/platform-server.js +10 -0
  198. package/tests/application_lifecycle.mjs +47 -5
  199. package/tests/conductor_main_e2e.mjs +21 -0
  200. package/tests/connections_api.mjs +18 -0
  201. package/tests/craft_mode_nav.mjs +5 -3
  202. package/tests/craft_mode_tallies.mjs +52 -16
  203. package/tests/discord_approvals.mjs +11 -0
  204. package/tests/discord_channels.mjs +20 -0
  205. package/tests/fixtures/gov-seated.mjs +64 -0
  206. package/tests/fixtures/government-ask-cards.json +1284 -0
  207. package/tests/government_act_alone_buildings.mjs +1 -0
  208. package/tests/government_agent_asks.mjs +322 -0
  209. package/tests/government_asks.mjs +408 -0
  210. package/tests/government_build_rights.mjs +253 -0
  211. package/tests/government_buildouts.mjs +382 -0
  212. package/tests/government_counts.mjs +143 -0
  213. package/tests/government_decision_rules.mjs +5 -1
  214. package/tests/government_laws.mjs +488 -0
  215. package/tests/government_laws_session.mjs +321 -0
  216. package/tests/government_leads.mjs +375 -0
  217. package/tests/government_positions.mjs +127 -0
  218. package/tests/government_presets.mjs +215 -0
  219. package/tests/government_routes.mjs +8 -1
  220. package/tests/government_routing.mjs +118 -0
  221. package/tests/government_seed.mjs +5 -0
  222. package/tests/government_treasury.mjs +394 -0
  223. package/tests/governor_docket.mjs +26 -2
  224. package/tests/hall_buildouts.mjs +208 -0
  225. package/tests/hall_city.mjs +14 -6
  226. package/tests/hall_city_living.mjs +617 -0
  227. package/tests/hall_gov_counts.mjs +336 -0
  228. package/tests/hall_gov_living.mjs +636 -0
  229. package/tests/hall_gov_real_site.mjs +655 -0
  230. package/tests/hall_matter_card.mjs +6 -1
  231. package/tests/hall_my_agents.mjs +481 -0
  232. package/tests/hall_nav.mjs +107 -4
  233. package/tests/hall_page_gate_map.mjs +4 -0
  234. package/tests/hall_preview_governor_fixtures.mjs +30 -3
  235. package/tests/hall_preview_scrub.mjs +59 -1
  236. package/tests/hall_preview_security.mjs +169 -0
  237. package/tests/hall_reading_world.mjs +44 -0
  238. package/tests/hall_roster_world.mjs +6 -2
  239. package/tests/hall_street.mjs +18 -2
  240. package/tests/hall_street_amend.mjs +1 -1
  241. package/tests/hall_street_deck.mjs +609 -0
  242. package/tests/hall_street_depth.mjs +559 -0
  243. package/tests/hall_street_desk.mjs +1 -1
  244. package/tests/hall_street_inside.mjs +29 -5
  245. package/tests/hall_tweak_editor.mjs +4 -1
  246. package/tests/hub_account_page.mjs +94 -3
  247. package/tests/hub_connections_page.mjs +507 -0
  248. package/tests/hub_invite_badge.mjs +1 -1
  249. package/tests/idp_device_exchange.mjs +18 -0
  250. package/tests/landing_legal_pages.mjs +8 -5
  251. package/tests/landing_page.mjs +23 -4
  252. package/tests/nav_permission_atoms.mjs +4 -1
  253. package/tests/profile_connections_ui.mjs +88 -25
  254. package/tests/projects_hub.mjs +15 -0
  255. package/tests/render_check.mjs +2 -2
  256. package/tests/ship_broadcast_routing.mjs +57 -16
  257. package/tests/ship_broadcast_server.mjs +40 -7
  258. package/tests/ship_merge_gate.mjs +19 -0
  259. package/tests/ui_design_kit.mjs +2 -2
  260. package/tests/watch_applications_queue.mjs +23 -0
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ // modules/government/positions.js — THE POSITIONS A BUILDER HOLDS (task 1004713,
4
+ // goal 1000111; ADR 0377).
5
+ //
6
+ // THE OWNER'S RULING (2026-10-09). A seat in the government is HELD, not a picked
7
+ // craft: you hold a seat in the government or you don't. A position is
8
+ // owner the owner seat — every active Archon (leads.ownerOf)
9
+ // lead the Lead in office at a building (government_leads, by vote)
10
+ // seat a seat on a building's own sitting: its decision-rules row puts
11
+ // matters to a sitting of the people its predicate names
12
+ // board a member of the board under the constitution in force
13
+ //
14
+ // WHERE EACH ANSWER COMES FROM. Never a second rule: the owner and Lead seats are
15
+ // asked of routing.resolveSeat (which asks leads.answerFor), the board and each
16
+ // building's sitting of board-membership's membership predicate. This file only
17
+ // folds the answers into one list for one builder. GET /me carries it as
18
+ // `positions` (through the government port), the way it carries `permissions`,
19
+ // and the hall shows the governing surfaces and the Governing mode — named by the
20
+ // first seat — only to someone who holds one. Cosmetic there: every page and
21
+ // route keeps its own server-side gate (ADR 0016).
22
+ //
23
+ // WHAT A POSITION IS NOT. A craft (PATCH /me/disciplines is ungated, so a
24
+ // craft-derived seat would be self-appointment), and an authority: holding one
25
+ // grants no permission (leads.js: the Lead's office confers none).
26
+ //
27
+ // PURE: no db. readPositions() below takes the reads it needs as functions, so
28
+ // routes/government.js asks them per request, uncached, and a test hands fakes.
29
+
30
+ const { resolveSeat, BUILDING_NAMES } = require('./routing');
31
+ const { ownerOf } = require('./leads');
32
+
33
+ const BUILDINGS = Object.freeze(Object.keys(BUILDING_NAMES));
34
+ // Which seat names the Governing mode when a builder holds several.
35
+ const KIND_ORDER = Object.freeze({ lead: 0, owner: 1, seat: 2, board: 3 });
36
+ const KINDS = Object.freeze(Object.keys(KIND_ORDER));
37
+ // Decision-rules values that name no predicate of their own: the board's sitting
38
+ // is the board seat, and a fixed gate or nobody seats no one.
39
+ const NOT_A_PREDICATE = Object.freeze(['board', 'gate', 'nobody']);
40
+
41
+ function titleOf(kind, building) {
42
+ const name = BUILDING_NAMES[building] || '';
43
+ if (kind === 'lead') return `${name} Lead`;
44
+ if (kind === 'seat') return `${name} seat`;
45
+ if (kind === 'owner') return 'Owner';
46
+ return 'Board member';
47
+ }
48
+
49
+ // positionsOf(builderId, ctx) → { held[], title, has_position }
50
+ // ctx.leads Lead rows in office (db.listLeads)
51
+ // ctx.owner leads.ownerOf(active Archons)
52
+ // ctx.board true when the builder sits on the board in force
53
+ // ctx.sittings [building] — the buildings whose own sitting seats them
54
+ function positionsOf(builderId, ctx) {
55
+ const c = ctx || {};
56
+ const id = builderId == null || builderId === '' ? null : String(builderId);
57
+ const held = [];
58
+ if (id) {
59
+ const seats = { leads: c.leads || [], owner: c.owner || { builders: [] } };
60
+ if (resolveSeat('owner', seats).builder_ids.includes(id)) held.push({ kind: 'owner', seat: 'owner', building: null });
61
+ for (const b of BUILDINGS) {
62
+ const r = resolveSeat(`lead:${b}`, seats);
63
+ if (!r.fell_to_owner && r.builder_ids.includes(id)) held.push({ kind: 'lead', seat: `lead:${b}`, building: b });
64
+ }
65
+ for (const b of BUILDINGS) {
66
+ if ((c.sittings || []).includes(b)) held.push({ kind: 'seat', seat: `board:${b}`, building: b });
67
+ }
68
+ if (c.board === true) held.push({ kind: 'board', seat: 'board', building: null });
69
+ }
70
+ held.sort((a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind]);
71
+ const positions = held.map((p) => ({ ...p, title: titleOf(p.kind, p.building) }));
72
+ return { held: positions, title: positions.length ? positions[0].title : null, has_position: positions.length > 0 };
73
+ }
74
+
75
+ // readPositions(builderId, reads) → the /me `positions` block, or null when any
76
+ // read failed (unknown is never "holds none": the hall then keeps its nav).
77
+ // reads.listLeads() Lead rows in office
78
+ // reads.listActiveArchons() the owner seat's holders
79
+ // reads.board() the board block in force
80
+ // reads.decisions(board) the resolved decision-rules rows
81
+ // reads.sits(id, membership) does the builder satisfy this predicate (fail closed)
82
+ async function readPositions(builderId, reads) {
83
+ if (builderId == null || builderId === '') return null;
84
+ try {
85
+ const board = reads.board() || {};
86
+ const [leads, archons, decisions, onBoard] = await Promise.all([
87
+ reads.listLeads(), reads.listActiveArchons(), reads.decisions(board), reads.sits(builderId, board.membership),
88
+ ]);
89
+ const sittings = [];
90
+ for (const d of decisions || []) {
91
+ if (!d || !d.sitting || !BUILDINGS.includes(d.area)) continue;
92
+ if (NOT_A_PREDICATE.includes(d.decided_by)) continue;
93
+ if (await reads.sits(builderId, d.decided_by)) sittings.push(d.area);
94
+ }
95
+ const p = positionsOf(builderId, { leads, owner: ownerOf(archons), board: onBoard === true, sittings });
96
+ return {
97
+ held: p.held,
98
+ title: p.title,
99
+ has_position: p.has_position,
100
+ from: 'the Leads in office, the owner seat (every active Archon), the board and each building\'s sitting under the constitution in force; never a craft',
101
+ };
102
+ } catch (_) {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ module.exports = { positionsOf, readPositions, titleOf, KINDS, KIND_ORDER };
@@ -0,0 +1,149 @@
1
+ 'use strict';
2
+
3
+ // modules/government/presets.js — HOW STRICT a project runs its AI agents
4
+ // (task 1004715, goal 1000111 "government is how a project steers its AI").
5
+ //
6
+ // A project picks how strict it is the way it picks a form of government. A
7
+ // STRICTNESS PRESET is a named bundle of laws (laws.js) and the locks they put on
8
+ // every builder's personal settings (builder-settings/law-locks.js):
9
+ //
10
+ // Open workshop few laws: land only, and going live waits for a person.
11
+ // Guild the standard set: Open's law, plus stuck agents skip on, and
12
+ // agents stay on the task (wandering capped at "focused").
13
+ // Factory strict: Guild's laws made tight (wandering fixed at "locked"),
14
+ // plus caps on every model slot, the ask format, and no
15
+ // unattended runs.
16
+ //
17
+ // NOTHING HERE WRITES A LAW, and nothing is seeded. A preset is a CATALOGUE: its
18
+ // laws are in force only once a vote passes them. Switching presets is ONE laws
19
+ // amendment (switchProposal → law-board.proposeLaws), filed and decided like any
20
+ // other: it enacts the preset's laws not yet in force and repeals the laws of the
21
+ // other presets that are, and it touches no law that belongs to no preset (a law
22
+ // the project passed on its own stays). The board passing it is the only write
23
+ // (board.closeItem → law-board.applyPassedLaws, guarded by government_027's
24
+ // trigger).
25
+ //
26
+ // WHICH PRESET IS IN FORCE is READ from the laws in force, never stored: a preset
27
+ // is in force when every one of its laws is (same words, same locks) and no other
28
+ // preset's law is. Anything else — a fresh project with no laws, or a book that
29
+ // mixes presets — is "no preset" (`current: null`), said plainly.
30
+ //
31
+ // WHAT EACH WOULD LOCK. The locks in force after a switch: the preset's own plus
32
+ // those of every law it would keep (the project's own laws). The hall counts them
33
+ // against the viewer's rows (My agents' preset strip, the Charter).
34
+ //
35
+ // ENFORCEMENT is the laws' own: a lock on a personal setting is enforced by the
36
+ // server on every read and write (law-locks.js); the ask format is already
37
+ // required of every AI session's blocker (ADR 0373); "No unattended runs" reaches
38
+ // every session as a law's words, and nothing yet refuses an unattended run.
39
+ //
40
+ // PURE: no db, no requires beyond laws.js. The route hands the laws in force in.
41
+
42
+ const laws = require('./laws');
43
+
44
+ // The laws a preset is made of. A law shared by two presets carries the same
45
+ // words AND the same locks in both, so a switch between them keeps it.
46
+ const LAW = {
47
+ land: { text: 'Land only. Ask before going live', effect_on_ai: 'Agents merge finished work; going live always waits for a person.', locks: [] },
48
+ stuck: { text: 'Stuck? Note it, skip it, keep going', effect_on_ai: 'A stuck agent leaves a note on the task and moves to the next one.', locks: [] },
49
+ focus: { text: 'Stay on the task; note side ideas', effect_on_ai: 'Agents finish the task in hand and leave one note for anything else.', locks: [{ setting: 'wandering', max: 'focused' }] },
50
+ tight: { text: 'Stay on the task; no side trips', effect_on_ai: 'Agents do the task in hand and nothing else.', locks: [{ setting: 'wandering', value: 'locked' }] },
51
+ ask: { text: 'Ask like a human', effect_on_ai: 'Every question to a person is one short card with tap answers, in the ask format.', locks: [] },
52
+ watched: { text: 'No unattended runs', effect_on_ai: 'A person starts and watches every agent run; nothing runs overnight or on a schedule.', locks: [] },
53
+ caps: {
54
+ text: 'Models stay under their caps',
55
+ effect_on_ai: 'The main session runs at most opus, helpers at most sonnet, routine work on haiku.',
56
+ locks: [{ setting: 'model.main', max: 'opus' }, { setting: 'model.subagentDefault', max: 'sonnet' }, { setting: 'model.subagentRoutine', max: 'haiku' }],
57
+ },
58
+ };
59
+
60
+ const PRESETS = Object.freeze([
61
+ { key: 'open', name: 'Open workshop', line: 'Few laws. Every builder keeps their own settings.', laws: [LAW.land] },
62
+ { key: 'guild', name: 'Guild', line: 'The standard set. Laws lock the settings they name; the rest is yours.', laws: [LAW.land, LAW.stuck, LAW.focus] },
63
+ { key: 'factory', name: 'Factory', line: 'Strict. Caps on, the ask format enforced, no unattended runs.', laws: [LAW.land, LAW.stuck, LAW.tight, LAW.ask, LAW.watched, LAW.caps] },
64
+ ]);
65
+ const KEYS = PRESETS.map((p) => p.key);
66
+
67
+ const norm = (t) => String(t == null ? '' : t).trim().toLowerCase().replace(/\s+/g, ' ');
68
+ const lockKey = (l) => (Object.prototype.hasOwnProperty.call(l, 'value') ? `${l.setting}=${JSON.stringify(l.value)}` : `${l.setting}<=${JSON.stringify(l.max)}`);
69
+ const locksKey = (locks) => (Array.isArray(locks) ? locks : []).filter((l) => l && typeof l.setting === 'string').map(lockKey).sort().join('|');
70
+ // Every preset law's words, in any preset: the laws a switch may repeal.
71
+ const CATALOGUE = new Set(PRESETS.reduce((a, p) => a.concat(p.laws.map((l) => norm(l.text))), []));
72
+
73
+ const presetOf = (key) => PRESETS.find((p) => p.key === key) || null;
74
+ const inForceOnly = (rows) => (Array.isArray(rows) ? rows : []).filter((r) => r && r.id != null && r.repealed_at == null && typeof r.text === 'string');
75
+ const same = (row, law) => norm(row.text) === norm(law.text) && locksKey(row.locks) === locksKey(law.locks);
76
+
77
+ // The preset in force, read from the laws in force (`rows`: db.listLaws() rows).
78
+ // Returns a preset key, or null when none is.
79
+ function currentPreset(rows) {
80
+ const ours = inForceOnly(rows).filter((r) => CATALOGUE.has(norm(r.text)));
81
+ if (!ours.length) return null;
82
+ const hit = PRESETS.find((p) => p.laws.length === ours.length && p.laws.every((law) => ours.some((r) => same(r, law))));
83
+ return hit ? hit.key : null;
84
+ }
85
+
86
+ // The ONE laws amendment a switch to `key` files: enact the preset's laws not in
87
+ // force (as written), repeal every other preset law in force (including one with
88
+ // the right words but other locks — the preset's own version replaces it). A law
89
+ // that is no preset's is never touched. Returns
90
+ // { ok: true, proposal: { enact, repeal }, rationale } or { ok: false, reason }.
91
+ function switchProposal(key, rows) {
92
+ const p = presetOf(key);
93
+ if (!p) return { ok: false, reason: 'unknown_preset', expected: KEYS.join(' | ') };
94
+ const inForce = inForceOnly(rows);
95
+ if (currentPreset(inForce) === key) return { ok: false, reason: 'preset_in_force' };
96
+ const kept = new Set();
97
+ const enact = [];
98
+ p.laws.forEach((law) => {
99
+ const row = inForce.find((r) => same(r, law));
100
+ if (row) kept.add(String(row.id));
101
+ else enact.push({ text: law.text, effect_on_ai: law.effect_on_ai, locks: law.locks.map((l) => ({ ...l })) });
102
+ });
103
+ const repeal = inForce.filter((r) => CATALOGUE.has(norm(r.text)) && !kept.has(String(r.id))).map((r) => String(r.id));
104
+ // the same strict check every laws proposal passes (shape, limits)
105
+ const v = laws.validateLawsProposal({ enact, repeal });
106
+ if (!v.ok) return v;
107
+ const from = currentPreset(inForce);
108
+ const fromName = from ? presetOf(from).name : 'no preset';
109
+ return { ok: true, proposal: v.proposal, rationale: `Switch from ${fromName} to ${p.name}: ${p.line}` };
110
+ }
111
+
112
+ // Every lock in force after a switch to `key`: the preset's own, plus every lock
113
+ // of a law the switch keeps (one that is no preset's).
114
+ function locksAfter(key, rows) {
115
+ const p = presetOf(key);
116
+ if (!p) return [];
117
+ const out = [];
118
+ const add = (law) => (Array.isArray(law.locks) ? law.locks : []).forEach((l) => {
119
+ if (l && typeof l.setting === 'string' && !out.some((x) => x.setting === l.setting)) out.push({ ...l, law_text: law.text });
120
+ });
121
+ inForceOnly(rows).filter((r) => !CATALOGUE.has(norm(r.text))).forEach(add);
122
+ p.laws.forEach(add);
123
+ return out;
124
+ }
125
+
126
+ // GET /government/presets: the three presets, which one is in force, and for each
127
+ // its laws and the locks a switch to it would leave in force.
128
+ function presetsView(rows) {
129
+ const current = currentPreset(rows);
130
+ return {
131
+ current,
132
+ presets: PRESETS.map((p) => ({
133
+ key: p.key,
134
+ name: p.name,
135
+ line: p.line,
136
+ current: p.key === current,
137
+ laws: p.laws.map((l) => ({ text: l.text, effect_on_ai: l.effect_on_ai, locks: l.locks.map((x) => ({ ...x })) })),
138
+ locks: locksAfter(p.key, rows),
139
+ })),
140
+ changed_by: 'a passed laws vote only: POST /government/board/presets files one',
141
+ };
142
+ }
143
+
144
+ module.exports = {
145
+ PRESETS,
146
+ currentPreset,
147
+ switchProposal,
148
+ presetsView,
149
+ };