@bongos/core 1.25.19 → 1.25.21
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 +237 -112
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +12 -0
- package/clients/bongos-client/index.cjs +12 -0
- package/clients/bongos-client/index.d.ts +26 -9
- package/clients/bongos-client/index.mjs +12 -0
- package/docs/adr/0406-every-project-gets-a-sandbox-that-follows-main-and-live-moves-only-when-a-person-promotes.md +10 -0
- package/docs/adr/0407-a-project-grant-is-made-by-the-owner-not-witnessed-at-sign-in.md +3 -1
- package/docs/api/openapi.json +444 -37
- package/docs/api-reference.md +11 -5
- package/docs/copy-inventory.md +223 -221
- package/docs/copy-registry.json +262 -244
- 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/government/board.js +8 -7
- package/modules/government/charter-advice.js +10 -3
- package/modules/government/charter.js +88 -4
- package/modules/government/migrations/government_039_vote_cast_via_filed.sql +22 -0
- package/modules/government/routes/government.js +12 -12
- package/modules/government/sole-member.js +75 -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 +35 -9
- 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 +16 -2
- package/modules/onboarding/guide-handoff.js +109 -0
- package/modules/onboarding/routes/genesis-work.js +9 -5
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/project-standing.js +49 -0
- package/modules/platform-identity/routes/my-projects.js +8 -0
- package/modules/provisioning/grants.js +126 -0
- package/modules/provisioning/guide-handoff.js +65 -0
- package/modules/provisioning/migrations/provisioning_038_render_promote.sql +43 -0
- package/modules/provisioning/migrations/provisioning_039_project_grants.sql +40 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/project-refusal.js +107 -0
- package/modules/provisioning/provisioning.js +4 -4
- package/modules/provisioning/render-lookup.js +36 -1
- package/modules/provisioning/render-promote.js +80 -0
- package/modules/provisioning/route-helpers.js +70 -16
- package/modules/provisioning/routes/env-manifest.js +21 -5
- package/modules/provisioning/routes/grants.js +141 -0
- package/modules/provisioning/routes/provisioning.js +12 -9
- package/modules/provisioning/routes/render-standup.js +82 -3
- package/modules/public-landing/public/projects.html +178 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +28 -0
- package/scripts/bongos/genesis.js +11 -1
- package/scripts/bongos/government-walk-sole.js +41 -0
- package/scripts/bongos/government-walk.js +15 -16
- package/scripts/bongos/provision-render-common.js +48 -0
- package/scripts/bongos/provision-render-promote.js +93 -0
- package/scripts/bongos/provision-render-sandbox.js +14 -2
- package/scripts/bongos/provision-render.js +4 -36
- package/scripts/bongos/provision.js +1 -0
- package/scripts/bongos/render-api.js +17 -2
- package/scripts/gds/government-walk-sole.js +6 -0
- package/scripts/gds/provision-render-common.js +6 -0
- package/scripts/gds/provision-render-promote.js +6 -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/fleet_cost_view.mjs +2 -0
- 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_government.mjs +5 -1
- package/tests/genesis_home.mjs +41 -0
- package/tests/government_charter_advice.mjs +49 -0
- package/tests/government_charter_forms.mjs +14 -3
- package/tests/government_seed.mjs +1 -0
- package/tests/government_sole_member.mjs +153 -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/project_catalog_routes.mjs +2 -0
- package/tests/provision_settings_apply.mjs +2 -1
- package/tests/provisioning_env_manifest.mjs +4 -0
- package/tests/provisioning_env_manifest_edit_delete.mjs +2 -0
- package/tests/provisioning_project_grants.mjs +458 -0
- package/tests/provisioning_project_owner_gate.mjs +3 -1
- package/tests/provisioning_project_refusal.mjs +316 -0
- package/tests/provisioning_render_route.mjs +1 -1
- package/tests/provisioning_restart.mjs +2 -0
- package/tests/provisioning_settings.mjs +2 -0
- package/tests/provisioning_settings_apply.mjs +1 -1
- package/tests/provisioning_settings_env.mjs +2 -2
- package/tests/provisioning_teardown_intent.mjs +2 -2
- package/tests/render_promote.mjs +309 -0
- package/tests/render_sandbox.mjs +9 -4
|
@@ -65,10 +65,11 @@ 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
|
-
const { requireProjectOwner, projectActor, isProjectOwner } = require('../route-helpers');
|
|
71
|
+
const { requireProjectOwner, projectActor, projectActorLabel, isProjectOwner, grantedView } = require('../route-helpers');
|
|
72
|
+
const GRANTEE = { grantee: 'operate' }; // the three routes here a granted helper may use (task 1004161, ADR 0407 §3) // the owner-or-archon gate (task 1004159, ADR 0328)
|
|
72
73
|
// the kernel ports this module provides (moved out at the size ratchet, task 1003578)
|
|
73
74
|
const { registerProvisioningSeams } = require('../seams');
|
|
74
75
|
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
@@ -362,7 +363,7 @@ module.exports = function provisioningRoutes() {
|
|
|
362
363
|
// exactly the set the panel preselected. An explicit [] is the different
|
|
363
364
|
// answer "none of them". maxItems is a sanity ceiling well above a catalog
|
|
364
365
|
// 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)
|
|
366
|
+
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
367
|
})) return;
|
|
367
368
|
const body = req.body || {};
|
|
368
369
|
const detailFault = badProjectDetail(body.detail);
|
|
@@ -485,6 +486,8 @@ module.exports = function provisioningRoutes() {
|
|
|
485
486
|
if (created && testAsk.test) await testProject.stampTestProject(pool, instance.id).catch((err) => log.error('[provisioning] test mark not stamped', err));
|
|
486
487
|
// The wizard's ship mode (task 1004629) rides beside it; a miss means the project asks at its first ship.
|
|
487
488
|
if (created) await birth.stampShipMode(pool, instance.id, body.ship_mode).catch((err) => log.error('[provisioning] ship mode not stamped', err));
|
|
489
|
+
// The guide's answers (task 1005274): the CALLER's own attached plan, never one the request names; a miss means the project opens unprefilled.
|
|
490
|
+
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
491
|
const demoRow = created && demoAsk.demo ? await demo.startDemoProject(pool, provisioning, instance.id, demoAsk.demo) : null;
|
|
489
492
|
// Enqueue the provision intent (idempotent: an already-open intent is returned)
|
|
490
493
|
// — but ONLY when the instance actually needs standing up. Blindly enqueueing on
|
|
@@ -656,7 +659,7 @@ module.exports = function provisioningRoutes() {
|
|
|
656
659
|
// GET /provisioning/instances/:id — read one instance (the manage page's read).
|
|
657
660
|
// rank: any authenticated builder (own resource; archon may read any — task
|
|
658
661
|
// 1003041). 404 otherwise — does not leak another owner's instance existence.
|
|
659
|
-
router.get('/provisioning/instances/:id', auth.requireBuilder, requireProjectOwner(loadInstance), async (req, res) => {
|
|
662
|
+
router.get('/provisioning/instances/:id', auth.requireBuilder, requireProjectOwner(loadInstance, GRANTEE), async (req, res) => {
|
|
660
663
|
const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
|
|
661
664
|
if (!id) return;
|
|
662
665
|
try {
|
|
@@ -674,7 +677,7 @@ module.exports = function provisioningRoutes() {
|
|
|
674
677
|
// panel recovers it from server state after a reload — not from sessionStorage.
|
|
675
678
|
const manifest = await provisioning.getLatestOAuthManifestForInstance(pool, id);
|
|
676
679
|
const githubSetupNow = await githubSetup.githubSetupRead(pool, inst, manifest, manifest ? provisioning.publicGithubApp(manifest).status : null, { withState: false });
|
|
677
|
-
res.json({
|
|
680
|
+
res.json(grantedView(req, {
|
|
678
681
|
// the standing install nudge rides the instance, as on the list read (task 1004763)
|
|
679
682
|
instance: { ...ownerInstance(inst), github_install: githubSetup.installNudge(githubSetupNow) },
|
|
680
683
|
open_intent: open ? { action: open.action, state: open.state } : null,
|
|
@@ -682,7 +685,7 @@ module.exports = function provisioningRoutes() {
|
|
|
682
685
|
// the four-step checklist from what the runner verified (task 1004649) — the
|
|
683
686
|
// install link rides WITHOUT the state token this read never carries
|
|
684
687
|
github_setup: githubSetupNow,
|
|
685
|
-
});
|
|
688
|
+
}));
|
|
686
689
|
} catch (err) {
|
|
687
690
|
log.error('[provisioning] GET /provisioning/instances/:id', err);
|
|
688
691
|
failFrom(res, err, 'read_failed');
|
|
@@ -872,7 +875,7 @@ module.exports = function provisioningRoutes() {
|
|
|
872
875
|
// GET /provisioning/instances/:id/settings — read a project's effective
|
|
873
876
|
// settings + the allowed values (so a form can render its choices).
|
|
874
877
|
// rank: any authenticated builder (own resource; archon may read any).
|
|
875
|
-
router.get('/provisioning/instances/:id/settings', auth.requireBuilder, requireProjectOwner(loadInstance), async (req, res) => {
|
|
878
|
+
router.get('/provisioning/instances/:id/settings', auth.requireBuilder, requireProjectOwner(loadInstance, GRANTEE), async (req, res) => {
|
|
876
879
|
try {
|
|
877
880
|
const inst = req.project;
|
|
878
881
|
// `planet` rides beside `settings` (task 1003347): the same row, read by a
|
|
@@ -1036,7 +1039,7 @@ module.exports = function provisioningRoutes() {
|
|
|
1036
1039
|
// Every refusal names the action that IS available in that state rather than
|
|
1037
1040
|
// hiding behind a code, and no branch promises a duration — a restart's downtime
|
|
1038
1041
|
// is exactly what this route cannot know (the F16 rule, ADR 0190 §7).
|
|
1039
|
-
router.post('/provisioning/instances/:id/restart', auth.requireBuilder, requireProjectOwner(loadInstance), async (req, res) => {
|
|
1042
|
+
router.post('/provisioning/instances/:id/restart', auth.requireBuilder, requireProjectOwner(loadInstance, GRANTEE), async (req, res) => {
|
|
1040
1043
|
const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
|
|
1041
1044
|
if (!id) return;
|
|
1042
1045
|
try {
|
|
@@ -1067,7 +1070,7 @@ module.exports = function provisioningRoutes() {
|
|
|
1067
1070
|
if (r.created) {
|
|
1068
1071
|
await provisioning.recordEvent(pool, {
|
|
1069
1072
|
instanceId: id, ownerBuilderId: inst.owner_builder_id, event: 'restart',
|
|
1070
|
-
detail:
|
|
1073
|
+
detail: `restart requested by ${projectActorLabel(req)}`, actor,
|
|
1071
1074
|
}).catch(() => {});
|
|
1072
1075
|
}
|
|
1073
1076
|
res.json({ ok: true, queued: r.created, action: 'restart', status: inst.status, message: r.created
|
|
@@ -23,7 +23,8 @@ const { pool, validateOrRespond, parseId } = api;
|
|
|
23
23
|
const provisioning = require('../provisioning');
|
|
24
24
|
const renderStandup = require('../render-standup');
|
|
25
25
|
const { failFrom } = require('../public-refusal');
|
|
26
|
-
const { lookupRender, RenderLookupError } = require('../render-lookup');
|
|
26
|
+
const { lookupRender, lookupPromotion, RenderLookupError } = require('../render-lookup');
|
|
27
|
+
const renderPromote = require('../render-promote');
|
|
27
28
|
const { appStatus } = require('../app-status');
|
|
28
29
|
const renderSandbox = require('../render-sandbox');
|
|
29
30
|
const envManifest = require('../env-manifest');
|
|
@@ -46,6 +47,10 @@ const LOOKUP_STATUS = { render_key_refused: 422, render_workspace_unreachable: 4
|
|
|
46
47
|
// OWNER ONLY, no archon override, for every Render route: the key and the account it acts
|
|
47
48
|
// in are the owner's. A missing project and someone else's answer the same 404, so an id is
|
|
48
49
|
// never confirmed.
|
|
50
|
+
// One sentence for "another run holds this project, so the key was dropped" — the sandbox
|
|
51
|
+
// and promote routes say it the same way (voice.md V1: one fixed sentence per concept).
|
|
52
|
+
const BUSY_KEY_NOT_KEPT = 'Something else is being done to this project right now, so your key was not kept. Try again in a minute.';
|
|
53
|
+
|
|
49
54
|
function ownedBy(inst, builder) { return !!(inst && builder && String(inst.owner_builder_id) === String(builder.id)); }
|
|
50
55
|
|
|
51
56
|
module.exports = function buildRenderStandupRouter({ fetchImpl } = {}) {
|
|
@@ -105,7 +110,9 @@ module.exports = function buildRenderStandupRouter({ fetchImpl } = {}) {
|
|
|
105
110
|
: [];
|
|
106
111
|
// The sandbox beside it (ADR 0406, task 1005289), in the same shape.
|
|
107
112
|
const sandbox = renderSandbox.sandboxStatus(inst);
|
|
108
|
-
|
|
113
|
+
// Promoting from it (ADR 0406 §2, task 1005290): whether the owner can, and the record.
|
|
114
|
+
const promote = { ...renderPromote.promoteView(inst), recent: await renderPromote.listPromotions(pool, id) };
|
|
115
|
+
res.json({ ok: true, app, sandbox, promote, env_vars: envVars, values_held: false });
|
|
109
116
|
} catch (err) {
|
|
110
117
|
log.error('[provisioning] GET /provisioning/instances/:id/render', err && err.message);
|
|
111
118
|
failFrom(res, err, 'render_status_failed');
|
|
@@ -139,7 +146,7 @@ module.exports = function buildRenderStandupRouter({ fetchImpl } = {}) {
|
|
|
139
146
|
const r = await renderSandbox.enqueueSandboxAdd(pool, id, { keyEnc: api.secretBox.encrypt(req.body.key), requestedBy: 'api:self' });
|
|
140
147
|
if (r.conflict) {
|
|
141
148
|
return res.fail('intent_conflict', { status: 409,
|
|
142
|
-
message:
|
|
149
|
+
message: BUSY_KEY_NOT_KEPT,
|
|
143
150
|
details: { action: r.intent.action, state: r.intent.state } });
|
|
144
151
|
}
|
|
145
152
|
await provisioning.recordEvent(pool, {
|
|
@@ -155,6 +162,78 @@ module.exports = function buildRenderStandupRouter({ fetchImpl } = {}) {
|
|
|
155
162
|
}
|
|
156
163
|
});
|
|
157
164
|
|
|
165
|
+
// POST /provisioning/instances/:id/render/promote/look — rank: any authenticated builder,
|
|
166
|
+
// and OWNER ONLY. What is waiting on the sandbox to go live (ADR 0406 §2, task 1005290):
|
|
167
|
+
// the change the live app runs, and every change that has run on the sandbox since, newest
|
|
168
|
+
// first. A read-only look at Render on the owner's key held for this one request (ADR 0348
|
|
169
|
+
// as widened by ADR 0406 §11): GET only, never stored, logged or sent back.
|
|
170
|
+
router.post('/provisioning/instances/:id/render/promote/look', api.requireBuilder, lookupLimit, async (req, res) => {
|
|
171
|
+
const id = parseId(req, res, { positiveInt: true });
|
|
172
|
+
if (!id) return;
|
|
173
|
+
if (validateOrRespond(req, res, { key: { required: true, type: 'string', maxLength: 200 } })) return;
|
|
174
|
+
try {
|
|
175
|
+
const inst = await provisioning.getInstanceById(pool, id);
|
|
176
|
+
if (!ownedBy(inst, req.builder)) return res.fail('instance_not_found', 404);
|
|
177
|
+
if (!renderStandup.KEY_RE.test(req.body.key)) return res.fail('render_request_invalid', { status: 400, message: 'That doesn’t look like a Render API key. It starts with “rnd_”.' });
|
|
178
|
+
const why = renderPromote.noPromoteReason(inst);
|
|
179
|
+
if (why) return res.fail('promote_unavailable', { status: 409, message: why });
|
|
180
|
+
const h = inst.app_host;
|
|
181
|
+
const out = await lookupPromotion(req.body.key, { ownerId: h.ownerId, liveServiceId: h.serviceId, sandboxServiceId: h.sandbox.serviceId, fetchImpl: fetchImpl || globalThis.fetch });
|
|
182
|
+
res.json({ ok: true, ...out });
|
|
183
|
+
} catch (err) {
|
|
184
|
+
if (err instanceof RenderLookupError) return res.fail(err.code, { status: LOOKUP_STATUS[err.code] || 502, message: err.message });
|
|
185
|
+
log.error('[provisioning] POST /provisioning/instances/:id/render/promote/look', err && err.message);
|
|
186
|
+
failFrom(res, err, 'render_lookup_failed');
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// POST /provisioning/instances/:id/render/promote — rank: any authenticated builder, and
|
|
191
|
+
// OWNER ONLY: the key and the live app are the owner's, so the server decides only the
|
|
192
|
+
// owner may move it (an archon on someone else's project gets the stranger's 404). Takes
|
|
193
|
+
// the change the owner chose (`commit_id`) and the change live ran when they looked
|
|
194
|
+
// (`expect_live_commit`); the runner refuses if either moved. This is the ONLY way Bongos
|
|
195
|
+
// moves a live app that has a sandbox, and the reply is a receipt: nothing is live yet.
|
|
196
|
+
router.post('/provisioning/instances/:id/render/promote', api.requireBuilder, sandboxLimit, async (req, res) => {
|
|
197
|
+
const id = parseId(req, res, { positiveInt: true });
|
|
198
|
+
if (!id) return;
|
|
199
|
+
if (validateOrRespond(req, res, {
|
|
200
|
+
key: { required: true, type: 'string', maxLength: 200 },
|
|
201
|
+
commit_id: { required: true, type: 'string', maxLength: 64 },
|
|
202
|
+
expect_live_commit: { type: 'string', maxLength: 64 },
|
|
203
|
+
})) return;
|
|
204
|
+
try {
|
|
205
|
+
const inst = await provisioning.getInstanceById(pool, id);
|
|
206
|
+
if (!ownedBy(inst, req.builder)) return res.fail('instance_not_found', 404);
|
|
207
|
+
if (!renderStandup.KEY_RE.test(req.body.key)) return res.fail('render_request_invalid', { status: 400, message: 'That doesn’t look like a Render API key. It starts with “rnd_”.' });
|
|
208
|
+
const commit = String(req.body.commit_id).trim().toLowerCase();
|
|
209
|
+
const expect = req.body.expect_live_commit == null ? null : String(req.body.expect_live_commit).trim().toLowerCase();
|
|
210
|
+
if (!renderPromote.COMMIT_RE.test(commit) || (expect !== null && !renderPromote.COMMIT_RE.test(expect))) {
|
|
211
|
+
return res.fail('promote_request_invalid', { status: 400, message: 'Choose one of the changes waiting on your sandbox.' });
|
|
212
|
+
}
|
|
213
|
+
const why = renderPromote.noPromoteReason(inst);
|
|
214
|
+
if (why) return res.fail('promote_unavailable', { status: 409, message: why });
|
|
215
|
+
if (!api.secretBox.isConfigured()) {
|
|
216
|
+
return res.fail('render_unavailable', { status: 503, message: 'The platform can’t take a key safely right now, so nothing was sent. Try again later.' });
|
|
217
|
+
}
|
|
218
|
+
const r = await renderPromote.enqueuePromote(pool, id, { keyEnc: api.secretBox.encrypt(req.body.key), commitId: commit, expectLiveCommit: expect, requestedBy: req.builder.id });
|
|
219
|
+
if (r.conflict) {
|
|
220
|
+
return res.fail('intent_conflict', { status: 409,
|
|
221
|
+
message: BUSY_KEY_NOT_KEPT,
|
|
222
|
+
details: { action: r.intent.action, state: r.intent.state } });
|
|
223
|
+
}
|
|
224
|
+
await provisioning.recordEvent(pool, {
|
|
225
|
+
instanceId: id, ownerBuilderId: inst.owner_builder_id, event: 'render-promote',
|
|
226
|
+
detail: `promote of ${commit.slice(0, 7)} to the live app requested by owner`, actor: 'api:self',
|
|
227
|
+
}).catch(() => {});
|
|
228
|
+
res.json({ ok: true, queued: true, action: 'render-promote',
|
|
229
|
+
message: 'Queued. That change goes to your live app in a moment; Render then builds it, which usually takes a few minutes. Your key is used once and then thrown away.' });
|
|
230
|
+
} catch (err) {
|
|
231
|
+
// Never log the request: it carries the key.
|
|
232
|
+
log.error('[provisioning] POST /provisioning/instances/:id/render/promote', err && err.message);
|
|
233
|
+
failFrom(res, err, 'render_promote_failed');
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
158
237
|
// POST /provisioning/instances/:id/render — rank: any authenticated builder, and OWNER
|
|
159
238
|
// ONLY: the key is the owner's and so is the account it acts in, so an archon on
|
|
160
239
|
// someone else's project gets the same 404 as a stranger. The reply is a receipt;
|
|
@@ -1579,6 +1579,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1579
1579
|
<h2 id="p1-h"><span class="n">01</span>Your project</h2>
|
|
1580
1580
|
<!-- auto-fill: the answers this account gave on its most recent project -->
|
|
1581
1581
|
<p class="fillOffer" id="physFillOffer" hidden><button type="button" class="linklike" id="physFromAccount">Fill these in from my last project</button></p>
|
|
1582
|
+
<!-- the guide's answers (task 1005274): shown only when this page filled itself in from the plan -->
|
|
1583
|
+
<p class="fillOffer hint" id="guideFill" hidden>Filled in from your talk with the guide. Change anything you like. <a id="guideFileLink" href="#" download="PROJECT-HANDOFF.md">Download the plan for your AI</a></p>
|
|
1582
1584
|
<!-- the demo route (task 1004420, BV2.PS09): opened in place at the top of part
|
|
1583
1585
|
01 by the small link under the heading; the rest of the page is unchanged
|
|
1584
1586
|
and marked "demo" (D9). The fit check is a copy of modules/provisioning/demo.js
|
|
@@ -2311,6 +2313,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2311
2313
|
}
|
|
2312
2314
|
if (app.links) v.root.appendChild(appPara([appLink(app.links.service, 'Open it on Render')]));
|
|
2313
2315
|
sandboxView(data.sandbox, v);
|
|
2316
|
+
promoteView(data.promote, data.sandbox, v);
|
|
2314
2317
|
var vars = (data.env_vars || []).filter(function (x) { return !x.set_for_you; });
|
|
2315
2318
|
if (vars.length) {
|
|
2316
2319
|
v.root.appendChild(appEl('h3', null, 'Still to set on Render'));
|
|
@@ -2378,8 +2381,60 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2378
2381
|
}
|
|
2379
2382
|
v.root.appendChild(box);
|
|
2380
2383
|
}
|
|
2384
|
+
/* ── promote: a person moves what ran on the sandbox to the live app (task 1005290, ADR 0406) ──
|
|
2385
|
+
The look at what is waiting and the press each take the owner's Render key, held in this
|
|
2386
|
+
closure and the field for the two requests and then dropped. The second press is the
|
|
2387
|
+
confirm: it names the change that goes live. Nothing here moves the live app by itself. */
|
|
2388
|
+
function promoteView(p, sb, v) {
|
|
2389
|
+
if (!p || !sb || ['none', 'requested'].indexOf(sb.state) >= 0) return;
|
|
2390
|
+
var box = appEl('div', 'appPromote');
|
|
2391
|
+
box.appendChild(appEl('h3', null, 'Going live'));
|
|
2392
|
+
box.appendChild(appPara([p.live_follows_main
|
|
2393
|
+
? 'Your live app still takes every finished change by itself. It stops once your sandbox is running or you promote for the first time.'
|
|
2394
|
+
: 'Your live app changes only when you promote. Look at your sandbox first, then promote what you are happy with.'], 'finePrint'));
|
|
2395
|
+
var last = p.last;
|
|
2396
|
+
if (last && last.state === 'requested') box.appendChild(appPara(['Promoting ' + (last.commit || 'your change') + ' to your live app…']));
|
|
2397
|
+
else if (last && last.state === 'sent') box.appendChild(appPara(['Sent ' + (last.commit || 'your change') + ' to your live app. Render is building it; that usually takes a few minutes.']));
|
|
2398
|
+
else if (last && last.state === 'failed') box.appendChild(appPara([String(last.error || 'The last promotion did not go through.')], 'err'));
|
|
2399
|
+
if (p.can) {
|
|
2400
|
+
v.prBtn = appEl('button', 'btn ghost small appPrBtn', 'See what is waiting to go live');
|
|
2401
|
+
v.prBtn.type = 'button';
|
|
2402
|
+
box.appendChild(v.prBtn);
|
|
2403
|
+
v.prForm = appEl('div', 'appRetry appPrForm');
|
|
2404
|
+
v.prForm.hidden = true;
|
|
2405
|
+
var lab = appEl('label', 'wizField');
|
|
2406
|
+
lab.appendChild(appEl('span', null, 'Render API key'));
|
|
2407
|
+
v.prKey = appEl('input', 'appRetryKey');
|
|
2408
|
+
v.prKey.type = 'password'; v.prKey.autocomplete = 'off'; v.prKey.spellcheck = false; v.prKey.maxLength = 200; v.prKey.placeholder = 'rnd_…';
|
|
2409
|
+
lab.appendChild(v.prKey);
|
|
2410
|
+
v.prForm.appendChild(lab);
|
|
2411
|
+
v.prLook = appEl('button', 'btn small', 'Look');
|
|
2412
|
+
v.prLook.type = 'button';
|
|
2413
|
+
v.prForm.appendChild(v.prLook);
|
|
2414
|
+
v.prList = appEl('div', 'appPrList');
|
|
2415
|
+
v.prForm.appendChild(v.prList);
|
|
2416
|
+
v.prHint = appEl('div', 'hint appRetryHint', 'Your key is used for this look and the promotion, then thrown away.');
|
|
2417
|
+
v.prHint.setAttribute('role', 'status');
|
|
2418
|
+
v.prForm.appendChild(v.prHint);
|
|
2419
|
+
box.appendChild(v.prForm);
|
|
2420
|
+
} else if (p.reason && sb.state !== 'failed') {
|
|
2421
|
+
box.appendChild(appPara([String(p.reason)], 'finePrint'));
|
|
2422
|
+
}
|
|
2423
|
+
if (p.recent && p.recent.length) {
|
|
2424
|
+
box.appendChild(appEl('h4', null, 'Recent promotions'));
|
|
2425
|
+
var ul = appEl('ul', 'appPrRecent');
|
|
2426
|
+
p.recent.forEach(function (r) {
|
|
2427
|
+
var when = r.at ? new Date(r.at).toLocaleString() : '';
|
|
2428
|
+
var what = r.state === 'failed' ? ' — did not go through' : (r.state === 'started' ? ' — being sent' : '');
|
|
2429
|
+
ul.appendChild(appEl('li', null, when + ': ' + r.commit + (r.message ? ' — ' + r.message : '') + what));
|
|
2430
|
+
});
|
|
2431
|
+
box.appendChild(ul);
|
|
2432
|
+
}
|
|
2433
|
+
v.root.appendChild(box);
|
|
2434
|
+
}
|
|
2435
|
+
function promotePending(data) { var l = data && data.promote && data.promote.last; return !!(l && l.state === 'requested'); }
|
|
2381
2436
|
function sandboxSettled(sb) { return !sb || ['none', 'left', 'removed'].indexOf(sb.state) >= 0 || !!sb.error || sb.stage === 'live' || sb.stage === 'quiet'; }
|
|
2382
|
-
function appTerminal(data) { var a = data && data.app; return (!a || a.state === 'disconnected' || !!a.error || a.stage === 'live') && sandboxSettled(data && data.sandbox); }
|
|
2437
|
+
function appTerminal(data) { var a = data && data.app; return (!a || a.state === 'disconnected' || !!a.error || a.stage === 'live') && sandboxSettled(data && data.sandbox) && !promotePending(data); }
|
|
2383
2438
|
function fetchAppStatus(id) {
|
|
2384
2439
|
return fetch(API + '/provisioning/instances/' + encodeURIComponent(id) + '/render', { credentials: 'same-origin' })
|
|
2385
2440
|
.then(function (res) { return res.ok ? res.json() : null; })
|
|
@@ -2414,6 +2469,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2414
2469
|
function tick() { fetchAppStatus(id).then(paint); }
|
|
2415
2470
|
function wire(v) {
|
|
2416
2471
|
if (v.sbBtn) wireSandbox(v);
|
|
2472
|
+
if (v.prBtn) wirePromote(v);
|
|
2417
2473
|
if (!v.retryBtn) return;
|
|
2418
2474
|
v.retryBtn.addEventListener('click', function () {
|
|
2419
2475
|
v.retryForm.hidden = false; v.retryBtn.hidden = true; v.keyEl.focus();
|
|
@@ -2443,6 +2499,50 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2443
2499
|
});
|
|
2444
2500
|
});
|
|
2445
2501
|
}
|
|
2502
|
+
function wirePromote(v) {
|
|
2503
|
+
var key = '';
|
|
2504
|
+
function hint(t, bad) { v.prHint.textContent = t; v.prHint.className = 'hint appRetryHint' + (bad ? ' err' : ''); }
|
|
2505
|
+
function send(path, body) {
|
|
2506
|
+
return fetch(API + '/provisioning/instances/' + encodeURIComponent(id) + path, {
|
|
2507
|
+
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
|
2508
|
+
}).then(function (res) {
|
|
2509
|
+
return res.json().catch(function () { return {}; }).then(function (d) {
|
|
2510
|
+
if (!res.ok) throw new Error((d.error && d.error.message) || 'That did not go through.');
|
|
2511
|
+
return d;
|
|
2512
|
+
});
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
function drop() { key = ''; v.prKey.value = ''; }
|
|
2516
|
+
v.prBtn.addEventListener('click', function () { v.prForm.hidden = false; v.prBtn.hidden = true; v.prKey.focus(); });
|
|
2517
|
+
v.prLook.addEventListener('click', function () {
|
|
2518
|
+
key = String(v.prKey.value || '').trim();
|
|
2519
|
+
if (!/^rnd_[A-Za-z0-9]{16,128}$/.test(key)) { key = ''; hint('That doesn’t look like a Render API key. It starts with “rnd_”.', true); return; }
|
|
2520
|
+
v.prLook.disabled = true; hint('Asking Render…');
|
|
2521
|
+
send('/render/promote/look', { key: key }).then(function (d) {
|
|
2522
|
+
v.prList.textContent = ''; v.prLook.disabled = false;
|
|
2523
|
+
v.prList.appendChild(appPara([d.live ? 'Live now: ' + d.live.short + (d.live.message ? ' — ' + d.live.message : '') : 'Nothing is live on your app yet.']));
|
|
2524
|
+
if (!d.waiting || !d.waiting.length) { drop(); hint('Nothing new is waiting: your live app already runs what your sandbox runs.'); return; }
|
|
2525
|
+
var ul = appEl('ul', 'appPrWaiting');
|
|
2526
|
+
d.waiting.forEach(function (w) { ul.appendChild(appEl('li', null, w.short + (w.message ? ' — ' + w.message : ''))); });
|
|
2527
|
+
v.prList.appendChild(appPara(['Waiting on your sandbox, newest first:']));
|
|
2528
|
+
v.prList.appendChild(ul);
|
|
2529
|
+
var go = appEl('button', 'btn small appPrGo', 'Promote ' + d.waiting[0].short + ' to my live app');
|
|
2530
|
+
go.type = 'button';
|
|
2531
|
+
v.prList.appendChild(go);
|
|
2532
|
+
hint('Promoting the newest brings everything listed above.');
|
|
2533
|
+
go.addEventListener('click', function () {
|
|
2534
|
+
if (!go.dataset.sure) { go.dataset.sure = '1'; go.textContent = 'Yes, put ' + d.waiting[0].short + ' live'; hint('Press again to confirm. Your live app will change.'); return; }
|
|
2535
|
+
go.disabled = true; hint('Sending…');
|
|
2536
|
+
var body = { key: key, commit_id: d.waiting[0].commit_id };
|
|
2537
|
+
if (d.live && d.live.commit_id) body.expect_live_commit = d.live.commit_id;
|
|
2538
|
+
send('/render/promote', body).then(function (r) {
|
|
2539
|
+
body.key = ''; drop(); hint(r.message || 'Queued.');
|
|
2540
|
+
v.prForm.hidden = true; view = null; clearTimeout(t); ticks = 0; tick();
|
|
2541
|
+
}).catch(function (err) { body.key = ''; drop(); go.disabled = false; hint(err.message + ' Enter your key again to retry.', true); });
|
|
2542
|
+
});
|
|
2543
|
+
}).catch(function (err) { drop(); v.prLook.disabled = false; hint(err.message, true); });
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2446
2546
|
function wireSandbox(v) {
|
|
2447
2547
|
v.sbBtn.addEventListener('click', function () { v.sbForm.hidden = false; v.sbBtn.hidden = true; v.sbKey.focus(); });
|
|
2448
2548
|
v.sbSend.addEventListener('click', function () {
|
|
@@ -5213,6 +5313,77 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5213
5313
|
});
|
|
5214
5314
|
paintTeam(state.teamShape || null);
|
|
5215
5315
|
|
|
5316
|
+
/* ── the guide's answers (task 1005274) ──
|
|
5317
|
+
A founder who talked with the start-a-project guide (modules/front-door-guide) is
|
|
5318
|
+
never asked the same thing twice. This browser holds their chat in localStorage
|
|
5319
|
+
(the landing's guide-scene.js writes it); once they are signed in, the chat moves
|
|
5320
|
+
onto their account (POST …/attach) and its plan fills in what it covers, and ONLY
|
|
5321
|
+
what is still unanswered: the kind, the sentence, the extras. The name stays the
|
|
5322
|
+
founder's (the plan has none). Every field stays the ordinary control it always
|
|
5323
|
+
was, so each answer is editable. The create then says from_guide, and the hub
|
|
5324
|
+
carries the plan's scope answers and the hand-off file into the new project
|
|
5325
|
+
(modules/provisioning/guide-handoff.js). No chat in this browser: nothing is read
|
|
5326
|
+
and nothing changes. */
|
|
5327
|
+
var GUIDE_KEY = 'cb-front-door-guide';
|
|
5328
|
+
var guideAsked = false;
|
|
5329
|
+
function guideChat() {
|
|
5330
|
+
try {
|
|
5331
|
+
if (!(window.__MODULES__ && window.__MODULES__.enabled && window.__MODULES__.enabled['front-door-guide'])) return null;
|
|
5332
|
+
var g = JSON.parse(localStorage.getItem(GUIDE_KEY) || 'null');
|
|
5333
|
+
return g && g.id && g.token ? g : null;
|
|
5334
|
+
} catch (e) { return null; }
|
|
5335
|
+
}
|
|
5336
|
+
/* applyGuidePlan(plan) — fills the unanswered fields from a plan; returns what it filled. */
|
|
5337
|
+
function applyGuidePlan(plan) {
|
|
5338
|
+
var filled = [];
|
|
5339
|
+
if (!plan || typeof plan !== 'object') return filled;
|
|
5340
|
+
if (!state.projType && plan.project_type && root.querySelector('#typeCards .tpill[data-type="' + plan.project_type + '"]')) {
|
|
5341
|
+
state.projType = plan.project_type; filled.push('type');
|
|
5342
|
+
}
|
|
5343
|
+
if (!String(state.description || '').trim() && typeof plan.summary === 'string' && plan.summary.trim()) {
|
|
5344
|
+
state.description = plan.summary.trim().slice(0, 600); filled.push('description');
|
|
5345
|
+
}
|
|
5346
|
+
if (state.modules === null && Array.isArray(plan.modules) && plan.modules.length) {
|
|
5347
|
+
state.modules = plan.modules.map(function (m) { return m && m.key; }).filter(function (k) { return typeof k === 'string' && k; });
|
|
5348
|
+
filled.push('modules');
|
|
5349
|
+
}
|
|
5350
|
+
return filled;
|
|
5351
|
+
}
|
|
5352
|
+
function paintGuideFill() {
|
|
5353
|
+
var note = $('guideFill');
|
|
5354
|
+
if (!note) return;
|
|
5355
|
+
note.hidden = !state.fromGuide;
|
|
5356
|
+
$('guideFileLink').href = API + '/front-door-guide/mine/handoff.md?name=' + encodeURIComponent(String(state.name || '').trim());
|
|
5357
|
+
}
|
|
5358
|
+
function guidePrefill() {
|
|
5359
|
+
if (guideAsked || !me) return;
|
|
5360
|
+
var g = guideChat();
|
|
5361
|
+
if (!g) return;
|
|
5362
|
+
guideAsked = true;
|
|
5363
|
+
fetch(API + '/front-door-guide/conversations/' + encodeURIComponent(g.id) + '/attach', {
|
|
5364
|
+
method: 'POST', credentials: 'same-origin',
|
|
5365
|
+
headers: { 'Content-Type': 'application/json' },
|
|
5366
|
+
body: JSON.stringify({ token: g.token })
|
|
5367
|
+
}).catch(function () { return null; }).then(function () {
|
|
5368
|
+
return fetch(API + '/front-door-guide/mine', { credentials: 'same-origin' });
|
|
5369
|
+
}).then(function (res) {
|
|
5370
|
+
return res && res.ok ? res.json() : null;
|
|
5371
|
+
}).then(function (data) {
|
|
5372
|
+
var plan = data && data.conversation && data.conversation.plan;
|
|
5373
|
+
if (!plan) return;
|
|
5374
|
+
applyGuidePlan(plan);
|
|
5375
|
+
state.fromGuide = true;
|
|
5376
|
+
saveDraft();
|
|
5377
|
+
paintType(state.projType || null);
|
|
5378
|
+
$('f-desc').value = state.description || '';
|
|
5379
|
+
renderModules();
|
|
5380
|
+
paintSetup();
|
|
5381
|
+
paintCreate();
|
|
5382
|
+
paintGuideFill();
|
|
5383
|
+
}).catch(function () { /* the guide is a convenience: the page works without it */ });
|
|
5384
|
+
}
|
|
5385
|
+
paintGuideFill();
|
|
5386
|
+
|
|
5216
5387
|
/* ── the module picker (task 1002339, BV1.R20) ──
|
|
5217
5388
|
ONE public read gives the panel both halves it needs: `optional` is the
|
|
5218
5389
|
catalog it renders, `bundles` is what each project type preselects
|
|
@@ -6933,6 +7104,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6933
7104
|
has commits rejects as unrelated history. state.mode already tracked
|
|
6934
7105
|
greenfield|adopt for the UI — it has to reach the server too. */
|
|
6935
7106
|
onboard_mode: state.mode === 'adopt' ? 'adopt' : 'greenfield',
|
|
7107
|
+
/* the page filled itself in from the guide's plan (task 1005274): the hub
|
|
7108
|
+
carries the scope answers and the hand-off file into the new project */
|
|
7109
|
+
from_guide: state.fromGuide ? true : undefined,
|
|
6936
7110
|
/* the ship row (task 1004629): becomes the new project's own ship mode */
|
|
6937
7111
|
ship_mode: shipModeChoice()
|
|
6938
7112
|
})
|
|
@@ -6963,6 +7137,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6963
7137
|
demo_invites: state.demo && state.demo.invites && state.demo.invites.length ? state.demo.invites.slice() : undefined
|
|
6964
7138
|
}));
|
|
6965
7139
|
sessionStorage.removeItem(DRAFT_KEY);
|
|
7140
|
+
/* the guide's chat has done its job: a second project must not fill in from it */
|
|
7141
|
+
if (state.fromGuide) localStorage.removeItem(GUIDE_KEY);
|
|
6966
7142
|
} catch (e) {}
|
|
6967
7143
|
createBusy = false;
|
|
6968
7144
|
renderDone();
|
|
@@ -8568,6 +8744,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
8568
8744
|
$('f-owner').value = state.owner;
|
|
8569
8745
|
saveDraft();
|
|
8570
8746
|
}
|
|
8747
|
+
guidePrefill();
|
|
8571
8748
|
if (currentView === 'new') enter(false);
|
|
8572
8749
|
}
|
|
8573
8750
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.21",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.25.
|
|
9
|
+
"version": "1.25.21",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.21",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -11858,5 +11858,33 @@
|
|
|
11858
11858
|
"id": "1005115",
|
|
11859
11859
|
"text": "A solo founder's Genesis now moves to the next stage by itself when a stage's work is finished, and every stage leaves a named document (the scope, the strategy...) they can come back and edit any time."
|
|
11860
11860
|
}
|
|
11861
|
+
],
|
|
11862
|
+
"1.25.20": [
|
|
11863
|
+
{
|
|
11864
|
+
"id": "1005112",
|
|
11865
|
+
"text": "While a new project is being set up, the menu now shows only the rooms that are open, a room that just opened is highlighted, a closed room can't be reached by its address, and the later setup stages say what finishing them ea"
|
|
11866
|
+
},
|
|
11867
|
+
{
|
|
11868
|
+
"id": "1005274",
|
|
11869
|
+
"text": "A founder who planned their project with the guide now finds the project form and the first Genesis question already filled in, plus a plan file for their own AI."
|
|
11870
|
+
}
|
|
11871
|
+
],
|
|
11872
|
+
"1.25.21": [
|
|
11873
|
+
{
|
|
11874
|
+
"id": "1004162",
|
|
11875
|
+
"text": "Someone already connected to a hosted project who can't manage it is now told what to do next (apply to join, accept their invite, or ask the owner) instead of \"not found\"; strangers still learn nothing, and private projects s"
|
|
11876
|
+
},
|
|
11877
|
+
{
|
|
11878
|
+
"id": "1005130",
|
|
11879
|
+
"text": "When founding a project, the charter step now tells the founder the project is already a monarchy, what every form of government will ask of them day to day, and plainly when a choice changes nothing."
|
|
11880
|
+
},
|
|
11881
|
+
{
|
|
11882
|
+
"id": "1004161",
|
|
11883
|
+
"text": "A project owner can now give a named teammate access to help run their hosted project (view it, restart it, keep its settings list) and take it back; signing in alone still gives nobody access."
|
|
11884
|
+
},
|
|
11885
|
+
{
|
|
11886
|
+
"id": "1005290",
|
|
11887
|
+
"text": "Finished work now reaches a project's test copy first; the owner sees what is waiting and presses promote to put it on the live site, and every promotion is recorded."
|
|
11888
|
+
}
|
|
11861
11889
|
]
|
|
11862
11890
|
}
|
|
@@ -58,7 +58,17 @@ function formatStage(s) {
|
|
|
58
58
|
// Pure: the charter stage's recommendation from the founder's answers (task 1005119).
|
|
59
59
|
// A recommendation only: choosing is still the founder's.
|
|
60
60
|
function formatAdvice(a) {
|
|
61
|
-
const out = [
|
|
61
|
+
const out = [];
|
|
62
|
+
// task 1005130: what is in force now, and what each form asks of you day to day.
|
|
63
|
+
if (a.in_force) out.push(` In force now: ${a.in_force.line}`);
|
|
64
|
+
if (a.forms) {
|
|
65
|
+
out.push(' The forms you can choose:');
|
|
66
|
+
for (const f of a.forms.filter((x) => x.available)) {
|
|
67
|
+
out.push(` ${f.name}${f.current ? ' (in force now)' : ''}: ${f.who_decides} ${f.to_close}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
out.push(` Recommended government: ${a.charter.name}`, ` Why: ${a.charter.why}`);
|
|
71
|
+
if (a.charter.on_choose) out.push(` ${a.charter.on_choose.line}`);
|
|
62
72
|
if (a.charter.council_rank) out.push(` It is governed by a council rank you create: ${a.charter.council_rank}.`);
|
|
63
73
|
out.push(' It is a recommendation. Any form, or your own settings, is still yours to choose.');
|
|
64
74
|
if (a.preset) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// scripts/bongos/government-walk-sole.js — the government walk's board-of-one rows
|
|
3
|
+
// (task 1005130, part C). Its own file so government-walk.js stays under the size
|
|
4
|
+
// ratchet; the walk calls it with its own helpers, so the rows run through the same
|
|
5
|
+
// real routers and in-memory store as every other row.
|
|
6
|
+
//
|
|
7
|
+
// WHY. A board of one decides by filing: when the sole member of a first_ratifier
|
|
8
|
+
// board files an amendment, their yes is cast in the same call (sole-member.js).
|
|
9
|
+
// These rows prove the other side of that rule through the real routes: a lower rank
|
|
10
|
+
// is refused by the route's own permission gate before anything is filed, and a
|
|
11
|
+
// filing by a builder the board does not seat — a charter change and a rules change —
|
|
12
|
+
// waits for the board with no vote cast.
|
|
13
|
+
|
|
14
|
+
// What a filing decided in the same call, when a board of one decided it.
|
|
15
|
+
const decidedOf = (r) => (r.json && r.json.decided) || {};
|
|
16
|
+
|
|
17
|
+
// The genesis pick was adopted by filing: the item passed, and the vote that passed it
|
|
18
|
+
// is the founder's own, recorded as cast by filing.
|
|
19
|
+
function adoptedByFiling({ store, stateOf, itemId, voterId }) {
|
|
20
|
+
const via = (store.votes.find((v) => v.item_id === String(itemId) && v.voter_id === String(voterId)) || {}).cast_via || 'none';
|
|
21
|
+
return { got: `${stateOf(itemId)}; the founder's yes, cast by ${via === 'filed' ? 'filing' : via}`, pass: stateOf(itemId) === 'passed' && via === 'filed' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function soleMemberRows({ check, call, vote, code, store, stateOf, itemIdOf, ids: { EVE, BEN, FOUNDER_ID }, SHORT_LAW }) {
|
|
25
|
+
await check('A builder without the right to propose is refused before anything is filed', '403; nothing filed', async () => {
|
|
26
|
+
const before = store.items.length;
|
|
27
|
+
const r = await call(EVE, 'POST', '/government/board/laws', { enact: [SHORT_LAW] });
|
|
28
|
+
return { got: `${r.status}; ${store.items.length === before ? 'nothing filed' : 'a sitting was opened'}`, pass: r.status === 403 && store.items.length === before };
|
|
29
|
+
});
|
|
30
|
+
await check('A charter and a rules change filed by a builder the board does not seat wait for the board', 'both sitting open; no vote cast', async () => {
|
|
31
|
+
const c = await call(BEN, 'POST', '/government/board/charter', { form: 'direct-democracy' });
|
|
32
|
+
const d = await call(BEN, 'POST', '/government/board/decision-rules', { rules: [{ area: 'review', decided_by: 'rank:council', rule: 'majority' }] });
|
|
33
|
+
const items = [itemIdOf(c), itemIdOf(d)];
|
|
34
|
+
const open = items.every((id) => id && stateOf(id) === 'open');
|
|
35
|
+
const cast = store.votes.filter((v) => items.includes(v.item_id)).length;
|
|
36
|
+
for (const id of items) if (id) await vote(FOUNDER_ID, id, 'no'); // the board returns both, so nothing is left standing
|
|
37
|
+
return { got: `${open ? 'both sitting open' : `${c.status} ${code(c)} ${stateOf(items[0])}, ${d.status} ${code(d)} ${stateOf(items[1])}`}; ${cast ? `${cast} vote cast` : 'no vote cast'}`, pass: open && cast === 0 };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { decidedOf, adoptedByFiling, soleMemberRows };
|