@bongos/core 1.19.564 → 1.19.566
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 +68 -33
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +4 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0253-the-takedown-lever-has-two-writers-and-provenance-is-a-column.md +144 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +91 -4
- package/docs/api-reference.md +3 -2
- package/docs/module-api-changelog.md +4 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/government/catalog.js +1 -1
- package/modules/platform-identity/migrations/platform_identity_020_visibility_pull.sql +106 -0
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/pollers/visibility-pull.js +99 -0
- package/modules/platform-identity/project-catalog.js +62 -4
- package/modules/platform-identity/project-stats.js +31 -0
- package/modules/platform-identity/projects-feed.js +17 -1
- package/modules/platform-identity/routes/projects.js +48 -1
- package/modules/platform-identity/visibility-pull.js +201 -0
- package/modules/public-landing/public/projects.html +89 -53
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/middleware/rate-limit.js +44 -2
- package/src/module-api.js +12 -1
- package/tests/manage_settings_shared_read.mjs +146 -0
- package/tests/module_api.mjs +1 -0
- package/tests/project_door_ui.mjs +16 -2
- package/tests/project_takedown_lever.mjs +177 -0
- package/tests/projects_hub.mjs +24 -2
- package/tests/visibility_pull.mjs +213 -0
- package/tests/visible_projects_feed.mjs +12 -1
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.566",
|
|
6
|
+
"core_contract": "1.19.566",
|
|
7
|
+
"source_commit": "b344a2bcd27acb87fd8d8bcac11241639954f7be",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-06T20:04:22.237Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 446,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2035,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2505,
|
|
20
|
+
"tree_sha256": "b8a38f68a219ad5d19cb432356353fa520857e09c5a5a8726caa463e577a99c1",
|
|
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": "f94a07e3c135a5d6c41a2d16f722e86b990362996042dc2a5e05b2020f8cf376"
|
|
376
376
|
},
|
|
377
377
|
{
|
|
378
378
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
379
379
|
"mode": "0000644",
|
|
380
|
-
"sha256": "
|
|
380
|
+
"sha256": "06d1814637fe2898f0d93a3870726d53102d02feeae4611fcca80f195e2e26e4"
|
|
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": "ee0e25848a938f675e7a2e6e0a48983308117ecafe93ca219f4f1df49cf7ea51"
|
|
391
391
|
},
|
|
392
392
|
{
|
|
393
393
|
"path": "clients/bongos-client/index.d.ts",
|
|
394
394
|
"mode": "0000644",
|
|
395
|
-
"sha256": "
|
|
395
|
+
"sha256": "f7c16a633c0f5c3ba13421815d08f5fb935b8737433acffe72fef018adffbf25"
|
|
396
396
|
},
|
|
397
397
|
{
|
|
398
398
|
"path": "clients/bongos-client/index.mjs",
|
|
399
399
|
"mode": "0000644",
|
|
400
|
-
"sha256": "
|
|
400
|
+
"sha256": "6a3e4df57180f3c182fbadabe94030d06f125373c9336a7a4160015f1df5b11e"
|
|
401
401
|
},
|
|
402
402
|
{
|
|
403
403
|
"path": "clients/bongos-client/package.json",
|
|
@@ -1779,20 +1779,25 @@
|
|
|
1779
1779
|
"mode": "0000644",
|
|
1780
1780
|
"sha256": "a36e7591da769ef46cb4f1c62d5b941278478dbb93e0a94c4f18e35a91866082"
|
|
1781
1781
|
},
|
|
1782
|
+
{
|
|
1783
|
+
"path": "docs/adr/0253-the-takedown-lever-has-two-writers-and-provenance-is-a-column.md",
|
|
1784
|
+
"mode": "0000644",
|
|
1785
|
+
"sha256": "1e17e1499be966af136b63f75693547d2e1e46f825af5755310b9096e949054f"
|
|
1786
|
+
},
|
|
1782
1787
|
{
|
|
1783
1788
|
"path": "docs/adr/README.md",
|
|
1784
1789
|
"mode": "0000644",
|
|
1785
|
-
"sha256": "
|
|
1790
|
+
"sha256": "0c072575a958a3377fe2c7237f9c5af69f00cf8aa1df2bd8c6a3f884ab656b37"
|
|
1786
1791
|
},
|
|
1787
1792
|
{
|
|
1788
1793
|
"path": "docs/api-reference.md",
|
|
1789
1794
|
"mode": "0000644",
|
|
1790
|
-
"sha256": "
|
|
1795
|
+
"sha256": "33d4d9b7530ebeebffc8c7200a137f5624b8319ff9a26e8b7b1e3209acb413ae"
|
|
1791
1796
|
},
|
|
1792
1797
|
{
|
|
1793
1798
|
"path": "docs/api/openapi.json",
|
|
1794
1799
|
"mode": "0000644",
|
|
1795
|
-
"sha256": "
|
|
1800
|
+
"sha256": "b4c2891985427587d5a065b6e7d69638c5cd554d42d5eb22572b9c82f898431f"
|
|
1796
1801
|
},
|
|
1797
1802
|
{
|
|
1798
1803
|
"path": "docs/architecture.md",
|
|
@@ -2667,7 +2672,7 @@
|
|
|
2667
2672
|
{
|
|
2668
2673
|
"path": "docs/module-api-changelog.md",
|
|
2669
2674
|
"mode": "0000644",
|
|
2670
|
-
"sha256": "
|
|
2675
|
+
"sha256": "7073e3d898b2a0d80c9cad6f2ed3a142f2071b9cd4095dae8c580acb2f2095e3"
|
|
2671
2676
|
},
|
|
2672
2677
|
{
|
|
2673
2678
|
"path": "docs/modules-contract.md",
|
|
@@ -4067,7 +4072,7 @@
|
|
|
4067
4072
|
{
|
|
4068
4073
|
"path": "modules/dev-box/app/src/vendor/bongos-client.cjs",
|
|
4069
4074
|
"mode": "0000644",
|
|
4070
|
-
"sha256": "
|
|
4075
|
+
"sha256": "ee0e25848a938f675e7a2e6e0a48983308117ecafe93ca219f4f1df49cf7ea51"
|
|
4071
4076
|
},
|
|
4072
4077
|
{
|
|
4073
4078
|
"path": "modules/dev-box/app/test/unit.mjs",
|
|
@@ -4307,7 +4312,7 @@
|
|
|
4307
4312
|
{
|
|
4308
4313
|
"path": "modules/government/catalog.js",
|
|
4309
4314
|
"mode": "0000644",
|
|
4310
|
-
"sha256": "
|
|
4315
|
+
"sha256": "e2d8d2a3187598fe0c647d30cd333627feeb1aa5352fac8bafc922cf40750e12"
|
|
4311
4316
|
},
|
|
4312
4317
|
{
|
|
4313
4318
|
"path": "modules/government/config.js",
|
|
@@ -6019,10 +6024,15 @@
|
|
|
6019
6024
|
"mode": "0000644",
|
|
6020
6025
|
"sha256": "112fd5dcc9d2bfbc8ec064963e1a783d8ef7855a1a47cf6575ea70da06803d68"
|
|
6021
6026
|
},
|
|
6027
|
+
{
|
|
6028
|
+
"path": "modules/platform-identity/migrations/platform_identity_020_visibility_pull.sql",
|
|
6029
|
+
"mode": "0000644",
|
|
6030
|
+
"sha256": "d4e7b3562068397f63ec66c63da330fd3363ef2a1dbab0d069c995c2a6dbb3b7"
|
|
6031
|
+
},
|
|
6022
6032
|
{
|
|
6023
6033
|
"path": "modules/platform-identity/module.json",
|
|
6024
6034
|
"mode": "0000644",
|
|
6025
|
-
"sha256": "
|
|
6035
|
+
"sha256": "cc6f7ab084ee74715241220339411d1a10c8d6697af143df452278ea2c634f79"
|
|
6026
6036
|
},
|
|
6027
6037
|
{
|
|
6028
6038
|
"path": "modules/platform-identity/platform-identity.js",
|
|
@@ -6034,20 +6044,25 @@
|
|
|
6034
6044
|
"mode": "0000644",
|
|
6035
6045
|
"sha256": "b6eea95dbe66a3126fc2c73d93237cf38dd648a803e910eb88fbd5c780ccef62"
|
|
6036
6046
|
},
|
|
6047
|
+
{
|
|
6048
|
+
"path": "modules/platform-identity/pollers/visibility-pull.js",
|
|
6049
|
+
"mode": "0000644",
|
|
6050
|
+
"sha256": "f842d6ae04dbd2360703f72d35695d9d35c0eb51699d210bf3aa7af67750a009"
|
|
6051
|
+
},
|
|
6037
6052
|
{
|
|
6038
6053
|
"path": "modules/platform-identity/project-catalog.js",
|
|
6039
6054
|
"mode": "0000644",
|
|
6040
|
-
"sha256": "
|
|
6055
|
+
"sha256": "7aa4866e134a9610a36b5c11efffc4ce53fa733be84d47f7b4c269a767c587c9"
|
|
6041
6056
|
},
|
|
6042
6057
|
{
|
|
6043
6058
|
"path": "modules/platform-identity/project-stats.js",
|
|
6044
6059
|
"mode": "0000644",
|
|
6045
|
-
"sha256": "
|
|
6060
|
+
"sha256": "20000528b99e9a52cd9bd2d02c7ca4b118a5b6432ae2902ff8f9bd0503d12d38"
|
|
6046
6061
|
},
|
|
6047
6062
|
{
|
|
6048
6063
|
"path": "modules/platform-identity/projects-feed.js",
|
|
6049
6064
|
"mode": "0000644",
|
|
6050
|
-
"sha256": "
|
|
6065
|
+
"sha256": "fac8b2f750a5a9fb3c05290223292449b04a45c33b6976da285126616a8e9e91"
|
|
6051
6066
|
},
|
|
6052
6067
|
{
|
|
6053
6068
|
"path": "modules/platform-identity/publish-gate.js",
|
|
@@ -6077,7 +6092,7 @@
|
|
|
6077
6092
|
{
|
|
6078
6093
|
"path": "modules/platform-identity/routes/projects.js",
|
|
6079
6094
|
"mode": "0000644",
|
|
6080
|
-
"sha256": "
|
|
6095
|
+
"sha256": "af098885cd3d7e83a9302c7eb8213c3448d73ab79be53b2b5ff1467fbab3685a"
|
|
6081
6096
|
},
|
|
6082
6097
|
{
|
|
6083
6098
|
"path": "modules/platform-identity/routes/public-profile.js",
|
|
@@ -6114,6 +6129,11 @@
|
|
|
6114
6129
|
"mode": "0000644",
|
|
6115
6130
|
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
6116
6131
|
},
|
|
6132
|
+
{
|
|
6133
|
+
"path": "modules/platform-identity/visibility-pull.js",
|
|
6134
|
+
"mode": "0000644",
|
|
6135
|
+
"sha256": "3eee70629402e2c9c8ee0a79466ed1c83f13cec151f60e5f57e7601813a47b58"
|
|
6136
|
+
},
|
|
6117
6137
|
{
|
|
6118
6138
|
"path": "modules/provisioning/CLAUDE.md",
|
|
6119
6139
|
"mode": "0000644",
|
|
@@ -6752,7 +6772,7 @@
|
|
|
6752
6772
|
{
|
|
6753
6773
|
"path": "modules/public-landing/public/projects.html",
|
|
6754
6774
|
"mode": "0000644",
|
|
6755
|
-
"sha256": "
|
|
6775
|
+
"sha256": "7aa74c059670bd351bf8a4295e408aef4a908e3095711aee93f75408ccd837d5"
|
|
6756
6776
|
},
|
|
6757
6777
|
{
|
|
6758
6778
|
"path": "modules/public-landing/public/projects.probes.json",
|
|
@@ -7497,12 +7517,12 @@
|
|
|
7497
7517
|
{
|
|
7498
7518
|
"path": "package-lock.json",
|
|
7499
7519
|
"mode": "0000644",
|
|
7500
|
-
"sha256": "
|
|
7520
|
+
"sha256": "319b45c1535e293ea0c0223c730ec9a08b1059469eb86edc61d3c3d8b9976eee"
|
|
7501
7521
|
},
|
|
7502
7522
|
{
|
|
7503
7523
|
"path": "package.json",
|
|
7504
7524
|
"mode": "0000644",
|
|
7505
|
-
"sha256": "
|
|
7525
|
+
"sha256": "3884c6ddda900305c539824babe08c00fbf942a50c5f7c23bd805d674cae32dc"
|
|
7506
7526
|
},
|
|
7507
7527
|
{
|
|
7508
7528
|
"path": "public-docs/index.html",
|
|
@@ -9007,7 +9027,7 @@
|
|
|
9007
9027
|
{
|
|
9008
9028
|
"path": "src/bongos/middleware/rate-limit.js",
|
|
9009
9029
|
"mode": "0000644",
|
|
9010
|
-
"sha256": "
|
|
9030
|
+
"sha256": "7a4a49df59dd3754fec468f1833514982b6e57c460dc531d5f6605cfe68d196f"
|
|
9011
9031
|
},
|
|
9012
9032
|
{
|
|
9013
9033
|
"path": "src/bongos/module-scope-map.js",
|
|
@@ -9202,7 +9222,7 @@
|
|
|
9202
9222
|
{
|
|
9203
9223
|
"path": "src/module-api.js",
|
|
9204
9224
|
"mode": "0000644",
|
|
9205
|
-
"sha256": "
|
|
9225
|
+
"sha256": "feb3d851877e533d53226ddb6b71bf5d2aa5a47c3a9b38d2bd4801d3a1e8a64b"
|
|
9206
9226
|
},
|
|
9207
9227
|
{
|
|
9208
9228
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11094,6 +11114,11 @@
|
|
|
11094
11114
|
"mode": "0000644",
|
|
11095
11115
|
"sha256": "9a1813d6036886ea07d1563e71a466e66c28554b75b22605464c795fd9fa2ebb"
|
|
11096
11116
|
},
|
|
11117
|
+
{
|
|
11118
|
+
"path": "tests/manage_settings_shared_read.mjs",
|
|
11119
|
+
"mode": "0000644",
|
|
11120
|
+
"sha256": "68a429b47a1ca36a41433c0d8c3298a0f54745a19b050675aedba658a2cea0ef"
|
|
11121
|
+
},
|
|
11097
11122
|
{
|
|
11098
11123
|
"path": "tests/me_ship_days_bound.mjs",
|
|
11099
11124
|
"mode": "0000644",
|
|
@@ -11192,7 +11217,7 @@
|
|
|
11192
11217
|
{
|
|
11193
11218
|
"path": "tests/module_api.mjs",
|
|
11194
11219
|
"mode": "0000644",
|
|
11195
|
-
"sha256": "
|
|
11220
|
+
"sha256": "39812c560bcd88e3b3a7938ed2447fbd4ab24b0f925e9662b375910bcbe7f1da"
|
|
11196
11221
|
},
|
|
11197
11222
|
{
|
|
11198
11223
|
"path": "tests/module_catalog.mjs",
|
|
@@ -11487,7 +11512,7 @@
|
|
|
11487
11512
|
{
|
|
11488
11513
|
"path": "tests/project_door_ui.mjs",
|
|
11489
11514
|
"mode": "0000644",
|
|
11490
|
-
"sha256": "
|
|
11515
|
+
"sha256": "97d570d16414bb5c71d54223702058018a13c58cdf563448c7d28e2fcf80488e"
|
|
11491
11516
|
},
|
|
11492
11517
|
{
|
|
11493
11518
|
"path": "tests/project_invite_ownership.mjs",
|
|
@@ -11529,6 +11554,11 @@
|
|
|
11529
11554
|
"mode": "0000644",
|
|
11530
11555
|
"sha256": "8cb5533aca29727c317450cf5730f309cdb5540265f8c150a8ea2242c7ef5656"
|
|
11531
11556
|
},
|
|
11557
|
+
{
|
|
11558
|
+
"path": "tests/project_takedown_lever.mjs",
|
|
11559
|
+
"mode": "0000644",
|
|
11560
|
+
"sha256": "361a8baae6b15efa86bdb366076b6ba2ad237f2fa63d600bee879db19042dbef"
|
|
11561
|
+
},
|
|
11532
11562
|
{
|
|
11533
11563
|
"path": "tests/project_visibility_db.mjs",
|
|
11534
11564
|
"mode": "0000644",
|
|
@@ -11537,7 +11567,7 @@
|
|
|
11537
11567
|
{
|
|
11538
11568
|
"path": "tests/projects_hub.mjs",
|
|
11539
11569
|
"mode": "0000644",
|
|
11540
|
-
"sha256": "
|
|
11570
|
+
"sha256": "f93044429430c9fff4fe6860bae87ff8f67bc49a485391e659422e63f209e08c"
|
|
11541
11571
|
},
|
|
11542
11572
|
{
|
|
11543
11573
|
"path": "tests/projects_hub_module_picker.mjs",
|
|
@@ -12454,10 +12484,15 @@
|
|
|
12454
12484
|
"mode": "0000644",
|
|
12455
12485
|
"sha256": "2a381bab380342e4fc556211d4821c85fdde1f676a6c5c6aa2421a89a573cd27"
|
|
12456
12486
|
},
|
|
12487
|
+
{
|
|
12488
|
+
"path": "tests/visibility_pull.mjs",
|
|
12489
|
+
"mode": "0000644",
|
|
12490
|
+
"sha256": "57be5f1160903113241700d5d0474debfbe46184de45ed8f3aa210910f810729"
|
|
12491
|
+
},
|
|
12457
12492
|
{
|
|
12458
12493
|
"path": "tests/visible_projects_feed.mjs",
|
|
12459
12494
|
"mode": "0000644",
|
|
12460
|
-
"sha256": "
|
|
12495
|
+
"sha256": "c5d4a17ba9765069a7850fef5d3843e076fbd08edbc7d690a36e2ff4299d10c2"
|
|
12461
12496
|
},
|
|
12462
12497
|
{
|
|
12463
12498
|
"path": "tests/wandering_prefs.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
|
+
- 358 operations across 55 resource groups
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -647,6 +647,8 @@ function createClient(opts = {}) {
|
|
|
647
647
|
postProjects: (args) => request("POST", "/projects", { hasBody: true }, args),
|
|
648
648
|
// PATCH /projects/{id}/featured — rank: metic+archon — PATCH /projects/:id/featured
|
|
649
649
|
patchProjectsIdFeatured: (args) => request("PATCH", "/projects/{id}/featured", { hasBody: true }, args),
|
|
650
|
+
// PATCH /projects/{id}/status — rank: metic+archon — PATCH /projects/:id/status
|
|
651
|
+
patchProjectsIdStatus: (args) => request("PATCH", "/projects/{id}/status", { hasBody: true }, args),
|
|
650
652
|
// GET /projects/featured — rank: public — GET /projects/featured
|
|
651
653
|
getProjectsFeatured: (args) => request("GET", "/projects/featured", { hasBody: false }, args),
|
|
652
654
|
// POST /projects/invite — rank: metic+archon — POST /projects/invite
|
|
@@ -646,6 +646,8 @@ function createClient(opts = {}) {
|
|
|
646
646
|
postProjects: (args) => request("POST", "/projects", { hasBody: true }, args),
|
|
647
647
|
// PATCH /projects/{id}/featured — rank: metic+archon — PATCH /projects/:id/featured
|
|
648
648
|
patchProjectsIdFeatured: (args) => request("PATCH", "/projects/{id}/featured", { hasBody: true }, args),
|
|
649
|
+
// PATCH /projects/{id}/status — rank: metic+archon — PATCH /projects/:id/status
|
|
650
|
+
patchProjectsIdStatus: (args) => request("PATCH", "/projects/{id}/status", { hasBody: true }, args),
|
|
649
651
|
// GET /projects/featured — rank: public — GET /projects/featured
|
|
650
652
|
getProjectsFeatured: (args) => request("GET", "/projects/featured", { hasBody: false }, args),
|
|
651
653
|
// POST /projects/invite — rank: metic+archon — POST /projects/invite
|
|
@@ -175,6 +175,8 @@ export interface PatchProjectSettingsRotDaysRequest { days: number }
|
|
|
175
175
|
export interface PatchProjectSettingsRotDaysResponse { ok: boolean; rot_days: unknown; source: unknown; describe: unknown }
|
|
176
176
|
export interface PatchProjectsIdFeaturedRequest { featured: boolean }
|
|
177
177
|
export interface PatchProjectsIdFeaturedResponse { project: unknown }
|
|
178
|
+
export interface PatchProjectsIdStatusRequest { status: string }
|
|
179
|
+
export interface PatchProjectsIdStatusResponse { project: unknown }
|
|
178
180
|
export interface PatchProvisioningInstancesIdDetailRequest { description?: string; detail?: Record<string, unknown>; modules?: string[] }
|
|
179
181
|
export interface PatchProvisioningInstancesIdDetailResponse { ok: boolean; description: unknown; detail: unknown; options: unknown; publish: unknown; modules: unknown }
|
|
180
182
|
export interface PatchProvisioningInstancesIdSettingsResponse { ok: boolean; saved: unknown; planet: unknown; push: unknown }
|
|
@@ -971,6 +973,8 @@ export interface BongosClient {
|
|
|
971
973
|
postProjects(args: RequestArgs & { body: PostProjectsRequest }): Promise<PostProjectsResponse>;
|
|
972
974
|
/** PATCH /projects/{id}/featured — rank: metic+archon */
|
|
973
975
|
patchProjectsIdFeatured(args: RequestArgs & { body: PatchProjectsIdFeaturedRequest }): Promise<PatchProjectsIdFeaturedResponse>;
|
|
976
|
+
/** PATCH /projects/{id}/status — rank: metic+archon */
|
|
977
|
+
patchProjectsIdStatus(args: RequestArgs & { body: PatchProjectsIdStatusRequest }): Promise<PatchProjectsIdStatusResponse>;
|
|
974
978
|
/** GET /projects/featured — rank: public */
|
|
975
979
|
getProjectsFeatured(args?: RequestArgs): Promise<GetProjectsFeaturedResponse>;
|
|
976
980
|
/** POST /projects/invite — rank: metic+archon */
|
|
@@ -643,6 +643,8 @@ export function createClient(opts = {}) {
|
|
|
643
643
|
postProjects: (args) => request("POST", "/projects", { hasBody: true }, args),
|
|
644
644
|
// PATCH /projects/{id}/featured — rank: metic+archon — PATCH /projects/:id/featured
|
|
645
645
|
patchProjectsIdFeatured: (args) => request("PATCH", "/projects/{id}/featured", { hasBody: true }, args),
|
|
646
|
+
// PATCH /projects/{id}/status — rank: metic+archon — PATCH /projects/:id/status
|
|
647
|
+
patchProjectsIdStatus: (args) => request("PATCH", "/projects/{id}/status", { hasBody: true }, args),
|
|
646
648
|
// GET /projects/featured — rank: public — GET /projects/featured
|
|
647
649
|
getProjectsFeatured: (args) => request("GET", "/projects/featured", { hasBody: false }, args),
|
|
648
650
|
// POST /projects/invite — rank: metic+archon — POST /projects/invite
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# ADR 0253 — The takedown lever has two writers, and provenance has to be a column
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-09-06
|
|
5
|
+
- **Task:** [task 1002327](https://cloudbongos.com/builders#/task/1002327) (BONGOS-V1, goal 1000046 — *Project creation*, criterion `proj-visibility-states` / C1), BV1.R08 — the build
|
|
6
|
+
- **Deciders:** Claude, building R08 against ADR 0252's design
|
|
7
|
+
- **Amends:** [ADR 0252](<redacted>.md) §4 and §5 — three premises that did not survive contact with the tree. The decisions ADR 0252 took are unchanged; what changes is what has to be built to make them true.
|
|
8
|
+
- **Related:** [ADR 0246](<redacted>.md) (the control-plane projection, and the writer §5 missed) · [ADR 0247](<redacted>.md) D4 (the fail-closed trap, found live one column over) · [ADR 0182](<redacted>.md) D5 (dark matter) · [ADR 0083](<redacted>.md) (the one-way import rule that forces §2's shape) · [ADR 0209](<redacted>.md) (the limiter precedent) · [ADR 0199](<redacted>.md) / [ADR 0226](<redacted>.md) (reporting degrades; enforcement refuses)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Why this ADR exists
|
|
13
|
+
|
|
14
|
+
ADR 0252 is a design ADR written from a spike. Building it surfaced three things about the
|
|
15
|
+
live tree that the design's own text gets wrong. None of them changes a decision — the pull
|
|
16
|
+
is still a pull, auto-appear still ships, `hidden` is still sticky — but each changes what
|
|
17
|
+
has to be **built**, and two of them are the difference between a working retraction lever
|
|
18
|
+
and a decorative one.
|
|
19
|
+
|
|
20
|
+
They are recorded here rather than by editing ADR 0252, because ADR 0252 is the record of a
|
|
21
|
+
decision that was actually taken on those terms. This is the record of what the terms turned
|
|
22
|
+
out to be.
|
|
23
|
+
|
|
24
|
+
## 2. `hidden` is sticky against TWO writers, and ADR 0252 names the wrong one
|
|
25
|
+
|
|
26
|
+
**ADR 0252 §5 says:** *"`platform_identity_projects.status` is `('active','hidden')` and the
|
|
27
|
+
public feed requires `active`. **No route writes it.** The only `status` write in the module
|
|
28
|
+
is on `sso_clients`."* From there it defends the takedown against the visibility pull, and
|
|
29
|
+
calls that "the single most important line in this ADR."
|
|
30
|
+
|
|
31
|
+
**The tree says otherwise.** `project-catalog.js:upsertOwnedProject` writes `status =
|
|
32
|
+
EXCLUDED.status` on every conflict, and `retireOwnedProject` writes `status = 'hidden'`
|
|
33
|
+
outright. The value comes from `catalog-bridge.js:catalogStatusForInstance`, which returns
|
|
34
|
+
`'active'` for any instance that is not `tearing_down`/`torn_down` — and ADR 0246's whole
|
|
35
|
+
point is that this projection is **re-taken at every write that can change its inputs**
|
|
36
|
+
(create, the detail answers, the settings door, a re-pointed address, plus the backfill
|
|
37
|
+
poller).
|
|
38
|
+
|
|
39
|
+
So the live failure is not the one ADR 0252 guards. It is:
|
|
40
|
+
|
|
41
|
+
1. A curator hides a project.
|
|
42
|
+
2. Its owner saves any setting at all.
|
|
43
|
+
3. The bridge re-takes the projection, assigns `status = 'active'`, and the project is back
|
|
44
|
+
on the public map.
|
|
45
|
+
|
|
46
|
+
**Decision.** The stickiness is a property of the ROW, not of one writer's manners.
|
|
47
|
+
`status_pinned` is set by the curated `PATCH /projects/:id/status` and honoured by
|
|
48
|
+
`upsertOwnedProject`; the pull, separately, never writes `status` at all (asserted against
|
|
49
|
+
the statement text, so a later `status` write in that file reds a test rather than silently
|
|
50
|
+
undoing every takedown).
|
|
51
|
+
|
|
52
|
+
**`retireOwnedProject` deliberately does NOT honour the pin.** It is a safety retraction for
|
|
53
|
+
an origin the project no longer answers on — the [ADR 0181](0181-teardown-state-gate-and-address-retention.md)
|
|
54
|
+
§2 stale-orb leak — and a curator's `'active'` pin must not be able to keep a dead address
|
|
55
|
+
drawing an orb. This is ADR 0252's own principle applied consistently: **narrowing is free;
|
|
56
|
+
widening is what costs.** Every guard in R08 refuses in the direction of less exposure.
|
|
57
|
+
|
|
58
|
+
## 3. Provenance has to be a stored column, because the discriminator does not exist
|
|
59
|
+
|
|
60
|
+
**ADR 0252 §4 tabulates** control-plane rows against self-hosted ones as though the catalog
|
|
61
|
+
knew which was which. It does not. `platform_identity_projects` has no `hosting_shape` and
|
|
62
|
+
no other column that distinguishes them; `hosting_shape` lives on `provisioning_instances`,
|
|
63
|
+
in a module `platform-identity` may not import (ADR 0083's one-way rule). Nor can it be
|
|
64
|
+
inferred: `owner_github_id` is set by the bridge, by `registerClient` **and** left null by
|
|
65
|
+
hand-seeded rows, so it separates nothing.
|
|
66
|
+
|
|
67
|
+
**Decision.** `visibility_source` records which writer owns the row's door
|
|
68
|
+
(`'control-plane'` | `'pull'` | NULL for unclaimed), stamped by whichever writer touches it.
|
|
69
|
+
The pull's predicate is then a local column read — `visibility_source IS DISTINCT FROM
|
|
70
|
+
'control-plane'` — re-asserted in the UPDATE's own `WHERE`, not merely checked before it, so
|
|
71
|
+
a bridge projection landing mid-sweep cannot lose the race. "The two never write the same
|
|
72
|
+
row" becomes an invariant the SQL enforces instead of a convention someone has to remember,
|
|
73
|
+
and no new cross-module seam is opened to ask a question on every sweep.
|
|
74
|
+
|
|
75
|
+
The rejected alternative — a `provisioning.instanceShape` port — is more general and worse:
|
|
76
|
+
it puts a cross-module read in a background loop to answer a question that never changes for
|
|
77
|
+
a given row, and it makes the catalog learn another module's vocabulary, which is exactly the
|
|
78
|
+
ceiling ADR 0246 set.
|
|
79
|
+
|
|
80
|
+
## 4. The tri-state cannot live in `visibility`, and the fail-closed default needs a backfill
|
|
81
|
+
|
|
82
|
+
`visibility` is `NOT NULL DEFAULT 'public'` (migration 011), so it physically cannot express
|
|
83
|
+
"never asked". The recency stamp ADR 0252 §4 asks for is therefore also the null-carrier:
|
|
84
|
+
`visibility_reported_at IS NULL` **is** the never-read state, and it is the only condition
|
|
85
|
+
under which ADR 0182 D5's "unreported ⇒ dark matter" applies. A failed read writes nothing at
|
|
86
|
+
all — which is what keeps the stamp meaning *last successful read* and makes "keep the last
|
|
87
|
+
known state" fall out rather than being implemented.
|
|
88
|
+
|
|
89
|
+
**And the fail-closed predicate needed a backfill, for exactly the reason ADR 0247 D4
|
|
90
|
+
gives.** Shipping `AND (visibility_source = 'control-plane' OR visibility_reported_at IS NOT
|
|
91
|
+
NULL)` without one would have emptied the public map on deploy, because both columns are new
|
|
92
|
+
and therefore NULL on every row — the same shape ADR 0247 D4 found live on this very table
|
|
93
|
+
(`description: null` on every row, so a fail-closed join gate wired that day would have
|
|
94
|
+
refused every join on the platform at once). Migration 020 stamps every pre-existing row
|
|
95
|
+
`'control-plane'`, which is safe because it is **true**: every row that exists today got its
|
|
96
|
+
door from the bridge or from an Archon, and no instance has ever been asked.
|
|
97
|
+
|
|
98
|
+
## 5. Two smaller corrections, recorded so they are not rediscovered
|
|
99
|
+
|
|
100
|
+
- **The manifest key is `brand`, not `branding`.** ADR 0252 §2 and task 1002327's body both
|
|
101
|
+
write `branding.project.visibility`; `buildInstanceManifest` projects the pack under
|
|
102
|
+
`brand`. Reading only the documented spelling would have made every pull a silent
|
|
103
|
+
no-verdict — a failure indistinguishable from "no self-hosted project ever reports". The
|
|
104
|
+
reader accepts both spellings and the test pins both.
|
|
105
|
+
- **The pull is a POLLER, not a passenger on the feed's fan-out.** The obvious cheap move —
|
|
106
|
+
add a fifth fetch beside the four `project-stats.js` already makes — cannot work, and the
|
|
107
|
+
reason is invisible until you try it: **the feed excludes exactly the rows that need
|
|
108
|
+
pulling.** A pullable project that has never answered is dark matter, so it is not in
|
|
109
|
+
`listPublicProjects`, so it is never enriched, so it never answers. A read-path pull can
|
|
110
|
+
only refresh projects that are *already visible*, which is backwards; it cannot bootstrap.
|
|
111
|
+
Stealth rows are excluded from enrichment too (deliberately — asking a stealth project's
|
|
112
|
+
API would tell it it was being enumerated), so one could never be seen coming back. And it
|
|
113
|
+
would make a database write a side effect of an anonymous, CORS-open read.
|
|
114
|
+
|
|
115
|
+
## 6. Consequences
|
|
116
|
+
|
|
117
|
+
- **The retraction lever actually holds.** Not because the pull is polite, but because the
|
|
118
|
+
row carries a latch every automatic writer honours.
|
|
119
|
+
- **`GET /projects/featured` is metered** (limiter #5, 120/min per IP, ADR 0209's shape: one
|
|
120
|
+
exported instance, never a factory). It guards the platform's most expensive anonymous
|
|
121
|
+
read — a cold call fans out to as many as 500 third-party origins — so the ceiling protects
|
|
122
|
+
other people's servers as much as ours.
|
|
123
|
+
- **A hand-seeded catalog row created after this lands is dark until it answers.** That is
|
|
124
|
+
ADR 0252 §2's design working as specified, but it is a behaviour change for the Archon
|
|
125
|
+
`POST /projects` path and worth knowing before someone types a row in and wonders where it
|
|
126
|
+
went.
|
|
127
|
+
- **ADR 0247 D4's `description: null` problem is NOT fixed here.** ADR 0247 names R08 as the
|
|
128
|
+
report that fills the catalog, but ADR 0252 §7 deliberately refuses to let the pull carry
|
|
129
|
+
name, tagline or description — so every catalog row still has `description: null` and the
|
|
130
|
+
publish gate is still unmet for self-hosted rows. The pull moves the door; it does not
|
|
131
|
+
publish the project. That gap is real and stays open.
|
|
132
|
+
|
|
133
|
+
## 7. Rejected
|
|
134
|
+
|
|
135
|
+
- **Editing ADR 0252 instead of amending it.** It records a decision taken on stated terms;
|
|
136
|
+
rewriting the terms afterwards loses what was actually decided and why.
|
|
137
|
+
- **Defending `hidden` against the pull only**, as ADR 0252 §5.2 literally specifies. It
|
|
138
|
+
would satisfy the ADR's text and leave the lever decorative against the writer that fires.
|
|
139
|
+
- **Inferring self-hosted from `owner_github_id`, or from the absence of a credentialed
|
|
140
|
+
`sso_clients` row.** Both are proxies that are wrong today for real rows.
|
|
141
|
+
- **A `provisioning.instanceShape` port.** §3.
|
|
142
|
+
- **Backfilling `visibility_reported_at = now()` instead of stamping the source.** It would
|
|
143
|
+
assert a reading that never happened, and it would make every existing row look freshly
|
|
144
|
+
confirmed by an instance that was never contacted.
|
package/docs/adr/README.md
CHANGED
|
@@ -340,6 +340,7 @@ This keeps the decision history honest and traceable.
|
|
|
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
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
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 |
|
|
343
|
+
| 0253 | [**The takedown lever has two writers, and provenance has to be a column** ([task 1002327](https://cloudbongos.com/builders#/task/1002327) · goal 1000046, criterion `proj-visibility-states` / C1, BV1.R08 — the build). Amends [ADR 0252](<redacted>.md) §4/§5: three premises that did not survive contact with the tree. The decisions stand; what changes is what has to be BUILT. **(1) `hidden` is sticky against TWO writers, and 0252 names the wrong one.** 0252 §5 says "no route writes [`status`]" and defends the takedown against the PULL. But `project-catalog.js:upsertOwnedProject` writes `status = EXCLUDED.status` on every conflict, sourced from `catalog-bridge.js:catalogStatusForInstance` — which returns `active` for any instance not torn down — and [ADR 0246](<redacted>.md) re-takes that projection at EVERY provisioning write. So the live failure is: curator hides a project → its owner saves any setting → it is back on the map. Stickiness therefore belongs to the ROW (`status_pinned`, set by the curated PATCH, honoured by the projection), not to one writer's manners; the pull separately writes no `status` at all, asserted against the statement text. `retireOwnedProject` deliberately does NOT honour the pin — it is a safety retraction for an origin the project no longer answers on ([ADR 0181](0181-teardown-state-gate-and-address-retention.md) §2), and narrowing is always free. **(2) Provenance must be a stored column.** 0252 §4 tabulates control-plane vs self-hosted rows as though the catalog knew which was which; it has no `hosting_shape` (that lives on `provisioning_instances`, in a module platform-identity may not import — [ADR 0083](<redacted>.md)) and `owner_github_id` separates nothing (set by the bridge, set by `registerClient`, null on hand-seeded rows). `visibility_source` (`control-plane`|`pull`|NULL) records it on the row, re-asserted in the UPDATE's own WHERE so a mid-sweep projection cannot lose the race — making "the two never write the same row" an invariant the SQL enforces. Rejected a `provisioning.instanceShape` port: a cross-module read in a background loop, for a value that never changes per row. **(3) The tri-state cannot live in `visibility`** (`NOT NULL DEFAULT 'public'`), so the recency stamp IS the null-carrier: `visibility_reported_at IS NULL` is the never-read state, and a failed read writes nothing — which is what makes "keep the last known state" fall out rather than be implemented. The fail-closed feed predicate needed a BACKFILL for [ADR 0247](<redacted>.md) D4's exact reason: both columns are new, so unbackfilled it would have emptied the public map on deploy. Migration 020 stamps every existing row `control-plane`, which is safe because it is TRUE — no instance has ever been asked. Two smaller corrections: the manifest key is **`brand`**, not `branding` (0252 §2 and the task body both misname it; reading only the documented spelling makes every pull a silent no-verdict), and the pull is a **POLLER, not a passenger on the feed's fan-out** — the feed excludes exactly the rows that need pulling (a never-answered row is dark matter, so it is never enriched, so it never answers), stealth rows are excluded from enrichment by design, and a read-path pull would make a DB write a side effect of an anonymous CORS-open read. Honest limit recorded: 0247 D4's `description: null` problem is NOT fixed here — 0252 §7 refuses to let the pull carry name/tagline/description, so the publish gate stays unmet for self-hosted rows. The pull moves the door; it does not publish the project.](<redacted>.md) | platform-identity / provisioning / abuse guard |
|
|
343
344
|
|
|
344
345
|
| 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 |
|
|
345
346
|
|