@bongos/core 1.25.18 → 1.25.20
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 +113 -83
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -1
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0408-a-genesis-stage-is-three-doors-onto-one-record.md +1 -1
- package/docs/api/openapi.json +81 -2
- package/docs/api-reference.md +7 -5
- package/docs/copy-inventory.md +219 -216
- package/docs/copy-registry.json +273 -246
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +348 -347
- package/docs/project-context.template.md +1 -1
- package/modules/front-door-guide/handoff.js +120 -0
- package/modules/front-door-guide/module.json +1 -1
- package/modules/front-door-guide/routes/front-door-guide-plan.js +24 -0
- package/modules/hall-ui/public/ai-studio.html +1 -1
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/genesis-home.js +51 -7
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/home-city-draw.js +14 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/market-watch-item.html +1 -1
- package/modules/hall-ui/public/market-watch-rules.html +1 -1
- package/modules/hall-ui/public/market-watch.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/money.html +1 -1
- package/modules/hall-ui/public/my-agents.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +32 -20
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +9 -4
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/lifecycle/home-city.js +17 -3
- package/modules/onboarding/founding-mode.js +45 -1
- package/modules/onboarding/genesis-home.js +70 -7
- package/modules/onboarding/genesis-work.js +68 -0
- package/modules/onboarding/guide-handoff.js +109 -0
- package/modules/onboarding/routes/genesis-work.js +11 -4
- package/modules/provisioning/guide-handoff.js +65 -0
- package/modules/provisioning/provisioning.js +2 -2
- package/modules/provisioning/routes/provisioning.js +4 -2
- package/modules/public-landing/public/projects.html +79 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/bongos/genesis.js +8 -1
- package/src/bongos/founding-page-gate.js +67 -0
- package/src/bongos/serve-internal.js +3 -0
- package/src/branding.js +4 -0
- package/src/module-api.js +1 -1
- package/tests/founding_mode.mjs +84 -8
- package/tests/founding_page_gate.mjs +105 -0
- package/tests/front_door_guide_existing.mjs +1 -0
- package/tests/genesis_home.mjs +105 -1
- package/tests/genesis_work.mjs +29 -0
- package/tests/genesis_work_routes.mjs +9 -0
- package/tests/guide_handoff.mjs +322 -0
- package/tests/home_city.mjs +52 -0
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/provision_settings_apply.mjs +2 -1
- package/tests/provisioning_settings_env.mjs +2 -2
|
@@ -420,7 +420,7 @@
|
|
|
420
420
|
<script src="/builders/shell-game.js?v=2026-11-11-view2"></script>
|
|
421
421
|
<script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
|
|
422
422
|
<script src="/builders/craft-mode.js?v=2026-11-10-visor"></script>
|
|
423
|
-
<script src="/builders/shell.js?v=2026-11-
|
|
423
|
+
<script src="/builders/shell.js?v=2026-11-13-founding"></script>
|
|
424
424
|
<script src="/builders/hall-dialog.js"></script>
|
|
425
425
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
426
426
|
<script src="/builders/palette.js" defer></script>
|
|
@@ -696,29 +696,39 @@
|
|
|
696
696
|
// ?denied=<permission>[&reason=unavailable]. Render the refusal — "you are
|
|
697
697
|
// signed in; you lack X" — then strip the query so a refresh or a shared link
|
|
698
698
|
// doesn't re-show a stale notice. Cosmetic only; the gate already ran.
|
|
699
|
+
function navItemById(id) {
|
|
700
|
+
var hit = null;
|
|
701
|
+
NAV.forEach(function (g) { g.items.forEach(function (it) { if (it.id === id) hit = it; }); });
|
|
702
|
+
return hit;
|
|
703
|
+
}
|
|
699
704
|
function renderDeniedNotice() {
|
|
700
705
|
// Hand-parsed (no URLSearchParams): shell.js is dependency-free by design,
|
|
701
706
|
// and the hall unit tests boot it in a bare vm context.
|
|
702
707
|
var search = (window.location && window.location.search) || '';
|
|
703
708
|
var m = /[?&]denied=([^&]+)/.exec(search);
|
|
704
|
-
|
|
705
|
-
var
|
|
706
|
-
if (
|
|
709
|
+
// task 1005112: a founding project's sleeping room sends its page here, naming it.
|
|
710
|
+
var sl = /[?&]asleep=([a-z-]{1,40})(?:&opens=([a-z]{1,20}))?(?:&|$)/.exec(search);
|
|
711
|
+
if (!m && !sl) return;
|
|
712
|
+
var denied = m ? decodeURIComponent(m[1]) : '';
|
|
713
|
+
if (m && !/^[a-z0-9_.-]{1,80}$/i.test(denied)) return;
|
|
707
714
|
var unavailable = /[?&]reason=unavailable(?:&|$)/.test(search);
|
|
715
|
+
var room = sl && navItemById(sl[1]);
|
|
708
716
|
var topbar = document.getElementById('app-topbar');
|
|
709
717
|
if (!topbar) return;
|
|
710
718
|
var el = document.createElement('div');
|
|
711
719
|
el.className = 'denied-notice';
|
|
712
720
|
el.setAttribute('role', 'status');
|
|
713
721
|
el.innerHTML =
|
|
714
|
-
'<span>' + (
|
|
722
|
+
'<span>' + (sl
|
|
723
|
+
? esc(room ? room.label : 'That room') + ' isn’t open yet. It ' + esc(WAKE_TEXT[sl[2]] || 'opens later') + '. Finish that stage of founding here on Home.'
|
|
724
|
+
: unavailable
|
|
715
725
|
? 'You’re signed in, but access to that page couldn’t be verified just now (the permission system didn’t answer), so it stayed closed. Try again in a minute.'
|
|
716
726
|
: 'You’re signed in, but that page needs the <code>' + esc(denied) + '</code> permission — your current role doesn’t hold it.') +
|
|
717
727
|
'</span>' +
|
|
718
728
|
'<button type="button" class="denied-notice__close" aria-label="Dismiss">×</button>';
|
|
719
729
|
el.querySelector('.denied-notice__close').addEventListener('click', function () { el.remove(); });
|
|
720
730
|
topbar.insertAdjacentElement('afterend', el);
|
|
721
|
-
var rest = search.replace(/[?&](?:denied|reason)=[^&]*/g, '').replace(/^&/, '?');
|
|
731
|
+
var rest = search.replace(/[?&](?:denied|reason|asleep|opens)=[^&]*/g, '').replace(/^&/, '?');
|
|
722
732
|
history.replaceState(null, '', window.location.pathname + rest + window.location.hash);
|
|
723
733
|
}
|
|
724
734
|
|
|
@@ -916,8 +926,10 @@
|
|
|
916
926
|
// OFF is the default and the fallback, and OFF REMOVES rather than hides: the band
|
|
917
927
|
// element and every asleep class and tag are taken out of the DOM, so a hall that
|
|
918
928
|
// is not founding is byte-for-byte the hall it was (tests/founding_mode.mjs pins
|
|
919
|
-
// it).
|
|
920
|
-
//
|
|
929
|
+
// it). task 1005112: an asleep room is HIDDEN, not dimmed, and the server refuses its
|
|
930
|
+
// page (src/bongos/founding-page-gate.js, the same /me answer), so this only draws.
|
|
931
|
+
// A room that woke since this browser last looked arrives marked, once.
|
|
932
|
+
// The stage in words a person would say, never a raw stage slug (the refusal notice).
|
|
921
933
|
var WAKE_TEXT = {
|
|
922
934
|
scope: 'opens after scope', strategize: 'opens after strategy', charter: 'opens after the charter',
|
|
923
935
|
legalize: 'opens after legal setup', legislation: 'opens after legislation', alive: 'opens when alive'
|
|
@@ -930,11 +942,9 @@
|
|
|
930
942
|
// mode — returns before touching a single node. That is the strongest form of the
|
|
931
943
|
// no-hijack condition: nothing is read, created, classed or removed.
|
|
932
944
|
if (!on && root.dataset.founding !== 'on') return;
|
|
933
|
-
document.querySelectorAll('#app-sidebar .nav-item--asleep').forEach(function (el) {
|
|
945
|
+
document.querySelectorAll('#app-sidebar .nav-item--asleep, #app-sidebar .nav-item--arrived').forEach(function (el) {
|
|
934
946
|
el.classList.remove('nav-item--asleep');
|
|
935
|
-
|
|
936
|
-
var tag = el.querySelector('[data-wakes-tag]');
|
|
937
|
-
if (tag && tag.parentNode) tag.parentNode.removeChild(tag);
|
|
947
|
+
if (!on) el.classList.remove('nav-item--arrived'); // the mark lasts the page, not one pass
|
|
938
948
|
});
|
|
939
949
|
var band = document.getElementById('founding-band');
|
|
940
950
|
if (!on) {
|
|
@@ -943,17 +953,19 @@
|
|
|
943
953
|
return;
|
|
944
954
|
}
|
|
945
955
|
root.dataset.founding = 'on';
|
|
956
|
+
// One key per browser ORIGIN, which is one project: a project's hall is its own host.
|
|
957
|
+
var seen = null, KEY = 'hall-founding-asleep';
|
|
958
|
+
try { seen = JSON.parse(window.localStorage.getItem(KEY) || 'null'); } catch (_) { seen = null; }
|
|
959
|
+
if (seen && typeof seen === 'object') Object.keys(seen).forEach(function (id) {
|
|
960
|
+
var el = !(id in f.asleep) && document.getElementById('nav-' + id);
|
|
961
|
+
if (el) el.classList.add('nav-item--arrived');
|
|
962
|
+
});
|
|
963
|
+
try { window.localStorage.setItem(KEY, JSON.stringify(f.asleep)); } catch (_) { /* private mode */ }
|
|
946
964
|
Object.keys(f.asleep).forEach(function (id) {
|
|
965
|
+
// Hidden whole (style.css), so it carries no "opens after" label: the stage that
|
|
966
|
+
// opens it is said on Home instead, and on the refusal notice below.
|
|
947
967
|
var el = document.getElementById('nav-' + id);
|
|
948
|
-
if (
|
|
949
|
-
var stage = String(f.asleep[id]);
|
|
950
|
-
el.classList.add('nav-item--asleep');
|
|
951
|
-
el.dataset.wakes = stage;
|
|
952
|
-
var tag = document.createElement('span');
|
|
953
|
-
tag.className = 'nav-item__wakes';
|
|
954
|
-
tag.setAttribute('data-wakes-tag', '');
|
|
955
|
-
tag.textContent = WAKE_TEXT[stage] || 'opens later';
|
|
956
|
-
el.appendChild(tag);
|
|
968
|
+
if (el) el.classList.add('nav-item--asleep');
|
|
957
969
|
});
|
|
958
970
|
if (!band) {
|
|
959
971
|
var body = document.querySelector('.app-body');
|
|
@@ -228,7 +228,7 @@
|
|
|
228
228
|
<script src="/builders/shell-game.js?v=2026-11-11-view2"></script>
|
|
229
229
|
<script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
|
|
230
230
|
<script src="/builders/craft-mode.js?v=2026-11-10-visor"></script>
|
|
231
|
-
<script src="/builders/shell.js?v=2026-11-
|
|
231
|
+
<script src="/builders/shell.js?v=2026-11-13-founding"></script>
|
|
232
232
|
<script src="/builders/hall-dialog.js"></script>
|
|
233
233
|
<script src="/builders/palette.js" defer></script>
|
|
234
234
|
<script src="/builders/gov-living.js?v=2026-10-09-positions1"></script>
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
<script src="/builders/shell-game.js?v=2026-11-11-view2"></script>
|
|
226
226
|
<script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
|
|
227
227
|
<script src="/builders/craft-mode.js?v=2026-11-10-visor"></script>
|
|
228
|
-
<script src="/builders/shell.js?v=2026-11-
|
|
228
|
+
<script src="/builders/shell.js?v=2026-11-13-founding"></script>
|
|
229
229
|
<script src="/builders/hall-dialog.js"></script>
|
|
230
230
|
<script src="/builders/palette.js" defer></script>
|
|
231
231
|
<!-- The kit, for its ONE renderer: the full-size ship-time visual figure
|
|
@@ -1082,10 +1082,15 @@ html[data-theme="dark"] .gplate__light { display: none; }
|
|
|
1082
1082
|
.genesis__grid { grid-template-columns: minmax(0, 1fr); }
|
|
1083
1083
|
.genesis__life { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
1084
1084
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
.
|
|
1088
|
-
|
|
1085
|
+
/* task 1005112: a room a founding project has not opened is not in the sidebar at all
|
|
1086
|
+
(the server refuses its page too, src/bongos/founding-page-gate.js), and a group whose
|
|
1087
|
+
every room sleeps hides its head with it. A room that woke since this browser last
|
|
1088
|
+
looked arrives with a mark, once: the stage's reward, not a silent re-render. */
|
|
1089
|
+
.nav-item--asleep { display: none; }
|
|
1090
|
+
html[data-founding="on"] .nav-group:not(:has(.nav-item:not(.nav-item--asleep):not([hidden]))) { display: none; }
|
|
1091
|
+
.nav-item--arrived { position: relative; color: var(--ink); background: color-mix(in srgb, var(--accent) 14%, transparent); animation: nav-arrive 1.6s var(--ds-ease, ease) 2; }
|
|
1092
|
+
.nav-item--arrived::after { content: ""; position: absolute; top: 8px; right: 8px; width: 6px; height: 6px; border-radius: var(--ds-radius-pill); background: var(--accent); }
|
|
1093
|
+
@keyframes nav-arrive { 50% { background: color-mix(in srgb, var(--accent) 32%, transparent); } }
|
|
1089
1094
|
|
|
1090
1095
|
/* task 1003737: the nav count badge — the hall's only badge mechanism, built
|
|
1091
1096
|
once and generically (shell.js `count:` names a need id; see the NAV model).
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
<script src="/builders/shell-game.js?v=2026-11-11-view2"></script>
|
|
61
61
|
<script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
|
|
62
62
|
<script src="/builders/craft-mode.js?v=2026-11-10-visor"></script>
|
|
63
|
-
<script src="/builders/shell.js?v=2026-11-
|
|
63
|
+
<script src="/builders/shell.js?v=2026-11-13-founding"></script>
|
|
64
64
|
<script src="/builders/hall-dialog.js"></script>
|
|
65
65
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
66
66
|
<script src="/builders/palette.js" defer></script>
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
<script src="/builders/shell-game.js?v=2026-11-11-view2"></script>
|
|
144
144
|
<script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
|
|
145
145
|
<script src="/builders/craft-mode.js?v=2026-11-10-visor"></script>
|
|
146
|
-
<script src="/builders/shell.js?v=2026-11-
|
|
146
|
+
<script src="/builders/shell.js?v=2026-11-13-founding"></script>
|
|
147
147
|
<script src="/builders/hall-dialog.js"></script>
|
|
148
148
|
<!-- task 1005148: the visor (glass over the scene, game mode only; ADR 0399), after the shell and hall-dialog.js -->
|
|
149
149
|
<script src="/builders/visor.js?v=2026-11-11-helm3"></script>
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
<script src="/builders/shell-game.js?v=2026-11-11-view2"></script>
|
|
147
147
|
<script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
|
|
148
148
|
<script src="/builders/craft-mode.js?v=2026-11-10-visor"></script>
|
|
149
|
-
<script src="/builders/shell.js?v=2026-11-
|
|
149
|
+
<script src="/builders/shell.js?v=2026-11-13-founding"></script>
|
|
150
150
|
<script src="/builders/hall-dialog.js"></script>
|
|
151
151
|
<script src="/builders/palette.js" defer></script>
|
|
152
152
|
<script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
|
|
@@ -272,7 +272,7 @@
|
|
|
272
272
|
<script src="/builders/shell-game.js?v=2026-11-11-view2"></script>
|
|
273
273
|
<script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
|
|
274
274
|
<script src="/builders/craft-mode.js?v=2026-11-10-visor"></script>
|
|
275
|
-
<script src="/builders/shell.js?v=2026-11-
|
|
275
|
+
<script src="/builders/shell.js?v=2026-11-13-founding"></script>
|
|
276
276
|
<script src="/builders/hall-dialog.js"></script>
|
|
277
277
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
278
278
|
<script src="/builders/palette.js" defer></script>
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
<script src="/builders/shell-game.js?v=2026-11-11-view2"></script>
|
|
176
176
|
<script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
|
|
177
177
|
<script src="/builders/craft-mode.js?v=2026-11-10-visor"></script>
|
|
178
|
-
<script src="/builders/shell.js?v=2026-11-
|
|
178
|
+
<script src="/builders/shell.js?v=2026-11-13-founding"></script>
|
|
179
179
|
<script src="/builders/hall-dialog.js"></script>
|
|
180
180
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
181
181
|
<script src="/builders/palette.js" defer></script>
|
|
@@ -370,14 +370,28 @@ function shapeCity(raw, viewer) {
|
|
|
370
370
|
};
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
// readFoundingDistrict() — while the project is founding, which government-district
|
|
374
|
+
// buildings are not raised yet (task 1005112): the onboarding port's own answer, the one
|
|
375
|
+
// /me carries. null when not founding, the module is off, or the read fails, which
|
|
376
|
+
// draws the district whole, exactly as before.
|
|
377
|
+
async function readFoundingDistrict(resolve = api.resolveOptional) {
|
|
378
|
+
try {
|
|
379
|
+
const onboarding = resolve('onboarding');
|
|
380
|
+
const f = onboarding && typeof onboarding.foundingModeFor === 'function' ? await onboarding.foundingModeFor() : null;
|
|
381
|
+
return f && f.active === true && f.unbuilt && typeof f.unbuilt === 'object' ? { stage: f.stage, unbuilt: f.unbuilt } : null;
|
|
382
|
+
} catch (_) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
373
387
|
// The GET /home/city handler, injectable so a test drives it with no express.
|
|
374
|
-
function homeCityHandler({ log, read = readCity } = {}) {
|
|
388
|
+
function homeCityHandler({ log, read = readCity, founding = readFoundingDistrict } = {}) {
|
|
375
389
|
return async function homeCityRoute(req, res) {
|
|
376
390
|
try {
|
|
377
391
|
const viewer = req.builder ? { id: req.builder.id, rank: req.builder.rank } : null;
|
|
378
|
-
const raw = await read({ weeks: req.query && req.query.weeks, viewer });
|
|
392
|
+
const [raw, district] = await Promise.all([read({ weeks: req.query && req.query.weeks, viewer }), founding()]);
|
|
379
393
|
res.set('Cache-Control', 'private, max-age=60');
|
|
380
|
-
res.json(shapeCity(raw, viewer));
|
|
394
|
+
res.json({ ...shapeCity(raw, viewer), founding: district });
|
|
381
395
|
} catch (err) {
|
|
382
396
|
if (log) log.error('GET /home/city', err);
|
|
383
397
|
res.fail('home_city_failed', { status: 500, message: 'internal error' });
|
|
@@ -50,6 +50,48 @@ const WAKES = Object.freeze({
|
|
|
50
50
|
});
|
|
51
51
|
const WAKES_WHEN_ALIVE = Object.freeze(['collab', 'roster']);
|
|
52
52
|
|
|
53
|
+
// THE GOVERNMENT DISTRICT RISES WITH GENESIS (task 1005112; V3 spec, task-cleanup
|
|
54
|
+
// answer 1: "Genesis is the founding of the government district"). Home's city draws
|
|
55
|
+
// the district as nine civic buildings (city-draw.js CIVIC_SLOTS, keyed as below). While
|
|
56
|
+
// a project is founding, a building stands only once the stage that raises it is behind;
|
|
57
|
+
// until then its plot is a marked-out foundation. Same rule and same shape as WAKES, so
|
|
58
|
+
// the server decides and every presentation (the city, the genesis card) reads it.
|
|
59
|
+
// Every stage raises something, so no stage ends the sense of progress; automation, the
|
|
60
|
+
// project running itself, rises when the project comes alive.
|
|
61
|
+
const BUILDS = Object.freeze({
|
|
62
|
+
scope: Object.freeze(['planning']),
|
|
63
|
+
strategize: Object.freeze(['release']),
|
|
64
|
+
charter: Object.freeze(['charter', 'people']),
|
|
65
|
+
legalize: Object.freeze(['treasury']),
|
|
66
|
+
legislation: Object.freeze(['rules', 'review', 'disputes']),
|
|
67
|
+
});
|
|
68
|
+
const BUILDS_WHEN_ALIVE = Object.freeze(['automation']);
|
|
69
|
+
|
|
70
|
+
// unbuiltBuildings(stage) — PURE. { buildingKey: raisingStage } for every district
|
|
71
|
+
// building not yet raised at this stage (the asleepRooms rule), or null for no stage.
|
|
72
|
+
function unbuiltBuildings(stage) {
|
|
73
|
+
const at = STAGES.indexOf(stage);
|
|
74
|
+
if (at < 0) return null;
|
|
75
|
+
const out = {};
|
|
76
|
+
for (const [raising, keys] of Object.entries(BUILDS)) {
|
|
77
|
+
if (STAGES.indexOf(raising) >= at) for (const k of keys) out[k] = raising;
|
|
78
|
+
}
|
|
79
|
+
for (const k of BUILDS_WHEN_ALIVE) out[k] = 'alive';
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// What finishing a stage that opens no room gives the founder, in a sentence (task
|
|
84
|
+
// 1005112). Three stages wake no sidebar room since tasks 1005046 and 1005199, and a
|
|
85
|
+
// founder must never be told nothing happens: this is said on the stage's row instead.
|
|
86
|
+
// Each line is true today: the charter puts the government in force, legalize settles
|
|
87
|
+
// how contributors are rewarded, and legislation's pass is what brings the project to
|
|
88
|
+
// life, which opens every room. A stage that wakes rooms says those instead.
|
|
89
|
+
const EARNS = Object.freeze({
|
|
90
|
+
charter: 'Puts your government in force',
|
|
91
|
+
legalize: 'Settles how contributors are rewarded',
|
|
92
|
+
legislation: 'Brings the project to life: every room opens',
|
|
93
|
+
});
|
|
94
|
+
|
|
53
95
|
// The project_settings keys. Namespaced 'founding.' like 'mingle.' and 'attention.'.
|
|
54
96
|
const KEYS = Object.freeze({
|
|
55
97
|
ACTIVE: 'founding.active',
|
|
@@ -118,6 +160,8 @@ function shapeFoundingMode(rows, now = new Date()) {
|
|
|
118
160
|
stage_count: STAGES.length - 1,
|
|
119
161
|
day: dayOf(rows.startedAt, now),
|
|
120
162
|
asleep,
|
|
163
|
+
// The government district's buildings not yet raised (task 1005112), keyed like CIVIC_SLOTS.
|
|
164
|
+
unbuilt: unbuiltBuildings(stage),
|
|
121
165
|
// Who closing the current stage is waiting on (spec D7) — display names, at most
|
|
122
166
|
// five, from the government module's genesis-stage sitting (task 1004429). null
|
|
123
167
|
// when the stage is not sitting, nobody is left to vote, or the read is missing.
|
|
@@ -226,5 +270,5 @@ async function setFoundingMode({ active, stage } = {}, builderId = null, deps =
|
|
|
226
270
|
module.exports = {
|
|
227
271
|
STAGES, WAKES, WAKES_WHEN_ALIVE, KEYS, stageAtKey,
|
|
228
272
|
dayOf, shapeFoundingMode, waitingOnFromGovernment,
|
|
229
|
-
foundingModeFor, setFoundingMode,
|
|
273
|
+
foundingModeFor, setFoundingMode, EARNS, BUILDS, BUILDS_WHEN_ALIVE,
|
|
230
274
|
};
|
|
@@ -27,6 +27,15 @@
|
|
|
27
27
|
// as `board.genesis_stage.passed` (the route factory listens), and every read
|
|
28
28
|
// reconciles again, so a missed nudge cannot strand a stage.
|
|
29
29
|
//
|
|
30
|
+
// WITH NO BOARD ROOM (task 1005115; owner, 2026-11-07: "I would automatically proceed").
|
|
31
|
+
// A project without the government module has nobody to put a stage to, so a stage
|
|
32
|
+
// whose tasks are all finished moves on BY ITSELF — reconcile does it, on every read and
|
|
33
|
+
// on every ship — and the view says so (`moved`: from, to, what woke). There is no
|
|
34
|
+
// close button to press on that path. Where there IS a Board Room the sitting stays:
|
|
35
|
+
// a team deciding together is not the same act as a solo founder acknowledging their
|
|
36
|
+
// own work. Either way, each stage leaves a DOCUMENT behind (genesis-work.js
|
|
37
|
+
// documentOf) that the founder can edit at any time without reopening the stage.
|
|
38
|
+
//
|
|
30
39
|
// THE BOUNDARY. Core through ../../src/module-api.js; the lifecycle and government
|
|
31
40
|
// modules through their ports only (resolveOptional), never a require.
|
|
32
41
|
|
|
@@ -158,7 +167,11 @@ function taskState(t) {
|
|
|
158
167
|
// task, named in `waits_on`) or 'open' — decided here, never re-derived in a browser.
|
|
159
168
|
// board { stage: genesisStageStatus row } or {}
|
|
160
169
|
// stageAt { stage: ISO } — when each stage began, for its due date
|
|
161
|
-
|
|
170
|
+
// boardRoom false on a project with no government module: stages advance by
|
|
171
|
+
// themselves, so nothing is ever "closable" and the alive door opens on
|
|
172
|
+
// finished legislation tasks (task 1005115)
|
|
173
|
+
// documents { stage: { title, filled, updated_at, editable } } from genesis-work.js
|
|
174
|
+
function shapeGenesis({ mode, plan, tasks = {}, board = {}, stageAt = {}, boardRoom = true, documents = {} }) {
|
|
162
175
|
const at = fm.STAGES.indexOf(mode.stage);
|
|
163
176
|
const stages = PROPER.map((key) => {
|
|
164
177
|
const info = STAGE_INFO[key];
|
|
@@ -181,6 +194,8 @@ function shapeGenesis({ mode, plan, tasks = {}, board = {}, stageAt = {} }) {
|
|
|
181
194
|
plate: info.plate,
|
|
182
195
|
done_when: info.done_when,
|
|
183
196
|
wakes: fm.WAKES[key] || [],
|
|
197
|
+
earns: (fm.WAKES[key] || []).length ? null : (fm.EARNS[key] || null), // task 1005112
|
|
198
|
+
raises: fm.BUILDS[key] || [], // task 1005112: the district buildings this stage raises
|
|
184
199
|
days,
|
|
185
200
|
minutes,
|
|
186
201
|
due,
|
|
@@ -190,7 +205,10 @@ function shapeGenesis({ mode, plan, tasks = {}, board = {}, stageAt = {} }) {
|
|
|
190
205
|
total: list.length,
|
|
191
206
|
// task 1005199: this stage (the current one or any later one) can be put to
|
|
192
207
|
// the Board Room now — its tasks are all finished and it is not already there.
|
|
193
|
-
can_close: at >= 1 && idx >= at && allFinished && !onBoard,
|
|
208
|
+
can_close: boardRoom && at >= 1 && idx >= at && allFinished && !onBoard,
|
|
209
|
+
// What this stage left behind (task 1005115): null where the store is not read.
|
|
210
|
+
document: documents[key] || null,
|
|
211
|
+
all_finished: allFinished,
|
|
194
212
|
board: b ? {
|
|
195
213
|
state: b.state, item_id: b.item_id || null, needs: b.needs || null,
|
|
196
214
|
one_vote_decides: !!b.one_vote_decides, waiting_on: b.waiting_on || [],
|
|
@@ -200,6 +218,14 @@ function shapeGenesis({ mode, plan, tasks = {}, board = {}, stageAt = {} }) {
|
|
|
200
218
|
const now = stages.find((s) => s.state === 'now') || null;
|
|
201
219
|
const sitting = now && now.board && now.board.state === 'sitting';
|
|
202
220
|
const passed = now && now.board && now.board.state === 'passed';
|
|
221
|
+
// The last move, so a presentation can say plainly that it happened and what opened
|
|
222
|
+
// (task 1005115). Derived from the stage stamps, never stored twice.
|
|
223
|
+
const prev = at >= 2 ? fm.STAGES[at - 1] : null;
|
|
224
|
+
const moved = prev && stageAt[mode.stage] ? {
|
|
225
|
+
from: prev, to: mode.stage, at: stageAt[mode.stage],
|
|
226
|
+
how: boardRoom ? 'board_passed' : 'tasks_finished',
|
|
227
|
+
opened: fm.WAKES[prev] || [],
|
|
228
|
+
} : null;
|
|
203
229
|
return {
|
|
204
230
|
stage: mode.stage,
|
|
205
231
|
stage_number: mode.stage_number,
|
|
@@ -211,9 +237,12 @@ function shapeGenesis({ mode, plan, tasks = {}, board = {}, stageAt = {} }) {
|
|
|
211
237
|
demo: plan && plan.demo ? plan.demo : null,
|
|
212
238
|
stages,
|
|
213
239
|
// The close door: every task of the current stage finished, and not already sitting.
|
|
214
|
-
can_close: !!(now && now.total > 0 && now.finished === now.total && !sitting && !passed),
|
|
215
|
-
// Alive: the last stage has passed its sitting
|
|
216
|
-
|
|
240
|
+
can_close: !!(boardRoom && now && now.total > 0 && now.finished === now.total && !sitting && !passed),
|
|
241
|
+
// Alive: the last stage has passed its sitting — or, with no Board Room, its tasks
|
|
242
|
+
// are all finished.
|
|
243
|
+
can_come_alive: !!(now && now.key === 'legislation' && (boardRoom ? passed : now.total > 0 && now.finished === now.total)),
|
|
244
|
+
board_room: boardRoom,
|
|
245
|
+
moved,
|
|
217
246
|
};
|
|
218
247
|
}
|
|
219
248
|
|
|
@@ -226,9 +255,18 @@ function seams(deps = {}) {
|
|
|
226
255
|
set: deps.setSetting || api.projectSettings.set,
|
|
227
256
|
lifecycle: deps.lifecycle !== undefined ? deps.lifecycle : api.resolveOptional('lifecycle.kickoffSeed'),
|
|
228
257
|
government: deps.government !== undefined ? deps.government : api.resolveOptional('government'),
|
|
258
|
+
// Each stage's document summary (task 1005115): the founder's store unless injected.
|
|
259
|
+
// Fails soft in readGenesis, so a hall without the tables still renders.
|
|
260
|
+
documents: deps.documents !== undefined ? deps.documents : () => require('./genesis-work').readDocuments(),
|
|
229
261
|
now: deps.now,
|
|
230
262
|
};
|
|
231
263
|
}
|
|
264
|
+
|
|
265
|
+
// hasBoardRoom(gov) — can a stage be put to a sitting at all? Without one a finished
|
|
266
|
+
// stage advances by itself (task 1005115).
|
|
267
|
+
const hasBoardRoom = (gov) => !!(gov && (typeof gov.closeGenesisStage === 'function'
|
|
268
|
+
|| typeof gov.openGenesisStageClose === 'function'));
|
|
269
|
+
const allDone = (list) => !!(list && list.length && list.every((t) => FINISHED.includes(t.status)));
|
|
232
270
|
const modeDeps = (s) => ({ getSetting: s.get, setSetting: s.set, government: s.government, now: s.now });
|
|
233
271
|
|
|
234
272
|
async function boardByStage(gov) {
|
|
@@ -245,6 +283,15 @@ async function boardByStage(gov) {
|
|
|
245
283
|
async function reconcile(s) {
|
|
246
284
|
let mode = await fm.foundingModeFor(modeDeps(s));
|
|
247
285
|
if (!mode || !PROPER.includes(mode.stage) || mode.stage === 'legislation') return mode;
|
|
286
|
+
if (!hasBoardRoom(s.government)) {
|
|
287
|
+
// No Board Room: finished work IS the decision (task 1005115). Move past every
|
|
288
|
+
// stage whose tasks are all finished, in order; legislation waits for the alive door.
|
|
289
|
+
const all = s.lifecycle && typeof s.lifecycle.genesisTasks === 'function' ? await s.lifecycle.genesisTasks() : {};
|
|
290
|
+
while (mode && PROPER.includes(mode.stage) && mode.stage !== 'legislation' && allDone(all[mode.stage])) {
|
|
291
|
+
mode = await fm.setFoundingMode({ stage: fm.STAGES[fm.STAGES.indexOf(mode.stage) + 1] }, null, modeDeps(s));
|
|
292
|
+
}
|
|
293
|
+
return mode;
|
|
294
|
+
}
|
|
248
295
|
const board = await boardByStage(s.government);
|
|
249
296
|
while (mode && PROPER.includes(mode.stage) && mode.stage !== 'legislation'
|
|
250
297
|
&& board[mode.stage] && board[mode.stage].state === 'passed') {
|
|
@@ -268,7 +315,11 @@ async function readGenesis(deps = {}) {
|
|
|
268
315
|
const row = await s.get(fm.stageAtKey(key));
|
|
269
316
|
if (row && row.value) stageAt[key] = row.value;
|
|
270
317
|
}
|
|
271
|
-
|
|
318
|
+
let documents = {};
|
|
319
|
+
if (typeof s.documents === 'function') {
|
|
320
|
+
try { documents = (await s.documents()) || {}; } catch (_) { documents = {}; } // the home still renders without them
|
|
321
|
+
}
|
|
322
|
+
return shapeGenesis({ mode, plan, tasks, board, stageAt, boardRoom: hasBoardRoom(s.government), documents });
|
|
272
323
|
}
|
|
273
324
|
|
|
274
325
|
// startFounding — a brand-new project enters genesis (founding-birth.js calls this once
|
|
@@ -403,6 +454,13 @@ async function closeStage(stageKey, builderId, deps = {}) {
|
|
|
403
454
|
// stage's pass waits for the earlier ones. A stage already behind, or the decide
|
|
404
455
|
// pre-stage, is still refused.
|
|
405
456
|
if (!PROPER.includes(stageKey)) return { ok: false, reason: 'unknown_stage' };
|
|
457
|
+
// No Board Room (task 1005115): the stage already moved on by itself when its tasks
|
|
458
|
+
// finished, so asking to close it again is answered with what happened, not refused.
|
|
459
|
+
if (!hasBoardRoom(s.government) && PROPER.includes(mode.stage)
|
|
460
|
+
&& fm.STAGES.indexOf(stageKey) < fm.STAGES.indexOf(mode.stage)) {
|
|
461
|
+
// next_stage is where the project IS now, which may be several stages on.
|
|
462
|
+
return { ok: true, closed: true, stage: stageKey, next_stage: mode.stage, item_id: null, confirm: null, already: true };
|
|
463
|
+
}
|
|
406
464
|
if (!PROPER.includes(mode.stage) || fm.STAGES.indexOf(stageKey) < fm.STAGES.indexOf(mode.stage)) {
|
|
407
465
|
return { ok: false, reason: 'not_current_stage', stage: mode.stage };
|
|
408
466
|
}
|
|
@@ -546,7 +604,12 @@ async function putFinishedStages(deps = {}) {
|
|
|
546
604
|
// stage; on a project that is not founding this costs the one genesis.enabled read.
|
|
547
605
|
// Best-effort: the founder's own close button is still there if this misses.
|
|
548
606
|
async function onTaskShipped(deps = {}) {
|
|
549
|
-
try {
|
|
607
|
+
try {
|
|
608
|
+
const s = seams(deps);
|
|
609
|
+
// No Board Room: the ship that finishes a stage moves the project on (task 1005115).
|
|
610
|
+
if (!hasBoardRoom(s.government)) { await reconcile(s); return []; }
|
|
611
|
+
return await putFinishedStages(deps);
|
|
612
|
+
} catch (_) { return []; }
|
|
550
613
|
}
|
|
551
614
|
|
|
552
615
|
module.exports = {
|
|
@@ -260,6 +260,73 @@ function contextMarkdown(stages) {
|
|
|
260
260
|
...out].join('\n').trimEnd() + '\n';
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
// ── each stage's DOCUMENT (task 1005115) ──────────────────────────────────────
|
|
264
|
+
// A finished stage leaves something behind the founder can come back to: the scope, the
|
|
265
|
+
// strategy, the charter … It is NOT a second store. The document IS the stage's record
|
|
266
|
+
// above (its answers, its free page, its link) read as one named page, so editing it is
|
|
267
|
+
// editing those, through the same writes, by the same person. It stays editable after
|
|
268
|
+
// the stage moves on, and editing it never reopens the stage or undoes anything: the
|
|
269
|
+
// stage's progress is founding-mode's, and nothing here reads or writes it.
|
|
270
|
+
const DOCUMENT_TITLES = Object.freeze({
|
|
271
|
+
scope: 'The scope', strategize: 'The strategy', charter: 'The charter',
|
|
272
|
+
legalize: 'The rewards', legislation: 'The voting rules',
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// documentOf(shape) → the stage's document: its title, whether anything is in it yet,
|
|
276
|
+
// and the founder's words as markdown. Each part names the write that edits it, so a
|
|
277
|
+
// presentation (the game's district, the hall) never has to know the store.
|
|
278
|
+
// The markdown is the founder's raw words, unescaped: a reader shows it as text or through
|
|
279
|
+
// an escaping markdown renderer, never as HTML.
|
|
280
|
+
function documentOf(s) {
|
|
281
|
+
const said = s.questions.filter((q) => q.answer);
|
|
282
|
+
const lines = [`# ${DOCUMENT_TITLES[s.stage]}`, ''];
|
|
283
|
+
let group = null;
|
|
284
|
+
for (const q of said) {
|
|
285
|
+
if (q.group && q.group !== group) { group = q.group; lines.push(`## ${group}`, ''); }
|
|
286
|
+
const a = q.answer;
|
|
287
|
+
const body = a.state === 'answered' ? (a.choice_label || a.text) : `${STATE_WORDS[a.state]}${a.text ? ` — ${a.text}` : ''}`;
|
|
288
|
+
lines.push(`**${q.ask}**${ORIGIN_WORDS[a.origin] || ''}`, '', String(body), '');
|
|
289
|
+
}
|
|
290
|
+
if (s.page) lines.push(said.length ? "## In the founder's own words" : '', '', s.page, '');
|
|
291
|
+
if (s.link) lines.push(`The founder's existing document: ${s.link}`, '');
|
|
292
|
+
const filled = said.length > 0 || !!s.page || !!s.link;
|
|
293
|
+
return {
|
|
294
|
+
stage: s.stage,
|
|
295
|
+
title: DOCUMENT_TITLES[s.stage],
|
|
296
|
+
filled,
|
|
297
|
+
markdown: filled ? lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n' : '',
|
|
298
|
+
counts: s.counts,
|
|
299
|
+
updated_at: latest([s.updated_at, ...said.map((q) => q.answer.updated_at)]),
|
|
300
|
+
// Editing never reopens the stage. Task 1005107 narrows this for a stage the Board
|
|
301
|
+
// Room passed; until then every document is open to its author.
|
|
302
|
+
editable: true,
|
|
303
|
+
edit: {
|
|
304
|
+
answer: `PUT /api/bongos/founding/work/${s.stage}/answers/:question`,
|
|
305
|
+
page: `PUT /api/bongos/founding/work/${s.stage}/page`,
|
|
306
|
+
link: `PUT /api/bongos/founding/work/${s.stage}/link`,
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const latest = (isos) => isos.filter(Boolean).sort().at(-1) || null;
|
|
312
|
+
|
|
313
|
+
async function readDocument(stage, deps = {}) {
|
|
314
|
+
const r = await readStage(stage, deps);
|
|
315
|
+
if (!r.ok) return r;
|
|
316
|
+
return { ok: true, document: documentOf(r.work) };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// readDocuments(deps) → { stage: { title, filled, updated_at } } — the light summary the
|
|
320
|
+
// genesis home carries per stage, so the page shows each stage's document in place.
|
|
321
|
+
async function readDocuments(deps = {}) {
|
|
322
|
+
const out = {};
|
|
323
|
+
for (const s of await readAll(deps)) {
|
|
324
|
+
const d = documentOf(s);
|
|
325
|
+
out[s.stage] = { title: d.title, filled: d.filled, updated_at: d.updated_at, editable: d.editable };
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
|
|
263
330
|
// assistPrompt(stage) → the brief the founder hands their own AI session for the
|
|
264
331
|
// assisted door. It works the stage's own questions (never a shape of its own),
|
|
265
332
|
// argues rather than authors, and records only what the founder decides.
|
|
@@ -480,5 +547,6 @@ async function readContext(deps = {}) {
|
|
|
480
547
|
// and are exercised through the operations (tests/genesis_work.mjs).
|
|
481
548
|
module.exports = {
|
|
482
549
|
STAGES, LIMITS, QUESTIONS, assistPrompt,
|
|
550
|
+
readDocument, readDocuments,
|
|
483
551
|
pgStore, readAll, readStage, setDoor, setPage, setLink, setAnswer, clearAnswer, answerHistory, readContext,
|
|
484
552
|
};
|