@bongos/core 1.19.233 → 1.19.235

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.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.233",
6
- "core_contract": "1.19.233",
7
- "source_commit": "17f28d2147bc72ed6a97cbc256e388a889e7ddec",
5
+ "core_version": "1.19.235",
6
+ "core_contract": "1.19.235",
7
+ "source_commit": "b91cbff1532b00e3cdd2204e713b35cf093c9b02",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-18T16:32:54.929Z",
9
+ "built_at": "2026-08-18T16:46:17.722Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 311,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1317,
14
+ "functional_verbatim": 1319,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1649,
20
- "tree_sha256": "211de1015cfb5ba8ea8bb0e6aa7467566e296ffea14ebc4cabd2875e21fb6b2c",
19
+ "file_count": 1651,
20
+ "tree_sha256": "d20a6edcee7ad8a7e81eefec0b07689a820b194634cdb5350c8c4263127990ea",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -347,12 +347,12 @@
347
347
  {
348
348
  "path": "clients/bongos-client/README.md",
349
349
  "mode": "0000644",
350
- "sha256": "841ea9c45ad482286f056e885cd25c6769931134a2ff066b638d7585a097888e"
350
+ "sha256": "5084f59df8019560f49971f00b40e9b8d1d6eb9511a9a78fc136ecaff025e57d"
351
351
  },
352
352
  {
353
353
  "path": "clients/bongos-client/bongos-client.global.js",
354
354
  "mode": "0000644",
355
- "sha256": "a9c482ca4c6d2cc313c76f3e098c3bdaa802f5c1253c749e3b5df6dfbae06463"
355
+ "sha256": "84050a82e7dc6f855b6b02a4c89b1321717b1fcf45fdc426ac6c53961f10012f"
356
356
  },
357
357
  {
358
358
  "path": "clients/bongos-client/examples/hello-world.mjs",
@@ -362,17 +362,17 @@
362
362
  {
363
363
  "path": "clients/bongos-client/index.cjs",
364
364
  "mode": "0000644",
365
- "sha256": "fafd3c715b3eb716d7f8355001cf4ea4a8254b78fd757c4a4343ad673a4f1bd8"
365
+ "sha256": "5c99db7b6b1b59e595c3c6a7002cf7baa3b130a198213b007448d02a427883f1"
366
366
  },
367
367
  {
368
368
  "path": "clients/bongos-client/index.d.ts",
369
369
  "mode": "0000644",
370
- "sha256": "dc1ef3a4e2843b836e828e5c8d290861dda56844e4f5727ad5d9b3001d7e4e6b"
370
+ "sha256": "14c4cc02b3965badc021489bb7dd0f80bb71c3cc57b2e1dc4dafa76d86128088"
371
371
  },
372
372
  {
373
373
  "path": "clients/bongos-client/index.mjs",
374
374
  "mode": "0000644",
375
- "sha256": "48ea243ec0016c7dc2842322100406c339b2b7d05bccf8f0119060fc0cf7e5b2"
375
+ "sha256": "d5de9d514dc1515fab89430301550566fe5c8c5941026c58eb107800746fb63c"
376
376
  },
377
377
  {
378
378
  "path": "clients/bongos-client/package.json",
@@ -1372,12 +1372,12 @@
1372
1372
  {
1373
1373
  "path": "docs/api-reference.md",
1374
1374
  "mode": "0000644",
1375
- "sha256": "18317d38ec567284fbb3fd3e351d8fecd10c0aa9371b353128e1e9ff0e6e7717"
1375
+ "sha256": "c0a604b304ff9936d08998b2b4206e7ecad8ff668ec704b08161c5d79d0bc04c"
1376
1376
  },
1377
1377
  {
1378
1378
  "path": "docs/api/openapi.json",
1379
1379
  "mode": "0000644",
1380
- "sha256": "f2f81530200394e99b8591d43839ba024ab0e055331f86c119fa0e5ab39abbce"
1380
+ "sha256": "be4bc7b6dd627fa7ea85e4a90a908588b81d6d5a5d4eb5a6f2c1858f155538f7"
1381
1381
  },
1382
1382
  {
1383
1383
  "path": "docs/architecture.md",
@@ -2902,7 +2902,7 @@
2902
2902
  {
2903
2903
  "path": "modules/government/board.js",
2904
2904
  "mode": "0000644",
2905
- "sha256": "98c0543844eb7a2143fc9541098e067702d3b76b4a546034b050a45105403d1a"
2905
+ "sha256": "9d3d9414ca1c1bafc901a7343cdccafc7261b0dba492134a9a1064e6b3d82b61"
2906
2906
  },
2907
2907
  {
2908
2908
  "path": "modules/government/catalog.js",
@@ -2917,7 +2917,7 @@
2917
2917
  {
2918
2918
  "path": "modules/government/db.js",
2919
2919
  "mode": "0000644",
2920
- "sha256": "dd937ea64d6e4cd6ff677c136c5b8a493fecbf75b63082d9449b4a4390d1a0af"
2920
+ "sha256": "ab75e3e0fd2b7eee747e525403f8a2ea863dbf1f4332cb3c6ec1b413ed7af7e3"
2921
2921
  },
2922
2922
  {
2923
2923
  "path": "modules/government/migrations/governance_001_tables.sql",
@@ -3027,7 +3027,7 @@
3027
3027
  {
3028
3028
  "path": "modules/government/routes/government.js",
3029
3029
  "mode": "0000644",
3030
- "sha256": "2c7ae89e1e8e91107a8e0a55cc82d6ee4f8804fedb76d07dbe5e502c4801e187"
3030
+ "sha256": "bf07f55f73d27d738840f4f0e0ffcb8a6d19225e7500dbb36e4a83a1109635db"
3031
3031
  },
3032
3032
  {
3033
3033
  "path": "modules/government/routes/rank-sync.js",
@@ -3272,12 +3272,12 @@
3272
3272
  {
3273
3273
  "path": "modules/hall-ui/public/government.html",
3274
3274
  "mode": "0000644",
3275
- "sha256": "ffd03d1fefcd4c4639f66812fe35dd3f74a2c65ed02a576a5d9703883df10736"
3275
+ "sha256": "87ec2d8c002edf0a440e658cb29e567a07544bc36bb535d4cb0fd5130629a92e"
3276
3276
  },
3277
3277
  {
3278
3278
  "path": "modules/hall-ui/public/government.js",
3279
3279
  "mode": "0000644",
3280
- "sha256": "39215495c3ec93b4426c95b0a16db76297a070aa678318507f8c064a57692e5d"
3280
+ "sha256": "505003cd76eb3760459ed53555b5c0803e1efaffb2e17ee15c7ed032a1607289"
3281
3281
  },
3282
3282
  {
3283
3283
  "path": "modules/hall-ui/public/hall-kit.js",
@@ -4322,12 +4322,12 @@
4322
4322
  {
4323
4323
  "path": "package-lock.json",
4324
4324
  "mode": "0000644",
4325
- "sha256": "c1798947aa88297385a13e4c3b16070a6a6e79208e077b5c40459580dfe3f55c"
4325
+ "sha256": "7731f20b3114fe6a083c06fe6b92d0048e4d5d136ba4fcc49f9a10f66c9577e5"
4326
4326
  },
4327
4327
  {
4328
4328
  "path": "package.json",
4329
4329
  "mode": "0000644",
4330
- "sha256": "25e6cf1d1ba69421a3a6ee12d77ab8609f6748aacb0d5390ccc0c287dc2c2ff0"
4330
+ "sha256": "844c9c21b09d3d41cd8663a37d651ec717a5ba25d5fb24fda5e57930ca278cb2"
4331
4331
  },
4332
4332
  {
4333
4333
  "path": "public-docs/index.html",
@@ -5807,7 +5807,7 @@
5807
5807
  {
5808
5808
  "path": "src/bongos/route-rank-check.js",
5809
5809
  "mode": "0000644",
5810
- "sha256": "a074972d1da13fb71acdd9d60126488f345ff59c33d83aca90cea50b8ed064ab"
5810
+ "sha256": "93b2a09220f4d96f69294531cbca274b1132d164cf32be2f66d40d30e1953551"
5811
5811
  },
5812
5812
  {
5813
5813
  "path": "src/bongos/routes.js",
@@ -5942,7 +5942,7 @@
5942
5942
  {
5943
5943
  "path": "src/module-api.js",
5944
5944
  "mode": "0000644",
5945
- "sha256": "aff6b3aeff836216d9b45d025375021888c25cc3f6942705341698652836bdd2"
5945
+ "sha256": "c3a588bdcb0781e8f71dd492f26b09a2212ababebf52e511dccb9018ad84f89b"
5946
5946
  },
5947
5947
  {
5948
5948
  "path": "src/module-loader/catalog.js",
@@ -6904,6 +6904,11 @@
6904
6904
  "mode": "0000644",
6905
6905
  "sha256": "74cfbd482b689df40348af3bdaf050f056e216ff18fdd6add4136d4589b7cd92"
6906
6906
  },
6907
+ {
6908
+ "path": "tests/government_charter_library.mjs",
6909
+ "mode": "0000644",
6910
+ "sha256": "9a52ac8fbbc9e7b0fc4b45841b9dbf831615e5d64eb1e3f0d9c446fc70552629"
6911
+ },
6907
6912
  {
6908
6913
  "path": "tests/government_claim_eligibility.mjs",
6909
6914
  "mode": "0000644",
@@ -6914,6 +6919,11 @@
6914
6919
  "mode": "0000644",
6915
6920
  "sha256": "330372412c80d8d33698eba68a5c19e79daff17b0d820a9a797c883071a5641f"
6916
6921
  },
6922
+ {
6923
+ "path": "tests/government_constitution_view.mjs",
6924
+ "mode": "0000644",
6925
+ "sha256": "64fc8006d5b3a562b01a38bfd0ad00a1357969b0ac52ce270e77d4d93eddca80"
6926
+ },
6917
6927
  {
6918
6928
  "path": "tests/government_fold.mjs",
6919
6929
  "mode": "0000644",
@@ -6972,7 +6982,7 @@
6972
6982
  {
6973
6983
  "path": "tests/government_routes.mjs",
6974
6984
  "mode": "0000644",
6975
- "sha256": "d32b42a6046361fdcb6ff26e92dfe2abd46fb76a218168c719377ca39614112b"
6985
+ "sha256": "88f1c323de51a9b9f710bd4ad8cbf86da75b226d7b99db2bc2054e065b81a70a"
6976
6986
  },
6977
6987
  {
6978
6988
  "path": "tests/government_seed.mjs",
@@ -5,7 +5,7 @@ A **generated**, zero-dependency typed client for the Bongos API — produced fr
5
5
  by hand; it regenerates when the spec changes, so it can never drift from the routes.
6
6
 
7
7
  - API version: **v1** (served at `/api/bongos/v1`)
8
- - 284 operations across 46 resource groups
8
+ - 285 operations across 46 resource groups
9
9
 
10
10
  ## Install
11
11
 
@@ -355,6 +355,8 @@ function createClient(opts = {}) {
355
355
  postGovernmentBoardItems: (args) => request("POST", "/government/board/items", { hasBody: true }, args),
356
356
  // POST /government/board/items/{itemId}/votes — rank: any-builder — POST /government/board/items/:itemId/votes
357
357
  postGovernmentBoardItemsItemIdVotes: (args) => request("POST", "/government/board/items/{itemId}/votes", { hasBody: true }, args),
358
+ // GET /government/constitution — rank: any-builder — GET /government/constitution
359
+ getGovernmentConstitution: (args) => request("GET", "/government/constitution", { hasBody: false }, args),
358
360
  // GET /government/permissions — rank: archon — GET /government/permissions
359
361
  getGovernmentPermissions: (args) => request("GET", "/government/permissions", { hasBody: false }, args),
360
362
  // GET /government/ranks — rank: archon — GET /government/ranks
@@ -354,6 +354,8 @@ function createClient(opts = {}) {
354
354
  postGovernmentBoardItems: (args) => request("POST", "/government/board/items", { hasBody: true }, args),
355
355
  // POST /government/board/items/{itemId}/votes — rank: any-builder — POST /government/board/items/:itemId/votes
356
356
  postGovernmentBoardItemsItemIdVotes: (args) => request("POST", "/government/board/items/{itemId}/votes", { hasBody: true }, args),
357
+ // GET /government/constitution — rank: any-builder — GET /government/constitution
358
+ getGovernmentConstitution: (args) => request("GET", "/government/constitution", { hasBody: false }, args),
357
359
  // GET /government/permissions — rank: archon — GET /government/permissions
358
360
  getGovernmentPermissions: (args) => request("GET", "/government/permissions", { hasBody: false }, args),
359
361
  // GET /government/ranks — rank: archon — GET /government/ranks
@@ -609,6 +609,8 @@ export interface BongosClient {
609
609
  postGovernmentBoardItems(args: RequestArgs & { body: PostGovernmentBoardItemsRequest }): Promise<PostGovernmentBoardItemsResponse>;
610
610
  /** POST /government/board/items/{itemId}/votes — rank: any-builder */
611
611
  postGovernmentBoardItemsItemIdVotes(args: RequestArgs & { body: PostGovernmentBoardItemsItemIdVotesRequest }): Promise<ApiResponse>;
612
+ /** GET /government/constitution — rank: any-builder */
613
+ getGovernmentConstitution(args?: RequestArgs): Promise<ApiResponse>;
612
614
  /** GET /government/permissions — rank: archon */
613
615
  getGovernmentPermissions(args?: RequestArgs): Promise<GetGovernmentPermissionsResponse>;
614
616
  /** GET /government/ranks — rank: archon */
@@ -351,6 +351,8 @@ export function createClient(opts = {}) {
351
351
  postGovernmentBoardItems: (args) => request("POST", "/government/board/items", { hasBody: true }, args),
352
352
  // POST /government/board/items/{itemId}/votes — rank: any-builder — POST /government/board/items/:itemId/votes
353
353
  postGovernmentBoardItemsItemIdVotes: (args) => request("POST", "/government/board/items/{itemId}/votes", { hasBody: true }, args),
354
+ // GET /government/constitution — rank: any-builder — GET /government/constitution
355
+ getGovernmentConstitution: (args) => request("GET", "/government/constitution", { hasBody: false }, args),
354
356
  // GET /government/permissions — rank: archon — GET /government/permissions
355
357
  getGovernmentPermissions: (args) => request("GET", "/government/permissions", { hasBody: false }, args),
356
358
  // GET /government/ranks — rank: archon — GET /government/ranks
@@ -5479,6 +5479,40 @@
5479
5479
  ]
5480
5480
  }
5481
5481
  },
5482
+ "/government/constitution": {
5483
+ "get": {
5484
+ "operationId": "get_government_constitution",
5485
+ "tags": [
5486
+ "government"
5487
+ ],
5488
+ "summary": "GET /government/constitution",
5489
+ "description": "board.vote.cast — the Constitution view (R18 of goal 1000069): the live board block plus the full amendment history, passed and returned alike. Same atom as the room — reading the rule you vote under is part of casting, and the section teaches (criterion C5). The live value resolves fresh per request, so an amendment applied a moment ago reads back immediately (the same instant-effect posture as everything else here).\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).\n\n**Permissions:** `board.vote.cast` (all required).",
5490
+ "x-rank": "any-builder",
5491
+ "x-source": "modules/government/routes/government.js",
5492
+ "x-permissions": [
5493
+ "board.vote.cast"
5494
+ ],
5495
+ "responses": {
5496
+ "200": {
5497
+ "description": "Success."
5498
+ },
5499
+ "400": {
5500
+ "$ref": "#/components/responses/BadRequest"
5501
+ },
5502
+ "401": {
5503
+ "$ref": "#/components/responses/Unauthorized"
5504
+ },
5505
+ "403": {
5506
+ "$ref": "#/components/responses/Forbidden"
5507
+ }
5508
+ },
5509
+ "security": [
5510
+ {
5511
+ "builderSession": []
5512
+ }
5513
+ ]
5514
+ }
5515
+ },
5482
5516
  "/government/permissions": {
5483
5517
  "get": {
5484
5518
  "operationId": "get_government_permissions",
@@ -18001,7 +18035,7 @@
18001
18035
  "description": "A required dependency/feature is not configured or is temporarily down."
18002
18036
  }
18003
18037
  },
18004
- "x-endpoint-count": 284,
18038
+ "x-endpoint-count": 285,
18005
18039
  "x-schema-count": 321,
18006
18040
  "x-undocumented-bodies": 8,
18007
18041
  "x-response-schemas": 215,
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Bongos API reference
4
4
 
5
- > **Generated from the live route files** — the route file is authoritative. 284 endpoints across 46 route files.
5
+ > **Generated from the live route files** — the route file is authoritative. 285 endpoints across 46 route files.
6
6
  > Machine-readable spec: [`docs/api/openapi.json`](api/openapi.json) (OpenAPI 3.1). Rendered docs site: **`/docs`** (e.g. `cloudbongos.com/docs`).
7
7
 
8
8
  Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>.md)): `public` < `any-builder` < `metic+archon` < `archon`.
@@ -216,7 +216,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
216
216
  | GET | `/api/bongos/goals/inbox` | `any-builder` | — | GET /goals/inbox — the caller's private-goals inbox (task 1952 / ADR 0112): { invitations: invites addressed to me (accept/decline); join… |
217
217
  | POST | `/api/bongos/goals/reorder` | `metic+archon` | `version_id`, `ordered_ids` | POST /goals/reorder — Archon drag-to-reorder: set the priority order of every goal WITHIN one version (task 1760). |
218
218
 
219
- ## `government` (12)
219
+ ## `government` (13)
220
220
 
221
221
  | Method | Path | Rank | Body | Description |
222
222
  |---|---|---|---|---|
@@ -226,6 +226,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
226
226
  | GET | `/api/bongos/government/board/items` | `any-builder` | — | board.vote.cast — the Board Room view: open items with their clock, the OPEN BALLOT, the closed history (R16 of goal 1000069). |
227
227
  | POST | `/api/bongos/government/board/items` | `metic+archon` | `subject_type`, `proposal`, `rationale_md` | board.item.open — put an AMENDMENT to the board (R14 of goal 1000069, ADR 0175 §7). |
228
228
  | POST | `/api/bongos/government/board/items/:itemId/votes` | `any-builder` | `direction`, `reason_md`, `section_key` | board.vote.cast — cast a vote on an open board item (R08 of goal 1000069, ADR 0175). |
229
+ | GET | `/api/bongos/government/constitution` | `any-builder` | — | board.vote.cast — the Constitution view (R18 of goal 1000069): the live board block plus the full amendment history, passed and returned … |
229
230
  | 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. |
230
231
  | GET | `/api/bongos/government/ranks` | `archon` | — | government.manage — the full rank list with each rank's grants. |
231
232
  | 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). |
@@ -520,6 +520,56 @@ async function boardRoomView({ closedLimit = 20 } = {}) {
520
520
  return { open: open.map(project), closed: closed.map(project) };
521
521
  }
522
522
 
523
+ // ── the Constitution view (BV1.R18 of goal 1000069, criterion C3/C5) ────────
524
+ //
525
+ // What this project RUNS — the live, sanitized board block, resolved fresh per
526
+ // request so an amendment applied a moment ago reads back immediately — plus
527
+ // the full change history: every closed amendment sitting, passed and
528
+ // RETURNED alike (a failed attempt to change the rules is part of the record;
529
+ // that is what "a switch can never be flipped quietly" means). Each history
530
+ // row carries the rule it was DECIDED UNDER (the item's snapshot), so the
531
+ // reader can verify every change was made legally by the constitution of its
532
+ // day — the §7 chain, auditable end to end.
533
+ // ── the charter ladder's recommendation (BV1.R19, criterion C5) ─────────────
534
+ //
535
+ // The developmental ladder from the Six Variables synthesis (Part III), as the
536
+ // spec's task text records it: formality is adopted only where informal
537
+ // coordination breaks. PURE and server-side so R26's proof can hold the
538
+ // recommendation against the real headcount — the library's one live datum.
539
+ // The bands deliberately track only the thresholds the synthesis NAMES
540
+ // (~9 name an executive, ~50 written charter + council, ~150 adjudication
541
+ // separates / federate beyond); everything else is reference prose, not code.
542
+ function recommendedCharter(headcount) {
543
+ const n = Number(headcount) || 0;
544
+ if (n <= 2) return { key: 'monarchy', rung: 'solo/pair', why: 'informal coordination has nothing to break yet — the founding monarchy is the honest shape' };
545
+ if (n <= 8) return { key: 'direct-democracy', rung: 'circle (3–8)', why: 'a consent circle of all trusted members — the rung this ladder was written on' };
546
+ if (n <= 49) return { key: 'presidential', rung: '~9: name an executive', why: 'past ~9, naming an executive beats everyone coordinating with everyone' };
547
+ if (n <= 149) return { key: 'foundation', rung: '~50: written charter + council', why: 'past ~50, the charter must be written down and a council carries it' };
548
+ return { key: 'federation', rung: '~150: adjudication separates — federate beyond', why: 'past ~150, one body cannot both govern and adjudicate; federate' };
549
+ }
550
+
551
+ async function constitutionView({ historyLimit = 50 } = {}) {
552
+ const { board: live } = loadGovernmentConfig();
553
+ const [rows, headcount] = await Promise.all([
554
+ db.listAmendmentHistory(historyLimit),
555
+ db.countActiveBuilders(),
556
+ ]);
557
+ const history = rows.map((r) => ({
558
+ item_id: String(r.item_id),
559
+ amendment_id: String(r.amendment_id),
560
+ proposed: r.proposed,
561
+ rationale_md: r.rationale_md,
562
+ proposed_by: r.proposed_by == null ? null : String(r.proposed_by),
563
+ proposer_login: r.proposer_login || null,
564
+ proposer_name: r.proposer_name || null,
565
+ opened_at: r.opened_at,
566
+ closed_at: r.closed_at,
567
+ outcome: r.outcome,
568
+ decided_under: r.decided_under || null,
569
+ }));
570
+ return { board: live, history, headcount, recommended: recommendedCharter(headcount) };
571
+ }
572
+
523
573
  // ── R13's read: the feedback a returned idea carries back to its author ─────
524
574
  //
525
575
  // One reasoned objection is what returns an idea (§3), so the feedback IS that
@@ -561,4 +611,6 @@ module.exports = {
561
611
  validateProposedConstitution,
562
612
  proposeAmendment,
563
613
  boardRoomView,
614
+ constitutionView,
615
+ recommendedCharter,
564
616
  };
@@ -509,6 +509,39 @@ async function getAmendmentSubjects(amendmentIds) {
509
509
  return rows;
510
510
  }
511
511
 
512
+ // The live headcount (R19): active humans, the same two filters as board
513
+ // membership — a machine principal is not a team member, and neither is a
514
+ // departed builder. Feeds the charter library's "recommended at YOUR headcount"
515
+ // line, which R26's proof holds against this exact number.
516
+ async function countActiveBuilders() {
517
+ const { rows } = await pool.query(
518
+ `SELECT count(*)::int AS n FROM builders WHERE status = 'active' AND system_role IS NULL`,
519
+ );
520
+ return rows[0] ? rows[0].n : 0;
521
+ }
522
+
523
+ // The constitution's change HISTORY (R18): every CLOSED amendment sitting —
524
+ // passed AND returned — joined to what was proposed, newest first. Returned
525
+ // proposals are deliberately part of the record: "a switch can never be flipped
526
+ // quietly" cuts both ways, and a failed attempt to change the rules is exactly
527
+ // the kind of fact an open ballot exists to keep visible. Served by
528
+ // government_008's closed-items index; bounded.
529
+ async function listAmendmentHistory(limit = 50) {
530
+ const { rows } = await pool.query(
531
+ `SELECT i.id AS item_id, i.opened_at, i.closed_at, i.outcome, i.constitution AS decided_under,
532
+ a.id AS amendment_id, a.proposed, a.rationale_md, a.proposed_by,
533
+ b.github_login AS proposer_login, b.display_name AS proposer_name
534
+ FROM government_board_items i
535
+ JOIN government_amendments a ON a.id = i.subject_id
536
+ LEFT JOIN builders b ON b.id = a.proposed_by
537
+ WHERE i.subject_type = 'constitutional_amendment' AND i.closed_at IS NOT NULL
538
+ ORDER BY i.closed_at DESC
539
+ LIMIT $1`,
540
+ [limit],
541
+ );
542
+ return rows;
543
+ }
544
+
512
545
  // --- amendments (BV1.R14 of goal 1000069) ------------------------------------
513
546
 
514
547
  async function createAmendment({ proposed, rationaleMd = null, proposedBy }) {
@@ -630,4 +663,6 @@ module.exports = {
630
663
  getVotesForItems,
631
664
  getIdeaSubjects,
632
665
  getAmendmentSubjects,
666
+ listAmendmentHistory,
667
+ countActiveBuilders,
633
668
  };
@@ -432,6 +432,21 @@ module.exports = function buildGovernmentRouter() {
432
432
  }
433
433
  });
434
434
 
435
+ // rank: perm board.vote.cast — the Constitution view (R18 of goal 1000069):
436
+ // the live board block plus the full amendment history, passed and returned
437
+ // alike. Same atom as the room — reading the rule you vote under is part of
438
+ // casting, and the section teaches (criterion C5). The live value resolves
439
+ // fresh per request, so an amendment applied a moment ago reads back
440
+ // immediately (the same instant-effect posture as everything else here).
441
+ router.get('/government/constitution', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
442
+ try {
443
+ res.json(await board.constitutionView({ historyLimit: 50 }));
444
+ } catch (err) {
445
+ console.error('[gds] GET /government/constitution', err);
446
+ res.fail('constitution_read_failed', { status: 500, message: 'internal error' });
447
+ }
448
+ });
449
+
435
450
  // rank: perm board.item.open — put an AMENDMENT to the board (R14 of goal
436
451
  // 1000069, ADR 0175 §7). The board's other subject never comes through here:
437
452
  // a Full Idea's window opens AUTOMATICALLY on grade pass (R07), so requesting
@@ -99,6 +99,6 @@
99
99
  <script src="/builders/palette.js" defer></script>
100
100
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
101
101
  <script src="/builders/hall-kit.js"></script>
102
- <script src="/builders/government.js?v=2026-08-18-board-room"></script>
102
+ <script src="/builders/government.js?v=2026-08-18-charter-library"></script>
103
103
  </body>
104
104
  </html>
@@ -402,25 +402,158 @@
402
402
  // ---- the three-section IA (R15) -------------------------------------------
403
403
 
404
404
  // Tab order is the spec's — Constitution · Board Room · Ranks & Permissions.
405
- // Since R16 the Board Room is furnished, so a non-archon (who cannot use the
406
- // admin tab) lands there; the Archon still lands on Ranks, their working
407
- // surface. Flip the non-archon initial to 'constitution' when R18 lands.
405
+ // A non-archon lands on the Constitution (the page's IA claim: this is the
406
+ // page that answers "how is this project governed"); the Archon still lands
407
+ // on Ranks, their working surface.
408
408
  function wireTabs({ isArchon }) {
409
409
  window.OTBKit.makeTabs({
410
410
  mount: document.getElementById('gov-tabs'),
411
411
  hash: true,
412
- initial: isArchon ? 'ranks' : 'board-room',
412
+ initial: isArchon ? 'ranks' : 'constitution',
413
413
  tabs: [
414
414
  { id: 'constitution', label: 'Constitution', panel: 'gov-panel-constitution' },
415
415
  { id: 'board-room', label: 'Board Room', panel: 'gov-panel-board' },
416
416
  { id: 'ranks', label: 'Ranks & Permissions', panel: 'gov-panel-ranks' },
417
417
  ],
418
418
  });
419
- // The Constitution frame stays an empty state until R18's read exists —
420
- // rendering live values without it would mean hardcoding what drifts.
421
- document.getElementById('gov-constitution').innerHTML = window.OTBKit.emptyStateHtml(
422
- 'The live constitution — what this project runs today, and how it has changed — appears here when the Board Room opens.',
423
- );
419
+ }
420
+
421
+ // ---- the Constitution panel (R18) -------------------------------------------
422
+
423
+ // Plain language over the three dials, straight from the SERVER's resolved
424
+ // value (never hardcoded — an amendment applied a moment ago reads back on
425
+ // the next load), plus the change history: every closed amendment sitting,
426
+ // passed and RETURNED alike, each with the rule it was decided under — so a
427
+ // switch can never be flipped quietly, and every change is auditable against
428
+ // the constitution of its day.
429
+
430
+ function membershipSentence(board) {
431
+ const m = String(board.membership || '');
432
+ const rank = m.startsWith('rank:') ? m.slice(5) : null;
433
+ return rank
434
+ ? `Every active builder holding the <strong>${escapeHtml(rank)}</strong> rank sits on the board — membership follows the rank, live.`
435
+ : `Membership predicate: <code>${escapeHtml(m || 'unset')}</code>.`;
436
+ }
437
+
438
+ function passRuleSentence(board) {
439
+ if (board.pass_rule === 'first_ratifier') {
440
+ return 'An item is decided by its <strong>first vote</strong> — yes ratifies, no returns. The day-one shape: one member acts, done.';
441
+ }
442
+ if (board.pass_rule === 'consent') {
443
+ return 'Items pass by <strong>consent</strong>: silence is consent, a single reasoned objection returns the item to its author, and a sitting that ends with no votes at all passes.';
444
+ }
445
+ return `Pass rule: <code>${escapeHtml(String(board.pass_rule || 'unset'))}</code>.`;
446
+ }
447
+
448
+ function windowSentence(board) {
449
+ if (board.window_minutes === null || board.window_minutes === undefined) {
450
+ return 'There is <strong>no clock</strong> — an item stays open until a vote decides it.';
451
+ }
452
+ const early = board.close_early_on_full_turnout
453
+ ? ', closing early the moment every member has voted'
454
+ : '';
455
+ return `A sitting runs <strong>${escapeHtml(String(board.window_minutes))} minutes</strong>${early}. An unvoted window nulls that member's vote — absence never blocks.`;
456
+ }
457
+
458
+ function historyRow(h) {
459
+ const who = escapeHtml(h.proposer_name || h.proposer_login || (h.proposed_by ? `builder ${h.proposed_by}` : 'unknown'));
460
+ const when = escapeHtml(String(h.closed_at).slice(0, 16).replace('T', ' '));
461
+ const pill = h.outcome === 'passed'
462
+ ? '<span class="gov-pill gov-pill--seeded">ratified</span>'
463
+ : '<span class="gov-pill gov-pill--locked">returned</span>';
464
+ const under = h.decided_under && h.decided_under.pass_rule
465
+ ? ` · decided under <code>${escapeHtml(h.decided_under.pass_rule)}</code>`
466
+ : '';
467
+ return `<div class="gov-assign-row">
468
+ <span class="gov-assign-row__who">${who} proposed <code>${escapeHtml(JSON.stringify(h.proposed))}</code>${h.rationale_md ? ` — ${escapeHtml(h.rationale_md)}` : ''}</span>
469
+ ${pill} <span class="muted">${when}${under}</span>
470
+ </div>`;
471
+ }
472
+
473
+ // ---- the charter library (R19) ---------------------------------------------
474
+
475
+ // The REFERENCE half of the Charter Builder — the seven structures, the three
476
+ // trajectories, the ladder — and deliberately not the configuration machinery
477
+ // (the scale argument in ADR 0175: formality is adopted only where informal
478
+ // coordination breaks). `selectable` means EXPRESSIBLE TODAY through the real
479
+ // config — a rank-predicate membership plus first_ratifier|consent — so nobody
480
+ // is misled about what a settings change can and cannot reach. Source: the
481
+ // Six-Variables synthesis (Verro Labs, Aug 2026), Parts III–IV, via the spec.
482
+ const CHARTERS = [
483
+ { key: 'monarchy', name: 'Monarchy / BDFL', selectable: true,
484
+ how: "membership rank:archon · first ratifier · no clock — the shipped default",
485
+ blurb: 'One trusted executive decides. Fast, legible, and honest at founding scale; the shape every project actually starts in.' },
486
+ { key: 'timocratic-republic', name: 'Timocratic Republic', selectable: true,
487
+ how: 'membership by a trusted rank (e.g. rank:metic) · consent · a timed sitting',
488
+ blurb: 'Standing accrues to those actively building: the franchise follows earned rank, not headcount.' },
489
+ { key: 'direct-democracy', name: 'Direct Democracy', selectable: true,
490
+ how: 'membership rank:xenos (everyone) · consent · a timed sitting',
491
+ blurb: 'Every builder sits on the board. Consent keeps it unblockable: silence passes, one reasoned objection returns.' },
492
+ { key: 'presidential', name: 'Presidential', selectable: false,
493
+ blurb: 'An ELECTED executive with a fixed term. Needs election machinery this system does not have yet — documented for later.' },
494
+ { key: 'cooperative', name: 'Cooperative', selectable: false,
495
+ blurb: 'Formal one-member-one-vote with membership shares and exit rules. Needs the membership-share machinery — documented for later.' },
496
+ { key: 'federation', name: 'Federation', selectable: false,
497
+ blurb: 'Multiple chambers or sub-projects, each self-governing, coordinating through a compact. The >150 shape — documented for later.' },
498
+ { key: 'foundation', name: 'Foundation', selectable: true,
499
+ how: 'a CUSTOM rank as the trustee body (the Ranks tab creates it) · consent · a timed sitting',
500
+ blurb: 'A written charter carried by a small appointed council. Expressible today by governing through a custom rank.' },
501
+ ];
502
+ const TRAJECTORIES = [
503
+ { name: 'Locked', blurb: 'The constitution stays as founded until deliberately amended — no automatic movement.' },
504
+ { name: 'Laddered', blurb: 'Steps up the developmental ladder as the team crosses each headcount threshold.' },
505
+ { name: 'Scheduled', blurb: 'Pre-commits the dates or milestones at which each step happens, decided in advance.' },
506
+ ];
507
+ const LADDER = 'solo/pair → circle (3–8) → ~9 name an executive → ~25 → ~50 written charter + council → ~150 adjudication separates → beyond: federate';
508
+
509
+ function charterCard(c, recommendedKey) {
510
+ const pill = c.selectable
511
+ ? '<span class="gov-pill gov-pill--seeded">selectable today</span>'
512
+ : '<span class="gov-pill gov-pill--locked">documented for later</span>';
513
+ const rec = c.key === recommendedKey ? ' <span class="gov-pill gov-pill--custom">recommended at your headcount</span>' : '';
514
+ const how = c.how ? `<div class="gov-perm__guards">as configuration: ${escapeHtml(c.how)}</div>` : '';
515
+ return `<div class="gov-assign-row"><span class="gov-assign-row__who"><strong>${escapeHtml(c.name)}</strong> — ${escapeHtml(c.blurb)}${how}</span>${pill}${rec}</div>`;
516
+ }
517
+
518
+ function charterLibraryHtml(data) {
519
+ const rec = data.recommended || {};
520
+ return `
521
+ <h2 class="gov-h">Charter library</h2>
522
+ <p class="gov-grid-note">The seven government structures, for getting a team up to speed before deciding when to flip a switch. <strong>Selectable today</strong> means expressible through the real configuration — a rank-based membership plus a pass rule — with no new code. This project has <strong>${escapeHtml(String(data.headcount ?? '?'))}</strong> active builder${data.headcount === 1 ? '' : 's'}: ${escapeHtml(rec.why || '')} <em>(${escapeHtml(rec.rung || '')})</em>.</p>
523
+ <div class="gov-section">${CHARTERS.map((c) => charterCard(c, rec.key)).join('')}</div>
524
+ <h2 class="gov-h">Trajectories</h2>
525
+ <p class="gov-grid-note">How a project moves between structures over time — summarized from the synthesis:</p>
526
+ <div class="gov-section">${TRAJECTORIES.map((t) => `<div class="gov-assign-row"><span class="gov-assign-row__who"><strong>${escapeHtml(t.name)}</strong> — ${escapeHtml(t.blurb)}</span></div>`).join('')}</div>
527
+ <p class="gov-grid-note muted">The developmental ladder: ${escapeHtml(LADDER)}. Formality is adopted only where informal coordination breaks.</p>
528
+ `;
529
+ }
530
+
531
+ async function loadConstitution() {
532
+ const box = document.getElementById('gov-constitution');
533
+ let data;
534
+ try {
535
+ data = await fetchJson('/government/constitution');
536
+ } catch (err) {
537
+ if (err && err.kind === '401') return;
538
+ box.innerHTML = `<p class="gov-empty">Could not load the constitution just now.</p>`;
539
+ return;
540
+ }
541
+ const b = data.board || {};
542
+ const history = data.history || [];
543
+ box.innerHTML = `
544
+ <section class="gov-section">
545
+ <div class="gov-section__head">What this project runs today</div>
546
+ <p class="gov-grid-note">${membershipSentence(b)}</p>
547
+ <p class="gov-grid-note">${passRuleSentence(b)}</p>
548
+ <p class="gov-grid-note">${windowSentence(b)}</p>
549
+ <p class="gov-grid-note muted">As configuration: <code>${escapeHtml(JSON.stringify(b))}</code>. Changing it is a board act — propose an amendment, and it is decided under the rule above.</p>
550
+ </section>
551
+ <h2 class="gov-h">How it has changed</h2>
552
+ ${history.length
553
+ ? `<div class="gov-section">${history.map(historyRow).join('')}</div>`
554
+ : `<p class="gov-empty">Never amended — this project still runs its founding constitution.</p>`}
555
+ ${charterLibraryHtml(data)}
556
+ `;
424
557
  }
425
558
 
426
559
  // ---- the Board Room panel (R16) --------------------------------------------
@@ -554,7 +687,8 @@
554
687
  document.getElementById('gov-sub').textContent = 'How this project is governed — the constitution it runs, the Board Room, and the ranks & permissions that administer it.';
555
688
  document.getElementById('gov-body').hidden = false;
556
689
  wireTabs({ isArchon });
557
- loadBoard(); // the room loads for everyone the page admits
690
+ loadBoard(); // the room loads for everyone the page admits
691
+ loadConstitution(); // so does the rule it runs under (R18)
558
692
 
559
693
  if (!isArchon) { sealRanksPanel(); return; }
560
694
  wireForms();
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.233",
3
+ "version": "1.19.235",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.233",
9
+ "version": "1.19.235",
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.233",
3
+ "version": "1.19.235",
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",
@@ -257,6 +257,9 @@ const GOVERNMENT_PINS = {
257
257
  // R16: the Board Room view — the open ballot is visible to whoever may cast
258
258
  // (xenos-floored atom; the room teaches as well as governs, criterion C5).
259
259
  'GET /government/board/items': 'perm:board.vote.cast',
260
+ // R18: the Constitution view — the live rule + the amendment history; reading
261
+ // the rule you vote under is part of casting.
262
+ 'GET /government/constitution': 'perm:board.vote.cast',
260
263
  };
261
264
 
262
265
  // Module-scoped privileged-rank pins: a privileged route that lives in a FEATURE
package/src/module-api.js CHANGED
@@ -893,8 +893,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
893
893
  // landed since 1.19.231 with no explicit bump. run 32133691409. (task 1002620)
894
894
  // 1.19.233 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
895
895
  // landed since 1.19.232 with no explicit bump. run 32160823445. (task 1002620)
896
+ // 1.19.234 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
897
+ // landed since 1.19.233 with no explicit bump. run 32161368847. (task 1002620)
898
+ // 1.19.235 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
899
+ // landed since 1.19.234 with no explicit bump. run 32161981393. (task 1002620)
896
900
  // ---------------------------------------------------------------------------
897
- const CORE_VERSION = '1.19.233'; // CI auto-patch carrier (ADR 0161); see changelog above
901
+ const CORE_VERSION = '1.19.235'; // CI auto-patch carrier (ADR 0161); see changelog above
898
902
 
899
903
  // A namespaced logger so a module's log lines are attributable + consistent.
900
904
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,77 @@
1
+ // tests/government_charter_library.mjs — the charter library (BV1.R19 of goal
2
+ // 1000069, criterion C5): seven constitutions, three trajectories, the ladder —
3
+ // and ONE live datum, the recommendation at this project's actual headcount.
4
+ //
5
+ // The recommendation is SERVER-SIDE and pure precisely so R26's proof can hold
6
+ // it against the real number; the library prose is client reference content.
7
+ import assert from 'node:assert/strict';
8
+ import { test } from 'node:test';
9
+ import { createRequire } from 'node:module';
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ const require = createRequire(import.meta.url);
15
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
16
+ const GOV = path.join(ROOT, 'modules', 'government');
17
+ const board = require(path.join(GOV, 'board.js'));
18
+ const db = require(path.join(GOV, 'db.js'));
19
+
20
+ const { recommendedCharter } = board;
21
+
22
+ test('the ladder bands track exactly the thresholds the synthesis names', () => {
23
+ assert.equal(recommendedCharter(1).key, 'monarchy');
24
+ assert.equal(recommendedCharter(2).key, 'monarchy', 'this project today: two builders → the founding monarchy');
25
+ assert.equal(recommendedCharter(3).key, 'direct-democracy', 'the circle rung starts at 3');
26
+ assert.equal(recommendedCharter(8).key, 'direct-democracy');
27
+ assert.equal(recommendedCharter(9).key, 'presidential', '~9: name an executive');
28
+ assert.equal(recommendedCharter(49).key, 'presidential');
29
+ assert.equal(recommendedCharter(50).key, 'foundation', '~50: written charter + council');
30
+ assert.equal(recommendedCharter(149).key, 'foundation');
31
+ assert.equal(recommendedCharter(150).key, 'federation', '~150: adjudication separates — federate');
32
+ assert.equal(recommendedCharter(0).key, 'monarchy', 'a zero/unknown headcount fails to the founding shape');
33
+ assert.equal(recommendedCharter('nonsense').key, 'monarchy');
34
+ });
35
+
36
+ test('constitutionView carries the live headcount and its recommendation', async () => {
37
+ const originals = { listAmendmentHistory: db.listAmendmentHistory, countActiveBuilders: db.countActiveBuilders };
38
+ db.listAmendmentHistory = async () => [];
39
+ db.countActiveBuilders = async () => 7;
40
+ try {
41
+ const view = await board.constitutionView();
42
+ assert.equal(view.headcount, 7);
43
+ assert.equal(view.recommended.key, 'direct-democracy');
44
+ assert.ok(view.recommended.rung.includes('circle'));
45
+ } finally { Object.assign(db, originals); }
46
+ });
47
+
48
+ test('the headcount counts the same population membership does (SQL pin)', () => {
49
+ const src = fs.readFileSync(path.join(GOV, 'db.js'), 'utf8');
50
+ const block = src.slice(src.indexOf('async function countActiveBuilders'), src.indexOf("// The constitution's change HISTORY"));
51
+ assert.match(block, /status = 'active'/);
52
+ assert.match(block, /system_role IS NULL/, 'a machine principal is not a team member');
53
+ });
54
+
55
+ test('the library presents all seven structures with honest selectable marks (client source pins)', () => {
56
+ const client = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'government.js'), 'utf8');
57
+ const lib = client.slice(client.indexOf('const CHARTERS'), client.indexOf('function charterCard'));
58
+ for (const name of ['Monarchy / BDFL', 'Timocratic Republic', 'Direct Democracy', 'Presidential', 'Cooperative', 'Federation', 'Foundation']) {
59
+ assert.ok(lib.includes(`name: '${name}'`), `the library must present ${name}`);
60
+ }
61
+ // Selectable ⇔ expressible through the REAL config (a rank predicate + a
62
+ // shipped pass rule). Every selectable entry must SAY HOW; every
63
+ // documented-for-later entry must not pretend.
64
+ const selectableTrue = (lib.match(/selectable: true/g) || []).length;
65
+ const selectableFalse = (lib.match(/selectable: false/g) || []).length;
66
+ assert.equal(selectableTrue + selectableFalse, 7, 'every structure is marked, none left ambiguous');
67
+ assert.equal(selectableTrue, 4, 'monarchy, timocratic republic, direct democracy, foundation — the config-expressible four');
68
+ const trueBlocks = lib.split('selectable: true').slice(1);
69
+ for (const b of trueBlocks) {
70
+ assert.ok(b.slice(0, 200).includes('how:'), 'a selectable structure names its configuration');
71
+ }
72
+ assert.match(client, /recommended at your headcount/, 'the one live datum is rendered');
73
+ assert.match(client, /c\.key === recommendedKey/, "the pill is driven by the SERVER's recommendation, never re-derived client-side");
74
+ for (const t of ['Locked', 'Laddered', 'Scheduled']) {
75
+ assert.ok(client.includes(`name: '${t}'`), `trajectory ${t} present`);
76
+ }
77
+ });
@@ -0,0 +1,88 @@
1
+ // tests/government_constitution_view.mjs — the Constitution section (BV1.R18 of
2
+ // goal 1000069, criteria C3/C5): what we run, and how it changed.
3
+ //
4
+ // DB-free (db monkeypatched). The invariants:
5
+ // 1. THE LIVE VALUE IS RESOLVED, NEVER HARDCODED — the view calls R05's
6
+ // config resolution per request, so an amendment applied a moment ago
7
+ // reads back immediately, and this vanilla checkout reads as monarchy.
8
+ // 2. THE HISTORY SHOWS RETURNED PROPOSALS TOO — a failed attempt to change
9
+ // the rules is part of the record ("a switch can never be flipped
10
+ // quietly" cuts both ways), each row carrying the rule it was DECIDED
11
+ // UNDER so every change is auditable against the constitution of its day.
12
+ // 3. THE READ RIDES THE VOTE ATOM — reading the rule you vote under is part
13
+ // of casting; never the manage atom.
14
+ import assert from 'node:assert/strict';
15
+ import { test } from 'node:test';
16
+ import { createRequire } from 'node:module';
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const require = createRequire(import.meta.url);
22
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
23
+ const GOV = path.join(ROOT, 'modules', 'government');
24
+ const board = require(path.join(GOV, 'board.js'));
25
+ const db = require(path.join(GOV, 'db.js'));
26
+ const { BOARD_DEFAULTS } = require(path.join(GOV, 'config.js'));
27
+
28
+ async function withDb(stubs, fn) {
29
+ const originals = {};
30
+ for (const [k, v] of Object.entries(stubs)) { originals[k] = db[k]; db[k] = v; }
31
+ try { return await fn(); }
32
+ finally { for (const [k, v] of Object.entries(originals)) db[k] = v; }
33
+ }
34
+
35
+ test('the live board block is RESOLVED per request — this vanilla checkout reads monarchy', async () => {
36
+ const view = await withDb({ listAmendmentHistory: async () => [], countActiveBuilders: async () => 2 }, () => board.constitutionView());
37
+ assert.deepEqual(view.board, BOARD_DEFAULTS,
38
+ 'no instance pack here, so the resolved value IS the neutral monarchy — never a hardcoded copy');
39
+ assert.deepEqual(view.history, []);
40
+ });
41
+
42
+ test('the history projects passed AND returned, with proposer identity and the decided-under snapshot', async () => {
43
+ const MONARCHY = { membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true };
44
+ const view = await withDb({
45
+ countActiveBuilders: async () => 2,
46
+ listAmendmentHistory: async (limit) => {
47
+ assert.equal(limit, 50);
48
+ return [
49
+ { item_id: 30n, amendment_id: 5n, proposed: { pass_rule: 'consent' }, rationale_md: 'open up',
50
+ proposed_by: 42n, proposer_login: 'sole-archon', proposer_name: 'Sole Archon',
51
+ opened_at: 'T1', closed_at: 'T2', outcome: 'passed', decided_under: MONARCHY },
52
+ { item_id: 29, amendment_id: 4, proposed: { window_minutes: 5 }, rationale_md: null,
53
+ proposed_by: null, proposer_login: null, proposer_name: null,
54
+ opened_at: 'T0', closed_at: 'T1', outcome: 'returned', decided_under: MONARCHY },
55
+ ];
56
+ },
57
+ }, () => board.constitutionView());
58
+ assert.equal(view.history.length, 2);
59
+ const [passed, returned] = view.history;
60
+ assert.equal(passed.outcome, 'passed');
61
+ assert.equal(passed.proposer_login, 'sole-archon');
62
+ assert.deepEqual(passed.decided_under, MONARCHY, 'auditable against the constitution of its day (§7 chain)');
63
+ assert.equal(returned.outcome, 'returned', 'a failed attempt to change the rules stays on the record');
64
+ assert.equal(returned.proposed_by, null, 'a departed proposer projects null, honestly');
65
+ for (const h of view.history) {
66
+ assert.equal(typeof h.item_id, 'string');
67
+ assert.equal(typeof h.amendment_id, 'string');
68
+ }
69
+ });
70
+
71
+ test('the history read includes returned outcomes and is bounded + close-ordered (SQL pins)', () => {
72
+ const src = fs.readFileSync(path.join(GOV, 'db.js'), 'utf8');
73
+ const block = src.slice(src.indexOf('async function listAmendmentHistory'), src.indexOf('// --- amendments'));
74
+ assert.ok(!block.includes("outcome = 'passed'"), 'returned proposals are part of the record — no outcome filter');
75
+ assert.match(block, /closed_at IS NOT NULL/, 'closed sittings only — an open proposal is the Board Room tab, not history');
76
+ assert.match(block, /ORDER BY i\.closed_at DESC/, 'newest change first');
77
+ assert.match(block, /LIMIT \$1/, 'bounded');
78
+ });
79
+
80
+ test('the read rides the vote atom, and the client renders the SERVER value (source pins)', () => {
81
+ const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
82
+ assert.equal(rrc.MODULE_EXPECTED_RANKS.government['GET /government/constitution'],
83
+ `${rrc.PERM_PIN_PREFIX}board.vote.cast`);
84
+ const client = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'government.js'), 'utf8');
85
+ assert.match(client, /fetchJson\('\/government\/constitution'\)/, 'the panel loads the resolved value');
86
+ assert.ok(!/'first_ratifier'[^]*hardcod/i.test(client), 'sanity');
87
+ assert.match(client, /loadConstitution\(\)/, 'the panel loads for everyone the page admits');
88
+ });
@@ -89,8 +89,11 @@ test('every /governance route gates via REAL middleware on exactly its PINNED at
89
89
  assert.equal(r.rank, FLOOR_LABEL[floor], `${r.key}: classifies to ${atom}'s floor label (${FLOOR_LABEL[floor]})`);
90
90
  }
91
91
  // The admin surface's own rule survives the generalization, stated directly:
92
- // everything that is not a board route still gates on government.manage.
93
- for (const r of classified.filter((x) => !x.key.includes('/board/'))) {
92
+ // everything outside the Board Room family — the /board/ routes and the
93
+ // /constitution read (R18: reading the rule you vote under is part of
94
+ // casting) — still gates on government.manage.
95
+ const boardFamily = (key) => key.includes('/board/') || key.endsWith('/government/constitution');
96
+ for (const r of classified.filter((x) => !boardFamily(x.key))) {
94
97
  assert.deepEqual(r.perms, ['government.manage'], `${r.key}: the admin surface gates on the manage atom`);
95
98
  }
96
99
  });