@bongos/core 1.19.233 → 1.19.234

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.234",
6
+ "core_contract": "1.19.234",
7
+ "source_commit": "e80285bcecac511d018c7d39b58a2ca05e6fc5a2",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-18T16:32:54.929Z",
9
+ "built_at": "2026-08-18T16:39:12.790Z",
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": 1318,
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": 1650,
20
+ "tree_sha256": "c706960c4712d454c74aca743c2612907744dcf50f641f8c9ee685e23db42f37",
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": "34a041cb7d001b71109b8243cfd2d34dd4185da4656d6eb31ed8af113490fd6b"
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": "11f06bb6eb105a7f52a5a6aeb94356a3cb975f82b1e4ed381bacef82463feba2"
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": "a76e7263c87178427061ff6b02588e524ca885b3964e54f693e4fc88c9ef5c26"
3276
3276
  },
3277
3277
  {
3278
3278
  "path": "modules/hall-ui/public/government.js",
3279
3279
  "mode": "0000644",
3280
- "sha256": "39215495c3ec93b4426c95b0a16db76297a070aa678318507f8c064a57692e5d"
3280
+ "sha256": "7d6ce981e085a0a5d3fed711088720c412a82b54c4c2b9f436f1e112b453c2d3"
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": "f08d3eab8dc3b1ef943b3ef19523e21c7676ba91e57f1791ce984b7847c27078"
4326
4326
  },
4327
4327
  {
4328
4328
  "path": "package.json",
4329
4329
  "mode": "0000644",
4330
- "sha256": "25e6cf1d1ba69421a3a6ee12d77ab8609f6748aacb0d5390ccc0c287dc2c2ff0"
4330
+ "sha256": "fe66476a7d77fe1f6ddaf04090e1af767696443fab948f0fc64713ed48c64b5d"
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": "509de05d397e1c27f123e67e666147efa117ce28e98ef00f1386687ca15a4097"
5946
5946
  },
5947
5947
  {
5948
5948
  "path": "src/module-loader/catalog.js",
@@ -6914,6 +6914,11 @@
6914
6914
  "mode": "0000644",
6915
6915
  "sha256": "330372412c80d8d33698eba68a5c19e79daff17b0d820a9a797c883071a5641f"
6916
6916
  },
6917
+ {
6918
+ "path": "tests/government_constitution_view.mjs",
6919
+ "mode": "0000644",
6920
+ "sha256": "daf26af68fcaa19a6de3793e0c9ab81b2981d8ed5e4d5ec5f860c686ac7610fb"
6921
+ },
6917
6922
  {
6918
6923
  "path": "tests/government_fold.mjs",
6919
6924
  "mode": "0000644",
@@ -6972,7 +6977,7 @@
6972
6977
  {
6973
6978
  "path": "tests/government_routes.mjs",
6974
6979
  "mode": "0000644",
6975
- "sha256": "d32b42a6046361fdcb6ff26e92dfe2abd46fb76a218168c719377ca39614112b"
6980
+ "sha256": "88f1c323de51a9b9f710bd4ad8cbf86da75b226d7b99db2bc2054e065b81a70a"
6976
6981
  },
6977
6982
  {
6978
6983
  "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,35 @@ 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
+ async function constitutionView({ historyLimit = 50 } = {}) {
534
+ const { board: live } = loadGovernmentConfig();
535
+ const rows = await db.listAmendmentHistory(historyLimit);
536
+ const history = rows.map((r) => ({
537
+ item_id: String(r.item_id),
538
+ amendment_id: String(r.amendment_id),
539
+ proposed: r.proposed,
540
+ rationale_md: r.rationale_md,
541
+ proposed_by: r.proposed_by == null ? null : String(r.proposed_by),
542
+ proposer_login: r.proposer_login || null,
543
+ proposer_name: r.proposer_name || null,
544
+ opened_at: r.opened_at,
545
+ closed_at: r.closed_at,
546
+ outcome: r.outcome,
547
+ decided_under: r.decided_under || null,
548
+ }));
549
+ return { board: live, history };
550
+ }
551
+
523
552
  // ── R13's read: the feedback a returned idea carries back to its author ─────
524
553
  //
525
554
  // One reasoned objection is what returns an idea (§3), so the feedback IS that
@@ -561,4 +590,5 @@ module.exports = {
561
590
  validateProposedConstitution,
562
591
  proposeAmendment,
563
592
  boardRoomView,
593
+ constitutionView,
564
594
  };
@@ -509,6 +509,28 @@ async function getAmendmentSubjects(amendmentIds) {
509
509
  return rows;
510
510
  }
511
511
 
512
+ // The constitution's change HISTORY (R18): every CLOSED amendment sitting —
513
+ // passed AND returned — joined to what was proposed, newest first. Returned
514
+ // proposals are deliberately part of the record: "a switch can never be flipped
515
+ // quietly" cuts both ways, and a failed attempt to change the rules is exactly
516
+ // the kind of fact an open ballot exists to keep visible. Served by
517
+ // government_008's closed-items index; bounded.
518
+ async function listAmendmentHistory(limit = 50) {
519
+ const { rows } = await pool.query(
520
+ `SELECT i.id AS item_id, i.opened_at, i.closed_at, i.outcome, i.constitution AS decided_under,
521
+ a.id AS amendment_id, a.proposed, a.rationale_md, a.proposed_by,
522
+ b.github_login AS proposer_login, b.display_name AS proposer_name
523
+ FROM government_board_items i
524
+ JOIN government_amendments a ON a.id = i.subject_id
525
+ LEFT JOIN builders b ON b.id = a.proposed_by
526
+ WHERE i.subject_type = 'constitutional_amendment' AND i.closed_at IS NOT NULL
527
+ ORDER BY i.closed_at DESC
528
+ LIMIT $1`,
529
+ [limit],
530
+ );
531
+ return rows;
532
+ }
533
+
512
534
  // --- amendments (BV1.R14 of goal 1000069) ------------------------------------
513
535
 
514
536
  async function createAmendment({ proposed, rationaleMd = null, proposedBy }) {
@@ -630,4 +652,5 @@ module.exports = {
630
652
  getVotesForItems,
631
653
  getIdeaSubjects,
632
654
  getAmendmentSubjects,
655
+ listAmendmentHistory,
633
656
  };
@@ -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-constitution"></script>
103
103
  </body>
104
104
  </html>
@@ -402,25 +402,99 @@
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
+ async function loadConstitution() {
474
+ const box = document.getElementById('gov-constitution');
475
+ let data;
476
+ try {
477
+ data = await fetchJson('/government/constitution');
478
+ } catch (err) {
479
+ if (err && err.kind === '401') return;
480
+ box.innerHTML = `<p class="gov-empty">Could not load the constitution just now.</p>`;
481
+ return;
482
+ }
483
+ const b = data.board || {};
484
+ const history = data.history || [];
485
+ box.innerHTML = `
486
+ <section class="gov-section">
487
+ <div class="gov-section__head">What this project runs today</div>
488
+ <p class="gov-grid-note">${membershipSentence(b)}</p>
489
+ <p class="gov-grid-note">${passRuleSentence(b)}</p>
490
+ <p class="gov-grid-note">${windowSentence(b)}</p>
491
+ <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>
492
+ </section>
493
+ <h2 class="gov-h">How it has changed</h2>
494
+ ${history.length
495
+ ? `<div class="gov-section">${history.map(historyRow).join('')}</div>`
496
+ : `<p class="gov-empty">Never amended — this project still runs its founding constitution.</p>`}
497
+ `;
424
498
  }
425
499
 
426
500
  // ---- the Board Room panel (R16) --------------------------------------------
@@ -554,7 +628,8 @@
554
628
  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
629
  document.getElementById('gov-body').hidden = false;
556
630
  wireTabs({ isArchon });
557
- loadBoard(); // the room loads for everyone the page admits
631
+ loadBoard(); // the room loads for everyone the page admits
632
+ loadConstitution(); // so does the rule it runs under (R18)
558
633
 
559
634
  if (!isArchon) { sealRanksPanel(); return; }
560
635
  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.234",
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.234",
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.234",
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,10 @@ 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)
896
898
  // ---------------------------------------------------------------------------
897
- const CORE_VERSION = '1.19.233'; // CI auto-patch carrier (ADR 0161); see changelog above
899
+ const CORE_VERSION = '1.19.234'; // CI auto-patch carrier (ADR 0161); see changelog above
898
900
 
899
901
  // A namespaced logger so a module's log lines are attributable + consistent.
900
902
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,87 @@
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 () => [] }, () => 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
+ listAmendmentHistory: async (limit) => {
46
+ assert.equal(limit, 50);
47
+ return [
48
+ { item_id: 30n, amendment_id: 5n, proposed: { pass_rule: 'consent' }, rationale_md: 'open up',
49
+ proposed_by: 42n, proposer_login: 'sole-archon', proposer_name: 'Sole Archon',
50
+ opened_at: 'T1', closed_at: 'T2', outcome: 'passed', decided_under: MONARCHY },
51
+ { item_id: 29, amendment_id: 4, proposed: { window_minutes: 5 }, rationale_md: null,
52
+ proposed_by: null, proposer_login: null, proposer_name: null,
53
+ opened_at: 'T0', closed_at: 'T1', outcome: 'returned', decided_under: MONARCHY },
54
+ ];
55
+ },
56
+ }, () => board.constitutionView());
57
+ assert.equal(view.history.length, 2);
58
+ const [passed, returned] = view.history;
59
+ assert.equal(passed.outcome, 'passed');
60
+ assert.equal(passed.proposer_login, 'sole-archon');
61
+ assert.deepEqual(passed.decided_under, MONARCHY, 'auditable against the constitution of its day (§7 chain)');
62
+ assert.equal(returned.outcome, 'returned', 'a failed attempt to change the rules stays on the record');
63
+ assert.equal(returned.proposed_by, null, 'a departed proposer projects null, honestly');
64
+ for (const h of view.history) {
65
+ assert.equal(typeof h.item_id, 'string');
66
+ assert.equal(typeof h.amendment_id, 'string');
67
+ }
68
+ });
69
+
70
+ test('the history read includes returned outcomes and is bounded + close-ordered (SQL pins)', () => {
71
+ const src = fs.readFileSync(path.join(GOV, 'db.js'), 'utf8');
72
+ const block = src.slice(src.indexOf('async function listAmendmentHistory'), src.indexOf('// --- amendments'));
73
+ assert.ok(!block.includes("outcome = 'passed'"), 'returned proposals are part of the record — no outcome filter');
74
+ assert.match(block, /closed_at IS NOT NULL/, 'closed sittings only — an open proposal is the Board Room tab, not history');
75
+ assert.match(block, /ORDER BY i\.closed_at DESC/, 'newest change first');
76
+ assert.match(block, /LIMIT \$1/, 'bounded');
77
+ });
78
+
79
+ test('the read rides the vote atom, and the client renders the SERVER value (source pins)', () => {
80
+ const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
81
+ assert.equal(rrc.MODULE_EXPECTED_RANKS.government['GET /government/constitution'],
82
+ `${rrc.PERM_PIN_PREFIX}board.vote.cast`);
83
+ const client = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'government.js'), 'utf8');
84
+ assert.match(client, /fetchJson\('\/government\/constitution'\)/, 'the panel loads the resolved value');
85
+ assert.ok(!/'first_ratifier'[^]*hardcod/i.test(client), 'sanity');
86
+ assert.match(client, /loadConstitution\(\)/, 'the panel loads for everyone the page admits');
87
+ });
@@ -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
  });