@bongos/core 1.19.231 → 1.19.232
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 +33 -23
- 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 +4 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +91 -3
- package/docs/api-reference.md +3 -2
- package/modules/government/board.js +110 -6
- package/modules/government/config.js +31 -0
- package/modules/government/db.js +22 -0
- package/modules/government/migrations/government_007_amendments.sql +25 -0
- package/modules/government/routes/government.js +37 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/route-rank-check.js +3 -0
- package/src/module-api.js +3 -1
- package/tests/government_board_amendment.mjs +226 -0
- package/tests/government_seed.mjs +1 -0
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.232",
|
|
6
|
+
"core_contract": "1.19.232",
|
|
7
|
+
"source_commit": "0410dea5ee4d77aab90467679874cc5abee21963",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-18T11:
|
|
9
|
+
"built_at": "2026-08-18T11:50:40.699Z",
|
|
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": 1315,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1647,
|
|
20
|
+
"tree_sha256": "94232cdfd24e7ff58e77b2642499e86fad84a4d09483c8b5150028b363080d4d",
|
|
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": "6be7d91bb8c0b620f3a67e071e96435946413330e05c97bf901fd7241bbd10f9"
|
|
351
351
|
},
|
|
352
352
|
{
|
|
353
353
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
354
354
|
"mode": "0000644",
|
|
355
|
-
"sha256": "
|
|
355
|
+
"sha256": "6af213e35ced87effe655c14a220b472768551c37f0cd7abddebcbdbcb7d81a9"
|
|
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": "18f00d369a1689973e2eeb33034302142884cb2836d3f1ea76b882f3379d0cf8"
|
|
366
366
|
},
|
|
367
367
|
{
|
|
368
368
|
"path": "clients/bongos-client/index.d.ts",
|
|
369
369
|
"mode": "0000644",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "ed6940e45f5634264705869043f0a795ddb4bea264f036789b3c80cc08424b97"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/index.mjs",
|
|
374
374
|
"mode": "0000644",
|
|
375
|
-
"sha256": "
|
|
375
|
+
"sha256": "e3d388b90604d290b3e6346129701b7f2a0a15b4c3b6abe915a43c8702daa1e8"
|
|
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": "3212eaeaaf4f1f64889fa6b144fcc48845ed12c4fe08b2a74d3c2bcffc503064"
|
|
1376
1376
|
},
|
|
1377
1377
|
{
|
|
1378
1378
|
"path": "docs/api/openapi.json",
|
|
1379
1379
|
"mode": "0000644",
|
|
1380
|
-
"sha256": "
|
|
1380
|
+
"sha256": "0709c0e537931a75055afea96cf48dd9f3820faa09cd781f07028b7d0b1f26c8"
|
|
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": "359de8efb5ec1365e6114a8c5b3d62aadd484ca7d94d1a36684b6c0c640aaa99"
|
|
2906
2906
|
},
|
|
2907
2907
|
{
|
|
2908
2908
|
"path": "modules/government/catalog.js",
|
|
@@ -2912,12 +2912,12 @@
|
|
|
2912
2912
|
{
|
|
2913
2913
|
"path": "modules/government/config.js",
|
|
2914
2914
|
"mode": "0000644",
|
|
2915
|
-
"sha256": "
|
|
2915
|
+
"sha256": "56bd51c927220b6bc0dc749ad1d591addbed25870e532b8d95da6ba5eab3f763"
|
|
2916
2916
|
},
|
|
2917
2917
|
{
|
|
2918
2918
|
"path": "modules/government/db.js",
|
|
2919
2919
|
"mode": "0000644",
|
|
2920
|
-
"sha256": "
|
|
2920
|
+
"sha256": "cb5c481fcd1a9f6becb63b88a1f0ca4004e592a2f70e02c968a3cc10378dabd5"
|
|
2921
2921
|
},
|
|
2922
2922
|
{
|
|
2923
2923
|
"path": "modules/government/migrations/governance_001_tables.sql",
|
|
@@ -2984,6 +2984,11 @@
|
|
|
2984
2984
|
"mode": "0000644",
|
|
2985
2985
|
"sha256": "b2c06a954e93487f7df85eb7333c65284e364499fb96c31aad30a263c36b8f73"
|
|
2986
2986
|
},
|
|
2987
|
+
{
|
|
2988
|
+
"path": "modules/government/migrations/government_007_amendments.sql",
|
|
2989
|
+
"mode": "0000644",
|
|
2990
|
+
"sha256": "5629112a9bb932873a00b8ddefa847d9a21ed0f8f59cc8a77ffb9c9c5e0eea49"
|
|
2991
|
+
},
|
|
2987
2992
|
{
|
|
2988
2993
|
"path": "modules/government/module.json",
|
|
2989
2994
|
"mode": "0000644",
|
|
@@ -3017,7 +3022,7 @@
|
|
|
3017
3022
|
{
|
|
3018
3023
|
"path": "modules/government/routes/government.js",
|
|
3019
3024
|
"mode": "0000644",
|
|
3020
|
-
"sha256": "
|
|
3025
|
+
"sha256": "2bde6d18afc3f24ae2eaeac08b16d83d3ab6ddb22b61da4b9b2f261ee4e844a6"
|
|
3021
3026
|
},
|
|
3022
3027
|
{
|
|
3023
3028
|
"path": "modules/government/routes/rank-sync.js",
|
|
@@ -4312,12 +4317,12 @@
|
|
|
4312
4317
|
{
|
|
4313
4318
|
"path": "package-lock.json",
|
|
4314
4319
|
"mode": "0000644",
|
|
4315
|
-
"sha256": "
|
|
4320
|
+
"sha256": "de0ff156427786537d3a4231d4fae7721d52027b9a25d6185bc53efac35b24d2"
|
|
4316
4321
|
},
|
|
4317
4322
|
{
|
|
4318
4323
|
"path": "package.json",
|
|
4319
4324
|
"mode": "0000644",
|
|
4320
|
-
"sha256": "
|
|
4325
|
+
"sha256": "c5fa9bf830e64903a205cf28feabd9255772343cc1f002f344fa92ede02f7f7d"
|
|
4321
4326
|
},
|
|
4322
4327
|
{
|
|
4323
4328
|
"path": "public-docs/index.html",
|
|
@@ -5797,7 +5802,7 @@
|
|
|
5797
5802
|
{
|
|
5798
5803
|
"path": "src/bongos/route-rank-check.js",
|
|
5799
5804
|
"mode": "0000644",
|
|
5800
|
-
"sha256": "
|
|
5805
|
+
"sha256": "34850de6361ab78ce8788e20c483ded22d3de9ff9fb42bf14497509b0870f66c"
|
|
5801
5806
|
},
|
|
5802
5807
|
{
|
|
5803
5808
|
"path": "src/bongos/routes.js",
|
|
@@ -5932,7 +5937,7 @@
|
|
|
5932
5937
|
{
|
|
5933
5938
|
"path": "src/module-api.js",
|
|
5934
5939
|
"mode": "0000644",
|
|
5935
|
-
"sha256": "
|
|
5940
|
+
"sha256": "1302cbcd4f4b6b35032165625c203741ccc846304680876d3789ce212d0bc746"
|
|
5936
5941
|
},
|
|
5937
5942
|
{
|
|
5938
5943
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6854,6 +6859,11 @@
|
|
|
6854
6859
|
"mode": "0000644",
|
|
6855
6860
|
"sha256": "8f5354cbacf15c9b49500f96b265fe52f5c0d4500a4ec7b41b4819a149782893"
|
|
6856
6861
|
},
|
|
6862
|
+
{
|
|
6863
|
+
"path": "tests/government_board_amendment.mjs",
|
|
6864
|
+
"mode": "0000644",
|
|
6865
|
+
"sha256": "7b20463f4d0940c43a5555a7b63d156202d334477267b7d2bc045e27e964d253"
|
|
6866
|
+
},
|
|
6857
6867
|
{
|
|
6858
6868
|
"path": "tests/government_board_close.mjs",
|
|
6859
6869
|
"mode": "0000644",
|
|
@@ -6957,7 +6967,7 @@
|
|
|
6957
6967
|
{
|
|
6958
6968
|
"path": "tests/government_seed.mjs",
|
|
6959
6969
|
"mode": "0000644",
|
|
6960
|
-
"sha256": "
|
|
6970
|
+
"sha256": "9c0c1049180013f30a3403a4e308112ed871119d9e60c459e9573108b35a5a27"
|
|
6961
6971
|
},
|
|
6962
6972
|
{
|
|
6963
6973
|
"path": "tests/government_session_scope.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
|
+
- 283 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
|
+
// POST /government/board/items — rank: metic+archon — POST /government/board/items
|
|
353
|
+
postGovernmentBoardItems: (args) => request("POST", "/government/board/items", { hasBody: true }, args),
|
|
352
354
|
// POST /government/board/items/{itemId}/votes — rank: any-builder — POST /government/board/items/:itemId/votes
|
|
353
355
|
postGovernmentBoardItemsItemIdVotes: (args) => request("POST", "/government/board/items/{itemId}/votes", { hasBody: true }, args),
|
|
354
356
|
// GET /government/permissions — rank: archon — GET /government/permissions
|
|
@@ -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
|
+
// POST /government/board/items — rank: metic+archon — POST /government/board/items
|
|
352
|
+
postGovernmentBoardItems: (args) => request("POST", "/government/board/items", { hasBody: true }, args),
|
|
351
353
|
// POST /government/board/items/{itemId}/votes — rank: any-builder — POST /government/board/items/:itemId/votes
|
|
352
354
|
postGovernmentBoardItemsItemIdVotes: (args) => request("POST", "/government/board/items/{itemId}/votes", { hasBody: true }, args),
|
|
353
355
|
// GET /government/permissions — rank: archon — GET /government/permissions
|
|
@@ -233,6 +233,8 @@ export interface PostGoalsRequest { version_id: string; title: string; descripti
|
|
|
233
233
|
export interface PostGoalsResponse { ok: boolean; goal: unknown; succeeds_goal_id: unknown }
|
|
234
234
|
export interface PostGovernmentAssignmentsRequest { builder_id: number; rank_key: string }
|
|
235
235
|
export interface PostGovernmentBoardItemsItemIdVotesRequest { direction: string; reason_md?: string; section_key?: string }
|
|
236
|
+
export interface PostGovernmentBoardItemsRequest { subject_type: string; proposal: Record<string, unknown>; rationale_md?: string }
|
|
237
|
+
export interface PostGovernmentBoardItemsResponse { ok: boolean; amendment: unknown; item: unknown }
|
|
236
238
|
export interface PostGovernmentRanksRankKeyResetResponse { ok: boolean; rank_key: unknown; seeded_rank: unknown; permission_count: unknown }
|
|
237
239
|
export interface PostGovernmentRanksRequest { rank_key: string; label: string; description?: string }
|
|
238
240
|
export interface PostGovernmentRanksResponse { rank: unknown }
|
|
@@ -601,6 +603,8 @@ export interface BongosClient {
|
|
|
601
603
|
postGovernmentAssignments(args: RequestArgs & { body: PostGovernmentAssignmentsRequest }): Promise<ApiResponse>;
|
|
602
604
|
/** DELETE /government/assignments/{builderId}/{rankKey} — rank: archon */
|
|
603
605
|
deleteGovernmentAssignmentsBuilderIdRankKey(args?: RequestArgs): Promise<DeleteGovernmentAssignmentsBuilderIdRankKeyResponse>;
|
|
606
|
+
/** POST /government/board/items — rank: metic+archon */
|
|
607
|
+
postGovernmentBoardItems(args: RequestArgs & { body: PostGovernmentBoardItemsRequest }): Promise<PostGovernmentBoardItemsResponse>;
|
|
604
608
|
/** POST /government/board/items/{itemId}/votes — rank: any-builder */
|
|
605
609
|
postGovernmentBoardItemsItemIdVotes(args: RequestArgs & { body: PostGovernmentBoardItemsItemIdVotesRequest }): Promise<ApiResponse>;
|
|
606
610
|
/** GET /government/permissions — rank: archon */
|
|
@@ -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
|
+
// POST /government/board/items — rank: metic+archon — POST /government/board/items
|
|
349
|
+
postGovernmentBoardItems: (args) => request("POST", "/government/board/items", { hasBody: true }, args),
|
|
348
350
|
// POST /government/board/items/{itemId}/votes — rank: any-builder — POST /government/board/items/:itemId/votes
|
|
349
351
|
postGovernmentBoardItemsItemIdVotes: (args) => request("POST", "/government/board/items/{itemId}/votes", { hasBody: true }, args),
|
|
350
352
|
// GET /government/permissions — rank: archon — GET /government/permissions
|
package/docs/api/openapi.json
CHANGED
|
@@ -5336,6 +5336,58 @@
|
|
|
5336
5336
|
]
|
|
5337
5337
|
}
|
|
5338
5338
|
},
|
|
5339
|
+
"/government/board/items": {
|
|
5340
|
+
"post": {
|
|
5341
|
+
"operationId": "post_government_board_items",
|
|
5342
|
+
"tags": [
|
|
5343
|
+
"government"
|
|
5344
|
+
],
|
|
5345
|
+
"summary": "POST /government/board/items",
|
|
5346
|
+
"description": "board.item.open — put an AMENDMENT to the board (R14 of goal 1000069, ADR 0175 §7). The board's other subject never comes through here: a Full Idea's window opens AUTOMATICALLY on grade pass (R07), so requesting one is refused with the reason rather than silently doubling the paths — exactly what the catalog's own guards note predicted this route would be. The proposal is validated STRICTLY in board.js (never sanitized — the board must not ratify text the proposer did not write), and the window runs under the constitution IN FORCE, which under monarchy resolves to \"the Archon ratifies\" — their own proposal included (§7's no-bootstrap-exception).\n\n**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `board.item.open` (all required).",
|
|
5347
|
+
"x-rank": "metic+archon",
|
|
5348
|
+
"x-source": "modules/government/routes/government.js",
|
|
5349
|
+
"x-permissions": [
|
|
5350
|
+
"board.item.open"
|
|
5351
|
+
],
|
|
5352
|
+
"requestBody": {
|
|
5353
|
+
"required": true,
|
|
5354
|
+
"content": {
|
|
5355
|
+
"application/json": {
|
|
5356
|
+
"schema": {
|
|
5357
|
+
"$ref": "#/components/schemas/PostGovernmentBoardItemsRequest"
|
|
5358
|
+
}
|
|
5359
|
+
}
|
|
5360
|
+
},
|
|
5361
|
+
"x-validated": true
|
|
5362
|
+
},
|
|
5363
|
+
"responses": {
|
|
5364
|
+
"200": {
|
|
5365
|
+
"description": "Success.",
|
|
5366
|
+
"content": {
|
|
5367
|
+
"application/json": {
|
|
5368
|
+
"schema": {
|
|
5369
|
+
"$ref": "#/components/schemas/PostGovernmentBoardItemsResponse"
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
5373
|
+
},
|
|
5374
|
+
"400": {
|
|
5375
|
+
"$ref": "#/components/responses/ValidationFailed"
|
|
5376
|
+
},
|
|
5377
|
+
"401": {
|
|
5378
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
5379
|
+
},
|
|
5380
|
+
"403": {
|
|
5381
|
+
"$ref": "#/components/responses/Forbidden"
|
|
5382
|
+
}
|
|
5383
|
+
},
|
|
5384
|
+
"security": [
|
|
5385
|
+
{
|
|
5386
|
+
"builderSession": []
|
|
5387
|
+
}
|
|
5388
|
+
]
|
|
5389
|
+
}
|
|
5390
|
+
},
|
|
5339
5391
|
"/government/board/items/{itemId}/votes": {
|
|
5340
5392
|
"post": {
|
|
5341
5393
|
"operationId": "post_government_board_items_itemId_votes",
|
|
@@ -16098,6 +16150,42 @@
|
|
|
16098
16150
|
],
|
|
16099
16151
|
"additionalProperties": false
|
|
16100
16152
|
},
|
|
16153
|
+
"PostGovernmentBoardItemsRequest": {
|
|
16154
|
+
"type": "object",
|
|
16155
|
+
"properties": {
|
|
16156
|
+
"subject_type": {
|
|
16157
|
+
"type": "string",
|
|
16158
|
+
"maxLength": 32
|
|
16159
|
+
},
|
|
16160
|
+
"proposal": {
|
|
16161
|
+
"type": "object"
|
|
16162
|
+
},
|
|
16163
|
+
"rationale_md": {
|
|
16164
|
+
"type": "string",
|
|
16165
|
+
"maxLength": 5000
|
|
16166
|
+
}
|
|
16167
|
+
},
|
|
16168
|
+
"required": [
|
|
16169
|
+
"subject_type",
|
|
16170
|
+
"proposal"
|
|
16171
|
+
],
|
|
16172
|
+
"additionalProperties": false
|
|
16173
|
+
},
|
|
16174
|
+
"PostGovernmentBoardItemsResponse": {
|
|
16175
|
+
"type": "object",
|
|
16176
|
+
"properties": {
|
|
16177
|
+
"ok": {
|
|
16178
|
+
"type": "boolean"
|
|
16179
|
+
},
|
|
16180
|
+
"amendment": {},
|
|
16181
|
+
"item": {}
|
|
16182
|
+
},
|
|
16183
|
+
"required": [
|
|
16184
|
+
"ok",
|
|
16185
|
+
"amendment",
|
|
16186
|
+
"item"
|
|
16187
|
+
]
|
|
16188
|
+
},
|
|
16101
16189
|
"PostGovernmentRanksRankKeyResetResponse": {
|
|
16102
16190
|
"type": "object",
|
|
16103
16191
|
"properties": {
|
|
@@ -17881,9 +17969,9 @@
|
|
|
17881
17969
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
17882
17970
|
}
|
|
17883
17971
|
},
|
|
17884
|
-
"x-endpoint-count":
|
|
17885
|
-
"x-schema-count":
|
|
17972
|
+
"x-endpoint-count": 283,
|
|
17973
|
+
"x-schema-count": 321,
|
|
17886
17974
|
"x-undocumented-bodies": 8,
|
|
17887
|
-
"x-response-schemas":
|
|
17975
|
+
"x-response-schemas": 215,
|
|
17888
17976
|
"x-generated-by": "scripts/gds/gen-api-docs.js"
|
|
17889
17977
|
}
|
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. 283 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` (11)
|
|
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
|
+
| 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). |
|
|
226
227
|
| 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). |
|
|
227
228
|
| 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. |
|
|
228
229
|
| GET | `/api/bongos/government/ranks` | `archon` | — | government.manage — the full rank list with each rank's grants. |
|
|
@@ -42,8 +42,13 @@
|
|
|
42
42
|
|
|
43
43
|
const api = require('../../src/module-api');
|
|
44
44
|
const db = require('./db');
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// config is required as a NAMESPACE so applyBoardAmendment resolves by property
|
|
46
|
+
// lookup at CALL time — a test can redirect the instance-pack write to a temp
|
|
47
|
+
// file (the reward-port delegation lesson: a destructured binding is frozen at
|
|
48
|
+
// load and un-patchable).
|
|
49
|
+
const governmentConfig = require('./config');
|
|
50
|
+
const { loadGovernmentConfig, PASS_RULES, MAX_WINDOW_MINUTES } = governmentConfig;
|
|
51
|
+
const { isBoardMember, resolveBoardMembers, parseMembershipPredicate } = require('./board-membership');
|
|
47
52
|
|
|
48
53
|
// ADR 0175 §5: the completeness bar a Full Idea must clear to reach the board.
|
|
49
54
|
// Held equal to economy's IDEA_FULL_PASS_BAR by test — see the header.
|
|
@@ -180,9 +185,19 @@ async function castVote({ itemId, voterId, direction, reasonMd = null, sectionKe
|
|
|
180
185
|
const constitution = item.constitution || {};
|
|
181
186
|
if (!(await isBoardMember(voterId, constitution))) return { ok: false, reason: 'not_board_member' };
|
|
182
187
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
188
|
+
// ANTI-SELF IS A FULL-IDEA RULE, NOT AN AMENDMENT RULE (R14, ADR 0175 §§7/11).
|
|
189
|
+
// The bright line exists because an idea's author GAINS from ratification
|
|
190
|
+
// (credits, legitimacy) — ceremonial self-ratification is what it prevents.
|
|
191
|
+
// An amendment pays nothing, and §7 explicitly blesses the monarchy act:
|
|
192
|
+
// under a one-Archon board with no clock, the proposer must be able to
|
|
193
|
+
// ratify their own amendment or the subject is dead on arrival — and adding
|
|
194
|
+
// a special escape for that case is the bootstrap exception §7 forbids.
|
|
195
|
+
// Skipping anti-self FOR AMENDMENTS is the rule that needs no exceptions.
|
|
196
|
+
if (item.subject_type === 'full_idea') {
|
|
197
|
+
const authorId = await db.getBoardItemAuthor(item);
|
|
198
|
+
if (authorId != null && String(authorId) === String(voterId)) {
|
|
199
|
+
return { ok: false, reason: 'anti_self_vote' };
|
|
200
|
+
}
|
|
186
201
|
}
|
|
187
202
|
|
|
188
203
|
const inserted = await db.insertBoardVote({
|
|
@@ -326,6 +341,24 @@ async function closeItem(item, { cause = 'vote' } = {}) {
|
|
|
326
341
|
|
|
327
342
|
const authorKarma = outcome === 'passed' ? await awardAuthorRatifyKarma(closed) : null;
|
|
328
343
|
|
|
344
|
+
// R14: a PASSED amendment becomes the live constitution — applied AFTER the
|
|
345
|
+
// close commits (the board's decision is the durable record), loudly on
|
|
346
|
+
// failure, never silently absorbed. The apply is re-drivable from the passed
|
|
347
|
+
// item + amendment rows. A returned amendment applies nothing.
|
|
348
|
+
let appliedConstitution = null;
|
|
349
|
+
if (outcome === 'passed' && closed.subject_type === 'constitutional_amendment') {
|
|
350
|
+
try {
|
|
351
|
+
const amendment = await db.getAmendment(closed.subject_id);
|
|
352
|
+
if (amendment) {
|
|
353
|
+
appliedConstitution = governmentConfig.applyBoardAmendment(amendment.proposed);
|
|
354
|
+
} else {
|
|
355
|
+
console.error(`[government] passed amendment item ${closed.id} names amendment ${closed.subject_id}, which does not exist — NOT applied`);
|
|
356
|
+
}
|
|
357
|
+
} catch (applyErr) {
|
|
358
|
+
console.error(`[government] amendment ${closed.subject_id} ratified but NOT applied (re-drivable from the passed item): ${applyErr && applyErr.message}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
329
362
|
if (outcome === 'passed') {
|
|
330
363
|
const payload = passPayload(closed, { authorId, completenessScore, passedAt: closed.closed_at });
|
|
331
364
|
try {
|
|
@@ -333,7 +366,11 @@ async function closeItem(item, { cause = 'vote' } = {}) {
|
|
|
333
366
|
} catch (_) { /* emitAsync isolates listener errors; nothing that moves credits subscribes */ }
|
|
334
367
|
}
|
|
335
368
|
|
|
336
|
-
return {
|
|
369
|
+
return {
|
|
370
|
+
closed: true, item: closed, outcome, cause, author_karma: authorKarma,
|
|
371
|
+
...(credit ? { credit } : {}),
|
|
372
|
+
...(appliedConstitution ? { applied_constitution: appliedConstitution } : {}),
|
|
373
|
+
};
|
|
337
374
|
}
|
|
338
375
|
|
|
339
376
|
// The post-vote hook R08's route fires after a FRESH cast. Reads everything it
|
|
@@ -351,6 +388,71 @@ async function maybeCloseAfterVote(itemId) {
|
|
|
351
388
|
return closeItem(item, { cause: 'vote' });
|
|
352
389
|
}
|
|
353
390
|
|
|
391
|
+
// ── proposing an amendment (BV1.R14 of goal 1000069, ADR 0175 §7) ────────────
|
|
392
|
+
//
|
|
393
|
+
// A proposal is validated STRICTLY, not sanitized. R05's sanitizer falls back
|
|
394
|
+
// field-by-field because a config FILE must boot sane no matter what is in it;
|
|
395
|
+
// a PROPOSAL is different — silently "correcting" it would put text before the
|
|
396
|
+
// board that the proposer never wrote, and the board would ratify a lie. Every
|
|
397
|
+
// field is required (a constitution is ratified whole, never by diff) and every
|
|
398
|
+
// invalid field is a named refusal.
|
|
399
|
+
function validateProposedConstitution(raw) {
|
|
400
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
401
|
+
return { ok: false, reason: 'proposal_not_an_object' };
|
|
402
|
+
}
|
|
403
|
+
const known = ['membership', 'pass_rule', 'window_minutes', 'close_early_on_full_turnout'];
|
|
404
|
+
for (const k of Object.keys(raw)) {
|
|
405
|
+
if (!known.includes(k)) return { ok: false, reason: 'unknown_field', field: k };
|
|
406
|
+
}
|
|
407
|
+
if (!parseMembershipPredicate(raw.membership)) {
|
|
408
|
+
return { ok: false, reason: 'bad_membership', expected: "the predicate grammar rank:<key>" };
|
|
409
|
+
}
|
|
410
|
+
if (!PASS_RULES.includes(raw.pass_rule)) {
|
|
411
|
+
return { ok: false, reason: 'bad_pass_rule', expected: PASS_RULES };
|
|
412
|
+
}
|
|
413
|
+
const w = raw.window_minutes;
|
|
414
|
+
if (!(w === null || (Number.isInteger(w) && w > 0 && w <= MAX_WINDOW_MINUTES))) {
|
|
415
|
+
return { ok: false, reason: 'bad_window_minutes', expected: `null (no clock) or an integer 1..${MAX_WINDOW_MINUTES}` };
|
|
416
|
+
}
|
|
417
|
+
if (typeof raw.close_early_on_full_turnout !== 'boolean') {
|
|
418
|
+
return { ok: false, reason: 'bad_close_early', expected: 'boolean' };
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
ok: true,
|
|
422
|
+
proposal: {
|
|
423
|
+
membership: raw.membership,
|
|
424
|
+
pass_rule: raw.pass_rule,
|
|
425
|
+
window_minutes: w,
|
|
426
|
+
close_early_on_full_turnout: raw.close_early_on_full_turnout,
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// File the amendment and open its window IN THE SAME BREATH — a proposal that
|
|
432
|
+
// exists but was never put to the board is a state nothing can act on. The
|
|
433
|
+
// window runs under the constitution IN FORCE (snapshotted, like every item):
|
|
434
|
+
// under monarchy that is first_ratifier with no clock, which §7 resolves to
|
|
435
|
+
// "the Archon ratifies" — including the Archon's own proposal (see castVote's
|
|
436
|
+
// anti-self note). Concurrent amendment windows are legal like any concurrent
|
|
437
|
+
// windows; applies land in close order and the last close wins, deterministic
|
|
438
|
+
// because applyBoardAmendment rewrites the whole block.
|
|
439
|
+
async function proposeAmendment({ proposal, rationaleMd = null, proposedBy } = {}) {
|
|
440
|
+
const v = validateProposedConstitution(proposal);
|
|
441
|
+
if (!v.ok) return { ok: false, ...v };
|
|
442
|
+
if (proposedBy == null || proposedBy === '') return { ok: false, reason: 'missing_proposer' };
|
|
443
|
+
const amendment = await db.createAmendment({ proposed: v.proposal, rationaleMd, proposedBy });
|
|
444
|
+
const { board } = loadGovernmentConfig();
|
|
445
|
+
const item = await db.openBoardItem({
|
|
446
|
+
subjectType: 'constitutional_amendment',
|
|
447
|
+
subjectId: amendment.id,
|
|
448
|
+
openedBy: proposedBy, // an amendment HAS a human opener, unlike a grade-opened idea
|
|
449
|
+
constitution: board,
|
|
450
|
+
windowMinutes: board.window_minutes,
|
|
451
|
+
});
|
|
452
|
+
// A fresh amendment id can never collide with the one-open-per-subject index.
|
|
453
|
+
return { ok: true, amendment, item };
|
|
454
|
+
}
|
|
455
|
+
|
|
354
456
|
// ── R13's read: the feedback a returned idea carries back to its author ─────
|
|
355
457
|
//
|
|
356
458
|
// One reasoned objection is what returns an idea (§3), so the feedback IS that
|
|
@@ -389,4 +491,6 @@ module.exports = {
|
|
|
389
491
|
closeItem,
|
|
390
492
|
maybeCloseAfterVote,
|
|
391
493
|
latestRejectionForIdea,
|
|
494
|
+
validateProposedConstitution,
|
|
495
|
+
proposeAmendment,
|
|
392
496
|
};
|
|
@@ -256,11 +256,42 @@ function loadGovernmentConfig({ neutralPath = NEUTRAL_PATH, instancePath = INSTA
|
|
|
256
256
|
});
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
// ── applying a RATIFIED amendment (BV1.R14 of goal 1000069, ADR 0175 §7) ────
|
|
260
|
+
//
|
|
261
|
+
// A passed amendment becomes the live constitution by writing the INSTANCE
|
|
262
|
+
// pack's board block — the same file a human editing the constitution would
|
|
263
|
+
// touch, so there is ONE resolution path (env → instance → neutral) and R05's
|
|
264
|
+
// precedence story stays true. The write is atomic (tmp + rename), preserves
|
|
265
|
+
// everything else in the instance pack, and re-runs sanitizeBoard as a belt —
|
|
266
|
+
// the proposal was strictly validated at propose time, but a file on disk must
|
|
267
|
+
// never hold a shape the resolver would have to guess at.
|
|
268
|
+
//
|
|
269
|
+
// WHY A FILE WRITE AND NOT A DB ROW: R05 made the constitution config-resolved
|
|
270
|
+
// on purpose (the board block is a RULE SHAPE, not authority — who holds the
|
|
271
|
+
// governing rank stays a DB row). Moving the live value into the DB on
|
|
272
|
+
// ratification would fork the resolution into two paths, the exact thing the
|
|
273
|
+
// board-snapshot design avoids. The instance pack is host-owned and untracked,
|
|
274
|
+
// so a deploy pull never clobbers it.
|
|
275
|
+
//
|
|
276
|
+
// The caller (board.js closeItem) runs this AFTER the close commits — the
|
|
277
|
+
// board's decision is the durable record; a failed apply is logged loudly and
|
|
278
|
+
// re-appliable from the passed item + amendment rows, never silently absorbed.
|
|
279
|
+
function applyBoardAmendment(proposed, { instancePath = INSTANCE_PATH } = {}) {
|
|
280
|
+
const instance = readJson(instancePath) || {};
|
|
281
|
+
const next = { ...instance, board: { ...sanitizeBoard(proposed) } };
|
|
282
|
+
const json = JSON.stringify(next, null, 2) + '\n';
|
|
283
|
+
const tmp = `${instancePath}.tmp-${process.pid}`;
|
|
284
|
+
fs.writeFileSync(tmp, json);
|
|
285
|
+
fs.renameSync(tmp, instancePath);
|
|
286
|
+
return next.board;
|
|
287
|
+
}
|
|
288
|
+
|
|
259
289
|
module.exports = {
|
|
260
290
|
resolveGovernmentConfig,
|
|
261
291
|
loadGovernmentConfig,
|
|
262
292
|
grantableOnly,
|
|
263
293
|
sanitizeBoard,
|
|
294
|
+
applyBoardAmendment,
|
|
264
295
|
BOARD_DEFAULTS,
|
|
265
296
|
PASS_RULES,
|
|
266
297
|
MAX_WINDOW_MINUTES,
|
package/modules/government/db.js
CHANGED
|
@@ -437,6 +437,26 @@ async function getIdeaCompleteness(ideaId) {
|
|
|
437
437
|
return rows[0] ? rows[0].completeness_score : null;
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
+
// --- amendments (BV1.R14 of goal 1000069) ------------------------------------
|
|
441
|
+
|
|
442
|
+
async function createAmendment({ proposed, rationaleMd = null, proposedBy }) {
|
|
443
|
+
const { rows } = await pool.query(
|
|
444
|
+
`INSERT INTO government_amendments (proposed, rationale_md, proposed_by)
|
|
445
|
+
VALUES ($1::jsonb, $2, $3)
|
|
446
|
+
RETURNING *`,
|
|
447
|
+
[JSON.stringify(proposed), rationaleMd, proposedBy],
|
|
448
|
+
);
|
|
449
|
+
return rows[0];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function getAmendment(id) {
|
|
453
|
+
const { rows } = await pool.query(
|
|
454
|
+
`SELECT * FROM government_amendments WHERE id = $1`,
|
|
455
|
+
[id],
|
|
456
|
+
);
|
|
457
|
+
return rows[0] || null;
|
|
458
|
+
}
|
|
459
|
+
|
|
440
460
|
// R13's read: the most recent RETURNED item for a subject, carrying its
|
|
441
461
|
// objection (the single 'no' vote — one reasoned objection is what returns an
|
|
442
462
|
// idea, ADR 0175 §3). LEFT JOIN because a returned-by-expiry item could in
|
|
@@ -531,4 +551,6 @@ module.exports = {
|
|
|
531
551
|
getIdeaCompleteness,
|
|
532
552
|
getExpiredOpenBoardItems,
|
|
533
553
|
getLatestReturnedItemWithObjection,
|
|
554
|
+
createAmendment,
|
|
555
|
+
getAmendment,
|
|
534
556
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- government_007_amendments.sql — the board's second subject gets a home
|
|
2
|
+
-- (task 1003027 / R14 of goal 1000069, ADR 0175 §7).
|
|
3
|
+
--
|
|
4
|
+
-- A constitutional amendment is a PROPOSED board block put to the board. The
|
|
5
|
+
-- items table deliberately carries only (subject_type, subject_id) — a subject
|
|
6
|
+
-- is a row somewhere else — so the proposal's text needs its own table. One row
|
|
7
|
+
-- per proposal, immutable once filed: a revised proposal is a NEW row and a new
|
|
8
|
+
-- window, the same never-edit-history posture as everything else on the board.
|
|
9
|
+
--
|
|
10
|
+
-- `proposed` is stored EXACTLY as validated at propose time (strict validation
|
|
11
|
+
-- in board.js — a proposal is never silently corrected the way a config FILE
|
|
12
|
+
-- is, because ratifying text the proposer did not write would be the board
|
|
13
|
+
-- lying). The apply step re-runs the R05 sanitizer as a belt anyway.
|
|
14
|
+
|
|
15
|
+
BEGIN;
|
|
16
|
+
|
|
17
|
+
CREATE TABLE IF NOT EXISTS government_amendments (
|
|
18
|
+
id bigserial PRIMARY KEY,
|
|
19
|
+
proposed jsonb NOT NULL,
|
|
20
|
+
rationale_md text,
|
|
21
|
+
proposed_by bigint REFERENCES builders(id) ON DELETE SET NULL,
|
|
22
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
COMMIT;
|
|
@@ -414,6 +414,43 @@ module.exports = function buildGovernmentRouter() {
|
|
|
414
414
|
}
|
|
415
415
|
});
|
|
416
416
|
|
|
417
|
+
// rank: perm board.item.open — put an AMENDMENT to the board (R14 of goal
|
|
418
|
+
// 1000069, ADR 0175 §7). The board's other subject never comes through here:
|
|
419
|
+
// a Full Idea's window opens AUTOMATICALLY on grade pass (R07), so requesting
|
|
420
|
+
// one is refused with the reason rather than silently doubling the paths —
|
|
421
|
+
// exactly what the catalog's own guards note predicted this route would be.
|
|
422
|
+
// The proposal is validated STRICTLY in board.js (never sanitized — the board
|
|
423
|
+
// must not ratify text the proposer did not write), and the window runs under
|
|
424
|
+
// the constitution IN FORCE, which under monarchy resolves to "the Archon
|
|
425
|
+
// ratifies" — their own proposal included (§7's no-bootstrap-exception).
|
|
426
|
+
router.post('/government/board/items', api.requireBuilder, api.requirePermission('board.item.open'), async (req, res) => {
|
|
427
|
+
if (validateOrRespond(req, res, {
|
|
428
|
+
subject_type: { required: true, type: 'string', maxLength: 32 },
|
|
429
|
+
proposal: { required: true, type: 'object' },
|
|
430
|
+
rationale_md: { type: 'string', maxLength: 5000 },
|
|
431
|
+
})) return;
|
|
432
|
+
if (req.body.subject_type !== 'constitutional_amendment') {
|
|
433
|
+
return res.fail('subject_opens_automatically', 409, {
|
|
434
|
+
subject_type: req.body.subject_type,
|
|
435
|
+
reason: 'a Full Idea window opens by itself when the idea passes the completeness grade (ADR 0175 §5); only a constitutional_amendment is proposed through this route',
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
const out = await board.proposeAmendment({
|
|
440
|
+
proposal: req.body.proposal,
|
|
441
|
+
rationaleMd: req.body.rationale_md ?? null,
|
|
442
|
+
proposedBy: req.builder.id,
|
|
443
|
+
});
|
|
444
|
+
if (!out.ok) {
|
|
445
|
+
return res.fail('invalid_proposal', 400, { reason: out.reason, ...(out.field ? { field: out.field } : {}), ...(out.expected ? { expected: out.expected } : {}) });
|
|
446
|
+
}
|
|
447
|
+
res.status(201).json({ ok: true, amendment: out.amendment, item: out.item });
|
|
448
|
+
} catch (err) {
|
|
449
|
+
console.error('[gds] POST /government/board/items', err);
|
|
450
|
+
res.fail('propose_failed', { status: 500, message: 'internal error' });
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
417
454
|
// rank: perm board.vote.cast — cast a vote on an open board item (R08 of goal
|
|
418
455
|
// 1000069, ADR 0175). The atom FLOORS AT XENOS deliberately (the R04 carry-
|
|
419
456
|
// note): the permission is only the coarse gate, and the real wall is the
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.232",
|
|
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.232",
|
|
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.232",
|
|
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",
|
|
@@ -251,6 +251,9 @@ const GOVERNMENT_PINS = {
|
|
|
251
251
|
// The XENOS-floored atom is the COARSE gate by design; the real wall is the
|
|
252
252
|
// in-handler membership re-check against the item's snapshotted constitution.
|
|
253
253
|
'POST /government/board/items/:itemId/votes': 'perm:board.vote.cast',
|
|
254
|
+
// R14: propose a constitutional amendment (the board's only proposed subject —
|
|
255
|
+
// a Full Idea's window opens automatically on grade pass).
|
|
256
|
+
'POST /government/board/items': 'perm:board.item.open',
|
|
254
257
|
};
|
|
255
258
|
|
|
256
259
|
// Module-scoped privileged-rank pins: a privileged route that lives in a FEATURE
|
package/src/module-api.js
CHANGED
|
@@ -889,8 +889,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
889
889
|
// landed since 1.19.229 with no explicit bump. run 32131450528. (task 1002620)
|
|
890
890
|
// 1.19.231 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
891
891
|
// landed since 1.19.230 with no explicit bump. run 32133132988. (task 1002620)
|
|
892
|
+
// 1.19.232 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
893
|
+
// landed since 1.19.231 with no explicit bump. run 32133691409. (task 1002620)
|
|
892
894
|
// ---------------------------------------------------------------------------
|
|
893
|
-
const CORE_VERSION = '1.19.
|
|
895
|
+
const CORE_VERSION = '1.19.232'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
894
896
|
|
|
895
897
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
896
898
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// tests/government_board_amendment.mjs — constitutional amendments as the
|
|
2
|
+
// board's second subject (BV1.R14 of goal 1000069, ADR 0175 §7).
|
|
3
|
+
//
|
|
4
|
+
// DB-free (db monkeypatched; the apply runs against a REAL temp file so the
|
|
5
|
+
// atomic-write path is exercised, not stubbed). The invariants:
|
|
6
|
+
//
|
|
7
|
+
// 1. A PROPOSAL IS VALIDATED STRICTLY, NEVER SANITIZED — a config file falls
|
|
8
|
+
// back field-by-field so a boot never breaks; a proposal that fell back
|
|
9
|
+
// would put text before the board its proposer never wrote. Every field
|
|
10
|
+
// required, every refusal named.
|
|
11
|
+
// 2. THE MONARCHY ACT WORKS END TO END — the sole Archon proposes, votes on
|
|
12
|
+
// their OWN amendment (anti-self is a full_idea rule; §7 blesses this and
|
|
13
|
+
// forbids a bootstrap exception), first_ratifier closes it, and the
|
|
14
|
+
// instance pack now carries the new constitution.
|
|
15
|
+
// 3. ANTI-SELF STILL BINDS FOR IDEAS — the carve-out is subject-typed, and
|
|
16
|
+
// the full_idea case is re-pinned right here beside it.
|
|
17
|
+
// 4. A RETURNED AMENDMENT APPLIES NOTHING; a passed one books NO credit
|
|
18
|
+
// (the R12 contract's subject_type filter) and applies AFTER the close.
|
|
19
|
+
import assert from 'node:assert/strict';
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import os from 'node:os';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
30
|
+
const board = require(path.join(GOV, 'board.js'));
|
|
31
|
+
const db = require(path.join(GOV, 'db.js'));
|
|
32
|
+
const config = require(path.join(GOV, 'config.js'));
|
|
33
|
+
|
|
34
|
+
const { validateProposedConstitution, proposeAmendment, castVote, maybeCloseAfterVote } = board;
|
|
35
|
+
|
|
36
|
+
const CONSENT_90 = Object.freeze({
|
|
37
|
+
membership: 'rank:archon', pass_rule: 'consent', window_minutes: 90, close_early_on_full_turnout: true,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
async function withDb(stubs, fn) {
|
|
41
|
+
const originals = {};
|
|
42
|
+
for (const [k, v] of Object.entries(stubs)) { originals[k] = db[k]; db[k] = v; }
|
|
43
|
+
try { return await fn(); }
|
|
44
|
+
finally { for (const [k, v] of Object.entries(originals)) db[k] = v; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── invariant 1: strict validation ──────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
test('every field is required and every refusal is named — no silent correction', () => {
|
|
50
|
+
assert.equal(validateProposedConstitution(CONSENT_90).ok, true);
|
|
51
|
+
assert.equal(validateProposedConstitution({ ...CONSENT_90, window_minutes: null }).ok, true, 'null clock is a legal explicit choice');
|
|
52
|
+
|
|
53
|
+
const cases = [
|
|
54
|
+
[null, 'proposal_not_an_object'],
|
|
55
|
+
['consent', 'proposal_not_an_object'],
|
|
56
|
+
[{ ...CONSENT_90, extra: true }, 'unknown_field'],
|
|
57
|
+
[{ ...CONSENT_90, membership: 'everyone' }, 'bad_membership'],
|
|
58
|
+
[{ ...CONSENT_90, membership: undefined }, 'bad_membership'],
|
|
59
|
+
[{ ...CONSENT_90, pass_rule: 'majority' }, 'bad_pass_rule'],
|
|
60
|
+
[{ ...CONSENT_90, window_minutes: 0 }, 'bad_window_minutes'],
|
|
61
|
+
[{ ...CONSENT_90, window_minutes: 1.5 }, 'bad_window_minutes'],
|
|
62
|
+
[{ ...CONSENT_90, close_early_on_full_turnout: 'yes' }, 'bad_close_early'],
|
|
63
|
+
];
|
|
64
|
+
for (const [raw, reason] of cases) {
|
|
65
|
+
const v = validateProposedConstitution(raw);
|
|
66
|
+
assert.equal(v.ok, false, `${JSON.stringify(raw)} must be refused`);
|
|
67
|
+
assert.equal(v.reason, reason);
|
|
68
|
+
}
|
|
69
|
+
// THE CONTRAST WITH THE SANITIZER, stated as an assertion: the sanitizer
|
|
70
|
+
// silently corrects a bad pass_rule; the proposal validator refuses it.
|
|
71
|
+
assert.equal(config.sanitizeBoard({ ...CONSENT_90, pass_rule: 'majority' }).pass_rule, 'first_ratifier');
|
|
72
|
+
assert.equal(validateProposedConstitution({ ...CONSENT_90, pass_rule: 'majority' }).ok, false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ── propose: files the row and opens the window in the same breath ──────────
|
|
76
|
+
|
|
77
|
+
test('a proposal files the amendment and opens its window under the constitution IN FORCE', async () => {
|
|
78
|
+
let opened = null;
|
|
79
|
+
await withDb({
|
|
80
|
+
createAmendment: async (args) => ({ id: '5', proposed: args.proposed, rationale_md: args.rationaleMd, proposed_by: args.proposedBy }),
|
|
81
|
+
openBoardItem: async (args) => { opened = args; return { id: '21', ...args }; },
|
|
82
|
+
}, async () => {
|
|
83
|
+
const out = await proposeAmendment({ proposal: CONSENT_90, rationaleMd: 'time to open up', proposedBy: '42' });
|
|
84
|
+
assert.equal(out.ok, true);
|
|
85
|
+
assert.equal(out.amendment.id, '5');
|
|
86
|
+
});
|
|
87
|
+
assert.equal(opened.subjectType, 'constitutional_amendment');
|
|
88
|
+
assert.equal(opened.subjectId, '5');
|
|
89
|
+
assert.equal(opened.openedBy, '42', 'an amendment HAS a human opener, unlike a grade-opened idea');
|
|
90
|
+
assert.equal(opened.constitution.pass_rule, 'first_ratifier',
|
|
91
|
+
'the window runs under the constitution IN FORCE (this checkout: monarchy), not the proposed one');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('an invalid proposal files NOTHING — refused before any write', async () => {
|
|
95
|
+
await withDb({
|
|
96
|
+
createAmendment: async () => { throw new Error('must not be reached'); },
|
|
97
|
+
openBoardItem: async () => { throw new Error('must not be reached'); },
|
|
98
|
+
}, async () => {
|
|
99
|
+
assert.equal((await proposeAmendment({ proposal: { ...CONSENT_90, pass_rule: 'majority' }, proposedBy: '42' })).ok, false);
|
|
100
|
+
assert.equal((await proposeAmendment({ proposal: CONSENT_90, proposedBy: null })).reason, 'missing_proposer');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ── invariants 2+3: the monarchy act, and the subject-typed anti-self ────────
|
|
105
|
+
|
|
106
|
+
const MONARCHY = Object.freeze({ membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true });
|
|
107
|
+
const amendmentItem = (over = {}) => ({
|
|
108
|
+
id: '21', subject_type: 'constitutional_amendment', subject_id: '5',
|
|
109
|
+
opened_by: '42', closed_at: null, expired: false, constitution: MONARCHY, ...over,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('the monarchy act end to end: the sole Archon ratifies their OWN amendment and the pack changes', async () => {
|
|
113
|
+
const tmp = path.join(os.tmpdir(), `gov-instance-${process.pid}-${Math.floor(performance.now() * 1000)}.json`);
|
|
114
|
+
// Redirect the instance-pack write to a temp file — board.js resolves
|
|
115
|
+
// applyBoardAmendment by property lookup at call time for exactly this.
|
|
116
|
+
const origApply = config.applyBoardAmendment;
|
|
117
|
+
config.applyBoardAmendment = (proposed) => origApply(proposed, { instancePath: tmp });
|
|
118
|
+
try {
|
|
119
|
+
const karma = [];
|
|
120
|
+
let votes = [];
|
|
121
|
+
let authorAsked = 0;
|
|
122
|
+
await withDb({
|
|
123
|
+
getBoardItemForVote: async () => amendmentItem(),
|
|
124
|
+
builderHoldsRankActive: async (builderId, rankKey) => builderId === '42' && rankKey === 'archon',
|
|
125
|
+
insertBoardVote: async (args) => { const v = { id: '1', voter_id: args.voterId, direction: args.direction }; votes.push(v); return v; },
|
|
126
|
+
insertVoteKarma: async () => {},
|
|
127
|
+
getVotesForItem: async () => votes,
|
|
128
|
+
getActiveBuildersWithRank: async () => [{ builder_id: '42', github_login: 'sole-archon', display_name: 'Sole Archon' }],
|
|
129
|
+
closeBoardItem: async ({ outcome, afterClose }) => {
|
|
130
|
+
const closed = { ...amendmentItem(), closed_at: 'T', outcome };
|
|
131
|
+
assert.equal(afterClose, null, 'an amendment books NO credit — the R12 contract filter');
|
|
132
|
+
return closed;
|
|
133
|
+
},
|
|
134
|
+
getAmendment: async (id) => { assert.equal(id, '5'); return { id: '5', proposed: CONSENT_90 }; },
|
|
135
|
+
insertAuthorRatifyKarma: async (args) => { karma.push(args); return true; },
|
|
136
|
+
// reached ONLY by the post-close ratify karma, never by the vote's
|
|
137
|
+
// anti-self (the carve-out) — the counter proves the order.
|
|
138
|
+
getBoardItemAuthor: async () => { authorAsked += 1; return '42'; },
|
|
139
|
+
}, async () => {
|
|
140
|
+
// The PROPOSER casts on their own amendment — legal (§7) — and it closes
|
|
141
|
+
// immediately under first_ratifier.
|
|
142
|
+
const cast = await castVote({ itemId: 21, voterId: '42', direction: 'yes' });
|
|
143
|
+
assert.equal(cast.ok, true, `the sole Archon's vote on their own amendment must land: ${JSON.stringify(cast)}`);
|
|
144
|
+
assert.equal(authorAsked, 0, 'anti-self was not even consulted for the amendment vote');
|
|
145
|
+
const closed = await maybeCloseAfterVote(21);
|
|
146
|
+
assert.equal(closed.closed, true);
|
|
147
|
+
assert.equal(closed.outcome, 'passed');
|
|
148
|
+
assert.ok(closed.applied_constitution, 'the amendment was applied on close');
|
|
149
|
+
assert.equal(closed.applied_constitution.pass_rule, 'consent', 'the project now runs consent');
|
|
150
|
+
assert.equal(closed.applied_constitution.window_minutes, 90);
|
|
151
|
+
assert.ok(authorAsked >= 1, 'the ratify karma DID resolve the author, after the close');
|
|
152
|
+
});
|
|
153
|
+
assert.equal(karma.length, 1, 'the proposer earned the ratification karma (+3, same act as an idea ratify)');
|
|
154
|
+
const onDisk = JSON.parse(fs.readFileSync(tmp, 'utf8'));
|
|
155
|
+
assert.equal(onDisk.board.pass_rule, 'consent', 'the TEMP instance pack carries the new constitution');
|
|
156
|
+
} finally {
|
|
157
|
+
config.applyBoardAmendment = origApply;
|
|
158
|
+
try { fs.unlinkSync(tmp); } catch (_) { /* not created */ }
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('anti-self STILL binds for a full_idea — the carve-out is subject-typed', async () => {
|
|
163
|
+
await withDb({
|
|
164
|
+
getBoardItemForVote: async () => amendmentItem({ subject_type: 'full_idea', subject_id: '7' }),
|
|
165
|
+
builderHoldsRankActive: async () => true,
|
|
166
|
+
getBoardItemAuthor: async () => '42',
|
|
167
|
+
insertBoardVote: async () => { throw new Error('must not be reached'); },
|
|
168
|
+
}, async () => {
|
|
169
|
+
assert.deepEqual(await castVote({ itemId: 21, voterId: '42', direction: 'yes' }),
|
|
170
|
+
{ ok: false, reason: 'anti_self_vote' });
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('a RETURNED amendment applies nothing', async () => {
|
|
175
|
+
await withDb({
|
|
176
|
+
getVotesForItem: async () => [{ voter_id: '9', direction: 'no', reason_md: 'r', section_key: 'fit' }],
|
|
177
|
+
closeBoardItem: async ({ outcome, afterClose }) => {
|
|
178
|
+
assert.equal(afterClose, null, 'no credit hook for an amendment either way');
|
|
179
|
+
return { ...amendmentItem(), closed_at: 'T', outcome };
|
|
180
|
+
},
|
|
181
|
+
getAmendment: async () => { throw new Error('must not be reached — a return applies nothing'); },
|
|
182
|
+
getBoardItemAuthor: async () => { throw new Error('must not be reached'); },
|
|
183
|
+
}, async () => {
|
|
184
|
+
const out = await board.closeItem(amendmentItem());
|
|
185
|
+
assert.equal(out.outcome, 'returned');
|
|
186
|
+
assert.equal(out.applied_constitution, undefined);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ── the apply itself, against a real file ─────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
test('applyBoardAmendment writes the instance pack atomically and preserves its other keys', () => {
|
|
193
|
+
const tmp = path.join(os.tmpdir(), `gov-apply-${process.pid}-${Math.floor(performance.now() * 1000)}.json`);
|
|
194
|
+
try {
|
|
195
|
+
fs.writeFileSync(tmp, JSON.stringify({ ranks: { metic: { label: 'Trusted' } } }, null, 2));
|
|
196
|
+
const applied = config.applyBoardAmendment(CONSENT_90, { instancePath: tmp });
|
|
197
|
+
assert.equal(applied.pass_rule, 'consent');
|
|
198
|
+
const onDisk = JSON.parse(fs.readFileSync(tmp, 'utf8'));
|
|
199
|
+
assert.equal(onDisk.ranks.metic.label, 'Trusted', 'the pack\'s OTHER keys survive the apply');
|
|
200
|
+
assert.deepEqual(onDisk.board, { ...CONSENT_90 });
|
|
201
|
+
// and the resolver now serves it — one resolution path, R05's precedence intact
|
|
202
|
+
const resolved = config.resolveGovernmentConfig({ neutral: {}, instance: onDisk, env: {} });
|
|
203
|
+
assert.equal(resolved.board.pass_rule, 'consent');
|
|
204
|
+
// a missing instance pack is created whole
|
|
205
|
+
fs.unlinkSync(tmp);
|
|
206
|
+
config.applyBoardAmendment(MONARCHY, { instancePath: tmp });
|
|
207
|
+
assert.equal(JSON.parse(fs.readFileSync(tmp, 'utf8')).board.pass_rule, 'first_ratifier');
|
|
208
|
+
} finally {
|
|
209
|
+
try { fs.unlinkSync(tmp); } catch (_) { /* gone */ }
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('the route pin names board.item.open, and the catalog predicted this exact route', () => {
|
|
214
|
+
const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
|
|
215
|
+
assert.equal(rrc.MODULE_EXPECTED_RANKS.government['POST /government/board/items'],
|
|
216
|
+
`${rrc.PERM_PIN_PREFIX}board.item.open`);
|
|
217
|
+
const catalog = fs.readFileSync(path.join(GOV, 'catalog.js'), 'utf8');
|
|
218
|
+
assert.ok(catalog.includes('in practice this guards an amendment'),
|
|
219
|
+
'the R04 catalog guards note called it: the only proposed subject is an amendment');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
test('this checkout stays vanilla: no test may leave a real config/government.json behind', () => {
|
|
224
|
+
assert.equal(fs.existsSync(path.join(ROOT, 'config', 'government.json')), false,
|
|
225
|
+
'the instance pack is host-owned and ABSENT here — a test that writes it pollutes the vanilla-boot fixtures (government_config.mjs guards the same fact)');
|
|
226
|
+
});
|
|
@@ -39,6 +39,7 @@ const NON_GRANT_MIGRATIONS = new Set([
|
|
|
39
39
|
'government_003_board_tables.sql', // ADR 0175 Board Room schema (task 1003016)
|
|
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
|
+
'government_007_amendments.sql', // R14 amendment proposals table (task 1003027)
|
|
42
43
|
]);
|
|
43
44
|
|
|
44
45
|
// Permission keys RENAMED after the grant migrations that first granted them.
|