@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,253 @@
|
|
|
1
|
+
// tests/government_build_rights.mjs — WHO MAY BUILD WITHOUT A VOTE comes from the
|
|
2
|
+
// FORM OF GOVERNMENT (task 1004710, goal 1000111; ADR 0375).
|
|
3
|
+
//
|
|
4
|
+
// What this pins (the task's done-when lines):
|
|
5
|
+
// 1. EACH FORM GIVES THE DOCUMENTED ANSWER. Monarchy: at once. Timocratic
|
|
6
|
+
// Republic: at once when the holder sits on the board, else a vote. Direct
|
|
7
|
+
// Democracy, Foundation, Custom: a vote. A district is always a vote. The
|
|
8
|
+
// form is READ FROM THE SETTINGS (charter.readCharter), so the real
|
|
9
|
+
// constitution decides, not a label. FORM_ANSWERS answers exactly the forms
|
|
10
|
+
// that can be in force, plus Custom; GOVERNMENT.md documents each.
|
|
11
|
+
// 2. THE RULE IS WRITTEN ONCE. build-rights.js is the only place it lives: the
|
|
12
|
+
// read (leads.js / decision-board.js) calls it, the hall reads its answer
|
|
13
|
+
// (tests/hall_street_deck.mjs), and no other file says "can_build_without_vote".
|
|
14
|
+
// 3. THE BUILDING'S OWN RULE and THE GOALS HOOK. A vacant building builds
|
|
15
|
+
// nothing; a sitting of its own decides ('decide'/'first' let a seated
|
|
16
|
+
// holder build at once, any other rule is a vote); a goal's limit beats the
|
|
17
|
+
// sitting and the form, a goal's grant beats only the form.
|
|
18
|
+
// 4. FAIL CLOSED. An unreadable decision row, constitution or member list reads
|
|
19
|
+
// as "a vote", never "at once".
|
|
20
|
+
// DB-free: db reads are monkeypatched by property.
|
|
21
|
+
//
|
|
22
|
+
// Run: node --test tests/government_build_rights.mjs
|
|
23
|
+
|
|
24
|
+
import { strict as assert } from 'node:assert';
|
|
25
|
+
import { test } from 'node:test';
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { createRequire } from 'node:module';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
34
|
+
const br = require(path.join(GOV, 'build-rights.js'));
|
|
35
|
+
const charter = require(path.join(GOV, 'charter.js'));
|
|
36
|
+
const dr = require(path.join(GOV, 'decision-rules.js'));
|
|
37
|
+
const leads = require(path.join(GOV, 'leads.js'));
|
|
38
|
+
const db = require(path.join(GOV, 'db.js'));
|
|
39
|
+
const config = require(path.join(GOV, 'config.js'));
|
|
40
|
+
const decisionBoard = require(path.join(GOV, 'decision-board.js'));
|
|
41
|
+
|
|
42
|
+
// Each available form's own board block, built by the real charter builder, so
|
|
43
|
+
// the form is read from settings exactly as a ratified charter would write them.
|
|
44
|
+
function boardOf(form) {
|
|
45
|
+
const r = charter.buildCharterProposal({ window_minutes: 1440, close_early_on_full_turnout: true }, { form, councilRank: 'trustees' });
|
|
46
|
+
assert.equal(r.ok, true, form);
|
|
47
|
+
assert.equal(charter.readCharter(r.proposal).form, form, form + ' reads as itself');
|
|
48
|
+
return r.proposal;
|
|
49
|
+
}
|
|
50
|
+
const asToday = (area, board = {}) => dr.resolveRow(dr.defaultRow(area), { board });
|
|
51
|
+
const sitting = (area, decidedBy, rule) => dr.resolveRow({ area, decided_by: decidedBy, rule, who: null }, {});
|
|
52
|
+
const rights = (board, holder, { building = 'planning', decision = asToday(building, board), goals = [] } = {}) =>
|
|
53
|
+
br.buildRightsFor({ building, board, decision, holder: { seat: 'lead:' + building, ...holder }, goals });
|
|
54
|
+
const atOnce = (r) => Object.fromEntries(Object.entries(r.capsules).map(([k, v]) => [k, v.without_vote]));
|
|
55
|
+
const ALL = { committee: true, floor: true, annex: true, district: false };
|
|
56
|
+
const NONE = { committee: false, floor: false, annex: false, district: false };
|
|
57
|
+
|
|
58
|
+
// ── 1. each form gives the documented answer ────────────────────────────────
|
|
59
|
+
|
|
60
|
+
test('Monarchy / BDFL: whoever holds the building builds it out at once, sitting on the board or not', () => {
|
|
61
|
+
const b = boardOf('monarchy');
|
|
62
|
+
for (const onBoard of [true, false]) {
|
|
63
|
+
const r = rights(b, { on_board: onBoard });
|
|
64
|
+
assert.equal(r.form, 'monarchy');
|
|
65
|
+
assert.equal(r.form_name, 'Monarchy / BDFL');
|
|
66
|
+
assert.deepEqual(atOnce(r), ALL);
|
|
67
|
+
assert.equal(r.capsules.floor.ground, 'form');
|
|
68
|
+
assert.equal(r.capsules.floor.why, 'The founder decides alone, so whoever holds this building builds it out at once.');
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('Timocratic Republic: at once when the holder has earned a vote (sits on the board); otherwise a vote', () => {
|
|
73
|
+
const b = boardOf('timocratic-republic');
|
|
74
|
+
assert.deepEqual(atOnce(rights(b, { on_board: true })), ALL);
|
|
75
|
+
const no = rights(b, { on_board: false });
|
|
76
|
+
assert.deepEqual(atOnce(no), NONE);
|
|
77
|
+
assert.equal(no.capsules.annex.why, 'Its holder has not earned a vote yet, so the board votes on it.');
|
|
78
|
+
assert.equal(no.capsules.annex.ground, 'form');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('Direct Democracy, Foundation and Custom: always a vote, whoever holds it', () => {
|
|
82
|
+
const custom = { membership: 'rank:metic+', pass_rule: 'unanimous', window_minutes: 1440 };
|
|
83
|
+
assert.equal(charter.readCharter(custom).form, 'custom');
|
|
84
|
+
const cases = [
|
|
85
|
+
[boardOf('direct-democracy'), 'direct-democracy', 'Every builder decides changes together, so it goes to a vote.'],
|
|
86
|
+
[boardOf('foundation'), 'foundation', 'The council of trustees decides changes, so it goes to a vote.'],
|
|
87
|
+
[custom, 'custom', 'These settings match no named form, so a change goes to a vote.'],
|
|
88
|
+
];
|
|
89
|
+
for (const [board, form, why] of cases) {
|
|
90
|
+
for (const onBoard of [true, false]) {
|
|
91
|
+
const r = rights(board, { on_board: onBoard });
|
|
92
|
+
assert.equal(r.form, form);
|
|
93
|
+
assert.deepEqual(atOnce(r), NONE, form);
|
|
94
|
+
assert.equal(r.capsules.committee.why, why);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('a district is a new part of the city: always a vote, under every form and every goal', () => {
|
|
100
|
+
const grantAll = [{ goal_id: '1', building: '*', allow: true }];
|
|
101
|
+
for (const f of charter.FORMS.filter((x) => x.available)) {
|
|
102
|
+
const r = rights(boardOf(f.key), { on_board: true }, { goals: grantAll });
|
|
103
|
+
assert.deepEqual(r.capsules.district, { without_vote: false, why: 'A district is a new part of the city, so the board votes on it.', ground: 'city' }, f.key);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('FORM_ANSWERS answers exactly the forms that can be in force, plus Custom; the documentation names each', () => {
|
|
108
|
+
const available = charter.FORMS.filter((f) => f.available).map((f) => f.key);
|
|
109
|
+
assert.deepEqual(Object.keys(br.FORM_ANSWERS).sort(), [...available, 'custom'].sort());
|
|
110
|
+
for (const f of charter.FORMS.filter((x) => !x.available)) assert.equal(br.FORM_ANSWERS[f.key], undefined, f.key + ' can never be in force');
|
|
111
|
+
const doc = fs.readFileSync(path.join(ROOT, 'GOVERNMENT.md'), 'utf8');
|
|
112
|
+
const section = doc.slice(doc.indexOf('## Who may build without a vote'));
|
|
113
|
+
assert.ok(section.length > 30, 'GOVERNMENT.md has the section');
|
|
114
|
+
for (const f of charter.FORMS.filter((x) => x.available)) assert.ok(section.includes(f.name), 'documents ' + f.name);
|
|
115
|
+
assert.ok(section.includes('Custom'));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('an unknown form reads as Custom: a vote', () => {
|
|
119
|
+
const r = br.buildRight({ building: 'planning', capsule: 'floor', form: 'presidential', decision: asToday('planning'), holder: { on_board: true } });
|
|
120
|
+
assert.equal(r.without_vote, false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ── 3. the building's own rule, and the goals hook ──────────────────────────
|
|
124
|
+
|
|
125
|
+
test('a vacant building builds nothing, under any form', () => {
|
|
126
|
+
const vacant = dr.resolveRow({ area: 'planning', decided_by: 'nobody', rule: 'vacant', who: null }, {});
|
|
127
|
+
const r = rights(boardOf('monarchy'), { on_board: true }, { decision: vacant });
|
|
128
|
+
assert.deepEqual(atOnce(r), NONE);
|
|
129
|
+
assert.equal(r.capsules.floor.ground, 'building');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('a sitting of its own: decide/first lets a seated holder build at once; consent/majority/unanimous/sign-off is a vote', () => {
|
|
133
|
+
const dd = boardOf('direct-democracy');
|
|
134
|
+
for (const rule of ['decide', 'first']) {
|
|
135
|
+
const d = sitting('planning', 'rank:council', rule);
|
|
136
|
+
const yes = rights(dd, { on_board: false, in_sitting: true }, { decision: d });
|
|
137
|
+
assert.deepEqual(atOnce(yes), ALL, rule + ': seated, so at once even under Direct Democracy');
|
|
138
|
+
assert.equal(yes.capsules.floor.ground, 'building');
|
|
139
|
+
assert.match(yes.capsules.floor.why, /^Planning is decided by .+, and its holder is one of them, so they build it out at once\.$/);
|
|
140
|
+
assert.deepEqual(atOnce(rights(dd, { on_board: true, in_sitting: false }, { decision: d })), NONE, rule + ': not seated there');
|
|
141
|
+
}
|
|
142
|
+
const mon = boardOf('monarchy');
|
|
143
|
+
for (const rule of ['consent', 'majority', 'unanimous', 'signoff']) {
|
|
144
|
+
const r = rights(mon, { on_board: true, in_sitting: true }, { decision: sitting('planning', 'rank:council', rule) });
|
|
145
|
+
assert.deepEqual(atOnce(r), NONE, rule + ': its sitting votes, even under Monarchy');
|
|
146
|
+
assert.match(r.capsules.annex.why, /so it goes to them as a vote\.$/);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('the goals hook: a limit beats the sitting and the form; a grant beats only the form', () => {
|
|
151
|
+
const mon = boardOf('monarchy');
|
|
152
|
+
const dd = boardOf('direct-democracy');
|
|
153
|
+
const limit = { goal_id: '7', building: 'planning', capsules: ['annex'], allow: false, why: 'Goal 7 keeps Planning as it is until launch.' };
|
|
154
|
+
const r1 = rights(mon, { on_board: true }, { goals: [limit] });
|
|
155
|
+
assert.deepEqual(atOnce(r1), { committee: true, floor: true, annex: false, district: false });
|
|
156
|
+
assert.deepEqual(r1.capsules.annex, { without_vote: false, why: limit.why, ground: 'goal' });
|
|
157
|
+
const seatedDecide = sitting('planning', 'rank:council', 'decide');
|
|
158
|
+
assert.equal(rights(dd, { in_sitting: true }, { decision: seatedDecide, goals: [{ ...limit, capsules: [] }] }).capsules.floor.without_vote, false, 'a limit beats the building\'s own sitting');
|
|
159
|
+
const grant = { goal_id: '8', building: '*', allow: true };
|
|
160
|
+
const r2 = rights(dd, { on_board: false }, { goals: [grant] });
|
|
161
|
+
assert.deepEqual(atOnce(r2), ALL);
|
|
162
|
+
assert.equal(r2.capsules.floor.ground, 'goal');
|
|
163
|
+
assert.equal(rights(dd, {}, { goals: [{ ...grant, building: 'release' }] }).capsules.floor.without_vote, false, 'a grant for another building');
|
|
164
|
+
assert.equal(rights(mon, { in_sitting: false }, { decision: sitting('planning', 'rank:council', 'majority'), goals: [grant] }).capsules.floor.without_vote, false, 'a grant never beats a sitting the project voted in');
|
|
165
|
+
const vacant = dr.resolveRow({ area: 'planning', decided_by: 'nobody', rule: 'vacant', who: null }, {});
|
|
166
|
+
assert.equal(rights(dd, {}, { decision: vacant, goals: [grant] }).capsules.floor.without_vote, false, 'nor a vacant building');
|
|
167
|
+
assert.equal(rights(dd, {}, { goals: [{ goal_id: '9', building: 'planning', allow: 'yes' }] }).capsules.floor.without_vote, false, 'a malformed term is ignored');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ── 4. fail closed ──────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
test('fail closed: no decision row, or no board, reads as a vote', () => {
|
|
173
|
+
const r = br.buildRightsFor({ building: 'planning', board: boardOf('monarchy'), decision: null, holder: { on_board: true } });
|
|
174
|
+
assert.deepEqual(atOnce(r), NONE);
|
|
175
|
+
assert.equal(br.buildRightsFor({ building: 'planning', board: null, decision: asToday('planning'), holder: { on_board: true } }).capsules.floor.without_vote, false);
|
|
176
|
+
assert.equal(br.buildRight({ building: 'moon', capsule: 'floor', form: 'monarchy', decision: asToday('planning') }).without_vote, false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ── 2. the read carries it, resolved on the server ───────────────────────────
|
|
180
|
+
|
|
181
|
+
const ARCHONS = [{ id: '3', github_login: 'ada', display_name: 'Ada' }];
|
|
182
|
+
const leadRow = { id: '1', building: 'planning', builder_id: '45', charter: 'Goals.', appointed_by_item_id: '40', appointed_at: 'T', removed_by_item_id: null, removed_at: null, github_login: 'dee', display_name: 'Dee' };
|
|
183
|
+
|
|
184
|
+
test('leadsView carries each building\'s build_rights for its HOLDER: the Lead where seated, the owner seat elsewhere', () => {
|
|
185
|
+
const board = boardOf('timocratic-republic');
|
|
186
|
+
const decisions = dr.resolveAll([], { board });
|
|
187
|
+
const v = leads.leadsView([leadRow], ARCHONS, { rights: { board, decisions, boardMemberIds: ['3'], sittingMemberIds: {}, goals: [] } });
|
|
188
|
+
const at = (k) => v.buildings.find((b) => b.building === k).build_rights;
|
|
189
|
+
assert.equal(at('planning').holder, 'lead:planning');
|
|
190
|
+
assert.equal(at('planning').capsules.floor.without_vote, false, 'Dee (45) has not earned a vote');
|
|
191
|
+
assert.equal(at('release').holder, 'owner');
|
|
192
|
+
assert.equal(at('release').capsules.floor.without_vote, true, 'the owner seat sits on the board');
|
|
193
|
+
assert.match(v.build_rights_from, /form of government/);
|
|
194
|
+
assert.equal(leads.leadsView([leadRow], ARCHONS).buildings[0].build_rights, null, 'no context, no answer');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
async function withStubs(stubs, fn) {
|
|
198
|
+
const saved = [];
|
|
199
|
+
for (const [obj, k, v] of stubs) { saved.push([obj, k, obj[k]]); obj[k] = v; }
|
|
200
|
+
try { return await fn(); } finally { for (const [obj, k, v] of saved) obj[k] = v; }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
test('the server resolves the context per request: the constitution in force, the table, who the board and each sitting seat', async () => {
|
|
204
|
+
const board = boardOf('timocratic-republic');
|
|
205
|
+
const asked = [];
|
|
206
|
+
await withStubs([
|
|
207
|
+
[db, 'listLeads', async () => [leadRow]],
|
|
208
|
+
[db, 'listActiveArchons', async () => ARCHONS],
|
|
209
|
+
[db, 'listDecisionRules', async () => [{ area: 'review', decided_by: 'rank:testers', rule: 'first', who: null, set_by_item_id: '2' }]],
|
|
210
|
+
[db, 'getActiveBuildersWithRank', async (keys) => { asked.push(keys.join(',')); return keys.includes('testers') ? [{ builder_id: '3' }] : [{ builder_id: '45' }, { builder_id: '3' }]; }],
|
|
211
|
+
[config, 'loadGovernmentConfig', () => ({ board })],
|
|
212
|
+
], async () => {
|
|
213
|
+
const v = await decisionBoard.leadsView();
|
|
214
|
+
const at = (k) => v.buildings.find((b) => b.building === k).build_rights;
|
|
215
|
+
assert.equal(at('planning').form, 'timocratic-republic');
|
|
216
|
+
assert.equal(at('planning').capsules.floor.without_vote, true, 'Dee sits on the metic+ board');
|
|
217
|
+
assert.equal(at('review').capsules.floor.ground, 'building');
|
|
218
|
+
assert.equal(at('review').capsules.floor.without_vote, true, 'the owner sits on Review\'s first-yes sitting');
|
|
219
|
+
assert.ok(asked.includes('testers'), 'the first-yes sitting was asked who it seats');
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('the server fails closed: an unreadable table or member list reads as a vote', async () => {
|
|
224
|
+
await withStubs([
|
|
225
|
+
[db, 'listLeads', async () => [leadRow]],
|
|
226
|
+
[db, 'listActiveArchons', async () => ARCHONS],
|
|
227
|
+
[db, 'listDecisionRules', async () => { throw new Error('down'); }],
|
|
228
|
+
[db, 'getActiveBuildersWithRank', async () => { throw new Error('down'); }],
|
|
229
|
+
[config, 'loadGovernmentConfig', () => ({ board: boardOf('monarchy') })],
|
|
230
|
+
], async () => {
|
|
231
|
+
const v = await decisionBoard.leadsView();
|
|
232
|
+
for (const b of v.buildings) assert.equal(b.build_rights.capsules.committee.without_vote, false, b.building);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// ── 2. written once ─────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
test('written once: no other file holds a build rule; the per-Lead flag is gone from the code and the overlay', () => {
|
|
239
|
+
const walk = (dir, out = []) => {
|
|
240
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
241
|
+
const p = path.join(dir, e.name);
|
|
242
|
+
if (e.isDirectory()) { if (e.name !== 'node_modules') walk(p, out); } else if (/\.(js|json)$/.test(e.name)) out.push(p);
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
};
|
|
246
|
+
const files = [...walk(path.join(ROOT, 'modules')), ...walk(path.join(ROOT, 'src')), ...walk(path.join(ROOT, 'scripts', 'hall-preview', 'fixtures'))];
|
|
247
|
+
const flag = files.filter((f) => fs.readFileSync(f, 'utf8').includes('can_build_without_vote') && !f.endsWith('build-rights.js'));
|
|
248
|
+
assert.deepEqual(flag.map((f) => path.relative(ROOT, f)), []);
|
|
249
|
+
const callers = files.filter((f) => /require\([^)]*build-rights/.test(fs.readFileSync(f, 'utf8')));
|
|
250
|
+
// leads.js asks the rule; buildouts.js (task 1004711) takes only its vocabulary, the four capsules
|
|
251
|
+
assert.deepEqual(callers.map((f) => path.relative(ROOT, f).replace(/\\/g, '/')), ['modules/government/buildouts.js', 'modules/government/leads.js']);
|
|
252
|
+
assert.match(fs.readFileSync(path.join(ROOT, 'modules', 'government', 'buildouts.js'), 'utf8'), /const \{ CAPSULES \} = require\('\.\/build-rights'\);/, 'buildouts.js reads the capsules, never a rule');
|
|
253
|
+
});
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// tests/government_buildouts.mjs — A LEAD'S BUILD-OUTS: a committee room, a new
|
|
2
|
+
// floor, an annex, or growing a building into a district, as data (task 1004711,
|
|
3
|
+
// goal 1000111; ADR 0378).
|
|
4
|
+
//
|
|
5
|
+
// What this pins (the task's done-when lines, and the walls behind them):
|
|
6
|
+
// 1. STRUCTURE IS DATA. A build-out is a row of government_030 (building,
|
|
7
|
+
// capsule, name, line, who asked, how it was allowed, when it started);
|
|
8
|
+
// GET /government/leads carries each building's `structure`: what stands
|
|
9
|
+
// (going up for a day, then built) and what waits on a vote, with the
|
|
10
|
+
// docket id its tally rides on.
|
|
11
|
+
// 2. BOTH PATHS WORK. POST /government/buildouts: only the building's holder
|
|
12
|
+
// may ask; where build_rights (build-rights.js, never a second rule) says
|
|
13
|
+
// without_vote it is on the record at once with the rule's ground; otherwise
|
|
14
|
+
// it is filed as a build_out matter through openMatter, the board matters
|
|
15
|
+
// path — under the board where the building has no sitting, for every
|
|
16
|
+
// district and for the Charter; at the building's sitting otherwise. A
|
|
17
|
+
// passed matter puts it on the record naming the item; a returned one
|
|
18
|
+
// builds nothing.
|
|
19
|
+
// 3. THE WALLS. One SQL writer (db.insertBuildout), called only from
|
|
20
|
+
// decision-board.js; the trigger refuses a vote its matter did not say, an
|
|
21
|
+
// at-once district, every edit and every delete.
|
|
22
|
+
// The proposal picture drawn from the real building is pinned in
|
|
23
|
+
// tests/hall_street_deck.mjs; the city's site and ghost in tests/hall_gov_living.mjs.
|
|
24
|
+
// DB-free: db reads/writes are monkeypatched by property.
|
|
25
|
+
//
|
|
26
|
+
// Run: node --test tests/government_buildouts.mjs
|
|
27
|
+
|
|
28
|
+
import { strict as assert } from 'node:assert';
|
|
29
|
+
import { test, after } from 'node:test';
|
|
30
|
+
import http from 'node:http';
|
|
31
|
+
import fs from 'node:fs';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import { createRequire } from 'node:module';
|
|
34
|
+
import { fileURLToPath } from 'node:url';
|
|
35
|
+
|
|
36
|
+
const require = createRequire(import.meta.url);
|
|
37
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
38
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
39
|
+
const bo = require(path.join(GOV, 'buildouts.js'));
|
|
40
|
+
const leads = require(path.join(GOV, 'leads.js'));
|
|
41
|
+
const charter = require(path.join(GOV, 'charter.js'));
|
|
42
|
+
const decisionBoard = require(path.join(GOV, 'decision-board.js'));
|
|
43
|
+
const board = require(path.join(GOV, 'board.js'));
|
|
44
|
+
const config = require(path.join(GOV, 'config.js'));
|
|
45
|
+
const db = require(path.join(GOV, 'db.js'));
|
|
46
|
+
|
|
47
|
+
const MIGRATION = fs.readFileSync(path.join(GOV, 'migrations', 'government_030_buildouts.sql'), 'utf8');
|
|
48
|
+
const MIGRATION_CODE = MIGRATION.replace(/--[^\n]*/g, '');
|
|
49
|
+
|
|
50
|
+
const ARCHONS = [{ id: '3', github_login: 'ada', display_name: 'Ada' }];
|
|
51
|
+
const DEE_LEAD = {
|
|
52
|
+
id: '1', building: 'planning', builder_id: '45', charter: 'Goals, priorities and the order of work.', appointed_by_item_id: '40',
|
|
53
|
+
appointed_at: '2026-10-09T12:00:00.000Z', removed_by_item_id: null, removed_at: null, github_login: 'dee', display_name: 'Dee',
|
|
54
|
+
};
|
|
55
|
+
function boardOf(form) {
|
|
56
|
+
const r = charter.buildCharterProposal({ window_minutes: 1440, close_early_on_full_turnout: true }, { form, councilRank: 'trustees' });
|
|
57
|
+
assert.equal(r.ok, true, form);
|
|
58
|
+
return r.proposal;
|
|
59
|
+
}
|
|
60
|
+
async function withStubs(stubs, fn) {
|
|
61
|
+
const saved = [];
|
|
62
|
+
for (const [obj, k, v] of stubs) { saved.push([obj, k, obj[k]]); obj[k] = v; }
|
|
63
|
+
try { return await fn(); } finally { for (const [obj, k, v] of saved.reverse()) obj[k] = v; }
|
|
64
|
+
}
|
|
65
|
+
const never = async () => { throw new Error('must not be reached'); };
|
|
66
|
+
// a project under `form`, Dee leading Planning, nothing built yet; `writes` collects every write
|
|
67
|
+
function project(form, { rules = [], standing = [], pending = [], writes = [] } = {}) {
|
|
68
|
+
return [
|
|
69
|
+
[config, 'loadGovernmentConfig', () => ({ board: boardOf(form) })],
|
|
70
|
+
[db, 'listLeads', async () => [DEE_LEAD]],
|
|
71
|
+
[db, 'listActiveArchons', async () => ARCHONS],
|
|
72
|
+
[db, 'listDecisionRules', async () => rules],
|
|
73
|
+
[db, 'getActiveBuildersWithRank', async () => [{ builder_id: '3' }]],
|
|
74
|
+
[db, 'listBuildouts', async () => standing],
|
|
75
|
+
[db, 'listPendingBuildouts', async () => pending],
|
|
76
|
+
[db, 'insertBuildout', async (a) => { writes.push(['insert', a]); return { done: true, buildout: { id: '7', ...a } }; }],
|
|
77
|
+
[db, 'createMatter', async (a) => { writes.push(['matter', a]); return { id: '8', ...a }; }],
|
|
78
|
+
[db, 'openBoardItem', async (a) => { writes.push(['item', a]); return { id: '50', subject_type: a.subjectType, subject_id: a.subjectId, constitution: a.constitution }; }],
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── 1. the shape ────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
test('a build-out is one of the four capsules at one of the nine buildings, with a short name and one line', () => {
|
|
85
|
+
assert.deepEqual(bo.checkBuildOut({ building: 'planning', capsule: 'floor', name: ' Road-map floor ', line: 'Where the next quarter is planned.' }).buildout,
|
|
86
|
+
{ type: 'build_out', building: 'planning', capsule: 'floor', name: 'Road-map floor', line: 'Where the next quarter is planned.' });
|
|
87
|
+
assert.deepEqual(bo.checkBuildOut({ building: 'review', capsule: 'committee' }).buildout,
|
|
88
|
+
{ type: 'build_out', building: 'review', capsule: 'committee', name: bo.DEFAULT_NAMES.committee, line: '' }, 'no name: the capsule\'s own');
|
|
89
|
+
const refused = [
|
|
90
|
+
[{ building: 'kitchen', capsule: 'floor' }, 'building'],
|
|
91
|
+
[{ building: 'planning', capsule: 'tower' }, 'capsule'],
|
|
92
|
+
[{ building: 'planning', capsule: 'floor', name: 'x' }, 'name'],
|
|
93
|
+
[{ building: 'planning', capsule: 'floor', name: 'x'.repeat(bo.NAME_MAX + 1) }, 'name'],
|
|
94
|
+
[{ building: 'planning', capsule: 'floor', line: 'two\nlines' }, 'line'],
|
|
95
|
+
[{ building: 'planning', capsule: 'floor', line: 'x'.repeat(bo.LINE_MAX + 1) }, 'line'],
|
|
96
|
+
[{ building: 'planning', capsule: 'floor', without_vote: true }, 'without_vote'],
|
|
97
|
+
];
|
|
98
|
+
for (const [raw, field] of refused) assert.equal(bo.checkBuildOut(raw).field, field, JSON.stringify(raw));
|
|
99
|
+
assert.equal(bo.matterAreaFor('release'), 'release');
|
|
100
|
+
assert.equal(bo.matterAreaFor('charter'), 'people', 'the Charter takes no matters: its build-outs are People matters');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('build_out is a matter action at the building it adds to, and nowhere else', () => {
|
|
104
|
+
const v = (area, a) => decisionBoard.validateMatterAction(area, a);
|
|
105
|
+
assert.equal(v('release', { type: 'build_out', building: 'release', capsule: 'annex', name: 'Staging annex' }).ok, true, 'at a building whose module acts nothing else');
|
|
106
|
+
assert.equal(v('rules', { type: 'build_out', building: 'rules', capsule: 'floor' }).ok, true, 'Rules has no other matter acts');
|
|
107
|
+
assert.equal(v('people', { type: 'build_out', building: 'charter', capsule: 'committee' }).ok, true);
|
|
108
|
+
assert.equal(v('planning', { type: 'build_out', building: 'release', capsule: 'floor' }).field, 'action.building');
|
|
109
|
+
assert.equal(v('planning', { type: 'build_out', building: 'planning', capsule: 'floor', ground: 'form' }).reason, 'unknown_field');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('progress is a real clock: a day from the start, then built', () => {
|
|
113
|
+
const t0 = Date.parse('2026-10-09T00:00:00.000Z');
|
|
114
|
+
const row = { started_at: '2026-10-09T00:00:00.000Z' };
|
|
115
|
+
assert.equal(bo.progressOf(row, t0), 0);
|
|
116
|
+
assert.equal(bo.progressOf(row, t0 + bo.BUILD_MS / 2), 0.5);
|
|
117
|
+
assert.equal(bo.progressOf(row, t0 + bo.BUILD_MS * 3), 1);
|
|
118
|
+
assert.equal(bo.BUILD_HOURS, 24);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ── 1. the read ─────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
test('GET /government/leads carries each building\'s structure: what stands, going up or built, and what waits on a vote', () => {
|
|
124
|
+
const now = Date.parse('2026-10-10T06:00:00.000Z');
|
|
125
|
+
const standing = [
|
|
126
|
+
{ id: '1', building: 'release', capsule: 'floor', name: 'Staging floor', line: 'Try each release first.', built_by: '3', authorised_by_item_id: null, ground: 'form', why: 'The founder decides alone.', started_at: '2026-10-10T00:00:00.000Z', github_login: 'ada', display_name: 'Ada' },
|
|
127
|
+
{ id: '2', building: 'review', capsule: 'committee', name: 'Copy room', line: '', built_by: '3', authorised_by_item_id: '44', ground: null, why: null, started_at: '2026-10-01T00:00:00.000Z' },
|
|
128
|
+
];
|
|
129
|
+
const pending = [{ item_id: '51', matter_id: '9', action: { type: 'build_out', building: 'treasury', capsule: 'district', name: 'Reserve quarter', line: '' }, opened_at: 'T', closes_at: null }];
|
|
130
|
+
const v = leads.leadsView([DEE_LEAD], ARCHONS, { structure: { rows: standing, pending, now } });
|
|
131
|
+
const at = (k) => v.buildings.find((b) => b.building === k).structure;
|
|
132
|
+
assert.deepEqual(at('release').standing[0], {
|
|
133
|
+
id: '1', building: 'release', capsule: 'floor', name: 'Staging floor', line: 'Try each release first.', state: 'building', progress: 0.25,
|
|
134
|
+
started_at: '2026-10-10T00:00:00.000Z', builder: { id: '3', login: 'ada', name: 'Ada' },
|
|
135
|
+
allowed: { path: 'at_once', ground: 'form', why: 'The founder decides alone.' },
|
|
136
|
+
});
|
|
137
|
+
assert.equal(at('review').standing[0].state, 'built');
|
|
138
|
+
assert.deepEqual(at('review').standing[0].allowed, { path: 'vote', item_id: '44', href: '/board-room?item=44' });
|
|
139
|
+
assert.deepEqual(at('treasury').proposed, [{ item_id: '51', matter_id: '9', docket_id: 'board:51', building: 'treasury', capsule: 'district', name: 'Reserve quarter', line: '', opened_at: 'T', closes_at: null, href: '/board-room?item=51' }]);
|
|
140
|
+
assert.deepEqual(at('planning'), { standing: [], proposed: [] });
|
|
141
|
+
assert.equal(leads.leadsView([DEE_LEAD], ARCHONS).buildings[0].structure, null, 'not read: null, never invented');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('the server read: a failed build-out read leaves structure out, the rest still answers', async () => {
|
|
145
|
+
await withStubs([...project('monarchy'), [db, 'listBuildouts', async () => { throw new Error('down'); }]], async () => {
|
|
146
|
+
const v = await decisionBoard.leadsView();
|
|
147
|
+
assert.equal(v.buildings.length, 9);
|
|
148
|
+
assert.equal(v.buildings[0].structure, null);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ── 2. the two paths ────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
test('at once: where the form of government lets the holder build, the build-out is on the record now with the rule\'s ground', async () => {
|
|
155
|
+
const writes = [];
|
|
156
|
+
await withStubs(project('monarchy', { writes }), async () => {
|
|
157
|
+
const out = await board.buildOut({ buildout: { building: 'planning', capsule: 'floor', name: 'Road-map floor' }, builderId: '45' });
|
|
158
|
+
assert.equal(out.ok, true, JSON.stringify(out));
|
|
159
|
+
assert.equal(out.path, 'at_once');
|
|
160
|
+
assert.equal(out.rights.ground, 'form');
|
|
161
|
+
});
|
|
162
|
+
assert.equal(writes.length, 1);
|
|
163
|
+
const [kind, a] = writes[0];
|
|
164
|
+
assert.equal(kind, 'insert');
|
|
165
|
+
assert.deepEqual(a, { building: 'planning', capsule: 'floor', name: 'Road-map floor', line: '', builtBy: '45', ground: 'form', why: a.why });
|
|
166
|
+
assert.match(a.why, /founder decides alone/);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('by vote: where it does not, the build-out is filed as a matter through the board matters path, and nothing is built', async () => {
|
|
170
|
+
const writes = [];
|
|
171
|
+
await withStubs(project('direct-democracy', { writes }), async () => {
|
|
172
|
+
const out = await board.buildOut({ buildout: { building: 'planning', capsule: 'annex', name: 'Research annex', line: 'Spikes live here.' }, builderId: '45' });
|
|
173
|
+
assert.equal(out.ok, true, JSON.stringify(out));
|
|
174
|
+
assert.equal(out.path, 'vote');
|
|
175
|
+
assert.equal(out.rights.without_vote, false);
|
|
176
|
+
assert.equal(out.decided.sat_under, 'board', 'Planning has no sitting of its own: the board in force decides');
|
|
177
|
+
});
|
|
178
|
+
assert.deepEqual(writes.map((w) => w[0]), ['matter', 'item'], 'no build-out written');
|
|
179
|
+
assert.equal(writes[0][1].area, 'planning');
|
|
180
|
+
assert.deepEqual(writes[0][1].action, { type: 'build_out', building: 'planning', capsule: 'annex', name: 'Research annex', line: 'Spikes live here.' });
|
|
181
|
+
assert.equal(writes[0][1].title, 'Add an annex to Planning: Research annex');
|
|
182
|
+
assert.equal(writes[1][1].subjectType, 'planning_matter');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('a district always goes to the board, even under a monarchy; the Charter\'s go to the board as People matters', async () => {
|
|
186
|
+
const writes = [];
|
|
187
|
+
await withStubs(project('monarchy', { writes }), async () => {
|
|
188
|
+
const d = await board.buildOut({ buildout: { building: 'planning', capsule: 'district' }, builderId: '45' });
|
|
189
|
+
assert.equal(d.path, 'vote');
|
|
190
|
+
assert.equal(d.rights.ground, 'city');
|
|
191
|
+
});
|
|
192
|
+
assert.deepEqual(writes.map((w) => w[0]), ['matter', 'item']);
|
|
193
|
+
const writes2 = [];
|
|
194
|
+
await withStubs(project('direct-democracy', { writes: writes2 }), async () => {
|
|
195
|
+
const c = await board.buildOut({ buildout: { building: 'charter', capsule: 'committee', name: 'Drafting room' }, builderId: '3' });
|
|
196
|
+
assert.equal(c.path, 'vote', JSON.stringify(c));
|
|
197
|
+
});
|
|
198
|
+
assert.equal(writes2[0][1].area, 'people');
|
|
199
|
+
assert.equal(writes2[1][1].subjectType, 'people_matter');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('a building with a sitting of its own: the vote sits there; a district still goes to the board', async () => {
|
|
203
|
+
const rules = [{ area: 'release', decided_by: 'rank:release-council', rule: 'consent', who: null, set_by_item_id: '2' }];
|
|
204
|
+
const writes = [];
|
|
205
|
+
await withStubs(project('monarchy', { rules, writes }), async () => {
|
|
206
|
+
const f = await board.buildOut({ buildout: { building: 'release', capsule: 'floor' }, builderId: '3' });
|
|
207
|
+
assert.equal(f.path, 'vote', JSON.stringify(f));
|
|
208
|
+
assert.equal(f.rights.ground, 'building');
|
|
209
|
+
assert.notEqual(f.decided.sat_under, 'board', 'it sits at Release\'s own council');
|
|
210
|
+
assert.equal(writes[1][1].constitution.membership, 'rank:release-council');
|
|
211
|
+
const d = await board.buildOut({ buildout: { building: 'release', capsule: 'district' }, builderId: '3' });
|
|
212
|
+
assert.equal(d.decided.sat_under, 'board');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('only the holder may ask: the Lead where seated, the owner seat elsewhere; nobody else, and nothing is written', async () => {
|
|
217
|
+
const writes = [];
|
|
218
|
+
await withStubs(project('monarchy', { writes }), async () => {
|
|
219
|
+
const stranger = await board.buildOut({ buildout: { building: 'planning', capsule: 'floor' }, builderId: '3' });
|
|
220
|
+
assert.equal(stranger.reason, 'not_the_holder', 'the owner does not build in a building its Lead holds');
|
|
221
|
+
assert.match(stranger.what_this_means, /led by Dee/);
|
|
222
|
+
const lead = await board.buildOut({ buildout: { building: 'release', capsule: 'floor' }, builderId: '45' });
|
|
223
|
+
assert.equal(lead.reason, 'not_the_holder', 'a Lead builds only their own building');
|
|
224
|
+
assert.equal((await board.buildOut({ buildout: { building: 'release', capsule: 'floor' }, builderId: '3' })).path, 'at_once', 'unseated: the owner holds it');
|
|
225
|
+
assert.equal((await board.buildOut({ buildout: { building: 'release', capsule: 'tower' }, builderId: '3' })).field, 'capsule');
|
|
226
|
+
});
|
|
227
|
+
assert.equal(writes.length, 1);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('fail closed: build rights that cannot be read are a vote', async () => {
|
|
231
|
+
const writes = [];
|
|
232
|
+
// the table is unreadable when the rights are read, and readable again when the matter is filed
|
|
233
|
+
let reads = 0;
|
|
234
|
+
const flaky = async () => { reads += 1; if (reads === 1) throw new Error('down'); return []; };
|
|
235
|
+
await withStubs([...project('monarchy', { writes }), [db, 'listDecisionRules', flaky]], async () => {
|
|
236
|
+
const out = await board.buildOut({ buildout: { building: 'planning', capsule: 'floor' }, builderId: '45' });
|
|
237
|
+
assert.equal(out.path, 'vote', JSON.stringify(out));
|
|
238
|
+
assert.equal(out.rights.without_vote, false);
|
|
239
|
+
});
|
|
240
|
+
assert.equal(writes.some((w) => w[0] === 'insert'), false);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
function closeWith({ action, outcome = 'passed', area = 'planning' }) {
|
|
244
|
+
const writes = [];
|
|
245
|
+
const subjectType = `${area}_matter`;
|
|
246
|
+
return withStubs([
|
|
247
|
+
[db, 'getVotesForItem', async () => [{ voter_id: '9', direction: outcome === 'passed' ? 'yes' : 'no' }]],
|
|
248
|
+
[db, 'closeBoardItem', async ({ outcome: o }) => ({ id: '50', subject_type: subjectType, subject_id: '8', opened_by: '45', closed_at: 'T', outcome: o })],
|
|
249
|
+
[db, 'getMatter', async () => ({ id: '8', area, title: 'Add a floor', action, proposed_by: '45' })],
|
|
250
|
+
[db, 'insertBuildout', async (a) => { writes.push(a); return { done: true, buildout: { id: '7' } }; }],
|
|
251
|
+
[db, 'insertAuthorRatifyKarma', never],
|
|
252
|
+
[db, 'getBoardItemAuthor', async () => '45'],
|
|
253
|
+
], async () => board.closeItem({ id: '50', subject_type: subjectType, subject_id: '8', opened_by: '45', closed_at: null, constitution: { membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true } }))
|
|
254
|
+
.then((out) => ({ out, writes }));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
test('a passed build_out matter puts it on the record naming the item; a returned one builds nothing', async () => {
|
|
258
|
+
const action = { type: 'build_out', building: 'planning', capsule: 'annex', name: 'Research annex', line: 'Spikes live here.' };
|
|
259
|
+
const passed = await closeWith({ action });
|
|
260
|
+
assert.equal(passed.out.outcome, 'passed');
|
|
261
|
+
assert.deepEqual(passed.out.acted, { area: 'planning', matter_id: '8', action: 'build_out', done: true, building: 'planning', capsule: 'annex' });
|
|
262
|
+
assert.deepEqual(passed.writes, [{ building: 'planning', capsule: 'annex', name: 'Research annex', line: 'Spikes live here.', builtBy: '45', itemId: '50' }]);
|
|
263
|
+
const returned = await closeWith({ action, outcome: 'returned' });
|
|
264
|
+
assert.deepEqual(returned.writes, []);
|
|
265
|
+
const charterRoom = await closeWith({ action: { type: 'build_out', building: 'charter', capsule: 'committee', name: 'Drafting room', line: '' }, area: 'people' });
|
|
266
|
+
assert.equal(charterRoom.writes[0].building, 'charter', 'a People matter builds at the Charter');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ── 3. the walls ────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
test('government_030: a vote builds only what its passed matter said; at once never a district; no edit, no delete', () => {
|
|
272
|
+
assert.match(MIGRATION_CODE, /CREATE TABLE IF NOT EXISTS government_buildouts/);
|
|
273
|
+
assert.match(MIGRATION_CODE, /CHECK \(capsule IN \('committee', 'floor', 'annex', 'district'\)\)/);
|
|
274
|
+
assert.match(MIGRATION_CODE, /CONSTRAINT government_buildouts_one_path CHECK \(\(authorised_by_item_id IS NULL\) <> \(ground IS NULL\)\)/, 'exactly one way it was allowed');
|
|
275
|
+
assert.match(MIGRATION_CODE, /CONSTRAINT government_buildouts_district_votes CHECK \(capsule <> 'district' OR authorised_by_item_id IS NOT NULL\)/);
|
|
276
|
+
assert.match(MIGRATION_CODE, /i\.subject_type = m\.area \|\| '_matter'\s+AND i\.outcome = 'passed'/, 'only a passed matter');
|
|
277
|
+
assert.match(MIGRATION_CODE, /act->>'type' IS DISTINCT FROM 'build_out'\s+OR act->>'building' IS DISTINCT FROM NEW\.building\s+OR act->>'capsule' IS DISTINCT FROM NEW\.capsule\s+OR act->>'name' IS DISTINCT FROM NEW\.name\s+OR act->>'line' IS DISTINCT FROM NEW\.line THEN\s+RAISE EXCEPTION/);
|
|
278
|
+
assert.match(MIGRATION_CODE, /IF TG_OP <> 'INSERT' THEN\s+RAISE EXCEPTION/, 'no edit, no delete');
|
|
279
|
+
assert.match(MIGRATION_CODE, /ELSIF NEW\.ground IS NULL OR NEW\.why IS NULL OR NEW\.capsule = 'district' THEN\s+RAISE EXCEPTION/);
|
|
280
|
+
assert.match(MIGRATION_CODE, /NEW\.started_at := now\(\)/, 'when is the database clock');
|
|
281
|
+
assert.match(MIGRATION_CODE, /BEFORE INSERT OR UPDATE OR DELETE ON government_buildouts/);
|
|
282
|
+
assert.doesNotMatch(MIGRATION_CODE, /INSERT INTO government_buildouts/, 'nothing is seeded');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('one writer, one caller: db.insertBuildout; only decision-board calls it; no route does', () => {
|
|
286
|
+
const dbSrc = fs.readFileSync(path.join(GOV, 'db.js'), 'utf8');
|
|
287
|
+
assert.equal((dbSrc.match(/INSERT INTO government_buildouts/g) || []).length, 1);
|
|
288
|
+
assert.equal((dbSrc.match(/UPDATE government_buildouts|DELETE FROM government_buildouts/g) || []).length, 0);
|
|
289
|
+
const callers = [];
|
|
290
|
+
const walk = (dir) => {
|
|
291
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
292
|
+
const p = path.join(dir, e.name);
|
|
293
|
+
if (e.isDirectory()) { if (e.name !== 'node_modules' && e.name !== 'migrations') walk(p); continue; }
|
|
294
|
+
if (e.name.endsWith('.js') && /insertBuildout\(/.test(fs.readFileSync(p, 'utf8'))) callers.push(path.relative(ROOT, p).replace(/\\/g, '/'));
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
walk(path.join(ROOT, 'modules'));
|
|
298
|
+
walk(path.join(ROOT, 'src'));
|
|
299
|
+
assert.deepEqual(callers.sort(), ['modules/government/db.js', 'modules/government/decision-board.js']);
|
|
300
|
+
// the at-once answer is read off the build rule, never decided here
|
|
301
|
+
const dbSrcBoard = fs.readFileSync(path.join(GOV, 'decision-board.js'), 'utf8');
|
|
302
|
+
assert.doesNotMatch(dbSrcBoard, /buildRight\(|FORM_ANSWERS/, 'decision-board asks build-rights through the read, never a second rule');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// ── the route, through the real router ──────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
308
|
+
const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
|
|
309
|
+
const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
|
|
310
|
+
const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
|
|
311
|
+
const express = require('express');
|
|
312
|
+
|
|
313
|
+
const realRequireBuilder = api.requireBuilder;
|
|
314
|
+
let caller = 45;
|
|
315
|
+
api.requireBuilder = (req, res, next) => { req.builder = { id: caller, rank: 'metic' }; next(); };
|
|
316
|
+
seams._reset();
|
|
317
|
+
let held = ['board.vote.cast'];
|
|
318
|
+
seams.registerProvider('government', { resolveBuilderPermissions: async () => held });
|
|
319
|
+
const buildGovernmentRouter = require(path.join(GOV, 'routes', 'government.js'));
|
|
320
|
+
const app = express();
|
|
321
|
+
app.use(express.json());
|
|
322
|
+
app.use(attachFail);
|
|
323
|
+
app.use(buildGovernmentRouter());
|
|
324
|
+
const server = http.createServer(app);
|
|
325
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
326
|
+
const BASE = `http://127.0.0.1:${server.address().port}`;
|
|
327
|
+
after(() => { server.close(); api.requireBuilder = realRequireBuilder; seams._reset(); });
|
|
328
|
+
async function call(method, p, body) {
|
|
329
|
+
const res = await fetch(`${BASE}${p}`, { method, headers: body === undefined ? {} : { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
330
|
+
let json = null; try { json = await res.json(); } catch { /* none */ }
|
|
331
|
+
return { status: res.status, json };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
test('POST /government/buildouts is pinned where the room reads; the holder is the wall inside', () => {
|
|
335
|
+
assert.equal(rrc.MODULE_EXPECTED_RANKS.government['POST /government/buildouts'], 'perm:board.vote.cast');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('POST /government/buildouts: 201 at once, 201 filed as a vote, 403 for anyone but the holder, 400 for a bad ask, 403 without the atom', async () => {
|
|
339
|
+
await withStubs(project('monarchy'), async () => {
|
|
340
|
+
const now = await call('POST', '/government/buildouts', { building: 'planning', capsule: 'committee', name: 'Triage room' });
|
|
341
|
+
assert.equal(now.status, 201, JSON.stringify(now.json));
|
|
342
|
+
assert.equal(now.json.path, 'at_once');
|
|
343
|
+
assert.equal(now.json.buildout.capsule, 'committee');
|
|
344
|
+
const vote = await call('POST', '/government/buildouts', { building: 'planning', capsule: 'district' });
|
|
345
|
+
assert.equal(vote.status, 201);
|
|
346
|
+
assert.equal(vote.json.path, 'vote');
|
|
347
|
+
assert.equal(vote.json.item.subject_type, 'planning_matter');
|
|
348
|
+
const other = await call('POST', '/government/buildouts', { building: 'release', capsule: 'floor' });
|
|
349
|
+
assert.equal(other.status, 403);
|
|
350
|
+
assert.equal(other.json.error.code, 'not_the_holder');
|
|
351
|
+
const bad = await call('POST', '/government/buildouts', { building: 'planning', capsule: 'tower' });
|
|
352
|
+
assert.equal(bad.status, 400);
|
|
353
|
+
assert.equal(bad.json.error.code, 'invalid_buildout');
|
|
354
|
+
});
|
|
355
|
+
held = [];
|
|
356
|
+
try { assert.equal((await call('POST', '/government/buildouts', { building: 'planning', capsule: 'floor' })).status, 403); } finally { held = ['board.vote.cast']; }
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test('the matters route meets the same wall: a build_out filed straight at POST /government/board/matters by anyone but the holder is refused, and nothing is filed', async () => {
|
|
360
|
+
const writes = [];
|
|
361
|
+
held = ['board.item.open', 'board.vote.cast'];
|
|
362
|
+
try {
|
|
363
|
+
await withStubs(project('direct-democracy', { writes }), async () => {
|
|
364
|
+
caller = 3; // the owner seat, but Planning is Dee's
|
|
365
|
+
const other = await call('POST', '/government/board/matters', { area: 'planning', title: 'Add a floor to Planning', action: { type: 'build_out', building: 'planning', capsule: 'floor' } });
|
|
366
|
+
assert.equal(other.status, 403, JSON.stringify(other.json));
|
|
367
|
+
assert.equal(other.json.error.code, 'not_the_holder');
|
|
368
|
+
assert.match(JSON.stringify(other.json), /Ask them to put it up/, 'the refusal says where to go instead');
|
|
369
|
+
assert.deepEqual(writes, [], 'nothing filed');
|
|
370
|
+
caller = 45; // Dee holds Planning: filed for the vote
|
|
371
|
+
const mine = await call('POST', '/government/board/matters', { area: 'planning', title: 'Add a floor to Planning', action: { type: 'build_out', building: 'planning', capsule: 'floor' } });
|
|
372
|
+
assert.equal(mine.status, 201, JSON.stringify(mine.json));
|
|
373
|
+
assert.deepEqual(writes.map((w) => w[0]), ['matter', 'item']);
|
|
374
|
+
});
|
|
375
|
+
// and openMatter itself, whoever calls it
|
|
376
|
+
await withStubs(project('direct-democracy'), async () => {
|
|
377
|
+
const out = await board.openMatter({ area: 'release', title: 'Add an annex', action: { type: 'build_out', building: 'release', capsule: 'annex' }, proposedBy: '45' });
|
|
378
|
+
assert.equal(out.reason, 'not_the_holder', 'a Lead is not the holder of a building they do not lead');
|
|
379
|
+
assert.match(out.what_this_means, /no Lead, so only the owner grows it\. Ask the owner/);
|
|
380
|
+
});
|
|
381
|
+
} finally { held = ['board.vote.cast']; caller = 45; }
|
|
382
|
+
});
|