@bongos/core 1.19.562 → 1.19.564
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 +61 -31
- 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/adr/0251-a-suggestion-of-who-to-invite-is-a-recruiting-surface-and-owes-the-opt-out.md +79 -0
- package/docs/adr/0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md +109 -0
- package/docs/adr/README.md +2 -0
- package/docs/api/openapi.json +69 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +127 -127
- package/docs/copy-registry.json +162 -162
- package/docs/module-api-changelog.md +4 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/lifecycle/db-versions.js +22 -0
- package/modules/lifecycle/db.js +2 -1
- package/modules/lifecycle/routes/version-route-authz.js +56 -0
- package/modules/lifecycle/routes/versions.js +22 -0
- package/modules/platform-identity/invite-suggestions.js +57 -0
- package/modules/platform-identity/routes/sso.js +8 -0
- package/modules/provisioning/routes/provisioning.js +56 -0
- package/modules/public-landing/public/assets/cosmos.css +16 -0
- package/modules/public-landing/public/projects.html +85 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/one_planning_version.mjs +161 -0
- package/tests/project_invite_suggestions.mjs +133 -0
- package/tests/project_invite_ui.mjs +171 -18
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.564",
|
|
6
|
+
"core_contract": "1.19.564",
|
|
7
|
+
"source_commit": "d4d0c5049c5c72237a81450221af13702a00ccb7",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-06T18:43:07.289Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 445,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2029,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2498,
|
|
20
|
+
"tree_sha256": "478ff6d025680dd25564647bac310bd570d0e58335d46a999cb9e4ed4d527aec",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -372,12 +372,12 @@
|
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/README.md",
|
|
374
374
|
"mode": "0000644",
|
|
375
|
-
"sha256": "
|
|
375
|
+
"sha256": "38988a49c2cdcc89db69602de1fe46cba0de430643c677352b9db36ed89c010c"
|
|
376
376
|
},
|
|
377
377
|
{
|
|
378
378
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
379
379
|
"mode": "0000644",
|
|
380
|
-
"sha256": "
|
|
380
|
+
"sha256": "bd7d31e9cff0d758cc66c1d4adcf52eeaa99acc2ef48d459265425e2a4d2fd19"
|
|
381
381
|
},
|
|
382
382
|
{
|
|
383
383
|
"path": "clients/bongos-client/examples/hello-world.mjs",
|
|
@@ -387,17 +387,17 @@
|
|
|
387
387
|
{
|
|
388
388
|
"path": "clients/bongos-client/index.cjs",
|
|
389
389
|
"mode": "0000644",
|
|
390
|
-
"sha256": "
|
|
390
|
+
"sha256": "33fbbbaf2a4a939b7351a7aa25264ddd609c1298d9ba809372a162983afe5038"
|
|
391
391
|
},
|
|
392
392
|
{
|
|
393
393
|
"path": "clients/bongos-client/index.d.ts",
|
|
394
394
|
"mode": "0000644",
|
|
395
|
-
"sha256": "
|
|
395
|
+
"sha256": "a0992ef72cffc0d200e62bd5315e64090eda7cbd3477a01ec041e2d08bb39b9e"
|
|
396
396
|
},
|
|
397
397
|
{
|
|
398
398
|
"path": "clients/bongos-client/index.mjs",
|
|
399
399
|
"mode": "0000644",
|
|
400
|
-
"sha256": "
|
|
400
|
+
"sha256": "e83e49178398441026fe6fd4822dd4eddae2c2316f5aa5006a090ef0d1a41b98"
|
|
401
401
|
},
|
|
402
402
|
{
|
|
403
403
|
"path": "clients/bongos-client/package.json",
|
|
@@ -1769,20 +1769,30 @@
|
|
|
1769
1769
|
"mode": "0000644",
|
|
1770
1770
|
"sha256": "da4549c19609d1f549e6f5a57b530c330765f1778f0a9b09a15a208f4575f986"
|
|
1771
1771
|
},
|
|
1772
|
+
{
|
|
1773
|
+
"path": "docs/adr/0251-a-suggestion-of-who-to-invite-is-a-recruiting-surface-and-owes-the-opt-out.md",
|
|
1774
|
+
"mode": "0000644",
|
|
1775
|
+
"sha256": "8ccfe21df7ab80ed872565bc879fb21a1b35ce81da55151035f2dc45cd40e749"
|
|
1776
|
+
},
|
|
1777
|
+
{
|
|
1778
|
+
"path": "docs/adr/0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md",
|
|
1779
|
+
"mode": "0000644",
|
|
1780
|
+
"sha256": "a36e7591da769ef46cb4f1c62d5b941278478dbb93e0a94c4f18e35a91866082"
|
|
1781
|
+
},
|
|
1772
1782
|
{
|
|
1773
1783
|
"path": "docs/adr/README.md",
|
|
1774
1784
|
"mode": "0000644",
|
|
1775
|
-
"sha256": "
|
|
1785
|
+
"sha256": "f3fac1ec223ee6804f1b0a47734f030cc5f97226efc17f7a618cb55099d8e642"
|
|
1776
1786
|
},
|
|
1777
1787
|
{
|
|
1778
1788
|
"path": "docs/api-reference.md",
|
|
1779
1789
|
"mode": "0000644",
|
|
1780
|
-
"sha256": "
|
|
1790
|
+
"sha256": "ae645afb5ce376b7e7b4ad0a5c00f63e48a2d0540fe79af29106d174a41ac3e8"
|
|
1781
1791
|
},
|
|
1782
1792
|
{
|
|
1783
1793
|
"path": "docs/api/openapi.json",
|
|
1784
1794
|
"mode": "0000644",
|
|
1785
|
-
"sha256": "
|
|
1795
|
+
"sha256": "a009aa37bc7698ac1469b45ac875b1b3884c0104b7f8a886e450082d503de45c"
|
|
1786
1796
|
},
|
|
1787
1797
|
{
|
|
1788
1798
|
"path": "docs/architecture.md",
|
|
@@ -1807,12 +1817,12 @@
|
|
|
1807
1817
|
{
|
|
1808
1818
|
"path": "docs/copy-inventory.md",
|
|
1809
1819
|
"mode": "0000644",
|
|
1810
|
-
"sha256": "
|
|
1820
|
+
"sha256": "375801be3c0c325da208e3f95fb6aa04e5a0136297673a562d143f61e4ca4c83"
|
|
1811
1821
|
},
|
|
1812
1822
|
{
|
|
1813
1823
|
"path": "docs/copy-registry.json",
|
|
1814
1824
|
"mode": "0000644",
|
|
1815
|
-
"sha256": "
|
|
1825
|
+
"sha256": "5a2ff67ac4a1cf8edf959b02db2a1c226de24c16a8b7b98762cfc1863d1a8f52"
|
|
1816
1826
|
},
|
|
1817
1827
|
{
|
|
1818
1828
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2657,7 +2667,7 @@
|
|
|
2657
2667
|
{
|
|
2658
2668
|
"path": "docs/module-api-changelog.md",
|
|
2659
2669
|
"mode": "0000644",
|
|
2660
|
-
"sha256": "
|
|
2670
|
+
"sha256": "726abfc14ae798baa3c7b55513db2ac74471ec78cc079efcf401b7ba409d25be"
|
|
2661
2671
|
},
|
|
2662
2672
|
{
|
|
2663
2673
|
"path": "docs/modules-contract.md",
|
|
@@ -4057,7 +4067,7 @@
|
|
|
4057
4067
|
{
|
|
4058
4068
|
"path": "modules/dev-box/app/src/vendor/bongos-client.cjs",
|
|
4059
4069
|
"mode": "0000644",
|
|
4060
|
-
"sha256": "
|
|
4070
|
+
"sha256": "33fbbbaf2a4a939b7351a7aa25264ddd609c1298d9ba809372a162983afe5038"
|
|
4061
4071
|
},
|
|
4062
4072
|
{
|
|
4063
4073
|
"path": "modules/dev-box/app/test/unit.mjs",
|
|
@@ -5467,12 +5477,12 @@
|
|
|
5467
5477
|
{
|
|
5468
5478
|
"path": "modules/lifecycle/db-versions.js",
|
|
5469
5479
|
"mode": "0000644",
|
|
5470
|
-
"sha256": "
|
|
5480
|
+
"sha256": "9f777643c44ebeef8b668a8120bf74d369564f904b2cb1aff54711bd5825c9c5"
|
|
5471
5481
|
},
|
|
5472
5482
|
{
|
|
5473
5483
|
"path": "modules/lifecycle/db.js",
|
|
5474
5484
|
"mode": "0000644",
|
|
5475
|
-
"sha256": "
|
|
5485
|
+
"sha256": "c6ab8ace534687d1989e4b192809a373294e539f7fb8624f512fda4ceea884ba"
|
|
5476
5486
|
},
|
|
5477
5487
|
{
|
|
5478
5488
|
"path": "modules/lifecycle/dead-deps.js",
|
|
@@ -5704,10 +5714,15 @@
|
|
|
5704
5714
|
"mode": "0000644",
|
|
5705
5715
|
"sha256": "366cb9c60952539ab8a946f8cf757754163b6cd6764a6430777b4529c86c6a75"
|
|
5706
5716
|
},
|
|
5717
|
+
{
|
|
5718
|
+
"path": "modules/lifecycle/routes/version-route-authz.js",
|
|
5719
|
+
"mode": "0000644",
|
|
5720
|
+
"sha256": "5db4f94ca52f634f41cdb489572e43eb6785744220d2565a716cff47bfa72b30"
|
|
5721
|
+
},
|
|
5707
5722
|
{
|
|
5708
5723
|
"path": "modules/lifecycle/routes/versions.js",
|
|
5709
5724
|
"mode": "0000644",
|
|
5710
|
-
"sha256": "
|
|
5725
|
+
"sha256": "a825191c1ef406ec4c0eb9455df5b65c5ecf25e23d2b9a9f78eeeae424fa2bf6"
|
|
5711
5726
|
},
|
|
5712
5727
|
{
|
|
5713
5728
|
"path": "modules/lifecycle/routes/visuals.js",
|
|
@@ -5904,6 +5919,11 @@
|
|
|
5904
5919
|
"mode": "0000644",
|
|
5905
5920
|
"sha256": "2eb5577450d3bc1d67b7eab9a13256889aca945c6ac6a6a0ca919e195952b7a1"
|
|
5906
5921
|
},
|
|
5922
|
+
{
|
|
5923
|
+
"path": "modules/platform-identity/invite-suggestions.js",
|
|
5924
|
+
"mode": "0000644",
|
|
5925
|
+
"sha256": "4fc8c9642e415b3dd0bc69968f6870fa9239f67f5cd5e93a301a9d8b53bd5c61"
|
|
5926
|
+
},
|
|
5907
5927
|
{
|
|
5908
5928
|
"path": "modules/platform-identity/migrations/platform_identity_001_tables.sql",
|
|
5909
5929
|
"mode": "0000644",
|
|
@@ -6072,7 +6092,7 @@
|
|
|
6072
6092
|
{
|
|
6073
6093
|
"path": "modules/platform-identity/routes/sso.js",
|
|
6074
6094
|
"mode": "0000644",
|
|
6075
|
-
"sha256": "
|
|
6095
|
+
"sha256": "dbc8eeab6b1911b7fdc8b29e3d10871d355d2dead926a0742e9a816a72215dba"
|
|
6076
6096
|
},
|
|
6077
6097
|
{
|
|
6078
6098
|
"path": "modules/platform-identity/scouting-authz.js",
|
|
@@ -6217,7 +6237,7 @@
|
|
|
6217
6237
|
{
|
|
6218
6238
|
"path": "modules/provisioning/routes/provisioning.js",
|
|
6219
6239
|
"mode": "0000644",
|
|
6220
|
-
"sha256": "
|
|
6240
|
+
"sha256": "60c583e1ae977c85575709abf74af7d0dc5147788509f11d1b26cad459d7b4cf"
|
|
6221
6241
|
},
|
|
6222
6242
|
{
|
|
6223
6243
|
"path": "modules/provisioning/starter-bundles.js",
|
|
@@ -6472,7 +6492,7 @@
|
|
|
6472
6492
|
{
|
|
6473
6493
|
"path": "modules/public-landing/public/assets/cosmos.css",
|
|
6474
6494
|
"mode": "0000644",
|
|
6475
|
-
"sha256": "
|
|
6495
|
+
"sha256": "51dc6eac59a85e24336597e554e84c7e76294e389b443cc40dd4e75b1684079d"
|
|
6476
6496
|
},
|
|
6477
6497
|
{
|
|
6478
6498
|
"path": "modules/public-landing/public/assets/verro-medallion.png",
|
|
@@ -6732,7 +6752,7 @@
|
|
|
6732
6752
|
{
|
|
6733
6753
|
"path": "modules/public-landing/public/projects.html",
|
|
6734
6754
|
"mode": "0000644",
|
|
6735
|
-
"sha256": "
|
|
6755
|
+
"sha256": "edc38e4ae7859c1dfc5422a4a0351c10f31cf119cab878dae0e25461c6121578"
|
|
6736
6756
|
},
|
|
6737
6757
|
{
|
|
6738
6758
|
"path": "modules/public-landing/public/projects.probes.json",
|
|
@@ -7477,12 +7497,12 @@
|
|
|
7477
7497
|
{
|
|
7478
7498
|
"path": "package-lock.json",
|
|
7479
7499
|
"mode": "0000644",
|
|
7480
|
-
"sha256": "
|
|
7500
|
+
"sha256": "5c872f20ae1fd789c219dab32e15b3a7ca446d0451dc518709be7091f2d7e255"
|
|
7481
7501
|
},
|
|
7482
7502
|
{
|
|
7483
7503
|
"path": "package.json",
|
|
7484
7504
|
"mode": "0000644",
|
|
7485
|
-
"sha256": "
|
|
7505
|
+
"sha256": "cd8cd65764759527985342aae0c228171fb47bba1164fd66a3282b1774c56aa8"
|
|
7486
7506
|
},
|
|
7487
7507
|
{
|
|
7488
7508
|
"path": "public-docs/index.html",
|
|
@@ -9182,7 +9202,7 @@
|
|
|
9182
9202
|
{
|
|
9183
9203
|
"path": "src/module-api.js",
|
|
9184
9204
|
"mode": "0000644",
|
|
9185
|
-
"sha256": "
|
|
9205
|
+
"sha256": "9cdb44aae69870c64cfb5ae2121af4b87bb09b2aeb77cb48827a029482b26862"
|
|
9186
9206
|
},
|
|
9187
9207
|
{
|
|
9188
9208
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11304,6 +11324,11 @@
|
|
|
11304
11324
|
"mode": "0000644",
|
|
11305
11325
|
"sha256": "f4da7e663ab6d7b93bb02ecefd18178fa654a314da07108c4316317f50115ad0"
|
|
11306
11326
|
},
|
|
11327
|
+
{
|
|
11328
|
+
"path": "tests/one_planning_version.mjs",
|
|
11329
|
+
"mode": "0000644",
|
|
11330
|
+
"sha256": "1f8c6924dac2c922a1f88c22693b4386ec16d5f0ae346609b3d1dc0003308a70"
|
|
11331
|
+
},
|
|
11307
11332
|
{
|
|
11308
11333
|
"path": "tests/orphaned_claim_ship.mjs",
|
|
11309
11334
|
"mode": "0000644",
|
|
@@ -11469,10 +11494,15 @@
|
|
|
11469
11494
|
"mode": "0000644",
|
|
11470
11495
|
"sha256": "743436b1191353bf687d8fb553d423a6003707a25a548367644928478569f171"
|
|
11471
11496
|
},
|
|
11497
|
+
{
|
|
11498
|
+
"path": "tests/project_invite_suggestions.mjs",
|
|
11499
|
+
"mode": "0000644",
|
|
11500
|
+
"sha256": "df1c61d6db69675d979b2a053e8771a977064283280a5cc04acddb25c801def4"
|
|
11501
|
+
},
|
|
11472
11502
|
{
|
|
11473
11503
|
"path": "tests/project_invite_ui.mjs",
|
|
11474
11504
|
"mode": "0000644",
|
|
11475
|
-
"sha256": "
|
|
11505
|
+
"sha256": "36a7ff3e5b8c4b9edd495d8a7e5b50dcecded8e608f148a62b4e37841c0eb36a"
|
|
11476
11506
|
},
|
|
11477
11507
|
{
|
|
11478
11508
|
"path": "tests/project_join_door.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
|
+
- 357 operations across 55 resource groups
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -677,6 +677,8 @@ function createClient(opts = {}) {
|
|
|
677
677
|
postProvisioningInstancesIdForceTeardown: (args) => request("POST", "/provisioning/instances/{id}/force-teardown", { hasBody: true }, args),
|
|
678
678
|
// POST /provisioning/instances/{id}/github-app — rank: any-builder — POST /provisioning/instances/:id/github-app
|
|
679
679
|
postProvisioningInstancesIdGithubApp: (args) => request("POST", "/provisioning/instances/{id}/github-app", { hasBody: true }, args),
|
|
680
|
+
// GET /provisioning/instances/{id}/invite-suggestions — rank: any-builder — GET /provisioning/instances/:id/invite-suggestions
|
|
681
|
+
getProvisioningInstancesIdInviteSuggestions: (args) => request("GET", "/provisioning/instances/{id}/invite-suggestions", { hasBody: false }, args),
|
|
680
682
|
// POST /provisioning/instances/{id}/restart — rank: any-builder — POST /provisioning/instances/:id/restart
|
|
681
683
|
postProvisioningInstancesIdRestart: (args) => request("POST", "/provisioning/instances/{id}/restart", { hasBody: true }, args),
|
|
682
684
|
// GET /provisioning/instances/{id}/settings — rank: any-builder — GET /provisioning/instances/:id/settings
|
|
@@ -676,6 +676,8 @@ function createClient(opts = {}) {
|
|
|
676
676
|
postProvisioningInstancesIdForceTeardown: (args) => request("POST", "/provisioning/instances/{id}/force-teardown", { hasBody: true }, args),
|
|
677
677
|
// POST /provisioning/instances/{id}/github-app — rank: any-builder — POST /provisioning/instances/:id/github-app
|
|
678
678
|
postProvisioningInstancesIdGithubApp: (args) => request("POST", "/provisioning/instances/{id}/github-app", { hasBody: true }, args),
|
|
679
|
+
// GET /provisioning/instances/{id}/invite-suggestions — rank: any-builder — GET /provisioning/instances/:id/invite-suggestions
|
|
680
|
+
getProvisioningInstancesIdInviteSuggestions: (args) => request("GET", "/provisioning/instances/{id}/invite-suggestions", { hasBody: false }, args),
|
|
679
681
|
// POST /provisioning/instances/{id}/restart — rank: any-builder — POST /provisioning/instances/:id/restart
|
|
680
682
|
postProvisioningInstancesIdRestart: (args) => request("POST", "/provisioning/instances/{id}/restart", { hasBody: true }, args),
|
|
681
683
|
// GET /provisioning/instances/{id}/settings — rank: any-builder — GET /provisioning/instances/:id/settings
|
|
@@ -95,6 +95,7 @@ export interface GetProjectsResponse { projects: unknown }
|
|
|
95
95
|
export interface GetProvisioningFleetResponse { instances: unknown }
|
|
96
96
|
export interface GetProvisioningGithubAppStatusResponse { ok: boolean; status: unknown; ready: unknown; terminal: unknown; domain: unknown; instance_status: unknown; app_slug: unknown }
|
|
97
97
|
export interface GetProvisioningHealthResponse { module: unknown; ok: boolean; coreVersion: unknown }
|
|
98
|
+
export interface GetProvisioningInstancesIdInviteSuggestionsResponse { suggested: unknown; seeking: unknown; reason: unknown; unavailable?: unknown }
|
|
98
99
|
export interface GetProvisioningInstancesIdResponse { instance: unknown; open_intent: unknown; github_app: unknown }
|
|
99
100
|
export interface GetProvisioningInstancesIdSettingsResponse { ok: boolean; settings: unknown; planet: unknown; options: unknown }
|
|
100
101
|
export interface GetProvisioningInstancesResponse { instances: unknown }
|
|
@@ -1000,6 +1001,8 @@ export interface BongosClient {
|
|
|
1000
1001
|
postProvisioningInstancesIdForceTeardown(args?: RequestArgs): Promise<PostProvisioningInstancesIdForceTeardownResponse>;
|
|
1001
1002
|
/** POST /provisioning/instances/{id}/github-app — rank: any-builder */
|
|
1002
1003
|
postProvisioningInstancesIdGithubApp(args?: RequestArgs & { body?: PostProvisioningInstancesIdGithubAppRequest }): Promise<PostProvisioningInstancesIdGithubAppResponse>;
|
|
1004
|
+
/** GET /provisioning/instances/{id}/invite-suggestions — rank: any-builder */
|
|
1005
|
+
getProvisioningInstancesIdInviteSuggestions(args?: RequestArgs): Promise<GetProvisioningInstancesIdInviteSuggestionsResponse>;
|
|
1003
1006
|
/** POST /provisioning/instances/{id}/restart — rank: any-builder */
|
|
1004
1007
|
postProvisioningInstancesIdRestart(args?: RequestArgs): Promise<PostProvisioningInstancesIdRestartResponse>;
|
|
1005
1008
|
/** GET /provisioning/instances/{id}/settings — rank: any-builder */
|
|
@@ -673,6 +673,8 @@ export function createClient(opts = {}) {
|
|
|
673
673
|
postProvisioningInstancesIdForceTeardown: (args) => request("POST", "/provisioning/instances/{id}/force-teardown", { hasBody: true }, args),
|
|
674
674
|
// POST /provisioning/instances/{id}/github-app — rank: any-builder — POST /provisioning/instances/:id/github-app
|
|
675
675
|
postProvisioningInstancesIdGithubApp: (args) => request("POST", "/provisioning/instances/{id}/github-app", { hasBody: true }, args),
|
|
676
|
+
// GET /provisioning/instances/{id}/invite-suggestions — rank: any-builder — GET /provisioning/instances/:id/invite-suggestions
|
|
677
|
+
getProvisioningInstancesIdInviteSuggestions: (args) => request("GET", "/provisioning/instances/{id}/invite-suggestions", { hasBody: false }, args),
|
|
676
678
|
// POST /provisioning/instances/{id}/restart — rank: any-builder — POST /provisioning/instances/:id/restart
|
|
677
679
|
postProvisioningInstancesIdRestart: (args) => request("POST", "/provisioning/instances/{id}/restart", { hasBody: true }, args),
|
|
678
680
|
// GET /provisioning/instances/{id}/settings — rank: any-builder — GET /provisioning/instances/:id/settings
|
package/docs/adr/0251-a-suggestion-of-who-to-invite-is-a-recruiting-surface-and-owes-the-opt-out.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# ADR 0251 — A suggestion of who to invite is a recruiting surface, and owes the recruiting opt-out
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-09-06
|
|
5
|
+
- **Task:** [task 1002347](https://cloudbongos.com/builders#/task/1002347) (BONGOS-V1, goal 1000046 — *Project creation*, criterion `proj-invite-builders` / C6), BV1.R28
|
|
6
|
+
- **Deciders:** Claude, under the Archon's standing scope
|
|
7
|
+
- **Depends on:** [ADR 0239](<redacted>.md) §6 (the seam this fills: the engine emits `invites.seeking` and names R28 as its reader) · [ADR 0249](<redacted>.md) (the widget these names arrive in, and its "only door in" rule) · [ADR 0083](<redacted>.md) (the one-way import rule that makes this a port)
|
|
8
|
+
- **Related:** [ADR 0210](<redacted>.md) (the `/scouting` gate this route deliberately does **not** reuse) · [ADR 0248](<redacted>.md) (the ownership fact the invite route already admits on) · [ADR 0016](<redacted>.md) (a surface never re-derives a server rule)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Context
|
|
13
|
+
|
|
14
|
+
[ADR 0239](<redacted>.md) §6 left one recommendation deliberately unfinished. The creation wizard's rule engine emits, per team shape, a **seek** rather than a list:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
solo → seeking: null ("suggest nobody", a real answer)
|
|
18
|
+
small-team → seeking: { kind: 'top-builders', limit: 3 }
|
|
19
|
+
community → seeking: { kind: 'top-builders', limit: 5 }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`invites.suggested` is always `[]` there, and a test pins that emptiness as a decision rather than a gap. The engine is a pure file that may run no query; ranking builders is a directory read; and `provisioning` may not import a sibling module. So the engine states *what to look for* and R28 — this ADR — turns it into names.
|
|
23
|
+
|
|
24
|
+
That leaves one genuinely open question, and it is not an architectural one: **which directory?**
|
|
25
|
+
|
|
26
|
+
## The decision that matters: the recruiting directory, not the leaderboard
|
|
27
|
+
|
|
28
|
+
Two readers could answer "who are the top builders", and they differ by exactly one SQL predicate:
|
|
29
|
+
|
|
30
|
+
| reader | lives in | consent |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `leaderboard()` | `economy` | joins `rollupVisibleSql` — **honours no recruiting opt-out**, on purpose |
|
|
33
|
+
| `listScoutingRollup()` | `platform-identity` | joins `scoutingListableSql` — **the opt-out is inside the query** |
|
|
34
|
+
|
|
35
|
+
The leaderboard is the cheaper answer in every mechanical sense: it is an explicitly public read route, it needs no port, and a browser could call it directly with no new server code at all.
|
|
36
|
+
|
|
37
|
+
**We use the scouting rollup anyway, because the opt-out means this exact screen.** `account-visibility.js` states the split in its own words — `hide_stats` suppresses *numbers* everywhere, while the recruiting opt-out "removes an account from the RECRUITING SURFACE only", and the community spec's D3 says the opt-out "does not affect leaderboards; it governs the recruiting surface only". A list of people the platform is proposing you recruit into your project **is** the recruiting surface. Sourcing it from the leaderboard would not be a shortcut around some incidental gate; it would route around the one control that exists for precisely this case, and hand an owner the name of every builder who asked not to be recruited.
|
|
38
|
+
|
|
39
|
+
The rule this fixes for later readers: **the cheapness of a public read is not an argument about who consented to appear in it.** `listScoutingRollup`'s own header already warns in the other direction ("a public LEADERBOARD must not reuse this reader"); this is the same boundary approached from the other side.
|
|
40
|
+
|
|
41
|
+
## Where the lookup lives, and why it is a port
|
|
42
|
+
|
|
43
|
+
`modules/platform-identity` gets a new function, `listInviteSuggestions`, exposed on the existing `platform-identity.hub` port. `modules/provisioning` reaches it with `resolveOptional` — the `catalog-bridge.js` precedent, and **not** declared in `consumes`: platform-identity is hub-only (`default: false`), so an instance running provisioning without it has no directory to ask and the honest answer is "no suggestions", never a 500.
|
|
44
|
+
|
|
45
|
+
Two things are enforced inside the module rather than asked of callers:
|
|
46
|
+
|
|
47
|
+
1. **`github_id` does not cross the port.** The projection is an explicit whitelist — login, display name, avatar, disciplines, and two counts. The invite route takes a *login*; the id names someone who has accepted nothing, and the surface already has a test forbidding it on screen. An explicit whitelist also means a new column on the rollup cannot leak by default.
|
|
48
|
+
2. **The exclusion is applied inside.** A caller passes the ids to drop (in practice the owner's own) and gets back rows with no ids in them, so dropping yourself from your own list never requires holding one. The read is over-fetched by the exclusion count so filtering can never return short of the limit.
|
|
49
|
+
|
|
50
|
+
## The gate is the one that already exists
|
|
51
|
+
|
|
52
|
+
`GET /provisioning/instances/:id/invite-suggestions` is own-scoped by `instanceAccessDenied` — the same gate, and the same **404**, as the manage-page read beside it.
|
|
53
|
+
|
|
54
|
+
It deliberately does **not** reuse `/scouting`'s gate (`project.curate` or a hub-witnessed `membership_kind='owner'`). A brand-new owner has no witnessed owner-membership row at the moment the done panel paints — that row is written later, by the child instance at SSO check-in — so the correct-looking gate would refuse the exact caller the feature exists for.
|
|
55
|
+
|
|
56
|
+
Reaching this route **is** the ownership answer ([ADR 0249](<redacted>.md) §4), so the surface re-derives nothing and cannot become a weaker second copy of a server rule ([ADR 0016](<redacted>.md)). 404 rather than 403 keeps it leaking no more instance existence than its neighbour.
|
|
57
|
+
|
|
58
|
+
## What the surface does with the answer
|
|
59
|
+
|
|
60
|
+
The widget keeps its shape from [ADR 0249](<redacted>.md): it still answers neither "is anyone signed in" nor "is this their project", and gains a **second door in** (`setInviteSuggestions`) beside `setInviteSlug` rather than learning to fetch for itself.
|
|
61
|
+
|
|
62
|
+
- **The reason is always said, including when the answer is nobody.** `seeking: null` returns its own sentence and never touches the directory. A solo owner is suggested nobody *on purpose*, and rendering that as an empty list would turn a stated answer into what looks like a failed lookup.
|
|
63
|
+
- **A picked name takes the same write path as a typed one** — it fills the field and calls `doInvite`. One write path means the refusal vocabulary, the busy guard and the pending receipt cannot drift into a second copy.
|
|
64
|
+
- **A filed invite stops being a suggestion.** The pending line is its receipt; offering the same person twice invites a refusal the owner cannot act on.
|
|
65
|
+
- **Every failure is silent.** A suggestion is an extra, and a project that was just created successfully must never look broken because an optional directory read did not answer.
|
|
66
|
+
|
|
67
|
+
## Consequences
|
|
68
|
+
|
|
69
|
+
- A builder who opted out of recruiting is **absent from suggestions while still ranking on the leaderboard**. That is the opt-out working, not a bug — but it does mean the suggested names and the leaderboard's top names legitimately differ, and anyone comparing the two screens should expect that.
|
|
70
|
+
- Suggestions are **hub-only**. An instance without `platform-identity` gets `unavailable: 'no_directory'` and an empty list; the step still works for typed logins.
|
|
71
|
+
- `invites.suggested` stays `[]` in the pure engine forever. The filling happens one layer out, so ADR 0239's test still passes unchanged and the engine stays I/O-free.
|
|
72
|
+
- Guilds are still out (task 1002348, blocked on goal 1000045). `seeking.kind` is already a discriminator, so a `guilds` kind is an added branch rather than a reshaping.
|
|
73
|
+
|
|
74
|
+
## Rejected
|
|
75
|
+
|
|
76
|
+
- **Read the economy leaderboard from the browser.** Cheapest by far and needs no server code — rejected on consent, above. This is the whole decision.
|
|
77
|
+
- **Reuse `GET /scouting`.** The right data behind a gate that refuses the caller this feature is for.
|
|
78
|
+
- **Fill `invites.suggested` inside `recommendationsForDetails`.** Would make a pure, synchronous, dependency-free file do I/O and import a sibling module — the two things ADR 0239 §6 and ADR 0083 respectively forbid.
|
|
79
|
+
- **A `provisioning → economy` port for the leaderboard.** Architecturally legal and still the wrong directory; the boundary was never the objection.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# ADR 0252 — The self-hosted visibility report is a PULL, and auto-appear buys a retraction lever
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-09-06
|
|
5
|
+
- **Task:** [task 1002321](https://cloudbongos.com/builders#/task/1002321) (BONGOS-V1, goal 1000046 — *Project creation*, criterion `proj-visibility-states` / C1), BV1.R02 — a spike whose output is this decision
|
|
6
|
+
- **Gates:** [task 1002327](https://cloudbongos.com/builders#/task/1002327) (BV1.R08 — the build)
|
|
7
|
+
- **Deciders:** the Archon (the two product calls in §3 and §5), Claude (the mechanism)
|
|
8
|
+
- **Depends on:** [ADR 0182](<redacted>.md) D3/D5 (the two axes, and the hand-off that named this ADR's scope) · [ADR 0238](<redacted>.md) (the owner's door is a provisioning settings key; the hub column stays the cache) · [ADR 0246](<redacted>.md) (the control-plane projection, and the line this ADR sits on)
|
|
9
|
+
- **Related:** [ADR 0144](<redacted>.md) (the tripwire this route avoids) · [ADR 0145](<redacted>.md) (why `standalone` holds no credential) · [ADR 0205](<redacted>.md) (what a self-report may and may not buy) · [ADR 0047](<redacted>.md) (the straight-through path and its abuse guard are one task) · [ADR 0199](<redacted>.md) / [ADR 0226](<redacted>.md) (a degraded report is not a state change)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. The problem, and a premise that turned out to be false
|
|
14
|
+
|
|
15
|
+
BV1.R02 was written to design "how a self-hosted instance reports its visibility to the hub, and how far the hub trusts it," and to settle the **auto-appear vs. Archon-curate** question the planning interview deferred.
|
|
16
|
+
|
|
17
|
+
Reading the tree first changed the question. **A self-hosted instance has no channel to the hub at all, and no credential with which to open one.** `registerClient` has exactly one caller in the repository — `scripts/gds/provision-net.js:84`, inside the operator's own control-plane `federateInstance` — and that call is gated:
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
if (inst.hosting_shape !== 'co-tenant') return { federate: false, reason: … };
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Every writer of `sso_clients` is control-plane; no route registers a client. So *federated* ⊆ *operator-provisioned co-tenant*, and *self-hosted* is a disjoint set holding nothing. That is deliberate on two counts: [ADR 0145](<redacted>.md) says in as many words **do not** extend federation to `standalone` as it exists today, and [ADR 0144](<redacted>.md)'s Known Limitation rests on it — *"today every secret-holder is an operator-provisioned, trusted instance … the proper fix must land before federation opens to untrusted third-party clients."*
|
|
24
|
+
|
|
25
|
+
So the literal reading of R08 — "a self-hosted instance authenticates and pushes a report" — cannot be built without first minting credentials for untrusted third parties, which walks straight into that tripwire. [ADR 0182](<redacted>.md) D5's phrasing ("accepts a report from the authenticated `sso_client`") described a client that, for this population, does not exist.
|
|
26
|
+
|
|
27
|
+
## 2. The channel is a PULL of the instance's own public manifest
|
|
28
|
+
|
|
29
|
+
**Decision.** The hub does not receive a pushed report. It **reads the instance's already-public declaration**: `GET <origin>/api/gds/instance` → `branding.project.visibility`.
|
|
30
|
+
|
|
31
|
+
That value is already there and already public. The owner's pick lives at `provisioning_instances.settings.visibility`, is handed to the instance as `<PREFIX>_VISIBILITY`, and the instance re-serves it on a CORS-open route (`src/branding.js`, `src/bongos/routes/instance.js`). The comment that put it there already anticipated this consumer: *"which is what lets a later task enforce the door there and report the value to the hub catalog (R08)."*
|
|
32
|
+
|
|
33
|
+
Five things a pull gets for free, each of which a push would have had to build:
|
|
34
|
+
|
|
35
|
+
1. **No credential.** Nothing is minted for anyone, so ADR 0144's tripwire is untouched and ADR 0145's rule holds. This is the whole reason the decision goes this way.
|
|
36
|
+
2. **No replay surface.** ADR 0182 D5 handed R02 "replay, rate" as its scope. A pull reads *current state*, so there is no message to capture and resend — the `jti`-style machinery [ADR 0144](<redacted>.md) D6 needed for the push direction is moot here. Replay is answered by not having a message.
|
|
37
|
+
3. **No wire change on anyone else's upgrade clock.** [ADR 0205](<redacted>.md) rejected adding a required body field precisely because "the callers are other instances on their own upgrade schedule," and [ADR 0201](<redacted>.md) D2.2 solved the same problem with a header. Here the field already ships in every core; nothing has to be added at all.
|
|
38
|
+
4. **The hub still authorizes nothing.** It reads a declaration and caches it, which is exactly what [ADR 0182](<redacted>.md) D5 requires and what [ADR 0238](<redacted>.md) rejected a `PATCH /projects/:id/visibility` for failing.
|
|
39
|
+
5. **The fetch is already written.** `modules/platform-identity/project-stats.js` fans out to project origins today behind a real SSRF guard — DNS-resolve-all with a public-address assertion, `redirect: 'error'` (a curated origin is trusted, a redirect target is not), per-request timeouts, a TTL cache, a negative cache and inflight collapsing. The visibility read rides that machinery rather than opening a second, weaker outbound path.
|
|
40
|
+
|
|
41
|
+
**The pull carries `visibility` and nothing else.** It is a three-value closed enum; an absent or off-vocabulary answer yields *no verdict* (§4), never a default. No free text, no name, no tagline, no art reaches the catalog through this channel — those stay curated fields, so the property the map has today (a project owner controls no free-text field that reaches the feed) is not spent here.
|
|
42
|
+
|
|
43
|
+
## 3. Auto-appear: no human stands between a public project and the map
|
|
44
|
+
|
|
45
|
+
**Decision (the Archon's).** A self-hosted project reporting `public` appears on the map with **no human act**. `featured` keeps the only job [ADR 0182](<redacted>.md) D3 left it — ranking among rows already there — and curation stays editorial.
|
|
46
|
+
|
|
47
|
+
The alternative (a one-time admission, or ongoing curation) was offered and declined. What was declined with it is the friction ADR 0182 D3 had already removed once: an owner setting their project public and seeing nothing happen until someone else acted.
|
|
48
|
+
|
|
49
|
+
**This decision is conditional, and the condition is §5.** Auto-appear is only defensible because retraction ships beside it; [ADR 0047](<redacted>.md) is explicit that the straight-through path and the abuse guard are the *same* task, and this ADR treats that as binding rather than advisory.
|
|
50
|
+
|
|
51
|
+
## 4. Trust follows provenance — the asymmetry is in the guards, not in a gate
|
|
52
|
+
|
|
53
|
+
**Decision (the Archon's).** Hosted and self-hosted projects are **not** held to the same bar. Both auto-appear; what differs is what each is trusted to *say*, because the evidence behind each differs:
|
|
54
|
+
|
|
55
|
+
| | control-plane / co-tenant | self-hosted |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| how `visibility` arrives | **assigned** by the bridge from `provisioning_instances.settings` | **pulled** from a manifest the hub does not control |
|
|
58
|
+
| evidence | an own-scoped, vocab-validated, audited route the operator's own plane ran | someone pointed a domain at the hub |
|
|
59
|
+
| status of the value | authoritative | a **cached claim, with a recency stamp** |
|
|
60
|
+
| may move | its own row | its own row, and only its own row |
|
|
61
|
+
|
|
62
|
+
Stated once, so it is not re-litigated: **an instance is authoritative about its own door and about nothing else.** That is [ADR 0016](<redacted>.md) and [ADR 0141](<redacted>.md)'s line held in a new place — identity and self-description federate, authority does not.
|
|
63
|
+
|
|
64
|
+
### Absent is a third state, and it must not purge
|
|
65
|
+
|
|
66
|
+
[ADR 0182](<redacted>.md) D5 says the absent-report default is fail-closed: unreported ⇒ dark matter. That stands, **scoped to a self-hosted row that has never once answered** — such a project is off the map rather than exposed by omission.
|
|
67
|
+
|
|
68
|
+
It must never be evaluated against control-plane rows, and it must never be read as "answered before, silent now." [ADR 0247](<redacted>.md) D4 found the live shape of this trap: at the time of writing, *every* catalog row carried `description: null`, so a naive fail-closed enforcement wired that day would have refused every join on the platform at once. The same hazard applies here.
|
|
69
|
+
|
|
70
|
+
So the tri-state is explicit — `null` (never read) ≠ `false` (read, and negative) ≠ `true`:
|
|
71
|
+
|
|
72
|
+
- **never successfully read** → dark matter (D5's rule, doing its job)
|
|
73
|
+
- **read before, unreachable now** → **keep the last known state, stamped with its recency.** A project does not leave the map because its server was down for a sweep. [ADR 0199](<redacted>.md): liveness is an overlay, never a lifecycle state. [ADR 0226](<redacted>.md): reporting may degrade where enforcement must refuse — and this is reporting.
|
|
74
|
+
- **read, and narrower than before** → applied immediately and unconditionally. A report that *reduces* exposure needs no trust at all: the worst a lying instance achieves is hiding itself.
|
|
75
|
+
|
|
76
|
+
That last line is the shape of the whole trust model. **Narrowing is free; widening is what costs.**
|
|
77
|
+
|
|
78
|
+
## 5. Auto-appear buys a retraction lever — and `hidden` is sticky
|
|
79
|
+
|
|
80
|
+
`platform_identity_projects.status` is `('active','hidden')` and the public feed requires `active`. **No route writes it.** The only `status` write in the module is on `sso_clients`. Today, removing a project from the map is hand-written SQL on production.
|
|
81
|
+
|
|
82
|
+
Under control-plane provisioning that was survivable, because the set of rows was bounded by who the operator had provisioned. Auto-appear replaces that bound with *whoever points a domain at us*, and a one-way door onto a public surface is not a door. So R08 ships, in the same task:
|
|
83
|
+
|
|
84
|
+
1. **`PATCH /projects/:id/status`** — `active|hidden`, `project.curate` floor (metic, per [ADR 0157](<redacted>.md)), audited.
|
|
85
|
+
2. **`hidden` is STICKY against the pull.** A retracted project that keeps reporting `public` **stays hidden**. This is the single most important line in this ADR: without it the next sweep silently undoes every takedown, and the lever is decorative. Retraction is a hub decision about the hub's own surface, and the pull may not overwrite it — the pull moves `visibility`, never `status`. The two columns answer different questions and this is where that separation earns its keep.
|
|
86
|
+
3. **A per-IP read limit on the public feed** (the [ADR 0209](<redacted>.md) precedent). `GET /projects/featured` is unauthenticated, CORS-open and currently unmetered, and each cold sweep can drive up to 500 rows × 4 outbound fetches.
|
|
87
|
+
|
|
88
|
+
### One gap this decision widens, named rather than left
|
|
89
|
+
|
|
90
|
+
`hall_origin` reaches the public feed from the remote instance's own discovery index, and the hub relays it into an `<a href>`. Its only check is that it *differs* from the project's site host — the opposite of a containment rule. Under curated provenance that was survivable; under auto-appear it is a phishing hop anyone can mint.
|
|
91
|
+
|
|
92
|
+
The corpus already has the right rule and applies it one field over: `shipUrlWithinClientOrigin` requires a pushed ship link to point **at or below** the reporting instance, and explains why the mirror rule is not symmetric (on a shared apex, `foo.github.io` must not publish links on bare `github.io`). **`hall_origin` gets the same downward-only containment**, filed as its own task against R08.
|
|
93
|
+
|
|
94
|
+
## 6. Consequences
|
|
95
|
+
|
|
96
|
+
- **A self-hosted project can put itself on the public map, and that is now a supported, un-gated path.** The hub will also, once the SEO work lands, serve it an indexable `cloudbongos.com/<slug>` page — so auto-appear is also auto-issue of a page on the hub's own domain. Accepted knowingly; §5 is the counterweight.
|
|
97
|
+
- **The hub reaches out to more origins.** The fan-out ceiling is unchanged (500 rows, TTL + negative caches), but the *population* is no longer bounded by provisioning. The feed read limit exists for this.
|
|
98
|
+
- **Two populations, one column, different provenance** — readers of `platform_identity_projects.visibility` cannot assume how it got there. The recency stamp is what tells them.
|
|
99
|
+
- **A self-hosted instance that never upgrades never appears.** It already serves `branding.project.visibility`, so in practice this is only the very old.
|
|
100
|
+
- **Nothing here federates a self-hosted instance.** It holds no credential, mints no assertion, writes no membership, and reports no analytics. The later analytics feed R02 was asked to anticipate is deliberately *not* decided here: it carries per-person data, which is exactly what [ADR 0205](<redacted>.md) proved cannot ride an unwitnessed self-report. It needs the ADR 0144 credential work first, and it is a separate decision.
|
|
101
|
+
|
|
102
|
+
## 7. Rejected
|
|
103
|
+
|
|
104
|
+
- **A pushed report authenticated by a client credential.** The literal reading of R08. Rejected: self-hosted instances hold no credential, and minting them is the exact move [ADR 0144](<redacted>.md) and [ADR 0145](<redacted>.md) say must not happen yet. It would also have owed replay protection, a rate budget and a wire-compatible field — three problems the pull does not have.
|
|
105
|
+
- **`PATCH /projects/:id/visibility` on the hub.** Already rejected by [ADR 0238](<redacted>.md) for making the hub authorize a door change; restated here because a pull could be mistaken for a licence to add one.
|
|
106
|
+
- **A one-time admission, and ongoing curation.** Both offered to the Archon; both declined in favour of auto-appear (§3).
|
|
107
|
+
- **Trusting the pull to move `status`.** The retraction lever would be decorative (§5.2).
|
|
108
|
+
- **Enforcing "unreported ⇒ dark matter" across all rows.** [ADR 0247](<redacted>.md) D4's live finding: it empties the map (§4).
|
|
109
|
+
- **Letting the pull carry name, tagline or art.** Today no project owner controls a free-text field that reaches the feed. That is a property worth keeping, and a visibility report is not the place to spend it.
|
package/docs/adr/README.md
CHANGED
|
@@ -338,6 +338,8 @@ This keeps the decision history honest and traceable.
|
|
|
338
338
|
|
|
339
339
|
| 0249 | [**The invite step is the done panel’s first act, not a rail step — and the same widget is the way back** ([task 1002346](https://cloudbongos.com/builders#/task/1002346) · goal 1000046, criterion `proj-invite-builders` / C6). BV1.R27 asks for a skippable invite step *in creation*, but an invite cannot be one of the wizard’s numbered panels: `POST /projects/invite` takes `{ client_id, github_login }` where `client_id` **is the slug**, and per [ADR 0246](<redacted>.md) the catalog row that slug resolves is projected **at create** — so before the Create step there is nothing to invite anyone to, and a pre-Create panel could only queue names and replay them, inventing a second unaudited invite path whose failures land after the owner has left. **Decision: the step is the FIRST act of the DONE panel**, keyed on the created record (not `instanceId`, which `startProgress` sets only *after* `renderDone`), so it also returns on a resumed watch; the rail is NOT renumbered (a captured learning on this file: a renumbering couples in four places, and a step that can only run after the terminal step is not a step in the rail). The SAME widget is mounted again on the project’s own page beside *Its extras* — the [ADR 0245](<redacted>.md) argument, one implementation and two mounts, so the refusal vocabulary cannot drift. The widget answers neither “is anyone signed in” nor “is this their project”: each call site hands over a slug or nothing (the created record; the own-scoped manage read that 404s for everyone else), because a page that re-derived ownership would be a second, weaker copy of a server rule. **The 403 is quoted carefully and that is the point** — [ADR 0248](<redacted>.md) paid two equalised reads per request to make the refusal identical for a real, stealth, absent or malformed target ([ADR 0182](<redacted>.md) D2a), and “you lack permission on **this project**” would assert in copy the existence the route refuses to confirm, so the page says only “this account can’t invite people here” and a test pins that wording. `account_not_found` and `unknown_project` are said in the route’s own words (the latter as a moment, not a dead end — the row is projected at create); anything else falls through to the server’s own message. The pending line is drawn from the 201 row’s `membership_kind` and the typed login — **never the `github_id`** it carries, which names someone who has accepted nothing yet. Creation is never blocked: the project is filed before the step is shown, so every refusal leaves it created and the step re-tryable, and Skip sends nothing. Rejected: a rail step before or after Create; the manage page alone (R27 asks for it *in creation*); reading the real membership directory back (no own-scoped route exists, and the session receipt is honest about being one).](<redacted>.md) | provisioning / project creation |
|
|
340
340
|
| 0250 | [**Strict versioning — the version boundary is the scope gate** ([task 1003589](https://cloudbongos.com/builders#/task/1003589) · goal 1000086). The work hierarchy ([ADR 0086](<redacted>.md)) has a bottom and no top: tasks close, criteria close, goals close, and a version never does. Measured live — **14 goals reading `achieved`/`archived` while holding 35 unfinished tasks**, 523 open tasks across 37 open goals, and **two versions `building` on one project**; every number was worse than the planning session had recorded two days earlier, which is the argument: the drift is the steady state, not a backlog. The cause is four mechanical gaps, not a social one — `POST /goals` fetches the version row and never reads its `status`; goal achievement is computed from criteria alone and never consults `tasks` (both the `db-goals.js` writer and the `done-when.js` cascade); `POST /goals/:id/archive` checks nothing at all; and no route ships a version, so version status has only ever moved as hand-run SQL on the droplet. **Decision, five rules:** the goal set closes when a version starts `building` (new goals go on the `planning` version — where the next version is scoped in advance) with a named 409 an agent can act on; a goal reaches `achieved` only when its criteria are satisfied **and** no task in it is non-terminal, where terminal is `shipped`|`abandoned` per [task 1003588](https://cloudbongos.com/builders#/task/1003588)'s one vocabulary and the mid-ship `completed`/`confirmed` hold it open; archiving a goal that holds open tasks refuses until the caller dispositions each (abandon with the reason, or move to a named surviving goal); `goal_id` becomes required and the per-version catch-all fallback is deleted; and exactly one version builds per project, auto-closing when its last non-maintenance goal achieves, with the single `planning` version promoting itself so the project is never without a live train. **The escape hatches are deliberate and visible**: an Archon may admit one *named* goal to a building version with a reason, and the overrides are counted on the version (the standard failure of a hard scope gate is a fake hotfix version cut to hold what did not fit — an untraced override is just a slower version of no rule); plus one auto-created maintenance goal per version, exempt from the auto-close count, so bugs never need an override to be filed. **Nothing here can refuse a ship, a claim, or a task** — every rule acts on a *closing* move. Rejected: counting `completed`/`confirmed` as finished (they can fail a grade or strand — the goals **board** buckets them as done, but that is a progress ring, and `task-status.js` keeps the two questions apart); a nightly reconciler that reopens wrongly-closed goals (detection is what you build when prevention is unavailable, and it is available here); a human review gate before a goal may close (the queue [ADR 0183](<redacted>.md) dismantled — the check is a fact the table already holds); concurrent building versions scoped by module (two live trains means every "what is in this version" question has two answers); retiring versions entirely (the version is the unit the owner sets scope in and `limitations/<version>-shipped.md` archives). **Amends [ADR 0157](<redacted>.md) for exactly one key:** `version.create` returns to the Archon floor. 0157's "authoring delegates, disposition does not" was right about what a version *was* — but once the boundary carries the scope gate, cutting a new version is no longer authoring, it is the way *around* the gate, so create joins close as a disposition over scope.](<redacted>.md) | lifecycle / versioning / scope |
|
|
341
|
+
| 0251 | [**A suggestion of who to invite is a recruiting surface, and owes the recruiting opt-out** ([task 1002347](https://cloudbongos.com/builders#/task/1002347) · goal 1000046, criterion `proj-invite-builders` / C6). BV1.R28 fills the seam [ADR 0239](<redacted>.md) §6 left open: the wizard’s pure rule engine emits a **seek** (`solo` → `null`, `small-team` → 3, `community` → 5) and never a list, because ranking builders is a directory read a pure file may not run and `provisioning` may not import a sibling. Two readers could answer “who are the top builders” and they differ by **one SQL predicate**: `economy`’s `leaderboard()` joins `rollupVisibleSql` and deliberately honours **no** recruiting opt-out, while `platform-identity`’s `listScoutingRollup()` joins `scoutingListableSql`, which carries it. The leaderboard is cheaper in every mechanical sense — already a public route, no port, callable straight from the browser. **Decision: use the recruiting directory anyway**, because `account-visibility.js` says the split in its own words (`hide_stats` suppresses *numbers* everywhere; the recruiting opt-out “removes an account from the RECRUITING SURFACE only”, community spec D3), and a list of people the platform proposes you recruit **is** that surface — sourcing it from the leaderboard would route around the one control built for this exact case and name every builder who asked not to be recruited. The rule for later readers: **the cheapness of a public read is not an argument about who consented to appear in it.** The lookup ships as `listInviteSuggestions` on the existing `platform-identity.hub` port, reached with `resolveOptional` and NOT declared in `consumes` (the `catalog-bridge.js` precedent — platform-identity is hub-only, so its absence is “no suggestions”, never a 500); `github_id` **does not cross the port** (explicit whitelist — login, display name, avatar, disciplines, two counts — so a new rollup column cannot leak by default), and the owner-exclusion is applied inside, over-read by the exclusion count so filtering never returns short. The route `GET /provisioning/instances/:id/invite-suggestions` is own-scoped by the **existing** `instanceAccessDenied` (same 404 as the manage read) and deliberately does NOT reuse `/scouting`’s gate ([ADR 0210](<redacted>.md)): a brand-new owner has no hub-witnessed owner row at the moment the done panel paints, so the correct-looking gate would refuse the exact caller the feature exists for — and reaching the route IS the ownership answer ([ADR 0249](<redacted>.md) §4). On the surface: a **second door in** (`setInviteSuggestions`) beside `setInviteSlug`, so the widget still answers neither “signed in” nor “theirs”; the reason is **always** said including when the answer is nobody (`seeking: null` costs no query — rendering a stated answer as an empty list would make it look like a failed lookup); a picked name fills the field and takes the **same write path** as a typed one, so the refusal vocabulary cannot fork; a filed invite stops being a suggestion; and every failure is silent, because a suggestion is an extra and a just-created project must not look broken over one. Rejected: the browser reading the economy leaderboard (the whole decision — consent, not architecture); reusing `/scouting` (right data, gate that refuses this caller); filling `invites.suggested` inside the engine (I/O in a pure file + a sibling import); a `provisioning → economy` port (legal, still the wrong directory — the boundary was never the objection).](<redacted>.md) | provisioning / platform-identity / privacy |
|
|
342
|
+
| 0252 | [**The self-hosted visibility report is a PULL, and auto-appear buys a retraction lever** ([task 1002321](https://cloudbongos.com/builders#/task/1002321) · goal 1000046, criterion `proj-visibility-states` / C1, BV1.R02 — a spike whose output is this decision; gates [task 1002327](https://cloudbongos.com/builders#/task/1002327) / R08). **The premise failed first:** a self-hosted instance has no channel to the hub and no credential to open one — `registerClient` has exactly one caller in the tree (the operator's own `provision-net.js`), gated `hosting_shape !== 'co-tenant'`, and no route registers a client. So *federated* ⊆ *operator-provisioned co-tenant* and *self-hosted* holds nothing, deliberately: [ADR 0145](<redacted>.md) says do NOT extend federation to `standalone`, and [ADR 0144](<redacted>.md)'s Known Limitation rests on "every secret-holder is an operator-provisioned, trusted instance … the proper fix must land before federation opens to untrusted third-party clients." [ADR 0182](<redacted>.md) D5's "accepts a report from the authenticated `sso_client`" described a client that, for this population, does not exist. **Decision 1 — the channel is a PULL of the instance's already-public manifest** (`GET <origin>/api/gds/instance` → `branding.project.visibility`, a value every core already serves and whose author anticipated this consumer). It buys five things a push must build: no credential (0144's tripwire untouched — the whole reason), **no replay surface** (a pull reads current state, so the `jti` machinery D5 handed R02 is moot — replay is answered by not having a message), no wire change on another instance's upgrade clock ([ADR 0205](<redacted>.md)'s and [ADR 0201](<redacted>.md) D2.2's rejected-required-field problem simply absent), the hub still authorising nothing (0182 D5 satisfied, [ADR 0238](<redacted>.md)'s rejected `PATCH /projects/:id/visibility` still rejected), and a fetch that already exists behind a real SSRF guard (`project-stats.js` — DNS-resolve-all, `redirect: 'error'`, TTL + negative caches). The pull carries `visibility` and NOTHING else, so the property the map has today — a project owner controls no free-text field that reaches the feed — is not spent. **Decision 2 (the Archon's) — auto-appear:** no human stands between a public project and the map; `featured` keeps only the ranking job [ADR 0182](<redacted>.md) D3 left it. Conditional on Decision 4. **Decision 3 (the Archon's) — trust follows provenance:** both populations auto-appear, and the asymmetry is in the GUARDS, not a gate — a control-plane row's `visibility` is *assigned* by the bridge from an own-scoped audited route (authoritative), a self-hosted row's is *pulled* from a manifest the hub does not control (a cached claim with a recency stamp, able to move its own row and nothing else). An instance is authoritative about its own door and about nothing else. **Absent is a THIRD state and must not purge:** `null` (never read) ≠ `false` (read, negative). 0182 D5's fail-closed default holds only for a self-hosted row that has NEVER answered; a row that answered before and is unreachable now keeps its last state with a recency stamp ([ADR 0199](<redacted>.md), [ADR 0226](<redacted>.md)), because [ADR 0247](<redacted>.md) D4 found the live shape of that trap — every catalog row at `description: null`, so naive fail-closed would have emptied the map at once. Narrowing is free; widening is what costs. **Decision 4 — auto-appear buys a retraction lever, in the same task** ([ADR 0047](<redacted>.md): the straight-through path and its abuse guard are one task, treated as binding): `platform_identity_projects.status` is `(active,hidden)` and **no route writes it** — takedown is hand SQL today, and a one-way door onto a public surface is not a door. R08 ships `PATCH /projects/:id/status` (curate floor, audited), a per-IP limit on the unmetered CORS-open feed read ([ADR 0209](<redacted>.md)), and above all **`hidden` STICKY against the pull** — a retracted project that keeps reporting `public` stays hidden, or the next sweep silently undoes every takedown and the lever is decorative. Also named: `hall_origin` reaches the feed from the remote's own discovery index checked only for *differing* from the project host — the opposite of containment — and gets `shipUrlWithinClientOrigin`'s downward-only rule, filed against R08. Rejected: a pushed credentialed report (the literal R08 — needs the credentials 0144/0145 forbid, plus replay, rate and a wire-compatible field, none of which the pull has); `PATCH /projects/:id/visibility` (0238 already); a one-time admission and ongoing curation (both offered to the Archon, both declined); trusting the pull to move `status`; enforcing "unreported ⇒ dark matter" across all rows; letting the pull carry name/tagline/art. The later ANALYTICS feed is deliberately NOT decided here — it carries per-person data, exactly what [ADR 0205](<redacted>.md) proved cannot ride an unwitnessed self-report, and it needs the 0144 credential work first.](<redacted>.md) | platform-identity / provisioning / federation trust |
|
|
341
343
|
|
|
342
344
|
| 0246 | [**Catalog membership is a projection of the provisioning row, not a side effect of federation** ([task 1003576](https://cloudbongos.com/builders#/task/1003576) · goal 1000046, criterion `proj-detail-interview` / C3). The BV1.R18 proof ([task 1002337](https://cloudbongos.com/builders#/task/1002337)) demonstrated C3 against live and found its last clause — “then it publishes” — structurally false: the owner’s verdict reads `provisioning_instances` while the public map reads `platform_identity_projects`, and the ONLY bridge was `federateInstance → registerClient → upsertProject`, which `shouldFederate` refuses for anything but `co-tenant` (the wizard hard-codes `standalone`) and which runs ONCE, at provision time, through a COALESCE. So a wizard-made project was never catalogued at all and a co-tenant one that answered later never updated — `publishable: true`, `{"projects": []}`, forever. The same missing row is [task 1003580](https://cloudbongos.com/builders#/task/1003580)’s GAP 1: `POST /projects/invite` resolves against `platform_identity_sso_clients`, whose only writer is that same `registerClient`. Underneath is one conflation — federation mints CREDENTIALS and was incidentally the only thing that minted MEMBERSHIP, while [ADR 0182](<redacted>.md) D4 already fixed the map’s gate at IDENTITY, which has nothing to do with hosting topology. **Decision: the catalog row is a PROJECTION of the provisioning row, re-derived at every write that can change its inputs** — two halves, neither sufficient alone. (1) THE ROW SHAPE: every control-plane project gets both rows whatever its hosting shape, and the client row carries NO credential (`secret_hash = ''`, `redirect_uris = '{}'`), so **the shape is the enforcement** rather than a flag a caller could forget — `redirectUriAllowed` refuses `/sso/authorize`, `clientSecretValid` refuses `/sso/token`, `/sso/membership/check-in` and `/sso/activity/rollup`, and the one leg that checked NEITHER (`/sso/device/start`, which resolves a client from a user-typed origin) grew an explicit `clientIsCredentialed` guard. (2) THE TIMING: re-taken on create, on `PATCH …/detail`, on `PATCH …/settings` when the door changes, and on `POST …/domain` (which re-keys the row, retiring the abandoned origin — [ADR 0181](0181-teardown-state-gate-and-address-retention.md) §2’s leak, in the catalog instead of DNS). **NO MIGRATION** — `secret_hash` is `NOT NULL` and `''` satisfies it while hashing to nothing. `description` and `visibility` are ASSIGNED, not COALESCEd (clearing a description must UN-publish); `featured`/`tagline`/`art_url`/`template` are curation and stay untouched, so [ADR 0182](<redacted>.md) D3 holds. It grants nothing — the row is attribution per [ADR 0238](<redacted>.md), ownership stays `provisioning_instances.owner_builder_id` — and because it writes rows keyed on owner-supplied values (a slug, a BYO domain) each write carries its **claim rule in the ON CONFLICT action’s own WHERE**: the client is refreshed only `WHERE secret_hash = ''` (a real client’s origin, which binds the CLI mint audience, can never be rewritten) and the project only `WHERE owner_github_id = EXCLUDED.owner_github_id` (`NULL = anything` is not true, so the hand-seeded rows cannot be claimed). Writing `visibility` does not contradict [ADR 0182](<redacted>.md) D5: the hub authorizes nothing — the authority already ran on the provisioning plane ([ADR 0238](<redacted>.md)’s own-scoped, audited PATCH), and a project the control plane does NOT own has no provisioning row, so D5’s instance report (R08) stays the only channel for those. `tearing_down`/`torn_down` project `hidden`; a `no_address` instance is not catalogued at all. Travels on the kernel port `project.catalog`, the `project.publishGate` seam in the other direction ([ADR 0083](<redacted>.md) forbids the import). Rejected: extending `shouldFederate` to every shape (hands a real secret to instances whose web.env the platform does not write, and keeps the once-at-standup bug); bridging only at the publish moment (leaves the invite with nothing to resolve); registering only at standup (the bug itself); a `catalog_only` boolean column (needs a migration and is weaker than the shape); letting the bridge write `featured`; and backfilling the three existing catalog rows from code.](<redacted>.md) | provisioning / project creation |
|
|
343
345
|
|