@bongos/core 1.19.56 โ†’ 1.19.58

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.56",
6
- "core_contract": "1.19.56",
7
- "source_commit": "0d53ea0c55e27185532763b669dd008990ef4c15",
5
+ "core_version": "1.19.58",
6
+ "core_contract": "1.19.58",
7
+ "source_commit": "138d3a9bbd75c0edac9f89c279e76e9ce9789b0b",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-11T20:52:25.036Z",
9
+ "built_at": "2026-08-11T23:20:42.316Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 291,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 1529,
20
- "tree_sha256": "04e6b2b1fce01ae88706efee727647c81f1124bab26434229dffa6cc02c53663",
20
+ "tree_sha256": "08cfb44880c063d98dfdd34b729592669fd80917f30027532bf691f3f0a2abe4",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -3057,7 +3057,7 @@
3057
3057
  {
3058
3058
  "path": "modules/hall-ui/public/goals-page.js",
3059
3059
  "mode": "0000644",
3060
- "sha256": "bacccdcedf5abfe57ebe6ae4f521f00dace123c11509554b2ef6c934fde2afb4"
3060
+ "sha256": "fcfe94b3e0cd00ddc881283d9897121518a7acc00b3d05b0715552911e2a0705"
3061
3061
  },
3062
3062
  {
3063
3063
  "path": "modules/hall-ui/public/goals.css",
@@ -3382,7 +3382,7 @@
3382
3382
  {
3383
3383
  "path": "modules/lifecycle/db.js",
3384
3384
  "mode": "0000644",
3385
- "sha256": "89febe6a0002d2277a61b14cc08c4076d1ab6c738fdea8f8a4a1348e330c0b9e"
3385
+ "sha256": "37303ffa7a795ea41db83f346bc736898720a3d1521434f566a71bbeb7b4dc15"
3386
3386
  },
3387
3387
  {
3388
3388
  "path": "modules/lifecycle/done-when.js",
@@ -3477,7 +3477,7 @@
3477
3477
  {
3478
3478
  "path": "modules/lifecycle/routes/goals.js",
3479
3479
  "mode": "0000644",
3480
- "sha256": "9651f16e968983ab378aabc3d2a5e3d6ef6ba3fc3aaa00727fc5aff673b8d10e"
3480
+ "sha256": "a291701ef3b3f8bb548d8e3475707e52562d0c9f03d334a885ff2022c90358dc"
3481
3481
  },
3482
3482
  {
3483
3483
  "path": "modules/lifecycle/routes/lifecycle.js",
@@ -4032,12 +4032,12 @@
4032
4032
  {
4033
4033
  "path": "package-lock.json",
4034
4034
  "mode": "0000644",
4035
- "sha256": "cb6369b4e8b98aeb502ff170d6d9f49c8b9155f5fa7c63698bf0379233acead8"
4035
+ "sha256": "32227d0bece0f036772ed8a6f01a5aee0ea6c47d81b471a3e7cf734aeb24f1ed"
4036
4036
  },
4037
4037
  {
4038
4038
  "path": "package.json",
4039
4039
  "mode": "0000644",
4040
- "sha256": "f460270fbecb69d9310b33c0274f122b561b45fd0a567758948c2921913a8c1a"
4040
+ "sha256": "b2931ffd8ff5d2fd5d320c3c435e3bc16d59a2757c248529d6160f305216a24d"
4041
4041
  },
4042
4042
  {
4043
4043
  "path": "public-docs/index.html",
@@ -5657,7 +5657,7 @@
5657
5657
  {
5658
5658
  "path": "src/module-api.js",
5659
5659
  "mode": "0000644",
5660
- "sha256": "18e724e13e387b61822904c5e435d28cce5cc57e95ab710db3bbf9fde238858b"
5660
+ "sha256": "98dd7f963174211b172a2023fe2d13a249362d7471d59ab6eb411424332bee3f"
5661
5661
  },
5662
5662
  {
5663
5663
  "path": "src/module-loader/loader.js",
@@ -6497,7 +6497,7 @@
6497
6497
  {
6498
6498
  "path": "tests/goal_membership_requests.mjs",
6499
6499
  "mode": "0000644",
6500
- "sha256": "5a90f9030e1e50a62779460788ac6a56374fd36a839adf40433dd6a184a9b787"
6500
+ "sha256": "4bc1ecb2bec4345b55e074a06009d31f97e4b5e06b214669a72e485298307ac8"
6501
6501
  },
6502
6502
  {
6503
6503
  "path": "tests/goal_routes.mjs",
@@ -6512,7 +6512,7 @@
6512
6512
  {
6513
6513
  "path": "tests/goal_tier.mjs",
6514
6514
  "mode": "0000644",
6515
- "sha256": "b2f2a0d1bb3c518f1a23a203d2525c1e27bd13f02bc52b287d82219af13ca383"
6515
+ "sha256": "508b8b99afe67f64c86b5e1b382052e02a4cc916bec58e28d396ba3716998baf"
6516
6516
  },
6517
6517
  {
6518
6518
  "path": "tests/goal_wall_proof.mjs",
@@ -1237,7 +1237,13 @@
1237
1237
  // manager-only).
1238
1238
  const lockSpan = g.visibility === 'private'
1239
1239
  ? '<span class="goal-badge goal-badge--locked" title="Private โ€” join is by invitation or an approved request">๐Ÿ”’ Private</span>' : '';
1240
- meta.innerHTML = `<span>${escapeHtml(g.version_id || '')}</span> ${statusBadge(g.status)} ${lockSpan} ${scope} ${depsSpan}`;
1240
+ // task 1002689 โ€” a PROTECTED-scope goal is join-gated too (Archon-mediated
1241
+ // membership, ADR 0086 ยง3): badge it so it doesn't read as open-join. The
1242
+ // flag is server-derived (scope_protected on the goal payload) โ€” the client
1243
+ // still never re-derives the protected roster.
1244
+ const gateSpan = g.scope_protected
1245
+ ? '<span class="goal-badge goal-badge--locked" title="Protected module scope โ€” join by request; an Archon admits members">๐Ÿ›ก Archon-mediated</span>' : '';
1246
+ meta.innerHTML = `<span>${escapeHtml(g.version_id || '')}</span> ${statusBadge(g.status)} ${lockSpan} ${gateSpan} ${scope} ${depsSpan}`;
1241
1247
  }
1242
1248
 
1243
1249
  // task 1762: the goal's own owner/manager (or an Archon) can invite/manage
@@ -1266,11 +1272,13 @@
1266
1272
 
1267
1273
  // task 1953 โ€” the goal-detail Actions row: Leave/Join (unchanged), a manager's
1268
1274
  // Private/Public toggle (PATCH /goals/:id/visibility), and โ€” for a non-member
1269
- // viewer of a PRIVATE goal โ€” Request to join / Requested (withdraw) in place of
1270
- // Join (private goals are join-GATED: POST /goals/:id/join 409s goal_private for
1271
- // a non-Archon non-member). An Archon keeps the plain Join button: the server
1272
- // lets them self-join any goal regardless of visibility (POST /goals/:id/join),
1273
- // so "request" would be a pointless extra step for them.
1275
+ // viewer of a PRIVATE or PROTECTED-scope goal โ€” Request to join / Requested
1276
+ // (withdraw) in place of Join. Both are join-GATED for a non-Archon non-member:
1277
+ // POST /goals/:id/join 409s goal_private on a private goal and 403s
1278
+ // archon_approval_required on a protected-scope one, while the server accepts a
1279
+ // join-request on either (task 1002689 โ€” scope_protected is server-derived on
1280
+ // the goal payload). An Archon keeps the plain Join button: the server lets
1281
+ // them self-join any goal, so "request" would be a pointless extra step.
1274
1282
  function renderActions(g, { joined, canManage }) {
1275
1283
  const actions = $('#goal-detail-actions');
1276
1284
  if (!actions) return;
@@ -1283,7 +1291,7 @@
1283
1291
  if (g.status === 'open') {
1284
1292
  if (joined) {
1285
1293
  parts.push('<button class="btn-ghost" id="goal-leave-btn" type="button">Leave goal</button>');
1286
- } else if (g.visibility === 'private' && !isArchon()) {
1294
+ } else if ((g.visibility === 'private' || g.scope_protected) && !isArchon()) {
1287
1295
  if (state.myPendingRequest) {
1288
1296
  parts.push('<span class="role-pill" title="Awaiting the owner/manager\'s approval">Requested</span>');
1289
1297
  parts.push('<button class="btn-ghost" id="goal-withdraw-btn" type="button">Withdraw</button>');
@@ -1468,7 +1476,7 @@
1468
1476
  renderList();
1469
1477
  } catch (err) {
1470
1478
  const code = err?.data?.error;
1471
- if (code === 'archon_approval_required') toast('This goal is Archon-mediated โ€” an Archon must add you.');
1479
+ if (code === 'archon_approval_required') toast('This goal is Archon-mediated โ€” request to join instead.');
1472
1480
  else if (code === 'goal_private') toast('This goal is private โ€” request to join instead.');
1473
1481
  else if (code === 'not_a_member') toast('You are not a member of this goal.');
1474
1482
  else if (code === 'goal_not_open') toast('This goal is not open.');
@@ -823,16 +823,20 @@ async function achieveGoalIfComplete({ goalId }, deps = {}) {
823
823
  return { achieved: true, goal: updated };
824
824
  }
825
825
 
826
- // addGoalMember โ€” join (or change role on re-join). Upserts on the (goal_id,
827
- // builder_id) PK so a member re-joining as lead is promoted, not a duplicate-key
828
- // error. role defaults to 'member'.
826
+ // addGoalMember โ€” join. Upserts on the (goal_id, builder_id) PK so a re-join is
827
+ // not a duplicate-key error, and the conflict arm PRESERVES the stored role
828
+ // (task 1002689): overwriting it let POST /goals/:id/join (always role 'member')
829
+ // and a POST /goals/:id/members re-add silently demote a lead โ€” including the
830
+ // owner's row โ€” bypassing authorizeMemberRoleChange's owner-only-demotion
831
+ // guards. Role changes go through PATCH /goals/:id/members/:builderId only
832
+ // (the acceptMembershipRequestAndAddMember precedent). role defaults to 'member'.
829
833
 
830
834
  async function addGoalMember({ goalId, builderId, role = 'member' }, deps = {}) {
831
835
  const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
832
836
  const { rows } = await activePool.query(
833
837
  `INSERT INTO goal_members (goal_id, builder_id, role)
834
838
  VALUES ($1, $2, $3)
835
- ON CONFLICT (goal_id, builder_id) DO UPDATE SET role = EXCLUDED.role
839
+ ON CONFLICT (goal_id, builder_id) DO UPDATE SET role = goal_members.role
836
840
  RETURNING goal_id, builder_id, role, joined_at`,
837
841
  [goalId, builderId, role]
838
842
  );
@@ -373,6 +373,16 @@ function authorizeWithdrawJoinRequest({ request, goalId, actorId }) {
373
373
  return { ok: true };
374
374
  }
375
375
 
376
+ // decorateGoalScope โ€” additive, derived scope_protected flag on goal payloads
377
+ // (task 1002689). A protected-scope goal is join-gated like a private one
378
+ // (Archon-mediated membership, ADR 0086 ยง3), but the hall cannot re-derive the
379
+ // protected roster client-side (module-scope-map is server-only) โ€” so the
380
+ // list/detail reads carry the boolean and the UI renders the request-to-join
381
+ // path instead of a dead-end Join button. Never persisted; a pure map.
382
+ function decorateGoalScope(goal) {
383
+ return goal ? { ...goal, scope_protected: scopeIncludesProtected(goal.scope_modules) } : goal;
384
+ }
385
+
376
386
  module.exports = function buildGoalsRouter() {
377
387
  const router = express.Router();
378
388
 
@@ -443,7 +453,7 @@ module.exports = function buildGoalsRouter() {
443
453
  }
444
454
  try {
445
455
  const goals = await db.listGoals({ versionId: version || null, status: status || null });
446
- return res.json({ goals });
456
+ return res.json({ goals: goals.map(decorateGoalScope) });
447
457
  } catch (err) {
448
458
  console.error('[gds] GET /goals', err);
449
459
  return res.fail('list_failed', { status: 500, message: 'internal error' });
@@ -512,7 +522,7 @@ module.exports = function buildGoalsRouter() {
512
522
  db.listCriteriaForGoal(id),
513
523
  db.getGoalDependencies(id), // task 1761 โ€” prerequisite goals + satisfied state
514
524
  ]);
515
- return res.json({ goal, members, criteria, dependencies });
525
+ return res.json({ goal: decorateGoalScope(goal), members, criteria, dependencies });
516
526
  } catch (err) {
517
527
  console.error('[gds] GET /goals/:id', err);
518
528
  return res.fail('fetch_failed', { status: 500, message: 'internal error' });
@@ -559,7 +569,7 @@ module.exports = function buildGoalsRouter() {
559
569
  return res.fail('goal_not_open', 409, { status: goal.status });
560
570
  }
561
571
  if (scopeIncludesProtected(goal.scope_modules) && req.builder.rank !== 'archon') {
562
- return res.fail('archon_approval_required', { status: 403, message: 'This goal is scoped to a protected module; an Archon must add you (POST /goals/:id/members).' });
572
+ return res.fail('archon_approval_required', { status: 403, message: 'This goal is scoped to a protected module; request to join (POST /goals/:id/requests) or ask an Archon to add you (POST /goals/:id/members).' });
563
573
  }
564
574
  // Private goals are join-GATED (task 1952 / ADR 0112): a non-member can't
565
575
  // self-join โ€” they request to join or wait for an invite. An existing member
@@ -1493,3 +1503,6 @@ module.exports.authorizeJoinRequestResponse = authorizeJoinRequestResponse;
1493
1503
  // task 1953: the withdraw gate โ€” exported for tests/goal_membership_requests.mjs,
1494
1504
  // same precedent as its siblings above.
1495
1505
  module.exports.authorizeWithdrawJoinRequest = authorizeWithdrawJoinRequest;
1506
+ // task 1002689: the derived scope_protected payload decorator โ€” exported for
1507
+ // tests/goal_membership_requests.mjs (DB-free), same precedent.
1508
+ module.exports.decorateGoalScope = decorateGoalScope;
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.56",
3
+ "version": "1.19.58",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.56",
9
+ "version": "1.19.58",
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.56",
3
+ "version": "1.19.58",
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
@@ -539,8 +539,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
539
539
  // landed since 1.19.54 with no explicit bump. run 31534150701. (task 1002620)
540
540
  // 1.19.56 โ€” CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
541
541
  // landed since 1.19.55 with no explicit bump. run 31535063954. (task 1002620)
542
+ // 1.19.57 โ€” CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
543
+ // landed since 1.19.56 with no explicit bump. run 31542659439. (task 1002620)
544
+ // 1.19.58 โ€” CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
545
+ // landed since 1.19.57 with no explicit bump. run 31546064883. (task 1002620)
542
546
  // ---------------------------------------------------------------------------
543
- const CORE_VERSION = '1.19.56'; // CI auto-patch carrier (ADR 0161); see changelog above
547
+ const CORE_VERSION = '1.19.58'; // CI auto-patch carrier (ADR 0161); see changelog above
544
548
 
545
549
  // A namespaced logger so a module's log lines are attributable + consistent.
546
550
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -41,6 +41,7 @@ const {
41
41
  authorizeInvitationResponse,
42
42
  authorizeJoinRequestResponse,
43
43
  authorizeWithdrawJoinRequest,
44
+ decorateGoalScope,
44
45
  } = require('../modules/lifecycle/routes/goals.js');
45
46
  const { MODULE_EXPECTED_RANKS } = require('../src/bongos/route-rank-check.js');
46
47
 
@@ -338,6 +339,30 @@ await test('authorizeWithdrawJoinRequest: the requester (string vs number id)
338
339
  assert.deepEqual(authorizeWithdrawJoinRequest({ request: REQUEST_FOR_WITHDRAW, goalId: '5', actorId: 2 }), { ok: true });
339
340
  });
340
341
 
342
+ // =========================================================================
343
+ // decorateGoalScope (task 1002689) โ€” goal payloads carry a derived
344
+ // scope_protected flag so the hall can render the join-gate on a
345
+ // protected-scope goal without re-deriving the roster client-side
346
+ // (module-scope-map is server-only). 'kernel' is definitionally protected;
347
+ // 'ideas' is not in the protected set.
348
+ // =========================================================================
349
+
350
+ await test('decorateGoalScope: protected-scope goal โ†’ scope_protected true', () => {
351
+ const g = decorateGoalScope({ id: 1, scope_modules: ['kernel'] });
352
+ assert.equal(g.scope_protected, true);
353
+ });
354
+ await test('decorateGoalScope: unprotected / empty scope โ†’ false; null passes through', () => {
355
+ assert.equal(decorateGoalScope({ id: 1, scope_modules: ['ideas'] }).scope_protected, false);
356
+ assert.equal(decorateGoalScope({ id: 1, scope_modules: [] }).scope_protected, false);
357
+ assert.equal(decorateGoalScope(null), null);
358
+ });
359
+ await test('decorateGoalScope: returns a copy, never mutates the row', () => {
360
+ const row = { id: 1, scope_modules: ['ideas'] };
361
+ const out = decorateGoalScope(row);
362
+ assert.notEqual(out, row);
363
+ assert.equal('scope_protected' in row, false);
364
+ });
365
+
341
366
  // =========================================================================
342
367
  // route-rank pins (task 1952/1953) โ€” the walls are in-handler, so the routes
343
368
  // stay 'any-builder'; an accidental requireRank re-gate must be a red build.
@@ -154,13 +154,14 @@ await test('setGoalStatus: unknown goal returns null', async () => {
154
154
  // =========================================================================
155
155
  // addGoalMember
156
156
  // =========================================================================
157
- await test('addGoalMember: upserts on the PK (re-join changes role, no dup-key)', async () => {
157
+ await test('addGoalMember: upsert PRESERVES the existing role (re-join cannot demote a lead)', async () => {
158
158
  const pool = fakePool((s) => (/^INSERT INTO goal_members/.test(s) ? { rows: [{ ...MEMBER_ROW, role: 'lead' }] } : null));
159
- const out = await addGoalMember({ goalId: 7, builderId: 42, role: 'lead' }, { pool });
160
- assert.equal(out.role, 'lead');
159
+ const out = await addGoalMember({ goalId: 7, builderId: 42, role: 'member' }, { pool });
160
+ assert.equal(out.role, 'lead', 'an existing lead row survives a role:member re-add');
161
161
  const ins = find(pool.queries, /^INSERT INTO goal_members/);
162
- assert.deepEqual(ins.params, [7, 42, 'lead']);
163
- assert.match(ins.sql, /ON CONFLICT \(goal_id, builder_id\) DO UPDATE SET role = EXCLUDED.role/, 'upsert, not plain insert');
162
+ assert.deepEqual(ins.params, [7, 42, 'member']);
163
+ assert.match(ins.sql, /ON CONFLICT \(goal_id, builder_id\) DO UPDATE SET role = goal_members\.role/,
164
+ 'conflict keeps the stored role โ€” role changes go through PATCH /goals/:id/members/:builderId only (task 1002689)');
164
165
  });
165
166
 
166
167
  await test('addGoalMember: role defaults to member', async () => {