@bongos/core 1.19.186 → 1.19.188

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.bongos-core.json +83 -78
  2. package/config/government.neutral.json +6 -6
  3. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +7 -0
  4. package/docs/adr/0172-editable-rank-roles-substrate-axis.md +8 -1
  5. package/docs/adr/README.md +2 -2
  6. package/docs/api/openapi.json +8 -8
  7. package/docs/api-reference.md +7 -7
  8. package/docs/architecture.md +1 -1
  9. package/docs/canonical-permissions.md +9 -9
  10. package/docs/file-map.md +1 -1
  11. package/modules/builder-settings/wandering-prefs.js +13 -13
  12. package/modules/government/catalog.js +75 -75
  13. package/modules/government/config.js +43 -33
  14. package/modules/government/db.js +57 -57
  15. package/modules/government/protected-surfaces.js +7 -7
  16. package/modules/government/protected-surfaces.json +4 -4
  17. package/modules/government/ranks.js +17 -17
  18. package/modules/government/resolver.js +24 -24
  19. package/modules/government/routes/government.js +86 -86
  20. package/modules/government/routes/rank-sync.js +6 -6
  21. package/modules/government/session-scopes.js +7 -7
  22. package/modules/government/session-scopes.json +5 -5
  23. package/modules/hall-ui/public/government.css +1 -1
  24. package/modules/hall-ui/public/government.html +1 -1
  25. package/modules/hall-ui/public/government.js +3 -3
  26. package/modules/lifecycle/claim-eligibility.js +1 -1
  27. package/modules/lifecycle/db.js +1 -1
  28. package/package-lock.json +2 -2
  29. package/package.json +1 -1
  30. package/scripts/gds/fitness.js +138 -0
  31. package/src/bongos/auth.js +2 -2
  32. package/src/module-api.js +5 -1
  33. package/tests/auth_page_gate.mjs +2 -2
  34. package/tests/{governance_abuse_matrix.mjs → government_abuse_matrix.mjs} +36 -36
  35. package/tests/{governance_catalog.mjs → government_catalog.mjs} +11 -11
  36. package/tests/{governance_claim_eligibility.mjs → government_claim_eligibility.mjs} +2 -2
  37. package/tests/{governance_config.mjs → government_config.mjs} +54 -54
  38. package/tests/{governance_fold.mjs → government_fold.mjs} +6 -6
  39. package/tests/{governance_fold_bench.mjs → government_fold_bench.mjs} +3 -3
  40. package/tests/{governance_live_proof.mjs → government_live_proof.mjs} +5 -5
  41. package/tests/{governance_ownership_scope.mjs → government_ownership_scope.mjs} +16 -16
  42. package/tests/{governance_page_gates.mjs → government_page_gates.mjs} +5 -5
  43. package/tests/{governance_parity.mjs → government_parity.mjs} +3 -3
  44. package/tests/{governance_principals.mjs → government_principals.mjs} +14 -14
  45. package/tests/{governance_protected_surfaces.mjs → government_protected_surfaces.mjs} +1 -1
  46. package/tests/{governance_role_sync.mjs → government_rank_sync.mjs} +2 -2
  47. package/tests/{governance_require_permission.mjs → government_require_permission.mjs} +1 -1
  48. package/tests/{governance_resolver.mjs → government_resolver.mjs} +6 -6
  49. package/tests/{governance_routes.mjs → government_routes.mjs} +32 -32
  50. package/tests/{governance_seed.mjs → government_seed.mjs} +8 -8
  51. package/tests/{governance_session_scope.mjs → government_session_scope.mjs} +10 -10
  52. package/tests/{governance_vanilla_boot.mjs → government_vanilla_boot.mjs} +26 -26
  53. package/tests/government_vocabulary.mjs +94 -0
  54. package/tests/hall_page_gate_map.mjs +1 -1
@@ -5108,7 +5108,7 @@
5108
5108
  "government"
5109
5109
  ],
5110
5110
  "summary": "GET /government/assignments",
5111
- "description": "government.manage — every builder↔role assignment row.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `government.manage` (all required).",
5111
+ "description": "government.manage — every builder↔rank assignment row.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `government.manage` (all required).",
5112
5112
  "x-rank": "archon",
5113
5113
  "x-source": "modules/government/routes/government.js",
5114
5114
  "x-permissions": [
@@ -5147,7 +5147,7 @@
5147
5147
  "government"
5148
5148
  ],
5149
5149
  "summary": "POST /government/assignments",
5150
- "description": "government.manage — assign a CUSTOM role to a builder. assigned_by is ALWAYS the session builder, never input (the audit trail's \"who did this\" must be the authenticated actor).\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `government.manage` (all required).",
5150
+ "description": "government.manage — assign a CUSTOM rank to a builder. assigned_by is ALWAYS the session builder, never input (the audit trail's \"who did this\" must be the authenticated actor).\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `government.manage` (all required).",
5151
5151
  "x-rank": "archon",
5152
5152
  "x-source": "modules/government/routes/government.js",
5153
5153
  "x-permissions": [
@@ -5192,7 +5192,7 @@
5192
5192
  "government"
5193
5193
  ],
5194
5194
  "summary": "DELETE /government/assignments/:builderId/:rankKey",
5195
- "description": "government.manage — remove a custom-role assignment. Idempotent: `removed` says whether a row went away.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `government.manage` (all required).",
5195
+ "description": "government.manage — remove a custom-rank assignment. Idempotent: `removed` says whether a row went away.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `government.manage` (all required).",
5196
5196
  "x-rank": "archon",
5197
5197
  "x-source": "modules/government/routes/government.js",
5198
5198
  "x-permissions": [
@@ -5256,7 +5256,7 @@
5256
5256
  "government"
5257
5257
  ],
5258
5258
  "summary": "GET /government/permissions",
5259
- "description": "government.manage — the permission catalog + principals, straight from code (the catalog is CODE, not a table); no DB read. Principals are read-only by construction. Three flags the tab renders its locks from, and the tab must read all three rather than just `grantable`, because since ADR 0172 grantability depends on WHICH role is selected: • `grantable` — the CUSTOM-role answer (system:false). Unchanged meaning. • `system` — an in-app Archon power: locked on a custom role, togglable on a sub-Archon rank-role. • `substrate` — locked EVERYWHERE, on every role, forever (ADR 0043).\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `government.manage` (all required).",
5259
+ "description": "government.manage — the permission catalog + principals, straight from code (the catalog is CODE, not a table); no DB read. Principals are read-only by construction. Three flags the tab renders its locks from, and the tab must read all three rather than just `grantable`, because since ADR 0172 grantability depends on WHICH rank is selected: • `grantable` — the CUSTOM-rank answer (system:false). Unchanged meaning. • `system` — an in-app Archon power: locked on a custom rank, togglable on a sub-Archon rank. • `substrate` — locked EVERYWHERE, on every rank, forever (ADR 0043).\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `government.manage` (all required).",
5260
5260
  "x-rank": "archon",
5261
5261
  "x-source": "modules/government/routes/government.js",
5262
5262
  "x-permissions": [
@@ -5297,7 +5297,7 @@
5297
5297
  "government"
5298
5298
  ],
5299
5299
  "summary": "GET /government/ranks",
5300
- "description": "government.manage — the full role list with each role's grants.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `government.manage` (all required).",
5300
+ "description": "government.manage — the full rank list with each rank's grants.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `government.manage` (all required).",
5301
5301
  "x-rank": "archon",
5302
5302
  "x-source": "modules/government/routes/government.js",
5303
5303
  "x-permissions": [
@@ -5336,7 +5336,7 @@
5336
5336
  "government"
5337
5337
  ],
5338
5338
  "summary": "POST /government/ranks",
5339
- "description": "government.manage — create a CUSTOM role (is_system false, no seeded_rank — the seeded four exist only via governance_002). Grants are added one at a time via the toggle below, so creation carries none.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `government.manage` (all required).",
5339
+ "description": "government.manage — create a CUSTOM rank (is_system false, no seeded_rank — the seeded four exist only via governance_002). Grants are added one at a time via the toggle below, so creation carries none.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `government.manage` (all required).",
5340
5340
  "x-rank": "archon",
5341
5341
  "x-source": "modules/government/routes/government.js",
5342
5342
  "x-permissions": [
@@ -5388,7 +5388,7 @@
5388
5388
  "government"
5389
5389
  ],
5390
5390
  "summary": "PUT /government/ranks/:rankKey/permissions/:permissionKey",
5391
- "description": "government.manage — grant one permission to a role (the toggle's ON half). The order of refusals is the authority order: role exists → role is not the Archon one → the key exists → the key is grantable TO THIS ROLE.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `government.manage` (all required).",
5391
+ "description": "government.manage — grant one permission to a rank (the toggle's ON half). The order of refusals is the authority order: rank exists → rank is not the Archon one → the key exists → the key is grantable TO THIS RANK.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `government.manage` (all required).",
5392
5392
  "x-rank": "archon",
5393
5393
  "x-source": "modules/government/routes/government.js",
5394
5394
  "x-permissions": [
@@ -5514,7 +5514,7 @@
5514
5514
  "government"
5515
5515
  ],
5516
5516
  "summary": "POST /government/ranks/:rankKey/reset",
5517
- "description": "government.manage — restore a SEEDED rank-role to its catalog default (ADR 0172). The counterweight to making the rank-roles editable: a rank's grant set is ~60 rows, so an owner who drifts one has no practical way back by clicking, and \"what does Trusted mean by default\" is a question only the catalog can answer. Restores catalog.seedForRank(role.seeded_rank) — the same derivation governance_002 seeded from, so the answer cannot drift from the floors even after later catalog edits. Deliberately NOT offered for a CUSTOM role: a custom role has no catalog default to return to (its \"default\" is the empty set it was created with), so the honest verb there is revoking the grants, which the toggle already does.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `government.manage` (all required).",
5517
+ "description": "government.manage — restore a SEEDED rank to its catalog default (ADR 0172). The counterweight to making the ranks editable: a rank's grant set is ~60 rows, so an owner who drifts one has no practical way back by clicking, and \"what does Trusted mean by default\" is a question only the catalog can answer. Restores catalog.seedForRank(rank.seeded_rank) — the same derivation governance_002 seeded from, so the answer cannot drift from the floors even after later catalog edits. Deliberately NOT offered for a CUSTOM rank: a custom rank has no catalog default to return to (its \"default\" is the empty set it was created with), so the honest verb there is revoking the grants, which the toggle already does.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `government.manage` (all required).",
5518
5518
  "x-rank": "archon",
5519
5519
  "x-source": "modules/government/routes/government.js",
5520
5520
  "x-permissions": [
@@ -218,15 +218,15 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
218
218
 
219
219
  | Method | Path | Rank | Body | Description |
220
220
  |---|---|---|---|---|
221
- | GET | `/api/bongos/government/assignments` | `archon` | — | government.manage — every builder↔role assignment row. |
222
- | POST | `/api/bongos/government/assignments` | `archon` | `builder_id`, `rank_key` | government.manage — assign a CUSTOM role to a builder. |
223
- | DELETE | `/api/bongos/government/assignments/:builderId/:rankKey` | `archon` | — | government.manage — remove a custom-role assignment. |
221
+ | GET | `/api/bongos/government/assignments` | `archon` | — | government.manage — every builder↔rank assignment row. |
222
+ | POST | `/api/bongos/government/assignments` | `archon` | `builder_id`, `rank_key` | government.manage — assign a CUSTOM rank to a builder. |
223
+ | DELETE | `/api/bongos/government/assignments/:builderId/:rankKey` | `archon` | — | government.manage — remove a custom-rank assignment. |
224
224
  | GET | `/api/bongos/government/permissions` | `archon` | — | government.manage — the permission catalog + principals, straight from code (the catalog is CODE, not a table); no DB read. |
225
- | GET | `/api/bongos/government/ranks` | `archon` | — | government.manage — the full role list with each role's grants. |
226
- | POST | `/api/bongos/government/ranks` | `archon` | `rank_key`, `label`, `description` | government.manage — create a CUSTOM role (is_system false, no seeded_rank — the seeded four exist only via governance_002). |
227
- | PUT | `/api/bongos/government/ranks/:rankKey/permissions/:permissionKey` | `archon` | — | government.manage — grant one permission to a role (the toggle's ON half). |
225
+ | GET | `/api/bongos/government/ranks` | `archon` | — | government.manage — the full rank list with each rank's grants. |
226
+ | POST | `/api/bongos/government/ranks` | `archon` | `rank_key`, `label`, `description` | government.manage — create a CUSTOM rank (is_system false, no seeded_rank — the seeded four exist only via governance_002). |
227
+ | PUT | `/api/bongos/government/ranks/:rankKey/permissions/:permissionKey` | `archon` | — | government.manage — grant one permission to a rank (the toggle's ON half). |
228
228
  | DELETE | `/api/bongos/government/ranks/:rankKey/permissions/:permissionKey` | `archon` | — | government.manage — revoke a grant (the toggle's OFF half). |
229
- | POST | `/api/bongos/government/ranks/:rankKey/reset` | `archon` | — | government.manage — restore a SEEDED rank-role to its catalog default (ADR 0172). |
229
+ | POST | `/api/bongos/government/ranks/:rankKey/reset` | `archon` | — | government.manage — restore a SEEDED rank to its catalog default (ADR 0172). |
230
230
 
231
231
  ## `grades` (1)
232
232
 
@@ -231,7 +231,7 @@ markdown describes, the DB enforces.**
231
231
 
232
232
  - **`example.com`** — game (Phaser + Colyseus). Static files from `public/`. `/builders` + `/builders/*` now **301-redirect to `builders.example.com/<rest>`** ([#739](https://example.com/builders#/task/739) then promoted to permanent 301 in [#741](https://example.com/builders#/task/741), ADR 0036 cutover); the apex no longer serves the builder surface directly.
233
233
  - **`status.example.com`** — public build dashboard. **Carved to the `status-ui` module in BV1.R84** ([ADR 0093](adr/<redacted>.md) §1): the surface moved from `public-status/` to `modules/status-ui/public/`, now served by the loader's web-surface seam (`moduleWebSurfaces` → `serve-internal.js`, routed on the `status.` Host prefix the manifest declares) instead of a hardcoded block — no behavior change. Layout reorganized in [#726](https://example.com/builders#/task/726) (ADR 0036): Pulse / Watch(uptime) / Architects / Works / Treasury / Lately Inscribed / The World — the grade-aggregate, estimation-drift, and repo-health tablets moved to the Archon `/watch` page. **⚠ Served OFF the droplet** (ADR 0029, outage resilience): the live page is **GitHub Pages from the separate repo `example-owner/example-status`** (CNAME → Fastly), reading data cross-origin from the droplet's `/api/gds/public/*` (CORS open) and an availability banner from the prober's `status-data` branch. The droplet still serves `modules/status-ui/public/` (on the `status.` host) as a same-origin fallback, but **DNS points at Pages, not the droplet** — so a droplet deploy does NOT update the live page. Changes to the surface publish to the mirror via `scripts/status-mirror-sync.js` (re-pointed at `modules/status-ui/public/`; status.js/style.css/cursors are byte-identical copies; index.html gets the off-box bits injected), run automatically by `.github/workflows/sync-status-mirror.yml` (needs the `STATUS_MIRROR_TOKEN` secret) ([#737](https://example.com/builders#/task/737)). **⚠ FOLLOW-UP (R84):** that workflow's path trigger still watches `public-status/**` and must be re-pointed at `modules/status-ui/public/**` by an owner/Archon — the GitHub App can't push `.github/workflows/*` (learning 107), so until then the mirror sync won't auto-fire on future status edits.
234
- - **`builders.example.com`** — dedicated builder surface (`isBuildersHost`, ADR 0036). **Carved to the `hall-ui` module in BV1.R85** ([ADR 0093](adr/<redacted>.md) §1): the surface moved from `public-builders/` to `modules/hall-ui/public/` (~13k LOC, the largest module) and is a loader-discovered web-surface module (`contributes.webSurfaces` host `builders.`). UNLIKE the generic status surface it keeps its **dedicated** serving block in `serve-internal.js` (`BUILDERS_DIR = modules/hall-ui/public/`), partitioned out of the generic web-surface loop on the `isBuildersHost` predicate. A thin rewrite shim maps every non-API path onto the existing `/builders/*` tree (so all serving + gating + asset-stamping is reused), serving the hall (`/`), the Metic+ **`/watch`** monitoring page (gated by `requireGovernancePage`), **`/settings`** (Voices + Craft prefs), plus ranks/sessions/primer/diagrams. No behavior change — byte-identical (only `window.__MODULES__.enabled` gains `hall-ui:true`). **Additive** — until its DNS A record + cert are live, the whole surface stays reachable at `example.com/builders/*`. No `.github/workflows/*` follow-up: unlike R84, no workflow path-trigger watches `public-builders/**`.
234
+ - **`builders.example.com`** — dedicated builder surface (`isBuildersHost`, ADR 0036). **Carved to the `hall-ui` module in BV1.R85** ([ADR 0093](adr/<redacted>.md) §1): the surface moved from `public-builders/` to `modules/hall-ui/public/` (~13k LOC, the largest module) and is a loader-discovered web-surface module (`contributes.webSurfaces` host `builders.`). UNLIKE the generic status surface it keeps its **dedicated** serving block in `serve-internal.js` (`BUILDERS_DIR = modules/hall-ui/public/`), partitioned out of the generic web-surface loop on the `isBuildersHost` predicate. A thin rewrite shim maps every non-API path onto the existing `/builders/*` tree (so all serving + gating + asset-stamping is reused), serving the hall (`/`), the Metic+ **`/watch`** monitoring page (gated by `requireGovernmentPage`), **`/settings`** (Voices + Craft prefs), plus ranks/sessions/primer/diagrams. No behavior change — byte-identical (only `window.__MODULES__.enabled` gains `hall-ui:true`). **Additive** — until its DNS A record + cert are live, the whole surface stays reachable at `example.com/builders/*`. No `.github/workflows/*` follow-up: unlike R84, no workflow path-trigger watches `public-builders/**`.
235
235
  - **`/downloads/devbox/*`** (every host, mounted right after the API so the builders-host shim can't rewrite it — [#904](https://example.com/builders#/task/904), [ADR 0045](adr/<redacted>.md)): branded download surface for the Dev Box desktop app. `/{mac-arm64,mac-x64,win-x64}` (+ `mac`/`win` aliases) 302 to the release assets on the public releases repo named in [`config/devbox-app.json`](../config/devbox-app.json); `latest.json` feeds the app's update check. Logic in [`src/devbox-downloads.js`](../src/devbox-downloads.js); the installers never ride the git deploy.
236
236
  - The `/api/gds/*` surface is reachable from every host. **Cross-subdomain sign-in:** the `gds_session` cookie is scoped to `.example.com` on the production hosts (apex/www/builders/status) and host-only on localhost/staging (no cross-env token bleed); the OAuth handshake runs on the canonical origin with a validated `?return=` (open-redirect-guarded) so the subdomain can land a builder back on itself.
237
237
  - Caddy gets a site block per host ([`infra/Caddyfile`](../infra/Caddyfile)), all reverse-proxying to `127.0.0.1:3000`.
@@ -141,7 +141,7 @@ or missing ranks fail closed.
141
141
  - **the four escalation keys**, which look operational but are not: `POST /api/gds/tasks/:id/confirm`
142
142
  (grader-bypass), `POST /api/gds/gate-approvals/:pr/approve` (CI deploy-gate override), protected-module
143
143
  **goal scope** ([ADR 0086](adr/<redacted>.md)), and `government.manage` (the
144
- roles/permissions tab). Delegating any one of them lets a Metic reach Archon in four steps — create a
144
+ ranks/permissions tab). Delegating any one of them lets a Metic reach Archon in four steps — create a
145
145
  protected-scope goal in the permission core, claim a task there (protected paths already floor at
146
146
  *metic* per [ADR 0043](adr/<redacted>.md)), confirm
147
147
  past the failed grade, approve their own red gate — so keeping rank Archon-only depends on keeping
@@ -152,17 +152,17 @@ or missing ranks fail closed.
152
152
 
153
153
  > **Amended by [ADR 0172](adr/<redacted>.md) (owner decision, 2026-08-16).**
154
154
  > The paragraphs above describe the SEEDED ladder, which is still what a fresh instance boots with — but
155
- > they are no longer a guarantee about a running one. The owner made the three sub-Archon **rank roles**
156
- > editable from the governance tab and chose to include the seven in-app Archon powers, `government.manage`
155
+ > they are no longer a guarantee about a running one. The owner made the three sub-Archon **ranks**
156
+ > editable from the Government tab and chose to include the seven in-app Archon powers, `government.manage`
157
157
  > and `builder.rank.set` among them. So "no system permission is grantable below archon" now reads:
158
- > *no system permission is grantable to a CUSTOM role, and none is seeded below archon* — an Archon may
159
- > nonetheless hand one to a rank role deliberately. The four-step escalation chain described above is
158
+ > *no system permission is grantable to a CUSTOM rank, and none is seeded below archon* — an Archon may
159
+ > nonetheless hand one to a rank deliberately. The four-step escalation chain described above is
160
160
  > therefore **open by decision, not by accident**: whoever holds `government.manage` can reach the rest.
161
- > Two things that did NOT move, and that bound the blast radius: the **Archon** rank-role is immutable
161
+ > Two things that did NOT move, and that bound the blast radius: the **Archon** rank is immutable
162
162
  > (nothing clickable revokes the permission needed to reach the tab), and the three **substrate** keys stay
163
- > non-grantable to every role — ADR 0043's enforcers read the rank ladder and never consult this database,
163
+ > non-grantable to every rank — ADR 0043's enforcers read the rank ladder and never consult this database,
164
164
  > so no amount of in-app escalation produces a push to `main`, a protected-path edit, or an ssh session.
165
- > `builders.rank` remains the only writer of who holds a rank role (R95b).
165
+ > `builders.rank` remains the only writer of who holds a rank (R95b).
166
166
 
167
167
  Earlier one-power delegations that ADR 0157 generalises: **task authoring** — `POST /api/gds/tasks`,
168
168
  `PATCH /api/gds/tasks/:id` (incl. the `newcomer_friendly` flag), `POST /api/gds/tasks/:id/promote`, and the
@@ -192,7 +192,7 @@ or missing ranks fail closed.
192
192
 
193
193
  **[ADR 0157](adr/<redacted>.md) added the whole operational block here** —
194
194
  20 permissions that had been Archon-gated only by ADR 0016's fail-closed default, never by a decision:
195
- the governance/monitoring **pages** (`/watch`, `/harbor`, `/gate` — `page.view.government`) and
195
+ the government/monitoring **pages** (`/watch`, `/harbor`, `/gate` — `page.view.government`) and
196
196
  `GET /api/gds/sessions/search` (whose own `/sessions` page had no server gate at all until
197
197
  task 1002710 put it behind `page.view.government` alongside its three group-mates); the
198
198
  **dev-box + provisioning fleets** (`GET /boxes[/cost-ledger]`,
package/docs/file-map.md CHANGED
@@ -37,7 +37,7 @@
37
37
  ├── brand/ ← repo-canonical design system (ADR 0081)
38
38
  │ └── cloud-bongos/ ← Cloud Bongos brand: README design guide + tokens.json + regen_*.py + assets/ (mascot, 9 animation states, Docker-homage sky-whale, coin logo + Baloo 2 wordmark, painted code-glyph charms). Built under task 1506 (mascot restyle 1529, code glyphs 1527); illustrations generated via Gemini (modules/art-pipeline/pipeline gen_api).
39
39
  │
40
- ├── config/ ← per-instance config packs. Each pair resolves env → `X.json` (this host's, boundary-excluded) → `X.neutral.json` (the portable starter, the twin that publishes with the core): branding (ADR 0062 §3), modules, design-tokens, and governance role templates (BV1.R96 — labels/descriptions/custom-role templates only; live grants + assignments are DB rows).
40
+ ├── config/ ← per-instance config packs. Each pair resolves env → `X.json` (this host's, boundary-excluded) → `X.neutral.json` (the portable starter, the twin that publishes with the core): branding (ADR 0062 §3), modules, design-tokens, and government rank templates (BV1.R96 — labels/descriptions/custom-rank templates only; live grants + assignments are DB rows).
41
41
  │
42
42
  ├── docs/
43
43
  │ ├── adr/ ← Architecture Decision Records
@@ -125,27 +125,27 @@ const LEVEL_CONTRACT = Object.freeze({
125
125
  // not name carries NO wandering metadata and resolves to the safest floor
126
126
  // (Locked), the same fail-closed posture the resolver takes for a rank with no
127
127
  // seeded role.
128
- const ROLE_WANDERING = Object.freeze({
128
+ const RANK_WANDERING = Object.freeze({
129
129
  xenos: Object.freeze({ default: 0, ceiling: 0 }),
130
130
  thetes: Object.freeze({ default: 1, ceiling: 1 }),
131
131
  metic: Object.freeze({ default: 2, ceiling: 3 }),
132
132
  archon: Object.freeze({ default: 3, ceiling: 3 }),
133
133
  });
134
- const NO_ROLE_WANDERING = Object.freeze({ default: 0, ceiling: 0 });
134
+ const NO_RANK_WANDERING = Object.freeze({ default: 0, ceiling: 0 });
135
135
 
136
- function roleWandering(roleKey) {
137
- return Object.prototype.hasOwnProperty.call(ROLE_WANDERING, roleKey)
138
- ? ROLE_WANDERING[roleKey]
139
- : NO_ROLE_WANDERING;
136
+ function rankWandering(rankKey) {
137
+ return Object.prototype.hasOwnProperty.call(RANK_WANDERING, rankKey)
138
+ ? RANK_WANDERING[rankKey]
139
+ : NO_RANK_WANDERING;
140
140
  }
141
141
 
142
142
  // DERIVED projections of the metadata above, kept because scripts/gds/session-eval.js
143
143
  // and the /me payload read them by rank. Deriving them means the two views cannot drift.
144
144
  const RANK_DEFAULT = Object.freeze(
145
- Object.fromEntries(Object.entries(ROLE_WANDERING).map(([role, meta]) => [role, meta.default])),
145
+ Object.fromEntries(Object.entries(RANK_WANDERING).map(([role, meta]) => [role, meta.default])),
146
146
  );
147
147
  const RANK_MAX = Object.freeze(
148
- Object.fromEntries(Object.entries(ROLE_WANDERING).map(([role, meta]) => [role, meta.ceiling])),
148
+ Object.fromEntries(Object.entries(RANK_WANDERING).map(([role, meta]) => [role, meta.ceiling])),
149
149
  );
150
150
 
151
151
  const TOP_LEVEL = LEVELS.length - 1; // 3
@@ -160,7 +160,7 @@ for (const key of LEVELS) {
160
160
  for (let i = 0; i < LEVELS.length; i++) {
161
161
  if (LEVEL_META[LEVELS[i]].n !== i) throw new Error(`wandering-prefs: LEVEL_META.${LEVELS[i]}.n must equal its index ${i}`);
162
162
  }
163
- for (const [role, meta] of Object.entries(ROLE_WANDERING)) {
163
+ for (const [role, meta] of Object.entries(RANK_WANDERING)) {
164
164
  if (!isLevelInt(meta.default)) throw new Error(`wandering-prefs: role '${role}' has a non-level default`);
165
165
  if (!isLevelInt(meta.ceiling)) throw new Error(`wandering-prefs: role '${role}' has a non-level ceiling`);
166
166
  if (meta.default > meta.ceiling) throw new Error(`wandering-prefs: default > ceiling for role '${role}'`);
@@ -182,11 +182,11 @@ function levelIndex(key) {
182
182
  }
183
183
 
184
184
  function defaultLevelForRank(rank) {
185
- return roleWandering(rank).default;
185
+ return rankWandering(rank).default;
186
186
  }
187
187
 
188
188
  function maxLevelForRank(rank) {
189
- return roleWandering(rank).ceiling;
189
+ return rankWandering(rank).ceiling;
190
190
  }
191
191
 
192
192
  // The override a builder has stored (the raw 'wandering' key off behavior_prefs),
@@ -303,8 +303,8 @@ module.exports = {
303
303
  LEVELS,
304
304
  LEVEL_META,
305
305
  LEVEL_CONTRACT,
306
- ROLE_WANDERING,
307
- roleWandering,
306
+ RANK_WANDERING,
307
+ rankWandering,
308
308
  RANK_DEFAULT,
309
309
  RANK_MAX,
310
310
  TOP_LEVEL,
@@ -1,44 +1,44 @@
1
- // modules/governance/catalog.js — the permission catalog + compatibility seed.
1
+ // modules/government/catalog.js — the permission catalog + compatibility seed.
2
2
  //
3
3
  // The single CODE source of truth for authority atoms (ADR 0151, BV1.R94). Every
4
4
  // permission is a stable string key that maps to a REAL server capability (a route,
5
5
  // a claim rule, a page) — a permission no route consumes does not exist. The
6
- // resolver (R95), the requirePermission gate (R97), R93's role seed, and the Archon
6
+ // resolver (R95), the requirePermission gate (R97), R93's rank seed, and the Archon
7
7
  // tab (R110/R111) all read from here; the enumerated catalog it transcribes is
8
8
  // docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md, grounded in
9
9
  // docs/governance/authority-sweep-2026-07-24.md.
10
10
  //
11
11
  // Two orthogonal axes per permission:
12
- // • system — true = trust-boundary. The governance API (R110) rejects adding it
13
- // to a CUSTOM role and the tab renders it LOCKED there; it is deploy/
12
+ // • system — true = trust-boundary. The government API (R110) rejects adding it
13
+ // to a CUSTOM rank and the tab renders it LOCKED there; it is deploy/
14
14
  // protected-path/rank-set/version-close/grade-bypass/cross-tier/
15
15
  // principal power, enforced by the out-of-band substrate (ADR 0043)
16
- // or reserved to Archon. The built-in `archon` rank-role legitimately
16
+ // or reserved to Archon. The built-in `archon` rank legitimately
17
17
  // HOLDS the archon-floored system perms.
18
- // — false = operational, freely composable into a custom role.
18
+ // — false = operational, freely composable into a custom rank.
19
19
  // `system` is NOT the whole grantability answer any more — see the
20
- // `substrate` axis below and `isGrantableTo()`, which is per-ROLE.
20
+ // `substrate` axis below and `isGrantableTo()`, which is per-RANK.
21
21
  // • substrate — true = this key's authority is enforced OUTSIDE the app, by the
22
22
  // pre-push hook / main-audit / the rank floor the git and ssh layers
23
23
  // read (ADR 0043). A grant row for one is INERT: no route consumes it
24
24
  // through requirePermission, so granting it would be a toggle that
25
25
  // looks like it worked and changes nothing. Therefore non-grantable to
26
- // EVERY role, seeded rank-roles included — the one wall ADR 0172's
26
+ // EVERY rank, seeded ranks included — the one wall ADR 0172's
27
27
  // widening deliberately did not move, because moving it would make the
28
28
  // tab lie rather than delegate. Implies system:true + floor 'archon'.
29
29
  // • floor — the rank that holds this permission TODAY via the compat seed
30
30
  // ('xenos' < 'thetes' < 'metic' < 'archon'), or null for a machine
31
- // PRINCIPAL permission held by NO rank-role (gated on exact
32
- // system_role, never a human/role — ADR 0151 §3). floor drives the
31
+ // PRINCIPAL permission held by NO rank (gated on exact
32
+ // system_role, never a human/rank — ADR 0151 §3). floor drives the
33
33
  // seed derivation below; it is seed DATA, not grantability (the
34
34
  // `system` flag is the wall). Ownership-scoped + baseline own-work
35
35
  // perms floor at 'xenos' (every authenticated builder holds them).
36
36
  //
37
37
  // PRINCIPALS — the second authority AXIS (BV1.R98, ADR 0151 §3). A builder's
38
- // authority is the UNION of their ROLES; a machine principal's authority is its
38
+ // authority is the UNION of their RANKS; a machine principal's authority is its
39
39
  // IDENTITY. Those are different axes and the model keeps them apart: a principal
40
40
  // permission carries `principal: '<system_role>'` and `floor: null`, so it is in
41
- // NO rank-role (not even archon's) and no custom role may hold it (`system:true`).
41
+ // NO rank (not even archon's) and no custom rank may hold it (`system:true`).
42
42
  // The two are a PARTITION, asserted at load: floor:null ⟺ principal set. The only
43
43
  // source of a principal permission is resolver.resolvePrincipalPermissions(), keyed
44
44
  // on the exact `builders.system_role` value — a column with no API writer at all
@@ -77,10 +77,10 @@
77
77
  // Archon included), and minting a key no route consumes to satisfy a symmetry
78
78
  // check would violate this module's first rule.
79
79
  //
80
- // The RANK_ROLE_SEED is DERIVED from floors (cumulative nested supersets), so the
80
+ // The RANK_SEED is DERIVED from floors (cumulative nested supersets), so the
81
81
  // "zero day-one behavior change" parity (R103) is structural, not hand-maintained:
82
- // role(rank) = { p : p.floor !== null and tier(p.floor) <= tier(rank) }, and the
83
- // resolver's UNION over a builder's one rank-role reproduces requireRank's
82
+ // rank(rank) = { p : p.floor !== null and tier(p.floor) <= tier(rank) }, and the
83
+ // resolver's UNION over a builder's one rank reproduces requireRank's
84
84
  // at-or-above threshold exactly.
85
85
 
86
86
  'use strict';
@@ -96,7 +96,7 @@ const PERMISSIONS = [
96
96
  // ── system: true — trust-boundary (substrate- or Archon-enforced) ─────────────
97
97
  //
98
98
  // The seven WITHOUT `substrate` are in-app Archon powers: a real route reads them
99
- // through requirePermission, so granting one to a sub-Archon rank-role genuinely
99
+ // through requirePermission, so granting one to a sub-Archon rank genuinely
100
100
  // delegates it (ADR 0172, the owner's lever). The three WITH `substrate: true` are
101
101
  // enforced out of band and can never be delegated by a row in this database.
102
102
  { key: 'builder.rank.set', system: true, floor: 'archon', guards: 'PATCH /builders/:id/rank — the single rank writer' },
@@ -112,7 +112,7 @@ const PERMISSIONS = [
112
112
  { key: 'principal.bfg', system: true, floor: null, principal: 'bfg', guards: '/memory/bfg/* — requireBfgPrincipal (exact system_role=bfg; NO rank holds it)' },
113
113
  { key: 'principal.discord_bot', system: true, floor: null, principal: 'discord_bot', guards: 'Discord bot routes — requireDiscordBot (exact system_role; dormant)' },
114
114
 
115
- // ── operational — floor archon (admin ops; grantable to a custom role) ────────
115
+ // ── operational — floor archon (admin ops; grantable to a custom rank) ────────
116
116
  //
117
117
  // ADR 0157 (owner decision, 2026-08-05) narrowed this block to IDENTITY: the Archon
118
118
  // rank is responsible for rank and identity only. Nineteen keys that sat here for
@@ -166,15 +166,15 @@ const PERMISSIONS = [
166
166
  // and operations: they spend, read, adjudicate, and shuffle work, but none of them
167
167
  // decides WHO HOLDS AUTHORITY, so the trusted working rank carries them. All were
168
168
  // floor 'archon' before this ADR; none is system:true, so all were already
169
- // delegable to a custom role — this only lowers the seeded floor.
169
+ // delegable to a custom rank — this only lowers the seeded floor.
170
170
  //
171
171
  // Deliberately NOT in this block, though the owner's rule would otherwise reach them:
172
172
  // • `version.close` — retained at archon; it is the DISPOSITION that finalises a
173
173
  // version's scope-truth, and canonical-permissions has long paired it with rank
174
174
  // as the two powers that never delegate. ADR 0157 keeps that pairing.
175
175
  // • `dependency.cross_tier.manage` — system:true, and the catalog's own invariant
176
- // (tests/governance_catalog.mjs) forbids granting a system key to metic. Moving
177
- // it would mean clearing the system flag, which widens what any CUSTOM role can
176
+ // (tests/government_catalog.mjs) forbids granting a system key to metic. Moving
177
+ // it would mean clearing the system flag, which widens what any CUSTOM rank can
178
178
  // be granted — a second-order change beyond the delegation that was asked for.
179
179
  { key: 'builder.budget.set', system: false, floor: 'metic', guards: 'PATCH /builders/:id/budget (ADR 0157, was archon)' },
180
180
  { key: 'criterion.unsatisfy', system: false, floor: 'metic', guards: 'POST /done-when/:id/unsatisfy (ADR 0157, was archon)' },
@@ -185,7 +185,7 @@ const PERMISSIONS = [
185
185
  // siblings are `goal.create` and `task.create`, both operational. Close is the
186
186
  // DISPOSITION that finalises a version's scope-truth; create opens it — the symmetry
187
187
  // argument was considered and rejected, because system:true would permanently bar
188
- // delegating version authoring to a custom role for no trust-boundary reason.
188
+ // delegating version authoring to a custom rank for no trust-boundary reason.
189
189
  // ADR 0157 then lowered create's floor to metic while close stayed archon, which is
190
190
  // the same asymmetry read from the other side: authoring delegates, disposition does not.
191
191
  { key: 'version.create', system: false, floor: 'metic', guards: 'POST /versions — create a version + its done-when criteria (ADR 0157, was archon)' },
@@ -237,7 +237,7 @@ const PERMISSIONS = [
237
237
  // working rank. The gate FUNCTION was renamed to match (`requireArchonPage` →
238
238
  // `requireGovernmentPage`) once the floor moved. Viewing is all this grants: every
239
239
  // write those pages reach is its own atom, and `government.manage` (the
240
- // roles/permissions tab) stayed archon.
240
+ // ranks/permissions tab) stayed archon.
241
241
  { key: 'page.view.government', system: false, floor: 'metic', guards: 'the government/monitoring pages — /watch, /harbor, /gate (requireGovernmentPage; write = government.manage, archon)' },
242
242
  ];
243
243
 
@@ -249,70 +249,70 @@ const _byKey = new Map();
249
249
  const _byPrincipal = new Map();
250
250
  const _byResource = new Map();
251
251
  for (const p of PERMISSIONS) {
252
- if (typeof p.key !== 'string' || !p.key) throw new Error(`governance catalog: permission missing a key`);
253
- if (typeof p.system !== 'boolean') throw new Error(`governance catalog: ${p.key} missing boolean 'system'`);
254
- if (p.floor !== null && !RANK_ORDER.includes(p.floor)) throw new Error(`governance catalog: ${p.key} has invalid floor "${p.floor}"`);
255
- if (_byKey.has(p.key)) throw new Error(`governance catalog: duplicate key "${p.key}"`);
252
+ if (typeof p.key !== 'string' || !p.key) throw new Error(`government catalog: permission missing a key`);
253
+ if (typeof p.system !== 'boolean') throw new Error(`government catalog: ${p.key} missing boolean 'system'`);
254
+ if (p.floor !== null && !RANK_ORDER.includes(p.floor)) throw new Error(`government catalog: ${p.key} has invalid floor "${p.floor}"`);
255
+ if (_byKey.has(p.key)) throw new Error(`government catalog: duplicate key "${p.key}"`);
256
256
  const principal = p.principal ?? null;
257
257
  if (principal !== null && (typeof principal !== 'string' || !principal)) {
258
- throw new Error(`governance catalog: ${p.key} has a non-string principal identity`);
258
+ throw new Error(`government catalog: ${p.key} has a non-string principal identity`);
259
259
  }
260
- // The two axes PARTITION the catalog: a permission is held by a rank-role (a real
260
+ // The two axes PARTITION the catalog: a permission is held by a rank (a real
261
261
  // floor) XOR by a machine identity (floor null + a principal). Neither-nor would be
262
- // a permission nothing can ever hold; both-and would let a role reach an identity.
262
+ // a permission nothing can ever hold; both-and would let a rank reach an identity.
263
263
  if ((p.floor === null) !== (principal !== null)) {
264
- throw new Error(`governance catalog: ${p.key} — floor:null and a principal identity must come together (roles and identities are separate axes)`);
264
+ throw new Error(`government catalog: ${p.key} — floor:null and a principal identity must come together (ranks and identities are separate axes)`);
265
265
  }
266
266
  if (p.key.startsWith(PRINCIPAL_KEY_PREFIX) !== (principal !== null)) {
267
- throw new Error(`governance catalog: ${p.key} — a "${PRINCIPAL_KEY_PREFIX}*" key and a principal identity must come together`);
267
+ throw new Error(`government catalog: ${p.key} — a "${PRINCIPAL_KEY_PREFIX}*" key and a principal identity must come together`);
268
268
  }
269
269
  if (principal !== null) {
270
- if (!p.system) throw new Error(`governance catalog: ${p.key} is a principal permission and must be system:true (never grantable to a role)`);
271
- if (_byPrincipal.has(principal)) throw new Error(`governance catalog: duplicate principal identity "${principal}"`);
270
+ if (!p.system) throw new Error(`government catalog: ${p.key} is a principal permission and must be system:true (never grantable to a rank)`);
271
+ if (_byPrincipal.has(principal)) throw new Error(`government catalog: duplicate principal identity "${principal}"`);
272
272
  }
273
273
  // ── the SUBSTRATE axis (ADR 0172) ─────────────────────────────────────────
274
274
  // A substrate key is enforced by the out-of-band layer (pre-push hook, main-audit,
275
- // the rank floor git/ssh read), so no role row can confer it. Three shape rules,
275
+ // the rank floor git/ssh read), so no rank row can confer it. Three shape rules,
276
276
  // each closing a way the marker could quietly stop meaning that:
277
277
  const substrate = p.substrate ?? false;
278
- if (typeof substrate !== 'boolean') throw new Error(`governance catalog: ${p.key} has a non-boolean 'substrate'`);
278
+ if (typeof substrate !== 'boolean') throw new Error(`government catalog: ${p.key} has a non-boolean 'substrate'`);
279
279
  if (substrate) {
280
280
  // Substrate power is trust-boundary power by definition — a substrate key that
281
- // was somehow operational would be freely grantable to a custom role.
282
- if (!p.system) throw new Error(`governance catalog: ${p.key} is substrate-enforced and must be system:true`);
281
+ // was somehow operational would be freely grantable to a custom rank.
282
+ if (!p.system) throw new Error(`government catalog: ${p.key} is substrate-enforced and must be system:true`);
283
283
  // A substrate key belongs to no machine identity: the two are different reasons
284
284
  // a key is unreachable, and collapsing them would hide one behind the other.
285
- if (principal !== null) throw new Error(`governance catalog: ${p.key} cannot be both substrate-enforced and a machine principal`);
285
+ if (principal !== null) throw new Error(`government catalog: ${p.key} cannot be both substrate-enforced and a machine principal`);
286
286
  // The out-of-band enforcers all compare against the top of the ladder. A lower
287
- // floor would seed the key into a rank-role that the substrate then still denies.
288
- if (p.floor !== 'archon') throw new Error(`governance catalog: ${p.key} is substrate-enforced and must floor at 'archon' (the substrate compares against the top of the ladder)`);
287
+ // floor would seed the key into a rank that the substrate then still denies.
288
+ if (p.floor !== 'archon') throw new Error(`government catalog: ${p.key} is substrate-enforced and must floor at 'archon' (the substrate compares against the top of the ladder)`);
289
289
  }
290
290
  // ── the OWNERSHIP axis (R100) ─────────────────────────────────────────────
291
291
  const scope = p.scope ?? null;
292
292
  const resource = p.resource ?? null;
293
293
  if (scope !== null && !OWNERSHIP_SCOPES.includes(scope)) {
294
- throw new Error(`governance catalog: ${p.key} has invalid scope "${scope}" (expected 'own', 'any', or absent)`);
294
+ throw new Error(`government catalog: ${p.key} has invalid scope "${scope}" (expected 'own', 'any', or absent)`);
295
295
  }
296
296
  // An ownership SCOPE and the RESOURCE it is scoped over are one fact spelled in
297
297
  // two fields; either alone is meaningless ('own' of what? / a resource nothing
298
298
  // is scoped over). Same partition posture as floor ⟺ principal above.
299
299
  if ((scope === null) !== (resource === null)) {
300
- throw new Error(`governance catalog: ${p.key} — scope and resource must come together (an ownership scope is always scoped over a named resource)`);
300
+ throw new Error(`government catalog: ${p.key} — scope and resource must come together (an ownership scope is always scoped over a named resource)`);
301
301
  }
302
302
  if (resource !== null && (typeof resource !== 'string' || !resource)) {
303
- throw new Error(`governance catalog: ${p.key} has a non-string resource family`);
303
+ throw new Error(`government catalog: ${p.key} has a non-string resource family`);
304
304
  }
305
305
  // The namespace can never outrun the data: a key that CALLS itself own-scoped
306
306
  // must BE declared own-scoped, or a gate reading the declared axis would treat a
307
307
  // parameterized permission as an unconditional grant. Deliberately one-way — the
308
308
  // converse would be false (me.wandering.set is own-scoped without the suffix).
309
309
  if (p.key.endsWith(OWN_KEY_SUFFIX) && scope !== 'own') {
310
- throw new Error(`governance catalog: ${p.key} ends in "${OWN_KEY_SUFFIX}" but does not declare scope:'own' — an ownership-scoped key must say so in data`);
310
+ throw new Error(`government catalog: ${p.key} ends in "${OWN_KEY_SUFFIX}" but does not declare scope:'own' — an ownership-scoped key must say so in data`);
311
311
  }
312
- // An ownership scope NARROWS a role-held permission; a machine identity has no
312
+ // An ownership scope NARROWS a rank-held permission; a machine identity has no
313
313
  // owner axis, so the two never compose.
314
314
  if (scope !== null && p.floor === null) {
315
- throw new Error(`governance catalog: ${p.key} is ownership-scoped but has no rank floor — the ownership axis narrows a ROLE permission, never an identity one`);
315
+ throw new Error(`government catalog: ${p.key} is ownership-scoped but has no rank floor — the ownership axis narrows a RANK permission, never an identity one`);
316
316
  }
317
317
  const frozen = Object.freeze({ ...p, principal, scope, resource, substrate });
318
318
  _byKey.set(p.key, frozen);
@@ -332,15 +332,15 @@ Object.freeze(PERMISSIONS);
332
332
  const floorTier = (rank) => RANK_ORDER.indexOf(rank);
333
333
  for (const [resource, fam] of _byResource) {
334
334
  if (fam.any.length > 0 && fam.own.length === 0) {
335
- throw new Error(`governance catalog: resource "${resource}" declares a cross-owner permission (${fam.any.join(', ')}) with no ownership-scoped counterpart`);
335
+ throw new Error(`government catalog: resource "${resource}" declares a cross-owner permission (${fam.any.join(', ')}) with no ownership-scoped counterpart`);
336
336
  }
337
337
  // Reaching EVERY owner's row must never be cheaper than reaching your own. If it
338
338
  // were, a rank could manage the whole fleet but not its own box — the model
339
- // inverted, and the governance tab would render the wider grant as the safer one.
339
+ // inverted, and the Government tab would render the wider grant as the safer one.
340
340
  for (const anyKey of fam.any) {
341
341
  for (const ownKey of fam.own) {
342
342
  if (floorTier(_byKey.get(anyKey).floor) < floorTier(_byKey.get(ownKey).floor)) {
343
- throw new Error(`governance catalog: ${anyKey} (floor ${_byKey.get(anyKey).floor}) sits BELOW ${ownKey} (floor ${_byKey.get(ownKey).floor}) — cross-owner authority must never be cheaper than own-only`);
343
+ throw new Error(`government catalog: ${anyKey} (floor ${_byKey.get(anyKey).floor}) sits BELOW ${ownKey} (floor ${_byKey.get(ownKey).floor}) — cross-owner authority must never be cheaper than own-only`);
344
344
  }
345
345
  }
346
346
  }
@@ -382,9 +382,9 @@ function isSystem(key) {
382
382
  const p = _byKey.get(key);
383
383
  return p ? p.system : null; // null = unknown key (caller fails closed)
384
384
  }
385
- // Grantable to a CUSTOM role = exists and not system. Unknown key → false (fail-closed).
386
- // This is the NARROW question — the custom-role wall, unchanged by ADR 0172 and
387
- // still what config.js strips against. For the per-role answer use isGrantableTo().
385
+ // Grantable to a CUSTOM rank = exists and not system. Unknown key → false (fail-closed).
386
+ // This is the NARROW question — the custom-rank wall, unchanged by ADR 0172 and
387
+ // still what config.js strips against. For the per-rank answer use isGrantableTo().
388
388
  function isGrantable(key) {
389
389
  const p = _byKey.get(key);
390
390
  return !!p && !p.system;
@@ -406,40 +406,40 @@ function substrateKeys() {
406
406
  return PERMISSIONS.filter((p) => p.substrate).map((p) => p.key);
407
407
  }
408
408
 
409
- // ── grantability, per ROLE (ADR 0172) ──────────────────────────────────────────
409
+ // ── grantability, per RANK (ADR 0172) ──────────────────────────────────────────
410
410
  //
411
- // The owner's 2026-08-16 decision made the three sub-Archon RANK roles editable, so
412
- // "is this grantable" stopped being a property of the key alone. `role` is a
411
+ // The owner's 2026-08-16 decision made the three sub-Archon RANK ranks editable, so
412
+ // "is this grantable" stopped being a property of the key alone. `rank` is a
413
413
  // government_ranks row ({ is_system, seeded_rank }); the four answers:
414
414
  //
415
- // • NEVER, to any role — a machine-principal key (held by an identity, not a role)
415
+ // • NEVER, to any rank — a machine-principal key (held by an identity, not a rank)
416
416
  // and a SUBSTRATE key. The substrate three are inert as grant rows: no route
417
417
  // consumes them through requirePermission, so a toggle for one would report
418
418
  // success and delegate nothing. Refusing them keeps the tab honest.
419
- // • the ARCHON rank-role — nothing. It is immutable and already holds everything,
420
- // and that immutability is also the LOCKOUT GUARD: the only role carrying
419
+ // • the ARCHON rank — nothing. It is immutable and already holds everything,
420
+ // and that immutability is also the LOCKOUT GUARD: the only rank carrying
421
421
  // `government.manage` by seed can never have it revoked, so no sequence of
422
422
  // clicks in this tab can lock the owner out of the tab.
423
- // • a SEEDED rank-role BELOW archon — everything else, the seven in-app Archon
423
+ // • a SEEDED rank BELOW archon — everything else, the seven in-app Archon
424
424
  // powers included. This is the delegation lever the owner asked for.
425
- // • a CUSTOM role — operational only: the pre-existing R110 wall, deliberately NOT
426
- // widened. A rank-role is the owner's own coarse ladder, moved by a rank write;
427
- // a custom role is ad-hoc and assignable by anyone holding `government.manage`,
425
+ // • a CUSTOM rank — operational only: the pre-existing R110 wall, deliberately NOT
426
+ // widened. A rank is the owner's own coarse ladder, moved by a rank write;
427
+ // a custom rank is ad-hoc and assignable by anyone holding `government.manage`,
428
428
  // so widening it would hand the second group the first group's lever.
429
429
  //
430
- // Fail-closed on everything unrecognised: unknown key, missing role, or a system role
430
+ // Fail-closed on everything unrecognised: unknown key, missing rank, or a system rank
431
431
  // whose seeded_rank is not a real rank all answer false.
432
- function isGrantableTo(key, role) {
432
+ function isGrantableTo(key, rank) {
433
433
  const p = _byKey.get(key);
434
- if (!p || !role || typeof role !== 'object') return false;
434
+ if (!p || !rank || typeof rank !== 'object') return false;
435
435
  if (p.principal !== null || p.substrate) return false;
436
- if (role.is_system) {
437
- return RANK_ORDER.includes(role.seeded_rank) && role.seeded_rank !== 'archon';
436
+ if (rank.is_system) {
437
+ return RANK_ORDER.includes(rank.seeded_rank) && rank.seeded_rank !== 'archon';
438
438
  }
439
439
  return !p.system;
440
440
  }
441
441
 
442
- // ── the principal axis (identity, not roles) ────────────────────────────────────
442
+ // ── the principal axis (identity, not ranks) ────────────────────────────────────
443
443
  // Reads only. Unknown/blank input fails closed to null/false — the same posture the
444
444
  // rest of the catalog takes, and the one the identity gates depend on.
445
445
  function principals() {
@@ -451,7 +451,7 @@ function principalRoles() {
451
451
  function principalKeys() {
452
452
  return PRINCIPALS.map((p) => p.permission);
453
453
  }
454
- // Is this key held by a machine identity rather than by any role? Unknown → false.
454
+ // Is this key held by a machine identity rather than by any rank? Unknown → false.
455
455
  function isPrincipalKey(key) {
456
456
  const p = _byKey.get(key);
457
457
  return !!p && p.principal !== null;
@@ -503,17 +503,17 @@ function crossOwnerKeysFor(key) {
503
503
  return fam ? fam.any : NO_KEYS;
504
504
  }
505
505
 
506
- // The compat seed, DERIVED from floors (cumulative supersets). A rank-role holds
506
+ // The compat seed, DERIVED from floors (cumulative supersets). A rank holds
507
507
  // every permission whose floor is a real rank at-or-below it; principals (floor
508
- // null) are held by NO rank-role. This is the "zero day-one change" guarantee —
509
- // role(rank) reproduces requireRank(rank)'s at-or-above threshold exactly (R103).
508
+ // null) are held by NO rank. This is the "zero day-one change" guarantee —
509
+ // rank(rank) reproduces requireRank(rank)'s at-or-above threshold exactly (R103).
510
510
  function seedForRank(rank) {
511
511
  const tier = RANK_ORDER.indexOf(rank);
512
- if (tier < 0) return []; // unknown/reserved rank → no role → no permissions (fail-closed; ADR 0151)
512
+ if (tier < 0) return []; // unknown/reserved rank → no rank → no permissions (fail-closed; ADR 0151)
513
513
  return PERMISSIONS.filter((p) => p.floor !== null && RANK_ORDER.indexOf(p.floor) <= tier).map((p) => p.key);
514
514
  }
515
515
 
516
- const RANK_ROLE_SEED = Object.freeze(
516
+ const RANK_SEED = Object.freeze(
517
517
  Object.fromEntries(RANK_ORDER.map((r) => [r, Object.freeze(seedForRank(r))])),
518
518
  );
519
519
 
@@ -530,7 +530,7 @@ module.exports = {
530
530
  systemKeys,
531
531
  grantableKeys,
532
532
  seedForRank,
533
- RANK_ROLE_SEED,
533
+ RANK_SEED,
534
534
  PRINCIPALS,
535
535
  principals,
536
536
  principalRoles,