@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.
- package/.bongos-core.json +47 -47
- package/docs/copy-inventory.md +16 -16
- package/docs/copy-registry.json +16 -16
- package/docs/module-api-changelog.md +2 -0
- package/docs/modules-contract.md +29 -0
- package/docs/page-readings.json +27 -26
- package/modules/agents/module.json +1 -0
- package/modules/autonomy/module.json +1 -0
- package/modules/builder-settings/module.json +1 -0
- package/modules/copy-desk/module.json +1 -0
- package/modules/design-styles/module.json +1 -0
- package/modules/discord/module.json +1 -0
- package/modules/economy/module.json +1 -0
- package/modules/government/module.json +1 -0
- package/modules/grading/module.json +1 -0
- package/modules/hall-ui/module.json +1 -0
- package/modules/hall-ui/public/modules.js +16 -4
- package/modules/ideas/module.json +1 -0
- package/modules/lifecycle/module.json +1 -0
- package/modules/memory/module.json +1 -0
- package/modules/npm-release/module.json +1 -0
- package/modules/onboarding/module.json +1 -0
- package/modules/page-notes/module.json +1 -0
- package/modules/pixel-art/module.json +1 -0
- package/modules/platform-identity/module.json +1 -0
- package/modules/provisioning/module.json +1 -0
- package/modules/public-landing/module.json +1 -0
- package/modules/render-deploy/module.json +1 -0
- package/modules/security/module.json +1 -0
- package/modules/sessions/module.json +1 -0
- package/modules/specialities/module.json +1 -0
- package/modules/status-ui/module.json +1 -0
- package/modules/ui-design/module.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/gds/module.js +4 -2
- package/src/module-api.js +1 -1
- package/src/module-loader/catalog.js +15 -1
- package/src/module-loader/manifest-schema.js +40 -1
- package/tests/hall_catalog_world.mjs +4 -1
- package/tests/module_catalog.mjs +34 -0
- 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.
|
|
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.
|
|
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.
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -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
|
}
|
package/scripts/gds/module.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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', () => {
|
package/tests/module_catalog.mjs
CHANGED
|
@@ -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
|
+
});
|