@bongos/core 1.20.57 → 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.
Files changed (43) hide show
  1. package/.bongos-core.json +126 -31
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +10 -0
  4. package/clients/bongos-client/index.cjs +10 -0
  5. package/clients/bongos-client/index.d.ts +12 -2
  6. package/clients/bongos-client/index.mjs +10 -0
  7. package/docs/api/openapi.json +211 -17
  8. package/docs/api-reference.md +7 -2
  9. package/docs/copy-inventory.md +20 -4
  10. package/docs/copy-registry.json +160 -6
  11. package/docs/module-api-changelog.md +2 -0
  12. package/docs/page-inventory.json +8 -3
  13. package/docs/page-readings.json +48 -48
  14. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea-cut.webp +0 -0
  15. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
  16. package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
  17. package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
  18. package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
  19. package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
  20. package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
  21. package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
  22. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
  23. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
  24. package/modules/hall-ui/public/genesis-home.js +349 -0
  25. package/modules/hall-ui/public/index-genesis-decide.states.json +13 -0
  26. package/modules/hall-ui/public/index-genesis.states.json +13 -0
  27. package/modules/hall-ui/public/index.html +2 -0
  28. package/modules/hall-ui/public/style.css +65 -0
  29. package/modules/hall-ui/records/genesis-home.md +18 -0
  30. package/modules/lifecycle/kickoff-checklist.js +54 -1
  31. package/modules/lifecycle/kickoff-seed.js +61 -2
  32. package/modules/lifecycle/routes/kickoff-seed.js +7 -1
  33. package/modules/onboarding/genesis-home.js +274 -0
  34. package/modules/onboarding/routes/onboarding.js +56 -0
  35. package/modules/ui-design/kit/fixtures/founding-genesis-decide.json +108 -0
  36. package/modules/ui-design/kit/fixtures/founding-genesis-stage.json +162 -0
  37. package/modules/ui-design/kit/fixtures/government-constitution-sample.json +205 -0
  38. package/modules/ui-design/kit/serve.js +7 -1
  39. package/package-lock.json +2 -2
  40. package/package.json +1 -1
  41. package/release-notes.json +6 -0
  42. package/src/module-api.js +1 -1
  43. package/tests/genesis_home.mjs +288 -0
@@ -0,0 +1,349 @@
1
+ // genesis-home.js — THE GENESIS HOME on the hall's Home view (task 1004423 / BV2.PS08).
2
+ //
3
+ // A founding project's Home leads with where genesis is: deciding the plan, then the
4
+ // five stages (scope, strategize, charter, legalize, legislation) worked as real tasks
5
+ // and closed through the Board Room, then the moment the project comes alive. The
6
+ // design of record is docs/design/mocks/project-startup/GenesisDecide, GenesisHall and
7
+ // Alive.dc.html; the server is modules/onboarding/genesis-home.js.
8
+ //
9
+ // THE BLEND RULE. Genesis is a mode the existing hall wears, so this is one more card
10
+ // in the Home banner, built from the hall's own parts (.scroll, .ledger, .btn-accent,
11
+ // the rank-badge pill grammar). It renders ONLY while html[data-founding="on"], which
12
+ // the shell stamps from /me before any widget mounts, and it never fetches otherwise:
13
+ // a hall that is not founding renders, and requests, exactly what it did before.
14
+ //
15
+ // The AI genesis guide in the canvas is out of scope (the task says so); each stage
16
+ // shows its plain done-when line in its place.
17
+ (function () {
18
+ 'use strict';
19
+ if (!window.OTB || typeof window.OTB.contributeHallWidget !== 'function') return;
20
+ var API = '/api/bongos';
21
+ var esc = function (s) { return window.OTB.escapeHtml(s); };
22
+ var toast = function (m) { if (window.OTB.toast) window.OTB.toast(m); };
23
+
24
+ var ROOM = {
25
+ roadmap: 'Roadmap', goals: 'Goals', 'goal-map': 'Goal map', work: 'Tasks',
26
+ 'board-room': 'Board Room', economy: 'Economy', ranks: 'Ranks',
27
+ };
28
+ var DAY_CHOICES = [1, 2, 3, 4, 5, 7, 10, 14, 21, 30];
29
+ var REFUSED = {
30
+ stage_tasks_unfinished: 'This stage still has unfinished tasks.',
31
+ not_current_stage: 'That stage is not the one in progress.',
32
+ legislation_not_passed: 'Legislation has to pass the Board Room first.',
33
+ not_deciding: 'The plan is already settled.',
34
+ };
35
+
36
+ function projectName() {
37
+ var b = window.__BRANDING__ || {};
38
+ return (b.identity && b.identity.productName) || 'the project';
39
+ }
40
+ function api() {
41
+ return window.BongosClient
42
+ ? window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false })
43
+ : null;
44
+ }
45
+ function send(method, path, body) {
46
+ var c = api();
47
+ if (!c) return Promise.reject(new Error('no client'));
48
+ return c.request(method, API + path, body ? { body: body } : undefined).then(function (res) {
49
+ if (res.ok) return res.data;
50
+ var e = new Error(path + ' → ' + res.status);
51
+ e.status = res.status;
52
+ e.code = res.data && res.data.error && res.data.error.code;
53
+ throw e;
54
+ });
55
+ }
56
+ function refusal(err) {
57
+ if (err && err.status === 403) return 'Only the project owner can do that.';
58
+ return (err && REFUSED[err.code]) || 'That did not go through. Try again in a moment.';
59
+ }
60
+
61
+ // The world pair: the black-ground JPEG in dark, the cutout in light (style.css picks).
62
+ function plateHtml(slug, cls) {
63
+ return '<span class="gplate ' + (cls || '') + '" aria-hidden="true">'
64
+ + '<img class="gplate__dark" src="/builders/genesis/' + esc(slug) + '.jpg" alt="" loading="lazy">'
65
+ + '<img class="gplate__light" src="/builders/genesis/' + esc(slug) + '-cut.webp" alt="" loading="lazy">'
66
+ + '</span>';
67
+ }
68
+
69
+ function stepperHtml(g) {
70
+ var names = ['decide'].concat(g.stages.map(function (s) { return s.key; }));
71
+ return '<ol class="genesis__steps" aria-label="Genesis stages">' + names.map(function (k, i) {
72
+ var cls = i < g.stage_number ? 'is-done' : i === g.stage_number ? 'is-now' : '';
73
+ return '<li class="genesis__step ' + cls + '"' + (i === g.stage_number ? ' aria-current="step"' : '') + '>'
74
+ + '<span class="genesis__step-n">' + i + '</span>' + esc(k) + '</li>';
75
+ }).join('') + '</ol>';
76
+ }
77
+
78
+ function headHtml(g) {
79
+ var now = g.stages.find(function (s) { return s.state === 'now'; });
80
+ var sub = g.deciding
81
+ ? 'Deciding the plan' + (g.day ? ' · day ' + g.day : '')
82
+ : 'Stage ' + g.stage_number + ' of ' + g.stage_count + ' · ' + (now ? now.label.toLowerCase() : g.stage) + (g.day ? ' · day ' + g.day : '');
83
+ return '<div class="genesis__head"><h2 id="genesis-h" class="scroll__h">Genesis</h2>'
84
+ + '<p class="genesis__sub">' + esc(sub) + '</p></div>' + stepperHtml(g);
85
+ }
86
+
87
+ // ── decide ────────────────────────────────────────────────────────────────
88
+
89
+ function decideHtml(g, gov) {
90
+ var rows = g.stages.map(function (s) {
91
+ var opts = DAY_CHOICES.map(function (d) {
92
+ return '<option value="' + d + '"' + (d === s.days ? ' selected' : '') + '>' + d + (d === 1 ? ' day' : ' days') + '</option>';
93
+ }).join('');
94
+ return '<li class="genesis__plan-row">'
95
+ + '<span class="genesis__plan-n">' + s.number + '</span>'
96
+ + '<span class="genesis__plan-t"><b>' + esc(s.label) + '</b><span>' + esc(s.done_when) + '</span></span>'
97
+ + '<label class="genesis__plan-len"><span class="sr-only">Length of ' + esc(s.label) + '</span>'
98
+ + '<select data-days="' + esc(s.key) + '">' + opts + '</select></label></li>';
99
+ }).join('');
100
+ return '<p class="scroll__lede">Settle the plan, then genesis files each stage as real tasks on the board. '
101
+ + 'Every choice below is already set to its recommended default, so on your own one click is enough.</p>'
102
+ + '<ol class="genesis__plan">' + rows + '</ol>'
103
+ + '<div class="genesis__picks">'
104
+ + governmentHtml(gov)
105
+ + '<fieldset class="genesis__pick"><legend>How many passes</legend>'
106
+ + radio('passes', '1', 'One', 'Each stage is decided once.', true)
107
+ + radio('passes', '2', 'Two', 'Once now, and again when your next builders join.', false)
108
+ + '</fieldset>'
109
+ + '<fieldset class="genesis__pick"><legend>Advisors</legend>'
110
+ + radio('advisors', 'none', 'None for now', 'You can add advisors at any stage.', true)
111
+ + radio('advisors', 'cbaas', 'CBaaS advisors', 'Ask for Cloud Bongos consultants to review each stage.', false)
112
+ + '</fieldset>'
113
+ + '<label class="genesis__check"><input type="checkbox" data-existing> This project already exists, so start by gathering what it has</label>'
114
+ + '</div>'
115
+ + '<div class="genesis__doors"><button type="button" class="btn-accent" data-genesis-plan>Use this plan</button></div>';
116
+ }
117
+
118
+ function radio(name, value, label, hint, checked) {
119
+ return '<label class="genesis__radio"><input type="radio" name="genesis-' + name + '" value="' + esc(value) + '"'
120
+ + (checked ? ' checked' : '') + '><span><b>' + esc(label) + '</b><span>' + esc(hint) + '</span></span></label>';
121
+ }
122
+
123
+ // The starting government: the real academic names, each with its plain sentence
124
+ // (spec D10), the recommended one picked. Read from the government module's own
125
+ // constitution view so the names and explanations are never typed twice.
126
+ function governmentHtml(gov) {
127
+ if (!gov || !gov.forms || !gov.forms.length) return '';
128
+ var rec = gov.recommended && gov.recommended.key;
129
+ var avail = gov.forms.filter(function (f) { return f.available; });
130
+ var later = gov.forms.filter(function (f) { return !f.available; });
131
+ return '<fieldset class="genesis__pick genesis__pick--gov"><legend>Starting government</legend>'
132
+ + '<label class="genesis__expected">Builders you expect in the first year '
133
+ + '<input type="number" min="1" max="100000" value="' + esc(String(gov.headcount || 1)) + '" data-expected></label>'
134
+ + avail.map(function (f) {
135
+ var hint = f.explanation + (f.key === rec && gov.recommended.why ? ' Recommended: ' + gov.recommended.why : '');
136
+ return radio('gov', f.key, f.name + (f.key === rec ? ' · recommended' : ''), hint, f.key === rec);
137
+ }).join('')
138
+ + (later.length ? '<p class="genesis__later">Coming later: ' + later.map(function (f) { return esc(f.name); }).join(', ') + '.</p>' : '')
139
+ + '</fieldset>';
140
+ }
141
+
142
+ // ── the stages ────────────────────────────────────────────────────────────
143
+
144
+ function lifeHtml(g) {
145
+ return '<ol class="genesis__life" aria-label="The project’s life so far">' + g.stages.map(function (s) {
146
+ return '<li class="genesis__life-item is-' + s.state + '">' + plateHtml(s.plate)
147
+ + '<span class="genesis__life-cap">' + esc(s.key) + '<b>' + esc(s.life) + '</b></span></li>';
148
+ }).join('') + '</ol>';
149
+ }
150
+
151
+ function stateText(s) {
152
+ if (s.state === 'done') return 'done';
153
+ if (s.state === 'later') return 'later';
154
+ return s.total ? s.finished + ' of ' + s.total + ' done' : 'in progress';
155
+ }
156
+
157
+ function stagesHtml(g) {
158
+ return '<ol class="genesis__stages">' + g.stages.map(function (s) {
159
+ var wakes = (s.wakes || []).map(function (id) { return ROOM[id] || id; });
160
+ var due = s.state === 'now' && s.due ? ' · due ' + new Date(s.due).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : '';
161
+ return '<li class="genesis__stage is-' + s.state + '">'
162
+ + '<span class="genesis__stage-n">0' + s.number + '</span>'
163
+ + '<span class="genesis__stage-t"><b>' + esc(s.label) + '</b>'
164
+ + '<span>' + esc(wakes.length ? 'Wakes ' + wakes.join(', ') : '') + esc(due) + '</span></span>'
165
+ + '<span class="genesis__stage-st">' + esc(stateText(s)) + '</span></li>';
166
+ }).join('') + '</ol>';
167
+ }
168
+
169
+ function nowHtml(g, viewerId) {
170
+ var s = g.stages.find(function (x) { return x.state === 'now'; });
171
+ if (!s) return '';
172
+ var tasks = s.tasks.length
173
+ ? '<ul class="genesis__tasks">' + s.tasks.map(function (t) {
174
+ return '<li class="genesis__task' + (t.finished ? ' is-finished' : '') + '">'
175
+ + '<a class="genesis__task-t" href="/task/' + encodeURIComponent(t.id) + '">' + esc(t.title) + '</a>'
176
+ + '<span class="genesis__task-st">' + esc(t.status) + '</span></li>';
177
+ }).join('') + '</ul>'
178
+ : '<p class="genesis__note">No tasks for this stage yet.</p>';
179
+ return '<section class="genesis__now" aria-labelledby="genesis-now-h">'
180
+ + '<h3 id="genesis-now-h" class="genesis__now-h">This stage’s work · ' + esc(s.label) + '</h3>'
181
+ + '<p class="genesis__note">Done when: ' + esc(s.done_when) + '</p>'
182
+ + tasks + closeHtml(g, s, viewerId) + '</section>';
183
+ }
184
+
185
+ // Who the close is waiting on, and the one door the viewer has (spec D7).
186
+ function closeHtml(g, s, viewerId) {
187
+ if (g.can_come_alive) {
188
+ return '<div class="genesis__doors"><p class="genesis__note">Legislation has passed. Genesis is complete.</p>'
189
+ + '<button type="button" class="btn-accent" data-genesis-alive>Bring ' + esc(projectName()) + ' to life</button></div>';
190
+ }
191
+ var b = s.board;
192
+ if (b && b.state === 'sitting') {
193
+ var names = (b.waiting_on || []).map(function (m) { return m.display_name || m.github_login; }).filter(Boolean);
194
+ var mine = (b.waiting_on || []).some(function (m) { return String(m.builder_id) === String(viewerId); });
195
+ return '<div class="genesis__doors"><p class="genesis__note">Put to the Board Room. '
196
+ + esc(names.length ? 'Waiting on ' + names.join(', ') + '.' : 'Waiting on the board.') + ' ' + esc(b.needs || '') + '</p>'
197
+ + (mine && b.item_id ? '<button type="button" class="btn-accent" data-genesis-confirm="' + esc(b.item_id) + '">Confirm this stage</button>' : '')
198
+ + '<a class="btn-ghost" href="/board-room">Open the Board Room</a></div>';
199
+ }
200
+ if (b && b.state === 'returned') {
201
+ return '<div class="genesis__doors"><p class="genesis__note">The board sent this stage back. Read why in the Board Room, then put it up again.</p>'
202
+ + (g.can_close ? '<button type="button" class="btn-accent" data-genesis-close="' + esc(s.key) + '">Put it to the board again</button>' : '')
203
+ + '<a class="btn-ghost" href="/board-room">Open the Board Room</a></div>';
204
+ }
205
+ if (g.can_close) {
206
+ return '<div class="genesis__doors"><button type="button" class="btn-accent" data-genesis-close="' + esc(s.key) + '">Close this stage</button></div>';
207
+ }
208
+ return '<p class="genesis__note">When every task above is finished, this stage can be put to the board.</p>';
209
+ }
210
+
211
+ function aliveHtml() {
212
+ var name = projectName();
213
+ return '<div class="genesis__alive">'
214
+ + '<div class="genesis__alive-plates">' + ['r5-life-1-primordial-sea', 'r5-life-2-first-cells', 'r5-life-3-genome', 'r5-life-4-membrane', 'r5-life-5-nervous-system']
215
+ .map(function (p) { return plateHtml(p, 'gplate--sm'); }).join('') + '</div>'
216
+ + '<h2 id="genesis-h" class="genesis__alive-h">' + esc(name) + ' is alive</h2>'
217
+ + '<p class="scroll__lede">Genesis is complete. Every room is open now: Community, the Builders list and the rest of the hall. '
218
+ + 'The founding builders keep their mark for good.</p>'
219
+ + '<div class="genesis__doors"><button type="button" class="btn-accent" data-genesis-enter>Enter the builders’ hall</button></div></div>';
220
+ }
221
+
222
+ // ── mount ─────────────────────────────────────────────────────────────────
223
+
224
+ function founding() {
225
+ return document.documentElement.getAttribute('data-founding') === 'on';
226
+ }
227
+
228
+ function render(host, state) {
229
+ var g = state.genesis;
230
+ if (!g) { host.hidden = true; return; }
231
+ host.innerHTML = headHtml(g) + (g.deciding
232
+ ? decideHtml(g, state.gov)
233
+ : lifeHtml(g) + '<div class="genesis__grid">' + stagesHtml(g) + nowHtml(g, state.viewer) + '</div>');
234
+ host.hidden = false;
235
+ }
236
+
237
+ function loadGov(expected) {
238
+ var q = expected ? '?headcount=' + encodeURIComponent(expected) : '';
239
+ return send('GET', '/government/constitution' + q).then(function (c) {
240
+ var ch = c && c.charter;
241
+ return ch ? { forms: ch.forms || [], recommended: c.recommended || null, headcount: expected || c.headcount || 1 } : null;
242
+ }).catch(function () { return null; });
243
+ }
244
+
245
+ function load(host, state) {
246
+ return send('GET', '/founding/genesis').then(function (d) {
247
+ state.genesis = d && d.genesis;
248
+ state.viewer = d && d.viewer_id;
249
+ if (state.genesis && state.genesis.deciding && !state.gov) {
250
+ return loadGov(null).then(function (gov) { state.gov = gov; render(host, state); });
251
+ }
252
+ render(host, state);
253
+ return null;
254
+ }).catch(function () { host.hidden = true; });
255
+ }
256
+
257
+ function planBody(host) {
258
+ var days = {};
259
+ host.querySelectorAll('[data-days]').forEach(function (el) { days[el.getAttribute('data-days')] = Number(el.value); });
260
+ var pick = function (n) { var el = host.querySelector('input[name="genesis-' + n + '"]:checked'); return el ? el.value : null; };
261
+ var exp = host.querySelector('[data-expected]');
262
+ var existing = host.querySelector('[data-existing]');
263
+ return {
264
+ days: days,
265
+ passes: Number(pick('passes') || 1),
266
+ advisors: pick('advisors') || 'none',
267
+ government: pick('gov'),
268
+ expected_builders: exp && exp.value ? Number(exp.value) : null,
269
+ existing: !!(existing && existing.checked),
270
+ };
271
+ }
272
+
273
+ function wire(host, state) {
274
+ var busy = false;
275
+ function act(p) {
276
+ if (busy) return;
277
+ busy = true;
278
+ p.catch(function (err) { toast(refusal(err)); })
279
+ .then(function () { busy = false; });
280
+ }
281
+ host.addEventListener('change', function (e) {
282
+ if (!e.target.matches('[data-expected]')) return;
283
+ var n = Number(e.target.value);
284
+ if (!(n >= 1)) return;
285
+ loadGov(n).then(function (gov) {
286
+ if (!gov) return;
287
+ var keep = planBody(host);
288
+ state.gov = gov;
289
+ render(host, state);
290
+ Object.keys(keep.days).forEach(function (k) { var el = host.querySelector('[data-days="' + k + '"]'); if (el) el.value = keep.days[k]; });
291
+ });
292
+ });
293
+ host.addEventListener('click', function (e) {
294
+ var t = e.target.closest('button');
295
+ if (!t || !host.contains(t)) return;
296
+ if (t.hasAttribute('data-genesis-plan')) {
297
+ act(send('POST', '/founding/plan', planBody(host)).then(function () { window.location.reload(); }));
298
+ } else if (t.hasAttribute('data-genesis-close')) {
299
+ // The solo fast-track (spec D4): when the founder's one yes decides the sitting
300
+ // and nobody else is asked, the close and the confirm are one click.
301
+ act(send('POST', '/founding/stages/' + encodeURIComponent(t.getAttribute('data-genesis-close')) + '/close', {})
302
+ .then(function () { return send('GET', '/founding/genesis'); })
303
+ .then(function (d) {
304
+ var g = d && d.genesis;
305
+ var s = g && g.stages.find(function (x) { return x.state === 'now'; });
306
+ var b = s && s.board;
307
+ var solo = b && b.state === 'sitting' && b.one_vote_decides && b.item_id
308
+ && (b.waiting_on || []).length === 1 && String(b.waiting_on[0].builder_id) === String(d.viewer_id);
309
+ return solo ? send('POST', '/government/board/items/' + encodeURIComponent(b.item_id) + '/votes', { direction: 'yes' }) : null;
310
+ })
311
+ .then(function () { return load(host, state); }));
312
+ } else if (t.hasAttribute('data-genesis-confirm')) {
313
+ act(send('POST', '/government/board/items/' + encodeURIComponent(t.getAttribute('data-genesis-confirm')) + '/votes', { direction: 'yes' })
314
+ .then(function () { return load(host, state); }));
315
+ } else if (t.hasAttribute('data-genesis-alive')) {
316
+ act(send('POST', '/founding/alive', {}).then(function () { host.innerHTML = aliveHtml(); }));
317
+ } else if (t.hasAttribute('data-genesis-enter')) {
318
+ window.location.reload();
319
+ }
320
+ });
321
+ }
322
+
323
+ window.OTB.contributeHallWidget({
324
+ // Order -1: ahead of the welcome card (0), because in founding mode the genesis home
325
+ // IS the landing (the direction doc's Part 2 table). It renders nothing otherwise, so
326
+ // outside founding the banner's order is exactly today's.
327
+ id: 'genesis-home', label: 'Genesis', order: -1, core: true,
328
+ view: 'home', zone: 'home-banner', labelledby: 'genesis-h', hiddenByDefault: true,
329
+ sectionClass: 'scroll scroll--genesis',
330
+ // Widgets mount before /me has been applied, so the founding attribute may not be
331
+ // stamped yet. Wait for it rather than asking the server: a hall that never turns
332
+ // founding on never makes a request, and the card stays hidden and empty.
333
+ mount: function (host) {
334
+ var started = false;
335
+ function start() {
336
+ if (started || !founding()) return;
337
+ started = true;
338
+ var state = { genesis: null, gov: null, viewer: null };
339
+ wire(host, state);
340
+ load(host, state);
341
+ }
342
+ start();
343
+ if (!started && typeof MutationObserver === 'function') {
344
+ var mo = new MutationObserver(function () { start(); if (started) mo.disconnect(); });
345
+ mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-founding'] });
346
+ }
347
+ },
348
+ });
349
+ }());
@@ -0,0 +1,13 @@
1
+ {
2
+ "_": "The genesis home on Home (task 1004423) while the founder decides the plan: each stage's length and done-when line, the starting government by its real name with its plain explanation (the recommended one picked, from government-constitution-sample.json), passes and advisors, every default pre-picked so a solo founder needs one click. /founding/genesis answers founding-genesis-decide.json, built from the real server shaper.",
3
+ "page": "/builders/",
4
+ "surface": "hall-ui",
5
+ "stub": { "prefix": "/builders/", "fixtures": { "me": "me-founding", "founding-genesis": "founding-genesis-decide", "government-constitution": "government-constitution-sample" } },
6
+ "modeQuery": false,
7
+ "ignoreRequests": ["/api/(gds|bongos)/me$", "/api/(gds|bongos)/me/", "/api/(gds|bongos)/tasks", "/api/(gds|bongos)/claims", "/api/(gds|bongos)/goals", "/api/(gds|bongos)/inbox", "/api/(gds|bongos)/live", "/api/(gds|bongos)/analytics/", "/api/(gds|bongos)/achievements", "/api/(gds|bongos)/versions", "/api/(gds|bongos)/leaderboard", "/api/(gds|bongos)/removal-proposals", "/api/(gds|bongos)/blockers"],
8
+ "_ignored": "removal-proposals and blockers are fixtures the kit stub does not carry; index.states.json's home render reports the same two 404s.",
9
+ "ignoreConsole": ["/api/(gds|bongos)/", "\\b401\\b", "\\b404\\b", "Unauthorized", "Not Found", "load failed"],
10
+ "states": {
11
+ "decide": { "auth": true, "actions": [["wait", 800]], "expect": { "visible": ["#genesis-home", "[data-genesis-plan]"] } }
12
+ }
13
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "_": "The genesis home on Home (task 1004423), mid-genesis: scope passed, strategize in progress. /me says founding (kit fixture me-founding.json) and /founding/genesis answers founding-genesis-stage.json, built from the real server shaper. The deciding step is index-genesis-decide.states.json; a hall that is not founding is index.states.json, where the card never renders and never asks. Run it with --states (Home has no last path segment).",
3
+ "page": "/builders/",
4
+ "surface": "hall-ui",
5
+ "stub": { "prefix": "/builders/", "fixtures": { "me": "me-founding", "founding-genesis": "founding-genesis-stage" } },
6
+ "modeQuery": false,
7
+ "ignoreRequests": ["/api/(gds|bongos)/me$", "/api/(gds|bongos)/me/", "/api/(gds|bongos)/tasks", "/api/(gds|bongos)/claims", "/api/(gds|bongos)/goals", "/api/(gds|bongos)/inbox", "/api/(gds|bongos)/live", "/api/(gds|bongos)/analytics/", "/api/(gds|bongos)/achievements", "/api/(gds|bongos)/versions", "/api/(gds|bongos)/leaderboard", "/api/(gds|bongos)/removal-proposals", "/api/(gds|bongos)/blockers"],
8
+ "_ignored": "removal-proposals and blockers are fixtures the kit stub does not carry; index.states.json's home render reports the same two 404s.",
9
+ "ignoreConsole": ["/api/(gds|bongos)/", "\\b401\\b", "\\b404\\b", "Unauthorized", "Not Found", "load failed"],
10
+ "states": {
11
+ "genesis": { "auth": true, "actions": [["wait", 800]], "expect": { "visible": ["#genesis-home", "#founding-band"] } }
12
+ }
13
+ }
@@ -121,6 +121,8 @@
121
121
  (serve-internal.js injectModuleHallWidgets). -->
122
122
  <script src="/builders/founding-mark.js?v=2026-09-30-founding" defer></script>
123
123
  <script src="/builders/hall-widgets.js?v=2026-09-30-founding" defer></script>
124
+ <!-- The genesis home (task 1004423): a Home banner card that renders only in founding mode. -->
125
+ <script src="/builders/genesis-home.js?v=2026-10-01-genesis" defer></script>
124
126
  <!-- BV1.R78 (task 1694): the Goals widget registers into the same registry. -->
125
127
  <script src="/builders/goals.js?v=2026-07-01-goals" defer></script>
126
128
  <!-- task 1003455: the shared blocker-row renderer (window.OTBBlockersLib), read by
@@ -733,6 +733,71 @@ main:focus { outline: none; }
733
733
  .founding-wall__seed { width: 14px; height: 14px; margin-bottom: 4px; box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 18%, transparent); }
734
734
  .founding-wall__name { padding: 4px 6px; margin: -4px -6px; font-weight: 500; color: var(--ink); }
735
735
  .founding-wall__sub { font-size: 11.5px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-faint); }
736
+ /* task 1004423 (BV2.PS08): THE GENESIS HOME — a Home banner card that exists only in
737
+ founding mode (genesis-home.js renders nothing otherwise). Built from the hall's
738
+ parts: the .scroll panel, hairline rows like the ledger, the rank-badge pill
739
+ grammar for the stepper, the accent for "now". */
740
+ .genesis__head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
741
+ .genesis__sub { margin: 0; font-size: 13px; color: var(--ink-soft); }
742
+ .genesis__steps { list-style: none; margin: 12px 0 18px; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
743
+ .genesis__step { display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 10px 0 4px; border-radius: 999px;
744
+ font-size: 11.5px; font-weight: 600; letter-spacing: 0.02em; color: var(--ink-faint); border: 1px solid var(--rule); }
745
+ .genesis__step-n { display: inline-grid; place-items: center; width: 16px; height: 16px; border-radius: 50%; font-size: 10px; background: var(--rule-soft); }
746
+ .genesis__step.is-done { color: var(--ink-soft); }
747
+ .genesis__step.is-now { color: var(--ink); border-color: color-mix(in srgb, var(--accent) 45%, transparent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
748
+ .genesis__step.is-now .genesis__step-n { background: var(--accent); color: var(--bg); }
749
+ .genesis__plan, .genesis__stages, .genesis__tasks, .genesis__life { list-style: none; margin: 0; padding: 0; }
750
+ .genesis__plan-row, .genesis__stage, .genesis__task { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-top: 1px solid var(--rule-soft); }
751
+ .genesis__plan-row:first-child, .genesis__stage:first-child, .genesis__task:first-child { border-top: 0; }
752
+ .genesis__plan-n, .genesis__stage-n { flex: none; width: 24px; font-family: var(--font-mono, monospace); font-size: 12px; color: var(--ink-faint); }
753
+ .genesis__plan-t, .genesis__stage-t { flex: 1; min-width: 0; display: grid; gap: 2px; }
754
+ .genesis__plan-t span, .genesis__stage-t span { font-size: 12.5px; color: var(--ink-soft); }
755
+ .genesis__plan-len select { min-height: 32px; }
756
+ .genesis__picks { display: grid; gap: 14px; margin-top: 16px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
757
+ .genesis__pick { margin: 0; padding: 0; border: 0; display: grid; gap: 6px; }
758
+ .genesis__pick legend { padding: 0; margin-bottom: 4px; font-size: 11.5px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-soft); }
759
+ .genesis__pick--gov { grid-column: 1 / -1; }
760
+ .genesis__radio { display: flex; gap: 10px; align-items: flex-start; min-height: 24px; cursor: pointer; }
761
+ .genesis__radio input { margin-top: 3px; flex: none; }
762
+ .genesis__radio > span { display: grid; gap: 1px; }
763
+ .genesis__radio > span span { font-size: 12.5px; color: var(--ink-soft); }
764
+ .genesis__expected { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 13px; color: var(--ink-soft); }
765
+ .genesis__expected input { width: 96px; min-height: 32px; }
766
+ .genesis__later, .genesis__note { margin: 6px 0 0; font-size: 12.5px; color: var(--ink-faint); }
767
+ .genesis__check { grid-column: 1 / -1; display: flex; gap: 10px; align-items: center; min-height: 24px; font-size: 13px; }
768
+ .genesis__doors { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 16px; }
769
+ .genesis__doors .genesis__note { flex-basis: 100%; margin: 0; }
770
+ .genesis__life { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; margin-bottom: 18px; }
771
+ .genesis__life-item { display: grid; gap: 6px; text-align: center; }
772
+ .genesis__life-item .gplate { aspect-ratio: 1; border-radius: 12px; background: var(--bg-frame, var(--surface-2)); }
773
+ .genesis__life-item.is-later .gplate { opacity: 0.35; }
774
+ .genesis__life-item.is-now .gplate { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 60%, transparent); }
775
+ .genesis__life-cap { display: grid; font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-faint); }
776
+ .genesis__life-cap b { font-weight: 500; text-transform: none; letter-spacing: 0; font-size: 12.5px; color: var(--ink-soft); }
777
+ .gplate { display: grid; place-items: center; overflow: hidden; }
778
+ .gplate img { width: 78%; height: auto; }
779
+ .gplate__dark { display: none; }
780
+ html[data-theme="dark"] .gplate__dark { display: block; }
781
+ html[data-theme="dark"] .gplate__light { display: none; }
782
+ @media (prefers-color-scheme: dark) {
783
+ html:not([data-theme="light"]) .gplate__dark { display: block; }
784
+ html:not([data-theme="light"]) .gplate__light { display: none; }
785
+ }
786
+ .gplate--sm { width: 56px; height: 56px; }
787
+ .genesis__grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); gap: 24px; }
788
+ .genesis__stage-st, .genesis__task-st { flex: none; font-size: 11.5px; letter-spacing: 0.04em; color: var(--ink-faint); }
789
+ .genesis__stage.is-now .genesis__stage-st { color: var(--accent-ink); }
790
+ .genesis__stage.is-later { opacity: 0.6; }
791
+ .genesis__now-h { margin: 0 0 4px; font-size: 15px; font-weight: 500; }
792
+ .genesis__task-t { flex: 1; min-width: 0; padding: 3px 0; }
793
+ .genesis__task.is-finished .genesis__task-t { color: var(--ink-faint); text-decoration: line-through; }
794
+ .genesis__alive { display: grid; justify-items: center; text-align: center; gap: 10px; padding: 12px 0; }
795
+ .genesis__alive-plates { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }
796
+ .genesis__alive-h { margin: 6px 0 0; font-size: 28px; font-weight: 400; }
797
+ @media (max-width: 760px) {
798
+ .genesis__grid { grid-template-columns: minmax(0, 1fr); }
799
+ .genesis__life { grid-template-columns: repeat(3, minmax(0, 1fr)); }
800
+ }
736
801
  .nav-item--asleep { opacity: 0.55; }
737
802
  .nav-item__wakes { display: none; margin-left: auto; font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-faint); }
738
803
  .app-sidebar:hover .nav-item__wakes,
@@ -0,0 +1,18 @@
1
+ # The genesis home — a Home banner card in founding mode: decide, five stages, alive (task 1004423)
2
+
3
+ **BV2.PS08 (goal 1000121; spec `docs/specs/<redacted>.md` D4, D7, D10).** The canvas frames are `docs/design/mocks/project-startup/GenesisDecide`, `GenesisHall` and `Alive.dc.html`. The server is `modules/onboarding/genesis-home.js`; the stage tasks are filed by the kickoff seeder (`modules/lifecycle/kickoff-seed.js` `seedGenesisStages`, on the `lifecycle.kickoffSeed` port).
4
+
5
+ Files: `genesis-home.js` (the card, `id: 'genesis-home'`), its CSS block beside the founding band in `style.css`, and `genesis/` (the five life plates, byte-identical copies of the world pair, because the hall host does not serve the landing's `assets/world/`; `tests/genesis_home.mjs` holds them equal).
6
+
7
+ ## Blended, not a second hall
8
+
9
+ - **One card in the Home banner**, at order -1 so it leads the page while founding (the direction doc makes the genesis home the founding-mode landing). The banner carries no zone budget. The card renders nothing and makes no request unless `html[data-founding="on"]`, so a hall that is not founding is today's Home byte for byte.
10
+ - **It waits for the shell, it does not ask the server.** Widgets mount before `/me` is applied, so the card watches the `data-founding` attribute (a MutationObserver) and starts the first time the shell stamps it.
11
+ - **The canvas's guide panel is out of scope**: each stage shows its plain done-when line instead. The canvas's founding builders panel is the founding wall (task 1004424), already on Home.
12
+ - **The hall's own parts**: `.scroll`, hairline rows in the ledger rhythm, `.btn-accent` / `.btn-ghost`, a stepper in the rank-badge pill grammar. The plates use the world pair: the black-ground JPEG under the dark theme, the cutout under light.
13
+
14
+ ## What it does
15
+
16
+ - **Decide**: each stage's length (pre-picked), its done-when line, the starting government by its real name with its plain explanation (the forms and the recommendation are read from `GET /government/constitution`, never retyped), passes, advisors, and a "this project already exists" tick for the brownfield path. **Use this plan** records the plan, files the five stage tasks, and moves to scope. A solo founder needs that one click.
17
+ - **Each stage**: the life strip, the stage list with what each stage wakes and its due date, and the current stage's tasks. **Close this stage** puts it to the Board Room. When the founder's single yes decides it (Monarchy / BDFL) and nobody else is asked, the card casts that yes in the same click (the D4 fast-track). Otherwise it says who the sitting is waiting on.
18
+ - **Alive**: after legislation passes, **Bring the project to life** switches founding mode off and shows the alive moment; **Enter the builders' hall** reloads into the full hall.
@@ -131,4 +131,57 @@ function kickoffChecklist(spec = {}, { provisioned = false, firstSignIn = false
131
131
  return tasks.map((t) => ({ ...t }));
132
132
  }
133
133
 
134
- module.exports = { kickoffChecklist, productionTopologyKickoff };
134
+ // ── genesis (task 1004423 / BV2.PS08) ───────────────────────────────────────
135
+ // The five genesis stages are worked as REAL tasks on the new project's board, filed
136
+ // by the same seeder as the kickoff checklist (kickoff-seed.js seedGenesisStages) the
137
+ // moment the founder settles the plan. Each stage closes only when its tasks are
138
+ // finished AND the constitution in force decides it (spec D7), so these are the tasks
139
+ // the Board Room's genesis-stage sitting checks. Ideator decisions, because a stage is
140
+ // a decision written down; an AI session can draft each one the way it drafts any task.
141
+ //
142
+ // A project that already exists (the brownfield path) is asked for what it has first,
143
+ // so its scope stage opens with gathering the existing documents.
144
+ const GENESIS_STEPS = Object.freeze({
145
+ scope: [
146
+ { title: 'Write the project scope: what we are making, for whom, and what is out of scope',
147
+ description: 'Genesis stage 1 (scope). One page the whole team can read: the thing being made, who it is for, what done looks like for the first version, and what is deliberately left out. When it is written, Goals and the Roadmap wake.' },
148
+ ],
149
+ strategize: [
150
+ { title: 'Write out the strategy, or link the one you already have',
151
+ description: 'Genesis stage 2 (strategize). How the project gets from here to its first version: the order of the big pieces, the risks, and what gets tried first. A strategy that already exists can be linked instead. When it is done, Tasks wake.' },
152
+ ],
153
+ charter: [
154
+ { title: 'Choose the starting government and write the bylaws',
155
+ description: 'Genesis stage 3 (charter). Pick the form of government the project starts under (the genesis plan recommends one from the team size) and write the short bylaws and code of conduct it runs by. When it is done, the Board Room wakes.' },
156
+ ],
157
+ legalize: [
158
+ { title: 'Decide how contributors are rewarded and who can be paid',
159
+ description: 'Genesis stage 4 (legalize). Whether builders earn credits, money or nothing, and the limits that apply (age, country, the entity behind the project). When it is done, the Economy wakes.' },
160
+ ],
161
+ legislation: [
162
+ { title: 'Set the voting rules: who votes and how many must agree',
163
+ description: 'Genesis stage 5 (legislation). Who has a say, and the rule a decision passes by. When it is done, Ranks wake, and the project can come alive.' },
164
+ ],
165
+ });
166
+ const GENESIS_EXISTING_FIRST = {
167
+ title: 'Gather what already exists: link the documents, code and notes the project has',
168
+ description: 'Genesis stage 1 (scope), for a project that already exists. Before writing anything new, link what is already there, so the scope is written from it rather than beside it.',
169
+ };
170
+
171
+ // genesisChecklist({ existing }) → { stageKey: [{ title, description, kind, discipline, ref }] }
172
+ // in genesis order. `ref` is the stable per-task key the seeder dedups on, so filing
173
+ // twice files nothing new. Pure — no I/O.
174
+ function genesisChecklist({ existing = false } = {}) {
175
+ const out = {};
176
+ for (const [stage, steps] of Object.entries(GENESIS_STEPS)) {
177
+ const list = stage === 'scope' && existing ? [GENESIS_EXISTING_FIRST, ...steps] : steps;
178
+ // The ref names the step, never its position, so the brownfield step slotting in
179
+ // ahead of scope's own cannot re-key it into a duplicate.
180
+ out[stage] = list.map((t) => ({
181
+ ...t, kind: 'decision', discipline: 'ideator', ref: t === GENESIS_EXISTING_FIRST ? 'genesis:scope:existing' : `genesis:${stage}`,
182
+ }));
183
+ }
184
+ return out;
185
+ }
186
+
187
+ module.exports = { kickoffChecklist, productionTopologyKickoff, genesisChecklist };
@@ -20,7 +20,7 @@
20
20
 
21
21
  const api = require('../../src/module-api');
22
22
  const db = require('./db');
23
- const { kickoffChecklist } = require('./kickoff-checklist');
23
+ const { kickoffChecklist, genesisChecklist } = require('./kickoff-checklist');
24
24
 
25
25
  // The spec the checklist reads, from the instance's own branding pack. `modules` is
26
26
  // empty on purpose: a hub-provisioned project is scaffolded with every optional module
@@ -71,4 +71,63 @@ async function seedKickoffIfEmpty({ builderId = null } = {}, deps = {}) {
71
71
  return { seeded, versionId };
72
72
  }
73
73
 
74
- module.exports = { seedKickoffIfEmpty, specFromBranding, FIRST_VERSION_SQL };
74
+ // ── genesis (task 1004423 / BV2.PS08) ───────────────────────────────────────
75
+ // The same seeder files the five genesis stages as real tasks when the founder settles
76
+ // the genesis plan — one seeder, not a second one (spec reuse table). The kickoff seed
77
+ // is idempotent by emptiness, which cannot work here: by genesis the board already
78
+ // holds the kickoff checklist. So each genesis task carries source 'genesis' and a
79
+ // stable source_ref per step (genesisChecklist's `ref`), and a step already filed is
80
+ // skipped. Filing twice files nothing new.
81
+ const GENESIS_SOURCE = 'genesis';
82
+
83
+ // seedGenesisStages({ builderId, existing }, deps) → { seeded, versionId } | { seeded: 0, skipped }
84
+ async function seedGenesisStages({ builderId = null, existing = false } = {}, deps = {}) {
85
+ const pool = deps.pool || api.pool;
86
+ const insert = deps.createTask || ((row) => db.createTask(row));
87
+ const find = deps.findTaskBySourceRef || ((q) => db.findTaskBySourceRef(q));
88
+ const { rows: versions } = await pool.query(FIRST_VERSION_SQL);
89
+ if (!versions.length) return { seeded: 0, skipped: 'no_version' };
90
+ const versionId = versions[0].id;
91
+ let seeded = 0;
92
+ for (const steps of Object.values(genesisChecklist({ existing }))) {
93
+ for (const t of steps) {
94
+ if (await find({ source: GENESIS_SOURCE, sourceRef: t.ref })) continue;
95
+ await insert({
96
+ versionId,
97
+ title: t.title,
98
+ description: t.description,
99
+ status: 'ready',
100
+ kind: t.kind,
101
+ discipline: t.discipline,
102
+ source: GENESIS_SOURCE,
103
+ sourceRef: t.ref,
104
+ createdBy: builderId,
105
+ // As the kickoff checklist: a project in genesis has no goals yet (Goals wake
106
+ // only when scope is done), so the catch-all is the honest home.
107
+ allowCatchAll: true,
108
+ });
109
+ seeded += 1;
110
+ }
111
+ }
112
+ return { seeded, versionId };
113
+ }
114
+
115
+ // genesisStageTasks(deps) → { stageKey: [{ id, title, status }] } for every genesis
116
+ // task on the board, grouped by the stage its source_ref names. The genesis home lists
117
+ // them, and a stage close hands their ids to the Board Room's finished-tasks gate.
118
+ async function genesisStageTasks(deps = {}) {
119
+ const pool = deps.pool || api.pool;
120
+ const { rows } = await pool.query(
121
+ `SELECT id, title, status, source_ref FROM tasks WHERE source = $1 ORDER BY id ASC`,
122
+ [GENESIS_SOURCE],
123
+ );
124
+ const out = {};
125
+ for (const r of rows) {
126
+ const stage = String(r.source_ref || '').split(':')[1];
127
+ if (!stage) continue;
128
+ (out[stage] = out[stage] || []).push({ id: String(r.id), title: r.title, status: r.status });
129
+ }
130
+ return out;
131
+ }
132
+
133
+ module.exports = { seedKickoffIfEmpty, specFromBranding, FIRST_VERSION_SQL, seedGenesisStages, genesisStageTasks, GENESIS_SOURCE };