@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.
- package/.bongos-core.json +616 -186
- 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 +4 -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 +128 -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-run.js +20 -5
- package/scripts/gds/autobongos-tweak-lane.js +262 -0
- package/scripts/gds/autobongos-verify.js +21 -2
- package/scripts/gds/claim.js +2 -0
- 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/gds/tweak-halt.js +1 -0
- 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/autobongos_tweak_lane.mjs +208 -0
- 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,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/government/treasury-db.js — the treasury's reads and writes (task
|
|
4
|
+
// 1004712, government_031; ADR 0379). The rules live in treasury.js; this file
|
|
5
|
+
// only reads real spend, the stored pots and loans, and writes the two things
|
|
6
|
+
// the treasury decides: a goal's budget and a loan's answer.
|
|
7
|
+
//
|
|
8
|
+
// REAL SPEND, NOT A COPY OF IT. A pot's spend is summed from cost_log through
|
|
9
|
+
// each task's goal on every read, in the calendar month cost-summary's by_month
|
|
10
|
+
// uses (to_char(recorded_at, 'YYYY-MM')), so the treasury and the spend chart
|
|
11
|
+
// can never disagree about a month. Nothing is cached (ADR 0016's posture).
|
|
12
|
+
//
|
|
13
|
+
// The pool comes through the published doorway (ADR 0091 §2).
|
|
14
|
+
|
|
15
|
+
const { pool } = require('../../src/module-api');
|
|
16
|
+
const api = require('../../src/module-api');
|
|
17
|
+
const treasury = require('./treasury');
|
|
18
|
+
const asksDb = require('./asks-db');
|
|
19
|
+
|
|
20
|
+
// This month's and last month's real spend: the totals, and this month's per goal.
|
|
21
|
+
async function readSpend(month) {
|
|
22
|
+
const last = treasury.prevMonth(month);
|
|
23
|
+
const [totals, byGoal] = await Promise.all([
|
|
24
|
+
pool.query(
|
|
25
|
+
`SELECT to_char(recorded_at, 'YYYY-MM') AS month, COALESCE(SUM(amount_usd), 0) AS total_usd
|
|
26
|
+
FROM cost_log
|
|
27
|
+
WHERE recorded_at >= now() - interval '75 days'
|
|
28
|
+
AND to_char(recorded_at, 'YYYY-MM') IN ($1, $2)
|
|
29
|
+
GROUP BY 1`,
|
|
30
|
+
[month, last],
|
|
31
|
+
),
|
|
32
|
+
pool.query(
|
|
33
|
+
`SELECT t.goal_id, COALESCE(SUM(cl.amount_usd), 0) AS total_usd
|
|
34
|
+
FROM cost_log cl
|
|
35
|
+
JOIN tasks t ON t.id = cl.task_id
|
|
36
|
+
WHERE cl.recorded_at >= now() - interval '40 days'
|
|
37
|
+
AND to_char(cl.recorded_at, 'YYYY-MM') = $1
|
|
38
|
+
AND t.goal_id IS NOT NULL
|
|
39
|
+
GROUP BY t.goal_id`,
|
|
40
|
+
[month],
|
|
41
|
+
),
|
|
42
|
+
]);
|
|
43
|
+
const of = (m) => Number((totals.rows.find((r) => r.month === m) || {}).total_usd) || 0;
|
|
44
|
+
const by = {};
|
|
45
|
+
for (const r of byGoal.rows) by[String(r.goal_id)] = Number(r.total_usd) || 0;
|
|
46
|
+
return { month_usd: of(month), last_month_usd: of(last), by_goal: by };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function listPots() {
|
|
50
|
+
const { rows } = await pool.query('SELECT goal_id, budget_usd, set_by, set_at FROM government_pots ORDER BY goal_id');
|
|
51
|
+
return rows;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function listOpenGoals() {
|
|
55
|
+
const { rows } = await pool.query("SELECT id, title, status FROM goals WHERE status = 'open' ORDER BY id");
|
|
56
|
+
return rows;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// This month's borrows, and any still open from before.
|
|
60
|
+
async function listBorrows(month) {
|
|
61
|
+
const { rows } = await pool.query(
|
|
62
|
+
`SELECT id, goal_id, month, amount_usd, budget_usd, spent_usd, filed_at, state, lent_usd, answer_key, answered_by, answered_at
|
|
63
|
+
FROM government_pot_borrows
|
|
64
|
+
WHERE month = $1 OR state = 'open'
|
|
65
|
+
ORDER BY id`,
|
|
66
|
+
[month],
|
|
67
|
+
);
|
|
68
|
+
return rows;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function getBorrow(id) {
|
|
72
|
+
const { rows } = await pool.query(
|
|
73
|
+
`SELECT b.id, b.goal_id, b.month, b.amount_usd, b.budget_usd, b.spent_usd, b.filed_at, b.state, b.lent_usd,
|
|
74
|
+
b.answer_key, b.answered_by, b.answered_at, g.title
|
|
75
|
+
FROM government_pot_borrows b JOIN goals g ON g.id = b.goal_id
|
|
76
|
+
WHERE b.id = $1::bigint`,
|
|
77
|
+
[id],
|
|
78
|
+
);
|
|
79
|
+
return rows[0] || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The spend cap, off unless the project set one (project_settings, read uncached).
|
|
83
|
+
async function readCap() {
|
|
84
|
+
const r = await api.projectSettings.resolveSpendCap();
|
|
85
|
+
return { usd: r.usd, source: r.source };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The whole treasury, read once, as treasury.buildTreasury shapes it.
|
|
89
|
+
async function readTreasury({ now = Date.now() } = {}) {
|
|
90
|
+
const month = treasury.monthKey(now);
|
|
91
|
+
const [goals, pots, spend, borrows, cap] = await Promise.all([listOpenGoals(), listPots(), readSpend(month), listBorrows(month), readCap()]);
|
|
92
|
+
return treasury.buildTreasury({ now, goals, pots, spend, borrows, cap });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Give a goal a pot, change its budget, or (budget null) take it away.
|
|
96
|
+
async function setPot({ goalId, budget, builderId }) {
|
|
97
|
+
if (budget == null) {
|
|
98
|
+
const { rowCount } = await pool.query('DELETE FROM government_pots WHERE goal_id = $1::bigint', [goalId]);
|
|
99
|
+
return { removed: rowCount > 0 };
|
|
100
|
+
}
|
|
101
|
+
const { rows } = await pool.query(
|
|
102
|
+
`INSERT INTO government_pots (goal_id, budget_usd, set_by, set_at)
|
|
103
|
+
VALUES ($1::bigint, $2, $3, now())
|
|
104
|
+
ON CONFLICT (goal_id) DO UPDATE SET budget_usd = EXCLUDED.budget_usd, set_by = EXCLUDED.set_by, set_at = now()
|
|
105
|
+
RETURNING goal_id, budget_usd, set_by, set_at`,
|
|
106
|
+
[goalId, budget, builderId == null ? null : builderId],
|
|
107
|
+
);
|
|
108
|
+
return { pot: rows[0] };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function goalById(goalId) {
|
|
112
|
+
const { rows } = await pool.query('SELECT id, title, status FROM goals WHERE id = $1::bigint', [goalId]);
|
|
113
|
+
return rows[0] || null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// FILE THE BORROWS. Every dry pot with nothing open, and no "no" this month,
|
|
117
|
+
// files one: the row (open), then its ask in the stored shape on the docket id
|
|
118
|
+
// treasury_spending:<id>, waiting on lead:treasury. The one-open index makes a
|
|
119
|
+
// racing second filer a no-op. → [{ goal_id, borrow_id, amount_usd }]
|
|
120
|
+
async function fileBorrows({ now = Date.now(), view = null } = {}) {
|
|
121
|
+
const t = view || await readTreasury({ now });
|
|
122
|
+
const borrows = await listBorrows(t.month);
|
|
123
|
+
const filed = [];
|
|
124
|
+
for (const pot of t.pots) {
|
|
125
|
+
if (!treasury.needsBorrow(pot, borrows, t.month)) continue;
|
|
126
|
+
const amount = treasury.borrowAmount(pot, now);
|
|
127
|
+
const ask = treasury.borrowAsk({ title: pot.title, amount_usd: amount, spent_usd: pot.spent_usd, budget_usd: pot.budget_usd, lent_usd: pot.lent_usd, cap: t.cap, month_usd: t.spend.month_usd });
|
|
128
|
+
const { rows } = await pool.query(
|
|
129
|
+
`INSERT INTO government_pot_borrows (goal_id, month, amount_usd, budget_usd, spent_usd)
|
|
130
|
+
VALUES ($1::bigint, $2, $3, $4, $5)
|
|
131
|
+
ON CONFLICT (goal_id) WHERE state = 'open' DO NOTHING
|
|
132
|
+
RETURNING id`,
|
|
133
|
+
[pot.goal_id, t.month, amount, pot.budget_usd + pot.lent_usd, pot.spent_usd],
|
|
134
|
+
);
|
|
135
|
+
if (!rows[0]) continue;
|
|
136
|
+
await asksDb.saveAsk({ matterId: treasury.matterId(rows[0].id), ask, builderId: null });
|
|
137
|
+
filed.push({ goal_id: pot.goal_id, borrow_id: String(rows[0].id), amount_usd: amount });
|
|
138
|
+
}
|
|
139
|
+
return filed;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Answer an open borrow. The trigger refuses a second answer, so a race is a 0-row no-op.
|
|
143
|
+
async function answerBorrow({ id, answer, builderId }) {
|
|
144
|
+
const { rows } = await pool.query(
|
|
145
|
+
`UPDATE government_pot_borrows
|
|
146
|
+
SET state = $2, lent_usd = $3, answer_key = $4, answered_by = $5
|
|
147
|
+
WHERE id = $1::bigint AND state = 'open'
|
|
148
|
+
RETURNING id, goal_id, month, amount_usd, budget_usd, spent_usd, filed_at, state, lent_usd, answer_key, answered_by, answered_at`,
|
|
149
|
+
[id, answer.state, answer.lent_usd, answer.answer_key, builderId],
|
|
150
|
+
);
|
|
151
|
+
return rows[0] || null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// The open borrows as docket matters, with their stored asks.
|
|
155
|
+
async function openBorrowRows() {
|
|
156
|
+
const { rows } = await pool.query(
|
|
157
|
+
`SELECT b.id, b.goal_id, b.month, b.amount_usd, b.budget_usd, b.spent_usd, b.filed_at, b.state, g.title
|
|
158
|
+
FROM government_pot_borrows b JOIN goals g ON g.id = b.goal_id
|
|
159
|
+
WHERE b.state = 'open'
|
|
160
|
+
ORDER BY b.id`,
|
|
161
|
+
);
|
|
162
|
+
const stored = await asksDb.asksFor(rows.map((r) => treasury.matterId(r.id)));
|
|
163
|
+
return rows.map((r) => ({ row: r, ask: stored.get(treasury.matterId(r.id)) || null }));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { readSpend, listPots, listOpenGoals, listBorrows, getBorrow, readCap, readTreasury, setPot, goalById, fileBorrows, answerBorrow, openBorrowRows };
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/government/treasury.js — THE TREASURY: a pot per goal, the reserve,
|
|
4
|
+
// and the borrow a dry pot asks for (task 1004712, goal 1000111 "government is
|
|
5
|
+
// how a project steers its AI"; ADR 0379).
|
|
6
|
+
//
|
|
7
|
+
// The project's AI spend is its treasury. The owner's rulings (2026-10-09):
|
|
8
|
+
// * there is NO spend limit by default. A cap is an optional project setting
|
|
9
|
+
// (project_settings['treasury.spend_cap_usd']), off until someone sets it;
|
|
10
|
+
// * a goal MAY have a pot: a monthly budget the Treasury's holder gives it;
|
|
11
|
+
// * when a pot runs dry, it asks to BORROW FROM THE RESERVE, and that borrow
|
|
12
|
+
// is a treasury matter: an ask in the stored ask shape (asks.js), waiting on
|
|
13
|
+
// lead:treasury, routed by routing.js to the Treasury's Lead, or the owner
|
|
14
|
+
// while it is unseated.
|
|
15
|
+
//
|
|
16
|
+
// WHAT IS REAL, AND FROM WHERE. Every dollar of spend is cost_log's: a pot's
|
|
17
|
+
// spend this month is the cost_log rows whose task is in the goal, this calendar
|
|
18
|
+
// month (the month cost-summary's by_month uses). What is stored is only what the
|
|
19
|
+
// treasury decided: a goal's budget (government_pots) and each loan
|
|
20
|
+
// (government_pot_borrows). Nothing here moves money or touches a payment
|
|
21
|
+
// provider; this is the project's own accounting over its real spend.
|
|
22
|
+
//
|
|
23
|
+
// pot.spent_usd this month's real spend on the goal's tasks
|
|
24
|
+
// pot.budget_usd the budget its holder gave it (stored)
|
|
25
|
+
// pot.lent_usd what the reserve lent it this month (stored loans)
|
|
26
|
+
// pot.dry spent >= budget + lent
|
|
27
|
+
// reserve last month's real spend that no pot was given this month,
|
|
28
|
+
// less what it has lent this month. A month that spent more
|
|
29
|
+
// than that is overdrawn (negative): with no limit, work runs
|
|
30
|
+
// anyway and the overspend shows as debt.
|
|
31
|
+
//
|
|
32
|
+
// A borrow asks for what the pot will need to finish the month at its pace
|
|
33
|
+
// (spend so far over the share of the month gone), rounded up to $50.
|
|
34
|
+
//
|
|
35
|
+
// PURE: no db, no clock of its own (now is passed in). treasury-db.js reads and
|
|
36
|
+
// writes; the poller (pollers/treasury-borrow.js) files borrows; the route reads.
|
|
37
|
+
|
|
38
|
+
const asks = require('./asks');
|
|
39
|
+
|
|
40
|
+
const MATTER_SOURCE = 'treasury_spending';
|
|
41
|
+
const SEAT = 'lead:treasury';
|
|
42
|
+
const ROUND_USD = 50;
|
|
43
|
+
const MAX_BUDGET_USD = 1000000;
|
|
44
|
+
const ANSWERS = Object.freeze(['lend', 'half', 'no']);
|
|
45
|
+
// An act on the docket matter: the deck maps an ask option's act 'answer:<key>'
|
|
46
|
+
// onto it (street-deck.js actFor), and the card sends it to the answer route.
|
|
47
|
+
const ACT_KEY = 'answer';
|
|
48
|
+
|
|
49
|
+
const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : 0; };
|
|
50
|
+
const round2 = (v) => Math.round(num(v) * 100) / 100;
|
|
51
|
+
const str = (v) => (v == null ? '' : String(v));
|
|
52
|
+
function usd(v) { return '$' + Math.round(num(v)); }
|
|
53
|
+
|
|
54
|
+
// 'YYYY-MM' of a UTC instant, and the month before it.
|
|
55
|
+
function monthKey(now) {
|
|
56
|
+
const d = new Date(num(now));
|
|
57
|
+
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`;
|
|
58
|
+
}
|
|
59
|
+
function prevMonth(key) {
|
|
60
|
+
const [y, m] = str(key).split('-').map(Number);
|
|
61
|
+
return m === 1 ? `${y - 1}-12` : `${y}-${String(m - 1).padStart(2, '0')}`;
|
|
62
|
+
}
|
|
63
|
+
// How much of this month is gone, counting today as begun: day 1 is 1/31, not 0.
|
|
64
|
+
function monthGone(now) {
|
|
65
|
+
const d = new Date(num(now));
|
|
66
|
+
const days = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 0)).getUTCDate();
|
|
67
|
+
return Math.min(1, d.getUTCDate() / days);
|
|
68
|
+
}
|
|
69
|
+
function roundUp(v) { return Math.max(ROUND_USD, Math.ceil(num(v) / ROUND_USD) * ROUND_USD); }
|
|
70
|
+
|
|
71
|
+
// A budget a person typed → whole dollars, or why not. null clears a pot.
|
|
72
|
+
function parseBudget(raw) {
|
|
73
|
+
if (raw === null) return { budget: null };
|
|
74
|
+
const n = Number(raw);
|
|
75
|
+
if (raw === undefined || raw === '' || !Number.isFinite(n)) return { reason: 'a budget is a number of dollars a month, or null to take the pot away' };
|
|
76
|
+
if (n <= 0) return { reason: 'a budget is more than $0; send null to take the pot away' };
|
|
77
|
+
if (n > MAX_BUDGET_USD) return { reason: `a budget is at most $${MAX_BUDGET_USD} a month` };
|
|
78
|
+
return { budget: Math.round(n * 100) / 100 };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The spend cap (a project setting; off by default). cap { usd|null, source }.
|
|
82
|
+
function capView(cap, monthUsd) {
|
|
83
|
+
const usdCap = cap && cap.usd != null && num(cap.usd) > 0 ? round2(cap.usd) : null;
|
|
84
|
+
if (usdCap == null) return { on: false, usd: null, over: false, source: (cap && cap.source) || 'default', line: 'Off. There is no spend limit unless the project sets one.' };
|
|
85
|
+
const over = num(monthUsd) >= usdCap;
|
|
86
|
+
return {
|
|
87
|
+
on: true, usd: usdCap, over, source: (cap && cap.source) || 'project_settings',
|
|
88
|
+
line: over ? `On at ${usd(usdCap)} a month, and this month is past it.` : `On at ${usd(usdCap)} a month.`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// What a dry pot asks the reserve for: enough to finish the month at its pace.
|
|
93
|
+
function borrowAmount(pot, now) {
|
|
94
|
+
const have = num(pot.budget_usd) + num(pot.lent_usd);
|
|
95
|
+
const pace = num(pot.spent_usd) / monthGone(now);
|
|
96
|
+
return roundUp(Math.max(pace, num(pot.spent_usd)) - have);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Should this pot file a borrow now? Dry, nothing open, and the treasury has not
|
|
100
|
+
// said no to it this month (a no stands until the month turns).
|
|
101
|
+
function needsBorrow(pot, borrows, month) {
|
|
102
|
+
if (!pot || !pot.dry) return false;
|
|
103
|
+
return !(borrows || []).some((b) => String(b.goal_id) === String(pot.goal_id)
|
|
104
|
+
&& (b.state === 'open' || (b.state === 'refused' && b.month === month)));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The whole treasury, as GET /government/treasury serves it.
|
|
108
|
+
// in: { now, goals: [{ id, title, status }], pots: [{ goal_id, budget_usd, set_at }],
|
|
109
|
+
// spend: { month_usd, last_month_usd, by_goal: { [goal_id]: usd } },
|
|
110
|
+
// borrows: [rows of this month, and any still open], cap: { usd, source } }
|
|
111
|
+
function buildTreasury({ now, goals = [], pots = [], spend = {}, borrows = [], cap = null } = {}) {
|
|
112
|
+
const month = monthKey(now);
|
|
113
|
+
const monthUsd = round2(spend.month_usd);
|
|
114
|
+
const lastUsd = round2(spend.last_month_usd);
|
|
115
|
+
const byGoal = spend.by_goal || {};
|
|
116
|
+
const open = new Map(goals.filter((g) => g && g.status === 'open').map((g) => [String(g.id), g]));
|
|
117
|
+
const loans = (borrows || []).map(borrowView);
|
|
118
|
+
const potRows = (pots || []).filter((p) => p && open.has(String(p.goal_id))).map((p) => {
|
|
119
|
+
const id = String(p.goal_id);
|
|
120
|
+
const spent = round2(byGoal[id]);
|
|
121
|
+
const budget = round2(p.budget_usd);
|
|
122
|
+
const mine = loans.filter((b) => b.goal_id === id);
|
|
123
|
+
const lent = round2(mine.filter((b) => b.state === 'lent' && b.month === month).reduce((t, b) => t + num(b.lent_usd), 0));
|
|
124
|
+
const pot = {
|
|
125
|
+
goal_id: id, title: str(open.get(id).title), budget_usd: budget, lent_usd: lent, spent_usd: spent,
|
|
126
|
+
left_usd: round2(budget + lent - spent), dry: spent >= budget + lent,
|
|
127
|
+
share: monthUsd > 0 ? Math.round((spent / monthUsd) * 10000) / 10000 : 0,
|
|
128
|
+
set_at: p.set_at instanceof Date ? p.set_at.toISOString() : (p.set_at || null),
|
|
129
|
+
};
|
|
130
|
+
// its borrow: the open one, else this month's latest answer
|
|
131
|
+
const live = mine.find((b) => b.state === 'open') || mine.filter((b) => b.month === month).sort((a, b) => Number(b.id) - Number(a.id))[0] || null;
|
|
132
|
+
pot.borrow = live;
|
|
133
|
+
return pot;
|
|
134
|
+
}).sort((a, b) => (b.dry - a.dry) || (b.spent_usd - a.spent_usd));
|
|
135
|
+
const given = round2(potRows.reduce((t, p) => t + p.budget_usd, 0));
|
|
136
|
+
const lentAll = round2(loans.filter((b) => b.state === 'lent' && b.month === month).reduce((t, b) => t + num(b.lent_usd), 0));
|
|
137
|
+
const reserveUsd = lastUsd > 0 ? round2(lastUsd - given - lentAll) : null;
|
|
138
|
+
const reserve = {
|
|
139
|
+
usd: reserveUsd, given_usd: given, lent_usd: lentAll, overdrawn: reserveUsd != null && reserveUsd < 0,
|
|
140
|
+
line: reserveUsd == null
|
|
141
|
+
? 'No month of spend stands behind the reserve yet.'
|
|
142
|
+
: reserveUsd < 0
|
|
143
|
+
? `Overdrawn by ${usd(-reserveUsd)}: work runs on, and the overspend shows as debt.`
|
|
144
|
+
: `${usd(reserveUsd)} of last month's spend that no pot was given.`,
|
|
145
|
+
};
|
|
146
|
+
return {
|
|
147
|
+
month,
|
|
148
|
+
cap: capView(cap, monthUsd),
|
|
149
|
+
spend: { month_usd: monthUsd, last_month_usd: lastUsd },
|
|
150
|
+
reserve,
|
|
151
|
+
pots: potRows,
|
|
152
|
+
basis: 'A pot is a budget its holder gave a goal; its spend is this month\'s real spend on the goal\'s tasks. The reserve is last month\'s real spend that no pot was given, less what it lent.',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function borrowView(r) {
|
|
157
|
+
if (!r) return null;
|
|
158
|
+
const iso = (v) => (v instanceof Date ? v.toISOString() : (v || null));
|
|
159
|
+
return {
|
|
160
|
+
id: String(r.id), goal_id: String(r.goal_id), month: str(r.month), matter_id: matterId(r.id),
|
|
161
|
+
amount_usd: round2(r.amount_usd), budget_usd: round2(r.budget_usd), spent_usd: round2(r.spent_usd),
|
|
162
|
+
state: str(r.state), lent_usd: r.lent_usd == null ? null : round2(r.lent_usd), answer_key: r.answer_key || null,
|
|
163
|
+
filed_at: iso(r.filed_at), answered_at: iso(r.answered_at),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function matterId(id) { return `${MATTER_SOURCE}:${id}`; }
|
|
167
|
+
|
|
168
|
+
// The goal as a question can name it: its title when short and plain, else "this goal".
|
|
169
|
+
function goalName(title) {
|
|
170
|
+
// 'Working area 1 — Project creation' reads as 'Project creation'
|
|
171
|
+
const t = str(title).replace(/\s+/g, ' ').trim().split(' — ').pop().trim();
|
|
172
|
+
if (!t || asks.words(t).length > 5 || t.length > 48 || /[?!.:`*#<>[\]]/.test(t)) return 'this goal';
|
|
173
|
+
return t;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The borrow ask, in the stored ask shape. Checked by asks.checkAsk before it is
|
|
177
|
+
// returned, so a goal title can never make it slop: an unusable name falls back
|
|
178
|
+
// to "this goal". → the stored ask.
|
|
179
|
+
function borrowAsk({ title, amount_usd, spent_usd, budget_usd, lent_usd = 0, cap = null, month_usd = 0 }) {
|
|
180
|
+
const amt = Math.round(num(amount_usd));
|
|
181
|
+
const half = Math.max(1, Math.round(amt / 2));
|
|
182
|
+
const pastCap = !!(cap && cap.on && num(month_usd) + amt > num(cap.usd));
|
|
183
|
+
const card = (name) => ({
|
|
184
|
+
question: `Lend ${name} ${usd(amt)} from the reserve?`,
|
|
185
|
+
options: [
|
|
186
|
+
{ key: 'lend', label: `Lend ${usd(amt)}`, result: 'The pot refills and work carries on as before.', act: `${ACT_KEY}:lend` },
|
|
187
|
+
{ key: 'half', label: `Lend ${usd(half)}`, result: 'Half now; it asks again if it runs dry.', act: `${ACT_KEY}:half` },
|
|
188
|
+
{ key: 'no', label: 'Say no', result: 'Work still carries on; the overspend shows as debt.', act: `${ACT_KEY}:no` },
|
|
189
|
+
],
|
|
190
|
+
recommend: pastCap
|
|
191
|
+
? { key: 'no', why: 'This month is past the spend cap the project set.' }
|
|
192
|
+
: { key: 'lend', why: 'There is no spend limit, so work runs anyway; lending keeps the books honest.' },
|
|
193
|
+
waits_on: SEAT,
|
|
194
|
+
if_nobody_answers: 'Work carries on and the overspend is booked against the reserve.',
|
|
195
|
+
detail: `${str(title).trim() || 'This goal'}: its pot is dry, ${usd(spent_usd)} spent of ${usd(num(budget_usd) + num(lent_usd))} this month. `
|
|
196
|
+
+ `It asks for ${usd(amt)}, enough to finish the month at its pace.`,
|
|
197
|
+
});
|
|
198
|
+
let v = asks.checkAsk(card(goalName(title)));
|
|
199
|
+
if (!v.ok) v = asks.checkAsk(card('this goal'));
|
|
200
|
+
if (!v.ok) throw new Error('treasury borrow ask refused: ' + asks.refusalLine(v.errors));
|
|
201
|
+
return v.ask;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// An answer → what is written: { state, lent_usd, answer_key } or null.
|
|
205
|
+
function answerOf(borrow, key) {
|
|
206
|
+
if (!borrow || borrow.state !== 'open' || !ANSWERS.includes(key)) return null;
|
|
207
|
+
const amt = round2(borrow.amount_usd);
|
|
208
|
+
if (key === 'no') return { state: 'refused', lent_usd: null, answer_key: 'no' };
|
|
209
|
+
return { state: 'lent', lent_usd: key === 'half' ? Math.max(1, Math.round(amt / 2)) : amt, answer_key: key };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// An open borrow → the docket matter (the treasury_spending source). `acts` only
|
|
213
|
+
// for a viewer who sits in the seat it waits on (canAnswer).
|
|
214
|
+
function borrowMatter(borrow, { title, canAnswer = false } = {}) {
|
|
215
|
+
const b = borrowView(borrow);
|
|
216
|
+
const name = str(title).trim() || `goal ${b.goal_id}`;
|
|
217
|
+
const half = Math.max(1, Math.round(b.amount_usd / 2));
|
|
218
|
+
return {
|
|
219
|
+
id: b.id,
|
|
220
|
+
building: 'treasury',
|
|
221
|
+
rule: 'decide',
|
|
222
|
+
state: 'needs',
|
|
223
|
+
title: `${name}: its pot is dry`,
|
|
224
|
+
short: `${usd(b.spent_usd)} spent of ${usd(b.budget_usd)}`,
|
|
225
|
+
where: 'treasury · borrow from the reserve',
|
|
226
|
+
opened_at: b.filed_at,
|
|
227
|
+
deadline: null,
|
|
228
|
+
href: `/goal/${b.goal_id}`,
|
|
229
|
+
ref: { type: 'pot_borrow', id: b.id },
|
|
230
|
+
detail: `${name} has spent ${usd(b.spent_usd)} of its ${usd(b.budget_usd)} pot this month and asks the reserve for ${usd(b.amount_usd)}.`,
|
|
231
|
+
...(canAnswer ? { acts: [{ key: ACT_KEY, options: [
|
|
232
|
+
{ key: 'lend', label: `Lend ${usd(b.amount_usd)}` }, { key: 'half', label: `Lend ${usd(half)}` }, { key: 'no', label: 'Say no' },
|
|
233
|
+
] }] } : {}),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = {
|
|
238
|
+
MATTER_SOURCE, SEAT, ROUND_USD, MAX_BUDGET_USD, ANSWERS, ACT_KEY,
|
|
239
|
+
monthKey, prevMonth, monthGone, parseBudget, capView, borrowAmount, needsBorrow,
|
|
240
|
+
buildTreasury, borrowView, matterId, goalName, borrowAsk, answerOf, borrowMatter,
|
|
241
|
+
};
|
|
@@ -92,8 +92,8 @@
|
|
|
92
92
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
93
93
|
|
|
94
94
|
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
95
|
-
<script src="/builders/craft-mode.js?v=2026-10-
|
|
96
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
95
|
+
<script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
|
|
96
|
+
<script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
|
|
97
97
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
98
98
|
<script src="/builders/palette.js" defer></script>
|
|
99
99
|
<script src="/builders/atlas.js?v=2026-09-02-index" defer></script>
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
52
52
|
chrome exists before the page script runs. -->
|
|
53
53
|
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
54
|
-
<script src="/builders/craft-mode.js?v=2026-10-
|
|
55
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
54
|
+
<script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
|
|
55
|
+
<script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
|
|
56
56
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
57
57
|
<script src="/builders/palette.js" defer></script>
|
|
58
58
|
<!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
62
62
|
|
|
63
63
|
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
64
|
-
<script src="/builders/craft-mode.js?v=2026-10-
|
|
65
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
64
|
+
<script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
|
|
65
|
+
<script src="/builders/shell.js?v=2026-10-10-hall-keys1"></script>
|
|
66
66
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
67
67
|
<script src="/builders/palette.js" defer></script>
|
|
68
68
|
<script src="/builders/dom-utils.js?v=2026-06-16-board"></script>
|
|
@@ -146,6 +146,8 @@
|
|
|
146
146
|
return `${escapeHtml(title)} ${pill('quiet', `${matterArea[1]} matter`)}`;
|
|
147
147
|
}
|
|
148
148
|
if (item.subject && item.subject.kind === 'decision_rules') return `Who decides what ${pill('quiet', 'amendment')}`;
|
|
149
|
+
// task 1004706: a change to the project's laws (enact, repeal, or both)
|
|
150
|
+
if (item.subject && item.subject.kind === 'laws') return `The project's laws ${pill('quiet', 'law')}`;
|
|
149
151
|
return `Constitutional amendment ${pill('quiet', 'amendment')}`;
|
|
150
152
|
}
|
|
151
153
|
|