@bongos/core 1.19.426 → 1.19.428
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 +65 -30
- package/docs/adr/0212-connections-on-the-profile-surface-and-addressing.md +61 -0
- package/docs/adr/0213-platform-terms-acceptance-is-the-hubs-own-witness.md +71 -0
- package/docs/adr/README.md +2 -0
- package/docs/copy-inventory.md +34 -18
- package/docs/copy-registry.json +179 -25
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/profile-connections.js +304 -0
- package/modules/hall-ui/public/profile.html +16 -1
- package/modules/hall-ui/public/profile.js +5 -0
- package/modules/hall-ui/public/style.css +38 -0
- package/modules/platform-identity/account-visibility.js +31 -12
- package/modules/platform-identity/connections.js +4 -3
- package/modules/platform-identity/migrations/platform_identity_019_terms_acceptance.sql +47 -0
- package/modules/platform-identity/platform-identity.js +12 -10
- package/modules/platform-identity/routes/connections.js +4 -3
- package/modules/platform-identity/terms-acceptance.js +97 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/terms.html +8 -0
- package/scripts/hall-preview/server.js +3 -0
- package/src/module-api.js +1 -1
- package/tests/connections_api.mjs +13 -8
- package/tests/connections_ownership.mjs +6 -1
- package/tests/effective_visibility_predicate.mjs +89 -14
- package/tests/profile_connections_ui.mjs +889 -0
- package/tests/profile_privacy_owner_view.mjs +31 -4
- package/tests/profile_route.mjs +15 -8
- package/tests/terms_acceptance.mjs +573 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.428",
|
|
6
|
+
"core_contract": "1.19.428",
|
|
7
|
+
"source_commit": "d7a0e4bd7e1d4b7424f411f5dfb321a77e8beaef",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-29T20:46:11.874Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 363,
|
|
13
13
|
"agent_docs_stubbed": 23,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1734,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2120,
|
|
20
|
+
"tree_sha256": "6cb0ad50d9d19f0418f4c77c403ec092b9b7a12735233523351dfb284c0e0703",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1554,10 +1554,20 @@
|
|
|
1554
1554
|
"mode": "0000644",
|
|
1555
1555
|
"sha256": "e7cc54c2186bda5ae76039a57ab285320bdd57c6879a51e55bcd145fe9261ad5"
|
|
1556
1556
|
},
|
|
1557
|
+
{
|
|
1558
|
+
"path": "docs/adr/0212-connections-on-the-profile-surface-and-addressing.md",
|
|
1559
|
+
"mode": "0000644",
|
|
1560
|
+
"sha256": "1251b6ba7b5fc79765aed5fcf9696d6f55d52acf6d6766200215a480f2d574a3"
|
|
1561
|
+
},
|
|
1562
|
+
{
|
|
1563
|
+
"path": "docs/adr/0213-platform-terms-acceptance-is-the-hubs-own-witness.md",
|
|
1564
|
+
"mode": "0000644",
|
|
1565
|
+
"sha256": "c3aba5879f639ea409d80bc8545085fc1892fe191868ac69c9be52507750f33a"
|
|
1566
|
+
},
|
|
1557
1567
|
{
|
|
1558
1568
|
"path": "docs/adr/README.md",
|
|
1559
1569
|
"mode": "0000644",
|
|
1560
|
-
"sha256": "
|
|
1570
|
+
"sha256": "7a79523a204bf0520f3fe3358b21b017fea0bb8daa3e37e8596841fcb1eac722"
|
|
1561
1571
|
},
|
|
1562
1572
|
{
|
|
1563
1573
|
"path": "docs/api-reference.md",
|
|
@@ -1592,12 +1602,12 @@
|
|
|
1592
1602
|
{
|
|
1593
1603
|
"path": "docs/copy-inventory.md",
|
|
1594
1604
|
"mode": "0000644",
|
|
1595
|
-
"sha256": "
|
|
1605
|
+
"sha256": "3f2d18b85eb807cb1531e40b77cbf9faad6a6027c65e5eb800f40d0f4eb43e89"
|
|
1596
1606
|
},
|
|
1597
1607
|
{
|
|
1598
1608
|
"path": "docs/copy-registry.json",
|
|
1599
1609
|
"mode": "0000644",
|
|
1600
|
-
"sha256": "
|
|
1610
|
+
"sha256": "a8f22a35222054e8356c50856ba48ff49d48c50a30a3d4a3ebf73bf6342e2d23"
|
|
1601
1611
|
},
|
|
1602
1612
|
{
|
|
1603
1613
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -1942,7 +1952,7 @@
|
|
|
1942
1952
|
{
|
|
1943
1953
|
"path": "docs/file-map.md",
|
|
1944
1954
|
"mode": "0000644",
|
|
1945
|
-
"sha256": "
|
|
1955
|
+
"sha256": "7c198c8f0f007919302cdd177e2e820d4c3ff5bd401af3007755c08ef74afcb3"
|
|
1946
1956
|
},
|
|
1947
1957
|
{
|
|
1948
1958
|
"path": "docs/handoff-template.md",
|
|
@@ -1952,7 +1962,7 @@
|
|
|
1952
1962
|
{
|
|
1953
1963
|
"path": "docs/module-api-changelog.md",
|
|
1954
1964
|
"mode": "0000644",
|
|
1955
|
-
"sha256": "
|
|
1965
|
+
"sha256": "52a19ff7373945df5df1c24205a62a35708de4ec2caf546a91977c665f05b0f1"
|
|
1956
1966
|
},
|
|
1957
1967
|
{
|
|
1958
1968
|
"path": "docs/modules-contract.md",
|
|
@@ -4114,6 +4124,11 @@
|
|
|
4114
4124
|
"mode": "0000644",
|
|
4115
4125
|
"sha256": "40f6f44283aa7dba25a2c983d77b6e6f53f40eefef3394122d34f7f42fc5f676"
|
|
4116
4126
|
},
|
|
4127
|
+
{
|
|
4128
|
+
"path": "modules/hall-ui/public/profile-connections.js",
|
|
4129
|
+
"mode": "0000644",
|
|
4130
|
+
"sha256": "b6ff1827da188dbd5a36091e6f5634d43ac21d386cc04b553b6f46c92827dff2"
|
|
4131
|
+
},
|
|
4117
4132
|
{
|
|
4118
4133
|
"path": "modules/hall-ui/public/profile-nudge.js",
|
|
4119
4134
|
"mode": "0000644",
|
|
@@ -4122,12 +4137,12 @@
|
|
|
4122
4137
|
{
|
|
4123
4138
|
"path": "modules/hall-ui/public/profile.html",
|
|
4124
4139
|
"mode": "0000644",
|
|
4125
|
-
"sha256": "
|
|
4140
|
+
"sha256": "3b48f9fedd44d702a4323bd7cbdc0e3f570ca50d2940c6f22508d6c5bc482f8e"
|
|
4126
4141
|
},
|
|
4127
4142
|
{
|
|
4128
4143
|
"path": "modules/hall-ui/public/profile.js",
|
|
4129
4144
|
"mode": "0000644",
|
|
4130
|
-
"sha256": "
|
|
4145
|
+
"sha256": "0885bb6b92dfdc54b9177b0cb949a692ebc8025a019f309d6d786f9dfaf2442d"
|
|
4131
4146
|
},
|
|
4132
4147
|
{
|
|
4133
4148
|
"path": "modules/hall-ui/public/ranks.html",
|
|
@@ -4217,7 +4232,7 @@
|
|
|
4217
4232
|
{
|
|
4218
4233
|
"path": "modules/hall-ui/public/style.css",
|
|
4219
4234
|
"mode": "0000644",
|
|
4220
|
-
"sha256": "
|
|
4235
|
+
"sha256": "41dcbf8e7b21b95c134cfa7df182fc1059d67a4185c38fc55ff310ecde4df5aa"
|
|
4221
4236
|
},
|
|
4222
4237
|
{
|
|
4223
4238
|
"path": "modules/hall-ui/public/task-detail.js",
|
|
@@ -4802,7 +4817,7 @@
|
|
|
4802
4817
|
{
|
|
4803
4818
|
"path": "modules/platform-identity/account-visibility.js",
|
|
4804
4819
|
"mode": "0000644",
|
|
4805
|
-
"sha256": "
|
|
4820
|
+
"sha256": "788d0a51e29e0bd7971a63b8ed168ce71beb2a30a170bd0dbfc457d96b275233"
|
|
4806
4821
|
},
|
|
4807
4822
|
{
|
|
4808
4823
|
"path": "modules/platform-identity/application-echo.js",
|
|
@@ -4812,7 +4827,7 @@
|
|
|
4812
4827
|
{
|
|
4813
4828
|
"path": "modules/platform-identity/connections.js",
|
|
4814
4829
|
"mode": "0000644",
|
|
4815
|
-
"sha256": "
|
|
4830
|
+
"sha256": "ed671aad4b6af46e56e29d5427ac2d5bdfc3ed414dbb981a27fce5097946e7ae"
|
|
4816
4831
|
},
|
|
4817
4832
|
{
|
|
4818
4833
|
"path": "modules/platform-identity/hub-device.js",
|
|
@@ -4914,6 +4929,11 @@
|
|
|
4914
4929
|
"mode": "0000644",
|
|
4915
4930
|
"sha256": "d5553c3d06e8d71b89e62db54a9a3a3ae9810d3f29b262c97dca1f8242aca879"
|
|
4916
4931
|
},
|
|
4932
|
+
{
|
|
4933
|
+
"path": "modules/platform-identity/migrations/platform_identity_019_terms_acceptance.sql",
|
|
4934
|
+
"mode": "0000644",
|
|
4935
|
+
"sha256": "112fd5dcc9d2bfbc8ec064963e1a783d8ef7855a1a47cf6575ea70da06803d68"
|
|
4936
|
+
},
|
|
4917
4937
|
{
|
|
4918
4938
|
"path": "modules/platform-identity/module.json",
|
|
4919
4939
|
"mode": "0000644",
|
|
@@ -4922,7 +4942,7 @@
|
|
|
4922
4942
|
{
|
|
4923
4943
|
"path": "modules/platform-identity/platform-identity.js",
|
|
4924
4944
|
"mode": "0000644",
|
|
4925
|
-
"sha256": "
|
|
4945
|
+
"sha256": "09a9be4a79097faf612ee52b1eea1c5b845bba873032853aab36c55411395833"
|
|
4926
4946
|
},
|
|
4927
4947
|
{
|
|
4928
4948
|
"path": "modules/platform-identity/pollers/code-sweep.js",
|
|
@@ -4937,7 +4957,7 @@
|
|
|
4937
4957
|
{
|
|
4938
4958
|
"path": "modules/platform-identity/routes/connections.js",
|
|
4939
4959
|
"mode": "0000644",
|
|
4940
|
-
"sha256": "
|
|
4960
|
+
"sha256": "d8f7404fc30052a4f815e735dff7981da56d00ccb902ace5fac132a35f59e6de"
|
|
4941
4961
|
},
|
|
4942
4962
|
{
|
|
4943
4963
|
"path": "modules/platform-identity/routes/my-projects.js",
|
|
@@ -4974,6 +4994,11 @@
|
|
|
4974
4994
|
"mode": "0000644",
|
|
4975
4995
|
"sha256": "b4da61899339ac5e9fac517efa4b3d98e39a8e7d38fd8b0cffbd8464ba85e902"
|
|
4976
4996
|
},
|
|
4997
|
+
{
|
|
4998
|
+
"path": "modules/platform-identity/terms-acceptance.js",
|
|
4999
|
+
"mode": "0000644",
|
|
5000
|
+
"sha256": "85aa2a81a80d09031282926bc5f63cef25e85630233fcbd7899ba783b2a62d59"
|
|
5001
|
+
},
|
|
4977
5002
|
{
|
|
4978
5003
|
"path": "modules/platform-identity/tests/platform-identity.mjs",
|
|
4979
5004
|
"mode": "0000644",
|
|
@@ -5532,12 +5557,12 @@
|
|
|
5532
5557
|
{
|
|
5533
5558
|
"path": "package-lock.json",
|
|
5534
5559
|
"mode": "0000644",
|
|
5535
|
-
"sha256": "
|
|
5560
|
+
"sha256": "b1c61170574781d889c2cb10f424790e26d06c24e908b81c593dec2d08f66d18"
|
|
5536
5561
|
},
|
|
5537
5562
|
{
|
|
5538
5563
|
"path": "package.json",
|
|
5539
5564
|
"mode": "0000644",
|
|
5540
|
-
"sha256": "
|
|
5565
|
+
"sha256": "7a042b2a93b97326961852672b0c7cb5083726465cafde6b3fb0d2e8fb3d5ae4"
|
|
5541
5566
|
},
|
|
5542
5567
|
{
|
|
5543
5568
|
"path": "public-docs/index.html",
|
|
@@ -6012,7 +6037,7 @@
|
|
|
6012
6037
|
{
|
|
6013
6038
|
"path": "public-landing/terms.html",
|
|
6014
6039
|
"mode": "0000644",
|
|
6015
|
-
"sha256": "
|
|
6040
|
+
"sha256": "c0e7026074a7485ac7f3e308a8c1fb6b4cac858d8e959dda03e65a3365763b2f"
|
|
6016
6041
|
},
|
|
6017
6042
|
{
|
|
6018
6043
|
"path": "public-landing/world/assets/clouds/cloud-island-large.png",
|
|
@@ -7302,7 +7327,7 @@
|
|
|
7302
7327
|
{
|
|
7303
7328
|
"path": "scripts/hall-preview/server.js",
|
|
7304
7329
|
"mode": "0000644",
|
|
7305
|
-
"sha256": "
|
|
7330
|
+
"sha256": "0a0d26971c469980b398fe9d7ba1cd85013a704ca0e188d8055ee26c8dfa8044"
|
|
7306
7331
|
},
|
|
7307
7332
|
{
|
|
7308
7333
|
"path": "scripts/migrate.sh",
|
|
@@ -7667,7 +7692,7 @@
|
|
|
7667
7692
|
{
|
|
7668
7693
|
"path": "src/module-api.js",
|
|
7669
7694
|
"mode": "0000644",
|
|
7670
|
-
"sha256": "
|
|
7695
|
+
"sha256": "0842a7b4bc18d90a71ed73aef09a6b30aa9b7ad747d03a6e9cef59d84a994ffd"
|
|
7671
7696
|
},
|
|
7672
7697
|
{
|
|
7673
7698
|
"path": "src/module-loader/catalog.js",
|
|
@@ -8247,12 +8272,12 @@
|
|
|
8247
8272
|
{
|
|
8248
8273
|
"path": "tests/connections_api.mjs",
|
|
8249
8274
|
"mode": "0000644",
|
|
8250
|
-
"sha256": "
|
|
8275
|
+
"sha256": "61af2ef3905b31a796e80168f1e267b608792d7195cf2e48d6a1ca85eb76ef5e"
|
|
8251
8276
|
},
|
|
8252
8277
|
{
|
|
8253
8278
|
"path": "tests/connections_ownership.mjs",
|
|
8254
8279
|
"mode": "0000644",
|
|
8255
|
-
"sha256": "
|
|
8280
|
+
"sha256": "58fa2cbe6866697402fc22037a4b615c642311f40c85e4b19555de55e7591eb1"
|
|
8256
8281
|
},
|
|
8257
8282
|
{
|
|
8258
8283
|
"path": "tests/consumer_layout_boot.mjs",
|
|
@@ -8532,7 +8557,7 @@
|
|
|
8532
8557
|
{
|
|
8533
8558
|
"path": "tests/effective_visibility_predicate.mjs",
|
|
8534
8559
|
"mode": "0000644",
|
|
8535
|
-
"sha256": "
|
|
8560
|
+
"sha256": "1aeeaea5d344543ab48b8dde994f4375bea9ef7678ce50e09402376334731f76"
|
|
8536
8561
|
},
|
|
8537
8562
|
{
|
|
8538
8563
|
"path": "tests/est_advisory.mjs",
|
|
@@ -9689,6 +9714,11 @@
|
|
|
9689
9714
|
"mode": "0000644",
|
|
9690
9715
|
"sha256": "013df2528c4998588c1d3f89a9a25f8fa1e912b8abc2d8e95c26c250869a386f"
|
|
9691
9716
|
},
|
|
9717
|
+
{
|
|
9718
|
+
"path": "tests/profile_connections_ui.mjs",
|
|
9719
|
+
"mode": "0000644",
|
|
9720
|
+
"sha256": "baa7abe3a99aca3c412cc856cfd86da8b0d749ef5e7b875939277f4e887ad747"
|
|
9721
|
+
},
|
|
9692
9722
|
{
|
|
9693
9723
|
"path": "tests/profile_fields.mjs",
|
|
9694
9724
|
"mode": "0000644",
|
|
@@ -9702,7 +9732,7 @@
|
|
|
9702
9732
|
{
|
|
9703
9733
|
"path": "tests/profile_privacy_owner_view.mjs",
|
|
9704
9734
|
"mode": "0000644",
|
|
9705
|
-
"sha256": "
|
|
9735
|
+
"sha256": "75cc928587baac3151104c3ff4a1cfd39436fc7255f3afac818079724d6dbab1"
|
|
9706
9736
|
},
|
|
9707
9737
|
{
|
|
9708
9738
|
"path": "tests/profile_rollup_consent.mjs",
|
|
@@ -9712,7 +9742,7 @@
|
|
|
9712
9742
|
{
|
|
9713
9743
|
"path": "tests/profile_route.mjs",
|
|
9714
9744
|
"mode": "0000644",
|
|
9715
|
-
"sha256": "
|
|
9745
|
+
"sha256": "6258fd5980e501e79d0e73415533c71aad89827665d949689df323683676a15b"
|
|
9716
9746
|
},
|
|
9717
9747
|
{
|
|
9718
9748
|
"path": "tests/profile_ui_cross_project.mjs",
|
|
@@ -10429,6 +10459,11 @@
|
|
|
10429
10459
|
"mode": "0000644",
|
|
10430
10460
|
"sha256": "3ff18b286a68b244a272b3f2606ae33b298622347aae97305152f235ca57b330"
|
|
10431
10461
|
},
|
|
10462
|
+
{
|
|
10463
|
+
"path": "tests/terms_acceptance.mjs",
|
|
10464
|
+
"mode": "0000644",
|
|
10465
|
+
"sha256": "513359af9b786feae7503c1c4b62e1b064f9a582d36c52932301b95e25f012fb"
|
|
10466
|
+
},
|
|
10432
10467
|
{
|
|
10433
10468
|
"path": "tests/test_path_guard.mjs",
|
|
10434
10469
|
"mode": "0000644",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# ADR 0212 — The connections section is built on the profile page that exists, and it addresses a builder by asking the API to name them
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-08-29
|
|
5
|
+
- **Task:** [task 1002296](https://cloudbongos.com/builders#/task/1002296) (R20 · goal 1000045 — Public builder profiles, criterion C6)
|
|
6
|
+
- **Amends:** [ADR 0202](<redacted>.md) D4 (the design of record) and [ADR 0207](<redacted>.md) (the API's own departures). Neither decision changes; this records where the *rendering* had to choose, and what the chosen surface cannot yet do.
|
|
7
|
+
- **Relates to:** ADR 0016 (a connection is never authority), ADR 0171 D4/D5 (leak by subtraction; no-store), `docs/specs/<redacted>.md` (the page spec whose section 6 this is)
|
|
8
|
+
|
|
9
|
+
> ADR **0210** and **0211** were spoken for by sibling sessions in flight; this ADR takes 0212 rather than race them (the ADR 0199 / 0202 / 0207 precedent).
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
R20 says "connections on the profile". The profile-page spec's section table marks section 6 **Hub ✓ / Instance —**, meaning the canonical `cloudbongos.com/u/<handle>` page. That page **does not exist**: it is goal 1000048 R14 / [task 1002365](https://cloudbongos.com/builders#/task/1002365), and [task 1002806](https://cloudbongos.com/builders#/task/1002806) established with evidence that there is no route, no page, no hub search entry and no sitemap row for it. Meanwhile `modules/hall-ui/public/profile.html` — the live profile at `/builders/profile` — carries a comment R04 left there on purpose: *"Reserved slots — connections, guilds, and actions (profile-page spec sections 6–8) mount here in later phases."*
|
|
14
|
+
|
|
15
|
+
Two problems follow, and they are the only two this ADR decides.
|
|
16
|
+
|
|
17
|
+
1. **Which page.** Build nothing and wait for a page nobody has scheduled, or build into the slot that was reserved for exactly this.
|
|
18
|
+
2. **How to name the other builder.** Every connections route addresses a counterparty by **handle** (ADR 0202 D1, ADR 0171 D8). The profile page's read, core `GET /builders/:id/profile`, whitelists nine public columns and **`handle` is not among them** — it knows only `github_login`. Widening that whitelist is [task 1002804](https://cloudbongos.com/builders#/task/1002804)'s canonical-URL work, not this one's.
|
|
19
|
+
|
|
20
|
+
## Decision
|
|
21
|
+
|
|
22
|
+
### D1 — Section 6 lands on the hall profile page, revealed only where the hub answers
|
|
23
|
+
|
|
24
|
+
The section mounts into R04's reserved slot on `modules/hall-ui/public/profile.{html,js}`, as a separate script (`profile-connections.js`) so neither the page script nor `platform-identity.js` grows against its line ratchet.
|
|
25
|
+
|
|
26
|
+
It follows the **cross-project record's** contract exactly, because that is the precedent for a hub-only section on this page: the markup ships `hidden`, and the script reveals it only when the hub's routes answered with something to show. On an instance without `platform-identity` every call 404s and the section never appears — degrade, don't 500 (profile-page spec constraint 4).
|
|
27
|
+
|
|
28
|
+
This does **not** pre-empt the canonical page. When `/u/<handle>` is built it renders the same three reads; two surfaces consuming one own-scope API is the composition ADR 0202 D4 §4 already describes (a viewer-relative number cannot ride a viewer-independent public response, so the *page* composes it from a second authenticated call — which is true of whichever page does the composing).
|
|
29
|
+
|
|
30
|
+
**Rejected.** *Waiting for the hub page* — R20's dependents ([task 1002298](https://cloudbongos.com/builders#/task/1002298), the C6 proof) would block on a task in a different goal with no owner. *Building `/u/<handle>` here* — the spec says in terms that only one such page gets built, and it is not this task's.
|
|
31
|
+
|
|
32
|
+
### D2 — The page hands the API the login it holds; the API hands back the handle, and every write uses that
|
|
33
|
+
|
|
34
|
+
`profile-connections.js` calls `GET /connections/degree/:handle` with the **github_login** it already renders on screen, and then uses the **`handle` the response echoes** for the connect write — never the login it started from.
|
|
35
|
+
|
|
36
|
+
This works because `findAccountByHandle` resolves the current handle *or* the single-slot `previous_handle`, and the route answers with `target.handle`. The API's own header says why it is built that way: *"an ACTION endpoint follows a rename rather than answering 301, so a stale 'Connect' link on a page still works."* Asking it to resolve a login is the same move a stale link makes.
|
|
37
|
+
|
|
38
|
+
The failure mode is the right one. A login that resolves to neither slot answers **404 — the same 404 as an unknown handle, a provisional account, and a private stranger** — and on all of them the section stays hidden and no connect button is drawn. So the degradation is exactly the set of cases where the button must not render anyway, plus one it arguably should: **a builder who renamed their handle twice loses the connect affordance on this surface.** That is stated here rather than discovered later. The fix is not a login→handle oracle; it is the handle-addressed page (D1), which never makes this hop.
|
|
39
|
+
|
|
40
|
+
**Rejected.** *Adding `handle` to `GET /builders/:id/profile`* — a core kernel file, its leak test, and the `rel=canonical` wiring that task 1002804 owns; two sessions editing that whitelist in the same week is the collision this coordination exists to avoid. *Putting `handle` inside `cross_project`* — that payload is `null` for a hidden account, and ADR 0202 D4 §3 is explicit that `hide_stats` suppresses the numbers beside an edge and never the edge, so the handle would vanish precisely where the section must still work. *Resolving through the public `GET /profiles/:handle`* — it 404s for a private account, including one the viewer is connected to, which D4 §2 says is addressable to them.
|
|
41
|
+
|
|
42
|
+
### D3 — Three rendering rules, each the last place a privacy decision can be undone
|
|
43
|
+
|
|
44
|
+
The API can only keep its promises if the screen keeps them too. Three are enforced by `tests/profile_connections_ui.mjs` rather than by memory.
|
|
45
|
+
|
|
46
|
+
1. **`degree: null` renders "Out of network", never "not connected"** — ADR 0202's Consequences, made literal. And the copy beside it is **constant**: it must read identically whether the response said `truncated` or not, because the target's own 1-hop truncation is deliberately unreported (ADR 0207 L2) and copy that varied with the flag would rebuild that side channel in words. A degree the API never defined falls to the same label rather than being invented one.
|
|
47
|
+
2. **A sent request claims exactly `201 { status: 'pending' }`** — "Request sent", disabled, and nothing about notification or acceptance. A re-ask swallowed by the 90-day cooldown answers identically and writes nothing, so on the next load the button is simply back to "Connect" **with no explanation anywhere** (ADR 0207 L1). Adding one would be the decline leaking through the UI after the API refused to leak it.
|
|
48
|
+
3. **Every count is the length of the list beside it**, and `seen_at` and `truncated` never reach the screen.
|
|
49
|
+
|
|
50
|
+
## Consequences
|
|
51
|
+
|
|
52
|
+
- Criterion C6's UI half exists on a real page today, and the canonical page inherits it as three own-scope calls rather than a rewrite.
|
|
53
|
+
- One honest gap, named above: a twice-renamed handle has no connect button on the instance profile. It fails closed, and it disappears when `/u/<handle>` lands.
|
|
54
|
+
- The section is descriptive only. Nothing it renders is read by `requireRank`, `requirePermission` or any admission gate, and nothing it renders is rollup-derived — which is why `hide_stats` does not suppress it (ADR 0016, ADR 0202 D4 §3).
|
|
55
|
+
- The proof runs the real router over a WHERE-derived fake pool on a real socket with the real client above it, so a change that makes the *API* less careful — a private stranger becoming addressable, the cooldown ceasing to swallow, the caller's consent floor dropping — fails this UI test too, not only the API's own.
|
|
56
|
+
|
|
57
|
+
## Deferred (named, not silently dropped)
|
|
58
|
+
|
|
59
|
+
- **A public per-builder connections list.** There is deliberately no such route: `GET /connections` is own-scope, so another builder's profile shows the viewer-relative label and the mutuals the viewer is entitled to, and nothing else. Publishing someone's connection list is a privacy-matrix question for the owner, not a builder (ADR 0202's "connections-specific visibility toggle" deferral).
|
|
60
|
+
- **Removing a connection from the other person's profile.** Removal lives on your own list, where the row and its id already are; putting it on their page would need a third read for one button.
|
|
61
|
+
- **Section 7 (guilds) and the wider section 8 actions row.** C7 and the governance-gated invite are their own tasks; the slot comment still names them.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# ADR 0213 — The platform T&C acceptance is the hub's own witness, written once, and consent becomes two columns
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-08-29
|
|
5
|
+
- **Task:** [task 1002913](https://cloudbongos.com/builders#/task/1002913) (goal 1000045 — Public builder profiles)
|
|
6
|
+
- **Relates to:** [ADR 0141](<redacted>.md) §4 (which deferred cross-project-aggregation consent to "the platform T&C" and conditioned the scouting view on it), [ADR 0171](<redacted>.md) D3 (fail-closed `profile_state`, and the sentence that reserved this task's place), [ADR 0016](<redacted>.md) (cross-project numbers are descriptive, never authority), [ADR 0205](<redacted>.md) D3 (the hub's own sign-in witness; `ensureAccount` vs `upsertAccount`), [ADR 0210](<redacted>.md) (the directory this consent covers), [ADR 0202](<redacted>.md) D4 (the shared effective-visibility reader), `docs/specs/<redacted>.md` (the "already carries platform T&C consent" premise), `docs/specs/<redacted>.md`
|
|
7
|
+
|
|
8
|
+
> **0211 and 0212** are held by sessions in flight; this ADR takes 0213 rather than race them (the ADR 0199 / 0202 / 0207 / 0210 precedent).
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
ADR 0141 §4 shipped the cross-project scouting rollup with a promise attached: consent for aggregating a builder's work across every hosted project *"is handled by the platform T&C, tracked separately, out of scope here"*, and the view *"must stay Archon-internal until [it] lands"*. ADR 0171 then superseded the gating half — goal 1000045's owner decision made the data public by default with a self-serve hide — and D3 built the consent boundary the decision needed, describing `profile_state='active'` as the state an account reaches when *"a human is demonstrably at the hub (**and where T&C acceptance can later be captured, task 1002913**)"*.
|
|
13
|
+
|
|
14
|
+
Both ADRs, in other words, wrote their consent story around an artifact that did not exist. The public-profiles spec states the premise plainly — profiles are public by default because that is *"tied to having a platform account, which already carries platform T&C consent"* — and the profile-page spec had already flagged, in as many words, that **that premise is not currently true**.
|
|
15
|
+
|
|
16
|
+
Three things were found on picking the task up, and they changed its shape:
|
|
17
|
+
|
|
18
|
+
1. **A `/terms` page already exists.** Task 1003251 landed `public-landing/terms.html`, routed and server-stamped. The task brief (written before it) says no T&C document exists anywhere in the tree; that is now stale. What does not exist is the *content* ADR 0141 deferred to it: the page covers accounts, code ownership, fair use and liability, and says nothing whatsoever about work crossing between projects. `/privacy` is silent on it too.
|
|
19
|
+
2. **The `scouting.js:6` reference the task points at is gone.** Task 1002290 rewrote that header for ADR 0210; it now describes consent as enforced by `scoutingListableSql`. So the last in-tree pointer to the missing artifact had already been overwritten — the debt was becoming invisible rather than getting paid.
|
|
20
|
+
3. **The un-gating this artifact was supposed to unlock has already happened**, via ADR 0171 and ADR 0210. Nothing waits on this ADR to widen. What waits on it is the *honesty* of a premise every public surface already rests on.
|
|
21
|
+
|
|
22
|
+
## Decision
|
|
23
|
+
|
|
24
|
+
### D1 — The acceptance record is the hub's own evidence, never a caller's claim
|
|
25
|
+
|
|
26
|
+
`platform_identity_accounts` gains `terms_accepted_at timestamptz` and `terms_version text` (migration `<redacted>.sql`). Both are written by exactly one statement: `upsertAccount`'s `activate` branch. That branch has exactly one caller — `GET /sso/authorize` — where `activate: true` is a **source literal** and every other field comes from the hub's own `resolveSession` result, gated by `sessionUsableForFederation`.
|
|
27
|
+
|
|
28
|
+
This is deliberately the ADR 0205 D3 property, reused. An acceptance record a caller could assert is evidence of nothing; the only thing that makes the record mean "this human accepted" is that no request-controlled value can reach the statement that writes it. `ensureAccount` — the federated check-in / device-poll path, which takes an *unproven* `github_id` from a request body — writes NULL, exactly as it writes `provisional`.
|
|
29
|
+
|
|
30
|
+
### D2 — Written once. The first acceptance is the one that stands
|
|
31
|
+
|
|
32
|
+
The conflict branch writes `COALESCE(platform_identity_accounts.terms_accepted_at, now())`. Signing in again re-reads the terms but does not restamp them, so the column keeps meaning *when consent was given* rather than *when this account was last seen*. `terms_version` is COALESCE'd the same way and always written in the same statement, so the pair cannot diverge.
|
|
33
|
+
|
|
34
|
+
### D3 — Consent is two columns, and the shared predicate is where that is enforced
|
|
35
|
+
|
|
36
|
+
`account-visibility.js` — the sole owner of every effective-visibility answer (ADR 0202 D4) — now spells the consent axis as `profile_state = 'active' AND terms_accepted_at IS NOT NULL`, in both its SQL and JS forms. `accountVisibleSql`/`isAccountVisible` compose from it rather than restating it, so the rollup, directory and connections predicates inherit the change without any of them naming the new column.
|
|
37
|
+
|
|
38
|
+
Writing it here rather than at the call sites is the whole point: both halves are stamped by the same statement, so for any account activated after this ADR they can never disagree — and requiring both in the predicate is what makes that structural instead of a promise. It is also what stops a row that predates the terms from publishing on a premise that was never true.
|
|
39
|
+
|
|
40
|
+
`tests/effective_visibility_predicate.mjs` extends its sole-owner rule to the new column, in the `${q}`-prefixed form the owner actually emits — the form the reach-COALESCE hole (task 1002290) taught us to pin rather than the bare spelling nobody writes.
|
|
41
|
+
|
|
42
|
+
### D4 — No backfill. Existing accounts re-accept on their next hub sign-in
|
|
43
|
+
|
|
44
|
+
Accounts that are already `active` get `terms_accepted_at IS NULL`, because no platform T&C covering cross-project aggregation has ever been shown to anyone and inventing a timestamp would fabricate the very artifact this task exists to make real. Under D3 they are therefore not publicly visible until their next hub-session authorize, which stamps them.
|
|
45
|
+
|
|
46
|
+
This is ADR 0171 D3's own backfill rule applied to its own sentence (*"all existing accounts start provisional and lazily upgrade on their next hub-session authorize"*), and it is what P03 ratified ([task 1002799](https://cloudbongos.com/builders#/task/1002799), owner, 2026-08-16): no backfill activation, on the rationale that a builder passes through the hub anyway. **This is the one consequence with a visible product effect, and it is on [blocker 1000123](https://cloudbongos.com/builders#/blocker/1000123) for the owner** — grandfathering is one line in the migration if they prefer it.
|
|
47
|
+
|
|
48
|
+
### D5 — Re-acceptance on a later revision is out of scope, and visible rather than silent
|
|
49
|
+
|
|
50
|
+
Because the version is recorded and never overwritten, publishing a new `TERMS_VERSION` leaves every row naming the text it actually agreed to, so the platform can see who has not accepted the new one. What to *do* about that — re-prompt, re-gate, or nothing — is a product flow, not a storage shape, and is not built here.
|
|
51
|
+
|
|
52
|
+
### D6 — The wording is the owner's, and only the wording was left pending
|
|
53
|
+
|
|
54
|
+
The mechanism ships complete. The text of the new section is a draft on [blocker 1000123](https://cloudbongos.com/builders#/blocker/1000123); `TERMS_VERSION` is date-stamped and asserted to match the page, so changing the wording is an edit to two strings.
|
|
55
|
+
|
|
56
|
+
## Consequences
|
|
57
|
+
|
|
58
|
+
- The premise the public-profile surfaces rest on becomes true for every account activated from here, and fail-closed for every account that predates it.
|
|
59
|
+
- **Every currently-`active` account is unpublished until its next hub sign-in** (D4). Reversible in one line; on the owner's blocker.
|
|
60
|
+
- `platform-identity.js` sat at 1497 lines against a hard `oversized_file_count: 0` ratchet, so the write-side SQL lives in a new `terms-acceptance.js` — the fifth instance of the "new file beside it" precedent (`account-visibility.js`, `application-echo.js`, `routes/connections.js`, `src/bongos/routes/my-sessions.js`). The file now measures exactly 1500 and **has no headroom left**; the next change to it must shrink it.
|
|
61
|
+
- The read/write split is load-bearing: `terms-acceptance.js` owns only statements that SET the columns, `account-visibility.js` owns the one that TESTS them. The guard's spelling rules deliberately do not match writes, which is what lets both files exist without one of them being a drift copy.
|
|
62
|
+
- **Settings will read wrong for a legacy account under D4** until someone closes it: `GET /me/cross-project` returns `profile_state` raw and the Settings consent note branches on it alone, so an account that signed in before the terms existed reads "active" while its profile is published nowhere. One own-scope boolean plus a copy change fixes it - filed as idea 1000875, and it disappears entirely if the owner grandfathers on blocker 1000123.
|
|
63
|
+
- `docs/specs/<redacted>.md`'s standing note that the consent premise "is not currently true for every account" is now addressed rather than outstanding.
|
|
64
|
+
|
|
65
|
+
## Rejected alternatives
|
|
66
|
+
|
|
67
|
+
- **A separate `platform_identity_terms_acceptances` table (one row per acceptance).** Correct if acceptance were an event stream — re-acceptance across versions, audit history. It is not one yet (D5), and a table whose only reader is a per-account NULL test is a join the visibility predicate would have to grow. Two columns, one statement, no join.
|
|
68
|
+
- **A click-through gate on `/sso/authorize`.** An explicit "I accept" button is stronger evidence than "by signing in you agree". It also puts an interstitial in the middle of a federated redirect that exists to be invisible, and it changes a sign-in flow two other live tasks were editing. The page now states the terms and the sign-in records acceptance of them; upgrading to an explicit gate is additive and does not invalidate any record written under this ADR.
|
|
69
|
+
- **Backfilling `terms_accepted_at` for already-active accounts** (D4) — records a consent that never happened, which is exactly the fiction this task exists to end. Offered to the owner as the alternative it is, not taken silently.
|
|
70
|
+
- **Deriving `profile_state` from `terms_accepted_at`** (dropping the column). The two facts are genuinely different — the hub witnessed a human, and that human accepted a text — and D5's revision case needs them separable.
|
|
71
|
+
- **Putting the acceptance predicate in `terms-acceptance.js`.** It would have made that file a second owner of an effective-visibility answer, which ADR 0202 D4 forbids and the structural guard fails.
|
package/docs/adr/README.md
CHANGED
|
@@ -297,6 +297,8 @@ This keeps the decision history honest and traceable.
|
|
|
297
297
|
| 0208 | [**Building the application lifecycle: the handle waits for its writer, and the vouch ADR 0201 D2.1 describes has no minter that can express it** ([task 1002284](https://cloudbongos.com/builders#/task/1002284), goal 1000045 — R08, criteria C3/C4). Builds [ADR 0201](<redacted>.md) D1 + D4 + D3.2 and records where the design could not be written literally. **The contradiction:** D1.2 says the migration adds "exactly" `applicant_github_id` + `vouched_at`; D2.6 says the applicant's handle is stored on the row. Decision — the handle waits for R09 ([task 1002285](https://cloudbongos.com/builders#/task/1002285)), because the ADR's own dependency table routes D2 *wholesale* (including the handle) there, and because R08 ships no vouch writer at all: a permanently-NULL `applicant_handle` would read as "this account has no handle" when it means "R09 has not shipped". **The hole:** D2.1's replay separation cannot be built as described. `mintHubAssertion` takes no `purpose`, and the asymmetry protecting `mintHubLogoutToken` works only because a logout token carries no identity — an application vouch carries both, so it collides with the login assertion's shape; worse, `verifyHubAssertion` does not reject a payload carrying a `purpose`, so a vouch minted that way would verify *as a login assertion* today. R09 needs a third mint/verify pair plus a tightening of `verifyHubAssertion` (free only while no minter emits a `purpose`), and a deliberate choice about the single-use `jti` store. Also decided: the echo's READ joins `platform_identity_sso_clients` (a `client_id` alone is not renderable) while the TABLE stays the three columns D1.3 named; a re-application refreshes `applied_at`, so the 30-day window runs from the most recent ask; an idempotent PATCH returns the row **without** re-stamping `resolved_by`/`resolved_at`, so the audit trail records who actually resolved it; `dismissed → invited` is refused `409 already_dismissed` naming `POST /access-requests/invite`; and `GET /my-projects` drops from `applied` any project the caller now builds on, so the hub never implies an outcome it is not allowed to hold. Honest limits: `vouched` is false for every real row until R09 lands (the two-class queue is proved by a seeded row, not by a live writer); `already_approved` still reaches the hub as "already_member" prose through `joinRelayOutcome`'s bare-409 branch, which is R10/R12's surface; and the proofs are DB-free, so the partial unique index is modelled rather than exercised — five mutations were run to show the fake pool bites. Rejected: adding `applicant_handle` anyway; bolting a `purpose` argument onto `mintHubAssertion`; a `status` column on the echo; refusing `dismissed → invited` with the existing `not_pending`; a new "project owner" atom.](<redacted>.md) | platform identity / admission lifecycle |
|
|
298
298
|
| 0209 | [**Two routes answering one question spend one budget; whether the answer itself collapses is the owner's call** ([task 1003339](https://cloudbongos.com/builders#/task/1003339), goal 1000045 — criterion `i-control-my-own-access-and-exposure`). `GET /access-requests/status` (public, returns `member|invited|pending|dismissed|none`) and `GET /auth/web/admission-status` (its bare-boolean twin) both answer "does this GitHub login have an account here?", and neither was throttled — `rateLimitMiddleware` returns early for every non-write method, and the one read limiter is mounted only under `/public/*`. Scripted at line rate either becomes a login→membership roster: who builds here, and confirmation that a named person is present. Decision: ship the ceiling as ONE budget — `accountExistenceReadRateLimit`, 120 reads/60s per IP over a single bucket, exported as one middleware INSTANCE rather than a factory and mounted on both routes, because two limiters each configured 120/60s look identical in a diff, pass any test that counts to the limit per route, and hand an enumerator 240/min by alternating. The onboarding module reaches it through the doorway, so `src/bongos/middleware/rate-limit.js` joins `KERNEL_FILES` on `logger.js`'s argument: authz decides WHO may ask, the ceiling decides HOW OFTEN, and the file imports nothing. Deliberately NOT decided here: collapsing the response so a non-subject caller cannot tell `pending`/`dismissed`/`none` from "not admitted" — the consumer sweep found route 1 has exactly ONE consumer (`scripts/gds/login.js`), which already treats `pending` and `none` identically, so collapsing costs exactly one thing: the only message in the codebase that tells a declined applicant they were declined. That is a judgement about what an applicant is owed, so it goes to the owner as a blocker with the evidence. Noted either way: collapsing does not close the oracle — `admitted:true` still means member-or-invited. Rejected: a separate limiter per route; a `createReadRateLimit` factory (two matching call sites get two buckets — the generality loses the property); vendoring a fourth copy of the sliding window; hardening only the smaller route; collapsing on my own judgement because "nothing much" reads it.](<redacted>.md) | security / rate limiting |
|
|
299
299
|
| 0207 | [**Building the connection graph: three departures from ADR 0202, and the three limits it is honest about** ([task 1002295](https://cloudbongos.com/builders#/task/1002295), goal 1000045 criterion C6 — the build of the connections wave). ADR 0202 decided the graph; building it surfaced three places where the decision's MECHANISM could not be written literally without losing the PROPERTY it was about, and three places where the shipped code cannot promise as much as a casual reading suggests. Departures: (1) **level-wise CTEs, not `WITH RECURSIVE`** — a recursive term can carry neither a per-level fan-out cap nor an ordered per-level truncation, which are the two things D3 actually decided, so a recursive build would have had a depth bound and no fan-out bound; a test asserts `RECURSIVE` appears in neither query. (2) **1st degree is an unbounded pair-index probe**, never a scan of the capped frontier — reading it off the `FANOUT_1`-truncated 1-hop reports "2nd" for someone in the viewer's own connections list once they pass 500 connections, and the rule this states is that **a cap may only ever make an answer less specific (`null`), never wrong**. (3) The shared visibility predicate lands in its own file, `modules/platform-identity/account-visibility.js` (`isAccountVisible` / `accountVisibleSql` / `accountActiveSql`), because task 1002967 had not shipped and `platform-identity.js` sat at the size ratchet with zero headroom — **1002967 adopts it rather than writing a second predicate**. Also decided: node visibility during traversal is viewer-relative and asymmetric on purpose (your OWN 1-hop floors on consent alone so a friend who goes private does not vanish from your list; every deeper hop clears the public predicate); cancel is a `DELETE` an ADDRESSEE may not use, so the 90-day cooldown cannot be dodged by calling a decline a delete; and the create response is `201 { status: 'pending' }` and nothing else, so a swallowed re-ask has nothing to fabricate. Limits stated rather than implied: a swallowed re-ask is silent in the RESPONSE and observable by its ABSENCE from the requester's outbox (the claim is narrowed, not the code — a decoy row would be a lie in the database to protect a lie in the API); the TARGET's 1-hop is capped too and that truncation is deliberately NOT reported, because saying so leaks one bit about a third party's degree; and the query COUNT is bounded while the frontier's WORK is O(sum of degree over the 1-hop), since `ORDER BY … LIMIT` materialises before it truncates — the revisit trigger is a measured p95, and the cheap next move is a `statement_timeout` returning the `null` D3 already chose. Deferred and named: a per-builder cap on OUTBOUND requests (the cooldown stops repeat asks to one person, not one builder asking a hundred — a real number is owner-signed), and a real-Postgres pass on the expire-then-insert write path, which CI cannot run.](<redacted>.md) | platform identity / social graph / privacy |
|
|
300
|
+
| 0212 | [**The connections section is built on the profile page that exists, and it addresses a builder by asking the API to name them** ([task 1002296](https://cloudbongos.com/builders#/task/1002296), R20 · goal 1000045 criterion C6 — the UI half of the connections wave). The profile-page spec puts section 6 on the canonical `cloudbongos.com/u/<handle>`, which [task 1002806](https://cloudbongos.com/builders#/task/1002806) proved does not exist (no route, no page, no search entry, no sitemap row) and which belongs to goal 1000048 R14. Decisions: (1) **section 6 lands in the reserved slot R04 left on the hall profile page** (`modules/hall-ui/public/profile.{html,js}`), as a separate `profile-connections.js` so neither the page script nor `platform-identity.js` grows against its line ratchet, and it follows the CROSS-PROJECT section’s contract exactly — the markup ships `hidden` and reveals only where the hub answered, so an instance without `platform-identity` never renders it. It does not pre-empt the canonical page, which inherits the same three own-scope calls. (2) **Addressing: the page hands the API the `github_login` it holds and uses the `handle` the response echoes back for every write** — core `GET /builders/:id/profile` whitelists nine columns and `handle` is not one of them, and widening that whitelist is task 1002804’s canonical-URL work. `findAccountByHandle` already resolves current-or-previous handle and the route answers with the canonical one, which is the same move the API built for a stale Connect link. It fails closed into the deliberately indistinguishable 404 (unknown / provisional / private stranger), so the ONE honest gap is stated rather than discovered: **a twice-renamed handle has no connect button on this surface**, and it disappears when `/u/<handle>` lands. (3) Three rendering rules, enforced by `tests/profile_connections_ui.mjs` (real router over a WHERE-derived fake pool on a real socket, real client above it): `degree: null` renders "Out of network" and never "not connected", with copy that is BYTE-IDENTICAL whether `truncated` was set — varying it would rebuild the ADR 0207 L2 side channel in words; a sent request claims exactly `201 { status: 'pending' }`, so a re-ask swallowed by the 90-day cooldown simply shows "Connect" again next load with no explanation anywhere; and every count is the length of the list beside it, with `seen_at` and `truncated` never reaching the screen. Rejected: waiting for the hub page (it would block C6’s proof on another goal); building `/u/<handle>` here (the spec says only one such page gets built); adding `handle` to the core profile whitelist or to `cross_project` (the latter is `null` under `hide_stats`, which is exactly where the edge must still show — ADR 0202 D4 §3); resolving through public `GET /profiles/:handle` (it 404s for a private account the viewer IS connected to). Deferred and named: a public per-builder connections list (there is deliberately no such route), removal from the other person’s profile, and sections 7–8.](<redacted>.md) | platform identity / social graph / hall UI |
|
|
300
301
|
| 0210 | [**The recruiting directory graduates on an instance-asserted ownership claim, and the recruiting opt-out gates the directory and nothing else** ([task 1002290](https://cloudbongos.com/builders#/task/1002290), R14, goal 1000045 criterion C5 — "project owners, not only Archons, browse builder profiles to recruit"). Two independent problems wore one task id. The reach flag `recruiter_discoverable` protected nobody — task 1002967 said so, and the Settings row stayed in the future tense because of it. And "widen the Archon-only /scouting" was already stale (ADR 0157 lowered `project.curate` to metic) while hiding the real question: **a project owner is not a rank**, and someone who registers a project is an Archon on their OWN instance and usually a bare `xenos` on the hub where the directory lives, so no floor reaches them without reaching every stranger. **D1** "project owner" on the hub is `<redacted>.membership_kind = 'owner'` — written by the PROJECT itself at `POST /sso/membership/check-in` from its own first-admin config, and refused unless the hub's own `signin_verified_at` witness stands (ADR 0205) — and never `platform_identity_projects.owner_github_id`, the hub-curated column ADR 0201 D3 names and forbids; the prohibition is about a hub-held list consulted before an ADMISSION on someone else's box, and this is the hub reading the instance's own mirrored claim before a read of the hub's OWN directory. **D2** the two doors are an OR in `modules/platform-identity/scouting-authz.js` (the `goal-authz.js` owner-or-rank shape, kept out of `platform-identity.js` at its size ratchet): ownership is asked first and a "no" falls through to the UNMODIFIED `requirePermission`, so a refusal keeps its exact code, `required`/`held` and floors map — the widening can only admit; the atom is spelled in the ROUTE's middleware list, because a gate hidden in the helper would make `route-rank-check.js` and `gen-api-docs.js` record this privileged route as `any-builder`; and a broken ownership read falls closed onto the rank door rather than admitting or 500ing. No atom is minted — `project.recruit` belongs to the console wave (spike 1002786 R3). **D3** `PAGE_GATES` gains an optional `alsoAdmits` naming a function on the `platform-identity.hub` port, declared on `requireProjectCuratePage` alone and consulted only after the permission check refused, so the `/people` shell admits exactly the two audiences its data route does; absent port, absent function or a throw all deny. **D4** the reach COALESCE gets its SQL twin in `account-visibility.js` (its sole owner) and composes into `scoutingListableSql` = rollup AND reach, which `listScoutingRollup` joins — but reach is deliberately NOT folded into `rollupVisibleSql`, because the community spec D3 says the opt-out governs the recruiting surface only and folding it down would silently empty a future public leaderboard. **D5** no migration: both columns exist and migration 003's `(github_id)` index already serves the probe. **D6** the ownership query JOINs `platform_identity_sso_clients` on `status='active'` — a membership row never expires and nothing ages it out, so suspending the client (the lever that already stops a project signing anyone in) had to revoke the read it derives, or suspending a rogue project would leave its owner reading every builder on the platform forever. The task's own adversarial review also found the sole-owner drift guard was matching only the UNALIASED spelling of the reach COALESCE — i.e. not the one that ships — and two fail-open defaults (a prototype-reachable `alsoAdmits`, and an alias guard resting on template evaluation order); all three fixed and mutation-checked. ⚠ Honest limits recorded, not fixed: the hall NAV still hides People from a project owner (task 1003349, sequenced with R15 which owns that surface), the `touchMembership` write path stays as ADR 0205 left it, and `openapi.json` cannot express "or owner". ⚠ And DELEGATES do not reach the directory yet (blocker 1000122 + task 1003350, linked) — an instance reports `owner` for its first admin only, so the registrant half works and the delegate half needs the instance to report `owner` for any archon, which changes a cross-version wire contract and a shipped bucketing; filed as blocker 1000122, and nothing here changes when it is answered. Rejected: lowering the floor further (a floor low enough to admit a registrant excludes nobody); a `project.owner` atom (ADR 0201 D3 rejected it); minting `project.recruit` here; gating on `owner_github_id`; folding reach into the rollup predicate; widening `membershipKind()` on our own judgement; shipping the route without the page.](<redacted>.md) | platform identity / authz + privacy |
|
|
302
|
+
| 0213 | [**The platform T&C acceptance is the hub's own witness, written once, and consent becomes two columns** ([task 1002913](https://cloudbongos.com/builders#/task/1002913), goal 1000045). ADR 0141 §4 deferred consent for cross-project aggregation to "the platform T&C" and ADR 0171 D3 wrote its own definition around the moment that acceptance "can later be captured" - but no acceptance record existed, and while `/terms` DOES exist (task 1003251) it says nothing about work crossing between projects, so the public-profile premise ("already carries platform T&C consent") was untrue for every account. The scouting.js:6 pointer the task named had also been overwritten by ADR 0210, so the debt was going invisible rather than getting paid. Decision: `terms_accepted_at` + `terms_version` on `platform_identity_accounts` (migration 019), written ONLY by `upsertAccount`'s `activate` branch - one caller, /sso/authorize, where `activate: true` is a source literal and every field comes from the hub's resolved session (the ADR 0205 D3 property; a record a caller could assert is evidence of nothing). COALESCE makes it write-once, so the column keeps meaning WHEN consent was given. The consent axis in `account-visibility.js` becomes `profile_state = 'active' AND terms_accepted_at IS NOT NULL`, composed into every other predicate, which is what makes the tie structural rather than a promise. NO backfill (D4): existing active accounts re-accept on their next hub sign-in - ADR 0171 D3's own rule and P03's ratified rationale - and that visible consequence, plus the wording itself, went to the owner on blocker 1000123. Rejected: an acceptances TABLE (no event stream yet), a click-through gate (an interstitial in a redirect built to be invisible; additive later), backfilling (records a consent that never happened), deriving profile_state from the timestamp, and putting the predicate in `terms-acceptance.js` (a second owner of an effective-visibility answer, which ADR 0202 D4 forbids).](<redacted>.md) | platform identity / consent / privacy |
|
|
301
303
|
|
|
302
304
|
> ⚠️ **Numbering collisions are now machine-enforced, not narrated.** Twenty numbers are shared by two ADRs each — assigned in parallel sessions before anything checked. The files keep their filenames (renumbering would break every existing citation, and a number once assigned is never reused), and this table disambiguates each pair as `NNNN-a` / `NNNN-b`. The authoritative list is `LEGACY_DUPLICATE_ADRS` in [`scripts/gds/adr-namespace.js`](../../scripts/gds/adr-namespace.js), frozen by exact filename: a **new** collision — or a third file joining a legacy number, or a rename of either half — hard-fails the `unit` CI gate, as does an index row that points at no file or an ADR with no row. The list may only shrink. This paragraph used to narrate the collisions one by one and had fallen eight behind reality; see [ADR 0195](<redacted>.md) for why the check exists and why the pairs are grandfathered rather than renumbered.
|