@bongos/core 1.19.1073 → 1.19.1075

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 (377) hide show
  1. package/.bongos-core.json +404 -549
  2. package/.claude/skills/feedback/SKILL.md +2 -2
  3. package/bin/bongos.js +1 -3
  4. package/clients/bongos-client/README.md +1 -1
  5. package/clients/bongos-client/bongos-client.global.js +4 -60
  6. package/clients/bongos-client/index.cjs +4 -60
  7. package/clients/bongos-client/index.d.ts +5 -90
  8. package/clients/bongos-client/index.mjs +4 -60
  9. package/config/branding.neutral.json +1 -5
  10. package/config/modules.neutral.json +2 -3
  11. package/config/scheduled-routines.json +0 -2
  12. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
  13. package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
  14. package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
  15. package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
  16. package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +2 -0
  17. package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
  18. package/docs/adr/README.md +1 -0
  19. package/docs/api/openapi.json +175 -1665
  20. package/docs/api-reference.md +9 -41
  21. package/docs/architecture.md +16 -1
  22. package/docs/branding-contract.md +0 -3
  23. package/docs/copy-inventory.md +454 -479
  24. package/docs/copy-registry.json +746 -978
  25. package/docs/file-map.md +5 -34
  26. package/docs/module-api-changelog.md +4 -0
  27. package/docs/modules-contract.md +13 -1
  28. package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
  29. package/docs/onboarding/diagrams/README.md +3 -3
  30. package/docs/onboarding/diagrams/assertions.json +2 -22
  31. package/docs/onboarding/primer.md +9 -28
  32. package/docs/page-inventory.json +4 -1
  33. package/docs/page-readings.json +1032 -1077
  34. package/docs/recipes/render-pilot.md +29 -10
  35. package/migrations/core_257_module_store_registry.sql +87 -0
  36. package/migrations/core_258_drop_dev_box_tables.sql +55 -0
  37. package/modules/agents/module.json +1 -0
  38. package/modules/autonomy/module.json +1 -0
  39. package/modules/builder-settings/module.json +1 -0
  40. package/modules/builder-settings/render-prefs.js +4 -5
  41. package/modules/copy-desk/docx.js +78 -5
  42. package/modules/copy-desk/module.json +1 -0
  43. package/modules/copy-desk/routes/copy-desk.js +10 -10
  44. package/modules/copy-desk/tests/copy_docx.mjs +107 -1
  45. package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
  46. package/modules/discord/module.json +1 -0
  47. package/modules/discord/ship-broadcast.js +3 -3
  48. package/modules/economy/credits.js +2 -2
  49. package/modules/economy/module.json +1 -0
  50. package/modules/economy/routes/credits.js +1 -2
  51. package/modules/government/catalog.js +13 -17
  52. package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
  53. package/modules/government/module.json +1 -0
  54. package/modules/government/protected-surfaces.json +3 -11
  55. package/modules/government/resolver.js +3 -46
  56. package/modules/government/routes/government.js +0 -2
  57. package/modules/government/session-scopes.js +43 -83
  58. package/modules/government/session-scopes.json +8 -18
  59. package/modules/grading/module.json +1 -0
  60. package/modules/hall-ui/module.json +1 -0
  61. package/modules/hall-ui/public/board-lib.js +2 -2
  62. package/modules/hall-ui/public/builders.js +29 -40
  63. package/modules/hall-ui/public/collab.html +1 -1
  64. package/modules/hall-ui/public/collab.js +1 -1
  65. package/modules/hall-ui/public/diagrams.html +3 -3
  66. package/modules/hall-ui/public/dom-utils.js +1 -1
  67. package/modules/hall-ui/public/gate.js +2 -2
  68. package/modules/hall-ui/public/gate.states.json +1 -1
  69. package/modules/hall-ui/public/hall-render.js +43 -79
  70. package/modules/hall-ui/public/index.html +5 -1
  71. package/modules/hall-ui/public/modules.js +1 -0
  72. package/modules/hall-ui/public/oversight.css +5 -5
  73. package/modules/hall-ui/public/palette.js +0 -11
  74. package/modules/hall-ui/public/primer.js +1 -1
  75. package/modules/hall-ui/public/sessions.html +1 -1
  76. package/modules/hall-ui/public/settings-sessions.js +1 -1
  77. package/modules/hall-ui/public/settings.css +0 -1
  78. package/modules/hall-ui/public/settings.html +4 -33
  79. package/modules/hall-ui/public/settings.js +3 -33
  80. package/modules/hall-ui/public/settings.states.json +2 -2
  81. package/modules/hall-ui/public/studio.html +1 -1
  82. package/modules/hall-ui/public/style.css +10 -13
  83. package/modules/hall-ui/public/task.html +4 -1
  84. package/modules/hall-ui/public/task.js +36 -1
  85. package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
  86. package/modules/hall-ui/public/tweak-editor.css +16 -0
  87. package/modules/hall-ui/public/tweak-editor.html +26 -3
  88. package/modules/hall-ui/public/tweak-editor.js +32 -1
  89. package/modules/hall-ui/public/work.js +1 -1
  90. package/modules/hall-ui/records/tweak-editor.md +1 -0
  91. package/modules/ideas/module.json +1 -0
  92. package/modules/ideas/projection.js +1 -1
  93. package/modules/lifecycle/db-claim-reads.js +1 -58
  94. package/modules/lifecycle/db-ship.js +4 -9
  95. package/modules/lifecycle/db.js +0 -4
  96. package/modules/lifecycle/github-push.js +12 -14
  97. package/modules/lifecycle/kickoff-checklist.js +0 -4
  98. package/modules/lifecycle/lifecycle.js +0 -8
  99. package/modules/lifecycle/module.json +1 -0
  100. package/modules/lifecycle/publish-reconciler.js +10 -10
  101. package/modules/lifecycle/routes/tasks.js +13 -15
  102. package/modules/lifecycle/ship-card.js +1 -1
  103. package/modules/lifecycle/task-visuals.js +2 -2
  104. package/modules/memory/module.json +1 -0
  105. package/modules/memory/routes/memory.js +2 -2
  106. package/modules/npm-release/module.json +1 -0
  107. package/modules/onboarding/module.json +1 -0
  108. package/modules/onboarding/onboarding-state.js +17 -36
  109. package/modules/onboarding/routes/access-requests.js +3 -3
  110. package/modules/platform-identity/module.json +1 -0
  111. package/modules/platform-identity/platform-identity.js +13 -5
  112. package/modules/platform-identity/routes/sso.js +5 -4
  113. package/modules/platform-identity/tests/platform-identity.mjs +17 -3
  114. package/modules/provisioning/capacity.js +1 -1
  115. package/modules/provisioning/module.json +1 -0
  116. package/modules/provisioning/provisioning.js +6 -5
  117. package/modules/provisioning/routes/provisioning.js +4 -4
  118. package/modules/provisioning/starter-bundles.js +5 -25
  119. package/modules/public-landing/module.json +1 -0
  120. package/modules/public-landing/public/assets/cosmos.css +1 -1
  121. package/modules/public-landing/public/contact.html +1 -1
  122. package/modules/public-landing/public/privacy.html +1 -1
  123. package/modules/public-landing/public/projects.html +101 -32
  124. package/modules/public-landing/public/projects.probes.json +2 -2
  125. package/modules/public-landing/public/projects.states.json +4 -3
  126. package/modules/public-landing/public/terms.html +1 -1
  127. package/modules/security/module.json +1 -0
  128. package/modules/sessions/db.js +1 -1
  129. package/modules/sessions/module.json +1 -0
  130. package/modules/specialities/module.json +1 -0
  131. package/modules/status-ui/module.json +1 -0
  132. package/modules/ui-design/module.json +1 -0
  133. package/package-lock.json +2 -2
  134. package/package.json +1 -1
  135. package/release-notes.json +84 -0
  136. package/scripts/gds/artifact-format.js +203 -0
  137. package/scripts/gds/artifact-staleness.js +2 -2
  138. package/scripts/gds/audit-authorship.sh +1 -13
  139. package/scripts/gds/backfill-task-rewards.js +5 -5
  140. package/scripts/gds/claim.js +0 -15
  141. package/scripts/gds/claude-materialize.js +2 -2
  142. package/scripts/gds/cli-lib.js +15 -15
  143. package/scripts/gds/codemod-rename-src-gds.js +1 -1
  144. package/scripts/gds/context-pack.js +5 -7
  145. package/scripts/gds/diagram-facts.js +12 -23
  146. package/scripts/gds/do-api.js +13 -80
  147. package/scripts/gds/doc-cli-guard.js +2 -0
  148. package/scripts/gds/doctor.js +4 -4
  149. package/scripts/gds/feedback-latest.js +139 -9
  150. package/scripts/gds/fitness-checks-identity.js +2 -7
  151. package/scripts/gds/fitness-checks-packaging.js +4 -4
  152. package/scripts/gds/fitness-checks-write-validation.js +4 -0
  153. package/scripts/gds/fitness-ratchets.js +5 -5
  154. package/scripts/gds/fitness.js +10 -10
  155. package/scripts/gds/gen-api-client.js +4 -4
  156. package/scripts/gds/gen-api-docs.js +1 -1
  157. package/scripts/gds/gen-diagrams.js +49 -71
  158. package/scripts/gds/gen-repo-map.js +13 -14
  159. package/scripts/gds/gen-session-index.js +8 -8
  160. package/scripts/gds/http-api-client.js +12 -19
  161. package/scripts/gds/init.js +1 -1
  162. package/scripts/gds/leak-scan-allowlist.js +1 -1
  163. package/scripts/gds/lint-infra-exec.js +1 -1
  164. package/scripts/gds/local-preview-lib.js +8 -39
  165. package/scripts/gds/local-preview.js +2 -15
  166. package/scripts/gds/migration-namespace.js +3 -3
  167. package/scripts/gds/module-artifact.js +212 -0
  168. package/scripts/gds/module.js +95 -6
  169. package/scripts/gds/package-core.js +5 -116
  170. package/scripts/gds/plain-cards.js +1 -1
  171. package/scripts/gds/provision-config.js +2 -2
  172. package/scripts/gds/provision-net.js +2 -3
  173. package/scripts/gds/provision.js +9 -9
  174. package/scripts/gds/publish-manifest.js +1 -2
  175. package/scripts/gds/push-path-brief.js +10 -10
  176. package/scripts/gds/regen-instance-docs.js +1 -1
  177. package/scripts/gds/rename-history-check.js +1 -1
  178. package/scripts/gds/run-unit-tests.js +2 -16
  179. package/scripts/gds/sandbox-stage.js +39 -341
  180. package/scripts/gds/session-digest-build.js +2 -2
  181. package/scripts/gds/ship-deploy-target.js +13 -13
  182. package/scripts/gds/ship-flow.js +7 -7
  183. package/scripts/gds/ship-land.js +9 -10
  184. package/scripts/gds/ship-merge.js +4 -4
  185. package/scripts/gds/ship-preflight-steps.js +4 -4
  186. package/scripts/gds/ship-regen.js +15 -49
  187. package/scripts/gds/ship.js +13 -33
  188. package/scripts/gds/skill-preflight.js +9 -8
  189. package/scripts/gds/start.js +0 -15
  190. package/scripts/hall-preview/README.md +5 -5
  191. package/scripts/hall-preview/server.js +6 -6
  192. package/scripts/render-diagrams.sh +1 -1
  193. package/src/bongos/api-errors.js +1 -1
  194. package/src/bongos/api-prefix.js +4 -4
  195. package/src/bongos/auth-github.js +2 -3
  196. package/src/bongos/auth.js +51 -126
  197. package/src/bongos/db-kernel.js +2 -3
  198. package/src/bongos/db.js +5 -28
  199. package/src/bongos/module-scope-map.js +14 -29
  200. package/src/bongos/module-store.js +141 -0
  201. package/src/bongos/platform-visibility-gate.js +1 -1
  202. package/src/bongos/route-rank-check.js +3 -16
  203. package/src/bongos/routes/auth.js +0 -116
  204. package/src/bongos/routes/builders.js +5 -15
  205. package/src/bongos/routes/instance.js +8 -14
  206. package/src/bongos/routes/me.js +7 -23
  207. package/src/bongos/routes/modules.js +83 -0
  208. package/src/bongos/routes/security.js +3 -3
  209. package/src/bongos/routes.js +7 -2
  210. package/src/bongos/serve-internal.js +5 -6
  211. package/src/bongos-downloads.js +1 -6
  212. package/src/branding.js +2 -2
  213. package/src/build-info.js +4 -6
  214. package/src/instance-config.js +2 -2
  215. package/src/module-api.js +13 -6
  216. package/src/module-loader/catalog.js +3 -0
  217. package/src/module-loader/loader.js +4 -4
  218. package/src/module-loader/manifest-schema.js +16 -8
  219. package/src/module-seams.js +2 -3
  220. package/src/modules.js +42 -14
  221. package/tests/agents_spend_guard.mjs +1 -1
  222. package/tests/api_alias_caller_ratchet.mjs +1 -1
  223. package/tests/api_alias_exceptions.mjs +1 -1
  224. package/tests/api_client.mjs +1 -16
  225. package/tests/api_path_404.mjs +1 -1
  226. package/tests/auth_page_gate.mjs +10 -10
  227. package/tests/backfill_task_rewards.mjs +1 -1
  228. package/tests/canonical_profile_url.mjs +1 -3
  229. package/tests/claim_action.mjs +4 -1
  230. package/tests/claim_from_task_and_home.mjs +127 -0
  231. package/tests/{box_ship_permissions.mjs → cli_allowlist_permissions.mjs} +5 -5
  232. package/tests/cli_sessions.mjs +1 -1
  233. package/tests/consumer_layout_boot.mjs +2 -0
  234. package/tests/context_pack.mjs +13 -13
  235. package/tests/copy_desk_page_docx.mjs +33 -2
  236. package/tests/copy_inventory.mjs +7 -7
  237. package/tests/core_upgrade_runner.mjs +6 -6
  238. package/tests/credit_grant.mjs +2 -2
  239. package/tests/criterion_suggest.mjs +1 -1
  240. package/tests/deploy_divergence_line.mjs +11 -11
  241. package/tests/design_tokens_sync.mjs +3 -2
  242. package/tests/diagram_facts_offset.mjs +1 -1
  243. package/tests/do_api.mjs +106 -0
  244. package/tests/feedback_latest.mjs +122 -0
  245. package/tests/fitness.mjs +44 -42
  246. package/tests/fitness_ratchets.mjs +2 -2
  247. package/tests/gate_approvals.mjs +0 -1
  248. package/tests/github_push_land_proof.mjs +1 -1
  249. package/tests/go_live.mjs +3 -3
  250. package/tests/goal_advisory.mjs +3 -3
  251. package/tests/goal_suggest.mjs +3 -3
  252. package/tests/government_abuse_matrix.mjs +21 -19
  253. package/tests/government_ownership_scope.mjs +33 -35
  254. package/tests/government_parity.mjs +2 -2
  255. package/tests/government_protected_surfaces.mjs +56 -37
  256. package/tests/government_require_permission.mjs +3 -3
  257. package/tests/government_seed.mjs +37 -4
  258. package/tests/government_session_scope.mjs +92 -366
  259. package/tests/hall_audit.mjs +1 -1
  260. package/tests/hall_error_envelope.mjs +4 -0
  261. package/tests/hall_landing_boot.mjs +4 -1
  262. package/tests/hall_palette.mjs +1 -16
  263. package/tests/hall_record_world.mjs +6 -0
  264. package/tests/hall_tweak_editor.mjs +52 -1
  265. package/tests/helpers.mjs +1 -1
  266. package/tests/host_topology_skips.mjs +3 -4
  267. package/tests/html_comment_nesting.mjs +137 -0
  268. package/tests/infra_exec_paths.mjs +9 -10
  269. package/tests/init.mjs +4 -5
  270. package/tests/instance_manifest.mjs +18 -25
  271. package/tests/kickoff_checklist.mjs +1 -1
  272. package/tests/lib_sh_resolution.mjs +0 -221
  273. package/tests/local_preview.mjs +0 -12
  274. package/tests/migration_namespace.mjs +6 -6
  275. package/tests/module-scope-map.mjs +9 -9
  276. package/tests/module_api.mjs +2 -2
  277. package/tests/module_catalog.mjs +1 -1
  278. package/tests/module_cli.mjs +20 -20
  279. package/tests/module_contributions.mjs +6 -6
  280. package/tests/module_loader.mjs +6 -6
  281. package/tests/module_manifest.mjs +26 -17
  282. package/tests/module_route_rank.mjs +2 -2
  283. package/tests/module_store_publish.mjs +307 -0
  284. package/tests/module_store_publish_route.mjs +121 -0
  285. package/tests/module_store_registry_migration.mjs +70 -0
  286. package/tests/modules.mjs +56 -26
  287. package/tests/modules_route_wiring.mjs +1 -1
  288. package/tests/onboarding_route_signals.mjs +21 -31
  289. package/tests/onboarding_state.mjs +35 -49
  290. package/tests/paste_token_session_store.mjs +1 -1
  291. package/tests/permission_path.mjs +0 -3
  292. package/tests/platform_boot.mjs +1 -2
  293. package/tests/platform_visibility_gate.mjs +1 -1
  294. package/tests/profile_route.mjs +1 -6
  295. package/tests/project_modules_ui.mjs +17 -17
  296. package/tests/projects_hub.mjs +5 -5
  297. package/tests/projects_hub_app_step.mjs +138 -0
  298. package/tests/projects_hub_module_picker.mjs +77 -76
  299. package/tests/projects_hub_pre_uat.mjs +4 -5
  300. package/tests/provision.mjs +7 -7
  301. package/tests/provisioning_capacity.mjs +1 -1
  302. package/tests/provisioning_recommendations.mjs +2 -1
  303. package/tests/provisioning_starter_bundles.mjs +0 -22
  304. package/tests/publish_branch_route.mjs +11 -19
  305. package/tests/publish_manifest.mjs +2 -3
  306. package/tests/publish_reconciler.mjs +8 -8
  307. package/tests/push_path_brief.mjs +10 -9
  308. package/tests/rank_tier_single_source.mjs +1 -13
  309. package/tests/regrade_eligibility.mjs +1 -1
  310. package/tests/rename_history_restraint.mjs +1 -1
  311. package/tests/repo_map.mjs +12 -13
  312. package/tests/runner_drift.mjs +3 -21
  313. package/tests/sandbox_stage.mjs +57 -390
  314. package/tests/seam_wiring_guard.mjs +11 -11
  315. package/tests/search_isolation.mjs +3 -3
  316. package/tests/session_records.mjs +1 -1
  317. package/tests/session_rename_fallback.mjs +1 -22
  318. package/tests/session_start_freshness.mjs +3 -3
  319. package/tests/session_token_hash_db.mjs +1 -1
  320. package/tests/ship_card.mjs +1 -1
  321. package/tests/ship_ci_deploy.mjs +17 -17
  322. package/tests/ship_error_shape.mjs +1 -1
  323. package/tests/ship_premerge.mjs +1 -1
  324. package/tests/ship_resume.mjs +1 -1
  325. package/tests/skill_preflight.mjs +2 -2
  326. package/tests/skip_is_not_pass.mjs +1 -1
  327. package/tests/task_visual_slots.mjs +31 -0
  328. package/tests/terms_acceptance.mjs +2 -2
  329. package/tests/upgrade.mjs +4 -4
  330. package/tests/watch_sealed_floor.mjs +0 -1
  331. package/tests/wizard_draft_resume.mjs +2 -2
  332. package/tests/wizard_intent_resume.mjs +21 -10
  333. package/tests/wizard_preselect_why.mjs +18 -18
  334. package/docs/onboarding/browser-terminal-guide.md +0 -73
  335. package/docs/recipes/managed-settings-remote-control.md +0 -416
  336. package/modules/dev-box/CLAUDE.md +0 -15
  337. package/modules/dev-box/box-access.js +0 -623
  338. package/modules/dev-box/box-credential.js +0 -94
  339. package/modules/dev-box/box-onboard.js +0 -486
  340. package/modules/dev-box/boxes.js +0 -1038
  341. package/modules/dev-box/db.js +0 -39
  342. package/modules/dev-box/module.json +0 -19
  343. package/modules/dev-box/routes/box.js +0 -1223
  344. package/scripts/gds/box-auth-check.js +0 -160
  345. package/scripts/gds/box-infra.js +0 -321
  346. package/scripts/gds/box-sync.js +0 -216
  347. package/scripts/gds/box.js +0 -1332
  348. package/scripts/gds/cf-tunnel.js +0 -558
  349. package/scripts/gds/smoke-box.sh +0 -150
  350. package/scripts/gds/tree-preflight.js +0 -241
  351. package/src/bongos/app-pair.js +0 -172
  352. package/tests/app_pair.mjs +0 -168
  353. package/tests/box_access.mjs +0 -578
  354. package/tests/box_auth_check.mjs +0 -114
  355. package/tests/box_code_staleness.mjs +0 -129
  356. package/tests/box_connect_e2e.mjs +0 -196
  357. package/tests/box_cost.mjs +0 -70
  358. package/tests/box_credential.mjs +0 -125
  359. package/tests/box_dns_repoint.mjs +0 -153
  360. package/tests/box_env.mjs +0 -64
  361. package/tests/box_host_keys.mjs +0 -126
  362. package/tests/box_infra_resolve.mjs +0 -246
  363. package/tests/box_onboard.mjs +0 -317
  364. package/tests/box_scope_session.mjs +0 -119
  365. package/tests/box_sweep_docs.mjs +0 -119
  366. package/tests/box_sync.mjs +0 -55
  367. package/tests/box_sync_scope_report.mjs +0 -126
  368. package/tests/box_task_scope.mjs +0 -169
  369. package/tests/box_task_scope_pause.mjs +0 -66
  370. package/tests/box_terminal_ssrf.mjs +0 -96
  371. package/tests/boxes.mjs +0 -1585
  372. package/tests/cf_ruleset_preflight.mjs +0 -142
  373. package/tests/cf_tunnel.mjs +0 -496
  374. package/tests/docs_scrubber_damage.mjs +0 -111
  375. package/tests/module_scope_active_claims.mjs +0 -94
  376. package/tests/ship_api_client_pathspec.mjs +0 -100
  377. package/tests/tree_preflight.mjs +0 -243
@@ -56,7 +56,7 @@ for (const fn of ['function ensureModsCatalog(', 'function renderMods(', 'functi
56
56
  const CATALOG = {
57
57
  core: ['memory', 'lifecycle', 'hall-ui'],
58
58
  optional: [
59
- { key: 'dev-box', label: 'Dev box', summary: 'A shared machine your builders work on.' },
59
+ { key: 'npm-release', label: 'npm release', summary: 'Publishes each release of your project to npm.' },
60
60
  { key: 'discord', label: 'Discord', summary: 'Mirrors what ships into a Discord channel.' },
61
61
  { key: 'agents', label: 'Agents', summary: 'Lets AI builders claim and ship work on their own.' },
62
62
  ],
@@ -76,7 +76,7 @@ function parseRows(html) {
76
76
 
77
77
  // Boot the region against a scripted catalog + detail channel and hand back the
78
78
  // handles a test drives it with.
79
- function boot({ selected = ['dev-box'], source = 'chosen', catalog = CATALOG, catalogOk = true, patchAnswer } = {}) {
79
+ function boot({ selected = ['npm-release'], source = 'chosen', catalog = CATALOG, catalogOk = true, patchAnswer } = {}) {
80
80
  const requests = [];
81
81
  const els = new Map();
82
82
  const listeners = new Map();
@@ -156,14 +156,14 @@ function boot({ selected = ['dev-box'], source = 'chosen', catalog = CATALOG, ca
156
156
 
157
157
  test('the band renders the optional catalog the server offered — a row and a sentence each', async () => {
158
158
  // Arrange
159
- const ui = boot({ selected: ['dev-box'] });
159
+ const ui = boot({ selected: ['npm-release'] });
160
160
 
161
161
  // Act
162
162
  await ui.render();
163
163
 
164
164
  // Assert
165
- assert.deepEqual(ui.rows().map((r) => r.key), ['dev-box', 'discord', 'agents']);
166
- assert.deepEqual(ui.rows().map((r) => r.label), ['Dev box', 'Discord', 'Agents']);
165
+ assert.deepEqual(ui.rows().map((r) => r.key), ['npm-release', 'discord', 'agents']);
166
+ assert.deepEqual(ui.rows().map((r) => r.label), ['npm release', 'Discord', 'Agents']);
167
167
  for (const r of ui.rows()) assert.ok(r.summary.length > 20, `${r.label} says what it is for`);
168
168
  });
169
169
 
@@ -171,11 +171,11 @@ test('what the project runs today is what is switched on — resolved by the ser
171
171
  // A project that SKIPPED the picker reads as its own starter bundle, and the
172
172
  // band must show that set rather than an empty one: the whole point of
173
173
  // effectiveModules resolving `source: 'bundle'` on the way out.
174
- const ui = boot({ selected: ['dev-box', 'discord'], source: 'bundle' });
174
+ const ui = boot({ selected: ['npm-release', 'discord'], source: 'bundle' });
175
175
 
176
176
  await ui.render();
177
177
 
178
- assert.deepEqual(ui.rows().filter((r) => r.on).map((r) => r.key), ['dev-box', 'discord']);
178
+ assert.deepEqual(ui.rows().filter((r) => r.on).map((r) => r.key), ['npm-release', 'discord']);
179
179
  assert.deepEqual(ui.rows().filter((r) => !r.on).map((r) => r.key), ['agents']);
180
180
  });
181
181
 
@@ -185,7 +185,7 @@ test('the always-on core is COUNTED and stated, and is not a row anyone can swit
185
185
  await ui.render();
186
186
 
187
187
  assert.match(ui.slot(), /the same 3 core parts/, 'the clause names how many, from the server\'s own list');
188
- assert.deepEqual(ui.rows().map((r) => r.key), ['dev-box', 'discord', 'agents'], 'and no core key is on the band');
188
+ assert.deepEqual(ui.rows().map((r) => r.key), ['npm-release', 'discord', 'agents'], 'and no core key is on the band');
189
189
  for (const key of CATALOG.core) {
190
190
  assert.equal(ui.rows().some((r) => r.key === key), false, `${key} is not a choice`);
191
191
  }
@@ -196,26 +196,26 @@ test('a toggle issues the write — the WHOLE list, in catalog order, on the own
196
196
  const ui = boot({ selected: ['discord'] });
197
197
  await ui.render();
198
198
 
199
- // Act — switch the dev box ON
200
- await ui.click('dev-box');
199
+ // Act — switch npm release ON
200
+ await ui.click('npm-release');
201
201
 
202
202
  // Assert
203
203
  const patch = ui.requests.find((r) => r.method === 'PATCH');
204
204
  assert.ok(patch, 'a PATCH was sent');
205
205
  assert.equal(patch.url, '/api/bongos/provisioning/instances/18/detail');
206
- assert.deepEqual(JSON.parse(patch.body), { modules: ['dev-box', 'discord'] },
206
+ assert.deepEqual(JSON.parse(patch.body), { modules: ['npm-release', 'discord'] },
207
207
  'catalog order, exactly as the column stores it — never append order');
208
208
  assert.equal(patch.credentials, 'same-origin', 'the cookie session is the auth — this page carries no token');
209
209
  });
210
210
 
211
211
  test('a toggle can REMOVE one too — the same list, one key shorter', async () => {
212
- const ui = boot({ selected: ['dev-box', 'agents'] });
212
+ const ui = boot({ selected: ['npm-release', 'agents'] });
213
213
  await ui.render();
214
214
 
215
215
  await ui.click('agents');
216
216
 
217
217
  const patch = ui.requests.find((r) => r.method === 'PATCH');
218
- assert.deepEqual(JSON.parse(patch.body), { modules: ['dev-box'] });
218
+ assert.deepEqual(JSON.parse(patch.body), { modules: ['npm-release'] });
219
219
  });
220
220
 
221
221
  test('switching the last one off sends an empty list — a real answer, not a skipped write', async () => {
@@ -243,8 +243,8 @@ test('the band repaints from the PLATFORM’S answer, and says the save changed
243
243
 
244
244
  test('a refusal is SAID in the server’s own words, and the band goes back to what the row still holds', async () => {
245
245
  const ui = boot({
246
- selected: ['dev-box'],
247
- patchAnswer: Promise.resolve({ ok: false, status: 400, _json: { error: { code: 'bad_modules', message: 'Unknown module "discrod" — the choices are: dev-box, discord, agents.' } } }),
246
+ selected: ['npm-release'],
247
+ patchAnswer: Promise.resolve({ ok: false, status: 400, _json: { error: { code: 'bad_modules', message: 'Unknown module "discrod" — the choices are: npm-release, discord, agents.' } } }),
248
248
  });
249
249
  await ui.render();
250
250
 
@@ -252,7 +252,7 @@ test('a refusal is SAID in the server’s own words, and the band goes back to w
252
252
 
253
253
  assert.equal(ui.msg().className, 'projMsg err');
254
254
  assert.match(ui.msg().textContent, /Unknown module "discrod"/);
255
- assert.deepEqual(ui.rows().filter((r) => r.on).map((r) => r.key), ['dev-box'],
255
+ assert.deepEqual(ui.rows().filter((r) => r.on).map((r) => r.key), ['npm-release'],
256
256
  'a refused write leaves the band showing the project’s real set');
257
257
  });
258
258
 
@@ -269,7 +269,7 @@ test('a signed-out save offers a way back in rather than a dead end', async () =
269
269
  });
270
270
 
271
271
  test('a catalog that ANSWERED badly says so; nothing about the project is claimed to have changed', async () => {
272
- const ui = boot({ selected: ['dev-box'], catalogOk: false });
272
+ const ui = boot({ selected: ['npm-release'], catalogOk: false });
273
273
 
274
274
  await ui.render();
275
275
  await ui.settle();
@@ -707,14 +707,14 @@ test('the About answers travel on the create request, and an empty one is OMITTE
707
707
 
708
708
  test('a renumbering of the wizard invalidates old drafts — the key carries the version', () => {
709
709
  // task 1002772 renumbered once and bumped to -v2; task 1002334 renumbered again;
710
- // task 1002339 (the module picker) is the third. A draft replayed under a
710
+ // task 1002339 (the module picker) is the third; task 1004351 (the app step) the fourth. A draft replayed under a
711
711
  // different numbering lands on the wrong panel with the rail already marking
712
712
  // the skipped steps done.
713
713
  const key = (SCRIPT.match(/var DRAFT_KEY = '([^']+)'/) || [, ''])[1];
714
714
  assert.match(key, /-v\d+$/, 'the draft key is versioned');
715
715
  const panels = (SCRIPT.match(/var panels = \{([^}]*)\}/) || [, ''])[1];
716
716
  const count = [...panels.matchAll(/\d+:\s*'panel-/g)].length;
717
- assert.equal(count, 9, 'gate + seven numbered steps + done — bump DRAFT_KEY when this changes');
717
+ assert.equal(count, 10, 'gate + eight numbered steps + done — bump DRAFT_KEY when this changes');
718
718
  });
719
719
 
720
720
  test('every row lays the same five parts — the ledger has no ragged row', () => {
@@ -2133,9 +2133,9 @@ test('the wizard takes the world by scope: no card ground, the six-step strip as
2133
2133
  assert.ok(at > -1, 'the #view-new block exists in cosmos.css');
2134
2134
  const block = COSMOS.slice(at);
2135
2135
  assert.match(block, /#view-new \.card\{background:none;border-radius:0;box-shadow:none;/, 'no card ground');
2136
- // seven tracks since task 1002339 added the Modules step; the count is the
2136
+ // eight tracks since task 1004351 added the App step (seven since 1002339); the count is the
2137
2137
  // rail's own, so it moves with the panels map rather than being a magic number
2138
- assert.match(block, /#view-new \.steps\{display:grid;grid-template-columns:repeat\(7,1fr\);/, 'the strip lays a track per numbered step');
2138
+ assert.match(block, /#view-new \.steps\{display:grid;grid-template-columns:repeat\(8,1fr\);/, 'the strip lays a track per numbered step');
2139
2139
  assert.match(block, /#view-new \.steps li::before\{content:"";position:absolute;top:-4px;left:0;right:auto;width:7px;height:7px;/, 'the connector becomes the 7px dot');
2140
2140
  assert.match(block, /#view-new \.steps li\.now::before\{background:var\(--accent\);border-color:var\(--accent\);outline:4px solid/, 'the lit step carries the accent and an outline halo');
2141
2141
  assert.match(block, /#view-new \.panel h1\{font-weight:300;[^}]*text-transform:lowercase;/, 'the step title in the display voice, lowercase');
@@ -2182,7 +2182,7 @@ test('the wizard\'s own structures wear the world: hairline fields, cells of one
2182
2182
  assert.match(block, /\.addrBig code\{display:block;font-family:var\(--font-display\);/, 'the address stands in the display voice');
2183
2183
  assert.doesNotMatch(block, /linear-gradient/, 'no tinted plates');
2184
2184
  assert.doesNotMatch(block, /box-shadow:(?!none)/, 'no box shadow in the wizard block');
2185
- assert.match(HUB, /<div class="nav">\s*<button type="button" class="btn ghost" data-back="6">← Back<\/button>\s*<button type="button" class="btn big" id="doCreate">Create my project<\/button>/, 'the review\'s doors sit on one row, back then the primary');
2185
+ assert.match(HUB, /<div class="nav">\s*<button type="button" class="btn ghost" data-back="7">← Back<\/button>\s*<button type="button" class="btn big" id="doCreate">Create my project<\/button>/, 'the review\'s doors sit on one row, back then the primary');
2186
2186
  assert.match(HUB, /<h2 class="stepsHead" id="doneStepsHead">Your steps — the parts only you can play<\/h2>/, 'the steps heading is the label face by class, not an inline style (the id is where a skipped invite step hands focus on, task 1002346)');
2187
2187
  // what is enqueued is unchanged (ADR 0111): the filing body the control plane reads
2188
2188
  assert.match(SCRIPT, /body: JSON\.stringify\(\{\s*slug: state\.slug,/, 'the filing still carries the slug first');
@@ -0,0 +1,138 @@
1
+ // tests/projects_hub_app_step.mjs — the create wizard's "where your app runs" step,
2
+ // EXECUTED (task 1004351, ADR 0345).
3
+ //
4
+ // The step is a native radio group, so almost all of its behaviour is one rule: the
5
+ // review may only ever name a place the owner could actually pick. Render is listed
6
+ // but disabled until the connect flow lands (task 1004352), and "decide later" is the
7
+ // owner's ruling for the default. A static regex cannot tell a guard that works from
8
+ // one that is never consulted, so — as tests/projects_hub_module_picker.mjs does for
9
+ // the picker — this slices the block out of projects.html by its own sentinels and
10
+ // runs it in `vm` under a small DOM stub.
11
+ //
12
+ // Run: node --test --test-reporter=tap tests/projects_hub_app_step.mjs
13
+
14
+ import test from 'node:test';
15
+ import assert from 'node:assert/strict';
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import vm from 'node:vm';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
22
+ const HUB = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
23
+
24
+ const START = '/* the app step (task 1004351)';
25
+ const END = ' function renderReview() {';
26
+ const from = HUB.indexOf(START);
27
+ const to = HUB.indexOf(END, from);
28
+ assert.ok(from > -1, 'the app-step block opens with its sentinel comment');
29
+ assert.ok(to > from, 'the app-step block ends where renderReview begins');
30
+ const BLOCK = HUB.slice(from, to);
31
+
32
+ const PANEL = (HUB.match(/<section class="panel" id="panel-app"[\s\S]*?<\/section>/) || [''])[0];
33
+
34
+ // Two radios, built from the real markup so the harness cannot drift from the page.
35
+ function radiosFromMarkup(renderEnabled) {
36
+ return [...PANEL.matchAll(/<input type="radio" name="appPlatform" value="([a-z-]+)"([^>]*)>/g)].map((m) => ({
37
+ name: 'appPlatform',
38
+ value: m[1],
39
+ disabled: m[1] === 'render' ? !renderEnabled && /\sdisabled/.test(m[2]) : /\sdisabled/.test(m[2]),
40
+ checked: /\schecked/.test(m[2]),
41
+ }));
42
+ }
43
+
44
+ function load({ draft = {}, renderEnabled = false } = {}) {
45
+ const inputs = radiosFromMarkup(renderEnabled);
46
+ let changeHandler = null;
47
+ const saves = [];
48
+ const root = {
49
+ querySelector(sel) {
50
+ if (sel === '#appOpts') return { addEventListener: (ev, fn) => { if (ev === 'change') changeHandler = fn; } };
51
+ throw new Error('unexpected selector ' + sel);
52
+ },
53
+ querySelectorAll(sel) {
54
+ assert.equal(sel, '#appOpts input[name=appPlatform]');
55
+ return inputs;
56
+ },
57
+ };
58
+ const state = Object.assign({ appPlatform: 'later' }, draft);
59
+ const ctx = { root, state, saveDraft: () => saves.push(state.appPlatform) };
60
+ vm.createContext(ctx);
61
+ vm.runInContext(BLOCK + '\nthis.api = { appPlatformChoice, syncAppStep, appRow };', ctx);
62
+ return { ...ctx.api, inputs, state, saves, change: (value) => changeHandler({ target: inputs.find((i) => i.value === value) }) };
63
+ }
64
+
65
+ test('the panel is real markup: Render listed but not pickable yet, Decide later the default', () => {
66
+ assert.ok(PANEL, '#panel-app exists');
67
+ assert.match(PANEL, /<input type="radio" name="appPlatform" value="render" disabled/, 'Render cannot be picked until task 1004352');
68
+ assert.match(PANEL, /<span class="soonTag">Coming soon<\/span>/, 'and says so in words, not by colour alone');
69
+ assert.match(PANEL, /<input type="radio" name="appPlatform" value="later" checked>/, 'Decide later is the default');
70
+ assert.match(PANEL, /We host your Builders' hall/, 'the step says the hall is ours to host (ADR 0345 D1), in the page\'s one spelling of it');
71
+ assert.match(PANEL, /<form data-next="8">/, 'Next leads to the review');
72
+ assert.match(PANEL, /data-back="6"/, 'Back returns to the address step');
73
+ });
74
+
75
+ test('the step sits between Address and Review in the panels map and on the rail', () => {
76
+ assert.match(HUB, /6: 'panel-address', 7: 'panel-app', 8: 'panel-review', 9: 'panel-done'/);
77
+ assert.match(HUB, /<li data-step="7"><span class="snum">7<\/span><span class="slbl">App<\/span><\/li>/);
78
+ assert.match(HUB, /<li data-step="8"><span class="snum">8<\/span><span class="slbl">Create<\/span><\/li>/);
79
+ assert.match(HUB, /data-back="7">← Back<\/button>\n\s*<button type="button" class="btn big" id="doCreate">/, "the review's Back returns to the app step");
80
+ assert.match(HUB, /if \(step === 7\) syncAppStep\(\);\n\s*if \(step === 8\) renderReview\(\);/, 'entering the step repaints it from the draft');
81
+ });
82
+
83
+ test('a fresh draft reads "decide later"', () => {
84
+ const w = load();
85
+ assert.equal(w.appPlatformChoice(), 'later');
86
+ assert.equal(w.appRow(), 'Not chosen yet — you can decide later');
87
+ });
88
+
89
+ test('a draft that names Render while Render is not pickable still reads "decide later"', () => {
90
+ // The rule: the review never names a place the owner could not actually pick.
91
+ const w = load({ draft: { appPlatform: 'render' } });
92
+ assert.equal(w.appPlatformChoice(), 'later');
93
+ assert.equal(w.appRow(), 'Not chosen yet — you can decide later');
94
+ w.syncAppStep();
95
+ assert.deepEqual(w.inputs.filter((i) => i.checked).map((i) => i.value), ['later'], 'and the step shows Decide later checked');
96
+ });
97
+
98
+ test('once Render is pickable, choosing it is what the review names', () => {
99
+ const w = load({ renderEnabled: true });
100
+ w.change('render');
101
+ assert.equal(w.state.appPlatform, 'render');
102
+ assert.deepEqual(w.saves, ['render'], 'the choice is saved to the draft');
103
+ assert.equal(w.appRow(), 'Render, on your own account');
104
+ w.syncAppStep();
105
+ assert.deepEqual(w.inputs.filter((i) => i.checked).map((i) => i.value), ['render']);
106
+ });
107
+
108
+ test('a change on a disabled radio is ignored, and an unknown or missing value reads "decide later"', () => {
109
+ const w = load();
110
+ w.change('render');
111
+ assert.equal(w.state.appPlatform, 'later', 'a disabled option never reaches the draft');
112
+ assert.deepEqual(w.saves, []);
113
+ for (const v of [undefined, '', 'vercel', 'x"]']) {
114
+ assert.equal(load({ draft: { appPlatform: v } }).appPlatformChoice(), 'later', `appPlatform=${JSON.stringify(v)}`);
115
+ }
116
+ });
117
+
118
+ test('the review carries the hall row and the app row in place of the old Shape row', () => {
119
+ const render = HUB.slice(HUB.indexOf('function renderReview()'));
120
+ const rows = render.slice(0, render.indexOf("$('reviewList').innerHTML"));
121
+ assert.ok(rows.includes("['Builders\\' hall', 'Hosted by us, on the platform’s current release'],"), 'the hall row');
122
+ assert.match(rows, /\['App', appRow\(\)\]/);
123
+ assert.doesNotMatch(rows, /\['Shape',/, 'the Shape row is gone');
124
+ });
125
+
126
+ test('the rail grid has one column per step, so adding a step never wraps the rail', () => {
127
+ // Adding the app step made the rail eight beats in a seven-column grid: at 1440 the
128
+ // Create beat wrapped onto a second row. The count lives in the shared sheet, so the
129
+ // two must be checked against each other, not each on its own.
130
+ const css = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'assets', 'cosmos.css'), 'utf8');
131
+ const cols = Number((css.match(/#view-new \.steps\{display:grid;grid-template-columns:repeat\((\d+),1fr\)/) || [])[1]);
132
+ const beats = [...HUB.matchAll(/<li[^>]*data-step="\d+"/g)].length;
133
+ assert.equal(cols, beats, 'the #view-new rail grid carries exactly one column per rail step');
134
+ });
135
+
136
+ test('the hall is spelled one way on this page', () => {
137
+ assert.doesNotMatch(HUB, /Builders Hall/, 'no "Builders Hall" beside the page\'s own "Builders\' hall"');
138
+ });