@bongos/core 1.24.79 → 1.24.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/.bongos-core.json +495 -350
  2. package/.claude/skills/ask-for-help/SKILL.md +1 -1
  3. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  4. package/.claude/skills/design/SKILL.md +1 -1
  5. package/.claude/skills/goal-create/SKILL.md +1 -1
  6. package/.claude/skills/planning-session/SKILL.md +1 -1
  7. package/clients/bongos-client/README.md +1 -1
  8. package/clients/bongos-client/bongos-client.global.js +8 -0
  9. package/clients/bongos-client/index.cjs +8 -0
  10. package/clients/bongos-client/index.d.ts +11 -0
  11. package/clients/bongos-client/index.mjs +8 -0
  12. package/docs/adr/0274-one-kernel-three-role-packs.md +2 -2
  13. package/docs/adr/0399-the-visor-is-glass-over-the-scene-and-owns-no-camera.md +91 -0
  14. package/docs/adr/0400-a-chain-reserves-work-and-each-sub-quest-is-still-claimed-alone.md +109 -0
  15. package/docs/adr/0401-one-options-menu-opens-every-settings-screen-in-a-frame.md +53 -0
  16. package/docs/adr/README.md +3 -0
  17. package/docs/api/openapi.json +195 -3
  18. package/docs/api-reference.md +8 -4
  19. package/docs/architecture.md +7 -3
  20. package/docs/copy-inventory.md +390 -370
  21. package/docs/copy-registry.json +719 -518
  22. package/docs/design/characters/character-sprites-source.webp +0 -0
  23. package/docs/file-map.md +7 -8
  24. package/docs/module-api-changelog.md +2 -0
  25. package/docs/modules-contract.md +5 -0
  26. package/docs/onboarding/slash-commands.md +1 -1
  27. package/docs/packs/{engineer.md → artisan.md} +5 -5
  28. package/docs/packs/artist.md +50 -4
  29. package/docs/page-inventory.json +15 -4
  30. package/docs/page-readings.json +3182 -3336
  31. package/docs/{packs/ideator.md → recipes/ideation-playbook.md} +5 -48
  32. package/migrations/core_296_crafts_artisan_artist.sql +72 -0
  33. package/migrations/core_297_builder_character.sql +30 -0
  34. package/modules/copy-desk/proposals.js +1 -1
  35. package/modules/economy/credits-by-craft.js +29 -15
  36. package/modules/hall-ui/public/ai-studio.html +4 -4
  37. package/modules/hall-ui/public/atlas.html +5 -5
  38. package/modules/hall-ui/public/atlas.js +3 -1
  39. package/modules/hall-ui/public/blockers.html +5 -5
  40. package/modules/hall-ui/public/board-room.html +4 -4
  41. package/modules/hall-ui/public/characters/artisan.png +0 -0
  42. package/modules/hall-ui/public/characters/artist.png +0 -0
  43. package/modules/hall-ui/public/characters/patron.png +0 -0
  44. package/modules/hall-ui/public/city.css +2 -1
  45. package/modules/hall-ui/public/city.html +9 -6
  46. package/modules/hall-ui/public/city.js +7 -1
  47. package/modules/hall-ui/public/collab.html +4 -4
  48. package/modules/hall-ui/public/commands.html +4 -4
  49. package/modules/hall-ui/public/copy-desk.html +4 -4
  50. package/modules/hall-ui/public/craft-mode.js +258 -64
  51. package/modules/hall-ui/public/deploy.html +4 -4
  52. package/modules/hall-ui/public/diagram-viewer.js +2 -1
  53. package/modules/hall-ui/public/dom-utils.js +1 -1
  54. package/modules/hall-ui/public/goal-map.html +9 -6
  55. package/modules/hall-ui/public/goals.html +5 -5
  56. package/modules/hall-ui/public/government.html +4 -4
  57. package/modules/hall-ui/public/hall-render.js +3 -1
  58. package/modules/hall-ui/public/home-city-draw.js +3 -3
  59. package/modules/hall-ui/public/home-city.css +4 -0
  60. package/modules/hall-ui/public/home-city.js +17 -9
  61. package/modules/hall-ui/public/home-gallery.js +8 -1
  62. package/modules/hall-ui/public/home-idea-box.css +2 -2
  63. package/modules/hall-ui/public/home-idea-box.js +3 -3
  64. package/modules/hall-ui/public/idea.html +5 -5
  65. package/modules/hall-ui/public/ideas.html +5 -5
  66. package/modules/hall-ui/public/index.html +11 -7
  67. package/modules/hall-ui/public/market-watch-item.html +4 -4
  68. package/modules/hall-ui/public/market-watch-rules.html +4 -4
  69. package/modules/hall-ui/public/market-watch.html +6 -9
  70. package/modules/hall-ui/public/market-watch.js +22 -45
  71. package/modules/hall-ui/public/modules.html +4 -4
  72. package/modules/hall-ui/public/money.html +4 -4
  73. package/modules/hall-ui/public/my-agents.html +4 -4
  74. package/modules/hall-ui/public/options-menu.js +409 -0
  75. package/modules/hall-ui/public/paintbrush.js +3 -3
  76. package/modules/hall-ui/public/palette.js +11 -13
  77. package/modules/hall-ui/public/primer.html +5 -5
  78. package/modules/hall-ui/public/profile-crafts.js +1 -1
  79. package/modules/hall-ui/public/profile-roles.js +23 -19
  80. package/modules/hall-ui/public/profile.css +1 -1
  81. package/modules/hall-ui/public/profile.html +19 -18
  82. package/modules/hall-ui/public/profile.js +19 -18
  83. package/modules/hall-ui/public/profile.states.json +4 -4
  84. package/modules/hall-ui/public/project-settings.html +4 -4
  85. package/modules/hall-ui/public/roster.html +4 -4
  86. package/modules/hall-ui/public/roster.js +3 -3
  87. package/modules/hall-ui/public/settings.css +24 -4
  88. package/modules/hall-ui/public/settings.html +21 -17
  89. package/modules/hall-ui/public/settings.js +44 -44
  90. package/modules/hall-ui/public/shell-game.css +65 -0
  91. package/modules/hall-ui/public/shell-game.js +283 -3
  92. package/modules/hall-ui/public/shell.js +7 -26
  93. package/modules/hall-ui/public/sky-interaction.js +7 -0
  94. package/modules/hall-ui/public/speciality-tools.js +1 -2
  95. package/modules/hall-ui/public/street-inside.js +3 -4
  96. package/modules/hall-ui/public/street.html +4 -4
  97. package/modules/hall-ui/public/studio.html +5 -5
  98. package/modules/hall-ui/public/style.css +73 -47
  99. package/modules/hall-ui/public/task-detail.js +5 -4
  100. package/modules/hall-ui/public/task.html +5 -5
  101. package/modules/hall-ui/public/thinking.css +10 -0
  102. package/modules/hall-ui/public/thinking.html +9 -6
  103. package/modules/hall-ui/public/tweak-editor.html +5 -5
  104. package/modules/hall-ui/public/tweak-editor.js +5 -1
  105. package/modules/hall-ui/public/update-pill.js +91 -2
  106. package/modules/hall-ui/public/visor.css +80 -0
  107. package/modules/hall-ui/public/visor.js +316 -0
  108. package/modules/hall-ui/public/watch-repo-invite.js +159 -0
  109. package/modules/hall-ui/public/watch.html +6 -5
  110. package/modules/hall-ui/public/watch.js +17 -2
  111. package/modules/hall-ui/public/welcome-boot.js +15 -6
  112. package/modules/hall-ui/public/work.html +5 -5
  113. package/modules/hall-ui/records/builders-roster.md +2 -2
  114. package/modules/hall-ui/records/craft-mode.md +13 -11
  115. package/modules/hall-ui/records/game-mode.md +2 -2
  116. package/modules/hall-ui/records/governor-street.md +1 -1
  117. package/modules/hall-ui/records/home-city.md +3 -1
  118. package/modules/hall-ui/records/market-watch-settings.md +2 -0
  119. package/modules/hall-ui/records/options-menu.md +15 -0
  120. package/modules/hall-ui/records/profile-roles.md +11 -11
  121. package/modules/hall-ui/records/sky.md +3 -3
  122. package/modules/hall-ui/records/start-screen.md +21 -0
  123. package/modules/hall-ui/records/tweak-editor.md +1 -1
  124. package/modules/hall-ui/records/update-pill.md +12 -0
  125. package/modules/hall-ui/records/visor.md +23 -0
  126. package/modules/hall-ui/records/welcome-tower.md +5 -5
  127. package/modules/ideas/ideator-stats.js +3 -1
  128. package/modules/ideas/inbox.js +2 -2
  129. package/modules/lifecycle/activity.js +1 -1
  130. package/modules/lifecycle/cascade.js +4 -4
  131. package/modules/lifecycle/goal-spec-task.js +1 -1
  132. package/modules/lifecycle/help-requests.js +19 -15
  133. package/modules/lifecycle/kickoff-checklist.js +3 -3
  134. package/modules/lifecycle/kickoff-seed.js +1 -1
  135. package/modules/lifecycle/migrations/lifecycle_021_crafts_artisan_artist.sql +29 -0
  136. package/modules/lifecycle/mingle.js +4 -4
  137. package/modules/lifecycle/page-tweak-write.js +1 -1
  138. package/modules/lifecycle/routes/task-write-routes.js +3 -1
  139. package/modules/lifecycle/routes/tasks.js +3 -3
  140. package/modules/lifecycle/ship-card.js +2 -3
  141. package/modules/lifecycle/task-classifier.js +39 -29
  142. package/modules/lifecycle/tests/cascade.mjs +7 -7
  143. package/modules/onboarding/founding-mode.js +3 -0
  144. package/modules/onboarding/genesis-home.js +21 -1
  145. package/modules/onboarding/routes/onboarding.js +2 -0
  146. package/modules/page-notes/notes.js +3 -2
  147. package/modules/page-notes/split.js +1 -1
  148. package/modules/platform-identity/craft-rollup.js +35 -7
  149. package/modules/platform-identity/migrations/platform_identity_036_crafts_artisan_artist.sql +45 -0
  150. package/modules/platform-identity/platform-identity.js +2 -2
  151. package/modules/platform-identity/routes/community-leaderboard.js +2 -1
  152. package/modules/platform-identity/routes/community-search.js +2 -1
  153. package/modules/platform-identity/routes/sso.js +2 -1
  154. package/modules/platform-identity/tests/platform-identity.mjs +6 -6
  155. package/modules/public-landing/public/community.html +3 -5
  156. package/modules/public-landing/public/index.html +1 -1
  157. package/modules/public-landing/public/projects.html +1 -1
  158. package/modules/specialities/migrations/specialities_004_crafts_artisan_artist.sql +30 -0
  159. package/modules/specialities/routes/specialities.js +2 -2
  160. package/modules/status-ui/public/status.js +1 -1
  161. package/modules/ui-design/kit/fixtures/community-leaderboard.json +10 -10
  162. package/modules/ui-design/kit/fixtures/community-search.json +2 -2
  163. package/modules/ui-design/kit/fixtures/scouting-recruiter.json +1 -1
  164. package/package-lock.json +2 -2
  165. package/package.json +1 -1
  166. package/release-notes.json +70 -0
  167. package/scripts/bongos/autobongos-loop.js +1 -1
  168. package/scripts/bongos/discipline-modes.json +7 -12
  169. package/scripts/bongos/fitness.js +1 -1
  170. package/scripts/bongos/gen-hall-sitemap.js +7 -7
  171. package/scripts/bongos/grade-smoke.js +2 -2
  172. package/scripts/bongos/init.js +2 -2
  173. package/scripts/bongos/newcomer-floor.config.json +1 -2
  174. package/scripts/bongos/newcomer-floor.js +4 -3
  175. package/scripts/bongos/newcomer-floor.templates.json +10 -12
  176. package/scripts/bongos/newcomer-floor.test.js +47 -38
  177. package/scripts/bongos/role-pack-guard.js +5 -4
  178. package/scripts/bongos/seed-tasks.example.json +3 -3
  179. package/scripts/bongos/setup.js +20 -107
  180. package/scripts/bongos/start.js +2 -2
  181. package/scripts/bongos/task.js +5 -4
  182. package/scripts/bongos/triage.js +1 -1
  183. package/scripts/bongos/worktree.js +16 -4
  184. package/scripts/setup-builder.js +6 -46
  185. package/src/bongos/auth-admission.js +8 -8
  186. package/src/bongos/craft-mode.js +2 -0
  187. package/src/bongos/db.js +39 -1
  188. package/src/bongos/github-repos.js +78 -0
  189. package/src/bongos/module-scope-map.js +22 -0
  190. package/src/bongos/role-stats.js +27 -11
  191. package/src/bongos/routes/github.js +100 -2
  192. package/src/bongos/routes/me.js +33 -0
  193. package/src/bongos/serve-internal.js +26 -9
  194. package/src/crafts.js +66 -0
  195. package/src/module-api.js +6 -1
  196. package/src/module-loader/manifest-schema.js +16 -0
  197. package/src/modules.js +2 -2
  198. package/src/role-responsibilities.js +6 -5
  199. package/tests/acclaim_feed_evidence_db.mjs +1 -1
  200. package/tests/activity_rollup_as_of.mjs +1 -1
  201. package/tests/api_alias_caller_ratchet.mjs +3 -3
  202. package/tests/application_lifecycle.mjs +1 -1
  203. package/tests/artist_review.mjs +1 -1
  204. package/tests/artist_review_goal.mjs +1 -1
  205. package/tests/blocker_hall_live_proof.mjs +1 -1
  206. package/tests/blocker_tasks_named_db.mjs +2 -2
  207. package/tests/canonical_profile_url.mjs +1 -1
  208. package/tests/cap_newcomer_tasks.mjs +13 -13
  209. package/tests/character_creation.mjs +294 -0
  210. package/tests/claim_gate_open_blocker.mjs +2 -2
  211. package/tests/claim_gate_rebase.mjs +6 -6
  212. package/tests/claude_materialize.mjs +1 -1
  213. package/tests/clawback_confirmed_backlog.mjs +3 -3
  214. package/tests/collab_page.mjs +8 -9
  215. package/tests/collab_self_settling.mjs +2 -2
  216. package/tests/community_leaderboard_route.mjs +13 -5
  217. package/tests/community_proof_db.mjs +3 -0
  218. package/tests/community_search_route.mjs +7 -6
  219. package/tests/conductor.mjs +1 -1
  220. package/tests/conductor_main_e2e.mjs +8 -8
  221. package/tests/copy_desk_flags_db.mjs +1 -1
  222. package/tests/copy_desk_page_approve.mjs +2 -2
  223. package/tests/copy_desk_page_claim.mjs +2 -2
  224. package/tests/copy_desk_page_next.mjs +1 -1
  225. package/tests/copy_desk_page_release.mjs +1 -1
  226. package/tests/copy_desk_page_submit.mjs +2 -2
  227. package/tests/core_258_box_session_claims_db.mjs +1 -1
  228. package/tests/craft_mode.mjs +48 -34
  229. package/tests/craft_mode_home.mjs +20 -20
  230. package/tests/craft_mode_looks.mjs +16 -15
  231. package/tests/craft_mode_nav.mjs +31 -29
  232. package/tests/craft_review_mode.mjs +46 -41
  233. package/tests/crafts_vocabulary.mjs +70 -0
  234. package/tests/cross_discipline_priority_direction.mjs +5 -5
  235. package/tests/fitness.mjs +7 -11
  236. package/tests/funded_project_pays_proof_db.mjs +1 -1
  237. package/tests/genesis_home.mjs +21 -1
  238. package/tests/github_chip.mjs +3 -3
  239. package/tests/github_repo_invite.mjs +379 -0
  240. package/tests/goal_criterion_confirm_ui.mjs +1 -1
  241. package/tests/goal_invite_consent.mjs +1 -1
  242. package/tests/goal_map_page.mjs +2 -2
  243. package/tests/goal_sort_staleness.mjs +1 -1
  244. package/tests/goal_spec_task.mjs +1 -1
  245. package/tests/goal_spec_task_db.mjs +3 -3
  246. package/tests/grade_engine.mjs +2 -2
  247. package/tests/grader_no_artifact.mjs +1 -1
  248. package/tests/grader_prompt_fencing.mjs +1 -1
  249. package/tests/grader_prompt_window_bound.mjs +1 -1
  250. package/tests/grader_reader_lens.mjs +2 -2
  251. package/tests/grader_severity_gate.mjs +1 -1
  252. package/tests/grader_unbacked_score.mjs +1 -1
  253. package/tests/hall_approval_queue.mjs +1 -1
  254. package/tests/hall_board_world.mjs +2 -2
  255. package/tests/hall_builders_page_pass.mjs +1 -1
  256. package/tests/hall_city.mjs +1 -1
  257. package/tests/hall_fullscreen.mjs +27 -3
  258. package/tests/hall_kit_charts.mjs +1 -1
  259. package/tests/hall_landing_boot.mjs +1 -1
  260. package/tests/hall_my_agents.mjs +16 -16
  261. package/tests/hall_nav.mjs +22 -15
  262. package/tests/hall_options_menu.mjs +247 -0
  263. package/tests/hall_oversight_world.mjs +1 -1
  264. package/tests/hall_palette.mjs +8 -9
  265. package/tests/hall_profile_world.mjs +3 -3
  266. package/tests/hall_record_world.mjs +2 -2
  267. package/tests/hall_settings_world.mjs +2 -1
  268. package/tests/hall_sitemap.mjs +6 -5
  269. package/tests/hall_start_screen.mjs +244 -0
  270. package/tests/hall_street.mjs +2 -2
  271. package/tests/hall_street_inside.mjs +17 -9
  272. package/tests/hall_studio_world.mjs +3 -3
  273. package/tests/hall_visor.mjs +224 -0
  274. package/tests/help_requests.mjs +47 -15
  275. package/tests/helpers.mjs +4 -4
  276. package/tests/home_city.mjs +6 -4
  277. package/tests/home_city_widget.mjs +33 -14
  278. package/tests/home_gallery_widget.mjs +4 -4
  279. package/tests/home_idea_box.mjs +6 -6
  280. package/tests/hub_community_page.mjs +3 -3
  281. package/tests/hub_craft_rollup.mjs +75 -36
  282. package/tests/hub_recent_ships.mjs +3 -2
  283. package/tests/idea_cascade.mjs +2 -2
  284. package/tests/idea_credit_streams_db.mjs +1 -1
  285. package/tests/idea_detail_page.mjs +1 -1
  286. package/tests/idea_ideate_full.mjs +5 -2
  287. package/tests/idea_spark_hall.mjs +1 -1
  288. package/tests/init.mjs +3 -3
  289. package/tests/instance_manifest.mjs +2 -2
  290. package/tests/kickoff_checklist.mjs +1 -1
  291. package/tests/leaderboard_roles.mjs +34 -25
  292. package/tests/lifecycle_claim_ladder_refs.mjs +55 -0
  293. package/tests/lightbulb.mjs +14 -10
  294. package/tests/main_discipline.mjs +8 -8
  295. package/tests/markdown_line_endings.mjs +1 -1
  296. package/tests/market_watch_pages.mjs +52 -74
  297. package/tests/me_streak_leaderboard_queries.mjs +3 -3
  298. package/tests/migration_allocation.mjs +1 -1
  299. package/tests/mingle.mjs +36 -21
  300. package/tests/module-scope-map.mjs +22 -2
  301. package/tests/module_api.mjs +1 -0
  302. package/tests/module_contributions.mjs +13 -13
  303. package/tests/module_manifest.mjs +29 -0
  304. package/tests/module_skills_hook.mjs +2 -2
  305. package/tests/modules.mjs +14 -7
  306. package/tests/nav_permission_atoms.mjs +2 -1
  307. package/tests/needs_rebase.mjs +1 -1
  308. package/tests/newcomer_restock.mjs +13 -13
  309. package/tests/page_notes_panel.mjs +22 -22
  310. package/tests/page_tweak_no_block.mjs +1 -1
  311. package/tests/paintbrush.mjs +7 -7
  312. package/tests/privacy_matrix.mjs +1 -0
  313. package/tests/profile_activity.mjs +20 -23
  314. package/tests/profile_earnings.mjs +1 -1
  315. package/tests/profile_nudge_prefs.mjs +4 -4
  316. package/tests/profile_role_headline.mjs +73 -46
  317. package/tests/profile_role_stats.mjs +55 -45
  318. package/tests/profile_rollup_consent.mjs +19 -6
  319. package/tests/profile_route.mjs +12 -12
  320. package/tests/profile_ui_cross_project.mjs +4 -4
  321. package/tests/project_invite_suggestions.mjs +2 -2
  322. package/tests/promote_goal_id.mjs +1 -1
  323. package/tests/publish_manifest.mjs +1 -1
  324. package/tests/rank_floor_wiring.mjs +1 -1
  325. package/tests/regrade_actual_minutes.mjs +1 -1
  326. package/tests/removal_proposal.mjs +2 -2
  327. package/tests/reward_gate.mjs +4 -4
  328. package/tests/reward_mode_cost_plus_only.mjs +1 -1
  329. package/tests/role_responsibilities.mjs +18 -12
  330. package/tests/role_stats_db.mjs +17 -16
  331. package/tests/scouting_rollup_fanout.mjs +5 -5
  332. package/tests/seed_tasks.mjs +5 -2
  333. package/tests/session_earnings_mirror_db.mjs +1 -1
  334. package/tests/session_reward_on_land_db.mjs +1 -1
  335. package/tests/session_reward_reconcile_db.mjs +2 -2
  336. package/tests/sessions_kit_pager.mjs +1 -1
  337. package/tests/settings_art_key_absent.mjs +2 -2
  338. package/tests/settings_main_role.mjs +21 -20
  339. package/tests/setup_flow.mjs +3 -3
  340. package/tests/setup_smoke.mjs +20 -0
  341. package/tests/ship_card.mjs +6 -6
  342. package/tests/specialities.mjs +16 -4
  343. package/tests/speciality_db_shape.mjs +6 -6
  344. package/tests/speciality_injection.mjs +17 -17
  345. package/tests/speciality_offer_ui.mjs +5 -5
  346. package/tests/speciality_offered.mjs +6 -5
  347. package/tests/speciality_session_skills.mjs +7 -7
  348. package/tests/speciality_skills.mjs +11 -11
  349. package/tests/speciality_walkthrough_ui.mjs +2 -2
  350. package/tests/sso_checkin_identity_spoof.mjs +2 -1
  351. package/tests/task_classifier_vocab.mjs +10 -0
  352. package/tests/task_cli.mjs +11 -3
  353. package/tests/task_criteria_unlink_db.mjs +2 -2
  354. package/tests/task_discipline_required.mjs +38 -16
  355. package/tests/task_goal_required.mjs +4 -4
  356. package/tests/task_needs_db.mjs +1 -1
  357. package/tests/tweak_replaces_artist_review.mjs +1 -1
  358. package/tests/update_pill.mjs +153 -2
  359. package/tests/upkeep_home.mjs +1 -1
  360. package/tests/v3_c1_options_proof.mjs +145 -0
  361. package/tests/visual_review.mjs +3 -3
  362. package/tests/watch_applications_queue.mjs +8 -5
  363. package/tests/watch_sealed_floor.mjs +3 -1
  364. package/tests/welcome_boot.mjs +12 -9
  365. package/tests/welcome_boot_browser.mjs +13 -13
  366. package/tests/worktree_helper.mjs +16 -1
@@ -3,7 +3,10 @@
3
3
  // criterion wa6-roles-read-true-in-public).
4
4
  //
5
5
  // The done-when: "engineer, artist and ideator fixtures each render their own
6
- // headline, matching the mock". Three layers, each run for real:
6
+ // headline, matching the mock". Since task 1005141 (BONGOS-V3) there are two
7
+ // crafts: the engineer is the ARTISAN, and the ideator folded into the ARTIST,
8
+ // so the ideas-only fixture is an artist whose pages count is zero. Three
9
+ // layers, each run for real:
7
10
  //
8
11
  // 1. The words. profile-roles.js (pure) over the three harness fixtures the
9
12
  // renders were taken from: each craft's tally, tiles and subtitle.
@@ -32,9 +35,16 @@ const OTBKit = require('../modules/hall-ui/public/hall-kit.js');
32
35
  const ROLES_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/profile-roles.js'), 'utf8');
33
36
  const PAGE_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/profile.js'), 'utf8');
34
37
 
35
- const ENGINEER = FIX('builders__profile');
38
+ const ARTISAN = FIX('builders__profile');
36
39
  const ARTIST = FIX('builders__2001__profile');
37
- const IDEATOR = FIX('builders__2002__profile');
40
+ // The ideas-only artist, as the server sends it: role-stats.js merges the page
41
+ // half in, and with copy-desk present a person with no pages gets true zeros
42
+ // (never a missing field), and `credits` is the craft's ledger share.
43
+ const IDEAS_FIX = FIX('builders__2002__profile');
44
+ const IDEAS_ARTIST = { ...IDEAS_FIX, roles: { ...IDEAS_FIX.roles, artist: {
45
+ ...IDEAS_FIX.roles.artist,
46
+ pages_approved: 0, pages: 0, surfaces_touched: 0, credits: 186, this_week: 0, surfaces: [], unavailable: false,
47
+ } } };
38
48
  const OPTS = { fmt: (n) => Number(n).toLocaleString('en-US'), currency: 'credits' };
39
49
  const words = (items) => items.map((t) => `${t.n} ${t.w}`);
40
50
 
@@ -42,63 +52,71 @@ const { test, summary } = makeRunner();
42
52
 
43
53
  // ---- 1. the words --------------------------------------------------------
44
54
 
45
- await test('an engineer leads with works shipped, and keeps karma', () => {
46
- const v = headline(ENGINEER, OPTS);
55
+ await test('an artisan leads with works shipped, and keeps karma', () => {
56
+ const v = headline(ARTISAN, OPTS);
47
57
  assert.deepEqual(words(v.tally), ['4,630 credits', '24 works shipped', '1 karma']);
48
58
  assert.deepEqual(words(v.tiles).slice(0, 4), ['24 works shipped', '4,630 credits', '1 karma', '2 achievements']);
49
59
  assert.equal(`${v.tally[1].n} ${v.tally[1].w}`, '24 works shipped', 'the main craft leads the tally');
50
60
  assert.deepEqual(v.surfaces, [], 'the per-surface rows are the artist\'s');
51
61
  });
52
62
 
53
- await test('an artist who never shipped a task leads with pages approved and the per-surface share', () => {
63
+ await test('an artist who never shipped a task leads with pages approved, her ideas and the per-surface share', () => {
54
64
  const v = headline(ARTIST, OPTS);
55
- assert.deepEqual(words(v.tally), ['412 credits', '9 pages approved', '+38 this week']);
56
- assert.deepEqual(words(v.tiles), ['9 pages approved', '270 credits from pages', '+38 this week', '4 thoughts filed', '6 karma']);
65
+ assert.deepEqual(words(v.tally), ['412 credits', '9 pages approved', '4 thoughts filed'],
66
+ 'one number from each half of the craft: pages, then thoughts (task 1005141)');
67
+ assert.deepEqual(words(v.tiles), ['9 pages approved', '+38 this week', '270 credits', '4 thoughts filed', '1 ignited',
68
+ '0 Full Ideas', '42 total size', '85% still alive', '6 karma'], 'the pages tiles, then the ideas tiles, in ONE set');
69
+ assert.equal(v.tiles[2].f, 'pages and ideas', 'the craft credits are both halves');
57
70
  assert.equal(v.tiles[0].f, '3 surfaces');
58
71
  assert.equal(`${v.tally[1].n} ${v.tally[1].w}`, '9 pages approved', 'the main craft leads the tally');
59
72
  assert.ok(!v.tally.some((t) => t.w === 'works shipped'), 'an artist\'s tally never says works shipped');
60
73
  assert.ok(!('sub' in v), 'no sub-line is computed: the page has none (task 1004827)');
61
74
  assert.deepEqual(v.surfaces.map((s) => s.line), ['6 of 12 tweaked', '2 of 19 tweaked', '1 of 6 tweaked']);
62
- assert.ok(!words(v.tiles).some((w) => /works shipped/.test(w)), 'no engineer figure on an artist\'s numbers');
63
- assert.equal(words(v.tiles).at(-1), '6 karma', 'karma ends every non-engineer set (the owner, 2026-10-01)');
75
+ assert.ok(!words(v.tiles).some((w) => /works shipped/.test(w)), 'no artisan figure on an artist\'s numbers');
76
+ assert.equal(words(v.tiles).at(-1), '6 karma', 'karma ends every non-artisan set (the owner, 2026-10-01)');
64
77
  });
65
78
 
66
- await test('an ideator leads with thoughts, size and what is still alive', () => {
67
- const v = headline(IDEATOR, OPTS);
68
- assert.deepEqual(words(v.tally), ['186 credits', '14 thoughts filed', '7 ignited']);
69
- assert.deepEqual(words(v.tiles), ['14 thoughts filed', '7 ignited', '3 Full Ideas', '186~ total size', '72% still alive', '3 karma']);
70
- assert.equal(v.tiles[2].f, '1 ratified by the board');
71
- assert.match(v.tiles[3].title, /Includes 24 credits of historical estimate/, 'the ~ says what it means');
72
- assert.equal(`${v.tally[1].n} ${v.tally[1].w}`, '14 thoughts filed', 'the main craft leads the tally');
79
+ await test('an artist who only files ideas shows thoughts, size and what is still alive', () => {
80
+ const v = headline(IDEAS_ARTIST, OPTS);
81
+ assert.deepEqual(words(v.tally), ['186 credits', '14 thoughts filed', '7 ignited'], 'no approved page: the lead is the ideas, never a zero (task 1005141)');
82
+ assert.deepEqual(words(v.tiles), ['0 pages approved', '+0 this week', '186 credits', '14 thoughts filed', '7 ignited',
83
+ '3 Full Ideas', '186~ total size', '72% still alive', '3 karma']);
84
+ assert.equal(v.tiles[5].f, '1 ratified by the board');
85
+ assert.match(v.tiles[6].title, /Includes 24 credits of historical estimate/, 'the ~ says what it means');
86
+ assert.deepEqual(v.surfaces, [], 'no surface rows without pages');
73
87
  });
74
88
 
75
89
  await test('several crafts each show, main first, in the server\'s order', () => {
76
- assert.deepEqual(headline(ARTIST, OPTS).crafts.map((c) => c.label), ['Artist, main role', 'Ideator']);
77
- assert.deepEqual(headline(ENGINEER, OPTS).crafts.map((c) => c.label), ['Engineer, main role', 'Ideator']);
78
- // The other craft adds its one headline tile after the main craft's set.
79
- assert.equal(words(headline(ENGINEER, OPTS).tiles).at(-1), '9 thoughts filed');
80
- const reordered = { ...ARTIST, roles: { ...ARTIST.roles, main: 'ideator', shown: ['ideator', 'artist'] } };
90
+ assert.deepEqual(headline(ARTIST, OPTS).crafts.map((c) => c.label), ['Artist, main role'], 'one artist chip, never two');
91
+ assert.deepEqual(headline(ARTISAN, OPTS).crafts.map((c) => c.label), ['Artisan, main role', 'Artist']);
92
+ // The other craft adds its one headline tile after the main craft's set; the
93
+ // artist's names their pages, with their thoughts beneath.
94
+ const second = headline(ARTISAN, OPTS).tiles.at(-1);
95
+ assert.equal(second.w, 'pages approved');
96
+ assert.equal(second.f, '9 thoughts filed');
97
+ const reordered = { ...ARTIST, roles: { ...ARTIST.roles, main: 'artisan', shown: ['artisan', 'artist'], artisan: { shipped: 0, credits: 0 } } };
81
98
  const v = headline(reordered, OPTS);
82
- assert.deepEqual(v.crafts.map((c) => c.label), ['Ideator, main role', 'Artist']);
83
- assert.equal(words(v.tiles).at(-2), '9 pages approved', 'the other craft\'s tile, just before karma');
99
+ assert.deepEqual(v.crafts.map((c) => c.label), ['Artisan, main role', 'Artist']);
100
+ assert.equal(words(v.tiles).at(-1), '9 pages approved', 'the other craft\'s tile closes the artisan set');
101
+ assert.equal(v.tiles.at(-1).f, '4 thoughts filed');
84
102
  assert.deepEqual(v.surfaces, [], 'the per-surface rows follow the MAIN craft, not any craft');
85
103
  });
86
104
 
87
105
  await test('a number the instance could not read is a dash, never a zero', () => {
88
- const noDesk = { ...ARTIST, roles: { ...ARTIST.roles, artist: { pages_approved: null, credits: null, this_week: null, surfaces_touched: null, surfaces: [], unavailable: true } } };
106
+ const noDesk = { ...ARTIST, roles: { ...ARTIST.roles, artist: { ...ARTIST.roles.artist, pages_approved: null, pages: null, this_week: null, surfaces_touched: null, surfaces: [] } } };
89
107
  const v = headline(noDesk, OPTS);
90
- assert.deepEqual(words(v.tally).slice(1), ['— pages approved', '— this week']);
108
+ assert.deepEqual(words(v.tally).slice(1), ['— pages approved', '4 thoughts filed'], 'the page half unknown, the ideas half still read');
91
109
  assert.equal(v.tiles[0].f, '', 'no "0 surfaces" under an unknown count');
92
- const noWork = { ...IDEATOR, roles: { ...IDEATOR.roles, ideator: { ...IDEATOR.roles.ideator, alive: { share: null, has_work: false, attributable: null } } } };
110
+ const noWork = { ...IDEAS_ARTIST, roles: { ...IDEAS_ARTIST.roles, artist: { ...IDEAS_ARTIST.roles.artist, alive: { share: null, has_work: false, attributable: null } } } };
93
111
  assert.deepEqual(headline(noWork, OPTS).tiles.at(-2), { n: '—', w: 'still alive', f: 'nothing has come of them yet' });
94
- const untraced = { ...IDEATOR, roles: { ...IDEATOR.roles, ideator: { ...IDEATOR.roles.ideator, alive: { share: null, has_work: true, attributable: false } } } };
112
+ const untraced = { ...IDEAS_ARTIST, roles: { ...IDEAS_ARTIST.roles, artist: { ...IDEAS_ARTIST.roles.artist, alive: { share: null, has_work: true, attributable: false } } } };
95
113
  assert.equal(headline(untraced, OPTS).tiles.at(-2).f, 'not measurable yet');
96
114
  });
97
115
 
98
- await test('a response from before the roles block reads as the engineer it always was', () => {
99
- const { roles, ...old } = ENGINEER;
116
+ await test('a response from before the roles block reads as the artisan (the engineer) it always was', () => {
117
+ const { roles, ...old } = ARTISAN;
100
118
  const v = headline(old, OPTS);
101
- assert.deepEqual(v.crafts.map((c) => c.label), ['Engineer, main role']);
119
+ assert.deepEqual(v.crafts.map((c) => c.label), ['Artisan, main role']);
102
120
  assert.equal(`${v.tally[1].n} ${v.tally[1].w}`, '24 works shipped', 'the main craft leads the tally');
103
121
  });
104
122
 
@@ -124,7 +142,7 @@ async function boot(profileData) {
124
142
  };
125
143
  const windowObj = {
126
144
  __BRANDING__: { currency: { label: 'credits' } },
127
- __MODULES__: { responsibilities: { engineer: 'E.', artist: 'A.', ideator: 'I.' } },
145
+ __MODULES__: { responsibilities: { artisan: 'E.', artist: 'A.' } },
128
146
  BongosClient: { createClient: () => api },
129
147
  // The real kit draws the surface bars; the section rail needs a real DOM and
130
148
  // is not what this suite is about, so it is left out (profile.js skips it).
@@ -154,22 +172,23 @@ const text = (html) => html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
154
172
  await test('the real page draws the artist\'s tally, chips, tiles and surface rows', async () => {
155
173
  const p = await boot(ARTIST);
156
174
  assert.equal(p.sub, '', 'no sub-line: the head names the person, rank and headline (task 1004827)');
157
- assert.match(text(p.head), /Artist, main role Ideator/);
158
- assert.match(text(p.head), /412 credits 9 pages approved \+38 this week/);
175
+ assert.match(text(p.head), /Artist, main role Answerable for/, 'one artist chip, and no ideator chip beside it');
176
+ assert.ok(!/Ideator/.test(p.head), 'the ideator is not a craft any more (task 1005141)');
177
+ assert.match(text(p.head), /412 credits 9 pages approved 4 thoughts filed/);
159
178
  assert.match(p.head, /Answerable for:/, 'the responsibility line stays');
160
- assert.match(text(p.nums), /^9 pages approved 3 surfaces 270 credits from pages \+38 this week 4 thoughts filed 1 ignited/);
179
+ assert.match(text(p.nums), /^9 pages approved 3 surfaces \+38 this week 270 credits pages and ideas 4 thoughts filed 1 ignited/);
161
180
  assert.equal((p.nums.match(/kit-bars__row/g) || []).length, 3, 'one kit bar row per surface');
162
181
  assert.match(text(p.nums), /Landing 6 of 12 tweaked/);
163
182
  assert.ok(!/works shipped/i.test(p.sub + p.head + p.nums), 'nothing on an artist\'s header says works shipped');
164
183
  });
165
184
 
166
- await test('the real page draws the ideator\'s and the engineer\'s own headlines', async () => {
167
- const i = await boot(IDEATOR);
185
+ await test('the real page draws the ideas-only artist\'s and the artisan\'s own headlines', async () => {
186
+ const i = await boot(IDEAS_ARTIST);
168
187
  assert.match(text(i.head), /186 credits 14 thoughts filed 7 ignited/);
169
188
  assert.match(text(i.nums), /186~ total size/);
170
189
  assert.match(text(i.nums), /72% still alive/);
171
- assert.ok(!i.nums.includes('kit-bars'), 'no surface rows for an ideator');
172
- const e = await boot({ ...ENGINEER, recent_ships: [] }); // the works ledger is not this suite's
190
+ assert.ok(!i.nums.includes('kit-bars'), 'no surface rows for an artist with no pages');
191
+ const e = await boot({ ...ARTISAN, recent_ships: [] }); // the works ledger is not this suite's
173
192
  assert.equal(e.sub, '', 'no sub-line (task 1004827)');
174
193
  assert.match(text(e.head), /4,630 credits 24 works shipped 1 karma/);
175
194
  });
@@ -181,21 +200,29 @@ await test('the server\'s roles block, through JSON, renders the artist\'s headl
181
200
  const { readRoleStats } = require('../src/bongos/role-stats.js');
182
201
  seams._reset();
183
202
  seams.registerProvider('reward', {
184
- craftForPreference: (d) => ({ engineer: 'engineer', artist: 'artist', ui: 'artist', ideator: 'ideator' }[d] || null),
185
- async creditsByCraft(ids) { return new Map([[String(ids[0]), { engineer: 0, artist: 370, ideator: 42, other: 0, total: 412 }]]); },
203
+ craftForPreference: (d) => ({ artisan: 'artisan', artist: 'artist', ui: 'artist' }[d] || null),
204
+ async creditsByCraft(ids) { return new Map([[String(ids[0]), { artisan: 0, artist: 412, other: 0, total: 412 }]]); },
186
205
  });
187
- seams.registerProvider('copy-desk.artistStats', async () => ARTIST.roles.artist);
188
- seams.registerProvider('ideas.ideatorStats', async () => ARTIST.roles.ideator);
206
+ // Each port answers ITS half of the artist block, as in production.
207
+ const PAGE_KEYS = ['pages_approved', 'pages', 'surfaces_touched', 'credits', 'this_week', 'week_since', 'surfaces'];
208
+ const pick = (keep) => Object.fromEntries(Object.entries(ARTIST.roles.artist).filter(([k]) => keep(k)));
209
+ seams.registerProvider('copy-desk.artistStats', async () => pick((k) => PAGE_KEYS.includes(k)));
210
+ seams.registerProvider('ideas.ideatorStats', async () => pick((k) => !PAGE_KEYS.includes(k)));
189
211
  const roles = await readRoleStats('2001', {
190
212
  shippedCount: 0,
191
- getRoleChoice: async () => ({ main_discipline: 'artist', preferred_disciplines: ['artist', 'ideator'] }),
213
+ getRoleChoice: async () => ({ main_discipline: 'artist', preferred_disciplines: ['artist'] }),
192
214
  });
215
+ assert.equal(roles.ideator, undefined, 'no ideator block on the wire');
216
+ assert.equal(roles.artist.filed, 4, 'the ideas half reached the artist block');
217
+ assert.equal(roles.artist.pages_approved, 9, 'and so did the pages half');
218
+ assert.equal(roles.artist.credits, 412, 'the craft credits are the ledger split, both halves');
193
219
  seams._reset();
194
220
  const { roles: _fixture, ...rest } = ARTIST;
195
221
  const p = await boot({ ...rest, roles });
196
222
  assert.equal(p.sub, '', 'no sub-line (task 1004827)');
197
- assert.match(text(p.head), /Artist, main role Ideator/);
198
- assert.match(text(p.head), /412 credits 9 pages approved \+38 this week/);
223
+ assert.match(text(p.head), /Artist, main role Answerable for/);
224
+ assert.match(text(p.head), /412 credits 9 pages approved 4 thoughts filed/);
225
+ assert.match(text(p.nums), /412 credits pages and ideas/);
199
226
  assert.equal((p.nums.match(/kit-bars__row/g) || []).length, 3);
200
227
  });
201
228
 
@@ -10,8 +10,9 @@
10
10
  // over the REAL page inventory, and economy's REAL fold of the ledger rows:
11
11
  //
12
12
  // Ines Varga an artist who has never shipped a task: pages and ideas only
13
- // Tobi Rahn an ideator who has never shipped a task: thoughts only
14
- // Mara Okonkwo an engineer, who also earned a little from an idea
13
+ // Tobi Rahn an artist (the old ideator, folded in by task 1005141) who has
14
+ // never shipped a task: thoughts only, no pages
15
+ // Mara Okonkwo an artisan, who also earned a little from an idea
15
16
  //
16
17
  // The SQL halves (which ledger rows the classification and the per-person
17
18
  // earnings reach) run against a real Postgres in tests/role_stats_db.mjs; here
@@ -40,29 +41,32 @@ async function t(name, fn) {
40
41
 
41
42
  const INES = 201; const TOBI = 202; const MARA = 203; const NEW = 204; const UI = 205; const LEA = 206;
42
43
  const ROWS = {
43
- [INES]: { id: INES, github_login: 'ines', display_name: 'Ines Varga', total_credits: 312, karma: 0, rank: 'thetes', preferred_disciplines: ['artist', 'ideator'], main_discipline: null },
44
- [TOBI]: { id: TOBI, github_login: 'tobi', display_name: 'Tobi Rahn', total_credits: 186, karma: 0, rank: 'thetes', preferred_disciplines: ['ideator'], main_discipline: null },
45
- [MARA]: { id: MARA, github_login: 'mara', display_name: 'Mara Okonkwo', total_credits: 2480, karma: 118, rank: 'metic', preferred_disciplines: ['engineer', 'ideator'], main_discipline: 'engineer' },
44
+ [INES]: { id: INES, github_login: 'ines', display_name: 'Ines Varga', total_credits: 312, karma: 0, rank: 'thetes', preferred_disciplines: ['artist'], main_discipline: null },
45
+ [TOBI]: { id: TOBI, github_login: 'tobi', display_name: 'Tobi Rahn', total_credits: 186, karma: 0, rank: 'thetes', preferred_disciplines: ['artist'], main_discipline: null },
46
+ [MARA]: { id: MARA, github_login: 'mara', display_name: 'Mara Okonkwo', total_credits: 2480, karma: 118, rank: 'metic', preferred_disciplines: ['artisan', 'artist'], main_discipline: 'artisan' },
46
47
  [NEW]: { id: NEW, github_login: 'new', display_name: 'Newcomer', total_credits: 0, karma: 0, rank: 'xenos', preferred_disciplines: [], main_discipline: null },
47
- [UI]: { id: UI, github_login: 'priya', display_name: 'Priya Das', total_credits: 60, karma: 0, rank: 'metic', preferred_disciplines: ['engineer', 'ui'], main_discipline: 'ui' },
48
+ [UI]: { id: UI, github_login: 'priya', display_name: 'Priya Das', total_credits: 60, karma: 0, rank: 'metic', preferred_disciplines: ['artisan', 'ui'], main_discipline: 'ui' },
48
49
  [LEA]: { id: LEA, github_login: 'lea', display_name: 'Lea Brandt', total_credits: 30, karma: 0, rank: 'thetes', preferred_disciplines: ['artist'], main_discipline: null },
49
50
  };
50
51
  const SHIPPED = { [INES]: 0, [TOBI]: 0, [MARA]: 64, [NEW]: 0, [UI]: 2, [LEA]: 0 };
51
52
 
52
53
  // What CREDITS_BY_CRAFT_SQL returns, grouped by (builder, discipline). Ines's
53
54
  // session reward was split across two tasks of different crafts, so her rows
54
- // arrive as halves; the fold has to make them exact again.
55
+ // arrive as halves; the fold has to make them exact again. Her page credits
56
+ // (120) and her idea credits (42) both count as artist money since task 1005141;
57
+ // the statement tags the idea streams 'ideas' so the sky's idea total can still
58
+ // read them alone (credits-by-craft.js IDEAS), so they arrive as two rows.
55
59
  const LEDGER_ROWS = [
56
60
  { builder_id: String(INES), discipline: 'artist', amount: 120 },
57
- { builder_id: String(INES), discipline: 'ideator', amount: 42 },
58
- { builder_id: String(INES), discipline: 'engineer', amount: 74.5 },
61
+ { builder_id: String(INES), discipline: 'ideas', amount: 42 },
62
+ { builder_id: String(INES), discipline: 'artisan', amount: 74.5 },
59
63
  { builder_id: String(INES), discipline: 'ui', amount: 74.5 },
60
64
  { builder_id: String(INES), discipline: 'other', amount: 1 },
61
- { builder_id: String(TOBI), discipline: 'ideator', amount: 186 },
62
- { builder_id: String(MARA), discipline: 'engineer', amount: 2460 },
63
- { builder_id: String(MARA), discipline: 'ideator', amount: 20 },
65
+ { builder_id: String(TOBI), discipline: 'ideas', amount: 186 },
66
+ { builder_id: String(MARA), discipline: 'artisan', amount: 2460 },
67
+ { builder_id: String(MARA), discipline: 'ideas', amount: 20 },
64
68
  { builder_id: String(UI), discipline: 'ui', amount: 60 },
65
- // a discipline nobody mapped yet counts as engineering, never vanishes
69
+ // a discipline nobody mapped yet counts as artisan work, never vanishes
66
70
  { builder_id: String(UI), discipline: 'some-module-craft', amount: 0 },
67
71
  ];
68
72
 
@@ -227,23 +231,26 @@ const profile = async (id) => {
227
231
 
228
232
  // ---- the artist who has never shipped a task --------------------------------
229
233
 
230
- await t('ARTIST, never shipped: leads with artist, by what she earned, and shows ideator beside it', async () => {
234
+ await t('ARTIST, never shipped: leads with artist, by what she earned, and shows artisan beside it', async () => {
231
235
  const { json } = await profile(INES);
232
236
  assert.equal(json.shipped_count, 0, 'the fixture is the case the task names: no shipped task at all');
233
237
  const r = json.roles;
234
238
  assert.equal(r.main, 'artist');
235
239
  assert.equal(r.main_basis, 'earned', 'no main role chosen yet, so it is inferred and says so');
236
- assert.deepEqual(r.shown, ['artist', 'ideator', 'engineer'],
237
- 'main first, then every craft she EARNED in, in her preferred order (her session reward was part engineering)');
238
- assert.equal(r.engineer.shipped, 0);
240
+ assert.deepEqual(r.shown, ['artist', 'artisan'],
241
+ 'main first, then every craft she EARNED in, ONCE each (her session reward was part artisan work)');
242
+ assert.equal(r.artisan.shipped, 0);
239
243
  });
240
244
 
241
- await t('ARTIST: pages approved, pages, N of M per surface, credits from pages and this week', async () => {
245
+ await t('ARTIST: pages approved, pages, N of M per surface, the craft credits and this week', async () => {
242
246
  const a = (await profile(INES)).json.roles.artist;
243
247
  assert.equal(a.pages_approved, 4, 'four approved rounds, each one paid');
244
248
  assert.equal(a.pages, 3, 'on three distinct pages (one was tweaked twice)');
245
249
  assert.equal(a.surfaces_touched, 2);
246
- assert.equal(a.credits, 120, 'credits from pages are the page rows only, not her other pay');
250
+ assert.equal(a.credits, 236, 'the artist craft ledger share: pages (120) + ideas (42) + the ui half of her session (74)');
251
+ assert.equal(a.size.paid, 42, 'her ideas total is her idea streams alone, never her page credits (task 1005141)');
252
+ assert.equal(a.filed, 4, 'and the same block carries her ideas (task 1005141)');
253
+ assert.equal(a.unavailable, false);
247
254
  assert.equal(a.this_week, 60, 'two approvals inside the trailing week');
248
255
  const landing = a.surfaces.find((s) => s.id === 'landing');
249
256
  const builders = a.surfaces.find((s) => s.id === 'builders');
@@ -258,30 +265,32 @@ await t('ARTIST: pages approved, pages, N of M per surface, credits from pages a
258
265
 
259
266
  await t('the credit split is exact: halves rounded by largest remainder, ui folded into artist', async () => {
260
267
  const c = (await profile(INES)).json.roles.credits_by_craft;
261
- assert.deepEqual(c, { engineer: 75, artist: 194, ideator: 42, other: 1, total: 312 },
268
+ assert.deepEqual(c, { artisan: 75, artist: 236, other: 1, total: 312 },
262
269
  '74.5 + 74.5 must not round to 150 or 148; the parts equal the whole');
263
- assert.equal(c.engineer + c.artist + c.ideator + c.other, c.total);
270
+ assert.equal(c.artisan + c.artist + c.other, c.total);
264
271
  });
265
272
 
266
- // ---- the ideator who has never shipped a task -------------------------------
273
+ // ---- the artist who only files ideas, and has never shipped a task -----------
267
274
 
268
- await t('IDEATOR, never shipped: leads with ideator, shows nothing it did not earn', async () => {
275
+ await t('IDEAS-ONLY ARTIST, never shipped: leads with artist, shows nothing it did not earn', async () => {
269
276
  const { json } = await profile(TOBI);
270
277
  assert.equal(json.shipped_count, 0);
271
- assert.equal(json.roles.main, 'ideator');
272
- assert.deepEqual(json.roles.shown, ['ideator']);
273
- assert.equal(json.roles.engineer, null, 'no engineering block for someone with no engineering credit');
274
- assert.equal(json.roles.artist, null);
278
+ assert.equal(json.roles.main, 'artist');
279
+ assert.deepEqual(json.roles.shown, ['artist']);
280
+ assert.equal(json.roles.artisan, null, 'no artisan block for someone with no artisan credit');
281
+ assert.equal(json.roles.ideator, undefined, 'there is no separate ideator craft any more');
282
+ assert.equal(json.roles.artist.pages_approved, 0, 'no pages approved is an answer, not a gap');
283
+ assert.equal(json.roles.artist.credits, 186, 'every one of them from ideas');
275
284
  });
276
285
 
277
- await t('IDEATOR: filed, ignited, Full Ideas, ratified, total size every stream + the estimate marker, still alive', async () => {
278
- const i = (await profile(TOBI)).json.roles.ideator;
286
+ await t('ARTIST ideas: filed, ignited, Full Ideas, ratified, total size every stream + the estimate marker, still alive', async () => {
287
+ const i = (await profile(TOBI)).json.roles.artist;
279
288
  assert.equal(i.filed, 8);
280
289
  assert.equal(i.ignited, 5, 'the five that were paid for work, a burned-out one included');
281
290
  assert.equal(i.full_ideas, 3);
282
291
  assert.equal(i.ratified, 1, 'the one the board carried');
283
292
  assert.deepEqual(i.size, { total: 196, paid: 186, estimated: 10, historical: true },
284
- 'the ledger ideator column (every stream) plus the historical estimate, marked');
293
+ 'the ledger artist column (every idea stream: Tobi has no pages) plus the historical estimate, marked');
285
294
  const expected = (64 * 1 + 42 * 0.6 + 18 + 22 * 0 + 26 * 0.85) / (64 + 42 + 18 + 22 + 26);
286
295
  assert.ok(Math.abs(i.alive.share - expected) < 1e-9, `size-weighted live share, got ${i.alive.share}`);
287
296
  assert.equal(i.alive.has_work, true);
@@ -294,30 +303,31 @@ await t('the trail is read at the public rank, so a count never changes with who
294
303
  assert.deepEqual(skyCalls, [{ builderId: TOBI, viewerRank: 'xenos' }]);
295
304
  });
296
305
 
297
- // ---- the engineer ------------------------------------------------------------
306
+ // ---- the artisan ------------------------------------------------------------
298
307
 
299
- await t('ENGINEER: unchanged headline (works shipped), chosen main role, a little ideator beside it', async () => {
308
+ await t('ARTISAN: unchanged headline (works shipped), chosen main role, a little artist (one idea) beside it', async () => {
300
309
  const r = (await profile(MARA)).json.roles;
301
- assert.equal(r.main, 'engineer');
310
+ assert.equal(r.main, 'artisan');
302
311
  assert.equal(r.main_basis, 'chosen');
303
- assert.deepEqual(r.shown, ['engineer', 'ideator']);
304
- assert.deepEqual(r.engineer, { shipped: 64, credits: 2460 });
305
- assert.equal(r.artist, null, 'no page read for someone with no artist credit');
306
- assert.equal(r.ideator.filed, 1);
307
- assert.equal(r.ideator.size.paid, 20);
312
+ assert.deepEqual(r.shown, ['artisan', 'artist']);
313
+ assert.deepEqual(r.artisan, { shipped: 64, credits: 2460 });
314
+ assert.equal(r.artist.pages_approved, 0, 'her artist credit is an idea, so the page count is a true zero');
315
+ assert.equal(r.artist.credits, 20);
316
+ assert.equal(r.artist.filed, 1);
317
+ assert.equal(r.artist.size.paid, 20);
308
318
  });
309
319
 
310
320
  // ---- the fallbacks and the boundary ------------------------------------------
311
321
 
312
- await t('main role: a chosen ui reads as artist; nothing chosen and nothing earned is engineering, said to be a default', async () => {
322
+ await t('main role: a chosen ui reads as artist; nothing chosen and nothing earned is artisan, said to be a default', async () => {
313
323
  const ui = (await profile(UI)).json.roles;
314
324
  assert.equal(ui.main, 'artist');
315
325
  assert.equal(ui.main_basis, 'chosen');
316
- assert.deepEqual(ui.preferred, ['engineer', 'artist']);
326
+ assert.deepEqual(ui.preferred, ['artisan', 'artist']);
317
327
  const nobody = (await profile(NEW)).json.roles;
318
- assert.equal(nobody.main, 'engineer');
328
+ assert.equal(nobody.main, 'artisan');
319
329
  assert.equal(nobody.main_basis, 'default');
320
- assert.deepEqual(nobody.shown, ['engineer']);
330
+ assert.deepEqual(nobody.shown, ['artisan']);
321
331
  assert.equal(nobody.credits_by_craft.total, 0, 'zero credits is an answer, not a gap');
322
332
  });
323
333
 
@@ -336,8 +346,8 @@ await t('a missing port reads as unknown (null), never as zero', async () => {
336
346
  seams.registerProvider('reward', { getBuilderAchievements: async () => [], creditsByCraft: (ids) => creditsByCraftLib.creditsByCraft(ids, { client: ledgerClient }) });
337
347
  try {
338
348
  const r = (await profile(TOBI)).json.roles;
339
- assert.equal(r.ideator.filed, null);
340
- assert.equal(r.ideator.unavailable, true);
349
+ assert.equal(r.artist.filed, null);
350
+ assert.equal(r.artist.unavailable, true);
341
351
  } finally {
342
352
  seams.registerProvider('ideas.ideatorStats', ideasPort);
343
353
  }
@@ -354,7 +364,7 @@ await t('apportion hands out exactly the rounding left over, larger remainder fi
354
364
  await t('the ledger statement classifies by reason, splits the session reward and escapes the artist prefix', () => {
355
365
  const sql = creditsByCraftLib.CREDITS_BY_CRAFT_SQL;
356
366
  assert.match(sql, /cl\.reason LIKE \$2 THEN 'artist'/);
357
- assert.match(sql, /idea\.credit\.%' OR cl\.reason = 'idea_promotion_bonus' THEN 'ideator'/);
367
+ assert.match(sql, /idea\.credit\.%' OR cl.reason = 'idea_promotion_bonus' THEN 'ideas'/);
358
368
  assert.match(sql, /GREATEST\(COALESCE\(cardinality\(sr\.task_ids\), 0\), 1\)/, 'an even split, and a session that shipped nothing still counts once');
359
369
  const last = ledgerSql[ledgerSql.length - 1];
360
370
  assert.equal(last.params[1], 'tweak.page\\_approved:%', 'the underscore is escaped, so it is not a LIKE wildcard');
@@ -144,14 +144,14 @@ try {
144
144
  await q(
145
145
  `INSERT INTO platform_identity_builder_project_activity
146
146
  (client_id, github_id, credits, tasks_shipped, karma, rank_held, disciplines, last_active)
147
- VALUES ($1, $6, 100, 5, 3, 'metic', '{engineer,ui}', '2026-01-01T00:00:00Z'),
147
+ VALUES ($1, $6, 100, 5, 3, 'metic', '{artisan,ui}', '2026-01-01T00:00:00Z'),
148
148
  ($2, $6, 50, 2, 1, 'archon', '{artist}', '2026-02-01T00:00:00Z'),
149
149
  ($3, $6, 25, 1, 0, NULL, '{}', '2026-03-01T00:00:00Z'),
150
150
  ($4, $6, 0, 0, 0, 'thetes', '{}', '2025-12-01T00:00:00Z'),
151
151
  ($5, $6, 0, 0, 0, 'thetes', '{}', '2025-12-01T00:00:00Z'),
152
- ($1, $7, 60, 4, 2, 'metic', '{engineer}', '2026-01-01T00:00:00Z'),
153
- ($1, $8, 10, 1, 0, 'xenos', '{engineer}', '2026-01-01T00:00:00Z'),
154
- ($1, $9, 90, 3, 1, 'metic', '{engineer}', '2026-01-01T00:00:00Z')`,
152
+ ($1, $7, 60, 4, 2, 'metic', '{artisan}', '2026-01-01T00:00:00Z'),
153
+ ($1, $8, 10, 1, 0, 'xenos', '{artisan}', '2026-01-01T00:00:00Z'),
154
+ ($1, $9, 90, 3, 1, 'metic', '{artisan}', '2026-01-01T00:00:00Z')`,
155
155
  [...ALL_CLIENTS, ACTIVE, HIDDEN, PROV, LEGACY],
156
156
  );
157
157
 
@@ -170,7 +170,7 @@ try {
170
170
  const r = await pi.getRollupForBuilder(ACTIVE, { pool });
171
171
  assert.deepEqual(r.totals, { projects: 5, credits: 175, works_shipped: 8, karma: 4 });
172
172
  pass++; console.log(' ok - totals aggregate un-fanned across the builder\'s projects');
173
- assert.deepEqual([...r.disciplines].sort(), ['artist', 'engineer', 'ui']);
173
+ assert.deepEqual([...r.disciplines].sort(), ['artisan', 'artist', 'ui']);
174
174
  pass++; console.log(' ok - disciplines are the deduped union');
175
175
  ok(new Date(r.last_active).toISOString() === '2026-03-01T00:00:00.000Z', 'last_active is the max');
176
176
  ok(r.projects.length === 1 && r.projects[0].name === 'Pub Proj', 'only the on-the-map Public project is named — unfeatured, and named anyway');
@@ -240,7 +240,7 @@ try {
240
240
  await pi.upsertActivity({
241
241
  clientId: PUB_CLIENT, githubId: LAZY, credits: 1,
242
242
  rank: 'Supreme Overlord',
243
- disciplines: ['engineer', 42, 'x'.repeat(33), 'a', 'b', 'c', 'd', 'e', 'f', 'g'],
243
+ disciplines: ['artisan', 42, 'x'.repeat(33), 'a', 'b', 'c', 'd', 'e', 'f', 'g'],
244
244
  }, { pool });
245
245
  const { rows: [act] } = await q(
246
246
  `SELECT rank_held, disciplines FROM platform_identity_builder_project_activity
@@ -249,6 +249,19 @@ try {
249
249
  ok(act.disciplines.length <= 8 && act.disciplines.every((d) => d.length <= 32),
250
250
  'disciplines land validated + capped');
251
251
 
252
+ // 6 — a reporting instance on a core older than the craft rename (task 1005141) still
253
+ // sends engineer / ideator. They land as today's crafts, folded and deduped, so the hub
254
+ // never shows an old name beside the new one (task 1005193).
255
+ await pi.upsertActivity({
256
+ clientId: PUB_CLIENT, githubId: LAZY, credits: 1,
257
+ disciplines: ['engineer', 'artist', 'ideator', 'ui', 'artisan'],
258
+ }, { pool });
259
+ const { rows: [folded] } = await q(
260
+ `SELECT disciplines FROM platform_identity_builder_project_activity
261
+ WHERE client_id = $1 AND github_id = $2`, [PUB_CLIENT, LAZY]);
262
+ assert.deepEqual(folded.disciplines, ['artisan', 'artist', 'ui']);
263
+ pass++; console.log(' ok - old craft names from an older core are folded to today\'s on ingest');
264
+
252
265
  console.log(`\nprofile_rollup_consent.mjs: ${pass} passed`);
253
266
  } finally {
254
267
  await cleanup();
@@ -69,7 +69,7 @@ const FULL_ROW = {
69
69
  deactivated_at: null,
70
70
  reactivated_at: null,
71
71
  deactivated_reason: SECRET_REASON,
72
- preferred_disciplines: ['engineer'],
72
+ preferred_disciplines: ['artisan'],
73
73
  system_role: 'superadmin',
74
74
  anthropic_email: SECRET_EMAIL,
75
75
  anthropic_user_id: SECRET_USER,
@@ -107,8 +107,8 @@ function stubPublicProfileExtras() {
107
107
  .map((r) => ({ name: r.name, origin: r.origin, rank_held: r.rank_held, recent_ships: [] }));
108
108
  return {
109
109
  totals: { projects: 2, credits: 150, works_shipped: 7, karma: 4 },
110
- disciplines: ['engineer', 'ui'],
111
- crafts: { artist: { pages_approved: 3, credits: 40 } },
110
+ disciplines: ['artisan', 'ui'],
111
+ crafts: { artist: { pages_approved: 3, filed: 2, full_ideas: null, ratified: null, credits: 40 } },
112
112
  last_active: '2026-08-01T00:00:00.000Z',
113
113
  projects: named,
114
114
  more_projects: CROSS_PROJECT_ROWS.length - named.length,
@@ -118,11 +118,11 @@ function stubPublicProfileExtras() {
118
118
  // crafts (task 1004440) is each craft's summed counts — always present, {} when
119
119
  // no project reported any — so it belongs in the exact list too.
120
120
  const CP_PUBLIC_KEYS = ['totals', 'disciplines', 'crafts', 'last_active', 'projects', 'more_projects'];
121
- // The only shape crafts may take: the three crafts, each a block of counts.
121
+ // The only shape crafts may take: the two crafts, each a block of counts. The
122
+ // artist block carries the pages AND the ideas (task 1005141 folded the ideator in).
122
123
  const CP_ROLE_FIELDS = {
123
- engineer: ['credits', 'shipped'],
124
- artist: ['credits', 'pages_approved'],
125
- ideator: ['credits', 'filed', 'full_ideas', 'ratified'],
124
+ artisan: ['credits', 'shipped'],
125
+ artist: ['credits', 'filed', 'full_ideas', 'pages_approved', 'ratified'],
126
126
  };
127
127
  // recent_ships rides the project row (task 1002803) and is ALWAYS present —
128
128
  // getRollupForBuilder defaults it to [] — so it belongs in the exact key list.
@@ -139,7 +139,7 @@ function assertCrossProjectContract(cp, text) {
139
139
  assert.deepEqual(Object.keys(cp).sort(), [...CP_PUBLIC_KEYS].sort(), 'cross_project keys are exactly the contract');
140
140
  assert.deepEqual(Object.keys(cp.totals).sort(), ['credits', 'karma', 'projects', 'works_shipped']);
141
141
  for (const [craft, block] of Object.entries(cp.crafts)) {
142
- assert.ok(CP_ROLE_FIELDS[craft], `crafts carries only the three crafts — found "${craft}"`);
142
+ assert.ok(CP_ROLE_FIELDS[craft], `crafts carries only the two crafts — found "${craft}"`);
143
143
  assert.deepEqual(Object.keys(block).sort(), CP_ROLE_FIELDS[craft], `${craft} carries only its named counts`);
144
144
  for (const v of Object.values(block)) assert.ok(v === null || Number.isInteger(v), 'a craft count is a number or null, never text');
145
145
  }
@@ -393,7 +393,7 @@ const SCOUTING_TABLE = [
393
393
  { profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'public', hide_stats: false,
394
394
  row: { github_id: '11', github_login: 'r06-visible', display_name: 'Visible', avatar_url: null,
395
395
  project_count: 2, total_credits: 5, total_tasks_shipped: 1, total_karma: 0,
396
- last_active: '2026-08-01T00:00:00.000Z', disciplines: ['engineer'] } },
396
+ last_active: '2026-08-01T00:00:00.000Z', disciplines: ['artisan'] } },
397
397
  { profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'public', hide_stats: true,
398
398
  row: { github_id: '12', github_login: HIDDEN_LOGIN, display_name: 'Hidden', avatar_url: null,
399
399
  project_count: 7, total_credits: 88817, total_tasks_shipped: 6613, total_karma: 5519,
@@ -405,7 +405,7 @@ const SCOUTING_TABLE = [
405
405
  { profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'private', hide_stats: false,
406
406
  row: { github_id: '14', github_login: PRIVATE_LOGIN, display_name: 'Private', avatar_url: null,
407
407
  project_count: 3, total_credits: 63127, total_tasks_shipped: 4409, total_karma: 2287,
408
- last_active: '2026-08-04T00:00:00.000Z', disciplines: ['engineer'] } },
408
+ last_active: '2026-08-04T00:00:00.000Z', disciplines: ['artisan'] } },
409
409
  ];
410
410
 
411
411
  const engine = {
@@ -458,7 +458,7 @@ const engine = {
458
458
  if (/AS works_shipped/.test(sql)) {
459
459
  return { rows: [{ projects: PROOF_PROJECTS.length, credits: sumOf('credits'),
460
460
  works_shipped: sumOf('tasks_shipped'), karma: sumOf('karma'),
461
- last_active: '2026-08-02T00:00:00.000Z', disciplines: ['engineer', 'ui'] }] };
461
+ last_active: '2026-08-02T00:00:00.000Z', disciplines: ['artisan', 'ui'] }] };
462
462
  }
463
463
  throw new Error(`fake engine: unexpected query ${sql.slice(0, 60)}`);
464
464
  },
@@ -496,7 +496,7 @@ await t('R06.1 a two-project builder\'s COMBINED record shows to another account
496
496
  assert.equal(cp.totals.credits, sumOf('credits'), 'credits are the sum across BOTH projects');
497
497
  assert.equal(cp.totals.works_shipped, sumOf('tasks_shipped'), 'ships are summed across both');
498
498
  assert.equal(cp.totals.karma, sumOf('karma'), 'karma is summed across both');
499
- assert.deepEqual(cp.disciplines, ['engineer', 'ui'], 'the disciplines union spans both projects');
499
+ assert.deepEqual(cp.disciplines, ['artisan', 'ui'], 'the disciplines union spans both projects');
500
500
  assert.equal(cp.projects.length, 1, 'only the Public project is NAMED — unfeatured, and named anyway');
501
501
  assert.equal(cp.projects[0].name, PROOF_PROJECTS[0].name);
502
502
  assert.equal(cp.more_projects, 1, 'the Private one is counted, never named — featured, and unnamed anyway');
@@ -37,12 +37,12 @@ const HTML = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/profile.htm
37
37
  const BUILDER = {
38
38
  id: 7, github_login: 'pheidias', display_name: 'Pheidias', avatar_url: null,
39
39
  rank: 'metic', total_credits: 120, karma: 3, created_at: '2026-01-01T00:00:00.000Z',
40
- preferred_disciplines: ['engineer'],
40
+ preferred_disciplines: ['artisan'],
41
41
  };
42
42
 
43
43
  const CP_FULL = {
44
44
  totals: { projects: 3, credits: 150, works_shipped: 7, karma: 4 },
45
- disciplines: ['engineer', 'ui'],
45
+ disciplines: ['artisan', 'ui'],
46
46
  last_active: '2026-08-01T00:00:00.000Z',
47
47
  projects: [
48
48
  { name: 'Public Proj', origin: 'https://pub.example', rank_held: 'metic' },
@@ -84,7 +84,7 @@ async function boot({ search = '', me, profile } = {}) {
84
84
  // the same reason categoryChip is: an undefined helper throws mid-render.
85
85
  roleAxisLabel: () => 'Role',
86
86
  roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
87
- roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
87
+ roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
88
88
  roleIsUnset: (d) => !d || d === 'unclassified',
89
89
  ROLE_UNSET: 'unclassified',
90
90
  $, escapeHtml,
@@ -172,7 +172,7 @@ test('full cross_project → tiles, named rows, plain-text standing, +N more', a
172
172
  assert.ok(!html.includes('rank-badge'), 'never the local rank-badge component for another project\'s rank (spec Decision 1)');
173
173
  assert.match(html, /\+ 1 more project/, 'non-public projects are counted, never named');
174
174
  // task 1003936: one word for the axis, and the values are named not slugged.
175
- assert.match(html, /Roles: Engineer, UI/);
175
+ assert.match(html, /Roles: Artisan, UI/);
176
176
  assert.match(html, /Last active 2026-08-01/);
177
177
  assert.ok(!html.includes('/settings#cross-project'), 'no self link for a stranger');
178
178
  });
@@ -34,8 +34,8 @@ function fakePool(rows) {
34
34
  }
35
35
 
36
36
  const BUILDERS = [
37
- { github_id: 1, github_login: 'ada', display_name: 'Ada L', avatar_url: 'a.png', disciplines: ['engineer'], project_count: 3, total_tasks_shipped: 40, total_credits: 900 },
38
- { github_id: 2, github_login: 'grace', display_name: 'Grace H', avatar_url: 'g.png', disciplines: ['engineer'], project_count: 2, total_tasks_shipped: 30, total_credits: 800 },
37
+ { github_id: 1, github_login: 'ada', display_name: 'Ada L', avatar_url: 'a.png', disciplines: ['artisan'], project_count: 3, total_tasks_shipped: 40, total_credits: 900 },
38
+ { github_id: 2, github_login: 'grace', display_name: 'Grace H', avatar_url: 'g.png', disciplines: ['artisan'], project_count: 2, total_tasks_shipped: 30, total_credits: 800 },
39
39
  { github_id: 3, github_login: 'linus', display_name: null, avatar_url: null, disciplines: null, project_count: 1, total_tasks_shipped: 10, total_credits: 100 },
40
40
  ];
41
41
 
@@ -230,7 +230,7 @@ await test('BEHAVIOUR: a failed create throws with the status, it does not retur
230
230
  // The hall promote once sent no discipline, and an idea whose wording names no
231
231
  // craft was refused "discipline was not supplied and could not be inferred".
232
232
  await test('BOTH PATHS: the hall promote payload always sends a discipline', () => {
233
- assert.match(HALL, /discipline: \(form\.discipline && form\.discipline\.value\) \|\| 'engineer'/,
233
+ assert.match(HALL, /discipline: \(form\.discipline && form\.discipline\.value\) \|\| 'artisan'/,
234
234
  'hall: discipline must reach the create payload, defaulting to engineer');
235
235
  assert.match(HALL, /<select name="discipline">/, 'hall: the promote form offers a Craft picker');
236
236
  });