@bongos/core 1.19.563 → 1.19.565

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/.bongos-core.json +65 -30
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +4 -0
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/adr/0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md +109 -0
  8. package/docs/adr/0253-the-takedown-lever-has-two-writers-and-provenance-is-a-column.md +144 -0
  9. package/docs/adr/README.md +2 -0
  10. package/docs/api/openapi.json +91 -4
  11. package/docs/api-reference.md +3 -2
  12. package/docs/module-api-changelog.md +4 -0
  13. package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
  14. package/modules/government/catalog.js +1 -1
  15. package/modules/platform-identity/migrations/platform_identity_020_visibility_pull.sql +106 -0
  16. package/modules/platform-identity/module.json +1 -1
  17. package/modules/platform-identity/pollers/visibility-pull.js +99 -0
  18. package/modules/platform-identity/project-catalog.js +62 -4
  19. package/modules/platform-identity/project-stats.js +31 -0
  20. package/modules/platform-identity/projects-feed.js +17 -1
  21. package/modules/platform-identity/routes/projects.js +48 -1
  22. package/modules/platform-identity/visibility-pull.js +201 -0
  23. package/package-lock.json +2 -2
  24. package/package.json +1 -1
  25. package/src/bongos/middleware/rate-limit.js +44 -2
  26. package/src/module-api.js +12 -1
  27. package/tests/module_api.mjs +1 -0
  28. package/tests/project_takedown_lever.mjs +177 -0
  29. package/tests/visibility_pull.mjs +213 -0
  30. 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.563",
6
- "core_contract": "1.19.563",
7
- "source_commit": "c8dc8ef54a7fd97c2ba21c436f6c1faf0a125f0f",
5
+ "core_version": "1.19.565",
6
+ "core_contract": "1.19.565",
7
+ "source_commit": "d4d7fdc583b75f4519d2f284349c53475e27f8dc",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-06T17:53:15.785Z",
9
+ "built_at": "2026-09-06T19:48:06.968Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 444,
12
+ "docs_redacted": 446,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2029,
14
+ "functional_verbatim": 2034,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2497,
20
- "tree_sha256": "d705f290c86bc4240b5cd119cb250092b6b2d7af6d8ffc8e870dd1f0ae6cdef3",
19
+ "file_count": 2504,
20
+ "tree_sha256": "32eef9b483861a2a84b7778a4fb37713f88901efef248266aaa73c278a519d2b",
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": "38988a49c2cdcc89db69602de1fe46cba0de430643c677352b9db36ed89c010c"
375
+ "sha256": "f94a07e3c135a5d6c41a2d16f722e86b990362996042dc2a5e05b2020f8cf376"
376
376
  },
377
377
  {
378
378
  "path": "clients/bongos-client/bongos-client.global.js",
379
379
  "mode": "0000644",
380
- "sha256": "bd7d31e9cff0d758cc66c1d4adcf52eeaa99acc2ef48d459265425e2a4d2fd19"
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": "33fbbbaf2a4a939b7351a7aa25264ddd609c1298d9ba809372a162983afe5038"
390
+ "sha256": "ee0e25848a938f675e7a2e6e0a48983308117ecafe93ca219f4f1df49cf7ea51"
391
391
  },
392
392
  {
393
393
  "path": "clients/bongos-client/index.d.ts",
394
394
  "mode": "0000644",
395
- "sha256": "a0992ef72cffc0d200e62bd5315e64090eda7cbd3477a01ec041e2d08bb39b9e"
395
+ "sha256": "f7c16a633c0f5c3ba13421815d08f5fb935b8737433acffe72fef018adffbf25"
396
396
  },
397
397
  {
398
398
  "path": "clients/bongos-client/index.mjs",
399
399
  "mode": "0000644",
400
- "sha256": "e83e49178398441026fe6fd4822dd4eddae2c2316f5aa5006a090ef0d1a41b98"
400
+ "sha256": "6a3e4df57180f3c182fbadabe94030d06f125373c9336a7a4160015f1df5b11e"
401
401
  },
402
402
  {
403
403
  "path": "clients/bongos-client/package.json",
@@ -1774,20 +1774,30 @@
1774
1774
  "mode": "0000644",
1775
1775
  "sha256": "8ccfe21df7ab80ed872565bc879fb21a1b35ce81da55151035f2dc45cd40e749"
1776
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
+ },
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
+ },
1777
1787
  {
1778
1788
  "path": "docs/adr/README.md",
1779
1789
  "mode": "0000644",
1780
- "sha256": "6d7f0b67401ca836ed08f5cea31ea578c10987718de48fecc0ac663528e9403d"
1790
+ "sha256": "0c072575a958a3377fe2c7237f9c5af69f00cf8aa1df2bd8c6a3f884ab656b37"
1781
1791
  },
1782
1792
  {
1783
1793
  "path": "docs/api-reference.md",
1784
1794
  "mode": "0000644",
1785
- "sha256": "ae645afb5ce376b7e7b4ad0a5c00f63e48a2d0540fe79af29106d174a41ac3e8"
1795
+ "sha256": "33d4d9b7530ebeebffc8c7200a137f5624b8319ff9a26e8b7b1e3209acb413ae"
1786
1796
  },
1787
1797
  {
1788
1798
  "path": "docs/api/openapi.json",
1789
1799
  "mode": "0000644",
1790
- "sha256": "a009aa37bc7698ac1469b45ac875b1b3884c0104b7f8a886e450082d503de45c"
1800
+ "sha256": "b4c2891985427587d5a065b6e7d69638c5cd554d42d5eb22572b9c82f898431f"
1791
1801
  },
1792
1802
  {
1793
1803
  "path": "docs/architecture.md",
@@ -2662,7 +2672,7 @@
2662
2672
  {
2663
2673
  "path": "docs/module-api-changelog.md",
2664
2674
  "mode": "0000644",
2665
- "sha256": "f13bd014e6a89aefd27dbfbaf639b7c3f3e8b90c3adb69cedfb6a0b11b0d3daf"
2675
+ "sha256": "17b7c3671b0652839a0983e0cbee2b00ca2438cb90e5d79771a1c7dd9415e64f"
2666
2676
  },
2667
2677
  {
2668
2678
  "path": "docs/modules-contract.md",
@@ -4062,7 +4072,7 @@
4062
4072
  {
4063
4073
  "path": "modules/dev-box/app/src/vendor/bongos-client.cjs",
4064
4074
  "mode": "0000644",
4065
- "sha256": "33fbbbaf2a4a939b7351a7aa25264ddd609c1298d9ba809372a162983afe5038"
4075
+ "sha256": "ee0e25848a938f675e7a2e6e0a48983308117ecafe93ca219f4f1df49cf7ea51"
4066
4076
  },
4067
4077
  {
4068
4078
  "path": "modules/dev-box/app/test/unit.mjs",
@@ -4302,7 +4312,7 @@
4302
4312
  {
4303
4313
  "path": "modules/government/catalog.js",
4304
4314
  "mode": "0000644",
4305
- "sha256": "57b29298fa5707d3a1d59e78612d2b039b1559d26b8a9116812d5c76e9c3af87"
4315
+ "sha256": "e2d8d2a3187598fe0c647d30cd333627feeb1aa5352fac8bafc922cf40750e12"
4306
4316
  },
4307
4317
  {
4308
4318
  "path": "modules/government/config.js",
@@ -6014,10 +6024,15 @@
6014
6024
  "mode": "0000644",
6015
6025
  "sha256": "112fd5dcc9d2bfbc8ec064963e1a783d8ef7855a1a47cf6575ea70da06803d68"
6016
6026
  },
6027
+ {
6028
+ "path": "modules/platform-identity/migrations/platform_identity_020_visibility_pull.sql",
6029
+ "mode": "0000644",
6030
+ "sha256": "d4e7b3562068397f63ec66c63da330fd3363ef2a1dbab0d069c995c2a6dbb3b7"
6031
+ },
6017
6032
  {
6018
6033
  "path": "modules/platform-identity/module.json",
6019
6034
  "mode": "0000644",
6020
- "sha256": "b798cbc2c6f4106c4eb94c9f7219ad1643bfb5ec9449e0384bf87b3f7ecccc30"
6035
+ "sha256": "cc6f7ab084ee74715241220339411d1a10c8d6697af143df452278ea2c634f79"
6021
6036
  },
6022
6037
  {
6023
6038
  "path": "modules/platform-identity/platform-identity.js",
@@ -6029,20 +6044,25 @@
6029
6044
  "mode": "0000644",
6030
6045
  "sha256": "b6eea95dbe66a3126fc2c73d93237cf38dd648a803e910eb88fbd5c780ccef62"
6031
6046
  },
6047
+ {
6048
+ "path": "modules/platform-identity/pollers/visibility-pull.js",
6049
+ "mode": "0000644",
6050
+ "sha256": "f842d6ae04dbd2360703f72d35695d9d35c0eb51699d210bf3aa7af67750a009"
6051
+ },
6032
6052
  {
6033
6053
  "path": "modules/platform-identity/project-catalog.js",
6034
6054
  "mode": "0000644",
6035
- "sha256": "6021d893691000a164d7d16e6f24b5bd91215c6dcb252557f0ffcd5d1d467e32"
6055
+ "sha256": "7aa4866e134a9610a36b5c11efffc4ce53fa733be84d47f7b4c269a767c587c9"
6036
6056
  },
6037
6057
  {
6038
6058
  "path": "modules/platform-identity/project-stats.js",
6039
6059
  "mode": "0000644",
6040
- "sha256": "2e898799632a2b5c2ead9cb44fa0b8029fcb6d217f7d5f26c8a39b34c75bbea7"
6060
+ "sha256": "20000528b99e9a52cd9bd2d02c7ca4b118a5b6432ae2902ff8f9bd0503d12d38"
6041
6061
  },
6042
6062
  {
6043
6063
  "path": "modules/platform-identity/projects-feed.js",
6044
6064
  "mode": "0000644",
6045
- "sha256": "7ba4e7f2a375a1197524106605ff5dd6e4e597b2926bf6498f784ede5099a447"
6065
+ "sha256": "fac8b2f750a5a9fb3c05290223292449b04a45c33b6976da285126616a8e9e91"
6046
6066
  },
6047
6067
  {
6048
6068
  "path": "modules/platform-identity/publish-gate.js",
@@ -6072,7 +6092,7 @@
6072
6092
  {
6073
6093
  "path": "modules/platform-identity/routes/projects.js",
6074
6094
  "mode": "0000644",
6075
- "sha256": "dac39b920025d696ba34e20aa4a9fbfb70d1cbc9ffe8f3b0da823f333d6a8d7a"
6095
+ "sha256": "af098885cd3d7e83a9302c7eb8213c3448d73ab79be53b2b5ff1467fbab3685a"
6076
6096
  },
6077
6097
  {
6078
6098
  "path": "modules/platform-identity/routes/public-profile.js",
@@ -6109,6 +6129,11 @@
6109
6129
  "mode": "0000644",
6110
6130
  "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
6111
6131
  },
6132
+ {
6133
+ "path": "modules/platform-identity/visibility-pull.js",
6134
+ "mode": "0000644",
6135
+ "sha256": "3eee70629402e2c9c8ee0a79466ed1c83f13cec151f60e5f57e7601813a47b58"
6136
+ },
6112
6137
  {
6113
6138
  "path": "modules/provisioning/CLAUDE.md",
6114
6139
  "mode": "0000644",
@@ -7492,12 +7517,12 @@
7492
7517
  {
7493
7518
  "path": "package-lock.json",
7494
7519
  "mode": "0000644",
7495
- "sha256": "feb9b3919e27637f41f78010f3a31d662da752e2c3ea56555f919fae02758cff"
7520
+ "sha256": "405ae99d31e8bad9522feb9eeaa8adf7154d0c018d2ea8a88661391e0ff512f6"
7496
7521
  },
7497
7522
  {
7498
7523
  "path": "package.json",
7499
7524
  "mode": "0000644",
7500
- "sha256": "29c0dd946891112260ed3da3ce17b036afa8019a9ca8a89defc4c291b7638d2e"
7525
+ "sha256": "8766e1ea5753cf0dcc31419c38775984d84fed2170f81cc6ce715d87471c9ec8"
7501
7526
  },
7502
7527
  {
7503
7528
  "path": "public-docs/index.html",
@@ -9002,7 +9027,7 @@
9002
9027
  {
9003
9028
  "path": "src/bongos/middleware/rate-limit.js",
9004
9029
  "mode": "0000644",
9005
- "sha256": "cd28f4517614b4b812aba3c051674ebaff8e78fd741a2e464966b687c459a654"
9030
+ "sha256": "7a4a49df59dd3754fec468f1833514982b6e57c460dc531d5f6605cfe68d196f"
9006
9031
  },
9007
9032
  {
9008
9033
  "path": "src/bongos/module-scope-map.js",
@@ -9197,7 +9222,7 @@
9197
9222
  {
9198
9223
  "path": "src/module-api.js",
9199
9224
  "mode": "0000644",
9200
- "sha256": "51c39233a591b55aa092d440634e57ef51840118b5e7d9d93784b9a6de502b29"
9225
+ "sha256": "6571323e133d1a8ce8cb6ecf15a668b0482c40352d64b0543739ba7407ae1da8"
9201
9226
  },
9202
9227
  {
9203
9228
  "path": "src/module-loader/catalog.js",
@@ -11187,7 +11212,7 @@
11187
11212
  {
11188
11213
  "path": "tests/module_api.mjs",
11189
11214
  "mode": "0000644",
11190
- "sha256": "c7eadd36ae57c2f8976bf30bbd4c5d6ac3db0c1a8f67319860b3e4f85b5b5bc1"
11215
+ "sha256": "39812c560bcd88e3b3a7938ed2447fbd4ab24b0f925e9662b375910bcbe7f1da"
11191
11216
  },
11192
11217
  {
11193
11218
  "path": "tests/module_catalog.mjs",
@@ -11524,6 +11549,11 @@
11524
11549
  "mode": "0000644",
11525
11550
  "sha256": "8cb5533aca29727c317450cf5730f309cdb5540265f8c150a8ea2242c7ef5656"
11526
11551
  },
11552
+ {
11553
+ "path": "tests/project_takedown_lever.mjs",
11554
+ "mode": "0000644",
11555
+ "sha256": "361a8baae6b15efa86bdb366076b6ba2ad237f2fa63d600bee879db19042dbef"
11556
+ },
11527
11557
  {
11528
11558
  "path": "tests/project_visibility_db.mjs",
11529
11559
  "mode": "0000644",
@@ -12449,10 +12479,15 @@
12449
12479
  "mode": "0000644",
12450
12480
  "sha256": "2a381bab380342e4fc556211d4821c85fdde1f676a6c5c6aa2421a89a573cd27"
12451
12481
  },
12482
+ {
12483
+ "path": "tests/visibility_pull.mjs",
12484
+ "mode": "0000644",
12485
+ "sha256": "57be5f1160903113241700d5d0474debfbe46184de45ed8f3aa210910f810729"
12486
+ },
12452
12487
  {
12453
12488
  "path": "tests/visible_projects_feed.mjs",
12454
12489
  "mode": "0000644",
12455
- "sha256": "4bf47b6aee92c54099c583726d05421a433ac5b5e82792a848d11c80d7ee7bd1"
12490
+ "sha256": "c5d4a17ba9765069a7850fef5d3843e076fbd08edbc7d690a36e2ff4299d10c2"
12456
12491
  },
12457
12492
  {
12458
12493
  "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
- - 357 operations across 55 resource groups
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,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.
@@ -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.