@bongos/core 1.19.641 → 1.19.642

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.641",
6
- "core_contract": "1.19.641",
7
- "source_commit": "7af21a0086bc6cbc069a459b0a30b90f1646f82e",
5
+ "core_version": "1.19.642",
6
+ "core_contract": "1.19.642",
7
+ "source_commit": "c6bea45e42c4f596aab75c09f8a0b5c1659a51bb",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-10T17:12:24.520Z",
9
+ "built_at": "2026-09-10T17:19:26.832Z",
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": 2600,
20
- "tree_sha256": "1fd37a4de38d4265a2f0956b03da63ce66a9e84de7ce8a24e1c66ae4465ea028",
20
+ "tree_sha256": "220035e0bd7f1d6881e5ff218c34a135e0b0822527a21e30e4c2aa5a67ee4625",
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": "967b335b7e06715fb8c07cea7af1141fa2dd3acb0da7873017048bb63aad15d0"
2775
+ "sha256": "12455239558665503941d95dda844f8de0d1db80ad9df889684ca896d251b663"
2776
2776
  },
2777
2777
  {
2778
2778
  "path": "docs/modules-contract.md",
@@ -7722,12 +7722,12 @@
7722
7722
  {
7723
7723
  "path": "package-lock.json",
7724
7724
  "mode": "0000644",
7725
- "sha256": "9a655dfca70f3edf6083c6533004860a8a6da53455d732526dea6c0c37f3fcf1"
7725
+ "sha256": "1a1e2e7386cf99e8554916563b80a12da0ead3338a232208a4adab57cedb23bd"
7726
7726
  },
7727
7727
  {
7728
7728
  "path": "package.json",
7729
7729
  "mode": "0000644",
7730
- "sha256": "80d6ffe532ebbd4d2165171ec5e2887487599e42c53e5411bb4048074d2eb0fb"
7730
+ "sha256": "e831988b7dfec47e49c0e0c763fd9cb467cf51c296c08ea6a51d1f2747a19398"
7731
7731
  },
7732
7732
  {
7733
7733
  "path": "public-docs/index.html",
@@ -9232,7 +9232,7 @@
9232
9232
  {
9233
9233
  "path": "src/bongos/db.js",
9234
9234
  "mode": "0000644",
9235
- "sha256": "075c29580b58aa648b203b37c5d558900daaaa8cbfeaceb871ae9b1733361928"
9235
+ "sha256": "b205304524629b0e867200e43a083bdf23b9121a6dbc2463c1e80e0e3fca8b8f"
9236
9236
  },
9237
9237
  {
9238
9238
  "path": "src/bongos/github-repos.js",
@@ -9482,7 +9482,7 @@
9482
9482
  {
9483
9483
  "path": "src/module-api.js",
9484
9484
  "mode": "0000644",
9485
- "sha256": "cc274e241228f0ad4bb534674500d4daf228abcf6434241fed7152e49590f43f"
9485
+ "sha256": "0ab1529d0da82c501530a79cfc3b7f47b00421677059a7afa7bf8382a9098fe6"
9486
9486
  },
9487
9487
  {
9488
9488
  "path": "src/module-loader/catalog.js",
@@ -10897,7 +10897,7 @@
10897
10897
  {
10898
10898
  "path": "tests/government_rank_sync.mjs",
10899
10899
  "mode": "0000644",
10900
- "sha256": "8750d0bbfc33403ff451b780935496a037dbab4a8e30bdc089abde8da8b7400a"
10900
+ "sha256": "d4bd68c98f6e4bbec59890dde225f27f1daf380397f39d329b447fb77fd51c14"
10901
10901
  },
10902
10902
  {
10903
10903
  "path": "tests/government_require_permission.mjs",
@@ -1731,5 +1731,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1731
1731
  landed since 1.19.639 with no explicit bump. run 34506041899. (task 1002620)
1732
1732
  1.19.641 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1733
1733
  landed since 1.19.640 with no explicit bump. run 34506718627. (task 1002620)
1734
+ 1.19.642 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1735
+ landed since 1.19.641 with no explicit bump. run 34507442448. (task 1002620)
1734
1736
  ---------------------------------------------------------------------------
1735
1737
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.641",
3
+ "version": "1.19.642",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.641",
9
+ "version": "1.19.642",
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.641",
3
+ "version": "1.19.642",
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/bongos/db.js CHANGED
@@ -517,11 +517,12 @@ async function getLatestRankChangeForBuilder(builderId) {
517
517
  // Graceful builder exit (V3.R88 #307). Deactivating a builder is reversible
518
518
  // and lossless: drachmae (credits) + credit_log, shipped tasks, and historical
519
519
  // session_logs all stay. What changes: status → inactive (blocks new claims via
520
- // claimTask's guard), rank → 'xenos' (sandboxed tier), active claims released
521
- // cleanly with a final 'abandoned' session_log sealing each, and the tasks
522
- // returned to 'ready' for someone else. Reactivation flips status back; rank
523
- // is NOT auto-restored (an Archon re-promotes deliberately via PATCH
524
- // /builders/:id/rank). Memory archival is local-first and handled by the
520
+ // claimTask's guard), rank → 'xenos' (sandboxed tier) WITH its seeded rank-role
521
+ // rotated to match (task 1003193 the direct SQL used to skip that), active claims
522
+ // released cleanly with a final 'abandoned' session_log sealing each, and the tasks
523
+ // returned to 'ready' for someone else. Reactivation flips status back and re-syncs
524
+ // the rank-role; rank itself is NOT auto-restored (an Archon re-promotes deliberately
525
+ // via PATCH /builders/:id/rank). Memory archival is local-first and handled by the
525
526
  // /builder-exit skill — the server never holds another builder's local memory
526
527
  // (the server-side memory store is criterion #54's job, not this task's).
527
528
 
@@ -571,6 +572,18 @@ async function deactivateBuilder({ builderId, reason, actorBuilderId }) {
571
572
  deactivated_at, reactivated_at, deactivated_reason`,
572
573
  [builderId, reason ?? null]
573
574
  );
575
+ // BV1.R95b — rotate the seeded rank-role IN THIS TRANSACTION, exactly as
576
+ // _applyRankChangeTx does (task 1003193). The UPDATE above forces rank to
577
+ // 'xenos' by direct SQL rather than through setBuilderRank, so without this the
578
+ // builder's OLD seeded rank-role (archon, say) survives in
579
+ // government_builder_ranks — getRankKeysForBuilder has no builders.status filter.
580
+ // It is not a live grant while they are inactive (every session is revoked below
581
+ // and requireBuilder denies 'inactive' per request), but reactivateBuilder flips
582
+ // status back without touching rank, so the stale-high role would return live.
583
+ // The rank is read back from the row just written rather than re-spelled here.
584
+ // Deliberately uncaught, for the same reason as the twin: a committed
585
+ // deactivation whose rotation failed IS the privilege-retention bug.
586
+ await syncGovernanceRankRole(client, builderId, updated[0].rank);
574
587
  // Interim audit (→ audit_log when R36 lands). delta=0 = no-op for credits.
575
588
  await client.query(
576
589
  `INSERT INTO credit_log (builder_id, task_id, delta, reason)
@@ -629,6 +642,15 @@ async function reactivateBuilder({ builderId, actorBuilderId }) {
629
642
  );
630
643
  // Rank is intentionally left at whatever it is (xenos after a deactivation):
631
644
  // an Archon re-promotes deliberately via PATCH /builders/:id/rank.
645
+ //
646
+ // REACTIVATION IS THE EXPOSURE (task 1003193). Rank does not move here, but
647
+ // status flipping back to 'active' makes whatever seeded rank-role the builder
648
+ // still holds live again — getActiveBuildersWithRank, builderHoldsRankActive,
649
+ // the board roll and the R10 tally denominator all read it. So sync to the row's
650
+ // CURRENT rank: a stale-high role left by a deactivation predating this fix (or
651
+ // drifted in from anywhere else) is removed as PART of the reactivation rather
652
+ // than after it. Idempotent — a consistent builder costs two SELECTs, no write.
653
+ await syncGovernanceRankRole(client, builderId, updated[0].rank);
632
654
  await client.query(
633
655
  `INSERT INTO credit_log (builder_id, task_id, delta, reason)
634
656
  VALUES ($1, NULL, 0, $2)`,
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.641'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.642'; // 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');
@@ -326,3 +326,130 @@ test('the writer EXECUTES its plan: a custom rank survives a rank change', async
326
326
  const del = c.find(/DELETE FROM government_builder_ranks/);
327
327
  assert.deepEqual(del.params, [11, ['archon']]);
328
328
  });
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // offboard + reactivation (task 1003193)
332
+ // ---------------------------------------------------------------------------
333
+ //
334
+ // deactivateBuilder forces rank='xenos' by direct SQL — not through
335
+ // setBuilderRank/_applyRankChangeTx, the only paths that rotated the role — so the
336
+ // offboarded builder's OLD seeded rank-role survived in government_builder_ranks,
337
+ // and getRankKeysForBuilder has no builders.status filter. The stale row is not a
338
+ // live grant while they are inactive; REACTIVATION is the exposure, because
339
+ // reactivateBuilder restores status='active' without touching rank, and a
340
+ // reactivated ex-Archon sitting at builders.rank='xenos' then matches
341
+ // getActiveBuildersWithRank / builderHoldsRankActive on the stale archon row.
342
+ //
343
+ // Asserted by EXECUTION, like the writer tests above: withTx is stubbed on
344
+ // db-kernel BEFORE src/bongos/db.js is required (it destructures at load), the
345
+ // government port is wired to the REAL syncRank, and both functions run against one
346
+ // recording client — so the rotation being on the same transaction, and after the
347
+ // rank write, is a fact about the code rather than a regex over it.
348
+
349
+ const kernel = require(path.join(ROOT, 'src/bongos/db-kernel.js'));
350
+ const seams = require(path.join(ROOT, 'src/module-seams.js'));
351
+ if (!seams.hasProvider('government.rankSync')) {
352
+ seams.registerProvider('government.rankSync', { syncRank: govDb.syncRank });
353
+ }
354
+
355
+ // One client for the whole transaction: the builders/claims/sessions statements the
356
+ // exit path issues AND the two government reads syncRank makes. Order the matchers
357
+ // specific-first — the assignments read also names government_ranks (it joins it).
358
+ function exitTxClient({ builderId, rank, status, held = [], newRank, newStatus }) {
359
+ const calls = [];
360
+ return {
361
+ calls,
362
+ find: (re) => calls.find((c) => re.test(c.sql)),
363
+ indexOf: (re) => calls.findIndex((c) => re.test(c.sql)),
364
+ async query(sql, params) {
365
+ calls.push({ sql, params });
366
+ if (/INSERT INTO|DELETE FROM/.test(sql)) return { rows: [] };
367
+ if (/FROM government_builder_ranks br/.test(sql)) return { rows: held };
368
+ if (/FROM government_ranks/.test(sql)) return { rows: SEEDED };
369
+ if (/FROM builders WHERE id = \$1 FOR UPDATE/.test(sql)) {
370
+ return { rows: [{ id: builderId, rank, status }] };
371
+ }
372
+ if (/FROM claims/.test(sql)) return { rows: [] };
373
+ if (/UPDATE builders/.test(sql)) {
374
+ return { rows: [{ id: builderId, rank: newRank, status: newStatus }] };
375
+ }
376
+ return { rows: [] };
377
+ },
378
+ };
379
+ }
380
+
381
+ let txClient = null;
382
+ kernel.withTx = async (fn) => fn(txClient);
383
+ const coreDb = require(path.join(ROOT, 'src/bongos/db.js'));
384
+
385
+ test('offboarding an Archon removes the archon rank-role in the deactivation transaction', async () => {
386
+ txClient = exitTxClient({
387
+ builderId: 25, rank: 'archon', status: 'active',
388
+ held: [seededRank_('archon')], newRank: 'xenos', newStatus: 'inactive',
389
+ });
390
+ await coreDb.deactivateBuilder({ builderId: 25, reason: 'left the team', actorBuilderId: 3 });
391
+
392
+ const del = txClient.find(/DELETE FROM government_builder_ranks/);
393
+ assert.ok(del, 'deactivateBuilder must rotate the seeded rank-role, not only write rank=xenos');
394
+ assert.deepEqual(del.params, [25, ['archon']], 'the stale archon assignment must be the row deleted');
395
+ const ins = txClient.find(/INSERT INTO government_builder_ranks/);
396
+ assert.deepEqual(ins.params, [25, ['xenos']], 'and the sandboxed rank-role takes its place');
397
+
398
+ assert.ok(txClient.indexOf(/DELETE FROM government_builder_ranks/) > txClient.indexOf(/UPDATE builders/),
399
+ 'the rotation must follow the rank write inside the same transaction');
400
+ });
401
+
402
+ test('reactivation re-syncs the rank-role, so a stale-high role cannot come back live', async () => {
403
+ // The pre-fix residue: rank was forced to xenos but the archon assignment stayed.
404
+ // Flipping status back to 'active' is the moment it becomes a live grant again.
405
+ txClient = exitTxClient({
406
+ builderId: 25, rank: 'xenos', status: 'inactive',
407
+ held: [seededRank_('archon')], newRank: 'xenos', newStatus: 'active',
408
+ });
409
+ await coreDb.reactivateBuilder({ builderId: 25, actorBuilderId: 3 });
410
+
411
+ const del = txClient.find(/DELETE FROM government_builder_ranks/);
412
+ assert.ok(del, 'reactivateBuilder must sync the rank-role to the row\'s current rank');
413
+ assert.deepEqual(del.params, [25, ['archon']],
414
+ 'a reactivated ex-Archon must not resolve Archon permissions on the stale row');
415
+ assert.deepEqual(txClient.find(/INSERT INTO government_builder_ranks/).params, [25, ['xenos']]);
416
+ });
417
+
418
+ test('a consistent builder is reactivated with no government write at all', async () => {
419
+ // The sync is a repair, not a rewrite: it must not churn rows on every offboard
420
+ // round-trip once the assignment already agrees with builders.rank.
421
+ txClient = exitTxClient({
422
+ builderId: 26, rank: 'xenos', status: 'inactive',
423
+ held: [seededRank_('xenos'), customRank('release-manager')], newRank: 'xenos', newStatus: 'active',
424
+ });
425
+ await coreDb.reactivateBuilder({ builderId: 26, actorBuilderId: 3 });
426
+
427
+ assert.ok(!txClient.find(/(INSERT INTO|DELETE FROM) government_builder_ranks/),
428
+ 'no rotation is due — and the custom rank must survive untouched');
429
+ });
430
+
431
+ test('a custom rank survives an offboard (only the seeded rank-role rotates)', async () => {
432
+ txClient = exitTxClient({
433
+ builderId: 27, rank: 'metic', status: 'active',
434
+ held: [seededRank_('metic'), customRank('release-manager')], newRank: 'xenos', newStatus: 'inactive',
435
+ });
436
+ await coreDb.deactivateBuilder({ builderId: 27, reason: null, actorBuilderId: 3 });
437
+
438
+ assert.deepEqual(txClient.find(/DELETE FROM government_builder_ranks/).params, [27, ['metic']],
439
+ 'offboarding must never revoke an Archon-assigned custom rank');
440
+ });
441
+
442
+ test('both exit paths rotate the role on the transaction client, uncaught', async () => {
443
+ // The posture half, which execution cannot show: no try/catch around the call, so
444
+ // a rotation that cannot be written rolls the status change back with it.
445
+ const src = fs.readFileSync(path.join(ROOT, 'src/bongos/db.js'), 'utf8');
446
+ for (const fn of ['deactivateBuilder', 'reactivateBuilder']) {
447
+ const body = src.slice(src.indexOf(`async function ${fn}(`));
448
+ const end = body.indexOf('\n}\n');
449
+ const scope = body.slice(0, end === -1 ? body.length : end);
450
+ const sync = scope.indexOf('await syncGovernanceRankRole(client');
451
+ assert.ok(sync !== -1, `${fn}: must rotate the seeded rank-role inside its transaction`);
452
+ assert.ok(!/try\s*{[\s\S]*syncGovernanceRankRole/.test(scope),
453
+ `${fn}: the rotation must not be swallowed — a failed rotation has to roll the write back`);
454
+ }
455
+ });