@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,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/government/law-board.js — the board's side of the project's LAWS
|
|
4
|
+
// (task 1004706, goal 1000111). laws.js is the pure half (what a law is, strict
|
|
5
|
+
// validation, the guard); this file is where the law book meets the board:
|
|
6
|
+
// proposing a change as an amendment, writing a passed one inside its closing
|
|
7
|
+
// transaction, and reading the book with who passed each law and when. Split out
|
|
8
|
+
// of board.js the way decision-board.js is; board.js calls in here and never the
|
|
9
|
+
// other way round (no require cycle).
|
|
10
|
+
//
|
|
11
|
+
// The window-open nudge (board.item.opened) is emitted by board.js's wrapper
|
|
12
|
+
// around proposeLaws, beside every other open.
|
|
13
|
+
|
|
14
|
+
const db = require('./db');
|
|
15
|
+
const { loadGovernmentConfig } = require('./config');
|
|
16
|
+
const laws = require('./laws');
|
|
17
|
+
const charter = require('./charter');
|
|
18
|
+
|
|
19
|
+
// ── proposing: an amendment of kind 'laws' ──────────────────────────────────
|
|
20
|
+
//
|
|
21
|
+
// Validated strictly, checked against the laws in force, filed as an amendment
|
|
22
|
+
// of kind 'laws' and sat under the constitution IN FORCE — exactly like a
|
|
23
|
+
// constitution amendment. Nothing here writes a law; only the sitting passing
|
|
24
|
+
// does (applyPassedLaws). Returns { ok, amendment, item, proposal } or a named
|
|
25
|
+
// refusal: validateLawsProposal's reasons | checkAgainstInForce's reasons
|
|
26
|
+
// | 'missing_proposer'.
|
|
27
|
+
async function proposeLaws({ enact, repeal, rationaleMd = null, proposedBy } = {}) {
|
|
28
|
+
const v = laws.validateLawsProposal({ ...(enact !== undefined ? { enact } : {}), ...(repeal !== undefined ? { repeal } : {}) });
|
|
29
|
+
if (!v.ok) return v;
|
|
30
|
+
if (proposedBy == null || proposedBy === '') return { ok: false, reason: 'missing_proposer' };
|
|
31
|
+
const inForce = await db.listLaws();
|
|
32
|
+
const c = laws.checkAgainstInForce(v.proposal, inForce);
|
|
33
|
+
if (!c.ok) return c;
|
|
34
|
+
|
|
35
|
+
const { board } = loadGovernmentConfig();
|
|
36
|
+
const amendment = await db.createAmendment({
|
|
37
|
+
proposed: { laws: v.proposal },
|
|
38
|
+
rationaleMd: rationaleMd || laws.proposalWords(v.proposal, inForce),
|
|
39
|
+
proposedBy,
|
|
40
|
+
kind: 'laws',
|
|
41
|
+
});
|
|
42
|
+
const item = await db.openBoardItem({
|
|
43
|
+
subjectType: 'constitutional_amendment',
|
|
44
|
+
subjectId: amendment.id,
|
|
45
|
+
openedBy: proposedBy,
|
|
46
|
+
constitution: board,
|
|
47
|
+
windowMinutes: board.window_minutes,
|
|
48
|
+
});
|
|
49
|
+
return { ok: true, amendment, item, proposal: v.proposal };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── the one write ───────────────────────────────────────────────────────────
|
|
53
|
+
//
|
|
54
|
+
// Runs INSIDE the transaction that closes the sitting (`client`); a throw rolls
|
|
55
|
+
// the close back. Guarded here (laws.lawsWriteAllowed), in db.applyLaws's SQL,
|
|
56
|
+
// and by government_027's trigger.
|
|
57
|
+
async function applyPassedLaws(client, closedRow, amendment) {
|
|
58
|
+
if (!laws.lawsWriteAllowed(closedRow, amendment)) {
|
|
59
|
+
throw new Error(`[government] laws may only be written by a passed laws amendment (item ${closedRow && closedRow.id})`);
|
|
60
|
+
}
|
|
61
|
+
// Re-validated as a belt: the row was validated strictly at propose time and
|
|
62
|
+
// is immutable since, so a refusal here means the stored row was altered.
|
|
63
|
+
const v = laws.validateLawsProposal((amendment.proposed || {}).laws);
|
|
64
|
+
if (!v.ok) throw new Error(`[government] passed laws amendment ${amendment.id} no longer validates (${v.reason}) — NOT applied`);
|
|
65
|
+
const out = await db.applyLaws(client, {
|
|
66
|
+
itemId: closedRow.id,
|
|
67
|
+
enact: v.proposal.enact,
|
|
68
|
+
repeal: v.proposal.repeal,
|
|
69
|
+
proposedBy: amendment.proposed_by == null ? null : amendment.proposed_by,
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
enacted: (out.enacted || []).map((r) => ({ id: String(r.id), text: r.text })),
|
|
73
|
+
repealed: (out.repealed || []).map((r) => String(r.id)),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── the law book ────────────────────────────────────────────────────────────
|
|
78
|
+
//
|
|
79
|
+
// Every law in force (or, with includeRepealed, every law ever passed), oldest
|
|
80
|
+
// first, each saying WHO passed it and WHEN: the government it was decided under
|
|
81
|
+
// (read as its form's name), who proposed it, who said yes, and the sitting.
|
|
82
|
+
const person = (id, login, name) => (id == null ? null : { id: String(id), login: login || null, name: name || login || null });
|
|
83
|
+
|
|
84
|
+
function projectLaw(row, yesByItem) {
|
|
85
|
+
const under = row.decided_under || null;
|
|
86
|
+
const form = under ? charter.readCharter(under) : null;
|
|
87
|
+
const itemId = String(row.passed_by_item_id);
|
|
88
|
+
return {
|
|
89
|
+
id: String(row.id),
|
|
90
|
+
text: row.text,
|
|
91
|
+
effect_on_ai: row.effect_on_ai || null,
|
|
92
|
+
locks: Array.isArray(row.locks) ? row.locks : [],
|
|
93
|
+
in_force: row.repealed_at == null,
|
|
94
|
+
passed_at: row.passed_at,
|
|
95
|
+
passed_by: {
|
|
96
|
+
item_id: itemId,
|
|
97
|
+
href: `/board-room?item=${encodeURIComponent(itemId)}`,
|
|
98
|
+
government: form ? form.name : null,
|
|
99
|
+
pass_rule: under && under.pass_rule ? under.pass_rule : null,
|
|
100
|
+
proposer: person(row.proposed_by, row.proposer_login, row.proposer_name),
|
|
101
|
+
yes: (yesByItem.get(itemId) || []).map((v) => person(v.voter_id, v.github_login, v.display_name)),
|
|
102
|
+
},
|
|
103
|
+
repealed_at: row.repealed_at || null,
|
|
104
|
+
repealed_by_item_id: row.repealed_by_item_id == null ? null : String(row.repealed_by_item_id),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function lawsView({ includeRepealed = false } = {}) {
|
|
109
|
+
const rows = await db.listLaws({ includeRepealed });
|
|
110
|
+
const itemIds = [...new Set(rows.map((r) => String(r.passed_by_item_id)))];
|
|
111
|
+
const yes = itemIds.length ? await db.listYesVoters(itemIds) : [];
|
|
112
|
+
const yesByItem = new Map();
|
|
113
|
+
for (const v of yes) {
|
|
114
|
+
const k = String(v.item_id);
|
|
115
|
+
if (!yesByItem.has(k)) yesByItem.set(k, []);
|
|
116
|
+
yesByItem.get(k).push(v);
|
|
117
|
+
}
|
|
118
|
+
const book = rows.map((r) => projectLaw(r, yesByItem));
|
|
119
|
+
return {
|
|
120
|
+
laws: book,
|
|
121
|
+
in_force: book.filter((l) => l.in_force).length,
|
|
122
|
+
changed_by: 'a passed amendment only: POST /government/board/laws files one',
|
|
123
|
+
limits: { text_max: laws.LAW_TEXT_MAX, effect_max: laws.EFFECT_MAX, locks_max: laws.MAX_LOCKS },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
proposeLaws,
|
|
129
|
+
applyPassedLaws,
|
|
130
|
+
lawsView,
|
|
131
|
+
};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/government/laws.js — STANDING ORDERS: the project's laws for its AI
|
|
4
|
+
// agents (task 1004706, goal 1000111 "government is how a project steers its AI").
|
|
5
|
+
//
|
|
6
|
+
// WHAT A LAW IS. A short, one-line rule every AI agent on the project obeys —
|
|
7
|
+
// "Stuck? Note it, skip it, keep going", "Land only. Ask before going live",
|
|
8
|
+
// "Ask like a human". It carries an optional line saying what it means for an
|
|
9
|
+
// agent, and may LOCK personal settings it overrides (a lock names a setting and
|
|
10
|
+
// either fixes its value or caps it with a max). Before this, rules like these
|
|
11
|
+
// lived in one builder's private AI notes, so nobody else's agents ever saw them.
|
|
12
|
+
//
|
|
13
|
+
// CHANGED ONLY BY A PASSED AMENDMENT, like the constitution. There is no route
|
|
14
|
+
// that writes a law. A change — enacting new laws, repealing laws in force, or
|
|
15
|
+
// both at once — is filed as an amendment of kind 'laws' (law-board.js
|
|
16
|
+
// proposeLaws), decided by the board IN FORCE like every other amendment, and
|
|
17
|
+
// written INSIDE the transaction that closes that sitting as passed
|
|
18
|
+
// (board.closeItem → law-board.applyPassedLaws → db.applyLaws). The database
|
|
19
|
+
// refuses any other write (government_027's trigger), so a hand-written INSERT
|
|
20
|
+
// or UPDATE is refused too, and a law is never deleted: a repeal is a passed
|
|
21
|
+
// amendment, and the repealed row stays on the record.
|
|
22
|
+
//
|
|
23
|
+
// WHO PASSED IT, AND WHEN. Every law row names the board item that passed it,
|
|
24
|
+
// its proposer and when it passed; the read joins that item's constitution (the
|
|
25
|
+
// government it was decided under) and its yes votes. A repeal records the item
|
|
26
|
+
// that repealed it, and when.
|
|
27
|
+
//
|
|
28
|
+
// READ INTO EVERY AI SESSION (task 1004707). forSession below turns the laws in
|
|
29
|
+
// force into the one block every session reads: GET /me carries it, and the
|
|
30
|
+
// Conductor hook relays it verbatim beside the builder's interaction profile.
|
|
31
|
+
// The server writes the words; nothing a builder edits locally can change them.
|
|
32
|
+
// A lock is ENFORCED on the builder's personal settings by builder-settings
|
|
33
|
+
// (law-locks.js), on every read of the effective value and on every write. The
|
|
34
|
+
// first laws themselves are the owner's to ratify (task 1004708).
|
|
35
|
+
//
|
|
36
|
+
// PURE: no db, no requires. law-board.js and the routes hand rows in.
|
|
37
|
+
|
|
38
|
+
const LAW_TEXT_MIN = 3;
|
|
39
|
+
const LAW_TEXT_MAX = 100;
|
|
40
|
+
const EFFECT_MAX = 200;
|
|
41
|
+
const MAX_LOCKS = 8;
|
|
42
|
+
const MAX_ENACT = 10;
|
|
43
|
+
const MAX_REPEAL = 20;
|
|
44
|
+
const LOCK_VALUE_MAX = 64;
|
|
45
|
+
// A personal setting's key, as the hall and the interaction profile spell them
|
|
46
|
+
// (technical_level, wandering, model.main, on_stuck).
|
|
47
|
+
const SETTING_KEY = /^[a-z][a-z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)?$/;
|
|
48
|
+
const SETTING_KEY_MAX = 64;
|
|
49
|
+
// One line: no line breaks or other control characters.
|
|
50
|
+
// eslint-disable-next-line no-control-regex
|
|
51
|
+
const CONTROL = /[\u0000-\u001f\u007f]/;
|
|
52
|
+
|
|
53
|
+
function oneLine(raw, { min, max }) {
|
|
54
|
+
if (typeof raw !== 'string') return null;
|
|
55
|
+
if (CONTROL.test(raw)) return null;
|
|
56
|
+
const s = raw.trim().replace(/\s+/g, ' ');
|
|
57
|
+
if (s.length < min || s.length > max) return null;
|
|
58
|
+
return s;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A lock: { setting, value } fixes the setting; { setting, max } caps it (a
|
|
62
|
+
// number, or the highest option of an ordered list such as wandering's).
|
|
63
|
+
function validateLock(raw, i) {
|
|
64
|
+
const at = `locks[${i}]`;
|
|
65
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, reason: 'bad_lock', field: at };
|
|
66
|
+
for (const k of Object.keys(raw)) {
|
|
67
|
+
if (k !== 'setting' && k !== 'value' && k !== 'max') return { ok: false, reason: 'unknown_field', field: `${at}.${k}` };
|
|
68
|
+
}
|
|
69
|
+
if (typeof raw.setting !== 'string' || raw.setting.length > SETTING_KEY_MAX || !SETTING_KEY.test(raw.setting)) {
|
|
70
|
+
return { ok: false, reason: 'bad_lock_setting', field: `${at}.setting` };
|
|
71
|
+
}
|
|
72
|
+
const hasValue = Object.prototype.hasOwnProperty.call(raw, 'value');
|
|
73
|
+
const hasMax = Object.prototype.hasOwnProperty.call(raw, 'max');
|
|
74
|
+
if (hasValue === hasMax) return { ok: false, reason: 'lock_needs_value_or_max', field: at, expected: 'exactly one of value, max' };
|
|
75
|
+
if (hasValue) {
|
|
76
|
+
const v = raw.value;
|
|
77
|
+
const okValue = typeof v === 'boolean'
|
|
78
|
+
|| (typeof v === 'number' && Number.isFinite(v))
|
|
79
|
+
|| (typeof v === 'string' && v.length >= 1 && v.length <= LOCK_VALUE_MAX && !CONTROL.test(v));
|
|
80
|
+
if (!okValue) return { ok: false, reason: 'bad_lock_value', field: `${at}.value` };
|
|
81
|
+
return { ok: true, lock: { setting: raw.setting, value: v } };
|
|
82
|
+
}
|
|
83
|
+
const m = raw.max;
|
|
84
|
+
const okMax = (typeof m === 'number' && Number.isFinite(m))
|
|
85
|
+
|| (typeof m === 'string' && m.length >= 1 && m.length <= LOCK_VALUE_MAX && !CONTROL.test(m));
|
|
86
|
+
if (!okMax) return { ok: false, reason: 'bad_lock_max', field: `${at}.max` };
|
|
87
|
+
return { ok: true, lock: { setting: raw.setting, max: m } };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function validateLaw(raw, i) {
|
|
91
|
+
const at = `enact[${i}]`;
|
|
92
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, reason: 'bad_law', field: at };
|
|
93
|
+
for (const k of Object.keys(raw)) {
|
|
94
|
+
if (k !== 'text' && k !== 'effect_on_ai' && k !== 'locks') return { ok: false, reason: 'unknown_field', field: `${at}.${k}` };
|
|
95
|
+
}
|
|
96
|
+
const text = oneLine(raw.text, { min: LAW_TEXT_MIN, max: LAW_TEXT_MAX });
|
|
97
|
+
if (!text) return { ok: false, reason: 'bad_text', field: `${at}.text`, expected: `one line of ${LAW_TEXT_MIN}-${LAW_TEXT_MAX} characters` };
|
|
98
|
+
let effect = null;
|
|
99
|
+
if (raw.effect_on_ai != null) {
|
|
100
|
+
effect = oneLine(raw.effect_on_ai, { min: 1, max: EFFECT_MAX });
|
|
101
|
+
if (!effect) return { ok: false, reason: 'bad_effect', field: `${at}.effect_on_ai`, expected: `one line of at most ${EFFECT_MAX} characters` };
|
|
102
|
+
}
|
|
103
|
+
const locksIn = raw.locks == null ? [] : raw.locks;
|
|
104
|
+
if (!Array.isArray(locksIn) || locksIn.length > MAX_LOCKS) return { ok: false, reason: 'bad_locks', field: `${at}.locks`, expected: `a list of at most ${MAX_LOCKS}` };
|
|
105
|
+
const locks = [];
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
for (let j = 0; j < locksIn.length; j++) {
|
|
108
|
+
const v = validateLock(locksIn[j], j);
|
|
109
|
+
if (!v.ok) return { ...v, field: `${at}.${v.field}` };
|
|
110
|
+
if (seen.has(v.lock.setting)) return { ok: false, reason: 'setting_locked_twice', field: `${at}.locks[${j}].setting`, setting: v.lock.setting };
|
|
111
|
+
seen.add(v.lock.setting);
|
|
112
|
+
locks.push(v.lock);
|
|
113
|
+
}
|
|
114
|
+
return { ok: true, law: { text, effect_on_ai: effect, locks } };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const sameText = (a, b) => String(a).trim().toLowerCase() === String(b).trim().toLowerCase();
|
|
118
|
+
|
|
119
|
+
// A proposal, validated STRICTLY — every field named, nothing corrected beyond
|
|
120
|
+
// trimming whitespace (the board must not ratify words the proposer never wrote).
|
|
121
|
+
// { enact?: [{ text, effect_on_ai?, locks? }], repeal?: [law id] }
|
|
122
|
+
// Returns { ok: true, proposal: { enact, repeal } } or { ok: false, reason, field?, expected? }.
|
|
123
|
+
function validateLawsProposal(raw) {
|
|
124
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, reason: 'bad_proposal', expected: '{ enact?: [law], repeal?: [law id] }' };
|
|
125
|
+
for (const k of Object.keys(raw)) {
|
|
126
|
+
if (k !== 'enact' && k !== 'repeal') return { ok: false, reason: 'unknown_field', field: k };
|
|
127
|
+
}
|
|
128
|
+
const enactIn = raw.enact == null ? [] : raw.enact;
|
|
129
|
+
const repealIn = raw.repeal == null ? [] : raw.repeal;
|
|
130
|
+
if (!Array.isArray(enactIn) || enactIn.length > MAX_ENACT) return { ok: false, reason: 'bad_enact', field: 'enact', expected: `a list of at most ${MAX_ENACT} laws` };
|
|
131
|
+
if (!Array.isArray(repealIn) || repealIn.length > MAX_REPEAL) return { ok: false, reason: 'bad_repeal', field: 'repeal', expected: `a list of at most ${MAX_REPEAL} law ids` };
|
|
132
|
+
if (!enactIn.length && !repealIn.length) return { ok: false, reason: 'nothing_to_change', expected: 'at least one law to enact or repeal' };
|
|
133
|
+
const enact = [];
|
|
134
|
+
for (let i = 0; i < enactIn.length; i++) {
|
|
135
|
+
const v = validateLaw(enactIn[i], i);
|
|
136
|
+
if (!v.ok) return v;
|
|
137
|
+
if (enact.some((l) => sameText(l.text, v.law.text))) return { ok: false, reason: 'duplicate_law', field: `enact[${i}].text` };
|
|
138
|
+
enact.push(v.law);
|
|
139
|
+
}
|
|
140
|
+
const repeal = [];
|
|
141
|
+
for (let i = 0; i < repealIn.length; i++) {
|
|
142
|
+
const r = repealIn[i];
|
|
143
|
+
const n = typeof r === 'string' && /^\d+$/.test(r) ? Number(r) : r;
|
|
144
|
+
if (typeof n !== 'number' || !Number.isSafeInteger(n) || n <= 0) return { ok: false, reason: 'bad_law_id', field: `repeal[${i}]` };
|
|
145
|
+
if (repeal.includes(String(n))) return { ok: false, reason: 'duplicate_law_id', field: `repeal[${i}]` };
|
|
146
|
+
repeal.push(String(n));
|
|
147
|
+
}
|
|
148
|
+
return { ok: true, proposal: { enact, repeal } };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Against the laws in force: a repeal must name a law in force, an enacted law
|
|
152
|
+
// must not already be in force (unless the same proposal repeals it), and no
|
|
153
|
+
// setting may be locked by two laws at once. `inForce` rows: { id, text, locks }.
|
|
154
|
+
// Returns { ok: true } or a named refusal.
|
|
155
|
+
function checkAgainstInForce(proposal, inForce) {
|
|
156
|
+
const rows = Array.isArray(inForce) ? inForce : [];
|
|
157
|
+
const byId = new Map(rows.map((l) => [String(l.id), l]));
|
|
158
|
+
for (const id of proposal.repeal) {
|
|
159
|
+
if (!byId.has(id)) return { ok: false, reason: 'not_in_force', law_id: id };
|
|
160
|
+
}
|
|
161
|
+
const staying = rows.filter((l) => !proposal.repeal.includes(String(l.id)));
|
|
162
|
+
for (let i = 0; i < proposal.enact.length; i++) {
|
|
163
|
+
const law = proposal.enact[i];
|
|
164
|
+
const twin = staying.find((l) => sameText(l.text, law.text));
|
|
165
|
+
if (twin) return { ok: false, reason: 'already_in_force', field: `enact[${i}].text`, law_id: String(twin.id) };
|
|
166
|
+
for (const lock of law.locks) {
|
|
167
|
+
const holder = staying.find((l) => (Array.isArray(l.locks) ? l.locks : []).some((x) => x && x.setting === lock.setting))
|
|
168
|
+
|| proposal.enact.slice(0, i).find((l) => l.locks.some((x) => x.setting === lock.setting));
|
|
169
|
+
if (holder) return { ok: false, reason: 'setting_already_locked', field: `enact[${i}].locks`, setting: lock.setting, ...(holder.id != null ? { law_id: String(holder.id) } : {}) };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { ok: true };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// The record's own words for a proposal ("Law: Ask like a human. Repeal: ...").
|
|
176
|
+
function proposalWords(proposal, inForce) {
|
|
177
|
+
const byId = new Map((Array.isArray(inForce) ? inForce : []).map((l) => [String(l.id), l]));
|
|
178
|
+
const parts = [];
|
|
179
|
+
proposal.enact.forEach((l) => parts.push(`Law: ${l.text}`));
|
|
180
|
+
proposal.repeal.forEach((id) => parts.push(`Repeal: ${byId.has(id) ? byId.get(id).text : `law ${id}`}`));
|
|
181
|
+
return parts.join('. ').replace(/([.!?])\.(\s|$)/g, '$1$2').replace(/([^.!?])$/, '$1.');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// THE GUARD. A law may be written only when the row that just closed is a
|
|
185
|
+
// PASSED constitutional amendment whose amendment is of kind 'laws' and is the
|
|
186
|
+
// item's own subject. db.applyLaws carries the same condition in its SQL, and
|
|
187
|
+
// government_027's trigger refuses every other write — three layers.
|
|
188
|
+
function lawsWriteAllowed(closedRow, amendment) {
|
|
189
|
+
return !!closedRow && !!amendment
|
|
190
|
+
&& closedRow.subject_type === 'constitutional_amendment'
|
|
191
|
+
&& closedRow.outcome === 'passed'
|
|
192
|
+
&& amendment.kind === 'laws'
|
|
193
|
+
&& String(amendment.id) === String(closedRow.subject_id);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── the session read (task 1004707) ─────────────────────────────────────────
|
|
197
|
+
//
|
|
198
|
+
// What every AI session on the project reads: the laws in force, oldest first,
|
|
199
|
+
// numbered, each with what it means for an agent and the settings it locks.
|
|
200
|
+
// `rows`: db.listLaws() rows (in force only). Returns
|
|
201
|
+
// { laws: [{ id, no, text, effect_on_ai, locks }], contract, inject }
|
|
202
|
+
// `contract` is the block the Conductor relays verbatim; '' (and inject:false)
|
|
203
|
+
// when no law is in force, so a project with no laws spends no context. The
|
|
204
|
+
// text is one line per law by construction (government_027's CHECKs), so a law
|
|
205
|
+
// can never smuggle a second instruction onto a line of its own.
|
|
206
|
+
const lockWords = (lock) => (Object.prototype.hasOwnProperty.call(lock, 'value')
|
|
207
|
+
? `${lock.setting} is fixed at ${lock.value}`
|
|
208
|
+
: `${lock.setting} is at most ${lock.max}`);
|
|
209
|
+
|
|
210
|
+
function forSession(rows) {
|
|
211
|
+
const list = (Array.isArray(rows) ? rows : [])
|
|
212
|
+
.filter((r) => r && r.id != null && r.repealed_at == null && typeof r.text === 'string');
|
|
213
|
+
const book = list.map((r, i) => ({
|
|
214
|
+
id: String(r.id),
|
|
215
|
+
no: i + 1,
|
|
216
|
+
text: r.text,
|
|
217
|
+
effect_on_ai: r.effect_on_ai || null,
|
|
218
|
+
locks: (Array.isArray(r.locks) ? r.locks : []).filter((l) => l && typeof l.setting === 'string'),
|
|
219
|
+
}));
|
|
220
|
+
if (!book.length) return { laws: [], contract: '', inject: false };
|
|
221
|
+
const lines = book.map((l) => {
|
|
222
|
+
let line = `${l.no}. ${l.text}`;
|
|
223
|
+
if (l.effect_on_ai) line += ` — ${l.effect_on_ai}`;
|
|
224
|
+
if (l.locks.length) line += ` (locks: ${l.locks.map(lockWords).join('; ')})`;
|
|
225
|
+
return line;
|
|
226
|
+
});
|
|
227
|
+
const contract = 'Project laws — passed by a vote of this project\'s government. Every AI agent on the project obeys them, and they come before this builder\'s own settings and preferences. A setting a law locks is enforced by the server; do not work around it:\n'
|
|
228
|
+
+ lines.join('\n');
|
|
229
|
+
return { laws: book, contract, inject: true };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
forSession,
|
|
234
|
+
LAW_TEXT_MAX,
|
|
235
|
+
EFFECT_MAX,
|
|
236
|
+
MAX_LOCKS,
|
|
237
|
+
validateLawsProposal,
|
|
238
|
+
checkAgainstInForce,
|
|
239
|
+
proposalWords,
|
|
240
|
+
lawsWriteAllowed,
|
|
241
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/government/leads.js — THE LEAD OF EACH BUILDING: who leads it, held as a
|
|
4
|
+
// position the government appoints and removes by vote (task 1004709, goal
|
|
5
|
+
// 1000111; ADR 0374). decision-board.js is the board side (opening the People
|
|
6
|
+
// matter, carrying out a passed one), board.js serves the read (GET
|
|
7
|
+
// /government/leads), and government_029 is the table and its guard.
|
|
8
|
+
//
|
|
9
|
+
// WHAT A LEAD IS. One builder in office at one of the nine buildings, carrying a
|
|
10
|
+
// short charter: one line saying what they answer for. A building has at most
|
|
11
|
+
// one Lead. A Lead is appointed and removed ONLY by a passed People matter — the
|
|
12
|
+
// actions appoint_lead { building, builder_id, charter } and remove_lead
|
|
13
|
+
// { building } (decision-board.js MATTER_ACTIONS) — and the table's trigger
|
|
14
|
+
// refuses every other write.
|
|
15
|
+
//
|
|
16
|
+
// WHO ANSWERS FOR A BUILDING. Its Lead; an UNSEATED building falls to the owner
|
|
17
|
+
// (owner ruling, 2026-10-09). The project has no owner field, so the owner seat
|
|
18
|
+
// is held by the founder's rank: every active Archon, the founding one (the
|
|
19
|
+
// lowest id, the Archon the project was bootstrapped with) first — never one
|
|
20
|
+
// Archon picked by a guess. answerFor() is that rule, written
|
|
21
|
+
// once, for the read here and for routing every ask to a seat (routing.js, task
|
|
22
|
+
// 1004705).
|
|
23
|
+
//
|
|
24
|
+
// WHAT IT DOES NOT DECIDE. Holding the Lead's office confers no authority of its
|
|
25
|
+
// own. Whether a building's holder may build it out without a vote comes from
|
|
26
|
+
// the FORM OF GOVERNMENT and the building's own decision rule (build-rights.js,
|
|
27
|
+
// task 1004710), never from the Lead or their charter; the read below carries
|
|
28
|
+
// that answer per building as `build_rights`. Positions on /me are task 1004713.
|
|
29
|
+
// What each building has grown under its holder rides the same read as
|
|
30
|
+
// `structure` (buildouts.js, task 1004711).
|
|
31
|
+
//
|
|
32
|
+
// PURE: no db, no requires beyond the buildings' vocabulary, the build rule and
|
|
33
|
+
// the build-outs' shape.
|
|
34
|
+
|
|
35
|
+
const { AREAS, AREA_KEYS } = require('./decision-rules');
|
|
36
|
+
const { buildRightsFor } = require('./build-rights');
|
|
37
|
+
const { structureOf } = require('./buildouts');
|
|
38
|
+
|
|
39
|
+
const CHARTER_MIN = 3;
|
|
40
|
+
const CHARTER_MAX = 140;
|
|
41
|
+
|
|
42
|
+
// The charter a Lead is appointed with when the matter names none: one line per
|
|
43
|
+
// building, saying what its Lead answers for.
|
|
44
|
+
const DEFAULT_CHARTERS = Object.freeze({
|
|
45
|
+
charter: 'The constitution: how the project is governed and how that changes.',
|
|
46
|
+
rules: 'The project settings and the standing rules of work.',
|
|
47
|
+
disputes: 'Security reports and the disputes over them.',
|
|
48
|
+
people: 'Who joins, and the ranks they hold.',
|
|
49
|
+
planning: 'Goals, priorities and the order of work.',
|
|
50
|
+
automation: 'Unattended building: when it runs and on what.',
|
|
51
|
+
review: 'Who checks finished work, and how.',
|
|
52
|
+
release: 'Releases and the path to going live.',
|
|
53
|
+
treasury: 'Spending limits and what the project pays for.',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const isBuilding = (b) => typeof b === 'string' && AREA_KEYS.includes(b);
|
|
57
|
+
|
|
58
|
+
// A charter line, normalised, or null when it breaks the rule (3-140 characters,
|
|
59
|
+
// one line). An absent charter takes the building's default.
|
|
60
|
+
function charterLine(raw, building) {
|
|
61
|
+
if (raw == null || raw === '') return DEFAULT_CHARTERS[building] || null;
|
|
62
|
+
if (typeof raw !== 'string' || /[\r\n]/.test(raw)) return null;
|
|
63
|
+
const s = raw.trim();
|
|
64
|
+
return s.length >= CHARTER_MIN && s.length <= CHARTER_MAX ? s : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The two People matter actions that seat and unseat a Lead.
|
|
68
|
+
const LEAD_ACTS = Object.freeze(['appoint_lead', 'remove_lead']);
|
|
69
|
+
const isLeadAct = (type) => LEAD_ACTS.includes(type);
|
|
70
|
+
|
|
71
|
+
// The owner seat: every active Archon, the founding one (lowest id) first; nobody
|
|
72
|
+
// when the project has no active Archon.
|
|
73
|
+
function ownerOf(archons) {
|
|
74
|
+
const list = (archons || [])
|
|
75
|
+
.filter((b) => b && b.id != null)
|
|
76
|
+
.slice()
|
|
77
|
+
.sort((a, b) => Number(a.id) - Number(b.id))
|
|
78
|
+
.map((b) => ({ id: String(b.id), login: b.github_login || null, name: b.display_name || b.github_login || null }));
|
|
79
|
+
return { seat: 'owner', rank: 'archon', builders: list };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Who answers for a building: its Lead in office, or the owner when unseated.
|
|
83
|
+
// `leadsInOffice` is a map building → lead ({ builder_id, ... }) or an array of rows.
|
|
84
|
+
function answerFor(building, leadsInOffice, owner) {
|
|
85
|
+
if (!isBuilding(building)) return null;
|
|
86
|
+
const rows = Array.isArray(leadsInOffice) ? leadsInOffice : Object.values(leadsInOffice || {});
|
|
87
|
+
const lead = rows.find((l) => l && l.building === building && !l.removed_at) || null;
|
|
88
|
+
if (lead) return { seat: `lead:${building}`, builder_ids: [String(lead.builder_id)], fell_to_owner: false };
|
|
89
|
+
const ids = ((owner && owner.builders) || []).map((b) => String(b.id));
|
|
90
|
+
return { seat: 'owner', builder_ids: ids, fell_to_owner: true };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// One Lead row as the read shows it.
|
|
94
|
+
function projectLead(r) {
|
|
95
|
+
return {
|
|
96
|
+
id: String(r.id),
|
|
97
|
+
building: r.building,
|
|
98
|
+
builder: { id: String(r.builder_id), login: r.github_login || null, name: r.display_name || r.github_login || null },
|
|
99
|
+
charter: r.charter,
|
|
100
|
+
since: r.appointed_at || null,
|
|
101
|
+
appointed_by: { item_id: String(r.appointed_by_item_id), href: `/board-room?item=${r.appointed_by_item_id}` },
|
|
102
|
+
...(r.removed_at ? { removed_at: r.removed_at, removed_by: { item_id: String(r.removed_by_item_id), href: `/board-room?item=${r.removed_by_item_id}` } } : {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Who may build a building out without a vote, from the context the server
|
|
107
|
+
// resolved (decision-board.leadsView): the board block in force, each building's
|
|
108
|
+
// resolved decision row, who sits on the board, who sits on each building's own
|
|
109
|
+
// sitting, and the goals hook. null when no context was read.
|
|
110
|
+
function rightsOf(building, answers, rights) {
|
|
111
|
+
if (!rights) return null;
|
|
112
|
+
const ids = ((answers && answers.builder_ids) || []).map(String);
|
|
113
|
+
const seatedIn = (list) => Array.isArray(list) && list.some((id) => ids.includes(String(id)));
|
|
114
|
+
const decision = (rights.decisions || []).find((d) => d && d.area === building) || null;
|
|
115
|
+
return buildRightsFor({
|
|
116
|
+
building,
|
|
117
|
+
board: rights.board,
|
|
118
|
+
decision,
|
|
119
|
+
holder: {
|
|
120
|
+
seat: answers ? answers.seat : null,
|
|
121
|
+
on_board: seatedIn(rights.boardMemberIds),
|
|
122
|
+
in_sitting: seatedIn((rights.sittingMemberIds || {})[building]),
|
|
123
|
+
},
|
|
124
|
+
goals: rights.goals || [],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// GET /government/leads: the nine buildings in street order, each with its Lead
|
|
129
|
+
// in office (or null), who answers for it, and what its holder may build without
|
|
130
|
+
// a vote (`build_rights`; null when the server could not read what it needs).
|
|
131
|
+
// `structure` ({ rows, pending, now }: the build-outs on the record and the
|
|
132
|
+
// open build_out sittings) gives each building what it has grown and what waits
|
|
133
|
+
// on a vote; without it each building's `structure` is null.
|
|
134
|
+
function leadsView(rows, archons, { includePast = false, rights = null, structure = null } = {}) {
|
|
135
|
+
const owner = ownerOf(archons);
|
|
136
|
+
const inOffice = (rows || []).filter((r) => r && !r.removed_at);
|
|
137
|
+
const buildings = AREAS.map((a) => {
|
|
138
|
+
const row = inOffice.find((r) => r.building === a.key) || null;
|
|
139
|
+
const answers = answerFor(a.key, inOffice, owner);
|
|
140
|
+
return {
|
|
141
|
+
building: a.key,
|
|
142
|
+
name: a.name,
|
|
143
|
+
lead: row ? projectLead(row) : null,
|
|
144
|
+
answers,
|
|
145
|
+
build_rights: rightsOf(a.key, answers, rights),
|
|
146
|
+
structure: structure ? structureOf(a.key, structure.rows, structure.pending, structure.now) : null,
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
buildings,
|
|
151
|
+
owner,
|
|
152
|
+
seated: buildings.filter((b) => b.lead).length,
|
|
153
|
+
build_rights_from: 'the form of government and each building\'s own decision rule (goals may grant or limit it later), never a Lead\'s charter',
|
|
154
|
+
...(includePast ? { past: (rows || []).filter((r) => r && r.removed_at).map(projectLead) } : {}),
|
|
155
|
+
changed_by: 'a passed People matter only: POST /government/board/matters with area people and an appoint_lead or remove_lead action',
|
|
156
|
+
grown_through: 'the building\'s holder: POST /government/buildouts builds at once where build_rights allows it, and otherwise files a build_out matter for a vote',
|
|
157
|
+
limits: { charter_min: CHARTER_MIN, charter_max: CHARTER_MAX },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
CHARTER_MAX,
|
|
163
|
+
DEFAULT_CHARTERS,
|
|
164
|
+
LEAD_ACTS,
|
|
165
|
+
isLeadAct,
|
|
166
|
+
charterLine,
|
|
167
|
+
answerFor,
|
|
168
|
+
ownerOf,
|
|
169
|
+
leadsView,
|
|
170
|
+
};
|