@bongos/core 1.19.250 → 1.19.252

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.
Files changed (36) hide show
  1. package/.bongos-core.json +100 -30
  2. package/clients/bongos-client/index.d.ts +1 -1
  3. package/docs/adr/0179-board-membership-predicate-grammar.md +113 -0
  4. package/docs/adr/README.md +1 -0
  5. package/docs/api/openapi.json +3 -0
  6. package/docs/api-reference.md +1 -1
  7. package/docs/copy-inventory.md +1432 -0
  8. package/docs/copy-registry.json +13311 -0
  9. package/modules/copy-desk/CLAUDE.md +15 -0
  10. package/modules/copy-desk/flags.js +245 -0
  11. package/modules/copy-desk/migrations/copy_desk_001_flags.sql +128 -0
  12. package/modules/copy-desk/module.json +11 -0
  13. package/modules/copy-desk/registry.js +317 -0
  14. package/modules/copy-desk/routes/copy-desk.js +272 -0
  15. package/modules/copy-desk/tests/copy_flags.mjs +246 -0
  16. package/modules/copy-desk/tests/copy_no_cms.mjs +104 -0
  17. package/modules/copy-desk/tests/copy_queue.mjs +257 -0
  18. package/modules/government/board-membership.js +20 -20
  19. package/modules/government/board.js +57 -3
  20. package/modules/government/config.js +14 -8
  21. package/modules/government/db.js +18 -7
  22. package/modules/government/membership-predicate.js +115 -0
  23. package/modules/government/routes/government.js +19 -0
  24. package/modules/hall-ui/public/government.js +54 -5
  25. package/package-lock.json +2 -2
  26. package/package.json +1 -1
  27. package/scripts/gds/publish-manifest.js +9 -0
  28. package/scripts/gds/run-unit-tests.js +5 -0
  29. package/src/module-api.js +5 -1
  30. package/tests/government_board_amendment.mjs +87 -1
  31. package/tests/government_board_membership.mjs +101 -10
  32. package/tests/government_board_membership_db.mjs +150 -0
  33. package/tests/government_board_vote.mjs +5 -3
  34. package/tests/government_constitution_view.mjs +53 -0
  35. package/tests/publish_manifest.mjs +82 -0
  36. package/tests/rank_tier_single_source.mjs +9 -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.250",
6
- "core_contract": "1.19.250",
7
- "source_commit": "f59cb8a3e16573333ac81a1b37976b46909d7f49",
5
+ "core_version": "1.19.252",
6
+ "core_contract": "1.19.252",
7
+ "source_commit": "202060eb7fde831d0bc2dad4f0cc70546998cf92",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-19T01:00:55.130Z",
9
+ "built_at": "2026-08-19T01:31:20.217Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 313,
13
- "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1334,
12
+ "docs_redacted": 315,
13
+ "agent_docs_stubbed": 22,
14
+ "functional_verbatim": 1345,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1668,
20
- "tree_sha256": "dff668aafc72a07d98fd1370bf3ad744a895c6336703607bbab6944e967e9518",
19
+ "file_count": 1682,
20
+ "tree_sha256": "a092898c2eb209b9c8782c4151ebdf913230930e12e439783a2e77ee73fbceba",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -367,7 +367,7 @@
367
367
  {
368
368
  "path": "clients/bongos-client/index.d.ts",
369
369
  "mode": "0000644",
370
- "sha256": "14023727fa9eea325fb6ae784d9edea291c228d19e4966618a3d953191a8ef2d"
370
+ "sha256": "1c790a9dca384a70cba29ba814cff546cdd69eb784f512fb88e06c336b3f75a9"
371
371
  },
372
372
  {
373
373
  "path": "clients/bongos-client/index.mjs",
@@ -1374,20 +1374,25 @@
1374
1374
  "mode": "0000644",
1375
1375
  "sha256": "36f95e2bd77c310e1be3632e9b9d84bf014d4d51ae73b450365ed353fefa8923"
1376
1376
  },
1377
+ {
1378
+ "path": "docs/adr/0179-board-membership-predicate-grammar.md",
1379
+ "mode": "0000644",
1380
+ "sha256": "782900e7a2e49c14d7796d94e2b63493659e3c6c3ad8eb8bd721fa589307612b"
1381
+ },
1377
1382
  {
1378
1383
  "path": "docs/adr/README.md",
1379
1384
  "mode": "0000644",
1380
- "sha256": "745dc09cea4db07614901e6744eeb11c12a7f45711a40cf35c777315452baf2e"
1385
+ "sha256": "bf9dddba0e72ab43ee6124fbba2a0479f1cb7dfc35ea4005f3382aae98e0066f"
1381
1386
  },
1382
1387
  {
1383
1388
  "path": "docs/api-reference.md",
1384
1389
  "mode": "0000644",
1385
- "sha256": "b99be08b4c6f2e81b12e3ddf80f4cfaa39423e0c87ff2790857ae16ad755fdc7"
1390
+ "sha256": "3eb89e224eea00629fb69752c7606e9ffed34171ab16fcb82120e030e5db8476"
1386
1391
  },
1387
1392
  {
1388
1393
  "path": "docs/api/openapi.json",
1389
1394
  "mode": "0000644",
1390
- "sha256": "1c59dad7ce9c5d20bf2ce8671308b5e66a48516c30dc7b48677b0f0382c39e4b"
1395
+ "sha256": "2fc5bd12b302ff6ef0bc4e0012282bcef8616942c23e9c5f0fbe7c9e6a71d6a9"
1391
1396
  },
1392
1397
  {
1393
1398
  "path": "docs/architecture.md",
@@ -1409,6 +1414,16 @@
1409
1414
  "mode": "0000644",
1410
1415
  "sha256": "f566ac3244920461753cf01454b986a6b669fced8c87a440e3ce99298b0cddf5"
1411
1416
  },
1417
+ {
1418
+ "path": "docs/copy-inventory.md",
1419
+ "mode": "0000644",
1420
+ "sha256": "68e4fc5231a55b272b1da464e07232dc0386f1a41614ee8a412718b082520e08"
1421
+ },
1422
+ {
1423
+ "path": "docs/copy-registry.json",
1424
+ "mode": "0000644",
1425
+ "sha256": "08b322d9f873384a7ea946a78b55566642ff3d30d09c8c693b94a3bbe805af37"
1426
+ },
1412
1427
  {
1413
1428
  "path": "docs/design-contract.md",
1414
1429
  "mode": "0000644",
@@ -2479,6 +2494,51 @@
2479
2494
  "mode": "0000644",
2480
2495
  "sha256": "e84c11a506bcd47b1f9b6be16bcd262d69aa7162f283c41da63687c5dd611845"
2481
2496
  },
2497
+ {
2498
+ "path": "modules/copy-desk/CLAUDE.md",
2499
+ "mode": "0000644",
2500
+ "sha256": "7cb9dac9cb87b679e1ae50e4112b3a7928a38ebb9902c8947a74d17bec4d9009"
2501
+ },
2502
+ {
2503
+ "path": "modules/copy-desk/flags.js",
2504
+ "mode": "0000644",
2505
+ "sha256": "b2959e28807e25a6c80691569a5653bc5adf6471ae1d4b4e33bd2cda5e5054cb"
2506
+ },
2507
+ {
2508
+ "path": "modules/copy-desk/migrations/copy_desk_001_flags.sql",
2509
+ "mode": "0000644",
2510
+ "sha256": "7569a5b33b9a2c86c588b49d086c443966066a412855d3f88367851a24edcfa7"
2511
+ },
2512
+ {
2513
+ "path": "modules/copy-desk/module.json",
2514
+ "mode": "0000644",
2515
+ "sha256": "fab0432813f79c3db41d8d7a4859d7173b93170af6b482978d3d4785602c03aa"
2516
+ },
2517
+ {
2518
+ "path": "modules/copy-desk/registry.js",
2519
+ "mode": "0000644",
2520
+ "sha256": "d1de5f38dfddae014878adb57021c18266b17884f8c35e798b7f5eea28853ef1"
2521
+ },
2522
+ {
2523
+ "path": "modules/copy-desk/routes/copy-desk.js",
2524
+ "mode": "0000644",
2525
+ "sha256": "a0b61896134bbd60d9aa6146ed3fb2623de37a400043e4165867436d60374cd4"
2526
+ },
2527
+ {
2528
+ "path": "modules/copy-desk/tests/copy_flags.mjs",
2529
+ "mode": "0000644",
2530
+ "sha256": "c5c4d4cf33e8193c47cb9150daf0e1e0eb4ad417b359066ff87f78f9485b50e0"
2531
+ },
2532
+ {
2533
+ "path": "modules/copy-desk/tests/copy_no_cms.mjs",
2534
+ "mode": "0000644",
2535
+ "sha256": "ddd1e59deaa737950b564436b670118e52c91050620cb034bb76a95d7cb13176"
2536
+ },
2537
+ {
2538
+ "path": "modules/copy-desk/tests/copy_queue.mjs",
2539
+ "mode": "0000644",
2540
+ "sha256": "1c3e45869742d2d406fb595085095a3ee9246c5a040172c18156b3a477cecfca"
2541
+ },
2482
2542
  {
2483
2543
  "path": "modules/dev-box/CLAUDE.md",
2484
2544
  "mode": "0000644",
@@ -2922,12 +2982,12 @@
2922
2982
  {
2923
2983
  "path": "modules/government/board-membership.js",
2924
2984
  "mode": "0000644",
2925
- "sha256": "5bf474bee95bc8a8982e4a52c9e5b19955a20195dc1affd51c07e99540c8c059"
2985
+ "sha256": "26f212bfb64239e98418618ec3304664180b06f3740b8c4f682f29d3bbf2f3e9"
2926
2986
  },
2927
2987
  {
2928
2988
  "path": "modules/government/board.js",
2929
2989
  "mode": "0000644",
2930
- "sha256": "5529a57a4aa92c3e1903764079a6a2b7d7eb3920c2f346e642a2158389f26b5d"
2990
+ "sha256": "c446f18a8aebc4de0656d7fddeb508c8ec242ce76b243c1c787be7e94e47e49e"
2931
2991
  },
2932
2992
  {
2933
2993
  "path": "modules/government/catalog.js",
@@ -2937,12 +2997,17 @@
2937
2997
  {
2938
2998
  "path": "modules/government/config.js",
2939
2999
  "mode": "0000644",
2940
- "sha256": "56bd51c927220b6bc0dc749ad1d591addbed25870e532b8d95da6ba5eab3f763"
3000
+ "sha256": "62fc47cb6775b1af86bf7f22773180b390d584230b87af6bb1ec20a841be57e1"
2941
3001
  },
2942
3002
  {
2943
3003
  "path": "modules/government/db.js",
2944
3004
  "mode": "0000644",
2945
- "sha256": "d50b12547863df08cf32c606fe91187e1239f5b0d339f9d34ed9f3b5403b3d15"
3005
+ "sha256": "6d85422869793382fc372a43b6a2b745bcb0e8a62c4fec06b780e812ae5066b9"
3006
+ },
3007
+ {
3008
+ "path": "modules/government/membership-predicate.js",
3009
+ "mode": "0000644",
3010
+ "sha256": "57a1054cb3ae65e8b6d9af6657601ea67a20dd658daf442e0fca729165a55461"
2946
3011
  },
2947
3012
  {
2948
3013
  "path": "modules/government/migrations/governance_001_tables.sql",
@@ -3052,7 +3117,7 @@
3052
3117
  {
3053
3118
  "path": "modules/government/routes/government.js",
3054
3119
  "mode": "0000644",
3055
- "sha256": "bf07f55f73d27d738840f4f0e0ffcb8a6d19225e7500dbb36e4a83a1109635db"
3120
+ "sha256": "33f0cf42c404e087a6f3817392299dd2a478f41f748932e108747059b756e703"
3056
3121
  },
3057
3122
  {
3058
3123
  "path": "modules/government/routes/rank-sync.js",
@@ -3317,7 +3382,7 @@
3317
3382
  {
3318
3383
  "path": "modules/hall-ui/public/government.js",
3319
3384
  "mode": "0000644",
3320
- "sha256": "b1de692afb7abb2bd95a91a31d7c8d244c9a9f702c8ab3d73214bb23472614ec"
3385
+ "sha256": "81fed1dde594d91206b596d4e3cc03bd8aef50627eecaba8f4f400b6e9e10a70"
3321
3386
  },
3322
3387
  {
3323
3388
  "path": "modules/hall-ui/public/hall-kit.js",
@@ -4367,12 +4432,12 @@
4367
4432
  {
4368
4433
  "path": "package-lock.json",
4369
4434
  "mode": "0000644",
4370
- "sha256": "d42566d923e50fa60869beed4e57bdf9549f87f83101c54558a29ca9bd848aab"
4435
+ "sha256": "6c248030c970ef5fe5c0a3216cb9b0f23e12bdc74cbb4c3ad78a0addf7a19b64"
4371
4436
  },
4372
4437
  {
4373
4438
  "path": "package.json",
4374
4439
  "mode": "0000644",
4375
- "sha256": "4fea4f30c5b2be65786e08dd645d614bb198bcac4c21c7d11190457a4aa1f566"
4440
+ "sha256": "1c22f0d309c5ae2531eb8c106c63ebb49f087186625def216e3d941e269f4fbe"
4376
4441
  },
4377
4442
  {
4378
4443
  "path": "public-docs/index.html",
@@ -5302,7 +5367,7 @@
5302
5367
  {
5303
5368
  "path": "scripts/gds/publish-manifest.js",
5304
5369
  "mode": "0000644",
5305
- "sha256": "390f9a981a587cb82c2d3e5a75d2823effaf723cc4d02810909caa1edbb90eac"
5370
+ "sha256": "387a6846692f05c4adee5651a6f45780ec12f24de12621e73014e79ea4c47342"
5306
5371
  },
5307
5372
  {
5308
5373
  "path": "scripts/gds/pulse.js",
@@ -5362,7 +5427,7 @@
5362
5427
  {
5363
5428
  "path": "scripts/gds/run-unit-tests.js",
5364
5429
  "mode": "0000644",
5365
- "sha256": "dd83af498409b0891ed6cd4de5906a182215040733aae677cee6f26e39d81177"
5430
+ "sha256": "96e8495ffded28083ba458e8d527c5ba25793a20d5f4c0fee5160cff778f4def"
5366
5431
  },
5367
5432
  {
5368
5433
  "path": "scripts/gds/sandbox-stage.js",
@@ -5992,7 +6057,7 @@
5992
6057
  {
5993
6058
  "path": "src/module-api.js",
5994
6059
  "mode": "0000644",
5995
- "sha256": "817ba3170d6f4b3132d26c50b25ccb769c8b99b9d436f886bae1ca4bddec6a3d"
6060
+ "sha256": "8995e188b10a782e44879050b5342f0b6c664e3657524be0c90f8d2ec6760244"
5996
6061
  },
5997
6062
  {
5998
6063
  "path": "src/module-loader/catalog.js",
@@ -6937,7 +7002,7 @@
6937
7002
  {
6938
7003
  "path": "tests/government_board_amendment.mjs",
6939
7004
  "mode": "0000644",
6940
- "sha256": "7b20463f4d0940c43a5555a7b63d156202d334477267b7d2bc045e27e964d253"
7005
+ "sha256": "54b0d380414a077b072080760a24cfc813d22249f8786b96003ab2e010081b4d"
6941
7006
  },
6942
7007
  {
6943
7008
  "path": "tests/government_board_close.mjs",
@@ -6952,7 +7017,12 @@
6952
7017
  {
6953
7018
  "path": "tests/government_board_membership.mjs",
6954
7019
  "mode": "0000644",
6955
- "sha256": "1a6f90e319c88ecd3e5588d5a6df44a9848c0adf08efd13bfcc52ac99e84d5d0"
7020
+ "sha256": "aa36727bb3fe750783f0adb82235fa7b2e5b1013c01b773a12d2dff6f0c5ce78"
7021
+ },
7022
+ {
7023
+ "path": "tests/government_board_membership_db.mjs",
7024
+ "mode": "0000644",
7025
+ "sha256": "7da08f5ad67ca1d42d0ef2afb641d1a5f1d5e0c6e0267e8b5f3ad0d275603a36"
6956
7026
  },
6957
7027
  {
6958
7028
  "path": "tests/government_board_view.mjs",
@@ -6962,7 +7032,7 @@
6962
7032
  {
6963
7033
  "path": "tests/government_board_vote.mjs",
6964
7034
  "mode": "0000644",
6965
- "sha256": "c1b9b0963dde0c033bd0eb411b63032a51edaa3d91751100593b7e3b4999c017"
7035
+ "sha256": "ed31b8c643d2b9279f4d7e70a1f2ab67d8fbe620c1f61ce8496ec858247d7d26"
6966
7036
  },
6967
7037
  {
6968
7038
  "path": "tests/government_board_vote_ui.mjs",
@@ -6997,7 +7067,7 @@
6997
7067
  {
6998
7068
  "path": "tests/government_constitution_view.mjs",
6999
7069
  "mode": "0000644",
7000
- "sha256": "64fc8006d5b3a562b01a38bfd0ad00a1357969b0ac52ce270e77d4d93eddca80"
7070
+ "sha256": "1fb474588046f937319bde1121f66428300cc0f63e36766ab4543c684a5a09bc"
7001
7071
  },
7002
7072
  {
7003
7073
  "path": "tests/government_fold.mjs",
@@ -7777,7 +7847,7 @@
7777
7847
  {
7778
7848
  "path": "tests/publish_manifest.mjs",
7779
7849
  "mode": "0000644",
7780
- "sha256": "8849310cc884c09785478faa8193c853468a46b79b63e72f1e362d95586fcdcc"
7850
+ "sha256": "a5e36e0a0d228638c499ccac2559bd947a85735e1a922a32c7c1377c98ed2a60"
7781
7851
  },
7782
7852
  {
7783
7853
  "path": "tests/publish_reconciler.mjs",
@@ -7792,7 +7862,7 @@
7792
7862
  {
7793
7863
  "path": "tests/rank_tier_single_source.mjs",
7794
7864
  "mode": "0000644",
7795
- "sha256": "ab8d36c686f52459d2f751e763ffda9ca7f8ecf7e8376f929cd5feee299557cc"
7865
+ "sha256": "92ab673d4e865c98c9d70ce326b99bee56aab70a3852f2d205af209de8d196ed"
7796
7866
  },
7797
7867
  {
7798
7868
  "path": "tests/rate_limit_public.mjs",
@@ -237,7 +237,7 @@ export interface PostGoalsRequest { version_id: string; title: string; descripti
237
237
  export interface PostGoalsResponse { ok: boolean; goal: unknown; succeeds_goal_id: unknown }
238
238
  export interface PostGovernmentAssignmentsRequest { builder_id: number; rank_key: string }
239
239
  export interface PostGovernmentBoardItemsItemIdVotesRequest { direction: string; reason_md?: string; section_key?: string }
240
- export interface PostGovernmentBoardItemsRequest { subject_type: string; proposal: Record<string, unknown>; rationale_md?: string }
240
+ export interface PostGovernmentBoardItemsRequest { subject_type: string; proposal: Record<string, unknown>; rationale_md?: string; acknowledge_self_removal?: boolean }
241
241
  export interface PostGovernmentBoardItemsResponse { ok: boolean; amendment: unknown; item: unknown }
242
242
  export interface PostGovernmentRanksRankKeyResetResponse { ok: boolean; rank_key: unknown; seeded_rank: unknown; permission_count: unknown }
243
243
  export interface PostGovernmentRanksRequest { rank_key: string; label: string; description?: string }
@@ -0,0 +1,113 @@
1
+ # 0179 — `rank:metic+` means metic and above; a bare `rank:metic` still means exactly metic
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-08-19
5
+ - **Tasks:** [#1003094](https://cloudbongos.com/builders#/task/1003094) (goal 1000069, the Board Room). Blocks R25's sign-off ([#1003038](https://cloudbongos.com/builders#/task/1003038)); unblocks the charter-library fix ([#1003095](https://cloudbongos.com/builders#/task/1003095))
6
+ - **Amends:** [ADR 0175](<redacted>.md) §7 — the constitution's `board.membership` predicate
7
+ - **Related:** ADR 0016 (authority server-side, per request, uncached), 0034 (rank inheritance), 0174 (ranks are ranks), 0083 (a module may not import a sibling)
8
+
9
+ ## Context
10
+
11
+ ADR 0175 gave the constitution a membership predicate with exactly one form, `rank:<key>`, resolved by
12
+ `WHERE rank_key = $1`. An **exact** match. So:
13
+
14
+ - `rank:metic` seated metics **only** — Archons excluded;
15
+ - `rank:xenos` seated **newcomers only**, not "everyone";
16
+ - **"Metic and above" was inexpressible.**
17
+
18
+ Everywhere else in this system a rank is a **ladder** and authority is cumulative — ADR 0034's
19
+ inheritance, the floor-derived permission seed, `requireRank('metic','archon')`. Board membership was
20
+ the single place `rank:X` meant *exactly* X, and nothing said so.
21
+
22
+ Rehearsing R25 end to end on a real Postgres found what that costs. An Archon proposed and ratified
23
+ `{membership: 'rank:metic', pass_rule: 'consent', …}` under the day-one monarchy. The apply worked
24
+ perfectly; the live constitution flipped. Then they tried to vote on the next Full Idea:
25
+
26
+ ```
27
+ POST /government/board/items/7/votes → 403 { code: 'not_board_member' }
28
+ ```
29
+
30
+ **The owner flipped the switch and fell off their own board.** The escape hatch that saved the sandbox
31
+ is itself the diagnosis: an Archon can still *propose* (`board.item.open` floors at metic) but not
32
+ *vote*, so the board would have deadlocked at one member with no one able to undo it.
33
+
34
+ The charter library made the same mistake one layer up — it told owners Direct Democracy was
35
+ selectable today by typing `membership rank:xenos (everyone)`, which seats newcomers only. Its own
36
+ recommended charter, "a consent circle of all trusted members", was not expressible at all.
37
+
38
+ ## Decision
39
+
40
+ ### 1. Explicit grammar — a bare key still means exactly that key
41
+
42
+ The owner's decision (2026-08-18), recorded on the task. Three forms:
43
+
44
+ ```
45
+ rank:metic exactly metic (v1 form, meaning UNCHANGED)
46
+ rank:metic+ metic and every rank above it (ladder)
47
+ rank:metic,archon exactly these ranks (union)
48
+ rank:xenos+,ops the ladder from xenos, plus a custom rank
49
+ ```
50
+
51
+ **Rejected — Option 1, redefining the bare form as "and above."** It was the smaller diff and it
52
+ matched every other authority read in the system, which is exactly what made it dangerous: it would
53
+ have silently changed the meaning of every constitution already ratified, including the shipped
54
+ monarchy default, the moment an instance upgraded. **Nothing in a government should change meaning
55
+ without a vote.** Because the bare form is untouched, this whole change is meaning-preserving.
56
+
57
+ **Rejected — Option 3, keep exact-match and fix only the prose.** Cheapest, and it leaves the ladder
58
+ inconsistency and the foot-gun in place. The library would have had to describe a limitation instead
59
+ of stating a rule.
60
+
61
+ ### 2. `+` is defined only on the standard ladder
62
+
63
+ `RANK_ORDER` is the four-rung standard ladder; a project's **custom** ranks are DB rows with no
64
+ position on it. "Metic and above" is a real question; "and above" about a rank that sits on no ladder
65
+ is not. So `rank:some-custom+` is a **parse refusal**, not a guess — and a refusal resolves to
66
+ **nobody**, per the module's existing fail-closed rule. A bare custom key is unaffected.
67
+
68
+ ### 3. The grammar is written once
69
+
70
+ It had been written **twice**: a shape regex in R05's config sanitizer and a parse regex in R06's
71
+ resolver, held together by a property test asserting "config keeps it ⇔ the resolver parses it". That
72
+ is sound while the grammar is one literal form. It stops being sound the moment a rule needs a
73
+ *lookup* — and `+` is a lookup. **Two hand-written copies of a rule containing a lookup is the same
74
+ class of drift that produced this bug**, so both now call one pure module,
75
+ `modules/government/membership-predicate.js` (it requires only `catalog`, so the sanitizer can use it
76
+ during boot without dragging in the DB layer).
77
+
78
+ ### 4. The disenfranchisement guard
79
+
80
+ A change to the government that removes the person making it is **the one shape that cannot
81
+ self-correct**: once it lands they can no longer vote, including to undo it.
82
+
83
+ `proposeAmendment` now refuses such a proposal **before any write**, naming the one-character fix
84
+ (`rank:<key>+`). It is overridable with an explicit `acknowledge_self_removal` — the owner's "refuse or
85
+ loudly confirm" — because handing the franchise to a wider body and stepping back is a legitimate
86
+ constitutional act, and a system that cannot express it is telling owners what they may decide. The
87
+ acknowledgement is a **separate parameter, never a proposal field**: the strict validator refuses
88
+ unknown fields, which is what keeps it out of the ratified text.
89
+
90
+ ### 5. The surfaces must describe the franchise they actually have
91
+
92
+ The hall's Constitution panel rendered the raw predicate, which would have read `rank:metic+` as
93
+ "builders holding the **metic+** rank" — naming a rank that does not exist and hiding what the suffix
94
+ does. **A panel that misdescribes who governs is this same bug one layer up**, so it now renders every
95
+ form in plain language, and says plainly that an unreadable predicate seats nobody.
96
+
97
+ ## Consequences
98
+
99
+ - **No ratified constitution changes meaning.** A one-element rank set queries byte-identically to the
100
+ old exact match. `rank:archon` — the shipped default — resolves to the same board it always did.
101
+ - **The two DB reads take a set** (`= ANY($1)`), with **DISTINCT**: a set can now match a builder
102
+ through more than one key, and a double-counted member silently raises the bar a consent vote must
103
+ clear.
104
+ - **The structural filters had to survive the rewrite**, and are tested against a real Postgres: a
105
+ deactivated builder and a machine principal still never govern. A widened franchise that also admits
106
+ a bot would be a worse bug than the one being fixed.
107
+ - **The parse result shape changed** from `{ rankKey }` to `{ rankKeys }`. Deliberate rather than
108
+ cosmetic — a caller reading "the" key was reading a single-rank assumption that is no longer true.
109
+ - **Task 1003095 can now write the TRUE predicate for each charter** — Direct Democracy becomes
110
+ `rank:xenos+`, Timocratic Republic `rank:metic+` — rather than rewording around a limitation. That
111
+ is the sibling fix this ADR unblocks, and it is what makes R26's honesty half pass.
112
+ - **Still not a source of authority truth.** The predicate names ranks; who holds a rank stays a DB
113
+ row resolved per request, uncached. Nothing here names a builder.
@@ -252,5 +252,6 @@ This keeps the decision history honest and traceable.
252
252
  | 0176 | [**Private-repo standup: a per-repo read-only deploy key, minted with the owner's token** ([task 1002735](https://cloudbongos.com/builders#/task/1002735), goal 1000032; walk finding N4, seen live on `mercury` 2026-08-17). A standalone instance's repo is cloned/pushed once with the owner's `repo`-scoped token (ADR 0155), but the durable `origin` rides the shared SSH deploy alias — which GitHub answers "Repository not found" for a private repo it isn't registered on, so every private standup failed at the recurring `git pull --ff-only origin main`, **token or not**. Fix: when the target repo answers *private* to the owner's stored token, provisioning mints the instance its OWN ed25519 deploy key beside the checkout (`.git/cloudbongos-deploy-key` — untracked, survives `git reset --hard`, dies with the checkout), registers it on the repo **read-only** as `cloudbongos-deploy-<slug>` (idempotent by title; a stale same-title key is replaced, never duplicated), then wires the CHECKOUT: `origin` re-pointed at plain `git@github.com` and `core.sshCommand` pinned to the key with `-F /dev/null` so no `~/.ssh/config` identity can shadow it. Public repos take the unchanged shared-alias path; pushes keep preferring the owner-token HTTPS URL (task 1002317), which is why read-only suffices; teardown best-effort revokes by title. Rejected: **GitHub App installation tokens** (hourly expiry means every unattended pull needs a minting sidecar or a hub round-trip) and **registering the shared platform key on the repo** (one shared secret granting the whole platform durable access to private code; per-repo keys keep the blast radius to one repo and are visible to the owner in their own repo settings). Follow-ups: the manage page's flip-to-private stays refused until already-provisioned instances get the same wiring, and builder dev boxes of a private-repo project need their own leg.](<redacted>.md) | provisioning / security |
253
253
  | 0177 | [**A goal membership is a `membership_kind`, and the rename ships expand-then-contract** ([task 1003013](https://cloudbongos.com/builders#/task/1003013), goal 1000068). ADR 0174 reserved `role` for the three crafts and enforced it on the GOVERNMENT surface only — by design, since `builders.system_role` and Discord's own noun are legitimate. That left a **fourth** use uncatalogued: `goal_members.role` (`'lead' | 'member'`), the MANAGER predicate behind goal authorization. Renamed to **`membership_kind`** — values unchanged, because the noun was the problem; `rank` is wrong (not a ladder position) and `position` is worse (ADR 0174 uses it as the prose word for a rank, recreating the "rank-role" qualifier collision). A boolean `is_lead` was seriously considered and rejected: the text rename keeps the copied CHECK and stays a reversible change on a live authz column, where `ADD boolean; UPDATE; DROP text` is data-destructive. The migration is **expand-then-contract, never `RENAME COLUMN`** — auto-rollback reverts the code pin and NOT the DB, so `core_220` ADDs the column and keeps the pair in lockstep with a BEFORE trigger (direction decided by which side changed), and a later task DROPs the old column with the `-- forward-unsafe` pragma, a backup, and its own window; no `SET NOT NULL` in step 1, because a static NOT NULL on a column the previous release never heard of is what the forward-safety gate rightly flags. The API field renames with it and carries **no `role` alias** (ADR 0174's Lineage lesson: a time-boxed shim outlives its release), pinned by the test of its absence. The reservation gains a **second narrow wall** rather than a widened first one — line-scoped inside the 5k-line `db.js` to the lines that name `goal_members`, so no allowlist is needed. A **fifth** use (`project_memberships.role`) is catalogued and filed, with this as its pattern.](<redacted>.md) | naming / lifecycle |
254
254
  | 0178 | [**The copy desk: a flag carries a judgement, and no column here could ever hold copy** ([task 1003113](https://cloudbongos.com/builders#/task/1003113), R02 of goal 1000074). [ADR 0081](<redacted>.md) made the repo the source of truth for design TOKENS; user-facing TEXT had no equivalent. R01 built the inventory (`copy-inventory.js` → `docs/copy-registry.json`), which answers *what does the product say* but cannot answer *what is wrong with it* — a judgement a person must record. This is that record plus the artist's queue. A new default-on module keyed **`copy-desk`, not `copy`**, for a concrete reason: the branding contract already owns a `copy` block, so the bare word reds the `core carries no module code` fitness check on the English word in `src/branding.js` — and the editorial "copy desk" is the more accurate name anyway. **The reason is required in the route AND in a CHECK constraint**: the route gives a person a usable error, the constraint means no future write path can create the reasonless flag the queue exists to not contain (a pointer with no judgement is *worse* than no flag — it still costs the artist a look). Closing requires a note by the same symmetry, since an unexplained close is indistinguishable from clearing a queue to move a number. The load-bearing decision is **NO LIVE CMS enforced by the schema's SHAPE**: `copy_desk_flags` has no column that could hold replacement text and must never gain one, because that non-goal does not erode via an "add a CMS" commit but via one plausible column, then one plausible read of it — `copy_no_cms.mjs` pins the write surface at exactly two verbs so R03's proposal path changes it in a reviewable diff. Rank is asymmetric: **filing is open to every rank** (the engineer who wrote the toast and the newcomer who could not read the button are the likeliest noticers, and neither is the artist — the reason floor is the quality gate, rank is not), closing is Metic+ **or the author** (withdrawing your own flag is a correction, not triage); nothing gates on craft, since an artist is a craft and not a rank. Duplicates are the only automatic signal (the one of stale/duplicated/off-voice needing no judgement layer) and the count **subtracts settled work**, or it never moves and teaches that the queue is decorative. A vanished target is **reported, never auto-closed** — the id is a hash over the text, so an edit routinely orphans a flag, and closing a human judgement because a hash moved is the family of error ADR 0162 rules out. Deliberately reports **no `generated_at`**: R01's contract is determinism, so a clock is the one field it cannot carry.](<redacted>.md) | design / artist loop |
255
+ | 0179 | [**`rank:metic+` means metic and above; a bare `rank:metic` still means exactly metic** ([task 1003094](https://cloudbongos.com/builders#/task/1003094), goal 1000069). [ADR 0175](<redacted>.md) gave the constitution ONE membership form, `rank:<key>`, resolved by an EXACT SQL match — so `rank:metic` seated metics ONLY (excluding Archons), `rank:xenos` seated NEWCOMERS ONLY rather than "everyone", and "Metic and above" was inexpressible. Everywhere else a rank is a LADDER and authority is cumulative (ADR 0034 inheritance, requireRank('metic','archon'), the floor-derived seed); board membership was the one place it meant "exactly", and nothing said so. Rehearsing R25 on a real Postgres, an Archon ratified `rank:metic` under monarchy — the apply worked perfectly — and the next vote came back 403 not_board_member: **the owner flipped the switch and fell off their own board**, with the board deadlocked at one member because an Archon can still PROPOSE but no longer VOTE. Owner decision: EXPLICIT GRAMMAR. `rank:metic+` (that rank and above), `rank:metic,archon` (union), and **a bare key still means EXACTLY that key** — rejecting the smaller diff (silently redefining the bare form) precisely because it would have changed the meaning of every already-ratified constitution, the monarchy default included, on upgrade: nothing in a government should change meaning without a vote. `+` is defined only on the STANDARD ladder — "and above" about a custom rank with no ladder position is a parse REFUSAL, not a guess, and a refusal seats nobody. The grammar had been written TWICE (a shape regex in the config sanitizer, a parse regex in the resolver, pinned by a property test) — sound for one literal form, unsound the moment a rule needs a LOOKUP, which is the same class of drift that produced the bug; both now call one pure `membership-predicate.js`. Adds the DISENFRANCHISEMENT GUARD: a proposal that would unseat its own proposer is refused before any write, naming the one-character fix, overridable with an explicit `acknowledge_self_removal` (a separate parameter, never a proposal field) because stepping back is a legitimate act. The two DB reads take a set via `= ANY($1)` + DISTINCT (a double-counted member silently raises the consent bar), the structural filters are re-proven against real Postgres (a deactivated builder and a machine principal still never govern), and the hall panel now describes every form in plain language — rendering `rank:metic+` as "the metic+ rank" would be the same bug one layer up. Unblocks task 1003095, which can now write each charter's TRUE predicate rather than wording around a limitation.](<redacted>.md) | government / the board |
255
256
 
256
257
  > ⚠️ **Numbering collision (known):** two ADRs share `0008`. The numbers were assigned in parallel sessions before the lock could be enforced. The files are kept under their existing filenames to preserve git history, but the table disambiguates them as `0008-a` / `0008-b`. New ADRs should resume at `0040` to avoid further collisions. (`0024` is also a real on-disk duplicate: the cloneable-repo/local-first-memory ADR and the multi-agent-system-architecture ADR were both numbered `0024` in parallel; both keep their filenames, disambiguated as `0024-a` / `0024-b` like `0008`. Likewise `0025` ended up with two on-disk files — the offsite-backup ADR and the structured-criterion↔task-link ADR — both kept under their existing filenames and disambiguated as `0025-a` / `0025-b`; separately, the BFG ADR had also claimed `0025` on 2026-05-30 and yielded to `0026`. A third happened 2026-06-06: `0036` was claimed in parallel by the builders-subdomain ADR ([#726](https://example.com/builders#/task/726)) and this Discord-channels ADR ([#727](https://example.com/builders#/task/727)); the Discord-channels ADR yielded to `0037`. A fourth landed 2026-06-10: `0044` was claimed in parallel by the palette-replacement ADR and the per-box game-preview ADR ([#897](https://example.com/builders#/task/897)); both keep their filenames, disambiguated as `0044-a` / `0044-b` like `0008`. A fifth was caught pre-land 2026-06-13: `0050` was claimed in parallel by the device-flow-admission ADR ([#1000](https://example.com/builders#/task/1000)) and the sandbox-for-everyone ADR ([#1001](https://example.com/builders#/task/1001)); the latter yielded to `0052` before merging, so there is no `0050` duplicate. A sixth was caught pre-land 2026-06-21: the tool-agnostic-design-layer ADR ([#1375](https://example.com/builders#/task/1375)) first collided at `0079` with the value-gated-autonomous-engagement ADR, then at `0080` with the llm-cache-api-transport ADR (both part of the 0075–0080 LLM-cost-reduction series, landed on `main` in parallel); it yielded to `0081` before merging, so there is no `0079`/`0080` design-layer duplicate. A seventh landed 2026-06-23: `0084` was claimed in parallel by the auto-derived-per-task-rank-floor ADR and the remove-default-spend-cap ADR (task 1499); the rank-floor ADR landed on `main` first, so the spend-cap ADR yielded to `0085`, leaving no `0084` duplicate. An eighth landed 2026-06-27: `0095` was claimed in parallel by the borrowed-memory-and-retrieval-concepts ADR (PR #459, landed first) and the cross-agent-context-management ADR ([#1651](https://example.com/builders#/task/1651), PR #460); both keep their filenames, disambiguated as `0095-a` / `0095-b` like `0008`. These two are companions — `0095-a` adopts the memory/retrieval concepts and explicitly defers the grep-vs-semantic question to `0095-b`.)
@@ -16483,6 +16483,9 @@
16483
16483
  "rationale_md": {
16484
16484
  "type": "string",
16485
16485
  "maxLength": 5000
16486
+ },
16487
+ "acknowledge_self_removal": {
16488
+ "type": "boolean"
16486
16489
  }
16487
16490
  },
16488
16491
  "required": [
@@ -233,7 +233,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
233
233
  | POST | `/api/bongos/government/assignments` | `archon` | `builder_id`, `rank_key` | government.manage — assign a CUSTOM rank to a builder. |
234
234
  | DELETE | `/api/bongos/government/assignments/:builderId/:rankKey` | `archon` | — | government.manage — remove a custom-rank assignment. |
235
235
  | 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). |
236
- | 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). |
236
+ | 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). |
237
237
  | 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). |
238
238
  | 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 … |
239
239
  | 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. |