@bongos/core 1.20.83 → 1.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/.bongos-core.json +186 -81
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +4 -0
  4. package/clients/bongos-client/index.cjs +4 -0
  5. package/clients/bongos-client/index.d.ts +7 -0
  6. package/clients/bongos-client/index.mjs +4 -0
  7. package/docs/adr/0051-full-session-transcript-corpus.md +3 -1
  8. package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +3 -3
  9. package/docs/adr/README.md +1 -1
  10. package/docs/api/openapi.json +153 -4
  11. package/docs/api-reference.md +5 -3
  12. package/docs/architecture.md +3 -0
  13. package/docs/copy-inventory.md +60 -59
  14. package/docs/copy-registry.json +72 -63
  15. package/docs/design/project-startup-direction.md +63 -0
  16. package/docs/file-map.md +2 -0
  17. package/docs/module-api-changelog.md +8 -0
  18. package/docs/page-inventory.json +9 -3
  19. package/docs/page-readings.json +427 -410
  20. package/docs/recipes/private-npm-distribution.md +1 -1
  21. package/migrations/core_269_session_transcripts.sql +35 -0
  22. package/migrations/core_270_craft_achievements.sql +128 -0
  23. package/modules/economy/achievements.js +199 -4
  24. package/modules/economy/credits.js +16 -0
  25. package/modules/economy/reward.js +3 -0
  26. package/modules/hall-ui/public/builders.js +1 -1
  27. package/modules/hall-ui/public/genesis-home.js +17 -3
  28. package/modules/hall-ui/public/hall-render.js +24 -2
  29. package/modules/hall-ui/public/index-genesis-demo.states.json +13 -0
  30. package/modules/hall-ui/public/roster.css +21 -0
  31. package/modules/hall-ui/public/roster.html +3 -0
  32. package/modules/hall-ui/public/roster.js +121 -21
  33. package/modules/hall-ui/public/roster.states.json +26 -1
  34. package/modules/hall-ui/records/builders-roster.md +20 -0
  35. package/modules/lifecycle/publish-reconciler.js +25 -0
  36. package/modules/lifecycle/workflow-dispatch.js +21 -11
  37. package/modules/npm-release/release.js +10 -1
  38. package/modules/onboarding/founding-birth.js +116 -0
  39. package/modules/onboarding/genesis-home.js +72 -5
  40. package/modules/onboarding/port.js +9 -2
  41. package/modules/onboarding/routes/onboarding.js +8 -1
  42. package/modules/provisioning/birth.js +64 -0
  43. package/modules/provisioning/dns-resolves.js +54 -0
  44. package/modules/provisioning/migrations/provisioning_036_dns_resolved.sql +30 -0
  45. package/modules/provisioning/pollers/liveness-sweep.js +5 -0
  46. package/modules/provisioning/provisioning.js +7 -6
  47. package/modules/provisioning/routes/provisioning.js +6 -2
  48. package/modules/provisioning/tests/liveness.mjs +48 -0
  49. package/modules/public-landing/public/projects-dns-pending.states.json +85 -0
  50. package/modules/public-landing/public/projects.html +40 -15
  51. package/modules/sessions/db.js +58 -0
  52. package/modules/sessions/routes/sessions.js +97 -0
  53. package/modules/status-ui/public/status.js +39 -7
  54. package/modules/status-ui/public/style.css +24 -0
  55. package/modules/ui-design/kit/fixtures/founding-genesis-demo.json +176 -0
  56. package/modules/ui-design/kit/fixtures/me-founding-demo.json +27 -0
  57. package/modules/ui-design/kit/fixtures/provisioning-instances-demo.json +3 -0
  58. package/modules/ui-design/kit/fixtures/provisioning-instances-dns-pending.json +49 -0
  59. package/modules/ui-design/kit/fixtures/provisioning-instances.json +3 -0
  60. package/package-lock.json +2 -2
  61. package/package.json +1 -1
  62. package/release-notes.json +45 -0
  63. package/scripts/gds/provision.js +77 -53
  64. package/scripts/gds/session-digest.js +11 -3
  65. package/scripts/gds/session-transcript-build.js +155 -0
  66. package/scripts/gds/session-transcript-upload.js +29 -0
  67. package/scripts/gds/ship-session-upload.js +6 -1
  68. package/scripts/gds/upgrade-commit-identity.js +32 -0
  69. package/scripts/gds/upgrade.js +1 -1
  70. package/scripts/hall-preview/refresh-fixtures.js +2 -0
  71. package/src/bongos/db.js +13 -0
  72. package/src/bongos/role-stats.js +36 -2
  73. package/src/bongos/routes/public.js +13 -2
  74. package/src/bongos/routes.js +3 -0
  75. package/src/branding.js +9 -0
  76. package/src/module-api.js +1 -1
  77. package/tests/achievements_reconcile.mjs +13 -0
  78. package/tests/craft_achievements.mjs +349 -0
  79. package/tests/founding_birth.mjs +200 -0
  80. package/tests/founding_builders.mjs +2 -1
  81. package/tests/founding_mode.mjs +3 -1
  82. package/tests/genesis_home.mjs +71 -2
  83. package/tests/hall_builders_page_pass.mjs +2 -1
  84. package/tests/hub_map_orb_states.mjs +5 -1
  85. package/tests/leaderboard_roles.mjs +272 -0
  86. package/tests/npm_release_release.mjs +37 -2
  87. package/tests/projects_hub.mjs +5 -3
  88. package/tests/projects_hub_dns_ready.mjs +153 -0
  89. package/tests/projects_hub_pre_uat.mjs +9 -5
  90. package/tests/provision_dns_first.mjs +171 -0
  91. package/tests/provision_settings_apply.mjs +6 -1
  92. package/tests/provisioning_settings_env.mjs +49 -0
  93. package/tests/session_transcript.mjs +273 -0
  94. package/tests/ui_design_kit.mjs +1 -1
  95. package/tests/upgrade_commit_identity.mjs +89 -0
  96. package/tests/wizard_demo.mjs +29 -3
@@ -0,0 +1,89 @@
1
+ // tests/upgrade_commit_identity.mjs
2
+ //
3
+ // task 1004514: a landed core move left its pin STAGED but uncommitted whenever the project's
4
+ // git named nobody (no user.name/user.email in the repo or for the account the move runs as).
5
+ // `git commit` refused, persistPin reported "could not commit", the move still landed, and the
6
+ // NEXT move read those leftovers as a dirty tree. Seen live on cloudbongos.com, 2026-10-01.
7
+ //
8
+ // These run persistPin against a REAL repo with every identity source switched off (global
9
+ // config pointed at /dev/null, system config disabled, invented identities refused), so the refusal is git's own. The first
10
+ // test proves the environment really does refuse; without it the fix test could pass vacuously.
11
+ //
12
+ // Run: node --test tests/upgrade_commit_identity.mjs
13
+
14
+ import { test, before, after } from 'node:test';
15
+ import assert from 'node:assert/strict';
16
+ import { createRequire } from 'node:module';
17
+ import { spawnSync } from 'node:child_process';
18
+ import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
19
+ import { tmpdir } from 'node:os';
20
+ import { join } from 'node:path';
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const { persistPin } = require('../scripts/gds/upgrade.js');
24
+ const { commitIdentityEnv } = require('../scripts/gds/upgrade-commit-identity.js');
25
+
26
+ // user.useConfigOnly: git on a Mac invents an identity from the hostname, where the box's git
27
+ // refuses; this makes every machine refuse the way the box did.
28
+ const ISOLATE = { GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_NOSYSTEM: '1',
29
+ GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: 'user.useConfigOnly', GIT_CONFIG_VALUE_0: 'true' };
30
+ const IDENTITY_VARS = ['GIT_AUTHOR_NAME', 'GIT_AUTHOR_EMAIL', 'GIT_COMMITTER_NAME', 'GIT_COMMITTER_EMAIL', 'EMAIL'];
31
+ let saved;
32
+ const dirs = [];
33
+
34
+ before(() => {
35
+ saved = Object.fromEntries([...Object.keys(ISOLATE), ...IDENTITY_VARS].map((k) => [k, process.env[k]]));
36
+ Object.assign(process.env, ISOLATE);
37
+ for (const k of IDENTITY_VARS) delete process.env[k];
38
+ });
39
+ after(() => {
40
+ for (const [k, v] of Object.entries(saved)) { if (v === undefined) delete process.env[k]; else process.env[k] = v; }
41
+ for (const d of dirs) rmSync(d, { recursive: true, force: true });
42
+ });
43
+
44
+ const git = (cwd, ...args) => spawnSync('git', args, { cwd, encoding: 'utf8' });
45
+
46
+ // A repo whose first commit carries a one-off identity, then names nobody — the live shape.
47
+ function repoWithPinChange({ identity = null } = {}) {
48
+ const dir = mkdtempSync(join(tmpdir(), 'pin-identity-'));
49
+ dirs.push(dir);
50
+ git(dir, 'init', '-q', '-b', 'main');
51
+ if (identity) { git(dir, 'config', 'user.name', identity.name); git(dir, 'config', 'user.email', identity.email); }
52
+ writeFileSync(join(dir, 'package.json'), '{"dependencies":{"@bongos/core":"1.20.77"}}\n');
53
+ writeFileSync(join(dir, 'package-lock.json'), '{}\n');
54
+ git(dir, '-c', 'user.name=scaffold', '-c', 'user.email=scaffold@example.invalid', 'add', '-A');
55
+ git(dir, '-c', 'user.name=scaffold', '-c', 'user.email=scaffold@example.invalid', 'commit', '-qm', 'scaffold');
56
+ writeFileSync(join(dir, 'package.json'), '{"dependencies":{"@bongos/core":"1.20.78"}}\n');
57
+ return dir;
58
+ }
59
+
60
+ const quiet = () => {};
61
+ const pin = (dir) => persistPin({ instanceDir: dir, fromVersion: '1.20.77', toVersion: '1.20.78', commitPin: true, push: false }, spawnSync, quiet, quiet);
62
+
63
+ test('the environment really names nobody: a plain commit is refused', () => {
64
+ const dir = repoWithPinChange();
65
+ git(dir, 'add', 'package.json');
66
+ const r = git(dir, 'commit', '-m', 'probe');
67
+ assert.notEqual(r.status, 0, 'git must refuse here, or the next test proves nothing');
68
+ });
69
+
70
+ test('a repo that names nobody still gets its pin committed, signed as the provisioner', () => {
71
+ const dir = repoWithPinChange();
72
+ const r = pin(dir);
73
+ assert.equal(r.committed, true, `persistPin: ${JSON.stringify(r)}`);
74
+ assert.equal(git(dir, 'status', '--porcelain').stdout, '', 'nothing staged or left behind for the next move');
75
+ const who = git(dir, 'log', '-1', '--format=%an <%ae>|%s').stdout.trim();
76
+ assert.match(who, /^Cloud Bongos <bot@[^>]+>\|pin @bongos\/core 1\.20\.78 \(was 1\.20\.77\)$/);
77
+ });
78
+
79
+ test('a repo that names someone keeps signing as them', () => {
80
+ const dir = repoWithPinChange({ identity: { name: 'Owner Person', email: 'owner@example.invalid' } });
81
+ assert.equal(commitIdentityEnv(dir, spawnSync), null, 'no fallback when git already has an answer');
82
+ assert.equal(pin(dir).committed, true);
83
+ assert.equal(git(dir, 'log', '-1', '--format=%an <%ae>').stdout.trim(), 'Owner Person <owner@example.invalid>');
84
+ });
85
+
86
+ test('an identity the caller already put in the env is not replaced', () => {
87
+ const dir = repoWithPinChange();
88
+ assert.equal(commitIdentityEnv(dir, spawnSync, { ...process.env, GIT_AUTHOR_EMAIL: 'ci@example.invalid' }), null);
89
+ });
@@ -111,14 +111,21 @@ let created = null;
111
111
  let started = null;
112
112
  provisioning.getInstanceBySlug = async () => null;
113
113
  provisioning.getInstanceByDomain = async () => null;
114
+ // The order the create does things in (task 1004506): the birth stamp and the demo
115
+ // marker must both be on the row BEFORE the provision intent exists, so the runner's
116
+ // web.env carries them. 'old-one' stands for a project that already existed.
117
+ const order = [];
118
+ const birth = require('../modules/provisioning/birth.js');
119
+ birth.stampBirth = async (_db, id) => { order.push(`birth:${id}`); };
114
120
  provisioning.createInstance = async (_db, args) => {
115
121
  created = args;
116
- return { instance: { id: 41, owner_builder_id: 7, slug: args.slug, status: 'requested', hosting_shape: 'byo-host', detail: args.detail || {} }, created: true };
122
+ const fresh = args.slug !== 'old-one';
123
+ return { instance: { id: 41, owner_builder_id: 7, slug: args.slug, status: fresh ? 'requested' : 'active', hosting_shape: 'byo-host', detail: args.detail || {} }, created: fresh };
117
124
  };
118
- provisioning.enqueueIntent = async (_db, _id, action) => ({ intent: { id: 9, action, state: 'pending' }, created: true });
125
+ provisioning.enqueueIntent = async (_db, _id, action) => { order.push(`enqueue:${action}`); return { intent: { id: 9, action, state: 'pending' }, created: true }; };
119
126
  provisioning.getOpenIntent = async () => null;
120
127
  provisioning.recordEvent = async () => {};
121
- demo.startDemoProject = async (_db, _p, id, d) => { started = { id, d }; return null; };
128
+ demo.startDemoProject = async (_db, _p, id, d) => { started = { id, d }; order.push(`demo:${id}`); return null; };
122
129
  api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner' }; next(); };
123
130
  let stored = null;
124
131
  provisioning.getInstanceById = async (_db, id) => (id === 41 ? { id: 41, owner_builder_id: 7, demo: stored } : null);
@@ -158,6 +165,25 @@ test('the create files a normal project marked demo, with the demo\'s picked ans
158
165
  assert.equal((await post('/provisioning/instances', { slug: 'bad-demo', hosting_shape: 'byo-host', demo: { people: 99, hours_each: 4 } })).status, 400);
159
166
  });
160
167
 
168
+ test('a new project is stamped born, once, before its provision intent; an existing one never is (task 1004506)', async () => {
169
+ order.length = 0;
170
+ assert.equal((await post('/provisioning/instances', { slug: 'born-demo', hosting_shape: 'byo-host', demo: { people: 2, hours_each: 3 } })).status, 201);
171
+ assert.deepEqual(order, ['birth:41', 'demo:41', 'enqueue:provision'], 'the marker and the demo box are on the row before the runner can read it');
172
+ order.length = 0;
173
+ assert.equal((await post('/provisioning/instances', { slug: 'born-real', hosting_shape: 'byo-host' })).status, 201);
174
+ assert.deepEqual(order, ['birth:41', 'enqueue:provision'], 'a real project is born too, with no demo box');
175
+ order.length = 0;
176
+ const again = await post('/provisioning/instances', { slug: 'old-one', hosting_shape: 'byo-host' });
177
+ assert.ok(again.status < 300, JSON.stringify(again.body));
178
+ assert.ok(!order.some((o) => o.startsWith('birth:') || o.startsWith('demo:')), 'a re-request of a project that already exists is never born (spec D8)');
179
+ const keep = birth.stampBirth;
180
+ birth.stampBirth = async () => { throw new Error('db hiccup'); };
181
+ try {
182
+ assert.equal((await post('/provisioning/instances', { slug: 'unlucky', hosting_shape: 'byo-host' })).status, 201,
183
+ 'a missed marker costs the project its genesis, never its creation');
184
+ } finally { birth.stampBirth = keep; }
185
+ });
186
+
161
187
  test('keep your planet? over the route: the owner answers once', async () => {
162
188
  stored = demo.demoRecord({ people: 3, hours_each: 4 }, new Date(Date.now() - 5 * 3600000));
163
189
  const r = await post('/provisioning/instances/41/demo', { outcome: 'kept' });