@bongos/core 1.19.184 → 1.19.186

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 +68 -63
  2. package/clients/bongos-client/bongos-client.global.js +19 -19
  3. package/clients/bongos-client/index.cjs +19 -19
  4. package/clients/bongos-client/index.d.ts +29 -29
  5. package/clients/bongos-client/index.mjs +19 -19
  6. package/docs/adr/0157-archon-is-rank-and-identity-only.md +2 -2
  7. package/docs/api/openapi.json +121 -121
  8. package/docs/api-reference.md +10 -10
  9. package/docs/canonical-permissions.md +6 -6
  10. package/modules/dev-box/app/src/vendor/bongos-client.cjs +19 -19
  11. package/modules/government/catalog.js +9 -9
  12. package/modules/government/db.js +9 -9
  13. package/modules/government/migrations/government_002_rename_permission_keys.sql +112 -0
  14. package/modules/government/module.json +1 -1
  15. package/modules/government/protected-surfaces.json +0 -1
  16. package/modules/government/ranks.js +13 -13
  17. package/modules/government/routes/{governance.js → government.js} +109 -84
  18. package/modules/government/routes/rank-sync.js +2 -2
  19. package/modules/hall-ui/public/gate.js +3 -3
  20. package/modules/hall-ui/public/{governance.css → government.css} +10 -10
  21. package/modules/hall-ui/public/{governance.html → government.html} +14 -14
  22. package/modules/hall-ui/public/{governance.js → government.js} +117 -117
  23. package/modules/hall-ui/public/harbor.js +3 -3
  24. package/modules/hall-ui/public/shell.js +21 -17
  25. package/modules/hall-ui/public/watch.js +2 -2
  26. package/modules/lifecycle/routes/gate-approvals.js +1 -1
  27. package/package-lock.json +2 -2
  28. package/package.json +1 -1
  29. package/scripts/gds/publish-manifest.js +0 -2
  30. package/scripts/gds/seed-governance-tasks.js +1 -1
  31. package/scripts/hall-preview/server.js +1 -1
  32. package/src/bongos/auth.js +13 -13
  33. package/src/bongos/module-scope-map.js +7 -22
  34. package/src/bongos/route-rank-check.js +12 -18
  35. package/src/bongos/serve-internal.js +10 -10
  36. package/src/module-api.js +6 -2
  37. package/tests/auth_resolve_failure.mjs +4 -4
  38. package/tests/governance_abuse_matrix.mjs +35 -35
  39. package/tests/governance_catalog.mjs +3 -3
  40. package/tests/governance_fold.mjs +2 -2
  41. package/tests/governance_fold_bench.mjs +2 -2
  42. package/tests/governance_live_proof.mjs +4 -4
  43. package/tests/governance_ownership_scope.mjs +1 -1
  44. package/tests/governance_page_gates.mjs +12 -12
  45. package/tests/governance_principals.mjs +2 -2
  46. package/tests/governance_protected_surfaces.mjs +41 -16
  47. package/tests/governance_require_permission.mjs +5 -5
  48. package/tests/governance_resolver.mjs +2 -2
  49. package/tests/governance_role_sync.mjs +23 -23
  50. package/tests/governance_routes.mjs +96 -96
  51. package/tests/governance_seed.mjs +24 -9
  52. package/tests/governance_session_scope.mjs +4 -4
  53. package/tests/hall_nav.mjs +8 -7
  54. package/tests/hall_page_gate_map.mjs +8 -8
@@ -50,15 +50,15 @@ const resolver = require('../modules/government/resolver.js');
50
50
  const wandering = require('../modules/builder-settings/wandering-prefs.js');
51
51
  const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
52
52
 
53
- const GATES = ['requireBuilderPage', 'requireNonXenosPage', 'requireGovernancePage'];
53
+ const GATES = ['requireBuilderPage', 'requireNonXenosPage', 'requireGovernmentPage'];
54
54
  const LIVE_RANKS = ['xenos', 'thetes', 'metic', 'archon'];
55
55
 
56
56
  // ═══ the ORACLE — hand-written, from the rank tests R107 deleted ═════════════
57
57
  // requireBuilderPage admitted any session at all → every live rank
58
58
  // requireNonXenosPage admitted rank !== 'xenos' → thetes and up
59
- // requireGovernancePage admitted rank === 'archon' → archon only …until ADR 0157
59
+ // requireGovernmentPage admitted rank === 'archon' → archon only …until ADR 0157
60
60
  //
61
- // ADR 0157 (owner decision, 2026-08-05) lowered `page.view.governance` archon→metic, so
61
+ // ADR 0157 (owner decision, 2026-08-05) lowered `page.view.government` archon→metic, so
62
62
  // the monitoring pages (/watch, /harbor, /gate) now open to the working rank. That gate
63
63
  // was spelled `requireArchonPage` through R107 and ADR 0157 — the name the rows below
64
64
  // were first written against — and was renamed once the floor moved; the audience here,
@@ -71,7 +71,7 @@ const LIVE_RANKS = ['xenos', 'thetes', 'metic', 'archon'];
71
71
  const PRE_R107_AUDIENCE = {
72
72
  requireBuilderPage: { xenos: true, thetes: true, metic: true, archon: true },
73
73
  requireNonXenosPage: { xenos: false, thetes: true, metic: true, archon: true },
74
- requireGovernancePage: { xenos: false, thetes: false, metic: true, archon: true },
74
+ requireGovernmentPage: { xenos: false, thetes: false, metic: true, archon: true },
75
75
  };
76
76
  // Where a SIGNED-IN builder who fails the gate is sent. Never the sign-in flow —
77
77
  // they already have a session, so that would loop. Since task 1002613 (map N-1)
@@ -81,12 +81,12 @@ const PRE_R107_AUDIENCE = {
81
81
  const DENIED_TO = {
82
82
  requireBuilderPage: '/builders',
83
83
  requireNonXenosPage: '/builders/not-ready',
84
- requireGovernancePage: '/builders',
84
+ requireGovernmentPage: '/builders',
85
85
  };
86
86
  const GATE_PERMISSION = {
87
87
  requireBuilderPage: 'page.view.public',
88
88
  requireNonXenosPage: 'page.view.builder',
89
- requireGovernancePage: 'page.view.governance',
89
+ requireGovernmentPage: 'page.view.government',
90
90
  };
91
91
  const deniedLoc = (gate, { unavailable = false } = {}) =>
92
92
  `${DENIED_TO[gate]}?denied=${GATE_PERMISSION[gate]}${unavailable ? '&reason=unavailable' : ''}`;
@@ -94,8 +94,8 @@ const deniedLoc = (gate, { unavailable = false } = {}) =>
94
94
  const GATE_FLOOR = {
95
95
  requireBuilderPage: 'xenos',
96
96
  requireNonXenosPage: 'thetes',
97
- // ADR 0157: `page.view.governance` floor archon→metic.
98
- requireGovernancePage: 'metic',
97
+ // ADR 0157: `page.view.government` floor archon→metic.
98
+ requireGovernmentPage: 'metic',
99
99
  };
100
100
  const SIGN_IN = '/api/gds/auth/web/start';
101
101
 
@@ -249,22 +249,22 @@ test('B4: the page gates resolve per request — no memoised authority (ADR 0016
249
249
  });
250
250
  const req = { headers: { cookie: 'gds_session=validtoken' } };
251
251
  let admitted = false;
252
- await auth.requireGovernancePage(req, fakeRes(), () => { admitted = true; });
252
+ await auth.requireGovernmentPage(req, fakeRes(), () => { admitted = true; });
253
253
  assert.equal(admitted, true, 'an Archon should reach the page');
254
254
  keys = []; // the role is revoked between requests
255
255
  const second = fakeRes();
256
256
  let admittedAgain = false;
257
- await auth.requireGovernancePage(req, second, () => { admittedAgain = true; });
257
+ await auth.requireGovernmentPage(req, second, () => { admittedAgain = true; });
258
258
  seams._reset();
259
259
  assert.equal(calls, 2, 'the resolver must be consulted on every request');
260
260
  assert.equal(admittedAgain, false, 'a revoked role must deny on the very next request');
261
- assert.equal(second._redirect.loc, deniedLoc('requireGovernancePage'));
261
+ assert.equal(second._redirect.loc, deniedLoc('requireGovernmentPage'));
262
262
  });
263
263
 
264
264
  test('B5: every permission a page gate names is a real catalog key', async () => {
265
265
  // Not an oracle — a sanity check that the gates did not migrate onto a typo,
266
266
  // which would silently deny everyone (fail-closed, but a bug all the same).
267
- for (const key of ['page.view.public', 'page.view.builder', 'page.view.governance']) {
267
+ for (const key of ['page.view.public', 'page.view.builder', 'page.view.government']) {
268
268
  assert.ok(catalog.byKey(key), `${key} must exist in the catalog`);
269
269
  }
270
270
  });
@@ -149,7 +149,7 @@ test('resolved principal sets are frozen — a caller cannot widen one', () => {
149
149
  // ── show, never grant ───────────────────────────────────────────────────────────
150
150
 
151
151
  test('the tab surface is a read-only projection: shown, flagged non-grantable', () => {
152
- const { governancePort } = require(path.join(GOV, 'routes', 'governance.js'));
152
+ const { governancePort } = require(path.join(GOV, 'routes', 'government.js'));
153
153
  assert.equal(typeof governancePort.listPrincipals, 'function');
154
154
  assert.equal(typeof governancePort.resolvePrincipalPermissions, 'function');
155
155
  const listed = governancePort.listPrincipals();
@@ -162,7 +162,7 @@ test('the tab surface is a read-only projection: shown, flagged non-grantable',
162
162
  });
163
163
 
164
164
  test('the governance port exposes no way to create, grant, or assign a principal', () => {
165
- const { governancePort } = require(path.join(GOV, 'routes', 'governance.js'));
165
+ const { governancePort } = require(path.join(GOV, 'routes', 'government.js'));
166
166
  const mutator = /(create|grant|assign|add|set|write|provision|revoke|delete|update)/i;
167
167
  for (const name of Object.keys(governancePort)) {
168
168
  if (!/principal/i.test(name)) continue;
@@ -92,22 +92,34 @@ const EXPECTED_GLOBS_BEFORE_R101 = [
92
92
  // Each entry is { after, glob, why }: `after` is the glob it is inserted
93
93
  // directly below, so the expected list is reconstructed in order rather than
94
94
  // re-pasted (a re-paste is how the two lists drift apart).
95
- const ADDED_SINCE_R101 = [
95
+ const ADDED_SINCE_R101 = [];
96
+
97
+ // Surfaces RENAMED since R101 — still protected, under a new path. Distinct from
98
+ // a removal on purpose: a rename keeps the protection and moves it, a removal
99
+ // ends it. The subset test below accepts a documented rename and rejects a bare
100
+ // disappearance, so "we renamed it" can never be used to quietly unprotect
101
+ // something — the replacement must actually be in the registry.
102
+ const RENAMED_SINCE_R101 = [
96
103
  {
97
- after: 'modules/governance/',
98
- glob: 'modules/government/',
99
- why: 'goal 1000068 / task 1003003 (ADR 0174) — the authz core is renamed '
100
- + 'governance→government. The new glob is registered BEFORE the folder '
101
- + 'moves because `protected` DERIVES from this registry: without it the '
102
- + 'new module key computes protected:false and both the Archon gate on '
103
- + 'goal creation and the pre-push block on these files would silently lapse.',
104
+ from: 'modules/governance/',
105
+ to: 'modules/government/',
106
+ why: 'goal 1000068 / ADR 0174 — the authz core was renamed governance→government. '
107
+ + 'The folder moved in task 1003004 and the one-release compatibility glob was '
108
+ + 'dropped in R05 (task 1003006) along with the /governance/* route aliases. '
109
+ + 'Protection did not lapse at any point: the new glob was registered BEFORE '
110
+ + 'the folder moved (task 1003003), so both were listed across the window.',
104
111
  },
105
112
  ];
106
113
 
107
114
  // The list the registry must produce TODAY: the pre-R101 set with each
108
115
  // documented addition spliced in at its declared position.
109
116
  const EXPECTED_GLOBS = (() => {
110
- const out = [...EXPECTED_GLOBS_BEFORE_R101];
117
+ let out = [...EXPECTED_GLOBS_BEFORE_R101];
118
+ for (const r of RENAMED_SINCE_R101) {
119
+ const at = out.indexOf(r.from);
120
+ assert.notEqual(at, -1, `RENAMED_SINCE_R101 renames '${r.from}', which is not in the list`);
121
+ out[at] = r.to;
122
+ }
111
123
  for (const a of ADDED_SINCE_R101) {
112
124
  const at = out.indexOf(a.after);
113
125
  assert.notEqual(at, -1, `ADDED_SINCE_R101 anchors on '${a.after}', which is not in the list`);
@@ -119,15 +131,28 @@ const EXPECTED_GLOBS = (() => {
119
131
  // ---------- behavior identity: the same globs, at the same floor ----------
120
132
 
121
133
  test('nothing protected before R101 has been unprotected since', () => {
122
- // The load-bearing half. Additions are ordinary policy; a REMOVAL silently
123
- // unprotects a trust surface, so it is asserted separately and must never be
124
- // satisfied by editing the pre-R101 array.
134
+ // The load-bearing half. A surface may be RENAMED (protection moves with it)
135
+ // but never simply dropped. Renames must be declared, and the declared
136
+ // replacement must actually be present — otherwise "it was renamed" becomes a
137
+ // way to unprotect a trust surface with a comment.
138
+ const renamedFrom = new Map(RENAMED_SINCE_R101.map((r) => [r.from, r.to]));
125
139
  for (const glob of EXPECTED_GLOBS_BEFORE_R101) {
126
- assert.ok(
127
- registry.PROTECTED_GLOBS.includes(glob),
140
+ if (registry.PROTECTED_GLOBS.includes(glob)) continue;
141
+ const to = renamedFrom.get(glob);
142
+ assert.ok(to,
128
143
  `'${glob}' was protected before R101 and is no longer in the registry — `
129
- + 'a protected surface cannot be dropped without an ADR saying why.',
130
- );
144
+ + 'a protected surface cannot be dropped without an ADR saying why. If it '
145
+ + 'was renamed, declare it in RENAMED_SINCE_R101.');
146
+ assert.ok(registry.PROTECTED_GLOBS.includes(to),
147
+ `'${glob}' is declared as renamed to '${to}', but '${to}' is not in the `
148
+ + 'registry — the protection went nowhere.');
149
+ }
150
+ });
151
+
152
+ test('every rename since R101 carries a documented reason', () => {
153
+ for (const r of RENAMED_SINCE_R101) {
154
+ assert.ok(r.why && r.why.length > 40,
155
+ `${r.from} -> ${r.to} must record WHY the surface moved.`);
131
156
  }
132
157
  });
133
158
 
@@ -63,12 +63,12 @@ test('a builder holding the permission passes the gate', async () => {
63
63
 
64
64
  test('a missing grant 403s with permission_forbidden and explains the denial', async () => {
65
65
  const { res, nexted } = await run(
66
- auth.requirePermission('governance.manage'), { builder: BUILDER }, holding('idea.triage'),
66
+ auth.requirePermission('government.manage'), { builder: BUILDER }, holding('idea.triage'),
67
67
  );
68
68
  assert.equal(nexted, false, 'next() must NOT be called');
69
69
  assert.equal(res._status, 403);
70
70
  assert.equal(res._json.error.code, 'permission_forbidden');
71
- assert.deepEqual(res._json.error.details.required, ['governance.manage']);
71
+ assert.deepEqual(res._json.error.details.required, ['government.manage']);
72
72
  assert.deepEqual(res._json.error.details.held, [], 'holds none of the required perms');
73
73
  });
74
74
 
@@ -107,11 +107,11 @@ test('the denial names the floor rank, not just the atom (task 1002613, map P-1)
107
107
 
108
108
  test('a port without permissionFloor still denies with a message; floors is null', async () => {
109
109
  const { res } = await run(
110
- auth.requirePermission('governance.manage'), { builder: BUILDER }, holding('idea.triage'),
110
+ auth.requirePermission('government.manage'), { builder: BUILDER }, holding('idea.triage'),
111
111
  );
112
112
  assert.equal(res._status, 403);
113
113
  assert.equal(res._json.error.details.floors, null);
114
- assert.ok(res._json.error.message.includes('governance.manage'),
114
+ assert.ok(res._json.error.message.includes('government.manage'),
115
115
  'the message names the missing atom even when no floor is resolvable');
116
116
  });
117
117
 
@@ -264,7 +264,7 @@ test('over the four seeded ranks, requirePermission admits exactly who requireRa
264
264
  'security.report.file',
265
265
  ];
266
266
  const cases = [
267
- ['governance.manage', ['archon']],
267
+ ['government.manage', ['archon']],
268
268
  ['task.claim.any', ['thetes', 'metic', 'archon']],
269
269
  ['cost.log', ['xenos', 'thetes', 'metic', 'archon']],
270
270
  // The CORE route gates migrated in BV1.R104, each paired with the requireRank
@@ -74,11 +74,11 @@ test('the resolver holds no cache — authority is read fresh per request (ADR 0
74
74
  });
75
75
 
76
76
  test('the kernel port exposes the resolver without core importing the module', () => {
77
- const { governancePort } = require(path.join(GOV, 'routes', 'governance.js'));
77
+ const { governancePort } = require(path.join(GOV, 'routes', 'government.js'));
78
78
  assert.equal(typeof governancePort.resolveBuilderPermissions, 'function');
79
79
  assert.equal(typeof governancePort.builderHasPermissions, 'function');
80
80
  assert.equal(typeof governancePort.isSystem, 'function');
81
81
  const manifest = JSON.parse(fs.readFileSync(path.join(GOV, 'module.json'), 'utf8'));
82
82
  assert.ok(manifest.provides.includes('government'), 'module.json must declare the government port');
83
- assert.ok(manifest.contributes.routes.includes('governance'), 'the port registers from the route factory');
83
+ assert.ok(manifest.contributes.routes.includes('government'), 'the port registers from the route factory');
84
84
  });
@@ -26,16 +26,16 @@ const { planRankSync, isSeededRank } = require(path.join(GOV, 'ranks.js'));
26
26
 
27
27
  // The seeded rank-roles exactly as governance_002 writes them.
28
28
  const SEEDED = [
29
- { role_key: 'xenos', seeded_rank: 'xenos', is_system: true },
30
- { role_key: 'thetes', seeded_rank: 'thetes', is_system: true },
31
- { role_key: 'metic', seeded_rank: 'metic', is_system: true },
32
- { role_key: 'archon', seeded_rank: 'archon', is_system: true },
29
+ { rank_key: 'xenos', seeded_rank: 'xenos', is_system: true },
30
+ { rank_key: 'thetes', seeded_rank: 'thetes', is_system: true },
31
+ { rank_key: 'metic', seeded_rank: 'metic', is_system: true },
32
+ { rank_key: 'archon', seeded_rank: 'archon', is_system: true },
33
33
  ];
34
- const seededRole = (rank) => ({ role_key: rank, seeded_rank: rank, is_system: true });
34
+ const seededRank_ = (rank) => ({ rank_key: rank, seeded_rank: rank, is_system: true });
35
35
  // An Archon-authored custom role from the R110 tab: no seeded_rank, not a system role.
36
- const customRole = (key) => ({ role_key: key, seeded_rank: null, is_system: false });
36
+ const customRank = (key) => ({ rank_key: key, seeded_rank: null, is_system: false });
37
37
 
38
- const plan = (rank, currentRoles) => planRankSync({ rank, currentRoles, seededRoles: SEEDED });
38
+ const plan = (rank, currentRanks) => planRankSync({ rank, currentRanks, seededRanks: SEEDED });
39
39
 
40
40
  // ---------------------------------------------------------------------------
41
41
  // registration — a builder with no roles gets the one matching their rank
@@ -60,7 +60,7 @@ test('registration: the firstAdmin bootstrap (seated straight to archon) assigns
60
60
  // ---------------------------------------------------------------------------
61
61
 
62
62
  test('promotion xenos→thetes: the new role is added and the old one removed', () => {
63
- const p = plan('thetes', [seededRole('xenos')]);
63
+ const p = plan('thetes', [seededRank_('xenos')]);
64
64
  assert.equal(p.target, 'thetes');
65
65
  assert.deepEqual([...p.add], ['thetes']);
66
66
  assert.deepEqual([...p.remove], ['xenos'], 'the superseded rank-role must be removed');
@@ -69,8 +69,8 @@ test('promotion xenos→thetes: the new role is added and the old one removed',
69
69
 
70
70
  test('promotion via the auto-graduation path leaves exactly one seeded role', () => {
71
71
  // maybeAutoGraduateToThetes runs the same _applyRankChangeTx, so the same plan.
72
- const p = plan('thetes', [seededRole('xenos')]);
73
- const seededHeld = p.next.filter((k) => SEEDED.some((s) => s.role_key === k));
72
+ const p = plan('thetes', [seededRank_('xenos')]);
73
+ const seededHeld = p.next.filter((k) => SEEDED.some((s) => s.rank_key === k));
74
74
  assert.equal(seededHeld.length, 1);
75
75
  });
76
76
 
@@ -79,7 +79,7 @@ test('promotion via the auto-graduation path leaves exactly one seeded role', ()
79
79
  // ---------------------------------------------------------------------------
80
80
 
81
81
  test('demotion archon→metic REMOVES the archon role (not merely adds metic)', () => {
82
- const p = plan('metic', [seededRole('archon')]);
82
+ const p = plan('metic', [seededRank_('archon')]);
83
83
  assert.deepEqual([...p.remove], ['archon'], 'the higher rank-role must be REMOVED on demotion');
84
84
  assert.deepEqual([...p.add], ['metic']);
85
85
  // The absence assertion is the one that matters: an additive-only sync would
@@ -89,7 +89,7 @@ test('demotion archon→metic REMOVES the archon role (not merely adds metic)',
89
89
  });
90
90
 
91
91
  test('demotion archon→xenos (a multi-rung drop) removes archon just the same', () => {
92
- const p = plan('xenos', [seededRole('archon')]);
92
+ const p = plan('xenos', [seededRank_('archon')]);
93
93
  assert.ok(p.remove.includes('archon'));
94
94
  assert.ok(!p.next.includes('archon'));
95
95
  assert.deepEqual([...p.next], ['xenos']);
@@ -98,7 +98,7 @@ test('demotion archon→xenos (a multi-rung drop) removes archon just the same',
98
98
  test('a builder holding several stale seeded roles is reduced to exactly one', () => {
99
99
  // Defence in depth: if drift ever left two seeded roles on one builder, the sync
100
100
  // must not pick one and leave the other — every non-matching seeded role goes.
101
- const p = plan('thetes', [seededRole('archon'), seededRole('metic'), seededRole('xenos')]);
101
+ const p = plan('thetes', [seededRank_('archon'), seededRank_('metic'), seededRank_('xenos')]);
102
102
  assert.deepEqual([...p.remove].sort(), ['archon', 'metic', 'xenos']);
103
103
  assert.deepEqual([...p.next], ['thetes']);
104
104
  });
@@ -108,16 +108,16 @@ test('a builder holding several stale seeded roles is reduced to exactly one', (
108
108
  // ---------------------------------------------------------------------------
109
109
 
110
110
  test('an Archon-assigned custom role survives a rank change', () => {
111
- const p = plan('metic', [seededRole('archon'), customRole('release-manager')]);
111
+ const p = plan('metic', [seededRank_('archon'), customRank('release-manager')]);
112
112
  assert.deepEqual([...p.remove], ['archon'], 'only the seeded rank-role rotates');
113
113
  assert.ok(!p.remove.includes('release-manager'), 'a custom role must never be revoked by a rank change');
114
114
  assert.deepEqual([...p.next], ['metic', 'release-manager']);
115
115
  });
116
116
 
117
117
  test('a custom role is not mistaken for the rank-role even when its key matches a rank name', () => {
118
- // role_key is UNIQUE so this cannot happen today, but the predicate must key on
118
+ // rank_key is UNIQUE so this cannot happen today, but the predicate must key on
119
119
  // is_system + seeded_rank rather than on the key's spelling.
120
- const impostor = { role_key: 'archon', seeded_rank: null, is_system: false };
120
+ const impostor = { rank_key: 'archon', seeded_rank: null, is_system: false };
121
121
  assert.equal(isSeededRank(impostor), false);
122
122
  const p = plan('metic', [impostor]);
123
123
  assert.deepEqual([...p.remove], [], 'a non-system role is never rotated away');
@@ -127,7 +127,7 @@ test('a builder on a non-seeded legacy rank resolves to zero seeded roles', () =
127
127
  // migration 023's CHECK still admits twelve reserved ranks. governance_002's INNER
128
128
  // JOIN gives them no row BY DESIGN (ADR 0151's fail-closed divergence) — preserve
129
129
  // it, and strip any seeded role they were carrying.
130
- const p = plan('hippeis', [seededRole('archon')]);
130
+ const p = plan('hippeis', [seededRank_('archon')]);
131
131
  assert.equal(p.target, null, 'no seeded role exists for a legacy rank — invent none');
132
132
  assert.deepEqual([...p.add], []);
133
133
  assert.deepEqual([...p.remove], ['archon'], 'a legacy rank must not retain a seeded rank-role');
@@ -135,7 +135,7 @@ test('a builder on a non-seeded legacy rank resolves to zero seeded roles', () =
135
135
  });
136
136
 
137
137
  test('a legacy-rank builder still keeps their custom roles', () => {
138
- const p = plan('hippeis', [seededRole('metic'), customRole('release-manager')]);
138
+ const p = plan('hippeis', [seededRank_('metic'), customRank('release-manager')]);
139
139
  assert.deepEqual([...p.next], ['release-manager']);
140
140
  });
141
141
 
@@ -144,23 +144,23 @@ test('a legacy-rank builder still keeps their custom roles', () => {
144
144
  // ---------------------------------------------------------------------------
145
145
 
146
146
  test('the sync is idempotent — running it twice changes nothing the second time', () => {
147
- const first = plan('metic', [seededRole('archon')]);
147
+ const first = plan('metic', [seededRank_('archon')]);
148
148
  assert.ok(first.add.length + first.remove.length > 0, 'the first run must do work');
149
- const after = first.next.map((key) => (SEEDED.find((s) => s.role_key === key) || customRole(key)));
150
- const second = planRankSync({ rank: 'metic', currentRoles: after, seededRoles: SEEDED });
149
+ const after = first.next.map((key) => (SEEDED.find((s) => s.rank_key === key) || customRank(key)));
150
+ const second = planRankSync({ rank: 'metic', currentRanks: after, seededRanks: SEEDED });
151
151
  assert.deepEqual([...second.add], []);
152
152
  assert.deepEqual([...second.remove], []);
153
153
  assert.deepEqual([...second.next], [...first.next]);
154
154
  });
155
155
 
156
156
  test('a builder already holding the right role produces no write at all', () => {
157
- const p = plan('metic', [seededRole('metic'), customRole('release-manager')]);
157
+ const p = plan('metic', [seededRank_('metic'), customRank('release-manager')]);
158
158
  assert.deepEqual([...p.add], []);
159
159
  assert.deepEqual([...p.remove], []);
160
160
  });
161
161
 
162
162
  test('the plan is frozen — a caller cannot widen it after the decision', () => {
163
- const p = plan('metic', [seededRole('archon')]);
163
+ const p = plan('metic', [seededRank_('archon')]);
164
164
  assert.ok(Object.isFrozen(p) && Object.isFrozen(p.add) && Object.isFrozen(p.remove));
165
165
  assert.throws(() => p.add.push('archon'), TypeError);
166
166
  });