@bongos/core 1.21.37 → 1.21.38
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 +44 -29
- 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 +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +77 -3
- package/docs/api-reference.md +3 -2
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +1 -1
- package/modules/government/board-withdraw.js +38 -0
- package/modules/government/board.js +1 -0
- package/modules/government/db.js +20 -0
- package/modules/government/migrations/government_026_board_outcome_withdrawn.sql +29 -0
- package/modules/government/routes/government.js +26 -0
- package/modules/hall-ui/public/street-card.js +16 -0
- package/modules/hall-ui/public/street-desk.js +2 -0
- package/modules/hall-ui/public/street.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/bongos/route-rank-check.js +4 -0
- package/src/module-api.js +1 -1
- package/tests/government_board_withdraw.mjs +144 -0
- package/tests/government_seed.mjs +1 -0
- package/tests/hall_street_desk.mjs +37 -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.21.
|
|
6
|
-
"core_contract": "1.21.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.21.38",
|
|
6
|
+
"core_contract": "1.21.38",
|
|
7
|
+
"source_commit": "8730dd7e93404c96083883df1023ab46f2d1704a",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-04T14:
|
|
9
|
+
"built_at": "2026-10-04T14:35:07.717Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 582,
|
|
13
13
|
"agent_docs_stubbed": 27,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2915,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3525,
|
|
20
|
+
"tree_sha256": "afabc41a468a36d85e3340f622556c6b3a28d1411f23b61e824a288936a6003b",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -317,12 +317,12 @@
|
|
|
317
317
|
{
|
|
318
318
|
"path": "clients/bongos-client/README.md",
|
|
319
319
|
"mode": "0000644",
|
|
320
|
-
"sha256": "
|
|
320
|
+
"sha256": "428e6d43f0f3e60c0777a0c937b2f0d96b957a66353d6bcb66c33d523f092252"
|
|
321
321
|
},
|
|
322
322
|
{
|
|
323
323
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
324
324
|
"mode": "0000644",
|
|
325
|
-
"sha256": "
|
|
325
|
+
"sha256": "3d883a2c48e51f1d4a3dfc25a0997ebafa068a03844102643fa67108c1b447f1"
|
|
326
326
|
},
|
|
327
327
|
{
|
|
328
328
|
"path": "clients/bongos-client/examples/hello-world.mjs",
|
|
@@ -332,17 +332,17 @@
|
|
|
332
332
|
{
|
|
333
333
|
"path": "clients/bongos-client/index.cjs",
|
|
334
334
|
"mode": "0000644",
|
|
335
|
-
"sha256": "
|
|
335
|
+
"sha256": "05964ae1ba8451e4b25810f568c3d5e8c87acb6dba93a3cb3949992c124e4460"
|
|
336
336
|
},
|
|
337
337
|
{
|
|
338
338
|
"path": "clients/bongos-client/index.d.ts",
|
|
339
339
|
"mode": "0000644",
|
|
340
|
-
"sha256": "
|
|
340
|
+
"sha256": "d5c51fd8aa78ec05ff4cdd22318db54c2940527c00a16c6c4f8ec9be5e41d207"
|
|
341
341
|
},
|
|
342
342
|
{
|
|
343
343
|
"path": "clients/bongos-client/index.mjs",
|
|
344
344
|
"mode": "0000644",
|
|
345
|
-
"sha256": "
|
|
345
|
+
"sha256": "3d3b0fed9ae284c918ca5d7fd54dc9892a1829f0dd250979994cd8f2c3d0da8b"
|
|
346
346
|
},
|
|
347
347
|
{
|
|
348
348
|
"path": "clients/bongos-client/package.json",
|
|
@@ -2292,12 +2292,12 @@
|
|
|
2292
2292
|
{
|
|
2293
2293
|
"path": "docs/api-reference.md",
|
|
2294
2294
|
"mode": "0000644",
|
|
2295
|
-
"sha256": "
|
|
2295
|
+
"sha256": "006e9f56a98916643a873612e1e34818e6bf8376654556bb9ed43cd2469b6c94"
|
|
2296
2296
|
},
|
|
2297
2297
|
{
|
|
2298
2298
|
"path": "docs/api/openapi.json",
|
|
2299
2299
|
"mode": "0000644",
|
|
2300
|
-
"sha256": "
|
|
2300
|
+
"sha256": "716ad5a04338ac72a92687ec62ada3c12cc450cbfd269c4bac96ef0f81c69048"
|
|
2301
2301
|
},
|
|
2302
2302
|
{
|
|
2303
2303
|
"path": "docs/architecture.md",
|
|
@@ -2837,7 +2837,7 @@
|
|
|
2837
2837
|
{
|
|
2838
2838
|
"path": "docs/module-api-changelog.md",
|
|
2839
2839
|
"mode": "0000644",
|
|
2840
|
-
"sha256": "
|
|
2840
|
+
"sha256": "7f6ea1708114d19a350bd0c04c0310f99cc98068b91a3a6e38b37159da09ef86"
|
|
2841
2841
|
},
|
|
2842
2842
|
{
|
|
2843
2843
|
"path": "docs/modules-contract.md",
|
|
@@ -2927,7 +2927,7 @@
|
|
|
2927
2927
|
{
|
|
2928
2928
|
"path": "docs/page-readings.json",
|
|
2929
2929
|
"mode": "0000644",
|
|
2930
|
-
"sha256": "
|
|
2930
|
+
"sha256": "893ee305237a03f5d7463691932eabc9f7c16df2f3f40b17046063eafc40b771"
|
|
2931
2931
|
},
|
|
2932
2932
|
{
|
|
2933
2933
|
"path": "docs/project-context.template.md",
|
|
@@ -4854,10 +4854,15 @@
|
|
|
4854
4854
|
"mode": "0000644",
|
|
4855
4855
|
"sha256": "26f212bfb64239e98418618ec3304664180b06f3740b8c4f682f29d3bbf2f3e9"
|
|
4856
4856
|
},
|
|
4857
|
+
{
|
|
4858
|
+
"path": "modules/government/board-withdraw.js",
|
|
4859
|
+
"mode": "0000644",
|
|
4860
|
+
"sha256": "debc9d9e01c2a698aa5039f4368c38d1ae43302b9fa345f63d8a5b05ff22536a"
|
|
4861
|
+
},
|
|
4857
4862
|
{
|
|
4858
4863
|
"path": "modules/government/board.js",
|
|
4859
4864
|
"mode": "0000644",
|
|
4860
|
-
"sha256": "
|
|
4865
|
+
"sha256": "d95e53c9aec8dc4614aec18da37644da44256d0977195ae32ad5951a1d50ba09"
|
|
4861
4866
|
},
|
|
4862
4867
|
{
|
|
4863
4868
|
"path": "modules/government/catalog.js",
|
|
@@ -4877,7 +4882,7 @@
|
|
|
4877
4882
|
{
|
|
4878
4883
|
"path": "modules/government/db.js",
|
|
4879
4884
|
"mode": "0000644",
|
|
4880
|
-
"sha256": "
|
|
4885
|
+
"sha256": "bc95bf1e21ee2c2dc5444a8fdd056893a2883fd8e55e01c7c765e2d69bee2c7b"
|
|
4881
4886
|
},
|
|
4882
4887
|
{
|
|
4883
4888
|
"path": "modules/government/decision-board.js",
|
|
@@ -5069,6 +5074,11 @@
|
|
|
5069
5074
|
"mode": "0000644",
|
|
5070
5075
|
"sha256": "5bcdd9defa3927adda5612d990d25dc8efaea8f2a0c159b5bd1b4154364512df"
|
|
5071
5076
|
},
|
|
5077
|
+
{
|
|
5078
|
+
"path": "modules/government/migrations/government_026_board_outcome_withdrawn.sql",
|
|
5079
|
+
"mode": "0000644",
|
|
5080
|
+
"sha256": "24296bac7d187f9f4c9290238c5ba8fa253e906839dac9ec46598f578adad4be"
|
|
5081
|
+
},
|
|
5072
5082
|
{
|
|
5073
5083
|
"path": "modules/government/module.json",
|
|
5074
5084
|
"mode": "0000644",
|
|
@@ -5107,7 +5117,7 @@
|
|
|
5107
5117
|
{
|
|
5108
5118
|
"path": "modules/government/routes/government.js",
|
|
5109
5119
|
"mode": "0000644",
|
|
5110
|
-
"sha256": "
|
|
5120
|
+
"sha256": "6bc11f4cff85844a8060d3ec688d3d608165d741f7da055d63fe8bf59c65a519"
|
|
5111
5121
|
},
|
|
5112
5122
|
{
|
|
5113
5123
|
"path": "modules/government/routes/rank-sync.js",
|
|
@@ -6192,7 +6202,7 @@
|
|
|
6192
6202
|
{
|
|
6193
6203
|
"path": "modules/hall-ui/public/street-card.js",
|
|
6194
6204
|
"mode": "0000644",
|
|
6195
|
-
"sha256": "
|
|
6205
|
+
"sha256": "ce16bc30bfe106e13a25592f01d75dc650f05482c40747646e8c73a1f35f8ddf"
|
|
6196
6206
|
},
|
|
6197
6207
|
{
|
|
6198
6208
|
"path": "modules/hall-ui/public/street-card.states.json",
|
|
@@ -6202,7 +6212,7 @@
|
|
|
6202
6212
|
{
|
|
6203
6213
|
"path": "modules/hall-ui/public/street-desk.js",
|
|
6204
6214
|
"mode": "0000644",
|
|
6205
|
-
"sha256": "
|
|
6215
|
+
"sha256": "d4b7f5a792afeb639134efeb36a26ff4a246d98cde44170e48d44e55403db664"
|
|
6206
6216
|
},
|
|
6207
6217
|
{
|
|
6208
6218
|
"path": "modules/hall-ui/public/street-draw.js",
|
|
@@ -6227,7 +6237,7 @@
|
|
|
6227
6237
|
{
|
|
6228
6238
|
"path": "modules/hall-ui/public/street.js",
|
|
6229
6239
|
"mode": "0000644",
|
|
6230
|
-
"sha256": "
|
|
6240
|
+
"sha256": "6131ed683e6542c3d7881c419bc96ab6e73dd1144716fd5a40762f345935e7cd"
|
|
6231
6241
|
},
|
|
6232
6242
|
{
|
|
6233
6243
|
"path": "modules/hall-ui/public/street.states.json",
|
|
@@ -9832,12 +9842,12 @@
|
|
|
9832
9842
|
{
|
|
9833
9843
|
"path": "package-lock.json",
|
|
9834
9844
|
"mode": "0000644",
|
|
9835
|
-
"sha256": "
|
|
9845
|
+
"sha256": "a7eac01934af2430cd0466fb7c77f6287c26b974746cbcf46320dc035721a8c6"
|
|
9836
9846
|
},
|
|
9837
9847
|
{
|
|
9838
9848
|
"path": "package.json",
|
|
9839
9849
|
"mode": "0000644",
|
|
9840
|
-
"sha256": "
|
|
9850
|
+
"sha256": "a167dfdd2b1dc9a0547f265936623c66b4e6ee8bce4e90d3094c4f7a3fb16d6a"
|
|
9841
9851
|
},
|
|
9842
9852
|
{
|
|
9843
9853
|
"path": "public-docs/index.html",
|
|
@@ -9857,7 +9867,7 @@
|
|
|
9857
9867
|
{
|
|
9858
9868
|
"path": "release-notes.json",
|
|
9859
9869
|
"mode": "0000644",
|
|
9860
|
-
"sha256": "
|
|
9870
|
+
"sha256": "6c63a14037b4bd1340966aceed25b84d34063bbc6bcd0bbc760bd18f7266e8f0"
|
|
9861
9871
|
},
|
|
9862
9872
|
{
|
|
9863
9873
|
"path": "scripts/bongos-mcp.js",
|
|
@@ -11922,7 +11932,7 @@
|
|
|
11922
11932
|
{
|
|
11923
11933
|
"path": "src/bongos/route-rank-check.js",
|
|
11924
11934
|
"mode": "0000644",
|
|
11925
|
-
"sha256": "
|
|
11935
|
+
"sha256": "bfe24c17a7f63f0d0dc59f3d50b298e0f09b86eabf79e74140b56b67f157a684"
|
|
11926
11936
|
},
|
|
11927
11937
|
{
|
|
11928
11938
|
"path": "src/bongos/routes.js",
|
|
@@ -12082,7 +12092,7 @@
|
|
|
12082
12092
|
{
|
|
12083
12093
|
"path": "src/module-api.js",
|
|
12084
12094
|
"mode": "0000644",
|
|
12085
|
-
"sha256": "
|
|
12095
|
+
"sha256": "79ba1f20ba3e6e5e23f5bde0ee3b82e3aaeb96d14b2279f2c77690c3ddb6bf89"
|
|
12086
12096
|
},
|
|
12087
12097
|
{
|
|
12088
12098
|
"path": "src/module-loader/catalog.js",
|
|
@@ -13994,6 +14004,11 @@
|
|
|
13994
14004
|
"mode": "0000644",
|
|
13995
14005
|
"sha256": "bb78d4fe2610e08a2d538d4e4a64cd3b7198da12b09da1313a58ed32a96dab5a"
|
|
13996
14006
|
},
|
|
14007
|
+
{
|
|
14008
|
+
"path": "tests/government_board_withdraw.mjs",
|
|
14009
|
+
"mode": "0000644",
|
|
14010
|
+
"sha256": "68a9035a76d72fe87ad5bbbf79514a93e06b5779a07059456b1ff338b53027a1"
|
|
14011
|
+
},
|
|
13997
14012
|
{
|
|
13998
14013
|
"path": "tests/government_catalog.mjs",
|
|
13999
14014
|
"mode": "0000644",
|
|
@@ -14112,7 +14127,7 @@
|
|
|
14112
14127
|
{
|
|
14113
14128
|
"path": "tests/government_seed.mjs",
|
|
14114
14129
|
"mode": "0000644",
|
|
14115
|
-
"sha256": "
|
|
14130
|
+
"sha256": "1024c3bf989d82790103a78e5866b43ad95f71310c1da6a292cffe9320cbd567"
|
|
14116
14131
|
},
|
|
14117
14132
|
{
|
|
14118
14133
|
"path": "tests/government_session_scope.mjs",
|
|
@@ -14482,7 +14497,7 @@
|
|
|
14482
14497
|
{
|
|
14483
14498
|
"path": "tests/hall_street_desk.mjs",
|
|
14484
14499
|
"mode": "0000644",
|
|
14485
|
-
"sha256": "
|
|
14500
|
+
"sha256": "d2b3177198022b06fac1b22a2e7b0db93770567cf80ed03b5e7fe3f0715a1f38"
|
|
14486
14501
|
},
|
|
14487
14502
|
{
|
|
14488
14503
|
"path": "tests/hall_street_inside.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
|
+
- 503 operations across 73 resource groups
|
|
9
9
|
|
|
10
10
|
## Use it from your project
|
|
11
11
|
|
|
@@ -513,6 +513,8 @@ function createClient(opts = {}) {
|
|
|
513
513
|
postGovernmentBoardItems: (args) => request("POST", "/government/board/items", { hasBody: true }, args),
|
|
514
514
|
// POST /government/board/items/{itemId}/votes — rank: any-builder — POST /government/board/items/:itemId/votes
|
|
515
515
|
postGovernmentBoardItemsItemIdVotes: (args) => request("POST", "/government/board/items/{itemId}/votes", { hasBody: true }, args),
|
|
516
|
+
// POST /government/board/items/{itemId}/withdraw — rank: metic+archon — POST /government/board/items/:itemId/withdraw
|
|
517
|
+
postGovernmentBoardItemsItemIdWithdraw: (args) => request("POST", "/government/board/items/{itemId}/withdraw", { hasBody: true }, args),
|
|
516
518
|
// POST /government/board/matters — rank: metic+archon — POST /government/board/matters
|
|
517
519
|
postGovernmentBoardMatters: (args) => request("POST", "/government/board/matters", { hasBody: true }, args),
|
|
518
520
|
// GET /government/constitution — rank: any-builder — GET /government/constitution
|
|
@@ -512,6 +512,8 @@ function createClient(opts = {}) {
|
|
|
512
512
|
postGovernmentBoardItems: (args) => request("POST", "/government/board/items", { hasBody: true }, args),
|
|
513
513
|
// POST /government/board/items/{itemId}/votes — rank: any-builder — POST /government/board/items/:itemId/votes
|
|
514
514
|
postGovernmentBoardItemsItemIdVotes: (args) => request("POST", "/government/board/items/{itemId}/votes", { hasBody: true }, args),
|
|
515
|
+
// POST /government/board/items/{itemId}/withdraw — rank: metic+archon — POST /government/board/items/:itemId/withdraw
|
|
516
|
+
postGovernmentBoardItemsItemIdWithdraw: (args) => request("POST", "/government/board/items/{itemId}/withdraw", { hasBody: true }, args),
|
|
515
517
|
// POST /government/board/matters — rank: metic+archon — POST /government/board/matters
|
|
516
518
|
postGovernmentBoardMatters: (args) => request("POST", "/government/board/matters", { hasBody: true }, args),
|
|
517
519
|
// GET /government/constitution — rank: any-builder — GET /government/constitution
|
|
@@ -350,6 +350,7 @@ export interface PostGovernmentBoardCharterResponse { ok: boolean; amendment: un
|
|
|
350
350
|
export interface PostGovernmentBoardDecisionRulesRequest { rules: unknown[]; rationale_md?: string }
|
|
351
351
|
export interface PostGovernmentBoardDecisionRulesResponse { ok: boolean; amendment: unknown; item: unknown; rules: unknown }
|
|
352
352
|
export interface PostGovernmentBoardItemsItemIdVotesRequest { direction: string; reason_md?: string; section_key?: string }
|
|
353
|
+
export interface PostGovernmentBoardItemsItemIdWithdrawResponse { ok: boolean; item_closed: boolean; outcome: unknown; item: unknown }
|
|
353
354
|
export interface PostGovernmentBoardItemsRequest { subject_type: string; proposal: Record<string, unknown>; rationale_md?: string; acknowledge_self_removal?: boolean }
|
|
354
355
|
export interface PostGovernmentBoardItemsResponse { ok: boolean; amendment: unknown; item: unknown }
|
|
355
356
|
export interface PostGovernmentBoardMattersRequest { area: string; title: string; detail_md?: string; action?: Record<string, unknown> }
|
|
@@ -982,6 +983,8 @@ export interface BongosClient {
|
|
|
982
983
|
postGovernmentBoardItems(args: RequestArgs & { body: PostGovernmentBoardItemsRequest }): Promise<PostGovernmentBoardItemsResponse>;
|
|
983
984
|
/** POST /government/board/items/{itemId}/votes — rank: any-builder */
|
|
984
985
|
postGovernmentBoardItemsItemIdVotes(args: RequestArgs & { body: PostGovernmentBoardItemsItemIdVotesRequest }): Promise<ApiResponse>;
|
|
986
|
+
/** POST /government/board/items/{itemId}/withdraw — rank: metic+archon */
|
|
987
|
+
postGovernmentBoardItemsItemIdWithdraw(args?: RequestArgs): Promise<PostGovernmentBoardItemsItemIdWithdrawResponse>;
|
|
985
988
|
/** POST /government/board/matters — rank: metic+archon */
|
|
986
989
|
postGovernmentBoardMatters(args: RequestArgs & { body: PostGovernmentBoardMattersRequest }): Promise<PostGovernmentBoardMattersResponse>;
|
|
987
990
|
/** GET /government/constitution — rank: any-builder */
|
|
@@ -509,6 +509,8 @@ export function createClient(opts = {}) {
|
|
|
509
509
|
postGovernmentBoardItems: (args) => request("POST", "/government/board/items", { hasBody: true }, args),
|
|
510
510
|
// POST /government/board/items/{itemId}/votes — rank: any-builder — POST /government/board/items/:itemId/votes
|
|
511
511
|
postGovernmentBoardItemsItemIdVotes: (args) => request("POST", "/government/board/items/{itemId}/votes", { hasBody: true }, args),
|
|
512
|
+
// POST /government/board/items/{itemId}/withdraw — rank: metic+archon — POST /government/board/items/:itemId/withdraw
|
|
513
|
+
postGovernmentBoardItemsItemIdWithdraw: (args) => request("POST", "/government/board/items/{itemId}/withdraw", { hasBody: true }, args),
|
|
512
514
|
// POST /government/board/matters — rank: metic+archon — POST /government/board/matters
|
|
513
515
|
postGovernmentBoardMatters: (args) => request("POST", "/government/board/matters", { hasBody: true }, args),
|
|
514
516
|
// GET /government/constitution — rank: any-builder — GET /government/constitution
|
package/docs/api/openapi.json
CHANGED
|
@@ -8846,6 +8846,61 @@
|
|
|
8846
8846
|
]
|
|
8847
8847
|
}
|
|
8848
8848
|
},
|
|
8849
|
+
"/government/board/items/{itemId}/withdraw": {
|
|
8850
|
+
"post": {
|
|
8851
|
+
"operationId": "post_government_board_items_itemId_withdraw",
|
|
8852
|
+
"tags": [
|
|
8853
|
+
"government"
|
|
8854
|
+
],
|
|
8855
|
+
"summary": "POST /government/board/items/:itemId/withdraw",
|
|
8856
|
+
"description": "board.item.open — withdraw your own open sitting (task 1004564). The same atom as putting one up: taking it back is the proposer's half of the same act. The atom is only the coarse gate; the walls are in board-withdraw.js and repeated in its write — only the builder who opened the sitting, only while it is open and inside its clock, and only before anyone has voted. It closes with outcome 'withdrawn' (government_026).\n\n**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `board.item.open` (all required).",
|
|
8857
|
+
"x-rank": "metic+archon",
|
|
8858
|
+
"x-source": "modules/government/routes/government.js",
|
|
8859
|
+
"x-permissions": [
|
|
8860
|
+
"board.item.open"
|
|
8861
|
+
],
|
|
8862
|
+
"parameters": [
|
|
8863
|
+
{
|
|
8864
|
+
"name": "itemId",
|
|
8865
|
+
"in": "path",
|
|
8866
|
+
"required": true,
|
|
8867
|
+
"schema": {
|
|
8868
|
+
"type": "string"
|
|
8869
|
+
},
|
|
8870
|
+
"description": "Path parameter `itemId`."
|
|
8871
|
+
}
|
|
8872
|
+
],
|
|
8873
|
+
"responses": {
|
|
8874
|
+
"200": {
|
|
8875
|
+
"description": "Success.",
|
|
8876
|
+
"content": {
|
|
8877
|
+
"application/json": {
|
|
8878
|
+
"schema": {
|
|
8879
|
+
"$ref": "#/components/schemas/PostGovernmentBoardItemsItemIdWithdrawResponse"
|
|
8880
|
+
}
|
|
8881
|
+
}
|
|
8882
|
+
}
|
|
8883
|
+
},
|
|
8884
|
+
"400": {
|
|
8885
|
+
"$ref": "#/components/responses/BadRequest"
|
|
8886
|
+
},
|
|
8887
|
+
"401": {
|
|
8888
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
8889
|
+
},
|
|
8890
|
+
"403": {
|
|
8891
|
+
"$ref": "#/components/responses/Forbidden"
|
|
8892
|
+
},
|
|
8893
|
+
"404": {
|
|
8894
|
+
"$ref": "#/components/responses/NotFound"
|
|
8895
|
+
}
|
|
8896
|
+
},
|
|
8897
|
+
"security": [
|
|
8898
|
+
{
|
|
8899
|
+
"builderSession": []
|
|
8900
|
+
}
|
|
8901
|
+
]
|
|
8902
|
+
}
|
|
8903
|
+
},
|
|
8849
8904
|
"/government/board/matters": {
|
|
8850
8905
|
"post": {
|
|
8851
8906
|
"operationId": "post_government_board_matters",
|
|
@@ -28701,6 +28756,25 @@
|
|
|
28701
28756
|
],
|
|
28702
28757
|
"additionalProperties": false
|
|
28703
28758
|
},
|
|
28759
|
+
"PostGovernmentBoardItemsItemIdWithdrawResponse": {
|
|
28760
|
+
"type": "object",
|
|
28761
|
+
"properties": {
|
|
28762
|
+
"ok": {
|
|
28763
|
+
"type": "boolean"
|
|
28764
|
+
},
|
|
28765
|
+
"item_closed": {
|
|
28766
|
+
"type": "boolean"
|
|
28767
|
+
},
|
|
28768
|
+
"outcome": {},
|
|
28769
|
+
"item": {}
|
|
28770
|
+
},
|
|
28771
|
+
"required": [
|
|
28772
|
+
"ok",
|
|
28773
|
+
"item_closed",
|
|
28774
|
+
"outcome",
|
|
28775
|
+
"item"
|
|
28776
|
+
]
|
|
28777
|
+
},
|
|
28704
28778
|
"PostGovernmentBoardItemsRequest": {
|
|
28705
28779
|
"type": "object",
|
|
28706
28780
|
"properties": {
|
|
@@ -32160,9 +32234,9 @@
|
|
|
32160
32234
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
32161
32235
|
}
|
|
32162
32236
|
},
|
|
32163
|
-
"x-endpoint-count":
|
|
32164
|
-
"x-schema-count":
|
|
32237
|
+
"x-endpoint-count": 503,
|
|
32238
|
+
"x-schema-count": 537,
|
|
32165
32239
|
"x-undocumented-bodies": 12,
|
|
32166
|
-
"x-response-schemas":
|
|
32240
|
+
"x-response-schemas": 356,
|
|
32167
32241
|
"x-generated-by": "scripts/gds/gen-api-docs.js"
|
|
32168
32242
|
}
|
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. 503 endpoints across 92 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/0016-trust-boundary-server-enforced-permissions.md)): `public` < `any-builder` < `metic+archon` < `archon`.
|
|
@@ -340,7 +340,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
340
340
|
| GET | `/api/bongos/goals/rollups` | `any-builder` | — | GET /goals/rollups?version= — one small row per goal: readiness counters, the last-activity timestamp, the craft set, and the next claima… |
|
|
341
341
|
| GET | `/api/bongos/goals/suggestions` | `any-builder` | — | GET /goals/suggestions — "which goal should this draft live in?", answered before anything is created (task 1003062 / R07). |
|
|
342
342
|
|
|
343
|
-
## `government` (
|
|
343
|
+
## `government` (20)
|
|
344
344
|
|
|
345
345
|
| Method | Path | Rank | Body | Description |
|
|
346
346
|
|---|---|---|---|---|
|
|
@@ -353,6 +353,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
353
353
|
| 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). |
|
|
354
354
|
| POST | `/api/bongos/government/board/items` | `metic+archon` | `subject_type`, `proposal`, `rationale_md`, `acknowledge_self_removal` | board.item.open — put an AMENDMENT to the board (R14 of goal 1000069, ADR 0175 §7). |
|
|
355
355
|
| 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). |
|
|
356
|
+
| POST | `/api/bongos/government/board/items/:itemId/withdraw` | `metic+archon` | — | board.item.open — withdraw your own open sitting (task 1004564). |
|
|
356
357
|
| POST | `/api/bongos/government/board/matters` | `metic+archon` | `area`, `title`, `detail_md`, `action` | board.item.open — put a MATTER to a building's sitting (task 1004529): the board's new subjects, one per building other than the charter. |
|
|
357
358
|
| GET | `/api/bongos/government/constitution` | `any-builder` | — | board.vote.cast — the Constitution view (R18 of goal 1000069): the live board block plus the full amendment history, passed and returned … |
|
|
358
359
|
| GET | `/api/bongos/government/docket` | `any-builder` | — | board.vote.cast — the governor's DOCKET (task 1004523, goal 1000125): ONE read of every matter waiting on, or moving for, a governor, whi… |
|
|
@@ -2901,5 +2901,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2901
2901
|
landed since 1.21.35 with no explicit bump. run 37207566188. (task 1002620)
|
|
2902
2902
|
1.21.37 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2903
2903
|
landed since 1.21.36 with no explicit bump. run 37208699890. (task 1002620)
|
|
2904
|
+
1.21.38 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2905
|
+
landed since 1.21.37 with no explicit bump. run 37209756963. (task 1002620)
|
|
2904
2906
|
---------------------------------------------------------------------------
|
|
2905
2907
|
```
|
package/docs/page-readings.json
CHANGED
|
@@ -3123,7 +3123,7 @@
|
|
|
3123
3123
|
"surface": "builders",
|
|
3124
3124
|
"title": "The street",
|
|
3125
3125
|
"states_read": ["street","fullscreen","card","switched","focus","here","inside-rules","inside-disputes","inside-people","inside-planning","inside-automation","inside-review","inside-release","inside-treasury","floor","amend","district","desk","desk-facts","desk-quiet","desk-people-builders","desk-people-tier","desk-disputes","desk-automation-goals","desk-release-core","desk-rules","desk-planning-priorities","desk-planning-ideas","desk-planning-blockers","sitting","object","voted","admission","settled"],
|
|
3126
|
-
"files_hash": "
|
|
3126
|
+
"files_hash": "e607f61966682b5b",
|
|
3127
3127
|
"reading_hash": "cc91568585bbcbe4",
|
|
3128
3128
|
"counts": {"lines":376,"placed":73,"shared":9,"unplaced":294},
|
|
3129
3129
|
"omitted": {"data":421},
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/government/board-withdraw.js — a proposer withdraws their own open
|
|
4
|
+
// board sitting before anyone has voted (task 1004564). Its own file because
|
|
5
|
+
// board.js sits at the size ratchet's line cap; it reads and writes through the
|
|
6
|
+
// same db slice and announces the close through board.js's own close event, so
|
|
7
|
+
// the Discord mirror and every other listener see one shape for every close.
|
|
8
|
+
|
|
9
|
+
const db = require('./db');
|
|
10
|
+
const board = require('./board');
|
|
11
|
+
|
|
12
|
+
// The proposer may take back a sitting they opened, but only while nobody has
|
|
13
|
+
// voted on it: once a vote is cast the sitting belongs to the voters too, and
|
|
14
|
+
// pulling it would erase their say. "Proposer" is the person who OPENED it
|
|
15
|
+
// (opened_by) — a Full Idea's sitting opens itself on its grade, so it has no
|
|
16
|
+
// proposer here and cannot be withdrawn this way. An item past its clock is
|
|
17
|
+
// refused as well: the sweep is about to decide it (under consent, silence
|
|
18
|
+
// passes it), and a withdraw then would dodge that outcome.
|
|
19
|
+
//
|
|
20
|
+
// Each refusal is read first so the caller gets a reason; the write itself
|
|
21
|
+
// repeats every guard in its WHERE (db.withdrawBoardItem), so a vote that
|
|
22
|
+
// arrives between the reads and the write leaves the sitting open
|
|
23
|
+
// ('item_changed') rather than withdrawn over a cast vote.
|
|
24
|
+
async function withdrawItem({ itemId, builderId } = {}) {
|
|
25
|
+
const item = await db.getBoardItemForVote(Number(itemId));
|
|
26
|
+
if (!item) return { ok: false, reason: 'item_not_found' };
|
|
27
|
+
if (item.closed_at) return { ok: false, reason: 'item_closed' };
|
|
28
|
+
if (item.opened_by == null || String(item.opened_by) !== String(builderId)) return { ok: false, reason: 'not_proposer' };
|
|
29
|
+
if (item.expired) return { ok: false, reason: 'vote_window_expired' };
|
|
30
|
+
const votes = await db.getVotesForItem(item.id);
|
|
31
|
+
if (votes.length > 0) return { ok: false, reason: 'votes_cast' };
|
|
32
|
+
const closed = await db.withdrawBoardItem({ itemId: item.id, builderId });
|
|
33
|
+
if (!closed) return { ok: false, reason: 'item_changed' };
|
|
34
|
+
await board.emitItemClosed(closed, { cause: 'withdrawn', votes: [] });
|
|
35
|
+
return { ok: true, item: closed };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { withdrawItem };
|
package/modules/government/db.js
CHANGED
|
@@ -393,6 +393,25 @@ async function getVotesForItem(itemId) {
|
|
|
393
393
|
return rows;
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
+
// The withdraw write (task 1004564) — a proposer takes back their own open
|
|
397
|
+
// sitting before anyone has voted. EVERY guard is in the WHERE, not only in
|
|
398
|
+
// withdrawItem (board-withdraw.js)'s reads: still open, opened by this builder, and no vote
|
|
399
|
+
// row on it. So a vote that lands between the caller's read and this write
|
|
400
|
+
// makes it match nothing (null), and the sitting stays open — the same
|
|
401
|
+
// single-winner shape as closeBoardItem.
|
|
402
|
+
async function withdrawBoardItem({ itemId, builderId }) {
|
|
403
|
+
const { rows } = await pool.query(
|
|
404
|
+
`UPDATE government_board_items
|
|
405
|
+
SET closed_at = now(), outcome = 'withdrawn'
|
|
406
|
+
WHERE id = $1 AND closed_at IS NULL
|
|
407
|
+
AND opened_by = $2
|
|
408
|
+
AND NOT EXISTS (SELECT 1 FROM government_board_votes WHERE item_id = $1)
|
|
409
|
+
RETURNING *`,
|
|
410
|
+
[itemId, builderId],
|
|
411
|
+
);
|
|
412
|
+
return rows[0] || null;
|
|
413
|
+
}
|
|
414
|
+
|
|
396
415
|
// The close write — STRUCTURALLY SINGLE-WINNER: WHERE closed_at IS NULL means
|
|
397
416
|
// two concurrent closers (a racing vote and the expiry sweep) write exactly one
|
|
398
417
|
// outcome; the loser gets null and reads the winner's row back if it cares.
|
|
@@ -881,6 +900,7 @@ module.exports = {
|
|
|
881
900
|
insertAuthorRatifyKarma,
|
|
882
901
|
getVotesForItem,
|
|
883
902
|
closeBoardItem,
|
|
903
|
+
withdrawBoardItem,
|
|
884
904
|
getIdeaCompleteness,
|
|
885
905
|
getExpiredOpenBoardItems,
|
|
886
906
|
getLatestReturnedItemWithObjection,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- government_026_board_outcome_withdrawn.sql — a board sitting may also end
|
|
2
|
+
-- 'withdrawn' or 'expired' (task 1004564, goal 1000125).
|
|
3
|
+
--
|
|
4
|
+
-- WHAT IT IS. government_003 limited government_board_items.outcome to 'passed'
|
|
5
|
+
-- and 'returned'. A proposer may now withdraw their own open sitting before
|
|
6
|
+
-- anyone has voted (POST /government/board/items/:itemId/withdraw), which closes
|
|
7
|
+
-- it with outcome 'withdrawn'. 'expired' is added in the same statement because
|
|
8
|
+
-- the docket already names it (modules/government/docket-floors.js
|
|
9
|
+
-- OUTCOME_WORDS) — nothing writes it yet; allowing it now saves a second
|
|
10
|
+
-- constraint swap later.
|
|
11
|
+
--
|
|
12
|
+
-- Additive only: the CHECK is WIDENED. Every row the old CHECK allowed ('passed',
|
|
13
|
+
-- 'returned', NULL for an open item) is still allowed, so no row can fail it and
|
|
14
|
+
-- no data changes. The closed-consistency CHECK (closed_at and outcome set
|
|
15
|
+
-- together) is untouched.
|
|
16
|
+
-- Idempotent: DROP CONSTRAINT IF EXISTS + ADD, the government_017/020 idiom.
|
|
17
|
+
-- The constraint name is the one Postgres gave government_003's inline CHECK.
|
|
18
|
+
-- Approved by the owner on 2026-10-04 — the only schema change that task carries.
|
|
19
|
+
|
|
20
|
+
BEGIN;
|
|
21
|
+
|
|
22
|
+
ALTER TABLE government_board_items
|
|
23
|
+
DROP CONSTRAINT IF EXISTS government_board_items_outcome_check;
|
|
24
|
+
|
|
25
|
+
ALTER TABLE government_board_items
|
|
26
|
+
ADD CONSTRAINT government_board_items_outcome_check
|
|
27
|
+
CHECK (outcome IN ('passed', 'returned', 'withdrawn', 'expired'));
|
|
28
|
+
|
|
29
|
+
COMMIT;
|
|
@@ -25,6 +25,7 @@ const db = require('../db');
|
|
|
25
25
|
const sessionScopes = require('../session-scopes');
|
|
26
26
|
const boardMembership = require('../board-membership');
|
|
27
27
|
const board = require('../board');
|
|
28
|
+
const boardWithdraw = require('../board-withdraw');
|
|
28
29
|
const recommendations = require('../recommendations');
|
|
29
30
|
const docket = require('../docket');
|
|
30
31
|
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
@@ -807,6 +808,31 @@ module.exports = function buildGovernmentRouter() {
|
|
|
807
808
|
}
|
|
808
809
|
});
|
|
809
810
|
|
|
811
|
+
// rank: perm board.item.open — withdraw your own open sitting (task 1004564).
|
|
812
|
+
// The same atom as putting one up: taking it back is the proposer's half of
|
|
813
|
+
// the same act. The atom is only the coarse gate; the walls are in
|
|
814
|
+
// board-withdraw.js and repeated in its write — only the builder who opened
|
|
815
|
+
// the sitting, only while it is open and inside its clock, and only before
|
|
816
|
+
// anyone has voted. It closes with outcome 'withdrawn' (government_026).
|
|
817
|
+
router.post('/government/board/items/:itemId/withdraw', api.requireBuilder, api.requirePermission('board.item.open'), async (req, res) => {
|
|
818
|
+
const itemId = parseId(req, res, { param: 'itemId', code: 'bad_item_id', positiveInt: true });
|
|
819
|
+
if (itemId === null) return;
|
|
820
|
+
try {
|
|
821
|
+
const out = await boardWithdraw.withdrawItem({ itemId, builderId: req.builder.id });
|
|
822
|
+
if (!out.ok) {
|
|
823
|
+
const status =
|
|
824
|
+
out.reason === 'item_not_found' ? 404 :
|
|
825
|
+
out.reason === 'not_proposer' ? 403 :
|
|
826
|
+
409; // item_closed / vote_window_expired / votes_cast / item_changed
|
|
827
|
+
return res.fail(out.reason, status, { item_id: itemId });
|
|
828
|
+
}
|
|
829
|
+
res.json({ ok: true, item_closed: true, outcome: out.item.outcome, item: out.item });
|
|
830
|
+
} catch (err) {
|
|
831
|
+
log.error('[gds] POST /government/board/items/:itemId/withdraw', err);
|
|
832
|
+
res.fail('withdraw_failed', { status: 500, message: 'internal error' });
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
|
|
810
836
|
// rank: perm board.vote.cast — cast a vote on an open board item (R08 of goal
|
|
811
837
|
// 1000069, ADR 0175). The atom FLOORS AT XENOS deliberately (the R04 carry-
|
|
812
838
|
// note): the permission is only the coarse gate, and the real wall is the
|
|
@@ -325,6 +325,7 @@
|
|
|
325
325
|
if (item.closed_at) {
|
|
326
326
|
cv.readOnly = true;
|
|
327
327
|
if (item.outcome === 'passed') cv.result = 'Passed ' + b.forN + '–' + b.againstN + '. In force from ' + str(item.closed_at).slice(0, 10) + '.';
|
|
328
|
+
else if (item.outcome === 'withdrawn') cv.result = b.isAuthor ? 'You withdrew it before anyone voted.' : 'Withdrawn by ' + (proposer || 'its author') + ' before anyone voted.';
|
|
328
329
|
else if (b.isAuthor && cv.counsel && cv.counsel.revise) { cv.result = 'It came back to you.'; cv.readOnly = false; cv.reviseForm = { text: words, label: kind === 'matter' ? 'the matter, in your words' : 'why, in your words' }; }
|
|
329
330
|
else cv.result = 'It went back to ' + (proposer || 'its author') + ' to revise.';
|
|
330
331
|
return cv;
|
|
@@ -346,6 +347,16 @@
|
|
|
346
347
|
cv.result = m.state === 'needs' ? 'Read the idea in the Board Room and vote there, with its sections in front of you.' : 'Not yours to vote on.';
|
|
347
348
|
return cv;
|
|
348
349
|
}
|
|
350
|
+
// task 1004564: the proposer may withdraw their own sitting until anyone has
|
|
351
|
+
// voted (POST .../withdraw; the server holds the same two walls). If they may
|
|
352
|
+
// also vote on it, withdraw takes the second place: an objection to your own
|
|
353
|
+
// proposal is a withdraw said the long way.
|
|
354
|
+
var withdrawable = b.isAuthor && !((item.votes || []).length);
|
|
355
|
+
if (withdrawable) {
|
|
356
|
+
cv.actions = b.youOpen ? [act('for', 'vote for', 'primary'), act('withdraw', 'withdraw', 'quiet')] : [act('withdraw', 'withdraw', 'quiet')];
|
|
357
|
+
cv.result = 'Yours. No one has voted yet, so you can still withdraw it.';
|
|
358
|
+
return cv;
|
|
359
|
+
}
|
|
349
360
|
if (b.youOpen) {
|
|
350
361
|
cv.actions = [act('for', 'vote for', 'primary'), act('object', 'object', 'quiet')];
|
|
351
362
|
cv.textActions = [act('abstain', 'abstain', 'text')];
|
|
@@ -575,6 +586,7 @@
|
|
|
575
586
|
if (s.itemId != null) {
|
|
576
587
|
if (key === 'for') return post('/government/board/items/' + enc(s.itemId) + '/votes', { direction: 'yes' });
|
|
577
588
|
if (key === 'object') return post('/government/board/items/' + enc(s.itemId) + '/votes', { direction: 'no', reason_md: str(inp.reason).trim(), section_key: str(inp.section) });
|
|
589
|
+
if (key === 'withdraw') return post('/government/board/items/' + enc(s.itemId) + '/withdraw', {});
|
|
578
590
|
if (key === 'revise') {
|
|
579
591
|
var text = str(inp.text).trim();
|
|
580
592
|
if (s.kind === 'amendment') return post('/government/board/items', { subject_type: 'constitutional_amendment', proposal: s.proposed, rationale_md: text });
|
|
@@ -629,6 +641,7 @@
|
|
|
629
641
|
return { toast: r.already_voted ? 'Your vote was already on the record.' : 'Your yes is on the record.', building: b, kind: 'light', close: false };
|
|
630
642
|
}
|
|
631
643
|
if (key === 'object') return { toast: r.item_closed ? 'Your objection is on the record. It goes back to its author to revise.' : 'Your objection is on the record.', building: b, kind: 'settle', close: false };
|
|
644
|
+
if (key === 'withdraw') return { toast: 'Withdrawn. The sitting is closed and nothing was decided.', building: b, kind: 'settle', close: true };
|
|
632
645
|
if (key === 'abstain') return { toast: 'Noted. Nothing goes on the record, and it still waits for your vote until it closes.', building: null, kind: null, close: true };
|
|
633
646
|
if (key === 'revise') return { toast: 'Revised. It sits again on the new wording.', building: b, kind: 'light', close: true };
|
|
634
647
|
if (key === 'hold') return { toast: 'Held. It waits for you.', building: null, kind: null, close: true };
|
|
@@ -678,6 +691,9 @@
|
|
|
678
691
|
backup_already_running: 'A backup is already running.',
|
|
679
692
|
intent_conflict: 'Another core run is going. Try again when it settles.',
|
|
680
693
|
criterion_needs_recording: 'This one needs a recording. Sign it off on its goal page.',
|
|
694
|
+
not_proposer: 'Only the person who put it up may withdraw it.',
|
|
695
|
+
votes_cast: 'Someone has voted on it, so it is the sitting\'s to decide now.',
|
|
696
|
+
item_changed: 'Someone voted just now, so it stays with the sitting.',
|
|
681
697
|
};
|
|
682
698
|
function refusal(status, body) {
|
|
683
699
|
var e = body && body.error ? body.error : {};
|
|
@@ -119,6 +119,8 @@
|
|
|
119
119
|
decide: 'decide', ratify: 'ratify', ratify_goal: 'make goal', up: 'move up', down: 'move down',
|
|
120
120
|
// task 1004561: a builder's monthly limit, and a sign-off undone
|
|
121
121
|
budget: 'set limit', unsign: 'undo',
|
|
122
|
+
// task 1004564: the proposer takes back their own sitting before any vote
|
|
123
|
+
withdraw: 'withdraw',
|
|
122
124
|
});
|
|
123
125
|
// What a row's chips ask, per act (a reason or an option is one tap, never text).
|
|
124
126
|
const ASKS = Object.freeze({ budget: 'monthly limit', unsign: 'why', decide: 'pick one', ratify_goal: 'which version', check: 'check', take_core: 'move to', rot: 'stale after', module_off: 'sure', invite: 'their GitHub name', object: 'why', force: 'why', switch_off: 'why', pause: 'why', deactivate: 'why', signoff: 'confirm', allow: 'which goal', tier: 'which tier', give: 'to whom', take: 'from whom' });
|
|
@@ -751,7 +751,7 @@
|
|
|
751
751
|
pause: 'paused', resume: 'resumed', tier: 'new tier', deactivate: 'deactivated', give: 'gave', take: 'took back',
|
|
752
752
|
check: 'checked', rot: 'set', module_on: 'turned on', module_off: 'turned off', backup: 'backed up', invite: 'invited',
|
|
753
753
|
decide: 'decided', ratify: 'ratified', ratify_goal: 'made a goal of',
|
|
754
|
-
budget: 'set the limit of', unsign: 'undid',
|
|
754
|
+
budget: 'set the limit of', unsign: 'undid', withdraw: 'withdrew',
|
|
755
755
|
};
|
|
756
756
|
// One act from a row: checked, sent through the card's own request (the
|
|
757
757
|
// server's permission checks decide), then the row clears, the floor counts
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.38",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.38",
|
|
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.21.
|
|
3
|
+
"version": "1.21.38",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -8986,5 +8986,11 @@
|
|
|
8986
8986
|
"id": "1004495",
|
|
8987
8987
|
"text": "The hall now looks different in each craft mode. In Artistry, Ideating and Governing the mode button, the current sidebar tab and the top edge of the page take the mode's own colour (moonlight or night ink, gold, city blue), t"
|
|
8988
8988
|
}
|
|
8989
|
+
],
|
|
8990
|
+
"1.21.38": [
|
|
8991
|
+
{
|
|
8992
|
+
"id": "1004564",
|
|
8993
|
+
"text": "If you put something up for a vote and nobody has voted yet, you can now withdraw it with one tap on your desk. Once anyone votes, it stays with the vote. A small database change makes \"withdrawn\" a valid way for a sitting to"
|
|
8994
|
+
}
|
|
8989
8995
|
]
|
|
8990
8996
|
}
|
|
@@ -292,6 +292,10 @@ const GOVERNMENT_PINS = {
|
|
|
292
292
|
// writes the table; neither route does.
|
|
293
293
|
'POST /government/board/decision-rules': 'perm:board.item.open',
|
|
294
294
|
'POST /government/board/matters': 'perm:board.item.open',
|
|
295
|
+
// task 1004564: a proposer withdraws their own open sitting before any vote —
|
|
296
|
+
// the proposer's half of putting one up, so the same atom. The proposer-only
|
|
297
|
+
// and no-votes walls are in withdrawItem (board-withdraw.js) and its write.
|
|
298
|
+
'POST /government/board/items/:itemId/withdraw': 'perm:board.item.open',
|
|
295
299
|
};
|
|
296
300
|
|
|
297
301
|
// Module-scoped privileged-rank pins: a privileged route that lives in a FEATURE
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.38'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// tests/government_board_withdraw.mjs — a proposer withdraws their own open
|
|
2
|
+
// sitting before anyone has voted (task 1004564, goal 1000125).
|
|
3
|
+
//
|
|
4
|
+
// WHAT IT GUARDS.
|
|
5
|
+
// 1. THE TWO WALLS IN withdrawItem (board-withdraw.js) — only the builder who OPENED the
|
|
6
|
+
// sitting, and only while no vote is on it. Each refusal asserts the write
|
|
7
|
+
// was never reached, so deleting either guard reds a test here (the stub
|
|
8
|
+
// write would otherwise "succeed").
|
|
9
|
+
// 2. THE SAME WALLS IN THE WRITE ITSELF — db.withdrawBoardItem's WHERE repeats
|
|
10
|
+
// open + proposer + no vote, so a vote landing between the reads and the
|
|
11
|
+
// write cannot be withdrawn over. Checked on the SQL the db actually sends.
|
|
12
|
+
// 3. THE MIGRATION — government_026 widens the outcome CHECK to carry
|
|
13
|
+
// 'withdrawn' and 'expired', is idempotent, and never narrows it.
|
|
14
|
+
//
|
|
15
|
+
// DB-free: the db functions (and, for 2, the pool's query) are monkeypatched,
|
|
16
|
+
// the sibling board tests' idiom.
|
|
17
|
+
//
|
|
18
|
+
// Run: node --test tests/government_board_withdraw.mjs
|
|
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 path from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
29
|
+
const db = require(path.join(GOV, 'db.js'));
|
|
30
|
+
const { withdrawItem } = require(path.join(GOV, 'board-withdraw.js'));
|
|
31
|
+
|
|
32
|
+
const CONSENT = { membership: 'rank:metic', pass_rule: 'consent', window_minutes: 1440, close_early_on_full_turnout: true };
|
|
33
|
+
const openItem = (over = {}) => ({
|
|
34
|
+
id: '21', subject_type: 'constitutional_amendment', subject_id: '4', opened_by: '42',
|
|
35
|
+
closed_at: null, outcome: null, expired: false, constitution: CONSENT, ...over,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
async function withDb(stubs, fn) {
|
|
39
|
+
const originals = {};
|
|
40
|
+
for (const [k, v] of Object.entries(stubs)) { originals[k] = db[k]; db[k] = v; }
|
|
41
|
+
try { return await fn(); }
|
|
42
|
+
finally { for (const [k, v] of Object.entries(originals)) db[k] = v; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A stub set whose write records whether it was reached.
|
|
46
|
+
function stubs(item, votes = []) {
|
|
47
|
+
const calls = { write: null };
|
|
48
|
+
return {
|
|
49
|
+
calls,
|
|
50
|
+
db: {
|
|
51
|
+
getBoardItemForVote: async () => item,
|
|
52
|
+
getVotesForItem: async () => votes,
|
|
53
|
+
withdrawBoardItem: async (args) => { calls.write = args; return { ...item, closed_at: '2026-10-04T10:00:00Z', outcome: 'withdrawn' }; },
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── 1. the walls in withdrawItem (board-withdraw.js) ───────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
test('the proposer withdraws their own open sitting before any vote: closed, outcome withdrawn', async () => {
|
|
61
|
+
const s = stubs(openItem());
|
|
62
|
+
const out = await withDb(s.db, () => withdrawItem({ itemId: 21, builderId: 42 }));
|
|
63
|
+
assert.equal(out.ok, true);
|
|
64
|
+
assert.equal(out.item.outcome, 'withdrawn');
|
|
65
|
+
assert.deepEqual(s.calls.write, { itemId: '21', builderId: 42 }, 'the write carries the proposer, so its WHERE can check them too');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('PROPOSER ONLY: anyone else is refused not_proposer, and the write is never reached', async () => {
|
|
69
|
+
const s = stubs(openItem({ opened_by: '42' }));
|
|
70
|
+
const out = await withDb(s.db, () => withdrawItem({ itemId: 21, builderId: 7 }));
|
|
71
|
+
assert.deepEqual(out, { ok: false, reason: 'not_proposer' });
|
|
72
|
+
assert.equal(s.calls.write, null, 'a refused withdraw writes nothing');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('PROPOSER ONLY: a sitting nobody opened (a Full Idea\'s, opened by its grade) cannot be withdrawn', async () => {
|
|
76
|
+
const s = stubs(openItem({ subject_type: 'full_idea', opened_by: null }));
|
|
77
|
+
const out = await withDb(s.db, () => withdrawItem({ itemId: 21, builderId: 42 }));
|
|
78
|
+
assert.deepEqual(out, { ok: false, reason: 'not_proposer' });
|
|
79
|
+
assert.equal(s.calls.write, null);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('NO VOTES: once anyone has voted — even the proposer — it is refused votes_cast, and the write is never reached', async () => {
|
|
83
|
+
for (const voter of ['7', '42']) {
|
|
84
|
+
const s = stubs(openItem(), [{ id: '1', item_id: '21', voter_id: voter, direction: 'yes' }]);
|
|
85
|
+
const out = await withDb(s.db, () => withdrawItem({ itemId: 21, builderId: 42 }));
|
|
86
|
+
assert.deepEqual(out, { ok: false, reason: 'votes_cast' }, `a vote by ${voter} holds the sitting`);
|
|
87
|
+
assert.equal(s.calls.write, null);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('closed, past its clock, missing, or a vote racing the write: each refused with its own reason', async () => {
|
|
92
|
+
const cases = [
|
|
93
|
+
[null, 'item_not_found'],
|
|
94
|
+
[openItem({ closed_at: '2026-10-01T00:00:00Z', outcome: 'returned' }), 'item_closed'],
|
|
95
|
+
[openItem({ expired: true }), 'vote_window_expired'],
|
|
96
|
+
];
|
|
97
|
+
for (const [item, reason] of cases) {
|
|
98
|
+
const s = stubs(item);
|
|
99
|
+
const out = await withDb(s.db, () => withdrawItem({ itemId: 21, builderId: 42 }));
|
|
100
|
+
assert.deepEqual(out, { ok: false, reason });
|
|
101
|
+
assert.equal(s.calls.write, null, `${reason} writes nothing`);
|
|
102
|
+
}
|
|
103
|
+
// the write's own WHERE matched nothing — a vote landed after the reads
|
|
104
|
+
const s = stubs(openItem());
|
|
105
|
+
s.db.withdrawBoardItem = async () => null;
|
|
106
|
+
const out = await withDb(s.db, () => withdrawItem({ itemId: 21, builderId: 42 }));
|
|
107
|
+
assert.deepEqual(out, { ok: false, reason: 'item_changed' });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ── 2. the same walls in the write ───────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
test('the write repeats every wall in its WHERE: open, opened by this builder, no vote row', async () => {
|
|
113
|
+
const { pool } = require(path.join(ROOT, 'src', 'bongos', 'pool'));
|
|
114
|
+
const original = pool.query;
|
|
115
|
+
let sent = null;
|
|
116
|
+
pool.query = async (sql, params) => { sent = { sql: String(sql).replace(/\s+/g, ' '), params }; return { rows: [] }; };
|
|
117
|
+
try {
|
|
118
|
+
const out = await db.withdrawBoardItem({ itemId: 21, builderId: 42 });
|
|
119
|
+
assert.equal(out, null, 'no row matched → null, the sitting untouched');
|
|
120
|
+
} finally { pool.query = original; }
|
|
121
|
+
assert.match(sent.sql, /UPDATE government_board_items SET closed_at = now\(\), outcome = 'withdrawn'/);
|
|
122
|
+
assert.match(sent.sql, /closed_at IS NULL/);
|
|
123
|
+
assert.match(sent.sql, /opened_by = \$2/);
|
|
124
|
+
assert.match(sent.sql, /NOT EXISTS \(SELECT 1 FROM government_board_votes WHERE item_id = \$1\)/);
|
|
125
|
+
assert.deepEqual(sent.params, [21, 42]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ── 3. the migration ─────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
test('government_026 widens the outcome CHECK to withdrawn and expired, idempotently, never narrowing it', () => {
|
|
131
|
+
const sql = fs.readFileSync(path.join(GOV, 'migrations', 'government_026_board_outcome_withdrawn.sql'), 'utf8');
|
|
132
|
+
const original = fs.readFileSync(path.join(GOV, 'migrations', 'government_003_board_tables.sql'), 'utf8');
|
|
133
|
+
assert.match(original, /outcome\s+text\s+CHECK \(outcome IN \('passed', 'returned'\)\)/, 'sanity: government_003 is the CHECK being widened');
|
|
134
|
+
assert.match(sql, /DROP CONSTRAINT IF EXISTS government_board_items_outcome_check;/, 'idempotent: safe to run twice');
|
|
135
|
+
const check = /ADD CONSTRAINT government_board_items_outcome_check\s+CHECK \(outcome IN \(([^)]*)\)\)/.exec(sql);
|
|
136
|
+
assert.ok(check, 'the widened CHECK is re-added under the same name');
|
|
137
|
+
const words = check[1].split(',').map((w) => w.trim().replace(/'/g, ''));
|
|
138
|
+
assert.deepEqual(words.sort(), ['expired', 'passed', 'returned', 'withdrawn']);
|
|
139
|
+
// the docket already names both new outcomes, so the CHECK and its words agree
|
|
140
|
+
const floors = fs.readFileSync(path.join(GOV, 'docket-floors.js'), 'utf8');
|
|
141
|
+
for (const w of words) assert.match(floors, new RegExp(`\\b${w}:`), `docket-floors OUTCOME_WORDS names '${w}'`);
|
|
142
|
+
// additive only: nothing in it rewrites a row or drops anything else
|
|
143
|
+
assert.doesNotMatch(sql.replace(/--.*$/gm, ''), /\b(UPDATE|DELETE|DROP TABLE|DROP COLUMN)\b/i);
|
|
144
|
+
});
|
|
@@ -45,6 +45,7 @@ const NON_GRANT_MIGRATIONS = new Set([
|
|
|
45
45
|
'government_018_passed_items_index.sql', // BV2.R17 passed-item lookup index (task 1004096)
|
|
46
46
|
'government_020_genesis_stage_subject.sql', // BV2.PS04 genesis_stage board subject CHECK (task 1004429)
|
|
47
47
|
'government_023_decision_rules.sql', // who decides what: the decision-rules table, matters (task 1004529)
|
|
48
|
+
'government_026_board_outcome_withdrawn.sql', // board outcome CHECK widened: withdrawn, expired (task 1004564)
|
|
48
49
|
]);
|
|
49
50
|
|
|
50
51
|
// Permission keys RENAMED after the grant migrations that first granted them.
|
|
@@ -202,6 +202,43 @@ test('a row\'s buttons are the card\'s acts under the board\'s words, and send t
|
|
|
202
202
|
assert.equal(D.rowsFor('charter', 0, dk, { now: NOW })[0].wait, 'reading…');
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
+
// task 1004564: the proposer's own sitting, before anyone has voted, is one row
|
|
206
|
+
// with one button — withdraw — sent to the withdraw route. A vote takes it away.
|
|
207
|
+
test('your own sitting with no vote on it: one row, withdraw, sent to its route; a vote or another viewer takes it away', () => {
|
|
208
|
+
const ms = [matter({ id: 'board:1', source: 'board', building: 'charter', state: 'moving', ref: { type: 'board_item', id: '1' } })];
|
|
209
|
+
const dk = docket(ms);
|
|
210
|
+
const room = (votes) => ({ open: [{ id: '1', subject_type: 'constitutional_amendment', subject: { title: LONG, author_id: '95' }, votes }], closed: [] });
|
|
211
|
+
const mine = D.rowsFor('charter', 0, dk, { room: room([]), now: NOW, viewerId: '95' })[0];
|
|
212
|
+
assert.deepEqual(mine.actions.map((a) => [a.key, a.label]), [['withdraw', 'withdraw']]);
|
|
213
|
+
assert.equal(mine.chips, null, 'withdraw asks nothing: no chips');
|
|
214
|
+
assert.deepEqual(j(C.request(mine.card, 'withdraw')), { method: 'POST', url: '/api/bongos/government/board/items/1/withdraw', body: {} });
|
|
215
|
+
assert.match(C.answer(mine.card, 'withdraw', { item_closed: true, outcome: 'withdrawn' }).toast, /^Withdrawn/);
|
|
216
|
+
assert.equal(C.refusal(409, { error: { code: 'votes_cast' } }), 'Someone has voted on it, so it is the sitting\'s to decide now.');
|
|
217
|
+
// one vote — anyone's — and the row is the sitting's again
|
|
218
|
+
const voted = D.rowsFor('charter', 0, dk, { room: room([{ voter_id: '7', direction: 'yes', name: 'Ana' }]), now: NOW, viewerId: '95' })[0];
|
|
219
|
+
assert.ok(!voted.actions.some((a) => a.key === 'withdraw'), 'no withdraw once a vote is cast');
|
|
220
|
+
// someone else's sitting never offers it
|
|
221
|
+
const theirs = D.rowsFor('charter', 0, dk, { room: room([]), now: NOW, viewerId: '7' })[0];
|
|
222
|
+
assert.ok(!theirs.actions.some((a) => a.key === 'withdraw'), 'only the proposer sees withdraw');
|
|
223
|
+
// a proposer who may also vote keeps yes first, withdraw second — still two buttons
|
|
224
|
+
const needs = D.rowsFor('charter', 0, docket([{ ...ms[0], state: 'needs' }]), { room: room([]), now: NOW, viewerId: '95' })[0];
|
|
225
|
+
assert.deepEqual(needs.actions.map((a) => a.key), ['for', 'withdraw']);
|
|
226
|
+
// and the line rule holds on every floor with the proposer looking
|
|
227
|
+
const own = { ...ROOM, open: ROOM.open.map((it) => it.subject_type === 'full_idea' ? it : { ...it, subject: { ...it.subject, author_id: '95' } }) };
|
|
228
|
+
const desks = everyDesk(docket(longMatters()), { room: own });
|
|
229
|
+
assert.ok(desks.flatMap((d) => d.rows).some((r) => r.actions.some((a) => a.key === 'withdraw')), 'the stress docket reached a withdraw row');
|
|
230
|
+
assert.deepEqual(desks.flatMap((d) => D.breaks(d)), []);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('a withdrawn sitting reads as withdrawn on the card, never as sent back to revise', () => {
|
|
234
|
+
const dk = docket([matter({ id: 'board:1', source: 'board', building: 'charter', state: 'settled', ref: { type: 'board_item', id: '1' } })]);
|
|
235
|
+
const room = { open: [], closed: [{ id: '1', subject_type: 'constitutional_amendment', subject: { title: 'Shorter windows', author_id: '95' }, votes: [], closed_at: '2026-10-03T10:00:00.000Z', outcome: 'withdrawn' }] };
|
|
236
|
+
const cv = (viewerId) => C.model({ docket: dk, key: 'board:1', room, now: NOW, viewerId });
|
|
237
|
+
assert.equal(cv('95').result, 'You withdrew it before anyone voted.');
|
|
238
|
+
assert.match(cv('7').result, /^Withdrawn by .* before anyone voted\.$/);
|
|
239
|
+
assert.equal(cv('95').readOnly, true);
|
|
240
|
+
});
|
|
241
|
+
|
|
205
242
|
// ── the page: the desk acts in place, and the game layer answers ──────────────
|
|
206
243
|
|
|
207
244
|
function fakeDom() {
|