@bongos/core 1.21.151 → 1.21.152

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 (43) hide show
  1. package/.bongos-core.json +47 -47
  2. package/docs/copy-inventory.md +16 -16
  3. package/docs/copy-registry.json +16 -16
  4. package/docs/module-api-changelog.md +2 -0
  5. package/docs/modules-contract.md +29 -0
  6. package/docs/page-readings.json +27 -26
  7. package/modules/agents/module.json +1 -0
  8. package/modules/autonomy/module.json +1 -0
  9. package/modules/builder-settings/module.json +1 -0
  10. package/modules/copy-desk/module.json +1 -0
  11. package/modules/design-styles/module.json +1 -0
  12. package/modules/discord/module.json +1 -0
  13. package/modules/economy/module.json +1 -0
  14. package/modules/government/module.json +1 -0
  15. package/modules/grading/module.json +1 -0
  16. package/modules/hall-ui/module.json +1 -0
  17. package/modules/hall-ui/public/modules.js +16 -4
  18. package/modules/ideas/module.json +1 -0
  19. package/modules/lifecycle/module.json +1 -0
  20. package/modules/memory/module.json +1 -0
  21. package/modules/npm-release/module.json +1 -0
  22. package/modules/onboarding/module.json +1 -0
  23. package/modules/page-notes/module.json +1 -0
  24. package/modules/pixel-art/module.json +1 -0
  25. package/modules/platform-identity/module.json +1 -0
  26. package/modules/provisioning/module.json +1 -0
  27. package/modules/public-landing/module.json +1 -0
  28. package/modules/render-deploy/module.json +1 -0
  29. package/modules/security/module.json +1 -0
  30. package/modules/sessions/module.json +1 -0
  31. package/modules/specialities/module.json +1 -0
  32. package/modules/status-ui/module.json +1 -0
  33. package/modules/ui-design/module.json +1 -0
  34. package/package-lock.json +2 -2
  35. package/package.json +1 -1
  36. package/release-notes.json +6 -0
  37. package/scripts/gds/module.js +4 -2
  38. package/src/module-api.js +1 -1
  39. package/src/module-loader/catalog.js +15 -1
  40. package/src/module-loader/manifest-schema.js +40 -1
  41. package/tests/hall_catalog_world.mjs +4 -1
  42. package/tests/module_catalog.mjs +34 -0
  43. package/tests/module_manifest.mjs +36 -0
@@ -4,6 +4,7 @@
4
4
  "description": "The platform capability that lets any builder on any instance create and design good-looking, functional UIs in line with that instance's world (scoped with the owner 2026-08-28; task 1003324, ADR 0197). It ships METHOD and never a world: the neutral design contract of ADR 0081 with its tool adapters (Claude Design, Figma), their sync CLIs and the validator; the /design playbook for ui-discipline work; the look-before-you-ship kit (kit/) and the style library (styles/). The thirteen design-style skills that lived in skills/ are the opt-in design-styles module since task 1004470; /design reaches them when that module is on. The instance's world — its branding pack, config/design-tokens.*, config/design-sources.json and its DESIGN.md — stays in config/ and at the repo root as the host boundary this module reads and never owns (ADR 0062 §7). Cloud Bongos's own front door is this module's first customer, not its owner. default-on because every instance has a face. No routes, no migrations, no port: declaration-only like character-anim, so disabling it withdraws only the skills it contributes and touches no served surface.",
5
5
  "version": "1.0.0",
6
6
  "coreVersion": "^1.19.0",
7
+ "category": { "parent": "pages" },
7
8
  "default": true,
8
9
  "maintenance": { "status": "core-maintained" },
9
10
  "contributes": { "skills": ["design", "design-sync", "figma-design-sync"], "disciplines": ["ui"] },
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.151",
3
+ "version": "1.21.152",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.151",
9
+ "version": "1.21.152",
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.21.151",
3
+ "version": "1.21.152",
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",
@@ -9920,5 +9920,11 @@
9920
9920
  "id": "1004739",
9921
9921
  "text": "The module test runner can now test against the private core: the server sends its own copy of the core into the private test repo, once per version."
9922
9922
  }
9923
+ ],
9924
+ "1.21.152": [
9925
+ {
9926
+ "id": "1003784",
9927
+ "text": "Every module now says which of the eight categories it belongs to, and the module list in the terminal and the hall's Modules tab both show it and let you search by it."
9928
+ }
9923
9929
  ]
9924
9930
  }
@@ -452,14 +452,16 @@ function formatCatalog(rows, { catalogOnly = false, query = '', core = null, del
452
452
  return out.join('\n');
453
453
  }
454
454
 
455
- out.push(` ${pad('', 5)}${pad('KEY', 20)}${pad('VERSION', 9)}${pad('SOURCE', 10)}${pad('AUTHOR', 20)}${pad('ORIGIN', 26)}${pad('LICENSE', 12)}UPKEEP`);
455
+ out.push(` ${pad('', 5)}${pad('KEY', 20)}${pad('VERSION', 9)}${pad('SOURCE', 10)}${pad('CATEGORY', 28)}${pad('AUTHOR', 20)}${pad('ORIGIN', 26)}${pad('LICENSE', 12)}UPKEEP`);
456
456
  for (const r of shown) {
457
457
  const mark = r.enabled ? '[x]' : '[ ]';
458
458
  // An inferred credit is marked so it never reads as something the author
459
459
  // typed — the catalog's whole job is honest attribution (ADR 0107 §2).
460
460
  const suffix = r.credit.inferred ? '*' : '';
461
461
  const upkeep = ((r.maintenance && r.maintenance.status) || 'undeclared') + (delisted.has(r.key) ? ' · DELISTED' : '');
462
- out.push(` ${pad(mark, 5)}${pad(clip(r.key, 19), 20)}${pad(clip(r.version || '—', 8), 9)}${pad(r.source, 10)}${pad(clip((r.credit.author || '—') + suffix, 19), 20)}${pad(clip((r.credit.origin || '—') + suffix, 25), 26)}${pad(clip(r.license || '—', 11), 12)}${upkeep}`);
462
+ // The parent key, plus the sub-category once one is approved (ADR 0342).
463
+ const cat = r.category ? r.category.parent + (r.category.sub ? `/${r.category.sub}` : '') : '—';
464
+ out.push(` ${pad(mark, 5)}${pad(clip(r.key, 19), 20)}${pad(clip(r.version || '—', 8), 9)}${pad(r.source, 10)}${pad(clip(cat, 27), 28)}${pad(clip((r.credit.author || '—') + suffix, 19), 20)}${pad(clip((r.credit.origin || '—') + suffix, 25), 26)}${pad(clip(r.license || '—', 11), 12)}${upkeep}`);
463
465
  }
464
466
  out.push('');
465
467
  out.push(' [x] enabled here · [ ] available, not enabled');
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.151'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.152'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -30,6 +30,7 @@
30
30
  const fs = require('node:fs');
31
31
  const path = require('node:path');
32
32
  const { deriveProvenance } = require('./provenance');
33
+ const { CATEGORY_PARENTS } = require('./manifest-schema');
33
34
 
34
35
  // The core's own published + source package names. A checkout whose package.json
35
36
  // says one of these IS the core, so its modules/ IS the catalog (see
@@ -99,6 +100,17 @@ function creditFor(manifest = {}, { source = 'instance', coreProjectName = CORE_
99
100
  };
100
101
  }
101
102
 
103
+ // Where a module is browsed (ADR 0342): its parent key, that parent's label, and its
104
+ // sub-category key — or null when the manifest declares none. The label is resolved
105
+ // HERE so the hall and the CLI never keep their own copy of the parent list. A parent
106
+ // outside the closed list cannot reach this (the validator refuses it), so an unknown
107
+ // one is null rather than shown under a label nobody chose.
108
+ function categoryFor(manifest = {}) {
109
+ const c = manifest.category;
110
+ if (!c || typeof c !== 'object' || !Object.hasOwn(CATEGORY_PARENTS, c.parent)) return null;
111
+ return { parent: c.parent, parentTitle: CATEGORY_PARENTS[c.parent], sub: c.sub || null };
112
+ }
113
+
102
114
  // One catalog row per discovered module. `loaded` is the loader's
103
115
  // discoverModules().loaded ([{ key, dir, manifest }]); `enabled` is the resolved
104
116
  // enablement map (src/modules.js modules()).
@@ -138,6 +150,7 @@ function buildCatalog({
138
150
  license: manifest.license || null,
139
151
  maintainer: manifest.maintainer || null,
140
152
  maintenance: manifest.maintenance || null,
153
+ category: categoryFor(manifest),
141
154
  };
142
155
  })
143
156
  .sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
@@ -160,7 +173,8 @@ function instanceRows(rows = []) {
160
173
  function searchCatalog(rows = [], query = '') {
161
174
  const q = String(query || '').trim().toLowerCase();
162
175
  if (!q) return rows;
163
- return rows.filter((r) => [r.key, r.title, r.description, r.credit.author, r.credit.origin, r.license]
176
+ return rows.filter((r) => [r.key, r.title, r.description, r.credit.author, r.credit.origin, r.license,
177
+ r.category && r.category.parentTitle, r.category && r.category.sub]
164
178
  .some((f) => f && String(f).toLowerCase().includes(q)));
165
179
  }
166
180
 
@@ -53,6 +53,10 @@
53
53
  // // actually declares in `contributes`, and every entry must be
54
54
  // // one it actually lists there — a typo'd exemption is an error,
55
55
  // // never a silent pass.
56
+ // "category": { // OPTIONAL. Where the module is browsed and ranked (ADR 0342).
57
+ // "parent": "money", // REQUIRED in-block: one of the eight CATEGORY_PARENTS keys.
58
+ // "sub": "stripe-payments" // OPTIONAL. An approved sub-category key — the comparable set
59
+ // }, // the store's cap of ten counts within; absent while pending.
56
60
  // "howto": { // OPTIONAL. Extras beside the module's HOWTO.md (ADR 0347 D3).
57
61
  // "artifactUrl": "https://claude.ai/…" // A Claude page with the same how-to, shown as an extra,
58
62
  // }, // never graded and never required — the file is the how-to.
@@ -106,6 +110,24 @@ const SPEND_KEYS = ['requiresPayer'];
106
110
  // The `howto` block (ADR 0347 D3) — closed for the same reason; not exported either.
107
111
  const HOWTO_KEYS = ['artifactUrl'];
108
112
 
113
+ // The eight fixed parent categories (ADR 0342 D1), key → the label a catalog shows.
114
+ // A parent is for browsing only: the cap of ten counts within a SUB-category, never a
115
+ // parent (D2). The list is closed so a typo'd "monye" fails loud rather than filing
116
+ // the module under a ninth parent nobody browses; changing it needs a new ADR.
117
+ // `sub` is shape-checked only — which sub-categories are approved is ledger state
118
+ // (task 1004355), not something a pure validator can know.
119
+ const CATEGORY_PARENTS = Object.freeze({
120
+ work: 'Work & planning',
121
+ people: 'Team & people',
122
+ quality: 'Quality & review',
123
+ money: 'Money',
124
+ communication: 'Communication',
125
+ pages: 'Pages & design',
126
+ hosting: 'Hosting & deploy',
127
+ knowledge: 'Knowledge',
128
+ });
129
+ const CATEGORY_KEYS = ['parent', 'sub'];
130
+
109
131
  // An https link on claude.ai (or a subdomain). Shape only: the store never fetches it.
110
132
  function isClaudeArtifactUrl(v) {
111
133
  if (!isStr(v)) return false;
@@ -320,6 +342,23 @@ function validateManifest(obj) {
320
342
  }
321
343
  }
322
344
 
345
+ // --- optional category (ADR 0342) ---
346
+ if ('category' in obj) {
347
+ const c = obj.category;
348
+ const parents = Object.keys(CATEGORY_PARENTS);
349
+ if (!isObj(c)) E('category: must be an object (e.g. { "parent": "money", "sub": "stripe-payments" })');
350
+ else {
351
+ for (const k of Object.keys(c)) {
352
+ if (!CATEGORY_KEYS.includes(k)) E(`category.${k}: unknown key (allowed: ${CATEGORY_KEYS.join(', ')}) — a module has exactly one sub-category (ADR 0342 D4)`);
353
+ }
354
+ if (!isStr(c.parent)) E(`category.parent: required — one of ${parents.join(', ')}`);
355
+ else if (!parents.includes(c.parent)) E(`category.parent: "${c.parent}" is not one of ${parents.join(', ')} (ADR 0342 D1)`);
356
+ if ('sub' in c && (!isStr(c.sub) || !KEY_RE.test(c.sub))) {
357
+ E('category.sub: must be a sub-category key (lowercase kebab-case) when present — leave it out while a new one is pending approval');
358
+ }
359
+ }
360
+ }
361
+
323
362
  // --- optional how-to extras (ADR 0347 D3) ---
324
363
  if ('howto' in obj) {
325
364
  const h = obj.howto;
@@ -370,5 +409,5 @@ function assertValidManifest(obj, label = 'module.json') {
370
409
 
371
410
  module.exports = {
372
411
  validateManifest, assertValidManifest, declaredEntries, entryLabel, KEY_RE, ALLOWED_CONTRIBUTES, ARRAY_OF_STRING_KEYS, ATTRIBUTION_KEYS,
373
- MAINTENANCE_STATUSES, MAINTENANCE_KEYS, SUNSET_STATUSES,
412
+ MAINTENANCE_STATUSES, MAINTENANCE_KEYS, SUNSET_STATUSES, CATEGORY_PARENTS,
374
413
  };
@@ -58,7 +58,7 @@ test('modules: the row is the oversight row — title with the mono key, clamped
58
58
  assert.match(js, /<span class="ov-row__title">\$\{escapeHtml\(m\.title \|\| m\.key\)\}<span class="ov-mono">\$\{escapeHtml\(m\.key\)\}<\/span>/, 'the key rides inside the title in mono');
59
59
  assert.match(js, /class="ov-row__sub mod-row__desc"/, 'the description is the sub line');
60
60
  assert.match(js, /<details class="mod-more"/, 'a details disclosure opens the rest');
61
- assert.match(js, /<div class="ov-row__facts">\$\{provenancePill\(m\)\}\$\{attributionFacts\(m\)\}\$\{upkeepFact\(m\)\}\$\{howtoFact\(m\)\}\$\{delistedFact\(m\)\}\$\{restartFact\(m\)\}<\/div>/, 'provenance, credit, upkeep, (for a store version, task 1004366) the how-to link and (task 1003797) the delisted pill and (task 1004463) the waits-for-a-restart pill are the facts line');
61
+ assert.match(js, /<div class="ov-row__facts">\$\{provenancePill\(m\)\}\$\{categoryFact\(m\)\}\$\{attributionFacts\(m\)\}\$\{upkeepFact\(m\)\}\$\{howtoFact\(m\)\}\$\{delistedFact\(m\)\}\$\{restartFact\(m\)\}<\/div>/, 'provenance, (task 1003784) the category, credit, upkeep, (for a store version, task 1004366) the how-to link and (task 1003797) the delisted pill and (task 1004463) the waits-for-a-restart pill are the facts line');
62
62
  assert.match(js, /<div class="ov-row__right mod-row__right">\$\{rightHtml\(m\)\}<\/div>/, 'the right column is one function');
63
63
  const css = read('modules.css');
64
64
  assert.match(css, /\.mod-row__desc \{[^}]*-webkit-line-clamp: 2/, 'the description clamps to two lines');
@@ -84,6 +84,9 @@ test('modules: upkeep colours the exception — undeclared is the warn pill, mai
84
84
  assert.match(js, /<span class="ov-fact">upkeep <b>\$\{escapeHtml\(status\)\}<\/b><\/span>/, 'a maintained module reads as a plain fact');
85
85
  assert.match(js, /fact-pill fact-pill--info">core-loaded</, 'core-loaded is the info pill');
86
86
  assert.match(js, /<span class="fact-chip">\$\{escapeHtml\(m\.license\)\}<\/span>/, 'the licence is a chip');
87
+ assert.match(js, /<span class="ov-fact">in <b>\$\{escapeHtml\(c\.parentTitle\)\}<\/b>\$\{sub\}<\/span>/,
88
+ 'the category is a plain fact, labelled from the server row (task 1003784)');
89
+ assert.match(js, /cat\.parentTitle, cat\.sub\]/, 'the hall search covers the category, as searchCatalog does');
87
90
  });
88
91
 
89
92
  test('modules: the kit filter bar carries the search and a state lens, and the empty state is the kit\'s', () => {
@@ -330,3 +330,37 @@ test('detectRoots: an explicit root bypasses the memo in both directions', () =>
330
330
  // …nor poison the memo for every later caller that asks about this process.
331
331
  assert.equal(catalog.detectRoots(), cached);
332
332
  });
333
+
334
+ // ---------------------------------------------------------------------------
335
+ // category — the ADR 0342 field, carried by the ONE model to both surfaces (task 1003784)
336
+ // ---------------------------------------------------------------------------
337
+
338
+ const categorized = () => catalog.buildCatalog({
339
+ loaded: [
340
+ loadedIn(CORE, 'economy', { category: { parent: 'money' } }),
341
+ loadedIn(CORE, 'discord', { category: { parent: 'communication', sub: 'team-chat' } }),
342
+ loadedIn(INST, 'game'),
343
+ ],
344
+ coreRoot: CORE, instanceRoot: INST,
345
+ });
346
+
347
+ test('buildCatalog: a row carries its category with the parent label; none declared is null', () => {
348
+ const byKey = Object.fromEntries(categorized().map((r) => [r.key, r]));
349
+ assert.deepEqual(byKey.economy.category, { parent: 'money', parentTitle: 'Money', sub: null });
350
+ assert.deepEqual(byKey.discord.category, { parent: 'communication', parentTitle: 'Communication', sub: 'team-chat' });
351
+ assert.equal(byKey.game.category, null);
352
+ });
353
+
354
+ test('searchCatalog: finds a module by its category label or sub-category', () => {
355
+ const rows = categorized();
356
+ assert.deepEqual(catalog.searchCatalog(rows, 'money').map((r) => r.key), ['economy']);
357
+ assert.deepEqual(catalog.searchCatalog(rows, 'team-chat').map((r) => r.key), ['discord']);
358
+ });
359
+
360
+ test('formatCatalog: the CATEGORY column shows parent/sub, and a dash when none is declared', () => {
361
+ const out = mod.formatCatalog(categorized());
362
+ assert.match(out, /CATEGORY/);
363
+ assert.match(out, /discord\s+\S+\s+core\s+communication\/team-chat/);
364
+ assert.match(out, /economy\s+\S+\s+core\s+money\s/);
365
+ assert.match(out, /game\s+\S+\s+instance\s+—\s/);
366
+ });
@@ -392,3 +392,39 @@ test('howto.artifactUrl: anything but an https claude.ai link is refused, as is
392
392
  .some((e) => e.startsWith('howto.artifactURL: unknown key')));
393
393
  assert.equal(validateManifest({ ...baseManifest(), howto: 'https://claude.ai/x' }).valid, false);
394
394
  });
395
+
396
+ // --- category (ADR 0342, task 1003784) ---
397
+
398
+ test('category: optional; a known parent, with or without a sub-category, is accepted', () => {
399
+ assert.equal(validateManifest(baseManifest()).valid, true, 'a manifest with no category still validates');
400
+ for (const category of [{ parent: 'money' }, { parent: 'communication', sub: 'team-chat' }]) {
401
+ const r = validateManifest({ ...baseManifest(), category });
402
+ assert.equal(r.valid, true, `${JSON.stringify(category)}: ${r.errors.join('; ')}`);
403
+ }
404
+ });
405
+
406
+ test('category: a parent outside the eight, a missing parent, a bad sub or an unknown key fails loud', () => {
407
+ const errs = (category) => validateManifest({ ...baseManifest(), category }).errors;
408
+ assert.ok(errs({ parent: 'monye' }).some((e) => e.startsWith('category.parent: "monye" is not one of')), 'a typo names the list');
409
+ assert.ok(errs({ sub: 'stripe-payments' }).some((e) => e.startsWith('category.parent: required')));
410
+ assert.ok(errs({ parent: 'money', sub: 'Stripe Payments' }).some((e) => e.startsWith('category.sub:')));
411
+ assert.ok(errs({ parent: 'money', sub: '' }).some((e) => e.startsWith('category.sub:')));
412
+ assert.ok(errs({ parent: 'money', subs: ['a', 'b'] }).some((e) => e.startsWith('category.subs: unknown key')),
413
+ 'one sub-category per module (D4) — a second field is a typo, not a list');
414
+ assert.equal(validateManifest({ ...baseManifest(), category: 'money' }).valid, false);
415
+ });
416
+
417
+ test('category: the eight parents are exactly ADR 0342 D1', () => {
418
+ const { CATEGORY_PARENTS } = require(path.join(ROOT, 'src', 'module-loader', 'manifest-schema.js'));
419
+ assert.deepEqual(Object.keys(CATEGORY_PARENTS),
420
+ ['work', 'people', 'quality', 'money', 'communication', 'pages', 'hosting', 'knowledge']);
421
+ });
422
+
423
+ test('category: every bundled module declares one (core modules get categories too, ADR 0342)', async () => {
424
+ const fs = await import('node:fs');
425
+ const dir = path.join(ROOT, 'modules');
426
+ const missing = fs.readdirSync(dir)
427
+ .filter((k) => fs.existsSync(path.join(dir, k, 'module.json')))
428
+ .filter((k) => !JSON.parse(fs.readFileSync(path.join(dir, k, 'module.json'), 'utf8')).category);
429
+ assert.deepEqual(missing, [], `declare "category": { "parent": … } in: ${missing.join(', ')}`);
430
+ });