@bongos/core 1.24.79 → 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 (366) hide show
  1. package/.bongos-core.json +495 -350
  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 +2 -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 +70 -0
  167. package/scripts/bongos/autobongos-loop.js +1 -1
  168. package/scripts/bongos/discipline-modes.json +7 -12
  169. package/scripts/bongos/fitness.js +1 -1
  170. package/scripts/bongos/gen-hall-sitemap.js +7 -7
  171. package/scripts/bongos/grade-smoke.js +2 -2
  172. package/scripts/bongos/init.js +2 -2
  173. package/scripts/bongos/newcomer-floor.config.json +1 -2
  174. package/scripts/bongos/newcomer-floor.js +4 -3
  175. package/scripts/bongos/newcomer-floor.templates.json +10 -12
  176. package/scripts/bongos/newcomer-floor.test.js +47 -38
  177. package/scripts/bongos/role-pack-guard.js +5 -4
  178. package/scripts/bongos/seed-tasks.example.json +3 -3
  179. package/scripts/bongos/setup.js +20 -107
  180. package/scripts/bongos/start.js +2 -2
  181. package/scripts/bongos/task.js +5 -4
  182. package/scripts/bongos/triage.js +1 -1
  183. package/scripts/bongos/worktree.js +16 -4
  184. package/scripts/setup-builder.js +6 -46
  185. package/src/bongos/auth-admission.js +8 -8
  186. package/src/bongos/craft-mode.js +2 -0
  187. package/src/bongos/db.js +39 -1
  188. package/src/bongos/github-repos.js +78 -0
  189. package/src/bongos/module-scope-map.js +22 -0
  190. package/src/bongos/role-stats.js +27 -11
  191. package/src/bongos/routes/github.js +100 -2
  192. package/src/bongos/routes/me.js +33 -0
  193. package/src/bongos/serve-internal.js +26 -9
  194. package/src/crafts.js +66 -0
  195. package/src/module-api.js +6 -1
  196. package/src/module-loader/manifest-schema.js +16 -0
  197. package/src/modules.js +2 -2
  198. package/src/role-responsibilities.js +6 -5
  199. package/tests/acclaim_feed_evidence_db.mjs +1 -1
  200. package/tests/activity_rollup_as_of.mjs +1 -1
  201. package/tests/api_alias_caller_ratchet.mjs +3 -3
  202. package/tests/application_lifecycle.mjs +1 -1
  203. package/tests/artist_review.mjs +1 -1
  204. package/tests/artist_review_goal.mjs +1 -1
  205. package/tests/blocker_hall_live_proof.mjs +1 -1
  206. package/tests/blocker_tasks_named_db.mjs +2 -2
  207. package/tests/canonical_profile_url.mjs +1 -1
  208. package/tests/cap_newcomer_tasks.mjs +13 -13
  209. package/tests/character_creation.mjs +294 -0
  210. package/tests/claim_gate_open_blocker.mjs +2 -2
  211. package/tests/claim_gate_rebase.mjs +6 -6
  212. package/tests/claude_materialize.mjs +1 -1
  213. package/tests/clawback_confirmed_backlog.mjs +3 -3
  214. package/tests/collab_page.mjs +8 -9
  215. package/tests/collab_self_settling.mjs +2 -2
  216. package/tests/community_leaderboard_route.mjs +13 -5
  217. package/tests/community_proof_db.mjs +3 -0
  218. package/tests/community_search_route.mjs +7 -6
  219. package/tests/conductor.mjs +1 -1
  220. package/tests/conductor_main_e2e.mjs +8 -8
  221. package/tests/copy_desk_flags_db.mjs +1 -1
  222. package/tests/copy_desk_page_approve.mjs +2 -2
  223. package/tests/copy_desk_page_claim.mjs +2 -2
  224. package/tests/copy_desk_page_next.mjs +1 -1
  225. package/tests/copy_desk_page_release.mjs +1 -1
  226. package/tests/copy_desk_page_submit.mjs +2 -2
  227. package/tests/core_258_box_session_claims_db.mjs +1 -1
  228. package/tests/craft_mode.mjs +48 -34
  229. package/tests/craft_mode_home.mjs +20 -20
  230. package/tests/craft_mode_looks.mjs +16 -15
  231. package/tests/craft_mode_nav.mjs +31 -29
  232. package/tests/craft_review_mode.mjs +46 -41
  233. package/tests/crafts_vocabulary.mjs +70 -0
  234. package/tests/cross_discipline_priority_direction.mjs +5 -5
  235. package/tests/fitness.mjs +7 -11
  236. package/tests/funded_project_pays_proof_db.mjs +1 -1
  237. package/tests/genesis_home.mjs +21 -1
  238. package/tests/github_chip.mjs +3 -3
  239. package/tests/github_repo_invite.mjs +379 -0
  240. package/tests/goal_criterion_confirm_ui.mjs +1 -1
  241. package/tests/goal_invite_consent.mjs +1 -1
  242. package/tests/goal_map_page.mjs +2 -2
  243. package/tests/goal_sort_staleness.mjs +1 -1
  244. package/tests/goal_spec_task.mjs +1 -1
  245. package/tests/goal_spec_task_db.mjs +3 -3
  246. package/tests/grade_engine.mjs +2 -2
  247. package/tests/grader_no_artifact.mjs +1 -1
  248. package/tests/grader_prompt_fencing.mjs +1 -1
  249. package/tests/grader_prompt_window_bound.mjs +1 -1
  250. package/tests/grader_reader_lens.mjs +2 -2
  251. package/tests/grader_severity_gate.mjs +1 -1
  252. package/tests/grader_unbacked_score.mjs +1 -1
  253. package/tests/hall_approval_queue.mjs +1 -1
  254. package/tests/hall_board_world.mjs +2 -2
  255. package/tests/hall_builders_page_pass.mjs +1 -1
  256. package/tests/hall_city.mjs +1 -1
  257. package/tests/hall_fullscreen.mjs +27 -3
  258. package/tests/hall_kit_charts.mjs +1 -1
  259. package/tests/hall_landing_boot.mjs +1 -1
  260. package/tests/hall_my_agents.mjs +16 -16
  261. package/tests/hall_nav.mjs +22 -15
  262. package/tests/hall_options_menu.mjs +247 -0
  263. package/tests/hall_oversight_world.mjs +1 -1
  264. package/tests/hall_palette.mjs +8 -9
  265. package/tests/hall_profile_world.mjs +3 -3
  266. package/tests/hall_record_world.mjs +2 -2
  267. package/tests/hall_settings_world.mjs +2 -1
  268. package/tests/hall_sitemap.mjs +6 -5
  269. package/tests/hall_start_screen.mjs +244 -0
  270. package/tests/hall_street.mjs +2 -2
  271. package/tests/hall_street_inside.mjs +17 -9
  272. package/tests/hall_studio_world.mjs +3 -3
  273. package/tests/hall_visor.mjs +224 -0
  274. package/tests/help_requests.mjs +47 -15
  275. package/tests/helpers.mjs +4 -4
  276. package/tests/home_city.mjs +6 -4
  277. package/tests/home_city_widget.mjs +33 -14
  278. package/tests/home_gallery_widget.mjs +4 -4
  279. package/tests/home_idea_box.mjs +6 -6
  280. package/tests/hub_community_page.mjs +3 -3
  281. package/tests/hub_craft_rollup.mjs +75 -36
  282. package/tests/hub_recent_ships.mjs +3 -2
  283. package/tests/idea_cascade.mjs +2 -2
  284. package/tests/idea_credit_streams_db.mjs +1 -1
  285. package/tests/idea_detail_page.mjs +1 -1
  286. package/tests/idea_ideate_full.mjs +5 -2
  287. package/tests/idea_spark_hall.mjs +1 -1
  288. package/tests/init.mjs +3 -3
  289. package/tests/instance_manifest.mjs +2 -2
  290. package/tests/kickoff_checklist.mjs +1 -1
  291. package/tests/leaderboard_roles.mjs +34 -25
  292. package/tests/lifecycle_claim_ladder_refs.mjs +55 -0
  293. package/tests/lightbulb.mjs +14 -10
  294. package/tests/main_discipline.mjs +8 -8
  295. package/tests/markdown_line_endings.mjs +1 -1
  296. package/tests/market_watch_pages.mjs +52 -74
  297. package/tests/me_streak_leaderboard_queries.mjs +3 -3
  298. package/tests/migration_allocation.mjs +1 -1
  299. package/tests/mingle.mjs +36 -21
  300. package/tests/module-scope-map.mjs +22 -2
  301. package/tests/module_api.mjs +1 -0
  302. package/tests/module_contributions.mjs +13 -13
  303. package/tests/module_manifest.mjs +29 -0
  304. package/tests/module_skills_hook.mjs +2 -2
  305. package/tests/modules.mjs +14 -7
  306. package/tests/nav_permission_atoms.mjs +2 -1
  307. package/tests/needs_rebase.mjs +1 -1
  308. package/tests/newcomer_restock.mjs +13 -13
  309. package/tests/page_notes_panel.mjs +22 -22
  310. package/tests/page_tweak_no_block.mjs +1 -1
  311. package/tests/paintbrush.mjs +7 -7
  312. package/tests/privacy_matrix.mjs +1 -0
  313. package/tests/profile_activity.mjs +20 -23
  314. package/tests/profile_earnings.mjs +1 -1
  315. package/tests/profile_nudge_prefs.mjs +4 -4
  316. package/tests/profile_role_headline.mjs +73 -46
  317. package/tests/profile_role_stats.mjs +55 -45
  318. package/tests/profile_rollup_consent.mjs +19 -6
  319. package/tests/profile_route.mjs +12 -12
  320. package/tests/profile_ui_cross_project.mjs +4 -4
  321. package/tests/project_invite_suggestions.mjs +2 -2
  322. package/tests/promote_goal_id.mjs +1 -1
  323. package/tests/publish_manifest.mjs +1 -1
  324. package/tests/rank_floor_wiring.mjs +1 -1
  325. package/tests/regrade_actual_minutes.mjs +1 -1
  326. package/tests/removal_proposal.mjs +2 -2
  327. package/tests/reward_gate.mjs +4 -4
  328. package/tests/reward_mode_cost_plus_only.mjs +1 -1
  329. package/tests/role_responsibilities.mjs +18 -12
  330. package/tests/role_stats_db.mjs +17 -16
  331. package/tests/scouting_rollup_fanout.mjs +5 -5
  332. package/tests/seed_tasks.mjs +5 -2
  333. package/tests/session_earnings_mirror_db.mjs +1 -1
  334. package/tests/session_reward_on_land_db.mjs +1 -1
  335. package/tests/session_reward_reconcile_db.mjs +2 -2
  336. package/tests/sessions_kit_pager.mjs +1 -1
  337. package/tests/settings_art_key_absent.mjs +2 -2
  338. package/tests/settings_main_role.mjs +21 -20
  339. package/tests/setup_flow.mjs +3 -3
  340. package/tests/setup_smoke.mjs +20 -0
  341. package/tests/ship_card.mjs +6 -6
  342. package/tests/specialities.mjs +16 -4
  343. package/tests/speciality_db_shape.mjs +6 -6
  344. package/tests/speciality_injection.mjs +17 -17
  345. package/tests/speciality_offer_ui.mjs +5 -5
  346. package/tests/speciality_offered.mjs +6 -5
  347. package/tests/speciality_session_skills.mjs +7 -7
  348. package/tests/speciality_skills.mjs +11 -11
  349. package/tests/speciality_walkthrough_ui.mjs +2 -2
  350. package/tests/sso_checkin_identity_spoof.mjs +2 -1
  351. package/tests/task_classifier_vocab.mjs +10 -0
  352. package/tests/task_cli.mjs +11 -3
  353. package/tests/task_criteria_unlink_db.mjs +2 -2
  354. package/tests/task_discipline_required.mjs +38 -16
  355. package/tests/task_goal_required.mjs +4 -4
  356. package/tests/task_needs_db.mjs +1 -1
  357. package/tests/tweak_replaces_artist_review.mjs +1 -1
  358. package/tests/update_pill.mjs +153 -2
  359. package/tests/upkeep_home.mjs +1 -1
  360. package/tests/v3_c1_options_proof.mjs +145 -0
  361. package/tests/visual_review.mjs +3 -3
  362. package/tests/watch_applications_queue.mjs +8 -5
  363. package/tests/watch_sealed_floor.mjs +3 -1
  364. package/tests/welcome_boot.mjs +12 -9
  365. package/tests/welcome_boot_browser.mjs +13 -13
  366. package/tests/worktree_helper.mjs +16 -1
@@ -681,7 +681,7 @@ test('selectAutonomousDispatch: GO + feed → picks rows[0] and shapes the claim
681
681
  const r = dispatch.selectAutonomousDispatch(
682
682
  { decision: 'go', reason: 'signal' },
683
683
  [
684
- { id: 458, title: 'MAS Phase 4.5', version_id: 'GDS-V4', est_minutes: 120, discipline: 'engineer' },
684
+ { id: 458, title: 'MAS Phase 4.5', version_id: 'GDS-V4', est_minutes: 120, discipline: 'artisan' },
685
685
  { id: 999, title: 'lower priority' },
686
686
  ],
687
687
  );
@@ -33,14 +33,14 @@ const SPECIALITY = 'SPECIALITY-CONTRACT: the engineer expert contract.';
33
33
  const ARTIST_SPECIALITY = 'ARTIST-CONTRACT: must not appear in an engineering session.';
34
34
 
35
35
  // The /me payload the hook consumes, in the route's own shape.
36
- function mePayload({ discipline = 'engineer' } = {}) {
36
+ function mePayload({ discipline = 'artisan' } = {}) {
37
37
  return {
38
38
  builder: { id: 4242, rank: 'metic' },
39
39
  active_claims: [{ task_id: 77, title: 'a claimed task', touches: [], worktree_name: 'task-77', discipline }],
40
40
  wandering: { effective_level: 'normal', inject: false, contract: '' },
41
41
  interaction: { inject: true, contract: INTERACTION },
42
42
  specialities: [
43
- { discipline: 'engineer', inject: true, contract: SPECIALITY },
43
+ { discipline: 'artisan', inject: true, contract: SPECIALITY },
44
44
  { discipline: 'artist', inject: true, contract: ARTIST_SPECIALITY },
45
45
  ],
46
46
  };
@@ -99,7 +99,7 @@ await test('the hook, run as settings.json runs it, injects the interaction prof
99
99
  });
100
100
 
101
101
  await test('the speciality contract for the CLAIMED craft reaches the session, and no other craft\'s', async () => {
102
- const r = await runHook(mePayload({ discipline: 'engineer' }));
102
+ const r = await runHook(mePayload({ discipline: 'artisan' }));
103
103
  assert.ok(r.out.includes(SPECIALITY), 'the engineer speciality is injected for an engineering claim');
104
104
  assert.ok(!r.out.includes(ARTIST_SPECIALITY), 'the artist contract is not');
105
105
  });
@@ -116,17 +116,17 @@ const craftMode = require('../src/bongos/craft-mode.js');
116
116
  const MODES = craftMode.readModes();
117
117
  function twoTreePayload() {
118
118
  const claim = (id, discipline) => ({ task_id: id, title: 't', touches: [], worktree_name: `task-${id}`, discipline, craft_mode: craftMode.describeCraftMode(discipline, MODES) });
119
- return { ...mePayload(), specialities: [], active_claims: [claim(77, 'engineer'), claim(88, 'ideator')] };
119
+ return { ...mePayload(), specialities: [], active_claims: [claim(77, 'artisan'), claim(88, 'artist')] };
120
120
  }
121
121
 
122
122
  await test('a session in a claimed worktree is told its craft\'s pack on the first prompt, and not the other tree\'s', async () => {
123
123
  const r = await runHook(twoTreePayload(), { worktree: 'task-77' });
124
124
  assert.equal(r.code, 0);
125
- assert.ok(r.out.includes('docs/packs/engineer.md'), `the engineer pack pointer reaches the session; stdout was: ${JSON.stringify(r.out.slice(0, 400))}`);
126
- assert.ok(!r.out.includes('docs/packs/ideator.md'), 'worktree task-88\'s ideator craft does not');
125
+ assert.ok(r.out.includes('docs/packs/artisan.md'), `the engineer pack pointer reaches the session; stdout was: ${JSON.stringify(r.out.slice(0, 400))}`);
126
+ assert.ok(!r.out.includes('docs/packs/artist.md'), 'worktree task-88\'s ideator craft does not');
127
127
  const other = await runHook(twoTreePayload(), { worktree: 'task-88' });
128
- assert.ok(other.out.includes('docs/packs/ideator.md'));
129
- assert.ok(!other.out.includes('docs/packs/engineer.md'));
128
+ assert.ok(other.out.includes('docs/packs/artist.md'));
129
+ assert.ok(!other.out.includes('docs/packs/artisan.md'));
130
130
  });
131
131
 
132
132
  await test('outside any claimed worktree no craft is guessed', async () => {
@@ -72,7 +72,7 @@ await insertTestBuilders(pool, [
72
72
  // 'building': the seeds own the one 'planning' version the partial index allows.
73
73
  const VER = 'ZZZ-CDF';
74
74
  await pool.query("INSERT INTO versions (id, name, status) VALUES ($1, 'copy desk fixture', 'building') ON CONFLICT (id) DO NOTHING", [VER]);
75
- await pool.query("INSERT INTO tasks (id, version_id, title, discipline) VALUES ($1, $2, 'copy desk fixture', 'engineer') ON CONFLICT (id) DO NOTHING", [T, VER]);
75
+ await pool.query("INSERT INTO tasks (id, version_id, title, discipline) VALUES ($1, $2, 'copy desk fixture', 'artisan') ON CONFLICT (id) DO NOTHING", [T, VER]);
76
76
 
77
77
  // Apply the migration verbatim — this also proves it is idempotent, since a
78
78
  // migrated DB has already run it.
@@ -33,8 +33,8 @@ const { db } = installPageLedger(api, {
33
33
  builders: [
34
34
  { id: ALICE, github_login: 'alice', display_name: 'Alice', preferred_disciplines: ['artist'] },
35
35
  { id: BOB, github_login: 'bob', display_name: 'Bob', preferred_disciplines: ['artist'] },
36
- { id: CARL, github_login: 'carl', display_name: 'Carl', preferred_disciplines: ['engineer'] },
37
- { id: DAN, github_login: 'dan', display_name: 'Dan', preferred_disciplines: ['engineer'] },
36
+ { id: CARL, github_login: 'carl', display_name: 'Carl', preferred_disciplines: ['artisan'] },
37
+ { id: DAN, github_login: 'dan', display_name: 'Dan', preferred_disciplines: ['artisan'] },
38
38
  ],
39
39
  });
40
40
 
@@ -29,8 +29,8 @@ const api = require('../src/module-api.js');
29
29
  const { db, seen } = installPageLedger(api, {
30
30
  builders: [
31
31
  { id: '7', github_login: 'alice', display_name: 'Alice', preferred_disciplines: ['artist'] },
32
- { id: '8', github_login: 'bob', display_name: 'Bob', preferred_disciplines: ['engineer', 'artist'] },
33
- { id: '9', github_login: 'carol', display_name: 'Carol', preferred_disciplines: ['engineer'] },
32
+ { id: '8', github_login: 'bob', display_name: 'Bob', preferred_disciplines: ['artisan', 'artist'] },
33
+ { id: '9', github_login: 'carol', display_name: 'Carol', preferred_disciplines: ['artisan'] },
34
34
  ],
35
35
  });
36
36
  const ALICE = '7';
@@ -30,7 +30,7 @@ const { db } = installPageLedger(api, {
30
30
  builders: [
31
31
  { id: '7', github_login: 'alice', display_name: 'Alice', preferred_disciplines: ['artist'] },
32
32
  { id: '8', github_login: 'bob', display_name: 'Bob', preferred_disciplines: ['artist'] },
33
- { id: '9', github_login: 'carol', display_name: 'Carol', preferred_disciplines: ['engineer'] },
33
+ { id: '9', github_login: 'carol', display_name: 'Carol', preferred_disciplines: ['artisan'] },
34
34
  ],
35
35
  });
36
36
  const ALICE = '7';
@@ -29,7 +29,7 @@ const CARL = '9'; // the applier, Metic
29
29
  const { db } = installPageLedger(api, {
30
30
  builders: [
31
31
  { id: ALICE, github_login: 'alice', display_name: 'Alice', preferred_disciplines: ['artist'] },
32
- { id: CARL, github_login: 'carl', display_name: 'Carl', preferred_disciplines: ['engineer'] },
32
+ { id: CARL, github_login: 'carl', display_name: 'Carl', preferred_disciplines: ['artisan'] },
33
33
  ],
34
34
  });
35
35
 
@@ -208,7 +208,7 @@ try {
208
208
  `page-tweak-unplaced/${studioTask().id}/L0005`,
209
209
  ]);
210
210
  for (const t of eng) {
211
- assert.deepEqual([t.status, t.discipline, t.kind, t.version_id, t.goal_id, t.created_by], ['backlog', 'engineer', 'feature', 'V9', '88', ALICE], 'an unplaced line lands in the artist review goal (task 1004819)');
211
+ assert.deepEqual([t.status, t.discipline, t.kind, t.version_id, t.goal_id, t.created_by], ['backlog', 'artisan', 'feature', 'V9', '88', ALICE], 'an unplaced line lands in the artist review goal (task 1004819)');
212
212
  assert.match(t.description, /builders:studio/);
213
213
  }
214
214
  const quiet = eng.find((t) => t.source_ref.endsWith('/L0005'));
@@ -316,7 +316,7 @@ try {
316
316
  assert.equal(eng.title, 'Remove "Every project on this instance, newest first" (landing:projects)');
317
317
  assert.match(eng.description, /stop showing a line/);
318
318
  assert.match(eng.description, /take it out/);
319
- assert.deepEqual([eng.status, eng.discipline], ['backlog', 'engineer']);
319
+ assert.deepEqual([eng.status, eng.discipline], ['backlog', 'artisan']);
320
320
  });
321
321
  // Task 1004776: a line that must keep its meaning. The real editor read marks
322
322
  // it and says what it must still say; the real submit WARNS (never refuses)
@@ -102,7 +102,7 @@ async function fixture() {
102
102
  const claimFrom = async (source) => {
103
103
  const session = await insertRow('builder_sessions', { builder_id: builder.id, token_hash: `${tag}-${source}`, source });
104
104
  // discipline + kind: core_247's CHECK refuses the column default (task 1004363).
105
- const task = await insertRow('tasks', { title: `${tag} ${source} task`, status: 'shipped', version_id: version.id, discipline: 'engineer', kind: 'feature' });
105
+ const task = await insertRow('tasks', { title: `${tag} ${source} task`, status: 'shipped', version_id: version.id, discipline: 'artisan', kind: 'feature' });
106
106
  const claim = await insertRow('claims', {
107
107
  task_id: task.id, builder_id: builder.id, creator_session_id: session.id,
108
108
  worktree_name: `${tag}-${source}`, released_at: new Date(), outcome: 'shipped',
@@ -1,7 +1,7 @@
1
1
  // tests/craft_mode.mjs — the hall's craft mode picker (task 1004492, goal 1000095 WA6).
2
2
  //
3
3
  // On entering the hall a picker asks once per session "What are you doing
4
- // now?" (Building, Artistry, Ideating or Governing); the answer is
4
+ // now?" (Artisan, Artist or Patron — task 1005141); the answer is
5
5
  // html[data-craft], and a switch in the top bar changes it without a reload.
6
6
  // These cases boot the REAL shell-trail.js + craft-mode.js + shell.js in a DOM
7
7
  // stub, in the order every hall page loads them, and drive them the way a
@@ -163,7 +163,7 @@ function boot({ craft = true, session = {}, store = {}, throws = false, optOut =
163
163
  };
164
164
  }
165
165
 
166
- const MODES = ['building', 'artistry', 'ideating', 'governing'];
166
+ const MODES = ['artisan', 'artist', 'patron'];
167
167
  const noSwitch = (html) => html.replace(/<div class="craft-switch[\s\S]*?<\/div>/, '');
168
168
 
169
169
  test('NO MODE IS THE HALL AS TODAY: with no mode chosen, nothing but the switch differs from a hall without craft modes', () => {
@@ -185,9 +185,10 @@ test('a signed-in builder is asked ONCE per session, and a reload does not ask a
185
185
  const html = p.picker().innerHTML;
186
186
  assert.match(html, /role="dialog" aria-modal="true"/);
187
187
  assert.match(html, /What are you doing now\?/);
188
- for (const m of ['building', 'artistry', 'ideating', 'governing']) assert.match(html, new RegExp(`data-craft-pick="${m}"`), `a ${m} door`);
189
- assert.equal([...html.matchAll(/data-craft-pick="/g)].length, 4, 'four doors and nothing else to pick');
190
- assert.doesNotMatch(html, /legacy/i, 'Legacy mode is gone (owner, 2026-10-04: Building has all those tabs anyway)');
188
+ for (const m of MODES) assert.match(html, new RegExp(`data-craft-pick="${m}"`), `a ${m} door`);
189
+ assert.equal([...html.matchAll(/data-craft-pick="/g)].length, 3, 'three doors and nothing else to pick');
190
+ assert.doesNotMatch(html, /data-craft-pick="ideating"/, 'Ideating folded into Artist (task 1005141): no door of its own');
191
+ assert.doesNotMatch(html, /legacy/i, 'Legacy mode is gone (owner, 2026-10-04: Building — now Artisan — has all those tabs anyway)');
191
192
  p.escape();
192
193
  assert.equal(p.picker(), null, 'Escape closes it without choosing');
193
194
  assert.equal(p.mode(), undefined, 'and leaves the hall with no mode');
@@ -206,13 +207,13 @@ test('choosing a door sets html[data-craft] for every page of the session, and s
206
207
  const session = {}, store = {};
207
208
  const p = boot({ session, store });
208
209
  p.signIn();
209
- p.pick('data-craft-pick', 'ideating');
210
+ p.pick('data-craft-pick', 'artist');
210
211
  assert.equal(p.picker(), null, 'the picker closes');
211
- assert.equal(p.mode(), 'ideating');
212
- assert.ok(p.events.some((e) => e.type === 'otb:craft' && e.detail.mode === 'ideating'), 'otb:craft fires with the mode');
213
- assert.deepEqual(p.pressed(), ['ideating'], 'the switch lights the chosen mode');
212
+ assert.equal(p.mode(), 'artist');
213
+ assert.ok(p.events.some((e) => e.type === 'otb:craft' && e.detail.mode === 'artist'), 'otb:craft fires with the mode');
214
+ assert.deepEqual(p.pressed(), ['artist'], 'the switch lights the chosen mode');
214
215
  const next = boot({ session, store });
215
- assert.equal(next.mode(), 'ideating', 'the next page of the session reads it before /me answers');
216
+ assert.equal(next.mode(), 'artist', 'the next page of the session reads it before /me answers');
216
217
  assert.equal(store['hall-craft-keep'], undefined, '"Ask me again next session" was ticked, so nothing outlives the session');
217
218
  const tomorrow = boot({ store });
218
219
  tomorrow.signIn();
@@ -224,25 +225,25 @@ test('unticking "Ask me again next session" keeps the mode for later sessions',
224
225
  const store = {};
225
226
  const p = boot({ store });
226
227
  p.signIn();
227
- p.pick('data-craft-pick', 'governing', { askAgain: false });
228
- assert.equal(store['hall-craft-keep'], 'governing');
228
+ p.pick('data-craft-pick', 'patron', { askAgain: false });
229
+ assert.equal(store['hall-craft-keep'], 'patron');
229
230
  const tomorrow = boot({ store });
230
231
  tomorrow.signIn();
231
- assert.equal(tomorrow.mode(), 'governing', 'the kept mode applies');
232
+ assert.equal(tomorrow.mode(), 'patron', 'the kept mode applies');
232
233
  assert.equal(tomorrow.picker(), null, 'and nobody is asked');
233
234
  });
234
235
 
235
- test('THE HEADER SWITCH changes the mode without a reload, and offers the four crafts', () => {
236
+ test('THE HEADER SWITCH changes the mode without a reload, and offers the three crafts', () => {
236
237
  const p = boot();
237
238
  const opts = [...p.topbar.innerHTML.matchAll(/data-craft-opt="([a-z]+)"/g)].map((m) => m[1]);
238
- assert.deepEqual(opts, MODES, 'the four crafts, and no Legacy');
239
- p.press('artistry');
240
- assert.equal(p.mode(), 'artistry');
241
- p.press('building');
242
- assert.equal(p.mode(), 'building', 'anyone can switch at any time');
243
- assert.deepEqual(p.pressed(), ['building']);
239
+ assert.deepEqual(opts, MODES, 'the three crafts, and no Legacy or Ideating');
240
+ p.press('artist');
241
+ assert.equal(p.mode(), 'artist');
242
+ p.press('artisan');
243
+ assert.equal(p.mode(), 'artisan', 'anyone can switch at any time');
244
+ assert.deepEqual(p.pressed(), ['artisan']);
244
245
  assert.equal(p.location.reloads, 0, 'no navigation happened');
245
- assert.equal(p.session['hall-craft'], 'building', 'and the session remembers it');
246
+ assert.equal(p.session['hall-craft'], 'artisan', 'and the session remembers it');
246
247
  });
247
248
 
248
249
  test('a browser that still has Legacy stored falls back to NO MODE (task 1004567)', () => {
@@ -256,31 +257,31 @@ test('a browser that still has Legacy stored falls back to NO MODE (task 1004567
256
257
  const kept = boot({ store });
257
258
  kept.signIn();
258
259
  assert.equal(kept.mode(), undefined, 'a kept Legacy from an earlier session is no mode too');
259
- assert.ok(kept.picker(), 'and a new session is asked to pick one of the four');
260
- p.press('building');
261
- assert.equal(p.mode(), 'building', 'and any of the four can be chosen from there');
260
+ assert.ok(kept.picker(), 'and a new session is asked to pick one of the three');
261
+ p.press('artisan');
262
+ assert.equal(p.mode(), 'artisan', 'and any of the three can be chosen from there');
262
263
  });
263
264
 
264
265
  test('on a phone the switch shows only the current mode, and tapping it opens the picker', () => {
265
- const session = { 'hall-craft': 'building' };
266
+ const session = { 'hall-craft': 'artisan' };
266
267
  const p = boot({ session, narrow: true });
267
- p.press('building');
268
+ p.press('artisan');
268
269
  assert.ok(p.picker(), 'the current mode opens the picker');
269
- assert.equal(p.mode(), 'building', 'without changing the mode');
270
+ assert.equal(p.mode(), 'artisan', 'without changing the mode');
270
271
  });
271
272
 
272
- test('artists start on Artistry: the door is pre-selected and the picker says why', () => {
273
+ test('artists start on Artist: the door is pre-selected and the picker says why', () => {
273
274
  const p = boot();
274
- p.signIn({ id: 3, rank: 'thetes', main_discipline: 'artist', preferred_disciplines: ['engineer', 'artist'] });
275
+ p.signIn({ id: 3, rank: 'thetes', main_discipline: 'artist', preferred_disciplines: ['artisan', 'artist'] });
275
276
  const html = p.picker().innerHTML;
276
- assert.match(html, /craft-door--artistry craft-door--pre"/);
277
- assert.match(html, /Artists start on Artistry\./);
277
+ assert.match(html, /craft-door--artist craft-door--pre"/);
278
+ assert.match(html, /Artists start here\./);
278
279
  const eng = boot();
279
- eng.signIn({ id: 4, rank: 'thetes', main_discipline: 'engineer', preferred_disciplines: ['artist'] });
280
+ eng.signIn({ id: 4, rank: 'thetes', main_discipline: 'artisan', preferred_disciplines: ['artist'] });
280
281
  assert.doesNotMatch(eng.picker().innerHTML, /craft-door--pre/, 'a main role that is not artist pre-selects nothing');
281
282
  const unset = boot();
282
283
  unset.signIn({ id: 5, rank: 'thetes', main_discipline: null, preferred_disciplines: ['artist'] });
283
- assert.match(unset.picker().innerHTML, /craft-door--artistry craft-door--pre"/, 'with no main role, the first craft decides');
284
+ assert.match(unset.picker().innerHTML, /craft-door--artist craft-door--pre"/, 'with no main role, the first craft decides');
284
285
  });
285
286
 
286
287
  test('storage that throws is never asked and never wears a mode', () => {
@@ -291,13 +292,26 @@ test('storage that throws is never asked and never wears a mode', () => {
291
292
  });
292
293
 
293
294
  test('a page can opt out with <body data-craft-mode="off">', () => {
294
- const p = boot({ optOut: true, session: { 'hall-craft': 'artistry' } });
295
+ const p = boot({ optOut: true, session: { 'hall-craft': 'artist' } });
295
296
  p.signIn();
296
297
  assert.doesNotMatch(p.topbar.innerHTML, /craft-switch/, 'no switch');
297
298
  assert.equal(p.picker(), null, 'no picker');
298
299
  assert.equal(p.mode(), undefined, 'no mode');
299
300
  });
300
301
 
302
+ test('a mode stored under its pre-rename id reads as the new craft (task 1005141), this session or kept', () => {
303
+ const map = { building: 'artisan', artistry: 'artist', ideating: 'artist', governing: 'patron' };
304
+ for (const [old, now] of Object.entries(map)) {
305
+ const p = boot({ session: { 'hall-craft': old, 'hall-craft-asked': '1' } });
306
+ assert.equal(p.mode(), now, `a session's stored ${old} wears ${now}`);
307
+ p.signIn();
308
+ assert.equal(p.picker(), null, `${old}: nobody loses their choice to the rename`);
309
+ assert.deepEqual(p.pressed(), [now]);
310
+ const kept = boot({ store: { 'hall-craft-keep': old } });
311
+ assert.equal(kept.mode(), now, `a kept ${old} wears ${now}`);
312
+ }
313
+ });
314
+
301
315
  test('a stored value that is not a mode is ignored, not trusted', () => {
302
316
  const p = boot({ session: { 'hall-craft': '"><img src=x>' }, store: { 'hall-craft-keep': 'admin' } });
303
317
  assert.equal(p.mode(), undefined);
@@ -1,9 +1,9 @@
1
1
  // tests/craft_mode_home.mjs — choosing a craft leaves another craft's page for Home
2
2
  // (task 1005067, goal 1000095).
3
3
  //
4
- // Owner, 2026-11-04: a governor on the city who picked Artistry, Ideating or Building
4
+ // Owner, 2026-11-04: a governor on the city who picked Artistry, Ideating or Building (now Artist or Artisan, task 1005141)
5
5
  // stayed in the governor's city. Each craft's home is its own dashboard (Home draws it;
6
- // the street is Governing's for now), so a page that belongs ONLY to other crafts is
6
+ // the street is Patron's, Governing until task 1005141), so a page that belongs ONLY to other crafts is
7
7
  // left for Home, while a shared page, a page no craft owns, and Home itself stay put.
8
8
  // These cases run the REAL craft-mode.js and drive its own choose(), recording where
9
9
  // the hop would go instead of navigating.
@@ -61,58 +61,58 @@ function boot({ page, active = null, govCity = false }) {
61
61
  }
62
62
 
63
63
  test('leaving the governor\'s city for any other craft goes to Home', () => {
64
- for (const mode of ['building', 'artistry', 'ideating']) {
64
+ for (const mode of ['artisan', 'artist']) {
65
65
  const { CM, hops } = boot({ page: 'city', active: 'city' });
66
66
  CM.choose(mode);
67
67
  assert.deepEqual(hops, ['/'], `${mode} from the city goes Home`);
68
68
  }
69
69
  });
70
70
 
71
- test('the street is Governing\'s own page, so other crafts leave it too', () => {
71
+ test('the street is Patron\'s own page, so other crafts leave it too', () => {
72
72
  const { CM, hops } = boot({ page: 'street' });
73
- assert.equal(CM.strandedBy('governing'), false);
74
- CM.choose('ideating');
73
+ assert.equal(CM.strandedBy('patron'), false);
74
+ CM.choose('artist');
75
75
  assert.deepEqual(hops, ['/']);
76
76
  });
77
77
 
78
- test('choosing Governing on another craft\'s page lands on the street', () => {
78
+ test('choosing Patron on another craft\'s page lands on the street', () => {
79
79
  const { CM, hops } = boot({ page: 'studio', active: 'studio', govCity: true });
80
- CM.choose('governing');
80
+ CM.choose('patron');
81
81
  assert.deepEqual(hops, ['/builders/street']);
82
82
  // without the street's script on the page, Home itself hops to the street
83
- assert.equal(boot({ page: 'studio' }).CM.homeHref('governing'), '/');
83
+ assert.equal(boot({ page: 'studio' }).CM.homeHref('patron'), '/');
84
84
  });
85
85
 
86
86
  test('a page the chosen craft also shows stays put', () => {
87
- // goals is in Building, Artistry and Ideating alike
87
+ // goals is in Artisan and Artist alike
88
88
  const { CM, hops } = boot({ page: 'goals', active: 'goals' });
89
- CM.choose('ideating');
90
- CM.choose('artistry');
89
+ CM.choose('artist');
90
+ CM.choose('artisan');
91
91
  assert.deepEqual(hops, []);
92
92
  });
93
93
 
94
94
  test('a page every craft keeps, and a page no craft owns, stay put', () => {
95
95
  for (const [page, active] of [['roster', 'roster'], ['my-agents', 'my-agents'], ['task', null], ['idea', null]]) {
96
96
  const { CM, hops } = boot({ page, active });
97
- CM.choose('governing');
98
- CM.choose('artistry');
97
+ CM.choose('patron');
98
+ CM.choose('artist');
99
99
  assert.deepEqual(hops, [], `${page} stays`);
100
100
  }
101
101
  });
102
102
 
103
103
  test('Home redraws itself for the craft, so it never hops from here', () => {
104
104
  const { CM, hops } = boot({ page: 'home', active: 'home' });
105
- CM.choose('artistry');
106
- CM.choose('governing');
105
+ CM.choose('artist');
106
+ CM.choose('patron');
107
107
  assert.deepEqual(hops, []);
108
108
  });
109
109
 
110
110
  // The Economy page (drachmae.html, nav item 'economy') was this case's real example
111
111
  // until task 1005046 folded it into the Primer; the rule it pins is unchanged, so a
112
- // made-up pairing keeps it: the marked nav item (Tasks, Building's alone) is read
113
- // before the page name (Studio, which Artistry also shows).
112
+ // made-up pairing keeps it: the marked nav item (Tasks, Artisan's alone) is read
113
+ // before the page name (Studio, which Artist also shows).
114
114
  test('the nav item wins over the page name', () => {
115
115
  const { CM } = boot({ page: 'studio', active: 'work' });
116
- assert.equal(CM.strandedBy('artistry'), true);
117
- assert.equal(CM.strandedBy('building'), false);
116
+ assert.equal(CM.strandedBy('artist'), true);
117
+ assert.equal(CM.strandedBy('artisan'), false);
118
118
  });
@@ -1,11 +1,12 @@
1
1
  // tests/craft_mode_looks.mjs — each craft mode's look (task 1004495, goal 1000095
2
2
  // WA6; direction docs/design/craft-mode-direction.md).
3
3
  //
4
- // Artistry, Ideating and Governing each wear a look: --craft / --craft-on tokens
4
+ // Artist and Patron each wear a look (task 1005141: Artistry and Ideating became Artist,
5
+ // Governing became Patron, Building became Artisan): --craft / --craft-on tokens
5
6
  // (light + dark twins) in style.css, the header eyebrow beside the page title and
6
- // the door's scene framed under the brand. Building and no mode wear
7
+ // the door's scene framed under the brand. Artisan and no mode wear
7
8
  // NOTHING — the hall as it always was. These cases run the REAL craft-mode.js over
8
- // a small DOM and read the REAL style.css, so a look that leaks into Building, a
9
+ // a small DOM and read the REAL style.css, so a look that leaks into Artisan, a
9
10
  // plate that is a second copy of the art, or a colour that loses its contrast reds.
10
11
  import assert from 'node:assert/strict';
11
12
  import { test } from 'node:test';
@@ -19,8 +20,8 @@ const HALL = path.join(ROOT, 'modules/hall-ui/public');
19
20
  const CRAFT = fs.readFileSync(path.join(HALL, 'craft-mode.js'), 'utf8');
20
21
  const CSS = fs.readFileSync(path.join(HALL, 'style.css'), 'utf8');
21
22
 
22
- const LOOKED = ['artistry', 'ideating', 'governing'];
23
- const WORDS = { artistry: 'the studio · night', ideating: 'the sky', governing: 'the city' };
23
+ const LOOKED = ['artist', 'patron'];
24
+ const WORDS = { artist: 'the studio · night', patron: 'the city' };
24
25
 
25
26
  // A small DOM: enough tree for insertBefore / removeChild / nextSibling to be real.
26
27
  function node(tag, id, cls) {
@@ -97,24 +98,24 @@ test('each looked mode frames its own door scene under the brand, and names itse
97
98
  }
98
99
  });
99
100
 
100
- test('BUILDING AND NO MODE WEAR NO LOOK: no eyebrow; Building keeps only the Home plate, showing the city', () => {
101
+ test('ARTISAN AND NO MODE WEAR NO LOOK: no eyebrow; Artisan keeps only the Home plate, showing the city', () => {
101
102
  const none = boot();
102
103
  assert.equal(none.root.dataset.craft, undefined);
103
104
  assert.equal(none.plates().length + none.marks().length, 0, 'no mode draws nothing');
104
- for (const m of ['building']) {
105
+ for (const m of ['artisan']) {
105
106
  const p = boot({ 'hall-craft': m });
106
107
  assert.equal(p.marks().length, 0, `${m} at boot names no look`);
107
108
  assert.equal(p.plates().length, 1, `${m}: the Home plate is there too (task 1005065)`);
108
109
  assert.equal(p.plates()[0].getAttribute('data-plate'), m);
109
- assert.ok(p.plates()[0].innerHTML.startsWith(p.craft.artHtml('governing')), `${m}: its plate shows the city`);
110
- p.craft.choose('governing');
110
+ assert.ok(p.plates()[0].innerHTML.startsWith(p.craft.artHtml('patron')), `${m}: its plate shows the city`);
111
+ p.craft.choose('patron');
111
112
  assert.equal(p.plates().length, 1);
112
- assert.equal(p.plates()[0].getAttribute('data-plate'), 'governing', 'switching mode swaps the plate');
113
+ assert.equal(p.plates()[0].getAttribute('data-plate'), 'patron', 'switching mode swaps the plate');
113
114
  p.craft.choose(m);
114
115
  assert.equal(p.marks().length, 0, `switching to ${m} removes the eyebrow`);
115
116
  assert.equal(p.plates()[0].getAttribute('data-plate'), m, 'and swaps the plate back');
116
117
  }
117
- assert.deepEqual(Object.keys(none.craft.LOOKS).sort(), [...LOOKED].sort(), 'only the three crafts have a look');
118
+ assert.deepEqual(Object.keys(none.craft.LOOKS).sort(), [...LOOKED].sort(), 'only the two looked crafts have a look');
118
119
  });
119
120
 
120
121
  test('the art is drawn once: craft-mode.js has one artHtml and exports it', () => {
@@ -126,11 +127,11 @@ test('the art is drawn once: craft-mode.js has one artHtml and exports it', () =
126
127
  // Every selector in style.css that names a craft mode.
127
128
  const CRAFT_SELECTORS = [...CSS.matchAll(/data-craft="([a-z]+)"/g)].map((m) => m[1]);
128
129
 
129
- test('style.css names only the three looked modes — Building has no rule at all', () => {
130
+ test('style.css names only the two looked modes — Artisan has no rule at all', () => {
130
131
  assert.ok(CRAFT_SELECTORS.length >= 12, `found ${CRAFT_SELECTORS.length} craft selectors`);
131
132
  for (const m of CRAFT_SELECTORS) assert.ok(LOOKED.includes(m), `a rule names data-craft="${m}"`);
132
- assert.doesNotMatch(CSS, /data-craft="building"/);
133
- assert.doesNotMatch(CSS, /data-craft\]/, 'no rule matches ANY mode, which would dress Building too');
133
+ assert.doesNotMatch(CSS, /data-craft="(building|artisan|artistry|ideating|governing)"/, 'no rule for Artisan, and none left under a pre-rename id');
134
+ assert.doesNotMatch(CSS, /data-craft\]/, 'no rule matches ANY mode, which would dress Artisan too');
134
135
  });
135
136
 
136
137
  // The token values, light and dark, read from the stylesheet itself.
@@ -165,7 +166,7 @@ test('every mode colour has a light and a dark twin that keep their contrast', (
165
166
  });
166
167
 
167
168
  test('the shared treatment, the eyebrow and the plate are written as the direction says', () => {
168
- const looked = 'html:is([data-craft="artistry"], [data-craft="ideating"], [data-craft="governing"])';
169
+ const looked = 'html:is([data-craft="artist"], [data-craft="patron"])';
169
170
  for (const target of ['.craft-switch__opt[aria-pressed="true"]', '.nav-item--active', '.nav-item--active::before', '.app-topbar']) {
170
171
  assert.ok(CSS.includes(`${looked} ${target}`), `the looked modes restyle ${target}`);
171
172
  }
@@ -127,15 +127,16 @@ function boot({ session = {}, local = {}, perms = { 'board.vote.cast': true } }
127
127
  };
128
128
  }
129
129
 
130
- const CRAFTS = ['building', 'artistry', 'ideating', 'governing'];
130
+ const CRAFTS = ['artisan', 'artist', 'patron'];
131
131
 
132
132
  test('THE ALWAYS-PRESENT LIST: one shared list, Builders first, then Collab and Profile, inside every mode', () => {
133
133
  const { craft } = boot();
134
134
  // task 1004685 (the living-city prototype): My agents joins as a PROPOSAL for the
135
135
  // owner — every craft has agents, and round-3 board 6 pairs it with the city.
136
136
  // task 1005071 (owner, 2026-11-04): Help — the old Docs group — joins, so every craft has the Primer.
137
- assert.deepEqual([...craft.ALWAYS_TABS], ['roster', 'collab', 'profile', 'my-agents', 'primer', 'commands', 'atlas', 'modules', 'apidocs'],
138
- 'Builders (NAV id roster) heads the list (owner, 2026-10-01); Collab and Profile join it (owner, 2026-10-03); My agents (prototype, task 1004685); Help (task 1005071)');
137
+ // task 1005144 (ADR 0401): My agents and Modules left the sidebar for the one options menu.
138
+ assert.deepEqual([...craft.ALWAYS_TABS], ['roster', 'collab', 'profile', 'primer', 'commands', 'atlas', 'apidocs'],
139
+ 'Builders (NAV id roster) heads the list (owner, 2026-10-01); Collab and Profile join it (owner, 2026-10-03); Help (task 1005071)');
139
140
  for (const m of CRAFTS) {
140
141
  const tabs = craft.tabsFor(m);
141
142
  assert.ok(Array.isArray(tabs), `${m} has a named tab list`);
@@ -147,8 +148,8 @@ test('THE ALWAYS-PRESENT LIST: one shared list, Builders first, then Collab and
147
148
 
148
149
  test('every tab a mode names is a real NAV item, and every NAV item lives in some mode', () => {
149
150
  const { craft } = boot();
150
- // 24 since task 1005051 folded the Roadmap into Goals (a sanity floor, not a count).
151
- assert.ok(NAV_IDS.length >= 24, `read ${NAV_IDS.length} NAV ids from shell.js`);
151
+ // 20 since task 1005144 moved four settings pages into the options menu (a sanity floor, not a count).
152
+ assert.ok(NAV_IDS.length >= 20, `read ${NAV_IDS.length} NAV ids from shell.js`);
152
153
  const named = new Set(CRAFTS.flatMap((m) => craft.tabsFor(m)));
153
154
  for (const id of named) assert.ok(NAV_IDS.includes(id), `${id} is a NAV id`);
154
155
  // Home is the one exception: in a mode the craft plate IS the Home button
@@ -167,14 +168,14 @@ test('ONE HOME: no mode shows the Home tab, because the plate is Home in every m
167
168
  assert.ok(none.showing().includes('home'), 'with no mode there is no plate, so the Home tab shows');
168
169
  });
169
170
 
170
- test('Government shows only in Governing, and Governing is the shortest list', () => {
171
+ test('Government shows only in Patron, and Patron is the shortest list', () => {
171
172
  const { craft } = boot();
172
- assert.ok(GOV_IDS.length >= 5, `read ${GOV_IDS.length} Government ids`);
173
+ assert.ok(GOV_IDS.length >= 3, `read ${GOV_IDS.length} Government ids`);
173
174
  for (const m of CRAFTS) {
174
- for (const id of GOV_IDS) assert.equal(craft.tabsFor(m).includes(id), m === 'governing', `${id} in ${m}`);
175
+ for (const id of GOV_IDS) assert.equal(craft.tabsFor(m).includes(id), m === 'patron', `${id} in ${m}`);
175
176
  }
176
177
  const lengths = CRAFTS.map((m) => craft.tabsFor(m).filter((id) => !GOV_IDS.includes(id)).length);
177
- assert.equal(Math.min(...lengths), lengths[CRAFTS.indexOf('governing')], 'outside its own Government group, Governing shows the fewest tabs');
178
+ assert.equal(Math.min(...lengths), lengths[CRAFTS.indexOf('patron')], 'outside its own Government group, Patron shows the fewest tabs');
178
179
  });
179
180
 
180
181
  test('NO MODE IS TODAY\'S NAV: every tab the gate allows, nothing marked — and a stored Legacy is no mode', () => {
@@ -196,19 +197,19 @@ test('each mode shows its own tabs, and switching updates the nav live', () => {
196
197
  assert.equal(p.root.dataset.craft, m);
197
198
  assert.deepEqual(new Set(p.showing()), new Set(p.craft.tabsFor(m)), `${m} shows exactly its list`);
198
199
  for (const id of ['roster', 'collab', 'profile']) assert.ok(p.showing().includes(id), `${id} stays in ${m}`);
199
- assert.equal(p.headShowing('run-the-project'), m === 'governing', `the Government head shows only where its tabs do (${m})`);
200
+ assert.equal(p.headShowing('run-the-project'), m === 'patron', `the Government head shows only where its tabs do (${m})`);
200
201
  }
201
- p.craft.choose('building');
202
- assert.deepEqual(new Set(p.showing()), new Set(p.craft.tabsFor('building')), 'back to Building, its tabs return without a reload');
202
+ p.craft.choose('artisan');
203
+ assert.deepEqual(new Set(p.showing()), new Set(p.craft.tabsFor('artisan')), 'back to Artisan, its tabs return without a reload');
203
204
  });
204
205
 
205
206
  test('a mode never shows a tab the gate hid, and a later gate pass keeps the mode', () => {
206
- const p = boot({ session: { 'hall-craft': 'governing' } });
207
+ const p = boot({ session: { 'hall-craft': 'patron' } });
207
208
  p.signIn('thetes');
208
- assert.ok(!p.showing().includes('watch'), 'a thetes in Governing does not get the metic-floored Watch tab');
209
+ assert.ok(!p.showing().includes('watch'), 'a thetes in Patron does not get the metic-floored Watch tab');
209
210
  assert.ok(!p.headShowing('run-the-project'), 'nor its group head');
210
211
  // task 1005071: sidebar order — Work, Community, You, Govern, then Help (every craft).
211
- assert.deepEqual(p.showing(), ['blockers', 'collab', 'roster', 'profile', 'my-agents', 'city', 'board-room', 'primer', 'commands', 'atlas', 'modules', 'apidocs'], 'what a thetes board member in Governing sees');
212
+ assert.deepEqual(p.showing(), ['blockers', 'collab', 'roster', 'profile', 'city', 'board-room', 'primer', 'commands', 'atlas', 'apidocs'], 'what a thetes board member in Patron sees');
212
213
  p.signIn('archon');
213
214
  assert.ok(p.showing().includes('watch'), 'promoted, the gate lets it through and the mode keeps it');
214
215
  assert.ok(p.headShowing('run-the-project'), 'and its group head comes back with it, which only a mode pass after the gate can know');
@@ -224,8 +225,8 @@ test('shell.js applies the mode AFTER its gate loop, and style.css hides what th
224
225
  assert.doesNotMatch(CRAFT, /\.hidden\s*=/, 'the mode never writes `hidden`, which the gate owns');
225
226
  });
226
227
 
227
- // ---------- the Governing fold (owner, 2026-10-08) ----------
228
- // Governing leads with the City and folds its other own tabs into one dropdown
228
+ // ---------- the Patron fold (owner, 2026-10-08; Governing until task 1005141) ----------
229
+ // Patron leads with the City and folds its other own tabs into one dropdown
229
230
  // under it. The fold MOVES nodes, which the parsed sidebar above cannot model, so
230
231
  // these cases run the real craft-mode.js against a small working tree: nodes with
231
232
  // parents, insertBefore and comments, laid out as shell.js's NAV groups are.
@@ -309,35 +310,36 @@ function bootTree({ mode = null, activeId = null } = {}) {
309
310
  return { craft, sidebar, fold, ids, order, byId };
310
311
  }
311
312
 
312
- test('GOVERNING LEADS WITH THE CITY: its other tabs fold into one dropdown right under it', () => {
313
+ test('PATRON LEADS WITH THE CITY: its other tabs fold into one dropdown right under it', () => {
313
314
  const before = bootTree().order();
314
- const p = bootTree({ mode: 'governing' });
315
+ const p = bootTree({ mode: 'patron' });
315
316
  const fold = p.fold();
316
- assert.ok(fold, 'the dropdown exists in Governing');
317
+ assert.ok(fold, 'the dropdown exists in Patron');
317
318
  assert.equal(p.byId('nav-city').nextSibling, fold, 'directly under the City tab');
318
- const own = p.craft.tabsFor('governing').filter((id) => !p.craft.ALWAYS_TABS.includes(id) && id !== 'city');
319
- assert.deepEqual(JSON.parse(JSON.stringify(p.ids(fold))), JSON.parse(JSON.stringify(own)), 'every other Governing tab is in it, in the mode\'s order');
319
+ const own = p.craft.tabsFor('patron').filter((id) => !p.craft.ALWAYS_TABS.includes(id) && id !== 'city');
320
+ assert.deepEqual(JSON.parse(JSON.stringify(p.ids(fold))), JSON.parse(JSON.stringify(own)), 'every other Patron tab is in it, in the mode\'s order');
320
321
  assert.ok(fold.classList.contains('nav-group--collapsed'), 'a dropdown starts closed');
321
322
  const head = fold.querySelector('.nav-group__head');
322
323
  assert.equal(head.getAttribute('aria-expanded'), 'false');
323
324
  assert.equal(head.getAttribute('aria-controls'), 'nav-items-craft-fold', 'the head names the region it opens');
324
- assert.match(head.innerHTML, /More governing/);
325
+ // "More governing" until task 1005141; "More patron" does not read, so the label is plain "More".
326
+ assert.match(head.innerHTML, /<span class="nav-group__head-label">More<\/span>/);
325
327
  assert.ok(head.hasAttribute('data-craft-off') === false, 'its head shows while a tab inside does');
326
328
  assert.ok(p.sidebar.querySelectorAll('.nav-group__head').find((h) => h.dataset.group === 'run-the-project').hasAttribute('data-craft-off'),
327
329
  'the emptied Government head hides, so no label sits over nothing');
328
- p.craft.choose('building');
329
- assert.equal(p.fold(), null, 'leaving Governing removes the dropdown');
330
+ p.craft.choose('artisan');
331
+ assert.equal(p.fold(), null, 'leaving Patron removes the dropdown');
330
332
  assert.deepEqual(p.order(), before, 'and puts every tab back exactly where it was');
331
- p.craft.choose('governing');
332
- p.craft.choose('governing');
333
+ p.craft.choose('patron');
334
+ p.craft.choose('patron');
333
335
  assert.equal(p.sidebar.querySelectorAll('.craft-fold').length, 1, 'choosing it again builds no second dropdown');
334
336
  });
335
337
 
336
338
  test('the dropdown opens on its own when the page you are on is inside it', () => {
337
- const p = bootTree({ mode: 'governing', activeId: 'watch' });
339
+ const p = bootTree({ mode: 'patron', activeId: 'watch' });
338
340
  assert.ok(!p.fold().classList.contains('nav-group--collapsed'), 'on Watch, the dropdown is open');
339
341
  assert.equal(p.fold().querySelector('.nav-group__head').getAttribute('aria-expanded'), 'true');
340
- assert.equal(bootTree({ mode: 'artistry' }).fold(), null, 'no other mode folds anything');
342
+ assert.equal(bootTree({ mode: 'artist' }).fold(), null, 'no other mode folds anything');
341
343
  assert.equal(bootTree().fold(), null, 'nor does no mode');
342
344
  });
343
345