@bongos/core 1.19.185 → 1.19.187

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.bongos-core.json +90 -85
  2. package/clients/bongos-client/index.d.ts +2 -2
  3. package/config/government.neutral.json +6 -6
  4. package/docs/adr/0172-editable-rank-roles-substrate-axis.md +1 -1
  5. package/docs/api/openapi.json +12 -12
  6. package/docs/api-reference.md +7 -7
  7. package/modules/builder-settings/wandering-prefs.js +13 -13
  8. package/modules/government/catalog.js +79 -79
  9. package/modules/government/config.js +43 -33
  10. package/modules/government/db.js +58 -58
  11. package/modules/government/protected-surfaces.js +7 -7
  12. package/modules/government/protected-surfaces.json +4 -4
  13. package/modules/government/ranks.js +17 -17
  14. package/modules/government/resolver.js +24 -24
  15. package/modules/government/routes/government.js +88 -88
  16. package/modules/government/routes/rank-sync.js +8 -8
  17. package/modules/government/session-scopes.js +7 -7
  18. package/modules/government/session-scopes.json +5 -5
  19. package/modules/hall-ui/public/gate.js +2 -2
  20. package/modules/hall-ui/public/{governance.css → government.css} +11 -11
  21. package/modules/hall-ui/public/{governance.html → government.html} +14 -14
  22. package/modules/hall-ui/public/{governance.js → government.js} +108 -108
  23. package/modules/hall-ui/public/harbor.js +2 -2
  24. package/modules/hall-ui/public/shell.js +19 -15
  25. package/modules/hall-ui/public/watch.js +1 -1
  26. package/modules/lifecycle/claim-eligibility.js +1 -1
  27. package/modules/lifecycle/db.js +1 -1
  28. package/package-lock.json +2 -2
  29. package/package.json +1 -1
  30. package/scripts/gds/fitness.js +138 -0
  31. package/scripts/hall-preview/server.js +1 -1
  32. package/src/bongos/auth.js +11 -11
  33. package/src/bongos/serve-internal.js +8 -8
  34. package/src/module-api.js +5 -1
  35. package/tests/auth_page_gate.mjs +2 -2
  36. package/tests/auth_resolve_failure.mjs +4 -4
  37. package/tests/{governance_abuse_matrix.mjs → government_abuse_matrix.mjs} +39 -39
  38. package/tests/{governance_catalog.mjs → government_catalog.mjs} +11 -11
  39. package/tests/{governance_claim_eligibility.mjs → government_claim_eligibility.mjs} +2 -2
  40. package/tests/{governance_config.mjs → government_config.mjs} +54 -54
  41. package/tests/{governance_fold.mjs → government_fold.mjs} +6 -6
  42. package/tests/{governance_fold_bench.mjs → government_fold_bench.mjs} +3 -3
  43. package/tests/{governance_live_proof.mjs → government_live_proof.mjs} +5 -5
  44. package/tests/{governance_ownership_scope.mjs → government_ownership_scope.mjs} +16 -16
  45. package/tests/{governance_page_gates.mjs → government_page_gates.mjs} +14 -14
  46. package/tests/{governance_parity.mjs → government_parity.mjs} +3 -3
  47. package/tests/{governance_principals.mjs → government_principals.mjs} +14 -14
  48. package/tests/{governance_protected_surfaces.mjs → government_protected_surfaces.mjs} +1 -1
  49. package/tests/{governance_role_sync.mjs → government_rank_sync.mjs} +2 -2
  50. package/tests/{governance_require_permission.mjs → government_require_permission.mjs} +1 -1
  51. package/tests/{governance_resolver.mjs → government_resolver.mjs} +6 -6
  52. package/tests/{governance_routes.mjs → government_routes.mjs} +36 -36
  53. package/tests/{governance_seed.mjs → government_seed.mjs} +8 -8
  54. package/tests/{governance_session_scope.mjs → government_session_scope.mjs} +10 -10
  55. package/tests/{governance_vanilla_boot.mjs → government_vanilla_boot.mjs} +26 -26
  56. package/tests/government_vocabulary.mjs +94 -0
  57. package/tests/hall_nav.mjs +8 -7
  58. package/tests/hall_page_gate_map.mjs +9 -9
@@ -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,
@@ -44,7 +44,7 @@ const EMPTY = process.argv.includes('--empty');
44
44
  // --rank: view the hall as a different rank — the /me fixture's builder.rank is
45
45
  // overridden in the response, so the Archon-gated pages (watch, harbor, gate)
46
46
  // render their real content instead of the sealed notice. Preview-only theatre:
47
- // the real pages are server-gated (requireGovernancePage / ADR 0016).
47
+ // the real pages are server-gated (requireGovernmentPage / ADR 0016).
48
48
  const RANK_OVERRIDE = arg('--rank');
49
49
  const MODULES_OFF = String(arg('--modules-off') || '').split(',').map((s) => s.trim()).filter(Boolean);
50
50
  // --modules-on: force-report modules as enabled — a dev box's local config
@@ -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
@@ -1437,12 +1437,12 @@ function signInWithReturn(req) {
1437
1437
  // ranks (catalog.RANK_ORDER):
1438
1438
  // requireBuilderPage was "any session" floor xenos → page.view.public
1439
1439
  // requireNonXenosPage was rank !== 'xenos' floor thetes → page.view.builder
1440
- // requireGovernancePage was rank === 'archon' floor archon → page.view.government
1440
+ // requireGovernmentPage was rank === 'archon' floor archon → page.view.government
1441
1441
  // That third gate was spelled `requireArchonPage` until ADR 0157 lowered
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
  //
@@ -1456,8 +1456,8 @@ function signInWithReturn(req) {
1456
1456
  const PAGE_GATES = Object.freeze({
1457
1457
  requireBuilderPage: Object.freeze({ permission: 'page.view.public', deniedTo: '/builders' }),
1458
1458
  requireNonXenosPage: Object.freeze({ permission: 'page.view.builder', deniedTo: '/builders/not-ready' }),
1459
- requireGovernancePage: Object.freeze({ permission: 'page.view.government', deniedTo: '/builders' }),
1460
- requireGovernanceManagePage: Object.freeze({ permission: 'government.manage', deniedTo: '/builders' }),
1459
+ requireGovernmentPage: Object.freeze({ permission: 'page.view.government', deniedTo: '/builders' }),
1460
+ requireGovernmentManagePage: Object.freeze({ permission: 'government.manage', deniedTo: '/builders' }),
1461
1461
  requireProjectCuratePage: Object.freeze({ permission: 'project.curate', deniedTo: '/builders' }),
1462
1462
  });
1463
1463
 
@@ -1540,8 +1540,8 @@ async function requireBuilderPage(req, res, next) {
1540
1540
  // every data endpoint these pages call is independently gated server-side, so no
1541
1541
  // cross-builder data leaks even if this gate were bypassed; it keeps a builder
1542
1542
  // without the permission from ever seeing the page shell.
1543
- async function requireGovernancePage(req, res, next) {
1544
- return runPageGate('requireGovernancePage', req, res, next);
1543
+ async function requireGovernmentPage(req, res, next) {
1544
+ return runPageGate('requireGovernmentPage', req, res, next);
1545
1545
  }
1546
1546
 
1547
1547
  // The /governance roles-and-permissions tab (BV1.R113) — gated on the
@@ -1549,8 +1549,8 @@ async function requireGovernancePage(req, res, next) {
1549
1549
  // /governance/* data route checks, so the page layer can never drift wider than
1550
1550
  // the API it fronts. Defense-in-depth as above: the data endpoints are all
1551
1551
  // independently gated; this keeps the tab shell from rendering for anyone else.
1552
- async function requireGovernanceManagePage(req, res, next) {
1553
- return runPageGate('requireGovernanceManagePage', req, res, next);
1552
+ async function requireGovernmentManagePage(req, res, next) {
1553
+ return runPageGate('requireGovernmentManagePage', req, res, next);
1554
1554
  }
1555
1555
 
1556
1556
  // The /people builder-directory page (task 1002577) — gated on the
@@ -1827,8 +1827,8 @@ module.exports = {
1827
1827
  SESSION_SCOPES,
1828
1828
  requireBuilder,
1829
1829
  requireBuilderPage,
1830
- requireGovernancePage,
1831
- requireGovernanceManagePage,
1830
+ requireGovernmentPage,
1831
+ requireGovernmentManagePage,
1832
1832
  requireNonXenosPage,
1833
1833
  requireProjectCuratePage,
1834
1834
  requireRank,
@@ -751,7 +751,7 @@ function mountInternalSurfaces(app) {
751
751
  // The /watch monitoring page and the /harbor dev-box management page (idea 257)
752
752
  // and the /gate page (task 1179) all sit behind `page.view.government` — Metic
753
753
  // and up since ADR 0157. Same server-side page gate, same posture: a builder
754
- // without the permission never receives the shell (requireGovernancePage — named
754
+ // without the permission never receives the shell (requireGovernmentPage — named
755
755
  // requireArchonPage before ADR 0157 — redirects them to the hall, and an
756
756
  // unauthenticated visitor into sign-in). The subdomain shim above has already
757
757
  // rewritten the bare /watch + /harbor + /gate onto /builders/*. (Defense in
@@ -766,13 +766,13 @@ function mountInternalSurfaces(app) {
766
766
  // data never leaked (the route gate held), but the page was the one item in the
767
767
  // group without a wall. Gated here so the group is uniform (task 1002710).
768
768
  const SESSIONS_PAGE_RE = /^\/builders\/sessions(?:\.html)?\/?$/;
769
- // The /governance roles-and-permissions tab (BV1.R113) sits one floor higher:
769
+ // The /government ranks-and-permissions tab (BV1.R113) sits one floor higher:
770
770
  // the `government.manage` atom itself (archon), the same key its data routes
771
- // check — requireGovernanceManagePage, not the metic-floored gate above.
772
- const GOVERNANCE_PAGE_RE = /^\/builders\/governance(?:\.html)?\/?$/;
771
+ // check — requireGovernmentManagePage, not the metic-floored gate above.
772
+ const GOVERNMENT_PAGE_RE = /^\/builders\/government(?:\.html)?\/?$/;
773
773
  // The /people builder directory (task 1002577): gated on `project.curate`, the
774
774
  // exact permission its data route (GET /scouting) checks — same posture as the
775
- // /governance tab above (the page can never drift wider than the API it fronts).
775
+ // /government tab above (the page can never drift wider than the API it fronts).
776
776
  const PEOPLE_PAGE_RE = /^\/builders\/people(?:\.html)?\/?$/;
777
777
  // /scouting is the LEGACY twin of /people — the same /api/gds/scouting directory,
778
778
  // left behind when task 1002577 graduated it into the hall. It has no nav entry
@@ -787,10 +787,10 @@ function mountInternalSurfaces(app) {
787
787
  const norm = path.posix.normalize(req.path);
788
788
  if (WATCH_PAGE_RE.test(norm) || HARBOR_PAGE_RE.test(norm) || GATE_PAGE_RE.test(norm)
789
789
  || SESSIONS_PAGE_RE.test(norm)) {
790
- return gdsAuth.requireGovernancePage(req, res, next);
790
+ return gdsAuth.requireGovernmentPage(req, res, next);
791
791
  }
792
- if (GOVERNANCE_PAGE_RE.test(norm)) {
793
- return gdsAuth.requireGovernanceManagePage(req, res, next);
792
+ if (GOVERNMENT_PAGE_RE.test(norm)) {
793
+ return gdsAuth.requireGovernmentManagePage(req, res, next);
794
794
  }
795
795
  if (PEOPLE_PAGE_RE.test(norm) || SCOUTING_PAGE_RE.test(norm)) {
796
796
  return gdsAuth.requireProjectCuratePage(req, res, next);
package/src/module-api.js CHANGED
@@ -797,8 +797,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
797
797
  // landed since 1.19.183 with no explicit bump. run 31974847472. (task 1002620)
798
798
  // 1.19.185 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
799
799
  // landed since 1.19.184 with no explicit bump. run 31976564067. (task 1002620)
800
+ // 1.19.186 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
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)
800
804
  // ---------------------------------------------------------------------------
801
- const CORE_VERSION = '1.19.185'; // CI auto-patch carrier (ADR 0161); see changelog above
805
+ const CORE_VERSION = '1.19.187'; // CI auto-patch carrier (ADR 0161); see changelog above
802
806
 
803
807
  // A namespaced logger so a module's log lines are attributable + consistent.
804
808
  // 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();
@@ -93,17 +93,17 @@ test('AUDIT: every async middleware mounted directly on a router owns its reject
93
93
  // rot: a NEW bare `async function require*` added later fails this test.
94
94
  // requireBuilder — the API gate. Guarded here (BV1.R109-follow-up).
95
95
  // requireBuilderPage \
96
- // requireGovernancePage \
97
- // requireGovernanceManagePage > all five delegate to runPageGate, guarded by BV1.R107.
96
+ // requireGovernmentPage \
97
+ // requireGovernmentManagePage > all five delegate to runPageGate, guarded by BV1.R107.
98
98
  // requireNonXenosPage / (the R113 roles-tab gate; the /people curate gate)
99
99
  // requireProjectCuratePage /
100
100
  // Anything else async and router-mounted must be added with its own try/catch.
101
101
  const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'auth.js'), 'utf8');
102
102
  const asyncMw = [...src.matchAll(/^async function (require\w+|allow\w+)\s*\(req, res, next\)/gm)].map((m) => m[1]);
103
- assert.deepEqual(asyncMw.sort(), ['requireBuilder', 'requireBuilderPage', 'requireGovernanceManagePage', 'requireGovernancePage', 'requireNonXenosPage', 'requireProjectCuratePage'],
103
+ assert.deepEqual(asyncMw.sort(), ['requireBuilder', 'requireBuilderPage', 'requireGovernmentManagePage', 'requireGovernmentPage', 'requireNonXenosPage', 'requireProjectCuratePage'],
104
104
  'a new async router middleware appeared — give it a try/catch and add it here');
105
105
  // The page gates must actually delegate (that is WHY they are safe).
106
- for (const name of ['requireBuilderPage', 'requireGovernancePage', 'requireGovernanceManagePage', 'requireNonXenosPage', 'requireProjectCuratePage']) {
106
+ for (const name of ['requireBuilderPage', 'requireGovernmentPage', 'requireGovernmentManagePage', 'requireNonXenosPage', 'requireProjectCuratePage']) {
107
107
  const fn = src.slice(src.indexOf(`async function ${name}`));
108
108
  assert.match(sliceFn(fn), /runPageGate\(/, `${name} must delegate to the guarded runPageGate`);
109
109
  }
@@ -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
 
@@ -264,13 +264,13 @@ test('V4: mass-assignment on role create is refused outright (strict schema)', a
264
264
  test('V4: a clean role create is never system and carries no grants', async () => {
265
265
  const r = await call('POST', '/government/ranks', { rank_key: 'sneaky-role', label: 'Sneaky' });
266
266
  assert.equal(r.status, 201);
267
- assert.equal(r.json.role.is_system, false, 'a created role is never system');
268
- assert.equal(r.json.role.seeded_rank, null);
269
- assert.deepEqual(r.json.role.permissions, [], 'creation carries no grants');
270
- const c = calls.find(([k]) => k === 'createRole');
267
+ assert.equal(r.json.rank.is_system, false, 'a created role is never system');
268
+ assert.equal(r.json.rank.seeded_rank, null);
269
+ assert.deepEqual(r.json.rank.permissions, [], 'creation carries no grants');
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';