@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.
- package/.bongos-core.json +603 -183
- package/.claude/skills/ask-for-help/SKILL.md +1 -1
- package/.claude/skills/bongos-feedback/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -0
- package/GOVERNMENT.md +128 -11
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +28 -0
- package/clients/bongos-client/index.cjs +28 -0
- package/clients/bongos-client/index.d.ts +45 -2
- package/clients/bongos-client/index.mjs +28 -0
- package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
- package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
- package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
- package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
- package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
- package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
- package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
- package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
- package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
- package/docs/adr/README.md +10 -1
- package/docs/api/openapi.json +989 -87
- package/docs/api-reference.md +18 -4
- package/docs/copy-inventory.md +343 -262
- package/docs/copy-registry.json +1223 -426
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/packs/engineer.md +2 -1
- package/docs/page-inventory.json +91 -6
- package/docs/page-readings.json +5017 -4458
- package/modules/builder-settings/law-locks.js +202 -0
- package/modules/builder-settings/routes/settings.js +66 -6
- package/modules/builder-settings/settings.js +7 -0
- package/modules/discord/ship-broadcast.js +22 -13
- package/modules/government/asks-db.js +54 -0
- package/modules/government/asks.js +269 -0
- package/modules/government/board-idea-reads.js +68 -0
- package/modules/government/board.js +21 -57
- package/modules/government/build-rights.js +148 -0
- package/modules/government/buildouts.js +160 -0
- package/modules/government/counts.js +81 -0
- package/modules/government/db.js +242 -0
- package/modules/government/decision-board.js +234 -8
- package/modules/government/decision-rules.js +2 -0
- package/modules/government/docket.js +114 -6
- package/modules/government/law-board.js +131 -0
- package/modules/government/laws.js +241 -0
- package/modules/government/leads.js +170 -0
- package/modules/government/migrations/government_027_laws.sql +143 -0
- package/modules/government/migrations/government_028_asks.sql +57 -0
- package/modules/government/migrations/government_029_leads.sql +135 -0
- package/modules/government/migrations/government_030_buildouts.sql +111 -0
- package/modules/government/migrations/government_031_treasury.sql +107 -0
- package/modules/government/module.json +2 -1
- package/modules/government/pollers/treasury-borrow.js +62 -0
- package/modules/government/positions.js +107 -0
- package/modules/government/presets.js +149 -0
- package/modules/government/routes/government.js +425 -2
- package/modules/government/routing.js +154 -0
- package/modules/government/treasury-db.js +166 -0
- package/modules/government/treasury.js +241 -0
- package/modules/hall-ui/public/atlas.html +2 -2
- package/modules/hall-ui/public/blockers.html +2 -2
- package/modules/hall-ui/public/board-room.html +2 -2
- package/modules/hall-ui/public/board-room.js +2 -0
- package/modules/hall-ui/public/city-draw.js +604 -11
- package/modules/hall-ui/public/city.css +157 -1
- package/modules/hall-ui/public/city.html +37 -7
- package/modules/hall-ui/public/city.js +441 -21
- package/modules/hall-ui/public/city.states.json +110 -5
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/commands.html +2 -2
- package/modules/hall-ui/public/copy-desk.html +2 -2
- package/modules/hall-ui/public/craft-mode.js +112 -29
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/diagram-viewer.css +12 -0
- package/modules/hall-ui/public/diagram-viewer.js +4 -0
- package/modules/hall-ui/public/diagrams.html +2 -2
- package/modules/hall-ui/public/drachmae.html +2 -2
- package/modules/hall-ui/public/fleet.html +2 -2
- package/modules/hall-ui/public/gate.html +2 -2
- package/modules/hall-ui/public/goal-map.html +2 -2
- package/modules/hall-ui/public/goals.html +2 -2
- package/modules/hall-ui/public/gov-feeds.js +141 -0
- package/modules/hall-ui/public/gov-living.js +1002 -0
- package/modules/hall-ui/public/gov-positions.js +97 -0
- package/modules/hall-ui/public/government.html +2 -2
- package/modules/hall-ui/public/hall-render.js +3 -0
- package/modules/hall-ui/public/idea.html +2 -2
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +3 -3
- package/modules/hall-ui/public/modules.html +2 -2
- package/modules/hall-ui/public/my-agents.css +334 -0
- package/modules/hall-ui/public/my-agents.html +72 -0
- package/modules/hall-ui/public/my-agents.js +603 -0
- package/modules/hall-ui/public/my-agents.states.json +37 -0
- package/modules/hall-ui/public/primer.html +2 -2
- package/modules/hall-ui/public/profile-connections.js +40 -28
- package/modules/hall-ui/public/profile.html +2 -2
- package/modules/hall-ui/public/project-settings.html +2 -2
- package/modules/hall-ui/public/ranks.html +2 -2
- package/modules/hall-ui/public/roadmap.html +2 -2
- package/modules/hall-ui/public/room.css +8 -0
- package/modules/hall-ui/public/roster.html +2 -2
- package/modules/hall-ui/public/sessions.html +2 -2
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +116 -16
- package/modules/hall-ui/public/street-card.js +18 -1
- package/modules/hall-ui/public/street-deck.css +299 -0
- package/modules/hall-ui/public/street-deck.js +1145 -0
- package/modules/hall-ui/public/street-depth.js +712 -0
- package/modules/hall-ui/public/street-desk.js +5 -2
- package/modules/hall-ui/public/street-draw.js +109 -16
- package/modules/hall-ui/public/street-inside.js +137 -22
- package/modules/hall-ui/public/street-office.js +705 -0
- package/modules/hall-ui/public/street.css +161 -11
- package/modules/hall-ui/public/street.html +41 -11
- package/modules/hall-ui/public/street.js +78 -66
- package/modules/hall-ui/public/street.states.json +129 -21
- package/modules/hall-ui/public/studio.html +2 -2
- package/modules/hall-ui/public/style.css +5 -1
- package/modules/hall-ui/public/task.html +2 -2
- package/modules/hall-ui/public/thinking.html +2 -2
- package/modules/hall-ui/public/tweak-editor.html +2 -2
- package/modules/hall-ui/public/watch.html +2 -2
- package/modules/hall-ui/public/watch.js +14 -1
- package/modules/hall-ui/public/work.html +2 -2
- package/modules/hall-ui/records/living-city.md +57 -0
- package/modules/hall-ui/records/my-agents.md +21 -0
- package/modules/hall-ui/records/oversight-family.md +1 -0
- package/modules/hall-ui/records/profile-connections.md +10 -0
- package/modules/hall-ui/records/reading-rooms.md +2 -0
- package/modules/ideas/routes/blockers.js +92 -3
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/moving.js +17 -1
- package/modules/lifecycle/routes/moving.js +6 -0
- package/modules/lifecycle/routes/project-settings.js +23 -0
- package/modules/onboarding/approval-broadcast.js +8 -0
- package/modules/onboarding/routes/access-requests.js +16 -6
- package/modules/platform-identity/connections.js +13 -2
- package/modules/platform-identity/routes/connections.js +7 -1
- package/modules/public-landing/public/account-private.states.json +2 -2
- package/modules/public-landing/public/account.html +65 -5
- package/modules/public-landing/public/account.states.json +3 -3
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
- package/modules/public-landing/public/assets/world.css +4 -4
- package/modules/public-landing/public/connections-empty.states.json +14 -0
- package/modules/public-landing/public/connections-inactive.states.json +14 -0
- package/modules/public-landing/public/connections.html +450 -0
- package/modules/public-landing/public/connections.probes.json +414 -0
- package/modules/public-landing/public/connections.states.json +17 -0
- package/modules/public-landing/public/contact.html +2 -2
- package/modules/public-landing/public/guild.html +2 -2
- package/modules/public-landing/public/index.html +2 -2
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/privacy.html +2 -2
- package/modules/public-landing/public/projects.html +2 -2
- package/modules/public-landing/public/projects.probes.json +6 -2
- package/modules/public-landing/public/terms.html +2 -2
- package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
- package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
- package/modules/ui-design/kit/fixtures/connections.json +22 -0
- package/modules/ui-design/kit/serve.js +95 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +122 -0
- package/scripts/discord/README.md +8 -5
- package/scripts/discord/post.js +12 -1
- package/scripts/gds/artifact-audit.js +20 -3
- package/scripts/gds/ask.js +164 -0
- package/scripts/gds/autobongos-verify.js +21 -2
- package/scripts/gds/government-walk.js +638 -4
- package/scripts/gds/pulse.js +9 -6
- package/scripts/gds/ship-escalation.js +22 -3
- package/scripts/gds/ship-finish.js +5 -4
- package/scripts/gds/ship-merge-gate.js +18 -6
- package/scripts/hall-preview/README.md +41 -1
- package/scripts/hall-preview/pref-writes.js +140 -0
- package/scripts/hall-preview/refresh-fixtures.js +101 -7
- package/scripts/hall-preview/scrub-identity.js +366 -7
- package/scripts/hall-preview/server.js +27 -1
- package/scripts/render-diagrams.sh +4 -2
- package/src/bongos/auth-idp.js +6 -1
- package/src/bongos/project-settings.js +41 -0
- package/src/bongos/route-rank-check.js +27 -0
- package/src/bongos/routes/me.js +68 -7
- package/src/bongos/serve-internal.js +5 -1
- package/src/module-api.js +7 -1
- package/src/platform-server.js +10 -0
- package/tests/application_lifecycle.mjs +47 -5
- package/tests/conductor_main_e2e.mjs +21 -0
- package/tests/connections_api.mjs +18 -0
- package/tests/craft_mode_nav.mjs +5 -3
- package/tests/craft_mode_tallies.mjs +52 -16
- package/tests/discord_approvals.mjs +11 -0
- package/tests/discord_channels.mjs +20 -0
- package/tests/fixtures/gov-seated.mjs +64 -0
- package/tests/fixtures/government-ask-cards.json +1284 -0
- package/tests/government_act_alone_buildings.mjs +1 -0
- package/tests/government_agent_asks.mjs +322 -0
- package/tests/government_asks.mjs +408 -0
- package/tests/government_build_rights.mjs +253 -0
- package/tests/government_buildouts.mjs +382 -0
- package/tests/government_counts.mjs +143 -0
- package/tests/government_decision_rules.mjs +5 -1
- package/tests/government_laws.mjs +488 -0
- package/tests/government_laws_session.mjs +321 -0
- package/tests/government_leads.mjs +375 -0
- package/tests/government_positions.mjs +127 -0
- package/tests/government_presets.mjs +215 -0
- package/tests/government_routes.mjs +8 -1
- package/tests/government_routing.mjs +118 -0
- package/tests/government_seed.mjs +5 -0
- package/tests/government_treasury.mjs +394 -0
- package/tests/governor_docket.mjs +26 -2
- package/tests/hall_buildouts.mjs +208 -0
- package/tests/hall_city.mjs +14 -6
- package/tests/hall_city_living.mjs +617 -0
- package/tests/hall_gov_counts.mjs +336 -0
- package/tests/hall_gov_living.mjs +636 -0
- package/tests/hall_gov_real_site.mjs +655 -0
- package/tests/hall_matter_card.mjs +6 -1
- package/tests/hall_my_agents.mjs +481 -0
- package/tests/hall_nav.mjs +107 -4
- package/tests/hall_page_gate_map.mjs +4 -0
- package/tests/hall_preview_governor_fixtures.mjs +30 -3
- package/tests/hall_preview_scrub.mjs +59 -1
- package/tests/hall_preview_security.mjs +169 -0
- package/tests/hall_reading_world.mjs +44 -0
- package/tests/hall_roster_world.mjs +6 -2
- package/tests/hall_street.mjs +18 -2
- package/tests/hall_street_amend.mjs +1 -1
- package/tests/hall_street_deck.mjs +609 -0
- package/tests/hall_street_depth.mjs +559 -0
- package/tests/hall_street_desk.mjs +1 -1
- package/tests/hall_street_inside.mjs +29 -5
- package/tests/hall_tweak_editor.mjs +4 -1
- package/tests/hub_account_page.mjs +94 -3
- package/tests/hub_connections_page.mjs +507 -0
- package/tests/hub_invite_badge.mjs +1 -1
- package/tests/idp_device_exchange.mjs +18 -0
- package/tests/landing_legal_pages.mjs +8 -5
- package/tests/landing_page.mjs +23 -4
- package/tests/nav_permission_atoms.mjs +4 -1
- package/tests/profile_connections_ui.mjs +88 -25
- package/tests/projects_hub.mjs +15 -0
- package/tests/render_check.mjs +2 -2
- package/tests/ship_broadcast_routing.mjs +57 -16
- package/tests/ship_broadcast_server.mjs +40 -7
- package/tests/ship_merge_gate.mjs +19 -0
- package/tests/ui_design_kit.mjs +2 -2
- 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;
|
|
@@ -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 };
|