@bongos/core 1.21.113 → 1.21.115
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 +608 -188
- 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 +709 -626
- package/docs/copy-registry.json +1673 -858
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/packs/engineer.md +2 -1
- package/docs/page-inventory.json +91 -6
- package/docs/page-readings.json +4875 -4748
- 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-hall-live.states.json +0 -5
- package/modules/public-landing/public/projects.html +169 -6
- package/modules/public-landing/public/projects.probes.json +10 -6
- package/modules/public-landing/public/projects.states.json +13 -13
- package/modules/public-landing/public/terms.html +2 -2
- package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
- package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
- package/modules/ui-design/kit/fixtures/connections.json +22 -0
- package/modules/ui-design/kit/serve.js +95 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +128 -0
- package/scripts/discord/README.md +8 -5
- package/scripts/discord/post.js +12 -1
- package/scripts/gds/artifact-audit.js +20 -3
- package/scripts/gds/ask.js +164 -0
- package/scripts/gds/autobongos-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/projects_hub_pre_uat.mjs +4 -2
- 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
- package/tests/wizard_physics.mjs +1 -1
- package/tests/wizard_physics_more.mjs +133 -0
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
const api = require('../../src/module-api');
|
|
16
16
|
const db = require('./db');
|
|
17
17
|
const { loadGovernmentConfig, isClockSafe } = require('./config');
|
|
18
|
-
const { isBoardMember } = require('./board-membership');
|
|
18
|
+
const { isBoardMember, resolveBoardMembers } = require('./board-membership');
|
|
19
19
|
const decisionRules = require('./decision-rules');
|
|
20
|
+
const { charterLine, isLeadAct, answerFor, ownerOf, leadsView: leadsBook } = require('./leads');
|
|
21
|
+
const buildouts = require('./buildouts');
|
|
20
22
|
|
|
21
23
|
const log = api.logger('government');
|
|
22
24
|
|
|
@@ -60,7 +62,15 @@ async function actOnPassedMatter(closed) {
|
|
|
60
62
|
try {
|
|
61
63
|
matter = await db.getMatter(closed.subject_id);
|
|
62
64
|
const action = matter && matter.action;
|
|
63
|
-
if (
|
|
65
|
+
if (action && action.type === 'build_out') {
|
|
66
|
+
// A build-out the vote allowed (task 1004711): put on the record naming
|
|
67
|
+
// this very item, which government_030's trigger checks.
|
|
68
|
+
result = { ...result, ...(await actOnBuildOutMatter(closed, matter, area)) };
|
|
69
|
+
} else if (area === 'people' && action && isLeadAct(action.type)) {
|
|
70
|
+
// A Lead appointed or removed (task 1004709): carried out here, in one
|
|
71
|
+
// transaction the table's trigger checks against this very item.
|
|
72
|
+
result = { ...result, ...(await actOnLeadMatter(closed, action)) };
|
|
73
|
+
} else if (area === 'people' && action && (action.type === 'assign_rank' || action.type === 'unassign_rank')) {
|
|
64
74
|
// The same walls as the direct route: a custom rank that still exists,
|
|
65
75
|
// never a seeded one (a seeded rank follows the builder's rank — R95b).
|
|
66
76
|
const rank = await db.getRankByKey(action.rank_key);
|
|
@@ -111,6 +121,45 @@ async function actOnPassedMatter(closed) {
|
|
|
111
121
|
};
|
|
112
122
|
}
|
|
113
123
|
|
|
124
|
+
// A passed appoint_lead / remove_lead (task 1004709). Re-validated first (the
|
|
125
|
+
// stored row was checked when opened and is immutable since); the appointee must
|
|
126
|
+
// still be an active builder of the project. Never throws past the caller's catch.
|
|
127
|
+
async function actOnLeadMatter(closed, action) {
|
|
128
|
+
const v = validateMatterAction('people', action);
|
|
129
|
+
if (!v.ok) return { action: action.type || null, done: false, reason: 'bad_action' };
|
|
130
|
+
const a = v.action;
|
|
131
|
+
if (a.type === 'appoint_lead' && !(await db.getActiveBuilder(a.builder_id))) {
|
|
132
|
+
return { action: a.type, done: false, reason: 'builder_gone', building: a.building };
|
|
133
|
+
}
|
|
134
|
+
const out = await db.applyLeadAct({ itemId: closed.id, action: a });
|
|
135
|
+
return {
|
|
136
|
+
action: a.type,
|
|
137
|
+
done: out.done === true,
|
|
138
|
+
building: a.building,
|
|
139
|
+
...(a.type === 'appoint_lead' ? { builder_id: String(a.builder_id) } : {}),
|
|
140
|
+
...(out.reason ? { reason: out.reason } : {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// A passed build_out (task 1004711). Re-validated first (the stored row was
|
|
145
|
+
// checked when opened and is immutable since). Never throws past the caller's catch.
|
|
146
|
+
async function actOnBuildOutMatter(closed, matter, area) {
|
|
147
|
+
const v = validateMatterAction(area, matter.action);
|
|
148
|
+
if (!v.ok) return { action: 'build_out', done: false, reason: 'bad_action' };
|
|
149
|
+
const a = v.action;
|
|
150
|
+
const out = await db.insertBuildout({
|
|
151
|
+
building: a.building, capsule: a.capsule, name: a.name, line: a.line,
|
|
152
|
+
builtBy: matter.proposed_by == null ? null : matter.proposed_by, itemId: closed.id,
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
action: 'build_out',
|
|
156
|
+
done: out.done === true,
|
|
157
|
+
building: a.building,
|
|
158
|
+
capsule: a.capsule,
|
|
159
|
+
...(out.reason ? { reason: out.reason } : {}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
114
163
|
// ── who decides what: proposing a change (task 1004529) ─────────────────────
|
|
115
164
|
//
|
|
116
165
|
// A change to who decides what at a building is an AMENDMENT — of kind
|
|
@@ -180,11 +229,15 @@ const MATTER_DETAIL_MAX = 5000;
|
|
|
180
229
|
// contributed to 'governor.matterAct' as { area, type, act }. This table is only
|
|
181
230
|
// the SHAPE of each act (field → kind), checked when the matter is opened and
|
|
182
231
|
// again before it acts. 'id' is a positive integer, 'rank' a custom rank key,
|
|
183
|
-
// 'usd' a non-negative amount or null (no limit)
|
|
232
|
+
// 'usd' a non-negative amount or null (no limit), 'building' one of the nine,
|
|
233
|
+
// 'charter' a Lead's one-line charter (absent: the building's default).
|
|
184
234
|
const MATTER_ACTIONS = Object.freeze({
|
|
185
235
|
people: Object.freeze({
|
|
186
236
|
assign_rank: Object.freeze({ builder_id: 'id', rank_key: 'rank' }),
|
|
187
237
|
unassign_rank: Object.freeze({ builder_id: 'id', rank_key: 'rank' }),
|
|
238
|
+
// task 1004709: the Lead of a building, appointed and removed by vote
|
|
239
|
+
appoint_lead: Object.freeze({ building: 'building', builder_id: 'id', charter: 'charter' }),
|
|
240
|
+
remove_lead: Object.freeze({ building: 'building' }),
|
|
188
241
|
}),
|
|
189
242
|
automation: Object.freeze({
|
|
190
243
|
switch_on: Object.freeze({}),
|
|
@@ -205,7 +258,12 @@ const MATTER_ACTIONS = Object.freeze({
|
|
|
205
258
|
const MATTER_ACT_POINT = 'governor.matterAct';
|
|
206
259
|
const MAX_BUDGET_USD = 100000;
|
|
207
260
|
|
|
208
|
-
function fieldValue(kind, raw) {
|
|
261
|
+
function fieldValue(kind, raw, action) {
|
|
262
|
+
if (kind === 'building') return decisionRules.AREA_KEYS.includes(raw) ? { ok: true, v: raw } : { ok: false };
|
|
263
|
+
if (kind === 'charter') {
|
|
264
|
+
const line = charterLine(raw, action && action.building);
|
|
265
|
+
return line ? { ok: true, v: line } : { ok: false };
|
|
266
|
+
}
|
|
209
267
|
if (kind === 'id') {
|
|
210
268
|
const n = Number(raw);
|
|
211
269
|
return Number.isSafeInteger(n) && n > 0 && raw !== null && raw !== '' && typeof raw !== 'boolean' ? { ok: true, v: n } : { ok: false };
|
|
@@ -220,6 +278,17 @@ function fieldValue(kind, raw) {
|
|
|
220
278
|
|
|
221
279
|
function validateMatterAction(area, action) {
|
|
222
280
|
if (action == null) return { ok: true, action: null };
|
|
281
|
+
// A build-out (task 1004711) is a matter at the building it adds to — the
|
|
282
|
+
// Charter's at People (buildouts.matterAreaFor) — carried out here, at every
|
|
283
|
+
// building, whichever modules are on.
|
|
284
|
+
if (action && typeof action === 'object' && action.type === 'build_out') {
|
|
285
|
+
const b = buildouts.checkBuildOut(action);
|
|
286
|
+
if (!b.ok) return { ok: false, reason: b.reason === 'unknown_field' ? 'unknown_field' : 'bad_action', field: b.field ? `action.${b.field}` : undefined };
|
|
287
|
+
if (buildouts.matterAreaFor(b.buildout.building) !== area) {
|
|
288
|
+
return { ok: false, reason: 'bad_action', field: 'action.building', expected: `a build-out is a matter at the building it adds to (${buildouts.matterAreaFor(b.buildout.building)})` };
|
|
289
|
+
}
|
|
290
|
+
return { ok: true, action: b.buildout };
|
|
291
|
+
}
|
|
223
292
|
const acts = MATTER_ACTIONS[area];
|
|
224
293
|
if (!acts) return { ok: false, reason: 'action_not_supported_here', expected: `a matter acts by itself only at ${Object.keys(MATTER_ACTIONS).join(', ')}` };
|
|
225
294
|
if (typeof action !== 'object' || Array.isArray(action)) return { ok: false, reason: 'bad_action' };
|
|
@@ -230,8 +299,9 @@ function validateMatterAction(area, action) {
|
|
|
230
299
|
}
|
|
231
300
|
const out = { type: action.type };
|
|
232
301
|
for (const [k, kind] of Object.entries(shape)) {
|
|
233
|
-
|
|
234
|
-
|
|
302
|
+
// a Lead's charter may be left out: the building's default is filed in its place
|
|
303
|
+
if (!Object.prototype.hasOwnProperty.call(action, k) && kind !== 'charter') return { ok: false, reason: 'bad_action', field: `action.${k}` };
|
|
304
|
+
const f = fieldValue(kind, action[k], action);
|
|
235
305
|
if (!f.ok) return { ok: false, reason: 'bad_action', field: `action.${k}` };
|
|
236
306
|
out[k] = f.v;
|
|
237
307
|
}
|
|
@@ -256,6 +326,36 @@ async function openMatter({ area, title, detailMd = null, action = null, propose
|
|
|
256
326
|
const { board } = loadGovernmentConfig();
|
|
257
327
|
const rows = await db.listDecisionRules();
|
|
258
328
|
const resolved = decisionRules.resolveAll(rows, { board }).find((r) => r.area === area);
|
|
329
|
+
// A Lead is the government's to appoint (task 1004709, ADR 0374): where People
|
|
330
|
+
// has no sitting of its own (decided as today), the matter sits under the board
|
|
331
|
+
// in force, like an amendment; where People names a sitting, it sits there.
|
|
332
|
+
const leadAct = !!(a.action && area === 'people' && isLeadAct(a.action.type));
|
|
333
|
+
// A build-out (task 1004711, ADR 0378) is the government's to allow, like a
|
|
334
|
+
// Lead: it sits under the board in force where its building has no sitting of
|
|
335
|
+
// its own, and ALWAYS when it is a district (a new part of the city) or the
|
|
336
|
+
// Charter's own (the board's building); otherwise at the building's sitting —
|
|
337
|
+
// the same places build-rights.js says the vote goes.
|
|
338
|
+
const isBuildOut = !!(a.action && a.action.type === 'build_out');
|
|
339
|
+
// Only the building's holder may ask, by this path too (ADR 0378 D2): a
|
|
340
|
+
// build-out filed straight at the matters route meets the same wall.
|
|
341
|
+
if (isBuildOut) {
|
|
342
|
+
const notYours = await buildOutHolderRefusal(a.action.building, proposedBy);
|
|
343
|
+
if (notYours) return notYours;
|
|
344
|
+
}
|
|
345
|
+
const boardBuilds = isBuildOut && (resolved.as_today || a.action.capsule === 'district' || a.action.building === 'charter');
|
|
346
|
+
if ((leadAct && resolved.as_today) || boardBuilds) {
|
|
347
|
+
const refused = leadAct ? await leadMatterRefusal(a.action) : null;
|
|
348
|
+
if (refused) return refused;
|
|
349
|
+
const matter = await db.createMatter({ area, title: title.trim(), detailMd, action: a.action, proposedBy });
|
|
350
|
+
const item = await db.openBoardItem({
|
|
351
|
+
subjectType: decisionRules.MATTER_SUBJECT_TYPES[area],
|
|
352
|
+
subjectId: matter.id,
|
|
353
|
+
openedBy: proposedBy,
|
|
354
|
+
constitution: board,
|
|
355
|
+
windowMinutes: board.window_minutes,
|
|
356
|
+
});
|
|
357
|
+
return { ok: true, matter, item, decided: { ...resolved, sat_under: 'board' } };
|
|
358
|
+
}
|
|
259
359
|
if (resolved.as_today) {
|
|
260
360
|
return {
|
|
261
361
|
ok: false, reason: 'decided_by_gate', area, gate: resolved.gate,
|
|
@@ -263,10 +363,13 @@ async function openMatter({ area, title, detailMd = null, action = null, propose
|
|
|
263
363
|
};
|
|
264
364
|
}
|
|
265
365
|
if (!resolved.sitting) return { ok: false, reason: 'no_one_holds_this', area, what_this_means: `No one holds ${resolved.name} yet, so nothing there can be decided.` };
|
|
266
|
-
if (a.action && area !== 'people' && !matterActFor(area, a.action.type)) {
|
|
366
|
+
if (a.action && area !== 'people' && !isBuildOut && !matterActFor(area, a.action.type)) {
|
|
267
367
|
return { ok: false, reason: 'action_not_wired', area, what_this_means: `Nothing at ${resolved.name} can carry that out right now, so it would pass and do nothing. Turn on the module that owns ${resolved.name}, or file the matter without an act.` };
|
|
268
368
|
}
|
|
269
|
-
if (
|
|
369
|
+
if (leadAct) {
|
|
370
|
+
const refused = await leadMatterRefusal(a.action);
|
|
371
|
+
if (refused) return refused;
|
|
372
|
+
} else if (a.action && area === 'people' && !isBuildOut) {
|
|
270
373
|
const rank = await db.getRankByKey(a.action.rank_key);
|
|
271
374
|
if (!rank) return { ok: false, reason: 'unknown_rank', rank_key: a.action.rank_key };
|
|
272
375
|
if (rank.is_system) return { ok: false, reason: 'seeded_rank', rank_key: a.action.rank_key, expected: 'a custom rank — a seeded rank follows the builder\'s rank' };
|
|
@@ -285,6 +388,21 @@ async function openMatter({ area, title, detailMd = null, action = null, propose
|
|
|
285
388
|
return { ok: true, matter, item, decided: resolved };
|
|
286
389
|
}
|
|
287
390
|
|
|
391
|
+
// Why a Lead matter cannot be filed, or null when it can (task 1004709): the
|
|
392
|
+
// appointee must be an active builder and not already lead the building on the
|
|
393
|
+
// same charter; a removal needs a Lead in office.
|
|
394
|
+
async function leadMatterRefusal(action) {
|
|
395
|
+
const inOffice = (await db.listLeads()).find((l) => l.building === action.building) || null;
|
|
396
|
+
if (action.type === 'remove_lead') {
|
|
397
|
+
return inOffice ? null : { ok: false, reason: 'no_lead', area: 'people', building: action.building, what_this_means: `${decisionRules.areaByKey(action.building).name} has no Lead to remove.` };
|
|
398
|
+
}
|
|
399
|
+
if (!(await db.getActiveBuilder(action.builder_id))) return { ok: false, reason: 'unknown_builder', builder_id: String(action.builder_id) };
|
|
400
|
+
if (inOffice && String(inOffice.builder_id) === String(action.builder_id) && inOffice.charter === action.charter) {
|
|
401
|
+
return { ok: false, reason: 'already_lead', area: 'people', building: action.building, what_this_means: 'They already lead it, on that charter.' };
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
288
406
|
// May this builder act ALONE at this building — the check a building's own
|
|
289
407
|
// direct route runs before acting (People's rank assignments here; every other
|
|
290
408
|
// building's routes through the doorway's requireActAlone, task 1004657 — the
|
|
@@ -332,6 +450,112 @@ async function announcePassedMatter(acted) {
|
|
|
332
450
|
|
|
333
451
|
// ── the reads board.js shows ────────────────────────────────────────────────
|
|
334
452
|
|
|
453
|
+
// GET /government/leads (task 1004709): the nine buildings, each with its Lead in
|
|
454
|
+
// office or null, and who answers for it (its Lead, or the owner when unseated).
|
|
455
|
+
// Read per request, uncached.
|
|
456
|
+
//
|
|
457
|
+
// Each building also carries `build_rights` (task 1004710): what its holder may
|
|
458
|
+
// build out without a vote, from the FORM OF GOVERNMENT in force and the
|
|
459
|
+
// building's own decision row (build-rights.js is the rule). The context is
|
|
460
|
+
// resolved here, server-side and uncached (ADR 0016): the board block, the
|
|
461
|
+
// resolved decision table, who the board seats, and who each 'decide' / 'first'
|
|
462
|
+
// sitting seats. Every part FAILS CLOSED: an unreadable part reads as "a vote",
|
|
463
|
+
// never as "at once". No goal stores build terms yet, so the goals hook is [].
|
|
464
|
+
async function buildRightsContext() {
|
|
465
|
+
let board = {};
|
|
466
|
+
// read through the module object (not the destructured name) so a test can
|
|
467
|
+
// hand it a constitution
|
|
468
|
+
try { board = require('./config').loadGovernmentConfig().board || {}; } catch (_) { board = {}; }
|
|
469
|
+
const decisions = (await decisionRulesFor(board)) || [];
|
|
470
|
+
const idsOf = async (membership) => {
|
|
471
|
+
try { return (await resolveBoardMembers({ membership })).map((m) => String(m.builder_id)); } catch (_) { return []; }
|
|
472
|
+
};
|
|
473
|
+
const boardMemberIds = await idsOf(board.membership);
|
|
474
|
+
const sittingMemberIds = {};
|
|
475
|
+
// only a sitting whose rule lets a seated member act alone can let its holder
|
|
476
|
+
// build at once, so only those are asked who they seat
|
|
477
|
+
const actsAlone = decisionRules.DECISION_RULES.filter((r) => r.actsAlone).map((r) => r.key);
|
|
478
|
+
for (const d of decisions) {
|
|
479
|
+
if (d.sitting && actsAlone.includes(d.rule)) sittingMemberIds[d.area] = await idsOf(d.decided_by);
|
|
480
|
+
}
|
|
481
|
+
return { board, decisions, boardMemberIds, sittingMemberIds, goals: [] };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Each building also carries its `structure` (task 1004711): the build-outs on
|
|
485
|
+
// the record (built, or going up) and the ones waiting on a vote. A failed read
|
|
486
|
+
// of either leaves the building's structure out (null), never invented.
|
|
487
|
+
// `structure: false` skips the build-out reads (a caller that needs only who
|
|
488
|
+
// answers for a building and its build_rights).
|
|
489
|
+
async function leadsView({ includePast = false, now = Date.now(), structure: withStructure = true } = {}) {
|
|
490
|
+
const [rows, archons, rights, standing, pending] = await Promise.all([
|
|
491
|
+
db.listLeads({ includePast }), db.listActiveArchons(), buildRightsContext(),
|
|
492
|
+
withStructure ? db.listBuildouts().catch(() => null) : null, withStructure ? db.listPendingBuildouts().catch(() => null) : null,
|
|
493
|
+
]);
|
|
494
|
+
const structure = standing && pending ? { rows: standing, pending, now } : null;
|
|
495
|
+
return leadsBook(rows, archons, { includePast, rights, structure });
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ── a Lead's build-outs (task 1004711, ADR 0378) ────────────────────────────
|
|
499
|
+
|
|
500
|
+
// The refusal when someone other than the building's holder asks, with where to go instead.
|
|
501
|
+
function notTheHolder(building, leadName) {
|
|
502
|
+
const area = decisionRules.areaByKey(building);
|
|
503
|
+
const name = area ? area.name : 'This building';
|
|
504
|
+
return {
|
|
505
|
+
ok: false, reason: 'not_the_holder', building,
|
|
506
|
+
what_this_means: leadName
|
|
507
|
+
? `${name} is led by ${leadName}, and only they grow it. Ask them to put it up.`
|
|
508
|
+
: `${name} has no Lead, so only the owner grows it. Ask the owner to put it up.`,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Only the building's holder may ask for a build-out, whichever path files it
|
|
513
|
+
// (answerFor, the one rule): null when `builderId` holds it, else the refusal.
|
|
514
|
+
async function buildOutHolderRefusal(building, builderId) {
|
|
515
|
+
const [rows, archons] = await Promise.all([db.listLeads(), db.listActiveArchons()]);
|
|
516
|
+
const answers = answerFor(building, rows, ownerOf(archons));
|
|
517
|
+
if (answers && answers.builder_ids.map(String).includes(String(builderId))) return null;
|
|
518
|
+
const lead = (rows || []).find((l) => l && l.building === building && !l.removed_at) || null;
|
|
519
|
+
return notTheHolder(building, lead ? lead.display_name || lead.github_login || 'its Lead' : null);
|
|
520
|
+
}
|
|
521
|
+
//
|
|
522
|
+
// POST /government/buildouts. The building's HOLDER (its Lead, or the owner
|
|
523
|
+
// seat when unseated — leads.answerFor) asks for a build-out; build-rights.js,
|
|
524
|
+
// read off the same server-side view GET /government/leads serves, says whether
|
|
525
|
+
// it goes up at once or goes to a vote. At once: put on the record now, with the
|
|
526
|
+
// rule's ground and why. A vote: filed as a build_out matter through openMatter,
|
|
527
|
+
// the same path POST /government/board/matters runs (the caller, board.js, adds
|
|
528
|
+
// the window-open nudge). Fails closed: rights that cannot be read are a vote.
|
|
529
|
+
//
|
|
530
|
+
// Returns { ok: true, path: 'at_once', buildout, rights } |
|
|
531
|
+
// { ok: true, path: 'vote', matter, item, decided, rights } |
|
|
532
|
+
// { ok: false, reason, … } ('bad_buildout', 'unknown_field', 'not_the_holder', openMatter's).
|
|
533
|
+
async function buildOut({ buildout, builderId } = {}) {
|
|
534
|
+
const b = buildouts.checkBuildOut({ type: 'build_out', ...(buildout || {}) });
|
|
535
|
+
if (!b.ok) return { ok: false, reason: b.reason, field: b.field };
|
|
536
|
+
if (builderId == null || builderId === '') return { ok: false, reason: 'missing_proposer' };
|
|
537
|
+
const a = b.buildout;
|
|
538
|
+
const view = await leadsView({ structure: false });
|
|
539
|
+
const here = view.buildings.find((x) => x.building === a.building) || null;
|
|
540
|
+
const holders = ((here && here.answers && here.answers.builder_ids) || []).map(String);
|
|
541
|
+
if (!holders.includes(String(builderId))) return notTheHolder(a.building, here && here.lead ? here.lead.builder.name : null);
|
|
542
|
+
const rights = here.build_rights && here.build_rights.capsules ? here.build_rights.capsules[a.capsule] || null : null;
|
|
543
|
+
if (rights && rights.without_vote === true && a.capsule !== 'district') {
|
|
544
|
+
const out = await db.insertBuildout({
|
|
545
|
+
building: a.building, capsule: a.capsule, name: a.name, line: a.line,
|
|
546
|
+
builtBy: builderId, ground: rights.ground, why: rights.why,
|
|
547
|
+
});
|
|
548
|
+
return { ok: true, path: 'at_once', buildout: out.buildout || null, rights };
|
|
549
|
+
}
|
|
550
|
+
const what = { committee: 'a committee room', floor: 'a new floor', annex: 'an annex', district: 'a district of its own' }[a.capsule];
|
|
551
|
+
const title = a.capsule === 'district' ? `Grow ${here.name} into a district: ${a.name}` : `Add ${what} to ${here.name}: ${a.name}`;
|
|
552
|
+
const filed = await openMatter({
|
|
553
|
+
area: buildouts.matterAreaFor(a.building), title: title.slice(0, MATTER_TITLE_MAX), detailMd: a.line || null, action: a, proposedBy: builderId,
|
|
554
|
+
});
|
|
555
|
+
if (!filed.ok) return { ...filed, rights };
|
|
556
|
+
return { ok: true, path: 'vote', matter: filed.matter, item: filed.item, decided: filed.decided, rights };
|
|
557
|
+
}
|
|
558
|
+
|
|
335
559
|
// The Board Room's matter subjects, keyed by id. No query when no matter is on
|
|
336
560
|
// the board, so a project that never amended itself reads exactly what it did.
|
|
337
561
|
async function matterSubjectsById(items) {
|
|
@@ -386,4 +610,6 @@ module.exports = {
|
|
|
386
610
|
MATTER_ACT_POINT,
|
|
387
611
|
openMatter,
|
|
388
612
|
checkActAlone,
|
|
613
|
+
leadsView,
|
|
614
|
+
buildOut,
|
|
389
615
|
};
|
|
@@ -115,6 +115,8 @@ const AREAS = Object.freeze([
|
|
|
115
115
|
Object.freeze({ key: 'treasury', name: 'Treasury', gate: 'builder.budget.set',
|
|
116
116
|
today: 'whoever may set spending limits', acts: Object.freeze([
|
|
117
117
|
'PATCH /builders/:id/budget',
|
|
118
|
+
// task 1004712: the project's optional spend cap (off by default: no limit)
|
|
119
|
+
'PATCH /project-settings/spend-cap',
|
|
118
120
|
]) }),
|
|
119
121
|
]);
|
|
120
122
|
const AREA_KEYS = Object.freeze(AREAS.map((a) => a.key));
|
|
@@ -83,6 +83,9 @@ const resolver = require('./resolver');
|
|
|
83
83
|
const decisionRules = require('./decision-rules');
|
|
84
84
|
const docketFloors = require('./docket-floors');
|
|
85
85
|
const governmentConfig = require('./config');
|
|
86
|
+
const asksDb = require('./asks-db');
|
|
87
|
+
const routing = require('./routing');
|
|
88
|
+
const { ownerOf } = require('./leads');
|
|
86
89
|
|
|
87
90
|
const log = api.logger('government');
|
|
88
91
|
|
|
@@ -108,6 +111,8 @@ const EXPECTED_SOURCES = Object.freeze([
|
|
|
108
111
|
Object.freeze({ source: 'gate_approvals', building: 'release', label: 'CI gate approvals' }),
|
|
109
112
|
Object.freeze({ source: 'autobongos_fence', building: 'automation', label: 'the Autobongos fence' }),
|
|
110
113
|
Object.freeze({ source: 'module_rechecks', building: 'release', label: 'module re-checks' }),
|
|
114
|
+
// task 1004712: a dry pot's borrow from the reserve, a treasury matter (treasury.js)
|
|
115
|
+
Object.freeze({ source: 'treasury_spending', building: 'treasury', label: 'spending matters' }),
|
|
111
116
|
]);
|
|
112
117
|
|
|
113
118
|
// Named by the design, not yet recorded anywhere. Each says why, in words.
|
|
@@ -116,8 +121,6 @@ const COMING_SOURCES = Object.freeze([
|
|
|
116
121
|
reason: 'There is no way to appeal a grade yet.' }),
|
|
117
122
|
Object.freeze({ source: 'standing_rules', building: 'rules', label: 'standing rules',
|
|
118
123
|
reason: 'A new standing rule has nowhere to be proposed yet. The one rule in force, idea routing, is counted in the rules-at-work reading.' }),
|
|
119
|
-
Object.freeze({ source: 'treasury_spending', building: 'treasury', label: 'spending matters',
|
|
120
|
-
reason: 'The project has no budget or spending votes recorded yet.' }),
|
|
121
124
|
]);
|
|
122
125
|
|
|
123
126
|
// Floors the design names whose engine does not exist yet. Drawn, and say why.
|
|
@@ -339,7 +342,9 @@ function countStates(matters) {
|
|
|
339
342
|
return c;
|
|
340
343
|
}
|
|
341
344
|
|
|
342
|
-
// The home line under "start governing", in the design's words.
|
|
345
|
+
// The home line under "start governing", in the design's words. `counts.needs`
|
|
346
|
+
// is what waits on the viewer: on a routed docket (task 1004705) the matters on
|
|
347
|
+
// the viewer's own seats, not every matter that needs somebody.
|
|
343
348
|
function homeLine(counts) {
|
|
344
349
|
if (!counts.needs && !counts.moving) return 'nothing waits on you.';
|
|
345
350
|
const parts = [];
|
|
@@ -519,6 +524,28 @@ function registeredContributions() {
|
|
|
519
524
|
return { sources, readings, floors, histories, desks };
|
|
520
525
|
}
|
|
521
526
|
|
|
527
|
+
// ── the ask each matter carries (task 1004703) ───────────────────────────────
|
|
528
|
+
|
|
529
|
+
// A matter an agent put to a person carries its ASK (asks.js): the question, two
|
|
530
|
+
// or three tap answers, the pick and why, the seat it waits on and what happens
|
|
531
|
+
// if nobody answers, stored per matter id in government_asks. Attached only to
|
|
532
|
+
// the matters this viewer already sees, so an ask is never more visible than its
|
|
533
|
+
// matter. A read that fails or is slow attaches none: the city still stands, and
|
|
534
|
+
// each matter keeps its own words.
|
|
535
|
+
async function attachAsks(matters, readAsks, timeoutMs) {
|
|
536
|
+
if (typeof readAsks !== 'function' || !matters.length) return;
|
|
537
|
+
try {
|
|
538
|
+
const found = await withTimeout(Promise.resolve(readAsks(matters.map((m) => m.id))), timeoutMs);
|
|
539
|
+
if (!found || typeof found.get !== 'function') return;
|
|
540
|
+
for (const m of matters) {
|
|
541
|
+
const ask = found.get(m.id);
|
|
542
|
+
if (ask) m.ask = ask;
|
|
543
|
+
}
|
|
544
|
+
} catch (err) {
|
|
545
|
+
log.warn({ err: err && err.message }, 'governor docket: asks unreadable');
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
522
549
|
// ── the collector ────────────────────────────────────────────────────────────
|
|
523
550
|
|
|
524
551
|
// Build the docket for one viewer. `permissions` is the viewer's resolved set;
|
|
@@ -529,7 +556,12 @@ function registeredContributions() {
|
|
|
529
556
|
// rather than a guess.
|
|
530
557
|
// `government` is what docketFor() read once for this module's own floors and
|
|
531
558
|
// record (loadGovernment below); null leaves those floors `unavailable`.
|
|
532
|
-
|
|
559
|
+
// `readAsks(matterIds)` → Map(matter id → ask) reads the stored asks (task
|
|
560
|
+
// 1004703, asks-db.js asksFor); null attaches none.
|
|
561
|
+
// `seats` is who sits in each seat (loadSeats below: Leads in office, the owner
|
|
562
|
+
// seat, each building's holders); given, every matter carries `route` (routing.js,
|
|
563
|
+
// task 1004705) and the docket says `routed: true`. null routes nothing.
|
|
564
|
+
async function buildDocket({ viewer, permissions, contributions = registeredContributions(), now = Date.now(), timeoutMs = SOURCE_TIMEOUT_MS, decisions = null, government = null, readAsks = null, seats = null } = {}) {
|
|
533
565
|
const held = new Set(permissions || []);
|
|
534
566
|
const ctx = Object.freeze({
|
|
535
567
|
builderId: String(viewer.id),
|
|
@@ -581,6 +613,9 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
|
|
|
581
613
|
const order = declared.map((d) => d.source);
|
|
582
614
|
sourceStatus.sort((a, b) => order.indexOf(a.source) - order.indexOf(b.source));
|
|
583
615
|
matters.sort(compareMatters);
|
|
616
|
+
await attachAsks(matters, readAsks, timeoutMs);
|
|
617
|
+
// THE DIRECTION DESK (task 1004705): who each matter waits on, from real seats.
|
|
618
|
+
const yours = seats ? routing.routeAll(matters, ctx.builderId, seats, now) : null;
|
|
584
619
|
|
|
585
620
|
// Readings: contributed + this module's own + the ones derived from matters.
|
|
586
621
|
const readingImpls = new Map(contributions.readings);
|
|
@@ -689,6 +724,7 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
|
|
|
689
724
|
historySources.sort((a, b) => (a.source < b.source ? -1 : 1));
|
|
690
725
|
|
|
691
726
|
const counts = countStates(matters);
|
|
727
|
+
if (yours != null) counts.yours = yours;
|
|
692
728
|
const decisionBy = new Map((Array.isArray(decisions) ? decisions : []).filter(Boolean).map((d) => [d.area, d]));
|
|
693
729
|
const holdersBy = govAllowed && government && government.holders ? government.holders : {};
|
|
694
730
|
const buildings = vocab.BUILDINGS.map((b) => {
|
|
@@ -712,7 +748,8 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
|
|
|
712
748
|
viewer: { id: ctx.builderId },
|
|
713
749
|
generated_at: new Date(now).toISOString(),
|
|
714
750
|
counts,
|
|
715
|
-
line: homeLine(counts),
|
|
751
|
+
line: homeLine(yours != null ? { needs: yours, moving: counts.moving } : counts),
|
|
752
|
+
routed: yours != null,
|
|
716
753
|
matters,
|
|
717
754
|
buildings,
|
|
718
755
|
sources: sourceStatus,
|
|
@@ -729,6 +766,41 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
|
|
|
729
766
|
};
|
|
730
767
|
}
|
|
731
768
|
|
|
769
|
+
// ONE matter, as this viewer's docket would show it (task 1004703): the write
|
|
770
|
+
// side of an ask may only reach a matter its writer can see. Reads that one
|
|
771
|
+
// source under the gate it declared — the same gate the docket applies — and
|
|
772
|
+
// finds the matter in it. → { matter } | { reason: 'no_such_source' |
|
|
773
|
+
// 'not_yours' | 'not_found' | 'unavailable' }. Pure over its inputs, like
|
|
774
|
+
// buildDocket.
|
|
775
|
+
async function findMatter({ viewer, permissions, matterId, contributions = registeredContributions(), now = Date.now(), timeoutMs = SOURCE_TIMEOUT_MS } = {}) {
|
|
776
|
+
const id = String(matterId || '');
|
|
777
|
+
const cut = id.indexOf(':');
|
|
778
|
+
const source = cut > 0 ? id.slice(0, cut) : '';
|
|
779
|
+
const decl = EXPECTED_SOURCES.find((d) => d.source === source);
|
|
780
|
+
const c = source === 'board' ? BOARD_SOURCE : new Map(contributions.sources).get(source);
|
|
781
|
+
if (!decl || !c) return { reason: 'no_such_source' };
|
|
782
|
+
const held = new Set(permissions || []);
|
|
783
|
+
const ctx = Object.freeze({ builderId: String(viewer.id), rank: viewer.rank || null, has: (p) => held.has(p) });
|
|
784
|
+
if (!gateAllows(c.gate, ctx)) return { reason: 'not_yours' };
|
|
785
|
+
const memo = new Map();
|
|
786
|
+
const once = (key, fn) => {
|
|
787
|
+
if (!memo.has(key)) memo.set(key, Promise.resolve().then(fn));
|
|
788
|
+
return memo.get(key);
|
|
789
|
+
};
|
|
790
|
+
try {
|
|
791
|
+
const raw = await withTimeout(Promise.resolve(c.read(ctx, { now, once })), timeoutMs);
|
|
792
|
+
const matter = (Array.isArray(raw) ? raw : []).map((m) => normalizeMatter(m, { source, building: c.building || decl.building })).find((m) => m && m.id === id);
|
|
793
|
+
return matter ? { matter } : { reason: 'not_found' };
|
|
794
|
+
} catch (err) {
|
|
795
|
+
log.warn({ source, err: err && err.message }, 'governor docket: a matter could not be read');
|
|
796
|
+
return { reason: 'unavailable' };
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
async function findMatterFor(builder, matterId) {
|
|
800
|
+
const permissions = await resolver.resolveBuilderPermissions(builder.id);
|
|
801
|
+
return findMatter({ viewer: builder, permissions, matterId });
|
|
802
|
+
}
|
|
803
|
+
|
|
732
804
|
// The route's entry point: resolve the viewer's permissions server-side (fresh,
|
|
733
805
|
// uncached — ADR 0016), then build.
|
|
734
806
|
async function docketFor(builder) {
|
|
@@ -740,7 +812,38 @@ async function docketFor(builder) {
|
|
|
740
812
|
return null;
|
|
741
813
|
});
|
|
742
814
|
const government = permissions.includes(GOVERNMENT_GATE.permission) ? await loadGovernment({ decisions }) : null;
|
|
743
|
-
|
|
815
|
+
const seats = government ? await loadSeats(government) : null;
|
|
816
|
+
return buildDocket({ viewer: builder, permissions, decisions, government, readAsks: asksDb.asksFor, seats });
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Who sits in each seat now (task 1004705), read per request, uncached (ADR
|
|
820
|
+
// 0016): the Leads in office, the owner seat (every active Archon) and each
|
|
821
|
+
// building's holders as loadGovernment resolved them. A read that fails routes
|
|
822
|
+
// nothing — the docket then says `routed: false` rather than guess whose it is.
|
|
823
|
+
async function loadSeats(government) {
|
|
824
|
+
try {
|
|
825
|
+
const [leads, archons] = await Promise.all([db.listLeads(), db.listActiveArchons()]);
|
|
826
|
+
return { leads, owner: ownerOf(archons), holders: (government && government.holders) || {} };
|
|
827
|
+
} catch (err) {
|
|
828
|
+
log.warn({ err: err && err.message }, 'governor docket: seats unreadable, nothing routed');
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// One matter with its stored ask and the seats, for passing it on to `toSeat`
|
|
834
|
+
// (task 1004705): → { matter, seats } | { reason } as findMatter answers. A
|
|
835
|
+
// board's holders are read only when a board seat is involved.
|
|
836
|
+
async function matterWithSeats(builder, matterId, toSeat = '') {
|
|
837
|
+
const found = await findMatterFor(builder, matterId);
|
|
838
|
+
if (!found.matter) return found;
|
|
839
|
+
const ask = (await asksDb.asksFor([found.matter.id])).get(found.matter.id);
|
|
840
|
+
if (ask) found.matter.ask = ask;
|
|
841
|
+
const boardSeat = [toSeat, ask && ask.waits_on].some((s) => /^board:/.test(String(s || '')));
|
|
842
|
+
const government = boardSeat
|
|
843
|
+
? await loadGovernment({ decisions: await board.decisionRulesView().catch(() => null) })
|
|
844
|
+
: { holders: {} };
|
|
845
|
+
const seats = await loadSeats(government);
|
|
846
|
+
return seats ? { matter: found.matter, seats } : { reason: 'unavailable' };
|
|
744
847
|
}
|
|
745
848
|
|
|
746
849
|
// This module's own floors and record, read ONCE per request (task 1004527),
|
|
@@ -809,4 +912,9 @@ module.exports = {
|
|
|
809
912
|
registeredContributions,
|
|
810
913
|
buildDocket,
|
|
811
914
|
docketFor,
|
|
915
|
+
attachAsks,
|
|
916
|
+
findMatter,
|
|
917
|
+
findMatterFor,
|
|
918
|
+
loadSeats,
|
|
919
|
+
matterWithSeats,
|
|
812
920
|
};
|