@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,44 +1,44 @@
1
- // modules/governance/catalog.js — the permission catalog + compatibility seed.
1
+ // modules/government/catalog.js — the permission catalog + compatibility seed.
2
2
  //
3
3
  // The single CODE source of truth for authority atoms (ADR 0151, BV1.R94). Every
4
4
  // permission is a stable string key that maps to a REAL server capability (a route,
5
5
  // a claim rule, a page) — a permission no route consumes does not exist. The
6
- // resolver (R95), the requirePermission gate (R97), R93's role seed, and the Archon
6
+ // resolver (R95), the requirePermission gate (R97), R93's rank seed, and the Archon
7
7
  // tab (R110/R111) all read from here; the enumerated catalog it transcribes is
8
8
  // docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md, grounded in
9
9
  // docs/governance/authority-sweep-2026-07-24.md.
10
10
  //
11
11
  // Two orthogonal axes per permission:
12
- // • system — true = trust-boundary. The governance API (R110) rejects adding it
13
- // to a CUSTOM role and the tab renders it LOCKED there; it is deploy/
12
+ // • system — true = trust-boundary. The government API (R110) rejects adding it
13
+ // to a CUSTOM rank and the tab renders it LOCKED there; it is deploy/
14
14
  // protected-path/rank-set/version-close/grade-bypass/cross-tier/
15
15
  // principal power, enforced by the out-of-band substrate (ADR 0043)
16
- // or reserved to Archon. The built-in `archon` rank-role legitimately
16
+ // or reserved to Archon. The built-in `archon` rank legitimately
17
17
  // HOLDS the archon-floored system perms.
18
- // — false = operational, freely composable into a custom role.
18
+ // — false = operational, freely composable into a custom rank.
19
19
  // `system` is NOT the whole grantability answer any more — see the
20
- // `substrate` axis below and `isGrantableTo()`, which is per-ROLE.
20
+ // `substrate` axis below and `isGrantableTo()`, which is per-RANK.
21
21
  // • substrate — true = this key's authority is enforced OUTSIDE the app, by the
22
22
  // pre-push hook / main-audit / the rank floor the git and ssh layers
23
23
  // read (ADR 0043). A grant row for one is INERT: no route consumes it
24
24
  // through requirePermission, so granting it would be a toggle that
25
25
  // looks like it worked and changes nothing. Therefore non-grantable to
26
- // EVERY role, seeded rank-roles included — the one wall ADR 0172's
26
+ // EVERY rank, seeded ranks included — the one wall ADR 0172's
27
27
  // widening deliberately did not move, because moving it would make the
28
28
  // tab lie rather than delegate. Implies system:true + floor 'archon'.
29
29
  // • floor — the rank that holds this permission TODAY via the compat seed
30
30
  // ('xenos' < 'thetes' < 'metic' < 'archon'), or null for a machine
31
- // PRINCIPAL permission held by NO rank-role (gated on exact
32
- // system_role, never a human/role — ADR 0151 §3). floor drives the
31
+ // PRINCIPAL permission held by NO rank (gated on exact
32
+ // system_role, never a human/rank — ADR 0151 §3). floor drives the
33
33
  // seed derivation below; it is seed DATA, not grantability (the
34
34
  // `system` flag is the wall). Ownership-scoped + baseline own-work
35
35
  // perms floor at 'xenos' (every authenticated builder holds them).
36
36
  //
37
37
  // PRINCIPALS — the second authority AXIS (BV1.R98, ADR 0151 §3). A builder's
38
- // authority is the UNION of their ROLES; a machine principal's authority is its
38
+ // authority is the UNION of their RANKS; a machine principal's authority is its
39
39
  // IDENTITY. Those are different axes and the model keeps them apart: a principal
40
40
  // permission carries `principal: '<system_role>'` and `floor: null`, so it is in
41
- // NO rank-role (not even archon's) and no custom role may hold it (`system:true`).
41
+ // NO rank (not even archon's) and no custom rank may hold it (`system:true`).
42
42
  // The two are a PARTITION, asserted at load: floor:null ⟺ principal set. The only
43
43
  // source of a principal permission is resolver.resolvePrincipalPermissions(), keyed
44
44
  // on the exact `builders.system_role` value — a column with no API writer at all
@@ -77,10 +77,10 @@
77
77
  // Archon included), and minting a key no route consumes to satisfy a symmetry
78
78
  // check would violate this module's first rule.
79
79
  //
80
- // The RANK_ROLE_SEED is DERIVED from floors (cumulative nested supersets), so the
80
+ // The RANK_SEED is DERIVED from floors (cumulative nested supersets), so the
81
81
  // "zero day-one behavior change" parity (R103) is structural, not hand-maintained:
82
- // role(rank) = { p : p.floor !== null and tier(p.floor) <= tier(rank) }, and the
83
- // resolver's UNION over a builder's one rank-role reproduces requireRank's
82
+ // rank(rank) = { p : p.floor !== null and tier(p.floor) <= tier(rank) }, and the
83
+ // resolver's UNION over a builder's one rank reproduces requireRank's
84
84
  // at-or-above threshold exactly.
85
85
 
86
86
  'use strict';
@@ -96,7 +96,7 @@ const PERMISSIONS = [
96
96
  // ── system: true — trust-boundary (substrate- or Archon-enforced) ─────────────
97
97
  //
98
98
  // The seven WITHOUT `substrate` are in-app Archon powers: a real route reads them
99
- // through requirePermission, so granting one to a sub-Archon rank-role genuinely
99
+ // through requirePermission, so granting one to a sub-Archon rank genuinely
100
100
  // delegates it (ADR 0172, the owner's lever). The three WITH `substrate: true` are
101
101
  // enforced out of band and can never be delegated by a row in this database.
102
102
  { key: 'builder.rank.set', system: true, floor: 'archon', guards: 'PATCH /builders/:id/rank — the single rank writer' },
@@ -112,7 +112,7 @@ const PERMISSIONS = [
112
112
  { key: 'principal.bfg', system: true, floor: null, principal: 'bfg', guards: '/memory/bfg/* — requireBfgPrincipal (exact system_role=bfg; NO rank holds it)' },
113
113
  { key: 'principal.discord_bot', system: true, floor: null, principal: 'discord_bot', guards: 'Discord bot routes — requireDiscordBot (exact system_role; dormant)' },
114
114
 
115
- // ── operational — floor archon (admin ops; grantable to a custom role) ────────
115
+ // ── operational — floor archon (admin ops; grantable to a custom rank) ────────
116
116
  //
117
117
  // ADR 0157 (owner decision, 2026-08-05) narrowed this block to IDENTITY: the Archon
118
118
  // rank is responsible for rank and identity only. Nineteen keys that sat here for
@@ -166,15 +166,15 @@ const PERMISSIONS = [
166
166
  // and operations: they spend, read, adjudicate, and shuffle work, but none of them
167
167
  // decides WHO HOLDS AUTHORITY, so the trusted working rank carries them. All were
168
168
  // floor 'archon' before this ADR; none is system:true, so all were already
169
- // delegable to a custom role — this only lowers the seeded floor.
169
+ // delegable to a custom rank — this only lowers the seeded floor.
170
170
  //
171
171
  // Deliberately NOT in this block, though the owner's rule would otherwise reach them:
172
172
  // • `version.close` — retained at archon; it is the DISPOSITION that finalises a
173
173
  // version's scope-truth, and canonical-permissions has long paired it with rank
174
174
  // as the two powers that never delegate. ADR 0157 keeps that pairing.
175
175
  // • `dependency.cross_tier.manage` — system:true, and the catalog's own invariant
176
- // (tests/governance_catalog.mjs) forbids granting a system key to metic. Moving
177
- // it would mean clearing the system flag, which widens what any CUSTOM role can
176
+ // (tests/government_catalog.mjs) forbids granting a system key to metic. Moving
177
+ // it would mean clearing the system flag, which widens what any CUSTOM rank can
178
178
  // be granted — a second-order change beyond the delegation that was asked for.
179
179
  { key: 'builder.budget.set', system: false, floor: 'metic', guards: 'PATCH /builders/:id/budget (ADR 0157, was archon)' },
180
180
  { key: 'criterion.unsatisfy', system: false, floor: 'metic', guards: 'POST /done-when/:id/unsatisfy (ADR 0157, was archon)' },
@@ -185,7 +185,7 @@ const PERMISSIONS = [
185
185
  // siblings are `goal.create` and `task.create`, both operational. Close is the
186
186
  // DISPOSITION that finalises a version's scope-truth; create opens it — the symmetry
187
187
  // argument was considered and rejected, because system:true would permanently bar
188
- // delegating version authoring to a custom role for no trust-boundary reason.
188
+ // delegating version authoring to a custom rank for no trust-boundary reason.
189
189
  // ADR 0157 then lowered create's floor to metic while close stayed archon, which is
190
190
  // the same asymmetry read from the other side: authoring delegates, disposition does not.
191
191
  { key: 'version.create', system: false, floor: 'metic', guards: 'POST /versions — create a version + its done-when criteria (ADR 0157, was archon)' },
@@ -237,7 +237,7 @@ const PERMISSIONS = [
237
237
  // working rank. The gate FUNCTION was renamed to match (`requireArchonPage` →
238
238
  // `requireGovernmentPage`) once the floor moved. Viewing is all this grants: every
239
239
  // write those pages reach is its own atom, and `government.manage` (the
240
- // roles/permissions tab) stayed archon.
240
+ // ranks/permissions tab) stayed archon.
241
241
  { key: 'page.view.government', system: false, floor: 'metic', guards: 'the government/monitoring pages — /watch, /harbor, /gate (requireGovernmentPage; write = government.manage, archon)' },
242
242
  ];
243
243
 
@@ -249,70 +249,70 @@ const _byKey = new Map();
249
249
  const _byPrincipal = new Map();
250
250
  const _byResource = new Map();
251
251
  for (const p of PERMISSIONS) {
252
- if (typeof p.key !== 'string' || !p.key) throw new Error(`governance catalog: permission missing a key`);
253
- if (typeof p.system !== 'boolean') throw new Error(`governance catalog: ${p.key} missing boolean 'system'`);
254
- if (p.floor !== null && !RANK_ORDER.includes(p.floor)) throw new Error(`governance catalog: ${p.key} has invalid floor "${p.floor}"`);
255
- if (_byKey.has(p.key)) throw new Error(`governance catalog: duplicate key "${p.key}"`);
252
+ if (typeof p.key !== 'string' || !p.key) throw new Error(`government catalog: permission missing a key`);
253
+ if (typeof p.system !== 'boolean') throw new Error(`government catalog: ${p.key} missing boolean 'system'`);
254
+ if (p.floor !== null && !RANK_ORDER.includes(p.floor)) throw new Error(`government catalog: ${p.key} has invalid floor "${p.floor}"`);
255
+ if (_byKey.has(p.key)) throw new Error(`government catalog: duplicate key "${p.key}"`);
256
256
  const principal = p.principal ?? null;
257
257
  if (principal !== null && (typeof principal !== 'string' || !principal)) {
258
- throw new Error(`governance catalog: ${p.key} has a non-string principal identity`);
258
+ throw new Error(`government catalog: ${p.key} has a non-string principal identity`);
259
259
  }
260
- // The two axes PARTITION the catalog: a permission is held by a rank-role (a real
260
+ // The two axes PARTITION the catalog: a permission is held by a rank (a real
261
261
  // floor) XOR by a machine identity (floor null + a principal). Neither-nor would be
262
- // a permission nothing can ever hold; both-and would let a role reach an identity.
262
+ // a permission nothing can ever hold; both-and would let a rank reach an identity.
263
263
  if ((p.floor === null) !== (principal !== null)) {
264
- throw new Error(`governance catalog: ${p.key} — floor:null and a principal identity must come together (roles and identities are separate axes)`);
264
+ throw new Error(`government catalog: ${p.key} — floor:null and a principal identity must come together (ranks and identities are separate axes)`);
265
265
  }
266
266
  if (p.key.startsWith(PRINCIPAL_KEY_PREFIX) !== (principal !== null)) {
267
- throw new Error(`governance catalog: ${p.key} — a "${PRINCIPAL_KEY_PREFIX}*" key and a principal identity must come together`);
267
+ throw new Error(`government catalog: ${p.key} — a "${PRINCIPAL_KEY_PREFIX}*" key and a principal identity must come together`);
268
268
  }
269
269
  if (principal !== null) {
270
- if (!p.system) throw new Error(`governance catalog: ${p.key} is a principal permission and must be system:true (never grantable to a role)`);
271
- if (_byPrincipal.has(principal)) throw new Error(`governance catalog: duplicate principal identity "${principal}"`);
270
+ if (!p.system) throw new Error(`government catalog: ${p.key} is a principal permission and must be system:true (never grantable to a rank)`);
271
+ if (_byPrincipal.has(principal)) throw new Error(`government catalog: duplicate principal identity "${principal}"`);
272
272
  }
273
273
  // ── the SUBSTRATE axis (ADR 0172) ─────────────────────────────────────────
274
274
  // A substrate key is enforced by the out-of-band layer (pre-push hook, main-audit,
275
- // the rank floor git/ssh read), so no role row can confer it. Three shape rules,
275
+ // the rank floor git/ssh read), so no rank row can confer it. Three shape rules,
276
276
  // each closing a way the marker could quietly stop meaning that:
277
277
  const substrate = p.substrate ?? false;
278
- if (typeof substrate !== 'boolean') throw new Error(`governance catalog: ${p.key} has a non-boolean 'substrate'`);
278
+ if (typeof substrate !== 'boolean') throw new Error(`government catalog: ${p.key} has a non-boolean 'substrate'`);
279
279
  if (substrate) {
280
280
  // Substrate power is trust-boundary power by definition — a substrate key that
281
- // was somehow operational would be freely grantable to a custom role.
282
- if (!p.system) throw new Error(`governance catalog: ${p.key} is substrate-enforced and must be system:true`);
281
+ // was somehow operational would be freely grantable to a custom rank.
282
+ if (!p.system) throw new Error(`government catalog: ${p.key} is substrate-enforced and must be system:true`);
283
283
  // A substrate key belongs to no machine identity: the two are different reasons
284
284
  // a key is unreachable, and collapsing them would hide one behind the other.
285
- if (principal !== null) throw new Error(`governance catalog: ${p.key} cannot be both substrate-enforced and a machine principal`);
285
+ if (principal !== null) throw new Error(`government catalog: ${p.key} cannot be both substrate-enforced and a machine principal`);
286
286
  // The out-of-band enforcers all compare against the top of the ladder. A lower
287
- // floor would seed the key into a rank-role that the substrate then still denies.
288
- if (p.floor !== 'archon') throw new Error(`governance catalog: ${p.key} is substrate-enforced and must floor at 'archon' (the substrate compares against the top of the ladder)`);
287
+ // floor would seed the key into a rank that the substrate then still denies.
288
+ if (p.floor !== 'archon') throw new Error(`government catalog: ${p.key} is substrate-enforced and must floor at 'archon' (the substrate compares against the top of the ladder)`);
289
289
  }
290
290
  // ── the OWNERSHIP axis (R100) ─────────────────────────────────────────────
291
291
  const scope = p.scope ?? null;
292
292
  const resource = p.resource ?? null;
293
293
  if (scope !== null && !OWNERSHIP_SCOPES.includes(scope)) {
294
- throw new Error(`governance catalog: ${p.key} has invalid scope "${scope}" (expected 'own', 'any', or absent)`);
294
+ throw new Error(`government catalog: ${p.key} has invalid scope "${scope}" (expected 'own', 'any', or absent)`);
295
295
  }
296
296
  // An ownership SCOPE and the RESOURCE it is scoped over are one fact spelled in
297
297
  // two fields; either alone is meaningless ('own' of what? / a resource nothing
298
298
  // is scoped over). Same partition posture as floor ⟺ principal above.
299
299
  if ((scope === null) !== (resource === null)) {
300
- throw new Error(`governance catalog: ${p.key} — scope and resource must come together (an ownership scope is always scoped over a named resource)`);
300
+ throw new Error(`government catalog: ${p.key} — scope and resource must come together (an ownership scope is always scoped over a named resource)`);
301
301
  }
302
302
  if (resource !== null && (typeof resource !== 'string' || !resource)) {
303
- throw new Error(`governance catalog: ${p.key} has a non-string resource family`);
303
+ throw new Error(`government catalog: ${p.key} has a non-string resource family`);
304
304
  }
305
305
  // The namespace can never outrun the data: a key that CALLS itself own-scoped
306
306
  // must BE declared own-scoped, or a gate reading the declared axis would treat a
307
307
  // parameterized permission as an unconditional grant. Deliberately one-way — the
308
308
  // converse would be false (me.wandering.set is own-scoped without the suffix).
309
309
  if (p.key.endsWith(OWN_KEY_SUFFIX) && scope !== 'own') {
310
- throw new Error(`governance catalog: ${p.key} ends in "${OWN_KEY_SUFFIX}" but does not declare scope:'own' — an ownership-scoped key must say so in data`);
310
+ throw new Error(`government catalog: ${p.key} ends in "${OWN_KEY_SUFFIX}" but does not declare scope:'own' — an ownership-scoped key must say so in data`);
311
311
  }
312
- // An ownership scope NARROWS a role-held permission; a machine identity has no
312
+ // An ownership scope NARROWS a rank-held permission; a machine identity has no
313
313
  // owner axis, so the two never compose.
314
314
  if (scope !== null && p.floor === null) {
315
- throw new Error(`governance catalog: ${p.key} is ownership-scoped but has no rank floor — the ownership axis narrows a ROLE permission, never an identity one`);
315
+ throw new Error(`government catalog: ${p.key} is ownership-scoped but has no rank floor — the ownership axis narrows a RANK permission, never an identity one`);
316
316
  }
317
317
  const frozen = Object.freeze({ ...p, principal, scope, resource, substrate });
318
318
  _byKey.set(p.key, frozen);
@@ -332,15 +332,15 @@ Object.freeze(PERMISSIONS);
332
332
  const floorTier = (rank) => RANK_ORDER.indexOf(rank);
333
333
  for (const [resource, fam] of _byResource) {
334
334
  if (fam.any.length > 0 && fam.own.length === 0) {
335
- throw new Error(`governance catalog: resource "${resource}" declares a cross-owner permission (${fam.any.join(', ')}) with no ownership-scoped counterpart`);
335
+ throw new Error(`government catalog: resource "${resource}" declares a cross-owner permission (${fam.any.join(', ')}) with no ownership-scoped counterpart`);
336
336
  }
337
337
  // Reaching EVERY owner's row must never be cheaper than reaching your own. If it
338
338
  // were, a rank could manage the whole fleet but not its own box — the model
339
- // inverted, and the governance tab would render the wider grant as the safer one.
339
+ // inverted, and the Government tab would render the wider grant as the safer one.
340
340
  for (const anyKey of fam.any) {
341
341
  for (const ownKey of fam.own) {
342
342
  if (floorTier(_byKey.get(anyKey).floor) < floorTier(_byKey.get(ownKey).floor)) {
343
- throw new Error(`governance catalog: ${anyKey} (floor ${_byKey.get(anyKey).floor}) sits BELOW ${ownKey} (floor ${_byKey.get(ownKey).floor}) — cross-owner authority must never be cheaper than own-only`);
343
+ throw new Error(`government catalog: ${anyKey} (floor ${_byKey.get(anyKey).floor}) sits BELOW ${ownKey} (floor ${_byKey.get(ownKey).floor}) — cross-owner authority must never be cheaper than own-only`);
344
344
  }
345
345
  }
346
346
  }
@@ -382,9 +382,9 @@ function isSystem(key) {
382
382
  const p = _byKey.get(key);
383
383
  return p ? p.system : null; // null = unknown key (caller fails closed)
384
384
  }
385
- // Grantable to a CUSTOM role = exists and not system. Unknown key → false (fail-closed).
386
- // This is the NARROW question — the custom-role wall, unchanged by ADR 0172 and
387
- // still what config.js strips against. For the per-role answer use isGrantableTo().
385
+ // Grantable to a CUSTOM rank = exists and not system. Unknown key → false (fail-closed).
386
+ // This is the NARROW question — the custom-rank wall, unchanged by ADR 0172 and
387
+ // still what config.js strips against. For the per-rank answer use isGrantableTo().
388
388
  function isGrantable(key) {
389
389
  const p = _byKey.get(key);
390
390
  return !!p && !p.system;
@@ -406,40 +406,40 @@ function substrateKeys() {
406
406
  return PERMISSIONS.filter((p) => p.substrate).map((p) => p.key);
407
407
  }
408
408
 
409
- // ── grantability, per ROLE (ADR 0172) ──────────────────────────────────────────
409
+ // ── grantability, per RANK (ADR 0172) ──────────────────────────────────────────
410
410
  //
411
- // The owner's 2026-08-16 decision made the three sub-Archon RANK roles editable, so
412
- // "is this grantable" stopped being a property of the key alone. `role` is a
411
+ // The owner's 2026-08-16 decision made the three sub-Archon RANK ranks editable, so
412
+ // "is this grantable" stopped being a property of the key alone. `rank` is a
413
413
  // government_ranks row ({ is_system, seeded_rank }); the four answers:
414
414
  //
415
- // • NEVER, to any role — a machine-principal key (held by an identity, not a role)
415
+ // • NEVER, to any rank — a machine-principal key (held by an identity, not a rank)
416
416
  // and a SUBSTRATE key. The substrate three are inert as grant rows: no route
417
417
  // consumes them through requirePermission, so a toggle for one would report
418
418
  // success and delegate nothing. Refusing them keeps the tab honest.
419
- // • the ARCHON rank-role — nothing. It is immutable and already holds everything,
420
- // and that immutability is also the LOCKOUT GUARD: the only role carrying
419
+ // • the ARCHON rank — nothing. It is immutable and already holds everything,
420
+ // and that immutability is also the LOCKOUT GUARD: the only rank carrying
421
421
  // `government.manage` by seed can never have it revoked, so no sequence of
422
422
  // clicks in this tab can lock the owner out of the tab.
423
- // • a SEEDED rank-role BELOW archon — everything else, the seven in-app Archon
423
+ // • a SEEDED rank BELOW archon — everything else, the seven in-app Archon
424
424
  // powers included. This is the delegation lever the owner asked for.
425
- // • a CUSTOM role — operational only: the pre-existing R110 wall, deliberately NOT
426
- // widened. A rank-role is the owner's own coarse ladder, moved by a rank write;
427
- // a custom role is ad-hoc and assignable by anyone holding `government.manage`,
425
+ // • a CUSTOM rank — operational only: the pre-existing R110 wall, deliberately NOT
426
+ // widened. A rank is the owner's own coarse ladder, moved by a rank write;
427
+ // a custom rank is ad-hoc and assignable by anyone holding `government.manage`,
428
428
  // so widening it would hand the second group the first group's lever.
429
429
  //
430
- // Fail-closed on everything unrecognised: unknown key, missing role, or a system role
430
+ // Fail-closed on everything unrecognised: unknown key, missing rank, or a system rank
431
431
  // whose seeded_rank is not a real rank all answer false.
432
- function isGrantableTo(key, role) {
432
+ function isGrantableTo(key, rank) {
433
433
  const p = _byKey.get(key);
434
- if (!p || !role || typeof role !== 'object') return false;
434
+ if (!p || !rank || typeof rank !== 'object') return false;
435
435
  if (p.principal !== null || p.substrate) return false;
436
- if (role.is_system) {
437
- return RANK_ORDER.includes(role.seeded_rank) && role.seeded_rank !== 'archon';
436
+ if (rank.is_system) {
437
+ return RANK_ORDER.includes(rank.seeded_rank) && rank.seeded_rank !== 'archon';
438
438
  }
439
439
  return !p.system;
440
440
  }
441
441
 
442
- // ── the principal axis (identity, not roles) ────────────────────────────────────
442
+ // ── the principal axis (identity, not ranks) ────────────────────────────────────
443
443
  // Reads only. Unknown/blank input fails closed to null/false — the same posture the
444
444
  // rest of the catalog takes, and the one the identity gates depend on.
445
445
  function principals() {
@@ -451,7 +451,7 @@ function principalRoles() {
451
451
  function principalKeys() {
452
452
  return PRINCIPALS.map((p) => p.permission);
453
453
  }
454
- // Is this key held by a machine identity rather than by any role? Unknown → false.
454
+ // Is this key held by a machine identity rather than by any rank? Unknown → false.
455
455
  function isPrincipalKey(key) {
456
456
  const p = _byKey.get(key);
457
457
  return !!p && p.principal !== null;
@@ -503,17 +503,17 @@ function crossOwnerKeysFor(key) {
503
503
  return fam ? fam.any : NO_KEYS;
504
504
  }
505
505
 
506
- // The compat seed, DERIVED from floors (cumulative supersets). A rank-role holds
506
+ // The compat seed, DERIVED from floors (cumulative supersets). A rank holds
507
507
  // every permission whose floor is a real rank at-or-below it; principals (floor
508
- // null) are held by NO rank-role. This is the "zero day-one change" guarantee —
509
- // role(rank) reproduces requireRank(rank)'s at-or-above threshold exactly (R103).
508
+ // null) are held by NO rank. This is the "zero day-one change" guarantee —
509
+ // rank(rank) reproduces requireRank(rank)'s at-or-above threshold exactly (R103).
510
510
  function seedForRank(rank) {
511
511
  const tier = RANK_ORDER.indexOf(rank);
512
- if (tier < 0) return []; // unknown/reserved rank → no role → no permissions (fail-closed; ADR 0151)
512
+ if (tier < 0) return []; // unknown/reserved rank → no rank → no permissions (fail-closed; ADR 0151)
513
513
  return PERMISSIONS.filter((p) => p.floor !== null && RANK_ORDER.indexOf(p.floor) <= tier).map((p) => p.key);
514
514
  }
515
515
 
516
- const RANK_ROLE_SEED = Object.freeze(
516
+ const RANK_SEED = Object.freeze(
517
517
  Object.fromEntries(RANK_ORDER.map((r) => [r, Object.freeze(seedForRank(r))])),
518
518
  );
519
519
 
@@ -530,7 +530,7 @@ module.exports = {
530
530
  systemKeys,
531
531
  grantableKeys,
532
532
  seedForRank,
533
- RANK_ROLE_SEED,
533
+ RANK_SEED,
534
534
  PRINCIPALS,
535
535
  principals,
536
536
  principalRoles,
@@ -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,