@bongos/core 1.19.655 → 1.19.656

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.655",
6
- "core_contract": "1.19.655",
7
- "source_commit": "0385761c80a2ab6960eb0553532e3d2f3a55714d",
5
+ "core_version": "1.19.656",
6
+ "core_contract": "1.19.656",
7
+ "source_commit": "c0c5d7b3e5b4f9be24da806b898180fc4bf0df47",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-11T00:16:29.379Z",
9
+ "built_at": "2026-09-11T00:27:13.017Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 472,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2607,
20
- "tree_sha256": "e0d112687c5e0ef345193d8be0fc71fca2ce378fba76eeb0441b4b5b347d160e",
20
+ "tree_sha256": "90e88ec865416bea870aef7c0ea90db47124defc012e894bc04037bea59f12a6",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/backlog-review/SKILL.md",
@@ -2782,7 +2782,7 @@
2782
2782
  {
2783
2783
  "path": "docs/module-api-changelog.md",
2784
2784
  "mode": "0000644",
2785
- "sha256": "7ab264db84c1eab7bc17fb54d875a7c56ed6142adc3c55fd45dc4e3470d30dae"
2785
+ "sha256": "448e81f5dfb15330d406be135e4ada5c3cfcf0751fca4457a7c91ab58d836d10"
2786
2786
  },
2787
2787
  {
2788
2788
  "path": "docs/modules-contract.md",
@@ -5662,7 +5662,7 @@
5662
5662
  {
5663
5663
  "path": "modules/lifecycle/db-rank-authz.js",
5664
5664
  "mode": "0000644",
5665
- "sha256": "ac33825a705a0d541fc7fe94858f967ef8ef9713a2b7a1f1a03ce268494bb317"
5665
+ "sha256": "e5feff1eb42ad631b6c0165c23224d8ac56ef7f40e5d238afb09013501e9db7c"
5666
5666
  },
5667
5667
  {
5668
5668
  "path": "modules/lifecycle/db-shared.js",
@@ -7742,12 +7742,12 @@
7742
7742
  {
7743
7743
  "path": "package-lock.json",
7744
7744
  "mode": "0000644",
7745
- "sha256": "8bd02249a0695644a522b26a0a2e93519c0359d2b47b10855f69b2e24654d9fb"
7745
+ "sha256": "667fc77f1bfd67239fa405a73b70b64ec77419d97da57bc8350c96ba3c68197b"
7746
7746
  },
7747
7747
  {
7748
7748
  "path": "package.json",
7749
7749
  "mode": "0000644",
7750
- "sha256": "a0ca266a85fbe549d63f4b62a04e4ac17f72bf1cd0500af154ca5597eb801c70"
7750
+ "sha256": "9bee6edf68d9dc8177a24349a4c444c1829ff77619a511be70c8a49329726ac8"
7751
7751
  },
7752
7752
  {
7753
7753
  "path": "public-docs/index.html",
@@ -9502,7 +9502,7 @@
9502
9502
  {
9503
9503
  "path": "src/module-api.js",
9504
9504
  "mode": "0000644",
9505
- "sha256": "503fd9b1f372ec8577fff63dc486b9e33a801a3114e5a151cf47d71f818bf4de"
9505
+ "sha256": "3077ef167f846985e33e1d8316b78cefa5e7ff4825b9a0a7e1b71187a49e4636"
9506
9506
  },
9507
9507
  {
9508
9508
  "path": "src/module-loader/catalog.js",
@@ -10847,7 +10847,7 @@
10847
10847
  {
10848
10848
  "path": "tests/government_claim_eligibility.mjs",
10849
10849
  "mode": "0000644",
10850
- "sha256": "7eb103c6a201345728af5002aae1be09c1f3b1459b74bef7ea14e990a28dd05b"
10850
+ "sha256": "1989a7e5e39a6bb177dfd86e610c0806c96fdc97f9149800de431af816f2c31a"
10851
10851
  },
10852
10852
  {
10853
10853
  "path": "tests/government_config.mjs",
@@ -1759,5 +1759,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1759
1759
  landed since 1.19.653 with no explicit bump. run 34544345437. (task 1002620)
1760
1760
  1.19.655 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1761
1761
  landed since 1.19.654 with no explicit bump. run 34545744588. (task 1002620)
1762
+ 1.19.656 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1763
+ landed since 1.19.655 with no explicit bump. run 34546461767. (task 1002620)
1762
1764
  ---------------------------------------------------------------------------
1763
1765
  ```
@@ -80,15 +80,33 @@ function xenosClaimAllowed(rank, newcomerFriendly) {
80
80
 
81
81
  // claimPermissionsFor — the builder's effective permission set for the claim
82
82
  // decision, read through the `governance` kernel PORT (never by importing the
83
- // module — the one-way rule, ADR 0083). Returns a Set, or NULL when authority
84
- // cannot be ESTABLISHED, which claimEligibilityAllows reads as "fall back to the
85
- // incumbent rank rule":
86
- // the governance module is not mounted (no port), or the resolver throws;
87
- // • the builder resolves to ZERO permissions i.e. they hold no role row.
88
- // governance_002's backfill is run-once and nothing yet re-assigns a rank-role
89
- // when a builder is created or re-ranked, so this is a live gap, not a theory.
90
- // Every seeded rank-role grants at least twelve permissions, so an empty set never
91
- // means "a real role that grants nothing" today.
83
+ // module — the one-way rule, ADR 0083).
84
+ //
85
+ // NULL means authority could not be ESTABLISHED, which claimEligibilityAllows
86
+ // reads as "fall back to the incumbent rank rule". Exactly two things mean that:
87
+ // • the governance module is not mounted (no port), so there is no authority
88
+ // system to ask;
89
+ // the resolver threw, so the answer is unknown rather than empty.
90
+ //
91
+ // AN EMPTY SET IS AN ANSWER, NOT A FAILURE (task 1003816). It used to be folded
92
+ // into the null above, on the stated grounds that governance_002's backfill was
93
+ // run-once and nothing re-assigned a rank-role afterwards — so a builder holding
94
+ // no role row was a live gap rather than a real verdict. That is no longer true,
95
+ // and the three writes that closed it are all shipped:
96
+ // • BV1.R95b rotates the role inside the rank-change transaction, and syncs it
97
+ // on every sign-in (src/bongos/auth-admission.js safeSyncRankRole);
98
+ // • task 1003193 rotates it in-tx on offboard AND reactivation;
99
+ // • governance_005 repaired the rows already written.
100
+ // governance_005 states the intended reading in as many words — a builder with no
101
+ // role row "resolves to the empty set (denied everywhere)" — and
102
+ // syncGovernanceRankRole calls that same outcome "the fail-closed answer". Folding
103
+ // it back into the rank rule is what stopped those three from being true at the
104
+ // claim gate, so the empty set now flows through as a real deny.
105
+ //
106
+ // The residual way to hold no role row is the sign-in sync failing transiently
107
+ // (it is deliberately best-effort so a governance hiccup cannot block a sign-in).
108
+ // Denying the claim is the correct read of that state, and it self-repairs on the
109
+ // next successful sign-in.
92
110
  //
93
111
  // Called BEFORE the claim transaction opens: authority is a per-request read (ADR
94
112
  // 0016 — resolved server-side, uncached), and taking a second pool connection while
@@ -98,7 +116,9 @@ async function claimPermissionsFor(builderId) {
98
116
  if (!governance || typeof governance.resolveBuilderPermissions !== 'function') return null;
99
117
  try {
100
118
  const keys = await governance.resolveBuilderPermissions(builderId);
101
- if (!keys || keys.length === 0) return null;
119
+ // A non-array is a broken port contract, not a verdict — resolveBuilderPermissions
120
+ // always returns an array. Treat it as "could not establish", never as "denied".
121
+ if (!Array.isArray(keys)) return null;
102
122
  return new Set(keys);
103
123
  } catch (err) {
104
124
  console.error('[gds] claim eligibility: permission resolve failed, falling back to the rank rule', err);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.655",
3
+ "version": "1.19.656",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.655",
9
+ "version": "1.19.656",
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.655",
3
+ "version": "1.19.656",
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.655'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.656'; // 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');
@@ -37,6 +37,13 @@ const LIFECYCLE = path.join(ROOT, 'modules', 'lifecycle');
37
37
  const eligibility = require(path.join(LIFECYCLE, 'claim-eligibility.js'));
38
38
  const db = require(path.join(LIFECYCLE, 'db.js'));
39
39
  const catalog = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
40
+ // The doorway, so section D can stand a stub `government` port up in front of the
41
+ // REAL claimPermissionsFor instead of re-implementing its branches in the test.
42
+ const moduleApi = require(path.join(ROOT, 'src', 'module-api.js'));
43
+ // claimPermissionsFor is NOT on the db.js facade — db-claims.js imports it from
44
+ // the authz module directly, so the test reads it from the same place the real
45
+ // caller does rather than from a re-export that does not exist.
46
+ const rankAuthz = require(path.join(LIFECYCLE, 'db-rank-authz.js'));
40
47
  import { lifecycleDbSource } from './helpers.mjs';
41
48
 
42
49
  const { CLAIM_ANY, CLAIM_NEWCOMER, permissionAllowsClaim, claimEligibilityAllows } = eligibility;
@@ -262,3 +269,78 @@ test('the abuse case stays blocked: a xenos cannot reach the general queue by an
262
269
  assert.equal(permissionAllowsClaim(held, false), false);
263
270
  assert.equal(permissionAllowsClaim(held, true), true);
264
271
  });
272
+
273
+ // --- D. claimPermissionsFor's three-way answer (task 1003816) ---------------
274
+ //
275
+ // The function this section covers had NO test of its own: every case above
276
+ // drives the pure consumer (claimEligibilityAllows) with a hand-made
277
+ // heldPermissions value, so nothing proved which value the REAL reader produces.
278
+ // That is exactly where the bug lived — an empty permission set was folded in
279
+ // with "could not establish authority" and silently became a rank-rule fallback.
280
+ //
281
+ // The distinction is the whole point, so it is asserted as a distinction: null
282
+ // (fall back) for the two unestablished cases, a Set (a real verdict, denied when
283
+ // empty) whenever the resolver actually answered.
284
+ const realResolveOptional = moduleApi.resolveOptional;
285
+ async function withGovernance(stub, fn) {
286
+ moduleApi.resolveOptional = (name) => (name === 'government' ? stub : realResolveOptional.call(moduleApi, name));
287
+ try { return await fn(); } finally { moduleApi.resolveOptional = realResolveOptional; }
288
+ }
289
+
290
+ test('no governance port — authority cannot be established, so the rank rule decides', async () => {
291
+ await withGovernance(undefined, async () => {
292
+ assert.equal(await rankAuthz.claimPermissionsFor('42'), null);
293
+ });
294
+ // A port object that does not implement the resolver is the same situation.
295
+ await withGovernance({}, async () => {
296
+ assert.equal(await rankAuthz.claimPermissionsFor('42'), null);
297
+ });
298
+ });
299
+
300
+ test('the resolver throwing is UNKNOWN, not empty — still the rank rule', async () => {
301
+ await withGovernance({ resolveBuilderPermissions: async () => { throw new Error('synthetic resolver outage'); } }, async () => {
302
+ assert.equal(await rankAuthz.claimPermissionsFor('42'), null);
303
+ });
304
+ });
305
+
306
+ test('a broken port contract (non-array) is unknown, never a deny', async () => {
307
+ for (const bad of [null, undefined, 'task.claim.any', 42, {}]) {
308
+ await withGovernance({ resolveBuilderPermissions: async () => bad }, async () => {
309
+ assert.equal(await rankAuthz.claimPermissionsFor('42'), null, `a ${typeof bad} return must not read as "denied"`);
310
+ });
311
+ }
312
+ });
313
+
314
+ test('ZERO permissions is a real verdict now, not a fallback (the 1003816 fix)', async () => {
315
+ await withGovernance({ resolveBuilderPermissions: async () => [] }, async () => {
316
+ const held = await rankAuthz.claimPermissionsFor('42');
317
+ assert.ok(held instanceof Set, 'an answered resolve returns a Set, so the rank rule is NOT consulted');
318
+ assert.equal(held.size, 0);
319
+ // And the consumer turns that into a deny for every queue, however permissive
320
+ // the incumbent rank rule would have been.
321
+ for (const nf of NF_VALUES) {
322
+ assert.equal(
323
+ claimEligibilityAllows({ heldPermissions: held, newcomerFriendly: nf, rankRuleAllows: true }),
324
+ false,
325
+ 'a builder with no role row is denied even where the rank rule would allow',
326
+ );
327
+ }
328
+ });
329
+ });
330
+
331
+ test('a real permission set is passed through as the verdict', async () => {
332
+ await withGovernance({ resolveBuilderPermissions: async () => [CLAIM_ANY] }, async () => {
333
+ const held = await rankAuthz.claimPermissionsFor('42');
334
+ assert.ok(held instanceof Set);
335
+ assert.equal(held.has(CLAIM_ANY), true);
336
+ assert.equal(claimEligibilityAllows({ heldPermissions: held, newcomerFriendly: false, rankRuleAllows: false }), true);
337
+ });
338
+ });
339
+
340
+ test('the doc comment no longer argues for the fallback it describes retiring', () => {
341
+ const src = fs.readFileSync(path.join(LIFECYCLE, 'db-rank-authz.js'), 'utf8');
342
+ assert.ok(
343
+ !/nothing yet re-assigns a rank-role/.test(src),
344
+ 'the stale "nothing yet re-assigns a rank-role" claim is false since BV1.R95b + task 1003193 — it must not survive the fix',
345
+ );
346
+ });