@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
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// modules/lifecycle/routes/kickoff-seed.js — registers the `lifecycle.kickoffSeed` port
|
|
4
4
|
// (task 1003239): the founder's first board, filed the moment the kernel seats them.
|
|
5
5
|
//
|
|
6
|
-
// Resolved by
|
|
6
|
+
// Resolved by two callers — the first-admin bootstrap in src/bongos/auth-admission.js
|
|
7
|
+
// (seedIfEmpty) and the genesis home in modules/onboarding (seedGenesis, genesisTasks) —
|
|
7
8
|
// through resolveOptional, so an instance without this module signs in exactly as
|
|
8
9
|
// before. A route factory for a routeless capability, for the reason routes/lifecycle.js
|
|
9
10
|
// and routes/rank-sync.js give: mountModuleRoutes is the boot hook that runs in the live
|
|
@@ -17,6 +18,11 @@ const seed = require('../kickoff-seed');
|
|
|
17
18
|
// Delegates at call time (never spreads) so a stubbed seed still bites.
|
|
18
19
|
const kickoffSeedPort = {
|
|
19
20
|
seedIfEmpty: (...a) => seed.seedKickoffIfEmpty(...a),
|
|
21
|
+
// task 1004423: the genesis home files the five stages through the same seeder, and
|
|
22
|
+
// reads them back by stage. Still no HTTP surface here: the genesis home's own routes
|
|
23
|
+
// (modules/onboarding) gate who may file them.
|
|
24
|
+
seedGenesis: (...a) => seed.seedGenesisStages(...a),
|
|
25
|
+
genesisTasks: (...a) => seed.genesisStageTasks(...a),
|
|
20
26
|
};
|
|
21
27
|
|
|
22
28
|
module.exports = function buildLifecycleKickoffSeedRouter() {
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/onboarding/genesis-home.js — THE GENESIS HOME: what a founding project does,
|
|
4
|
+
// stage by stage, until it comes alive (task 1004423 / BV2.PS08, goal 1000121; spec
|
|
5
|
+
// docs/specs/bongos-v2-project-startup.md D4 + D7; design of record
|
|
6
|
+
// docs/design/mocks/project-startup/GenesisDecide|GenesisHall|Alive.dc.html).
|
|
7
|
+
//
|
|
8
|
+
// The flow, in founding-mode.js's stages:
|
|
9
|
+
// decide the founder settles a plan: each stage's length and what counts as
|
|
10
|
+
// done, the starting government, how many passes, optional advisors.
|
|
11
|
+
// Saving it files the five stages as REAL tasks (the kickoff seeder, on
|
|
12
|
+
// the lifecycle port) and moves the project to scope.
|
|
13
|
+
// scope .. each stage is worked as tasks. When they are all finished (shipped or
|
|
14
|
+
// legislation dropped) the stage is put to the Board Room under the constitution in
|
|
15
|
+
// force (the government port's genesis-stage subject, spec D7). Under
|
|
16
|
+
// Monarchy / BDFL the founder's one yes closes it, which is the solo
|
|
17
|
+
// fast-track's "one confirm per stage" (D4). A pass moves the project to
|
|
18
|
+
// the next stage, and the rooms that stage woke stop sleeping
|
|
19
|
+
// (founding-mode.js WAKES, which the shell already reads).
|
|
20
|
+
// alive once legislation has passed, the founder brings the project to life:
|
|
21
|
+
// founding mode switches off and the full hall opens.
|
|
22
|
+
//
|
|
23
|
+
// WHERE THE STATE LIVES. The stage is founding-mode's (project_settings). The plan is
|
|
24
|
+
// one more project_settings row, `founding.plan`, JSON. The tasks are the board's. The
|
|
25
|
+
// decision is the Board Room's: a stage moves on only because a sitting PASSED, read
|
|
26
|
+
// back from the government port, never because a client said so. A pass is announced
|
|
27
|
+
// as `board.genesis_stage.passed` (the route factory listens), and every read
|
|
28
|
+
// reconciles again, so a missed nudge cannot strand a stage.
|
|
29
|
+
//
|
|
30
|
+
// THE BOUNDARY. Core through ../../src/module-api.js; the lifecycle and government
|
|
31
|
+
// modules through their ports only (resolveOptional), never a require.
|
|
32
|
+
|
|
33
|
+
const fm = require('./founding-mode');
|
|
34
|
+
|
|
35
|
+
const PLAN_KEY = 'founding.plan';
|
|
36
|
+
const PROPER = Object.freeze(fm.STAGES.slice(1)); // the five stages after 'decide'
|
|
37
|
+
|
|
38
|
+
// Each stage's life-strip plate (the world pair under the hall's genesis/ folder, task
|
|
39
|
+
// 1004425), what the design calls that moment of life, and its plain done-when line.
|
|
40
|
+
const STAGE_INFO = Object.freeze({
|
|
41
|
+
scope: Object.freeze({ label: 'Scope', life: 'primordial sea', plate: 'r5-life-1-primordial-sea', days: 7,
|
|
42
|
+
done_when: 'The scope is written: what you are making, for whom, and what is left out.' }),
|
|
43
|
+
strategize: Object.freeze({ label: 'Strategize', life: 'first cells', plate: 'r5-life-2-first-cells', days: 7,
|
|
44
|
+
done_when: 'The strategy is written out, or the one you have is linked.' }),
|
|
45
|
+
charter: Object.freeze({ label: 'Charter', life: 'the genome', plate: 'r5-life-3-genome', days: 4,
|
|
46
|
+
done_when: 'The starting government is chosen and the bylaws are written.' }),
|
|
47
|
+
legalize: Object.freeze({ label: 'Legalize', life: 'the membrane', plate: 'r5-life-4-membrane', days: 7,
|
|
48
|
+
done_when: 'Rewards are decided, and who can be paid.' }),
|
|
49
|
+
legislation: Object.freeze({ label: 'Legislation', life: 'the nervous system', plate: 'r5-life-5-nervous-system', days: 3,
|
|
50
|
+
done_when: 'The voting rules are set: who votes, and how many must agree.' }),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const PASSES = Object.freeze([1, 2]);
|
|
54
|
+
const ADVISORS = Object.freeze(['none', 'cbaas']);
|
|
55
|
+
const MAX_DAYS = 90;
|
|
56
|
+
|
|
57
|
+
const intIn = (v, lo, hi) => Number.isInteger(v) && v >= lo && v <= hi;
|
|
58
|
+
|
|
59
|
+
// parsePlan(body) — PURE. The founder's plan, defaults filled in, or why it is refused.
|
|
60
|
+
// The government is recorded, not applied: the charter stage (task 1004427) files it as
|
|
61
|
+
// an amendment, so here it is only a well-formed form key.
|
|
62
|
+
function parsePlan(body = {}) {
|
|
63
|
+
const b = body && typeof body === 'object' ? body : {};
|
|
64
|
+
const days = {};
|
|
65
|
+
for (const s of PROPER) {
|
|
66
|
+
const raw = b.days && b.days[s] != null ? Number(b.days[s]) : STAGE_INFO[s].days;
|
|
67
|
+
if (!intIn(raw, 1, MAX_DAYS)) return { ok: false, reason: 'bad_days', stage: s };
|
|
68
|
+
days[s] = raw;
|
|
69
|
+
}
|
|
70
|
+
const passes = b.passes == null ? 1 : Number(b.passes);
|
|
71
|
+
if (!PASSES.includes(passes)) return { ok: false, reason: 'bad_passes' };
|
|
72
|
+
const advisors = b.advisors == null ? 'none' : String(b.advisors);
|
|
73
|
+
if (!ADVISORS.includes(advisors)) return { ok: false, reason: 'bad_advisors' };
|
|
74
|
+
const government = b.government == null || b.government === '' ? null : String(b.government);
|
|
75
|
+
if (government !== null && !/^[a-z][a-z-]{1,39}$/.test(government)) return { ok: false, reason: 'bad_government' };
|
|
76
|
+
const expected = b.expected_builders == null || b.expected_builders === '' ? null : Number(b.expected_builders);
|
|
77
|
+
if (expected !== null && !intIn(expected, 1, 100000)) return { ok: false, reason: 'bad_expected_builders' };
|
|
78
|
+
return { ok: true, plan: { days, passes, advisors, government, expected_builders: expected, existing: b.existing === true } };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readPlanValue(raw) {
|
|
82
|
+
if (!raw) return null;
|
|
83
|
+
try { const p = JSON.parse(raw); return p && typeof p === 'object' ? p : null; } catch (_) { return null; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const FINISHED = Object.freeze(['shipped', 'abandoned']); // the Board Room's own gate, mirrored for display
|
|
87
|
+
|
|
88
|
+
// shapeGenesis({ mode, plan, tasks, board, stageAt }) — PURE. The genesis home's view.
|
|
89
|
+
// mode foundingModeFor's shape (non-null)
|
|
90
|
+
// plan the saved plan or null (deciding)
|
|
91
|
+
// tasks { stage: [{ id, title, status }] }
|
|
92
|
+
// board { stage: genesisStageStatus row } or {}
|
|
93
|
+
// stageAt { stage: ISO } — when each stage began, for its due date
|
|
94
|
+
function shapeGenesis({ mode, plan, tasks = {}, board = {}, stageAt = {} }) {
|
|
95
|
+
const at = fm.STAGES.indexOf(mode.stage);
|
|
96
|
+
const stages = PROPER.map((key) => {
|
|
97
|
+
const info = STAGE_INFO[key];
|
|
98
|
+
const idx = fm.STAGES.indexOf(key);
|
|
99
|
+
const list = (tasks[key] || []).map((t) => ({ ...t, finished: FINISHED.includes(t.status) }));
|
|
100
|
+
const b = board[key] || null;
|
|
101
|
+
const days = plan && plan.days && plan.days[key] ? plan.days[key] : info.days;
|
|
102
|
+
const began = stageAt[key] ? new Date(stageAt[key]) : null;
|
|
103
|
+
const due = began && Number.isFinite(began.getTime()) ? new Date(began.getTime() + days * 86400000).toISOString() : null;
|
|
104
|
+
return {
|
|
105
|
+
key,
|
|
106
|
+
number: idx,
|
|
107
|
+
label: info.label,
|
|
108
|
+
life: info.life,
|
|
109
|
+
plate: info.plate,
|
|
110
|
+
done_when: info.done_when,
|
|
111
|
+
wakes: fm.WAKES[key] || [],
|
|
112
|
+
days,
|
|
113
|
+
due,
|
|
114
|
+
state: idx < at ? 'done' : idx === at ? 'now' : 'later',
|
|
115
|
+
tasks: list,
|
|
116
|
+
finished: list.filter((t) => t.finished).length,
|
|
117
|
+
total: list.length,
|
|
118
|
+
board: b ? {
|
|
119
|
+
state: b.state, item_id: b.item_id || null, needs: b.needs || null,
|
|
120
|
+
one_vote_decides: !!b.one_vote_decides, waiting_on: b.waiting_on || [],
|
|
121
|
+
} : null,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
const now = stages.find((s) => s.state === 'now') || null;
|
|
125
|
+
const sitting = now && now.board && now.board.state === 'sitting';
|
|
126
|
+
const passed = now && now.board && now.board.state === 'passed';
|
|
127
|
+
return {
|
|
128
|
+
stage: mode.stage,
|
|
129
|
+
stage_number: mode.stage_number,
|
|
130
|
+
stage_count: mode.stage_count,
|
|
131
|
+
day: mode.day,
|
|
132
|
+
deciding: mode.stage === 'decide',
|
|
133
|
+
plan: plan || null,
|
|
134
|
+
stages,
|
|
135
|
+
// The close door: every task of the current stage finished, and not already sitting.
|
|
136
|
+
can_close: !!(now && now.total > 0 && now.finished === now.total && !sitting && !passed),
|
|
137
|
+
// Alive: the last stage has passed its sitting.
|
|
138
|
+
can_come_alive: !!(now && now.key === 'legislation' && passed),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── the reads and writes, over injectable seams ──────────────────────────────
|
|
143
|
+
|
|
144
|
+
function seams(deps = {}) {
|
|
145
|
+
const api = deps.api || require('../../src/module-api');
|
|
146
|
+
return {
|
|
147
|
+
get: deps.getSetting || api.projectSettings.get,
|
|
148
|
+
set: deps.setSetting || api.projectSettings.set,
|
|
149
|
+
lifecycle: deps.lifecycle !== undefined ? deps.lifecycle : api.resolveOptional('lifecycle.kickoffSeed'),
|
|
150
|
+
government: deps.government !== undefined ? deps.government : api.resolveOptional('government'),
|
|
151
|
+
now: deps.now,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const modeDeps = (s) => ({ getSetting: s.get, setSetting: s.set, government: s.government, now: s.now });
|
|
155
|
+
|
|
156
|
+
async function boardByStage(gov) {
|
|
157
|
+
if (!gov || typeof gov.genesisStageStatus !== 'function') return {};
|
|
158
|
+
const status = await gov.genesisStageStatus();
|
|
159
|
+
const out = {};
|
|
160
|
+
for (const row of (status && status.stages) || []) out[row.key] = row;
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// reconcile — move on from every stage whose sitting has passed. The ONLY thing that
|
|
165
|
+
// advances a stage, called on the pass event and on every read and write. Legislation
|
|
166
|
+
// does not advance: its pass opens the alive door instead.
|
|
167
|
+
async function reconcile(s) {
|
|
168
|
+
let mode = await fm.foundingModeFor(modeDeps(s));
|
|
169
|
+
if (!mode || !PROPER.includes(mode.stage) || mode.stage === 'legislation') return mode;
|
|
170
|
+
const board = await boardByStage(s.government);
|
|
171
|
+
while (mode && PROPER.includes(mode.stage) && mode.stage !== 'legislation'
|
|
172
|
+
&& board[mode.stage] && board[mode.stage].state === 'passed') {
|
|
173
|
+
const next = fm.STAGES[fm.STAGES.indexOf(mode.stage) + 1];
|
|
174
|
+
mode = await fm.setFoundingMode({ stage: next }, null, modeDeps(s));
|
|
175
|
+
}
|
|
176
|
+
return mode;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// readGenesis(deps) → the view, or null when the project is not founding.
|
|
180
|
+
async function readGenesis(deps = {}) {
|
|
181
|
+
const s = seams(deps);
|
|
182
|
+
const mode = await reconcile(s);
|
|
183
|
+
if (!mode) return null;
|
|
184
|
+
const planRow = await s.get(PLAN_KEY);
|
|
185
|
+
const plan = readPlanValue(planRow && planRow.value);
|
|
186
|
+
const tasks = s.lifecycle && typeof s.lifecycle.genesisTasks === 'function' ? await s.lifecycle.genesisTasks() : {};
|
|
187
|
+
const board = await boardByStage(s.government);
|
|
188
|
+
const stageAt = {};
|
|
189
|
+
for (const key of PROPER) {
|
|
190
|
+
const row = await s.get(fm.stageAtKey(key));
|
|
191
|
+
if (row && row.value) stageAt[key] = row.value;
|
|
192
|
+
}
|
|
193
|
+
return shapeGenesis({ mode, plan, tasks, board, stageAt });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// startFounding — a brand-new project enters genesis (the create flow's birth calls
|
|
197
|
+
// this). Refused once the project has come alive: genesis happens once (spec D8).
|
|
198
|
+
async function startFounding(builderId, deps = {}) {
|
|
199
|
+
const s = seams(deps);
|
|
200
|
+
const ended = await s.get(fm.KEYS.ENDED_AT);
|
|
201
|
+
if (ended && ended.value) return { ok: false, reason: 'already_alive' };
|
|
202
|
+
const mode = await fm.setFoundingMode({ active: true }, builderId, modeDeps(s));
|
|
203
|
+
return { ok: true, mode };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// savePlan — the decide stage's door: record the plan, file the five stages as tasks,
|
|
207
|
+
// and move to scope. Only while deciding.
|
|
208
|
+
async function savePlan(body, builderId, deps = {}) {
|
|
209
|
+
const s = seams(deps);
|
|
210
|
+
const mode = await fm.foundingModeFor(modeDeps(s));
|
|
211
|
+
if (!mode) return { ok: false, reason: 'not_founding' };
|
|
212
|
+
if (mode.stage !== 'decide') return { ok: false, reason: 'not_deciding' };
|
|
213
|
+
const parsed = parsePlan(body);
|
|
214
|
+
if (!parsed.ok) return parsed;
|
|
215
|
+
if (!s.lifecycle || typeof s.lifecycle.seedGenesis !== 'function') return { ok: false, reason: 'no_board' };
|
|
216
|
+
await s.set(PLAN_KEY, JSON.stringify(parsed.plan), builderId);
|
|
217
|
+
const seeded = await s.lifecycle.seedGenesis({ builderId, existing: parsed.plan.existing });
|
|
218
|
+
await fm.setFoundingMode({ stage: 'scope' }, builderId, modeDeps(s));
|
|
219
|
+
return { ok: true, plan: parsed.plan, seeded: seeded && seeded.seeded };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// closeStage — put the current stage to the Board Room. The finished-tasks gate is the
|
|
223
|
+
// government module's (it checks the ids against the board); this only names the
|
|
224
|
+
// stage's tasks. With no government module there is no Board Room to put it to, and
|
|
225
|
+
// the founder's confirm closes the stage itself once its tasks are finished.
|
|
226
|
+
async function closeStage(stageKey, builderId, deps = {}) {
|
|
227
|
+
const s = seams(deps);
|
|
228
|
+
const mode = await reconcile(s);
|
|
229
|
+
if (!mode) return { ok: false, reason: 'not_founding' };
|
|
230
|
+
if (mode.stage !== stageKey) return { ok: false, reason: 'not_current_stage', stage: mode.stage };
|
|
231
|
+
const all = s.lifecycle && typeof s.lifecycle.genesisTasks === 'function' ? await s.lifecycle.genesisTasks() : {};
|
|
232
|
+
const list = all[stageKey] || [];
|
|
233
|
+
const gov = s.government;
|
|
234
|
+
if (gov && typeof gov.openGenesisStageClose === 'function') {
|
|
235
|
+
const r = await gov.openGenesisStageClose({ stageKey, taskIds: list.map((t) => t.id), openedBy: builderId });
|
|
236
|
+
if (!r.opened && r.reason !== 'already_open') return { ok: false, reason: r.reason, unfinished: r.unfinished || [] };
|
|
237
|
+
return { ok: true, item_id: r.item ? String(r.item.id) : null, already_open: !r.opened };
|
|
238
|
+
}
|
|
239
|
+
if (!list.length || !list.every((t) => FINISHED.includes(t.status))) return { ok: false, reason: 'stage_tasks_unfinished' };
|
|
240
|
+
if (stageKey !== 'legislation') {
|
|
241
|
+
await fm.setFoundingMode({ stage: fm.STAGES[fm.STAGES.indexOf(stageKey) + 1] }, builderId, modeDeps(s));
|
|
242
|
+
}
|
|
243
|
+
return { ok: true, item_id: null, closed: true };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// comeAlive — the last door. Only once legislation has passed (or, with no Board Room,
|
|
247
|
+
// once its tasks are finished): founding mode switches off and every room opens.
|
|
248
|
+
async function comeAlive(builderId, deps = {}) {
|
|
249
|
+
const s = seams(deps);
|
|
250
|
+
const mode = await reconcile(s);
|
|
251
|
+
if (!mode) return { ok: false, reason: 'not_founding' };
|
|
252
|
+
if (mode.stage !== 'legislation') return { ok: false, reason: 'not_at_legislation' };
|
|
253
|
+
if (s.government && typeof s.government.genesisStageStatus === 'function') {
|
|
254
|
+
const board = await boardByStage(s.government);
|
|
255
|
+
if (!board.legislation || board.legislation.state !== 'passed') return { ok: false, reason: 'legislation_not_passed' };
|
|
256
|
+
} else {
|
|
257
|
+
const all = s.lifecycle && typeof s.lifecycle.genesisTasks === 'function' ? await s.lifecycle.genesisTasks() : {};
|
|
258
|
+
const list = all.legislation || [];
|
|
259
|
+
if (!list.length || !list.every((t) => FINISHED.includes(t.status))) return { ok: false, reason: 'legislation_not_passed' };
|
|
260
|
+
}
|
|
261
|
+
await fm.setFoundingMode({ active: false }, builderId, modeDeps(s));
|
|
262
|
+
return { ok: true };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// onStagePassed — the `board.genesis_stage.passed` listener. Best-effort: the next read
|
|
266
|
+
// reconciles anyway.
|
|
267
|
+
async function onStagePassed(deps = {}) {
|
|
268
|
+
try { await reconcile(seams(deps)); } catch (_) { /* recovered on the next read */ }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = {
|
|
272
|
+
PLAN_KEY, STAGE_INFO, PROPER, parsePlan, shapeGenesis,
|
|
273
|
+
readGenesis, startFounding, savePlan, closeStage, comeAlive, onStagePassed,
|
|
274
|
+
};
|
|
@@ -35,6 +35,7 @@ const api = require('../../../src/module-api');
|
|
|
35
35
|
const onboardingPort = require('../port');
|
|
36
36
|
const buildAccessRequestsRouter = require('./access-requests');
|
|
37
37
|
const foundingBuilders = require('../founding-builders');
|
|
38
|
+
const genesis = require('../genesis-home');
|
|
38
39
|
|
|
39
40
|
// Register the `onboarding` kernel PORT. See the header + port.js for the full
|
|
40
41
|
// consumer list and the graceful-degradation contract.
|
|
@@ -61,5 +62,60 @@ module.exports = function buildOnboardingRouter() {
|
|
|
61
62
|
res.set('Cache-Control', 'private, max-age=60');
|
|
62
63
|
res.json({ builders });
|
|
63
64
|
});
|
|
65
|
+
mountGenesisRoutes(router);
|
|
64
66
|
return router;
|
|
65
67
|
};
|
|
68
|
+
|
|
69
|
+
// ── the genesis home (task 1004423) ──────────────────────────────────────────
|
|
70
|
+
// Read by any signed-in builder of a founding project (everyone sees where genesis
|
|
71
|
+
// is); every write is the project owner's, behind the same atom that guards the
|
|
72
|
+
// project's other settings. The decision that moves a stage on is NOT here: it is the
|
|
73
|
+
// Board Room's vote, and these routes only put a finished stage to it.
|
|
74
|
+
const GENESIS_REFUSALS = {
|
|
75
|
+
not_founding: 409, not_deciding: 409, not_current_stage: 409, already_alive: 409,
|
|
76
|
+
not_at_legislation: 409, legislation_not_passed: 409, stage_tasks_unfinished: 409,
|
|
77
|
+
no_stage_tasks: 409, unknown_stage: 400, no_board: 503,
|
|
78
|
+
bad_days: 400, bad_passes: 400, bad_advisors: 400, bad_government: 400, bad_expected_builders: 400,
|
|
79
|
+
};
|
|
80
|
+
let genesisListening = false;
|
|
81
|
+
|
|
82
|
+
function mountGenesisRoutes(router) {
|
|
83
|
+
// A stage's sitting passed: move the project on now rather than at the next read.
|
|
84
|
+
// Registered once per process; the route factory runs again on a test rebuild.
|
|
85
|
+
if (!genesisListening && typeof api.on === 'function') {
|
|
86
|
+
genesisListening = true;
|
|
87
|
+
api.on('board.genesis_stage.passed', () => genesis.onStagePassed());
|
|
88
|
+
}
|
|
89
|
+
const owns = api.requirePermission('project.settings.manage');
|
|
90
|
+
const refuse = (res, r) => res.status(GENESIS_REFUSALS[r.reason] || 409).json({
|
|
91
|
+
error: { code: r.reason, message: r.reason }, ...(r.unfinished ? { unfinished: r.unfinished } : {}),
|
|
92
|
+
});
|
|
93
|
+
const handle = (fn) => async (req, res) => {
|
|
94
|
+
try {
|
|
95
|
+
const r = await fn(req);
|
|
96
|
+
if (r && r.ok === false) return refuse(res, r);
|
|
97
|
+
res.set('Cache-Control', 'no-store');
|
|
98
|
+
res.json(r);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
api.logger('onboarding').error('[onboarding] genesis', err);
|
|
101
|
+
res.status(500).json({ error: { code: 'genesis_failed', message: 'internal error' } });
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
router.get('/founding/genesis', api.requireBuilder, handle(async (req) => ({ genesis: await genesis.readGenesis(), viewer_id: String(req.builder.id) })));
|
|
105
|
+
router.post('/founding/start', api.requireBuilder, owns, handle((req) => genesis.startFounding(req.builder.id)));
|
|
106
|
+
// The plan's ranges and vocabularies are genesis-home.js parsePlan's (one source);
|
|
107
|
+
// this schema only names the fields so an unknown one is refused.
|
|
108
|
+
router.post('/founding/plan', api.requireBuilder, owns, (req, res, next) => {
|
|
109
|
+
if (api.validateOrRespond(req, res, {
|
|
110
|
+
days: { type: 'object' },
|
|
111
|
+
passes: { type: 'integer', min: 1, max: 2 },
|
|
112
|
+
advisors: { type: 'string', maxLength: 16 },
|
|
113
|
+
government: { type: 'string', maxLength: 40 },
|
|
114
|
+
expected_builders: { type: 'integer', min: 1, max: 100000 },
|
|
115
|
+
existing: { type: 'boolean' },
|
|
116
|
+
})) return;
|
|
117
|
+
next();
|
|
118
|
+
}, handle((req) => genesis.savePlan(req.body || {}, req.builder.id)));
|
|
119
|
+
router.post('/founding/stages/:stage/close', api.requireBuilder, owns, handle((req) => genesis.closeStage(String(req.params.stage), req.builder.id)));
|
|
120
|
+
router.post('/founding/alive', api.requireBuilder, owns, handle((req) => genesis.comeAlive(req.builder.id)));
|
|
121
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The genesis home while deciding (task 1004423): the plan step, every default pre-picked.",
|
|
3
|
+
"genesis": {
|
|
4
|
+
"stage": "decide",
|
|
5
|
+
"stage_number": 0,
|
|
6
|
+
"stage_count": 5,
|
|
7
|
+
"day": 1,
|
|
8
|
+
"deciding": true,
|
|
9
|
+
"plan": null,
|
|
10
|
+
"stages": [
|
|
11
|
+
{
|
|
12
|
+
"key": "scope",
|
|
13
|
+
"number": 1,
|
|
14
|
+
"label": "Scope",
|
|
15
|
+
"life": "primordial sea",
|
|
16
|
+
"plate": "r5-life-1-primordial-sea",
|
|
17
|
+
"done_when": "The scope is written: what you are making, for whom, and what is left out.",
|
|
18
|
+
"wakes": [
|
|
19
|
+
"roadmap",
|
|
20
|
+
"goals",
|
|
21
|
+
"goal-map"
|
|
22
|
+
],
|
|
23
|
+
"days": 7,
|
|
24
|
+
"due": null,
|
|
25
|
+
"state": "later",
|
|
26
|
+
"tasks": [],
|
|
27
|
+
"finished": 0,
|
|
28
|
+
"total": 0,
|
|
29
|
+
"board": null
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"key": "strategize",
|
|
33
|
+
"number": 2,
|
|
34
|
+
"label": "Strategize",
|
|
35
|
+
"life": "first cells",
|
|
36
|
+
"plate": "r5-life-2-first-cells",
|
|
37
|
+
"done_when": "The strategy is written out, or the one you have is linked.",
|
|
38
|
+
"wakes": [
|
|
39
|
+
"work"
|
|
40
|
+
],
|
|
41
|
+
"days": 7,
|
|
42
|
+
"due": null,
|
|
43
|
+
"state": "later",
|
|
44
|
+
"tasks": [],
|
|
45
|
+
"finished": 0,
|
|
46
|
+
"total": 0,
|
|
47
|
+
"board": null
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"key": "charter",
|
|
51
|
+
"number": 3,
|
|
52
|
+
"label": "Charter",
|
|
53
|
+
"life": "the genome",
|
|
54
|
+
"plate": "r5-life-3-genome",
|
|
55
|
+
"done_when": "The starting government is chosen and the bylaws are written.",
|
|
56
|
+
"wakes": [
|
|
57
|
+
"board-room"
|
|
58
|
+
],
|
|
59
|
+
"days": 4,
|
|
60
|
+
"due": null,
|
|
61
|
+
"state": "later",
|
|
62
|
+
"tasks": [],
|
|
63
|
+
"finished": 0,
|
|
64
|
+
"total": 0,
|
|
65
|
+
"board": null
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"key": "legalize",
|
|
69
|
+
"number": 4,
|
|
70
|
+
"label": "Legalize",
|
|
71
|
+
"life": "the membrane",
|
|
72
|
+
"plate": "r5-life-4-membrane",
|
|
73
|
+
"done_when": "Rewards are decided, and who can be paid.",
|
|
74
|
+
"wakes": [
|
|
75
|
+
"economy"
|
|
76
|
+
],
|
|
77
|
+
"days": 7,
|
|
78
|
+
"due": null,
|
|
79
|
+
"state": "later",
|
|
80
|
+
"tasks": [],
|
|
81
|
+
"finished": 0,
|
|
82
|
+
"total": 0,
|
|
83
|
+
"board": null
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"key": "legislation",
|
|
87
|
+
"number": 5,
|
|
88
|
+
"label": "Legislation",
|
|
89
|
+
"life": "the nervous system",
|
|
90
|
+
"plate": "r5-life-5-nervous-system",
|
|
91
|
+
"done_when": "The voting rules are set: who votes, and how many must agree.",
|
|
92
|
+
"wakes": [
|
|
93
|
+
"ranks"
|
|
94
|
+
],
|
|
95
|
+
"days": 3,
|
|
96
|
+
"due": null,
|
|
97
|
+
"state": "later",
|
|
98
|
+
"tasks": [],
|
|
99
|
+
"finished": 0,
|
|
100
|
+
"total": 0,
|
|
101
|
+
"board": null
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"can_close": false,
|
|
105
|
+
"can_come_alive": false
|
|
106
|
+
},
|
|
107
|
+
"viewer_id": "1"
|
|
108
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The genesis home mid-way (task 1004423): scope passed, strategize in progress with its task active.",
|
|
3
|
+
"genesis": {
|
|
4
|
+
"stage": "strategize",
|
|
5
|
+
"stage_number": 2,
|
|
6
|
+
"stage_count": 5,
|
|
7
|
+
"day": 9,
|
|
8
|
+
"deciding": false,
|
|
9
|
+
"plan": {
|
|
10
|
+
"days": {
|
|
11
|
+
"scope": 7,
|
|
12
|
+
"strategize": 7,
|
|
13
|
+
"charter": 4,
|
|
14
|
+
"legalize": 7,
|
|
15
|
+
"legislation": 3
|
|
16
|
+
},
|
|
17
|
+
"passes": 2,
|
|
18
|
+
"advisors": "none",
|
|
19
|
+
"government": "monarchy",
|
|
20
|
+
"expected_builders": 3,
|
|
21
|
+
"existing": false
|
|
22
|
+
},
|
|
23
|
+
"stages": [
|
|
24
|
+
{
|
|
25
|
+
"key": "scope",
|
|
26
|
+
"number": 1,
|
|
27
|
+
"label": "Scope",
|
|
28
|
+
"life": "primordial sea",
|
|
29
|
+
"plate": "r5-life-1-primordial-sea",
|
|
30
|
+
"done_when": "The scope is written: what you are making, for whom, and what is left out.",
|
|
31
|
+
"wakes": [
|
|
32
|
+
"roadmap",
|
|
33
|
+
"goals",
|
|
34
|
+
"goal-map"
|
|
35
|
+
],
|
|
36
|
+
"days": 7,
|
|
37
|
+
"due": "2026-10-08T00:00:00.000Z",
|
|
38
|
+
"state": "done",
|
|
39
|
+
"tasks": [
|
|
40
|
+
{
|
|
41
|
+
"id": "101",
|
|
42
|
+
"title": "Write the project scope: what we are making, for whom, and what is out of scope",
|
|
43
|
+
"status": "shipped",
|
|
44
|
+
"finished": true
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"finished": 1,
|
|
48
|
+
"total": 1,
|
|
49
|
+
"board": {
|
|
50
|
+
"state": "passed",
|
|
51
|
+
"item_id": "7",
|
|
52
|
+
"needs": null,
|
|
53
|
+
"one_vote_decides": true,
|
|
54
|
+
"waiting_on": []
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"key": "strategize",
|
|
59
|
+
"number": 2,
|
|
60
|
+
"label": "Strategize",
|
|
61
|
+
"life": "first cells",
|
|
62
|
+
"plate": "r5-life-2-first-cells",
|
|
63
|
+
"done_when": "The strategy is written out, or the one you have is linked.",
|
|
64
|
+
"wakes": [
|
|
65
|
+
"work"
|
|
66
|
+
],
|
|
67
|
+
"days": 7,
|
|
68
|
+
"due": "2026-10-15T00:00:00.000Z",
|
|
69
|
+
"state": "now",
|
|
70
|
+
"tasks": [
|
|
71
|
+
{
|
|
72
|
+
"id": "102",
|
|
73
|
+
"title": "Write out the strategy, or link the one you already have",
|
|
74
|
+
"status": "active",
|
|
75
|
+
"finished": false
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"finished": 0,
|
|
79
|
+
"total": 1,
|
|
80
|
+
"board": null
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"key": "charter",
|
|
84
|
+
"number": 3,
|
|
85
|
+
"label": "Charter",
|
|
86
|
+
"life": "the genome",
|
|
87
|
+
"plate": "r5-life-3-genome",
|
|
88
|
+
"done_when": "The starting government is chosen and the bylaws are written.",
|
|
89
|
+
"wakes": [
|
|
90
|
+
"board-room"
|
|
91
|
+
],
|
|
92
|
+
"days": 4,
|
|
93
|
+
"due": null,
|
|
94
|
+
"state": "later",
|
|
95
|
+
"tasks": [
|
|
96
|
+
{
|
|
97
|
+
"id": "103",
|
|
98
|
+
"title": "Choose the starting government and write the bylaws",
|
|
99
|
+
"status": "ready",
|
|
100
|
+
"finished": false
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
"finished": 0,
|
|
104
|
+
"total": 1,
|
|
105
|
+
"board": null
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"key": "legalize",
|
|
109
|
+
"number": 4,
|
|
110
|
+
"label": "Legalize",
|
|
111
|
+
"life": "the membrane",
|
|
112
|
+
"plate": "r5-life-4-membrane",
|
|
113
|
+
"done_when": "Rewards are decided, and who can be paid.",
|
|
114
|
+
"wakes": [
|
|
115
|
+
"economy"
|
|
116
|
+
],
|
|
117
|
+
"days": 7,
|
|
118
|
+
"due": null,
|
|
119
|
+
"state": "later",
|
|
120
|
+
"tasks": [
|
|
121
|
+
{
|
|
122
|
+
"id": "104",
|
|
123
|
+
"title": "Decide how contributors are rewarded and who can be paid",
|
|
124
|
+
"status": "ready",
|
|
125
|
+
"finished": false
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"finished": 0,
|
|
129
|
+
"total": 1,
|
|
130
|
+
"board": null
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"key": "legislation",
|
|
134
|
+
"number": 5,
|
|
135
|
+
"label": "Legislation",
|
|
136
|
+
"life": "the nervous system",
|
|
137
|
+
"plate": "r5-life-5-nervous-system",
|
|
138
|
+
"done_when": "The voting rules are set: who votes, and how many must agree.",
|
|
139
|
+
"wakes": [
|
|
140
|
+
"ranks"
|
|
141
|
+
],
|
|
142
|
+
"days": 3,
|
|
143
|
+
"due": null,
|
|
144
|
+
"state": "later",
|
|
145
|
+
"tasks": [
|
|
146
|
+
{
|
|
147
|
+
"id": "105",
|
|
148
|
+
"title": "Set the voting rules: who votes and how many must agree",
|
|
149
|
+
"status": "ready",
|
|
150
|
+
"finished": false
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
"finished": 0,
|
|
154
|
+
"total": 1,
|
|
155
|
+
"board": null
|
|
156
|
+
}
|
|
157
|
+
],
|
|
158
|
+
"can_close": false,
|
|
159
|
+
"can_come_alive": false
|
|
160
|
+
},
|
|
161
|
+
"viewer_id": "1"
|
|
162
|
+
}
|