@bongos/core 1.21.112 → 1.21.114
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +616 -186
- package/.claude/skills/ask-for-help/SKILL.md +1 -1
- package/.claude/skills/bongos-feedback/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -0
- package/GOVERNMENT.md +128 -11
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +28 -0
- package/clients/bongos-client/index.cjs +28 -0
- package/clients/bongos-client/index.d.ts +45 -2
- package/clients/bongos-client/index.mjs +28 -0
- package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
- package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
- package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
- package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
- package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
- package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
- package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
- package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
- package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
- package/docs/adr/README.md +10 -1
- package/docs/api/openapi.json +989 -87
- package/docs/api-reference.md +18 -4
- package/docs/copy-inventory.md +343 -262
- package/docs/copy-registry.json +1223 -426
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/packs/engineer.md +2 -1
- package/docs/page-inventory.json +91 -6
- package/docs/page-readings.json +5017 -4458
- package/modules/builder-settings/law-locks.js +202 -0
- package/modules/builder-settings/routes/settings.js +66 -6
- package/modules/builder-settings/settings.js +7 -0
- package/modules/discord/ship-broadcast.js +22 -13
- package/modules/government/asks-db.js +54 -0
- package/modules/government/asks.js +269 -0
- package/modules/government/board-idea-reads.js +68 -0
- package/modules/government/board.js +21 -57
- package/modules/government/build-rights.js +148 -0
- package/modules/government/buildouts.js +160 -0
- package/modules/government/counts.js +81 -0
- package/modules/government/db.js +242 -0
- package/modules/government/decision-board.js +234 -8
- package/modules/government/decision-rules.js +2 -0
- package/modules/government/docket.js +114 -6
- package/modules/government/law-board.js +131 -0
- package/modules/government/laws.js +241 -0
- package/modules/government/leads.js +170 -0
- package/modules/government/migrations/government_027_laws.sql +143 -0
- package/modules/government/migrations/government_028_asks.sql +57 -0
- package/modules/government/migrations/government_029_leads.sql +135 -0
- package/modules/government/migrations/government_030_buildouts.sql +111 -0
- package/modules/government/migrations/government_031_treasury.sql +107 -0
- package/modules/government/module.json +2 -1
- package/modules/government/pollers/treasury-borrow.js +62 -0
- package/modules/government/positions.js +107 -0
- package/modules/government/presets.js +149 -0
- package/modules/government/routes/government.js +425 -2
- package/modules/government/routing.js +154 -0
- package/modules/government/treasury-db.js +166 -0
- package/modules/government/treasury.js +241 -0
- package/modules/hall-ui/public/atlas.html +2 -2
- package/modules/hall-ui/public/blockers.html +2 -2
- package/modules/hall-ui/public/board-room.html +2 -2
- package/modules/hall-ui/public/board-room.js +2 -0
- package/modules/hall-ui/public/city-draw.js +604 -11
- package/modules/hall-ui/public/city.css +157 -1
- package/modules/hall-ui/public/city.html +37 -7
- package/modules/hall-ui/public/city.js +441 -21
- package/modules/hall-ui/public/city.states.json +110 -5
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/commands.html +2 -2
- package/modules/hall-ui/public/copy-desk.html +2 -2
- package/modules/hall-ui/public/craft-mode.js +112 -29
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/diagram-viewer.css +12 -0
- package/modules/hall-ui/public/diagram-viewer.js +4 -0
- package/modules/hall-ui/public/diagrams.html +2 -2
- package/modules/hall-ui/public/drachmae.html +2 -2
- package/modules/hall-ui/public/fleet.html +2 -2
- package/modules/hall-ui/public/gate.html +2 -2
- package/modules/hall-ui/public/goal-map.html +2 -2
- package/modules/hall-ui/public/goals.html +2 -2
- package/modules/hall-ui/public/gov-feeds.js +141 -0
- package/modules/hall-ui/public/gov-living.js +1002 -0
- package/modules/hall-ui/public/gov-positions.js +97 -0
- package/modules/hall-ui/public/government.html +2 -2
- package/modules/hall-ui/public/hall-render.js +3 -0
- package/modules/hall-ui/public/idea.html +2 -2
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +3 -3
- package/modules/hall-ui/public/modules.html +2 -2
- package/modules/hall-ui/public/my-agents.css +334 -0
- package/modules/hall-ui/public/my-agents.html +72 -0
- package/modules/hall-ui/public/my-agents.js +603 -0
- package/modules/hall-ui/public/my-agents.states.json +37 -0
- package/modules/hall-ui/public/primer.html +2 -2
- package/modules/hall-ui/public/profile-connections.js +40 -28
- package/modules/hall-ui/public/profile.html +2 -2
- package/modules/hall-ui/public/project-settings.html +2 -2
- package/modules/hall-ui/public/ranks.html +2 -2
- package/modules/hall-ui/public/roadmap.html +2 -2
- package/modules/hall-ui/public/room.css +8 -0
- package/modules/hall-ui/public/roster.html +2 -2
- package/modules/hall-ui/public/sessions.html +2 -2
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +116 -16
- package/modules/hall-ui/public/street-card.js +18 -1
- package/modules/hall-ui/public/street-deck.css +299 -0
- package/modules/hall-ui/public/street-deck.js +1145 -0
- package/modules/hall-ui/public/street-depth.js +712 -0
- package/modules/hall-ui/public/street-desk.js +5 -2
- package/modules/hall-ui/public/street-draw.js +109 -16
- package/modules/hall-ui/public/street-inside.js +137 -22
- package/modules/hall-ui/public/street-office.js +705 -0
- package/modules/hall-ui/public/street.css +161 -11
- package/modules/hall-ui/public/street.html +41 -11
- package/modules/hall-ui/public/street.js +78 -66
- package/modules/hall-ui/public/street.states.json +129 -21
- package/modules/hall-ui/public/studio.html +2 -2
- package/modules/hall-ui/public/style.css +5 -1
- package/modules/hall-ui/public/task.html +2 -2
- package/modules/hall-ui/public/thinking.html +2 -2
- package/modules/hall-ui/public/tweak-editor.html +2 -2
- package/modules/hall-ui/public/watch.html +2 -2
- package/modules/hall-ui/public/watch.js +14 -1
- package/modules/hall-ui/public/work.html +2 -2
- package/modules/hall-ui/records/living-city.md +57 -0
- package/modules/hall-ui/records/my-agents.md +21 -0
- package/modules/hall-ui/records/oversight-family.md +1 -0
- package/modules/hall-ui/records/profile-connections.md +10 -0
- package/modules/hall-ui/records/reading-rooms.md +2 -0
- package/modules/ideas/routes/blockers.js +92 -3
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/moving.js +17 -1
- package/modules/lifecycle/routes/moving.js +6 -0
- package/modules/lifecycle/routes/project-settings.js +23 -0
- package/modules/onboarding/approval-broadcast.js +8 -0
- package/modules/onboarding/routes/access-requests.js +16 -6
- package/modules/platform-identity/connections.js +13 -2
- package/modules/platform-identity/routes/connections.js +7 -1
- package/modules/public-landing/public/account-private.states.json +2 -2
- package/modules/public-landing/public/account.html +65 -5
- package/modules/public-landing/public/account.states.json +3 -3
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
- package/modules/public-landing/public/assets/world.css +4 -4
- package/modules/public-landing/public/connections-empty.states.json +14 -0
- package/modules/public-landing/public/connections-inactive.states.json +14 -0
- package/modules/public-landing/public/connections.html +450 -0
- package/modules/public-landing/public/connections.probes.json +414 -0
- package/modules/public-landing/public/connections.states.json +17 -0
- package/modules/public-landing/public/contact.html +2 -2
- package/modules/public-landing/public/guild.html +2 -2
- package/modules/public-landing/public/index.html +2 -2
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/privacy.html +2 -2
- package/modules/public-landing/public/projects.html +2 -2
- package/modules/public-landing/public/projects.probes.json +6 -2
- package/modules/public-landing/public/terms.html +2 -2
- package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
- package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
- package/modules/ui-design/kit/fixtures/connections.json +22 -0
- package/modules/ui-design/kit/serve.js +95 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +128 -0
- package/scripts/discord/README.md +8 -5
- package/scripts/discord/post.js +12 -1
- package/scripts/gds/artifact-audit.js +20 -3
- package/scripts/gds/ask.js +164 -0
- package/scripts/gds/autobongos-run.js +20 -5
- package/scripts/gds/autobongos-tweak-lane.js +262 -0
- package/scripts/gds/autobongos-verify.js +21 -2
- package/scripts/gds/claim.js +2 -0
- package/scripts/gds/government-walk.js +638 -4
- package/scripts/gds/pulse.js +9 -6
- package/scripts/gds/ship-escalation.js +22 -3
- package/scripts/gds/ship-finish.js +5 -4
- package/scripts/gds/ship-merge-gate.js +18 -6
- package/scripts/gds/tweak-halt.js +1 -0
- package/scripts/hall-preview/README.md +41 -1
- package/scripts/hall-preview/pref-writes.js +140 -0
- package/scripts/hall-preview/refresh-fixtures.js +101 -7
- package/scripts/hall-preview/scrub-identity.js +366 -7
- package/scripts/hall-preview/server.js +27 -1
- package/scripts/render-diagrams.sh +4 -2
- package/src/bongos/auth-idp.js +6 -1
- package/src/bongos/project-settings.js +41 -0
- package/src/bongos/route-rank-check.js +27 -0
- package/src/bongos/routes/me.js +68 -7
- package/src/bongos/serve-internal.js +5 -1
- package/src/module-api.js +7 -1
- package/src/platform-server.js +10 -0
- package/tests/application_lifecycle.mjs +47 -5
- package/tests/autobongos_tweak_lane.mjs +208 -0
- package/tests/conductor_main_e2e.mjs +21 -0
- package/tests/connections_api.mjs +18 -0
- package/tests/craft_mode_nav.mjs +5 -3
- package/tests/craft_mode_tallies.mjs +52 -16
- package/tests/discord_approvals.mjs +11 -0
- package/tests/discord_channels.mjs +20 -0
- package/tests/fixtures/gov-seated.mjs +64 -0
- package/tests/fixtures/government-ask-cards.json +1284 -0
- package/tests/government_act_alone_buildings.mjs +1 -0
- package/tests/government_agent_asks.mjs +322 -0
- package/tests/government_asks.mjs +408 -0
- package/tests/government_build_rights.mjs +253 -0
- package/tests/government_buildouts.mjs +382 -0
- package/tests/government_counts.mjs +143 -0
- package/tests/government_decision_rules.mjs +5 -1
- package/tests/government_laws.mjs +488 -0
- package/tests/government_laws_session.mjs +321 -0
- package/tests/government_leads.mjs +375 -0
- package/tests/government_positions.mjs +127 -0
- package/tests/government_presets.mjs +215 -0
- package/tests/government_routes.mjs +8 -1
- package/tests/government_routing.mjs +118 -0
- package/tests/government_seed.mjs +5 -0
- package/tests/government_treasury.mjs +394 -0
- package/tests/governor_docket.mjs +26 -2
- package/tests/hall_buildouts.mjs +208 -0
- package/tests/hall_city.mjs +14 -6
- package/tests/hall_city_living.mjs +617 -0
- package/tests/hall_gov_counts.mjs +336 -0
- package/tests/hall_gov_living.mjs +636 -0
- package/tests/hall_gov_real_site.mjs +655 -0
- package/tests/hall_matter_card.mjs +6 -1
- package/tests/hall_my_agents.mjs +481 -0
- package/tests/hall_nav.mjs +107 -4
- package/tests/hall_page_gate_map.mjs +4 -0
- package/tests/hall_preview_governor_fixtures.mjs +30 -3
- package/tests/hall_preview_scrub.mjs +59 -1
- package/tests/hall_preview_security.mjs +169 -0
- package/tests/hall_reading_world.mjs +44 -0
- package/tests/hall_roster_world.mjs +6 -2
- package/tests/hall_street.mjs +18 -2
- package/tests/hall_street_amend.mjs +1 -1
- package/tests/hall_street_deck.mjs +609 -0
- package/tests/hall_street_depth.mjs +559 -0
- package/tests/hall_street_desk.mjs +1 -1
- package/tests/hall_street_inside.mjs +29 -5
- package/tests/hall_tweak_editor.mjs +4 -1
- package/tests/hub_account_page.mjs +94 -3
- package/tests/hub_connections_page.mjs +507 -0
- package/tests/hub_invite_badge.mjs +1 -1
- package/tests/idp_device_exchange.mjs +18 -0
- package/tests/landing_legal_pages.mjs +8 -5
- package/tests/landing_page.mjs +23 -4
- package/tests/nav_permission_atoms.mjs +4 -1
- package/tests/profile_connections_ui.mjs +88 -25
- package/tests/projects_hub.mjs +15 -0
- package/tests/render_check.mjs +2 -2
- package/tests/ship_broadcast_routing.mjs +57 -16
- package/tests/ship_broadcast_server.mjs +40 -7
- package/tests/ship_merge_gate.mjs +19 -0
- package/tests/ui_design_kit.mjs +2 -2
- package/tests/watch_applications_queue.mjs +23 -0
|
@@ -25,11 +25,21 @@ const db = require('../db');
|
|
|
25
25
|
const sessionScopes = require('../session-scopes');
|
|
26
26
|
const boardMembership = require('../board-membership');
|
|
27
27
|
const board = require('../board');
|
|
28
|
+
const laws = require('../laws');
|
|
29
|
+
const presets = require('../presets');
|
|
28
30
|
const boardWithdraw = require('../board-withdraw');
|
|
29
31
|
const recommendations = require('../recommendations');
|
|
30
32
|
const docket = require('../docket');
|
|
33
|
+
const counts = require('../counts');
|
|
34
|
+
const asks = require('../asks');
|
|
35
|
+
const asksDb = require('../asks-db');
|
|
36
|
+
const routing = require('../routing');
|
|
31
37
|
const governmentConfig = require('../config');
|
|
32
38
|
const membershipPredicate = require('../membership-predicate');
|
|
39
|
+
const positions = require('../positions');
|
|
40
|
+
const decisionBoard = require('../decision-board');
|
|
41
|
+
const treasury = require('../treasury');
|
|
42
|
+
const treasuryDb = require('../treasury-db');
|
|
33
43
|
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
34
44
|
const log = api.logger('government');
|
|
35
45
|
|
|
@@ -130,8 +140,51 @@ const governmentPort = {
|
|
|
130
140
|
// building's own direct route runs before letting one builder act alone.
|
|
131
141
|
decisionRulesView: board.decisionRulesView,
|
|
132
142
|
checkActAlone: board.checkActAlone,
|
|
143
|
+
// task 1004707 — the laws every AI session reads. GET /me carries this block
|
|
144
|
+
// and the Conductor relays its contract verbatim; builder-settings enforces
|
|
145
|
+
// each law's locks on the builder's personal settings. Read per request,
|
|
146
|
+
// uncached (a law passed a moment ago is in the next read). Read-only: a law
|
|
147
|
+
// is written only by a passed amendment (law-board.js), never through here.
|
|
148
|
+
lawsForSession: async () => laws.forSession(await db.listLaws()),
|
|
149
|
+
// task 1004713 — the POSITIONS a builder holds (positions.js): owner, Lead,
|
|
150
|
+
// a building's sitting, the board. GET /me carries it so the hall shows the
|
|
151
|
+
// governing surfaces only to a seat holder. Read per request, uncached; null
|
|
152
|
+
// when a read failed (unknown, never "none"). Grants nothing.
|
|
153
|
+
positionsFor: (builderId) => positions.readPositions(builderId, {
|
|
154
|
+
listLeads: () => db.listLeads(),
|
|
155
|
+
listActiveArchons: () => db.listActiveArchons(),
|
|
156
|
+
board: () => governmentConfig.loadGovernmentConfig().board || {},
|
|
157
|
+
decisions: (b) => decisionBoard.decisionRulesFor(b),
|
|
158
|
+
sits: (id, membership) => boardMembership.isBoardMember(id, { membership }),
|
|
159
|
+
}),
|
|
160
|
+
// task 1004703 — THE ASK. The one shape every request from an agent to a person
|
|
161
|
+
// takes (asks.js). A module that files such a request (ideas' POST /blockers)
|
|
162
|
+
// checks it here, refuses slop with refusalLine's words, stores it per docket
|
|
163
|
+
// matter, and reads it back for its own list. Authority stays with the filing
|
|
164
|
+
// route: these only check, store and read the shape.
|
|
165
|
+
checkAsk: asks.checkAsk,
|
|
166
|
+
refusalLine: asks.refusalLine,
|
|
167
|
+
// task 1004704: what a hand filing from an AI session must carry, in plain words.
|
|
168
|
+
askRequiredMessage: asks.ASK_REQUIRED_MESSAGE,
|
|
169
|
+
saveAsk: (...a) => asksDb.saveAsk(...a),
|
|
170
|
+
asksFor: (...a) => asksDb.asksFor(...a),
|
|
133
171
|
};
|
|
134
172
|
|
|
173
|
+
// An ask arrives bare, or wrapped as { ask } (the shape POST /blockers takes it in).
|
|
174
|
+
function askBody(req) {
|
|
175
|
+
const b = req.body;
|
|
176
|
+
return b && typeof b === 'object' && b.ask !== undefined ? b.ask : b;
|
|
177
|
+
}
|
|
178
|
+
// The one refusal every ask route answers with: 400 `ask_refused`, the fixes in
|
|
179
|
+
// plain words as the message, and every broken rule as details.
|
|
180
|
+
function refuseAsk(res, verdict) {
|
|
181
|
+
return res.fail('ask_refused', {
|
|
182
|
+
status: 400,
|
|
183
|
+
message: asks.refusalLine(verdict.errors),
|
|
184
|
+
details: { errors: verdict.errors, rules: asks.ASK_RULES },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
135
188
|
function registerSeams() {
|
|
136
189
|
if (!api.hasProvider('government')) {
|
|
137
190
|
api.registerProvider('government', governmentPort);
|
|
@@ -198,8 +251,37 @@ function contributeRankDesk() {
|
|
|
198
251
|
});
|
|
199
252
|
}
|
|
200
253
|
|
|
254
|
+
// THE TREASURY'S SPENDING MATTERS (task 1004712, ADR 0379): every open borrow a
|
|
255
|
+
// dry pot filed, as a docket matter at the Treasury, under the treasury read's own
|
|
256
|
+
// atom. Its acts go only to a viewer who sits in the seat its stored ask waits on
|
|
257
|
+
// (routing.resolveSeat over the real seats, read per request); the answer route
|
|
258
|
+
// checks the same again, so the docket only hides what would be refused.
|
|
259
|
+
function contributeTreasury() {
|
|
260
|
+
api.contribute('governor.docket', {
|
|
261
|
+
source: treasury.MATTER_SOURCE,
|
|
262
|
+
building: 'treasury',
|
|
263
|
+
gate: { permission: 'board.vote.cast' },
|
|
264
|
+
read: async (ctx) => {
|
|
265
|
+
const open = await treasuryDb.openBorrowRows();
|
|
266
|
+
if (!open.length) return [];
|
|
267
|
+
const seats = await docket.loadSeats({ holders: {} });
|
|
268
|
+
return open.map(({ row, ask }) => {
|
|
269
|
+
const r = seats ? routing.resolveSeat((ask && ask.waits_on) || treasury.SEAT, seats) : null;
|
|
270
|
+
return treasury.borrowMatter(row, { title: row.title, canAnswer: !!(r && r.builder_ids.includes(String(ctx.builderId))) });
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Who answers for the Treasury today: its Lead, or the owner seat while unseated.
|
|
277
|
+
async function treasuryHolder() {
|
|
278
|
+
const seats = await docket.loadSeats({ holders: {} });
|
|
279
|
+
return seats ? routing.resolveSeat(treasury.SEAT, seats) : null;
|
|
280
|
+
}
|
|
281
|
+
|
|
201
282
|
module.exports = function buildGovernmentRouter() {
|
|
202
283
|
contributeRankDesk();
|
|
284
|
+
contributeTreasury();
|
|
203
285
|
registerSeams();
|
|
204
286
|
const router = express.Router();
|
|
205
287
|
|
|
@@ -663,7 +745,9 @@ module.exports = function buildGovernmentRouter() {
|
|
|
663
745
|
// matters: [{ id: '<source>:<id>', source, building, rule, state,
|
|
664
746
|
// title, short, where, opened_at, deadline,
|
|
665
747
|
// proposer: { id, login, name } | null,
|
|
666
|
-
// tally: { for, against, open, needs } | null, href, ref
|
|
748
|
+
// tally: { for, against, open, needs } | null, href, ref,
|
|
749
|
+
// ask?: { question, options, recommend, waits_on, // task 1004703:
|
|
750
|
+
// if_nobody_answers, detail, written_at } }], // the stored ask
|
|
667
751
|
// buildings: [{ key, name, group, counts, matter_ids }], // street order
|
|
668
752
|
// sources: [{ source, building, label, status, count, reason }],
|
|
669
753
|
// // status: live | not_yours | coming | off | unavailable
|
|
@@ -682,6 +766,96 @@ module.exports = function buildGovernmentRouter() {
|
|
|
682
766
|
}
|
|
683
767
|
});
|
|
684
768
|
|
|
769
|
+
// rank: perm board.vote.cast — the government's COUNTS in one read (task 1004716):
|
|
770
|
+
// what is on the viewer ("to do"), what waits on others, what is to decide, how
|
|
771
|
+
// many agents are working and how much is moving. The docket's own atom and its
|
|
772
|
+
// own per-source gates (counts.countsFor reads the docket this viewer would), so
|
|
773
|
+
// a count never includes a matter the viewer could not see.
|
|
774
|
+
// Response: { viewer: { id }, generated_at, on_you, on_others, open,
|
|
775
|
+
// agents_at_work, moving, routed, yours: [matter id], others: [matter id] }
|
|
776
|
+
// (a figure that could not be read is null, never 0).
|
|
777
|
+
router.get('/government/counts', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
778
|
+
try {
|
|
779
|
+
res.json(await counts.countsFor(req.builder));
|
|
780
|
+
} catch (err) {
|
|
781
|
+
log.error('[gds] GET /government/counts', err);
|
|
782
|
+
res.fail('counts_read_failed', { status: 500, message: 'internal error' });
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// rank: perm board.vote.cast — CHECK an ask without storing it (task 1004703):
|
|
787
|
+
// the agent's dry run before it files. Answers the same verdict a write would:
|
|
788
|
+
// { ok: true, ask } (the stored form), or 400 `ask_refused` whose message says
|
|
789
|
+
// in plain words what to fix and whose details list every broken rule. Writes
|
|
790
|
+
// nothing and reads nothing; the docket's own atom (xenos floor), like the write.
|
|
791
|
+
//
|
|
792
|
+
// Body: the ask — { question, options: [{ key, label, result, act? }],
|
|
793
|
+
// recommend: { key, why }, waits_on, if_nobody_answers, detail? } — bare, or as { ask }.
|
|
794
|
+
router.post('/government/asks/check', api.requireBuilder, api.requirePermission('board.vote.cast'), (req, res) => {
|
|
795
|
+
const verdict = asks.checkAsk(askBody(req));
|
|
796
|
+
if (!verdict.ok) return refuseAsk(res, verdict);
|
|
797
|
+
res.json({ ok: true, ask: verdict.ask, question_words: verdict.question_words, rules: asks.ASK_RULES });
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
// rank: perm board.vote.cast — put an ASK on a docket matter (task 1004703):
|
|
801
|
+
// the one shape every request from an agent to a person takes. The matter is
|
|
802
|
+
// named by its docket id (`blockers:12`, `board:34`); the writer must hold the
|
|
803
|
+
// docket's atom AND see the matter, through the same per-source gate the
|
|
804
|
+
// docket applies (docket.findMatter), so an ask is never written onto a matter
|
|
805
|
+
// its writer cannot see. A slop ask is refused (400 `ask_refused`, every fix
|
|
806
|
+
// named) and nothing is stored. A second PUT replaces the ask whole.
|
|
807
|
+
//
|
|
808
|
+
// Body: the ask (as POST /government/asks/check). → 200 { ok, matter_id, ask }
|
|
809
|
+
router.put('/government/asks/:matterId', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
810
|
+
const matterId = String(req.params.matterId || '');
|
|
811
|
+
if (!asks.isMatterId(matterId)) return res.fail('bad_matter_id', { status: 400, message: 'Name the matter by its docket id, like blockers:12.' });
|
|
812
|
+
const verdict = asks.checkAsk(askBody(req));
|
|
813
|
+
if (!verdict.ok) return refuseAsk(res, verdict);
|
|
814
|
+
try {
|
|
815
|
+
const found = await docket.findMatterFor(req.builder, matterId);
|
|
816
|
+
if (!found.matter) {
|
|
817
|
+
if (found.reason === 'unavailable') return res.fail('matter_unavailable', { status: 503, message: 'That matter could not be read just now, so nothing was saved. Try again.' });
|
|
818
|
+
// a matter you cannot see answers exactly as one that does not exist
|
|
819
|
+
return res.fail('matter_not_found', { status: 404, message: 'No open matter by that id that you can see.' });
|
|
820
|
+
}
|
|
821
|
+
const ask = await asksDb.saveAsk({ matterId, ask: verdict.ask, builderId: req.builder.id });
|
|
822
|
+
res.json({ ok: true, matter_id: matterId, ask });
|
|
823
|
+
} catch (err) {
|
|
824
|
+
log.error('[gds] PUT /government/asks/:matterId', err);
|
|
825
|
+
res.fail('ask_save_failed', { status: 500, message: 'internal error' });
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// rank: perm board.vote.cast — PASS AN ASK ON to another seat (task 1004705,
|
|
830
|
+
// the direction desk). Only whoever the ask waits on today may hand it on
|
|
831
|
+
// (routing.passRefusal, from real seats: its Lead, the owner seat, a board's
|
|
832
|
+
// holders), and only an ask that is stored; a board sitting is its voters' and
|
|
833
|
+
// is never passed. The ask is kept word for word; only its seat changes, so it
|
|
834
|
+
// leaves the passer's "to do" and lands on the new seat's.
|
|
835
|
+
//
|
|
836
|
+
// Body: { seat } → 200 { ok, matter_id, ask, route }
|
|
837
|
+
router.post('/government/asks/:matterId/pass', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
838
|
+
const matterId = String(req.params.matterId || '');
|
|
839
|
+
if (!asks.isMatterId(matterId)) return res.fail('bad_matter_id', { status: 400, message: 'Name the matter by its docket id, like blockers:12.' });
|
|
840
|
+
if (validateOrRespond(req, res, { seat: { required: true, type: 'string', minLength: 1, maxLength: 80 } })) return;
|
|
841
|
+
const seat = req.body.seat.trim();
|
|
842
|
+
try {
|
|
843
|
+
const found = await docket.matterWithSeats(req.builder, matterId, seat);
|
|
844
|
+
if (!found.matter) {
|
|
845
|
+
if (found.reason === 'unavailable') return res.fail('matter_unavailable', { status: 503, message: 'That matter could not be read just now, so nothing was passed on. Try again.' });
|
|
846
|
+
return res.fail('matter_not_found', { status: 404, message: 'No open matter by that id that you can see.' });
|
|
847
|
+
}
|
|
848
|
+
const refused = routing.passRefusal(found.matter, req.builder.id, seat, found.seats);
|
|
849
|
+
if (refused) return res.fail(refused.code, { status: refused.status, message: refused.message });
|
|
850
|
+
const ask = await asksDb.saveAsk({ matterId, ask: { ...found.matter.ask, waits_on: seat }, builderId: req.builder.id });
|
|
851
|
+
const route = routing.routeMatter({ ...found.matter, ask }, req.builder.id, found.seats);
|
|
852
|
+
res.json({ ok: true, matter_id: matterId, ask, route });
|
|
853
|
+
} catch (err) {
|
|
854
|
+
log.error('[gds] POST /government/asks/:matterId/pass', err);
|
|
855
|
+
res.fail('ask_pass_failed', { status: 500, message: 'internal error' });
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
|
|
685
859
|
// rank: perm board.item.open — put an AMENDMENT to the board (R14 of goal
|
|
686
860
|
// 1000069, ADR 0175 §7). The board's other subject never comes through here:
|
|
687
861
|
// a Full Idea's window opens AUTOMATICALLY on grade pass (R07), so requesting
|
|
@@ -841,6 +1015,125 @@ module.exports = function buildGovernmentRouter() {
|
|
|
841
1015
|
}
|
|
842
1016
|
});
|
|
843
1017
|
|
|
1018
|
+
// rank: perm board.item.open — propose a change to the project's LAWS (task
|
|
1019
|
+
// 1004706): laws to enact, laws in force to repeal, or both, filed as an
|
|
1020
|
+
// amendment of kind 'laws' and sat under the constitution IN FORCE, like every
|
|
1021
|
+
// amendment. Nothing here writes a law — only the sitting passing does, inside
|
|
1022
|
+
// its closing transaction. The same atom as proposing any amendment.
|
|
1023
|
+
//
|
|
1024
|
+
// Body: { enact?: [{ text, effect_on_ai?, locks? }], repeal?: [law id], rationale_md? }
|
|
1025
|
+
// text one line, 3-100 characters: what every agent obeys
|
|
1026
|
+
// effect_on_ai one line, at most 200: what it means for an agent
|
|
1027
|
+
// locks at most 8 of { setting, value } (fixes it) or { setting, max } (caps it)
|
|
1028
|
+
router.post('/government/board/laws', api.requireBuilder, api.requirePermission('board.item.open'), async (req, res) => {
|
|
1029
|
+
if (validateOrRespond(req, res, {
|
|
1030
|
+
enact: { type: 'array' },
|
|
1031
|
+
repeal: { type: 'array' },
|
|
1032
|
+
rationale_md: { type: 'string', maxLength: 5000 },
|
|
1033
|
+
})) return;
|
|
1034
|
+
try {
|
|
1035
|
+
const out = await board.proposeLaws({
|
|
1036
|
+
enact: req.body.enact,
|
|
1037
|
+
repeal: req.body.repeal,
|
|
1038
|
+
rationaleMd: req.body.rationale_md ?? null,
|
|
1039
|
+
proposedBy: req.builder.id,
|
|
1040
|
+
});
|
|
1041
|
+
if (!out.ok) {
|
|
1042
|
+
if (out.reason === 'already_in_force' || out.reason === 'setting_already_locked' || out.reason === 'not_in_force') {
|
|
1043
|
+
return res.fail(out.reason, 409, {
|
|
1044
|
+
...(out.law_id ? { law_id: out.law_id } : {}),
|
|
1045
|
+
...(out.setting ? { setting: out.setting } : {}),
|
|
1046
|
+
...(out.field ? { field: out.field } : {}),
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
return res.fail('invalid_laws', 400, {
|
|
1050
|
+
reason: out.reason,
|
|
1051
|
+
...(out.field ? { field: out.field } : {}),
|
|
1052
|
+
...(out.expected ? { expected: out.expected } : {}),
|
|
1053
|
+
...(out.setting ? { setting: out.setting } : {}),
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
res.status(201).json({ ok: true, amendment: out.amendment, item: out.item, proposal: out.proposal });
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
log.error('[gds] POST /government/board/laws', err);
|
|
1059
|
+
res.fail('propose_failed', { status: 500, message: 'internal error' });
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
// rank: perm board.vote.cast — the law book (task 1004706): every law in force,
|
|
1064
|
+
// oldest first, each with who passed it (the government it was decided under,
|
|
1065
|
+
// its proposer, who said yes, the sitting) and when. ?include=repealed adds the
|
|
1066
|
+
// laws repealed since, with when and by which sitting. Every builder's agents
|
|
1067
|
+
// obey these, so the read floors where the room's does. A read; nothing here
|
|
1068
|
+
// writes a law.
|
|
1069
|
+
router.get('/government/laws', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
1070
|
+
try {
|
|
1071
|
+
res.json(await board.lawsView({ includeRepealed: req.query.include === 'repealed' }));
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
log.error('[gds] GET /government/laws', err);
|
|
1074
|
+
res.fail('laws_read_failed', { status: 500, message: 'internal error' });
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
// rank: perm board.vote.cast — HOW STRICT the project runs its agents (task
|
|
1079
|
+
// 1004715): the three strictness presets (Open workshop, Guild, Factory), which
|
|
1080
|
+
// one the laws in force amount to (read, never stored: null when none), and for
|
|
1081
|
+
// each its laws and the locks a switch to it would leave in force. A read.
|
|
1082
|
+
router.get('/government/presets', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
1083
|
+
try {
|
|
1084
|
+
res.json(presets.presetsView(await db.listLaws()));
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
log.error('[gds] GET /government/presets', err);
|
|
1087
|
+
res.fail('presets_read_failed', { status: 500, message: 'internal error' });
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
// rank: perm board.item.open — SWITCH the project's strictness preset (task
|
|
1092
|
+
// 1004715). A switch is a vote: ONE laws amendment (the same path as POST
|
|
1093
|
+
// /government/board/laws) that enacts the preset's laws not in force and repeals
|
|
1094
|
+
// the other presets' laws that are. Nothing here writes a law; only the sitting
|
|
1095
|
+
// passing does. The same atom as proposing any law.
|
|
1096
|
+
//
|
|
1097
|
+
// Body: { preset: 'open' | 'guild' | 'factory' }
|
|
1098
|
+
router.post('/government/board/presets', api.requireBuilder, api.requirePermission('board.item.open'), async (req, res) => {
|
|
1099
|
+
if (validateOrRespond(req, res, { preset: { type: 'string', required: true, maxLength: 32 } })) return;
|
|
1100
|
+
try {
|
|
1101
|
+
const sw = presets.switchProposal(req.body.preset, await db.listLaws());
|
|
1102
|
+
if (!sw.ok) {
|
|
1103
|
+
if (sw.reason === 'preset_in_force') return res.fail('preset_in_force', 409, { preset: req.body.preset });
|
|
1104
|
+
return res.fail('invalid_preset', 400, { reason: sw.reason, ...(sw.expected ? { expected: sw.expected } : {}) });
|
|
1105
|
+
}
|
|
1106
|
+
const out = await board.proposeLaws({ ...sw.proposal, rationaleMd: sw.rationale, proposedBy: req.builder.id });
|
|
1107
|
+
if (!out.ok) {
|
|
1108
|
+
if (out.reason === 'already_in_force' || out.reason === 'setting_already_locked' || out.reason === 'not_in_force') {
|
|
1109
|
+
return res.fail(out.reason, 409, { ...(out.law_id ? { law_id: out.law_id } : {}), ...(out.setting ? { setting: out.setting } : {}) });
|
|
1110
|
+
}
|
|
1111
|
+
return res.fail('invalid_preset', 400, { reason: out.reason });
|
|
1112
|
+
}
|
|
1113
|
+
res.status(201).json({ ok: true, preset: req.body.preset, amendment: out.amendment, item: out.item, proposal: out.proposal });
|
|
1114
|
+
} catch (err) {
|
|
1115
|
+
log.error('[gds] POST /government/board/presets', err);
|
|
1116
|
+
res.fail('propose_failed', { status: 500, message: 'internal error' });
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
// rank: perm board.vote.cast — the Lead of each building (task 1004709): the
|
|
1121
|
+
// nine buildings in street order, each with its Lead in office (who, their
|
|
1122
|
+
// charter, since when, the sitting that appointed them) or null, and who answers
|
|
1123
|
+
// for it — its Lead, or the owner (every active Archon, the founding one first)
|
|
1124
|
+
// when it is unseated.
|
|
1125
|
+
// ?include=past adds the appointments that have ended. A read; a Lead is
|
|
1126
|
+
// appointed and removed only by a passed People matter (POST
|
|
1127
|
+
// /government/board/matters, action appoint_lead or remove_lead).
|
|
1128
|
+
router.get('/government/leads', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
1129
|
+
try {
|
|
1130
|
+
res.json(await board.leadsView({ includePast: req.query.include === 'past' }));
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
log.error('[gds] GET /government/leads', err);
|
|
1133
|
+
res.fail('leads_read_failed', { status: 500, message: 'internal error' });
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
|
|
844
1137
|
// rank: perm board.item.open — put a MATTER to a building's sitting (task
|
|
845
1138
|
// 1004529): the board's new subjects, one per building other than the
|
|
846
1139
|
// charter. Refused unless the building's row names a sitting — under the
|
|
@@ -849,7 +1142,12 @@ module.exports = function buildGovernmentRouter() {
|
|
|
849
1142
|
//
|
|
850
1143
|
// Body: { area, title, detail_md?, action? }
|
|
851
1144
|
// action (People only): { type: 'assign_rank' | 'unassign_rank', builder_id, rank_key }
|
|
852
|
-
// — carried out when the sitting passes, under the same walls as the direct route
|
|
1145
|
+
// — carried out when the sitting passes, under the same walls as the direct route;
|
|
1146
|
+
// or { type: 'appoint_lead', building, builder_id, charter? } | { type: 'remove_lead', building }
|
|
1147
|
+
// (task 1004709) — the Lead of a building. Where People is decided as today
|
|
1148
|
+
// this one matter sits under the board in force instead of refusing.
|
|
1149
|
+
// Or, at any building, { type: 'build_out', building, capsule, name?, line? } (task
|
|
1150
|
+
// 1004711) — a build-out put to a vote; POST /government/buildouts files it for the holder.
|
|
853
1151
|
router.post('/government/board/matters', api.requireBuilder, api.requirePermission('board.item.open'), async (req, res) => {
|
|
854
1152
|
if (validateOrRespond(req, res, {
|
|
855
1153
|
area: { required: true, type: 'string', maxLength: 32 },
|
|
@@ -869,11 +1167,17 @@ module.exports = function buildGovernmentRouter() {
|
|
|
869
1167
|
if (out.reason === 'decided_by_gate' || out.reason === 'no_one_holds_this' || out.reason === 'action_not_wired') {
|
|
870
1168
|
return res.fail(out.reason, 409, { area: out.area, ...(out.gate ? { gate: out.gate } : {}), what_this_means: out.what_this_means });
|
|
871
1169
|
}
|
|
1170
|
+
if (out.reason === 'no_lead' || out.reason === 'already_lead') {
|
|
1171
|
+
return res.fail(out.reason, 409, { area: out.area, building: out.building, what_this_means: out.what_this_means });
|
|
1172
|
+
}
|
|
1173
|
+
// task 1004711: a build-out is the building holder's to ask for, by this route too
|
|
1174
|
+
if (out.reason === 'not_the_holder') return res.fail(out.reason, 403, { building: out.building, what_this_means: out.what_this_means });
|
|
872
1175
|
return res.fail('invalid_matter', 400, {
|
|
873
1176
|
reason: out.reason,
|
|
874
1177
|
...(out.field ? { field: out.field } : {}),
|
|
875
1178
|
...(out.expected ? { expected: out.expected } : {}),
|
|
876
1179
|
...(out.rank_key ? { rank_key: out.rank_key } : {}),
|
|
1180
|
+
...(out.builder_id ? { builder_id: out.builder_id } : {}),
|
|
877
1181
|
});
|
|
878
1182
|
}
|
|
879
1183
|
res.status(201).json({ ok: true, matter: out.matter, item: out.item, decided: out.decided });
|
|
@@ -883,6 +1187,125 @@ module.exports = function buildGovernmentRouter() {
|
|
|
883
1187
|
}
|
|
884
1188
|
});
|
|
885
1189
|
|
|
1190
|
+
// rank: perm board.vote.cast — a Lead's BUILD-OUT (task 1004711, ADR 0378): a
|
|
1191
|
+
// committee room, a new floor, an annex, or growing the building into a
|
|
1192
|
+
// district. The coarse atom is the room's (every builder who reads the Leads);
|
|
1193
|
+
// the wall is server-side and per request: only the building's HOLDER — its
|
|
1194
|
+
// Lead in office, or the owner seat (every active Archon) when it is unseated
|
|
1195
|
+
// — may ask (403 not_the_holder). build_rights (build-rights.js, the one rule)
|
|
1196
|
+
// then decides: at once → 201 { path: 'at_once', buildout }, on the record now
|
|
1197
|
+
// and drawn as a live site; otherwise → 201 { path: 'vote', matter, item },
|
|
1198
|
+
// filed as a build_out matter through the same openMatter the matters route
|
|
1199
|
+
// runs, drawn as a ghost with the sitting's tally until it closes.
|
|
1200
|
+
//
|
|
1201
|
+
// Body: { building, capsule: committee|floor|annex|district, name?, line? }
|
|
1202
|
+
router.post('/government/buildouts', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
1203
|
+
if (validateOrRespond(req, res, {
|
|
1204
|
+
building: { required: true, type: 'string', maxLength: 32 },
|
|
1205
|
+
capsule: { required: true, type: 'string', maxLength: 32 },
|
|
1206
|
+
name: { type: 'string', maxLength: 80 },
|
|
1207
|
+
line: { type: 'string', maxLength: 280 },
|
|
1208
|
+
})) return;
|
|
1209
|
+
try {
|
|
1210
|
+
const out = await board.buildOut({
|
|
1211
|
+
buildout: { building: req.body.building, capsule: req.body.capsule, name: req.body.name, line: req.body.line },
|
|
1212
|
+
builderId: req.builder.id,
|
|
1213
|
+
});
|
|
1214
|
+
if (!out.ok) {
|
|
1215
|
+
if (out.reason === 'not_the_holder') return res.fail(out.reason, 403, { building: out.building, what_this_means: out.what_this_means });
|
|
1216
|
+
if (out.reason === 'decided_by_gate' || out.reason === 'no_one_holds_this') {
|
|
1217
|
+
return res.fail(out.reason, 409, { area: out.area, what_this_means: out.what_this_means });
|
|
1218
|
+
}
|
|
1219
|
+
return res.fail('invalid_buildout', 400, { reason: out.reason, ...(out.field ? { field: out.field } : {}) });
|
|
1220
|
+
}
|
|
1221
|
+
if (out.path === 'at_once') return res.status(201).json({ ok: true, path: out.path, buildout: out.buildout, rights: out.rights });
|
|
1222
|
+
res.status(201).json({ ok: true, path: out.path, matter: out.matter, item: out.item, decided: out.decided, rights: out.rights });
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
log.error('[gds] POST /government/buildouts', err);
|
|
1225
|
+
res.fail('buildout_failed', { status: 500, message: 'internal error' });
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
// rank: perm board.vote.cast — THE TREASURY (task 1004712, ADR 0379): this
|
|
1230
|
+
// month's real spend and last month's, the spend cap (a project setting, off
|
|
1231
|
+
// by default: no limit), the reserve, and every open goal's pot — its budget,
|
|
1232
|
+
// what the reserve lent it this month, its real spend this month (cost_log
|
|
1233
|
+
// through the goal's tasks) and its borrow, if it asked. Plus who answers for
|
|
1234
|
+
// the Treasury today. A read; the room's atom, as GET /government/leads.
|
|
1235
|
+
router.get('/government/treasury', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
1236
|
+
try {
|
|
1237
|
+
const [view, holder] = await Promise.all([treasuryDb.readTreasury(), treasuryHolder()]);
|
|
1238
|
+
res.json({ ...view, answers: holder ? { seat: holder.seat, label: holder.label, fell_to_owner: holder.fell_to_owner, yours: holder.builder_ids.includes(String(req.builder.id)) } : null });
|
|
1239
|
+
} catch (err) {
|
|
1240
|
+
log.error('[gds] GET /government/treasury', err);
|
|
1241
|
+
res.fail('treasury_read_failed', { status: 500, message: 'internal error' });
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
// rank: perm board.vote.cast — GIVE A GOAL A POT, change its budget, or take it
|
|
1246
|
+
// away (budget_usd: null). The room's atom; the wall is server-side and per
|
|
1247
|
+
// request: only whoever answers for the Treasury today — its Lead, or the owner
|
|
1248
|
+
// seat while it is unseated — may (403 not_the_holder). Only an open goal.
|
|
1249
|
+
//
|
|
1250
|
+
// Body: { budget_usd: number | null } → 200 { ok, pot | removed }
|
|
1251
|
+
router.put('/government/treasury/pots/:goalId', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
1252
|
+
const goalId = parseId(req, res, { param: 'goalId', code: 'bad_goal_id', positiveInt: true });
|
|
1253
|
+
if (goalId === null) return;
|
|
1254
|
+
if (validateOrRespond(req, res, { budget_usd: {} })) return; // a number of dollars, or null to take the pot away; parsed below
|
|
1255
|
+
const parsed = treasury.parseBudget(req.body ? req.body.budget_usd : undefined);
|
|
1256
|
+
if (parsed.reason) return res.fail('bad_budget', { status: 400, message: parsed.reason });
|
|
1257
|
+
try {
|
|
1258
|
+
const holder = await treasuryHolder();
|
|
1259
|
+
if (!holder) return res.fail('seats_unavailable', { status: 503, message: 'Who answers for the Treasury could not be read just now, so nothing was set. Try again.' });
|
|
1260
|
+
if (!holder.builder_ids.includes(String(req.builder.id))) {
|
|
1261
|
+
return res.fail('not_the_holder', 403, { building: 'treasury', what_this_means: `Only ${holder.label} sets a goal's pot.` });
|
|
1262
|
+
}
|
|
1263
|
+
const goal = await treasuryDb.goalById(goalId);
|
|
1264
|
+
if (!goal) return res.fail('goal_not_found', { status: 404, message: 'No goal by that id.' });
|
|
1265
|
+
if (goal.status !== 'open' && parsed.budget != null) return res.fail('goal_not_open', { status: 409, message: 'Only an open goal has a pot.' });
|
|
1266
|
+
const out = await treasuryDb.setPot({ goalId, budget: parsed.budget, builderId: req.builder.id });
|
|
1267
|
+
res.json({ ok: true, goal_id: String(goalId), ...(out.pot ? { pot: { goal_id: String(out.pot.goal_id), budget_usd: Number(out.pot.budget_usd), set_at: out.pot.set_at } } : { removed: out.removed }) });
|
|
1268
|
+
} catch (err) {
|
|
1269
|
+
log.error('[gds] PUT /government/treasury/pots/:goalId', err);
|
|
1270
|
+
res.fail('pot_set_failed', { status: 500, message: 'internal error' });
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
// rank: perm board.vote.cast — ANSWER A BORROW: lend it all, lend half, or say
|
|
1275
|
+
// no. The room's atom; the wall is the direction desk's: only whoever the
|
|
1276
|
+
// borrow's ask waits on today may answer it (routing.routeMatter over real
|
|
1277
|
+
// seats: the Treasury's Lead, or the owner seat while it is unseated, or the
|
|
1278
|
+
// seat it was passed to). Answered once; a second answer is 409. Saying no
|
|
1279
|
+
// stops nothing: with no spend limit, work runs on and the overspend is debt.
|
|
1280
|
+
//
|
|
1281
|
+
// Body: { key: 'lend' | 'half' | 'no' } → 200 { ok, borrow }
|
|
1282
|
+
router.post('/government/treasury/borrows/:borrowId/answer', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
1283
|
+
const borrowId = parseId(req, res, { param: 'borrowId', code: 'bad_borrow_id', positiveInt: true });
|
|
1284
|
+
if (borrowId === null) return;
|
|
1285
|
+
if (validateOrRespond(req, res, { key: { required: true, type: 'string', maxLength: 16 } })) return;
|
|
1286
|
+
const key = req.body && typeof req.body.key === 'string' ? req.body.key.trim() : '';
|
|
1287
|
+
if (!treasury.ANSWERS.includes(key)) return res.fail('bad_answer', { status: 400, message: 'Answer lend, half or no.' });
|
|
1288
|
+
try {
|
|
1289
|
+
const borrow = await treasuryDb.getBorrow(borrowId);
|
|
1290
|
+
if (!borrow) return res.fail('borrow_not_found', { status: 404, message: 'No borrow by that id.' });
|
|
1291
|
+
if (borrow.state !== 'open') return res.fail('already_answered', { status: 409, message: 'This borrow was already answered.' });
|
|
1292
|
+
const found = await docket.matterWithSeats(req.builder, treasury.matterId(borrowId));
|
|
1293
|
+
if (!found.matter) {
|
|
1294
|
+
if (found.reason === 'unavailable') return res.fail('matter_unavailable', { status: 503, message: 'That borrow could not be read just now, so nothing was answered. Try again.' });
|
|
1295
|
+
return res.fail('borrow_not_found', { status: 404, message: 'No open borrow by that id that you can see.' });
|
|
1296
|
+
}
|
|
1297
|
+
const route = routing.routeMatter(found.matter, req.builder.id, found.seats);
|
|
1298
|
+
if (!route.yours) return res.fail('not_yours', { status: 403, message: `Only ${route.label} answers this borrow.` });
|
|
1299
|
+
const written = await treasuryDb.answerBorrow({ id: borrowId, answer: treasury.answerOf(borrow, key), builderId: req.builder.id });
|
|
1300
|
+
if (!written) return res.fail('already_answered', { status: 409, message: 'This borrow was already answered.' });
|
|
1301
|
+
log.info(`[gds] treasury borrow ${borrowId} (goal ${borrow.goal_id}) answered ${key} by builder ${req.builder.id}`);
|
|
1302
|
+
res.json({ ok: true, borrow: treasury.borrowView(written) });
|
|
1303
|
+
} catch (err) {
|
|
1304
|
+
log.error('[gds] POST /government/treasury/borrows/:borrowId/answer', err);
|
|
1305
|
+
res.fail('borrow_answer_failed', { status: 500, message: 'internal error' });
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
|
|
886
1309
|
// rank: perm board.item.open — withdraw your own open sitting (task 1004564).
|
|
887
1310
|
// The same atom as putting one up: taking it back is the proposer's half of
|
|
888
1311
|
// the same act. The atom is only the coarse gate; the walls are in
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/government/routing.js — THE DIRECTION DESK: every ask routed to a
|
|
4
|
+
// real seat (task 1004705, goal 1000111 "government is how a project steers its
|
|
5
|
+
// AI"; ADR 0376).
|
|
6
|
+
//
|
|
7
|
+
// WHAT IT DECIDES. Which seat an open docket matter waits on, who sits in that
|
|
8
|
+
// seat today, and so whether it is THIS viewer's to answer ("to do") or someone
|
|
9
|
+
// else's ("waits on others"). The hall used to answer that from the prototype
|
|
10
|
+
// overlay; the server answers it now, from real seats, and the docket carries the
|
|
11
|
+
// answer on every matter as `route`.
|
|
12
|
+
//
|
|
13
|
+
// THE SEAT A MATTER WAITS ON.
|
|
14
|
+
// 1. Its stored ask's `waits_on` (asks.js): the seat its agent named, or the
|
|
15
|
+
// one somebody it waited on passed it to (POST /government/asks/:matter/pass).
|
|
16
|
+
// 2. With no stored ask, the seat that answers for its building: a sitting of
|
|
17
|
+
// the board (source 'board') waits on `board:<building>`, its voters;
|
|
18
|
+
// anything else on `lead:<building>`.
|
|
19
|
+
//
|
|
20
|
+
// WHO SITS IN IT (the seat as resolved; never a guess):
|
|
21
|
+
// owner every active Archon (leads.js ownerOf)
|
|
22
|
+
// lead:<b> its Lead in office; an UNSEATED building falls to the
|
|
23
|
+
// owner (owner ruling) — leads.answerFor, written once
|
|
24
|
+
// board:<b> the members its decision-rules row seats today
|
|
25
|
+
// (docket.loadGovernment holders); nobody seated →
|
|
26
|
+
// falls to the owner. A board sitting with no ask of
|
|
27
|
+
// its own is its voters': the docket's per-voter
|
|
28
|
+
// `needs` (board.awaitsVoter) already says whose it is.
|
|
29
|
+
// committee:<b>.<name> no committee is held in a table yet, so it falls to
|
|
30
|
+
// the building's Lead (and so, unseated, to the owner).
|
|
31
|
+
//
|
|
32
|
+
// IF NOBODY ANSWERS. An ask past its matter's deadline, or one whose seat nobody
|
|
33
|
+
// holds (no Archon, nobody seated), is on nobody's "to do": it does what its own
|
|
34
|
+
// `if_nobody_answers` line says, and the route carries that line as
|
|
35
|
+
// `nobody.outcome`, so every surface prints the same words.
|
|
36
|
+
//
|
|
37
|
+
// PURE: no db. docket.js reads the seats (leads, Archons, holders) once per
|
|
38
|
+
// request, uncached (ADR 0016), and routes every matter with routeAll.
|
|
39
|
+
|
|
40
|
+
const { answerFor } = require('./leads');
|
|
41
|
+
const asks = require('./asks');
|
|
42
|
+
|
|
43
|
+
const BUILDING_NAMES = Object.freeze({
|
|
44
|
+
charter: 'Charter', rules: 'Rules', disputes: 'Disputes', people: 'People', planning: 'Planning',
|
|
45
|
+
automation: 'Automation', review: 'Review', release: 'Release', treasury: 'Treasury',
|
|
46
|
+
});
|
|
47
|
+
const isBuilding = (b) => Object.prototype.hasOwnProperty.call(BUILDING_NAMES, b);
|
|
48
|
+
|
|
49
|
+
// The seat a matter waits on before anything falls: its stored ask's, else its building's.
|
|
50
|
+
function seatOf(matter) {
|
|
51
|
+
const m = matter || {};
|
|
52
|
+
const stored = m.ask && typeof m.ask.waits_on === 'string' && asks.SEAT.test(m.ask.waits_on) ? m.ask.waits_on : null;
|
|
53
|
+
if (stored) return stored;
|
|
54
|
+
const b = isBuilding(m.building) ? m.building : 'planning';
|
|
55
|
+
return m.source === 'board' ? `board:${b}` : `lead:${b}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// How a seat reads, after "waits on".
|
|
59
|
+
function seatLabel(seat) {
|
|
60
|
+
const s = String(seat || '');
|
|
61
|
+
if (s === 'owner') return 'the owner';
|
|
62
|
+
const m = /^(lead|board):([a-z]+)$/.exec(s);
|
|
63
|
+
if (m) return `the ${BUILDING_NAMES[m[2]] || m[2]} ${m[1] === 'lead' ? 'Lead' : 'board'}`;
|
|
64
|
+
return asks.seatWords(s);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const idsOf = (list) => (Array.isArray(list) ? list : [])
|
|
68
|
+
.map((p) => (p && typeof p === 'object' ? (p.builder_id ?? p.id) : p))
|
|
69
|
+
.filter((id) => id != null)
|
|
70
|
+
.map(String);
|
|
71
|
+
|
|
72
|
+
// resolveSeat(asked, ctx) → { asked, seat, label, builder_ids, fell_to_owner }
|
|
73
|
+
// asked the seat the matter names
|
|
74
|
+
// seat the seat that answers it, after any fall
|
|
75
|
+
// builder_ids who sits there now ([] when nobody does)
|
|
76
|
+
// ctx { leads: Lead rows in office, owner: leads ownerOf(archons) shape
|
|
77
|
+
// { builders: [{ id }] }, holders: { building: [people] | null } }
|
|
78
|
+
function resolveSeat(asked, ctx) {
|
|
79
|
+
const c = ctx || {};
|
|
80
|
+
const owner = c.owner || { builders: [] };
|
|
81
|
+
const ownerIds = idsOf(owner.builders);
|
|
82
|
+
const seated = (seat, ids) => ({ asked, seat, label: seatLabel(seat), builder_ids: ids, fell_to_owner: false });
|
|
83
|
+
const toOwner = () => ({ asked, seat: 'owner', label: seatLabel('owner'), builder_ids: ownerIds, fell_to_owner: true });
|
|
84
|
+
if (asked === 'owner') return seated('owner', ownerIds);
|
|
85
|
+
let m = /^lead:([a-z]+)$/.exec(String(asked || ''));
|
|
86
|
+
if (m) {
|
|
87
|
+
const a = answerFor(m[1], c.leads || [], owner);
|
|
88
|
+
return !a || a.fell_to_owner ? toOwner() : seated(a.seat, a.builder_ids.map(String));
|
|
89
|
+
}
|
|
90
|
+
if ((m = /^board:([a-z]+)$/.exec(String(asked || '')))) {
|
|
91
|
+
const held = idsOf((c.holders || {})[m[1]]);
|
|
92
|
+
return held.length ? seated(asked, held) : toOwner();
|
|
93
|
+
}
|
|
94
|
+
if ((m = /^committee:([a-z]+)\.[a-z0-9_]+$/.exec(String(asked || '')))) {
|
|
95
|
+
return { ...resolveSeat(`lead:${m[1]}`, c), asked };
|
|
96
|
+
}
|
|
97
|
+
return toOwner();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// routeMatter(matter, viewerId, ctx, now) → the route the docket carries:
|
|
101
|
+
// { seat, label, asked, fell_to_owner, yours, nobody: null | { why, outcome } }
|
|
102
|
+
// `yours` is the one answer to "is this on me": an open matter that needs a
|
|
103
|
+
// person, on a seat the viewer sits in, that somebody can still answer.
|
|
104
|
+
function routeMatter(matter, viewerId, ctx, now = Date.now()) {
|
|
105
|
+
const m = matter || {};
|
|
106
|
+
const needs = m.state === 'needs';
|
|
107
|
+
// a sitting's voters are the item's own (its snapshot), never a fall to the owner
|
|
108
|
+
const ownVote = m.source === 'board' && !m.ask;
|
|
109
|
+
const asked = seatOf(m);
|
|
110
|
+
const r = ownVote ? { asked, seat: asked, label: seatLabel(asked), builder_ids: [], fell_to_owner: false } : resolveSeat(asked, ctx);
|
|
111
|
+
const deadline = m.deadline ? Date.parse(m.deadline) : NaN;
|
|
112
|
+
const outcome = m.ask && m.ask.if_nobody_answers ? String(m.ask.if_nobody_answers) : null;
|
|
113
|
+
let nobody = null;
|
|
114
|
+
if (needs && Number.isFinite(deadline) && deadline <= now) nobody = { why: 'lapsed', outcome };
|
|
115
|
+
else if (needs && !ownVote && !r.builder_ids.length) nobody = { why: 'empty_seat', outcome };
|
|
116
|
+
const viewer = viewerId == null ? null : String(viewerId);
|
|
117
|
+
const sits = ownVote ? needs : r.builder_ids.includes(viewer);
|
|
118
|
+
return {
|
|
119
|
+
seat: r.seat,
|
|
120
|
+
label: r.label,
|
|
121
|
+
asked: r.asked,
|
|
122
|
+
fell_to_owner: r.fell_to_owner,
|
|
123
|
+
yours: needs && !nobody && !!viewer && sits,
|
|
124
|
+
nobody,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Route every matter in place; → how many are the viewer's.
|
|
129
|
+
function routeAll(matters, viewerId, ctx, now = Date.now()) {
|
|
130
|
+
let yours = 0;
|
|
131
|
+
for (const m of matters || []) {
|
|
132
|
+
m.route = routeMatter(m, viewerId, ctx, now);
|
|
133
|
+
if (m.route.yours) yours += 1;
|
|
134
|
+
}
|
|
135
|
+
return yours;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// May this viewer pass this matter's ask on to `toSeat`? → null (yes) or a
|
|
139
|
+
// refusal { code, status, message }. Only whoever the ask waits on may hand it
|
|
140
|
+
// to another seat; a board sitting is its voters' and is never passed; the new
|
|
141
|
+
// seat must be a seat, and a different one.
|
|
142
|
+
function passRefusal(matter, viewerId, toSeat, ctx, now = Date.now()) {
|
|
143
|
+
const m = matter || {};
|
|
144
|
+
if (!m.ask) return { code: 'no_ask', status: 409, message: 'This matter has no ask to pass on yet.' };
|
|
145
|
+
if (m.source === 'board') return { code: 'vote_not_passable', status: 409, message: 'A vote is its voters\' to answer; it cannot be passed on.' };
|
|
146
|
+
const to = String(toSeat || '').trim();
|
|
147
|
+
if (!asks.SEAT.test(to)) return { code: 'unknown_seat', status: 400, message: 'Name a seat: owner, lead:<building>, committee:<building>.<name> or board:<building>.' };
|
|
148
|
+
const route = routeMatter(m, viewerId, ctx, now);
|
|
149
|
+
if (!route.yours) return { code: 'not_yours', status: 403, message: 'Only whoever this waits on can pass it on.' };
|
|
150
|
+
if (to === m.ask.waits_on || resolveSeat(to, ctx).seat === route.seat) return { code: 'same_seat', status: 409, message: 'It already waits on that seat.' };
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = { seatOf, seatLabel, resolveSeat, routeMatter, routeAll, passRefusal, BUILDING_NAMES };
|