@bongos/core 1.25.19 → 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 +108 -78
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -1
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +36 -2
- package/docs/api-reference.md +5 -4
- package/docs/copy-inventory.md +217 -215
- package/docs/copy-registry.json +256 -238
- package/docs/module-api-changelog.md +2 -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 +23 -4
- 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 +2 -0
- package/modules/onboarding/guide-handoff.js +109 -0
- package/modules/onboarding/routes/genesis-work.js +9 -5
- 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 +10 -0
- 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 +24 -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
|
@@ -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
|
};
|
|
@@ -194,6 +194,8 @@ function shapeGenesis({ mode, plan, tasks = {}, board = {}, stageAt = {}, boardR
|
|
|
194
194
|
plate: info.plate,
|
|
195
195
|
done_when: info.done_when,
|
|
196
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
|
|
197
199
|
days,
|
|
198
200
|
minutes,
|
|
199
201
|
due,
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/onboarding/guide-handoff.js — THE GUIDE'S ANSWERS, ARRIVED (task 1005274, goal
|
|
4
|
+
// 1000106). The founder talked with the start-a-project guide before signing up; the hub
|
|
5
|
+
// stamped what came out of it on the new project (modules/provisioning/guide-handoff.js),
|
|
6
|
+
// and it reached this instance as one web.env line, mapped by src/branding.js onto the
|
|
7
|
+
// server-only founding.guideHandoff (base64url JSON { scope, markdown }).
|
|
8
|
+
//
|
|
9
|
+
// Here it lands, once, in the two places a later reader already looks:
|
|
10
|
+
// * The Genesis scope interview: each carried answer is written as ORIGIN carried_over
|
|
11
|
+
// through genesis-work's own if-empty write, so it never replaces anything the founder
|
|
12
|
+
// already typed, and the interview shows it as carried over and editable (any edit by
|
|
13
|
+
// the founder makes it theirs). Only the questions the hub's explicit map answered are
|
|
14
|
+
// written; the rest stay empty.
|
|
15
|
+
// * The hand-off file: kept in project_settings (HANDOFF_KEY) and served at the top of
|
|
16
|
+
// GET /founding/context, the read `npx bongos genesis context` prints and the project
|
|
17
|
+
// context scaffold (docs/project-context.template.md) tells every session to run first.
|
|
18
|
+
// So the founder's own AI meets it in its first session without being told where.
|
|
19
|
+
//
|
|
20
|
+
// WHEN: lazily, on the first genesis read (routes/genesis-work.js), not at boot — the
|
|
21
|
+
// founding-birth.js reasoning: by then the database is up and the founder is seated. NOT
|
|
22
|
+
// tied to genesis being switched on: a paused project still keeps its founder's words.
|
|
23
|
+
// ONCE: the hand-off file's presence is the done-mark, written LAST, so a run that fails
|
|
24
|
+
// half-way is retried and the if-empty writes make the retry harmless. No marker (a project
|
|
25
|
+
// whose founder never used the guide, or one made before this shipped): resolves without
|
|
26
|
+
// touching the database.
|
|
27
|
+
|
|
28
|
+
const HANDOFF_KEY = 'founding.guide_handoff';
|
|
29
|
+
|
|
30
|
+
// handoffMarker(founding) — PURE. founding.guideHandoff → { scope, markdown } or null.
|
|
31
|
+
function handoffMarker(founding) {
|
|
32
|
+
const raw = founding && typeof founding === 'object' ? founding.guideHandoff : null;
|
|
33
|
+
if (typeof raw !== 'string' || !raw.trim()) return null;
|
|
34
|
+
try {
|
|
35
|
+
const h = JSON.parse(Buffer.from(raw.trim(), 'base64url').toString('utf8'));
|
|
36
|
+
if (!h || typeof h.markdown !== 'string' || !h.markdown.trim()) return null;
|
|
37
|
+
const scope = h.scope && typeof h.scope === 'object' && !Array.isArray(h.scope) ? h.scope : {};
|
|
38
|
+
return { scope, markdown: h.markdown };
|
|
39
|
+
} catch (_) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function markerFrom(deps) {
|
|
45
|
+
if (Object.prototype.hasOwnProperty.call(deps, 'marker')) return deps.marker;
|
|
46
|
+
try { return handoffMarker(require('../../src/module-api').branding().founding); } catch (_) { return null; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const settingsOf = (deps) => ({
|
|
50
|
+
get: deps.getSetting || require('../../src/module-api').projectSettings.get,
|
|
51
|
+
set: deps.setSetting || require('../../src/module-api').projectSettings.set,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// carryOver(deps) → what it did: { ran:false, reason } or { ran:true, carried:[ids], kept:[ids] }.
|
|
55
|
+
// carried questions now holding the guide's words
|
|
56
|
+
// kept questions the founder had already answered, left alone
|
|
57
|
+
async function carryOver(deps = {}) {
|
|
58
|
+
const marker = markerFrom(deps);
|
|
59
|
+
if (!marker) return { ran: false, reason: 'no_handoff' };
|
|
60
|
+
const { get, set } = settingsOf(deps);
|
|
61
|
+
const done = await get(HANDOFF_KEY);
|
|
62
|
+
if (done && done.value) return { ran: false, reason: 'already' };
|
|
63
|
+
const work = require('./genesis-work');
|
|
64
|
+
const carried = [];
|
|
65
|
+
const kept = [];
|
|
66
|
+
for (const [question, text] of Object.entries(marker.scope)) {
|
|
67
|
+
if (typeof text !== 'string' || !text.trim()) continue;
|
|
68
|
+
const r = await work.setAnswer('scope', question, { state: 'answered', text, origin: 'carried_over' }, null, deps, { ifEmpty: true });
|
|
69
|
+
if (r.ok === false) continue; // a question this core no longer asks: skipped, never invented
|
|
70
|
+
(r.changed ? carried : kept).push(question);
|
|
71
|
+
}
|
|
72
|
+
await set(HANDOFF_KEY, marker.markdown, null);
|
|
73
|
+
return { ran: true, carried, kept };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ensureHandoff(deps) — the once-per-process door the reads call. Never throws: a hand-off
|
|
77
|
+
// hiccup must never fail the read that triggered it; a failed run is retried next read.
|
|
78
|
+
let once = null;
|
|
79
|
+
function ensureHandoff(deps = {}) {
|
|
80
|
+
if (deps.fresh) return carryOver(deps).catch(() => ({ ran: false, reason: 'error' }));
|
|
81
|
+
if (once) return once;
|
|
82
|
+
const p = carryOver(deps).then((r) => r, (err) => {
|
|
83
|
+
once = null;
|
|
84
|
+
try { require('../../src/module-api').logger('onboarding').error('[onboarding] guide hand-off failed', err); } catch (_) { /* no logger */ }
|
|
85
|
+
return { ran: false, reason: 'error' };
|
|
86
|
+
});
|
|
87
|
+
once = p;
|
|
88
|
+
return p;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// readHandoff(deps) → the hand-off file's markdown, or null. No marker, no read: a project
|
|
92
|
+
// whose founder never used the guide never touches the database for it.
|
|
93
|
+
async function readHandoff(deps = {}) {
|
|
94
|
+
if (!markerFrom(deps)) return null;
|
|
95
|
+
const row = await settingsOf(deps).get(HANDOFF_KEY);
|
|
96
|
+
return row && typeof row.value === 'string' && row.value.trim() ? row.value : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// withHandoff(context, markdown) — PURE. The planning read with the hand-off file first:
|
|
100
|
+
// it is where the project started, and the founder's own answers below it are their
|
|
101
|
+
// latest word.
|
|
102
|
+
function withHandoff(context, markdown) {
|
|
103
|
+
if (!markdown) return { ...context, handoff: null };
|
|
104
|
+
const said = context.markdown ? `\n---\n\n${context.markdown}` : '';
|
|
105
|
+
return { ...context, handoff: markdown, markdown: `${markdown.trimEnd()}\n${said}` };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
module.exports = { HANDOFF_KEY, handoffMarker, carryOver, ensureHandoff, readHandoff, withHandoff };
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
const api = require('../../../src/module-api');
|
|
14
14
|
const work = require('../genesis-work');
|
|
15
|
+
const handoff = require('../guide-handoff'); // the start-a-project guide's answers, carried in once (task 1005274)
|
|
15
16
|
|
|
16
17
|
const REFUSALS = {
|
|
17
18
|
unknown_stage: 404, unknown_question: 404, bad_door: 400, bad_state: 400, bad_origin: 400,
|
|
@@ -37,13 +38,16 @@ module.exports = function mountGenesisWorkRoutes(router, deps = {}) {
|
|
|
37
38
|
const stage = (req) => String(req.params.stage);
|
|
38
39
|
const q = (req) => String(req.params.question);
|
|
39
40
|
|
|
40
|
-
// The read path for every door and for AI sessions planning a goal or task.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
// The read path for every door and for AI sessions planning a goal or task. Each first
|
|
42
|
+
// lands the guide's hand-off if one is waiting (../guide-handoff.js), so the scope
|
|
43
|
+
// interview opens with the carried-over answers and the context leads with the file.
|
|
44
|
+
const arrived = (fn) => async (req) => { await handoff.ensureHandoff(deps); return fn(req); };
|
|
45
|
+
router.get('/founding/work', api.requireBuilder, handle(arrived(async () => ({ stages: await work.readAll(deps) }))));
|
|
46
|
+
router.get('/founding/context', api.requireBuilder, handle(arrived(async () => handoff.withHandoff(await work.readContext(deps), await handoff.readHandoff(deps)))));
|
|
47
|
+
router.get('/founding/work/:stage', api.requireBuilder, handle(arrived((req) => work.readStage(stage(req), deps))));
|
|
44
48
|
// The stage's document (task 1005115): its record read as one named page, editable
|
|
45
49
|
// through the writes below at any time, without reopening the stage.
|
|
46
|
-
router.get('/founding/work/:stage/document', api.requireBuilder, handle((req) => work.readDocument(stage(req), deps)));
|
|
50
|
+
router.get('/founding/work/:stage/document', api.requireBuilder, handle(arrived((req) => work.readDocument(stage(req), deps))));
|
|
47
51
|
router.get('/founding/work/:stage/assist', api.requireBuilder, handle(async (req) => {
|
|
48
52
|
const prompt = work.assistPrompt(stage(req));
|
|
49
53
|
return prompt ? { prompt } : { ok: false, reason: 'unknown_stage' };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/provisioning/guide-handoff.js — THE GUIDE'S ANSWERS RIDE INTO THE NEW PROJECT
|
|
4
|
+
// (task 1005274, goal 1000106). A founder who talked with the start-a-project guide
|
|
5
|
+
// (modules/front-door-guide) should never be asked the same thing twice. The creation
|
|
6
|
+
// page pre-fills itself from the plan; this file carries the rest across to the new
|
|
7
|
+
// project, whose database this process cannot reach — the birth marker's path exactly
|
|
8
|
+
// (birth.js, modules/onboarding/founding-birth.js):
|
|
9
|
+
//
|
|
10
|
+
// 1. The create request says `from_guide: true` (the page sends it only when it
|
|
11
|
+
// pre-filled from a plan). The route asks the guide, through the
|
|
12
|
+
// `front-door-guide.handoff` port, for the CALLER's own newest attached plan — never a
|
|
13
|
+
// plan named by the request, so nobody can carry someone else's words in.
|
|
14
|
+
// 2. stampGuideHandoff writes the reserved write-once settings key `guide_handoff`
|
|
15
|
+
// ({ scope, markdown }) on the row this request JUST made.
|
|
16
|
+
// 3. The companion GUIDE_HANDOFF carries it into the instance's web.env as one
|
|
17
|
+
// base64url line; src/branding.js maps it onto the server-only founding.guideHandoff,
|
|
18
|
+
// and modules/onboarding/guide-handoff.js writes it once: the scope answers as
|
|
19
|
+
// carried over (never over the founder's own) and the hand-off file.
|
|
20
|
+
//
|
|
21
|
+
// No guide, no flag, no port (an instance without the guide) or no plan: nothing is
|
|
22
|
+
// stamped and the companion goes out empty, so a visitor who never used the guide sees
|
|
23
|
+
// exactly what they saw before.
|
|
24
|
+
|
|
25
|
+
const KEY = 'guide_handoff';
|
|
26
|
+
const MAX_BYTES = 24000; // the stored JSON; handoff.js already bounds the markdown
|
|
27
|
+
|
|
28
|
+
const storedOf = (row) => (row && row.settings && typeof row.settings === 'object' && !Array.isArray(row.settings) ? row.settings : {});
|
|
29
|
+
|
|
30
|
+
// cleanHandoff(h) — PURE. The port's answer reduced to exactly { scope, markdown }, or null.
|
|
31
|
+
function cleanHandoff(h) {
|
|
32
|
+
if (!h || typeof h !== 'object' || typeof h.markdown !== 'string' || !h.markdown.trim()) return null;
|
|
33
|
+
const scope = {};
|
|
34
|
+
if (h.scope && typeof h.scope === 'object') {
|
|
35
|
+
for (const [k, v] of Object.entries(h.scope)) if (/^[a-z_]{1,40}$/.test(k) && typeof v === 'string' && v.trim()) scope[k] = v;
|
|
36
|
+
}
|
|
37
|
+
const out = { scope, markdown: h.markdown };
|
|
38
|
+
return Buffer.byteLength(JSON.stringify(out)) <= MAX_BYTES ? out : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// guideHandoffEnv(row) — PURE. The companion line's value: base64url JSON, or '' for none.
|
|
42
|
+
function guideHandoffEnv(row) {
|
|
43
|
+
const h = cleanHandoff(storedOf(row)[KEY]);
|
|
44
|
+
return h ? Buffer.from(JSON.stringify(h), 'utf8').toString('base64url') : '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// stampGuideHandoff(db, instanceId, builderId, { name, resolve }) — the create route's
|
|
48
|
+
// call, on the row it just made. Returns true when a hand-off was stamped. Write-once.
|
|
49
|
+
async function stampGuideHandoff(db, instanceId, builderId, { name = '', resolve } = {}) {
|
|
50
|
+
const port = (resolve || require('../../src/module-api').resolveOptional)('front-door-guide.handoff');
|
|
51
|
+
if (typeof port !== 'function') return false;
|
|
52
|
+
const h = cleanHandoff(await port(builderId, { name }));
|
|
53
|
+
if (!h) return false;
|
|
54
|
+
await db.query(
|
|
55
|
+
`UPDATE provisioning_instances
|
|
56
|
+
SET settings = COALESCE(settings, '{}'::jsonb) || jsonb_build_object($2::text, $3::jsonb)
|
|
57
|
+
WHERE id = $1 AND NOT (COALESCE(settings, '{}'::jsonb) ? $2::text)`,
|
|
58
|
+
[instanceId, KEY, JSON.stringify(h)],
|
|
59
|
+
);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const GUIDE_COMPANION_ENV = Object.freeze({ GUIDE_HANDOFF: guideHandoffEnv });
|
|
64
|
+
|
|
65
|
+
module.exports = { KEY, GUIDE_COMPANION_ENV, stampGuideHandoff };
|
|
@@ -25,7 +25,7 @@ const { normalizeModuleSelection, effectiveModules } = require('./starter-bundle
|
|
|
25
25
|
const { PLANET_PHYSICS_QUESTIONS, canonicalAnswer } = require('./planet-physics');
|
|
26
26
|
const { softLimitsFor } = require('./soft-limits'); const { demoView } = require('./demo'); const { isTestProject, TEST_COMPANION_ENV } = require('./test-project'); // the demo marker's owner view (task 1004420); the test mark (task 1005036)
|
|
27
27
|
const { dnsResolves, recordDnsResolved, lookupAnswers } = require('./dns-resolves'); // has the platform's lookup seen the address answer (task 1004505)
|
|
28
|
-
const { LOOK_SETTINGS_VOCAB, LOOK_DEFAULTS, LOOK_COMPANION_ENV } = require('./look'); const { BIRTH_COMPANION_ENV } = require('./birth'); // born into genesis (task 1004506)
|
|
28
|
+
const { LOOK_SETTINGS_VOCAB, LOOK_DEFAULTS, LOOK_COMPANION_ENV } = require('./look'); const { BIRTH_COMPANION_ENV } = require('./birth'); const { GUIDE_COMPANION_ENV } = require('./guide-handoff'); // born into genesis (task 1004506); the guide's hand-off (task 1005274)
|
|
29
29
|
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
31
31
|
// Constants (mirror the CHECK constraints in provisioning_001_tables.sql)
|
|
@@ -513,7 +513,7 @@ function settingsEnvVars(row, prefix) {
|
|
|
513
513
|
// cutoff and every unresolved review counts, which is the right answer for a
|
|
514
514
|
// project that has been strict since it stood up.
|
|
515
515
|
const SETTINGS_COMPANION_ENV = Object.freeze({
|
|
516
|
-
ARTIST_GATE_SINCE: (row) => artistGateStrictSince(row) || '', ...LOOK_COMPANION_ENV, ...BIRTH_COMPANION_ENV, ...TEST_COMPANION_ENV, // + accent, logo (look.js); birth, demo box (birth.js); test mark (test-project.js)
|
|
516
|
+
ARTIST_GATE_SINCE: (row) => artistGateStrictSince(row) || '', ...LOOK_COMPANION_ENV, ...BIRTH_COMPANION_ENV, ...TEST_COMPANION_ENV, ...GUIDE_COMPANION_ENV, // + accent, logo (look.js); birth, demo box (birth.js); test mark (test-project.js); the guide's hand-off (guide-handoff.js)
|
|
517
517
|
});
|
|
518
518
|
|
|
519
519
|
// The web.env block the runner writes at STANDUP (task 1003139): a comment that
|
|
@@ -65,7 +65,7 @@ const catalogBridge = require('../catalog-bridge');
|
|
|
65
65
|
const capacity = require('../capacity'); // the box's slot ceiling (ADR 0285, task 1003927)
|
|
66
66
|
const { callbackPage } = require('./callback-page'); const githubSetup = require('../github-setup');
|
|
67
67
|
// the LOUD body validators (task 1003504) — lifted out when this file hit the size ratchet
|
|
68
|
-
const { badProjectDetail, badModuleSelection, badProjectType, screenedOut } = require('./body-validators'); const demo = require('../demo'); const birth = require('../birth'); const testProject = require('../test-project');
|
|
68
|
+
const { badProjectDetail, badModuleSelection, badProjectType, screenedOut } = require('./body-validators'); const demo = require('../demo'); const birth = require('../birth'); const testProject = require('../test-project'); const guideHandoff = require('../guide-handoff');
|
|
69
69
|
const { failFrom } = require('../public-refusal'); // every catch: a declared refusal reaches the owner, anything else stays opaque (task 1004126)
|
|
70
70
|
const { settingsPush, failPushConflict } = require('../settings-push'); // a settings save's push + receipt, shared with routes/look.js (task 1004421)
|
|
71
71
|
const { requireProjectOwner, projectActor, isProjectOwner } = require('../route-helpers'); // the owner-or-archon gate (task 1004159, ADR 0328)
|
|
@@ -362,7 +362,7 @@ module.exports = function provisioningRoutes() {
|
|
|
362
362
|
// exactly the set the panel preselected. An explicit [] is the different
|
|
363
363
|
// answer "none of them". maxItems is a sanity ceiling well above a catalog
|
|
364
364
|
// that is three modules wide today.
|
|
365
|
-
modules: { type: 'array', itemsType: 'string', maxItems: 50 }, terms_acknowledged: { type: 'boolean' }, demo: { type: 'object' }, ship_mode: { type: 'string', enum: ['auto', 'manual'] }, genesis: { type: 'boolean' }, test: { type: 'boolean' }, // the wizard's terms tick (task 1004417, ../terms-ack.js); the demo route's { people, hours_each } (task 1004420, ../demo.js)
|
|
365
|
+
modules: { type: 'array', itemsType: 'string', maxItems: 50 }, terms_acknowledged: { type: 'boolean' }, demo: { type: 'object' }, ship_mode: { type: 'string', enum: ['auto', 'manual'] }, genesis: { type: 'boolean' }, test: { type: 'boolean' }, from_guide: { type: 'boolean' }, // from_guide: the page pre-filled from the guide's plan (task 1005274, ../guide-handoff.js); the wizard's terms tick (task 1004417, ../terms-ack.js); the demo route's { people, hours_each } (task 1004420, ../demo.js)
|
|
366
366
|
})) return;
|
|
367
367
|
const body = req.body || {};
|
|
368
368
|
const detailFault = badProjectDetail(body.detail);
|
|
@@ -485,6 +485,8 @@ module.exports = function provisioningRoutes() {
|
|
|
485
485
|
if (created && testAsk.test) await testProject.stampTestProject(pool, instance.id).catch((err) => log.error('[provisioning] test mark not stamped', err));
|
|
486
486
|
// The wizard's ship mode (task 1004629) rides beside it; a miss means the project asks at its first ship.
|
|
487
487
|
if (created) await birth.stampShipMode(pool, instance.id, body.ship_mode).catch((err) => log.error('[provisioning] ship mode not stamped', err));
|
|
488
|
+
// The guide's answers (task 1005274): the CALLER's own attached plan, never one the request names; a miss means the project opens unprefilled.
|
|
489
|
+
if (created && body.from_guide === true) await guideHandoff.stampGuideHandoff(pool, instance.id, req.builder.id, { name: slug }).catch((err) => log.error('[provisioning] guide hand-off not stamped', err));
|
|
488
490
|
const demoRow = created && demoAsk.demo ? await demo.startDemoProject(pool, provisioning, instance.id, demoAsk.demo) : null;
|
|
489
491
|
// Enqueue the provision intent (idempotent: an already-open intent is returned)
|
|
490
492
|
// — but ONLY when the instance actually needs standing up. Blindly enqueueing on
|