@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,143 @@
1
+ -- government_027_laws.sql — standing orders: the project's laws for its AI
2
+ -- agents, changed only by a passed amendment (task 1004706, goal 1000111).
3
+ --
4
+ -- WHAT THIS ADDS
5
+ --
6
+ -- 1. government_amendments.kind gains 'laws' — an amendment that enacts new
7
+ -- laws, repeals laws in force, or both. One amendments table, one subject
8
+ -- type, one pass path: a laws amendment is decided by the board IN FORCE
9
+ -- exactly like any other amendment (government_023's pattern).
10
+ --
11
+ -- 2. government_laws — ONE ROW PER LAW, ever. A law is a short one-line rule
12
+ -- every AI agent on the project obeys, an optional line saying what it
13
+ -- means for an agent, and the personal settings it LOCKS (a JSON list of
14
+ -- { setting, value } or { setting, max }). Each row records WHO passed it
15
+ -- and WHEN: the board item that passed it (passed_by_item_id — its
16
+ -- snapshotted constitution is the government that decided it, its yes
17
+ -- votes are who said yes), the proposer, and passed_at. A repeal records
18
+ -- the item that repealed it and when; the row stays on the record.
19
+ --
20
+ -- 3. A GUARD TRIGGER: a law is written ONLY by a passed laws amendment.
21
+ -- INSERT — passed_by_item_id must be a CLOSED, PASSED constitutional
22
+ -- amendment whose amendment row is of kind 'laws'; the row is
23
+ -- born in force (no repeal) and passed_at is the database clock.
24
+ -- UPDATE — only a repeal: a law in force gains repealed_by_item_id (a
25
+ -- passed laws amendment) and repealed_at; nothing else about
26
+ -- it may change, and a repealed law never changes again.
27
+ -- DELETE — refused outright. A law leaves force by a repeal, on the record.
28
+ -- There is no route that writes this table, and the trigger makes that
29
+ -- structural: a hand-written INSERT or UPDATE is refused too.
30
+ --
31
+ -- No law is seeded here: the first laws are the owner's to ratify (task
32
+ -- 1004708). Additive and idempotent: a new table, a CHECK that only WIDENS
33
+ -- (dropped and re-added under the same name, the government_020 pattern), and
34
+ -- CREATE OR REPLACE for the trigger function and trigger (Postgres 14+;
35
+ -- production runs 16).
36
+
37
+ BEGIN;
38
+
39
+ -- ---------------------------------------------------------------------------
40
+ -- 1. amendments: a third kind
41
+ -- ---------------------------------------------------------------------------
42
+ ALTER TABLE government_amendments
43
+ DROP CONSTRAINT IF EXISTS government_amendments_kind_check;
44
+
45
+ ALTER TABLE government_amendments
46
+ ADD CONSTRAINT government_amendments_kind_check
47
+ CHECK (kind IN ('constitution', 'decision_rules', 'laws'));
48
+
49
+ -- ---------------------------------------------------------------------------
50
+ -- 2. the law book
51
+ -- ---------------------------------------------------------------------------
52
+ CREATE TABLE IF NOT EXISTS government_laws (
53
+ id bigserial PRIMARY KEY,
54
+ -- one line: what every agent obeys
55
+ text text NOT NULL
56
+ CHECK (char_length(btrim(text)) BETWEEN 3 AND 100 AND text !~ '[\r\n]'),
57
+ -- one line: what it means for an agent (display; the session reads it, 1004707)
58
+ effect_on_ai text CHECK (effect_on_ai IS NULL OR (char_length(effect_on_ai) BETWEEN 1 AND 200 AND effect_on_ai !~ '[\r\n]')),
59
+ -- the personal settings it locks: [{ "setting": k, "value": v } | { "setting": k, "max": m }]
60
+ locks jsonb NOT NULL DEFAULT '[]'::jsonb
61
+ CHECK (jsonb_typeof(locks) = 'array' AND jsonb_array_length(locks) <= 8),
62
+ proposed_by bigint REFERENCES builders(id) ON DELETE SET NULL,
63
+ -- WHO passed it: the passed laws amendment's board item (its constitution
64
+ -- snapshot is the government that decided it; its yes votes, who said yes)
65
+ passed_by_item_id bigint NOT NULL REFERENCES government_board_items(id) ON DELETE RESTRICT,
66
+ -- WHEN: the database clock at the close that passed it (set by the trigger)
67
+ passed_at timestamptz NOT NULL DEFAULT now(),
68
+ repealed_by_item_id bigint REFERENCES government_board_items(id) ON DELETE RESTRICT,
69
+ repealed_at timestamptz,
70
+
71
+ CONSTRAINT government_laws_repeal_shape CHECK ((repealed_by_item_id IS NULL) = (repealed_at IS NULL))
72
+ );
73
+
74
+ -- The laws in force, in the order they passed (the read every session will make).
75
+ CREATE INDEX IF NOT EXISTS government_laws_in_force_idx
76
+ ON government_laws (passed_at, id) WHERE repealed_at IS NULL;
77
+
78
+ -- ---------------------------------------------------------------------------
79
+ -- 3. the guard: written only by a passed laws amendment
80
+ -- ---------------------------------------------------------------------------
81
+ CREATE OR REPLACE FUNCTION government_laws_passed_laws_item(p_item_id bigint) RETURNS boolean
82
+ LANGUAGE sql STABLE AS $fn$
83
+ SELECT $1 IS NOT NULL AND EXISTS (
84
+ SELECT 1
85
+ FROM government_board_items i
86
+ JOIN government_amendments a ON a.id = i.subject_id
87
+ WHERE i.id = $1
88
+ AND i.subject_type = 'constitutional_amendment'
89
+ AND i.outcome = 'passed'
90
+ AND a.kind = 'laws'
91
+ )
92
+ $fn$;
93
+
94
+ CREATE OR REPLACE FUNCTION government_laws_guard() RETURNS trigger
95
+ LANGUAGE plpgsql AS $guard$
96
+ BEGIN
97
+ IF TG_OP = 'DELETE' THEN
98
+ RAISE EXCEPTION 'government_laws: a law cannot be deleted; it leaves force only by a passed repeal'
99
+ USING ERRCODE = 'check_violation';
100
+ END IF;
101
+
102
+ IF TG_OP = 'INSERT' THEN
103
+ IF NOT government_laws_passed_laws_item(NEW.passed_by_item_id) THEN
104
+ RAISE EXCEPTION 'government_laws changes only through a passed laws amendment (board item %)', NEW.passed_by_item_id
105
+ USING ERRCODE = 'check_violation';
106
+ END IF;
107
+ IF NEW.repealed_by_item_id IS NOT NULL OR NEW.repealed_at IS NOT NULL THEN
108
+ RAISE EXCEPTION 'government_laws: a law is born in force'
109
+ USING ERRCODE = 'check_violation';
110
+ END IF;
111
+ NEW.passed_at := now();
112
+ RETURN NEW;
113
+ END IF;
114
+
115
+ -- UPDATE: only a repeal of a law in force, by a passed laws amendment.
116
+ IF OLD.repealed_at IS NOT NULL THEN
117
+ RAISE EXCEPTION 'government_laws: law % was repealed and is closed to change', OLD.id
118
+ USING ERRCODE = 'check_violation';
119
+ END IF;
120
+ IF NEW.id IS DISTINCT FROM OLD.id
121
+ OR NEW.text IS DISTINCT FROM OLD.text
122
+ OR NEW.effect_on_ai IS DISTINCT FROM OLD.effect_on_ai
123
+ OR NEW.locks IS DISTINCT FROM OLD.locks
124
+ OR NEW.proposed_by IS DISTINCT FROM OLD.proposed_by
125
+ OR NEW.passed_by_item_id IS DISTINCT FROM OLD.passed_by_item_id
126
+ OR NEW.passed_at IS DISTINCT FROM OLD.passed_at THEN
127
+ RAISE EXCEPTION 'government_laws: a law in force cannot be edited; repeal it and pass a new one'
128
+ USING ERRCODE = 'check_violation';
129
+ END IF;
130
+ IF NOT government_laws_passed_laws_item(NEW.repealed_by_item_id) THEN
131
+ RAISE EXCEPTION 'government_laws changes only through a passed laws amendment (board item %)', NEW.repealed_by_item_id
132
+ USING ERRCODE = 'check_violation';
133
+ END IF;
134
+ NEW.repealed_at := now();
135
+ RETURN NEW;
136
+ END
137
+ $guard$;
138
+
139
+ CREATE OR REPLACE TRIGGER government_laws_amendment_only
140
+ BEFORE INSERT OR UPDATE OR DELETE ON government_laws
141
+ FOR EACH ROW EXECUTE FUNCTION government_laws_guard();
142
+
143
+ COMMIT;
@@ -0,0 +1,57 @@
1
+ -- government_028_asks.sql — THE ASK: one stored shape for every request an AI
2
+ -- agent puts to a person (task 1004703, goal 1000111).
3
+ --
4
+ -- WHAT THIS ADDS
5
+ --
6
+ -- government_asks — ONE ROW PER DOCKET MATTER that carries an ask, keyed by
7
+ -- the matter's docket id (`blockers:12`, `board:34`; docket.js normalizeMatter).
8
+ -- An ask is a question of at most twelve words, two or three tap answers each
9
+ -- with a one-line result, a pick and why, the seat it waits on, and what
10
+ -- happens if nobody answers. The long text an agent first wrote may ride along
11
+ -- as `detail`, never as the ask itself.
12
+ --
13
+ -- The full rule set is checked in modules/government/asks.js before a write
14
+ -- (it names every fix). The CHECKs below are the structural floor under it, so
15
+ -- a hand-written INSERT cannot store slop either: a question over twelve
16
+ -- words, one that does not end in '?', a second line, fewer than two or more
17
+ -- than three answers, a pick that is not one of the answers, a seat outside
18
+ -- the routable grammar, or a missing "if nobody answers".
19
+ --
20
+ -- Additive and idempotent: a new table (IF NOT EXISTS) and its constraints
21
+ -- declared with it. No row is seeded.
22
+
23
+ BEGIN;
24
+
25
+ CREATE TABLE IF NOT EXISTS government_asks (
26
+ -- the docket matter this ask is for: '<source>:<id>'
27
+ matter_id text PRIMARY KEY
28
+ CHECK (matter_id ~ '^[a-z][a-z_]{1,39}:[A-Za-z0-9._-]{1,64}$'),
29
+ -- one question, at most twelve words, ending in a question mark
30
+ question text NOT NULL
31
+ CHECK (char_length(question) BETWEEN 2 AND 160
32
+ AND question !~ '[\r\n]'
33
+ AND question ~ '\?$'
34
+ AND array_length(regexp_split_to_array(btrim(question), '\s+'), 1) <= 12),
35
+ -- two or three tap answers: [{ key, label, result, act? }]
36
+ options jsonb NOT NULL
37
+ CHECK (jsonb_typeof(options) = 'array' AND jsonb_array_length(options) BETWEEN 2 AND 3),
38
+ -- the pick (one of the answers' keys) and why, in one line
39
+ recommend_key text NOT NULL CHECK (recommend_key ~ '^[a-z][a-z0-9_-]{0,23}$'),
40
+ recommend_why text NOT NULL
41
+ CHECK (char_length(btrim(recommend_why)) BETWEEN 1 AND 200 AND recommend_why !~ '[\r\n]'),
42
+ -- the seat it waits on: owner | lead:<building> | committee:<building>.<name> | board:<building>
43
+ waits_on text NOT NULL
44
+ CHECK (waits_on ~ '^(owner|(lead|board):(charter|rules|disputes|people|planning|automation|review|release|treasury)|committee:(charter|rules|disputes|people|planning|automation|review|release|treasury)\.[a-z][a-z0-9_]{0,31})$'),
45
+ -- what happens if nobody answers, in one line
46
+ if_nobody_answers text NOT NULL
47
+ CHECK (char_length(btrim(if_nobody_answers)) BETWEEN 1 AND 160 AND if_nobody_answers !~ '[\r\n]'),
48
+ -- the long text, riding along (never the ask)
49
+ detail text CHECK (detail IS NULL OR char_length(detail) <= 4000),
50
+ written_by bigint REFERENCES builders(id) ON DELETE SET NULL,
51
+ written_at timestamptz NOT NULL DEFAULT now(),
52
+
53
+ -- the pick is one of the answers offered
54
+ CONSTRAINT government_asks_pick_offered CHECK (options @> jsonb_build_array(jsonb_build_object('key', recommend_key)))
55
+ );
56
+
57
+ COMMIT;
@@ -0,0 +1,135 @@
1
+ -- government_029_leads.sql — the Lead of each building, a held position the
2
+ -- government appoints and removes by vote (task 1004709, goal 1000111).
3
+ --
4
+ -- WHAT THIS ADDS
5
+ --
6
+ -- government_leads — ONE ROW PER APPOINTMENT, ever. A building of the nine
7
+ -- has at most one Lead in office (the partial unique index); a Lead carries a
8
+ -- short charter (one line: what the Lead answers for). Each row records WHO
9
+ -- appointed them and WHEN: the passed People matter's board item
10
+ -- (appointed_by_item_id — its snapshotted constitution is the government that
11
+ -- decided it, its yes votes who said yes) and appointed_at. Leaving office
12
+ -- records the item that ended it (removed_by_item_id — a passed People matter
13
+ -- that removed this Lead, or appointed the next one) and removed_at; the row
14
+ -- stays on the record.
15
+ --
16
+ -- A GUARD TRIGGER: a Lead is written ONLY by a passed People matter that says so.
17
+ -- INSERT — appointed_by_item_id must be a CLOSED, PASSED people_matter item
18
+ -- whose matter's action is appoint_lead for exactly this building,
19
+ -- builder and charter; the row is born in office, appointed_at is
20
+ -- the database clock.
21
+ -- UPDATE — only leaving office: a Lead in office gains removed_by_item_id (a
22
+ -- passed people_matter whose action is remove_lead, or appoint_lead,
23
+ -- for the same building) and removed_at; nothing else may change,
24
+ -- and a row that left office never changes again.
25
+ -- DELETE — refused outright. A Lead leaves office by a vote, on the record.
26
+ -- There is no route that writes this table, and the trigger makes that
27
+ -- structural: a hand-written INSERT or UPDATE is refused too.
28
+ --
29
+ -- No Lead is seated here: every building starts unseated, so it falls to the
30
+ -- owner until the government appoints one. Additive and idempotent: a new
31
+ -- table, CREATE OR REPLACE for the functions and the trigger (Postgres 14+;
32
+ -- production runs 16).
33
+
34
+ BEGIN;
35
+
36
+ CREATE TABLE IF NOT EXISTS government_leads (
37
+ id bigserial PRIMARY KEY,
38
+ -- the building this Lead answers for (the nine, decision-rules.js AREAS)
39
+ building text NOT NULL
40
+ CHECK (building IN ('charter', 'rules', 'disputes', 'people', 'planning',
41
+ 'automation', 'review', 'release', 'treasury')),
42
+ builder_id bigint NOT NULL REFERENCES builders(id) ON DELETE RESTRICT,
43
+ -- the Lead's short charter: one line, what they answer for
44
+ charter text NOT NULL
45
+ CHECK (char_length(btrim(charter)) BETWEEN 3 AND 140 AND charter !~ '[\r\n]'),
46
+ -- WHO appointed them: the passed People matter's board item
47
+ appointed_by_item_id bigint NOT NULL REFERENCES government_board_items(id) ON DELETE RESTRICT,
48
+ -- WHEN: the database clock when the appointment was carried out (the trigger)
49
+ appointed_at timestamptz NOT NULL DEFAULT now(),
50
+ removed_by_item_id bigint REFERENCES government_board_items(id) ON DELETE RESTRICT,
51
+ removed_at timestamptz,
52
+
53
+ CONSTRAINT government_leads_removal_shape CHECK ((removed_by_item_id IS NULL) = (removed_at IS NULL))
54
+ );
55
+
56
+ -- One Lead in office per building.
57
+ CREATE UNIQUE INDEX IF NOT EXISTS government_leads_in_office_uniq
58
+ ON government_leads (building) WHERE removed_at IS NULL;
59
+
60
+ -- An appointment is carried out once: one row per passed appointing item.
61
+ CREATE UNIQUE INDEX IF NOT EXISTS government_leads_appointed_by_uniq
62
+ ON government_leads (appointed_by_item_id);
63
+
64
+ -- The action of the passed People matter a board item decided, or NULL when the
65
+ -- item is not a closed, passed people_matter.
66
+ CREATE OR REPLACE FUNCTION government_leads_passed_people_action(p_item_id bigint) RETURNS jsonb
67
+ LANGUAGE sql STABLE AS $fn$
68
+ SELECT m.action
69
+ FROM government_board_items i
70
+ JOIN government_matters m ON m.id = i.subject_id
71
+ WHERE i.id = $1
72
+ AND i.subject_type = 'people_matter'
73
+ AND i.outcome = 'passed'
74
+ AND m.area = 'people'
75
+ $fn$;
76
+
77
+ CREATE OR REPLACE FUNCTION government_leads_guard() RETURNS trigger
78
+ LANGUAGE plpgsql AS $guard$
79
+ DECLARE
80
+ act jsonb;
81
+ BEGIN
82
+ IF TG_OP = 'DELETE' THEN
83
+ RAISE EXCEPTION 'government_leads: a Lead cannot be deleted; they leave office only by a passed People matter'
84
+ USING ERRCODE = 'check_violation';
85
+ END IF;
86
+
87
+ IF TG_OP = 'INSERT' THEN
88
+ act := government_leads_passed_people_action(NEW.appointed_by_item_id);
89
+ IF act IS NULL
90
+ OR act->>'type' IS DISTINCT FROM 'appoint_lead'
91
+ OR act->>'building' IS DISTINCT FROM NEW.building
92
+ OR act->>'builder_id' IS DISTINCT FROM NEW.builder_id::text
93
+ OR act->>'charter' IS DISTINCT FROM NEW.charter THEN
94
+ RAISE EXCEPTION 'government_leads changes only through a passed People matter that appoints this Lead (board item %)', NEW.appointed_by_item_id
95
+ USING ERRCODE = 'check_violation';
96
+ END IF;
97
+ IF NEW.removed_by_item_id IS NOT NULL OR NEW.removed_at IS NOT NULL THEN
98
+ RAISE EXCEPTION 'government_leads: a Lead is born in office'
99
+ USING ERRCODE = 'check_violation';
100
+ END IF;
101
+ NEW.appointed_at := now();
102
+ RETURN NEW;
103
+ END IF;
104
+
105
+ -- UPDATE: only leaving office, by a passed People matter for this building.
106
+ IF OLD.removed_at IS NOT NULL THEN
107
+ RAISE EXCEPTION 'government_leads: appointment % has ended and is closed to change', OLD.id
108
+ USING ERRCODE = 'check_violation';
109
+ END IF;
110
+ IF NEW.id IS DISTINCT FROM OLD.id
111
+ OR NEW.building IS DISTINCT FROM OLD.building
112
+ OR NEW.builder_id IS DISTINCT FROM OLD.builder_id
113
+ OR NEW.charter IS DISTINCT FROM OLD.charter
114
+ OR NEW.appointed_by_item_id IS DISTINCT FROM OLD.appointed_by_item_id
115
+ OR NEW.appointed_at IS DISTINCT FROM OLD.appointed_at THEN
116
+ RAISE EXCEPTION 'government_leads: a Lead in office cannot be edited; remove them and appoint again'
117
+ USING ERRCODE = 'check_violation';
118
+ END IF;
119
+ act := government_leads_passed_people_action(NEW.removed_by_item_id);
120
+ IF act IS NULL
121
+ OR act->>'type' NOT IN ('remove_lead', 'appoint_lead')
122
+ OR act->>'building' IS DISTINCT FROM OLD.building THEN
123
+ RAISE EXCEPTION 'government_leads changes only through a passed People matter for this building (board item %)', NEW.removed_by_item_id
124
+ USING ERRCODE = 'check_violation';
125
+ END IF;
126
+ NEW.removed_at := now();
127
+ RETURN NEW;
128
+ END
129
+ $guard$;
130
+
131
+ CREATE OR REPLACE TRIGGER government_leads_matter_only
132
+ BEFORE INSERT OR UPDATE OR DELETE ON government_leads
133
+ FOR EACH ROW EXECUTE FUNCTION government_leads_guard();
134
+
135
+ COMMIT;
@@ -0,0 +1,111 @@
1
+ -- government_030_buildouts.sql — a Lead's build-outs: the structure a building
2
+ -- grows under its holder, as data (task 1004711, goal 1000111; ADR 0378).
3
+ --
4
+ -- WHAT THIS ADDS
5
+ --
6
+ -- government_buildouts — ONE ROW PER BUILD-OUT, ever: a committee room, a new
7
+ -- floor, an annex, or growing the building into a district, added to one of
8
+ -- the nine buildings, with a short name and one line saying what it is for.
9
+ -- A row is never edited and never deleted: each is one change to the city, on
10
+ -- the record. It records WHO asked (built_by, the building's holder) and HOW it
11
+ -- was allowed — exactly one of:
12
+ -- * authorised_by_item_id — a closed, PASSED matter whose action is
13
+ -- build_out for exactly this building, capsule, name and line (the vote
14
+ -- path); or
15
+ -- * ground + why — the build-rights rule that let its holder build at once
16
+ -- (modules/government/build-rights.js: 'form', 'goal' or 'building'). The
17
+ -- server asks that rule before writing; this table cannot run it, so it
18
+ -- holds the shape: a district is never built at once (its ground is
19
+ -- 'city', always a vote).
20
+ -- started_at is the database clock; the hall draws a build-out as a live site
21
+ -- for a day from then (buildouts.js BUILD_HOURS), then as built.
22
+ --
23
+ -- A GUARD TRIGGER refuses an INSERT whose vote a passed matter did not say,
24
+ -- any at-once row without its ground, every UPDATE and every DELETE.
25
+ --
26
+ -- Nothing is seeded. Additive and idempotent: a new table, CREATE OR REPLACE for
27
+ -- the functions and the trigger (Postgres 14+; production runs 16).
28
+
29
+ BEGIN;
30
+
31
+ CREATE TABLE IF NOT EXISTS government_buildouts (
32
+ id bigserial PRIMARY KEY,
33
+ -- the building it is added to (the nine, decision-rules.js AREAS)
34
+ building text NOT NULL
35
+ CHECK (building IN ('charter', 'rules', 'disputes', 'people', 'planning',
36
+ 'automation', 'review', 'release', 'treasury')),
37
+ -- which build-out (build-rights.js CAPSULES)
38
+ capsule text NOT NULL
39
+ CHECK (capsule IN ('committee', 'floor', 'annex', 'district')),
40
+ name text NOT NULL
41
+ CHECK (char_length(btrim(name)) BETWEEN 2 AND 40 AND name !~ '[\r\n]'),
42
+ line text NOT NULL DEFAULT ''
43
+ CHECK (char_length(line) <= 140 AND line !~ '[\r\n]'),
44
+ -- WHO asked: the building's holder
45
+ built_by bigint REFERENCES builders(id) ON DELETE SET NULL,
46
+ -- HOW it was allowed: a passed vote ...
47
+ authorised_by_item_id bigint REFERENCES government_board_items(id) ON DELETE RESTRICT,
48
+ -- ... or the rule that let its holder build at once
49
+ ground text CHECK (ground IS NULL OR ground IN ('form', 'goal', 'building')),
50
+ why text CHECK (why IS NULL OR char_length(why) <= 300),
51
+ started_at timestamptz NOT NULL DEFAULT now(),
52
+
53
+ CONSTRAINT government_buildouts_one_path CHECK ((authorised_by_item_id IS NULL) <> (ground IS NULL)),
54
+ CONSTRAINT government_buildouts_district_votes CHECK (capsule <> 'district' OR authorised_by_item_id IS NOT NULL)
55
+ );
56
+
57
+ -- A passed vote builds once.
58
+ CREATE UNIQUE INDEX IF NOT EXISTS government_buildouts_item_uniq
59
+ ON government_buildouts (authorised_by_item_id) WHERE authorised_by_item_id IS NOT NULL;
60
+
61
+ CREATE INDEX IF NOT EXISTS government_buildouts_building_idx
62
+ ON government_buildouts (building, started_at);
63
+
64
+ -- The action of the passed matter a board item decided, or NULL when the item
65
+ -- is not a closed, passed matter.
66
+ CREATE OR REPLACE FUNCTION government_buildouts_passed_action(p_item_id bigint) RETURNS jsonb
67
+ LANGUAGE sql STABLE AS $fn$
68
+ SELECT m.action
69
+ FROM government_board_items i
70
+ JOIN government_matters m ON m.id = i.subject_id
71
+ WHERE i.id = $1
72
+ AND i.subject_type = m.area || '_matter'
73
+ AND i.outcome = 'passed'
74
+ $fn$;
75
+
76
+ CREATE OR REPLACE FUNCTION government_buildouts_guard() RETURNS trigger
77
+ LANGUAGE plpgsql AS $guard$
78
+ DECLARE
79
+ act jsonb;
80
+ BEGIN
81
+ IF TG_OP <> 'INSERT' THEN
82
+ RAISE EXCEPTION 'government_buildouts: a build-out is on the record; it is never edited or deleted'
83
+ USING ERRCODE = 'check_violation';
84
+ END IF;
85
+
86
+ IF NEW.authorised_by_item_id IS NOT NULL THEN
87
+ act := government_buildouts_passed_action(NEW.authorised_by_item_id);
88
+ IF act IS NULL
89
+ OR act->>'type' IS DISTINCT FROM 'build_out'
90
+ OR act->>'building' IS DISTINCT FROM NEW.building
91
+ OR act->>'capsule' IS DISTINCT FROM NEW.capsule
92
+ OR act->>'name' IS DISTINCT FROM NEW.name
93
+ OR act->>'line' IS DISTINCT FROM NEW.line THEN
94
+ RAISE EXCEPTION 'government_buildouts: board item % is not a passed matter that builds this', NEW.authorised_by_item_id
95
+ USING ERRCODE = 'check_violation';
96
+ END IF;
97
+ ELSIF NEW.ground IS NULL OR NEW.why IS NULL OR NEW.capsule = 'district' THEN
98
+ RAISE EXCEPTION 'government_buildouts: built at once only on a build-rights ground, and never a district'
99
+ USING ERRCODE = 'check_violation';
100
+ END IF;
101
+
102
+ NEW.started_at := now();
103
+ RETURN NEW;
104
+ END
105
+ $guard$;
106
+
107
+ CREATE OR REPLACE TRIGGER government_buildouts_on_the_record
108
+ BEFORE INSERT OR UPDATE OR DELETE ON government_buildouts
109
+ FOR EACH ROW EXECUTE FUNCTION government_buildouts_guard();
110
+
111
+ COMMIT;
@@ -0,0 +1,107 @@
1
+ -- government_031_treasury.sql — THE TREASURY: a pot per goal, the reserve, and
2
+ -- the borrow a dry pot asks for (task 1004712, goal 1000111; ADR 0379).
3
+ --
4
+ -- The project's AI spend is its treasury. Owner rulings (2026-10-09): there is
5
+ -- NO spend limit by default (a cap is an optional project setting, off by
6
+ -- default: project_settings['treasury.spend_cap_usd'], no row = off); a goal
7
+ -- may have a pot; when a pot runs dry its agent asks to borrow from the
8
+ -- reserve, and that borrow is a treasury matter the government decides.
9
+ --
10
+ -- This is the project's own accounting over its REAL spend (cost_log). Nothing
11
+ -- here moves money or talks to a payment provider: a pot's spend is read from
12
+ -- cost_log through each task's goal, every time; only the budget a goal is
13
+ -- given and the loans the treasury made are stored.
14
+ --
15
+ -- WHAT THIS ADDS
16
+ --
17
+ -- government_pots — ONE ROW PER GOAL THAT HAS A POT: the monthly budget the
18
+ -- Treasury's holder gave it. A goal without a row has no pot. The budget is
19
+ -- a setting, so it may be changed or taken away (the row deleted).
20
+ --
21
+ -- government_pot_borrows — ONE ROW PER BORROW ASK, ever: a dry pot asking
22
+ -- the reserve for money in one calendar month ('YYYY-MM'), how much, what the
23
+ -- pot stood at when it asked, and how the treasury answered. It is filed
24
+ -- 'open' (its ask is stored in government_asks under the docket id
25
+ -- 'treasury_spending:<id>' and routed to the Treasury's holder) and answered
26
+ -- once: 'lent' (lent_usd is what was lent: all or half) or 'refused'. A GUARD
27
+ -- TRIGGER refuses an answer on a row already answered, any change to what
28
+ -- was asked, and every DELETE: a loan is on the record.
29
+ --
30
+ -- Nothing is seeded. Additive and idempotent: new tables, CREATE OR REPLACE for
31
+ -- the function and the trigger (Postgres 14+; production runs 16).
32
+
33
+ BEGIN;
34
+
35
+ CREATE TABLE IF NOT EXISTS government_pots (
36
+ goal_id bigint PRIMARY KEY REFERENCES goals(id) ON DELETE CASCADE,
37
+ -- dollars a month; no default and no ceiling beyond sanity
38
+ budget_usd numeric(12,2) NOT NULL CHECK (budget_usd > 0 AND budget_usd <= 1000000),
39
+ set_by bigint REFERENCES builders(id) ON DELETE SET NULL,
40
+ set_at timestamptz NOT NULL DEFAULT now()
41
+ );
42
+
43
+ CREATE TABLE IF NOT EXISTS government_pot_borrows (
44
+ id bigserial PRIMARY KEY,
45
+ goal_id bigint NOT NULL REFERENCES goals(id) ON DELETE RESTRICT,
46
+ month text NOT NULL CHECK (month ~ '^[0-9]{4}-(0[1-9]|1[0-2])$'),
47
+ -- what it asks for, and what the pot stood at when it asked
48
+ amount_usd numeric(12,2) NOT NULL CHECK (amount_usd > 0 AND amount_usd <= 1000000),
49
+ budget_usd numeric(12,2) NOT NULL CHECK (budget_usd >= 0),
50
+ spent_usd numeric(12,2) NOT NULL CHECK (spent_usd >= 0),
51
+ filed_at timestamptz NOT NULL DEFAULT now(),
52
+ -- how the treasury answered
53
+ state text NOT NULL DEFAULT 'open' CHECK (state IN ('open', 'lent', 'refused')),
54
+ lent_usd numeric(12,2) CHECK (lent_usd IS NULL OR (lent_usd > 0 AND lent_usd <= amount_usd)),
55
+ answer_key text CHECK (answer_key IS NULL OR answer_key IN ('lend', 'half', 'no')),
56
+ answered_by bigint REFERENCES builders(id) ON DELETE SET NULL,
57
+ answered_at timestamptz,
58
+
59
+ CONSTRAINT government_pot_borrows_answered CHECK (
60
+ (state = 'open' AND lent_usd IS NULL AND answer_key IS NULL AND answered_at IS NULL) OR
61
+ (state = 'lent' AND lent_usd IS NOT NULL AND answer_key IN ('lend', 'half') AND answered_at IS NOT NULL) OR
62
+ (state = 'refused' AND lent_usd IS NULL AND answer_key = 'no' AND answered_at IS NOT NULL))
63
+ );
64
+
65
+ -- A pot asks once at a time: one open borrow per goal.
66
+ CREATE UNIQUE INDEX IF NOT EXISTS government_pot_borrows_one_open
67
+ ON government_pot_borrows (goal_id) WHERE state = 'open';
68
+
69
+ CREATE INDEX IF NOT EXISTS government_pot_borrows_month_idx
70
+ ON government_pot_borrows (month, goal_id);
71
+
72
+ CREATE OR REPLACE FUNCTION government_pot_borrows_guard() RETURNS trigger
73
+ LANGUAGE plpgsql AS $guard$
74
+ BEGIN
75
+ IF TG_OP = 'DELETE' THEN
76
+ RAISE EXCEPTION 'government_pot_borrows: a borrow is on the record; it is never deleted'
77
+ USING ERRCODE = 'check_violation';
78
+ END IF;
79
+ IF TG_OP = 'INSERT' THEN
80
+ IF NEW.state <> 'open' THEN
81
+ RAISE EXCEPTION 'government_pot_borrows: a borrow is filed open and answered after'
82
+ USING ERRCODE = 'check_violation';
83
+ END IF;
84
+ NEW.filed_at := now();
85
+ RETURN NEW;
86
+ END IF;
87
+ -- UPDATE: only the one answer, on an open row; what was asked never changes
88
+ IF OLD.state <> 'open' THEN
89
+ RAISE EXCEPTION 'government_pot_borrows: borrow % was already answered', OLD.id
90
+ USING ERRCODE = 'check_violation';
91
+ END IF;
92
+ IF NEW.goal_id IS DISTINCT FROM OLD.goal_id OR NEW.month IS DISTINCT FROM OLD.month
93
+ OR NEW.amount_usd IS DISTINCT FROM OLD.amount_usd OR NEW.budget_usd IS DISTINCT FROM OLD.budget_usd
94
+ OR NEW.spent_usd IS DISTINCT FROM OLD.spent_usd OR NEW.filed_at IS DISTINCT FROM OLD.filed_at THEN
95
+ RAISE EXCEPTION 'government_pot_borrows: what a borrow asked for is never edited'
96
+ USING ERRCODE = 'check_violation';
97
+ END IF;
98
+ NEW.answered_at := now();
99
+ RETURN NEW;
100
+ END
101
+ $guard$;
102
+
103
+ CREATE OR REPLACE TRIGGER government_pot_borrows_on_the_record
104
+ BEFORE INSERT OR UPDATE OR DELETE ON government_pot_borrows
105
+ FOR EACH ROW EXECUTE FUNCTION government_pot_borrows_guard();
106
+
107
+ COMMIT;
@@ -15,7 +15,8 @@
15
15
  "rank-sync"
16
16
  ],
17
17
  "pollers": [
18
- "board-expiry"
18
+ "board-expiry",
19
+ "treasury-borrow"
19
20
  ]
20
21
  },
21
22
  "provides": [
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ // modules/government/pollers/treasury-borrow.js — A DRY POT ASKS TO BORROW (task
4
+ // 1004712, goal 1000111; ADR 0379).
5
+ //
6
+ // The owner's ruling: when a goal's pot runs dry, it asks to borrow from the
7
+ // reserve, and that borrow is a treasury matter the government decides. This is
8
+ // the treasury's own agent: every sweep it reads the treasury (real spend, the
9
+ // stored pots and loans) and files a borrow for every dry pot that has nothing
10
+ // open and was not told no this month (treasury-db.fileBorrows). The ask lands on
11
+ // the docket as treasury_spending:<id>, waiting on the Treasury's holder.
12
+ //
13
+ // It never stops work and never moves money: there is no spend limit by default,
14
+ // and a borrow is accounting. A project with no pots files nothing.
15
+ //
16
+ // THE SWEEP SHAPE is board-expiry's: a first sweep shortly after boot, then on an
17
+ // interval, quiet when there is nothing to file, one line per borrow filed, and a
18
+ // read that fails is logged and retried next sweep.
19
+
20
+ const api = require('../../../src/module-api');
21
+ const treasuryDb = require('../treasury-db');
22
+
23
+ const logger = api.logger('government');
24
+
25
+ const INTERVAL_MS = Number(process.env.TREASURY_BORROW_SWEEP_MS) || 15 * 60_000;
26
+ const FIRST_SWEEP_DELAY_MS = Number(process.env.TREASURY_BORROW_FIRST_MS) || 60_000;
27
+
28
+ let intervalTimer = null;
29
+ let firstTimer = null;
30
+
31
+ async function runSweep({ fileFn = treasuryDb.fileBorrows, now = Date.now, log = (m) => logger.info(m), error = (m) => logger.error(m) } = {}) {
32
+ let filed;
33
+ try {
34
+ filed = await fileFn({ now: now() });
35
+ } catch (e) {
36
+ error(`[treasury-borrow] sweep failed (next sweep retries): ${e && e.message}`);
37
+ return { filed: 0, failed: true };
38
+ }
39
+ for (const f of filed || []) log(`[treasury-borrow] goal ${f.goal_id}'s pot is dry → borrow ${f.borrow_id} asks the reserve for $${f.amount_usd}`);
40
+ return { filed: (filed || []).length, failed: false };
41
+ }
42
+
43
+ function start(deps = {}) {
44
+ if (process.env.TREASURY_BORROW_DISABLED === '1') {
45
+ logger.info('[treasury-borrow] disabled (TREASURY_BORROW_DISABLED=1)');
46
+ return;
47
+ }
48
+ if (intervalTimer) return;
49
+ const tick = () => { runSweep(deps).catch((e) => logger.error(`[treasury-borrow] sweep crashed: ${e && e.message}`)); };
50
+ firstTimer = setTimeout(tick, FIRST_SWEEP_DELAY_MS);
51
+ if (firstTimer.unref) firstTimer.unref();
52
+ intervalTimer = setInterval(tick, INTERVAL_MS);
53
+ if (intervalTimer.unref) intervalTimer.unref();
54
+ logger.info(`[treasury-borrow] started (every ${Math.round(INTERVAL_MS / 60000)}m; first sweep in ${Math.round(FIRST_SWEEP_DELAY_MS / 1000)}s)`);
55
+ }
56
+
57
+ function stop() {
58
+ if (firstTimer) { clearTimeout(firstTimer); firstTimer = null; }
59
+ if (intervalTimer) { clearInterval(intervalTimer); intervalTimer = null; }
60
+ }
61
+
62
+ module.exports = { start, stop, runSweep, INTERVAL_MS };