@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
@@ -1,24 +1,24 @@
1
- // public-builders/governance.js — Governance (/builders/governance).
1
+ // modules/hall-ui/public/government.js — Government (/builders/government).
2
2
  //
3
- // The roles/permissions/assignments tab (BV1.R111, ADR 0151 §2) over the R110
4
- // CRUD routes. Discord-style: pick a role on the left, toggle its permissions in
5
- // the grid, assign custom roles to builders below.
3
+ // The ranks/permissions/assignments tab (BV1.R111, ADR 0151 §2) over the R110
4
+ // CRUD routes. Discord-style: pick a rank on the left, toggle its permissions in
5
+ // the grid, assign custom ranks to builders below.
6
6
  //
7
- // Since ADR 0172 grantability depends on WHICH role is selected, so this page's
7
+ // Since ADR 0172 grantability depends on WHICH rank is selected, so this page's
8
8
  // central job is rendering three different locks honestly. The SERVER enforces all
9
9
  // of them (ADR 0016 — nothing here grants anything; a disabled button is a
10
10
  // courtesy, `isGrantableTo` on the route is the wall):
11
11
  // • SUBSTRATE permissions (deploy / protected paths / ssh) render LOCKED for
12
- // every role, always. They are enforced outside the app (ADR 0043), so a grant
12
+ // every rank, always. They are enforced outside the app (ADR 0043), so a grant
13
13
  // row confers nothing — showing a working toggle would be the UI lying.
14
- // • OWNER POWERS (the seven in-app `system` keys) render LOCKED on a custom role
15
- // and TOGGLABLE on a sub-Archon rank-role. That asymmetry is the whole of the
14
+ // • OWNER POWERS (the seven in-app `system` keys) render LOCKED on a custom rank
15
+ // and TOGGLABLE on a sub-Archon rank. That asymmetry is the whole of the
16
16
  // owner's 2026-08-16 decision, and the section header says so out loud.
17
- // • the ARCHON rank-role is read-only in full: it holds everything, and its
17
+ // • the ARCHON rank is read-only in full: it holds everything, and its
18
18
  // immutability is what makes it impossible to click your way into locking the
19
19
  // owner out of this tab. The API refuses it as `archon_rank_immutable`.
20
- // ASSIGNMENT of any rank-role is still refused (`seeded_rank_assignment_forbidden`)
21
- // — you get a rank-role by holding the rank, so that dial is the rank itself.
20
+ // ASSIGNMENT of any rank is still refused (`seeded_rank_assignment_forbidden`)
21
+ // — you get a rank by holding the rank, so that dial is the rank itself.
22
22
  //
23
23
  // Like The Gate, this reads /api/bongos/me for a friendly Sealed notice —
24
24
  // DEFENCE IN DEPTH only. Every /government/* endpoint (reads included) is ALSO
@@ -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
  (() => {
@@ -63,50 +63,50 @@
63
63
 
64
64
  // The R110 refusal codes, translated for the person clicking.
65
65
  const ERROR_COPY = {
66
- permission_not_grantable: 'That permission can’t be granted to this role. (The server’s reply says which of the three walls applied.)',
67
- archon_rank_immutable: 'The Archon role is read-only — it holds every permission, which is what keeps you from locking yourself out of this page.',
68
- rank_has_no_default: 'Only a rank role has a catalog default to reset to. Revoke a custom role’s permissions individually instead.',
69
- seeded_rank_assignment_forbidden: 'Rank roles are assigned by rank itself — change the builder’s rank instead.',
70
- rank_exists: 'A role with that key already exists.',
71
- rank_not_found: 'That role no longer exists — refreshing.',
66
+ permission_not_grantable: 'That permission can’t be granted to this rank. (The server’s reply says which of the three walls applied.)',
67
+ archon_rank_immutable: 'The Owner rank is read-only — it holds every permission, which is what keeps you from locking yourself out of this page.',
68
+ rank_has_no_default: 'Only a ladder rank has a catalog default to reset to. Revoke a custom rank’s permissions individually instead.',
69
+ seeded_rank_assignment_forbidden: 'Ladder ranks are assigned by rank itself — change the builder’s rank instead.',
70
+ rank_exists: 'A rank with that key already exists.',
71
+ rank_not_found: 'That rank no longer exists — refreshing.',
72
72
  builder_not_found: 'No builder with that id.',
73
73
  unknown_permission: 'The permission catalog does not define that key.',
74
- bad_rank_key: 'Role keys are kebab-case: lowercase letters, digits, dashes.',
74
+ bad_rank_key: 'Rank keys are kebab-case: lowercase letters, digits, dashes.',
75
75
  validation_failed: 'Missing or invalid fields.',
76
76
  };
77
77
  const friendlyError = (code) => ERROR_COPY[code] || null;
78
78
 
79
79
  // ---- state ---------------------------------------------------------------
80
80
 
81
- let roles = []; // [{ rank_key, label, description, is_system, seeded_rank, permissions: [] }]
81
+ let ranks = []; // [{ rank_key, label, description, is_system, seeded_rank, permissions: [] }]
82
82
  let permissions = []; // catalog rows: [{ key, system, floor, guards, grantable }]
83
83
  let principals = []; // [{ system_role, permission, guards }]
84
- let assignments = []; // [{ builder_id, github_login, display_name, rank, rank_key, role_label, is_system }]
85
- let selectedRoleKey = null;
84
+ let assignments = []; // [{ builder_id, github_login, display_name, rank, rank_key, rank_label, is_system }]
85
+ let selectedRankKey = null;
86
86
 
87
- const selectedRole = () => roles.find((r) => r.rank_key === selectedRoleKey) || null;
87
+ const selectedRank = () => ranks.find((r) => r.rank_key === selectedRankKey) || null;
88
88
 
89
- // ---- render: role list ---------------------------------------------------
89
+ // ---- render: rank list ---------------------------------------------------
90
90
 
91
- function renderRoles() {
92
- const list = document.getElementById('gov-role-list');
91
+ function renderRanks() {
92
+ const list = document.getElementById('gov-rank-list');
93
93
  list.innerHTML = '';
94
- for (const role of roles) {
95
- // The Archon row is called out as read-only, because it is the one role in
94
+ for (const rank of ranks) {
95
+ // The Archon row is called out as read-only, because it is the one rank in
96
96
  // this list a click will not change and the reader should know before trying.
97
- const pill = isArchonRole(role)
98
- ? `<span class="gov-pill gov-pill--locked">rank role · read-only</span>`
99
- : role.is_system
100
- ? `<span class="gov-pill gov-pill--seeded">rank role</span>`
97
+ const pill = isArchonRank(rank)
98
+ ? `<span class="gov-pill gov-pill--locked">ladder · read-only</span>`
99
+ : rank.is_system
100
+ ? `<span class="gov-pill gov-pill--seeded">ladder</span>`
101
101
  : `<span class="gov-pill gov-pill--custom">custom</span>`;
102
102
  const btn = document.createElement('button');
103
103
  btn.type = 'button';
104
- btn.className = 'gov-role';
105
- btn.setAttribute('aria-pressed', role.rank_key === selectedRoleKey ? 'true' : 'false');
104
+ btn.className = 'gov-rank';
105
+ btn.setAttribute('aria-pressed', rank.rank_key === selectedRankKey ? 'true' : 'false');
106
106
  btn.innerHTML =
107
- `<span class="gov-role__head"><span class="gov-role__label">${escapeHtml(role.label || role.rank_key)}</span>${pill}</span>
108
- <span class="gov-role__meta"><code>${escapeHtml(role.rank_key)}</code> · ${(role.permissions || []).length} permission${(role.permissions || []).length === 1 ? '' : 's'}</span>`;
109
- btn.addEventListener('click', () => { selectedRoleKey = role.rank_key; renderRoles(); renderGrid(); });
107
+ `<span class="gov-rank__head"><span class="gov-rank__label">${escapeHtml(rank.label || rank.rank_key)}</span>${pill}</span>
108
+ <span class="gov-rank__meta"><code>${escapeHtml(rank.rank_key)}</code> · ${(rank.permissions || []).length} permission${(rank.permissions || []).length === 1 ? '' : 's'}</span>`;
109
+ btn.addEventListener('click', () => { selectedRankKey = rank.rank_key; renderRanks(); renderGrid(); });
110
110
  list.appendChild(btn);
111
111
  }
112
112
  }
@@ -117,34 +117,34 @@
117
117
  // right control, NEVER to decide authority — the route runs the real one and
118
118
  // refuses regardless of what this returns. Kept in the same order as the server's
119
119
  // so a future catalog change lands in one obvious place on each side.
120
- const isArchonRole = (role) => !!role && role.is_system && role.seeded_rank === 'archon';
121
- const isRankRole = (role) => !!role && role.is_system;
122
- function grantableTo(p, role) {
123
- if (!role || p.substrate) return false;
124
- if (role.is_system) return role.seeded_rank !== 'archon';
120
+ const isArchonRank = (rank) => !!rank && rank.is_system && rank.seeded_rank === 'archon';
121
+ const isRankRank = (rank) => !!rank && rank.is_system;
122
+ function grantableTo(p, rank) {
123
+ if (!rank || p.substrate) return false;
124
+ if (rank.is_system) return rank.seeded_rank !== 'archon';
125
125
  return !!p.grantable;
126
126
  }
127
127
 
128
- function permRow(p, role) {
129
- const held = !!role && (role.permissions || []).includes(p.key);
128
+ function permRow(p, rank) {
129
+ const held = !!rank && (rank.permissions || []).includes(p.key);
130
130
  const row = document.createElement('div');
131
131
  row.className = 'gov-perm';
132
132
 
133
133
  // Why locked, in the words that tell the reader whether it is a policy they
134
134
  // could change or a fact they cannot. Three causes, three sentences.
135
135
  const lockReason = p.substrate
136
- ? 'Enforced outside the app (the pre-push hook, the deploy audit, the server itself). Granting it here would change nothing, so it is locked for every role.'
137
- : isArchonRole(role)
138
- ? 'The Archon role holds everything and is read-only.'
139
- : 'An owner-level permission. It can be handed to a rank role, but never to a custom one.';
136
+ ? 'Enforced outside the app (the pre-push hook, the deploy audit, the server itself). Granting it here would change nothing, so it is locked for every rank.'
137
+ : isArchonRank(rank)
138
+ ? 'The Archon rank holds everything and is read-only.'
139
+ : 'An owner-level permission. It can be handed to a ladder rank, but never to a custom one.';
140
140
 
141
141
  let control;
142
- if (!role) {
142
+ if (!rank) {
143
143
  control = '';
144
- } else if (!grantableTo(p, role)) {
144
+ } else if (!grantableTo(p, rank)) {
145
145
  control = `<span class="gov-pill gov-pill--locked" title="${escapeHtml(lockReason)}">locked</span>`;
146
- // The Archon role is read-only rather than empty-handed, so say what it holds.
147
- if (held && isArchonRole(role)) control = `<span class="gov-held-note">held</span>${control}`;
146
+ // The Archon rank is read-only rather than empty-handed, so say what it holds.
147
+ if (held && isArchonRank(rank)) control = `<span class="gov-held-note">held</span>${control}`;
148
148
  } else {
149
149
  control = `<button type="button" class="gov-toggle" aria-pressed="${held ? 'true' : 'false'}" data-perm="${escapeHtml(p.key)}">${held ? 'granted' : 'grant'}</button>`;
150
150
  }
@@ -158,7 +158,7 @@
158
158
  ${control}`;
159
159
 
160
160
  const btn = row.querySelector('button.gov-toggle');
161
- if (btn) btn.addEventListener('click', () => toggleGrant(role, p.key, !held, btn));
161
+ if (btn) btn.addEventListener('click', () => toggleGrant(rank, p.key, !held, btn));
162
162
  return row;
163
163
  }
164
164
 
@@ -170,34 +170,34 @@
170
170
  return el;
171
171
  }
172
172
 
173
- function gridNote(role) {
174
- if (!role) return 'Pick a role to see and edit what it may do.';
175
- if (isArchonRole(role)) {
176
- return 'The Archon role holds every permission and is read-only — that is what makes it impossible to lock yourself out of this page. To give a power away, hand it to one of the ranks below instead.';
173
+ function gridNote(rank) {
174
+ if (!rank) return 'Pick a rank to see and edit what it may do.';
175
+ if (isArchonRank(rank)) {
176
+ return 'The Archon rank holds every permission and is read-only — that is what makes it impossible to lock yourself out of this page. To give a power away, hand it to one of the ranks below instead.';
177
177
  }
178
- if (isRankRole(role)) {
179
- return `Everything a ${role.label || role.rank_key} can do, and yours to change — every builder at this rank is affected immediately. “Reset to default” puts it back to the shipped ladder.`;
178
+ if (isRankRank(rank)) {
179
+ return `Everything a ${rank.label || rank.rank_key} can do, and yours to change — every builder at this rank is affected immediately. “Reset to default” puts it back to the shipped ladder.`;
180
180
  }
181
- return 'Toggle the everyday permissions this role carries. Owner powers can only go to a rank role, not to a custom one.';
181
+ return 'Toggle the everyday permissions this rank carries. Owner powers can only go to a ladder rank, not to a custom one.';
182
182
  }
183
183
 
184
184
  function renderGrid() {
185
185
  const grid = document.getElementById('gov-grid');
186
- const role = selectedRole();
187
- document.getElementById('gov-grid-title').textContent = role
188
- ? `Permissions — ${role.label || role.rank_key}` : 'Permissions';
189
- document.getElementById('gov-grid-note').textContent = gridNote(role);
186
+ const rank = selectedRank();
187
+ document.getElementById('gov-grid-title').textContent = rank
188
+ ? `Permissions — ${rank.label || rank.rank_key}` : 'Permissions';
189
+ document.getElementById('gov-grid-note').textContent = gridNote(rank);
190
190
 
191
- // The reset escape hatch — only a rank role has a catalog default, and the
191
+ // The reset escape hatch — only a ladder rank has a catalog default, and the
192
192
  // Archon one never drifts from it, so it shows on exactly the three editable ones.
193
193
  const resetBox = document.getElementById('gov-reset');
194
194
  resetBox.innerHTML = '';
195
- if (isRankRole(role) && !isArchonRole(role)) {
195
+ if (isRankRank(rank) && !isArchonRank(rank)) {
196
196
  const btn = document.createElement('button');
197
197
  btn.type = 'button';
198
198
  btn.className = 'gov-btn gov-btn--ghost gov-btn--small';
199
199
  btn.textContent = 'Reset to default';
200
- btn.addEventListener('click', () => resetRole(role, btn));
200
+ btn.addEventListener('click', () => resetRank(rank, btn));
201
201
  resetBox.appendChild(btn);
202
202
  }
203
203
 
@@ -206,30 +206,30 @@
206
206
  // identity is not a row anyone can act on here.
207
207
  const humanPerms = permissions.filter((p) => !principals.some((pr) => pr.permission === p.key));
208
208
  // Three groups, split by WHY a row is or isn't editable rather than by the old
209
- // single `grantable` flag — which since ADR 0172 answers only the custom-role case.
209
+ // single `grantable` flag — which since ADR 0172 answers only the custom-rank case.
210
210
  const substrate = humanPerms.filter((p) => p.substrate);
211
211
  const ownerPowers = humanPerms.filter((p) => p.system && !p.substrate);
212
212
  const operational = humanPerms.filter((p) => !p.system);
213
213
 
214
- const ownerHead = !role || isRankRole(role)
214
+ const ownerHead = !rank || isRankRank(rank)
215
215
  ? `Owner powers (${ownerPowers.length})`
216
- : `Owner powers — locked for custom roles (${ownerPowers.length})`;
216
+ : `Owner powers — locked for custom ranks (${ownerPowers.length})`;
217
217
 
218
- grid.appendChild(section(`Operational (${operational.length})`, operational.map((p) => permRow(p, role))));
219
- grid.appendChild(section(ownerHead, ownerPowers.map((p) => permRow(p, role))));
220
- grid.appendChild(section(`Enforced outside the app — always locked (${substrate.length})`, substrate.map((p) => permRow(p, role))));
218
+ grid.appendChild(section(`Operational (${operational.length})`, operational.map((p) => permRow(p, rank))));
219
+ grid.appendChild(section(ownerHead, ownerPowers.map((p) => permRow(p, rank))));
220
+ grid.appendChild(section(`Enforced outside the app — always locked (${substrate.length})`, substrate.map((p) => permRow(p, rank))));
221
221
  }
222
222
 
223
- // Restore a rank role to the shipped ladder. Confirmed because it is the one
224
- // action here that discards work in bulk — a drifted role can be ~60 rows.
225
- async function resetRole(role, btn) {
226
- const label = role.label || role.rank_key;
223
+ // Restore a ladder rank to the shipped ladder. Confirmed because it is the one
224
+ // action here that discards work in bulk — a drifted rank can be ~60 rows.
225
+ async function resetRank(rank, btn) {
226
+ const label = rank.label || rank.rank_key;
227
227
  if (!window.confirm(`Reset ${label} to its default permissions?\n\nEvery change you've made to this rank is discarded, and it takes effect for every builder at this rank on their next request.`)) return;
228
228
  btn.disabled = true;
229
229
  try {
230
- const out = await sendJson('POST', `/government/ranks/${encodeURIComponent(role.rank_key)}/reset`);
230
+ const out = await sendJson('POST', `/government/ranks/${encodeURIComponent(rank.rank_key)}/reset`);
231
231
  toast(`${label} reset to its default ${out.permission_count} permissions.`);
232
- await reloadRoles();
232
+ await reloadRanks();
233
233
  } catch (err) {
234
234
  if (err && err.kind === '401') return;
235
235
  toast(`Could not reset: ${err.message || err}`);
@@ -237,12 +237,12 @@
237
237
  }
238
238
  }
239
239
 
240
- async function toggleGrant(role, permissionKey, grant, btn) {
240
+ async function toggleGrant(rank, permissionKey, grant, btn) {
241
241
  btn.disabled = true;
242
242
  try {
243
- await sendJson(grant ? 'PUT' : 'DELETE', `/government/ranks/${encodeURIComponent(role.rank_key)}/permissions/${encodeURIComponent(permissionKey)}`);
243
+ await sendJson(grant ? 'PUT' : 'DELETE', `/government/ranks/${encodeURIComponent(rank.rank_key)}/permissions/${encodeURIComponent(permissionKey)}`);
244
244
  toast(grant ? `Granted ${permissionKey}.` : `Revoked ${permissionKey}.`);
245
- await reloadRoles();
245
+ await reloadRanks();
246
246
  } catch (err) {
247
247
  if (err && err.kind === '401') return;
248
248
  toast(`Could not ${grant ? 'grant' : 'revoke'}: ${err.message || err}`);
@@ -256,7 +256,7 @@
256
256
  const box = document.getElementById('gov-assignments');
257
257
  box.innerHTML = '';
258
258
  if (assignments.length === 0) {
259
- box.innerHTML = `<p class="gov-empty">No role assignments yet.</p>`;
259
+ box.innerHTML = `<p class="gov-empty">No rank assignments yet.</p>`;
260
260
  return;
261
261
  }
262
262
  const wrap = document.createElement('section');
@@ -265,11 +265,11 @@
265
265
  const row = document.createElement('div');
266
266
  row.className = 'gov-assign-row';
267
267
  const who = `${escapeHtml(a.display_name || a.github_login || `builder ${a.builder_id}`)} <span class="muted">(#${escapeHtml(String(a.builder_id))} · ${escapeHtml(a.rank || '')})</span>`;
268
- const rolePill = a.is_system
269
- ? `<span class="gov-pill gov-pill--seeded">${escapeHtml(a.role_label || a.rank_key)} · via rank</span>`
270
- : `<span class="gov-pill gov-pill--custom">${escapeHtml(a.role_label || a.rank_key)}</span>`;
268
+ const rankPill = a.is_system
269
+ ? `<span class="gov-pill gov-pill--seeded">${escapeHtml(a.rank_label || a.rank_key)} · via rank</span>`
270
+ : `<span class="gov-pill gov-pill--custom">${escapeHtml(a.rank_label || a.rank_key)}</span>`;
271
271
  const action = a.is_system ? '' : `<button type="button" class="gov-btn gov-btn--ghost gov-btn--small" data-act="unassign">Remove</button>`;
272
- row.innerHTML = `<span class="gov-assign-row__who">${who}</span>${rolePill}${action}`;
272
+ row.innerHTML = `<span class="gov-assign-row__who">${who}</span>${rankPill}${action}`;
273
273
  const btn = row.querySelector('[data-act=unassign]');
274
274
  if (btn) btn.addEventListener('click', () => unassign(a, btn));
275
275
  wrap.appendChild(row);
@@ -278,18 +278,18 @@
278
278
  }
279
279
 
280
280
  function renderAssignSelect() {
281
- const sel = document.getElementById('gov-assign-role');
282
- const custom = roles.filter((r) => !r.is_system);
281
+ const sel = document.getElementById('gov-assign-rank');
282
+ const custom = ranks.filter((r) => !r.is_system);
283
283
  sel.innerHTML = custom.length
284
284
  ? custom.map((r) => `<option value="${escapeHtml(r.rank_key)}">${escapeHtml(r.label || r.rank_key)}</option>`).join('')
285
- : `<option value="" disabled selected>No custom roles yet</option>`;
285
+ : `<option value="" disabled selected>No custom ranks yet</option>`;
286
286
  }
287
287
 
288
288
  async function unassign(a, btn) {
289
289
  btn.disabled = true;
290
290
  try {
291
291
  await sendJson('DELETE', `/government/assignments/${encodeURIComponent(a.builder_id)}/${encodeURIComponent(a.rank_key)}`);
292
- toast(`Removed ${a.role_label || a.rank_key} from builder ${a.builder_id}.`);
292
+ toast(`Removed ${a.rank_label || a.rank_key} from builder ${a.builder_id}.`);
293
293
  await reloadAssignments();
294
294
  } catch (err) {
295
295
  if (err && err.kind === '401') return;
@@ -320,11 +320,11 @@
320
320
 
321
321
  // ---- loads ---------------------------------------------------------------
322
322
 
323
- async function reloadRoles() {
323
+ async function reloadRanks() {
324
324
  const data = await fetchJson('/government/ranks');
325
- roles = data.roles || [];
326
- if (!roles.some((r) => r.rank_key === selectedRoleKey)) selectedRoleKey = roles.length ? roles[0].rank_key : null;
327
- renderRoles();
325
+ ranks = data.ranks || [];
326
+ if (!ranks.some((r) => r.rank_key === selectedRankKey)) selectedRankKey = ranks.length ? ranks[0].rank_key : null;
327
+ renderRanks();
328
328
  renderGrid();
329
329
  renderAssignSelect();
330
330
  }
@@ -339,7 +339,7 @@
339
339
  const perms = await fetchJson('/government/permissions');
340
340
  permissions = perms.permissions || [];
341
341
  principals = perms.principals || [];
342
- await reloadRoles();
342
+ await reloadRanks();
343
343
  await reloadAssignments();
344
344
  renderPrincipals();
345
345
  }
@@ -359,13 +359,13 @@
359
359
  if (desc) body.description = desc;
360
360
  try {
361
361
  const r = await sendJson('POST', '/government/ranks', body);
362
- toast(`Created role ${r.role ? r.role.rank_key : body.rank_key}.`);
362
+ toast(`Created rank ${r.rank ? r.rank.rank_key : body.rank_key}.`);
363
363
  e.target.reset();
364
- selectedRoleKey = body.rank_key;
365
- await reloadRoles();
364
+ selectedRankKey = body.rank_key;
365
+ await reloadRanks();
366
366
  } catch (err) {
367
367
  if (err && err.kind === '401') return;
368
- toast(`Could not create role: ${err.message || err}`);
368
+ toast(`Could not create rank: ${err.message || err}`);
369
369
  } finally {
370
370
  btn.disabled = false;
371
371
  }
@@ -375,12 +375,12 @@
375
375
  e.preventDefault();
376
376
  const btn = e.target.querySelector('button');
377
377
  const builderId = Number(document.getElementById('gov-assign-builder').value);
378
- const roleKey = document.getElementById('gov-assign-role').value;
379
- if (!roleKey) { toast('Create a custom role first.'); return; }
378
+ const rankKey = document.getElementById('gov-assign-rank').value;
379
+ if (!rankKey) { toast('Create a custom rank first.'); return; }
380
380
  btn.disabled = true;
381
381
  try {
382
- await sendJson('POST', '/government/assignments', { builder_id: builderId, rank_key: roleKey });
383
- toast(`Assigned ${roleKey} to builder ${builderId}.`);
382
+ await sendJson('POST', '/government/assignments', { builder_id: builderId, rank_key: rankKey });
383
+ toast(`Assigned ${rankKey} to builder ${builderId}.`);
384
384
  e.target.reset();
385
385
  await reloadAssignments();
386
386
  } catch (err) {
@@ -402,9 +402,9 @@
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
- // rank-role can hold it — rank is an accurate client-side proxy.
407
+ // rank can hold it — rank is an accurate client-side proxy.
408
408
  let me;
409
409
  try {
410
410
  me = await fetchJson('/me');
@@ -416,7 +416,7 @@
416
416
  const rank = (me && (me.builder || me).rank || '').toLowerCase();
417
417
  if (rank !== 'archon') { showSealed(); return; }
418
418
 
419
- document.getElementById('gov-sub').textContent = 'Roles, permissions, and who holds them. Ranks below Archon are yours to shape; what the substrate enforces stays locked.';
419
+ document.getElementById('gov-sub').textContent = 'Ranks, permissions, and who holds them. Ranks below Archon are yours to shape; what the substrate enforces stays locked.';
420
420
  document.getElementById('gov-body').hidden = false;
421
421
  wireForms();
422
422
  try {
@@ -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
 
@@ -9,7 +9,7 @@
9
9
  // shows a restricted notice below the Metic floor — DEFENCE IN DEPTH only. The data +
10
10
  // mutation endpoints (/boxes, /boxes/:id/close, /boxes/:id/block) are ALSO gated
11
11
  // server-side on `box.fleet.manage` (ADR 0016), and the page itself is gated by
12
- // serve-internal.js requireGovernancePage — whose own floor is `page.view.government`,
12
+ // serve-internal.js requireGovernmentPage — whose own floor is `page.view.government`,
13
13
  // metic since ADR 0157. On a 401 it redirects into the web sign-in flow.
14
14
 
15
15
  (() => {
@@ -328,7 +328,7 @@
328
328
  const rank = (me && (me.builder || me).rank || '').toLowerCase();
329
329
  // ADR 0157: Harbor sits at the `page.view.government` floor (metic), and the fleet
330
330
  // data it reads is `box.fleet.manage` (also metic now). Cosmetic —
331
- // requireGovernancePage + the route gates are the wall; this just picks body vs
331
+ // requireGovernmentPage + the route gates are the wall; this just picks body vs
332
332
  // sealed notice.
333
333
  if (rank !== 'metic' && rank !== 'archon') { showSealed(); return; }
334
334
 
@@ -12,7 +12,7 @@
12
12
  // top bar (drawer toggle, page title, greeting slot) into the static mounts
13
13
  // (#app-sidebar / #app-topbar / #app-foot) every hall page carries;
14
14
  // - rank-gate nav items COSMETICALLY (server-side page gates remain the real
15
- // access control — serve-internal.js requireGovernancePage etc.);
15
+ // access control — serve-internal.js requireGovernmentPage etc.);
16
16
  // - on the index (body[data-page="home"]) route the three views (#home,
17
17
  // #standing, #leaderboard) plus the #achievements alias into Standing. The
18
18
  // `#/task|idea|blocker/N` deep-link hashes belong to builders.js and are
@@ -97,13 +97,15 @@
97
97
  // work — signed, but for newcomers only once the "ship three" stage is
98
98
  // current (mirrors the old #hall-nav-work rule, task #833)
99
99
  // archon — rank === 'archon'
100
- // governance — rank is metic or archon (ADR 0157): the monitoring/ops pages.
100
+ // government — rank is metic or archon (ADR 0157): the monitoring/ops pages.
101
+ // NB this gate key is a RANK ALIAS meaning metic-or-archon; it is NOT the
102
+ // `government` MODULE key, and the two are unrelated (ADR 0174).
101
103
  // Mirrors the server's `page.view.government` floor, which that ADR
102
- // lowered archon→metic. Cosmetic only — requireGovernancePage is the wall.
104
+ // lowered archon→metic. Cosmetic only — requireGovernmentPage is the wall.
103
105
  // `view` marks an index view (hash-routed); `module` gates on window.__MODULES__.
104
106
  // task 2175: grouped by AUDIENCE of the data, so the sidebar reads in coherent
105
107
  // runs instead of one long list — the project (unlabeled), You, Community,
106
- // Docs, Governance. A group's `gate` must match its most permissive item's, or the
108
+ // Docs, Government. A group's `gate` must match its most permissive item's, or the
107
109
  // head renders over nothing.
108
110
  var NAV = [
109
111
  { group: '', items: [
@@ -143,7 +145,7 @@
143
145
  // requireProjectCuratePage is the wall — but a nav that disagrees with the
144
146
  // gate teaches the ladder wrong. The module gate hides the tab on
145
147
  // instances without the platform-identity hub.
146
- { id: 'people', label: 'People', icon: 'people', href: '/people', gate: 'governance', module: 'platform-identity' },
148
+ { id: 'people', label: 'People', icon: 'people', href: '/people', gate: 'government', module: 'platform-identity' },
147
149
  ] },
148
150
  { group: 'Docs', gate: 'signed', items: [
149
151
  // task 1002710: primer + diagrams were gate:'member' (rank !== xenos) to
@@ -168,18 +170,20 @@
168
170
  // subdomain — external so the rewrite shim doesn't turn it into /builders/docs.
169
171
  { id: 'apidocs', label: 'API docs', icon: 'docs', href: '/docs', gate: 'signed', external: true },
170
172
  ] },
171
- // Relabeled 'Archon' → 'Governance' in ADR 0157: these pages are no longer
173
+ // Relabeled 'Archon' → 'Governance' in ADR 0157, then → 'Government' by ADR 0174: these pages are no longer
172
174
  // Archon-only, so a group header reading "Archon" over items a Metic can open
173
175
  // would misdescribe the ladder to the builders most likely to be learning it.
174
- { group: 'Governance', gate: 'governance', items: [
175
- { id: 'watch', label: 'Watch', icon: 'admin', href: '/watch', gate: 'governance' },
176
- { id: 'harbor', label: 'Harbor', icon: 'admin', href: '/harbor', gate: 'governance', module: 'dev-box' },
177
- { id: 'gate', label: 'Gate', icon: 'admin', href: '/gate', gate: 'governance' },
178
- { id: 'sessions', label: 'Sessions', icon: 'admin', href: '/sessions', gate: 'governance' },
176
+ { group: 'Government', gate: 'government', items: [
177
+ { id: 'watch', label: 'Watch', icon: 'admin', href: '/watch', gate: 'government' },
178
+ { id: 'harbor', label: 'Harbor', icon: 'admin', href: '/harbor', gate: 'government', module: 'dev-box' },
179
+ { id: 'gate', label: 'Gate', icon: 'admin', href: '/gate', gate: 'government' },
180
+ { id: 'sessions', label: 'Sessions', icon: 'admin', href: '/sessions', gate: 'government' },
179
181
  // BV1.R113: the roles-and-permissions tab (ADR 0151). The one genuinely
180
- // Archon-only item the 'governance' gate comment anticipated — cosmetic
181
- // only; requireGovernanceManagePage (the government.manage atom) is the wall.
182
- { id: 'governance', label: 'Roles', icon: 'admin', href: '/governance', gate: 'archon' },
182
+ // Archon-only item the 'government' gate comment anticipated — cosmetic
183
+ // only; requireGovernmentManagePage (the government.manage atom) is the wall.
184
+ { id: 'government', label: 'Permissions', icon: 'admin', href: '/government', gate: 'archon' } // NOT 'Ranks': /ranks is the signed-in ladder explainer and two nav items
185
+ // reading 'Ranks' is the confusion this rename exists to remove. This tab
186
+ // is where a rank's permissions are granted, so it says what it does.,
183
187
  ] },
184
188
  ];
185
189
 
@@ -423,7 +427,7 @@
423
427
  // ADR 0157: the monitoring/ops pages sit at the `page.view.government` floor,
424
428
  // which is metic. Kept as its own key rather than widening 'archon', so a
425
429
  // genuinely Archon-only nav item can still be added later.
426
- case 'governance': return authed && (access.rank === 'metic' || access.rank === 'archon');
430
+ case 'government': return authed && (access.rank === 'metic' || access.rank === 'archon');
427
431
  default: return false;
428
432
  }
429
433
  }
@@ -887,7 +887,7 @@
887
887
 
888
888
  // ADR 0157: Watch sits at the `page.view.government` floor (metic), and every panel
889
889
  // it reads (roster, security reports, audit log, sessions, box costs) delegated to
890
- // metic with it. Cosmetic — requireGovernancePage + the route gates are the wall.
890
+ // metic with it. Cosmetic — requireGovernmentPage + the route gates are the wall.
891
891
  if (!builder || (builder.rank !== 'metic' && builder.rank !== 'archon')) {
892
892
  showSealed('This page is available from the Metic rank up. Your current rank does not grant access.');
893
893
  return;
@@ -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.185",
3
+ "version": "1.19.187",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.185",
9
+ "version": "1.19.187",
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.185",
3
+ "version": "1.19.187",
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",