@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
@@ -12,8 +12,8 @@
12
12
  // the ring road, or a task with no building;
13
13
  // * the line under the button saying anything but the docket's own line;
14
14
  // * the start governing button going anywhere but the street;
15
- // * the switch not using the hall's own preference, so the city opens in a
16
- // different mode from every other hall page;
15
+ // * a light/dark switch coming back: the hall is dark only (task 1005192,
16
+ // ADR 0402), so the city draws its night faces and reads no stored choice;
17
17
  // * motion that escapes the Kill Switch, or docket text reaching innerHTML.
18
18
  //
19
19
  // Run: node tests/hall_city.mjs
@@ -263,7 +263,7 @@ test('parcels ride only the roads of plots that hold a district, and leave throu
263
263
 
264
264
  // ── the page ────────────────────────────────────────────────────────────────
265
265
 
266
- test('the page holds only the wordmark, the switch, the city, one button and one line', () => {
266
+ test('the page holds only the wordmark, the city, one door and one line, and no light/dark switch', () => {
267
267
  const html = read('city.html').replace(/<!--[\s\S]*?-->/g, ''); // the markup, not the prose about it
268
268
  assert.equal((html.match(/<header\b/g) || []).length, 1);
269
269
  assert.equal((html.match(/<main\b/g) || []).length, 1);
@@ -271,7 +271,8 @@ test('the page holds only the wordmark, the switch, the city, one button and one
271
271
  assert.match(html, /<a class="city-start" id="cityStart" href="#decide"/);
272
272
  assert.equal((html.match(/<a\b/g) || []).length, 3, 'one door, and the two faint archived links to the street');
273
273
  assert.ok(html.includes('<p class="city-archived">Archived: <a href="/builders/street">Street</a> &amp; <a href="/builders/street#building=charter">Building Level Views</a>.</p>'), 'the street is archived behind two faint links');
274
- assert.equal((html.match(/<button\b/g) || []).length, 1, 'one switch');
274
+ assert.equal((html.match(/<button\b/g) || []).length, 0, 'no switch: the hall is dark only (task 1005192, ADR 0402)');
275
+ assert.doesNotMatch(html, /cityMode/);
275
276
  assert.match(html, /class="city-mark">\{\{productName\}\}</);
276
277
  assert.equal(City.STREET_HREF, '/builders/street');
277
278
  });
@@ -289,7 +290,7 @@ test('the city sits in the hall chrome, behind the shell\'s full-screen switch',
289
290
  // task 1004685 (the living city): gov-living.js, the model under the drawing, and
290
291
  // gov-feeds.js, its one reader per page (shared with the shell's tally), load before it
291
292
  // task 1004702: the card answers its first ask in place, so the deck's card and sender (and the desk rows under them) load before the page
292
- assert.deepEqual(scripts, ['dom-utils', 'shell-game', 'shell-trail', 'craft-mode', 'shell', 'hall-dialog', 'palette', 'gov-living', 'gov-feeds', 'city-draw', 'hall-kit', 'street-draw', 'street-card', 'street-inside', 'street-desk', 'street-deck', 'city-decide', 'city-switchboard', 'city'], 'the shell first, then the shared dialog (task 1005085), as on every hall page');
293
+ assert.deepEqual(scripts, ['dom-utils', 'shell-game', 'shell-trail', 'craft-mode', 'shell', 'hall-dialog', 'visor', 'palette', 'gov-living', 'gov-feeds', 'city-draw', 'hall-kit', 'street-draw', 'street-card', 'street-inside', 'street-desk', 'street-deck', 'city-decide', 'city-switchboard', 'city'], 'the shell first, then the shared dialog (task 1005085) and the visor (task 1005148)');
293
294
  // task 1004925: every script but dom-utils.js is deferred, so none holds up the
294
295
  // parser; defer runs them in document order, which is why the order above still
295
296
  // holds. dom-utils.js stays synchronous: the one-escaper guard
@@ -346,7 +347,7 @@ function fakeDom() {
346
347
  return el;
347
348
  };
348
349
  const html = mk('html');
349
- for (const id of ['cityMode', 'cityModeIcon', 'cityStage', 'cityFlat', 'cityBlds', 'cityParcels', 'cityPins', 'cityLine', 'cityStart']) mk('x').setAttribute('id', id);
350
+ for (const id of ['cityStage', 'cityFlat', 'cityBlds', 'cityParcels', 'cityPins', 'cityLine', 'cityStart']) mk('x').setAttribute('id', id);
350
351
  const document = {
351
352
  documentElement: html,
352
353
  getElementById: (id) => byId.get(id) || null,
@@ -421,33 +422,18 @@ test('a failed docket read draws no pins and says the docket could not be read',
421
422
  assert.equal(p.byId.get('cityLine').textContent, City.FALLBACK_LINE);
422
423
  });
423
424
 
424
- test('the switch reads and writes the hall\'s own theme preference', async () => {
425
- const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark: true });
426
- assert.equal(p.html.attrs['data-theme'], 'light', 'the stored hall choice beats the OS');
427
- assert.equal(p.byId.get('cityMode').attrs['aria-label'], 'Switch to dark mode');
428
- const before = JSON.stringify(p.byId.get('cityFlat').children[0].attrs);
429
- p.byId.get('cityMode').fire('click');
430
- assert.equal(p.store.get('hall-theme'), 'dark');
431
- assert.equal(p.html.attrs['data-theme'], 'dark');
432
- assert.equal(p.byId.get('cityMode').attrs['aria-label'], 'Switch to light mode');
433
- assert.notEqual(JSON.stringify(p.byId.get('cityFlat').children[0].attrs), before, 'the city redraws in the new mode');
425
+ test('the city is dark only: no stored choice and no OS setting reaches it (task 1005192, ADR 0402)', async () => {
426
+ const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark: false, shell: { applyTheme() { throw new Error('the city no longer drives the shell'); } } });
427
+ assert.equal(p.html.attrs['data-theme'], 'dark', 'a stored light choice and a light OS are both ignored');
428
+ assert.equal(p.store.get('hall-theme'), 'light', 'and the old preference is never written');
429
+ assert.equal(p.observers.length, 0, 'nothing flips the theme, so nothing watches it');
434
430
  const q = await bootPage({ feeds: FEEDS([]), stored: null, osDark: true });
435
- assert.equal(q.html.attrs['data-theme'], 'dark', 'no stored choice follows the OS');
431
+ assert.equal(JSON.stringify(q.byId.get('cityFlat').children[0].attrs), JSON.stringify(p.byId.get('cityFlat').children[0].attrs), 'one drawing whatever was stored');
432
+ const src = read('city.js');
433
+ assert.doesNotMatch(src, /hall-theme|localStorage|prefers-color-scheme/, 'city.js reads no theme preference');
436
434
  });
437
435
 
438
- test('in the hall, the city and the top bar\'s theme switch never disagree (task 1004544)', async () => {
439
- let shellPaints = 0;
440
- const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark: true, shell: { applyTheme() { shellPaints++; } } });
441
- assert.equal(p.observers.length, 1, 'the page watches html[data-theme]');
442
- p.byId.get('cityMode').fire('click');
443
- assert.equal(shellPaints, 1, 'this switch repaints the shell\'s');
444
- const before = JSON.stringify(p.byId.get('cityFlat').children[0].attrs);
445
- p.store.set('hall-theme', 'light'); p.html.setAttribute('data-theme', 'light'); // the shell's switch
446
- p.observers[0].fn();
447
- assert.equal(p.byId.get('cityMode').attrs['aria-label'], 'Switch to dark mode', 'this switch follows the shell\'s');
448
- assert.notEqual(JSON.stringify(p.byId.get('cityFlat').children[0].attrs), before, 'and the city redraws in that mode');
449
- assert.equal(shellPaints, 1, 'following the shell does not echo back to it');
450
- });
436
+
451
437
 
452
438
  test('the artboard fits the stage the hall chrome leaves, not the window (task 1004544)', async () => {
453
439
  const p = await bootPage({ feeds: FEEDS([]), stage: { clientWidth: 688, clientHeight: 900 } });
@@ -9,6 +9,11 @@
9
9
  // button (it loads later) without re-drawing it;
10
10
  // * THE SIZES — primary is DESIGN.md's 46–48px door, the row size and the
11
11
  // text button keep the 24px floor, the field clears it;
12
+ // * THE HELM SHAPE AND VOICE (task 1005192, ADR 0402) — in the hall a button
13
+ // is the cut corner in the name voice, every piece of it a hall-tier token
14
+ // spent with the old pill/lowercase as its fallback (the status page links
15
+ // this sheet without the hall's tier), and a cut button's focus ring is
16
+ // drawn inside the shape, because a clip-path clips an outline;
12
17
  // * CONTRAST, MEASURED — the button's ink on its fill (rest AND hover), the
13
18
  // ghost's and the text button's ink on both grounds, the field's text and
14
19
  // its edge (3:1, WCAG 1.4.11), in both modes, resolved from the real
@@ -144,11 +149,11 @@ test('style.css imports controls.css right after the shared token sheet', () =>
144
149
 
145
150
  // ── 2. the sizes ─────────────────────────────────────────────────────────────
146
151
 
147
- test('primary is the 46–48px pill; the row size and the text button keep the 24px floor', () => {
152
+ test('primary is the 46–48px door; the row size and the text button keep the 24px floor', () => {
148
153
  const base = sharedRule('.btn');
149
154
  const h = px(prop(base, 'min-height'));
150
155
  assert.ok(h >= 46 && h <= 48, `.btn is ${h}px — DESIGN.md's door is 46–48px`);
151
- assert.equal(prop(base, 'border-radius'), 'var(--radius-pill)', 'pill or nothing (DESIGN.md, Shapes)');
156
+ assert.equal(prop(base, 'border-radius'), 'var(--press-radius, var(--radius-pill))', 'the hall\'s corner, and the pill where the hall\'s tier is not loaded');
152
157
  assert.match(prop(base, 'border'), /transparent$/, 'the edge is reserved, so a modifier that sets only its colour draws');
153
158
  for (const sel of ['.btn--sm', '.btn--text']) {
154
159
  assert.ok(px(prop(sharedWith(sel, 'min-height'), 'min-height')) >= 24, `${sel} keeps 24px of height`);
@@ -203,6 +208,49 @@ test('no hall sheet puts white ink on an accent fill', () => {
203
208
  const PRIMARY = ['.btn', '.btn-accent', '.landing__btn', '.task__claim', '.setup-gate__save', '.td-btn--primary', '.pbtn--save', '.craft-save'];
204
209
  const GHOST = ['.btn--ghost', '.btn-ghost', '.landing__btn--quiet', '.td-btn', '.wip__release', '.pbtn', '.settings-btn'];
205
210
  const SMALL = ['.btn--sm', '.btn-ghost', '.task__claim', '.td-btn', '.wip__release'];
211
+ // the ghost's fill: the hall's press fill, and nothing at all without the hall's tier
212
+ const GHOST_FILL = 'var(--press-fill, transparent)';
213
+
214
+ // ── 3b. the Helm shape and voice (task 1005192, ADR 0402) ───────────────────
215
+
216
+ test('in the hall a button is the cut corner in the name voice, and without the hall\'s tier it is the pill it was', () => {
217
+ const base = sharedRule('.btn');
218
+ // every Helm piece is a token with the old look as its fallback
219
+ assert.equal(prop(base, 'clip-path'), 'var(--cut-path, none)');
220
+ assert.equal(prop(base, 'font-family'), 'var(--name-face, var(--font-body))');
221
+ assert.equal(prop(base, 'font-weight'), 'var(--name-weight, 500)');
222
+ assert.equal(prop(base, 'letter-spacing'), 'var(--name-space-tight, normal)');
223
+ assert.equal(prop(base, 'text-transform'), 'var(--name-case, lowercase)');
224
+ assert.equal(prop(sharedWith('.btn--sm', 'clip-path'), 'clip-path'), 'var(--cut-path-sm, none)', 'the row size takes the smaller cut');
225
+ // the tokens are the hall tier's, and the status page (which links this sheet) defines none of them
226
+ const status = fs.readFileSync(path.join(ROOT, 'modules', 'status-ui', 'public', 'style.css'), 'utf8');
227
+ for (const name of ['--cut-path', '--cut-path-sm', '--press-radius', '--press-fill', '--press-focus', '--name-face', '--name-weight', '--name-space-tight', '--name-case']) {
228
+ assert.ok(root[name], `${name} is in the hall's token block`);
229
+ assert.ok(!new RegExp(`${name}\\s*:`).test(status), `the status page does not define ${name}, so the fallback stands there`);
230
+ }
231
+ assert.equal(root['--cut-path'], 'polygon(var(--cut) 0, 100% 0, 100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%, 0 100%, 0 var(--cut))', 'top-left and bottom-right are cut');
232
+ assert.equal(root['--name-case'], 'uppercase');
233
+ // a link in a sentence is not a cut control and does not shout
234
+ const text = sharedWith('.btn--text', 'clip-path');
235
+ assert.equal(prop(text, 'clip-path'), 'none');
236
+ assert.equal(prop(text, 'text-transform'), 'none');
237
+ });
238
+
239
+ test('a cut button keeps a visible focus ring: inset, two-tone, 3:1 on its own fill', () => {
240
+ // clip-path clips the outline and the outer halo, so the base rule hands the shell's
241
+ // :focus-visible rule an inset ring instead; the text button (not cut) hands it back
242
+ assert.equal(prop(sharedRule('.btn'), '--focus-ring'), 'var(--press-focus)');
243
+ assert.equal(prop(sharedWith('.btn--text', '--focus-ring'), '--focus-ring'), 'initial');
244
+ const shell = rulesOf(styleSrc).find((x) => x.sels.length === 1 && x.sels[0] === ':focus-visible');
245
+ assert.ok(shell, 'the shell\'s ring rule');
246
+ assert.match(prop(shell.body, 'box-shadow'), /^var\(--focus-ring, /, 'the shell\'s ring spends what a cut control hands in');
247
+ assert.equal(root['--press-focus'], 'inset 0 0 0 2px var(--focus), inset 0 0 0 4px var(--bg)', 'the focus colour on the edge, then a 2px band of the ground');
248
+ // one of the two tones reads against each fill a cut button paints (WCAG 1.4.11, 3:1), in the mode the hall renders
249
+ for (const [what, fill] of [['primary', 'var(--accent)'], ['primary hover', 'var(--accent-lift)'], ['ghost on the page', onGround(GHOST_FILL, 'var(--bg)', 'dark')], ['ghost on a panel', onGround(GHOST_FILL, 'var(--bg-card)', 'dark')], ['danger', 'var(--danger)']]) {
250
+ const best = Math.max(contrast('var(--focus)', fill, 'dark'), contrast('var(--bg)', fill, 'dark'));
251
+ assert.ok(best >= 3, `dark ${what}: the ring's better tone is ${best.toFixed(2)}:1 on the fill`);
252
+ }
253
+ });
206
254
 
207
255
  test('every older button name sits in the shared rule it means', () => {
208
256
  const fill = (sel) => {
@@ -210,11 +258,11 @@ test('every older button name sits in the shared rule it means', () => {
210
258
  return r.length ? prop(r[0].body, 'background') : null;
211
259
  };
212
260
  for (const sel of PRIMARY) assert.equal(fill(sel), 'var(--accent)', `${sel} is a primary door`);
213
- for (const sel of GHOST) assert.equal(fill(sel), 'transparent', `${sel} is a ghost`);
261
+ for (const sel of GHOST) assert.equal(fill(sel), GHOST_FILL, `${sel} is a ghost`);
214
262
  for (const sel of SMALL) assert.equal(prop(sharedWith(sel, 'min-height'), 'min-height'), '30px', `${sel} is the row size`);
215
263
  // and the primary-on-a-ghost-base names come AFTER the ghost rule, or the ghost wins on order
216
264
  const list = rulesOf(controls);
217
- const ghostAt = list.findIndex((x) => x.sels.includes('.td-btn') && prop(x.body, 'background') === 'transparent');
265
+ const ghostAt = list.findIndex((x) => x.sels.includes('.td-btn') && prop(x.body, 'background') === GHOST_FILL);
218
266
  const primaryAt = list.findIndex((x) => x.sels.includes('.td-btn--primary') && prop(x.body, 'background'));
219
267
  assert.ok(primaryAt > ghostAt, '.td-btn--primary follows .td-btn, so the shared order paints it');
220
268
  });
@@ -93,7 +93,8 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
93
93
  addEventListener() {}, dispatchEvent() {},
94
94
  get cookie() { return ''; }, set cookie(_v) {},
95
95
  };
96
- const location = { hostname: 'h', origin: 'https://h', href: 'https://h/' + page + search, pathname: path || '/' + page, hash, search, assign() {}, replace() {} };
96
+ const assigned = []; // task 1005144: where the page sent the browser (game mode's Escape opens the menu)
97
+ const location = { hostname: 'h', origin: 'https://h', href: 'https://h/' + page + search, pathname: path || '/' + page, hash, search, assign(u) { assigned.push(u); }, replace() {} };
97
98
  // `session` is the TAB's sessionStorage: pass one object to several boots and
98
99
  // they are page loads in one tab (the trail, task 1004476).
99
100
  const sessionStorage = {
@@ -133,6 +134,7 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
133
134
  const root = documentObj.documentElement;
134
135
  return {
135
136
  store,
137
+ assigned,
136
138
  game: () => windowObj.OTBShellGame,
137
139
  gameStamp: () => root.dataset.game === 'on',
138
140
  // The "how to leave" line: showing = the shell created it and it is not hidden.
@@ -518,8 +520,8 @@ test('game mode: every hall page carries the switch — none opts out, and each
518
520
  test('game mode: leaving brings the bars back for THIS page only, and never rewrites a page\'s own memory', () => {
519
521
  const store = { 'hall-game': 'on', 'hall-fullscreen:thinking': 'on' };
520
522
  const goals = boot({ page: 'goals', optIn: false, store });
521
- goals.press('Escape');
522
- assert.equal(goals.on(), false, 'Escape still leaves');
523
+ goals.button().click();
524
+ assert.equal(goals.on(), false, 'the corner button still leaves (Escape opens the options menu, below)');
523
525
  assert.equal(store['hall-fullscreen:goals'], undefined, 'nothing remembered for the page');
524
526
  assert.equal(boot({ page: 'goals', optIn: false, store }).on(), true, 'the next load opens without bars again');
525
527
  const sky = boot({ page: 'thinking', store });
@@ -528,6 +530,28 @@ test('game mode: leaving brings the bars back for THIS page only, and never rewr
528
530
  assert.equal(store['hall-fullscreen:thinking'], 'on', 'the sky\'s own choice survives game mode');
529
531
  });
530
532
 
533
+ // task 1005144 (BONGOS-V3 R04, ADR 0399 §2.4 row 6): in game mode the last Escape is the
534
+ // options menu's. It opens the menu, remembering where it came from, and the bars stay away.
535
+ test('game mode: the last Escape opens the options menu, and the bars stay hidden', () => {
536
+ const store = { 'hall-game': 'on' };
537
+ const goals = boot({ page: 'goals', optIn: false, store, hash: '#/goal/7' });
538
+ goals.press('Escape');
539
+ assert.equal(goals.on(), true, 'the shell did not take the press');
540
+ assert.deepEqual(goals.assigned, ['/settings?from=' + encodeURIComponent('/goals#/goal/7')], 'the menu, and the way back');
541
+ const apex = boot({ page: 'goals', optIn: false, store, path: '/builders/goals' });
542
+ apex.press('Escape');
543
+ assert.deepEqual(apex.assigned, ['/builders/settings?from=' + encodeURIComponent('/builders/goals')], 'on the apex too');
544
+ const used = boot({ page: 'goals', optIn: false, store });
545
+ used.press('Escape', { pageUses: true });
546
+ used.press('Escape', { target: { tagName: 'INPUT' } });
547
+ assert.deepEqual(used.assigned, [], 'a press a page used, or one in a field, never opens it');
548
+ const plain = boot({ page: 'goals', optIn: false, store: {} });
549
+ plain.button().click();
550
+ plain.press('Escape');
551
+ assert.deepEqual(plain.assigned, [], 'outside game mode Escape is the shell\'s, as before');
552
+ assert.equal(plain.on(), false, 'and it leaves full screen');
553
+ });
554
+
531
555
  test('game mode: the Escape hand-off is unchanged — a press the page used, or one in a field, does not leave', () => {
532
556
  const p = boot({ page: 'goals', optIn: false, store: { 'hall-game': 'on' } });
533
557
  p.press('Escape', { pageUses: true });
@@ -0,0 +1,187 @@
1
+ // tests/hall_helm.mjs — Helm, the look the builders' hall wears (task 1005192, ADR 0402).
2
+ //
3
+ // WHY. Two owner decisions are easy to break without anyone seeing it on their own
4
+ // machine: Helm is the hall's alone (the landing, the hub and the status page keep
5
+ // their look), and the hall is dark only (no page offers light, and a light OS never
6
+ // flashes it). Both are decided in one serve-time seam, src/hall-helm.js, which
7
+ // recognises a hall page by the sheet it links. This file runs that seam over the
8
+ // real pages, and holds the pieces of the kit a browser would hide:
9
+ // * every hall page is served with html[data-theme="dark"], and nothing else is;
10
+ // * the Helm faces reach a hall whose pack names the default faces, and never a
11
+ // hall that chose its own, and never the landing or the status page;
12
+ // * the font stylesheet loads the way tests/google_fonts_nonblocking.mjs asks of
13
+ // a typed-in link (both preconnects on the page, display=swap), which that sweep
14
+ // cannot see because the link is built at serve time;
15
+ // * no hall script writes a theme choice, and the shell offers no switch;
16
+ // * the Helm neutrals: the two dark twins carry the night, the ice and no black;
17
+ // * the kit's recipes exist once, as hall-tier tokens and shared rules.
18
+ //
19
+ // Run: node --test tests/hall_helm.mjs
20
+ import { test } from 'node:test';
21
+ import assert from 'node:assert/strict';
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+ import { createRequire } from 'node:module';
26
+
27
+ const require = createRequire(import.meta.url);
28
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
29
+ const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
30
+ const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8');
31
+ const decomment = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
32
+
33
+ const { applyHelm, isHallPage, wearsHelmFaces, stampHtml, HELM_FONTS_HREF } = require('../src/hall-helm.js');
34
+ const { injectBrandingGlobal } = require('../src/bongos/serve-internal.js');
35
+ const { clientBranding, resolveBranding } = require('../src/branding.js');
36
+
37
+ const NEUTRAL = JSON.parse(read('config', 'branding.neutral.json'));
38
+ const DEFAULTS = NEUTRAL.theme.ui.fonts;
39
+ const hallPages = fs.readdirSync(HALL).filter((f) => f.endsWith('.html'));
40
+ const htmlTag = (html) => (html.match(/<html\b[^>]*>/i) || [''])[0];
41
+
42
+ // ── the seam ────────────────────────────────────────────────────────────────
43
+
44
+ test('a hall page is the one that links the hall sheet; the landing, the hub and the status page are not', () => {
45
+ for (const f of hallPages) assert.ok(isHallPage(read('modules', 'hall-ui', 'public', f)), `${f} links the hall's sheet`);
46
+ for (const p of [['modules', 'public-landing', 'public', 'index.html'], ['modules', 'public-landing', 'public', 'projects.html'], ['modules', 'status-ui', 'public', 'index.html'], ['modules', 'ui-design', 'public', 'brand.html']]) {
47
+ assert.equal(isHallPage(read(...p)), false, `${p.join('/')} is not a hall page`);
48
+ }
49
+ });
50
+
51
+ test('every hall page is served pinned dark, in the Helm faces, and no other surface is touched', () => {
52
+ const brand = clientBranding(resolveBranding({ neutral: NEUTRAL }));
53
+ for (const f of hallPages) {
54
+ const out = injectBrandingGlobal(read('modules', 'hall-ui', 'public', f), brand, {});
55
+ const tag = htmlTag(out);
56
+ assert.match(tag, / data-theme="dark"/, `${f}: dark before first paint`);
57
+ assert.match(tag, / data-faces="helm"/, `${f}: the Helm faces`);
58
+ assert.equal((tag.match(/data-theme=/g) || []).length, 1, `${f}: stamped once`);
59
+ assert.ok(out.includes(`href="${HELM_FONTS_HREF.replace(/&/g, '&amp;')}"`), `${f}: the two families are linked`);
60
+ // the link follows the pack's own splice, so it is the last word in <head>
61
+ assert.ok(out.indexOf('Barlow') > out.indexOf('--font-display:'), `${f}: after the pack's :root`);
62
+ }
63
+ for (const p of [['modules', 'public-landing', 'public', 'index.html'], ['modules', 'status-ui', 'public', 'index.html']]) {
64
+ const src = read(...p);
65
+ const out = injectBrandingGlobal(src, brand, {});
66
+ assert.equal(htmlTag(out), htmlTag(src), `${p.join('/')}: its <html> tag is as authored`);
67
+ assert.doesNotMatch(out, /Barlow|data-faces/, `${p.join('/')}: no Helm face`);
68
+ }
69
+ });
70
+
71
+ test('a hall that wears faces of its own keeps them; it is still dark only', () => {
72
+ const page = '<!doctype html><html lang="en"><head><link rel="stylesheet" href="/builders/style.css?v=1"></head><body></body></html>';
73
+ const own = { fonts: { display: '"Archivo", sans-serif', body: DEFAULTS.body, links: [] } };
74
+ assert.equal(wearsHelmFaces(own, DEFAULTS), false, 'one face of its own is a pack that chose');
75
+ const kept = applyHelm(page, own, DEFAULTS);
76
+ assert.match(htmlTag(kept.html), /data-theme="dark"/);
77
+ assert.doesNotMatch(kept.html, /data-faces/);
78
+ assert.equal(kept.tail, '', 'and no Helm stylesheet is linked');
79
+ // the default faces, and a pack that names none, take Helm
80
+ assert.equal(wearsHelmFaces({ fonts: { display: DEFAULTS.display, body: DEFAULTS.body } }, DEFAULTS), true);
81
+ assert.equal(wearsHelmFaces({}, DEFAULTS), true);
82
+ // a look from the style library with its own faces (ADR 0358) keeps them through the real resolver
83
+ const looked = clientBranding(resolveBranding({ neutral: NEUTRAL, env: { BONGOS_LOOK: 'expedition' } }));
84
+ if (looked.theme.look === 'expedition') {
85
+ const out = injectBrandingGlobal(page, looked, {});
86
+ assert.match(htmlTag(out), /data-theme="dark"/);
87
+ assert.doesNotMatch(out, /data-faces|Barlow/, 'the expedition look keeps Archivo');
88
+ }
89
+ });
90
+
91
+ test('the stamp replaces an attribute already there and leaves the others alone', () => {
92
+ assert.equal(stampHtml('<html lang="en" data-artist="on">', 'data-theme', 'dark'), '<html lang="en" data-artist="on" data-theme="dark">');
93
+ assert.equal(stampHtml('<html data-theme="light" lang="en">', 'data-theme', 'dark'), '<html lang="en" data-theme="dark">');
94
+ assert.equal(applyHelm('<html><head></head></html>', {}, DEFAULTS).html, '<html><head></head></html>', 'a page with no hall sheet is untouched');
95
+ });
96
+
97
+ test('the Helm stylesheet loads without blocking: swap, and both preconnects on every hall page', () => {
98
+ assert.match(HELM_FONTS_HREF, /^https:\/\/fonts\.googleapis\.com\/css2\?/);
99
+ assert.match(HELM_FONTS_HREF, /[?&]display=swap$/);
100
+ assert.match(HELM_FONTS_HREF, /family=Barlow\+Condensed:wght@400;500;600/);
101
+ assert.match(HELM_FONTS_HREF, /family=Barlow:wght@400;500/);
102
+ const missing = [];
103
+ for (const f of hallPages) {
104
+ const html = read('modules', 'hall-ui', 'public', f);
105
+ if (!/<link rel="preconnect" href="https:\/\/fonts\.googleapis\.com">/.test(html)) missing.push(`${f}: fonts.googleapis.com`);
106
+ if (!/<link rel="preconnect" href="https:\/\/fonts\.gstatic\.com" crossorigin>/.test(html)) missing.push(`${f}: fonts.gstatic.com (crossorigin)`);
107
+ }
108
+ assert.deepEqual(missing, [], 'a hall page that links a web font warms both font origins first');
109
+ });
110
+
111
+ // ── dark only ───────────────────────────────────────────────────────────────
112
+
113
+ test('no hall script stores or offers a theme, and the shell pins dark', () => {
114
+ const shell = read('modules', 'hall-ui', 'public', 'shell.js');
115
+ assert.match(shell, /function applyTheme\(\) \{\s*document\.documentElement\.dataset\.theme = 'dark';\s*\}/, 'the shell sets dark, whatever was stored');
116
+ assert.doesNotMatch(shell, /theme-toggle|toggleTheme/, 'no switch on the top bar');
117
+ const writers = [];
118
+ for (const f of fs.readdirSync(HALL).filter((x) => x.endsWith('.js'))) {
119
+ const src = read('modules', 'hall-ui', 'public', f).replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:'"])\/\/.*$/gm, '$1');
120
+ if (/['"]hall-theme['"]/.test(src)) writers.push(`${f}: names the retired 'hall-theme' preference`);
121
+ if (/dataset\.theme\s*=\s*(?![\s=]|'dark')|setAttribute\(\s*['"]data-theme['"]\s*,\s*(?!\s|'dark')/.test(src)) writers.push(`${f}: writes a theme other than dark`);
122
+ }
123
+ assert.deepEqual(writers, []);
124
+ for (const f of hallPages) assert.doesNotMatch(read('modules', 'hall-ui', 'public', f), /id="(theme-toggle|cityMode|streetMode)"/, `${f}: no light/dark switch`);
125
+ assert.doesNotMatch(decomment(read('modules', 'hall-ui', 'public', 'style.css')), /\.theme-toggle/, 'and no styles for one');
126
+ });
127
+
128
+ // ── the Helm tier ───────────────────────────────────────────────────────────
129
+
130
+ const STYLE = read('modules', 'hall-ui', 'public', 'style.css');
131
+ const blockAt = (marker) => {
132
+ const at = STYLE.indexOf(marker);
133
+ assert.notEqual(at, -1, `style.css holds ${marker}`);
134
+ const open = STYLE.indexOf('{', at);
135
+ let depth = 0;
136
+ for (let i = open; i < STYLE.length; i++) {
137
+ if (STYLE[i] === '{') depth++;
138
+ else if (STYLE[i] === '}' && --depth === 0) return STYLE.slice(open + 1, i);
139
+ }
140
+ throw new Error('unbalanced');
141
+ };
142
+ const decls = (body) => Object.fromEntries([...decomment(body).matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)].map((m) => [m[1], m[2].replace(/\s+/g, ' ').trim()]));
143
+ const root = decls(blockAt('\n:root {'));
144
+ const dark = decls(blockAt('html[data-theme="dark"] {'));
145
+
146
+ test('the dark tier is the Helm neutrals: night grounds, white and ice inks, ice hairlines', () => {
147
+ assert.deepEqual(
148
+ Object.fromEntries(['--bg', '--bg-card', '--bg-deep', '--bg-frame', '--ink', '--ink-soft', '--rule', '--rule-soft', '--void'].map((k) => [k, dark[k]])),
149
+ { '--bg': '#050d16', '--bg-card': '#0a1622', '--bg-deep': '#050d16', '--bg-frame': '#07111b', '--ink': '#ffffff', '--ink-soft': '#c8f1f7', '--rule': 'rgba(200, 241, 247, .4)', '--rule-soft': 'rgba(200, 241, 247, .2)', '--void': '#050d16' },
150
+ );
151
+ // the accent is still the pack's: the moon is never pinned (the Unpinned Accent Rule)
152
+ for (const name of ['--accent', '--accent-deep', '--sea', '--terracotta', '--font-display', '--font-body']) assert.equal(dark[name], undefined, `${name} is not set in the dark tier`);
153
+ // in dark a label is the name voice
154
+ assert.equal(dark['--label-face'], 'var(--font-display)');
155
+ });
156
+
157
+ test('the Helm faces are hall-tier tokens, handed to the two font tokens only under html[data-faces="helm"]', () => {
158
+ assert.match(root['--helm-display'], /^"Barlow Condensed", /);
159
+ assert.match(root['--helm-body'], /^"Barlow", /);
160
+ // the contract tier still equals the pack (tests/hall_tokens.mjs holds that); Helm does not overwrite it there
161
+ assert.equal(root['--font-display'], DEFAULTS.display);
162
+ const faces = decls(blockAt('\nhtml[data-faces="helm"] {'));
163
+ assert.deepEqual(faces, { '--font-display': 'var(--helm-display)', '--font-body': 'var(--helm-body)' });
164
+ assert.equal((decomment(STYLE).match(/Barlow/g) || []).length, 2, 'the family names live once each, in the token block');
165
+ for (const f of fs.readdirSync(HALL).filter((x) => x.endsWith('.css') && x !== 'style.css')) {
166
+ assert.doesNotMatch(read('modules', 'hall-ui', 'public', f), /Barlow/, `${f} takes the face from the tokens`);
167
+ }
168
+ });
169
+
170
+ test('the kit\'s recipes are named once: the name voice, the cut, the segment, the panel over art', () => {
171
+ assert.equal(root['--name-face'], 'var(--font-display)');
172
+ assert.equal(root['--name-case'], 'uppercase');
173
+ for (const k of ['--name-space', '--name-space-wide', '--name-space-tight']) {
174
+ const em = Number((root[k].match(/^(\.\d+)em$/) || [])[1]);
175
+ assert.ok(em >= 0.16 && em <= 0.3, `${k} is wide tracking (${root[k]})`);
176
+ }
177
+ assert.equal(root['--cut'], '12px');
178
+ assert.equal(root['--seg-skew'], '-28deg');
179
+ assert.equal(root['--night-glass'], 'color-mix(in srgb, var(--bg) 72%, transparent)');
180
+ const controls = decomment(read('modules', 'hall-ui', 'public', 'controls.css'));
181
+ const seg = controls.match(/\n\.seg \{([^}]*)\}/);
182
+ assert.ok(seg, 'the segment is a shared rule');
183
+ assert.match(seg[1], /transform: skewX\(var\(--seg-skew\)\);/);
184
+ assert.match(seg[1], /height: var\(--seg-h\);/);
185
+ assert.match(controls, /\n\.seg--done \{ background: var\(--ink-soft\);/, 'a finished step is information, so it is ice, not the accent');
186
+ assert.match(controls, /\n\.seg--now \{ background: var\(--accent\);/, 'the accent is the step you can act on');
187
+ });
@@ -379,7 +379,7 @@ test('the migrated pages run for real against the kit and emit its classes', ()
379
379
  assert.equal(typeof s.window.OTBHallRender, 'function', 'the renderer factory survived the migration');
380
380
  assert.equal(typeof s.window.OTBKit.barSeriesHtml, 'function');
381
381
  assert.match(
382
- s.window.OTBKit.barSeriesHtml([{ label: 'engineer', value: 3 }]),
382
+ s.window.OTBKit.barSeriesHtml([{ label: 'artisan', value: 3 }]),
383
383
  /class="kit-bars"/,
384
384
  );
385
385
  });
@@ -72,7 +72,7 @@ async function boot({ meStatus, settle = false }) {
72
72
  // the same reason categoryChip is: an undefined helper throws mid-render.
73
73
  roleAxisLabel: () => 'Role',
74
74
  roleLabel: (d) => (!d ? '' : d === 'unclassified' ? 'No role yet' : d === 'ui' ? 'UI' : d.charAt(0).toUpperCase() + d.slice(1)),
75
- roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
75
+ roleOptions: () => [{ value: '', label: '— any —' }, { value: 'artisan', label: 'Artisan' }, { value: 'artist', label: 'Artist' }],
76
76
  roleIsUnset: (d) => !d || d === 'unclassified',
77
77
  ROLE_UNSET: 'unclassified',
78
78
  moduleOn: () => true,
@@ -388,48 +388,48 @@ function bootCraft({ session = {} } = {}) {
388
388
  }
389
389
  const seatOf = (has, title) => ({ known: true, has_position: has, title: has ? title : null });
390
390
 
391
- test('CRAFT: with no seat, Governing is not offered anywhere', () => {
391
+ test('CRAFT: with no seat, Patron (Governing until task 1005141) is not offered anywhere', () => {
392
392
  const b = bootCraft();
393
393
  b.craft.setPositions(seatOf(false));
394
- assert.match(b.craft.switchHtml(), /data-craft-opt="governing"[^>]* hidden>/, 'the switch hides it');
395
- b.craft.choose('governing');
394
+ assert.match(b.craft.switchHtml(), /data-craft-opt="patron"[^>]* hidden>/, 'the switch hides it');
395
+ b.craft.choose('patron');
396
396
  assert.equal(b.root.dataset.craft, undefined, 'and choosing it does nothing');
397
397
  b.craft.open();
398
398
  const picker = b.body.children[0];
399
399
  assert.ok(picker, 'the picker opened');
400
- assert.doesNotMatch(picker.innerHTML, /data-craft-pick="governing"/, 'no Governing door');
401
- assert.match(picker.innerHTML, /data-doors="3"/, 'three doors, laid out as three');
400
+ assert.doesNotMatch(picker.innerHTML, /data-craft-pick="patron"/, 'no Patron door');
401
+ assert.match(picker.innerHTML, /data-doors="2"/, 'two doors, laid out as two');
402
402
  });
403
403
 
404
- test('CRAFT: a stored Governing reads as no mode for a builder known to hold no seat', () => {
404
+ test('CRAFT: a stored Patron — even under its pre-rename id, Governing — reads as no mode for a builder known to hold no seat', () => {
405
405
  const b = bootCraft({ session: { 'hall-craft': 'governing' } });
406
- assert.equal(b.craft.current(), 'governing', 'unknown seats: today\'s behaviour');
406
+ assert.equal(b.craft.current(), 'patron', 'unknown seats: today\'s behaviour, the old id read as Patron (task 1005141)');
407
407
  b.craft.setPositions(seatOf(false));
408
408
  assert.equal(b.craft.current(), null);
409
409
  assert.equal(b.session['hall-craft'], undefined, 'and the stored choice is dropped');
410
410
  });
411
411
 
412
- test('CRAFT: a seat names the Governing mode', () => {
412
+ test('CRAFT: a seat names the Patron mode', () => {
413
413
  const b = bootCraft();
414
414
  b.craft.setPositions(seatOf(true, 'Treasury Lead'));
415
415
  assert.equal(b.craft.seatName(), 'Treasury Lead');
416
- assert.match(b.craft.switchHtml(), /data-craft-opt="governing"[^>]*>[\s\S]*?Treasury Lead<\/span>/);
417
- b.craft.choose('governing');
418
- assert.equal(b.root.dataset.craft, 'governing');
416
+ assert.match(b.craft.switchHtml(), /data-craft-opt="patron"[^>]*>[\s\S]*?Treasury Lead<\/span>/);
417
+ b.craft.choose('patron');
418
+ assert.equal(b.root.dataset.craft, 'patron');
419
419
  b.craft.open();
420
420
  assert.match(b.body.children[0].innerHTML, /craft-door__name">Treasury Lead</);
421
- assert.match(b.body.children[0].innerHTML, /data-doors="4"/);
421
+ assert.match(b.body.children[0].innerHTML, /data-doors="3"/);
422
422
  });
423
423
 
424
- test('CRAFT: unknown seats are today\'s picker — four doors and a free Governing', () => {
424
+ test('CRAFT: unknown seats are today\'s picker — three doors and a free Patron', () => {
425
425
  const b = bootCraft();
426
426
  b.craft.setPositions(null);
427
427
  assert.equal(b.craft.seatName(), null);
428
428
  assert.doesNotMatch(b.craft.switchHtml(), / hidden>/);
429
- assert.match(b.craft.switchHtml(), />Governing<\/span>/);
429
+ assert.match(b.craft.switchHtml(), />Patron<\/span>/);
430
430
  b.craft.setPositions({ known: false, has_position: false });
431
- b.craft.choose('governing');
432
- assert.equal(b.root.dataset.craft, 'governing', 'an unknown answer never takes the mode away');
431
+ b.craft.choose('patron');
432
+ assert.equal(b.root.dataset.craft, 'patron', 'an unknown answer never takes the mode away');
433
433
  });
434
434
 
435
435
  // ---------------------------------------------------------------------------