@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
@@ -142,7 +142,7 @@ try {
142
142
  'BEFORE: the thetes builder cannot triage (403 permission_forbidden)');
143
143
 
144
144
  // Act 2 — an Archon creates a custom role and grants the operational permission.
145
- r = await call(ARCHON_TOKEN, 'POST', '/governance/roles',
145
+ r = await call(ARCHON_TOKEN, 'POST', '/government/ranks',
146
146
  { role_key: ROLE_KEY, label: 'GLP demo triage role' });
147
147
  ok(r.status === 201, 'the Archon creates a custom role (201)');
148
148
  r = await call(ARCHON_TOKEN, 'PUT', `/governance/roles/${ROLE_KEY}/permissions/${OPERATIONAL_KEY}`);
@@ -153,13 +153,13 @@ try {
153
153
  r = await call(ARCHON_TOKEN, 'PUT', `/governance/roles/${ROLE_KEY}/permissions/${SYSTEM_KEY}`);
154
154
  ok(r.status === 400 && r.body.error.code === 'permission_not_grantable',
155
155
  `system '${SYSTEM_KEY}' is NOT grantable, even by the Archon (400 permission_not_grantable)`);
156
- r = await call(ARCHON_TOKEN, 'PUT', `/governance/roles/${ROLE_KEY}/permissions/governance.manage`);
156
+ r = await call(ARCHON_TOKEN, 'PUT', `/governance/roles/${ROLE_KEY}/permissions/government.manage`);
157
157
  ok(r.status === 400 && r.body.error.code === 'permission_not_grantable',
158
- 'the meta-escalation (governance.manage) is refused the same way');
158
+ 'the meta-escalation (government.manage) is refused the same way');
159
159
 
160
160
  // Act 4 — assign, then the same builder performs the action: no re-login, no
161
161
  // restart — the per-request, uncached resolve (ADR 0016) picks the role up.
162
- r = await call(ARCHON_TOKEN, 'POST', '/governance/assignments',
162
+ r = await call(ARCHON_TOKEN, 'POST', '/government/assignments',
163
163
  { builder_id: THETES, role_key: ROLE_KEY });
164
164
  ok(r.status === 201, 'the Archon assigns the custom role to the thetes builder (201)');
165
165
  r = await call(THETES_TOKEN, 'PATCH', `/inbox/${ideaId}`, { status: 'discarded' });
@@ -176,7 +176,7 @@ try {
176
176
  // …the resolver reports it honestly (it is a defined catalog key; matrix V10
177
177
  // proves no route consumes it via requirePermission, so it admits nothing
178
178
  // in-app)…
179
- const resolver = require('../modules/governance/resolver.js');
179
+ const resolver = require('../modules/government/resolver.js');
180
180
  const perms = await resolver.resolveBuilderPermissions(THETES);
181
181
  ok(perms.includes(OPERATIONAL_KEY), 'the resolved set includes the operational grant');
182
182
  console.log(` (resolved set ${perms.includes(SYSTEM_KEY) ? 'includes' : 'omits'} the forced '${SYSTEM_KEY}' — in-app it guards no route either way)`);
@@ -187,7 +187,7 @@ try {
187
187
  // protected-path diff while the forced grant row sits in the database.
188
188
  const { checkPermissionPaths } = require('../src/bongos/permission-path-check.js');
189
189
  const sub = checkPermissionPaths({
190
- changedFiles: ['migrations/999_glp_demo_evil.sql', 'modules/governance/catalog.js'],
190
+ changedFiles: ['migrations/999_glp_demo_evil.sql', 'modules/government/catalog.js'],
191
191
  builderRank: 'thetes',
192
192
  });
193
193
  ok(sub.hardFail === true && sub.findings.length === 2,
@@ -30,7 +30,7 @@ import { fileURLToPath } from 'node:url';
30
30
 
31
31
  const require = createRequire(import.meta.url);
32
32
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
33
- const GOV = path.join(ROOT, 'modules', 'governance');
33
+ const GOV = path.join(ROOT, 'modules', 'government');
34
34
  const catalog = require(path.join(GOV, 'catalog.js'));
35
35
  const resolver = require(path.join(GOV, 'resolver.js'));
36
36
  const db = require(path.join(GOV, 'db.js'));
@@ -397,7 +397,7 @@ test('G2: the role resolve is untouched — own-scoped keys still resolve, princ
397
397
  });
398
398
 
399
399
  test('G3: the kernel port carries the ownership axis on the ONE governance port', () => {
400
- const { governancePort } = require(path.join(GOV, 'routes', 'governance.js'));
400
+ const { governancePort } = require(path.join(GOV, 'routes', 'government.js'));
401
401
  for (const name of ['authorizeOwned', 'authorizeOwnedOrAny', 'sameOwner', 'isOwnershipScoped', 'crossOwnerKeysFor']) {
402
402
  assert.equal(typeof governancePort[name], 'function', `the port must expose ${name}`);
403
403
  }
@@ -45,8 +45,8 @@ dbKernel.lookupSession = async () => {
45
45
 
46
46
  const auth = require('../src/bongos/auth.js');
47
47
  const seams = require('../src/module-seams.js');
48
- const catalog = require('../modules/governance/catalog.js');
49
- const resolver = require('../modules/governance/resolver.js');
48
+ const catalog = require('../modules/government/catalog.js');
49
+ const resolver = require('../modules/government/resolver.js');
50
50
  const wandering = require('../modules/builder-settings/wandering-prefs.js');
51
51
  const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
52
52
 
@@ -58,7 +58,7 @@ const LIVE_RANKS = ['xenos', 'thetes', 'metic', 'archon'];
58
58
  // requireNonXenosPage admitted rank !== 'xenos' → thetes and up
59
59
  // requireGovernancePage admitted rank === 'archon' → archon only …until ADR 0157
60
60
  //
61
- // ADR 0157 (owner decision, 2026-08-05) lowered `page.view.governance` archon→metic, so
61
+ // ADR 0157 (owner decision, 2026-08-05) lowered `page.view.government` archon→metic, so
62
62
  // the monitoring pages (/watch, /harbor, /gate) now open to the working rank. That gate
63
63
  // was spelled `requireArchonPage` through R107 and ADR 0157 — the name the rows below
64
64
  // were first written against — and was renamed once the floor moved; the audience here,
@@ -86,7 +86,7 @@ const DENIED_TO = {
86
86
  const GATE_PERMISSION = {
87
87
  requireBuilderPage: 'page.view.public',
88
88
  requireNonXenosPage: 'page.view.builder',
89
- requireGovernancePage: 'page.view.governance',
89
+ requireGovernancePage: 'page.view.government',
90
90
  };
91
91
  const deniedLoc = (gate, { unavailable = false } = {}) =>
92
92
  `${DENIED_TO[gate]}?denied=${GATE_PERMISSION[gate]}${unavailable ? '&reason=unavailable' : ''}`;
@@ -94,7 +94,7 @@ const deniedLoc = (gate, { unavailable = false } = {}) =>
94
94
  const GATE_FLOOR = {
95
95
  requireBuilderPage: 'xenos',
96
96
  requireNonXenosPage: 'thetes',
97
- // ADR 0157: `page.view.governance` floor archon→metic.
97
+ // ADR 0157: `page.view.government` floor archon→metic.
98
98
  requireGovernancePage: 'metic',
99
99
  };
100
100
  const SIGN_IN = '/api/gds/auth/web/start';
@@ -126,7 +126,7 @@ async function runGate(name, { rank = null, resolve, port = true } = {}) {
126
126
  lookupThrows = false;
127
127
  seams._reset();
128
128
  if (port) {
129
- seams.registerProvider('governance', {
129
+ seams.registerProvider('government', {
130
130
  // The real seed for that rank — the rows governance_002 gives the one
131
131
  // rank-role a builder holds. The INPUT, not the oracle.
132
132
  resolveBuilderPermissions: resolve || (async () => catalog.seedForRank(rank)),
@@ -144,7 +144,7 @@ async function runGate(name, { rank = null, resolve, port = true } = {}) {
144
144
 
145
145
  test('A0: the hand-written audience table agrees with the real threshold function', () => {
146
146
  // A second, catalog-free oracle for the literal above: auth.rankMeetsThreshold
147
- // is the production rank predicate and reads nothing from modules/governance/.
147
+ // is the production rank predicate and reads nothing from modules/government/.
148
148
  for (const gate of GATES) {
149
149
  for (const rank of LIVE_RANKS) {
150
150
  assert.equal(
@@ -203,7 +203,7 @@ test('B1: a session-lookup error fails CLOSED (redirect to sign-in, no page)', a
203
203
  for (const gate of GATES) {
204
204
  nextSession = null;
205
205
  seams._reset();
206
- seams.registerProvider('governance', { resolveBuilderPermissions: async () => catalog.seedForRank('archon') });
206
+ seams.registerProvider('government', { resolveBuilderPermissions: async () => catalog.seedForRank('archon') });
207
207
  lookupThrows = true;
208
208
  const res = fakeRes();
209
209
  let nexted = false;
@@ -244,7 +244,7 @@ test('B4: the page gates resolve per request — no memoised authority (ADR 0016
244
244
  nextSession = { builder_id: 7, rank: 'archon' };
245
245
  lookupThrows = false;
246
246
  seams._reset();
247
- seams.registerProvider('governance', {
247
+ seams.registerProvider('government', {
248
248
  resolveBuilderPermissions: async () => { calls += 1; return keys; },
249
249
  });
250
250
  const req = { headers: { cookie: 'gds_session=validtoken' } };
@@ -264,7 +264,7 @@ test('B4: the page gates resolve per request — no memoised authority (ADR 0016
264
264
  test('B5: every permission a page gate names is a real catalog key', async () => {
265
265
  // Not an oracle — a sanity check that the gates did not migrate onto a typo,
266
266
  // which would silently deny everyone (fail-closed, but a bug all the same).
267
- for (const key of ['page.view.public', 'page.view.builder', 'page.view.governance']) {
267
+ for (const key of ['page.view.public', 'page.view.builder', 'page.view.government']) {
268
268
  assert.ok(catalog.byKey(key), `${key} must exist in the catalog`);
269
269
  }
270
270
  });
@@ -57,8 +57,8 @@ const { RANKS } = require(path.join(ROOT, 'src', 'bongos', 'db-kernel.js'));
57
57
  const routeRank = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
58
58
  const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
59
59
  const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
60
- const catalog = require(path.join(ROOT, 'modules', 'governance', 'catalog.js'));
61
- const resolver = require(path.join(ROOT, 'modules', 'governance', 'resolver.js'));
60
+ const catalog = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
61
+ const resolver = require(path.join(ROOT, 'modules', 'government', 'resolver.js'));
62
62
 
63
63
  const { PERMISSIONS, RANK_ORDER, seedForRank } = catalog;
64
64
  const FLOORED = PERMISSIONS.filter((p) => p.floor !== null);
@@ -287,7 +287,7 @@ function fakeRes() {
287
287
  // not the logic — resolvedFor() is the real resolver over the real seed.
288
288
  async function permissionAdmits(key, rank) {
289
289
  seams._reset();
290
- seams.registerProvider('governance', { resolveBuilderPermissions: async () => resolvedFor(rank) });
290
+ seams.registerProvider('government', { resolveBuilderPermissions: async () => resolvedFor(rank) });
291
291
  let nexted = false;
292
292
  await auth.requirePermission(key)({ builder: { id: 7, rank } }, fakeRes(), () => { nexted = true; });
293
293
  seams._reset();
@@ -22,7 +22,7 @@ import { fileURLToPath } from 'node:url';
22
22
 
23
23
  const require = createRequire(import.meta.url);
24
24
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
- const GOV = path.join(ROOT, 'modules', 'governance');
25
+ const GOV = path.join(ROOT, 'modules', 'government');
26
26
  const catalog = require(path.join(GOV, 'catalog.js'));
27
27
  const resolver = require(path.join(GOV, 'resolver.js'));
28
28
 
@@ -149,7 +149,7 @@ test('resolved principal sets are frozen — a caller cannot widen one', () => {
149
149
  // ── show, never grant ───────────────────────────────────────────────────────────
150
150
 
151
151
  test('the tab surface is a read-only projection: shown, flagged non-grantable', () => {
152
- const { governancePort } = require(path.join(GOV, 'routes', 'governance.js'));
152
+ const { governancePort } = require(path.join(GOV, 'routes', 'government.js'));
153
153
  assert.equal(typeof governancePort.listPrincipals, 'function');
154
154
  assert.equal(typeof governancePort.resolvePrincipalPermissions, 'function');
155
155
  const listed = governancePort.listPrincipals();
@@ -162,7 +162,7 @@ test('the tab surface is a read-only projection: shown, flagged non-grantable',
162
162
  });
163
163
 
164
164
  test('the governance port exposes no way to create, grant, or assign a principal', () => {
165
- const { governancePort } = require(path.join(GOV, 'routes', 'governance.js'));
165
+ const { governancePort } = require(path.join(GOV, 'routes', 'government.js'));
166
166
  const mutator = /(create|grant|assign|add|set|write|provision|revoke|delete|update)/i;
167
167
  for (const name of Object.keys(governancePort)) {
168
168
  if (!/principal/i.test(name)) continue;
@@ -8,7 +8,7 @@
8
8
  //
9
9
  // The load-bearing test is BEHAVIOR-IDENTITY. R101 moved WHERE the truth lives
10
10
  // (a hardcoded PROTECTED_GLOBS constant in src/bongos/permission-path-check.js →
11
- // governance-owned data in modules/governance/protected-surfaces.json). It did
11
+ // governance-owned data in modules/government/protected-surfaces.json). It did
12
12
  // NOT change WHAT is protected. EXPECTED_GLOBS_BEFORE_R101 below is the literal
13
13
  // pre-change constant, pasted verbatim; the registry must reproduce it exactly,
14
14
  // in order.
@@ -22,7 +22,7 @@ import { fileURLToPath } from 'node:url';
22
22
 
23
23
  const require = createRequire(import.meta.url);
24
24
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
- const GOV = path.join(ROOT, 'modules', 'governance');
25
+ const GOV = path.join(ROOT, 'modules', 'government');
26
26
 
27
27
  const ppc = require(path.join(ROOT, 'src', 'bongos', 'permission-path-check.js'));
28
28
  const registry = require(path.join(GOV, 'protected-surfaces.js'));
@@ -92,22 +92,34 @@ const EXPECTED_GLOBS_BEFORE_R101 = [
92
92
  // Each entry is { after, glob, why }: `after` is the glob it is inserted
93
93
  // directly below, so the expected list is reconstructed in order rather than
94
94
  // re-pasted (a re-paste is how the two lists drift apart).
95
- const ADDED_SINCE_R101 = [
95
+ const ADDED_SINCE_R101 = [];
96
+
97
+ // Surfaces RENAMED since R101 — still protected, under a new path. Distinct from
98
+ // a removal on purpose: a rename keeps the protection and moves it, a removal
99
+ // ends it. The subset test below accepts a documented rename and rejects a bare
100
+ // disappearance, so "we renamed it" can never be used to quietly unprotect
101
+ // something — the replacement must actually be in the registry.
102
+ const RENAMED_SINCE_R101 = [
96
103
  {
97
- after: 'modules/governance/',
98
- glob: 'modules/government/',
99
- why: 'goal 1000068 / task 1003003 (ADR 0174) — the authz core is renamed '
100
- + 'governance→government. The new glob is registered BEFORE the folder '
101
- + 'moves because `protected` DERIVES from this registry: without it the '
102
- + 'new module key computes protected:false and both the Archon gate on '
103
- + 'goal creation and the pre-push block on these files would silently lapse.',
104
+ from: 'modules/governance/',
105
+ to: 'modules/government/',
106
+ why: 'goal 1000068 / ADR 0174 — the authz core was renamed governance→government. '
107
+ + 'The folder moved in task 1003004 and the one-release compatibility glob was '
108
+ + 'dropped in R05 (task 1003006) along with the /governance/* route aliases. '
109
+ + 'Protection did not lapse at any point: the new glob was registered BEFORE '
110
+ + 'the folder moved (task 1003003), so both were listed across the window.',
104
111
  },
105
112
  ];
106
113
 
107
114
  // The list the registry must produce TODAY: the pre-R101 set with each
108
115
  // documented addition spliced in at its declared position.
109
116
  const EXPECTED_GLOBS = (() => {
110
- const out = [...EXPECTED_GLOBS_BEFORE_R101];
117
+ let out = [...EXPECTED_GLOBS_BEFORE_R101];
118
+ for (const r of RENAMED_SINCE_R101) {
119
+ const at = out.indexOf(r.from);
120
+ assert.notEqual(at, -1, `RENAMED_SINCE_R101 renames '${r.from}', which is not in the list`);
121
+ out[at] = r.to;
122
+ }
111
123
  for (const a of ADDED_SINCE_R101) {
112
124
  const at = out.indexOf(a.after);
113
125
  assert.notEqual(at, -1, `ADDED_SINCE_R101 anchors on '${a.after}', which is not in the list`);
@@ -119,15 +131,28 @@ const EXPECTED_GLOBS = (() => {
119
131
  // ---------- behavior identity: the same globs, at the same floor ----------
120
132
 
121
133
  test('nothing protected before R101 has been unprotected since', () => {
122
- // The load-bearing half. Additions are ordinary policy; a REMOVAL silently
123
- // unprotects a trust surface, so it is asserted separately and must never be
124
- // satisfied by editing the pre-R101 array.
134
+ // The load-bearing half. A surface may be RENAMED (protection moves with it)
135
+ // but never simply dropped. Renames must be declared, and the declared
136
+ // replacement must actually be present — otherwise "it was renamed" becomes a
137
+ // way to unprotect a trust surface with a comment.
138
+ const renamedFrom = new Map(RENAMED_SINCE_R101.map((r) => [r.from, r.to]));
125
139
  for (const glob of EXPECTED_GLOBS_BEFORE_R101) {
126
- assert.ok(
127
- registry.PROTECTED_GLOBS.includes(glob),
140
+ if (registry.PROTECTED_GLOBS.includes(glob)) continue;
141
+ const to = renamedFrom.get(glob);
142
+ assert.ok(to,
128
143
  `'${glob}' was protected before R101 and is no longer in the registry — `
129
- + 'a protected surface cannot be dropped without an ADR saying why.',
130
- );
144
+ + 'a protected surface cannot be dropped without an ADR saying why. If it '
145
+ + 'was renamed, declare it in RENAMED_SINCE_R101.');
146
+ assert.ok(registry.PROTECTED_GLOBS.includes(to),
147
+ `'${glob}' is declared as renamed to '${to}', but '${to}' is not in the `
148
+ + 'registry — the protection went nowhere.');
149
+ }
150
+ });
151
+
152
+ test('every rename since R101 carries a documented reason', () => {
153
+ for (const r of RENAMED_SINCE_R101) {
154
+ assert.ok(r.why && r.why.length > 40,
155
+ `${r.from} -> ${r.to} must record WHY the surface moved.`);
131
156
  }
132
157
  });
133
158
 
@@ -234,7 +259,7 @@ test('a known path is protected at the right floor; an unknown path is not', ()
234
259
  assert.equal(ppc.floorFor('src/bongos/auth.js'), 'metic');
235
260
  assert.equal(ppc.permissionFor('src/bongos/auth.js'), 'path.protected.modify');
236
261
  assert.equal(registry.isProtected('migrations/999_x.sql'), true, 'subtree globs still match');
237
- assert.equal(registry.floorFor('modules/governance/catalog.js'), 'metic');
262
+ assert.equal(registry.floorFor('modules/government/catalog.js'), 'metic');
238
263
 
239
264
  assert.equal(ppc.floorFor('public/game/main.js'), null);
240
265
  assert.equal(ppc.permissionFor('docs/session-logs/2026-07-30-x.md'), null);
@@ -1,7 +1,7 @@
1
1
  // tests/governance_require_permission.mjs — the requirePermission gate (BV1.R97, ADR 0151).
2
2
  //
3
3
  // DB-free. The gate reaches the resolver through the `governance` KERNEL PORT
4
- // (the one-way rule — core never requires modules/governance/), so a stub
4
+ // (the one-way rule — core never requires modules/government/), so a stub
5
5
  // provider registered on the seam is the whole test double: no Postgres, no
6
6
  // router, no live module. That is also the point being asserted — if auth.js ever
7
7
  // went back to importing the module directly, these tests could not stub it.
@@ -23,7 +23,7 @@ const auth = require(path.join(ROOT, 'src', 'bongos', 'auth.js'));
23
23
  const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
24
24
  const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
25
25
  const { classify } = require(path.join(ROOT, 'src', 'bongos', 'api-errors.js'));
26
- const catalog = require(path.join(ROOT, 'modules', 'governance', 'catalog.js'));
26
+ const catalog = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
27
27
 
28
28
  // Minimal res double capturing status + body, with res.fail() installed exactly
29
29
  // as the router installs it (mirrors tests/rank_gate.mjs).
@@ -42,7 +42,7 @@ function fakeRes() {
42
42
  // resolveBuilderPermissions; pass null to leave the port UNREGISTERED.
43
43
  async function run(gate, req, resolve) {
44
44
  seams._reset();
45
- if (resolve !== null) seams.registerProvider('governance', { resolveBuilderPermissions: resolve });
45
+ if (resolve !== null) seams.registerProvider('government', { resolveBuilderPermissions: resolve });
46
46
  const res = fakeRes();
47
47
  let nexted = false;
48
48
  await gate(req, res, () => { nexted = true; });
@@ -63,12 +63,12 @@ test('a builder holding the permission passes the gate', async () => {
63
63
 
64
64
  test('a missing grant 403s with permission_forbidden and explains the denial', async () => {
65
65
  const { res, nexted } = await run(
66
- auth.requirePermission('governance.manage'), { builder: BUILDER }, holding('idea.triage'),
66
+ auth.requirePermission('government.manage'), { builder: BUILDER }, holding('idea.triage'),
67
67
  );
68
68
  assert.equal(nexted, false, 'next() must NOT be called');
69
69
  assert.equal(res._status, 403);
70
70
  assert.equal(res._json.error.code, 'permission_forbidden');
71
- assert.deepEqual(res._json.error.details.required, ['governance.manage']);
71
+ assert.deepEqual(res._json.error.details.required, ['government.manage']);
72
72
  assert.deepEqual(res._json.error.details.held, [], 'holds none of the required perms');
73
73
  });
74
74
 
@@ -89,7 +89,7 @@ test('the denial names the floor rank, not just the atom (task 1002613, map P-1)
89
89
  // this test moves WITH a floor change instead of pinning stale prose — the
90
90
  // D-16/N-3 failure shape the fix exists to avoid.
91
91
  seams._reset();
92
- seams.registerProvider('governance', {
92
+ seams.registerProvider('government', {
93
93
  resolveBuilderPermissions: holding('idea.triage'),
94
94
  permissionFloor: (key) => (catalog.byKey(key) || {}).floor || null,
95
95
  });
@@ -107,11 +107,11 @@ test('the denial names the floor rank, not just the atom (task 1002613, map P-1)
107
107
 
108
108
  test('a port without permissionFloor still denies with a message; floors is null', async () => {
109
109
  const { res } = await run(
110
- auth.requirePermission('governance.manage'), { builder: BUILDER }, holding('idea.triage'),
110
+ auth.requirePermission('government.manage'), { builder: BUILDER }, holding('idea.triage'),
111
111
  );
112
112
  assert.equal(res._status, 403);
113
113
  assert.equal(res._json.error.details.floors, null);
114
- assert.ok(res._json.error.message.includes('governance.manage'),
114
+ assert.ok(res._json.error.message.includes('government.manage'),
115
115
  'the message names the missing atom even when no floor is resolvable');
116
116
  });
117
117
 
@@ -143,7 +143,7 @@ test('an UNREGISTERED governance port denies, it does not admit', async () => {
143
143
 
144
144
  test('a malformed port (no resolveBuilderPermissions) denies', async () => {
145
145
  seams._reset();
146
- seams.registerProvider('governance', { isSystem: () => true });
146
+ seams.registerProvider('government', { isSystem: () => true });
147
147
  const res = fakeRes();
148
148
  let nexted = false;
149
149
  await auth.requirePermission('idea.triage')({ builder: BUILDER }, res, () => { nexted = true; });
@@ -175,7 +175,7 @@ test('the gate resolves per request — no memoised authority (ADR 0016)', async
175
175
  let calls = 0;
176
176
  let keys = ['idea.triage'];
177
177
  seams._reset();
178
- seams.registerProvider('governance', { resolveBuilderPermissions: async () => { calls += 1; return keys; } });
178
+ seams.registerProvider('government', { resolveBuilderPermissions: async () => { calls += 1; return keys; } });
179
179
  const gate = auth.requirePermission('idea.triage');
180
180
  const first = fakeRes();
181
181
  let admitted = false;
@@ -264,7 +264,7 @@ test('over the four seeded ranks, requirePermission admits exactly who requireRa
264
264
  'security.report.file',
265
265
  ];
266
266
  const cases = [
267
- ['governance.manage', ['archon']],
267
+ ['government.manage', ['archon']],
268
268
  ['task.claim.any', ['thetes', 'metic', 'archon']],
269
269
  ['cost.log', ['xenos', 'thetes', 'metic', 'archon']],
270
270
  // The CORE route gates migrated in BV1.R104, each paired with the requireRank
@@ -16,7 +16,7 @@ import { fileURLToPath } from 'node:url';
16
16
 
17
17
  const require = createRequire(import.meta.url);
18
18
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
19
- const GOV = path.join(ROOT, 'modules', 'governance');
19
+ const GOV = path.join(ROOT, 'modules', 'government');
20
20
  const resolver = require(path.join(GOV, 'resolver.js'));
21
21
  const catalog = require(path.join(GOV, 'catalog.js'));
22
22
  const { effectivePermissions, resolveBuilderPermissions, builderHasPermissions } = resolver;
@@ -74,11 +74,11 @@ 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', 'governance.js'));
77
+ const { governancePort } = require(path.join(GOV, 'routes', 'government.js'));
78
78
  assert.equal(typeof governancePort.resolveBuilderPermissions, 'function');
79
79
  assert.equal(typeof governancePort.builderHasPermissions, 'function');
80
80
  assert.equal(typeof governancePort.isSystem, 'function');
81
81
  const manifest = JSON.parse(fs.readFileSync(path.join(GOV, 'module.json'), 'utf8'));
82
- assert.ok(manifest.provides.includes('governance'), 'module.json must declare the governance port');
83
- assert.ok(manifest.contributes.routes.includes('governance'), 'the port registers from the route factory');
82
+ assert.ok(manifest.provides.includes('government'), 'module.json must declare the government port');
83
+ assert.ok(manifest.contributes.routes.includes('government'), 'the port registers from the route factory');
84
84
  });
@@ -1,8 +1,8 @@
1
1
  // tests/governance_role_sync.mjs — rank→role assignment stays in sync (BV1.R95b, ADR 0151).
2
2
  //
3
- // DB-free. The decision the writer executes is a pure function (modules/governance/
4
- // rank-roles.js planRankRoleSync), so these exercise PRODUCTION logic directly rather
5
- // than a re-implementation: db.syncRankRole reads the two row sets, calls this exact
3
+ // DB-free. The decision the writer executes is a pure function (modules/government/
4
+ // ranks.js planRankSync), so these exercise PRODUCTION logic directly rather
5
+ // than a re-implementation: db.syncRank reads the two row sets, calls this exact
6
6
  // function, and executes its verdict. The SQL halves — the DELETE/INSERT the writer
7
7
  // issues and the repair migration — are asserted structurally.
8
8
  //
@@ -21,21 +21,21 @@ import { fileURLToPath } from 'node:url';
21
21
 
22
22
  const require = createRequire(import.meta.url);
23
23
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
24
- const GOV = path.join(ROOT, 'modules', 'governance');
25
- const { planRankRoleSync, isSeededRankRole } = require(path.join(GOV, 'rank-roles.js'));
24
+ const GOV = path.join(ROOT, 'modules', 'government');
25
+ const { planRankSync, isSeededRank } = require(path.join(GOV, 'ranks.js'));
26
26
 
27
27
  // The seeded rank-roles exactly as governance_002 writes them.
28
28
  const SEEDED = [
29
- { role_key: 'xenos', seeded_rank: 'xenos', is_system: true },
30
- { role_key: 'thetes', seeded_rank: 'thetes', is_system: true },
31
- { role_key: 'metic', seeded_rank: 'metic', is_system: true },
32
- { role_key: 'archon', seeded_rank: 'archon', is_system: true },
29
+ { rank_key: 'xenos', seeded_rank: 'xenos', is_system: true },
30
+ { rank_key: 'thetes', seeded_rank: 'thetes', is_system: true },
31
+ { rank_key: 'metic', seeded_rank: 'metic', is_system: true },
32
+ { rank_key: 'archon', seeded_rank: 'archon', is_system: true },
33
33
  ];
34
- const seededRole = (rank) => ({ role_key: rank, seeded_rank: rank, is_system: true });
34
+ const seededRank_ = (rank) => ({ rank_key: rank, seeded_rank: rank, is_system: true });
35
35
  // An Archon-authored custom role from the R110 tab: no seeded_rank, not a system role.
36
- const customRole = (key) => ({ role_key: key, seeded_rank: null, is_system: false });
36
+ const customRank = (key) => ({ rank_key: key, seeded_rank: null, is_system: false });
37
37
 
38
- const plan = (rank, currentRoles) => planRankRoleSync({ rank, currentRoles, seededRoles: SEEDED });
38
+ const plan = (rank, currentRanks) => planRankSync({ rank, currentRanks, seededRanks: SEEDED });
39
39
 
40
40
  // ---------------------------------------------------------------------------
41
41
  // registration — a builder with no roles gets the one matching their rank
@@ -60,7 +60,7 @@ test('registration: the firstAdmin bootstrap (seated straight to archon) assigns
60
60
  // ---------------------------------------------------------------------------
61
61
 
62
62
  test('promotion xenos→thetes: the new role is added and the old one removed', () => {
63
- const p = plan('thetes', [seededRole('xenos')]);
63
+ const p = plan('thetes', [seededRank_('xenos')]);
64
64
  assert.equal(p.target, 'thetes');
65
65
  assert.deepEqual([...p.add], ['thetes']);
66
66
  assert.deepEqual([...p.remove], ['xenos'], 'the superseded rank-role must be removed');
@@ -69,8 +69,8 @@ test('promotion xenos→thetes: the new role is added and the old one removed',
69
69
 
70
70
  test('promotion via the auto-graduation path leaves exactly one seeded role', () => {
71
71
  // maybeAutoGraduateToThetes runs the same _applyRankChangeTx, so the same plan.
72
- const p = plan('thetes', [seededRole('xenos')]);
73
- const seededHeld = p.next.filter((k) => SEEDED.some((s) => s.role_key === k));
72
+ const p = plan('thetes', [seededRank_('xenos')]);
73
+ const seededHeld = p.next.filter((k) => SEEDED.some((s) => s.rank_key === k));
74
74
  assert.equal(seededHeld.length, 1);
75
75
  });
76
76
 
@@ -79,7 +79,7 @@ test('promotion via the auto-graduation path leaves exactly one seeded role', ()
79
79
  // ---------------------------------------------------------------------------
80
80
 
81
81
  test('demotion archon→metic REMOVES the archon role (not merely adds metic)', () => {
82
- const p = plan('metic', [seededRole('archon')]);
82
+ const p = plan('metic', [seededRank_('archon')]);
83
83
  assert.deepEqual([...p.remove], ['archon'], 'the higher rank-role must be REMOVED on demotion');
84
84
  assert.deepEqual([...p.add], ['metic']);
85
85
  // The absence assertion is the one that matters: an additive-only sync would
@@ -89,7 +89,7 @@ test('demotion archon→metic REMOVES the archon role (not merely adds metic)',
89
89
  });
90
90
 
91
91
  test('demotion archon→xenos (a multi-rung drop) removes archon just the same', () => {
92
- const p = plan('xenos', [seededRole('archon')]);
92
+ const p = plan('xenos', [seededRank_('archon')]);
93
93
  assert.ok(p.remove.includes('archon'));
94
94
  assert.ok(!p.next.includes('archon'));
95
95
  assert.deepEqual([...p.next], ['xenos']);
@@ -98,7 +98,7 @@ test('demotion archon→xenos (a multi-rung drop) removes archon just the same',
98
98
  test('a builder holding several stale seeded roles is reduced to exactly one', () => {
99
99
  // Defence in depth: if drift ever left two seeded roles on one builder, the sync
100
100
  // must not pick one and leave the other — every non-matching seeded role goes.
101
- const p = plan('thetes', [seededRole('archon'), seededRole('metic'), seededRole('xenos')]);
101
+ const p = plan('thetes', [seededRank_('archon'), seededRank_('metic'), seededRank_('xenos')]);
102
102
  assert.deepEqual([...p.remove].sort(), ['archon', 'metic', 'xenos']);
103
103
  assert.deepEqual([...p.next], ['thetes']);
104
104
  });
@@ -108,17 +108,17 @@ test('a builder holding several stale seeded roles is reduced to exactly one', (
108
108
  // ---------------------------------------------------------------------------
109
109
 
110
110
  test('an Archon-assigned custom role survives a rank change', () => {
111
- const p = plan('metic', [seededRole('archon'), customRole('release-manager')]);
111
+ const p = plan('metic', [seededRank_('archon'), customRank('release-manager')]);
112
112
  assert.deepEqual([...p.remove], ['archon'], 'only the seeded rank-role rotates');
113
113
  assert.ok(!p.remove.includes('release-manager'), 'a custom role must never be revoked by a rank change');
114
114
  assert.deepEqual([...p.next], ['metic', 'release-manager']);
115
115
  });
116
116
 
117
117
  test('a custom role is not mistaken for the rank-role even when its key matches a rank name', () => {
118
- // role_key is UNIQUE so this cannot happen today, but the predicate must key on
118
+ // rank_key is UNIQUE so this cannot happen today, but the predicate must key on
119
119
  // is_system + seeded_rank rather than on the key's spelling.
120
- const impostor = { role_key: 'archon', seeded_rank: null, is_system: false };
121
- assert.equal(isSeededRankRole(impostor), false);
120
+ const impostor = { rank_key: 'archon', seeded_rank: null, is_system: false };
121
+ assert.equal(isSeededRank(impostor), false);
122
122
  const p = plan('metic', [impostor]);
123
123
  assert.deepEqual([...p.remove], [], 'a non-system role is never rotated away');
124
124
  });
@@ -127,7 +127,7 @@ test('a builder on a non-seeded legacy rank resolves to zero seeded roles', () =
127
127
  // migration 023's CHECK still admits twelve reserved ranks. governance_002's INNER
128
128
  // JOIN gives them no row BY DESIGN (ADR 0151's fail-closed divergence) — preserve
129
129
  // it, and strip any seeded role they were carrying.
130
- const p = plan('hippeis', [seededRole('archon')]);
130
+ const p = plan('hippeis', [seededRank_('archon')]);
131
131
  assert.equal(p.target, null, 'no seeded role exists for a legacy rank — invent none');
132
132
  assert.deepEqual([...p.add], []);
133
133
  assert.deepEqual([...p.remove], ['archon'], 'a legacy rank must not retain a seeded rank-role');
@@ -135,7 +135,7 @@ test('a builder on a non-seeded legacy rank resolves to zero seeded roles', () =
135
135
  });
136
136
 
137
137
  test('a legacy-rank builder still keeps their custom roles', () => {
138
- const p = plan('hippeis', [seededRole('metic'), customRole('release-manager')]);
138
+ const p = plan('hippeis', [seededRank_('metic'), customRank('release-manager')]);
139
139
  assert.deepEqual([...p.next], ['release-manager']);
140
140
  });
141
141
 
@@ -144,23 +144,23 @@ test('a legacy-rank builder still keeps their custom roles', () => {
144
144
  // ---------------------------------------------------------------------------
145
145
 
146
146
  test('the sync is idempotent — running it twice changes nothing the second time', () => {
147
- const first = plan('metic', [seededRole('archon')]);
147
+ const first = plan('metic', [seededRank_('archon')]);
148
148
  assert.ok(first.add.length + first.remove.length > 0, 'the first run must do work');
149
- const after = first.next.map((key) => (SEEDED.find((s) => s.role_key === key) || customRole(key)));
150
- const second = planRankRoleSync({ rank: 'metic', currentRoles: after, seededRoles: SEEDED });
149
+ const after = first.next.map((key) => (SEEDED.find((s) => s.rank_key === key) || customRank(key)));
150
+ const second = planRankSync({ rank: 'metic', currentRanks: after, seededRanks: SEEDED });
151
151
  assert.deepEqual([...second.add], []);
152
152
  assert.deepEqual([...second.remove], []);
153
153
  assert.deepEqual([...second.next], [...first.next]);
154
154
  });
155
155
 
156
156
  test('a builder already holding the right role produces no write at all', () => {
157
- const p = plan('metic', [seededRole('metic'), customRole('release-manager')]);
157
+ const p = plan('metic', [seededRank_('metic'), customRank('release-manager')]);
158
158
  assert.deepEqual([...p.add], []);
159
159
  assert.deepEqual([...p.remove], []);
160
160
  });
161
161
 
162
162
  test('the plan is frozen — a caller cannot widen it after the decision', () => {
163
- const p = plan('metic', [seededRole('archon')]);
163
+ const p = plan('metic', [seededRank_('archon')]);
164
164
  assert.ok(Object.isFrozen(p) && Object.isFrozen(p.add) && Object.isFrozen(p.remove));
165
165
  assert.throws(() => p.add.push('archon'), TypeError);
166
166
  });
@@ -175,15 +175,15 @@ const repairSql = fs.readFileSync(path.join(migDir, 'governance_005_resync_build
175
175
  const stripSqlComments = (sql) => sql.replace(/--[^\n]*/g, '');
176
176
 
177
177
  test('the writer DELETEs the superseded assignments, not just INSERTs the new one', () => {
178
- assert.match(dbSrc, /DELETE FROM governance_builder_roles/,
179
- 'syncRankRole must delete superseded rank-roles — an additive-only writer preserves privilege retention');
180
- assert.match(dbSrc, /INSERT INTO governance_builder_roles[\s\S]*ON CONFLICT \(builder_id, role_key\) DO NOTHING/,
178
+ assert.match(dbSrc, /DELETE FROM government_builder_ranks/,
179
+ 'syncRank must delete superseded rank-roles — an additive-only writer preserves privilege retention');
180
+ assert.match(dbSrc, /INSERT INTO government_builder_ranks[\s\S]*ON CONFLICT \(builder_id, rank_key\) DO NOTHING/,
181
181
  'the assignment insert must be idempotent');
182
182
  });
183
183
 
184
184
  test('the writer runs on the CALLER\'s transaction client when given one', () => {
185
- assert.match(dbSrc, /async function syncRankRole\(client,/,
186
- 'syncRankRole must take the caller\'s client so the rotation is atomic with the rank write');
185
+ assert.match(dbSrc, /async function syncRank\(client,/,
186
+ 'syncRank must take the caller\'s client so the rotation is atomic with the rank write');
187
187
  assert.match(dbSrc, /const q = client \|\| pool;/,
188
188
  'and fall back to the pool only when there is no surrounding transaction');
189
189
  });
@@ -197,7 +197,7 @@ test('both rank-change twins rotate the role inside their transaction', () => {
197
197
  assert.ok(upd !== -1, `${rel}: expected the rank UPDATE in _applyRankChangeTx`);
198
198
  assert.ok(sync !== -1, `${rel}: the rank write must rotate the seeded rank-role`);
199
199
  assert.ok(sync > upd, `${rel}: the rotation must follow the rank write inside the same transaction`);
200
- assert.match(src, /resolveOptional\('governance\.roleSync'\)/,
200
+ assert.match(src, /resolveOptional\('government\.rankSync'\)/,
201
201
  `${rel}: must reach governance through the kernel port, never by importing the module`);
202
202
  }
203
203
  });