@bongos/core 1.19.648 → 1.19.649

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 CHANGED
@@ -2,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.648",
6
- "core_contract": "1.19.648",
7
- "source_commit": "0606a231569b44bdc2521fcee567031ea8ea6f08",
5
+ "core_version": "1.19.649",
6
+ "core_contract": "1.19.649",
7
+ "source_commit": "10ae5631783d03386a93457958ac7846e3ad4b83",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-10T19:06:50.891Z",
9
+ "built_at": "2026-09-10T19:54:28.577Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 470,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2603,
20
- "tree_sha256": "3260c3c3da2ae8a6b6630824444ada3145268a70b9194585231bb3391392db80",
20
+ "tree_sha256": "05f4d97454a0e6f6d8eadf6c13d88c68d8619b32269b20409e5a79505dad4596",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/backlog-review/SKILL.md",
@@ -2772,7 +2772,7 @@
2772
2772
  {
2773
2773
  "path": "docs/module-api-changelog.md",
2774
2774
  "mode": "0000644",
2775
- "sha256": "f646af7c4521017e44e5dfef0b160c18907bfa0f07b5986603593485a34f2742"
2775
+ "sha256": "454e5258c7dccd1013445f290acf0cfbd0cf396c9cb5bbcf8a08b911770bfa07"
2776
2776
  },
2777
2777
  {
2778
2778
  "path": "docs/modules-contract.md",
@@ -7727,12 +7727,12 @@
7727
7727
  {
7728
7728
  "path": "package-lock.json",
7729
7729
  "mode": "0000644",
7730
- "sha256": "83135ce62c34647b4bc57872526cb9f550a9fef2148c585839d57c600763961b"
7730
+ "sha256": "356482b30a64a35a5ae0a670d0b26e0b10dcdd71ae106e8b22fb8f5843282c05"
7731
7731
  },
7732
7732
  {
7733
7733
  "path": "package.json",
7734
7734
  "mode": "0000644",
7735
- "sha256": "a9a2455f79599041389b23a18607666ce74eba9be0199d21fbc62553824463f6"
7735
+ "sha256": "30e3bed231acb41d36332cae243423aee74a09604e87c966908543fefde74b65"
7736
7736
  },
7737
7737
  {
7738
7738
  "path": "public-docs/index.html",
@@ -9487,7 +9487,7 @@
9487
9487
  {
9488
9488
  "path": "src/module-api.js",
9489
9489
  "mode": "0000644",
9490
- "sha256": "dadc321fdd8e6a6f856bb9260c555284b9859361c556d47b14b25df45db900b0"
9490
+ "sha256": "eaa8edfbeec057365723ebfc9d148061b96be50cc37a1ad51b7d050f6e2906c4"
9491
9491
  },
9492
9492
  {
9493
9493
  "path": "src/module-loader/catalog.js",
@@ -9497,7 +9497,7 @@
9497
9497
  {
9498
9498
  "path": "src/module-loader/loader.js",
9499
9499
  "mode": "0000644",
9500
- "sha256": "7c03edea5abee5cce4dfb2f22626e15ee2257ba44f6024e0bf74ff35e8ed0f42"
9500
+ "sha256": "5e417760b49f19d48af3dbd72a90edb53b6a9710ce54ca33052f4fbe86cb9229"
9501
9501
  },
9502
9502
  {
9503
9503
  "path": "src/module-loader/manifest-schema.js",
@@ -9517,7 +9517,7 @@
9517
9517
  {
9518
9518
  "path": "src/module-seams.js",
9519
9519
  "mode": "0000644",
9520
- "sha256": "60cdc1e4ffb22a843a1581909e742b9672fca776bfe5716e72aab77c63df6099"
9520
+ "sha256": "34378efdff13b7e44e38f248026aa927073e0c0c15db670cabaea7aa939c4959"
9521
9521
  },
9522
9522
  {
9523
9523
  "path": "src/modules.js",
@@ -11632,7 +11632,7 @@
11632
11632
  {
11633
11633
  "path": "tests/module_loader.mjs",
11634
11634
  "mode": "0000644",
11635
- "sha256": "24aed55c34771eb1bdaec9a51b1e818549298455d50a9ade96db3cc1f06c034b"
11635
+ "sha256": "5d09d5ccedf777c55509dc98365b46c443848c854158ace66fa17c99ba538737"
11636
11636
  },
11637
11637
  {
11638
11638
  "path": "tests/module_manifest.mjs",
@@ -11827,7 +11827,7 @@
11827
11827
  {
11828
11828
  "path": "tests/platform_boot.mjs",
11829
11829
  "mode": "0000644",
11830
- "sha256": "fb28eed2a6933805d2af7dc0f8d8e966f921c6556683e1f22b440c2f4a32760d"
11830
+ "sha256": "02a3d4540ddecfe530022df0c6cab4f48258b78efd44224c097622e45b8cc0d5"
11831
11831
  },
11832
11832
  {
11833
11833
  "path": "tests/platform_visibility_gate.mjs",
@@ -1745,5 +1745,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1745
1745
  landed since 1.19.646 with no explicit bump. run 34515733973. (task 1002620)
1746
1746
  1.19.648 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1747
1747
  landed since 1.19.647 with no explicit bump. run 34518387614. (task 1002620)
1748
+ 1.19.649 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1749
+ landed since 1.19.648 with no explicit bump. run 34523111338. (task 1002620)
1748
1750
  ---------------------------------------------------------------------------
1749
1751
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.648",
3
+ "version": "1.19.649",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.648",
9
+ "version": "1.19.649",
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.19.648",
3
+ "version": "1.19.649",
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/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.648'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.649'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -20,6 +20,10 @@ const path = require('node:path');
20
20
  const { validateManifest } = require('./manifest-schema');
21
21
  const { satisfies } = require('./semver');
22
22
  const { resolveCoreRoot, resolveInstanceRoot } = require('../instance-config');
23
+ // Required DIRECTLY, never through src/module-api.js: the doorway pulls in auth
24
+ // and would re-form the eval-time cycle coreVersion() exists to avoid.
25
+ // src/module-seams.js is pure (it requires nothing), so this is cycle-safe.
26
+ const { verifyPortsSatisfied } = require('../module-seams');
23
27
 
24
28
  // Collapse roots that resolve to the SAME absolute path, preserving order.
25
29
  // Normalizes via path.resolve so a cwd that arrives with a trailing '.'/'//'
@@ -129,6 +133,39 @@ function discoveredRegistry(opts) {
129
133
  return out;
130
134
  }
131
135
 
136
+ // The ENABLED slice of the discovered set, in discovery order. The one place the
137
+ // `isEnabled` gate is spelled out, so verification and mounting can never disagree
138
+ // about which modules are in play.
139
+ function enabledModules({ isEnabled } = {}, opts) {
140
+ return discovered(opts).loaded.filter(({ key }) => typeof isEnabled !== 'function' || isEnabled(key));
141
+ }
142
+
143
+ // BOOT-TIME seam verification (BV1.R41), run over the ENABLED manifests before a
144
+ // single module router is mounted. THROWS on an unsatisfiable set — a module that
145
+ // consumes a port no enabled module provides, or a port claimed by two of them.
146
+ //
147
+ // Why it throws rather than warns: without it the failure surfaces as resolve()'s
148
+ // runtime throw the first time the seam is used — an arbitrarily long time after
149
+ // boot, on a request path, in a process that reported healthy. ADR 0150's "fail
150
+ // loud, never boot half-wired" is the same argument: refuse at boot, name the
151
+ // module and the port, and let the operator fix the module set.
152
+ //
153
+ // Only the ENABLED set is checked, deliberately. A disabled module's `consumes` is
154
+ // not a wiring error — it is a module that isn't there.
155
+ function verifyEnabledPorts(enabled) {
156
+ const { ok, errors } = verifyPortsSatisfied(enabled.map(({ key, manifest }) => ({
157
+ key,
158
+ provides: manifest.provides || [],
159
+ consumes: manifest.consumes || [],
160
+ })));
161
+ if (ok) return;
162
+ throw new Error([
163
+ '[module-loader] unsatisfiable module wiring — refusing to boot half-wired:',
164
+ ...errors.map((e) => ` - ${e}`),
165
+ 'Fix the enabled module set (config/modules.json, or a <PREFIX>_MODULE_<KEY> env override) so every consumed port has exactly one provider.',
166
+ ].join('\n'));
167
+ }
168
+
132
169
  // Mount each discovered + ENABLED module's route factories onto `router`. A
133
170
  // module declares contributes.routes:["box"] → factory at modules/<key>/routes/box.js
134
171
  // exporting a function returning an express Router. Auth model: audit is composed
@@ -136,10 +173,18 @@ function discoveredRegistry(opts) {
136
173
  // its required rank via api.requireBuilder / api.requireRank (public read routes
137
174
  // need no gate). The BV1.R44 fitness check enforces that every write route is
138
175
  // rank-gated. Returns the count mounted. No-op when nothing is discovered/enabled.
176
+ //
177
+ // Verifies the seam wiring FIRST (verifyEnabledPorts, above). This is the boot hook
178
+ // the check belongs on because it is the one hook every server that mounts modules
179
+ // runs — the live API, platform-server, and the test harness alike — so the
180
+ // verification cannot be skipped by adding an entrypoint, the way a separately
181
+ // called check could be. It runs before the first `require` of a module's route
182
+ // factory, so a half-wired instance never gets as far as loading module code.
139
183
  function mountModuleRoutes(router, { isEnabled } = {}, opts) {
184
+ const enabled = enabledModules({ isEnabled }, opts);
185
+ verifyEnabledPorts(enabled);
140
186
  let mounted = 0;
141
- for (const { key, dir, manifest } of discovered(opts).loaded) {
142
- if (typeof isEnabled === 'function' && !isEnabled(key)) continue;
187
+ for (const { dir, manifest } of enabled) {
143
188
  const routeKeys = (manifest.contributes && manifest.contributes.routes) || [];
144
189
  for (const rk of routeKeys) {
145
190
  const factory = require(path.join(dir, 'routes', `${rk}.js`));
@@ -75,10 +75,14 @@ function listPorts() {
75
75
 
76
76
  // BOOT-TIME (manifest-level) verification, distinct from resolve()'s RUNTIME
77
77
  // throw: given the ENABLED modules' manifests, confirm every consumed port is
78
- // provided by some enabled module, and no port has two providers. The loader
79
- // (BV1.R41) runs this before mounting so a required capability with no provider
80
- // is caught at boot with a clear message not as a surprise throw the first time
81
- // the seam is used. Pure (no registry mutation), so it is safe to run anytime.
78
+ // provided by some enabled module, and no port has two providers. The loader's
79
+ // mountModuleRoutes (BV1.R41) runs this via verifyEnabledPorts before mounting any
80
+ // module router, and THROWS on a non-ok result, so a required capability with no
81
+ // provider is caught at boot with a clear message not as a surprise throw the
82
+ // first time the seam is used. This function itself only REPORTS: it is pure (no
83
+ // registry mutation, no throw), so it is safe to run anytime, and the loader owns
84
+ // the decision to refuse the boot. Wired + regression-pinned by task 1003820,
85
+ // which found it defined, exported, tested, and called by nothing.
82
86
  // manifests: [{ key, provides: [...], consumes: [...] }] → { ok, errors: [...] }.
83
87
  function verifyPortsSatisfied(manifests = []) {
84
88
  const provided = new Map(); // port -> [providerKey,...]
@@ -155,6 +155,71 @@ test('mountModuleRoutes: mounts an enabled module router, skips a disabled one',
155
155
  loader._reset();
156
156
  });
157
157
 
158
+ // ---------------------------------------------------------------------------
159
+ // task 1003820 — the mount hook must VERIFY the seam wiring before it mounts.
160
+ //
161
+ // verifyPortsSatisfied was defined, exported through the doorway, and unit-tested
162
+ // in isolation, but no production code called it: a module consuming a port no
163
+ // enabled module provided booted clean and threw later, on a request path, out of
164
+ // resolve(). These pin the CALL, not the function — see also the spawn-boot half
165
+ // in tests/platform_boot.mjs, which is what actually turns red if the call site is
166
+ // deleted (learning 1000154).
167
+ // ---------------------------------------------------------------------------
168
+
169
+ test('task 1003820: mountModuleRoutes refuses an enabled module consuming an unprovided port', () => {
170
+ loader._reset();
171
+ const factory = "module.exports = () => function moduleRoute(req, res, next) { next(); };";
172
+ const root = fixtureRoot({
173
+ consumer: {
174
+ manifest: validManifest('consumer', { consumes: ['reward'], contributes: { routes: ['main'] } }),
175
+ files: { 'routes/main.js': factory },
176
+ },
177
+ });
178
+ const used = [];
179
+ const fakeRouter = { use: (fn) => used.push(fn) };
180
+ assert.throws(
181
+ () => loader.mountModuleRoutes(fakeRouter, { isEnabled: () => true }, { roots: [root], core: '1.0.0' }),
182
+ (e) => /unsatisfiable module wiring/.test(e.message)
183
+ && /"consumer" consumes port "reward"/.test(e.message),
184
+ 'the refusal names the module and the port it cannot satisfy'
185
+ );
186
+ assert.equal(used.length, 0, 'it refuses BEFORE requiring any module route factory');
187
+ loader._reset();
188
+ });
189
+
190
+ test('task 1003820: a DISABLED module’s unsatisfied consumes is not a wiring error', () => {
191
+ loader._reset();
192
+ const factory = "module.exports = () => function moduleRoute(req, res, next) { next(); };";
193
+ const root = fixtureRoot({
194
+ consumer: { manifest: validManifest('consumer', { consumes: ['reward'] }) },
195
+ plain: {
196
+ manifest: validManifest('plain', { contributes: { routes: ['main'] } }),
197
+ files: { 'routes/main.js': factory },
198
+ },
199
+ });
200
+ const used = [];
201
+ const fakeRouter = { use: (fn) => used.push(fn) };
202
+ // Only the ENABLED set is verified: a module that is off is a module that is
203
+ // not there, so its consumes names nothing that has to be provided.
204
+ const mounted = loader.mountModuleRoutes(fakeRouter, { isEnabled: (k) => k === 'plain' }, { roots: [root], core: '1.0.0' });
205
+ assert.equal(mounted, 1);
206
+ loader._reset();
207
+ });
208
+
209
+ test('task 1003820: mountModuleRoutes refuses two enabled providers of one port', () => {
210
+ loader._reset();
211
+ const root = fixtureRoot({
212
+ a: { manifest: validManifest('a', { provides: ['reward'] }) },
213
+ b: { manifest: validManifest('b', { provides: ['reward'] }) },
214
+ });
215
+ assert.throws(
216
+ () => loader.mountModuleRoutes({ use: () => {} }, { isEnabled: () => true }, { roots: [root], core: '1.0.0' }),
217
+ (e) => /provided by more than one enabled module/.test(e.message),
218
+ 'the single-provider invariant is enforced at boot, not at first resolve()'
219
+ );
220
+ loader._reset();
221
+ });
222
+
158
223
  test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49/R72)', () => {
159
224
  // dev-box graduated first (modules/dev-box/, BV1.R43), art-pipeline next
160
225
  // (BV1.R48 / task 1417), discord next (BV1.R47 / task 1416), game next
@@ -352,6 +417,9 @@ test('the demo proof module mounts + serves through the loader (BV1.R52 / task 1
352
417
 
353
418
  const used = [];
354
419
  const fakeRouter = { use: (fn) => used.push(fn) };
420
+ // Since task 1003820 this also proves the SHIPPED enabled set is satisfiable:
421
+ // mountModuleRoutes verifies the seam wiring first and throws on a half-wired
422
+ // set, so a manifest change that strands a consumed port reds this test.
355
423
  const mounted = loader.mountModuleRoutes(fakeRouter, { isEnabled: isModuleEnabled });
356
424
  assert.ok(mounted >= 1, 'at least the demo router mounted');
357
425
 
@@ -284,3 +284,56 @@ test('task 1002419: a real `node src/platform-server.js` boot announces the reco
284
284
  child.kill('SIGKILL');
285
285
  }
286
286
  });
287
+
288
+ // ---------------------------------------------------------------------------
289
+ // task 1003820 — a half-wired module set must REFUSE the boot.
290
+ //
291
+ // The mutation-proof half of the wiring guard. tests/module_loader.mjs calls
292
+ // mountModuleRoutes directly, so it stays green if the verifyEnabledPorts call
293
+ // site is deleted; this spawns the real entrypoint the way docker-entrypoint.sh
294
+ // does and asserts the process dies with the wiring error, so removing the call
295
+ // turns it red (learning 1000154 — the same shape as task 1002419 above).
296
+ //
297
+ // The unsatisfiable set is built from REAL modules via a documented env knob, not
298
+ // a fixture: economy provides the `reward` port and ideas consumes it, both
299
+ // default:true, so turning economy off strands ideas. DB-free — the refusal
300
+ // happens while the router is being built, long before anything opens a socket.
301
+ // ---------------------------------------------------------------------------
302
+
303
+ test('task 1003820: platform-server refuses to boot when an enabled module consumes an unprovided port', async () => {
304
+ const port = await freePort();
305
+ const child = spawn(process.execPath, [path.join(REPO_ROOT, 'src', 'platform-server.js')], {
306
+ cwd: REPO_ROOT,
307
+ env: {
308
+ ...process.env,
309
+ PORT: String(port),
310
+ HOST: '127.0.0.1',
311
+ // economy provides "reward"; ideas consumes it and stays on.
312
+ CLOUDBONGOS_MODULE_ECONOMY: '0',
313
+ },
314
+ });
315
+
316
+ let out = '';
317
+ child.stdout.on('data', (d) => { out += d; });
318
+ child.stderr.on('data', (d) => { out += d; });
319
+
320
+ // SIGKILL in a finally: if the guard regresses, the child boots CLEANLY and never
321
+ // exits, so without this the suite would hang on a live server instead of failing.
322
+ try {
323
+ const code = await new Promise((resolve, reject) => {
324
+ const timer = setTimeout(
325
+ () => reject(new Error(`platform-server stayed up; it must refuse a half-wired boot. Log:${out}`)),
326
+ 20_000
327
+ );
328
+ child.on('error', (e) => { clearTimeout(timer); reject(e); });
329
+ child.on('exit', (c) => { clearTimeout(timer); resolve(c); });
330
+ });
331
+
332
+ assert.notEqual(code, 0, `the boot must FAIL, not come up half-wired (exit ${code}). Log:${out}`);
333
+ assert.match(out, /unsatisfiable module wiring/, 'the refusal says what is wrong');
334
+ assert.match(out, /"ideas" consumes port "reward"/, 'it names the module and the port');
335
+ assert.doesNotMatch(out, /listening/, 'it never reaches the listen announcement');
336
+ } finally {
337
+ child.kill('SIGKILL');
338
+ }
339
+ });