@bongos/core 1.24.78 → 1.24.80

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 (369) hide show
  1. package/.bongos-core.json +498 -353
  2. package/.claude/skills/ask-for-help/SKILL.md +1 -1
  3. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  4. package/.claude/skills/design/SKILL.md +1 -1
  5. package/.claude/skills/goal-create/SKILL.md +1 -1
  6. package/.claude/skills/planning-session/SKILL.md +1 -1
  7. package/clients/bongos-client/README.md +1 -1
  8. package/clients/bongos-client/bongos-client.global.js +8 -0
  9. package/clients/bongos-client/index.cjs +8 -0
  10. package/clients/bongos-client/index.d.ts +11 -0
  11. package/clients/bongos-client/index.mjs +8 -0
  12. package/docs/adr/0274-one-kernel-three-role-packs.md +2 -2
  13. package/docs/adr/0399-the-visor-is-glass-over-the-scene-and-owns-no-camera.md +91 -0
  14. package/docs/adr/0400-a-chain-reserves-work-and-each-sub-quest-is-still-claimed-alone.md +109 -0
  15. package/docs/adr/0401-one-options-menu-opens-every-settings-screen-in-a-frame.md +53 -0
  16. package/docs/adr/README.md +3 -0
  17. package/docs/api/openapi.json +195 -3
  18. package/docs/api-reference.md +8 -4
  19. package/docs/architecture.md +7 -3
  20. package/docs/copy-inventory.md +390 -370
  21. package/docs/copy-registry.json +719 -518
  22. package/docs/design/characters/character-sprites-source.webp +0 -0
  23. package/docs/file-map.md +7 -8
  24. package/docs/module-api-changelog.md +4 -0
  25. package/docs/modules-contract.md +5 -0
  26. package/docs/onboarding/slash-commands.md +1 -1
  27. package/docs/packs/{engineer.md → artisan.md} +5 -5
  28. package/docs/packs/artist.md +50 -4
  29. package/docs/page-inventory.json +15 -4
  30. package/docs/page-readings.json +3182 -3336
  31. package/docs/{packs/ideator.md → recipes/ideation-playbook.md} +5 -48
  32. package/migrations/core_296_crafts_artisan_artist.sql +72 -0
  33. package/migrations/core_297_builder_character.sql +30 -0
  34. package/modules/copy-desk/proposals.js +1 -1
  35. package/modules/economy/credits-by-craft.js +29 -15
  36. package/modules/hall-ui/public/ai-studio.html +4 -4
  37. package/modules/hall-ui/public/atlas.html +5 -5
  38. package/modules/hall-ui/public/atlas.js +3 -1
  39. package/modules/hall-ui/public/blockers.html +5 -5
  40. package/modules/hall-ui/public/board-room.html +4 -4
  41. package/modules/hall-ui/public/characters/artisan.png +0 -0
  42. package/modules/hall-ui/public/characters/artist.png +0 -0
  43. package/modules/hall-ui/public/characters/patron.png +0 -0
  44. package/modules/hall-ui/public/city.css +2 -1
  45. package/modules/hall-ui/public/city.html +9 -6
  46. package/modules/hall-ui/public/city.js +7 -1
  47. package/modules/hall-ui/public/collab.html +4 -4
  48. package/modules/hall-ui/public/commands.html +4 -4
  49. package/modules/hall-ui/public/copy-desk.html +4 -4
  50. package/modules/hall-ui/public/craft-mode.js +258 -64
  51. package/modules/hall-ui/public/deploy.html +4 -4
  52. package/modules/hall-ui/public/diagram-viewer.js +2 -1
  53. package/modules/hall-ui/public/dom-utils.js +1 -1
  54. package/modules/hall-ui/public/goal-map.html +9 -6
  55. package/modules/hall-ui/public/goals.html +5 -5
  56. package/modules/hall-ui/public/government.html +4 -4
  57. package/modules/hall-ui/public/hall-render.js +3 -1
  58. package/modules/hall-ui/public/home-city-draw.js +3 -3
  59. package/modules/hall-ui/public/home-city.css +4 -0
  60. package/modules/hall-ui/public/home-city.js +17 -9
  61. package/modules/hall-ui/public/home-gallery.js +8 -1
  62. package/modules/hall-ui/public/home-idea-box.css +2 -2
  63. package/modules/hall-ui/public/home-idea-box.js +3 -3
  64. package/modules/hall-ui/public/idea.html +5 -5
  65. package/modules/hall-ui/public/ideas.html +5 -5
  66. package/modules/hall-ui/public/index.html +11 -7
  67. package/modules/hall-ui/public/market-watch-item.html +4 -4
  68. package/modules/hall-ui/public/market-watch-rules.html +4 -4
  69. package/modules/hall-ui/public/market-watch.html +6 -9
  70. package/modules/hall-ui/public/market-watch.js +22 -45
  71. package/modules/hall-ui/public/modules.html +4 -4
  72. package/modules/hall-ui/public/money.html +4 -4
  73. package/modules/hall-ui/public/my-agents.html +4 -4
  74. package/modules/hall-ui/public/options-menu.js +409 -0
  75. package/modules/hall-ui/public/paintbrush.js +3 -3
  76. package/modules/hall-ui/public/palette.js +11 -13
  77. package/modules/hall-ui/public/primer.html +5 -5
  78. package/modules/hall-ui/public/profile-crafts.js +1 -1
  79. package/modules/hall-ui/public/profile-roles.js +23 -19
  80. package/modules/hall-ui/public/profile.css +1 -1
  81. package/modules/hall-ui/public/profile.html +19 -18
  82. package/modules/hall-ui/public/profile.js +19 -18
  83. package/modules/hall-ui/public/profile.states.json +4 -4
  84. package/modules/hall-ui/public/project-settings.html +4 -4
  85. package/modules/hall-ui/public/roster.html +4 -4
  86. package/modules/hall-ui/public/roster.js +3 -3
  87. package/modules/hall-ui/public/settings.css +24 -4
  88. package/modules/hall-ui/public/settings.html +21 -17
  89. package/modules/hall-ui/public/settings.js +44 -44
  90. package/modules/hall-ui/public/shell-game.css +65 -0
  91. package/modules/hall-ui/public/shell-game.js +283 -3
  92. package/modules/hall-ui/public/shell.js +7 -26
  93. package/modules/hall-ui/public/sky-interaction.js +7 -0
  94. package/modules/hall-ui/public/speciality-tools.js +1 -2
  95. package/modules/hall-ui/public/street-inside.js +3 -4
  96. package/modules/hall-ui/public/street.html +4 -4
  97. package/modules/hall-ui/public/studio.html +5 -5
  98. package/modules/hall-ui/public/style.css +73 -47
  99. package/modules/hall-ui/public/task-detail.js +5 -4
  100. package/modules/hall-ui/public/task.html +5 -5
  101. package/modules/hall-ui/public/thinking.css +10 -0
  102. package/modules/hall-ui/public/thinking.html +9 -6
  103. package/modules/hall-ui/public/tweak-editor.html +5 -5
  104. package/modules/hall-ui/public/tweak-editor.js +5 -1
  105. package/modules/hall-ui/public/update-pill.js +91 -2
  106. package/modules/hall-ui/public/visor.css +80 -0
  107. package/modules/hall-ui/public/visor.js +316 -0
  108. package/modules/hall-ui/public/watch-repo-invite.js +159 -0
  109. package/modules/hall-ui/public/watch.html +6 -5
  110. package/modules/hall-ui/public/watch.js +17 -2
  111. package/modules/hall-ui/public/welcome-boot.js +15 -6
  112. package/modules/hall-ui/public/work.html +5 -5
  113. package/modules/hall-ui/records/builders-roster.md +2 -2
  114. package/modules/hall-ui/records/craft-mode.md +13 -11
  115. package/modules/hall-ui/records/game-mode.md +2 -2
  116. package/modules/hall-ui/records/governor-street.md +1 -1
  117. package/modules/hall-ui/records/home-city.md +3 -1
  118. package/modules/hall-ui/records/market-watch-settings.md +2 -0
  119. package/modules/hall-ui/records/options-menu.md +15 -0
  120. package/modules/hall-ui/records/profile-roles.md +11 -11
  121. package/modules/hall-ui/records/sky.md +3 -3
  122. package/modules/hall-ui/records/start-screen.md +21 -0
  123. package/modules/hall-ui/records/tweak-editor.md +1 -1
  124. package/modules/hall-ui/records/update-pill.md +12 -0
  125. package/modules/hall-ui/records/visor.md +23 -0
  126. package/modules/hall-ui/records/welcome-tower.md +5 -5
  127. package/modules/ideas/ideator-stats.js +3 -1
  128. package/modules/ideas/inbox.js +2 -2
  129. package/modules/lifecycle/activity.js +1 -1
  130. package/modules/lifecycle/cascade.js +4 -4
  131. package/modules/lifecycle/goal-spec-task.js +1 -1
  132. package/modules/lifecycle/help-requests.js +19 -15
  133. package/modules/lifecycle/kickoff-checklist.js +3 -3
  134. package/modules/lifecycle/kickoff-seed.js +1 -1
  135. package/modules/lifecycle/migrations/lifecycle_021_crafts_artisan_artist.sql +29 -0
  136. package/modules/lifecycle/mingle.js +4 -4
  137. package/modules/lifecycle/page-tweak-write.js +1 -1
  138. package/modules/lifecycle/routes/task-write-routes.js +3 -1
  139. package/modules/lifecycle/routes/tasks.js +3 -3
  140. package/modules/lifecycle/ship-card.js +2 -3
  141. package/modules/lifecycle/task-classifier.js +39 -29
  142. package/modules/lifecycle/tests/cascade.mjs +7 -7
  143. package/modules/onboarding/founding-mode.js +3 -0
  144. package/modules/onboarding/genesis-home.js +21 -1
  145. package/modules/onboarding/routes/onboarding.js +2 -0
  146. package/modules/page-notes/notes.js +3 -2
  147. package/modules/page-notes/split.js +1 -1
  148. package/modules/platform-identity/craft-rollup.js +35 -7
  149. package/modules/platform-identity/migrations/platform_identity_036_crafts_artisan_artist.sql +45 -0
  150. package/modules/platform-identity/platform-identity.js +2 -2
  151. package/modules/platform-identity/routes/community-leaderboard.js +2 -1
  152. package/modules/platform-identity/routes/community-search.js +2 -1
  153. package/modules/platform-identity/routes/sso.js +2 -1
  154. package/modules/platform-identity/tests/platform-identity.mjs +6 -6
  155. package/modules/public-landing/public/community.html +3 -5
  156. package/modules/public-landing/public/index.html +1 -1
  157. package/modules/public-landing/public/projects.html +1 -1
  158. package/modules/specialities/migrations/specialities_004_crafts_artisan_artist.sql +30 -0
  159. package/modules/specialities/routes/specialities.js +2 -2
  160. package/modules/status-ui/public/status.js +1 -1
  161. package/modules/ui-design/kit/fixtures/community-leaderboard.json +10 -10
  162. package/modules/ui-design/kit/fixtures/community-search.json +2 -2
  163. package/modules/ui-design/kit/fixtures/scouting-recruiter.json +1 -1
  164. package/package-lock.json +2 -2
  165. package/package.json +1 -1
  166. package/release-notes.json +76 -0
  167. package/scripts/bongos/autobongos-host.js +19 -4
  168. package/scripts/bongos/autobongos-loop.js +1 -1
  169. package/scripts/bongos/claude-materialize.js +20 -1
  170. package/scripts/bongos/discipline-modes.json +7 -12
  171. package/scripts/bongos/fitness.js +1 -1
  172. package/scripts/bongos/gen-hall-sitemap.js +7 -7
  173. package/scripts/bongos/grade-smoke.js +2 -2
  174. package/scripts/bongos/init.js +2 -2
  175. package/scripts/bongos/newcomer-floor.config.json +1 -2
  176. package/scripts/bongos/newcomer-floor.js +4 -3
  177. package/scripts/bongos/newcomer-floor.templates.json +10 -12
  178. package/scripts/bongos/newcomer-floor.test.js +47 -38
  179. package/scripts/bongos/role-pack-guard.js +5 -4
  180. package/scripts/bongos/seed-tasks.example.json +3 -3
  181. package/scripts/bongos/setup.js +20 -107
  182. package/scripts/bongos/start.js +2 -2
  183. package/scripts/bongos/task.js +5 -4
  184. package/scripts/bongos/triage.js +1 -1
  185. package/scripts/bongos/worktree.js +16 -4
  186. package/scripts/setup-builder.js +6 -46
  187. package/src/bongos/auth-admission.js +8 -8
  188. package/src/bongos/craft-mode.js +2 -0
  189. package/src/bongos/db.js +39 -1
  190. package/src/bongos/github-repos.js +78 -0
  191. package/src/bongos/module-scope-map.js +22 -0
  192. package/src/bongos/role-stats.js +27 -11
  193. package/src/bongos/routes/github.js +100 -2
  194. package/src/bongos/routes/me.js +33 -0
  195. package/src/bongos/serve-internal.js +26 -9
  196. package/src/crafts.js +66 -0
  197. package/src/module-api.js +6 -1
  198. package/src/module-loader/manifest-schema.js +16 -0
  199. package/src/modules.js +2 -2
  200. package/src/role-responsibilities.js +6 -5
  201. package/tests/acclaim_feed_evidence_db.mjs +1 -1
  202. package/tests/activity_rollup_as_of.mjs +1 -1
  203. package/tests/api_alias_caller_ratchet.mjs +3 -3
  204. package/tests/application_lifecycle.mjs +1 -1
  205. package/tests/artist_review.mjs +1 -1
  206. package/tests/artist_review_goal.mjs +1 -1
  207. package/tests/autobongos_host.mjs +19 -1
  208. package/tests/blocker_hall_live_proof.mjs +1 -1
  209. package/tests/blocker_tasks_named_db.mjs +2 -2
  210. package/tests/canonical_profile_url.mjs +1 -1
  211. package/tests/cap_newcomer_tasks.mjs +13 -13
  212. package/tests/character_creation.mjs +294 -0
  213. package/tests/claim_gate_open_blocker.mjs +2 -2
  214. package/tests/claim_gate_rebase.mjs +6 -6
  215. package/tests/claude_materialize.mjs +75 -1
  216. package/tests/clawback_confirmed_backlog.mjs +3 -3
  217. package/tests/collab_page.mjs +8 -9
  218. package/tests/collab_self_settling.mjs +2 -2
  219. package/tests/community_leaderboard_route.mjs +13 -5
  220. package/tests/community_proof_db.mjs +3 -0
  221. package/tests/community_search_route.mjs +7 -6
  222. package/tests/conductor.mjs +1 -1
  223. package/tests/conductor_main_e2e.mjs +8 -8
  224. package/tests/copy_desk_flags_db.mjs +1 -1
  225. package/tests/copy_desk_page_approve.mjs +2 -2
  226. package/tests/copy_desk_page_claim.mjs +2 -2
  227. package/tests/copy_desk_page_next.mjs +1 -1
  228. package/tests/copy_desk_page_release.mjs +1 -1
  229. package/tests/copy_desk_page_submit.mjs +2 -2
  230. package/tests/core_258_box_session_claims_db.mjs +1 -1
  231. package/tests/craft_mode.mjs +48 -34
  232. package/tests/craft_mode_home.mjs +20 -20
  233. package/tests/craft_mode_looks.mjs +16 -15
  234. package/tests/craft_mode_nav.mjs +31 -29
  235. package/tests/craft_review_mode.mjs +46 -41
  236. package/tests/crafts_vocabulary.mjs +70 -0
  237. package/tests/cross_discipline_priority_direction.mjs +5 -5
  238. package/tests/fitness.mjs +7 -11
  239. package/tests/funded_project_pays_proof_db.mjs +1 -1
  240. package/tests/genesis_home.mjs +21 -1
  241. package/tests/github_chip.mjs +3 -3
  242. package/tests/github_repo_invite.mjs +379 -0
  243. package/tests/goal_criterion_confirm_ui.mjs +1 -1
  244. package/tests/goal_invite_consent.mjs +1 -1
  245. package/tests/goal_map_page.mjs +2 -2
  246. package/tests/goal_sort_staleness.mjs +1 -1
  247. package/tests/goal_spec_task.mjs +1 -1
  248. package/tests/goal_spec_task_db.mjs +3 -3
  249. package/tests/grade_engine.mjs +2 -2
  250. package/tests/grader_no_artifact.mjs +1 -1
  251. package/tests/grader_prompt_fencing.mjs +1 -1
  252. package/tests/grader_prompt_window_bound.mjs +1 -1
  253. package/tests/grader_reader_lens.mjs +2 -2
  254. package/tests/grader_severity_gate.mjs +1 -1
  255. package/tests/grader_unbacked_score.mjs +1 -1
  256. package/tests/hall_approval_queue.mjs +1 -1
  257. package/tests/hall_board_world.mjs +2 -2
  258. package/tests/hall_builders_page_pass.mjs +1 -1
  259. package/tests/hall_city.mjs +1 -1
  260. package/tests/hall_fullscreen.mjs +27 -3
  261. package/tests/hall_kit_charts.mjs +1 -1
  262. package/tests/hall_landing_boot.mjs +1 -1
  263. package/tests/hall_my_agents.mjs +16 -16
  264. package/tests/hall_nav.mjs +22 -15
  265. package/tests/hall_options_menu.mjs +247 -0
  266. package/tests/hall_oversight_world.mjs +1 -1
  267. package/tests/hall_palette.mjs +8 -9
  268. package/tests/hall_profile_world.mjs +3 -3
  269. package/tests/hall_record_world.mjs +2 -2
  270. package/tests/hall_settings_world.mjs +2 -1
  271. package/tests/hall_sitemap.mjs +6 -5
  272. package/tests/hall_start_screen.mjs +244 -0
  273. package/tests/hall_street.mjs +2 -2
  274. package/tests/hall_street_inside.mjs +17 -9
  275. package/tests/hall_studio_world.mjs +3 -3
  276. package/tests/hall_visor.mjs +224 -0
  277. package/tests/help_requests.mjs +47 -15
  278. package/tests/helpers.mjs +4 -4
  279. package/tests/home_city.mjs +6 -4
  280. package/tests/home_city_widget.mjs +33 -14
  281. package/tests/home_gallery_widget.mjs +4 -4
  282. package/tests/home_idea_box.mjs +6 -6
  283. package/tests/hub_community_page.mjs +3 -3
  284. package/tests/hub_craft_rollup.mjs +75 -36
  285. package/tests/hub_recent_ships.mjs +3 -2
  286. package/tests/idea_cascade.mjs +2 -2
  287. package/tests/idea_credit_streams_db.mjs +1 -1
  288. package/tests/idea_detail_page.mjs +1 -1
  289. package/tests/idea_ideate_full.mjs +5 -2
  290. package/tests/idea_spark_hall.mjs +1 -1
  291. package/tests/init.mjs +3 -3
  292. package/tests/instance_manifest.mjs +2 -2
  293. package/tests/kickoff_checklist.mjs +1 -1
  294. package/tests/leaderboard_roles.mjs +34 -25
  295. package/tests/lifecycle_claim_ladder_refs.mjs +55 -0
  296. package/tests/lightbulb.mjs +14 -10
  297. package/tests/main_discipline.mjs +8 -8
  298. package/tests/markdown_line_endings.mjs +1 -1
  299. package/tests/market_watch_pages.mjs +52 -74
  300. package/tests/me_streak_leaderboard_queries.mjs +3 -3
  301. package/tests/migration_allocation.mjs +1 -1
  302. package/tests/mingle.mjs +36 -21
  303. package/tests/module-scope-map.mjs +22 -2
  304. package/tests/module_api.mjs +1 -0
  305. package/tests/module_contributions.mjs +13 -13
  306. package/tests/module_manifest.mjs +29 -0
  307. package/tests/module_skills_hook.mjs +2 -2
  308. package/tests/modules.mjs +14 -7
  309. package/tests/nav_permission_atoms.mjs +2 -1
  310. package/tests/needs_rebase.mjs +1 -1
  311. package/tests/newcomer_restock.mjs +13 -13
  312. package/tests/page_notes_panel.mjs +22 -22
  313. package/tests/page_tweak_no_block.mjs +1 -1
  314. package/tests/paintbrush.mjs +7 -7
  315. package/tests/privacy_matrix.mjs +1 -0
  316. package/tests/profile_activity.mjs +20 -23
  317. package/tests/profile_earnings.mjs +1 -1
  318. package/tests/profile_nudge_prefs.mjs +4 -4
  319. package/tests/profile_role_headline.mjs +73 -46
  320. package/tests/profile_role_stats.mjs +55 -45
  321. package/tests/profile_rollup_consent.mjs +19 -6
  322. package/tests/profile_route.mjs +12 -12
  323. package/tests/profile_ui_cross_project.mjs +4 -4
  324. package/tests/project_invite_suggestions.mjs +2 -2
  325. package/tests/promote_goal_id.mjs +1 -1
  326. package/tests/publish_manifest.mjs +1 -1
  327. package/tests/rank_floor_wiring.mjs +1 -1
  328. package/tests/regrade_actual_minutes.mjs +1 -1
  329. package/tests/removal_proposal.mjs +2 -2
  330. package/tests/reward_gate.mjs +4 -4
  331. package/tests/reward_mode_cost_plus_only.mjs +1 -1
  332. package/tests/role_responsibilities.mjs +18 -12
  333. package/tests/role_stats_db.mjs +17 -16
  334. package/tests/scouting_rollup_fanout.mjs +5 -5
  335. package/tests/seed_tasks.mjs +5 -2
  336. package/tests/session_earnings_mirror_db.mjs +1 -1
  337. package/tests/session_reward_on_land_db.mjs +1 -1
  338. package/tests/session_reward_reconcile_db.mjs +2 -2
  339. package/tests/sessions_kit_pager.mjs +1 -1
  340. package/tests/settings_art_key_absent.mjs +2 -2
  341. package/tests/settings_main_role.mjs +21 -20
  342. package/tests/setup_flow.mjs +3 -3
  343. package/tests/setup_smoke.mjs +20 -0
  344. package/tests/ship_card.mjs +6 -6
  345. package/tests/specialities.mjs +16 -4
  346. package/tests/speciality_db_shape.mjs +6 -6
  347. package/tests/speciality_injection.mjs +17 -17
  348. package/tests/speciality_offer_ui.mjs +5 -5
  349. package/tests/speciality_offered.mjs +6 -5
  350. package/tests/speciality_session_skills.mjs +7 -7
  351. package/tests/speciality_skills.mjs +11 -11
  352. package/tests/speciality_walkthrough_ui.mjs +2 -2
  353. package/tests/sso_checkin_identity_spoof.mjs +2 -1
  354. package/tests/task_classifier_vocab.mjs +10 -0
  355. package/tests/task_cli.mjs +11 -3
  356. package/tests/task_criteria_unlink_db.mjs +2 -2
  357. package/tests/task_discipline_required.mjs +38 -16
  358. package/tests/task_goal_required.mjs +4 -4
  359. package/tests/task_needs_db.mjs +1 -1
  360. package/tests/tweak_replaces_artist_review.mjs +1 -1
  361. package/tests/update_pill.mjs +153 -2
  362. package/tests/upkeep_home.mjs +1 -1
  363. package/tests/v3_c1_options_proof.mjs +145 -0
  364. package/tests/visual_review.mjs +3 -3
  365. package/tests/watch_applications_queue.mjs +8 -5
  366. package/tests/watch_sealed_floor.mjs +3 -1
  367. package/tests/welcome_boot.mjs +12 -9
  368. package/tests/welcome_boot_browser.mjs +13 -13
  369. package/tests/worktree_helper.mjs +16 -1
@@ -43,11 +43,15 @@ test('an unknown craft is refused', () => {
43
43
  assert.ok(reasons({ needs_craft: 'wizard', what_is_stuck: 'help' }).includes('unknown_craft'));
44
44
  });
45
45
 
46
- test('the crafts are ADR 0174\'s three, plus the ui craft the ui-design module contributes', () => {
46
+ test('the crafts are the two working characters, plus the ui craft the ui-design module contributes', () => {
47
47
  // 'ui' joined the vocabulary with task 1003487, which made the discipline
48
- // selectable everywhere a builder chooses work. The DB CHECK is widened in
49
- // lockstep by modules/lifecycle/migrations/lifecycle_006_help_requests_ui_craft.sql.
50
- assert.deepEqual(help.helpCrafts().sort(), ['artist', 'builder', 'ideator', 'ui']);
48
+ // selectable everywhere a builder chooses work. Since task 1005141 (BONGOS-V3)
49
+ // the help craft IS the discipline word: artisan (was 'builder'/'engineer') and
50
+ // artist (which absorbed 'ideator'). The DB CHECK is widened in lockstep by
51
+ // modules/lifecycle/migrations/lifecycle_021_crafts_artisan_artist.sql.
52
+ assert.deepEqual(help.helpCrafts().sort(), ['artisan', 'artist', 'ui']);
53
+ assert.ok(!help.helpCrafts().includes('builder'), "the old 'builder' help spelling is not offered");
54
+ assert.ok(!help.helpCrafts().includes('ideator'), 'ideator folded into artist');
51
55
  assert.equal(help.disciplineForCraft('ui'), 'ui', 'ui is spelled the same on both sides of the seam');
52
56
  assert.equal(help.craftForDiscipline('ui'), 'ui');
53
57
  });
@@ -57,23 +61,46 @@ test('every craft the app offers is permitted by the help_requests CHECK', () =>
57
61
  // a compiled-in CHECK, and widening one without the other is invisible until an
58
62
  // INSERT fails in production as a 500 on a feature that reads as supported.
59
63
  const sql = fs.readFileSync(
60
- path.join(ROOT, 'modules', 'lifecycle', 'migrations', 'lifecycle_006_help_requests_ui_craft.sql'), 'utf8');
61
- const m = sql.match(/needs_craft IN \(([^)]*)\)/);
64
+ path.join(ROOT, 'modules', 'lifecycle', 'migrations', 'lifecycle_021_crafts_artisan_artist.sql'), 'utf8');
65
+ // The CHECK's list, not the UPDATE's WHERE that renames the old rows above it.
66
+ const m = sql.match(/CHECK \(needs_craft IS NULL OR needs_craft IN \(([^)]*)\)/);
62
67
  assert.ok(m, 'the migration states the permitted craft list');
63
68
  const allowed = m[1].split(',').map((x) => x.trim().replace(/^'|'$/g, ''));
64
69
  for (const c of help.helpCrafts()) {
65
70
  assert.ok(allowed.includes(c), `craft '${c}' is offered by helpCrafts() but the CHECK forbids it`);
66
71
  }
72
+ // A rolled-back release still writes the old spellings, so the CHECK keeps them.
73
+ for (const old of ['builder', 'ideator']) {
74
+ assert.ok(allowed.includes(old), `the CHECK still admits the pre-1005141 spelling '${old}'`);
75
+ }
67
76
  });
68
77
 
69
- test('THE VOCABULARY SEAM: builder maps to the discipline spelled engineer', () => {
70
- // builders.preferred_disciplines spells this craft 'engineer'. If C02 re-derives
71
- // the mapping instead of consuming disciplineForCraft, the two spellings drift
72
- // and a builder silently matches nothing.
73
- assert.equal(help.disciplineForCraft('builder'), 'engineer');
78
+ test('THE VOCABULARY SEAM is closed: the help craft and the discipline are one word', () => {
79
+ // Before task 1005141 the engineering craft was 'builder' here and 'engineer' in
80
+ // builders.preferred_disciplines. Now both say 'artisan'. If C02 re-derives the
81
+ // mapping instead of consuming disciplineForCraft, a builder silently matches nothing.
82
+ assert.equal(help.disciplineForCraft('artisan'), 'artisan');
74
83
  assert.equal(help.disciplineForCraft('artist'), 'artist');
75
- assert.equal(help.disciplineForCraft('ideator'), 'ideator');
76
84
  assert.equal(help.disciplineForCraft('wizard'), null);
85
+ // Old spellings are read as today's craft, on both sides of the seam.
86
+ assert.equal(help.disciplineForCraft('builder'), 'artisan', "the old help spelling 'builder' still resolves");
87
+ assert.equal(help.disciplineForCraft('ideator'), 'artist', 'ideator folded into artist');
88
+ assert.equal(help.craftForDiscipline('engineer'), 'artisan', 'a stored engineer discipline is the artisan craft');
89
+ assert.equal(help.craftForDiscipline('ideator'), 'artist', 'a stored ideator discipline is the artist craft');
90
+ assert.equal(help.craftForDiscipline('artisan'), 'artisan');
91
+ });
92
+
93
+ test("an OLD craft spelling validates and is stored as today's", async () => {
94
+ // An old CLI or a bookmarked form still posts needs_craft 'builder'/'ideator'.
95
+ assert.deepEqual(reasons({ needs_craft: 'builder', what_is_stuck: 'help' }), []);
96
+ assert.deepEqual(reasons({ needs_craft: 'ideator', what_is_stuck: 'help' }), []);
97
+ assert.deepEqual(reasons({ needs_craft: 'engineer', what_is_stuck: 'help' }), []);
98
+ const calls = [];
99
+ const pool = { query: async (sql, params) => { calls.push(params); return { rows: [{ id: 1 }] }; } };
100
+ await help.createHelpRequest({ requestedBy: 1, needsCraft: 'builder', whatIsStuck: 'x' }, { pool });
101
+ await help.createHelpRequest({ requestedBy: 1, needsCraft: 'ideator', whatIsStuck: 'x' }, { pool });
102
+ assert.equal(calls[0][1], 'artisan', "'builder' is stored as 'artisan'");
103
+ assert.equal(calls[1][1], 'artist', "'ideator' is stored as 'artist'");
77
104
  });
78
105
 
79
106
  // ---- what is stuck --------------------------------------------------------
@@ -98,7 +125,7 @@ test('a request may name a task OR a goal, never both', () => {
98
125
  });
99
126
 
100
127
  test('context is optional', () => {
101
- assert.deepEqual(reasons({ needs_craft: 'ideator', what_is_stuck: 'a shape question' }), []);
128
+ assert.deepEqual(reasons({ needs_craft: 'artist', what_is_stuck: 'a shape question' }), []);
102
129
  });
103
130
 
104
131
  // ---- the schema keeps the same promises -----------------------------------
@@ -106,7 +133,10 @@ test('the CHECK constraints mirror the validator, so neither is the only guard',
106
133
  // The validator exists so a refusal names the field; the constraints are the
107
134
  // authority. Both must exist or one of the two promises is unkept.
108
135
  const sql = read('modules', 'lifecycle', 'migrations', 'lifecycle_004_help_requests.sql');
136
+ // lifecycle_004 is history (its vocabulary was ADR 0174's); the live CHECK is lifecycle_021's.
109
137
  assert.match(sql, /needs_craft IN \('artist', 'builder', 'ideator'\)/);
138
+ assert.match(read('modules', 'lifecycle', 'migrations', 'lifecycle_021_crafts_artisan_artist.sql'),
139
+ /needs_craft IN \('artisan', 'artist'/, 'the renamed CHECK names the new crafts');
110
140
  assert.match(sql, /\(needs_craft IS NOT NULL\) <> \(needs_builder_id IS NOT NULL\)/, 'exactly-one-addressee');
111
141
  assert.match(sql, /NOT \(task_id IS NOT NULL AND goal_id IS NOT NULL\)/, 'at most one context');
112
142
  assert.match(sql, /length\(btrim\(what_is_stuck\)\) > 0/, 'non-empty description');
@@ -350,9 +380,11 @@ test('the /me read is IN the fan-out, not a serialized await before it', () => {
350
380
  test('the count is ONE query, and its craft map is generated from the JS map', () => {
351
381
  // Two sequential queries on a hot path is a cost every builder pays on every
352
382
  // open. The SQL CASE is generated from DISCIPLINE_TO_CRAFT so the two spellings
353
- // of 'engineer' cannot drift apart.
383
+ // of a craft cannot drift apart — and a stored old discipline still maps.
354
384
  const sql = help.craftMapSql('d');
355
- assert.match(sql, /WHEN 'engineer' THEN 'builder'/);
385
+ assert.match(sql, /WHEN 'artisan' THEN 'artisan'/);
386
+ assert.match(sql, /WHEN 'engineer' THEN 'artisan'/, 'a legacy engineer discipline still counts');
387
+ assert.match(sql, /WHEN 'ideator' THEN 'artist'/, 'a legacy ideator discipline still counts');
356
388
  for (const craft of help.helpCrafts()) {
357
389
  const disc = help.disciplineForCraft(craft);
358
390
  assert.ok(sql.includes(`WHEN '${disc}' THEN '${craft}'`), `${disc}->${craft} is in the SQL map`);
package/tests/helpers.mjs CHANGED
@@ -540,11 +540,11 @@ const PROFILE_CRAFTS_JS = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/publi
540
540
  //
541
541
  // `roles` is the profile's roles.shown, main first (task 1004435): one Activity
542
542
  // tab each. The default leads with the ideator, so the trail is the first tab
543
- // and a suite that reads it needs no switch; switchCraft('engineer') opens the
543
+ // and a suite that reads it needs no switch; switchCraft('artisan') opens the
544
544
  // works and goals, switchCraft('artist') the approved pages.
545
545
  // `search` is the page's ?query (task 1005050: ?id= decides whose profile it is), and
546
546
  // `withOwn` also loads profile-own.js, the "Just you" screen drawn on your own profile.
547
- export function bootProfilePage({ signedIn = true, trail = [], roles = ['ideator', 'engineer'], artistPages = [], search = '', withOwn = false } = {}) {
547
+ export function bootProfilePage({ signedIn = true, trail = [], roles = ['artist', 'artisan'], artistPages = [], search = '', withOwn = false } = {}) {
548
548
  const els = new Map();
549
549
  const makeEl = () => ({
550
550
  dataset: {}, style: {}, textContent: '', hidden: false, _html: '',
@@ -579,7 +579,7 @@ export function bootProfilePage({ signedIn = true, trail = [], roles = ['ideator
579
579
  }
580
580
  if (url.includes('/activity?')) {
581
581
  const isGoals = url.includes('section=goals');
582
- return { status: 200, ok: true, data: { rows: isGoals ? [{ id: 7, title: 'A goal', disciplines: ['artist'], task_shipped: 1, task_total: 2 }] : [{ id: 42, title: 'A work', discipline: 'engineer', shipped_at: '2026-08-01T00:00:00Z' }], total: 1 } };
582
+ return { status: 200, ok: true, data: { rows: isGoals ? [{ id: 7, title: 'A goal', disciplines: ['artist'], task_shipped: 1, task_total: 2 }] : [{ id: 42, title: 'A work', discipline: 'artisan', shipped_at: '2026-08-01T00:00:00Z' }], total: 1 } };
583
583
  }
584
584
  if (url.includes('/copy-desk/artists/')) {
585
585
  return { status: 200, ok: true, data: { rows: artistPages, total: artistPages.length } };
@@ -632,7 +632,7 @@ export function bootProfilePage({ signedIn = true, trail = [], roles = ['ideator
632
632
  // the same reason categoryChip is: an undefined helper throws mid-render.
633
633
  roleAxisLabel: () => 'Role',
634
634
  roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
635
- roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
635
+ roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
636
636
  roleIsUnset: (d) => !d || d === 'unclassified',
637
637
  ROLE_UNSET: 'unclassified',
638
638
  escapeHtml: realOTB().escapeHtml, // the real one (task 1004876)
@@ -419,16 +419,18 @@ await test('on the builder\'s Home the core and the museum are faded; a craft\'s
419
419
  assert.match(css, /\.hc-quiet:hover,\s*\.hc-svg \.hc-quiet:focus-visible\s*\{[^}]*filter:\s*none/, 'back to colour under the pointer or the keyboard');
420
420
  });
421
421
 
422
- await test('a building opens its goal; the museum and the band of stars are doors to the artist\'s and the ideator\'s Home', () => {
422
+ await test('a building opens its goal; the museum and the band of stars are both doors to the artist\'s Home (the ideator folded in, task 1005141)', () => {
423
423
  const m = Draw.cityModel(FIXTURE);
424
424
  const ls = linksOf(all(Draw.citySvg(m, m.W - 1, { mode: 'dark', gov: Gov }).nodes));
425
425
  const towers = ls.filter((n) => n.attrs.class === 'hc-tower');
426
426
  assert.equal(towers.length, m.areas.length);
427
427
  towers.forEach((n) => assert.match(n.attrs.href, /^\/goals#\/goal\/\d+$/));
428
428
  assert.ok(m.areas.every((a) => towers.some((n) => n.attrs.href === `/goals#/goal/${a.id}`)), 'each area links to its own goal');
429
- assert.equal(ls.find((n) => /hc-museum/.test(n.attrs.class)).attrs['data-craft-door'], 'artistry');
429
+ assert.equal(ls.find((n) => /hc-museum/.test(n.attrs.class)).attrs['data-craft-door'], 'artist');
430
430
  const sky = ls.find((n) => /hc-sky/.test(n.attrs.class));
431
- assert.equal(sky.attrs['data-craft-door'], 'ideating');
431
+ assert.equal(sky.attrs['data-craft-door'], undefined, 'the stars switch no mode');
432
+ assert.equal(sky.attrs.href, '/thinking', 'they open the sky itself, the old ideator room');
433
+ assert.ok(ls.every((n) => !/^(artistry|ideating|governing|building)$/.test(n.attrs['data-craft-door'] || '')), 'no door names a pre-rename mode');
432
434
  assert.ok(ls.every((n) => n.attrs['aria-label']), 'every door is named for a screen reader');
433
435
  const stars = all([sky]).filter((n) => n.tag === 'circle');
434
436
  const box = Draw.viewBoxOf(m);
@@ -536,7 +538,7 @@ await test('outlines never show through a nearer building, and the three doors l
536
538
  const group = (k) => hits.filter((h) => h.attrs['data-group'] === k);
537
539
  assert.ok(group('area').every((h) => h.attrs.href === '/goals'), 'Building opens the goals');
538
540
  assert.ok(group('civic').every((h) => h.attrs.href === Gov.STREET_HREF), 'Government opens the street');
539
- assert.ok(group('museum').every((h) => h.attrs['data-craft-door'] === 'artistry'), 'Art switches to Artistry');
541
+ assert.ok(group('museum').every((h) => h.attrs['data-craft-door'] === 'artist'), 'Art switches to Artist');
540
542
  ['area', 'civic', 'museum'].forEach((k) => {
541
543
  const g = group(k);
542
544
  assert.ok(g.length > 0);
@@ -154,31 +154,33 @@ test('with reduced motion nothing glides: every control jumps straight to where
154
154
  assert.equal(s.t, 0);
155
155
  });
156
156
 
157
- test('the museum and the band of stars switch the craft; the core and a building are plain links', { skip }, async () => {
157
+ test('the museum switches the craft to Artist; the band of stars, the core and a building are plain links (the stars open Your thinking since task 1005141)', { skip }, async () => {
158
158
  const { ctx, page } = await boot();
159
159
  await page.waitForFunction(() => document.querySelectorAll('.hc-rung').length > 0);
160
- await page.evaluate(() => { document.querySelector('.hc-svg a.hc-museum').dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); document.querySelector('.hc-svg a.hc-sky').dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); });
161
- const r = await page.evaluate(() => ({ chosen: window.__chosen, core: document.querySelector('.hc-svg a.hc-core').getAttribute('href'), tower: document.querySelector('.hc-svg a.hc-tower').getAttribute('href') }));
160
+ await page.evaluate(() => { document.querySelector('.hc-svg a.hc-museum').dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); });
161
+ const r = await page.evaluate(() => ({ chosen: window.__chosen, sky: document.querySelector('.hc-svg a.hc-sky').getAttribute('href'), skyDoor: document.querySelector('.hc-svg a.hc-sky').getAttribute('data-craft-door'), core: document.querySelector('.hc-svg a.hc-core').getAttribute('href'), tower: document.querySelector('.hc-svg a.hc-tower').getAttribute('href') }));
162
162
  await ctx.close();
163
- assert.deepEqual(r.chosen, ['artistry', 'ideating']);
163
+ assert.deepEqual(r.chosen, ['artist'], 'the museum opens the artist home');
164
+ assert.equal(r.sky, '/thinking', 'the stars open the sky itself');
165
+ assert.equal(r.skyDoor, null, 'and switch no craft');
164
166
  assert.equal(r.core, '/builders/street');
165
167
  assert.match(r.tower, /^\/goals#\/goal\/\d+$/);
166
168
  });
167
169
 
168
- test('the governor\'s Home is the street: Governing on Home goes there, opened or chosen; other crafts stay', { skip }, async () => {
169
- const opened = await boot({ craft: 'governing' });
170
+ test('the governor\'s Home is the street: Patron on Home goes there, opened or chosen; other crafts stay', { skip }, async () => {
171
+ const opened = await boot({ craft: 'patron' });
170
172
  const onOpen = await opened.page.evaluate(() => ({ hops: window.__hops, asked: window.__asked, built: !!document.querySelector('.hc-svg') }));
171
173
  await opened.ctx.close();
172
- assert.deepEqual(onOpen.hops, ['/builders/street'], 'Home opened with Governing chosen goes to the street');
174
+ assert.deepEqual(onOpen.hops, ['/builders/street'], 'Home opened with Patron chosen goes to the street');
173
175
  assert.deepEqual(onOpen.asked, [], 'and asks the server for nothing on the way');
174
176
  assert.equal(onOpen.built, false, 'and builds no city it is leaving');
175
177
 
176
178
  const { ctx, page } = await boot();
177
179
  await page.waitForFunction(() => document.querySelectorAll('.hc-rung').length > 0);
178
180
  const flip = (m) => page.evaluate((mode) => { document.documentElement.dataset.craft = mode; window.__craft = mode; document.dispatchEvent(new CustomEvent('otb:craft', { detail: { mode } })); return window.__hops.slice(); }, m);
179
- assert.deepEqual(await flip('artistry'), [], 'Artistry stays on Home');
180
- assert.deepEqual(await flip('ideating'), [], 'Ideating stays on Home');
181
- assert.deepEqual(await flip('governing'), ['/builders/street'], 'choosing Governing on Home goes to the street');
181
+ assert.deepEqual(await flip('artist'), [], 'Artist stays on Home');
182
+ assert.deepEqual(await flip('artisan'), [], 'Artisan stays on Home');
183
+ assert.deepEqual(await flip('patron'), ['/builders/street'], 'choosing Patron on Home goes to the street');
182
184
  await ctx.close();
183
185
  });
184
186
 
@@ -201,6 +203,23 @@ test('a phone width scrolls nothing sideways', { skip }, async () => {
201
203
  });
202
204
 
203
205
  // ---- the ideator's sky (task 1005059) ----
206
+ // Since task 1005141 no craft lands on the sky: Ideating folded into Artist, and craft-mode.js
207
+ // reads a stored 'ideating' as 'artist' (tests/craft_mode.mjs). The sky view stays in
208
+ // home-city.js behind one switch so it can come back as its own view, so the cases below
209
+ // still drive it by handing the widget a raw 'ideating' — a craft the real switch never
210
+ // returns any more. The first case pins the new truth: every real craft draws the city.
211
+
212
+ test('NO CRAFT LANDS ON THE SKY (task 1005141): Artist, Artisan and no mode all draw the city', { skip }, async () => {
213
+ for (const craft of ['artist', 'artisan', null]) {
214
+ const { ctx, page } = await boot({ craft });
215
+ await page.waitForFunction(() => document.querySelectorAll('.hc-rung').length > 0);
216
+ const r = await page.evaluate(() => ({ sky: document.getElementById('home-city').classList.contains('hc-is-sky'), towers: document.querySelectorAll('.hc-tower').length, hits: document.querySelectorAll('.hc-hit').length }));
217
+ await ctx.close();
218
+ assert.equal(r.sky, false, `${craft}: no sky`);
219
+ assert.ok(r.towers > 0, `${craft}: the city's towers`);
220
+ assert.equal(r.hits, 0, `${craft}: no skyline hit groups`);
221
+ }
222
+ });
204
223
 
205
224
  // a point inside the hit group that the group itself takes (a nearer building may cover part of it)
206
225
  const pointOf = (page, group) => page.evaluate((g) => {
@@ -215,7 +234,7 @@ const pointOf = (page, group) => page.evaluate((g) => {
215
234
  }, group);
216
235
  const lit = (page) => page.evaluate(() => Object.fromEntries(['area', 'civic', 'museum'].map((g) => [g, [...document.querySelectorAll('.hc-ol-' + g)].every((o) => Number(getComputedStyle(o).strokeOpacity) > 0.5)])));
217
236
 
218
- test('in Ideating, Home draws the sky over the skyline from the same one read, and the ladder ranks ideators', { skip }, async () => {
237
+ test('the dormant sky view, handed a raw ideating, draws the sky over the skyline from the same one read, and the ladder ranks ideators', { skip }, async () => {
219
238
  const { ctx, page } = await boot({ craft: 'ideating' });
220
239
  await page.waitForFunction(() => document.querySelectorAll('.hc-rung').length > 0);
221
240
  const r = await page.evaluate(() => ({
@@ -251,7 +270,7 @@ test('hovering the skyline outlines exactly one whole group: Building, Governmen
251
270
  assert.deepEqual(rest, { area: false, civic: false, museum: false });
252
271
  });
253
272
 
254
- test('each group is one door: the goals, the street, and the artist\'s Home', { skip }, async () => {
273
+ test('in the dormant sky, each group is one door: the goals, the street, and the artist\'s Home', { skip }, async () => {
255
274
  const { ctx, page } = await boot({ craft: 'ideating' });
256
275
  await page.waitForFunction(() => document.querySelectorAll('.hc-hit').length > 0);
257
276
  await page.evaluate(() => document.querySelector('.hc-hit[data-group="museum"]').dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })));
@@ -263,7 +282,7 @@ test('each group is one door: the goals, the street, and the artist\'s Home', {
263
282
  small: [...document.querySelectorAll('.hc-hit')].map((a) => a.getBoundingClientRect()).filter((b) => b.height < 24).length,
264
283
  }));
265
284
  await ctx.close();
266
- assert.deepEqual(r.chosen, ['artistry']);
285
+ assert.deepEqual(r.chosen, ['artist']);
267
286
  assert.deepEqual(r.area, ['/goals']);
268
287
  assert.deepEqual(r.civic, ['/builders/street']);
269
288
  assert.deepEqual(r.stops.sort(), ['area', 'civic', 'museum']);
@@ -273,7 +292,7 @@ test('each group is one door: the goals, the street, and the artist\'s Home', {
273
292
  test('switching the craft swaps the sky and the city in place, with no second read', { skip }, async () => {
274
293
  const { ctx, page } = await boot({ craft: 'ideating' });
275
294
  await page.waitForFunction(() => document.querySelectorAll('.hc-hit').length > 0);
276
- await page.evaluate(() => { window.__craft = 'building'; document.dispatchEvent(new CustomEvent('otb:craft', { detail: { mode: 'building' } })); });
295
+ await page.evaluate(() => { window.__craft = 'artisan'; document.dispatchEvent(new CustomEvent('otb:craft', { detail: { mode: 'artisan' } })); });
277
296
  const r = await page.evaluate(() => ({ asked: window.__asked.length, sky: document.getElementById('home-city').classList.contains('hc-is-sky'), towers: document.querySelectorAll('.hc-tower').length, hits: document.querySelectorAll('.hc-hit').length }));
278
297
  await ctx.close();
279
298
  assert.deepEqual(r, { asked: 1, sky: false, towers: 10, hits: 0 });
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // Runs modules/hall-ui/public/home-gallery.js (and, for the craft switch, the city's
5
5
  // three files) in a real page with the reads stubbed by the hall-preview fixture: the
6
- // wall is asked for once, only in Artistry, Most popular first; the arrows and keys walk
6
+ // wall is asked for once, only in Artist (Artistry until task 1005141), Most popular first; the arrows and keys walk
7
7
  // it; the label opens closer on a click; the heart is the viewer's own, given and taken
8
8
  // back, and never changes size; Latest asks again in that order; full screen keeps the
9
9
  // signature and heart and drops the label; the full gallery hangs every work and a click
@@ -199,17 +199,17 @@ test('at phone width the gallery fits: nothing scrolls sideways and every contro
199
199
  assert.deepEqual(m.small, []);
200
200
  });
201
201
 
202
- test('Home is the gallery only in Artistry: the city\'s widget switches on the craft and asks for the wall only then', { skip }, async () => {
202
+ test('Home is the gallery only in Artist: the city\'s widget switches on the craft and asks for the wall only then', { skip }, async () => {
203
203
  const { ctx, page } = await boot({ city: true });
204
204
  await page.evaluate(() => window.__widget.mount(document.getElementById('home-city')));
205
205
  await page.waitForFunction(() => document.querySelectorAll('.hc-rung').length > 0);
206
206
  const building = await page.evaluate(() => ({ asked: window.__asked.slice(), gallery: getComputedStyle(document.querySelector('.hg')).display, stage: getComputedStyle(document.querySelector('.hc-stage')).display }));
207
207
  assert.deepEqual(building.asked, ['GET /api/bongos/home/city?weeks=10'], 'the builder\'s Home never asks for the wall');
208
208
  assert.equal(building.gallery, 'none');
209
- await page.evaluate(() => { document.documentElement.dataset.craft = 'artistry'; document.dispatchEvent(new CustomEvent('otb:craft', { detail: { mode: 'artistry' } })); });
209
+ await page.evaluate(() => { document.documentElement.dataset.craft = 'artist'; document.dispatchEvent(new CustomEvent('otb:craft', { detail: { mode: 'artist' } })); });
210
210
  await page.waitForFunction(() => document.querySelectorAll('.hg-hang').length > 0);
211
211
  const art = await page.evaluate(() => ({ asked: window.__asked.slice(), gallery: getComputedStyle(document.querySelector('.hg')).display, stage: getComputedStyle(document.querySelector('.hc-stage')).display, hidden: document.getElementById('home-city').hidden }));
212
- await page.evaluate(() => { document.documentElement.dataset.craft = 'building'; document.dispatchEvent(new CustomEvent('otb:craft', { detail: { mode: 'building' } })); });
212
+ await page.evaluate(() => { document.documentElement.dataset.craft = 'artisan'; document.dispatchEvent(new CustomEvent('otb:craft', { detail: { mode: 'artisan' } })); });
213
213
  const back = await page.evaluate(() => getComputedStyle(document.querySelector('.hc-stage')).display);
214
214
  await ctx.close();
215
215
  assert.deepEqual(art.asked.slice(1), ['GET /api/bongos/home/gallery?order=popular']);
@@ -2,11 +2,11 @@
2
2
  // goal 1000095, the owner's work-first hall).
3
3
  //
4
4
  // Runs modules/hall-ui/public/home-idea-box.js with the city's three files in a real
5
- // page, the reads stubbed by the hall-preview fixture: the box shows only in Ideating
5
+ // page, the reads stubbed by the hall-preview fixture: the box shows only in the artist's Home
6
6
  // and sits above the sky; it is ONE box and a button (owner, 2026-11-05); filing is
7
7
  // POST /inbox with { title, body_md: '', kind: 'feature' }; an empty line files
8
8
  // nothing; a refused filing keeps the words and says why; a filed one clears and links
9
- // to Your thinking; the box shows in Ideating even when the sky could not be read; at
9
+ // to Your thinking; the box shows in the artist's Home even when the sky could not be read; at
10
10
  // phone width nothing scrolls sideways. The pure halves run in node. Skips the browser
11
11
  // cases where no system Edge or Chrome is installed.
12
12
  //
@@ -45,7 +45,7 @@ const stubs = (craft, { city = true, refuse = false } = {}) => `
45
45
  window.OTBCraftMode = { iconHtml: () => '', choose: () => {}, current: () => document.documentElement.dataset.craft || null };
46
46
  window.OTB = { contributeHallWidget(w) { window.__widget = w; } };`;
47
47
 
48
- async function boot({ craft = 'ideating', width = 1440, ...opts } = {}) {
48
+ async function boot({ craft = 'artist', width = 1440, ...opts } = {}) {
49
49
  const ctx = await browser.newContext({ viewport: { width, height: 900 }, reducedMotion: 'reduce' });
50
50
  const page = await ctx.newPage();
51
51
  await page.setContent(`<!doctype html><html data-theme="dark"${craft ? ` data-craft="${craft}"` : ''}><head><style>${CSS}</style></head><body><section id="welcome-scroll" class="scroll">setup guide</section><section id="home-city" class="scroll home-city" hidden></section></body></html>`);
@@ -65,7 +65,7 @@ test('the pure halves: the first line is the title and the rest the details; a r
65
65
  assert.match(Box.explain(null), /Nothing was saved/);
66
66
  });
67
67
 
68
- test('in Ideating the box is the first thing in the section, ONE box and a button, and the setup guide is hidden', { skip }, async () => {
68
+ test('in the Artist Home the box is the first thing in the section, ONE box and a button, and the setup guide is hidden', { skip }, async () => {
69
69
  const { ctx, page } = await boot();
70
70
  await page.waitForFunction(() => document.querySelector('.hc-svg').childElementCount > 0);
71
71
  const s = await page.evaluate(() => {
@@ -92,7 +92,7 @@ test('in Ideating the box is the first thing in the section, ONE box and a butto
92
92
  });
93
93
 
94
94
  test('in any other craft, or none, there is no box and the setup guide shows', { skip }, async () => {
95
- for (const craft of ['building', null]) {
95
+ for (const craft of ['artisan', null]) {
96
96
  const { ctx, page } = await boot({ craft });
97
97
  const s = await page.evaluate(() => ({ hidden: document.querySelector('.hib').hidden, guide: getComputedStyle(document.getElementById('welcome-scroll')).display }));
98
98
  await ctx.close();
@@ -159,7 +159,7 @@ test('a refused filing keeps the words and says why', { skip }, async () => {
159
159
  assert.equal(s.link, false);
160
160
  });
161
161
 
162
- test('the box shows in Ideating even when the sky could not be read', { skip }, async () => {
162
+ test('the box shows in the Artist Home even when the sky could not be read', { skip }, async () => {
163
163
  const { ctx, page } = await boot({ city: false });
164
164
  const s = await page.evaluate(() => ({ host: document.getElementById('home-city').hidden, box: document.querySelector('.hib').hidden }));
165
165
  await ctx.close();
@@ -205,7 +205,7 @@ const find = (n, cls) => {
205
205
  return null;
206
206
  };
207
207
 
208
- const ROW = (place, handle, credits, extra = {}) => ({ place, handle, github_login: handle, display_name: handle.toUpperCase(), avatar_url: `https://avatars.example/${handle}`, credits, tasks_shipped: 2, karma: 1, disciplines: ['engineer'], ...extra });
208
+ const ROW = (place, handle, credits, extra = {}) => ({ place, handle, github_login: handle, display_name: handle.toUpperCase(), avatar_url: `https://avatars.example/${handle}`, credits, tasks_shipped: 2, karma: 1, disciplines: ['artisan'], ...extra });
209
209
  const WEEK = () => ({ week: { start: '2026-10-19', end: '2026-10-26' },
210
210
  winner: { week_start: '2026-10-19', handle: 'ana', github_login: 'ana', display_name: 'Ana <i>L</i>', avatar_url: 'https://avatars.example/ana', credits: 1240, tasks_shipped: 1 },
211
211
  past: [{ week_start: '2026-10-12', handle: 'bo', github_login: 'bo', display_name: null, avatar_url: 'data:x', credits: 300, tasks_shipped: 2 },
@@ -229,7 +229,7 @@ const READS = () => ({
229
229
  '/community/leaderboard?window=all': { status: 200, body: { window: 'all', count: 99, results: [ROW(26, 'ana', 1200), ROW(27, 'bo', 1500, { tasks_shipped: 1, karma: 0, avatar_url: 'javascript:alert(1)', display_name: null })] } },
230
230
  '/community/leaderboard?window=30d': { status: 200, body: { window: '30d', results: [ROW(1, 'cy', 40)] } },
231
231
  '/community/leaderboard?window=all&discipline=artist': { status: 200, body: { window: 'all', results: [] } },
232
- '/community/search?q=pri': { status: 200, body: { results: [{ handle: 'priya', github_login: 'priyan', display_name: 'Priya', avatar_url: null, disciplines: ['engineer', 'ui'] }, { handle: 'pri-hidden', github_login: 'ph', display_name: 'Pri <b>H</b>', avatar_url: null, disciplines: null }] } },
232
+ '/community/search?q=pri': { status: 200, body: { results: [{ handle: 'priya', github_login: 'priyan', display_name: 'Priya', avatar_url: null, disciplines: ['artisan', 'ui'] }, { handle: 'pri-hidden', github_login: 'ph', display_name: 'Pri <b>H</b>', avatar_url: null, disciplines: null }] } },
233
233
  '/community/search?q=pri&discipline=artist': { status: 200, body: { results: [] } },
234
234
  '/scouting': { status: 403, body: { error: { code: 'forbidden' } } },
235
235
  });
@@ -465,7 +465,7 @@ test('find: nothing read on arrival; too short is refused before any read; a hid
465
465
  assert.equal(ui.gets().pop(), '/api/bongos/community/search?q=pri');
466
466
  assert.equal(ui.$('findStatus').textContent, '2 builders found.');
467
467
  const [priya, hidden] = kids(ui.$('findList'));
468
- assert.equal(find(priya, 'mineRow__hint').textContent, 'engineer · designer');
468
+ assert.equal(find(priya, 'mineRow__hint').textContent, 'artisan · designer');
469
469
  assert.equal(find(hidden, 'mineRow__name').textContent, 'Pri <b>H</b>', 'textContent: a name is never markup');
470
470
  assert.equal(find(hidden, 'mineRow__hint'), null, 'disciplines null (hidden numbers): nothing said about their craft');
471
471
  assert.equal(ui.$('findBtn').disabled, false);