@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,160 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/government/buildouts.js — A LEAD'S BUILD-OUTS: the structure a
|
|
4
|
+
// building grows under its holder, as data (task 1004711, goal 1000111; ADR
|
|
5
|
+
// 0378). db.js holds the reads and the one write, decision-board.js decides a
|
|
6
|
+
// build-out (at once, or filed as a vote) and carries out a passed one,
|
|
7
|
+
// routes/government.js serves POST /government/buildouts, and government_030
|
|
8
|
+
// is the table and its guard.
|
|
9
|
+
//
|
|
10
|
+
// WHAT A BUILD-OUT IS. One of the four capsules build-rights.js names, added to
|
|
11
|
+
// one of the nine buildings: a committee room, a new floor, an annex, or growing
|
|
12
|
+
// the building into a district. It carries a short name and one line saying
|
|
13
|
+
// what it is for. A build-out is never edited or taken down: each row is one
|
|
14
|
+
// change to the city, on the record.
|
|
15
|
+
//
|
|
16
|
+
// THE TWO PATHS, decided by the ONE rule (build-rights.js, task 1004710):
|
|
17
|
+
// * AT ONCE — the building's holder (its Lead, or the owner seat when it is
|
|
18
|
+
// unseated: leads.answerFor) asks, and build_rights for that capsule says
|
|
19
|
+
// `without_vote`: the row is written now, carrying the rule's `ground` and
|
|
20
|
+
// `why`. The city shows it being built (a live site, scaffold and crane)
|
|
21
|
+
// for BUILD_HOURS from when it started; then it stands as built.
|
|
22
|
+
// * BY VOTE — otherwise the ask is filed as a matter (action build_out)
|
|
23
|
+
// through the board's own matters path (decision-board openMatter, the same
|
|
24
|
+
// code POST /government/board/matters runs). While the sitting is open the
|
|
25
|
+
// city draws it as a dashed ghost with the sitting's tally arc; when it
|
|
26
|
+
// passes, the row is written naming the passed item and it goes up.
|
|
27
|
+
//
|
|
28
|
+
// WHO MAY ASK. Only the building's holder: a Lead grows the structure under
|
|
29
|
+
// them. Whether they may do it without a vote is never theirs to say.
|
|
30
|
+
//
|
|
31
|
+
// PURE: no db.
|
|
32
|
+
|
|
33
|
+
const { AREA_KEYS } = require('./decision-rules');
|
|
34
|
+
const { CAPSULES } = require('./build-rights');
|
|
35
|
+
|
|
36
|
+
// How long a build-out stands as a live site before it reads as built. Progress
|
|
37
|
+
// is the share of this window since it started: a real clock, not a number
|
|
38
|
+
// anyone sets.
|
|
39
|
+
const BUILD_HOURS = 24;
|
|
40
|
+
const BUILD_MS = BUILD_HOURS * 3600 * 1000;
|
|
41
|
+
const NAME_MIN = 2;
|
|
42
|
+
const NAME_MAX = 40;
|
|
43
|
+
const LINE_MAX = 140;
|
|
44
|
+
|
|
45
|
+
// What each capsule is called when its holder names nothing.
|
|
46
|
+
const DEFAULT_NAMES = Object.freeze({
|
|
47
|
+
committee: 'Committee room',
|
|
48
|
+
floor: 'New floor',
|
|
49
|
+
annex: 'Annex',
|
|
50
|
+
district: 'District',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const isBuilding = (b) => typeof b === 'string' && AREA_KEYS.includes(b);
|
|
54
|
+
const isCapsule = (c) => typeof c === 'string' && CAPSULES.includes(c);
|
|
55
|
+
|
|
56
|
+
// A build-out's name, normalised, or null when it breaks the rule (2-40
|
|
57
|
+
// characters, one line). An absent name takes the capsule's default.
|
|
58
|
+
function nameOf(raw, capsule) {
|
|
59
|
+
if (raw == null || raw === '') return DEFAULT_NAMES[capsule] || null;
|
|
60
|
+
if (typeof raw !== 'string' || /[\r\n]/.test(raw)) return null;
|
|
61
|
+
const s = raw.trim();
|
|
62
|
+
return s.length >= NAME_MIN && s.length <= NAME_MAX ? s : null;
|
|
63
|
+
}
|
|
64
|
+
// Its line: one line of at most 140 characters, or '' when none is given.
|
|
65
|
+
function lineOf(raw) {
|
|
66
|
+
if (raw == null || raw === '') return '';
|
|
67
|
+
if (typeof raw !== 'string' || /[\r\n]/.test(raw)) return null;
|
|
68
|
+
const s = raw.trim();
|
|
69
|
+
return s.length <= LINE_MAX ? s : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A build-out ask, checked strictly: { ok, buildout } or { ok: false, reason, field }.
|
|
73
|
+
// The same shape is the matter action (type 'build_out') a vote carries.
|
|
74
|
+
function checkBuildOut(raw) {
|
|
75
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, reason: 'bad_buildout', field: null };
|
|
76
|
+
for (const k of Object.keys(raw)) {
|
|
77
|
+
if (!['type', 'building', 'capsule', 'name', 'line'].includes(k)) return { ok: false, reason: 'unknown_field', field: k };
|
|
78
|
+
}
|
|
79
|
+
if (raw.type != null && raw.type !== 'build_out') return { ok: false, reason: 'bad_buildout', field: 'type' };
|
|
80
|
+
if (!isBuilding(raw.building)) return { ok: false, reason: 'bad_buildout', field: 'building' };
|
|
81
|
+
if (!isCapsule(raw.capsule)) return { ok: false, reason: 'bad_buildout', field: 'capsule' };
|
|
82
|
+
const name = nameOf(raw.name, raw.capsule);
|
|
83
|
+
if (!name) return { ok: false, reason: 'bad_buildout', field: 'name' };
|
|
84
|
+
const line = lineOf(raw.line);
|
|
85
|
+
if (line == null) return { ok: false, reason: 'bad_buildout', field: 'line' };
|
|
86
|
+
return { ok: true, buildout: { type: 'build_out', building: raw.building, capsule: raw.capsule, name, line } };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Where a build-out vote is filed: a matter at the building itself. The Charter
|
|
90
|
+
// takes no matters (its changes are amendments), so its build-outs go to the
|
|
91
|
+
// board as a People matter, the building that answers for positions.
|
|
92
|
+
const matterAreaFor = (building) => (building === 'charter' ? 'people' : building);
|
|
93
|
+
|
|
94
|
+
// How far up a build-out stands at `now` (ms): 0..1 over BUILD_HOURS from its start.
|
|
95
|
+
function progressOf(row, now) {
|
|
96
|
+
const t = Date.parse(row && row.started_at);
|
|
97
|
+
if (!Number.isFinite(t) || !Number.isFinite(now)) return 1;
|
|
98
|
+
return Math.max(0, Math.min(1, Math.round(((now - t) / BUILD_MS) * 1000) / 1000));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// One standing build-out as the read shows it.
|
|
102
|
+
function projectBuildOut(r, now) {
|
|
103
|
+
const progress = progressOf(r, now);
|
|
104
|
+
const started = r.started_at instanceof Date ? r.started_at.toISOString() : r.started_at || null;
|
|
105
|
+
return {
|
|
106
|
+
id: String(r.id),
|
|
107
|
+
building: r.building,
|
|
108
|
+
capsule: r.capsule,
|
|
109
|
+
name: r.name,
|
|
110
|
+
line: r.line || '',
|
|
111
|
+
state: progress >= 1 ? 'built' : 'building',
|
|
112
|
+
progress,
|
|
113
|
+
started_at: started,
|
|
114
|
+
builder: r.built_by == null ? null : { id: String(r.built_by), login: r.github_login || null, name: r.display_name || r.github_login || null },
|
|
115
|
+
// how it was allowed: a passed vote, or the rule that let its holder build at once
|
|
116
|
+
allowed: r.authorised_by_item_id != null
|
|
117
|
+
? { path: 'vote', item_id: String(r.authorised_by_item_id), href: `/board-room?item=${r.authorised_by_item_id}` }
|
|
118
|
+
: { path: 'at_once', ground: r.ground, why: r.why || null },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// One proposal waiting on its sitting: an open board item whose matter is a build_out.
|
|
123
|
+
function projectProposal(p) {
|
|
124
|
+
const a = (p && p.action) || {};
|
|
125
|
+
return {
|
|
126
|
+
item_id: String(p.item_id),
|
|
127
|
+
matter_id: String(p.matter_id),
|
|
128
|
+
// the docket's id for this sitting: its tally rides there
|
|
129
|
+
docket_id: `board:${p.item_id}`,
|
|
130
|
+
building: a.building,
|
|
131
|
+
capsule: a.capsule,
|
|
132
|
+
name: a.name,
|
|
133
|
+
line: a.line || '',
|
|
134
|
+
opened_at: p.opened_at || null,
|
|
135
|
+
closes_at: p.closes_at || null,
|
|
136
|
+
href: `/board-room?item=${p.item_id}`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Per building: what stands (built or going up) and what waits on a vote.
|
|
141
|
+
// `rows` are government_buildouts rows, `pending` the open build_out sittings
|
|
142
|
+
// ({ item_id, matter_id, action, opened_at, closes_at }).
|
|
143
|
+
function structureOf(building, rows, pending, now) {
|
|
144
|
+
return {
|
|
145
|
+
standing: (rows || []).filter((r) => r && r.building === building).map((r) => projectBuildOut(r, now)),
|
|
146
|
+
proposed: (pending || []).filter((p) => p && p.action && p.action.building === building).map(projectProposal),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
BUILD_HOURS,
|
|
152
|
+
BUILD_MS,
|
|
153
|
+
DEFAULT_NAMES,
|
|
154
|
+
NAME_MAX,
|
|
155
|
+
LINE_MAX,
|
|
156
|
+
checkBuildOut,
|
|
157
|
+
matterAreaFor,
|
|
158
|
+
progressOf,
|
|
159
|
+
structureOf,
|
|
160
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/government/counts.js — the government's counts, in ONE server read
|
|
4
|
+
// (task 1004716, goal 1000111): GET /government/counts.
|
|
5
|
+
//
|
|
6
|
+
// WHY. The city's plaque and door, the street's header, the decision deck and the
|
|
7
|
+
// top bar's Governing tally all print the same five numbers ("24 to do", "3 wait
|
|
8
|
+
// on others", "38 to decide", "5 agents working", "7 moving"). The hall counted
|
|
9
|
+
// them itself over about five reads (the docket, the blockers, the seats, the
|
|
10
|
+
// claims, GET /moving), and a page that only wanted the tally paid for all of
|
|
11
|
+
// them. Now the server answers the counts once, and the hall's one count rule
|
|
12
|
+
// (modules/hall-ui/public/gov-living.js countsOf) reads them from here.
|
|
13
|
+
//
|
|
14
|
+
// THE RULE IS THE DOCKET'S, not a second one. Whether a matter is on you is the
|
|
15
|
+
// route the docket already carries (routing.routeMatter `yours`, task 1004705):
|
|
16
|
+
// on_you docket matters whose route is yours (the docket's counts.yours)
|
|
17
|
+
// on_others matters that need a person, are not yours, and somebody can
|
|
18
|
+
// still answer (an overdue or unheld ask has settled: route.nobody)
|
|
19
|
+
// open on_you + on_others: what the deck holds
|
|
20
|
+
// agents_at_work open claims, one per agent (GET /tasks/claimable active_claims)
|
|
21
|
+
// moving tasks being built or waiting to land (GET /moving: active +
|
|
22
|
+
// confirmed); never below agents_at_work
|
|
23
|
+
// A docket that could not be routed (the seats were unreadable) says every matter
|
|
24
|
+
// that needs a person is on you, as the hall did before routing existed.
|
|
25
|
+
// A count that could not be read is null, never 0: zero says nothing waits on you.
|
|
26
|
+
//
|
|
27
|
+
// `yours` and `others` name the matters, so a page can take away the ones it has
|
|
28
|
+
// answered this visit without reading the docket again.
|
|
29
|
+
|
|
30
|
+
const api = require('../../src/module-api');
|
|
31
|
+
|
|
32
|
+
// → the counts over a docket (docket.buildDocket's shape) and the traffic counts.
|
|
33
|
+
// Pure; the route and the hall's parity test (tests/government_counts.mjs) call it.
|
|
34
|
+
function countsOf(docket, traffic) {
|
|
35
|
+
const d = docket && typeof docket === 'object' ? docket : null;
|
|
36
|
+
const t = traffic && typeof traffic === 'object' ? traffic : {};
|
|
37
|
+
const yours = [];
|
|
38
|
+
const others = [];
|
|
39
|
+
if (d) {
|
|
40
|
+
const routed = d.routed === true;
|
|
41
|
+
for (const m of Array.isArray(d.matters) ? d.matters : []) {
|
|
42
|
+
if (!m || m.id == null || m.state !== 'needs') continue;
|
|
43
|
+
const r = routed && m.route && typeof m.route === 'object' ? m.route : null;
|
|
44
|
+
if (r && r.nobody) continue;
|
|
45
|
+
(routed ? (r && r.yours) : true) ? yours.push(String(m.id)) : others.push(String(m.id));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const n = (v) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null);
|
|
49
|
+
return {
|
|
50
|
+
on_you: d ? yours.length : null,
|
|
51
|
+
on_others: d ? others.length : null,
|
|
52
|
+
open: d ? yours.length + others.length : null,
|
|
53
|
+
agents_at_work: n(t.agents_at_work),
|
|
54
|
+
moving: n(t.moving),
|
|
55
|
+
routed: !!(d && d.routed === true),
|
|
56
|
+
yours: d ? yours : null,
|
|
57
|
+
others: d ? others : null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The two traffic counts through the `lifecycle.traffic` port (modules/lifecycle/
|
|
62
|
+
// moving.js); a module that is off, or a count that fails, leaves its figure null.
|
|
63
|
+
async function readTraffic() {
|
|
64
|
+
const port = typeof api.resolveOptional === 'function' ? api.resolveOptional('lifecycle.traffic', null) : null;
|
|
65
|
+
if (!port) return {};
|
|
66
|
+
const [moving, agents] = await Promise.all([
|
|
67
|
+
Promise.resolve().then(() => port.countMoving()).catch(() => null),
|
|
68
|
+
Promise.resolve().then(() => port.countAgentsAtWork()).catch(() => null),
|
|
69
|
+
]);
|
|
70
|
+
return { moving: moving && typeof moving.moving === 'number' ? moving.moving : null, agents_at_work: agents };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// GET /government/counts for this viewer: the docket they would read (the same
|
|
74
|
+
// per-source gates, the same seats) and the traffic, counted once.
|
|
75
|
+
async function countsFor(builder, deps = {}) {
|
|
76
|
+
const docket = deps.docket || require('./docket');
|
|
77
|
+
const [d, traffic] = await Promise.all([docket.docketFor(builder), (deps.readTraffic || readTraffic)()]);
|
|
78
|
+
return Object.assign({ viewer: { id: d && d.viewer ? d.viewer.id : null }, generated_at: d && d.generated_at ? d.generated_at : new Date().toISOString() }, countsOf(d, traffic));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { countsOf, countsFor };
|
package/modules/government/db.js
CHANGED
|
@@ -687,6 +687,238 @@ async function applyDecisionRules(client, { itemId, rules }) {
|
|
|
687
687
|
return written;
|
|
688
688
|
}
|
|
689
689
|
|
|
690
|
+
// --- standing orders: the project's laws (task 1004706, government_027) -----
|
|
691
|
+
|
|
692
|
+
// The law book. In force only by default, in the order they passed; with
|
|
693
|
+
// includeRepealed, every law ever passed. Each row carries the government it was
|
|
694
|
+
// decided under (the passing item's snapshotted constitution) and its proposer.
|
|
695
|
+
// Read per request, uncached — a law passed a moment ago reads back at once.
|
|
696
|
+
async function listLaws({ includeRepealed = false } = {}) {
|
|
697
|
+
const { rows } = await pool.query(
|
|
698
|
+
`SELECT l.id, l.text, l.effect_on_ai, l.locks, l.proposed_by, l.passed_by_item_id, l.passed_at,
|
|
699
|
+
l.repealed_by_item_id, l.repealed_at, i.constitution AS decided_under,
|
|
700
|
+
b.github_login AS proposer_login, b.display_name AS proposer_name
|
|
701
|
+
FROM government_laws l
|
|
702
|
+
JOIN government_board_items i ON i.id = l.passed_by_item_id
|
|
703
|
+
LEFT JOIN builders b ON b.id = l.proposed_by
|
|
704
|
+
WHERE $1::boolean OR l.repealed_at IS NULL
|
|
705
|
+
ORDER BY l.passed_at, l.id`,
|
|
706
|
+
[includeRepealed === true],
|
|
707
|
+
);
|
|
708
|
+
return rows;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Who said yes on these sittings — the "who passed it" half a law row points at.
|
|
712
|
+
// Open ballot (ADR 0175 §9), so nothing here is private. Oldest vote first.
|
|
713
|
+
async function listYesVoters(itemIds) {
|
|
714
|
+
if (!itemIds || !itemIds.length) return [];
|
|
715
|
+
const { rows } = await pool.query(
|
|
716
|
+
`SELECT v.item_id, v.voter_id, v.cast_at, b.github_login, b.display_name
|
|
717
|
+
FROM government_board_votes v
|
|
718
|
+
LEFT JOIN builders b ON b.id = v.voter_id
|
|
719
|
+
WHERE v.item_id = ANY($1::bigint[]) AND v.direction = 'yes'
|
|
720
|
+
ORDER BY v.cast_at, v.id`,
|
|
721
|
+
[itemIds],
|
|
722
|
+
);
|
|
723
|
+
return rows;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// THE ONE WRITE, and it runs only INSIDE the transaction that closes a laws
|
|
727
|
+
// amendment as passed (board.closeItem's afterClose — `client` is that
|
|
728
|
+
// transaction). Each statement writes nothing unless the named item is a passed
|
|
729
|
+
// laws amendment (the WHERE EXISTS), and government_027's trigger refuses any
|
|
730
|
+
// write that is not — so a law that skipped the board cannot land even through
|
|
731
|
+
// hand-written SQL. An enacted law that did not land throws, which rolls the
|
|
732
|
+
// close back (the sitting is retried, never passed-but-unapplied). A repeal of a
|
|
733
|
+
// law another sitting already repealed is not an error: it is already out of
|
|
734
|
+
// force, and the count says so.
|
|
735
|
+
async function applyLaws(client, { itemId, enact = [], repeal = [], proposedBy = null }) {
|
|
736
|
+
const enacted = [];
|
|
737
|
+
for (const law of enact) {
|
|
738
|
+
const { rows } = await client.query(
|
|
739
|
+
`INSERT INTO government_laws (text, effect_on_ai, locks, proposed_by, passed_by_item_id)
|
|
740
|
+
SELECT $1::text, $2::text, $3::jsonb, $4::bigint, $5::bigint
|
|
741
|
+
WHERE EXISTS (
|
|
742
|
+
SELECT 1 FROM government_board_items i
|
|
743
|
+
JOIN government_amendments a ON a.id = i.subject_id
|
|
744
|
+
WHERE i.id = $5::bigint AND i.subject_type = 'constitutional_amendment'
|
|
745
|
+
AND i.outcome = 'passed' AND a.kind = 'laws')
|
|
746
|
+
RETURNING id, text, effect_on_ai, locks, proposed_by, passed_by_item_id, passed_at`,
|
|
747
|
+
[law.text, law.effect_on_ai ?? null, JSON.stringify(law.locks || []), proposedBy, itemId],
|
|
748
|
+
);
|
|
749
|
+
if (rows.length !== 1) throw new Error(`law "${law.text}" not written (item ${itemId} is not a passed laws amendment)`);
|
|
750
|
+
enacted.push(rows[0]);
|
|
751
|
+
}
|
|
752
|
+
let repealed = [];
|
|
753
|
+
if (repeal.length) {
|
|
754
|
+
const { rows } = await client.query(
|
|
755
|
+
`UPDATE government_laws
|
|
756
|
+
SET repealed_by_item_id = $1::bigint, repealed_at = now()
|
|
757
|
+
WHERE id = ANY($2::bigint[]) AND repealed_at IS NULL
|
|
758
|
+
AND EXISTS (
|
|
759
|
+
SELECT 1 FROM government_board_items i
|
|
760
|
+
JOIN government_amendments a ON a.id = i.subject_id
|
|
761
|
+
WHERE i.id = $1::bigint AND i.subject_type = 'constitutional_amendment'
|
|
762
|
+
AND i.outcome = 'passed' AND a.kind = 'laws')
|
|
763
|
+
RETURNING id, repealed_at`,
|
|
764
|
+
[itemId, repeal],
|
|
765
|
+
);
|
|
766
|
+
repealed = rows;
|
|
767
|
+
}
|
|
768
|
+
return { enacted, repealed };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// --- the Lead of each building (task 1004709, government_029) -------------
|
|
772
|
+
|
|
773
|
+
// Every appointment, in office first by default. Each row carries the Lead's
|
|
774
|
+
// login and name. Read per request, uncached — an appointment carried out a
|
|
775
|
+
// moment ago reads back at once.
|
|
776
|
+
async function listLeads({ includePast = false } = {}) {
|
|
777
|
+
const { rows } = await pool.query(
|
|
778
|
+
`SELECT l.id, l.building, l.builder_id, l.charter, l.appointed_by_item_id, l.appointed_at,
|
|
779
|
+
l.removed_by_item_id, l.removed_at, b.github_login, b.display_name
|
|
780
|
+
FROM government_leads l
|
|
781
|
+
LEFT JOIN builders b ON b.id = l.builder_id
|
|
782
|
+
WHERE $1::boolean OR l.removed_at IS NULL
|
|
783
|
+
ORDER BY l.appointed_at, l.id`,
|
|
784
|
+
[includePast === true],
|
|
785
|
+
);
|
|
786
|
+
return rows;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Every active Archon (the founder's rank), oldest id first: the owner seat
|
|
790
|
+
// (leads.js ownerOf), the founding Archon first.
|
|
791
|
+
async function listActiveArchons() {
|
|
792
|
+
const { rows } = await pool.query(
|
|
793
|
+
`SELECT id, github_login, display_name
|
|
794
|
+
FROM builders
|
|
795
|
+
WHERE rank = 'archon' AND status = 'active' AND system_role IS NULL
|
|
796
|
+
ORDER BY id`,
|
|
797
|
+
);
|
|
798
|
+
return rows;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// One builder of this project, or null: who a Lead appointment names. Only an
|
|
802
|
+
// active person (no machine identity) can be appointed.
|
|
803
|
+
async function getActiveBuilder(builderId) {
|
|
804
|
+
const { rows } = await pool.query(
|
|
805
|
+
`SELECT id, github_login, display_name FROM builders
|
|
806
|
+
WHERE id = $1 AND status = 'active' AND system_role IS NULL`,
|
|
807
|
+
[builderId],
|
|
808
|
+
);
|
|
809
|
+
return rows[0] || null;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// THE ONE WRITE: carry out a PASSED People matter's Lead act, in one
|
|
813
|
+
// transaction. appoint_lead ends the building's Lead in office (if any) and
|
|
814
|
+
// seats the new one; remove_lead ends the Lead in office. Every statement names
|
|
815
|
+
// the passed item, and government_029's trigger refuses any write that item
|
|
816
|
+
// does not say — so a Lead that skipped the vote cannot land even through
|
|
817
|
+
// hand-written SQL. Carried out once: a second run of the same item seats
|
|
818
|
+
// nobody twice (the unique index on appointed_by_item_id) and says so.
|
|
819
|
+
// Returns { done, reason?, lead?, ended? }.
|
|
820
|
+
async function applyLeadAct({ itemId, action }) {
|
|
821
|
+
const client = await pool.connect();
|
|
822
|
+
try {
|
|
823
|
+
await client.query('BEGIN');
|
|
824
|
+
// the building's row of office, locked so two passed matters land one at a time
|
|
825
|
+
const { rows: current } = await client.query(
|
|
826
|
+
`SELECT id, builder_id, appointed_by_item_id FROM government_leads
|
|
827
|
+
WHERE building = $1 AND removed_at IS NULL
|
|
828
|
+
FOR UPDATE`,
|
|
829
|
+
[action.building],
|
|
830
|
+
);
|
|
831
|
+
const sitting = current[0] || null;
|
|
832
|
+
if (action.type === 'appoint_lead' && sitting && String(sitting.appointed_by_item_id) === String(itemId)) {
|
|
833
|
+
await client.query('ROLLBACK');
|
|
834
|
+
return { done: true, reason: 'already_carried_out' };
|
|
835
|
+
}
|
|
836
|
+
if (action.type === 'remove_lead' && !sitting) {
|
|
837
|
+
await client.query('ROLLBACK');
|
|
838
|
+
return { done: false, reason: 'no_lead' };
|
|
839
|
+
}
|
|
840
|
+
let ended = null;
|
|
841
|
+
if (sitting) {
|
|
842
|
+
const { rows } = await client.query(
|
|
843
|
+
`UPDATE government_leads SET removed_by_item_id = $1::bigint, removed_at = now()
|
|
844
|
+
WHERE id = $2 AND removed_at IS NULL
|
|
845
|
+
RETURNING id, builder_id, removed_at`,
|
|
846
|
+
[itemId, sitting.id],
|
|
847
|
+
);
|
|
848
|
+
ended = rows[0] || null;
|
|
849
|
+
}
|
|
850
|
+
let lead = null;
|
|
851
|
+
if (action.type === 'appoint_lead') {
|
|
852
|
+
const { rows } = await client.query(
|
|
853
|
+
`INSERT INTO government_leads (building, builder_id, charter, appointed_by_item_id)
|
|
854
|
+
VALUES ($1, $2::bigint, $3, $4::bigint)
|
|
855
|
+
ON CONFLICT (appointed_by_item_id) DO NOTHING
|
|
856
|
+
RETURNING id, building, builder_id, charter, appointed_by_item_id, appointed_at`,
|
|
857
|
+
[action.building, action.builder_id, action.charter, itemId],
|
|
858
|
+
);
|
|
859
|
+
if (!rows.length) {
|
|
860
|
+
await client.query('ROLLBACK');
|
|
861
|
+
return { done: true, reason: 'already_carried_out' };
|
|
862
|
+
}
|
|
863
|
+
lead = rows[0];
|
|
864
|
+
}
|
|
865
|
+
await client.query('COMMIT');
|
|
866
|
+
return { done: true, lead, ended };
|
|
867
|
+
} catch (err) {
|
|
868
|
+
await client.query('ROLLBACK').catch(() => {});
|
|
869
|
+
throw err;
|
|
870
|
+
} finally {
|
|
871
|
+
client.release();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// --- a Lead's build-outs (task 1004711, government_030) ---------------------
|
|
876
|
+
|
|
877
|
+
// Every build-out on the record, oldest first, each with who asked. Read per
|
|
878
|
+
// request, uncached: one built a moment ago reads back at once.
|
|
879
|
+
async function listBuildouts() {
|
|
880
|
+
const { rows } = await pool.query(
|
|
881
|
+
`SELECT o.id, o.building, o.capsule, o.name, o.line, o.built_by, o.authorised_by_item_id,
|
|
882
|
+
o.ground, o.why, o.started_at, b.github_login, b.display_name
|
|
883
|
+
FROM government_buildouts o
|
|
884
|
+
LEFT JOIN builders b ON b.id = o.built_by
|
|
885
|
+
ORDER BY o.started_at, o.id`,
|
|
886
|
+
);
|
|
887
|
+
return rows;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// The build-outs waiting on a vote: every OPEN sitting whose matter's action is
|
|
891
|
+
// a build_out, oldest first.
|
|
892
|
+
async function listPendingBuildouts() {
|
|
893
|
+
const { rows } = await pool.query(
|
|
894
|
+
`SELECT i.id AS item_id, m.id AS matter_id, m.action, i.opened_at, i.closes_at
|
|
895
|
+
FROM government_board_items i
|
|
896
|
+
JOIN government_matters m ON m.id = i.subject_id AND i.subject_type = m.area || '_matter'
|
|
897
|
+
WHERE i.closed_at IS NULL
|
|
898
|
+
AND m.action->>'type' = 'build_out'
|
|
899
|
+
ORDER BY i.opened_at, i.id`,
|
|
900
|
+
);
|
|
901
|
+
return rows;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// THE ONE WRITE: put a build-out on the record. Exactly one of `itemId` (a
|
|
905
|
+
// passed build_out matter: the vote path) or `ground` + `why` (the build-rights
|
|
906
|
+
// answer that let its holder build at once). government_030's trigger refuses a
|
|
907
|
+
// vote its matter did not say, an at-once district, and any later edit. A
|
|
908
|
+
// passed item builds once: a second run returns { done: true, reason:
|
|
909
|
+
// 'already_carried_out' }.
|
|
910
|
+
async function insertBuildout({ building, capsule, name, line = '', builtBy = null, itemId = null, ground = null, why = null }) {
|
|
911
|
+
const { rows } = await pool.query(
|
|
912
|
+
`INSERT INTO government_buildouts (building, capsule, name, line, built_by, authorised_by_item_id, ground, why)
|
|
913
|
+
VALUES ($1, $2, $3, $4, $5::bigint, $6::bigint, $7, $8)
|
|
914
|
+
ON CONFLICT (authorised_by_item_id) WHERE authorised_by_item_id IS NOT NULL DO NOTHING
|
|
915
|
+
RETURNING id, building, capsule, name, line, built_by, authorised_by_item_id, ground, why, started_at`,
|
|
916
|
+
[building, capsule, name, line, builtBy, itemId, ground, why],
|
|
917
|
+
);
|
|
918
|
+
if (!rows.length) return { done: true, reason: 'already_carried_out' };
|
|
919
|
+
return { done: true, buildout: rows[0] };
|
|
920
|
+
}
|
|
921
|
+
|
|
690
922
|
// A matter put to a building's sitting. Immutable once filed, like an amendment.
|
|
691
923
|
async function createMatter({ area, title, detailMd = null, action = null, proposedBy }) {
|
|
692
924
|
const { rows } = await pool.query(
|
|
@@ -955,6 +1187,16 @@ module.exports = {
|
|
|
955
1187
|
getAmendment,
|
|
956
1188
|
listDecisionRules,
|
|
957
1189
|
applyDecisionRules,
|
|
1190
|
+
listLaws,
|
|
1191
|
+
listYesVoters,
|
|
1192
|
+
applyLaws,
|
|
1193
|
+
listLeads,
|
|
1194
|
+
listActiveArchons,
|
|
1195
|
+
getActiveBuilder,
|
|
1196
|
+
applyLeadAct,
|
|
1197
|
+
listBuildouts,
|
|
1198
|
+
listPendingBuildouts,
|
|
1199
|
+
insertBuildout,
|
|
958
1200
|
createMatter,
|
|
959
1201
|
getMatter,
|
|
960
1202
|
getMatterSubjects,
|