@bongos/core 1.20.56 → 1.20.58
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 +128 -33
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +10 -0
- package/clients/bongos-client/index.cjs +10 -0
- package/clients/bongos-client/index.d.ts +12 -2
- package/clients/bongos-client/index.mjs +10 -0
- package/docs/api/openapi.json +211 -17
- package/docs/api-reference.md +7 -2
- package/docs/copy-inventory.md +36 -20
- package/docs/copy-registry.json +177 -23
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +8 -3
- package/docs/page-readings.json +40 -40
- package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
- package/modules/hall-ui/public/genesis-home.js +349 -0
- package/modules/hall-ui/public/government.js +3 -1
- package/modules/hall-ui/public/index-genesis-decide.states.json +13 -0
- package/modules/hall-ui/public/index-genesis.states.json +13 -0
- package/modules/hall-ui/public/index.html +2 -0
- package/modules/hall-ui/public/style.css +65 -0
- package/modules/hall-ui/records/genesis-home.md +18 -0
- package/modules/lifecycle/kickoff-checklist.js +54 -1
- package/modules/lifecycle/kickoff-seed.js +61 -2
- package/modules/lifecycle/routes/kickoff-seed.js +7 -1
- package/modules/onboarding/genesis-home.js +274 -0
- package/modules/onboarding/routes/onboarding.js +56 -0
- package/modules/ui-design/kit/fixtures/founding-genesis-decide.json +108 -0
- package/modules/ui-design/kit/fixtures/founding-genesis-stage.json +162 -0
- package/modules/ui-design/kit/fixtures/government-constitution-sample.json +205 -0
- package/modules/ui-design/kit/serve.js +7 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/genesis_home.mjs +288 -0
- package/tests/government_recommendations.mjs +1 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "GET /government/constitution as the genesis plan reads it (task 1004423): the charter view and the recommendation for a one-builder project, from the real charter.js and recommendations.js.",
|
|
3
|
+
"charter": {
|
|
4
|
+
"forms": [
|
|
5
|
+
{
|
|
6
|
+
"key": "monarchy",
|
|
7
|
+
"name": "Monarchy / BDFL",
|
|
8
|
+
"available": true,
|
|
9
|
+
"franchise": "founder",
|
|
10
|
+
"pass_rule": "first_ratifier",
|
|
11
|
+
"explanation": "One leader decides. In a project the founder makes the calls, which is fast, clear and honest for a solo start."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"key": "timocratic-republic",
|
|
15
|
+
"name": "Timocratic Republic",
|
|
16
|
+
"available": true,
|
|
17
|
+
"franchise": "trusted",
|
|
18
|
+
"pass_rule": "consent",
|
|
19
|
+
"explanation": "A say is earned, not given. Builders who have reached the trusted rank decide together."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"key": "direct-democracy",
|
|
23
|
+
"name": "Direct Democracy",
|
|
24
|
+
"available": true,
|
|
25
|
+
"franchise": "everyone",
|
|
26
|
+
"pass_rule": "consent",
|
|
27
|
+
"explanation": "Every builder votes directly. A decision passes unless someone objects with a reason."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"key": "foundation",
|
|
31
|
+
"name": "Foundation",
|
|
32
|
+
"available": true,
|
|
33
|
+
"franchise": "council",
|
|
34
|
+
"pass_rule": "consent",
|
|
35
|
+
"explanation": "A written charter looked after by a small council of trustees you appoint."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"key": "presidential",
|
|
39
|
+
"name": "Presidential",
|
|
40
|
+
"available": false,
|
|
41
|
+
"explanation": "The team elects a leader for a fixed term. Coming later, because it needs elections."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"key": "cooperative",
|
|
45
|
+
"name": "Cooperative",
|
|
46
|
+
"available": false,
|
|
47
|
+
"explanation": "Every member holds a share and gets one vote. Coming later, because it needs member shares."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"key": "federation",
|
|
51
|
+
"name": "Federation",
|
|
52
|
+
"available": false,
|
|
53
|
+
"explanation": "Self-governing teams coordinate under one shared pact. Coming later, for projects with many teams."
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"variables": [
|
|
57
|
+
{
|
|
58
|
+
"key": "franchise",
|
|
59
|
+
"name": "Franchise",
|
|
60
|
+
"question": "Who gets a vote?",
|
|
61
|
+
"status": "live",
|
|
62
|
+
"explanation": "Who sits on the board and votes on the project's decisions.",
|
|
63
|
+
"options": [
|
|
64
|
+
{
|
|
65
|
+
"key": "founder",
|
|
66
|
+
"name": "The founder",
|
|
67
|
+
"predicate": "rank:archon",
|
|
68
|
+
"seats": [
|
|
69
|
+
"archon"
|
|
70
|
+
],
|
|
71
|
+
"explanation": "Only the founder votes (the Archon rank). Right for a solo start, when there is nobody else to ask."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"key": "trusted",
|
|
75
|
+
"name": "Trusted builders and up",
|
|
76
|
+
"predicate": "rank:metic+",
|
|
77
|
+
"seats": [
|
|
78
|
+
"metic",
|
|
79
|
+
"archon"
|
|
80
|
+
],
|
|
81
|
+
"explanation": "Builders who have earned the trusted rank by shipping get a vote, along with the founder."
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"key": "everyone",
|
|
85
|
+
"name": "Every builder",
|
|
86
|
+
"predicate": "rank:xenos+",
|
|
87
|
+
"seats": [
|
|
88
|
+
"xenos",
|
|
89
|
+
"thetes",
|
|
90
|
+
"metic",
|
|
91
|
+
"archon"
|
|
92
|
+
],
|
|
93
|
+
"explanation": "Everyone on the project gets a vote, from the newest builder to the founder."
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"key": "council",
|
|
97
|
+
"name": "A council you name",
|
|
98
|
+
"predicate": null,
|
|
99
|
+
"seats": null,
|
|
100
|
+
"explanation": "Only the members of a council rank you create get a vote. You choose who sits on it."
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"key": "pass_rule",
|
|
106
|
+
"name": "Pass rule (winning coalition)",
|
|
107
|
+
"question": "How many must agree?",
|
|
108
|
+
"status": "live",
|
|
109
|
+
"explanation": "How much agreement a decision needs before it passes.",
|
|
110
|
+
"options": [
|
|
111
|
+
{
|
|
112
|
+
"key": "first_ratifier",
|
|
113
|
+
"name": "One yes is enough",
|
|
114
|
+
"explanation": "One yes from any member decides it."
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"key": "consent",
|
|
118
|
+
"name": "Passes unless someone objects",
|
|
119
|
+
"explanation": "It passes unless a member objects, once everyone has voted or the clock runs out."
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"key": "majority",
|
|
123
|
+
"name": "More than half",
|
|
124
|
+
"explanation": "More than half of all members must say yes."
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"key": "unanimous",
|
|
128
|
+
"name": "Everyone who votes agrees",
|
|
129
|
+
"explanation": "Everyone who votes must agree, and at least one member must say yes."
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"key": "veto_points",
|
|
135
|
+
"name": "Veto points",
|
|
136
|
+
"question": "How many checkpoints must a big change pass?",
|
|
137
|
+
"status": "later",
|
|
138
|
+
"explanation": "How many separate approvals a big change must collect before it happens."
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"key": "executive_legislative",
|
|
142
|
+
"name": "Executive–legislative relationship",
|
|
143
|
+
"question": "Does the leader serve at the team's confidence?",
|
|
144
|
+
"status": "later",
|
|
145
|
+
"explanation": "Whether the team can replace its leader by vote, or the leader holds office for a set term."
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"key": "judicial_review",
|
|
149
|
+
"name": "Judicial review",
|
|
150
|
+
"question": "Who can void a decision that breaks the charter?",
|
|
151
|
+
"status": "later",
|
|
152
|
+
"explanation": "Who may cancel a decision that goes against the project's charter."
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"key": "loyalty_of_enforcement",
|
|
156
|
+
"name": "Loyalty of enforcement",
|
|
157
|
+
"question": "Does power belong to the office or the person?",
|
|
158
|
+
"status": "always_on",
|
|
159
|
+
"explanation": "Whoever holds a rank holds its powers, and nobody keeps them after leaving it."
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"key": "trajectory",
|
|
163
|
+
"name": "Trajectory",
|
|
164
|
+
"question": "How does the government change as you grow?",
|
|
165
|
+
"status": "later",
|
|
166
|
+
"explanation": "Whether the government stays put, grows with the team, or changes on a schedule.",
|
|
167
|
+
"options": [
|
|
168
|
+
{
|
|
169
|
+
"key": "locked",
|
|
170
|
+
"name": "Locked",
|
|
171
|
+
"explanation": "The government stays as founded until the project votes to change it."
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"key": "laddered",
|
|
175
|
+
"name": "Laddered",
|
|
176
|
+
"explanation": "The government steps up automatically as the team crosses each size threshold."
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"key": "scheduled",
|
|
180
|
+
"name": "Scheduled",
|
|
181
|
+
"explanation": "The government changes on dates the project commits to in advance."
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
"reads_as": {
|
|
187
|
+
"form": "monarchy",
|
|
188
|
+
"name": "Monarchy / BDFL",
|
|
189
|
+
"explanation": "One leader decides. In a project the founder makes the calls, which is fast, clear and honest for a solo start.",
|
|
190
|
+
"franchise": "founder",
|
|
191
|
+
"pass_rule": "first_ratifier"
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
"headcount": 1,
|
|
195
|
+
"recommended": {
|
|
196
|
+
"key": "monarchy",
|
|
197
|
+
"name": "Monarchy / BDFL",
|
|
198
|
+
"rung": "solo/pair",
|
|
199
|
+
"why": "informal coordination has nothing to break yet — the founding monarchy is the honest shape",
|
|
200
|
+
"type": "not-sure",
|
|
201
|
+
"size": "solo",
|
|
202
|
+
"council_rank": null,
|
|
203
|
+
"later": null
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -108,7 +108,13 @@ const FEEDS = {
|
|
|
108
108
|
// /founding/builders (task 1004424) answers a never-founded project's empty list by default,
|
|
109
109
|
// so every existing render stays today's page; a founding variant states file points it at
|
|
110
110
|
// founding-builders-sample via stub.fixtures.
|
|
111
|
-
|
|
111
|
+
// /founding/genesis and /government/constitution (task 1004423) ship no default file: the
|
|
112
|
+
// genesis home asks for them only in founding mode, so every existing render is unchanged,
|
|
113
|
+
// and the genesis variants point them at their samples.
|
|
114
|
+
const AUTHED = {
|
|
115
|
+
'/my-projects': 'my-projects', '/my-projects/invites': 'my-projects__invites', '/founding/builders': 'founding-builders',
|
|
116
|
+
'/founding/genesis': 'founding-genesis', '/government/constitution': 'government-constitution',
|
|
117
|
+
};
|
|
112
118
|
const UNAUTH = { error: { code: 'unauthenticated', message: 'unauthenticated' } };
|
|
113
119
|
|
|
114
120
|
// GET /provisioning/starter-bundles as the create wizard's module picker reads it
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.58",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.58",
|
|
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.20.
|
|
3
|
+
"version": "1.20.58",
|
|
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
|
@@ -8453,5 +8453,17 @@
|
|
|
8453
8453
|
"id": "1004424",
|
|
8454
8454
|
"text": "Everyone who joins a project while it is being founded now becomes a founding builder, numbered in the order they arrived, and the owner is marked founder. The mark shows as a small seed badge on the builders list and on their"
|
|
8455
8455
|
}
|
|
8456
|
+
],
|
|
8457
|
+
"1.20.57": [
|
|
8458
|
+
{
|
|
8459
|
+
"id": "1004479",
|
|
8460
|
+
"text": "Each recommended rank on the Government page now shows just its name and one plain line of what it can do. The extra description that said the same thing twice is gone."
|
|
8461
|
+
}
|
|
8462
|
+
],
|
|
8463
|
+
"1.20.58": [
|
|
8464
|
+
{
|
|
8465
|
+
"id": "1004423",
|
|
8466
|
+
"text": "A project that is being founded now has a genesis home at the top of its hall. The founder settles a plan in one click (each stage's length, the starting government by its real name with a plain explanation, and how many passe"
|
|
8467
|
+
}
|
|
8456
8468
|
]
|
|
8457
8469
|
}
|
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.20.
|
|
78
|
+
const CORE_VERSION = '1.20.58'; // 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,288 @@
|
|
|
1
|
+
// tests/genesis_home.mjs — THE GENESIS HOME (task 1004423 / BV2.PS08, goal 1000121).
|
|
2
|
+
//
|
|
3
|
+
// Done-when: a founded project walks decide → five stages → alive, and the full hall
|
|
4
|
+
// opens. Spec D7: a stage closes when its tasks are finished AND the constitution in
|
|
5
|
+
// force decides it (the Board Room's genesis-stage sitting). D4: a solo founder gets a
|
|
6
|
+
// fast-track, defaults pre-picked and one confirm per stage, and genesis is never
|
|
7
|
+
// skipped. The blend rule: a hall that is not founding renders, and requests, exactly
|
|
8
|
+
// what it did before.
|
|
9
|
+
//
|
|
10
|
+
// Everything here RUNS: the real checklist and seeder over a fake pool, the real
|
|
11
|
+
// genesis-home flow over an in-memory project_settings store with fake lifecycle and
|
|
12
|
+
// government ports, and the real hall card in a vm.
|
|
13
|
+
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
import { test } from 'node:test';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import vm from 'node:vm';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
24
|
+
const { genesisChecklist } = require('../modules/lifecycle/kickoff-checklist.js');
|
|
25
|
+
const seed = require('../modules/lifecycle/kickoff-seed.js');
|
|
26
|
+
const gh = require('../modules/onboarding/genesis-home.js');
|
|
27
|
+
const fm = require('../modules/onboarding/founding-mode.js');
|
|
28
|
+
|
|
29
|
+
const STAGES5 = ['scope', 'strategize', 'charter', 'legalize', 'legislation'];
|
|
30
|
+
|
|
31
|
+
// ── the checklist + the seeder ───────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
test('genesis files a task per stage, keyed by a ref that names the step, not its place', () => {
|
|
34
|
+
const plain = genesisChecklist();
|
|
35
|
+
assert.deepEqual(Object.keys(plain), STAGES5);
|
|
36
|
+
for (const s of STAGES5) {
|
|
37
|
+
assert.equal(plain[s].length, 1);
|
|
38
|
+
assert.equal(plain[s][0].ref, `genesis:${s}`);
|
|
39
|
+
assert.equal(plain[s][0].kind, 'decision');
|
|
40
|
+
assert.equal(plain[s][0].discipline, 'ideator');
|
|
41
|
+
}
|
|
42
|
+
const existing = genesisChecklist({ existing: true });
|
|
43
|
+
assert.deepEqual(existing.scope.map((t) => t.ref), ['genesis:scope:existing', 'genesis:scope'],
|
|
44
|
+
'the brownfield step comes first and does not re-key scope\'s own step');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('the seeder files every genesis step once, under the catch-all, and never twice', async () => {
|
|
48
|
+
const filed = [];
|
|
49
|
+
const deps = {
|
|
50
|
+
pool: { query: async () => ({ rows: [{ id: 'V1' }] }) },
|
|
51
|
+
createTask: async (row) => { filed.push(row); },
|
|
52
|
+
findTaskBySourceRef: async ({ source, sourceRef }) => filed.find((r) => r.source === source && r.sourceRef === sourceRef) || null,
|
|
53
|
+
};
|
|
54
|
+
const first = await seed.seedGenesisStages({ builderId: 1 }, deps);
|
|
55
|
+
assert.equal(first.seeded, 5);
|
|
56
|
+
assert.ok(filed.every((r) => r.status === 'ready' && r.source === 'genesis' && r.allowCatchAll === true && r.versionId === 'V1'));
|
|
57
|
+
const again = await seed.seedGenesisStages({ builderId: 1, existing: true }, deps);
|
|
58
|
+
assert.equal(again.seeded, 1, 'a second run files only the step that was not there (the brownfield one)');
|
|
59
|
+
assert.equal(filed.length, 6);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('the board read groups genesis tasks by the stage their ref names', async () => {
|
|
63
|
+
const pool = { query: async (sql, params) => {
|
|
64
|
+
assert.deepEqual(params, ['genesis']);
|
|
65
|
+
return { rows: [
|
|
66
|
+
{ id: 1, title: 'a', status: 'shipped', source_ref: 'genesis:scope' },
|
|
67
|
+
{ id: 2, title: 'b', status: 'ready', source_ref: 'genesis:scope:existing' },
|
|
68
|
+
{ id: 3, title: 'c', status: 'ready', source_ref: 'genesis:charter' },
|
|
69
|
+
] };
|
|
70
|
+
} };
|
|
71
|
+
const out = await seed.genesisStageTasks({ pool });
|
|
72
|
+
assert.deepEqual(Object.keys(out), ['scope', 'charter']);
|
|
73
|
+
assert.deepEqual(out.scope.map((t) => t.id), ['1', '2']);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ── the plan ─────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
test('an empty plan is the recommended defaults: the solo fast-track is one click', () => {
|
|
79
|
+
const r = gh.parsePlan({});
|
|
80
|
+
assert.equal(r.ok, true);
|
|
81
|
+
assert.deepEqual(r.plan.days, { scope: 7, strategize: 7, charter: 4, legalize: 7, legislation: 3 });
|
|
82
|
+
assert.equal(r.plan.passes, 1);
|
|
83
|
+
assert.equal(r.plan.advisors, 'none');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('a plan out of range is refused with the reason', () => {
|
|
87
|
+
assert.equal(gh.parsePlan({ days: { scope: 0 } }).reason, 'bad_days');
|
|
88
|
+
assert.equal(gh.parsePlan({ passes: 3 }).reason, 'bad_passes');
|
|
89
|
+
assert.equal(gh.parsePlan({ advisors: 'robots' }).reason, 'bad_advisors');
|
|
90
|
+
assert.equal(gh.parsePlan({ government: 'Not A Key' }).reason, 'bad_government');
|
|
91
|
+
assert.equal(gh.parsePlan({ expected_builders: 0 }).reason, 'bad_expected_builders');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ── the flow, end to end over fakes ──────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
function world({ withGovernment = true } = {}) {
|
|
97
|
+
const store = new Map();
|
|
98
|
+
const tasks = {};
|
|
99
|
+
const board = {};
|
|
100
|
+
let now = new Date('2026-10-01T00:00:00Z');
|
|
101
|
+
const opened = [];
|
|
102
|
+
const lifecycle = {
|
|
103
|
+
seedGenesis: async () => {
|
|
104
|
+
for (const s of STAGES5) tasks[s] = [{ id: String(100 + STAGES5.indexOf(s)), title: s, status: 'ready' }];
|
|
105
|
+
return { seeded: 5 };
|
|
106
|
+
},
|
|
107
|
+
genesisTasks: async () => JSON.parse(JSON.stringify(tasks)),
|
|
108
|
+
};
|
|
109
|
+
const government = withGovernment ? {
|
|
110
|
+
genesisStageStatus: async () => ({ stages: STAGES5.map((key) => ({ key, ...(board[key] || { state: 'not_put', waiting_on: [] }) })) }),
|
|
111
|
+
openGenesisStageClose: async ({ stageKey, taskIds }) => {
|
|
112
|
+
opened.push({ stageKey, taskIds });
|
|
113
|
+
if (!(tasks[stageKey] || []).every((t) => ['shipped', 'abandoned'].includes(t.status))) {
|
|
114
|
+
return { opened: false, reason: 'stage_tasks_unfinished', unfinished: [{ task_id: taskIds[0] }] };
|
|
115
|
+
}
|
|
116
|
+
board[stageKey] = { state: 'sitting', item_id: '9', one_vote_decides: true, waiting_on: [{ builder_id: '1' }] };
|
|
117
|
+
return { opened: true, item: { id: 9 } };
|
|
118
|
+
},
|
|
119
|
+
} : null;
|
|
120
|
+
const deps = () => ({
|
|
121
|
+
getSetting: async (k) => (store.has(k) ? { key: k, value: store.get(k) } : null),
|
|
122
|
+
setSetting: async (k, v) => { store.set(k, String(v)); return { key: k, value: String(v) }; },
|
|
123
|
+
lifecycle, government, now,
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
store, tasks, board, opened, deps,
|
|
127
|
+
finish: (s) => tasks[s].forEach((t) => { t.status = 'shipped'; }),
|
|
128
|
+
pass: (s) => { board[s] = { state: 'passed', item_id: '9', waiting_on: [] }; },
|
|
129
|
+
tick: (days) => { now = new Date(now.getTime() + days * 86400000); },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
test('decide → five stages through the Board Room → alive, and the hall opens', async () => {
|
|
134
|
+
const w = world();
|
|
135
|
+
assert.equal(await gh.readGenesis(w.deps()), null, 'not founding: there is no genesis home');
|
|
136
|
+
assert.equal((await gh.startFounding(1, w.deps())).ok, true);
|
|
137
|
+
let g = await gh.readGenesis(w.deps());
|
|
138
|
+
assert.equal(g.deciding, true);
|
|
139
|
+
assert.equal(g.stage, 'decide');
|
|
140
|
+
|
|
141
|
+
const saved = await gh.savePlan({}, 1, w.deps());
|
|
142
|
+
assert.equal(saved.ok, true);
|
|
143
|
+
assert.equal(saved.seeded, 5);
|
|
144
|
+
assert.ok(JSON.parse(w.store.get(gh.PLAN_KEY)).days, 'the plan is recorded');
|
|
145
|
+
g = await gh.readGenesis(w.deps());
|
|
146
|
+
assert.equal(g.stage, 'scope');
|
|
147
|
+
assert.equal(g.can_close, false, 'a stage with unfinished tasks cannot be put to the board');
|
|
148
|
+
assert.equal((await gh.savePlan({}, 1, w.deps())).reason, 'not_deciding', 'the plan is settled once');
|
|
149
|
+
|
|
150
|
+
for (const s of STAGES5) {
|
|
151
|
+
assert.equal((await gh.readGenesis(w.deps())).stage, s);
|
|
152
|
+
assert.equal((await gh.closeStage(s, 1, w.deps())).reason, 'stage_tasks_unfinished', `${s}: the Board Room's gate refuses unfinished work`);
|
|
153
|
+
w.finish(s);
|
|
154
|
+
assert.equal((await gh.readGenesis(w.deps())).can_close, true);
|
|
155
|
+
const closed = await gh.closeStage(s, 1, w.deps());
|
|
156
|
+
assert.equal(closed.ok, true);
|
|
157
|
+
assert.equal(closed.item_id, '9');
|
|
158
|
+
assert.deepEqual(w.opened.at(-1), { stageKey: s, taskIds: [String(100 + STAGES5.indexOf(s))] }, 'the stage\'s own task ids go to the gate');
|
|
159
|
+
g = await gh.readGenesis(w.deps());
|
|
160
|
+
assert.equal(g.stage, s, 'sitting is not passing: the stage holds until the vote');
|
|
161
|
+
assert.equal(g.can_close, false);
|
|
162
|
+
if (s !== 'legislation') assert.equal((await gh.comeAlive(1, w.deps())).ok, false);
|
|
163
|
+
w.pass(s);
|
|
164
|
+
await gh.onStagePassed(w.deps());
|
|
165
|
+
}
|
|
166
|
+
g = await gh.readGenesis(w.deps());
|
|
167
|
+
assert.equal(g.stage, 'legislation', 'legislation does not advance past itself');
|
|
168
|
+
assert.equal(g.can_come_alive, true);
|
|
169
|
+
assert.equal((await gh.comeAlive(1, w.deps())).ok, true);
|
|
170
|
+
assert.equal(await gh.readGenesis(w.deps()), null, 'founding mode is off: the full hall opens');
|
|
171
|
+
assert.ok(w.store.get(fm.KEYS.ENDED_AT), 'the founding window closed, for the founding mark');
|
|
172
|
+
assert.equal((await gh.startFounding(1, w.deps())).reason, 'already_alive', 'genesis happens once');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('only the stage in progress can be put to the board', async () => {
|
|
176
|
+
const w = world();
|
|
177
|
+
await gh.startFounding(1, w.deps());
|
|
178
|
+
await gh.savePlan({}, 1, w.deps());
|
|
179
|
+
const r = await gh.closeStage('charter', 1, w.deps());
|
|
180
|
+
assert.equal(r.reason, 'not_current_stage');
|
|
181
|
+
assert.equal(w.opened.length, 0);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('a returned stage stays put and can be put up again', async () => {
|
|
185
|
+
const w = world();
|
|
186
|
+
await gh.startFounding(1, w.deps());
|
|
187
|
+
await gh.savePlan({}, 1, w.deps());
|
|
188
|
+
w.finish('scope');
|
|
189
|
+
w.board.scope = { state: 'returned', item_id: '9', waiting_on: [] };
|
|
190
|
+
const g = await gh.readGenesis(w.deps());
|
|
191
|
+
assert.equal(g.stage, 'scope');
|
|
192
|
+
assert.equal(g.can_close, true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('with no Board Room module the founder\'s confirm closes a finished stage', async () => {
|
|
196
|
+
const w = world({ withGovernment: false });
|
|
197
|
+
await gh.startFounding(1, w.deps());
|
|
198
|
+
await gh.savePlan({}, 1, w.deps());
|
|
199
|
+
assert.equal((await gh.closeStage('scope', 1, w.deps())).reason, 'stage_tasks_unfinished');
|
|
200
|
+
w.finish('scope');
|
|
201
|
+
assert.equal((await gh.closeStage('scope', 1, w.deps())).ok, true);
|
|
202
|
+
assert.equal((await gh.readGenesis(w.deps())).stage, 'strategize');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('the view: states, wakes, the life plates and a due date from the plan', () => {
|
|
206
|
+
const mode = { stage: 'strategize', stage_number: 2, stage_count: 5, day: 9 };
|
|
207
|
+
const g = gh.shapeGenesis({
|
|
208
|
+
mode, plan: { days: { scope: 7, strategize: 10, charter: 4, legalize: 7, legislation: 3 } },
|
|
209
|
+
tasks: { strategize: [{ id: '1', title: 't', status: 'active' }] },
|
|
210
|
+
stageAt: { strategize: '2026-10-08T00:00:00Z' },
|
|
211
|
+
});
|
|
212
|
+
assert.deepEqual(g.stages.map((s) => s.state), ['done', 'now', 'later', 'later', 'later']);
|
|
213
|
+
assert.deepEqual(g.stages[1].wakes, ['work']);
|
|
214
|
+
assert.equal(g.stages[1].plate, 'r5-life-2-first-cells');
|
|
215
|
+
assert.equal(g.stages[1].due, '2026-10-18T00:00:00.000Z');
|
|
216
|
+
assert.equal(g.stages[1].finished, 0);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ── the routes ───────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
test('every genesis write is the project owner\'s; the read is any signed-in builder\'s', () => {
|
|
222
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules', 'onboarding', 'routes', 'onboarding.js'), 'utf8');
|
|
223
|
+
assert.match(src, /const owns = api\.requirePermission\('project\.settings\.manage'\);/);
|
|
224
|
+
for (const p of ['/founding/start', '/founding/plan', '/founding/stages/:stage/close', '/founding/alive']) {
|
|
225
|
+
assert.ok(src.includes(`router.post('${p}', api.requireBuilder, owns,`), `${p} is owner-gated`);
|
|
226
|
+
}
|
|
227
|
+
assert.match(src, /router\.get\('\/founding\/genesis', api\.requireBuilder,/);
|
|
228
|
+
assert.match(src, /api\.on\('board\.genesis_stage\.passed'/, 'a pass moves the stage on without waiting for a read');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ── the hall card, RUN ───────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
const CARD = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'genesis-home.js'), 'utf8');
|
|
234
|
+
|
|
235
|
+
function bootCard({ founding }) {
|
|
236
|
+
const calls = [];
|
|
237
|
+
let widget = null;
|
|
238
|
+
let observer = null;
|
|
239
|
+
const attrs = founding ? { 'data-founding': 'on' } : {};
|
|
240
|
+
const sandbox = {
|
|
241
|
+
window: {
|
|
242
|
+
OTB: { escapeHtml: (s) => String(s), contributeHallWidget: (w) => { widget = w; } },
|
|
243
|
+
BongosClient: { createClient: () => ({ request: async (m, p) => { calls.push(`${m} ${p}`); return { ok: true, status: 200, data: { genesis: null } }; } }) },
|
|
244
|
+
},
|
|
245
|
+
document: { documentElement: { getAttribute: (k) => attrs[k] || null } },
|
|
246
|
+
MutationObserver: class { constructor(fn) { observer = fn; } observe() {} disconnect() {} },
|
|
247
|
+
Promise,
|
|
248
|
+
};
|
|
249
|
+
vm.createContext(sandbox);
|
|
250
|
+
vm.runInContext(CARD, sandbox, { filename: 'genesis-home.js' });
|
|
251
|
+
const host = { hidden: true, innerHTML: '', addEventListener() {}, querySelector: () => null, querySelectorAll: () => [] };
|
|
252
|
+
widget.mount(host);
|
|
253
|
+
return { widget, host, calls, attrs, fire: () => observer && observer() };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
test('the card registers in the Home banner, ahead of the welcome, hidden by default', () => {
|
|
257
|
+
const { widget } = bootCard({ founding: false });
|
|
258
|
+
assert.equal(widget.zone, 'home-banner');
|
|
259
|
+
assert.equal(widget.view, 'home');
|
|
260
|
+
assert.equal(widget.hiddenByDefault, true);
|
|
261
|
+
assert.ok(widget.order < 0);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('a hall that is not founding: no markup and no request', async () => {
|
|
265
|
+
const { host, calls } = bootCard({ founding: false });
|
|
266
|
+
await new Promise((r) => setImmediate(r));
|
|
267
|
+
assert.equal(host.innerHTML, '');
|
|
268
|
+
assert.deepEqual(calls, []);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('founding mode stamped after mount still starts the card, once', async () => {
|
|
272
|
+
const b = bootCard({ founding: false });
|
|
273
|
+
b.attrs['data-founding'] = 'on';
|
|
274
|
+
b.fire();
|
|
275
|
+
b.fire();
|
|
276
|
+
await new Promise((r) => setImmediate(r));
|
|
277
|
+
assert.deepEqual(b.calls, ['GET /api/bongos/founding/genesis']);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('the hall carries the five life plates byte-identical to the world\'s', () => {
|
|
281
|
+
const world = path.join(ROOT, 'modules', 'public-landing', 'public', 'assets', 'world');
|
|
282
|
+
const hall = path.join(ROOT, 'modules', 'hall-ui', 'public', 'genesis');
|
|
283
|
+
for (const s of Object.values(gh.STAGE_INFO)) {
|
|
284
|
+
for (const f of [`${s.plate}.jpg`, `${s.plate}-cut.webp`]) {
|
|
285
|
+
assert.ok(fs.readFileSync(path.join(world, f)).equals(fs.readFileSync(path.join(hall, f))), `${f} matches the world plate`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
@@ -129,4 +129,5 @@ test('the panel says what a recommended rank can do in words, never in permissio
|
|
|
129
129
|
const row = client.slice(client.indexOf('function recoRankRow'), client.indexOf('function recommendationHtml'));
|
|
130
130
|
assert.match(row, /r\.can/, 'the rank row renders the sentence');
|
|
131
131
|
assert.doesNotMatch(row, /permissions/, 'the rank row does not render the raw keys');
|
|
132
|
+
assert.doesNotMatch(row, /r\.description/, 'one line per rank: the description is not repeated above the Can line (task 1004479)');
|
|
132
133
|
});
|