@bongos/core 1.19.406 → 1.19.408
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 +41 -26
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0201-cross-project-application-vouched-row-live-record.md +112 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +3 -0
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +172 -172
- package/docs/copy-registry.json +219 -219
- package/docs/design/mocks/hub/planets.html +232 -0
- package/docs/design/projects-hub-direction-v2.md +10 -0
- package/modules/platform-identity/migrations/platform_identity_012_project_template.sql +26 -0
- package/modules/platform-identity/platform-identity.js +18 -8
- package/modules/platform-identity/routes/projects.js +4 -0
- package/modules/platform-identity/tests/platform-identity.mjs +29 -0
- package/modules/ui-design/kit/fixtures/featured.json +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +107 -68
- package/public-landing/projects.html +40 -40
- package/public-landing/projects.states.json +1 -1
- package/src/module-api.js +14 -19
- package/tests/projects_hub.mjs +109 -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.408",
|
|
6
|
+
"core_contract": "1.19.408",
|
|
7
|
+
"source_commit": "71369601472c7ae483a6ea2424d88b78bcb65cbb",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-28T22:26:12.724Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 352,
|
|
13
13
|
"agent_docs_stubbed": 23,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1664,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2039,
|
|
20
|
+
"tree_sha256": "0ea6ef5d0535e0aa9795fcebfdc4a776e146222d66075daed31f9eee36c3b9a9",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -372,7 +372,7 @@
|
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/index.d.ts",
|
|
374
374
|
"mode": "0000644",
|
|
375
|
-
"sha256": "
|
|
375
|
+
"sha256": "f695402b7bd5f2db12f1c2415f452a575312d41d7a0c7ea57750c50573941222"
|
|
376
376
|
},
|
|
377
377
|
{
|
|
378
378
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -1504,6 +1504,11 @@
|
|
|
1504
1504
|
"mode": "0000644",
|
|
1505
1505
|
"sha256": "d73c7f4ebaf18bf81e461532144422b8d343698c50964687c52b13301af42c66"
|
|
1506
1506
|
},
|
|
1507
|
+
{
|
|
1508
|
+
"path": "docs/adr/0201-cross-project-application-vouched-row-live-record.md",
|
|
1509
|
+
"mode": "0000644",
|
|
1510
|
+
"sha256": "88c0a614b2f317785e4bbe360a9aba686e4ef8f302ff75cb441763960c3b2f36"
|
|
1511
|
+
},
|
|
1507
1512
|
{
|
|
1508
1513
|
"path": "docs/adr/0202-connections-graph-and-bounded-degree.md",
|
|
1509
1514
|
"mode": "0000644",
|
|
@@ -1512,17 +1517,17 @@
|
|
|
1512
1517
|
{
|
|
1513
1518
|
"path": "docs/adr/README.md",
|
|
1514
1519
|
"mode": "0000644",
|
|
1515
|
-
"sha256": "
|
|
1520
|
+
"sha256": "6bd4f20377ad7bf4134be679a5875b0ef50784dc48a2937aefd6607a78bd403e"
|
|
1516
1521
|
},
|
|
1517
1522
|
{
|
|
1518
1523
|
"path": "docs/api-reference.md",
|
|
1519
1524
|
"mode": "0000644",
|
|
1520
|
-
"sha256": "
|
|
1525
|
+
"sha256": "7c92e8f6e3c000dfd7620542606973cf25ce022ef9e5bfe124a7ff4912bc91d6"
|
|
1521
1526
|
},
|
|
1522
1527
|
{
|
|
1523
1528
|
"path": "docs/api/openapi.json",
|
|
1524
1529
|
"mode": "0000644",
|
|
1525
|
-
"sha256": "
|
|
1530
|
+
"sha256": "2fde31533808aeee53406bbbf9ee5c5bbf77947b6b2619eecf2b5f7608e30b6b"
|
|
1526
1531
|
},
|
|
1527
1532
|
{
|
|
1528
1533
|
"path": "docs/architecture.md",
|
|
@@ -1547,12 +1552,12 @@
|
|
|
1547
1552
|
{
|
|
1548
1553
|
"path": "docs/copy-inventory.md",
|
|
1549
1554
|
"mode": "0000644",
|
|
1550
|
-
"sha256": "
|
|
1555
|
+
"sha256": "f31eb17c518b831422d5685d315378da4078a588b79fc4eeb90dece1db935752"
|
|
1551
1556
|
},
|
|
1552
1557
|
{
|
|
1553
1558
|
"path": "docs/copy-registry.json",
|
|
1554
1559
|
"mode": "0000644",
|
|
1555
|
-
"sha256": "
|
|
1560
|
+
"sha256": "93010c69bf1a366941e7bb226cf25aee4134423fe9bc4a0da5c0e9ce3317342e"
|
|
1556
1561
|
},
|
|
1557
1562
|
{
|
|
1558
1563
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -1599,6 +1604,11 @@
|
|
|
1599
1604
|
"mode": "0000644",
|
|
1600
1605
|
"sha256": "195002c1d320a42f15bcae68d38fd60d49facd339a34a32860b32cbebcbf15a7"
|
|
1601
1606
|
},
|
|
1607
|
+
{
|
|
1608
|
+
"path": "docs/design/mocks/hub/planets.html",
|
|
1609
|
+
"mode": "0000644",
|
|
1610
|
+
"sha256": "f6588907896fed45fa541f65e6a2bb3b55f986367360f335f5b30b2a77f90aa7"
|
|
1611
|
+
},
|
|
1602
1612
|
{
|
|
1603
1613
|
"path": "docs/design/mocks/hub/v1.html",
|
|
1604
1614
|
"mode": "0000644",
|
|
@@ -1792,7 +1802,7 @@
|
|
|
1792
1802
|
{
|
|
1793
1803
|
"path": "docs/design/projects-hub-direction-v2.md",
|
|
1794
1804
|
"mode": "0000644",
|
|
1795
|
-
"sha256": "
|
|
1805
|
+
"sha256": "b785d60798bd70e2a8f914ff93e94fc6f94723819abc9ccafff7ce96ef9921f6"
|
|
1796
1806
|
},
|
|
1797
1807
|
{
|
|
1798
1808
|
"path": "docs/design/projects-hub-direction.md",
|
|
@@ -4744,6 +4754,11 @@
|
|
|
4744
4754
|
"mode": "0000644",
|
|
4745
4755
|
"sha256": "18e3641beba26d8a515616ba9bdb1bcc83151e620d6e038243d7d67850da3be8"
|
|
4746
4756
|
},
|
|
4757
|
+
{
|
|
4758
|
+
"path": "modules/platform-identity/migrations/platform_identity_012_project_template.sql",
|
|
4759
|
+
"mode": "0000644",
|
|
4760
|
+
"sha256": "48941621e5230a23785b832fa840e0c7f7b47f3dd1b01f7d254157482c4e3e6a"
|
|
4761
|
+
},
|
|
4747
4762
|
{
|
|
4748
4763
|
"path": "modules/platform-identity/module.json",
|
|
4749
4764
|
"mode": "0000644",
|
|
@@ -4752,7 +4767,7 @@
|
|
|
4752
4767
|
{
|
|
4753
4768
|
"path": "modules/platform-identity/platform-identity.js",
|
|
4754
4769
|
"mode": "0000644",
|
|
4755
|
-
"sha256": "
|
|
4770
|
+
"sha256": "65edb9332915171309305bf610bdc2834a57913a1776cb5a9566a082115f82cd"
|
|
4756
4771
|
},
|
|
4757
4772
|
{
|
|
4758
4773
|
"path": "modules/platform-identity/pollers/code-sweep.js",
|
|
@@ -4777,7 +4792,7 @@
|
|
|
4777
4792
|
{
|
|
4778
4793
|
"path": "modules/platform-identity/routes/projects.js",
|
|
4779
4794
|
"mode": "0000644",
|
|
4780
|
-
"sha256": "
|
|
4795
|
+
"sha256": "28663a3945f415c9b9ab092c776cf92fc4134d0b8afd8bb302dd65ca7ce9fb3a"
|
|
4781
4796
|
},
|
|
4782
4797
|
{
|
|
4783
4798
|
"path": "modules/platform-identity/routes/public-profile.js",
|
|
@@ -4797,7 +4812,7 @@
|
|
|
4797
4812
|
{
|
|
4798
4813
|
"path": "modules/platform-identity/tests/platform-identity.mjs",
|
|
4799
4814
|
"mode": "0000644",
|
|
4800
|
-
"sha256": "
|
|
4815
|
+
"sha256": "aeb251b84ee3c5cc789a540280bd8ecef0358d9384aac7e833dbe544b8008a54"
|
|
4801
4816
|
},
|
|
4802
4817
|
{
|
|
4803
4818
|
"path": "modules/platform-identity/ui/.gitkeep",
|
|
@@ -5237,7 +5252,7 @@
|
|
|
5237
5252
|
{
|
|
5238
5253
|
"path": "modules/ui-design/kit/fixtures/featured.json",
|
|
5239
5254
|
"mode": "0000644",
|
|
5240
|
-
"sha256": "
|
|
5255
|
+
"sha256": "c9eca47c15c2450c38d1c452faa313926e1fe0c4b1b3afa6cb533048b4249755"
|
|
5241
5256
|
},
|
|
5242
5257
|
{
|
|
5243
5258
|
"path": "modules/ui-design/kit/fixtures/leaderboard.json",
|
|
@@ -5332,12 +5347,12 @@
|
|
|
5332
5347
|
{
|
|
5333
5348
|
"path": "package-lock.json",
|
|
5334
5349
|
"mode": "0000644",
|
|
5335
|
-
"sha256": "
|
|
5350
|
+
"sha256": "63b7f6feace1305778d1fa21b32d2d33ae1ee88aebf183bedaf9f9679534ea0f"
|
|
5336
5351
|
},
|
|
5337
5352
|
{
|
|
5338
5353
|
"path": "package.json",
|
|
5339
5354
|
"mode": "0000644",
|
|
5340
|
-
"sha256": "
|
|
5355
|
+
"sha256": "b1a7bc13880d04b8cd9826e67923adf4c17401981de557fdbb65d514b5d80610"
|
|
5341
5356
|
},
|
|
5342
5357
|
{
|
|
5343
5358
|
"path": "public-docs/index.html",
|
|
@@ -5577,7 +5592,7 @@
|
|
|
5577
5592
|
{
|
|
5578
5593
|
"path": "public-landing/assets/cosmos.css",
|
|
5579
5594
|
"mode": "0000644",
|
|
5580
|
-
"sha256": "
|
|
5595
|
+
"sha256": "9c7c390683c0cd15093f599392e600bf76ceb9a9e85791ccd95a0c79361963db"
|
|
5581
5596
|
},
|
|
5582
5597
|
{
|
|
5583
5598
|
"path": "public-landing/assets/verro-medallion.png",
|
|
@@ -5687,7 +5702,7 @@
|
|
|
5687
5702
|
{
|
|
5688
5703
|
"path": "public-landing/projects.html",
|
|
5689
5704
|
"mode": "0000644",
|
|
5690
|
-
"sha256": "
|
|
5705
|
+
"sha256": "e33a777d5d938892822c4dc6f66295ce0332c6336d25b67147d56519941240ee"
|
|
5691
5706
|
},
|
|
5692
5707
|
{
|
|
5693
5708
|
"path": "public-landing/projects.probes.json",
|
|
@@ -5697,7 +5712,7 @@
|
|
|
5697
5712
|
{
|
|
5698
5713
|
"path": "public-landing/projects.states.json",
|
|
5699
5714
|
"mode": "0000644",
|
|
5700
|
-
"sha256": "
|
|
5715
|
+
"sha256": "effa79569e3ef5ddb6fbff14d3121c077521c4b9599d19d253cdca6ef8965257"
|
|
5701
5716
|
},
|
|
5702
5717
|
{
|
|
5703
5718
|
"path": "public-landing/terms.html",
|
|
@@ -7352,7 +7367,7 @@
|
|
|
7352
7367
|
{
|
|
7353
7368
|
"path": "src/module-api.js",
|
|
7354
7369
|
"mode": "0000644",
|
|
7355
|
-
"sha256": "
|
|
7370
|
+
"sha256": "83a93bb955f0dfbd5c509b0d3285d9bbec9eb582217778564aa3e70805fc9316"
|
|
7356
7371
|
},
|
|
7357
7372
|
{
|
|
7358
7373
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9362,7 +9377,7 @@
|
|
|
9362
9377
|
{
|
|
9363
9378
|
"path": "tests/projects_hub.mjs",
|
|
9364
9379
|
"mode": "0000644",
|
|
9365
|
-
"sha256": "
|
|
9380
|
+
"sha256": "a60ccd5e60f85de08178ba17cf96652d49a9ceacfdde79426dc1711cfead9906"
|
|
9366
9381
|
},
|
|
9367
9382
|
{
|
|
9368
9383
|
"path": "tests/projects_hub_pre_uat.mjs",
|
|
@@ -296,7 +296,7 @@ export interface PostMyProjectsJoinRequest { origin: string; note?: string }
|
|
|
296
296
|
export interface PostMyProjectsJoinResponse { ok: boolean; status: unknown; project: unknown; message: unknown }
|
|
297
297
|
export interface PostOverrideRequestsIdDecideRequest { decision: string; note?: string }
|
|
298
298
|
export interface PostProjectsInviteRequest { client_id: string; github_login: string }
|
|
299
|
-
export interface PostProjectsRequest { origin: string; name: string; tagline?: string; description?: string; art_url?: string; type?: string }
|
|
299
|
+
export interface PostProjectsRequest { origin: string; name: string; tagline?: string; description?: string; art_url?: string; type?: string; template?: string }
|
|
300
300
|
export interface PostProjectsResponse { project: unknown }
|
|
301
301
|
export interface PostProvisioningInstancesIdDomainRequest { domain: string }
|
|
302
302
|
export interface PostProvisioningInstancesIdDomainResponse { ok: boolean; changed: boolean; queued: unknown; domain: unknown; message: unknown; replaced?: unknown }
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# ADR 0201 — An application is the project's own `access_requests` row, vouched for by a hub-signed assertion carried in a header; the record it carries is resolved live and never stored
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-08-28
|
|
5
|
+
- **Task:** [task 1002283](https://cloudbongos.com/builders#/task/1002283) (R07, goal 1000045 — public builder profiles; criteria C3/C4)
|
|
6
|
+
- **Relates to:** ADR 0141 (federated hub IdP — identity federated, authority local; §1 the signed assertion, §3 the join relay), ADR 0171 (the profile↔rollup join, `hide_stats`, `profile_state`, `no-store`), ADR 0194 (the joinability door the public POST answers first), ADR 0192 (a gated project's member door), ADR 0016 (the DB enforces), ADR 0116 (the error envelope), ADR 0157 (`access_request.review` is *identity* and stays archon), [`docs/specs/<redacted>.md`](../specs/<redacted>.md) D3/D4/D7 (spike 1002785), [`docs/specs/<redacted>.md`](../specs/<redacted>.md) D1/D2/D3 (spike 1002786)
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
C3 says a builder browsing the orbs page applies to a project **through the existing join path**, and C4 says the application **carries the applicant's cross-project record** so the receiving project sees a track record, not a name — with the owner able to approve or decline. The goal's own hard constraint (ADR 0016 / 0141) is that those numbers are descriptive analytics, never authority.
|
|
11
|
+
|
|
12
|
+
Every piece already exists, and none of them are joined up:
|
|
13
|
+
|
|
14
|
+
- **The relay** ([`modules/platform-identity/routes/my-projects.js`](../../modules/platform-identity/routes/my-projects.js)) — `POST /my-projects/join { origin, note }` resolves the origin against the hub's own `sso_clients` registry (the SSRF guard: never the caller's raw string), then makes a **plain, unauthenticated** `POST ${client.origin}/api/gds/access-requests` carrying `{ github_login, display_name, note }`. `joinRelayOutcome` translates the answer into one of five hub-authored sentences. The applicant's note already rides it (task 1002330); the privacy spec's claim that the note is hardcoded is stale.
|
|
15
|
+
- **The row** (`migrations/060_access_requests.sql`, untouched since) — `pending → invited | dismissed`, terminal rows kept as the audit trail, one open row per login (a partial unique index on `lower(github_login)`).
|
|
16
|
+
- **The gate** — `hasInvitedAccessRequest` in [`src/bongos/db-kernel.js`](../../src/bongos/db-kernel.js) reads `status='invited'` directly, and `firstTimeAdmissionBlocked` is the single decision all four sign-in paths call. `invited` is not a queue label; it *is* the admission verdict.
|
|
17
|
+
- **The record** — `getPublicProfileExtras(githubId)` returns `null` unless `profile_state='active' AND NOT hide_stats`, and `GET /profiles/:handle` already serves it publicly, CORS-enabled (`corsPublicGet`), `Cache-Control: no-store` (ADR 0171 D5), rate-limited per IP.
|
|
18
|
+
|
|
19
|
+
Two facts found in this spike shape everything below.
|
|
20
|
+
|
|
21
|
+
**The relayed request is indistinguishable from a stranger's.** The target's `POST /access-requests` is the one public unauthenticated write on the GDS, and the relay presents nothing the applicant could not have typed into the project's own landing page. Attaching a cross-project record to a `github_login` that nobody vouched for turns the queue into an impersonation surface: anyone can file a request naming a well-known builder, the owner reads that builder's excellent record beside it, approves — and the person who actually signs in is whoever holds *that* GitHub account. The record is exactly the thing that makes the unauthenticated write dangerous, so C4 cannot be built without closing it.
|
|
22
|
+
|
|
23
|
+
**`validateOrRespond` is strict by default** ([`src/bongos/routes/_helpers.js`](../../src/bongos/routes/_helpers.js)) — an unknown body field is rejected `validation_failed` 400. The public POST validates strictly (`modules/onboarding/routes/access-requests.js`). A 400 is not one of `joinRelayOutcome`'s five cases, so it returns `null` and the hub answers "the project could not be reached" (502). Adding a body field to the relay would therefore break applications to **every project pinned to a core older than the change** — which, since projects pin their own core, is most of them, indefinitely.
|
|
24
|
+
|
|
25
|
+
## Decision
|
|
26
|
+
|
|
27
|
+
### D1 — The application is an `access_requests` row on the target project. No cross-project applications table, on either side.
|
|
28
|
+
|
|
29
|
+
1. **Extend `access_requests`; do not add a table.** `status='invited'` is what `hasInvitedAccessRequest` reads, so a parallel record would need its own branch wired into `firstTimeAdmissionBlocked` — a second place where admission is decided, which is precisely the drift that decision exists as one function to prevent. It would also need its own queue, its own audit trail and its own reconciliation with the row the same person may already hold. The console wave (spike 1002786 D2) reached the same conclusion for invites; two waves inventing two tables for one door is the outcome to avoid.
|
|
30
|
+
|
|
31
|
+
2. **The migration adds exactly what a vouch needs: `applicant_github_id text` and `vouched_at timestamptz`, both nullable.** `text`, not `bigint`, because core `builders.github_id` is `text` (`migrations/003_pms.sql`) and the hub's numeric cast is the module's own concern (ADR 0171 D2 already handles non-numeric principals there). `applicant_github_id IS NOT NULL` **is** the vouch mark — no `source` enum, because a second column would let the two disagree. The console wave's `kind` / `created_by` columns (spike 1002786 R2) are a **separate** additive migration; the two waves must not both add `kind`, and either order works.
|
|
32
|
+
|
|
33
|
+
3. **Nothing about the application is stored on the hub except when it was filed.** A hub-held row saying a project has a pending application would be a hub-held admission fact — authority on the wrong side of the boundary, and stale the moment the owner acts. So the hub keeps an **echo** for one purpose only: so the applicant's own surfaces (My Projects, the orbs card) can say *"you applied on <date>"* without asking every project on every render. The echo carries `(github_id, client_id, applied_at)` and **never a status** — it is not allowed to claim what the project decided. An approval becomes visible the way membership always becomes visible: the check-in (ADR 0141 §3) creates the membership row and the card flips to *member*. The echo ages out of the applicant's view after **30 days**, the same read-time constant the privacy spec D4 chose, so a declined application does not read as "applied" forever.
|
|
34
|
+
|
|
35
|
+
4. **Re-applying is not an error.** The partial unique index already makes one open row per login the invariant; the endpoint answers `requested` for an application that is already open rather than a 409, so a double-clicked Apply button never tells the applicant they failed. A dismissal does **not** bar the door — resolved rows do not occupy the index, and a project that wants a permanent bar has `invite_only` (ADR 0194).
|
|
36
|
+
|
|
37
|
+
### D2 — The hub vouches for the applicant's identity in a **header**; the record itself is resolved **live**, in the reviewer's browser, and is never snapshotted.
|
|
38
|
+
|
|
39
|
+
1. **The relay carries a hub-signed, audience-scoped assertion in `<redacted>`.** It reuses `mintHubAssertion` ([`modules/platform-identity/hub-keys.js`](../../modules/platform-identity/hub-keys.js)) with a `purpose: 'application'` marker — the discriminator `mintHubLogoutToken` already established, so a captured device-login assertion cannot be replayed as an application vouch or the reverse. The target verifies it **offline** with the existing `verifyHubAssertion` ([`src/bongos/auth-idp.js`](../../src/bongos/auth-idp.js)) against the `<PREFIX>_HUB_PUBLIC_KEY` every federated instance is already provisioned with (ADR 0141 §4): signature, issuer, `aud === this instance's client_id`, unexpired. No new secret, no new key, no instance→hub call at apply time.
|
|
40
|
+
|
|
41
|
+
2. **A header, not a body field — and that is the whole compatibility story.** Per Context, a body field would 400 on every older pin and the hub would report the project unreachable. A header is invisible to the strict validator, so a project on any pin keeps taking applications unchanged, and a project that upgrades starts vouching with no coordination at all. (The CLI's `POST /auth/device/exchange` carries its assertion in the body; it could, because that route was new. This one is not.)
|
|
42
|
+
|
|
43
|
+
3. **An absent or invalid assertion writes the row anyway — unvouched.** It is never a refusal. Refusing would lock out applicants from an older hub, and would hand an attacker a way to make applications fail by sending garbage. Unvouched is simply the row the project has always had: a name and a note, with no record beside it.
|
|
44
|
+
|
|
45
|
+
4. **The record is never copied. The row stores an identity; the record is fetched at render.** C2 requires the hide to be honored *live everywhere, including pending applications* — the goal's own text says hiding after applying re-hides it on the owner's view. A stored snapshot is a copy the builder can no longer withdraw: honoring a later hide would mean a purge sweep across every instance that ever received one, and any instance missed is a permanent leak. Privacy spec D7 already decided "live at review time, never a stored snapshot"; this ADR ratifies it and adds the vouch (D2.1) that makes the live render *safe* rather than an impersonation surface.
|
|
46
|
+
|
|
47
|
+
5. **The fetch is made by the reviewer's browser against the hub's already-public `GET /profiles/:handle`, not by the instance's server.** It is CORS-enabled, `no-store`, and `getPublicProfileExtras` applies `profile_state='active' AND NOT hide_stats` inside the module — so C2 is honored by construction, at the instant of render, with no cache to outlive a toggle. Doing it server-side instead would extend ADR 0141's always-on hub dependency from sign-in onto the console's own request path, and would need the client-authenticated read plus timeout/retry/cache that ADR 0171 D1 rejected for exactly this reason and named as a deliberate separate task. It would also meter the per-IP read limit against the *instance* rather than each reviewer. A hub outage degrades to "the platform record isn't available right now" beside a queue that still works — the degrade-don't-500 rule.
|
|
48
|
+
|
|
49
|
+
6. **The vouch carries the `handle` as well as the `github_id`, and a handle-less account shows no record.** `GET /profiles/:handle` is addressed by handle; `github_id` is the stable join key but has no public read. The handle at apply time is stored on the row, and the previous-handle 301 window (ADR 0171 D8) covers a rename. An account with no handle yet, or a `provisional` one (404 by design, ADR 0171 D3), renders as an application with a note and no record — which is honest, not broken.
|
|
50
|
+
|
|
51
|
+
7. **A private account's application carries name, handle and note, and nothing else — in V1.** The D3 recruiter sliver is gated on `project.recruit`, an *instance* permission the hub cannot check from a cross-origin browser read, and inventing a fourth authorization plane to carry it is not this goal's job. The apply UI states exactly what the application shares (privacy spec D7's rule). The sliver half is already sequenced behind the recruiter port — task 1002972 depends on task 1002968 — and lands there, not here.
|
|
52
|
+
|
|
53
|
+
### D3 — "Project owner" is the project's own archon. There is no cross-project owner authority, and none is created.
|
|
54
|
+
|
|
55
|
+
1. **The registrant is whoever the instance seated as its first Archon** (`maybeSeatFirstAdmin`) — for a platform-provisioned project, the founder who filled the create wizard. **Delegates are whoever that Archon grants on that instance**, through the governance UI that already exists: promote to archon, lower a permission's floor, or a custom role. Because the instance *is* the project (ADR 0108; console spec D1), per-project scoping dissolves — the instance is the scope, so the governance grant needs no resource column and the per-project-scoped-permission problem never arises.
|
|
56
|
+
|
|
57
|
+
2. **The check is the atom that already guards this act: `access_request.review`, floor archon** (`modules/government/catalog.js`), which pins `GET /access-requests`, `PATCH /access-requests/:id` and `POST /access-requests/invite`. **This goal mints no new permission.** Widening the queue's *read* half to metics is the console wave's `project.recruit` (spike 1002786 R3); until it lands, the owner view ships on `access_request.review` alone and moves over when it does. Adding a third atom for the same door would be the drift.
|
|
58
|
+
|
|
59
|
+
3. **`platform_identity_projects.owner_github_id` stays advisory attribution and is never read as a gate** — its own comment says so, and `project-stats.js` already declines to read it, taking the project's own archon off its own leaderboard instead (ADR 0170). A hub-held owner list consulted before an admission would be the hub deciding who may admit on someone else's box: the exact inversion ADR 0141 exists to prevent. The hub's role here is unchanged from §3 — relay, echo, and a link to the project's own console.
|
|
60
|
+
|
|
61
|
+
4. **So the owner's pending-applications view is a page on the project's own instance**, and the hub links to it. It absorbs the Watch page's access-requests section on the same terms the console spec set (one queue, one home).
|
|
62
|
+
|
|
63
|
+
### D4 — The lifecycle keeps three states; approval is a door, not a membership.
|
|
64
|
+
|
|
65
|
+
1. **No new status values.** `invited` means approved — the door opens and **the acceptance act is signing in** (console spec D2), whose authoritative record is the `builders` row plus the membership check-in. An `accepted` state would be a second place membership is asserted, free to drift from the one that actually admits. `dismissed` means declined. Both are terminal and the row stays as the audit trail.
|
|
66
|
+
|
|
67
|
+
2. **Rescind is `invited → dismissed`, and the PATCH must learn to accept it.** Today it resolves only from `pending`. An approval given by mistake, before the applicant has ever signed in, must be revocable: `hasInvitedAccessRequest` stops matching and the door re-locks. After they have signed in the row is history and offboarding is the instance's own act (`/builder-exit`) — this route must never pretend to remove a member. The reverse correction (`dismissed → invited`) needs no new path: `POST /access-requests/invite` already resolves or inserts an `invited` row for any login.
|
|
68
|
+
|
|
69
|
+
3. **Approval expires read-time at 30 days, whatever created it.** The privacy spec D4 set that constant for recruit invites; an `invited` row produced by approving an application is the same open admission door, and one rule for `status='invited'` is the only version of this that cannot rot into two. An **application** carries no expiry — a stale `pending` row admits nobody, and the owner dismissing it is the whole mechanism.
|
|
70
|
+
|
|
71
|
+
4. **Approve and decline are idempotent on the value they already hold** (a PATCH to the current terminal state returns the row, not a 409), so a double-clicked button in the queue is safe. Every refusal names the state that blocked it and the action available there, in the ADR 0116 envelope.
|
|
72
|
+
|
|
73
|
+
## What this unblocks
|
|
74
|
+
|
|
75
|
+
| Task | Depends on |
|
|
76
|
+
|---|---|
|
|
77
|
+
| [task 1002284](https://cloudbongos.com/builders#/task/1002284) — R08, the application API (apply / list-pending / approve / decline) | D1 (the migration + the row), D4 (the statuses, the rescind gap, idempotency), D3.2 (the atom it pins) |
|
|
78
|
+
| [task 1002285](https://cloudbongos.com/builders#/task/1002285) — R09, the application carries the cross-project record (+ note) | D2 wholesale — the assertion, the header, the live browser read, the handle |
|
|
79
|
+
| [task 1002286](https://cloudbongos.com/builders#/task/1002286) — R10, "Apply" on the in-core browse surface | D1.3 (the echo is where *applied* comes from), D1.4 (a re-apply is not an error) |
|
|
80
|
+
| [task 1002287](https://cloudbongos.com/builders#/task/1002287) — R11, the owner pending-applications view | D3 (who may see it, and that the page lives on the project), D2.5 (the record is fetched from the page) |
|
|
81
|
+
| [task 1002288](https://cloudbongos.com/builders#/task/1002288) — R12, "Apply" on the orbs front door | D1.3, D1.4 — the same state and the same idempotency, from the separate static site |
|
|
82
|
+
| [task 1002289](https://cloudbongos.com/builders#/task/1002289) — R13, PROOF: apply → owner sees record → approves → applicant joins | all four; the proof must also pin the unvouched case (no record rendered) and a hide toggled between two renders |
|
|
83
|
+
| [task 1002972](https://cloudbongos.com/builders#/task/1002972) — applications carry the applicant profile: note through the relay, live render in the queue | D2.4 (live, never a snapshot) and D2.7 (the private-account sliver is its half, behind [task 1002968](https://cloudbongos.com/builders#/task/1002968)) |
|
|
84
|
+
|
|
85
|
+
Two things R08 should not rediscover: the relay's applicant note already exists (task 1002330), and `PATCH /access-requests/:id` resolving only from `pending` is a real gap D4.2 asks it to close.
|
|
86
|
+
|
|
87
|
+
## Consequences
|
|
88
|
+
|
|
89
|
+
- A project running any core pin keeps taking applications exactly as it does today; upgrading is what turns the vouch on. A vanilla or standalone instance holds no hub public key, verifies nothing, and degrades to the local queue it already has — which is the goal spec's stated degrade for non-federated instances.
|
|
90
|
+
- The queue gains a two-class row: vouched (a platform account, a record beside it) and unvouched (a name and a note). The owner must be able to see which is which — an unvouched row that *looked* vouched is the whole vulnerability, so the distinction is UI-load-bearing, not decoration.
|
|
91
|
+
- `GET /profiles/:handle` picks up a second class of caller. Its per-IP limit meters each reviewer separately (D2.5), and its `no-store` is now doing double duty: it is what makes a hide land on the owner's next queue render.
|
|
92
|
+
- The hub can say *when* someone applied and never *what came of it*. A card that wants to say "declined" is asking for a fact the hub is not allowed to hold; the honest reading is membership appearing, or not.
|
|
93
|
+
- The echo is the one new hub table this goal adds, and it is deliberately statusless so it can never be mistaken for the application.
|
|
94
|
+
|
|
95
|
+
## Rejected
|
|
96
|
+
|
|
97
|
+
- **A cross-project `applications` table on the hub.** It would hold an admission fact about someone else's project, need its own path into `firstTimeAdmissionBlocked` to mean anything, and be stale from the moment the owner acted. C3 asks for the existing join path precisely to avoid this.
|
|
98
|
+
- **A separate `project_applications` table on the instance**, beside `access_requests`. Two queues for one door, two audit trails, and the same person able to hold a row in each.
|
|
99
|
+
- **A profile snapshot stored on the application row.** Fails C2's live hide, and honoring a later hide would mean sweeping every instance that ever received a copy.
|
|
100
|
+
- **Adding the assertion to the relay body.** 400s on every older pin under strict validation, which `joinRelayOutcome` reads as unreachable — breaking applications to most projects, for good.
|
|
101
|
+
- **Refusing an application whose assertion is missing or invalid.** Locks out an older hub and hands an attacker a denial vector; unvouched is the row the project has always had.
|
|
102
|
+
- **An instance→hub server-side pull for the record.** Extends the always-on hub dependency onto the console's request path and needs the client-authenticated read with timeout/retry/cache that ADR 0171 D1 rejected and named as its own task.
|
|
103
|
+
- **Reading `owner_github_id` as an authorization.** The hub deciding who may admit on someone else's instance — the inversion ADR 0141 exists to prevent — and the column's own comment already says it is advisory.
|
|
104
|
+
- **A new "project owner" permission atom.** `access_request.review` already guards this exact act at the right floor; widening its read half is the console wave's `project.recruit`, and a third atom for one door is drift.
|
|
105
|
+
- **An `accepted` status.** Membership is asserted by the sign-in and the check-in; a second assertion is free to disagree with the one that admits.
|
|
106
|
+
|
|
107
|
+
## Deferred (named, not dropped)
|
|
108
|
+
|
|
109
|
+
- **The private-account sliver in the queue** — needs `project.recruit` gating the hub can see; task 1002972 behind task 1002968.
|
|
110
|
+
- **The console page absorbing the Watch section, and the `kind` / `created_by` columns** — spike 1002786's wave, whose migration must not collide with D1.2.
|
|
111
|
+
- **A hub-side aggregate of every project's queue** ("all my projects' applications in one view") — needs the push-model federation `project-stats.js` describes; the console spec already records it as the eventual shape.
|
|
112
|
+
- **Guild applications (C8)** — spike R23; a guild applying is a different applicant class and this ADR decides nothing for it.
|
package/docs/adr/README.md
CHANGED
|
@@ -288,6 +288,7 @@ This keeps the decision history honest and traceable.
|
|
|
288
288
|
| 0199 | [**An unreachable ACTIVE project gets a restart of its own, not a re-provision — and the restart never claims the verdict its probe cannot reach** ([task 1003238](https://cloudbongos.com/builders#/task/1003238), goal 1000058). The liveness overlay (task 1002696) could report a project unreachable while its status stayed `active`, and that reading came with no action: `PROVISION_ENQUEUE_FROM` deliberately excludes `active` (task 1002691 — a blind re-enqueue left a stale provision intent blocking the owner’s own teardown), so the card’s existing *Try again* / *Bring it back* doors 409 on an active project by construction, and `settings-apply` — the only leg that runs from `active` — refuses unless a stored value actually changed, which is exactly the case a stuck project does not have. Decisions: (1) a `restart` intent (migration `provisioning_011`, `INTENT_ACTIONS` in lockstep) behind `RESTART_FROM = {active}` + `RESTART_SHAPES = {co-tenant, standalone}`; the web tier still only ENQUEUES (ADR 0111 §2). (2) `restartInstance` is `settingsApplyInstance` MINUS the env patch — one `systemctl restart`, then the same patient healthz budget; it reads and writes no file, so ADR 0190’s in-place-patch hazard cannot arise. (3) The `probeIsVerdict` narrowness carries over and is load-bearing: the liveness sweep probes the PUBLIC `https://<domain>/healthz` while the runner verifies LOOPBACK, so a project can answer here while DNS/Caddy/TLS is what is broken — no up-verdict is written for a domained instance, and the event says “answering on the box; whether its public address answers is for the next sweep to say” rather than a sentence the still-`unreachable` card would contradict. (4) A project that stays down is `ok:true` — the restart RAN; re-pending it to the attempt cap would restart a broken project four more times and end by stamping an `error_note` over the owner’s own action. Only a THROW returns `ok:false`, and it returns rather than throws because the drain loop’s catch would relabel a merely-unreachable instance `error` (ADR 0190 §2). (5) `enqueueRestart` is idempotent against its own kind in EITHER open state — where it differs from the settings push, which must refuse to ride a RUNNING sibling that may have read the row already; a restart carries no payload, so one already running IS the ask. (6) A pending restart joins the teardown’s supersede list, more sharply than its siblings since the owner reaches for Restart exactly when they might next give up and delete. (7) The door shows only at `active && last_verified_up === false && shape !== dedicated && !open_intent`, mirrored on card and manage page; `null` (never probed) is NOT `false` and raises no door. (8) Every refusal names the action that IS available there, the state gate is checked before the shape gate, and no branch promises a duration (F16). The `INTENT_ACTIONS`↔migration set-equality assertion moved to the NEWEST migration’s test — pinned to 010 it would fail for every future action. Rejected: widening `PROVISION_ENQUEUE_FROM`; re-sending unchanged settings to borrow their restart; reporting a loopback pass as “answering again”; retrying a restart that left the project down; a Restart door on every active card.](<redacted>.md) | provisioning / project lifecycle |
|
|
289
289
|
| 0198 | [**Third-party skill vendoring: a scan verdict the session cannot override, a licence allowlist, a provenance sidecar, one preamble block, and no auto-update** ([task 1003321](https://cloudbongos.com/builders#/task/1003321), goal 1000080 — the second of the five ui-design tasks). Twelve design skills installed on one builder's machine were to become the `ui-design` module's vendored set. Both origins reduced to DANGEROUS under `/scan-before-install` at the default tier: `Leonxlnx/taste-skill` on one floor critical (an injection-phrase hit the panel identifies as a filename placeholder) with a caution panel; `pbakaus/impeccable` on three floor criticals and a panel that upheld shipped PostToolUse/Stop hooks written into five harness configs, a child agent spawned with permissions bypassed from a loopback HTTP route, a directive telling the model to disregard its harness system prompt, a page-embedded live-server token and an unverified remote bundle download. DECISION: the scan verdict is the gate and a session cannot override it — `dangerous` lands a rebuild spec (a GDS task), never a copy, and vendoring one anyway is an owner ADR plus a test amendment; a licence allowlist (Apache-2.0, MIT, BSD-2/3, CC-BY-4.0); a `PROVENANCE.md` sidecar with the full pinned sha; ONE preamble block (`skills/PREAMBLE.md`, byte-identical, fenced by two markers so an upstream refresh is a three-way merge) that reads the instance's world before any taste rule; `skills/vendor/<name>/` as the home because `fitness-ratchets.js` already exempts `vendor/` from the first-party budgets; the manifest stays the single declaration and `claude-materialize.js` step 1b lands a declared module skill — sidecar included — in every instance's `.claude/skills/` and withdraws it with the module; no auto-update; the playwright plugin a documented prerequisite. The roster ships EMPTY and that is the policy working: the machinery is proven by fixture (`tests/ui_design_skills.mjs`), the twelve are named in `/design` with their verdicts, two rebuild-spec tasks hold the documented functionality. Rejected: treating the placeholder hit as a false positive; scanning a fork or subtree; vendoring impeccable with the hooks stripped; `.claude/skills/` as the home; a second roster file.](<redacted>.md) | modules / design system / security |
|
|
290
290
|
| 0200 | [**A mistyped bring-your-own domain can be corrected — the address it leaves is released to a blackhole, and the attach leg stops regenerating web.env from the row** ([task 1003237](https://cloudbongos.com/builders#/task/1003237), goal 1000058). `POST /provisioning/instances/:id/domain` refused a second address with a deliberate `409 domain_already_set` ("A genuine BYO address stays un-re-pointable, as before") while direction §8 said "attach/change stays for custom domains" — so a founder who mistyped one character in the create wizard was permanently addressed at a hostname they did not own, with no path back but teardown. Decisions: (1) a BYO address re-points under the guards the FIRST attach already applied — `isReservedHostname`, the task-1002704 in-apex squat rule, `getInstanceByDomain` uniqueness, `DOMAIN_ATTACH_FROM` (which already admitted `active`) — none added, none weakened; the platform-assigned `<slug>.<apex>` is un-re-pointable because the squat rule says so. (2) The address it leaves is RELEASED: `provisioning_012` adds `released_domains text[]`, written in the SAME statement as the new domain (a crash between the two would leave a hostname aimed at platform infra with nothing in the row to say so), an ARRAY because a founder can mistype twice before the runner drains. Only a BYO address is queued — the assigned `<slug>.<apex>` is the platform’s own name and the revive path re-uses it (ADR 0181 §2). (3) Release is a REPOINT to `192.0.2.1` (RFC 5737 TEST-NET-1), not a delete — the box lane’s idea-309 shape: a delete opens an NXDOMAIN window resolvers negative-cache for the zone’s SOA neg-TTL (~30 min, not editable), and an UPSERT needs only the contract every operator `BOX_DNS_HOOK` already implements. The Caddy vhost and the tls-check allow-list release THEMSELVES (the snippet is per-slug and carries the current domain; tls-check answers from the row). (4) A failed release is `ok:true` + `releaseFailed`, and KEEPS the queue so the next attach retries — a release that failed must never fail a re-point that already landed. (5) `domainAttachInstance` now patches web.env IN PLACE via `upsertEnvVars`/`originEnvVarsFor`, closing the ADR 0190 §2 hazard (idea 1000842) on this path: the template re-write regenerated the file FROM THE ROW, blanking the OAuth creds `placeManifestCreds` placed, the App private key and the federation block — masked for a FIRST attach by wizard order, and NOT masked for a re-point, which runs on a project the founder is already signed in to. Dry-run plans; an unreadable/empty file is refused, never patched. (6) A CHANGED domain clears `last_verified_{at,up}` — the overlay is a verdict on an ADDRESS (task 1002696), so carrying it would let the card claim the new name "last answered 2 minutes ago" when only the old one was ever probed (F16). (7) The card gains a third door — *Use a different domain*, pre-filled so a typo is edited not retyped — and says what it costs where the button is. Rejected: amending direction §8 to call the address fixed; deleting the old record; sequencing behind task 1002897; a `previous_domain` scalar (drops the middle address).](<redacted>.md) | provisioning / project lifecycle |
|
|
291
|
+
| 0201 | [**An application is the project's own `access_requests` row, vouched for by a hub-signed assertion carried in a HEADER; the record it carries is resolved live and never stored** ([task 1002283](https://cloudbongos.com/builders#/task/1002283), goal 1000045 — public builder profiles, C3/C4). The join relay (`POST /my-projects/join`) already posts `{github_login, display_name, note}` to the target's PUBLIC, unauthenticated `POST /access-requests` — presenting nothing the applicant could not have typed into the project's own landing page. Attaching a cross-project record to a login nobody vouched for turns the owner's queue into an impersonation surface: file a request naming a well-known builder, the owner reads that builder's track record beside it and approves, and whoever holds that GitHub account signs in. So the record is the thing that makes the unauthenticated write dangerous, and C4 cannot be built without closing it. Decisions: **D1** the application stays an `access_requests` row on the target — `status='invited'` IS what `hasInvitedAccessRequest` reads, so a parallel record would need a second branch in `firstTimeAdmissionBlocked`, the one decision that exists as one function to prevent drift; the migration adds only `applicant_github_id text` (core's own type) + `vouched_at`, the non-null id IS the vouch mark, and the console wave's `kind`/`created_by` stay a separate additive migration; the hub keeps a STATUSLESS echo `(github_id, client_id, applied_at)` so the applicant's own cards can say "you applied on <date>" without polling every project — it may never claim what the project decided (an approval becomes visible the way membership always does, via the check-in), and it ages out at 30 days; re-applying answers `requested`, not 409. **D2** the hub signs an audience-scoped `purpose: 'application'` assertion with the existing `mintHubAssertion`, verified OFFLINE by the existing `verifyHubAssertion` against the already-provisioned `<PREFIX>_HUB_PUBLIC_KEY` — no new secret, no apply-time hub call — and it travels in `<redacted>` because `validateOrRespond` is STRICT by default: a body field would 400 on every older pin, and `joinRelayOutcome` reads a 400 as "could not be reached", breaking applications to most projects for good (the CLI's device exchange carries its assertion in the body because that route was new; this one is not). A missing or invalid assertion writes the row UNVOUCHED, never refuses — refusing locks out an older hub and hands an attacker a denial vector. The record itself is never copied: the row stores an identity (+ the handle, since `GET /profiles/:handle` is the addressed read), and the REVIEWER'S BROWSER fetches the hub's already-public, already-CORS'd, already-`no-store` profile at render, where `profile_state='active' AND NOT hide_stats` is applied inside the module — so C2's live hide is honoured by construction with no cache to outlive it, and a hub outage degrades to "record unavailable" beside a queue that still works. An instance→hub server pull was rejected on ADR 0171 D1's own grounds. A private account's application carries name + handle + note in V1; the D3 sliver needs `project.recruit`, which the hub cannot check cross-origin. **D3** "project owner" is the project's OWN archon — the registrant is whoever `maybeSeatFirstAdmin` seated, delegates are whoever that archon grants on that instance, and because the instance IS the project the per-project-scoped-permission problem dissolves; the check is the atom that already guards the act, `access_request.review` (floor archon), and this goal mints NO new permission (widening the read half is the console wave's `project.recruit`). `platform_identity_projects.owner_github_id` stays advisory and is never a gate — a hub-held owner list consulted before an admission is the inversion ADR 0141 exists to prevent. **D4** three statuses, unchanged: `invited` IS the approval and the acceptance act is signing in (an `accepted` state would be a second place membership is asserted, free to drift from the one that admits); rescind is `invited → dismissed`, which the PATCH must learn to accept (it resolves only from `pending` today) and which must never pretend to remove a member who already signed in; an `invited` row expires read-time at 30 days whatever created it, while an application never expires because a stale `pending` admits nobody; approve/decline are idempotent on the value they hold. Rejected: a cross-project `applications` table on either side; a stored profile snapshot; the assertion in the body; refusing an unvouched application; the server-side pull; reading `owner_github_id` as authorization; a new "project owner" atom; an `accepted` status.](<redacted>.md) | platform identity / applications |
|
|
291
292
|
| 0202 | [**A connection is one directed row on the platform account, and every degree label is computed over the graph the viewer is allowed to see** ([task 1002294](https://cloudbongos.com/builders#/task/1002294), goal 1000045 criterion C6 — the design spike for the connections wave). Criterion C6 asks for LinkedIn-shaped connections; the cross-project data plane it must sit on is account-global (ADR 0141) and governed by an owner-signed privacy matrix (`docs/specs/<redacted>.md` D1/D2), where ADR 0171 D4 already bans "a count plus a hidden total" as a leak by subtraction. Decisions: (1) **one directed request row** on a new `platform_identity_connections` — `requester`/`addressee` kept because the inbox, cancel and cooldown all need to know who asked, with `pair_lo`/`pair_hi` GENERATED from LEAST/GREATEST and a partial unique index over `pending|accepted` so a duplicate AND a simultaneous reciprocal request are database errors, not route bugs; module-owned and `platform_identity_`-namespaced (ADR 0083 §5). (2) The `goal_membership_requests` lifecycle (`pending → accepted|rejected|cancelled`, atomic resolve-and-act) with three deliberate divergences: a decline is **silent**, a re-ask inside its 90-day cooldown is **swallowed and answered as if it worked** (ADR 0194 §3 applied to a person — a 409 saying "declined" would disclose what the silence protects), and pending requests expire at **30 days read-time** (privacy spec D4), with expiry resolved in the same statement that inserts, since `now()` cannot live in an index predicate. Remove **hard-deletes** — a "A removed B" tombstone is data neither party consented to keep and no read path uses. (3) Degree = a **recursive CTE seeded at the viewer**, `DEGREE_MAX = 3` with `FANOUT_1`/`FANOUT_2` caps and a deterministic frontier order; past the caps the label is `null` ("out of network"), never a number the query did not establish. Envelope stated honestly: single-digit ms at hub scale, caps bite around 10⁴ accounts, and they convert a hub-heavy account's millions-wide 3-hop into a *missing label* on a page that still renders. (4) Connections get **no predicate of their own** — they consume task 1002967's single effective-visibility reader, published as JS and as `accountVisibleSql(alias)` (the ADR 0182 form); `profile_state='active'` floors the feature, a private account answers **404 not 403** and never appears in a public list, `hide_stats` suppresses the numbers beside an edge but not the edge, degree is an **own-scope read** so viewer-relative labels coexist with viewer-independent public responses — and every count is the length of the array the viewer may see. (5) "Builders you may know" from mutuals + shared projects + recency, one explainable linear score, a named project only where its catalogue row is publicly listed; shared goals refused (instance-local), guilds a reserved empty slot, and **Discord refused outright** (outside the trust boundary; the hub stores no Discord identity), as are the GitHub follower graph and address-book import. Rejected: two mirrored rows on accept; a symmetric edge with no requester; a core table beside `builders`; a materialised closure/adjacency table (privacy-filtered at write time = a cached visibility decision, which ADR 0171 D2 forbids); an in-process graph cache; a graph database. Deferred and named: a connections-specific hide toggle, blocking, what an accepted connection unlocks on a private account, and notifications beyond the existing attention card. Exposes to the guild/orb wave: `neighbours`, `degreeBetween`, `mutualCount` — **not** a stored strength scalar.](<redacted>.md) | platform identity / privacy / social graph |
|
|
292
293
|
|
|
293
294
|
> ⚠️ **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
|
@@ -19106,6 +19106,9 @@
|
|
|
19106
19106
|
},
|
|
19107
19107
|
"type": {
|
|
19108
19108
|
"type": "string"
|
|
19109
|
+
},
|
|
19110
|
+
"template": {
|
|
19111
|
+
"type": "string"
|
|
19109
19112
|
}
|
|
19110
19113
|
},
|
|
19111
19114
|
"required": [
|
package/docs/api-reference.md
CHANGED
|
@@ -415,7 +415,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
415
415
|
| Method | Path | Rank | Body | Description |
|
|
416
416
|
|---|---|---|---|---|
|
|
417
417
|
| GET | `/api/bongos/projects` | `metic+archon` | — | the full curation list (includes unfeatured + owner attribution). |
|
|
418
|
-
| POST | `/api/bongos/projects` | `metic+archon` | `origin`, `name`, `tagline`, `description`, `art_url`, `type` | create / update a catalog entry (manual seed for hand-wired instances; federated projects auto-catalogue at registration). |
|
|
418
|
+
| POST | `/api/bongos/projects` | `metic+archon` | `origin`, `name`, `tagline`, `description`, `art_url`, `type`, `template` | create / update a catalog entry (manual seed for hand-wired instances; federated projects auto-catalogue at registration). |
|
|
419
419
|
| PATCH | `/api/bongos/projects/:id/featured` | `metic+archon` | `featured` | the feature / unfeature control (the ADR 0141 §5 curation gate). |
|
|
420
420
|
| GET | `/api/bongos/projects/featured` | `public` | — | a curated public showcase; no auth by design. |
|
|
421
421
|
| POST | `/api/bongos/projects/invite` | `metic+archon` | `client_id`, `github_login` | invite a builder into a project (ADR 0141 §5, task #1002115). |
|