@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,29 +1,29 @@
1
1
  'use strict';
2
2
 
3
- // modules/governance/resolver.js — the permission resolver (ADR 0151, BV1.R95).
3
+ // modules/government/resolver.js — the permission resolver (ADR 0151, BV1.R95).
4
4
  //
5
- // Turns a builder's ROLES into their effective PERMISSION SET: the union of every
6
- // permission granted to every role they hold. This is the heart of the
5
+ // Turns a builder's RANKS into their effective PERMISSION SET: the union of every
6
+ // permission granted to every rank they hold. This is the heart of the
7
7
  // permissions-as-atom model — `requirePermission()` (R97) and every migrated gate
8
8
  // (R104-R107) resolve authority through here.
9
9
  //
10
- // TWO AXES, two entry points (R98, ADR 0151 §3). Roles are one axis; a machine
10
+ // TWO AXES, two entry points (R98, ADR 0151 §3). Ranks are one axis; a machine
11
11
  // PRINCIPAL's capability is the other, resolved from its exact `system_role` and
12
- // never from a role. They are kept apart on purpose — see resolvePrincipalPermissions
12
+ // never from a rank. They are kept apart on purpose — see resolvePrincipalPermissions
13
13
  // below, and the principal keys effectivePermissions drops.
14
14
  //
15
15
  // It preserves ADR 0016 exactly:
16
16
  // • server-side — the answer is computed on the server from DB rows, never from
17
17
  // a token claim or anything the client can influence;
18
18
  // • per request — resolveBuilderPermissions() is called on the request that
19
- // needs it, so a revoked role takes effect on the builder's very next request;
19
+ // needs it, so a revoked rank takes effect on the builder's very next request;
20
20
  // • UNCACHED — there is no memo, no TTL, no process-local map. Caching authority
21
21
  // is the one optimisation this file must never take (a cached grant survives
22
22
  // its revocation, which is the failure mode the trust boundary exists to
23
23
  // prevent). If a future profile shows the join is hot, the answer is a DB
24
24
  // index, not a cache.
25
25
  //
26
- // Fail-closed throughout: a builder with no roles resolves to the EMPTY set, and a
26
+ // Fail-closed throughout: a builder with no ranks resolves to the EMPTY set, and a
27
27
  // grant naming a key the code catalog doesn't define is DROPPED (see
28
28
  // effectivePermissions below) rather than admitted on trust.
29
29
 
@@ -31,18 +31,18 @@ const catalog = require('./catalog');
31
31
  const db = require('./db');
32
32
  const sessionScopes = require('./session-scopes');
33
33
 
34
- // Pure core of the ROLE axis, split out so it is unit-testable without a database.
34
+ // Pure core of the RANK axis, split out so it is unit-testable without a database.
35
35
  //
36
36
  // Filters raw grant rows against the code catalog: the catalog is the source of
37
37
  // truth for what a permission IS (every key maps to a real route — modules/
38
- // governance/CLAUDE.md), so a DB row naming an unknown key is stale or hand-
38
+ // government/CLAUDE.md), so a DB row naming an unknown key is stale or hand-
39
39
  // inserted and grants NOTHING. Returns a frozen, sorted, de-duplicated array.
40
40
  //
41
41
  // It also DROPS every principal key (R98). A machine identity's capability is the
42
- // other axis — it comes from `builders.system_role`, never from a role — so a grant
42
+ // other axis — it comes from `builders.system_role`, never from a rank — so a grant
43
43
  // row naming `principal.*` confers nothing here no matter how it got written (a
44
- // hand-inserted row, a future bug in the governance CRUD's system-permission
45
- // rejection). That makes "a role cannot confer a principal permission" structural
44
+ // hand-inserted row, a future bug in the government CRUD's system-permission
45
+ // rejection). That makes "a rank cannot confer a principal permission" structural
46
46
  // rather than merely policed at the write side.
47
47
  function effectivePermissions(rawKeys) {
48
48
  const known = new Set();
@@ -55,8 +55,8 @@ function effectivePermissions(rawKeys) {
55
55
  }
56
56
 
57
57
  // The PRINCIPAL axis (R98, ADR 0151 §3) — deliberately a SEPARATE entry point, not
58
- // a branch inside the role resolve. A builder's authority is the union of their
59
- // roles; a machine principal's authority is its identity, and collapsing the two
58
+ // a branch inside the rank resolve. A builder's authority is the union of their
59
+ // ranks; a machine principal's authority is its identity, and collapsing the two
60
60
  // into one lookup is exactly the mistake that would let a rank reach a principal
61
61
  // capability.
62
62
  //
@@ -64,7 +64,7 @@ function effectivePermissions(rawKeys) {
64
64
  // already reads fresh from the DB per request (alongside rank), and which no API
65
65
  // route writes — a principal is provisioned out-of-band by migration, under a DB
66
66
  // CHECK allowlist. The match is EXACT string equality, so NULL (every human,
67
- // including an Archon), a differently-cased value, or an unknown role resolves to
67
+ // including an Archon), a differently-cased value, or an unknown rank resolves to
68
68
  // the empty set. Mirrors requireBfgPrincipal / requireDiscordBot in src/bongos/
69
69
  // auth.js, which stay the HTTP enforcement point.
70
70
  function resolvePrincipalPermissions(systemRole) {
@@ -111,7 +111,7 @@ async function builderHasPermissions(builderId, required) {
111
111
  // two different places, and folding it into the plain resolve would silently drop
112
112
  // the second one.
113
113
  //
114
- // 1. the ROLE fact — does the builder hold the key? (this file, from the DB)
114
+ // 1. the RANK fact — does the builder hold the key? (this file, from the DB)
115
115
  // 2. the OWNER fact — is the concrete row theirs? (REQUEST-scoped; only the
116
116
  // handler that loaded the row knows it)
117
117
  //
@@ -122,7 +122,7 @@ async function builderHasPermissions(builderId, required) {
122
122
  // path here that turns an ownership-scoped key into an admit on its own.
123
123
  //
124
124
  // Pure + DB-free where it can be: the comparison below never touches Postgres, and
125
- // the role fact reuses resolveBuilderPermissions, so ADR 0016 holds unchanged —
125
+ // the rank fact reuses resolveBuilderPermissions, so ADR 0016 holds unchanged —
126
126
  // per-request, server-side, no stored answer.
127
127
 
128
128
  // The owner comparison, normalized once so ~15 sites stop hand-rolling it. STRICT:
@@ -169,27 +169,27 @@ async function authorizeOwnedOrAny(builderId, ownKey, context) {
169
169
 
170
170
  // ── the SESSION-SCOPE axis (R99, ADR 0151 §3) ───────────────────────────────────
171
171
  // The FOURTH entry point, separate for the same reason the other three are: it
172
- // answers a different question with a different input. Roles answer "what may this
172
+ // answers a different question with a different input. Ranks answer "what may this
173
173
  // PERSON do"; a principal answers "what may this MACHINE do"; an ownership scope
174
174
  // answers "whose row may it be done to"; a session-scope answers "how much of that
175
175
  // does THIS CREDENTIAL carry". A dev-box token is minted onto a machine outside the
176
176
  // ADR 0016 trust boundary (#919/SR#2, ADR 0053), so it must stay limited even when
177
- // its builder is an Archon — which no role-shaped answer can express.
177
+ // its builder is an Archon — which no rank-shaped answer can express.
178
178
  //
179
179
  // A SESSION-SCOPE CAN ONLY EVER NARROW, in both directions:
180
- // • INTERSECTION, never union. The scope may not GRANT a permission the roles do
181
- // not confer — so the composition below is written as a FILTER of the ROLE
180
+ // • INTERSECTION, never union. The scope may not GRANT a permission the ranks do
181
+ // not confer — so the composition below is written as a FILTER of the RANK
182
182
  // answer. There is no branch here that can add a key, which is what makes
183
183
  // "a box token cannot escalate" structural rather than merely intended.
184
184
  // • DENY-BY-DEFAULT. A key the allow-list does not name is denied; an
185
185
  // unrecognised session source resolves to NOTHING rather than to "full".
186
186
  // Widening is therefore not expressible without deleting the filter, and R103's
187
- // parity is untouched: effectivePermissions is the unchanged ROLE answer this
187
+ // parity is untouched: effectivePermissions is the unchanged RANK answer this
188
188
  // composes ON TOP of, never inside.
189
189
  //
190
190
  // Pure and DB-free (the DB half is resolveBuilderPermissions'), so an unknown key
191
191
  // on an allow-list is inert as well as refused at load: it can never appear in the
192
- // catalog-filtered role answer, so it can never survive the intersection.
192
+ // catalog-filtered rank answer, so it can never survive the intersection.
193
193
  function narrowToSessionScope(rawKeys, source) {
194
194
  const scope = sessionScopes.scopeFor(source);
195
195
  if (!scope) return Object.freeze([]); // unrecognised source → no authority at all
@@ -199,7 +199,7 @@ function narrowToSessionScope(rawKeys, source) {
199
199
  return Object.freeze(held.filter((key) => allowed.has(key)));
200
200
  }
201
201
 
202
- // What can THIS SESSION actually do? The builder's roles ∩ this session's scope,
202
+ // What can THIS SESSION actually do? The builder's ranks ∩ this session's scope,
203
203
  // as a frozen sorted array of catalog keys. Fails closed BEFORE touching the DB on
204
204
  // a source the registry does not declare.
205
205
  async function resolveSessionPermissions(builderId, source) {
@@ -1,12 +1,12 @@
1
1
  'use strict';
2
2
 
3
- // The governance module's kernel PORT registration (ADR 0151, BV1.R95).
3
+ // The government module's kernel PORT registration (ADR 0151, BV1.R95).
4
4
  //
5
- // governance exposes the permission RESOLVER as the `governance` kernel PORT.
5
+ // government exposes the permission RESOLVER as the `government` kernel PORT.
6
6
  // CORE files that must NOT import a module resolve it instead — first among them
7
7
  // src/bongos/auth.js, whose `requirePermission()` gate (R97) needs a builder's
8
8
  // effective permission set on every gated request. The one-way rule (ADR 0083)
9
- // forbids core requiring modules/governance/ directly; the port is how the
9
+ // forbids core requiring modules/government/ directly; the port is how the
10
10
  // capability crosses that line.
11
11
  //
12
12
  // Why a route factory for a (currently) routeless capability: `mountModuleRoutes`
@@ -14,7 +14,7 @@
14
14
  // rebuilds the GDS router, so registering here keeps the port present for the
15
15
  // route-building tests too. Registration is hasProvider-guarded so
16
16
  // re-instantiation never trips the seam's single-provider duplicate guard.
17
- // (Mirrors modules/economy/routes/reward.js.) The R110 governance CRUD
17
+ // (Mirrors modules/economy/routes/reward.js.) The R110 government CRUD
18
18
  // endpoints live on this same router, below.
19
19
 
20
20
  const express = require('express');
@@ -27,7 +27,7 @@ const sessionScopes = require('../session-scopes');
27
27
  // The port surface. Deliberately narrow: resolution + the catalog reads a gate or
28
28
  // the Archon tab needs. It grows with R98-R101 (principals, session-scopes,
29
29
  // ownership scoping, the protected-surface registry) — one object port rather
30
- // than a port per capability, so core resolves `governance` once.
30
+ // than a port per capability, so core resolves `government` once.
31
31
  //
32
32
  // The principal half (R98) is READ-ONLY by construction: resolution keyed on an
33
33
  // exact system_role, plus `listPrincipals` for the tab to render. There is no
@@ -47,10 +47,10 @@ const sessionScopes = require('../session-scopes');
47
47
  // The session-scope half (R99) is READ-ONLY and deliberately NOT the enforcement
48
48
  // path: the kernel gate (auth.requireBuilder) reads the same registry as static
49
49
  // data, so a request never depends on this port being registered to stay
50
- // deny-by-default. What the port adds is the composed answer — roles ∩ this
51
- // session's scope — for the governance tab and for any gate that wants the
50
+ // deny-by-default. What the port adds is the composed answer — ranks ∩ this
51
+ // session's scope — for the Government tab and for any gate that wants the
52
52
  // session's real authority rather than the builder's.
53
- const governancePort = {
53
+ const governmentPort = {
54
54
  resolveBuilderPermissions: resolver.resolveBuilderPermissions,
55
55
  // The pure catalog-drop half, for the session-load fold (task 1002556): the
56
56
  // kernel feeds it the raw grant keys its own session query carried, so a
@@ -58,7 +58,7 @@ const governancePort = {
58
58
  // resolveBuilderPermissions given the same rows — DB-free by construction.
59
59
  effectivePermissions: resolver.effectivePermissions,
60
60
  builderHasPermissions: resolver.builderHasPermissions,
61
- getRoleKeysForBuilder: db.getRoleKeysForBuilder,
61
+ getRankKeysForBuilder: db.getRankKeysForBuilder,
62
62
  isSystem: catalog.isSystem,
63
63
  isGrantable: catalog.isGrantable,
64
64
  permissionKeys: catalog.permissionKeys,
@@ -82,13 +82,13 @@ const governancePort = {
82
82
 
83
83
  function registerSeams() {
84
84
  if (!api.hasProvider('government')) {
85
- api.registerProvider('government', governancePort);
85
+ api.registerProvider('government', governmentPort);
86
86
  }
87
87
  // The session-load fold (task 1002556): the kernel's lookupSession embeds this
88
88
  // fragment so the SAME round-trip carries the builder's raw grant keys, and
89
89
  // requirePermission runs the pure effectivePermissions instead of a second
90
90
  // query. Registered HERE — capability-named, module-agnostic on the kernel
91
- // side (ADR 0083 §Decision #4) — because this module owns the governance SQL.
91
+ // side (ADR 0083 §Decision #4) — because this module owns the government SQL.
92
92
  // Contract: a boot-time CONSTANT (never user input) yielding one
93
93
  // `raw_permission_keys` column against the kernel query's alias `s`
94
94
  // (builder_sessions). Raw keys only — the catalog-drop stays in the resolver.
@@ -103,38 +103,38 @@ function registerSeams() {
103
103
  }
104
104
  }
105
105
 
106
- // ── the R110 CRUD surface (ADR 0151 §2 — the governance tab's server half) ────
106
+ // ── the R110 CRUD surface (ADR 0151 §2 — the Government tab's server half) ────
107
107
  //
108
108
  // Every route — the three reads included — gates on the `government.manage`
109
- // atom (system, floor archon): this surface EDITS the role/permission graph
109
+ // atom (system, floor archon): this surface EDITS the rank/permission graph
110
110
  // itself, the meta-escalation the catalog flags non-grantable, and even listing
111
111
  // it exposes the full authority map. The invariants are enforced HERE, against
112
112
  // the catalog, not in the db layer (governance_001's app-side-validation split).
113
113
  //
114
114
  // ADR 0172 (owner decision, 2026-08-16) split what used to be ONE wall — "a seeded
115
- // rank-role is immutable" — into two, because grants and assignment are immutable
115
+ // rank is immutable" — into two, because grants and assignment are immutable
116
116
  // for different reasons and only one of those reasons still holds:
117
117
  //
118
- // • GRANTS on the three sub-Archon rank-roles are now EDITABLE. Their rows were
118
+ // • GRANTS on the three sub-Archon ranks are now EDITABLE. Their rows were
119
119
  // always ordinary `government_rank_permissions` rows (the governance_002 seed
120
120
  // writes them); the old refusal was policy, not structure. What a rank means is
121
121
  // now the owner's to set from the tab, and `catalog.isGrantableTo()` answers
122
- // which keys per role. The ARCHON rank-role stays immutable — it holds
122
+ // which keys per rank. The ARCHON rank stays immutable — it holds
123
123
  // everything, and its immutability is what makes lockout impossible.
124
- // • ASSIGNMENT of every seeded rank-role is still refused, unchanged: it derives
124
+ // • ASSIGNMENT of every seeded rank is still refused, unchanged: it derives
125
125
  // from `builders.rank` via the R95b sync, so a second writer here would fork
126
126
  // authority truth and the sync would silently undo it on the next rank write.
127
- // Changing who holds a rank-role is still `PATCH /builders/:id/rank`.
127
+ // Changing who holds a rank is still `PATCH /builders/:id/rank`.
128
128
  //
129
- // The custom-role wall is unchanged: a `system:true` key stays non-grantable there
130
- // (R94's wall — "The governance API (R110) rejects granting it"), and the three
131
- // SUBSTRATE keys stay non-grantable to everything, rank-roles included, because a
129
+ // The custom-rank wall is unchanged: a `system:true` key stays non-grantable there
130
+ // (R94's wall — "The government API (R110) rejects granting it"), and the three
131
+ // SUBSTRATE keys stay non-grantable to everything, ranks included, because a
132
132
  // grant row for one delegates nothing (ADR 0043 enforces them out of band).
133
133
  const { validateOrRespond, LIMITS, parseId } = api;
134
134
  const RANK_KEY_RE = /^[a-z][a-z0-9-]*$/;
135
135
  const RANK_KEY_MAX = 64;
136
136
 
137
- module.exports = function buildGovernanceRouter() {
137
+ module.exports = function buildGovernmentRouter() {
138
138
  registerSeams();
139
139
  const router = express.Router();
140
140
 
@@ -150,40 +150,40 @@ module.exports = function buildGovernanceRouter() {
150
150
  return rankKey;
151
151
  }
152
152
 
153
- // Load a role whose GRANTS this API may edit (ADR 0172): 404 on a missing role,
154
- // refuse only the Archon rank-role. Every other role — the three sub-Archon
155
- // rank-roles and every custom one — is grant-editable; WHICH keys is then
156
- // `catalog.isGrantableTo(key, role)`, per role. Returns null after responding.
157
- async function loadGrantEditableRole(res, rankKey) {
158
- const role = await db.getRoleByKey(rankKey);
159
- if (!role) {
153
+ // Load a rank whose GRANTS this API may edit (ADR 0172): 404 on a missing rank,
154
+ // refuse only the Archon rank. Every other rank — the three sub-Archon
155
+ // ranks and every custom one — is grant-editable; WHICH keys is then
156
+ // `catalog.isGrantableTo(key, rank)`, per rank. Returns null after responding.
157
+ async function loadGrantEditableRank(res, rankKey) {
158
+ const rank = await db.getRankByKey(rankKey);
159
+ if (!rank) {
160
160
  res.fail('rank_not_found', 404, { rank_key: rankKey });
161
161
  return null;
162
162
  }
163
- if (role.is_system && role.seeded_rank === ARCHON) {
163
+ if (rank.is_system && rank.seeded_rank === ARCHON) {
164
164
  res.fail('archon_rank_immutable', 403, { rank_key: rankKey, reason: ARCHON_REASON });
165
165
  return null;
166
166
  }
167
- return role;
167
+ return rank;
168
168
  }
169
169
 
170
- // Load a role this API may ASSIGN. Unchanged from R110: every seeded rank-role is
170
+ // Load a rank this API may ASSIGN. Unchanged from R110: every seeded rank is
171
171
  // refused, because assignment derives from builders.rank via the R95b sync.
172
- async function loadAssignableRole(res, rankKey) {
173
- const role = await db.getRoleByKey(rankKey);
174
- if (!role) {
172
+ async function loadAssignableRank(res, rankKey) {
173
+ const rank = await db.getRankByKey(rankKey);
174
+ if (!rank) {
175
175
  res.fail('rank_not_found', 404, { rank_key: rankKey });
176
176
  return null;
177
177
  }
178
- if (role.is_system) {
178
+ if (rank.is_system) {
179
179
  res.fail('seeded_rank_assignment_forbidden', 403, { rank_key: rankKey, reason: ASSIGN_REASON });
180
180
  return null;
181
181
  }
182
- return role;
182
+ return rank;
183
183
  }
184
184
  const ARCHON = 'archon';
185
- const ARCHON_REASON = 'the Archon rank-role holds every permission and is immutable — it is the role that can never be locked out of this tab (ADR 0172)';
186
- const ASSIGN_REASON = 'a seeded rank-role is assigned by rank (R95b sync); change the builder\'s rank instead';
185
+ const ARCHON_REASON = 'the Archon rank-rank holds every permission and is immutable — it is the rank that can never be locked out of this tab (ADR 0172)';
186
+ const ASSIGN_REASON = 'a seeded rank-rank is assigned by rank (R95b sync); change the builder\'s rank instead';
187
187
 
188
188
  // ── one-release compatibility aliases: /governance/* -> /government/* ──────
189
189
  // ADR 0174 committed to keeping the pre-rename paths answering for exactly one
@@ -205,19 +205,19 @@ module.exports = function buildGovernanceRouter() {
205
205
  router.all(`${LEGACY_PATH_PREFIX}/*rest`, (req, res, next) => {
206
206
  const rest = req.originalUrl.slice(req.originalUrl.indexOf(LEGACY_PATH_PREFIX) + LEGACY_PATH_PREFIX.length);
207
207
  if (!rest) return next();
208
- const target = `/government${rest.replace(/^\/roles(?=\/|$)/, '/ranks')}`;
208
+ const target = `/government${rest.replace(/^\/ranks(?=\/|$)/, '/ranks')}`;
209
209
  console.warn(`[government] legacy path hit: ${req.method} ${req.originalUrl} -> ${target} (ADR 0174 alias; remove one release after the rename)`);
210
210
  return res.redirect(308, target);
211
211
  });
212
212
 
213
- // rank: perm government.manage — the full role list with each role's grants.
213
+ // rank: perm government.manage — the full rank list with each rank's grants.
214
214
  router.get('/government/ranks', api.requireBuilder, api.requirePermission('government.manage'), async (req, res) => {
215
215
  try {
216
- const roles = await db.listRolesWithGrants();
217
- res.json({ roles });
216
+ const ranks = await db.listRanksWithGrants();
217
+ res.json({ ranks });
218
218
  } catch (err) {
219
219
  console.error('[gds] GET /government/ranks', err);
220
- res.fail('roles_list_failed', { status: 500, message: 'internal error' });
220
+ res.fail('ranks_list_failed', { status: 500, message: 'internal error' });
221
221
  }
222
222
  });
223
223
 
@@ -225,11 +225,11 @@ module.exports = function buildGovernanceRouter() {
225
225
  // from code (the catalog is CODE, not a table); no DB read. Principals are
226
226
  // read-only by construction. Three flags the tab renders its locks from, and the
227
227
  // tab must read all three rather than just `grantable`, because since ADR 0172
228
- // grantability depends on WHICH role is selected:
229
- // • `grantable` — the CUSTOM-role answer (system:false). Unchanged meaning.
230
- // • `system` — an in-app Archon power: locked on a custom role, togglable on
231
- // a sub-Archon rank-role.
232
- // • `substrate` — locked EVERYWHERE, on every role, forever (ADR 0043).
228
+ // grantability depends on WHICH rank is selected:
229
+ // • `grantable` — the CUSTOM-rank answer (system:false). Unchanged meaning.
230
+ // • `system` — an in-app Archon power: locked on a custom rank, togglable on
231
+ // a sub-Archon rank.
232
+ // • `substrate` — locked EVERYWHERE, on every rank, forever (ADR 0043).
233
233
  router.get('/government/permissions', api.requireBuilder, api.requirePermission('government.manage'), (req, res) => {
234
234
  const permissions = catalog.PERMISSIONS.map((p) => ({
235
235
  key: p.key,
@@ -244,7 +244,7 @@ module.exports = function buildGovernanceRouter() {
244
244
  res.json({ permissions, principals: catalog.principals() });
245
245
  });
246
246
 
247
- // rank: perm government.manage — every builder↔role assignment row.
247
+ // rank: perm government.manage — every builder↔rank assignment row.
248
248
  router.get('/government/assignments', api.requireBuilder, api.requirePermission('government.manage'), async (req, res) => {
249
249
  try {
250
250
  const assignments = await db.listAssignments();
@@ -255,7 +255,7 @@ module.exports = function buildGovernanceRouter() {
255
255
  }
256
256
  });
257
257
 
258
- // rank: perm government.manage — create a CUSTOM role (is_system false, no
258
+ // rank: perm government.manage — create a CUSTOM rank (is_system false, no
259
259
  // seeded_rank — the seeded four exist only via governance_002). Grants are
260
260
  // added one at a time via the toggle below, so creation carries none.
261
261
  router.post('/government/ranks', api.requireBuilder, api.requirePermission('government.manage'), async (req, res) => {
@@ -269,50 +269,50 @@ module.exports = function buildGovernanceRouter() {
269
269
  return res.fail('bad_rank_key', 400, { rank_key: rankKey, expected: 'a kebab-case slug (^[a-z][a-z0-9-]*$)' });
270
270
  }
271
271
  try {
272
- const role = await db.createRole({
272
+ const rank = await db.createRank({
273
273
  rankKey,
274
274
  label: req.body.label,
275
275
  description: req.body.description ?? null,
276
276
  createdBy: req.builder.id,
277
277
  });
278
- res.status(201).json({ role: { ...role, permissions: [] } });
278
+ res.status(201).json({ rank: { ...rank, permissions: [] } });
279
279
  } catch (err) {
280
280
  if (err && err.code === '23505') {
281
281
  return res.fail('rank_exists', 409, { rank_key: rankKey });
282
282
  }
283
283
  console.error('[gds] POST /government/ranks', err);
284
- res.fail('role_create_failed', { status: 500, message: 'internal error' });
284
+ res.fail('rank_create_failed', { status: 500, message: 'internal error' });
285
285
  }
286
286
  });
287
287
 
288
- // rank: perm government.manage — grant one permission to a role (the toggle's ON
289
- // half). The order of refusals is the authority order: role exists → role is not
290
- // the Archon one → the key exists → the key is grantable TO THIS ROLE.
288
+ // rank: perm government.manage — grant one permission to a rank (the toggle's ON
289
+ // half). The order of refusals is the authority order: rank exists → rank is not
290
+ // the Archon one → the key exists → the key is grantable TO THIS RANK.
291
291
  router.put('/government/ranks/:rankKey/permissions/:permissionKey', api.requireBuilder, api.requirePermission('government.manage'), async (req, res) => {
292
292
  const rankKey = rankKeyParam(req, res);
293
293
  if (rankKey === null) return;
294
294
  const permissionKey = req.params.permissionKey;
295
295
  try {
296
- const role = await loadGrantEditableRole(res, rankKey);
297
- if (role === null) return;
296
+ const rank = await loadGrantEditableRank(res, rankKey);
297
+ if (rank === null) return;
298
298
  if (!catalog.byKey(permissionKey)) {
299
299
  return res.fail('unknown_permission', 400, { permission_key: permissionKey });
300
300
  }
301
- if (!catalog.isGrantableTo(permissionKey, role)) {
301
+ if (!catalog.isGrantableTo(permissionKey, rank)) {
302
302
  return res.fail('permission_not_grantable', 400, {
303
303
  permission_key: permissionKey,
304
304
  rank_key: rankKey,
305
305
  // Name the ACTUAL wall. "Not grantable" now has three distinct causes and
306
306
  // a caller that cannot tell them apart cannot tell a policy choice
307
- // (widen the role) from a structural one (nothing will ever work).
307
+ // (widen the rank) from a structural one (nothing will ever work).
308
308
  reason: catalog.isSubstrate(permissionKey)
309
- ? 'substrate-enforced (ADR 0043) — enforced outside the app by the pre-push hook / main-audit / the rank floor, so a grant row here would confer nothing. Non-grantable to EVERY role.'
309
+ ? 'substrate-enforced (ADR 0043) — enforced outside the app by the pre-push hook / main-audit / the rank floor, so a grant row here would confer nothing. Non-grantable to EVERY rank.'
310
310
  : catalog.isPrincipalKey(permissionKey)
311
- ? 'a machine-principal permission — held by an exact builders.system_role identity, never by a role (ADR 0151 §3)'
312
- : 'a system (trust-boundary) permission is non-grantable to a CUSTOM role; only a sub-Archon rank-role may carry one (ADR 0172)',
311
+ ? 'a machine-principal permission — held by an exact builders.system_role identity, never by a rank (ADR 0151 §3)'
312
+ : 'a system (trust-boundary) permission is non-grantable to a CUSTOM rank; only a sub-Archon rank-rank may carry one (ADR 0172)',
313
313
  });
314
314
  }
315
- const granted = await db.grantRolePermission({ rankKey, permissionKey });
315
+ const granted = await db.grantRankPermission({ rankKey, permissionKey });
316
316
  res.json({ ok: true, rank_key: rankKey, permission_key: permissionKey, granted });
317
317
  } catch (err) {
318
318
  console.error('[gds] PUT /government/ranks/:rankKey/permissions/:permissionKey', err);
@@ -329,9 +329,9 @@ module.exports = function buildGovernanceRouter() {
329
329
  if (rankKey === null) return;
330
330
  const permissionKey = req.params.permissionKey;
331
331
  try {
332
- const role = await loadGrantEditableRole(res, rankKey);
333
- if (role === null) return;
334
- const removed = await db.revokeRolePermission({ rankKey, permissionKey });
332
+ const rank = await loadGrantEditableRank(res, rankKey);
333
+ if (rank === null) return;
334
+ const removed = await db.revokeRankPermission({ rankKey, permissionKey });
335
335
  res.json({ ok: true, rank_key: rankKey, permission_key: permissionKey, removed });
336
336
  } catch (err) {
337
337
  console.error('[gds] DELETE /government/ranks/:rankKey/permissions/:permissionKey', err);
@@ -339,42 +339,42 @@ module.exports = function buildGovernanceRouter() {
339
339
  }
340
340
  });
341
341
 
342
- // rank: perm government.manage — restore a SEEDED rank-role to its catalog
343
- // default (ADR 0172). The counterweight to making the rank-roles editable: a
342
+ // rank: perm government.manage — restore a SEEDED rank to its catalog
343
+ // default (ADR 0172). The counterweight to making the ranks editable: a
344
344
  // rank's grant set is ~60 rows, so an owner who drifts one has no practical way
345
345
  // back by clicking, and "what does Trusted mean by default" is a question only
346
- // the catalog can answer. Restores catalog.seedForRank(role.seeded_rank) — the
346
+ // the catalog can answer. Restores catalog.seedForRank(rank.seeded_rank) — the
347
347
  // same derivation governance_002 seeded from, so the answer cannot drift from
348
348
  // the floors even after later catalog edits.
349
349
  //
350
- // Deliberately NOT offered for a CUSTOM role: a custom role has no catalog
350
+ // Deliberately NOT offered for a CUSTOM rank: a custom rank has no catalog
351
351
  // default to return to (its "default" is the empty set it was created with), so
352
352
  // the honest verb there is revoking the grants, which the toggle already does.
353
353
  router.post('/government/ranks/:rankKey/reset', api.requireBuilder, api.requirePermission('government.manage'), async (req, res) => {
354
354
  const rankKey = rankKeyParam(req, res);
355
355
  if (rankKey === null) return;
356
356
  try {
357
- const role = await loadGrantEditableRole(res, rankKey);
358
- if (role === null) return;
359
- if (!role.is_system || !catalog.RANK_ORDER.includes(role.seeded_rank)) {
357
+ const rank = await loadGrantEditableRank(res, rankKey);
358
+ if (rank === null) return;
359
+ if (!rank.is_system || !catalog.RANK_ORDER.includes(rank.seeded_rank)) {
360
360
  return res.fail('rank_has_no_default', 400, {
361
361
  rank_key: rankKey,
362
- reason: 'only a seeded rank-role has a catalog default to reset to; revoke a custom role\'s grants instead',
362
+ reason: 'only a seeded rank-rank has a catalog default to reset to; revoke a custom rank\'s grants instead',
363
363
  });
364
364
  }
365
365
  // The seed is the floor derivation, which never contains a principal key and
366
366
  // — below archon — never a system one. Filtering by isGrantableTo anyway keeps
367
367
  // reset from being a back door that writes what the toggle refuses.
368
- const keys = catalog.seedForRank(role.seeded_rank).filter((k) => catalog.isGrantableTo(k, role));
369
- const count = await db.resetRoleGrants({ rankKey, permissionKeys: keys });
370
- res.json({ ok: true, rank_key: rankKey, seeded_rank: role.seeded_rank, permission_count: count });
368
+ const keys = catalog.seedForRank(rank.seeded_rank).filter((k) => catalog.isGrantableTo(k, rank));
369
+ const count = await db.resetRankGrants({ rankKey, permissionKeys: keys });
370
+ res.json({ ok: true, rank_key: rankKey, seeded_rank: rank.seeded_rank, permission_count: count });
371
371
  } catch (err) {
372
372
  console.error('[gds] POST /government/ranks/:rankKey/reset', err);
373
373
  res.fail('reset_failed', { status: 500, message: 'internal error' });
374
374
  }
375
375
  });
376
376
 
377
- // rank: perm government.manage — assign a CUSTOM role to a builder.
377
+ // rank: perm government.manage — assign a CUSTOM rank to a builder.
378
378
  // assigned_by is ALWAYS the session builder, never input (the audit trail's
379
379
  // "who did this" must be the authenticated actor).
380
380
  router.post('/government/assignments', api.requireBuilder, api.requirePermission('government.manage'), async (req, res) => {
@@ -387,9 +387,9 @@ module.exports = function buildGovernanceRouter() {
387
387
  return res.fail('bad_rank_key', 400, { rank_key: rankKey, expected: 'a kebab-case slug (^[a-z][a-z0-9-]*$)' });
388
388
  }
389
389
  try {
390
- const role = await loadAssignableRole(res, rankKey);
391
- if (role === null) return;
392
- const assigned = await db.assignRole({
390
+ const rank = await loadAssignableRank(res, rankKey);
391
+ if (rank === null) return;
392
+ const assigned = await db.assignRank({
393
393
  builderId: req.body.builder_id,
394
394
  rankKey,
395
395
  assignedBy: req.builder.id,
@@ -404,7 +404,7 @@ module.exports = function buildGovernanceRouter() {
404
404
  }
405
405
  });
406
406
 
407
- // rank: perm government.manage — remove a custom-role assignment. Idempotent:
407
+ // rank: perm government.manage — remove a custom-rank assignment. Idempotent:
408
408
  // `removed` says whether a row went away.
409
409
  router.delete('/government/assignments/:builderId/:rankKey', api.requireBuilder, api.requirePermission('government.manage'), async (req, res) => {
410
410
  const builderId = parseId(req, res, { param: 'builderId', code: 'bad_builder_id', positiveInt: true });
@@ -412,9 +412,9 @@ module.exports = function buildGovernanceRouter() {
412
412
  const rankKey = rankKeyParam(req, res);
413
413
  if (rankKey === null) return;
414
414
  try {
415
- const role = await loadAssignableRole(res, rankKey);
416
- if (role === null) return;
417
- const removed = await db.unassignRole({ builderId, rankKey });
415
+ const rank = await loadAssignableRank(res, rankKey);
416
+ if (rank === null) return;
417
+ const removed = await db.unassignRank({ builderId, rankKey });
418
418
  res.json({ ok: true, builder_id: builderId, rank_key: rankKey, removed });
419
419
  } catch (err) {
420
420
  console.error('[gds] DELETE /government/assignments/:builderId/:rankKey', err);
@@ -425,4 +425,4 @@ module.exports = function buildGovernanceRouter() {
425
425
  return router;
426
426
  };
427
427
 
428
- module.exports.governancePort = governancePort;
428
+ module.exports.governmentPort = governmentPort;
@@ -1,27 +1,27 @@
1
1
  'use strict';
2
2
 
3
- // The governance module's rank-role SYNC port (ADR 0151, BV1.R95b).
3
+ // The government module's rank SYNC port (ADR 0151, BV1.R95b).
4
4
  //
5
- // A second, deliberately tiny kernel port beside `governance` (routes/governance.js).
6
- // `governance` is the READ surface every gate resolves on every request; this is the
5
+ // A second, deliberately tiny kernel port beside `government` (routes/government.js).
6
+ // `government` is the READ surface every gate resolves on every request; this is the
7
7
  // single WRITE core needs — "this builder's rank is now X, make their seeded
8
- // rank-role match". They are kept apart on purpose:
8
+ // rank match". They are kept apart on purpose:
9
9
  // • the read port is on the hot path of every permission-gated request and is
10
10
  // resolved by requirePermission(); a mutation has no business hanging off it;
11
11
  // • this one is resolved by exactly three writers (the two _applyRankChangeTx
12
12
  // twins + the sign-in path) and is a TRANSACTION PARTICIPANT — it takes the
13
- // caller's client so the role rotation commits or rolls back with the rank write.
13
+ // caller's client so the rank rotation commits or rolls back with the rank write.
14
14
  // The port name is namespaced (`government.rankSync`) like the `ideas.capture` /
15
15
  // `art.getBuilderGeminiKeyRow` / `grade.builderRollingAvgGrade` ports.
16
16
  //
17
- // Registered from a route factory for the same reason routes/governance.js is:
17
+ // Registered from a route factory for the same reason routes/government.js is:
18
18
  // mountModuleRoutes is the one boot hook that runs BOTH in the live server and
19
19
  // whenever a test rebuilds the router, and registration is hasProvider-guarded so a
20
20
  // rebuild never trips the seam's single-provider duplicate guard.
21
21
  //
22
22
  // The router is EMPTY and must stay empty. Rank-derived assignment is automatic and
23
23
  // has no HTTP surface; the MANUAL assignment CRUD is R110's Archon-gated tab on the
24
- // governance router. An endpoint here would be a way to hand out a rank-role without
24
+ // government router. An endpoint here would be a way to hand out a rank without
25
25
  // changing the rank.
26
26
 
27
27
  const express = require('express');
@@ -41,7 +41,7 @@ function registerSeams() {
41
41
  }
42
42
  }
43
43
 
44
- module.exports = function buildGovernanceRoleSyncRouter() {
44
+ module.exports = function buildGovernmentRankSyncRouter() {
45
45
  registerSeams();
46
46
  return express.Router();
47
47
  };