@bongos/core 1.24.79 → 1.24.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (413) hide show
  1. package/.bongos-core.json +554 -394
  2. package/.claude/skills/ask-for-help/SKILL.md +1 -1
  3. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  4. package/.claude/skills/design/SKILL.md +1 -1
  5. package/.claude/skills/goal-create/SKILL.md +1 -1
  6. package/.claude/skills/planning-session/SKILL.md +1 -1
  7. package/clients/bongos-client/README.md +1 -1
  8. package/clients/bongos-client/bongos-client.global.js +8 -0
  9. package/clients/bongos-client/index.cjs +8 -0
  10. package/clients/bongos-client/index.d.ts +11 -0
  11. package/clients/bongos-client/index.mjs +8 -0
  12. package/docs/adr/0274-one-kernel-three-role-packs.md +2 -2
  13. package/docs/adr/0399-the-visor-is-glass-over-the-scene-and-owns-no-camera.md +91 -0
  14. package/docs/adr/0400-a-chain-reserves-work-and-each-sub-quest-is-still-claimed-alone.md +109 -0
  15. package/docs/adr/0401-one-options-menu-opens-every-settings-screen-in-a-frame.md +53 -0
  16. package/docs/adr/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md +142 -0
  17. package/docs/adr/README.md +4 -0
  18. package/docs/api/openapi.json +195 -3
  19. package/docs/api-reference.md +8 -4
  20. package/docs/architecture.md +7 -3
  21. package/docs/copy-inventory.md +393 -378
  22. package/docs/copy-registry.json +722 -578
  23. package/docs/design/characters/character-sprites-source.webp +0 -0
  24. package/docs/file-map.md +7 -8
  25. package/docs/module-api-changelog.md +4 -0
  26. package/docs/modules-contract.md +5 -0
  27. package/docs/onboarding/slash-commands.md +1 -1
  28. package/docs/packs/{engineer.md → artisan.md} +5 -5
  29. package/docs/packs/artist.md +50 -4
  30. package/docs/page-inventory.json +16 -7
  31. package/docs/page-readings.json +3184 -3338
  32. package/docs/{packs/ideator.md → recipes/ideation-playbook.md} +5 -48
  33. package/migrations/core_296_crafts_artisan_artist.sql +72 -0
  34. package/migrations/core_297_builder_character.sql +30 -0
  35. package/modules/copy-desk/proposals.js +1 -1
  36. package/modules/economy/credits-by-craft.js +29 -15
  37. package/modules/hall-ui/public/ai-studio.html +6 -4
  38. package/modules/hall-ui/public/ai-studio.states.json +1 -0
  39. package/modules/hall-ui/public/atlas.html +5 -5
  40. package/modules/hall-ui/public/atlas.js +3 -1
  41. package/modules/hall-ui/public/atlas.states.json +1 -0
  42. package/modules/hall-ui/public/blockers.html +5 -5
  43. package/modules/hall-ui/public/board-room.html +4 -4
  44. package/modules/hall-ui/public/board-room.states.json +1 -0
  45. package/modules/hall-ui/public/characters/artisan.png +0 -0
  46. package/modules/hall-ui/public/characters/artist.png +0 -0
  47. package/modules/hall-ui/public/characters/patron.png +0 -0
  48. package/modules/hall-ui/public/city.css +3 -4
  49. package/modules/hall-ui/public/city.html +9 -9
  50. package/modules/hall-ui/public/city.js +17 -56
  51. package/modules/hall-ui/public/city.states.json +7 -32
  52. package/modules/hall-ui/public/collab.html +4 -4
  53. package/modules/hall-ui/public/collab.states.json +1 -0
  54. package/modules/hall-ui/public/commands.html +4 -4
  55. package/modules/hall-ui/public/commands.states.json +1 -0
  56. package/modules/hall-ui/public/controls.css +63 -11
  57. package/modules/hall-ui/public/copy-desk.html +4 -4
  58. package/modules/hall-ui/public/copy-desk.states.json +1 -0
  59. package/modules/hall-ui/public/craft-mode.js +258 -64
  60. package/modules/hall-ui/public/deploy.html +4 -4
  61. package/modules/hall-ui/public/deploy.states.json +1 -0
  62. package/modules/hall-ui/public/diagram-viewer.js +2 -1
  63. package/modules/hall-ui/public/dom-utils.js +1 -1
  64. package/modules/hall-ui/public/goal-map.html +9 -6
  65. package/modules/hall-ui/public/goals.html +5 -5
  66. package/modules/hall-ui/public/goals.states.json +1 -0
  67. package/modules/hall-ui/public/government.html +4 -4
  68. package/modules/hall-ui/public/government.states.json +1 -0
  69. package/modules/hall-ui/public/hall-render.js +3 -1
  70. package/modules/hall-ui/public/home-city-draw.js +3 -3
  71. package/modules/hall-ui/public/home-city.css +4 -0
  72. package/modules/hall-ui/public/home-city.js +17 -9
  73. package/modules/hall-ui/public/home-gallery.js +8 -1
  74. package/modules/hall-ui/public/home-idea-box.css +2 -2
  75. package/modules/hall-ui/public/home-idea-box.js +3 -3
  76. package/modules/hall-ui/public/idea.html +5 -5
  77. package/modules/hall-ui/public/idea.states.json +1 -0
  78. package/modules/hall-ui/public/ideas.html +5 -5
  79. package/modules/hall-ui/public/ideas.states.json +1 -0
  80. package/modules/hall-ui/public/index-founding.states.json +1 -0
  81. package/modules/hall-ui/public/index-genesis-decide.states.json +1 -0
  82. package/modules/hall-ui/public/index-genesis-demo.states.json +1 -0
  83. package/modules/hall-ui/public/index-genesis.states.json +1 -0
  84. package/modules/hall-ui/public/index.html +11 -7
  85. package/modules/hall-ui/public/index.states.json +1 -0
  86. package/modules/hall-ui/public/market-watch-item.html +4 -4
  87. package/modules/hall-ui/public/market-watch-item.states.json +1 -0
  88. package/modules/hall-ui/public/market-watch-rules.html +4 -4
  89. package/modules/hall-ui/public/market-watch-rules.states.json +1 -0
  90. package/modules/hall-ui/public/market-watch.html +6 -9
  91. package/modules/hall-ui/public/market-watch.js +22 -45
  92. package/modules/hall-ui/public/market-watch.states.json +1 -0
  93. package/modules/hall-ui/public/modules.html +4 -4
  94. package/modules/hall-ui/public/modules.states.json +1 -0
  95. package/modules/hall-ui/public/money.html +4 -4
  96. package/modules/hall-ui/public/money.states.json +1 -0
  97. package/modules/hall-ui/public/my-agents.html +4 -4
  98. package/modules/hall-ui/public/my-agents.states.json +1 -0
  99. package/modules/hall-ui/public/options-menu.js +409 -0
  100. package/modules/hall-ui/public/paintbrush.js +3 -3
  101. package/modules/hall-ui/public/palette.js +11 -13
  102. package/modules/hall-ui/public/primer.html +5 -5
  103. package/modules/hall-ui/public/primer.states.json +1 -0
  104. package/modules/hall-ui/public/profile-crafts.js +1 -1
  105. package/modules/hall-ui/public/profile-roles.js +23 -19
  106. package/modules/hall-ui/public/profile.css +1 -1
  107. package/modules/hall-ui/public/profile.html +19 -18
  108. package/modules/hall-ui/public/profile.js +19 -18
  109. package/modules/hall-ui/public/profile.states.json +5 -4
  110. package/modules/hall-ui/public/project-settings.html +4 -4
  111. package/modules/hall-ui/public/roster.html +4 -4
  112. package/modules/hall-ui/public/roster.js +3 -3
  113. package/modules/hall-ui/public/roster.states.json +1 -0
  114. package/modules/hall-ui/public/settings.css +24 -4
  115. package/modules/hall-ui/public/settings.html +21 -17
  116. package/modules/hall-ui/public/settings.js +44 -44
  117. package/modules/hall-ui/public/settings.states.json +1 -0
  118. package/modules/hall-ui/public/shell-game.css +110 -0
  119. package/modules/hall-ui/public/shell-game.js +316 -3
  120. package/modules/hall-ui/public/shell.js +15 -66
  121. package/modules/hall-ui/public/sky-interaction.js +7 -0
  122. package/modules/hall-ui/public/speciality-tools.js +1 -2
  123. package/modules/hall-ui/public/street-card.states.json +1 -0
  124. package/modules/hall-ui/public/street-inside.js +3 -4
  125. package/modules/hall-ui/public/street.css +1 -5
  126. package/modules/hall-ui/public/street.html +6 -10
  127. package/modules/hall-ui/public/street.js +8 -59
  128. package/modules/hall-ui/public/street.states.json +7 -32
  129. package/modules/hall-ui/public/studio.html +5 -5
  130. package/modules/hall-ui/public/studio.states.json +1 -0
  131. package/modules/hall-ui/public/style.css +197 -112
  132. package/modules/hall-ui/public/task-detail.js +5 -4
  133. package/modules/hall-ui/public/task.html +5 -5
  134. package/modules/hall-ui/public/task.states.json +1 -0
  135. package/modules/hall-ui/public/thinking.css +10 -0
  136. package/modules/hall-ui/public/thinking.html +9 -6
  137. package/modules/hall-ui/public/thinking.states.json +1 -0
  138. package/modules/hall-ui/public/tweak-editor.html +5 -5
  139. package/modules/hall-ui/public/tweak-editor.js +5 -1
  140. package/modules/hall-ui/public/tweak-editor.states.json +1 -0
  141. package/modules/hall-ui/public/update-pill.js +91 -2
  142. package/modules/hall-ui/public/visor.css +80 -0
  143. package/modules/hall-ui/public/visor.js +316 -0
  144. package/modules/hall-ui/public/watch-repo-invite.js +159 -0
  145. package/modules/hall-ui/public/watch.html +6 -5
  146. package/modules/hall-ui/public/watch.js +17 -2
  147. package/modules/hall-ui/public/watch.states.json +1 -0
  148. package/modules/hall-ui/public/welcome-boot.js +15 -6
  149. package/modules/hall-ui/public/work-founding.states.json +1 -0
  150. package/modules/hall-ui/public/work.html +5 -5
  151. package/modules/hall-ui/public/work.states.json +1 -0
  152. package/modules/hall-ui/records/builders-roster.md +2 -2
  153. package/modules/hall-ui/records/craft-mode.md +13 -11
  154. package/modules/hall-ui/records/game-mode.md +2 -2
  155. package/modules/hall-ui/records/governor-street.md +1 -1
  156. package/modules/hall-ui/records/home-city.md +3 -1
  157. package/modules/hall-ui/records/market-watch-settings.md +2 -0
  158. package/modules/hall-ui/records/options-menu.md +15 -0
  159. package/modules/hall-ui/records/profile-roles.md +11 -11
  160. package/modules/hall-ui/records/sky.md +3 -3
  161. package/modules/hall-ui/records/start-screen.md +25 -0
  162. package/modules/hall-ui/records/tweak-editor.md +1 -1
  163. package/modules/hall-ui/records/update-pill.md +12 -0
  164. package/modules/hall-ui/records/visor.md +23 -0
  165. package/modules/hall-ui/records/welcome-tower.md +5 -5
  166. package/modules/hall-ui/specimens/components.states.json +2 -1
  167. package/modules/ideas/ideator-stats.js +3 -1
  168. package/modules/ideas/inbox.js +2 -2
  169. package/modules/lifecycle/activity.js +1 -1
  170. package/modules/lifecycle/cascade.js +4 -4
  171. package/modules/lifecycle/goal-spec-task.js +1 -1
  172. package/modules/lifecycle/help-requests.js +19 -15
  173. package/modules/lifecycle/kickoff-checklist.js +3 -3
  174. package/modules/lifecycle/kickoff-seed.js +1 -1
  175. package/modules/lifecycle/migrations/lifecycle_021_crafts_artisan_artist.sql +29 -0
  176. package/modules/lifecycle/mingle.js +4 -4
  177. package/modules/lifecycle/page-tweak-write.js +1 -1
  178. package/modules/lifecycle/routes/task-write-routes.js +3 -1
  179. package/modules/lifecycle/routes/tasks.js +3 -3
  180. package/modules/lifecycle/ship-card.js +2 -3
  181. package/modules/lifecycle/task-classifier.js +39 -29
  182. package/modules/lifecycle/tests/cascade.mjs +7 -7
  183. package/modules/onboarding/founding-mode.js +3 -0
  184. package/modules/onboarding/genesis-home.js +21 -1
  185. package/modules/onboarding/routes/onboarding.js +2 -0
  186. package/modules/page-notes/notes.js +3 -2
  187. package/modules/page-notes/split.js +1 -1
  188. package/modules/platform-identity/craft-rollup.js +35 -7
  189. package/modules/platform-identity/migrations/platform_identity_036_crafts_artisan_artist.sql +45 -0
  190. package/modules/platform-identity/platform-identity.js +2 -2
  191. package/modules/platform-identity/routes/community-leaderboard.js +2 -1
  192. package/modules/platform-identity/routes/community-search.js +2 -1
  193. package/modules/platform-identity/routes/sso.js +2 -1
  194. package/modules/platform-identity/tests/platform-identity.mjs +6 -6
  195. package/modules/public-landing/public/community.html +3 -5
  196. package/modules/public-landing/public/index.html +1 -1
  197. package/modules/public-landing/public/projects.html +1 -1
  198. package/modules/specialities/migrations/specialities_004_crafts_artisan_artist.sql +30 -0
  199. package/modules/specialities/routes/specialities.js +2 -2
  200. package/modules/status-ui/public/status.js +1 -1
  201. package/modules/ui-design/kit/fixtures/community-leaderboard.json +10 -10
  202. package/modules/ui-design/kit/fixtures/community-search.json +2 -2
  203. package/modules/ui-design/kit/fixtures/scouting-recruiter.json +1 -1
  204. package/modules/ui-design/public/brand.html +2 -2
  205. package/package-lock.json +2 -2
  206. package/package.json +1 -1
  207. package/release-notes.json +76 -0
  208. package/scripts/bongos/autobongos-loop.js +1 -1
  209. package/scripts/bongos/discipline-modes.json +7 -12
  210. package/scripts/bongos/fitness.js +1 -1
  211. package/scripts/bongos/gen-hall-sitemap.js +7 -7
  212. package/scripts/bongos/grade-smoke.js +2 -2
  213. package/scripts/bongos/init.js +2 -2
  214. package/scripts/bongos/newcomer-floor.config.json +1 -2
  215. package/scripts/bongos/newcomer-floor.js +4 -3
  216. package/scripts/bongos/newcomer-floor.templates.json +10 -12
  217. package/scripts/bongos/newcomer-floor.test.js +47 -38
  218. package/scripts/bongos/role-pack-guard.js +5 -4
  219. package/scripts/bongos/seed-tasks.example.json +3 -3
  220. package/scripts/bongos/setup.js +20 -107
  221. package/scripts/bongos/start.js +2 -2
  222. package/scripts/bongos/task.js +5 -4
  223. package/scripts/bongos/triage.js +1 -1
  224. package/scripts/bongos/worktree.js +16 -4
  225. package/scripts/hall-preview/README.md +2 -3
  226. package/scripts/setup-builder.js +6 -46
  227. package/src/bongos/auth-admission.js +8 -8
  228. package/src/bongos/craft-mode.js +2 -0
  229. package/src/bongos/db.js +39 -1
  230. package/src/bongos/github-repos.js +78 -0
  231. package/src/bongos/module-scope-map.js +22 -0
  232. package/src/bongos/role-stats.js +27 -11
  233. package/src/bongos/routes/github.js +100 -2
  234. package/src/bongos/routes/me.js +33 -0
  235. package/src/bongos/serve-internal.js +33 -11
  236. package/src/crafts.js +66 -0
  237. package/src/hall-helm.js +79 -0
  238. package/src/module-api.js +6 -1
  239. package/src/module-loader/manifest-schema.js +16 -0
  240. package/src/modules.js +2 -2
  241. package/src/role-responsibilities.js +6 -5
  242. package/tests/acclaim_feed_evidence_db.mjs +1 -1
  243. package/tests/activity_rollup_as_of.mjs +1 -1
  244. package/tests/api_alias_caller_ratchet.mjs +3 -3
  245. package/tests/application_lifecycle.mjs +1 -1
  246. package/tests/artist_review.mjs +1 -1
  247. package/tests/artist_review_goal.mjs +1 -1
  248. package/tests/blocker_hall_live_proof.mjs +1 -1
  249. package/tests/blocker_tasks_named_db.mjs +2 -2
  250. package/tests/canonical_profile_url.mjs +1 -1
  251. package/tests/cap_newcomer_tasks.mjs +13 -13
  252. package/tests/character_creation.mjs +294 -0
  253. package/tests/claim_gate_open_blocker.mjs +2 -2
  254. package/tests/claim_gate_rebase.mjs +6 -6
  255. package/tests/claude_materialize.mjs +1 -1
  256. package/tests/clawback_confirmed_backlog.mjs +3 -3
  257. package/tests/collab_page.mjs +8 -9
  258. package/tests/collab_self_settling.mjs +2 -2
  259. package/tests/community_leaderboard_route.mjs +13 -5
  260. package/tests/community_proof_db.mjs +3 -0
  261. package/tests/community_search_route.mjs +7 -6
  262. package/tests/conductor.mjs +1 -1
  263. package/tests/conductor_main_e2e.mjs +8 -8
  264. package/tests/copy_desk_flags_db.mjs +1 -1
  265. package/tests/copy_desk_page_approve.mjs +2 -2
  266. package/tests/copy_desk_page_claim.mjs +2 -2
  267. package/tests/copy_desk_page_next.mjs +1 -1
  268. package/tests/copy_desk_page_release.mjs +1 -1
  269. package/tests/copy_desk_page_submit.mjs +2 -2
  270. package/tests/core_258_box_session_claims_db.mjs +1 -1
  271. package/tests/craft_mode.mjs +48 -34
  272. package/tests/craft_mode_home.mjs +20 -20
  273. package/tests/craft_mode_looks.mjs +16 -15
  274. package/tests/craft_mode_nav.mjs +31 -29
  275. package/tests/craft_review_mode.mjs +46 -41
  276. package/tests/crafts_vocabulary.mjs +70 -0
  277. package/tests/cross_discipline_priority_direction.mjs +5 -5
  278. package/tests/design_reference.mjs +1 -1
  279. package/tests/fitness.mjs +7 -11
  280. package/tests/funded_project_pays_proof_db.mjs +1 -1
  281. package/tests/genesis_home.mjs +21 -1
  282. package/tests/github_chip.mjs +3 -3
  283. package/tests/github_repo_invite.mjs +379 -0
  284. package/tests/goal_criterion_confirm_ui.mjs +1 -1
  285. package/tests/goal_invite_consent.mjs +1 -1
  286. package/tests/goal_map_page.mjs +2 -2
  287. package/tests/goal_sort_staleness.mjs +1 -1
  288. package/tests/goal_spec_task.mjs +1 -1
  289. package/tests/goal_spec_task_db.mjs +3 -3
  290. package/tests/grade_engine.mjs +2 -2
  291. package/tests/grader_no_artifact.mjs +1 -1
  292. package/tests/grader_prompt_fencing.mjs +1 -1
  293. package/tests/grader_prompt_window_bound.mjs +1 -1
  294. package/tests/grader_reader_lens.mjs +2 -2
  295. package/tests/grader_severity_gate.mjs +1 -1
  296. package/tests/grader_unbacked_score.mjs +1 -1
  297. package/tests/hall_approval_queue.mjs +1 -1
  298. package/tests/hall_board_world.mjs +2 -2
  299. package/tests/hall_builders_page_pass.mjs +3 -2
  300. package/tests/hall_city.mjs +16 -30
  301. package/tests/hall_controls.mjs +52 -4
  302. package/tests/hall_fullscreen.mjs +27 -3
  303. package/tests/hall_helm.mjs +187 -0
  304. package/tests/hall_kit_charts.mjs +1 -1
  305. package/tests/hall_landing_boot.mjs +1 -1
  306. package/tests/hall_my_agents.mjs +16 -16
  307. package/tests/hall_nav.mjs +22 -15
  308. package/tests/hall_options_menu.mjs +247 -0
  309. package/tests/hall_oversight_world.mjs +1 -1
  310. package/tests/hall_palette.mjs +8 -9
  311. package/tests/hall_profile_world.mjs +3 -3
  312. package/tests/hall_record_world.mjs +6 -4
  313. package/tests/hall_settings_world.mjs +2 -1
  314. package/tests/hall_sitemap.mjs +6 -5
  315. package/tests/hall_start_screen.mjs +307 -0
  316. package/tests/hall_street.mjs +15 -28
  317. package/tests/hall_street_inside.mjs +17 -9
  318. package/tests/hall_studio_world.mjs +3 -3
  319. package/tests/hall_topbar_320.mjs +4 -2
  320. package/tests/hall_visor.mjs +224 -0
  321. package/tests/help_requests.mjs +47 -15
  322. package/tests/helpers.mjs +4 -4
  323. package/tests/home_city.mjs +6 -4
  324. package/tests/home_city_widget.mjs +33 -14
  325. package/tests/home_gallery_widget.mjs +4 -4
  326. package/tests/home_idea_box.mjs +6 -6
  327. package/tests/hub_community_page.mjs +3 -3
  328. package/tests/hub_craft_rollup.mjs +75 -36
  329. package/tests/hub_recent_ships.mjs +3 -2
  330. package/tests/idea_cascade.mjs +2 -2
  331. package/tests/idea_credit_streams_db.mjs +1 -1
  332. package/tests/idea_detail_page.mjs +1 -1
  333. package/tests/idea_ideate_full.mjs +5 -2
  334. package/tests/idea_spark_hall.mjs +1 -1
  335. package/tests/init.mjs +3 -3
  336. package/tests/instance_manifest.mjs +2 -2
  337. package/tests/kickoff_checklist.mjs +1 -1
  338. package/tests/leaderboard_roles.mjs +34 -25
  339. package/tests/lifecycle_claim_ladder_refs.mjs +55 -0
  340. package/tests/lightbulb.mjs +14 -10
  341. package/tests/main_discipline.mjs +8 -8
  342. package/tests/markdown_line_endings.mjs +1 -1
  343. package/tests/market_watch_pages.mjs +52 -74
  344. package/tests/me_streak_leaderboard_queries.mjs +3 -3
  345. package/tests/migration_allocation.mjs +1 -1
  346. package/tests/mingle.mjs +36 -21
  347. package/tests/module-scope-map.mjs +22 -2
  348. package/tests/module_api.mjs +1 -0
  349. package/tests/module_contributions.mjs +13 -13
  350. package/tests/module_manifest.mjs +29 -0
  351. package/tests/module_skills_hook.mjs +2 -2
  352. package/tests/modules.mjs +14 -7
  353. package/tests/nav_permission_atoms.mjs +2 -1
  354. package/tests/needs_rebase.mjs +1 -1
  355. package/tests/newcomer_restock.mjs +13 -13
  356. package/tests/page_notes_panel.mjs +22 -22
  357. package/tests/page_tweak_no_block.mjs +1 -1
  358. package/tests/paintbrush.mjs +7 -7
  359. package/tests/privacy_matrix.mjs +1 -0
  360. package/tests/profile_activity.mjs +20 -23
  361. package/tests/profile_earnings.mjs +1 -1
  362. package/tests/profile_nudge_prefs.mjs +4 -4
  363. package/tests/profile_role_headline.mjs +73 -46
  364. package/tests/profile_role_stats.mjs +55 -45
  365. package/tests/profile_rollup_consent.mjs +19 -6
  366. package/tests/profile_route.mjs +12 -12
  367. package/tests/profile_ui_cross_project.mjs +4 -4
  368. package/tests/project_invite_suggestions.mjs +2 -2
  369. package/tests/promote_goal_id.mjs +1 -1
  370. package/tests/publish_manifest.mjs +1 -1
  371. package/tests/rank_floor_wiring.mjs +1 -1
  372. package/tests/regrade_actual_minutes.mjs +1 -1
  373. package/tests/removal_proposal.mjs +2 -2
  374. package/tests/reward_gate.mjs +4 -4
  375. package/tests/reward_mode_cost_plus_only.mjs +1 -1
  376. package/tests/role_responsibilities.mjs +18 -12
  377. package/tests/role_stats_db.mjs +17 -16
  378. package/tests/scouting_rollup_fanout.mjs +5 -5
  379. package/tests/seed_tasks.mjs +5 -2
  380. package/tests/session_earnings_mirror_db.mjs +1 -1
  381. package/tests/session_reward_on_land_db.mjs +1 -1
  382. package/tests/session_reward_reconcile_db.mjs +2 -2
  383. package/tests/sessions_kit_pager.mjs +1 -1
  384. package/tests/settings_art_key_absent.mjs +2 -2
  385. package/tests/settings_main_role.mjs +21 -20
  386. package/tests/setup_flow.mjs +3 -3
  387. package/tests/setup_smoke.mjs +20 -0
  388. package/tests/ship_card.mjs +6 -6
  389. package/tests/specialities.mjs +16 -4
  390. package/tests/speciality_db_shape.mjs +6 -6
  391. package/tests/speciality_injection.mjs +17 -17
  392. package/tests/speciality_offer_ui.mjs +5 -5
  393. package/tests/speciality_offered.mjs +6 -5
  394. package/tests/speciality_session_skills.mjs +7 -7
  395. package/tests/speciality_skills.mjs +11 -11
  396. package/tests/speciality_walkthrough_ui.mjs +2 -2
  397. package/tests/sso_checkin_identity_spoof.mjs +2 -1
  398. package/tests/task_classifier_vocab.mjs +10 -0
  399. package/tests/task_cli.mjs +11 -3
  400. package/tests/task_criteria_unlink_db.mjs +2 -2
  401. package/tests/task_discipline_required.mjs +38 -16
  402. package/tests/task_goal_required.mjs +4 -4
  403. package/tests/task_needs_db.mjs +1 -1
  404. package/tests/tweak_replaces_artist_review.mjs +1 -1
  405. package/tests/update_pill.mjs +153 -2
  406. package/tests/upkeep_home.mjs +1 -1
  407. package/tests/v3_c1_options_proof.mjs +145 -0
  408. package/tests/visual_review.mjs +3 -3
  409. package/tests/watch_applications_queue.mjs +8 -5
  410. package/tests/watch_sealed_floor.mjs +3 -1
  411. package/tests/welcome_boot.mjs +12 -9
  412. package/tests/welcome_boot_browser.mjs +13 -13
  413. package/tests/worktree_helper.mjs +16 -1
@@ -0,0 +1,307 @@
1
+ // tests/hall_start_screen.mjs — the game's start screen and load screen (task 1005143, BONGOS-V3 R03).
2
+ //
3
+ // WHY. Done-when: a sign-in passes through both before the world. That is a chain of hand-offs a
4
+ // reader would only notice breaking as "the hall looks like it used to": the welcome hands a fresh
5
+ // sign-in to shell-game.js, Start turns game mode on, the load screen builds the city week by week
6
+ // from the one bounded city read, and the craft picker waits until the world is showing. So this
7
+ // RUNS the real welcome-boot.js and shell-game.js: the hand-off and the pure parts in a vm, and
8
+ // the two screens in a real browser over Home's own markup (skipped where there is no browser).
9
+ //
10
+ // Run: node tests/hall_start_screen.mjs
11
+ import assert from 'node:assert/strict';
12
+ import { test, after } from 'node:test';
13
+ import { createRequire } from 'node:module';
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import vm from 'node:vm';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const require = createRequire(import.meta.url);
20
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
21
+ const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
22
+ const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
23
+ const CITY = fs.readFileSync(path.join(ROOT, 'scripts', 'hall-preview', 'fixtures', 'home__city.json'), 'utf8');
24
+
25
+ // ---------- in a vm: the pure parts and the welcome's hand-off ----------
26
+ function sandbox({ store = {}, game = true } = {}) {
27
+ const started = [], asked = [];
28
+ const window = {
29
+ location: { search: '', pathname: '/', hash: '' },
30
+ __BRANDING__: { identity: { worldName: 'Harbor' } },
31
+ localStorage: {
32
+ getItem: (k) => (k in store ? store[k] : null),
33
+ setItem: (k, v) => { store[k] = String(v); },
34
+ removeItem: (k) => { delete store[k]; },
35
+ },
36
+ sessionStorage: { getItem: () => null, setItem() {} },
37
+ OTBCraftMode: { markAsked: () => asked.push('marked'), onAccess: (a, b) => asked.push(['picker', a.authed, b.id]) },
38
+ };
39
+ const document = { documentElement: { dataset: {}, classList: { add() {}, remove() {} } }, addEventListener() {} };
40
+ const box = { window, document, URLSearchParams, setTimeout, clearTimeout, console };
41
+ vm.createContext(box);
42
+ if (game) {
43
+ vm.runInContext(read('shell-game.js'), box, { filename: 'shell-game.js' });
44
+ window.OTBShellGame.start = (o) => { started.push(o); return true; };
45
+ }
46
+ vm.runInContext(read('welcome-boot.js'), box, { filename: 'welcome-boot.js' });
47
+ return { window, store, started, asked };
48
+ }
49
+
50
+ test('the menu is Start, Options, Achievements, Extras, Quit, in that order', () => {
51
+ const { window } = sandbox();
52
+ const MENU = JSON.parse(JSON.stringify(window.OTBShellGame.MENU)); // out of the vm's realm
53
+ assert.deepEqual(MENU.map((m) => m.label), ['Start', 'Options', 'Achievements', 'Extras', 'Quit']);
54
+ const links = MENU.filter((m) => m.href).map((m) => [m.id, m.href]);
55
+ assert.deepEqual(links, [['options', '/settings'], ['achievements', '/profile']]);
56
+ for (const [, href] of links) assert.ok(fs.existsSync(path.join(HALL, href.slice(1) + '.html')), href + ' is a hall page');
57
+ });
58
+
59
+ test('the load screen walks the weeks oldest first and lands exactly on now', () => {
60
+ const { window } = sandbox();
61
+ const G = window.OTBShellGame;
62
+ const stops = Array.from(G.historySteps(10, 30));
63
+ assert.equal(stops.length, 31);
64
+ assert.equal(stops[0], 0);
65
+ assert.equal(stops[30], 9, 'the last stop is the latest week, not a rounding short of it');
66
+ assert.ok(stops.every((v, i) => i === 0 || v >= stops[i - 1]), 'never backwards');
67
+ assert.deepEqual(Array.from(G.historySteps(1, 30)).filter((v) => v !== 0), [], 'a one-week project stands still');
68
+ assert.equal(G.whenLabel(10, 0), 'The city, 9 weeks ago');
69
+ assert.equal(G.whenLabel(10, 8), 'The city, 1 week ago');
70
+ assert.equal(G.whenLabel(10, 9), 'The city, now');
71
+ });
72
+
73
+ test('the tips rotate, and every one is a sentence', () => {
74
+ const { window } = sandbox();
75
+ const G = window.OTBShellGame;
76
+ const n = G.TIPS.length;
77
+ assert.ok(n >= 3);
78
+ assert.equal(G.tipAt(0), G.TIPS[0]);
79
+ assert.equal(G.tipAt(n), G.TIPS[0], 'wraps');
80
+ assert.equal(G.tipAt(n + 1), G.TIPS[1]);
81
+ assert.equal(G.tipAt('garbage'), G.TIPS[0]);
82
+ for (const t of G.TIPS) assert.match(t, /^[A-Z].*\.$/);
83
+ });
84
+
85
+ test('the load screen fills one segment per week, up to the week it is showing (task 1005192)', () => {
86
+ const { window } = sandbox();
87
+ const G = window.OTBShellGame;
88
+ assert.equal(G.segsDone(10, 0), 1, 'the oldest week is the first segment');
89
+ assert.equal(G.segsDone(10, 4.5), 5);
90
+ assert.equal(G.segsDone(10, 9), 10, 'now fills the row');
91
+ assert.equal(G.segsDone(10, 99), 10, 'never more than there are weeks');
92
+ assert.equal(G.segsDone(0, 0), 0);
93
+ });
94
+
95
+ test('a fresh sign-in is handed to the start screen with the still, and the craft picker waits for the world', () => {
96
+ const s = sandbox({ store: { 'hall-welcome-signin': '1' } });
97
+ const access = { authed: true }, builder = { id: 7, display_name: 'Ada' };
98
+ assert.equal(s.window.OTBWelcome.onAccess(access, builder), true, 'the shell is told a screen is up, so it asks no picker');
99
+ assert.equal(s.started.length, 1);
100
+ assert.equal(s.started[0].name, 'Harbor');
101
+ assert.match(s.started[0].art, /^\/builders\/welcome-(day|night)\.webp$/);
102
+ assert.equal(s.store['hall-welcome-signin'], undefined, 'the flag is spent');
103
+ assert.deepEqual(s.asked, [], 'nothing is asked while the start screen is up');
104
+ s.started[0].done();
105
+ assert.deepEqual(s.asked, [['picker', true, 7]], 'the picker is asked once the world is showing');
106
+ });
107
+
108
+ test('a visit that is not a fresh sign-in keeps the welcome, and a hall without shell-game.js greets a sign-in as before', () => {
109
+ const daily = sandbox();
110
+ assert.equal(daily.window.OTBWelcome.onAccess({ authed: true }, { id: 7 }), true);
111
+ assert.deepEqual([daily.started.length, daily.asked], [0, ['marked']], 'the day\'s first visit is the welcome\'s');
112
+ const bare = sandbox({ store: { 'hall-welcome-signin': '1' }, game: false });
113
+ assert.equal(bare.window.OTBWelcome.onAccess({ authed: true }, { id: 7 }), true);
114
+ assert.deepEqual(bare.asked, ['marked']);
115
+ });
116
+
117
+ test('Home loads the sheet and the script, and the sheet types in no size, corner or breakpoint', () => {
118
+ const html = read('index.html');
119
+ assert.match(html, /<link rel="stylesheet" href="\/builders\/shell-game\.css[^"]*">/);
120
+ assert.ok(html.indexOf('welcome-boot.js') < html.indexOf('shell.js?'), 'the welcome, which hands over, loads before the shell');
121
+ const css = read('shell-game.css').replace(/\/\*[\s\S]*?\*\//g, '');
122
+ assert.doesNotMatch(css, /font-size:\s*[\d.]/);
123
+ assert.doesNotMatch(css, /border-radius:\s*[\d.]/);
124
+ assert.doesNotMatch(css, /@media/);
125
+ assert.doesNotMatch(css, /#[0-9a-fA-F]{3,8}\b/, 'tokens, or black and white: no private palette');
126
+ // Helm (task 1005192, ADR 0402): the literal floor is zero, the voice and the panel are the hall tier's tokens
127
+ assert.doesNotMatch(css, /rgba?\(|hsla?\(|box-shadow:\s*[\d-]|text-shadow:\s*[\d-]|font-family:(?!\s*var\()/, 'no typed colour, shadow or face');
128
+ assert.match(css, /\.gs__title \{[^}]*font-family: var\(--name-face\);[^}]*letter-spacing: var\(--name-space-wide\); text-transform: var\(--name-case\);/, 'the title is the name voice');
129
+ assert.match(css, /\.gs__item \{[^}]*font-family: var\(--name-face\);[^}]*text-transform: var\(--name-case\);/, 'so is every choice');
130
+ assert.match(css, /\.gs__menu, \.gs__load \{[^}]*background: var\(--night-glass\);/, 'both panels are the night over the art');
131
+ assert.match(css, /\.gs__item\[data-gs="start"\] \{\s*background: var\(--accent\); color: var\(--accent-on\);/, 'Start is the one thing lit, under the dark ink');
132
+ assert.match(css, /\.gs__item:focus-visible \{[^}]*box-shadow: var\(--press-focus\);/, 'a row rings inside itself, in two tones');
133
+ });
134
+
135
+ // ---------- in a browser: the two screens over Home's own markup ----------
136
+ const lib = require(path.join(ROOT, 'modules', 'ui-design', 'kit', 'lib.js'));
137
+ let browser = null;
138
+ try { browser = await lib.launchBrowser(); } catch (e) { if (!(e instanceof lib.NoBrowserError)) throw e; }
139
+ after(() => browser && browser.close());
140
+ const skip = !browser && 'no system Edge or Chrome';
141
+
142
+ const ORIGIN = 'http://hall.test';
143
+ const TYPES = { '.js': 'text/javascript', '.css': 'text/css', '.webp': 'image/webp' };
144
+ const INDEX = read('index.html').replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, '').replace(/\{\{[^}]*\}\}/g, '');
145
+ const STUB = '<script>window.__BRANDING__ = { identity: { worldName: "Harbor" } }; window.BongosClient = { createClient: () => ({ request: async () => ({ ok: false, status: 404, data: null }) }) };</script>';
146
+ const SCRIPTS = ['dom-utils.js', 'welcome-boot.js', 'shell-game.js', 'shell-trail.js', 'craft-mode.js', 'shell.js', 'city-draw.js', 'home-city-draw.js']
147
+ .map((f) => `<script src="/builders/${f}"></script>`).join('');
148
+ const PAGE = INDEX.replace('</body>', STUB + SCRIPTS + '</body>');
149
+
150
+ async function hall({ city = true, reducedMotion = 'no-preference', viewport = { width: 1280, height: 800 } } = {}) {
151
+ const ctx = await browser.newContext({ viewport, reducedMotion });
152
+ await ctx.route(ORIGIN + '/**', (route) => {
153
+ const p = new URL(route.request().url()).pathname;
154
+ if (p === '/' || p === '/index.html') return route.fulfill({ contentType: 'text/html', body: PAGE });
155
+ if (p === '/api/bongos/home/city') return city ? route.fulfill({ contentType: 'application/json', body: CITY }) : route.fulfill({ status: 500, body: '' });
156
+ const ext = path.extname(p);
157
+ const f = path.join(HALL, p.replace(/^\/builders\//, ''));
158
+ if (p.startsWith('/builders/') && TYPES[ext] && fs.existsSync(f)) return route.fulfill({ contentType: TYPES[ext], body: fs.readFileSync(f) });
159
+ return route.fulfill({ status: 404, body: '' });
160
+ });
161
+ return ctx;
162
+ }
163
+ // A page that has just come back from signing in: the flag the sign-in click left, then /me.
164
+ async function signedIn(ctx) {
165
+ const page = await ctx.newPage();
166
+ const errors = [];
167
+ page.on('pageerror', (e) => errors.push(String(e.message)));
168
+ await page.addInitScript(() => { try { localStorage.setItem('hall-welcome-signin', '1'); } catch (_) { /* no storage */ } });
169
+ await page.goto(ORIGIN + '/');
170
+ await page.waitForFunction(() => !!window.OTBShell && !!window.OTBWelcome && !!window.OTBShellGame);
171
+ await page.evaluate(() => {
172
+ const me = { authed: true, rank: 'metic', builder: { id: 7, display_name: 'Ada', github_login: 'ada' } };
173
+ window.OTBShell.applyAccess(me);
174
+ window.OTBShell.applyAccess(me);
175
+ });
176
+ return { page, errors };
177
+ }
178
+ const state = (page) => page.evaluate(() => ({
179
+ start: !!document.getElementById('game-start'),
180
+ welcome: !!document.getElementById('welcome-boot'),
181
+ picker: !!document.getElementById('craft-picker'),
182
+ game: document.documentElement.dataset.game || null,
183
+ full: document.documentElement.dataset.fullscreen || null,
184
+ }));
185
+
186
+ test('a sign-in passes through the start screen and the load screen before the world', { skip }, async () => {
187
+ const ctx = await hall();
188
+ const { page, errors } = await signedIn(ctx);
189
+ await page.waitForSelector('#game-start');
190
+ assert.deepEqual(await state(page), { start: true, welcome: false, picker: false, game: null, full: null },
191
+ 'the start screen, not the welcome and not the picker; game mode is not on until Start');
192
+ const menu = await page.evaluate(() => ({
193
+ title: document.getElementById('gs-title').textContent,
194
+ items: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras) .gs__item')).map((e) => e.textContent),
195
+ focus: document.activeElement.textContent,
196
+ small: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras) .gs__item')).filter((e) => e.getBoundingClientRect().height < 46).length,
197
+ }));
198
+ assert.deepEqual(menu, { title: 'Harbor', items: ['Start', 'Options', 'Achievements', 'Extras', 'Quit'], focus: 'Start', small: 0 });
199
+
200
+ // Helm (task 1005192): one full-height night panel on the left, the mark over the name, a line at the foot.
201
+ const panel = await page.evaluate(() => {
202
+ const box = (sel) => { const r = document.querySelector(sel).getBoundingClientRect(); return { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) }; };
203
+ const start = document.querySelector('[data-gs="start"]'), title = document.getElementById('gs-title');
204
+ return {
205
+ menu: box('.gs__menu'), start: box('[data-gs="start"]'), view: { w: window.innerWidth, h: window.innerHeight },
206
+ mark: !!document.querySelector('.gs__head .gs__mark'), line: document.getElementById('gs-line').textContent,
207
+ caps: getComputedStyle(title).textTransform, startCaps: getComputedStyle(start).textTransform,
208
+ key: getComputedStyle(start, '::after').content, sideways: document.getElementById('game-start').scrollWidth - document.getElementById('game-start').clientWidth,
209
+ };
210
+ });
211
+ assert.equal(panel.menu.w, 400, 'the panel is 400 wide on a desk');
212
+ assert.ok(panel.menu.x > 0 && panel.menu.x < panel.view.w / 4, 'and stands on the left, off the edge');
213
+ assert.deepEqual([panel.menu.y, panel.menu.h], [0, panel.view.h], 'the full height of the screen');
214
+ assert.ok(panel.start.w >= panel.menu.w - 4, 'Start is a bar the width of the panel');
215
+ assert.equal(panel.mark, true, 'the mark stands over the name');
216
+ assert.match(panel.line, /^[A-Z].*\.$/, 'the foot carries one sentence');
217
+ assert.deepEqual([panel.caps, panel.startCaps], ['uppercase', 'uppercase'], 'names are capitals');
218
+ assert.equal(panel.key, '"Enter"', 'Start names the key that takes it');
219
+ assert.equal(panel.sideways, 0);
220
+
221
+ // The arrows walk the menu; Escape stays with the screen and does not flip the shell underneath.
222
+ await page.keyboard.press('ArrowDown');
223
+ assert.equal(await page.evaluate(() => document.activeElement.textContent), 'Options');
224
+ await page.keyboard.press('ArrowUp');
225
+ await page.keyboard.press('Escape');
226
+ await page.evaluate(() => new Promise((r) => setTimeout(r, 50)));
227
+ assert.equal((await state(page)).start, true, 'Escape does not dismiss the start screen');
228
+
229
+ // Extras opens its links; Escape folds them away again.
230
+ await page.click('[data-gs="extras"]');
231
+ assert.equal(await page.isVisible('#gs-extras a'), true);
232
+ await page.keyboard.press('Escape');
233
+ assert.equal(await page.isVisible('#gs-extras a'), false);
234
+
235
+ // Start: game mode on, the load screen with a tip, and the city building up to now.
236
+ await page.click('[data-gs="start"]');
237
+ await page.waitForSelector('#gs-load:not([hidden])');
238
+ assert.equal(await page.isVisible('.gs__menu'), false);
239
+ assert.match(await page.textContent('#gs-tip'), /\.$/);
240
+ await page.waitForFunction(() => /weeks? ago$/.test(document.getElementById('gs-when').textContent));
241
+ assert.ok(await page.evaluate(() => document.querySelectorAll('#gs-city *').length > 20), 'the city is drawn, not a placeholder');
242
+ const weeks = JSON.parse(CITY).weeks.length;
243
+ assert.equal(await page.evaluate(() => document.querySelectorAll('#gs-segs:not([hidden]) > .seg').length), weeks, 'one segment per week of the history');
244
+ await page.waitForFunction(() => { const w = document.getElementById('gs-when'); return !w || w.textContent === 'The city, now'; });
245
+ const segs = await page.evaluate(() => { const b = document.getElementById('gs-segs'); return b ? [b.querySelectorAll('.seg--done').length, b.getAttribute('aria-hidden')] : null; });
246
+ if (segs) assert.deepEqual(segs, [weeks, 'true'], 'at now every segment is filled; the words beside them say it, so the row is hidden from a screen reader');
247
+ await page.waitForSelector('#game-start', { state: 'detached' });
248
+ await page.waitForSelector('#craft-picker');
249
+ assert.deepEqual(await state(page), { start: false, welcome: false, picker: true, game: 'on', full: 'on' },
250
+ 'the world: game mode on, the bars gone, and only now the craft picker');
251
+ assert.equal(await page.evaluate(() => localStorage.getItem('hall-game')), 'on');
252
+ assert.deepEqual(errors, []);
253
+ await ctx.close();
254
+ });
255
+
256
+ test('on a phone the panel is the whole width, nothing scrolls sideways and every choice clears the floor (task 1005192)', { skip }, async () => {
257
+ for (const [w, h] of [[390, 844], [320, 700]]) {
258
+ const ctx = await hall({ viewport: { width: w, height: h } });
259
+ const { page, errors } = await signedIn(ctx);
260
+ await page.waitForSelector('#game-start');
261
+ const m = await page.evaluate(() => {
262
+ const gs = document.getElementById('game-start'), r = document.querySelector('.gs__menu').getBoundingClientRect();
263
+ return {
264
+ x: Math.round(r.left), w: Math.round(r.width), sideways: gs.scrollWidth - gs.clientWidth,
265
+ small: Array.from(document.querySelectorAll('.gs__item')).filter((e) => !e.closest('[hidden]')).filter((e) => { const b = e.getBoundingClientRect(); return b.height < 24 || b.width < 24 || b.right > window.innerWidth + 1; }).length,
266
+ };
267
+ });
268
+ assert.deepEqual(m, { x: 0, w, sideways: 0, small: 0 }, `at ${w}px`);
269
+ await page.click('[data-gs="start"]');
270
+ await page.waitForSelector('#gs-load:not([hidden])');
271
+ assert.equal(await page.evaluate(() => { const gs = document.getElementById('game-start'); return gs.scrollWidth - gs.clientWidth; }), 0, `the load screen fits ${w}px`);
272
+ assert.deepEqual(errors, []);
273
+ await ctx.close();
274
+ }
275
+ });
276
+
277
+ test('Quit closes the tab, and says so when the browser keeps it open', { skip }, async () => {
278
+ const ctx = await hall();
279
+ const { page } = await signedIn(ctx);
280
+ await page.waitForSelector('#game-start');
281
+ await page.evaluate(() => { window.__closed = 0; window.close = () => { window.__closed += 1; }; });
282
+ await page.click('[data-gs="quit"]');
283
+ await page.waitForSelector('#gs-note:not([hidden])');
284
+ assert.equal(await page.evaluate(() => window.__closed), 1);
285
+ assert.match(await page.textContent('#gs-note'), /Close it to quit/);
286
+ await ctx.close();
287
+ });
288
+
289
+ test('with no city to draw, or with reduced motion, the still and the tip stand and the world still opens', { skip }, async () => {
290
+ for (const opts of [{ city: false }, { reducedMotion: 'reduce' }]) {
291
+ const ctx = await hall(opts);
292
+ const { page, errors } = await signedIn(ctx);
293
+ await page.waitForSelector('#game-start');
294
+ await page.click('[data-gs="start"]');
295
+ await page.waitForSelector('#gs-load:not([hidden])');
296
+ assert.match(await page.textContent('#gs-tip'), /\.$/);
297
+ if (opts.reducedMotion) {
298
+ await page.waitForFunction(() => { const w = document.getElementById('gs-when'); return !w || !w.hidden; });
299
+ assert.equal(await page.evaluate(() => { const w = document.getElementById('gs-when'); return w ? w.textContent : 'The city, now'; }), 'The city, now',
300
+ 'no week-by-week run: the finished city at once');
301
+ }
302
+ await page.waitForSelector('#game-start', { state: 'detached' });
303
+ assert.equal((await state(page)).game, 'on');
304
+ assert.deepEqual(errors, []);
305
+ await ctx.close();
306
+ }
307
+ });
@@ -122,11 +122,11 @@ test('a task not started is the prototype\'s workItem "ready" slab, and the more
122
122
  const p = new Proto({ mode: 'dark' });
123
123
  const T = p.theme('dark');
124
124
  const a = []; const b = [];
125
- p.workItem(a, T, 'engineer', 'ready', 660, 19, 0);
125
+ p.workItem(a, T, 'artisan', 'ready', 660, 19, 0);
126
126
  S.work(b, S.theme('dark'), 'slab', 660, 0, 19, 6, false);
127
127
  assert.deepEqual(b, cityTokens(a), 'the slab, at a base of 0 as workItem draws it');
128
128
  const c = []; const d = [];
129
- p.workItem(c, T, 'engineer', 'backlog', 660, 40, 0);
129
+ p.workItem(c, T, 'artisan', 'backlog', 660, 40, 0);
130
130
  S.work(d, S.theme('dark'), 'stakes', 660, 0, 40, 0, false);
131
131
  assert.deepEqual(d, c, 'the stakes');
132
132
  });
@@ -488,7 +488,8 @@ test('the page holds the prototype\'s header, the street, the readings and the c
488
488
  assert.equal((html.match(/<main\b/g) || []).length, 1);
489
489
  assert.equal((html.match(/<h1\b/g) || []).length, 1);
490
490
  assert.match(html, /<a class="street-mark" id="streetHome" href="\/builders\/city"[^>]*>\{\{productName\}\}</);
491
- for (const id of ['hudNeeds', 'hudMoving', 'hudShare', 'streetMode', 'streetSigns', 'streetPins', 'streetDoors', 'streetCard']) assert.match(html, new RegExp(`id="${id}"`), id);
491
+ assert.doesNotMatch(html, /streetMode/, 'no light/dark switch: the hall is dark only (task 1005192, ADR 0402)');
492
+ for (const id of ['hudNeeds', 'hudMoving', 'hudShare', 'streetSigns', 'streetPins', 'streetDoors', 'streetCard']) assert.match(html, new RegExp(`id="${id}"`), id);
492
493
  const scripts = [...html.matchAll(/<script src="\/builders\/([a-z-]+)\.js/g)].map((m) => m[1]);
493
494
  assert.deepEqual(scripts.filter((s) => !/^street-(deck|office)$/.test(s)), ['dom-utils', 'shell-game', 'shell-trail', 'craft-mode', 'shell', 'hall-dialog', 'palette', 'gov-living', 'gov-feeds', 'city-draw', 'street-draw', 'street-card', 'street-inside', 'street-depth', 'street-desk', 'street-amend', 'street'], 'the hall shell (task 1004544), the living model and its one reader (task 1004685: city-draw.js counts with it, so it loads first), the shared kit, then the card (task 1004528), the interiors (task 1004526), the living street\'s exterior (street-depth.js), the floor desk (task 1004558) and the drafted amendment (task 1004530); the deck rides beside them');
494
495
  assert.equal(City.STREET_HREF, '/builders/street', 'the city\'s door is this page');
@@ -552,7 +553,7 @@ function fakeDom() {
552
553
  };
553
554
  const focused = { el: null };
554
555
  const html = mk('html');
555
- const ids = ['streetMode', 'streetModeIcon', 'streetModeWord', 'streetWindow', 'streetSizer', 'streetBoard', 'streetSvg', 'streetFlat', 'streetBlds', 'streetCrates', 'streetCols', 'streetGroups', 'streetDoors', 'streetPins', 'streetSigns', 'streetEvening', 'streetEveningLine', 'hudNeeds', 'hudMoving', 'hudShare', 'hudDot', 'streetHome', 'streetCard', 'streetCardWhere', 'streetCardWhen', 'streetCardTitle', 'streetCardBody', 'streetCardOpen', 'streetCardClose', 'streetToast'];
556
+ const ids = ['streetWindow', 'streetSizer', 'streetBoard', 'streetSvg', 'streetFlat', 'streetBlds', 'streetCrates', 'streetCols', 'streetGroups', 'streetDoors', 'streetPins', 'streetSigns', 'streetEvening', 'streetEveningLine', 'hudNeeds', 'hudMoving', 'hudShare', 'hudDot', 'streetHome', 'streetCard', 'streetCardWhere', 'streetCardWhen', 'streetCardTitle', 'streetCardBody', 'streetCardOpen', 'streetCardClose', 'streetToast'];
556
557
  for (const id of ids) mk('x').setAttribute('id', id);
557
558
  const document = { documentElement: html, getElementById: (id) => byId.get(id) || null, createElement: (t) => mk(t), createElementNS: (_ns, t) => mk(t) };
558
559
  return { document, byId, html, focused };
@@ -701,32 +702,18 @@ test('the header counts wear the pins\' colours, so they double as the legend',
701
702
  assert.ok(size('urgent') > size('needs') && size('needs') > size('moving') && size('moving') > size('settled'), 'urgent > needs > moving > settled');
702
703
  });
703
704
 
704
- test('the switch reads and writes the hall\'s own theme preference and redraws the street', async () => {
705
- const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark: true });
706
- assert.equal(p.html.attrs['data-theme'], 'light', 'the stored hall choice beats the OS');
707
- assert.equal(p.byId.get('streetMode').attrs['aria-label'], 'Switch to dark mode');
708
- assert.equal(p.byId.get('streetModeWord').textContent, 'dark');
709
- const before = JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs);
710
- p.byId.get('streetMode').fire('click');
711
- assert.equal(p.store.get('hall-theme'), 'dark');
712
- assert.equal(p.html.attrs['data-theme'], 'dark');
713
- assert.notEqual(JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs), before, 'the street redraws in the new mode');
714
- });
715
-
716
- test('in the hall, the street and the top bar\'s theme switch never disagree (task 1004544)', async () => {
717
- let shellPaints = 0;
718
- const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark: true, shell: { applyTheme() { shellPaints++; } } });
719
- assert.equal(p.observers.length, 1, 'the page watches html[data-theme]');
720
- p.byId.get('streetMode').fire('click');
721
- assert.equal(shellPaints, 1, 'this switch repaints the shell\'s');
722
- const before = JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs);
723
- p.store.set('hall-theme', 'light'); p.html.setAttribute('data-theme', 'light'); // the shell's switch
724
- p.observers[0].fn();
725
- assert.equal(p.byId.get('streetModeWord').textContent, 'dark', 'this switch follows the shell\'s');
726
- assert.notEqual(JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs), before, 'and the street redraws in that mode');
727
- assert.equal(shellPaints, 1, 'following the shell does not echo back to it');
705
+ test('the street is dark only: no stored choice and no OS setting reaches it (task 1005192, ADR 0402)', async () => {
706
+ const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark: false, shell: { applyTheme() { throw new Error('the street no longer drives the shell'); } } });
707
+ assert.equal(p.html.attrs['data-theme'], 'dark', 'a stored light choice and a light OS are both ignored');
708
+ assert.equal(p.store.get('hall-theme'), 'light', 'and the old preference is never written');
709
+ assert.equal(p.observers.length, 0, 'nothing flips the theme, so nothing watches it');
710
+ const q = await bootPage({ feeds: FEEDS([]), stored: null, osDark: true });
711
+ assert.equal(JSON.stringify(q.byId.get('streetBlds').children[0].children[0].attrs), JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs), 'one drawing whatever was stored');
712
+ assert.doesNotMatch(read('street.js'), /hall-theme|prefers-color-scheme/, 'street.js reads no theme preference');
728
713
  });
729
714
 
715
+
716
+
730
717
  test('arriving through a door marks that building, and a failed docket says so', async () => {
731
718
  const p = await bootPage({ feeds: FEEDS([]), hash: '#building=treasury' });
732
719
  const here = p.byId.get('streetDoors').children.filter((a) => a.attrs['aria-current'] === 'location');
@@ -131,7 +131,15 @@ test('every district is the prototype\'s road of work, both modes', () => {
131
131
  const p = proto(mode, { view: key, ...st });
132
132
  const want = p.districtView(p.theme(mode), key);
133
133
  const got = I.districtView(S.theme(mode), key, p.districtInfo(key));
134
- for (const k of ['kicker', 'name', 'line', 'road', 'ground', 'tx', 'ty', 's', 'shapes', 'legend']) assert.deepEqual(j(got[k]), j(want[k]), `${mode} ${key} ${k}`);
134
+ for (const k of ['kicker', 'name', 'line', 'road', 'ground', 'tx', 'ty', 's', 'shapes']) assert.deepEqual(j(got[k]), j(want[k]), `${mode} ${key} ${k}`);
135
+ // The legend's craft line is the one place the hall leaves the prototype's words
136
+ // (task 1005141): the engineer is the artisan, and the ideator — folded into the
137
+ // artist — no longer has a pavilion of its own. The status line is unchanged.
138
+ assert.deepEqual(j(got.legend), [
139
+ { key: 'a', text: 'Shape is the craft: tower for artisan, studio roof for artist, glass front for UI.' },
140
+ j(want.legend[1]),
141
+ ], `${mode} ${key} legend`);
142
+ assert.equal(want.legend[0].key, 'a', 'the prototype still has the craft line first');
135
143
  assert.deepEqual(got.items.map((i) => [i.x, i.y, i.w, i.name, i.meta, i.stage, i.stageColor, i.stageEdge]), want.items.map((i) => [i.x, i.y, i.w, i.name, i.meta, i.stage, i.stageColor, i.stageEdge]), `${mode} ${key} items`);
136
144
  }
137
145
  }
@@ -142,7 +150,7 @@ test('workItem and crown are verbatim: every craft at every stage, every crown',
142
150
  for (const mode of ['dark', 'light']) {
143
151
  const p = proto(mode);
144
152
  const T = p.theme(mode);
145
- for (const craft of ['engineer', 'artist', 'ui', 'ideator']) {
153
+ for (const craft of ['artisan', 'artist', 'ui']) {
146
154
  for (const stage of ['backlog', 'ready', 'being built', 'shipped', 'being graded', 'awaiting a tester']) {
147
155
  const a = []; const b = [];
148
156
  p.workItem(a, T, craft, stage, 86, 40, 92);
@@ -418,16 +426,16 @@ test('the lift runs to the chosen floor, and the chosen block flips on every pic
418
426
 
419
427
  test('a district is its own goal\'s tasks: shape from the craft, stage from the status, label the kind', () => {
420
428
  const t = (id, status, discipline, kind = 'feature', goal_id = '7', extra = {}) => ({ id: String(id), title: `task ${id}`, status, discipline, kind, goal_id, est_minutes: 120, ...extra });
421
- const tasks = [t(1, 'shipped', 'engineer'), t(2, 'ready', 'artist'), t(3, 'active', 'ui'), t(4, 'confirmed', 'ideator', 'spike'), t(5, 'backlog', null, 'bug'), t(6, 'blocked', 'engineer'), t(7, 'completed', 'artist'), t(8, 'cut', 'engineer'), t(9, 'shipped', 'ui', 'feature', '8')];
429
+ const tasks = [t(1, 'shipped', 'artisan'), t(2, 'ready', 'artist'), t(3, 'active', 'ui'), t(4, 'confirmed', 'artist', 'spike'), t(5, 'backlog', null, 'bug'), t(6, 'blocked', 'artisan'), t(7, 'completed', 'artist'), t(8, 'cut', 'artisan'), t(9, 'shipped', 'ui', 'feature', '8')];
422
430
  const d = I.districtInfo('7', goal(7), tasks);
423
431
  assert.deepEqual(d.items.map((i) => [i[0], i[1], i[2], i[3], i[5]]), [
424
- ['task 1', 'feature', 'engineer', 'shipped', 'shipped'],
425
- ['task 4', 'spike', 'ideator', 'graded · landing', 'graded · landing'],
432
+ ['task 1', 'feature', 'artisan', 'shipped', 'shipped'],
433
+ ['task 4', 'spike', 'artist', 'graded · landing', 'graded · landing'],
426
434
  ['task 7', 'feature', 'artist', 'being graded', 'being graded'],
427
435
  ['task 3', 'feature', 'ui', 'being built', 'being built'],
428
436
  ['task 2', 'feature', 'artist', 'ready', 'ready'],
429
- ['task 6', 'feature', 'engineer', 'backlog', 'blocked'],
430
- ['task 5', 'bug', 'engineer', 'backlog', 'backlog'],
437
+ ['task 6', 'feature', 'artisan', 'backlog', 'blocked'],
438
+ ['task 5', 'bug', 'artisan', 'backlog', 'backlog'],
431
439
  ], 'in the order work moves; a cut task and another goal\'s are not drawn; no craft is a tower');
432
440
  assert.equal(d.items[0][4], 56, 'height from the estimate, never a random');
433
441
  assert.equal(d.items[4][4], 0, 'a slab or a stake has no height');
@@ -439,7 +447,7 @@ test('a district is its own goal\'s tasks: shape from the craft, stage from the
439
447
  });
440
448
 
441
449
  test('past ten pieces of work the road keeps the ones in flight and counts the rest', () => {
442
- const tasks = Array.from({ length: 30 }, (_, i) => ({ id: String(i + 1), title: `t${i + 1}`, status: i < 3 ? 'active' : i < 25 ? 'shipped' : 'backlog', discipline: 'engineer', kind: 'feature', goal_id: '9', shipped_at: `2026-09-${String(1 + (i % 28)).padStart(2, '0')}T00:00:00Z` }));
450
+ const tasks = Array.from({ length: 30 }, (_, i) => ({ id: String(i + 1), title: `t${i + 1}`, status: i < 3 ? 'active' : i < 25 ? 'shipped' : 'backlog', discipline: 'artisan', kind: 'feature', goal_id: '9', shipped_at: `2026-09-${String(1 + (i % 28)).padStart(2, '0')}T00:00:00Z` }));
443
451
  const d = I.districtInfo('9', goal(9), tasks);
444
452
  assert.equal(d.items.length, I.MAX_ITEMS);
445
453
  assert.equal(d.more, 20);
@@ -636,7 +644,7 @@ test('a matter is a desk row: its name opens it in place; back to the street ret
636
644
  });
637
645
 
638
646
  test('a district address reads its goal\'s tasks once and draws them as buildings', async () => {
639
- const tasks = [{ id: '1', title: 'checkout', status: 'shipped', discipline: 'engineer', kind: 'feature', goal_id: '7' }, { id: '2', title: 'tax forms', status: 'active', discipline: 'ui', kind: 'feature', goal_id: '7' }, { id: '3', title: 'other goal', status: 'ready', goal_id: '8' }];
647
+ const tasks = [{ id: '1', title: 'checkout', status: 'shipped', discipline: 'artisan', kind: 'feature', goal_id: '7' }, { id: '2', title: 'tax forms', status: 'active', discipline: 'ui', kind: 'feature', goal_id: '7' }, { id: '3', title: 'other goal', status: 'ready', goal_id: '8' }];
640
648
  const p = await bootPage({ hash: '#district=7', feeds: FEEDS([], { '/api/bongos/tasks?goal_id=7': { tasks } }) });
641
649
  await new Promise((r) => setTimeout(r, 0));
642
650
  assert.equal(p.calls.filter((c) => c === '/api/bongos/tasks?goal_id=7').length, 1);
@@ -277,7 +277,7 @@ async function bootStudio({ reviews = [], shipped = {}, rank = 'metic', flags =
277
277
  // the same reason categoryChip is: an undefined helper throws mid-render.
278
278
  roleAxisLabel: () => 'Role',
279
279
  roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
280
- roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
280
+ roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
281
281
  roleIsUnset: (d) => !d || d === 'unclassified',
282
282
  ROLE_UNSET: 'unclassified',
283
283
  escapeHtml: (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])),
@@ -592,9 +592,9 @@ function bootShell() {
592
592
  test('the Studio nav entry is warmed only for an artist-craft builder', () => {
593
593
  const { applyAccess, navStudio } = bootShell();
594
594
 
595
- applyAccess({ authed: true, rank: 'archon', builder: { display_name: 'Artist A', preferred_disciplines: ['artist', 'ideator'] } });
595
+ applyAccess({ authed: true, rank: 'archon', builder: { display_name: 'Artist A', preferred_disciplines: ['artist'] } });
596
596
  assert.ok(navStudio()._classes.has('nav-item--craft'), 'an artist-craft builder gets the warmed entry');
597
597
 
598
- applyAccess({ authed: true, rank: 'archon', builder: { display_name: 'Eng E', preferred_disciplines: ['engineer'] } });
598
+ applyAccess({ authed: true, rank: 'archon', builder: { display_name: 'Eng E', preferred_disciplines: ['artisan'] } });
599
599
  assert.ok(!navStudio()._classes.has('nav-item--craft'), 'and a non-artist builder does not (the class is toggled off)');
600
600
  });
@@ -63,7 +63,7 @@ async function measureBar(width, scheme) {
63
63
  const ctx = await context(width, scheme);
64
64
  const page = await ctx.newPage();
65
65
  await page.goto(ORIGIN + '/work');
66
- await page.waitForSelector('#app-topbar .topbar-side #theme-toggle');
66
+ await page.waitForSelector('#app-topbar .topbar-side #zoom-toggle');
67
67
  return page.evaluate(() => {
68
68
  const side = document.querySelector('#app-topbar .topbar-side');
69
69
  if (!document.getElementById('upd-pill')) {
@@ -86,9 +86,11 @@ for (const scheme of ['light', 'dark']) {
86
86
  test(`at 320px the top bar fits, every button inside it and at least 24px (${scheme})`, { skip }, async () => {
87
87
  const m = await measureBar(320, scheme);
88
88
  const ids = m.buttons.map((b) => b.id);
89
- for (const want of ['upd-pill', 'topbar-jump', 'zoom-toggle', 'theme-toggle', 'fullscreen-toggle']) {
89
+ for (const want of ['upd-pill', 'topbar-jump', 'zoom-toggle', 'fullscreen-toggle']) {
90
90
  assert.ok(ids.includes(want), `${want} is on the bar (got ${ids.join(', ')})`);
91
91
  }
92
+ // the hall is dark only (task 1005192, ADR 0402): the bar offers no light/dark switch
93
+ assert.ok(!ids.includes('theme-toggle'), 'no theme switch on the bar');
92
94
  assert.ok(m.scrollWidth <= 320, `the page is ${m.scrollWidth}px wide at 320 (the top bar spills)`);
93
95
  for (const b of m.buttons) {
94
96
  assert.ok(b.left >= 0 && b.right <= 320, `${b.id} sits at ${Math.round(b.left)}..${Math.round(b.right)}, outside 0..320`);