@bongos/core 1.19.428 → 1.19.430

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.bongos-core.json +69 -44
  2. package/clients/bongos-client/index.d.ts +4 -4
  3. package/docs/adr/0214-canonical-profile-url-one-resolver-dark-until-the-page-exists.md +90 -0
  4. package/docs/adr/README.md +1 -0
  5. package/docs/api/openapi.json +13 -2
  6. package/docs/api-reference.md +1 -1
  7. package/docs/copy-inventory.md +137 -128
  8. package/docs/copy-registry.json +248 -166
  9. package/docs/design/mocks/hub/picker.html +421 -0
  10. package/docs/design/projects-hub-direction-v2.md +22 -0
  11. package/docs/file-map.md +2 -1
  12. package/docs/module-api-changelog.md +4 -0
  13. package/modules/hall-ui/public/profile-canonical.js +137 -0
  14. package/modules/hall-ui/public/profile-connections.js +20 -9
  15. package/modules/hall-ui/public/profile.html +9 -0
  16. package/modules/hall-ui/public/profile.js +5 -0
  17. package/modules/platform-identity/canonical-profile.js +143 -0
  18. package/modules/platform-identity/routes/projects.js +24 -0
  19. package/modules/platform-identity/routes/sso.js +7 -0
  20. package/modules/provisioning/module.json +1 -1
  21. package/modules/provisioning/provisioning.js +63 -2
  22. package/modules/provisioning/routes/provisioning.js +28 -6
  23. package/modules/provisioning/tests/provisioning.mjs +34 -0
  24. package/modules/ui-design/kit/fixtures/featured-hub.json +1 -1
  25. package/modules/ui-design/kit/fixtures/featured.json +1 -1
  26. package/modules/ui-design/kit/fixtures/provisioning-instances.json +1 -1
  27. package/modules/ui-design/kit/serve.js +28 -4
  28. package/package-lock.json +2 -2
  29. package/package.json +1 -1
  30. package/public-landing/assets/cosmos.css +37 -0
  31. package/public-landing/projects.html +164 -5
  32. package/public-landing/projects.probes.json +12 -0
  33. package/public-landing/projects.states.json +3 -2
  34. package/src/bongos/routes/builders.js +24 -2
  35. package/src/bongos/serve-internal.js +28 -2
  36. package/src/module-api.js +1 -1
  37. package/tests/canonical_profile_url.mjs +628 -0
  38. package/tests/profile_connections_ui.mjs +38 -0
  39. package/tests/profile_route.mjs +30 -5
  40. package/tests/projects_hub.mjs +122 -7
  41. package/tests/provisioning_settings.mjs +81 -2
  42. package/tests/provisioning_settings_env.mjs +6 -0
@@ -0,0 +1,421 @@
1
+ <!doctype html>
2
+ <html lang="en" data-mode="dark">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>The planet picker on the manage page, in the world</title>
7
+ <link rel="icon" href="data:,">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
11
+ <link rel="stylesheet" href="../../../../public-landing/assets/cosmos.css">
12
+ <style>
13
+ body{
14
+ /* The composition round for task 1003347 (docs/design/projects-hub-direction-v2.md §16): the
15
+ planet picker in the manage page's "its planet" section, the slot task 1003345 left. cosmos.css
16
+ carries the hub's contract and the planets (their crops, materials and styles); the manage
17
+ frame is mine.html's (§14.1, built); this sheet adds only the picker's composition. Dark only
18
+ (ADR 0204). Every colour is a token. */
19
+ --orb-shadow:opacity(1);
20
+ }
21
+ /* ── the planet, docked: the sky's object, relative instead of absolute ── */
22
+ .mock .worldOrb{position:relative;left:auto;top:auto;transform:none;display:block;cursor:default;}
23
+ .mock .worldOrb .disc{display:block;margin:0;}
24
+
25
+ /* ── the manage frame (mine.html §14.1, as built) ── */
26
+ .view{max-width:1180px;margin:0 auto;padding:34px var(--gutter) 60px;}
27
+ .vTitle{font-family:var(--font-display);font-weight:300;font-size:clamp(28px,3.4vw,44px);letter-spacing:-.035em;line-height:1.04;text-transform:lowercase;color:var(--ink);margin:0;}
28
+ .vLede{font-size:15.5px;line-height:1.5;color:var(--ink-soft);max-width:62ch;margin:0 0 22px;}
29
+ .backRow{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;}
30
+ .backRow a{color:var(--ink-faint);text-decoration:none;display:inline-block;padding:3px 0;min-height:24px;line-height:18px;}
31
+ .backRow a:hover{color:var(--ink);}
32
+ .mHead{display:grid;grid-template-columns:auto 1fr;gap:0 26px;align-items:center;padding:22px 0 8px;}
33
+ .mHead .titleRow{display:flex;align-items:baseline;gap:16px;flex-wrap:wrap;}
34
+ .ledger{display:grid;grid-template-columns:172px 1fr;gap:0 22px;margin:14px 0 0;}
35
+ .ledger > *{border-top:1px solid var(--rule-soft);padding:13px 0;}
36
+ .ledger .k{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);padding-top:16px;}
37
+ .ledger .v{font-size:15px;line-height:1.5;color:var(--ink);}
38
+ .ledger .v a{font-family:var(--font-mono);font-size:13.5px;color:var(--ink-soft);text-decoration:none;display:inline-block;padding:3px 0;min-height:24px;line-height:18px;}
39
+ .ledger .v a:hover{color:var(--ink);text-decoration:underline;}
40
+ .ledger .v small{display:block;font-size:13px;color:var(--ink-soft);}
41
+ .swDot{display:inline-block;width:11px;height:11px;border-radius:50%;background:var(--sw);border:1px solid var(--rule);vertical-align:-1px;margin-right:8px;}
42
+ .rail{display:grid;grid-template-columns:repeat(3,1fr);gap:0 10px;list-style:none;margin:0;padding:0;}
43
+ .rail li{position:relative;border-top:1px solid var(--rule);padding:12px 0 0;font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);line-height:1.3;}
44
+ .rail li::before{content:"";position:absolute;top:-4px;left:0;width:7px;height:7px;border-radius:50%;border:1px solid var(--rule);background:var(--bg);}
45
+ .rail.isLive li.done{border-top-color:var(--ok);color:var(--ok);}
46
+ .rail.isLive li.done::before{background:var(--ok);border-color:var(--ok);}
47
+ .railNote{grid-column:1/-1;margin:8px 0 0;font-family:var(--font-body);font-size:13px;line-height:1.45;color:var(--ink-soft);}
48
+ .railWrap{display:grid;gap:0;}
49
+ .doors{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-start;align-items:center;}
50
+ .door{display:inline-flex;align-items:center;justify-content:center;min-height:40px;padding:0 18px;border:1px solid var(--rule);border-radius:var(--radius-pill);background:none;font-family:var(--font-body);font-size:14.5px;color:var(--ink);text-transform:lowercase;text-decoration:none;cursor:pointer;white-space:nowrap;}
51
+ .door:hover{border-color:var(--ink-faint);}
52
+ .door.primary{background:var(--accent);border-color:var(--accent);color:var(--accent-on);font-weight:500;}
53
+ .door.primary:hover{background:color-mix(in srgb,var(--accent) 88%,var(--ink));}
54
+ .door.danger{border-color:color-mix(in srgb,var(--err) 45%,transparent);color:var(--err);}
55
+ .door.text{border-color:transparent;color:var(--ink-soft);text-decoration:underline;text-underline-offset:4px;padding:0 6px;}
56
+ .door.text:hover{color:var(--ink);border-color:transparent;}
57
+ .mSec{border-top:1px solid var(--rule);padding:30px 0 34px;display:grid;grid-template-columns:minmax(200px,300px) 1fr;gap:0 40px;}
58
+ .mSec h2{font-family:var(--font-display);font-weight:300;font-size:clamp(22px,2.4vw,28px);letter-spacing:-.03em;line-height:1.08;text-transform:lowercase;color:var(--ink);margin:0 0 8px;}
59
+ .mSec .why{font-size:13.5px;line-height:1.5;color:var(--ink-faint);max-width:34ch;margin:0;}
60
+ .mSec .body{display:grid;gap:16px;align-content:start;}
61
+ .choice{display:grid;grid-template-columns:repeat(2,1fr);border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);}
62
+ .choice label{display:grid;gap:6px;padding:16px 18px 18px;border-left:1px solid var(--rule-soft);cursor:pointer;color:var(--ink-soft);}
63
+ .choice label:first-child{border-left:0;padding-left:0;}
64
+ .choice .ch{display:flex;align-items:center;gap:10px;font-family:var(--font-body);font-weight:500;font-size:15px;color:var(--ink);}
65
+ .choice .ch::before{content:"";width:7px;height:7px;border-radius:50%;border:1px solid var(--rule);background:var(--bg);flex:none;}
66
+ .choice label.on .ch::before{background:var(--accent);border-color:var(--accent);outline:4px solid color-mix(in srgb,var(--accent) 22%,transparent);}
67
+ .choice .cb{font-size:13px;line-height:1.45;color:var(--ink-faint);}
68
+ .runs{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);}
69
+ .runs b{color:var(--ok);font-weight:400;}
70
+ .typed{display:grid;gap:10px;max-width:440px;}
71
+ .hairField{display:flex;align-items:center;gap:12px;border-bottom:1px solid var(--rule);padding:10px 0;}
72
+ .hairField input{flex:1;min-width:0;background:none;border:0;font-family:var(--font-body);font-size:15.5px;color:var(--ink);padding:4px 0;}
73
+ .hairField input::placeholder{color:var(--ink-faint);}
74
+ .hairField input:focus{outline:none;}
75
+ .hairField:focus-within{border-bottom-color:var(--ink);}
76
+ input.vh{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);}
77
+
78
+ /* ── THE PICKER: the preview planet on the left, the four bands on the right ── */
79
+ .pick{display:grid;grid-template-columns:168px 1fr;gap:0 34px;align-items:start;}
80
+ .pick .preview{display:grid;gap:12px;justify-items:center;padding-top:6px;}
81
+ .pick .preview .worldOrb{--d:132px;}
82
+ .pick .words{font-size:14px;line-height:1.45;color:var(--ink);text-align:center;max-width:168px;}
83
+ .pick .words small{display:block;font-size:12.5px;color:var(--ink-soft);}
84
+ .bands{display:grid;border-bottom:1px solid var(--rule);}
85
+ .band{display:grid;grid-template-columns:88px 1fr;gap:0 18px;align-items:center;border-top:1px solid var(--rule);}
86
+ .band:first-child{border-top-color:var(--rule);}
87
+ .band .bl{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);padding:14px 0;}
88
+ /* v1 · the bands: hairline cells of one band, a word each, the chosen one carries the dot */
89
+ .cells{display:grid;grid-auto-flow:column;grid-auto-columns:1fr;}
90
+ .cells label{display:flex;align-items:center;gap:10px;min-height:48px;padding:10px 16px;border-left:1px solid var(--rule-soft);cursor:pointer;font-family:var(--font-body);font-size:14.5px;color:var(--ink-soft);}
91
+ .cells label::before{content:"";width:7px;height:7px;border-radius:50%;border:1px solid var(--rule);background:var(--bg);flex:none;}
92
+ .cells label.on{color:var(--ink);}
93
+ .cells label.on::before{background:var(--accent);border-color:var(--accent);outline:4px solid color-mix(in srgb,var(--accent) 22%,transparent);}
94
+ .cells label:hover{color:var(--ink);}
95
+ /* the colour: five swatches, the chosen one ringed by a hairline of the ink (a swatch is its own colour) */
96
+ .swatches{display:flex;gap:14px;align-items:center;padding:12px 0 12px 16px;border-left:1px solid var(--rule-soft);}
97
+ .sw{position:relative;width:28px;height:28px;border-radius:50%;border:1px solid var(--rule);background:var(--sw);cursor:pointer;}
98
+ .sw.on{outline:1px solid var(--ink);outline-offset:3px;}
99
+ /* v2 · the objects: the cell IS the planet, in the current material and colour; the chosen one
100
+ sits in the relationship ring (a hairline circle behind the object, DESIGN.md), the word under it */
101
+ .objs label{display:grid;justify-items:center;gap:6px;padding:12px 8px 12px;border-left:1px solid var(--rule-soft);cursor:pointer;}
102
+ .objs label::before{display:none;}
103
+ .objs .obj{position:relative;display:grid;place-items:center;width:78px;height:78px;}
104
+ .objs label .worldOrb{--d:60px;}
105
+ .objs label .nm{font-family:var(--label-face);font-size:11px;letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);}
106
+ .objs label.on .nm{color:var(--ink);}
107
+ .objs label .ring{position:absolute;inset:0;border-radius:50%;border:1px solid var(--accent);opacity:0;}
108
+ .objs label.on .ring{opacity:1;}
109
+ .objs label .worldOrb{position:relative;z-index:1;}
110
+ .pickDoors{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-top:4px;}
111
+ .receipt{margin:0;font-size:14px;line-height:1.5;color:var(--ok);}
112
+ .receipt.note{color:var(--ink-soft);}
113
+
114
+ /* ── the sheet furniture ── */
115
+ section[data-state]{display:none;}
116
+ body[data-state="bands"] section[data-state="bands"],
117
+ body[data-state="objects"] section[data-state="objects"],
118
+ body[data-state="saved"] section[data-state="saved"]{display:block;}
119
+ .vh{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;}
120
+ .skip{position:absolute;left:12px;top:-60px;background:var(--bg-card);color:var(--ink);padding:11px 18px;border-radius:999px;}
121
+ .skip:focus{top:12px;}
122
+ .bar{height:var(--top-h);display:flex;align-items:center;padding:0 var(--gutter);border-bottom:1px solid var(--rule);font-family:var(--font-body);font-weight:500;font-size:17px;letter-spacing:-.015em;color:var(--ink);}
123
+ .mock .foot{padding:18px var(--gutter);font-size:14px;color:var(--ink-soft);text-align:center;}
124
+ a:focus-visible,button:focus-visible,label:focus-within{outline:2px solid var(--accent);outline-offset:3px;}
125
+
126
+ @media (max-width:900px){
127
+ .mHead{grid-template-columns:1fr;gap:14px;}
128
+ .ledger{grid-template-columns:1fr;gap:0;}
129
+ .ledger .k{border-top:1px solid var(--rule-soft);padding-bottom:0;}
130
+ .ledger .v{border-top:0;padding-top:6px;}
131
+ .mSec{grid-template-columns:1fr;gap:14px;}
132
+ .choice{grid-template-columns:1fr;}
133
+ .choice label{border-left:0;padding-left:0;border-top:1px solid var(--rule-soft);}
134
+ .choice label:first-child{border-top:0;}
135
+ /* the picker stacks: the preview above; each band's label above its choices; the cells become
136
+ the filter pills (hairline, the chosen one inverted to ink), so four bands stay one screen */
137
+ .pick{grid-template-columns:1fr;gap:22px 0;}
138
+ .pick .preview{justify-items:start;}
139
+ .pick .words{text-align:left;max-width:none;}
140
+ .band{grid-template-columns:1fr;gap:0;}
141
+ .band .bl{padding:14px 0 6px;}
142
+ .cells{display:flex;flex-wrap:wrap;gap:8px;padding:0 0 14px;}
143
+ .cells label{min-height:36px;padding:0 14px;border:1px solid var(--rule);border-radius:var(--radius-pill);gap:0;font-size:14px;}
144
+ .cells label::before{display:none;}
145
+ .cells label.on{background:var(--ink);color:var(--bg);border-color:var(--ink);}
146
+ .objs label{border:0;padding:6px 4px 8px;}
147
+ .objs label.on{background:none;}
148
+ .objs label .worldOrb{--d:52px;}
149
+ .objs .obj{width:66px;height:66px;}
150
+ .swatches{padding:6px 0 14px;border-left:0;}
151
+ }
152
+ </style>
153
+ </head>
154
+ <body class="mock" data-state="bands">
155
+ <!--
156
+ The planet picker on the manage page, the composition round (task 1003347, goal 1000080).
157
+ ?state=bands v1 the bands: the preview planet, then shape, material, style and colour as hairline
158
+ cells of one band each, a word per cell, the chosen one carrying the dot; the platform's
159
+ own pick shown first, "chosen for it by the platform"
160
+ ?state=objects v2 the objects: the same four bands, but the cell IS the planet, drawn in the current
161
+ material and colour; the chosen one sits in the relationship ring; the word under it
162
+ ?state=saved after the save: the receipt under the doors, the head and the ledger row repainted,
163
+ "your choice", and the door back to the platform's pick
164
+ The card's backdrop is not on this sheet: the poster's plate is the project's own art today, and a
165
+ platform-drawn plate set is an art round of its own; deferred, see §16.
166
+ -->
167
+ <a class="skip" href="#main">Skip to content</a>
168
+ <header class="bar">Cloud Bongos</header>
169
+ <main id="main">
170
+ <h1 class="vh">The planet picker on the manage page</h1>
171
+
172
+ <section data-state="bands">
173
+ <div class="view">
174
+ <p class="backRow"><a href="#">your projects</a></p>
175
+ <div class="mHead">
176
+ <span class="worldOrb" data-shape="pair" data-material="gold" data-style="wash" style="--acc:var(--terracotta);--d:96px"><span class="disc"></span></span>
177
+ <div>
178
+ <div class="titleRow"><h2 class="vTitle">demo</h2><span class="chip live"><span class="cdot"></span>live <span class="rec">checked 3m ago</span></span></div>
179
+ <p class="vLede" style="margin:8px 0 0">Its address, its repository, what the platform last verified about it, and the controls you have today.</p>
180
+ </div>
181
+ </div>
182
+ <div class="ledger">
183
+ <span class="k">repository</span><span class="v"><a href="#">builder/demo</a></span>
184
+ <span class="k">address</span><span class="v">Answering at <a href="#">demo.cloudbongos.com</a><small>last answered 3 minutes ago</small></span>
185
+ <span class="k">planet</span><span class="v"><span class="swDot" style="--sw:var(--terracotta)"></span>the bongo pair in gold, washed<small>chosen for it by the platform. Pick your own below.</small></span>
186
+ </div>
187
+ <div class="railWrap" style="margin:22px 0 0;max-width:520px"><ul class="rail isLive"><li class="done">Request filed</li><li class="done">Standing it up</li><li class="done">Live</li></ul><p class="railNote">Up and answering since 1 Aug.</p></div>
188
+ <div class="doors" style="margin:22px 0 34px"><a class="door" href="#">open site</a><button type="button" class="door">restart it</button></div>
189
+
190
+ <div class="mSec">
191
+ <div><h2>who can see it</h2><p class="why">The platform's request. What the project runs is read back from its own manifest.</p></div>
192
+ <div class="body">
193
+ <div class="choice">
194
+ <label class="on"><input class="vh" type="radio" name="vis" checked><span class="ch">Everyone</span><span class="cb">Anyone can open this project's pages. Signing in is only needed to take part.</span></label>
195
+ <label><input class="vh" type="radio" name="vis"><span class="ch">Only signed-in members</span><span class="cb">Visitors meet a sign-in door first.</span></label>
196
+ </div>
197
+ <p class="runs">runs <b>everyone</b>, read from the project 2m ago</p>
198
+ </div>
199
+ </div>
200
+
201
+ <div class="mSec">
202
+ <div><h2>its planet</h2><p class="why">How this project shows on the sky, in your list and on its card. Saved here, the platform draws it that way everywhere; nothing on your project restarts for it.</p></div>
203
+ <div class="body">
204
+ <div class="pick">
205
+ <div class="preview">
206
+ <span class="worldOrb" data-shape="pair" data-material="gold" data-style="wash" style="--acc:var(--terracotta)"><span class="disc"></span></span>
207
+ <p class="words">the bongo pair in gold, washed<small>the platform's pick for it</small></p>
208
+ </div>
209
+ <div class="bands">
210
+ <div class="band"><span class="bl">shape</span>
211
+ <div class="cells" role="radiogroup" aria-label="Shape">
212
+ <label><input class="vh" type="radio" name="shape">the blob</label>
213
+ <label class="on"><input class="vh" type="radio" name="shape" checked>the bongo pair</label>
214
+ <label><input class="vh" type="radio" name="shape">the bead</label>
215
+ </div>
216
+ </div>
217
+ <div class="band"><span class="bl">material</span>
218
+ <div class="cells" role="radiogroup" aria-label="Material">
219
+ <label><input class="vh" type="radio" name="material">chrome</label>
220
+ <label><input class="vh" type="radio" name="material">frost</label>
221
+ <label class="on"><input class="vh" type="radio" name="material" checked>gold</label>
222
+ <label><input class="vh" type="radio" name="material">obsidian</label>
223
+ </div>
224
+ </div>
225
+ <div class="band"><span class="bl">style</span>
226
+ <div class="cells" role="radiogroup" aria-label="Style">
227
+ <label><input class="vh" type="radio" name="style">tinted</label>
228
+ <label class="on"><input class="vh" type="radio" name="style" checked>washed</label>
229
+ <label><input class="vh" type="radio" name="style">plain</label>
230
+ </div>
231
+ </div>
232
+ <div class="band"><span class="bl">colour</span>
233
+ <div class="swatches" role="radiogroup" aria-label="Colour">
234
+ <label class="sw" style="--sw:var(--sea)"><input class="vh" type="radio" name="colour"><span class="vh">sea</span></label>
235
+ <label class="sw on" style="--sw:var(--terracotta)"><input class="vh" type="radio" name="colour" checked><span class="vh">terracotta</span></label>
236
+ <label class="sw" style="--sw:var(--gold-soft)"><input class="vh" type="radio" name="colour"><span class="vh">soft gold</span></label>
237
+ <label class="sw" style="--sw:var(--sun-lit)"><input class="vh" type="radio" name="colour"><span class="vh">sunlit</span></label>
238
+ <label class="sw" style="--sw:var(--accent)"><input class="vh" type="radio" name="colour"><span class="vh">the accent</span></label>
239
+ </div>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ <div class="pickDoors"><button type="button" class="door primary">save its planet</button></div>
244
+ </div>
245
+ </div>
246
+
247
+ <div class="mSec">
248
+ <div><h2>take it offline</h2><p class="why">Stops the server and its nightly backups. It stays in your list, marked torn down, and the repository is untouched.</p></div>
249
+ <div class="body">
250
+ <div class="typed">
251
+ <label class="hairField"><input type="text" placeholder="Type the project's name to confirm" aria-label="Type the project's name to confirm"></label>
252
+ <div class="doors"><button type="button" class="door danger">tear it down</button></div>
253
+ </div>
254
+ </div>
255
+ </div>
256
+ </div>
257
+ </section>
258
+
259
+ <section data-state="objects">
260
+ <div class="view">
261
+ <p class="backRow"><a href="#">your projects</a></p>
262
+ <div class="mHead">
263
+ <span class="worldOrb" data-shape="pair" data-material="gold" data-style="wash" style="--acc:var(--terracotta);--d:96px"><span class="disc"></span></span>
264
+ <div>
265
+ <div class="titleRow"><h2 class="vTitle">demo</h2><span class="chip live"><span class="cdot"></span>live <span class="rec">checked 3m ago</span></span></div>
266
+ <p class="vLede" style="margin:8px 0 0">Its address, its repository, what the platform last verified about it, and the controls you have today.</p>
267
+ </div>
268
+ </div>
269
+ <div class="ledger">
270
+ <span class="k">repository</span><span class="v"><a href="#">builder/demo</a></span>
271
+ <span class="k">address</span><span class="v">Answering at <a href="#">demo.cloudbongos.com</a><small>last answered 3 minutes ago</small></span>
272
+ <span class="k">planet</span><span class="v"><span class="swDot" style="--sw:var(--terracotta)"></span>the bongo pair in gold, washed<small>chosen for it by the platform. Pick your own below.</small></span>
273
+ </div>
274
+ <div class="doors" style="margin:22px 0 34px"><a class="door" href="#">open site</a><button type="button" class="door">restart it</button></div>
275
+
276
+ <div class="mSec">
277
+ <div><h2>its planet</h2><p class="why">How this project shows on the sky, in your list and on its card. Saved here, the platform draws it that way everywhere; nothing on your project restarts for it.</p></div>
278
+ <div class="body">
279
+ <div class="pick">
280
+ <div class="preview">
281
+ <span class="worldOrb" data-shape="pair" data-material="gold" data-style="wash" style="--acc:var(--terracotta)"><span class="disc"></span></span>
282
+ <p class="words">the bongo pair in gold, washed<small>the platform's pick for it</small></p>
283
+ </div>
284
+ <div class="bands">
285
+ <div class="band"><span class="bl">shape</span>
286
+ <div class="cells objs" role="radiogroup" aria-label="Shape">
287
+ <label><input class="vh" type="radio" name="shape2"><span class="obj"><span class="ring"></span><span class="worldOrb" data-shape="blob" data-material="gold" data-style="wash" style="--acc:var(--terracotta)"><span class="disc"></span></span></span><span class="nm">the blob</span></label>
288
+ <label class="on"><input class="vh" type="radio" name="shape2" checked><span class="obj"><span class="ring"></span><span class="worldOrb" data-shape="pair" data-material="gold" data-style="wash" style="--acc:var(--terracotta)"><span class="disc"></span></span></span><span class="nm">the pair</span></label>
289
+ <label><input class="vh" type="radio" name="shape2"><span class="obj"><span class="ring"></span><span class="worldOrb" data-shape="bead" data-material="gold" data-style="wash" style="--acc:var(--terracotta)"><span class="disc"></span></span></span><span class="nm">the bead</span></label>
290
+ </div>
291
+ </div>
292
+ <div class="band"><span class="bl">material</span>
293
+ <div class="cells objs" role="radiogroup" aria-label="Material">
294
+ <label><input class="vh" type="radio" name="material2"><span class="obj"><span class="ring"></span><span class="worldOrb" data-shape="pair" data-style="wash" style="--acc:var(--terracotta)"><span class="disc"></span></span></span><span class="nm">chrome</span></label>
295
+ <label><input class="vh" type="radio" name="material2"><span class="obj"><span class="ring"></span><span class="worldOrb" data-shape="pair" data-material="frost" data-style="wash" style="--acc:var(--terracotta)"><span class="disc"></span></span></span><span class="nm">frost</span></label>
296
+ <label class="on"><input class="vh" type="radio" name="material2" checked><span class="obj"><span class="ring"></span><span class="worldOrb" data-shape="pair" data-material="gold" data-style="wash" style="--acc:var(--terracotta)"><span class="disc"></span></span></span><span class="nm">gold</span></label>
297
+ <label><input class="vh" type="radio" name="material2"><span class="obj"><span class="ring"></span><span class="worldOrb" data-shape="pair" data-material="obsidian" data-style="wash" style="--acc:var(--terracotta)"><span class="disc"></span></span></span><span class="nm">obsidian</span></label>
298
+ </div>
299
+ </div>
300
+ <div class="band"><span class="bl">style</span>
301
+ <div class="cells objs" role="radiogroup" aria-label="Style">
302
+ <label><input class="vh" type="radio" name="style2"><span class="obj"><span class="ring"></span><span class="worldOrb" data-shape="pair" data-material="gold" data-style="tint" style="--acc:var(--terracotta)"><span class="disc"></span></span></span><span class="nm">tinted</span></label>
303
+ <label class="on"><input class="vh" type="radio" name="style2" checked><span class="obj"><span class="ring"></span><span class="worldOrb" data-shape="pair" data-material="gold" data-style="wash" style="--acc:var(--terracotta)"><span class="disc"></span></span></span><span class="nm">washed</span></label>
304
+ <label><input class="vh" type="radio" name="style2"><span class="obj"><span class="ring"></span><span class="worldOrb" data-shape="pair" data-material="gold" data-style="plain" style="--acc:var(--terracotta)"><span class="disc"></span></span></span><span class="nm">plain</span></label>
305
+ </div>
306
+ </div>
307
+ <div class="band"><span class="bl">colour</span>
308
+ <div class="swatches" role="radiogroup" aria-label="Colour">
309
+ <label class="sw" style="--sw:var(--sea)"><input class="vh" type="radio" name="colour2"><span class="vh">sea</span></label>
310
+ <label class="sw on" style="--sw:var(--terracotta)"><input class="vh" type="radio" name="colour2" checked><span class="vh">terracotta</span></label>
311
+ <label class="sw" style="--sw:var(--gold-soft)"><input class="vh" type="radio" name="colour2"><span class="vh">soft gold</span></label>
312
+ <label class="sw" style="--sw:var(--sun-lit)"><input class="vh" type="radio" name="colour2"><span class="vh">sunlit</span></label>
313
+ <label class="sw" style="--sw:var(--accent)"><input class="vh" type="radio" name="colour2"><span class="vh">the accent</span></label>
314
+ </div>
315
+ </div>
316
+ </div>
317
+ </div>
318
+ <div class="pickDoors"><button type="button" class="door primary">save its planet</button></div>
319
+ </div>
320
+ </div>
321
+
322
+ <div class="mSec">
323
+ <div><h2>take it offline</h2><p class="why">Stops the server and its nightly backups. It stays in your list, marked torn down, and the repository is untouched.</p></div>
324
+ <div class="body">
325
+ <div class="typed">
326
+ <label class="hairField"><input type="text" placeholder="Type the project's name to confirm" aria-label="Type the project's name to confirm"></label>
327
+ <div class="doors"><button type="button" class="door danger">tear it down</button></div>
328
+ </div>
329
+ </div>
330
+ </div>
331
+ </div>
332
+ </section>
333
+
334
+ <section data-state="saved">
335
+ <div class="view">
336
+ <p class="backRow"><a href="#">your projects</a></p>
337
+ <div class="mHead">
338
+ <span class="worldOrb" data-shape="bead" data-material="obsidian" data-style="tint" style="--acc:var(--sea);--d:96px"><span class="disc"></span></span>
339
+ <div>
340
+ <div class="titleRow"><h2 class="vTitle">demo</h2><span class="chip live"><span class="cdot"></span>live <span class="rec">checked 3m ago</span></span></div>
341
+ <p class="vLede" style="margin:8px 0 0">Its address, its repository, what the platform last verified about it, and the controls you have today.</p>
342
+ </div>
343
+ </div>
344
+ <div class="ledger">
345
+ <span class="k">repository</span><span class="v"><a href="#">builder/demo</a></span>
346
+ <span class="k">address</span><span class="v">Answering at <a href="#">demo.cloudbongos.com</a><small>last answered 3 minutes ago</small></span>
347
+ <span class="k">planet</span><span class="v"><span class="swDot" style="--sw:var(--sea)"></span>the bead in obsidian, tinted<small>your choice.</small></span>
348
+ </div>
349
+ <div class="doors" style="margin:22px 0 34px"><a class="door" href="#">open site</a><button type="button" class="door">restart it</button></div>
350
+
351
+ <div class="mSec">
352
+ <div><h2>its planet</h2><p class="why">How this project shows on the sky, in your list and on its card. Saved here, the platform draws it that way everywhere; nothing on your project restarts for it.</p></div>
353
+ <div class="body">
354
+ <div class="pick">
355
+ <div class="preview">
356
+ <span class="worldOrb" data-shape="bead" data-material="obsidian" data-style="tint" style="--acc:var(--sea)"><span class="disc"></span></span>
357
+ <p class="words">the bead in obsidian, tinted<small>your choice</small></p>
358
+ </div>
359
+ <div class="bands">
360
+ <div class="band"><span class="bl">shape</span>
361
+ <div class="cells" role="radiogroup" aria-label="Shape">
362
+ <label><input class="vh" type="radio" name="shape3">the blob</label>
363
+ <label><input class="vh" type="radio" name="shape3">the bongo pair</label>
364
+ <label class="on"><input class="vh" type="radio" name="shape3" checked>the bead</label>
365
+ </div>
366
+ </div>
367
+ <div class="band"><span class="bl">material</span>
368
+ <div class="cells" role="radiogroup" aria-label="Material">
369
+ <label><input class="vh" type="radio" name="material3">chrome</label>
370
+ <label><input class="vh" type="radio" name="material3">frost</label>
371
+ <label><input class="vh" type="radio" name="material3">gold</label>
372
+ <label class="on"><input class="vh" type="radio" name="material3" checked>obsidian</label>
373
+ </div>
374
+ </div>
375
+ <div class="band"><span class="bl">style</span>
376
+ <div class="cells" role="radiogroup" aria-label="Style">
377
+ <label class="on"><input class="vh" type="radio" name="style3" checked>tinted</label>
378
+ <label><input class="vh" type="radio" name="style3">washed</label>
379
+ <label><input class="vh" type="radio" name="style3">plain</label>
380
+ </div>
381
+ </div>
382
+ <div class="band"><span class="bl">colour</span>
383
+ <div class="swatches" role="radiogroup" aria-label="Colour">
384
+ <label class="sw on" style="--sw:var(--sea)"><input class="vh" type="radio" name="colour3" checked><span class="vh">sea</span></label>
385
+ <label class="sw" style="--sw:var(--terracotta)"><input class="vh" type="radio" name="colour3"><span class="vh">terracotta</span></label>
386
+ <label class="sw" style="--sw:var(--gold-soft)"><input class="vh" type="radio" name="colour3"><span class="vh">soft gold</span></label>
387
+ <label class="sw" style="--sw:var(--sun-lit)"><input class="vh" type="radio" name="colour3"><span class="vh">sunlit</span></label>
388
+ <label class="sw" style="--sw:var(--accent)"><input class="vh" type="radio" name="colour3"><span class="vh">the accent</span></label>
389
+ </div>
390
+ </div>
391
+ </div>
392
+ </div>
393
+ <div class="pickDoors"><button type="button" class="door primary">save its planet</button><button type="button" class="door text">let the platform choose again</button></div>
394
+ <p class="receipt" role="status">Saved. The sky, your list and the card draw it this way from here.</p>
395
+ </div>
396
+ </div>
397
+
398
+ <div class="mSec">
399
+ <div><h2>take it offline</h2><p class="why">Stops the server and its nightly backups. It stays in your list, marked torn down, and the repository is untouched.</p></div>
400
+ <div class="body">
401
+ <div class="typed">
402
+ <label class="hairField"><input type="text" placeholder="Type the project's name to confirm" aria-label="Type the project's name to confirm"></label>
403
+ <div class="doors"><button type="button" class="door danger">tear it down</button></div>
404
+ </div>
405
+ </div>
406
+ </div>
407
+ </div>
408
+ </section>
409
+ </main>
410
+ <footer class="foot">A composition sheet for task 1003347. Nothing here is served.</footer>
411
+ <script>
412
+ /* The mock's state contract: read ?state= once, apply one state (bands | objects | saved).
413
+ No motion in script; nothing is built here. */
414
+ (function () {
415
+ var state = new URLSearchParams(location.search || location.hash.slice(1) || (window.__Q || '')).get('state') || 'bands';
416
+ if (['bands', 'objects', 'saved'].indexOf(state) === -1) state = 'bands';
417
+ document.body.setAttribute('data-state', state);
418
+ })();
419
+ </script>
420
+ </body>
421
+ </html>
@@ -234,3 +234,25 @@ Three mastheads, rendered dark and light at 1440 / 390 / 320 (ALL CLEAN) and mea
234
234
  **The kit.** Six per-step states in `projects.states.json` (`in-new-type` … `in-new-done`), each replaying the walk from the name step in a fresh context, and a **wizard driver** in `projects.probes.json` that files a project end to end at 1440 (and reaches the review at 390): the name, the type cell, the about textarea and the team cell, home, the handle, a domain, the review, `#doCreate`; then the done panel with the standup rail lit and the human steps rendered. The stub gained the handle check (`/provisioning/slug-available` → free) and its filing receipt now names an instance — Harbor, the fixture's standing-up row — so the done panel polls a real row and the rail reads "standing it up". Three things the driver taught: the stub's `/github/repo*` answer reads as repo access granted, so the home step takes the **fresh** path and names the repository from the project (no manual fields); a **dotless host assigns no address**, so the address step opens its domain fold itself and the walk brings `mercury.example.com` (a click on the summary would close it); the done clause's inline link sat under the 24px floor, the same problem the sheet had. AFTER: `render.js --page /projects` ALL CLEAN (66 states × 3 sizes), the empty and floor siblings ALL CLEAN, probes 41/41.
235
235
 
236
236
  **Retargeted.** `tests/projects_hub.mjs` gains two pins: the frame by scope (no card ground, six tracks, the dot, the lit step's halo, the lowercase title, the pills, the inverted sign-in, the halo gone, no box-shadow, no literal colour) and the structures (hairline fields, the bands, the ledger, the numbered steps with every child in the text column — the sheet's own bug — the rail without a pulse, the tick gone, the address in the display voice, no gradient) plus **the filing body pinned** (slug, `hosting_shape`, `target_ref`, `onboard_mode` — nothing enqueued changed, ADR 0111) and the kit's states and driver present. `tests/onboard.mjs` needed no retarget: the human steps' strings and tags are JS-rendered and untouched. The copy registry regenerated (line numbers moved with the block; no string changed).
237
+
238
+ ## 16. The planet picker: the composition round (task 1003347)
239
+
240
+ **Date:** 2026-08-29 · the owner's note on the round-3 review ("backdrop and materials are good, we should have a projects setting that allows you to select which material and color you want. This could also house and project card backdrop customization"), filed as task 1003347 behind the manage page (§14.1) and the materials (§13). The slot is the manage page's "its planet" section and the ledger's planet row ("chosen for it by the platform" until now).
241
+
242
+ **The round.** One sheet, `docs/design/mocks/hub/picker.html` (`?state=bands|objects|saved`; check-mock ALL RULES PASS; shot at 1440 and 390 through the kit's stub, ALL CLEAN): the manage frame as built (§14.1), and the "its planet" section with **the preview planet on the left** (the sky's object at 132px, its words under it: "the bongo pair in gold, washed", then "the platform's pick for it" or "your choice") and **four bands on the right** (shape · material · style · colour, each a label-face word in an 88px column with its choices beside it, on shared hairlines); the doors under them (save its planet as the primary pill; once a choice is stored, "let the platform choose again" as an underlined text door); the receipt as one line in the ok ink. **v1 the bands**: the choices as hairline cells of one band, a word each, the chosen one carrying the dot (the grammar of the two settings above it); the colour as five swatches on the world's five accents, the chosen one ringed by a hairline of the ink. **v2 the objects**: the same bands, but the cell IS the planet, drawn in that shape, material or style in the colour on, the chosen one sitting in the relationship ring (the hairline circle behind the object, DESIGN.md), the word under it in the label face. Under 900px both stack the preview above the bands; v1's cells become the list's filter pills (the chosen one inverted to ink) so four bands stay one screen; v2 keeps its objects in a row at 52px. The shots found one thing the sheet fixed: an object's ring must sit on an equal box per cell (the pair is wide and short, the bead tall), not on the object's own box. The pick page: https://claude.ai/code/artifact/<redacted> (the owner picks v1 or v2; if no pick lands, the build takes v1 the bands).
243
+
244
+ **What the sheet decides.** The choice is the thirty-six template names the sky already knows (shape × material × style, §13) plus one of the world's five accents, never a free colour (the Fifteen Rule); it shows the same on the sky, the list, the manage page and the card; unset, the platform's pick stands: a curated template if there is one, else the walk; the save is a settings save like visibility and joinability, with a receipt, and nothing on the project restarts for it (the planet is the platform's to draw, not the instance's to run). **What it does not.** The card's backdrop: the poster's plate is the project's own `art_url` today and the world has no plate set for a card; a platform-drawn set to choose from is an art round of its own (on the owner's key now that the free coverage has ended), filed as task 1003352 behind this one.
245
+
246
+ **Not built yet.** The page waits for the pick (the round's method). The build: the settings row grows `planet_template` and `planet_accent` as platform-side keys (validated, stored, audited; never transported to the instance's env, so `settingsEnvVars` and the runner's consumed-check stay on the instance's keys), the PATCH answering a no-restart receipt when only those change; `publicInstance` carries both so the list and the manage page read them from the row; a `provisioning.projectPlanets` seam lets `/projects/featured` carry them for the matching origin, so the sky, `planetOf` and the poster read the owner's choice before the curated template and the walk; the section in `cosmos.css` by scope; a kit state and a probe that saves through the stub; `tests/projects_hub.mjs` and the settings suites retargeted; the copy registry regenerated.
247
+
248
+ ### 16.1 The build: v1 the bands (task 1003347)
249
+
250
+ **Date:** 2026-08-29 · the pick page carried no comment in the session, and the round named its fallback ("if no pick lands, the build takes v1 the bands"), so the build took it — **v1 the bands**, the settings' own grammar. The pick page stays up; v2 the objects is a CSS-and-markup round away (the cell drawing the planet in place of the word).
251
+
252
+ **What was built.** The planet is now the owner's to choose, and one choice reads the same everywhere the platform draws it. **The row.** The provisioning row's jsonb `settings` column carries `planet_template` (one of the thirty-six names, mirrored as `PLANET_TEMPLATES` so provisioning never imports the hub's module — the `PROJECT_TYPES` precedent, pinned name for name) and `planet_accent` (one of the five accents BY NAME: sea, terracotta, gold-soft, sun-lit, accent — a token name, never a colour). They are read by `planetChoice(row)`, not `effectiveSettings`: the planet is deliberately not a setting in `SETTINGS_VOCAB`'s sense — a setting is a policy the instance obeys (it rides web.env and a restart applies it), the planet is the platform's to draw, so it has no default (null = the platform's own pick: a curated template, else the walk), never rides the env transport, and never restarts anything. `GET …/settings` answers `planet` beside `settings`; the PATCH validates both keys (nullable: null hands the pick back), writes them with the same audited merge, and answers a no-restart receipt when only the planet changed ("Saved. The sky, your list and the card draw it this way from here; nothing on your project restarts for it."); a policy key re-sent over a retired push's error_note still re-kicks the push, a planet re-sent does not. `publicInstance` carries both keys, so the list and the manage page read the choice off the row with no second read. **The feed.** A new port, `provisioning.projectPlanets` (hosts → the owners' choices, newest row per host), registered beside `provisioning.instanceState`; `/projects/featured` resolves it optionally (an instance without provisioning has no choices to carry; never a required capability, so the hub boots without it) and spreads `planet_template` / `planet_accent` on every project — the wire always carries both keys. **The page.** `accentOf` / `templateOf` read the owner's choice first, then the curated template, then the walk; `planetOf(inst)` reads the row's own choice before the catalog or the slug walk and says whose pick it is (`chosen`), so the sky, the list row, the manage head and the poster's dot agree; the ledger's planet row reads "your choice." once one is stored. The "its planet" section (`#mPlanetPick`: the title, the volatile slot `#mPick`, the fold `#mPickWhat` as a sibling) sits between how people join and the ender: the preview planet on the left (the sky's object at 132px, its words under it, "the platform's pick for it" or "your choice"), four bands on the right (shape · material · style as hairline cells of one band, a word each, the chosen one carrying the dot; the colour as five swatches on their tokens, the chosen one ringed by a hairline of the ink), the doors under them (Save its planet; Let the platform choose again once a choice is stored), the receipt as the section's own status line. The pending pick follows the cells (the preview redraws on every change, the focus kept), survives the minute tick, and survives a catalog that lands late unless its resolution actually changed; the save is the settings PATCH the two sections above use, its reply a receipt — the head, the ledger row and the list repaint from the row and the sky re-reads the feed. Under 900px the preview stacks above the bands and the cells become the list's filter pills, the chosen one inverted to ink. `cosmos.css` carries it by scope (`#view-manage .pick…`): no card ground, no box shadow, no literal colour.
253
+
254
+ **The card's backdrop, deferred.** The poster's plate is the project's own `art_url` today; a platform-drawn plate set to choose from is an art round of its own — task 1003352, behind this one.
255
+
256
+ **The kit.** The stub remembers a saved planet for the run (a `PLANETS` map, the teardown-ask precedent) and answers the route's own receipt shapes, so the page's re-read finds its choice; both featured fixtures carry the keys on every row and Demo's owner has chosen `pair-obsidian-tint` on sea (the feed path — kept the pair's box so the sky's composition, and a signed-out Shift probe's blank-spot click, stand); Harbor's row carries `blob-frost-tint` on sun-lit (the row path — the list draws it with no catalog row). A state, `in-manage-planet`, renders the section on Demo; three probes save a pick and read the receipt, the head and the ledger row back, hand the pick back to the platform, and save through the pills at 390. `render.js --page /projects` ALL CLEAN (67 states), the empty and floor siblings ALL CLEAN, probes 44/44.
257
+
258
+ **Retargeted.** `tests/projects_hub.mjs`: the mine-row and walk pins name the owner's choice first, the field-path pin reads Demo's choice over the curated template, and five new pins hold the vocabulary lockstep (client ↔ provisioning ↔ platform-identity), the section's markup and fold, the script (the preview, the radiogroups, the tick and late-catalog rules, the PATCH body, no open-intent re-read), the sheet by scope with the 900px pills, the kit's state/probes/fixtures/stub, and the seam. `tests/provisioning_settings.mjs` (the planet: saved, echoed, audited, no push; null clears; no change; no re-kick; a mixed save; off-vocabulary → 400), `modules/provisioning/tests/provisioning.mjs` (the mirror, `planetChoice`, `publicInstance`), `tests/provisioning_settings_env.mjs` (never in the env). The API docs regenerated; the copy registry regenerated.
package/docs/file-map.md CHANGED
@@ -244,7 +244,7 @@
244
244
  │ ├── terrain.js ← (Phase D + ADR 0012, CJS): TERRAIN + OBJECT enums + BLOCKING_TERRAINS + BLOCKING_OBJECTS (server twin of public/game/world/terrain.js)
245
245
  │ └── map.js ← (Phase D + ADR 0012): two-grid INITIAL_MAP {ground, objects}; isBlocked checks both layers (mirrors public/game/world/{terrain,map}.js)
246
246
  │ (public-status/ moved to modules/status-ui/public/ in BV1.R84 — see the modules/ block above)
247
- │ (public-builders/ moved to modules/hall-ui/public/ in BV1.R85 — the authenticated builders' hall; see the modules/ block above. It keeps its DEDICATED serving block in src/bongos/serve-internal.js: the builders.<apex> + apex /builders/* shim, canonical redirects, the citizen/Archon page gates, asset-stamping, and the module hall-widget injection — partitioned out of the generic web-surface loop. index.html=the hall dashboard; work/board/roadmap/goals/watch/harbor/gate/pair/settings/ranks/sessions/primer/diagrams/atlas pages; ideas.{html,js,css}=the Full Idea composer (task 1002928 / R152 of goal 1000062 — the five R141 fields with a live R142 completeness meter, both fetched from the server so the page carries no copy of the rules; drafts in localStorage so navigation costs nothing. Task 1002930 / R154 added the SPARK QUEUE above it and a second mode: `?develop=<id>` reuses the same form to develop a spark — the spark's words shown above the fields, its title read-only, and the file routed to POST /inbox/:id/develop instead of POST /inbox, under its own per-idea draft key); idea.html/idea.js=the idea-detail PAGE (/idea/:id, task 1002929 / R153 — the SECOND two-segment hall page after /task/:id: grade, the five answers in template order, the score, and both attributions. The GRADE decides what the page contains — a quick idea gets no five-field skeleton. task-detail.js keeps the #/idea/N overlay because ~320 of its lines are the Metic+ triage panel; the overlay now links here); profile.html/profile.js=the public, shareable builder profile page (/profile?id=N — rank, works, credits, karma, achievements; task 1467); profile-connections.js=section 6 of that page, the hub-only connections block (task 1002296 / R20 of goal 1000045 — the viewer-relative 1st/2nd/3rd/out-of-network label, the mutuals it was computed from, the connect button, and on your own profile both request queues plus your connections ledger. Loaded BEFORE profile.js, which calls window.OTBConnections.render behind a guard, so a page that omits the script simply has no section; the section reveals only when the hub's /connections routes answer, exactly as the cross-project record does. ADR 0212 records why it is on this page rather than the not-yet-built /u/&lt;handle&gt;, and why it addresses a builder by github_login and then uses the canonical handle the API echoes back); task.html/task.js=the task-detail page (/task/:id, task 751 — replaced the old #/task/N overlay); builders.js=the controller; dom-utils.js/hall-widgets.js=the widget+settings-panel registries.)
247
+ │ (public-builders/ moved to modules/hall-ui/public/ in BV1.R85 — the authenticated builders' hall; see the modules/ block above. It keeps its DEDICATED serving block in src/bongos/serve-internal.js: the builders.<apex> + apex /builders/* shim, canonical redirects, the citizen/Archon page gates, asset-stamping, and the module hall-widget injection — partitioned out of the generic web-surface loop. index.html=the hall dashboard; work/board/roadmap/goals/watch/harbor/gate/pair/settings/ranks/sessions/primer/diagrams/atlas pages; ideas.{html,js,css}=the Full Idea composer (task 1002928 / R152 of goal 1000062 — the five R141 fields with a live R142 completeness meter, both fetched from the server so the page carries no copy of the rules; drafts in localStorage so navigation costs nothing. Task 1002930 / R154 added the SPARK QUEUE above it and a second mode: `?develop=<id>` reuses the same form to develop a spark — the spark's words shown above the fields, its title read-only, and the file routed to POST /inbox/:id/develop instead of POST /inbox, under its own per-idea draft key); idea.html/idea.js=the idea-detail PAGE (/idea/:id, task 1002929 / R153 — the SECOND two-segment hall page after /task/:id: grade, the five answers in template order, the score, and both attributions. The GRADE decides what the page contains — a quick idea gets no five-field skeleton. task-detail.js keeps the #/idea/N overlay because ~320 of its lines are the Metic+ triage panel; the overlay now links here); profile.html/profile.js=the public, shareable builder profile page (/profile?id=N — rank, works, credits, karma, achievements; task 1467); profile-connections.js=section 6 of that page, the hub-only connections block (task 1002296 / R20 of goal 1000045 — the viewer-relative 1st/2nd/3rd/out-of-network label, the mutuals it was computed from, the connect button, and on your own profile both request queues plus your connections ledger. Loaded BEFORE profile.js, which calls window.OTBConnections.render behind a guard, so a page that omits the script simply has no section; the section reveals only when the hub's /connections routes answer, exactly as the cross-project record does. ADR 0212 records why it is on this page rather than the not-yet-built /u/&lt;handle&gt;, and why it addresses a builder by github_login and then uses the canonical handle the API echoes back); profile-canonical.js=the canonical-profile wiring for that page (task 1002804 / P08, ADR 0214 — on a FEDERATED instance the rel=canonical plus the visible "see the cross-project record" link-out ADR 0171 D1 asks for; on the HUB the numeric ?id= form redirects to the handle URL instead, because a page cannot both redirect to a URL and declare it canonical. All three read ONE resolver, modules/platform-identity/canonical-profile.js, which answers null while /u/&lt;handle&gt; does not exist — so everything is dark today and one boolean turns it all on together); task.html/task.js=the task-detail page (/task/:id, task 751 — replaced the old #/task/N overlay); builders.js=the controller; dom-utils.js/hall-widgets.js=the widget+settings-panel registries.)
248
248
  │
249
249
  ├── infra/ ← ON-BOX scripts (task 1002699): the four reporter/heartbeat crons every dev box runs from its /workspace clone — box-heartbeat.sh (idle-sweep activity signal), box-report-version.sh (HEAD → POST /box/version), box-report-host-keys.sh (SSH host keys → POST /box/host-keys), box-report-terminal.sh (ttyd URL+credential → POST /box/terminal). Delivered by the workspace clone itself (modules/dev-box/box-access.js sparse scope includes infra/); crons self-installed by box-source-fetch.sh's per-tick bring-up. API base resolves GDS_BASE env → session-file api_base → skip (no hardcoded host in the neutral core).
250
250
  ├── devbox-app/ ← OTB Dev Box — Electron menu-bar/tray switch for the builder's box (#904, ADR 0045; internal). SELF-CONTAINED: own package.json (never feeds the droplet's npm ci); built by .github/workflows/build-devbox-app.yml; published to the public releases repo per config/devbox-app.json; release recipe docs/recipes/devbox-app-release.md
@@ -351,6 +351,7 @@ tests/
351
351
  ├── own_scope_guard.mjs ← (task 1003343, goal 1000045 criterion own-scope-held-by-a-test): the OWN-SCOPE structural guard, generalised out of memory_isolation.mjs. An explicit inventory of own-scoped route files; inside them the route's declared PATH decides — a path naming no builder must never read a subject from the request (direct, optional-chained, destructured, aliased, computed-key or header), a path that DOES name one needs a per-route exemption whose gate the guard verifies is present. Repo-wide backstop: any file mounting a `/me` route must be in the inventory. Source-grepping, so it runs in the DB-free `unit` lane.
352
352
  ├── account_existence_rate_limit.mjs ← (task 1003339, goal 1000045, ADR 0209): the two PUBLIC account-existence probes (onboarding GET /access-requests/status + core GET /auth/web/admission-status) share ONE per-IP budget. Counts to the limit on each route, proves the budget is shared in both directions and when alternated, moves Date.now to prove the window slides on the real mounted route, and proves a refused probe issues no query. DB-free; mutation-checked three ways.
353
353
  ├── effective_visibility_predicate.mjs ← (task 1002967, goal 1000045): the EFFECTIVE-VISIBILITY structural guard over `modules/platform-identity/account-visibility.js` (the file task 1002295 landed and this task ADOPTED rather than duplicating). Three layers — SQL and JS agree row-for-row over the privacy-spec D2 matrix; that file is the only one allowed to compare `profile_state`/`account_visibility`/`hide_stats` to a literal (per-occurrence exemptions, each with a reason and a checked obligation); and every `SELECT` on `platform_identity_accounts` is enumerated FROM SOURCE and must join a shared predicate, be declared non-public, or be a checked deferral to a named caller — so a read that does not exist yet (hub search, the sitemap) fails the build until its author classifies it. Behavioural half runs the real `/profiles/:handle` route over an in-memory accounts table filtered by each statement's own WHERE clause: private → 404 (never 403), byte-identical to stranger and owner alike. DB-free `unit` lane.
354
+ ├── canonical_profile_url.mjs ← (task 1002804, goal 1000045 criterion C1, ADR 0214): the P08 proof — the canonical profile URL is resolved in ONE place, `handle` reaches `GET /builders/:id/profile` under the EXISTENCE predicate (so a hidden builder keeps their address), absent/provisional/private are BYTE-IDENTICAL so the numeric-id redirect cannot become an existence oracle, and the widened response still leaks no private column. Real core router over a WHERE-derived fake pool on a real socket; the real browser script in a vm above a stub DOM, run twice — as shipped (every surface dark, because `/u/<handle>` still 404s) and with the liveness constant flipped, so the three live behaviours are proved rather than deferred. Asserts its own DB-free lane membership. DB-free `unit` lane.
354
355
  ├── application_lifecycle.mjs ← (task 1002284, goal 1000045, ADR 0201 D1/D3.2/D4 + ADR 0208): the application lifecycle EXECUTED — apply, list-pending, approve, decline, RESCIND (`invited → dismissed`), idempotency on the state already held, and the two things that must never happen: a builder without `access_request.review` moving the queue, and a VOUCH buying admission. Real router, REAL `requirePermission` resolving against the real government catalog's rank seed, and the REAL `hasInvitedAccessRequest` from `src/bongos/db-kernel.js` pointed at the same in-memory `access_requests` table (a stubbed `./pool`), whose filter is built from each statement's own WHERE clause and which refuses a predicate it cannot read. The no-authority proof seeds a vouched row and asserts the approve path and the admission read are byte-identical to the unvouched twin, and that no statement on either path names `applicant_github_id`/`vouched_at`. DB-free `unit` lane.
355
356
  ├── application_echo.mjs ← (task 1002284, goal 1000045, ADR 0201 D1.3/D1.4): the hub's STATUSLESS echo of an application. Part A pins the domain functions — the read binds `APPLICATION_ECHO_WINDOW_DAYS` itself (one 30, not three), the write is an upsert on the pair, neither statement mentions a status, and a non-numeric principal is refused before any query (ADR 0171 D2). Part B runs the real `POST /my-projects/join` and `GET /my-projects` over an in-memory `platform_identity_application_echoes` table with the relay's answer scripted: only an accepted application leaves a trace — a standing membership, either door refusal, a members-only project and an unreachable project leave none — and best-effort is asserted in both directions (a failed echo write still reports the filed application; a failed echo read still lands the memberships listing). DB-free `unit` lane.
356
357
  ├── scouting_owner_access_and_reach.mjs ← (task 1002290, goal 1000045 C5, ADR 0210): the R14 proof — WHO may read the cross-project recruiting directory, and WHO appears in it. A project owner (an instance-asserted, hub-witnessed `membership_kind='owner'`) reads `GET /scouting` with no rank at all; a builder who is neither owner nor `project.curate` holder gets the unchanged 403; an un-witnessed owner claim is refused; a broken ownership read falls closed onto the permission gate rather than admitting or 500ing. And the recruiting opt-out finally bites: a public, unhidden, active account that opted out is absent from `/scouting` and the hall's `/people` (the same endpoint), returns when it opts back in, and the opt-out grants and denies no permission — an opted-out owner still reads the directory while being absent from their own copy of it. Fake pool filtered by each statement's own WHERE clause, real route, real `requirePermission` over the real government catalog, real HTTP. Mutation-checked three ways. DB-free `unit` lane.
@@ -1293,5 +1293,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1293
1293
  landed since 1.19.426 with no explicit bump. run 33271340372. (task 1002620)
1294
1294
  1.19.428 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1295
1295
  landed since 1.19.427 with no explicit bump. run 33274315388. (task 1002620)
1296
+ 1.19.429 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1297
+ landed since 1.19.428 with no explicit bump. run 33274860035. (task 1002620)
1298
+ 1.19.430 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1299
+ landed since 1.19.429 with no explicit bump. run 33275468532. (task 1002620)
1296
1300
  ---------------------------------------------------------------------------
1297
1301
  ```