@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
@@ -117,18 +117,18 @@ function classifyTaskKind(title, description) {
117
117
  // -------------------------------------------------------------------------
118
118
  //
119
119
  // Orthogonal to kind. Classifies the builder-interest axis (migration 086):
120
- // engineer — the prompter is reasoning about algorithms, infra, data
120
+ // artisan — the prompter is reasoning about algorithms, infra, data
121
121
  // plumbing, dev tooling, scanners, schemas, integrations.
122
122
  // The person guiding the prompt brings software-engineering
123
123
  // experience.
124
- // artist — producing or shipping visual artifacts: sprite/tile
125
- // generation, palettes, walk animations, aesthetic passes.
126
- // ideator — thinking/writing/scoping: quest narrative, world/level
127
- // design, lore, briefs, research, naming.
124
+ // artist — producing or shipping visual artifacts (sprite/tile
125
+ // generation, palettes, walk animations, aesthetic passes)
126
+ // and, since task 1005141, the old ideator's lane too:
127
+ // thinking/writing/scoping, narrative, lore, briefs, naming.
128
128
  // unclassified — signals weak or balanced; hand-triage.
129
129
  //
130
130
  // The scoring engine first decides engineer-vs-creative; a creative verdict
131
- // is then split into artist/ideator by splitCreativeDiscipline().
131
+ // is then split into artist/ui by splitCreativeDiscipline().
132
132
  //
133
133
  // Calibrated against Lars's hand-classifications of 25 tasks (mig 014
134
134
  // backfill, 2026-05-12). Key insights from those decisions:
@@ -148,11 +148,11 @@ function classifyTaskKind(title, description) {
148
148
  // summed and capped. The 0.4 description weight is what stops incidental
149
149
  // creative phrasing in long descriptions from flipping engineering tasks.
150
150
 
151
- // Migration 086 split the old "creative" lane into 'artist' (visual asset
152
- // production) and 'ideator' (thinking/writing/scoping) and renamed
153
- // 'engineering' → 'engineer'. The binary scoring engine below still decides
154
- // engineer-vs-creative; a creative verdict is then disambiguated into
155
- // artist/ideator/ui by splitCreativeDiscipline(). When that split is unclear,
151
+ // Migration 086 split the old "creative" lane into artist (visual asset
152
+ // production) and ideator (thinking/writing/scoping); task 1005141 folded the
153
+ // ideator back into the artist craft and renamed engineer → artisan. The binary
154
+ // scoring engine below still decides engineer-vs-creative; a creative verdict
155
+ // is then disambiguated into artist/ui by splitCreativeDiscipline(). When that split is unclear,
156
156
  // it returns 'unclassified' (hand-triage) — matching the #615 backfill rule.
157
157
  //
158
158
  // Migration 176 (task 1376) added 'ui' — UI/interface DESIGN work (Figma,
@@ -160,7 +160,7 @@ function classifyTaskKind(title, description) {
160
160
  // pixel-art `artist` lane (ADR 0079): producing interface designs is a
161
161
  // distinct discipline from producing pixel-art assets, and routes to its own
162
162
  // /design work-mode (discipline-modes.json) instead of /paint.
163
- const VALID_DISCIPLINES = new Set(['engineer', 'artist', 'ideator', 'ui', 'unclassified']);
163
+ const VALID_DISCIPLINES = new Set(['artisan', 'artist', 'ui', 'unclassified']);
164
164
 
165
165
  // The disciplines a WRITE may set (task 1002988). 'unclassified' is deliberately
166
166
  // absent: it is a triage state, not a craft, and a create that lands one produces
@@ -169,12 +169,18 @@ const VALID_DISCIPLINES = new Set(['engineer', 'artist', 'ideator', 'ui', 'uncla
169
169
  // ?discipline=unclassified filter keep resolving while the backfill runs.
170
170
  // 'ui' stays: ADR 0272 keeps it as a module-contributed discipline, and the owner
171
171
  // reconfirmed that on 2026-09-17 when abandoning task 1002987.
172
- const WRITABLE_DISCIPLINES = new Set(['engineer', 'artist', 'ideator', 'ui']);
172
+ const WRITABLE_DISCIPLINES = new Set(['artisan', 'artist', 'ui']);
173
173
 
174
174
  // Mirrors the `kind` branch in the create routes: act on the classifier only when
175
175
  // it is at least half sure, otherwise treat it as no answer at all.
176
176
  const DISCIPLINE_CONFIDENCE_FLOOR = 0.5;
177
177
 
178
+ // The one place this module reads an old craft spelling as today's (task 1005141):
179
+ // engineer → artisan, ideator → artist. 'unclassified' and 'ui' pass through.
180
+ function normalizeDisciplineValue(d) {
181
+ return typeof d === 'string' ? require('../../src/module-api').crafts.normalizeCraft(d) : d;
182
+ }
183
+
178
184
  // Resolve the discipline for a task WRITE. Honours an explicit valid value,
179
185
  // otherwise INFERS from title + description + touches, and reports failure so the
180
186
  // caller can 400 — per the owner decision of 2026-09-17, a bare 400 on a missing
@@ -186,6 +192,8 @@ const DISCIPLINE_CONFIDENCE_FLOOR = 0.5;
186
192
  // have left the other quietly minting the very rows this task exists to remove.
187
193
  // Returns { discipline, inferred?, confidence? } or { error, unclassifiable? }.
188
194
  function resolveWriteDiscipline({ discipline, title, description, touches } = {}) {
195
+ // An old spelling ('engineer', 'ideator') is today's craft, not a bad one (task 1005141).
196
+ discipline = normalizeDisciplineValue(discipline);
189
197
  if (discipline != null && discipline !== 'unclassified') {
190
198
  return WRITABLE_DISCIPLINES.has(discipline) ? { discipline } : { error: 'bad_discipline' };
191
199
  }
@@ -228,10 +236,10 @@ const TASK_FIELD_BOUNDS = {
228
236
  const VOTABLE_TASK_STATUSES = new Set(['confirmed', 'shipped']);
229
237
 
230
238
  // Disambiguation signals for the creative lane (migration 086). Visual
231
- // art-production cues → artist; narrative / design-thinking / writing cues →
232
- // ideator. Scored by distinct-rule hit count over title+description, plus
233
- // touches[] path prefixes; the stronger side wins, ties / no-signal fall to
234
- // 'unclassified' for hand-triage.
239
+ // art-production cues and narrative / design-thinking / writing cues are both
240
+ // the artist craft since task 1005141 (the ideator folded into it); the two
241
+ // lists stay separate so each reads as what it detects. Scored by distinct-rule
242
+ // hit count over title+description, plus touches[] path prefixes.
235
243
  const ARTIST_SIGNALS = [
236
244
  /\bpixel[-\s]?art\b/i,
237
245
  /\btile-?sheets?\b/i,
@@ -278,23 +286,24 @@ const UI_PATH_PREFIXES = [
278
286
  'docs/design/',
279
287
  ];
280
288
 
281
- // Split a 'creative' verdict into 'artist' | 'ideator' | 'ui' | 'unclassified'.
282
- // Three sub-lanes are scored by distinct-rule hit count + touches[] hits; the
283
- // strongest single lane wins. A tie at the top (including 0-0-0) → hand-triage.
289
+ // Split a 'creative' verdict into 'artist' | 'ui' | 'unclassified'.
290
+ // The art and ideas signals are summed into ONE artist score (one craft since
291
+ // task 1005141 — scoring them as two lanes would call an even art/ideas task a
292
+ // tie and send it to hand-triage); it competes with the ui score. The stronger
293
+ // lane wins; a tie (including 0-0) → hand-triage.
284
294
  function splitCreativeDiscipline(title, description, touches) {
285
295
  const text = `${title || ''} ${description || ''}`;
286
296
  const artist = ARTIST_SIGNALS.filter((re) => re.test(text)).length
287
- + countTouchesHits(touches, ARTIST_PATH_PREFIXES);
288
- const ideator = IDEATOR_SIGNALS.filter((re) => re.test(text)).length
289
- + countTouchesHits(touches, IDEATOR_PATH_PREFIXES);
297
+ + countTouchesHits(touches, ARTIST_PATH_PREFIXES)
298
+ + IDEATOR_SIGNALS.filter((re) => re.test(text)).length
299
+ + countTouchesHits(touches, IDEATOR_PATH_PREFIXES);
290
300
  const ui = UI_SIGNALS.filter((re) => re.test(text)).length
291
301
  + countTouchesHits(touches, UI_PATH_PREFIXES);
292
302
  const ranked = [
293
303
  { d: 'artist', n: artist },
294
- { d: 'ideator', n: ideator },
295
304
  { d: 'ui', n: ui },
296
305
  ].sort((a, b) => b.n - a.n);
297
- // No signal anywhere, or the top two are tied → hand-triage.
306
+ // No signal anywhere, or the two lanes are tied → hand-triage.
298
307
  if (ranked[0].n === 0 || ranked[0].n === ranked[1].n) return 'unclassified';
299
308
  return ranked[0].d;
300
309
  }
@@ -326,7 +335,7 @@ const CREATIVE_TEXT_RULES = [
326
335
  // UI/interface DESIGN cues (mig 176 / task 1376) — route the creative verdict
327
336
  // to the `ui` sub-lane via splitCreativeDiscipline. Kept here (not engineering)
328
337
  // so "UI redesign of the hall" / "wireframe the checkout" land creative-first;
329
- // splitCreativeDiscipline then picks `ui` over `artist`/`ideator`.
338
+ // splitCreativeDiscipline then picks `ui` over `artist`.
330
339
  { re: /\b(UI|UX)\s+(design|redesign|pass|polish|refresh|overhaul)\b/i, weight: 0.8 },
331
340
  { re: /\b(user\s+(interface|experience)|interface\s+design|wireframes?|mock-?ups?)\b/i, weight: 0.8 },
332
341
  { re: /\b(figma|claude[-\s]?design)\b.*\b(redesign|design|layout|screen|mock-?up|wireframe)\b/i, weight: 0.8 },
@@ -503,7 +512,7 @@ function classifyTaskDiscipline(title, description, touches) {
503
512
  const d = splitCreativeDiscipline(titleText, descText, touches);
504
513
  return { discipline: d, confidence: d === 'unclassified' ? 0 : cT.max };
505
514
  }
506
- return { discipline: 'engineer', confidence: eT.max };
515
+ return { discipline: 'artisan', confidence: eT.max };
507
516
  }
508
517
  // (b) Same max-weight (e.g. both 0.85). Combine title-rule count and
509
518
  // touches[] hit count into a single asymmetry score — a side that
@@ -517,7 +526,7 @@ function classifyTaskDiscipline(title, description, touches) {
517
526
  const d = splitCreativeDiscipline(titleText, descText, touches);
518
527
  return { discipline: d, confidence: d === 'unclassified' ? 0 : 0.6 + Math.min(0.3, cAsym * 0.05) };
519
528
  }
520
- return { discipline: 'engineer', confidence: 0.6 + Math.min(0.3, -cAsym * 0.05) };
529
+ return { discipline: 'artisan', confidence: 0.6 + Math.min(0.3, -cAsym * 0.05) };
521
530
  }
522
531
  return { discipline: 'unclassified', confidence: 0 };
523
532
  }
@@ -541,7 +550,7 @@ function classifyTaskDiscipline(title, description, touches) {
541
550
  const d = splitCreativeDiscipline(titleText, descText, touches);
542
551
  return { discipline: d, confidence: d === 'unclassified' ? 0 : creative };
543
552
  }
544
- return { discipline: 'engineer', confidence: engineering };
553
+ return { discipline: 'artisan', confidence: engineering };
545
554
  }
546
555
 
547
556
  module.exports = {
@@ -550,6 +559,7 @@ module.exports = {
550
559
  buildBlockerVendorRe,
551
560
  VALID_KINDS,
552
561
  VALID_DISCIPLINES,
562
+ normalizeDisciplineValue,
553
563
  WRITABLE_DISCIPLINES,
554
564
  resolveWriteDiscipline,
555
565
  VOTABLE_TASK_STATUSES,
@@ -41,7 +41,7 @@ const shipped = (over = {}) => ({
41
41
  version_id: 'BONGOS-V1',
42
42
  goal_id: '1000074',
43
43
  title: 'R09 · Reword the hall empty states',
44
- discipline: 'engineer',
44
+ discipline: 'artisan',
45
45
  kind: 'feature',
46
46
  status: 'shipped',
47
47
  ...over,
@@ -51,7 +51,7 @@ const shipped = (over = {}) => ({
51
51
  const REVIEW_RULE = {
52
52
  key: 'test-artist-review',
53
53
  discipline: 'artist',
54
- when: (t) => t.discipline === 'engineer',
54
+ when: (t) => t.discipline === 'artisan',
55
55
  title: (t) => `Review the copy that shipped in task ${t.id}`,
56
56
  body: (t) => `Look at what ${t.title} put in front of a person.`,
57
57
  };
@@ -195,7 +195,7 @@ await test('the production table is valid, and a duplicate key is refused', () =
195
195
 
196
196
  const shippedCopy = (over = {}) => ({
197
197
  id: '7001', version_id: 'BONGOS-V1', goal_id: '1000074',
198
- title: 'Reword the hall empty states', discipline: 'engineer',
198
+ title: 'Reword the hall empty states', discipline: 'artisan',
199
199
  kind: 'feature', status: 'shipped', module_key: 'hall-ui', source: 'manual',
200
200
  ...over,
201
201
  });
@@ -262,8 +262,8 @@ await test('"copy or visuals" is a NARROW predicate — three ways in, and a def
262
262
  assert.equal(H(shippedCopy({ module_key: null, source: 'copy-proposal' })), true, 'a landed copy proposal');
263
263
  assert.equal(H(shippedCopy({ module_key: null, source: 'manual', discipline: 'artist' })), true, 'work an artist did');
264
264
  // The default is NO — a backend/infra ship is not copy/visual work.
265
- assert.equal(H(shippedCopy({ module_key: 'lifecycle', discipline: 'engineer', source: 'manual' })), false);
266
- assert.equal(H(shippedCopy({ module_key: null, discipline: 'engineer', source: 'manual' })), false);
265
+ assert.equal(H(shippedCopy({ module_key: 'lifecycle', discipline: 'artisan', source: 'manual' })), false);
266
+ assert.equal(H(shippedCopy({ module_key: null, discipline: 'artisan', source: 'manual' })), false);
267
267
  assert.equal(H(null), false);
268
268
  });
269
269
 
@@ -303,7 +303,7 @@ await test('the runner still hands the artist gate to the rules, and a reader th
303
303
 
304
304
  await test('a page file changed under modules/*/public/ is visual work whatever the label says', () => {
305
305
  const H = cascade.hasCopyOrVisualWork;
306
- const unlabelled = shippedCopy({ module_key: null, discipline: 'engineer' });
306
+ const unlabelled = shippedCopy({ module_key: null, discipline: 'artisan' });
307
307
  assert.equal(H(unlabelled), false, 'the label alone says no — the gap task 1003480 fell through');
308
308
  for (const p of [
309
309
  'modules/hall-ui/public/goal-forest-lib.js',
@@ -316,7 +316,7 @@ await test('a page file changed under modules/*/public/ is visual work whatever
316
316
 
317
317
  await test('a backend file in a UI module is NOT visual work — page files only', () => {
318
318
  const H = cascade.hasCopyOrVisualWork;
319
- const unlabelled = shippedCopy({ module_key: null, discipline: 'engineer' });
319
+ const unlabelled = shippedCopy({ module_key: null, discipline: 'artisan' });
320
320
  assert.equal(H(unlabelled, [
321
321
  'modules/hall-ui/module.json',
322
322
  'modules/public-landing/routes/hub.js',
@@ -55,6 +55,9 @@ const KEYS = Object.freeze({
55
55
  // When the planet came alive (founding switched off). With STARTED_AT it bounds the
56
56
  // window a builder must have joined in to carry the founding mark (task 1004424).
57
57
  ENDED_AT: 'founding.ended_at',
58
+ // When the owner skipped genesis instead of finishing it (genesis-home.js skipGenesis). Write-once, so the
59
+ // record says the planet came alive by skip, not by the five stages.
60
+ SKIPPED_AT: 'founding.skipped_at',
58
61
  // Genesis is OFF unless a project turns it on (owner, 2026-11-01: not polished enough, so every
59
62
  // project opens as the full hall). Read as ordinary 'true'; absent or anything else is OFF, which is
60
63
  // also what pauses a project that was already mid-founding. A demo, or a project born with genesis,
@@ -426,6 +426,26 @@ async function comeAlive(builderId, deps = {}) {
426
426
  return { ok: true };
427
427
  }
428
428
 
429
+ // skipGenesis — the owner's way out of genesis from ANY stage (incl. decide), with no Board
430
+ // Room sitting and no finished-tasks gate. Founding mode switches off exactly as comeAlive
431
+ // does (ended_at stamped, the full hall opens), and the skip is recorded once in
432
+ // founding.skipped_at with the stage it left, so the record shows it was skipped, not
433
+ // finished. Genesis still happens once (spec D8): a project that is already alive refuses.
434
+ // Filed genesis tasks stay on the board; nothing is deleted.
435
+ async function skipGenesis(builderId, deps = {}) {
436
+ const s = seams(deps);
437
+ const ended = await s.get(fm.KEYS.ENDED_AT);
438
+ if (ended && ended.value) return { ok: false, reason: 'already_alive' };
439
+ const mode = await fm.foundingModeFor(modeDeps(s));
440
+ if (!mode) return { ok: false, reason: 'not_founding' };
441
+ const prior = await s.get(fm.KEYS.SKIPPED_AT);
442
+ if (!prior || !prior.value) {
443
+ await s.set(fm.KEYS.SKIPPED_AT, JSON.stringify({ at: (s.now || new Date()).toISOString(), stage: mode.stage }), builderId);
444
+ }
445
+ await fm.setFoundingMode({ active: false }, builderId, modeDeps(s));
446
+ return { ok: true, skipped: true, from_stage: mode.stage };
447
+ }
448
+
429
449
  // onStagePassed — the `board.genesis_stage.passed` listener. Best-effort: the next read
430
450
  // reconciles anyway.
431
451
  async function onStagePassed(deps = {}) {
@@ -434,7 +454,7 @@ async function onStagePassed(deps = {}) {
434
454
 
435
455
  module.exports = {
436
456
  PLAN_KEY, STAGE_INFO, PROPER, DEMO_PEOPLE, DEMO_HOURS, intIn, parsePlan, shapeGenesis, demoMinutes, demoPlan,
437
- readGenesis, startFounding, savePlan, applyGovernmentChoice, closeStage, comeAlive, onStagePassed,
457
+ readGenesis, startFounding, savePlan, applyGovernmentChoice, closeStage, comeAlive, skipGenesis, onStagePassed,
438
458
  // the founding-mode deps this file reads the mode with, for founding-birth.js (task 1004506)
439
459
  modeDepsFor: (deps) => modeDeps(seams(deps)),
440
460
  };
@@ -186,4 +186,6 @@ function mountGenesisRoutes(router) {
186
186
  }
187
187
  });
188
188
  router.post('/founding/alive', api.requireBuilder, owns, handle((req) => genesis.comeAlive(req.builder.id)));
189
+ // Skip genesis: the owner opens the full hall from any stage, no sitting, no gate.
190
+ router.post('/founding/skip', api.requireBuilder, owns, handle((req) => genesis.skipGenesis(req.builder.id)));
189
191
  }
@@ -91,10 +91,11 @@ function referencedImageIds(bodyMd) {
91
91
  // the ideas are triaged like any other, and nothing here grants anything.
92
92
  const FENCE_LOOKALIKE_RE = /~{4,}[A-Za-z0-9-]*~{4,}/g;
93
93
  // Two writers share the split: the artist's feedback doc (the palette) and the
94
- // ideator's notes (the lightbulb, task 1004498). Only the framing words differ.
94
+ // artist's notes (the lightbulb, task 1004498; the ideator folded into the artist in
95
+ // task 1005141, so the second voice is named for what it writes). Only the framing words differ.
95
96
  const VOICES = {
96
97
  artist: { who: 'artist', what: 'feedback about one page of a web app', ask: 'separate change the artist asks for or problem they point at', label: 'ARTIST-FEEDBACK', doc: 'doc', split: 'feedback' },
97
- ideator: { who: 'ideator', what: 'notes, written while looking at one page of a web app', ask: 'separate idea, change or problem the ideator raises', label: 'IDEATOR-NOTES', doc: 'notes', split: 'notes' },
98
+ notes: { who: 'artist', what: 'notes, written while looking at one page of a web app', ask: 'separate idea, change or problem the artist raises', label: 'IDEATOR-NOTES', doc: 'notes', split: 'notes' },
98
99
  };
99
100
  function buildSplitPrompt({ pageTitle, pageId, bodyMd, voice = 'artist', nonce = crypto.randomBytes(8).toString('hex') }) {
100
101
  const v = VOICES[voice] || VOICES.artist;
@@ -76,7 +76,7 @@ async function fileNote(note, { grade, capture, db, log } = {}) {
76
76
  // an empty list means nothing filed and the caller says so.
77
77
  async function fileIdeatorNotes(submit, { grade, capture, log, authorId, authorName } = {}) {
78
78
  const page = { page_id: submit.page_id, page_path: submit.page_path, page_title: submit.page_title, body_md: submit.body_md };
79
- let ideas = await aiIdeas({ note: page, grade, voice: 'ideator', timeoutMs: IDEATOR_TIMEOUT_MS });
79
+ let ideas = await aiIdeas({ note: page, grade, voice: 'notes', timeoutMs: IDEATOR_TIMEOUT_MS });
80
80
  let splitBy = 'ai';
81
81
  if (!ideas) { ideas = [N.wholeDocIdea({ pageTitle: page.page_title, pageId: page.page_id, bodyMd: page.body_md })]; splitBy = 'whole'; }
82
82
  const filed = [];
@@ -5,15 +5,16 @@
5
5
  // A reporting instance sends, per craft the builder has EARNED credits in, that
6
6
  // craft's headline counts (src/bongos/auth-admission.js readCraftRollup):
7
7
  //
8
- // engineer { shipped, credits }
9
- // artist { pages_approved, credits }
10
- // ideator { filed, full_ideas, ratified, credits }
8
+ // artisan { shipped, credits }
9
+ // artist { pages_approved, filed, full_ideas, ratified, credits }
10
+ //
11
+ // (the old engineer / artist / ideator shape is folded in at ingest, foldLegacy)
11
12
  //
12
13
  // Two halves live here, next to each other so they cannot disagree on the shape:
13
14
  //
14
15
  // sanitizeCrafts — what upsertActivity BINDS. The report is third-party
15
16
  // self-report (ADR 0171 D7), so the shape and the caps are the hub's, not
16
- // the sender's: only the three crafts, only the named counts, each a
17
+ // the sender's: only the two crafts, only the named counts, each a
17
18
  // non-negative integer under a ceiling or null. Anything else is dropped.
18
19
  // A count is never text, so nothing here can carry a title or wording.
19
20
  //
@@ -30,10 +31,36 @@
30
31
  'use strict';
31
32
 
32
33
  const CRAFT_FIELDS = Object.freeze({
33
- engineer: Object.freeze(['shipped', 'credits']),
34
- artist: Object.freeze(['pages_approved', 'credits']),
35
- ideator: Object.freeze(['filed', 'full_ideas', 'ratified', 'credits']),
34
+ artisan: Object.freeze(['shipped', 'credits']),
35
+ artist: Object.freeze(['pages_approved', 'filed', 'full_ideas', 'ratified', 'credits']),
36
36
  });
37
+
38
+ // A reporting instance on a core older than task 1005141 still sends the old
39
+ // crafts: `engineer`, and `artist` + `ideator` as two blocks. They are folded
40
+ // into today's shape BEFORE the hub's caps run, so an old report and a new one
41
+ // land identically: engineer becomes artisan, and the ideator's counts join the
42
+ // artist block with the two credit figures summed. A craft already reported under
43
+ // its new name wins over an old-name block for the same craft.
44
+ function foldLegacy(raw) {
45
+ const out = { ...raw };
46
+ if (out.engineer && !out.artisan) out.artisan = out.engineer;
47
+ delete out.engineer;
48
+ const ideator = out.ideator;
49
+ delete out.ideator;
50
+ if (ideator && typeof ideator === 'object' && !Array.isArray(ideator)) {
51
+ const art = out.artist && typeof out.artist === 'object' && !Array.isArray(out.artist) ? out.artist : {};
52
+ // Each half is capped BEFORE the sum: a hostile -3 on one side must not
53
+ // quietly shave the other side's honest count.
54
+ const a = cleanCount(art.credits);
55
+ const i = cleanCount(ideator.credits);
56
+ out.artist = {
57
+ ...art,
58
+ filed: ideator.filed, full_ideas: ideator.full_ideas, ratified: ideator.ratified,
59
+ credits: a == null && i == null ? null : (a || 0) + (i || 0),
60
+ };
61
+ }
62
+ return out;
63
+ }
37
64
  // Far above any honest count, low enough that a sum over every project stays a
38
65
  // safe integer and a hostile report cannot dominate a profile by one number.
39
66
  const MAX_CRAFT_COUNT = 10_000_000;
@@ -45,6 +72,7 @@ function cleanCount(v) {
45
72
  function sanitizeCrafts(raw) {
46
73
  const out = {};
47
74
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return out;
75
+ raw = foldLegacy(raw);
48
76
  for (const [craft, fields] of Object.entries(CRAFT_FIELDS)) {
49
77
  const block = raw[craft];
50
78
  if (!block || typeof block !== 'object' || Array.isArray(block)) continue;
@@ -0,0 +1,45 @@
1
+ -- platform_identity_036 — the hub's per-project activity rollup follows core_296's craft
2
+ -- rename (task 1005141, BONGOS-V3 C1): engineer → artisan, ideator → artist.
3
+ --
4
+ -- `disciplines` is mapped and deduped. `crafts` (platform_identity_029) is keyed by
5
+ -- craft: the engineer block becomes the artisan block, and the ideator block's counts
6
+ -- (filed, full_ideas, ratified) merge into the artist block, with credits summed —
7
+ -- the shape modules/platform-identity/craft-rollup.js now sanitizes. A report from an
8
+ -- instance still on an older core is folded the same way at ingest, so these rows and
9
+ -- fresh reports agree.
10
+
11
+ BEGIN;
12
+
13
+ UPDATE platform_identity_builder_project_activity a
14
+ SET disciplines = ARRAY(
15
+ SELECT m.craft FROM (
16
+ SELECT CASE d WHEN 'engineer' THEN 'artisan' WHEN 'ideator' THEN 'artist' ELSE d END AS craft,
17
+ min(ord) AS first_seen
18
+ FROM unnest(a.disciplines) WITH ORDINALITY AS u(d, ord)
19
+ GROUP BY 1
20
+ ) m ORDER BY m.first_seen)
21
+ WHERE a.disciplines && ARRAY['engineer', 'ideator']::text[];
22
+
23
+ UPDATE platform_identity_builder_project_activity
24
+ SET crafts = (crafts - 'engineer') || jsonb_build_object('artisan', crafts -> 'engineer')
25
+ WHERE crafts ? 'engineer' AND NOT crafts ? 'artisan';
26
+
27
+ UPDATE platform_identity_builder_project_activity
28
+ SET crafts = (crafts - 'ideator') || jsonb_build_object('artist',
29
+ coalesce(crafts -> 'artist', '{}'::jsonb)
30
+ || jsonb_strip_nulls(jsonb_build_object(
31
+ 'filed', crafts -> 'ideator' -> 'filed',
32
+ 'full_ideas', crafts -> 'ideator' -> 'full_ideas',
33
+ 'ratified', crafts -> 'ideator' -> 'ratified'))
34
+ || jsonb_build_object('credits',
35
+ CASE WHEN jsonb_typeof(crafts -> 'artist' -> 'credits') = 'number'
36
+ OR jsonb_typeof(crafts -> 'ideator' -> 'credits') = 'number'
37
+ THEN to_jsonb(coalesce((crafts -> 'artist' ->> 'credits')::bigint, 0)
38
+ + coalesce((crafts -> 'ideator' ->> 'credits')::bigint, 0))
39
+ ELSE 'null'::jsonb END))
40
+ WHERE crafts ? 'ideator';
41
+
42
+ INSERT INTO schema_migrations (version) VALUES ('platform_identity_036_crafts_artisan_artist')
43
+ ON CONFLICT (version) DO NOTHING;
44
+
45
+ COMMIT;
@@ -671,8 +671,8 @@ async function upsertActivity({ clientId, githubId, credits = 0, tasksShipped =
671
671
  // api.LIVE_RANK_LADDER is a published doorway export (src/module-api.js, a
672
672
  // lazy getter over db-kernel's canonical four-rank ladder).
673
673
  const safeRank = api.LIVE_RANK_LADDER.includes(rank) ? rank : null;
674
- const disc = (Array.isArray(disciplines) ? disciplines : [])
675
- .filter((d) => typeof d === 'string' && d.length > 0 && d.length <= MAX_DISCIPLINE_LENGTH)
674
+ const disc = api.crafts.normalizeCrafts((Array.isArray(disciplines) ? disciplines : []) // an older core's engineer/ideator → today's crafts, as foldLegacy (task 1005193)
675
+ .filter((d) => typeof d === 'string' && d.length > 0 && d.length <= MAX_DISCIPLINE_LENGTH))
676
676
  .slice(0, MAX_ROLLUP_DISCIPLINES);
677
677
  const ships = sanitizeRecentShips(recentShips, clientOrigin);
678
678
  // Each craft's own counts (task 1004440), shaped and capped here — see craft-rollup.js.
@@ -55,7 +55,8 @@ module.exports = function communityLeaderboardRoutes() {
55
55
  if (!leaderboard.isWindow(window)) {
56
56
  return res.fail('bad_window', { status: 400, message: `window must be one of: ${leaderboard.WINDOWS.join(', ')}` });
57
57
  }
58
- const discipline = (typeof req.query.discipline === 'string' ? req.query.discipline : '').trim();
58
+ // An old link's engineer/ideator reads as today's craft (task 1005141).
59
+ const discipline = require('../../../src/module-api').crafts.normalizeCraft((typeof req.query.discipline === 'string' ? req.query.discipline : '').trim());
59
60
  if (discipline.length > leaderboard.MAX_DISCIPLINE_CHARS) {
60
61
  return res.fail('discipline_too_long', { status: 400, message: `discipline must be at most ${leaderboard.MAX_DISCIPLINE_CHARS} characters` });
61
62
  }
@@ -63,7 +63,8 @@ module.exports = function communitySearchRoutes() {
63
63
  if (q.length > builderSearch.MAX_QUERY_CHARS) {
64
64
  return res.fail('query_too_long', { status: 400, message: `q must be at most ${builderSearch.MAX_QUERY_CHARS} characters` });
65
65
  }
66
- const discipline = (typeof req.query.discipline === 'string' ? req.query.discipline : '').trim();
66
+ // An old link's engineer/ideator reads as today's craft (task 1005141).
67
+ const discipline = require('../../../src/module-api').crafts.normalizeCraft((typeof req.query.discipline === 'string' ? req.query.discipline : '').trim());
67
68
  if (discipline.length > builderSearch.MAX_DISCIPLINE_CHARS) {
68
69
  return res.fail('discipline_too_long', { status: 400, message: `discipline must be at most ${builderSearch.MAX_DISCIPLINE_CHARS} characters` });
69
70
  }
@@ -691,7 +691,8 @@ module.exports = function ssoRoutes() {
691
691
  tasksShipped: b.tasks_shipped,
692
692
  karma: b.karma,
693
693
  rank: b.rank || null,
694
- disciplines: Array.isArray(b.disciplines) ? b.disciplines : [],
694
+ // An instance on an older core still reports engineer/ideator (task 1005141).
695
+ disciplines: Array.isArray(b.disciplines) ? api.crafts.normalizeCrafts(b.disciplines) : [],
695
696
  recentShips: b.recent_ships,
696
697
  crafts: b.crafts,
697
698
  // The AUTHENTICATED client's registered origin — the bound every pushed
@@ -606,14 +606,14 @@ const upsertCall = (pool) => pool.calls.find((c) => /INSERT INTO platform_identi
606
606
 
607
607
  await ta('upsertActivity upserts per (client_id, github_id) with the talent snapshot', async () => {
608
608
  const pool = activityPool();
609
- await P.upsertActivity({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3, rank: 'metic', disciplines: ['engineer'] }, { pool });
609
+ await P.upsertActivity({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3, rank: 'metic', disciplines: ['artisan'] }, { pool });
610
610
  const c = upsertCall(pool);
611
611
  assert.match(c.text, /ON CONFLICT \(client_id, github_id\) DO UPDATE/);
612
612
  // recent_ships (task 1002803) rides the same row as a jsonb STRING param —
613
613
  // an empty list when the sender reported none, never NULL. Its own cap,
614
614
  // shape and link bound are covered by tests/hub_recent_ships.mjs. The last param
615
615
  // is the report's as-of stamp (task 1004268): null when the sender gave none.
616
- assert.deepEqual(c.params, ['c1', 42, 100, 5, 3, 'metic', ['engineer'], '[]', null, '{}']);
616
+ assert.deepEqual(c.params, ['c1', 42, 100, 5, 3, 'metic', ['artisan'], '[]', null, '{}']);
617
617
  });
618
618
 
619
619
  await ta('upsertActivity sanitizes third-party self-report (ADR 0171 D7): off-ladder rank → null, disciplines validated + capped', async () => {
@@ -622,7 +622,7 @@ await ta('upsertActivity sanitizes third-party self-report (ADR 0171 D7): off-la
622
622
  clientId: 'c1', githubId: 42,
623
623
  rank: 'Supreme Overlord', // not one of the four core rank keys → stored null
624
624
  disciplines: [
625
- 'engineer', 42, null, { evil: true }, // non-strings dropped
625
+ 'artisan', 42, null, { evil: true }, // non-strings dropped
626
626
  'x'.repeat(33), // over-length dropped
627
627
  'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', // valid tail — capped to 8 total
628
628
  ],
@@ -633,7 +633,7 @@ await ta('upsertActivity sanitizes third-party self-report (ADR 0171 D7): off-la
633
633
  assert.ok(disc.length <= 8, `disciplines capped at 8 (got ${disc.length})`);
634
634
  assert.ok(disc.every((d) => typeof d === 'string' && d.length > 0 && d.length <= 32),
635
635
  'only bounded string elements survive');
636
- assert.equal(disc[0], 'engineer', 'valid elements pass through in order');
636
+ assert.equal(disc[0], 'artisan', 'valid elements pass through in order');
637
637
  });
638
638
 
639
639
  await ta('upsertActivity keeps a legitimate core rank key', async () => {
@@ -733,7 +733,7 @@ await ta('getRollupForBuilder guards non-numeric github_id BEFORE any query (no
733
733
 
734
734
  await ta('getRollupForBuilder aggregates un-fanned + names only publicly-catalogued projects', async () => {
735
735
  const pool = mockPool([
736
- { rows: [{ projects: 2, credits: 150, works_shipped: 7, karma: 4, last_active: 'T', disciplines: ['engineer', 'ui'] }] },
736
+ { rows: [{ projects: 2, credits: 150, works_shipped: 7, karma: 4, last_active: 'T', disciplines: ['artisan', 'ui'] }] },
737
737
  { rows: [
738
738
  { name: 'Public Proj', origin: 'https://pub.example', rank_held: 'metic', is_public: true },
739
739
  { name: 'Secret Proj', origin: 'https://sec.example', rank_held: 'archon', is_public: false },
@@ -741,7 +741,7 @@ await ta('getRollupForBuilder aggregates un-fanned + names only publicly-catalog
741
741
  ]);
742
742
  const out = await P.getRollupForBuilder(42, { pool });
743
743
  assert.deepEqual(out.totals, { projects: 2, credits: 150, works_shipped: 7, karma: 4 });
744
- assert.deepEqual(out.disciplines, ['engineer', 'ui']);
744
+ assert.deepEqual(out.disciplines, ['artisan', 'ui']);
745
745
  assert.equal(out.projects.length, 1, 'only the public catalog row is named');
746
746
  // recent_ships rides the project row it belongs to (task 1002803), so a work
747
747
  // title can only surface where the project name already may.
@@ -262,9 +262,8 @@
262
262
  <div class="commFilters">
263
263
  <div class="commTabs" id="craftTabs" role="group" aria-label="Role">
264
264
  <button class="commTab" type="button" data-craft="" aria-pressed="true">Everyone</button>
265
- <button class="commTab" type="button" data-craft="engineer" aria-pressed="false">Engineers</button>
265
+ <button class="commTab" type="button" data-craft="artisan" aria-pressed="false">Artisans</button>
266
266
  <button class="commTab" type="button" data-craft="artist" aria-pressed="false">Artists</button>
267
- <button class="commTab" type="button" data-craft="ideator" aria-pressed="false">Ideators</button>
268
267
  <button class="commTab" type="button" data-craft="ui" aria-pressed="false">Designers</button>
269
268
  </div>
270
269
  <div class="commWhen">
@@ -314,9 +313,8 @@
314
313
  <label class="vh" for="findCraft">Role</label>
315
314
  <select id="findCraft">
316
315
  <option value="">Any role</option>
317
- <option value="engineer">Engineers</option>
316
+ <option value="artisan">Artisans</option>
318
317
  <option value="artist">Artists</option>
319
- <option value="ideator">Ideators</option>
320
318
  <option value="ui">Designers</option>
321
319
  </select>
322
320
  <button id="findBtn" type="submit" class="btn small">Search</button>
@@ -365,7 +363,7 @@
365
363
  var WINDOWS = ['7d', '30d', '365d', 'all'];
366
364
  var QUERY_MIN = 2;
367
365
  var QUERY_MAX = 100;
368
- var CRAFT_WORD = { engineer: 'engineer', artist: 'artist', ideator: 'ideator', ui: 'designer' };
366
+ var CRAFT_WORD = { artisan: 'artisan', artist: 'artist', ui: 'designer' };
369
367
  // The five sub-pages this file is served as (platform-server.js's /community routes);
370
368
  // the path picks one, and its menu items are marked current. The home adds no title.
371
369
  var VIEWS = {
@@ -774,7 +774,7 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
774
774
  <div class="layer">
775
775
  <h1 data-brand-tagline><span class="l1">Building Digitally,</span><span class="l2">as simple as Bongos.</span></h1>
776
776
 
777
- <p class="lede">Bringing AI to the builder, ideator, and artist in everyone.</p>
777
+ <p class="lede">Bringing AI to the artisan and the artist in everyone.</p>
778
778
 
779
779
  <div class="doors">
780
780
  <div class="doorbar">
@@ -1389,7 +1389,7 @@ summary{min-height:24px;padding:3px 0;}
1389
1389
  <div class="card mSec" id="mInvite">
1390
1390
  <h2>Invite a builder</h2>
1391
1391
  <div id="mInv">
1392
- <p class="finePrint">By GitHub username. The invite is sent from your project’s own hall — that is the invite that lets them in — and they see it on their projects page here too.</p>
1392
+ <p class="finePrint">By GitHub username. The invite is sent from your project’s own hall — that is the invite that lets them in — and they see it on their projects page here too. The hall’s invite box can also invite them to the project’s GitHub repository, which is a separate invite they accept on GitHub.</p>
1393
1393
  <div class="attach">
1394
1394
  <label class="vh" for="mInviteWho">GitHub username</label>
1395
1395
  <input type="text" id="mInviteWho" placeholder="GitHub username" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="200">
@@ -0,0 +1,30 @@
1
+ -- specialities_004 — specialities follow core_296's craft rename (task 1005141,
2
+ -- BONGOS-V3 C1): engineer → artisan, ideator → artist.
3
+ --
4
+ -- A speciality's discipline is immutable to its OWNER (the update path refuses a
5
+ -- change); this is a vocabulary rename, not a move between crafts, so it rewrites the
6
+ -- catalog and the adoption copy together.
7
+ --
8
+ -- The fold has one collision: specialities_adoptions allows ONE active adoption per
9
+ -- builder per craft. A builder with an active artist speciality AND an active ideator
10
+ -- one would hold two artist adoptions after the rename, so the ideator adoption is
11
+ -- deactivated first (deactivating is how a builder switches; nothing is deleted, and
12
+ -- they can re-adopt it from their speciality page).
13
+
14
+ BEGIN;
15
+
16
+ UPDATE specialities_adoptions i
17
+ SET active = FALSE
18
+ WHERE i.active AND i.discipline = 'ideator'
19
+ AND EXISTS (SELECT 1 FROM specialities_adoptions a
20
+ WHERE a.builder_id = i.builder_id AND a.active AND a.discipline = 'artist');
21
+
22
+ UPDATE specialities_catalog SET discipline = 'artisan' WHERE discipline = 'engineer';
23
+ UPDATE specialities_catalog SET discipline = 'artist' WHERE discipline = 'ideator';
24
+ UPDATE specialities_adoptions SET discipline = 'artisan' WHERE discipline = 'engineer';
25
+ UPDATE specialities_adoptions SET discipline = 'artist' WHERE discipline = 'ideator';
26
+
27
+ INSERT INTO schema_migrations (version) VALUES ('specialities_004_crafts_artisan_artist')
28
+ ON CONFLICT (version) DO NOTHING;
29
+
30
+ COMMIT;