@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
@@ -4,7 +4,11 @@
4
4
  // The cross-project rollup used to carry one ship count (with no status filter)
5
5
  // and "Roles" from the declared preference alone, so on the hub an artist with
6
6
  // forty approved pages, or an ideator the board ratified, read as an engineer
7
- // with nothing shipped. Three halves, each driven through its real code:
7
+ // with nothing shipped. Since task 1005141 (BONGOS-V3) there are two crafts:
8
+ // the engineer is the ARTISAN, and the ideator folded into the ARTIST, whose one
9
+ // block carries the page count AND the idea counts under one credit split. An
10
+ // instance on an older core still reports the old three; the hub folds them.
11
+ // Three halves, each driven through its real code:
8
12
  //
9
13
  // 1. THE SENDER (core auth-admission.js reportActivityRollup) counts SHIPPED
10
14
  // works only, sends each EARNED craft's counts (the ledger split decides,
@@ -13,7 +17,7 @@
13
17
  // the body strictly — re-sends once without `crafts` when an older hub
14
18
  // refuses the field, so the totals are never lost.
15
19
  // 2. THE HUB ROUTE + upsertActivity accept `crafts` and bind only the
16
- // sanitised shape: three crafts, named counts, integers or null. Nothing a
20
+ // sanitised shape: two crafts, named counts, integers or null. Nothing a
17
21
  // sender writes can carry text through it.
18
22
  // 3. getRollupForBuilder sums each craft over EVERY project row — named or
19
23
  // not, exactly as the totals are (the aggregate is not the leak, the name
@@ -66,12 +70,12 @@ const seams = require('../src/module-seams.js');
66
70
  // One rollup through the real sender. `byCraft` is economy's ledger split for
67
71
  // builder 7 (null = no economy module); `ports` the craft stats; `statuses` the
68
72
  // HTTP status each successive send answers.
69
- async function sendRollup({ byCraft = null, preferred = ['engineer'], ports = {}, statuses = [200] } = {}) {
73
+ async function sendRollup({ byCraft = null, preferred = ['artisan'], ports = {}, statuses = [200] } = {}) {
70
74
  seams._reset();
71
75
  if (byCraft) {
72
76
  seams.registerProvider('reward', {
73
77
  creditsByCraft: async (ids) => new Map(ids.map((id) => [String(id), byCraft])),
74
- craftForPreference: (d) => (d === 'ui' ? 'artist' : ['engineer', 'artist', 'ideator'].includes(d) ? d : null),
78
+ craftForPreference: (d) => (d === 'ui' ? 'artist' : ['artisan', 'artist'].includes(d) ? d : null),
75
79
  });
76
80
  }
77
81
  for (const [k, fn] of Object.entries(ports)) seams.registerProvider(k, fn);
@@ -110,52 +114,51 @@ await t('the ship count asks for SHIPPED works only (the missing status filter)'
110
114
  assert.match(q.text, /status = 'shipped'/, 'a count without the status filter also counts work handed back after shipping');
111
115
  });
112
116
 
113
- await t('an artist who is also an ideator sends both crafts\' counts, and only counts', async () => {
117
+ await t('an artist with pages AND ideas sends ONE artist block of both halves\' counts, and only counts', async () => {
114
118
  const { sent } = await sendRollup({
115
- byCraft: { engineer: 0, artist: 1100, ideator: 160, other: 0, total: 1260 },
119
+ byCraft: { artisan: 0, artist: 1260, other: 0, total: 1260 },
116
120
  preferred: ['ui'],
117
121
  ports: { 'copy-desk.artistStats': ARTIST_STATS, 'ideas.ideatorStats': IDEATOR_STATS },
118
122
  });
119
123
  assert.equal(sent.length, 1);
120
124
  const body = sent[0];
121
125
  assert.deepEqual(body.crafts, {
122
- artist: { pages_approved: 42, credits: 1100 },
123
- ideator: { filed: 12, full_ideas: 2, ratified: 1, credits: 160 },
124
- }, 'exactly the named counts — no surface line, no size, no wording');
126
+ artist: { pages_approved: 42, filed: 12, full_ideas: 2, ratified: 1, credits: 1260 },
127
+ }, 'exactly the named counts — no surface line, no size, no wording; credits are the craft ledger split');
125
128
  assert.ok(!JSON.stringify(body).includes('secret wording'), 'no page wording leaves the instance');
126
- assert.deepEqual(body.disciplines, ['artist', 'ideator'], 'crafts from evidence of work, in preferred order (ui → artist)');
129
+ assert.deepEqual(body.disciplines, ['artist'], 'crafts from evidence of work, once each (ui → artist)');
127
130
  assert.equal(body.tasks_shipped, 5, 'CONTROL: the totals still ride');
128
131
  });
129
132
 
130
133
  await t('an unearned craft sends nothing, and its stats port is never asked', async () => {
131
134
  let asked = 0;
132
135
  const { sent } = await sendRollup({
133
- byCraft: { engineer: 900, artist: 0, ideator: 0, other: 0, total: 900 },
134
- preferred: ['artist', 'engineer'],
136
+ byCraft: { artisan: 900, artist: 0, other: 0, total: 900 },
137
+ preferred: ['artist', 'artisan'],
135
138
  ports: { 'copy-desk.artistStats': async () => { asked++; return {}; } },
136
139
  });
137
- assert.deepEqual(sent[0].crafts, { engineer: { shipped: 5, credits: 900 } });
138
- assert.deepEqual(sent[0].disciplines, ['engineer'], 'a declared but unearned role is not shown once work exists elsewhere');
140
+ assert.deepEqual(sent[0].crafts, { artisan: { shipped: 5, credits: 900 } });
141
+ assert.deepEqual(sent[0].disciplines, ['artisan'], 'a declared but unearned role is not shown once work exists elsewhere');
139
142
  assert.equal(asked, 0, 'a craft that will not be sent is not read');
140
143
  });
141
144
 
142
145
  await t('nothing earned, or no economy to ask → the declared preference, and no crafts field', async () => {
143
- const none = await sendRollup({ byCraft: { engineer: 0, artist: 0, ideator: 0, other: 0, total: 0 }, preferred: ['ideator'] });
144
- assert.deepEqual(none.sent[0].disciplines, ['ideator'], 'the fallback is the preference');
146
+ const none = await sendRollup({ byCraft: { artisan: 0, artist: 0, other: 0, total: 0 }, preferred: ['artist'] });
147
+ assert.deepEqual(none.sent[0].disciplines, ['artist'], 'the fallback is the preference');
145
148
  assert.ok(!('crafts' in none.sent[0]), 'no crafts field, so an older hub never sees one');
146
- const vanilla = await sendRollup({ byCraft: null, preferred: ['engineer', 'ui'] });
147
- assert.deepEqual(vanilla.sent[0].disciplines, ['engineer', 'ui']);
149
+ const vanilla = await sendRollup({ byCraft: null, preferred: ['artisan', 'ui'] });
150
+ assert.deepEqual(vanilla.sent[0].disciplines, ['artisan', 'ui']);
148
151
  assert.ok(!('crafts' in vanilla.sent[0]));
149
152
  });
150
153
 
151
154
  await t('a craft whose port cannot be asked sends null counts, never 0', async () => {
152
- const { sent } = await sendRollup({ byCraft: { engineer: 0, artist: 50, ideator: 0, other: 0, total: 50 }, preferred: [] });
153
- assert.deepEqual(sent[0].crafts, { artist: { pages_approved: null, credits: 50 } });
155
+ const { sent } = await sendRollup({ byCraft: { artisan: 0, artist: 50, other: 0, total: 50 }, preferred: [] });
156
+ assert.deepEqual(sent[0].crafts, { artist: { pages_approved: null, filed: null, full_ideas: null, ratified: null, credits: 50 } });
154
157
  });
155
158
 
156
159
  await t('an older hub refusing `crafts` (400) gets the report again without it — once', async () => {
157
160
  const { sent } = await sendRollup({
158
- byCraft: { engineer: 0, artist: 1100, ideator: 0, other: 0, total: 1100 },
161
+ byCraft: { artisan: 0, artist: 1100, other: 0, total: 1100 },
159
162
  ports: { 'copy-desk.artistStats': ARTIST_STATS },
160
163
  statuses: [400, 200],
161
164
  });
@@ -195,6 +198,7 @@ function hubRoute() {
195
198
  resolveSession: async () => null,
196
199
  logger: () => ({ info() {}, warn() {}, error() {} }),
197
200
  LIVE_RANK_LADDER: realApi.LIVE_RANK_LADDER,
201
+ crafts: realApi.crafts,
198
202
  withTx: realApi.withTx,
199
203
  corsPublicGet: () => (_q, _s, next) => next(),
200
204
  on: () => () => {},
@@ -225,26 +229,60 @@ await t('a report carrying crafts is accepted and binds the craft counts', async
225
229
  assert.equal(res.failure, null, JSON.stringify(res.failure));
226
230
  assert.match(insert.text, /reported_as_of, crafts\)/, 'the upsert writes the crafts column');
227
231
  assert.match(insert.text, /crafts = EXCLUDED\.crafts/, 'and a later report replaces it');
228
- assert.deepEqual(JSON.parse(insert.params[9]), { artist: { pages_approved: 42, credits: 1100 } });
232
+ assert.deepEqual(JSON.parse(insert.params[9]), {
233
+ artist: { pages_approved: 42, filed: null, full_ideas: null, ratified: null, credits: 1100 },
234
+ }, 'a count the report did not carry is bound as null, never 0');
229
235
  });
230
236
 
231
- await t('the hub keeps only the three crafts and their named integer counts', async () => {
237
+ await t('a report from an older core (engineer / artist / ideator) is folded into today\'s two crafts', async () => {
238
+ const { res, insert } = await postRollup({
239
+ crafts: {
240
+ engineer: { shipped: 3, credits: 9 },
241
+ artist: { pages_approved: 42, credits: 1100 },
242
+ ideator: { filed: 12, full_ideas: 2, ratified: 1, credits: 160 },
243
+ },
244
+ });
245
+ assert.equal(res.failure, null, JSON.stringify(res.failure));
246
+ assert.deepEqual(JSON.parse(insert.params[9]), {
247
+ artisan: { shipped: 3, credits: 9 },
248
+ artist: { pages_approved: 42, filed: 12, full_ideas: 2, ratified: 1, credits: 1260 },
249
+ }, 'engineer → artisan; the ideator counts join the artist block, credits summed');
250
+ });
251
+
252
+ await t('the hub keeps only the two crafts and their named integer counts', async () => {
232
253
  const { insert } = await postRollup({
233
254
  crafts: {
234
- artist: { pages_approved: 'forty', credits: -3, title: '<script>' },
235
- ideator: { filed: 1.5, full_ideas: 99_000_000, ratified: 2, credits: 7, idea_title: 'A secret plan' },
255
+ artist: { pages_approved: 'forty', filed: 1.5, full_ideas: 99_000_000, ratified: 2, credits: -3, title: '<script>', idea_title: 'A secret plan' },
236
256
  governor: { laws: 4 },
237
- engineer: 'lots',
257
+ artisan: 'lots',
238
258
  },
239
259
  });
240
260
  const bound = JSON.parse(insert.params[9]);
241
261
  assert.deepEqual(bound, {
242
- artist: { pages_approved: null, credits: null },
243
- ideator: { filed: null, full_ideas: null, ratified: 2, credits: 7 },
262
+ artist: { pages_approved: null, filed: null, full_ideas: null, ratified: 2, credits: null },
244
263
  });
245
264
  assert.ok(!insert.params[9].includes('secret') && !insert.params[9].includes('script'), 'no text survives');
246
265
  });
247
266
 
267
+ await t('an old-shape report is capped half by half: a hostile count on one side cannot shave the other', async () => {
268
+ const { insert } = await postRollup({
269
+ crafts: {
270
+ artist: { pages_approved: 'forty', credits: -3, title: '<script>' },
271
+ ideator: { filed: 1.5, full_ideas: 99_000_000, ratified: 2, credits: 7, idea_title: 'A secret plan' },
272
+ },
273
+ });
274
+ assert.deepEqual(JSON.parse(insert.params[9]), {
275
+ artist: { pages_approved: null, filed: null, full_ideas: null, ratified: 2, credits: 7 },
276
+ }, 'the -3 is dropped before the sum, never subtracted from the 7');
277
+ assert.ok(!insert.params[9].includes('secret') && !insert.params[9].includes('script'), 'no text survives the fold');
278
+ });
279
+
280
+ await t('an older core\'s disciplines (engineer, ideator) are read as today\'s crafts, once each', async () => {
281
+ const { res, insert } = await postRollup({ disciplines: ['engineer', 'ideator', 'artist', 'ui'] });
282
+ assert.equal(res.failure, null, JSON.stringify(res.failure));
283
+ assert.deepEqual(insert.params[6], ['artisan', 'artist', 'ui'], 'engineer → artisan; ideator and artist are one craft; ui stays');
284
+ });
285
+
248
286
  await t('a report with no crafts (an older instance) binds the empty object', async () => {
249
287
  const { res, insert } = await postRollup({});
250
288
  assert.deepEqual(res.body, { ok: true });
@@ -271,6 +309,7 @@ await t('getRollupForBuilder sums each craft over EVERY project row, named or no
271
309
  return { rows: [
272
310
  { name: 'Pub', origin: 'https://pub.example', rank_held: 'metic', recent_ships: [], is_public: true,
273
311
  crafts: { artist: { pages_approved: 40, credits: 1000 } } },
312
+ // A row stored by an older core, before the hub migration ran: folded on read.
274
313
  { name: 'Hidden', origin: 'https://priv.example', rank_held: 'xenos', recent_ships: [], is_public: false,
275
314
  crafts: { artist: { pages_approved: 2, credits: null }, ideator: { filed: 5, full_ideas: 1, ratified: null, credits: 60 } } },
276
315
  { name: 'Old', origin: 'https://old.example', rank_held: null, recent_ships: [], is_public: true, crafts: {} },
@@ -281,9 +320,8 @@ await t('getRollupForBuilder sums each craft over EVERY project row, named or no
281
320
  const r = await pi.getRollupForBuilder(42, { pool });
282
321
  assert.match(pool.find(/recent_ships/).text, /a\.crafts/, 'the per-project read carries crafts');
283
322
  assert.deepEqual(r.crafts, {
284
- artist: { pages_approved: 42, credits: 1000 },
285
- ideator: { filed: 5, full_ideas: 1, ratified: null, credits: 60 },
286
- }, 'a private project is counted, as its totals are; a count nobody reported stays null');
323
+ artist: { pages_approved: 42, filed: 5, full_ideas: 1, ratified: null, credits: 1060 },
324
+ }, 'a private project is counted, as its totals are; an old-shape row folds in; a count nobody reported stays null');
287
325
  assert.deepEqual(r.projects.map((p) => p.name), ['Pub', 'Old'], 'CONTROL: the private project is still not NAMED');
288
326
  assert.ok(!JSON.stringify(r.projects).includes('pages_approved'), 'per-project numbers never cross, only the sum');
289
327
  });
@@ -323,8 +361,8 @@ const tileOf = (html, label) => {
323
361
  return m ? m[1] : null;
324
362
  };
325
363
 
326
- await t('an artist\'s and an ideator\'s tiles show their own craft\'s numbers', async () => {
327
- const html = await renderXp(CP({ artist: { pages_approved: 42, credits: 1100 }, ideator: { filed: 12, full_ideas: 2, ratified: 1, credits: 160 } }));
364
+ await t('the artist\'s tiles show both halves of the craft: pages, thoughts and Full Ideas', async () => {
365
+ const html = await renderXp(CP({ artist: { pages_approved: 42, filed: 12, full_ideas: 2, ratified: 1, credits: 1260 } }));
328
366
  assert.equal(tileOf(html, 'Pages approved'), '42');
329
367
  assert.equal(tileOf(html, 'Thoughts filed'), '12');
330
368
  assert.equal(tileOf(html, 'Full Ideas'), '2');
@@ -334,12 +372,13 @@ await t('an artist\'s and an ideator\'s tiles show their own craft\'s numbers',
334
372
  });
335
373
 
336
374
  await t('a count no project could report is a dash, never 0', async () => {
337
- const html = await renderXp(CP({ artist: { pages_approved: null, credits: 50 } }));
375
+ const html = await renderXp(CP({ artist: { pages_approved: null, filed: null, full_ideas: null, ratified: null, credits: 50 } }));
338
376
  assert.equal(tileOf(html, 'Pages approved'), '—');
377
+ assert.equal(tileOf(html, 'Thoughts filed'), '—');
339
378
  });
340
379
 
341
- await t('no crafts (an engineer, or an older hub) → no craft band at all', async () => {
342
- for (const crafts of [undefined, {}, { engineer: { shipped: 3, credits: 9 } }]) {
380
+ await t('no crafts (an artisan, or an older hub) → no craft band at all', async () => {
381
+ for (const crafts of [undefined, {}, { artisan: { shipped: 3, credits: 9 } }]) {
343
382
  const html = await renderXp(CP(crafts));
344
383
  assert.ok(!html.includes('stat-row--craft'), `no craft band for ${JSON.stringify(crafts)}`);
345
384
  assert.equal((html.match(/stat-tile__num/g) || []).length, 4);
@@ -184,6 +184,7 @@ await t('the route passes recent_ships through and takes the origin from the CLI
184
184
  resolveSession: async () => null,
185
185
  logger: () => ({ info() {}, warn() {}, error() {} }),
186
186
  LIVE_RANK_LADDER: realApi.LIVE_RANK_LADDER,
187
+ crafts: realApi.crafts, // the rollup normalizes reported disciplines (task 1005141)
187
188
  corsPublicGet: () => (_q, _s, next) => next(),
188
189
  on: () => () => {},
189
190
  hasProvider: () => false,
@@ -201,7 +202,7 @@ await t('the route passes recent_ships through and takes the origin from the CLI
201
202
  const req = {
202
203
  body: {
203
204
  client_id: 'c1', client_secret: SECRET, github_id: 42,
204
- credits: 10, tasks_shipped: 2, karma: 1, rank: 'metic', disciplines: ['engineer'],
205
+ credits: 10, tasks_shipped: 2, karma: 1, rank: 'metic', disciplines: ['artisan'],
205
206
  // Five, so the HUB's cap is what decides — a sender that ignores its own
206
207
  // cap must not widen the section.
207
208
  recent_ships: [1, 2, 3, 4, 5].map((n) => ship({ id: n, title: `work ${n}`, url: `${ORIGIN}/builders#/task/${n}` })),
@@ -222,7 +223,7 @@ console.log('\n4. a work title surfaces only where the project name may:');
222
223
  await t('getRollupForBuilder attaches recent_ships to publicly-named projects only', async () => {
223
224
  const pool = mockPool((sql) => {
224
225
  if (/WITH agg AS/.test(sql)) {
225
- return { rows: [{ projects: 2, credits: 5, works_shipped: 2, karma: 0, last_active: null, disciplines: ['engineer'] }] };
226
+ return { rows: [{ projects: 2, credits: 5, works_shipped: 2, karma: 0, last_active: null, disciplines: ['artisan'] }] };
226
227
  }
227
228
  return {
228
229
  rows: [
@@ -51,7 +51,7 @@ test('a Full Idea matches, and files for the ENGINEER', () => {
51
51
  const { matched, errors } = C.cascadesFor(idea(), C.IDEA_CASCADES, {}, C.TRIGGERS.idea);
52
52
  assert.deepEqual(errors, []);
53
53
  assert.equal(matched.length, 1);
54
- assert.equal(matched[0].rule.discipline, 'engineer');
54
+ assert.equal(matched[0].rule.discipline, 'artisan');
55
55
  assert.match(matched[0].spec.title, /Scope the Full Idea: Price swaps from mechanism/);
56
56
  });
57
57
 
@@ -138,7 +138,7 @@ test('the runner files the task from the event payload alone', async () => {
138
138
  assert.deepEqual(report.errors, []);
139
139
  assert.equal(report.created.length, 1);
140
140
  assert.equal(d.created.length, 1);
141
- assert.equal(d.created[0].discipline, 'engineer');
141
+ assert.equal(d.created[0].discipline, 'artisan');
142
142
  });
143
143
 
144
144
  test('an already-filed cascade is SKIPPED, not filed twice', async () => {
@@ -147,7 +147,7 @@ async function fixture(client, { completeness = 70, developed = false, taskCount
147
147
  const task = await insertRow(client, 'tasks', {
148
148
  // discipline is named for the same reason as the version status above:
149
149
  // tasks_discipline_check admits only the live crafts.
150
- version_id: version.id, title: `ic6 task ${i + 1}`, status: 'shipped', discipline: 'engineer',
150
+ version_id: version.id, title: `ic6 task ${i + 1}`, status: 'shipped', discipline: 'artisan',
151
151
  });
152
152
  await insertRow(client, 'ideas_task_lineage', {
153
153
  task_id: task.id, idea_id: idea.id, origin: 'promote',
@@ -95,7 +95,7 @@ function render(idea, { template = TEMPLATE, templateFails = false, ideaStatus =
95
95
  // the same reason categoryChip is: an undefined helper throws mid-render.
96
96
  roleAxisLabel: () => 'Role',
97
97
  roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
98
- roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
98
+ roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
99
99
  roleIsUnset: (d) => !d || d === 'unclassified',
100
100
  ROLE_UNSET: 'unclassified',
101
101
  escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])),
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // WHERE THAT PLAYBOOK LIVES (task 1002990). It used to be the /ideate skill. The
7
7
  // kernel/pack split moved it into the Ideator role pack and deleted the skill, so
8
- // this suite now reads docs/packs/ideator.md. Every contract below is unchanged —
8
+ // this suite now reads docs/packs/artist.md. Every contract below is unchanged —
9
9
  // the file moved, the promises did not — and pointing the suite at the new home is
10
10
  // what stops the split from silently dropping the guard with the skill.
11
11
  //
@@ -37,7 +37,10 @@ import { namesFieldKey } from './helpers.mjs';
37
37
 
38
38
  const require = createRequire(import.meta.url);
39
39
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
40
- const SKILL = fs.readFileSync(path.join(ROOT, 'docs/packs/ideator.md'), 'utf8');
40
+ // The ideas half's detail moved to docs/recipes/ideation-playbook.md when the ideator
41
+ // folded into the artist (task 1005141): the pack and its playbook are read as one text.
42
+ const SKILL = fs.readFileSync(path.join(ROOT, 'docs/packs/artist.md'), 'utf8')
43
+ + '\n' + fs.readFileSync(path.join(ROOT, 'docs/recipes/ideation-playbook.md'), 'utf8');
41
44
  // Prose assertions run against a WHITESPACE-NORMALISED copy. Markdown is hard-
42
45
  // wrapped, so any phrase long enough to be worth asserting will eventually
43
46
  // straddle a newline — and a regex that does not allow for that fails on a
@@ -195,7 +195,7 @@ function boot({
195
195
  // the same reason categoryChip is: an undefined helper throws mid-render.
196
196
  roleAxisLabel: () => 'Role',
197
197
  roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
198
- roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
198
+ roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
199
199
  roleIsUnset: (d) => !d || d === 'unclassified',
200
200
  ROLE_UNSET: 'unclassified',
201
201
  escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])),
package/tests/init.mjs CHANGED
@@ -134,7 +134,7 @@ test('kickoffTasks: includes the load-bearing owner-manual steps', () => {
134
134
 
135
135
  // Every kickoff task must satisfy the DB CHECK constraints (tasks_kind_check /
136
136
  // tasks_discipline_check) or the insert is rejected — which is how the branding
137
- // step vanished on 1.18.18: kind:'ideator' (a discipline, not a kind) violated
137
+ // step vanished on 1.18.18: kind:'artist' (a discipline, not a kind) violated
138
138
  // tasks_kind_check and postTask absorbed the failure. Pin the vocabulary at the
139
139
  // source so this class of bug can't reach a user again.
140
140
  test('kickoffTasks: every kind/discipline satisfies the DB CHECK vocabulary', () => {
@@ -156,7 +156,7 @@ test('kickoffTasks: the branding step is a decision BY an ideator (kind vs disci
156
156
  const look = init.kickoffTasks(fullSpec()).find((t) => /instance look/i.test(t.title));
157
157
  assert.ok(look, 'the branding kickoff step exists');
158
158
  assert.equal(look.kind, 'decision');
159
- assert.equal(look.discipline, 'ideator');
159
+ assert.equal(look.discipline, 'artist');
160
160
  });
161
161
 
162
162
  // ---- validateSpec ----
@@ -695,7 +695,7 @@ async function runSeedTests() {
695
695
  const look = findTaskPost(api, /instance look/i);
696
696
  assert.ok(look, 'the branding kickoff task was POSTed');
697
697
  assert.equal(look.body.kind, 'decision');
698
- assert.equal(look.body.discipline, 'ideator');
698
+ assert.equal(look.body.discipline, 'artist');
699
699
  });
700
700
 
701
701
  await at('seedViaApi: a rejected task POST is LOUD — failures returned, counts reflect actual vs expected', async () => {
@@ -31,11 +31,11 @@ const BRAND_VANILLA = {
31
31
  };
32
32
  const MODULES_OTB = {
33
33
  enabled: { game: true, 'art-pipeline': true, discord: true },
34
- disciplines: ['engineer', 'artist'],
34
+ disciplines: ['artisan', 'artist'],
35
35
  };
36
36
  const MODULES_VANILLA = {
37
37
  enabled: { game: false, 'art-pipeline': false, discord: false },
38
- disciplines: ['engineer'],
38
+ disciplines: ['artisan'],
39
39
  };
40
40
  const REPO = { owner: 'example-owner', repo: 'example', branch: 'main', blob_base: 'x', tree_base: 'y' };
41
41
 
@@ -96,7 +96,7 @@ test('the look step is a decision BY an ideator in every variant', () => {
96
96
  const look = kickoffChecklist(spec(), opts).find((t) => /instance look/i.test(t.title));
97
97
  assert.ok(look, `look step present for ${JSON.stringify(opts)}`);
98
98
  assert.equal(look.kind, 'decision');
99
- assert.equal(look.discipline, 'ideator');
99
+ assert.equal(look.discipline, 'artist');
100
100
  }
101
101
  });
102
102
 
@@ -12,9 +12,10 @@
12
12
  // credits (the figure a role ranks by is in the row, ADR 0255), and a roles
13
13
  // failure costs the roles, never the board.
14
14
  // 3. THE ROSTER. The REAL roster.js over the harness's public fixture: a Roles
15
- // column, the Everyone / Engineers / Artists / Ideators toggle ranking by
16
- // the credits that craft earned, and a dash, not a 0, for a builder with no
17
- // AI sessions.
15
+ // column, the Everyone / Artisans / Artists toggle ranking by the credits
16
+ // that craft earned, and a dash, not a 0, for a builder with no AI sessions.
17
+ // (Two crafts since task 1005141: the engineer is the artisan, and the
18
+ // ideator folded into the artist, so idea credits rank as artist credits.)
18
19
  // 4. THE STANDING CARD and 5. THE STATUS PAGE: the real functions, run.
19
20
 
20
21
  import { strict as assert } from 'node:assert';
@@ -49,26 +50,27 @@ await test('a page of builders: chosen, earned and preferred main roles, and a c
49
50
  const calls = [];
50
51
  seams._reset();
51
52
  seams.registerProvider('reward', rewardOver([
52
- { builder_id: '1', discipline: 'engineer', amount: 900 },
53
- { builder_id: '1', discipline: 'ideator', amount: 20 },
54
- { builder_id: '2', discipline: 'artist', amount: 370 },
55
- { builder_id: '2', discipline: 'ideator', amount: 42 },
56
- { builder_id: '3', discipline: 'ideator', amount: 186 },
53
+ { builder_id: '1', discipline: 'artisan', amount: 900 },
54
+ { builder_id: '1', discipline: 'artist', amount: 20 },
55
+ // pages (370) and ideas (42): one craft, so the grouped statement sends one row
56
+ { builder_id: '2', discipline: 'artist', amount: 412 },
57
+ { builder_id: '3', discipline: 'artist', amount: 186 },
57
58
  ], calls));
58
59
  const choices = new Map([
59
- ['2', { preferred_disciplines: ['artist', 'ideator'], main_discipline: 'artist' }],
60
- ['3', { preferred_disciplines: ['ideator', 'artist'], main_discipline: null }],
60
+ ['2', { preferred_disciplines: ['artist'], main_discipline: 'artist' }],
61
+ ['3', { preferred_disciplines: ['artisan', 'artist'], main_discipline: null }],
61
62
  ['4', { preferred_disciplines: ['ui'], main_discipline: null }],
62
63
  ]);
63
64
  const out = await readRolesForBuilders(['1', '2', 3, '4', 'x', '2'], { getRoleChoices: async (ids) => { calls.push(['choices', ids]); return choices; } });
64
65
  assert.deepEqual([...out.keys()], ['1', '2', '3', '4'], 'every valid id once, a bad one dropped');
65
66
  const r = (id) => JSON.parse(JSON.stringify(out.get(id)));
66
- assert.deepEqual([r('1').main, r('1').main_basis, r('1').shown], ['engineer', 'earned', ['engineer', 'ideator']]);
67
- assert.deepEqual([r('2').main, r('2').main_basis, r('2').shown], ['artist', 'chosen', ['artist', 'ideator']]);
68
- assert.deepEqual(r('3').shown, ['ideator'], 'a preferred craft that earned nothing does not show');
67
+ assert.deepEqual([r('1').main, r('1').main_basis, r('1').shown], ['artisan', 'earned', ['artisan', 'artist']]);
68
+ assert.deepEqual([r('2').main, r('2').main_basis, r('2').shown], ['artist', 'chosen', ['artist']], 'one artist, never two');
69
+ assert.deepEqual([r('3').main, r('3').main_basis, r('3').shown], ['artist', 'earned', ['artist']], 'a preferred craft that earned nothing does not show');
69
70
  assert.deepEqual([r('4').main, r('4').main_basis, r('4').shown], ['artist', 'preferred', ['artist']], 'ui is the artist\'s craft, as on the profile');
70
- assert.equal(r('2').credits_by_craft.artist, 370);
71
+ assert.equal(r('2').credits_by_craft.artist, 412, 'pages and ideas, one craft');
71
72
  assert.equal(r('2').credits_by_craft.total, 412);
73
+ assert.equal(r('2').credits_by_craft.ideator, undefined, 'no ideator column any more');
72
74
  assert.deepEqual(calls.map((c) => c[0]).sort(), ['choices', 'credits'], 'two reads for the whole page, never one per row');
73
75
  seams._reset();
74
76
  });
@@ -90,7 +92,7 @@ async function bootPublic({ rolesThrow = false } = {}) {
90
92
  { id: '2001', github_login: 'art', total_credits: 370, tokens_used: '0' },
91
93
  ],
92
94
  creditsByCraft: async (ids) => creditsByCraftLib.foldCraftRows(ids, [
93
- { builder_id: '1', discipline: 'engineer', amount: 900 },
95
+ { builder_id: '1', discipline: 'artisan', amount: 900 },
94
96
  { builder_id: '2001', discipline: 'artist', amount: 370 },
95
97
  ]),
96
98
  });
@@ -117,7 +119,7 @@ await test('GET /public/leaderboard: every row names its roles and the credits e
117
119
  const { status, body } = await s.get('/public/leaderboard');
118
120
  assert.equal(status, 200);
119
121
  const [eng, art] = body.leaderboard;
120
- assert.deepEqual(eng.roles.shown, ['engineer']);
122
+ assert.deepEqual(eng.roles.shown, ['artisan']);
121
123
  assert.deepEqual(art.roles.shown, ['artist']);
122
124
  assert.equal(art.roles.main_basis, 'chosen');
123
125
  assert.equal(art.roles.credits_by_craft.artist, 370, 'the figure an Artists toggle ranks by is in the row');
@@ -190,8 +192,11 @@ await test('the roster reads the public list and draws a Roles column, main role
190
192
  assert.deepEqual(labels, ['Place', 'Builder', 'Roles', 'Rank', 'Credits', 'Karma', 'Tokens']);
191
193
  const art = r.rows.find((b) => String(b.id) === '2001');
192
194
  const html = r.opts.rowHtml(art);
193
- assert.match(html, /<span class="lb-role lb-role--lead">Artist<\/span><span class="lb-role">Ideator<\/span>/);
194
- assert.match(r.els['roster-roles'].innerHTML, /Everyone[\s\S]*Engineers[\s\S]*Artists[\s\S]*Ideators/);
195
+ assert.match(html, /<td class="ledger__roles"><span class="lb-roles"><span class="lb-role lb-role--lead">Artist<\/span><\/span><\/td>/, 'one Artist chip, and no Ideator beside it');
196
+ const mixed = r.rows.find((b) => String(b.id) === '3');
197
+ assert.match(r.opts.rowHtml(mixed), /<span class="lb-role lb-role--lead">Artist<\/span><span class="lb-role">Artisan<\/span>/, 'main role first');
198
+ assert.match(r.els['roster-roles'].innerHTML, /Everyone[\s\S]*Artisans[\s\S]*Artists/);
199
+ assert.doesNotMatch(r.els['roster-roles'].innerHTML, /Ideators|Engineers/);
195
200
  });
196
201
 
197
202
  await test('a builder with no AI sessions reads a dash and a sentence, never a 0 of tokens', async () => {
@@ -216,11 +221,15 @@ await test('the Artists toggle shows only artists, ranked by the credits that cr
216
221
  assert.deepEqual(ids.sort(), artists.map((b) => String(b.id)).sort(), 'exactly the builders shown as artists');
217
222
  const credits = r.rows.map((b) => b.roles.credits_by_craft.artist);
218
223
  assert.deepEqual(credits, [...credits].sort((a, b) => b - a), 'ranked by artist credits, not total credits');
219
- assert.equal(String(r.rows[0].id), '2001', 'the browser-only artist leads the artists');
224
+ // The top artisan's idea credits (1,159) are artist credits now, so they lead
225
+ // the artists; the browser-only artist is next, by her own 412, never a 0.
226
+ assert.deepEqual(r.rows.slice(0, 2).map((b) => String(b.id)), ['1', '2001'], 'ranked by artist credits, idea credits included');
220
227
  const labels = r.opts.columns.map((c) => (typeof c === 'string' ? c : c.label));
221
228
  assert.equal(labels[4], 'Credits as artist', 'the header names the craft');
222
229
  assert.match(r.opts.rowHtml(r.rows[0]), /ledger__place">1</, 'the place is the place among artists');
223
- assert.match(r.opts.rowHtml(r.rows[0]), /<td class="ledger__num">370<\/td>/, 'and the figure is the artist credits');
230
+ assert.match(r.opts.rowHtml(r.rows[0]), /<td class="ledger__num">1159<\/td>/, 'and the figure is the artist credits, not the 38,644 total');
231
+ assert.match(r.opts.rowHtml(r.rows[1]), /ledger__place">2</);
232
+ assert.match(r.opts.rowHtml(r.rows[1]), /<td class="ledger__num">412<\/td>/, 'the browser-only artist reads her artist credits');
224
233
  r.click('');
225
234
  assert.equal(r.rows.length, FIX.leaderboard.length, 'Everyone brings every builder back');
226
235
  });
@@ -242,9 +251,9 @@ await test('the standing card adds the place inside the builder\'s main role, on
242
251
  vm.runInContext(`${code}; lbState.rows = rows; this.place = leaderboardPlace; this.text = rolePlaceText;`, sandbox);
243
252
  const art = sandbox.place('builder-7');
244
253
  assert.equal(art.role.craft, 'artist');
245
- assert.equal(sandbox.text(art), `#1 as an artist`);
254
+ assert.equal(sandbox.text(art), `#2 as an artist`, 'fourth overall, second among artists: something new, so it is said');
246
255
  const top = sandbox.place(FIX.leaderboard[0].github_login);
247
- assert.equal(sandbox.text(top), '', 'the top engineer is #1 either way, so nothing is added');
256
+ assert.equal(sandbox.text(top), '', 'the top artisan is #1 either way, so nothing is added');
248
257
  // task 1005054: the stats strip (and its rank tile) left Home; the Standing chip's card carries the role place
249
258
  assert.match(src, /\$\{rolePlaceText\(rollPlace\) \? `<span title="Your place among the/);
250
259
  assert.match(read('modules/hall-ui/public/builders.js'), /getJSON\(`\$\{API\}\/public\/leaderboard`\)/, 'the hall reads the list with the roles');
@@ -264,11 +273,11 @@ await test('the status leaderboard names each builder\'s roles, main one first,
264
273
  const sandbox = { document: { createElement: node } };
265
274
  vm.createContext(sandbox);
266
275
  vm.runInContext(`${code}; this.renderRoles = renderRoles;`, sandbox);
267
- const row = sandbox.renderRoles({ shown: ['artist', 'ideator', 'wizard'] });
276
+ const row = sandbox.renderRoles({ shown: ['artist', 'artisan', 'ideator', 'wizard'] });
268
277
  assert.deepEqual(row.children.map((c) => [c.className, c.textContent]), [
269
278
  ['leader__role leader__role--lead', 'Artist'],
270
- ['leader__role', 'Ideator'],
271
- ], 'a word the page does not know is dropped, not printed');
279
+ ['leader__role', 'Artisan'],
280
+ ], 'a word the page does not know (the retired ideator included) is dropped, not printed');
272
281
  assert.equal(sandbox.renderRoles(null), null, 'a row without roles draws nothing');
273
282
  assert.match(src, /const roles = renderRoles\(b\.roles\);\s*if \(roles\) name\.appendChild\(roles\);/);
274
283
  });
@@ -0,0 +1,55 @@
1
+ // tests/lifecycle_claim_ladder_refs.mjs — the claim refusal ladder in
2
+ // modules/lifecycle/CLAUDE.md points at code by NAME, never by line number.
3
+ //
4
+ // task 1005183: the ladder cited `claims.js:138`, `:176`, `db-claims.js:414` and a dozen
5
+ // more bare `:NNN` anchors; every one had drifted (gateOneActiveClaimPerSession was at
6
+ // :208, claimTasksBatch at :541). A line number rots on the next edit above it and then
7
+ // sends a reader to the wrong code with full confidence. A function name or a refusal
8
+ // code is greppable and fails loudly here when it is renamed away.
9
+ import { test } from 'node:test';
10
+ import assert from 'node:assert/strict';
11
+ import { readFileSync } from 'node:fs';
12
+ import { createRequire } from 'node:module';
13
+
14
+ const require = createRequire(import.meta.url);
15
+
16
+ const doc = readFileSync(new URL('../modules/lifecycle/CLAUDE.md', import.meta.url), 'utf8');
17
+ const dbClaims = readFileSync(new URL('../modules/lifecycle/db-claims.js', import.meta.url), 'utf8');
18
+ const routes = readFileSync(new URL('../modules/lifecycle/routes/claims.js', import.meta.url), 'utf8');
19
+
20
+ function ladderSection() {
21
+ const start = doc.indexOf('## The claim refusal ladder');
22
+ assert.ok(start >= 0, 'the ladder section exists');
23
+ const next = doc.indexOf('\n## ', start + 5);
24
+ return doc.slice(start, next < 0 ? undefined : next);
25
+ }
26
+
27
+ test('the claim ladder carries no line-number anchors (task 1005183)', () => {
28
+ const s = ladderSection();
29
+ const stale = s.match(/`[\w./-]*:\d+`|`[\w./-]+\.js:\d+`|\b[\w-]+\.js:\d+\b/g) || [];
30
+ assert.deepEqual(stale, [], `line-number anchors rot — name the function or refusal code instead: ${stale.join(', ')}`);
31
+ });
32
+
33
+ test('every function the ladder names exists where it says', () => {
34
+ // The db-claims entry points are loaded, not grepped: a rename or a dropped export
35
+ // reds this even when the old wording survives in a comment.
36
+ const claims = require('../modules/lifecycle/db-claims.js');
37
+ for (const fn of ['claimTask', 'claimTasksBatch', 'validateClaimBatch', 'claimUniqueViolationError']) {
38
+ assert.equal(typeof claims[fn], 'function', `db-claims.js exports ${fn}`);
39
+ assert.ok(ladderSection().includes(fn), `the ladder names ${fn}`);
40
+ }
41
+ // The two route gates are private to the router, so only their definitions can be checked.
42
+ for (const fn of ['gateOneActiveClaimPerSession', 'gateNewcomerClaim']) {
43
+ assert.match(routes, new RegExp(`function ${fn}\\b`), `routes/claims.js defines ${fn}`);
44
+ }
45
+ assert.match(routes, /router\.post\('\/claims', auth\.requireBuilder, gateOneActiveClaimPerSession, gateNewcomerClaim,/);
46
+ });
47
+
48
+ test('every refusal code the ladder names is thrown in db-claims.js', () => {
49
+ const s = ladderSection();
50
+ for (const code of ['BUILDER_INACTIVE', 'REBASE_REQUIRED', 'TASK_NOT_FOUND', 'TASK_NOT_READY', 'OPEN_BLOCKER',
51
+ 'rank_too_low_for_task', 'INSUFFICIENT_RANK', 'DEPS_NOT_SHIPPED']) {
52
+ assert.ok(s.includes(code), `ladder names ${code}`);
53
+ assert.ok(dbClaims.includes(`'${code}'`), `db-claims.js carries ${code}`);
54
+ }
55
+ });