@bongos/core 1.19.424 → 1.19.426
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 +59 -34
- package/docs/adr/0210-scouting-graduates-on-an-instance-asserted-owner-claim.md +100 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +1 -1
- package/docs/copy-inventory.md +184 -184
- package/docs/copy-registry.json +235 -235
- package/docs/design/mocks/hub/wizard.html +421 -0
- package/docs/design/projects-hub-direction-v2.md +26 -0
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/people.js +5 -5
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/settings.js +12 -12
- package/modules/platform-identity/account-visibility.js +47 -1
- package/modules/platform-identity/platform-identity.js +14 -11
- package/modules/platform-identity/routes/scouting.js +61 -21
- package/modules/platform-identity/routes/sso.js +6 -0
- package/modules/platform-identity/scouting-authz.js +139 -0
- package/modules/ui-design/kit/serve.js +3 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +63 -0
- package/public-landing/projects.html +207 -199
- package/public-landing/projects.probes.json +7 -1
- package/public-landing/projects.states.json +7 -1
- package/src/bongos/auth.js +59 -3
- package/src/module-api.js +1 -1
- package/tests/account_privacy_flags.mjs +23 -13
- package/tests/effective_visibility_predicate.mjs +49 -4
- package/tests/projects_hub.mjs +56 -0
- package/tests/scouting_owner_access_and_reach.mjs +544 -0
- package/tests/scouting_page_gate.mjs +185 -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.426",
|
|
6
|
+
"core_contract": "1.19.426",
|
|
7
|
+
"source_commit": "6e32dc947ac0c7879f4df33ac9a6642485b921f9",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-29T19:
|
|
9
|
+
"built_at": "2026-08-29T19:24:28.557Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 361,
|
|
13
13
|
"agent_docs_stubbed": 23,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1729,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2113,
|
|
20
|
+
"tree_sha256": "dbe9e6dd3b2d878e12342577089f1fb18739cf3b570acea9df12a15b9ba6a42b",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1549,10 +1549,15 @@
|
|
|
1549
1549
|
"mode": "0000644",
|
|
1550
1550
|
"sha256": "bdf26860d1f10413a30a770ab420d49fdf58f922d75e23fb1cf9f174f7e469e4"
|
|
1551
1551
|
},
|
|
1552
|
+
{
|
|
1553
|
+
"path": "docs/adr/0210-scouting-graduates-on-an-instance-asserted-owner-claim.md",
|
|
1554
|
+
"mode": "0000644",
|
|
1555
|
+
"sha256": "e7cc54c2186bda5ae76039a57ab285320bdd57c6879a51e55bcd145fe9261ad5"
|
|
1556
|
+
},
|
|
1552
1557
|
{
|
|
1553
1558
|
"path": "docs/adr/README.md",
|
|
1554
1559
|
"mode": "0000644",
|
|
1555
|
-
"sha256": "
|
|
1560
|
+
"sha256": "3aa9b5f26de5b7e4911ce1ee8debb6fd58dc1543e487a1ec1a51e5995a6a473c"
|
|
1556
1561
|
},
|
|
1557
1562
|
{
|
|
1558
1563
|
"path": "docs/api-reference.md",
|
|
@@ -1562,7 +1567,7 @@
|
|
|
1562
1567
|
{
|
|
1563
1568
|
"path": "docs/api/openapi.json",
|
|
1564
1569
|
"mode": "0000644",
|
|
1565
|
-
"sha256": "
|
|
1570
|
+
"sha256": "1a80f11c01764ec8e1542f9f5b3b2b37123656778646dd316c1e32180973b310"
|
|
1566
1571
|
},
|
|
1567
1572
|
{
|
|
1568
1573
|
"path": "docs/architecture.md",
|
|
@@ -1587,12 +1592,12 @@
|
|
|
1587
1592
|
{
|
|
1588
1593
|
"path": "docs/copy-inventory.md",
|
|
1589
1594
|
"mode": "0000644",
|
|
1590
|
-
"sha256": "
|
|
1595
|
+
"sha256": "f554b543f8f13d00649476abd2aba52d973eae5db9dc1fede6956c63f1d6e0e7"
|
|
1591
1596
|
},
|
|
1592
1597
|
{
|
|
1593
1598
|
"path": "docs/copy-registry.json",
|
|
1594
1599
|
"mode": "0000644",
|
|
1595
|
-
"sha256": "
|
|
1600
|
+
"sha256": "adb2bd7b686eb3943c7bdf8c2129d0de3e80ebb9db99b7507a4320a569beb383"
|
|
1596
1601
|
},
|
|
1597
1602
|
{
|
|
1598
1603
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -1704,6 +1709,11 @@
|
|
|
1704
1709
|
"mode": "0000644",
|
|
1705
1710
|
"sha256": "fe34413d588f2b1afd327a9246a8ce5c995cbe7e5e50232db8115d8e615b6c5d"
|
|
1706
1711
|
},
|
|
1712
|
+
{
|
|
1713
|
+
"path": "docs/design/mocks/hub/wizard.html",
|
|
1714
|
+
"mode": "0000644",
|
|
1715
|
+
"sha256": "11a86d23198e58f302a899ba8cd4452bf0247fa537644540bd45dfa5f1af17ea"
|
|
1716
|
+
},
|
|
1707
1717
|
{
|
|
1708
1718
|
"path": "docs/design/mocks/landing-page-mock.html",
|
|
1709
1719
|
"mode": "0000644",
|
|
@@ -1882,7 +1892,7 @@
|
|
|
1882
1892
|
{
|
|
1883
1893
|
"path": "docs/design/projects-hub-direction-v2.md",
|
|
1884
1894
|
"mode": "0000644",
|
|
1885
|
-
"sha256": "
|
|
1895
|
+
"sha256": "63e580a90c728ac1cf39b8c7a26e660839fdd831e1c2ec5f9b9fb5dfbd7316c9"
|
|
1886
1896
|
},
|
|
1887
1897
|
{
|
|
1888
1898
|
"path": "docs/design/projects-hub-direction.md",
|
|
@@ -1932,7 +1942,7 @@
|
|
|
1932
1942
|
{
|
|
1933
1943
|
"path": "docs/file-map.md",
|
|
1934
1944
|
"mode": "0000644",
|
|
1935
|
-
"sha256": "
|
|
1945
|
+
"sha256": "a5b1b7926513202fefed48db17bb50fa7727a15ee48e6981b524fc4f76db28ca"
|
|
1936
1946
|
},
|
|
1937
1947
|
{
|
|
1938
1948
|
"path": "docs/handoff-template.md",
|
|
@@ -1942,7 +1952,7 @@
|
|
|
1942
1952
|
{
|
|
1943
1953
|
"path": "docs/module-api-changelog.md",
|
|
1944
1954
|
"mode": "0000644",
|
|
1945
|
-
"sha256": "
|
|
1955
|
+
"sha256": "b32e4941a78d82f7cbbdb5a42608ebb46370b46088ce56432e4c8ec19c2365ec"
|
|
1946
1956
|
},
|
|
1947
1957
|
{
|
|
1948
1958
|
"path": "docs/modules-contract.md",
|
|
@@ -4087,7 +4097,7 @@
|
|
|
4087
4097
|
{
|
|
4088
4098
|
"path": "modules/hall-ui/public/people.js",
|
|
4089
4099
|
"mode": "0000644",
|
|
4090
|
-
"sha256": "
|
|
4100
|
+
"sha256": "7e0e2f091fb8c286220bbd5f790497ab54c78e569f59519a00fa5739d11bd451"
|
|
4091
4101
|
},
|
|
4092
4102
|
{
|
|
4093
4103
|
"path": "modules/hall-ui/public/primer.css",
|
|
@@ -4187,12 +4197,12 @@
|
|
|
4187
4197
|
{
|
|
4188
4198
|
"path": "modules/hall-ui/public/settings.html",
|
|
4189
4199
|
"mode": "0000644",
|
|
4190
|
-
"sha256": "
|
|
4200
|
+
"sha256": "81da0b750da880c411a8604687a8c300852c631ffdd07d4fd1bb3c4533e650c0"
|
|
4191
4201
|
},
|
|
4192
4202
|
{
|
|
4193
4203
|
"path": "modules/hall-ui/public/settings.js",
|
|
4194
4204
|
"mode": "0000644",
|
|
4195
|
-
"sha256": "
|
|
4205
|
+
"sha256": "d3ddd434100c65a5839e3004ad1a5c4537d2da23ecd4441ee3eff3aa0ee8b5a1"
|
|
4196
4206
|
},
|
|
4197
4207
|
{
|
|
4198
4208
|
"path": "modules/hall-ui/public/shell.js",
|
|
@@ -4792,7 +4802,7 @@
|
|
|
4792
4802
|
{
|
|
4793
4803
|
"path": "modules/platform-identity/account-visibility.js",
|
|
4794
4804
|
"mode": "0000644",
|
|
4795
|
-
"sha256": "
|
|
4805
|
+
"sha256": "04deda249e65ceb416064ecb503ff101600c57ae84be9bf2ecab1de4b6701879"
|
|
4796
4806
|
},
|
|
4797
4807
|
{
|
|
4798
4808
|
"path": "modules/platform-identity/application-echo.js",
|
|
@@ -4912,7 +4922,7 @@
|
|
|
4912
4922
|
{
|
|
4913
4923
|
"path": "modules/platform-identity/platform-identity.js",
|
|
4914
4924
|
"mode": "0000644",
|
|
4915
|
-
"sha256": "
|
|
4925
|
+
"sha256": "f2a30bba329bf7d9940ab0af4c28417ba316c317cfc83e37e9a082cb9a160897"
|
|
4916
4926
|
},
|
|
4917
4927
|
{
|
|
4918
4928
|
"path": "modules/platform-identity/pollers/code-sweep.js",
|
|
@@ -4952,12 +4962,17 @@
|
|
|
4952
4962
|
{
|
|
4953
4963
|
"path": "modules/platform-identity/routes/scouting.js",
|
|
4954
4964
|
"mode": "0000644",
|
|
4955
|
-
"sha256": "
|
|
4965
|
+
"sha256": "e9afb0c1839c2933b1222158a3fccca8c3e3659282bb705e9d5c80575471200c"
|
|
4956
4966
|
},
|
|
4957
4967
|
{
|
|
4958
4968
|
"path": "modules/platform-identity/routes/sso.js",
|
|
4959
4969
|
"mode": "0000644",
|
|
4960
|
-
"sha256": "
|
|
4970
|
+
"sha256": "b31ded3abdef839e72f3f87b5a96984332c1836b8a88a5c452aeaaefc4b713d6"
|
|
4971
|
+
},
|
|
4972
|
+
{
|
|
4973
|
+
"path": "modules/platform-identity/scouting-authz.js",
|
|
4974
|
+
"mode": "0000644",
|
|
4975
|
+
"sha256": "b4da61899339ac5e9fac517efa4b3d98e39a8e7d38fd8b0cffbd8464ba85e902"
|
|
4961
4976
|
},
|
|
4962
4977
|
{
|
|
4963
4978
|
"path": "modules/platform-identity/tests/platform-identity.mjs",
|
|
@@ -5472,7 +5487,7 @@
|
|
|
5472
5487
|
{
|
|
5473
5488
|
"path": "modules/ui-design/kit/serve.js",
|
|
5474
5489
|
"mode": "0000644",
|
|
5475
|
-
"sha256": "
|
|
5490
|
+
"sha256": "fed82c37da8cae16ca1a29e521f83a8f91c72a6de4b4b6a9b21b0ed2e633d45b"
|
|
5476
5491
|
},
|
|
5477
5492
|
{
|
|
5478
5493
|
"path": "modules/ui-design/module.json",
|
|
@@ -5517,12 +5532,12 @@
|
|
|
5517
5532
|
{
|
|
5518
5533
|
"path": "package-lock.json",
|
|
5519
5534
|
"mode": "0000644",
|
|
5520
|
-
"sha256": "
|
|
5535
|
+
"sha256": "c16499122c1680ba466c0245ddb69d6225fd9152e76f9e626c8522ff237cd20b"
|
|
5521
5536
|
},
|
|
5522
5537
|
{
|
|
5523
5538
|
"path": "package.json",
|
|
5524
5539
|
"mode": "0000644",
|
|
5525
|
-
"sha256": "
|
|
5540
|
+
"sha256": "ba7f2f7a6795a2ecd9316195ff8ac0df597e077f7d8bce4efed6d45a5945ea88"
|
|
5526
5541
|
},
|
|
5527
5542
|
{
|
|
5528
5543
|
"path": "public-docs/index.html",
|
|
@@ -5762,7 +5777,7 @@
|
|
|
5762
5777
|
{
|
|
5763
5778
|
"path": "public-landing/assets/cosmos.css",
|
|
5764
5779
|
"mode": "0000644",
|
|
5765
|
-
"sha256": "
|
|
5780
|
+
"sha256": "80f54ccdf5093065b48770c8c855f69d588483e19247446dc4a5d5735a3aaf11"
|
|
5766
5781
|
},
|
|
5767
5782
|
{
|
|
5768
5783
|
"path": "public-landing/assets/verro-medallion.png",
|
|
@@ -5982,17 +5997,17 @@
|
|
|
5982
5997
|
{
|
|
5983
5998
|
"path": "public-landing/projects.html",
|
|
5984
5999
|
"mode": "0000644",
|
|
5985
|
-
"sha256": "
|
|
6000
|
+
"sha256": "7856b0f10d782ea43cde64fe3dbbce0c6b295fbd58bef592e87973e298d9f15b"
|
|
5986
6001
|
},
|
|
5987
6002
|
{
|
|
5988
6003
|
"path": "public-landing/projects.probes.json",
|
|
5989
6004
|
"mode": "0000644",
|
|
5990
|
-
"sha256": "
|
|
6005
|
+
"sha256": "d8667a5f7ccd7e03b6cda3dcfd1646a4927a68fdcc93480564943d0915db05a0"
|
|
5991
6006
|
},
|
|
5992
6007
|
{
|
|
5993
6008
|
"path": "public-landing/projects.states.json",
|
|
5994
6009
|
"mode": "0000644",
|
|
5995
|
-
"sha256": "
|
|
6010
|
+
"sha256": "ad16ce4e24cbcee908c963022abf49b4e7f17ab1715ede31f743e278343059d6"
|
|
5996
6011
|
},
|
|
5997
6012
|
{
|
|
5998
6013
|
"path": "public-landing/terms.html",
|
|
@@ -7397,7 +7412,7 @@
|
|
|
7397
7412
|
{
|
|
7398
7413
|
"path": "src/bongos/auth.js",
|
|
7399
7414
|
"mode": "0000644",
|
|
7400
|
-
"sha256": "
|
|
7415
|
+
"sha256": "517e25a2ca9b9adafa993725802af9e7fc1708f0a5609102ddce593e82478ca2"
|
|
7401
7416
|
},
|
|
7402
7417
|
{
|
|
7403
7418
|
"path": "src/bongos/cascade-dispatch.js",
|
|
@@ -7652,7 +7667,7 @@
|
|
|
7652
7667
|
{
|
|
7653
7668
|
"path": "src/module-api.js",
|
|
7654
7669
|
"mode": "0000644",
|
|
7655
|
-
"sha256": "
|
|
7670
|
+
"sha256": "4ca579815846fc51d427439c0e5e9d15e6102d628fe97b013c0234d59fe691c2"
|
|
7656
7671
|
},
|
|
7657
7672
|
{
|
|
7658
7673
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7712,7 +7727,7 @@
|
|
|
7712
7727
|
{
|
|
7713
7728
|
"path": "tests/account_privacy_flags.mjs",
|
|
7714
7729
|
"mode": "0000644",
|
|
7715
|
-
"sha256": "
|
|
7730
|
+
"sha256": "d0c32156de102ab98447542def38978caded0dbe03e7a6f5004240484ba037ec"
|
|
7716
7731
|
},
|
|
7717
7732
|
{
|
|
7718
7733
|
"path": "tests/achievements_reconcile.mjs",
|
|
@@ -8517,7 +8532,7 @@
|
|
|
8517
8532
|
{
|
|
8518
8533
|
"path": "tests/effective_visibility_predicate.mjs",
|
|
8519
8534
|
"mode": "0000644",
|
|
8520
|
-
"sha256": "
|
|
8535
|
+
"sha256": "3563a287a7faa8bdf26dbde292584f62b4514814a0cfdf99e58e13f02514233b"
|
|
8521
8536
|
},
|
|
8522
8537
|
{
|
|
8523
8538
|
"path": "tests/est_advisory.mjs",
|
|
@@ -9717,7 +9732,7 @@
|
|
|
9717
9732
|
{
|
|
9718
9733
|
"path": "tests/projects_hub.mjs",
|
|
9719
9734
|
"mode": "0000644",
|
|
9720
|
-
"sha256": "
|
|
9735
|
+
"sha256": "670cc7c20dbbc1da38d8a79bc5ee9dcead2453a2f782ef5354b1f91cae13d56d"
|
|
9721
9736
|
},
|
|
9722
9737
|
{
|
|
9723
9738
|
"path": "tests/projects_hub_pre_uat.mjs",
|
|
@@ -9974,6 +9989,16 @@
|
|
|
9974
9989
|
"mode": "0000644",
|
|
9975
9990
|
"sha256": "b499ea62cdc1a615e565ce7b0379b2cab83b9aaa8098361f94d3eb93ebc7f927"
|
|
9976
9991
|
},
|
|
9992
|
+
{
|
|
9993
|
+
"path": "tests/scouting_owner_access_and_reach.mjs",
|
|
9994
|
+
"mode": "0000644",
|
|
9995
|
+
"sha256": "5c53f51280b77816b5498808ab097a209a368c2e3b6cbdbc6a8ed2a55e11d8b9"
|
|
9996
|
+
},
|
|
9997
|
+
{
|
|
9998
|
+
"path": "tests/scouting_page_gate.mjs",
|
|
9999
|
+
"mode": "0000644",
|
|
10000
|
+
"sha256": "33c586229a164e9a90804bd00485f9b3cfc665213adf1717841103d2cd5d8293"
|
|
10001
|
+
},
|
|
9977
10002
|
{
|
|
9978
10003
|
"path": "tests/scouting_rollup_fanout.mjs",
|
|
9979
10004
|
"mode": "0000644",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# ADR 0210 — The recruiting directory graduates on an instance-asserted ownership claim, and the recruiting opt-out gates the directory and nothing else
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-08-29
|
|
5
|
+
- **Task:** [task 1002290](https://cloudbongos.com/builders#/task/1002290) (R14, goal 1000045 — Public builder profiles, criterion C5)
|
|
6
|
+
- **Relates to:** [ADR 0201](<redacted>.md) D3 (what "project owner" means, and the hub-held owner list it forbids as a gate), [ADR 0141](<redacted>.md) §3/§4 (federated identity; authority stays local), [ADR 0016](<redacted>.md) (cross-project numbers are descriptive, never authority), [ADR 0205](<redacted>.md) D1/D2/D4 (the hub's own sign-in witness), [ADR 0171](<redacted>.md) D2/D3 (consent in the query), [ADR 0157](<redacted>.md) (`project.curate` archon → metic), `docs/specs/<redacted>.md` D1/D2 (the recruiting opt-out), `docs/specs/<redacted>.md` D3 (leaderboards, which the opt-out must NOT touch)
|
|
7
|
+
|
|
8
|
+
> **0208** is held by a session in flight; this ADR takes 0210 rather than race it (the ADR 0199 / 0202 / 0207 precedent).
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
Goal 1000045 C5 says **"project owners (not only Archons) browse builder profiles to recruit"**, and R14 is the task that makes it true. Two things stood between the goal and that sentence, and they turned out to be independent problems wearing one task id.
|
|
13
|
+
|
|
14
|
+
**The reach flag gated nothing.** `recruiter_discoverable` (privacy spec D1) shipped with task 1002966 and was wired into the *effective-answer* helper by task 1002967 — which then reported, accurately, that it still protected nobody: *"a public account that opted out of recruiting still appears in `/scouting`."* The Settings row said so out loud, in the future tense, because a toggle that claims protection it does not have is worse than no toggle. So one half of this task is a predicate.
|
|
15
|
+
|
|
16
|
+
**"Archon-only" was already stale, and the phrase in the task was hiding a real question.** R14's brief says "widen the Archon-only `/scouting`", but ADR 0157 had already lowered `project.curate` from archon to metic, and `src/bongos/auth.js` carried a comment predicting that R14 would widen the atom's audience further and that the page gate would "follow it automatically". Reading that literally would have made this task a floor change — and a floor change would have missed the audience the criterion names. **A project owner is not a rank.** Somebody who registers a project on the platform is an Archon *on their own instance* and, on the hub where the directory lives, very often a `xenos` holding nothing at all. No floor reaches them without also reaching every stranger who ever signed in.
|
|
17
|
+
|
|
18
|
+
## Decision
|
|
19
|
+
|
|
20
|
+
### D1 — "Project owner" on the hub is an **instance-asserted, hub-witnessed** `membership_kind = 'owner'`, and never the catalog's `owner_github_id`
|
|
21
|
+
|
|
22
|
+
ADR 0201 D3 already answered *who* a project owner is — the project's own archon: the registrant `maybeSeatFirstAdmin` seated, plus whoever that archon grants on that instance — and it forbade one specific implementation: `platform_identity_projects.owner_github_id` **"stays advisory attribution and is never read as a gate"**, because a hub-held owner list consulted before an admission is the hub deciding who may admit on someone else's box.
|
|
23
|
+
|
|
24
|
+
That prohibition is about `owner_github_id`, and it is about *admission*. Both halves matter, because the record this ADR does gate on is a different row with a different provenance:
|
|
25
|
+
|
|
26
|
+
| | `platform_identity_projects.owner_github_id` | `<redacted>.membership_kind` |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| Written by | hub curation | the **project itself**, at `POST /sso/membership/check-in`, authenticated by `client_id` + `client_secret` |
|
|
29
|
+
| Decided by | a hub row | the instance's own config — `src/bongos/auth-admission.js` `membershipKind()` |
|
|
30
|
+
| Vouched by | nothing | the hub's **own** witness: `touchMembership` refuses to write unless `signin_verified_at` is set, stamped only where the hub itself authenticated the human (ADR 0205 D1/D2) |
|
|
31
|
+
|
|
32
|
+
So gating on `membership_kind = 'owner'` is the hub reading **the instance's own claim about its own archon, mirrored** — authority stays local, exactly as ADR 0141 requires. And the act being gated is a **hub read of the hub's own directory**, not an admission on anyone else's box. Neither leg of ADR 0201 D3's objection reaches it.
|
|
33
|
+
|
|
34
|
+
The witness is not decoration. Client credentials say which *project* is calling; they say nothing about *who* the `github_id` in the body is (ADR 0205, security report 1000027). An owner claim the hub never witnessed is a client's unproven self-report, and the gate refuses it — in SQL *and* in the JS predicate the query's result is passed through, so deleting either layer is caught.
|
|
35
|
+
|
|
36
|
+
### D2 — The two doors are an OR, the rank door is passed in **already built**, and the new door can only ever admit
|
|
37
|
+
|
|
38
|
+
`GET /scouting` admits a `project.curate` holder **OR** a project owner. The OR lives in `modules/platform-identity/scouting-authz.js` (the `modules/lifecycle/goal-authz.js` shape — owner-or-rank as a pure module sibling), never in `platform-identity.js`, which is at the file-size ratchet.
|
|
39
|
+
|
|
40
|
+
Three properties are deliberate, and each rules out an implementation that would have looked simpler:
|
|
41
|
+
|
|
42
|
+
1. **Ownership is asked first; a "no" falls through to the UNMODIFIED permission gate.** So a caller who is neither gets byte-for-byte the 403 this route always produced — same code, same `required`/`held`, same floors map naming the metic floor. The widening cannot change, soften, or swallow a refusal. Intercepting the permission gate's response to retry would have been the alternative, and it would have owned that 403's shape forever.
|
|
43
|
+
2. **`requirePermission('project.curate')` is spelled in the route's own middleware list** and handed to the helper already built. A gate hidden inside the helper is invisible to `route-rank-check.js` and `gen-api-docs.js`, which classify a route from its middleware — this route would have been recorded as `any-builder`, wrong in the dangerous direction, and the generated API reference would have published it that way.
|
|
44
|
+
3. **The ownership question fails CLOSED onto the permission gate.** A database blip must not admit and must not 500; it falls to the rank door, which denies unless the caller holds the atom. The failure mode of the new door is "the old door, unchanged".
|
|
45
|
+
|
|
46
|
+
**No new permission atom is minted.** ADR 0201 D3 says this goal mints none, and the recruiting atom the specs name — `project.recruit` — belongs to the project-admin-console wave ([spike 1002786](https://cloudbongos.com/builders#/task/1002786) R3), which has not landed. Minting it here would fork its definition across two waves. When it lands, it replaces the `project.curate` half of this OR; the ownership half is unaffected.
|
|
47
|
+
|
|
48
|
+
### D3 — The page gate follows the route through one declared `alsoAdmits` door, or the widening is invisible
|
|
49
|
+
|
|
50
|
+
`/builders/people` fronts `GET /scouting` and is gated by `requireProjectCuratePage`, whose whole vocabulary was "a permission". A widened route behind a permission-only page gate leaves the new audience staring at a redirect — the feature would exist and be unreachable by the people it was built for.
|
|
51
|
+
|
|
52
|
+
`PAGE_GATES` therefore gains an optional `alsoAdmits`, naming a function on the `platform-identity.hub` port (the module that owns the ownership fact). It is declared on exactly one gate, consulted **only after the permission check has already refused**, and fails closed every way it can fail — no port (every non-hub instance), no function, or a throw all deny. It can widen; it cannot narrow.
|
|
53
|
+
|
|
54
|
+
It is also consulted on the *authority-outage* branch (`held === null`), which is deliberate and worth stating: the ADR 0016 rule that "admitting when the authority system is missing is a privilege escalation" is about admitting *on the absence of an answer*. Here a different, still-working authority answered yes. Not asking would make the page narrower than the route it fronts, which is the drift this gate exists to prevent.
|
|
55
|
+
|
|
56
|
+
### D4 — Reach composes into a **directory** predicate, and deliberately does not fold into the rollup one
|
|
57
|
+
|
|
58
|
+
`account-visibility.js` is the sole owner of every effective-visibility answer, and `tests/effective_visibility_predicate.mjs` enforces that no other file may spell one. So the SQL twin of the reach COALESCE (`recruiterReachSql`) lands there beside its JS twin, and a new `scoutingListableSql` composes it with `rollupVisibleSql`. `listScoutingRollup` — the one reader behind both `/scouting` and the hall's `/people` — joins that.
|
|
59
|
+
|
|
60
|
+
**Reach is NOT folded into `rollupVisibleSql`**, and this is the decision most likely to be "simplified" later. `hide_stats` suppresses numbers *everywhere, for every reader*; the recruiting opt-out governs *the recruiting surface only*. The community spec's D3 says it in as many words — *"the recruiting opt-out does not affect leaderboards — it governs the recruiting surface only"*. Folding reach downward would silently empty a future public leaderboard of every opted-out builder, and it would do it invisibly, because a leaderboard missing people looks exactly like a leaderboard.
|
|
61
|
+
|
|
62
|
+
That makes `listScoutingRollup` **the recruiting reader specifically**, not a generic rollup reader. A public leaderboard must join `rollupVisibleSql` and must not reuse it. That constraint is written into the function's own doc-comment, into `scoutingListableSql`'s, and into an assertion in `tests/effective_visibility_predicate.mjs` that fails if reach ever appears in the rollup predicate.
|
|
63
|
+
|
|
64
|
+
### D5 — No migration, and that is the finding rather than an omission
|
|
65
|
+
|
|
66
|
+
R14 was pre-assigned `<redacted>*`. Nothing needed it: both columns exist (migration 012), and `<redacted>` already carries the `(github_id)` index migration 003 added for "My Projects" — the ownership probe is that same lookup plus two cheap column tests on a handful of rows, stopping at the first. A partial index would be speculative optimisation against a table a builder has a handful of rows in. The number stays unused.
|
|
67
|
+
|
|
68
|
+
### D6 — The ownership claim answers to the platform's own revocation switch
|
|
69
|
+
|
|
70
|
+
`hasOwnerMembership` joins `platform_identity_sso_clients` and requires `status = 'active'`. This was added after the adversarial review, and it closes the only hole in D1 that mattered: **a membership row never expires and nothing ages it out.** Once written, the only things that stop it saying `owner` are the instance reporting a lower kind at a later check-in, or the builder leaving of their own accord — neither of which is available to the platform operator dealing with a rogue or compromised project. Suspending its client (`setClientStatus`) already stops that project signing anyone in; a read *derived* from that project must answer to the same switch, or suspending a project would leave its owner reading every builder on the platform, indefinitely.
|
|
71
|
+
|
|
72
|
+
The general rule, worth stating once because the next derived permission will meet it too: **a durable row used as an authorization input needs a revocation dimension, and the right one is whatever lever already exists — not a new one.**
|
|
73
|
+
|
|
74
|
+
## Consequences
|
|
75
|
+
|
|
76
|
+
- **The opt-out is real, on both surfaces, immediately.** A public, unhidden, active account that switches "Discoverable by recruiters" off leaves `/scouting` and the hall's People directory on the next read; switching it back on restores them. The Settings copy moves to the present tense on task 1002967's own precedent, and `tests/account_privacy_flags.mjs` flips with it — the assertion pair is unchanged in kind (copy must track enforcement), only in tense.
|
|
77
|
+
- **A private account that is recruiter-discoverable is still absent.** Reach never buys addressability: the spec's D2 "private · discoverable" row is served by the D3 *sliver* (`getRecruiterSliver`, still unbuilt behind task 1002968), never by the full rollup. Fail-closed until it lands.
|
|
78
|
+
- **⚠ DELEGATES DO NOT REACH THE DIRECTORY YET, AND THAT IS AN OWNER CALL.** The goal defines "project owner" as *"the registrant of the project on the platform, plus delegates they grant"*. `src/bongos/auth-admission.js` reports `membership_kind: 'owner'` **for the instance's `firstAdminLogin()` only** — so the **registrant half works and the delegate half does not**: a second archon on a project holds `member` on the hub and reaches `/scouting` only if they separately hold `project.curate` there. Closing it means the instance reporting `owner` for *any* archon at check-in, which widens what `owner` means on a shipped surface (My Projects buckets projects by it) and changes what every federated instance tells the hub. That is a product call, not an implementation detail, so it is filed as [blocker 1000122](https://cloudbongos.com/builders#/blocker/1000122) with the delegate work itself tracked as [task 1003350](https://cloudbongos.com/builders#/task/1003350), which the blocker is linked to. **Nothing in this ADR changes when it is answered** — the gate reads whatever the instance reports, which is precisely why the registrant half could ship on its own rather than waiting behind a question only the owner can settle.
|
|
79
|
+
- **Being scoutable, and opting out of it, confer nothing.** ADR 0016 holds in both directions and is proved in both: an opted-out builder who owns a project still *reads* the directory while being absent from it — including from their own copy of it, because the read is viewer-independent and a reader must never be able to infer a privacy setting from whose list they got.
|
|
80
|
+
- **The proof lives in the DB-free lane** (`tests/scouting_owner_access_and_reach.mjs`): a fake pool whose filter is built from each statement's own `WHERE` clause, under the real route, the real `requirePermission` over the real government catalog, and real HTTP. CI has no Postgres, so a real-DB proof there would self-skip and gate nothing (task 1002282). Mutation-checked three ways — strip the reach half, kill the ownership door, drop the SQL witness term — each goes red.
|
|
81
|
+
- **`/scouting`'s recorded rank is unchanged** (`metic+archon`), because the ownership door is invisible to a static scanner and the atom is spelled where the auditor reads it. The generated API reference says the same thing it said before, which is the honest answer: the *floor* did not move, a second door opened beside it.
|
|
82
|
+
|
|
83
|
+
## Honest limits — what this task did NOT close
|
|
84
|
+
|
|
85
|
+
These came out of the adversarial review this task ran on its own diff. Each is real, each is recorded rather than quietly left, and none of them is fixed here — writing them down is the point, because a limit nobody records reads later as a guarantee.
|
|
86
|
+
|
|
87
|
+
- **⚠ THE HALL NAV STILL DOES NOT SHOW A PROJECT OWNER THE DOOR.** The page gate admits them (D3), but `modules/hall-ui/public/shell.js` still gates the People nav item on `government` (metic|archon) inside a `Community` group gated on `member` (`authed && rank !== 'xenos'`). D3's own premise is that a project owner is often a bare `xenos` — so that visitor sees neither the group nor the link, and the only way in is typing `/builders/people`. This is the same "exists but is unreachable" shape D3 exists to prevent, moved one layer up. It is **not** fixed here because the fix is a hall-UI change — a new `is_project_owner` flag on the boot payload, a gate key in `gateAllows`, and a widened group gate — that belongs with [R15](https://cloudbongos.com/builders#/task/1002291), the owner recruiting UI, which owns that surface and will need the same flag anyway. Filed as [task 1003349](https://cloudbongos.com/builders#/task/1003349). Until it lands, the widened audience is reachable by URL and by the API, not by navigation.
|
|
88
|
+
- **The write path behind the ownership claim was not tightened.** `src/bongos/auth-admission.js` reports `owner` for whoever matches the instance's `FIRST_ADMIN` config — not for whoever actually holds archon — and `touchMembership` writes whatever kind the authenticated client sends, with no cap on how many `owner` rows one client may hold. So a provisioned instance can name arbitrary GitHub accounts as its owner over time. This is a **pre-existing** property of a shipped, cross-version wire contract (ADR 0205); what this task changed is that the field is now an *authorization input*, which is why it is named here rather than left implicit. Two things bound it: client registration is operator-run, and D6's suspension join is the lever that revokes a whole rogue project at once. Tightening `touchMembership` itself belongs with the delegate decision in blocker 1000122, since both are about what an instance may assert.
|
|
89
|
+
- **`openapi.json` cannot express "or owner".** The generated `x-permissions: ["project.curate"]` and its "all required" phrasing survive, because the generator classifies a route from its middleware and there is no field for a second, non-permission audience. A client generating an access check from that field gets an over-strict answer — wrong, but erring safe.
|
|
90
|
+
- **The ownership probe runs before the rank check for every caller,** including the operator path, and on every denied `/builders/people` load. Deliberate: the ordering is what keeps the 403 byte-identical (D2 §1). It is one indexed lookup on a table where a builder has a handful of rows.
|
|
91
|
+
|
|
92
|
+
## Rejected alternatives
|
|
93
|
+
|
|
94
|
+
- **Lower `project.curate`'s floor further (to thetes or xenos).** The obvious reading of "widen the Archon-only /scouting", and wrong: a floor is a rank, a project owner is a relationship, and no floor low enough to admit a registrant excludes anyone at all. It would have delivered "everyone browses the talent directory" and called it C5.
|
|
95
|
+
- **Mint a `project.owner` atom.** ADR 0201 D3 already rejected exactly this ("a new 'project owner' permission atom") for the reason that still holds: ownership is per-project and grantable only by the project, so an atom on the hub would be a second place ownership is asserted, free to drift from the one the instance reports.
|
|
96
|
+
- **Mint `project.recruit` here.** The right atom for a recruiting surface, and it belongs to the console wave that defines it (spike 1002786 R3). Minting it in this task would fork its definition across two waves before either had shipped.
|
|
97
|
+
- **Gate on `platform_identity_projects.owner_github_id`.** The one implementation ADR 0201 D3 names and forbids; its own column comment says advisory. See D1.
|
|
98
|
+
- **Fold reach into `rollupVisibleSql`.** One predicate instead of two, and it silently breaks the leaderboards the community spec D3 explicitly protects. See D4.
|
|
99
|
+
- **Widen `membershipKind()` to report every archon as `owner`.** The fix for the delegate gap, and it changes a cross-version wire contract plus the meaning of a value a shipped surface buckets on. Filed as blocker 1000122 instead of guessed.
|
|
100
|
+
- **Leave the page gate alone and ship the route.** Half a graduation: the audience the criterion names would hold an API they could not open a page for.
|
package/docs/adr/README.md
CHANGED
|
@@ -297,5 +297,6 @@ 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
|
+
| 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 |
|
|
300
301
|
|
|
301
302
|
> ⚠️ **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.
|
package/docs/api/openapi.json
CHANGED
|
@@ -12055,7 +12055,7 @@
|
|
|
12055
12055
|
"scouting"
|
|
12056
12056
|
],
|
|
12057
12057
|
"summary": "GET /scouting",
|
|
12058
|
-
"description": "the internal talent directory across every project.
|
|
12058
|
+
"description": "the internal talent directory across every project. Read by a `project.curate` holder OR a project owner (ADR 0210, task 1002290); a caller who is neither gets the unchanged permission refusal before any cross-builder data is read.\n\n**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `project.curate` (all required).",
|
|
12059
12059
|
"x-rank": "metic+archon",
|
|
12060
12060
|
"x-source": "modules/platform-identity/routes/scouting.js",
|
|
12061
12061
|
"x-permissions": [
|