@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
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // 1. Gate classification + pins: route-rank-check's own classifier is run over
6
6
  // the real routes source — every /governance route must gate on the
7
- // `governance.manage` atom through real middleware, and every declared
7
+ // `government.manage` atom through real middleware, and every declared
8
8
  // route must be pinned in MODULE_EXPECTED_RANKS. The pin set and the
9
9
  // declared set are compared BOTH ways, so adding an unpinned governance
10
10
  // route (or orphaning a pin) fails here before the grader sees it.
@@ -17,7 +17,7 @@
17
17
  // tests assert that, so a later tightening can't quietly re-close the owner's
18
18
  // lever); the ARCHON rank-role and every SUBSTRATE key stay refused; rank-role
19
19
  // ASSIGNMENT is still rank-derived (R95b); and every route denies without the
20
- // `governance.manage` grant.
20
+ // `government.manage` grant.
21
21
  //
22
22
  // Run: node tests/governance_routes.mjs
23
23
 
@@ -33,53 +33,53 @@ const require = createRequire(import.meta.url);
33
33
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
34
34
  const api = require(path.join(ROOT, 'src', 'module-api.js'));
35
35
  const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
36
- const catalog = require(path.join(ROOT, 'modules', 'governance', 'catalog.js'));
37
- const govDb = require(path.join(ROOT, 'modules', 'governance', 'db.js'));
36
+ const catalog = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
37
+ const govDb = require(path.join(ROOT, 'modules', 'government', 'db.js'));
38
38
  const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
39
39
  const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
40
40
  const express = require('express');
41
41
 
42
- const ROUTES_SRC = path.join(ROOT, 'modules', 'governance', 'routes', 'governance.js');
42
+ const ROUTES_SRC = path.join(ROOT, 'modules', 'government', 'routes', 'government.js');
43
43
 
44
44
  // ===========================================================================
45
45
  // Layer 1 — gate classification + route-rank-check pins
46
46
  // ===========================================================================
47
47
 
48
48
  const classified = rrc.classifyText(fs.readFileSync(ROUTES_SRC, 'utf8'))
49
- .filter((r) => r.route.startsWith('/governance'));
49
+ .filter((r) => r.route.startsWith('/government'));
50
50
 
51
51
  test('the routes file declares the full R110 surface', () => {
52
52
  const declared = new Set(classified.map((r) => r.key));
53
53
  const wanted = [
54
- 'GET /governance/roles',
55
- 'GET /governance/permissions',
56
- 'GET /governance/assignments',
57
- 'POST /governance/roles',
58
- 'PUT /governance/roles/:roleKey/permissions/:permissionKey',
59
- 'DELETE /governance/roles/:roleKey/permissions/:permissionKey',
60
- 'POST /governance/roles/:roleKey/reset',
61
- 'POST /governance/assignments',
62
- 'DELETE /governance/assignments/:builderId/:roleKey',
54
+ 'GET /government/ranks',
55
+ 'GET /government/permissions',
56
+ 'GET /government/assignments',
57
+ 'POST /government/ranks',
58
+ 'PUT /government/ranks/:rankKey/permissions/:permissionKey',
59
+ 'DELETE /government/ranks/:rankKey/permissions/:permissionKey',
60
+ 'POST /government/ranks/:rankKey/reset',
61
+ 'POST /government/assignments',
62
+ 'DELETE /government/assignments/:builderId/:rankKey',
63
63
  ];
64
64
  for (const key of wanted) assert.ok(declared.has(key), `missing route ${key}`);
65
65
  });
66
66
 
67
- test('every /governance route gates on governance.manage via REAL middleware', () => {
68
- assert.ok(classified.length > 0, 'no /governance routes found to classify');
67
+ test('every /governance route gates on government.manage via REAL middleware', () => {
68
+ assert.ok(classified.length > 0, 'no /government routes found to classify');
69
69
  for (const r of classified) {
70
70
  assert.equal(r.via, 'middleware', `${r.key}: gate must be middleware, not an annotation`);
71
- assert.deepEqual(r.perms, ['governance.manage'], `${r.key}: must gate on exactly governance.manage`);
72
- assert.equal(r.rank, 'archon', `${r.key}: governance.manage floors at archon`);
71
+ assert.deepEqual(r.perms, ['government.manage'], `${r.key}: must gate on exactly government.manage`);
72
+ assert.equal(r.rank, 'archon', `${r.key}: government.manage floors at archon`);
73
73
  }
74
74
  });
75
75
 
76
76
  test('every declared /governance route is pinned, and every pin exists (both directions)', () => {
77
- const pins = rrc.MODULE_EXPECTED_RANKS.governance;
78
- assert.ok(pins, 'MODULE_EXPECTED_RANKS has no governance block');
77
+ const pins = rrc.MODULE_EXPECTED_RANKS.government;
78
+ assert.ok(pins, 'MODULE_EXPECTED_RANKS has no government block');
79
79
  const declared = new Set(classified.map((r) => r.key));
80
80
  for (const key of declared) {
81
- assert.equal(pins[key], `${rrc.PERM_PIN_PREFIX}governance.manage`,
82
- `${key} must be pinned to perm:governance.manage`);
81
+ assert.equal(pins[key], `${rrc.PERM_PIN_PREFIX}government.manage`,
82
+ `${key} must be pinned to perm:government.manage`);
83
83
  }
84
84
  for (const key of Object.keys(pins)) {
85
85
  assert.ok(declared.has(key), `pin ${key} names a route the file no longer declares`);
@@ -88,7 +88,7 @@ test('every declared /governance route is pinned, and every pin exists (both dir
88
88
 
89
89
  test('checkRouteRanks passes over the real tree (no governance findings)', () => {
90
90
  const result = rrc.checkRouteRanks();
91
- const ours = result.findings.filter((f) => String(f.file).includes('modules/governance/'));
91
+ const ours = result.findings.filter((f) => String(f.file).includes('modules/government/'));
92
92
  assert.deepEqual(ours, [], `governance findings: ${JSON.stringify(ours, null, 2)}`);
93
93
  });
94
94
 
@@ -99,18 +99,18 @@ test('checkRouteRanks passes over the real tree (no governance findings)', () =>
99
99
  // The db stub: every function the routes call, recording args. Reset per test.
100
100
  const calls = [];
101
101
  const stub = {
102
- listRolesWithGrants: async () => [{ role_key: 'metic', label: 'Metic', description: null, is_system: true, seeded_rank: 'metic', created_by: null, created_at: 't', permissions: ['task.create'] }],
103
- listAssignments: async () => [{ builder_id: 7, github_login: 'x', display_name: 'X', rank: 'metic', role_key: 'metic', role_label: 'Metic', is_system: true, seeded_rank: 'metic', assigned_by: null, assigned_at: 't' }],
102
+ listRolesWithGrants: async () => [{ rank_key: 'metic', label: 'Metic', description: null, is_system: true, seeded_rank: 'metic', created_by: null, created_at: 't', permissions: ['task.create'] }],
103
+ listAssignments: async () => [{ builder_id: 7, github_login: 'x', display_name: 'X', rank: 'metic', rank_key: 'metic', role_label: 'Metic', is_system: true, seeded_rank: 'metic', assigned_by: null, assigned_at: 't' }],
104
104
  // Every rank name is a seeded rank-role; anything else is a custom one. Keyed off
105
105
  // the catalog's ladder rather than a hand-listed pair, so a rank added to
106
106
  // RANK_ORDER is exercised here instead of quietly falling through as "custom".
107
107
  getRoleByKey: async (k) => (k === 'missing' ? null : {
108
- role_key: k,
108
+ rank_key: k,
109
109
  label: k,
110
110
  is_system: catalog.RANK_ORDER.includes(k),
111
111
  seeded_rank: catalog.RANK_ORDER.includes(k) ? k : null,
112
112
  }),
113
- createRole: async (a) => ({ role_key: a.roleKey, label: a.label, description: a.description ?? null, is_system: false, seeded_rank: null, created_by: a.createdBy, created_at: 't' }),
113
+ createRole: async (a) => ({ rank_key: a.rankKey, label: a.label, description: a.description ?? null, is_system: false, seeded_rank: null, created_by: a.createdBy, created_at: 't' }),
114
114
  grantRolePermission: async () => true,
115
115
  revokeRolePermission: async () => true,
116
116
  resetRoleGrants: async ({ permissionKeys }) => permissionKeys.length,
@@ -131,11 +131,11 @@ api.requireBuilder = (req, res, next) => { req.builder = { id: 42, rank: 'archon
131
131
 
132
132
  function grant(keys) {
133
133
  seams._reset();
134
- seams.registerProvider('governance', { resolveBuilderPermissions: async () => keys });
134
+ seams.registerProvider('government', { resolveBuilderPermissions: async () => keys });
135
135
  }
136
- grant(['governance.manage']);
136
+ grant(['government.manage']);
137
137
 
138
- const buildGovernanceRouter = require(path.join(ROOT, 'modules', 'governance', 'routes', 'governance.js'));
138
+ const buildGovernanceRouter = require(path.join(ROOT, 'modules', 'government', 'routes', 'government.js'));
139
139
  const app = express();
140
140
  app.use(express.json());
141
141
  app.use(attachFail);
@@ -165,41 +165,41 @@ async function call(method, p, body) {
165
165
 
166
166
  // --- the deny wall -----------------------------------------------------------
167
167
 
168
- test('without governance.manage EVERY route 403s (reads included)', async () => {
168
+ test('without government.manage EVERY route 403s (reads included)', async () => {
169
169
  grant(['idea.triage', 'task.create']);
170
170
  for (const [method, p] of [
171
- ['GET', '/governance/roles'],
172
- ['GET', '/governance/permissions'],
173
- ['GET', '/governance/assignments'],
174
- ['POST', '/governance/roles'],
175
- ['PUT', '/governance/roles/r/permissions/task.create'],
176
- ['DELETE', '/governance/roles/r/permissions/task.create'],
177
- ['POST', '/governance/assignments'],
178
- ['DELETE', '/governance/assignments/7/r'],
171
+ ['GET', '/government/ranks'],
172
+ ['GET', '/government/permissions'],
173
+ ['GET', '/government/assignments'],
174
+ ['POST', '/government/ranks'],
175
+ ['PUT', '/government/ranks/r/permissions/task.create'],
176
+ ['DELETE', '/government/ranks/r/permissions/task.create'],
177
+ ['POST', '/government/assignments'],
178
+ ['DELETE', '/government/assignments/7/r'],
179
179
  ]) {
180
180
  const r = await call(method, p, method === 'GET' ? undefined : {});
181
181
  assert.equal(r.status, 403, `${method} ${p} must deny`);
182
182
  assert.equal(r.json.error.code, 'permission_forbidden', `${method} ${p}`);
183
183
  }
184
- grant(['governance.manage']);
184
+ grant(['government.manage']);
185
185
  });
186
186
 
187
187
  // --- reads -------------------------------------------------------------------
188
188
 
189
- test('GET /governance/roles returns the role list with grants', async () => {
190
- const r = await call('GET', '/governance/roles');
189
+ test('GET /government/ranks returns the role list with grants', async () => {
190
+ const r = await call('GET', '/government/ranks');
191
191
  assert.equal(r.status, 200);
192
- assert.equal(r.json.roles[0].role_key, 'metic');
192
+ assert.equal(r.json.roles[0].rank_key, 'metic');
193
193
  assert.deepEqual(r.json.roles[0].permissions, ['task.create']);
194
194
  });
195
195
 
196
- test('GET /governance/permissions renders the catalog: grantable flags + principals, no DB call', async () => {
197
- const r = await call('GET', '/governance/permissions');
196
+ test('GET /government/permissions renders the catalog: grantable flags + principals, no DB call', async () => {
197
+ const r = await call('GET', '/government/permissions');
198
198
  assert.equal(r.status, 200);
199
199
  assert.equal(calls.length, 0, 'the permission catalog is code, not a table');
200
200
  const byKey = new Map(r.json.permissions.map((p) => [p.key, p]));
201
201
  assert.equal(byKey.size, catalog.permissionKeys().length, 'every catalog key is listed');
202
- const sys = byKey.get('governance.manage');
202
+ const sys = byKey.get('government.manage');
203
203
  assert.equal(sys.system, true);
204
204
  assert.equal(sys.grantable, false, 'a system permission renders non-grantable');
205
205
  const op = byKey.get('idea.triage');
@@ -209,56 +209,56 @@ test('GET /governance/permissions renders the catalog: grantable flags + princip
209
209
  assert.ok(r.json.principals.every((p) => p.grantable === false));
210
210
  });
211
211
 
212
- test('GET /governance/assignments returns builder↔role rows', async () => {
213
- const r = await call('GET', '/governance/assignments');
212
+ test('GET /government/assignments returns builder↔role rows', async () => {
213
+ const r = await call('GET', '/government/assignments');
214
214
  assert.equal(r.status, 200);
215
215
  assert.equal(r.json.assignments[0].builder_id, 7);
216
- assert.equal(r.json.assignments[0].role_key, 'metic');
216
+ assert.equal(r.json.assignments[0].rank_key, 'metic');
217
217
  });
218
218
 
219
219
  // --- create role -------------------------------------------------------------
220
220
 
221
- test('POST /governance/roles creates a custom role, created_by = the session builder', async () => {
222
- const r = await call('POST', '/governance/roles', { role_key: 'triage-helper', label: 'Triage helper' });
221
+ test('POST /government/ranks creates a custom role, created_by = the session builder', async () => {
222
+ const r = await call('POST', '/government/ranks', { rank_key: 'triage-helper', label: 'Triage helper' });
223
223
  assert.equal(r.status, 201);
224
- assert.equal(r.json.role.role_key, 'triage-helper');
224
+ assert.equal(r.json.role.rank_key, 'triage-helper');
225
225
  assert.equal(r.json.role.is_system, false);
226
226
  const c = calls.find(([k]) => k === 'createRole');
227
227
  assert.equal(c[1].createdBy, 42, 'created_by must be the session builder, never input');
228
228
  });
229
229
 
230
- test('POST /governance/roles validates: label required, role_key must be a slug', async () => {
231
- const missing = await call('POST', '/governance/roles', { role_key: 'x-role' });
230
+ test('POST /government/ranks validates: label required, rank_key must be a slug', async () => {
231
+ const missing = await call('POST', '/government/ranks', { rank_key: 'x-role' });
232
232
  assert.equal(missing.status, 400);
233
233
  assert.equal(missing.json.error.code, 'validation_failed');
234
234
  for (const bad of ['Has Space', 'UPPER', '-lead', 'dots.bad', '']) {
235
- const r = await call('POST', '/governance/roles', { role_key: bad, label: 'L' });
236
- assert.equal(r.status, 400, `role_key "${bad}" must be rejected`);
235
+ const r = await call('POST', '/government/ranks', { rank_key: bad, label: 'L' });
236
+ assert.equal(r.status, 400, `rank_key "${bad}" must be rejected`);
237
237
  }
238
238
  assert.equal(calls.filter(([k]) => k === 'createRole').length, 0, 'nothing reached the db');
239
239
  });
240
240
 
241
- test('POST /governance/roles maps a unique-violation to role_exists 409', async () => {
241
+ test('POST /government/ranks maps a unique-violation to rank_exists 409', async () => {
242
242
  const prev = stub.createRole;
243
243
  stub.createRole = async () => { const e = new Error('dup'); e.code = '23505'; throw e; };
244
- const r = await call('POST', '/governance/roles', { role_key: 'metic-two', label: 'L' });
244
+ const r = await call('POST', '/government/ranks', { rank_key: 'metic-two', label: 'L' });
245
245
  stub.createRole = prev;
246
246
  assert.equal(r.status, 409);
247
- assert.equal(r.json.error.code, 'role_exists');
247
+ assert.equal(r.json.error.code, 'rank_exists');
248
248
  });
249
249
 
250
250
  // --- toggle a grant ----------------------------------------------------------
251
251
 
252
252
  test('PUT grant: an operational permission lands on a custom role', async () => {
253
- const r = await call('PUT', '/governance/roles/triage-helper/permissions/idea.triage');
253
+ const r = await call('PUT', '/government/ranks/triage-helper/permissions/idea.triage');
254
254
  assert.equal(r.status, 200);
255
255
  const c = calls.find(([k]) => k === 'grantRolePermission');
256
- assert.deepEqual(c[1], { roleKey: 'triage-helper', permissionKey: 'idea.triage' });
256
+ assert.deepEqual(c[1], { rankKey: 'triage-helper', permissionKey: 'idea.triage' });
257
257
  });
258
258
 
259
259
  test('PUT grant: a SYSTEM permission is rejected as non-grantable — the R110 wall', async () => {
260
- for (const sysKey of ['governance.manage', 'deploy.push_main', 'builder.rank.set']) {
261
- const r = await call('PUT', `/governance/roles/triage-helper/permissions/${sysKey}`);
260
+ for (const sysKey of ['government.manage', 'deploy.push_main', 'builder.rank.set']) {
261
+ const r = await call('PUT', `/government/ranks/triage-helper/permissions/${sysKey}`);
262
262
  assert.equal(r.status, 400, `${sysKey} must be refused`);
263
263
  assert.equal(r.json.error.code, 'permission_not_grantable');
264
264
  assert.equal(calls.filter(([k]) => k === 'grantRolePermission').length, 0, 'the refusal is BEFORE the db');
@@ -266,29 +266,29 @@ test('PUT grant: a SYSTEM permission is rejected as non-grantable — the R110 w
266
266
  });
267
267
 
268
268
  test('PUT grant: an unknown permission fails closed', async () => {
269
- const r = await call('PUT', '/governance/roles/triage-helper/permissions/no.such.key');
269
+ const r = await call('PUT', '/government/ranks/triage-helper/permissions/no.such.key');
270
270
  assert.equal(r.status, 400);
271
271
  assert.equal(r.json.error.code, 'unknown_permission');
272
272
  });
273
273
 
274
274
  test('PUT grant: a sub-Archon rank-role is EDITABLE (ADR 0172)', async () => {
275
- const r = await call('PUT', '/governance/roles/metic/permissions/idea.triage');
275
+ const r = await call('PUT', '/government/ranks/metic/permissions/idea.triage');
276
276
  assert.equal(r.status, 200);
277
277
  const c = calls.find(([k]) => k === 'grantRolePermission');
278
- assert.deepEqual(c[1], { roleKey: 'metic', permissionKey: 'idea.triage' });
278
+ assert.deepEqual(c[1], { rankKey: 'metic', permissionKey: 'idea.triage' });
279
279
  });
280
280
 
281
281
  test('PUT grant: an in-app owner power CAN go to a rank-role but never to a custom one', async () => {
282
- const rank = await call('PUT', '/governance/roles/metic/permissions/builder.rank.set');
282
+ const rank = await call('PUT', '/government/ranks/metic/permissions/builder.rank.set');
283
283
  assert.equal(rank.status, 200, 'the owner may delegate an in-app Archon power to a rank');
284
- const custom = await call('PUT', '/governance/roles/triage-helper/permissions/builder.rank.set');
284
+ const custom = await call('PUT', '/government/ranks/triage-helper/permissions/builder.rank.set');
285
285
  assert.equal(custom.status, 400, 'a custom role keeps the R110 wall');
286
286
  assert.equal(custom.json.error.code, 'permission_not_grantable');
287
287
  });
288
288
 
289
289
  test('PUT grant: a SUBSTRATE key is refused even on a rank-role (it would confer nothing)', async () => {
290
290
  for (const key of ['deploy.push_main', 'path.protected.modify', 'infra.ssh']) {
291
- const r = await call('PUT', `/governance/roles/metic/permissions/${key}`);
291
+ const r = await call('PUT', `/government/ranks/metic/permissions/${key}`);
292
292
  assert.equal(r.status, 400, `${key} must be refused`);
293
293
  assert.equal(r.json.error.code, 'permission_not_grantable');
294
294
  assert.match(r.json.error.details.reason || '', /substrate/i,
@@ -298,39 +298,39 @@ test('PUT grant: a SUBSTRATE key is refused even on a rank-role (it would confer
298
298
  });
299
299
 
300
300
  test('PUT grant: the ARCHON rank-role stays immutable (the lockout guard)', async () => {
301
- const r = await call('PUT', '/governance/roles/archon/permissions/idea.triage');
301
+ const r = await call('PUT', '/government/ranks/archon/permissions/idea.triage');
302
302
  assert.equal(r.status, 403);
303
- assert.equal(r.json.error.code, 'archon_role_immutable');
303
+ assert.equal(r.json.error.code, 'archon_rank_immutable');
304
304
  assert.equal(calls.filter(([k]) => k === 'grantRolePermission').length, 0);
305
305
  });
306
306
 
307
307
  test('PUT grant: an unknown role 404s', async () => {
308
- const r = await call('PUT', '/governance/roles/missing/permissions/idea.triage');
308
+ const r = await call('PUT', '/government/ranks/missing/permissions/idea.triage');
309
309
  assert.equal(r.status, 404);
310
- assert.equal(r.json.error.code, 'role_not_found');
310
+ assert.equal(r.json.error.code, 'rank_not_found');
311
311
  });
312
312
 
313
313
  test('DELETE grant revokes without consulting the catalog (stale keys stay removable)', async () => {
314
- const r = await call('DELETE', '/governance/roles/triage-helper/permissions/some.retired.key');
314
+ const r = await call('DELETE', '/government/ranks/triage-helper/permissions/some.retired.key');
315
315
  assert.equal(r.status, 200);
316
316
  const c = calls.find(([k]) => k === 'revokeRolePermission');
317
- assert.deepEqual(c[1], { roleKey: 'triage-helper', permissionKey: 'some.retired.key' });
317
+ assert.deepEqual(c[1], { rankKey: 'triage-helper', permissionKey: 'some.retired.key' });
318
318
  });
319
319
 
320
320
  test('DELETE grant: revocable on a sub-Archon rank-role, refused on the Archon one', async () => {
321
- const metic = await call('DELETE', '/governance/roles/metic/permissions/idea.triage');
321
+ const metic = await call('DELETE', '/government/ranks/metic/permissions/idea.triage');
322
322
  assert.equal(metic.status, 200, 'taking a permission away from a rank is the other half of editable');
323
323
 
324
- const archon = await call('DELETE', '/governance/roles/archon/permissions/idea.triage');
324
+ const archon = await call('DELETE', '/government/ranks/archon/permissions/idea.triage');
325
325
  assert.equal(archon.status, 403);
326
- assert.equal(archon.json.error.code, 'archon_role_immutable');
326
+ assert.equal(archon.json.error.code, 'archon_rank_immutable');
327
327
  assert.equal(calls.filter(([k]) => k === 'revokeRolePermission').length, 0);
328
328
  });
329
329
 
330
330
  // --- reset (ADR 0172) --------------------------------------------------------
331
331
 
332
332
  test('POST reset: a rank-role goes back to its floor-derived catalog seed', async () => {
333
- const r = await call('POST', '/governance/roles/thetes/reset');
333
+ const r = await call('POST', '/government/ranks/thetes/reset');
334
334
  assert.equal(r.status, 200);
335
335
  assert.equal(r.json.seeded_rank, 'thetes');
336
336
  const c = calls.find(([k]) => k === 'resetRoleGrants');
@@ -338,57 +338,57 @@ test('POST reset: a rank-role goes back to its floor-derived catalog seed', asyn
338
338
  });
339
339
 
340
340
  test('POST reset: refused on a custom role (no catalog default to return to)', async () => {
341
- const r = await call('POST', '/governance/roles/triage-helper/reset');
341
+ const r = await call('POST', '/government/ranks/triage-helper/reset');
342
342
  assert.equal(r.status, 400);
343
- assert.equal(r.json.error.code, 'role_has_no_default');
343
+ assert.equal(r.json.error.code, 'rank_has_no_default');
344
344
  assert.equal(calls.filter(([k]) => k === 'resetRoleGrants').length, 0);
345
345
  });
346
346
 
347
347
  // --- assignments -------------------------------------------------------------
348
348
 
349
- test('POST /governance/assignments assigns a custom role, assigned_by = the session builder', async () => {
350
- const r = await call('POST', '/governance/assignments', { builder_id: 7, role_key: 'triage-helper' });
349
+ test('POST /government/assignments assigns a custom role, assigned_by = the session builder', async () => {
350
+ const r = await call('POST', '/government/assignments', { builder_id: 7, rank_key: 'triage-helper' });
351
351
  assert.equal(r.status, 201);
352
352
  const c = calls.find(([k]) => k === 'assignRole');
353
- assert.deepEqual(c[1], { builderId: 7, roleKey: 'triage-helper', assignedBy: 42 });
353
+ assert.deepEqual(c[1], { builderId: 7, rankKey: 'triage-helper', assignedBy: 42 });
354
354
  });
355
355
 
356
- test('POST /governance/assignments refuses a seeded rank-role (rank is the writer — R95b)', async () => {
357
- const r = await call('POST', '/governance/assignments', { builder_id: 7, role_key: 'archon' });
356
+ test('POST /government/assignments refuses a seeded rank-role (rank is the writer — R95b)', async () => {
357
+ const r = await call('POST', '/government/assignments', { builder_id: 7, rank_key: 'archon' });
358
358
  assert.equal(r.status, 403);
359
- assert.equal(r.json.error.code, 'seeded_role_assignment_forbidden');
359
+ assert.equal(r.json.error.code, 'seeded_rank_assignment_forbidden');
360
360
  assert.equal(calls.filter(([k]) => k === 'assignRole').length, 0);
361
361
  });
362
362
 
363
- test('POST /governance/assignments maps a missing builder (FK) to 404', async () => {
363
+ test('POST /government/assignments maps a missing builder (FK) to 404', async () => {
364
364
  const prev = stub.assignRole;
365
365
  stub.assignRole = async () => { const e = new Error('fk'); e.code = '23503'; throw e; };
366
- const r = await call('POST', '/governance/assignments', { builder_id: 999, role_key: 'triage-helper' });
366
+ const r = await call('POST', '/government/assignments', { builder_id: 999, rank_key: 'triage-helper' });
367
367
  stub.assignRole = prev;
368
368
  assert.equal(r.status, 404);
369
369
  assert.equal(r.json.error.code, 'builder_not_found');
370
370
  });
371
371
 
372
- test('POST /governance/assignments validates builder_id and role_key', async () => {
373
- const r = await call('POST', '/governance/assignments', { builder_id: 'seven', role_key: 'triage-helper' });
372
+ test('POST /government/assignments validates builder_id and rank_key', async () => {
373
+ const r = await call('POST', '/government/assignments', { builder_id: 'seven', rank_key: 'triage-helper' });
374
374
  assert.equal(r.status, 400);
375
375
  assert.equal(r.json.error.code, 'validation_failed');
376
376
  });
377
377
 
378
- test('DELETE /governance/assignments/:builderId/:roleKey unassigns a custom role', async () => {
379
- const r = await call('DELETE', '/governance/assignments/7/triage-helper');
378
+ test('DELETE /government/assignments/:builderId/:rankKey unassigns a custom role', async () => {
379
+ const r = await call('DELETE', '/government/assignments/7/triage-helper');
380
380
  assert.equal(r.status, 200);
381
381
  const c = calls.find(([k]) => k === 'unassignRole');
382
- assert.deepEqual(c[1], { builderId: 7, roleKey: 'triage-helper' });
382
+ assert.deepEqual(c[1], { builderId: 7, rankKey: 'triage-helper' });
383
383
  });
384
384
 
385
385
  test('DELETE /governance/assignments refuses a seeded rank-role', async () => {
386
- const r = await call('DELETE', '/governance/assignments/7/metic');
386
+ const r = await call('DELETE', '/government/assignments/7/metic');
387
387
  assert.equal(r.status, 403);
388
- assert.equal(r.json.error.code, 'seeded_role_assignment_forbidden');
388
+ assert.equal(r.json.error.code, 'seeded_rank_assignment_forbidden');
389
389
  });
390
390
 
391
391
  test('DELETE /governance/assignments validates the builder id param', async () => {
392
- const r = await call('DELETE', '/governance/assignments/not-a-number/triage-helper');
392
+ const r = await call('DELETE', '/government/assignments/not-a-number/triage-helper');
393
393
  assert.equal(r.status, 400);
394
394
  });
@@ -15,9 +15,9 @@ import { fileURLToPath } from 'node:url';
15
15
 
16
16
  const require = createRequire(import.meta.url);
17
17
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
18
- const { RANK_ROLE_SEED, RANK_ORDER } = require(path.join(ROOT, 'modules', 'governance', 'catalog.js'));
18
+ const { RANK_ROLE_SEED, RANK_ORDER } = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
19
19
 
20
- const MIG_DIR = path.join(ROOT, 'modules', 'governance', 'migrations');
20
+ const MIG_DIR = path.join(ROOT, 'modules', 'government', 'migrations');
21
21
  const seedSql = fs.readFileSync(path.join(MIG_DIR, 'governance_002_seed_and_backfill.sql'), 'utf8');
22
22
  const tablesSql = fs.readFileSync(path.join(MIG_DIR, 'governance_001_tables.sql'), 'utf8');
23
23
 
@@ -28,8 +28,31 @@ const tablesSql = fs.readFileSync(path.join(MIG_DIR, 'governance_001_tables.sql'
28
28
  // ADR 0152), and what must equal RANK_ROLE_SEED is the union of them all, not any one
29
29
  // file. Read in filename order; every file is scanned for `SELECT '<role>', unnest(…)`
30
30
  // blocks, so a new grant migration needs no change here.
31
+ // Migrations that do NOT grant. Excluded by NAME rather than by "does it
32
+ // INSERT?" so that `a grant migration must INSERT something` below stays a real
33
+ // assertion: a file we believe grants but which inserts nothing is a bug worth
34
+ // failing on, and a content-sniffing filter would quietly excuse it.
35
+ const NON_GRANT_MIGRATIONS = new Set([
36
+ 'governance_001_tables.sql', // creates the three tables
37
+ 'government_001_rename_from_governance.sql', // ADR 0174 table rename (task 1003004)
38
+ 'government_002_rename_permission_keys.sql', // ADR 0174 key rename (task 1003006)
39
+ ]);
40
+
41
+ // Permission keys RENAMED after the grant migrations that first granted them.
42
+ // The union model below is append-only — it reads INSERTs — so a rename is
43
+ // invisible to it: the historical migration still grants the old spelling while
44
+ // the catalog now declares the new one. Applying this map to the union is what
45
+ // makes the comparison describe the EFFECTIVE grant set after every migration
46
+ // has run, which is what the seed actually is.
47
+ // Each entry must be performed by a real migration; government_002 does these two.
48
+ const RENAMED_KEYS = new Map([
49
+ ['governance.manage', 'government.manage'],
50
+ ['page.view.governance', 'page.view.government'],
51
+ ]);
52
+ const applyRenames = (keys) => keys.map((k) => RENAMED_KEYS.get(k) || k);
53
+
31
54
  const grantMigrationFiles = fs.readdirSync(MIG_DIR)
32
- .filter((f) => f.endsWith('.sql') && f !== 'governance_001_tables.sql')
55
+ .filter((f) => f.endsWith('.sql') && !NON_GRANT_MIGRATIONS.has(f))
33
56
  .sort();
34
57
  const grantSqlByFile = grantMigrationFiles.map((f) => [f, fs.readFileSync(path.join(MIG_DIR, f), 'utf8')]);
35
58
 
@@ -53,7 +76,7 @@ test('the union of every grant migration matches RANK_ROLE_SEED exactly (no drif
53
76
  for (const b of blocks) {
54
77
  assert.equal(b.keys.length, new Set(b.keys).size, `${role}: duplicate permission in the ${b.file} array`);
55
78
  }
56
- const granted = blocks.flatMap((b) => b.keys);
79
+ const granted = applyRenames(blocks.flatMap((b) => b.keys));
57
80
  // set-equality (order in SQL is illustrative; the grant set is what matters)
58
81
  assert.deepEqual(new Set(granted), new Set(RANK_ROLE_SEED[role]),
59
82
  `${role}: grant migrations drift from catalog RANK_ROLE_SEED`);
@@ -18,7 +18,7 @@
18
18
  // ORACLE DISCIPLINE. A test whose two sides read the same source of truth passes
19
19
  // while proving nothing, so nothing load-bearing here is driven by the registry:
20
20
  // • the Archon-box assertions name Archon powers LITERALLY (builder.rank.set,
21
- // governance.manage, deploy.push_main …) and cross-check them against `floor`
21
+ // government.manage, deploy.push_main …) and cross-check them against `floor`
22
22
  // run through auth.rankMeetsThreshold — the REAL production rank gate, a
23
23
  // pre-R99 oracle;
24
24
  // • the deny-by-default assertions pin a hand-written expected set, so widening
@@ -38,7 +38,7 @@ import { fileURLToPath } from 'node:url';
38
38
 
39
39
  const require = createRequire(import.meta.url);
40
40
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
41
- const GOV = path.join(ROOT, 'modules', 'governance');
41
+ const GOV = path.join(ROOT, 'modules', 'government');
42
42
 
43
43
  // auth.js destructures lookupSession at load, so the stub must be installed before
44
44
  // ANYTHING pulls auth.js in (src/module-api does, via the governance route file).
@@ -66,7 +66,7 @@ const EXPECTED_BOX_SCOPE = ['box.manage.own'];
66
66
  // Archon powers, named literally. If a box session ever resolves one of these the
67
67
  // #919 containment is gone.
68
68
  const ARCHON_POWERS = [
69
- 'builder.rank.set', 'governance.manage', 'deploy.push_main', 'version.close',
69
+ 'builder.rank.set', 'government.manage', 'deploy.push_main', 'version.close',
70
70
  'task.confirm.grade_bypass', 'path.protected.modify', 'infra.ssh',
71
71
  'builder.roster.read', 'claim.release.any', 'memory.read.any', 'audit_log.read',
72
72
  ];
@@ -326,7 +326,7 @@ test('F2: an unscoped session reaches every route, declared or not', () => {
326
326
  assert.equal(auth.sessionScopeAdmits(source, undefined), true);
327
327
  assert.equal(auth.sessionScopeAdmits(source, []), true);
328
328
  assert.equal(auth.sessionScopeAdmits(source, ['box.manage.own']), true);
329
- assert.equal(auth.sessionScopeAdmits(source, ['governance.manage']), true);
329
+ assert.equal(auth.sessionScopeAdmits(source, ['government.manage']), true);
330
330
  }
331
331
  });
332
332
 
@@ -419,7 +419,7 @@ test('H3: the resolve is still per-request and frozen — no memoised session au
419
419
  // ═══ I — the ONE governance port carries the axis ════════════════════════════
420
420
 
421
421
  test('I1: the session-scope axis lands on the single governance port', () => {
422
- const { governancePort } = require(path.join(GOV, 'routes', 'governance.js'));
422
+ const { governancePort } = require(path.join(GOV, 'routes', 'government.js'));
423
423
  for (const name of ['resolveSessionPermissions', 'narrowToSessionScope', 'sessionScopeFor', 'listSessionScopes']) {
424
424
  assert.equal(typeof governancePort[name], 'function', `the port must expose ${name}`);
425
425
  }
@@ -1,7 +1,7 @@
1
1
  // tests/governance_vanilla_boot.mjs — the VANILLA BOOT proof (BV1.R102, ADR 0151).
2
2
  //
3
3
  // The core/host split only holds if a FRESH instance — one that has never been
4
- // branded, with no config/governance.json on disk and no rows in
4
+ // branded, with no config/government.json on disk and no rows in
5
5
  // governance_builder_roles — comes up sane, anonymous, and powerless. This file is
6
6
  // that proof. It is deliberately scoped to what R96's tests/governance_config.mjs
7
7
  // does NOT cover: that file pins the config CONTRACT (precedence, per-field merge,
@@ -37,7 +37,7 @@ import { fileURLToPath } from 'node:url';
37
37
 
38
38
  const require = createRequire(import.meta.url);
39
39
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
40
- const GOV = path.join(ROOT, 'modules', 'governance');
40
+ const GOV = path.join(ROOT, 'modules', 'government');
41
41
  const catalog = require(path.join(GOV, 'catalog.js'));
42
42
  const { loadGovernanceConfig, resolveGovernanceConfig, NEUTRAL_PATH, INSTANCE_PATH } = require(path.join(GOV, 'config.js'));
43
43
  const { effectivePermissions, resolveBuilderPermissions } = require(path.join(GOV, 'resolver.js'));
@@ -127,10 +127,10 @@ test('the identity guard is not a no-op — a planted host brand in the neutral
127
127
  // trip it, in the exact file this task is about.
128
128
  for (const brand of ['Off the Boats', 'amazonprimea.com']) {
129
129
  const poisoned = NEUTRAL_TEXT.replace('"roles"', `"instance": "${brand}",\n "roles"`);
130
- const found = fitness.scanPromptIdentity('config/governance.neutral.json', poisoned, '.json');
130
+ const found = fitness.scanPromptIdentity('config/government.neutral.json', poisoned, '.json');
131
131
  assert.ok(found.length >= 1, `a "${brand}" literal in the neutral pack must be caught`);
132
132
  }
133
- assert.deepEqual(fitness.scanPromptIdentity('config/governance.neutral.json', NEUTRAL_TEXT, '.json'), [], 'the real pack is clean');
133
+ assert.deepEqual(fitness.scanPromptIdentity('config/government.neutral.json', NEUTRAL_TEXT, '.json'), [], 'the real pack is clean');
134
134
  });
135
135
 
136
136
  // ── (c) the trust boundary on a vanilla boot ────────────────────────────────────
@@ -32,12 +32,12 @@ const SRC = fs.readFileSync(path.join(ROOT, 'src/bongos/serve-internal.js'), 'ut
32
32
  // correct for the genuinely public pages and a BUG anywhere else, so every such
33
33
  // row is spelled out rather than left to a default.
34
34
  //
35
- // Audiences (the floors live in modules/governance/catalog.js; the gate
35
+ // Audiences (the floors live in modules/government/catalog.js; the gate
36
36
  // functions in src/bongos/auth.js):
37
37
  // requireBuilderPage page.view.public any signed-in builder (xenos+)
38
- // requireGovernancePage page.view.governance metic+ (ADR 0157)
38
+ // requireGovernancePage page.view.government metic+ (ADR 0157)
39
39
  // requireProjectCuratePage project.curate metic+
40
- // requireGovernanceManagePage governance.manage archon
40
+ // requireGovernanceManagePage government.manage archon
41
41
  const ORACLE = {
42
42
  // Any signed-in builder — no rank floor.
43
43
  '/builders/work': 'requireBuilderPage',
@@ -336,8 +336,8 @@ app.use(attachFail);
336
336
  // currently impersonating, so the cases below exercise the PERMISSION decision rather
337
337
  // than the port-missing fallback.
338
338
  const seams = require('../src/module-seams.js');
339
- const catalog = require('../modules/governance/catalog.js');
340
- seams.registerProvider('governance', {
339
+ const catalog = require('../modules/government/catalog.js');
340
+ seams.registerProvider('government', {
341
341
  resolveBuilderPermissions: async () => catalog.seedForRank(
342
342
  (fixture.session && fixture.session.rank) || 'xenos',
343
343
  ),
@@ -197,7 +197,7 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
197
197
  // That is precisely how the agents regression reached an open PR (task 1002485).
198
198
  loader._reset();
199
199
  const snap = loader.loaderSnapshot();
200
- assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'dev-box', 'discord', 'economy', 'governance', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'platform-identity', 'provisioning', 'security', 'sessions', 'status-ui']);
200
+ assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'dev-box', 'discord', 'economy', 'government', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'platform-identity', 'provisioning', 'security', 'sessions', 'status-ui']);
201
201
  assert.deepEqual(snap.errors, []);
202
202
  assert.deepEqual(snap.skipped, []);
203
203
  loader._reset();
@@ -43,8 +43,8 @@ async function t(name, fn) {
43
43
  // src/bongos/routes, so we patch the resolved module before requiring the
44
44
  // routes.
45
45
  const authPath = require.resolve('../src/bongos/auth.js');
46
- const catalog = require('../modules/governance/catalog.js');
47
- // Mirrors the canonical ladder in modules/governance/catalog.js — omitting a rung
46
+ const catalog = require('../modules/government/catalog.js');
47
+ // Mirrors the canonical ladder in modules/government/catalog.js — omitting a rung
48
48
  // would resolve its floor to Infinity and deny everyone, including archon. Kept at
49
49
  // module scope (BV1.R104) so requireRank and requirePermission below share ONE
50
50
  // ladder; R105's branch still had the stale 3-rung inline copy that dropped thetes.