@bongos/core 1.19.185 → 1.19.187

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 (58) hide show
  1. package/.bongos-core.json +90 -85
  2. package/clients/bongos-client/index.d.ts +2 -2
  3. package/config/government.neutral.json +6 -6
  4. package/docs/adr/0172-editable-rank-roles-substrate-axis.md +1 -1
  5. package/docs/api/openapi.json +12 -12
  6. package/docs/api-reference.md +7 -7
  7. package/modules/builder-settings/wandering-prefs.js +13 -13
  8. package/modules/government/catalog.js +79 -79
  9. package/modules/government/config.js +43 -33
  10. package/modules/government/db.js +58 -58
  11. package/modules/government/protected-surfaces.js +7 -7
  12. package/modules/government/protected-surfaces.json +4 -4
  13. package/modules/government/ranks.js +17 -17
  14. package/modules/government/resolver.js +24 -24
  15. package/modules/government/routes/government.js +88 -88
  16. package/modules/government/routes/rank-sync.js +8 -8
  17. package/modules/government/session-scopes.js +7 -7
  18. package/modules/government/session-scopes.json +5 -5
  19. package/modules/hall-ui/public/gate.js +2 -2
  20. package/modules/hall-ui/public/{governance.css → government.css} +11 -11
  21. package/modules/hall-ui/public/{governance.html → government.html} +14 -14
  22. package/modules/hall-ui/public/{governance.js → government.js} +108 -108
  23. package/modules/hall-ui/public/harbor.js +2 -2
  24. package/modules/hall-ui/public/shell.js +19 -15
  25. package/modules/hall-ui/public/watch.js +1 -1
  26. package/modules/lifecycle/claim-eligibility.js +1 -1
  27. package/modules/lifecycle/db.js +1 -1
  28. package/package-lock.json +2 -2
  29. package/package.json +1 -1
  30. package/scripts/gds/fitness.js +138 -0
  31. package/scripts/hall-preview/server.js +1 -1
  32. package/src/bongos/auth.js +11 -11
  33. package/src/bongos/serve-internal.js +8 -8
  34. package/src/module-api.js +5 -1
  35. package/tests/auth_page_gate.mjs +2 -2
  36. package/tests/auth_resolve_failure.mjs +4 -4
  37. package/tests/{governance_abuse_matrix.mjs → government_abuse_matrix.mjs} +39 -39
  38. package/tests/{governance_catalog.mjs → government_catalog.mjs} +11 -11
  39. package/tests/{governance_claim_eligibility.mjs → government_claim_eligibility.mjs} +2 -2
  40. package/tests/{governance_config.mjs → government_config.mjs} +54 -54
  41. package/tests/{governance_fold.mjs → government_fold.mjs} +6 -6
  42. package/tests/{governance_fold_bench.mjs → government_fold_bench.mjs} +3 -3
  43. package/tests/{governance_live_proof.mjs → government_live_proof.mjs} +5 -5
  44. package/tests/{governance_ownership_scope.mjs → government_ownership_scope.mjs} +16 -16
  45. package/tests/{governance_page_gates.mjs → government_page_gates.mjs} +14 -14
  46. package/tests/{governance_parity.mjs → government_parity.mjs} +3 -3
  47. package/tests/{governance_principals.mjs → government_principals.mjs} +14 -14
  48. package/tests/{governance_protected_surfaces.mjs → government_protected_surfaces.mjs} +1 -1
  49. package/tests/{governance_role_sync.mjs → government_rank_sync.mjs} +2 -2
  50. package/tests/{governance_require_permission.mjs → government_require_permission.mjs} +1 -1
  51. package/tests/{governance_resolver.mjs → government_resolver.mjs} +6 -6
  52. package/tests/{governance_routes.mjs → government_routes.mjs} +36 -36
  53. package/tests/{governance_seed.mjs → government_seed.mjs} +8 -8
  54. package/tests/{governance_session_scope.mjs → government_session_scope.mjs} +10 -10
  55. package/tests/{governance_vanilla_boot.mjs → government_vanilla_boot.mjs} +26 -26
  56. package/tests/government_vocabulary.mjs +94 -0
  57. package/tests/hall_nav.mjs +8 -7
  58. package/tests/hall_page_gate_map.mjs +9 -9
@@ -1,4 +1,4 @@
1
- // tests/governance_resolver.mjs — the permission resolver (BV1.R95, ADR 0151).
1
+ // tests/government_resolver.mjs — the permission resolver (BV1.R95, ADR 0151).
2
2
  //
3
3
  // DB-free: exercises the resolver's PURE core (effectivePermissions) plus the
4
4
  // fail-closed short-circuits and the kernel-port shape. The live DB join is
@@ -45,7 +45,7 @@ test('the result is frozen — a caller cannot mutate a resolved permission set'
45
45
 
46
46
  // R98 narrowed this: the role axis carries every catalog key EXCEPT the machine-
47
47
  // principal ones, which come from an identity and never from a role. The principal
48
- // axis has its own file (tests/governance_principals.mjs).
48
+ // axis has its own file (tests/government_principals.mjs).
49
49
  test('every role-held catalog key survives resolution (the seed can grant anything defined)', () => {
50
50
  const roleHeld = catalog.permissionKeys().filter((k) => !catalog.isPrincipalKey(k));
51
51
  assert.deepEqual(effectivePermissions(catalog.permissionKeys()), [...roleHeld].sort());
@@ -74,10 +74,10 @@ test('the resolver holds no cache — authority is read fresh per request (ADR 0
74
74
  });
75
75
 
76
76
  test('the kernel port exposes the resolver without core importing the module', () => {
77
- const { governancePort } = require(path.join(GOV, 'routes', 'government.js'));
78
- assert.equal(typeof governancePort.resolveBuilderPermissions, 'function');
79
- assert.equal(typeof governancePort.builderHasPermissions, 'function');
80
- assert.equal(typeof governancePort.isSystem, 'function');
77
+ const { governmentPort } = require(path.join(GOV, 'routes', 'government.js'));
78
+ assert.equal(typeof governmentPort.resolveBuilderPermissions, 'function');
79
+ assert.equal(typeof governmentPort.builderHasPermissions, 'function');
80
+ assert.equal(typeof governmentPort.isSystem, 'function');
81
81
  const manifest = JSON.parse(fs.readFileSync(path.join(GOV, 'module.json'), 'utf8'));
82
82
  assert.ok(manifest.provides.includes('government'), 'module.json must declare the government port');
83
83
  assert.ok(manifest.contributes.routes.includes('government'), 'the port registers from the route factory');
@@ -1,4 +1,4 @@
1
- // tests/governance_routes.mjs — the governance CRUD API (BV1.R110, ADR 0151).
1
+ // tests/government_routes.mjs — the governance CRUD API (BV1.R110, ADR 0151).
2
2
  //
3
3
  // Two layers, both DB-free (the memory_isolation.mjs pattern):
4
4
  //
@@ -19,7 +19,7 @@
19
19
  // ASSIGNMENT is still rank-derived (R95b); and every route denies without the
20
20
  // `government.manage` grant.
21
21
  //
22
- // Run: node tests/governance_routes.mjs
22
+ // Run: node tests/government_routes.mjs
23
23
 
24
24
  import { strict as assert } from 'node:assert';
25
25
  import { test, after } from 'node:test';
@@ -99,23 +99,23 @@ 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 () => [{ rank_key: 'metic', label: 'Metic', description: null, is_system: true, seeded_rank: 'metic', created_by: null, created_at: 't', permissions: ['task.create'] }],
102
+ listRanksWithGrants: async () => [{ rank_key: 'metic', label: 'Metic', description: null, is_system: true, seeded_rank: 'metic', created_by: null, created_at: 't', permissions: ['task.create'] }],
103
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
- getRoleByKey: async (k) => (k === 'missing' ? null : {
107
+ getRankByKey: async (k) => (k === 'missing' ? null : {
108
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) => ({ 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
- grantRolePermission: async () => true,
115
- revokeRolePermission: async () => true,
116
- resetRoleGrants: async ({ permissionKeys }) => permissionKeys.length,
117
- assignRole: async () => true,
118
- unassignRole: async () => true,
113
+ createRank: 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
+ grantRankPermission: async () => true,
115
+ revokeRankPermission: async () => true,
116
+ resetRankGrants: async ({ permissionKeys }) => permissionKeys.length,
117
+ assignRank: async () => true,
118
+ unassignRank: async () => true,
119
119
  };
120
120
  const REAL = {};
121
121
  for (const k of Object.keys(stub)) {
@@ -135,11 +135,11 @@ function grant(keys) {
135
135
  }
136
136
  grant(['government.manage']);
137
137
 
138
- const buildGovernanceRouter = require(path.join(ROOT, 'modules', 'government', 'routes', 'government.js'));
138
+ const buildGovernmentRouter = 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);
142
- app.use(buildGovernanceRouter());
142
+ app.use(buildGovernmentRouter());
143
143
  const server = http.createServer(app);
144
144
  await new Promise((r) => server.listen(0, '127.0.0.1', r));
145
145
  const BASE = `http://127.0.0.1:${server.address().port}`;
@@ -189,8 +189,8 @@ test('without government.manage EVERY route 403s (reads included)', async () =>
189
189
  test('GET /government/ranks returns the role list with grants', async () => {
190
190
  const r = await call('GET', '/government/ranks');
191
191
  assert.equal(r.status, 200);
192
- assert.equal(r.json.roles[0].rank_key, 'metic');
193
- assert.deepEqual(r.json.roles[0].permissions, ['task.create']);
192
+ assert.equal(r.json.ranks[0].rank_key, 'metic');
193
+ assert.deepEqual(r.json.ranks[0].permissions, ['task.create']);
194
194
  });
195
195
 
196
196
  test('GET /government/permissions renders the catalog: grantable flags + principals, no DB call', async () => {
@@ -221,9 +221,9 @@ test('GET /government/assignments returns builder↔role rows', async () => {
221
221
  test('POST /government/ranks creates a custom role, created_by = the session builder', async () => {
222
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.rank_key, 'triage-helper');
225
- assert.equal(r.json.role.is_system, false);
226
- const c = calls.find(([k]) => k === 'createRole');
224
+ assert.equal(r.json.rank.rank_key, 'triage-helper');
225
+ assert.equal(r.json.rank.is_system, false);
226
+ const c = calls.find(([k]) => k === 'createRank');
227
227
  assert.equal(c[1].createdBy, 42, 'created_by must be the session builder, never input');
228
228
  });
229
229
 
@@ -235,14 +235,14 @@ test('POST /government/ranks validates: label required, rank_key must be a slug'
235
235
  const r = await call('POST', '/government/ranks', { rank_key: bad, label: 'L' });
236
236
  assert.equal(r.status, 400, `rank_key "${bad}" must be rejected`);
237
237
  }
238
- assert.equal(calls.filter(([k]) => k === 'createRole').length, 0, 'nothing reached the db');
238
+ assert.equal(calls.filter(([k]) => k === 'createRank').length, 0, 'nothing reached the db');
239
239
  });
240
240
 
241
241
  test('POST /government/ranks maps a unique-violation to rank_exists 409', async () => {
242
- const prev = stub.createRole;
243
- stub.createRole = async () => { const e = new Error('dup'); e.code = '23505'; throw e; };
242
+ const prev = stub.createRank;
243
+ stub.createRank = async () => { const e = new Error('dup'); e.code = '23505'; throw e; };
244
244
  const r = await call('POST', '/government/ranks', { rank_key: 'metic-two', label: 'L' });
245
- stub.createRole = prev;
245
+ stub.createRank = prev;
246
246
  assert.equal(r.status, 409);
247
247
  assert.equal(r.json.error.code, 'rank_exists');
248
248
  });
@@ -252,7 +252,7 @@ test('POST /government/ranks maps a unique-violation to rank_exists 409', async
252
252
  test('PUT grant: an operational permission lands on a custom role', async () => {
253
253
  const r = await call('PUT', '/government/ranks/triage-helper/permissions/idea.triage');
254
254
  assert.equal(r.status, 200);
255
- const c = calls.find(([k]) => k === 'grantRolePermission');
255
+ const c = calls.find(([k]) => k === 'grantRankPermission');
256
256
  assert.deepEqual(c[1], { rankKey: 'triage-helper', permissionKey: 'idea.triage' });
257
257
  });
258
258
 
@@ -261,7 +261,7 @@ test('PUT grant: a SYSTEM permission is rejected as non-grantable — the R110 w
261
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
- assert.equal(calls.filter(([k]) => k === 'grantRolePermission').length, 0, 'the refusal is BEFORE the db');
264
+ assert.equal(calls.filter(([k]) => k === 'grantRankPermission').length, 0, 'the refusal is BEFORE the db');
265
265
  }
266
266
  });
267
267
 
@@ -274,7 +274,7 @@ test('PUT grant: an unknown permission fails closed', async () => {
274
274
  test('PUT grant: a sub-Archon rank-role is EDITABLE (ADR 0172)', async () => {
275
275
  const r = await call('PUT', '/government/ranks/metic/permissions/idea.triage');
276
276
  assert.equal(r.status, 200);
277
- const c = calls.find(([k]) => k === 'grantRolePermission');
277
+ const c = calls.find(([k]) => k === 'grantRankPermission');
278
278
  assert.deepEqual(c[1], { rankKey: 'metic', permissionKey: 'idea.triage' });
279
279
  });
280
280
 
@@ -293,7 +293,7 @@ test('PUT grant: a SUBSTRATE key is refused even on a rank-role (it would confer
293
293
  assert.equal(r.json.error.code, 'permission_not_grantable');
294
294
  assert.match(r.json.error.details.reason || '', /substrate/i,
295
295
  'the refusal must name the substrate wall, not the custom-role one — the two are a policy choice apart');
296
- assert.equal(calls.filter(([k]) => k === 'grantRolePermission').length, 0);
296
+ assert.equal(calls.filter(([k]) => k === 'grantRankPermission').length, 0);
297
297
  }
298
298
  });
299
299
 
@@ -301,7 +301,7 @@ test('PUT grant: the ARCHON rank-role stays immutable (the lockout guard)', asyn
301
301
  const r = await call('PUT', '/government/ranks/archon/permissions/idea.triage');
302
302
  assert.equal(r.status, 403);
303
303
  assert.equal(r.json.error.code, 'archon_rank_immutable');
304
- assert.equal(calls.filter(([k]) => k === 'grantRolePermission').length, 0);
304
+ assert.equal(calls.filter(([k]) => k === 'grantRankPermission').length, 0);
305
305
  });
306
306
 
307
307
  test('PUT grant: an unknown role 404s', async () => {
@@ -313,7 +313,7 @@ test('PUT grant: an unknown role 404s', async () => {
313
313
  test('DELETE grant revokes without consulting the catalog (stale keys stay removable)', async () => {
314
314
  const r = await call('DELETE', '/government/ranks/triage-helper/permissions/some.retired.key');
315
315
  assert.equal(r.status, 200);
316
- const c = calls.find(([k]) => k === 'revokeRolePermission');
316
+ const c = calls.find(([k]) => k === 'revokeRankPermission');
317
317
  assert.deepEqual(c[1], { rankKey: 'triage-helper', permissionKey: 'some.retired.key' });
318
318
  });
319
319
 
@@ -324,7 +324,7 @@ test('DELETE grant: revocable on a sub-Archon rank-role, refused on the Archon o
324
324
  const archon = await call('DELETE', '/government/ranks/archon/permissions/idea.triage');
325
325
  assert.equal(archon.status, 403);
326
326
  assert.equal(archon.json.error.code, 'archon_rank_immutable');
327
- assert.equal(calls.filter(([k]) => k === 'revokeRolePermission').length, 0);
327
+ assert.equal(calls.filter(([k]) => k === 'revokeRankPermission').length, 0);
328
328
  });
329
329
 
330
330
  // --- reset (ADR 0172) --------------------------------------------------------
@@ -333,7 +333,7 @@ test('POST reset: a rank-role goes back to its floor-derived catalog seed', asyn
333
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
- const c = calls.find(([k]) => k === 'resetRoleGrants');
336
+ const c = calls.find(([k]) => k === 'resetRankGrants');
337
337
  assert.deepEqual([...c[1].permissionKeys].sort(), [...catalog.seedForRank('thetes')].sort());
338
338
  });
339
339
 
@@ -341,7 +341,7 @@ test('POST reset: refused on a custom role (no catalog default to return to)', a
341
341
  const r = await call('POST', '/government/ranks/triage-helper/reset');
342
342
  assert.equal(r.status, 400);
343
343
  assert.equal(r.json.error.code, 'rank_has_no_default');
344
- assert.equal(calls.filter(([k]) => k === 'resetRoleGrants').length, 0);
344
+ assert.equal(calls.filter(([k]) => k === 'resetRankGrants').length, 0);
345
345
  });
346
346
 
347
347
  // --- assignments -------------------------------------------------------------
@@ -349,7 +349,7 @@ test('POST reset: refused on a custom role (no catalog default to return to)', a
349
349
  test('POST /government/assignments assigns a custom role, assigned_by = the session builder', async () => {
350
350
  const r = await call('POST', '/government/assignments', { builder_id: 7, rank_key: 'triage-helper' });
351
351
  assert.equal(r.status, 201);
352
- const c = calls.find(([k]) => k === 'assignRole');
352
+ const c = calls.find(([k]) => k === 'assignRank');
353
353
  assert.deepEqual(c[1], { builderId: 7, rankKey: 'triage-helper', assignedBy: 42 });
354
354
  });
355
355
 
@@ -357,14 +357,14 @@ test('POST /government/assignments refuses a seeded rank-role (rank is the write
357
357
  const r = await call('POST', '/government/assignments', { builder_id: 7, rank_key: 'archon' });
358
358
  assert.equal(r.status, 403);
359
359
  assert.equal(r.json.error.code, 'seeded_rank_assignment_forbidden');
360
- assert.equal(calls.filter(([k]) => k === 'assignRole').length, 0);
360
+ assert.equal(calls.filter(([k]) => k === 'assignRank').length, 0);
361
361
  });
362
362
 
363
363
  test('POST /government/assignments maps a missing builder (FK) to 404', async () => {
364
- const prev = stub.assignRole;
365
- stub.assignRole = async () => { const e = new Error('fk'); e.code = '23503'; throw e; };
364
+ const prev = stub.assignRank;
365
+ stub.assignRank = async () => { const e = new Error('fk'); e.code = '23503'; throw e; };
366
366
  const r = await call('POST', '/government/assignments', { builder_id: 999, rank_key: 'triage-helper' });
367
- stub.assignRole = prev;
367
+ stub.assignRank = prev;
368
368
  assert.equal(r.status, 404);
369
369
  assert.equal(r.json.error.code, 'builder_not_found');
370
370
  });
@@ -378,7 +378,7 @@ test('POST /government/assignments validates builder_id and rank_key', async ()
378
378
  test('DELETE /government/assignments/:builderId/:rankKey unassigns a custom role', async () => {
379
379
  const r = await call('DELETE', '/government/assignments/7/triage-helper');
380
380
  assert.equal(r.status, 200);
381
- const c = calls.find(([k]) => k === 'unassignRole');
381
+ const c = calls.find(([k]) => k === 'unassignRank');
382
382
  assert.deepEqual(c[1], { builderId: 7, rankKey: 'triage-helper' });
383
383
  });
384
384
 
@@ -1,8 +1,8 @@
1
- // tests/governance_seed.mjs — the compat-seed migration drift guard (BV1.R93, ADR 0151).
1
+ // tests/government_seed.mjs — the compat-seed migration drift guard (BV1.R93, ADR 0151).
2
2
  //
3
3
  // The seed migration (governance_002_seed_and_backfill.sql) MATERIALISES the role→
4
4
  // permission grants as SQL. This parses that file and asserts the four role arrays
5
- // equal the code catalog's RANK_ROLE_SEED exactly — so the SQL snapshot can never
5
+ // equal the code catalog's RANK_SEED exactly — so the SQL snapshot can never
6
6
  // silently drift from the catalog (the "no second list that drifts" discipline). If
7
7
  // you change catalog.js, this goes red until the migration is regenerated (a NEW
8
8
  // migration for a NEW grant — migrations are run-once).
@@ -15,7 +15,7 @@ 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', 'government', 'catalog.js'));
18
+ const { RANK_SEED, RANK_ORDER } = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
19
19
 
20
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');
@@ -25,7 +25,7 @@ const tablesSql = fs.readFileSync(path.join(MIG_DIR, 'governance_001_tables.sql'
25
25
  // grant migration. Migrations are run-once, so a floor change in catalog.js can never
26
26
  // be made by editing 002 — an already-migrated database would keep the old grant. It
27
27
  // arrives as a follow-on migration instead (governance_003 = task.abandon → metic,
28
- // ADR 0152), and what must equal RANK_ROLE_SEED is the union of them all, not any one
28
+ // ADR 0152), and what must equal RANK_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
31
  // Migrations that do NOT grant. Excluded by NAME rather than by "does it
@@ -69,7 +69,7 @@ function grantBlocksForRole(role) {
69
69
  return blocks;
70
70
  }
71
71
 
72
- test('the union of every grant migration matches RANK_ROLE_SEED exactly (no drift)', () => {
72
+ test('the union of every grant migration matches RANK_SEED exactly (no drift)', () => {
73
73
  for (const role of RANK_ORDER) {
74
74
  const blocks = grantBlocksForRole(role);
75
75
  assert.ok(blocks.length > 0, `no grant block for ${role} in any governance migration`);
@@ -78,11 +78,11 @@ test('the union of every grant migration matches RANK_ROLE_SEED exactly (no drif
78
78
  }
79
79
  const granted = applyRenames(blocks.flatMap((b) => b.keys));
80
80
  // set-equality (order in SQL is illustrative; the grant set is what matters)
81
- assert.deepEqual(new Set(granted), new Set(RANK_ROLE_SEED[role]),
82
- `${role}: grant migrations drift from catalog RANK_ROLE_SEED`);
81
+ assert.deepEqual(new Set(granted), new Set(RANK_SEED[role]),
82
+ `${role}: grant migrations drift from catalog RANK_SEED`);
83
83
  // no key re-granted in a LATER migration (a no-op INSERT that muddies the ledger)
84
84
  assert.equal(granted.length, new Set(granted).size, `${role}: the same permission is granted twice across migrations`);
85
- assert.equal(new Set(granted).size, RANK_ROLE_SEED[role].length, `${role}: grant count mismatch`);
85
+ assert.equal(new Set(granted).size, RANK_SEED[role].length, `${role}: grant count mismatch`);
86
86
  }
87
87
  });
88
88
 
@@ -1,4 +1,4 @@
1
- // tests/governance_session_scope.mjs — session-scopes as a first-class axis
1
+ // tests/government_session_scope.mjs — session-scopes as a first-class axis
2
2
  // (BV1.R99, ADR 0151 §3; the model behind ADR 0053). DB-free.
3
3
  //
4
4
  // A session-scope is capability attached to the TOKEN, not to the builder: a
@@ -75,7 +75,7 @@ const ARCHON_POWERS = [
75
75
  const METIC_POWERS = ['idea.triage', 'task.create', 'task.edit', 'goal.create', 'box.source.full'];
76
76
 
77
77
  // Stub the ONE DB read the resolver makes, so the composition is exercised
78
- // end-to-end without Postgres (the tests/governance_ownership_scope.mjs pattern).
78
+ // end-to-end without Postgres (the tests/government_ownership_scope.mjs pattern).
79
79
  const realGetPermissionKeys = db.getPermissionKeysForBuilder;
80
80
  async function holding(keys, fn) {
81
81
  db.getPermissionKeysForBuilder = async () => keys;
@@ -370,7 +370,7 @@ test('G2: requireBuilder DENIES a session whose source the registry does not dec
370
370
 
371
371
  test('H1: R95/R98/R100 invariants still hold — the role answer is untouched', async () => {
372
372
  // R99 composes ON TOP of effectivePermissions; widening INSIDE it would red
373
- // tests/governance_parity.mjs Parts A and C.
373
+ // tests/government_parity.mjs Parts A and C.
374
374
  assert.deepEqual(effectivePermissions(['principal.bfg', 'box.manage.own']), ['box.manage.own']);
375
375
  assert.deepEqual(effectivePermissions(['idea.triage', 'not.a.real.permission']), ['idea.triage']);
376
376
  for (const rank of RANK_ORDER) {
@@ -419,16 +419,16 @@ 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', 'government.js'));
422
+ const { governmentPort } = require(path.join(GOV, 'routes', 'government.js'));
423
423
  for (const name of ['resolveSessionPermissions', 'narrowToSessionScope', 'sessionScopeFor', 'listSessionScopes']) {
424
- assert.equal(typeof governancePort[name], 'function', `the port must expose ${name}`);
424
+ assert.equal(typeof governmentPort[name], 'function', `the port must expose ${name}`);
425
425
  }
426
426
  // still the same one object port the earlier slices registered
427
427
  for (const name of ['resolveBuilderPermissions', 'resolvePrincipalPermissions', 'authorizeOwned']) {
428
- assert.equal(typeof governancePort[name], 'function');
428
+ assert.equal(typeof governmentPort[name], 'function');
429
429
  }
430
- assert.deepEqual([...governancePort.sessionScopeFor('box').permissions], EXPECTED_BOX_SCOPE);
431
- assert.equal(governancePort.sessionScopeFor('ghost'), null);
432
- assert.deepEqual([...governancePort.narrowToSessionScope(seedForRank('archon'), 'box')], EXPECTED_BOX_SCOPE);
433
- assert.ok(governancePort.listSessionScopes().length >= 2);
430
+ assert.deepEqual([...governmentPort.sessionScopeFor('box').permissions], EXPECTED_BOX_SCOPE);
431
+ assert.equal(governmentPort.sessionScopeFor('ghost'), null);
432
+ assert.deepEqual([...governmentPort.narrowToSessionScope(seedForRank('archon'), 'box')], EXPECTED_BOX_SCOPE);
433
+ assert.ok(governmentPort.listSessionScopes().length >= 2);
434
434
  });
@@ -1,9 +1,9 @@
1
- // tests/governance_vanilla_boot.mjs — the VANILLA BOOT proof (BV1.R102, ADR 0151).
1
+ // tests/government_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
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
- // that proof. It is deliberately scoped to what R96's tests/governance_config.mjs
6
+ // that proof. It is deliberately scoped to what R96's tests/government_config.mjs
7
7
  // does NOT cover: that file pins the config CONTRACT (precedence, per-field merge,
8
8
  // the three fail-closed strips) driven by INJECTED fixtures; this one boots the real
9
9
  // thing off disk and asserts the three vanilla properties:
@@ -23,7 +23,7 @@
23
23
  // outcome ADR 0016 exists to rule out, and this file is what would go red.
24
24
  //
25
25
  // DB-free throughout: the config layer is pure + disk, and the grant side is asserted
26
- // against catalog.RANK_ROLE_SEED (code) rather than a live join. Crucially the two
26
+ // against catalog.RANK_SEED (code) rather than a live join. Crucially the two
27
27
  // sides read DIFFERENT sources — the seed side reads catalog.js, the config side
28
28
  // reads the neutral pack's raw bytes — so a change to either one moves only one side
29
29
  // and the assertion goes red.
@@ -39,7 +39,7 @@ const require = createRequire(import.meta.url);
39
39
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
40
40
  const GOV = path.join(ROOT, 'modules', 'government');
41
41
  const catalog = require(path.join(GOV, 'catalog.js'));
42
- const { loadGovernanceConfig, resolveGovernanceConfig, NEUTRAL_PATH, INSTANCE_PATH } = require(path.join(GOV, 'config.js'));
42
+ const { loadGovernmentConfig, resolveGovernmentConfig, NEUTRAL_PATH, INSTANCE_PATH } = require(path.join(GOV, 'config.js'));
43
43
  const { effectivePermissions, resolveBuilderPermissions } = require(path.join(GOV, 'resolver.js'));
44
44
  const fitness = require(path.join(ROOT, 'scripts', 'gds', 'fitness.js'));
45
45
  const { envPrefix } = require(path.join(ROOT, 'src', 'instance-config'));
@@ -51,10 +51,10 @@ const NEUTRAL_TEXT = fs.readFileSync(NEUTRAL_PATH, 'utf8');
51
51
 
52
52
  // The vanilla boot, exactly as a fresh instance performs it: real neutral pack on
53
53
  // disk, no instance pack, no env.
54
- const vanilla = () => loadGovernanceConfig({ env: NO_ENV });
54
+ const vanilla = () => loadGovernmentConfig({ env: NO_ENV });
55
55
  // Everything config hands the boot, flattened — the ONLY authority a file could
56
56
  // possibly contribute.
57
- const configUnion = (cfg) => [...new Set(Object.values(cfg.roles).flatMap((r) => r.permissions))].sort();
57
+ const configUnion = (cfg) => [...new Set(Object.values(cfg.ranks).flatMap((r) => r.permissions))].sort();
58
58
 
59
59
  // Runs `fn(dir)` against a scratch dir, always cleaned up. Nothing under it exists
60
60
  // until the test writes it, so `path.join(dir, 'absent.json')` is a genuine ENOENT.
@@ -68,9 +68,9 @@ const withScratchDir = (fn) => {
68
68
  test('a fresh instance boots the four rank-roles with usable labels and descriptions', () => {
69
69
  assert.equal(fs.existsSync(INSTANCE_PATH), false, 'fixture guard: this checkout IS the vanilla case (no instance pack)');
70
70
  const out = vanilla();
71
- assert.deepEqual(Object.keys(out.roles).sort(), [...catalog.RANK_ORDER].sort());
71
+ assert.deepEqual(Object.keys(out.ranks).sort(), [...catalog.RANK_ORDER].sort());
72
72
  for (const rank of catalog.RANK_ORDER) {
73
- const role = out.roles[rank];
73
+ const role = out.ranks[rank];
74
74
  assert.equal(role.seeded, true, `${rank} must boot as a SEEDED rank-role, not a custom one`);
75
75
  assert.ok(role.label && role.label !== rank, `${rank} must carry a human label from the neutral pack, got "${role.label}"`);
76
76
  assert.ok(role.description.length > 10, `${rank} must carry a usable description, got "${role.description}"`);
@@ -83,13 +83,13 @@ test('a core with NO config packs at all still boots four roles rather than none
83
83
  // set must never shrink below them — a boot that dropped `archon` would leave the
84
84
  // instance with grants no template describes.
85
85
  withScratchDir((dir) => {
86
- const out = loadGovernanceConfig({
86
+ const out = loadGovernmentConfig({
87
87
  neutralPath: path.join(dir, 'absent-neutral.json'),
88
88
  instancePath: path.join(dir, 'absent-instance.json'),
89
89
  env: NO_ENV,
90
90
  });
91
- assert.deepEqual(Object.keys(out.roles).sort(), [...catalog.RANK_ORDER].sort());
92
- for (const rank of catalog.RANK_ORDER) assert.equal(out.roles[rank].label, rank, 'a role with no template falls back to its canonical rank name');
91
+ assert.deepEqual(Object.keys(out.ranks).sort(), [...catalog.RANK_ORDER].sort());
92
+ for (const rank of catalog.RANK_ORDER) assert.equal(out.ranks[rank].label, rank, 'a role with no template falls back to its canonical rank name');
93
93
  });
94
94
  });
95
95
 
@@ -104,8 +104,8 @@ test('a malformed config pack fails LOUD — it never degrades into a silent gra
104
104
  withScratchDir((dir) => {
105
105
  const broken = path.join(dir, 'broken.json');
106
106
  fs.writeFileSync(broken, '{ not json');
107
- assert.throws(() => loadGovernanceConfig({ instancePath: broken, env: NO_ENV }), SyntaxError);
108
- assert.throws(() => loadGovernanceConfig({ neutralPath: broken, env: NO_ENV }), SyntaxError);
107
+ assert.throws(() => loadGovernmentConfig({ instancePath: broken, env: NO_ENV }), SyntaxError);
108
+ assert.throws(() => loadGovernmentConfig({ neutralPath: broken, env: NO_ENV }), SyntaxError);
109
109
  });
110
110
  });
111
111
 
@@ -126,7 +126,7 @@ test('the identity guard is not a no-op — a planted host brand in the neutral
126
126
  // The mutation test, kept permanent. Both host literals fitness knows about must
127
127
  // trip it, in the exact file this task is about.
128
128
  for (const brand of ['Off the Boats', 'amazonprimea.com']) {
129
- const poisoned = NEUTRAL_TEXT.replace('"roles"', `"instance": "${brand}",\n "roles"`);
129
+ const poisoned = NEUTRAL_TEXT.replace('"ranks"', `"instance": "${brand}",\n "ranks"`);
130
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
  }
@@ -141,7 +141,7 @@ test('a vanilla boot grants ZERO permissions through config', () => {
141
141
  const out = vanilla();
142
142
  assert.deepEqual(configUnion(out), [], 'config contributed authority to a vanilla boot');
143
143
  for (const key of catalog.systemKeys()) {
144
- for (const role of Object.values(out.roles)) {
144
+ for (const role of Object.values(out.ranks)) {
145
145
  assert.ok(!role.permissions.includes(key), `vanilla boot handed role "${role.key}" the system permission "${key}"`);
146
146
  }
147
147
  }
@@ -158,10 +158,10 @@ test('the rank-roles’ grants derive from catalog floors, never from the neutra
158
158
  // (code), the file side is the pack's raw bytes. Moving either one moves one side
159
159
  // only, so this cannot pass vacuously.
160
160
  for (const rank of catalog.RANK_ORDER) {
161
- assert.ok(catalog.RANK_ROLE_SEED[rank].length > 0, `${rank} must hold a floor-derived grant set`);
161
+ assert.ok(catalog.RANK_SEED[rank].length > 0, `${rank} must hold a floor-derived grant set`);
162
162
  }
163
- assert.ok(catalog.RANK_ROLE_SEED.archon.includes('deploy.push_main'), 'fixture guard: archon holds the seed’s system power');
164
- assert.ok(!catalog.RANK_ROLE_SEED.xenos.includes('deploy.push_main'), 'fixture guard: the floors are a real ladder');
163
+ assert.ok(catalog.RANK_SEED.archon.includes('deploy.push_main'), 'fixture guard: archon holds the seed’s system power');
164
+ assert.ok(!catalog.RANK_SEED.xenos.includes('deploy.push_main'), 'fixture guard: the floors are a real ladder');
165
165
  for (const key of catalog.permissionKeys()) {
166
166
  assert.ok(!NEUTRAL_TEXT.includes(key), `the neutral pack names the permission "${key}" — grants belong to governance_002's floors, never to a file`);
167
167
  }
@@ -173,22 +173,22 @@ test('a POISONED neutral pack still cannot grant on a vanilla boot', () => {
173
173
  // edit to it is the vanilla-boot threat. A seeded role may be relabelled and never
174
174
  // re-granted; a custom template may hold operational keys and never a system one.
175
175
  const poisoned = JSON.parse(NEUTRAL_TEXT);
176
- for (const rank of catalog.RANK_ORDER) poisoned.roles[rank].permissions = [...catalog.permissionKeys()];
177
- poisoned.roles.superuser = { label: 'Superuser', permissions: [...catalog.permissionKeys()] };
178
- const out = resolveGovernanceConfig({ neutral: poisoned, env: NO_ENV });
176
+ for (const rank of catalog.RANK_ORDER) poisoned.ranks[rank].permissions = [...catalog.permissionKeys()];
177
+ poisoned.ranks.superuser = { label: 'Superuser', permissions: [...catalog.permissionKeys()] };
178
+ const out = resolveGovernmentConfig({ neutral: poisoned, env: NO_ENV });
179
179
  for (const rank of catalog.RANK_ORDER) {
180
- assert.deepEqual(out.roles[rank].permissions, [], `a poisoned neutral pack re-granted the seeded role "${rank}"`);
180
+ assert.deepEqual(out.ranks[rank].permissions, [], `a poisoned neutral pack re-granted the seeded role "${rank}"`);
181
181
  }
182
182
  for (const key of catalog.systemKeys()) {
183
- assert.ok(!out.roles.superuser.permissions.includes(key), `a neutral-pack template smuggled the system permission "${key}"`);
183
+ assert.ok(!out.ranks.superuser.permissions.includes(key), `a neutral-pack template smuggled the system permission "${key}"`);
184
184
  }
185
- assert.deepEqual(out.roles.superuser.permissions, [...catalog.grantableKeys()].sort(), 'operational keys are all a template may ever propose');
185
+ assert.deepEqual(out.ranks.superuser.permissions, [...catalog.grantableKeys()].sort(), 'operational keys are all a template may ever propose');
186
186
  });
187
187
 
188
188
  test('the env layer cannot widen authority on a vanilla boot', () => {
189
189
  const env = { [`${envPrefix()}_GOVERNANCE_ROLE_LABELS`]: 'archon=Owner' };
190
- const out = loadGovernanceConfig({ env });
191
- assert.equal(out.roles.archon.label, 'Owner', 'env may relabel');
190
+ const out = loadGovernmentConfig({ env });
191
+ assert.equal(out.ranks.archon.label, 'Owner', 'env may relabel');
192
192
  assert.deepEqual(configUnion(out), [], 'env must never add a permission');
193
193
  });
194
194
 
@@ -0,0 +1,94 @@
1
+ // tests/government_vocabulary.mjs — the ADR 0174 vocabulary wall
2
+ // (goal 1000068 R07, task 1003008).
3
+ //
4
+ // The rule under test says: "role" means a CRAFT — Artist, Ideator, or Builder —
5
+ // and never a rank. `scripts/gds/fitness.js` checkVocabularyWall enforces it on
6
+ // the government surface, in the CI `unit` check.
7
+ //
8
+ // WHY THIS FILE EXISTS AT ALL. A naming rule is the easiest kind of check to
9
+ // ship broken: allowlist enough and it passes forever while proving nothing, and
10
+ // nobody notices because green is green. So the load-bearing tests here are the
11
+ // MUTATION tests — plant a violation, assert the build reds. A rule that cannot
12
+ // be made to fail is not a rule.
13
+ import assert from 'node:assert/strict';
14
+ import { test } from 'node:test';
15
+ import { createRequire } from 'node:module';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const require = createRequire(import.meta.url);
20
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
21
+ const fitness = require(path.join(ROOT, 'scripts', 'gds', 'fitness.js'));
22
+
23
+ // ---------- the wall holds on the real tree ----------
24
+
25
+ test('the government surface is clean today', () => {
26
+ const r = fitness.checkVocabularyWall();
27
+ assert.equal(r.ok, true,
28
+ `the vocabulary wall is failing on the real tree:\n${r.violations.join('\n')}`);
29
+ assert.ok(r.note.includes('file(s) scanned'), 'the check must report what it scanned');
30
+ });
31
+
32
+ // ---------- the mutation tests: the rule can actually fail ----------
33
+
34
+ test('a planted "role" on the government surface REDS the build', () => {
35
+ const planted = withPlantedLine('// a custom role may hold this');
36
+ assert.ok(planted.some((v) => /"role"/.test(v)),
37
+ 'a "role" on the government surface must be caught — otherwise this rule is decoration');
38
+ });
39
+
40
+ test('a planted "governance" on the government surface REDS the build', () => {
41
+ const planted = withPlantedLine('// see the governance tab');
42
+ assert.ok(planted.some((v) => /"governance"/.test(v)),
43
+ 'the pre-rename name must be caught — it is how the old vocabulary creeps back');
44
+ });
45
+
46
+ // Plant a line into a real government-surface file, run the check, restore.
47
+ // Uses the real file rather than a fixture so the test exercises the same path
48
+ // resolution the CI gate does.
49
+ function withPlantedLine(line) {
50
+ const fs = require('node:fs');
51
+ const target = path.join(ROOT, 'modules', 'government', 'resolver.js');
52
+ const original = fs.readFileSync(target, 'utf8');
53
+ try {
54
+ fs.writeFileSync(target, `${original}\n${line}\n`);
55
+ return fitness.checkVocabularyWall().violations;
56
+ } finally {
57
+ fs.writeFileSync(target, original);
58
+ }
59
+ }
60
+
61
+ // ---------- the allowlist is small, reasoned, and real ----------
62
+
63
+ test('every allowlist entry carries a written reason', () => {
64
+ for (const a of fitness.VOCAB_ALLOW) {
65
+ assert.ok(a.file && a.match, 'an allowlist entry needs a file and a match');
66
+ assert.ok(a.why && a.why.length > 60,
67
+ `${a.file} (${a.match}) must record WHY it is excused — an undocumented `
68
+ + 'exemption is indistinguishable from giving up.');
69
+ }
70
+ });
71
+
72
+ test('the allowlist stays small — growth means finish the rename, not add a line', () => {
73
+ // Every current entry is a COMPATIBILITY surface that must name the old thing:
74
+ // the legacy instance-config key, the legacy env var, the 308 route alias, and
75
+ // the manifest sentence that states the ban itself. If this cap is in the way,
76
+ // the honest move is almost always to delete a compatibility shim whose window
77
+ // has closed — not to raise the number.
78
+ assert.ok(fitness.VOCAB_ALLOW.length <= 5,
79
+ `the allowlist has grown to ${fitness.VOCAB_ALLOW.length}. A naming rule that `
80
+ + 'needs a long allowlist has stopped being evidence. Finish the rename or '
81
+ + 'retire a shim instead of raising this cap.');
82
+ });
83
+
84
+ test('every allowlist entry still matches something — no stale exemptions', () => {
85
+ const fs = require('node:fs');
86
+ for (const a of fitness.VOCAB_ALLOW) {
87
+ const abs = path.join(ROOT, a.file);
88
+ assert.ok(fs.existsSync(abs), `${a.file} is allowlisted but does not exist`);
89
+ const body = fs.readFileSync(abs, 'utf8').toLowerCase();
90
+ assert.ok(body.includes(a.match.toLowerCase()),
91
+ `${a.file} no longer contains "${a.match}" — the exemption outlived its cause `
92
+ + 'and should be deleted, not carried.');
93
+ }
94
+ });
@@ -127,21 +127,22 @@ function hrefResolves(item) {
127
127
 
128
128
  test('sidebar groups read by audience, in order', () => {
129
129
  const nav = parseNav(boot().sidebarHtml);
130
- // 'Archon' → 'Governance' in ADR 0157: the group's pages opened to Metic, so a header
130
+ // 'Archon' → 'Governance' in ADR 0157 (the group's pages opened to Metic, so a header
131
+ // naming the top rank was wrong), then → 'Government' by ADR 0174: a header
131
132
  // reading "Archon" over items a Metic can open would misdescribe the ladder.
132
- assert.deepEqual(nav.map((g) => g.label), ['', 'You', 'Community', 'Docs', 'Governance']);
133
+ assert.deepEqual(nav.map((g) => g.label), ['', 'You', 'Community', 'Docs', 'Government']);
133
134
  assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'work'],
134
135
  'the unlabeled group is the project itself (task 1002425 order)');
135
136
  assert.deepEqual(nav[1].items.map((i) => i.id), ['standing', 'profile'], 'You = your own numbers');
136
137
  // People (task 1002577): the /scouting directory graduated into the hall.
137
138
  // Gated cosmetically to match its data endpoint — `project.curate`, floor METIC,
138
- // so the nav entry reads 'governance' since task 1002710 (it said 'archon', which
139
+ // so the nav entry reads 'government' since ADR 0174 (it said 'archon', then
139
140
  // was stricter than the wall). requireProjectCuratePage is the wall server-side.
140
141
  assert.deepEqual(nav[2].items.map((i) => i.id), ['leaderboard', 'people'], 'Community = everyone else');
141
142
  assert.deepEqual(nav[3].items.map((i) => i.id),
142
143
  ['primer', 'ranks', 'economy', 'diagrams', 'atlas', 'modules', 'apidocs'], 'Docs = reference material');
143
- // 'governance' (the Roles tab, BV1.R113) is the group's one archon-gated item.
144
- assert.deepEqual(nav[4].items.map((i) => i.id), ['watch', 'harbor', 'gate', 'sessions', 'governance']);
144
+ // 'government' (the Permissions tab, BV1.R113) is the group's one archon-gated item.
145
+ assert.deepEqual(nav[4].items.map((i) => i.id), ['watch', 'harbor', 'gate', 'sessions', 'government']);
145
146
  });
146
147
 
147
148
  test('Economy lives in Docs and points at the reward-schedule page', () => {
@@ -190,11 +191,11 @@ test('Achievements has no nav entry, but #achievements still routes to Standing'
190
191
 
191
192
  test('collapsible groups carry a stable storage key (collapse persistence)', () => {
192
193
  const html = boot().sidebarHtml;
193
- // 'archon' → 'governance' follows the ADR 0157 relabel. The slug is the
194
+ // 'archon' → 'governance' → 'government' follows ADR 0157 then ADR 0174. The slug is the
194
195
  // hall-nav-collapsed storage key, so any builder who had collapsed the old "archon"
195
196
  // group gets the new one expanded once — cosmetic, and preferable to keeping a stale
196
197
  // slug that no longer matches the visible label.
197
- for (const g of ['you', 'community', 'docs', 'governance']) {
198
+ for (const g of ['you', 'community', 'docs', 'government']) {
198
199
  assert.match(html, new RegExp(`data-group="${g}"`), `${g} is collapsible (hall-nav-collapsed key)`);
199
200
  }
200
201
  assert.ok(!/nav-group--collapsible[^>]*>(?!<button)/.test(html.split('<div class="nav-group')[1] || ''),