@bongos/core 1.19.232 → 1.19.233
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 +36 -26
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +2 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +33 -1
- package/docs/api-reference.md +3 -2
- package/modules/government/board.js +68 -0
- package/modules/government/db.js +77 -0
- package/modules/government/migrations/government_008_closed_items_index.sql +17 -0
- package/modules/government/routes/government.js +18 -0
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/government.js +122 -19
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/route-rank-check.js +3 -0
- package/src/bongos/serve-internal.js +8 -1
- package/src/module-api.js +3 -1
- package/tests/government_board_view.mjs +129 -0
- package/tests/government_seed.mjs +1 -0
- package/tests/hall_page_gate_map.mjs +6 -2
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.233",
|
|
6
|
+
"core_contract": "1.19.233",
|
|
7
|
+
"source_commit": "17f28d2147bc72ed6a97cbc256e388a889e7ddec",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-18T16:32:54.929Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 311,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1317,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1649,
|
|
20
|
+
"tree_sha256": "211de1015cfb5ba8ea8bb0e6aa7467566e296ffea14ebc4cabd2875e21fb6b2c",
|
|
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": "
|
|
350
|
+
"sha256": "841ea9c45ad482286f056e885cd25c6769931134a2ff066b638d7585a097888e"
|
|
351
351
|
},
|
|
352
352
|
{
|
|
353
353
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
354
354
|
"mode": "0000644",
|
|
355
|
-
"sha256": "
|
|
355
|
+
"sha256": "a9c482ca4c6d2cc313c76f3e098c3bdaa802f5c1253c749e3b5df6dfbae06463"
|
|
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": "
|
|
365
|
+
"sha256": "fafd3c715b3eb716d7f8355001cf4ea4a8254b78fd757c4a4343ad673a4f1bd8"
|
|
366
366
|
},
|
|
367
367
|
{
|
|
368
368
|
"path": "clients/bongos-client/index.d.ts",
|
|
369
369
|
"mode": "0000644",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "dc1ef3a4e2843b836e828e5c8d290861dda56844e4f5727ad5d9b3001d7e4e6b"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/index.mjs",
|
|
374
374
|
"mode": "0000644",
|
|
375
|
-
"sha256": "
|
|
375
|
+
"sha256": "48ea243ec0016c7dc2842322100406c339b2b7d05bccf8f0119060fc0cf7e5b2"
|
|
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": "
|
|
1375
|
+
"sha256": "18317d38ec567284fbb3fd3e351d8fecd10c0aa9371b353128e1e9ff0e6e7717"
|
|
1376
1376
|
},
|
|
1377
1377
|
{
|
|
1378
1378
|
"path": "docs/api/openapi.json",
|
|
1379
1379
|
"mode": "0000644",
|
|
1380
|
-
"sha256": "
|
|
1380
|
+
"sha256": "f2f81530200394e99b8591d43839ba024ab0e055331f86c119fa0e5ab39abbce"
|
|
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": "
|
|
2905
|
+
"sha256": "98c0543844eb7a2143fc9541098e067702d3b76b4a546034b050a45105403d1a"
|
|
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": "
|
|
2920
|
+
"sha256": "dd937ea64d6e4cd6ff677c136c5b8a493fecbf75b63082d9449b4a4390d1a0af"
|
|
2921
2921
|
},
|
|
2922
2922
|
{
|
|
2923
2923
|
"path": "modules/government/migrations/governance_001_tables.sql",
|
|
@@ -2989,6 +2989,11 @@
|
|
|
2989
2989
|
"mode": "0000644",
|
|
2990
2990
|
"sha256": "5629112a9bb932873a00b8ddefa847d9a21ed0f8f59cc8a77ffb9c9c5e0eea49"
|
|
2991
2991
|
},
|
|
2992
|
+
{
|
|
2993
|
+
"path": "modules/government/migrations/government_008_closed_items_index.sql",
|
|
2994
|
+
"mode": "0000644",
|
|
2995
|
+
"sha256": "2392050b4295f9fdb4105b8434a4c44833b5f0e850f11b41afe82ea1860fef34"
|
|
2996
|
+
},
|
|
2992
2997
|
{
|
|
2993
2998
|
"path": "modules/government/module.json",
|
|
2994
2999
|
"mode": "0000644",
|
|
@@ -3022,7 +3027,7 @@
|
|
|
3022
3027
|
{
|
|
3023
3028
|
"path": "modules/government/routes/government.js",
|
|
3024
3029
|
"mode": "0000644",
|
|
3025
|
-
"sha256": "
|
|
3030
|
+
"sha256": "2c7ae89e1e8e91107a8e0a55cc82d6ee4f8804fedb76d07dbe5e502c4801e187"
|
|
3026
3031
|
},
|
|
3027
3032
|
{
|
|
3028
3033
|
"path": "modules/government/routes/rank-sync.js",
|
|
@@ -3267,12 +3272,12 @@
|
|
|
3267
3272
|
{
|
|
3268
3273
|
"path": "modules/hall-ui/public/government.html",
|
|
3269
3274
|
"mode": "0000644",
|
|
3270
|
-
"sha256": "
|
|
3275
|
+
"sha256": "ffd03d1fefcd4c4639f66812fe35dd3f74a2c65ed02a576a5d9703883df10736"
|
|
3271
3276
|
},
|
|
3272
3277
|
{
|
|
3273
3278
|
"path": "modules/hall-ui/public/government.js",
|
|
3274
3279
|
"mode": "0000644",
|
|
3275
|
-
"sha256": "
|
|
3280
|
+
"sha256": "39215495c3ec93b4426c95b0a16db76297a070aa678318507f8c064a57692e5d"
|
|
3276
3281
|
},
|
|
3277
3282
|
{
|
|
3278
3283
|
"path": "modules/hall-ui/public/hall-kit.js",
|
|
@@ -4317,12 +4322,12 @@
|
|
|
4317
4322
|
{
|
|
4318
4323
|
"path": "package-lock.json",
|
|
4319
4324
|
"mode": "0000644",
|
|
4320
|
-
"sha256": "
|
|
4325
|
+
"sha256": "c1798947aa88297385a13e4c3b16070a6a6e79208e077b5c40459580dfe3f55c"
|
|
4321
4326
|
},
|
|
4322
4327
|
{
|
|
4323
4328
|
"path": "package.json",
|
|
4324
4329
|
"mode": "0000644",
|
|
4325
|
-
"sha256": "
|
|
4330
|
+
"sha256": "25e6cf1d1ba69421a3a6ee12d77ab8609f6748aacb0d5390ccc0c287dc2c2ff0"
|
|
4326
4331
|
},
|
|
4327
4332
|
{
|
|
4328
4333
|
"path": "public-docs/index.html",
|
|
@@ -5802,7 +5807,7 @@
|
|
|
5802
5807
|
{
|
|
5803
5808
|
"path": "src/bongos/route-rank-check.js",
|
|
5804
5809
|
"mode": "0000644",
|
|
5805
|
-
"sha256": "
|
|
5810
|
+
"sha256": "a074972d1da13fb71acdd9d60126488f345ff59c33d83aca90cea50b8ed064ab"
|
|
5806
5811
|
},
|
|
5807
5812
|
{
|
|
5808
5813
|
"path": "src/bongos/routes.js",
|
|
@@ -5902,7 +5907,7 @@
|
|
|
5902
5907
|
{
|
|
5903
5908
|
"path": "src/bongos/serve-internal.js",
|
|
5904
5909
|
"mode": "0000644",
|
|
5905
|
-
"sha256": "
|
|
5910
|
+
"sha256": "885076ed4dce0676735055df3f841a72ac41846568d39434a80d48eacced3e16"
|
|
5906
5911
|
},
|
|
5907
5912
|
{
|
|
5908
5913
|
"path": "src/bongos/stealth-gate.js",
|
|
@@ -5937,7 +5942,7 @@
|
|
|
5937
5942
|
{
|
|
5938
5943
|
"path": "src/module-api.js",
|
|
5939
5944
|
"mode": "0000644",
|
|
5940
|
-
"sha256": "
|
|
5945
|
+
"sha256": "aff6b3aeff836216d9b45d025375021888c25cc3f6942705341698652836bdd2"
|
|
5941
5946
|
},
|
|
5942
5947
|
{
|
|
5943
5948
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6879,6 +6884,11 @@
|
|
|
6879
6884
|
"mode": "0000644",
|
|
6880
6885
|
"sha256": "1a6f90e319c88ecd3e5588d5a6df44a9848c0adf08efd13bfcc52ac99e84d5d0"
|
|
6881
6886
|
},
|
|
6887
|
+
{
|
|
6888
|
+
"path": "tests/government_board_view.mjs",
|
|
6889
|
+
"mode": "0000644",
|
|
6890
|
+
"sha256": "104dbfd5510f4abf4580cf89f57e4be15667b4b6e6f171a2635786bfd4d65f9e"
|
|
6891
|
+
},
|
|
6882
6892
|
{
|
|
6883
6893
|
"path": "tests/government_board_vote.mjs",
|
|
6884
6894
|
"mode": "0000644",
|
|
@@ -6967,7 +6977,7 @@
|
|
|
6967
6977
|
{
|
|
6968
6978
|
"path": "tests/government_seed.mjs",
|
|
6969
6979
|
"mode": "0000644",
|
|
6970
|
-
"sha256": "
|
|
6980
|
+
"sha256": "cd67c76a30e1c69838f99afbb8edd17a0ec2109a8105b61805b9564e27464137"
|
|
6971
6981
|
},
|
|
6972
6982
|
{
|
|
6973
6983
|
"path": "tests/government_session_scope.mjs",
|
|
@@ -7057,7 +7067,7 @@
|
|
|
7057
7067
|
{
|
|
7058
7068
|
"path": "tests/hall_page_gate_map.mjs",
|
|
7059
7069
|
"mode": "0000644",
|
|
7060
|
-
"sha256": "
|
|
7070
|
+
"sha256": "98a5317bf8599941802794a5a0679e1ad2ff84dbaa7c36c88140a5f5fcd30780"
|
|
7061
7071
|
},
|
|
7062
7072
|
{
|
|
7063
7073
|
"path": "tests/hall_palette.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
|
-
-
|
|
8
|
+
- 284 operations across 46 resource groups
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -349,6 +349,8 @@ function createClient(opts = {}) {
|
|
|
349
349
|
postGovernmentAssignments: (args) => request("POST", "/government/assignments", { hasBody: true }, args),
|
|
350
350
|
// DELETE /government/assignments/{builderId}/{rankKey} — rank: archon — DELETE /government/assignments/:builderId/:rankKey
|
|
351
351
|
deleteGovernmentAssignmentsBuilderIdRankKey: (args) => request("DELETE", "/government/assignments/{builderId}/{rankKey}", { hasBody: false }, args),
|
|
352
|
+
// GET /government/board/items — rank: any-builder — GET /government/board/items
|
|
353
|
+
getGovernmentBoardItems: (args) => request("GET", "/government/board/items", { hasBody: false }, args),
|
|
352
354
|
// POST /government/board/items — rank: metic+archon — POST /government/board/items
|
|
353
355
|
postGovernmentBoardItems: (args) => request("POST", "/government/board/items", { hasBody: true }, args),
|
|
354
356
|
// POST /government/board/items/{itemId}/votes — rank: any-builder — POST /government/board/items/:itemId/votes
|
|
@@ -348,6 +348,8 @@ function createClient(opts = {}) {
|
|
|
348
348
|
postGovernmentAssignments: (args) => request("POST", "/government/assignments", { hasBody: true }, args),
|
|
349
349
|
// DELETE /government/assignments/{builderId}/{rankKey} — rank: archon — DELETE /government/assignments/:builderId/:rankKey
|
|
350
350
|
deleteGovernmentAssignmentsBuilderIdRankKey: (args) => request("DELETE", "/government/assignments/{builderId}/{rankKey}", { hasBody: false }, args),
|
|
351
|
+
// GET /government/board/items — rank: any-builder — GET /government/board/items
|
|
352
|
+
getGovernmentBoardItems: (args) => request("GET", "/government/board/items", { hasBody: false }, args),
|
|
351
353
|
// POST /government/board/items — rank: metic+archon — POST /government/board/items
|
|
352
354
|
postGovernmentBoardItems: (args) => request("POST", "/government/board/items", { hasBody: true }, args),
|
|
353
355
|
// POST /government/board/items/{itemId}/votes — rank: any-builder — POST /government/board/items/:itemId/votes
|
|
@@ -603,6 +603,8 @@ export interface BongosClient {
|
|
|
603
603
|
postGovernmentAssignments(args: RequestArgs & { body: PostGovernmentAssignmentsRequest }): Promise<ApiResponse>;
|
|
604
604
|
/** DELETE /government/assignments/{builderId}/{rankKey} — rank: archon */
|
|
605
605
|
deleteGovernmentAssignmentsBuilderIdRankKey(args?: RequestArgs): Promise<DeleteGovernmentAssignmentsBuilderIdRankKeyResponse>;
|
|
606
|
+
/** GET /government/board/items — rank: any-builder */
|
|
607
|
+
getGovernmentBoardItems(args?: RequestArgs): Promise<ApiResponse>;
|
|
606
608
|
/** POST /government/board/items — rank: metic+archon */
|
|
607
609
|
postGovernmentBoardItems(args: RequestArgs & { body: PostGovernmentBoardItemsRequest }): Promise<PostGovernmentBoardItemsResponse>;
|
|
608
610
|
/** POST /government/board/items/{itemId}/votes — rank: any-builder */
|
|
@@ -345,6 +345,8 @@ export function createClient(opts = {}) {
|
|
|
345
345
|
postGovernmentAssignments: (args) => request("POST", "/government/assignments", { hasBody: true }, args),
|
|
346
346
|
// DELETE /government/assignments/{builderId}/{rankKey} — rank: archon — DELETE /government/assignments/:builderId/:rankKey
|
|
347
347
|
deleteGovernmentAssignmentsBuilderIdRankKey: (args) => request("DELETE", "/government/assignments/{builderId}/{rankKey}", { hasBody: false }, args),
|
|
348
|
+
// GET /government/board/items — rank: any-builder — GET /government/board/items
|
|
349
|
+
getGovernmentBoardItems: (args) => request("GET", "/government/board/items", { hasBody: false }, args),
|
|
348
350
|
// POST /government/board/items — rank: metic+archon — POST /government/board/items
|
|
349
351
|
postGovernmentBoardItems: (args) => request("POST", "/government/board/items", { hasBody: true }, args),
|
|
350
352
|
// POST /government/board/items/{itemId}/votes — rank: any-builder — POST /government/board/items/:itemId/votes
|
package/docs/api/openapi.json
CHANGED
|
@@ -5337,6 +5337,38 @@
|
|
|
5337
5337
|
}
|
|
5338
5338
|
},
|
|
5339
5339
|
"/government/board/items": {
|
|
5340
|
+
"get": {
|
|
5341
|
+
"operationId": "get_government_board_items",
|
|
5342
|
+
"tags": [
|
|
5343
|
+
"government"
|
|
5344
|
+
],
|
|
5345
|
+
"summary": "GET /government/board/items",
|
|
5346
|
+
"description": "board.vote.cast — the Board Room view: open items with their clock, the OPEN BALLOT, the closed history (R16 of goal 1000069). Gated on the XENOS-floored vote atom, deliberately: seeing the ballot is part of casting (§9 — a board whose votes are invisible constrains nobody), and the room teaches as well as governs (criterion C5). NOT government.manage — that is the admin tab's atom; the room must be visible to whoever the constitution may enfranchise. Membership still gates nothing here: this is a read, and the write (the vote) re-checks membership in-handler.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).\n\n**Permissions:** `board.vote.cast` (all required).",
|
|
5347
|
+
"x-rank": "any-builder",
|
|
5348
|
+
"x-source": "modules/government/routes/government.js",
|
|
5349
|
+
"x-permissions": [
|
|
5350
|
+
"board.vote.cast"
|
|
5351
|
+
],
|
|
5352
|
+
"responses": {
|
|
5353
|
+
"200": {
|
|
5354
|
+
"description": "Success."
|
|
5355
|
+
},
|
|
5356
|
+
"400": {
|
|
5357
|
+
"$ref": "#/components/responses/BadRequest"
|
|
5358
|
+
},
|
|
5359
|
+
"401": {
|
|
5360
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
5361
|
+
},
|
|
5362
|
+
"403": {
|
|
5363
|
+
"$ref": "#/components/responses/Forbidden"
|
|
5364
|
+
}
|
|
5365
|
+
},
|
|
5366
|
+
"security": [
|
|
5367
|
+
{
|
|
5368
|
+
"builderSession": []
|
|
5369
|
+
}
|
|
5370
|
+
]
|
|
5371
|
+
},
|
|
5340
5372
|
"post": {
|
|
5341
5373
|
"operationId": "post_government_board_items",
|
|
5342
5374
|
"tags": [
|
|
@@ -17969,7 +18001,7 @@
|
|
|
17969
18001
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
17970
18002
|
}
|
|
17971
18003
|
},
|
|
17972
|
-
"x-endpoint-count":
|
|
18004
|
+
"x-endpoint-count": 284,
|
|
17973
18005
|
"x-schema-count": 321,
|
|
17974
18006
|
"x-undocumented-bodies": 8,
|
|
17975
18007
|
"x-response-schemas": 215,
|
package/docs/api-reference.md
CHANGED
|
@@ -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.
|
|
5
|
+
> **Generated from the live route files** — the route file is authoritative. 284 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,13 +216,14 @@ 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` (
|
|
219
|
+
## `government` (12)
|
|
220
220
|
|
|
221
221
|
| Method | Path | Rank | Body | Description |
|
|
222
222
|
|---|---|---|---|---|
|
|
223
223
|
| GET | `/api/bongos/government/assignments` | `archon` | — | government.manage — every builder↔rank assignment row. |
|
|
224
224
|
| POST | `/api/bongos/government/assignments` | `archon` | `builder_id`, `rank_key` | government.manage — assign a CUSTOM rank to a builder. |
|
|
225
225
|
| DELETE | `/api/bongos/government/assignments/:builderId/:rankKey` | `archon` | — | government.manage — remove a custom-rank assignment. |
|
|
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). |
|
|
226
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). |
|
|
227
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). |
|
|
228
229
|
| 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. |
|
|
@@ -453,6 +453,73 @@ async function proposeAmendment({ proposal, rationaleMd = null, proposedBy } = {
|
|
|
453
453
|
return { ok: true, amendment, item };
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
+
// ── the Board Room view (BV1.R16 of goal 1000069, ADR 0175 §9) ──────────────
|
|
457
|
+
//
|
|
458
|
+
// One assembled read for the room: open items with their clock, the OPEN
|
|
459
|
+
// BALLOT (people, directions, timestamps, objections), and the bounded closed
|
|
460
|
+
// history — enriched with what each subject IS (an idea's title, an
|
|
461
|
+
// amendment's proposed constitution). Deliberately a VIEW, not a policy
|
|
462
|
+
// surface: nothing here decides anything, and the payload carries the same
|
|
463
|
+
// constitution SNAPSHOT the deciding paths use, so the room describes the rule
|
|
464
|
+
// each item actually runs under. `remaining_seconds` comes from the database
|
|
465
|
+
// clock per row — the client renders a countdown, it never does clock math.
|
|
466
|
+
//
|
|
467
|
+
// The subject AUTHOR rides along (an idea's captor / an amendment's proposer)
|
|
468
|
+
// so R17's vote UI can grey out the anti-self case for ideas without a second
|
|
469
|
+
// round trip — the SERVER still refuses regardless (castVote), this is only
|
|
470
|
+
// the courtesy rendering.
|
|
471
|
+
async function boardRoomView({ closedLimit = 20 } = {}) {
|
|
472
|
+
const [open, closed] = await Promise.all([
|
|
473
|
+
db.listOpenBoardItems(),
|
|
474
|
+
db.listClosedBoardItems(closedLimit),
|
|
475
|
+
]);
|
|
476
|
+
const all = [...open, ...closed];
|
|
477
|
+
const votes = await db.getVotesForItems(all.map((i) => i.id));
|
|
478
|
+
const ideaIds = [...new Set(all.filter((i) => i.subject_type === 'full_idea').map((i) => i.subject_id))];
|
|
479
|
+
const amendmentIds = [...new Set(all.filter((i) => i.subject_type === 'constitutional_amendment').map((i) => i.subject_id))];
|
|
480
|
+
const [ideas, amendments] = await Promise.all([
|
|
481
|
+
db.getIdeaSubjects(ideaIds),
|
|
482
|
+
db.getAmendmentSubjects(amendmentIds),
|
|
483
|
+
]);
|
|
484
|
+
const ideaById = new Map(ideas.map((r) => [String(r.id), r]));
|
|
485
|
+
const amendmentById = new Map(amendments.map((r) => [String(r.id), r]));
|
|
486
|
+
const votesByItem = new Map();
|
|
487
|
+
for (const v of votes) {
|
|
488
|
+
const key = String(v.item_id);
|
|
489
|
+
if (!votesByItem.has(key)) votesByItem.set(key, []);
|
|
490
|
+
votesByItem.get(key).push({
|
|
491
|
+
voter_id: String(v.voter_id),
|
|
492
|
+
github_login: v.github_login,
|
|
493
|
+
display_name: v.display_name,
|
|
494
|
+
direction: v.direction,
|
|
495
|
+
reason_md: v.reason_md,
|
|
496
|
+
section_key: v.section_key,
|
|
497
|
+
cast_at: v.cast_at,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
const project = (item) => {
|
|
501
|
+
const subject = item.subject_type === 'full_idea'
|
|
502
|
+
? (() => { const r = ideaById.get(String(item.subject_id)); return r ? { title: r.title, author_id: String(r.captured_by) } : null; })()
|
|
503
|
+
: (() => { const r = amendmentById.get(String(item.subject_id)); return r ? { proposed: r.proposed, rationale_md: r.rationale_md, author_id: r.proposed_by == null ? null : String(r.proposed_by) } : null; })();
|
|
504
|
+
return {
|
|
505
|
+
id: String(item.id),
|
|
506
|
+
subject_type: item.subject_type,
|
|
507
|
+
subject_id: String(item.subject_id),
|
|
508
|
+
subject, // null when the subject row is gone — the room says so honestly
|
|
509
|
+
opened_by: item.opened_by == null ? null : String(item.opened_by),
|
|
510
|
+
opened_at: item.opened_at,
|
|
511
|
+
closes_at: item.closes_at,
|
|
512
|
+
remaining_seconds: item.remaining_seconds ?? null,
|
|
513
|
+
expired: item.expired === true,
|
|
514
|
+
closed_at: item.closed_at || null,
|
|
515
|
+
outcome: item.outcome || null,
|
|
516
|
+
constitution: item.constitution || null,
|
|
517
|
+
votes: votesByItem.get(String(item.id)) || [],
|
|
518
|
+
};
|
|
519
|
+
};
|
|
520
|
+
return { open: open.map(project), closed: closed.map(project) };
|
|
521
|
+
}
|
|
522
|
+
|
|
456
523
|
// ── R13's read: the feedback a returned idea carries back to its author ─────
|
|
457
524
|
//
|
|
458
525
|
// One reasoned objection is what returns an idea (§3), so the feedback IS that
|
|
@@ -493,4 +560,5 @@ module.exports = {
|
|
|
493
560
|
latestRejectionForIdea,
|
|
494
561
|
validateProposedConstitution,
|
|
495
562
|
proposeAmendment,
|
|
563
|
+
boardRoomView,
|
|
496
564
|
};
|
package/modules/government/db.js
CHANGED
|
@@ -437,6 +437,78 @@ async function getIdeaCompleteness(ideaId) {
|
|
|
437
437
|
return rows[0] ? rows[0].completeness_score : null;
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
+
// --- the Board Room reads (BV1.R16 of goal 1000069) --------------------------
|
|
441
|
+
//
|
|
442
|
+
// The room's view: open items with their clock, the OPEN BALLOT (voter
|
|
443
|
+
// identity, direction, timestamp, objection), and the closed history. Remaining
|
|
444
|
+
// time is computed by the DATABASE clock per row (the same posture as expiry),
|
|
445
|
+
// so the client never does clock math against its own skew. Concurrent windows
|
|
446
|
+
// are normal — the reads are lists, ordered.
|
|
447
|
+
|
|
448
|
+
async function listOpenBoardItems() {
|
|
449
|
+
const { rows } = await pool.query(
|
|
450
|
+
`SELECT *,
|
|
451
|
+
(closes_at IS NOT NULL AND closes_at < now()) AS expired,
|
|
452
|
+
CASE WHEN closes_at IS NULL THEN NULL
|
|
453
|
+
ELSE GREATEST(0, EXTRACT(EPOCH FROM (closes_at - now()))::int) END AS remaining_seconds
|
|
454
|
+
FROM government_board_items
|
|
455
|
+
WHERE closed_at IS NULL
|
|
456
|
+
ORDER BY opened_at DESC`,
|
|
457
|
+
);
|
|
458
|
+
return rows;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Bounded closed history, newest close first. Served by government_008's
|
|
462
|
+
// closed-items index — the closed set grows forever (resubmission is
|
|
463
|
+
// unbounded), and this runs on every Board Room page load.
|
|
464
|
+
async function listClosedBoardItems(limit = 20) {
|
|
465
|
+
const { rows } = await pool.query(
|
|
466
|
+
`SELECT * FROM government_board_items
|
|
467
|
+
WHERE closed_at IS NOT NULL
|
|
468
|
+
ORDER BY closed_at DESC
|
|
469
|
+
LIMIT $1`,
|
|
470
|
+
[limit],
|
|
471
|
+
);
|
|
472
|
+
return rows;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// The open ballot for a SET of items in one query — voter identity joined so
|
|
476
|
+
// the room shows people, not ids (§9: a board whose votes are invisible
|
|
477
|
+
// constrains nobody).
|
|
478
|
+
async function getVotesForItems(itemIds) {
|
|
479
|
+
if (!itemIds || !itemIds.length) return [];
|
|
480
|
+
const { rows } = await pool.query(
|
|
481
|
+
`SELECT v.*, b.github_login, b.display_name
|
|
482
|
+
FROM government_board_votes v
|
|
483
|
+
JOIN builders b ON b.id = v.voter_id
|
|
484
|
+
WHERE v.item_id = ANY($1::bigint[])
|
|
485
|
+
ORDER BY v.cast_at, v.id`,
|
|
486
|
+
[itemIds],
|
|
487
|
+
);
|
|
488
|
+
return rows;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Subject enrichment, one query per subject family. Shared-table reads (the
|
|
492
|
+
// boundary is code imports, not table access) — a full_idea shows its title,
|
|
493
|
+
// an amendment its proposed block + rationale.
|
|
494
|
+
async function getIdeaSubjects(ideaIds) {
|
|
495
|
+
if (!ideaIds || !ideaIds.length) return [];
|
|
496
|
+
const { rows } = await pool.query(
|
|
497
|
+
`SELECT id, title, captured_by FROM idea_inbox WHERE id = ANY($1::bigint[])`,
|
|
498
|
+
[ideaIds],
|
|
499
|
+
);
|
|
500
|
+
return rows;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function getAmendmentSubjects(amendmentIds) {
|
|
504
|
+
if (!amendmentIds || !amendmentIds.length) return [];
|
|
505
|
+
const { rows } = await pool.query(
|
|
506
|
+
`SELECT id, proposed, rationale_md, proposed_by FROM government_amendments WHERE id = ANY($1::bigint[])`,
|
|
507
|
+
[amendmentIds],
|
|
508
|
+
);
|
|
509
|
+
return rows;
|
|
510
|
+
}
|
|
511
|
+
|
|
440
512
|
// --- amendments (BV1.R14 of goal 1000069) ------------------------------------
|
|
441
513
|
|
|
442
514
|
async function createAmendment({ proposed, rationaleMd = null, proposedBy }) {
|
|
@@ -553,4 +625,9 @@ module.exports = {
|
|
|
553
625
|
getLatestReturnedItemWithObjection,
|
|
554
626
|
createAmendment,
|
|
555
627
|
getAmendment,
|
|
628
|
+
listOpenBoardItems,
|
|
629
|
+
listClosedBoardItems,
|
|
630
|
+
getVotesForItems,
|
|
631
|
+
getIdeaSubjects,
|
|
632
|
+
getAmendmentSubjects,
|
|
556
633
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- government_008_closed_items_index.sql — index the closed-history read
|
|
2
|
+
-- (task 1003029 / R16 of goal 1000069).
|
|
3
|
+
--
|
|
4
|
+
-- The Board Room's closed history reads "newest closes first, LIMIT n" on every
|
|
5
|
+
-- page load, and the closed set grows forever (one row per window; resubmission
|
|
6
|
+
-- is unbounded by design). The table's other indexes are partial on OPEN items
|
|
7
|
+
-- (the sweep, the one-open-per-subject wall) or on outcome alone — none order
|
|
8
|
+
-- by close time. Same reasoning as government_006's returned-items index.
|
|
9
|
+
-- Additive, idempotent, no rows touched.
|
|
10
|
+
|
|
11
|
+
BEGIN;
|
|
12
|
+
|
|
13
|
+
CREATE INDEX IF NOT EXISTS government_board_items_closed_by_time
|
|
14
|
+
ON government_board_items (closed_at DESC)
|
|
15
|
+
WHERE closed_at IS NOT NULL;
|
|
16
|
+
|
|
17
|
+
COMMIT;
|
|
@@ -414,6 +414,24 @@ module.exports = function buildGovernmentRouter() {
|
|
|
414
414
|
}
|
|
415
415
|
});
|
|
416
416
|
|
|
417
|
+
// rank: perm board.vote.cast — the Board Room view: open items with their
|
|
418
|
+
// clock, the OPEN BALLOT, the closed history (R16 of goal 1000069). Gated on
|
|
419
|
+
// the XENOS-floored vote atom, deliberately: seeing the ballot is part of
|
|
420
|
+
// casting (§9 — a board whose votes are invisible constrains nobody), and the
|
|
421
|
+
// room teaches as well as governs (criterion C5). NOT government.manage —
|
|
422
|
+
// that is the admin tab's atom; the room must be visible to whoever the
|
|
423
|
+
// constitution may enfranchise. Membership still gates nothing here: this is
|
|
424
|
+
// a read, and the write (the vote) re-checks membership in-handler.
|
|
425
|
+
router.get('/government/board/items', api.requireBuilder, api.requirePermission('board.vote.cast'), async (req, res) => {
|
|
426
|
+
try {
|
|
427
|
+
const view = await board.boardRoomView({ closedLimit: 20 });
|
|
428
|
+
res.json({ ...view, viewer_id: String(req.builder.id) });
|
|
429
|
+
} catch (err) {
|
|
430
|
+
console.error('[gds] GET /government/board/items', err);
|
|
431
|
+
res.fail('board_read_failed', { status: 500, message: 'internal error' });
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
417
435
|
// rank: perm board.item.open — put an AMENDMENT to the board (R14 of goal
|
|
418
436
|
// 1000069, ADR 0175 §7). The board's other subject never comes through here:
|
|
419
437
|
// 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-
|
|
102
|
+
<script src="/builders/government.js?v=2026-08-18-board-room"></script>
|
|
103
103
|
</body>
|
|
104
104
|
</html>
|
|
@@ -401,30 +401,121 @@
|
|
|
401
401
|
|
|
402
402
|
// ---- the three-section IA (R15) -------------------------------------------
|
|
403
403
|
|
|
404
|
-
// Tab order is the spec's — Constitution · Board Room · Ranks & Permissions
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
function wireTabs() {
|
|
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.
|
|
408
|
+
function wireTabs({ isArchon }) {
|
|
409
409
|
window.OTBKit.makeTabs({
|
|
410
410
|
mount: document.getElementById('gov-tabs'),
|
|
411
411
|
hash: true,
|
|
412
|
-
initial: 'ranks',
|
|
412
|
+
initial: isArchon ? 'ranks' : 'board-room',
|
|
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
|
|
420
|
-
//
|
|
421
|
-
// the R18 read exists would mean hardcoding a value that drifts.
|
|
419
|
+
// The Constitution frame stays an empty state until R18's read exists —
|
|
420
|
+
// rendering live values without it would mean hardcoding what drifts.
|
|
422
421
|
document.getElementById('gov-constitution').innerHTML = window.OTBKit.emptyStateHtml(
|
|
423
422
|
'The live constitution — what this project runs today, and how it has changed — appears here when the Board Room opens.',
|
|
424
423
|
);
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ---- the Board Room panel (R16) --------------------------------------------
|
|
427
|
+
|
|
428
|
+
// The room renders what the server assembled: open items with the database's
|
|
429
|
+
// own countdown, the OPEN BALLOT (people, directions, timestamps, objections),
|
|
430
|
+
// and the closed history. Nothing here decides anything — casting arrives with
|
|
431
|
+
// R17; this panel is the accountability surface (§9: a board whose votes are
|
|
432
|
+
// invisible constrains nobody).
|
|
433
|
+
|
|
434
|
+
function fmtRemaining(seconds) {
|
|
435
|
+
if (seconds === null || seconds === undefined) return null;
|
|
436
|
+
const s = Number(seconds);
|
|
437
|
+
if (!Number.isFinite(s) || s <= 0) return 'closing…';
|
|
438
|
+
if (s < 90) return `closes in ${Math.max(1, Math.round(s))}s`;
|
|
439
|
+
if (s < 5400) return `closes in ${Math.round(s / 60)}m`;
|
|
440
|
+
return `closes in ${(s / 3600).toFixed(1)}h`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function subjectLine(item) {
|
|
444
|
+
if (item.subject_type === 'full_idea') {
|
|
445
|
+
const title = item.subject ? item.subject.title : `idea ${item.subject_id}`;
|
|
446
|
+
return `<a href="/#/idea/${escapeHtml(String(item.subject_id))}">${escapeHtml(title)}</a> <span class="gov-pill gov-pill--floor">Full Idea</span>`;
|
|
447
|
+
}
|
|
448
|
+
return `Constitutional amendment <span class="gov-pill gov-pill--floor">amendment</span>`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function clockLine(item) {
|
|
452
|
+
if (item.closes_at) {
|
|
453
|
+
const rem = fmtRemaining(item.remaining_seconds);
|
|
454
|
+
return rem || 'timed sitting';
|
|
455
|
+
}
|
|
456
|
+
const rule = item.constitution && item.constitution.pass_rule;
|
|
457
|
+
return rule === 'first_ratifier'
|
|
458
|
+
? 'no clock — closes on the first vote'
|
|
459
|
+
: 'no clock';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function ballotHtml(item, viewerId) {
|
|
463
|
+
if (!item.votes.length) {
|
|
464
|
+
return `<p class="gov-empty">No votes yet${item.closed_at ? ' — closed unopposed' : ''}.</p>`;
|
|
465
|
+
}
|
|
466
|
+
const rows = item.votes.map((v) => {
|
|
467
|
+
const who = escapeHtml(v.display_name || v.github_login || `builder ${v.voter_id}`);
|
|
468
|
+
const mine = String(v.voter_id) === String(viewerId) ? ' <span class="gov-held-note">your vote</span>' : '';
|
|
469
|
+
const when = escapeHtml(String(v.cast_at).slice(0, 16).replace('T', ' '));
|
|
470
|
+
const objection = v.direction === 'no'
|
|
471
|
+
? `<div class="gov-perm__guards">objected to <code>${escapeHtml(v.section_key || '')}</code> — ${escapeHtml(v.reason_md || '')}</div>`
|
|
472
|
+
: '';
|
|
473
|
+
return `<div class="gov-assign-row"><span class="gov-assign-row__who">${who}${mine}</span>
|
|
474
|
+
<span class="gov-pill ${v.direction === 'no' ? 'gov-pill--locked' : 'gov-pill--seeded'}">${v.direction}</span>
|
|
475
|
+
<span class="muted">${when}</span>${objection}</div>`;
|
|
476
|
+
}).join('');
|
|
477
|
+
return `<div class="gov-section">${rows}</div>`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function itemCard(item, viewerId) {
|
|
481
|
+
const yes = item.votes.filter((v) => v.direction === 'yes').length;
|
|
482
|
+
const no = item.votes.filter((v) => v.direction === 'no').length;
|
|
483
|
+
const state = item.closed_at
|
|
484
|
+
? `<span class="gov-pill ${item.outcome === 'passed' ? 'gov-pill--seeded' : 'gov-pill--locked'}">${escapeHtml(item.outcome || '')}</span>`
|
|
485
|
+
: `<span class="gov-pill gov-pill--custom">open</span> <span class="muted">${escapeHtml(clockLine(item))}</span>`;
|
|
486
|
+
const amendmentDetail = item.subject_type === 'constitutional_amendment' && item.subject
|
|
487
|
+
? `<div class="gov-perm__guards">proposes: <code>${escapeHtml(JSON.stringify(item.subject.proposed))}</code>${item.subject.rationale_md ? ` — ${escapeHtml(item.subject.rationale_md)}` : ''}</div>`
|
|
488
|
+
: '';
|
|
489
|
+
const mine = item.votes.find((v) => String(v.voter_id) === String(viewerId));
|
|
490
|
+
const myLine = item.closed_at ? '' : (mine
|
|
491
|
+
? `<p class="gov-grid-note">You voted <strong>${escapeHtml(mine.direction)}</strong>. One vote per member — no changes.</p>`
|
|
492
|
+
: `<p class="gov-grid-note">You have not voted on this item. Casting arrives with the vote form.</p>`);
|
|
493
|
+
return `<section class="gov-section">
|
|
494
|
+
<div class="gov-section__head">${subjectLine(item)} ${state}</div>
|
|
495
|
+
${amendmentDetail}
|
|
496
|
+
<p class="gov-grid-note">tally: ${yes} yes · ${no} no — sitting under <code>${escapeHtml((item.constitution && item.constitution.pass_rule) || '?')}</code>, membership <code>${escapeHtml((item.constitution && item.constitution.membership) || '?')}</code></p>
|
|
497
|
+
${myLine}
|
|
498
|
+
${ballotHtml(item, viewerId)}
|
|
499
|
+
</section>`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function loadBoard() {
|
|
503
|
+
const box = document.getElementById('gov-board');
|
|
504
|
+
let data;
|
|
505
|
+
try {
|
|
506
|
+
data = await fetchJson('/government/board/items');
|
|
507
|
+
} catch (err) {
|
|
508
|
+
if (err && err.kind === '401') return;
|
|
509
|
+
box.innerHTML = `<p class="gov-empty">Could not load the Board Room just now.</p>`;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const viewerId = data.viewer_id;
|
|
513
|
+
const open = (data.open || []).map((i) => itemCard(i, viewerId)).join('');
|
|
514
|
+
const closed = (data.closed || []).map((i) => itemCard(i, viewerId)).join('');
|
|
515
|
+
box.innerHTML =
|
|
516
|
+
(open || window.OTBKit.emptyStateHtml('Nothing is before the board. When a Full Idea clears the completeness grade, its window opens here.'))
|
|
517
|
+
+ `<h2 class="gov-h">Closed sittings</h2>`
|
|
518
|
+
+ (closed || `<p class="gov-empty">No sitting has closed yet.</p>`);
|
|
428
519
|
}
|
|
429
520
|
|
|
430
521
|
// ---- init ----------------------------------------------------------------
|
|
@@ -435,11 +526,20 @@
|
|
|
435
526
|
document.getElementById('gov-sub').textContent = 'Restricted.';
|
|
436
527
|
}
|
|
437
528
|
|
|
529
|
+
// Since R16 the page shell admits page.view.government (metic+), and only the
|
|
530
|
+
// RANKS tab is the archon admin surface — so the seal narrowed from the whole
|
|
531
|
+
// page to that one panel. Cosmetic defence-in-depth as ever: every
|
|
532
|
+
// /government/* CRUD endpoint still gates on government.manage server-side,
|
|
533
|
+
// and the board read carries its own atom.
|
|
534
|
+
function sealRanksPanel() {
|
|
535
|
+
const panel = document.getElementById('gov-panel-ranks');
|
|
536
|
+
const body = document.getElementById('gov-body');
|
|
537
|
+
// Replace the admin surface with the sealed note, inside its own tab only.
|
|
538
|
+
panel.innerHTML = '<p class="gov-sealed">Ranks & Permissions is the Archon administration surface. The Constitution and the Board Room are yours to read.</p>';
|
|
539
|
+
body.hidden = false;
|
|
540
|
+
}
|
|
541
|
+
|
|
438
542
|
async function init() {
|
|
439
|
-
// Confirm rank for a friendly sealed notice; the server gates the page and
|
|
440
|
-
// every /government endpoint on `government.manage` regardless (cosmetic
|
|
441
|
-
// defence-in-depth). government.manage is system:true, so only the archon
|
|
442
|
-
// rank can hold it — rank is an accurate client-side proxy.
|
|
443
543
|
let me;
|
|
444
544
|
try {
|
|
445
545
|
me = await fetchJson('/me');
|
|
@@ -449,17 +549,20 @@
|
|
|
449
549
|
me = null;
|
|
450
550
|
}
|
|
451
551
|
const rank = (me && (me.builder || me).rank || '').toLowerCase();
|
|
452
|
-
|
|
552
|
+
const isArchon = rank === 'archon';
|
|
453
553
|
|
|
454
554
|
document.getElementById('gov-sub').textContent = 'How this project is governed — the constitution it runs, the Board Room, and the ranks & permissions that administer it.';
|
|
455
555
|
document.getElementById('gov-body').hidden = false;
|
|
456
|
-
wireTabs();
|
|
556
|
+
wireTabs({ isArchon });
|
|
557
|
+
loadBoard(); // the room loads for everyone the page admits
|
|
558
|
+
|
|
559
|
+
if (!isArchon) { sealRanksPanel(); return; }
|
|
457
560
|
wireForms();
|
|
458
561
|
try {
|
|
459
562
|
await load();
|
|
460
563
|
} catch (err) {
|
|
461
564
|
if (err && err.kind === '401') return;
|
|
462
|
-
if (err && err.kind === '403') {
|
|
565
|
+
if (err && err.kind === '403') { sealRanksPanel(); return; }
|
|
463
566
|
document.getElementById('gov-grid').innerHTML = `<p class="gov-empty">Could not load government data just now.</p>`;
|
|
464
567
|
}
|
|
465
568
|
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.233",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.233",
|
|
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.
|
|
3
|
+
"version": "1.19.233",
|
|
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",
|
|
@@ -254,6 +254,9 @@ const GOVERNMENT_PINS = {
|
|
|
254
254
|
// R14: propose a constitutional amendment (the board's only proposed subject —
|
|
255
255
|
// a Full Idea's window opens automatically on grade pass).
|
|
256
256
|
'POST /government/board/items': 'perm:board.item.open',
|
|
257
|
+
// R16: the Board Room view — the open ballot is visible to whoever may cast
|
|
258
|
+
// (xenos-floored atom; the room teaches as well as governs, criterion C5).
|
|
259
|
+
'GET /government/board/items': 'perm:board.vote.cast',
|
|
257
260
|
};
|
|
258
261
|
|
|
259
262
|
// Module-scoped privileged-rank pins: a privileged route that lives in a FEATURE
|
|
@@ -789,8 +789,15 @@ function mountInternalSurfaces(app) {
|
|
|
789
789
|
|| SESSIONS_PAGE_RE.test(norm)) {
|
|
790
790
|
return gdsAuth.requireGovernmentPage(req, res, next);
|
|
791
791
|
}
|
|
792
|
+
// R16 (goal 1000069) widened the government shell from the manage atom
|
|
793
|
+
// (archon) to page.view.government (metic+): since R15 the page is THREE
|
|
794
|
+
// rooms — Constitution, Board Room, Ranks & Permissions — and only the
|
|
795
|
+
// third is the archon admin surface. Its DATA routes all still gate on
|
|
796
|
+
// government.manage individually (defence in depth unchanged); the board +
|
|
797
|
+
// constitution reads carry their own atoms. The shell follows the widest
|
|
798
|
+
// legitimate audience — the audience changed when the page did.
|
|
792
799
|
if (GOVERNMENT_PAGE_RE.test(norm)) {
|
|
793
|
-
return gdsAuth.
|
|
800
|
+
return gdsAuth.requireGovernmentPage(req, res, next);
|
|
794
801
|
}
|
|
795
802
|
if (PEOPLE_PAGE_RE.test(norm) || SCOUTING_PAGE_RE.test(norm)) {
|
|
796
803
|
return gdsAuth.requireProjectCuratePage(req, res, next);
|
package/src/module-api.js
CHANGED
|
@@ -891,8 +891,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
891
891
|
// landed since 1.19.230 with no explicit bump. run 32133132988. (task 1002620)
|
|
892
892
|
// 1.19.232 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
893
893
|
// landed since 1.19.231 with no explicit bump. run 32133691409. (task 1002620)
|
|
894
|
+
// 1.19.233 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
895
|
+
// landed since 1.19.232 with no explicit bump. run 32160823445. (task 1002620)
|
|
894
896
|
// ---------------------------------------------------------------------------
|
|
895
|
-
const CORE_VERSION = '1.19.
|
|
897
|
+
const CORE_VERSION = '1.19.233'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
896
898
|
|
|
897
899
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
898
900
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// tests/government_board_view.mjs — the Board Room view (BV1.R16 of goal
|
|
2
|
+
// 1000069, ADR 0175 §9): open items with their clock, the open ballot, and the
|
|
3
|
+
// closed history — assembled server-side, decided nowhere.
|
|
4
|
+
//
|
|
5
|
+
// DB-free (db monkeypatched, the sibling board tests' idiom). The invariants:
|
|
6
|
+
// 1. THE VIEW DECIDES NOTHING — it projects rows; the payload carries each
|
|
7
|
+
// item's SNAPSHOT so the room describes the rule the item actually runs
|
|
8
|
+
// under, and remaining time is the database's number, passed through.
|
|
9
|
+
// 2. SUBJECTS ARE ENRICHED, HONESTLY — an idea shows its title + author, an
|
|
10
|
+
// amendment its proposed block; a subject whose row is GONE projects
|
|
11
|
+
// subject:null rather than being dropped or invented.
|
|
12
|
+
// 3. THE BALLOT IS OPEN — voter identity rides every vote row (§9), and an
|
|
13
|
+
// objection carries its reason + section for the room to show.
|
|
14
|
+
// 4. THE READ IS GATED ON THE VOTE ATOM, NOT THE MANAGE ATOM — seeing the
|
|
15
|
+
// ballot is part of casting; the pin (and the generalized gate test in
|
|
16
|
+
// government_routes.mjs) hold the route to board.vote.cast.
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { test } from 'node:test';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
27
|
+
const board = require(path.join(GOV, 'board.js'));
|
|
28
|
+
const db = require(path.join(GOV, 'db.js'));
|
|
29
|
+
|
|
30
|
+
const { boardRoomView } = board;
|
|
31
|
+
|
|
32
|
+
const MONARCHY = { membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true };
|
|
33
|
+
|
|
34
|
+
async function withDb(stubs, fn) {
|
|
35
|
+
const originals = {};
|
|
36
|
+
for (const [k, v] of Object.entries(stubs)) { originals[k] = db[k]; db[k] = v; }
|
|
37
|
+
try { return await fn(); }
|
|
38
|
+
finally { for (const [k, v] of Object.entries(originals)) db[k] = v; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test('the assembled view: enrichment, the open ballot, the snapshot, the database clock — all passthrough', async () => {
|
|
42
|
+
const view = await withDb({
|
|
43
|
+
listOpenBoardItems: async () => [
|
|
44
|
+
{ id: 11n, subject_type: 'full_idea', subject_id: 7n, opened_by: null, opened_at: 'T1', closes_at: 'T2', remaining_seconds: 540, expired: false, closed_at: null, outcome: null, constitution: MONARCHY },
|
|
45
|
+
{ id: 12n, subject_type: 'constitutional_amendment', subject_id: 5n, opened_by: 42n, opened_at: 'T1', closes_at: null, remaining_seconds: null, expired: false, closed_at: null, outcome: null, constitution: MONARCHY },
|
|
46
|
+
],
|
|
47
|
+
listClosedBoardItems: async (limit) => { assert.equal(limit, 20); return [
|
|
48
|
+
{ id: 9, subject_type: 'full_idea', subject_id: 6, opened_by: null, opened_at: 'T0', closes_at: null, closed_at: 'T3', outcome: 'returned', constitution: MONARCHY },
|
|
49
|
+
]; },
|
|
50
|
+
getVotesForItems: async (ids) => {
|
|
51
|
+
assert.deepEqual(ids.map(String).sort(), ['11', '12', '9'].sort(), 'one ballot query for ALL items, open and closed');
|
|
52
|
+
return [
|
|
53
|
+
{ item_id: 11n, voter_id: 42n, github_login: 'first-member', display_name: 'First Member', direction: 'yes', reason_md: null, section_key: null, cast_at: 'T2' },
|
|
54
|
+
{ item_id: 9, voter_id: 43, github_login: 'second-member', display_name: null, direction: 'no', reason_md: 'fit is thin', section_key: 'fit', cast_at: 'T2' },
|
|
55
|
+
];
|
|
56
|
+
},
|
|
57
|
+
getIdeaSubjects: async (ids) => {
|
|
58
|
+
assert.deepEqual(ids.map(String).sort(), ['6', '7']);
|
|
59
|
+
return [{ id: 7n, title: 'A big idea', captured_by: 9n }]; // idea 6 is GONE — deleted subject
|
|
60
|
+
},
|
|
61
|
+
getAmendmentSubjects: async (ids) => {
|
|
62
|
+
assert.deepEqual(ids.map(String), ['5']);
|
|
63
|
+
return [{ id: 5n, proposed: { pass_rule: 'consent' }, rationale_md: 'open up', proposed_by: 42n }];
|
|
64
|
+
},
|
|
65
|
+
}, () => boardRoomView());
|
|
66
|
+
|
|
67
|
+
assert.equal(view.open.length, 2);
|
|
68
|
+
assert.equal(view.closed.length, 1);
|
|
69
|
+
|
|
70
|
+
const idea = view.open[0];
|
|
71
|
+
assert.deepEqual(idea.subject, { title: 'A big idea', author_id: '9' }, 'an idea shows its title + author (R17 greys anti-self from this)');
|
|
72
|
+
assert.equal(idea.remaining_seconds, 540, 'the DATABASE clock, passed through — the client never does clock math');
|
|
73
|
+
assert.deepEqual(idea.constitution, MONARCHY, 'the SNAPSHOT rides along — the room describes the rule the item runs under');
|
|
74
|
+
assert.equal(idea.votes.length, 1);
|
|
75
|
+
assert.equal(idea.votes[0].github_login, 'first-member', 'the ballot is OPEN — identity on every vote row');
|
|
76
|
+
|
|
77
|
+
const amendment = view.open[1];
|
|
78
|
+
assert.deepEqual(amendment.subject, { proposed: { pass_rule: 'consent' }, rationale_md: 'open up', author_id: '42' });
|
|
79
|
+
assert.equal(amendment.remaining_seconds, null, 'no clock is null, not zero');
|
|
80
|
+
|
|
81
|
+
const gone = view.closed[0];
|
|
82
|
+
assert.equal(gone.subject, null, 'a deleted subject projects null — shown honestly, never dropped or invented');
|
|
83
|
+
assert.equal(gone.outcome, 'returned');
|
|
84
|
+
assert.equal(gone.votes[0].section_key, 'fit', 'the objection travels with the closed history');
|
|
85
|
+
// every id is a string — Postgres bigints and JS numbers must compare equal downstream
|
|
86
|
+
for (const item of [...view.open, ...view.closed]) {
|
|
87
|
+
assert.equal(typeof item.id, 'string');
|
|
88
|
+
assert.equal(typeof item.subject_id, 'string');
|
|
89
|
+
for (const v of item.votes) assert.equal(typeof v.voter_id, 'string');
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('an empty board is two empty lists — no error, no placeholder rows', async () => {
|
|
94
|
+
const view = await withDb({
|
|
95
|
+
listOpenBoardItems: async () => [],
|
|
96
|
+
listClosedBoardItems: async () => [],
|
|
97
|
+
getVotesForItems: async (ids) => { assert.deepEqual(ids, []); return []; },
|
|
98
|
+
getIdeaSubjects: async (ids) => { assert.deepEqual(ids, []); return []; },
|
|
99
|
+
getAmendmentSubjects: async (ids) => { assert.deepEqual(ids, []); return []; },
|
|
100
|
+
}, () => boardRoomView());
|
|
101
|
+
assert.deepEqual(view, { open: [], closed: [] });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('the reads are indexed and clock math stays in SQL (source pins)', () => {
|
|
105
|
+
const src = fs.readFileSync(path.join(GOV, 'db.js'), 'utf8');
|
|
106
|
+
const open = src.slice(src.indexOf('async function listOpenBoardItems'), src.indexOf('async function listClosedBoardItems'));
|
|
107
|
+
assert.match(open, /EXTRACT\(EPOCH FROM \(closes_at - now\(\)\)\)/, 'remaining time is the DATABASE clock');
|
|
108
|
+
assert.match(open, /GREATEST\(0,/, 'a just-expired item reads 0, never negative');
|
|
109
|
+
const closed = src.slice(src.indexOf('async function listClosedBoardItems'), src.indexOf('async function getVotesForItems'));
|
|
110
|
+
assert.match(closed, /ORDER BY closed_at DESC/, 'newest closes first');
|
|
111
|
+
const migration = fs.readFileSync(path.join(GOV, 'migrations', 'government_008_closed_items_index.sql'), 'utf8');
|
|
112
|
+
assert.ok(migration.includes('government_board_items_closed_by_time'), 'the closed-history read is indexed (the R13 grader lesson, applied up front)');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('the room is gated on the vote atom, and the page shell widened WITH the tab seal (the R16 pair)', () => {
|
|
116
|
+
const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
|
|
117
|
+
assert.equal(rrc.MODULE_EXPECTED_RANKS.government['GET /government/board/items'],
|
|
118
|
+
`${rrc.PERM_PIN_PREFIX}board.vote.cast`, 'seeing the ballot is part of casting — never the manage atom');
|
|
119
|
+
// The page-gate widening and the client-side ranks seal land TOGETHER: the
|
|
120
|
+
// shell admitting metic+ without the admin tab sealing itself would render a
|
|
121
|
+
// broken tab; the seal without the widening would seal a page nobody new can
|
|
122
|
+
// reach. Both halves pinned here so neither can be reverted alone.
|
|
123
|
+
const serve = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'serve-internal.js'), 'utf8');
|
|
124
|
+
const idx = serve.indexOf('GOVERNMENT_PAGE_RE.test(norm)');
|
|
125
|
+
assert.ok(serve.slice(idx, idx + 200).includes('requireGovernmentPage'),
|
|
126
|
+
'the shell admits page.view.government (metic+), not the manage atom');
|
|
127
|
+
const client = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'government.js'), 'utf8');
|
|
128
|
+
assert.match(client, /sealRanksPanel/, 'the admin tab seals itself for non-archons');
|
|
129
|
+
});
|
|
@@ -40,6 +40,7 @@ const NON_GRANT_MIGRATIONS = new Set([
|
|
|
40
40
|
'government_005_seeded_rank_descriptions.sql', // ADR 0174 seeded-description text fix (task 1003083)
|
|
41
41
|
'government_006_returned_items_index.sql', // R13 returned-item lookup index (task 1003026)
|
|
42
42
|
'government_007_amendments.sql', // R14 amendment proposals table (task 1003027)
|
|
43
|
+
'government_008_closed_items_index.sql', // R16 closed-history index (task 1003029)
|
|
43
44
|
]);
|
|
44
45
|
|
|
45
46
|
// Permission keys RENAMED after the grant migrations that first granted them.
|
|
@@ -67,8 +67,12 @@ const ORACLE = {
|
|
|
67
67
|
'/builders/people': 'requireProjectCuratePage',
|
|
68
68
|
'/builders/scouting': 'requireProjectCuratePage',
|
|
69
69
|
|
|
70
|
-
//
|
|
71
|
-
|
|
70
|
+
// Metic+ since R16 (goal 1000069): the Government page is three rooms —
|
|
71
|
+
// Constitution · Board Room · Ranks & Permissions — and only the third is the
|
|
72
|
+
// archon admin surface, whose DATA routes all still gate on government.manage
|
|
73
|
+
// individually. The shell follows the widest legitimate audience; the board
|
|
74
|
+
// reads carry the vote atom, the manage tab seals itself for non-archons.
|
|
75
|
+
'/builders/government': 'requireGovernmentPage',
|
|
72
76
|
|
|
73
77
|
// Deliberately UNGATED — the landing card, the rank ladder, the reward
|
|
74
78
|
// schedule and a builder's public portfolio all render for a stranger.
|