@bongos/core 1.19.186 → 1.19.188

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 (54) hide show
  1. package/.bongos-core.json +83 -78
  2. package/config/government.neutral.json +6 -6
  3. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +7 -0
  4. package/docs/adr/0172-editable-rank-roles-substrate-axis.md +8 -1
  5. package/docs/adr/README.md +2 -2
  6. package/docs/api/openapi.json +8 -8
  7. package/docs/api-reference.md +7 -7
  8. package/docs/architecture.md +1 -1
  9. package/docs/canonical-permissions.md +9 -9
  10. package/docs/file-map.md +1 -1
  11. package/modules/builder-settings/wandering-prefs.js +13 -13
  12. package/modules/government/catalog.js +75 -75
  13. package/modules/government/config.js +43 -33
  14. package/modules/government/db.js +57 -57
  15. package/modules/government/protected-surfaces.js +7 -7
  16. package/modules/government/protected-surfaces.json +4 -4
  17. package/modules/government/ranks.js +17 -17
  18. package/modules/government/resolver.js +24 -24
  19. package/modules/government/routes/government.js +86 -86
  20. package/modules/government/routes/rank-sync.js +6 -6
  21. package/modules/government/session-scopes.js +7 -7
  22. package/modules/government/session-scopes.json +5 -5
  23. package/modules/hall-ui/public/government.css +1 -1
  24. package/modules/hall-ui/public/government.html +1 -1
  25. package/modules/hall-ui/public/government.js +3 -3
  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/src/bongos/auth.js +2 -2
  32. package/src/module-api.js +5 -1
  33. package/tests/auth_page_gate.mjs +2 -2
  34. package/tests/{governance_abuse_matrix.mjs → government_abuse_matrix.mjs} +36 -36
  35. package/tests/{governance_catalog.mjs → government_catalog.mjs} +11 -11
  36. package/tests/{governance_claim_eligibility.mjs → government_claim_eligibility.mjs} +2 -2
  37. package/tests/{governance_config.mjs → government_config.mjs} +54 -54
  38. package/tests/{governance_fold.mjs → government_fold.mjs} +6 -6
  39. package/tests/{governance_fold_bench.mjs → government_fold_bench.mjs} +3 -3
  40. package/tests/{governance_live_proof.mjs → government_live_proof.mjs} +5 -5
  41. package/tests/{governance_ownership_scope.mjs → government_ownership_scope.mjs} +16 -16
  42. package/tests/{governance_page_gates.mjs → government_page_gates.mjs} +5 -5
  43. package/tests/{governance_parity.mjs → government_parity.mjs} +3 -3
  44. package/tests/{governance_principals.mjs → government_principals.mjs} +14 -14
  45. package/tests/{governance_protected_surfaces.mjs → government_protected_surfaces.mjs} +1 -1
  46. package/tests/{governance_role_sync.mjs → government_rank_sync.mjs} +2 -2
  47. package/tests/{governance_require_permission.mjs → government_require_permission.mjs} +1 -1
  48. package/tests/{governance_resolver.mjs → government_resolver.mjs} +6 -6
  49. package/tests/{governance_routes.mjs → government_routes.mjs} +32 -32
  50. package/tests/{governance_seed.mjs → government_seed.mjs} +8 -8
  51. package/tests/{governance_session_scope.mjs → government_session_scope.mjs} +10 -10
  52. package/tests/{governance_vanilla_boot.mjs → government_vanilla_boot.mjs} +26 -26
  53. package/tests/government_vocabulary.mjs +94 -0
  54. package/tests/hall_page_gate_map.mjs +1 -1
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "_comment": [
3
3
  "The SESSION-SCOPE registry (BV1.R99, ADR 0151 §3; the model behind ADR 0053).",
4
- "A session's scope is capability attached to the TOKEN, orthogonal to the builder's roles.",
4
+ "A session's scope is capability attached to the TOKEN, orthogonal to the builder's ranks.",
5
5
  "Each row declares EITHER unscoped:true (the session narrows nothing) OR a deny-by-default",
6
6
  "permissions allow-list (unlisted => denied). A scope can only ever NARROW: effective",
7
- "authority is (the builder's role permissions) INTERSECT (this allow-list), never a union.",
8
- "Every key here is validated against modules/governance/catalog.js at load by",
9
- "modules/governance/session-scopes.js — unknown, system:true and principal keys are refused.",
7
+ "authority is (the builder's rank permissions) INTERSECT (this allow-list), never a union.",
8
+ "Every key here is validated against modules/government/catalog.js at load by",
9
+ "modules/government/session-scopes.js — unknown, system:true and principal keys are refused.",
10
10
  "Read as DATA by src/bongos/auth.js (fs, never require — ADR 0083's one-way rule)."
11
11
  ],
12
12
  "scopes": [
@@ -14,7 +14,7 @@
14
14
  "source": "cli",
15
15
  "label": "CLI session",
16
16
  "unscoped": true,
17
- "note": "Device Flow token on a builder's own machine — inside the trust boundary, carries the builder's full role authority."
17
+ "note": "Device Flow token on a builder's own machine — inside the trust boundary, carries the builder's full rank authority."
18
18
  },
19
19
  {
20
20
  "source": "web",
@@ -1,4 +1,4 @@
1
- /* governance.css — Page-local styles for the Governance page (roles /
1
+ /* government.css — Page-local styles for the Government page (ranks /
2
2
  permissions / assignments — BV1.R111, ADR 0151). Built on the shared design
3
3
  tokens (style.css), mirroring gate.css's card + pill idiom; the ?v= cache-bust
4
4
  is stamped from this file's content hash at boot (src/asset-version.js). */
@@ -73,6 +73,6 @@
73
73
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
74
74
  <script src="/builders/palette.js" defer></script>
75
75
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
76
- <script src="/builders/government.js?v=2026-08-06-governance"></script>
76
+ <script src="/builders/government.js?v=2026-08-16-government"></script>
77
77
  </body>
78
78
  </html>
@@ -26,7 +26,7 @@
26
26
  // web sign-in.
27
27
  //
28
28
  // DELIBERATELY NOT IN THE SIDEBAR YET: the nav entry + the server page gate are
29
- // R113 (the next slice) — tests/hall_nav.mjs pins the Governance nav group and
29
+ // R113 (the next slice) — tests/hall_nav.mjs pins the Government nav group and
30
30
  // grows with that slice, not this one. Until then the page is reached by URL.
31
31
 
32
32
  (() => {
@@ -402,7 +402,7 @@
402
402
 
403
403
  async function init() {
404
404
  // Confirm rank for a friendly sealed notice; the server gates the page and
405
- // every /governance endpoint on `government.manage` regardless (cosmetic
405
+ // every /government endpoint on `government.manage` regardless (cosmetic
406
406
  // defence-in-depth). government.manage is system:true, so only the archon
407
407
  // rank can hold it — rank is an accurate client-side proxy.
408
408
  let me;
@@ -424,7 +424,7 @@
424
424
  } catch (err) {
425
425
  if (err && err.kind === '401') return;
426
426
  if (err && err.kind === '403') { showSealed(); return; }
427
- document.getElementById('gov-grid').innerHTML = `<p class="gov-empty">Could not load governance data just now.</p>`;
427
+ document.getElementById('gov-grid').innerHTML = `<p class="gov-empty">Could not load government data just now.</p>`;
428
428
  }
429
429
  }
430
430
 
@@ -21,7 +21,7 @@
21
21
  // separate, consecutive gates — see claimTask / claimTasksBatch /
22
22
  // validateClaimBatch.
23
23
  //
24
- // Pure + DB-free so tests/governance_claim_eligibility.mjs can exercise every
24
+ // Pure + DB-free so tests/government_claim_eligibility.mjs can exercise every
25
25
  // rank × permission × requires_rank combination without Postgres.
26
26
 
27
27
  // The two catalog keys this axis reads (modules/governance/catalog.js). Exported
@@ -197,7 +197,7 @@ const GENERAL_QUEUE_RANKS = new Set(LIVE_RANK_LADDER.filter((r) => r !== 'xenos'
197
197
  // `task.claim.newcomer` (claim-eligibility.js) and keep this as the fallback for a
198
198
  // builder whose role row the run-once compat backfill never created. It stays
199
199
  // exported: tests/rank_gate.mjs pins it, and it is the oracle the parity test in
200
- // tests/governance_claim_eligibility.mjs compares the permission predicate against.
200
+ // tests/government_claim_eligibility.mjs compares the permission predicate against.
201
201
 
202
202
  function xenosClaimAllowed(rank, newcomerFriendly) {
203
203
  if (GENERAL_QUEUE_RANKS.has(rank)) return true;
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.186",
3
+ "version": "1.19.188",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.186",
9
+ "version": "1.19.188",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.186",
3
+ "version": "1.19.188",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -1640,6 +1640,141 @@ function checkWriteRoutesValidated() {
1640
1640
 
1641
1641
  // ---- runner ----------------------------------------------------------------
1642
1642
 
1643
+
1644
+ // ── Check 19 — the ADR 0174 vocabulary wall (goal 1000068 R07, task 1003008) ──
1645
+ //
1646
+ // "Role" means ONE thing in this codebase: a craft — Artist, Ideator, or Builder.
1647
+ // The permission system is the GOVERNMENT and the positions in it are RANKS.
1648
+ // Before ADR 0174 the same word named both, and goal 1000066 was about to claim
1649
+ // it for the crafts while modules/governance/ still used it for the ladder. This
1650
+ // check is what stops that collision reforming the first time someone copies an
1651
+ // old file.
1652
+ //
1653
+ // TWO RULES, both scoped to the GOVERNMENT SURFACE — not the whole repo. A repo-
1654
+ // wide ban would be wrong: `builders.system_role` is a core column for machine
1655
+ // identities, Discord's API owns its own "role" noun, and the crafts are supposed
1656
+ // to use the word.
1657
+ //
1658
+ // 1. no "role" under modules/government/ or on the Government hall page
1659
+ // 2. no "governance" there either — the old name of the same surface
1660
+ //
1661
+ // EXCLUDED BY CONSTRUCTION (not by allowlist):
1662
+ // • modules/government/migrations/governance_*.sql — the already-applied
1663
+ // migrations keep their filenames forever (schema_migrations is stem-keyed;
1664
+ // renaming one re-runs it, and governance_002 is a seed/backfill). Their
1665
+ // bodies necessarily name the pre-rename tables they created.
1666
+ // • docs/adr/, docs/session-logs/, docs/governance/ — the historical record.
1667
+ // An ADR describes a decision at a point in time; rewriting it to match
1668
+ // today's vocabulary would erase the evidence that the words changed.
1669
+ //
1670
+ // The ALLOWLIST below is deliberately tiny and each entry carries its reason. A
1671
+ // rule that passes because everything is suppressed reads as evidence while
1672
+ // proving nothing — if this list grows, that is the signal to finish the rename,
1673
+ // not to add a line.
1674
+ const VOCAB_ALLOW = [
1675
+ {
1676
+ file: 'modules/government/config.js',
1677
+ match: 'roles',
1678
+ why: 'the legacy INSTANCE-config key fallback. The core ships `ranks`, but an '
1679
+ + 'external self-hosted instance may still have a config/government.json '
1680
+ + 'authored as `roles`; reading it is what stops their templates vanishing '
1681
+ + 'on upgrade. Removing this line would break them silently.',
1682
+ },
1683
+ {
1684
+ file: 'modules/government/config.js',
1685
+ match: 'governance_role_labels',
1686
+ why: 'the legacy ENV-VAR fallback. <PREFIX>_GOVERNMENT_RANK_LABELS is the current '
1687
+ + 'name, but an operator sets this in their own shell or env file, outside this '
1688
+ + 'repo, so the old spelling is still read. Dropping it would silently stop '
1689
+ + 'honouring a label override someone already configured.',
1690
+ },
1691
+ {
1692
+ file: 'modules/government/routes/government.js',
1693
+ match: '/governance',
1694
+ why: 'the one-release 308 alias from the pre-rename route paths (ADR 0174). A '
1695
+ + 'compatibility shim has to name the thing it is compatible with. Note the '
1696
+ + 'match requires the leading SLASH — this permits citing the legacy PATH, not '
1697
+ + 'the bare word. Delete with the alias block one release after the rename ships.',
1698
+ },
1699
+ {
1700
+ file: 'modules/government/module.json',
1701
+ match: 'role',
1702
+ why: 'the manifest states the rule itself — "The word `role` is deliberately '
1703
+ + 'absent here". Meta-commentary about the ban has to be able to name it.',
1704
+ },
1705
+ ];
1706
+
1707
+ // A line CITING a historical artifact by its real name: an ADR filename, the
1708
+ // docs/governance/ evidence folder, or one of the already-applied
1709
+ // governance_NNN migrations. These names are permanent (see the header), so a
1710
+ // reference to one is a citation, not drift.
1711
+ const VOCAB_HISTORICAL_REF = /docs\/adr\/|docs\/governance\/|docs\/session-logs\/|governance_0\d\d/;
1712
+
1713
+ const VOCAB_SURFACE = [
1714
+ 'modules/government',
1715
+ 'modules/hall-ui/public/government.js',
1716
+ 'modules/hall-ui/public/government.html',
1717
+ 'modules/hall-ui/public/government.css',
1718
+ ];
1719
+ const VOCAB_EXTS = new Set(['.js', '.json', '.html', '.css']);
1720
+ const VOCAB_BANNED = [
1721
+ { re: /\brole/i, label: 'role', hint: '"role" is reserved for the crafts (Artist / Ideator / Builder). On this surface the noun is RANK.' },
1722
+ { re: /governance/i, label: 'governance', hint: 'the pre-ADR-0174 name of this surface. It is the GOVERNMENT now.' },
1723
+ ];
1724
+
1725
+ function vocabFilesToScan() {
1726
+ const out = [];
1727
+ const walk = (abs, rel) => {
1728
+ let st;
1729
+ try { st = fs.statSync(abs); } catch { return; }
1730
+ if (st.isDirectory()) {
1731
+ // the historical migrations keep their names + bodies (see header)
1732
+ if (rel.endsWith('/migrations')) return;
1733
+ for (const e of fs.readdirSync(abs)) walk(path.join(abs, e), `${rel}/${e}`);
1734
+ return;
1735
+ }
1736
+ if (VOCAB_EXTS.has(path.extname(abs))) out.push({ abs, rel });
1737
+ };
1738
+ for (const s of VOCAB_SURFACE) walk(path.join(CORE_ROOT, s), s);
1739
+ return out;
1740
+ }
1741
+
1742
+ function checkVocabularyWall() {
1743
+ const violations = [];
1744
+ let scanned = 0;
1745
+ for (const { abs, rel } of vocabFilesToScan()) {
1746
+ scanned += 1;
1747
+ const lines = fs.readFileSync(abs, 'utf8').split('\n');
1748
+ lines.forEach((line, i) => {
1749
+ // `role="…"` is HTML's own ARIA attribute — a different word that happens to
1750
+ // be spelled the same. Strip attributes before matching rather than
1751
+ // allowlisting each occurrence: this is a property of HTML, not of any one file.
1752
+ const scanLine = line.replace(/\srole\s*=\s*("[^"]*"|'[^']*')/g, ' ');
1753
+ for (const b of VOCAB_BANNED) {
1754
+ if (!b.re.test(scanLine)) continue;
1755
+ // A REFERENCE to a historical artifact by its real name is not a
1756
+ // vocabulary violation — it is a citation. The artifacts themselves are
1757
+ // excluded by construction (see the header); naming one has to be legal
1758
+ // too, or the code could not point at the migration that created its own
1759
+ // tables. Same principle, applied to the line rather than the file.
1760
+ if (VOCAB_HISTORICAL_REF.test(scanLine)) continue;
1761
+ const excused = VOCAB_ALLOW.some((a) => a.file === rel && scanLine.toLowerCase().includes(a.match));
1762
+ if (excused) continue;
1763
+ violations.push(`${rel}:${i + 1}: "${b.label}" — ${b.hint} → ${line.trim().slice(0, 90)}`);
1764
+ }
1765
+ });
1766
+ }
1767
+ return {
1768
+ name: 'the word "role" means a craft, never a rank (ADR 0174)',
1769
+ ok: violations.length === 0,
1770
+ hardFail: violations.length > 0,
1771
+ violations,
1772
+ warnings: [],
1773
+ note: `${scanned} government-surface file(s) scanned for "role"/"governance"; `
1774
+ + `${VOCAB_ALLOW.length} documented allowlist entr(y|ies). Historical migrations + docs/adr are excluded by construction, not suppressed.`,
1775
+ };
1776
+ }
1777
+
1643
1778
  const CHECKS = [
1644
1779
  checkCoreHostBoundary,
1645
1780
  checkClaudeMdBudget,
@@ -1661,6 +1796,7 @@ const CHECKS = [
1661
1796
  checkCoreRequiresExcludedConfig, // task 1824 — core→instance-config require guard
1662
1797
  checkCoreRequiresExcludedModule, // Check 16b — task 2135: core→excluded-module require guard (R89 split)
1663
1798
  checkErrorEnvelope, // Check 17 — BV1 R03 / task 1990: no route hand-rolls a flat error (ADR 0116)
1799
+ checkVocabularyWall, // Check 19 — ADR 0174 / goal 1000068 R07: "role" is a craft, never a rank
1664
1800
  checkWriteRoutesValidated, // Check 18 — BV1 R12 / task 1999: every body-reading write route validates (ADR 0118)
1665
1801
  ];
1666
1802
 
@@ -1690,6 +1826,8 @@ function main() {
1690
1826
  if (require.main === module) main();
1691
1827
 
1692
1828
  module.exports = {
1829
+ checkVocabularyWall,
1830
+ VOCAB_ALLOW,
1693
1831
  runAll,
1694
1832
  resolvesToHostGame,
1695
1833
  checkCoreHostBoundary,
@@ -1191,7 +1191,7 @@ async function resolveSession(req) {
1191
1191
  // inside the session's scope at all". That projection is deliberately the ROLE-FREE
1192
1192
  // half — folding the builder's roles in here would change who can reach the
1193
1193
  // bootstrap routes (a builder on a rank with no seeded governance role resolves to
1194
- // zero permissions by design — see tests/governance_parity.mjs Part D), and R99 is
1194
+ // zero permissions by design — see tests/government_parity.mjs Part D), and R99 is
1195
1195
  // a behaviour-identical remodelling of the #919 gate, not a re-gating of it.
1196
1196
  //
1197
1197
  // GOVERNANCE OWNS THE POLICY, THE KERNEL STAYS THE ENFORCER. The declaration is
@@ -1442,7 +1442,7 @@ function signInWithReturn(req) {
1442
1442
  // page.view.government's floor archon→metic and the name stopped being true; the
1443
1443
  // floor now lives in modules/government/catalog.js, not in the function's name.
1444
1444
  // The one accepted divergence is the one governance_002 already documents
1445
- // (tests/governance_parity.mjs Part D): a builder on a reserved/divine rank holds
1445
+ // (tests/government_parity.mjs Part D): a builder on a reserved/divine rank holds
1446
1446
  // no seeded role, so they now fail a page gate a bare rank test would have
1447
1447
  // admitted. That is a TIGHTENING, never a widening.
1448
1448
  //
package/src/module-api.js CHANGED
@@ -799,8 +799,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
799
799
  // landed since 1.19.184 with no explicit bump. run 31976564067. (task 1002620)
800
800
  // 1.19.186 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
801
801
  // landed since 1.19.185 with no explicit bump. run 31977207459. (task 1002620)
802
+ // 1.19.187 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
803
+ // landed since 1.19.186 with no explicit bump. run 31978548058. (task 1002620)
804
+ // 1.19.188 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
805
+ // landed since 1.19.187 with no explicit bump. run 31978931152. (task 1002620)
802
806
  // ---------------------------------------------------------------------------
803
- const CORE_VERSION = '1.19.186'; // CI auto-patch carrier (ADR 0161); see changelog above
807
+ const CORE_VERSION = '1.19.188'; // CI auto-patch carrier (ADR 0161); see changelog above
804
808
 
805
809
  // A namespaced logger so a module's log lines are attributable + consistent.
806
810
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -31,8 +31,8 @@ const auth = require('../src/bongos/auth.js');
31
31
 
32
32
  // BV1.R107: the page gate now also resolves a permission (page.view.public) through
33
33
  // the `governance` kernel port, so an authenticated request needs the port stubbed —
34
- // exactly as tests/governance_require_permission.mjs stubs it for requirePermission.
35
- // The audience is unchanged; tests/governance_page_gates.mjs is the parity proof.
34
+ // exactly as tests/government_require_permission.mjs stubs it for requirePermission.
35
+ // The audience is unchanged; tests/government_page_gates.mjs is the parity proof.
36
36
  const seams = require('../src/module-seams.js');
37
37
  const catalog = require('../modules/government/catalog.js');
38
38
  seams._reset();
@@ -1,4 +1,4 @@
1
- // tests/governance_abuse_matrix.mjs — the abuse matrix (BV1.R112, ADR 0151).
1
+ // tests/government_abuse_matrix.mjs — the abuse matrix (BV1.R112, ADR 0151).
2
2
  //
3
3
  // Mechanically proves the safety guarantee the governance module asserts in
4
4
  // prose: NO configuration surface can grant a `system: true` (trust-boundary)
@@ -32,10 +32,10 @@
32
32
  // V12 seed integrity: no rank-role below archon holds a system key; no
33
33
  // rank-role at all holds a principal key
34
34
  //
35
- // DB-free (the governance_routes.mjs pattern): real router + real
35
+ // DB-free (the government_routes.mjs pattern): real router + real
36
36
  // requirePermission over a stub `governance` port, db sibling monkeypatched.
37
37
  //
38
- // Run: node tests/governance_abuse_matrix.mjs
38
+ // Run: node tests/government_abuse_matrix.mjs
39
39
 
40
40
  import { strict as assert } from 'node:assert';
41
41
  import { test, after } from 'node:test';
@@ -108,16 +108,16 @@ test('every system permission floors at archon or is a principal (never seeded l
108
108
 
109
109
  const calls = [];
110
110
  const stub = {
111
- getRoleByKey: async (k) => (SEEDED_ROLES.includes(k)
111
+ getRankByKey: async (k) => (SEEDED_ROLES.includes(k)
112
112
  ? { rank_key: k, label: k, is_system: true, seeded_rank: k }
113
113
  : { rank_key: k, label: k, is_system: false, seeded_rank: null }),
114
- createRole: async (a) => ({ rank_key: a.rankKey, label: a.label, description: a.description ?? null, is_system: false, seeded_rank: null, created_by: a.createdBy, created_at: 't' }),
115
- grantRolePermission: async () => true,
116
- revokeRolePermission: async () => true,
117
- resetRoleGrants: async ({ permissionKeys }) => permissionKeys.length,
118
- assignRole: async () => true,
119
- unassignRole: async () => true,
120
- listRolesWithGrants: async () => [],
114
+ createRank: async (a) => ({ rank_key: a.rankKey, label: a.label, description: a.description ?? null, is_system: false, seeded_rank: null, created_by: a.createdBy, created_at: 't' }),
115
+ grantRankPermission: async () => true,
116
+ revokeRankPermission: async () => true,
117
+ resetRankGrants: async ({ permissionKeys }) => permissionKeys.length,
118
+ assignRank: async () => true,
119
+ unassignRank: async () => true,
120
+ listRanksWithGrants: async () => [],
121
121
  listAssignments: async () => [],
122
122
  };
123
123
  const REAL = {};
@@ -138,11 +138,11 @@ function grant(keys) {
138
138
  }
139
139
  grant(['government.manage']);
140
140
 
141
- const buildGovernanceRouter = require(path.join(ROOT, 'modules', 'government', 'routes', 'government.js'));
141
+ const buildGovernmentRouter = require(path.join(ROOT, 'modules', 'government', 'routes', 'government.js'));
142
142
  const app = express();
143
143
  app.use(express.json());
144
144
  app.use(attachFail);
145
- app.use(buildGovernanceRouter());
145
+ app.use(buildGovernmentRouter());
146
146
  const server = http.createServer(app);
147
147
  await new Promise((r) => server.listen(0, '127.0.0.1', r));
148
148
  const BASE = `http://127.0.0.1:${server.address().port}`;
@@ -171,7 +171,7 @@ test('V1: EVERY system permission is refused onto a custom role, before the db',
171
171
  const r = await call('PUT', `/government/ranks/custom-role/permissions/${k}`);
172
172
  assert.equal(r.status, 400, `${k} must be refused`);
173
173
  assert.equal(r.json.error.code, 'permission_not_grantable', k);
174
- assert.equal(calls.filter(([c]) => c === 'grantRolePermission').length, 0,
174
+ assert.equal(calls.filter(([c]) => c === 'grantRankPermission').length, 0,
175
175
  `${k}: the refusal must land BEFORE any db write`);
176
176
  }
177
177
  });
@@ -188,13 +188,13 @@ test('V2a: the ARCHON rank-role refuses EVERY write — grant, revoke and reset
188
188
  const r = await call(method, `/government/ranks/archon/permissions/${k}`);
189
189
  assert.equal(r.status, 403, `archon ← ${method} ${k} must be refused`);
190
190
  assert.equal(r.json.error.code, 'archon_rank_immutable', `archon ← ${method} ${k}`);
191
- assert.equal(calls.filter(([c]) => c === 'grantRolePermission' || c === 'revokeRolePermission').length, 0);
191
+ assert.equal(calls.filter(([c]) => c === 'grantRankPermission' || c === 'revokeRankPermission').length, 0);
192
192
  }
193
193
  }
194
194
  const r = await call('POST', '/government/ranks/archon/reset');
195
195
  assert.equal(r.status, 403, 'reset must not be a back door into the archon role');
196
196
  assert.equal(r.json.error.code, 'archon_rank_immutable');
197
- assert.equal(calls.filter(([c]) => c === 'resetRoleGrants').length, 0);
197
+ assert.equal(calls.filter(([c]) => c === 'resetRankGrants').length, 0);
198
198
  });
199
199
 
200
200
  test('V2b: EVERY editable rank-role still refuses every SUBSTRATE and PRINCIPAL key', async () => {
@@ -204,7 +204,7 @@ test('V2b: EVERY editable rank-role still refuses every SUBSTRATE and PRINCIPAL
204
204
  const r = await call('PUT', `/government/ranks/${role}/permissions/${k}`);
205
205
  assert.equal(r.status, 400, `${role} ← ${k} must be refused`);
206
206
  assert.equal(r.json.error.code, 'permission_not_grantable', `${role} ← ${k}`);
207
- assert.equal(calls.filter(([c]) => c === 'grantRolePermission').length, 0,
207
+ assert.equal(calls.filter(([c]) => c === 'grantRankPermission').length, 0,
208
208
  `${role} ← ${k}: the refusal must land BEFORE any db write`);
209
209
  }
210
210
  }
@@ -215,7 +215,7 @@ test('V2c: an editable rank-role ACCEPTS the in-app owner powers (the ADR 0172 l
215
215
  for (const k of [...OWNER_POWERS, 'idea.triage']) {
216
216
  const r = await call('PUT', `/government/ranks/${role}/permissions/${k}`);
217
217
  assert.equal(r.status, 200, `${role} ← ${k} must be ACCEPTED — this delegation is the owner's decision (ADR 0172)`);
218
- assert.equal(calls.filter(([c]) => c === 'grantRolePermission').length, 1, `${role} ← ${k} must reach the db`);
218
+ assert.equal(calls.filter(([c]) => c === 'grantRankPermission').length, 1, `${role} ← ${k} must reach the db`);
219
219
  }
220
220
  }
221
221
  });
@@ -224,7 +224,7 @@ test('V2d: reset restores the CATALOG seed, never anything wider', async () => {
224
224
  for (const role of EDITABLE_RANK_ROLES) {
225
225
  const r = await call('POST', `/government/ranks/${role}/reset`);
226
226
  assert.equal(r.status, 200, `${role} must be resettable`);
227
- const [, args] = calls.find(([c]) => c === 'resetRoleGrants');
227
+ const [, args] = calls.find(([c]) => c === 'resetRankGrants');
228
228
  assert.deepEqual([...args.permissionKeys].sort(), [...catalog.seedForRank(role)].sort(),
229
229
  `${role} reset must write exactly the floor-derived seed`);
230
230
  for (const k of args.permissionKeys) {
@@ -236,7 +236,7 @@ test('V2d: reset restores the CATALOG seed, never anything wider', async () => {
236
236
  const r = await call('POST', '/government/ranks/custom-role/reset');
237
237
  assert.equal(r.status, 400);
238
238
  assert.equal(r.json.error.code, 'rank_has_no_default');
239
- assert.equal(calls.filter(([c]) => c === 'resetRoleGrants').length, 0);
239
+ assert.equal(calls.filter(([c]) => c === 'resetRankGrants').length, 0);
240
240
  });
241
241
 
242
242
  test('V3: EVERY seeded rank-role refuses assignment (rank is the only writer — R95b)', async () => {
@@ -244,7 +244,7 @@ test('V3: EVERY seeded rank-role refuses assignment (rank is the only writer —
244
244
  const r = await call('POST', '/government/assignments', { builder_id: 7, rank_key: role });
245
245
  assert.equal(r.status, 403, `assigning seeded role ${role} must be refused`);
246
246
  assert.equal(r.json.error.code, 'seeded_rank_assignment_forbidden', role);
247
- assert.equal(calls.filter(([c]) => c === 'assignRole').length, 0);
247
+ assert.equal(calls.filter(([c]) => c === 'assignRank').length, 0);
248
248
  }
249
249
  });
250
250
 
@@ -267,10 +267,10 @@ test('V4: a clean role create is never system and carries no grants', async () =
267
267
  assert.equal(r.json.rank.is_system, false, 'a created role is never system');
268
268
  assert.equal(r.json.rank.seeded_rank, null);
269
269
  assert.deepEqual(r.json.rank.permissions, [], 'creation carries no grants');
270
- const c = calls.find(([k]) => k === 'createRole');
270
+ const c = calls.find(([k]) => k === 'createRank');
271
271
  assert.deepEqual(Object.keys(c[1]).sort(), ['createdBy', 'description', 'label', 'rankKey'],
272
272
  'nothing beyond the four whitelisted fields reaches the db');
273
- assert.equal(calls.filter(([k]) => k === 'grantRolePermission').length, 0);
273
+ assert.equal(calls.filter(([k]) => k === 'grantRankPermission').length, 0);
274
274
  });
275
275
 
276
276
  test('V5: a caller without government.manage cannot reach ANY write vector at all', async () => {
@@ -295,35 +295,35 @@ test('V5: a caller without government.manage cannot reach ANY write vector at al
295
295
  // ===========================================================================
296
296
 
297
297
  test('V6: a config template proposing EVERY system key resolves with all of them stripped', () => {
298
- const resolved = govConfig.resolveGovernanceConfig({
299
- instance: { roles: { 'evil-template': { label: 'Evil', permissions: [...SYSTEM_KEYS, 'no.such.key', 'idea.triage'] } } },
298
+ const resolved = govConfig.resolveGovernmentConfig({
299
+ instance: { ranks: { 'evil-template': { label: 'Evil', permissions: [...SYSTEM_KEYS, 'no.such.key', 'idea.triage'] } } },
300
300
  env: {},
301
301
  });
302
- const evil = resolved.roles['evil-template'];
302
+ const evil = resolved.ranks['evil-template'];
303
303
  assert.deepEqual([...evil.permissions], ['idea.triage'],
304
304
  'only the operational key survives; every system + unknown key is stripped');
305
305
  });
306
306
 
307
307
  test('V6: a seeded rank-role template cannot re-grant — its permissions drop wholesale', () => {
308
- const resolved = govConfig.resolveGovernanceConfig({
309
- instance: { roles: { thetes: { label: 'Upgraded', permissions: ['idea.triage', ...SYSTEM_KEYS] } } },
308
+ const resolved = govConfig.resolveGovernmentConfig({
309
+ instance: { ranks: { thetes: { label: 'Upgraded', permissions: ['idea.triage', ...SYSTEM_KEYS] } } },
310
310
  env: {},
311
311
  });
312
- assert.deepEqual([...resolved.roles.thetes.permissions], [],
312
+ assert.deepEqual([...resolved.ranks.thetes.permissions], [],
313
313
  'a seeded role takes NO permissions from config, operational ones included');
314
314
  });
315
315
 
316
316
  test('V6: the env layer relabels only — it cannot add a permission or a role', () => {
317
317
  // The role must exist in the merged config for the relabel to land (an env
318
318
  // var can only RELABEL a declared role) — mirror the neutral starter's shape.
319
- const resolved = govConfig.resolveGovernanceConfig({
320
- neutral: { roles: { metic: { label: 'Metic' } } },
319
+ const resolved = govConfig.resolveGovernmentConfig({
320
+ neutral: { ranks: { metic: { label: 'Metic' } } },
321
321
  instance: {},
322
322
  env: { OTB_GOVERNANCE_ROLE_LABELS: 'metic=Overlord,ghost-role=Ghost' },
323
323
  });
324
- assert.equal(resolved.roles.metic.label, 'Overlord', 'relabeling works');
325
- assert.equal(resolved.roles['ghost-role'], undefined, 'an env var cannot mint a role');
326
- assert.deepEqual([...resolved.roles.metic.permissions], [],
324
+ assert.equal(resolved.ranks.metic.label, 'Overlord', 'relabeling works');
325
+ assert.equal(resolved.ranks['ghost-role'], undefined, 'an env var cannot mint a role');
326
+ assert.deepEqual([...resolved.ranks.metic.permissions], [],
327
327
  'the relabeled seeded role still takes no grants from config or env');
328
328
  });
329
329
 
@@ -438,7 +438,7 @@ test('V11: the substrate checker is structurally grant-blind (node builtins only
438
438
 
439
439
  test('V12: no rank-role below archon holds a system key; none holds a principal key', () => {
440
440
  for (const rank of SEEDED_ROLES) {
441
- const seed = catalog.RANK_ROLE_SEED[rank];
441
+ const seed = catalog.RANK_SEED[rank];
442
442
  for (const k of seed) {
443
443
  assert.equal(catalog.isPrincipalKey(k), false, `${rank} seeds principal key ${k}`);
444
444
  if (rank !== 'archon') {
@@ -446,7 +446,7 @@ test('V12: no rank-role below archon holds a system key; none holds a principal
446
446
  }
447
447
  }
448
448
  }
449
- const archonSystem = catalog.RANK_ROLE_SEED.archon.filter((k) => catalog.isSystem(k)).sort();
449
+ const archonSystem = catalog.RANK_SEED.archon.filter((k) => catalog.isSystem(k)).sort();
450
450
  const expected = SYSTEM_KEYS.filter((k) => catalog.byKey(k).floor === 'archon').sort();
451
451
  assert.deepEqual(archonSystem, expected,
452
452
  'archon holds exactly the archon-floored system keys — principals stay out of every rank-role');
@@ -1,4 +1,4 @@
1
- // tests/governance_catalog.mjs — the permission catalog + compat seed (BV1.R94, ADR 0151).
1
+ // tests/government_catalog.mjs — the permission catalog + compat seed (BV1.R94, ADR 0151).
2
2
  //
3
3
  // Guards the "zero day-one behavior change" promise structurally: the seed is
4
4
  // DERIVED from per-permission floors, so these invariants prove the derivation is
@@ -12,7 +12,7 @@ import { fileURLToPath } from 'node:url';
12
12
  const require = createRequire(import.meta.url);
13
13
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
14
14
  const cat = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
15
- const { PERMISSIONS, RANK_ORDER, RANK_ROLE_SEED, permissionKeys, byKey, isSystem, isGrantable, systemKeys, grantableKeys, seedForRank } = cat;
15
+ const { PERMISSIONS, RANK_ORDER, RANK_SEED, permissionKeys, byKey, isSystem, isGrantable, systemKeys, grantableKeys, seedForRank } = cat;
16
16
 
17
17
  test('every permission is well-formed (key, boolean system, valid floor)', () => {
18
18
  assert.ok(PERMISSIONS.length >= 60, `expected a substantial catalog, saw ${PERMISSIONS.length}`);
@@ -98,17 +98,17 @@ test('isGrantableTo fails closed on every unrecognised input', () => {
98
98
  });
99
99
 
100
100
  test('the four rank-roles exist; seed keys all reference real permissions', () => {
101
- assert.deepEqual(Object.keys(RANK_ROLE_SEED).sort(), [...RANK_ORDER].sort());
101
+ assert.deepEqual(Object.keys(RANK_SEED).sort(), [...RANK_ORDER].sort());
102
102
  const all = new Set(permissionKeys());
103
103
  for (const rank of RANK_ORDER) {
104
- for (const k of RANK_ROLE_SEED[rank]) assert.ok(all.has(k), `seed[${rank}] references unknown ${k}`);
104
+ for (const k of RANK_SEED[rank]) assert.ok(all.has(k), `seed[${rank}] references unknown ${k}`);
105
105
  }
106
106
  });
107
107
 
108
108
  test('seed is cumulative: archon ⊇ metic ⊇ thetes ⊇ xenos (nested supersets)', () => {
109
109
  for (let i = 1; i < RANK_ORDER.length; i++) {
110
- const lower = new Set(RANK_ROLE_SEED[RANK_ORDER[i - 1]]);
111
- const higher = new Set(RANK_ROLE_SEED[RANK_ORDER[i]]);
110
+ const lower = new Set(RANK_SEED[RANK_ORDER[i - 1]]);
111
+ const higher = new Set(RANK_SEED[RANK_ORDER[i]]);
112
112
  for (const k of lower) assert.ok(higher.has(k), `${RANK_ORDER[i]} missing ${k} that ${RANK_ORDER[i - 1]} has`);
113
113
  assert.ok(higher.size > lower.size, `${RANK_ORDER[i]} should strictly extend ${RANK_ORDER[i - 1]}`);
114
114
  }
@@ -116,7 +116,7 @@ test('seed is cumulative: archon ⊇ metic ⊇ thetes ⊇ xenos (nested superset
116
116
 
117
117
  test('a role holds a permission iff its floor is a real rank at-or-below (derivation is faithful)', () => {
118
118
  for (const rank of RANK_ORDER) {
119
- const held = new Set(RANK_ROLE_SEED[rank]);
119
+ const held = new Set(RANK_SEED[rank]);
120
120
  const tier = RANK_ORDER.indexOf(rank);
121
121
  for (const p of PERMISSIONS) {
122
122
  const shouldHold = p.floor !== null && RANK_ORDER.indexOf(p.floor) <= tier;
@@ -127,12 +127,12 @@ test('a role holds a permission iff its floor is a real rank at-or-below (deriva
127
127
 
128
128
  test('system permissions are Archon-only in the seed (never in xenos/thetes/metic)', () => {
129
129
  for (const rank of ['xenos', 'thetes', 'metic']) {
130
- for (const k of RANK_ROLE_SEED[rank]) {
130
+ for (const k of RANK_SEED[rank]) {
131
131
  assert.equal(isSystem(k), false, `${rank} seed must not hold system perm ${k}`);
132
132
  }
133
133
  }
134
134
  // and archon DOES hold the archon-floored system perms (that is how Archon gets its power)
135
- const archon = new Set(RANK_ROLE_SEED.archon);
135
+ const archon = new Set(RANK_SEED.archon);
136
136
  assert.ok(archon.has('builder.rank.set'));
137
137
  assert.ok(archon.has('deploy.push_main'));
138
138
  });
@@ -142,14 +142,14 @@ test('machine principals are held by NO rank-role and are system + floor null',
142
142
  assert.equal(p.system, true, `${p.key} must be system`);
143
143
  assert.equal(p.floor, null, `${p.key} must have floor null (held by no rank-role)`);
144
144
  for (const rank of RANK_ORDER) {
145
- assert.ok(!RANK_ROLE_SEED[rank].includes(p.key), `${rank} must not hold principal ${p.key}`);
145
+ assert.ok(!RANK_SEED[rank].includes(p.key), `${rank} must not hold principal ${p.key}`);
146
146
  }
147
147
  }
148
148
  assert.ok(PERMISSIONS.some((p) => p.key === 'principal.bfg'));
149
149
  });
150
150
 
151
151
  test('threshold spot-checks reproduce today\'s requireRank floors', () => {
152
- const has = (rank, k) => RANK_ROLE_SEED[rank].includes(k);
152
+ const has = (rank, k) => RANK_SEED[rank].includes(k);
153
153
  // idea.triage: metic+ only (requireRank('metic','archon'))
154
154
  assert.equal(has('xenos', 'idea.triage'), false);
155
155
  assert.equal(has('thetes', 'idea.triage'), false);
@@ -1,4 +1,4 @@
1
- // tests/governance_claim_eligibility.mjs — claim eligibility as PERMISSIONS
1
+ // tests/government_claim_eligibility.mjs — claim eligibility as PERMISSIONS
2
2
  // (BV1.R106, ADR 0151).
3
3
  //
4
4
  // DB-free. The whole point of R106 is that the queue half of claim-time authority
@@ -23,7 +23,7 @@
23
23
  // GENERAL_QUEUE_RANKS and knows nothing about catalog floors.
24
24
  // The new side reads catalog.seedForRank. A check whose two sides both read
25
25
  // catalog.floor would pass while proving nothing (the lesson tests/
26
- // governance_parity.mjs writes down); neither oracle here does.
26
+ // government_parity.mjs writes down); neither oracle here does.
27
27
  import assert from 'node:assert/strict';
28
28
  import { test } from 'node:test';
29
29
  import { createRequire } from 'node:module';