@bongos/core 1.19.186 → 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 (49) hide show
  1. package/.bongos-core.json +78 -73
  2. package/config/government.neutral.json +6 -6
  3. package/docs/adr/0172-editable-rank-roles-substrate-axis.md +1 -1
  4. package/docs/api/openapi.json +8 -8
  5. package/docs/api-reference.md +7 -7
  6. package/modules/builder-settings/wandering-prefs.js +13 -13
  7. package/modules/government/catalog.js +75 -75
  8. package/modules/government/config.js +43 -33
  9. package/modules/government/db.js +57 -57
  10. package/modules/government/protected-surfaces.js +7 -7
  11. package/modules/government/protected-surfaces.json +4 -4
  12. package/modules/government/ranks.js +17 -17
  13. package/modules/government/resolver.js +24 -24
  14. package/modules/government/routes/government.js +86 -86
  15. package/modules/government/routes/rank-sync.js +6 -6
  16. package/modules/government/session-scopes.js +7 -7
  17. package/modules/government/session-scopes.json +5 -5
  18. package/modules/hall-ui/public/government.css +1 -1
  19. package/modules/hall-ui/public/government.html +1 -1
  20. package/modules/hall-ui/public/government.js +3 -3
  21. package/modules/lifecycle/claim-eligibility.js +1 -1
  22. package/modules/lifecycle/db.js +1 -1
  23. package/package-lock.json +2 -2
  24. package/package.json +1 -1
  25. package/scripts/gds/fitness.js +138 -0
  26. package/src/bongos/auth.js +2 -2
  27. package/src/module-api.js +3 -1
  28. package/tests/auth_page_gate.mjs +2 -2
  29. package/tests/{governance_abuse_matrix.mjs → government_abuse_matrix.mjs} +36 -36
  30. package/tests/{governance_catalog.mjs → government_catalog.mjs} +11 -11
  31. package/tests/{governance_claim_eligibility.mjs → government_claim_eligibility.mjs} +2 -2
  32. package/tests/{governance_config.mjs → government_config.mjs} +54 -54
  33. package/tests/{governance_fold.mjs → government_fold.mjs} +6 -6
  34. package/tests/{governance_fold_bench.mjs → government_fold_bench.mjs} +3 -3
  35. package/tests/{governance_live_proof.mjs → government_live_proof.mjs} +5 -5
  36. package/tests/{governance_ownership_scope.mjs → government_ownership_scope.mjs} +16 -16
  37. package/tests/{governance_page_gates.mjs → government_page_gates.mjs} +5 -5
  38. package/tests/{governance_parity.mjs → government_parity.mjs} +3 -3
  39. package/tests/{governance_principals.mjs → government_principals.mjs} +14 -14
  40. package/tests/{governance_protected_surfaces.mjs → government_protected_surfaces.mjs} +1 -1
  41. package/tests/{governance_role_sync.mjs → government_rank_sync.mjs} +2 -2
  42. package/tests/{governance_require_permission.mjs → government_require_permission.mjs} +1 -1
  43. package/tests/{governance_resolver.mjs → government_resolver.mjs} +6 -6
  44. package/tests/{governance_routes.mjs → government_routes.mjs} +32 -32
  45. package/tests/{governance_seed.mjs → government_seed.mjs} +8 -8
  46. package/tests/{governance_session_scope.mjs → government_session_scope.mjs} +10 -10
  47. package/tests/{governance_vanilla_boot.mjs → government_vanilla_boot.mjs} +26 -26
  48. package/tests/government_vocabulary.mjs +94 -0
  49. package/tests/hall_page_gate_map.mjs +1 -1
@@ -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
121
+ // The db half of the government API routes. Deliberately DUMB writes: the
122
+ // authority invariants (system perms non-grantable, seeded ranks immutable
123
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;
@@ -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) {