@bongos/core 1.19.514 → 1.19.516

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 (38) hide show
  1. package/.bongos-core.json +81 -36
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +12 -0
  4. package/clients/bongos-client/index.cjs +12 -0
  5. package/clients/bongos-client/index.d.ts +22 -2
  6. package/clients/bongos-client/index.mjs +12 -0
  7. package/docs/adr/0240-a-skipped-module-picker-stores-nothing-and-resolves-the-bundle-on-read.md +157 -0
  8. package/docs/adr/README.md +1 -0
  9. package/docs/api/openapi.json +362 -6
  10. package/docs/api-reference.md +12 -2
  11. package/docs/copy-inventory.md +224 -218
  12. package/docs/copy-registry.json +357 -291
  13. package/docs/module-api-changelog.md +4 -0
  14. package/modules/dev-box/app/src/vendor/bongos-client.cjs +12 -0
  15. package/modules/government/catalog.js +6 -0
  16. package/modules/government/migrations/government_009_mingle_manage.sql +55 -0
  17. package/modules/lifecycle/migrations/lifecycle_007_mingle.sql +134 -0
  18. package/modules/lifecycle/mingle.js +337 -0
  19. package/modules/lifecycle/module.json +1 -0
  20. package/modules/lifecycle/routes/mingle.js +116 -0
  21. package/modules/provisioning/migrations/provisioning_014_module_selection.sql +60 -0
  22. package/modules/provisioning/provisioning.js +24 -4
  23. package/modules/provisioning/routes/provisioning.js +59 -2
  24. package/modules/provisioning/starter-bundles.js +91 -0
  25. package/modules/public-landing/public/assets/cosmos.css +1 -1
  26. package/modules/public-landing/public/projects.html +230 -35
  27. package/modules/public-landing/public/projects.probes.json +2 -2
  28. package/modules/public-landing/public/projects.states.json +5 -4
  29. package/modules/ui-design/kit/serve.js +32 -0
  30. package/package-lock.json +2 -2
  31. package/package.json +1 -1
  32. package/src/module-api.js +1 -1
  33. package/tests/government_seed.mjs +35 -0
  34. package/tests/mingle.mjs +370 -0
  35. package/tests/projects_hub.mjs +72 -7
  36. package/tests/projects_hub_module_picker.mjs +369 -0
  37. package/tests/provisioning_module_selection.mjs +286 -0
  38. package/tests/provisioning_starter_bundles.mjs +131 -0
@@ -34,10 +34,13 @@ import { createRequire } from 'node:module';
34
34
  // the knip ratchet (see the repo's knip learning).
35
35
  import {
36
36
  ALWAYS_ON_CORE,
37
+ OPTIONAL_MODULES,
37
38
  STARTER_BUNDLES,
38
39
  HUB_ONLY_MODULES,
39
40
  bundleForProjectType,
40
41
  allStarterBundles,
42
+ normalizeModuleSelection,
43
+ effectiveModules,
41
44
  } from '../modules/provisioning/starter-bundles.js';
42
45
 
43
46
  const require = createRequire(import.meta.url);
@@ -266,3 +269,131 @@ test('the route never reaches the database — the bundles are a declaration, no
266
269
  const res = await get('/api/bongos/provisioning/starter-bundles');
267
270
  assert.equal(res.status, 200);
268
271
  });
272
+
273
+ // ── the opt-in catalog + the selection rules (BV1.R20, task 1002339) ─────────
274
+
275
+ test('the opt-in catalog declared for the picker IS the on-disk opt-in set', () => {
276
+ // Arrange — the manifests are the catalog; OPTIONAL_MODULES is the declaration
277
+ // the picker renders. This is the same link the bundle<->catalog tests above
278
+ // make, applied to the choices: a new default:false module cannot ship without
279
+ // owner-facing copy, and a declared choice cannot outlive its module.
280
+ const onDisk = manifestsOnDisk();
281
+ const optInOnDisk = [...onDisk.values()]
282
+ .filter((m) => m.default !== true && !HUB_ONLY_MODULES.includes(m.key))
283
+ .map((m) => m.key)
284
+ .sort();
285
+
286
+ // Act
287
+ const declared = OPTIONAL_MODULES.map((m) => m.key).sort();
288
+
289
+ // Assert
290
+ assert.deepEqual(declared, optInOnDisk,
291
+ 'the picker\'s catalog has drifted from the manifests — a module was added, removed, or flipped its default');
292
+ });
293
+
294
+ test('every opt-in module carries owner-facing copy, and no module jargon rides along', () => {
295
+ for (const row of OPTIONAL_MODULES) {
296
+ assert.ok(row.label && row.label.length <= 40, `"${row.key}" needs a short label`);
297
+ assert.ok(row.summary && row.summary.length > 20, `"${row.key}" needs a real one-line summary`);
298
+ // The owner choosing here has met none of these modules; a manifest sentence
299
+ // ("Mirror only, never authority (ADR 0033)") is true and unusable.
300
+ assert.doesNotMatch(`${row.label} ${row.summary}`, /\bADR\b|\bjsonb\b|\bAPI\b|\bwebhook\b|_/i,
301
+ `"${row.key}" reads like a manifest, not like a choice`);
302
+ }
303
+ });
304
+
305
+ test('every module a bundle preselects is a row the picker can actually render', () => {
306
+ // A preselect with no catalog row would light up nothing — the panel would
307
+ // silently ship a choice the owner cannot see or switch off.
308
+ const offered = OPTIONAL_MODULES.map((m) => m.key);
309
+ for (const [type, bundle] of Object.entries(STARTER_BUNDLES)) {
310
+ for (const key of bundle.modules) {
311
+ assert.ok(offered.includes(key), `bundle "${type}" preselects "${key}", which the picker does not offer`);
312
+ }
313
+ }
314
+ });
315
+
316
+ test('normalizeModuleSelection: absent is null, an empty answer is [], and order is the catalog\'s', () => {
317
+ // Arrange + Act + Assert — the three-valued contract the nullable column exists for
318
+ assert.equal(normalizeModuleSelection(undefined), null, 'never answered');
319
+ assert.equal(normalizeModuleSelection(null), null, 'never answered');
320
+ assert.equal(normalizeModuleSelection('discord'), null, 'a non-array is not an answer');
321
+ assert.deepEqual(normalizeModuleSelection([]), [], 'the owner said none — a real answer');
322
+ // catalog order, not the caller's
323
+ assert.deepEqual(normalizeModuleSelection(['discord', 'agents']), ['agents', 'discord']);
324
+ assert.deepEqual(normalizeModuleSelection(['discord', 'discord']), ['discord'], 'deduped');
325
+ });
326
+
327
+ test('normalizeModuleSelection drops what was never a choice — always-on, hub-only, unknown', () => {
328
+ assert.deepEqual(normalizeModuleSelection(['discord', 'memory']), ['discord'], 'always-on is the floor, not a pick');
329
+ assert.deepEqual(normalizeModuleSelection(['provisioning']), [], 'the control plane is not a project module');
330
+ assert.deepEqual(normalizeModuleSelection(['nope', 42, null]), [], 'nothing is invented from junk');
331
+ });
332
+
333
+ test('effectiveModules: a skipped picker yields exactly the declared type\'s bundle', () => {
334
+ // Arrange — the row a skip writes: type declared, modules NULL
335
+ const row = { type: 'game', modules: null };
336
+
337
+ // Act
338
+ const eff = effectiveModules(row);
339
+
340
+ // Assert — the preselect and the fallback are the SAME declaration, which is
341
+ // what makes "skip = what the panel showed you" true rather than a promise.
342
+ assert.deepEqual(eff.selected, bundleForProjectType('game').bundle);
343
+ assert.deepEqual(eff.modules, bundleForProjectType('game').modules);
344
+ assert.equal(eff.source, 'bundle', 'and a reader can tell nobody answered');
345
+ assert.deepEqual(eff.core, [...ALWAYS_ON_CORE]);
346
+ });
347
+
348
+ test('effectiveModules: an answered picker wins over the bundle, including "none of them"', () => {
349
+ assert.deepEqual(effectiveModules({ type: 'game', modules: ['agents'] }).selected, ['agents']);
350
+ assert.equal(effectiveModules({ type: 'game', modules: ['agents'] }).source, 'chosen');
351
+ // the case a NOT NULL DEFAULT '[]' column could never have expressed
352
+ const none = effectiveModules({ type: 'game', modules: [] });
353
+ assert.deepEqual(none.selected, [], 'the owner switched the bundle off, and it stays off');
354
+ assert.deepEqual(none.modules, [...ALWAYS_ON_CORE]);
355
+ assert.equal(none.source, 'chosen');
356
+ });
357
+
358
+ test('effectiveModules: an undeclared type resolves to the core, never a guess', () => {
359
+ for (const row of [{ type: null, modules: null }, { type: 'not-sure', modules: null }, {}]) {
360
+ const eff = effectiveModules(row);
361
+ assert.deepEqual(eff.selected, [], `${JSON.stringify(row)} preselects nothing`);
362
+ assert.deepEqual(eff.modules, [...ALWAYS_ON_CORE]);
363
+ }
364
+ });
365
+
366
+ test('GET /provisioning/starter-bundles carries the opt-in catalog the picker renders', async () => {
367
+ // Act
368
+ const res = await get('/api/bongos/provisioning/starter-bundles');
369
+
370
+ // Assert — one read gives the picker both halves: what is preselected, and
371
+ // what may be chosen. A second round trip is what lets them disagree.
372
+ assert.equal(res.status, 200, res.raw);
373
+ assert.deepEqual(res.body.optional, OPTIONAL_MODULES.map((m) => ({ ...m })));
374
+ for (const row of res.body.optional) {
375
+ assert.deepEqual(Object.keys(row).sort(), ['key', 'label', 'summary']);
376
+ }
377
+ });
378
+
379
+ test('the design kit\'s stub fixture is the same payload this route serves', () => {
380
+ // modules/ui-design/kit/serve.js answers the picker's read with a hand-written
381
+ // fixture (the stub imports no module, by design — see modules/public-landing/
382
+ // CLAUDE.md's "the kit's stub is not the server" trap). This is the pin that
383
+ // stops the picture the kit renders from drifting away from the real payload:
384
+ // task 1003352 shipped a body the real route would have 400'd on, past 47
385
+ // green probes and a clean render, because nothing compared the two.
386
+ const stub = fs.readFileSync(path.join(fileURLToPath(new URL('..', import.meta.url)), 'modules/ui-design/kit/serve.js'), 'utf8');
387
+ const src = (stub.match(/const STARTER_BUNDLES_FIXTURE = (\{[\s\S]*?\n\};)/) || [, ''])[1];
388
+ assert.ok(src, 'the fixture is extractable');
389
+ // eslint-disable-next-line no-new-func -- reading a literal out of our own repo file
390
+ const fixture = new Function(`return ${src.replace(/;$/, '')}`)();
391
+
392
+ assert.deepEqual(fixture.core, [...ALWAYS_ON_CORE], 'the core the panel counts');
393
+ assert.deepEqual(fixture.optional, OPTIONAL_MODULES.map((m) => ({ ...m })), 'the catalog the panel renders');
394
+ assert.deepEqual(
395
+ fixture.bundles.map((b) => ({ type: b.type, bundle: b.bundle })),
396
+ allStarterBundles().map((b) => ({ type: b.type, bundle: b.bundle })),
397
+ 'and what each type preselects',
398
+ );
399
+ });