@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,22 +1,22 @@
1
1
  'use strict';
2
2
 
3
- // modules/governance/config.js — the governance config contract (ADR 0151, BV1.R96).
3
+ // modules/government/config.js — the government config contract (ADR 0151, BV1.R96).
4
4
  //
5
- // Role TEMPLATES and their display labels are per-instance CONFIG; live grants and
5
+ // Rank TEMPLATES and their display labels are per-instance CONFIG; live grants and
6
6
  // assignments are DB rows. This file resolves the config half, mirroring
7
7
  // src/branding.js precedence exactly:
8
8
  //
9
9
  // env override -> instance config (config/government.json) -> neutral
10
10
  // starter (config/government.neutral.json)
11
11
  //
12
- // A fresh instance with NO config resolves to the PLAIN neutral role set — never to
12
+ // A fresh instance with NO config resolves to the PLAIN neutral rank set — never to
13
13
  // a host's labels (fail-to-vanilla, the same posture branding takes). R102 proves
14
14
  // that boot.
15
15
  //
16
16
  // WHAT LIVES HERE vs WHAT LIVES IN THE DB — get this wrong and config becomes a
17
17
  // second source of authority truth, which is exactly what ADR 0016 forbids:
18
- // • config — TEMPLATES/DEFAULTS: a role's display label, its description, and the
19
- // permission list a starting CUSTOM role template proposes. Shapes only.
18
+ // • config — TEMPLATES/DEFAULTS: a rank's display label, its description, and the
19
+ // permission list a starting CUSTOM rank template proposes. Shapes only.
20
20
  // • DB — LIVE STATE: government_ranks / government_rank_permissions /
21
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.
@@ -25,21 +25,21 @@
25
25
  // 1. A template naming a `system: true` catalog key is STRIPPED. An instance's
26
26
  // config file is editable by anyone who can write the checkout, so it must never
27
27
  // be able to hand out deploy / rank-set / protected-path power. catalog
28
- // .isGrantable() is that wall — the same one the governance API and the Archon
28
+ // .isGrantable() is that wall — the same one the government API and the Archon
29
29
  // tab use, so this cannot drift from the catalog.
30
30
  // 2. A template naming a key the catalog does not define is DROPPED (the catalog is
31
31
  // the source of truth for what a permission IS — resolver.js, same rule).
32
- // 3. A `permissions` list on one of the four SEEDED rank-roles is DROPPED wholesale.
32
+ // 3. A `permissions` list on one of the four SEEDED ranks is DROPPED wholesale.
33
33
  // Their grants are DERIVED from catalog floors and materialised by
34
- // governance_002; config may relabel a seeded role, never re-grant it.
34
+ // governance_002; config may relabel a seeded rank, never re-grant it.
35
35
  //
36
36
  // Stripping rather than throwing matches resolver.js: a typo in a config file must
37
37
  // fail CLOSED, not brick the boot.
38
38
  //
39
39
  // SLICE BOUNDARY: this file is the config CONTRACT, and deliberately has no production
40
- // caller yet — R96 is the "what shape is a role template, and what can config never
40
+ // caller yet — R96 is the "what shape is a rank template, and what can config never
41
41
  // do" decision, landed before the surfaces that read it. R110/R111 (the Archon
42
- // governance tab) is the first intended consumer, and R102 (the vanilla-boot proof)
42
+ // Government tab) is the first intended consumer, and R102 (the vanilla-boot proof)
43
43
  // asserts against the neutral starter. Wiring a speculative caller now would guess at
44
44
  // their needs; the tests pin the contract until they land.
45
45
 
@@ -62,7 +62,7 @@ const SEEDED_RANK_ROLES = new Set(catalog.RANK_ORDER);
62
62
 
63
63
  const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
64
64
 
65
- // Instance roles merge over neutral PER ROLE KEY and per field, so an instance can
65
+ // Instance ranks merge over neutral PER RANK KEY and per field, so an instance can
66
66
  // relabel `metic` without restating the other three. The shape is exactly two levels
67
67
  // deep, so this is a targeted merge rather than branding's generic deepMerge.
68
68
  function mergeRoles(neutral, instance) {
@@ -80,13 +80,18 @@ function mergeRoles(neutral, instance) {
80
80
  // widen authority, so there is no env knob that adds a permission. It exists for the
81
81
  // co-tenant case branding's AUTH_IDP override exists for — several hosted instances
82
82
  // sharing the neutral pack, differentiated only by env, with no config file of their
83
- // own. Format: "<PREFIX>_GOVERNANCE_ROLE_LABELS=metic=Trusted,archon=Owner". It can
84
- // only RELABEL a role the merged config already defines — a typo names no role and is
85
- // inert, rather than quietly growing the role set from an env string.
86
- function applyLabelOverrides(roles, env) {
87
- const raw = api.resolveEnv('GOVERNANCE_ROLE_LABELS', { env });
88
- if (!raw) return roles;
89
- const out = { ...roles };
83
+ // own. Format: "<PREFIX>_GOVERNMENT_RANK_LABELS=metic=Trusted,archon=Owner". It can
84
+ // only RELABEL a rank the merged config already defines — a typo names no rank and is
85
+ // inert, rather than quietly growing the rank set from an env string.
86
+ function applyLabelOverrides(ranks, env) {
87
+ // GOVERNMENT_RANK_LABELS is the post-ADR-0174 name. The pre-rename spelling is
88
+ // still read because an instance sets this in its own shell/env file, OUTSIDE
89
+ // this repo — dropping it would silently stop honouring a label override an
90
+ // operator already configured. Remove once instances have migrated.
91
+ const raw = api.resolveEnv('GOVERNMENT_RANK_LABELS', { env })
92
+ || api.resolveEnv('GOVERNANCE_ROLE_LABELS', { env });
93
+ if (!raw) return ranks;
94
+ const out = { ...ranks };
90
95
  for (const pair of String(raw).split(',')) {
91
96
  const eq = pair.indexOf('=');
92
97
  if (eq <= 0) continue;
@@ -116,27 +121,32 @@ function sanitizeRole(key, raw) {
116
121
  seeded,
117
122
  label: typeof raw.label === 'string' && raw.label ? raw.label : key,
118
123
  description: typeof raw.description === 'string' ? raw.description : '',
119
- // Rule 3: a seeded rank-role's grants come from catalog floors via governance_002.
124
+ // Rule 3: a seeded rank's grants come from catalog floors via governance_002.
120
125
  permissions: seeded ? Object.freeze([]) : grantableOnly(raw.permissions),
121
126
  });
122
127
  }
123
128
 
124
129
  // Pure resolver — all inputs injected, so tests drive it with zero I/O (the
125
130
  // resolveBranding pattern).
126
- function resolveGovernanceConfig({ neutral, instance, env = process.env } = {}) {
127
- const merged = applyLabelOverrides(mergeRoles((neutral || {}).roles, (instance || {}).roles), env);
128
- const roles = {};
131
+ function resolveGovernmentConfig({ neutral, instance, env = process.env } = {}) {
132
+ // `ranks` is the post-ADR-0174 key. An INSTANCE file authored before the rename
133
+ // may still say `roles`; accept it so an external self-hosted instance does not
134
+ // silently lose its templates on upgrade. The neutral file shipped in core is
135
+ // always `ranks`, so this fallback only ever reads host-owned config.
136
+ const instanceRanks = (instance || {}).ranks || (instance || {}).roles;
137
+ const merged = applyLabelOverrides(mergeRoles((neutral || {}).ranks, instanceRanks), env);
138
+ const ranks = {};
129
139
  for (const key of Object.keys(merged).sort()) {
130
- if (!isObj(merged[key])) continue; // a malformed entry is not a role, and grants nothing
131
- roles[key] = sanitizeRole(key, merged[key]);
140
+ if (!isObj(merged[key])) continue; // a malformed entry is not a rank, and grants nothing
141
+ ranks[key] = sanitizeRole(key, merged[key]);
132
142
  }
133
- // The four rank-roles ALWAYS exist (governance_002 seeds them unconditionally), so a
143
+ // The four ranks ALWAYS exist (governance_002 seeds them unconditionally), so a
134
144
  // config that omits or mangles one falls back to the canonical rank name rather than
135
- // dropping the role out of the display set.
145
+ // dropping the rank out of the display set.
136
146
  for (const key of catalog.RANK_ORDER) {
137
- if (!roles[key]) roles[key] = sanitizeRole(key, {});
147
+ if (!ranks[key]) ranks[key] = sanitizeRole(key, {});
138
148
  }
139
- return Object.freeze({ roles: Object.freeze(roles) });
149
+ return Object.freeze({ ranks: Object.freeze(ranks) });
140
150
  }
141
151
 
142
152
  // READ, never require(): a bare require('../../config/X.json') of a boundary-excluded
@@ -148,7 +158,7 @@ function readJson(p) {
148
158
 
149
159
  // Disk loader with the documented precedence. BOTH files are optional: the instance
150
160
  // pack because a vanilla instance ships only the neutral starter, and the neutral
151
- // starter because the four rank-roles still resolve to their canonical names without
161
+ // starter because the four ranks still resolve to their canonical names without
152
162
  // it. Unlike branding — where a missing identity is fatal — no value here is required,
153
163
  // so the loader fails to sane defaults rather than throwing.
154
164
  //
@@ -157,8 +167,8 @@ function readJson(p) {
157
167
  // consumer needs it on a hot path, resolve once at boot and pass the frozen result —
158
168
  // do NOT add a cache here (this file sits next to resolver.js, whose no-cache rule is
159
169
  // absolute; keeping config cache-free too means nobody has to check which is which).
160
- function loadGovernanceConfig({ neutralPath = NEUTRAL_PATH, instancePath = INSTANCE_PATH, env = process.env } = {}) {
161
- return resolveGovernanceConfig({
170
+ function loadGovernmentConfig({ neutralPath = NEUTRAL_PATH, instancePath = INSTANCE_PATH, env = process.env } = {}) {
171
+ return resolveGovernmentConfig({
162
172
  neutral: readJson(neutralPath) || {},
163
173
  instance: readJson(instancePath) || {},
164
174
  env,
@@ -166,8 +176,8 @@ function loadGovernanceConfig({ neutralPath = NEUTRAL_PATH, instancePath = INSTA
166
176
  }
167
177
 
168
178
  module.exports = {
169
- resolveGovernanceConfig,
170
- loadGovernanceConfig,
179
+ resolveGovernmentConfig,
180
+ loadGovernmentConfig,
171
181
  grantableOnly,
172
182
  NEUTRAL_PATH,
173
183
  INSTANCE_PATH,
@@ -1,22 +1,22 @@
1
1
  'use strict';
2
2
 
3
- // modules/governance/db.js — the governance db slice (ADR 0151, BV1.R95).
3
+ // modules/government/db.js — the government db slice (ADR 0151, BV1.R95).
4
4
  //
5
- // Reads the role/grant/assignment rows governance_001_tables.sql defines. The
5
+ // Reads the rank/grant/assignment rows governance_001_tables.sql defines. The
6
6
  // per-domain db-slice pattern (ADR 0091 §2): the pool comes through the published
7
7
  // doorway (src/module-api.js), never `../../src/bongos/db`.
8
8
  //
9
9
  // NO CACHING, deliberately. Every read here runs against the live DB on the
10
- // request that needs it, so a revoked role takes effect on the builder's very
10
+ // request that needs it, so a revoked rank takes effect on the builder's very
11
11
  // next request — the ADR 0016 trust-boundary rule the resolver inherits.
12
12
 
13
13
  const { pool } = require('../../src/module-api');
14
14
  const { planRankSync } = require('./ranks');
15
15
 
16
- // Every permission_key granted to any role the builder holds. Duplicates across
17
- // roles are collapsed by the DISTINCT — the resolver's UNION semantics. Returns
16
+ // Every permission_key granted to any rank the builder holds. Duplicates across
17
+ // ranks are collapsed by the DISTINCT — the resolver's UNION semantics. Returns
18
18
  // a flat array of raw key strings (validated against the catalog by resolver.js);
19
- // [] for a builder with no roles, which fails closed to zero authority.
19
+ // [] for a builder with no ranks, which fails closed to zero authority.
20
20
  async function getPermissionKeysForBuilder(builderId) {
21
21
  const { rows } = await pool.query(
22
22
  `SELECT DISTINCT rp.permission_key
@@ -28,9 +28,9 @@ async function getPermissionKeysForBuilder(builderId) {
28
28
  return rows.map((r) => r.permission_key);
29
29
  }
30
30
 
31
- // The role keys a builder holds, for introspection surfaces (the Archon tab,
31
+ // The rank keys a builder holds, for introspection surfaces (the Archon tab,
32
32
  // R110/R111) and for explaining a 403. Not part of the resolve path.
33
- async function getRoleKeysForBuilder(builderId) {
33
+ async function getRankKeysForBuilder(builderId) {
34
34
  const { rows } = await pool.query(
35
35
  `SELECT rank_key FROM government_builder_ranks WHERE builder_id = $1 ORDER BY rank_key`,
36
36
  [builderId],
@@ -38,24 +38,24 @@ async function getRoleKeysForBuilder(builderId) {
38
38
  return rows.map((r) => r.rank_key);
39
39
  }
40
40
 
41
- // --- the rank→role WRITE half (BV1.R95b) ------------------------------------
41
+ // --- the rank→rank WRITE half (BV1.R95b) ------------------------------------
42
42
  //
43
43
  // Everything above this line only READS. Until R95b the only write to
44
44
  // government_builder_ranks in the whole repo was governance_002's run-once
45
- // backfill, so a builder created after it had NO role (locked out of every
46
- // permission gate) and a builder whose rank CHANGED kept their OLD rank-role
45
+ // backfill, so a builder created after it had NO rank (locked out of every
46
+ // permission gate) and a builder whose rank CHANGED kept their OLD rank
47
47
  // (a demoted Archon still resolving Archon permissions — the ADR 0016 defect).
48
48
  //
49
49
  // The writer below is a TRANSACTION PARTICIPANT: every caller hands it the client
50
- // its own BEGIN/COMMIT is running on, so the role rotation commits — or rolls back
50
+ // its own BEGIN/COMMIT is running on, so the rank rotation commits — or rolls back
51
51
  // — atomically with the rank write it accompanies. Core reaches it through the
52
52
  // `government.rankSync` kernel port (routes/rank-sync.js); it never imports this
53
53
  // module (ADR 0083).
54
54
 
55
- // The seeded rank-roles as the DB actually defines them. Read per call rather than
55
+ // The seeded ranks as the DB actually defines them. Read per call rather than
56
56
  // cached: it is the same authority data the resolver refuses to memoise, and this
57
57
  // runs on rank writes + sign-in, never in a hot loop.
58
- async function getSeededRankRoles(q) {
58
+ async function getSeededRanks(q) {
59
59
  const { rows } = await q.query(
60
60
  `SELECT rank_key, seeded_rank, is_system
61
61
  FROM government_ranks
@@ -64,9 +64,9 @@ async function getSeededRankRoles(q) {
64
64
  return rows;
65
65
  }
66
66
 
67
- // A builder's assignments joined to their role definitions, so the plan can tell a
68
- // seeded rank-role from an Archon-assigned custom one.
69
- async function getRoleAssignmentsForBuilder(q, builderId) {
67
+ // A builder's assignments joined to their rank definitions, so the plan can tell a
68
+ // seeded rank from an Archon-assigned custom one.
69
+ async function getRankAssignmentsForBuilder(q, builderId) {
70
70
  const { rows } = await q.query(
71
71
  `SELECT br.rank_key, r.is_system, r.seeded_rank
72
72
  FROM government_builder_ranks br
@@ -77,15 +77,15 @@ async function getRoleAssignmentsForBuilder(q, builderId) {
77
77
  return rows;
78
78
  }
79
79
 
80
- // syncRank(client, { builderId, rank }) — make the builder's seeded rank-role
80
+ // syncRank(client, { builderId, rank }) — make the builder's seeded rank
81
81
  // match `rank`: add the matching one, DELETE every other seeded one, leave custom
82
- // roles alone. `client` is the caller's open transaction client; pass null/undefined
82
+ // ranks 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).
84
84
  //
85
- // Idempotent — a builder whose role already matches costs two SELECTs and no write,
85
+ // Idempotent — a builder whose rank already matches costs two SELECTs and no write,
86
86
  // so calling it on every sign-in quietly repairs drift instead of accumulating it.
87
87
  // The DELETE is keyed on the plan's `remove` list, which is built only from rows the
88
- // join proved are is_system seeded roles, so a custom role can never be caught by it.
88
+ // join proved are is_system seeded ranks, so a custom rank can never be caught by it.
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).
@@ -94,8 +94,8 @@ async function syncRank(client, { builderId, rank } = {}) {
94
94
  return { target: null, added: [], removed: [] };
95
95
  }
96
96
  const q = client || pool;
97
- const seededRoles = await getSeededRankRoles(q);
98
- const currentRoles = await getRoleAssignmentsForBuilder(q, builderId);
97
+ const seededRoles = await getSeededRanks(q);
98
+ const currentRoles = await getRankAssignmentsForBuilder(q, builderId);
99
99
  const plan = planRankSync({ rank, currentRoles, seededRoles });
100
100
 
101
101
  if (plan.remove.length > 0) {
@@ -116,16 +116,16 @@ async function syncRank(client, { builderId, rank } = {}) {
116
116
  return { target: plan.target, added: [...plan.add], removed: [...plan.remove] };
117
117
  }
118
118
 
119
- // --- the R110 CRUD surface (roles / grants / assignments) --------------------
119
+ // --- the R110 CRUD surface (ranks / grants / assignments) --------------------
120
120
  //
121
- // The db half of the governance API routes. Deliberately DUMB writes: the
122
- // authority invariants (system perms non-grantable, seeded rank-roles immutable
123
- // through the API) are enforced in routes/governance.js against the catalog —
121
+ // The db half of the government API routes. Deliberately DUMB writes: the
122
+ // authority invariants (system perms non-grantable, seeded ranks immutable
123
+ // through the API) are enforced in routes/government.js against the catalog —
124
124
  // the same split governance_001 documents (permission_key is validated app-side,
125
125
  // not a FK). Grant/assign are ON CONFLICT DO NOTHING so the tab's toggle is
126
126
  // idempotent; each returns whether a row actually changed.
127
127
 
128
- async function listRolesWithGrants() {
128
+ async function listRanksWithGrants() {
129
129
  const { rows } = await pool.query(
130
130
  `SELECT r.rank_key, r.label, r.description, r.is_system, r.seeded_rank,
131
131
  r.created_by, r.created_at,
@@ -139,62 +139,62 @@ async function listRolesWithGrants() {
139
139
  return rows;
140
140
  }
141
141
 
142
- async function getRoleByKey(roleKey) {
142
+ async function getRankByKey(rankKey) {
143
143
  const { rows } = await pool.query(
144
144
  `SELECT rank_key, label, description, is_system, seeded_rank, created_by, created_at
145
145
  FROM government_ranks
146
146
  WHERE rank_key = $1`,
147
- [roleKey],
147
+ [rankKey],
148
148
  );
149
149
  return rows[0] ?? null;
150
150
  }
151
151
 
152
- async function createRole({ roleKey, label, description, createdBy }) {
152
+ async function createRank({ rankKey, label, description, createdBy }) {
153
153
  const { rows } = await pool.query(
154
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
156
  RETURNING rank_key, label, description, is_system, seeded_rank, created_by, created_at`,
157
- [roleKey, label, description ?? null, createdBy],
157
+ [rankKey, label, description ?? null, createdBy],
158
158
  );
159
159
  return rows[0];
160
160
  }
161
161
 
162
- async function grantRolePermission({ roleKey, permissionKey }) {
162
+ async function grantRankPermission({ rankKey, permissionKey }) {
163
163
  const { rowCount } = await pool.query(
164
164
  `INSERT INTO government_rank_permissions (rank_key, permission_key)
165
165
  VALUES ($1, $2)
166
166
  ON CONFLICT (rank_key, permission_key) DO NOTHING`,
167
- [roleKey, permissionKey],
167
+ [rankKey, permissionKey],
168
168
  );
169
169
  return rowCount > 0;
170
170
  }
171
171
 
172
- async function revokeRolePermission({ roleKey, permissionKey }) {
172
+ async function revokeRankPermission({ rankKey, permissionKey }) {
173
173
  const { rowCount } = await pool.query(
174
174
  `DELETE FROM government_rank_permissions
175
175
  WHERE rank_key = $1 AND permission_key = $2`,
176
- [roleKey, permissionKey],
176
+ [rankKey, permissionKey],
177
177
  );
178
178
  return rowCount > 0;
179
179
  }
180
180
 
181
- // Replace a role's entire grant set in ONE transaction (ADR 0172's reset). The
182
- // delete+insert pair must be atomic: a crash between them would leave the role
183
- // holding NOTHING, and for a rank-role that is every builder at that rank losing
181
+ // Replace a rank's entire grant set in ONE transaction (ADR 0172's reset). The
182
+ // delete+insert pair must be atomic: a crash between them would leave the rank
183
+ // holding NOTHING, and for a rank that is every builder at that rank losing
184
184
  // every permission until someone noticed. The caller supplies the keys (the route
185
185
  // derives them from catalog.seedForRank), so this stays a dumb write like its
186
186
  // neighbours — the authority decision is made against the catalog, in the route.
187
- async function resetRoleGrants({ roleKey, permissionKeys }) {
187
+ async function resetRankGrants({ rankKey, permissionKeys }) {
188
188
  const client = await pool.connect();
189
189
  try {
190
190
  await client.query('BEGIN');
191
- await client.query(`DELETE FROM government_rank_permissions WHERE rank_key = $1`, [roleKey]);
191
+ await client.query(`DELETE FROM government_rank_permissions WHERE rank_key = $1`, [rankKey]);
192
192
  if (permissionKeys.length > 0) {
193
193
  await client.query(
194
194
  `INSERT INTO government_rank_permissions (rank_key, permission_key)
195
195
  SELECT $1, unnest($2::text[])
196
196
  ON CONFLICT (rank_key, permission_key) DO NOTHING`,
197
- [roleKey, [...permissionKeys]],
197
+ [rankKey, [...permissionKeys]],
198
198
  );
199
199
  }
200
200
  await client.query('COMMIT');
@@ -210,7 +210,7 @@ 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.rank_key, r.label AS role_label, r.is_system, r.seeded_rank,
213
+ br.rank_key, r.label AS rank_label, r.is_system, r.seeded_rank,
214
214
  br.assigned_by, br.assigned_at
215
215
  FROM government_builder_ranks br
216
216
  JOIN builders b ON b.id = br.builder_id
@@ -220,38 +220,38 @@ async function listAssignments() {
220
220
  return rows;
221
221
  }
222
222
 
223
- async function assignRole({ builderId, roleKey, assignedBy }) {
223
+ async function assignRank({ builderId, rankKey, assignedBy }) {
224
224
  const { rowCount } = await pool.query(
225
225
  `INSERT INTO government_builder_ranks (builder_id, rank_key, assigned_by)
226
226
  VALUES ($1, $2, $3)
227
227
  ON CONFLICT (builder_id, rank_key) DO NOTHING`,
228
- [builderId, roleKey, assignedBy],
228
+ [builderId, rankKey, assignedBy],
229
229
  );
230
230
  return rowCount > 0;
231
231
  }
232
232
 
233
- async function unassignRole({ builderId, roleKey }) {
233
+ async function unassignRank({ builderId, rankKey }) {
234
234
  const { rowCount } = await pool.query(
235
235
  `DELETE FROM government_builder_ranks
236
236
  WHERE builder_id = $1 AND rank_key = $2`,
237
- [builderId, roleKey],
237
+ [builderId, rankKey],
238
238
  );
239
239
  return rowCount > 0;
240
240
  }
241
241
 
242
242
  module.exports = {
243
243
  getPermissionKeysForBuilder,
244
- getRoleKeysForBuilder,
245
- getSeededRankRoles,
246
- getRoleAssignmentsForBuilder,
244
+ getRankKeysForBuilder,
245
+ getSeededRanks,
246
+ getRankAssignmentsForBuilder,
247
247
  syncRank,
248
- listRolesWithGrants,
249
- getRoleByKey,
250
- createRole,
251
- grantRolePermission,
252
- revokeRolePermission,
253
- resetRoleGrants,
248
+ listRanksWithGrants,
249
+ getRankByKey,
250
+ createRank,
251
+ grantRankPermission,
252
+ revokeRankPermission,
253
+ resetRankGrants,
254
254
  listAssignments,
255
- assignRole,
256
- unassignRole,
255
+ assignRank,
256
+ unassignRank,
257
257
  };
@@ -1,14 +1,14 @@
1
1
  'use strict';
2
2
 
3
- // modules/governance/protected-surfaces.js — the protected-surface registry's
4
- // GOVERNANCE-side API (ADR 0151 §4, BV1.R101).
3
+ // modules/government/protected-surfaces.js — the protected-surface registry's
4
+ // GOVERNMENT-side API (ADR 0151 §4, BV1.R101).
5
5
  //
6
6
  // POLICY OWNS / SUBSTRATE ENFORCES — the whole point of this slice.
7
7
  //
8
- // GOVERNANCE OWNS THE DATA. protected-surfaces.json (next to this file) says
8
+ // GOVERNMENT OWNS THE DATA. protected-surfaces.json (next to this file) says
9
9
  // WHICH repo paths are protected, at WHAT rank floor, mapped to WHICH
10
10
  // `system: true` catalog permission. That file is the source of truth; the
11
- // Archon governance tab (R110/R111) edits it as policy. Before R101 the same
11
+ // Archon Government tab (R110/R111) edits it as policy. Before R101 the same
12
12
  // information was a hardcoded PROTECTED_GLOBS constant inside the kernel.
13
13
  //
14
14
  // THE OUT-OF-BAND SUBSTRATE STAYS THE ENFORCER. Nothing here enforces
@@ -20,7 +20,7 @@
20
20
  // Moving enforcement into the app is the regression R101 exists to prevent
21
21
  // (ADR 0151 §"Alternatives considered").
22
22
  //
23
- // NO DB DEPENDENCY. A DB-backed override is a governance-tab concern layered
23
+ // NO DB DEPENDENCY. A DB-backed override is a government-tab concern layered
24
24
  // on top and must never become a dependency of the hook — the hook runs on a
25
25
  // laptop with no database and no network. This module is therefore
26
26
  // synchronous, DB-free and requires nothing but the catalog and the doorway.
@@ -43,10 +43,10 @@ const ppc = api.permissionPathCheck;
43
43
  const SURFACES = ppc.PROTECTED_SURFACES;
44
44
 
45
45
  // ── invariants asserted at module load (fail loud on an authoring typo) ───────
46
- // The governance module is where the registry meets the permission catalog, so
46
+ // The government module is where the registry meets the permission catalog, so
47
47
  // this is where "every protected surface maps to a real, non-grantable
48
48
  // permission" is checked. A surface mapping to an unknown key, or to a
49
- // `system: false` (grantable) key, would mean an Archon could hand a custom role
49
+ // `system: false` (grantable) key, would mean an Archon could hand a custom rank
50
50
  // the power the substrate is supposed to reserve — exactly the wall ADR 0151 §4
51
51
  // builds. Fail loud rather than silently protecting nothing.
52
52
  for (const s of SURFACES) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_readme": [
3
- "The PROTECTED-SURFACE REGISTRY (ADR 0151 §4, BV1.R101). GOVERNANCE OWNS THIS DATA:",
3
+ "The PROTECTED-SURFACE REGISTRY (ADR 0151 §4, BV1.R101). GOVERNMENT OWNS THIS DATA:",
4
4
  "which repo paths are protected, at what rank floor, mapped to which system:true catalog",
5
5
  "permission. It replaces the PROTECTED_GLOBS constant that used to be hardcoded in",
6
6
  "src/bongos/permission-path-check.js.",
@@ -8,14 +8,14 @@
8
8
  "POLICY OWNS / SUBSTRATE ENFORCES. Editing this file re-maps POLICY. It does NOT move",
9
9
  "enforcement into the app: the pre-push git hook, the grader pre-pass, main-audit.js,",
10
10
  "gate-review.js and the goal-scope check all read this registry OUT OF BAND (no DB, no",
11
- "server, no HTTP) and remain the enforcers (ADR 0043). A governance-tab edit can widen or",
11
+ "server, no HTTP) and remain the enforcers (ADR 0043). A government-tab edit can widen or",
12
12
  "narrow the protected set and re-map a floor; it can never make the request path the",
13
13
  "authority.",
14
14
  "",
15
15
  "WHY JSON IN THE REPO AND NOT A DB TABLE: the enforcers run where there is no database and",
16
16
  "no server (a git pre-push hook on a laptop, a CI job). The registry's canonical form must",
17
17
  "therefore be synchronously loadable from the checkout. A DB-backed override is a",
18
- "governance-tab concern layered on top — never a dependency of the hook.",
18
+ "government-tab concern layered on top — never a dependency of the hook.",
19
19
  "",
20
20
  "SCHEMA: floor/permission at the top level are the defaults; a surface may override either.",
21
21
  "'glob' follows the permission-path-check matcher: an exact path, a trailing '/' subtree,",
@@ -34,7 +34,7 @@
34
34
  { "glob": "modules/lifecycle/routes/", "group": "authz", "why": "BV1.R86: the lifecycle routes carved out of src/bongos/routes/ — the gates follow the routes" },
35
35
  { "glob": "src/bongos/routes.js", "group": "authz", "why": "the route MOUNT table (a new ungated route lands here)" },
36
36
  { "glob": "server.js", "group": "authz", "why": "the Express entrypoint — wires every router + host routing" },
37
- { "glob": "modules/government/", "group": "authz", "why": "the same authz core under its post-ADR-0174 name (goal 1000068, task 1003003). Listed BEFORE the folder moves, deliberately: `protected` is DERIVED from this registry, so a module key whose globs are absent here computes protected:false — which would silently drop the Archon gate on goal creation AND the pre-push block on these files for the whole window between the key landing and the folder arriving. The prospective-carve precedent is the modules/governance/ entry directly above (ADR 0086 §4)." },
37
+ { "glob": "modules/government/", "group": "authz", "why": "the same authz core under its post-ADR-0174 name (goal 1000068, task 1003003). Listed BEFORE the folder moves, deliberately: `protected` is DERIVED from this registry, so a module key whose globs are absent here computes protected:false — which would silently drop the Archon gate on goal creation AND the pre-push block on these files for the whole window between the key landing and the folder arriving. The prospective-carve precedent is the modules/government/ entry directly above (ADR 0086 §4)." },
38
38
  { "glob": "modules/dev-box/boxes.js", "group": "box-access", "why": "boxScopeForRank / provision floor — decides who gets a full source clone" },
39
39
  { "glob": "modules/dev-box/box-access.js", "group": "box-access", "why": "decideSourceAccess (rank → starter|full source)" },
40
40
  { "glob": "modules/dev-box/box-credential.js", "group": "box-access", "why": "mints the box source credential" },
@@ -1,40 +1,40 @@
1
1
  'use strict';
2
2
 
3
- // modules/governance/ranks.js — the PURE rank→role rotation decision
3
+ // modules/government/ranks.js — the PURE rank→rank rotation decision
4
4
  // (ADR 0151, BV1.R95b).
5
5
  //
6
- // A builder holds exactly ONE seeded rank-role. governance_002 seeds four of them
6
+ // A builder holds exactly ONE seeded rank. governance_002 seeds four of them
7
7
  // (is_system=true, seeded_rank set) and backfills each builder the one matching
8
8
  // their rank — but that backfill is run-once, so nothing kept it true afterwards.
9
9
  // This is the decision the writer (db.syncRank) executes to keep it true:
10
- // given a builder's CURRENT assignments and a rank, which seeded role to ADD and
10
+ // given a builder's CURRENT assignments and a rank, which seeded rank to ADD and
11
11
  // which to REMOVE.
12
12
  //
13
13
  // THE REMOVAL IS THE SECURITY HALF. An additive-only sync would leave a demoted
14
- // Archon still holding the `archon` role, so resolveBuilderPermissions() would keep
14
+ // Archon still holding the `archon` rank, so resolveBuilderPermissions() would keep
15
15
  // returning Archon permissions. That is the exact privilege retention ADR 0016's
16
16
  // "a demotion takes effect on the builder's very next request" promise forbids: the
17
17
  // resolver is correctly per-request and uncached, but it reads an ASSIGNMENT, and a
18
18
  // stale assignment breaks the freshness guarantee one layer beneath the resolver.
19
19
  //
20
- // CUSTOM ROLES ARE NEVER TOUCHED. An Archon-assigned role (is_system=false, the
20
+ // CUSTOM RANKS ARE NEVER TOUCHED. An Archon-assigned rank (is_system=false, the
21
21
  // R110 tab's output) must survive a rank change — only rows that are BOTH
22
- // is_system AND carry a seeded_rank rotate. Keying the removal on "every role this
22
+ // is_system AND carry a seeded_rank rotate. Keying the removal on "every rank this
23
23
  // builder holds" would silently revoke delegated authority on every promotion.
24
24
  //
25
- // A rank with NO seeded role — one of the twelve reserved ranks migration 023's
26
- // CHECK still admits — yields target=null: every seeded role is removed and none is
25
+ // A rank with NO seeded rank — one of the twelve reserved ranks migration 023's
26
+ // CHECK still admits — yields target=null: every seeded rank is removed and none is
27
27
  // added, so the builder resolves to ZERO permissions. That is governance_002's
28
28
  // deliberate fail-closed divergence (ADR 0151), preserved rather than re-derived —
29
- // never invent a role for a rank the seed does not define.
29
+ // never invent a rank for a rank the seed does not define.
30
30
  //
31
31
  // Pure: no DB, no I/O, no module state. The writer reads the two row sets and
32
32
  // executes this verdict, so the unit tests exercise production logic directly.
33
33
 
34
- // A row (an assignment joined to government_ranks, or a role row) is a SEEDED
35
- // rank-role only when it is both a system role and names the rank it reproduces.
34
+ // A row (an assignment joined to government_ranks, or a rank row) is a SEEDED
35
+ // rank only when it is both a system rank and names the rank it reproduces.
36
36
  // Both halves are required: is_system alone would admit a future non-rank system
37
- // role, and seeded_rank alone would trust a column a custom role could carry.
37
+ // rank, and seeded_rank alone would trust a column a custom rank could carry.
38
38
  function isSeededRank(row) {
39
39
  return !!row
40
40
  && row.is_system === true
@@ -46,15 +46,15 @@ function isSeededRank(row) {
46
46
  //
47
47
  // rank the builder's rank AFTER the write being synced.
48
48
  // currentRanks the builder's assignments, each { rank_key, is_system, seeded_rank }.
49
- // seededRanks the seeded rank-roles the DB defines, each { rank_key, seeded_rank,
50
- // is_system }. Passed in rather than hardcoded so the four role keys
49
+ // seededRanks the seeded ranks the DB defines, each { rank_key, seeded_rank,
50
+ // is_system }. Passed in rather than hardcoded so the four rank keys
51
51
  // stay DB truth — this file never re-declares the seed.
52
52
  //
53
53
  // target the seeded rank_key the builder should end up with, or null when the
54
- // rank has no seeded role (fail-closed divergence, above).
54
+ // rank has no seeded rank (fail-closed divergence, above).
55
55
  // add [] or [target] — empty when it is already held (idempotence).
56
- // remove every OTHER seeded rank-role currently held. Custom roles never appear.
57
- // next the resulting rank_key set, sorted — "what the role set becomes".
56
+ // remove every OTHER seeded rank currently held. Custom ranks never appear.
57
+ // next the resulting rank_key set, sorted — "what the rank set becomes".
58
58
  function planRankSync({ rank, currentRanks = [], seededRanks = [] } = {}) {
59
59
  const match = seededRanks.find((r) => isSeededRank(r) && r.seeded_rank === rank);
60
60
  const target = match ? match.rank_key : null;