@bongos/core 1.21.8 → 1.21.9
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 +55 -30
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +8 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0363-who-decides-what-is-a-table-changed-only-by-amendment.md +52 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +182 -3
- package/docs/api-reference.md +4 -2
- package/docs/module-api-changelog.md +2 -0
- package/modules/discord/board-broadcast.js +2 -0
- package/modules/government/board.js +82 -23
- package/modules/government/db.js +81 -6
- package/modules/government/decision-board.js +300 -0
- package/modules/government/decision-rules.js +312 -0
- package/modules/government/docket.js +49 -9
- package/modules/government/migrations/government_023_decision_rules.sql +190 -0
- package/modules/government/routes/government.js +115 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/bongos/route-rank-check.js +6 -0
- package/src/module-api.js +1 -1
- package/tests/government_abuse_matrix.mjs +3 -0
- package/tests/government_decision_rules.mjs +701 -0
- package/tests/government_routes.mjs +4 -0
- package/tests/government_seed.mjs +1 -0
|
@@ -40,6 +40,15 @@
|
|
|
40
40
|
// rows — the screen can say a building has matters you cannot see without
|
|
41
41
|
// leaking them.
|
|
42
42
|
//
|
|
43
|
+
// WHO DECIDES, FROM THE TABLE (task 1004529). Every building carries
|
|
44
|
+
// `decision` — who decides it and by which rule — read from the project's
|
|
45
|
+
// decision-rules table (decision-rules.js), so the street and the building
|
|
46
|
+
// interiors state the project's own government rather than a fixed one. A
|
|
47
|
+
// sitting at a building (one of the board's matter subjects) carries the rule it
|
|
48
|
+
// was opened under, which is the table's row at that moment, snapshotted on the
|
|
49
|
+
// item. A contributed matter keeps the rule its own route decides it by: that
|
|
50
|
+
// route's gate is what decides it until its module defers to the row.
|
|
51
|
+
//
|
|
43
52
|
// NOTHING IS INVENTED. A source the design names that has no data yet (grade
|
|
44
53
|
// appeals, votes on new standing rules, the treasury's spending matters) is
|
|
45
54
|
// reported `coming`; a source whose module is off is `off`; a read that throws or
|
|
@@ -51,6 +60,7 @@ const board = require('./board');
|
|
|
51
60
|
const db = require('./db');
|
|
52
61
|
const { resolveBoardMembers } = require('./board-membership');
|
|
53
62
|
const resolver = require('./resolver');
|
|
63
|
+
const decisionRules = require('./decision-rules');
|
|
54
64
|
|
|
55
65
|
const log = api.logger('government');
|
|
56
66
|
|
|
@@ -214,13 +224,17 @@ function boardMatter(item, { viewerId, members }) {
|
|
|
214
224
|
else if (voted || memberIds.has(viewer) || authorId === viewer) state = 'moving';
|
|
215
225
|
else state = 'settled';
|
|
216
226
|
|
|
217
|
-
const
|
|
218
|
-
const
|
|
227
|
+
const matterArea = decisionRules.areaForSubjectType(item.subject_type);
|
|
228
|
+
const building = matterArea || (item.subject_type === 'full_idea' ? 'planning' : 'charter');
|
|
229
|
+
const title = matterArea
|
|
230
|
+
? ((item.subject && item.subject.title) || `A matter at ${building}`)
|
|
231
|
+
: item.subject_type === 'constitutional_amendment'
|
|
219
232
|
? (item.subject && item.subject.rationale_md ? firstLine(item.subject.rationale_md) : 'A change to the charter')
|
|
220
233
|
: item.subject_type === 'genesis_stage'
|
|
221
234
|
? `Close the founding stage: ${(item.subject && item.subject.label) || item.subject_id}`
|
|
222
235
|
: ((item.subject && item.subject.title) || `Full idea ${item.subject_id}`);
|
|
223
|
-
const where =
|
|
236
|
+
const where = matterArea ? `${building} · matter`
|
|
237
|
+
: item.subject_type === 'constitutional_amendment' ? 'charter · amendment'
|
|
224
238
|
: item.subject_type === 'genesis_stage' ? 'charter · founding'
|
|
225
239
|
: 'planning · idea to ratify';
|
|
226
240
|
const forN = votes.filter((v) => v.direction === 'yes').length;
|
|
@@ -233,10 +247,15 @@ function boardMatter(item, { viewerId, members }) {
|
|
|
233
247
|
return {
|
|
234
248
|
id: item.id,
|
|
235
249
|
building,
|
|
236
|
-
rule
|
|
250
|
+
// A building's sitting carries the table's own rule on its snapshot (so a
|
|
251
|
+
// sign-off reads as sign-off, a one-person decision as decide); every other
|
|
252
|
+
// sitting reads the board's pass rule.
|
|
253
|
+
rule: (vocab.RULE_KEYS.includes(constitution.rule) ? constitution.rule : null)
|
|
254
|
+
|| vocab.PASS_RULE_TO_RULE[constitution.pass_rule] || null,
|
|
237
255
|
state,
|
|
238
256
|
title,
|
|
239
|
-
short:
|
|
257
|
+
short: matterArea ? 'matter'
|
|
258
|
+
: item.subject_type === 'constitutional_amendment' ? 'amendment'
|
|
240
259
|
: item.subject_type === 'genesis_stage' ? 'founding stage' : 'idea to ratify',
|
|
241
260
|
where,
|
|
242
261
|
opened_at: item.opened_at,
|
|
@@ -340,7 +359,10 @@ function registeredContributions() {
|
|
|
340
359
|
// Build the docket for one viewer. `permissions` is the viewer's resolved set;
|
|
341
360
|
// `contributions` defaults to what is registered on the seam (a test passes its
|
|
342
361
|
// own). Never throws for a source: each is isolated and timed.
|
|
343
|
-
|
|
362
|
+
// `decisions` is the resolved decision-rules table (board.decisionRulesView), or
|
|
363
|
+
// null when it could not be read — each building's `decision` is then null
|
|
364
|
+
// rather than a guess.
|
|
365
|
+
async function buildDocket({ viewer, permissions, contributions = registeredContributions(), now = Date.now(), timeoutMs = SOURCE_TIMEOUT_MS, decisions = null } = {}) {
|
|
344
366
|
const held = new Set(permissions || []);
|
|
345
367
|
const ctx = Object.freeze({
|
|
346
368
|
builderId: String(viewer.id),
|
|
@@ -416,9 +438,17 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
|
|
|
416
438
|
}));
|
|
417
439
|
|
|
418
440
|
const counts = countStates(matters);
|
|
441
|
+
const decisionBy = new Map((Array.isArray(decisions) ? decisions : []).filter(Boolean).map((d) => [d.area, d]));
|
|
419
442
|
const buildings = vocab.BUILDINGS.map((b) => {
|
|
420
443
|
const here = matters.filter((m) => m.building === b.key);
|
|
421
|
-
|
|
444
|
+
const d = decisionBy.get(b.key) || null;
|
|
445
|
+
return {
|
|
446
|
+
key: b.key, name: b.name, group: b.group, counts: countStates(here), matter_ids: here.map((m) => m.id),
|
|
447
|
+
decision: d ? {
|
|
448
|
+
who: d.who, rule: d.rule, rule_words: d.rule_words, decided_by: d.decided_by,
|
|
449
|
+
as_today: d.as_today, sitting: d.sitting, set_by_item_id: d.set_by_item_id,
|
|
450
|
+
} : null,
|
|
451
|
+
};
|
|
422
452
|
});
|
|
423
453
|
return {
|
|
424
454
|
viewer: { id: ctx.builderId },
|
|
@@ -429,7 +459,11 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
|
|
|
429
459
|
buildings,
|
|
430
460
|
sources: sourceStatus,
|
|
431
461
|
readings,
|
|
432
|
-
vocabulary: {
|
|
462
|
+
vocabulary: {
|
|
463
|
+
buildings: vocab.BUILDINGS, rules: vocab.RULES, states: vocab.STATES,
|
|
464
|
+
// The table's rule set — the matter glyphs plus vacant (no one holds it).
|
|
465
|
+
decision_rules: decisionRules.DECISION_RULES.map((r) => ({ key: r.key, words: r.words })),
|
|
466
|
+
},
|
|
433
467
|
};
|
|
434
468
|
}
|
|
435
469
|
|
|
@@ -437,7 +471,13 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
|
|
|
437
471
|
// uncached — ADR 0016), then build.
|
|
438
472
|
async function docketFor(builder) {
|
|
439
473
|
const permissions = await resolver.resolveBuilderPermissions(builder.id);
|
|
440
|
-
|
|
474
|
+
// The table is read per request, uncached (ADR 0016). A failed read leaves
|
|
475
|
+
// every building's decision null; it never fails the city.
|
|
476
|
+
const decisions = await board.decisionRulesView().catch((err) => {
|
|
477
|
+
log.warn({ err: err && err.message }, 'governor docket: decision rules unreadable');
|
|
478
|
+
return null;
|
|
479
|
+
});
|
|
480
|
+
return buildDocket({ viewer: builder, permissions, decisions });
|
|
441
481
|
}
|
|
442
482
|
|
|
443
483
|
module.exports = {
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
-- government_023_decision_rules.sql — who decides what, per building, changed
|
|
2
|
+
-- only by a passed amendment (task 1004529, goal 1000125 the Governor City).
|
|
3
|
+
-- Decision of record: docs/adr/0363-who-decides-what-is-a-table-changed-only-by-amendment.md
|
|
4
|
+
--
|
|
5
|
+
-- WHAT THIS ADDS
|
|
6
|
+
--
|
|
7
|
+
-- 1. government_decision_rules — ONE ROW PER BUILDING of the project's
|
|
8
|
+
-- government (charter, rules, disputes, people, planning, automation,
|
|
9
|
+
-- review, release, treasury). Each row says WHO decides matters at that
|
|
10
|
+
-- building and BY WHICH RULE (decide alone, first yes, consent, majority,
|
|
11
|
+
-- unanimous, sign-off, or vacant — nobody holds it yet). An instance is one
|
|
12
|
+
-- project, so the area is the whole key.
|
|
13
|
+
--
|
|
14
|
+
-- 2. The nine DEFAULT rows, each meaning "exactly as today": `decided_by =
|
|
15
|
+
-- 'gate'` (whoever holds the building's fixed permission acts alone, through
|
|
16
|
+
-- its own page) and, for the charter, `decided_by = 'board'` (the
|
|
17
|
+
-- constitution's own board, under its own pass rule). A default row carries
|
|
18
|
+
-- no rule of its own (rule IS NULL). Seeding them changes NO permission for
|
|
19
|
+
-- any existing project.
|
|
20
|
+
--
|
|
21
|
+
-- 3. A GUARD TRIGGER: after the seed, a row may only be changed by a passed
|
|
22
|
+
-- decision-rules amendment. Every insert or update must name the board item
|
|
23
|
+
-- that carried it (`set_by_item_id`), and that item must be a CLOSED, PASSED
|
|
24
|
+
-- constitutional amendment whose amendment row is of kind 'decision_rules'.
|
|
25
|
+
-- There is no settings route that writes this table, and the trigger makes
|
|
26
|
+
-- that structural — a hand-written UPDATE is refused too. Deleting a row is
|
|
27
|
+
-- refused outright. The one exception is re-seeding a MISSING area with its
|
|
28
|
+
-- as-today default, so a re-run of this file stays safe.
|
|
29
|
+
--
|
|
30
|
+
-- 4. government_amendments.kind — 'constitution' (every existing row, and the
|
|
31
|
+
-- board block amendments filed today) or 'decision_rules' (a change to rows
|
|
32
|
+
-- of the table above). One amendments table, one subject type, one pass
|
|
33
|
+
-- path: a rules amendment is decided by the board in force exactly like any
|
|
34
|
+
-- other amendment.
|
|
35
|
+
--
|
|
36
|
+
-- 5. government_matters + eight new board subjects. Today the board votes on
|
|
37
|
+
-- three things (a Full Idea, a constitutional amendment, a genesis stage).
|
|
38
|
+
-- This adds one subject per building other than the charter — rules_matter,
|
|
39
|
+
-- disputes_matter, people_matter, planning_matter, automation_matter,
|
|
40
|
+
-- review_matter, release_matter, treasury_matter — eleven in all. A matter
|
|
41
|
+
-- is put to a sitting ONLY when its building's row names a sitting (an
|
|
42
|
+
-- amendment moved it off the fixed gate); under the default rows none can
|
|
43
|
+
-- be opened, so nothing about today's behaviour changes.
|
|
44
|
+
--
|
|
45
|
+
-- Additive and idempotent: new tables, a new column with a default every
|
|
46
|
+
-- existing row satisfies, CHECKs that only WIDEN (dropped and re-added under
|
|
47
|
+
-- the same name, the government_020 pattern), and CREATE OR REPLACE for the
|
|
48
|
+
-- trigger function and trigger (Postgres 14+; production runs 16).
|
|
49
|
+
|
|
50
|
+
BEGIN;
|
|
51
|
+
|
|
52
|
+
-- ---------------------------------------------------------------------------
|
|
53
|
+
-- 4. amendments gain a kind (first: the guard below reads it).
|
|
54
|
+
-- ---------------------------------------------------------------------------
|
|
55
|
+
ALTER TABLE government_amendments
|
|
56
|
+
ADD COLUMN IF NOT EXISTS kind text NOT NULL DEFAULT 'constitution';
|
|
57
|
+
|
|
58
|
+
ALTER TABLE government_amendments
|
|
59
|
+
DROP CONSTRAINT IF EXISTS government_amendments_kind_check;
|
|
60
|
+
|
|
61
|
+
ALTER TABLE government_amendments
|
|
62
|
+
ADD CONSTRAINT government_amendments_kind_check
|
|
63
|
+
CHECK (kind IN ('constitution', 'decision_rules'));
|
|
64
|
+
|
|
65
|
+
-- ---------------------------------------------------------------------------
|
|
66
|
+
-- 1. the table
|
|
67
|
+
-- ---------------------------------------------------------------------------
|
|
68
|
+
CREATE TABLE IF NOT EXISTS government_decision_rules (
|
|
69
|
+
area text PRIMARY KEY
|
|
70
|
+
CHECK (area IN ('charter', 'rules', 'disputes', 'people', 'planning',
|
|
71
|
+
'automation', 'review', 'release', 'treasury')),
|
|
72
|
+
-- 'gate' — as today: whoever holds the building's fixed permission acts alone
|
|
73
|
+
-- 'board' — the constitution's own board (the charter's only value)
|
|
74
|
+
-- 'nobody' — vacant: no one holds this building yet
|
|
75
|
+
-- otherwise a board membership predicate (rank:<key>, rank:<key>+, rank:a,b),
|
|
76
|
+
-- resolved per request and uncached like every other franchise read (ADR 0016).
|
|
77
|
+
decided_by text NOT NULL CHECK (char_length(decided_by) BETWEEN 1 AND 200),
|
|
78
|
+
rule text CHECK (rule IN ('decide', 'first', 'consent', 'majority',
|
|
79
|
+
'unanimous', 'signoff', 'vacant')),
|
|
80
|
+
-- The design's words for the seat ("Mira under $50, the council above").
|
|
81
|
+
-- Display only; it never decides anything.
|
|
82
|
+
who text CHECK (who IS NULL OR char_length(who) BETWEEN 1 AND 120),
|
|
83
|
+
-- The passed amendment's board item that set this row. NULL only on a default.
|
|
84
|
+
set_by_item_id bigint REFERENCES government_board_items(id) ON DELETE RESTRICT,
|
|
85
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
86
|
+
|
|
87
|
+
-- A default row carries no rule; every other row carries exactly one.
|
|
88
|
+
CONSTRAINT government_decision_rules_rule_shape CHECK (
|
|
89
|
+
(decided_by IN ('gate', 'board') AND rule IS NULL)
|
|
90
|
+
OR (decided_by = 'nobody' AND rule = 'vacant')
|
|
91
|
+
OR (decided_by NOT IN ('gate', 'board', 'nobody') AND rule IS NOT NULL AND rule <> 'vacant')
|
|
92
|
+
),
|
|
93
|
+
-- The charter is the board, always: its own rule is changed by amending the
|
|
94
|
+
-- constitution's board block, never through this table. And only the charter
|
|
95
|
+
-- may be 'board'.
|
|
96
|
+
CONSTRAINT government_decision_rules_charter_is_the_board CHECK (
|
|
97
|
+
(area = 'charter') = (decided_by = 'board')
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
-- ---------------------------------------------------------------------------
|
|
102
|
+
-- 2. the defaults — exactly today's behaviour. Inserted BEFORE the guard
|
|
103
|
+
-- exists; on a re-run only a missing area is inserted (and the guard admits
|
|
104
|
+
-- exactly that shape).
|
|
105
|
+
-- ---------------------------------------------------------------------------
|
|
106
|
+
INSERT INTO government_decision_rules (area, decided_by, rule)
|
|
107
|
+
SELECT v.area, v.decided_by, NULL
|
|
108
|
+
FROM (VALUES
|
|
109
|
+
('charter', 'board'),
|
|
110
|
+
('rules', 'gate'),
|
|
111
|
+
('disputes', 'gate'),
|
|
112
|
+
('people', 'gate'),
|
|
113
|
+
('planning', 'gate'),
|
|
114
|
+
('automation', 'gate'),
|
|
115
|
+
('review', 'gate'),
|
|
116
|
+
('release', 'gate'),
|
|
117
|
+
('treasury', 'gate')
|
|
118
|
+
) AS v(area, decided_by)
|
|
119
|
+
WHERE NOT EXISTS (SELECT 1 FROM government_decision_rules r WHERE r.area = v.area);
|
|
120
|
+
|
|
121
|
+
-- ---------------------------------------------------------------------------
|
|
122
|
+
-- 3. the guard: changed only by a passed decision-rules amendment.
|
|
123
|
+
-- ---------------------------------------------------------------------------
|
|
124
|
+
CREATE OR REPLACE FUNCTION government_decision_rules_guard() RETURNS trigger
|
|
125
|
+
LANGUAGE plpgsql AS $guard$
|
|
126
|
+
BEGIN
|
|
127
|
+
IF TG_OP = 'DELETE' THEN
|
|
128
|
+
RAISE EXCEPTION 'government_decision_rules: a building cannot be removed from the government'
|
|
129
|
+
USING ERRCODE = 'check_violation';
|
|
130
|
+
END IF;
|
|
131
|
+
|
|
132
|
+
-- Re-seeding a missing area with its as-today default (a re-run of the
|
|
133
|
+
-- migration). Only on INSERT: an UPDATE back to the default is a change like
|
|
134
|
+
-- any other and needs its amendment.
|
|
135
|
+
IF TG_OP = 'INSERT' AND NEW.set_by_item_id IS NULL
|
|
136
|
+
AND NEW.decided_by IN ('gate', 'board') AND NEW.rule IS NULL THEN
|
|
137
|
+
RETURN NEW;
|
|
138
|
+
END IF;
|
|
139
|
+
|
|
140
|
+
IF NEW.set_by_item_id IS NULL OR NOT EXISTS (
|
|
141
|
+
SELECT 1
|
|
142
|
+
FROM government_board_items i
|
|
143
|
+
JOIN government_amendments a ON a.id = i.subject_id
|
|
144
|
+
WHERE i.id = NEW.set_by_item_id
|
|
145
|
+
AND i.subject_type = 'constitutional_amendment'
|
|
146
|
+
AND i.outcome = 'passed'
|
|
147
|
+
AND a.kind = 'decision_rules'
|
|
148
|
+
) THEN
|
|
149
|
+
RAISE EXCEPTION 'government_decision_rules changes only through a passed decision-rules amendment (board item %)', NEW.set_by_item_id
|
|
150
|
+
USING ERRCODE = 'check_violation';
|
|
151
|
+
END IF;
|
|
152
|
+
|
|
153
|
+
NEW.updated_at := now();
|
|
154
|
+
RETURN NEW;
|
|
155
|
+
END
|
|
156
|
+
$guard$;
|
|
157
|
+
|
|
158
|
+
CREATE OR REPLACE TRIGGER government_decision_rules_amendment_only
|
|
159
|
+
BEFORE INSERT OR UPDATE OR DELETE ON government_decision_rules
|
|
160
|
+
FOR EACH ROW EXECUTE FUNCTION government_decision_rules_guard();
|
|
161
|
+
|
|
162
|
+
-- ---------------------------------------------------------------------------
|
|
163
|
+
-- 5. matters, and the eight new subjects
|
|
164
|
+
-- ---------------------------------------------------------------------------
|
|
165
|
+
CREATE TABLE IF NOT EXISTS government_matters (
|
|
166
|
+
id bigserial PRIMARY KEY,
|
|
167
|
+
area text NOT NULL
|
|
168
|
+
CHECK (area IN ('rules', 'disputes', 'people', 'planning',
|
|
169
|
+
'automation', 'review', 'release', 'treasury')),
|
|
170
|
+
title text NOT NULL CHECK (char_length(btrim(title)) BETWEEN 1 AND 200),
|
|
171
|
+
detail_md text CHECK (detail_md IS NULL OR char_length(detail_md) <= 5000),
|
|
172
|
+
-- What the building does by itself when the sitting passes, where it can
|
|
173
|
+
-- (today: People giving or taking a custom rank). NULL = a recorded decision.
|
|
174
|
+
action jsonb,
|
|
175
|
+
proposed_by bigint REFERENCES builders(id) ON DELETE SET NULL,
|
|
176
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
ALTER TABLE government_board_items
|
|
180
|
+
DROP CONSTRAINT IF EXISTS government_board_items_subject_type_check;
|
|
181
|
+
|
|
182
|
+
ALTER TABLE government_board_items
|
|
183
|
+
ADD CONSTRAINT government_board_items_subject_type_check
|
|
184
|
+
CHECK (subject_type IN (
|
|
185
|
+
'full_idea', 'constitutional_amendment', 'genesis_stage',
|
|
186
|
+
'rules_matter', 'disputes_matter', 'people_matter', 'planning_matter',
|
|
187
|
+
'automation_matter', 'review_matter', 'release_matter', 'treasury_matter'
|
|
188
|
+
));
|
|
189
|
+
|
|
190
|
+
COMMIT;
|
|
@@ -122,6 +122,10 @@ const governmentPort = {
|
|
|
122
122
|
// `charter` block. Opening a sitting decides nothing; the vote does.
|
|
123
123
|
proposeCharter: board.proposeCharter,
|
|
124
124
|
genesisStageStatus: board.genesisStageStatus,
|
|
125
|
+
// task 1004529 — who decides what. The table, resolved, and the check a
|
|
126
|
+
// building's own direct route runs before letting one builder act alone.
|
|
127
|
+
decisionRulesView: board.decisionRulesView,
|
|
128
|
+
checkActAlone: board.checkActAlone,
|
|
125
129
|
};
|
|
126
130
|
|
|
127
131
|
function registerSeams() {
|
|
@@ -225,6 +229,31 @@ module.exports = function buildGovernmentRouter() {
|
|
|
225
229
|
}
|
|
226
230
|
return rank;
|
|
227
231
|
}
|
|
232
|
+
// WHO DECIDES AT PEOPLE (task 1004529). Giving or taking a custom rank is
|
|
233
|
+
// People's own act, and the project's decision-rules table says who may do it
|
|
234
|
+
// alone. As today (the default row) the route's own permission is the whole
|
|
235
|
+
// gate, unchanged. Once a passed amendment hands People to a sitting, one
|
|
236
|
+
// builder may no longer do it alone — the route refuses and names the matter
|
|
237
|
+
// route instead — unless the row's rule lets a seated member decide alone and
|
|
238
|
+
// this builder is seated. Server-side, per request, uncached (ADR 0016).
|
|
239
|
+
// Fail-CLOSED: an unreadable row refuses rather than waving the act through.
|
|
240
|
+
async function peopleMayActAlone(req, res) {
|
|
241
|
+
let out;
|
|
242
|
+
try {
|
|
243
|
+
out = await board.checkActAlone('people', req.builder.id);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
log.error('[gds] people decision rule unreadable', err);
|
|
246
|
+
res.fail('decision_rule_unreadable', { status: 503, message: 'who decides at People could not be read just now' });
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
if (out.ok) return true;
|
|
250
|
+
res.fail('decided_by_sitting', 409, {
|
|
251
|
+
area: out.area, rule: out.rule, who: out.who,
|
|
252
|
+
what_this_means: out.what_this_means, open_with: out.open_with,
|
|
253
|
+
});
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
228
257
|
const ARCHON = 'archon';
|
|
229
258
|
const ARCHON_REASON = 'the Archon rank holds every permission and is immutable — it is the rank that can never be locked out of this tab (ADR 0172)';
|
|
230
259
|
const ASSIGN_REASON = 'a seeded rank is assigned by rank (R95b sync); change the builder\'s rank instead';
|
|
@@ -447,6 +476,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
447
476
|
try {
|
|
448
477
|
const rank = await loadAssignableRank(res, rankKey);
|
|
449
478
|
if (rank === null) return;
|
|
479
|
+
if (!(await peopleMayActAlone(req, res))) return;
|
|
450
480
|
const assigned = await db.assignRank({
|
|
451
481
|
builderId: req.body.builder_id,
|
|
452
482
|
rankKey,
|
|
@@ -472,6 +502,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
472
502
|
try {
|
|
473
503
|
const rank = await loadAssignableRank(res, rankKey);
|
|
474
504
|
if (rank === null) return;
|
|
505
|
+
if (!(await peopleMayActAlone(req, res))) return;
|
|
475
506
|
const removed = await db.unassignRank({ builderId, rankKey });
|
|
476
507
|
res.json({ ok: true, builder_id: builderId, rank_key: rankKey, removed });
|
|
477
508
|
} catch (err) {
|
|
@@ -678,6 +709,90 @@ module.exports = function buildGovernmentRouter() {
|
|
|
678
709
|
}
|
|
679
710
|
});
|
|
680
711
|
|
|
712
|
+
// rank: perm board.item.open — propose a change to WHO DECIDES WHAT (task
|
|
713
|
+
// 1004529): one or more buildings' rows of the decision-rules table, filed as
|
|
714
|
+
// an amendment of kind 'decision_rules' and sat under the constitution IN
|
|
715
|
+
// FORCE, like every amendment. Nothing here changes the table — only the
|
|
716
|
+
// sitting passing does, inside its closing transaction. The same atom as
|
|
717
|
+
// proposing any amendment.
|
|
718
|
+
//
|
|
719
|
+
// Body: { rules: [{ area, decided_by, rule?, who? }], rationale_md? }
|
|
720
|
+
// area ∈ rules disputes people planning automation review release treasury
|
|
721
|
+
// (the charter is the board — change it with a constitution amendment)
|
|
722
|
+
// decided_by 'gate' (as today) | 'nobody' (vacant) | rank:<key> | rank:<key>+ | rank:a,b
|
|
723
|
+
// rule decide | first | consent | majority | unanimous | signoff (vacant with 'nobody';
|
|
724
|
+
// none with 'gate')
|
|
725
|
+
// who the seat in words, display only ("Mira under $50, the council above")
|
|
726
|
+
router.post('/government/board/decision-rules', api.requireBuilder, api.requirePermission('board.item.open'), async (req, res) => {
|
|
727
|
+
if (validateOrRespond(req, res, {
|
|
728
|
+
rules: { required: true, type: 'array' },
|
|
729
|
+
rationale_md: { type: 'string', maxLength: 5000 },
|
|
730
|
+
})) return;
|
|
731
|
+
try {
|
|
732
|
+
const out = await board.proposeDecisionRules({
|
|
733
|
+
rules: req.body.rules,
|
|
734
|
+
rationaleMd: req.body.rationale_md ?? null,
|
|
735
|
+
proposedBy: req.builder.id,
|
|
736
|
+
});
|
|
737
|
+
if (!out.ok) {
|
|
738
|
+
if (out.reason === 'no_change') return res.fail('no_change', 409, { reason: 'every building named is already decided this way' });
|
|
739
|
+
return res.fail('invalid_decision_rules', 400, {
|
|
740
|
+
reason: out.reason,
|
|
741
|
+
...(out.area ? { area: out.area } : {}),
|
|
742
|
+
...(out.field ? { field: out.field } : {}),
|
|
743
|
+
...(out.expected ? { expected: out.expected } : {}),
|
|
744
|
+
...(out.rank_key ? { rank_key: out.rank_key } : {}),
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
res.status(201).json({ ok: true, amendment: out.amendment, item: out.item, rules: out.rules });
|
|
748
|
+
} catch (err) {
|
|
749
|
+
log.error('[gds] POST /government/board/decision-rules', err);
|
|
750
|
+
res.fail('propose_failed', { status: 500, message: 'internal error' });
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
// rank: perm board.item.open — put a MATTER to a building's sitting (task
|
|
755
|
+
// 1004529): the board's new subjects, one per building other than the
|
|
756
|
+
// charter. Refused unless the building's row names a sitting — under the
|
|
757
|
+
// default rows every building is decided as today and this route opens
|
|
758
|
+
// nothing. The sitting is snapshotted from the row (who and how).
|
|
759
|
+
//
|
|
760
|
+
// Body: { area, title, detail_md?, action? }
|
|
761
|
+
// action (People only): { type: 'assign_rank' | 'unassign_rank', builder_id, rank_key }
|
|
762
|
+
// — carried out when the sitting passes, under the same walls as the direct route.
|
|
763
|
+
router.post('/government/board/matters', api.requireBuilder, api.requirePermission('board.item.open'), async (req, res) => {
|
|
764
|
+
if (validateOrRespond(req, res, {
|
|
765
|
+
area: { required: true, type: 'string', maxLength: 32 },
|
|
766
|
+
title: { required: true, type: 'string', maxLength: 200 },
|
|
767
|
+
detail_md: { type: 'string', maxLength: 5000 },
|
|
768
|
+
action: { type: 'object' },
|
|
769
|
+
})) return;
|
|
770
|
+
try {
|
|
771
|
+
const out = await board.openMatter({
|
|
772
|
+
area: req.body.area,
|
|
773
|
+
title: req.body.title,
|
|
774
|
+
detailMd: req.body.detail_md ?? null,
|
|
775
|
+
action: req.body.action ?? null,
|
|
776
|
+
proposedBy: req.builder.id,
|
|
777
|
+
});
|
|
778
|
+
if (!out.ok) {
|
|
779
|
+
if (out.reason === 'decided_by_gate' || out.reason === 'no_one_holds_this') {
|
|
780
|
+
return res.fail(out.reason, 409, { area: out.area, ...(out.gate ? { gate: out.gate } : {}), what_this_means: out.what_this_means });
|
|
781
|
+
}
|
|
782
|
+
return res.fail('invalid_matter', 400, {
|
|
783
|
+
reason: out.reason,
|
|
784
|
+
...(out.field ? { field: out.field } : {}),
|
|
785
|
+
...(out.expected ? { expected: out.expected } : {}),
|
|
786
|
+
...(out.rank_key ? { rank_key: out.rank_key } : {}),
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
res.status(201).json({ ok: true, matter: out.matter, item: out.item, decided: out.decided });
|
|
790
|
+
} catch (err) {
|
|
791
|
+
log.error('[gds] POST /government/board/matters', err);
|
|
792
|
+
res.fail('matter_failed', { status: 500, message: 'internal error' });
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
|
|
681
796
|
// rank: perm board.vote.cast — cast a vote on an open board item (R08 of goal
|
|
682
797
|
// 1000069, ADR 0175). The atom FLOORS AT XENOS deliberately (the R04 carry-
|
|
683
798
|
// note): the permission is only the coarse gate, and the real wall is the
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.9",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.9",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.9",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -8804,5 +8804,11 @@
|
|
|
8804
8804
|
"id": "1004441",
|
|
8805
8805
|
"text": "Checked and signed off on the live site: artists and ideators now read as their own craft on profiles, the builders list and the status page. Their earnings charts show, and builders can choose their main role in Settings."
|
|
8806
8806
|
}
|
|
8807
|
+
],
|
|
8808
|
+
"1.21.9": [
|
|
8809
|
+
{
|
|
8810
|
+
"id": "1004529",
|
|
8811
|
+
"text": "Each project can now set who decides what in each part of its government: the charter, rules, disputes, people, planning, automation, review, release and treasury. For each part it names who decides and how: one person decides"
|
|
8812
|
+
}
|
|
8807
8813
|
]
|
|
8808
8814
|
}
|
|
@@ -281,6 +281,12 @@ const GOVERNMENT_PINS = {
|
|
|
281
281
|
// task 1004523: the governor's docket — every matter awaiting a governor. The
|
|
282
282
|
// coarse gate is the room's; each source inside is scoped to its own atom.
|
|
283
283
|
'GET /government/docket': 'perm:board.vote.cast',
|
|
284
|
+
// task 1004529: who decides what — a change to the decision-rules table is an
|
|
285
|
+
// amendment by another door (same atom as proposing one), and a building's
|
|
286
|
+
// matter is put to its sitting by the same atom. Only a passed amendment
|
|
287
|
+
// writes the table; neither route does.
|
|
288
|
+
'POST /government/board/decision-rules': 'perm:board.item.open',
|
|
289
|
+
'POST /government/board/matters': 'perm:board.item.open',
|
|
284
290
|
};
|
|
285
291
|
|
|
286
292
|
// Module-scoped privileged-rank pins: a privileged route that lives in a FEATURE
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.9'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -120,6 +120,9 @@ const stub = {
|
|
|
120
120
|
unassignRank: async () => true,
|
|
121
121
|
listRanksWithGrants: async () => [],
|
|
122
122
|
listAssignments: async () => [],
|
|
123
|
+
// task 1004529: People's row is read before a rank is given or taken. No rows
|
|
124
|
+
// = every building as today, so this matrix's walls are the route's own.
|
|
125
|
+
listDecisionRules: async () => [],
|
|
123
126
|
};
|
|
124
127
|
const REAL = {};
|
|
125
128
|
for (const k of Object.keys(stub)) {
|