@bongos/core 1.19.64 → 1.19.66
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 +55 -40
- package/docs/adr/0166-module-maintenance-and-deprecation-policy.md +91 -0
- package/docs/adr/README.md +1 -0
- package/docs/modules-contract.md +23 -1
- package/modules/autonomy/module.json +1 -0
- package/modules/builder-settings/module.json +1 -0
- package/modules/dev-box/module.json +1 -0
- package/modules/discord/module.json +1 -0
- package/modules/economy/module.json +1 -0
- package/modules/governance/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 +17 -1
- package/modules/ideas/module.json +1 -0
- package/modules/lifecycle/module.json +1 -0
- package/modules/memory/module.json +1 -0
- package/modules/onboarding/module.json +1 -0
- package/modules/platform-identity/module.json +1 -0
- package/modules/provisioning/module.json +1 -0
- package/modules/security/module.json +1 -0
- package/modules/sessions/module.json +1 -0
- package/modules/status-ui/module.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +11 -2
- package/public-landing/projects.html +591 -0
- package/scripts/gds/module.js +103 -9
- package/scripts/gds/upgrade.js +50 -5
- package/src/bongos/routes/modules.js +4 -0
- package/src/module-api.js +5 -1
- package/src/module-loader/manifest-schema.js +69 -2
- package/src/platform-server.js +9 -0
- package/tests/module_cli.mjs +126 -0
- package/tests/module_manifest.mjs +65 -0
- package/tests/public_landing_projects.mjs +121 -0
- package/tests/upgrade.mjs +94 -0
package/scripts/gds/module.js
CHANGED
|
@@ -34,7 +34,10 @@
|
|
|
34
34
|
// because a brand-new module is never on the ALLOWLIST until it is
|
|
35
35
|
// accepted (§6); that omission must not read as "denied content." Also
|
|
36
36
|
// requires the manifest to declare an AGPL-3.0 license (inbound = outbound,
|
|
37
|
-
// ADR 0107 §4 — no CLA)
|
|
37
|
+
// ADR 0107 §4 — no CLA) and a `maintenance` posture (ADR 0107 §7 / ADR
|
|
38
|
+
// 0166 — core-maintained, or maintained by a named maintainer; a module
|
|
39
|
+
// already deprecated/orphaned cannot be submitted INTO the catalog).
|
|
40
|
+
// On a pass, `--sign-off "Name <email>"` records a
|
|
38
41
|
// lightweight DCO-style affirmation (own work, licensed AGPL-3.0 — never a
|
|
39
42
|
// copyright-assignment CLA) to modules/<key>/.upstream-signoff.json.
|
|
40
43
|
//
|
|
@@ -54,7 +57,8 @@ const path = require('node:path');
|
|
|
54
57
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
55
58
|
const MODULES_DIR = path.join(REPO_ROOT, 'modules');
|
|
56
59
|
|
|
57
|
-
const { validateManifest, assertValidManifest, KEY_RE } = require('../../src/module-loader/manifest-schema');
|
|
60
|
+
const { validateManifest, assertValidManifest, KEY_RE, SUNSET_STATUSES } = require('../../src/module-loader/manifest-schema');
|
|
61
|
+
const { parseVersion, cmp } = require('../../src/module-loader/semver');
|
|
58
62
|
const loader = require('../../src/module-loader/loader');
|
|
59
63
|
const { resolveModules, MODULE_REGISTRY, NEUTRAL_PATH, INSTANCE_PATH } = require('../../src/modules');
|
|
60
64
|
// The public-mirror publish boundary (ADR 0098) — REUSED, never re-typed, for
|
|
@@ -115,6 +119,11 @@ function buildManifest(key, { core = coreVersion() } = {}) {
|
|
|
115
119
|
origin: 'TODO: the instance/project this module was built in',
|
|
116
120
|
license: 'AGPL-3.0',
|
|
117
121
|
maintainer: 'TODO: who to credit/contact if this module is upstreamed',
|
|
122
|
+
// Upkeep posture (ADR 0107 §7 / ADR 0166). Scaffolded as `maintained`
|
|
123
|
+
// because a brand-new module's author IS its maintainer — the honest
|
|
124
|
+
// default. Change it to `core-maintained` only if the core team has agreed
|
|
125
|
+
// to own it, and to `deprecated`/`orphaned` (with a path) when that changes.
|
|
126
|
+
maintenance: { status: 'maintained' },
|
|
118
127
|
contributes: { routes: [key] },
|
|
119
128
|
};
|
|
120
129
|
}
|
|
@@ -191,13 +200,51 @@ kernel seams (\`api.registerProvider\` / \`api.resolve\` / \`api.on\` / \`api.em
|
|
|
191
200
|
`;
|
|
192
201
|
}
|
|
193
202
|
|
|
203
|
+
// Has the core advanced past a deprecated module's declared removal version?
|
|
204
|
+
// `removeAfter: "1.12.0"` means "may be deleted once core is ABOVE 1.12.0".
|
|
205
|
+
function isPastRemoveAfter(maintenance, core) {
|
|
206
|
+
const after = maintenance && maintenance.removeAfter;
|
|
207
|
+
const a = parseVersion(after);
|
|
208
|
+
const c = parseVersion(core);
|
|
209
|
+
return !!(a && c && cmp(c, a) > 0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// One line of "who do I chase about this?" for a row, from its maintenance block
|
|
213
|
+
// (ADR 0166). Shared by cmdUpgrade and upgrade.js's real-bump pre-flight so both
|
|
214
|
+
// surfaces say the same thing about the same module.
|
|
215
|
+
function formatMaintenanceHint(row = {}) {
|
|
216
|
+
const m = row.maintenance;
|
|
217
|
+
if (!m || !m.status) {
|
|
218
|
+
return 'no maintenance declared — nobody is on the hook for this module (ADR 0166: declare a `maintenance` block)';
|
|
219
|
+
}
|
|
220
|
+
const parts = [`maintenance: ${m.status}`];
|
|
221
|
+
if (m.status === 'maintained') parts.push(`maintainer ${row.maintainer || '(unnamed!)'}`);
|
|
222
|
+
if (m.status === 'core-maintained') parts.push('core-maintained — fixing this is the core\'s job');
|
|
223
|
+
if (m.successor) parts.push(`superseded by "${m.successor}"`);
|
|
224
|
+
if (m.removeAfter) parts.push(`removable after core ${m.removeAfter}`);
|
|
225
|
+
if (m.note) parts.push(m.note);
|
|
226
|
+
return parts.join('; ');
|
|
227
|
+
}
|
|
228
|
+
|
|
194
229
|
// Decide compatibility from rows (pure — tests drive it with injected rows).
|
|
195
|
-
// A row: { key, coreVersion, enabled, satisfied }.
|
|
196
|
-
//
|
|
230
|
+
// A row: { key, coreVersion, enabled, satisfied, maintenance?, maintainer? }.
|
|
231
|
+
//
|
|
232
|
+
// BLOCKING = an enabled module that does not satisfy the core (fail-closed).
|
|
233
|
+
// WARNINGS = a DISABLED mismatch. These are not cosmetic: an accepted catalog
|
|
234
|
+
// module ships `default: false` (ADR 0107 §6), so EVERY rotted catalog entry
|
|
235
|
+
// lands in this bucket. Dropping warnings on the floor is exactly how "the
|
|
236
|
+
// catalog fills with silently-broken entries" happens (ADR 0107 §7) — every
|
|
237
|
+
// caller must surface them, even though none of them block.
|
|
238
|
+
// DEPRECATIONS = modules whose own manifest says they are on the way out;
|
|
239
|
+
// EXPIRED = deprecated modules the core has already advanced past `removeAfter`
|
|
240
|
+
// (they are due for deletion, not just a label).
|
|
197
241
|
function evaluateUpgrade({ rows = [], errors = [], core } = {}) {
|
|
198
242
|
const blocking = rows.filter((r) => r.enabled && !r.satisfied);
|
|
199
243
|
const warnings = rows.filter((r) => !r.enabled && !r.satisfied);
|
|
200
|
-
|
|
244
|
+
const deprecations = rows.filter((r) => r.maintenance && SUNSET_STATUSES.includes(r.maintenance.status));
|
|
245
|
+
const expired = deprecations.filter((r) => isPastRemoveAfter(r.maintenance, core));
|
|
246
|
+
const undeclared = rows.filter((r) => !r.maintenance || !r.maintenance.status);
|
|
247
|
+
return { ok: blocking.length === 0, blocking, warnings, deprecations, expired, undeclared, rows, errors, core };
|
|
201
248
|
}
|
|
202
249
|
|
|
203
250
|
// ---------------------------------------------------------------------------
|
|
@@ -261,12 +308,20 @@ function gatherUpgrade({ modulesDir = MODULES_DIR, core = coreVersion(), env = p
|
|
|
261
308
|
try { enabled = resolveModules({ neutral, instance, env, envPrefix: prefix, registry }); }
|
|
262
309
|
catch { /* a malformed config shouldn't crash the pre-check; treat all as disabled */ }
|
|
263
310
|
|
|
311
|
+
// Maintenance posture travels with the row (ADR 0166) so both the pre-check
|
|
312
|
+
// printout and the real bump can say WHO owns a module that no longer fits —
|
|
313
|
+
// an incompatibility with no owner named is the orphaning failure itself.
|
|
314
|
+
const upkeep = (manifest) => ({
|
|
315
|
+
maintenance: (manifest && manifest.maintenance) || null,
|
|
316
|
+
maintainer: (manifest && manifest.maintainer) || null,
|
|
317
|
+
});
|
|
318
|
+
|
|
264
319
|
const rows = [];
|
|
265
320
|
for (const { key, manifest } of loaded) {
|
|
266
|
-
rows.push({ key, coreVersion: manifest.coreVersion, enabled: enabled[key] === true, satisfied: true });
|
|
321
|
+
rows.push({ key, coreVersion: manifest.coreVersion, enabled: enabled[key] === true, satisfied: true, ...upkeep(manifest) });
|
|
267
322
|
}
|
|
268
323
|
for (const { key, manifest, reason } of skippedFull) {
|
|
269
|
-
rows.push({ key, coreVersion: manifest ? manifest.coreVersion : null, enabled: enabled[key] === true, satisfied: false, reason });
|
|
324
|
+
rows.push({ key, coreVersion: manifest ? manifest.coreVersion : null, enabled: enabled[key] === true, satisfied: false, reason, ...upkeep(manifest) });
|
|
270
325
|
}
|
|
271
326
|
rows.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
272
327
|
return { rows, errors, core };
|
|
@@ -320,6 +375,29 @@ function checkModulePublishability(key, { modulesDir = MODULES_DIR } = {}) {
|
|
|
320
375
|
'AGPL-3.0 (inbound = outbound, ADR 0107 §4)',
|
|
321
376
|
});
|
|
322
377
|
}
|
|
378
|
+
|
|
379
|
+
// ADR 0107 §7 / ADR 0166 — the maintenance gate. Acceptance into the shared
|
|
380
|
+
// catalog is a commitment to keep the module true as the core advances, so
|
|
381
|
+
// the module must SAY who is committing before it can be filed for review.
|
|
382
|
+
// The shape is already schema-validated above (a `maintained` module must
|
|
383
|
+
// name a maintainer; a sunset one must state its path); what this adds is
|
|
384
|
+
// that the block cannot simply be absent. Refusing here — at submit time —
|
|
385
|
+
// is what keeps the catalog from filling with entries nobody owns.
|
|
386
|
+
const maint = manifestRaw.maintenance;
|
|
387
|
+
if (!maint || !maint.status) {
|
|
388
|
+
findings.push({
|
|
389
|
+
kind: 'maintenance',
|
|
390
|
+
detail: 'module.json declares no `maintenance.status` — an upstreamed module must state its upkeep ' +
|
|
391
|
+
'posture (ADR 0107 §7): "core-maintained" (the core team owns it) or "maintained" with a named ' +
|
|
392
|
+
'`maintainer`. A module nobody owns rots silently as the core advances.',
|
|
393
|
+
});
|
|
394
|
+
} else if (SUNSET_STATUSES.includes(maint.status)) {
|
|
395
|
+
findings.push({
|
|
396
|
+
kind: 'maintenance',
|
|
397
|
+
detail: `module.json declares maintenance.status "${maint.status}" — a module already on its way out ` +
|
|
398
|
+
'cannot be submitted INTO the catalog. Upstream a maintained module, or its successor.',
|
|
399
|
+
});
|
|
400
|
+
}
|
|
323
401
|
}
|
|
324
402
|
|
|
325
403
|
// Classify every file AS IF it already lived at its canonical modules/<key>/
|
|
@@ -428,13 +506,28 @@ function cmdUpgrade(_args, { log = console.log, errlog = console.error, gather =
|
|
|
428
506
|
const state = r.enabled ? 'enabled' : 'disabled';
|
|
429
507
|
let note = '';
|
|
430
508
|
if (!r.satisfied) {
|
|
509
|
+
// A DISABLED mismatch is harmless to THIS boot, but it is not harmless to
|
|
510
|
+
// the catalog — it is a rotted entry with an owner who needs telling
|
|
511
|
+
// (ADR 0166). Name that owner right here rather than shrugging at it.
|
|
431
512
|
note = ` — coreVersion ${r.coreVersion || '(none)'} not satisfied by core ${verdict.core}` +
|
|
432
|
-
(r.enabled ? ' — will NOT mount' : '
|
|
513
|
+
(r.enabled ? ' — will NOT mount' : ' — disabled, so it does not block this boot, but it is BROKEN if enabled') +
|
|
514
|
+
`\n ↳ ${formatMaintenanceHint(r)}`;
|
|
433
515
|
}
|
|
434
516
|
log(` ${mark} ${r.key.padEnd(18)} ${String(r.coreVersion || '').padEnd(10)} ${state}${note}`);
|
|
435
517
|
}
|
|
436
518
|
for (const e of data.errors) errlog(` ! ${e.dir}: ${(e.errors || []).join('; ')}`);
|
|
437
519
|
|
|
520
|
+
// Deprecation roll-up (ADR 0166) — separate from compatibility: a module can
|
|
521
|
+
// satisfy the core perfectly and still be on its way out.
|
|
522
|
+
for (const d of verdict.deprecations) {
|
|
523
|
+
const overdue = verdict.expired.includes(d);
|
|
524
|
+
errlog(` ${overdue ? '✗' : '!'} ${d.key}: ${d.maintenance.status}${overdue ? ` — PAST removeAfter ${d.maintenance.removeAfter} (core is ${verdict.core}); it is due for removal` : ''}`);
|
|
525
|
+
errlog(` ↳ ${formatMaintenanceHint(d)}`);
|
|
526
|
+
}
|
|
527
|
+
if (verdict.undeclared.length) {
|
|
528
|
+
errlog(` ! ${verdict.undeclared.length} module(s) declare no \`maintenance\` block — nobody is on the hook for them as the core advances (ADR 0166): ${verdict.undeclared.map((r) => r.key).join(', ')}`);
|
|
529
|
+
}
|
|
530
|
+
|
|
438
531
|
if (!verdict.ok) {
|
|
439
532
|
errlog(`\nREFUSING: ${verdict.blocking.length} enabled module(s) incompatible with core ${verdict.core}: ${verdict.blocking.map((b) => b.key).join(', ')}.`);
|
|
440
533
|
errlog(`Widen each module's coreVersion to a range that includes ${verdict.core} (or hold the core upgrade) before proceeding.`);
|
|
@@ -459,7 +552,7 @@ function cmdCheck(args, { log = console.log, errlog = console.error, check = che
|
|
|
459
552
|
|
|
460
553
|
log(`bongos module check — upstreaming pre-check for "${key}"`);
|
|
461
554
|
if (result.ok) {
|
|
462
|
-
log(' ✓ AGPL/publish pre-check passed — no denied content, license is AGPL-3.0.');
|
|
555
|
+
log(' ✓ AGPL/publish pre-check passed — no denied content, license is AGPL-3.0, upkeep is declared.');
|
|
463
556
|
} else {
|
|
464
557
|
for (const f of result.findings) errlog(` ✗ ${f.kind}: ${f.detail}`);
|
|
465
558
|
}
|
|
@@ -582,6 +675,7 @@ async function main(argv) {
|
|
|
582
675
|
module.exports = {
|
|
583
676
|
main, cmdNew, cmdUpgrade, cmdCheck, cmdSubmit, printHelp,
|
|
584
677
|
scaffoldModule, gatherUpgrade, evaluateUpgrade, buildManifest,
|
|
678
|
+
formatMaintenanceHint, isPastRemoveAfter,
|
|
585
679
|
checkModulePublishability, recordSignOff, readSignOff, DCO_STATEMENT,
|
|
586
680
|
listModuleFiles, packageModuleForSubmission,
|
|
587
681
|
routeTemplate, claudeMdTemplate, titleize, camel, coreVersion,
|
package/scripts/gds/upgrade.js
CHANGED
|
@@ -141,18 +141,59 @@ function vendorTarball({ instanceDir, fromTgz, targetVersion }, fsImpl = fs) {
|
|
|
141
141
|
|
|
142
142
|
// Lightweight module-compat pre-check reusing module.js against the TARGET core version. Best-effort:
|
|
143
143
|
// a vanilla consumer has no modules/ (trivially OK); any failure downgrades to a warning, never blocks
|
|
144
|
-
// the MVP bump. Returns { ok, blocking:[], note }.
|
|
144
|
+
// the MVP bump. Returns { ok, blocking:[], warnings:[], deprecations:[], expired:[], note }.
|
|
145
|
+
//
|
|
146
|
+
// WARNINGS ARE NOT OPTIONAL OUTPUT (ADR 0166 / task 1001773). This function used to return only
|
|
147
|
+
// `blocking`, so a DISABLED module whose coreVersion the target no longer satisfies vanished from the
|
|
148
|
+
// bump entirely. That is precisely the catalog-rot case ADR 0107 §7 names: an accepted catalog module
|
|
149
|
+
// ships `default: false` (§6), so EVERY rotted catalog entry is disabled by construction and every one
|
|
150
|
+
// of them was invisible here. The bump still doesn't BLOCK on them — a disabled module breaks nothing
|
|
151
|
+
// at boot — but it must SAY so, with the maintainer named, or the incompatibility is discovered by
|
|
152
|
+
// whoever next tries to enable it.
|
|
145
153
|
function preflightModules({ instanceDir, targetVersion }, deps = {}) {
|
|
154
|
+
const empty = { blocking: [], warnings: [], deprecations: [], expired: [] };
|
|
146
155
|
const modulesDir = path.join(instanceDir, 'modules');
|
|
147
|
-
if (!fs.existsSync(modulesDir)) return { ok: true,
|
|
156
|
+
if (!fs.existsSync(modulesDir)) return { ok: true, ...empty, note: 'no modules/ — nothing to pre-check' };
|
|
148
157
|
try {
|
|
149
158
|
const mod = deps.moduleLib || require('./module');
|
|
150
159
|
const data = mod.gatherUpgrade({ modulesDir, core: targetVersion });
|
|
151
160
|
const verdict = mod.evaluateUpgrade(data);
|
|
152
|
-
|
|
161
|
+
const rotted = (verdict.warnings || []).length;
|
|
162
|
+
const note = !verdict.ok
|
|
163
|
+
? 'incompatible enabled module(s)'
|
|
164
|
+
: rotted
|
|
165
|
+
? `all enabled modules satisfy the target, but ${rotted} disabled module(s) do NOT`
|
|
166
|
+
: 'all enabled modules satisfy the target';
|
|
167
|
+
return {
|
|
168
|
+
ok: verdict.ok,
|
|
169
|
+
blocking: verdict.blocking || [],
|
|
170
|
+
warnings: verdict.warnings || [],
|
|
171
|
+
deprecations: verdict.deprecations || [],
|
|
172
|
+
expired: verdict.expired || [],
|
|
173
|
+
note,
|
|
174
|
+
};
|
|
153
175
|
} catch (e) {
|
|
154
|
-
return { ok: true,
|
|
176
|
+
return { ok: true, ...empty, note: `module pre-check skipped (${e.message})` };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Print the non-blocking half of the module pre-check: rotted-but-disabled modules and
|
|
181
|
+
// self-declared deprecations, each with its maintenance hint so the reader knows who to chase.
|
|
182
|
+
// Split out of runUpgrade so it is testable without driving a whole bump.
|
|
183
|
+
function reportModulePreflight(mods, targetVersion, { log = console.log, err = console.error, hint } = {}) {
|
|
184
|
+
const fmt = hint || ((row) => { try { return require('./module').formatMaintenanceHint(row); } catch { return ''; } });
|
|
185
|
+
const lines = [];
|
|
186
|
+
for (const w of mods.warnings || []) {
|
|
187
|
+
lines.push(` ! module "${w.key}" is DISABLED here, so it does not block this bump — but its coreVersion ${w.coreVersion || '(none)'} is not satisfied by core ${targetVersion}: enabling it would silently fail to mount.`);
|
|
188
|
+
lines.push(` ↳ ${fmt(w)}`);
|
|
189
|
+
}
|
|
190
|
+
for (const d of mods.deprecations || []) {
|
|
191
|
+
const overdue = (mods.expired || []).includes(d);
|
|
192
|
+
lines.push(` ! module "${d.key}" is ${d.maintenance.status}${overdue ? ` and PAST its removeAfter ${d.maintenance.removeAfter} (core ${targetVersion}) — it is due for removal` : ''}.`);
|
|
193
|
+
lines.push(` ↳ ${fmt(d)}`);
|
|
155
194
|
}
|
|
195
|
+
for (const l of lines) err(l);
|
|
196
|
+
return lines;
|
|
156
197
|
}
|
|
157
198
|
|
|
158
199
|
function npmInstall(instanceDir, run = spawnSync) {
|
|
@@ -582,6 +623,10 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
582
623
|
}
|
|
583
624
|
const mods = preflightModules({ instanceDir, targetVersion }, deps);
|
|
584
625
|
log(` ✓ pre-flight: ${mods.note}`);
|
|
626
|
+
// Surface the non-blocking findings (rotted-but-disabled + deprecated modules) BEFORE the
|
|
627
|
+
// blocking check returns, so they are reported on a refused bump too — a refusal is exactly
|
|
628
|
+
// when someone is reading this output (ADR 0166).
|
|
629
|
+
(deps.reportModulePreflight || reportModulePreflight)(mods, targetVersion, { log, err });
|
|
585
630
|
if (!mods.ok) return { ok: false, error: `module pre-check failed: ${mods.blocking.map((b) => b.key).join(', ')} incompatible with core ${targetVersion}` };
|
|
586
631
|
|
|
587
632
|
// 1b. mandatory post-restart health check (audit H9 / task 1002222, finding F5). A bump that
|
|
@@ -822,7 +867,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
822
867
|
module.exports = {
|
|
823
868
|
runUpgrade, parseArgs, main, rollback,
|
|
824
869
|
versionFromTgzPath, versionFromDep, readInstalledCoreVersion, readPinnedCoreVersion, writePin, writeRegistryPin, restorePin,
|
|
825
|
-
vendorTarball, gitTreeClean, preflightModules, npmInstall, runMigrate, regenerateApiArtifacts, regenerateNavDocs, NAV_WHOLE_FILE_GENERATORS, missingDocAssets, healDocAssets, DOC_ASSET_GENERATORS, restartService, recordLedger, pollHealth,
|
|
870
|
+
vendorTarball, gitTreeClean, preflightModules, reportModulePreflight, npmInstall, runMigrate, regenerateApiArtifacts, regenerateNavDocs, NAV_WHOLE_FILE_GENERATORS, missingDocAssets, healDocAssets, DOC_ASSET_GENERATORS, restartService, recordLedger, pollHealth,
|
|
826
871
|
readManifest, resolveReferenceManifest, verifyInstalledPin, // task 1002216 (audit H8) — integrity-pin verification
|
|
827
872
|
CORE_PKG, ARTIFACT,
|
|
828
873
|
};
|
|
@@ -99,6 +99,10 @@ function listModules() {
|
|
|
99
99
|
origin: manifest.origin || null,
|
|
100
100
|
license: manifest.license || null,
|
|
101
101
|
maintainer: manifest.maintainer || null,
|
|
102
|
+
// Upkeep posture (ADR 0107 §7 / ADR 0166) — the catalog's job is to show
|
|
103
|
+
// who keeps a module true as the core advances, so a deprecated or
|
|
104
|
+
// unowned entry reads as such instead of looking like any other module.
|
|
105
|
+
maintenance: manifest.maintenance || null,
|
|
102
106
|
};
|
|
103
107
|
}).sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
104
108
|
}
|
package/src/module-api.js
CHANGED
|
@@ -555,8 +555,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
555
555
|
// landed since 1.19.62 with no explicit bump. run 31553743979. (task 1002620)
|
|
556
556
|
// 1.19.64 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
557
557
|
// landed since 1.19.63 with no explicit bump. run 31560059785. (task 1002620)
|
|
558
|
+
// 1.19.65 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
559
|
+
// landed since 1.19.64 with no explicit bump. run 31562193292. (task 1002620)
|
|
560
|
+
// 1.19.66 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
561
|
+
// landed since 1.19.65 with no explicit bump. run 31599966075. (task 1002620)
|
|
558
562
|
// ---------------------------------------------------------------------------
|
|
559
|
-
const CORE_VERSION = '1.19.
|
|
563
|
+
const CORE_VERSION = '1.19.66'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
560
564
|
|
|
561
565
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
562
566
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -18,6 +18,15 @@
|
|
|
18
18
|
// "origin": "…", // OPTIONAL. Origin instance/project this module was built in.
|
|
19
19
|
// "license": "…", // OPTIONAL. e.g. "AGPL-3.0" — the license this module is offered under.
|
|
20
20
|
// "maintainer": "…", // OPTIONAL. Who to credit/contact if this module is upstreamed.
|
|
21
|
+
// "maintenance": { // OPTIONAL. Upkeep posture (ADR 0107 §7 / ADR 0166) — who keeps
|
|
22
|
+
// "status": "maintained", // REQUIRED in-block: core-maintained | maintained | deprecated | orphaned.
|
|
23
|
+
// "since": "1.8.0", // OPTIONAL. Core version the status took effect.
|
|
24
|
+
// "removeAfter": "1.12.0", // OPTIONAL. Core version after which it may be deleted.
|
|
25
|
+
// "successor": "econ-v2", // OPTIONAL. Module key that replaces it.
|
|
26
|
+
// "note": "…" // OPTIONAL. The deprecation path in one sentence.
|
|
27
|
+
// }, // this module true as the core advances, and what happens if
|
|
28
|
+
// // nobody does. `maintained` needs a named `maintainer`;
|
|
29
|
+
// // `deprecated`/`orphaned` need a `successor` or a `note`.
|
|
21
30
|
// "contributes": { // REQUIRED object (may be empty). What the module mounts:
|
|
22
31
|
// "routes": ["box"], // route-factory keys (mounted behind kernel-composed auth)
|
|
23
32
|
// "rooms": ["WorldRoom"], // Colyseus room classes (realtime seam)
|
|
@@ -40,7 +49,7 @@
|
|
|
40
49
|
// validateManifest(obj) -> { valid, errors[], manifest } — pure, no I/O.
|
|
41
50
|
// assertValidManifest(obj) throws on the first failure (loader convenience).
|
|
42
51
|
|
|
43
|
-
const { isValidRange } = require('./semver');
|
|
52
|
+
const { isValidRange, parseVersion } = require('./semver');
|
|
44
53
|
|
|
45
54
|
const KEY_RE = /^[a-z][a-z0-9-]*$/;
|
|
46
55
|
|
|
@@ -54,6 +63,23 @@ const ALLOWED_CONTRIBUTES = [...ARRAY_OF_STRING_KEYS, 'webSurfaces', 'migrations
|
|
|
54
63
|
// Top-level attribution fields (ADR 0107 §2) — optional, portable strings.
|
|
55
64
|
const ATTRIBUTION_KEYS = ['author', 'origin', 'license', 'maintainer'];
|
|
56
65
|
|
|
66
|
+
// The `maintenance` block (ADR 0107 §7, decided in ADR 0166). A module pinned to
|
|
67
|
+
// an old coreVersion rots as the core advances, so every module states — in the
|
|
68
|
+
// manifest, where the upgrade pre-check can read it — WHO keeps it true and what
|
|
69
|
+
// happens if nobody does. The four statuses are the whole policy:
|
|
70
|
+
// core-maintained the core team owns it; a core change that breaks it is the
|
|
71
|
+
// core's problem to fix before shipping.
|
|
72
|
+
// maintained a named `maintainer` owns it (required — an unnamed owner
|
|
73
|
+
// IS the orphaning failure this field exists to prevent).
|
|
74
|
+
// deprecated on the way out; needs a path (`successor` and/or `note`),
|
|
75
|
+
// optionally a `removeAfter` core version.
|
|
76
|
+
// orphaned nobody owns it. Still needs a `note`/`successor` so the
|
|
77
|
+
// next reader knows the situation, not just the label.
|
|
78
|
+
const MAINTENANCE_STATUSES = ['core-maintained', 'maintained', 'deprecated', 'orphaned'];
|
|
79
|
+
const MAINTENANCE_KEYS = ['status', 'since', 'removeAfter', 'successor', 'note'];
|
|
80
|
+
// Statuses that describe a module on its way out — these owe a deprecation path.
|
|
81
|
+
const SUNSET_STATUSES = ['deprecated', 'orphaned'];
|
|
82
|
+
|
|
57
83
|
const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
|
|
58
84
|
const isStr = (v) => typeof v === 'string' && v.length > 0;
|
|
59
85
|
const isStrArray = (v) => Array.isArray(v) && v.every(isStr);
|
|
@@ -93,6 +119,44 @@ function validateManifest(obj) {
|
|
|
93
119
|
if (k in obj && !isStr(obj[k])) E(`${k}: must be a non-empty string when present`);
|
|
94
120
|
}
|
|
95
121
|
|
|
122
|
+
// --- optional maintenance block (ADR 0107 §7 / ADR 0166) ---
|
|
123
|
+
// Closed key set (like contributes) so a typo'd "removeafter" fails loud rather
|
|
124
|
+
// than silently declaring no removal date at all.
|
|
125
|
+
if ('maintenance' in obj) {
|
|
126
|
+
const m = obj.maintenance;
|
|
127
|
+
if (!isObj(m)) {
|
|
128
|
+
E('maintenance: must be an object (e.g. { "status": "maintained" })');
|
|
129
|
+
} else {
|
|
130
|
+
for (const k of Object.keys(m)) {
|
|
131
|
+
if (!MAINTENANCE_KEYS.includes(k)) E(`maintenance.${k}: unknown key (allowed: ${MAINTENANCE_KEYS.join(', ')})`);
|
|
132
|
+
}
|
|
133
|
+
if (!isStr(m.status)) E(`maintenance.status: required — one of ${MAINTENANCE_STATUSES.join(', ')}`);
|
|
134
|
+
else if (!MAINTENANCE_STATUSES.includes(m.status)) {
|
|
135
|
+
E(`maintenance.status: "${m.status}" is not one of ${MAINTENANCE_STATUSES.join(', ')}`);
|
|
136
|
+
}
|
|
137
|
+
for (const k of ['since', 'removeAfter']) {
|
|
138
|
+
if (k in m && !parseVersion(m[k])) E(`maintenance.${k}: must be an exact core version "X.Y.Z" when present`);
|
|
139
|
+
}
|
|
140
|
+
if ('successor' in m && (!isStr(m.successor) || !KEY_RE.test(m.successor))) {
|
|
141
|
+
E('successor: must be a module key (lowercase kebab-case) when present');
|
|
142
|
+
}
|
|
143
|
+
if ('note' in m && !isStr(m.note)) E('maintenance.note: must be a non-empty string when present');
|
|
144
|
+
|
|
145
|
+
// The two rules that make the block load-bearing rather than decorative:
|
|
146
|
+
// a "maintained" module must NAME its maintainer, and a module on its way
|
|
147
|
+
// out must state the path out. Everything else above is shape-checking.
|
|
148
|
+
if (m.status === 'maintained' && !isStr(obj.maintainer)) {
|
|
149
|
+
E('maintenance.status "maintained" requires a named top-level `maintainer` (ADR 0166) — use "orphaned" if nobody owns it');
|
|
150
|
+
}
|
|
151
|
+
if (SUNSET_STATUSES.includes(m.status) && !isStr(m.successor) && !isStr(m.note)) {
|
|
152
|
+
E(`maintenance.status "${m.status}" requires a deprecation path — a "successor" module key and/or a "note" (ADR 0166)`);
|
|
153
|
+
}
|
|
154
|
+
if ('removeAfter' in m && !SUNSET_STATUSES.includes(m.status)) {
|
|
155
|
+
E(`maintenance.removeAfter only applies to a ${SUNSET_STATUSES.join('/')} module (status is "${m.status}")`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
96
160
|
// --- contributes (required object) ---
|
|
97
161
|
if (!isObj(obj.contributes)) {
|
|
98
162
|
E('contributes: required object (use {} if the module contributes nothing)');
|
|
@@ -152,4 +216,7 @@ function assertValidManifest(obj, label = 'module.json') {
|
|
|
152
216
|
return obj;
|
|
153
217
|
}
|
|
154
218
|
|
|
155
|
-
module.exports = {
|
|
219
|
+
module.exports = {
|
|
220
|
+
validateManifest, assertValidManifest, KEY_RE, ALLOWED_CONTRIBUTES, ARRAY_OF_STRING_KEYS, ATTRIBUTION_KEYS,
|
|
221
|
+
MAINTENANCE_STATUSES, MAINTENANCE_KEYS, SUNSET_STATUSES,
|
|
222
|
+
};
|
package/src/platform-server.js
CHANGED
|
@@ -143,6 +143,15 @@ function buildPlatformApp() {
|
|
|
143
143
|
if (!html) return next();
|
|
144
144
|
res.type('html').send(html);
|
|
145
145
|
});
|
|
146
|
+
// The owner's my-projects surface (goal 1000059, task 1002693) — the durable
|
|
147
|
+
// place to find, watch, and manage one's instances after the /create tab
|
|
148
|
+
// closes. A sibling landing page, branding-injected like /create.
|
|
149
|
+
app.get('/projects', (req, res, next) => {
|
|
150
|
+
if (!isApexHost(req)) return next();
|
|
151
|
+
const html = landingPage('projects.html');
|
|
152
|
+
if (!html) return next();
|
|
153
|
+
res.type('html').send(html);
|
|
154
|
+
});
|
|
146
155
|
const landingAssets = express.static(LANDING_DIR, { index: false, fallthrough: true });
|
|
147
156
|
app.use((req, res, next) => {
|
|
148
157
|
if (!isApexHost(req)) return next();
|
package/tests/module_cli.mjs
CHANGED
|
@@ -418,3 +418,129 @@ test('`bongos module submit` is documented in help / dispatches to module.js sub
|
|
|
418
418
|
assert.ok(lines.some((l) => /module submit/.test(l)));
|
|
419
419
|
assert.equal(await mod.main(['node', 'module.js', 'submit']), 2, 'no key => usage error');
|
|
420
420
|
});
|
|
421
|
+
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
// Maintenance & deprecation policy (ADR 0107 §7 / ADR 0166, task 1001773)
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
|
|
426
|
+
test('evaluateUpgrade: sorts self-declared deprecations, and flags the ones the core has passed removeAfter', () => {
|
|
427
|
+
const v = mod.evaluateUpgrade({
|
|
428
|
+
core: '1.13.0',
|
|
429
|
+
rows: [
|
|
430
|
+
{ key: 'healthy', coreVersion: '^1.0.0', enabled: true, satisfied: true, maintenance: { status: 'core-maintained' } },
|
|
431
|
+
{ key: 'sunset', coreVersion: '^1.0.0', enabled: false, satisfied: true, maintenance: { status: 'deprecated', successor: 'newer', removeAfter: '1.12.0' } },
|
|
432
|
+
{ key: 'sunset-later', coreVersion: '^1.0.0', enabled: false, satisfied: true, maintenance: { status: 'deprecated', note: 'going', removeAfter: '1.20.0' } },
|
|
433
|
+
{ key: 'nobody', coreVersion: '^1.0.0', enabled: false, satisfied: true, maintenance: { status: 'orphaned', note: 'unowned' } },
|
|
434
|
+
],
|
|
435
|
+
});
|
|
436
|
+
assert.equal(v.ok, true, 'a deprecation is not a compatibility failure — it must not block a bump');
|
|
437
|
+
assert.deepEqual(v.deprecations.map((d) => d.key), ['sunset', 'sunset-later', 'nobody']);
|
|
438
|
+
assert.deepEqual(v.expired.map((d) => d.key), ['sunset'], 'core 1.13.0 is past removeAfter 1.12.0 but not 1.20.0');
|
|
439
|
+
assert.deepEqual(v.undeclared, []);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('evaluateUpgrade: a module with no maintenance block is reported as undeclared (nobody is on the hook)', () => {
|
|
443
|
+
const v = mod.evaluateUpgrade({
|
|
444
|
+
core: '1.0.0',
|
|
445
|
+
rows: [
|
|
446
|
+
{ key: 'owned', coreVersion: '^1.0.0', enabled: true, satisfied: true, maintenance: { status: 'core-maintained' } },
|
|
447
|
+
{ key: 'orphan-risk', coreVersion: '^1.0.0', enabled: false, satisfied: true },
|
|
448
|
+
],
|
|
449
|
+
});
|
|
450
|
+
assert.deepEqual(v.undeclared.map((r) => r.key), ['orphan-risk']);
|
|
451
|
+
assert.equal(v.ok, true, 'an undeclared posture is a reporting gap, not a compatibility failure');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test('formatMaintenanceHint: names the owner, or says plainly that there is none', () => {
|
|
455
|
+
assert.match(mod.formatMaintenanceHint({ key: 'x' }), /no maintenance declared/);
|
|
456
|
+
assert.match(
|
|
457
|
+
mod.formatMaintenanceHint({ key: 'x', maintainer: 'Ada <ada@example.com>', maintenance: { status: 'maintained' } }),
|
|
458
|
+
/maintainer Ada <ada@example\.com>/,
|
|
459
|
+
);
|
|
460
|
+
assert.match(mod.formatMaintenanceHint({ key: 'x', maintenance: { status: 'maintained' } }), /unnamed!/);
|
|
461
|
+
const sunset = mod.formatMaintenanceHint({ key: 'x', maintenance: { status: 'deprecated', successor: 'y', removeAfter: '1.12.0', note: 'use y' } });
|
|
462
|
+
assert.match(sunset, /superseded by "y"/);
|
|
463
|
+
assert.match(sunset, /removable after core 1\.12\.0/);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test('gatherUpgrade: carries each module\'s maintenance posture onto its row', () => {
|
|
467
|
+
loader._reset();
|
|
468
|
+
const root = tmpModulesRoot();
|
|
469
|
+
mod.scaffoldModule({ key: 'econ', modulesDir: root, core: '1.0.0' });
|
|
470
|
+
const data = mod.gatherUpgrade({ modulesDir: root, core: '1.0.0', env: {}, neutral: {}, instance: {}, prefix: 'CLOUDBONGOS' });
|
|
471
|
+
const row = data.rows.find((r) => r.key === 'econ');
|
|
472
|
+
assert.equal(row.maintenance.status, 'maintained', 'the scaffolder stamps an honest default');
|
|
473
|
+
assert.ok(row.maintainer, 'the maintainer travels with the row so the hint can name them');
|
|
474
|
+
loader._reset();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test('cmdUpgrade: a DISABLED incompatible module is reported with its owner, not silently dropped', () => {
|
|
478
|
+
// The catalog-rot case: an accepted module ships default:false (ADR 0107 §6), so
|
|
479
|
+
// every rotted catalog entry is disabled by construction. Exit stays 0 — it breaks
|
|
480
|
+
// nothing at boot — but it must be visible and attributed.
|
|
481
|
+
const lines = [];
|
|
482
|
+
const sink = { log: (s) => lines.push(String(s)), errlog: (s) => lines.push(String(s)) };
|
|
483
|
+
const code = mod.cmdUpgrade([], {
|
|
484
|
+
...sink,
|
|
485
|
+
gather: () => ({
|
|
486
|
+
core: '2.0.0',
|
|
487
|
+
errors: [],
|
|
488
|
+
rows: [{
|
|
489
|
+
key: 'stale-catalog-entry', coreVersion: '^1.0.0', enabled: false, satisfied: false,
|
|
490
|
+
maintainer: 'Ada <ada@example.com>', maintenance: { status: 'maintained' },
|
|
491
|
+
}],
|
|
492
|
+
}),
|
|
493
|
+
});
|
|
494
|
+
assert.equal(code, 0, 'a disabled mismatch must not block');
|
|
495
|
+
const out = lines.join('\n');
|
|
496
|
+
assert.match(out, /stale-catalog-entry/);
|
|
497
|
+
assert.match(out, /BROKEN if enabled/);
|
|
498
|
+
assert.match(out, /maintainer Ada <ada@example\.com>/, 'the owner is named so someone can be told');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test('cmdUpgrade: reports an expired deprecation and modules with no declared upkeep', () => {
|
|
502
|
+
const lines = [];
|
|
503
|
+
const sink = { log: (s) => lines.push(String(s)), errlog: (s) => lines.push(String(s)) };
|
|
504
|
+
const code = mod.cmdUpgrade([], {
|
|
505
|
+
...sink,
|
|
506
|
+
gather: () => ({
|
|
507
|
+
core: '1.13.0',
|
|
508
|
+
errors: [],
|
|
509
|
+
rows: [
|
|
510
|
+
{ key: 'sunset', coreVersion: '^1.0.0', enabled: false, satisfied: true, maintenance: { status: 'deprecated', successor: 'newer', removeAfter: '1.12.0' } },
|
|
511
|
+
{ key: 'unowned', coreVersion: '^1.0.0', enabled: false, satisfied: true },
|
|
512
|
+
],
|
|
513
|
+
}),
|
|
514
|
+
});
|
|
515
|
+
assert.equal(code, 0);
|
|
516
|
+
const out = lines.join('\n');
|
|
517
|
+
assert.match(out, /sunset: deprecated/);
|
|
518
|
+
assert.match(out, /PAST removeAfter 1\.12\.0/);
|
|
519
|
+
assert.match(out, /declare no `maintenance` block/);
|
|
520
|
+
assert.match(out, /unowned/);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test('checkModulePublishability: refuses a module that declares no maintenance posture', () => {
|
|
524
|
+
const root = tmpModulesRoot();
|
|
525
|
+
const { dir } = mod.scaffoldModule({ key: 'unowned', modulesDir: root, core: '1.0.0' });
|
|
526
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'module.json'), 'utf8'));
|
|
527
|
+
delete manifest.maintenance;
|
|
528
|
+
fs.writeFileSync(path.join(dir, 'module.json'), JSON.stringify(manifest, null, 2));
|
|
529
|
+
const r = mod.checkModulePublishability('unowned', { modulesDir: root });
|
|
530
|
+
assert.equal(r.ok, false, 'acceptance is a commitment to upkeep — it must be stated');
|
|
531
|
+
assert.ok(r.findings.some((f) => f.kind === 'maintenance' && /core-maintained/.test(f.detail)), JSON.stringify(r.findings));
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test('checkModulePublishability: refuses a module that is ALREADY deprecated or orphaned', () => {
|
|
535
|
+
const root = tmpModulesRoot();
|
|
536
|
+
for (const status of ['deprecated', 'orphaned']) {
|
|
537
|
+
const key = `sunset-${status}`;
|
|
538
|
+
const { dir } = mod.scaffoldModule({ key, modulesDir: root, core: '1.0.0' });
|
|
539
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'module.json'), 'utf8'));
|
|
540
|
+
manifest.maintenance = { status, note: 'on the way out' };
|
|
541
|
+
fs.writeFileSync(path.join(dir, 'module.json'), JSON.stringify(manifest, null, 2));
|
|
542
|
+
const r = mod.checkModulePublishability(key, { modulesDir: root });
|
|
543
|
+
assert.equal(r.ok, false, `${status} must not be submittable INTO the catalog`);
|
|
544
|
+
assert.ok(r.findings.some((f) => f.kind === 'maintenance' && /way out/.test(f.detail)));
|
|
545
|
+
}
|
|
546
|
+
});
|
|
@@ -211,3 +211,68 @@ test('dependencies: a non-object, or a non-string range, is rejected', () => {
|
|
|
211
211
|
assert.ok(r.errors.some((e) => e.startsWith('dependencies')), `error should mention dependencies: ${r.errors}`);
|
|
212
212
|
}
|
|
213
213
|
});
|
|
214
|
+
|
|
215
|
+
// --- maintenance & deprecation policy (ADR 0107 §7 / ADR 0166) ---
|
|
216
|
+
|
|
217
|
+
test('maintenance: omitted is fine (optional — pre-policy manifests still validate)', () => {
|
|
218
|
+
assert.equal(validateManifest(baseManifest()).valid, true);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('maintenance: each of the four statuses validates when its own obligations are met', () => {
|
|
222
|
+
const cases = [
|
|
223
|
+
{ maintenance: { status: 'core-maintained' } },
|
|
224
|
+
{ maintainer: 'A. Builder', maintenance: { status: 'maintained' } },
|
|
225
|
+
{ maintenance: { status: 'deprecated', successor: 'econ-v2', removeAfter: '1.12.0' } },
|
|
226
|
+
{ maintenance: { status: 'orphaned', note: 'Author unreachable since 2026-07; enable at your own risk.' } },
|
|
227
|
+
];
|
|
228
|
+
for (const extra of cases) {
|
|
229
|
+
const r = validateManifest({ ...baseManifest(), ...extra });
|
|
230
|
+
assert.equal(r.valid, true, `${extra.maintenance.status}: ${r.errors.join('; ')}`);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('maintenance: "maintained" without a named top-level maintainer is rejected', () => {
|
|
235
|
+
const r = validateManifest({ ...baseManifest(), maintenance: { status: 'maintained' } });
|
|
236
|
+
assert.equal(r.valid, false, 'an unnamed maintainer IS the orphaning failure the policy prevents');
|
|
237
|
+
assert.ok(r.errors.some((e) => /maintained.*maintainer/.test(e)), r.errors.join('; '));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('maintenance: a sunset status with no deprecation path (no successor, no note) is rejected', () => {
|
|
241
|
+
for (const status of ['deprecated', 'orphaned']) {
|
|
242
|
+
const r = validateManifest({ ...baseManifest(), maintenance: { status } });
|
|
243
|
+
assert.equal(r.valid, false, `${status} must state a path out`);
|
|
244
|
+
assert.ok(r.errors.some((e) => /deprecation path/.test(e)), r.errors.join('; '));
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('maintenance: an unknown status, an unknown key, or a bad version is rejected', () => {
|
|
249
|
+
const bad = [
|
|
250
|
+
{ status: 'someday' },
|
|
251
|
+
{ status: 'core-maintained', removeafter: '1.2.0' },
|
|
252
|
+
{ status: 'core-maintained', since: 'v1.2' },
|
|
253
|
+
{ status: 'deprecated', successor: 'Econ_V2' },
|
|
254
|
+
'core-maintained',
|
|
255
|
+
];
|
|
256
|
+
for (const m of bad) {
|
|
257
|
+
const r = validateManifest({ ...baseManifest(), maintenance: m });
|
|
258
|
+
assert.equal(r.valid, false, `should reject ${JSON.stringify(m)}`);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('maintenance: removeAfter only applies to a module on its way out', () => {
|
|
263
|
+
const r = validateManifest({ ...baseManifest(), maintenance: { status: 'core-maintained', removeAfter: '1.12.0' } });
|
|
264
|
+
assert.equal(r.valid, false);
|
|
265
|
+
assert.ok(r.errors.some((e) => /removeAfter only applies/.test(e)), r.errors.join('; '));
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('every bundled module declares a maintenance posture (the catalog has no unowned entries)', async () => {
|
|
269
|
+
const fsm = (await import('node:fs')).default;
|
|
270
|
+
const modulesDir = path.join(ROOT, 'modules');
|
|
271
|
+
const undeclared = fsm.readdirSync(modulesDir)
|
|
272
|
+
.filter((k) => fsm.existsSync(path.join(modulesDir, k, 'module.json')))
|
|
273
|
+
.filter((k) => {
|
|
274
|
+
const m = JSON.parse(fsm.readFileSync(path.join(modulesDir, k, 'module.json'), 'utf8'));
|
|
275
|
+
return !m.maintenance || !m.maintenance.status;
|
|
276
|
+
});
|
|
277
|
+
assert.deepEqual(undeclared, [], `these modules declare no maintenance.status: ${undeclared.join(', ')}`);
|
|
278
|
+
});
|