@bongos/core 1.25.17 → 1.25.19
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 +135 -65
- 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 +17 -1
- package/clients/bongos-client/index.mjs +12 -0
- package/docs/adr/0408-a-genesis-stage-is-three-doors-onto-one-record.md +2 -1
- package/docs/api/openapi.json +288 -6
- package/docs/api-reference.md +14 -4
- package/docs/copy-inventory.md +132 -121
- package/docs/copy-registry.json +347 -195
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +3 -0
- package/docs/page-readings.json +63 -63
- package/modules/app-host/CLAUDE.md +15 -0
- package/modules/app-host/module.json +20 -0
- package/modules/app-host/public/settings-widget.js +143 -0
- package/modules/app-host/routes/app-host.js +88 -0
- package/modules/government/charter-advice.js +103 -0
- package/modules/government/presets.js +42 -0
- package/modules/government/recommendations.js +102 -7
- package/modules/government/routes/government.js +1 -0
- package/modules/hall-ui/public/genesis-home.js +28 -3
- package/modules/lifecycle/first-read-catalog.js +62 -0
- package/modules/lifecycle/first-read-db.js +47 -9
- package/modules/lifecycle/first-read.js +88 -23
- package/modules/lifecycle/migrations/lifecycle_023_repo_map_dismissed.sql +19 -0
- package/modules/lifecycle/routes/first-read.js +27 -5
- package/modules/onboarding/genesis-home.js +68 -7
- package/modules/onboarding/genesis-work.js +169 -4
- package/modules/onboarding/routes/genesis-work.js +5 -2
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/routes/sso-app-status.js +65 -0
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/seams.js +32 -1
- package/modules/provisioning/starter-bundles.js +1 -0
- package/modules/public-landing/public/assets/guide-core.js +475 -0
- package/modules/public-landing/public/assets/guide-hall.css +121 -0
- package/modules/public-landing/public/assets/guide-hall.js +164 -0
- package/modules/public-landing/public/assets/guide-scene.css +9 -0
- package/modules/public-landing/public/assets/guide-scene.js +58 -381
- package/modules/public-landing/public/index.html +46 -1
- package/modules/public-landing/public/projects.html +11 -2
- package/modules/ui-design/kit/serve.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +24 -0
- package/scripts/bongos/first-read.js +45 -11
- package/scripts/bongos/genesis.js +29 -2
- package/scripts/bongos/publish-manifest.js +1 -0
- package/scripts/bongos/seam-wiring-guard.js +3 -2
- package/scripts/hall-preview/server.js +2 -0
- package/src/bongos/auth-admission.js +64 -0
- package/src/bongos/auth.js +2 -1
- package/src/bongos/module-scope-map.js +4 -0
- package/src/bongos/routes/core-update.js +3 -16
- package/src/bongos/routes/modules.js +11 -0
- package/src/bongos/serve-internal.js +20 -6
- package/src/module-api.js +6 -1
- package/src/module-loader/loader.js +2 -2
- package/tests/app_host.mjs +413 -0
- package/tests/first_read.mjs +184 -8
- package/tests/genesis_home.mjs +81 -1
- package/tests/genesis_work.mjs +103 -3
- package/tests/genesis_work_routes.mjs +9 -0
- package/tests/government_charter_advice.mjs +150 -0
- package/tests/landing_guide_hall.mjs +364 -0
- package/tests/landing_guide_scene.mjs +8 -5
- package/tests/module_api.mjs +1 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/npm_release_where.mjs +13 -3
package/tests/genesis_home.mjs
CHANGED
|
@@ -226,7 +226,7 @@ function world({ withGovernment = true } = {}) {
|
|
|
226
226
|
const deps = () => ({
|
|
227
227
|
getSetting: async (k) => (store.has(k) ? { key: k, value: store.get(k) } : null),
|
|
228
228
|
setSetting: async (k, v) => { store.set(k, String(v)); return { key: k, value: String(v) }; },
|
|
229
|
-
lifecycle, government, now,
|
|
229
|
+
lifecycle, government, now, documents: null,
|
|
230
230
|
});
|
|
231
231
|
return {
|
|
232
232
|
store, tasks, board, opened, deps,
|
|
@@ -385,6 +385,67 @@ test('with no Board Room module the founder\'s confirm closes a finished stage',
|
|
|
385
385
|
assert.equal((await gh.readGenesis(w.deps())).stage, 'strategize');
|
|
386
386
|
});
|
|
387
387
|
|
|
388
|
+
test('task 1005115: with no Board Room a finished stage moves on by itself, and the view says so', async () => {
|
|
389
|
+
const w = world({ withGovernment: false });
|
|
390
|
+
await gh.startFounding(1, w.deps());
|
|
391
|
+
await gh.savePlan({}, 1, w.deps());
|
|
392
|
+
let g = await gh.readGenesis(w.deps());
|
|
393
|
+
assert.equal(g.board_room, false);
|
|
394
|
+
assert.equal(g.moved, null, 'nothing has moved past a stage yet');
|
|
395
|
+
w.finish('scope');
|
|
396
|
+
// A ship moves it on; no read and no button needed.
|
|
397
|
+
assert.deepEqual(await gh.onTaskShipped(w.deps()), []);
|
|
398
|
+
assert.equal(w.store.get(fm.KEYS.STAGE), 'strategize', 'the ship that finished scope advanced it');
|
|
399
|
+
g = await gh.readGenesis(w.deps());
|
|
400
|
+
assert.equal(g.stage, 'strategize');
|
|
401
|
+
assert.equal(g.can_close, false, 'there is no close button on this path');
|
|
402
|
+
assert.equal(g.moved.from, 'scope');
|
|
403
|
+
assert.equal(g.moved.to, 'strategize');
|
|
404
|
+
assert.equal(g.moved.how, 'tasks_finished');
|
|
405
|
+
assert.deepEqual(g.moved.opened, fm.WAKES.scope, 'and it names what opened');
|
|
406
|
+
assert.equal(g.stages[0].state, 'done');
|
|
407
|
+
// Every read reconciles too, through several finished stages in order.
|
|
408
|
+
w.finish('strategize'); w.finish('charter');
|
|
409
|
+
g = await gh.readGenesis(w.deps());
|
|
410
|
+
assert.equal(g.stage, 'legalize');
|
|
411
|
+
// Legislation does not pass itself: its finished tasks open the alive door.
|
|
412
|
+
w.finish('legalize'); w.finish('legislation');
|
|
413
|
+
g = await gh.readGenesis(w.deps());
|
|
414
|
+
assert.equal(g.stage, 'legislation');
|
|
415
|
+
assert.equal(g.can_close, false);
|
|
416
|
+
assert.equal(g.can_come_alive, true);
|
|
417
|
+
// The old close call on a stage that already moved answers what happened.
|
|
418
|
+
const again = await gh.closeStage('scope', 1, w.deps());
|
|
419
|
+
assert.equal(again.ok, true);
|
|
420
|
+
assert.equal(again.already, true);
|
|
421
|
+
assert.equal(again.next_stage, 'legislation', 'it names where the project is now, not the stage after scope');
|
|
422
|
+
assert.equal((await gh.comeAlive(1, w.deps())).ok, true);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('task 1005115: with a Board Room nothing moves by itself; the sitting still decides', async () => {
|
|
426
|
+
const w = world();
|
|
427
|
+
await gh.startFounding(1, w.deps());
|
|
428
|
+
await gh.savePlan({}, 1, w.deps());
|
|
429
|
+
w.finish('scope');
|
|
430
|
+
const g = await gh.readGenesis(w.deps());
|
|
431
|
+
assert.equal(g.stage, 'scope');
|
|
432
|
+
assert.equal(g.board_room, true);
|
|
433
|
+
assert.equal(g.can_close, true);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test('task 1005115: each stage on the genesis home carries its document', async () => {
|
|
437
|
+
const w = world({ withGovernment: false });
|
|
438
|
+
await gh.startFounding(1, w.deps());
|
|
439
|
+
await gh.savePlan({}, 1, w.deps());
|
|
440
|
+
const documents = async () => ({ scope: { title: 'The scope', filled: true, updated_at: null, editable: true } });
|
|
441
|
+
let g = await gh.readGenesis({ ...w.deps(), documents });
|
|
442
|
+
assert.equal(g.stages[0].document.title, 'The scope');
|
|
443
|
+
assert.equal(g.stages[1].document, null);
|
|
444
|
+
g = await gh.readGenesis({ ...w.deps(), documents: async () => { throw new Error('db down'); } });
|
|
445
|
+
assert.equal(g.stage, 'scope', 'the home still renders when the documents cannot be read');
|
|
446
|
+
assert.equal(g.stages[0].document, null);
|
|
447
|
+
});
|
|
448
|
+
|
|
388
449
|
test('the view: states, wakes, the life plates and a due date from the plan', () => {
|
|
389
450
|
const mode = { stage: 'strategize', stage_number: 2, stage_count: 5, day: 9 };
|
|
390
451
|
const g = gh.shapeGenesis({
|
|
@@ -605,6 +666,25 @@ test('the stage reads as a checklist: done, open and locked, and a locked row of
|
|
|
605
666
|
assert.doesNotMatch(r304, /data-genesis-copy/);
|
|
606
667
|
});
|
|
607
668
|
|
|
669
|
+
test('task 1005115: with no Board Room the card says the stage moved by itself, and lists what each stage left', async () => {
|
|
670
|
+
const genesis = gh.shapeGenesis({
|
|
671
|
+
mode: { stage: 'strategize', stage_number: 2, stage_count: 5, day: 3 },
|
|
672
|
+
plan: { days: {} },
|
|
673
|
+
tasks: { scope: [{ id: '1', title: 'Write the scope', status: 'shipped' }], strategize: [{ id: '2', title: 'Write the strategy', status: 'ready' }] },
|
|
674
|
+
stageAt: { strategize: '2026-10-08T00:00:00Z' },
|
|
675
|
+
boardRoom: false,
|
|
676
|
+
documents: { scope: { title: 'The scope', filled: true, updated_at: null, editable: true }, strategize: { title: 'The strategy', filled: false } },
|
|
677
|
+
});
|
|
678
|
+
const { host } = bootCard({ founding: true, data: { genesis, viewer_id: '1' } });
|
|
679
|
+
await new Promise((r) => setImmediate(r));
|
|
680
|
+
await new Promise((r) => setImmediate(r));
|
|
681
|
+
assert.match(host.innerHTML, /data-genesis-moved>Scope is done, so this stage opened by itself\. Now open: /);
|
|
682
|
+
assert.match(host.innerHTML, /moves on by itself/);
|
|
683
|
+
assert.doesNotMatch(host.innerHTML, /put this stage to the board/, 'no board to put it to');
|
|
684
|
+
assert.match(host.innerHTML, /<b>The scope<\/b>/);
|
|
685
|
+
assert.doesNotMatch(host.innerHTML, /The strategy<\/b>/, 'an empty document is not listed');
|
|
686
|
+
});
|
|
687
|
+
|
|
608
688
|
test('the hall carries the five life plates byte-identical to the world\'s', () => {
|
|
609
689
|
const world = path.join(ROOT, 'modules', 'public-landing', 'public', 'assets', 'world');
|
|
610
690
|
const hall = path.join(ROOT, 'modules', 'hall-ui', 'public', 'genesis');
|
package/tests/genesis_work.mjs
CHANGED
|
@@ -222,10 +222,81 @@ if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
|
|
222
222
|
|
|
223
223
|
await test('a stage that has not adopted questions still takes a page and a link', async () => {
|
|
224
224
|
const deps = { store: memStore() };
|
|
225
|
-
const r = await gw.setPage('
|
|
225
|
+
const r = await gw.setPage('legalize', 'Credits only to start.', 1, deps);
|
|
226
226
|
assert.equal(r.work.adopted, false);
|
|
227
|
-
assert.equal(r.work.page, '
|
|
228
|
-
assert.equal((await gw.setAnswer('
|
|
227
|
+
assert.equal(r.work.page, 'Credits only to start.');
|
|
228
|
+
assert.equal((await gw.setAnswer('legalize', 'anything', { text: 'x' }, 1, deps)).reason, 'unknown_question');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ── the charter stage (task 1005119) ──────────────────────────────────────
|
|
232
|
+
// A stand-in government port that records what it was handed, so the test sees
|
|
233
|
+
// exactly which answers reach the recommender.
|
|
234
|
+
const fakeGov = () => {
|
|
235
|
+
const calls = [];
|
|
236
|
+
return { calls, async charterAdvice(input) { calls.push(input); return { charter: { key: 'monarchy' }, input }; } };
|
|
237
|
+
};
|
|
238
|
+
const charterDeps = (extra = {}) => ({ store: memStore(), government: fakeGov(), getSetting: async () => null, ...extra });
|
|
239
|
+
|
|
240
|
+
await test('charter: a closed answer must be one of its choices, and a label reads back as its key', async () => {
|
|
241
|
+
const deps = charterDeps();
|
|
242
|
+
const bad = await gw.setAnswer('charter', 'final_say', { text: 'my cat' }, 1, deps);
|
|
243
|
+
assert.equal(bad.reason, 'not_a_choice');
|
|
244
|
+
assert.deepEqual(bad.expected, ['founder', 'trusted', 'everyone', 'council']);
|
|
245
|
+
const r = await gw.setAnswer('charter', 'final_say', { text: 'Everyone on the project' }, 1, deps);
|
|
246
|
+
const q = qOf(r, 'final_say');
|
|
247
|
+
assert.equal(q.answer.text, 'everyone', 'stored as the key');
|
|
248
|
+
assert.equal(q.answer.choice_label, 'Everyone on the project');
|
|
249
|
+
assert.ok(q.choices.some((c) => c.key === 'council'), 'the choices ride the view');
|
|
250
|
+
// prose questions take any words
|
|
251
|
+
const p = await gw.setAnswer('charter', 'feel', { text: 'Calm and careful.' }, 1, deps);
|
|
252
|
+
assert.equal(qOf(p, 'feel').answer.text, 'Calm and careful.');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await test('charter: every question is skippable and answerable "not sure yet"', async () => {
|
|
256
|
+
const deps = charterDeps();
|
|
257
|
+
for (const q of gw.QUESTIONS.charter) {
|
|
258
|
+
assert.equal((await gw.setAnswer('charter', q.id, { state: 'not_sure' }, 1, deps)).ok, true, q.id);
|
|
259
|
+
assert.equal((await gw.setAnswer('charter', q.id, { state: 'skipped' }, 1, deps)).ok, true, q.id);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await test('charter: only given answers reach the recommender, closed as keys and prose as words', async () => {
|
|
264
|
+
const gov = fakeGov();
|
|
265
|
+
const deps = charterDeps({ government: gov, getSetting: async () => ({ value: JSON.stringify({ expected_builders: 6 }) }) });
|
|
266
|
+
await gw.setAnswer('charter', 'final_say', { text: 'trusted' }, 1, deps);
|
|
267
|
+
await gw.setAnswer('charter', 'agreement', { state: 'not_sure' }, 1, deps);
|
|
268
|
+
await gw.setAnswer('charter', 'speed', { state: 'skipped' }, 1, deps);
|
|
269
|
+
await gw.setAnswer('charter', 'leave_behaviour', { text: 'Lying about shipped work.' }, 1, deps);
|
|
270
|
+
const r = await gw.readStage('charter', deps);
|
|
271
|
+
const last = gov.calls[gov.calls.length - 1];
|
|
272
|
+
assert.deepEqual(last.answers, { final_say: 'trusted' }, 'not sure yet and skipped are not sent');
|
|
273
|
+
assert.deepEqual(last.prose, { leave_behaviour: 'Lying about shipped work.' });
|
|
274
|
+
assert.equal(last.headcount, 6, "the founder's expected headcount from the plan");
|
|
275
|
+
assert.equal(r.work.advice.charter.key, 'monarchy', 'the advice rides the stage');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await test('charter: no government module, or a failing one, still reads the stage (advice null)', async () => {
|
|
279
|
+
const none = await gw.readStage('charter', charterDeps({ government: null }));
|
|
280
|
+
assert.equal(none.ok, true);
|
|
281
|
+
assert.equal(none.work.advice, null);
|
|
282
|
+
const broken = await gw.readStage('charter', charterDeps({ government: { charterAdvice: async () => { throw new Error('db down'); } } }));
|
|
283
|
+
assert.equal(broken.work.advice, null);
|
|
284
|
+
const other = await gw.readStage('strategize', charterDeps());
|
|
285
|
+
assert.equal(other.work.advice, undefined, 'only the charter stage carries advice');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await test('charter: the assisted brief lists the keys to record for each closed question', async () => {
|
|
289
|
+
const p = gw.assistPrompt('charter');
|
|
290
|
+
assert.match(p, /\[final_say\]/);
|
|
291
|
+
assert.match(p, /Record one of: founder \(Me, the founder\) · trusted/);
|
|
292
|
+
assert.match(p, /\[leave_behaviour\]/);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
await test('charter: the planning read path shows a closed answer in words, not its key', async () => {
|
|
296
|
+
const deps = charterDeps();
|
|
297
|
+
await gw.setAnswer('charter', 'agreement', { text: 'consent' }, 1, deps);
|
|
298
|
+
const { markdown } = await gw.readContext(deps);
|
|
299
|
+
assert.match(markdown, /How much agreement should a decision need\?\*\*\n It passes unless someone objects/);
|
|
229
300
|
});
|
|
230
301
|
|
|
231
302
|
await test('an empty project gives the planning read path nothing to print', async () => {
|
|
@@ -299,6 +370,35 @@ if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
|
|
|
299
370
|
assert.match(out, /\(empty\)/);
|
|
300
371
|
});
|
|
301
372
|
|
|
373
|
+
await test('task 1005115: a stage\'s document is its record read as one named, editable page', async () => {
|
|
374
|
+
const deps = { store: memStore() };
|
|
375
|
+
let d = (await gw.readDocument('strategize', deps)).document;
|
|
376
|
+
assert.equal(d.title, 'The strategy');
|
|
377
|
+
assert.equal(d.filled, false, 'nothing written yet: an empty document, not a heading');
|
|
378
|
+
assert.equal(d.markdown, '');
|
|
379
|
+
await gw.setAnswer('strategize', 'big_pieces', { text: 'Ledger, then sync' }, 1, deps);
|
|
380
|
+
await gw.setAnswer('strategize', 'risks', { state: 'not_sure', text: 'bank API' }, 1, deps);
|
|
381
|
+
await gw.setPage('strategize', 'We start small.', 1, deps);
|
|
382
|
+
d = (await gw.readDocument('strategize', deps)).document;
|
|
383
|
+
assert.equal(d.filled, true);
|
|
384
|
+
assert.match(d.markdown, /^# The strategy\n/);
|
|
385
|
+
assert.match(d.markdown, /Ledger, then sync/);
|
|
386
|
+
assert.match(d.markdown, /Not sure yet — bank API/);
|
|
387
|
+
assert.match(d.markdown, /We start small\./);
|
|
388
|
+
assert.equal(d.editable, true);
|
|
389
|
+
assert.equal(d.edit.page, 'PUT /api/bongos/founding/work/strategize/page');
|
|
390
|
+
assert.ok(d.updated_at, 'the document says when it last changed');
|
|
391
|
+
// Editing it is editing the record: the same write, seen at once in the document.
|
|
392
|
+
await gw.setAnswer('strategize', 'big_pieces', { text: 'Sync first' }, 1, deps);
|
|
393
|
+
assert.match((await gw.readDocument('strategize', deps)).document.markdown, /Sync first/);
|
|
394
|
+
assert.equal((await gw.readDocument('nope', deps)).reason, 'unknown_stage');
|
|
395
|
+
const all = await gw.readDocuments(deps);
|
|
396
|
+
assert.deepEqual(Object.keys(all), ['scope', 'strategize', 'charter', 'legalize', 'legislation']);
|
|
397
|
+
assert.equal(all.strategize.filled, true);
|
|
398
|
+
assert.equal(all.scope.filled, false);
|
|
399
|
+
assert.equal(all.charter.title, 'The charter');
|
|
400
|
+
});
|
|
401
|
+
|
|
302
402
|
console.log(`\ngenesis_work: ${passed} passed, ${failed} failed`);
|
|
303
403
|
if (failed) process.exit(1);
|
|
304
404
|
}
|
|
@@ -67,6 +67,15 @@ try {
|
|
|
67
67
|
assert.equal(r.data.work.questions.find((q) => q.id === 'big_pieces').answer.text, 'Ledger first.');
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
+
await test('task 1005115: any builder reads a stage\'s document; an unknown stage is 404', async () => {
|
|
71
|
+
assert.equal((await call('GET', '/founding/work/strategize/document', { as: null })).status, 401);
|
|
72
|
+
const r = await call('GET', '/founding/work/strategize/document', { as: OTHER });
|
|
73
|
+
assert.equal(r.status, 200);
|
|
74
|
+
assert.equal(r.data.document.title, 'The strategy');
|
|
75
|
+
assert.match(r.data.document.markdown, /Ledger first\./, 'the answer written above is in the document');
|
|
76
|
+
assert.equal((await call('GET', '/founding/work/nope/document')).status, 404);
|
|
77
|
+
});
|
|
78
|
+
|
|
70
79
|
await test('a builder who is not the owner cannot write', async () => {
|
|
71
80
|
for (const [m, p, body] of [
|
|
72
81
|
['PUT', '/founding/work/strategize/answers/risks', { text: 'x' }],
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// tests/government_charter_advice.mjs — the genesis charter stage's recommendation
|
|
2
|
+
// from the founder's own answers (task 1005119).
|
|
3
|
+
//
|
|
4
|
+
// The rules held here:
|
|
5
|
+
// • ONE recommender: the answers widen recommendedCharter; with none it answers
|
|
6
|
+
// exactly as the size ladder always has, for every type and size;
|
|
7
|
+
// • every recommendation is an adoptable form, says why in the founder's answers,
|
|
8
|
+
// and a Foundation still brings its council rank;
|
|
9
|
+
// • the principles point at a strictness preset (or none), and saying what a preset
|
|
10
|
+
// does is part of the answer;
|
|
11
|
+
// • the drafted bylaws list the recommended preset's own laws word for word, so the
|
|
12
|
+
// prose and the law book agree, and say plainly when no laws are switched on;
|
|
13
|
+
// • the closed answers' keys are the genesis questions' choices (two modules, one
|
|
14
|
+
// vocabulary, held equal here because neither may import the other).
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import { test } from 'node:test';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const reco = require(path.join(ROOT, 'modules', 'government', 'recommendations.js'));
|
|
24
|
+
const presets = require(path.join(ROOT, 'modules', 'government', 'presets.js'));
|
|
25
|
+
const charter = require(path.join(ROOT, 'modules', 'government', 'charter.js'));
|
|
26
|
+
const advice = require(path.join(ROOT, 'modules', 'government', 'charter-advice.js'));
|
|
27
|
+
const gw = require(path.join(ROOT, 'modules', 'onboarding', 'genesis-work.js'));
|
|
28
|
+
|
|
29
|
+
const TYPES = [...reco.PROJECT_TYPES.map((t) => t.key), undefined];
|
|
30
|
+
const SIZES = [0, 1, 2, 3, 8, 9, 49, 50, 149, 150, 1000];
|
|
31
|
+
|
|
32
|
+
test('the answer keys are exactly the charter questions\' choices', () => {
|
|
33
|
+
const closed = Object.fromEntries(gw.QUESTIONS.charter.filter((q) => q.choices).map((q) => [q.id, q.choices.map((c) => c.key)]));
|
|
34
|
+
const expected = { ...reco.CHARTER_ANSWERS, ...presets.PRINCIPLE_ANSWERS };
|
|
35
|
+
assert.deepEqual(Object.keys(closed).sort(), Object.keys(expected).sort());
|
|
36
|
+
for (const [id, keys] of Object.entries(expected)) assert.deepEqual(closed[id], [...keys], id);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('with no answers (or only "not sure"), the ladder answers exactly as before', () => {
|
|
40
|
+
for (const type of TYPES) {
|
|
41
|
+
for (const n of SIZES) {
|
|
42
|
+
const plain = reco.recommendedCharter(n, type);
|
|
43
|
+
for (const a of [undefined, {}, { final_say: 'maybe', agreement: null }]) {
|
|
44
|
+
const r = reco.recommendedCharter(n, type, a);
|
|
45
|
+
assert.equal(r.key, plain.key);
|
|
46
|
+
assert.equal(r.why, plain.why);
|
|
47
|
+
assert.equal(r.from_answers, false);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('the founder\'s final say picks the form, and why quotes them', () => {
|
|
54
|
+
const r = reco.recommendedCharter(1, 'game', { final_say: 'everyone' });
|
|
55
|
+
assert.equal(r.key, 'direct-democracy', 'a solo founder who wants everyone deciding is not told monarchy');
|
|
56
|
+
assert.match(r.why, /You said everyone on the project should have the final say/);
|
|
57
|
+
assert.equal(r.from_answers, true);
|
|
58
|
+
const council = reco.recommendedCharter(5, 'research', { final_say: 'council' });
|
|
59
|
+
assert.equal(council.key, 'foundation');
|
|
60
|
+
assert.equal(council.council_rank, 'steering-committee', 'a Foundation still brings its council');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('the other answers move the franchise when the final say is not given', () => {
|
|
64
|
+
assert.equal(reco.recommendedCharter(5, null, { answers_to: 'yes' }).key, 'foundation');
|
|
65
|
+
const back = reco.recommendedCharter(1, null, { step_back: 'yes' });
|
|
66
|
+
assert.equal(back.key, 'timocratic-republic');
|
|
67
|
+
assert.match(back.why, /step back/);
|
|
68
|
+
assert.equal(reco.recommendedCharter(5, null, { strangers: 'yes' }).key, 'timocratic-republic');
|
|
69
|
+
const fast = reco.recommendedCharter(5, null, { speed: 'fast' });
|
|
70
|
+
assert.equal(fast.key, 'direct-democracy', 'speed alone keeps the ladder\'s form');
|
|
71
|
+
assert.match(fast.why, /Who votes follows your size/, 'the part the answers did not decide says the ladder decided it');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('how much agreement tunes the pass rule and says where it is set', () => {
|
|
75
|
+
const r = reco.recommendedCharter(20, null, { agreement: 'majority' });
|
|
76
|
+
assert.equal(r.key, 'timocratic-republic');
|
|
77
|
+
assert.equal(r.pass_rule, 'majority');
|
|
78
|
+
assert.equal(r.pass_rule_tuned, true);
|
|
79
|
+
assert.match(r.why, /legislation stage/);
|
|
80
|
+
const solo = reco.recommendedCharter(1, null, { final_say: 'founder', agreement: 'unanimous' });
|
|
81
|
+
assert.equal(solo.pass_rule, 'first_ratifier', 'a board of one is decided by its one yes');
|
|
82
|
+
assert.match(solo.why, /only voter/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('every answer combination recommends an adoptable form whose settings build a proposal', () => {
|
|
86
|
+
const A = reco.CHARTER_ANSWERS;
|
|
87
|
+
const available = charter.FORMS.filter((f) => f.available).map((f) => f.key);
|
|
88
|
+
const live = { membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true };
|
|
89
|
+
for (const fs of [undefined, ...A.final_say]) for (const ag of [undefined, ...A.agreement]) for (const yn of [undefined, 'yes', 'no']) {
|
|
90
|
+
const r = reco.recommendedCharter(5, 'business', { final_say: fs, agreement: ag, strangers: yn, answers_to: yn, step_back: yn });
|
|
91
|
+
assert.ok(available.includes(r.key), r.key);
|
|
92
|
+
assert.ok(typeof r.why === 'string' && r.why.length > 10);
|
|
93
|
+
const built = charter.buildCharterProposal(live, { franchise: r.franchise, passRule: r.pass_rule, councilRank: r.council_rank });
|
|
94
|
+
assert.equal(built.ok, true, JSON.stringify(r));
|
|
95
|
+
assert.equal(charter.readCharter(built.proposal).franchise, r.franchise);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('principles point at a preset, or at none', () => {
|
|
100
|
+
assert.equal(presets.recommendedPreset({}), null, 'no preset is a fine answer');
|
|
101
|
+
assert.equal(presets.recommendedPreset({ trust_level: 'a_lot' }).key, 'open');
|
|
102
|
+
assert.equal(presets.recommendedPreset({ trust_level: 'some' }).key, 'guild');
|
|
103
|
+
assert.equal(presets.recommendedPreset({ trust_level: 'little' }).key, 'factory');
|
|
104
|
+
assert.equal(presets.recommendedPreset({ trust_level: 'a_lot', catch_or_fix: 'catch_early' }).key, 'guild');
|
|
105
|
+
assert.equal(presets.recommendedPreset({ trust_level: 'little', catch_or_fix: 'move_fast' }).key, 'guild');
|
|
106
|
+
const p = presets.recommendedPreset({ trust_level: 'some' });
|
|
107
|
+
assert.match(p.why, /You said/);
|
|
108
|
+
assert.match(p.is, /not a style guide/);
|
|
109
|
+
assert.ok(p.laws.length > 0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('the draft names the recommended preset\'s own laws word for word', () => {
|
|
113
|
+
const a = advice.buildAdvice({
|
|
114
|
+
answers: { final_say: 'trusted', trust_level: 'little' },
|
|
115
|
+
prose: { feel: 'Calm and careful.', leave_behaviour: 'Lying about shipped work.', write_down: 'We never ship on Fridays.' },
|
|
116
|
+
headcount: 12,
|
|
117
|
+
});
|
|
118
|
+
assert.equal(a.charter.key, 'timocratic-republic');
|
|
119
|
+
assert.equal(a.preset.key, 'factory');
|
|
120
|
+
const factory = presets.PRESETS.find((p) => p.key === 'factory');
|
|
121
|
+
for (const law of factory.laws) assert.ok(a.draft_md.includes(`**${law.text}.** ${law.effect_on_ai}`), law.text);
|
|
122
|
+
assert.ok(a.preset.locks.some((l) => l.setting === 'wandering'), 'what it would lock is said');
|
|
123
|
+
assert.match(a.draft_md, /Timocratic Republic/);
|
|
124
|
+
assert.match(a.draft_md, /Calm and careful\./);
|
|
125
|
+
assert.match(a.draft_md, /Lying about shipped work\./);
|
|
126
|
+
assert.match(a.draft_md, /We never ship on Fridays\./);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('with no preset the draft says no laws are switched on', () => {
|
|
130
|
+
const a = advice.buildAdvice({ answers: {}, headcount: 1 });
|
|
131
|
+
assert.equal(a.preset, null);
|
|
132
|
+
assert.match(a.draft_md, /No laws are switched on/);
|
|
133
|
+
assert.equal(a.charter.key, 'monarchy');
|
|
134
|
+
assert.doesNotMatch(a.draft_md, /Code of conduct/, 'no section for words the founder has not written');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('the port entry reads the head count and the law book', async () => {
|
|
138
|
+
const calls = [];
|
|
139
|
+
const db = {
|
|
140
|
+
countActiveBuilders: async () => { calls.push('count'); return 40; },
|
|
141
|
+
listLaws: async () => { calls.push('laws'); return []; },
|
|
142
|
+
};
|
|
143
|
+
const live = await advice.charterAdvice({ answers: {}, prose: { feel: ' ' } }, { db });
|
|
144
|
+
assert.equal(live.headcount, 40);
|
|
145
|
+
assert.equal(live.charter.key, 'timocratic-republic');
|
|
146
|
+
assert.ok(calls.includes('laws'));
|
|
147
|
+
const expected = await advice.charterAdvice({ answers: {}, headcount: 2 }, { db });
|
|
148
|
+
assert.equal(expected.headcount, 2, 'the founder\'s expected number wins over the live count');
|
|
149
|
+
assert.equal(expected.charter.key, 'monarchy');
|
|
150
|
+
});
|