@bongos/core 1.19.183 → 1.19.185

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.
Files changed (81) hide show
  1. package/.bongos-core.json +102 -92
  2. package/clients/bongos-client/bongos-client.global.js +19 -19
  3. package/clients/bongos-client/index.cjs +19 -19
  4. package/clients/bongos-client/index.d.ts +29 -29
  5. package/clients/bongos-client/index.mjs +19 -19
  6. package/docs/adr/0157-archon-is-rank-and-identity-only.md +2 -2
  7. package/docs/api/openapi.json +117 -117
  8. package/docs/api-reference.md +10 -10
  9. package/docs/canonical-permissions.md +6 -6
  10. package/modules/dev-box/app/src/vendor/bongos-client.cjs +19 -19
  11. package/modules/{governance → government}/catalog.js +8 -8
  12. package/modules/{governance → government}/config.js +6 -6
  13. package/modules/{governance → government}/db.js +44 -44
  14. package/modules/government/migrations/government_001_rename_from_governance.sql +213 -0
  15. package/modules/government/migrations/government_002_rename_permission_keys.sql +112 -0
  16. package/modules/government/module.json +22 -0
  17. package/modules/{governance → government}/protected-surfaces.json +0 -1
  18. package/modules/{governance/rank-roles.js → government/ranks.js} +18 -18
  19. package/modules/{governance/routes/governance.js → government/routes/government.js} +109 -84
  20. package/modules/{governance/routes/role-sync.js → government/routes/rank-sync.js} +7 -7
  21. package/modules/hall-ui/public/gate.js +1 -1
  22. package/modules/hall-ui/public/governance.js +38 -38
  23. package/modules/hall-ui/public/harbor.js +2 -2
  24. package/modules/hall-ui/public/shell.js +3 -3
  25. package/modules/hall-ui/public/watch.js +1 -1
  26. package/modules/lifecycle/db.js +6 -6
  27. package/modules/lifecycle/routes/gate-approvals.js +1 -1
  28. package/package-lock.json +2 -2
  29. package/package.json +1 -1
  30. package/scripts/gds/fitness.js +11 -1
  31. package/scripts/gds/migration-namespace.js +15 -3
  32. package/scripts/gds/publish-manifest.js +2 -3
  33. package/scripts/gds/seed-governance-tasks.js +3 -3
  34. package/src/bongos/auth.js +16 -16
  35. package/src/bongos/db.js +6 -6
  36. package/src/bongos/module-scope-map.js +7 -22
  37. package/src/bongos/permission-path-check.js +3 -3
  38. package/src/bongos/route-rank-check.js +15 -21
  39. package/src/bongos/serve-internal.js +2 -2
  40. package/src/module-api.js +7 -3
  41. package/tests/auth_page_gate.mjs +2 -2
  42. package/tests/criterion_authoring_gate.mjs +1 -1
  43. package/tests/governance_abuse_matrix.mjs +38 -38
  44. package/tests/governance_catalog.mjs +4 -4
  45. package/tests/governance_claim_eligibility.mjs +1 -1
  46. package/tests/governance_config.mjs +7 -7
  47. package/tests/governance_fold.mjs +4 -4
  48. package/tests/governance_fold_bench.mjs +2 -2
  49. package/tests/governance_live_proof.mjs +6 -6
  50. package/tests/governance_ownership_scope.mjs +2 -2
  51. package/tests/governance_page_gates.mjs +10 -10
  52. package/tests/governance_parity.mjs +3 -3
  53. package/tests/governance_principals.mjs +3 -3
  54. package/tests/governance_protected_surfaces.mjs +44 -19
  55. package/tests/governance_require_permission.mjs +11 -11
  56. package/tests/governance_resolver.mjs +4 -4
  57. package/tests/governance_role_sync.mjs +35 -35
  58. package/tests/governance_routes.mjs +98 -98
  59. package/tests/governance_seed.mjs +27 -4
  60. package/tests/governance_session_scope.mjs +5 -5
  61. package/tests/governance_vanilla_boot.mjs +4 -4
  62. package/tests/hall_page_gate_map.mjs +3 -3
  63. package/tests/memory_isolation.mjs +2 -2
  64. package/tests/module_loader.mjs +1 -1
  65. package/tests/onboarding_observability.mjs +2 -2
  66. package/tests/rank_tier_single_source.mjs +2 -2
  67. package/tests/route_permission_check.mjs +2 -2
  68. package/modules/governance/module.json +0 -11
  69. /package/config/{governance.neutral.json → government.neutral.json} +0 -0
  70. /package/modules/{governance → government}/CLAUDE.md +0 -0
  71. /package/modules/{governance → government}/migrations/governance_001_tables.sql +0 -0
  72. /package/modules/{governance → government}/migrations/governance_002_seed_and_backfill.sql +0 -0
  73. /package/modules/{governance → government}/migrations/governance_003_task_abandon_metic.sql +0 -0
  74. /package/modules/{governance → government}/migrations/governance_004_criterion_create.sql +0 -0
  75. /package/modules/{governance → government}/migrations/governance_005_resync_builder_rank_roles.sql +0 -0
  76. /package/modules/{governance → government}/migrations/governance_006_version_create.sql +0 -0
  77. /package/modules/{governance → government}/migrations/governance_007_delegate_archon_ops_to_metic.sql +0 -0
  78. /package/modules/{governance → government}/protected-surfaces.js +0 -0
  79. /package/modules/{governance → government}/resolver.js +0 -0
  80. /package/modules/{governance → government}/session-scopes.js +0 -0
  81. /package/modules/{governance → government}/session-scopes.json +0 -0
@@ -11,7 +11,7 @@
11
11
  // • this one is resolved by exactly three writers (the two _applyRankChangeTx
12
12
  // twins + the sign-in path) and is a TRANSACTION PARTICIPANT — it takes the
13
13
  // caller's client so the role rotation commits or rolls back with the rank write.
14
- // The port name is namespaced (`governance.roleSync`) like the `ideas.capture` /
14
+ // The port name is namespaced (`government.rankSync`) like the `ideas.capture` /
15
15
  // `art.getBuilderGeminiKeyRow` / `grade.builderRollingAvgGrade` ports.
16
16
  //
17
17
  // Registered from a route factory for the same reason routes/governance.js is:
@@ -28,16 +28,16 @@ const express = require('express');
28
28
  const api = require('../../../src/module-api');
29
29
  const db = require('../db');
30
30
 
31
- // DELEGATE, never spread — a test that stubs db.syncRankRole must still bite at call
31
+ // DELEGATE, never spread — a test that stubs db.syncRank must still bite at call
32
32
  // time (the modules/onboarding/port.js precedent; a spread captures the original
33
33
  // reference at module-load).
34
- const rankRoleSyncPort = {
35
- syncRankRole: (...a) => db.syncRankRole(...a),
34
+ const rankSyncPort = {
35
+ syncRank: (...a) => db.syncRank(...a),
36
36
  };
37
37
 
38
38
  function registerSeams() {
39
- if (!api.hasProvider('governance.roleSync')) {
40
- api.registerProvider('governance.roleSync', rankRoleSyncPort);
39
+ if (!api.hasProvider('government.rankSync')) {
40
+ api.registerProvider('government.rankSync', rankSyncPort);
41
41
  }
42
42
  }
43
43
 
@@ -46,4 +46,4 @@ module.exports = function buildGovernanceRoleSyncRouter() {
46
46
  return express.Router();
47
47
  };
48
48
 
49
- module.exports.rankRoleSyncPort = rankRoleSyncPort;
49
+ module.exports.rankSyncPort = rankSyncPort;
@@ -217,7 +217,7 @@
217
217
  me = null;
218
218
  }
219
219
  const rank = (me && (me.builder || me).rank || '').toLowerCase();
220
- // ADR 0157: the Gate page sits at the `page.view.governance` floor (metic). The
220
+ // ADR 0157: the Gate page sits at the `page.view.government` floor (metic). The
221
221
  // server's requireGovernancePage is the wall — this only decides whether to render the
222
222
  // body or the sealed notice, so it must agree or a Metic sees "sealed" on a page
223
223
  // they were just admitted to.
@@ -16,13 +16,13 @@
16
16
  // owner's 2026-08-16 decision, and the section header says so out loud.
17
17
  // • the ARCHON rank-role is read-only in full: it holds everything, and its
18
18
  // immutability is what makes it impossible to click your way into locking the
19
- // owner out of this tab. The API refuses it as `archon_role_immutable`.
20
- // ASSIGNMENT of any rank-role is still refused (`seeded_role_assignment_forbidden`)
19
+ // owner out of this tab. The API refuses it as `archon_rank_immutable`.
20
+ // ASSIGNMENT of any rank-role is still refused (`seeded_rank_assignment_forbidden`)
21
21
  // — you get a rank-role by holding the rank, so that dial is the rank itself.
22
22
  //
23
23
  // Like The Gate, this reads /api/bongos/me for a friendly Sealed notice —
24
- // DEFENCE IN DEPTH only. Every /governance/* endpoint (reads included) is ALSO
25
- // gated server-side on the `governance.manage` atom (archon floor). On 401 →
24
+ // DEFENCE IN DEPTH only. Every /government/* endpoint (reads included) is ALSO
25
+ // gated server-side on the `government.manage` atom (archon floor). On 401 →
26
26
  // web sign-in.
27
27
  //
28
28
  // DELIBERATELY NOT IN THE SIDEBAR YET: the nav entry + the server page gate are
@@ -64,27 +64,27 @@
64
64
  // The R110 refusal codes, translated for the person clicking.
65
65
  const ERROR_COPY = {
66
66
  permission_not_grantable: 'That permission can’t be granted to this role. (The server’s reply says which of the three walls applied.)',
67
- archon_role_immutable: 'The Archon role is read-only — it holds every permission, which is what keeps you from locking yourself out of this page.',
68
- role_has_no_default: 'Only a rank role has a catalog default to reset to. Revoke a custom role’s permissions individually instead.',
69
- seeded_role_assignment_forbidden: 'Rank roles are assigned by rank itself — change the builder’s rank instead.',
70
- role_exists: 'A role with that key already exists.',
71
- role_not_found: 'That role no longer exists — refreshing.',
67
+ archon_rank_immutable: 'The Archon role is read-only — it holds every permission, which is what keeps you from locking yourself out of this page.',
68
+ rank_has_no_default: 'Only a rank role has a catalog default to reset to. Revoke a custom role’s permissions individually instead.',
69
+ seeded_rank_assignment_forbidden: 'Rank roles are assigned by rank itself — change the builder’s rank instead.',
70
+ rank_exists: 'A role with that key already exists.',
71
+ rank_not_found: 'That role no longer exists — refreshing.',
72
72
  builder_not_found: 'No builder with that id.',
73
73
  unknown_permission: 'The permission catalog does not define that key.',
74
- bad_role_key: 'Role keys are kebab-case: lowercase letters, digits, dashes.',
74
+ bad_rank_key: 'Role keys are kebab-case: lowercase letters, digits, dashes.',
75
75
  validation_failed: 'Missing or invalid fields.',
76
76
  };
77
77
  const friendlyError = (code) => ERROR_COPY[code] || null;
78
78
 
79
79
  // ---- state ---------------------------------------------------------------
80
80
 
81
- let roles = []; // [{ role_key, label, description, is_system, seeded_rank, permissions: [] }]
81
+ let roles = []; // [{ rank_key, label, description, is_system, seeded_rank, permissions: [] }]
82
82
  let permissions = []; // catalog rows: [{ key, system, floor, guards, grantable }]
83
83
  let principals = []; // [{ system_role, permission, guards }]
84
- let assignments = []; // [{ builder_id, github_login, display_name, rank, role_key, role_label, is_system }]
84
+ let assignments = []; // [{ builder_id, github_login, display_name, rank, rank_key, role_label, is_system }]
85
85
  let selectedRoleKey = null;
86
86
 
87
- const selectedRole = () => roles.find((r) => r.role_key === selectedRoleKey) || null;
87
+ const selectedRole = () => roles.find((r) => r.rank_key === selectedRoleKey) || null;
88
88
 
89
89
  // ---- render: role list ---------------------------------------------------
90
90
 
@@ -102,11 +102,11 @@
102
102
  const btn = document.createElement('button');
103
103
  btn.type = 'button';
104
104
  btn.className = 'gov-role';
105
- btn.setAttribute('aria-pressed', role.role_key === selectedRoleKey ? 'true' : 'false');
105
+ btn.setAttribute('aria-pressed', role.rank_key === selectedRoleKey ? 'true' : 'false');
106
106
  btn.innerHTML =
107
- `<span class="gov-role__head"><span class="gov-role__label">${escapeHtml(role.label || role.role_key)}</span>${pill}</span>
108
- <span class="gov-role__meta"><code>${escapeHtml(role.role_key)}</code> · ${(role.permissions || []).length} permission${(role.permissions || []).length === 1 ? '' : 's'}</span>`;
109
- btn.addEventListener('click', () => { selectedRoleKey = role.role_key; renderRoles(); renderGrid(); });
107
+ `<span class="gov-role__head"><span class="gov-role__label">${escapeHtml(role.label || role.rank_key)}</span>${pill}</span>
108
+ <span class="gov-role__meta"><code>${escapeHtml(role.rank_key)}</code> · ${(role.permissions || []).length} permission${(role.permissions || []).length === 1 ? '' : 's'}</span>`;
109
+ btn.addEventListener('click', () => { selectedRoleKey = role.rank_key; renderRoles(); renderGrid(); });
110
110
  list.appendChild(btn);
111
111
  }
112
112
  }
@@ -176,7 +176,7 @@
176
176
  return 'The Archon role holds every permission and is read-only — that is what makes it impossible to lock yourself out of this page. To give a power away, hand it to one of the ranks below instead.';
177
177
  }
178
178
  if (isRankRole(role)) {
179
- return `Everything a ${role.label || role.role_key} can do, and yours to change — every builder at this rank is affected immediately. “Reset to default” puts it back to the shipped ladder.`;
179
+ return `Everything a ${role.label || role.rank_key} can do, and yours to change — every builder at this rank is affected immediately. “Reset to default” puts it back to the shipped ladder.`;
180
180
  }
181
181
  return 'Toggle the everyday permissions this role carries. Owner powers can only go to a rank role, not to a custom one.';
182
182
  }
@@ -185,7 +185,7 @@
185
185
  const grid = document.getElementById('gov-grid');
186
186
  const role = selectedRole();
187
187
  document.getElementById('gov-grid-title').textContent = role
188
- ? `Permissions — ${role.label || role.role_key}` : 'Permissions';
188
+ ? `Permissions — ${role.label || role.rank_key}` : 'Permissions';
189
189
  document.getElementById('gov-grid-note').textContent = gridNote(role);
190
190
 
191
191
  // The reset escape hatch — only a rank role has a catalog default, and the
@@ -223,11 +223,11 @@
223
223
  // Restore a rank role to the shipped ladder. Confirmed because it is the one
224
224
  // action here that discards work in bulk — a drifted role can be ~60 rows.
225
225
  async function resetRole(role, btn) {
226
- const label = role.label || role.role_key;
226
+ const label = role.label || role.rank_key;
227
227
  if (!window.confirm(`Reset ${label} to its default permissions?\n\nEvery change you've made to this rank is discarded, and it takes effect for every builder at this rank on their next request.`)) return;
228
228
  btn.disabled = true;
229
229
  try {
230
- const out = await sendJson('POST', `/governance/roles/${encodeURIComponent(role.role_key)}/reset`);
230
+ const out = await sendJson('POST', `/government/ranks/${encodeURIComponent(role.rank_key)}/reset`);
231
231
  toast(`${label} reset to its default ${out.permission_count} permissions.`);
232
232
  await reloadRoles();
233
233
  } catch (err) {
@@ -240,7 +240,7 @@
240
240
  async function toggleGrant(role, permissionKey, grant, btn) {
241
241
  btn.disabled = true;
242
242
  try {
243
- await sendJson(grant ? 'PUT' : 'DELETE', `/governance/roles/${encodeURIComponent(role.role_key)}/permissions/${encodeURIComponent(permissionKey)}`);
243
+ await sendJson(grant ? 'PUT' : 'DELETE', `/government/ranks/${encodeURIComponent(role.rank_key)}/permissions/${encodeURIComponent(permissionKey)}`);
244
244
  toast(grant ? `Granted ${permissionKey}.` : `Revoked ${permissionKey}.`);
245
245
  await reloadRoles();
246
246
  } catch (err) {
@@ -266,8 +266,8 @@
266
266
  row.className = 'gov-assign-row';
267
267
  const who = `${escapeHtml(a.display_name || a.github_login || `builder ${a.builder_id}`)} <span class="muted">(#${escapeHtml(String(a.builder_id))} · ${escapeHtml(a.rank || '')})</span>`;
268
268
  const rolePill = a.is_system
269
- ? `<span class="gov-pill gov-pill--seeded">${escapeHtml(a.role_label || a.role_key)} · via rank</span>`
270
- : `<span class="gov-pill gov-pill--custom">${escapeHtml(a.role_label || a.role_key)}</span>`;
269
+ ? `<span class="gov-pill gov-pill--seeded">${escapeHtml(a.role_label || a.rank_key)} · via rank</span>`
270
+ : `<span class="gov-pill gov-pill--custom">${escapeHtml(a.role_label || a.rank_key)}</span>`;
271
271
  const action = a.is_system ? '' : `<button type="button" class="gov-btn gov-btn--ghost gov-btn--small" data-act="unassign">Remove</button>`;
272
272
  row.innerHTML = `<span class="gov-assign-row__who">${who}</span>${rolePill}${action}`;
273
273
  const btn = row.querySelector('[data-act=unassign]');
@@ -281,15 +281,15 @@
281
281
  const sel = document.getElementById('gov-assign-role');
282
282
  const custom = roles.filter((r) => !r.is_system);
283
283
  sel.innerHTML = custom.length
284
- ? custom.map((r) => `<option value="${escapeHtml(r.role_key)}">${escapeHtml(r.label || r.role_key)}</option>`).join('')
284
+ ? custom.map((r) => `<option value="${escapeHtml(r.rank_key)}">${escapeHtml(r.label || r.rank_key)}</option>`).join('')
285
285
  : `<option value="" disabled selected>No custom roles yet</option>`;
286
286
  }
287
287
 
288
288
  async function unassign(a, btn) {
289
289
  btn.disabled = true;
290
290
  try {
291
- await sendJson('DELETE', `/governance/assignments/${encodeURIComponent(a.builder_id)}/${encodeURIComponent(a.role_key)}`);
292
- toast(`Removed ${a.role_label || a.role_key} from builder ${a.builder_id}.`);
291
+ await sendJson('DELETE', `/government/assignments/${encodeURIComponent(a.builder_id)}/${encodeURIComponent(a.rank_key)}`);
292
+ toast(`Removed ${a.role_label || a.rank_key} from builder ${a.builder_id}.`);
293
293
  await reloadAssignments();
294
294
  } catch (err) {
295
295
  if (err && err.kind === '401') return;
@@ -321,22 +321,22 @@
321
321
  // ---- loads ---------------------------------------------------------------
322
322
 
323
323
  async function reloadRoles() {
324
- const data = await fetchJson('/governance/roles');
324
+ const data = await fetchJson('/government/ranks');
325
325
  roles = data.roles || [];
326
- if (!roles.some((r) => r.role_key === selectedRoleKey)) selectedRoleKey = roles.length ? roles[0].role_key : null;
326
+ if (!roles.some((r) => r.rank_key === selectedRoleKey)) selectedRoleKey = roles.length ? roles[0].rank_key : null;
327
327
  renderRoles();
328
328
  renderGrid();
329
329
  renderAssignSelect();
330
330
  }
331
331
 
332
332
  async function reloadAssignments() {
333
- const data = await fetchJson('/governance/assignments');
333
+ const data = await fetchJson('/government/assignments');
334
334
  assignments = data.assignments || [];
335
335
  renderAssignments();
336
336
  }
337
337
 
338
338
  async function load() {
339
- const perms = await fetchJson('/governance/permissions');
339
+ const perms = await fetchJson('/government/permissions');
340
340
  permissions = perms.permissions || [];
341
341
  principals = perms.principals || [];
342
342
  await reloadRoles();
@@ -352,16 +352,16 @@
352
352
  const btn = e.target.querySelector('button');
353
353
  btn.disabled = true;
354
354
  const body = {
355
- role_key: document.getElementById('gov-create-key').value.trim(),
355
+ rank_key: document.getElementById('gov-create-key').value.trim(),
356
356
  label: document.getElementById('gov-create-label').value.trim(),
357
357
  };
358
358
  const desc = document.getElementById('gov-create-desc').value.trim();
359
359
  if (desc) body.description = desc;
360
360
  try {
361
- const r = await sendJson('POST', '/governance/roles', body);
362
- toast(`Created role ${r.role ? r.role.role_key : body.role_key}.`);
361
+ const r = await sendJson('POST', '/government/ranks', body);
362
+ toast(`Created role ${r.role ? r.role.rank_key : body.rank_key}.`);
363
363
  e.target.reset();
364
- selectedRoleKey = body.role_key;
364
+ selectedRoleKey = body.rank_key;
365
365
  await reloadRoles();
366
366
  } catch (err) {
367
367
  if (err && err.kind === '401') return;
@@ -379,7 +379,7 @@
379
379
  if (!roleKey) { toast('Create a custom role first.'); return; }
380
380
  btn.disabled = true;
381
381
  try {
382
- await sendJson('POST', '/governance/assignments', { builder_id: builderId, role_key: roleKey });
382
+ await sendJson('POST', '/government/assignments', { builder_id: builderId, rank_key: roleKey });
383
383
  toast(`Assigned ${roleKey} to builder ${builderId}.`);
384
384
  e.target.reset();
385
385
  await reloadAssignments();
@@ -402,8 +402,8 @@
402
402
 
403
403
  async function init() {
404
404
  // Confirm rank for a friendly sealed notice; the server gates the page and
405
- // every /governance endpoint on `governance.manage` regardless (cosmetic
406
- // defence-in-depth). governance.manage is system:true, so only the archon
405
+ // every /governance endpoint on `government.manage` regardless (cosmetic
406
+ // defence-in-depth). government.manage is system:true, so only the archon
407
407
  // rank-role can hold it — rank is an accurate client-side proxy.
408
408
  let me;
409
409
  try {
@@ -9,7 +9,7 @@
9
9
  // shows a restricted notice below the Metic floor — DEFENCE IN DEPTH only. The data +
10
10
  // mutation endpoints (/boxes, /boxes/:id/close, /boxes/:id/block) are ALSO gated
11
11
  // server-side on `box.fleet.manage` (ADR 0016), and the page itself is gated by
12
- // serve-internal.js requireGovernancePage — whose own floor is `page.view.governance`,
12
+ // serve-internal.js requireGovernancePage — whose own floor is `page.view.government`,
13
13
  // metic since ADR 0157. On a 401 it redirects into the web sign-in flow.
14
14
 
15
15
  (() => {
@@ -326,7 +326,7 @@
326
326
  me = null;
327
327
  }
328
328
  const rank = (me && (me.builder || me).rank || '').toLowerCase();
329
- // ADR 0157: Harbor sits at the `page.view.governance` floor (metic), and the fleet
329
+ // ADR 0157: Harbor sits at the `page.view.government` floor (metic), and the fleet
330
330
  // data it reads is `box.fleet.manage` (also metic now). Cosmetic —
331
331
  // requireGovernancePage + the route gates are the wall; this just picks body vs
332
332
  // sealed notice.
@@ -98,7 +98,7 @@
98
98
  // current (mirrors the old #hall-nav-work rule, task #833)
99
99
  // archon — rank === 'archon'
100
100
  // governance — rank is metic or archon (ADR 0157): the monitoring/ops pages.
101
- // Mirrors the server's `page.view.governance` floor, which that ADR
101
+ // Mirrors the server's `page.view.government` floor, which that ADR
102
102
  // lowered archon→metic. Cosmetic only — requireGovernancePage is the wall.
103
103
  // `view` marks an index view (hash-routed); `module` gates on window.__MODULES__.
104
104
  // task 2175: grouped by AUDIENCE of the data, so the sidebar reads in coherent
@@ -178,7 +178,7 @@
178
178
  { id: 'sessions', label: 'Sessions', icon: 'admin', href: '/sessions', gate: 'governance' },
179
179
  // BV1.R113: the roles-and-permissions tab (ADR 0151). The one genuinely
180
180
  // Archon-only item the 'governance' gate comment anticipated — cosmetic
181
- // only; requireGovernanceManagePage (the governance.manage atom) is the wall.
181
+ // only; requireGovernanceManagePage (the government.manage atom) is the wall.
182
182
  { id: 'governance', label: 'Roles', icon: 'admin', href: '/governance', gate: 'archon' },
183
183
  ] },
184
184
  ];
@@ -420,7 +420,7 @@
420
420
  case 'member': return member;
421
421
  case 'work': return authed && (member || access.showWork);
422
422
  case 'archon': return authed && access.rank === 'archon';
423
- // ADR 0157: the monitoring/ops pages sit at the `page.view.governance` floor,
423
+ // ADR 0157: the monitoring/ops pages sit at the `page.view.government` floor,
424
424
  // which is metic. Kept as its own key rather than widening 'archon', so a
425
425
  // genuinely Archon-only nav item can still be added later.
426
426
  case 'governance': return authed && (access.rank === 'metic' || access.rank === 'archon');
@@ -885,7 +885,7 @@
885
885
  return;
886
886
  }
887
887
 
888
- // ADR 0157: Watch sits at the `page.view.governance` floor (metic), and every panel
888
+ // ADR 0157: Watch sits at the `page.view.government` floor (metic), and every panel
889
889
  // it reads (roster, security reports, audit log, sessions, box costs) delegated to
890
890
  // metic with it. Cosmetic — requireGovernancePage + the route gates are the wall.
891
891
  if (!builder || (builder.rank !== 'metic' && builder.rank !== 'archon')) {
@@ -220,7 +220,7 @@ function xenosClaimAllowed(rank, newcomerFriendly) {
220
220
  // 0016 — resolved server-side, uncached), and taking a second pool connection while
221
221
  // a tx holds FOR UPDATE locks is a needless deadlock/exhaustion hazard.
222
222
  async function claimPermissionsFor(builderId) {
223
- const governance = seams.resolveOptional('governance');
223
+ const governance = seams.resolveOptional('government');
224
224
  if (!governance || typeof governance.resolveBuilderPermissions !== 'function') return null;
225
225
  try {
226
226
  const keys = await governance.resolveBuilderPermissions(builderId);
@@ -408,12 +408,12 @@ function disambiguateWorktreeName(desired, taken) {
408
408
  //
409
409
  // Returns the updated builder row, or throws { code:'BUILDER_NOT_FOUND' } /
410
410
  // { code:'RANK_UNCHANGED' }.
411
- // syncGovernanceRankRole — keep governance_builder_roles in step with builders.rank
411
+ // syncGovernanceRankRole — keep government_builder_ranks in step with builders.rank
412
412
  // (BV1.R95b, ADR 0151). This module's rank writer backs the automatic xenos→thetes
413
413
  // graduation, which had the same hole as the Archon-driven path: the rank moved and
414
414
  // the role did not, so a freshly-graduated Thetes kept the xenos role.
415
415
  //
416
- // Resolved through the `governance.roleSync` kernel port (never a sibling require —
416
+ // Resolved through the `government.rankSync` kernel port (never a sibling require —
417
417
  // ADR 0083) and run on the CALLER's transaction client, so the rotation commits
418
418
  // atomically with the ship + graduation. A missing port (governance disabled) skips:
419
419
  // no role ⇒ zero permissions, fail-closed.
@@ -421,9 +421,9 @@ function disambiguateWorktreeName(desired, taken) {
421
421
  // TWINNED with src/bongos/db.js (the boundary-forced _applyRankChangeTx duplication,
422
422
  // ADR 0091 §2) — keep the two in sync.
423
423
  async function syncGovernanceRankRole(client, builderId, rank) {
424
- const roleSync = seams.resolveOptional('governance.roleSync');
425
- if (!roleSync || typeof roleSync.syncRankRole !== 'function') return null;
426
- return roleSync.syncRankRole(client, { builderId, rank });
424
+ const roleSync = seams.resolveOptional('government.rankSync');
425
+ if (!roleSync || typeof roleSync.syncRank !== 'function') return null;
426
+ return roleSync.syncRank(client, { builderId, rank });
427
427
  }
428
428
 
429
429
  // _applyRankChangeTx — the in-transaction CORE of a rank change, shared by the
@@ -68,7 +68,7 @@ module.exports = function buildGateApprovalsRouter() {
68
68
  // BV1.R105 left this on requireRank because `gate_approval.decide` was declared over
69
69
  // the /approve WRITE only and no atom named the read. ADR 0157 extends the catalog
70
70
  // with exactly that atom — `gate_approval.read`, floor metic — so the mapping is now
71
- // one the catalog makes. The split is load-bearing, not cosmetic: `page.view.governance`
71
+ // one the catalog makes. The split is load-bearing, not cosmetic: `page.view.government`
72
72
  // opens /gate to Metic, and without a Metic-readable data endpoint that page would
73
73
  // render nothing but a 403. Approving a held PR stays archon (`.decide`, system).
74
74
  router.get('/gate-approvals', auth.requireBuilder, auth.requirePermission('gate_approval.read'), async (req, res) => {
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.183",
3
+ "version": "1.19.185",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.183",
9
+ "version": "1.19.185",
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.183",
3
+ "version": "1.19.185",
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",
@@ -732,6 +732,16 @@ const LEGACY_NAMESPACED = {
732
732
  // game module: world_events + players were created before the module system (V1 schema). The
733
733
  // schema_migrations table is a core tracking table that every migration may re-create idempotently.
734
734
  game: new Set(['world_events', 'players', 'schema_migrations']),
735
+ // government module: the module KEY was renamed governance→government (ADR 0174, goal 1000068),
736
+ // so its own historical migrations now read as "outside the namespace" purely because the key
737
+ // moved underneath them. These three tables ARE this module's — under the name it used to have.
738
+ // They are not renamed in place because schema_migrations is keyed by filename STEM: renaming an
739
+ // already-applied migration file makes it look unapplied and re-runs it, and governance_002 is a
740
+ // seed/backfill. government_001_rename_from_governance.sql renames the live tables forward; these
741
+ // entries let the historical files that created them keep describing what actually happened.
742
+ government: new Set([
743
+ 'governance_roles', 'governance_builder_roles', 'governance_role_permissions',
744
+ ]),
735
745
  };
736
746
  const DESTRUCTIVE_DDL = [
737
747
  { re: /\bdrop\s+table\b/i, label: 'DROP TABLE' },
@@ -1141,7 +1151,7 @@ function checkKernelPromptsNoHardcodedIdentity() {
1141
1151
  // this check nothing in CI could see it.
1142
1152
  //
1143
1153
  // Only the NEUTRAL packs are scanned, deliberately: an instance pack (config/
1144
- // branding.json, config/governance.json) is host content that legitimately names
1154
+ // branding.json, config/government.json) is host content that legitimately names
1145
1155
  // its host, and is boundary-excluded from the published core anyway.
1146
1156
  function checkNeutralConfigsNoHostIdentity() {
1147
1157
  const violations = [];
@@ -57,6 +57,15 @@ const LEGACY_BARE_CORE_MAX = 187;
57
57
  // module migrations must be `<key>_`-prefixed; these stay bare (already applied on prod).
58
58
  const LEGACY_BARE_MODULES = new Set(['game', 'demo']);
59
59
 
60
+ // RENAMED_MODULE_PREFIXES: a module whose KEY was renamed keeps its historical
61
+ // migration-filename prefix for files that were already applied. schema_migrations
62
+ // is keyed by filename STEM, so renaming an applied migration makes it look
63
+ // unapplied and RE-RUNS it — and a re-run seed/backfill is a data event, not a
64
+ // no-op. The already-applied files therefore keep the old prefix forever; only
65
+ // NEW migrations must carry the module's current namespace.
66
+ // government <- governance (ADR 0174, goal 1000068)
67
+ const RENAMED_MODULE_PREFIXES = { government: new Set(['governance']) };
68
+
60
69
  // ---- parsing ---------------------------------------------------------------
61
70
 
62
71
  const BARE_RE = /^(\d{3,})_(.+)\.sql$/;
@@ -116,9 +125,11 @@ function filenameViolation(source, basename, { moduleKey } = {}) {
116
125
  ? null // legacy bare module — grandfathered
117
126
  : `module "${moduleKey}" migration "${basename}" is bare — a NEW module migration must be ${ns}_-prefixed (ADR 0083 §Decision #5 / ADR 0108 §5): use ${ns}_${pad3(p.number)}_${p.slug}.sql`;
118
127
  }
119
- return p.prefix === ns
120
- ? null
121
- : `module "${moduleKey}" migration "${basename}" carries the "${p.prefix}_" prefix — it must be ${ns}_-prefixed to its own module (ADR 0083 §Decision #5).`;
128
+ if (p.prefix === ns) return null;
129
+ // A renamed module's already-applied files keep their historical prefix (see
130
+ // RENAMED_MODULE_PREFIXES) — the stem is the schema_migrations key, so it cannot move.
131
+ if ((RENAMED_MODULE_PREFIXES[moduleKey] || new Set()).has(p.prefix)) return null;
132
+ return `module "${moduleKey}" migration "${basename}" carries the "${p.prefix}_" prefix — it must be ${ns}_-prefixed to its own module (ADR 0083 §Decision #5).`;
122
133
  }
123
134
 
124
135
  // ---- cross-root stem-collision detector ------------------------------------
@@ -186,6 +197,7 @@ function lineageReport({ core = [], instance = [], modulesByKey = {} } = {}) {
186
197
 
187
198
  module.exports = {
188
199
  CORE_NS,
200
+ RENAMED_MODULE_PREFIXES,
189
201
  INSTANCE_NS,
190
202
  RESERVED_NS,
191
203
  LEGACY_BARE_CORE_MAX,
@@ -61,7 +61,6 @@ const PUBLISH_ALLOWLIST = [
61
61
  'modules/sessions/',
62
62
  'modules/autonomy/',
63
63
  'modules/security/',
64
- 'modules/governance/', // configurable roles & permissions (BV1 goal 1000030, ADR 0151) — the rank/permission machinery publishes AS the open-source methodology (ADR 0065 §4 / ADR 0098); it is in PROTECTED_GLOBS for write-protection, NOT publish-exclusion. default:true. Renamed → modules/government/ (ADR 0174); dropped with the folder in goal 1000068 R04.
65
64
  'modules/government/', // the same machinery under its post-ADR-0174 name (goal 1000068, task 1003003). Added in lockstep with the protected-surfaces.json entry: publish_manifest asserts every PROTECTED_GLOB is either topology-excluded or publishable, so registering the glob as protected WITHOUT this line default-denies the module from the published core — the open-source methodology would silently lose its permission machinery at the next release.
66
65
  'modules/hall-ui/',
67
66
  'modules/status-ui/',
@@ -75,7 +74,7 @@ const PUBLISH_ALLOWLIST = [
75
74
  'config/design-tokens.neutral.json',
76
75
  'config/design-tokens.schema.json', // the design-token contract (generic schema, no instance values)
77
76
  'config/hierarchy.neutral.json',
78
- 'config/governance.neutral.json', // the starting role templates a fresh instance inherits (BV1.R96)
77
+ 'config/government.neutral.json', // the starting rank templates a fresh instance inherits (BV1.R96; renamed from governance.neutral.json by ADR 0174)
79
78
  // --- the portable methodology core, governance, and sanitized docs ---
80
79
  'CLAUDE.md',
81
80
  'README.md',
@@ -212,7 +211,7 @@ const INSTANCE_EXCLUDE = [
212
211
  'config/modules.json',
213
212
  'config/devbox-app.json',
214
213
  'config/hierarchy.json',
215
- 'config/governance.json',
214
+ 'config/government.json',
216
215
  'CODEOWNERS', // names the owner (a protected surface, excluded as identity)
217
216
  // instance SEED migrations — this host's scope-truth SQL (planning seeds, world data).
218
217
  // ADR 0105: a consumer's core must not carry them; migrate.sh reads instance seeds from
@@ -195,10 +195,10 @@ const TASKS = [
195
195
  {
196
196
  ref: 'R96', kind: 'feature', priority: 4, est_minutes: 100, manual_degree: 2, credits_reward: 40,
197
197
  criterion_ids: [C4], depends_on_refs: ['R93', 'R94'],
198
- title: 'config/governance.json + neutral + resolve-over-neutral',
198
+ title: 'config/government.json + neutral + resolve-over-neutral',
199
199
  description: `**Why:** role templates/defaults are per-instance config (the branding pattern); live data is DB.
200
200
 
201
- **Done when:** \`config/governance.json\` (instance) resolves over \`config/governance.neutral.json\` (neutral) with env override on top, mirroring src/branding.js precedence; it seeds the starting role templates a fresh instance inherits; live assignments stay in the DB (R93). Depends on R93/R94.`,
201
+ **Done when:** \`config/government.json\` (instance) resolves over \`config/government.neutral.json\` (neutral) with env override on top, mirroring src/branding.js precedence; it seeds the starting role templates a fresh instance inherits; live assignments stay in the DB (R93). Depends on R93/R94.`,
202
202
  },
203
203
  {
204
204
  ref: 'R102', kind: 'verify', priority: 3, est_minutes: 70, manual_degree: 2, credits_reward: 35,
@@ -216,7 +216,7 @@ const TASKS = [
216
216
  title: 'Governance API routes (roles/perms/assignments CRUD)',
217
217
  description: `**Why:** the tab's server surface.
218
218
 
219
- **Done when:** routes list roles/permissions/assignments, create a role, toggle an operational permission on a role, and assign a role to a builder — all gated \`governance.manage\` (Archon), system permissions rejected as non-grantable server-side, and pinned in route-rank-check. Depends on R97 + R109.`,
219
+ **Done when:** routes list roles/permissions/assignments, create a role, toggle an operational permission on a role, and assign a role to a builder — all gated \`government.manage\` (Archon), system permissions rejected as non-grantable server-side, and pinned in route-rank-check. Depends on R97 + R109.`,
220
220
  },
221
221
  {
222
222
  ref: 'R111', kind: 'feature', priority: 3, est_minutes: 180, manual_degree: 2, credits_reward: 55,
@@ -367,7 +367,7 @@ async function maybeSeatFirstAdmin(builder) {
367
367
  // flips xenos→archon outside any transaction), and it quietly repairs a builder whose
368
368
  // assignment drifted for any other reason.
369
369
  //
370
- // Resolved through the `governance.roleSync` kernel port — auth.js is a KERNEL_FILE
370
+ // Resolved through the `government.rankSync` kernel port — auth.js is a KERNEL_FILE
371
371
  // and may not require a module (ADR 0083 / ADR 0091 §1). Runs on the pool: there is
372
372
  // no surrounding transaction here, unlike the two rank-change paths, which pass their
373
373
  // own client so the rotation is atomic with the rank write.
@@ -377,8 +377,8 @@ async function maybeSeatFirstAdmin(builder) {
377
377
  async function safeSyncRankRole(builder) {
378
378
  if (!builder || builder.id == null) return;
379
379
  try {
380
- await seams.resolveOptional('governance.roleSync')
381
- ?.syncRankRole(null, { builderId: builder.id, rank: builder.rank });
380
+ await seams.resolveOptional('government.rankSync')
381
+ ?.syncRank(null, { builderId: builder.id, rank: builder.rank });
382
382
  } catch (err) {
383
383
  console.error('[auth] rank-role sync failed (non-fatal):', err.message);
384
384
  }
@@ -1195,8 +1195,8 @@ async function resolveSession(req) {
1195
1195
  // a behaviour-identical remodelling of the #919 gate, not a re-gating of it.
1196
1196
  //
1197
1197
  // GOVERNANCE OWNS THE POLICY, THE KERNEL STAYS THE ENFORCER. The declaration is
1198
- // governance-owned data in modules/governance/session-scopes.json, validated
1199
- // against the permission catalog at load by modules/governance/session-scopes.js.
1198
+ // governance-owned data in modules/government/session-scopes.json, validated
1199
+ // against the permission catalog at load by modules/government/session-scopes.js.
1200
1200
  // Core reads it as DATA with fs — never `require` (ADR 0083's one-way rule: a
1201
1201
  // module's code and enable/disable state must never be something the kernel
1202
1202
  // depends on). The sibling trust primitive permission-path-check.js reads the
@@ -1211,7 +1211,7 @@ async function resolveSession(req) {
1211
1211
  // registry does not declare is DENIED rather than defaulting to full authority. A
1212
1212
  // null/absent source is a DECLARED row (rows minted before migration 090 added the
1213
1213
  // column, which predate the 'box' source existing), not an implicit default.
1214
- const SESSION_SCOPE_REGISTRY_PATH = path.join(__dirname, '..', '..', 'modules', 'governance', 'session-scopes.json');
1214
+ const SESSION_SCOPE_REGISTRY_PATH = path.join(__dirname, '..', '..', 'modules', 'government', 'session-scopes.json');
1215
1215
 
1216
1216
  // Pure: validate + normalize the parsed registry. Split from the file read so the
1217
1217
  // shipped assertions are what the tests exercise. `label` identifies the file in
@@ -1437,10 +1437,10 @@ function signInWithReturn(req) {
1437
1437
  // ranks (catalog.RANK_ORDER):
1438
1438
  // requireBuilderPage was "any session" floor xenos → page.view.public
1439
1439
  // requireNonXenosPage was rank !== 'xenos' floor thetes → page.view.builder
1440
- // requireGovernancePage was rank === 'archon' floor archon → page.view.governance
1440
+ // requireGovernancePage was rank === 'archon' floor archon → page.view.government
1441
1441
  // That third gate was spelled `requireArchonPage` until ADR 0157 lowered
1442
- // page.view.governance's floor archon→metic and the name stopped being true; the
1443
- // floor now lives in modules/governance/catalog.js, not in the function's name.
1442
+ // page.view.government's floor archon→metic and the name stopped being true; the
1443
+ // floor now lives in modules/government/catalog.js, not in the function's name.
1444
1444
  // The one accepted divergence is the one governance_002 already documents
1445
1445
  // (tests/governance_parity.mjs Part D): a builder on a reserved/divine rank holds
1446
1446
  // no seeded role, so they now fail a page gate a bare rank test would have
@@ -1456,8 +1456,8 @@ function signInWithReturn(req) {
1456
1456
  const PAGE_GATES = Object.freeze({
1457
1457
  requireBuilderPage: Object.freeze({ permission: 'page.view.public', deniedTo: '/builders' }),
1458
1458
  requireNonXenosPage: Object.freeze({ permission: 'page.view.builder', deniedTo: '/builders/not-ready' }),
1459
- requireGovernancePage: Object.freeze({ permission: 'page.view.governance', deniedTo: '/builders' }),
1460
- requireGovernanceManagePage: Object.freeze({ permission: 'governance.manage', deniedTo: '/builders' }),
1459
+ requireGovernancePage: Object.freeze({ permission: 'page.view.government', deniedTo: '/builders' }),
1460
+ requireGovernanceManagePage: Object.freeze({ permission: 'government.manage', deniedTo: '/builders' }),
1461
1461
  requireProjectCuratePage: Object.freeze({ permission: 'project.curate', deniedTo: '/builders' }),
1462
1462
  });
1463
1463
 
@@ -1476,7 +1476,7 @@ const PAGE_GATES = Object.freeze({
1476
1476
  // through to the DB resolve, so the fold can only ever be an optimization.
1477
1477
  async function resolveHeldPermissions(builderId, rawKeys) {
1478
1478
  if (builderId === null || builderId === undefined || builderId === '') return null;
1479
- const governance = seams.resolveOptional('governance');
1479
+ const governance = seams.resolveOptional('government');
1480
1480
  if (!governance || typeof governance.resolveBuilderPermissions !== 'function') return null;
1481
1481
  if (Array.isArray(rawKeys) && typeof governance.effectivePermissions === 'function') {
1482
1482
  try {
@@ -1534,7 +1534,7 @@ async function requireBuilderPage(req, res, next) {
1534
1534
  }
1535
1535
 
1536
1536
  // The /watch monitoring surface, the /harbor dev-box page and the /gate page
1537
- // (#726, idea 257, task 1179) — Metic and up, per `page.view.governance`'s catalog
1537
+ // (#726, idea 257, task 1179) — Metic and up, per `page.view.government`'s catalog
1538
1538
  // floor. Named `requireArchonPage` until ADR 0157 lowered that floor archon→metic
1539
1539
  // (older ADRs still use the old name; this line is the trail). Defense-in-depth:
1540
1540
  // every data endpoint these pages call is independently gated server-side, so no
@@ -1545,7 +1545,7 @@ async function requireGovernancePage(req, res, next) {
1545
1545
  }
1546
1546
 
1547
1547
  // The /governance roles-and-permissions tab (BV1.R113) — gated on the
1548
- // `governance.manage` ATOM itself (system, archon floor), the same key every
1548
+ // `government.manage` ATOM itself (system, archon floor), the same key every
1549
1549
  // /governance/* data route checks, so the page layer can never drift wider than
1550
1550
  // the API it fronts. Defense-in-depth as above: the data endpoints are all
1551
1551
  // independently gated; this keeps the tab shell from rendering for anyone else.
@@ -1716,14 +1716,14 @@ function requireRank(...allowed) {
1716
1716
  // time — never hardcoded prose, which goes stale when a floor moves (the
1717
1717
  // D-16/N-3 failure shape ADR 0157 left behind).
1718
1718
  //
1719
- // Usage: router.post('/roles', auth.requireBuilder, auth.requirePermission('governance.manage'), handler)
1719
+ // Usage: router.post('/roles', auth.requireBuilder, auth.requirePermission('government.manage'), handler)
1720
1720
  function requirePermission(...perms) {
1721
1721
  const required = Object.freeze([...perms]);
1722
1722
  return async function permissionGate(req, res, next) {
1723
1723
  const deny = (held) => {
1724
1724
  const missing = held === null ? [...required] : required.filter((key) => !held.includes(key));
1725
1725
  const floors = {};
1726
- const governance = seams.resolveOptional('governance');
1726
+ const governance = seams.resolveOptional('government');
1727
1727
  if (governance && typeof governance.permissionFloor === 'function') {
1728
1728
  for (const key of missing) {
1729
1729
  const floor = governance.permissionFloor(key);