@bongos/core 1.21.88 → 1.21.89
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 +24 -19
- package/docs/copy-inventory.md +17 -17
- package/docs/copy-registry.json +19 -19
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +415 -416
- package/modules/hall-ui/public/genesis-home.js +26 -1
- package/modules/hall-ui/public/street-amend.js +57 -4
- package/modules/hall-ui/public/street.js +13 -0
- package/modules/onboarding/genesis-home.js +43 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/genesis_government.mjs +220 -0
|
@@ -33,6 +33,17 @@
|
|
|
33
33
|
not_deciding: 'The plan is already settled.',
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
// What saving the plan did to the government, when it was not settled in the same
|
|
37
|
+
// click. The server's charter outcome (modules/onboarding/genesis-home.js
|
|
38
|
+
// applyGovernmentChoice); in_force and none need no sentence.
|
|
39
|
+
function charterNote(c) {
|
|
40
|
+
if (!c) return '';
|
|
41
|
+
if (c.state === 'sitting') return 'Your starting government is in the Board Room for a vote.';
|
|
42
|
+
if (c.state === 'needs_council') return 'A Foundation needs a council first. Add a council rank, then choose Foundation on the Charter.';
|
|
43
|
+
if (c.state === 'not_filed') return 'The plan is saved, but the government could not be changed just now. You can change it on the Charter.';
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
function projectName() {
|
|
37
48
|
var b = window.__BRANDING__ || {};
|
|
38
49
|
return (b.identity && b.identity.productName) || 'the project';
|
|
@@ -307,7 +318,21 @@
|
|
|
307
318
|
var t = e.target.closest('button');
|
|
308
319
|
if (!t || !host.contains(t)) return;
|
|
309
320
|
if (t.hasAttribute('data-genesis-plan')) {
|
|
310
|
-
|
|
321
|
+
// The government picked here is filed as a charter amendment with the plan
|
|
322
|
+
// (task 1004656). When the founder's one yes decides it, saving the plan is
|
|
323
|
+
// that yes too, the same one-click fast-track the stage closes take.
|
|
324
|
+
act(send('POST', '/founding/plan', planBody(host)).then(function (out) {
|
|
325
|
+
var c = out && out.charter;
|
|
326
|
+
if (c && c.state === 'sitting' && c.one_vote_decides && c.item_id) {
|
|
327
|
+
return send('POST', '/government/board/items/' + encodeURIComponent(c.item_id) + '/votes', { direction: 'yes' })
|
|
328
|
+
.catch(function () { return null; });
|
|
329
|
+
}
|
|
330
|
+
var said = charterNote(c);
|
|
331
|
+
if (!said) return null;
|
|
332
|
+
toast(said);
|
|
333
|
+
// Give the sentence time to be read before the reload takes it away.
|
|
334
|
+
return new Promise(function (done) { setTimeout(done, 3500); });
|
|
335
|
+
}).then(function () { window.location.reload(); }));
|
|
311
336
|
} else if (t.hasAttribute('data-genesis-close')) {
|
|
312
337
|
// The solo fast-track (spec D4): when the founder's one yes decides the sitting
|
|
313
338
|
// and nobody else is asked, the close and the confirm are one click.
|
|
@@ -50,6 +50,38 @@
|
|
|
50
50
|
var d = constitution && constitution.decision_rules;
|
|
51
51
|
return d && Array.isArray(d.rules) ? d.rules.filter(function (r) { return r && r.key; }) : [];
|
|
52
52
|
}
|
|
53
|
+
// The forms of government a charter draft can pick in one tap (task 1004656), from
|
|
54
|
+
// the constitution's own charter block (modules/government/charter.js), so a name
|
|
55
|
+
// is never typed twice. A form is a franchise plus a pass rule; its seat is the
|
|
56
|
+
// franchise option's predicate. Foundation seats a council the owner names (a
|
|
57
|
+
// custom rank this page cannot know), so it is listed as needing one rather than
|
|
58
|
+
// offered; the forms that cannot be adopted yet are listed as later.
|
|
59
|
+
function formsOf(constitution) {
|
|
60
|
+
var ch = constitution && constitution.charter;
|
|
61
|
+
var forms = ch && Array.isArray(ch.forms) ? ch.forms : [];
|
|
62
|
+
var seats = {};
|
|
63
|
+
(ch && Array.isArray(ch.variables) ? ch.variables : []).forEach(function (v) {
|
|
64
|
+
if (v && v.key === 'franchise' && Array.isArray(v.options)) {
|
|
65
|
+
v.options.forEach(function (o) { if (o && o.key && o.predicate) seats[o.key] = o.predicate; });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
var out = { pick: [], council: [], later: [] };
|
|
69
|
+
forms.forEach(function (f) {
|
|
70
|
+
if (!f || !f.key) return;
|
|
71
|
+
if (!f.available) { out.later.push(str(f.name)); return; }
|
|
72
|
+
var seat = seats[f.franchise];
|
|
73
|
+
if (!seat || !FROM_PASS[f.pass_rule]) { out.council.push(str(f.name)); return; }
|
|
74
|
+
out.pick.push({ value: f.key, words: str(f.name), decided_by: seat, rule: FROM_PASS[f.pass_rule] });
|
|
75
|
+
});
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
// Which form the draft's settings make, or null (the name follows the settings).
|
|
79
|
+
function formOf(d) {
|
|
80
|
+
var list = (d && d.forms && d.forms.pick) || [];
|
|
81
|
+
for (var i = 0; i < list.length; i++) if (list[i].decided_by === d.decided_by && list[i].rule === d.rule) return list[i];
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
53
85
|
function rowOf(constitution, area) {
|
|
54
86
|
var as = areasOf(constitution) || [];
|
|
55
87
|
for (var i = 0; i < as.length; i++) if (as[i] && as[i].area === area) return as[i];
|
|
@@ -103,6 +135,7 @@
|
|
|
103
135
|
? { decided_by: board.membership, rule: FROM_PASS[board.pass_rule] || null }
|
|
104
136
|
: { decided_by: row.decided_by, rule: row.decided_by === 'gate' ? null : row.rule },
|
|
105
137
|
decided_by: null, rule: null, reason: '',
|
|
138
|
+
forms: charter ? formsOf(constitution) : { pick: [], council: [], later: [] },
|
|
106
139
|
};
|
|
107
140
|
if (charter) {
|
|
108
141
|
d.decided_by = board.membership;
|
|
@@ -131,11 +164,21 @@
|
|
|
131
164
|
}
|
|
132
165
|
function copy(d) { var o = {}; for (var k in d) if (Object.prototype.hasOwnProperty.call(d, k)) o[k] = d[k]; return o; }
|
|
133
166
|
|
|
134
|
-
// One pick: field 'who' | 'rule' | 'reason'. A value
|
|
135
|
-
// is refused (the draft comes back unchanged).
|
|
167
|
+
// One pick: field 'form' (the charter only) | 'who' | 'rule' | 'reason'. A value
|
|
168
|
+
// outside the vocabulary is refused (the draft comes back unchanged). A form sets
|
|
169
|
+
// who and which rule together; picking either afterwards leaves the form to be
|
|
170
|
+
// read back from the settings.
|
|
136
171
|
function pick(d, field, value) {
|
|
137
172
|
if (!d || !d.ok) return d;
|
|
138
173
|
var n = copy(d);
|
|
174
|
+
if (field === 'form') {
|
|
175
|
+
var list = (d.forms && d.forms.pick) || [], f = null;
|
|
176
|
+
for (var i = 0; i < list.length; i++) if (list[i].value === value) f = list[i];
|
|
177
|
+
if (!f || !hasValue(d.whoOptions, f.decided_by)) return d;
|
|
178
|
+
n.decided_by = f.decided_by;
|
|
179
|
+
n.rule = f.rule;
|
|
180
|
+
return settle(n);
|
|
181
|
+
}
|
|
139
182
|
if (field === 'who') { if (!hasValue(d.whoOptions, value)) return d; n.decided_by = value; }
|
|
140
183
|
else if (field === 'rule') { if (!hasValue(d.ruleOptions, value)) return d; n.rule = value; }
|
|
141
184
|
else if (field === 'reason') { n.reason = str(value).slice(0, REASON_MAX); return n; }
|
|
@@ -190,6 +233,10 @@
|
|
|
190
233
|
if (check(d)) return null;
|
|
191
234
|
var api = (card() && card().API) || '/api/bongos';
|
|
192
235
|
var why = str(d.reason).trim();
|
|
236
|
+
// A named form goes through the charter route, so the record reads
|
|
237
|
+
// "Charter: Direct Democracy" and the server builds the same constitution.
|
|
238
|
+
var form = d.charter ? formOf(d) : null;
|
|
239
|
+
if (form) return { method: 'POST', url: api + '/government/board/charter', body: { form: form.value, rationale_md: why } };
|
|
193
240
|
if (d.charter) return { method: 'POST', url: api + '/government/board/items', body: { subject_type: 'constitutional_amendment', proposal: proposal(d), rationale_md: why } };
|
|
194
241
|
return { method: 'POST', url: api + '/government/board/decision-rules', body: { rules: [ruleRow(d)], rationale_md: why } };
|
|
195
242
|
}
|
|
@@ -205,6 +252,9 @@
|
|
|
205
252
|
nobody_is_vacant: 'An empty building decides nothing.',
|
|
206
253
|
bad_membership: 'That is not a seat this project knows.',
|
|
207
254
|
bad_pass_rule: 'That is not a way the board can decide.',
|
|
255
|
+
council_needs_a_rank: 'A Foundation needs a council rank first.',
|
|
256
|
+
unknown_council_rank: 'There is no such council rank yet, so that would seat no one.',
|
|
257
|
+
form_not_available: 'That form of government is not available yet.',
|
|
208
258
|
};
|
|
209
259
|
function refusal(status, body) {
|
|
210
260
|
var e = body && body.error ? body.error : {};
|
|
@@ -213,7 +263,7 @@
|
|
|
213
263
|
if (e.code === 'no_change') return 'That already is how it works. Nothing was filed.';
|
|
214
264
|
if (status === 401) return 'Sign in to file an amendment.';
|
|
215
265
|
if (status === 403) return 'Filing an amendment is not yours to do here. The server refused it.';
|
|
216
|
-
if ((e.code === 'invalid_decision_rules' || e.code === 'invalid_proposal') && det.reason) {
|
|
266
|
+
if ((e.code === 'invalid_decision_rules' || e.code === 'invalid_proposal' || e.code === 'invalid_charter') && det.reason) {
|
|
217
267
|
return REASONS[det.reason] || 'The server refused it: ' + str(det.reason).replace(/_/g, ' ') + '.';
|
|
218
268
|
}
|
|
219
269
|
var C = card();
|
|
@@ -224,7 +274,9 @@
|
|
|
224
274
|
function answer(d, body) {
|
|
225
275
|
var item = body && body.item;
|
|
226
276
|
return {
|
|
227
|
-
toast:
|
|
277
|
+
toast: d && d.charter && formOf(d)
|
|
278
|
+
? 'Filed. Changing the government to ' + formOf(d).words + ' is on the Charter for the council to vote on.'
|
|
279
|
+
: 'Filed. Your amendment to ' + (d && d.name ? d.name : 'this building') + ' is on the Charter for the council to vote on.',
|
|
228
280
|
building: 'charter',
|
|
229
281
|
matter: item && item.id != null ? 'board:' + item.id : null,
|
|
230
282
|
kind: 'light',
|
|
@@ -234,6 +286,7 @@
|
|
|
234
286
|
var api = {
|
|
235
287
|
RANKS: RANKS, draftFor: draftFor, pick: pick, changed: changed, change: change,
|
|
236
288
|
check: check, request: request, refusal: refusal, answer: answer, whoOptions: whoOptions, ruleOptions: ruleOptions,
|
|
289
|
+
formsOf: formsOf, formOf: formOf,
|
|
237
290
|
};
|
|
238
291
|
root.GovAmend = api;
|
|
239
292
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
@@ -1099,6 +1099,19 @@
|
|
|
1099
1099
|
} else ch.appendChild(node('p', 'inside-note', 'This is how it works today. Pick who decides, or how, to change it.'));
|
|
1100
1100
|
box.appendChild(ch);
|
|
1101
1101
|
var picks = node('div', 'inside-draft__picks');
|
|
1102
|
+
// The Charter's draft leads with the forms of government, one tap each (task
|
|
1103
|
+
// 1004656): a form sets who decides and how together, and the chip lit is the
|
|
1104
|
+
// form the settings make, so a hand-picked mix lights none.
|
|
1105
|
+
if (d.charter && d.forms && d.forms.pick.length) {
|
|
1106
|
+
var lit = A.formOf(d);
|
|
1107
|
+
picks.appendChild(draftField('government', 'insideDraftForm', draftChips('insideDraftForm', d.forms.pick, lit ? lit.value : null, function (v) {
|
|
1108
|
+
dr.d = A.pick(dr.d, 'form', v); dr.error = ''; bumpDraft('insideDraftForm');
|
|
1109
|
+
}), true));
|
|
1110
|
+
var aside = [];
|
|
1111
|
+
if (d.forms.council.length) aside.push(d.forms.council.join(', ') + ' needs a council rank first.');
|
|
1112
|
+
if (d.forms.later.length) aside.push('Coming later: ' + d.forms.later.join(', ') + '.');
|
|
1113
|
+
if (aside.length) picks.appendChild(node('p', 'inside-note inside-draft__later', aside.join(' ')));
|
|
1114
|
+
}
|
|
1102
1115
|
picks.appendChild(draftField('who decides', 'insideDraftWho', draftChips('insideDraftWho', d.whoOptions, d.decided_by, function (v) {
|
|
1103
1116
|
dr.d = A.pick(dr.d, 'who', v); dr.error = ''; bumpDraft('insideDraftWho');
|
|
1104
1117
|
}), true));
|
|
@@ -57,8 +57,8 @@ const MAX_DAYS = 90;
|
|
|
57
57
|
const intIn = (v, lo, hi) => Number.isInteger(v) && v >= lo && v <= hi;
|
|
58
58
|
|
|
59
59
|
// parsePlan(body) — PURE. The founder's plan, defaults filled in, or why it is refused.
|
|
60
|
-
//
|
|
61
|
-
//
|
|
60
|
+
// Here the government is only a well-formed form key; savePlan files it as a charter
|
|
61
|
+
// amendment once the plan is saved (applyGovernmentChoice, task 1004656).
|
|
62
62
|
function parsePlan(body = {}) {
|
|
63
63
|
const b = body && typeof body === 'object' ? body : {};
|
|
64
64
|
const days = {};
|
|
@@ -281,7 +281,46 @@ async function savePlan(body, builderId, deps = {}) {
|
|
|
281
281
|
await s.set(PLAN_KEY, JSON.stringify(parsed.plan), builderId);
|
|
282
282
|
const seeded = await s.lifecycle.seedGenesis({ builderId, existing: parsed.plan.existing });
|
|
283
283
|
await fm.setFoundingMode({ stage: 'scope' }, builderId, modeDeps(s));
|
|
284
|
-
|
|
284
|
+
const charter = await applyGovernmentChoice(parsed.plan.government, builderId, s);
|
|
285
|
+
return { ok: true, plan: parsed.plan, seeded: seeded && seeded.seeded, charter };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// applyGovernmentChoice — the government picked at decide becomes the one in force
|
|
289
|
+
// (task 1004656). It is filed as an ordinary charter amendment under the constitution
|
|
290
|
+
// in force (the government port's proposeCharter), so the board decides it like any
|
|
291
|
+
// change to the rules: under the monarchy default the founder's own yes applies it,
|
|
292
|
+
// and every later stage is then closed by the government they chose (spec D7).
|
|
293
|
+
// Never refuses the plan — the plan is saved first, and a choice that cannot be filed
|
|
294
|
+
// says why in `state`:
|
|
295
|
+
// none no government was picked
|
|
296
|
+
// in_force the constitution already reads as that form (Monarchy at birth)
|
|
297
|
+
// sitting filed; `item_id`, and `one_vote_decides` when one yes carries it
|
|
298
|
+
// needs_council Foundation, which needs a council rank before it can seat anyone
|
|
299
|
+
// not_filed anything else (`reason`), or no government module to file it with
|
|
300
|
+
async function applyGovernmentChoice(form, builderId, s) {
|
|
301
|
+
if (!form) return { state: 'none' };
|
|
302
|
+
const gov = s.government;
|
|
303
|
+
if (!gov || typeof gov.proposeCharter !== 'function') return { state: 'not_filed', form, reason: 'no_government' };
|
|
304
|
+
let out;
|
|
305
|
+
try {
|
|
306
|
+
out = await gov.proposeCharter({ form, proposedBy: builderId });
|
|
307
|
+
} catch (_) {
|
|
308
|
+
return { state: 'not_filed', form, reason: 'charter_failed' };
|
|
309
|
+
}
|
|
310
|
+
if (out && out.ok) {
|
|
311
|
+
const snap = (out.item && out.item.constitution) || {};
|
|
312
|
+
return {
|
|
313
|
+
state: 'sitting',
|
|
314
|
+
form,
|
|
315
|
+
item_id: out.item ? String(out.item.id) : null,
|
|
316
|
+
one_vote_decides: snap.pass_rule === 'first_ratifier',
|
|
317
|
+
reads_as: out.reads_as ? out.reads_as.name : null,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const reason = (out && out.reason) || 'charter_failed';
|
|
321
|
+
if (reason === 'no_change') return { state: 'in_force', form };
|
|
322
|
+
if (reason === 'council_needs_a_rank') return { state: 'needs_council', form };
|
|
323
|
+
return { state: 'not_filed', form, reason };
|
|
285
324
|
}
|
|
286
325
|
|
|
287
326
|
// closeStage — put the current stage to the Board Room. The finished-tasks gate is the
|
|
@@ -335,7 +374,7 @@ async function onStagePassed(deps = {}) {
|
|
|
335
374
|
|
|
336
375
|
module.exports = {
|
|
337
376
|
PLAN_KEY, STAGE_INFO, PROPER, DEMO_PEOPLE, DEMO_HOURS, intIn, parsePlan, shapeGenesis, demoMinutes, demoPlan,
|
|
338
|
-
readGenesis, startFounding, savePlan, closeStage, comeAlive, onStagePassed,
|
|
377
|
+
readGenesis, startFounding, savePlan, applyGovernmentChoice, closeStage, comeAlive, onStagePassed,
|
|
339
378
|
// the founding-mode deps this file reads the mode with, for founding-birth.js (task 1004506)
|
|
340
379
|
modeDepsFor: (deps) => modeDeps(seams(deps)),
|
|
341
380
|
};
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.89",
|
|
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.89",
|
|
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.89",
|
|
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
|
@@ -9358,5 +9358,11 @@
|
|
|
9358
9358
|
"id": "1004482",
|
|
9359
9359
|
"text": "Upgrading a project now clears out commands Bongos used to ship but has since retired (the old dev-box commands, the old /dev /ideate /paint playbooks, the pixel-art commands), so the Commands page and Claude's menu only list"
|
|
9360
9360
|
}
|
|
9361
|
+
],
|
|
9362
|
+
"1.21.89": [
|
|
9363
|
+
{
|
|
9364
|
+
"id": "1004656",
|
|
9365
|
+
"text": "A new project now really runs under the government its founder picks. Before, the choice made while creating a project was stored and then ignored, so every project stayed under one-person rule. Now picking a government files"
|
|
9366
|
+
}
|
|
9361
9367
|
]
|
|
9362
9368
|
}
|
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.89'; // 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');
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// tests/genesis_government.mjs — THE GOVERNMENT PICKED AT GENESIS IS THE ONE IN FORCE
|
|
2
|
+
// (task 1004656, goal 1000111 criterion gov-born-governed).
|
|
3
|
+
//
|
|
4
|
+
// WHAT IT GUARDS. A founder picks a starting government on the genesis decide screen.
|
|
5
|
+
// Before this task the pick was stored in the plan and never read again, so every
|
|
6
|
+
// project left genesis on the default monarchy it never chose. Now saving the plan
|
|
7
|
+
// files the pick as a charter amendment under the constitution in force, and under the
|
|
8
|
+
// monarchy a project is born with, the founder's own yes applies it. What can go wrong:
|
|
9
|
+
// * the pick saved but nothing filed (the old bug), or filed and never applied;
|
|
10
|
+
// * a pick that cannot be filed refusing the whole plan (the plan must always save);
|
|
11
|
+
// * Foundation filed with no council, which would seat nobody;
|
|
12
|
+
// * the hall's Charter picker filing a constitution the server would not build.
|
|
13
|
+
//
|
|
14
|
+
// The path RUNS end to end over the real code: genesis-home's savePlan with the REAL
|
|
15
|
+
// government port's proposeCharter, castVote and close (db stubbed, the board's own
|
|
16
|
+
// logic real), and the ratified text written through the stubbed apply. The hall
|
|
17
|
+
// picker runs the real street-amend.js over the server's real charter view.
|
|
18
|
+
//
|
|
19
|
+
// Run: node --test tests/genesis_government.mjs
|
|
20
|
+
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import { test } from 'node:test';
|
|
23
|
+
import { createRequire } from 'node:module';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
30
|
+
const gh = require(path.join(ROOT, 'modules', 'onboarding', 'genesis-home.js'));
|
|
31
|
+
const board = require(path.join(GOV, 'board.js'));
|
|
32
|
+
const db = require(path.join(GOV, 'db.js'));
|
|
33
|
+
const config = require(path.join(GOV, 'config.js'));
|
|
34
|
+
const charter = require(path.join(GOV, 'charter.js'));
|
|
35
|
+
const DR = require(path.join(GOV, 'decision-rules.js'));
|
|
36
|
+
const A = require(path.join(ROOT, 'modules', 'hall-ui', 'public', 'street-amend.js'));
|
|
37
|
+
|
|
38
|
+
const MONARCHY = Object.freeze({ membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true });
|
|
39
|
+
const FOUNDER = '42';
|
|
40
|
+
const j = (v) => JSON.parse(JSON.stringify(v));
|
|
41
|
+
|
|
42
|
+
// A founding project in its decide stage: an in-memory project_settings store, a fake
|
|
43
|
+
// lifecycle seeder, and the REAL government port surface genesis-home calls.
|
|
44
|
+
function project() {
|
|
45
|
+
const store = new Map();
|
|
46
|
+
const deps = {
|
|
47
|
+
getSetting: async (k) => (store.has(k) ? { key: k, value: store.get(k) } : null),
|
|
48
|
+
setSetting: async (k, v) => { store.set(k, String(v)); return { key: k, value: String(v) }; },
|
|
49
|
+
lifecycle: { seedGenesis: async () => ({ seeded: 5 }), genesisTasks: async () => ({}) },
|
|
50
|
+
government: {
|
|
51
|
+
proposeCharter: board.proposeCharter,
|
|
52
|
+
genesisStageStatus: async () => ({ stages: [] }),
|
|
53
|
+
},
|
|
54
|
+
now: new Date('2026-10-08T00:00:00Z'),
|
|
55
|
+
};
|
|
56
|
+
return { store, deps };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The board's db, stubbed to one founder under the monarchy a project is born with;
|
|
60
|
+
// everything that decides (validation, the self-removal guard, the tally, the close)
|
|
61
|
+
// is the real board.js. Returns what was filed and what was applied.
|
|
62
|
+
async function underMonarchy(fn) {
|
|
63
|
+
const seen = { filed: null, applied: [], votes: [] };
|
|
64
|
+
const originals = {};
|
|
65
|
+
const stubs = {
|
|
66
|
+
builderHoldsRankActive: async () => true,
|
|
67
|
+
getRankByKey: async () => null,
|
|
68
|
+
createAmendment: async (args) => { seen.filed = args; return { id: '5', proposed: args.proposed, proposed_by: args.proposedBy }; },
|
|
69
|
+
openBoardItem: async (args) => ({ id: '21', subject_type: args.subjectType, subject_id: '5', opened_by: args.openedBy, constitution: args.constitution, closes_at: null, closed_at: null }),
|
|
70
|
+
getBoardItemForVote: async () => ({ id: '21', subject_type: 'constitutional_amendment', subject_id: '5', opened_by: FOUNDER, constitution: MONARCHY, closed_at: null, expired: false }),
|
|
71
|
+
insertBoardVote: async (row) => { const v = { voter_id: row.voterId, direction: row.direction }; seen.votes.push(v); return v; },
|
|
72
|
+
insertVoteKarma: async () => {},
|
|
73
|
+
getVotesForItem: async () => seen.votes,
|
|
74
|
+
getActiveBuildersWithRank: async () => [{ builder_id: FOUNDER, github_login: 'founder', display_name: 'Founder' }],
|
|
75
|
+
getBoardItemAuthor: async () => FOUNDER,
|
|
76
|
+
closeBoardItem: async ({ outcome }) => ({ id: '21', subject_type: 'constitutional_amendment', subject_id: '5', opened_by: FOUNDER, closed_at: 'T', outcome }),
|
|
77
|
+
insertAuthorRatifyKarma: async () => true,
|
|
78
|
+
getAmendment: async () => ({ id: '5', proposed: seen.filed.proposed }),
|
|
79
|
+
};
|
|
80
|
+
for (const [k, v] of Object.entries(stubs)) { originals[k] = db[k]; db[k] = v; }
|
|
81
|
+
const originalApply = config.applyBoardAmendment;
|
|
82
|
+
config.applyBoardAmendment = (proposed) => { seen.applied.push(proposed); return proposed; };
|
|
83
|
+
try { await fn(seen); } finally {
|
|
84
|
+
for (const [k, v] of Object.entries(originals)) db[k] = v;
|
|
85
|
+
config.applyBoardAmendment = originalApply;
|
|
86
|
+
}
|
|
87
|
+
return seen;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function decide(plan) {
|
|
91
|
+
const p = project();
|
|
92
|
+
assert.equal((await gh.startFounding(FOUNDER, p.deps)).ok, true);
|
|
93
|
+
const out = await gh.savePlan(plan, FOUNDER, p.deps);
|
|
94
|
+
return { p, out };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
test('this checkout is born a monarchy, the state every genesis starts from', () => {
|
|
98
|
+
assert.equal(charter.readCharter(config.loadGovernmentConfig().board).form, 'monarchy');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
for (const form of ['timocratic-republic', 'direct-democracy']) {
|
|
102
|
+
test(`picking ${form} at decide files it, the founder's one yes applies it, and the constitution reads as ${form}`, async () => {
|
|
103
|
+
const seen = await underMonarchy(async () => {
|
|
104
|
+
const { out } = await decide({ government: form });
|
|
105
|
+
assert.equal(out.ok, true, 'the plan saves');
|
|
106
|
+
assert.equal(out.charter.state, 'sitting', 'the pick is filed, not just recorded');
|
|
107
|
+
assert.equal(out.charter.one_vote_decides, true, 'under the monarchy the founder\'s yes carries it');
|
|
108
|
+
assert.equal(out.charter.item_id, '21');
|
|
109
|
+
// what the hall does next: the founder's yes, then the close
|
|
110
|
+
await board.castVote({ itemId: out.charter.item_id, voterId: FOUNDER, direction: 'yes' });
|
|
111
|
+
assert.equal((await board.maybeCloseAfterVote(out.charter.item_id)).outcome, 'passed');
|
|
112
|
+
});
|
|
113
|
+
assert.equal(seen.applied.length, 1, 'the ratified constitution is applied once');
|
|
114
|
+
assert.equal(charter.readCharter(seen.applied[0]).form, form, 'the constitution in force is the government the founder picked');
|
|
115
|
+
assert.match(seen.filed.rationaleMd, /^Charter: /, 'the record says in words what was chosen');
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
test('picking Monarchy files nothing: it is already the government in force', async () => {
|
|
120
|
+
const seen = await underMonarchy(async () => {
|
|
121
|
+
const { out } = await decide({ government: 'monarchy' });
|
|
122
|
+
assert.equal(out.ok, true);
|
|
123
|
+
assert.deepEqual(out.charter, { state: 'in_force', form: 'monarchy' });
|
|
124
|
+
});
|
|
125
|
+
assert.equal(seen.filed, null);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('Foundation needs a council first: the plan saves, nothing is filed, and the reason is named', async () => {
|
|
129
|
+
const seen = await underMonarchy(async () => {
|
|
130
|
+
const { p, out } = await decide({ government: 'foundation' });
|
|
131
|
+
assert.equal(out.ok, true, 'the plan is never refused over the government');
|
|
132
|
+
assert.deepEqual(out.charter, { state: 'needs_council', form: 'foundation' });
|
|
133
|
+
assert.equal(JSON.parse(p.store.get(gh.PLAN_KEY)).government, 'foundation', 'the pick is still recorded');
|
|
134
|
+
});
|
|
135
|
+
assert.equal(seen.filed, null, 'a council nobody sits on is never filed');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('no pick, no government module, a refusal, a throw: the plan still saves and says what happened', async () => {
|
|
139
|
+
let r = await decide({});
|
|
140
|
+
assert.deepEqual(r.out.charter, { state: 'none' });
|
|
141
|
+
const p = project();
|
|
142
|
+
await gh.startFounding(FOUNDER, p.deps);
|
|
143
|
+
p.deps.government = { genesisStageStatus: async () => ({ stages: [] }) };
|
|
144
|
+
r = { out: await gh.savePlan({ government: 'direct-democracy' }, FOUNDER, p.deps) };
|
|
145
|
+
assert.equal(r.out.ok, true);
|
|
146
|
+
assert.deepEqual(r.out.charter, { state: 'not_filed', form: 'direct-democracy', reason: 'no_government' });
|
|
147
|
+
for (const [proposeCharter, reason] of [
|
|
148
|
+
[async () => ({ ok: false, reason: 'unknown_form' }), 'unknown_form'],
|
|
149
|
+
[async () => { throw new Error('board down'); }, 'charter_failed'],
|
|
150
|
+
]) {
|
|
151
|
+
const q = project();
|
|
152
|
+
await gh.startFounding(FOUNDER, q.deps);
|
|
153
|
+
q.deps.government = { proposeCharter, genesisStageStatus: async () => ({ stages: [] }) };
|
|
154
|
+
const out = await gh.savePlan({ government: 'direct-democracy' }, FOUNDER, q.deps);
|
|
155
|
+
assert.equal(out.ok, true, `${reason}: the plan saves`);
|
|
156
|
+
assert.equal(out.charter.state, 'not_filed');
|
|
157
|
+
assert.equal(out.charter.reason, reason);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('a demo stays the monarchy it is born with (owner ruling 2026-10-01): nothing is filed', () => {
|
|
162
|
+
assert.equal(gh.demoPlan({ people: 3, hours_each: 4 }).government, 'monarchy');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ── the hall's Charter picker ───────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
function constitutionView(boardBlock = MONARCHY) {
|
|
168
|
+
return j({
|
|
169
|
+
board: boardBlock,
|
|
170
|
+
charter: charter.charterView(boardBlock),
|
|
171
|
+
decision_rules: {
|
|
172
|
+
areas: DR.resolveAll([], { board: boardBlock }),
|
|
173
|
+
rules: DR.DECISION_RULES.map((r) => ({ key: r.key, words: r.words })),
|
|
174
|
+
changed_by: 'a passed amendment only',
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
test('the Charter offers the forms that can be adopted in one tap; Foundation and the later forms are named, not offered', () => {
|
|
180
|
+
const d = A.draftFor('charter', constitutionView());
|
|
181
|
+
assert.deepEqual(d.forms.pick.map((f) => f.words), ['Monarchy / BDFL', 'Timocratic Republic', 'Direct Democracy']);
|
|
182
|
+
assert.deepEqual(d.forms.council, ['Foundation']);
|
|
183
|
+
assert.deepEqual(d.forms.later, ['Presidential', 'Cooperative', 'Federation']);
|
|
184
|
+
assert.equal(A.draftFor('review', constitutionView()).forms.pick.length, 0, 'only the Charter carries forms');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('a tapped form sets who and how together, files through the charter route, and the server builds the same constitution', () => {
|
|
188
|
+
for (const key of ['timocratic-republic', 'direct-democracy']) {
|
|
189
|
+
let d = A.draftFor('charter', constitutionView());
|
|
190
|
+
d = A.pick(d, 'form', key);
|
|
191
|
+
assert.equal(A.formOf(d).value, key, 'the lit chip is the form the settings make');
|
|
192
|
+
d = A.pick(d, 'reason', 'More of us should decide.');
|
|
193
|
+
const req = A.request(d);
|
|
194
|
+
assert.equal(req.url, '/api/bongos/government/board/charter');
|
|
195
|
+
assert.deepEqual(req.body, { form: key, rationale_md: 'More of us should decide.' });
|
|
196
|
+
// what the server files for that body
|
|
197
|
+
const built = charter.buildCharterProposal(MONARCHY, { form: key });
|
|
198
|
+
assert.equal(built.ok, true);
|
|
199
|
+
assert.equal(built.proposal.membership, d.decided_by, `${key}: the seat the hall showed is the seat the server files`);
|
|
200
|
+
assert.equal(charter.readCharter(built.proposal).form, key);
|
|
201
|
+
assert.match(A.answer(d, { item: { id: 7 } }).toast, new RegExp(A.formOf(d).words.replace(/[/]/g, '\\/')));
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('picking who or how by hand after a form lights no form, and files as a plain amendment', () => {
|
|
206
|
+
let d = A.pick(A.draftFor('charter', constitutionView()), 'form', 'direct-democracy');
|
|
207
|
+
d = A.pick(d, 'rule', 'majority');
|
|
208
|
+
assert.equal(A.formOf(d), null, 'the name follows the settings: a mix is no named form');
|
|
209
|
+
d = A.pick(d, 'reason', 'A majority, not consent.');
|
|
210
|
+
assert.equal(A.request(d).url, '/api/bongos/government/board/items');
|
|
211
|
+
assert.equal(A.pick(d, 'form', 'presidential'), d, 'a form outside the offered set changes nothing');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('the charter route\'s refusals are said in words', () => {
|
|
215
|
+
const d = A.draftFor('charter', constitutionView());
|
|
216
|
+
const say = (reason) => A.refusal(400, { error: { code: 'invalid_charter', details: { reason } } });
|
|
217
|
+
assert.match(say('council_needs_a_rank'), /council rank/);
|
|
218
|
+
assert.match(say('form_not_available'), /not available yet/);
|
|
219
|
+
assert.ok(d.ok);
|
|
220
|
+
});
|