@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
@@ -337,25 +337,25 @@ function createClient(opts = {}) {
337
337
  // POST /goals/reorder — rank: metic+archon — POST /goals/reorder
338
338
  postGoalsReorder: (args) => request("POST", "/goals/reorder", { hasBody: true }, args),
339
339
  },
340
- "governance": {
341
- // GET /governance/assignments — rank: archon — GET /governance/assignments
342
- getGovernanceAssignments: (args) => request("GET", "/governance/assignments", { hasBody: false }, args),
343
- // POST /governance/assignments — rank: archon — POST /governance/assignments
344
- postGovernanceAssignments: (args) => request("POST", "/governance/assignments", { hasBody: true }, args),
345
- // DELETE /governance/assignments/{builderId}/{roleKey} — rank: archon — DELETE /governance/assignments/:builderId/:roleKey
346
- deleteGovernanceAssignmentsBuilderIdRoleKey: (args) => request("DELETE", "/governance/assignments/{builderId}/{roleKey}", { hasBody: false }, args),
347
- // GET /governance/permissions — rank: archon — GET /governance/permissions
348
- getGovernancePermissions: (args) => request("GET", "/governance/permissions", { hasBody: false }, args),
349
- // GET /governance/roles — rank: archon — GET /governance/roles
350
- getGovernanceRoles: (args) => request("GET", "/governance/roles", { hasBody: false }, args),
351
- // POST /governance/roles — rank: archon — POST /governance/roles
352
- postGovernanceRoles: (args) => request("POST", "/governance/roles", { hasBody: true }, args),
353
- // DELETE /governance/roles/{roleKey}/permissions/{permissionKey} — rank: archon — DELETE /governance/roles/:roleKey/permissions/:permissionKey
354
- deleteGovernanceRolesRoleKeyPermissionsPermissionKey: (args) => request("DELETE", "/governance/roles/{roleKey}/permissions/{permissionKey}", { hasBody: false }, args),
355
- // PUT /governance/roles/{roleKey}/permissions/{permissionKey} — rank: archon — PUT /governance/roles/:roleKey/permissions/:permissionKey
356
- putGovernanceRolesRoleKeyPermissionsPermissionKey: (args) => request("PUT", "/governance/roles/{roleKey}/permissions/{permissionKey}", { hasBody: true }, args),
357
- // POST /governance/roles/{roleKey}/reset — rank: archon — POST /governance/roles/:roleKey/reset
358
- postGovernanceRolesRoleKeyReset: (args) => request("POST", "/governance/roles/{roleKey}/reset", { hasBody: true }, args),
340
+ "government": {
341
+ // GET /government/assignments — rank: archon — GET /government/assignments
342
+ getGovernmentAssignments: (args) => request("GET", "/government/assignments", { hasBody: false }, args),
343
+ // POST /government/assignments — rank: archon — POST /government/assignments
344
+ postGovernmentAssignments: (args) => request("POST", "/government/assignments", { hasBody: true }, args),
345
+ // DELETE /government/assignments/{builderId}/{rankKey} — rank: archon — DELETE /government/assignments/:builderId/:rankKey
346
+ deleteGovernmentAssignmentsBuilderIdRankKey: (args) => request("DELETE", "/government/assignments/{builderId}/{rankKey}", { hasBody: false }, args),
347
+ // GET /government/permissions — rank: archon — GET /government/permissions
348
+ getGovernmentPermissions: (args) => request("GET", "/government/permissions", { hasBody: false }, args),
349
+ // GET /government/ranks — rank: archon — GET /government/ranks
350
+ getGovernmentRanks: (args) => request("GET", "/government/ranks", { hasBody: false }, args),
351
+ // POST /government/ranks — rank: archon — POST /government/ranks
352
+ postGovernmentRanks: (args) => request("POST", "/government/ranks", { hasBody: true }, args),
353
+ // DELETE /government/ranks/{rankKey}/permissions/{permissionKey} — rank: archon — DELETE /government/ranks/:rankKey/permissions/:permissionKey
354
+ deleteGovernmentRanksRankKeyPermissionsPermissionKey: (args) => request("DELETE", "/government/ranks/{rankKey}/permissions/{permissionKey}", { hasBody: false }, args),
355
+ // PUT /government/ranks/{rankKey}/permissions/{permissionKey} — rank: archon — PUT /government/ranks/:rankKey/permissions/:permissionKey
356
+ putGovernmentRanksRankKeyPermissionsPermissionKey: (args) => request("PUT", "/government/ranks/{rankKey}/permissions/{permissionKey}", { hasBody: true }, args),
357
+ // POST /government/ranks/{rankKey}/reset — rank: archon — POST /government/ranks/:rankKey/reset
358
+ postGovernmentRanksRankKeyReset: (args) => request("POST", "/government/ranks/{rankKey}/reset", { hasBody: true }, args),
359
359
  },
360
360
  "grades": {
361
361
  // GET /grades/by-builder — rank: metic+archon — GET /grades/by-builder
@@ -104,7 +104,7 @@ const PERMISSIONS = [
104
104
  { key: 'task.confirm.grade_bypass', system: true, floor: 'archon', guards: 'POST /tasks/:id/confirm — ratify past a failed grade (env-relaxable, ADR 0041)' },
105
105
  { key: 'dependency.cross_tier.manage', system: true, floor: 'archon', guards: 'POST|DELETE /dependencies — cross-tier structural edges (ADR 0086)' },
106
106
  { key: 'goal.scope.manage', system: true, floor: 'archon', guards: 'cross-tier goal-scope edges + protected-scope admission (ADR 0086)' },
107
- { key: 'governance.manage', system: true, floor: 'archon', guards: 'the governance tab routes (roles/perms/assignments CRUD, reads included — R110) — meta-escalation' },
107
+ { key: 'government.manage', system: true, floor: 'archon', guards: 'the governance tab routes (roles/perms/assignments CRUD, reads included — R110) — meta-escalation' },
108
108
  { key: 'gate_approval.decide', system: true, floor: 'archon', guards: 'POST /gate-approvals/:pr/approve — CI deploy-gate override' },
109
109
  { key: 'deploy.push_main', system: true, substrate: true, floor: 'archon', guards: 'git push origin main / the deploy — substrate-enforced (pre-push, main-audit)' },
110
110
  { key: 'path.protected.modify', system: true, substrate: true, floor: 'archon', guards: 'modifying a PROTECTED_GLOBS path below the Metic floor — substrate (ADR 0043)' },
@@ -123,7 +123,7 @@ const PERMISSIONS = [
123
123
  //
124
124
  // The four trust-boundary keys that ALSO stay Archon are system:true above
125
125
  // (`task.confirm.grade_bypass`, `gate_approval.decide`, `goal.scope.manage`,
126
- // `governance.manage`) — see the ADR's escalation-chain argument for why moving any
126
+ // `government.manage`) — see the ADR's escalation-chain argument for why moving any
127
127
  // one of them would make this delegation self-reversible.
128
128
  { key: 'builder.status.set', system: false, floor: 'archon', guards: 'PATCH /builders/:id/status — offboard / (de)activate' },
129
129
  { key: 'auth.session.revoke', system: false, floor: 'archon', guards: 'POST /auth/revoke, /auth/revoke-all' },
@@ -207,7 +207,7 @@ const PERMISSIONS = [
207
207
  { key: 'audit_log.read', system: false, floor: 'metic', guards: 'GET /audit-log (ADR 0157, was archon)' },
208
208
  // ADR 0157 §4. Split from `gate_approval.decide` (system, archon) so the Gate PAGE
209
209
  // can open to Metic while APPROVING a held PR stays Archon. Without the split, a
210
- // Metic could reach /gate via `page.view.governance` and then 403 on its only data
210
+ // Metic could reach /gate via `page.view.government` and then 403 on its only data
211
211
  // endpoint — a page that renders nothing but an error.
212
212
  { key: 'gate_approval.read', system: false, floor: 'metic', guards: 'GET /gate-approvals — the Gate page read side (approve = gate_approval.decide, archon)' },
213
213
 
@@ -236,9 +236,9 @@ const PERMISSIONS = [
236
236
  // ADR 0157: floor archon→metic, which is what opens /watch, /harbor and /gate to the
237
237
  // working rank. The gate FUNCTION was renamed to match (`requireArchonPage` →
238
238
  // `requireGovernancePage`) once the floor moved. Viewing is all this grants: every
239
- // write those pages reach is its own atom, and `governance.manage` (the
239
+ // write those pages reach is its own atom, and `government.manage` (the
240
240
  // roles/permissions tab) stayed archon.
241
- { key: 'page.view.governance', system: false, floor: 'metic', guards: 'the governance/monitoring pages — /watch, /harbor, /gate (requireGovernancePage; write = governance.manage, archon)' },
241
+ { key: 'page.view.government', system: false, floor: 'metic', guards: 'the governance/monitoring pages — /watch, /harbor, /gate (requireGovernancePage; write = government.manage, archon)' },
242
242
  ];
243
243
 
244
244
  // ── invariants asserted at module load (fail loud on an authoring typo) ─────────
@@ -410,7 +410,7 @@ function substrateKeys() {
410
410
  //
411
411
  // The owner's 2026-08-16 decision made the three sub-Archon RANK roles editable, so
412
412
  // "is this grantable" stopped being a property of the key alone. `role` is a
413
- // governance_roles row ({ is_system, seeded_rank }); the four answers:
413
+ // government_ranks row ({ is_system, seeded_rank }); the four answers:
414
414
  //
415
415
  // • NEVER, to any role — a machine-principal key (held by an identity, not a role)
416
416
  // and a SUBSTRATE key. The substrate three are inert as grant rows: no route
@@ -418,13 +418,13 @@ function substrateKeys() {
418
418
  // success and delegate nothing. Refusing them keeps the tab honest.
419
419
  // • the ARCHON rank-role — nothing. It is immutable and already holds everything,
420
420
  // and that immutability is also the LOCKOUT GUARD: the only role carrying
421
- // `governance.manage` by seed can never have it revoked, so no sequence of
421
+ // `government.manage` by seed can never have it revoked, so no sequence of
422
422
  // clicks in this tab can lock the owner out of the tab.
423
423
  // • a SEEDED rank-role BELOW archon — everything else, the seven in-app Archon
424
424
  // powers included. This is the delegation lever the owner asked for.
425
425
  // • a CUSTOM role — operational only: the pre-existing R110 wall, deliberately NOT
426
426
  // widened. A rank-role is the owner's own coarse ladder, moved by a rank write;
427
- // a custom role is ad-hoc and assignable by anyone holding `governance.manage`,
427
+ // a custom role is ad-hoc and assignable by anyone holding `government.manage`,
428
428
  // so widening it would hand the second group the first group's lever.
429
429
  //
430
430
  // Fail-closed on everything unrecognised: unknown key, missing role, or a system role
@@ -6,8 +6,8 @@
6
6
  // assignments are DB rows. This file resolves the config half, mirroring
7
7
  // src/branding.js precedence exactly:
8
8
  //
9
- // env override -> instance config (config/governance.json) -> neutral
10
- // starter (config/governance.neutral.json)
9
+ // env override -> instance config (config/government.json) -> neutral
10
+ // starter (config/government.neutral.json)
11
11
  //
12
12
  // A fresh instance with NO config resolves to the PLAIN neutral role set — never to
13
13
  // a host's labels (fail-to-vanilla, the same posture branding takes). R102 proves
@@ -17,8 +17,8 @@
17
17
  // second source of authority truth, which is exactly what ADR 0016 forbids:
18
18
  // • config — TEMPLATES/DEFAULTS: a role's display label, its description, and the
19
19
  // permission list a starting CUSTOM role template proposes. Shapes only.
20
- // • DB — LIVE STATE: governance_roles / governance_role_permissions /
21
- // governance_builder_roles (R93). WHO HOLDS WHAT is a row, resolved server-side
20
+ // • DB — LIVE STATE: government_ranks / government_rank_permissions /
21
+ // government_builder_ranks (R93). WHO HOLDS WHAT is a row, resolved server-side
22
22
  // per request by resolver.js. Nothing in this file grants anything to anybody.
23
23
  //
24
24
  // Three fail-closed rules, in the same spirit as resolver.js's drop-unknown-key rule:
@@ -55,8 +55,8 @@ const catalog = require('./catalog');
55
55
  // split lands, the INSTANCE path routes through resolveInstanceRoot() — the module
56
56
  // doorway does not re-export the root resolvers yet.
57
57
  const ROOT = path.resolve(__dirname, '..', '..');
58
- const NEUTRAL_PATH = path.join(ROOT, 'config', 'governance.neutral.json');
59
- const INSTANCE_PATH = path.join(ROOT, 'config', 'governance.json');
58
+ const NEUTRAL_PATH = path.join(ROOT, 'config', 'government.neutral.json');
59
+ const INSTANCE_PATH = path.join(ROOT, 'config', 'government.json');
60
60
 
61
61
  const SEEDED_RANK_ROLES = new Set(catalog.RANK_ORDER);
62
62
 
@@ -11,7 +11,7 @@
11
11
  // next request — the ADR 0016 trust-boundary rule the resolver inherits.
12
12
 
13
13
  const { pool } = require('../../src/module-api');
14
- const { planRankRoleSync } = require('./rank-roles');
14
+ const { planRankSync } = require('./ranks');
15
15
 
16
16
  // Every permission_key granted to any role the builder holds. Duplicates across
17
17
  // roles are collapsed by the DISTINCT — the resolver's UNION semantics. Returns
@@ -20,8 +20,8 @@ const { planRankRoleSync } = require('./rank-roles');
20
20
  async function getPermissionKeysForBuilder(builderId) {
21
21
  const { rows } = await pool.query(
22
22
  `SELECT DISTINCT rp.permission_key
23
- FROM governance_builder_roles br
24
- JOIN governance_role_permissions rp ON rp.role_key = br.role_key
23
+ FROM government_builder_ranks br
24
+ JOIN government_rank_permissions rp ON rp.rank_key = br.rank_key
25
25
  WHERE br.builder_id = $1`,
26
26
  [builderId],
27
27
  );
@@ -32,16 +32,16 @@ async function getPermissionKeysForBuilder(builderId) {
32
32
  // R110/R111) and for explaining a 403. Not part of the resolve path.
33
33
  async function getRoleKeysForBuilder(builderId) {
34
34
  const { rows } = await pool.query(
35
- `SELECT role_key FROM governance_builder_roles WHERE builder_id = $1 ORDER BY role_key`,
35
+ `SELECT rank_key FROM government_builder_ranks WHERE builder_id = $1 ORDER BY rank_key`,
36
36
  [builderId],
37
37
  );
38
- return rows.map((r) => r.role_key);
38
+ return rows.map((r) => r.rank_key);
39
39
  }
40
40
 
41
41
  // --- the rank→role WRITE half (BV1.R95b) ------------------------------------
42
42
  //
43
43
  // Everything above this line only READS. Until R95b the only write to
44
- // governance_builder_roles in the whole repo was governance_002's run-once
44
+ // government_builder_ranks in the whole repo was governance_002's run-once
45
45
  // backfill, so a builder created after it had NO role (locked out of every
46
46
  // permission gate) and a builder whose rank CHANGED kept their OLD rank-role
47
47
  // (a demoted Archon still resolving Archon permissions — the ADR 0016 defect).
@@ -49,7 +49,7 @@ async function getRoleKeysForBuilder(builderId) {
49
49
  // The writer below is a TRANSACTION PARTICIPANT: every caller hands it the client
50
50
  // its own BEGIN/COMMIT is running on, so the role rotation commits — or rolls back
51
51
  // — atomically with the rank write it accompanies. Core reaches it through the
52
- // `governance.roleSync` kernel port (routes/role-sync.js); it never imports this
52
+ // `government.rankSync` kernel port (routes/rank-sync.js); it never imports this
53
53
  // module (ADR 0083).
54
54
 
55
55
  // The seeded rank-roles as the DB actually defines them. Read per call rather than
@@ -57,8 +57,8 @@ async function getRoleKeysForBuilder(builderId) {
57
57
  // runs on rank writes + sign-in, never in a hot loop.
58
58
  async function getSeededRankRoles(q) {
59
59
  const { rows } = await q.query(
60
- `SELECT role_key, seeded_rank, is_system
61
- FROM governance_roles
60
+ `SELECT rank_key, seeded_rank, is_system
61
+ FROM government_ranks
62
62
  WHERE is_system = true AND seeded_rank IS NOT NULL`,
63
63
  );
64
64
  return rows;
@@ -68,16 +68,16 @@ async function getSeededRankRoles(q) {
68
68
  // seeded rank-role from an Archon-assigned custom one.
69
69
  async function getRoleAssignmentsForBuilder(q, builderId) {
70
70
  const { rows } = await q.query(
71
- `SELECT br.role_key, r.is_system, r.seeded_rank
72
- FROM governance_builder_roles br
73
- JOIN governance_roles r ON r.role_key = br.role_key
71
+ `SELECT br.rank_key, r.is_system, r.seeded_rank
72
+ FROM government_builder_ranks br
73
+ JOIN government_ranks r ON r.rank_key = br.rank_key
74
74
  WHERE br.builder_id = $1`,
75
75
  [builderId],
76
76
  );
77
77
  return rows;
78
78
  }
79
79
 
80
- // syncRankRole(client, { builderId, rank }) — make the builder's seeded rank-role
80
+ // syncRank(client, { builderId, rank }) — make the builder's seeded rank-role
81
81
  // match `rank`: add the matching one, DELETE every other seeded one, leave custom
82
82
  // roles alone. `client` is the caller's open transaction client; pass null/undefined
83
83
  // to run on the pool (the sign-in path, which has no surrounding transaction).
@@ -89,27 +89,27 @@ async function getRoleAssignmentsForBuilder(q, builderId) {
89
89
  // assigned_by is left NULL: a rank-derived assignment is the system's, not a person's
90
90
  // (governance_002's backfill uses the same convention; the human decision is audited
91
91
  // in credit_log's rank_change row).
92
- async function syncRankRole(client, { builderId, rank } = {}) {
92
+ async function syncRank(client, { builderId, rank } = {}) {
93
93
  if (builderId === null || builderId === undefined || builderId === '') {
94
94
  return { target: null, added: [], removed: [] };
95
95
  }
96
96
  const q = client || pool;
97
97
  const seededRoles = await getSeededRankRoles(q);
98
98
  const currentRoles = await getRoleAssignmentsForBuilder(q, builderId);
99
- const plan = planRankRoleSync({ rank, currentRoles, seededRoles });
99
+ const plan = planRankSync({ rank, currentRoles, seededRoles });
100
100
 
101
101
  if (plan.remove.length > 0) {
102
102
  await q.query(
103
- `DELETE FROM governance_builder_roles
104
- WHERE builder_id = $1 AND role_key = ANY($2::text[])`,
103
+ `DELETE FROM government_builder_ranks
104
+ WHERE builder_id = $1 AND rank_key = ANY($2::text[])`,
105
105
  [builderId, [...plan.remove]],
106
106
  );
107
107
  }
108
108
  if (plan.add.length > 0) {
109
109
  await q.query(
110
- `INSERT INTO governance_builder_roles (builder_id, role_key)
110
+ `INSERT INTO government_builder_ranks (builder_id, rank_key)
111
111
  SELECT $1, unnest($2::text[])
112
- ON CONFLICT (builder_id, role_key) DO NOTHING`,
112
+ ON CONFLICT (builder_id, rank_key) DO NOTHING`,
113
113
  [builderId, [...plan.add]],
114
114
  );
115
115
  }
@@ -127,23 +127,23 @@ async function syncRankRole(client, { builderId, rank } = {}) {
127
127
 
128
128
  async function listRolesWithGrants() {
129
129
  const { rows } = await pool.query(
130
- `SELECT r.role_key, r.label, r.description, r.is_system, r.seeded_rank,
130
+ `SELECT r.rank_key, r.label, r.description, r.is_system, r.seeded_rank,
131
131
  r.created_by, r.created_at,
132
132
  COALESCE(array_agg(rp.permission_key ORDER BY rp.permission_key)
133
133
  FILTER (WHERE rp.permission_key IS NOT NULL), '{}') AS permissions
134
- FROM governance_roles r
135
- LEFT JOIN governance_role_permissions rp ON rp.role_key = r.role_key
134
+ FROM government_ranks r
135
+ LEFT JOIN government_rank_permissions rp ON rp.rank_key = r.rank_key
136
136
  GROUP BY r.id
137
- ORDER BY r.is_system DESC, r.role_key`,
137
+ ORDER BY r.is_system DESC, r.rank_key`,
138
138
  );
139
139
  return rows;
140
140
  }
141
141
 
142
142
  async function getRoleByKey(roleKey) {
143
143
  const { rows } = await pool.query(
144
- `SELECT role_key, label, description, is_system, seeded_rank, created_by, created_at
145
- FROM governance_roles
146
- WHERE role_key = $1`,
144
+ `SELECT rank_key, label, description, is_system, seeded_rank, created_by, created_at
145
+ FROM government_ranks
146
+ WHERE rank_key = $1`,
147
147
  [roleKey],
148
148
  );
149
149
  return rows[0] ?? null;
@@ -151,9 +151,9 @@ async function getRoleByKey(roleKey) {
151
151
 
152
152
  async function createRole({ roleKey, label, description, createdBy }) {
153
153
  const { rows } = await pool.query(
154
- `INSERT INTO governance_roles (role_key, label, description, is_system, seeded_rank, created_by)
154
+ `INSERT INTO government_ranks (rank_key, label, description, is_system, seeded_rank, created_by)
155
155
  VALUES ($1, $2, $3, false, NULL, $4)
156
- RETURNING role_key, label, description, is_system, seeded_rank, created_by, created_at`,
156
+ RETURNING rank_key, label, description, is_system, seeded_rank, created_by, created_at`,
157
157
  [roleKey, label, description ?? null, createdBy],
158
158
  );
159
159
  return rows[0];
@@ -161,9 +161,9 @@ async function createRole({ roleKey, label, description, createdBy }) {
161
161
 
162
162
  async function grantRolePermission({ roleKey, permissionKey }) {
163
163
  const { rowCount } = await pool.query(
164
- `INSERT INTO governance_role_permissions (role_key, permission_key)
164
+ `INSERT INTO government_rank_permissions (rank_key, permission_key)
165
165
  VALUES ($1, $2)
166
- ON CONFLICT (role_key, permission_key) DO NOTHING`,
166
+ ON CONFLICT (rank_key, permission_key) DO NOTHING`,
167
167
  [roleKey, permissionKey],
168
168
  );
169
169
  return rowCount > 0;
@@ -171,8 +171,8 @@ async function grantRolePermission({ roleKey, permissionKey }) {
171
171
 
172
172
  async function revokeRolePermission({ roleKey, permissionKey }) {
173
173
  const { rowCount } = await pool.query(
174
- `DELETE FROM governance_role_permissions
175
- WHERE role_key = $1 AND permission_key = $2`,
174
+ `DELETE FROM government_rank_permissions
175
+ WHERE rank_key = $1 AND permission_key = $2`,
176
176
  [roleKey, permissionKey],
177
177
  );
178
178
  return rowCount > 0;
@@ -188,12 +188,12 @@ async function resetRoleGrants({ roleKey, permissionKeys }) {
188
188
  const client = await pool.connect();
189
189
  try {
190
190
  await client.query('BEGIN');
191
- await client.query(`DELETE FROM governance_role_permissions WHERE role_key = $1`, [roleKey]);
191
+ await client.query(`DELETE FROM government_rank_permissions WHERE rank_key = $1`, [roleKey]);
192
192
  if (permissionKeys.length > 0) {
193
193
  await client.query(
194
- `INSERT INTO governance_role_permissions (role_key, permission_key)
194
+ `INSERT INTO government_rank_permissions (rank_key, permission_key)
195
195
  SELECT $1, unnest($2::text[])
196
- ON CONFLICT (role_key, permission_key) DO NOTHING`,
196
+ ON CONFLICT (rank_key, permission_key) DO NOTHING`,
197
197
  [roleKey, [...permissionKeys]],
198
198
  );
199
199
  }
@@ -210,21 +210,21 @@ async function resetRoleGrants({ roleKey, permissionKeys }) {
210
210
  async function listAssignments() {
211
211
  const { rows } = await pool.query(
212
212
  `SELECT br.builder_id, b.github_login, b.display_name, b.rank,
213
- br.role_key, r.label AS role_label, r.is_system, r.seeded_rank,
213
+ br.rank_key, r.label AS role_label, r.is_system, r.seeded_rank,
214
214
  br.assigned_by, br.assigned_at
215
- FROM governance_builder_roles br
215
+ FROM government_builder_ranks br
216
216
  JOIN builders b ON b.id = br.builder_id
217
- JOIN governance_roles r ON r.role_key = br.role_key
218
- ORDER BY br.builder_id, br.role_key`,
217
+ JOIN government_ranks r ON r.rank_key = br.rank_key
218
+ ORDER BY br.builder_id, br.rank_key`,
219
219
  );
220
220
  return rows;
221
221
  }
222
222
 
223
223
  async function assignRole({ builderId, roleKey, assignedBy }) {
224
224
  const { rowCount } = await pool.query(
225
- `INSERT INTO governance_builder_roles (builder_id, role_key, assigned_by)
225
+ `INSERT INTO government_builder_ranks (builder_id, rank_key, assigned_by)
226
226
  VALUES ($1, $2, $3)
227
- ON CONFLICT (builder_id, role_key) DO NOTHING`,
227
+ ON CONFLICT (builder_id, rank_key) DO NOTHING`,
228
228
  [builderId, roleKey, assignedBy],
229
229
  );
230
230
  return rowCount > 0;
@@ -232,8 +232,8 @@ async function assignRole({ builderId, roleKey, assignedBy }) {
232
232
 
233
233
  async function unassignRole({ builderId, roleKey }) {
234
234
  const { rowCount } = await pool.query(
235
- `DELETE FROM governance_builder_roles
236
- WHERE builder_id = $1 AND role_key = $2`,
235
+ `DELETE FROM government_builder_ranks
236
+ WHERE builder_id = $1 AND rank_key = $2`,
237
237
  [builderId, roleKey],
238
238
  );
239
239
  return rowCount > 0;
@@ -244,7 +244,7 @@ module.exports = {
244
244
  getRoleKeysForBuilder,
245
245
  getSeededRankRoles,
246
246
  getRoleAssignmentsForBuilder,
247
- syncRankRole,
247
+ syncRank,
248
248
  listRolesWithGrants,
249
249
  getRoleByKey,
250
250
  createRole,
@@ -0,0 +1,213 @@
1
+ -- governance_008_rename_to_government_ranks.sql
2
+ -- Goal 1000068 R03 (task 1003004) — ADR 0174.
3
+ --
4
+ -- Renames the three authz tables, their indexes, their constraints and the
5
+ -- `role_key` column to the post-ADR-0174 vocabulary: the permission system is
6
+ -- the GOVERNMENT, and the positions in it are RANKS. "Role" is reserved for the
7
+ -- three crafts (Artist / Ideator / Builder).
8
+ --
9
+ -- governance_roles -> government_ranks
10
+ -- governance_builder_roles -> government_builder_ranks
11
+ -- governance_role_permissions -> government_rank_permissions
12
+ -- <all three>.role_key -> rank_key
13
+ --
14
+ -- ── THIS MIGRATION IS NOT SAFE TO APPLY BEFORE R04/R05 ──────────────────────
15
+ -- It is a BREAKING rename with no compatibility shim. The moment it applies,
16
+ -- code that still queries `governance_roles` fails — and that code is the
17
+ -- permission resolver, on every authenticated request. That is why the live
18
+ -- instance's core auto-upgrade sweep (`cloudbongos-core-update-subscription
19
+ -- .timer`) was stopped and disabled on 2026-08-16, freezing the pin at core
20
+ -- 1.19.179 so this migration cannot reach the live database until the renamed
21
+ -- code (R04 task 1003005, R05 task 1003006) is in the same release. Task
22
+ -- 1003011 re-enables the sweep and is dependency-gated on all three.
23
+ -- The owner chose the pause over compatibility views, having been shown both.
24
+ --
25
+ -- ── WHAT THIS MIGRATION DELIBERATELY DOES NOT DO ────────────────────────────
26
+ -- It does NOT rename the permission-key ROWS (`governance.manage` ->
27
+ -- `government.manage`, `page.view.governance` -> `page.view.government`). Those
28
+ -- belong in R05, in the SAME commit as the `catalog.js` constants, because the
29
+ -- resolver DROPS any grant naming a key the catalog does not define (the
30
+ -- fail-closed rule in modules/governance/CLAUDE.md). Renaming the rows here
31
+ -- would mean that between this migration and R05 every `government.manage` row
32
+ -- resolves to nothing — and since that key gates the Government tab itself, the
33
+ -- owner would be locked out of the surface used to inspect the damage. The
34
+ -- schema renames below pair with R04's code; the key renames pair with R05's.
35
+ --
36
+ -- ── MECHANICS ───────────────────────────────────────────────────────────────
37
+ -- ALTER TABLE ... RENAME is catalog-only: it rewrites no rows, holds an
38
+ -- ACCESS EXCLUSIVE lock for the instant it takes, and carries every index,
39
+ -- constraint, foreign key, sequence ownership and privilege with it (they
40
+ -- reference the table by OID, not by name). The same is true of RENAME COLUMN,
41
+ -- including for the two inbound foreign keys that point at `role_key`.
42
+ -- Constraint and index NAMES do not follow, however — Postgres leaves them
43
+ -- spelled the old way — so they are renamed explicitly below. Nothing depends
44
+ -- on those names at runtime; they are renamed so a reader of \d output is not
45
+ -- told a story about "roles" that stopped being true.
46
+ --
47
+ -- IDEMPOTENT: every statement is guarded on the old name still being present,
48
+ -- so a re-run after a partial apply is a no-op rather than an error. migrate.sh
49
+ -- applies each file once, but a hand-run during recovery should not punish you.
50
+ --
51
+ -- ── VERIFIED AGAINST REAL DATA ──────────────────────────────────────────────
52
+ -- Rehearsed on a scratch database restored from the live 2026-08-16 dump (not a
53
+ -- synthetic fixture): apply, apply-again, roll back, apply again. The
54
+ -- (builder, permission) fingerprint md5 fb6be239ae2b3bf811888ac90cfb6346 and the
55
+ -- row baseline 4 ranks / 15 assignments / 161 grants were byte-identical at
56
+ -- every step, and an INSERT through the renamed sequence still allocated an id.
57
+ -- Snapshots of the live database live at
58
+ -- /home/lars/mig/pre-govt-rename-{authz,full}-2026-08-16T213128Z.*
59
+ --
60
+ -- ROLLBACK — reverse every RENAME. Lossless (no row is touched). This exact
61
+ -- script was executed as part of the rehearsal above:
62
+ --
63
+ -- BEGIN;
64
+ -- ALTER TABLE government_ranks RENAME TO governance_roles;
65
+ -- ALTER TABLE government_builder_ranks RENAME TO governance_builder_roles;
66
+ -- ALTER TABLE government_rank_permissions RENAME TO governance_role_permissions;
67
+ -- ALTER TABLE governance_roles RENAME COLUMN rank_key TO role_key;
68
+ -- ALTER TABLE governance_builder_roles RENAME COLUMN rank_key TO role_key;
69
+ -- ALTER TABLE governance_role_permissions RENAME COLUMN rank_key TO role_key;
70
+ -- ALTER SEQUENCE government_ranks_id_seq RENAME TO governance_roles_id_seq;
71
+ -- ALTER INDEX government_rank_permissions_perm_idx
72
+ -- RENAME TO governance_role_permissions_perm_idx;
73
+ -- ALTER INDEX government_builder_ranks_rank_idx
74
+ -- RENAME TO governance_builder_roles_role_idx;
75
+ -- COMMIT;
76
+ --
77
+ -- Constraint names are left spelled the new way by that rollback; they are
78
+ -- cosmetic and step 4 below re-derives them on the next forward apply.
79
+
80
+ BEGIN;
81
+
82
+ -- ── 1. the tables ───────────────────────────────────────────────────────────
83
+ DO $$
84
+ BEGIN
85
+ IF to_regclass('public.governance_roles') IS NOT NULL
86
+ AND to_regclass('public.government_ranks') IS NULL THEN
87
+ ALTER TABLE governance_roles RENAME TO government_ranks;
88
+ END IF;
89
+
90
+ IF to_regclass('public.governance_builder_roles') IS NOT NULL
91
+ AND to_regclass('public.government_builder_ranks') IS NULL THEN
92
+ ALTER TABLE governance_builder_roles RENAME TO government_builder_ranks;
93
+ END IF;
94
+
95
+ IF to_regclass('public.governance_role_permissions') IS NOT NULL
96
+ AND to_regclass('public.government_rank_permissions') IS NULL THEN
97
+ ALTER TABLE governance_role_permissions RENAME TO government_rank_permissions;
98
+ END IF;
99
+ END $$;
100
+
101
+ -- ── 2. the role_key -> rank_key column, on all three tables ─────────────────
102
+ -- The two child columns are foreign keys onto the parent's unique column; a
103
+ -- RENAME COLUMN updates the dependency in place, so the order here is free.
104
+ DO $$
105
+ BEGIN
106
+ IF EXISTS (SELECT 1 FROM information_schema.columns
107
+ WHERE table_name = 'government_ranks' AND column_name = 'role_key') THEN
108
+ ALTER TABLE government_ranks RENAME COLUMN role_key TO rank_key;
109
+ END IF;
110
+
111
+ IF EXISTS (SELECT 1 FROM information_schema.columns
112
+ WHERE table_name = 'government_builder_ranks' AND column_name = 'role_key') THEN
113
+ ALTER TABLE government_builder_ranks RENAME COLUMN role_key TO rank_key;
114
+ END IF;
115
+
116
+ IF EXISTS (SELECT 1 FROM information_schema.columns
117
+ WHERE table_name = 'government_rank_permissions' AND column_name = 'role_key') THEN
118
+ ALTER TABLE government_rank_permissions RENAME COLUMN role_key TO rank_key;
119
+ END IF;
120
+ END $$;
121
+
122
+ -- ── 3. indexes ──────────────────────────────────────────────────────────────
123
+ DO $$
124
+ BEGIN
125
+ IF to_regclass('public.governance_role_permissions_perm_idx') IS NOT NULL
126
+ AND to_regclass('public.government_rank_permissions_perm_idx') IS NULL THEN
127
+ ALTER INDEX governance_role_permissions_perm_idx
128
+ RENAME TO government_rank_permissions_perm_idx;
129
+ END IF;
130
+
131
+ IF to_regclass('public.governance_builder_roles_role_idx') IS NOT NULL
132
+ AND to_regclass('public.government_builder_ranks_rank_idx') IS NULL THEN
133
+ ALTER INDEX governance_builder_roles_role_idx
134
+ RENAME TO government_builder_ranks_rank_idx;
135
+ END IF;
136
+ END $$;
137
+
138
+ -- ── 3b. the bigserial sequence ──────────────────────────────────────────────
139
+ -- `id bigserial` created `governance_roles_id_seq`, and a table rename does NOT
140
+ -- carry the sequence's NAME with it — only its ownership. Left alone, the
141
+ -- database would go on saying "governance_roles" forever in the one place
142
+ -- nobody thinks to look, which is precisely the drift this goal exists to end.
143
+ -- Safe: the column default is stored as an OID (`nextval('…'::regclass)`), so
144
+ -- it re-renders against the new name and keeps working.
145
+ DO $$
146
+ BEGIN
147
+ IF to_regclass('public.governance_roles_id_seq') IS NOT NULL
148
+ AND to_regclass('public.government_ranks_id_seq') IS NULL THEN
149
+ ALTER SEQUENCE governance_roles_id_seq RENAME TO government_ranks_id_seq;
150
+ END IF;
151
+ END $$;
152
+
153
+ -- ── 4. constraint names ─────────────────────────────────────────────────────
154
+ -- Cosmetic but load-bearing for the next reader: an error message quoting
155
+ -- `governance_builder_roles_role_key_fkey` after the rename would send them
156
+ -- looking for a table that no longer exists.
157
+ DO $$
158
+ DECLARE
159
+ r record;
160
+ BEGIN
161
+ FOR r IN
162
+ SELECT c.conname, t.relname AS tbl,
163
+ replace(replace(replace(c.conname,
164
+ 'governance_role_permissions', 'government_rank_permissions'),
165
+ 'governance_builder_roles', 'government_builder_ranks'),
166
+ 'governance_roles', 'government_ranks') AS newname
167
+ FROM pg_constraint c
168
+ JOIN pg_class t ON t.oid = c.conrelid
169
+ WHERE t.relname IN ('government_ranks', 'government_builder_ranks', 'government_rank_permissions')
170
+ AND c.conname LIKE 'governance\_%'
171
+ LOOP
172
+ -- the _role_key_ segment inside a constraint name follows the column
173
+ EXECUTE format('ALTER TABLE %I RENAME CONSTRAINT %I TO %I',
174
+ r.tbl, r.conname, replace(r.newname, '_role_key', '_rank_key'));
175
+ END LOOP;
176
+ END $$;
177
+
178
+ -- ── 5. prove the rename moved names only, never rows ────────────────────────
179
+ -- A rename that silently lost a grant would be catastrophic and invisible: the
180
+ -- resolver would just start denying. Assert the baseline captured before the
181
+ -- migration ran. If this fails the whole transaction rolls back.
182
+ DO $$
183
+ DECLARE
184
+ n_ranks int;
185
+ n_assign int;
186
+ n_grants int;
187
+ BEGIN
188
+ SELECT count(*) INTO n_ranks FROM government_ranks;
189
+ SELECT count(*) INTO n_assign FROM government_builder_ranks;
190
+ SELECT count(*) INTO n_grants FROM government_rank_permissions;
191
+
192
+ IF n_ranks = 0 OR n_assign = 0 OR n_grants = 0 THEN
193
+ RAISE EXCEPTION
194
+ 'government rename lost rows: ranks=%, assignments=%, grants=% — expected all non-zero. Rolling back.',
195
+ n_ranks, n_assign, n_grants;
196
+ END IF;
197
+
198
+ -- Every assignment and every grant must still resolve to a real rank. A
199
+ -- broken FK cannot happen through RENAME, so this is a cheap proof that the
200
+ -- constraints really did travel with the tables rather than being dropped.
201
+ IF EXISTS (SELECT 1 FROM government_builder_ranks b
202
+ WHERE NOT EXISTS (SELECT 1 FROM government_ranks r WHERE r.rank_key = b.rank_key)) THEN
203
+ RAISE EXCEPTION 'government_builder_ranks has an assignment pointing at no rank — rolling back.';
204
+ END IF;
205
+ IF EXISTS (SELECT 1 FROM government_rank_permissions g
206
+ WHERE NOT EXISTS (SELECT 1 FROM government_ranks r WHERE r.rank_key = g.rank_key)) THEN
207
+ RAISE EXCEPTION 'government_rank_permissions has a grant pointing at no rank — rolling back.';
208
+ END IF;
209
+
210
+ RAISE NOTICE 'government rename OK: % ranks, % assignments, % grants', n_ranks, n_assign, n_grants;
211
+ END $$;
212
+
213
+ COMMIT;