@bongos/core 1.24.79 → 1.24.81

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 (413) hide show
  1. package/.bongos-core.json +554 -394
  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/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md +142 -0
  17. package/docs/adr/README.md +4 -0
  18. package/docs/api/openapi.json +195 -3
  19. package/docs/api-reference.md +8 -4
  20. package/docs/architecture.md +7 -3
  21. package/docs/copy-inventory.md +393 -378
  22. package/docs/copy-registry.json +722 -578
  23. package/docs/design/characters/character-sprites-source.webp +0 -0
  24. package/docs/file-map.md +7 -8
  25. package/docs/module-api-changelog.md +4 -0
  26. package/docs/modules-contract.md +5 -0
  27. package/docs/onboarding/slash-commands.md +1 -1
  28. package/docs/packs/{engineer.md → artisan.md} +5 -5
  29. package/docs/packs/artist.md +50 -4
  30. package/docs/page-inventory.json +16 -7
  31. package/docs/page-readings.json +3184 -3338
  32. package/docs/{packs/ideator.md → recipes/ideation-playbook.md} +5 -48
  33. package/migrations/core_296_crafts_artisan_artist.sql +72 -0
  34. package/migrations/core_297_builder_character.sql +30 -0
  35. package/modules/copy-desk/proposals.js +1 -1
  36. package/modules/economy/credits-by-craft.js +29 -15
  37. package/modules/hall-ui/public/ai-studio.html +6 -4
  38. package/modules/hall-ui/public/ai-studio.states.json +1 -0
  39. package/modules/hall-ui/public/atlas.html +5 -5
  40. package/modules/hall-ui/public/atlas.js +3 -1
  41. package/modules/hall-ui/public/atlas.states.json +1 -0
  42. package/modules/hall-ui/public/blockers.html +5 -5
  43. package/modules/hall-ui/public/board-room.html +4 -4
  44. package/modules/hall-ui/public/board-room.states.json +1 -0
  45. package/modules/hall-ui/public/characters/artisan.png +0 -0
  46. package/modules/hall-ui/public/characters/artist.png +0 -0
  47. package/modules/hall-ui/public/characters/patron.png +0 -0
  48. package/modules/hall-ui/public/city.css +3 -4
  49. package/modules/hall-ui/public/city.html +9 -9
  50. package/modules/hall-ui/public/city.js +17 -56
  51. package/modules/hall-ui/public/city.states.json +7 -32
  52. package/modules/hall-ui/public/collab.html +4 -4
  53. package/modules/hall-ui/public/collab.states.json +1 -0
  54. package/modules/hall-ui/public/commands.html +4 -4
  55. package/modules/hall-ui/public/commands.states.json +1 -0
  56. package/modules/hall-ui/public/controls.css +63 -11
  57. package/modules/hall-ui/public/copy-desk.html +4 -4
  58. package/modules/hall-ui/public/copy-desk.states.json +1 -0
  59. package/modules/hall-ui/public/craft-mode.js +258 -64
  60. package/modules/hall-ui/public/deploy.html +4 -4
  61. package/modules/hall-ui/public/deploy.states.json +1 -0
  62. package/modules/hall-ui/public/diagram-viewer.js +2 -1
  63. package/modules/hall-ui/public/dom-utils.js +1 -1
  64. package/modules/hall-ui/public/goal-map.html +9 -6
  65. package/modules/hall-ui/public/goals.html +5 -5
  66. package/modules/hall-ui/public/goals.states.json +1 -0
  67. package/modules/hall-ui/public/government.html +4 -4
  68. package/modules/hall-ui/public/government.states.json +1 -0
  69. package/modules/hall-ui/public/hall-render.js +3 -1
  70. package/modules/hall-ui/public/home-city-draw.js +3 -3
  71. package/modules/hall-ui/public/home-city.css +4 -0
  72. package/modules/hall-ui/public/home-city.js +17 -9
  73. package/modules/hall-ui/public/home-gallery.js +8 -1
  74. package/modules/hall-ui/public/home-idea-box.css +2 -2
  75. package/modules/hall-ui/public/home-idea-box.js +3 -3
  76. package/modules/hall-ui/public/idea.html +5 -5
  77. package/modules/hall-ui/public/idea.states.json +1 -0
  78. package/modules/hall-ui/public/ideas.html +5 -5
  79. package/modules/hall-ui/public/ideas.states.json +1 -0
  80. package/modules/hall-ui/public/index-founding.states.json +1 -0
  81. package/modules/hall-ui/public/index-genesis-decide.states.json +1 -0
  82. package/modules/hall-ui/public/index-genesis-demo.states.json +1 -0
  83. package/modules/hall-ui/public/index-genesis.states.json +1 -0
  84. package/modules/hall-ui/public/index.html +11 -7
  85. package/modules/hall-ui/public/index.states.json +1 -0
  86. package/modules/hall-ui/public/market-watch-item.html +4 -4
  87. package/modules/hall-ui/public/market-watch-item.states.json +1 -0
  88. package/modules/hall-ui/public/market-watch-rules.html +4 -4
  89. package/modules/hall-ui/public/market-watch-rules.states.json +1 -0
  90. package/modules/hall-ui/public/market-watch.html +6 -9
  91. package/modules/hall-ui/public/market-watch.js +22 -45
  92. package/modules/hall-ui/public/market-watch.states.json +1 -0
  93. package/modules/hall-ui/public/modules.html +4 -4
  94. package/modules/hall-ui/public/modules.states.json +1 -0
  95. package/modules/hall-ui/public/money.html +4 -4
  96. package/modules/hall-ui/public/money.states.json +1 -0
  97. package/modules/hall-ui/public/my-agents.html +4 -4
  98. package/modules/hall-ui/public/my-agents.states.json +1 -0
  99. package/modules/hall-ui/public/options-menu.js +409 -0
  100. package/modules/hall-ui/public/paintbrush.js +3 -3
  101. package/modules/hall-ui/public/palette.js +11 -13
  102. package/modules/hall-ui/public/primer.html +5 -5
  103. package/modules/hall-ui/public/primer.states.json +1 -0
  104. package/modules/hall-ui/public/profile-crafts.js +1 -1
  105. package/modules/hall-ui/public/profile-roles.js +23 -19
  106. package/modules/hall-ui/public/profile.css +1 -1
  107. package/modules/hall-ui/public/profile.html +19 -18
  108. package/modules/hall-ui/public/profile.js +19 -18
  109. package/modules/hall-ui/public/profile.states.json +5 -4
  110. package/modules/hall-ui/public/project-settings.html +4 -4
  111. package/modules/hall-ui/public/roster.html +4 -4
  112. package/modules/hall-ui/public/roster.js +3 -3
  113. package/modules/hall-ui/public/roster.states.json +1 -0
  114. package/modules/hall-ui/public/settings.css +24 -4
  115. package/modules/hall-ui/public/settings.html +21 -17
  116. package/modules/hall-ui/public/settings.js +44 -44
  117. package/modules/hall-ui/public/settings.states.json +1 -0
  118. package/modules/hall-ui/public/shell-game.css +110 -0
  119. package/modules/hall-ui/public/shell-game.js +316 -3
  120. package/modules/hall-ui/public/shell.js +15 -66
  121. package/modules/hall-ui/public/sky-interaction.js +7 -0
  122. package/modules/hall-ui/public/speciality-tools.js +1 -2
  123. package/modules/hall-ui/public/street-card.states.json +1 -0
  124. package/modules/hall-ui/public/street-inside.js +3 -4
  125. package/modules/hall-ui/public/street.css +1 -5
  126. package/modules/hall-ui/public/street.html +6 -10
  127. package/modules/hall-ui/public/street.js +8 -59
  128. package/modules/hall-ui/public/street.states.json +7 -32
  129. package/modules/hall-ui/public/studio.html +5 -5
  130. package/modules/hall-ui/public/studio.states.json +1 -0
  131. package/modules/hall-ui/public/style.css +197 -112
  132. package/modules/hall-ui/public/task-detail.js +5 -4
  133. package/modules/hall-ui/public/task.html +5 -5
  134. package/modules/hall-ui/public/task.states.json +1 -0
  135. package/modules/hall-ui/public/thinking.css +10 -0
  136. package/modules/hall-ui/public/thinking.html +9 -6
  137. package/modules/hall-ui/public/thinking.states.json +1 -0
  138. package/modules/hall-ui/public/tweak-editor.html +5 -5
  139. package/modules/hall-ui/public/tweak-editor.js +5 -1
  140. package/modules/hall-ui/public/tweak-editor.states.json +1 -0
  141. package/modules/hall-ui/public/update-pill.js +91 -2
  142. package/modules/hall-ui/public/visor.css +80 -0
  143. package/modules/hall-ui/public/visor.js +316 -0
  144. package/modules/hall-ui/public/watch-repo-invite.js +159 -0
  145. package/modules/hall-ui/public/watch.html +6 -5
  146. package/modules/hall-ui/public/watch.js +17 -2
  147. package/modules/hall-ui/public/watch.states.json +1 -0
  148. package/modules/hall-ui/public/welcome-boot.js +15 -6
  149. package/modules/hall-ui/public/work-founding.states.json +1 -0
  150. package/modules/hall-ui/public/work.html +5 -5
  151. package/modules/hall-ui/public/work.states.json +1 -0
  152. package/modules/hall-ui/records/builders-roster.md +2 -2
  153. package/modules/hall-ui/records/craft-mode.md +13 -11
  154. package/modules/hall-ui/records/game-mode.md +2 -2
  155. package/modules/hall-ui/records/governor-street.md +1 -1
  156. package/modules/hall-ui/records/home-city.md +3 -1
  157. package/modules/hall-ui/records/market-watch-settings.md +2 -0
  158. package/modules/hall-ui/records/options-menu.md +15 -0
  159. package/modules/hall-ui/records/profile-roles.md +11 -11
  160. package/modules/hall-ui/records/sky.md +3 -3
  161. package/modules/hall-ui/records/start-screen.md +25 -0
  162. package/modules/hall-ui/records/tweak-editor.md +1 -1
  163. package/modules/hall-ui/records/update-pill.md +12 -0
  164. package/modules/hall-ui/records/visor.md +23 -0
  165. package/modules/hall-ui/records/welcome-tower.md +5 -5
  166. package/modules/hall-ui/specimens/components.states.json +2 -1
  167. package/modules/ideas/ideator-stats.js +3 -1
  168. package/modules/ideas/inbox.js +2 -2
  169. package/modules/lifecycle/activity.js +1 -1
  170. package/modules/lifecycle/cascade.js +4 -4
  171. package/modules/lifecycle/goal-spec-task.js +1 -1
  172. package/modules/lifecycle/help-requests.js +19 -15
  173. package/modules/lifecycle/kickoff-checklist.js +3 -3
  174. package/modules/lifecycle/kickoff-seed.js +1 -1
  175. package/modules/lifecycle/migrations/lifecycle_021_crafts_artisan_artist.sql +29 -0
  176. package/modules/lifecycle/mingle.js +4 -4
  177. package/modules/lifecycle/page-tweak-write.js +1 -1
  178. package/modules/lifecycle/routes/task-write-routes.js +3 -1
  179. package/modules/lifecycle/routes/tasks.js +3 -3
  180. package/modules/lifecycle/ship-card.js +2 -3
  181. package/modules/lifecycle/task-classifier.js +39 -29
  182. package/modules/lifecycle/tests/cascade.mjs +7 -7
  183. package/modules/onboarding/founding-mode.js +3 -0
  184. package/modules/onboarding/genesis-home.js +21 -1
  185. package/modules/onboarding/routes/onboarding.js +2 -0
  186. package/modules/page-notes/notes.js +3 -2
  187. package/modules/page-notes/split.js +1 -1
  188. package/modules/platform-identity/craft-rollup.js +35 -7
  189. package/modules/platform-identity/migrations/platform_identity_036_crafts_artisan_artist.sql +45 -0
  190. package/modules/platform-identity/platform-identity.js +2 -2
  191. package/modules/platform-identity/routes/community-leaderboard.js +2 -1
  192. package/modules/platform-identity/routes/community-search.js +2 -1
  193. package/modules/platform-identity/routes/sso.js +2 -1
  194. package/modules/platform-identity/tests/platform-identity.mjs +6 -6
  195. package/modules/public-landing/public/community.html +3 -5
  196. package/modules/public-landing/public/index.html +1 -1
  197. package/modules/public-landing/public/projects.html +1 -1
  198. package/modules/specialities/migrations/specialities_004_crafts_artisan_artist.sql +30 -0
  199. package/modules/specialities/routes/specialities.js +2 -2
  200. package/modules/status-ui/public/status.js +1 -1
  201. package/modules/ui-design/kit/fixtures/community-leaderboard.json +10 -10
  202. package/modules/ui-design/kit/fixtures/community-search.json +2 -2
  203. package/modules/ui-design/kit/fixtures/scouting-recruiter.json +1 -1
  204. package/modules/ui-design/public/brand.html +2 -2
  205. package/package-lock.json +2 -2
  206. package/package.json +1 -1
  207. package/release-notes.json +76 -0
  208. package/scripts/bongos/autobongos-loop.js +1 -1
  209. package/scripts/bongos/discipline-modes.json +7 -12
  210. package/scripts/bongos/fitness.js +1 -1
  211. package/scripts/bongos/gen-hall-sitemap.js +7 -7
  212. package/scripts/bongos/grade-smoke.js +2 -2
  213. package/scripts/bongos/init.js +2 -2
  214. package/scripts/bongos/newcomer-floor.config.json +1 -2
  215. package/scripts/bongos/newcomer-floor.js +4 -3
  216. package/scripts/bongos/newcomer-floor.templates.json +10 -12
  217. package/scripts/bongos/newcomer-floor.test.js +47 -38
  218. package/scripts/bongos/role-pack-guard.js +5 -4
  219. package/scripts/bongos/seed-tasks.example.json +3 -3
  220. package/scripts/bongos/setup.js +20 -107
  221. package/scripts/bongos/start.js +2 -2
  222. package/scripts/bongos/task.js +5 -4
  223. package/scripts/bongos/triage.js +1 -1
  224. package/scripts/bongos/worktree.js +16 -4
  225. package/scripts/hall-preview/README.md +2 -3
  226. package/scripts/setup-builder.js +6 -46
  227. package/src/bongos/auth-admission.js +8 -8
  228. package/src/bongos/craft-mode.js +2 -0
  229. package/src/bongos/db.js +39 -1
  230. package/src/bongos/github-repos.js +78 -0
  231. package/src/bongos/module-scope-map.js +22 -0
  232. package/src/bongos/role-stats.js +27 -11
  233. package/src/bongos/routes/github.js +100 -2
  234. package/src/bongos/routes/me.js +33 -0
  235. package/src/bongos/serve-internal.js +33 -11
  236. package/src/crafts.js +66 -0
  237. package/src/hall-helm.js +79 -0
  238. package/src/module-api.js +6 -1
  239. package/src/module-loader/manifest-schema.js +16 -0
  240. package/src/modules.js +2 -2
  241. package/src/role-responsibilities.js +6 -5
  242. package/tests/acclaim_feed_evidence_db.mjs +1 -1
  243. package/tests/activity_rollup_as_of.mjs +1 -1
  244. package/tests/api_alias_caller_ratchet.mjs +3 -3
  245. package/tests/application_lifecycle.mjs +1 -1
  246. package/tests/artist_review.mjs +1 -1
  247. package/tests/artist_review_goal.mjs +1 -1
  248. package/tests/blocker_hall_live_proof.mjs +1 -1
  249. package/tests/blocker_tasks_named_db.mjs +2 -2
  250. package/tests/canonical_profile_url.mjs +1 -1
  251. package/tests/cap_newcomer_tasks.mjs +13 -13
  252. package/tests/character_creation.mjs +294 -0
  253. package/tests/claim_gate_open_blocker.mjs +2 -2
  254. package/tests/claim_gate_rebase.mjs +6 -6
  255. package/tests/claude_materialize.mjs +1 -1
  256. package/tests/clawback_confirmed_backlog.mjs +3 -3
  257. package/tests/collab_page.mjs +8 -9
  258. package/tests/collab_self_settling.mjs +2 -2
  259. package/tests/community_leaderboard_route.mjs +13 -5
  260. package/tests/community_proof_db.mjs +3 -0
  261. package/tests/community_search_route.mjs +7 -6
  262. package/tests/conductor.mjs +1 -1
  263. package/tests/conductor_main_e2e.mjs +8 -8
  264. package/tests/copy_desk_flags_db.mjs +1 -1
  265. package/tests/copy_desk_page_approve.mjs +2 -2
  266. package/tests/copy_desk_page_claim.mjs +2 -2
  267. package/tests/copy_desk_page_next.mjs +1 -1
  268. package/tests/copy_desk_page_release.mjs +1 -1
  269. package/tests/copy_desk_page_submit.mjs +2 -2
  270. package/tests/core_258_box_session_claims_db.mjs +1 -1
  271. package/tests/craft_mode.mjs +48 -34
  272. package/tests/craft_mode_home.mjs +20 -20
  273. package/tests/craft_mode_looks.mjs +16 -15
  274. package/tests/craft_mode_nav.mjs +31 -29
  275. package/tests/craft_review_mode.mjs +46 -41
  276. package/tests/crafts_vocabulary.mjs +70 -0
  277. package/tests/cross_discipline_priority_direction.mjs +5 -5
  278. package/tests/design_reference.mjs +1 -1
  279. package/tests/fitness.mjs +7 -11
  280. package/tests/funded_project_pays_proof_db.mjs +1 -1
  281. package/tests/genesis_home.mjs +21 -1
  282. package/tests/github_chip.mjs +3 -3
  283. package/tests/github_repo_invite.mjs +379 -0
  284. package/tests/goal_criterion_confirm_ui.mjs +1 -1
  285. package/tests/goal_invite_consent.mjs +1 -1
  286. package/tests/goal_map_page.mjs +2 -2
  287. package/tests/goal_sort_staleness.mjs +1 -1
  288. package/tests/goal_spec_task.mjs +1 -1
  289. package/tests/goal_spec_task_db.mjs +3 -3
  290. package/tests/grade_engine.mjs +2 -2
  291. package/tests/grader_no_artifact.mjs +1 -1
  292. package/tests/grader_prompt_fencing.mjs +1 -1
  293. package/tests/grader_prompt_window_bound.mjs +1 -1
  294. package/tests/grader_reader_lens.mjs +2 -2
  295. package/tests/grader_severity_gate.mjs +1 -1
  296. package/tests/grader_unbacked_score.mjs +1 -1
  297. package/tests/hall_approval_queue.mjs +1 -1
  298. package/tests/hall_board_world.mjs +2 -2
  299. package/tests/hall_builders_page_pass.mjs +3 -2
  300. package/tests/hall_city.mjs +16 -30
  301. package/tests/hall_controls.mjs +52 -4
  302. package/tests/hall_fullscreen.mjs +27 -3
  303. package/tests/hall_helm.mjs +187 -0
  304. package/tests/hall_kit_charts.mjs +1 -1
  305. package/tests/hall_landing_boot.mjs +1 -1
  306. package/tests/hall_my_agents.mjs +16 -16
  307. package/tests/hall_nav.mjs +22 -15
  308. package/tests/hall_options_menu.mjs +247 -0
  309. package/tests/hall_oversight_world.mjs +1 -1
  310. package/tests/hall_palette.mjs +8 -9
  311. package/tests/hall_profile_world.mjs +3 -3
  312. package/tests/hall_record_world.mjs +6 -4
  313. package/tests/hall_settings_world.mjs +2 -1
  314. package/tests/hall_sitemap.mjs +6 -5
  315. package/tests/hall_start_screen.mjs +307 -0
  316. package/tests/hall_street.mjs +15 -28
  317. package/tests/hall_street_inside.mjs +17 -9
  318. package/tests/hall_studio_world.mjs +3 -3
  319. package/tests/hall_topbar_320.mjs +4 -2
  320. package/tests/hall_visor.mjs +224 -0
  321. package/tests/help_requests.mjs +47 -15
  322. package/tests/helpers.mjs +4 -4
  323. package/tests/home_city.mjs +6 -4
  324. package/tests/home_city_widget.mjs +33 -14
  325. package/tests/home_gallery_widget.mjs +4 -4
  326. package/tests/home_idea_box.mjs +6 -6
  327. package/tests/hub_community_page.mjs +3 -3
  328. package/tests/hub_craft_rollup.mjs +75 -36
  329. package/tests/hub_recent_ships.mjs +3 -2
  330. package/tests/idea_cascade.mjs +2 -2
  331. package/tests/idea_credit_streams_db.mjs +1 -1
  332. package/tests/idea_detail_page.mjs +1 -1
  333. package/tests/idea_ideate_full.mjs +5 -2
  334. package/tests/idea_spark_hall.mjs +1 -1
  335. package/tests/init.mjs +3 -3
  336. package/tests/instance_manifest.mjs +2 -2
  337. package/tests/kickoff_checklist.mjs +1 -1
  338. package/tests/leaderboard_roles.mjs +34 -25
  339. package/tests/lifecycle_claim_ladder_refs.mjs +55 -0
  340. package/tests/lightbulb.mjs +14 -10
  341. package/tests/main_discipline.mjs +8 -8
  342. package/tests/markdown_line_endings.mjs +1 -1
  343. package/tests/market_watch_pages.mjs +52 -74
  344. package/tests/me_streak_leaderboard_queries.mjs +3 -3
  345. package/tests/migration_allocation.mjs +1 -1
  346. package/tests/mingle.mjs +36 -21
  347. package/tests/module-scope-map.mjs +22 -2
  348. package/tests/module_api.mjs +1 -0
  349. package/tests/module_contributions.mjs +13 -13
  350. package/tests/module_manifest.mjs +29 -0
  351. package/tests/module_skills_hook.mjs +2 -2
  352. package/tests/modules.mjs +14 -7
  353. package/tests/nav_permission_atoms.mjs +2 -1
  354. package/tests/needs_rebase.mjs +1 -1
  355. package/tests/newcomer_restock.mjs +13 -13
  356. package/tests/page_notes_panel.mjs +22 -22
  357. package/tests/page_tweak_no_block.mjs +1 -1
  358. package/tests/paintbrush.mjs +7 -7
  359. package/tests/privacy_matrix.mjs +1 -0
  360. package/tests/profile_activity.mjs +20 -23
  361. package/tests/profile_earnings.mjs +1 -1
  362. package/tests/profile_nudge_prefs.mjs +4 -4
  363. package/tests/profile_role_headline.mjs +73 -46
  364. package/tests/profile_role_stats.mjs +55 -45
  365. package/tests/profile_rollup_consent.mjs +19 -6
  366. package/tests/profile_route.mjs +12 -12
  367. package/tests/profile_ui_cross_project.mjs +4 -4
  368. package/tests/project_invite_suggestions.mjs +2 -2
  369. package/tests/promote_goal_id.mjs +1 -1
  370. package/tests/publish_manifest.mjs +1 -1
  371. package/tests/rank_floor_wiring.mjs +1 -1
  372. package/tests/regrade_actual_minutes.mjs +1 -1
  373. package/tests/removal_proposal.mjs +2 -2
  374. package/tests/reward_gate.mjs +4 -4
  375. package/tests/reward_mode_cost_plus_only.mjs +1 -1
  376. package/tests/role_responsibilities.mjs +18 -12
  377. package/tests/role_stats_db.mjs +17 -16
  378. package/tests/scouting_rollup_fanout.mjs +5 -5
  379. package/tests/seed_tasks.mjs +5 -2
  380. package/tests/session_earnings_mirror_db.mjs +1 -1
  381. package/tests/session_reward_on_land_db.mjs +1 -1
  382. package/tests/session_reward_reconcile_db.mjs +2 -2
  383. package/tests/sessions_kit_pager.mjs +1 -1
  384. package/tests/settings_art_key_absent.mjs +2 -2
  385. package/tests/settings_main_role.mjs +21 -20
  386. package/tests/setup_flow.mjs +3 -3
  387. package/tests/setup_smoke.mjs +20 -0
  388. package/tests/ship_card.mjs +6 -6
  389. package/tests/specialities.mjs +16 -4
  390. package/tests/speciality_db_shape.mjs +6 -6
  391. package/tests/speciality_injection.mjs +17 -17
  392. package/tests/speciality_offer_ui.mjs +5 -5
  393. package/tests/speciality_offered.mjs +6 -5
  394. package/tests/speciality_session_skills.mjs +7 -7
  395. package/tests/speciality_skills.mjs +11 -11
  396. package/tests/speciality_walkthrough_ui.mjs +2 -2
  397. package/tests/sso_checkin_identity_spoof.mjs +2 -1
  398. package/tests/task_classifier_vocab.mjs +10 -0
  399. package/tests/task_cli.mjs +11 -3
  400. package/tests/task_criteria_unlink_db.mjs +2 -2
  401. package/tests/task_discipline_required.mjs +38 -16
  402. package/tests/task_goal_required.mjs +4 -4
  403. package/tests/task_needs_db.mjs +1 -1
  404. package/tests/tweak_replaces_artist_review.mjs +1 -1
  405. package/tests/update_pill.mjs +153 -2
  406. package/tests/upkeep_home.mjs +1 -1
  407. package/tests/v3_c1_options_proof.mjs +145 -0
  408. package/tests/visual_review.mjs +3 -3
  409. package/tests/watch_applications_queue.mjs +8 -5
  410. package/tests/watch_sealed_floor.mjs +3 -1
  411. package/tests/welcome_boot.mjs +12 -9
  412. package/tests/welcome_boot_browser.mjs +13 -13
  413. package/tests/worktree_helper.mjs +16 -1
@@ -0,0 +1,294 @@
1
+ // tests/character_creation.mjs — character creation and the three sprites (task 1005147,
2
+ // BONGOS-V3 G2, criterion you-are-a-character-in-first-person).
3
+ //
4
+ // A builder picks one of three characters when they join, and the pick lives on their
5
+ // ACCOUNT (core_297, PUT /me/character). These cases run the REAL craft-mode.js in a
6
+ // small DOM stub and drive it the way the shell does once /me answers: no character
7
+ // opens character creation; a pick is saved and worn; a held character is the only one
8
+ // offered and the top bar wears its sprite; changing it is signing out. The server half
9
+ // (migration, route, db write) is pinned beside them.
10
+ import assert from 'node:assert/strict';
11
+ import { test } from 'node:test';
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import vm from 'node:vm';
15
+ import { createRequire } from 'node:module';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { realOTB } from './helpers.mjs';
18
+
19
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
20
+ const HALL = path.join(ROOT, 'modules/hall-ui/public');
21
+ const CRAFT = fs.readFileSync(path.join(HALL, 'craft-mode.js'), 'utf8');
22
+ const require = createRequire(import.meta.url);
23
+
24
+ function el(tag) {
25
+ const attrs = {}, listeners = {};
26
+ const e = {
27
+ tagName: tag, id: '', className: '', dataset: {}, children: [], parentNode: null, _html: '', textContent: '',
28
+ classList: { _s: new Set(), add(c) { this._s.add(c); }, remove(c) { this._s.delete(c); }, toggle(c, on) { if (on) this._s.add(c); else this._s.delete(c); }, contains(c) { return this._s.has(c); } },
29
+ setAttribute(k, v) { attrs[k] = String(v); }, getAttribute(k) { return k in attrs ? attrs[k] : null; },
30
+ hasAttribute(k) { return k in attrs; }, removeAttribute(k) { delete attrs[k]; },
31
+ addEventListener(t, fn) { (listeners[t] ||= []).push(fn); },
32
+ fire(t, ev) { for (const fn of listeners[t] || []) fn(ev); },
33
+ appendChild(c) { c.parentNode = e; e.children.push(c); return c; },
34
+ removeChild(c) { e.children = e.children.filter((x) => x !== c); c.parentNode = null; },
35
+ contains() { return false; },
36
+ focus() {},
37
+ querySelector(sel) { return e._q(sel); },
38
+ querySelectorAll() { return []; },
39
+ set innerHTML(v) { e._html = String(v); e._err = null; }, get innerHTML() { return e._html; },
40
+ _q(sel) {
41
+ if (sel === '[data-char-error]' && /data-char-error/.test(e._html)) return (e._err ||= el('P'));
42
+ return null;
43
+ },
44
+ };
45
+ return e;
46
+ }
47
+
48
+ function boot({ store = {}, session = {}, put = () => ({ ok: true, status: 200, data: null }) } = {}) {
49
+ const root = el('HTML');
50
+ const body = el('BODY');
51
+ const requests = [];
52
+ const keyListeners = [];
53
+ const docObj = {
54
+ body, documentElement: root, activeElement: null, readyState: 'complete',
55
+ getElementById(id) {
56
+ if (id === 'craft-picker') return body.children.find((c) => c.id === 'craft-picker') || null;
57
+ return null;
58
+ },
59
+ querySelector: () => null, querySelectorAll: () => [],
60
+ createElement: (t) => el(t.toUpperCase()),
61
+ addEventListener(t, fn) { if (t === 'keydown') keyListeners.push(fn); },
62
+ removeEventListener(t, fn) { const i = keyListeners.indexOf(fn); if (i >= 0) keyListeners.splice(i, 1); },
63
+ dispatchEvent() { return true; },
64
+ };
65
+ const mk = (obj) => ({
66
+ getItem: (k) => (k in obj ? obj[k] : null),
67
+ setItem: (k, v) => { obj[k] = String(v); },
68
+ removeItem: (k) => { delete obj[k]; },
69
+ });
70
+ const win = {
71
+ OTB: realOTB(),
72
+ localStorage: mk(store), sessionStorage: mk(session),
73
+ location: { search: '', assign() {} },
74
+ matchMedia: () => ({ matches: false }),
75
+ BongosClient: {
76
+ createClient: () => ({
77
+ request(method, url, opts) {
78
+ requests.push({ method, url, body: opts && opts.body });
79
+ return Promise.resolve(put(method, url, opts));
80
+ },
81
+ }),
82
+ },
83
+ };
84
+ const sandbox = {
85
+ window: win, document: docObj, console, Promise,
86
+ CustomEvent: class { constructor(t, i) { this.type = t; Object.assign(this, i); } },
87
+ };
88
+ sandbox.globalThis = sandbox;
89
+ vm.createContext(sandbox);
90
+ vm.runInContext(CRAFT, sandbox, { filename: 'craft-mode.js' });
91
+ const picker = () => body.children.find((c) => c.id === 'craft-picker') || null;
92
+ const signedIn = { authed: true };
93
+ return { win, root, body, store, session, requests, keyListeners, picker, signedIn, C: win.OTBCraftMode };
94
+ }
95
+ const builder = { id: 7, github_login: 'ada', main_discipline: null };
96
+ const flush = () => new Promise((r) => setImmediate(r));
97
+
98
+ test('no character yet: character creation opens, three characters, nothing to dismiss', () => {
99
+ const t = boot();
100
+ assert.equal(t.C.characterGate(t.signedIn, { builder, character: null }), true, 'the gate holds the welcome and the picker back');
101
+ const p = t.picker();
102
+ assert.ok(p, 'the creation screen is up');
103
+ assert.match(p.innerHTML, /Choose your character/);
104
+ const doors = [...p.innerHTML.matchAll(/data-char-pick="([a-z]+)"/g)].map((m) => m[1]);
105
+ assert.deepEqual(doors, ['artisan', 'artist', 'patron'], 'Artisan, Artist, Patron — three, in that order');
106
+ for (const id of doors) assert.ok(p.innerHTML.includes(`/builders/characters/${id}.png`), `${id} stands on its sprite`);
107
+ assert.ok(!/data-craft-close/.test(p.innerHTML), 'no close button: the pick is the way through');
108
+ // Escape does not dismiss it.
109
+ let prevented = false;
110
+ for (const fn of t.keyListeners.slice()) fn({ key: 'Escape', preventDefault() { prevented = true; } });
111
+ assert.ok(prevented && t.picker(), 'Escape is swallowed and the screen stays');
112
+ });
113
+
114
+ test('an artist-led builder starts on the Artist door', () => {
115
+ const t = boot();
116
+ t.C.characterGate(t.signedIn, { builder: { ...builder, main_discipline: 'artist' }, character: null });
117
+ assert.match(t.picker().innerHTML, /craft-door--pre[^"]*" type="button" data-char-pick="artist"/);
118
+ });
119
+
120
+ test('picking saves the character to the account and the hall wears it', async () => {
121
+ const t = boot({ put: (m, u, o) => ({ ok: true, status: 200, data: { character: o.body.character } }) });
122
+ t.C.characterGate(t.signedIn, { builder, character: null });
123
+ const target = { closest: (sel) => (sel === 'button[data-char-pick]' ? { getAttribute: () => 'artist' } : null) };
124
+ t.picker().fire('click', { target });
125
+ await flush();
126
+ assert.deepEqual(JSON.parse(JSON.stringify(t.requests[0])), { method: 'PUT', url: '/api/bongos/me/character', body: { character: 'artist' } });
127
+ assert.equal(t.picker(), null, 'the screen closes');
128
+ assert.equal(t.root.dataset.craft, 'artist', 'the hall wears the character');
129
+ assert.equal(t.store['hall-character'], 'artist', 'remembered for the next first paint');
130
+ assert.equal(t.C.character(), 'artist');
131
+ });
132
+
133
+ test('a failed save keeps the screen up and says so', async () => {
134
+ const t = boot({ put: () => ({ ok: false, status: 500, data: null }) });
135
+ t.C.characterGate(t.signedIn, { builder, character: null });
136
+ const target = { closest: () => ({ getAttribute: () => 'artisan' }) };
137
+ t.picker().fire('click', { target });
138
+ await flush(); await flush();
139
+ assert.ok(t.picker(), 'still choosing');
140
+ const err = t.picker().querySelector('[data-char-error]');
141
+ assert.match(err.textContent, /could not be saved/);
142
+ assert.equal(err.hasAttribute('hidden'), false);
143
+ });
144
+
145
+ test('a held character is the only one offered, and the top bar wears its sprite', () => {
146
+ const t = boot();
147
+ assert.equal(t.C.characterGate(t.signedIn, { builder, character: 'artisan' }), false, 'nothing to create: the welcome may show');
148
+ assert.equal(t.picker(), null);
149
+ assert.equal(t.root.dataset.craft, 'artisan');
150
+ assert.equal(t.C.offered('artisan'), true);
151
+ assert.equal(t.C.offered('artist'), false, 'another character cannot be worn without signing out');
152
+ assert.equal(t.C.offered('patron'), false);
153
+ const sw = t.C.switchHtml();
154
+ assert.match(sw, /craft-switch--character/);
155
+ assert.match(sw, /data-craft-me/);
156
+ assert.match(sw, /\/builders\/characters\/artisan\.png/);
157
+ assert.ok(!/data-craft-opt=/.test(sw), 'the three-way switch is gone');
158
+ // A session's stored mode from before the character cannot override it.
159
+ t.session['hall-craft'] = 'patron';
160
+ assert.equal(t.C.current(), 'artisan');
161
+ });
162
+
163
+ test('the sprite is drawn on first paint from the last held character, before /me answers', () => {
164
+ const t = boot({ store: { 'hall-character': 'patron' } });
165
+ assert.match(t.C.switchHtml(), /\/builders\/characters\/patron\.png/);
166
+ });
167
+
168
+ test('an older core that says nothing about characters changes nothing', () => {
169
+ const t = boot();
170
+ assert.equal(t.C.characterGate(t.signedIn, { builder }), false);
171
+ assert.equal(t.picker(), null);
172
+ assert.equal(t.C.character(), undefined);
173
+ assert.equal(t.C.offered('artist'), true, 'the three crafts stay offered as before');
174
+ });
175
+
176
+ test('a visitor who is not signed in is never asked', () => {
177
+ const t = boot();
178
+ assert.equal(t.C.characterGate({ authed: false }, { character: null }), false);
179
+ assert.equal(t.picker(), null);
180
+ });
181
+
182
+ test('the character section never writes `hidden` onto the nav: a character only marks [data-craft-off]', () => {
183
+ const start = CRAFT.indexOf('// ---------- character creation (task 1005147');
184
+ const end = CRAFT.indexOf('// ---------- the shell\'s three calls');
185
+ const section = CRAFT.slice(start, end);
186
+ assert.ok(start > 0 && end > start);
187
+ assert.ok(!/nav-item|app-sidebar/.test(section), 'the character code does not touch the nav itself; applyNav does');
188
+ });
189
+
190
+ test('the owner\'s three sprites are on disk, one per character', () => {
191
+ for (const id of ['artisan', 'artist', 'patron']) {
192
+ const f = path.join(HALL, 'characters', id + '.png');
193
+ assert.ok(fs.existsSync(f), `${id}.png`);
194
+ const head = fs.readFileSync(f).subarray(0, 8);
195
+ assert.deepEqual([...head], [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], `${id}.png is a PNG`);
196
+ }
197
+ });
198
+
199
+ // ---------- the server half ----------
200
+ test('core_297 adds the account column, and the CHECK names exactly the three characters', () => {
201
+ const sql = fs.readFileSync(path.join(ROOT, 'migrations/core_297_builder_character.sql'), 'utf8');
202
+ assert.match(sql, /ADD COLUMN IF NOT EXISTS game_character text;/);
203
+ assert.match(sql, /game_character IN \('artisan', 'artist', 'patron'\)/);
204
+ assert.match(sql, /INSERT INTO schema_migrations \(version\) VALUES \('core_297_builder_character'\)/);
205
+ });
206
+
207
+ test('PUT /me/character is a self-service write behind requireBuilder, and /me reports the character fail-open', () => {
208
+ const src = fs.readFileSync(path.join(ROOT, 'src/bongos/routes/me.js'), 'utf8');
209
+ assert.match(src, /router\.put\('\/me\/character', auth\.requireBuilder,/);
210
+ assert.match(src, /db\.setBuilderCharacter\(req\.builder\.id, raw\)/, 'only ever the caller\'s own row');
211
+ assert.match(src, /character: await characterP,/);
212
+ assert.match(src, /try \{ return await db\.getBuilderCharacter\(req\.builder\.id\); \} catch \(_\) \{ return null; \}/);
213
+ });
214
+
215
+ // The real route over HTTP: the real me.js router and error envelope, with only the
216
+ // sign-in (auth.requireBuilder) and the two db calls stood in for.
217
+ test('PUT /me/character over HTTP: sets, clears, and refuses with stable error codes', async () => {
218
+ const express = require('express');
219
+ const authPath = require.resolve('../src/bongos/auth.js');
220
+ const dbPath = require.resolve('../src/bongos/db.js');
221
+ const mePath = require.resolve('../src/bongos/routes/me.js');
222
+ const auth = require(authPath);
223
+ const db = require(dbPath);
224
+ const saved = { requireBuilder: auth.requireBuilder, setBuilderCharacter: db.setBuilderCharacter };
225
+ const writes = [];
226
+ auth.requireBuilder = (req, res, next) => {
227
+ if (req.get('x-test-signed-out')) return res.status(401).json({ error: { code: 'unauthorized' } });
228
+ req.builder = { id: 41, rank: 'xenos' };
229
+ next();
230
+ };
231
+ db.setBuilderCharacter = async (id, c) => {
232
+ writes.push([id, c]);
233
+ if (c !== null && !['artisan', 'artist', 'patron'].includes(c)) { const e = new Error('x'); e.code = 'invalid_character'; throw e; }
234
+ return c;
235
+ };
236
+ delete require.cache[mePath];
237
+ const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
238
+ const app = express();
239
+ app.use(express.json());
240
+ app.use(attachFail);
241
+ app.use(require(mePath)());
242
+ const server = app.listen(0);
243
+ const base = `http://127.0.0.1:${server.address().port}`;
244
+ const put = (body, headers = {}) => fetch(base + '/me/character', {
245
+ method: 'PUT', headers: { 'content-type': 'application/json', ...headers }, body: JSON.stringify(body),
246
+ }).then(async (r) => ({ status: r.status, body: await r.json() }));
247
+ try {
248
+ let r = await put({ character: 'artist' });
249
+ assert.equal(r.status, 200);
250
+ assert.equal(r.body.character, 'artist');
251
+ r = await put({ character: null });
252
+ assert.equal(r.status, 200, 'null is the deliberate clear (change character)');
253
+ assert.equal(r.body.character, null);
254
+ r = await put({});
255
+ assert.equal(r.status, 400);
256
+ assert.equal(r.body.error.code, 'character_required', 'a stable slug, not prose');
257
+ r = await put({ character: 'emperor' });
258
+ assert.equal(r.status, 422);
259
+ assert.equal(r.body.error.code, 'invalid_character');
260
+ r = await put({ character: 7 });
261
+ assert.equal(r.status, 400, 'a non-string is refused by the body schema');
262
+ r = await put({ character: 'artist', rank: 'archon' });
263
+ assert.equal(r.status, 400, 'strict: an unknown field (an attempt to write anything else) is refused');
264
+ r = await put({ character: 'artist' }, { 'x-test-signed-out': '1' });
265
+ assert.equal(r.status, 401, 'signed out never reaches the write');
266
+ assert.deepEqual(writes.map((w) => w[0]), [41, 41, 41], 'every write that ran was the caller\'s own row');
267
+ } finally {
268
+ server.close();
269
+ auth.requireBuilder = saved.requireBuilder;
270
+ db.setBuilderCharacter = saved.setBuilderCharacter;
271
+ delete require.cache[mePath];
272
+ }
273
+ });
274
+
275
+ test('setBuilderCharacter writes a character, clears with null, normalizes an old spelling, refuses anything else', async () => {
276
+ const poolPath = require.resolve('../src/bongos/pool.js');
277
+ const dbPath = require.resolve('../src/bongos/db.js');
278
+ const seen = [];
279
+ const real = require.cache[poolPath];
280
+ require.cache[poolPath] = { id: poolPath, filename: poolPath, loaded: true, exports: { pool: { query: async (sql, params) => { seen.push(params); return { rows: [{ game_character: params[1] }] }; } } } };
281
+ delete require.cache[dbPath];
282
+ try {
283
+ const db = require(dbPath);
284
+ assert.equal(await db.setBuilderCharacter(7, 'artist'), 'artist');
285
+ assert.equal(await db.setBuilderCharacter(7, null), null);
286
+ assert.equal(await db.setBuilderCharacter(7, 'ruler'), 'patron', 'the sprite\'s old name lands as Patron');
287
+ await assert.rejects(db.setBuilderCharacter(7, 'governor-general'), { code: 'invalid_character' });
288
+ await assert.rejects(db.setBuilderCharacter(7, 'ui'), { code: 'invalid_character' }, 'a discipline is not a character');
289
+ assert.deepEqual(seen.map((p) => p[0]), [7, 7, 7], 'every write is the one builder\'s own row');
290
+ } finally {
291
+ if (real) require.cache[poolPath] = real; else delete require.cache[poolPath];
292
+ delete require.cache[dbPath];
293
+ }
294
+ });
@@ -53,8 +53,8 @@ try {
53
53
  // Archon so no rank/newcomer gate interferes; the gate under test is OPEN_BLOCKER.
54
54
  await q(`INSERT INTO builders (id,github_id,github_login,display_name,rank) VALUES ($1,'ob-w','ob-worker','OB Worker','archon')`, [WORKER]);
55
55
  // TRAP: ready (feed-hidden) with an open blocker linked. CONTROL: ready, clean.
56
- await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'trap','ready',10,'feature','engineer')`, [TRAP, VER]);
57
- await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'control','ready',10,'feature','engineer')`, [CONTROL, VER]);
56
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'trap','ready',10,'feature','artisan')`, [TRAP, VER]);
57
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'control','ready',10,'feature','artisan')`, [CONTROL, VER]);
58
58
  await q(`INSERT INTO blockers (id,title,status) VALUES ($1,'Owner must flip the switch','open')`, [BLOCKER]);
59
59
  // blocker_ref is NOT NULL and part of the key; linkBlockerToTask writes 'blocker:<id>'.
60
60
  await q(`INSERT INTO task_blockers (task_id,blocker_id,blocker_ref) VALUES ($1,$2,$3)`, [TRAP, BLOCKER, `blocker:${BLOCKER}`]);
@@ -60,11 +60,11 @@ try {
60
60
  // Archon so no rank/newcomer gate interferes; the gate under test is REBASE_REQUIRED.
61
61
  await q(`INSERT INTO builders (id,github_id,github_login,display_name,rank) VALUES ($1,'cg-w','cg-worker','CG Worker','archon')`, [WORKER]);
62
62
  // STALLED: a confirmed task owned by WORKER, flagged needs_rebase.
63
- await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by,discipline) VALUES ($1,$2,'stalled','confirmed',10,'feature',$3,'engineer')`, [STALLED, VER, WORKER]);
63
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by,discipline) VALUES ($1,$2,'stalled','confirmed',10,'feature',$3,'artisan')`, [STALLED, VER, WORKER]);
64
64
  await q(`INSERT INTO claims (task_id,builder_id,claimed_at) VALUES ($1,$2, now())`, [STALLED, WORKER]);
65
65
  await db.flagTaskNeedsRebase(String(STALLED), 'strand:no_branch_no_tip');
66
66
  // FRESH: a ready, claimable task the worker will try to pick up.
67
- await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'fresh','ready',10,'feature','engineer')`, [FRESH, VER]);
67
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'fresh','ready',10,'feature','artisan')`, [FRESH, VER]);
68
68
 
69
69
  // 1) With STALLED flagged, claiming FRESH is refused with REBASE_REQUIRED.
70
70
  let threw = null;
@@ -116,13 +116,13 @@ try {
116
116
  await q(`UPDATE claims SET released_at = now() WHERE task_id = $1 AND released_at IS NULL`, [FRESH]);
117
117
 
118
118
  const seedStrand = async (id, reason, ageHours) => {
119
- await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by,discipline) VALUES ($1,$2,'aged','confirmed',10,'feature',$3,'engineer')`, [id, VER, WORKER]);
119
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by,discipline) VALUES ($1,$2,'aged','confirmed',10,'feature',$3,'artisan')`, [id, VER, WORKER]);
120
120
  await q(`INSERT INTO claims (task_id,builder_id,claimed_at) VALUES ($1,$2, now())`, [id, WORKER]);
121
121
  await db.flagTaskNeedsRebase(String(id), reason);
122
122
  await q(`UPDATE tasks SET needs_rebase_at = now() - ($2::bigint * interval '1 hour') WHERE id = $1`, [id, ageHours]);
123
123
  };
124
124
 
125
- await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'fresh2','ready',10,'feature','engineer')`, [FRESH2, VER]);
125
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'fresh2','ready',10,'feature','artisan')`, [FRESH2, VER]);
126
126
 
127
127
  // 4) An unprovable strand AGED past the grace no longer refuses a claim.
128
128
  await seedStrand(AGED, 'strand:no_branch_no_tip', 48);
@@ -154,7 +154,7 @@ try {
154
154
  // 7) task 1004370 — the one task the gate lets through: a P1 kind=infra fix,
155
155
  // the shape a CI-unblock is filed in. With AGED_OTHER still gating, claiming
156
156
  // it succeeds; FRESH2 (a P1 feature) was refused in 6), so both halves count.
157
- await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,priority,discipline) VALUES ($1,$2,'ci fix','ready',10,'infra',1,'engineer')`, [CIFIX, VER]);
157
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,priority,discipline) VALUES ($1,$2,'ci fix','ready',10,'infra',1,'artisan')`, [CIFIX, VER]);
158
158
  const ciClaim = await db.claimTask({ taskId: String(CIFIX), builderId: String(WORKER), worktreeName: 'cg-cifix' });
159
159
  ok(ciClaim && Number(ciClaim.task_id) === CIFIX,
160
160
  'a P1 infra task is claimable while a rebase is owed — the CI-unblock fix is never gated out');
@@ -171,7 +171,7 @@ try {
171
171
  await q(`UPDATE tasks SET status='shipped', needs_rebase=false WHERE id=$1`, [AGED_OTHER]);
172
172
  await q(`UPDATE claims SET released_at = now() WHERE task_id = ANY($1::bigint[]) AND released_at IS NULL`, [[CIFIX]]);
173
173
  await seedStrand(GUARDED, 'strand:branch_modifies_executed_code:.gitattributes', 1);
174
- await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'fresh3','ready',10,'feature','engineer')`, [FRESH3, VER]);
174
+ await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'fresh3','ready',10,'feature','artisan')`, [FRESH3, VER]);
175
175
  const claim3 = await db.claimTask({ taskId: String(FRESH3), builderId: String(WORKER), worktreeName: 'cg-fresh3' });
176
176
  ok(claim3 && Number(claim3.task_id) === FRESH3, 'a confirmed task parked on a server-side guard does not refuse a new claim');
177
177
  ok((await db.needsRebaseTasksForBuilder(String(WORKER))).some((t) => Number(t.id) === GUARDED),
@@ -764,7 +764,7 @@ t('the materialise-at-claim trigger is DATA: the ui mode opts in, every other di
764
764
  const modes = JSON.parse(readFileSync(join(ROOT, 'scripts', 'bongos', 'discipline-modes.json'), 'utf8')).modes;
765
765
  assert.equal(modes.ui.materializeModuleSkills, true, 'a ui claim brings the skills /design names');
766
766
  assert.equal(modes.ui.skill, 'design', 'and still routes to /design');
767
- for (const d of ['engineer', 'ideator', 'artist']) {
767
+ for (const d of ['artisan', 'artist']) {
768
768
  assert.ok(!modes[d].materializeModuleSkills, `${d} did not opt in, so its claim output is unchanged`);
769
769
  }
770
770
  });
@@ -60,9 +60,9 @@ try {
60
60
  // Two still-confirmed tasks (unlanded) + one legitimately shipped task.
61
61
  // discipline is required since core_247 (the column default 'unclassified' fails its CHECK).
62
62
  await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by,discipline) VALUES
63
- ($1,$4,'confirmed A','confirmed',20,'feature',$2,'engineer'),
64
- ($5,$4,'confirmed B','confirmed',15,'feature',$3,'engineer'),
65
- ($6,$4,'shipped legit','shipped',30,'feature',$2,'engineer')`, [T_A, A, B, VER, T_B, T_S]);
63
+ ($1,$4,'confirmed A','confirmed',20,'feature',$2,'artisan'),
64
+ ($5,$4,'confirmed B','confirmed',15,'feature',$3,'artisan'),
65
+ ($6,$4,'shipped legit','shipped',30,'feature',$2,'artisan')`, [T_A, A, B, VER, T_B, T_S]);
66
66
  // Seed the credits the OLD confirm-time rule would have paid (credit_log INSERT
67
67
  // fires the total_credits trigger, exactly like a real award).
68
68
  await q(`INSERT INTO credit_log (builder_id,task_id,delta,reason,description) VALUES
@@ -481,8 +481,8 @@ test('roleWord and roleLabel disagree exactly where they should', () => {
481
481
  // Executed, not grepped: the acronym exception has to survive both ways, or
482
482
  // "You asked uis" ships the next time a ui task files a help request.
483
483
  const { roleLabel, roleWord } = domUtils();
484
- assert.equal(roleLabel('engineer'), 'Engineer');
485
- assert.equal(roleWord('engineer'), 'engineer');
484
+ assert.equal(roleLabel('artisan'), 'Artisan');
485
+ assert.equal(roleWord('artisan'), 'artisan');
486
486
  assert.equal(roleLabel('ui'), 'UI');
487
487
  assert.equal(roleWord('ui'), 'UI', 'an acronym is not a common noun in any sentence');
488
488
  assert.equal(roleLabel('unclassified'), 'No role yet');
@@ -524,13 +524,13 @@ test('the mingle sub-line reads as English for every pair, and never shows the c
524
524
  const word = (d) => (OTB.roleIsUnset(d) ? '' : OTB.roleWord(d));
525
525
  const line = (a, b) => L.pairSentence(word(a), word(b));
526
526
 
527
- assert.equal(line('engineer', 'artist'), 'An engineer — you are an artist. Mingle pairs across roles.');
528
- assert.equal(line('artist', 'ideator'), 'An artist — you are an ideator. Mingle pairs across roles.');
527
+ assert.equal(line('artisan', 'artist'), 'An artisan — you are an artist. Mingle pairs across roles.');
528
+ assert.equal(line('artist', 'artisan'), 'An artist — you are an artisan. Mingle pairs across roles.');
529
529
 
530
530
  // 'ui' is the acronym exception roleWord() already makes, and it is why the article
531
531
  // cannot be a plain vowel test — "an UI" is how that shortcut fails.
532
- assert.equal(line('ui', 'engineer'), 'A UI — you are an engineer. Mingle pairs across roles.');
533
- assert.equal(line('engineer', 'ui'), 'An engineer — you are a UI. Mingle pairs across roles.');
532
+ assert.equal(line('ui', 'artisan'), 'A UI — you are an artisan. Mingle pairs across roles.');
533
+ assert.equal(line('artisan', 'ui'), 'An artisan — you are a UI. Mingle pairs across roles.');
534
534
  });
535
535
 
536
536
  test('a pair with an unset role drops the comparison rather than naming the default', () => {
@@ -541,7 +541,7 @@ test('a pair with an unset role drops the comparison rather than naming the defa
541
541
  // The defect this replaced rendered the database's own word — "A unclassified — you are
542
542
  // a engineer" — which is the example V2 names. Nothing a person did not choose may reach
543
543
  // the sentence, whichever side is missing.
544
- for (const pair of [[OTB.ROLE_UNSET, 'engineer'], ['engineer', OTB.ROLE_UNSET], [null, 'artist'], ['', '']]) {
544
+ for (const pair of [[OTB.ROLE_UNSET, 'artisan'], ['artisan', OTB.ROLE_UNSET], [null, 'artist'], ['', '']]) {
545
545
  const out = L.pairSentence(word(pair[0]), word(pair[1]));
546
546
  assert.equal(out, 'Mingle pairs across roles.', `pair ${JSON.stringify(pair)}`);
547
547
  assert.doesNotMatch(out, /unclassified/);
@@ -550,9 +550,8 @@ test('a pair with an unset role drops the comparison rather than naming the defa
550
550
 
551
551
  test('aOrAn is the article rule, stated once', () => {
552
552
  const L = lib();
553
- assert.equal(L.aOrAn('engineer'), 'an');
553
+ assert.equal(L.aOrAn('artisan'), 'an');
554
554
  assert.equal(L.aOrAn('artist'), 'an');
555
- assert.equal(L.aOrAn('ideator'), 'an');
556
555
  assert.equal(L.aOrAn('UI'), 'a', 'the acronym is spoken "you-eye", so it takes "a"');
557
556
  assert.equal(L.aOrAn('ui'), 'a');
558
557
  });
@@ -257,8 +257,8 @@ test('a craft the caller does not hold is dropped before it binds', async () =>
257
257
  // The C01 hole, one surface over: a craft value that did not come from the
258
258
  // caller's own profile must never widen a scope.
259
259
  const r = recorder();
260
- await help.listArchivedForBuilder({ builderId: 7, crafts: ['wizard', 'ideator'] }, { pool: r.pool });
261
- assert.deepEqual(r.calls[0].params[1], ['ideator']);
260
+ await help.listArchivedForBuilder({ builderId: 7, crafts: ['wizard', 'artist'] }, { pool: r.pool });
261
+ assert.deepEqual(r.calls[0].params[1], ['artist']);
262
262
  });
263
263
 
264
264
  test('the archive ROUTE reads crafts from the profile and takes nothing from the query', () => {
@@ -52,11 +52,11 @@ const account = (id, handle, extra = {}) => ({
52
52
 
53
53
  // Deliberately NOT in rank order — the module ranks, the fake does not.
54
54
  const BOARD = [
55
- { acct: account('205', 'third-builder'), credits: 50, tasks_shipped: 1, karma: 0, disciplines: ['artist', 'engineer'] },
55
+ { acct: account('205', 'third-builder'), credits: 50, tasks_shipped: 1, karma: 0, disciplines: ['artist', 'artisan'] },
56
56
  { acct: account('201', 'hidden-builder', { hide_stats: true }), credits: 250, tasks_shipped: 8, karma: 1, disciplines: ['artist'] },
57
- { acct: account('200', 'top-builder'), credits: 300, tasks_shipped: 9, karma: 4, disciplines: ['engineer'] },
58
- { acct: account('202', 'private-builder', { account_visibility: 'private' }), credits: 240, tasks_shipped: 7, karma: 1, disciplines: ['engineer'] },
59
- { acct: account('203', 'prov-builder', { profile_state: 'provisional', terms_accepted_at: null }), credits: 230, tasks_shipped: 7, karma: 1, disciplines: ['engineer'] },
57
+ { acct: account('200', 'top-builder'), credits: 300, tasks_shipped: 9, karma: 4, disciplines: ['artisan'] },
58
+ { acct: account('202', 'private-builder', { account_visibility: 'private' }), credits: 240, tasks_shipped: 7, karma: 1, disciplines: ['artisan'] },
59
+ { acct: account('203', 'prov-builder', { profile_state: 'provisional', terms_accepted_at: null }), credits: 230, tasks_shipped: 7, karma: 1, disciplines: ['artisan'] },
60
60
  // Opted out of RECRUITING only — spec D3: that opt-out does not touch a leaderboard.
61
61
  { acct: account('204', 'optout-builder', { recruiter_discoverable: false }), credits: 120, tasks_shipped: 3, karma: 0, disciplines: ['ui'] },
62
62
  // Ties top-builder on every number: the handle decides, so it ranks second.
@@ -99,6 +99,7 @@ const moduleApiPath = require.resolve('../src/module-api.js');
99
99
  require.cache[moduleApiPath] = {
100
100
  id: moduleApiPath, filename: moduleApiPath, loaded: true,
101
101
  exports: {
102
+ crafts: require('../src/crafts.js'), // the real vocabulary (task 1005141)
102
103
  pool: engine,
103
104
  corsPublicGet: () => (_req, res, next) => { res.set('Access-Control-Allow-Origin', '*'); next(); },
104
105
  logger: () => ({ error: () => {}, info: () => {}, warn: () => {} }),
@@ -168,7 +169,7 @@ await t('all-time binds no window start; a window binds exactly its start', asyn
168
169
  await t('ranked credits → ships → karma → handle, with a trimmed discipline filter and clamped paging', async () => {
169
170
  const all = await lb.listLeaderboard({ window: 'all', pool: engine });
170
171
  assert.deepEqual(all.results.map((r) => r.handle), ['top-builder', 'twin-builder', 'optout-builder', 'third-builder']);
171
- const eng = await lb.listLeaderboard({ window: 'all', discipline: ' engineer ', limit: 5000, offset: 1, pool: engine });
172
+ const eng = await lb.listLeaderboard({ window: 'all', discipline: ' artisan ', limit: 5000, offset: 1, pool: engine });
172
173
  assert.deepEqual(eng.results.map((r) => [r.place, r.handle]), [[2, 'third-builder']]);
173
174
  assert.equal(eng.limit, lb.MAX_LIMIT);
174
175
  });
@@ -280,6 +281,13 @@ await t('a discipline filter narrows the board and is echoed', async () => {
280
281
  'the hidden builder holds artist too, and still does not appear');
281
282
  });
282
283
 
284
+ await t('an old link\'s ideator reads as today\'s artist (task 1005141)', async () => {
285
+ const r = await get(port, `${PATH}?window=30d&discipline=ideator`);
286
+ assert.equal(r.status, 200);
287
+ assert.equal(r.json.discipline, 'artist');
288
+ assert.deepEqual(r.json.results.map((x) => x.handle), ['third-builder']);
289
+ });
290
+
283
291
  await t('place counts from the page offset, and page echoes the clamped values', async () => {
284
292
  const r = await get(port, `${PATH}?limit=1&offset=1`);
285
293
  assert.equal(r.status, 200);
@@ -137,6 +137,9 @@ require.cache[moduleApiPath] = {
137
137
  pool,
138
138
  corsPublicGet: () => (_req, _res, next) => next(),
139
139
  logger: () => ({ error: (o, m) => console.error(`route error: ${m}`, o), info: () => {}, warn: () => {} }),
140
+ // The routes fold an old craft name in ?discipline= through it (task 1005141); the real
141
+ // doorway's getter, so this stand-in cannot drift from it (task 1005193).
142
+ crafts: require('../src/crafts.js'),
140
143
  },
141
144
  };
142
145
  const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
@@ -102,7 +102,7 @@ const ACCOUNTS = [
102
102
  // active + public + NOT hidden — the plain control.
103
103
  { github_id: '100', handle: 'plain-vis', github_login: 'plainvis', display_name: 'Plain Visible',
104
104
  avatar_url: null, profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'public', hide_stats: false,
105
- disciplines: ['engineer'] },
105
+ disciplines: ['artisan'] },
106
106
  // active + public + HIDDEN — findable by name, not by discipline.
107
107
  { github_id: '101', handle: 'hidden-builder', github_login: 'hiddenbuilder', display_name: 'Hidden Builder',
108
108
  avatar_url: null, profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'public', hide_stats: true,
@@ -110,11 +110,11 @@ const ACCOUNTS = [
110
110
  // active + PRIVATE — absent entirely, findable or not.
111
111
  { github_id: '102', handle: 'private-builder', github_login: 'privatebuilder', display_name: 'Private Builder',
112
112
  avatar_url: null, profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'private', hide_stats: false,
113
- disciplines: ['engineer'] },
113
+ disciplines: ['artisan'] },
114
114
  // PROVISIONAL — absent entirely.
115
115
  { github_id: '103', handle: 'provisional-builder', github_login: 'provbuilder', display_name: 'Provisional Builder',
116
116
  avatar_url: null, profile_state: 'provisional', terms_accepted_at: null, account_visibility: 'public', hide_stats: false,
117
- disciplines: ['engineer'] },
117
+ disciplines: ['artisan'] },
118
118
  // active + public + NOT hidden, but 'octocat' sits mid-handle — a query for
119
119
  // it is NOT a prefix match, so this fixture exercises the FUZZY (`%`) leg
120
120
  // specifically, not the LIKE prefix leg.
@@ -185,6 +185,7 @@ const moduleApiPath = require.resolve('../src/module-api.js');
185
185
  require.cache[moduleApiPath] = {
186
186
  id: moduleApiPath, filename: moduleApiPath, loaded: true,
187
187
  exports: {
188
+ crafts: require('../src/crafts.js'), // the real vocabulary (task 1005141)
188
189
  pool: engine,
189
190
  corsPublicGet: () => (_req, _res, next) => next(),
190
191
  logger: () => ({ error: () => {}, info: () => {}, warn: () => {} }),
@@ -252,7 +253,7 @@ await t('the SAME hide_stats account drops out of a DISCIPLINE-filtered search',
252
253
  await t('a private account never appears — by name or by its real discipline', async () => {
253
254
  const byName = await get(port, '/api/bongos/community/search?q=private-builder');
254
255
  assert.ok(!byName.json.results.some((x) => x.github_login === 'privatebuilder'));
255
- const byDiscipline = await get(port, '/api/bongos/community/search?q=private&discipline=engineer');
256
+ const byDiscipline = await get(port, '/api/bongos/community/search?q=private&discipline=artisan');
256
257
  assert.ok(!byDiscipline.json.results.some((x) => x.github_login === 'privatebuilder'));
257
258
  });
258
259
 
@@ -271,8 +272,8 @@ await t('a case-differing, NON-PREFIX query hits the fuzzy leg (not just the LIK
271
272
  });
272
273
 
273
274
  await t('the plain visible account IS found under its real discipline, and ABSENT under another', async () => {
274
- const hit = await get(port, '/api/bongos/community/search?q=plain-vis&discipline=engineer');
275
- assert.ok(hit.json.results.some((x) => x.github_login === 'plainvis'), 'engineer is its real discipline');
275
+ const hit = await get(port, '/api/bongos/community/search?q=plain-vis&discipline=artisan');
276
+ assert.ok(hit.json.results.some((x) => x.github_login === 'plainvis'), 'artisan is its real discipline');
276
277
  const miss = await get(port, '/api/bongos/community/search?q=plain-vis&discipline=artist');
277
278
  assert.ok(!miss.json.results.some((x) => x.github_login === 'plainvis'), 'artist is not its discipline');
278
279
  });
@@ -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.