@bongos/core 1.19.441 → 1.19.443

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 (171) hide show
  1. package/.bongos-core.json +496 -441
  2. package/clients/bongos-client/index.d.ts +1 -2
  3. package/config/branding.neutral.json +1 -1
  4. package/docs/adr/0218-the-apex-root-web-surface.md +55 -0
  5. package/docs/adr/README.md +1 -0
  6. package/docs/api/openapi.json +3 -20
  7. package/docs/api-reference.md +1 -1
  8. package/docs/architecture.md +2 -1
  9. package/docs/copy-inventory.md +363 -359
  10. package/docs/copy-registry.json +608 -561
  11. package/docs/design/mocks/hub/mine.html +1 -1
  12. package/docs/design/mocks/hub/picker.html +1 -1
  13. package/docs/design/mocks/hub/planets.html +1 -1
  14. package/docs/design/mocks/hub/v1.html +3 -3
  15. package/docs/design/mocks/hub/v2.html +2 -2
  16. package/docs/design/mocks/hub/v3.html +5 -5
  17. package/docs/design/mocks/hub/wizard.html +1 -1
  18. package/docs/design/mocks/landing/v1.html +1 -1
  19. package/docs/design/mocks/landing/v2.html +1 -1
  20. package/docs/design/mocks/landing/v3.html +1 -1
  21. package/docs/design/mocks/landing-page-mock.html +3 -3
  22. package/docs/design/mocks/projects-hub-mock.html +1 -1
  23. package/docs/design/mocks/projects-hub-variants.html +1 -1
  24. package/docs/design/mocks/projects-page-mock.html +1 -1
  25. package/docs/design/projects-hub-direction-v2.md +23 -0
  26. package/docs/file-map.md +3 -3
  27. package/docs/module-api-changelog.md +4 -0
  28. package/docs/modules-contract.md +9 -2
  29. package/docs/recipes/ui-look-before-you-ship.md +4 -4
  30. package/modules/copy-desk/tests/copy_flags.mjs +4 -4
  31. package/modules/platform-identity/routes/projects.js +2 -1
  32. package/modules/provisioning/provisioning.js +11 -2
  33. package/modules/provisioning/routes/provisioning.js +10 -5
  34. package/modules/public-landing/CLAUDE.md +15 -0
  35. package/modules/public-landing/module.json +15 -0
  36. package/{public-landing → modules/public-landing/public}/assets/cosmos.css +8 -0
  37. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  38. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +14 -0
  39. package/modules/public-landing/public/assets/world/r4-card-deepfield.jpg +0 -0
  40. package/modules/public-landing/public/assets/world/r4-card-deepfield.jpg.json +14 -0
  41. package/modules/public-landing/public/assets/world/r4-card-dust.jpg +0 -0
  42. package/modules/public-landing/public/assets/world/r4-card-dust.jpg.json +14 -0
  43. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  44. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +14 -0
  45. package/{public-landing → modules/public-landing/public}/projects-floor.states.json +28 -1
  46. package/{public-landing → modules/public-landing/public}/projects.html +101 -1
  47. package/{public-landing → modules/public-landing/public}/projects.probes.json +6 -0
  48. package/{public-landing → modules/public-landing/public}/projects.states.json +1 -0
  49. package/modules/ui-design/kit/fixtures/featured-hub.json +1 -1
  50. package/modules/ui-design/kit/lib.js +2 -2
  51. package/modules/ui-design/kit/render.js +1 -1
  52. package/modules/ui-design/kit/serve.js +32 -7
  53. package/package-lock.json +2 -2
  54. package/package.json +1 -1
  55. package/scripts/gds/copy-inventory.js +3 -3
  56. package/scripts/gds/fitness-checks-identity.js +18 -1
  57. package/scripts/gds/publish-manifest.js +1 -1
  58. package/src/bongos/module-scope-map.js +9 -7
  59. package/src/bongos/serve-internal.js +13 -1
  60. package/src/module-api.js +1 -1
  61. package/src/module-loader/loader.js +34 -17
  62. package/src/module-loader/manifest-schema.js +16 -8
  63. package/src/platform-server.js +50 -18
  64. package/tests/copy_inventory.mjs +0 -0
  65. package/tests/cosmos_token_contrast.mjs +6 -6
  66. package/tests/fitness.mjs +20 -0
  67. package/tests/landing_legal_pages.mjs +2 -1
  68. package/tests/landing_page.mjs +7 -5
  69. package/tests/module-scope-map.mjs +2 -2
  70. package/tests/module_loader.mjs +4 -1
  71. package/tests/onboard.mjs +5 -5
  72. package/tests/projects_hub.mjs +105 -20
  73. package/tests/projects_hub_pre_uat.mjs +5 -3
  74. package/tests/provisioning_settings.mjs +49 -4
  75. package/tests/public_landing_projects.mjs +2 -2
  76. package/tests/terms_acceptance.mjs +1 -1
  77. package/tests/ui_design_kit.mjs +3 -3
  78. package/tests/world_tokens.mjs +3 -2
  79. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_00.webp +0 -0
  80. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_01.webp +0 -0
  81. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_02.webp +0 -0
  82. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_03.webp +0 -0
  83. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_04.webp +0 -0
  84. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_05.webp +0 -0
  85. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_06.webp +0 -0
  86. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_07.webp +0 -0
  87. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_08.webp +0 -0
  88. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_09.webp +0 -0
  89. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_10.webp +0 -0
  90. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_11.webp +0 -0
  91. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_12.webp +0 -0
  92. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_13.webp +0 -0
  93. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_14.webp +0 -0
  94. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_15.webp +0 -0
  95. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_16.webp +0 -0
  96. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_17.webp +0 -0
  97. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_18.webp +0 -0
  98. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_19.webp +0 -0
  99. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_20.webp +0 -0
  100. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_21.webp +0 -0
  101. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_22.webp +0 -0
  102. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_23.webp +0 -0
  103. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_24.webp +0 -0
  104. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_25.webp +0 -0
  105. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_26.webp +0 -0
  106. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_27.webp +0 -0
  107. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_28.webp +0 -0
  108. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_29.webp +0 -0
  109. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_30.webp +0 -0
  110. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_31.webp +0 -0
  111. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_32.webp +0 -0
  112. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_33.webp +0 -0
  113. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_34.webp +0 -0
  114. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_35.webp +0 -0
  115. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_36.webp +0 -0
  116. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_37.webp +0 -0
  117. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_38.webp +0 -0
  118. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_39.webp +0 -0
  119. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_40.webp +0 -0
  120. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_41.webp +0 -0
  121. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_42.webp +0 -0
  122. /package/{public-landing → modules/public-landing/public}/assets/anim/idle/frame_43.webp +0 -0
  123. /package/{public-landing → modules/public-landing/public}/assets/verro-medallion.png +0 -0
  124. /package/{public-landing → modules/public-landing/public}/assets/world/og-card.jpg +0 -0
  125. /package/{public-landing → modules/public-landing/public}/assets/world/r2-bongos-chrome-a-cut.webp +0 -0
  126. /package/{public-landing → modules/public-landing/public}/assets/world/r2-bongos-chrome-a-cut.webp.json +0 -0
  127. /package/{public-landing → modules/public-landing/public}/assets/world/r2-bongos-chrome-b.jpg +0 -0
  128. /package/{public-landing → modules/public-landing/public}/assets/world/r2-bongos-chrome-b.jpg.json +0 -0
  129. /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  130. /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blob-one-cut.webp.json +0 -0
  131. /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blob-one.jpg +0 -0
  132. /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blob-one.jpg.json +0 -0
  133. /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blobs-set-cut.webp +0 -0
  134. /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blobs-set-cut.webp.json +0 -0
  135. /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blobs-set.jpg +0 -0
  136. /package/{public-landing → modules/public-landing/public}/assets/world/r2-chrome-blobs-set.jpg.json +0 -0
  137. /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-bead-cut.webp +0 -0
  138. /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-bead-cut.webp.json +0 -0
  139. /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-blob-cut.webp +0 -0
  140. /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-blob-cut.webp.json +0 -0
  141. /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-pair-cut.webp +0 -0
  142. /package/{public-landing → modules/public-landing/public}/assets/world/r3-frost-pair-cut.webp.json +0 -0
  143. /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-bead-cut.webp +0 -0
  144. /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-bead-cut.webp.json +0 -0
  145. /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-blob-cut.webp +0 -0
  146. /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-blob-cut.webp.json +0 -0
  147. /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-pair-cut.webp +0 -0
  148. /package/{public-landing → modules/public-landing/public}/assets/world/r3-gold-pair-cut.webp.json +0 -0
  149. /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-bead-cut.webp +0 -0
  150. /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-bead-cut.webp.json +0 -0
  151. /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-blob-cut.webp +0 -0
  152. /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-blob-cut.webp.json +0 -0
  153. /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-pair-cut.webp +0 -0
  154. /package/{public-landing → modules/public-landing/public}/assets/world/r3-obsidian-pair-cut.webp.json +0 -0
  155. /package/{public-landing → modules/public-landing/public}/assets/world/r3-space-nebula.jpg +0 -0
  156. /package/{public-landing → modules/public-landing/public}/assets/world/r3-space-nebula.jpg.json +0 -0
  157. /package/{public-landing → modules/public-landing/public}/assets/world.css +0 -0
  158. /package/{public-landing → modules/public-landing/public}/buddha.png +0 -0
  159. /package/{public-landing → modules/public-landing/public}/contact.html +0 -0
  160. /package/{public-landing → modules/public-landing/public}/index.html +0 -0
  161. /package/{public-landing → modules/public-landing/public}/index.probes.json +0 -0
  162. /package/{public-landing → modules/public-landing/public}/index.states.json +0 -0
  163. /package/{public-landing → modules/public-landing/public}/privacy.html +0 -0
  164. /package/{public-landing → modules/public-landing/public}/projects-empty.states.json +0 -0
  165. /package/{public-landing → modules/public-landing/public}/terms.html +0 -0
  166. /package/{public-landing → modules/public-landing/public}/world/assets/clouds/cloud-island-large.png +0 -0
  167. /package/{public-landing → modules/public-landing/public}/world/assets/clouds/cloud-puff.png +0 -0
  168. /package/{public-landing → modules/public-landing/public}/world/assets/creatures/creature-whale-cargo.png +0 -0
  169. /package/{public-landing → modules/public-landing/public}/world/assets/creatures/creature-whale-glide.png +0 -0
  170. /package/{public-landing → modules/public-landing/public}/world/assets/plates/plate-clouds-near.png +0 -0
  171. /package/{public-landing → modules/public-landing/public}/world/assets/plates/scene-establishing.png +0 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "kind": "card-band",
3
+ "model": "soul_location (Higgsfield) — nano_banana_pro was asked first; its queue had not started a job after 25 minutes, so the round moved to the model that answered",
4
+ "job_id": "debaaba3-6094-4670-9b08-c578fc6e8bb9",
5
+ "aspect_ratio": "5:2",
6
+ "source_aspect_ratio": "21:9",
7
+ "generated": "2026-08-30T17:09Z",
8
+ "round": 4,
9
+ "task": 1003352,
10
+ "prompt": "Ultra-wide deep-field astronomical photograph on a pure black ground: many very small, very distant galaxies and faint stars scattered sparsely across an almost entirely black frame, a few with the faintest cool blue or warm amber tint, no bright object anywhere, extremely quiet and even. No planets, no moons, no large nebula, no ground plane, no horizon, no lens flare, no text, no watermark, no logo, no border. Pure black dominates the frame.",
11
+ "source": ".impeccable/mocks/world-assets/r4-card-deepfield-src.jpg",
12
+ "encoded": "ffmpeg crop=2560:1024:0:24 (21:9 → 5:2, centred), JPEG -q:v 4",
13
+ "use": "the poster card's backdrop on /projects, chosen by an owner as card_backdrop=\"deepfield\": a deep field of very distant galaxies on near-black; the quietest of the four. Dark only (ADR 0204); composed so the left third and the lower corners stay near-black, where the card puts its disc, its name and its two figure callouts."
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "kind": "card-band",
3
+ "model": "soul_location (Higgsfield) — nano_banana_pro was asked first; its queue had not started a job after 25 minutes, so the round moved to the model that answered",
4
+ "job_id": "7ec35328-3943-4765-8dd7-8392fb8c48c3",
5
+ "aspect_ratio": "5:2",
6
+ "source_aspect_ratio": "21:9",
7
+ "generated": "2026-08-30T17:09Z",
8
+ "round": 4,
9
+ "task": 1003352,
10
+ "prompt": "Ultra-wide cinematic deep-space photograph on a pure black ground: soft interstellar dust lanes in warm bronze and dark amber sweeping diagonally across the centre-right, very low contrast, a haze rather than a shape, a scatter of tiny faint stars. The left third and both lower corners stay almost pure black and empty. No planets, no moons, no nebula pillars, no ground plane, no horizon, no lens flare, no text, no watermark, no logo, no border. Pure black dominates the frame.",
11
+ "source": ".impeccable/mocks/world-assets/r4-card-dust-src.jpg",
12
+ "encoded": "ffmpeg crop=2560:1024:0:24 (21:9 → 5:2, centred), eq=gamma=1.20, JPEG -q:v 4",
13
+ "use": "the poster card's backdrop on /projects, chosen by an owner as card_backdrop=\"dust\": warm bronze dust lanes, very low contrast; a haze rather than a shape. Dark only (ADR 0204); composed so the left third and the lower corners stay near-black, where the card puts its disc, its name and its two figure callouts."
14
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "kind": "card-band",
3
+ "model": "soul_location (Higgsfield) — nano_banana_pro was asked first; its queue had not started a job after 25 minutes, so the round moved to the model that answered",
4
+ "job_id": "35ad2c18-aaa3-413c-9fd8-958e5017d77c",
5
+ "aspect_ratio": "5:2",
6
+ "source_aspect_ratio": "21:9",
7
+ "generated": "2026-08-30T17:09Z",
8
+ "round": 4,
9
+ "task": 1003352,
10
+ "prompt": "Ultra-wide cinematic deep-space photograph on a pure black ground: a faint nebula wash in muted violet and deep blue with a soft bronze warmth drifting through the centre-right of the frame, very sparse pin-sharp stars of varied brightness, gentle depth. The left third and both lower corners stay almost pure black and empty. No planets, no moons, no ground plane, no horizon, no lens flare, no text, no watermark, no logo, no border. Pure black dominates the frame.",
11
+ "source": ".impeccable/mocks/world-assets/r4-card-nebula-src.jpg",
12
+ "encoded": "ffmpeg crop=2560:1024:0:24 (21:9 → 5:2, centred), eq=gamma=1.75:saturation=1.30, JPEG -q:v 4 — the render was too dark to read at the card's 560px; a gamma lift raises the wash without crushing the black the disc and the name sit on (contrast would have)",
13
+ "use": "the poster card's backdrop on /projects, chosen by an owner as card_backdrop=\"nebula\": a violet-and-bronze nebula wash, sparse stars — the field's own language in the card's shape. Dark only (ADR 0204); composed so the left third and the lower corners stay near-black, where the card puts its disc, its name and its two figure callouts."
14
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "_": "The all-null floor card (task 1003301) on the hub's own catalog (task 1003345): the same page, the same stub, the catalog aliased to featured-hub.json — the landing's three plus Tidewater, a null row nobody owns. Demo, the landing catalog's floor row, is owned by the signed-in stub, and an owned orb's plain click opens its manage view, never its card. Rendered by name: node modules/ui-design/kit/render.js --states public-landing/projects-floor.states.json",
2
+ "_": "The all-null floor card (task 1003301) on the hub's own catalog (task 1003345): the same page, the same stub, the catalog aliased to featured-hub.json — the landing's three plus Tidewater, a null row nobody owns. Demo, the landing catalog's floor row, is owned by the signed-in stub, and an owned orb's plain click opens its manage view, never its card. Since task 1003352 it also carries the card BACKDROP state: featured-hub gives Off the Boats a card_backdrop, so the owner's plate can be seen standing in front of the project's own picture. Rendered by name: node modules/ui-design/kit/render.js --states public-landing/projects-floor.states.json",
3
3
  "page": "/projects",
4
4
  "surface": "public-landing",
5
5
  "modes": [
@@ -58,6 +58,33 @@
58
58
  "#cardListed"
59
59
  ]
60
60
  }
61
+ },
62
+ "card-backdrop": {
63
+ "auth": true,
64
+ "actions": [
65
+ [
66
+ "dispatchClick",
67
+ "#orbField a.worldOrb[data-origin=\"https://amazonprimea.com\"]"
68
+ ],
69
+ [
70
+ "wait",
71
+ 600
72
+ ]
73
+ ],
74
+ "expect": {
75
+ "dialog": true,
76
+ "visible": [
77
+ "#cardArt",
78
+ "#cardOpen"
79
+ ],
80
+ "attr": [
81
+ [
82
+ "#cardArt",
83
+ "src",
84
+ "/assets/world/r4-card-nebula.jpg"
85
+ ]
86
+ ]
87
+ }
61
88
  }
62
89
  }
63
90
  }
@@ -769,6 +769,18 @@ summary{min-height:24px;padding:3px 0;}
769
769
  </details>
770
770
  </div>
771
771
 
772
+ <!-- its card (task 1003352): the poster card's backdrop, a plate the
773
+ platform drew. Beside the planet, on the same settings channel, and
774
+ like it: nothing is pushed to the project and nothing restarts. -->
775
+ <div class="card mSec" id="mCard">
776
+ <h2>Its card</h2>
777
+ <div id="mCardPick"><p class="finePrint">Reading the project…</p></div>
778
+ <details class="finePrint" id="mCardWhat">
779
+ <summary>What the choice changes</summary>
780
+ <p>The backdrop behind this project’s name on its card — the panel that opens when someone picks it out of the map. Four plates the platform drew, or the project’s own picture if you have given it one. Saving here changes nothing on the project itself: no restart, nothing to apply.</p>
781
+ </details>
782
+ </div>
783
+
772
784
  <!-- live since task 1003042: the teardown control, for a project in ANY
773
785
  state that still holds something. The server used to refuse a
774
786
  'requested' or an 'error' project outright — and force-teardown reused
@@ -1820,7 +1832,7 @@ summary{min-height:24px;padding:3px 0;}
1820
1832
  var href = p.isSelf ? '/' : safeHttpUrl(p.origin);
1821
1833
  /* the art: catalog text an Archon typed, scheme-checked server-side and
1822
1834
  again here before it becomes a src */
1823
- cardArt(p.isSelf ? '' : safeHttpUrl(p.art_url));
1835
+ cardArt(p.isSelf ? '' : (backdropPlate(p.card_backdrop) || safeHttpUrl(p.art_url)));
1824
1836
  $('cardDot').style.setProperty('--acc', p.isSelf ? 'var(--accent)' : accentOf(p));
1825
1837
  $('cardName').textContent = p.name || '';
1826
1838
  $('cardTag').textContent = p.tagline || '';
@@ -4851,6 +4863,7 @@ summary{min-height:24px;padding:3px 0;}
4851
4863
  /* its planet (task 1003347) reads off the row — fetch-free too; a pick in
4852
4864
  progress survives the tick because only a different project re-renders it */
4853
4865
  renderPick(inst);
4866
+ renderCardPick(inst);
4854
4867
  /* the page names its project — the heading holds #mName; and in the tab */
4855
4868
  document.title = (inst.slug ? inst.slug + ' — ' : '') + BASE_TITLE;
4856
4869
  /* the card's own story, on the project's own page (task 1003232): the
@@ -5432,6 +5445,20 @@ summary{min-height:24px;padding:3px 0;}
5432
5445
  var PICK_STYLES = [['tint', 'tinted'], ['wash', 'washed'], ['plain', 'plain']];
5433
5446
  var PICK_ACCENTS = [['sea', 'sea'], ['terracotta', 'terracotta'], ['gold-soft', 'soft gold'], ['sun-lit', 'sunlit'], ['accent', 'the accent']];
5434
5447
  var PICK_FIELDS = { mPickShape: 'shape', mPickMaterial: 'material', mPickStyle: 'style', mPickAccent: 'accent' };
5448
+ /* ── the card's backdrop (task 1003352): the same KIND of key as the planet —
5449
+ a closed list of platform-drawn plates, saved on the same settings channel,
5450
+ pushed to no instance and restarting nothing. '' is "its own picture": the
5451
+ card falls back to the project's art_url, which is all it read before. The
5452
+ plates are the world's own material (DESIGN.md, round 4), dark only. ── */
5453
+ var CARD_BACKDROPS = [['', 'its own picture'], ['nebula', 'nebula'], ['dust', 'dust'], ['deepfield', 'deep field'], ['chrome', 'chrome']];
5454
+ var CARD_PLATES = { nebula: 'r4-card-nebula.jpg', dust: 'r4-card-dust.jpg', deepfield: 'r4-card-deepfield.jpg', chrome: 'r4-card-chrome.jpg' };
5455
+ var CARD_WORDS = { nebula: 'a violet and bronze nebula wash', dust: 'warm bronze dust lanes',
5456
+ deepfield: 'a deep field of distant galaxies', chrome: 'a sweep of liquid chrome' };
5457
+ /* a key → the plate's path, or '' for anything this platform does not draw
5458
+ (an unknown key from an older row must never become a broken src) */
5459
+ function backdropPlate(key) {
5460
+ return CARD_PLATES[key] ? '/assets/world/' + CARD_PLATES[key] : '';
5461
+ }
5435
5462
  var mPick = null; /* the pending choice: { shape, material, style, accent } */
5436
5463
  var mPickFor = null; /* which project it belongs to — a different one starts afresh */
5437
5464
  var mPickTouched = false; /* the owner changed something since the seed — a late catalog must not undo it */
@@ -5540,6 +5567,79 @@ summary{min-height:24px;padding:3px 0;}
5540
5567
  });
5541
5568
  }
5542
5569
 
5570
+ /* ── its card (task 1003352; the owner's ask on the planet round: "this could
5571
+ also house project card backdrop customization"). Its own section BESIDE the
5572
+ planet rather than inside it, so each heading stays true — one is how the
5573
+ project is drawn everywhere, the other is one surface's backdrop. Same
5574
+ mechanics as the planet, key for key: reads off the row, saves on the
5575
+ settings PATCH, and the reply is a receipt the card repaints from. ── */
5576
+ var mCard = null; /* the pending choice: the backdrop key, or '' */
5577
+ var mCardFor = null; /* which project it belongs to */
5578
+ function renderCardPick(inst) {
5579
+ if (mCard === null || mCardFor !== inst.id) { mCard = inst.card_backdrop || ''; mCardFor = inst.id; }
5580
+ var plate = backdropPlate(mCard);
5581
+ $('mCardPick').innerHTML =
5582
+ '<div class="pick">' +
5583
+ '<div class="pickPreview"><span class="cardBand"' +
5584
+ (plate ? ' style="background-image:url(' + esc(plate) + ')"' : ' data-own') + ' aria-hidden="true"></span>' +
5585
+ /* the line speaks for the ROW, so a pick that is ahead of the row says
5586
+ so — the preview above it is already showing the plate */
5587
+ '<p class="pickWords">' + esc(CARD_WORDS[mCard] || 'the project’s own picture, when it has one') +
5588
+ '<small>' + (mCard !== (inst.card_backdrop || '') ? 'not saved yet'
5589
+ : inst.card_backdrop ? 'your choice' : 'nothing chosen') + '</small></p></div>' +
5590
+ '<div class="pickBands">' + pickBand('Backdrop', 'mCardBackdrop', CARD_BACKDROPS, mCard) + '</div>' +
5591
+ '</div>' +
5592
+ '<div class="actions"><button type="button" class="btn small" data-card-save>Save its card</button></div>' +
5593
+ '<div class="projMsg" id="mCardMsg" role="status" aria-live="polite"></div>';
5594
+ }
5595
+ $('mCardPick').addEventListener('change', function (ev) {
5596
+ var t = ev.target;
5597
+ if (!t || t.type !== 'radio' || t.name !== 'mCardBackdrop' || !manageInst) return;
5598
+ mCard = t.value;
5599
+ renderCardPick(manageInst); /* the preview follows the pick, keeping the cell's focus */
5600
+ var again = document.querySelector('#mCardPick input[value="' + t.value + '"]');
5601
+ if (again) again.focus({ preventScroll: true });
5602
+ });
5603
+ $('mCardPick').addEventListener('click', function (ev) {
5604
+ var t = ev.target.closest && ev.target.closest('button');
5605
+ if (t && t.hasAttribute('data-card-save')) doCardSave();
5606
+ });
5607
+ function doCardSave() {
5608
+ if (!manageInst || mCard === null) return;
5609
+ var box = $('mCardMsg');
5610
+ document.querySelectorAll('#mCardPick .actions button').forEach(function (b) { b.disabled = true; });
5611
+ box.className = 'projMsg'; box.textContent = 'Saving…';
5612
+ fetch(API + '/provisioning/instances/' + encodeURIComponent(manageId) + '/settings', {
5613
+ method: 'PATCH', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
5614
+ body: JSON.stringify({ card_backdrop: mCard || null })
5615
+ }).then(function (res) {
5616
+ return jsonOrEmpty(res).then(function (data) {
5617
+ if (res.ok) {
5618
+ /* the row now holds the choice; the card reads it from there */
5619
+ manageInst.card_backdrop = (data.planet || {}).card_backdrop || null;
5620
+ var row = byId[manageInst.id];
5621
+ if (row) row.card_backdrop = manageInst.card_backdrop;
5622
+ mCard = null;
5623
+ renderCardPick(manageInst);
5624
+ var done = $('mCardMsg');
5625
+ done.className = 'projMsg ok';
5626
+ done.textContent = (data.push && data.push.message) || 'Saved.';
5627
+ loadCatalog();
5628
+ return;
5629
+ }
5630
+ var e = apiError(data, res.status);
5631
+ if (res.status === 401) { box.className = 'projMsg err'; box.innerHTML = signInAgain(); return; }
5632
+ box.className = 'projMsg err';
5633
+ box.textContent = e.message || 'The choice could not be saved — try again in a moment.';
5634
+ });
5635
+ }).catch(function () {
5636
+ box.className = 'projMsg err';
5637
+ box.textContent = 'Couldn’t reach the platform — check your connection and try again.';
5638
+ }).then(function () {
5639
+ document.querySelectorAll('#mCardPick .actions button').forEach(function (b) { b.disabled = false; });
5640
+ });
5641
+ }
5642
+
5543
5643
  /* ── how people join (task 1003044): the joinability toggle — the visibility
5544
5644
  section's shape, key for key: the CHOICE from the platform's settings row,
5545
5645
  what the project RUNS from its own manifest (brand.project.joinability),
@@ -131,6 +131,12 @@
131
131
  { "name": "manage flows: try again from the snagged project's own page re-files it too", "auth": true, "width": 1440, "url": "/projects?view=manage&id=103",
132
132
  "steps": [["wait",900],["click","[data-m-retry]"],["wait",900]],
133
133
  "expect": {"text":[["#mMsg","Queued. The platform picks it up from here."]],"hasClass":[["#mMsg","ok",true]],"hidden":["#mAddress"]} },
134
+ { "name": "manage flows: its card (task 1003352) — pick the nebula plate; save; the receipt lands and the preview takes the plate", "auth": true, "width": 1440, "url": "/projects?view=manage&id=101",
135
+ "steps": [["wait",900],["check","#mCardPick input[name=\"mCardBackdrop\"][value=\"nebula\"]"],["wait",150],["click","[data-card-save]"],["wait",700]],
136
+ "expect": {"text":[["#mCardMsg","Saved. The sky, your list and the card draw it this way from here; nothing on your project restarts for it."]],"hasClass":[["#mCardMsg","ok",true]],"count":[["#mCardPick .cardBand[data-own]",0]]} },
137
+ { "name": "manage flows: its card — handing it back to the platform restores the project's own picture", "continue": true,
138
+ "steps": [["check","#mCardPick input[name=\"mCardBackdrop\"][value=\"\"]"],["wait",150],["click","[data-card-save]"],["wait",700]],
139
+ "expect": {"hasClass":[["#mCardMsg","ok",true]],"count":[["#mCardPick .cardBand[data-own]",1]]} },
134
140
  { "name": "manage flows: how people join — pick a cell, save, the receipt lands under the choice", "auth": true, "width": 1440, "url": "/projects?view=manage&id=101",
135
141
  "steps": [["wait",900],["check","input[name=\"mJoinPick\"][value=\"apply\"]"],["click","[data-join-save]"],["wait",600]],
136
142
  "expect": {"text":[["#mJoinMsg","Saved. The project restarts to apply it."],["#mName","demo"]],"checked":"input[name=\"mJoinPick\"][value=\"apply\"]","visible":["#mOrb .worldOrb","#mFacts .ledger .k"]} },
@@ -21,6 +21,7 @@
21
21
  "in-manage": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mMain", "#mChip .chip", "#mOrb .worldOrb", "#mFacts .ledger .k", "#mActs", "#mAddress"], "hidden": ["#qWrap", "#mGate", "#mMissing", "#mErr"], "text": [["#mName", "demo"], ["#mTitle", "demo"]] } },
22
22
  "in-manage-address": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mAddress", "#mAddress h2", "#mDom .plain", "#mDomName", "#mDom [data-m-attach]", "#mDomWhat summary"], "count": [["#mDom .attach input[type=text]", 1], ["#mDom .attach button", 1]] } },
23
23
  "in-manage-planet": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mPlanetPick", "#mPick .pick .pickPreview .worldOrb .disc", "#mPick [data-pick-save]", "#mPick [data-pick-reset]", "#mPickWhat summary"], "count": [["#mPick .pickCells[role=\"radiogroup\"]", 3], ["#mPick .pickCell", 10], ["#mPick .pickCell.on", 3], ["#mPick .pickSw", 5], ["#mPick .pickSw.on", 1]], "attr": [["#mPick .pickPreview .worldOrb", "data-template", "pair-obsidian-tint"], ["#mOrb .worldOrb", "data-template", "pair-obsidian-tint"]], "checked": "#mPick input[name=\"mPickAccent\"][value=\"sea\"]", "text": [["#mPlanet .v small", "your choice."]] } },
24
+ "in-manage-card": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mCard", "#mCardPick .pick .pickPreview .cardBand", "#mCardPick [data-card-save]", "#mCardWhat summary"], "count": [["#mCardPick .pickCells[role=\"radiogroup\"]", 1], ["#mCardPick .pickCell", 5], ["#mCardPick .pickCell.on", 1]], "checked": "#mCardPick input[name=\"mCardBackdrop\"][value=\"\"]", "attr": [["#mCardPick .cardBand", "data-own", ""]] } },
24
25
  "in-manage-snag": { "auth": true, "url": "/projects?view=manage&id=103", "expect": { "visible": ["#mMain", "#mErr", "#mActs"], "hidden": ["#mAddress"], "text": [["#mName", "tidepool"]] } },
25
26
  "in-manage-gone": { "auth": true, "url": "/projects?view=manage&id=104", "expect": { "visible": ["#mMain", "#mActs"], "hidden": ["#mErr", "#mAddress"], "text": [["#mName", "old-lantern"]] } },
26
27
  "in-manage-missing": { "auth": true, "url": "/projects?view=manage&id=999", "expect": { "visible": ["#mMissing"], "hidden": ["#mMain", "#mGate"] } },
@@ -1 +1 @@
1
- {"_":"The hub's catalog (task 1003345): the landing's featured.json plus Tidewater, an all-null row nobody owns — the card-floor state opens ITS card, because Demo is owned by the signed-in stub (provisioning-instances.json) and an owned orb's plain click opens the manage view, never the card. Served under the name \"featured\" by projects.states.json's stub.fixtures alias; the landing keeps featured.json.","projects":[{"origin":"https://amazonprimea.com","name":"Off the Boats","tagline":"A singular shop, and a broader videogame in a persistent ancient Greek world that collides with modern day commerce & products.","art_url":"/assets/world/r2-chrome-blob-one.jpg","type":"game","template":"bead-wash","planet_template":null,"planet_accent":null,"created_at":"2026-07-15T22:12:57.732Z","builders":11,"tags":["art-pipeline","character-anim","demo","game"],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":["https://avatars.githubusercontent.com/u/283020391?v=4","https://avatars.githubusercontent.com/u/64553469?v=4","https://avatars.githubusercontent.com/u/290184380?v=4","https://avatars.githubusercontent.com/u/295990394?v=4","https://avatars.githubusercontent.com/u/35337514?v=4"],"shipped_30d":0,"shipped_30d_capped":false,"hall_origin":"https://builders.amazonprimea.com"},{"origin":"https://demo.cloudbongos.com","name":"Demo","tagline":"A live demo instance on Cloud Bongos.","art_url":null,"type":null,"template":"pair-gold-wash","planet_template":"pair-obsidian-tint","planet_accent":"sea","created_at":"2026-07-14T19:16:38.160Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null},{"origin":"https://emersonian-circles.cloudbongos.com","name":"Emersonian Circles","tagline":"An independent project built on Cloud Bongos.","art_url":null,"type":null,"template":null,"planet_template":null,"planet_accent":null,"created_at":"2026-07-25T02:21:55.641Z","builders":1,"tags":[],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":[],"shipped_30d":30,"shipped_30d_capped":true,"hall_origin":null},{"origin":"https://tidewater.cloudbongos.com","name":"Tidewater","tagline":"A project that has declared nothing yet.","art_url":null,"type":null,"template":null,"planet_template":null,"planet_accent":null,"created_at":"2026-07-20T08:00:00.000Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null}]}
1
+ {"_":"The hub's catalog (task 1003345): the landing's featured.json plus Tidewater, an all-null row nobody owns — the card-floor state opens ITS card, because Demo is owned by the signed-in stub (provisioning-instances.json) and an owned orb's plain click opens the manage view, never the card. Served under the name \"featured\" by projects.states.json's stub.fixtures alias; the landing keeps featured.json.","projects":[{"origin":"https://amazonprimea.com","name":"Off the Boats","tagline":"A singular shop, and a broader videogame in a persistent ancient Greek world that collides with modern day commerce & products.","art_url":"/assets/world/r2-chrome-blob-one.jpg","type":"game","template":"bead-wash","planet_template":null,"planet_accent":null,"card_backdrop":"nebula","created_at":"2026-07-15T22:12:57.732Z","builders":11,"tags":["art-pipeline","character-anim","demo","game"],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":["https://avatars.githubusercontent.com/u/283020391?v=4","https://avatars.githubusercontent.com/u/64553469?v=4","https://avatars.githubusercontent.com/u/290184380?v=4","https://avatars.githubusercontent.com/u/295990394?v=4","https://avatars.githubusercontent.com/u/35337514?v=4"],"shipped_30d":0,"shipped_30d_capped":false,"hall_origin":"https://builders.amazonprimea.com"},{"origin":"https://demo.cloudbongos.com","name":"Demo","tagline":"A live demo instance on Cloud Bongos.","art_url":null,"type":null,"template":"pair-gold-wash","planet_template":"pair-obsidian-tint","planet_accent":"sea","created_at":"2026-07-14T19:16:38.160Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null},{"origin":"https://emersonian-circles.cloudbongos.com","name":"Emersonian Circles","tagline":"An independent project built on Cloud Bongos.","art_url":null,"type":null,"template":null,"planet_template":null,"planet_accent":null,"created_at":"2026-07-25T02:21:55.641Z","builders":1,"tags":[],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":[],"shipped_30d":30,"shipped_30d_capped":true,"hall_origin":null},{"origin":"https://tidewater.cloudbongos.com","name":"Tidewater","tagline":"A project that has declared nothing yet.","art_url":null,"type":null,"template":null,"planet_template":null,"planet_accent":null,"created_at":"2026-07-20T08:00:00.000Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null}]}
@@ -53,7 +53,7 @@ function normalizePageArg(page) {
53
53
  if (typeof page === 'string' && /^[A-Za-z]:[\\/]/.test(page) && /[\\/]$/.test(page)) return '/';
54
54
  return page;
55
55
  }
56
- // A surface is a directory of the checkout: 'public-landing', 'modules/<key>/public',
56
+ // A surface is a directory of the checkout ('modules/<key>/public') or a module KEY
57
57
  // or a module KEY whose manifest declares a webSurfaces dir. Nothing is hardcoded.
58
58
  function resolveSurfaceDir(surface, root = REPO_ROOT) {
59
59
  if (!surface) return null;
@@ -234,7 +234,7 @@ function reducedMotionProblems({ running = [], frames = 0 } = {}) {
234
234
  }
235
235
 
236
236
  // ── the states file ───────────────────────────────────────────────────────────
237
- // { "page": "/projects", "surface"?: "public-landing", "stub"?: { prefix?, child? },
237
+ // { "page": "/projects", "surface"?: "public-landing" (a module key), "stub"?: { prefix?, child? },
238
238
  // "modes"?: ["dark" | "light", …] (the modes the page renders; default both — a page that pins
239
239
  // one mode declares it here), "modeQuery"?: "mode" | false, "ignoreRequests"?: [regex strings], "ignoreConsole"?: [...],
240
240
  // stub.fixtures?: { "<fixture name>": "<other fixture file>" } (serve another file under a name — an empty list, a
@@ -8,7 +8,7 @@
8
8
  // through two stubs this file starts itself (signed out, signed in), then a
9
9
  // reduced-motion pass per mode. Writes the shots + a JSON report; exits non-zero on any problem.
10
10
  //
11
- // node modules/ui-design/kit/render.js --page /projects (public-landing/projects.states.json)
11
+ // node modules/ui-design/kit/render.js --page /projects (surface defaults to the public-landing module key)
12
12
  // node modules/ui-design/kit/render.js --page /builders/ranks --surface hall-ui
13
13
  // node modules/ui-design/kit/render.js --states <file> (explicit file)
14
14
  // --modes dark,light --sizes 1440,390,320 --only a,b --out <dir>
@@ -10,9 +10,9 @@
10
10
  // fixtures/ so a page boots with real-shaped data and no database. Serves docs/
11
11
  // raw so a mock under docs/design/mocks/ renders with its relative links.
12
12
  //
13
- // node modules/ui-design/kit/serve.js -> http://127.0.0.1:3412/ (public-landing)
14
- // SURFACE=<dir or module key> a checkout dir ("public-landing", "modules/hall-ui/public") or a
15
- // module key whose manifest declares a webSurfaces dir
13
+ // node modules/ui-design/kit/serve.js -> http://127.0.0.1:3412/ (the public-landing module)
14
+ // SURFACE=<dir or module key> a checkout dir ("modules/hall-ui/public") or a module
15
+ // key ("public-landing") whose manifest declares a webSurfaces dir
16
16
  // PREFIX=/builders/ the URL prefix the surface's pages + assets are served under
17
17
  // STUB_REPO=<checkout> the checkout to serve (default: the one this file sits in)
18
18
  // STUB_PORT=<n> default 3412 (0 = any free port; the port is printed)
@@ -137,15 +137,40 @@ const ASKED = new Map();
137
137
  // the run — the page repaints from the row it re-reads, so the row must carry the choice; a
138
138
  // PATCH with only planet keys gets the route's no-restart receipt, a policy key the push
139
139
  const PLANETS = new Map();
140
- const PLANET_KEYS = ['planet_template', 'planet_accent'];
141
- const NO_PLANET = { planet_template: null, planet_accent: null };
140
+ const PLANET_KEYS = ['planet_template', 'planet_accent', 'card_backdrop'];
141
+ const NO_PLANET = { planet_template: null, planet_accent: null, card_backdrop: null };
142
142
  function readBody(req, cb) {
143
143
  let d = '';
144
144
  req.on('data', (c) => { d += c; });
145
145
  req.on('end', () => { let b = {}; try { b = JSON.parse(d || '{}'); } catch { b = {}; } cb(b); });
146
146
  }
147
+ // The real route validates STRICTLY against its vocabularies (an undeclared key
148
+ // or an off-vocabulary value is a 400). The stub mirrored none of that, so the
149
+ // probes stayed green on a body the server would have refused — which is how a
150
+ // missing card_backdrop entry in the route's schema shipped past a full probe
151
+ // run (task 1003352, grade round 2). The lists are copied on purpose: a kit
152
+ // module may not import core. They are small, closed, and pinned by a test.
153
+ const STUB_VOCAB = {
154
+ platform_visibility: ['public', 'gated'],
155
+ joinability: ['open', 'apply', 'invite_only'],
156
+ planet_template: null, // the template set is long; shape is checked, not membership
157
+ planet_accent: ['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent'],
158
+ card_backdrop: ['nebula', 'dust', 'deepfield', 'chrome'],
159
+ };
160
+ function settingsRejection(body) {
161
+ for (const [key, value] of Object.entries(body)) {
162
+ if (!(key in STUB_VOCAB)) return `unknown field: ${key}`;
163
+ if (value === null) continue; // null hands a pick back
164
+ if (typeof value !== 'string') return `${key} must be a string`;
165
+ const allowed = STUB_VOCAB[key];
166
+ if (allowed && !allowed.includes(value)) return `${key} is not one of: ${allowed.join(', ')}`;
167
+ }
168
+ return null;
169
+ }
147
170
  function settingsPatch(res, id, body) {
148
- if (PLANET_KEYS.some((k) => k in body)) PLANETS.set(String(id), { planet_template: body.planet_template || null, planet_accent: body.planet_accent || null });
171
+ const bad = settingsRejection(body);
172
+ if (bad) return json(res, 400, { error: { code: 'validation_failed', message: bad } });
173
+ if (PLANET_KEYS.some((k) => k in body)) PLANETS.set(String(id), Object.fromEntries(PLANET_KEYS.map((k) => [k, body[k] || null])));
149
174
  const policy = ['platform_visibility', 'joinability'].some((k) => k in body);
150
175
  const push = policy ? FLOW_RECEIPTS.settings.push
151
176
  : { queued: false, restart: false, message: 'Saved. The sky, your list and the card draw it this way from here; nothing on your project restarts for it.' };
@@ -154,7 +179,7 @@ function settingsPatch(res, id, body) {
154
179
  // the row carries its planet: a saved pick for the run, else the fixture's own (or null)
155
180
  function withPlanet(inst) {
156
181
  const saved = PLANETS.get(String(inst.id));
157
- return { ...inst, planet_template: inst.planet_template || null, planet_accent: inst.planet_accent || null, ...(saved || {}) };
182
+ return { ...inst, ...Object.fromEntries(PLANET_KEYS.map((k) => [k, inst[k] || null])), ...(saved || {}) };
158
183
  }
159
184
  function flowReceipt(rest, method) {
160
185
  if (method === 'POST' && rest === '/provisioning/instances') return FLOW_RECEIPTS.provision;
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.441",
3
+ "version": "1.19.443",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.441",
9
+ "version": "1.19.443",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.441",
3
+ "version": "1.19.443",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -54,12 +54,12 @@ const REPORT_REL = 'docs/copy-inventory.md';
54
54
  const SURFACES = [
55
55
  { id: 'builders-hall', kind: 'dir', at: 'modules/hall-ui/public', exts: ['.html', '.js'] },
56
56
  { id: 'status-dashboard', kind: 'dir', at: 'modules/status-ui/public', exts: ['.html', '.js'] },
57
- // THE LANDING SURFACE IS public-landing/, NOT src/platform-server.js. The first
57
+ // THE LANDING SURFACE IS modules/public-landing/public/, NOT src/platform-server.js. The first
58
58
  // cut pointed here at the server file, which only fs.readFileSync's these
59
59
  // templates — so its JS patterns matched a handful of incidental literals and the
60
60
  // page's ACTUAL copy (hero, meta/og text, every section) was silently absent
61
61
  // while the registry reported a number. Caught by the grader on task 1003112.
62
- { id: 'landing', kind: 'dir', at: 'public-landing', exts: ['.html'] },
62
+ { id: 'landing', kind: 'dir', at: 'modules/public-landing/public', exts: ['.html'] },
63
63
  ];
64
64
 
65
65
  // Attributes whose value is COPY a person reads (not wiring). The matchers are
@@ -70,7 +70,7 @@ const COPY_ATTR_RES = COPY_ATTRS.map((attr) => ({ attr, re: new RegExp(`\\b${att
70
70
  // What this tool knowingly does not see. Printed in the report — an inventory that
71
71
  // implies completeness it does not have is worse than a short one.
72
72
  const BLIND_SPOTS = [
73
- 'Landing-page transforms: `public-landing/*.html` is scanned as authored, but the server rewrites it before a visitor sees it — `{{token}}` placeholders are filled from the branding pack, and `<!--apex-only:start--> … <!--apex-only:end-->` sections are STRIPPED on a child instance (src/platform-server.js `landingPage()`). So a row here may be copy that this instance never renders, and a rendered sentence may differ from its authored form.',
73
+ 'Landing-page transforms: `modules/public-landing/public/*.html` is scanned as authored, but the server rewrites it before a visitor sees it — `{{token}}` placeholders are filled from the branding pack, and `<!--apex-only:start--> … <!--apex-only:end-->` sections are STRIPPED on a child instance (src/platform-server.js `landingPage()`). So a row here may be copy that this instance never renders, and a rendered sentence may differ from its authored form.',
74
74
  'Server-side messages: route failures (`res.fail` reasons), validation text and API error copy live in server JS and are not walked here. A person reads them, so widening to them is a deliberate later step, not an oversight to quietly fix.',
75
75
  'Skill and playbook prose: `.claude/skills/**/SKILL.md` is read by agents and by builders and is arguably the largest body of project voice. Out of scope on purpose — it is not UI, and mixing it in would drown the UI signal.',
76
76
  'Discord copy: outbound broadcast text is composed in `modules/discord/*.js` at send time. User-facing, but not part of any UI surface.',
@@ -141,11 +141,27 @@ const IDENTITY_SCAN_SKIP_DIRS = new Set(['node_modules', '.git', 'generated', 'i
141
141
  // silently hid ~330 provision.js lines from this gate). Re-exported for the tests.
142
142
  const { stripCommentsForIdentity } = require('./identity-strip');
143
143
 
144
+ // An APEX-ONLY region is content the server DELETES on every instance that is not
145
+ // the hub (`stripApexOnly`, src/platform-server.js — ADR 0152; the front door became
146
+ // a module in ADR 0218, which is what first brought these pages under this gate).
147
+ // This check exists so "a non-OTB instance renders ITS OWN brand"; text inside these
148
+ // sentinels never reaches a non-hub instance at all, so it cannot produce that
149
+ // failure. The hub's own landing legitimately NAMES the projects hosted on it, and
150
+ // that catalog is exactly what the sentinels wrap.
151
+ //
152
+ // BLANKED, not removed, so a violation's reported line number still points at the
153
+ // real line. Applied BEFORE the comment strip, which would otherwise eat the
154
+ // sentinels (they are HTML comments) and take the region's boundaries with them.
155
+ const APEX_ONLY_REGION = /<!--apex-only:start-->[\s\S]*?<!--apex-only:end-->/g;
156
+ function blankApexOnly(text) {
157
+ return String(text).replace(APEX_ONLY_REGION, (m) => m.replace(/[^\n]/g, ' '));
158
+ }
159
+
144
160
  // Pure: scan one file's comment-stripped source for a hardcoded host-brand literal.
145
161
  // Returns violation strings. Exported for the "actually catches a re-hardcode" test.
146
162
  function scanPromptIdentity(relFile, text, ext = path.extname(relFile)) {
147
163
  const violations = [];
148
- stripCommentsForIdentity(text, ext).split('\n').forEach((raw, i) => {
164
+ stripCommentsForIdentity(blankApexOnly(text), ext).split('\n').forEach((raw, i) => {
149
165
  if (!raw.trim()) return;
150
166
  for (const re of PROMPT_IDENTITY_LITERALS) {
151
167
  const m = raw.match(re);
@@ -318,4 +334,5 @@ module.exports = {
318
334
  collectIdentityFiles,
319
335
  scanPromptIdentity,
320
336
  stripCommentsForIdentity,
337
+ blankApexOnly, // ADR 0218 — apex-only regions never reach a child instance (tested)
321
338
  };
@@ -66,6 +66,7 @@ const PUBLISH_ALLOWLIST = [
66
66
  'modules/ui-design/', // the design contract + its adapters/CLIs + the /design playbook (task 1003324, ADR 0197). default:true, so it MUST be here: a released core without it would materialize skills (design, design-sync, figma-design-sync) whose CLIs and adapters do not exist.
67
67
  'modules/hall-ui/',
68
68
  'modules/status-ui/',
69
+ 'modules/public-landing/', // the front door (task 1003322, ADR 0218) — the repo-root public-landing/ entry this replaces was always here, and for the same reason: default:true, and it is the only thing an instance serves at its apex root. Omit it and a released core answers the game-less 404 tail on its own home page.
69
70
  'modules/dev-box/',
70
71
  'modules/discord/',
71
72
  'modules/provisioning/', // instance-hosting control plane — CORE like its sibling dev-box (same do-api/cf-tunnel toolchain, all core). Reclassified IN so the core CLI scripts/gds/provision.js resolves it from the core package post-split, not a MODULE_NOT_FOUND (task 2135). default:false — shipped ≠ enabled.
@@ -137,7 +138,6 @@ const PUBLISH_ALLOWLIST = [
137
138
  '.gitattributes',
138
139
  'package.json',
139
140
  'package-lock.json',
140
- 'public-landing/',
141
141
  // --- served-at-runtime app assets a standalone MUST carry (task 2078) ---
142
142
  // The generated typed API client is served at /lib/bongos-client.js as
143
143
  // window.BongosClient; shell.js + builders.js call it at the TOP of every hall
@@ -206,13 +206,15 @@ const MODULE_GLOBS = {
206
206
  // (moduleWebSurfaces → serve-internal). Its public read endpoints stay SHARED
207
207
  // core in routes/public.js (the hall + the off-box mirror consume them too).
208
208
  'status-ui': ['modules/status-ui/'],
209
- // The instance's public landing surface (index/projects/create), served by
210
- // platform-server.js from the repo-root public-landing/ dir — not yet a carved
211
- // module, so the glob lists where the code lives TODAY (the auto-tighten
212
- // contract above). Its own key, not provisioning's: the front door is broader
213
- // than instance hosting, and the web-surface precedent (hall-ui/status-ui) is
214
- // one key per surface (task 1002690, goal 1000059).
215
- 'public-landing': ['public-landing/'],
209
+ // Task 1003322 (ADR 0218): the front door graduated to modules/public-landing/ —
210
+ // public-landing/ moved to modules/public-landing/public/ and is now a loader-
211
+ // discovered web-surface module. Where hall-ui and status-ui name a Host PREFIX it
212
+ // declares apexRoot:true, since the apex is whatever host is left once the
213
+ // subdomains are taken; like the hall it keeps its DEDICATED serving block (in
214
+ // platform-server.js, re-pointed at the module dir). The KEY is unchanged from
215
+ // task 1002690 — its own key, not provisioning's, because the front door is broader
216
+ // than instance hosting and the web-surface precedent is one key per surface.
217
+ 'public-landing': ['modules/public-landing/'],
216
218
  // --- the design capability (task 1003324, ADR 0197): the design contract, its
217
219
  // adapters + sync CLIs + validator, the /design playbook, and the reserved homes
218
220
  // for the kit / the vendored UI skills / the style library. default-on,
@@ -211,7 +211,8 @@ function dropEmptyHrefLinks(html, values) {
211
211
  // '<' would otherwise break out of its attribute or close <title> early, and
212
212
  // the values now reach four times as many places as when this was written.
213
213
  // No token is ever emitted into a <script> or a CSS context (checked across
214
- // public-landing/, hall-ui/, status-ui/) — that would need different escaping.
214
+ // public-landing/, hall-ui/, status-ui/ — all under modules/ since task 1003322)
215
+ // — that would need different escaping.
215
216
  // Ampersand first, or the other replacements get double-escaped.
216
217
  function esc(s) {
217
218
  return String(s == null ? '' : s)
@@ -701,9 +702,20 @@ function mountInternalSurfaces(app) {
701
702
  // non-builders host. (The webSurfaces manifest declaration still makes the hall a
702
703
  // loader-DISCOVERED module — it feeds the scope map, the fitness keys, docs, and
703
704
  // window.__MODULES__; it is the dedicated serving below that reads the bytes.)
705
+ //
706
+ // The APEX-ROOT surface (modules/public-landing/, ADR 0218) is partitioned out the
707
+ // same way, on its own declaration rather than on a host: it carries no host prefix
708
+ // at all, because the apex is whatever is LEFT once the subdomains are taken. It is
709
+ // served by platform-server.js's dedicated landing block (the apex-only strip, the
710
+ // brand-token stamp, the ?v= content stamp, the five routes). Keeping it out of
711
+ // isModuleSurfaceHost is load-bearing, not tidiness — that predicate is what the
712
+ // /builders guards below use to step aside, and a surface matching every host would
713
+ // make all of them self-skip.
704
714
  const buildersHostReq = (host) => ({ headers: { host } });
705
715
  const allWebSurfaces = moduleWebSurfaces({ isEnabled: isModuleEnabled });
706
716
  const webSurfaces = allWebSurfaces
717
+ .filter((s) => typeof s.host === 'string') // APEX-ROOT surfaces carry no host (ADR 0218): they are not
718
+ // routed by prefix here, and must stay OUT of isModuleSurfaceHost
707
719
  .filter((s) => !isBuildersHost(buildersHostReq(s.host))) // hall served by the dedicated block, not here
708
720
  .map((s) => ({
709
721
  key: s.key,
package/src/module-api.js CHANGED
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
33
33
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
34
34
  // the entry to that file. Look for a version's history there, not here.
35
35
  // ---------------------------------------------------------------------------
36
- const CORE_VERSION = '1.19.441'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
36
+ const CORE_VERSION = '1.19.443'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
37
37
 
38
38
  // A namespaced logger so a module's log lines are attributable + consistent.
39
39
  // Usage: const log = api.logger('dev-box'); log.info('mounted');