@bongos/core 1.19.538 → 1.19.540
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 +44 -29
- package/docs/adr/0249-the-invite-step-is-the-done-panel-s-first-act-not-a-rail-step.md +115 -0
- package/docs/adr/README.md +2 -0
- package/docs/copy-inventory.md +168 -158
- package/docs/copy-registry.json +304 -202
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/hall-kit.js +105 -6
- package/modules/hall-ui/public/profile-charts.js +39 -4
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/profile.js +8 -1
- package/modules/hall-ui/public/style.css +31 -0
- package/modules/public-landing/public/assets/cosmos.css +18 -0
- package/modules/public-landing/public/projects.html +220 -1
- package/modules/public-landing/public/projects.states.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/routes/me.js +42 -0
- package/src/module-api.js +1 -1
- package/tests/hall_audit.mjs +4 -1
- package/tests/hall_kit_charts.mjs +75 -0
- package/tests/hall_profile_world.mjs +4 -1
- package/tests/me_ship_days_bound.mjs +76 -0
- package/tests/project_invite_ui.mjs +502 -0
- package/tests/projects_hub.mjs +1 -1
- package/tests/projects_hub_pre_uat.mjs +2 -2
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.540",
|
|
6
|
+
"core_contract": "1.19.540",
|
|
7
|
+
"source_commit": "5fc647cb0f3cec892a9ecf58528bbd0158250e44",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-04T16:31:49.656Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 441,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1999,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2464,
|
|
20
|
+
"tree_sha256": "36d8ec4b4166ff3dedaed13accf062736759569e3ef0dbaf8655662c4b896a9b",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1754,10 +1754,15 @@
|
|
|
1754
1754
|
"mode": "0000644",
|
|
1755
1755
|
"sha256": "cbcac548fc3b683fd7219e312ab1542768c973aca5112d8cbc9ad0b2d09da650"
|
|
1756
1756
|
},
|
|
1757
|
+
{
|
|
1758
|
+
"path": "docs/adr/0249-the-invite-step-is-the-done-panel-s-first-act-not-a-rail-step.md",
|
|
1759
|
+
"mode": "0000644",
|
|
1760
|
+
"sha256": "38fa5f4ce3a91eaf3302527a4228ddb1951ea128e83b2ab8e0ad5def658e0a6d"
|
|
1761
|
+
},
|
|
1757
1762
|
{
|
|
1758
1763
|
"path": "docs/adr/README.md",
|
|
1759
1764
|
"mode": "0000644",
|
|
1760
|
-
"sha256": "
|
|
1765
|
+
"sha256": "464fa796eeaa7d3a2d6f6d7a2c36d01fc9dfbb613f0824f00b9710bf568c0cce"
|
|
1761
1766
|
},
|
|
1762
1767
|
{
|
|
1763
1768
|
"path": "docs/api-reference.md",
|
|
@@ -1792,12 +1797,12 @@
|
|
|
1792
1797
|
{
|
|
1793
1798
|
"path": "docs/copy-inventory.md",
|
|
1794
1799
|
"mode": "0000644",
|
|
1795
|
-
"sha256": "
|
|
1800
|
+
"sha256": "9b9201deae4f4e747989864f4498c3a0e138c52ae35579113a0568cd8fb9033b"
|
|
1796
1801
|
},
|
|
1797
1802
|
{
|
|
1798
1803
|
"path": "docs/copy-registry.json",
|
|
1799
1804
|
"mode": "0000644",
|
|
1800
|
-
"sha256": "
|
|
1805
|
+
"sha256": "77cd169d027eecf5bb5e729610c292f9622c5a12cecbdbbc517cc34b22e2fa6a"
|
|
1801
1806
|
},
|
|
1802
1807
|
{
|
|
1803
1808
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2642,7 +2647,7 @@
|
|
|
2642
2647
|
{
|
|
2643
2648
|
"path": "docs/module-api-changelog.md",
|
|
2644
2649
|
"mode": "0000644",
|
|
2645
|
-
"sha256": "
|
|
2650
|
+
"sha256": "8e3f784b21310cbe1b35bbbed75b512d9a1818810cf90eb303cc60008ae68253"
|
|
2646
2651
|
},
|
|
2647
2652
|
{
|
|
2648
2653
|
"path": "docs/modules-contract.md",
|
|
@@ -4777,7 +4782,7 @@
|
|
|
4777
4782
|
{
|
|
4778
4783
|
"path": "modules/hall-ui/public/hall-kit.js",
|
|
4779
4784
|
"mode": "0000644",
|
|
4780
|
-
"sha256": "
|
|
4785
|
+
"sha256": "402344a4e8c5c8a9e983c5e341dc2ea8ab40cab4d5512715b62bd7736060ab74"
|
|
4781
4786
|
},
|
|
4782
4787
|
{
|
|
4783
4788
|
"path": "modules/hall-ui/public/hall-render.js",
|
|
@@ -4977,7 +4982,7 @@
|
|
|
4977
4982
|
{
|
|
4978
4983
|
"path": "modules/hall-ui/public/profile-charts.js",
|
|
4979
4984
|
"mode": "0000644",
|
|
4980
|
-
"sha256": "
|
|
4985
|
+
"sha256": "8a630558026a76af240eb4b10ad11086fca7005386015574574b6b754a89322c"
|
|
4981
4986
|
},
|
|
4982
4987
|
{
|
|
4983
4988
|
"path": "modules/hall-ui/public/profile-connections.js",
|
|
@@ -4997,12 +5002,12 @@
|
|
|
4997
5002
|
{
|
|
4998
5003
|
"path": "modules/hall-ui/public/profile.html",
|
|
4999
5004
|
"mode": "0000644",
|
|
5000
|
-
"sha256": "
|
|
5005
|
+
"sha256": "5cc4f5c553d22c6aa9c08b554532314ac164e023ac2b99aa88a413ff2ed85cfa"
|
|
5001
5006
|
},
|
|
5002
5007
|
{
|
|
5003
5008
|
"path": "modules/hall-ui/public/profile.js",
|
|
5004
5009
|
"mode": "0000644",
|
|
5005
|
-
"sha256": "
|
|
5010
|
+
"sha256": "6922889598ea4c179c32cff98891a1f86d02c9eb8c1732c403f3b93ce3daceb3"
|
|
5006
5011
|
},
|
|
5007
5012
|
{
|
|
5008
5013
|
"path": "modules/hall-ui/public/profile.states.json",
|
|
@@ -5147,7 +5152,7 @@
|
|
|
5147
5152
|
{
|
|
5148
5153
|
"path": "modules/hall-ui/public/style.css",
|
|
5149
5154
|
"mode": "0000644",
|
|
5150
|
-
"sha256": "
|
|
5155
|
+
"sha256": "c403fad2fbca7cfec844ae10107c9c5cf8f259cbaf3b695f058de6ef4e757b0d"
|
|
5151
5156
|
},
|
|
5152
5157
|
{
|
|
5153
5158
|
"path": "modules/hall-ui/public/task-detail.js",
|
|
@@ -6397,7 +6402,7 @@
|
|
|
6397
6402
|
{
|
|
6398
6403
|
"path": "modules/public-landing/public/assets/cosmos.css",
|
|
6399
6404
|
"mode": "0000644",
|
|
6400
|
-
"sha256": "
|
|
6405
|
+
"sha256": "0560e91405dabceb22a898ca73ef1830b28ad3e45b0da36ee4ffef770303c1f2"
|
|
6401
6406
|
},
|
|
6402
6407
|
{
|
|
6403
6408
|
"path": "modules/public-landing/public/assets/verro-medallion.png",
|
|
@@ -6657,7 +6662,7 @@
|
|
|
6657
6662
|
{
|
|
6658
6663
|
"path": "modules/public-landing/public/projects.html",
|
|
6659
6664
|
"mode": "0000644",
|
|
6660
|
-
"sha256": "
|
|
6665
|
+
"sha256": "1a7ba9243c257e767520aae6a164bf9730cd7834d6b1c9bc63c8202a2e711599"
|
|
6661
6666
|
},
|
|
6662
6667
|
{
|
|
6663
6668
|
"path": "modules/public-landing/public/projects.probes.json",
|
|
@@ -6667,7 +6672,7 @@
|
|
|
6667
6672
|
{
|
|
6668
6673
|
"path": "modules/public-landing/public/projects.states.json",
|
|
6669
6674
|
"mode": "0000644",
|
|
6670
|
-
"sha256": "
|
|
6675
|
+
"sha256": "a2d3ecc5e893bcaa5632c2d693d5b642c7041cc285e382d572954284ad64e241"
|
|
6671
6676
|
},
|
|
6672
6677
|
{
|
|
6673
6678
|
"path": "modules/public-landing/public/terms.html",
|
|
@@ -7402,12 +7407,12 @@
|
|
|
7402
7407
|
{
|
|
7403
7408
|
"path": "package-lock.json",
|
|
7404
7409
|
"mode": "0000644",
|
|
7405
|
-
"sha256": "
|
|
7410
|
+
"sha256": "2b42ac2bcb878baf1aa06fafc797c6f9b778eb989f46f1e1d53b46cf35b2124c"
|
|
7406
7411
|
},
|
|
7407
7412
|
{
|
|
7408
7413
|
"path": "package.json",
|
|
7409
7414
|
"mode": "0000644",
|
|
7410
|
-
"sha256": "
|
|
7415
|
+
"sha256": "a461dae944c1fc1c49b88250e96d2b6a4eaff4b60a12e96b730f15b8b43bef2d"
|
|
7411
7416
|
},
|
|
7412
7417
|
{
|
|
7413
7418
|
"path": "public-docs/index.html",
|
|
@@ -9017,7 +9022,7 @@
|
|
|
9017
9022
|
{
|
|
9018
9023
|
"path": "src/bongos/routes/me.js",
|
|
9019
9024
|
"mode": "0000644",
|
|
9020
|
-
"sha256": "
|
|
9025
|
+
"sha256": "1b715d2640ba69a68dbaaa4838e6c391aa9c07f13ea827aa90954848602f3b9d"
|
|
9021
9026
|
},
|
|
9022
9027
|
{
|
|
9023
9028
|
"path": "src/bongos/routes/modules.js",
|
|
@@ -9092,7 +9097,7 @@
|
|
|
9092
9097
|
{
|
|
9093
9098
|
"path": "src/module-api.js",
|
|
9094
9099
|
"mode": "0000644",
|
|
9095
|
-
"sha256": "
|
|
9100
|
+
"sha256": "e1b39ba5609d7066607c7a1d90ebbef58237deab27cbe74d1cacef7e55a76a33"
|
|
9096
9101
|
},
|
|
9097
9102
|
{
|
|
9098
9103
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10497,7 +10502,7 @@
|
|
|
10497
10502
|
{
|
|
10498
10503
|
"path": "tests/hall_audit.mjs",
|
|
10499
10504
|
"mode": "0000644",
|
|
10500
|
-
"sha256": "
|
|
10505
|
+
"sha256": "a6e8de5ee046df5726853cae4a78f2f8be01b053b551b906d9fef9201cec1299"
|
|
10501
10506
|
},
|
|
10502
10507
|
{
|
|
10503
10508
|
"path": "tests/hall_board_world.mjs",
|
|
@@ -10522,7 +10527,7 @@
|
|
|
10522
10527
|
{
|
|
10523
10528
|
"path": "tests/hall_kit_charts.mjs",
|
|
10524
10529
|
"mode": "0000644",
|
|
10525
|
-
"sha256": "
|
|
10530
|
+
"sha256": "b3c95d32e4c2402699070c08fa4eaa9f460cd4d2b813f8b15e39d29fc5aa5b4d"
|
|
10526
10531
|
},
|
|
10527
10532
|
{
|
|
10528
10533
|
"path": "tests/hall_nav.mjs",
|
|
@@ -10552,7 +10557,7 @@
|
|
|
10552
10557
|
{
|
|
10553
10558
|
"path": "tests/hall_profile_world.mjs",
|
|
10554
10559
|
"mode": "0000644",
|
|
10555
|
-
"sha256": "
|
|
10560
|
+
"sha256": "d969b2f8a7928b00bca1767eaa2a3d8f224849f100f74a1c3beae27d89962b50"
|
|
10556
10561
|
},
|
|
10557
10562
|
{
|
|
10558
10563
|
"path": "tests/hall_reading_world.mjs",
|
|
@@ -10944,6 +10949,11 @@
|
|
|
10944
10949
|
"mode": "0000644",
|
|
10945
10950
|
"sha256": "9a1813d6036886ea07d1563e71a466e66c28554b75b22605464c795fd9fa2ebb"
|
|
10946
10951
|
},
|
|
10952
|
+
{
|
|
10953
|
+
"path": "tests/me_ship_days_bound.mjs",
|
|
10954
|
+
"mode": "0000644",
|
|
10955
|
+
"sha256": "94eadffb3fd04e1916ddc251d3fca49bb7b6b9dce15eaf2a1051c2b483c7a017"
|
|
10956
|
+
},
|
|
10947
10957
|
{
|
|
10948
10958
|
"path": "tests/membership_kind_lockstep.mjs",
|
|
10949
10959
|
"mode": "0000644",
|
|
@@ -11329,6 +11339,11 @@
|
|
|
11329
11339
|
"mode": "0000644",
|
|
11330
11340
|
"sha256": "743436b1191353bf687d8fb553d423a6003707a25a548367644928478569f171"
|
|
11331
11341
|
},
|
|
11342
|
+
{
|
|
11343
|
+
"path": "tests/project_invite_ui.mjs",
|
|
11344
|
+
"mode": "0000644",
|
|
11345
|
+
"sha256": "288ee2d4d736c8481d459a902e1d5ba02f823685ee4c42c8c22cce76dcf94918"
|
|
11346
|
+
},
|
|
11332
11347
|
{
|
|
11333
11348
|
"path": "tests/project_join_door.mjs",
|
|
11334
11349
|
"mode": "0000644",
|
|
@@ -11362,7 +11377,7 @@
|
|
|
11362
11377
|
{
|
|
11363
11378
|
"path": "tests/projects_hub.mjs",
|
|
11364
11379
|
"mode": "0000644",
|
|
11365
|
-
"sha256": "
|
|
11380
|
+
"sha256": "56b1ecd69244798b704b787e13fd9fc74ec07e575a847ef6603df7430b63a51e"
|
|
11366
11381
|
},
|
|
11367
11382
|
{
|
|
11368
11383
|
"path": "tests/projects_hub_module_picker.mjs",
|
|
@@ -11372,7 +11387,7 @@
|
|
|
11372
11387
|
{
|
|
11373
11388
|
"path": "tests/projects_hub_pre_uat.mjs",
|
|
11374
11389
|
"mode": "0000644",
|
|
11375
|
-
"sha256": "
|
|
11390
|
+
"sha256": "e16e0488e1364a09503cec17a85e19ed6045fd8c6430c472c050934e1cad4013"
|
|
11376
11391
|
},
|
|
11377
11392
|
{
|
|
11378
11393
|
"path": "tests/promote_goal_id.mjs",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# ADR 0249 — The invite step is the done panel's first act, not a rail step — and the same widget is the way back
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-09-04
|
|
5
|
+
- **Task:** [task 1002346](https://cloudbongos.com/builders#/task/1002346) (BONGOS-V1, goal 1000046 — *Project creation*, criterion `proj-invite-builders` / C6), BV1.R27
|
|
6
|
+
- **Deciders:** Claude, under the Archon's standing scope
|
|
7
|
+
- **Depends on:** [ADR 0246](<redacted>.md) (a control-plane project is a catalog row keyed on its slug — what makes `client_id` resolve at all) · [ADR 0248](<redacted>.md) (the project's own owner may invite — what makes the wizard's audience anything but a 403)
|
|
8
|
+
- **Related:** [ADR 0245](<redacted>.md) (an optional step is a default rather than a dead end only if there is a way back) · [ADR 0237](<redacted>.md) / [ADR 0240](<redacted>.md) (the wizard's other skippable steps) · [ADR 0182](<redacted>.md) D2a (a refusal may not identify a stealth project)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Context
|
|
13
|
+
|
|
14
|
+
BV1.R27 asks for a **skippable invite-builders step in creation**, using the existing
|
|
15
|
+
per-individual invite. The wizard is a numbered rail — name · type · about · modules · home ·
|
|
16
|
+
address · create — and every other optional step (About, Modules) is one of those numbered
|
|
17
|
+
panels, answered *before* the project is filed and carried into the create request.
|
|
18
|
+
|
|
19
|
+
An invite cannot be. `POST /projects/invite` takes `{ client_id, github_login }` where
|
|
20
|
+
`client_id` **is the project's slug**, and it resolves that slug against a catalog row. Per
|
|
21
|
+
ADR 0246 that row is a **projection of the provisioning row, written at create** — so before
|
|
22
|
+
the Create step there is no row, and any invite filed on a numbered panel would either 404 or
|
|
23
|
+
have to be queued client-side and replayed after the create, inventing a second, unaudited
|
|
24
|
+
invite path whose failures arrive after the owner has left the screen.
|
|
25
|
+
|
|
26
|
+
The route also has no "invite later" queue and no bulk form: it pre-seeds exactly one
|
|
27
|
+
`pending` membership per call, for a GitHub login that already has a platform account.
|
|
28
|
+
|
|
29
|
+
## Decision
|
|
30
|
+
|
|
31
|
+
**The invite step is the FIRST act of the done panel, not a step in the rail** — shown the
|
|
32
|
+
moment the created-instance record carries an id and a slug, and skippable there. Four parts:
|
|
33
|
+
|
|
34
|
+
1. **It is keyed on the created record, not on `instanceId`.** The done panel already
|
|
35
|
+
resumes from `sessionStorage`'s created record on reload, and `startProgress` sets
|
|
36
|
+
`instanceId` *after* `renderDone()` runs. Keying the step on the record is what makes it
|
|
37
|
+
appear on the first paint and again on a resumed watch, with no new state to keep in step.
|
|
38
|
+
|
|
39
|
+
2. **The rail is not renumbered.** "A wizard step renumbering couples in four places" is a
|
|
40
|
+
captured learning on this exact file, and the draft key has been bumped twice for it. A
|
|
41
|
+
step that can only run after the terminal step is not a step in the rail — putting it there
|
|
42
|
+
would buy the coupling and still have to be re-ordered to sit after Create.
|
|
43
|
+
|
|
44
|
+
3. **The same widget is mounted twice**, the ADR 0245 argument: an optional step is a default
|
|
45
|
+
rather than a dead end only if there is a way back, so the project's own page carries an
|
|
46
|
+
*Invite a builder* section beside *Its extras*, on the same route with the same behaviour.
|
|
47
|
+
One implementation, two mounts, keyed state per mount — never two copies of the refusal
|
|
48
|
+
vocabulary that could drift apart.
|
|
49
|
+
|
|
50
|
+
4. **The widget answers neither "is anyone signed in" nor "is this their project."** Each call
|
|
51
|
+
site already knows and hands over a slug or nothing, which is the widget's only door in
|
|
52
|
+
(`setInviteSlug`). The wizard's answer is the created record; the manage page's is that
|
|
53
|
+
the per-id provisioning read is own-scoped and 404s for everyone else, so *reaching*
|
|
54
|
+
`renderManage` IS the ownership answer. A page that re-derived ownership would be a second,
|
|
55
|
+
weaker copy of a server rule ([ADR 0016](<redacted>.md)).
|
|
56
|
+
|
|
57
|
+
### The 403 is quoted carefully, and that is the point
|
|
58
|
+
|
|
59
|
+
ADR 0248 made the invite refusal **byte-identical and identically slow** whether the project is
|
|
60
|
+
real, stealth, absent, or the body malformed — deliberately, so a stranger cannot use it as a
|
|
61
|
+
membership oracle over private projects (ADR 0182 D2a). A surface is free to undo that in
|
|
62
|
+
copy: *"you don't have permission on this project"* asserts the project exists. So the page
|
|
63
|
+
says only what it knows — **"This account can't invite people here."** — and the pin against
|
|
64
|
+
that wording is a test, not a comment, because the tempting rewrite is the wrong one.
|
|
65
|
+
|
|
66
|
+
The other three refusals are said in the route's own vocabulary: `account_not_found` becomes
|
|
67
|
+
*"that builder hasn't signed in here yet"* (V1 requires an existing account, and the sentence
|
|
68
|
+
says what to do about it); `unknown_project` becomes *"this project isn't on the map yet — give
|
|
69
|
+
it a moment"*, because the catalog row is projected at create, so this is a moment rather than
|
|
70
|
+
a dead end; anything else falls through to the **server's own message** rather than a reason
|
|
71
|
+
the page invented.
|
|
72
|
+
|
|
73
|
+
### What it publishes of an invitee
|
|
74
|
+
|
|
75
|
+
The 201 row is `{ id, client_id, github_id, membership_kind }`. The pending line is drawn from
|
|
76
|
+
the login the inviter typed plus the row's own `membership_kind` — **never the `github_id`**,
|
|
77
|
+
which identifies someone who has accepted nothing yet, and never a state word this page
|
|
78
|
+
invented: a kind other than `pending` is shown verbatim rather than papered over.
|
|
79
|
+
|
|
80
|
+
## Consequences
|
|
81
|
+
|
|
82
|
+
- **Creation is never blocked by an invite.** The project is filed before the step is shown at
|
|
83
|
+
all, so every refusal leaves a created project and a re-tryable box. Skip sends nothing and
|
|
84
|
+
hands focus to the human-steps heading rather than stranding it in the box it just hid.
|
|
85
|
+
- **One builder per call, by design.** Bulk invite, an invite queue, and inviting someone with
|
|
86
|
+
no platform account are the route's own follow-ups, not this surface's to simulate.
|
|
87
|
+
- **The pending lines are session-local and per-project.** They are a receipt for what this
|
|
88
|
+
session filed, not a membership directory: switching the mount to another slug clears them,
|
|
89
|
+
because following them across projects would be a lie. The real directory is the project's
|
|
90
|
+
hall, and reading it back here would need a route this task does not add.
|
|
91
|
+
- **The R30 proof ([task 1002349](https://cloudbongos.com/builders#/task/1002349)) can run
|
|
92
|
+
without creating a project**, because the manage mount is the same widget on the same route.
|
|
93
|
+
- **A wizard step that must run after Create now has a home.** Anything else in that shape —
|
|
94
|
+
the first-invite nudge, a post-standup choice — belongs in the done panel beside this one
|
|
95
|
+
rather than in the rail, and the rail's seven numbered steps keep meaning "answered before
|
|
96
|
+
the project is filed".
|
|
97
|
+
|
|
98
|
+
## Alternatives rejected
|
|
99
|
+
|
|
100
|
+
- **An eighth numbered rail step, before Create.** Rejected: there is no project to invite to
|
|
101
|
+
yet, so the step could only collect names and replay them after the create — a second invite
|
|
102
|
+
path, unaudited, whose failures surface after the owner has moved on. It also buys the
|
|
103
|
+
four-place renumbering coupling for a step that must run last anyway.
|
|
104
|
+
- **A numbered step AFTER Create.** Rejected: the rail's contract is "answered before the
|
|
105
|
+
project is filed" (`paintRail` reads the whole rail as finished once the project lands), and
|
|
106
|
+
a step numbered 8 would have to be exempted from both the rail's back-navigation and its
|
|
107
|
+
finished state — the coupling with none of the meaning.
|
|
108
|
+
- **Only the manage page, with a pointer from the done panel** (the task 1002342 shape).
|
|
109
|
+
Rejected: R27 asks for the step *in creation*, and the moment an owner most wants to invite
|
|
110
|
+
people is the moment their project stands up. The pointer is kept as well, not instead.
|
|
111
|
+
- **Read the project's membership back and render the real directory.** Rejected: no
|
|
112
|
+
own-scoped read of a project's pending memberships exists on the hub, and inventing one is a
|
|
113
|
+
route change this task is not scoped for. The session receipt is honest about being one.
|
|
114
|
+
- **Word the 403 as "you don't have permission on this project."** Rejected: see above — it
|
|
115
|
+
reopens the existence oracle ADR 0248 paid two equalised reads per request to close.
|
package/docs/adr/README.md
CHANGED
|
@@ -336,6 +336,8 @@ This keeps the decision history honest and traceable.
|
|
|
336
336
|
| 0247 | [**The join door is ONE composed answer, and dark matter has no live verdict to compose with yet** ([task 1002331](https://cloudbongos.com/builders#/task/1002331) · goal 1000046, criterion `proj-join-policy`). [ADR 0182](<redacted>.md) D6 wrote the rule — visibility is the ceiling, join policy may only narrow it — and nothing implemented it: the public `POST /access-requests` read `joinability` alone, so an owner could set their project private or stealth on the map and the application endpoint behaved exactly as before. R12 adds the third knob (`join_grant` = view | apply | full, one line of settings vocabulary, default `full` so nothing existing changes) and composes all three in one pure kernel file, `src/bongos/project-door.js`, published through the doorway so no module can re-open a door the others closed. Three things worth carrying: the door vocabulary is a list ordered widest→strictest, so “the stricter wins” is the data structure rather than a branch; the refusal never names WHICH knob closed the door, because a stealth project that could be identified by its 403 gives back exactly what R06’s server-side redaction protects; and `locked` (dark matter) is specified, tested and deliberately UNWIRED — the instance cannot derive publishability (no description in its pack, and a pushed verdict goes stale the moment the owner writes one), while the hub’s cached copy says every live project is dark matter today, so enforcing off it would refuse every join on the platform.](<redacted>.md) | onboarding / provisioning / project creation |
|
|
337
337
|
| 0248 | [**A hub project’s owner is the provisioning row, carried on a port and bound to both halves of the catalog projection** ([task 1003580](https://cloudbongos.com/builders#/task/1003580) · goal 1000046, criterion `proj-invite-builders`). [ADR 0246](<redacted>.md) made a wizard-made project resolvable by `POST /projects/invite` and named the remainder: the route is gated `project.curate` (floor metic) with no own-scope arm, so the project’s own owner — a `xenos` on the hub — got 403 on their own project, blocking BV1.R27–R29 and the C6 proof. The arm admits a `project.curate` holder **OR** the owner, where owner means `provisioning_instances.owner_builder_id` — never the hub’s `owner_github_id` columns, which [ADR 0238](<redacted>.md) fixes as attribution. Three things worth carrying: the fact travels on a NEW port `provisioning.projectOwnedBy` answering a **boolean**, so the comparison stays on the plane that holds the row and no consumer handles an ownership id it could mis-compare; the port is asked about **both halves of the ADR 0246 projection** (slug AND origin), because a hand-seeded catalog row’s name is otherwise a squattable claim on that project’s membership directory; and ownership is asked BEFORE the target is resolved, so the refusal stays the untouched `requirePermission` 403 and cannot become an existence oracle over stealth projects ([ADR 0182](<redacted>.md) D2a). The atom stays spelled in the route’s own middleware list ([ADR 0210](<redacted>.md)’s rule), so the route still classifies `metic+archon`.](<redacted>.md) | onboarding / provisioning / project creation |
|
|
338
338
|
|
|
339
|
+
| 0249 | [**The invite step is the done panel’s first act, not a rail step — and the same widget is the way back** ([task 1002346](https://cloudbongos.com/builders#/task/1002346) · goal 1000046, criterion `proj-invite-builders` / C6). BV1.R27 asks for a skippable invite step *in creation*, but an invite cannot be one of the wizard’s numbered panels: `POST /projects/invite` takes `{ client_id, github_login }` where `client_id` **is the slug**, and per [ADR 0246](<redacted>.md) the catalog row that slug resolves is projected **at create** — so before the Create step there is nothing to invite anyone to, and a pre-Create panel could only queue names and replay them, inventing a second unaudited invite path whose failures land after the owner has left. **Decision: the step is the FIRST act of the DONE panel**, keyed on the created record (not `instanceId`, which `startProgress` sets only *after* `renderDone`), so it also returns on a resumed watch; the rail is NOT renumbered (a captured learning on this file: a renumbering couples in four places, and a step that can only run after the terminal step is not a step in the rail). The SAME widget is mounted again on the project’s own page beside *Its extras* — the [ADR 0245](<redacted>.md) argument, one implementation and two mounts, so the refusal vocabulary cannot drift. The widget answers neither “is anyone signed in” nor “is this their project”: each call site hands over a slug or nothing (the created record; the own-scoped manage read that 404s for everyone else), because a page that re-derived ownership would be a second, weaker copy of a server rule. **The 403 is quoted carefully and that is the point** — [ADR 0248](<redacted>.md) paid two equalised reads per request to make the refusal identical for a real, stealth, absent or malformed target ([ADR 0182](<redacted>.md) D2a), and “you lack permission on **this project**” would assert in copy the existence the route refuses to confirm, so the page says only “this account can’t invite people here” and a test pins that wording. `account_not_found` and `unknown_project` are said in the route’s own words (the latter as a moment, not a dead end — the row is projected at create); anything else falls through to the server’s own message. The pending line is drawn from the 201 row’s `membership_kind` and the typed login — **never the `github_id`** it carries, which names someone who has accepted nothing yet. Creation is never blocked: the project is filed before the step is shown, so every refusal leaves it created and the step re-tryable, and Skip sends nothing. Rejected: a rail step before or after Create; the manage page alone (R27 asks for it *in creation*); reading the real membership directory back (no own-scoped route exists, and the session receipt is honest about being one).](<redacted>.md) | provisioning / project creation |
|
|
340
|
+
|
|
339
341
|
| 0246 | [**Catalog membership is a projection of the provisioning row, not a side effect of federation** ([task 1003576](https://cloudbongos.com/builders#/task/1003576) · goal 1000046, criterion `proj-detail-interview` / C3). The BV1.R18 proof ([task 1002337](https://cloudbongos.com/builders#/task/1002337)) demonstrated C3 against live and found its last clause — “then it publishes” — structurally false: the owner’s verdict reads `provisioning_instances` while the public map reads `platform_identity_projects`, and the ONLY bridge was `federateInstance → registerClient → upsertProject`, which `shouldFederate` refuses for anything but `co-tenant` (the wizard hard-codes `standalone`) and which runs ONCE, at provision time, through a COALESCE. So a wizard-made project was never catalogued at all and a co-tenant one that answered later never updated — `publishable: true`, `{"projects": []}`, forever. The same missing row is [task 1003580](https://cloudbongos.com/builders#/task/1003580)’s GAP 1: `POST /projects/invite` resolves against `platform_identity_sso_clients`, whose only writer is that same `registerClient`. Underneath is one conflation — federation mints CREDENTIALS and was incidentally the only thing that minted MEMBERSHIP, while [ADR 0182](<redacted>.md) D4 already fixed the map’s gate at IDENTITY, which has nothing to do with hosting topology. **Decision: the catalog row is a PROJECTION of the provisioning row, re-derived at every write that can change its inputs** — two halves, neither sufficient alone. (1) THE ROW SHAPE: every control-plane project gets both rows whatever its hosting shape, and the client row carries NO credential (`secret_hash = ''`, `redirect_uris = '{}'`), so **the shape is the enforcement** rather than a flag a caller could forget — `redirectUriAllowed` refuses `/sso/authorize`, `clientSecretValid` refuses `/sso/token`, `/sso/membership/check-in` and `/sso/activity/rollup`, and the one leg that checked NEITHER (`/sso/device/start`, which resolves a client from a user-typed origin) grew an explicit `clientIsCredentialed` guard. (2) THE TIMING: re-taken on create, on `PATCH …/detail`, on `PATCH …/settings` when the door changes, and on `POST …/domain` (which re-keys the row, retiring the abandoned origin — [ADR 0181](0181-teardown-state-gate-and-address-retention.md) §2’s leak, in the catalog instead of DNS). **NO MIGRATION** — `secret_hash` is `NOT NULL` and `''` satisfies it while hashing to nothing. `description` and `visibility` are ASSIGNED, not COALESCEd (clearing a description must UN-publish); `featured`/`tagline`/`art_url`/`template` are curation and stay untouched, so [ADR 0182](<redacted>.md) D3 holds. It grants nothing — the row is attribution per [ADR 0238](<redacted>.md), ownership stays `provisioning_instances.owner_builder_id` — and because it writes rows keyed on owner-supplied values (a slug, a BYO domain) each write carries its **claim rule in the ON CONFLICT action’s own WHERE**: the client is refreshed only `WHERE secret_hash = ''` (a real client’s origin, which binds the CLI mint audience, can never be rewritten) and the project only `WHERE owner_github_id = EXCLUDED.owner_github_id` (`NULL = anything` is not true, so the hand-seeded rows cannot be claimed). Writing `visibility` does not contradict [ADR 0182](<redacted>.md) D5: the hub authorizes nothing — the authority already ran on the provisioning plane ([ADR 0238](<redacted>.md)’s own-scoped, audited PATCH), and a project the control plane does NOT own has no provisioning row, so D5’s instance report (R08) stays the only channel for those. `tearing_down`/`torn_down` project `hidden`; a `no_address` instance is not catalogued at all. Travels on the kernel port `project.catalog`, the `project.publishGate` seam in the other direction ([ADR 0083](<redacted>.md) forbids the import). Rejected: extending `shouldFederate` to every shape (hands a real secret to instances whose web.env the platform does not write, and keeps the once-at-standup bug); bridging only at the publish moment (leaves the invite with nothing to resolve); registering only at standup (the bug itself); a `catalog_only` boolean column (needs a migration and is weaker than the shape); letting the bridge write `featured`; and backfilling the three existing catalog rows from code.](<redacted>.md) | provisioning / project creation |
|
|
340
342
|
|
|
341
343
|
> ⚠️ **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.
|